feat: split-pane main page with scope-aware file explorer #19

Merged
lz merged 29 commits from feat/session-explorer into main 2026-05-24 11:54:19 +02:00
Owner

Summary

Revamps the main page (/) into a split layout: workspaces + sessions on the left, a scope-aware file explorer + viewer/editor on the right. The explorer browses and edits files in either a workspace's main worktree (/workspace) or any session's worktree (/workspace/.nexus/worktrees/<name>). Markdown plan files render as preview by default with a source toggle.

Both file-explorer surfaces (the new main page and the existing /config-explorer) now consume a single FileExplorerPanel component behind a FileExplorerProvider abstraction, so markdown preview and breadcrumbs work on the Config Explorer too.

What's new

File explorer (server)

  • /api/workers/:id/files/* — six HTTP endpoints (tree, file GET/PUT, mkdir, rename, path DELETE, upload) that talk to worker containers via docker exec.
  • WorkerFsService at nexus/src/lib/server/worker-fs/. Path data flows via argv ($0); base64 payload streams via stdin (avoids MAX_ARG_STRLEN 128 KiB cap).
  • Path safety extracted into a shared lib/server/file-explorer/path-safety.ts module so both backends use the same regex, depth caps, and binary sniff.
  • mv -n on rename closes the stat-then-mv TOCTOU race.

File explorer (client)

  • FileExplorerProvider interface with ConfigVolumeProvider (existing endpoints) and WorkerFsProvider (new endpoints).
  • FileExplorerPanel — the shared component used by both the main page and the Config Explorer. Toolbar with breadcrumb, scope chip, refresh / +file / +folder / upload / save, plus md preview↔source toggle on the right.
  • MarkdownViewmarked + isomorphic-dompurify, dynamic-imported on first .md open. Sanitized, GFM-aware, forces target=_blank rel=noopener noreferrer on external links.
  • FileTree / FileEditor moved to lib/components/file-explorer/; old locations are now ComponentProps-typed re-export shims so /config-explorer keeps working unchanged.

Main page

  • MainSplit — split shell with scope state mirrored to URL (?scope=workspace&worker=… or ?scope=session&worker=…&session=…&base=…); mobile slide-in pattern (right pane slides in over workspaces).
  • Workers.svelte + Sessions.svelte wire onSelectScope upward. Running workspace cards are full-card click targets that select the workspace as the file-explorer scope.
  • Session rows: clickable when scope-selection is wired; selected session row paints a green tint that bleeds to the card edges; chevron tints green when active.
  • Workspace cards: tighter button sizing per the mockup, a faint accent outline on the currently-explored card, relativeTime(created_at) ("2 days ago") instead of a raw datetime.

Header + tokens

  • Header: nav buttons borderless with hover/active tint; left cluster (Workspaces / Config / Providers) sits next to the logo, Settings + Lock anchor right. aria-current="page" on the active link.
  • + New link removed from the header — the bottom-of-list "+ New workspace" CTA covers it.
  • Accent token split: --accent stays at #cddc39 for text/borders/icons; new --accent-bg at #b5c249 for solid button fills (.btn.primary, settings ON-toggle, install-skill CTA) so the bright text hue doesn't read as "glowing" when used as a large background.
  • Workspace agent button: relabeled "+ workspace agent" → start agent / "× workspace agent" → stop agent; repositioned to the left of Remove.

Configurable max file size

  • New instance setting max_file_bytes (env FILE_EXPLORER_MAX_FILE_BYTES, default 262144 = unchanged from the prior hardcoded value; range 1 KiB to 10 MiB). Surfaced in the Settings pane.

What to look at first

  • lib/components/file-explorer/provider.ts — the load-bearing abstraction.
  • lib/server/worker-fs/service.ts + lib/server/lib/docker-exec.ts:execAndCaptureWithStdin — the shell-injection model. Paths via argv ($0), payload via stdin. service.test.ts locks the no-payload-in-argv contract.
  • lib/components/file-explorer/FileExplorerPanel.svelte — the single panel both surfaces consume.
  • The two shims at lib/components/config-explorer/{FileTree,FileEditor}.svelte — confirm /config-explorer/+page.svelte keeps working through them.

Spec + plan

  • Design spec: docs/superpowers/specs/2026-05-23-main-page-file-explorer-design.md
  • Implementation plan: docs/superpowers/plans/2026-05-23-main-page-file-explorer.md

Test plan

Vitest unit suite (326 passing, 65 SQLite/POSIX-shell host-skips on Windows; CI runs them on node:22-alpine):

  • pnpm test clean
  • pnpm typecheck clean
  • Worker-fs path safety: 25 cases on assertSafePath (rejections + acceptances incl. dotfiles), plus the WorkerFsService allowlist + traversal rejection.
  • Worker-fs read/write: stat short-circuit, MAX+1 sentinel for size, binary detection, JSON validation, stdin streaming locks the no-argv-payload contract.
  • Worker-fs rename: mv -n race-window backstop.
  • Markdown sanitization: 5 cases (script-strip, javascript: URL strip, target=_blank, GFM checkboxes preserved, bold + heading).
  • Provider abstraction: 7 cases asserting URL/body shapes match both backends.
  • URL scope sync: 7 round-trip cases on scopeFromSearch / scopeToSearch.
  • relativeTime: 12 boundary cases for "just now" → "N years ago".
  • max_file_bytes registry: default, env override, bounds validation.

Manual smoke (verified locally via docker compose up):

  • / loads with workspaces on the left, "No workspace selected" prompt on the right.
  • Clicking a running workspace card sets it as scope; the card outlines yellow; inner buttons + session rows stop propagation.
  • Clicking a session row switches scope to that session's worktree; row + chevron tint green; the green bleeds to the card edges.
  • Selecting a .md file → preview by default, preview | source toggle in the toolbar's right cluster.
  • Selecting a non-.md file → editor opens.
  • Config Explorer (/config-explorer): same panel, neutral "~/.claude" scope chip; markdown files now render as preview; SKILL.md from the Installed Assets Panel opens in the panel via bind:this.
  • Refreshing the page → scope hydrates from URL.
  • Mobile viewport (≤768px) → right pane slides in/out; back chevron returns to workspaces.
  • Stop the worker → file ops 503; the standard toast surfaces the error.

Known v1 trade-offs (documented in the spec)

  • No file watching / live-reload — manual refresh button only.
  • No conflict detection when claude edits a file mid-edit (last-write-wins; the dirty dot is the only nod).
  • No search across files.
  • No git gutters (the API surface stays compatible; a future GET .../diff endpoint can plug in without UI restructure).
  • Sessions.svelte row has an svelte-check a11y advisory (tabindex + conditional role) that's a static-analysis limitation; the row IS keyboard-focusable when scope selection is wired.
## Summary Revamps the main page (`/`) into a split layout: workspaces + sessions on the left, a scope-aware file explorer + viewer/editor on the right. The explorer browses and edits files in either a workspace's main worktree (`/workspace`) or any session's worktree (`/workspace/.nexus/worktrees/<name>`). Markdown plan files render as preview by default with a source toggle. Both file-explorer surfaces (the new main page and the existing `/config-explorer`) now consume a single `FileExplorerPanel` component behind a `FileExplorerProvider` abstraction, so markdown preview and breadcrumbs work on the Config Explorer too. ## What's new ### File explorer (server) - **`/api/workers/:id/files/*`** — six HTTP endpoints (tree, file GET/PUT, mkdir, rename, path DELETE, upload) that talk to worker containers via `docker exec`. - **`WorkerFsService`** at `nexus/src/lib/server/worker-fs/`. Path data flows via argv (`$0`); base64 payload streams via stdin (avoids `MAX_ARG_STRLEN` 128 KiB cap). - **Path safety** extracted into a shared `lib/server/file-explorer/path-safety.ts` module so both backends use the same regex, depth caps, and binary sniff. - **`mv -n`** on rename closes the stat-then-mv TOCTOU race. ### File explorer (client) - **`FileExplorerProvider`** interface with `ConfigVolumeProvider` (existing endpoints) and `WorkerFsProvider` (new endpoints). - **`FileExplorerPanel`** — the shared component used by both the main page and the Config Explorer. Toolbar with breadcrumb, scope chip, refresh / +file / +folder / upload / save, plus md preview↔source toggle on the right. - **`MarkdownView`** — `marked` + `isomorphic-dompurify`, dynamic-imported on first `.md` open. Sanitized, GFM-aware, forces `target=_blank rel=noopener noreferrer` on external links. - **`FileTree`** / **`FileEditor`** moved to `lib/components/file-explorer/`; old locations are now `ComponentProps`-typed re-export shims so `/config-explorer` keeps working unchanged. ### Main page - **`MainSplit`** — split shell with scope state mirrored to URL (`?scope=workspace&worker=…` or `?scope=session&worker=…&session=…&base=…`); mobile slide-in pattern (right pane slides in over workspaces). - **`Workers.svelte`** + **`Sessions.svelte`** wire `onSelectScope` upward. Running workspace cards are full-card click targets that select the workspace as the file-explorer scope. - Session rows: clickable when scope-selection is wired; selected session row paints a green tint that bleeds to the card edges; chevron tints green when active. - Workspace cards: tighter button sizing per the mockup, a faint accent outline on the currently-explored card, `relativeTime(created_at)` ("2 days ago") instead of a raw datetime. ### Header + tokens - **Header**: nav buttons borderless with hover/active tint; left cluster (Workspaces / Config / Providers) sits next to the logo, Settings + Lock anchor right. `aria-current="page"` on the active link. - **`+ New` link removed** from the header — the bottom-of-list "+ New workspace" CTA covers it. - **Accent token split**: `--accent` stays at `#cddc39` for text/borders/icons; new **`--accent-bg`** at `#b5c249` for solid button fills (`.btn.primary`, settings ON-toggle, install-skill CTA) so the bright text hue doesn't read as "glowing" when used as a large background. - **Workspace agent button**: relabeled "+ workspace agent" → **`start agent`** / "× workspace agent" → **`stop agent`**; repositioned to the left of Remove. ### Configurable max file size - New instance setting `max_file_bytes` (env `FILE_EXPLORER_MAX_FILE_BYTES`, default `262144` = unchanged from the prior hardcoded value; range 1 KiB to 10 MiB). Surfaced in the Settings pane. ## What to look at first - **`lib/components/file-explorer/provider.ts`** — the load-bearing abstraction. - **`lib/server/worker-fs/service.ts`** + **`lib/server/lib/docker-exec.ts:execAndCaptureWithStdin`** — the shell-injection model. Paths via argv (`$0`), payload via stdin. `service.test.ts` locks the no-payload-in-argv contract. - **`lib/components/file-explorer/FileExplorerPanel.svelte`** — the single panel both surfaces consume. - **The two shims** at `lib/components/config-explorer/{FileTree,FileEditor}.svelte` — confirm `/config-explorer/+page.svelte` keeps working through them. ## Spec + plan - Design spec: `docs/superpowers/specs/2026-05-23-main-page-file-explorer-design.md` - Implementation plan: `docs/superpowers/plans/2026-05-23-main-page-file-explorer.md` ## Test plan Vitest unit suite (326 passing, 65 SQLite/POSIX-shell host-skips on Windows; CI runs them on `node:22-alpine`): - [x] `pnpm test` clean - [x] `pnpm typecheck` clean - [x] Worker-fs path safety: 25 cases on `assertSafePath` (rejections + acceptances incl. dotfiles), plus the `WorkerFsService` allowlist + traversal rejection. - [x] Worker-fs read/write: stat short-circuit, MAX+1 sentinel for size, binary detection, JSON validation, stdin streaming locks the no-argv-payload contract. - [x] Worker-fs rename: `mv -n` race-window backstop. - [x] Markdown sanitization: 5 cases (script-strip, `javascript:` URL strip, target=_blank, GFM checkboxes preserved, bold + heading). - [x] Provider abstraction: 7 cases asserting URL/body shapes match both backends. - [x] URL scope sync: 7 round-trip cases on `scopeFromSearch` / `scopeToSearch`. - [x] `relativeTime`: 12 boundary cases for "just now" → "N years ago". - [x] `max_file_bytes` registry: default, env override, bounds validation. Manual smoke (verified locally via `docker compose up`): - [x] `/` loads with workspaces on the left, "No workspace selected" prompt on the right. - [x] Clicking a running workspace card sets it as scope; the card outlines yellow; inner buttons + session rows stop propagation. - [x] Clicking a session row switches scope to that session's worktree; row + chevron tint green; the green bleeds to the card edges. - [x] Selecting a `.md` file → preview by default, `preview | source` toggle in the toolbar's right cluster. - [x] Selecting a non-`.md` file → editor opens. - [x] Config Explorer (`/config-explorer`): same panel, neutral "~/.claude" scope chip; markdown files now render as preview; SKILL.md from the Installed Assets Panel opens in the panel via `bind:this`. - [x] Refreshing the page → scope hydrates from URL. - [x] Mobile viewport (≤768px) → right pane slides in/out; back chevron returns to workspaces. - [x] Stop the worker → file ops 503; the standard toast surfaces the error. ## Known v1 trade-offs (documented in the spec) - No file watching / live-reload — manual refresh button only. - No conflict detection when claude edits a file mid-edit (last-write-wins; the dirty dot is the only nod). - No search across files. - No git gutters (the API surface stays compatible; a future `GET .../diff` endpoint can plug in without UI restructure). - `Sessions.svelte` row has an svelte-check a11y advisory (`tabindex` + conditional `role`) that's a static-analysis limitation; the row IS keyboard-focusable when scope selection is wired.
lz added 18 commits 2026-05-24 01:22:14 +02:00
Adds the approved spec for a split-pane main page with a scope-aware
file explorer that browses workspace and session worktrees, generalizes
the config-explorer components behind a provider abstraction, and
introduces a worker-fs service via docker exec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure helpers split out of volume-fs.ts so the upcoming worker-fs service
can share the exact same regex, depth caps, and binary-sniff logic
without copy-paste drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Task 1 migration of assertSafePath tests dropped two leading-dot
acceptance cases that existed in the original volume-fs.test.ts
('.credentials.json', 'inner/.config'). Per AGENTS.md fact #13 these
guard against future regex tightening silently re-hiding dotfile-name
artifacts like .nexus-skill.json. Also added two tests for the
non-string defensive guard that crosses HTTP boundaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hardcoded 256 KiB cap with a configurable limit (1 KiB to
10 MiB). Default is unchanged so existing deployments behave identically.
Surfaced in the Settings pane like other instance settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issues find inside the worker container and parses the output into the
existing TreeNode shape. Base directory is validated against an
allowlist (/workspace and its session worktrees only). Path safety
helpers are shared with volume-fs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
readFile uses stat to short-circuit on missing/symlink/too-large, then
head -c (maxBytes+1) | base64 to spot truncation. writeFile decodes
base64 into a tmp file inside the destination dir and atomically renames.
Path data is always passed via argv (\$0 / \$1), never interpolated into
the script string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior Task 4 implementation passed the base64 payload as a single
argv element to sh -c, which would fail in production for any file
larger than ~96 KiB (MAX_ARG_STRLEN = 128 KiB per argv element on Linux).
Unit tests didn't catch it because they mocked execAndCapture and never
actually shelled out.

Adds execAndCaptureWithStdin to lib/docker-exec.ts (sibling of
execAndCapture, mockable the same way) and switches writeFile to use it.
Path data still goes via argv (\$0), only the payload moves to stdin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mkdir uses mkdir -p; rename guards against missing source / existing
destination; deletePath refuses symlinks. All three accept paths via argv
only, never interpolated into a shell script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pre-check stat in rename was racy: between the stat and the mv, a
file could appear at the destination and be silently overwritten. mv -n
(no-clobber) is the POSIX-portable backstop — keep the pre-check stat
for the friendly EXISTS error message in the common case, and let mv
exit 0 without writing if the race fires.

Also documents the deletePath stat-then-rm race with a comment; the
worst case is removing the link itself, not the target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six endpoints (tree GET, file GET/PUT, mkdir, rename, path DELETE, upload)
that mirror the /api/config-explorer/* shape, plus a tiny resolve helper
that 404s on missing containers and 503s on stopped ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dynamic-imports marked + isomorphic-dompurify on first render; strips
scripts and javascript: URLs; forces target=_blank on external links;
preserves GFM task list checkboxes.

The pure renderer lives in a `markdown-renderer.ts` sibling so vitest
can import it without the svelte vite plugin; MarkdownView.svelte
re-exports it from its module-script for callers that prefer the
component path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FileTree and FileEditor move to lib/components/file-explorer/ and are
re-exported from lib/components/config-explorer/ as compat shims. Adds
ConfigVolumeProvider (existing endpoints) and WorkerFsProvider (new
worker-fs endpoints) implementing a common FileExplorerProvider interface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin wrappers around /api/workers/[id]/files/* that other UI code can
call without constructing a WorkerFsProvider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-pane file explorer wired up to a FileExplorerProvider. Owns tree/
buffer/dirty state, switches between MarkdownView (preview) and
FileEditor (source/code) based on file extension + the operator's toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MainSplit owns the split layout and the explorer scope. Scope is mirrored
to the URL via ?scope=&worker=&session=&base= so deep links and the
browser back button work. Mobile slides the right pane in/out.

Typecheck temporarily fails because Workers/Sessions don't accept the
new onSelectScope prop yet — landed in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Workers gains a files button on each workspace card; Sessions makes
session rows full-row click targets with a hover chevron. Both pass the
scope upward via an optional onSelectScope callback so standalone usage
(no callback) is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The main page is now a split layout with workspaces on the left and a
scope-aware file explorer on the right.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(main-page): binary-safe uploads in WorkerFileExplorer
Some checks failed
ci / nexus (pull_request) Failing after 1m17s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
72e284b738
handleUpload was decoding files via f.text() (UTF-8), which substituted
the replacement character for any non-text byte and corrupted binary
uploads (images, PDFs). Switch to bytesToBase64(arrayBuffer()) — same
helper the existing Config Explorer page uses. Promotes the existing
local bytesToBase64 in api/client.ts to an export.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lz added 8 commits 2026-05-24 11:15:41 +02:00
Nav items (Workspaces / + New / Config / Providers) sit on the left next
to the logo; Settings + Lock stay on the right. Buttons drop the border
in favor of a subtle background tint on active/hover, matching the
mockup's nav-btn pattern.
Screen readers were losing the active-nav signal after the bordered
style was dropped in 73a33c8 — the visual cue (yellow tint) is purely
sighted now. aria-current=page restores semantic equivalence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds util/time.ts with a deterministic relativeTime helper (12 unit
tests), swaps the workspace card's container_name · datetime label to
container_name · 2 days ago. MainSplit now passes its scope down to
Workers (and through to Sessions), so the currently-explored workspace
card gets a subtle accent outline. Card-action buttons drop to the
mockup's 4px 7px / 10px sizing without affecting buttons elsewhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the selectedScope prop (plumbed in Task 2) to highlight the
currently-explored session with a subtle running-green tint matching
the mockup. The branch line under the session name gets an explicit
muted gray instead of inheriting body text color.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review follow-up to bf5aef8: the row background turns green on
active, but the chevron stayed muted (and turned yellow on hover),
weakening the visual cue. Pin the chevron to var(--running) when active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The file tree dropped the bordered card look in favor of a spare,
muted-default IDE-style list per the mockup. File names now split into
name + .ext spans so the extension renders in a dimmer color; dotfiles
(.env) keep the bare name. Padding tightens to match the 3px 10px row
height of the mockup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The main page and the Config Explorer now share a single file-explorer
panel: toolbar with breadcrumb, scope chip (green for workspace, lime
for session, neutral for config), refresh / +file / +folder / upload /
save, plus md preview/source toggle on the right. Markdown preview
works in the Config Explorer too. The InstalledAssetsPanel opens files
through an exported openFile() method on the panel.

WorkerFileExplorer.svelte deleted (its body moved into FileExplorerPanel).
config-explorer/+page.svelte drops its bespoke toolbar + local file
state — the panel owns both now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(file-explorer): rename refresh()→reloadTree(), clean stale comments
Some checks failed
ci / nexus (pull_request) Failing after 1m10s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
6b6530a40d
Code-review follow-up to 9aef44d. The exposed reload-but-keep-selection
method was named refresh(), but the toolbar's ↺ button does a full
reload (drops selection). IDE convention has refresh as the
discard-cache variant, so the names were misleading. Renaming to
reloadTree() makes the intent — the InstalledAssetsPanel telling the
panel "an outside actor changed the disk, please re-fetch" — explicit.

Also cleaned the WorkerFileExplorer reference in the
config-explorer/FileTree.svelte shim comment (that component no longer
exists; FileExplorerPanel is the consumer now).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lz added 2 commits 2026-05-24 11:28:50 +02:00
Running workspace cards become click targets that select the workspace
as the file-explorer scope - the explicit [files] button is removed.
Inside the card, every action button (Stop / term / Logs / Remove / +
workspace agent) and the session rows stopPropagation so they don't
trigger the card-level scope select.

Active session rows now stretch their green tint to the card edges via
negative horizontal margins + matching padding, while keeping their
content position. The 3px health border on the card's left edge stays
visible (border-box semantics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ui: softer primary buttons, drop +New header link, smaller session btns
Some checks failed
ci / nexus (pull_request) Failing after 1m9s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
aa0c7bb376
Three coupled polish items from the live preview:

- Adds .btn.primary-soft to app.css (accent-tinted bg + accent text/border,
  same shape as the mockup's .btn.files-primary). Applied to the three
  buttons the user flagged as too contrasted: "+ New workspace",
  "+ session", "+ workspace agent".
- Drops "+ New" from the header nav. The bottom-of-list "+ New workspace"
  button covers the action; the header link was redundant.
- Extends the existing .btn.sm size override from .card-actions only to
  the whole .card scope so session-row buttons (term, ×) and the
  add-session form's "+ session" button shrink to match the mockup
  (4px 7px / 10px font).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ui: revert primary-soft, reorder + relabel workspace agent button
Some checks failed
ci / nexus (pull_request) Failing after 1m7s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
3a3bd63ece
Three coupled polish items:

- Revert .btn.primary-soft. The mockup's primary buttons are solid
  yellow + black text (the loud .btn.primary), not tinted. The
  "less contrasted" feel comes from the smaller .btn.sm size already
  applied inside cards, not from softer colors. Drops the unused
  .btn.primary-soft rule from app.css; restores .btn.primary on
  "+ New workspace", "+ session".

- Workspace agent button moves to the LEFT of Remove. Drops the
  margin-left:auto that anchored it right; now Stop / term / Logs /
  start-or-stop-agent / Remove flow left-to-right.

- Shorter labels: "+ workspace agent" → "start agent", "× workspace
  agent" → "stop agent". The button's color (primary yellow for start,
  danger red for stop) already communicates intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lz force-pushed feat/session-explorer from 3a3bd63ece
Some checks failed
ci / nexus (pull_request) Failing after 1m7s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
to ec75a8b926
Some checks failed
ci / nexus (pull_request) Failing after 1m9s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
2026-05-24 11:52:09 +02:00
Compare
lz merged commit 8381c60748 into main 2026-05-24 11:54:19 +02:00
lz deleted branch feat/session-explorer 2026-05-24 11:56:38 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!19
No description provided.