fix(desktop): keep the window gutter on the panel paper

With the workspace panel open, the titlebar's right section was paper right
up to a 16px notch of sidebar grey welded to the window's top-right corner.

The paper was painted by the window header, but the drag gutter — and, on
Windows, the window controls — are its transparent siblings inside the same
frame, so whatever their parent showed came through. Closed, that is
invisible: the whole strip is the sidebar's ground and the gutter just
continues it. Open, it is a strip of trough next to a white panel.

The frame now owns the ground for everything above the panel, so the header,
the gutter and the native controls share one surface.
This commit is contained in:
程序员阿江(Relakkes)
2026-09-14 11:56:37 +08:00
parent dac0bf5477
commit f0a016d3cf
2 changed files with 55 additions and 2 deletions
@@ -1545,6 +1545,51 @@ describe('TabBar', () => {
expect(tab).not.toHaveAttribute('data-desktop-drag-region')
})
it('keeps the window gutter on the panel paper while the workspace is open', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
const { useWorkspaceStore } = await import('../../stores/workspaceStore')
// Imported here, not at the top of the file: `vi.resetModules()` hands the
// strip a fresh provider module, and a statically imported one would be a
// second React context that the strip can never see.
const { WorkspaceHeaderProvider } = await import('./WorkspaceHeaderContext')
const sessionId = 'gutter-session'
useTabStore.setState({
tabs: [{ sessionId, title: 'Gutter session', type: 'session', status: 'idle' }],
activeTabId: sessionId,
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
// The window owns the header slot; without it the strip falls back to
// the dock and there is no header frame to paint.
render(<WorkspaceHeaderProvider><TabBar /></WorkspaceHeaderProvider>)
})
const frame = screen.getByTestId('workspace-header-frame')
const gutter = screen.getByTestId('tab-bar-drag-gutter')
// Closed, the trough runs to the window edge and the gutter disappears
// into it. Painting the frame unconditionally would paste a white block
// over the end of the strip instead.
expect(frame).not.toHaveClass('bg-[var(--color-surface)]')
// Open, everything above the panel is one ground. The gutter is a
// transparent sibling of the header, so whichever element paints paper has
// to contain it — otherwise the trough shows through the strip and the
// window's top-right corner carries a grey notch beside the panel.
await act(async () => {
useWorkspaceStore.getState().toggleWorkspace(sessionId)
})
expect(frame).toHaveClass('bg-[var(--color-surface)]')
expect(frame).toContainElement(gutter)
})
it('lifts the active tab onto the paper ground without turning it into a pill', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
+10 -2
View File
@@ -621,15 +621,23 @@ export function TabBar() {
against paper, it all but disappears on the trough (1.12:1 on 素白),
which left the toolbar looking welded to the last tab.
*/}
/*
The frame owns the paper, not the header inside it. The drag gutter —
and, on Windows, the window controls — are the header's siblings in
here, and they are transparent: while the workspace is closed that is
invisible, because the whole strip is the sidebar's ground. Next to an
open panel it showed as a 16px strip of trough welded to the window's
top-right corner. One ground for everything above the panel.
*/
<div
data-testid="workspace-header-frame"
style={hasWorkspaceHeader ? { width: workspaceHeader.width, maxWidth: '100%' } : undefined}
className="flex min-w-0 shrink-0 items-stretch"
className={`flex min-w-0 shrink-0 items-stretch ${hasWorkspaceHeader ? 'bg-[var(--color-surface)]' : ''}`}
>
<div
data-testid="workspace-window-header"
className={hasWorkspaceHeader
? 'flex min-w-0 flex-1 items-center gap-1 bg-[var(--color-surface)] pr-2'
? 'flex min-w-0 flex-1 items-center gap-1 pr-2'
: 'relative flex shrink-0 items-center gap-1 px-2 before:absolute before:left-0 before:top-1/2 before:h-4 before:w-px before:-translate-y-1/2 before:bg-[var(--color-tab-separator)]'}
>
{hasWorkspaceHeader ? <div ref={workspaceHeader.ref} data-testid="workspace-header-slot" className="tab-bar-interactive flex h-[52px] min-w-0 flex-1" /> : null}