Skip to content
Open
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
17 changes: 16 additions & 1 deletion src/AcpExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
} from "@agentclientprotocol/sdk";

export const LEGACY_SET_SESSION_MODEL_METHOD = "session/set_model";
export const SET_SESSION_TITLE_METHOD = "session/setTitle";

export type LegacySessionModel = {
modelId: string;
Expand All @@ -26,6 +27,13 @@ export type LegacySetSessionModelRequest = {

export type LegacySetSessionModelResponse = {}

export type SetSessionTitleRequest = {
sessionId: SessionId;
title: string;
}

export type SetSessionTitleResponse = {}

export type LegacyNewSessionResponse = NewSessionResponse & {
models?: LegacySessionModelState | null;
}
Expand All @@ -42,11 +50,13 @@ export type ExtMethodRequest =
AuthenticationStatusRequest
| AuthenticationLogoutRequest
| LegacySetSessionModelExtRequest
| SetSessionTitleExtRequest

export function isExtMethodRequest(request: { method: string, params: Record<string, unknown> }): request is ExtMethodRequest {
return request.method === "authentication/status"
|| request.method === "authentication/logout"
|| request.method === LEGACY_SET_SESSION_MODEL_METHOD;
|| request.method === LEGACY_SET_SESSION_MODEL_METHOD
|| request.method === SET_SESSION_TITLE_METHOD;
}

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

export type SetSessionTitleExtRequest = {
method: typeof SET_SESSION_TITLE_METHOD;
params: SetSessionTitleRequest;
}

export async function legacySetSessionModel(
connection: Pick<ClientContext, "request">,
params: LegacySetSessionModelRequest,
Expand Down
7 changes: 7 additions & 0 deletions src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,13 @@ export class CodexAcpClient {
await this.codexClient.threadArchive({threadId: sessionId});
}

async setSessionTitle(sessionId: string, title: string): Promise<void> {
await this.codexClient.threadSetName({
threadId: sessionId,
name: title,
});
}

async runReview(
sessionId: string,
target: ReviewTarget,
Expand Down
43 changes: 36 additions & 7 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ import {
type LegacySessionModelState,
type LegacySetSessionModelRequest,
type LegacySetSessionModelResponse,
type SetSessionTitleRequest,
type SetSessionTitleResponse,
isExtMethodRequest,
LEGACY_SET_SESSION_MODEL_METHOD,
SET_SESSION_TITLE_METHOD,
} from "./AcpExtensions";
import {
createCollabAgentToolCallUpdate,
Expand Down Expand Up @@ -175,6 +178,14 @@ export class CodexAcpServer {
this.clientInfo = _params.clientInfo ?? null;
this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities);
await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
const sessionCapabilities = {
resume: { },
list: { },
close: { },
delete: { },
additionalDirectories: {},
setTitle: {},
};
return {
protocolVersion: acp.PROTOCOL_VERSION,
agentInfo: {
Expand All @@ -191,13 +202,7 @@ export class CodexAcpServer {
embeddedContext: true,
image: true
},
sessionCapabilities: {
resume: { },
list: { },
close: { },
delete: { },
additionalDirectories: {},
},
sessionCapabilities,
mcpCapabilities: {
acp: false,
http: true,
Expand All @@ -222,6 +227,8 @@ export class CodexAcpServer {
}
case LEGACY_SET_SESSION_MODEL_METHOD:
return await this.unstable_setSessionModel(this.parseLegacySetSessionModelParams(methodRequest.params));
case SET_SESSION_TITLE_METHOD:
return await this.setSessionTitle(this.parseSetSessionTitleParams(methodRequest.params));
}
}

Expand Down Expand Up @@ -676,6 +683,16 @@ export class CodexAcpServer {
};
}

async setSessionTitle(params: SetSessionTitleRequest): Promise<SetSessionTitleResponse> {
logger.log("Set session title requested", {
sessionId: params.sessionId,
titleLength: params.title.length,
});
await this.checkAuthorization();
await this.runWithProcessCheck(() => this.codexAcpClient.setSessionTitle(params.sessionId, params.title));
return {};
}

private applyFastModeChange(sessionState: SessionState, value: string): void {
if (value !== FAST_MODE_ON && value !== FAST_MODE_OFF) {
throw RequestError.invalidParams();
Expand Down Expand Up @@ -765,6 +782,18 @@ export class CodexAcpServer {
};
}

private parseSetSessionTitleParams(params: Record<string, unknown>): SetSessionTitleRequest {
const sessionId = params["sessionId"];
const title = params["title"];
if (typeof sessionId !== "string" || typeof title !== "string") {
throw RequestError.invalidParams();
}
return {
sessionId: sessionId,
title: title,
};
}

private createSessionConfigOptions(sessionState: SessionState): Array<acp.SessionConfigOption> {
const currentModelId = ModelId.fromString(sessionState.currentModelId);
const configOptions = [
Expand Down
6 changes: 6 additions & 0 deletions src/CodexAppServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import type {
ThreadReadResponse,
ThreadResumeParams,
ThreadResumeResponse,
ThreadSetNameParams,
ThreadSetNameResponse,
ThreadStartParams,
ThreadStartResponse,
ThreadUnsubscribeParams,
Expand Down Expand Up @@ -309,6 +311,10 @@ export class CodexAppServerClient {
return await this.sendRequest({ method: "thread/archive", params: params });
}

async threadSetName(params: ThreadSetNameParams): Promise<ThreadSetNameResponse> {
return await this.sendRequest({ method: "thread/name/set", params: params });
}

async threadUnsubscribe(params: ThreadUnsubscribeParams): Promise<ThreadUnsubscribeResponse> {
return await this.sendRequest({ method: "thread/unsubscribe", params: params });
}
Expand Down
18 changes: 18 additions & 0 deletions src/CodexCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export class CodexCommands {
description: "Display session configuration and token usage.",
input: null
},
{
name: "rename",
description: "Rename this session.",
input: { hint: "new session title" }
},
{
name: "review",
description: "Review uncommitted changes, or review with custom instructions.",
Expand Down Expand Up @@ -201,6 +206,19 @@ export class CodexCommands {
});
return { handled: true };
}
case "rename": {
if (command.rest.length === 0) {
await this.sendCommandUsageMessage(commandName, "new session title", sessionId);
return { handled: true };
}
await this.runWithProcessCheck(() => this.codexAcpClient.setSessionTitle(sessionId, command.rest));
const session = new ACPSessionConnection(this.connection, sessionId);
await session.update({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: `Renamed session to ${JSON.stringify(command.rest)}.` }
});
return { handled: true };
}
case "logout": {
await this.runWithProcessCheck(() => this.codexAcpClient.logout());
await this.onLogout();
Expand Down
54 changes: 54 additions & 0 deletions src/__tests__/CodexACPAgent/CodexAcpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {AgentMode} from "../../AgentMode";
import type {Model, ReviewStartResponse, TurnCompletedNotification, TurnStartParams} from "../../app-server/v2";
import type {RateLimitsMap} from "../../RateLimitsMap";
import {ModelId} from "../../ModelId";
import {SET_SESSION_TITLE_METHOD} from "../../AcpExtensions";

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

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

it('handles rename slash command through Codex app server', async () => {
const { mockFixture, turnStartSpy } = setupPromptFixture();
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
.mockResolvedValue({});

await mockFixture.getCodexAcpAgent().prompt({
sessionId: "session-id",
prompt: [{ type: "text", text: "/rename Quarterly review" }],
});

expect(threadSetNameSpy).toHaveBeenCalledWith({
threadId: "session-id",
name: "Quarterly review",
});
expect(turnStartSpy).not.toHaveBeenCalled();
const [event] = mockFixture.getAcpConnectionEvents([]);
expect(event).toBeDefined();
expect(event!.args[0].update.content.text).toBe('Renamed session to "Quarterly review".');
});

it('passes skill slash commands through to Codex', async () => {
const { mockFixture, turnStartSpy } = setupPromptFixture();

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

it('reports missing rename slash command input', async () => {
const { mockFixture } = setupPromptFixture();
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
.mockResolvedValue({});

await mockFixture.getCodexAcpAgent().prompt({
sessionId: "session-id",
prompt: [{ type: "text", text: "/rename" }],
});

expect(threadSetNameSpy).not.toHaveBeenCalled();
const [event] = mockFixture.getAcpConnectionEvents([]);
expect(event).toBeDefined();
expect(event!.args[0].update.content.text).toBe('Command "/rename" requires new session title.');
});

it('handles session setTitle extension through Codex app server', async () => {
const mockFixture = createCodexMockTestFixture();
vi.spyOn(mockFixture.getCodexAcpClient(), "authRequired").mockResolvedValue(false);
const threadSetNameSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "threadSetName")
.mockResolvedValue({});

await expect(mockFixture.getCodexAcpAgent().extMethod(SET_SESSION_TITLE_METHOD, {
sessionId: "thread-id",
title: "Renamed from client",
})).resolves.toEqual({});

expect(threadSetNameSpy).toHaveBeenCalledWith({
threadId: "thread-id",
name: "Renamed from client",
});
});

it('handles logout command', async () => {
const codexAcpAgent = fixture.getCodexAcpAgent();
await codexAcpAgent.initialize({protocolVersion: 1});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"description": "Display session configuration and token usage.",
"input": null
},
{
"name": "rename",
"description": "Rename this session.",
"input": {
"hint": "new session title"
}
},
{
"name": "review",
"description": "Review uncommitted changes, or review with custom instructions.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"description": "Display session configuration and token usage.",
"input": null
},
{
"name": "rename",
"description": "Rename this session.",
"input": {
"hint": "new session title"
}
},
{
"name": "review",
"description": "Review uncommitted changes, or review with custom instructions.",
Expand Down
7 changes: 7 additions & 0 deletions src/__tests__/CodexACPAgent/data/load-session-history.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"description": "Display session configuration and token usage.",
"input": null
},
{
"name": "rename",
"description": "Rename this session.",
"input": {
"hint": "new session title"
}
},
{
"name": "review",
"description": "Review uncommitted changes, or review with custom instructions.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"description": "Display session configuration and token usage.",
"input": null
},
{
"name": "rename",
"description": "Rename this session.",
"input": {
"hint": "new session title"
}
},
{
"name": "review",
"description": "Review uncommitted changes, or review with custom instructions.",
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/CodexACPAgent/initialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('CodexACPAgent - initialize', () => {
close: {},
delete: {},
additionalDirectories: {},
setTitle: {},
},
mcpCapabilities: {
acp: false,
Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import packageJson from "../package.json";
import {logger} from "./Logger";
import {runLoginCommand} from "./login";
import {runCodexCli} from "./CodexCli";
import {LEGACY_SET_SESSION_MODEL_METHOD} from "./AcpExtensions";
import {LEGACY_SET_SESSION_MODEL_METHOD, SET_SESSION_TITLE_METHOD} from "./AcpExtensions";

const emptyExtensionParamsParser = z.preprocess(
(params) => params ?? {},
Expand All @@ -24,6 +24,11 @@ const legacySetSessionModelParamsParser = z.object({
modelId: z.string(),
}).passthrough();

const setSessionTitleParamsParser = z.object({
sessionId: z.string(),
title: z.string(),
}).passthrough();

if (process.argv.includes("--version")) {
console.log(`${packageJson.name} ${packageJson.version}`);
process.exit(0);
Expand Down Expand Up @@ -129,5 +134,6 @@ function startAcpServer() {
.onRequest("authentication/status", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/status", ctx.params))
.onRequest("authentication/logout", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/logout", ctx.params))
.onRequest(LEGACY_SET_SESSION_MODEL_METHOD, legacySetSessionModelParamsParser, (ctx) => getAgent().extMethod(LEGACY_SET_SESSION_MODEL_METHOD, ctx.params))
.onRequest(SET_SESSION_TITLE_METHOD, setSessionTitleParamsParser, (ctx) => getAgent().extMethod(SET_SESSION_TITLE_METHOD, ctx.params))
.connect(acpJsonStream);
}