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);
|
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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user