From cde664e7287a26890b69d6d4d3d8d6967f99e525 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 2 Sep 2026 16:42:24 +1000 Subject: [PATCH] feat(desktop): move the Project Canvas beside project chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Canvas preview sat above the message timeline as a fixed 25.2/28.8rem strip, so it ate the top of every project channel's chat: the timeline started below the fold, and a "Chat preview boundary" marker had to be drawn across the full Canvas to show authors where that fold landed. Put the preview in a right-hand pane instead. Chat keeps its full height, and the Canvas gets a tall column, which suits a dashboard of stacked widgets far better than a letterbox. - `ChannelViewOverride` gains `mainColumnHeaderPlacement` ("top" | "right", default top). `ChannelPaneMainColumn` lays the header out as a sibling column (`order-last`, `clamp(24rem, 38%, 28.8rem)`) when it is "right" and keeps the existing `contents` pass-through otherwise, so no other channel view changes. `ProjectChannelHome` asks for "right" on the chat tab and "top" on the canvas tab, where the surface still fills the pane. - The pane is container-queried, not viewport-queried: it appears only once the chat column itself is at least 52rem wide, so a narrow window shows chat at full width and users reach the Canvas through its tab. `ChannelPane` also passes `hideRightHeader` when a split auxiliary pane is open, so a thread, profile, or agent panel takes that space rather than competing with it. The frame stays mounted (`display: none`), so opening and closing a thread never reloads the package or drops its live subscriptions. - The preview-boundary marker goes away with the fold it described, and "Show full canvas" becomes a tooltipped icon button pinned bottom-right, since a full-width labelled button no longer fits a 24rem column. It follows the existing icon-button idiom: one `aria-label` on the button, matching tooltip copy. The e2e spec binds the new geometry — chat and composer both end at or before the pane's left edge and the pane is taller than it is wide; the narrow-viewport test now asserts the pane is hidden and the full tab is the way in; and a new 1920px test opens a thread to check the pane yields, chat takes the width, and the frame's `src` survives the round trip. Verified: desktop unit suite 5987 passing, project-channel-canvas 8 passing, the project and thread layout specs 6 passing, and tsc, biome, px-text, pubkey-truncation, and the file-size gate clean. Three specs fail on this branch independent of this change — project-cold-start.spec.ts:76 and :107 plus project-empty-state-alignment.spec.ts:48 look for `project-home-context-panel`, whose component has had no callers since the channel-first migration. Co-Authored-By: Claude Opus 5 Signed-off-by: Matt Toohey --- .../src/features/channels/ui/ChannelPane.tsx | 2 +- .../channels/ui/ChannelPaneMainColumn.tsx | 36 +++++- .../ui/ChannelViewOverrideContext.tsx | 1 + .../projects/ui/ProjectChannelHome.tsx | 2 + .../project-canvas/ProjectCanvasSurface.tsx | 47 +++----- .../tests/e2e/project-channel-canvas.spec.ts | 110 ++++++++++++++---- 6 files changed, 146 insertions(+), 52 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index f31ee2adb6..87e0c57963 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -623,7 +623,7 @@ export const ChannelPane = React.memo(function ChannelPane({ ) : null} - +
- {mainColumnHeader}
+ {mainColumnHeader} +
+
{children} diff --git a/desktop/src/features/channels/ui/ChannelViewOverrideContext.tsx b/desktop/src/features/channels/ui/ChannelViewOverrideContext.tsx index 5be245c628..cafb2321eb 100644 --- a/desktop/src/features/channels/ui/ChannelViewOverrideContext.tsx +++ b/desktop/src/features/channels/ui/ChannelViewOverrideContext.tsx @@ -5,6 +5,7 @@ type ChannelViewOverride = { hideMainColumnBody?: boolean; isChannelViewActive: boolean; mainColumnHeader?: React.ReactNode; + mainColumnHeaderPlacement?: "top" | "right"; mainContent: React.ReactNode; onSelectChannelView: () => void; }; diff --git a/desktop/src/features/projects/ui/ProjectChannelHome.tsx b/desktop/src/features/projects/ui/ProjectChannelHome.tsx index 38396101d9..06a7da44b6 100644 --- a/desktop/src/features/projects/ui/ProjectChannelHome.tsx +++ b/desktop/src/features/projects/ui/ProjectChannelHome.tsx @@ -700,6 +700,8 @@ export function ProjectChannelHome({ snapshots={canvasSnapshots} /> ) : null, + mainColumnHeaderPlacement: + activeView === "chat" ? "right" : "top", mainContent, onSelectChannelView: () => selectView("chat"), }} diff --git a/desktop/src/features/projects/ui/project-canvas/ProjectCanvasSurface.tsx b/desktop/src/features/projects/ui/project-canvas/ProjectCanvasSurface.tsx index a9d46e240f..7e9a87d92d 100644 --- a/desktop/src/features/projects/ui/project-canvas/ProjectCanvasSurface.tsx +++ b/desktop/src/features/projects/ui/project-canvas/ProjectCanvasSurface.tsx @@ -2,6 +2,7 @@ import { Maximize2 } from "lucide-react"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; import { ProjectCanvasHost } from "./ProjectCanvasHost"; import type { ProjectCanvasBroker } from "./projectCanvasBroker"; import type { ProjectCanvasSnapshots } from "./projectCanvasProtocol"; @@ -32,9 +33,7 @@ export function ProjectCanvasSurface({
- {full ? ( -