fix(desktop): front the workspace pane from the router location

Capabilities, Messaging and Artifacts render inside the `workspace` pane,
so navigating to one has to bring that pane to the front of its group.
Nothing did. With the main zone parked on a session tile, the route and
the page content changed behind the tile and the click looked dead until
the app restarted. Session switches already front the pane in
`store/session-states.ts`; pages had no equivalent.

Front it from the router location, in the effect that already mirrors
`$workspaceIsPage`. One place decides, so every entry point inherits it:
sidebar, keybinds, palette, Command Center, contributed statusbar and
titlebar `to` targets, back/forward, and cold-start restore — which no
longer needs its own call.

`navigateToWorkspacePage` stays for the one case the location can't see:
hitting Capabilities while already on `/skills` leaves the location
untouched, so no effect fires and only an imperative reveal brings the
page back.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 12:54:45 -05:00
parent 8323bf3d71
commit f6ea8b462e
3 changed files with 51 additions and 25 deletions

View file

@ -17,7 +17,7 @@ import { openUpdatesWindow, startUpdatePoller, stopUpdatePoller } from '@/store/
import { isSecondaryWindow } from '@/store/windows'
import { requestComposerFocus, requestComposerInsert } from '../../chat/composer/focus'
import { appViewForPath, isOverlayView, navigateToWorkspacePage, NEW_CHAT_ROUTE, sessionRoute } from '../../routes'
import { appViewForPath, isOverlayView, NEW_CHAT_ROUTE, sessionRoute } from '../../routes'
interface DesktopIntegrationsParams {
chatOpen: boolean
@ -99,7 +99,7 @@ export function useDesktopIntegrations({
const route = getRememberedRoute()
if (route && route !== NEW_CHAT_ROUTE && !isOverlayView(appViewForPath(route))) {
navigateToWorkspacePage(navigate, route, { replace: true })
navigate(route, { replace: true })
return
}

View file

@ -82,7 +82,7 @@ import {
routeSessionId,
sessionRoute,
SETTINGS_ROUTE,
syncWorkspaceIsPage
syncWorkspaceRoute
} from '../routes'
import { SessionPickerOverlay } from '../session-picker-overlay'
import { SessionSwitcher } from '../session-switcher'
@ -187,11 +187,12 @@ export function ContribWiring({ children }: { children: ReactNode }) {
routedSessionIdRef.current = null
}, [])
// Mirror "the workspace is showing a full page" into its atom — the
// workspace pane contribution re-registers headerVeto from it, so the main
// zone's tab bar stands down on pages (and returns with the chat).
// Point the workspace at the route: the pane contribution re-registers
// headerVeto from $workspaceIsPage (so the main zone's tab bar stands down
// on pages), and a page route fronts the pane so it can't stay stuck behind
// a focused session tile.
useEffect(() => {
syncWorkspaceIsPage(location.pathname)
syncWorkspaceRoute(location.pathname)
}, [location.pathname])
const {

View file

@ -199,6 +199,15 @@ export function appViewForPath(pathname: string): AppView {
return APP_VIEW_BY_PATH.get(path) ?? 'chat'
}
/** Does `to` land on a full page rendered INSIDE the workspace pane
* (skills/messaging/artifacts/contributed routes)? Overlays don't count
* they float over whatever the workspace is already showing. */
function isWorkspacePageRoute(to: string): boolean {
const view = appViewForPath(to)
return view !== 'chat' && !isOverlayView(view)
}
/** True while the workspace pane shows a FULL PAGE (skills/messaging/
* artifacts/plugin routes) instead of the chat. Published by the wiring
* (which owns the router location); the workspace pane contribution mirrors
@ -206,33 +215,49 @@ export function appViewForPath(pathname: string): AppView {
* (settings/) don't count the chat stays beneath them. */
export const $workspaceIsPage = atom(false)
export function syncWorkspaceIsPage(pathname: string): void {
const view = appViewForPath(pathname)
const isPage = view !== 'chat' && !isOverlayView(view)
function revealWorkspacePane(): void {
noteActiveTreeGroup(null)
revealTreePane('workspace')
}
/**
* Point the workspace at `pathname`: mirror "showing a full page" into
* `$workspaceIsPage`, and FRONT the pane when it is one.
*
* A page renders inside `workspace`, so a main zone parked on a session tile
* keeps the tile on screen while the route and the page content change behind
* it the navigation looks dead (#72602). Session switches already front the
* pane in `store/session-states.ts`; pages had no equivalent.
*
* The router location drives this, so every entry point gets it without opting
* in: sidebar, keybinds, command palette, Command Center, contributed
* statusbar/titlebar `to` targets, back/forward, and cold-start restore.
*/
export function syncWorkspaceRoute(pathname: string): void {
const isPage = isWorkspacePageRoute(pathname)
if (isPage !== $workspaceIsPage.get()) {
$workspaceIsPage.set(isPage)
}
if (isPage) {
revealWorkspacePane()
}
}
/**
* Navigate to `path`, and if it lands on a full page rendered inside the
* `workspace` pane (skills/messaging/artifacts/contributed routes) also
* front the `workspace` pane in the pane tree.
* Navigate to `to`, fronting the workspace pane when it is a page route.
*
* Without this, `navigate()` alone updates the route and the content inside
* `workspace` correctly, but if a session tile is currently focused in the
* same tab group, `workspace` stays behind it (issue #72602). This mirrors
* what `$selectedStoredSessionId.listen` already does for session switches
* in `store/session-states.ts`.
* `syncWorkspaceRoute` covers route CHANGES; this covers the RE-CLICK, the one
* case it can't see hitting Capabilities while already on `/skills` with a
* tile focused leaves the location untouched, so no effect fires and only an
* imperative reveal brings the page back. Use it wherever a nav affordance can
* be triggered from the page it targets.
*/
export function navigateToWorkspacePage(navigate: NavigateLike, path: string, options?: { replace?: boolean }): void {
navigate(path, options)
export function navigateToWorkspacePage(navigate: NavigateLike, to: string, options?: { replace?: boolean }): void {
navigate(to, options)
const view = appViewForPath(path)
if (view !== 'chat' && !isOverlayView(view)) {
noteActiveTreeGroup(null)
revealTreePane('workspace')
if (isWorkspacePageRoute(to)) {
revealWorkspacePane()
}
}