feat: align keyboard shortcuts with macOS/Warp/VS Code conventions#1741
feat: align keyboard shortcuts with macOS/Warp/VS Code conventions#1741h4rz wants to merge 5 commits intogeneralaction:mainfrom
Conversation
- Cmd+T opens new Conversation (was: toggle theme) - Cmd+W closes active Conversation with PTY-busy confirmation dialog - Cmd+P unbound (was: toggle kanban); kanban stays in palette + titlebar - Cmd+1-8 jump to Conversation N; Cmd+9 jumps to last Conversation - Ctrl+Tab / Ctrl+Shift+Tab cycle Conversations forward/back - Cmd+Shift+T reopens last closed Conversation (max 10 stack) - Cmd+Shift+P opens Command Palette (alias for Cmd+K) - Toggle-Kanban entry added to Cmd+K palette - PTY graceful shutdown: SIGINT → 1500ms → hard kill via new pty:killGraceful IPC - Settings migration drops legacy Cmd+T=theme and Cmd+P=kanban defaults Closes generalaction#1008 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Theme toggle: Cmd+Shift+T was displaced by Cmd+T (new chat), now bound to Cmd+Shift+L (L=Light, matches Linear convention). Kanban toggle: Cmd+P was displaced and left palette-only, now bound to Cmd+Shift+K (K=Kanban mnemonic, parallel to Cmd+K palette). Both shortcuts are real defaults again (defaultDisabled removed), user-overridable via Settings. The legacy Cmd+T and Cmd+P migration still fires to clean up any stored old defaults. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR aligns keyboard shortcuts with macOS/Warp/VS Code conventions by reclaiming
Confidence Score: 3/5Two P1 defects — a PTY process leak and a platform-specific dead shortcut — need fixes before merging. The PTY ID mismatch causes an orphaned agent process whenever a user closes the main conversation tab with another tab open. The Ctrl+Tab collision means the primary new navigation feature (chat cycling) is completely non-functional on Windows/Linux. Both are regressions on the changed code paths, warranting a 3. src/renderer/components/ChatInterface.tsx (PTY ID mismatch), src/renderer/hooks/useKeyboardShortcuts.ts (Ctrl+Tab collision on non-Mac)
|
| Filename | Overview |
|---|---|
| src/renderer/components/ChatInterface.tsx | Adds close/reopen/cycle conversation event handlers; contains a PTY ID mismatch for the main conversation that causes a process leak, and an as any cast for ptyKillGraceful. |
| src/renderer/hooks/useKeyboardShortcuts.ts | Adds six new shortcut definitions and registrations; NEXT_CHAT/PREV_CHAT collide with NEXT_TASK/PREV_TASK on non-Mac (both use Ctrl+Tab), making chat cycling unreachable on Windows/Linux. |
| src/main/services/ptyManager.ts | New killPtyGraceful: SIGINT → 1500ms timeout → hard kill; logic is sound but the onExit listener is not unregistered on the force-kill path. |
| src/main/settings.ts | Adds new keyboard binding fields; settings migration correctly identifies and drops legacy Cmd+T=theme and Cmd+P=kanban defaults while preserving user-customized bindings. |
| src/main/services/ptyIpc.ts | Registers the pty:killGraceful IPC handler; correctly cleans up owners and listeners maps on success. |
| src/main/preload.ts | Exposes ptyKillGraceful via contextBridge; type cast on the ipcRenderer.invoke return is consistent with other similar bridges. |
| src/renderer/types/electron-api.d.ts | Correctly declares ptyKillGraceful in the global electronAPI type. |
| src/renderer/components/AppKeyboardShortcuts.tsx | Wires new shortcut handlers via window.dispatchEvent custom events; onCommandPaletteAlt correctly reuses handleToggleCommandPalette. |
| src/renderer/views/Workspace.tsx | Adds a useEffect to forward the native menu Close Tab event to the emdash:close-active-chat custom event; straightforward. |
| src/test/renderer/useKeyboardShortcuts.test.ts | New tests cover Cmd+1–8 (zero-based index), Cmd+9 (-1 sentinel), Ctrl+number on non-Mac, and edge-case rejections; good coverage for getAgentTabSelectionIndex. |
Sequence Diagram
sequenceDiagram
participant User
participant useKeyboardShortcuts
participant AppKeyboardShortcuts
participant ChatInterface
participant ptyIpc
participant ptyManager
User->>useKeyboardShortcuts: Cmd+W (closeChat)
useKeyboardShortcuts->>AppKeyboardShortcuts: onCloseChat()
AppKeyboardShortcuts->>ChatInterface: dispatchEvent(emdash:close-active-chat)
ChatInterface->>ChatInterface: handleCloseChat(conversationId)
alt PTY busy
ChatInterface->>User: Show confirmation dialog
User->>ChatInterface: Confirm close
end
ChatInterface->>ChatInterface: doCloseChat(conversationId)
Note over ChatInterface: Bug: uses makePtyId('chat', conversationId)
Note over ChatInterface: Main conv PTY registered as makePtyId('main', taskId)
ChatInterface->>ptyIpc: ptyKillGraceful(terminalId)
ptyIpc->>ptyManager: killPtyGraceful(id)
ptyManager->>ptyManager: proc.kill('SIGINT')
ptyManager->>ptyManager: await exitPromise (max 1500ms)
alt Timed out
ptyManager->>ptyManager: proc.kill() hard
end
ptyManager-->>ptyIpc: ok: true
ChatInterface->>ChatInterface: rpc.db.deleteConversation(conversationId)
User->>useKeyboardShortcuts: Cmd+Shift+T (reopenClosedChat)
useKeyboardShortcuts->>AppKeyboardShortcuts: onReopenClosedChat()
AppKeyboardShortcuts->>ChatInterface: dispatchEvent(emdash:reopen-closed-chat)
ChatInterface->>ChatInterface: closedConversationStackRef.pop()
ChatInterface->>ChatInterface: rpc.db.createConversation(provider, title)
ChatInterface->>ChatInterface: setActiveConversationId(newConv.id)
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/renderer/components/ChatInterface.tsx
Line: 741
Comment:
**Wrong PTY ID for main conversation — PTY leak**
`doCloseChat` always constructs the terminal ID with kind `'chat'`, but the main conversation's PTY is registered with kind `'main'` and `task.id` as the suffix (see line 236: `makePtyId(agent, 'main', task.id)`). When the user closes the **main** conversation tab (possible once a second conversation exists), `ptyKillGraceful` receives an ID that does not exist in `ptys`, silently returns early, and the underlying process keeps running indefinitely.
```ts
// convToDelete.isMain determines which PTY scheme was used to register the process
const terminalId = convToDelete?.isMain
? makePtyId(convAgent, 'main', task.id)
: makePtyId(convAgent, 'chat', conversationId);
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/renderer/hooks/useKeyboardShortcuts.ts
Line: 240-256
Comment:
**`Ctrl+Tab` collision with `NEXT_TASK` on non-Mac platforms**
On Windows and Linux, `getPlatformTaskSwitchDefaults()` assigns `Ctrl+Tab` / `Ctrl+Shift+Tab` to `NEXT_TASK` / `PREV_TASK` (the task-level switcher). The new `NEXT_CHAT` and `PREV_CHAT` shortcuts use the identical bindings. Since `nextProject` is registered earlier in the `maybeShortcuts` array, it wins every time and the chat-cycle shortcuts are completely unreachable on non-Mac. Consider using different defaults on non-Mac (e.g. guarding `NEXT_CHAT`/`PREV_CHAT` with `defaultDisabled: true` on non-Mac, or picking a non-conflicting binding).
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/renderer/components/ChatInterface.tsx
Line: 762
Comment:
**`as any` cast bypasses the typed API**
`ptyKillGraceful` is properly declared in `electron-api.d.ts`, so the cast is only necessary because the local `declare const window` on lines 55–59 shadows the global type with a narrower shape. Rather than casting, extend or remove the local declaration so the full `electronAPI` type is visible here.
```suggestion
await window.electronAPI.ptyKillGraceful(terminalId);
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/main/services/ptyManager.ts
Line: 1732-1734
Comment:
**`onExit` listener not removed on timeout path**
`rec.proc.onExit(() => resolve())` registers a permanent listener. When the process is force-killed after the timeout, the listener fires after `ptys.delete(id)` has already run — at that point `rec` is still in scope so this is harmless, but it leaves a dangling listener on the now-killed `IPty` object. Consider capturing the disposable returned by `onExit` (if `node-pty` provides one) and calling it after the race settles, or using a one-shot wrapper.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "feat: assign new default shortcuts for t..." | Re-trigger Greptile
…eopen P1 regressions from the keyboard-shortcut PR: - A1: Fix PTY leak on main tab close — use makePtyId(agent,'main',task.id) instead of hardcoded 'chat' kind so the correct process is killed - A2: Move non-mac project cycling off Ctrl+Tab (conflicted with new chat cycling) to Ctrl+Shift+]/[ (VS Code editor-group convention) Real Cmd+Shift+T reopen via DB soft-archive (B1-B5): - Add archivedAt column to conversations (migration 0013) - DatabaseService: archiveConversation / unarchiveConversation methods; getConversations / getOrCreateDefaultConversation filter archived rows - IPC: wire archive/unarchive into databaseController - ChatInterface: close archives instead of deletes; reopen unarchives the original row so full message history is restored (not a blank new tab) P2 hygiene (C1-C5): - Fix multiAgent truthiness check (use .enabled) - Capture killPtyGraceful onExit disposable; call dispose() after race to prevent dangling listener; short-circuit if PTY already absent - Remove as any cast on ptyKillGraceful call (extend declare const window) - Guard doCloseChat against rapid double-invoke via closingConversationIdsRef - Remove unused defaultDisabled field from AppShortcut / getEffectiveConfig Tests: non-mac shortcut conflict check, archive/unarchive DB behavior, killPtyGraceful quick-resolve + disposable-called assertions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three chat-tab shortcuts lacked allowInInput:true, so they were silently skipped by the isEditableTarget guard whenever the chat input held focus — which is almost always. Ctrl+Tab / Ctrl+Shift+Tab already had the flag; this brings the new bindings in line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
entry.ts was requiring ./utils/shellEnv before patching Module._resolveFilename,
so the nested require('@shared/text/stripAnsi') inside shellEnv.js threw
"Cannot find module" and initializeShellEnvironment silently no-opped. Locale
init is load-bearing on macOS 26+ for ICU; reorder so the alias resolver runs
first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes #1008
Summary
Fixes three convention violations and adds the tab-management shortcuts that Chrome, Warp, and VS Code users expect.
Reclaimed keys (old behavior moved to palette or new binding):
Cmd+T→ New Conversation (was: toggle theme → nowCmd+Shift+L)Cmd+W→ Close active Conversation with PTY-busy confirmation dialog (was: no-op in shortcut registry)Cmd+P→ Unbound (was: toggle kanban → nowCmd+Shift+K)New shortcuts:
Cmd+1–Cmd+8→ jump to Conversation N (was partially implemented;Cmd+9now jumps to last)Ctrl+Tab/Ctrl+Shift+Tab→ cycle Conversations forward/back with wrapCmd+Shift+T→ reopen last closed Conversation (restores agent + title; max 10 stack)Cmd+Shift+P→ Command Palette alias (VS Code muscle memory; primaryCmd+Kunchanged)Cmd+Shift+K→ Toggle Kanban (new home for displacedCmd+Paction)Cmd+Shift+L→ Toggle Theme (new home for displacedCmd+Taction)Other changes:
SIGINT→ 1500ms → hard kill via newpty:killGracefulIPCCmd+Kpalette (Kanban was previously only in titlebar)Cmd+T=themeandCmd+P=kanbanstored defaults on upgrade; user-customized bindings are preservedTest Plan
Cmd+Topens Create Conversation modal;Cmd+Shift+Ltoggles themeCmd+Won idle conversation closes immediately; on busy PTY shows confirmation dialog; on single conversation does nothingCmd+1–Cmd+8jump to Conversation N;Cmd+9jumps to lastCtrl+Tab/Ctrl+Shift+Tabcycle and wrapCmd+Shift+Treopens last closed Conversation (same agent)Cmd+Shift+Popens Command PaletteCmd+Shift+Ktoggles Kanban;Cmd+Pdoes nothingCmd+K→ "kanban" entry appears and workstoggleTheme: {key:'t', modifier:'cmd'}→ binding removed on launch🤖 Generated with Claude Code