Skip to content

Commit 40bf2b4

Browse files
Show skill reads as ACP read tool calls
1 parent c516b9a commit 40bf2b4

4 files changed

Lines changed: 136 additions & 6 deletions

File tree

src/CodexAcpServer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
createImageGenerationUpdate,
5555
createImageViewUpdate,
5656
createMcpToolCallUpdate,
57+
createSkillReadUpdate,
5758
formatWebSearchTitle,
5859
} from "./CodexToolCallMapper";
5960
import {
@@ -1027,7 +1028,11 @@ export class CodexAcpServer {
10271028
private createUserMessageUpdates(item: ThreadItem & { type: "userMessage" }): UpdateSessionEvent[] {
10281029
const updates: UpdateSessionEvent[] = [];
10291030
const messageId = item.id;
1030-
for (const input of item.content) {
1031+
for (const [index, input] of item.content.entries()) {
1032+
if (input.type === "skill") {
1033+
updates.push(createSkillReadUpdate(`${messageId}.skill.${index}`, input));
1034+
continue;
1035+
}
10311036
const blocks = this.userInputToContentBlocks(input);
10321037
for (const block of blocks) {
10331038
updates.push(createUserMessageChunk(block, messageId));

src/CodexEventHandler.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
createImageViewUpdate,
4747
createMcpRawInput,
4848
createMcpRawOutput,
49+
createSkillReadUpdate,
4950
createFuzzyFileSearchComplete,
5051
createFuzzyFileSearchStartOrUpdate,
5152
createMcpToolCallUpdate,
@@ -86,13 +87,20 @@ export class CodexEventHandler {
8687

8788
async handleNotification(notification: ServerNotification) {
8889
const session = new ACPSessionConnection(this.connection, this.sessionState.sessionId);
89-
const updateEvent = await this.createUpdateEvent(notification);
90-
if (updateEvent) {
90+
const updateEvents = await this.createUpdateEvent(notification);
91+
for (const updateEvent of this.asUpdateEvents(updateEvents)) {
9192
await session.update(updateEvent);
9293
}
9394
}
9495

95-
private async createUpdateEvent(notification: ServerNotification): Promise<UpdateSessionEvent | null> {
96+
private asUpdateEvents(update: UpdateSessionEvent | UpdateSessionEvent[] | null): UpdateSessionEvent[] {
97+
if (!update) {
98+
return [];
99+
}
100+
return Array.isArray(update) ? update : [update];
101+
}
102+
103+
private async createUpdateEvent(notification: ServerNotification): Promise<UpdateSessionEvent | UpdateSessionEvent[] | null> {
96104
/*
97105
TODO split UpdateSessionEvent to improve completion
98106
createUpdateEvent({
@@ -304,7 +312,7 @@ export class CodexEventHandler {
304312
return createAgentTextThoughtChunk(text, messageId);
305313
}
306314

307-
private async createItemEvent(event: ItemStartedNotification): Promise<UpdateSessionEvent | null> {
315+
private async createItemEvent(event: ItemStartedNotification): Promise<UpdateSessionEvent | UpdateSessionEvent[] | null> {
308316
switch (event.item.type) {
309317
case "fileChange":
310318
return await createFileChangeUpdate(event.item);
@@ -331,9 +339,10 @@ export class CodexEventHandler {
331339
return createImageGenerationStartUpdate(event.item);
332340
case "collabAgentToolCall":
333341
return createCollabAgentToolCallUpdate(event.item);
342+
case "userMessage":
343+
return this.createUserMessageSkillReadUpdates(event.item);
334344
case "subAgentActivity":
335345
case "sleep":
336-
case "userMessage":
337346
case "hookPrompt":
338347
case "agentMessage":
339348
case "reasoning":
@@ -345,6 +354,15 @@ export class CodexEventHandler {
345354
}
346355
}
347356

357+
private createUserMessageSkillReadUpdates(item: ThreadItem & { type: "userMessage" }): UpdateSessionEvent[] {
358+
return item.content.flatMap((input, index) => {
359+
if (input.type !== "skill") {
360+
return [];
361+
}
362+
return [createSkillReadUpdate(`${item.id}.skill.${index}`, input)];
363+
});
364+
}
365+
348366
private async completeItemEvent(event: ItemCompletedNotification): Promise<UpdateSessionEvent | null> {
349367
switch (event.item.type) {
350368
case "fileChange":

src/CodexToolCallMapper.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
McpToolCallStatus,
2626
PatchApplyStatus,
2727
ThreadItem,
28+
UserInput,
2829
} from "./app-server/v2";
2930
import type { JsonValue } from "./app-server/serde_json/JsonValue";
3031
import {logger} from "./Logger";
@@ -41,6 +42,7 @@ type GuardianApprovalReviewNotification =
4142
type WebSearchItem = ThreadItem & { type: "webSearch" };
4243
type CollabAgentToolCallItem = ThreadItem & { type: "collabAgentToolCall" };
4344
type CommandExecutionItem = ThreadItem & { type: "commandExecution" };
45+
type SkillInput = Extract<UserInput, { type: "skill" }>;
4446
type AcpToolCallEvent = Extract<UpdateSessionEvent, { sessionUpdate: "tool_call" }>;
4547

4648
function toAcpStatus(status: CodexItemStatus): AcpToolCallStatus {
@@ -176,6 +178,25 @@ export function createImageViewUpdate(
176178
};
177179
}
178180

181+
export function createSkillReadUpdate(
182+
id: string,
183+
skill: SkillInput
184+
): UpdateSessionEvent {
185+
return {
186+
sessionUpdate: "tool_call",
187+
toolCallId: id,
188+
kind: "read",
189+
title: `Read ${skill.name} skill`,
190+
status: "completed",
191+
locations: [{ path: skill.path }],
192+
rawInput: {
193+
type: "skill",
194+
name: skill.name,
195+
path: skill.path,
196+
},
197+
};
198+
}
199+
179200
export function createImageGenerationStartUpdate(
180201
item: ThreadItem & { type: "imageGeneration" }
181202
): UpdateSessionEvent {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import type { ServerNotification } from "../../app-server";
3+
import type { SessionState } from "../../CodexAcpServer";
4+
import { AgentMode } from "../../AgentMode";
5+
import {
6+
createCodexMockTestFixture,
7+
createTestSessionState,
8+
setupPromptAndSendNotifications,
9+
type CodexMockTestFixture,
10+
} from "../acp-test-utils";
11+
12+
describe("CodexEventHandler - skill read events", () => {
13+
let mockFixture: CodexMockTestFixture;
14+
const sessionId = "test-session-id";
15+
16+
beforeEach(() => {
17+
mockFixture = createCodexMockTestFixture();
18+
vi.clearAllMocks();
19+
});
20+
21+
const sessionState: SessionState = createTestSessionState({
22+
sessionId,
23+
currentModelId: "model-id[effort]",
24+
agentMode: AgentMode.DEFAULT_AGENT_MODE,
25+
});
26+
27+
it("maps skill user inputs to read tool calls", async () => {
28+
const notification: ServerNotification = {
29+
method: "item/started",
30+
params: {
31+
threadId: sessionId,
32+
turnId: "turn-1",
33+
startedAtMs: 0,
34+
item: {
35+
type: "userMessage",
36+
id: "user-message-1",
37+
clientId: null,
38+
content: [
39+
{
40+
type: "skill",
41+
name: "UI Accessibility",
42+
path: "/skills/ui-accessibility/SKILL.md",
43+
},
44+
{
45+
type: "skill",
46+
name: "Code Style",
47+
path: "/skills/code-style/SKILL.md",
48+
},
49+
],
50+
},
51+
},
52+
};
53+
54+
await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [notification]);
55+
56+
const updates = mockFixture.getAcpConnectionEvents([]).map(event => event.args[0].update);
57+
expect(updates).toEqual([
58+
{
59+
sessionUpdate: "tool_call",
60+
toolCallId: "user-message-1.skill.0",
61+
kind: "read",
62+
title: "Read UI Accessibility skill",
63+
status: "completed",
64+
locations: [{ path: "/skills/ui-accessibility/SKILL.md" }],
65+
rawInput: {
66+
type: "skill",
67+
name: "UI Accessibility",
68+
path: "/skills/ui-accessibility/SKILL.md",
69+
},
70+
},
71+
{
72+
sessionUpdate: "tool_call",
73+
toolCallId: "user-message-1.skill.1",
74+
kind: "read",
75+
title: "Read Code Style skill",
76+
status: "completed",
77+
locations: [{ path: "/skills/code-style/SKILL.md" }],
78+
rawInput: {
79+
type: "skill",
80+
name: "Code Style",
81+
path: "/skills/code-style/SKILL.md",
82+
},
83+
},
84+
]);
85+
});
86+
});

0 commit comments

Comments
 (0)