Files
cameleer-server/ui/src/components/command-palette/CommandPalette.tsx
hsiegeln 86e016874a
All checks were successful
CI / build (push) Successful in 59s
CI / docker (push) Successful in 46s
CI / deploy (push) Successful in 25s
Fix command palette: agent ID propagation, result selection, and scope tabs
- 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>
2026-03-13 17:13:14 +01:00

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,
);
}