-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add command palette with autocomplete and fuzzy matching #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
airplne
wants to merge
15
commits into
main
Choose a base branch
from
feature/command-palette
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Implements interactive command palette that appears when typing /. Includes autocomplete, keyboard navigation, fuzzy matching, and "did you mean" suggestions for typos. Phase 1: Suggestion Algorithm - src/ui/utils/command-suggestions.ts: Levenshtein distance + matching logic - Supports: exact, prefix, alias, substring, and fuzzy matching - Scoring system: exact (1000) > prefix (900) > alias-exact (800) > alias-prefix (700) > substring (600) > fuzzy (500-distance*100) - tests/unit/command-suggestions.test.ts: 25 tests for algorithm Phase 2: UI Component - src/ui/components/command-palette.tsx: Visual command list - Shows: command name, description, matched alias, selection indicator - Cyan borders, inverse text for selected item - Help hint: Tab/arrows/Esc/Enter Phase 3: Input Integration - src/ui/components/input.tsx: Integrated palette display - Shows palette when input starts with / - Arrow keys navigate suggestions (only when palette visible) - Tab autocompletes to selected command - Escape closes palette - No conflict with bracketed paste or tool navigation Phase 4: "Did You Mean" Suggestions - src/commands/index.ts: Enhanced unknown command errors - Uses fuzzy matching to suggest similar commands - Example: "/promt" → "Did you mean: /prompt?" UX Features: - Type "/" → shows all commands - Type "/pr" → filters to /prompt - Type "/pf" → matches alias - Type "/promt" → fuzzy matches to /prompt - ↑/↓ navigate, Tab autocompletes, Esc closes Test results: 238/238 tests pass (+25 new tests) Build: TypeScript compilation clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Addresses Codex NO-GO findings:
Fix A (HIGH): Esc now actually closes palette
- Add paletteDismissed state to track user dismissal
- Esc sets paletteDismissed=true (hides palette, keeps input)
- Typing/backspace/paste clears dismissal (palette returns)
- Palette only shows while editing command (before first space)
Fix B (HIGH): Arrow keys don't conflict with tool navigation
- Add onPaletteVisibilityChange callback to InputPrompt
- App tracks isCommandPaletteOpen state via callback
- Tool navigation useInput checks !isCommandPaletteOpen
- When palette open: arrows navigate palette only
- When palette closed: arrows enter tool navigation as before
Fix C (LOW): "Did you mean" shows top 1-3 suggestions
- Add getDidYouMeanSuggestions() (plural) returning up to 3 matches
- Filter to fuzzy/substring only (not exact/prefix)
- Update error message to show multiple suggestions:
- Did you mean:
/prompt
/promptfile (alias: pf)
- Keep deprecated singular function for compatibility
Changes:
- src/ui/components/input.tsx: dismissal state + visibility callback
- src/ui/app.tsx: track palette state, skip tool nav when open
- src/ui/utils/command-suggestions.ts: plural suggester + filter
- src/commands/index.ts: show top 3 suggestions in errors
- tests/unit/command-suggestions.test.ts: update tests
Test results: 239/239 tests pass (+1 new test)
Build: TypeScript compilation clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Problem: Arrow keys don't move cursor in TUI input. Edits only work
by appending at end. Cursor is a static "|" marker, not interactive.
Solution: Implement cursor-based editing with pure utility + tests.
Phase 1: Pure Editor Utility (src/ui/utils/input-editor.ts)
- EditorState: { value, cursorIndex }
- Operations:
- insertAtCursor(state, text) - insert at cursor, advance
- deleteBackward(state) - backspace before cursor
- deleteForward(state) - delete at cursor
- moveCursor(state, 'left'|'right') - navigate
- setValue(state, newValue) - replace all, cursor to end
- Rendering:
- renderWithCursor(value, cursor) - inserts | at position
- getDisplayWithCursor(state) - handles single/multi-line
Phase 2: Comprehensive Tests (tests/unit/input-editor.test.ts)
- Insert at start/middle/end (+4 tests)
- Backspace at boundaries (+3 tests)
- Delete at boundaries (+3 tests)
- Left/right movement (+4 tests)
- Cursor clamping (+3 tests)
- setValue behavior (+1 test)
- Rendering with cursor (+6 tests)
- Multi-line display (+4 tests)
- Complex scenarios (+3 tests: edit sequences, paste at cursor)
Phase 3: Input Integration (src/ui/components/input.tsx)
- Replace value state with editorState (EditorState)
- Left/right arrows: moveCursor() (when palette NOT visible)
- Backspace: deleteBackward()
- Delete: deleteForward()
- Typing: insertAtCursor()
- Paste: insertAtCursor() at current position, advance cursor
- Cursor rendered in actual position (even across lines)
Note: Home/End keys not implemented (Ink doesn't expose key.home/key.end)
Behavior:
- Type "hello", press ←←, type "X" → "helXlo" with cursor after X
- Press Delete → deletes char at cursor
- Paste at cursor → inserts there, advances cursor
- ←/→ only active when palette closed (palette uses arrows)
- Paste clears paletteDismissed (palette returns after paste+edit)
Test results: 274/274 tests pass (+35 new tests)
Build: TypeScript compilation clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Addresses Codex NO-GO findings for PR #11: Fix 1 (HIGH): Home/End keys now functional - Add isHome/isEnd detection with escape sequence fallbacks: - Home: \x1b[H, \x1bOH, \x1b[1~ - End: \x1b[F, \x1bOF, \x1b[4~ - Call moveCursorToBoundary('start'|'end') - Works across terminal variations Fix 2 (HIGH): Arrow key escape sequence fallbacks - Left: \x1b[D, \x1bOD (in addition to key.leftArrow) - Right: \x1b[C, \x1bOC (in addition to key.rightArrow) - Ensures cursor movement works even when Ink doesn't detect arrows Fix 3 (HIGH): Multi-line cursor always visible - Old: "line1... [3 lines, cursor on line 2]" (no visible |) - New: "line1... [3 lines]\n(line 2) li|ne2" (cursor visible) - Compute cursor line + column index - Render cursor line with | at exact position - Format: `(line N) [line content with | inserted]` Fix 4: Test updates - Replace "cursor on line N" checks with visible cursor assertions - Expect display.toContain('(line 2)') AND display.toContain('li|ne2') - Ensures tests enforce visible cursor rendering Impact: - Home/End jump to start/end of input - ←/→ work reliably across terminal types - Multi-line preview shows WHERE cursor is (not just which line) - Paste at cursor in multi-line input is now visually trackable Test results: 274/274 tests pass (unchanged) Build: TypeScript compilation clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Problem: When pasting /prompt commands, bracketed paste markers (\x1b[200~,
\x1b[201~) or stripped forms ([200~, [201~) leak into command arguments,
corrupting file paths like "docs/file.md[200~" and causing validateAndOpen()
to correctly fail with "Path does not exist".
Root cause: Terminals sometimes split ESC from paste sequences, so [200~ bypasses
the input.startsWith('\x1b') filter and gets inserted into editor value.
Solution: Three-layer defense (input + parser + handler).
Layer 1: Input Component (src/ui/components/input.tsx)
- Import containsPasteMarkers() from command utils
- Filter input chunks containing paste markers in useInput
- Check: !input.startsWith('\x1b') AND !containsPasteMarkers(input)
Layer 2: Parser (src/commands/parser.ts)
- Sanitize control sequences in parseInput() before parsing
- Removes bracketed paste markers + general ANSI/VT sequences
- Ensures ParsedCommand.args are clean
Layer 3: Prompt Handler (src/commands/handlers/prompt.ts)
- Belt-and-suspenders: sanitize filePathRaw before resolving
- Prevents any leaked markers from reaching validateAndOpen()
New Utility: src/commands/utils.ts
- sanitizeControlSequences(input): removes paste markers + ANSI codes
- containsPasteMarkers(input): detection helper
- Handles: \x1b[200~, \x1b[201~, [200~, [201~, ANSI/VT sequences
Comprehensive Tests:
- tests/unit/command-parser-sanitize.test.ts (18 new tests)
- Sanitization function tests
- parseInput with paste markers (ESC prefix + stripped)
- Real-world /prompt scenarios
- tests/unit/prompt-command.test.ts (+1 regression test)
- Verifies /prompt succeeds even if args contain paste markers
Impact:
- Pasted: /prompt docs/GROK-SUBAGENT-SMOKE-TEST.md → works reliably
- No more "Path does not exist ... [200~" errors
- All command arguments protected from control sequence corruption
Test results: 293/293 tests pass (+19 new tests)
Build: TypeScript compilation clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Implements session-level auto-approval for Edit/Write tool confirmations,
reducing repeated prompts while keeping Bash always-confirm for safety.
Problem: Every Edit/Write requires y/n confirmation, slowing workflow.
Solution: Add "Auto-accept edits (session)" option in confirmation dialog.
Core Implementation:
1. Confirmation Decision Utility (src/ui/utils/confirm-decision.ts)
- shouldAutoApprove(toolName, autoAcceptEdits): returns true for Edit/Write when enabled
- getConfirmOptions(toolName): returns 3 options for Edit/Write, 2 for Bash
- isAutoAcceptEligible(toolName): checks if tool can be auto-accepted
- AUTO_ACCEPT_ELIGIBLE_TOOLS: ['Edit', 'Write'] (never includes Bash)
2. Enhanced Confirmation Dialog (src/ui/components/confirm.tsx)
- Now shows 3 options for Edit/Write:
▶ [y] Allow once
[a] Auto-accept edits (session)
[n] Deny
- Bash and other tools show only [y] Allow once / [n] Deny
- Tab: cycle forward through options
- Shift+Tab (\x1b[Z): cycle backward
- Enter: select highlighted option
- Letter shortcuts: y/a/n still work
- Escape: deny (convenience)
- Shows "Auto-accept edits: ENABLED" when feature is active
3. App State Integration (src/ui/app.tsx)
- Add autoAcceptEdits state (session-only, resets on exit)
- handleConfirmation: check shouldAutoApprove() before showing dialog
- handleConfirmResponse: handle ConfirmDecision type (not boolean)
- Header indicator: [AUTO-EDIT: ON] when enabled
- Auto-approved tools bypass dialog entirely
4. /auto-edit Command (src/commands/handlers/auto-edit.ts)
- Usage: /auto-edit on|off
- Aliases: /autoedit, /ae
- Action: set_auto_edit to toggle state
- Output explains scope (Edit/Write only, session-only)
Type Changes:
- ConfirmDecision: 'allow_once' | 'auto_accept_edits' | 'deny'
- CommandAction: add set_auto_edit action type
- ConfirmDialog onResponse: now receives ConfirmDecision (not boolean)
Comprehensive Tests (tests/unit/confirm-decision.test.ts):
- shouldAutoApprove: Edit/Write approved when enabled, Bash never (5 tests)
- getConfirmOptions: 3 for Edit/Write, 2 for Bash (6 tests)
- isAutoAcceptEligible: correct tool classification (4 tests)
- Decision flow scenarios (1 test)
UX Flow:
1. Edit tool triggers confirm dialog
2. User presses Tab to "Auto-accept edits (session)", then Enter
3. Future Edit/Write calls auto-approve (no dialog)
4. Bash still always prompts
5. /auto-edit off disables feature
6. Header shows [AUTO-EDIT: ON] when active
Security:
- Session-only (no persistence to disk/env)
- Bash explicitly excluded from auto-accept
- User can disable at any time via /auto-edit off
Test results: 308/308 tests pass (+15 new tests)
Build: TypeScript compilation clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Problem: Tab/Shift+Tab didn't cycle through confirmation options in the dialog. Root cause: Terminal sequence detection was insufficient for Pop!_OS and tmux. Solution: Comprehensive terminal sequence handling + tests. Shift+Tab Detection (backwards navigation): - ESC [ Z (standard CSI sequence) - ESC [ 1 ; 2 Z (extended CSI with modifiers) - [Z (ESC-stripped by terminal) - [1;2Z (ESC-stripped extended) - key.tab && key.shift (Ink key fields when available) Tab Detection (forward navigation): - key.tab (Ink key field, primary) - input === '\t' (fallback for literal tab) Changes: - src/ui/components/confirm.tsx: Expanded Shift+Tab detection with 5 fallbacks - src/ui/components/confirm.tsx: Check key.tab first, then input fallback - tests/unit/confirm-decision.test.ts: +6 tests for terminal sequences Tests verify: - Standard Shift+Tab (ESC [ Z) - Extended Shift+Tab (ESC [ 1 ; 2 Z) - ESC-stripped variants ([Z, [1;2Z]) - Selection cycling logic (forward/backward wrapping) Impact: - Tab/Shift+Tab now works across terminal variations - Fixes navigation in Pop!_OS, tmux, GNOME Terminal - Selection wraps correctly (0 ← Shift+Tab wraps to last option) Test results: 314/314 tests pass (+6 new tests) Build: TypeScript compilation clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Problem: Backspace and Delete keys do nothing in main chat input on Pop!_OS GNOME Terminal and tmux. Cursor frozen, text doesn't delete. Root cause: Ink's key.backspace/key.delete flags not set on these terminals. Backspace arrives as raw bytes (\x7f, \b, \x08) without flag, falls through to text insertion path, gets filtered or corrupts string with control chars. Solution: Detect Backspace/Delete via both Ink flags AND byte codes. Changes to src/ui/components/input.tsx: Backspace Detection (before text insertion): - key.backspace (Ink flag, primary) - input === '\x7f' (DEL byte, most common for Backspace) - input === '\b' or '\x08' (BS byte, Control-H variant) - Return early after handling Delete Detection: - key.delete (Ink flag, primary) - input === '\x1b[3~' (standard delete sequence) - input === '[3~' (ESC-stripped variant) - Return early after handling Text Insertion Guard: - Added control character filter (< 0x20 except tab) - Prevents DEL/BS bytes from corrupting editor value - Ensures only printable text gets inserted Debug Overlay (src/ui/components/confirm.tsx): - Press '?' in confirm dialog to enable debug mode - Shows: input (escaped) + key flags (tab, shift, return, etc.) - Helps diagnose terminal sequence variations - Can be used to verify Tab/Shift+Tab detection Tests (tests/unit/input-editor.test.ts): - Multiple backspaces from different cursor positions (+1 test) - Control char corruption prevention (+1 test) - Terminal byte code detection (DEL, BS, delete sequence) (+3 tests) Impact: - Backspace now works on Pop!_OS GNOME Terminal and tmux - Delete key works reliably - No control char corruption in editor value - Works with or without Ink key flags Test results: 319/319 tests pass (+5 new tests) Build: TypeScript compilation clean Manual verification steps: 1. Run grok 2. Type "hello world" 3. Press Backspace → text deletes, cursor moves left 4. Press ← then Delete → character at cursor deletes 5. Verify in both GNOME Terminal and tmux 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
… exist
CRITICAL BUG: Line 486 returned unconditionally, blocking ALL keypresses
(including Backspace) from reaching InputPrompt when tool outputs existed.
Root cause: app.tsx useInput handler for tool navigation had:
if (!isNavigatingTools) {
if (key.downArrow) { ... return; }
if (key.upArrow) { ... return; }
return; // ← KILLED ALL INPUT
}
This meant ANY keypress when tools exist but not navigating = early return.
Backspace, typing, everything blocked.
Fix: Remove line 486. Only return on arrow keys, let other keys pass through.
Test: Type text, press Backspace → now works.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
…ping
CRITICAL: isActive={selectedToolIndex === null} disabled InputPrompt when
navigating tool outputs, blocking Backspace and all text input.
Fix: isActive={true} always. Tool navigation and input editing are independent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Comprehensive review of PRP-KEYTAR-IGNORE-SCRIPTS-DETECTION.md: - Verdict: GO with minor enhancements (90% ready) - Identified gaps: code anchors, test mocking strategy, async signature - Recommended additions before execution - Estimated effort: 2-3 hours, LOW risk 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
…rategy Added three critical enhancements for execution readiness: 1. Code Anchors (Step 1 & 2): - Complete implementation of getNpmIgnoreScripts() with edge cases - Exact injection point: src/auth/credential-store.ts:327 - Modification strategy for all platform cases (linux/darwin/win32) - Discovered getAvailability() already async - only getRemediation() needs change 2. Test Mocking Strategy (Step 4): - Extract keytar loading to src/auth/keytar-loader.ts for testability - Complete vitest mocking pattern with vi.mock() - 4 test cases: ignore-scripts true/false/null, successful load - Mock dynamic import() via extracted loader 3. Edge Case Handling (Step 1): - npm not in PATH (ENOENT) → null - npm timeout (2s max) → null - Windows env var variants (npm_config_* + NPM_CONFIG_*) - Unexpected output → null - All errors gracefully fallback to standard remediation Call Site Analysis: - Found 6 existing callers (all already use await) - No caller updates needed - getAvailability() already async - Only getRemediation() (private) needs async signature PRP now 100% execution-ready with clear implementation path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
Adds interactive command palette with autocomplete, keyboard navigation, fuzzy matching, and "did you mean" suggestions.
Problem: Typing
/in Grok TUI shows nothing - users must already know command names. No autocomplete, no command discovery, no typo assistance.Solution: Interactive command palette that appears when typing
/, with smart filtering and keyboard navigation.Features
1. Live Command Filtering
Type
/to see all commands, then filter as you type:2. Smart Matching
Prefix match:
Alias match:
Fuzzy match (typo tolerance):
3. Keyboard Navigation
4. "Did You Mean" for Unknown Commands
Implementation
Phase 1: Suggestion Algorithm (
src/ui/utils/command-suggestions.ts)Levenshtein Distance:
Matching Types (ranked):
/prompt→prompt/pr→prompt/pf→prompt/promptf→prompt(viapromptfile)/odel→model/promt→prompt(1 edit = 400 points)Phase 2: UI Component (
src/ui/components/command-palette.tsx)Phase 3: Input Integration (
src/ui/components/input.tsx)useMemo)value.startsWith('/')and not pastingPhase 4: Error Enhancement (
src/commands/index.ts)getDidYouMeanSuggestion()for best matchCompatibility
No conflicts with existing features:
isPastingisActive=false)Test Coverage
New Test File:
tests/unit/command-suggestions.test.ts(25 tests)Test Results: 238/238 pass (+25 from 213)
Verification
✅ npm run build - TypeScript compilation clean ✅ npm test - 238/238 tests pass (+25 new tests) ✅ No conflicts with paste/navigation modesManual Testing
After merge, verify in TUI:
/→ see all 7 commands/pr→ see/prompt/pf→ matches/prompt (via pf)/promt→ matches/prompt/prompt/promt→ error shows "Did you mean: /prompt?"🤖 Generated with Claude Code