From 6ab56781f64bf188adc4dd0fc09776630aca66c8 Mon Sep 17 00:00:00 2001 From: jtenniswood Date: Tue, 18 Aug 2026 17:39:45 +0100 Subject: [PATCH 1/8] =?UTF-8?q?feat(studio):=20Figma=20shell=20redesign=20?= =?UTF-8?q?=E2=80=94=20top=20nav,=20card=20surface,=20right=20session=20li?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the new workspace shell from Figma (node 4:2): the left rail is gone. A 64px top navigation bar sits on a fixed dark-green radial gradient — white Stacklok mark, the five surfaces as a pill nav (active route renders as a light-sage pill with icon + label; the rest are icon-only with tooltips), and the ⌘K global search styled as the design's search field. All five surfaces render inside one rounded 20px card that follows the theme; the gradient shell is identical in light and dark. The chat workspace flips: conversation on the left (title in a 64px card header, content and composer on the design's 768px measure), "Session List" on the right — recency-bucketed daemon sessions with the design's row treatment (3px brand accent + tint on the active row, muted relative times, a pulsing brand dot for running sessions) and an AGENTS section fed by the daemon's real GET /v1/agents roster (clicking one starts a new draft). The resize handle and collapse, keyboard shortcuts, capability-gated rename/delete, and the draft mint flow all carry over mirrored. Design content with no daemon backing is deliberately not fabricated: the mock PROJECTS folders render as recency groups per the base PR's decision, and the "New project" button, notification bell, and per-agent chat nesting are dropped. The old shell (console-shell, shell, navbar, nav-drawer, rail sidebar) and its orphaned hooks are deleted; ShortcutsProvider moved into the workspace layout; nav config consolidated into one nav-items module. Co-Authored-By: Claude Fable 5 --- studio/knip.ts | 1 + .../workspace/chat/_components/chat-view.tsx | 75 ++-- .../chat/_components/chat-workspace.tsx | 66 ++- .../chat/_components/session-sidebar.tsx | 62 ++- studio/src/app/workspace/layout.tsx | 33 +- studio/src/components/app/nav-items.ts | 39 +- studio/src/components/shell/console-shell.tsx | 52 --- studio/src/components/shell/nav-drawer.tsx | 64 --- studio/src/components/shell/nav-items.ts | 64 --- studio/src/components/shell/navbar.tsx | 36 -- studio/src/components/shell/shell.tsx | 77 ---- studio/src/components/shell/sidebar.tsx | 420 ------------------ studio/src/components/shell/top-nav.tsx | 99 +++++ .../features/agent/hooks/use-agent-roster.ts | 40 ++ studio/src/features/agent/index.ts | 1 + studio/src/hooks/use-nav-collapsed.ts | 44 -- studio/src/hooks/use-rail-width.ts | 59 --- 17 files changed, 329 insertions(+), 903 deletions(-) delete mode 100644 studio/src/components/shell/console-shell.tsx delete mode 100644 studio/src/components/shell/nav-drawer.tsx delete mode 100644 studio/src/components/shell/nav-items.ts delete mode 100644 studio/src/components/shell/navbar.tsx delete mode 100644 studio/src/components/shell/shell.tsx delete mode 100644 studio/src/components/shell/sidebar.tsx create mode 100644 studio/src/components/shell/top-nav.tsx create mode 100644 studio/src/features/agent/hooks/use-agent-roster.ts delete mode 100644 studio/src/hooks/use-nav-collapsed.ts delete mode 100644 studio/src/hooks/use-rail-width.ts diff --git a/studio/knip.ts b/studio/knip.ts index a6d0c5455e..6b5cbbbf9b 100644 --- a/studio/knip.ts +++ b/studio/knip.ts @@ -27,6 +27,7 @@ const config: KnipConfig = { "@radix-ui/react-checkbox", "@radix-ui/react-popover", "@radix-ui/react-progress", + "@radix-ui/react-separator", "@radix-ui/react-toggle", "@radix-ui/react-toggle-group", ], diff --git a/studio/src/app/workspace/chat/_components/chat-view.tsx b/studio/src/app/workspace/chat/_components/chat-view.tsx index c3d10668b2..e1df97c169 100644 --- a/studio/src/app/workspace/chat/_components/chat-view.tsx +++ b/studio/src/app/workspace/chat/_components/chat-view.tsx @@ -9,7 +9,7 @@ import { Loader2, MessageCircle, MessageSquareText, - PanelLeftOpen, + PanelRightClose, PanelRightOpen, Pencil, RotateCcw, @@ -439,39 +439,18 @@ export function ChatView({ panelMaximized && "hidden", )} > -
- - - - - - {isMobile - ? "Back to chats" - : sidebarOpen - ? "Hide sidebar" - : "Show sidebar"} - - +
+ {isMobile && ( + + )}

)} + {!isMobile && ( + + + + + + {sidebarOpen ? "Hide sidebar" : "Show sidebar"} + + + )} {!isMobile && ( @@ -539,7 +540,7 @@ export function ChatView({ onAddToChat={(text) => setAppendText(text)} onAskInSideChat={(text) => setAppendText(text)} /> -
+
{messages.map((msg) => (
-
+
{error && (
diff --git a/studio/src/app/workspace/chat/_components/chat-workspace.tsx b/studio/src/app/workspace/chat/_components/chat-workspace.tsx index 0a776c4569..4ad26b6c8b 100644 --- a/studio/src/app/workspace/chat/_components/chat-workspace.tsx +++ b/studio/src/app/workspace/chat/_components/chat-workspace.tsx @@ -1,6 +1,6 @@ "use client"; -import { Loader2, PanelLeft, SquarePen } from "lucide-react"; +import { Loader2, PanelRight, SquarePen } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; @@ -11,7 +11,9 @@ import { } from "@/components/ui/tooltip"; import { type AgentSession, + type RosterAgent, useAgentChat, + useAgentRoster, useAgentSessions, } from "@/features/agent"; import { useConfirm } from "@/hooks/use-confirm"; @@ -25,6 +27,7 @@ import { ChatInput } from "../../_components/chat-input"; import { ResizeHandle } from "../../_components/resize-handle"; import { ChatView } from "./chat-view"; import { + AgentList, type SessionActions, SessionList, SidebarGroup, @@ -76,6 +79,7 @@ function SidebarContent({ isLoading, error, groups, + agents, selectedId, onSelect, actions, @@ -84,14 +88,17 @@ function SidebarContent({ isLoading: boolean; error: string | null; groups: { label: string; sessions: AgentSession[] }[]; + agents: RosterAgent[]; selectedId: string; onSelect: (id: string) => void; actions: SessionActions; }) { return ( <> -
-

Chats

+
+

+ Session List +

); @@ -167,11 +181,11 @@ function DraftView({ )}
@@ -224,6 +238,7 @@ export function ChatWorkspace({ sessionId }: { sessionId?: string }) { renameSession, refreshSessions, } = useAgentSessions(); + const { agents } = useAgentRoster(); const router = useRouter(); const { name: agentName } = useAgentDisplayName(); const isMobile = useIsMobile(); @@ -448,6 +463,7 @@ export function ChatWorkspace({ sessionId }: { sessionId?: string }) { isLoading: sessionsLoading, error: sessionsError, groups, + agents, selectedId, onSelect: handleSelectSession, actions: sessionActions, @@ -523,6 +539,22 @@ export function ChatWorkspace({ sessionId }: { sessionId?: string }) { return (
{dialogs} +
+ {selectedSession ? ( + chatView(sidebarOpen, () => setSidebarOpen((o) => !o)) + ) : ( + setSidebarOpen(true)} + /> + )} +
+ {sidebarOpen && (isCompact ? ( <> @@ -534,10 +566,12 @@ export function ChatWorkspace({ sessionId }: { sessionId?: string }) { onClick={() => setSidebarOpen(false)} />
+ {/* Handle on the panel's left edge: dragging left widens */} @@ -546,32 +580,18 @@ export function ChatWorkspace({ sessionId }: { sessionId?: string }) { ) : (
+ {/* Handle on the panel's left edge: dragging left widens */}
))} - -
- {selectedSession ? ( - chatView(sidebarOpen, () => setSidebarOpen((o) => !o)) - ) : ( - setSidebarOpen(true)} - /> - )} -
); } diff --git a/studio/src/app/workspace/chat/_components/session-sidebar.tsx b/studio/src/app/workspace/chat/_components/session-sidebar.tsx index 92dbfb12e0..faa607509a 100644 --- a/studio/src/app/workspace/chat/_components/session-sidebar.tsx +++ b/studio/src/app/workspace/chat/_components/session-sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { Ellipsis, Loader2, Pencil, Trash2 } from "lucide-react"; +import { Bot, Ellipsis, Pencil, Trash2 } from "lucide-react"; import { useState } from "react"; import { DropdownMenu, @@ -9,7 +9,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import type { AgentSession } from "@/features/agent"; +import type { AgentSession, RosterAgent } from "@/features/agent"; import { formatRelativeTime } from "@/lib/formatters"; import { cn } from "@/lib/utils"; @@ -109,12 +109,15 @@ function SessionRow({ actions: SessionActions; }) { const [menuOpen, setMenuOpen] = useState(false); + const isRunning = session.isStreaming || session.state === "running"; return (
- {session.isStreaming ? ( - @@ -215,7 +219,7 @@ export function SessionList({ @@ -223,3 +227,37 @@ export function SessionList({
); } + +/** + * The daemon's real agent roster, listed below the chat groups. Agents are + * not chat containers — selecting one simply starts a new chat draft. + */ +export function AgentList({ + agents, + onStartChat, +}: { + agents: RosterAgent[]; + onStartChat: () => void; +}) { + return ( +
+ {agents.map((agent) => ( + + ))} +
+ ); +} diff --git a/studio/src/app/workspace/layout.tsx b/studio/src/app/workspace/layout.tsx index 30f6493ec8..b24950b8b7 100644 --- a/studio/src/app/workspace/layout.tsx +++ b/studio/src/app/workspace/layout.tsx @@ -1,7 +1,17 @@ -import { ConsoleShell } from "@/components/shell/console-shell"; -import { SidebarProvider } from "@/components/ui/sidebar"; +import { TopNav } from "@/components/shell/top-nav"; import { RuntimeStatusProvider } from "@/features/agent/runtime-status"; +import { ShortcutsProvider } from "@/lib/shortcuts/use-shortcuts"; +/** + * The workspace shell: a fixed dark-green radial gradient carrying the top + * navigation bar, with all five surfaces rendered inside one rounded card + * that follows the theme. The gradient itself is a fixed brand colour — + * identical in light and dark themes — so only the card interior themes. + * + * `RuntimeStatusProvider` stays outermost: its offline banner renders above + * the top nav at full width. Workspace sections manage their own scrolling + * and padding inside the card (`h-full overflow-y-auto …`). + */ export default function WorkspaceLayout({ children, }: Readonly<{ @@ -9,14 +19,19 @@ export default function WorkspaceLayout({ }>) { return ( - - {/* Not SidebarInset: the shell renders the page's one
, and - SidebarInset is itself a
, which would nest them. This div - carries the flex sizing the shell layout needs. */} -
- {children} + +
+ + {/* relative makes the card the containing block for absolutely- + positioned descendants with no positioned ancestor of their own + — notably the hidden form-integration checkbox Radix renders + beside each Switch inside a
. Without it those boxes + resolve to the document and grow the page itself. */} +
+ {children} +
- +
); } diff --git a/studio/src/components/app/nav-items.ts b/studio/src/components/app/nav-items.ts index 626f91bc5a..59876fed8c 100644 --- a/studio/src/components/app/nav-items.ts +++ b/studio/src/components/app/nav-items.ts @@ -1,9 +1,9 @@ /** - * The Atrium workspace console's sidebar configuration. - * - * The app is Atrium-only: a single rail of workspace destinations — the in-app - * chat plus the state that outlives a turn (Skills, Memory, Scheduled). The - * wordmark and home both point at Chats. + * The Atrium workspace navigation configuration — the single canonical + * nav-items module. The top nav is generic over the `ShellNav` built here: + * a single row of workspace destinations — the in-app chat plus the state + * that outlives a turn (Skills, Memory, Scheduled). The wordmark and home + * both point at Chats. */ import { @@ -13,9 +13,36 @@ import { MessageCircle, Settings, } from "lucide-react"; -import type { ShellNav } from "@/components/shell/nav-items"; +import type { ComponentType } from "react"; import { ATRIUM_WORKSPACE_HOME } from "@/lib/feature-flags"; +interface NavItem { + /** Stable key for React keys and test selectors. */ + readonly key: string; + /** Nav label (pill text when active, tooltip when inactive). */ + readonly label: string; + /** Absolute route this destination points at. */ + readonly href: string; + /** Nav icon. */ + readonly icon: ComponentType<{ className?: string }>; +} + +/** + * The console's navigation configuration. One value drives the whole shell: + * the top-nav destinations and the wordmark's home link. + */ +export interface ShellNav { + /** The console's root route (used for the "home" active-state rule). */ + readonly homeHref: string; + /** Where the wordmark links, if different from `homeHref` (e.g. Atrium + sends the logo to the workspace chats rather than the gateway home). */ + readonly logoHref?: string; + /** `aria-label` for the `