Skip to content

Commit bcd632e

Browse files
Merge branch 'main' into codex/plan-command-action
2 parents 303bca9 + f3f1b3c commit bcd632e

21 files changed

Lines changed: 747 additions & 50 deletions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "1.1.2",
6+
"version": "1.1.3",
77
"description": "",
88
"main": "dist/index.js",
99
"bin": {

src/CodexAcpClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,10 @@ export class CodexAcpClient {
621621
await this.waitForSessionNotifications(sessionId);
622622
return await elicitationHandler.handleElicitation(params);
623623
},
624+
handleUserInput: async (params) => {
625+
await this.waitForSessionNotifications(sessionId);
626+
return await elicitationHandler.handleUserInput(params);
627+
},
624628
});
625629
}
626630

src/CodexAcpServer.ts

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
} from "./AcpExtensions";
5454
import {
5555
createCollabAgentToolCallUpdate,
56+
createCompletedContextCompactionUpdate,
5657
createCommandExecutionCompleteUpdate,
5758
createCommandExecutionUpdate,
5859
createDynamicToolCallUpdate,
@@ -110,6 +111,8 @@ export interface SessionState {
110111
sessionMcpServers?: Array<string>;
111112
terminalOutputMode: TerminalOutputMode;
112113
currentGoal?: ThreadGoalSnapshot | null;
114+
sessionTitle: string | null;
115+
sessionTitleSource: "unset" | "fallback" | "explicit" | "unknown";
113116
}
114117

115118
interface ActiveAuthState {
@@ -425,6 +428,8 @@ export class CodexAcpServer {
425428
currentModelSupportsFast: currentModelSupportsFast,
426429
sessionMcpServers: sessionMcpServers,
427430
terminalOutputMode: this.terminalOutputMode,
431+
sessionTitle: null,
432+
sessionTitleSource: "sessionId" in request ? "unknown" : "unset",
428433
};
429434
this.sessions.set(sessionId, sessionState);
430435
resumeSubscribed = false;
@@ -972,6 +977,8 @@ export class CodexAcpServer {
972977
currentModelSupportsFast: currentModelSupportsFast,
973978
sessionMcpServers: sessionMcpServers,
974979
terminalOutputMode: this.terminalOutputMode,
980+
sessionTitle: null,
981+
sessionTitleSource: "unset",
975982
};
976983
this.sessions.set(sessionId, sessionState);
977984
subscribed = false;
@@ -999,6 +1006,7 @@ export class CodexAcpServer {
9991006
private async streamThreadHistory(sessionId: string, thread: Thread): Promise<void> {
10001007
const session = new ACPSessionConnection(this.connection, sessionId);
10011008
const sessionState = this.getSessionState(sessionId);
1009+
await this.publishThreadHistoryTitle(session, sessionState, thread);
10021010
const responseItemFallbackUpdates = await createResponseItemHistoryFallbackUpdates(
10031011
thread,
10041012
sessionState.terminalOutputMode,
@@ -1020,6 +1028,67 @@ export class CodexAcpServer {
10201028
}
10211029
}
10221030

1031+
private async publishThreadHistoryTitle(
1032+
session: ACPSessionConnection,
1033+
sessionState: SessionState,
1034+
thread: Thread,
1035+
): Promise<void> {
1036+
const explicitTitle = this.normalizeSessionTitle(thread.name);
1037+
if (explicitTitle) {
1038+
sessionState.sessionTitle = explicitTitle;
1039+
sessionState.sessionTitleSource = "explicit";
1040+
await session.update({
1041+
sessionUpdate: "session_info_update",
1042+
title: explicitTitle,
1043+
});
1044+
return;
1045+
}
1046+
1047+
const historyTitle = this.findFirstUserMessageTitle(thread)
1048+
?? this.normalizeSessionTitle(thread.preview);
1049+
await this.publishFallbackSessionTitle(sessionState, historyTitle);
1050+
}
1051+
1052+
private findFirstUserMessageTitle(thread: Thread): string | null {
1053+
for (const turn of thread.turns) {
1054+
for (const item of turn.items) {
1055+
if (item.type !== "userMessage") continue;
1056+
const title = this.normalizeSessionTitle(item.content
1057+
.filter((input): input is Extract<UserInput, {type: "text"}> => input.type === "text")
1058+
.map(input => input.text)
1059+
.join(" "));
1060+
if (title) return title;
1061+
}
1062+
}
1063+
return null;
1064+
}
1065+
1066+
private async publishFallbackSessionTitle(
1067+
sessionState: SessionState,
1068+
title: string | null,
1069+
): Promise<void> {
1070+
if (sessionState.sessionTitleSource !== "unset" || !title) return;
1071+
sessionState.sessionTitle = title;
1072+
sessionState.sessionTitleSource = "fallback";
1073+
const session = new ACPSessionConnection(this.connection, sessionState.sessionId);
1074+
await session.update({
1075+
sessionUpdate: "session_info_update",
1076+
title,
1077+
});
1078+
}
1079+
1080+
private createPromptFallbackTitle(prompt: acp.ContentBlock[]): string | null {
1081+
return this.normalizeSessionTitle(prompt
1082+
.filter((block): block is Extract<acp.ContentBlock, {type: "text"}> => block.type === "text")
1083+
.map(block => block.text)
1084+
.join(" "));
1085+
}
1086+
1087+
private normalizeSessionTitle(title: string | null | undefined): string | null {
1088+
const normalized = title?.replace(/\s+/g, " ").trim() ?? "";
1089+
return normalized.length > 0 ? normalized : null;
1090+
}
1091+
10231092
private async createHistoryUpdates(item: ThreadItem, sessionState: SessionState): Promise<UpdateSessionEvent[]> {
10241093
switch (item.type) {
10251094
case "userMessage":
@@ -1066,7 +1135,7 @@ export class CodexAcpServer {
10661135
case "exitedReviewMode":
10671136
return [this.createReviewModeUpdate(item, false)];
10681137
case "contextCompaction":
1069-
return [this.createContextCompactionUpdate()];
1138+
return [createCompletedContextCompactionUpdate(item)];
10701139
case "plan":
10711140
return [this.createPlanUpdate(item)];
10721141
}
@@ -1119,16 +1188,6 @@ export class CodexAcpServer {
11191188
};
11201189
}
11211190

1122-
private createContextCompactionUpdate(): UpdateSessionEvent {
1123-
return {
1124-
sessionUpdate: "agent_message_chunk",
1125-
content: {
1126-
type: "text",
1127-
text: "Context compacted.",
1128-
},
1129-
};
1130-
}
1131-
11321191
private createPlanUpdate(
11331192
item: ThreadItem & { type: "plan" }
11341193
): UpdateSessionEvent {
@@ -1636,6 +1695,11 @@ export class CodexAcpServer {
16361695
throw error;
16371696
}
16381697

1698+
await this.publishFallbackSessionTitle(
1699+
sessionState,
1700+
this.createPromptFallbackTitle(params.prompt),
1701+
);
1702+
16391703
return {
16401704
stopReason: "end_turn",
16411705
usage: this.buildPromptUsage(sessionState.lastTokenUsage),

src/CodexAppServerClient.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import type {
5151
ThreadStartResponse,
5252
ThreadUnsubscribeParams,
5353
ThreadUnsubscribeResponse,
54+
ToolRequestUserInputParams,
55+
ToolRequestUserInputResponse,
5456
TurnCompletedNotification,
5557
TurnInterruptParams,
5658
TurnInterruptResponse,
@@ -73,6 +75,7 @@ export interface ApprovalHandler {
7375

7476
export interface ElicitationHandler {
7577
handleElicitation(params: McpServerElicitationRequestParams): Promise<McpServerElicitationRequestResponse>;
78+
handleUserInput(params: ToolRequestUserInputParams): Promise<ToolRequestUserInputResponse>;
7679
}
7780

7881
export type McpStartupFailure = {
@@ -110,6 +113,12 @@ const McpServerElicitationRequest = new RequestType<
110113
void
111114
>('mcpServer/elicitation/request');
112115

116+
const ToolRequestUserInputRequest = new RequestType<
117+
ToolRequestUserInputParams,
118+
ToolRequestUserInputResponse,
119+
void
120+
>('item/tool/requestUserInput');
121+
113122
const GOAL_RUNTIME_EFFECTS_GRACE_MS = 1_000;
114123

115124
/**
@@ -217,6 +226,17 @@ export class CodexAppServerClient {
217226
}
218227
return await handler.handleElicitation(params);
219228
});
229+
230+
this.connection.onRequest(ToolRequestUserInputRequest, async (params) => {
231+
if (this.isStaleTurn(params.threadId, params.turnId)) {
232+
return { answers: {} };
233+
}
234+
const handler = this.elicitationHandlers.get(params.threadId);
235+
if (!handler) {
236+
return { answers: {} };
237+
}
238+
return await handler.handleUserInput(params);
239+
});
220240
}
221241

222242
onApprovalRequest(threadId: string, handler: ApprovalHandler): void {

src/CodexCommands.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,8 @@ export class CodexCommands {
282282
return { handled: true };
283283
}
284284
default:
285-
await this.sendUnknownCommandMessage(commandName, sessionId);
286-
return { handled: true };
285+
// Let Codex resolve unrecognized commands as raw prompts.
286+
return { handled: false };
287287
}
288288
}
289289

@@ -385,19 +385,6 @@ export class CodexCommands {
385385
await session.update(createAgentTextMessageChunk(`Command "/${name}" requires ${inputHint}.`));
386386
}
387387

388-
private async sendUnknownCommandMessage(name: string, sessionId: string): Promise<void> {
389-
const lines = this.getBuiltinCommands().map(command => `- /${command.name}: ${command.description}`);
390-
const text = [
391-
`Unknown command "/${name}".`,
392-
"Available commands:"
393-
];
394-
if (lines.length > 0) {
395-
text.push(...lines);
396-
}
397-
const session = new ACPSessionConnection(this.connection, sessionId);
398-
await session.update(createAgentTextMessageChunk(text.join("\n")));
399-
}
400-
401388
private buildStatusMessage(sessionState: SessionState): string {
402389
const agentMode = sessionState.agentMode;
403390
const accountText = this.formatAccountInfo(sessionState.account);

0 commit comments

Comments
 (0)