From 3f044f988748e5b52a0f4ba2576bb8cebe122d8f Mon Sep 17 00:00:00 2001 From: KoNit-K <124019182+KoNit-K@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:24:01 +0800 Subject: [PATCH 1/2] fix(desktop): hide fixed titlebar clusters on contributed full pages Plugin ROUTES_AREA pages own their titlebar chrome; the app clusters were sitting on top of it because only overlays hid them. Co-authored-by: Cursor --- .../src/app/routes.titlebar-clusters.test.ts | 21 ++++++ apps/desktop/src/app/routes.ts | 9 +++ .../src/app/shell/titlebar-controls.test.tsx | 70 +++++++++++++++++++ .../src/app/shell/titlebar-controls.tsx | 11 +-- 4 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/app/routes.titlebar-clusters.test.ts create mode 100644 apps/desktop/src/app/shell/titlebar-controls.test.tsx diff --git a/apps/desktop/src/app/routes.titlebar-clusters.test.ts b/apps/desktop/src/app/routes.titlebar-clusters.test.ts new file mode 100644 index 0000000000..b023d14448 --- /dev/null +++ b/apps/desktop/src/app/routes.titlebar-clusters.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' + +import { hidesFixedTitlebarClusters, isOverlayView } from './routes' + +describe('hidesFixedTitlebarClusters', () => { + it('hides clusters on contributed full pages and overlays', () => { + expect(hidesFixedTitlebarClusters('extension')).toBe(true) + expect(hidesFixedTitlebarClusters('settings')).toBe(true) + }) + + it('keeps clusters on chat and first-party workspace pages', () => { + expect(hidesFixedTitlebarClusters('chat')).toBe(false) + expect(hidesFixedTitlebarClusters('skills')).toBe(false) + expect(hidesFixedTitlebarClusters('messaging')).toBe(false) + expect(hidesFixedTitlebarClusters('artifacts')).toBe(false) + }) + + it('does not treat extension as an overlay', () => { + expect(isOverlayView('extension')).toBe(false) + }) +}) diff --git a/apps/desktop/src/app/routes.ts b/apps/desktop/src/app/routes.ts index 03a5117951..8c54a6ac35 100644 --- a/apps/desktop/src/app/routes.ts +++ b/apps/desktop/src/app/routes.ts @@ -141,6 +141,15 @@ export function isOverlayView(view: AppView): boolean { return OVERLAY_VIEWS.has(view) } +/** True when TitlebarControls must unmount the app's fixed tool clusters. + * Overlays already own the window; contributed full pages (`extension`) + * bring their own titlebar chrome (`titleBar.center` etc.) and the clusters + * would otherwise sit on top of it. First-party workspace pages + * (skills/messaging/artifacts) keep the clusters. */ +export function hidesFixedTitlebarClusters(view: AppView): boolean { + return isOverlayView(view) || view === 'extension' +} + /** The pathname of a router target. Every classifier below reasons about a * PATH, but callers navigate to full targets (`/skills?tab=mcp`), and an * unstripped query reaches the session-id parser — `/skills?tab=mcp` reads as diff --git a/apps/desktop/src/app/shell/titlebar-controls.test.tsx b/apps/desktop/src/app/shell/titlebar-controls.test.tsx new file mode 100644 index 0000000000..56f3a53c11 --- /dev/null +++ b/apps/desktop/src/app/shell/titlebar-controls.test.tsx @@ -0,0 +1,70 @@ +// @vitest-environment jsdom +import { cleanup, render, screen } from '@testing-library/react' +import { MemoryRouter } from 'react-router' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { registry } from '@/contrib/registry' +import { I18nProvider } from '@/i18n' + +import { ROUTES_AREA } from '../routes' + +import { TitlebarControls } from './titlebar-controls' + +function renderControls(pathname: string) { + return render( + + + {}} /> + + + ) +} + +const windowControls = () => screen.queryByLabelText('Window controls') +const appControls = () => screen.queryByLabelText('App controls') + +describe('TitlebarControls fixed clusters', () => { + let disposeRoute: () => void + + beforeEach(() => { + disposeRoute = registry.register({ + area: ROUTES_AREA, + data: { path: '/kanban' }, + id: 'test-kanban-route', + render: () => null + }) + }) + + afterEach(() => { + disposeRoute() + cleanup() + }) + + it('hides the app clusters on a contributed full-page route', () => { + renderControls('/kanban') + + expect(windowControls()).toBeNull() + expect(appControls()).toBeNull() + }) + + it('keeps the app clusters on chat', () => { + renderControls('/') + + expect(windowControls()).not.toBeNull() + expect(appControls()).not.toBeNull() + }) + + it('hides the app clusters on an overlay', () => { + renderControls('/settings') + + expect(windowControls()).toBeNull() + expect(appControls()).toBeNull() + }) + + it('keeps the app clusters on a first-party workspace page', () => { + renderControls('/skills') + + expect(windowControls()).not.toBeNull() + expect(appControls()).not.toBeNull() + }) +}) diff --git a/apps/desktop/src/app/shell/titlebar-controls.tsx b/apps/desktop/src/app/shell/titlebar-controls.tsx index d54d839361..c3a2f507bf 100644 --- a/apps/desktop/src/app/shell/titlebar-controls.tsx +++ b/apps/desktop/src/app/shell/titlebar-controls.tsx @@ -25,7 +25,7 @@ import { } from '@/store/layout' import { $unreadSessionCount } from '@/store/session-dot-state' -import { appViewForPath, isOverlayView } from '../routes' +import { appViewForPath, hidesFixedTitlebarClusters } from '../routes' import { TITLEBAR_ICON_BADGE_SCALE, @@ -235,11 +235,12 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: } ] - // While a full-screen overlay (settings, command center, …) is open it should + // While a full-screen overlay (settings, command center, …) or a contributed + // full-context plugin page (`extension`, e.g. /kanban) is open it should // visually own the window. These control clusters are `fixed` at a higher - // z-index than the overlay card, so they'd otherwise bleed over it — hide them - // and let the overlay's own chrome (close button, drag region) take over. - if (isOverlayView(appViewForPath(location.pathname))) { + // z-index than the overlay card / plugin titlebar chrome, so they'd otherwise + // bleed over it — hide them and let that surface's own chrome take over. + if (hidesFixedTitlebarClusters(appViewForPath(location.pathname))) { return null } From 564895fea8167eb23a00e25134df8c3720537eeb Mon Sep 17 00:00:00 2001 From: KoNit-K <124019182+KoNit-K@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:27:06 +0800 Subject: [PATCH 2/2] fix(desktop): keep plugin titlebar slots on extension pages Hiding the app clusters on contributed full pages must not unmount titleBar.left/center/right; overlays still return null. Co-authored-by: Cursor --- apps/desktop/src/app/routes.ts | 10 ++--- .../src/app/shell/titlebar-controls.test.tsx | 38 ++++++++++++---- .../src/app/shell/titlebar-controls.tsx | 45 ++++++++++++------- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src/app/routes.ts b/apps/desktop/src/app/routes.ts index 8c54a6ac35..7940f6e62f 100644 --- a/apps/desktop/src/app/routes.ts +++ b/apps/desktop/src/app/routes.ts @@ -141,11 +141,11 @@ export function isOverlayView(view: AppView): boolean { return OVERLAY_VIEWS.has(view) } -/** True when TitlebarControls must unmount the app's fixed tool clusters. - * Overlays already own the window; contributed full pages (`extension`) - * bring their own titlebar chrome (`titleBar.center` etc.) and the clusters - * would otherwise sit on top of it. First-party workspace pages - * (skills/messaging/artifacts) keep the clusters. */ +/** True when TitlebarControls must hide the app's fixed tool clusters. + * Overlays already own the window (clusters AND titleBar slots unmount). + * Contributed full pages (`extension`) hide the app clusters but keep the + * titleBar slots so plugin chrome can own that space. First-party workspace + * pages (skills/messaging/artifacts) keep the clusters. */ export function hidesFixedTitlebarClusters(view: AppView): boolean { return isOverlayView(view) || view === 'extension' } diff --git a/apps/desktop/src/app/shell/titlebar-controls.test.tsx b/apps/desktop/src/app/shell/titlebar-controls.test.tsx index 56f3a53c11..769a95d317 100644 --- a/apps/desktop/src/app/shell/titlebar-controls.test.tsx +++ b/apps/desktop/src/app/shell/titlebar-controls.test.tsx @@ -22,21 +22,29 @@ function renderControls(pathname: string) { const windowControls = () => screen.queryByLabelText('Window controls') const appControls = () => screen.queryByLabelText('App controls') +const pluginChrome = () => screen.queryByText('plugin-chrome') describe('TitlebarControls fixed clusters', () => { - let disposeRoute: () => void + let dispose: () => void beforeEach(() => { - disposeRoute = registry.register({ - area: ROUTES_AREA, - data: { path: '/kanban' }, - id: 'test-kanban-route', - render: () => null - }) + dispose = registry.registerMany([ + { + area: ROUTES_AREA, + data: { path: '/kanban' }, + id: 'test-kanban-route', + render: () => null + }, + { + area: 'titleBar.center', + id: 'test-plugin-chrome', + render: () => plugin-chrome + } + ]) }) afterEach(() => { - disposeRoute() + dispose() cleanup() }) @@ -47,6 +55,14 @@ describe('TitlebarControls fixed clusters', () => { expect(appControls()).toBeNull() }) + it('keeps plugin titlebar contributions on a contributed full-page route', () => { + renderControls('/kanban') + + expect(pluginChrome()).not.toBeNull() + expect(windowControls()).toBeNull() + expect(appControls()).toBeNull() + }) + it('keeps the app clusters on chat', () => { renderControls('/') @@ -61,6 +77,12 @@ describe('TitlebarControls fixed clusters', () => { expect(appControls()).toBeNull() }) + it('hides plugin titlebar contributions on an overlay', () => { + renderControls('/settings') + + expect(pluginChrome()).toBeNull() + }) + it('keeps the app clusters on a first-party workspace page', () => { renderControls('/skills') diff --git a/apps/desktop/src/app/shell/titlebar-controls.tsx b/apps/desktop/src/app/shell/titlebar-controls.tsx index c3a2f507bf..5f5fd1718f 100644 --- a/apps/desktop/src/app/shell/titlebar-controls.tsx +++ b/apps/desktop/src/app/shell/titlebar-controls.tsx @@ -25,7 +25,7 @@ import { } from '@/store/layout' import { $unreadSessionCount } from '@/store/session-dot-state' -import { appViewForPath, hidesFixedTitlebarClusters } from '../routes' +import { appViewForPath, hidesFixedTitlebarClusters, isOverlayView } from '../routes' import { TITLEBAR_ICON_BADGE_SCALE, @@ -235,32 +235,43 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: } ] - // While a full-screen overlay (settings, command center, …) or a contributed - // full-context plugin page (`extension`, e.g. /kanban) is open it should - // visually own the window. These control clusters are `fixed` at a higher - // z-index than the overlay card / plugin titlebar chrome, so they'd otherwise - // bleed over it — hide them and let that surface's own chrome take over. - if (hidesFixedTitlebarClusters(appViewForPath(location.pathname))) { + const view = appViewForPath(location.pathname) + + // Overlays own the window. These clusters are `fixed` at a higher z-index + // than the overlay card, so they'd otherwise bleed over it — hide them (and + // the nested titleBar slots) and let the overlay's own chrome take over. + if (isOverlayView(view)) { return null } + const titlebarSlots = ( + <> + + + + + ) + const leftClusterClass = cn( + titlebarToolClusterClass, + 'left-(--titlebar-controls-left) top-(--titlebar-controls-top) translate-y-(--titlebar-controls-y-nudge)' + ) + + // Contributed full-context plugin pages (`extension`) own the titlebar band. + // Hide the app's tool clusters but keep plugin slots in the same fixed + // position so `titleBar.center` (e.g. kanban's board switcher) stays mounted. + if (hidesFixedTitlebarClusters(view)) { + return
{titlebarSlots}
+ } + const visibleLeftTools = [sidebarTool, ...systemTools, ...leftTools, ...tools].filter(tool => !tool.hidden) return ( <> -
+
{visibleLeftTools.map(tool => ( ))} - - - + {titlebarSlots}