Skip to content

Add keyboard navigation and jump-to-message modal for long conversations#2361

Open
Basit-Balogun10 wants to merge 8 commits into
PostHog:mainfrom
Basit-Balogun10:posthog-code/chat-timeline
Open

Add keyboard navigation and jump-to-message modal for long conversations#2361
Basit-Balogun10 wants to merge 8 commits into
PostHog:mainfrom
Basit-Balogun10:posthog-code/chat-timeline

Conversation

@Basit-Balogun10
Copy link
Copy Markdown

@Basit-Balogun10 Basit-Balogun10 commented May 25, 2026

Problem

Long conversations become hard to navigate once they grow beyond a few turns. Search (Ctrl+F) can find text, but it cannot move between messages or jump to a specific turn in the conversation thread.

Closes #2360. Builds on #2321 to make the shortcuts configurable

Changes

  • Added conversation navigation shortcuts so users can move through user messages with Alt/Option+Up and Alt/Option+Down.
  • Added a message jump picker to jump directly to a specific user message.
  • Kept the selected message in view while navigating so the keyboard flow stays anchored in the thread.
  • Exposed the new bindings in the shortcuts sheet so the mac equivalent is visible as ⌥↑ and ⌥↓.

How did you test this?

Validated the touched renderer files with targeted static checks (get_errors).

Manual verification flow:

  1. Open a task with a long conversation thread.
  2. On macOS, press Option+Up and Option+Down to move backward and forward through user messages.
  3. On Windows/Linux, press Alt+Up and Alt+Down to verify the same navigation path.
  4. Press Cmd/Ctrl+J to open the message jump picker.
  5. Type part of a message, select a result, and confirm the conversation scrolls to that turn.
  6. Open the keyboard shortcuts sheet and confirm the new bindings are listed with the correct mac glyphs.
  7. Repeat the navigation while focus is inside the conversation editor to confirm the shortcuts still work in-context.

Demo

chat-timeline-modal-and-keyboard-navigation-posthog-code.webm

Publish to changelog?

no

Users can now remap any of the 17 configurable shortcuts via Settings >
Shortcuts (or the ⌘/ sheet). Custom bindings fully replace all defaults
(including alternates) and multiple custom combos per action are supported.
Bindings persist across sessions via electronStorage.

- Add `configurable` flag + `DEFAULT_KEYBINDINGS` map to keyboard-shortcuts.ts
- New `keybindingsStore` (persist + electronStorage) with array-based custom combos,
  conflict detection helper, and individual/bulk reset
- New `useShortcut(id)` hook — reactive Zustand selector, feeds useHotkeys
- New `Keycap` component extracted to avoid circular imports
- New `ShortcutRecorder` component: click + to enter recording mode, captures
  keydown, shows conflict toast, per-binding × remove, per-shortcut ↩ reset
- Update all useHotkeys call sites (GlobalEventHandlers, SpaceSwitcher,
  usePanelKeyboardShortcuts, ExternalAppsOpener) to use useShortcut()
- KeyboardShortcutsSheet: configurable rows render ShortcutRecorder instead of
  static keycaps; "Reset all shortcuts" button shown when customisations exist

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
Bare letter keys (e.g. just "k") would fire every time that character is
typed anywhere in the app. Require at least mod/ctrl/alt to be held.

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
24 tests covering resolveKey, addKeybinding, removeKeybinding,
resetShortcut, resetAll, getKey, and findConflict — including
conflict detection against comma-separated default alternates.

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
- KeyboardShortcutsSheet header now reads the "shortcuts" key via
  useShortcut() so the trigger keycap updates when remapped
- ExternalAppsOpener dropdown labels for open-in-editor and copy-path
  now derive from useShortcut() + formatHotkeyParts() instead of
  hardcoded Mac-only symbols

test(e2e): add Playwright shortcut sheet tests

Covers sheet open/close, category sections, hover controls, recording
mode entry/cancellation, bare-key rejection, saving bindings, conflict
detection, removing bindings, per-shortcut reset, and reset-all.

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
Hardcoded Cmd glyphs were leaking onto Windows in the send-messages
dropdown and the tiptap paste hint, and two handlers were gated on
metaKey only so the corresponding shortcut never fired on Windows
(mod+1..9 task switching, Cmd/Ctrl-click multi-select in the inbox).

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
- Add prompt-history-prev/next to CONFIGURABLE_SHORTCUT_IDS and
  DEFAULT_KEYBINDINGS so they appear in the shortcuts sheet and
  can be rebound like any other shortcut
- Add tiptapEventToCombo() — accepts shift-only combos (no Ctrl/Meta
  required) so shift+up/down can be matched against live bindings
- Fix eventToCombo() to normalise Arrow-prefixed key names (ArrowUp to up)
- Wire useTiptapEditor to resolve prompt-history keys from the store
  instead of hardcoding event.shiftKey
- Fix paste hint toast to show the live paste-as-file binding instead
  of the hardcoded mod+shift+v string
- Fix noStaticElementInteractions lint on recording modal backdrop
- Rewrite E2E shortcut tests to match the current recording modal UI
  (chips + right-click context menu) rather than the old hover-button
  and inline-input design
- Deduplicate in updateKeybinding — conflict detection excludes the
  shortcut being edited so editing one binding to match another on the
  same shortcut could produce ["ctrl+q","ctrl+q"], duplicate React keys
  and broken chip reconciliation
- Remove ArrowUp/Down gate around prompt-history navigation so custom
  non-arrow bindings (e.g. Ctrl+K) actually fire when pressed, not just
  when the physical key is an arrow
- Remove obvious section-divider comments and redundant JSX labels
  (Header, Scrollable list, Sticky footer); keep non-obvious rationale
  comments (window-level capture, backdrop dismiss, canAddMore budget,
  dedup note, ArrowKey gate explanation)
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 25, 2026

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/code/src/renderer/features/sessions/components/MessageJumpPicker.tsx:88-93
Superfluous alias: `allEntries` is always identical to `visibleEntries` and used only once in `handleSelect`. The intermediate name adds no clarity and violates the "no superfluous parts" rule.

```suggestion
  const handleSelect = useCallback(
    (id: string | null) => {
      if (id === null) return;
      const entry = visibleEntries.find((e) => e.id === id);
```

### Issue 2 of 3
apps/code/src/renderer/constants/keyboard-shortcuts.ts:282-305
**Dual source of truth for configurability**

`CONFIGURABLE_SHORTCUT_IDS` and the `configurable: true` flag on each `KeyboardShortcut` entry must be kept in sync manually. The flag controls which UI renderer (`ShortcutRecorder` vs `ShortcutKeys`) is shown in the sheet; the const array drives the TypeScript type and store logic. Adding a new shortcut requires updating both, but there is no compile-time or runtime guard that catches a mismatch — a shortcut could show `ShortcutRecorder` in the UI while the store type rejects its ID, or vice versa. Consider deriving one from the other: either generate `CONFIGURABLE_SHORTCUT_IDS` by filtering `KEYBOARD_SHORTCUTS` where `configurable === true`, or drop the `configurable` flag and compute it from membership in `CONFIGURABLE_SHORTCUT_IDS`.

### Issue 3 of 3
apps/code/src/renderer/features/sessions/components/ConversationView.tsx:217-221
**Jump picker doesn't anchor subsequent keyboard navigation**

`handleJumpToIndex` scrolls the list but never calls `setKeyboardFocusedMessageId`. After using `Cmd/Ctrl+J` to jump to a message, pressing `Alt+Up` / `Alt+Down` continues from whatever `keyboardFocusedMessageId` was before the picker opened (or from the first/last message if it was `null`), not from the just-jumped-to turn. A user who jump-navigates and then tries to step forward from that position will get a disorienting result. Consider calling `setKeyboardFocusedMessageId` with the target message's ID inside `handleJumpToIndex` (or pass the setter down to the picker).

Reviews (1): Last reviewed commit: "feat(code): add conversation message nav..." | Re-trigger Greptile

Comment on lines +88 to +93
const allEntries = visibleEntries;

const handleSelect = useCallback(
(id: string | null) => {
if (id === null) return;
const entry = allEntries.find((e) => e.id === id);
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.

P2 Superfluous alias: allEntries is always identical to visibleEntries and used only once in handleSelect. The intermediate name adds no clarity and violates the "no superfluous parts" rule.

Suggested change
const allEntries = visibleEntries;
const handleSelect = useCallback(
(id: string | null) => {
if (id === null) return;
const entry = allEntries.find((e) => e.id === id);
const handleSelect = useCallback(
(id: string | null) => {
if (id === null) return;
const entry = visibleEntries.find((e) => e.id === id);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/sessions/components/MessageJumpPicker.tsx
Line: 88-93

Comment:
Superfluous alias: `allEntries` is always identical to `visibleEntries` and used only once in `handleSelect`. The intermediate name adds no clarity and violates the "no superfluous parts" rule.

```suggestion
  const handleSelect = useCallback(
    (id: string | null) => {
      if (id === null) return;
      const entry = visibleEntries.find((e) => e.id === id);
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 282 to +305
editor: "Editor",
};

export const CONFIGURABLE_SHORTCUT_IDS = [
"command-menu",
"new-task",
"settings",
"shortcuts",
"inbox",
"prev-task",
"next-task",
"space-up",
"space-down",
"go-back",
"go-forward",
"toggle-left-sidebar",
"toggle-review-panel",
"close-tab",
"open-in-editor",
"copy-path",
"toggle-focus",
"file-picker",
"message-prev",
"message-next",
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.

P2 Dual source of truth for configurability

CONFIGURABLE_SHORTCUT_IDS and the configurable: true flag on each KeyboardShortcut entry must be kept in sync manually. The flag controls which UI renderer (ShortcutRecorder vs ShortcutKeys) is shown in the sheet; the const array drives the TypeScript type and store logic. Adding a new shortcut requires updating both, but there is no compile-time or runtime guard that catches a mismatch — a shortcut could show ShortcutRecorder in the UI while the store type rejects its ID, or vice versa. Consider deriving one from the other: either generate CONFIGURABLE_SHORTCUT_IDS by filtering KEYBOARD_SHORTCUTS where configurable === true, or drop the configurable flag and compute it from membership in CONFIGURABLE_SHORTCUT_IDS.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/constants/keyboard-shortcuts.ts
Line: 282-305

Comment:
**Dual source of truth for configurability**

`CONFIGURABLE_SHORTCUT_IDS` and the `configurable: true` flag on each `KeyboardShortcut` entry must be kept in sync manually. The flag controls which UI renderer (`ShortcutRecorder` vs `ShortcutKeys`) is shown in the sheet; the const array drives the TypeScript type and store logic. Adding a new shortcut requires updating both, but there is no compile-time or runtime guard that catches a mismatch — a shortcut could show `ShortcutRecorder` in the UI while the store type rejects its ID, or vice versa. Consider deriving one from the other: either generate `CONFIGURABLE_SHORTCUT_IDS` by filtering `KEYBOARD_SHORTCUTS` where `configurable === true`, or drop the `configurable` flag and compute it from membership in `CONFIGURABLE_SHORTCUT_IDS`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +217 to +221

useHotkeys(previousMessageKey, () => handleNavigateMessage(-1), {
enableOnFormTags: true,
enableOnContentEditable: true,
preventDefault: true,
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.

P2 Jump picker doesn't anchor subsequent keyboard navigation

handleJumpToIndex scrolls the list but never calls setKeyboardFocusedMessageId. After using Cmd/Ctrl+J to jump to a message, pressing Alt+Up / Alt+Down continues from whatever keyboardFocusedMessageId was before the picker opened (or from the first/last message if it was null), not from the just-jumped-to turn. A user who jump-navigates and then tries to step forward from that position will get a disorienting result. Consider calling setKeyboardFocusedMessageId with the target message's ID inside handleJumpToIndex (or pass the setter down to the picker).

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/sessions/components/ConversationView.tsx
Line: 217-221

Comment:
**Jump picker doesn't anchor subsequent keyboard navigation**

`handleJumpToIndex` scrolls the list but never calls `setKeyboardFocusedMessageId`. After using `Cmd/Ctrl+J` to jump to a message, pressing `Alt+Up` / `Alt+Down` continues from whatever `keyboardFocusedMessageId` was before the picker opened (or from the first/last message if it was `null`), not from the just-jumped-to turn. A user who jump-navigates and then tries to step forward from that position will get a disorienting result. Consider calling `setKeyboardFocusedMessageId` with the target message's ID inside `handleJumpToIndex` (or pass the setter down to the picker).

How can I resolve this? If you propose a fix, please make it concise.

@Basit-Balogun10 Basit-Balogun10 changed the title Add keyboard navigation for long conversations Add keyboard navigation and jump-to-message modal for long conversations May 25, 2026
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.

Add keyboard navigation and jump-to-message modal for long conversations

1 participant