Rename Agents to Applications, remove Exchanges, implement Routes search
- 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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user