Skip to content

feat: align keyboard shortcuts with macOS/Warp/VS Code conventions#1741

Closed
h4rz wants to merge 5 commits intogeneralaction:mainfrom
h4rz:emdash/feat-proposal-align-keyboard-4yg
Closed

feat: align keyboard shortcuts with macOS/Warp/VS Code conventions#1741
h4rz wants to merge 5 commits intogeneralaction:mainfrom
h4rz:emdash/feat-proposal-align-keyboard-4yg

Conversation

@h4rz
Copy link
Copy Markdown

@h4rz h4rz commented Apr 20, 2026

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+TNew Conversation (was: toggle theme → now Cmd+Shift+L)
  • Cmd+WClose active Conversation with PTY-busy confirmation dialog (was: no-op in shortcut registry)
  • Cmd+PUnbound (was: toggle kanban → now Cmd+Shift+K)

New shortcuts:

  • Cmd+1Cmd+8 → jump to Conversation N (was partially implemented; Cmd+9 now jumps to last)
  • Ctrl+Tab / Ctrl+Shift+Tab → cycle Conversations forward/back with wrap
  • Cmd+Shift+T → reopen last closed Conversation (restores agent + title; max 10 stack)
  • Cmd+Shift+P → Command Palette alias (VS Code muscle memory; primary Cmd+K unchanged)
  • Cmd+Shift+K → Toggle Kanban (new home for displaced Cmd+P action)
  • Cmd+Shift+L → Toggle Theme (new home for displaced Cmd+T action)

Other changes:

  • PTY graceful shutdown: SIGINT → 1500ms → hard kill via new pty:killGraceful IPC
  • "Toggle Kanban" entry added to Cmd+K palette (Kanban was previously only in titlebar)
  • Settings migration drops legacy Cmd+T=theme and Cmd+P=kanban stored defaults on upgrade; user-customized bindings are preserved

Test Plan

  • Cmd+T opens Create Conversation modal; Cmd+Shift+L toggles theme
  • Cmd+W on idle conversation closes immediately; on busy PTY shows confirmation dialog; on single conversation does nothing
  • Cmd+1Cmd+8 jump to Conversation N; Cmd+9 jumps to last
  • Ctrl+Tab / Ctrl+Shift+Tab cycle and wrap
  • Cmd+Shift+T reopens last closed Conversation (same agent)
  • Cmd+Shift+P opens Command Palette
  • Cmd+Shift+K toggles Kanban; Cmd+P does nothing
  • Cmd+K → "kanban" entry appears and works
  • Settings with old toggleTheme: {key:'t', modifier:'cmd'} → binding removed on launch

🤖 Generated with Claude Code

h4rz and others added 2 commits April 19, 2026 13:57
- 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>
@h4rz h4rz closed this Apr 20, 2026
@h4rz h4rz reopened this Apr 20, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR aligns keyboard shortcuts with macOS/Warp/VS Code conventions by reclaiming Cmd+T (new conversation), Cmd+W (close conversation), and Cmd+P (unbound), adding Ctrl+Tab chat cycling, Cmd+Shift+T reopen, and a settings migration to drop legacy bindings. The PTY graceful-shutdown path (SIGINT → 1500 ms → hard kill) is a solid addition.

  • P1 — PTY leak on main conversation close: doCloseChat always builds the terminal ID as makePtyId(agent, 'chat', conversationId), but the main conversation's PTY is registered as makePtyId(agent, 'main', task.id). Closing the main tab when a second conversation exists silently no-ops ptyKillGraceful and leaves the agent process running.
  • P1 — Ctrl+Tab unreachable on non-Mac: NEXT_CHAT and NEXT_TASK both bind to Ctrl+Tab on Windows/Linux; task navigation fires first and chat cycling is never triggered.

Confidence Score: 3/5

Two 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)

Important Files Changed

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)
Loading
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

Comment thread src/renderer/components/ChatInterface.tsx Outdated
Comment thread src/renderer/hooks/useKeyboardShortcuts.ts
Comment thread src/renderer/components/ChatInterface.tsx Outdated
Comment thread src/main/services/ptyManager.ts
h4rz and others added 3 commits April 20, 2026 11:52
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[proposal] align keyboard shortcuts with macOS, Warp, and VS Code conventions

2 participants