Add native context menu to delete threads#12

merged1
main
juliusmarminge/sidebar-delete-thread
openedFeb 11, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 25d ago

Summary

  • Right-click on threads in the sidebar to delete them with a native context menu
  • Automatically stops any active provider session before removing the thread
  • Uses Electron's native Menu API for platform-consistent UI

Test plan

  • Right-click a thread in the sidebar
  • Verify native context menu appears with "Delete" option
  • Click Delete and confirm the thread is removed
  • Test with active sessions to verify they're stopped before deletion

🤖 Generated with Claude Code


Open with Devin

Summary by CodeRabbit

  • New Features
    • Right-click context menus added on desktop and web (with a DOM fallback for non-desktop environments).
    • Users can delete threads from the context menu.
    • Active sessions are stopped (if present) before a thread is removed.
    • After deletion, active thread selection is reassigned automatically or cleared.
Synced from GitHub

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a cross-platform context menu: Electron main IPC handler and preload bridge, contract/type additions, web fallback UI, and web UI + store updates to show a thread context menu and delete threads when "Delete" is selected.

Changes

Cohort / File(s)Summary
Desktop main & preload
apps/desktop/src/main.ts, apps/desktop/src/preload.ts
Add CONTEXT_MENU_CHANNEL ("desktop:context-menu") and import Menu in main; handle IPC invoke by building a native Menu, showing a popup, and resolving selected id or null. Expose showContextMenu(items) on desktopBridge in preload.
Shared contract types
packages/contracts/src/ipc.ts
Extend NativeApi with `contextMenu.show(items: readonly { id: T; label: string }[], position?: { x: number; y: number }) => Promise<T
Web bridge, fallback & types
apps/web/src/wsNativeApi.ts, apps/web/src/contextMenuFallback.ts, apps/web/src/vite-env.d.ts
Add api.contextMenu.show which calls window.desktopBridge.showContextMenu(items) when available, otherwise uses showContextMenuFallback(items, position). Implement DOM fallback showContextMenuFallback and add showContextMenu to DesktopBridge types.
Web UI & state
apps/web/src/components/Sidebar.tsx, apps/web/src/store.ts
Wire onContextMenu on thread items to call contextMenu.show; handle selection of "Delete" by stopping active session (if any) and dispatching DELETE_THREAD. Add DELETE_THREAD action and reducer case to remove the thread and adjust activeThreadId.
Manifest
package.json
Minor package manifest edits (small line changes).

Sequence Diagram(s)

mermaid
1sequenceDiagram
2 participant User
3 participant Sidebar as "Web UI (Sidebar)"
4 participant API as "NativeApi / Bridge"
5 participant Main as "Electron Main"
6 participant Store as "App Store"
7
8 User->>Sidebar: Right-click thread (x,y)
9 Sidebar->>API: contextMenu.show(items, position)
10 alt Desktop bridge available
11 API->>Main: IPC invoke "desktop:context-menu" with items
12 Main->>Main: Build native Menu and popup at focused window
13 User->>Main: Select option or dismiss
14 Main-->>API: Resolve selected item id or null
15 else Web fallback
16 API->>API: showContextMenuFallback(items, position)
17 User->>API: Select option or dismiss
18 API-->>Sidebar: Resolve selected item id or null
19 end
20 Sidebar->>Sidebar: If selected "Delete": stop session (if any)
21 Sidebar->>Store: Dispatch DELETE_THREAD(threadId)
22 Store->>Store: Remove thread, reassign activeThreadId if needed
23 Store-->>Sidebar: Updated state

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 30.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'Add native context menu to delete threads' directly and accurately reflects the main change: introducing a native context menu feature for deleting threads in the sidebar.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch juliusmarminge/sidebar-delete-thread

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

Synced from GitHub

Add a native context menu for thread deletion and wire right-click in Sidebar to invoke NativeApi.contextMenu.show via Electron IPC in Sidebar.tsx and main.ts

Introduce desktop:context-menu IPC and expose window.desktopBridge.showContextMenu; update NativeApi with contextMenu.show; handle thread deletion and session stop on selection in Sidebar.tsx.

📍Where to Start

Start with the registerIpcHandlers addition for desktop:context-menu in main.ts, then follow the bridge in preload.ts and the consumer in Sidebar.tsx.


Macroscope summarized eef2901.

CcoderabbitaiBotreviewed · 3 inline threadsFeb 11, 2026
Synced from GitHub

Actionable comments posted: 3

🤖 Fix all issues with AI agents
1In `@apps/desktop/src/main.ts`:
2- Line 6: Replace usages of BrowserWindow.getFocusedWindow() when building/popup
3context menus in the ipc handler with
4BrowserWindow.fromWebContents(event.sender) so the menu is resolved from the IPC
5sender's webContents; update the IPC handler that receives the menu payload (the
6ipcMain.handle callback) to validate/parse the incoming array into the shared
7ContextMenuItem type using the existing IPC validation utility before mapping to
8Menu.buildFromTemplate, and pass BrowserWindow.fromWebContents(event.sender)
9into Menu.popup to avoid null or wrong focused-window cases.
10
11In `@apps/renderer/src/components/Sidebar.tsx`:
12- Around line 87-112: handleThreadContextMenu currently swallows errors from
13api.providers.stopSession and proceeds to dispatch DELETE_THREAD, which can
14orphan an active session; change the flow in handleThreadContextMenu so that if
15stopSession throws you do not delete the thread unless you can detect a harmless
16"already stopped" condition—catch the error from api.providers.stopSession,
17check the error message/code for an “already stopped” sentinel (or otherwise
18surface the error via UI/api.logger) and abort the dispatch({ type:
19"DELETE_THREAD", ... }) when stopSession failed, only allowing deletion when
20stopSession succeeded or the error explicitly indicates the session was already
21stopped; reference handleThreadContextMenu, api.providers.stopSession, and the
22DELETE_THREAD dispatch to locate and update the logic.
23
24In `@apps/renderer/src/store.ts`:
25- Around line 45-46: The DELETE_THREAD action currently falls back to the first
26remaining thread globally which can switch projects; change the reducer logic
27that handles { type: "DELETE_THREAD"; threadId: string } so that if the deleted
28thread equals the current active thread (activeThreadId) you find the next
29active thread from the same project only (e.g., filter threads by projectId ===
30deletedThread.projectId) and set activeThreadId to that thread's id or null if
31none; apply the same same-project fallback change to the other deletion-handling
32block referenced (the logic duplicated around the other DELETE_THREAD handling
33at the second location).
apps/web/src/components/Sidebar.tsxlines 87–112Outdated · a1da684Open thread
……@@ -63,103 +72,45 @@ export default function Sidebar() {
109+ dispatch({ type: "DELETE_THREAD", threadId });
110+ },
111+ [api, dispatch, state.threads],
112+ );
CcoderabbitaiBoton lines 87–112Feb 11, 2026

⚠️ Potential issue | 🟠 Major

Don’t delete the thread if stopping the session fails.
Errors are swallowed and deletion proceeds, which can orphan a running provider session with no UI to stop it. Consider surfacing the error (or at least aborting deletion) unless you can distinguish “already stopped.”

🛡️ Safer flow
diff
1 if (thread.session?.sessionId) {
2 try {
3 await api.providers.stopSession({
4 sessionId: thread.session.sessionId,
5 });
6- } catch {
7- // Session may already be stopped
8+ } catch (error) {
9+ // Consider surfacing error so we don't orphan a running session
10+ return;
11 }
12 }
13
14 dispatch({ type: "DELETE_THREAD", threadId });
📝 Committable suggestion

‼️ IMPORTANT Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

suggestion
1 const handleThreadContextMenu = useCallback(
2 async (threadId: string) => {
3 if (!api) return;
4 const clicked = await api.contextMenu.show([
5 { id: "delete", label: "Delete" },
6 ]);
7 if (clicked !== "delete") return;
8
9 const thread = state.threads.find((t) => t.id === threadId);
10 if (!thread) return;
11
12 // Stop active session if running
13 if (thread.session?.sessionId) {
14 try {
15 await api.providers.stopSession({
16 sessionId: thread.session.sessionId,
17 });
18 } catch (error) {
19 // Consider surfacing error so we don't orphan a running session
20 return;
21 }
22 }
23
24 dispatch({ type: "DELETE_THREAD", threadId });
25 },
26 [api, dispatch, state.threads],
27 );
🤖 Prompt for AI Agents
1In `@apps/renderer/src/components/Sidebar.tsx` around lines 87 - 112,
2handleThreadContextMenu currently swallows errors from api.providers.stopSession
3and proceeds to dispatch DELETE_THREAD, which can orphan an active session;
4change the flow in handleThreadContextMenu so that if stopSession throws you do
5not delete the thread unless you can detect a harmless "already stopped"
6condition—catch the error from api.providers.stopSession, check the error
7message/code for an “already stopped” sentinel (or otherwise surface the error
8via UI/api.logger) and abort the dispatch({ type: "DELETE_THREAD", ... }) when
9stopSession failed, only allowing deletion when stopSession succeeded or the
10error explicitly indicates the session was already stopped; reference
11handleThreadContextMenu, api.providers.stopSession, and the DELETE_THREAD
12dispatch to locate and update the logic.
apps/web/src/store.tslines 45–46Outdated · a1da684Open thread
……@@ -33,7 +42,8 @@ type Action =
3544 | { type: "SET_THREAD_MODEL"; threadId: string; model: string }
36- | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode };
45+ | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode }
46+ | { type: "DELETE_THREAD"; threadId: string };
CcoderabbitaiBoton lines 45–46Feb 11, 2026

⚠️ Potential issue | 🟡 Minor

Active-thread fallback should stay within the same project.
When deleting the active thread, picking the first remaining thread globally can jump the user into an unrelated project. Prefer the next thread in the same project (or null if none).

🔧 Suggested adjustment
diff
1 case "DELETE_THREAD": {
2+ const deleted = state.threads.find((t) => t.id === action.threadId);
3 const threads = state.threads.filter((t) => t.id !== action.threadId);
4- const activeThreadId =
5- state.activeThreadId === action.threadId
6- ? (threads[0]?.id ?? null)
7- : state.activeThreadId;
8+ const fallback =
9+ deleted?.projectId
10+ ? threads.find((t) => t.projectId === deleted.projectId) ?? null
11+ : threads[0] ?? null;
12+ const activeThreadId =
13+ state.activeThreadId === action.threadId
14+ ? (fallback?.id ?? null)
15+ : state.activeThreadId;
16 return { ...state, threads, activeThreadId };
17 }

Also applies to: 332-338

🤖 Prompt for AI Agents
1In `@apps/renderer/src/store.ts` around lines 45 - 46, The DELETE_THREAD action
2currently falls back to the first remaining thread globally which can switch
3projects; change the reducer logic that handles { type: "DELETE_THREAD";
4threadId: string } so that if the deleted thread equals the current active
5thread (activeThreadId) you find the next active thread from the same project
6only (e.g., filter threads by projectId === deletedThread.projectId) and set
7activeThreadId to that thread's id or null if none; apply the same same-project
8fallback change to the other deletion-handling block referenced (the logic
9duplicated around the other DELETE_THREAD handling at the second location).
apps/desktop/src/main.tsline 6Outdated · a1da684Open thread
……@@ -1,211 +1,364 @@
60- if (isQuitting || restartTimer) return;
4+import { spawn } from "node:child_process";
5+import path from "node:path";
6+import { BrowserWindow, Menu, app, dialog, ipcMain, session, shell } from "electron";

⚠️ Potential issue | 🟡 Minor

Resolve the menu window from the IPC sender to avoid null focused-window cases.
BrowserWindow.getFocusedWindow() can be null or the wrong window in multi-window or background scenarios; using the sender’s webContents is safer. Also consider using the shared ContextMenuItem type and parsing payloads via the existing IPC validation pattern to avoid malformed item arrays.

🔧 Suggested update
diff
1-import {
2- EDITORS,
3- IPC_CHANNELS,
4- type TerminalCommandInput,
5- type TerminalCommandResult,
6- agentConfigSchema,
7- agentSessionIdSchema,
8- newTodoInputSchema,
9- providerInterruptTurnInputSchema,
10- providerRespondToRequestInputSchema,
11- providerSendTurnInputSchema,
12- providerSessionStartInputSchema,
13- providerStopSessionInputSchema,
14- terminalCommandInputSchema,
15- todoIdSchema,
16-} from "@acme/contracts";
17+import {
18+ EDITORS,
19+ IPC_CHANNELS,
20+ type ContextMenuItem,
21+ type TerminalCommandInput,
22+ type TerminalCommandResult,
23+ agentConfigSchema,
24+ agentSessionIdSchema,
25+ newTodoInputSchema,
26+ providerInterruptTurnInputSchema,
27+ providerRespondToRequestInputSchema,
28+ providerSendTurnInputSchema,
29+ providerSessionStartInputSchema,
30+ providerStopSessionInputSchema,
31+ terminalCommandInputSchema,
32+ todoIdSchema,
33+} from "@acme/contracts";
34@@
35- ipcMain.handle(IPC_CHANNELS.contextMenuShow, async (_event, items: { id: string; label: string }[]) => {
36- const window = BrowserWindow.getFocusedWindow();
37+ ipcMain.handle(IPC_CHANNELS.contextMenuShow, async (event, items: ContextMenuItem[]) => {
38+ const window =
39+ BrowserWindow.fromWebContents(event.sender) ?? BrowserWindow.getFocusedWindow();
40 if (!window) return null;
web
1Electron BrowserWindow.fromWebContents usage for Menu.popup — recommended window selection when showing a context menu from ipcMain.handle?

Also applies to: 152-169

🤖 Prompt for AI Agents
1In `@apps/desktop/src/main.ts` at line 6, Replace usages of
2BrowserWindow.getFocusedWindow() when building/popup context menus in the ipc
3handler with BrowserWindow.fromWebContents(event.sender) so the menu is resolved
4from the IPC sender's webContents; update the IPC handler that receives the menu
5payload (the ipcMain.handle callback) to validate/parse the incoming array into
6the shared ContextMenuItem type using the existing IPC validation utility before
7mapping to Menu.buildFromTemplate, and pass
8BrowserWindow.fromWebContents(event.sender) into Menu.popup to avoid null or
9wrong focused-window cases.
apps/web/src/components/Sidebar.tsxline 171Outdated · 7121ed5Open thread
……@@ -160,6 +160,33 @@ export default function Sidebar() {
168+ ]);
169+ if (clicked !== "delete") return;
170+
171+ const thread = state.threads.find((t) => t.id === threadId);
MmacroscopeappBoton line 171Feb 11, 2026

🟢 Low

components/Sidebar.tsx:171 The state.threads captured in the closure may become stale while awaiting api.contextMenu.show. Consider re-reading the thread from the current store state after the await returns, before stopping the session.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
text
1In file apps/web/src/components/Sidebar.tsx around line 171:
2
3The `state.threads` captured in the closure may become stale while awaiting `api.contextMenu.show`. Consider re-reading the thread from the current store state after the await returns, before stopping the session.
apps/web/src/components/Sidebar.tsxline 320Outdated · 7121ed5Open thread
……@@ -290,6 +317,10 @@ export default function Sidebar() {
290317 threadId: thread.id,
291318 })
292319 }
320+ onContextMenu={(e) => {
MmacroscopeappBoton line 320Feb 11, 2026

🟢 Low

components/Sidebar.tsx:320 Consider only calling e.preventDefault() when api.contextMenu.show will actually display a menu. Currently in non-Electron web mode, the native context menu is blocked but nothing appears because desktopBridge is undefined.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
text
1In file apps/web/src/components/Sidebar.tsx around line 320:
2
3Consider only calling `e.preventDefault()` when `api.contextMenu.show` will actually display a menu. Currently in non-Electron web mode, the native context menu is blocked but nothing appears because `desktopBridge` is undefined.
Synced from GitHub

Greptile Overview

Greptile Summary

This PR adds a native, platform-consistent context menu on sidebar thread items to support deleting threads.

Changes span the IPC contract (packages/contracts/src/ipc.ts), Electron main process (apps/desktop/src/main.ts) and preload bridge (apps/desktop/src/preload.ts) to expose a nativeApi.contextMenu.show() method that returns the clicked menu item id (or null if dismissed). The renderer (apps/renderer/src/components/Sidebar.tsx) wires onContextMenu on each thread button to show the menu, optionally stop an active provider session for that thread, then dispatches a new DELETE_THREAD action handled in the store reducer (apps/renderer/src/store.ts) to remove the thread and adjust activeThreadId.

No merge-blocking issues were found in the reviewed changes; the IPC wiring and type contract are consistent end-to-end.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk.
  • Changes are small and localized, add a new IPC channel + preload bridge + renderer handler, and the new reducer action cleanly updates state. The contract between renderer/preload/main is consistent and there are no observable runtime/type-breaking issues introduced.
  • No files require special attention

Important Files Changed

FilenameOverview
apps/desktop/src/main.tsAdds IPC handler to show a native context menu via Electron Menu and return clicked item id/null; change is self-contained and matches renderer/preload contract.
apps/desktop/src/preload.tsAdds nativeApi.contextMenu.show() that forwards menu item templates to main process via ipcRenderer.invoke; aligns with NativeApi typing.
apps/renderer/src/components/Sidebar.tsxAdds right-click handler on thread buttons to show native context menu and dispatch DELETE_THREAD after stopping any active provider session.
apps/renderer/src/store.tsIntroduces DELETE_THREAD action in reducer to remove a thread and adjust activeThreadId when deleting the active thread.
packages/contracts/src/ipc.tsExtends IPC_CHANNELS and NativeApi contract with contextMenuShow channel and ContextMenuItem/contextMenu.show types.

Sequence Diagram

mermaid
1sequenceDiagram
2 participant R as Renderer (Sidebar)
3 participant P as Preload (contextBridge)
4 participant M as Main Process (ipcMain)
5 participant E as Electron Menu
6 participant S as Store/Reducer
7
8 R->>P: nativeApi.contextMenu.show([{id,label}])
9 P->>M: ipcRenderer.invoke("context-menu:show", items)
10 M->>E: Menu.buildFromTemplate(items)
11 M->>E: menu.popup(window)
12 alt User clicks item
13 E-->>M: click handler (resolve(item.id))
14 M-->>P: Promise resolves with id
15 P-->>R: Promise resolves with id
16 R->>P: nativeApi.providers.stopSession({sessionId}) (optional)
17 P->>M: ipcRenderer.invoke("provider:session:stop", payload)
18 M-->>P: resolves
19 P-->>R: resolves
20 R->>S: dispatch({type:"DELETE_THREAD", threadId})
21 S-->>R: state updates (threads filtered, activeThreadId updated)
22 else User dismisses menu
23 E-->>M: popup callback (resolve(null))
24 M-->>P: Promise resolves null
25 P-->>R: Promise resolves null
26 end
CcoderabbitaiBotreviewed · 1 inline threadFeb 11, 2026
Synced from GitHub

Actionable comments posted: 1

🤖 Fix all issues with AI agents
1In `@packages/contracts/src/ipc.ts`:
2- Around line 28-31: Remove the unused exported interface ContextMenuItem from
3the file: delete the `export interface ContextMenuItem { id: string; label:
4string }` declaration and any related export references so the contract aligns
5with the inline generic used by `contextMenu.show` (which relies on `{ id: T;
6label: string }[]` for proper inference); also run a quick grep for
7`ContextMenuItem` and remove any leftover imports/usages elsewhere if found.
packages/contracts/src/ipc.tslines 28–31Outdated · 5e8adbdOpen thread
……@@ -25,6 +25,11 @@ export const EDITORS = [
28+export interface ContextMenuItem {
29+ id: string;
30+ label: string;
31+}
CcoderabbitaiBoton lines 28–31Feb 11, 2026

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

shell
1# Check if ContextMenuItem is imported/used anywhere in the codebase
2rg -n 'ContextMenuItem' --type=ts

Repository: pingdotgg/codething-mvp

Length of output: 134


🏁 Script executed:

shell
1# Read the relevant file to see the context
2head -n 100 packages/contracts/src/ipc.ts | cat -n

Repository: pingdotgg/codething-mvp

Length of output: 3101


🏁 Script executed:

shell
1# Check if there are any other exports or references to this interface
2fd -t f -e ts -e tsx | xargs rg -l 'ContextMenuItem' 2>/dev/null

Repository: pingdotgg/codething-mvp

Length of output: 96


The ContextMenuItem interface is unused and should be removed.

This interface is exported at lines 28-31 but is not used anywhere in the codebase. The contextMenu.show method intentionally uses an inline generic type { id: T; label: string }[] instead, which enables proper type inference for the returned ID (e.g., Promise<"delete" | null>). The unused export should be removed to avoid confusion about the actual contract.

🤖 Prompt for AI Agents
1In `@packages/contracts/src/ipc.ts` around lines 28 - 31, Remove the unused
2exported interface ContextMenuItem from the file: delete the `export interface
3ContextMenuItem { id: string; label: string }` declaration and any related
4export references so the contract aligns with the inline generic used by
5`contextMenu.show` (which relies on `{ id: T; label: string }[]` for proper
6inference); also run a quick grep for `ContextMenuItem` and remove any leftover
7imports/usages elsewhere if found.

✅ Addressed in commit ab360e3

Added 1 commit

  • Add context menu to delete threads from sidebareef2901
Synced from GitHub

Updated the PR head to 69ee3165a after merging current origin/staging.

  • Bot runs now use the canonical sidebar lifecycle/context path, including archived restore behavior.
  • Run snapshots persist their completion-time token total through migration 058; the run list reads that stored value.
  • Instructions use the canonical arbitrary-position slash parser and a bounded portalled menu with an always-mounted listbox.

Verification run on this head: focused bot and merge suites (22 files, 547 tests with one existing skip), five scoped package typechecks, scoped lint/format, Canon checks, seam gate, and staged diff/NUL/palette audit.