feat(artifacts): agent-registered files with operator annotations #31

Manually merged
lz merged 37 commits from feat/artifacts into main 2026-05-28 22:57:52 +02:00
Owner

Summary

Adds an Artifacts feature. Agents in worker containers register files (plans, design docs, reports) with Nexus via notify-artifact <path> [label]. The operator sees them in a new Artifacts tab, can annotate markdown with text-selection comments via mark.js overlays, and submits structured feedback back to the running Claude session as a .nexus/feedback/<ts>-<slug>.md file plus a tmux send-keys nudge.

Also adds Docker Desktop support via docker-compose.override.yml so docker compose up -d works on Windows/macOS — the base file's network_mode: host is Linux-only.

Design: docs/superpowers/specs/2026-05-27-artifacts-design.md. Plan: docs/superpowers/plans/2026-05-27-artifacts.md.

What ships

  • notify-artifact POSIX-sh helper + bridge-IP-guarded agent endpoint at /api/agent/artifacts. Re-registering the same path drops prior comments (they reference the old revision).
  • Artifacts tab: list + viewer panel; pin from File Explorer; unpin with confirm-if-comments; markdown viewer with selection → comment popover → anchored highlights; sidebar with open/submitted cards.
  • Pop-out window route (/sessions/[sessionId]/artifacts/[artifactId]) sharing the same viewer + controller as the in-app panel.
  • Submit flow: writes the feedback file via WorkerFsService (Nexus has no /workspace mount), tmux-nudges the live session window, flips comment statuses, defaults to auto-pressing Enter so the operator doesn't have to alt-tab into the terminal.
  • New-artifact toast + browser-tab title badge (burst-windowed).
  • public_url instance setting → notify-artifact response includes an Open: <url> line; @xterm/addon-web-links makes it ctrl-clickable in the session terminal. Updated default CLAUDE.md instructs Claude to paste that URL back into chat (operators can't see tool I/O).
  • Docker Desktop: override file drops host-mode, publishes 127.0.0.1:3001, attaches Nexus to the workers bridge with a static IP. New WORKER_CALLBACK_URL env var lets the operator override the workers-bridge-gateway URL derivation (required on Docker Desktop where the gateway routes to the Linux VM, and where host.docker.internal would NAT the worker's source IP and break agent-ip-guard).

Schema + size

  • Migration 0009_artifacts: artifacts (UNIQUE(session_id, path), FK CASCADE) + artifact_comments (FK CASCADE).
  • New settings registry entry: public_url (instance, ^https?://.+ or empty).
  • New deps: mark.js + @types/mark.js, @xterm/addon-web-links.
  • 423 tests pass, 0 typecheck errors (when the lockfile is in sync — see notes below).

Notes worth knowing during review

  • Filesystem boundary: Nexus has no bind-mount to /workspace. All worker-side file IO goes through WorkerFsService (docker exec with atomic temp-file + mv) or execAndCapture. The submit-artifact service is filesystem-agnostic — it injects a writeFeedback(relPath, content) callback for test isolation.
  • Multiplexed exec stream: Docker's exec stdout is 8-byte-framed when Tty=false. The submit endpoint uses the shared execAndCapture helper rather than raw byte concat — earlier inline implementations corrupted tmux list-windows output.
  • Source-line stamping: v1 saves comments with line_start/end: null and mark.js matches first occurrence. The marked renderer doesn't emit reliable per-block source-line attributes; the AnnotatableMarkdown component has an inert distance-heuristic ready for when this lands as a follow-up.
  • Polling latency: the in-app artifact list polls every 5s (followed by a 2s toast burst-window) — up to 7s between notify-artifact and the toast appearing. Tracked as #34; the broader SSE migration covering previews/sessions/workers too is #35.
  • Lockfile drift: nexus/pnpm-lock.yaml has been stale since branch commit be6b6ae (the @xterm/* scoped-package migration). Local pnpm run check shows 10 unresolved-import errors against @xterm/* and mark.js. Linux Docker build still works (--frozen-lockfile=false). Worth a separate chore: refresh lockfile after this merges.

Tested

Driven end-to-end on Docker Desktop (Windows 11):

  • notify-artifact roundtrip → row + toast → click → markdown renders.
  • Highlight passage → composer stays inside viewport even near edges → Ctrl+Enter to save → mark.js overlay paints + sidebar card appears live (in-panel AND pop-out).
  • Mid-word selections highlight (accuracy: partially); highlighted text readable on dark theme (color: inherit).
  • Approve & continue and Submit comments both auto-press Enter in the Claude session, then flip the right-pane tab back to Terminal.
  • Comment sidebar footer stays pinned with many comments (forced via DB inserts).
  • Pop-out markdown column uses the full viewport width (no global 640px cap leakage).
  • pnpm test: 423 passed, 79 skipped (the skipped are the pre-existing better-sqlite3 native-binding gate on Windows).

Not exercised: the base docker-compose.yml on a real Linux host (only the Docker Desktop override path was driven); pin/unpin/re-register flows (unchanged on this branch and previously verified).

Test plan for the merger

  • On Linux: docker compose up -d (with override file removed or absent) — confirm host-mode still works as before.
  • On Docker Desktop: docker compose up -d — confirm Nexus UI at http://localhost:3001.
  • Set public_url in Settings → Instance.
  • Spawn a worker, open a session, attach to its terminal.
  • echo "# my plan" > /tmp/p.md && notify-artifact /tmp/p.md "phase 1" — verify Open: <url> line, toast in Nexus, row in Artifacts tab.
  • Highlight a mid-word slice → Comment → Ctrl+Enter; verify overlay + sidebar card without F5.
  • Open the pop-out (↗); repeat near the bottom-right corner; verify composer doesn't clip.
  • Submit the comment; verify .nexus/feedback/<ts>-...md exists in the worktree AND Claude received the prompt + Enter; verify the tab auto-flips to Terminal.
  • Re-run notify-artifact /tmp/p.md "phase 1 v2"; verify prior comments cleared.
  • Pin from File Explorer; unpin a file with comments (confirm prompt); Approve & continue with zero comments.
  • Reset default CLAUDE.md to pick up the URL-reporting guidance.
## Summary Adds an **Artifacts** feature. Agents in worker containers register files (plans, design docs, reports) with Nexus via `notify-artifact <path> [label]`. The operator sees them in a new **Artifacts** tab, can annotate markdown with text-selection comments via mark.js overlays, and submits structured feedback back to the running Claude session as a `.nexus/feedback/<ts>-<slug>.md` file plus a tmux `send-keys` nudge. Also adds Docker Desktop support via `docker-compose.override.yml` so `docker compose up -d` works on Windows/macOS — the base file's `network_mode: host` is Linux-only. Design: [`docs/superpowers/specs/2026-05-27-artifacts-design.md`](docs/superpowers/specs/2026-05-27-artifacts-design.md). Plan: [`docs/superpowers/plans/2026-05-27-artifacts.md`](docs/superpowers/plans/2026-05-27-artifacts.md). ## What ships - `notify-artifact` POSIX-sh helper + bridge-IP-guarded agent endpoint at `/api/agent/artifacts`. Re-registering the same path drops prior comments (they reference the old revision). - Artifacts tab: list + viewer panel; pin from File Explorer; unpin with confirm-if-comments; markdown viewer with selection → comment popover → anchored highlights; sidebar with open/submitted cards. - Pop-out window route (`/sessions/[sessionId]/artifacts/[artifactId]`) sharing the same viewer + controller as the in-app panel. - Submit flow: writes the feedback file via `WorkerFsService` (Nexus has no `/workspace` mount), tmux-nudges the live session window, flips comment statuses, defaults to auto-pressing Enter so the operator doesn't have to alt-tab into the terminal. - New-artifact toast + browser-tab title badge (burst-windowed). - `public_url` instance setting → `notify-artifact` response includes an `Open: <url>` line; `@xterm/addon-web-links` makes it ctrl-clickable in the session terminal. Updated default CLAUDE.md instructs Claude to paste that URL back into chat (operators can't see tool I/O). - Docker Desktop: override file drops host-mode, publishes `127.0.0.1:3001`, attaches Nexus to the workers bridge with a static IP. New `WORKER_CALLBACK_URL` env var lets the operator override the workers-bridge-gateway URL derivation (required on Docker Desktop where the gateway routes to the Linux VM, and where `host.docker.internal` would NAT the worker's source IP and break `agent-ip-guard`). ## Schema + size - Migration `0009_artifacts`: `artifacts` (UNIQUE(session_id, path), FK CASCADE) + `artifact_comments` (FK CASCADE). - New settings registry entry: `public_url` (instance, `^https?://.+` or empty). - New deps: `mark.js` + `@types/mark.js`, `@xterm/addon-web-links`. - 423 tests pass, 0 typecheck errors (when the lockfile is in sync — see notes below). ## Notes worth knowing during review - **Filesystem boundary**: Nexus has no bind-mount to `/workspace`. All worker-side file IO goes through `WorkerFsService` (`docker exec` with atomic temp-file + mv) or `execAndCapture`. The submit-artifact service is filesystem-agnostic — it injects a `writeFeedback(relPath, content)` callback for test isolation. - **Multiplexed exec stream**: Docker's exec stdout is 8-byte-framed when `Tty=false`. The submit endpoint uses the shared `execAndCapture` helper rather than raw byte concat — earlier inline implementations corrupted `tmux list-windows` output. - **Source-line stamping**: v1 saves comments with `line_start/end: null` and mark.js matches first occurrence. The `marked` renderer doesn't emit reliable per-block source-line attributes; the `AnnotatableMarkdown` component has an inert distance-heuristic ready for when this lands as a follow-up. - **Polling latency**: the in-app artifact list polls every 5s (followed by a 2s toast burst-window) — up to 7s between `notify-artifact` and the toast appearing. Tracked as **#34**; the broader SSE migration covering previews/sessions/workers too is **#35**. - **Lockfile drift**: `nexus/pnpm-lock.yaml` has been stale since branch commit `be6b6ae` (the `@xterm/*` scoped-package migration). Local `pnpm run check` shows 10 unresolved-import errors against `@xterm/*` and `mark.js`. Linux Docker build still works (`--frozen-lockfile=false`). Worth a separate `chore: refresh lockfile` after this merges. ## Tested Driven end-to-end on Docker Desktop (Windows 11): - `notify-artifact` roundtrip → row + toast → click → markdown renders. - Highlight passage → composer stays inside viewport even near edges → Ctrl+Enter to save → mark.js overlay paints + sidebar card appears live (in-panel AND pop-out). - Mid-word selections highlight (`accuracy: partially`); highlighted text readable on dark theme (`color: inherit`). - Approve & continue and Submit comments both auto-press Enter in the Claude session, then flip the right-pane tab back to Terminal. - Comment sidebar footer stays pinned with many comments (forced via DB inserts). - Pop-out markdown column uses the full viewport width (no global 640px cap leakage). - `pnpm test`: 423 passed, 79 skipped (the skipped are the pre-existing `better-sqlite3` native-binding gate on Windows). Not exercised: the base `docker-compose.yml` on a real Linux host (only the Docker Desktop override path was driven); pin/unpin/re-register flows (unchanged on this branch and previously verified). ## Test plan for the merger - [x] On Linux: `docker compose up -d` (with override file removed or absent) — confirm host-mode still works as before. - [x] On Docker Desktop: `docker compose up -d` — confirm Nexus UI at `http://localhost:3001`. - [x] Set `public_url` in Settings → Instance. - [x] Spawn a worker, open a session, attach to its terminal. - [x] `echo "# my plan" > /tmp/p.md && notify-artifact /tmp/p.md "phase 1"` — verify `Open: <url>` line, toast in Nexus, row in Artifacts tab. - [x] Highlight a mid-word slice → Comment → Ctrl+Enter; verify overlay + sidebar card without F5. - [x] Open the pop-out (↗); repeat near the bottom-right corner; verify composer doesn't clip. - [x] Submit the comment; verify `.nexus/feedback/<ts>-...md` exists in the worktree AND Claude received the prompt + Enter; verify the tab auto-flips to Terminal. - [x] Re-run `notify-artifact /tmp/p.md "phase 1 v2"`; verify prior comments cleared. - [x] Pin from File Explorer; unpin a file with comments (confirm prompt); Approve & continue with zero comments. - [x] Reset default CLAUDE.md to pick up the URL-reporting guidance.
lz added 33 commits 2026-05-28 01:15:54 +02:00
docs(agent-instructions): broaden dev-server host/origin guidance
All checks were successful
ci / nexus (pull_request) Successful in 2m17s
ci / images (./nexus, agent-nexus) (pull_request) Successful in 3m48s
ci / images (./worker, nexus-worker) (pull_request) Successful in 1m8s
5ed6d11056
Replace the narrow Vite-only paragraph with a Dev-server host/origin
checks subsection that explains the principle (operator's browser
carries the operator's hostname, not 127.0.0.1) and lists recipes for
Vite, SvelteKit/Astro/Nuxt/Remix, Next.js >=15.2, and CRA/webpack-dev-server.

Addresses the feedback on PR #22: pre-warn agents about
allowedDevOrigins / allowedHosts / WDS_SOCKET_HOST so dev-server origin
checks can be configured before the first browser request fails.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec for a new Artifacts right-pane tab and `notify-artifact` worker helper,
mirroring the previews flow. Markdown artifacts get a select-text annotation
UI; submit-back writes a Plannotator-style feedback file into the worktree
and nudges the session's Claude via `tmux send-keys`. Pop-out route allows
viewing/annotating an artifact in a dedicated browser window alongside the
terminal.
xterm.js renamed its npm packages to the @xterm/* scope; legacy
xterm/xterm-addon-* still publish but are EOL. Bump to the latest
v6 line at the same time and swap the canvas renderer for WebGL,
since canvas-rendering was removed from xterm.js core in v6.

  xterm@^5.3.0                  → @xterm/xterm@^6.0.0
  xterm-addon-fit@^0.8.0        → @xterm/addon-fit@^0.11.0
  xterm-addon-canvas@^0.5.0     → @xterm/addon-webgl@^0.19.0

SessionTerminal.svelte updates:
- type-only imports rewritten to the scoped names
- dynamic-import block in onMount loads WebglAddon in place of
  CanvasAddon (the box-drawing-seam fix the canvas addon provided
  carries over — WebGL renders glyphs cell-perfect the same way)
- inline comment updated to mention WebGL as the v6 successor

CSS class names (.xterm, .xterm-viewport) emitted by the library are
unchanged across the rename, so the existing :global() selectors
keep working.

Also: the artifacts spec at docs/superpowers/specs/2026-05-27-
artifacts-design.md updated to reference @xterm/addon-web-links
under the new scoped naming and drop the v5-pin justification.

Verified: pnpm typecheck clean, 474/474 vitest tests pass.
Five phases:
1. Foundation: migration + service core + agent endpoint + notify-artifact CLI
2. Operator UI read path: tab + list + Pin button
3. Annotation: comment endpoints + AnnotatableMarkdown + CommentSidebar
4. Feedback loop: feedback-format + tmux-nudge + submit endpoint
5. Polish: public_url + web-links addon + toast + pop-out + CLAUDE.md

Each task is TDD-driven with frequent commits. Manual smoke gate at task 26.
Nexus has no bind mount to the worker /workspace volume, so the
original fs.realpath / fs.stat calls always ENOENT'd, breaking
notify-artifact entirely. Route file IO through the same
WorkerFsService that the operator GET endpoint uses (Task 8).
Adds a "pin" button to the FileExplorerPanel toolbar, visible only when
the operator is in session scope (artifacts are per-session). MainSplit
threads the new pinTarget={workerId, sessionId} prop only for session
scopes; workspace/config scopes hide the button entirely.

Clicking pin POSTs the selected file's path to the session's artifacts
endpoint and toasts the outcome ("Pinned to Artifacts" for new pins,
"Already in Artifacts (re-pinned)" for repeats). Binary files are
disabled at the button level since they can't be rendered as artifacts.

Deviation from the plan template: FileEditor.svelte and
MarkdownView.svelte are pure viewers with no chrome — the surrounding
toolbar lives in FileExplorerPanel. Adding the button there satisfies
"visible on both file and markdown views" automatically and avoids
introducing per-viewer toolbars.
Operator-facing endpoints for adding and deleting artifact comments. Auth
is gated globally in hooks.server.ts (no per-handler check). POST validates
the body via zod (anchor_text/body length caps from artifacts/types, line
range refinement) and verifies the artifact belongs to the URL session.
DELETE joins artifact_comments to artifacts and 404s unless the comment's
artifact is owned by the URL session.
Sequential session->artifact->comment lookups attribute each 404 to the
right resource and reject requests where commentId belongs to a comment
under a different artifactId in the same session.
- Drop unused untrack() wrapper around mark.js IIFE; keep void reads
  for explicit dependency registration.
- Document that the each() distance heuristic is inert in v1 because
  lineFromNode always returns null; kept ready for future
  data-source-line stamping.
- Add aria-label to the composer textarea.
Docker's exec stdout is 8-byte-framed when Tty=false (per-chunk header:
[stream_type, 0, 0, 0, len32]). The inline `execInContainer` helper in
the submit endpoint treated those bytes as raw UTF-8, so every line of
output got a garbage prefix. `tmux list-windows -F '#W'` parsing failed
silently and every real submission returned HTTP 207 prompt_typed:false.

Drop the hand-rolled stream reader and reuse `execAndCapture` from
`$server/lib/docker-exec`, which already demuxes the framing correctly.
feat(artifacts): document notify-artifact in default CLAUDE.md
All checks were successful
ci / nexus (pull_request) Successful in 2m43s
ci / images (./nexus, agent-nexus) (pull_request) Successful in 4m0s
ci / images (./worker, nexus-worker) (pull_request) Successful in 2m5s
b4ed005836
lz added 4 commits 2026-05-28 22:15:05 +02:00
Enable `docker compose up -d` on Docker Desktop (Windows/macOS), where
the Linux-only host-mode setup doesn't work — published host ports
don't bind inside the Docker VM where host-mode containers actually
live. `docker-compose.override.yml` (auto-merged by compose) drops
`network_mode: host` on the nexus service, attaches it to the
workers bridge with a fixed static IP, and publishes 3001 to the
host.

`WORKER_CALLBACK_URL` is the operator override for the URL workers
POST callbacks to (notify-preview, notify-artifact). It takes
precedence over the bridge-gateway derivation in
`resolveWorkerNetwork`. Required on Docker Desktop because the
gateway IP routes to the Linux VM, not to Nexus, and routing via
`host.docker.internal` would NAT the worker's source IP and break
`agent-ip-guard`. The override is set to Nexus's static bridge IP
(e.g. `http://172.30.0.2:3001`) so worker callbacks arrive with
their real bridge IP intact. Workers see this value as `NEXUS_URL`
in their environment.

Drive-by revert: `docker-compose.yml` had accidentally dropped the
`127.0.0.1:` loopback restriction on the docker-proxy port mapping
in an earlier iteration. Restore it — the comment above already
says "Loopback-only", and the override resets it to `2375:2375` for
Docker Desktop where the proxy must be reachable from inside the
Docker VM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ArtifactsPanel` (the in-app sidebar) and the artifact pop-out route
both held a per-artifact "load → optimistic edit → refetch" state
machine inline. The two copies duplicated annotation tracking,
add/delete handlers, submit-with-spinner, and stale-fetch guards —
and were already drifting (the pop-out treated the new `interactive`
ArtifactKind branch differently because the panel hadn't been
updated yet).

Lift that orchestration into `ArtifactDetailController` (a Svelte 5
runes class) and the rendering into the shared `ArtifactViewer`
component. The panel and pop-out now hold a controller instance and
render `<ArtifactViewer>` with the same props.

The panel's view column was `<main>` — combined with the global
`main { max-width: 640px }` rule in `app.css`, that capped artifact
content at 640px wide regardless of viewport. Swap to `<section>` so
the wider artifact UI is unaffected. The pop-out's body wrapper
keeps `<main>` for app-shell purposes but adds a `:has(.popout)`
override that lifts the same cap.

`MainSplit` threads `sessionLabel` (for the viewer header) and
`onAfterSubmit` (so the parent can close the panel after submit)
into the panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `AnnotatableMarkdown`: mark.js `accuracy: 'exactly'` only matches
  whole words, so mid-word selections silently dropped their
  highlight. Switch to `'partially'` so any substring selection
  paints.
- `AnnotatableMarkdown`: the browser's UA stylesheet forces `<mark>`
  to `color: black`, which made highlighted text on a darker theme
  unreadable. Add `color: inherit` on `.annot`.
- `AnnotatableMarkdown`: the comment composer was positioned at the
  raw selection coordinates, so a near-edge selection clipped the
  popover. Clamp X to the viewport, and if the bottom edge would
  clip, flip the popover above the selection instead.
- `AnnotatableMarkdown`: Ctrl/Cmd+Enter inside the composer now
  submits — matches the convention used in the rest of the app. The
  Save button's tooltip advertises it.
- `CommentSidebar`: the action footer (Submit / Cancel) used to flow
  with the comment list, so it scrolled off-screen with many
  comments. Wrap the list in a `.scroll-area` and pin the footer
  outside it so it always sticks at the bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(artifacts): default submit auto-send + strengthen URL reporting guidance
Some checks failed
ci / nexus (pull_request) Failing after 1m26s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
19b5ea47f7
`auto_send` on the submit endpoint now defaults to `true`. When an
operator clicks "Approve & continue" or "Submit comments" in the UI
they want the feedback delivered to Claude, not just typed at the
prompt — having to alt-tab into the terminal to hit Enter is a
papercut nobody asked for. The flag stays opt-out for callers (e.g.
scripted submits) that genuinely just want to stage the prompt.

Strengthen `default-CLAUDE.md` so Claude actually pastes the
`notify-artifact` URL into the chat after registering an artifact.
Operators only see Claude's chat output — they don't see tool I/O
or stdout from helper scripts — so a silent registration leaves them
hunting for the artifact in the side panel. The instructions now
explicitly call for echoing the URL back to the operator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lz force-pushed feat/artifacts from 19b5ea47f7
Some checks failed
ci / nexus (pull_request) Failing after 1m26s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
to 24758d27ae
Some checks failed
ci / nexus (pull_request) Failing after 1m42s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
2026-05-28 22:35:31 +02:00
Compare
lz force-pushed feat/artifacts from 24758d27ae
Some checks failed
ci / nexus (pull_request) Failing after 1m42s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
to efdaabe724
Some checks failed
ci / nexus (pull_request) Failing after 1m27s
ci / images (./nexus, agent-nexus) (pull_request) Has been skipped
ci / images (./worker, nexus-worker) (pull_request) Has been skipped
ci / nexus (push) Failing after 1m45s
ci / images (./nexus, agent-nexus) (push) Has been skipped
ci / images (./worker, nexus-worker) (push) Has been skipped
2026-05-28 22:57:13 +02:00
Compare
lz manually merged commit efdaabe724 into main 2026-05-28 22:57:52 +02:00
lz deleted branch feat/artifacts 2026-05-28 23:03:37 +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!31
No description provided.