Skip to content

Commit 71d7889

Browse files
authored
feat: Propagate thread info updates (#159)
1 parent 115da1d commit 71d7889

7 files changed

Lines changed: 279 additions & 7 deletions

src/CodexAcpClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ export class CodexAcpClient {
492492
let sessions = listResponse.data.map((thread) => ({
493493
sessionId: thread.id,
494494
cwd: thread.cwd,
495-
title: thread.preview || null,
495+
title: (thread.name ?? thread.preview) || null,
496496
updatedAt: new Date(thread.updatedAt * 1000).toISOString(),
497497
}));
498498
if (requestedCwd) {
@@ -501,7 +501,7 @@ export class CodexAcpClient {
501501
.map((thread) => ({
502502
sessionId: thread.id,
503503
cwd: thread.cwd,
504-
title: thread.preview || null,
504+
title: (thread.name ?? thread.preview) || null,
505505
updatedAt: new Date(thread.updatedAt * 1000).toISOString(),
506506
}));
507507
if (filtered.length > 0 || path.isAbsolute(requestedCwd)) {

src/CodexEventHandler.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,27 @@ export class CodexEventHandler {
9090
return null;
9191
case "thread/tokenUsage/updated":
9292
return this.createUsageUpdate(notification.params);
93+
case "thread/name/updated":
94+
return {
95+
sessionUpdate: "session_info_update",
96+
title: notification.params.threadName ?? null,
97+
};
98+
case "thread/status/changed":
99+
return this.createCodexSessionInfoUpdate({
100+
threadStatus: notification.params.status,
101+
});
102+
case "thread/archived":
103+
return this.createCodexSessionInfoUpdate({
104+
archived: true,
105+
});
106+
case "thread/unarchived":
107+
return this.createCodexSessionInfoUpdate({
108+
archived: false,
109+
});
110+
case "thread/closed":
111+
return this.createCodexSessionInfoUpdate({
112+
closed: true,
113+
});
93114
case "item/commandExecution/outputDelta":
94115
return this.createCommandOutputDeltaEvent(notification.params);
95116
case "item/mcpToolCall/progress":
@@ -136,10 +157,6 @@ export class CodexEventHandler {
136157
case "serverRequest/resolved":
137158
case "model/verification":
138159
case "windows/worldWritableWarning":
139-
case "thread/status/changed":
140-
case "thread/archived":
141-
case "thread/unarchived":
142-
case "thread/closed":
143160
case "thread/realtime/started":
144161
case "thread/realtime/itemAdded":
145162
case "thread/realtime/transcript/delta":
@@ -156,7 +173,6 @@ export class CodexEventHandler {
156173
case "externalAgentConfig/import/completed":
157174
case "rawResponseItem/completed":
158175
case "thread/started":
159-
case "thread/name/updated":
160176
case "item/plan/delta":
161177
case "thread/goal/updated":
162178
case "thread/goal/cleared":
@@ -166,6 +182,15 @@ export class CodexEventHandler {
166182
}
167183
}
168184

185+
private createCodexSessionInfoUpdate(codexMetadata: Record<string, unknown>): UpdateSessionEvent {
186+
return {
187+
sessionUpdate: "session_info_update",
188+
_meta: {
189+
codex: codexMetadata,
190+
},
191+
};
192+
}
193+
169194
private async createTextEvent(event: AgentMessageDeltaNotification): Promise<UpdateSessionEvent> {
170195
return {
171196
sessionUpdate: "agent_message_chunk",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"sessions": [
3+
{
4+
"sessionId": "sess-1",
5+
"cwd": "/repo/project",
6+
"title": "Saved title",
7+
"updatedAt": "1970-01-01T00:03:20.000Z"
8+
}
9+
],
10+
"nextCursor": null
11+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"method": "sessionUpdate",
3+
"args": [
4+
{
5+
"sessionId": "test-session-id",
6+
"update": {
7+
"sessionUpdate": "session_info_update",
8+
"_meta": {
9+
"codex": {
10+
"threadStatus": {
11+
"type": "active",
12+
"activeFlags": [
13+
"waitingOnApproval"
14+
]
15+
}
16+
}
17+
}
18+
}
19+
}
20+
]
21+
}
22+
{
23+
"method": "sessionUpdate",
24+
"args": [
25+
{
26+
"sessionId": "test-session-id",
27+
"update": {
28+
"sessionUpdate": "session_info_update",
29+
"_meta": {
30+
"codex": {
31+
"archived": true
32+
}
33+
}
34+
}
35+
}
36+
]
37+
}
38+
{
39+
"method": "sessionUpdate",
40+
"args": [
41+
{
42+
"sessionId": "test-session-id",
43+
"update": {
44+
"sessionUpdate": "session_info_update",
45+
"_meta": {
46+
"codex": {
47+
"archived": false
48+
}
49+
}
50+
}
51+
}
52+
]
53+
}
54+
{
55+
"method": "sessionUpdate",
56+
"args": [
57+
{
58+
"sessionId": "test-session-id",
59+
"update": {
60+
"sessionUpdate": "session_info_update",
61+
"_meta": {
62+
"codex": {
63+
"closed": true
64+
}
65+
}
66+
}
67+
}
68+
]
69+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"method": "sessionUpdate",
3+
"args": [
4+
{
5+
"sessionId": "test-session-id",
6+
"update": {
7+
"sessionUpdate": "session_info_update",
8+
"title": "Renamed session"
9+
}
10+
}
11+
]
12+
}
13+
{
14+
"method": "sessionUpdate",
15+
"args": [
16+
{
17+
"sessionId": "test-session-id",
18+
"update": {
19+
"sessionUpdate": "session_info_update",
20+
"title": null
21+
}
22+
}
23+
]
24+
}

src/__tests__/CodexACPAgent/list-sessions.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,47 @@ describe("CodexACPAgent - list sessions", () => {
8585
"data/list-sessions.json"
8686
);
8787
});
88+
89+
it("should prefer the explicit thread name as the session title", async () => {
90+
const fixture = createCodexMockTestFixture();
91+
const codexAcpAgent = fixture.getCodexAcpAgent();
92+
const codexAcpClient = fixture.getCodexAcpClient();
93+
const codexAppServerClient = fixture.getCodexAppServerClient();
94+
95+
codexAcpClient.authRequired = vi.fn().mockResolvedValue(false);
96+
97+
const thread: Thread = {
98+
id: "sess-1",
99+
forkedFromId: null,
100+
preview: "Preview text",
101+
ephemeral: false,
102+
modelProvider: "openai",
103+
createdAt: 100,
104+
updatedAt: 200,
105+
status: { type: "idle" },
106+
path: null,
107+
cwd: "/repo/project",
108+
cliVersion: "0.0.0",
109+
source: "cli",
110+
agentNickname: null,
111+
agentRole: null,
112+
gitInfo: null,
113+
name: "Saved title",
114+
turns: [],
115+
};
116+
117+
codexAppServerClient.threadList = vi.fn().mockResolvedValue({
118+
data: [thread],
119+
nextCursor: null,
120+
});
121+
122+
const response = await codexAcpAgent.listSessions({
123+
cwd: null,
124+
cursor: null,
125+
});
126+
127+
await expect(`${JSON.stringify(response, null, 2)}\n`).toMatchFileSnapshot(
128+
"data/list-sessions-thread-name.json"
129+
);
130+
});
88131
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import type { ServerNotification } from "../../app-server";
3+
import { setupPromptTestSession } from "../acp-test-utils";
4+
5+
describe("CodexEventHandler - session info updates", () => {
6+
const sessionId = "test-session-id";
7+
8+
it("maps thread name updates to ACP session info updates", async () => {
9+
const { mockFixture } = setupPromptTestSession({ sessionId });
10+
11+
await mockFixture.getCodexAcpAgent().prompt({
12+
sessionId,
13+
prompt: [{ type: "text", text: "test" }],
14+
});
15+
16+
mockFixture.clearAcpConnectionDump();
17+
18+
const notifications: ServerNotification[] = [
19+
{
20+
method: "thread/name/updated",
21+
params: {
22+
threadId: sessionId,
23+
threadName: "Renamed session",
24+
},
25+
},
26+
{
27+
method: "thread/name/updated",
28+
params: {
29+
threadId: sessionId,
30+
},
31+
},
32+
];
33+
34+
for (const notification of notifications) {
35+
mockFixture.sendServerNotification(notification);
36+
}
37+
38+
await vi.waitFor(() => {
39+
expect(mockFixture.getAcpConnectionEvents([])).toHaveLength(2);
40+
});
41+
42+
await expect(`${mockFixture.getAcpConnectionDump([])}\n`).toMatchFileSnapshot(
43+
"data/session-info-update-title.json"
44+
);
45+
});
46+
47+
it("maps Codex thread lifecycle metadata to ACP session info updates", async () => {
48+
const { mockFixture } = setupPromptTestSession({ sessionId });
49+
50+
await mockFixture.getCodexAcpAgent().prompt({
51+
sessionId,
52+
prompt: [{ type: "text", text: "test" }],
53+
});
54+
55+
mockFixture.clearAcpConnectionDump();
56+
57+
const notifications: ServerNotification[] = [
58+
{
59+
method: "thread/status/changed",
60+
params: {
61+
threadId: sessionId,
62+
status: {
63+
type: "active",
64+
activeFlags: ["waitingOnApproval"],
65+
},
66+
},
67+
},
68+
{
69+
method: "thread/archived",
70+
params: {
71+
threadId: sessionId,
72+
},
73+
},
74+
{
75+
method: "thread/unarchived",
76+
params: {
77+
threadId: sessionId,
78+
},
79+
},
80+
{
81+
method: "thread/closed",
82+
params: {
83+
threadId: sessionId,
84+
},
85+
},
86+
];
87+
88+
for (const notification of notifications) {
89+
mockFixture.sendServerNotification(notification);
90+
}
91+
92+
await vi.waitFor(() => {
93+
expect(mockFixture.getAcpConnectionEvents([])).toHaveLength(4);
94+
});
95+
96+
await expect(`${mockFixture.getAcpConnectionDump([])}\n`).toMatchFileSnapshot(
97+
"data/session-info-update-metadata.json"
98+
);
99+
});
100+
});

0 commit comments

Comments
 (0)