Skip to content

feat(sessions): keyboard navigation and jump-to-message picker#3406

Closed
charlesvien wants to merge 23 commits into
PostHog:mainfrom
Basit-Balogun10:posthog-code/chat-timeline
Closed

feat(sessions): keyboard navigation and jump-to-message picker#3406
charlesvien wants to merge 23 commits into
PostHog:mainfrom
Basit-Balogun10:posthog-code/chat-timeline

Conversation

@charlesvien

Copy link
Copy Markdown
Member

Recreates #2361, which GitHub refuses to reopen because the head branch was force-pushed after it was closed. Same branch and author (@Basit-Balogun10), now including the two commits that port the feature onto the new ChatThread (8ff1896, f733e63).

Note: still stacked on #2321, so the diff includes its 21 commits (cross-fork branches can't be PR bases). The actual change here is the last two commits.

Adds Alt/Option+Up/Down navigation between user messages, a Cmd/Ctrl+J jump-to-message picker with date filtering and the matching entries in the shortcuts sheet. See #2361 for the full description and demo videos.

Closes #2360.

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)
The configurable shortcuts logic was built in primitives/ but the app always
imported from features/command/. Copy the enhanced version (ShortcutRecorder
rows, search bar, reset-all footer) into features/command/ and point
ShortcutsSettings at that canonical location.
… disable-with-tooltip instead of hide

- Remove divider between edit actions and destructive actions
- Remove red color from Remove binding (non-destructive; easily reversible)
- Move Reset to default before Remove binding so Remove is last
- Show Add another binding as disabled-with-tooltip at max capacity instead of hiding it
- Add sticky footer to shortcuts settings page
- Replace fullscreen ShortcutRecordingModal with inline InlineRecorder that
  captures keys directly in the chip row — no backdrop, no modal
- Live partial preview: modifier-only presses show a pulsing partial combo
  (e.g. Ctrl… then Ctrl+Shift+K) before the final key is pressed
- Pencil icon appears on chip hover; click chip or pencil to edit inline;
  click outside or Escape to cancel
- Remove Keycap press animation — chips are purely visual, no confusing
  press effect on non-editable shortcuts
- Fix eventToCombo Mac bug: Ctrl key on Mac was mapped to mod (⌘) instead
  of ctrl (⌃); same fix in tiptapEventToCombo and eventToSearchCombo
- Allow shift+non-printable combos (shift+up/down) in the recorder so
  prompt-history defaults can be re-recorded; block shift+letter to avoid
  conflicts with normal typing
- Move ShortcutRecorder from primitives/ into features/command/
- Delete dead primitives/KeyboardShortcutsSheet.tsx
- Fix Windows encoding corruption in useTiptapEditor.ts comment
Re-targets PostHog#2361's Alt/Option+Up/Down message navigation and Cmd/Ctrl+J
jump-to-message picker (with date filter) from the legacy ConversationView
onto the new ChatThread view, so the feature survives once useNewChatThread
is flipped on and ConversationView is removed.

Adds the message-prev/message-next/message-jump shortcuts (new in PostHog#2361,
not part of PostHog#2321) and wires navigation + the jump picker through
ChatThread's own scroller engine (scrollToMessage by id) instead of the
legacy VirtualizedList's index-based scrollToIndex.
Move the keyboard-focus indicator from ChatMessage (the outer row
wrapper) to ChatBubble (the actual visible bubble) — it was tracing a
box wider than what's painted, reading as disconnected corner brackets.
Match the bubble's own 8px radius so the ring's corners round cleanly
instead of clashing with the bubble underneath (this was also the root
cause of an apparent top-edge clipping artifact). Use a brighter,
higher-contrast neutral (gray-11) instead of the app's warm yellow/orange
brand accent, which reads as jarring against the new rounded-bubble
design — not a fit for every accent-colored highlight.

MessageJumpPicker's date-filter trigger now shows the full preset label
("Last month") instead of an abbreviated one ("Last mo"); shortLabel had
no other consumer so it's removed.
@trunk-io

trunk-io Bot commented Jul 13, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(sessions): ChatThread focus ring + j..." | Re-trigger Greptile

Comment on lines +287 to +297
const handleSave = useCallback(
(combo: string) => {
if (recordingMode?.type === "edit") {
updateKeybinding(id, recordingMode.key, combo);
} else {
addKeybinding(id, combo);
}
stopRecording();
},
[recordingMode, id, updateKeybinding, addKeybinding, stopRecording],
);

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.

P1 Added Binding Replaces Defaults

When a shortcut is still using defaults, “Add another binding” saves only the new combo because addKeybinding starts from an empty custom array. A user adding a second binding for New task loses mod+n and mod+t, so the advertised defaults stop working after the first customization.

Suggested change
const handleSave = useCallback(
(combo: string) => {
if (recordingMode?.type === "edit") {
updateKeybinding(id, recordingMode.key, combo);
} else {
addKeybinding(id, combo);
}
stopRecording();
},
[recordingMode, id, updateKeybinding, addKeybinding, stopRecording],
);
const handleSave = useCallback(
(combo: string) => {
if (recordingMode?.type === "edit") {
updateKeybinding(id, recordingMode.key, combo);
} else {
if (!hasCustom) {
for (const key of defaultBindings) {
addKeybinding(id, key);
}
}
addKeybinding(id, combo);
}
stopRecording();
},
[
recordingMode,
id,
updateKeybinding,
addKeybinding,
stopRecording,
hasCustom,
defaultBindings,
],
);

Comment on lines +271 to +275
if (customTo) {
filtered = filtered.filter(
(e) => e.timestamp <= new Date(customTo).getTime(),
);
}

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 End Minute Excludes Messages

The custom to filter compares messages against the exact start of the selected minute. If a user chooses 14:30 as the end time, messages sent at 14:30:01 through 14:30:59 are hidden from the picker, so they cannot be selected even though they fall within the chosen minute.

Suggested change
if (customTo) {
filtered = filtered.filter(
(e) => e.timestamp <= new Date(customTo).getTime(),
);
}
if (customTo) {
const to = new Date(customTo);
to.setSeconds(59, 999);
filtered = filtered.filter((e) => e.timestamp <= to.getTime());
}

@charlesvien

Copy link
Copy Markdown
Member Author

Superseded by #3407, same work squashed into a single commit and rebased onto main.

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

2 participants