From 501a1742ccc0299abdc65e1b5037622ffab047da Mon Sep 17 00:00:00 2001 From: jtenniswood Date: Wed, 2 Sep 2026 11:26:58 +0100 Subject: [PATCH] feat(studio): workspace shell, runtime status, and the domain-hook spine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frame every surface mounts in. app/workspace/layout.tsx wires the gradient chrome, the runtime-status provider, shortcuts, the storage-health banner, and the top navigation; components/shell carries the nav and the ⌘K global-search palette (static routes + live session/thread rows). features/agent/runtime-status.tsx is the single connection authority: one 5s dual poll (daemon + controller) gates every surface, carries the feature set, server capabilities and deployment identity off GET /v1/compatibility, shows the offline banner that names the fix, and refreshes composer capabilities on reconnect. Probe failure renders state — never fabricated content. The domain hooks land over the transport tier: use-agent-sessions/cron/ skills/memory/roster and storage health, plus composer-capabilities, lib/thread-map (28 tests; the palette needs it), lib/shortcuts and the static shortcuts page, and lib/feature-flags. knip sheds the foundation-era ignores whose first consumers arrive here; palette links to surfaces landing later in the series 404 into the branded not-found page until then. Co-Authored-By: Claude Fable 5 --- studio/knip.ts | 6 - studio/src/app/workspace/layout.tsx | 52 +++ studio/src/app/workspace/shortcuts/page.tsx | 65 +++ studio/src/components/app/nav-items.ts | 76 ++++ .../components/shell/atrium-search-data.ts | 82 ++++ studio/src/components/shell/global-search.tsx | 166 ++++++++ .../components/shell/search-static.test.ts | 73 ++++ studio/src/components/shell/search-static.ts | 66 +++ studio/src/components/shell/search-types.ts | 51 +++ studio/src/components/shell/top-nav.tsx | 103 +++++ .../features/agent/composer-capabilities.ts | 74 ++++ .../agent/hooks/use-agent-cron.test.ts | 167 ++++++++ .../features/agent/hooks/use-agent-cron.ts | 235 +++++++++++ .../features/agent/hooks/use-agent-memory.ts | 78 ++++ .../features/agent/hooks/use-agent-roster.ts | 48 +++ .../agent/hooks/use-agent-sessions.ts | 235 +++++++++++ .../features/agent/hooks/use-agent-skills.ts | 180 ++++++++ .../agent/hooks/use-storage-health.ts | 46 +++ studio/src/features/agent/index.ts | 31 ++ studio/src/features/agent/runtime-status.tsx | 222 ++++++++++ .../features/agent/storage-health-banner.tsx | 39 ++ studio/src/lib/feature-flags.ts | 7 + studio/src/lib/protocol/index.ts | 1 + studio/src/lib/protocol/schedules.ts | 2 +- studio/src/lib/shortcuts/registry.test.ts | 121 ++++++ studio/src/lib/shortcuts/registry.ts | 178 ++++++++ studio/src/lib/shortcuts/use-shortcuts.tsx | 79 ++++ studio/src/lib/thread-map.test.ts | 300 ++++++++++++++ studio/src/lib/thread-map.ts | 387 ++++++++++++++++++ 29 files changed, 3163 insertions(+), 7 deletions(-) create mode 100644 studio/src/app/workspace/layout.tsx create mode 100644 studio/src/app/workspace/shortcuts/page.tsx create mode 100644 studio/src/components/app/nav-items.ts create mode 100644 studio/src/components/shell/atrium-search-data.ts create mode 100644 studio/src/components/shell/global-search.tsx create mode 100644 studio/src/components/shell/search-static.test.ts create mode 100644 studio/src/components/shell/search-static.ts create mode 100644 studio/src/components/shell/search-types.ts create mode 100644 studio/src/components/shell/top-nav.tsx create mode 100644 studio/src/features/agent/composer-capabilities.ts create mode 100644 studio/src/features/agent/hooks/use-agent-cron.test.ts create mode 100644 studio/src/features/agent/hooks/use-agent-cron.ts create mode 100644 studio/src/features/agent/hooks/use-agent-memory.ts create mode 100644 studio/src/features/agent/hooks/use-agent-roster.ts create mode 100644 studio/src/features/agent/hooks/use-agent-sessions.ts create mode 100644 studio/src/features/agent/hooks/use-agent-skills.ts create mode 100644 studio/src/features/agent/hooks/use-storage-health.ts create mode 100644 studio/src/features/agent/index.ts create mode 100644 studio/src/features/agent/runtime-status.tsx create mode 100644 studio/src/features/agent/storage-health-banner.tsx create mode 100644 studio/src/lib/feature-flags.ts create mode 100644 studio/src/lib/shortcuts/registry.test.ts create mode 100644 studio/src/lib/shortcuts/registry.ts create mode 100644 studio/src/lib/shortcuts/use-shortcuts.tsx create mode 100644 studio/src/lib/thread-map.test.ts create mode 100644 studio/src/lib/thread-map.ts diff --git a/studio/knip.ts b/studio/knip.ts index 8c20a3df87..db5052d3f6 100644 --- a/studio/knip.ts +++ b/studio/knip.ts @@ -18,9 +18,7 @@ const config: KnipConfig = { "src/features/**", // Reached only through the UI kit until later PRs in the stacked series // land their first app-level consumers; each line leaves with that PR. - "src/hooks/use-is-truncated.ts", "src/hooks/use-mobile.ts", - "src/lib/typography.ts", ], ignoreDependencies: [ // Tailwind v4 is imported via CSS (@import "tailwindcss"), not JS @@ -31,17 +29,13 @@ const config: KnipConfig = { // (these leave the list as their first reached consumers land later in // the stacked series: lucide-react/cmdk/tooltip with the workspace shell, // select/switch/tabs with the surface pages) - "lucide-react", - "cmdk", "@radix-ui/react-alert-dialog", - "@radix-ui/react-dialog", "@radix-ui/react-dropdown-menu", "@radix-ui/react-label", "@radix-ui/react-scroll-area", "@radix-ui/react-select", "@radix-ui/react-switch", "@radix-ui/react-tabs", - "@radix-ui/react-tooltip", "@radix-ui/react-avatar", "@radix-ui/react-checkbox", "@radix-ui/react-popover", diff --git a/studio/src/app/workspace/layout.tsx b/studio/src/app/workspace/layout.tsx new file mode 100644 index 0000000000..31a8200472 --- /dev/null +++ b/studio/src/app/workspace/layout.tsx @@ -0,0 +1,52 @@ +import { TopNav } from "@/components/shell/top-nav"; +import { RuntimeStatusProvider } from "@/features/agent/runtime-status"; +import { StorageHealthBanner } from "@/features/agent/storage-health-banner"; +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<{ + children: React.ReactNode; +}>) { + return ( + + + {/* The design's green radial gradient; dark mode deepens each stop so + the shell recedes behind the dark card instead of outglowing it. + Top padding tracks the status-bar safe area: the installed iOS + PWA (black-translucent status bar + viewport-fit cover) draws + under the clock, so the nav must start below it while the + gradient still paints behind it. */} +
+ {/* Storage health rides the same full-width banner band as the + runtime status banners: visible from every surface, because a + degraded store shows up as chats missing from the sidebar. */} + + + {/* 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. */} + {/* On mobile the card goes full-bleed — no gradient margin, only + the top corners stay rounded where it meets the nav band — so + content spans the screen; the gradient survives only behind the + top nav and the tab bar. The inset card is a ≥500px treatment. */} +
+ {children} +
+
+
+
+ ); +} diff --git a/studio/src/app/workspace/shortcuts/page.tsx b/studio/src/app/workspace/shortcuts/page.tsx new file mode 100644 index 0000000000..1e9aade517 --- /dev/null +++ b/studio/src/app/workspace/shortcuts/page.tsx @@ -0,0 +1,65 @@ +import type { Metadata } from "next"; +import { keycaps, SHORTCUT_GROUPS, SHORTCUTS } from "@/lib/shortcuts/registry"; +import { pageTitleClass } from "@/lib/typography"; + +export const metadata: Metadata = { title: "Keyboard shortcuts — Workspace" }; + +/** A single keycap. */ +function Key({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +/** + * Reference page listing every keyboard shortcut, rendered directly from the + * shortcut registry so it can't drift from the live bindings. Reached from the + * profile menu and by pressing `?`. + */ +export default function KeyboardShortcutsPage() { + return ( +
+
+
+

+ Keyboard shortcuts +

+

+ Work faster with the keyboard. On Windows and Linux, use Ctrl + wherever ⌘ is shown. +

+
+ +
+ {SHORTCUT_GROUPS.map((group) => ( +
+

+ {group} +

+
    + {SHORTCUTS.filter((s) => s.group === group).map((s) => ( +
  • + + {s.description} + + + {keycaps(s.combo).map((k, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: positional keycaps + {k} + ))} + +
  • + ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/studio/src/components/app/nav-items.ts b/studio/src/components/app/nav-items.ts new file mode 100644 index 0000000000..d0dbfd1ccd --- /dev/null +++ b/studio/src/components/app/nav-items.ts @@ -0,0 +1,76 @@ +/** + * 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 { Clock, GraduationCap, MessageCircle, Settings } from "lucide-react"; +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 `