feat(sessions): archive worker sessions from the sidebar row#180
feat(sessions): archive worker sessions from the sidebar row#180ashish921998 wants to merge 6 commits into
Conversation
Terminated workers can now be archived to declutter the sidebar without
destroying the row. Right-clicking a terminated worker offers Archive
next to Restore; archived workers move behind a collapsed "Archived (n)"
disclosure at the end of the project's worker list, where Unarchive
brings them back.
Daemon side, sessions gain a nullable archived_at (migration 0011,
mirroring the projects soft-delete pattern) with POST
/sessions/{id}/archive and /unarchive routes and an `archived` list
filter. Archiving requires a terminated session (409
SESSION_NOT_TERMINATED otherwise) and restore clears the flag, so a
running agent can never be hidden. The sessions CDC update trigger is
recreated so archive flips fan out session_updated events; the full
UpdateSession write deliberately leaves archived_at untouched so
lifecycle writes can't clobber user intent.
Also removes a stray aria-hidden on the context-menu overlay that hid
every menu item (kill/restore included) from the accessibility tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e archive UI to the new shell The base branch now carries main's agent-orchestrator-clone shell, which deleted App.tsx/SideRail.tsx and rewrote the Sidebar this feature had wired its archive UI into. The backend is untouched by the redesign; the UI re-lands on the new surfaces: - Sidebar worker rows get a right-click context menu (the ui/context-menu component survived the redesign): Archive on terminated workers, Unarchive on archived ones, no menu on running workers. - Archived workers move behind a per-project "Archived (n)" disclosure at the end of the session sub-list, dimmed, still navigable. - useWorkspaceQuery splits archivedSessions out of sessions so the kanban board, sidebar counts, and every other default surface ignore archived workers; SessionView's lookup includes them so their rows still open. - archive/unarchive mutations live in the _shell route beside createProject/createTask and invalidate the workspace query. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThis PR adds soft-archive/unarchive for terminated worker sessions across the full stack: a new
Confidence Score: 5/5Safe to merge — the new archive/unarchive paths are well-guarded at both the service and SQL layers, with no data-corruption risk. The running-session invariant is enforced twice: the service rejects non-terminated sessions with a 409, and the SQL WHERE is_terminated=1 guard prevents a concurrent restore from slipping through. The UpdateSession write deliberately omits archived_at, so lifecycle operations can never clobber user intent. The frontend ContextMenu properly awaits onSelect() and renders inline errors, so API failures reach the user. The store-layer round-trip test verifies UpdateSession cannot overwrite archived_at, and the service tests cover all the key invariant paths. No files require special attention. Important Files Changed
Reviews (3): Last reviewed commit: "Merge branch 'feat/wire-core-workflows' ..." | Re-trigger Greptile |
| INSERT INTO change_log (project_id, session_id, event_type, payload, created_at) | ||
| VALUES (NEW.project_id, NEW.id, 'session_updated', | ||
| json_object('id', NEW.id, 'activity', NEW.activity_state, 'isTerminated', json(CASE WHEN NEW.is_terminated THEN 'true' ELSE 'false' END)), | ||
| NEW.updated_at); |
There was a problem hiding this comment.
CDC event payload omits
isArchived
The trigger fires correctly when archived_at changes, but the json_object payload only carries id, activity, and isTerminated. A session_updated event triggered by an archive or unarchive flip gives downstream consumers no way to derive the new archive state from the event alone — they must issue a separate GET /sessions/{id} to learn it. The frontend currently invalidates and re-fetches on every archive action, so this doesn't break the current UI. Future CDC consumers that expect the payload to be a self-contained diff (as isTerminated already is) will silently receive stale data on archive flips. Adding isArchived: CASE WHEN NEW.archived_at IS NOT NULL THEN json('true') ELSE json('false') END to the payload would make archive flips consistent with the existing terminated-state field.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| /** | ||
| * Archived workers, split out so every default surface (sidebar list, side | ||
| * rail, counts) ignores them; the sidebar shows them behind an | ||
| * "Archived (n)" disclosure. | ||
| */ | ||
| archivedSessions?: WorkspaceSession[]; |
There was a problem hiding this comment.
archivedSessions is typed as optional (archivedSessions?) but useWorkspaceQuery always populates it unconditionally. Every call site that accesses it already uses ?? [] to guard the optional, adding noise. Making the field required avoids scattered defensive fallbacks and keeps the type honest with how it is actually built.
| /** | |
| * Archived workers, split out so every default surface (sidebar list, side | |
| * rail, counts) ignores them; the sidebar shows them behind an | |
| * "Archived (n)" disclosure. | |
| */ | |
| archivedSessions?: WorkspaceSession[]; | |
| /** | |
| * Archived workers, split out so every default surface (sidebar list, side | |
| * rail, counts) ignores them; the sidebar shows them behind an | |
| * "Archived (n)" disclosure. | |
| */ | |
| archivedSessions: WorkspaceSession[]; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| onSessionContextMenu: (event: React.MouseEvent, session: WorkspaceSession) => void; | ||
| }) { | ||
| const projectActive = selection.activeProjectId === workspace.id && !selection.activeSessionId; | ||
| const archivedSessions = workspace.archivedSessions ?? []; |
There was a problem hiding this comment.
With
archivedSessions always present on the workspace (built unconditionally in useWorkspaceQuery), the ?? [] fallback is a dead branch. If the type is made required (see workspace.ts comment), this can be simplified to remove the unnecessary fallback.
| const archivedSessions = workspace.archivedSessions ?? []; | |
| const archivedSessions = workspace.archivedSessions; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
I don't think we want to bring back archiving worker sessions. We can kill a session and restore it anyways so there is no need of archiving. |
…ions # Conflicts: # frontend/src/renderer/hooks/useWorkspaceQuery.ts
| if _, err := s.store.ArchiveSession(ctx, id, s.now().UTC()); err != nil { | ||
| return fmt.Errorf("archive %s: %w", id, err) | ||
| } | ||
| return nil |
There was a problem hiding this comment.
Silent false-success when restore races the archive SQL guard
The bool result from s.store.ArchiveSession is discarded (_, err). The SQL intentionally has an is_terminated = 1 guard to prevent a concurrent restore from slipping past the service-level IsTerminated check, but when that guard fires it returns 0 rows — and the service returns nil, causing an HTTP 200 {"ok": true} response even though nothing was archived. The session state is correct (the invariant holds), but the caller is told the archive succeeded when it was silently a no-op.
What
Terminated workers can now be archived to declutter the sidebar without destroying the row, and unarchived to bring them back.
archived_at(migration 0011, mirroring the projects soft-delete pattern);POST /sessions/{id}/archiveand/unarchiveroutes; anarchivedfilter onGET /sessions;isArchivedon the Session read model.SESSION_NOT_TERMINATED) and restore clears the flag, so an active agent can never be hidden. The fullUpdateSessionwrite never touchesarchived_at, so lifecycle writes can't clobber user intent. The sessions CDC update trigger is recreated so archive flips fan outsession_updatedevents.useWorkspaceQuerysplitsarchivedSessionsout ofsessions, so the kanban board, sidebar counts, and every other default surface ignore archived workers automatically;SessionViewstill resolves them so archived rows open normally.Notes
feat/wire-core-workflows); retarget tomainonce that merges. The UI was originally built on the old emdash shell and re-landed on the new agent-orchestrator-clone shell in the merge commit.ui/context-menucomponent (from feat: wire the core workflows — daemon status, orchestrator attach, git rail, lifecycle #178) hadaria-hiddenon its overlay, hiding every menu item from the accessibility tree — fixed here; the new tests query menu items by role.ao session list) shows archived sessions with no marker; a CLI flag/column is a possible follow-up.Verified
npm run lint(backend tests + golangci) green,go test -raceon touched packages green, spec drift/route parity green,npm run apiproduces no diff🤖 Generated with Claude Code