Skip to content

Commit 7080ea7

Browse files
authored
fix: Map Codex image events to ACP tool calls (#185)
Handle more events around images. Ports over similar logic on the Rust side.
1 parent a81e071 commit 7080ea7

9 files changed

Lines changed: 482 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
formatWebSearchTitle,
4345
} from "./CodexToolCallMapper";
@@ -837,9 +839,9 @@ export class CodexAcpServer implements acp.Agent {
837839
case "webSearch":
838840
return [this.createWebSearchUpdate(item)];
839841
case "imageView":
840-
return [this.createImageViewUpdate(item)];
842+
return [createImageViewUpdate(item)];
841843
case "imageGeneration":
842-
return [];
844+
return [createImageGenerationUpdate(item)];
843845
case "enteredReviewMode":
844846
return [this.createReviewModeUpdate(item, true)];
845847
case "exitedReviewMode":
@@ -908,22 +910,6 @@ export class CodexAcpServer implements acp.Agent {
908910
};
909911
}
910912

911-
private createImageViewUpdate(
912-
item: ThreadItem & { type: "imageView" }
913-
): UpdateSessionEvent {
914-
return {
915-
sessionUpdate: "tool_call",
916-
toolCallId: item.id,
917-
kind: "read",
918-
title: "View image",
919-
status: "completed",
920-
locations: [{ path: item.path }],
921-
rawInput: {
922-
path: item.path,
923-
},
924-
};
925-
}
926-
927913
private createReviewModeUpdate(
928914
item: ThreadItem & { type: "enteredReviewMode" | "exitedReviewMode" },
929915
entered: boolean

src/CodexEventHandler.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import {
3434
createFileChangeUpdate,
3535
createGuardianApprovalReviewToolCall,
3636
createGuardianApprovalReviewToolCallUpdate,
37+
createImageGenerationCompleteUpdate,
38+
createImageGenerationStartUpdate,
39+
createImageGenerationUpdate,
40+
createImageViewUpdate,
3741
createMcpRawInput,
3842
createMcpRawOutput,
3943
createFuzzyFileSearchComplete,
@@ -54,6 +58,8 @@ export class CodexEventHandler {
5458
private failure: RequestError | null = null;
5559
private readonly activeFuzzyFileSearchSessions = new Set<string>();
5660
private readonly activeGuardianApprovalReviews = new Set<string>();
61+
private readonly activeImageGenerationItems = new Set<string>();
62+
private readonly emittedImageViewItems = new Set<string>();
5763

5864
constructor(connection: acp.AgentSideConnection, sessionState: SessionState) {
5965
this.connection = connection;
@@ -296,13 +302,17 @@ export class CodexEventHandler {
296302
return await createDynamicToolCallUpdate(event.item);
297303
case "webSearch":
298304
return createWebSearchStartUpdate(event.item);
305+
case "imageView":
306+
this.emittedImageViewItems.add(event.item.id);
307+
return createImageViewUpdate(event.item);
308+
case "imageGeneration":
309+
this.activeImageGenerationItems.add(event.item.id);
310+
return createImageGenerationStartUpdate(event.item);
299311
case "collabAgentToolCall":
300312
case "userMessage":
301313
case "hookPrompt":
302314
case "agentMessage":
303315
case "reasoning":
304-
case "imageView":
305-
case "imageGeneration":
306316
case "enteredReviewMode":
307317
case "exitedReviewMode":
308318
case "contextCompaction":
@@ -330,6 +340,16 @@ export class CodexEventHandler {
330340
}
331341
case "commandExecution":
332342
return this.completeCommandExecutionEvent(event.item);
343+
case "imageView":
344+
if (this.emittedImageViewItems.delete(event.item.id)) {
345+
return null;
346+
}
347+
return createImageViewUpdate(event.item);
348+
case "imageGeneration":
349+
if (this.activeImageGenerationItems.delete(event.item.id)) {
350+
return createImageGenerationCompleteUpdate(event.item);
351+
}
352+
return createImageGenerationUpdate(event.item);
333353
case "reasoning":
334354
const summary = event.item.summary[0];
335355
if (!summary) return null;
@@ -346,8 +366,6 @@ export class CodexEventHandler {
346366
case "userMessage":
347367
case "hookPrompt":
348368
case "agentMessage":
349-
case "imageView":
350-
case "imageGeneration":
351369
case "enteredReviewMode":
352370
case "plan":
353371
return null;

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";
@@ -114,6 +114,69 @@ export async function createDynamicToolCallUpdate(
114114
return createExecuteToolCallUpdate(item, item.tool, { arguments: item.arguments })
115115
}
116116

117+
export function createImageViewUpdate(
118+
item: ThreadItem & { type: "imageView" }
119+
): UpdateSessionEvent {
120+
const displayPath = item.path;
121+
return {
122+
sessionUpdate: "tool_call",
123+
toolCallId: item.id,
124+
kind: "read",
125+
title: `View Image ${displayPath}`,
126+
status: "completed",
127+
content: [createContent({
128+
type: "resource_link",
129+
name: displayPath,
130+
uri: displayPath,
131+
})],
132+
locations: [{ path: item.path }],
133+
rawInput: {
134+
path: item.path,
135+
},
136+
};
137+
}
138+
139+
export function createImageGenerationStartUpdate(
140+
item: ThreadItem & { type: "imageGeneration" }
141+
): UpdateSessionEvent {
142+
return {
143+
sessionUpdate: "tool_call",
144+
toolCallId: item.id,
145+
kind: "other",
146+
title: "Image generation",
147+
status: "in_progress",
148+
rawInput: {
149+
id: item.id,
150+
},
151+
};
152+
}
153+
154+
export function createImageGenerationCompleteUpdate(
155+
item: ThreadItem & { type: "imageGeneration" }
156+
): UpdateSessionEvent {
157+
return {
158+
sessionUpdate: "tool_call_update",
159+
toolCallId: item.id,
160+
status: imageGenerationToolStatus(item.status),
161+
content: imageGenerationContent(item),
162+
rawOutput: imageGenerationRawOutput(item),
163+
};
164+
}
165+
166+
export function createImageGenerationUpdate(
167+
item: ThreadItem & { type: "imageGeneration" }
168+
): UpdateSessionEvent {
169+
return {
170+
sessionUpdate: "tool_call",
171+
toolCallId: item.id,
172+
kind: "other",
173+
title: "Image generation",
174+
status: imageGenerationToolStatus(item.status),
175+
content: imageGenerationContent(item),
176+
rawOutput: imageGenerationRawOutput(item),
177+
};
178+
}
179+
117180
export async function createExecuteToolCallUpdate(
118181
item: ThreadItem & ({ type: "mcpToolCall" } | { type: "dynamicToolCall" }),
119182
title: string,
@@ -450,6 +513,74 @@ function shellQuote(arg: string): string {
450513
return `'${arg.replace(/'/g, `'\\''`)}'`;
451514
}
452515

516+
function imageGenerationToolStatus(status: string): AcpToolCallStatus {
517+
switch (status) {
518+
case "completed":
519+
return "completed";
520+
case "generating":
521+
case "in_progress":
522+
case "inProgress":
523+
case "incomplete":
524+
return "in_progress";
525+
case "failed":
526+
return "failed";
527+
default:
528+
return "completed";
529+
}
530+
}
531+
532+
function imageGenerationContent(
533+
item: ThreadItem & { type: "imageGeneration" }
534+
): ToolCallContent[] {
535+
const content: ToolCallContent[] = [];
536+
537+
if (item.revisedPrompt && item.revisedPrompt.trim() !== "") {
538+
content.push(createContent({
539+
type: "text",
540+
text: `Revised prompt: ${item.revisedPrompt}`,
541+
}));
542+
}
543+
544+
if (item.result.trim() !== "") {
545+
const image: ContentBlock = item.savedPath && item.savedPath.trim() !== ""
546+
? {
547+
type: "image",
548+
data: item.result,
549+
mimeType: "image/png",
550+
uri: item.savedPath,
551+
}
552+
: {
553+
type: "image",
554+
data: item.result,
555+
mimeType: "image/png",
556+
};
557+
content.push(createContent(image));
558+
}
559+
560+
return content;
561+
}
562+
563+
function imageGenerationRawOutput(
564+
item: ThreadItem & { type: "imageGeneration" }
565+
): Record<string, string | null> {
566+
const output: Record<string, string | null> = {
567+
status: item.status,
568+
revisedPrompt: item.revisedPrompt,
569+
result: item.result,
570+
};
571+
if ("savedPath" in item) {
572+
output["savedPath"] = item.savedPath ?? null;
573+
}
574+
return output;
575+
}
576+
577+
function createContent(content: ContentBlock): ToolCallContent {
578+
return {
579+
type: "content",
580+
content,
581+
};
582+
}
583+
453584
async function createPatchContent(change: FileUpdateChange): Promise<ToolCallContent | null> {
454585
try {
455586
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)