Skip to content

Commit c71803c

Browse files
committed
fix: Map Codex image events to ACP tool calls
Handle more events around images. Ports over similar logic on the Rust side.
1 parent 1d76251 commit c71803c

9 files changed

Lines changed: 477 additions & 23 deletions

src/CodexAcpServer.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import {
3838
createCommandExecutionUpdate,
3939
createDynamicToolCallUpdate,
4040
createFileChangeUpdate,
41+
createImageGenerationUpdate,
42+
createImageViewUpdate,
4143
createMcpToolCallUpdate,
4244
} from "./CodexToolCallMapper";
4345
import {
@@ -800,9 +802,9 @@ export class CodexAcpServer implements acp.Agent {
800802
case "webSearch":
801803
return [this.createWebSearchUpdate(item)];
802804
case "imageView":
803-
return [this.createImageViewUpdate(item)];
805+
return [createImageViewUpdate(item)];
804806
case "imageGeneration":
805-
return [];
807+
return [createImageGenerationUpdate(item)];
806808
case "enteredReviewMode":
807809
return [this.createReviewModeUpdate(item, true)];
808810
case "exitedReviewMode":
@@ -871,22 +873,6 @@ export class CodexAcpServer implements acp.Agent {
871873
};
872874
}
873875

874-
private createImageViewUpdate(
875-
item: ThreadItem & { type: "imageView" }
876-
): UpdateSessionEvent {
877-
return {
878-
sessionUpdate: "tool_call",
879-
toolCallId: item.id,
880-
kind: "read",
881-
title: "View image",
882-
status: "completed",
883-
locations: [{ path: item.path }],
884-
rawInput: {
885-
path: item.path,
886-
},
887-
};
888-
}
889-
890876
private createReviewModeUpdate(
891877
item: ThreadItem & { type: "enteredReviewMode" | "exitedReviewMode" },
892878
entered: boolean

src/CodexEventHandler.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import {
2828
createCommandExecutionUpdate,
2929
createDynamicToolCallUpdate,
3030
createFileChangeUpdate,
31+
createImageGenerationCompleteUpdate,
32+
createImageGenerationStartUpdate,
33+
createImageGenerationUpdate,
34+
createImageViewUpdate,
3135
createMcpRawInput,
3236
createMcpRawOutput,
3337
createFuzzyFileSearchComplete,
@@ -45,6 +49,8 @@ export class CodexEventHandler {
4549
private readonly sessionState: SessionState;
4650
private failure: RequestError | null = null;
4751
private readonly activeFuzzyFileSearchSessions = new Set<string>();
52+
private readonly activeImageGenerationItems = new Set<string>();
53+
private readonly emittedImageViewItems = new Set<string>();
4854

4955
constructor(connection: acp.AgentSideConnection, sessionState: SessionState) {
5056
this.connection = connection;
@@ -255,14 +261,18 @@ export class CodexEventHandler {
255261
return await createMcpToolCallUpdate(event.item);
256262
case "dynamicToolCall":
257263
return await createDynamicToolCallUpdate(event.item);
264+
case "imageView":
265+
this.emittedImageViewItems.add(event.item.id);
266+
return createImageViewUpdate(event.item);
267+
case "imageGeneration":
268+
this.activeImageGenerationItems.add(event.item.id);
269+
return createImageGenerationStartUpdate(event.item);
258270
case "collabAgentToolCall":
259271
case "userMessage":
260272
case "hookPrompt":
261273
case "agentMessage":
262274
case "reasoning":
263275
case "webSearch":
264-
case "imageView":
265-
case "imageGeneration":
266276
case "enteredReviewMode":
267277
case "exitedReviewMode":
268278
case "contextCompaction":
@@ -290,6 +300,16 @@ export class CodexEventHandler {
290300
}
291301
case "commandExecution":
292302
return this.completeCommandExecutionEvent(event.item);
303+
case "imageView":
304+
if (this.emittedImageViewItems.delete(event.item.id)) {
305+
return null;
306+
}
307+
return createImageViewUpdate(event.item);
308+
case "imageGeneration":
309+
if (this.activeImageGenerationItems.delete(event.item.id)) {
310+
return createImageGenerationCompleteUpdate(event.item);
311+
}
312+
return createImageGenerationUpdate(event.item);
293313
case "reasoning":
294314
const summary = event.item.summary[0];
295315
if (!summary) return null;
@@ -305,8 +325,6 @@ export class CodexEventHandler {
305325
case "hookPrompt":
306326
case "agentMessage":
307327
case "webSearch":
308-
case "imageView":
309-
case "imageGeneration":
310328
case "enteredReviewMode":
311329
case "exitedReviewMode":
312330
case "contextCompaction":

src/CodexToolCallMapper.ts

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ToolCallContent } from "@agentclientprotocol/sdk";
1+
import type { ContentBlock, ToolCallContent } from "@agentclientprotocol/sdk";
22
import { applyPatch, parsePatch, reversePatch } from "diff";
33
import { readFile } from "node:fs/promises";
44
import path from "node:path";
@@ -104,6 +104,69 @@ export async function createDynamicToolCallUpdate(
104104
return createExecuteToolCallUpdate(item, item.tool, { arguments: item.arguments })
105105
}
106106

107+
export function createImageViewUpdate(
108+
item: ThreadItem & { type: "imageView" }
109+
): UpdateSessionEvent {
110+
const displayPath = item.path;
111+
return {
112+
sessionUpdate: "tool_call",
113+
toolCallId: item.id,
114+
kind: "read",
115+
title: `View Image ${displayPath}`,
116+
status: "completed",
117+
content: [createContent({
118+
type: "resource_link",
119+
name: displayPath,
120+
uri: displayPath,
121+
})],
122+
locations: [{ path: item.path }],
123+
rawInput: {
124+
path: item.path,
125+
},
126+
};
127+
}
128+
129+
export function createImageGenerationStartUpdate(
130+
item: ThreadItem & { type: "imageGeneration" }
131+
): UpdateSessionEvent {
132+
return {
133+
sessionUpdate: "tool_call",
134+
toolCallId: item.id,
135+
kind: "other",
136+
title: "Image generation",
137+
status: "in_progress",
138+
rawInput: {
139+
id: item.id,
140+
},
141+
};
142+
}
143+
144+
export function createImageGenerationCompleteUpdate(
145+
item: ThreadItem & { type: "imageGeneration" }
146+
): UpdateSessionEvent {
147+
return {
148+
sessionUpdate: "tool_call_update",
149+
toolCallId: item.id,
150+
status: imageGenerationToolStatus(item.status),
151+
content: imageGenerationContent(item),
152+
rawOutput: imageGenerationRawOutput(item),
153+
};
154+
}
155+
156+
export function createImageGenerationUpdate(
157+
item: ThreadItem & { type: "imageGeneration" }
158+
): UpdateSessionEvent {
159+
return {
160+
sessionUpdate: "tool_call",
161+
toolCallId: item.id,
162+
kind: "other",
163+
title: "Image generation",
164+
status: imageGenerationToolStatus(item.status),
165+
content: imageGenerationContent(item),
166+
rawOutput: imageGenerationRawOutput(item),
167+
};
168+
}
169+
107170
export async function createExecuteToolCallUpdate(
108171
item: ThreadItem & ({ type: "mcpToolCall" } | { type: "dynamicToolCall" }),
109172
title: string,
@@ -257,6 +320,74 @@ function createSearchTitle(query: string | null, path: string | null): string {
257320
return "Search";
258321
}
259322

323+
function imageGenerationToolStatus(status: string): AcpToolCallStatus {
324+
switch (status) {
325+
case "completed":
326+
return "completed";
327+
case "generating":
328+
case "in_progress":
329+
case "inProgress":
330+
case "incomplete":
331+
return "in_progress";
332+
case "failed":
333+
return "failed";
334+
default:
335+
return "completed";
336+
}
337+
}
338+
339+
function imageGenerationContent(
340+
item: ThreadItem & { type: "imageGeneration" }
341+
): ToolCallContent[] {
342+
const content: ToolCallContent[] = [];
343+
344+
if (item.revisedPrompt && item.revisedPrompt.trim() !== "") {
345+
content.push(createContent({
346+
type: "text",
347+
text: `Revised prompt: ${item.revisedPrompt}`,
348+
}));
349+
}
350+
351+
if (item.result.trim() !== "") {
352+
const image: ContentBlock = item.savedPath && item.savedPath.trim() !== ""
353+
? {
354+
type: "image",
355+
data: item.result,
356+
mimeType: "image/png",
357+
uri: item.savedPath,
358+
}
359+
: {
360+
type: "image",
361+
data: item.result,
362+
mimeType: "image/png",
363+
};
364+
content.push(createContent(image));
365+
}
366+
367+
return content;
368+
}
369+
370+
function imageGenerationRawOutput(
371+
item: ThreadItem & { type: "imageGeneration" }
372+
): Record<string, string | null> {
373+
const output: Record<string, string | null> = {
374+
status: item.status,
375+
revisedPrompt: item.revisedPrompt,
376+
result: item.result,
377+
};
378+
if ("savedPath" in item) {
379+
output["savedPath"] = item.savedPath ?? null;
380+
}
381+
return output;
382+
}
383+
384+
function createContent(content: ContentBlock): ToolCallContent {
385+
return {
386+
type: "content",
387+
content,
388+
};
389+
}
390+
260391
async function createPatchContent(change: FileUpdateChange): Promise<ToolCallContent | null> {
261392
try {
262393
switch (change.kind.type) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"method": "sessionUpdate",
3+
"args": [
4+
{
5+
"sessionId": "test-session-id",
6+
"update": {
7+
"sessionUpdate": "tool_call",
8+
"toolCallId": "image-generation-completed-only",
9+
"kind": "other",
10+
"title": "Image generation",
11+
"status": "completed",
12+
"content": [
13+
{
14+
"type": "content",
15+
"content": {
16+
"type": "image",
17+
"data": "iVBORw0KGgo=",
18+
"mimeType": "image/png"
19+
}
20+
}
21+
],
22+
"rawOutput": {
23+
"status": "completed",
24+
"revisedPrompt": null,
25+
"result": "iVBORw0KGgo="
26+
}
27+
}
28+
}
29+
]
30+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"method": "sessionUpdate",
3+
"args": [
4+
{
5+
"sessionId": "test-session-id",
6+
"update": {
7+
"sessionUpdate": "tool_call",
8+
"toolCallId": "image-generation-1",
9+
"kind": "other",
10+
"title": "Image generation",
11+
"status": "in_progress",
12+
"rawInput": {
13+
"id": "image-generation-1"
14+
}
15+
}
16+
}
17+
]
18+
}
19+
{
20+
"method": "sessionUpdate",
21+
"args": [
22+
{
23+
"sessionId": "test-session-id",
24+
"update": {
25+
"sessionUpdate": "tool_call_update",
26+
"toolCallId": "image-generation-1",
27+
"status": "completed",
28+
"content": [
29+
{
30+
"type": "content",
31+
"content": {
32+
"type": "text",
33+
"text": "Revised prompt: A tiny blue square"
34+
}
35+
},
36+
{
37+
"type": "content",
38+
"content": {
39+
"type": "image",
40+
"data": "iVBORw0KGgo=",
41+
"mimeType": "image/png",
42+
"uri": "/tmp/codex/generated-blue-square.png"
43+
}
44+
}
45+
],
46+
"rawOutput": {
47+
"status": "completed",
48+
"revisedPrompt": "A tiny blue square",
49+
"result": "iVBORw0KGgo=",
50+
"savedPath": "/tmp/codex/generated-blue-square.png"
51+
}
52+
}
53+
}
54+
]
55+
}

0 commit comments

Comments
 (0)