Files
apes-sidebar/tests/tab-content-memo.spec.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

58 lines
2.4 KiB
TypeScript

/**
* The TabContent memo comparator: geometry/store re-renders of the Sidebar
* shell must not reconcile every mounted tab (issue #315), while the fields
* a cell's output or callbacks depend on MUST invalidate it. The pane-move
* case (review P1): moveTab reuses the SAME tab object when a tab moves
* panes, so paneId — not tab identity — is what identifies the move and
* must be compared (onOpenDiff closes over the pane id).
*/
import { describe, expect, it } from 'vitest'
import { tabContentCompare, type TabContentMemoKey } from '../src/client/tab-content-memo.ts'
import type { SidebarTab } from '../src/client/state.ts'
function makeKey(overrides: Partial<TabContentMemoKey> = {}): TabContentMemoKey {
return {
tab: { id: 'tab:1', type: 'git', title: 'Git' } as SidebarTab,
paneId: 'pane:a',
sessionId: 'session:1',
cwd: '/workspace',
visible: true,
expanded: [],
revealed: [],
localeRevision: 'en-US',
tabsVersion: 0,
effectiveTabId: undefined,
...overrides,
}
}
describe('tabContentCompare', () => {
it('skips re-render when nothing render-affecting changed (callback identity alone is ignored)', () => {
const key = makeKey()
expect(tabContentCompare(key, { ...key })).toBe(true)
})
it('invalidates on a tab moving between panes even though the tab OBJECT is reused (moveTab P1)', () => {
const sameTab = { id: 'tab:1', type: 'git', title: 'Git' } as SidebarTab
const before = makeKey({ tab: sameTab, paneId: 'pane:a' })
const after = makeKey({ tab: sameTab, paneId: 'pane:b' }) // moveTab reinserts the same object
expect(before.tab).toBe(after.tab)
expect(tabContentCompare(before, after)).toBe(false)
})
it('invalidates on locale switches (copy freshness)', () => {
expect(tabContentCompare(makeKey(), makeKey({ localeRevision: 'zh-CN' }))).toBe(false)
})
it('invalidates on tab-registry updates (descriptor refresh)', () => {
expect(tabContentCompare(makeKey(), makeKey({ tabsVersion: 1 }))).toBe(false)
})
it('invalidates on visible/expanded/session/cwd changes', () => {
expect(tabContentCompare(makeKey(), makeKey({ visible: false }))).toBe(false)
expect(tabContentCompare(makeKey(), makeKey({ expanded: ['/workspace'] }))).toBe(false)
expect(tabContentCompare(makeKey(), makeKey({ sessionId: 'session:2' }))).toBe(false)
expect(tabContentCompare(makeKey(), makeKey({ cwd: '/other' }))).toBe(false)
})
})