Skip to content

Commit 93978a8

Browse files
Add session rename support
1 parent 956f71a commit 93978a8

12 files changed

Lines changed: 173 additions & 9 deletions

src/AcpExtensions.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
} from "@agentclientprotocol/sdk";
88

99
export const LEGACY_SET_SESSION_MODEL_METHOD = "session/set_model";
10+
export const SET_SESSION_TITLE_METHOD = "session/setTitle";
1011

1112
export type LegacySessionModel = {
1213
modelId: string;
@@ -26,6 +27,13 @@ export type LegacySetSessionModelRequest = {
2627

2728
export type LegacySetSessionModelResponse = {}
2829

30+
export type SetSessionTitleRequest = {
31+
sessionId: SessionId;
32+
title: string;
33+
}
34+
35+
export type SetSessionTitleResponse = {}
36+
2937
export type LegacyNewSessionResponse = NewSessionResponse & {
3038
models?: LegacySessionModelState | null;
3139
}
@@ -42,11 +50,13 @@ export type ExtMethodRequest =
4250
AuthenticationStatusRequest
4351
| AuthenticationLogoutRequest
4452
| LegacySetSessionModelExtRequest
53+
| SetSessionTitleExtRequest
4554

4655
export function isExtMethodRequest(request: { method: string, params: Record<string, unknown> }): request is ExtMethodRequest {
4756
return request.method === "authentication/status"
4857
|| request.method === "authentication/logout"
49-
|| request.method === LEGACY_SET_SESSION_MODEL_METHOD;
58+
|| request.method === LEGACY_SET_SESSION_MODEL_METHOD
59+
|| request.method === SET_SESSION_TITLE_METHOD;
5060
}
5161

5262
export type AuthenticationStatusRequest = { method: "authentication/status", params: {} }
@@ -60,6 +70,11 @@ export type LegacySetSessionModelExtRequest = {
6070
params: LegacySetSessionModelRequest;
6171
}
6272

73+
export type SetSessionTitleExtRequest = {
74+
method: typeof SET_SESSION_TITLE_METHOD;
75+
params: SetSessionTitleRequest;
76+
}
77+
6378
export async function legacySetSessionModel(
6479
connection: Pick<ClientContext, "request">,
6580
params: LegacySetSessionModelRequest,

src/CodexAcpClient.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ export class CodexAcpClient {
318318
await this.codexClient.threadArchive({threadId: sessionId});
319319
}
320320

321+
async setSessionTitle(sessionId: string, title: string): Promise<void> {
322+
await this.codexClient.threadSetName({
323+
threadId: sessionId,
324+
name: title,
325+
});
326+
}
327+
321328
async runReview(
322329
sessionId: string,
323330
target: ReviewTarget,

src/CodexAcpServer.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ import {
4040
type LegacySessionModelState,
4141
type LegacySetSessionModelRequest,
4242
type LegacySetSessionModelResponse,
43+
type SetSessionTitleRequest,
44+
type SetSessionTitleResponse,
4345
isExtMethodRequest,
4446
LEGACY_SET_SESSION_MODEL_METHOD,
47+
SET_SESSION_TITLE_METHOD,
4548
} from "./AcpExtensions";
4649
import {
4750
createCollabAgentToolCallUpdate,
@@ -175,6 +178,14 @@ export class CodexAcpServer {
175178
this.clientInfo = _params.clientInfo ?? null;
176179
this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities);
177180
await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
181+
const sessionCapabilities = {
182+
resume: { },
183+
list: { },
184+
close: { },
185+
delete: { },
186+
additionalDirectories: {},
187+
setTitle: {},
188+
};
178189
return {
179190
protocolVersion: acp.PROTOCOL_VERSION,
180191
agentInfo: {
@@ -191,13 +202,7 @@ export class CodexAcpServer {
191202
embeddedContext: true,
192203
image: true
193204
},
194-
sessionCapabilities: {
195-
resume: { },
196-
list: { },
197-
close: { },
198-
delete: { },
199-
additionalDirectories: {},
200-
},
205+
sessionCapabilities,
201206
mcpCapabilities: {
202207
acp: false,
203208
http: true,
@@ -222,6 +227,8 @@ export class CodexAcpServer {
222227
}
223228
case LEGACY_SET_SESSION_MODEL_METHOD:
224229
return await this.unstable_setSessionModel(this.parseLegacySetSessionModelParams(methodRequest.params));
230+
case SET_SESSION_TITLE_METHOD:
231+
return await this.setSessionTitle(this.parseSetSessionTitleParams(methodRequest.params));
225232
}
226233
}
227234

@@ -676,6 +683,16 @@ export class CodexAcpServer {
676683
};
677684
}
678685

686+
async setSessionTitle(params: SetSessionTitleRequest): Promise<SetSessionTitleResponse> {
687+
logger.log("Set session title requested", {
688+
sessionId: params.sessionId,
689+
titleLength: params.title.length,
690+
});
691+
await this.checkAuthorization();
692+
await this.runWithProcessCheck(() => this.codexAcpClient.setSessionTitle(params.sessionId, params.title));
693+
return {};
694+
}
695+
679696
private applyFastModeChange(sessionState: SessionState, value: string): void {
680697
if (value !== FAST_MODE_ON && value !== FAST_MODE_OFF) {
681698
throw RequestError.invalidParams();
@@ -765,6 +782,18 @@ export class CodexAcpServer {
765782
};
766783
}
767784

785+
private parseSetSessionTitleParams(params: Record<string, unknown>): SetSessionTitleRequest {
786+
const sessionId = params["sessionId"];
787+
const title = params["title"];
788+
if (typeof sessionId !== "string" || typeof title !== "string") {
789+
throw RequestError.invalidParams();
790+
}
791+
return {
792+
sessionId: sessionId,
793+
title: title,
794+
};
795+
}
796+
768797
private createSessionConfigOptions(sessionState: SessionState): Array<acp.SessionConfigOption> {
769798
const currentModelId = ModelId.fromString(sessionState.currentModelId);
770799
const configOptions = [

src/CodexAppServerClient.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import type {
3838
ThreadReadResponse,
3939
ThreadResumeParams,
4040
ThreadResumeResponse,
41+
ThreadSetNameParams,
42+
ThreadSetNameResponse,
4143
ThreadStartParams,
4244
ThreadStartResponse,
4345
ThreadUnsubscribeParams,
@@ -309,6 +311,10 @@ export class CodexAppServerClient {
309311
return await this.sendRequest({ method: "thread/archive", params: params });
310312
}
311313

314+
async threadSetName(params: ThreadSetNameParams): Promise<ThreadSetNameResponse> {
315+
return await this.sendRequest({ method: "thread/name/set", params: params });
316+
}
317+
312318
async threadUnsubscribe(params: ThreadUnsubscribeParams): Promise<ThreadUnsubscribeResponse> {
313319
return await this.sendRequest({ method: "thread/unsubscribe", params: params });
314320
}

src/CodexCommands.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ export class CodexCommands {
101101
description: "Display session configuration and token usage.",
102102
input: null
103103
},
104+
{
105+
name: "rename",
106+
description: "Rename this session.",
107+
input: { hint: "new session title" }
108+
},
104109
{
105110
name: "review",
106111
description: "Review uncommitted changes, or review with custom instructions.",
@@ -201,6 +206,19 @@ export class CodexCommands {
201206
});
202207
return { handled: true };
203208
}
209+
case "rename": {
210+
if (command.rest.length === 0) {
211+
await this.sendCommandUsageMessage(commandName, "new session title", sessionId);
212+
return { handled: true };
213+
}
214+
await this.runWithProcessCheck(() => this.codexAcpClient.setSessionTitle(sessionId, command.rest));
215+
const session = new ACPSessionConnection(this.connection, sessionId);
216+
await session.update({
217+
sessionUpdate: "agent_message_chunk",
218+
content: { type: "text", text: `Renamed session to ${JSON.stringify(command.rest)}.` }
219+
});
220+
return { handled: true };
221+
}
204222
case "logout": {
205223
await this.runWithProcessCheck(() => this.codexAcpClient.logout());
206224
await this.onLogout();

src/__tests__/CodexACPAgent/CodexAcpClient.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {AgentMode} from "../../AgentMode";
1616
import type {Model, ReviewStartResponse, TurnCompletedNotification, TurnStartParams} from "../../app-server/v2";
1717
import type {RateLimitsMap} from "../../RateLimitsMap";
1818
import {ModelId} from "../../ModelId";
19+
import {SET_SESSION_TITLE_METHOD} from "../../AcpExtensions";
1920

2021
describe('ACP server test', { timeout: 40_000 }, () => {
2122

@@ -1291,6 +1292,26 @@ describe('ACP server test', { timeout: 40_000 }, () => {
12911292
await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/command-status.json");
12921293
});
12931294

1295+
it('handles rename slash command through Codex app server', async () => {
1296+
const { mockFixture, turnStartSpy } = setupPromptFixture();
1297+
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
1298+
.mockResolvedValue({});
1299+
1300+
await mockFixture.getCodexAcpAgent().prompt({
1301+
sessionId: "session-id",
1302+
prompt: [{ type: "text", text: "/rename Quarterly review" }],
1303+
});
1304+
1305+
expect(threadSetNameSpy).toHaveBeenCalledWith({
1306+
threadId: "session-id",
1307+
name: "Quarterly review",
1308+
});
1309+
expect(turnStartSpy).not.toHaveBeenCalled();
1310+
const [event] = mockFixture.getAcpConnectionEvents([]);
1311+
expect(event).toBeDefined();
1312+
expect(event!.args[0].update.content.text).toBe('Renamed session to "Quarterly review".');
1313+
});
1314+
12941315
it('passes skill slash commands through to Codex', async () => {
12951316
const { mockFixture, turnStartSpy } = setupPromptFixture();
12961317

@@ -1519,6 +1540,39 @@ describe('ACP server test', { timeout: 40_000 }, () => {
15191540
expect(event!.args[0].update.content.text).toBe('Command "/review-branch" requires branch name.');
15201541
});
15211542

1543+
it('reports missing rename slash command input', async () => {
1544+
const { mockFixture } = setupPromptFixture();
1545+
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
1546+
.mockResolvedValue({});
1547+
1548+
await mockFixture.getCodexAcpAgent().prompt({
1549+
sessionId: "session-id",
1550+
prompt: [{ type: "text", text: "/rename" }],
1551+
});
1552+
1553+
expect(threadSetNameSpy).not.toHaveBeenCalled();
1554+
const [event] = mockFixture.getAcpConnectionEvents([]);
1555+
expect(event).toBeDefined();
1556+
expect(event!.args[0].update.content.text).toBe('Command "/rename" requires new session title.');
1557+
});
1558+
1559+
it('handles session setTitle extension through Codex app server', async () => {
1560+
const mockFixture = createCodexMockTestFixture();
1561+
vi.spyOn(mockFixture.getCodexAcpClient(), "authRequired").mockResolvedValue(false);
1562+
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
1563+
.mockResolvedValue({});
1564+
1565+
await expect(mockFixture.getCodexAcpAgent().extMethod(SET_SESSION_TITLE_METHOD, {
1566+
sessionId: "thread-id",
1567+
title: "Renamed from client",
1568+
})).resolves.toEqual({});
1569+
1570+
expect(threadSetNameSpy).toHaveBeenCalledWith({
1571+
threadId: "thread-id",
1572+
name: "Renamed from client",
1573+
});
1574+
});
1575+
15221576
it('handles logout command', async () => {
15231577
const codexAcpAgent = fixture.getCodexAcpAgent();
15241578
await codexAcpAgent.initialize({protocolVersion: 1});

src/__tests__/CodexACPAgent/data/available-commands-build-in.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
"description": "Display session configuration and token usage.",
2222
"input": null
2323
},
24+
{
25+
"name": "rename",
26+
"description": "Rename this session.",
27+
"input": {
28+
"hint": "new session title"
29+
}
30+
},
2431
{
2532
"name": "review",
2633
"description": "Review uncommitted changes, or review with custom instructions.",

src/__tests__/CodexACPAgent/data/available-commands-skills.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
"description": "Display session configuration and token usage.",
2222
"input": null
2323
},
24+
{
25+
"name": "rename",
26+
"description": "Rename this session.",
27+
"input": {
28+
"hint": "new session title"
29+
}
30+
},
2431
{
2532
"name": "review",
2633
"description": "Review uncommitted changes, or review with custom instructions.",

src/__tests__/CodexACPAgent/data/load-session-history.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
"description": "Display session configuration and token usage.",
2222
"input": null
2323
},
24+
{
25+
"name": "rename",
26+
"description": "Rename this session.",
27+
"input": {
28+
"hint": "new session title"
29+
}
30+
},
2431
{
2532
"name": "review",
2633
"description": "Review uncommitted changes, or review with custom instructions.",

src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
"description": "Display session configuration and token usage.",
2222
"input": null
2323
},
24+
{
25+
"name": "rename",
26+
"description": "Rename this session.",
27+
"input": {
28+
"hint": "new session title"
29+
}
30+
},
2431
{
2532
"name": "review",
2633
"description": "Review uncommitted changes, or review with custom instructions.",

0 commit comments

Comments
 (0)