diff --git a/apps/web/src/shell/app-shell.tsx b/apps/web/src/shell/app-shell.tsx index a6b91cf45..db5d0e694 100644 --- a/apps/web/src/shell/app-shell.tsx +++ b/apps/web/src/shell/app-shell.tsx @@ -67,6 +67,14 @@ export function AppShell({ } }; + // Open a channel into the canvas without leaving the current page. Unlike + // handleChannelChange, this never touches the URL — a channel row click in + // col2 should pop the conversation open in col4 and keep the user on /library + // (or wherever they are). Deep-link navigation is reserved for the URL. + const handleOpenInCanvas = (channelId: string) => { + setCanvasState(openChannelInCanvas(channelId)); + }; + return (
@@ -83,6 +91,7 @@ export function AppShell({ canvasOpen={canvasState.open} onToggleCanvas={() => setCanvasState(toggleCanvasColumn)} canvasAllowed={canvasAllowed} + onOpenInCanvas={handleOpenInCanvas} /> )}
diff --git a/apps/web/src/shell/contextual-panel.tsx b/apps/web/src/shell/contextual-panel.tsx index 3fcf5e674..9599fe65f 100644 --- a/apps/web/src/shell/contextual-panel.tsx +++ b/apps/web/src/shell/contextual-panel.tsx @@ -25,20 +25,22 @@ export function ContextualPanel({ canvasOpen, onToggleCanvas, canvasAllowed, + onOpenInCanvas, }: { readonly path: string; readonly onNavigate: (to: string) => void; readonly canvasOpen: boolean; readonly onToggleCanvas: () => void; readonly canvasAllowed: boolean; + readonly onOpenInCanvas: (channelId: string) => void; }) { const contribution = resolvePanelContribution(path); - const pageBand = contribution?.pageBand({ path, onNavigate }) ?? { + const renderCtx = { path, onNavigate, onOpenInCanvas }; + const pageBand = contribution?.pageBand(renderCtx) ?? { title: "Workbench", subtitle: "Navigate from the rail", }; - const pageSpecific = - contribution?.pageSpecific?.({ path, onNavigate }) ?? null; + const pageSpecific = contribution?.pageSpecific?.(renderCtx) ?? null; const [pins] = useState(() => loadPins()); diff --git a/apps/web/src/shell/panel-contribution.ts b/apps/web/src/shell/panel-contribution.ts index ce2ee3a93..531bc02f3 100644 --- a/apps/web/src/shell/panel-contribution.ts +++ b/apps/web/src/shell/panel-contribution.ts @@ -22,6 +22,12 @@ export type PageBand = { export type PanelRenderContext = { readonly path: string; readonly onNavigate: (to: string) => void; + /** + * Open a channel into the canvas without leaving the current page. Channel + * rows in the panel call this instead of navigating so a user on /library can + * pop a conversation open in column 4 and stay where they are. + */ + readonly onOpenInCanvas: (channelId: string) => void; }; export type PanelContribution = { diff --git a/apps/web/src/shell/panel-contributions.tsx b/apps/web/src/shell/panel-contributions.tsx index 9a71f3ca2..5b48941d1 100644 --- a/apps/web/src/shell/panel-contributions.tsx +++ b/apps/web/src/shell/panel-contributions.tsx @@ -19,10 +19,10 @@ function pathMatches(prefix: string, path: string): boolean { function ChannelsBand({ path, - onNavigate, + onOpenInCanvas, }: { readonly path: string; - readonly onNavigate: (to: string) => void; + readonly onOpenInCanvas: (channelId: string) => void; }) { const { selectedTenantId } = useBench(); const activity = useBenchActivity(selectedTenantId); @@ -72,7 +72,7 @@ function ChannelsBand({ key={channel.id} name={channel.title || "Untitled channel"} selected={channel.id === activeId} - onSelect={() => onNavigate(`${channelPath(channel.id)}`)} + onSelect={() => onOpenInCanvas(channel.id)} /> ))}
@@ -85,7 +85,7 @@ function ChannelsBand({ key={channel.id} name={channel.title || "Untitled chat"} selected={channel.id === activeId} - onSelect={() => onNavigate(`${channelPath(channel.id)}`)} + onSelect={() => onOpenInCanvas(channel.id)} /> ))}
@@ -282,7 +282,7 @@ export function ensurePanelContributions(): void { ], }), pageSpecific: (ctx) => ( - + ), }); diff --git a/apps/web/test/contextual-panel.test.tsx b/apps/web/test/contextual-panel.test.tsx index 9b62c610d..9f26da0cd 100644 --- a/apps/web/test/contextual-panel.test.tsx +++ b/apps/web/test/contextual-panel.test.tsx @@ -24,6 +24,7 @@ function renderPanel(path: string): string { { { { { }); expect(registry.list()).toHaveLength(1); expect( - registry - .resolve("/agents") - ?.pageBand({ path: "/agents", onNavigate: () => undefined }).title, + registry.resolve("/agents")?.pageBand({ + path: "/agents", + onNavigate: () => undefined, + onOpenInCanvas: () => undefined, + }).title, ).toBe("Agents v2"); });