feat(studio): steering, threads, mock tour, and the schedules/skills/settings overhaul - #668
Closed
jtenniswood wants to merge 104 commits into
Closed
jtenniswood wants to merge 104 commits into
jtenniswood wants to merge 104 commits into
Conversation
Vendor drop of the Atrium workspace UI from stacklok/enterprise-ui-prototypes @ claude/atrium-ui-updates (649bc1efb91f889e117c79f4189d8abba34c22d4), filtered before import so prototype residue never enters this repo's history. Every imported file is byte-identical to its source. Excluded at import (413 of 572 tracked files): - auth stack (better-auth/pg/OIDC), signin flow, permissions context - legacy ai-sdk assistant sidebar, /api/chat, MCP client wiring - MSW mock server, mock OIDC/registry/user-management routes, hey-api generated clients and their OpenAPI specs - Atrium demo data (features/agent/mock-data.ts, skills fixtures): Studio is daemon-only; probe failure renders offline, never demo data - disk-touching harness routes (skills/agents authoring deferred to a follow-up; transcript route superseded by the daemon transcript API) - prototype deploy/repo infra (Docker, helm, GitHub workflows, husky, renovate, pnpm lockfile/workspace) and residue tests/e2e specs The app does not build at this commit; wiring lands in the commits that follow. The complete filter manifest is recorded in the pull request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eight imported files carried `SPDX-License-Identifier: Proprietary` headers — stray artifacts of the source repository's enterprise-platform sync that contradicted its own Apache-2.0 LICENSE. This repository has no SPDX header convention of its own; the headers are corrected to Apache-2.0 — the license the source repository actually grants this content under — rather than dropped, so the imported files keep their provenance visible. Kept separate from the vendor drop so the relicensing is auditable on its own. A CI grep gate (added with the studio workflow later in this series) rejects Proprietary headers so they cannot regress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turn the imported prototype into mecatl-studio: - package.json: rename, prune every residue dependency (ai-sdk stack, better-auth/pg, msw/express/oidc-provider, hey-api, dexie, redoc, archiver/tar-stream, ...), npm with a regenerated package-lock.json, pnpm security overrides translated to npm `overrides` (kept only for packages still present in the tree), engines >=22.13, .nvmrc 22.22.0, drop husky/lint-staged (git hooks in a Node subdir of a Go monorepo would hijack .git/hooks for every contributor; CI is the enforcement point). - next.config.ts: keep the strict security headers; drop standalone output, mock rewrites, tracing includes, and the /api/proxy CSP carve-out (all served the deleted deployment). - Flatten the (authenticated) route group: its only job was verifySession. src/app/workspace/layout.tsx keeps the shell without PermissionsProvider/assistant/config-server gates. - client-providers: drop the connector-status demo context; keep next-themes + Toaster + the Suspense boundary useSearchParams needs. - Root layout/metadata rebranded to Mecatl Studio; mock-scenario dev panel unmounted. - vitest/knip/tsconfig/biome/playwright configs trimmed of deleted paths (MSW setup, auth env pins, @api/@Mocks aliases, generated-file overrides); .npmrc (pnpm-only settings) removed. - src/lib/utils.ts reduced to cn(): the other helpers parsed ToolHive registry metadata via the deleted generated types (their test file covered only those helpers and goes with them). - Restore three files the import filter over-matched, byte-identical to the source branch: workspace/_components/{chat-input.tsx, composer-mentions.ts,resize-handle.tsx} and src/components/brand-logo.tsx (+ test). - TEMPORARY typed-empty shims for the excluded demo fixtures (features/agent/mock-data.ts, skills/_data/skills.ts) keep the not-yet-converted hooks compiling; the daemon-only hook rewrite deletes both. The mock-data-bound e2e spec and search-data test are removed; their daemon-backed replacements land with the test suite later in this series. next build, tsc --noEmit, biome check, and vitest (44 tests) are green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the prototype's permissive /api/harness* proxies with the Studio server tier from feat/studio-module (PR #548), carried near-verbatim: - src/lib/server-proxy.ts — Host/Origin/CSRF trust gate, header allowlists in both directions, server-side bearer injection in external mode, managed/external mode switch (MECATL_BASE_URL presence), controller mutations tagged with the server-set x-mecatl-studio-request header, daemon-unreachable mapped to a friendly 503. New here: workspace injection for POST /v1/sessions and /v1/teams (managed: from controller /status; external: from MECATL_WORKSPACE), adopted from the prototype's proxy so the browser never learns or chooses the workspace path. - src/lib/controller-security.mjs — verbatim: controller CSRF + DNS-rebinding policy and MCP gateway egress validation. - scripts/local-controller.mjs — verbatim (import path only): the managed-mode supervisor. Random loopback daemon port, generated bearer, restart/rollback queue, model-router and operator settings, MCP gateway OAuth with the unguessable loopback proxy. - scripts/dev-local.mjs — verbatim: process orchestration and the managed/external switch; npm scripts now match the old studio's (dev, dev:web, mecatl, start --production). Routes are the old studio names: /api/mecatl/[...path] and /api/mecatl-control/[...path]; the browser client only changes its two base-path constants. In external mode /api/mecatl-control synthesizes /status and answers 409 for every other control path. Env inventory is the old studio's exactly (MECATL_BASE_URL, MECATL_AUTH_TOKEN, MECATL_WORKSPACE, MECATL_STUDIO_PUBLIC_ORIGIN, MECATL_STUDIO_ORIGINS, MECATL_STUDIO_PROVIDER, MECATL_ALLOW_INSECURE_LOOPBACK_MCP); the prototype's MECATL_URL, MECATL_CONTROL_URL, MECATL_ALLOW_REMOTE, MECATL_SKILLS_DIR and MECATL_STORE_DIR are gone, documented in .env.example. The client functions still pointing at the removed disk routes (/api/harness-skills, /api/harness-agents) are pruned with the hook rewrites later in this series. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One wire seam under src/lib/protocol/, the only reader of raw daemon JSON. The base is the old studio's tested decoders; the session and schedule halves re-land the closed PRs #615 and #616 designs; the event translation replaces the prototype's looser inline translate(). - events.ts: parseMecatlEvent (structural, throw-on-missing-type) + translateEvent. The terminal `result` frame now always reaches the UI: stop, error text, and permanence ride a run_result StreamEvent — a failed turn must render as failed, never as an empty success. Usage carries all five token fields. permission.retract withdraws its ask. subagent.start / team.start roster / parallel.branch starts become delegation badges; advisory kinds (tool.progress, compaction, provider.route, recover_notice, no_progress) become one-line notices; and an UNKNOWN event kind is surfaced as "not rendered yet", never silently dropped. - sessions.ts (#615): SessionSummary with capability-driven actions (omitted capability = denial, closed per-action reasons, inspect_only_kind as the one not-a-chat reason), bounded-cursor inventory pages, and the authoritative message-level transcript decoder. - schedules.ts (#616): full spec decode (protojson AND stdlib-JSON shapes for timestamps, durations, enums), fire-stage inference with the "pending" claim sentinel, fire-history decoding (newest first, in-flight keyed off the absent stop), and encodeScheduleSpec — the protojson request builder with the carried-spec round-trip, because PUT replaces the whole spec and responses can never be echoed back as requests. client.ts rides the seam: streamHarnessPrompt gains the 120s idle timeout and the saw-result guard (a stream that closes without a terminal frame throws); approvals send the daemon's three-way verdict (allow_once / allow_always / deny); new wrappers cover the session inventory walk, rename (adopting the clamped title echo), delete, transcript, schedule rows/fires/save, and slash-command discovery (workspace query injected by the proxy, like session bodies). 26 decoder/translation tests pin the invariants, including the protojson-request/stdlib-response asymmetry and the carried-field round-trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete the demo layer entirely: the mock-data shim is gone, and no hook fabricates content when the daemon is away. An unreachable daemon is now a rendered state, never demo data. - RuntimeStatusProvider (features/agent/runtime-status.tsx): one connection authority polling the daemon and controller every 5s, replacing eleven independent one-shot probes that could disagree and never noticed a daemon dying after mount. Offline renders a shell banner naming the fix, with Retry; surfaces gate their loads on it and reload on reconnect. It also refreshes the composer capabilities on every reconnect. - use-agent-sessions: the chat list is the daemon's session store. Bounded cursor walk + 20s poll; rows removed only when a COMPLETE walk proves them gone; non-chat rows (subagents, team members, scheduled fires) filtered by the decoder's inspect_only_kind rule; rename optimistic with rollback, adopting the daemon's clamped title echo; delete daemon-backed with the 404-only-removal rule; action eligibility read from row capabilities, never re-derived. - use-agent-chat: the sidebar id IS the daemon session id — the lazy UI-id→daemon-id map is gone. Opening a chat rehydrates from the authoritative transcript endpoint (incomplete transcripts say so). A null id is a draft whose session is minted on first send. Handles the full translated stream: failed turns render failed (permanent errors say so), permission retractions withdraw their ask, notices and delegation badges accumulate on the turn, usage keeps all five token counts, and retryLast() resends the last prompt after a failure. - use-agent-cron: schedule registry only, via the protocol decoders; "scheduler not wired" (the daemon's own error) is distinct from an empty registry; every action re-reads durable state; quick-create builds a plan-mode, non-mutating spec; full rows (mode, mutating, workspace, carried spec) exposed for the schedules surface. - use-agent-memory: user-model index with size/sha metadata; a daemon running --no-user-model is a distinct disabled state; the mock edit path is gone (memory is read-only by construction). - use-agent-roster: the daemon's resolved subagent inventory. - use-agent-projects: placeholder — the daemon has no project concept; the grouping UI goes with the sidebar rework. - composer-capabilities: @-mentions from GET /v1/agents and /-commands from GET /v1/commands (workspace query injected by the proxy, which now also injects workspace into schedule creation bodies). - atrium-search-data: fixture index removed; the live index lands with the surface wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chats — the flat server-backed workspace. The project/agent grouping and every demo flow are gone; the sidebar is one recency-bucketed list of daemon sessions (Today / This week / Earlier) with rename/delete gated by row capabilities and the daemon's refusal reasons shown, never re-derived. "New chat" is a draft route; the daemon session is minted on first send and the URL adopts its id. Failed turns render as destructive alerts (never empty successes), notices and delegation badges ride the turn, approvals offer the daemon's real three-way verdict, an error strip offers Retry, and the header shows live token usage. Scheduled — full authoring and audit. The create/edit dialogs share one form: cron + IANA timezone + max fires XOR one-shot + retry, and an explicit "allow file and shell writes" opt-in that couples mutating with a write-capable mode so the invalid pairing is unconstructible; edits seed from the stored row and round-trip its carried spec (PUT replaces the whole spec). Rows carry mode/mutating/workspace/owner badges and fire-stage indicators; the detail page shows the real fire log (newest first, in-flight tracked, refusals verbatim) with per-fire transcripts; "not wired" is distinguished from "empty" using the daemon's own words. Skills — the resolved inventory, read-only: provenance badges (learned vs workspace), and an honest note where SKILL.md content used to be — the client tier has no body-read endpoint. Memory — three honest states (disabled with the daemon's reason, empty, populated), store footprint (bytes + sha prefix), no editing affordances, and no fabricated timestamps. Settings — appearance/notifications plus the runtime sections: provider status (read-only; credentials never enter Studio), the full model router editor with the operator-settings lock, and MCP gateway connect with a user-entered, controller-validated URL (bearer or OAuth with status polling). External mode renders all three as owned by the deployment. Global search indexes live data (sessions, schedules, skills, memory keys) built per palette open; transcripts are not indexed. Rides a new hermetic server-tier suite (tests/rendered-html.test.mjs): a real production build in external mode against a fake recording daemon proves bearer injection, workspace injection, the CSRF 403, the external-mode 409 policy, and that an unreachable daemon is a friendly 503 — plus the controller CSRF/DNS-rebinding truth table and gateway egress validation. Dead prototype residue flagged by knip is deleted (fixture data, orphaned components, unused exports, date-fns/zod). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five Playwright smokes drive the production build in external mode against a fixture mecated (tests/e2e/fixture-daemon.mjs) — a test double behind the API seam, speaking the daemon's real wire shapes (stdlib-JSON responses, SSE prompt relay), which is not the UI-level demo fallback Studio forbids. The specs assert daemon-sourced renders: the chat list and rehydrated transcript, schedule posture badges, the skill inventory, the read-only user model, and external mode marking runtime settings as deployment-owned. The suite immediately earned its keep: the browser client's base path had silently lost its /v1 prefix in the proxy graft (the prototype's proxy used to rewrite /api/harness/* onto /v1/*; the grafted tier is transparent), so every daemon call 404'd in a real browser while unit and server-tier suites — which speak full paths — stayed green. The prefix now lives in the client's own URLs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- .github/workflows/studio.yml: path-scoped to studio/**; SHA-pinned actions; Node from studio/.nvmrc with the npm cache; installs run --ignore-scripts (nothing in the tree needs install scripts, so the supply-chain surface stays off in CI). Job "checks" runs biome, tsc, knip, the vitest suite, the production build plus the hermetic server-tier suite, npm audit --audit-level=high, and a grep gate rejecting Proprietary SPDX headers. A separate "e2e" job runs the Playwright fixture-daemon smokes so a browser-infra flake never masks the checks. - dependabot: the repo's first npm entry, for /studio (weekly, minor+patch grouped, matching the gomod grouping style). - Taskfiles: the root include gains the studio namespace; the studio Taskfile carries the supervisor-aware dev/stop/restart/status tasks (stop kills the controller first so it tears down its mecated instead of orphaning it) with test/lint retargeted to the new stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ADR 0228 — Studio: the Atrium workspace as mecatl's daemon-only web client. The module boundary (a Node module, never a Go module), the two pure deployment modes, the daemon-only rule (offline is a rendered state, never demo data), the single typed wire seam, the inherited security posture, the deliberate non-features, and the toolchain. Subsumes the unmerged ADR drafts from PR #548. - ADR 0229 — Studio's chat list is the daemon's session store: the sidebar id IS the daemon session id, complete-walk removal, clamped rename echoes, capability-driven actions, draft-on-first-send, and the stated costs (mtime reordering, synthetic transcript turns, no live re-attach over HTTP). Re-lands the design from PR #615. - AGENTS.md gains the studio component bullet with the same-PR wire-break rule; docs/architecture.md gains the Studio client section; PRODUCTION-READINESS gains the Studio row with its open deferrals; user-docs/what-you-get/studio.md is the operator guide; studio/CLAUDE.md carries the rules-that-have-teeth list, each named to its backing test. llms.txt regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's Node 22 ships npm 10, which rejected the lockfile a local npm 11 wrote (`npm ci`: missing transitive typescript peer). The lockfile is regenerated with npm@10.9.4 — the toolchain CI actually runs — and `npm ci --ignore-scripts` is verified against it. The audit gate then flagged the postcss vendored inside next@16.2.11 (GHSA-qx2v-qp2m-jg93 and friends, high): fixed upstream in next@16.3.1, the version the previous studio module already ran. Bumped; the react-compiler build, unit, hermetic, and e2e suites all pass on it, and `npm audit --audit-level=high` exits clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The freshness gate regenerates llms.txt and diffs; the Studio row added to PRODUCTION-READINESS after the previous regeneration shifted the link graph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lish
Settings becomes five subpages behind a left secondary nav (Appearance,
Notifications, Provider, Model router, MCP gateway) instead of one long
scroll, each still backed by the same shared useHarnessRuntime instance
so the runtime sections share one status/busy/error channel.
New browser-local identity preferences (src/lib/profile-preferences.ts —
cosmetic only, nothing here has a daemon concept to write back to):
- Agent display name (defaults to "Mecatl", was hardcoded "Assistant"
everywhere), editable from Settings → Appearance.
- User avatar: a person-icon placeholder by default, replacing the
prototype's broken remote portrait (blocked by our own CSP — this was
also silently killing the whole message list, since the crash inside
<UserAvatar> was swallowed by an error boundary with the transcript
rehydrated behind it). Upload stores a small local data URL; no
daemon endpoint exists or is implied.
Both render in a new two-group "Profile" card ("You" / "Agent").
Provider settings gain real visibility without adding a credential
path: the controller now line-scans auth.yaml for provider block NAMES
only (never values, never a full YAML parse) and reports them plus
which one MECATL_STUDIO_PROVIDER currently selects. The Provider page
lists them, marks the active one, and states the exact file + env var
to add or switch — credentials still never cross the browser/controller
boundary (ADR 0228), so no add/remove-provider UI was built.
provider.route events move from an advisory notice to silent: routing
detail ("Azure", a raw provider name) was leaking under assistant
turns as noise with no actionable content.
Empty-state polish: Skills and Scheduled get an icon, human copy, and
(Scheduled) a working create CTA inline, replacing a bare line of
muted text. Memory drops its footer store-footprint line, its two
explainer paragraphs on the list and detail pages, and the Refresh
button (the runtime-status provider already reloads it on reconnect).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gs e2e The settings-subpages commit carried a package-lock.json regenerated by a local npm 11, which deduped away a transitive typescript entry npm 10 requires — the same desync fixed once before. Regenerated with npm@10.9.4 and pinned `packageManager: "npm@10.9.4"` so corepack-aware setups stop reintroducing it. The same commit moved the runtime settings sections to their own subpages, which orphaned the external-mode e2e assertion still visiting /workspace/settings; it now targets /workspace/settings/provider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sion list 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 <noreply@anthropic.com>
…ist bound The conversation and composer keep the 768px measure but hug the pane's left edge instead of centering; the session list's resize clamp rises from 500px to 720px. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sending from a draft minted the daemon session and router.replace()d /workspace/chat to /workspace/chat/<id> — but moving the optional catch-all from zero segments to one changes the route shape, so Next remounts the page. The remount replaced the chat hook instance mid-stream: tokens rendered into unmounted state and the pane sat empty until a reload (the prototype's own comments warned about this remount; its mock module-stores existed to survive it). The URL now updates with native history.replaceState, which the App Router syncs without remounting, so the in-flight stream keeps its state and renders live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- A brand-colored spinner sits left of the chat title while a response streams. - The session list and the thread/file side panels now share one persisted resize setting (usePanelWidth, localStorage-backed, 200-720px): resizing either carries to the other and survives reloads. The side panel previously reset to a hardcoded width every open. - Approval action badges key by position: parsed actions can repeat or parse without a verb, which produced duplicate React keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dark mode deepens each stop of the green radial gradient so the shell
recedes behind the dark card instead of outglowing it; light mode keeps
the design's original stops.
A draft chat now renders the same 64px title bar as an open chat
("New chat" + the sidebar toggle) instead of a bare centered composer
with a floating button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
task studio:dev re-runs the fingerprinted install task whenever package.json changes — with the system npm on PATH. An npm 11 there rewrites the lockfile in a shape npm 10 (what CI's Node 22 ships) rejects, which has now broken npm ci twice. The install command runs through npx npm@10.9.4, matching the packageManager pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new browser-local preference (Settings → Appearance → Session list position) moves the session list to either side of the chat. The docked panel, the compact overlay, the resize handle's drag direction, and the panel-toggle icons all follow the chosen side; the thread and document panels stay on the right regardless. The Appearance page's theme picker is regrouped alongside the new control as labeled pill rows. Also aligns the resize handle's default max width (720px) with the shared panel-width store's clamp — dragging previously stopped at the handle's stale 500px cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the session list is on the left, its toggle renders leading in the chat header (before the generating spinner and title); when right, it stays in the trailing action cluster. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The approval badges' index-in-key tripped noArrayIndexKey at error level in CI (the ids are now precomputed per row, the pattern the message bubbles already use), and the side panel's drag effect needed its stable setWidth in the dependency array. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ns, working tap-to-open The shell's CSS gates used Tailwind's sm: (640px) while the JS hooks call <500px mobile; they now agree on 500px (min-[500px]: variants). The gradient edge margin halves below the breakpoint (10px vs 20px). Tapping a chat row on a phone did nothing: useIsMobile is undefined on the first render, so sidebarOpen initialised as if on desktop, and the row's router.push changed the optional catch-all's segment count — remounting the page and re-running that init, so the list rendered straight back over the chat. Selection now uses native history.pushState (no remount, the draft-mint precedent) and a measured-mobile effect closes the list over a selected chat on mount. The search palette becomes a full page below the breakpoint — the on-screen keyboard eats half the viewport, so a floating dialog leaves no room for results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Web app manifest (standalone display, /workspace/chat start URL, brand theme/background, 192/512/maskable icons generated from the logo mark on a solid brand ground), apple-touch-icon + appleWebApp metadata, and a viewport export with media-queried theme colors matching the shell gradient plus viewportFit cover for the notch. Deliberately no service worker: Studio is a local daemon client, so offline caching would lie about live state — and installability no longer requires one on Chrome and never did on iOS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ns, focus Bottom sheets follow mobile conventions: full-bleed with square corners, no X button, a centered grab handle, iOS safe-area padding, and dependency-free drag-to-close (handle or unscrolled body; >25% height or a flick dismisses, taps and scrolls are untouched). Threads (and file/canvas panels) now work on mobile: the same desktop panels render maximized inside a 94dvh bottom sheet with their window controls hidden — the grab handle owns dismissal — so the thread composer sits above the on-screen keyboard. Message actions: the hover-only reveal gains a touch path — a horizontal left swipe on a message reveals the action row, right swipe hides it. The Edit pencil is gone on all form factors (the daemon has no message-edit capability; it was prototype residue). The thread composer gains parity with the main one (model selector + memory control), and entering a chat or thread focuses the input: tiptap autofocus on mount plus a focusKey refocus when the open chat changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The five surfaces move to a native-style bottom tab bar on mobile (Material-3-style: light pill behind the active icon, small always-on labels, safe-area padding), sitting on the same green gradient; the top bar keeps only logo + search there. The gap between the content card and the bar is trimmed to a sliver. Settings gets a native mobile IA: the index renders grouped inset lists (icon chip, label, chevron) linking to each subpage, and subpages show a "back to Settings" header with the section's own title. Desktop keeps the left secondary nav and lands on the first section via a client redirect (the viewport decides, so the server can't). Profile splits out of Appearance into its own page on both form factors, and the section inventory is shared from one module so the nav, the drill-down list, and the back-header can't drift. Also drops the sheet base's gap-4 on bottom sheets — the grab handle's own padding is the only space wanted above sheet content, which was padding the thread title down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eaner copy Mobile settings subpages swap the back-link + big title for the chat header pattern: a full-bleed h-16 bar with a back arrow and the section title. The desktop secondary nav now pivots on the same 500px edge as the mobile drill-down list (hidden + min-[500px]:flex, not max-[499px]:hidden), so a fractional viewport width can never render both at once. Tab bar labels drop leading-none — truncate clips overflow, so the line box must clear descenders. The profile picture no longer rejects large files: the browser downscales any image to a 512px-longest-edge JPEG before storing it. Settings copy diet: card descriptions and how-it-works paragraphs are gone across all six pages. What stays is functional text only — offline and unconfigured-state recovery notes, the operator-managed refusal, error/notice channels, and the one warning a rule requires (writes that restart the daemon say so). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mobile settings index adopts the inset-grouped-list convention: filled cards with no border, a solid per-item tinted icon square with a white glyph, hairline dividers inset to the text edge, and tighter 46px rows. Memory leaves the top-level nav (top bar and mobile tab bar both read the one nav config) and becomes a settings section on both form factors: /workspace/settings/memory with the entry detail beneath it. Old /workspace/memory URLs redirect. Section resolution is now prefix-aware, so a memory entry keeps the desktop nav highlight and gets the mobile back header — whose arrow now goes up one level (entry to list, list to index) instead of always to the index. The inline Back button hides on mobile where that header covers it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A little gap between top-nav pills so hover and active states don't touch. On mobile the schedules create button shortens to "New" (the full label was truncating the page title) and the session-list position control hides (the list is full-screen there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The You and Agent cards drop their subtitles and share one grammar: label-left rows with short descriptions (Your name / Agent name / Picture) and the avatar + change button as the right-hand control. The add-provider dialog becomes an explicit three-step guide (choose, paste-into-file with the path and <YOUR_KEY> note, save + Re-check); its tests follow the new copy and the management fake supplies the active-provider switch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…utton
The mock-provider notice and the error banners float in the composer
stack over the transcript, but their 5% tints had no solid base — the
messages bled through. background-color + the tint as a flat gradient
keeps the token color and goes opaque. The global search input gets a
short placeholder ("Search…") and pr-8 so text never runs under the
dialog's close button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One structure across settings: SettingsCard + divided label-left / control-right rows (gateway, model-router's top controls, Labs, provider header blocks), with the per-category router editors kept as row-spanning blocks where a 1:1 conversion would fight the form. The Refresh control is deleted from runtime-status-line universally (the spinner and error/notice channel stay; it renders nothing when quiet). Labs' "Show mock features" OFF now switches the daemon back to the first configured real provider when it was genuinely on the mock — inline progress, verbatim errors, inert in external mode or when already on a real model. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /providers/active reassigns the active provider and restarts mecated on the spot, and /status gains isMock — the server half the already-committed client switch and Labs coupling call into. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tailwind's animate-bounce translates 25% of the element's own height — a 1.5px twitch on a 6px dot. A dedicated keyframe hops a fixed 5px with squash-and-stretch easing, staggered per dot as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…olls User messages stay plain text, but the one deterministic markdown shape the app itself composes — a leading "> " block from add-to-chat and thread-root quoting — now renders as a styled blockquote above the user's words (pure splitter, tested). And the maximized side panel gains min-h-0 so it constrains inside the mobile bottom sheet instead of outgrowing it, letting the thread body's overflow-y-auto actually scroll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The strip renders as a single divided container (pending steers first, then the queue) on a solid background instead of stacked translucent panels, and each queued row's Steer/Edit/Delete moves under a ⋯ menu. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d fixes
- The composer's model menu lists the daemon's live models (Studio-side
disabled set respected) with a daemon-picks sentinel labeled
"Auto-routed" only while the model router is actually enabled
("Default model" otherwise); a draft's pick rides session creation as
model_id, and live sessions show their real model in the locked slot.
- Thread panel gains the selection toolbar ("Add to thread" only); the
main toolbar's second action reads "Ask in a chat thread"; nested
quote markers collapse so re-quoted text shows words, not markup.
- Chat, draft, and thread composers share one gutter (px-3 lg:px-4
pb-4).
- Add-provider dialog: Done + Re-check as pills, the copy icon lives
inside the code box, and the helper text is one human sentence naming
the file, the providers: key, and the placeholder swap.
- MCP gateway card is a plain stacked form again (labels above
full-width fields, no dividers).
- Provider model lists are dedicated pages at /workspace/provider/{name}
(old settings-nested URL redirects).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The steer drain echo now SPLITS the stream: the injected message lands as a user turn and a fresh assistant bubble takes every later token, so the reply to a steer renders below it instead of the pre-steer bubble silently growing above — the "steering doesn't seem to do anything" illusion. The provider page collapses to a single list: real providers plus the built-in mock as a first-class row (active + running chips on whichever is live, Set as active in each kebab), Add provider above the list, and the status block / auth-file line / Switch-to-mock button all gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nity Steering is now invisible plumbing: an accepted steer renders your message directly in the transcript (no holding panel, no "steering…" chip — pendingSteers is internal watermark bookkeeping only), and at the drain boundary the bubbles move to sit right above a fresh assistant bubble so the reply streams below them. Orphaned steers pull their bubbles back out when the text requeues. The "already has an active run" failure: a parked approval left the composer thinking no run was live, so Enter fired a raw prompt into the daemon's funnel. Run-active now includes waiting_approval, and sendMessage queues (never fires, never drops) whenever a run is live — defense in depth under the composer's own routing. The approval panel stops badge-ifying every line of the tool's args (a Write ask's file content became hundreds of "Tool ·" pills, and content words like "delete" painted it destructive): one pill naming the tool, destructiveness judged on the tool name, args in a clamped scrollable preview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Write tool call carries the produced file in its args, so the assistant turn now shows each written file as an attachment chip (deduped by path, last write wins, full path on the tooltip) that opens in the canvas preview with the markdown/code/image routing. One shared fileFromToolCall derivation feeds both the live stream and transcript rehydration; Edit calls are deliberately skipped (their args don't carry the final content, and a card that can't preview is a dead end). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A live capture showed Write args carry "path", not "file_path" — the derivation read the wrong key so no chip ever rendered. Both spellings are accepted now, pinned by a test using the daemon-shaped args. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The classifier and category selects filtered models to the ToolHive gateway's provider id — a leftover from the gateway-coupled design that blanked both dropdowns on any plain-provider deployment. The daemon's live model listing is the routable set, so the pickers now offer it (minus Studio-side disabled models), and an already-saved id still survives via ModelSelect's keep-selected fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Personalize page carries theme, scale, session-list side, and the Enter-while-replying behavior; the Messages section is gone (its URL redirects) and every cross-reference follows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One row on Personalize: the Enable button plus a bell-icon test button beside it (enabled only once granted); the standalone Notifications section is gone and its URL redirects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The router page discloses progressively: off = a single switch; on = the classifier row plus a separate Categories card. Categories render as stacked rows (name, model badge, description) with the default pinned first and badged; each row's kebab offers Set as default / Edit / Delete, and create/edit happens in a full-screen-on-mobile modal whose own validation keeps bad categories out of the draft. The default-category select is gone (rename follows, delete clears); validation stays load-bearing but only speaks while routing is enabled (or as an honest incomplete-setup line when a dirty invalid draft cannot be saved even disabled). Model dropdowns use the standard sans font; the illustrated empty state owns the first Add. On Personalize, every right-hand control shares one w-44 footprint (selects, the scale stepper, and the notification enable + bell-test pair). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sent image attachments carry their wire bytes as data: URLs, so the message chips show thumbnails and open a real canvas preview instead of "No preview available". Composer pills are clickable too (object-URL lifecycle managed, no blob leaks), and both surfaces share the brand palette. Attachments no longer vanish after a reply: the transcript carries no bytes, so a session-scoped record of what this visit sent re-attaches them on every rehydrate, matched to user turns in send order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dots shrink to size-1 and center inside the avatar column; the phase label starts exactly where message text does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vider
Sent-attachment bytes persist per session in IndexedDB (bounded to the
last 20 sends, fail-soft to session-only memory), and the rehydrate
merge seeds from it on a fresh visit — so thumbnails and previews
outlive a browser refresh. The "[conversation compacted]" notice stops
being a truncated grey line: it renders as a labeled divider ("Earlier
messages summarized") with the daemon's full explanation on the
tooltip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The panel pins the root message right above the replies, so the scoping quote composeThreadPrompt prepends renders stripped there — quotes of OTHER selections (add-to-thread) stay meaningful and intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ills widths The thread panel header gains a ⋯ menu: Show/Hide Tools for the reply bubbles, and "Open as full chat" — the thread detaches from its root (registry + indicator record removed), the panel closes, and the session opens as the main chat where the sidebar now lists it. The skills table's Name column shrinks to its content (bounded at 360px) and Description fills the remainder, putting the ellipsis at the far right. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tour's closing message points at the genuine reply action instead, and the test pins that no mock message carries canned replies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
h-7 on both the message chips and composer pills, so thumbnail and glyph variants align instead of the image chips running taller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dialog is Name, Prompt, and the trigger — timezone (machine's zone applies silently), max runs, one-shot presets, retry, and the write-access box are gone; edits still round-trip every stored value through the seeded form state. Time inputs no longer snap back to the default mid-segment-edit (transient empty values skip the rebuild), the edit dialog title is a static "Edit Schedule", label gaps widened, and markdown lists re-declare the body leading Tailwind's text-sm was silently resetting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New skill opens on a chooser — Upload or Create manually — and manual mode drops the upload button along with the old restart warning. Router validation goes quiet (Save stays gated, the red lines are gone), the category dialog loses its placeholder specimens and mono name font, the first category created becomes the default, and the footer reads Save/Discard. The classifier row says "Select a fast, small model for categorization." with Select placeholders throughout. Providers: Add moves below the list on the left, and the built-in mock no longer lists as a provider. Config-write notices render as an info banner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A skill can be a whole folder (SKILL.md plus scripts/, references/,
assets/…), and the detail page now shows it: a Files card lists the
bundled files with sizes and opens bounded read-only previews. The
controller serves both — GET /skills/{name}/files walks the folder
(symlinks never followed, dot-entries skipped, 500-entry cap) and
GET /skills/{name}/file?path= reads one text file (path confined to the
skill folder, 256KB cap, binary refused) — on whichever side, enabled
or disabled, holds the skill. External mode keeps the metadata-only
note. Non-chat screens also drop their oversized bottom padding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
|
Consolidated into #618 — feat/studio-atrium now fast-forwarded to include all of today's feat/studio-mobile work (identical diff against main). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A day of Studio work on top of the Atrium port, plus the one daemon change it needed. Builds on (and will supersede) #618 the way #618 superseded #548 — this branch contains the full
studio/tree with main merged in, so the diff against main is the complete Studio picture.Daemon (Go)
Unary HTTP steer endpoints — the ADR 0232 follow-up:
POST /v1/sessions/{id}/steer(accepted|appended|too_late; HTTP never promotes — the caller keeps the text ontoo_late) andPOST /v1/sessions/{id}/steer-cancel, the EvSteer drain-echomessage_idstamp extracted into one shared helper used by both relays, and thesteercapability bit on the HTTP echo. Offline httptest coverage;Service.Steer's gRPC promote path pinned unchanged.Studio
Merged
origin/main(steer-while-running #570, session-storage #645 etc.); the Studio ADRs were renumbered 0233/0234 to resolve the collision with main's 0228–0232.Gates
task lintgreen;task testgreen except two pre-existinginternal/adapter/store/jsonlstorefailures that reproduce identically on a cleanorigin/maincheckout on macOS (the atomic-replacement probe vs the/tmpsymlink) — not from this branch.matlatl check --strictclean,llms.txtregenerated.🤖 Generated with Claude Code