feat(settings): add archived thread bulk actions - #3615

Closed
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean
Closed

feat(settings): add archived thread bulk actions#3615
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean

Conversation

@adityavardhansharma

@adityavardhansharmaadityavardhansharma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Settings → Archive: Added multi-select support for archived threads using environment-scoped thread keys, so duplicate thread IDs across connected environments remain distinct.
  • Bulk actions: Added bulk delete and bulk unarchive for selected archived threads. Delete reuses the existing scoped deletedThreadKeys behavior so batch deletion stays aligned with the sidebar flow.
  • UI: Added a slim inline control strip above archived thread groups: a left-aligned "Select all" checkbox anchored to the row checkbox column, with Delete / Unarchive buttons appearing on the right only when one or more threads are selected.
  • Reliability: Bulk delete and unarchive aggregate per-thread failures into a single summary toast instead of stacking one toast per failed thread.
  • Tests: Added scoped archive selection helper tests covering duplicate thread IDs across environments, select-all key generation, and stale-selection pruning.

Why

Adds bulk select, unarchive, and delete for archived threads — the panel previously only supported acting on one thread at a time. Built on the app's per-environment archive model (snapshots load per environment, actions route through scoped refs), with destructive actions kept predictable.

UI Changes

Old UI

image

New UI

imageimage

Video

t3code_bulk.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Open in Devin Review

Note

Medium Risk
Bulk permanent deletes and sequential thread mutations are user-data operations; behavior is mitigated by confirmations, scoped refs, and partial-failure retention, but mistakes or partial batch failures still matter.

Overview
Settings → Archived threads now supports multi-select and bulk Delete / Unarchive, using environment-scoped selection keys (environmentId:threadId) so the same thread ID in different environments stays distinct.

A Select all bar and per-row checkboxes drive bulk actions; confirmations use AlertDialog (bulk delete respects the existing confirm thread delete setting). Bulk runs call unarchiveThread / deleteThread per selection, keep failed rows selected, refresh snapshots, and show one summary error toast instead of one per failure. Bulk delete passes deletedThreadKeys and ignorePostDeleteCleanupFailure so batch behavior aligns with sidebar deletes without failing the whole batch on post-delete worktree cleanup.

Pure helpers (archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, pruneArchivedThreadSelection) and unit tests cover cross-environment IDs and stale selection pruning.

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

Note

Add bulk select, unarchive, and delete actions to the archived threads settings panel

  • Adds multi-select checkboxes to each archived thread row, a "Select all" checkbox (indeterminate when partially selected), and bulk Unarchive/Delete buttons with item counts in SettingsPanels.tsx.
  • Bulk operations iterate over selected entries, call unarchiveThread or deleteThread per thread, retain failed selections, refresh the archived list, and show aggregated error toasts on failure.
  • Selection keys are scoped by environment and thread ID using new helpers archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, and pruneArchivedThreadSelection in SettingsPanels.logic.ts; pruning keeps selections in sync as the archived snapshot changes.
  • Extends deleteThread in useThreadActions.ts to accept ignorePostDeleteCleanupFailure, so bulk delete treats post-delete cleanup errors as non-fatal (an error toast is still shown).
  • All per-thread controls and bulk action buttons are disabled while a bulk operation is pending.
📊 Macroscope summarized afd5772. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

Copy link
Copy Markdown

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: 79f64bd5-9e28-46f8-82fb-92cbb09fa2a1

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 30, 2026

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

title: `Failed to unarchive ${failedCount} of ${totalCount} threads`,
description: lastErrorMessage,
}),
);

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.

Interrupted bulk failures stay silent

Medium Severity

In the new bulk unarchive and bulk delete loops, any per-thread command that fails with an interrupt-only result is kept in the selection but is excluded from failedCount, so the aggregated summary toast never runs. After a mixed batch, succeeded threads disappear while interrupted ones stay selected with no explanation.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}
}

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.

Bulk actions ignore command interrupts

Medium Severity

In bulk unarchive and bulk delete, when a per-thread command returns an interrupted failure, the loop still processes the rest of the selection. Sidebar multi-delete stops on the first failure, including interrupts. Continuing after an interrupt can delete or unarchive threads the user did not intend to include in the batch.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. 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.

Context menu during bulk pending

Medium Severity

While a bulk delete or unarchive is in progress, row checkboxes and per-row Unarchive buttons are disabled, but the archived-thread context menu stays active. A user can still delete or unarchive the same threads in parallel with the in-flight bulk operation, causing overlapping mutations and inconsistent selection or archive state.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

Comment on lines +1635 to +1659
const executeBulkDeleteArchivedThreads = useCallback(async () => {
if (selectedArchivedThreadEntries.length === 0 || bulkActionPendingRef.current !== null) {
return;
}
const totalCount = selectedArchivedThreadEntries.length;
const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
unsuccessfulThreadKeys.add(entry.key);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
failedCount += 1;
lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}

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.

🟡 Mediumsettings/SettingsPanels.tsx:1635

executeBulkDeleteArchivedThreads builds deletedThreadKeys from the full selection and passes that same set to every deleteThread call, even after an earlier delete fails. The failed thread is still in the set, so deleteThread excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from deletedThreadKeys when its deletion fails so later iterations only account for threads that were actually deleted.

- const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));+ const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
+ deletedThreadKeys.delete(entry.key);
unsuccessfulThreadKeys.add(entry.key);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/SettingsPanels.tsx around lines 1635-1659:
`executeBulkDeleteArchivedThreads` builds `deletedThreadKeys` from the full selection and passes that same set to every `deleteThread` call, even after an earlier delete fails. The failed thread is still in the set, so `deleteThread` excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from `deletedThreadKeys` when its deletion fails so later iterations only account for threads that were actually deleted.

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.

🟡 Medium

When opts.ignorePostDeleteCleanupFailure is true, deleteThread still calls toastManager.add(...) before returning deleteResult. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the toastManager.add call with !opts.ignorePostDeleteCleanupFailure so the caller owns the failure presentation.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useThreadActions.ts around line 327:
When `opts.ignorePostDeleteCleanupFailure` is `true`, `deleteThread` still calls `toastManager.add(...)` before returning `deleteResult`. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the `toastManager.add` call with `!opts.ignorePostDeleteCleanupFailure` so the caller owns the failure presentation.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (bulk delete/unarchive for archived threads) with substantial new logic and state management. Additionally, there are 5 unresolved medium-severity review comments identifying bugs in the bulk action error handling, selection state, and race conditions that should be addressed before merging.

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

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

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@adityavardhansharma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(settings): add archived thread bulk actions - #3615

Closed
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean
Closed

feat(settings): add archived thread bulk actions#3615
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean

Conversation

@adityavardhansharma

@adityavardhansharmaadityavardhansharma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Settings → Archive: Added multi-select support for archived threads using environment-scoped thread keys, so duplicate thread IDs across connected environments remain distinct.
  • Bulk actions: Added bulk delete and bulk unarchive for selected archived threads. Delete reuses the existing scoped deletedThreadKeys behavior so batch deletion stays aligned with the sidebar flow.
  • UI: Added a slim inline control strip above archived thread groups: a left-aligned "Select all" checkbox anchored to the row checkbox column, with Delete / Unarchive buttons appearing on the right only when one or more threads are selected.
  • Reliability: Bulk delete and unarchive aggregate per-thread failures into a single summary toast instead of stacking one toast per failed thread.
  • Tests: Added scoped archive selection helper tests covering duplicate thread IDs across environments, select-all key generation, and stale-selection pruning.

Why

Adds bulk select, unarchive, and delete for archived threads — the panel previously only supported acting on one thread at a time. Built on the app's per-environment archive model (snapshots load per environment, actions route through scoped refs), with destructive actions kept predictable.

UI Changes

Old UI

image

New UI

imageimage

Video

t3code_bulk.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Open in Devin Review

Note

Medium Risk
Bulk permanent deletes and sequential thread mutations are user-data operations; behavior is mitigated by confirmations, scoped refs, and partial-failure retention, but mistakes or partial batch failures still matter.

Overview
Settings → Archived threads now supports multi-select and bulk Delete / Unarchive, using environment-scoped selection keys (environmentId:threadId) so the same thread ID in different environments stays distinct.

A Select all bar and per-row checkboxes drive bulk actions; confirmations use AlertDialog (bulk delete respects the existing confirm thread delete setting). Bulk runs call unarchiveThread / deleteThread per selection, keep failed rows selected, refresh snapshots, and show one summary error toast instead of one per failure. Bulk delete passes deletedThreadKeys and ignorePostDeleteCleanupFailure so batch behavior aligns with sidebar deletes without failing the whole batch on post-delete worktree cleanup.

Pure helpers (archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, pruneArchivedThreadSelection) and unit tests cover cross-environment IDs and stale selection pruning.

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

Note

Add bulk select, unarchive, and delete actions to the archived threads settings panel

  • Adds multi-select checkboxes to each archived thread row, a "Select all" checkbox (indeterminate when partially selected), and bulk Unarchive/Delete buttons with item counts in SettingsPanels.tsx.
  • Bulk operations iterate over selected entries, call unarchiveThread or deleteThread per thread, retain failed selections, refresh the archived list, and show aggregated error toasts on failure.
  • Selection keys are scoped by environment and thread ID using new helpers archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, and pruneArchivedThreadSelection in SettingsPanels.logic.ts; pruning keeps selections in sync as the archived snapshot changes.
  • Extends deleteThread in useThreadActions.ts to accept ignorePostDeleteCleanupFailure, so bulk delete treats post-delete cleanup errors as non-fatal (an error toast is still shown).
  • All per-thread controls and bulk action buttons are disabled while a bulk operation is pending.
📊 Macroscope summarized afd5772. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

Copy link
Copy Markdown

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: 79f64bd5-9e28-46f8-82fb-92cbb09fa2a1

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 30, 2026

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

title: `Failed to unarchive ${failedCount} of ${totalCount} threads`,
description: lastErrorMessage,
}),
);

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.

Interrupted bulk failures stay silent

Medium Severity

In the new bulk unarchive and bulk delete loops, any per-thread command that fails with an interrupt-only result is kept in the selection but is excluded from failedCount, so the aggregated summary toast never runs. After a mixed batch, succeeded threads disappear while interrupted ones stay selected with no explanation.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}
}

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.

Bulk actions ignore command interrupts

Medium Severity

In bulk unarchive and bulk delete, when a per-thread command returns an interrupted failure, the loop still processes the rest of the selection. Sidebar multi-delete stops on the first failure, including interrupts. Continuing after an interrupt can delete or unarchive threads the user did not intend to include in the batch.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. 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.

Context menu during bulk pending

Medium Severity

While a bulk delete or unarchive is in progress, row checkboxes and per-row Unarchive buttons are disabled, but the archived-thread context menu stays active. A user can still delete or unarchive the same threads in parallel with the in-flight bulk operation, causing overlapping mutations and inconsistent selection or archive state.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

Comment on lines +1635 to +1659
const executeBulkDeleteArchivedThreads = useCallback(async () => {
if (selectedArchivedThreadEntries.length === 0 || bulkActionPendingRef.current !== null) {
return;
}
const totalCount = selectedArchivedThreadEntries.length;
const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
unsuccessfulThreadKeys.add(entry.key);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
failedCount += 1;
lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}

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.

🟡 Mediumsettings/SettingsPanels.tsx:1635

executeBulkDeleteArchivedThreads builds deletedThreadKeys from the full selection and passes that same set to every deleteThread call, even after an earlier delete fails. The failed thread is still in the set, so deleteThread excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from deletedThreadKeys when its deletion fails so later iterations only account for threads that were actually deleted.

- const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));+ const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
+ deletedThreadKeys.delete(entry.key);
unsuccessfulThreadKeys.add(entry.key);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/SettingsPanels.tsx around lines 1635-1659:
`executeBulkDeleteArchivedThreads` builds `deletedThreadKeys` from the full selection and passes that same set to every `deleteThread` call, even after an earlier delete fails. The failed thread is still in the set, so `deleteThread` excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from `deletedThreadKeys` when its deletion fails so later iterations only account for threads that were actually deleted.

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.

🟡 Medium

When opts.ignorePostDeleteCleanupFailure is true, deleteThread still calls toastManager.add(...) before returning deleteResult. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the toastManager.add call with !opts.ignorePostDeleteCleanupFailure so the caller owns the failure presentation.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useThreadActions.ts around line 327:
When `opts.ignorePostDeleteCleanupFailure` is `true`, `deleteThread` still calls `toastManager.add(...)` before returning `deleteResult`. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the `toastManager.add` call with `!opts.ignorePostDeleteCleanupFailure` so the caller owns the failure presentation.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (bulk delete/unarchive for archived threads) with substantial new logic and state management. Additionally, there are 5 unresolved medium-severity review comments identifying bugs in the bulk action error handling, selection state, and race conditions that should be addressed before merging.

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

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

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@adityavardhansharma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(settings): add archived thread bulk actions - #3615

Closed
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean
Closed

feat(settings): add archived thread bulk actions#3615
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean

Conversation

@adityavardhansharma

@adityavardhansharmaadityavardhansharma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Settings → Archive: Added multi-select support for archived threads using environment-scoped thread keys, so duplicate thread IDs across connected environments remain distinct.
  • Bulk actions: Added bulk delete and bulk unarchive for selected archived threads. Delete reuses the existing scoped deletedThreadKeys behavior so batch deletion stays aligned with the sidebar flow.
  • UI: Added a slim inline control strip above archived thread groups: a left-aligned "Select all" checkbox anchored to the row checkbox column, with Delete / Unarchive buttons appearing on the right only when one or more threads are selected.
  • Reliability: Bulk delete and unarchive aggregate per-thread failures into a single summary toast instead of stacking one toast per failed thread.
  • Tests: Added scoped archive selection helper tests covering duplicate thread IDs across environments, select-all key generation, and stale-selection pruning.

Why

Adds bulk select, unarchive, and delete for archived threads — the panel previously only supported acting on one thread at a time. Built on the app's per-environment archive model (snapshots load per environment, actions route through scoped refs), with destructive actions kept predictable.

UI Changes

Old UI

image

New UI

imageimage

Video

t3code_bulk.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Open in Devin Review

Note

Medium Risk
Bulk permanent deletes and sequential thread mutations are user-data operations; behavior is mitigated by confirmations, scoped refs, and partial-failure retention, but mistakes or partial batch failures still matter.

Overview
Settings → Archived threads now supports multi-select and bulk Delete / Unarchive, using environment-scoped selection keys (environmentId:threadId) so the same thread ID in different environments stays distinct.

A Select all bar and per-row checkboxes drive bulk actions; confirmations use AlertDialog (bulk delete respects the existing confirm thread delete setting). Bulk runs call unarchiveThread / deleteThread per selection, keep failed rows selected, refresh snapshots, and show one summary error toast instead of one per failure. Bulk delete passes deletedThreadKeys and ignorePostDeleteCleanupFailure so batch behavior aligns with sidebar deletes without failing the whole batch on post-delete worktree cleanup.

Pure helpers (archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, pruneArchivedThreadSelection) and unit tests cover cross-environment IDs and stale selection pruning.

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

Note

Add bulk select, unarchive, and delete actions to the archived threads settings panel

  • Adds multi-select checkboxes to each archived thread row, a "Select all" checkbox (indeterminate when partially selected), and bulk Unarchive/Delete buttons with item counts in SettingsPanels.tsx.
  • Bulk operations iterate over selected entries, call unarchiveThread or deleteThread per thread, retain failed selections, refresh the archived list, and show aggregated error toasts on failure.
  • Selection keys are scoped by environment and thread ID using new helpers archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, and pruneArchivedThreadSelection in SettingsPanels.logic.ts; pruning keeps selections in sync as the archived snapshot changes.
  • Extends deleteThread in useThreadActions.ts to accept ignorePostDeleteCleanupFailure, so bulk delete treats post-delete cleanup errors as non-fatal (an error toast is still shown).
  • All per-thread controls and bulk action buttons are disabled while a bulk operation is pending.
📊 Macroscope summarized afd5772. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

Copy link
Copy Markdown

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: 79f64bd5-9e28-46f8-82fb-92cbb09fa2a1

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 30, 2026

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

title: `Failed to unarchive ${failedCount} of ${totalCount} threads`,
description: lastErrorMessage,
}),
);

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.

Interrupted bulk failures stay silent

Medium Severity

In the new bulk unarchive and bulk delete loops, any per-thread command that fails with an interrupt-only result is kept in the selection but is excluded from failedCount, so the aggregated summary toast never runs. After a mixed batch, succeeded threads disappear while interrupted ones stay selected with no explanation.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}
}

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.

Bulk actions ignore command interrupts

Medium Severity

In bulk unarchive and bulk delete, when a per-thread command returns an interrupted failure, the loop still processes the rest of the selection. Sidebar multi-delete stops on the first failure, including interrupts. Continuing after an interrupt can delete or unarchive threads the user did not intend to include in the batch.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. 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.

Context menu during bulk pending

Medium Severity

While a bulk delete or unarchive is in progress, row checkboxes and per-row Unarchive buttons are disabled, but the archived-thread context menu stays active. A user can still delete or unarchive the same threads in parallel with the in-flight bulk operation, causing overlapping mutations and inconsistent selection or archive state.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

Comment on lines +1635 to +1659
const executeBulkDeleteArchivedThreads = useCallback(async () => {
if (selectedArchivedThreadEntries.length === 0 || bulkActionPendingRef.current !== null) {
return;
}
const totalCount = selectedArchivedThreadEntries.length;
const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
unsuccessfulThreadKeys.add(entry.key);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
failedCount += 1;
lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}

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.

🟡 Mediumsettings/SettingsPanels.tsx:1635

executeBulkDeleteArchivedThreads builds deletedThreadKeys from the full selection and passes that same set to every deleteThread call, even after an earlier delete fails. The failed thread is still in the set, so deleteThread excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from deletedThreadKeys when its deletion fails so later iterations only account for threads that were actually deleted.

- const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));+ const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
+ deletedThreadKeys.delete(entry.key);
unsuccessfulThreadKeys.add(entry.key);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/SettingsPanels.tsx around lines 1635-1659:
`executeBulkDeleteArchivedThreads` builds `deletedThreadKeys` from the full selection and passes that same set to every `deleteThread` call, even after an earlier delete fails. The failed thread is still in the set, so `deleteThread` excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from `deletedThreadKeys` when its deletion fails so later iterations only account for threads that were actually deleted.

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.

🟡 Medium

When opts.ignorePostDeleteCleanupFailure is true, deleteThread still calls toastManager.add(...) before returning deleteResult. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the toastManager.add call with !opts.ignorePostDeleteCleanupFailure so the caller owns the failure presentation.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useThreadActions.ts around line 327:
When `opts.ignorePostDeleteCleanupFailure` is `true`, `deleteThread` still calls `toastManager.add(...)` before returning `deleteResult`. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the `toastManager.add` call with `!opts.ignorePostDeleteCleanupFailure` so the caller owns the failure presentation.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (bulk delete/unarchive for archived threads) with substantial new logic and state management. Additionally, there are 5 unresolved medium-severity review comments identifying bugs in the bulk action error handling, selection state, and race conditions that should be addressed before merging.

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

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

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@adityavardhansharma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(settings): add archived thread bulk actions - #3615

Closed
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean
Closed

feat(settings): add archived thread bulk actions#3615
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean

Conversation

@adityavardhansharma

@adityavardhansharmaadityavardhansharma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Settings → Archive: Added multi-select support for archived threads using environment-scoped thread keys, so duplicate thread IDs across connected environments remain distinct.
  • Bulk actions: Added bulk delete and bulk unarchive for selected archived threads. Delete reuses the existing scoped deletedThreadKeys behavior so batch deletion stays aligned with the sidebar flow.
  • UI: Added a slim inline control strip above archived thread groups: a left-aligned "Select all" checkbox anchored to the row checkbox column, with Delete / Unarchive buttons appearing on the right only when one or more threads are selected.
  • Reliability: Bulk delete and unarchive aggregate per-thread failures into a single summary toast instead of stacking one toast per failed thread.
  • Tests: Added scoped archive selection helper tests covering duplicate thread IDs across environments, select-all key generation, and stale-selection pruning.

Why

Adds bulk select, unarchive, and delete for archived threads — the panel previously only supported acting on one thread at a time. Built on the app's per-environment archive model (snapshots load per environment, actions route through scoped refs), with destructive actions kept predictable.

UI Changes

Old UI

image

New UI

imageimage

Video

t3code_bulk.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Open in Devin Review

Note

Medium Risk
Bulk permanent deletes and sequential thread mutations are user-data operations; behavior is mitigated by confirmations, scoped refs, and partial-failure retention, but mistakes or partial batch failures still matter.

Overview
Settings → Archived threads now supports multi-select and bulk Delete / Unarchive, using environment-scoped selection keys (environmentId:threadId) so the same thread ID in different environments stays distinct.

A Select all bar and per-row checkboxes drive bulk actions; confirmations use AlertDialog (bulk delete respects the existing confirm thread delete setting). Bulk runs call unarchiveThread / deleteThread per selection, keep failed rows selected, refresh snapshots, and show one summary error toast instead of one per failure. Bulk delete passes deletedThreadKeys and ignorePostDeleteCleanupFailure so batch behavior aligns with sidebar deletes without failing the whole batch on post-delete worktree cleanup.

Pure helpers (archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, pruneArchivedThreadSelection) and unit tests cover cross-environment IDs and stale selection pruning.

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

Note

Add bulk select, unarchive, and delete actions to the archived threads settings panel

  • Adds multi-select checkboxes to each archived thread row, a "Select all" checkbox (indeterminate when partially selected), and bulk Unarchive/Delete buttons with item counts in SettingsPanels.tsx.
  • Bulk operations iterate over selected entries, call unarchiveThread or deleteThread per thread, retain failed selections, refresh the archived list, and show aggregated error toasts on failure.
  • Selection keys are scoped by environment and thread ID using new helpers archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, and pruneArchivedThreadSelection in SettingsPanels.logic.ts; pruning keeps selections in sync as the archived snapshot changes.
  • Extends deleteThread in useThreadActions.ts to accept ignorePostDeleteCleanupFailure, so bulk delete treats post-delete cleanup errors as non-fatal (an error toast is still shown).
  • All per-thread controls and bulk action buttons are disabled while a bulk operation is pending.
📊 Macroscope summarized afd5772. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

Copy link
Copy Markdown

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: 79f64bd5-9e28-46f8-82fb-92cbb09fa2a1

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 30, 2026

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

title: `Failed to unarchive ${failedCount} of ${totalCount} threads`,
description: lastErrorMessage,
}),
);

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.

Interrupted bulk failures stay silent

Medium Severity

In the new bulk unarchive and bulk delete loops, any per-thread command that fails with an interrupt-only result is kept in the selection but is excluded from failedCount, so the aggregated summary toast never runs. After a mixed batch, succeeded threads disappear while interrupted ones stay selected with no explanation.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}
}

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.

Bulk actions ignore command interrupts

Medium Severity

In bulk unarchive and bulk delete, when a per-thread command returns an interrupted failure, the loop still processes the rest of the selection. Sidebar multi-delete stops on the first failure, including interrupts. Continuing after an interrupt can delete or unarchive threads the user did not intend to include in the batch.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. 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.

Context menu during bulk pending

Medium Severity

While a bulk delete or unarchive is in progress, row checkboxes and per-row Unarchive buttons are disabled, but the archived-thread context menu stays active. A user can still delete or unarchive the same threads in parallel with the in-flight bulk operation, causing overlapping mutations and inconsistent selection or archive state.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

Comment on lines +1635 to +1659
const executeBulkDeleteArchivedThreads = useCallback(async () => {
if (selectedArchivedThreadEntries.length === 0 || bulkActionPendingRef.current !== null) {
return;
}
const totalCount = selectedArchivedThreadEntries.length;
const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
unsuccessfulThreadKeys.add(entry.key);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
failedCount += 1;
lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}

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.

🟡 Mediumsettings/SettingsPanels.tsx:1635

executeBulkDeleteArchivedThreads builds deletedThreadKeys from the full selection and passes that same set to every deleteThread call, even after an earlier delete fails. The failed thread is still in the set, so deleteThread excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from deletedThreadKeys when its deletion fails so later iterations only account for threads that were actually deleted.

- const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));+ const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
+ deletedThreadKeys.delete(entry.key);
unsuccessfulThreadKeys.add(entry.key);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/SettingsPanels.tsx around lines 1635-1659:
`executeBulkDeleteArchivedThreads` builds `deletedThreadKeys` from the full selection and passes that same set to every `deleteThread` call, even after an earlier delete fails. The failed thread is still in the set, so `deleteThread` excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from `deletedThreadKeys` when its deletion fails so later iterations only account for threads that were actually deleted.

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.

🟡 Medium

When opts.ignorePostDeleteCleanupFailure is true, deleteThread still calls toastManager.add(...) before returning deleteResult. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the toastManager.add call with !opts.ignorePostDeleteCleanupFailure so the caller owns the failure presentation.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useThreadActions.ts around line 327:
When `opts.ignorePostDeleteCleanupFailure` is `true`, `deleteThread` still calls `toastManager.add(...)` before returning `deleteResult`. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the `toastManager.add` call with `!opts.ignorePostDeleteCleanupFailure` so the caller owns the failure presentation.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (bulk delete/unarchive for archived threads) with substantial new logic and state management. Additionally, there are 5 unresolved medium-severity review comments identifying bugs in the bulk action error handling, selection state, and race conditions that should be addressed before merging.

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

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

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@adityavardhansharma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(settings): add archived thread bulk actions - #3615

Closed
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean
Closed

feat(settings): add archived thread bulk actions#3615
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean

Conversation

@adityavardhansharma

@adityavardhansharmaadityavardhansharma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Settings → Archive: Added multi-select support for archived threads using environment-scoped thread keys, so duplicate thread IDs across connected environments remain distinct.
  • Bulk actions: Added bulk delete and bulk unarchive for selected archived threads. Delete reuses the existing scoped deletedThreadKeys behavior so batch deletion stays aligned with the sidebar flow.
  • UI: Added a slim inline control strip above archived thread groups: a left-aligned "Select all" checkbox anchored to the row checkbox column, with Delete / Unarchive buttons appearing on the right only when one or more threads are selected.
  • Reliability: Bulk delete and unarchive aggregate per-thread failures into a single summary toast instead of stacking one toast per failed thread.
  • Tests: Added scoped archive selection helper tests covering duplicate thread IDs across environments, select-all key generation, and stale-selection pruning.

Why

Adds bulk select, unarchive, and delete for archived threads — the panel previously only supported acting on one thread at a time. Built on the app's per-environment archive model (snapshots load per environment, actions route through scoped refs), with destructive actions kept predictable.

UI Changes

Old UI

image

New UI

imageimage

Video

t3code_bulk.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Open in Devin Review

Note

Medium Risk
Bulk permanent deletes and sequential thread mutations are user-data operations; behavior is mitigated by confirmations, scoped refs, and partial-failure retention, but mistakes or partial batch failures still matter.

Overview
Settings → Archived threads now supports multi-select and bulk Delete / Unarchive, using environment-scoped selection keys (environmentId:threadId) so the same thread ID in different environments stays distinct.

A Select all bar and per-row checkboxes drive bulk actions; confirmations use AlertDialog (bulk delete respects the existing confirm thread delete setting). Bulk runs call unarchiveThread / deleteThread per selection, keep failed rows selected, refresh snapshots, and show one summary error toast instead of one per failure. Bulk delete passes deletedThreadKeys and ignorePostDeleteCleanupFailure so batch behavior aligns with sidebar deletes without failing the whole batch on post-delete worktree cleanup.

Pure helpers (archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, pruneArchivedThreadSelection) and unit tests cover cross-environment IDs and stale selection pruning.

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

Note

Add bulk select, unarchive, and delete actions to the archived threads settings panel

  • Adds multi-select checkboxes to each archived thread row, a "Select all" checkbox (indeterminate when partially selected), and bulk Unarchive/Delete buttons with item counts in SettingsPanels.tsx.
  • Bulk operations iterate over selected entries, call unarchiveThread or deleteThread per thread, retain failed selections, refresh the archived list, and show aggregated error toasts on failure.
  • Selection keys are scoped by environment and thread ID using new helpers archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, and pruneArchivedThreadSelection in SettingsPanels.logic.ts; pruning keeps selections in sync as the archived snapshot changes.
  • Extends deleteThread in useThreadActions.ts to accept ignorePostDeleteCleanupFailure, so bulk delete treats post-delete cleanup errors as non-fatal (an error toast is still shown).
  • All per-thread controls and bulk action buttons are disabled while a bulk operation is pending.
📊 Macroscope summarized afd5772. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

Copy link
Copy Markdown

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: 79f64bd5-9e28-46f8-82fb-92cbb09fa2a1

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 30, 2026

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

title: `Failed to unarchive ${failedCount} of ${totalCount} threads`,
description: lastErrorMessage,
}),
);

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.

Interrupted bulk failures stay silent

Medium Severity

In the new bulk unarchive and bulk delete loops, any per-thread command that fails with an interrupt-only result is kept in the selection but is excluded from failedCount, so the aggregated summary toast never runs. After a mixed batch, succeeded threads disappear while interrupted ones stay selected with no explanation.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}
}

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.

Bulk actions ignore command interrupts

Medium Severity

In bulk unarchive and bulk delete, when a per-thread command returns an interrupted failure, the loop still processes the rest of the selection. Sidebar multi-delete stops on the first failure, including interrupts. Continuing after an interrupt can delete or unarchive threads the user did not intend to include in the batch.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. 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.

Context menu during bulk pending

Medium Severity

While a bulk delete or unarchive is in progress, row checkboxes and per-row Unarchive buttons are disabled, but the archived-thread context menu stays active. A user can still delete or unarchive the same threads in parallel with the in-flight bulk operation, causing overlapping mutations and inconsistent selection or archive state.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

Comment on lines +1635 to +1659
const executeBulkDeleteArchivedThreads = useCallback(async () => {
if (selectedArchivedThreadEntries.length === 0 || bulkActionPendingRef.current !== null) {
return;
}
const totalCount = selectedArchivedThreadEntries.length;
const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
unsuccessfulThreadKeys.add(entry.key);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
failedCount += 1;
lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}

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.

🟡 Mediumsettings/SettingsPanels.tsx:1635

executeBulkDeleteArchivedThreads builds deletedThreadKeys from the full selection and passes that same set to every deleteThread call, even after an earlier delete fails. The failed thread is still in the set, so deleteThread excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from deletedThreadKeys when its deletion fails so later iterations only account for threads that were actually deleted.

- const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));+ const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
+ deletedThreadKeys.delete(entry.key);
unsuccessfulThreadKeys.add(entry.key);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/SettingsPanels.tsx around lines 1635-1659:
`executeBulkDeleteArchivedThreads` builds `deletedThreadKeys` from the full selection and passes that same set to every `deleteThread` call, even after an earlier delete fails. The failed thread is still in the set, so `deleteThread` excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from `deletedThreadKeys` when its deletion fails so later iterations only account for threads that were actually deleted.

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.

🟡 Medium

When opts.ignorePostDeleteCleanupFailure is true, deleteThread still calls toastManager.add(...) before returning deleteResult. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the toastManager.add call with !opts.ignorePostDeleteCleanupFailure so the caller owns the failure presentation.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useThreadActions.ts around line 327:
When `opts.ignorePostDeleteCleanupFailure` is `true`, `deleteThread` still calls `toastManager.add(...)` before returning `deleteResult`. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the `toastManager.add` call with `!opts.ignorePostDeleteCleanupFailure` so the caller owns the failure presentation.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (bulk delete/unarchive for archived threads) with substantial new logic and state management. Additionally, there are 5 unresolved medium-severity review comments identifying bugs in the bulk action error handling, selection state, and race conditions that should be addressed before merging.

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

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

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@adityavardhansharma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(settings): add archived thread bulk actions - #3615

Closed
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean
Closed

feat(settings): add archived thread bulk actions#3615
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean

Conversation

@adityavardhansharma

@adityavardhansharmaadityavardhansharma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Settings → Archive: Added multi-select support for archived threads using environment-scoped thread keys, so duplicate thread IDs across connected environments remain distinct.
  • Bulk actions: Added bulk delete and bulk unarchive for selected archived threads. Delete reuses the existing scoped deletedThreadKeys behavior so batch deletion stays aligned with the sidebar flow.
  • UI: Added a slim inline control strip above archived thread groups: a left-aligned "Select all" checkbox anchored to the row checkbox column, with Delete / Unarchive buttons appearing on the right only when one or more threads are selected.
  • Reliability: Bulk delete and unarchive aggregate per-thread failures into a single summary toast instead of stacking one toast per failed thread.
  • Tests: Added scoped archive selection helper tests covering duplicate thread IDs across environments, select-all key generation, and stale-selection pruning.

Why

Adds bulk select, unarchive, and delete for archived threads — the panel previously only supported acting on one thread at a time. Built on the app's per-environment archive model (snapshots load per environment, actions route through scoped refs), with destructive actions kept predictable.

UI Changes

Old UI

image

New UI

imageimage

Video

t3code_bulk.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Open in Devin Review

Note

Medium Risk
Bulk permanent deletes and sequential thread mutations are user-data operations; behavior is mitigated by confirmations, scoped refs, and partial-failure retention, but mistakes or partial batch failures still matter.

Overview
Settings → Archived threads now supports multi-select and bulk Delete / Unarchive, using environment-scoped selection keys (environmentId:threadId) so the same thread ID in different environments stays distinct.

A Select all bar and per-row checkboxes drive bulk actions; confirmations use AlertDialog (bulk delete respects the existing confirm thread delete setting). Bulk runs call unarchiveThread / deleteThread per selection, keep failed rows selected, refresh snapshots, and show one summary error toast instead of one per failure. Bulk delete passes deletedThreadKeys and ignorePostDeleteCleanupFailure so batch behavior aligns with sidebar deletes without failing the whole batch on post-delete worktree cleanup.

Pure helpers (archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, pruneArchivedThreadSelection) and unit tests cover cross-environment IDs and stale selection pruning.

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

Note

Add bulk select, unarchive, and delete actions to the archived threads settings panel

  • Adds multi-select checkboxes to each archived thread row, a "Select all" checkbox (indeterminate when partially selected), and bulk Unarchive/Delete buttons with item counts in SettingsPanels.tsx.
  • Bulk operations iterate over selected entries, call unarchiveThread or deleteThread per thread, retain failed selections, refresh the archived list, and show aggregated error toasts on failure.
  • Selection keys are scoped by environment and thread ID using new helpers archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, and pruneArchivedThreadSelection in SettingsPanels.logic.ts; pruning keeps selections in sync as the archived snapshot changes.
  • Extends deleteThread in useThreadActions.ts to accept ignorePostDeleteCleanupFailure, so bulk delete treats post-delete cleanup errors as non-fatal (an error toast is still shown).
  • All per-thread controls and bulk action buttons are disabled while a bulk operation is pending.
📊 Macroscope summarized afd5772. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

Copy link
Copy Markdown

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: 79f64bd5-9e28-46f8-82fb-92cbb09fa2a1

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 30, 2026

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

title: `Failed to unarchive ${failedCount} of ${totalCount} threads`,
description: lastErrorMessage,
}),
);

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.

Interrupted bulk failures stay silent

Medium Severity

In the new bulk unarchive and bulk delete loops, any per-thread command that fails with an interrupt-only result is kept in the selection but is excluded from failedCount, so the aggregated summary toast never runs. After a mixed batch, succeeded threads disappear while interrupted ones stay selected with no explanation.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}
}

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.

Bulk actions ignore command interrupts

Medium Severity

In bulk unarchive and bulk delete, when a per-thread command returns an interrupted failure, the loop still processes the rest of the selection. Sidebar multi-delete stops on the first failure, including interrupts. Continuing after an interrupt can delete or unarchive threads the user did not intend to include in the batch.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. 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.

Context menu during bulk pending

Medium Severity

While a bulk delete or unarchive is in progress, row checkboxes and per-row Unarchive buttons are disabled, but the archived-thread context menu stays active. A user can still delete or unarchive the same threads in parallel with the in-flight bulk operation, causing overlapping mutations and inconsistent selection or archive state.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

Comment on lines +1635 to +1659
const executeBulkDeleteArchivedThreads = useCallback(async () => {
if (selectedArchivedThreadEntries.length === 0 || bulkActionPendingRef.current !== null) {
return;
}
const totalCount = selectedArchivedThreadEntries.length;
const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
unsuccessfulThreadKeys.add(entry.key);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
failedCount += 1;
lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}

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.

🟡 Mediumsettings/SettingsPanels.tsx:1635

executeBulkDeleteArchivedThreads builds deletedThreadKeys from the full selection and passes that same set to every deleteThread call, even after an earlier delete fails. The failed thread is still in the set, so deleteThread excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from deletedThreadKeys when its deletion fails so later iterations only account for threads that were actually deleted.

- const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));+ const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
+ deletedThreadKeys.delete(entry.key);
unsuccessfulThreadKeys.add(entry.key);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/SettingsPanels.tsx around lines 1635-1659:
`executeBulkDeleteArchivedThreads` builds `deletedThreadKeys` from the full selection and passes that same set to every `deleteThread` call, even after an earlier delete fails. The failed thread is still in the set, so `deleteThread` excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from `deletedThreadKeys` when its deletion fails so later iterations only account for threads that were actually deleted.

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.

🟡 Medium

When opts.ignorePostDeleteCleanupFailure is true, deleteThread still calls toastManager.add(...) before returning deleteResult. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the toastManager.add call with !opts.ignorePostDeleteCleanupFailure so the caller owns the failure presentation.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useThreadActions.ts around line 327:
When `opts.ignorePostDeleteCleanupFailure` is `true`, `deleteThread` still calls `toastManager.add(...)` before returning `deleteResult`. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the `toastManager.add` call with `!opts.ignorePostDeleteCleanupFailure` so the caller owns the failure presentation.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (bulk delete/unarchive for archived threads) with substantial new logic and state management. Additionally, there are 5 unresolved medium-severity review comments identifying bugs in the bulk action error handling, selection state, and race conditions that should be addressed before merging.

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

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

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@adityavardhansharma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(settings): add archived thread bulk actions - #3615

Closed
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean
Closed

feat(settings): add archived thread bulk actions#3615
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean

Conversation

@adityavardhansharma

@adityavardhansharmaadityavardhansharma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Settings → Archive: Added multi-select support for archived threads using environment-scoped thread keys, so duplicate thread IDs across connected environments remain distinct.
  • Bulk actions: Added bulk delete and bulk unarchive for selected archived threads. Delete reuses the existing scoped deletedThreadKeys behavior so batch deletion stays aligned with the sidebar flow.
  • UI: Added a slim inline control strip above archived thread groups: a left-aligned "Select all" checkbox anchored to the row checkbox column, with Delete / Unarchive buttons appearing on the right only when one or more threads are selected.
  • Reliability: Bulk delete and unarchive aggregate per-thread failures into a single summary toast instead of stacking one toast per failed thread.
  • Tests: Added scoped archive selection helper tests covering duplicate thread IDs across environments, select-all key generation, and stale-selection pruning.

Why

Adds bulk select, unarchive, and delete for archived threads — the panel previously only supported acting on one thread at a time. Built on the app's per-environment archive model (snapshots load per environment, actions route through scoped refs), with destructive actions kept predictable.

UI Changes

Old UI

image

New UI

imageimage

Video

t3code_bulk.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Open in Devin Review

Note

Medium Risk
Bulk permanent deletes and sequential thread mutations are user-data operations; behavior is mitigated by confirmations, scoped refs, and partial-failure retention, but mistakes or partial batch failures still matter.

Overview
Settings → Archived threads now supports multi-select and bulk Delete / Unarchive, using environment-scoped selection keys (environmentId:threadId) so the same thread ID in different environments stays distinct.

A Select all bar and per-row checkboxes drive bulk actions; confirmations use AlertDialog (bulk delete respects the existing confirm thread delete setting). Bulk runs call unarchiveThread / deleteThread per selection, keep failed rows selected, refresh snapshots, and show one summary error toast instead of one per failure. Bulk delete passes deletedThreadKeys and ignorePostDeleteCleanupFailure so batch behavior aligns with sidebar deletes without failing the whole batch on post-delete worktree cleanup.

Pure helpers (archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, pruneArchivedThreadSelection) and unit tests cover cross-environment IDs and stale selection pruning.

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

Note

Add bulk select, unarchive, and delete actions to the archived threads settings panel

  • Adds multi-select checkboxes to each archived thread row, a "Select all" checkbox (indeterminate when partially selected), and bulk Unarchive/Delete buttons with item counts in SettingsPanels.tsx.
  • Bulk operations iterate over selected entries, call unarchiveThread or deleteThread per thread, retain failed selections, refresh the archived list, and show aggregated error toasts on failure.
  • Selection keys are scoped by environment and thread ID using new helpers archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, and pruneArchivedThreadSelection in SettingsPanels.logic.ts; pruning keeps selections in sync as the archived snapshot changes.
  • Extends deleteThread in useThreadActions.ts to accept ignorePostDeleteCleanupFailure, so bulk delete treats post-delete cleanup errors as non-fatal (an error toast is still shown).
  • All per-thread controls and bulk action buttons are disabled while a bulk operation is pending.
📊 Macroscope summarized afd5772. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

Copy link
Copy Markdown

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: 79f64bd5-9e28-46f8-82fb-92cbb09fa2a1

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 30, 2026

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

title: `Failed to unarchive ${failedCount} of ${totalCount} threads`,
description: lastErrorMessage,
}),
);

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.

Interrupted bulk failures stay silent

Medium Severity

In the new bulk unarchive and bulk delete loops, any per-thread command that fails with an interrupt-only result is kept in the selection but is excluded from failedCount, so the aggregated summary toast never runs. After a mixed batch, succeeded threads disappear while interrupted ones stay selected with no explanation.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}
}

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.

Bulk actions ignore command interrupts

Medium Severity

In bulk unarchive and bulk delete, when a per-thread command returns an interrupted failure, the loop still processes the rest of the selection. Sidebar multi-delete stops on the first failure, including interrupts. Continuing after an interrupt can delete or unarchive threads the user did not intend to include in the batch.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. 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.

Context menu during bulk pending

Medium Severity

While a bulk delete or unarchive is in progress, row checkboxes and per-row Unarchive buttons are disabled, but the archived-thread context menu stays active. A user can still delete or unarchive the same threads in parallel with the in-flight bulk operation, causing overlapping mutations and inconsistent selection or archive state.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

Comment on lines +1635 to +1659
const executeBulkDeleteArchivedThreads = useCallback(async () => {
if (selectedArchivedThreadEntries.length === 0 || bulkActionPendingRef.current !== null) {
return;
}
const totalCount = selectedArchivedThreadEntries.length;
const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
unsuccessfulThreadKeys.add(entry.key);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
failedCount += 1;
lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}

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.

🟡 Mediumsettings/SettingsPanels.tsx:1635

executeBulkDeleteArchivedThreads builds deletedThreadKeys from the full selection and passes that same set to every deleteThread call, even after an earlier delete fails. The failed thread is still in the set, so deleteThread excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from deletedThreadKeys when its deletion fails so later iterations only account for threads that were actually deleted.

- const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));+ const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
+ deletedThreadKeys.delete(entry.key);
unsuccessfulThreadKeys.add(entry.key);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/SettingsPanels.tsx around lines 1635-1659:
`executeBulkDeleteArchivedThreads` builds `deletedThreadKeys` from the full selection and passes that same set to every `deleteThread` call, even after an earlier delete fails. The failed thread is still in the set, so `deleteThread` excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from `deletedThreadKeys` when its deletion fails so later iterations only account for threads that were actually deleted.

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.

🟡 Medium

When opts.ignorePostDeleteCleanupFailure is true, deleteThread still calls toastManager.add(...) before returning deleteResult. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the toastManager.add call with !opts.ignorePostDeleteCleanupFailure so the caller owns the failure presentation.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useThreadActions.ts around line 327:
When `opts.ignorePostDeleteCleanupFailure` is `true`, `deleteThread` still calls `toastManager.add(...)` before returning `deleteResult`. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the `toastManager.add` call with `!opts.ignorePostDeleteCleanupFailure` so the caller owns the failure presentation.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (bulk delete/unarchive for archived threads) with substantial new logic and state management. Additionally, there are 5 unresolved medium-severity review comments identifying bugs in the bulk action error handling, selection state, and race conditions that should be addressed before merging.

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

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

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@adityavardhansharma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(settings): add archived thread bulk actions - #3615

Closed
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean
Closed

feat(settings): add archived thread bulk actions#3615
adityavardhansharma wants to merge 5 commits into
pingdotgg:mainfrom
adityavardhansharma:feat/settings-archived-threads-bulk-delete-clean

Conversation

@adityavardhansharma

@adityavardhansharmaadityavardhansharma commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Settings → Archive: Added multi-select support for archived threads using environment-scoped thread keys, so duplicate thread IDs across connected environments remain distinct.
  • Bulk actions: Added bulk delete and bulk unarchive for selected archived threads. Delete reuses the existing scoped deletedThreadKeys behavior so batch deletion stays aligned with the sidebar flow.
  • UI: Added a slim inline control strip above archived thread groups: a left-aligned "Select all" checkbox anchored to the row checkbox column, with Delete / Unarchive buttons appearing on the right only when one or more threads are selected.
  • Reliability: Bulk delete and unarchive aggregate per-thread failures into a single summary toast instead of stacking one toast per failed thread.
  • Tests: Added scoped archive selection helper tests covering duplicate thread IDs across environments, select-all key generation, and stale-selection pruning.

Why

Adds bulk select, unarchive, and delete for archived threads — the panel previously only supported acting on one thread at a time. Built on the app's per-environment archive model (snapshots load per environment, actions route through scoped refs), with destructive actions kept predictable.

UI Changes

Old UI

image

New UI

imageimage

Video

t3code_bulk.mp4

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Open in Devin Review

Note

Medium Risk
Bulk permanent deletes and sequential thread mutations are user-data operations; behavior is mitigated by confirmations, scoped refs, and partial-failure retention, but mistakes or partial batch failures still matter.

Overview
Settings → Archived threads now supports multi-select and bulk Delete / Unarchive, using environment-scoped selection keys (environmentId:threadId) so the same thread ID in different environments stays distinct.

A Select all bar and per-row checkboxes drive bulk actions; confirmations use AlertDialog (bulk delete respects the existing confirm thread delete setting). Bulk runs call unarchiveThread / deleteThread per selection, keep failed rows selected, refresh snapshots, and show one summary error toast instead of one per failure. Bulk delete passes deletedThreadKeys and ignorePostDeleteCleanupFailure so batch behavior aligns with sidebar deletes without failing the whole batch on post-delete worktree cleanup.

Pure helpers (archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, pruneArchivedThreadSelection) and unit tests cover cross-environment IDs and stale selection pruning.

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

Note

Add bulk select, unarchive, and delete actions to the archived threads settings panel

  • Adds multi-select checkboxes to each archived thread row, a "Select all" checkbox (indeterminate when partially selected), and bulk Unarchive/Delete buttons with item counts in SettingsPanels.tsx.
  • Bulk operations iterate over selected entries, call unarchiveThread or deleteThread per thread, retain failed selections, refresh the archived list, and show aggregated error toasts on failure.
  • Selection keys are scoped by environment and thread ID using new helpers archivedThreadSelectionKey, buildArchivedThreadSelectionKeys, and pruneArchivedThreadSelection in SettingsPanels.logic.ts; pruning keeps selections in sync as the archived snapshot changes.
  • Extends deleteThread in useThreadActions.ts to accept ignorePostDeleteCleanupFailure, so bulk delete treats post-delete cleanup errors as non-fatal (an error toast is still shown).
  • All per-thread controls and bulk action buttons are disabled while a bulk operation is pending.
📊 Macroscope summarized afd5772. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

Copy link
Copy Markdown

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: 79f64bd5-9e28-46f8-82fb-92cbb09fa2a1

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 30, 2026

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

title: `Failed to unarchive ${failedCount} of ${totalCount} threads`,
description: lastErrorMessage,
}),
);

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.

Interrupted bulk failures stay silent

Medium Severity

In the new bulk unarchive and bulk delete loops, any per-thread command that fails with an interrupt-only result is kept in the selection but is excluded from failedCount, so the aggregated summary toast never runs. After a mixed batch, succeeded threads disappear while interrupted ones stay selected with no explanation.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}
}

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.

Bulk actions ignore command interrupts

Medium Severity

In bulk unarchive and bulk delete, when a per-thread command returns an interrupted failure, the loop still processes the rest of the selection. Sidebar multi-delete stops on the first failure, including interrupts. Continuing after an interrupt can delete or unarchive threads the user did not intend to include in the batch.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. 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.

Context menu during bulk pending

Medium Severity

While a bulk delete or unarchive is in progress, row checkboxes and per-row Unarchive buttons are disabled, but the archived-thread context menu stays active. A user can still delete or unarchive the same threads in parallel with the in-flight bulk operation, causing overlapping mutations and inconsistent selection or archive state.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit afd5772. Configure here.

Comment on lines +1635 to +1659
const executeBulkDeleteArchivedThreads = useCallback(async () => {
if (selectedArchivedThreadEntries.length === 0 || bulkActionPendingRef.current !== null) {
return;
}
const totalCount = selectedArchivedThreadEntries.length;
const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
unsuccessfulThreadKeys.add(entry.key);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
failedCount += 1;
lastErrorMessage = error instanceof Error ? error.message : "An error occurred.";
}
}

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.

🟡 Mediumsettings/SettingsPanels.tsx:1635

executeBulkDeleteArchivedThreads builds deletedThreadKeys from the full selection and passes that same set to every deleteThread call, even after an earlier delete fails. The failed thread is still in the set, so deleteThread excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from deletedThreadKeys when its deletion fails so later iterations only account for threads that were actually deleted.

- const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));+ const deletedThreadKeys = new Set(selectedArchivedThreadEntries.map((entry) => entry.key));
const unsuccessfulThreadKeys = new Set<string>();
let failedCount = 0;
let lastErrorMessage = "An error occurred.";
bulkActionPendingRef.current = "delete";
setBulkActionPending("delete");
try {
for (const entry of selectedArchivedThreadEntries) {
const result = await deleteThread(entry.threadRef, {
deletedThreadKeys,
ignorePostDeleteCleanupFailure: true,
});
if (result._tag === "Failure") {
+ deletedThreadKeys.delete(entry.key);
unsuccessfulThreadKeys.add(entry.key);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/SettingsPanels.tsx around lines 1635-1659:
`executeBulkDeleteArchivedThreads` builds `deletedThreadKeys` from the full selection and passes that same set to every `deleteThread` call, even after an earlier delete fails. The failed thread is still in the set, so `deleteThread` excludes it from the surviving-threads list when computing orphaned worktrees — meaning a worktree still referenced by the failed thread can be incorrectly treated as orphaned and force-removed. Consider removing a thread's key from `deletedThreadKeys` when its deletion fails so later iterations only account for threads that were actually deleted.

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.

🟡 Medium

When opts.ignorePostDeleteCleanupFailure is true, deleteThread still calls toastManager.add(...) before returning deleteResult. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the toastManager.add call with !opts.ignorePostDeleteCleanupFailure so the caller owns the failure presentation.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useThreadActions.ts around line 327:
When `opts.ignorePostDeleteCleanupFailure` is `true`, `deleteThread` still calls `toastManager.add(...)` before returning `deleteResult`. A bulk-delete caller passing this flag expects to handle errors with a single aggregated toast, but each thread that hits a worktree-removal failure still emits its own individual error toast, causing duplicate notifications. Consider guarding the `toastManager.add` call with `!opts.ignorePostDeleteCleanupFailure` so the caller owns the failure presentation.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new feature (bulk delete/unarchive for archived threads) with substantial new logic and state management. Additionally, there are 5 unresolved medium-severity review comments identifying bugs in the bulk action error handling, selection state, and race conditions that should be addressed before merging.

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

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

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@adityavardhansharma