Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/web/src/shell/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<CanvasAvailabilityProvider allowed={canvasAllowed}>
<div className="shell-frame" ref={frameRef}>
Expand All @@ -83,6 +91,7 @@ export function AppShell({
canvasOpen={canvasState.open}
onToggleCanvas={() => setCanvasState(toggleCanvasColumn)}
canvasAllowed={canvasAllowed}
onOpenInCanvas={handleOpenInCanvas}
/>
)}
<div className="shell-main" ref={mainRef}>
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/shell/contextual-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<readonly Pin[]>(() => loadPins());

Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/shell/panel-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/shell/panel-contributions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)}
/>
))}
</div>
Expand All @@ -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)}
/>
))}
</div>
Expand Down Expand Up @@ -282,7 +282,7 @@ export function ensurePanelContributions(): void {
],
}),
pageSpecific: (ctx) => (
<ChannelsBand path={ctx.path} onNavigate={ctx.onNavigate} />
<ChannelsBand path={ctx.path} onOpenInCanvas={ctx.onOpenInCanvas} />
),
});

Expand Down
4 changes: 4 additions & 0 deletions apps/web/test/contextual-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function renderPanel(path: string): string {
<ContextualPanel
path={path}
onNavigate={noop}
onOpenInCanvas={noop}
canvasOpen={false}
onToggleCanvas={noop}
canvasAllowed={false}
Expand Down Expand Up @@ -66,6 +67,7 @@ describe("ContextualPanel", () => {
<ContextualPanel
path="/"
onNavigate={noop}
onOpenInCanvas={noop}
canvasOpen={false}
onToggleCanvas={noop}
canvasAllowed={false}
Expand Down Expand Up @@ -128,6 +130,7 @@ describe("ContextualPanel", () => {
<ContextualPanel
path="/"
onNavigate={noop}
onOpenInCanvas={noop}
canvasOpen={false}
onToggleCanvas={noop}
canvasAllowed={false}
Expand Down Expand Up @@ -204,6 +207,7 @@ describe("ContextualPanel", () => {
<ContextualPanel
path="/"
onNavigate={noop}
onOpenInCanvas={noop}
canvasOpen={false}
onToggleCanvas={noop}
canvasAllowed={false}
Expand Down
8 changes: 5 additions & 3 deletions apps/web/test/panel-contribution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ describe("createPanelRegistry", () => {
});
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");
});

Expand Down
Loading