mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
431 lines
13 KiB
TypeScript
431 lines
13 KiB
TypeScript
import type { ToolTimelineEntry } from '../store/chatRuntimeSlice';
|
|
|
|
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;
|
|
question?: 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',
|
|
agent_prepare_context: 'Preparing context',
|
|
};
|
|
|
|
/**
|
|
* 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 } {
|
|
const parsedArgs = parseToolArgs(entry.argsBuffer);
|
|
|
|
if (entry.name === 'spawn_subagent' && parsedArgs?.agent_id === 'integrations_agent') {
|
|
const provider =
|
|
inferIntegrationName(parsedArgs.toolkit) ?? inferIntegrationNameFromPrompt(parsedArgs.prompt);
|
|
return {
|
|
title: provider ? integrationActivityTitle(provider) : 'Checking your connected app',
|
|
detail: parsedArgs.prompt?.trim() || entry.detail,
|
|
};
|
|
}
|
|
|
|
if (entry.name === 'integrations_agent' || entry.name === 'subagent:integrations_agent') {
|
|
const provider =
|
|
inferIntegrationName(entry.sourceToolName) ??
|
|
inferIntegrationName(parsedArgs?.toolkit) ??
|
|
inferIntegrationNameFromPrompt(entry.detail) ??
|
|
inferIntegrationNameFromPrompt(parsedArgs?.prompt);
|
|
|
|
return {
|
|
title: provider ? integrationActivityTitle(provider) : 'Checking your connected app',
|
|
detail: entry.detail,
|
|
};
|
|
}
|
|
|
|
if (entry.name === 'subagent:researcher' || entry.name === 'researcher') {
|
|
return { title: 'Researching', detail: entry.detail };
|
|
}
|
|
if (entry.name === 'agent_prepare_context') {
|
|
return { title: 'Preparing context', detail: parsedArgs?.question?.trim() || entry.detail };
|
|
}
|
|
if (entry.name === 'subagent:context_scout' || entry.name === 'context_scout') {
|
|
return { title: 'Scouting context', detail: entry.detail };
|
|
}
|
|
if (entry.name === 'composio_list_connections') {
|
|
return { title: 'Viewing your Connections', detail: entry.detail };
|
|
}
|
|
if (entry.name === 'subagent:orchestrator' || entry.name === 'orchestrator') {
|
|
return { title: 'Planning next steps', detail: entry.detail };
|
|
}
|
|
if (entry.name === 'subagent:critic' || entry.name === 'critic') {
|
|
return { title: 'Reviewing the work', detail: entry.detail };
|
|
}
|
|
if (entry.name === 'subagent:tools_agent' || entry.name === 'tools_agent') {
|
|
return { title: 'Using tools', detail: entry.detail };
|
|
}
|
|
if (entry.name === 'subagent:code_executor' || entry.name === 'code_executor') {
|
|
return { title: 'Running code', detail: entry.detail };
|
|
}
|
|
|
|
if (entry.name.startsWith('delegate_')) {
|
|
const provider =
|
|
inferIntegrationName(parsedArgs?.toolkit) ??
|
|
inferIntegrationNameFromPrompt(parsedArgs?.prompt) ??
|
|
inferIntegrationName(entry.name);
|
|
|
|
let title: string;
|
|
if (provider) {
|
|
title = integrationActivityTitle(provider);
|
|
} else if (entry.name === 'delegate_to_integrations_agent') {
|
|
const rawToolkit = parsedArgs?.toolkit?.trim();
|
|
title = rawToolkit
|
|
? integrationActivityTitle(humanizeIdentifier(rawToolkit))
|
|
: 'Checking your connected app';
|
|
} else {
|
|
title = humanizeIdentifier(entry.name);
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
export function promptFromArgsBuffer(argsBuffer?: string): string | undefined {
|
|
return parseToolArgs(argsBuffer)?.prompt?.trim() || undefined;
|
|
}
|
|
|
|
/** A web source an agent fetched/browsed during a run. */
|
|
export interface AgentSource {
|
|
/** Stable id (the originating timeline entry id). */
|
|
id: string;
|
|
/** Display title — the URL hostname. */
|
|
title: string;
|
|
/** Full URL. */
|
|
url: string;
|
|
}
|
|
|
|
/** Tools whose `url` arg represents a real web source the agent visited. */
|
|
const URL_SOURCE_TOOLS = new Set(['web_fetch', 'http_request', 'curl', 'browser', 'browser_open']);
|
|
|
|
/**
|
|
* Extract the distinct web sources an agent run touched, for the
|
|
* "Agent Process Source" panel. Derived from real `url` args on
|
|
* fetch/browse timeline entries — never fabricated. Deduplicated by URL,
|
|
* preserving first-seen order.
|
|
*/
|
|
export function extractAgentSources(entries: ToolTimelineEntry[]): AgentSource[] {
|
|
const seen = new Set<string>();
|
|
const sources: AgentSource[] = [];
|
|
for (const entry of entries) {
|
|
const baseName = entry.name.replace(/^subagent:/, '');
|
|
if (!URL_SOURCE_TOOLS.has(baseName)) continue;
|
|
const url = parseToolArgs(entry.argsBuffer)?.url?.trim();
|
|
if (!url || seen.has(url)) continue;
|
|
seen.add(url);
|
|
sources.push({ id: entry.id, title: hostnameFromUrl(url) ?? url, url });
|
|
}
|
|
return sources;
|
|
}
|
|
|
|
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.
|
|
* `delegate_summarize`, `delegate_router`) don't get fake-humanised
|
|
* into bogus "integration" labels in the tool timeline.
|
|
*/
|
|
const KNOWN_TOOLKIT_RE =
|
|
/^(gmail|notion|github|slack|discord|linear|jira|google_calendar|google_drive|calendar)$/i;
|
|
|
|
export function inferIntegrationName(input?: string): string | undefined {
|
|
if (!input) return undefined;
|
|
|
|
const delegateMatch = input.match(/^delegate_(.+)$/);
|
|
if (delegateMatch && KNOWN_TOOLKIT_RE.test(delegateMatch[1])) {
|
|
return normalizeIntegrationName(delegateMatch[1]);
|
|
}
|
|
|
|
if (KNOWN_TOOLKIT_RE.test(input)) {
|
|
return normalizeIntegrationName(input);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function integrationActivityTitle(provider: string): string {
|
|
switch (provider) {
|
|
case 'GitHub':
|
|
case 'Gmail':
|
|
case 'Linear':
|
|
case 'Jira':
|
|
return `Making requests to your ${provider} account`;
|
|
case 'Notion':
|
|
return 'Working in your Notion workspace';
|
|
case 'Slack':
|
|
case 'Discord':
|
|
return `Working in your ${provider} workspace`;
|
|
case 'Google Calendar':
|
|
return 'Updating your Google Calendar';
|
|
case 'Google Drive':
|
|
return 'Working in your Google Drive';
|
|
default:
|
|
return `Checking your ${provider}`;
|
|
}
|
|
}
|
|
|
|
function inferIntegrationNameFromPrompt(prompt?: string): string | undefined {
|
|
if (!prompt) return undefined;
|
|
const known = [
|
|
'Notion',
|
|
'Gmail',
|
|
'GitHub',
|
|
'Slack',
|
|
'Discord',
|
|
'Linear',
|
|
'Jira',
|
|
'Google Calendar',
|
|
'Google Drive',
|
|
];
|
|
|
|
const lower = prompt.toLowerCase();
|
|
return known.find(name => lower.includes(name.toLowerCase()));
|
|
}
|
|
|
|
function parseToolArgs(argsBuffer?: string): ParsedToolArgs | null {
|
|
if (!argsBuffer) return null;
|
|
try {
|
|
const parsed = JSON.parse(argsBuffer) as ParsedToolArgs;
|
|
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function normalizeIntegrationName(value: string): string {
|
|
switch (value.toLowerCase()) {
|
|
case 'github':
|
|
return 'GitHub';
|
|
case 'gmail':
|
|
return 'Gmail';
|
|
case 'google_calendar':
|
|
case 'calendar':
|
|
return 'Google Calendar';
|
|
case 'google_drive':
|
|
return 'Google Drive';
|
|
default:
|
|
return humanizeIdentifier(value);
|
|
}
|
|
}
|
|
|
|
function humanizeIdentifier(value: string): string {
|
|
return value
|
|
.replace(/^subagent:/, '')
|
|
.replace(/^delegate_/, '')
|
|
.replace(/_/g, ' ')
|
|
.replace(/\b\w/g, char => char.toUpperCase());
|
|
}
|