Skip to content

feat(sessions): archive worker sessions from the sidebar row#180

Open
ashish921998 wants to merge 6 commits into
feat/wire-core-workflowsfrom
feat/archive-worker-sessions
Open

feat(sessions): archive worker sessions from the sidebar row#180
ashish921998 wants to merge 6 commits into
feat/wire-core-workflowsfrom
feat/archive-worker-sessions

Conversation

@ashish921998

Copy link
Copy Markdown
Collaborator

What

Terminated workers can now be archived to declutter the sidebar without destroying the row, and unarchived to bring them back.

  • Sidebar row UX: right-click a terminated worker → Archive worker. The row leaves the default list and lands behind a collapsed "Archived (n)" disclosure at the end of that project's session list (dimmed, still navigable). Right-click an archived row → Unarchive worker. Running workers get no menu — archive is terminated-only.
  • Daemon: sessions gain a nullable archived_at (migration 0011, mirroring the projects soft-delete pattern); POST /sessions/{id}/archive and /unarchive routes; an archived filter on GET /sessions; isArchived on the Session read model.
  • Invariants: archiving a running session 409s (SESSION_NOT_TERMINATED) and restore clears the flag, so an active agent can never be hidden. The full UpdateSession write never touches archived_at, so lifecycle writes can't clobber user intent. The sessions CDC update trigger is recreated so archive flips fan out session_updated events.
  • Renderer: useWorkspaceQuery splits archivedSessions out of sessions, so the kanban board, sidebar counts, and every other default surface ignore archived workers automatically; SessionView still resolves them so archived rows open normally.

Notes

Verified

  • npm run lint (backend tests + golangci) green, go test -race on touched packages green, spec drift/route parity green, npm run api produces no diff
  • Frontend: 123 vitest tests green (new: store/service/controller archive tests, query-split test, 3 Sidebar context-menu/disclosure tests), typecheck clean

🤖 Generated with Claude Code

ashish921998 and others added 4 commits June 11, 2026 16:58
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-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds soft-archive/unarchive for terminated worker sessions across the full stack: a new archived_at nullable column (migration 0011), two new API routes with a 409 guard against archiving running sessions, a SQL-level race guard in the ArchiveSession statement, and a sidebar UX that splits archived workers behind a collapsible "Archived (n)" disclosure. It also fixes an aria-hidden accessibility regression on the context-menu overlay from #178.

  • Backend: migration adds archived_at, Archive/Unarchive service methods, archived list-filter, IsArchived on the read model, and CDC trigger recreation so archive flips produce session_updated events.
  • Frontend: useWorkspaceQuery splits archivedSessions out of sessions; the sidebar renders a right-click context menu on terminated rows (archive) and archived rows (unarchive); SessionView resolves archived rows so navigating to them still works.

Confidence Score: 5/5

Safe 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

Filename Overview
backend/internal/storage/sqlite/migrations/0011_add_session_archived_at.sql Adds archived_at column, recreates CDC trigger to fire on archive flips, includes correct Down migration; NULL-safe IS NOT comparison is used properly.
backend/internal/service/session/service.go Archive/Unarchive/Restore all enforce the running-session invariant; matchesSessionFilter logic for the new Archived pointer flag is correct; toSession populates IsArchived in both branches.
frontend/src/renderer/components/Sidebar.tsx Context-menu guard (sessionMenuItems) correctly gates archive on status===terminated and unarchive on session.archived; the ContextMenu component handles async errors with inline display.
frontend/src/renderer/hooks/useWorkspaceQuery.ts Clean split of archivedSessions from sessions; archived field always set to session.isArchived ?? false so downstream code never sees undefined.
frontend/src/renderer/components/ui/context-menu.tsx Removes aria-hidden from the full-screen overlay so menu items are accessible to screen readers; run() already awaits onSelect() and renders inline errors.

Reviews (3): Last reviewed commit: "Merge branch 'feat/wire-core-workflows' ..." | Re-trigger Greptile

Comment thread backend/internal/service/session/service.go
Comment on lines +25 to +28
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Comment on lines +233 to +238
/**
* 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[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
/**
* 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 ?? [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

@harshitsinghbhandari

Copy link
Copy Markdown
Collaborator

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
Comment on lines +222 to +225
if _, err := s.store.ArchiveSession(ctx, id, s.now().UTC()); err != nil {
return fmt.Errorf("archive %s: %w", id, err)
}
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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