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);
}
.iconRoute {
composes: resultIcon;
background: rgba(168, 85, 247, 0.12);
color: var(--purple);
}
/* ── Result Body ── */
.resultBody {
flex: 1;

View File

@@ -1,5 +1,5 @@
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 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 (
<>
<div className={styles.iconAgent}>
@@ -101,7 +101,34 @@ function AgentResult({ data, query }: { data: AgentInstance; query: string }) {
</div>
</div>
<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>
</>
);
@@ -117,8 +144,11 @@ export function ResultItem({ result, selected, query, onClick }: ResultItemProps
{result.type === 'execution' && (
<ExecutionResult data={result.data as ExecutionSummary} query={query} />
)}
{result.type === 'agent' && (
<AgentResult data={result.data as AgentInstance} query={query} />
{result.type === 'application' && (
<ApplicationResult data={result.data as AgentInstance} query={query} />
)}
{result.type === 'route' && (
<RouteResult data={result.data as RouteInfo} query={query} />
)}
</div>
);

View File

@@ -11,34 +11,14 @@ interface ResultsListProps {
}
export function ResultsList({ results, isLoading, onSelect }: ResultsListProps) {
const { selectedIndex, query, scope } = useCommandPalette();
const { selectedIndex, query } = useCommandPalette();
const listRef = useRef<HTMLDivElement>(null);
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]');
items?.[selectedIndex]?.scrollIntoView({ block: 'nearest' });
}, [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) {
return (
<div className={styles.results}>
@@ -70,7 +50,8 @@ export function ResultsList({ results, isLoading, onSelect }: ResultsListProps)
// Group results by type
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;
@@ -93,10 +74,27 @@ export function ResultsList({ results, isLoading, onSelect }: ResultsListProps)
})}
</>
)}
{agents.length > 0 && (
{applications.length > 0 && (
<>
<div className={styles.groupLabel}>Agents</div>
{agents.map((r) => {
<div className={styles.groupLabel}>Applications</div>
{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++;
return (
<ResultItem

View File

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

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand';
export type PaletteScope = 'all' | 'executions' | 'agents' | 'routes' | 'exchanges';
export type PaletteScope = 'all' | 'executions' | 'applications' | 'routes';
export interface PaletteFilter {
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 { useDebouncedValue } from './utils';
export interface RouteInfo {
routeId: string;
agentIds: string[];
}
export interface PaletteResult {
type: 'execution' | 'agent';
type: 'execution' | 'application' | 'route';
id: string;
data: ExecutionSummary | AgentInstance;
data: ExecutionSummary | AgentInstance | RouteInfo;
}
function isExecutionScope(scope: PaletteScope) {
return scope === 'all' || scope === 'executions';
}
function isAgentScope(scope: PaletteScope) {
return scope === 'all' || scope === 'agents';
function isApplicationScope(scope: PaletteScope) {
return scope === 'all' || scope === 'applications';
}
function isRouteScope(scope: PaletteScope) {
return scope === 'all' || scope === 'routes';
}
export function usePaletteSearch() {
@@ -57,7 +66,7 @@ export function usePaletteSearch() {
if (error) throw new Error('Failed to load agents');
return data!;
},
enabled: isOpen && isAgentScope(scope),
enabled: isOpen && (isApplicationScope(scope) || isRouteScope(scope)),
staleTime: 30_000,
});
@@ -73,21 +82,53 @@ export function usePaletteSearch() {
return a.id.toLowerCase().includes(q) || a.group.toLowerCase().includes(q);
});
const agentResults: PaletteResult[] = filteredAgents.slice(0, 10).map((a) => ({
type: 'agent' as const,
const applicationResults: PaletteResult[] = filteredAgents.slice(0, 10).map((a) => ({
type: 'application' as const,
id: a.id,
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[] = [];
if (scope === 'all') results = [...executionResults, ...agentResults];
if (scope === 'all') results = [...executionResults, ...applicationResults, ...routeResults];
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 {
results,
executionCount: executionsQuery.data?.total ?? 0,
agentCount: filteredAgents.length,
applicationCount: filteredAgents.length,
routeCount: filteredRoutes.length,
isLoading: executionsQuery.isFetching || agentsQuery.isFetching,
};
}

View File

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