Files
apes-sidebar/src/client/tab-content-memo.ts
T
HuanLinOTO e7c4578791 feat: inline pinned tabs in TabBar (replaces PinnedRail)
M4 rewrite per user feedback: pinned terminals should display in the
Tabs bar itself, not a separate rail. Clicking activates in-place
(cross-session terminal rendering), no session jump.

Changes:
- Delete PinnedRail.tsx + tests/pinned-rail.spec.tsx
- pinned.ts: add createPinnedVirtualTab (virtual id = pinned:<sess>:<id>,
  meta.__pinnedHome = { sessionId, cwd, tabId }), injectPinnedIntoTree
  (appends virtual tabs to first leaf, overrides active), isPinnedVirtualId,
  parsePinnedVirtualId, isPinnedVirtualTab, getPinnedHomeScope
- tab-content-memo.ts: add effectiveTabId to memo key — TabContent passes
  the original tab id (not the virtual id) to the descriptor component so
  TerminalView's WS URL resolves to the home PTY
- Sidebar.tsx: augmented tree (injectPinnedIntoTree), activePinnedTabId
  state, wrappedActions (intercept virtual ids for activate/close/unpin/
  drag/float), renderTab passes home scope + effectiveTabId for pinned tabs,
  pinnedRevision replaces railRevision
- TabBar.tsx: pinned virtual tabs non-draggable, pin icon prefix, stripped
  context menu (Unpin/Close only, no float/close-others/left/right)
- sidebar.module.css: remove rail styles, add .pinnedTab (italic + tertiary)
- Docs: README/README_EN/AGENTS.md/design doc updated for inline approach

157/157 client-side tests pass; typecheck + build clean
2026-08-26 21:40:32 +08:00

56 lines
2.4 KiB
TypeScript

/**
* The memo comparator for one tab's content cell (TabContent in Sidebar.tsx).
*
* Kept as a pure module so the "what re-renders a tab cell" contract is unit
* testable: geometry/store re-renders of the Sidebar shell must NOT reconcile
* every mounted tab (xterm/CodeMirror/FileTree subtrees — issue #315), while
* locale switches (localeRevision), tab-registry updates (tabsVersion), and
* a tab MOVING BETWEEN PANES (paneId — onOpenDiff closes over the pane id,
* and moveTab reuses the SAME tab object, so only the pane id identifies the
* move) must re-render the cell.
*/
import type { SidebarTab } from './state.ts'
/** The fields whose change invalidates a tab cell's memo. Anything a cell's
* render (or its callbacks) depends on must be listed here. */
export interface TabContentMemoKey {
tab: SidebarTab
/** The pane this tab currently lives in. onOpenDiff closes over it, and
* moveTab reuses the same tab object when the tab moves panes — without
* comparing paneId the cell would keep the source pane's callback. */
paneId: string
sessionId: string
cwd: string | undefined
visible: boolean
expanded: string[]
/** Files highlighted in the file tree (the "Show in folder" reveal). */
revealed: string[]
localeRevision: string
tabsVersion: number
/** When the tab is a pinned virtual tab (injected from another session),
* this overrides `tab.id` passed to the tab descriptor's component so
* TerminalView connects to the home session's PTY by the original id.
* Undefined for regular tabs (no override). */
effectiveTabId: string | undefined
}
/** True when the cell may skip a re-render (all render-affecting fields
* unchanged). Callback/context identities are deliberately ignored: their
* captured dependencies are stable or covered by the compared fields
* (onReferenceFile → sessionId/cwd, onSubagentJump/onToggleDir → stable
* refs/closures, onOpenDiff → paneId). */
export function tabContentCompare(prev: TabContentMemoKey, next: TabContentMemoKey): boolean {
return (
prev.tab === next.tab &&
prev.paneId === next.paneId &&
prev.sessionId === next.sessionId &&
prev.cwd === next.cwd &&
prev.visible === next.visible &&
prev.expanded === next.expanded &&
prev.revealed === next.revealed &&
prev.localeRevision === next.localeRevision &&
prev.tabsVersion === next.tabsVersion &&
prev.effectiveTabId === next.effectiveTabId
)
}