Skip to content

Add archived shell snapshot support#2593

Open
juliusmarminge wants to merge 7 commits intomainfrom
t3code/ddbdeb2b
Open

Add archived shell snapshot support#2593
juliusmarminge wants to merge 7 commits intomainfrom
t3code/ddbdeb2b

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented May 8, 2026

Summary

  • Adds a dedicated getArchivedShellSnapshot orchestration read path so archived threads can be loaded separately from the main shell snapshot.
  • Filters active shell queries to exclude archived threads, and adds archived-thread snapshot queries and supporting migration indexes.
  • Updates WebSocket routing and web settings UI to fetch, display, refresh, unarchive, and delete archived threads from connected environments.
  • Extends tests and fixtures across server and web layers for the new archived snapshot contract.

Testing

  • bun fmt
  • bun lint
  • bun typecheck
  • bun run test
  • Not run: manual browser verification of the archived threads settings panel

Note

Medium Risk
Introduces a new archived-thread read path and changes shell snapshot filtering, which can affect clients relying on the previous snapshot contents. Also adds new RPC wiring and SQL query/index changes that could impact performance or data returned if incorrect.

Overview
Adds a dedicated ProjectionSnapshotQuery.getArchivedShellSnapshot read path that returns only archived thread shell summaries, while updating getShellSnapshot (and related targeted queries) to exclude archived threads.

Wires this through a new WebSocket RPC orchestration.getArchivedShellSnapshot (contracts, server routing, web RPC client/environment API) and updates live shell event handling so thread.archived removes the thread and thread.unarchived re-upserts it.

Updates the web Archived Threads settings panel to fetch archived snapshots per environment via a new SWR/atom-backed useArchivedThreadSnapshots, and refreshes that state after archive/unarchive/delete actions; adds a DB migration creating indexes to support the active vs archived shell queries, and extends test mocks/fixtures accordingly.

Reviewed by Cursor Bugbot for commit f9fc1ec. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add getArchivedShellSnapshot RPC to expose archived threads in the settings panel

  • Adds a new getArchivedShellSnapshot query to ProjectionSnapshotQuery that returns only archived (non-deleted, archived_at IS NOT NULL) threads with their sessions and latest turns, while getShellSnapshot is updated to exclude archived threads.
  • Adds database indexes via migration 030 to support efficient active/archived thread shell queries.
  • Wires the new RPC through contracts, WebSocket layer, RPC client, and EnvironmentApi so the web client can call orchestration.getArchivedShellSnapshot.
  • Updates the Archived Threads panel in settings to load data via the new useArchivedThreadSnapshots hook, with loading/error states and auto-refresh after archive/unarchive/delete actions.
  • Behavioral Change: getShellSnapshot no longer includes archived threads; callers that previously saw archived threads in the main snapshot will no longer see them there.

Macroscope summarized f9fc1ec.

- Split archived threads into a dedicated shell snapshot query and RPC
- Update server and web clients to surface archived threads separately
- Add migration indexes and coverage for archived thread flows
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4aef964e-3376-422d-880a-8e167a02afb3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/ddbdeb2b

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels May 8, 2026
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Race condition in useEffect can show stale archived data
    • Added a monotonically-incrementing request ID ref so that only the latest invocation of refreshArchivedThreads applies its result to state, discarding stale responses from superseded fetches.

Create PR

Or push these changes by commenting:

@cursor push d38381c1ae
Preview (d38381c1ae)
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -1310,7 +1310,10 @@
     [projects],
   );
 
+  const refreshIdRef = useRef(0);
+
   const refreshArchivedThreads = useCallback(async () => {
+    const id = ++refreshIdRef.current;
     setIsLoadingArchive(true);
     try {
       const snapshots = await Promise.all(
@@ -1325,9 +1328,13 @@
           };
         }),
       );
-      setArchivedSnapshots(snapshots.filter((snapshot) => snapshot !== null));
+      if (id === refreshIdRef.current) {
+        setArchivedSnapshots(snapshots.filter((snapshot) => snapshot !== null));
+      }
     } finally {
-      setIsLoadingArchive(false);
+      if (id === refreshIdRef.current) {
+        setIsLoadingArchive(false);
+      }
     }
   }, [environmentIds]);

You can send follow-ups to the cloud agent here.

Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 8, 2026

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces a new feature for archived thread support across database, server, and frontend layers, with behavioral changes to how threads are filtered in snapshots. Additionally, two unresolved review comments identify bugs (duplicate React keys, invisible archived threads in certain scenarios) that require attention.

You can customize Macroscope's approvability policy. Learn more.

- Extract shared archived-thread snapshot state
- Refresh archived lists after archive, unarchive, and delete actions
- Tighten settings row and add-environment button presentation
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels May 8, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Thread filter missing environment ID check causes cross-environment mixing
    • Added thread.environmentId === project.environmentId to the filter predicate so threads are correctly scoped to their environment.
  • ✅ Fixed: Unsorted environment IDs produce unstable atom family key
    • Added .sort() to the environment IDs array before joining, ensuring the atom family key is stable regardless of input order.

Create PR

Or push these changes by commenting:

@cursor push 43f1d07964
Preview (43f1d07964)
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -1334,7 +1334,10 @@
       .map((project) => ({
         project,
         threads: threads
-          .filter((thread) => thread.projectId === project.id)
+          .filter(
+            (thread) =>
+              thread.projectId === project.id && thread.environmentId === project.environmentId,
+          )
           .toSorted((left, right) => {
             const leftKey = left.archivedAt ?? left.createdAt;
             const rightKey = right.archivedAt ?? right.createdAt;

diff --git a/apps/web/src/lib/archivedThreadsState.ts b/apps/web/src/lib/archivedThreadsState.ts
--- a/apps/web/src/lib/archivedThreadsState.ts
+++ b/apps/web/src/lib/archivedThreadsState.ts
@@ -19,7 +19,7 @@
 const knownArchivedThreadEnvironmentKeys = new Set<string>();
 
 function makeArchivedThreadsEnvironmentKey(environmentIds: ReadonlyArray<EnvironmentId>): string {
-  return environmentIds.join(ARCHIVED_THREADS_ENVIRONMENT_KEY_SEPARATOR);
+  return [...environmentIds].sort().join(ARCHIVED_THREADS_ENVIRONMENT_KEY_SEPARATOR);
 }
 
 function parseArchivedThreadsEnvironmentKey(key: string): ReadonlyArray<EnvironmentId> {

You can send follow-ups to the cloud agent here.

Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/lib/archivedThreadsState.ts
- Switch the environment trigger from icon-only to a labeled compact button
- Adjust sizing and muted styling to better fit the connections settings UI
- Reduce muted foreground opacity in provider and source control settings
- Improve readability of auth/status rows without changing layout
- Add environmentId check to thread filter in ArchivedThreadsPanel to
  prevent threads from different environments being grouped together
  when they share the same projectId.

- Sort environment IDs before joining in makeArchivedThreadsEnvironmentKey
  to produce a stable atom family key regardless of input order.
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.

🟠 High

<SettingsSection
key={project.id}
title={project.name}

The key prop on line 1415 uses only project.id, but projects from different environments can share the same ID. When two environments have projects with identical IDs, React logs duplicate key warnings and may reconcile incorrectly. The code already uses composite keys ${environmentId}:${project.id} in projectsByEnvironmentAndId, so the render key should match that pattern.

Suggested change
<SettingsSection
- key={project.id}
+ key={`${project.environmentId}:${project.id}`}
title={project.name}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/settings/SettingsPanels.tsx around lines 1414-1416:

The `key` prop on line 1415 uses only `project.id`, but projects from different environments can share the same ID. When two environments have projects with identical IDs, React logs duplicate key warnings and may reconcile incorrectly. The code already uses composite keys `${environmentId}:${project.id}` in `projectsByEnvironmentAndId`, so the render key should match that pattern.

Evidence trail:
apps/web/src/components/settings/SettingsPanels.tsx lines 1310-1318 (composite key `${environmentId}:${project.id}` used in Map), line 1333 (Map values iterated), line 1415 (`key={project.id}` only uses project.id). Commit: REVIEWED_COMMIT.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Delete archived thread silently fails from context menu
    • Added a fallback path in deleteThread that dispatches the delete command directly when the thread isn't found in the main store, and removed the early-return in confirmAndDeleteThread so archived threads can be deleted from the context menu.
  • ✅ Fixed: Redundant ORDER BY on NULL-only column
    • Removed the redundant archived_at ASC from the ORDER BY clause in listActiveThreadRows since the WHERE clause already guarantees all rows have archived_at IS NULL.

Create PR

Or push these changes by commenting:

@cursor push 07c1ad3dc4
Preview (07c1ad3dc4)
diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
--- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@@ -349,7 +349,7 @@
         FROM projection_threads
         WHERE deleted_at IS NULL
           AND archived_at IS NULL
-        ORDER BY project_id ASC, archived_at ASC, created_at ASC, thread_id ASC
+        ORDER BY project_id ASC, created_at ASC, thread_id ASC
       `,
   });
 

diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts
--- a/apps/web/src/hooks/useThreadActions.ts
+++ b/apps/web/src/hooks/useThreadActions.ts
@@ -105,7 +105,16 @@
       const api = readEnvironmentApi(target.environmentId);
       if (!api) return;
       const resolved = resolveThreadTarget(target);
-      if (!resolved) return;
+      if (!resolved) {
+        // Thread not in main store (e.g. archived thread) — dispatch delete directly.
+        await api.orchestration.dispatchCommand({
+          type: "thread.delete",
+          commandId: newCommandId(),
+          threadId: target.threadId,
+        });
+        refreshArchivedThreadsForEnvironment(target.environmentId);
+        return;
+      }
       const { thread, threadRef } = resolved;
       const state = useStore.getState();
       const threads = selectThreadsForEnvironment(state, threadRef.environmentId);
@@ -258,13 +267,12 @@
       if (!api) return;
       const localApi = readLocalApi();
       const resolved = resolveThreadTarget(target);
-      if (!resolved) return;
-      const { thread } = resolved;
 
       if (confirmThreadDelete && localApi) {
+        const title = resolved?.thread.title ?? "this thread";
         const confirmed = await localApi.dialogs.confirm(
           [
-            `Delete thread "${thread.title}"?`,
+            `Delete thread "${title}"?`,
             "This permanently clears conversation history for this thread.",
           ].join("\n"),
         );

You can send follow-ups to the cloud agent here.

Comment thread apps/web/src/components/settings/SettingsPanels.tsx
Comment thread apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts Outdated
@juliusmarminge
Copy link
Copy Markdown
Member Author

@cursor push 07c1ad3

- deleteThread: dispatch delete command directly when thread is not found
  in the main store (e.g. archived threads excluded by the new WHERE clause)
- confirmAndDeleteThread: remove early-return on missing resolved thread;
  fall back to generic title for confirmation dialog
- Remove redundant ORDER BY archived_at ASC from listActiveThreadRows query
  (WHERE clause already guarantees archived_at IS NULL)

Applied via @cursor push command
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue. You can view the agent here.

Reviewed by Cursor Bugbot for commit f9fc1ec. Configure here.

const environmentIds = useMemo(
() => [...new Set(projects.map((project) => project.environmentId))],
[projects],
);
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.

Archived threads invisible when all threads are archived

Medium Severity

The environmentIds used to fetch archived snapshots is derived from selectProjectsAcrossEnvironments, which only returns projects visible in the main (active) shell snapshot. Since getShellSnapshot now filters out archived threads and only includes projects with at least one active thread, an environment where all threads are archived will have zero projects in the store — causing its environmentId to be missing from the list. The useArchivedThreadSnapshots hook then never fetches archived threads for that environment, making them invisible in the panel.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f9fc1ec. Configure here.

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.

Bugbot Autofix determined this is a false positive.

The getShellSnapshot function returns ALL non-deleted projects regardless of thread status (only filtering by deletedAt === null), so environments with only archived threads still have their projects in the store and their environmentIds are correctly included; the activeProjectIds filtering referenced by the bug report exists only in getArchivedShellSnapshot, not getShellSnapshot.

You can send follow-ups to the cloud agent here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants