From 440642880c269d20ebf559340762dd9da0ed55f5 Mon Sep 17 00:00:00 2001 From: jessicat <8797119+jess-cat@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:53:41 +0100 Subject: [PATCH] Navigation IA overhaul: native apps under APPLICATIONS, GSV ship menu (OVERVIEW/AGENTS/MODELS/TASKS), collapsed-first reveal rail, connect nomenclature, external-app provenance tags --- web/src/app/components/ui/AgentToolsPanel.tsx | 2 +- web/src/app/components/ui/IconMenu.tsx | 63 ++-- .../chat/components/ChatAgentPanel.tsx | 2 +- .../files/components/FilesSurfaceSummary.tsx | 4 +- .../CardListTemplateMockPage.tsx | 2 +- .../gsv-console/components/GsvConsole.tsx | 41 ++- .../connect-flows/applicationConnectMock.tsx | 10 +- .../IntegrationOnboardingFlow.tsx | 2 +- .../integrations/IntegrationsPage.tsx | 2 +- .../gsv-console/library/LibraryPage.tsx | 2 +- .../list-template/ListTemplateMockPage.tsx | 6 +- .../gsv-console/machines/MachinesPage.tsx | 2 +- .../packages/ApplicationImportFlow.tsx | 8 +- .../gsv-console/packages/PackageListPage.tsx | 41 ++- .../gsv-console/pages/ConsoleCrewPage.tsx | 2 +- .../pages/ConsoleOverviewPanels.tsx | 6 +- .../gsv-console/runtime/RuntimePage.tsx | 2 +- web/src/app/features/gsv-shell/GsvShell.tsx | 126 +++---- .../features/gsv-shell/desktop/GsvDesktop.tsx | 176 +++++----- .../gsv-shell/domain/desktopObjects.test.ts | 48 ++- .../gsv-shell/domain/desktopObjects.ts | 34 +- .../features/gsv-shell/domain/shellModel.ts | 83 +++-- .../gsv-shell/hooks/useGsvShellState.ts | 73 ++-- .../gsv-shell/navigation/ShellRail.tsx | 140 +++++--- .../gsv-shell/routing/shellRoutes.test.ts | 22 ++ .../features/gsv-shell/routing/shellRoutes.ts | 1 + .../features/gsv-shell/styles/gsvShell.css | 172 +++------ web/src/design-system/catalog.tsx | 2 + web/src/design-system/previews.tsx | 4 +- .../stories/DesktopHint.story.tsx | 4 +- .../stories/ExternalApplicationTile.story.tsx | 331 ++++++++++++++++++ .../design-system/stories/IconMenu.story.tsx | 29 +- 32 files changed, 917 insertions(+), 525 deletions(-) create mode 100644 web/src/design-system/stories/ExternalApplicationTile.story.tsx diff --git a/web/src/app/components/ui/AgentToolsPanel.tsx b/web/src/app/components/ui/AgentToolsPanel.tsx index 56fb1171b..1e249c688 100644 --- a/web/src/app/components/ui/AgentToolsPanel.tsx +++ b/web/src/app/components/ui/AgentToolsPanel.tsx @@ -319,7 +319,7 @@ export function AgentToolsPanel({
- - - - + {cells.map((cell) => ( + + ))}
); diff --git a/web/src/app/features/chat/components/ChatAgentPanel.tsx b/web/src/app/features/chat/components/ChatAgentPanel.tsx index a148ffb32..395dbca40 100644 --- a/web/src/app/features/chat/components/ChatAgentPanel.tsx +++ b/web/src/app/features/chat/components/ChatAgentPanel.tsx @@ -126,7 +126,7 @@ export function ChatAgentPanel({ onChange={setQuery} /> - - {shell.pickerId === "gsv" ? ( -
- shell.setPickerId(null)} - onFiles={() => openShellSurface("files")} - onRepositories={() => openShellSurface("repositories")} - onLibrary={() => openShellSurface("library")} - onTerminal={() => openShellSurface("terminal")} - onSettings={() => openShellSurface("settings")} - /> -
- ) : null} - - - ) : null} @@ -624,9 +594,9 @@ export function GsvShell({ onResizeStart={shell.startChatDrag} onToggleOpen={() => shell.setChatOpen((value) => !value)} onToggleMax={shell.toggleChatMax} - onOpenCrew={() => openSettingsRoute("crew")} - onOpenModels={() => openSettingsRoute("models")} - onOpenTasks={() => openSettingsRoute("tasks")} + onOpenCrew={() => openShellSurface("crew")} + onOpenModels={() => openShellSurface("models")} + onOpenTasks={() => openShellSurface("runtime")} onProcessStarted={selectStartedChatProcess} onSelectConversation={setSelectedChatConversationId} title={activeChatProcess?.title ?? "Chat"} diff --git a/web/src/app/features/gsv-shell/desktop/GsvDesktop.tsx b/web/src/app/features/gsv-shell/desktop/GsvDesktop.tsx index 3cd12382a..49065a601 100644 --- a/web/src/app/features/gsv-shell/desktop/GsvDesktop.tsx +++ b/web/src/app/features/gsv-shell/desktop/GsvDesktop.tsx @@ -3,9 +3,10 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { AddAction } from "../../../components/ui/AddAction"; import { GsvMark } from "../../../components/ui/GsvMark"; import { DesktopHint } from "./DesktopHint"; -import { IconMenu } from "../../../components/ui/IconMenu"; +import { Icon } from "../../../components/ui/Icon"; import { ObjectCard } from "../../../components/ui/ObjectCard"; import { StatusDot } from "../../../components/ui/StatusDot"; +import { Tag } from "../../../components/ui/Tag"; import { Tile } from "../../../components/ui/Tile"; import { type DesktopChildObject, @@ -17,18 +18,17 @@ import { export type DesktopInventoryState = "ready" | "loading" | "offline" | "error"; -const DESKTOP_HINT = ["> CLICK A NODE TO EXPLORE", "> CLICK GSV FOR CONTROLS"]; -const DESKTOP_HINT_MIN = "CLICK A NODE TO EXPLORE · CLICK GSV FOR CONTROLS"; +const DESKTOP_HINT = ["> CLICK A NODE TO EXPLORE", "> CLICK GSV FOR SHIP OVERVIEW"]; +const DESKTOP_HINT_MIN = "CLICK A NODE TO EXPLORE · CLICK GSV FOR SHIP OVERVIEW"; type GsvDesktopProps = { desktopObjects: readonly DesktopObject[]; + /** Crew agents (machine accounts) — shown in the HUD beside the machine count. */ + agentCount: number; inventoryMessage: string; inventoryState: DesktopInventoryState; selectedObjectId: DesktopObjectId | null; - gsvOpen: boolean; onSelectObject: (id: DesktopObjectId | null) => void; - onToggleGsv: () => void; - onCloseGsv: () => void; onCreateObject: (id: DesktopObjectId) => void; onOpenObject: (child: DesktopChildObject) => void; onOpenSurface: (surface: ShellSurfaceId) => void; @@ -87,33 +87,33 @@ function addObjectLabel(id: DesktopObjectId): string { return "CONNECT NEW MACHINE"; } if (id === "integrations") { - return "NEW INTEGRATION"; + return "CONNECT NEW INTEGRATION"; } - return "NEW APPLICATION"; + return "CONNECT NEW APPLICATION"; } -function inventoryTitle(state: DesktopInventoryState, totalObjects: number): string { - if (state === "loading") { - return "GSV · LOADING"; - } +/** HUD title while the live inventory is not ready (ready shows counts). */ +function inventoryTitle(state: DesktopInventoryState): string { if (state === "offline") { return "GSV · OFFLINE"; } if (state === "error") { return "GSV · ERROR"; } - return `GSV · ${totalObjects} OBJECTS`; + return "GSV · LOADING"; +} + +function countLine(count: number, noun: string): string { + return `${count} ${noun}${count === 1 ? "" : "S"}`; } export function GsvDesktop({ desktopObjects, + agentCount, inventoryMessage, inventoryState, selectedObjectId, - gsvOpen, onSelectObject, - onToggleGsv, - onCloseGsv, onCreateObject, onOpenObject, onOpenSurface, @@ -137,42 +137,15 @@ export function GsvDesktop({ ? desktopObjects.find((object) => object.id === activeObjectId) ?? null : null; - // Hovering the GSV mark previews the controls; clicking persists them. A short - // grace delay lets the pointer travel from the mark into the popover. - const [gsvHovered, setGsvHovered] = useState(false); - const gsvHoverTimer = useRef | undefined>(undefined); - const openGsvHover = () => { - if (gsvHoverTimer.current) clearTimeout(gsvHoverTimer.current); - setGsvHovered(true); - }; - const closeGsvHover = () => { - if (gsvHoverTimer.current) clearTimeout(gsvHoverTimer.current); - gsvHoverTimer.current = setTimeout(() => setGsvHovered(false), 140); - }; - const closeGsvControls = () => { - if (gsvHoverTimer.current) clearTimeout(gsvHoverTimer.current); - setGsvHovered(false); - // Idempotent close — not the functional toggle. Outside-click also calls - // onSelectObject(null), which already clears gsvOpen in the parent; a toggle - // here would flip it back open within the same batched update. - if (gsvOpen) onCloseGsv(); - }; - useEffect(() => () => { - if (gsvHoverTimer.current) clearTimeout(gsvHoverTimer.current); - }, []); - const gsvActive = gsvOpen || gsvHovered; - const branchCount = Math.max(desktopObjects.length, 1); - const totalObjects = desktopObjects.reduce((sum, object) => sum + object.children.length, 0); - const desktopStateClass = `${selectedObject ? " has-selected-object" : ""}${gsvOpen ? " has-gsv-open" : ""}`; + const machineCount = desktopObjects.find((object) => object.id === "machines")?.children.length ?? 0; + const desktopStateClass = selectedObject ? " has-selected-object" : ""; const inventoryReady = inventoryState === "ready"; const hudStatus = selectedObject ? selectedObject.statusLabel - : gsvOpen - ? "GSV / CONTROL" - : inventoryReady - ? "SYSTEM MAP" - : inventoryState.toUpperCase(); + : inventoryReady + ? "SYSTEM MAP" + : inventoryState.toUpperCase(); // When a node is selected, settle the view so its objects are readable: scroll // up to centre the strip, but never past the point where the tiles row sits at @@ -253,7 +226,6 @@ export function GsvDesktop({ aria-label="GSV desktop" onClick={() => { onSelectObject(null); - closeGsvControls(); }} >