Skip to content

feat(studio): Web UI workspace - #618

Closed
jtenniswood wants to merge 135 commits into
mainfrom
feat/studio-atrium
Closed

feat(studio): Web UI workspace#618
jtenniswood wants to merge 135 commits into
mainfrom
feat/studio-atrium

Conversation

@jtenniswood

@jtenniswood jtenniswood commented Aug 18, 2026

Copy link
Copy Markdown

Summary

Studio becomes the Atrium workspace: the designed five-surface UI (Chats · Scheduled · Skills · Memory · Settings) from stacklok/enterprise-ui-prototypes@claude/atrium-ui-updates (649bc1e), mounted on the hardened server tier from #548daemon-only, with the daemon as the single source of truth for every record.

Supersedes #548 (feat/studio-module): its proxy/controller/security tier, hermetic test harness, CI shape, Taskfile wiring, and ADR posture are carried here near-verbatim (mapped in a comment on that PR); its single-file UI is replaced. Also re-lands the designs from the closed #615 (server-backed chats) and #616 (schedule authoring + fire audit).

What landed, commit by commit

The vendor drop is skippable; every commit after it is honest diff. Please merge with a merge commit or rebase, not squash — the vendor-drop/relicense split is the review structure.

  1. chore: import Atrium UI (filtered vendor drop) — byte-identical import of 159 files; 413 of 572 tracked files excluded before import so prototype residue never enters history: the better-auth/pg OIDC stack, the legacy ai-sdk assistant, the MSW mock server + generated clients, all demo fixtures (2,807-line mock-data, skills fixtures), disk-touching authoring routes (deferred), and the prototype's deploy infra.
  2. chore: correct stray Proprietary license headers — 8 files carried SPDX-License-Identifier: Proprietary contradicting the source repo's Apache-2.0 LICENSE; corrected to Apache-2.0 (the license the source actually grants), in its own commit for auditability. CI gates against regression.
  3. chore: rebrand and retool — npm + committed lockfile (pnpm security pins carried as npm overrides), Node 22 LTS, Biome, husky dropped (git hooks in a Go monorepo's Node subdir would hijack .git/hooks), route-group flatten, dep prune.
  4. feat: graft the hardened server tierfeat(studio): bring Mecatl Studio in-repo as a Node module #548's server-proxy.ts (Host/Origin/CSRF gate, header allowlists, server-side bearer), controller-security.mjs, the managed-mode controller + supervisor, /api/mecatl{,-control} routes, external-mode 409 policy — plus server-side workspace injection so the browser never learns or chooses paths.
  5. feat: merge the protocol seam — one typed reader of raw daemon JSON (src/lib/protocol/): feat(studio): back the chat list with the daemon's session store #615's session decoders (capability-driven actions, complete-walk rule), feat(studio): let the Schedules panel author and audit schedules #616's schedule encode/decode (protojson-request/stdlib-response asymmetry, carried-spec round-trip), and event translation where failed turns always surface (stop/error/permanent), approvals are three-way, retractions withdraw their ask, delegation becomes badges, and unknown event kinds render as notices, never silent drops. 26 decoder tests.
  6. feat: daemon-only hooks behind a shared runtime status — the mock layer is deleted outright. One 5s-poll connection authority gates every surface, shows the offline banner (naming the fix), and reloads on reconnect. Sessions = the daemon inventory; @-mentions/slash-commands = GET /v1/agents / GET /v1/commands.
  7. feat: wire the five surfaces daemon-first — flat recency-bucketed chat sidebar with capability-gated rename/delete, draft-on-first-send, failed-turn alerts + retry, three-way approvals, live usage; schedule authoring (cron+tz+max-fires ⊕ one-shot+retry) with an explicit write opt-in that makes the invalid mutating/plan pairing unconstructible, edit with carried-spec round-trip, and the real fire log with per-fire transcripts; read-only skills with provenance; memory with honest disabled/empty states and store footprint; settings with provider status (no credential entry, ever), the model-router editor, and a validated user-entered MCP gateway. Plus the hermetic server-tier suite (see Verification).
  8. test: browser e2e over a fixture daemon — five Playwright smokes through the real proxy against a wire-faithful fixture daemon. This suite immediately caught a real bug: the client base path had silently lost its /v1 prefix in the proxy graft (the prototype's proxy used to rewrite paths; the grafted tier is transparent) — invisible to every unit/server-tier test, fatal in a browser.
  9. ci: — path-scoped workflow (biome, tsc, knip, vitest, build + hermetic suite, npm audit --audit-level=high, SPDX gate; separate e2e job), the repo's first npm dependabot entry, Taskfile namespace.
  10. docs: — ADR 0228 (the module + daemon-only posture) and ADR 0229 (the session store as the record of chats), architecture/AGENTS/readiness/user-docs updates, studio/CLAUDE.md with the rules-that-have-teeth list (each named to its backing test), llms.txt regenerated.

Explicit non-goals (stated in ADR 0228)

  • Live re-attach to a running session — the live tail is gRPC-only (StreamSessionLive); Studio shows running state from the inventory and reads the transcript at run end. Needs a daemon-side HTTP/SSE mirror.
  • Mid-run steering — a prompt during a live run is a daemon 409 by design.
  • Memory writes — no daemon API, by design (injection-scanning would be bypassed).
  • Skill/agent-definition authoring, learned-skills review, team runs, plan-approval flow — named follow-ups (the prototype's dormant authoring code was excluded; it returns controller-mediated).
  • Cost display — the daemon accounts tokens, not currency.

Verification

  • npx vitest run — 70 tests (protocol decoders/encoders, components).
  • npm run test:server — 6 hermetic tests against a real production next start in external mode + a fake recording daemon: SSR, server-side bearer injection, workspace injection, CSRF 403, external-mode 409s, and an unreachable daemon is a friendly 503, never demo content.
  • npx playwright test — 5 browser smokes over the fixture daemon (chat list + transcript, schedule badges, skills, memory, external-mode settings).
  • npx tsc --noEmit, biome check, npx knip, npm audit --audit-level=high — clean.
  • Repo side: task docs:check (matlatl strict — 232 docs, 0 broken links/orphans), ADR-uniqueness gate, task docs:llms regenerated.
  • Not yet exercised here: a live managed-mode session against a real bin/mecated (requires task build + provider credentials); the managed-mode controller code is carried unchanged from feat(studio): bring Mecatl Studio in-repo as a Node module #548.

🤖 Generated with Claude Code


Addendum: Figma shell redesign + UX round (folded in from #625)

A second wave of commits lands the new visual design (Figma node 4-2) plus interactive fixes found while testing against a live daemon:

  • New shell: the left rail is replaced by a top navigation bar on a dark-green radial gradient (active-route pill, icon-only siblings, ⌘K search field); all five surfaces render in one rounded 20px card. Dark mode deepens the gradient stops.
  • Chat layout: conversation left (768px measure, left-aligned) with the title, generating-spinner, and header controls in a 64px bar; Session List right by default with the design's row treatment and a real AGENTS section from GET /v1/agents. A new Appearance setting docks the list left or right (threads/canvas stay right); the sidebar toggle follows the docked edge.
  • Settings: subpages behind a left secondary nav (Appearance, Notifications, Provider, Model router, MCP gateway); browser-local Profile prefs (agent display name — default "Mecatl" — and user picture); provider page lists auth.yaml provider names (never values) with the active one marked.
  • Design content not fabricated: the mock PROJECTS folders render as recency groups; New-project button, bell, and per-agent chat nesting dropped (no daemon backing).
  • Fixes caught live: the draft's first stream survived its own session mint (native history.replaceState instead of a remounting router.replace); /v1 base-path regression caught by the new Playwright suite; shared persisted width for all chat panels; npm-10 lockfile pinning end-to-end (packageManager + Taskfile install via npx npm@10.9.4).

Verified throughout: tsc/biome/knip clean, vitest 70/70, hermetic server-tier 6/6, Playwright 5/5, plus live managed-mode sessions against a real mecated (OpenRouter) in light and dark.


Addendum: mobile round (folded in from #647)

A third wave makes Studio genuinely usable on a phone — verified live at 375×812 and on real iOS/Android devices over the LAN — then keeps iterating on device feedback:

  • Mobile shell (<500px): one breakpoint everywhere (the CSS gates and JS hooks agreed on 500px), full-bleed content card with 12px top corners, 18px root font (the CSS/JS sm: mismatch was real), a bumped icon scale, and slimmer h-14 bars. Desktop's gradient border trims to 12px.
  • PWA: installable on Android/iOS — manifest, brand icons, apple-touch-icon, media-queried theme colours, viewport-fit=cover with safe-area padding top and bottom, canvas painted the gradient's terminal colour (the iOS stale-dvh launch strip), pinch-zoom off, and interactive-widget=resizes-content so Android's keyboard resizes the layout viewport instead of burying the composer.
  • Interactions found dead on-device and fixed: tap-to-open chat rows (the optional-catch-all remount trap + pre-measurement sidebar state), long-press on a chat row for rename/delete (capability-gated bottom sheet; the hover "…" was also re-gated to show at every desktop width, not just ≥1024px), swipe-to-reveal message actions, and the Edit pencil deleted on all form factors (no daemon capability behind it).
  • Bottom sheets as the mobile idiom: square full-bleed corners, grab handle, drag-to-close; threads/files/canvas open maximized in a 94dvh sheet; the composer's model+effort and memory pickers, chat options, and settings selects all use it.
  • Composer: single docked row on mobile (options sheet on the left — Add a file, Model, Memory — mic that becomes send once text exists), full-bleed with a hairline top border at the chat-header height; draft view docks the composer in the same place on all widths.
  • Chat: pinned-follow auto-scroll (stay pinned at the bottom through sends and streaming; hold position when scrolled up), a floating scroll-to-bottom arrow, a streaming activity line (phase + elapsed), token usage as a chat total in the context menus, and roomier response line height.
  • Image attachments now reach the model: picked images ride the daemon prompt's multimodal parts (the composer previously collected files and posted {text} only). Pickers/drop-zone scope to image/* (the daemon has no document part kind — deliberate), and big or HEIC photos downscale/re-encode to JPEG client-side (phone photos 413'd the daemon's byte budget). Proven end-to-end against a live OpenRouter model.
  • Settings: native drill-down IA on mobile (grouped index → chat-style back header), Memory moved from the top nav into Settings (old URLs redirect), Profile/Agent identity split with picture uploads for both (in-browser downscale), an interface-scale stepper (85–130% over the per-viewport defaults), select-style Theme/list-position fields, and an explainer-copy diet.
  • LAN access for phones: allowedDevOrigins derives from the same MECATL_STUDIO_PUBLIC_ORIGIN allowlist the API proxy trusts — without it Next's dev cross-origin protection 403'd the chunk requests, so pages rendered but never hydrated on any phone. Dev-only; unset keeps localhost-only behaviour.

Verified throughout: tsc/biome/knip clean, vitest 70/70, hermetic server-tier 6/6, next build, plus live phone testing against a managed-mode mecated (OpenRouter) — including an end-to-end image prompt answered correctly by the routed model.

jtenniswood and others added 10 commits August 18, 2026 14:11
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>
jtenniswood and others added 2 commits August 18, 2026 15:37
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>
@jtenniswood jtenniswood changed the title feat(studio): the Atrium workspace — mecatl's daemon-only web client feat(studio): Web UI workspace Aug 18, 2026
jtenniswood and others added 3 commits August 18, 2026 17:09
…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>
jtenniswood and others added 5 commits August 18, 2026 17:46
…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>
jtenniswood and others added 2 commits August 18, 2026 18:58
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>
jtenniswood and others added 2 commits September 1, 2026 20:09
…tcha

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chat rows gain "Debug with AI" (gated on session_debug): the ADR-0254
consent dialog states plainly that stored transcript evidence goes to
the model, then a no-fs debug session bound to the target opens as an
ordinary chat, badged "Debug" in the sidebar with its row capabilities
honored (the daemon refuses deleting debug sessions — mirrored, not
hidden). The proxy learned that debug/no-fs creates REQUIRE an empty
workspace and skips injection exactly there (a plain create still
injects — re-verified live; the H4 pin gets a dedicated exact-set
assertion rather than a loosened shared allowlist). Title provenance
decodes onto every row — the audit found no clobber path today, so the
deliverable is the decode, the operator-stamp mirror in optimistic
renames, and a guard comment binding future auto-titling. The agents
roster now carries each specialist's pinned model ("auto" when routed),
a color hint through a named-colors/hex allowlist (frontmatter is
verbatim — arbitrary CSS is not honored), and tools/permissions in the
tooltip. (Requirements F1, F4, D2-roster.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jtenniswood jtenniswood reopened this Sep 1, 2026
jtenniswood and others added 3 commits September 1, 2026 20:49
- unexport/remove symbols knip flagged; ignore mkfifo binary
- go mod tidy from repo root (ko build was blocked by -mod=readonly)
- regenerate llms.txt from a clean worktree (local scratch docs had
  contaminated the corpus counts)
- add the request-trust unit tests that were left untracked

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Sep 2, 2026
@jtenniswood

Copy link
Copy Markdown
Author

This PR is now landing as a stack of 21 PRs so it can be reviewed piece by piece: #966 (foundation) → #967 (Go steer endpoints) → #968#971 (server tier, protocol seam, harness, shell) → #972#977 (the five surfaces + OIDC) → #978#983 (advanced chat tiers) → #984#986 (Labs, mobile, docs + parity sweep).

Each PR is CI-green on its base; the assembled top of the stack is byte-identical to this branch in studio/src, studio/tests, and studio/scripts (machine-checked in #986), so the shipped experience is exactly what this branch built. The handful of deliberate deviations (the ADR renumber off real id collisions with main, the dead SteerEnqueue/alias removal, docs honesty repairs, the .env.example OIDC rows) are enumerated in #986. Merge bottom-up with merge commits, never squash — #966 carries the byte-identical vendor drop and the license correction as verbatim commits.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants