Skip to content
Closed
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
4 changes: 3 additions & 1 deletion src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {toTokenCount} from "./TokenCount";
import {
createCommandExecutionUpdate,
createDynamicToolCallUpdate,
createFileChangePatchUpdate,
createFileChangeUpdate,
createMcpRawInput,
createMcpRawOutput,
Expand Down Expand Up @@ -138,6 +139,8 @@ export class CodexEventHandler {
return this.handleFuzzyFileSearchSessionUpdated(notification.params);
case "fuzzyFileSearch/sessionCompleted":
return this.handleFuzzyFileSearchSessionCompleted(notification.params);
case "item/fileChange/patchUpdated":
return await createFileChangePatchUpdate(notification.params);
// ignored events
case "command/exec/outputDelta":
case "item/autoApprovalReview/started":
Expand All @@ -150,7 +153,6 @@ export class CodexEventHandler {
case "turn/diff/updated":
case "item/commandExecution/terminalInteraction":
case "item/fileChange/outputDelta":
case "item/fileChange/patchUpdated":
case "account/updated":
case "fs/changed":
case "mcpServer/startupStatus/updated":
Expand Down
34 changes: 28 additions & 6 deletions src/CodexToolCallMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,7 @@ function toAcpStatus(status: CodexItemStatus): AcpToolCallStatus {
export async function createFileChangeUpdate(
item: ThreadItem & { type: "fileChange" }
): Promise<UpdateSessionEvent> {
const patches: ToolCallContent[] = [];
for (const change of item.changes) {
const content = await createPatchContent(change);
if (content) patches.push(content);
// ignore unparseable diffs
}
const patches = await createFileChangeContent(item.changes);
return {
sessionUpdate: "tool_call",
toolCallId: item.id,
Expand All @@ -56,6 +51,33 @@ export async function createFileChangeUpdate(
};
}

export async function createFileChangePatchUpdate(
event: { itemId: string, changes: Array<FileUpdateChange> }
): Promise<UpdateSessionEvent | null> {
if (event.changes.length === 0) {
return null;
}
const patches = await createFileChangeContent(event.changes);
return {
sessionUpdate: "tool_call_update",
toolCallId: event.itemId,
title: "Editing files",
kind: "edit",
status: "in_progress",
content: patches,
};
}

async function createFileChangeContent(changes: Array<FileUpdateChange>): Promise<ToolCallContent[]> {
const patches: ToolCallContent[] = [];
for (const change of changes) {
const content = await createPatchContent(change);
if (content) patches.push(content);
// ignore unparseable diffs
}
return patches;
}

export async function createCommandExecutionUpdate(
item: ThreadItem & { type: "commandExecution" }
): Promise<UpdateSessionEvent> {
Expand Down
61 changes: 61 additions & 0 deletions src/__tests__/CodexACPAgent/data/file-change-patch-updated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "test-session-id",
"update": {
"sessionUpdate": "tool_call",
"toolCallId": "file-change-patch-live",
"title": "Editing files",
"kind": "edit",
"status": "in_progress",
"content": [
{
"type": "diff",
"oldText": null,
"newText": "class FileA\n",
"path": "/test/project/FileA.kt",
"_meta": {
"kind": "add"
}
}
]
}
}
]
}
{
"method": "sessionUpdate",
"args": [
{
"sessionId": "test-session-id",
"update": {
"sessionUpdate": "tool_call_update",
"toolCallId": "file-change-patch-live",
"title": "Editing files",
"kind": "edit",
"status": "in_progress",
"content": [
{
"type": "diff",
"oldText": null,
"newText": "class FileA {\n fun hello() = \"hi\"\n}\n",
"path": "/test/project/FileA.kt",
"_meta": {
"kind": "add"
}
},
{
"type": "diff",
"oldText": null,
"newText": "class FileB\n",
"path": "/test/project/FileB.kt",
"_meta": {
"kind": "add"
}
}
]
}
}
]
}
61 changes: 60 additions & 1 deletion src/__tests__/CodexACPAgent/file-change-events.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { SessionState } from '../../CodexAcpServer';
import type { ServerNotification } from '../../app-server';
import { createFileChangeUpdate } from '../../CodexToolCallMapper';
import { createFileChangePatchUpdate, createFileChangeUpdate } from '../../CodexToolCallMapper';
import type { ThreadItem } from '../../app-server/v2';
import { createCodexMockTestFixture, createTestSessionState, setupPromptAndSendNotifications, type CodexMockTestFixture } from '../acp-test-utils';
import {AgentMode} from "../../AgentMode";
Expand Down Expand Up @@ -115,6 +115,65 @@ describe('CodexEventHandler - file change events', () => {
);
});

it('should handle file change patch updates', async () => {
const fileChangeStarted: ServerNotification = {
method: 'item/started',
params: {
threadId: sessionId,
turnId: 'turn-1',
startedAtMs: 0,
item: {
type: 'fileChange',
id: 'file-change-patch-live',
changes: [
{
path: '/test/project/FileA.kt',
kind: { type: 'add' },
diff: 'class FileA\n',
},
],
status: 'inProgress',
},
},
};
const fileChangePatchUpdated: ServerNotification = {
method: 'item/fileChange/patchUpdated',
params: {
threadId: sessionId,
turnId: 'turn-1',
itemId: 'file-change-patch-live',
changes: [
{
path: '/test/project/FileA.kt',
kind: { type: 'add' },
diff: 'class FileA {\n fun hello() = "hi"\n}\n',
},
{
path: '/test/project/FileB.kt',
kind: { type: 'add' },
diff: 'class FileB\n',
},
],
},
};

await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [
fileChangeStarted,
fileChangePatchUpdated,
]);

await expect(mockFixture.getAcpConnectionDump(['id'])).toMatchFileSnapshot(
'data/file-change-patch-updated.json'
);
});

it('should ignore empty file change patch updates', async () => {
await expect(createFileChangePatchUpdate({
itemId: 'file-change-empty-patch',
changes: [],
})).resolves.toBeNull();
});

it('should handle new file creation with raw content', async () => {
// Codex sends raw file content (not unified diff) for new files
const newFileNotification: ServerNotification = {
Expand Down
Loading