From 8463abc59d4e2ef141ee8969f0dbb661e62a1b14 Mon Sep 17 00:00:00 2001
From: imccyu <276526105+imccyu@users.noreply.github.com>
Date: Wed, 30 Sep 2026 20:45:40 +0800
Subject: [PATCH] fix(sidebar): preserve Session Tab lifetimes and pane-local
pages
---
.../ui-sidebar-right/src/client/service.ts | 9 +-
.../src/client/shell/SidebarRight.tsx | 10 +-
.../tests/seat.client.spec.tsx | 98 +++++++++++++++++++
3 files changed, 108 insertions(+), 9 deletions(-)
diff --git a/packages/client/ui-sidebar-right/src/client/service.ts b/packages/client/ui-sidebar-right/src/client/service.ts
index c4bfc1252b..f3d90e7584 100644
--- a/packages/client/ui-sidebar-right/src/client/service.ts
+++ b/packages/client/ui-sidebar-right/src/client/service.ts
@@ -37,7 +37,7 @@ import { sidebarTargetFromElement, type SidebarRightTarget } from './focus.ts'
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { FloatRect, PaneId, TabId, TabRecord } from '@deepseek-ai/dsh-client-ui-dockkit'
-import { activeDockPaneId, canSplit, findContentTab, dockPaneIds, findTabPane, getPane } from '@deepseek-ai/dsh-client-ui-dockkit'
+import { activeDockPaneId, canSplit, findContentTab, findPaneContentTab, dockPaneIds, findTabPane, getPane } from '@deepseek-ai/dsh-client-ui-dockkit'
import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SidebarRightNavigationParams, SidebarRightResourceParams, SidebarRightTabParamsFor } from './contract/params.ts'
@@ -440,8 +440,11 @@ export class SidebarRightController implements ISidebarRight {
}, (tabId) => { this.tabDomain.navigate(sessionId, tabId, { address, params }) }) }
const layout = surface?.layout
const replaced = placement.replaceTab === undefined ? undefined : layout?.tabs[placement.replaceTab]
- const revealed = layout === undefined || placement.revealIfOpened === false
- ? undefined : findContentTab(layout, claim.contentId, claim.kind)
+ const replacementPane = replaced === undefined || layout === undefined ? undefined : findTabPane(layout, replaced.id)
+ const revealed = layout === undefined ? undefined
+ : claim.contentId === pageAddress(claim.kind)
+ ? findPaneContentTab(layout, replacementPane?.host === 'dock' ? replacementPane.id : placement.paneId ?? activeDockPaneId(layout), claim.contentId, claim.kind)
+ : placement.revealIfOpened === false ? undefined : findContentTab(layout, claim.contentId, claim.kind)
if (replaced === undefined || replaced.id === revealed) { commit(); return }
this.removeAfterCleanup(sessionId, replaced, commit)
}
diff --git a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx
index 39fc5f6778..484dfe458b 100644
--- a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx
+++ b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx
@@ -210,8 +210,8 @@ function TabSlot({
}), [tab.id, seat, fullscreen, active, signal, tabActions, useStore, useTabNavigation, shortcuts])
const content = renderSlot(seat, {}, { entryKey: definition?.id ?? tab.kind, fallback, hookContext })
return seat === 'sidebar.right.pane.tab.title'
- ? {content}
- :
{content}
+ ? {content}
+ : {content}
}
/**
@@ -312,10 +312,8 @@ function SidebarPanel(panel: PanelProps & { width: number; panelRef: RefObject
{
+ const h = await mountSeat()
+ await act(async () => {
+ h.runtime.ctx.sidebarRightTabs.register({ id: 'test/shared', kind: 'shared', keepMounted: true, title: () => 'Shared' })
+ h.runtime.slots.register({ name: 'sidebar.right.pane.tab', key: 'test/shared' }, () => )
+ })
+ act(() => { h.controller.openTab('shared') })
+ const tab = h.controller.active()!
+ const body = document.querySelector('[data-page-input]')!
+ const cell = body.closest('[data-dockkit-host]')
+ const close = vi.fn()
+ h.runtime.ctx.effect(() => h.controller.registerCloseHandler('shared', close))
+ act(() => { h.controller.openTab('shared', { replaceTab: tab.id, revealIfOpened: false }) })
+ expect(close).not.toHaveBeenCalled()
+ let otherPane: PaneId | undefined
+ act(() => { otherPane = h.controller.split() })
+ fireEvent.click(body)
+ expect(h.controller.active()?.id).toBe(tab.id)
+ expect(otherPane).toBeDefined()
+ act(() => { h.controller.openTab('shared', { paneId: otherPane!, revealIfOpened: false, preferNewPane: true }) })
+ expect(h.controller.tabsIn(SESSION).filter(tab => tab.kind === 'shared')).toHaveLength(2)
+ const duplicate = h.controller.active()!
+ expect(duplicate.id).not.toBe(tab.id)
+ expect(document.querySelectorAll('[data-page-input]')).toHaveLength(2)
+ act(() => { h.controller.close(duplicate.id) })
+ act(() => { h.controller.float(tab.id) })
+ expect(document.querySelector('[data-page-input]')).toBe(body)
+ expect(body.closest('[data-dockkit-host]')).toBe(cell)
+ const floating = h.controller.focusedTarget(body)!
+ expect(floating.host).toBe('float')
+ act(() => { h.actions.setExpanded(SESSION, false) })
+ expect(body.closest('[hidden]')).toBeNull()
+ expect(body.closest('[aria-hidden="true"]')).toBeNull()
+ act(() => { body.focus() })
+ expect(h.controller.focusedTarget()?.tabId).toBe(tab.id)
+ act(() => { h.controller.dock(floating.paneId) })
+ expect(document.querySelector('[data-page-input]')).toBe(body)
+ expect(body.closest('[aria-hidden="true"]')).not.toBeNull()
+ act(() => { h.controller.toggleExpanded() })
+ expect(body.closest('[data-dockkit-host]')).toBe(cell)
+})
+
+it('retains a collapsed Session Tab and remounts an unretained hidden body', async () => {
+ const h = await mountSeat()
+ let latest: SidebarRightTabInfo | undefined
+ function Body(props: PropsRuntime<'sidebar.right.pane.tab'>) {
+ latest = props.useTabInfo()
+ return
+ }
+ await act(async () => {
+ h.runtime.ctx.sidebarRightTabs.register({ id: 'test/retention', kind: 'retention', title: () => 'Retention' })
+ h.runtime.slots.register({ name: 'sidebar.right.pane.tab', key: 'test/retention' }, Body)
+ })
+ act(() => { h.controller.openTab('retention') })
+ const tab = h.controller.active()!
+ const body = document.querySelector('[data-retention-parity]')!
+ const signal = latest!.tab.signal
+ act(() => { h.controller.toggleExpanded() })
+ expect(body.isConnected).toBe(true)
+ expect(latest!.sidebar.expanded).toBe(false)
+ expect(latest!.tab.visible).toBe(false)
+ act(() => { h.controller.toggleExpanded() })
+ expect(document.querySelector('[data-retention-parity]')).toBe(body)
+ act(() => { h.controller.openTab('guide') })
+ expect(body.isConnected).toBe(false)
+ expect(signal.aborted).toBe(false)
+ act(() => { h.controller.focus(tab.id) })
+ expect(document.querySelector('[data-retention-parity]')).not.toBe(body)
+ expect(latest!.tab.signal).toBe(signal)
+})
+
+it('rebinds a restored Session Tab lifetime even when close and undo share one render', async () => {
+ const h = await mountSeat()
+ let lifetime: AbortSignal | undefined
+ function Body(props: PropsRuntime<'sidebar.right.pane.tab'>) {
+ lifetime = props.useTabInfo().tab.signal
+ const [count, setCount] = useState(0)
+ return
+ }
+ await act(async () => {
+ h.runtime.ctx.sidebarRightTabs.register({ id: 'test/restored', kind: 'restored', keepMounted: true, title: () => 'Restored' })
+ h.runtime.slots.register({ name: 'sidebar.right.pane.tab', key: 'test/restored' }, Body)
+ })
+ act(() => { h.controller.openTab('restored') })
+ const tab = h.controller.active()!
+ const original = document.querySelector('[data-restored-tab]')!
+ const before = lifetime!
+ fireEvent.click(original)
+ act(() => { h.controller.close(tab.id); h.controller._undo() })
+ expect(h.controller.active()?.id).toBe(tab.id)
+ expect(before.aborted).toBe(true)
+ expect(lifetime).not.toBe(before)
+ expect(document.querySelector('[data-restored-tab]')).not.toBe(original)
+ expect(original.isConnected).toBe(false)
+ expect(document.querySelector('[data-restored-tab]')?.textContent).toBe('0')
+})
+
function element(container: HTMLElement, selector: string): HTMLElement {
const node = container.querySelector(selector)
if (node === null) throw new Error(`expected ${selector}`)
@@ -803,6 +900,7 @@ describe('slot-owned useTabInfo', () => {
h.controller.split()
})
const anchor = element(h.view.container, '[data-dockkit-split-button]').parentElement!
+ fireEvent.keyDown(anchor, { key: 'Tab' })
fireEvent.focus(anchor)
expect(document.querySelector('[role="tooltip"]')?.getAttribute('aria-label')).toBe('Two panes is the limit ⌘ \\')
act(() => { h.catalog.set([{ ...split, binding: { code: 'KeyG', modifiers: ['control'] },