From e20b95024bdfd03781d49f174be38e079bd60707 Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Mon, 24 Aug 2026 00:01:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E6=B5=AE=E7=AA=97=E5=86=85=20tab=20?= =?UTF-8?q?=E7=9A=84=20visible=20=E6=81=92=E4=B8=BA=20true=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=B7=9F=E9=9A=8F=E9=9D=A2=E6=9D=BF=E5=BC=80?= =?UTF-8?q?=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderTab 的可见性原为 panelOpen && active——浮窗内容随侧边栏收起被判定为 不可见,遵守 visible 契约(不可见时暂停/空渲染)的消费插件 tab 会在浮窗里 白屏。改为 placement 三态(top/bottom/float),float 恒 true(兑现 AGENTS §7.5 契约);回归测试断言面板收起后浮窗内的注册插件 tab 仍收到 visible=true。 --- src/client/Sidebar.tsx | 20 ++++++++++++++++---- tests/free-window.spec.tsx | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/client/Sidebar.tsx b/src/client/Sidebar.tsx index 5f5b036..258d582 100644 --- a/src/client/Sidebar.tsx +++ b/src/client/Sidebar.tsx @@ -1191,7 +1191,13 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { * polling while the page is not actually visible). The pane id travels * with the tab so diff tabs can split below their source pane. */ - const renderTab = (tab: SidebarTab, active: boolean, paneId: string, bottom = false) => ( + // `placement` decides the visibility contract handed to the tab component: + // pane tabs are visible while their panel is open and they are active, but + // a free window is its own surface — its tab stays visible no matter what + // the panels do (the AGENTS §7.5 contract; plugin components honor + // `visible` to pause work, so tying floats to panelOpen would blank them + // the moment the sidebar collapses). + const renderTab = (tab: SidebarTab, active: boolean, paneId: string, placement: 'top' | 'bottom' | 'float' = 'top') => ( { subagentJumpRef.current = childSessionId }} onOpenDiff={(diffTab) => { store.reduce(s => openDiffTab(s, paneId, diffTab)) }} localeRevision={localeRevision} @@ -1469,7 +1481,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { newTabOptions={buildNewTabOptions(state, ctx, { sessionId, cwd })} actions={actions} onNewTab={onNewTab} - renderTab={(tab, active, paneId) => renderTab(tab, active, paneId, true)} + renderTab={(tab, active, paneId) => renderTab(tab, active, paneId, 'bottom')} getTabIcon={tabIconOf} getTabBadge={tabBadgeOf} /> @@ -1488,7 +1500,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { renderTab(tab, active, paneId, 'float')} getTabIcon={tabIconOf} onRaise={() => { store.reduce(s => raiseFloat(s, float.id)) }} onMove={(x, y) => { store.reduce(s => moveFloat(s, float.id, x, y)) }} diff --git a/tests/free-window.spec.tsx b/tests/free-window.spec.tsx index 694c807..d3fd343 100644 --- a/tests/free-window.spec.tsx +++ b/tests/free-window.spec.tsx @@ -23,7 +23,7 @@ import { act } from 'react-dom/test-utils' ;(globalThis as Record).IS_REACT_ACT_ENVIRONMENT = true import { Sidebar } from '../src/client/Sidebar.tsx' -import { createSidebarStore, type SidebarStore, floatTab as floatTabReducer } from '../src/client/state.ts' +import { createSidebarStore, togglePanel, type SidebarStore, floatTab as floatTabReducer } from '../src/client/state.ts' import { createBetterSidebarService, type BetterSidebarService } from '../src/client/service.ts' import { serializeDrag } from '../src/client/TabBar.tsx' @@ -73,7 +73,15 @@ function mountSidebar(): Mounted { document.body.append(container) const store = createSidebarStore() const service = createBetterSidebarService(store) - service.registerTab({ id: 'notes', title: 'Notes', single: true, component: () => createElement('div', null, 'notes body') }) + service.registerTab({ + id: 'notes', + title: 'Notes', + single: true, + // Pins the visibility contract into the DOM: plugin components honor + // `visible` to pause work, so the float tests can assert what a + // registering plugin would actually receive. + component: ({ visible }) => createElement('div', { 'data-visible': String(visible) }, 'notes body'), + }) store.setPrefs({ ...store.getPrefs(), openByDefault: true }) store.setSession('s1') const localeSnapshot = { active: 'en' } @@ -294,6 +302,26 @@ describe('free windows: the window', () => { } }) + it('a floated plugin tab stays visible while the panel collapses (placement contract)', () => { + fakeConversationColumn() + const { container, store, service, unmount } = mountSidebar() + try { + act(() => { service.openTab({ type: 'notes', title: 'Notes' }) }) + act(() => { store.reduce(s => floatTabReducer(s, 'notes', 512, 384)) }) + const win = container.querySelector('[data-dsh-float-window]')! + expect(win.querySelector('[data-visible]')!.dataset.visible).toBe('true') + // Collapse the sidebar: the pane's seeded home tab goes invisible + // (panel closed), but the float is its own surface — a registering + // plugin's component must keep receiving visible=true (AGENTS §7.5). + act(() => { store.reduce(togglePanel) }) + expect(win.querySelector('[data-visible]')!.dataset.visible).toBe('true') + // (No pane-side control: the pane's remaining tab is the builtin editor + // home tab, which does not surface `visible` into the DOM.) + } finally { + unmount() + } + }) + it('releasing the header over a pane docks the tab back (live pane highlight)', async () => { fakeConversationColumn() const { container, store, service, unmount } = mountSidebar()