Skip to content

feat(tui): implement keyboard navigation, jump, page scroll, and help overlay#162

Merged
snipcodeit merged 2 commits intomainfrom
feat/118-implement-fuzzy-search-and-keyboard
Mar 3, 2026
Merged

feat(tui): implement keyboard navigation, jump, page scroll, and help overlay#162
snipcodeit merged 2 commits intomainfrom
feat/118-implement-fuzzy-search-and-keyboard

Conversation

@snipcodeit
Copy link
Owner

Summary

  • Wires all keyboard actions that were defined in DEFAULT_BINDINGS but had no handlers: jump-top (g/Home), jump-bottom (G/End), page-up/page-down (PgUp/PgDn), tab-focus-reverse (Shift+Tab), help (?), and force-quit (Ctrl+C)
  • Adds a centered help overlay (blessed.box) that lists all keyboard shortcuts, toggled with ? and closeable with q
  • quit now hierarchically closes help or search mode before exiting the browser
  • Adds createdAt, url, body, and comments to listIssues so the detail pane and static table age column have complete data

Closes #118

Milestone Context

  • Milestone: v4 — Interactive CLI & TUI
  • Phase: 27 — TUI Issue Browser with Fuzzy Search
  • Issue: 2 of 9 in milestone

Changes

lib/tui/index.cjs

  • Added PAGE_SIZE = 10 constant for page scroll amount
  • Added helpVisible state variable
  • Passed helpVisible to renderer.render() in draw()
  • Added handlers for all previously unhandled keyboard events: jump-top, jump-bottom, page-up, page-down, tab-focus-reverse, help, force-quit
  • Updated quit handler to close help overlay or exit search mode before quitting

lib/tui/keyboard.cjs

  • Added Ctrl+C (\u0003) → force-quit binding to DEFAULT_BINDINGS

lib/tui/renderer.cjs

  • Added helpOverlay (blessed.box, centered, 52×20) listing all keyboard shortcuts
  • Updated render() to accept helpVisible and call helpOverlay.show()/.hide()
  • Added Ctrl+C passthrough in search mode so force-quit works while typing
  • Status bar now shows [?] help hint in normal mode
  • Fixed cursor display to use unicode escape (\u2588) instead of literal

lib/github.cjs

  • Added createdAt, url, body, comments to the listIssues JSON field list

Test Plan

  • MGW_NO_TUI=1 node bin/mgw.cjs issues --limit 5 — static table shows Age column with relative times (not -)
  • MGW_NO_TUI=1 node bin/mgw.cjs issues --search "tui" — search filters static table correctly
  • Unit: node -e "const { KeyboardHandler, DEFAULT_BINDINGS } = require('./lib/tui/keyboard.cjs'); const kb = new KeyboardHandler(); let hits = []; ['jump-top','jump-bottom','page-up','page-down','help','force-quit','tab-focus-reverse'].forEach(e => kb.on(e, () => hits.push(e))); kb.dispatch('g'); kb.dispatch('G'); kb.dispatch('\u001B[5~'); kb.dispatch('\u001B[6~'); kb.dispatch('?'); kb.dispatch('\u0003'); kb.dispatch('\u001B[Z'); console.log(hits.join(','));" — all 7 events fire
  • Interactive TTY: ? opens help overlay listing shortcuts, ? or q closes it
  • Interactive TTY: g/Home jumps to first item, G/End jumps to last
  • Interactive TTY: PgDn moves selection down 10, PgUp moves up 10
  • Interactive TTY: Shift+Tab cycles focus in reverse order
  • Interactive TTY: Ctrl+C force-quits from any mode including search
  • Interactive TTY: Detail pane shows URL and body (previously empty)

🤖 Generated with Claude Code

…w:issues TUI

Wire all keyboard actions defined in DEFAULT_BINDINGS but previously missing
handlers: jump-top (g/Home), jump-bottom (G/End), page-up/page-down (PgUp/PgDn),
tab-focus-reverse (Shift+Tab), help (?), and force-quit (Ctrl+C).

- index.cjs: add PAGE_SIZE constant, helpVisible state, handlers for all missing
  actions; quit now closes help/search before exiting; force-quit calls process.exit
- keyboard.cjs: add Ctrl+C → force-quit binding to DEFAULT_BINDINGS
- renderer.cjs: add help overlay (blessed.box, centered, lists all shortcuts);
  render() accepts helpVisible to show/hide overlay; Ctrl+C passes through in
  search mode; status bar now shows [?] help hint; cursor char via unicode escape
- github.cjs: add createdAt, url, body, comments to listIssues JSON fields so
  the detail pane and static table age column have complete data

Closes #118

Co-Authored-By: Stephen Miller <21005207+snipcodeit@users.noreply.github.com>
Copy link
Owner Author

@snipcodeit snipcodeit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #162 — keyboard navigation, jump, page scroll, help overlay

Overall the implementation is solid — the keyboard handler wiring is clean and the help overlay approach works well. However there are several issues that need addressing before merge.


CRITICAL: tab-focus-reverse references a pane that does not exist in this PR

In lib/tui/index.cjs, the tab-focus-reverse handler cycles through ['list', 'detail', 'filter']. But 'filter' as a focusable pane is implemented in PR #163, not here. If this PR is merged before #163, Tab in reverse order will cycle to a filter pane that has no render path, causing a silent visual bug (the pane focus silently shifts to an unrendered state).

The PR should either:

  1. Document this as a hard dependency on #163 and ensure they are merged together, OR
  2. Use ['list', 'detail'] here and extend it in #163

The help overlay also shows [f] filter in the hint text — the f key for filter-focus is added in PR #163, not here. The hint will show a non-functional shortcut.


CRITICAL: No test file added

The test plan lists 7 unit-testable assertions (e.g. "all 7 events fire" inline command). These should live in test/tui-keyboard.test.cjs rather than inline command snippets in the PR description. The TUI test plan specifically tests KeyboardHandler dispatching which is side-effect-free and straightforward to unit test:

const { KeyboardHandler, DEFAULT_BINDINGS } = require('./lib/tui/keyboard.cjs');

describe('DEFAULT_BINDINGS — all new bindings dispatch correctly', () => {
  it('dispatches jump-top on g', () => { ... });
  it('dispatches jump-bottom on G', () => { ... });
  it('dispatches page-up / page-down', () => { ... });
  it('dispatches help on ?', () => { ... });
  it('dispatches force-quit on Ctrl+C', () => { ... });
  it('dispatches tab-focus-reverse on Shift+Tab', () => { ... });
});

Without this, the new bindings have zero automated coverage.


Double Ctrl+C dispatch in search mode

In renderer.cjs, the search mode key handler explicitly dispatches '\u0003' to the keyboard when Ctrl+C is pressed:

if (ch === '\u0003' || key.full === 'C-c') {
  keyboard.dispatch('\u0003', key);
  return;
}

But DEFAULT_BINDINGS also registers '\u0003': 'force-quit' so keyboard.dispatch would fire the force-quit event. This is correct, but the comment says // Always allow Ctrl+C to force-quit even in search mode — it would be clearer to emit 'force-quit' directly rather than going through raw character dispatch:

keyboard.emit('force-quit');
return;

This avoids relying on the binding table being correct and makes intent explicit.


startKeyboard() already in use — verify no double-registration

The startKeyboard() method in renderer.cjs registers a screen-level key handler. If createBlessedRenderer() is called more than once (e.g. in tests or restart scenarios), startKeyboard() could register multiple listeners. Confirm there is a guard or that it is only ever called once per lifecycle.


Good things

  • Hierarchical quit (help → search → exit) is clean and correct
  • force-quit as a separate event from quit is the right design
  • Unicode escape \u2588 for cursor fix is correct
  • helpVisible threaded cleanly through draw()renderer.render()

- Add test/keyboard-nav.test.cjs with 25 tests covering KeyboardHandler dispatch,
  helpVisible toggle pattern, updateSearch, bind/unbind, DEFAULT_BINDINGS export,
  and custom bindings constructor
- Add comment to lib/tui/index.cjs tab-focus-reverse handler noting that the
  'filter' tab stop depends on the filter pane from PR #119
@snipcodeit snipcodeit merged commit 474e302 into main Mar 3, 2026
1 check passed
@snipcodeit snipcodeit deleted the feat/118-implement-fuzzy-search-and-keyboard branch March 3, 2026 02:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Changes to core library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement fuzzy search and keyboard navigation in mgw:issues TUI

1 participant