diff --git a/sidecar/src/Aryx.AgentHost/Contracts/ProtocolModels.cs b/sidecar/src/Aryx.AgentHost/Contracts/ProtocolModels.cs
index 8bf620b..90c6549 100644
--- a/sidecar/src/Aryx.AgentHost/Contracts/ProtocolModels.cs
+++ b/sidecar/src/Aryx.AgentHost/Contracts/ProtocolModels.cs
@@ -456,6 +456,7 @@ public sealed class AgentActivityEventDto : SidecarEventDto
public string? SourceAgentName { get; init; }
public string? ToolName { get; init; }
public string? ToolCallId { get; init; }
+ public IReadOnlyDictionary? ToolArguments { get; init; }
public IReadOnlyList? FileChanges { get; init; }
}
diff --git a/sidecar/src/Aryx.AgentHost/Services/WorkflowRequestInfoInterpreter.cs b/sidecar/src/Aryx.AgentHost/Services/WorkflowRequestInfoInterpreter.cs
index f6bb163..7f0f502 100644
--- a/sidecar/src/Aryx.AgentHost/Services/WorkflowRequestInfoInterpreter.cs
+++ b/sidecar/src/Aryx.AgentHost/Services/WorkflowRequestInfoInterpreter.cs
@@ -12,6 +12,8 @@ internal static class WorkflowRequestInfoInterpreter
private const string ToolCallingActivityType = "tool-calling";
private const string CodeInterpreterToolName = "code interpreter";
private const string ImageGenerationToolName = "image generation";
+ private const int MaxToolArgumentValueLength = 4000;
+ private const string TruncatedToolArgumentValue = "[truncated]";
private static readonly JsonSerializerOptions JsonOptions = JsonSerialization.CreateWebOptions();
public static AgentActivityEventDto? TryCreateActivityFromRequest(
@@ -80,6 +82,7 @@ internal static class WorkflowRequestInfoInterpreter
AgentName = activeAgent.AgentName,
ToolName = tool.ToolName,
ToolCallId = tool.ToolCallId,
+ ToolArguments = tool.ToolArguments,
};
}
@@ -103,8 +106,8 @@ internal static class WorkflowRequestInfoInterpreter
return new HandoffRequestInterpretation(handoffAgent);
}
- return TryGetToolRequestInfo(requestInfo, out string toolName, out string? toolCallId)
- ? new ToolRequestInterpretation(toolName, toolCallId)
+ return TryGetToolRequestInfo(requestInfo, out string toolName, out string? toolCallId, out IReadOnlyDictionary? toolArguments)
+ ? new ToolRequestInterpretation(toolName, toolCallId, toolArguments)
: new UnknownRequestInterpretation();
}
@@ -137,33 +140,38 @@ internal static class WorkflowRequestInfoInterpreter
private static bool TryGetToolRequestInfo(
RequestInfoEvent requestInfo,
out string toolName,
- out string? toolCallId)
+ out string? toolCallId,
+ out IReadOnlyDictionary? toolArguments)
{
- return TryGetStableToolRequestInfo(requestInfo.Request.Data, out toolName, out toolCallId)
- || TryGetEvaluationToolRequestInfo(requestInfo.Request.Data, out toolName, out toolCallId);
+ return TryGetStableToolRequestInfo(requestInfo.Request.Data, out toolName, out toolCallId, out toolArguments)
+ || TryGetEvaluationToolRequestInfo(requestInfo.Request.Data, out toolName, out toolCallId, out toolArguments);
}
private static bool TryGetStableToolRequestInfo(
PortableValue requestData,
out string toolName,
- out string? toolCallId)
+ out string? toolCallId,
+ out IReadOnlyDictionary? toolArguments)
{
if (requestData.Is(out FunctionCallContent? functionCall))
{
toolName = NormalizeOptionalString(functionCall.Name) ?? "function";
toolCallId = NormalizeOptionalString(functionCall.CallId);
+ toolArguments = NormalizeToolArguments(functionCall.Arguments);
return true;
}
toolName = string.Empty;
toolCallId = null;
+ toolArguments = null;
return false;
}
private static bool TryGetEvaluationToolRequestInfo(
PortableValue requestData,
out string toolName,
- out string? toolCallId)
+ out string? toolCallId,
+ out IReadOnlyDictionary? toolArguments)
{
if (requestData.Is(out McpServerToolCallContent? mcpToolCall))
{
@@ -171,6 +179,7 @@ internal static class WorkflowRequestInfoInterpreter
?? NormalizeOptionalString(mcpToolCall.ServerName)
?? string.Empty;
toolCallId = NormalizeOptionalString(mcpToolCall.CallId);
+ toolArguments = NormalizeToolArguments(mcpToolCall.Arguments);
return toolName.Length > 0;
}
@@ -178,6 +187,7 @@ internal static class WorkflowRequestInfoInterpreter
{
toolName = CodeInterpreterToolName;
toolCallId = NormalizeOptionalString(codeInterpreterToolCall.CallId);
+ toolArguments = NormalizeCodeInterpreterToolArguments(codeInterpreterToolCall);
return true;
}
@@ -185,14 +195,196 @@ internal static class WorkflowRequestInfoInterpreter
{
toolName = ImageGenerationToolName;
toolCallId = null;
+ toolArguments = null;
return true;
}
toolName = string.Empty;
toolCallId = null;
+ toolArguments = null;
return false;
}
+ private static IReadOnlyDictionary? NormalizeToolArguments(
+ IEnumerable>? arguments)
+ {
+ if (arguments is null)
+ {
+ return null;
+ }
+
+ Dictionary normalized = new(StringComparer.Ordinal);
+ foreach (KeyValuePair argument in arguments)
+ {
+ string? key = NormalizeOptionalString(argument.Key);
+ if (key is null)
+ {
+ continue;
+ }
+
+ object? value = NormalizeToolArgumentValue(argument.Value);
+ if (value is null)
+ {
+ continue;
+ }
+
+ normalized[key] = value;
+ }
+
+ return normalized.Count > 0 ? normalized : null;
+ }
+
+ private static IReadOnlyDictionary? NormalizeCodeInterpreterToolArguments(
+ CodeInterpreterToolCallContent codeInterpreterToolCall)
+ {
+ IList? rawInputs = codeInterpreterToolCall.Inputs;
+ if (rawInputs is not { Count: > 0 })
+ {
+ return null;
+ }
+
+ List
)}
+ {/* Tool call argument details */}
+ {event.kind === 'tool-call' && (
+
+ )}
+
{/* File change preview for tool-call events */}
{event.kind === 'tool-call' && event.fileChanges && event.fileChanges.length > 0 && (
diff --git a/src/renderer/lib/toolCallSummary.ts b/src/renderer/lib/toolCallSummary.ts
new file mode 100644
index 0000000..f0213c4
--- /dev/null
+++ b/src/renderer/lib/toolCallSummary.ts
@@ -0,0 +1,116 @@
+const MAX_SUMMARY_LENGTH = 80;
+
+function truncateSummary(value: string): string {
+ const firstLine = value.split('\n')[0] ?? '';
+ const cleaned = firstLine.trim();
+ if (cleaned.length <= MAX_SUMMARY_LENGTH) return cleaned;
+ return `${cleaned.slice(0, MAX_SUMMARY_LENGTH)}…`;
+}
+
+function stringArg(args: Record, key: string): string | undefined {
+ const value = args[key];
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
+}
+
+function summarizePath(args: Record): string | undefined {
+ const path = stringArg(args, 'path');
+ if (!path) return undefined;
+ const range = args['view_range'] ?? args['viewRange'];
+ if (Array.isArray(range) && range.length === 2) {
+ return truncateSummary(`${path}:${range[0]}-${range[1]}`);
+ }
+ return truncateSummary(path);
+}
+
+function summarizeGitHub(toolName: string, args: Record): string | undefined {
+ const owner = stringArg(args, 'owner');
+ const repo = stringArg(args, 'repo');
+ const query = stringArg(args, 'query');
+
+ if (query) return truncateSummary(query);
+ if (owner && repo) return truncateSummary(`${owner}/${repo}`);
+ return undefined;
+}
+
+type SummaryExtractor = (args: Record, toolName: string) => string | undefined;
+
+const toolSummarizers: Record = {
+ powershell: (args) => stringArg(args, 'command') ? truncateSummary(stringArg(args, 'command')!) : undefined,
+ view: (args) => summarizePath(args),
+ edit: (args) => summarizePath(args),
+ create: (args) => summarizePath(args),
+ grep: (args) => stringArg(args, 'pattern') ? truncateSummary(stringArg(args, 'pattern')!) : undefined,
+ glob: (args) => stringArg(args, 'pattern') ? truncateSummary(stringArg(args, 'pattern')!) : undefined,
+ lsp: (args) => {
+ const op = stringArg(args, 'operation');
+ const file = stringArg(args, 'file');
+ if (op && file) return truncateSummary(`${op} ${file}`);
+ return op ? truncateSummary(op) : undefined;
+ },
+ web_fetch: (args) => stringArg(args, 'url') ? truncateSummary(stringArg(args, 'url')!) : undefined,
+ sql: (args) => stringArg(args, 'description') ? truncateSummary(stringArg(args, 'description')!) : undefined,
+ task: (args) => stringArg(args, 'description') ? truncateSummary(stringArg(args, 'description')!) : undefined,
+ ask_user: (args) => stringArg(args, 'question') ? truncateSummary(stringArg(args, 'question')!) : undefined,
+ skill: (args) => stringArg(args, 'skill') ? truncateSummary(stringArg(args, 'skill')!) : undefined,
+ report_intent: (args) => stringArg(args, 'intent') ? truncateSummary(stringArg(args, 'intent')!) : undefined,
+};
+
+function fallbackSummary(args: Record): string | undefined {
+ for (const value of Object.values(args)) {
+ if (typeof value === 'string' && value.trim().length > 0 && value !== '[truncated]') {
+ return truncateSummary(value);
+ }
+ }
+ return undefined;
+}
+
+export function formatToolCallSummary(
+ toolName: string | undefined,
+ toolArguments: Record | undefined,
+): string | undefined {
+ if (!toolName || !toolArguments || Object.keys(toolArguments).length === 0) {
+ return undefined;
+ }
+
+ // Check for GitHub tools (github-*)
+ if (toolName.startsWith('github-')) {
+ return summarizeGitHub(toolName, toolArguments);
+ }
+
+ const summarizer = toolSummarizers[toolName];
+ if (summarizer) {
+ return summarizer(toolArguments, toolName);
+ }
+
+ return fallbackSummary(toolArguments);
+}
+
+export function formatToolArgumentValue(value: unknown): string {
+ if (value === null || value === undefined) return '';
+ if (typeof value === 'string') return value;
+ if (typeof value === 'boolean' || typeof value === 'number') return String(value);
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
+
+/** Keys that are redundant with the label itself or too noisy to display inline. */
+const HIDDEN_ARGUMENT_KEYS = new Set([
+ 'description', // often duplicates the summary
+]);
+
+export function getDisplayableArguments(
+ toolArguments: Record | undefined,
+): Array<[string, unknown]> {
+ if (!toolArguments) return [];
+
+ return Object.entries(toolArguments).filter(
+ ([key, value]) =>
+ !HIDDEN_ARGUMENT_KEYS.has(key)
+ && value !== null
+ && value !== undefined
+ && value !== '',
+ );
+}
diff --git a/src/shared/contracts/sidecar.ts b/src/shared/contracts/sidecar.ts
index 5ddae5f..b9f25e4 100644
--- a/src/shared/contracts/sidecar.ts
+++ b/src/shared/contracts/sidecar.ts
@@ -290,6 +290,7 @@ export interface AgentActivityEvent {
sourceAgentName?: string;
toolName?: string;
toolCallId?: string;
+ toolArguments?: Record;
fileChanges?: ToolCallFileChangePreview[];
}
diff --git a/src/shared/domain/event.ts b/src/shared/domain/event.ts
index 1bbefee..bb566fc 100644
--- a/src/shared/domain/event.ts
+++ b/src/shared/domain/event.ts
@@ -46,6 +46,7 @@ export interface SessionEventRecord {
sourceAgentName?: string;
toolName?: string;
toolCallId?: string;
+ toolArguments?: Record;
fileChanges?: ToolCallFileChangePreview[];
run?: SessionRunRecord;
error?: string;
diff --git a/src/shared/domain/runTimeline.ts b/src/shared/domain/runTimeline.ts
index 2dff5e7..ffa6cf0 100644
--- a/src/shared/domain/runTimeline.ts
+++ b/src/shared/domain/runTimeline.ts
@@ -60,6 +60,7 @@ export interface RunTimelineEventRecord {
targetAgentName?: string;
toolName?: string;
toolCallId?: string;
+ toolArguments?: Record;
fileChanges?: ToolCallFileChangePreview[];
approvalId?: string;
approvalKind?: ApprovalCheckpointKind;
@@ -120,6 +121,7 @@ export interface AppendRunActivityEventInput {
sourceAgentName?: string;
toolName?: string;
toolCallId?: string;
+ toolArguments?: Record;
fileChanges?: ToolCallFileChangePreview[];
}
@@ -824,6 +826,7 @@ export function appendRunActivityEvent(
agentName: agent.agentName ?? existingEvent?.agentName,
toolName: normalizeOptionalString(input.toolName) ?? existingEvent?.toolName,
toolCallId,
+ toolArguments: input.toolArguments ?? existingEvent?.toolArguments,
fileChanges: mergeToolCallFileChanges(existingEvent?.fileChanges, input.fileChanges),
};
return existingIndex >= 0
diff --git a/tests/renderer/toolCallSummary.test.ts b/tests/renderer/toolCallSummary.test.ts
new file mode 100644
index 0000000..704da6c
--- /dev/null
+++ b/tests/renderer/toolCallSummary.test.ts
@@ -0,0 +1,173 @@
+import { describe, expect, test } from 'bun:test';
+
+import {
+ formatToolCallSummary,
+ formatToolArgumentValue,
+ getDisplayableArguments,
+} from '@renderer/lib/toolCallSummary';
+
+describe('formatToolCallSummary', () => {
+ test('returns undefined when toolName is missing', () => {
+ expect(formatToolCallSummary(undefined, { command: 'ls' })).toBeUndefined();
+ });
+
+ test('returns undefined when toolArguments is missing', () => {
+ expect(formatToolCallSummary('powershell', undefined)).toBeUndefined();
+ });
+
+ test('returns undefined when toolArguments is empty', () => {
+ expect(formatToolCallSummary('powershell', {})).toBeUndefined();
+ });
+
+ test('extracts command for powershell', () => {
+ expect(formatToolCallSummary('powershell', { command: 'git status' })).toBe('git status');
+ });
+
+ test('truncates long powershell commands', () => {
+ const longCommand = 'a'.repeat(100);
+ const result = formatToolCallSummary('powershell', { command: longCommand });
+ expect(result!.length).toBeLessThanOrEqual(81); // 80 + ellipsis
+ expect(result!.endsWith('…')).toBe(true);
+ });
+
+ test('extracts path for view tool', () => {
+ expect(formatToolCallSummary('view', { path: '/src/index.ts' })).toBe('/src/index.ts');
+ });
+
+ test('includes view range when present', () => {
+ expect(formatToolCallSummary('view', { path: '/src/index.ts', view_range: [10, 25] }))
+ .toBe('/src/index.ts:10-25');
+ });
+
+ test('supports viewRange camelCase variant', () => {
+ expect(formatToolCallSummary('view', { path: '/src/index.ts', viewRange: [1, 50] }))
+ .toBe('/src/index.ts:1-50');
+ });
+
+ test('extracts path for edit tool', () => {
+ expect(formatToolCallSummary('edit', { path: '/src/utils.ts', old_str: 'foo' }))
+ .toBe('/src/utils.ts');
+ });
+
+ test('extracts path for create tool', () => {
+ expect(formatToolCallSummary('create', { path: '/new-file.ts', file_text: 'content' }))
+ .toBe('/new-file.ts');
+ });
+
+ test('extracts pattern for grep', () => {
+ expect(formatToolCallSummary('grep', { pattern: 'TODO', path: '/src' })).toBe('TODO');
+ });
+
+ test('extracts pattern for glob', () => {
+ expect(formatToolCallSummary('glob', { pattern: '**/*.ts' })).toBe('**/*.ts');
+ });
+
+ test('extracts operation and file for lsp', () => {
+ expect(formatToolCallSummary('lsp', { operation: 'goToDefinition', file: '/src/app.ts' }))
+ .toBe('goToDefinition /src/app.ts');
+ });
+
+ test('extracts just operation when file is missing for lsp', () => {
+ expect(formatToolCallSummary('lsp', { operation: 'workspaceSymbol', query: 'Foo' }))
+ .toBe('workspaceSymbol');
+ });
+
+ test('extracts url for web_fetch', () => {
+ expect(formatToolCallSummary('web_fetch', { url: 'https://example.com' }))
+ .toBe('https://example.com');
+ });
+
+ test('extracts description for sql', () => {
+ expect(formatToolCallSummary('sql', { description: 'Insert todos', query: 'INSERT ...' }))
+ .toBe('Insert todos');
+ });
+
+ test('extracts description for task', () => {
+ expect(formatToolCallSummary('task', { description: 'Run tests' })).toBe('Run tests');
+ });
+
+ test('extracts question for ask_user', () => {
+ expect(formatToolCallSummary('ask_user', { question: 'Which database?' })).toBe('Which database?');
+ });
+
+ test('extracts intent for report_intent', () => {
+ expect(formatToolCallSummary('report_intent', { intent: 'Exploring codebase' }))
+ .toBe('Exploring codebase');
+ });
+
+ test('summarizes github tools with query', () => {
+ expect(formatToolCallSummary('github-search_code', { query: 'FunctionCallContent' }))
+ .toBe('FunctionCallContent');
+ });
+
+ test('summarizes github tools with owner/repo', () => {
+ expect(formatToolCallSummary('github-get_file_contents', { owner: 'octocat', repo: 'hello-world' }))
+ .toBe('octocat/hello-world');
+ });
+
+ test('falls back to first string value for unknown tools', () => {
+ expect(formatToolCallSummary('unknown_tool', { target: 'production', count: 42 }))
+ .toBe('production');
+ });
+
+ test('skips [truncated] values in fallback', () => {
+ expect(formatToolCallSummary('unknown_tool', { data: '[truncated]', label: 'test' }))
+ .toBe('test');
+ });
+
+ test('uses first line only for multiline commands', () => {
+ const result = formatToolCallSummary('powershell', { command: 'echo hello\necho world' });
+ expect(result).toBe('echo hello');
+ });
+});
+
+describe('formatToolArgumentValue', () => {
+ test('formats string values directly', () => {
+ expect(formatToolArgumentValue('hello')).toBe('hello');
+ });
+
+ test('formats numbers', () => {
+ expect(formatToolArgumentValue(42)).toBe('42');
+ });
+
+ test('formats booleans', () => {
+ expect(formatToolArgumentValue(true)).toBe('true');
+ });
+
+ test('formats null as empty string', () => {
+ expect(formatToolArgumentValue(null)).toBe('');
+ });
+
+ test('formats objects as pretty JSON', () => {
+ const result = formatToolArgumentValue({ a: 1, b: 'two' });
+ expect(result).toContain('"a": 1');
+ expect(result).toContain('"b": "two"');
+ });
+
+ test('formats arrays as pretty JSON', () => {
+ const result = formatToolArgumentValue([1, 2, 3]);
+ expect(result).toContain('1');
+ expect(result).toContain('3');
+ });
+});
+
+describe('getDisplayableArguments', () => {
+ test('returns empty array when toolArguments is undefined', () => {
+ expect(getDisplayableArguments(undefined)).toEqual([]);
+ });
+
+ test('filters out null and undefined values', () => {
+ const result = getDisplayableArguments({ a: 'value', b: null, c: undefined, d: 'ok' });
+ expect(result).toEqual([['a', 'value'], ['d', 'ok']]);
+ });
+
+ test('filters out empty string values', () => {
+ const result = getDisplayableArguments({ a: '', b: 'value' });
+ expect(result).toEqual([['b', 'value']]);
+ });
+
+ test('filters out description key', () => {
+ const result = getDisplayableArguments({ description: 'Insert todos', query: 'INSERT ...' });
+ expect(result).toEqual([['query', 'INSERT ...']]);
+ });
+});