src/vs/platform/agentHost/node/codex/codexReplayMapper.ts
217 LOC · 170 covered · 47 uncovered · 30 ranges · 36 concepts · 10 introducers · 20 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
codexReplayMapper.ts ×6
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { generateUuid } from '../../../../base/common/uuid.js';
import { toToolCallMeta } from '../../common/meta/agentToolCallMeta.js';
import {
MessageKind,
ResponsePartKind,
ToolCallConfirmationReason,
ToolCallStatus,
ToolResultContentType,
type ResponsePart,
type ToolCallResponsePart,
type ToolResultContent,
type Turn,
} from '../../common/state/sessionState.js';
import {
describeFileChange,
describeWebSearch,
fileChangeOutput,
turnStateFromStatus,
} from './codexMapAppServerEvents.js';
import { unwrapShellInvocation } from './codexShellCommand.js';
import type { Thread } from './protocol/generated/v2/Thread.js';
import type { ThreadItem } from './protocol/generated/v2/ThreadItem.js';
import type { Turn as CodexTurn } from './protocol/generated/v2/Turn.js';
/**
* Reconstruct protocol {@link Turn}s from codex's `thread/read` response.
*
* Codex stores each conversation as a stream of {@link CodexTurn}, each
* with an array of {@link ThreadItem}s. We collapse that into the agent
* host's turn shape: each user message opens a turn; subsequent assistant
* items become response parts on that turn until `turn/completed` closes it.
*
* Produces:
* - `userMessage` → opens a `Turn` with `userMessage: { text }`
* - `agentMessage` → `MarkdownResponsePart` with the full text
* - `commandExecution` → completed terminal `ToolCallResponsePart`
* - `webSearch` → completed web-search `ToolCallResponsePart`
* - `fileChange` → completed file-edit `ToolCallResponsePart`
* - everything else → currently dropped (reasoning/plan/mcp/collab)
*
* Mirrors the live mapper's translation kernel — including the sandbox
* pre-flight coalescing (see {@link codexMapAppServerEvents}) — so restored
* sessions render identically to active ones.
*/
export function replayThreadToTurns(thread: Thread): Turn[] {
for (const codexTurn of thread.turns ?? []) {
if (turn) {
}
}
/** A completed `commandExecution` item narrowed to its terminal fields. */
type CommandExecutionItem = Extract<ThreadItem, { type: 'commandExecution' }>;
let userText = '';
const parts: ResponsePart[] = [];
// A successful command that produced no output may be a sandbox pre-flight
// that codex immediately re-ran under an approval prompt (same command, new
// item). Defer emitting it so the re-run can coalesce into a single box —
// mirroring the live mapper's `pendingPreflight` state machine.
let pendingPreflight: { command: string; item: CommandExecutionItem } | undefined;
const flushPreflight = () => {
if (pendingPreflight) {
parts.push(shellToolCallPart(pendingPreflight.item, pendingPreflight.command));
pendingPreflight = undefined;
}
for (const item of codexTurn.items ?? []) {
if (item.type === 'commandExecution') {
if (pendingPreflight && pendingPreflight.command === command) {
// item (it carries the real output/approval), dropping the
// output-less pre-flight box.
pendingPreflight = undefined;
parts.push(shellToolCallPart(item, command));
continue;
}
const success = item.status === 'completed' && (item.exitCode === 0 || item.exitCode === null);
const output = item.aggregatedOutput ?? '';
if (success && !output) {
continue;
}
continue;
}
// Any other item supersedes a deferred pre-flight: finalize it first so
// a genuinely output-less command still renders as a single box.
flushPreflight();
if (item.type === 'userMessage') {
for (const c of item.content) {
if (c.type === 'text') {
collected.push(c.text);
}
}
if (collected.length > 0) {
userText = collected.join('\n\n');
}
parts.push({
kind: ResponsePartKind.Markdown,
id: generateUuid(),
content: item.text,
});
}
parts.push(webSearchToolCallPart(item));
parts.push(fileChangeToolCallPart(item));
}
// Other item types (plan/reasoning/mcpToolCall/collabAgentToolCall/…)
codexReplayMapper.ts ×7
// are not yet reconstructed in replay.
}
flushPreflight();
// If we got nothing recognizable, drop the turn — there's nothing for
// the UI to render.
if (!userText && parts.length === 0) {
}
id: codexTurn.id,
message: { text: userText, origin: { kind: MessageKind.User } },
responseParts: parts,
usage: undefined,
state: turnStateFromStatus(codexTurn.status),
};
}
function textContent(output: string): ToolResultContent[] | undefined {
codexReplayMapper.ts ×5
return output ? [{ type: ToolResultContentType.Text, text: output }] : undefined;
}
function shellToolCallPart(item: CommandExecutionItem, command: string): ToolCallResponsePart {
codexReplayMapper.ts ×5
const success = item.status === 'completed' && (item.exitCode === 0 || item.exitCode === null);
const output = item.aggregatedOutput ?? '';
const exit = item.exitCode;
const pastTense = success
? `Ran \`${command}\``
: exit !== null
? `Ran \`${command}\` (exit ${exit})`
: `Ran \`${command}\` (failed)`;
kind: ResponsePartKind.ToolCall,
toolCall: {
status: ToolCallStatus.Completed,
toolCallId: generateUuid(),
toolName: 'shell',
displayName: 'Run shell command',
_meta: toToolCallMeta({ toolKind: 'terminal' }),
invocationMessage: command,
toolInput: command,
confirmed: ToolCallConfirmationReason.NotNeeded,
success,
pastTenseMessage: pastTense,
content: textContent(output),
error: success ? undefined : { message: exit !== null ? `Exit code ${exit}` : 'Command failed' },
},
};
}
function webSearchToolCallPart(item: Extract<ThreadItem, { type: 'webSearch' }>): ToolCallResponsePart {
const query = describeWebSearch(item.query, item.action);
return {
kind: ResponsePartKind.ToolCall,
toolCall: {
status: ToolCallStatus.Completed,
toolCallId: generateUuid(),
toolName: 'web_search',
displayName: 'Web search',
_meta: toToolCallMeta({ toolKind: 'search' }),
invocationMessage: query,
toolInput: query,
confirmed: ToolCallConfirmationReason.NotNeeded,
success: true,
pastTenseMessage: `Searched ${query}`,
},
};
}
function fileChangeToolCallPart(item: Extract<ThreadItem, { type: 'fileChange' }>): ToolCallResponsePart {
const success = item.status === 'completed';
const summary = describeFileChange(item.changes) || 'Apply file changes';
const output = fileChangeOutput(item.changes);
return {
kind: ResponsePartKind.ToolCall,
toolCall: {
status: ToolCallStatus.Completed,
toolCallId: generateUuid(),
toolName: 'file_edit',
displayName: 'Apply file changes',
invocationMessage: summary,
confirmed: ToolCallConfirmationReason.NotNeeded,
success,
pastTenseMessage: success ? 'Applied file changes' : 'Failed to apply file changes',
content: textContent(output),
error: success ? undefined : { message: `Patch ${item.status}` },
},
};
}