mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(ui): user-friendly tool and agent labels in the chat tool timeline (#3054)
This commit is contained in:
@@ -91,6 +91,49 @@ describe('subMascotModelsFromTimeline', () => {
|
||||
])
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('prefers subagent displayName from registry over humanized agent id', () => {
|
||||
const models = subMascotModelsFromTimeline([
|
||||
subagentEntry({
|
||||
id: 'thread-1:subagent:sub-1:code_executor',
|
||||
name: 'subagent:code_executor',
|
||||
subagent: {
|
||||
taskId: 'sub-1',
|
||||
agentId: 'code_executor',
|
||||
displayName: 'Code Executor',
|
||||
toolCalls: [],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(models).toHaveLength(1);
|
||||
expect(models[0].label).toBe('Code Executor');
|
||||
});
|
||||
|
||||
it('prefers entry displayName when subagent displayName is absent', () => {
|
||||
const models = subMascotModelsFromTimeline([
|
||||
subagentEntry({
|
||||
displayName: 'Custom Label',
|
||||
subagent: { taskId: 'sub-1', agentId: 'researcher', toolCalls: [] },
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(models[0].label).toBe('Custom Label');
|
||||
});
|
||||
|
||||
it('uses formatToolName for running child tool activity', () => {
|
||||
const models = subMascotModelsFromTimeline([
|
||||
subagentEntry({
|
||||
subagent: {
|
||||
taskId: 'sub-1',
|
||||
agentId: 'researcher',
|
||||
toolCalls: [{ callId: 'c1', toolName: 'web_fetch', status: 'running' }],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(models[0].activity).toBe('Using Fetching');
|
||||
});
|
||||
});
|
||||
|
||||
describe('<SubMascotLayer />', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import debug from 'debug';
|
||||
import { type FC, useMemo } from 'react';
|
||||
|
||||
import type { ToolTimelineEntry, ToolTimelineEntryStatus } from '../../store/chatRuntimeSlice';
|
||||
import { formatToolName } from '../../utils/toolTimelineFormatting';
|
||||
import { type MascotFace, RiveMascot } from './Mascot';
|
||||
import type { MascotColor } from './Mascot/mascotPalette';
|
||||
|
||||
@@ -49,7 +50,7 @@ function truncateActivity(value: string): string {
|
||||
return `${trimmed.slice(0, ACTIVITY_LIMIT - 3).trimEnd()}...`;
|
||||
}
|
||||
|
||||
function humanizeIdentifier(value: string): string {
|
||||
function humanizeAgentId(value: string): string {
|
||||
const cleaned = value
|
||||
.replace(/^subagent:/, '')
|
||||
.replace(/[_-]+/g, ' ')
|
||||
@@ -84,7 +85,7 @@ function activityForEntry(entry: ToolTimelineEntry): string {
|
||||
|
||||
const lastRunningTool = [...subagent.toolCalls].reverse().find(call => call.status === 'running');
|
||||
if (lastRunningTool) {
|
||||
return `Using ${humanizeIdentifier(lastRunningTool.toolName)}`;
|
||||
return `Using ${formatToolName(lastRunningTool.toolName)}`;
|
||||
}
|
||||
|
||||
if (subagent.childIteration) {
|
||||
@@ -119,7 +120,7 @@ export function subMascotModelsFromTimeline(entries: ToolTimelineEntry[]): SubMa
|
||||
return {
|
||||
id: entry.id,
|
||||
agentId,
|
||||
label: humanizeIdentifier(agentId),
|
||||
label: entry.subagent?.displayName ?? entry.displayName ?? humanizeAgentId(agentId),
|
||||
status: entry.status,
|
||||
face: faceForStatus(entry.status),
|
||||
activity: activityForEntry(entry),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import type { SubagentActivity, ToolTimelineEntry } from '../../../store/chatRuntimeSlice';
|
||||
import { formatTimelineEntry } from '../../../utils/toolTimelineFormatting';
|
||||
import { formatTimelineEntry, formatToolName } from '../../../utils/toolTimelineFormatting';
|
||||
import { parseWorkerThreadRef } from '../utils/workerThreadRef';
|
||||
import { WorkerThreadRefCard, type WorkerThreadStatus } from './WorkerThreadRefCard';
|
||||
|
||||
@@ -115,8 +115,8 @@ export function SubagentActivityBlock({
|
||||
className="flex items-center gap-1.5"
|
||||
data-testid="subagent-tool-call">
|
||||
<span className={`text-[9px] ${tone}`}>•</span>
|
||||
<span className="font-mono text-[10px] text-stone-700 dark:text-neutral-200">
|
||||
{call.toolName}
|
||||
<span className="text-[10px] text-stone-700 dark:text-neutral-200">
|
||||
{formatToolName(call.toolName)}
|
||||
</span>
|
||||
{call.iteration != null ? (
|
||||
<span className="text-[9px] text-stone-400 dark:text-neutral-500">
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('SubagentActivityBlock', () => {
|
||||
expect(block.textContent).toContain('4.2s');
|
||||
});
|
||||
|
||||
it('renders one row per child tool call with status + timing', () => {
|
||||
it('renders one row per child tool call with formatted names, status + timing', () => {
|
||||
renderInStore(
|
||||
<SubagentActivityBlock
|
||||
subagent={{
|
||||
@@ -62,18 +62,20 @@ describe('SubagentActivityBlock', () => {
|
||||
toolCalls: [
|
||||
{ callId: 'c1', toolName: 'web_search', status: 'success', elapsedMs: 312 },
|
||||
{ callId: 'c2', toolName: 'composio_execute', status: 'running', iteration: 2 },
|
||||
{ callId: 'c3', toolName: 'noisy', status: 'error', elapsedMs: 50 },
|
||||
{ callId: 'c3', toolName: 'file_read', status: 'error', elapsedMs: 50 },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const calls = screen.getAllByTestId('subagent-tool-call');
|
||||
expect(calls).toHaveLength(3);
|
||||
expect(calls[0].textContent).toContain('web_search');
|
||||
expect(calls[0].textContent).toContain('Searching the web');
|
||||
expect(calls[0].textContent).toContain('success');
|
||||
expect(calls[0].textContent).toContain('312ms');
|
||||
expect(calls[1].textContent).toContain('Composio Execute');
|
||||
expect(calls[1].textContent).toContain('running');
|
||||
expect(calls[1].textContent).toContain('·t2');
|
||||
expect(calls[2].textContent).toContain('Reading file');
|
||||
expect(calls[2].textContent).toContain('error');
|
||||
});
|
||||
|
||||
@@ -154,7 +156,7 @@ describe('ToolTimelineBlock — subagent rendering', () => {
|
||||
|
||||
const calls = screen.getAllByTestId('subagent-tool-call');
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].textContent).toContain('web_search');
|
||||
expect(calls[0].textContent).toContain('Searching the web');
|
||||
expect(screen.getByTestId('subagent-activity').textContent).toContain('turn 1/5');
|
||||
});
|
||||
|
||||
|
||||
@@ -485,6 +485,7 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
subagent: {
|
||||
taskId: event.skill_id,
|
||||
agentId: event.tool_name,
|
||||
displayName: event.subagent?.display_name,
|
||||
workerThreadId: event.subagent?.worker_thread_id,
|
||||
mode: event.subagent?.mode,
|
||||
dedicatedThread: event.subagent?.dedicated_thread,
|
||||
|
||||
@@ -198,6 +198,8 @@ export interface SubagentProgressDetail {
|
||||
output_chars?: number;
|
||||
/** Persistent worker sub-thread id backing the delegation (on `subagent_spawned`). */
|
||||
worker_thread_id?: string;
|
||||
/** Human-readable display name from the agent registry. */
|
||||
display_name?: string;
|
||||
}
|
||||
|
||||
/** Extended payload for `subagent_spawned`. */
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface SubagentActivity {
|
||||
taskId: string;
|
||||
/** Sub-agent definition id (e.g. `researcher`). */
|
||||
agentId: string;
|
||||
/** Human-readable display name from the agent registry (e.g. "Researcher"). */
|
||||
displayName?: string;
|
||||
/**
|
||||
* Persistent worker sub-thread id (`worker-<uuid>`) backing this
|
||||
* delegation, when one was created. Lets the drawer reopen the full
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { ToolTimelineEntry } from '../../store/chatRuntimeSlice';
|
||||
import { formatTimelineEntry } from '../toolTimelineFormatting';
|
||||
import { formatTimelineEntry, formatToolName } from '../toolTimelineFormatting';
|
||||
|
||||
function entry(overrides: Partial<ToolTimelineEntry>): ToolTimelineEntry {
|
||||
return { id: 'x', name: 'delegate_notion', round: 1, status: 'running', ...overrides };
|
||||
@@ -126,4 +126,122 @@ describe('formatTimelineEntry', () => {
|
||||
detail: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('formats shell tool with truncated command detail', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({ name: 'shell', argsBuffer: JSON.stringify({ command: 'cargo test --lib' }) })
|
||||
)
|
||||
).toEqual({ title: 'Running command', detail: 'cargo test --lib' });
|
||||
});
|
||||
|
||||
it('formats web_fetch with hostname in title', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({
|
||||
name: 'web_fetch',
|
||||
argsBuffer: JSON.stringify({ url: 'https://docs.example.com/api/v2/users' }),
|
||||
})
|
||||
)
|
||||
).toEqual({
|
||||
title: 'Fetching docs.example.com',
|
||||
detail: 'https://docs.example.com/api/v2/users',
|
||||
});
|
||||
});
|
||||
|
||||
it('formats web_search with query in title', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({ name: 'web_search', argsBuffer: JSON.stringify({ query: 'rust async trait' }) })
|
||||
)
|
||||
).toEqual({ title: 'Searching: rust async trait' });
|
||||
});
|
||||
|
||||
it('formats file_read with shortened path', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({
|
||||
name: 'file_read',
|
||||
argsBuffer: JSON.stringify({ path: 'src/openhuman/agent/progress.rs' }),
|
||||
})
|
||||
)
|
||||
).toEqual({ title: 'Reading file', detail: '…/agent/progress.rs' });
|
||||
});
|
||||
|
||||
it('formats edit tool with file path', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({
|
||||
name: 'edit',
|
||||
argsBuffer: JSON.stringify({ file_path: 'app/src/components/App.tsx' }),
|
||||
})
|
||||
)
|
||||
).toEqual({ title: 'Editing file', detail: '…/components/App.tsx' });
|
||||
});
|
||||
|
||||
it('formats grep with pattern in title', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({ name: 'grep', argsBuffer: JSON.stringify({ pattern: 'SubagentSpawned' }) })
|
||||
)
|
||||
).toEqual({ title: 'Searching: SubagentSpawned' });
|
||||
});
|
||||
|
||||
it('formats git_operations with subcommand', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({ name: 'git_operations', argsBuffer: JSON.stringify({ command: 'diff --stat' }) })
|
||||
)
|
||||
).toEqual({ title: 'Git diff', detail: 'diff --stat' });
|
||||
});
|
||||
|
||||
it('formats screenshot as a simple label', () => {
|
||||
expect(formatTimelineEntry(entry({ name: 'screenshot' }))).toEqual({
|
||||
title: 'Taking screenshot',
|
||||
});
|
||||
});
|
||||
|
||||
it('formats glob with pattern detail', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({ name: 'glob', argsBuffer: JSON.stringify({ pattern: '**/*.test.ts' }) })
|
||||
)
|
||||
).toEqual({ title: 'Finding: **/*.test.ts' });
|
||||
});
|
||||
|
||||
it('formats list with directory path', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({ name: 'list', argsBuffer: JSON.stringify({ path: 'src/openhuman/tools' }) })
|
||||
)
|
||||
).toEqual({ title: 'Listing directory', detail: 'src/openhuman/tools' });
|
||||
});
|
||||
|
||||
it('formats browser_open with hostname', () => {
|
||||
expect(
|
||||
formatTimelineEntry(
|
||||
entry({
|
||||
name: 'browser_open',
|
||||
argsBuffer: JSON.stringify({ url: 'https://github.com/tinyhumansai/openhuman' }),
|
||||
})
|
||||
)
|
||||
).toEqual({ title: 'Browsing github.com' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatToolName', () => {
|
||||
it('returns human-readable names for known tools', () => {
|
||||
expect(formatToolName('shell')).toBe('Running command');
|
||||
expect(formatToolName('web_fetch')).toBe('Fetching');
|
||||
expect(formatToolName('file_read')).toBe('Reading file');
|
||||
expect(formatToolName('edit')).toBe('Editing file');
|
||||
expect(formatToolName('grep')).toBe('Searching code');
|
||||
expect(formatToolName('git_operations')).toBe('Git operation');
|
||||
expect(formatToolName('screenshot')).toBe('Taking screenshot');
|
||||
expect(formatToolName('lsp')).toBe('Code intelligence');
|
||||
});
|
||||
|
||||
it('falls back to humanized identifier for unknown tools', () => {
|
||||
expect(formatToolName('custom_fancy_tool')).toBe('Custom Fancy Tool');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,79 @@ interface ParsedToolArgs {
|
||||
agent_id?: string;
|
||||
prompt?: string;
|
||||
toolkit?: string;
|
||||
command?: string;
|
||||
url?: string;
|
||||
path?: string;
|
||||
file_path?: string;
|
||||
pattern?: string;
|
||||
query?: string;
|
||||
tool_name?: string;
|
||||
}
|
||||
|
||||
const TOOL_DISPLAY_NAMES: Record<string, string> = {
|
||||
shell: 'Running command',
|
||||
node_exec: 'Running command',
|
||||
npm_exec: 'Running command',
|
||||
web_fetch: 'Fetching',
|
||||
http_request: 'Fetching',
|
||||
curl: 'Fetching',
|
||||
web_search: 'Searching the web',
|
||||
gitbooks_search: 'Searching docs',
|
||||
file_read: 'Reading file',
|
||||
file_write: 'Writing file',
|
||||
edit: 'Editing file',
|
||||
apply_patch: 'Applying patch',
|
||||
grep: 'Searching code',
|
||||
glob: 'Finding files',
|
||||
list: 'Listing directory',
|
||||
read_diff: 'Reading diff',
|
||||
git_operations: 'Git operation',
|
||||
browser: 'Browsing',
|
||||
browser_open: 'Opening browser',
|
||||
screenshot: 'Taking screenshot',
|
||||
image_info: 'Analyzing image',
|
||||
install_tool: 'Installing tool',
|
||||
lsp: 'Code intelligence',
|
||||
keyboard: 'Typing',
|
||||
mouse: 'Clicking',
|
||||
csv_export: 'Exporting CSV',
|
||||
update_memory_md: 'Updating memory',
|
||||
read_workspace_state: 'Reading workspace',
|
||||
current_time: 'Checking time',
|
||||
schedule: 'Scheduling',
|
||||
detect_tools: 'Detecting tools',
|
||||
tool_stats: 'Tool statistics',
|
||||
vault_write_markdown: 'Writing to vault',
|
||||
run_linter: 'Running linter',
|
||||
run_tests: 'Running tests',
|
||||
proxy_config: 'Configuring proxy',
|
||||
update_check: 'Checking for updates',
|
||||
update_apply: 'Applying update',
|
||||
pushover: 'Sending notification',
|
||||
insert_sql_record: 'Inserting record',
|
||||
mcp_list_servers: 'Listing MCP servers',
|
||||
mcp_list_tools: 'Listing MCP tools',
|
||||
mcp_call_tool: 'Calling MCP tool',
|
||||
mcp_setup_search: 'Searching MCP tools',
|
||||
mcp_setup_get: 'Getting MCP tool',
|
||||
mcp_setup_install_and_connect: 'Installing MCP server',
|
||||
mcp_setup_request_secret: 'Requesting secret',
|
||||
mcp_setup_test_connection: 'Testing connection',
|
||||
polymarket: 'Checking markets',
|
||||
gmail_unsubscribe: 'Unsubscribing',
|
||||
gitbooks_get_page: 'Reading docs page',
|
||||
audio_generate_podcast: 'Generating podcast',
|
||||
audio_email_podcast: 'Emailing podcast',
|
||||
audio_generate_and_email_podcast: 'Generating & emailing podcast',
|
||||
composio_list_connections: 'Viewing your Connections',
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a raw tool name into a short human-readable label.
|
||||
* Used for subagent child tool rows and sub-mascot activity text.
|
||||
*/
|
||||
export function formatToolName(toolName: string): string {
|
||||
return TOOL_DISPLAY_NAMES[toolName] ?? humanizeIdentifier(toolName);
|
||||
}
|
||||
|
||||
export function formatTimelineEntry(entry: ToolTimelineEntry): { title: string; detail?: string } {
|
||||
@@ -56,15 +129,6 @@ export function formatTimelineEntry(entry: ToolTimelineEntry): { title: string;
|
||||
inferIntegrationNameFromPrompt(parsedArgs?.prompt) ??
|
||||
inferIntegrationName(entry.name);
|
||||
|
||||
// The collapsed `delegate_to_integrations_agent` tool has no toolkit
|
||||
// baked into its name; if we couldn't infer a provider from args or
|
||||
// prompt, surface a generic connected-app label (matches the
|
||||
// `spawn_subagent → integrations_agent` fallback above) instead of
|
||||
// humanising the tool name into "To Integrations Agent". Unknown
|
||||
// toolkit slugs from args fall back to a humanised toolkit label so
|
||||
// composio integrations outside KNOWN_TOOLKIT_RE still render
|
||||
// meaningfully (e.g. `slack_bot` → "Slack Bot") rather than the
|
||||
// generic copy.
|
||||
let title: string;
|
||||
if (provider) {
|
||||
title = integrationActivityTitle(provider);
|
||||
@@ -80,6 +144,12 @@ export function formatTimelineEntry(entry: ToolTimelineEntry): { title: string;
|
||||
return { title, detail: entry.detail ?? parsedArgs?.prompt };
|
||||
}
|
||||
|
||||
// ── Tool-specific formatting with args-derived detail ──────────────
|
||||
const toolDetail = formatToolDetail(entry.name, parsedArgs);
|
||||
if (toolDetail) {
|
||||
return { title: toolDetail.title, detail: toolDetail.detail ?? entry.detail };
|
||||
}
|
||||
|
||||
return {
|
||||
title: entry.displayName ?? humanizeIdentifier(entry.name),
|
||||
detail: entry.detail ?? parsedArgs?.prompt,
|
||||
@@ -90,6 +160,137 @@ export function promptFromArgsBuffer(argsBuffer?: string): string | undefined {
|
||||
return parseToolArgs(argsBuffer)?.prompt?.trim() || undefined;
|
||||
}
|
||||
|
||||
const MAX_DETAIL_LEN = 120;
|
||||
|
||||
function truncateDetail(value: string): string {
|
||||
const cleaned = value.trim().replace(/\s+/g, ' ');
|
||||
if (cleaned.length <= MAX_DETAIL_LEN) return cleaned;
|
||||
return `${cleaned.slice(0, MAX_DETAIL_LEN - 1)}…`;
|
||||
}
|
||||
|
||||
function hostnameFromUrl(url: string): string | undefined {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function shortenPath(filePath: string): string {
|
||||
const parts = filePath.split('/');
|
||||
if (parts.length <= 3) return filePath;
|
||||
return `…/${parts.slice(-2).join('/')}`;
|
||||
}
|
||||
|
||||
function formatToolDetail(
|
||||
name: string,
|
||||
args: ParsedToolArgs | null
|
||||
): { title: string; detail?: string } | null {
|
||||
switch (name) {
|
||||
case 'shell':
|
||||
case 'node_exec':
|
||||
case 'npm_exec': {
|
||||
const cmd = args?.command?.trim();
|
||||
return { title: 'Running command', detail: cmd ? truncateDetail(cmd) : undefined };
|
||||
}
|
||||
|
||||
case 'web_fetch':
|
||||
case 'http_request':
|
||||
case 'curl': {
|
||||
const url = args?.url?.trim();
|
||||
const host = url ? hostnameFromUrl(url) : undefined;
|
||||
return {
|
||||
title: host ? `Fetching ${host}` : 'Fetching',
|
||||
detail: url ? truncateDetail(url) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
case 'web_search': {
|
||||
const query = args?.query?.trim();
|
||||
return { title: query ? `Searching: ${truncateDetail(query)}` : 'Searching the web' };
|
||||
}
|
||||
|
||||
case 'gitbooks_search': {
|
||||
const query = args?.query?.trim();
|
||||
return { title: query ? `Searching docs: ${truncateDetail(query)}` : 'Searching docs' };
|
||||
}
|
||||
|
||||
case 'file_read': {
|
||||
const p = args?.path?.trim() ?? args?.file_path?.trim();
|
||||
return { title: 'Reading file', detail: p ? shortenPath(p) : undefined };
|
||||
}
|
||||
|
||||
case 'file_write':
|
||||
case 'vault_write_markdown': {
|
||||
const p = args?.path?.trim() ?? args?.file_path?.trim();
|
||||
return { title: 'Writing file', detail: p ? shortenPath(p) : undefined };
|
||||
}
|
||||
|
||||
case 'edit':
|
||||
case 'apply_patch': {
|
||||
const p = args?.path?.trim() ?? args?.file_path?.trim();
|
||||
return { title: 'Editing file', detail: p ? shortenPath(p) : undefined };
|
||||
}
|
||||
|
||||
case 'grep': {
|
||||
const pat = args?.pattern?.trim();
|
||||
return { title: pat ? `Searching: ${truncateDetail(pat)}` : 'Searching code' };
|
||||
}
|
||||
|
||||
case 'glob': {
|
||||
const pat = args?.pattern?.trim();
|
||||
return { title: pat ? `Finding: ${truncateDetail(pat)}` : 'Finding files' };
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
const p = args?.path?.trim();
|
||||
return { title: 'Listing directory', detail: p ? shortenPath(p) : undefined };
|
||||
}
|
||||
|
||||
case 'git_operations': {
|
||||
const cmd = args?.command?.trim();
|
||||
if (cmd) {
|
||||
const verb = cmd.split(/\s+/)[0];
|
||||
return { title: `Git ${verb}`, detail: truncateDetail(cmd) };
|
||||
}
|
||||
return { title: 'Git operation' };
|
||||
}
|
||||
|
||||
case 'browser':
|
||||
case 'browser_open': {
|
||||
const url = args?.url?.trim();
|
||||
const host = url ? hostnameFromUrl(url) : undefined;
|
||||
return { title: host ? `Browsing ${host}` : 'Browsing' };
|
||||
}
|
||||
|
||||
case 'screenshot':
|
||||
return { title: 'Taking screenshot' };
|
||||
|
||||
case 'image_info':
|
||||
return { title: 'Analyzing image' };
|
||||
|
||||
case 'install_tool': {
|
||||
const tn = args?.tool_name?.trim();
|
||||
return { title: tn ? `Installing ${tn}` : 'Installing tool' };
|
||||
}
|
||||
|
||||
case 'lsp':
|
||||
return { title: 'Code intelligence' };
|
||||
|
||||
case 'run_tests':
|
||||
return { title: 'Running tests' };
|
||||
|
||||
case 'run_linter':
|
||||
return { title: 'Running linter' };
|
||||
|
||||
case 'read_diff':
|
||||
return { title: 'Reading diff' };
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognise the small set of known integration toolkit slugs. Used to
|
||||
* gate `inferIntegrationName` so unknown `delegate_<x>` names (e.g.
|
||||
|
||||
@@ -256,6 +256,11 @@ pub struct SubagentProgressDetail {
|
||||
/// uses it to reopen the full parent↔subagent conversation from memory.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub worker_thread_id: Option<String>,
|
||||
/// Human-readable display name from the agent registry (e.g.
|
||||
/// "Researcher", "Coding Agent"). The frontend uses this for
|
||||
/// consistent agent labels across timeline, sub-mascots, and drawer.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@@ -73,6 +73,10 @@ pub enum AgentProgress {
|
||||
/// full parent↔subagent conversation from memory after the live
|
||||
/// turn ends. `None` for live-only runs (no parent context).
|
||||
worker_thread_id: Option<String>,
|
||||
/// Human-readable display name from the agent registry (e.g.
|
||||
/// "Researcher", "Coding Agent"). Falls back to `agent_id` in
|
||||
/// the UI when absent.
|
||||
display_name: Option<String>,
|
||||
},
|
||||
|
||||
/// A sub-agent completed successfully.
|
||||
|
||||
@@ -414,6 +414,9 @@ impl AgentOrchestrationSession {
|
||||
});
|
||||
|
||||
if let Some(progress) = parent.on_progress.clone() {
|
||||
let resolved_display_name = AgentDefinitionRegistry::global()
|
||||
.and_then(|reg| reg.get(&agent_id))
|
||||
.map(|def| def.display_name().to_string());
|
||||
let _ = progress
|
||||
.send(AgentProgress::SubagentSpawned {
|
||||
agent_id: agent_id.clone(),
|
||||
@@ -422,6 +425,7 @@ impl AgentOrchestrationSession {
|
||||
dedicated_thread: false,
|
||||
prompt_chars: prompt.chars().count(),
|
||||
worker_thread_id: None,
|
||||
display_name: resolved_display_name,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ pub(crate) async fn dispatch_subagent(
|
||||
dedicated_thread: false,
|
||||
prompt_chars: prompt.chars().count(),
|
||||
worker_thread_id: None,
|
||||
display_name: Some(definition.display_name().to_string()),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -266,6 +266,7 @@ impl Tool for SpawnParallelAgentsTool {
|
||||
dedicated_thread: false,
|
||||
prompt_chars: prompt.chars().count(),
|
||||
worker_thread_id: None,
|
||||
display_name: Some(definition.display_name().to_string()),
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -433,6 +433,7 @@ impl Tool for SpawnSubagentTool {
|
||||
dedicated_thread,
|
||||
prompt_chars: prompt.chars().count(),
|
||||
worker_thread_id: worker_thread_id.clone(),
|
||||
display_name: Some(definition.display_name().to_string()),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1154,13 +1154,15 @@ fn spawn_progress_bridge(
|
||||
dedicated_thread,
|
||||
prompt_chars,
|
||||
worker_thread_id,
|
||||
display_name,
|
||||
} => {
|
||||
let label = display_name.as_deref().unwrap_or(&agent_id);
|
||||
publish_web_channel_event(WebChannelEvent {
|
||||
event: "subagent_spawned".to_string(),
|
||||
client_id: client_id.clone(),
|
||||
thread_id: thread_id.clone(),
|
||||
request_id: request_id.clone(),
|
||||
message: Some(format!("Sub-agent '{agent_id}' spawned")),
|
||||
message: Some(format!("Sub-agent '{label}' spawned")),
|
||||
tool_name: Some(agent_id),
|
||||
skill_id: Some(task_id),
|
||||
round: Some(round),
|
||||
@@ -1169,6 +1171,7 @@ fn spawn_progress_bridge(
|
||||
dedicated_thread: Some(dedicated_thread),
|
||||
prompt_chars: Some(prompt_chars as u64),
|
||||
worker_thread_id,
|
||||
display_name,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
|
||||
@@ -145,6 +145,7 @@ impl TurnStateMirror {
|
||||
mode,
|
||||
dedicated_thread,
|
||||
worker_thread_id,
|
||||
display_name,
|
||||
..
|
||||
} => {
|
||||
self.state.phase = Some(TurnPhase::Subagent);
|
||||
@@ -155,7 +156,7 @@ impl TurnStateMirror {
|
||||
round: self.state.iteration,
|
||||
status: ToolTimelineStatus::Running,
|
||||
args_buffer: None,
|
||||
display_name: Some(agent_id.clone()),
|
||||
display_name: display_name.clone().or_else(|| Some(agent_id.clone())),
|
||||
detail: None,
|
||||
source_tool_name: Some("spawn_subagent".to_string()),
|
||||
subagent: Some(SubagentActivity {
|
||||
|
||||
Reference in New Issue
Block a user