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
22 changes: 4 additions & 18 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
createCommandExecutionUpdate,
createDynamicToolCallUpdate,
createFileChangeUpdate,
createImageGenerationUpdate,
createImageViewUpdate,
createMcpToolCallUpdate,
} from "./CodexToolCallMapper";
import {
Expand Down Expand Up @@ -800,9 +802,9 @@ export class CodexAcpServer implements acp.Agent {
case "webSearch":
return [this.createWebSearchUpdate(item)];
case "imageView":
return [this.createImageViewUpdate(item)];
return [createImageViewUpdate(item)];
case "imageGeneration":
return [];
return [createImageGenerationUpdate(item)];
case "enteredReviewMode":
return [this.createReviewModeUpdate(item, true)];
case "exitedReviewMode":
Expand Down Expand Up @@ -871,22 +873,6 @@ export class CodexAcpServer implements acp.Agent {
};
}

private createImageViewUpdate(
item: ThreadItem & { type: "imageView" }
): UpdateSessionEvent {
return {
sessionUpdate: "tool_call",
toolCallId: item.id,
kind: "read",
title: "View image",
status: "completed",
locations: [{ path: item.path }],
rawInput: {
path: item.path,
},
};
}

private createReviewModeUpdate(
item: ThreadItem & { type: "enteredReviewMode" | "exitedReviewMode" },
entered: boolean
Expand Down
26 changes: 22 additions & 4 deletions src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
createCommandExecutionUpdate,
createDynamicToolCallUpdate,
createFileChangeUpdate,
createImageGenerationCompleteUpdate,
createImageGenerationStartUpdate,
createImageGenerationUpdate,
createImageViewUpdate,
createMcpRawInput,
createMcpRawOutput,
createFuzzyFileSearchComplete,
Expand All @@ -45,6 +49,8 @@ export class CodexEventHandler {
private readonly sessionState: SessionState;
private failure: RequestError | null = null;
private readonly activeFuzzyFileSearchSessions = new Set<string>();
private readonly activeImageGenerationItems = new Set<string>();
private readonly emittedImageViewItems = new Set<string>();

constructor(connection: acp.AgentSideConnection, sessionState: SessionState) {
this.connection = connection;
Expand Down Expand Up @@ -255,14 +261,18 @@ export class CodexEventHandler {
return await createMcpToolCallUpdate(event.item);
case "dynamicToolCall":
return await createDynamicToolCallUpdate(event.item);
case "imageView":
this.emittedImageViewItems.add(event.item.id);
return createImageViewUpdate(event.item);
case "imageGeneration":
this.activeImageGenerationItems.add(event.item.id);
return createImageGenerationStartUpdate(event.item);
case "collabAgentToolCall":
case "userMessage":
case "hookPrompt":
case "agentMessage":
case "reasoning":
case "webSearch":
case "imageView":
case "imageGeneration":
case "enteredReviewMode":
case "exitedReviewMode":
case "contextCompaction":
Expand Down Expand Up @@ -290,6 +300,16 @@ export class CodexEventHandler {
}
case "commandExecution":
return this.completeCommandExecutionEvent(event.item);
case "imageView":
if (this.emittedImageViewItems.delete(event.item.id)) {
return null;
}
return createImageViewUpdate(event.item);
case "imageGeneration":
if (this.activeImageGenerationItems.delete(event.item.id)) {
return createImageGenerationCompleteUpdate(event.item);
}
return createImageGenerationUpdate(event.item);
case "reasoning":
const summary = event.item.summary[0];
if (!summary) return null;
Expand All @@ -305,8 +325,6 @@ export class CodexEventHandler {
case "hookPrompt":
case "agentMessage":
case "webSearch":
case "imageView":
case "imageGeneration":
case "enteredReviewMode":
case "exitedReviewMode":
case "contextCompaction":
Expand Down
133 changes: 132 additions & 1 deletion src/CodexToolCallMapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ToolCallContent } from "@agentclientprotocol/sdk";
import type { ContentBlock, ToolCallContent } from "@agentclientprotocol/sdk";
import { applyPatch, parsePatch, reversePatch } from "diff";
import { readFile } from "node:fs/promises";
import path from "node:path";
Expand Down Expand Up @@ -104,6 +104,69 @@ export async function createDynamicToolCallUpdate(
return createExecuteToolCallUpdate(item, item.tool, { arguments: item.arguments })
}

export function createImageViewUpdate(
item: ThreadItem & { type: "imageView" }
): UpdateSessionEvent {
const displayPath = item.path;
return {
sessionUpdate: "tool_call",
toolCallId: item.id,
kind: "read",
title: `View Image ${displayPath}`,
status: "completed",
content: [createContent({
type: "resource_link",
name: displayPath,
uri: displayPath,
})],
locations: [{ path: item.path }],
rawInput: {
path: item.path,
},
};
}

export function createImageGenerationStartUpdate(
item: ThreadItem & { type: "imageGeneration" }
): UpdateSessionEvent {
return {
sessionUpdate: "tool_call",
toolCallId: item.id,
kind: "other",
title: "Image generation",
status: "in_progress",
rawInput: {
id: item.id,
},
};
}

export function createImageGenerationCompleteUpdate(
item: ThreadItem & { type: "imageGeneration" }
): UpdateSessionEvent {
return {
sessionUpdate: "tool_call_update",
toolCallId: item.id,
status: imageGenerationToolStatus(item.status),
content: imageGenerationContent(item),
rawOutput: imageGenerationRawOutput(item),
};
}

export function createImageGenerationUpdate(
item: ThreadItem & { type: "imageGeneration" }
): UpdateSessionEvent {
return {
sessionUpdate: "tool_call",
toolCallId: item.id,
kind: "other",
title: "Image generation",
status: imageGenerationToolStatus(item.status),
content: imageGenerationContent(item),
rawOutput: imageGenerationRawOutput(item),
};
}

export async function createExecuteToolCallUpdate(
item: ThreadItem & ({ type: "mcpToolCall" } | { type: "dynamicToolCall" }),
title: string,
Expand Down Expand Up @@ -257,6 +320,74 @@ function createSearchTitle(query: string | null, path: string | null): string {
return "Search";
}

function imageGenerationToolStatus(status: string): AcpToolCallStatus {
switch (status) {
case "completed":
return "completed";
case "generating":
case "in_progress":
case "inProgress":
case "incomplete":
return "in_progress";
case "failed":
return "failed";
default:
return "completed";
}
}

function imageGenerationContent(
item: ThreadItem & { type: "imageGeneration" }
): ToolCallContent[] {
const content: ToolCallContent[] = [];

if (item.revisedPrompt && item.revisedPrompt.trim() !== "") {
content.push(createContent({
type: "text",
text: `Revised prompt: ${item.revisedPrompt}`,
}));
}

if (item.result.trim() !== "") {
const image: ContentBlock = item.savedPath && item.savedPath.trim() !== ""
? {
type: "image",
data: item.result,
mimeType: "image/png",
uri: item.savedPath,
}
: {
type: "image",
data: item.result,
mimeType: "image/png",
};
content.push(createContent(image));
}

return content;
}

function imageGenerationRawOutput(
item: ThreadItem & { type: "imageGeneration" }
): Record<string, string | null> {
const output: Record<string, string | null> = {
status: item.status,
revisedPrompt: item.revisedPrompt,
result: item.result,
};
if ("savedPath" in item) {
output["savedPath"] = item.savedPath ?? null;
}
return output;
}

function createContent(content: ContentBlock): ToolCallContent {
return {
type: "content",
content,
};
}

async function createPatchContent(change: FileUpdateChange): Promise<ToolCallContent | null> {
try {
switch (change.kind.type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "test-session-id",
"update": {
"sessionUpdate": "tool_call",
"toolCallId": "image-generation-completed-only",
"kind": "other",
"title": "Image generation",
"status": "completed",
"content": [
{
"type": "content",
"content": {
"type": "image",
"data": "iVBORw0KGgo=",
"mimeType": "image/png"
}
}
],
"rawOutput": {
"status": "completed",
"revisedPrompt": null,
"result": "iVBORw0KGgo="
}
}
}
]
}
55 changes: 55 additions & 0 deletions src/__tests__/CodexACPAgent/data/image-generation-flow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "test-session-id",
"update": {
"sessionUpdate": "tool_call",
"toolCallId": "image-generation-1",
"kind": "other",
"title": "Image generation",
"status": "in_progress",
"rawInput": {
"id": "image-generation-1"
}
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "test-session-id",
"update": {
"sessionUpdate": "tool_call_update",
"toolCallId": "image-generation-1",
"status": "completed",
"content": [
{
"type": "content",
"content": {
"type": "text",
"text": "Revised prompt: A tiny blue square"
}
},
{
"type": "content",
"content": {
"type": "image",
"data": "iVBORw0KGgo=",
"mimeType": "image/png",
"uri": "/tmp/codex/generated-blue-square.png"
}
}
],
"rawOutput": {
"status": "completed",
"revisedPrompt": "A tiny blue square",
"result": "iVBORw0KGgo=",
"savedPath": "/tmp/codex/generated-blue-square.png"
}
}
}
]
}
Loading
Loading