feat(artifacts): agent-registered files with operator annotations #31
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/artifacts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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>.mdfile plus a tmuxsend-keysnudge.Also adds Docker Desktop support via
docker-compose.override.ymlsodocker compose up -dworks on Windows/macOS — the base file'snetwork_mode: hostis Linux-only.Design:
docs/superpowers/specs/2026-05-27-artifacts-design.md. Plan:docs/superpowers/plans/2026-05-27-artifacts.md.What ships
notify-artifactPOSIX-sh helper + bridge-IP-guarded agent endpoint at/api/agent/artifacts. Re-registering the same path drops prior comments (they reference the old revision)./sessions/[sessionId]/artifacts/[artifactId]) sharing the same viewer + controller as the in-app panel.WorkerFsService(Nexus has no/workspacemount), 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.public_urlinstance setting →notify-artifactresponse includes anOpen: <url>line;@xterm/addon-web-linksmakes 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).127.0.0.1:3001, attaches Nexus to the workers bridge with a static IP. NewWORKER_CALLBACK_URLenv var lets the operator override the workers-bridge-gateway URL derivation (required on Docker Desktop where the gateway routes to the Linux VM, and wherehost.docker.internalwould NAT the worker's source IP and breakagent-ip-guard).Schema + size
0009_artifacts:artifacts(UNIQUE(session_id, path), FK CASCADE) +artifact_comments(FK CASCADE).public_url(instance,^https?://.+or empty).mark.js+@types/mark.js,@xterm/addon-web-links.Notes worth knowing during review
/workspace. All worker-side file IO goes throughWorkerFsService(docker execwith atomic temp-file + mv) orexecAndCapture. The submit-artifact service is filesystem-agnostic — it injects awriteFeedback(relPath, content)callback for test isolation.Tty=false. The submit endpoint uses the sharedexecAndCapturehelper rather than raw byte concat — earlier inline implementations corruptedtmux list-windowsoutput.line_start/end: nulland mark.js matches first occurrence. Themarkedrenderer doesn't emit reliable per-block source-line attributes; theAnnotatableMarkdowncomponent has an inert distance-heuristic ready for when this lands as a follow-up.notify-artifactand the toast appearing. Tracked as #34; the broader SSE migration covering previews/sessions/workers too is #35.nexus/pnpm-lock.yamlhas been stale since branch commitbe6b6ae(the@xterm/*scoped-package migration). Localpnpm run checkshows 10 unresolved-import errors against@xterm/*andmark.js. Linux Docker build still works (--frozen-lockfile=false). Worth a separatechore: refresh lockfileafter this merges.Tested
Driven end-to-end on Docker Desktop (Windows 11):
notify-artifactroundtrip → row + toast → click → markdown renders.accuracy: partially); highlighted text readable on dark theme (color: inherit).pnpm test: 423 passed, 79 skipped (the skipped are the pre-existingbetter-sqlite3native-binding gate on Windows).Not exercised: the base
docker-compose.ymlon 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
docker compose up -d(with override file removed or absent) — confirm host-mode still works as before.docker compose up -d— confirm Nexus UI athttp://localhost:3001.public_urlin Settings → Instance.echo "# my plan" > /tmp/p.md && notify-artifact /tmp/p.md "phase 1"— verifyOpen: <url>line, toast in Nexus, row in Artifacts tab..nexus/feedback/<ts>-...mdexists in the worktree AND Claude received the prompt + Enter; verify the tab auto-flips to Terminal.notify-artifact /tmp/p.md "phase 1 v2"; verify prior comments cleared.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.`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>19b5ea47f724758d27ae24758d27aeefdaabe724