diff --git a/packages/server/src/server/agent/activity-curator.test.ts b/packages/server/src/server/agent/activity-curator.test.ts index 6dad2d9e0..cceebd458 100644 --- a/packages/server/src/server/agent/activity-curator.test.ts +++ b/packages/server/src/server/agent/activity-curator.test.ts @@ -73,6 +73,29 @@ describe("curateAgentActivity", () => { expect(result).toContain("[Shell] npm test"); }); + it("renders terminal tool calls as one-line command summaries", () => { + const timeline: AgentTimelineItem[] = [ + toolCallItem({ + callId: "terminal-1", + name: "terminal", + detail: { + type: "plain_text", + label: `skills/paseo-chat/bin/chat.sh post --room storage-revamp --body $'first line + +second line'`, + icon: "square_terminal", + }, + }), + ]; + + const result = curateAgentActivity(timeline); + + expect(result).toContain( + "[Terminal] skills/paseo-chat/bin/chat.sh post --room storage-revamp --body $'first line second line'", + ); + expect(result).not.toContain("[Interacted with terminal]"); + }); + it("does not infer summary from raw input when detail is missing", () => { const timeline: AgentTimelineItem[] = [ toolCallItem({ diff --git a/packages/server/src/server/agent/activity-curator.ts b/packages/server/src/server/agent/activity-curator.ts index 08aa6a705..bc3384dbc 100644 --- a/packages/server/src/server/agent/activity-curator.ts +++ b/packages/server/src/server/agent/activity-curator.ts @@ -4,6 +4,7 @@ import { buildToolCallDisplayModel } from "../../shared/tool-call-display.js"; const DEFAULT_MAX_ITEMS = 40; const MAX_TOOL_INPUT_CHARS = 400; +const MAX_TOOL_SUMMARY_CHARS = 200; function appendText(buffer: string, text: string): string { const normalized = text.trim(); @@ -45,6 +46,20 @@ function formatToolInputJson(input: unknown): string | null { } } +function formatToolSummary(summary: string | undefined): string | null { + if (typeof summary !== "string") { + return null; + } + const normalized = summary.replace(/\s+/g, " ").trim(); + if (!normalized) { + return null; + } + if (normalized.length <= MAX_TOOL_SUMMARY_CHARS) { + return normalized; + } + return `${normalized.slice(0, MAX_TOOL_SUMMARY_CHARS - 3)}...`; +} + function hasNonEmptyObject(value: unknown): boolean { return ( typeof value === "object" && @@ -215,7 +230,7 @@ export function curateAgentActivity( metadata: item.metadata, }); const displayName = display.displayName; - const summary = display.summary; + const summary = formatToolSummary(display.summary); if (isLikelyExternalToolName(item.name) && inputJson) { lines.push(`[${displayName}] ${inputJson}`); break; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 619a2bdd1..4b7ed2f3b 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -2039,6 +2039,7 @@ class CodexAppServerAgentSession implements AgentSession { private pendingCommandOutputDeltas = new Map(); private pendingFileChangeOutputDeltas = new Map(); private terminalCommandByProcessId = new Map(); + private pendingUnlabeledTerminalInteractions = new Set(); private emittedTerminalInteractionKeys = new Set(); private emittedExecCommandStartedCallIds = new Set(); private emittedExecCommandCompletedCallIds = new Set(); @@ -3000,12 +3001,16 @@ class CodexAppServerAgentSession implements AgentSession { if (!this.shouldEmitTerminalInteractionKey(interactionKey)) { return; } + const command = + (parsed.processId ? this.terminalCommandByProcessId.get(parsed.processId) : undefined) ?? + null; + if (!command && parsed.processId) { + this.pendingUnlabeledTerminalInteractions.add(parsed.processId); + } const timelineItem = mapCodexTerminalInteractionToToolCall({ processId: parsed.processId, fallbackCallId: parsed.callId, - command: - (parsed.processId ? this.terminalCommandByProcessId.get(parsed.processId) : undefined) ?? - null, + command, }); this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem }); return; @@ -3217,6 +3222,18 @@ class CodexAppServerAgentSession implements AgentSession { return; } this.terminalCommandByProcessId.set(processId, displayCommand); + if (!this.pendingUnlabeledTerminalInteractions.has(processId)) { + return; + } + this.pendingUnlabeledTerminalInteractions.delete(processId); + this.emitEvent({ + type: "timeline", + provider: CODEX_PROVIDER, + item: mapCodexTerminalInteractionToToolCall({ + processId, + command: displayCommand, + }), + }); } private shouldEmitTerminalInteractionKey(key: string): boolean { diff --git a/packages/server/src/shared/tool-call-display.test.ts b/packages/server/src/shared/tool-call-display.test.ts index de5839691..136d9e851 100644 --- a/packages/server/src/shared/tool-call-display.test.ts +++ b/packages/server/src/shared/tool-call-display.test.ts @@ -119,7 +119,7 @@ describe("shared tool-call display mapping", () => { }); expect(display).toEqual({ - displayName: "Interacted with terminal", + displayName: "Terminal", }); }); @@ -136,7 +136,7 @@ describe("shared tool-call display mapping", () => { }); expect(display).toEqual({ - displayName: "Interacted with terminal", + displayName: "Terminal", summary: "npm run test", }); }); diff --git a/packages/server/src/shared/tool-call-display.ts b/packages/server/src/shared/tool-call-display.ts index 5b10c8370..1ba55b734 100644 --- a/packages/server/src/shared/tool-call-display.ts +++ b/packages/server/src/shared/tool-call-display.ts @@ -129,7 +129,7 @@ function buildUnknownDetailOverride(input: ToolCallDisplayInput): DetailDisplay } if (lowerName === "terminal") { return { - displayName: "Interacted with terminal", + displayName: "Terminal", summary: input.detail.type === "plain_text" ? readString(input.detail.label) : undefined, }; }