Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
} from "./FastModeConfig";
import packageJson from "../package.json";
import {isJetBrains2026_1Client} from "./JBUtils";
import {resolveTerminalOutputMode, type TerminalOutputMode} from "./TerminalOutputMode";

export interface SessionState {
sessionId: string,
Expand All @@ -68,6 +69,7 @@ export interface SessionState {
fastModeEnabled: boolean;
currentModelSupportsFast: boolean;
sessionMcpServers?: Array<string>;
terminalOutputMode: TerminalOutputMode;
}

interface PendingMcpStartupSession {
Expand Down Expand Up @@ -100,6 +102,7 @@ export class CodexAcpServer implements acp.Agent {
private readonly getExitCode: () => number | null;
private readonly availableCommands: CodexCommands;
private clientInfo: acp.Implementation | null;
private terminalOutputMode: TerminalOutputMode;

private readonly sessions: Map<string, SessionState>;
private readonly pendingMcpStartupSessions: Map<string, PendingMcpStartupSession>;
Expand Down Expand Up @@ -127,6 +130,7 @@ export class CodexAcpServer implements acp.Agent {
this.defaultAuthRequest = defaultAuthRequest ?? null;
this.getExitCode = getExitCode ?? (() => null);
this.clientInfo = null;
this.terminalOutputMode = "terminal_output_delta";
this.availableCommands = new CodexCommands(
connection,
codexAcpClient,
Expand All @@ -139,6 +143,7 @@ export class CodexAcpServer implements acp.Agent {
): Promise<acp.InitializeResponse> {
logger.log("Initialize request received");
this.clientInfo = _params.clientInfo ?? null;
this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities);
await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
return {
protocolVersion: acp.PROTOCOL_VERSION,
Expand Down Expand Up @@ -349,6 +354,7 @@ export class CodexAcpServer implements acp.Agent {
fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
currentModelSupportsFast: currentModelSupportsFast,
sessionMcpServers: sessionMcpServers,
terminalOutputMode: this.terminalOutputMode,
}
this.sessions.set(sessionId, sessionState);
resumeSubscribed = false;
Expand Down Expand Up @@ -738,6 +744,7 @@ export class CodexAcpServer implements acp.Agent {
fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
currentModelSupportsFast: currentModelSupportsFast,
sessionMcpServers: sessionMcpServers,
terminalOutputMode: this.terminalOutputMode,
};
this.sessions.set(sessionId, sessionState);
subscribed = false;
Expand Down
84 changes: 67 additions & 17 deletions src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import type {
ItemCompletedNotification,
ItemStartedNotification, ThreadItem,
ModelReroutedNotification,
TerminalInteractionNotification,
ThreadTokenUsageUpdatedNotification,
TurnPlanUpdatedNotification,
WarningNotification
} from "./app-server/v2";
import type { McpStartupCompleteEvent } from "./app-server";
import {toTokenCount} from "./TokenCount";
import {
commandExecutionUsesTerminalOutput,
createCommandExecutionUpdate,
createDynamicToolCallUpdate,
createFileChangeUpdate,
Expand All @@ -36,6 +38,7 @@ import {
fuzzyFileSearchToolCallId,
} from "./CodexToolCallMapper";
import { stripShellPrefix } from "./CommandUtils";
import {createTerminalOutputMeta, type TerminalOutputMode} from "./TerminalOutputMode";

export { stripShellPrefix };

Expand All @@ -45,6 +48,8 @@ export class CodexEventHandler {
private readonly sessionState: SessionState;
private failure: RequestError | null = null;
private readonly activeFuzzyFileSearchSessions = new Set<string>();
private readonly terminalCommandIds = new Set<string>();
private readonly terminalCommandOutputIds = new Set<string>();

constructor(connection: acp.AgentSideConnection, sessionState: SessionState) {
this.connection = connection;
Expand Down Expand Up @@ -138,6 +143,8 @@ export class CodexEventHandler {
return this.handleFuzzyFileSearchSessionUpdated(notification.params);
case "fuzzyFileSearch/sessionCompleted":
return this.handleFuzzyFileSearchSessionCompleted(notification.params);
case "item/commandExecution/terminalInteraction":
return this.createTerminalInteractionEvent(notification.params);
// ignored events
case "command/exec/outputDelta":
case "item/autoApprovalReview/started":
Expand All @@ -148,7 +155,6 @@ export class CodexEventHandler {
case "item/reasoning/summaryPartAdded":
case "item/reasoning/textDelta":
case "turn/diff/updated":
case "item/commandExecution/terminalInteraction":
case "item/fileChange/outputDelta":
case "item/fileChange/patchUpdated":
case "account/updated":
Expand Down Expand Up @@ -249,8 +255,15 @@ export class CodexEventHandler {
switch (event.item.type) {
case "fileChange":
return await createFileChangeUpdate(event.item);
case "commandExecution":
case "commandExecution": {
if (commandExecutionUsesTerminalOutput(event.item)) {
this.terminalCommandIds.add(event.item.id);
} else {
this.terminalCommandIds.delete(event.item.id);
this.terminalCommandOutputIds.delete(event.item.id);
}
return await createCommandExecutionUpdate(event.item);
}
case "mcpToolCall":
return await createMcpToolCallUpdate(event.item);
case "dynamicToolCall":
Expand Down Expand Up @@ -316,18 +329,40 @@ export class CodexEventHandler {
}

private createCommandOutputDeltaEvent(event: CommandExecutionOutputDeltaNotification): UpdateSessionEvent {
if (this.terminalCommandIds.has(event.itemId) && event.delta.length > 0) {
this.terminalCommandOutputIds.add(event.itemId);
}
return this.createCommandOutputEvent(event.itemId, event.delta, this.commandOutputMode(event.itemId));
}

private createCommandOutputEvent(
itemId: string,
data: string,
terminalOutputMode: TerminalOutputMode
): UpdateSessionEvent {
return {
sessionUpdate: "tool_call_update",
toolCallId: event.itemId,
_meta: {
terminal_output_delta: {
data: event.delta,
terminal_id: event.itemId
}
}
toolCallId: itemId,
_meta: createTerminalOutputMeta(terminalOutputMode, itemId, data),
}
}

private createTerminalInteractionEvent(event: TerminalInteractionNotification): UpdateSessionEvent {
return this.createCommandOutputDeltaEvent({
threadId: event.threadId,
turnId: event.turnId,
itemId: event.itemId,
delta: `\n${event.stdin}\n`,
});
}

private commandOutputMode(itemId: string): TerminalOutputMode {
if (this.sessionState.terminalOutputMode === "terminal_output" && !this.terminalCommandIds.has(itemId)) {
return "terminal_output_delta";
}
return this.sessionState.terminalOutputMode;
}

private createMcpToolProgressEvent(event: { itemId: string, message: string }): UpdateSessionEvent {
const logDelta = event.message.trim();
return {
Expand Down Expand Up @@ -376,22 +411,37 @@ export class CodexEventHandler {
}

private completeCommandExecutionEvent(item: ThreadItem & { "type": "commandExecution" }): UpdateSessionEvent {
return {
const update: UpdateSessionEvent = {
sessionUpdate: "tool_call_update",
toolCallId: item.id,
status: item.status === "completed" ? "completed" : "failed",
rawOutput: {
formatted_output: item.aggregatedOutput ?? "",
exit_code: item.exitCode
},
_meta: {
terminal_exit: {
exit_code: item.exitCode,
signal: null,
terminal_id: item.id
}
}
};

const commandHadTerminal = this.terminalCommandIds.delete(item.id);
const commandHadOutput = this.terminalCommandOutputIds.delete(item.id);
if (!commandHadTerminal) {
return update;
}
const terminalMeta: Record<string, unknown> = {};
if (!commandHadOutput && item.aggregatedOutput) {
Object.assign(
terminalMeta,
createTerminalOutputMeta(this.sessionState.terminalOutputMode, item.id, item.aggregatedOutput)
);
}
terminalMeta["terminal_exit"] = {
exit_code: item.exitCode,
signal: null,
terminal_id: item.id
};
return {
...update,
_meta: terminalMeta,
};
}

private async updatePlan(event: TurnPlanUpdatedNotification): Promise<UpdateSessionEvent> {
Expand Down
115 changes: 64 additions & 51 deletions src/CodexToolCallMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {logger} from "./Logger";

type CodexItemStatus = CommandExecutionStatus | PatchApplyStatus | McpToolCallStatus | DynamicToolCallStatus;
type AcpToolCallStatus = "pending" | "in_progress" | "completed" | "failed";
type CommandExecutionItem = ThreadItem & { type: "commandExecution" };
type AcpToolCallEvent = Extract<UpdateSessionEvent, { sessionUpdate: "tool_call" }>;

function toAcpStatus(status: CodexItemStatus): AcpToolCallStatus {
switch (status) {
Expand Down Expand Up @@ -56,32 +58,23 @@ export async function createFileChangeUpdate(
};
}

export async function createCommandExecutionUpdate(
item: ThreadItem & { type: "commandExecution" }
): Promise<UpdateSessionEvent> {
export async function createCommandExecutionUpdate(item: CommandExecutionItem): Promise<UpdateSessionEvent> {
const commandAction = item.commandActions.length === 1 ? item.commandActions[0] : undefined;
if (commandAction) {
return createCommandActionEvent(item.id, item.status, item.cwd, commandAction);
}
const command = stripShellPrefix(item.command);
return {
return createTerminalCommandEvent({
sessionUpdate: "tool_call",
toolCallId: item.id,
kind: "execute",
title: command,
status: toAcpStatus(item.status),
content: [{ type: "terminal", terminalId: item.id }],
rawInput: {
command: item.command,
cwd: item.cwd,
},
_meta: {
terminal_info: {
cwd: item.cwd,
terminal_id: item.id,
},
},
};
}, item.id, item.cwd);
}

export async function createMcpToolCallUpdate(
Expand Down Expand Up @@ -197,50 +190,70 @@ function createCommandActionEvent(
commandAction: CommandAction
): UpdateSessionEvent {
const acpStatus = toAcpStatus(status);
if (commandAction.type === "read") {
return {
sessionUpdate: "tool_call",
toolCallId: id,
status: acpStatus,
kind: "read",
title: `Read file '${commandAction.path}'`,
locations: [{ path: commandAction.path }],
};
} else if (commandAction.type === "search") {
return {
sessionUpdate: "tool_call",
toolCallId: id,
status: acpStatus,
kind: "search",
title: createSearchTitle(commandAction.query, commandAction.path),
};
} else if (commandAction.type === "listFiles") {
const title = commandAction.path
? `List files in '${commandAction.path}'`
: "List files";
return {
sessionUpdate: "tool_call",
toolCallId: id,
status: acpStatus,
kind: "read",
title: title,
};
switch (commandAction.type) {
case "read":
return {
sessionUpdate: "tool_call",
toolCallId: id,
status: acpStatus,
kind: "read",
title: `Read file '${commandAction.path}'`,
locations: [{ path: commandAction.path }],
};
case "search":
return {
sessionUpdate: "tool_call",
toolCallId: id,
status: acpStatus,
kind: "search",
title: createSearchTitle(commandAction.query, commandAction.path),
};
case "listFiles": {
const title = commandAction.path
? `List files in '${commandAction.path}'`
: "List files";
return {
sessionUpdate: "tool_call",
toolCallId: id,
status: acpStatus,
kind: "read",
title: title,
};
}
case "unknown":
return createTerminalCommandEvent({
sessionUpdate: "tool_call",
toolCallId: id,
status: acpStatus,
kind: "execute",
title: stripShellPrefix(commandAction.command),
rawInput: {
command: commandAction.command,
cwd,
},
}, id, cwd);
}
}

export function commandExecutionUsesTerminalOutput(item: CommandExecutionItem): boolean {
const commandAction = item.commandActions.length === 1 ? item.commandActions[0] : undefined;
return commandAction === undefined || commandAction.type === "unknown";
}

function createTerminalCommandEvent(
event: AcpToolCallEvent,
terminalId: string,
cwd: string,
): UpdateSessionEvent {
const { rawInput, ...eventWithoutRawInput } = event;
return {
sessionUpdate: "tool_call",
toolCallId: id,
status: acpStatus,
kind: "execute",
title: stripShellPrefix(commandAction.command),
content: [{ type: "terminal", terminalId: id }],
rawInput: {
command: commandAction.command,
cwd,
},
...eventWithoutRawInput,
content: [{ type: "terminal", terminalId }],
...(rawInput === undefined ? {} : { rawInput }),
_meta: {
terminal_info: {
cwd,
terminal_id: id,
terminal_id: terminalId,
},
},
};
Expand Down
Loading
Loading