FABRIC/src/tui/components/CommandPalette.ts
jeda 57e8193f7b feat(bd-2kf): Add comprehensive test coverage for parser and store
- Add 36 parser tests covering:
  - parseLogLine with valid/invalid inputs
  - parseLogLines for multi-line parsing
  - formatEvent with all options
  - Edge cases: malformed JSON, missing fields, colorization

- Add 35 store tests covering:
  - InMemoryEventStore add/query operations
  - Worker status tracking (active/idle/error)
  - Event filtering by worker, level, bead, timestamp
  - maxEvents limit and LRU trimming
  - getStore/resetStore singleton management

- Close phase beads (bd-2pa, bd-n8l, bd-2nu) as infrastructure complete
- Close test beads (bd-5eh, bd-2en) with comprehensive coverage
- Total: 91 tests passing across parser, store, and tailer

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-03 10:43:24 +00:00

263 lines
6.1 KiB
TypeScript

/**
* CommandPalette Component
*
* Universal search/command interface triggered by Ctrl+K.
*/
import * as blessed from 'blessed';
import { colors } from '../utils/colors.js';
export interface CommandPaletteOptions {
/** Parent screen */
parent: blessed.Widgets.Screen;
/** Callback when command is submitted */
onSubmit?: (command: string) => void;
/** Callback when search changes */
onSearch?: (query: string) => void;
}
export interface CommandSuggestion {
/** Display text */
label: string;
/** Category */
category: string;
/** Action to perform */
action: string;
}
/**
* Default command suggestions
*/
const DEFAULT_SUGGESTIONS: CommandSuggestion[] = [
{ label: 'Filter by worker', category: 'Filter', action: 'filter:worker:' },
{ label: 'Filter by level', category: 'Filter', action: 'filter:level:' },
{ label: 'Filter by bead', category: 'Filter', action: 'filter:bead:' },
{ label: 'Clear filters', category: 'Action', action: 'clear' },
{ label: 'Toggle pause', category: 'Action', action: 'pause' },
{ label: 'Refresh', category: 'Action', action: 'refresh' },
{ label: 'Help', category: 'Navigation', action: 'help' },
{ label: 'Quit', category: 'Navigation', action: 'quit' },
];
/**
* CommandPalette provides a searchable command interface
*/
export class CommandPalette {
private box: blessed.Widgets.BoxElement;
private input: blessed.Widgets.TextboxElement;
private suggestionBox: blessed.Widgets.ListElement;
private onSubmit?: (command: string) => void;
private onSearch?: (query: string) => void;
private suggestions: CommandSuggestion[];
private filteredSuggestions: CommandSuggestion[];
private selectedIndex = 0;
constructor(options: CommandPaletteOptions) {
this.onSubmit = options.onSubmit;
this.onSearch = options.onSearch;
this.suggestions = [...DEFAULT_SUGGESTIONS];
this.filteredSuggestions = [...this.suggestions];
// Container box
this.box = blessed.box({
parent: options.parent,
top: 'center',
left: 'center',
width: '60%',
height: 12,
hidden: true,
style: {
bg: 'black',
},
});
// Input textbox
this.input = blessed.textbox({
parent: this.box,
top: 0,
left: 0,
right: 0,
height: 3,
border: { type: 'line' },
style: {
border: { fg: colors.focus },
focus: {
border: { fg: colors.focus },
},
},
label: ' Command (Ctrl+K to close) ',
inputOnFocus: true,
});
// Suggestions list
this.suggestionBox = blessed.list({
parent: this.box,
top: 3,
left: 0,
right: 0,
bottom: 0,
border: { type: 'line' },
style: {
border: { fg: colors.border },
selected: {
bg: colors.focus,
fg: 'black',
},
},
keys: true,
vi: true,
mouse: true,
});
this.bindEvents();
this.renderSuggestions();
}
private bindEvents(): void {
// Input changes
this.input.on('keypress', (ch, key) => {
if (key.name === 'escape') {
this.hide();
return;
}
if (key.name === 'down') {
this.selectNext();
return;
}
if (key.name === 'up') {
this.selectPrevious();
return;
}
if (key.name === 'enter') {
this.executeSelected();
return;
}
// Filter suggestions based on input
const value = this.input.getValue();
this.filterSuggestions(value);
});
// Submit on enter
this.input.on('submit', (value) => {
if (this.onSubmit) {
this.onSubmit(value);
}
this.hide();
});
// Cancel on escape
this.input.key(['escape'], () => {
this.hide();
});
}
private filterSuggestions(query: string): void {
const q = query.toLowerCase();
this.filteredSuggestions = this.suggestions.filter(s =>
s.label.toLowerCase().includes(q) ||
s.category.toLowerCase().includes(q) ||
s.action.toLowerCase().includes(q)
);
this.selectedIndex = 0;
this.renderSuggestions();
}
private renderSuggestions(): void {
const items = this.filteredSuggestions.map((s, i) => {
const selected = i === this.selectedIndex ? '{green-fg}' : '';
const end = i === this.selectedIndex ? '{/}' : '';
return `${selected}${s.category}: ${s.label}${end}`;
});
this.suggestionBox.setItems(items);
this.suggestionBox.select(this.selectedIndex);
this.box.screen.render();
}
private selectNext(): void {
if (this.filteredSuggestions.length === 0) return;
this.selectedIndex = (this.selectedIndex + 1) % this.filteredSuggestions.length;
this.renderSuggestions();
}
private selectPrevious(): void {
if (this.filteredSuggestions.length === 0) return;
this.selectedIndex = this.selectedIndex === 0
? this.filteredSuggestions.length - 1
: this.selectedIndex - 1;
this.renderSuggestions();
}
private executeSelected(): void {
const selected = this.filteredSuggestions[this.selectedIndex];
if (selected && this.onSubmit) {
this.onSubmit(selected.action);
}
this.hide();
}
/**
* Show the command palette
*/
show(): void {
this.box.show();
this.input.setValue('');
this.filteredSuggestions = [...this.suggestions];
this.selectedIndex = 0;
this.renderSuggestions();
this.input.focus();
this.box.screen.render();
}
/**
* Hide the command palette
*/
hide(): void {
this.box.hide();
this.box.screen.render();
}
/**
* Toggle visibility
*/
toggle(): void {
if (this.box.hidden) {
this.show();
} else {
this.hide();
}
}
/**
* Check if visible
*/
isVisible(): boolean {
return !this.box.hidden;
}
/**
* Add custom suggestion
*/
addSuggestion(suggestion: CommandSuggestion): void {
this.suggestions.push(suggestion);
}
/**
* Clear custom suggestions
*/
clearSuggestions(): void {
this.suggestions = [...DEFAULT_SUGGESTIONS];
}
}
export function createCommandPalette(options: CommandPaletteOptions): CommandPalette {
return new CommandPalette(options);
}