From a37bb06c8d7410448a98a24e3087205d54c59b26 Mon Sep 17 00:00:00 2001 From: Dinesh Pandiyan Date: Wed, 21 Oct 2026 18:13:50 +1100 Subject: [PATCH] feat: server status indicator and consistent header interactions - replace CPU/RAM chip with icon-only server status popover - add --shell-hover so header controls show hover on shell canvas - move header into shell top gutter to center controls vertically --- DESIGN.md | 18 ++++- apps/web/src/DiffWorkspace.tsx | 4 +- apps/web/src/ServerMetrics.tsx | 51 ------------- apps/web/src/ServerStatus.tsx | 123 ++++++++++++++++++++++++++++++ apps/web/src/style.css | 73 +++++++++++++----- apps/web/src/tokens.css | 2 + apps/web/test/browser/app.spec.ts | 76 +++++++++++++++--- 7 files changed, 264 insertions(+), 83 deletions(-) delete mode 100644 apps/web/src/ServerMetrics.tsx create mode 100644 apps/web/src/ServerStatus.tsx diff --git a/DESIGN.md b/DESIGN.md index 29580b7d..6d570a34 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -102,6 +102,7 @@ measured elements or replace the application tokens with framework defaults. | Control boundary | `--border-control` | Inputs and controls requiring stronger boundaries | | Emphasized boundary | `--border-strong` | Rare boundaries requiring emphasis beyond focus or error styling | | Neutral hover | `--hover` | Available controls under the pointer, compact count surfaces | +| Shell hover | `--shell-hover` | Hover/expanded header controls on `--shell-canvas` | | Accent | `--accent` | Selection, focus, navigation links, primary commit action | | Accent interaction | `--accent-hover` | Hover/active on filled primary actions | | Selected surface | `--accent-bg` | Selected file or pressed toggle | @@ -120,7 +121,7 @@ measured elements or replace the application tokens with framework defaults. - Status always includes text, a sign, an icon, or a shape. Preserve staging's empty/half/full dot distinction; its half-fill is information, not decoration. - CPU and RAM in the status bar describe the Diffx server process, not - the browser or whole machine. Keep that scope explicit in the tooltip. + the browser or whole machine. Keep that scope explicit in the status popup. - Pierre owns syntax colors, changed-line fills, word highlights, line selection, and code-theme surfaces. App status tokens govern the surrounding UI only. - Quiet frames use the light canvas/dot tokens or their dark equivalents from @@ -204,7 +205,9 @@ such detail into a token. New layout spacing must use the scale. Keep these frames outside Pierre's measured file geometry. At compact widths, remove the empty navigation island and retain the inset review surface; modal drawers use the same inset and corners without clipping focus or menus. -- Header: `--topbar-height` (36 desktop, 44 narrow or coarse pointer). Keep +- Header: `--topbar-height` (36 desktop, 44 narrow or coarse pointer). The + header replaces the shell's top gutter, sitting flush with the window edge + directly above the islands, so its controls center between both edges. Keep desktop controls at 32px, header icons at 16px, the divider at 16px, and group spacing at 6px so the header recedes into the editor chrome. Toolbar and sidebar tabs share the compact 36px desktop rhythm (`--control-small` plus two @@ -220,7 +223,8 @@ such detail into a token. New layout spacing must use the scale. - Header uses `--shell-canvas`, a quiet brand, and the review identity as its primary context. Keep the secondary scope heading and muted monospace path inline; hide the path at 768–1011px and both below 768px. Group ghost Refresh and - sync actions at the trailing edge; share `--hover` feedback with the trigger. + sync actions at the trailing edge; share `--shell-hover` feedback with the + trigger, since `--hover` is indistinguishable from the shell canvas. Theme is the rightmost header action in every layout, with the same ghost styling, 32px desktop hit area and 16px icon. Keep its outer edge flush with the shell; never apply the diff scrollbar inset to header actions. Snapshot deletion is absent @@ -272,7 +276,13 @@ such detail into a token. New layout spacing must use the scale. Keep the same-stream notice available. Unchanged submissions must not create unread prompts. Use existing ghost-button typography, icons and accessible labels; preserve the header's review identity and compact-layout controls. -- Status bar: `--statusbar-height` (28), with shortcuts shed before server metrics. +- Status bar: `--statusbar-height` (28), with shortcuts shed before server status. + Server status is an icon-only compact ghost trigger at the trailing edge: + success check when ready, reduced-motion-safe spinner while connecting, and + error alert when metrics are unreachable; its accessible name carries the + state. Hover, focus plus Enter, or tap opens a `--surface-raised` popover + titled by the state, with a “Diffx server process” section listing CPU and RAM. + Add future system details as further sections in the same popover. - The diff display toolbar uses `--panel`, matching the header and status bar. - Desktop navigation panel: `--sidebar-width` defaults to 330px. `Navigation.tsx` owns the shared frame and resize handle for Diff and Repos; `use-sidebar.ts` owns diff --git a/apps/web/src/DiffWorkspace.tsx b/apps/web/src/DiffWorkspace.tsx index fa9b7df8..247fcffe 100644 --- a/apps/web/src/DiffWorkspace.tsx +++ b/apps/web/src/DiffWorkspace.tsx @@ -39,7 +39,7 @@ import { placementFor, type CommentAnnotation, } from "./review-model.ts"; -import { ServerMetrics } from "./ServerMetrics.tsx"; +import { ServerStatus } from "./ServerStatus.tsx"; import { WorkspaceFeedback } from "./WorkspaceFeedback.tsx"; import { capabilityEnabled, useSession } from "./session-context.tsx"; @@ -840,7 +840,7 @@ export function DiffWorkspace() { )} - + diff --git a/apps/web/src/ServerMetrics.tsx b/apps/web/src/ServerMetrics.tsx deleted file mode 100644 index fafcb2a5..00000000 --- a/apps/web/src/ServerMetrics.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { api, type ApiServerMetrics } from "@diffx/api"; -import { useEffect, useState } from "react"; -import { formatBytes } from "./server-metrics.ts"; - -const refreshMilliseconds = 2_500; - -export function ServerMetrics() { - const [metrics, setMetrics] = useState(null); - useEffect(() => { - let disposed = false; - let controller = new AbortController(); - async function refresh() { - controller.abort(); - controller = new AbortController(); - try { - const { data } = await api.GET("/api/v2/metrics", { - signal: controller.signal, - }); - if (!disposed && data) setMetrics(data); - } catch {} - } - void refresh(); - const timer = window.setInterval(() => void refresh(), refreshMilliseconds); - return () => { - disposed = true; - controller.abort(); - window.clearInterval(timer); - }; - }, []); - - const cpu = metrics ? `${metrics.cpuUsage.toFixed(1)}%` : "--%"; - const ram = metrics ? formatBytes(metrics.rssBytes) : "-- MB"; - const title = metrics - ? `Diffx server process\nResident RAM (RSS): ${ram}\nCPU usage: ${cpu}` - : "Diffx server process metrics"; - return ( - - - CPU - {cpu} - - - - RAM - {ram} - - - ); -} diff --git a/apps/web/src/ServerStatus.tsx b/apps/web/src/ServerStatus.tsx new file mode 100644 index 00000000..faad4bed --- /dev/null +++ b/apps/web/src/ServerStatus.tsx @@ -0,0 +1,123 @@ +import { Popover } from "@base-ui/react/popover"; +import { api, type ApiServerMetrics } from "@diffx/api"; +import { + CircleAlertIcon, + CircleCheckIcon, + LoaderCircleIcon, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { formatBytes } from "./server-metrics.ts"; + +const refreshMilliseconds = 2_500; + +type ServerHealth = "checking" | "ready" | "unavailable"; + +const statusLabels: Record = { + checking: "Connecting", + ready: "Ready", + unavailable: "Server unavailable", +}; + +export function ServerStatus() { + const [health, setHealth] = useState("checking"); + const [metrics, setMetrics] = useState(null); + useEffect(() => { + let disposed = false; + let controller = new AbortController(); + async function refresh() { + controller.abort(); + controller = new AbortController(); + const { signal } = controller; + try { + const { data } = await api.GET("/api/v2/metrics", { signal }); + if (disposed) return; + if (data) { + setMetrics(data); + setHealth("ready"); + } else setHealth("unavailable"); + } catch { + if (!disposed && !signal.aborted) setHealth("unavailable"); + } + } + void refresh(); + const timer = window.setInterval(() => void refresh(), refreshMilliseconds); + return () => { + disposed = true; + controller.abort(); + window.clearInterval(timer); + }; + }, []); + + const label = statusLabels[health]; + const current = health === "ready" ? metrics : null; + const cpu = current ? `${current.cpuUsage.toFixed(1)}%` : "--"; + const ram = current ? formatBytes(current.rssBytes) : "--"; + return ( + + + } + > + + + + + + + + {label} + + {health === "unavailable" && ( + + Can’t reach the diffx server. Retrying automatically. + + )} +
+

Diffx server process

+
+
+
CPU
+
{cpu}
+
+
+
RAM
+
{ram}
+
+
+
+
+
+
+
+ ); +} + +function StatusIcon({ health }: { health: ServerHealth }) { + const Icon = + health === "ready" + ? CircleCheckIcon + : health === "checking" + ? LoaderCircleIcon + : CircleAlertIcon; + return