diff --git a/ui/src/components/command-palette/CommandPalette.tsx b/ui/src/components/command-palette/CommandPalette.tsx
index 5f4b6281..5680ba16 100644
--- a/ui/src/components/command-palette/CommandPalette.tsx
+++ b/ui/src/components/command-palette/CommandPalette.tsx
@@ -1,94 +1,11 @@
-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'];
+import { useEffect } from 'react';
+import { useCommandPalette } from './use-command-palette';
+/**
+ * Headless component: only registers the global Cmd+K / Ctrl+K keyboard shortcut.
+ * The palette UI itself is rendered inline within SearchFilters.
+ */
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.id);
- 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') {
@@ -106,28 +23,5 @@ export function CommandPalette() {
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(
-
{
- if (e.target === e.currentTarget) {
- close();
- reset();
- }
- }}>
-
-
,
- document.body,
- );
+ return null;
}
diff --git a/ui/src/pages/executions/ResultsTable.module.css b/ui/src/pages/executions/ResultsTable.module.css
index d15bdede..10ce542e 100644
--- a/ui/src/pages/executions/ResultsTable.module.css
+++ b/ui/src/pages/executions/ResultsTable.module.css
@@ -28,6 +28,35 @@
white-space: nowrap;
}
+.thSortable {
+ cursor: pointer;
+ transition: color 0.15s;
+}
+
+.thSortable:hover {
+ color: var(--text-secondary);
+}
+
+.thActive {
+ color: var(--amber);
+}
+
+.sortArrow {
+ display: inline-block;
+ margin-left: 4px;
+ font-size: 9px;
+ opacity: 0.3;
+ transition: opacity 0.15s;
+}
+
+.thSortable:hover .sortArrow {
+ opacity: 0.6;
+}
+
+.thActive .sortArrow {
+ opacity: 1;
+}
+
.row {
border-bottom: 1px solid var(--border-subtle);
transition: background 0.1s;
diff --git a/ui/src/pages/executions/ResultsTable.tsx b/ui/src/pages/executions/ResultsTable.tsx
index f764f939..acaa3ec3 100644
--- a/ui/src/pages/executions/ResultsTable.tsx
+++ b/ui/src/pages/executions/ResultsTable.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
import type { ExecutionSummary } from '../../api/schema';
import { StatusPill } from '../../components/shared/StatusPill';
import { DurationBar } from '../../components/shared/DurationBar';
@@ -12,6 +12,9 @@ interface ResultsTableProps {
loading: boolean;
}
+type SortColumn = 'startTime' | 'status' | 'agentId' | 'routeId' | 'correlationId' | 'durationMs';
+type SortDir = 'asc' | 'desc';
+
function formatTime(iso: string) {
return new Date(iso).toLocaleTimeString('en-GB', {
hour: '2-digit',
@@ -21,8 +24,74 @@ function formatTime(iso: string) {
});
}
+function compareFn(a: ExecutionSummary, b: ExecutionSummary, col: SortColumn, dir: SortDir): number {
+ let cmp = 0;
+ switch (col) {
+ case 'startTime':
+ cmp = a.startTime.localeCompare(b.startTime);
+ break;
+ case 'status':
+ cmp = a.status.localeCompare(b.status);
+ break;
+ case 'agentId':
+ cmp = a.agentId.localeCompare(b.agentId);
+ break;
+ case 'routeId':
+ cmp = a.routeId.localeCompare(b.routeId);
+ break;
+ case 'correlationId':
+ cmp = (a.correlationId ?? '').localeCompare(b.correlationId ?? '');
+ break;
+ case 'durationMs':
+ cmp = a.durationMs - b.durationMs;
+ break;
+ }
+ return dir === 'asc' ? cmp : -cmp;
+}
+
+interface SortableThProps {
+ label: string;
+ column: SortColumn;
+ activeColumn: SortColumn | null;
+ direction: SortDir;
+ onSort: (col: SortColumn) => void;
+ style?: React.CSSProperties;
+}
+
+function SortableTh({ label, column, activeColumn, direction, onSort, style }: SortableThProps) {
+ const isActive = activeColumn === column;
+ return (
+ onSort(column)}
+ >
+ {label}
+
+ {isActive ? (direction === 'asc' ? '\u25B2' : '\u25BC') : '\u25B4'}
+
+ |
+ );
+}
+
export function ResultsTable({ results, loading }: ResultsTableProps) {
const [expandedId, setExpandedId] = useState(null);
+ const [sortColumn, setSortColumn] = useState(null);
+ const [sortDir, setSortDir] = useState('desc');
+
+ const sortedResults = useMemo(() => {
+ if (!sortColumn) return results;
+ return [...results].sort((a, b) => compareFn(a, b, sortColumn, sortDir));
+ }, [results, sortColumn, sortDir]);
+
+ function handleSort(col: SortColumn) {
+ if (sortColumn === col) {
+ setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
+ } else {
+ setSortColumn(col);
+ setSortDir('desc');
+ }
+ }
if (loading && results.length === 0) {
return (
@@ -46,16 +115,16 @@ export function ResultsTable({ results, loading }: ResultsTableProps) {
|
- Timestamp |
- Status |
- Application |
- Route |
- Correlation ID |
- Duration |
+
+
+
+
+
+
- {results.map((exec) => {
+ {sortedResults.map((exec) => {
const isExpanded = expandedId === exec.executionId;
return (
s.open);
+ const { results, executionCount, agentCount, isLoading } = usePaletteSearch();
+ const dropdownRef = useRef(null);
+
+ 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.id);
+ execSearch.setText('');
+ execSearch.setRouteId('');
+ execSearch.setProcessorType('');
+ }
+ 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],
+ );
+
+ // Close on click outside
+ useEffect(() => {
+ if (!isOpen) return;
+ function onClickOutside(e: MouseEvent) {
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
+ close();
+ reset();
+ }
+ }
+ document.addEventListener('mousedown', onClickOutside);
+ return () => document.removeEventListener('mousedown', onClickOutside);
+ }, [isOpen, close, reset]);
+
+ // Keyboard handling when open
+ useEffect(() => {
+ if (!isOpen) return;
+ const SCOPES = ['all', 'executions', 'agents'] as const;
+ function handleKeyDown(e: KeyboardEvent) {
+ 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 as typeof SCOPES[number]);
+ setScope(SCOPES[(idx + 1) % SCOPES.length]);
+ break;
+ }
+ }
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [isOpen, close, reset, selectedIndex, setSelectedIndex, results, handleSelect, scope, setScope]);
const activeTags: { label: string; onRemove: () => void }[] = [];
if (text) activeTags.push({ label: `text:"${text}"`, onRemove: () => setText('') });
@@ -31,19 +121,30 @@ export function SearchFilters() {
return (
- {/* Row 1: Search trigger (opens command palette) */}
+ {/* Row 1: Search bar with inline palette */}
-
{ if (e.key === 'Enter' || e.key === ' ') openPalette(); }}>
-
-
- {text || routeId || agentId || processorType
- ? [text, routeId && `route:${routeId}`, agentId && `agent:${agentId}`, processorType && `processor:${processorType}`].filter(Boolean).join(' ')
- : 'Search by correlation ID, error message, route ID...'}
-
-
⌘K
+
+ {isOpen ? (
+
+ ) : (
+
{ if (e.key === 'Enter' || e.key === ' ') openPalette(); }}>
+
+
+ {text || routeId || agentId || processorType
+ ? [text, routeId && `route:${routeId}`, agentId && `agent:${agentId}`, processorType && `processor:${processorType}`].filter(Boolean).join(' ')
+ : 'Search by correlation ID, error message, route ID...'}
+
+
⌘K
+
+ )}
diff --git a/ui/src/pages/executions/use-execution-search.ts b/ui/src/pages/executions/use-execution-search.ts
index 61567ac2..67c8fdf6 100644
--- a/ui/src/pages/executions/use-execution-search.ts
+++ b/ui/src/pages/executions/use-execution-search.ts
@@ -1,6 +1,14 @@
import { create } from 'zustand';
import type { SearchRequest } from '../../api/schema';
+function todayMidnight(): string {
+ const d = new Date();
+ d.setHours(0, 0, 0, 0);
+ // Format as datetime-local value: YYYY-MM-DDTHH:mm
+ const pad = (n: number) => n.toString().padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00`;
+}
+
interface ExecutionSearchState {
status: string[];
timeFrom: string;
@@ -31,7 +39,7 @@ interface ExecutionSearchState {
export const useExecutionSearch = create
((set, get) => ({
status: ['COMPLETED', 'FAILED'],
- timeFrom: '',
+ timeFrom: todayMidnight(),
timeTo: '',
durationMin: null,
durationMax: null,
@@ -62,7 +70,7 @@ export const useExecutionSearch = create((set, get) => ({
clearAll: () =>
set({
status: ['COMPLETED', 'FAILED', 'RUNNING'],
- timeFrom: '',
+ timeFrom: todayMidnight(),
timeTo: '',
durationMin: null,
durationMax: null,