Skip to content

Commit faeb0ca

Browse files
Add session rename support
1 parent 7fb6ddd commit faeb0ca

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
@@ -319,6 +319,13 @@ export class CodexAcpClient {
319319
await this.codexClient.threadArchive({threadId: sessionId});
320320
}
321321

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

src/CodexAcpServer.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ import {
4141
type LegacySessionModelState,
4242
type LegacySetSessionModelRequest,
4343
type LegacySetSessionModelResponse,
44+
type SetSessionTitleRequest,
45+
type SetSessionTitleResponse,
4446
isExtMethodRequest,
4547
LEGACY_SET_SESSION_MODEL_METHOD,
48+
SET_SESSION_TITLE_METHOD,
4649
} from "./AcpExtensions";
4750
import {
4851
createCollabAgentToolCallUpdate,
@@ -183,6 +186,14 @@ export class CodexAcpServer {
183186
this.clientInfo = _params.clientInfo ?? null;
184187
this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities);
185188
await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
189+
const sessionCapabilities = {
190+
resume: { },
191+
list: { },
192+
close: { },
193+
delete: { },
194+
additionalDirectories: {},
195+
setTitle: {},
196+
};
186197
return {
187198
protocolVersion: acp.PROTOCOL_VERSION,
188199
agentInfo: {
@@ -199,13 +210,7 @@ export class CodexAcpServer {
199210
embeddedContext: true,
200211
image: true
201212
},
202-
sessionCapabilities: {
203-
resume: { },
204-
list: { },
205-
close: { },
206-
delete: { },
207-
additionalDirectories: {},
208-
},
213+
sessionCapabilities,
209214
mcpCapabilities: {
210215
acp: false,
211216
http: true,
@@ -230,6 +235,8 @@ export class CodexAcpServer {
230235
}
231236
case LEGACY_SET_SESSION_MODEL_METHOD:
232237
return await this.unstable_setSessionModel(this.parseLegacySetSessionModelParams(methodRequest.params));
238+
case SET_SESSION_TITLE_METHOD:
239+
return await this.setSessionTitle(this.parseSetSessionTitleParams(methodRequest.params));
233240
}
234241
}
235242

@@ -684,6 +691,16 @@ export class CodexAcpServer {
684691
};
685692
}
686693

694+
async setSessionTitle(params: SetSessionTitleRequest): Promise<SetSessionTitleResponse> {
695+
logger.log("Set session title requested", {
696+
sessionId: params.sessionId,
697+
titleLength: params.title.length,
698+
});
699+
await this.checkAuthorization();
700+
await this.runWithProcessCheck(() => this.codexAcpClient.setSessionTitle(params.sessionId, params.title));
701+
return {};
702+
}
703+
687704
private applyFastModeChange(sessionState: SessionState, value: string): void {
688705
if (value !== FAST_MODE_ON && value !== FAST_MODE_OFF) {
689706
throw RequestError.invalidParams();
@@ -773,6 +790,18 @@ export class CodexAcpServer {
773790
};
774791
}
775792

793+
private parseSetSessionTitleParams(params: Record<string, unknown>): SetSessionTitleRequest {
794+
const sessionId = params["sessionId"];
795+
const title = params["title"];
796+
if (typeof sessionId !== "string" || typeof title !== "string") {
797+
throw RequestError.invalidParams();
798+
}
799+
return {
800+
sessionId: sessionId,
801+
title: title,
802+
};
803+
}
804+
776805
private createSessionConfigOptions(sessionState: SessionState): Array<acp.SessionConfigOption> {
777806
const currentModelId = ModelId.fromString(sessionState.currentModelId);
778807
const configOptions = [

src/CodexAppServerClient.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import type {
4747
ThreadReadResponse,
4848
ThreadResumeParams,
4949
ThreadResumeResponse,
50+
ThreadSetNameParams,
51+
ThreadSetNameResponse,
5052
ThreadStartParams,
5153
ThreadStartResponse,
5254
ThreadUnsubscribeParams,
@@ -509,6 +511,10 @@ export class CodexAppServerClient {
509511
return await this.sendRequest({ method: "thread/archive", params: params });
510512
}
511513

514+
async threadSetName(params: ThreadSetNameParams): Promise<ThreadSetNameResponse> {
515+
return await this.sendRequest({ method: "thread/name/set", params: params });
516+
}
517+
512518
async threadUnsubscribe(params: ThreadUnsubscribeParams): Promise<ThreadUnsubscribeResponse> {
513519
return await this.sendRequest({ method: "thread/unsubscribe", params: params });
514520
}

src/CodexCommands.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ export class CodexCommands {
108108
description: "Display session configuration and token usage.",
109109
input: null
110110
},
111+
{
112+
name: "rename",
113+
description: "Rename this session.",
114+
input: { hint: "new session title" }
115+
},
111116
{
112117
name: "review",
113118
description: "Review uncommitted changes, or review with custom instructions.",
@@ -216,6 +221,19 @@ export class CodexCommands {
216221
});
217222
return { handled: true };
218223
}
224+
case "rename": {
225+
if (command.rest.length === 0) {
226+
await this.sendCommandUsageMessage(commandName, "new session title", sessionId);
227+
return { handled: true };
228+
}
229+
await this.runWithProcessCheck(() => this.codexAcpClient.setSessionTitle(sessionId, command.rest));
230+
const session = new ACPSessionConnection(this.connection, sessionId);
231+
await session.update({
232+
sessionUpdate: "agent_message_chunk",
233+
content: { type: "text", text: `Renamed session to ${JSON.stringify(command.rest)}.` }
234+
});
235+
return { handled: true };
236+
}
219237
case "logout": {
220238
await this.runWithProcessCheck(() => this.codexAcpClient.logout());
221239
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, ThreadGoal, 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

@@ -1306,6 +1307,26 @@ describe('ACP server test', { timeout: 40_000 }, () => {
13061307
await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/command-status.json");
13071308
});
13081309

1310+
it('handles rename slash command through Codex app server', async () => {
1311+
const { mockFixture, turnStartSpy } = setupPromptFixture();
1312+
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
1313+
.mockResolvedValue({});
1314+
1315+
await mockFixture.getCodexAcpAgent().prompt({
1316+
sessionId: "session-id",
1317+
prompt: [{ type: "text", text: "/rename Quarterly review" }],
1318+
});
1319+
1320+
expect(threadSetNameSpy).toHaveBeenCalledWith({
1321+
threadId: "session-id",
1322+
name: "Quarterly review",
1323+
});
1324+
expect(turnStartSpy).not.toHaveBeenCalled();
1325+
const [event] = mockFixture.getAcpConnectionEvents([]);
1326+
expect(event).toBeDefined();
1327+
expect(event!.args[0].update.content.text).toBe('Renamed session to "Quarterly review".');
1328+
});
1329+
13091330
it('passes skill slash commands through to Codex', async () => {
13101331
const { mockFixture, turnStartSpy } = setupPromptFixture();
13111332

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

2642+
it('reports missing rename slash command input', async () => {
2643+
const { mockFixture } = setupPromptFixture();
2644+
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
2645+
.mockResolvedValue({});
2646+
2647+
await mockFixture.getCodexAcpAgent().prompt({
2648+
sessionId: "session-id",
2649+
prompt: [{ type: "text", text: "/rename" }],
2650+
});
2651+
2652+
expect(threadSetNameSpy).not.toHaveBeenCalled();
2653+
const [event] = mockFixture.getAcpConnectionEvents([]);
2654+
expect(event).toBeDefined();
2655+
expect(event!.args[0].update.content.text).toBe('Command "/rename" requires new session title.');
2656+
});
2657+
2658+
it('handles session setTitle extension through Codex app server', async () => {
2659+
const mockFixture = createCodexMockTestFixture();
2660+
vi.spyOn(mockFixture.getCodexAcpClient(), "authRequired").mockResolvedValue(false);
2661+
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
2662+
.mockResolvedValue({});
2663+
2664+
await expect(mockFixture.getCodexAcpAgent().extMethod(SET_SESSION_TITLE_METHOD, {
2665+
sessionId: "thread-id",
2666+
title: "Renamed from client",
2667+
})).resolves.toEqual({});
2668+
2669+
expect(threadSetNameSpy).toHaveBeenCalledWith({
2670+
threadId: "thread-id",
2671+
name: "Renamed from client",
2672+
});
2673+
});
2674+
26212675
it('handles logout command', async () => {
26222676
const codexAcpAgent = fixture.getCodexAcpAgent();
26232677
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)