diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 8d3fb5d752..48bce1414e 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -440,6 +440,7 @@ function runtimeEventToActivities( kind: "tool.updated", summary: event.payload.title ?? "Tool updated", payload: { + ...(event.itemId ? { itemId: event.itemId } : {}), itemType: event.payload.itemType, ...(event.payload.status ? { status: event.payload.status } : {}), ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), @@ -463,8 +464,9 @@ function runtimeEventToActivities( kind: "tool.completed", summary: event.payload.title ?? "Tool", payload: { + ...(event.itemId ? { itemId: event.itemId } : {}), itemType: event.payload.itemType, - ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), + ...(event.payload.detail ? { detail: event.payload.detail } : {}), }, turnId: toTurnId(event.turnId) ?? null, ...maybeSequence, @@ -484,8 +486,9 @@ function runtimeEventToActivities( kind: "tool.started", summary: `${event.payload.title ?? "Tool"} started`, payload: { + ...(event.itemId ? { itemId: event.itemId } : {}), itemType: event.payload.itemType, - ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), + ...(event.payload.detail ? { detail: event.payload.detail } : {}), }, turnId: toTurnId(event.turnId) ?? null, ...maybeSequence, diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 8b9f3b59e7..c936fe86a2 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -213,7 +213,10 @@ function toCanonicalItemType(raw: unknown): CanonicalItemType { return "unknown"; } -function itemTitle(itemType: CanonicalItemType): string | undefined { +function itemTitle( + itemType: CanonicalItemType, + started = false, +): string | undefined { switch (itemType) { case "assistant_message": return "Assistant message"; @@ -224,7 +227,7 @@ function itemTitle(itemType: CanonicalItemType): string | undefined { case "plan": return "Plan"; case "command_execution": - return "Ran command"; + return started ? "Running command" : "Ran command"; case "file_change": return "File change"; case "mcp_tool_call": @@ -562,6 +565,7 @@ function mapItemLifecycle( : lifecycle === "item.completed" ? "completed" : undefined; + const title = itemTitle(itemType, lifecycle === "item.started"); return { ...runtimeEventBase(event, canonicalThreadId), @@ -569,7 +573,7 @@ function mapItemLifecycle( payload: { itemType, ...(status ? { status } : {}), - ...(itemTitle(itemType) ? { title: itemTitle(itemType) } : {}), + ...(title ? { title } : {}), ...(detail ? { detail } : {}), ...(event.payload !== undefined ? { data: event.payload } : {}), }, diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index e823569c13..daaa3baa70 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -346,7 +346,10 @@ export const MessagesTimeline = memo(function MessagesTimeline({ )}
{visibleEntries.map((workEntry) => ( - + ))}
diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index fc33827014..b745f3ec57 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -42,7 +42,9 @@ export interface WorkLogEntry { tone: "thinking" | "tool" | "info" | "error"; toolTitle?: string; itemType?: ToolLifecycleItemType; + itemId?: string; requestKind?: PendingApproval["requestKind"]; + collapseKey?: string; } interface DerivedWorkLogEntry extends WorkLogEntry { @@ -461,14 +463,23 @@ export function deriveWorkLogEntries( const ordered = [...activities].toSorted(compareActivitiesByOrder); const entries = ordered .filter((activity) => (latestTurnId ? activity.turnId === latestTurnId : true)) - .filter((activity) => activity.kind !== "tool.started") + .filter((activity) => { + if (activity.kind !== "tool.started") { + return true; + } + const payload = + activity.payload && typeof activity.payload === "object" + ? (activity.payload as Record) + : null; + return payload?.itemType === "command_execution"; + }) .filter((activity) => activity.kind !== "task.started" && activity.kind !== "task.completed") .filter((activity) => activity.kind !== "context-window.updated") .filter((activity) => activity.summary !== "Checkpoint captured") .filter((activity) => !isPlanBoundaryToolActivity(activity)) .map(toDerivedWorkLogEntry); return collapseDerivedWorkLogEntries(entries).map( - ({ activityKind: _activityKind, collapseKey: _collapseKey, ...entry }) => entry, + ({ activityKind: _activityKind, ...entry }) => entry, ); } @@ -500,6 +511,7 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo activityKind: activity.kind, }; const itemType = extractWorkLogItemType(payload); + const itemId = extractWorkLogItemId(payload); const requestKind = extractWorkLogRequestKind(payload); if (payload && typeof payload.detail === "string" && payload.detail.length > 0) { const detail = stripTrailingExitCode(payload.detail).output; @@ -519,6 +531,9 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo if (itemType) { entry.itemType = itemType; } + if (itemId) { + entry.itemId = itemId; + } if (requestKind) { entry.requestKind = requestKind; } @@ -533,13 +548,28 @@ function collapseDerivedWorkLogEntries( entries: ReadonlyArray, ): DerivedWorkLogEntry[] { const collapsed: DerivedWorkLogEntry[] = []; + const openLifecycleRowIndexByCollapseKey = new Map(); for (const entry of entries) { - const previous = collapsed.at(-1); - if (previous && shouldCollapseToolLifecycleEntries(previous, entry)) { - collapsed[collapsed.length - 1] = mergeDerivedWorkLogEntries(previous, entry); - continue; + const collapseKey = entry.collapseKey; + if (collapseKey) { + const openIndex = openLifecycleRowIndexByCollapseKey.get(collapseKey); + if (openIndex !== undefined) { + const previous = collapsed[openIndex]; + if (previous && shouldCollapseToolLifecycleEntries(previous, entry)) { + collapsed[openIndex] = mergeDerivedWorkLogEntries(previous, entry); + if (entry.activityKind === "tool.completed") { + openLifecycleRowIndexByCollapseKey.delete(collapseKey); + } + continue; + } + } } + collapsed.push(entry); + if (collapseKey && (entry.activityKind === "tool.started" || entry.activityKind === "tool.updated")) { + openLifecycleRowIndexByCollapseKey.set(collapseKey, collapsed.length - 1); + continue; + } } return collapsed; } @@ -548,7 +578,11 @@ function shouldCollapseToolLifecycleEntries( previous: DerivedWorkLogEntry, next: DerivedWorkLogEntry, ): boolean { - if (previous.activityKind !== "tool.updated" && previous.activityKind !== "tool.completed") { + if ( + previous.activityKind !== "tool.started" && + previous.activityKind !== "tool.updated" && + previous.activityKind !== "tool.completed" + ) { return false; } if (next.activityKind !== "tool.updated" && next.activityKind !== "tool.completed") { @@ -596,16 +630,24 @@ function mergeChangedFiles( } function deriveToolLifecycleCollapseKey(entry: DerivedWorkLogEntry): string | undefined { - if (entry.activityKind !== "tool.updated" && entry.activityKind !== "tool.completed") { + if ( + entry.activityKind !== "tool.started" && + entry.activityKind !== "tool.updated" && + entry.activityKind !== "tool.completed" + ) { return undefined; } + const itemId = entry.itemId?.trim() ?? ""; + if (itemId.length > 0) { + return itemId; + } const normalizedLabel = normalizeCompactToolLabel(entry.toolTitle ?? entry.label); - const detail = entry.detail?.trim() ?? ""; + const commandOrDetail = (entry.command ?? entry.detail)?.trim() ?? ""; const itemType = entry.itemType ?? ""; - if (normalizedLabel.length === 0 && detail.length === 0 && itemType.length === 0) { + if (normalizedLabel.length === 0 && commandOrDetail.length === 0 && itemType.length === 0) { return undefined; } - return [itemType, normalizedLabel, detail].join("\u001f"); + return [itemType, normalizedLabel, commandOrDetail].join("\u001f"); } function normalizeCompactToolLabel(value: string): string { @@ -698,6 +740,10 @@ function extractWorkLogItemType( return undefined; } +function extractWorkLogItemId(payload: Record | null): string | undefined { + return typeof payload?.itemId === "string" && payload.itemId.length > 0 ? payload.itemId : undefined; +} + function extractWorkLogRequestKind( payload: Record | null, ): WorkLogEntry["requestKind"] | undefined {