feat(ui): rebuild the logged-in shell, chat pane, dashboard widgets and sign-in - #226
Conversation
Persistent rail navigation, chat pane with sticky composer, native ECharts dashboard widgets, PinInput code step, and the defects folded into the work. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
Fourteen tasks across six phases, each with its failing test, code and commit, from the CSP font fix through the rebuilt shell, chat pane, native dashboard widgets, six-digit sign-in and the embedded-asset gate. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
The MCP app CSP only emitted font-src when a resource domain was declared, so the data: woff2 files the apps build inlines were blocked and every embedded view rendered in the system font. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
Router-free rail with New chat, the chat list with rename and delete, the dashboard list, and a search field. The old history drawer stays until the shell adopts the rail. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
…ent rail The rail is the only navigation: New chat, the chat list with rename and delete, the dashboard list, and search on Cmd-K. The footer, the Live dot and the Dashboard/Chat toggle are gone; sign out and the external links live in the account menu. The session no longer fetches a thread it has just named, which removes the 404 on every new chat. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
One grotesk for UI, prose and headings at weight 600, Geist Mono for code, ids, timestamps and numeric cells, tabular figures throughout. Headings no longer use the mono display face. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
The typeface swap changed the release dependency graph, so notices-check (part of `just check`) went stale. Regenerated with the go.mod toolchain. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
Mantine's Typography styles a bare code element as an inline chip, and that chip was applying inside a fenced block: one ragged background rectangle per line, at a smaller size than the pre around it. The block now inherits its background, padding and type from the pre. A rail row gave its title about a hundred pixels of a 219 pixel row because the time sat beside it and never shrank, so long titles were cut after a dozen characters and the cut point moved with the length of the time. Title and time are now stacked, and the title has the row to itself. The mobile drawer autofocused search whichever way it opened, so tapping the burger to read the lists raised the keyboard over them. The shell now remembers why the drawer opened and focuses search only for Cmd-K. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
…nt shrink The error-rate sparkline was a sibling of its tile, so it floated below the tile's border and left the two tiles in the row ending at different heights. A metric tile now takes children, draws them inside its own border, and fills its grid cell. The service map's force layout ran again from scratch on every thirty-second poll, so the nodes moved while the map was being read. The widget draws a circular layout, which puts a service in the same place every time. The chat view keeps the force layout: it is drawn once and never polled. The logs table let its body cell take the whole row and squeezed the severity badge to a coloured dot, which left severity as colour alone. The badge and service cells now hold their width. A card that did not fit its default width started a new row and left the columns beside it empty for the rest of the dashboard. Placement now offers the gap on the row at the width that is left, when the card can still be read at that width. The overview card's default height drops to three rows to match its content, and the dashboard's control row wraps instead of clipping "Add view" and "Ask Fanout" at 390px. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
Mantine's light-scheme dimmed grey is 3.3:1 on this ground, under WCAG AA, and dimmed is not decoration here: page descriptions, the updated time, rail times and section labels, the composer hint and healthy error rates all use it. The light scheme now takes the chart palette's muted grey, which clears AA at 4.8:1 and matches the axis labels beside it. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
A circle badge sizes itself to one character, so a two-digit count rendered as "1…" — a truncated number reads as wrong data. The count keeps its pill. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
…ne cleanups, dashboard search empty state A dashboard saved by a newer build can name a view this one has never heard of. The layout memo read a size off that missing key and the card looked up a body that was not there, so one unknown type blanked the whole page. The layout falls back to the overview size and the card renders a body that says the view is not supported. The trace widget computed its bar window from a span list that can be empty, where Math.min returns Infinity; the window now lives with the bars that use it. Its copy button holds the reset timer so a second click is not cut short by the first one's, and clears it when the card goes away. The sparkline registers the line chart it draws instead of relying on whichever widget imported one first, and holds its option steady across renders. The logs widget shows five entries as the spec says, and a dashboard search that matches nothing says so rather than leaving an empty section under the label. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
… entry point The embedded views rebuilt their chart on every render: init, setOption and dispose all lived in one effect keyed on the option object and the click handler, both of which callers pass inline. The wrapper now mirrors the host copy — one instance per mount, the handler through a ref registered once, and option changes applied in place with notMerge so a series that leaves the data leaves the chart. The font imports name index.css rather than leaning on the exports map. The ruling asked for the latin subset, but @fontsource-variable/geist 5.3.0 ships no per-subset stylesheet, so index.css is the same set of faces the bare specifier already resolved to. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
The spec said timestamps come from the AG-UI message when present, which no message carries; they are stamped in this session and nowhere else, so a restored thread shows the copy button without a time. The Direction comment pointed at ui/apps/src/contracts.ts, which is now ui/contracts.ts. Claude-Session: https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N
There was a problem hiding this comment.
🟡 Changes recommended
The new rail imperative focus handle is wired through a ref prop that won’t work on a function component, and useLastUpdated leaks a React Query subscription without unsubscribing.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR rebuilds the logged-in web UI around a persistent navigation rail, a full-height chat pane, and native dashboard widgets, while also updating the shared UI tokens/theme (notably the Geist typeface) and aligning embedded MCP views and server defaults to the new presentation.
Changes:
- Introduces a new AppShell-based layout with a left navigation rail (chats + dashboards + search) and updated chat routing/empty state/composer behavior.
- Replaces dashboard “widgets” with native ECharts-based cards (plus config/remove actions), and updates default widget sizing/placement on both client and server.
- Moves shared formatting/contracts/chart helpers into
ui/and updates embedded views + iframe CSP to correctly load inlined fonts.
File summaries
| File | Description |
|---|---|
| ui/tokens.ts | Updates font token stacks to Geist Variable / Geist Mono Variable. |
| ui/theme.ts | Aligns Mantine theme headings + dimmed color with new typography and chart palette. |
| ui/host/src/widgets/widgets.test.tsx | Adds integration-style widget tests with mocked fetch + EChart. |
| ui/host/src/widgets/widget-card.tsx | Introduces widget card chrome with actions/config modal and unknown-type fallback. |
| ui/host/src/widgets/trace.tsx | Adds Trace widget (summary tiles, mini waterfall, copy trace id). |
| ui/host/src/widgets/topology.tsx | Adds Topology widget using ECharts graph with stable node ordering and chat deep-link. |
| ui/host/src/widgets/pieces.tsx | Adds shared widget UI pieces (Metric, Empty, Error, Sparkline, HealthBadge). |
| ui/host/src/widgets/performance.tsx | Adds Performance widget (ops + p95 trend + top endpoints table). |
| ui/host/src/widgets/overview.tsx | Adds Overview widget (health/error rate tiles + sparkline + distribution bar). |
| ui/host/src/widgets/logs.tsx | Adds Logs widget (severity histogram + entries table). |
| ui/host/src/widgets/data.ts | Adds shared widget data/query helpers and “last updated” tracking hook. |
| ui/host/src/widgets/data.test.tsx | Tests widgetParams/config helpers and the update-time hook. |
| ui/host/src/widgets/configure.tsx | Adds per-widget configuration modal writing widget.config. |
| ui/host/src/widgets/assistant.tsx | Adds assistant widget with window-scoped question chips. |
| ui/host/src/widgets/activity.tsx | Adds activity widget (service table sorted by health/error rate). |
| ui/host/src/theme.test.ts | Tests the new typeface and light-scheme dimmed override. |
| ui/host/src/test-setup.ts | Adds happy-dom document.fonts stub for Mantine autosize behavior. |
| ui/host/src/shell.tsx | New AppShell layout: header + rail navbar + mobile drawer + account menu. |
| ui/host/src/routes/dashboards.index.tsx | Updates dashboard routes to use the new app context import. |
| ui/host/src/routes/dashboards.$dashboardId.tsx | Updates dashboard routes to use the new app context import. |
| ui/host/src/routes/chat.index.tsx | Routes /chat/ directly to ChatPage (draft chat behavior). |
| ui/host/src/routes/chat.$threadId.tsx | Routes /chat/$threadId to ChatPage from the new module. |
| ui/host/src/rail.tsx | New rail component: chat history + dashboards list + rename/delete + search. |
| ui/host/src/rail.test.tsx | Tests rail rendering, searching, and thread rename/delete flows. |
| ui/host/src/mcp-app-frame.tsx | Updates iframe CSP (font-src data:) and removes tool-specific min-heights. |
| ui/host/src/mcp-app-frame.test.tsx | Updates tests for new iframe min-height and CSP behavior. |
| ui/host/src/main.tsx | Switches loaded typeface packages to Geist variable families. |
| ui/host/src/index.css | Adds new rail/widget/chat pane CSS and tabular-nums base styling. |
| ui/host/src/echart.tsx | Adds host-side ECharts wrapper (single instance, resize observer, aria). |
| ui/host/src/echart.test.tsx | Tests EChart instance reuse and click handler routing. |
| ui/host/src/dashboard.test.tsx | Updates dashboard tests for new header behavior and widget placement. |
| ui/host/src/dashboard-layout.ts | Adds widget type list, per-type defaults, and “next free slot” placement. |
| ui/host/src/dashboard-layout.test.ts | Tests widget placement and default sizing invariants. |
| ui/host/src/chat.tsx | New chat pane UI: markdown chrome, empty state, composer, errors, embedded views. |
| ui/host/src/chat.test.tsx | Tests chat suggestions, markdown chrome, running state, retry/missing thread, clipboard failures. |
| ui/host/src/chat-history.tsx | Removes the legacy chat history drawer implementation. |
| ui/host/src/chat-history.test.tsx | Removes tests for the legacy chat history drawer. |
| ui/host/src/auth.test.tsx | Extends auth tests for new sign-in behavior and viewer exposure. |
| ui/host/src/app.test.tsx | Adds tests for draft chat behavior (no thread fetch) and restore retry semantics. |
| ui/host/src/app-context.ts | Introduces a shared app context API (activity labels, dashboard prompt, etc.). |
| ui/host/src/api.ts | Centralizes host API helpers/types and query keys used by rail/widgets. |
| ui/host/package.json | Swaps font dependencies, adds echarts to host app deps. |
| ui/host/bun.lock | Lockfile updates for new deps (Geist variable fonts, echarts, tslib changes). |
| ui/format.ts | Adds shared formatting helpers (percent/duration/windowLabel/timestamps). |
| ui/contracts.ts | Adds shared TS contracts for observability payloads + envelopes. |
| ui/chart.ts | Adds shared chart/status helpers for host + embedded views. |
| ui/apps/src/trace.tsx | Updates embedded trace view to import shared helpers/contracts and adds retry UI. |
| ui/apps/src/topology.tsx | Updates embedded topology view to import shared helpers/contracts and adds retry UI. |
| ui/apps/src/performance.tsx | Updates embedded performance view to import shared helpers/contracts and adds retry UI. |
| ui/apps/src/overview.tsx | Updates embedded overview view to import shared helpers/contracts and adds retry UI. |
| ui/apps/src/logs.tsx | Updates embedded logs view to import shared helpers/contracts and adds retry UI. |
| ui/apps/src/echart.tsx | Aligns embedded EChart wrapper behavior with host (instance reuse, latest click handler). |
| ui/apps/src/components.tsx | Updates embedded app shell/header/status UI + swaps to Geist variable font CSS. |
| ui/apps/package.json | Swaps embedded app font dependencies to Geist variable families. |
| ui/apps/bun.lock | Lockfile updates for embedded app font dependency swap. |
| THIRD_PARTY_NOTICES | Updates notices for font packages (IBM Plex → Geist). |
| internal/ui/dist/index.html | Updates compiled asset references after UI rebuild. |
| internal/ui/dist/assets/useNavigate-BEpS2iE5.js | Removes an old compiled asset. |
| internal/ui/dist/assets/Table-BcfmFrO2.js | Adds a new compiled asset. |
| internal/ui/dist/assets/routes-UYU1YEPx.js | Removes an old compiled asset. |
| internal/ui/dist/assets/routes-BnFL5jvM.js | Adds a new compiled asset. |
| internal/ui/dist/assets/dashboards.index-DzfpVEyI.js | Removes an old compiled asset. |
| internal/ui/dist/assets/dashboards.index-B2QXPF6y.js | Adds a new compiled asset. |
| internal/ui/dist/assets/dashboards._dashboardId-KKL4Xn45.js | Adds a new compiled asset. |
| internal/ui/dist/assets/dashboards._dashboardId-Cd4hot0K.js | Removes an old compiled asset. |
| internal/ui/dist/assets/chat.index-BdqtxCeK.js | Adds a new compiled asset. |
| internal/ui/dist/assets/chat.index-BCfx7LKm.js | Removes an old compiled asset. |
| internal/ui/dist/assets/chat._threadId-CzfhQ5YC.js | Removes an old compiled asset. |
| internal/ui/dist/assets/chat._threadId-BdqtxCeK.js | Adds a new compiled asset. |
| internal/observability/contracts.go | Updates comment reference to the new shared TS contract path. |
| internal/dashboard/service.go | Updates server default dashboard layout to match new widget sizes. |
| internal/dashboard/service_test.go | Adds test asserting server default layout matches client widget sizing/validation. |
| internal/agent/runtime.go | Updates system prompt to forbid text diagrams/restating attached views. |
| internal/agent/runtime_test.go | Adds test asserting the new system prompt constraints are present. |
| docs/superpowers/specs/2026-09-05-logged-in-ui-shell-design.md | Adds the UI shell rebuild spec document. |
Review details
Suppressed comments (1)
ui/host/src/rail.tsx:48
- Even after renaming the prop in
RailProps, the component still destructuresrefand passes it touseImperativeHandle. Update the parameter name anduseImperativeHandlecall to match the renamed prop (or use a real forwarded ref).
- Files reviewed: 61/120 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
There are a few correctness issues in new UI code (notably unguarded async state updates in clipboard copy handlers and non-deterministic thread-group ordering) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 61/120 changed files
- Comments generated: 3
- Review effort level: Lite
CopyButton in chat.tsx scheduled a bare setTimeout it never held or cleared, so a second click inside the 1500ms window got cut short by the first click's reset timer, and nothing was cleared on unmount. CopyTraceID in trace.tsx already had the right shape (ref-held timer, cleared on unmount and before rescheduling); extract it into a shared useCopy hook in copy.ts and have both call sites use it. Claude-Session: https://claude.ai/code/session_014P9NvJjVkRiJPpdKevKkNE
groupThreads returned sections in whatever order the Map first saw their labels in, which followed the order threads arrived from the API rather than a stable most-to-least-recent order. Fix the section order to Today, Yesterday, Previous 7 days, Older via a module-level constant, skipping labels with no threads. Claude-Session: https://claude.ai/code/session_014P9NvJjVkRiJPpdKevKkNE
There was a problem hiding this comment.
🔵 Needs a closer look
The change set is very large (new shell + navigation + chat + widget system + dependency/build artifact updates), so it needs final human review despite only minor actionable issues found here.
Review details
- Files reviewed: 62/121 changed files
- Comments generated: 2
- Review effort level: Lite
rel=noreferrer already implies noopener per the HTML spec, so this changes no behaviour; the pair is the idiom readers and scanners expect. Claude-Session: https://claude.ai/code/session_014P9NvJjVkRiJPpdKevKkNE
There was a problem hiding this comment.
🟡 Changes recommended
A few small but concrete issues remain (Burger aria-label doesn’t reflect open/close state, avatar initial can become empty, and Widget type narrowing forces unsafe casts for unknown widget types).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 62/121 changed files
- Comments generated: 4
- Review effort level: Lite
The header Burger always said "Open navigation" even while the drawer was open, and AccountMenu's avatar initial fell back to email or "?" only when name was falsy, so a whitespace-only name won the `||`, trimmed to empty, and rendered a blank avatar. The Burger now flips its aria-label with drawerOpened, and the initial is computed from trimmed candidates so an empty one correctly falls through to the next. Claude-Session: https://claude.ai/code/session_014P9NvJjVkRiJPpdKevKkNE
Widget.type claimed WidgetType, an exhaustiveness the code never had — WidgetCard already falls back to UnknownWidget for a type it does not recognize, and dashboard.tsx needed two `as` casts just to hand it a record. Widget now aliases DashboardWidgetRecord (type: string, the truth of what the server can send), and each lookup into a WidgetType-keyed map goes through a small Object.hasOwn guard instead of a cast, so the maps themselves stay exhaustive over WidgetType. Claude-Session: https://claude.ai/code/session_014P9NvJjVkRiJPpdKevKkNE
There was a problem hiding this comment.
🔵 Needs a closer look
The change set is a broad UI/UX rebuild spanning routing, layout, widgets, shared contracts, and rebuilt dist artifacts, so it warrants final human review despite strong test coverage.
Review details
- Files reviewed: 62/121 changed files
- Comments generated: 0 new
- Review effort level: Lite
What changed
The logged-in app is rebuilt around a persistent navigation rail, with the chat pane, dashboard widgets, sign-in code step and typeface reworked. Spec:
docs/superpowers/specs/2026-09-05-logged-in-ui-shell-design.md. Plan:docs/superpowers/plans/2026-09-05-logged-in-ui-shell.md(17 tasks, each reviewed).mdthe rail lives in a drawer behind a burger.font-src 'self' data:in the frame CSP), size to their content from a 240px floor, and carry a slimmer header with an icon refresh and a Retry on error.widget.config, per-type default sizes, and free-slot placement that shrinks to the space left on the row. The page header drops Refresh, Saved and the Dashboards dropdown; data refetches every 30 s with an "Updated" line. The server's default layout matches the new sizes.PinInputthat verifies on the sixth digit, a Verify button, Resend with a 30 s cooldown, Change email, "Check your email" copy, headings at 28/32px; setup success copies the wholeAuthorizationheader line.format.ts,contracts.tsand the chart helpers moved toui/for both bundles.Defects fixed on the way: embedded views in the system font, "994.6 Ms", the per-chat 404, header clipping at 390px, the 3 s dashboard poll, a spinner-forever on thread-load failure, an unhandled clipboard rejection, a countdown timer that never stopped after sign-in.
Verification
just checkon 971d21f: all checks passed (host suite 66 tests across 12 files, apps and hosttsc, Go tests, notices, docs and site checks).Manual pass on a local instance with 60 minutes of seeded traffic at 1440x900 and 390x844, light and dark: shell, chat with embedded views and markdown tables, dashboard with all seven widget types, configure and remove, sign-in code step.
A dedicated UX walk of every screen produced 51 findings; the 15 Important ones are fixed in this branch, the 36 Minor ones are listed below.
just checkjust test-racenot run: auth, API, ingest, query and MCP paths are unchanged apart from a one-sentence system-prompt edit and a default-layout constant, both covered bygo test.User-facing behavior and configuration docs are current (spec and plan committed under
docs/superpowers/)No credentials, private telemetry, host details, or enterprise-only source are included
API, migration, ingest, MCP/AG-UI, or release-contract changes are called out: none. Existing dashboards keep their stored layouts (pre-release, no migration); the server default only applies to new owners.
Follow-ups (not in this PR)
GET /api/agent/threads/{id}returns only the last run's activity message, so a restored thread loses earlier embedded views.site/to Geist so app and site match.ui/appsfont subsets: the fontsource variable package ships nolatin.css; hand-written@font-facerules would cut about 34 KB per bundle.https://claude.ai/code/session_015K38gEgWmGkXJyyC8GiZ8N