- Propagate authenticated agent identity through write buffers via TaggedExecution/TaggedDiagram wrappers so ClickHouse rows get real agent IDs instead of empty strings - Add execution_id to text search LIKE clause so selecting an execution by ID in the palette actually finds it - Clear status filter to all three statuses on palette selection so the chosen execution/agent isn't filtered out - Add disabled Routes and Exchanges scope tabs with "coming soon" state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
4.4 KiB
TypeScript
134 lines
4.4 KiB
TypeScript
import { useEffect, useCallback } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useCommandPalette, type PaletteScope } from './use-command-palette';
|
|
import { usePaletteSearch, type PaletteResult } from './use-palette-search';
|
|
import { useExecutionSearch } from '../../pages/executions/use-execution-search';
|
|
import { PaletteInput } from './PaletteInput';
|
|
import { ScopeTabs } from './ScopeTabs';
|
|
import { ResultsList } from './ResultsList';
|
|
import { PaletteFooter } from './PaletteFooter';
|
|
import type { ExecutionSummary, AgentInstance } from '../../api/schema';
|
|
import styles from './CommandPalette.module.css';
|
|
|
|
const SCOPES: PaletteScope[] = ['all', 'executions', 'agents'];
|
|
|
|
export function CommandPalette() {
|
|
const { isOpen, close, scope, setScope, selectedIndex, setSelectedIndex, reset, filters } =
|
|
useCommandPalette();
|
|
const { results, executionCount, agentCount, isLoading } = usePaletteSearch();
|
|
const execSearch = useExecutionSearch();
|
|
|
|
const handleSelect = useCallback(
|
|
(result: PaletteResult) => {
|
|
if (result.type === 'execution') {
|
|
const exec = result.data as ExecutionSummary;
|
|
execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']);
|
|
execSearch.setText(exec.executionId);
|
|
execSearch.setRouteId('');
|
|
execSearch.setAgentId('');
|
|
execSearch.setProcessorType('');
|
|
} else if (result.type === 'agent') {
|
|
const agent = result.data as AgentInstance;
|
|
execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']);
|
|
execSearch.setAgentId(agent.agentId);
|
|
execSearch.setText('');
|
|
execSearch.setRouteId('');
|
|
execSearch.setProcessorType('');
|
|
}
|
|
// Apply any active palette filters to the execution search
|
|
for (const f of filters) {
|
|
if (f.key === 'status') execSearch.setStatus([f.value.toUpperCase()]);
|
|
if (f.key === 'route') execSearch.setRouteId(f.value);
|
|
if (f.key === 'agent') execSearch.setAgentId(f.value);
|
|
if (f.key === 'processor') execSearch.setProcessorType(f.value);
|
|
}
|
|
close();
|
|
reset();
|
|
},
|
|
[close, reset, execSearch, filters],
|
|
);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent) => {
|
|
if (!isOpen) return;
|
|
|
|
switch (e.key) {
|
|
case 'Escape':
|
|
e.preventDefault();
|
|
close();
|
|
reset();
|
|
break;
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
setSelectedIndex(
|
|
results.length > 0 ? (selectedIndex + 1) % results.length : 0,
|
|
);
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
setSelectedIndex(
|
|
results.length > 0
|
|
? (selectedIndex - 1 + results.length) % results.length
|
|
: 0,
|
|
);
|
|
break;
|
|
case 'Enter':
|
|
e.preventDefault();
|
|
if (results[selectedIndex]) {
|
|
handleSelect(results[selectedIndex]);
|
|
}
|
|
break;
|
|
case 'Tab':
|
|
e.preventDefault();
|
|
const idx = SCOPES.indexOf(scope);
|
|
setScope(SCOPES[(idx + 1) % SCOPES.length]);
|
|
break;
|
|
}
|
|
},
|
|
[isOpen, close, reset, selectedIndex, setSelectedIndex, results, handleSelect, scope, setScope],
|
|
);
|
|
|
|
// Global Cmd+K / Ctrl+K listener
|
|
useEffect(() => {
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
const store = useCommandPalette.getState();
|
|
if (store.isOpen) {
|
|
store.close();
|
|
store.reset();
|
|
} else {
|
|
store.open();
|
|
}
|
|
}
|
|
}
|
|
document.addEventListener('keydown', onKeyDown);
|
|
return () => document.removeEventListener('keydown', onKeyDown);
|
|
}, []);
|
|
|
|
// Keyboard handling when open
|
|
useEffect(() => {
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [handleKeyDown]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return createPortal(
|
|
<div className={styles.overlay} onClick={(e) => {
|
|
if (e.target === e.currentTarget) {
|
|
close();
|
|
reset();
|
|
}
|
|
}}>
|
|
<div className={styles.modal}>
|
|
<PaletteInput />
|
|
<ScopeTabs executionCount={executionCount} agentCount={agentCount} />
|
|
<ResultsList results={results} isLoading={isLoading} onSelect={handleSelect} />
|
|
<PaletteFooter />
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|