Skip to content

feat(tui): add label and milestone filter pane to issues browser#163

Merged
snipcodeit merged 2 commits intomainfrom
feat/119-add-label-and-milestone-filter-pane-to
Mar 3, 2026
Merged

feat(tui): add label and milestone filter pane to issues browser#163
snipcodeit merged 2 commits intomainfrom
feat/119-add-label-and-milestone-filter-pane-to

Conversation

@snipcodeit
Copy link
Owner

Summary

  • Adds lib/tui/filter.cjs — a pure FilterState class that extracts available labels and milestones from the loaded issue set, manages active selections with keyboard-navigable cursor, applies filters client-side before fuzzy search runs, and persists last-used selections to .mgw/config.json
  • Adds an interactive filter pane overlay to renderer.cjs that replaces the detail pane when focused — shows label/milestone checkboxes ([x]) and state radio buttons with > cursor highlighting, plus a filter bar summary row showing active filters
  • Wires the full filter-then-search pipeline in index.cjs: filterState.apply(issues) narrows the list before FuzzySearch runs, so search works within the filtered view; filter selections persist on pane close
  • Adds [f] filter-focus, [Space] filter-toggle, [c] filter-clear, and Ctrl+C force-quit bindings to keyboard.cjs

Closes #119

Milestone Context

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

Changes

lib/tui/filter.cjs (new)

  • FilterState class: availableLabels, availableMilestones extracted from issue set at construction
  • Cursor navigation: cursorDown(), cursorUp(), nextSection(), prevSection() move through Labels/Milestones/State sections
  • toggleCursor() — toggles label/milestone membership, selects state option
  • apply(issues) — filters by active labels (AND), active milestones (OR), and state; runs before fuzzy search
  • clearAll() — resets all selections
  • toJSON() / constructor hydration — persistence contract for .mgw/config.json
  • extractLabels() and extractMilestones() — exported helpers

lib/tui/renderer.cjs

  • Added filterPane (blessed box, overlays detail pane when focusPane === 'filter'): renders label checkboxes, milestone checkboxes, state radio buttons with cursor indicators
  • _formatFilterPane(filterState) — builds pane content from FilterState
  • _formatFilterBar(filterState) — builds summary row (e.g. Filter: Labels: bug | Milestone: v4)
  • Updated render() to show/hide filter pane vs detail pane based on focusPane; passes filterState through
  • startKeyboard() updated: in filter mode, j/k// emit filter-scroll-*; Tab/Shift+Tab emit filter-next/prev-section
  • Header updated to include [f] filter hint
  • Help overlay updated with filter shortcuts

lib/tui/index.cjs

  • Imports FilterState from ./filter.cjs
  • _loadPersistedFilters() / _savePersistedFilters() — read/write .mgw/config.json tuiFilters key
  • CLI initialFilter flags seed the FilterState (label, milestone, state)
  • refilter() — recalculates filtered = searchIn(filterState.apply(issues), query) after any filter or search change
  • Keyboard handlers: filter-focus, filter-scroll-down/up, filter-next/prev-section, filter-toggle, filter-clear
  • help, jump-top/bottom, page-up/down, tab-focus-reverse, force-quit handlers added

lib/tui/keyboard.cjs

  • Added 'f': 'filter-focus', ' ': 'filter-toggle', 'c': 'filter-clear', '\u0003': 'force-quit'

Test Plan

  • MGW_NO_TUI=1 node -e "require('./lib/tui/filter.cjs')" — module loads without error
  • Unit: FilterState label toggle — fs.toggleCursor() on label bugfs.activeLabels has bug; toggle again → removed
  • Unit: FilterState state filter — fs.activeState = 'closed' then fs.apply(issues) returns only closed issues
  • Unit: FilterState milestone filter — select milestone v4 → only issues with that milestone returned
  • Unit: Filter+search pipeline — filter to milestone v4, then search 'auth' → intersection
  • Unit: fs.clearAll()isEmpty === true
  • Unit: fs.toJSON() / constructor hydration round-trip — active selections survive JSON serialisation
  • MGW_NO_TUI=1 node bin/mgw.cjs issues --limit 5 — static table renders (no filter pane, no error)
  • Interactive TTY: f opens filter pane (detail pane replaced by filter pane with label/milestone/state sections)
  • Interactive TTY: j/k navigate cursor within filter pane rows
  • Interactive TTY: Tab advances cursor to next section (Labels → Milestones → State → Labels)
  • Interactive TTY: Space toggles selected label/milestone; issue list updates in real time
  • Interactive TTY: Space on State section selects that state option; list updates
  • Interactive TTY: f again closes filter pane (returns to detail pane)
  • Interactive TTY: c clears all active filters from anywhere; list returns to full set
  • Interactive TTY: filter bar row reflects active filters when filter pane is closed
  • Interactive TTY: quit and relaunch — last-used filters restored from .mgw/config.json
  • Interactive TTY: Ctrl+C force-quits from filter mode

🤖 Generated with Claude Code

Adds an interactive filter pane to the mgw:issues TUI browser that lets
users narrow the issue list by labels, milestones, and state in real time.

- New `lib/tui/filter.cjs`: FilterState class extracts available labels
  and milestones from the loaded issue set, manages active selections,
  applies filters client-side before fuzzy search runs, and persists
  last-used selections to .mgw/config.json
- `lib/tui/renderer.cjs`: filter pane overlay (replaces detail pane when
  focused) shows label/milestone checkboxes and state radio buttons with
  cursor highlighting; filter bar summary row updates to reflect active
  filters; help overlay updated with new shortcuts
- `lib/tui/index.cjs`: FilterState wired into browser state; filter-then-
  search pipeline (filterState.apply → FuzzySearch); keyboard handlers for
  filter-focus, filter-toggle, filter-clear, filter-scroll-down/up,
  filter-next/prev-section; filter selections persist on close
- `lib/tui/keyboard.cjs`: added [f] filter-focus, [Space] filter-toggle,
  [c] filter-clear, and Ctrl+C force-quit bindings

Closes #119

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.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 #163 — label and milestone filter pane

The FilterState class is well-designed — clean separation of concerns, good persistence contract, and the filter logic (AND for labels, OR for milestones) is correct. However there is one critical runtime bug and several gaps.


CRITICAL: FuzzySearch.searchIn does not exist

In lib/tui/index.cjs, _applyFiltersAndSearch() calls:

return query ? search.searchIn(afterFilter, query) : afterFilter;

But FuzzySearch (in lib/tui/search.cjs) only exposes a search(query) method that searches this.items — it has no searchIn(subset, query) method. This will throw:

TypeError: search.searchIn is not a function

at runtime the moment a filter is active and the user types a search query. The PR body even acknowledges this: "FuzzySearch does not have a searchIn method — we need a helper that creates a temporary FuzzySearch over a subset." — but no helper was implemented.

Fix options:

  1. Add a searchIn(items, query) method to FuzzySearch:
    searchIn(items, query) {
      const temp = new FuzzySearch(items, { keys: this.keys });
      return temp.search(query);
    }
  2. Or construct a new FuzzySearch inline in _applyFiltersAndSearch:
    function _applyFiltersAndSearch() {
      const afterFilter = filterState.apply(issues);
      if (!query) return afterFilter;
      const scoped = new FuzzySearch(afterFilter, { keys: ['title', 'number', 'labels'] });
      return scoped.search(query);
    }

Option 1 is cleaner and keeps search.cjs as the single source of search logic.


CRITICAL: No test file added for filter.cjs

FilterState is a pure in-memory class with no I/O dependencies — it is ideal for unit testing. The PR test plan lists 17 testable assertions but none are implemented. test/tui-filter.test.cjs should cover:

  • extractLabels / extractMilestones deduplication and sort
  • Label toggle (add/remove)
  • Milestone toggle
  • State filter selection via toggleCursor()
  • apply() — AND logic for labels, OR logic for milestones, state filtering
  • clearAll() resets everything
  • toJSON() / constructor hydration round-trip
  • isEmpty getter

These are all synchronous, no mocking needed.


CRITICAL: _savePersistedFilters is never called

_savePersistedFilters(filterState) is defined in lib/tui/index.cjs but is never called in the keyboard handler wiring shown in the diff. There is no filter-toggle or filter-clear handler that calls it after mutations. Without saves, the persistence feature (one of the stated test plan items) is non-functional.

Expected pattern in each mutating handler:

keyboard.on('filter-toggle', () => {
  filterState.toggleCursor();
  _savePersistedFilters(filterState);
  filtered = _applyFiltersAndSearch();
  draw();
});

keyboard.on('filter-clear', () => {
  filterState.clearAll();
  _savePersistedFilters(filterState);
  filtered = _applyFiltersAndSearch();
  draw();
});

filtered variable not updated on filter change

The existing filtered variable is initialized as search.search(query) (searching all issues). When filters are applied via _applyFiltersAndSearch(), the result should replace filtered. But the keyboard handlers need to explicitly reassign filtered = _applyFiltersAndSearch() after any filter mutation. Verify all mutating handlers do this.


Merge order dependency on #162

This PR references helpVisible and PAGE_SIZE which are introduced in PR #162. Ensure #162 is merged first. If merged out of order, this PR will fail at runtime.


Good things

  • FilterState class design is excellent — clean cursor model, good section abstraction
  • Label AND / Milestone OR filter semantics are explicitly documented and correct
  • Persisted filter hydration clamps to available options (prevents stale selections)
  • clearAll() correctly resets activeState to 'open' (not 'all')
  • toJSON() returning plain arrays (not Sets) is correct for JSON serialization

…terState tests

- Fix _applyFiltersAndSearch() to call _searchIn() instead of the
  non-existent search.searchIn() method (TypeError at runtime)
- Add _savePersistedFilters() call to filter-toggle handler so label/
  milestone selections are persisted to .mgw/config.json on each toggle,
  matching the existing behaviour in filter-clear and select/quit handlers
- Add test/filter.test.cjs: 42 unit tests covering FilterState construction
  (label/milestone extraction, persisted state restore), toggleCursor()
  add/remove semantics, apply() with label/state/milestone/combined filters,
  clearAll(), isEmpty, and toJSON() round-trip serialisation
@snipcodeit snipcodeit merged commit 3b81f33 into main Mar 3, 2026
1 check passed
@snipcodeit snipcodeit deleted the feat/119-add-label-and-milestone-filter-pane-to 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.

Add label and milestone filter pane to mgw:issues TUI

1 participant