diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 2c07ac91b1e..38766b63201 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -545,6 +545,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) } : {}), @@ -568,8 +569,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 } : {}), ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), }, turnId: toTurnId(event.turnId) ?? null, @@ -588,10 +590,11 @@ function runtimeEventToActivities( createdAt: event.createdAt, tone: "tool", kind: "tool.started", - summary: `${event.payload.title ?? "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 28af1cda27b..83951c8c5d4 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -235,7 +235,7 @@ function toCanonicalItemType(raw: string | undefined | null): 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"; @@ -246,7 +246,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": @@ -469,6 +469,7 @@ function mapItemLifecycle( : lifecycle === "item.completed" ? "completed" : undefined; + const title = itemTitle(itemType, lifecycle === "item.started"); return { ...runtimeEventBase(event, canonicalThreadId), @@ -476,7 +477,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/session-logic.ts b/apps/web/src/session-logic.ts index a7767672fa1..07ba656abdb 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -58,7 +58,9 @@ export interface WorkLogEntry { tone: "thinking" | "tool" | "info" | "error"; toolTitle?: string; itemType?: ToolLifecycleItemType; + itemId?: string; requestKind?: PendingApproval["requestKind"]; + collapseKey?: string; } interface DerivedWorkLogEntry extends WorkLogEntry { @@ -487,14 +489,13 @@ 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) => activity.kind !== "task.started") .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, ); } @@ -553,6 +554,7 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo activityKind: activity.kind, }; const itemType = extractWorkLogItemType(payload); + const itemId = extractWorkLogItemId(payload); const requestKind = extractWorkLogRequestKind(payload); if (detail) { entry.detail = detail; @@ -572,6 +574,9 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo if (itemType) { entry.itemType = itemType; } + if (itemId) { + entry.itemId = itemId; + } if (requestKind) { entry.requestKind = requestKind; } @@ -589,13 +594,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; } @@ -604,7 +624,7 @@ 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") { @@ -641,6 +661,8 @@ function mergeDerivedWorkLogEntries( return { ...previous, ...next, + id: previous.id, + createdAt: previous.createdAt, ...(detail ? { detail } : {}), ...(command ? { command } : {}), ...(rawCommand ? { rawCommand } : {}), @@ -665,19 +687,23 @@ 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; } if (entry.toolCallId) { return `tool:${entry.toolCallId}`; } + 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 { @@ -1030,6 +1056,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 {