fix(ui): 浮窗内 tab 的 visible 恒为 true,不再跟随面板开合
renderTab 的可见性原为 panelOpen && active——浮窗内容随侧边栏收起被判定为 不可见,遵守 visible 契约(不可见时暂停/空渲染)的消费插件 tab 会在浮窗里 白屏。改为 placement 三态(top/bottom/float),float 恒 true(兑现 AGENTS §7.5 契约);回归测试断言面板收起后浮窗内的注册插件 tab 仍收到 visible=true。
This commit is contained in:
+16
-4
@@ -1191,7 +1191,13 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
|||||||
* polling while the page is not actually visible). The pane id travels
|
* polling while the page is not actually visible). The pane id travels
|
||||||
* with the tab so diff tabs can split below their source pane.
|
* 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') => (
|
||||||
<TabContent
|
<TabContent
|
||||||
tab={tab}
|
tab={tab}
|
||||||
paneId={paneId}
|
paneId={paneId}
|
||||||
@@ -1202,7 +1208,13 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
|||||||
onReferenceFile={referenceInChat}
|
onReferenceFile={referenceInChat}
|
||||||
ctx={ctx}
|
ctx={ctx}
|
||||||
store={store}
|
store={store}
|
||||||
visible={bottom ? state.bottomOpen && active : state.panelOpen && active}
|
visible={
|
||||||
|
placement === 'float'
|
||||||
|
? true
|
||||||
|
: placement === 'bottom'
|
||||||
|
? state.bottomOpen && active
|
||||||
|
: state.panelOpen && active
|
||||||
|
}
|
||||||
onSubagentJump={(childSessionId) => { subagentJumpRef.current = childSessionId }}
|
onSubagentJump={(childSessionId) => { subagentJumpRef.current = childSessionId }}
|
||||||
onOpenDiff={(diffTab) => { store.reduce(s => openDiffTab(s, paneId, diffTab)) }}
|
onOpenDiff={(diffTab) => { store.reduce(s => openDiffTab(s, paneId, diffTab)) }}
|
||||||
localeRevision={localeRevision}
|
localeRevision={localeRevision}
|
||||||
@@ -1469,7 +1481,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
|||||||
newTabOptions={buildNewTabOptions(state, ctx, { sessionId, cwd })}
|
newTabOptions={buildNewTabOptions(state, ctx, { sessionId, cwd })}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
onNewTab={onNewTab}
|
onNewTab={onNewTab}
|
||||||
renderTab={(tab, active, paneId) => renderTab(tab, active, paneId, true)}
|
renderTab={(tab, active, paneId) => renderTab(tab, active, paneId, 'bottom')}
|
||||||
getTabIcon={tabIconOf}
|
getTabIcon={tabIconOf}
|
||||||
getTabBadge={tabBadgeOf}
|
getTabBadge={tabBadgeOf}
|
||||||
/>
|
/>
|
||||||
@@ -1488,7 +1500,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
|||||||
<FreeWindow
|
<FreeWindow
|
||||||
key={float.id}
|
key={float.id}
|
||||||
float={float}
|
float={float}
|
||||||
renderTab={renderTab}
|
renderTab={(tab, active, paneId) => renderTab(tab, active, paneId, 'float')}
|
||||||
getTabIcon={tabIconOf}
|
getTabIcon={tabIconOf}
|
||||||
onRaise={() => { store.reduce(s => raiseFloat(s, float.id)) }}
|
onRaise={() => { store.reduce(s => raiseFloat(s, float.id)) }}
|
||||||
onMove={(x, y) => { store.reduce(s => moveFloat(s, float.id, x, y)) }}
|
onMove={(x, y) => { store.reduce(s => moveFloat(s, float.id, x, y)) }}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { act } from 'react-dom/test-utils'
|
|||||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||||
|
|
||||||
import { Sidebar } from '../src/client/Sidebar.tsx'
|
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 { createBetterSidebarService, type BetterSidebarService } from '../src/client/service.ts'
|
||||||
import { serializeDrag } from '../src/client/TabBar.tsx'
|
import { serializeDrag } from '../src/client/TabBar.tsx'
|
||||||
|
|
||||||
@@ -73,7 +73,15 @@ function mountSidebar(): Mounted {
|
|||||||
document.body.append(container)
|
document.body.append(container)
|
||||||
const store = createSidebarStore()
|
const store = createSidebarStore()
|
||||||
const service = createBetterSidebarService(store)
|
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.setPrefs({ ...store.getPrefs(), openByDefault: true })
|
||||||
store.setSession('s1')
|
store.setSession('s1')
|
||||||
const localeSnapshot = { active: 'en' }
|
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<HTMLElement>('[data-dsh-float-window]')!
|
||||||
|
expect(win.querySelector<HTMLElement>('[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<HTMLElement>('[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 () => {
|
it('releasing the header over a pane docks the tab back (live pane highlight)', async () => {
|
||||||
fakeConversationColumn()
|
fakeConversationColumn()
|
||||||
const { container, store, service, unmount } = mountSidebar()
|
const { container, store, service, unmount } = mountSidebar()
|
||||||
|
|||||||
Reference in New Issue
Block a user