# refactor\(web\): chat shortcuts live with the hooks that own their actions · PR \#23097 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23097)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: refactor/split3-chatview-shortcuts

Target branch: refactor/split3-chatview-controls-bar

Head commit: 300d3ced33bc5b8afd5a823cd7bd27e9102db4c3

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/23097)

[Commits](https://git.cafe/pingdotgg/t3code/pull/23097/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/23097/diffs?format=markdown)

## Description

---

ChatView built the whole keyboard shortcut table inline: about 130 lines of handlers that reached into the terminal actions, the right panel actions, thread settlement and pinning, and the composer handle. ChatView had to pull every one of those pieces out of their hooks just to hand them to `useChatShortcuts`. The hook itself kept its input in a ref assigned from a layout effect.

Each owner now returns its own chords:

- `useChatTerminalActions` returns `shortcuts` for `terminal.toggle/split/splitVertical/close/new`. It checks terminal permissions when the key is pressed instead of ChatView reading two scopes on every render.
- `useRightPanelActions` returns `rightPanel.toggle/toggleMaximized/close`, `threadPanel.toggle` and `diff.toggle`.
- `useThreadLifecycle` owns `thread.settle` and `thread.pin`, together with the settled and pinned state they read. ChatView no longer calls `useThreadActions` or exports that state.
- The composer and branch toolbar chords are `composerShortcutCommands` in `chat/composerShortcuts.ts`. `useChatShortcuts` takes `composerRef` and `branchToolbarRef` and resolves those chords inside its key handler, which is the only place a ref read is allowed. It also uses the refs for type-to-focus, replacing the `typeIntoComposer` callback.
- `useChatShortcuts` reads its input through `useEffectEvent` instead of `useLayoutEffect(() => { latest.current = input })`. The listeners still attach once.

ChatView keeps the chords that combine state from several owners (copy reference, find, cycle host, the queue chords, stop, and the project-script fallback). The behavior is unchanged.

| File | before | after |
| --- | --- | --- |
| ChatView.tsx | 4,007 | 3,899 |
| useChatShortcuts.ts | 106 | 111 |
| composerShortcuts.ts | | 38 |
| useChatTerminalActions.ts | 408 | 451 |
| useRightPanelActions.ts | 313 | 330 |
| useThreadLifecycle.tsx | 582 | 616 |

`useStableCallback` calls in ChatView: 33 to 30. No render boundaries change; the composer render counts match #23093.

Tested:
- apps/web typecheck and lint on the touched files.
- React Compiler coverage: 0 bailouts on ChatView and the touched hooks. ChatView stays in the slow set.
- Hands-on in a dev build, server thread:
  - Ctrl+J opens and closes the terminal drawer.
  - With the terminal focused: Ctrl+N adds a terminal, Ctrl+D splits it, Ctrl+W asks for confirmation and closes it.
  - Ctrl+Alt+B toggles the right panel, Ctrl+Shift+B toggles the thread panel.
  - Ctrl+D opens the Diff surface and Ctrl+W closes it.
  - Ctrl+Shift+M opens the model picker, Ctrl+Shift+E the reasoning menu, Ctrl+Shift+A the mode list.
  - Typing a letter with nothing focused lands it in the composer.
  - Ctrl+Shift+P pins and then unpins (undo toast), and Ctrl+Shift+C copies the thread ID.
  - Ctrl+F opens find and Escape closes it, returning focus to the composer.
  - Ctrl+Shift+G does nothing on a server thread, same as main: the branch toolbar only mounts in the composer context strip.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

