Add archived shell snapshot support#2593
Conversation
- 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
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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.
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.
ApprovabilityVerdict: 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
There was a problem hiding this comment.
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.environmentIdto the filter predicate so threads are correctly scoped to their environment.
- Added
- ✅ 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.
- Added
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.
- 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.
There was a problem hiding this comment.
🟠 High
t3code/apps/web/src/components/settings/SettingsPanels.tsx
Lines 1414 to 1416 in e78f20c
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.
| <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.
There was a problem hiding this comment.
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 ASCfrom the ORDER BY clause in listActiveThreadRows since the WHERE clause already guarantees all rows have archived_at IS NULL.
- Removed the redundant
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.
- 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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
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], | ||
| ); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit f9fc1ec. Configure here.
There was a problem hiding this comment.
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.



Summary
getArchivedShellSnapshotorchestration read path so archived threads can be loaded separately from the main shell snapshot.Testing
bun fmtbun lintbun typecheckbun run testNote
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.getArchivedShellSnapshotread path that returns only archived thread shell summaries, while updatinggetShellSnapshot(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 sothread.archivedremoves the thread andthread.unarchivedre-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
getArchivedShellSnapshotRPC to expose archived threads in the settings panelgetArchivedShellSnapshotquery toProjectionSnapshotQuerythat returns only archived (non-deleted, archived_at IS NOT NULL) threads with their sessions and latest turns, whilegetShellSnapshotis updated to exclude archived threads.EnvironmentApiso the web client can callorchestration.getArchivedShellSnapshot.useArchivedThreadSnapshotshook, with loading/error states and auto-refresh after archive/unarchive/delete actions.getShellSnapshotno longer includes archived threads; callers that previously saw archived threads in the main snapshot will no longer see them there.Macroscope summarized f9fc1ec.