feat(tui): Command palette fuzzy-find any action with Ctrl+P or : prefix#438
feat(tui): Command palette fuzzy-find any action with Ctrl+P or : prefix#438
Conversation
|
Bosun CI signal: Bosun-created PR currently has failing checks.
|
There was a problem hiding this comment.
Pull request overview
Adds a TUI command palette that can fuzzy-find and execute actions, intended to open via Ctrl+P or : and remember recent commands across runs.
Changes:
- Introduces command-palette helpers to build/rank actions and persist a small recent-history file.
- Wires a new Ink
CommandPalettecomponent into the TUI app, including workflow template fetching and action execution. - Adds unit + integration test coverage for palette ranking, history, and basic execution.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/tui/fuzzy.js | Adds a small re-export shim for fuzzy scoring helpers. |
| tui/lib/command-palette.mjs | New helpers for palette actions, fuzzy ranking, and persisted history. |
| tui/app.mjs | Integrates palette open/close, action execution, workflow fetching, and recent-history wiring. |
| tui/CommandPalette.js | New Ink UI component for querying and selecting actions. |
| tests/tui/command-palette.test.mjs | Unit tests for ranking and history helpers. |
| tests/tui/command-palette.integration.test.mjs | Ink integration test for opening palette and executing an action. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const handleInput = useCallback((input, key) => { | ||
| if (key?.ctrl && String(input || "") === "p") { | ||
| setPaletteOpen(true); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Ctrl+P detection is checking input === "p", but Ink typically reports Ctrl key combos as a control character in input (e.g. Ctrl+P is \u0010) with the letter in key.name. This will likely prevent the palette from opening in real terminals. Prefer checking key.ctrl && key.name === "p" (or matching the control sequence) instead of comparing input to "p".
| if (screenInputLocked) { | ||
| if (key?.ctrl && String(input || "") === "p") { | ||
| setPaletteOpen(true); | ||
| } | ||
| return; |
There was a problem hiding this comment.
Same Ctrl+P handling issue as above inside the screenInputLocked branch: comparing input to "p" may not match Ctrl+P. Use key.name / key.sequence so the palette can still open while inputs are captured by a screen.
| async function fetchJson(url, init = {}) { | ||
| const response = await fetch(url, init); | ||
| const payload = await response.json().catch(() => ({})); | ||
| if (!response.ok || payload?.ok === false) { | ||
| throw new Error(payload?.error || `Request failed: ${response.status}`); | ||
| } | ||
| return payload; | ||
| } |
There was a problem hiding this comment.
fetchJson duplicates logic that already exists in TuiWsBridge.requestJson() (including https/ws protocol mapping and Bearer token injection). Using hard-coded http://${host}:${port} here can break when the UI server is https/wss or requires the token header. Consider routing these requests through bridge.requestJson() (with a fallback for injected mock clients) instead of direct fetch().
| } else if (action.type === "config") { | ||
| if (action.id.startsWith("config:refresh:")) { | ||
| setRefreshCountdownSec(Number(action.id.split(":").at(-1) || 2)); | ||
| } |
There was a problem hiding this comment.
Config palette actions currently only call setRefreshCountdownSec(...), which changes the displayed countdown but doesn’t update the underlying refreshMs value used by resetRefreshCountdown() (so it can revert on the next stats tick). Either make refresh interval configurable via state and use it consistently, or remove these config actions until they have an effect.
| } else if (action.type === "config") { | |
| if (action.id.startsWith("config:refresh:")) { | |
| setRefreshCountdownSec(Number(action.id.split(":").at(-1) || 2)); | |
| } |
| @@ -0,0 +1 @@ | |||
| export { scoreFuzzyMatch, rankFuzzyMatches } from "../../tui/lib/fuzzy-score.mjs"; | |||
There was a problem hiding this comment.
This file re-exports fuzzy helpers but isn’t referenced anywhere in the repo (no imports found). If it’s not needed, consider removing it; if it is intended as a shared entrypoint, add the corresponding import(s) or documentation so it’s clear why it exists.
| for (const task of Array.isArray(tasks) ? tasks : []) { | ||
| const taskId = String(task?.id || "").trim(); | ||
| if (!taskId) continue; | ||
| for (const action of TASK_ACTIONS) { | ||
| actions.push(actionWithSearch({ | ||
| id: `task:${action.key}:${taskId}`, | ||
| type: "task", | ||
| command: action.key, | ||
| icon: action.icon, | ||
| label: action.key === "create" ? `${action.label}` : `${action.label} ${task.title || taskId}`, | ||
| shortcut: action.shortcut, | ||
| context: action.key === "create" ? "Tasks" : `Task ${taskId}`, | ||
| payload: { taskId, task }, | ||
| keywords: [task.title, task.status, taskId], | ||
| })); | ||
| } |
There was a problem hiding this comment.
TASK_ACTIONS includes create, and the per-task loop generates a "Create task" action for every existing task (task:create:<taskId>). This produces many duplicate create entries (plus the separate task:create:new action) and can crowd out the result list. Consider removing create from the per-task loop (handle create as a single global action) so only update/delete are task-scoped.
| { key: "config:connectOnly:on", label: "Enable connect-only mode", icon: "⚙", context: "Config" }, | ||
| { key: "config:connectOnly:off", label: "Disable connect-only mode", icon: "⚙", context: "Config" }, |
There was a problem hiding this comment.
CONFIG_ACTIONS defines connect-only toggles, but executePaletteAction in tui/app.mjs doesn’t implement any behavior for config:connectOnly:on/off. Either implement these actions end-to-end or remove them from the palette so users don’t select a no-op command.
| { key: "config:connectOnly:on", label: "Enable connect-only mode", icon: "⚙", context: "Config" }, | |
| { key: "config:connectOnly:off", label: "Disable connect-only mode", icon: "⚙", context: "Config" }, |
8ef73c2 to
63dae12
Compare
|
Bosun PR classification: Bosun-created.
|
…uzzy-find-any-action-w
…uzzy-find-any-action-w
Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>
Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>
Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>
Task-ID: 2316d321-5b65-4768-be5d-952e1c5cdec8\n\nAutomated PR for task 2316d321-5b65-4768-be5d-952e1c5cdec8