Rename Agents to Applications, remove Exchanges, implement Routes search
All checks were successful
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 46s
CI / deploy (push) Successful in 29s

- Rename "Agents" scope/labels to "Applications" throughout command palette
- Remove "Exchanges" scope (was disabled placeholder)
- Implement "Routes" scope: derives routes from agents' routeIds, filterable
  by route ID or owning application name
- Selecting a route filters executions by routeId
- Route results show purple icon, route ID, and owning application(s)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-13 18:13:09 +01:00
parent c3cfb39f81
commit cccd3f07be
7 changed files with 141 additions and 64 deletions

View File

@@ -279,6 +279,12 @@
color: var(--rose); color: var(--rose);
} }
.iconRoute {
composes: resultIcon;
background: rgba(168, 85, 247, 0.12);
color: var(--purple);
}
/* ── Result Body ── */ /* ── Result Body ── */
.resultBody { .resultBody {
flex: 1; flex: 1;

View File

@@ -1,5 +1,5 @@
import type { ExecutionSummary, AgentInstance } from '../../api/schema'; import type { ExecutionSummary, AgentInstance } from '../../api/schema';
import type { PaletteResult } from './use-palette-search'; import type { PaletteResult, RouteInfo } from './use-palette-search';
import { highlightMatch, formatRelativeTime } from './utils'; import { highlightMatch, formatRelativeTime } from './utils';
import styles from './CommandPalette.module.css'; import styles from './CommandPalette.module.css';
@@ -80,7 +80,7 @@ function ExecutionResult({ data, query }: { data: ExecutionSummary; query: strin
); );
} }
function AgentResult({ data, query }: { data: AgentInstance; query: string }) { function ApplicationResult({ data, query }: { data: AgentInstance; query: string }) {
return ( return (
<> <>
<div className={styles.iconAgent}> <div className={styles.iconAgent}>
@@ -101,7 +101,34 @@ function AgentResult({ data, query }: { data: AgentInstance; query: string }) {
</div> </div>
</div> </div>
<div className={styles.resultRight}> <div className={styles.resultRight}>
<span className={styles.resultTime}>Agent</span> <span className={styles.resultTime}>Application</span>
</div>
</>
);
}
function RouteResult({ data, query }: { data: RouteInfo; query: string }) {
return (
<>
<div className={styles.iconRoute}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="6" cy="19" r="3" />
<path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15" />
<circle cx="18" cy="5" r="3" />
</svg>
</div>
<div className={styles.resultBody}>
<div className={styles.resultTitle}>
<HighlightedText text={data.routeId} query={query} />
</div>
<div className={styles.resultMeta}>
<span>{data.agentIds.length} {data.agentIds.length === 1 ? 'application' : 'applications'}</span>
<span className={styles.sep} />
<span>{data.agentIds.join(', ')}</span>
</div>
</div>
<div className={styles.resultRight}>
<span className={styles.resultTime}>Route</span>
</div> </div>
</> </>
); );
@@ -117,8 +144,11 @@ export function ResultItem({ result, selected, query, onClick }: ResultItemProps
{result.type === 'execution' && ( {result.type === 'execution' && (
<ExecutionResult data={result.data as ExecutionSummary} query={query} /> <ExecutionResult data={result.data as ExecutionSummary} query={query} />
)} )}
{result.type === 'agent' && ( {result.type === 'application' && (
<AgentResult data={result.data as AgentInstance} query={query} /> <ApplicationResult data={result.data as AgentInstance} query={query} />
)}
{result.type === 'route' && (
<RouteResult data={result.data as RouteInfo} query={query} />
)} )}
</div> </div>
); );

View File

@@ -11,34 +11,14 @@ interface ResultsListProps {
} }
export function ResultsList({ results, isLoading, onSelect }: ResultsListProps) { export function ResultsList({ results, isLoading, onSelect }: ResultsListProps) {
const { selectedIndex, query, scope } = useCommandPalette(); const { selectedIndex, query } = useCommandPalette();
const listRef = useRef<HTMLDivElement>(null); const listRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
const el = listRef.current?.querySelector('[data-palette-item].selected, [data-palette-item]:nth-child(' + (selectedIndex + 1) + ')');
if (!el) return;
const items = listRef.current?.querySelectorAll('[data-palette-item]'); const items = listRef.current?.querySelectorAll('[data-palette-item]');
items?.[selectedIndex]?.scrollIntoView({ block: 'nearest' }); items?.[selectedIndex]?.scrollIntoView({ block: 'nearest' });
}, [selectedIndex]); }, [selectedIndex]);
if (scope === 'routes' || scope === 'exchanges') {
const label = scope === 'routes' ? 'Route' : 'Exchange';
return (
<div className={styles.results}>
<div className={styles.emptyState}>
<svg className={styles.emptyIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M12 6v6l4 2" />
<circle cx="12" cy="12" r="10" />
</svg>
<span className={styles.emptyText}>{label} search coming soon</span>
<span className={styles.emptyHint}>
This feature is planned for a future release
</span>
</div>
</div>
);
}
if (isLoading && results.length === 0) { if (isLoading && results.length === 0) {
return ( return (
<div className={styles.results}> <div className={styles.results}>
@@ -70,7 +50,8 @@ export function ResultsList({ results, isLoading, onSelect }: ResultsListProps)
// Group results by type // Group results by type
const executions = results.filter((r) => r.type === 'execution'); const executions = results.filter((r) => r.type === 'execution');
const agents = results.filter((r) => r.type === 'agent'); const applications = results.filter((r) => r.type === 'application');
const routes = results.filter((r) => r.type === 'route');
let globalIndex = 0; let globalIndex = 0;
@@ -93,10 +74,27 @@ export function ResultsList({ results, isLoading, onSelect }: ResultsListProps)
})} })}
</> </>
)} )}
{agents.length > 0 && ( {applications.length > 0 && (
<> <>
<div className={styles.groupLabel}>Agents</div> <div className={styles.groupLabel}>Applications</div>
{agents.map((r) => { {applications.map((r) => {
const idx = globalIndex++;
return (
<ResultItem
key={r.id}
result={r}
selected={idx === selectedIndex}
query={query}
onClick={() => onSelect(r)}
/>
);
})}
</>
)}
{routes.length > 0 && (
<>
<div className={styles.groupLabel}>Routes</div>
{routes.map((r) => {
const idx = globalIndex++; const idx = globalIndex++;
return ( return (
<ResultItem <ResultItem

View File

@@ -3,25 +3,26 @@ import styles from './CommandPalette.module.css';
interface ScopeTabsProps { interface ScopeTabsProps {
executionCount: number; executionCount: number;
agentCount: number; applicationCount: number;
routeCount: number;
} }
const SCOPES: { key: PaletteScope; label: string; disabled?: boolean }[] = [ const SCOPES: { key: PaletteScope; label: string }[] = [
{ key: 'all', label: 'All' }, { key: 'all', label: 'All' },
{ key: 'executions', label: 'Executions' }, { key: 'executions', label: 'Executions' },
{ key: 'agents', label: 'Agents' }, { key: 'applications', label: 'Applications' },
{ key: 'routes', label: 'Routes', disabled: true }, { key: 'routes', label: 'Routes' },
{ key: 'exchanges', label: 'Exchanges', disabled: true },
]; ];
export function ScopeTabs({ executionCount, agentCount }: ScopeTabsProps) { export function ScopeTabs({ executionCount, applicationCount, routeCount }: ScopeTabsProps) {
const { scope, setScope } = useCommandPalette(); const { scope, setScope } = useCommandPalette();
function getCount(key: PaletteScope): string | number { function getCount(key: PaletteScope): number {
if (key === 'all') return executionCount + agentCount; if (key === 'all') return executionCount + applicationCount + routeCount;
if (key === 'executions') return executionCount; if (key === 'executions') return executionCount;
if (key === 'agents') return agentCount; if (key === 'applications') return applicationCount;
return '\u2014'; if (key === 'routes') return routeCount;
return 0;
} }
return ( return (
@@ -29,14 +30,8 @@ export function ScopeTabs({ executionCount, agentCount }: ScopeTabsProps) {
{SCOPES.map((s) => ( {SCOPES.map((s) => (
<button <button
key={s.key} key={s.key}
className={ className={scope === s.key ? styles.scopeTabActive : styles.scopeTab}
s.disabled onClick={() => setScope(s.key)}
? styles.scopeTabDisabled
: scope === s.key
? styles.scopeTabActive
: styles.scopeTab
}
onClick={() => !s.disabled && setScope(s.key)}
> >
{s.label} {s.label}
<span className={styles.scopeCount}>{getCount(s.key)}</span> <span className={styles.scopeCount}>{getCount(s.key)}</span>

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand'; import { create } from 'zustand';
export type PaletteScope = 'all' | 'executions' | 'agents' | 'routes' | 'exchanges'; export type PaletteScope = 'all' | 'executions' | 'applications' | 'routes';
export interface PaletteFilter { export interface PaletteFilter {
key: 'status' | 'route' | 'agent' | 'processor'; key: 'status' | 'route' | 'agent' | 'processor';

View File

@@ -4,18 +4,27 @@ import type { ExecutionSummary, AgentInstance } from '../../api/schema';
import { useCommandPalette, type PaletteScope } from './use-command-palette'; import { useCommandPalette, type PaletteScope } from './use-command-palette';
import { useDebouncedValue } from './utils'; import { useDebouncedValue } from './utils';
export interface RouteInfo {
routeId: string;
agentIds: string[];
}
export interface PaletteResult { export interface PaletteResult {
type: 'execution' | 'agent'; type: 'execution' | 'application' | 'route';
id: string; id: string;
data: ExecutionSummary | AgentInstance; data: ExecutionSummary | AgentInstance | RouteInfo;
} }
function isExecutionScope(scope: PaletteScope) { function isExecutionScope(scope: PaletteScope) {
return scope === 'all' || scope === 'executions'; return scope === 'all' || scope === 'executions';
} }
function isAgentScope(scope: PaletteScope) { function isApplicationScope(scope: PaletteScope) {
return scope === 'all' || scope === 'agents'; return scope === 'all' || scope === 'applications';
}
function isRouteScope(scope: PaletteScope) {
return scope === 'all' || scope === 'routes';
} }
export function usePaletteSearch() { export function usePaletteSearch() {
@@ -57,7 +66,7 @@ export function usePaletteSearch() {
if (error) throw new Error('Failed to load agents'); if (error) throw new Error('Failed to load agents');
return data!; return data!;
}, },
enabled: isOpen && isAgentScope(scope), enabled: isOpen && (isApplicationScope(scope) || isRouteScope(scope)),
staleTime: 30_000, staleTime: 30_000,
}); });
@@ -73,21 +82,53 @@ export function usePaletteSearch() {
return a.id.toLowerCase().includes(q) || a.group.toLowerCase().includes(q); return a.id.toLowerCase().includes(q) || a.group.toLowerCase().includes(q);
}); });
const agentResults: PaletteResult[] = filteredAgents.slice(0, 10).map((a) => ({ const applicationResults: PaletteResult[] = filteredAgents.slice(0, 10).map((a) => ({
type: 'agent' as const, type: 'application' as const,
id: a.id, id: a.id,
data: a, data: a,
})); }));
// Derive unique routes from all agents
const routeMap = new Map<string, string[]>();
for (const agent of agentsQuery.data ?? []) {
for (const routeId of agent.routeIds ?? []) {
const existing = routeMap.get(routeId);
if (existing) {
if (!existing.includes(agent.id)) existing.push(agent.id);
} else {
routeMap.set(routeId, [agent.id]);
}
}
}
const allRoutes: RouteInfo[] = Array.from(routeMap.entries()).map(([routeId, agentIds]) => ({
routeId,
agentIds,
}));
const filteredRoutes = allRoutes.filter((r) => {
if (!debouncedQuery) return true;
const q = debouncedQuery.toLowerCase();
return r.routeId.toLowerCase().includes(q) || r.agentIds.some((a) => a.toLowerCase().includes(q));
});
const routeResults: PaletteResult[] = filteredRoutes.slice(0, 10).map((r) => ({
type: 'route' as const,
id: r.routeId,
data: r,
}));
let results: PaletteResult[] = []; let results: PaletteResult[] = [];
if (scope === 'all') results = [...executionResults, ...agentResults]; if (scope === 'all') results = [...executionResults, ...applicationResults, ...routeResults];
else if (scope === 'executions') results = executionResults; else if (scope === 'executions') results = executionResults;
else if (scope === 'agents') results = agentResults; else if (scope === 'applications') results = applicationResults;
else if (scope === 'routes') results = routeResults;
return { return {
results, results,
executionCount: executionsQuery.data?.total ?? 0, executionCount: executionsQuery.data?.total ?? 0,
agentCount: filteredAgents.length, applicationCount: filteredAgents.length,
routeCount: filteredRoutes.length,
isLoading: executionsQuery.isFetching || agentsQuery.isFetching, isLoading: executionsQuery.isFetching || agentsQuery.isFetching,
}; };
} }

View File

@@ -1,7 +1,7 @@
import { useRef, useEffect, useCallback } from 'react'; import { useRef, useEffect, useCallback } from 'react';
import { useExecutionSearch } from './use-execution-search'; import { useExecutionSearch } from './use-execution-search';
import { useCommandPalette } from '../../components/command-palette/use-command-palette'; import { useCommandPalette } from '../../components/command-palette/use-command-palette';
import { usePaletteSearch, type PaletteResult } from '../../components/command-palette/use-palette-search'; import { usePaletteSearch, type PaletteResult, type RouteInfo } from '../../components/command-palette/use-palette-search';
import { PaletteInput } from '../../components/command-palette/PaletteInput'; import { PaletteInput } from '../../components/command-palette/PaletteInput';
import { ScopeTabs } from '../../components/command-palette/ScopeTabs'; import { ScopeTabs } from '../../components/command-palette/ScopeTabs';
import { ResultsList } from '../../components/command-palette/ResultsList'; import { ResultsList } from '../../components/command-palette/ResultsList';
@@ -27,7 +27,7 @@ export function SearchFilters() {
const { isOpen, close, scope, setScope, selectedIndex, setSelectedIndex, reset, filters } = const { isOpen, close, scope, setScope, selectedIndex, setSelectedIndex, reset, filters } =
useCommandPalette(); useCommandPalette();
const openPalette = useCommandPalette((s) => s.open); const openPalette = useCommandPalette((s) => s.open);
const { results, executionCount, agentCount, isLoading } = usePaletteSearch(); const { results, executionCount, applicationCount, routeCount, isLoading } = usePaletteSearch();
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const handleSelect = useCallback( const handleSelect = useCallback(
@@ -39,13 +39,20 @@ export function SearchFilters() {
execSearch.setRouteId(''); execSearch.setRouteId('');
execSearch.setAgentId(''); execSearch.setAgentId('');
execSearch.setProcessorType(''); execSearch.setProcessorType('');
} else if (result.type === 'agent') { } else if (result.type === 'application') {
const agent = result.data as AgentInstance; const agent = result.data as AgentInstance;
execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']); execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']);
execSearch.setAgentId(agent.id); execSearch.setAgentId(agent.id);
execSearch.setText(''); execSearch.setText('');
execSearch.setRouteId(''); execSearch.setRouteId('');
execSearch.setProcessorType(''); execSearch.setProcessorType('');
} else if (result.type === 'route') {
const route = result.data as RouteInfo;
execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']);
execSearch.setRouteId(route.routeId);
execSearch.setText('');
execSearch.setAgentId('');
execSearch.setProcessorType('');
} }
for (const f of filters) { for (const f of filters) {
if (f.key === 'status') execSearch.setStatus([f.value.toUpperCase()]); if (f.key === 'status') execSearch.setStatus([f.value.toUpperCase()]);
@@ -75,7 +82,7 @@ export function SearchFilters() {
// Keyboard handling when open // Keyboard handling when open
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
const SCOPES = ['all', 'executions', 'agents'] as const; const SCOPES = ['all', 'executions', 'applications', 'routes'] as const;
function handleKeyDown(e: KeyboardEvent) { function handleKeyDown(e: KeyboardEvent) {
switch (e.key) { switch (e.key) {
case 'Escape': case 'Escape':
@@ -127,7 +134,7 @@ export function SearchFilters() {
{isOpen ? ( {isOpen ? (
<div className={styles.paletteInline}> <div className={styles.paletteInline}>
<PaletteInput /> <PaletteInput />
<ScopeTabs executionCount={executionCount} agentCount={agentCount} /> <ScopeTabs executionCount={executionCount} applicationCount={applicationCount} routeCount={routeCount} />
<ResultsList results={results} isLoading={isLoading} onSelect={handleSelect} /> <ResultsList results={results} isLoading={isLoading} onSelect={handleSelect} />
<PaletteFooter /> <PaletteFooter />
</div> </div>