Skip to content
79 changes: 73 additions & 6 deletions src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export class CodexAcpClient {
return this.codexClient.accountRead({refreshToken: false});
}

async resumeSession(request: acp.ResumeSessionRequest): Promise<SessionMetadata> {
async resumeSession(request: acp.ResumeSessionRequest, onSubscribed?: () => void): Promise<SessionMetadata> {
await this.refreshSkills(request.cwd, request._meta);

const response = await this.codexClient.threadResume({
Expand All @@ -208,6 +208,7 @@ export class CodexAcpClient {
modelProvider: this.getResumeModelProvider(),
threadId: request.sessionId,
});
onSubscribed?.();
const codexModels = await this.fetchAvailableModels();
const currentModelId = this.createModelId(codexModels, response.model, response.reasoningEffort).toString();
return {
Expand All @@ -218,13 +219,14 @@ export class CodexAcpClient {
}
}

async loadSession(request: acp.LoadSessionRequest): Promise<SessionMetadataWithThread> {
async loadSession(request: acp.LoadSessionRequest, onSubscribed?: () => void): Promise<SessionMetadataWithThread> {
const response = await this.codexClient.threadResume({
config: await this.createSessionConfig(request.cwd, request.mcpServers ?? []),
cwd: request.cwd,
modelProvider: this.getResumeModelProvider(),
threadId: request.sessionId,
});
onSubscribed?.();
const codexModels = await this.fetchAvailableModels();
const currentModelId = this.createModelId(codexModels, response.model, response.reasoningEffort).toString();
return {
Expand Down Expand Up @@ -371,11 +373,35 @@ export class CodexAcpClient {
sessionId: string,
eventHandler: (result: ServerNotification) => void,
approvalHandler: ApprovalHandler,
elicitationHandler: ElicitationHandler
) {
elicitationHandler: ElicitationHandler,
cancellation?: Promise<void>,
): Promise<boolean> {
const pendingTurnStartFence = this.codexClient.pendingTurnStartFence(sessionId);
if (pendingTurnStartFence) {
if (cancellation) {
const fenceResult = await Promise.race([
pendingTurnStartFence.then(() => "ready" as const),
cancellation.then(() => "cancelled" as const),
]);
if (fenceResult === "cancelled") {
return false;
}
} else {
await pendingTurnStartFence;
}
}
this.codexClient.onServerNotification(sessionId, eventHandler);
this.codexClient.onApprovalRequest(sessionId, approvalHandler);
this.codexClient.onElicitationRequest(sessionId, elicitationHandler);
return true;
}

disposeSession(sessionId: string): void {
this.codexClient.clearSessionHandlers(sessionId);
}

async closeSession(sessionId: string): Promise<void> {
await this.codexClient.threadUnsubscribe({ threadId: sessionId });
}

async sendPrompt(
Expand All @@ -385,11 +411,29 @@ export class CodexAcpClient {
serviceTier: ServiceTier | null,
disableSummary: boolean,
cwd: string,
onTurnStarted?: (turnId: string) => void,
onTurnStartRequested?: () => void,
isCancelled?: () => boolean,
cancellation?: Promise<TurnCompletedNotification>,
): Promise<TurnCompletedNotification> {
const input = buildPromptItems(request.prompt);
const effort = modelId.effort as ReasoningEffort | null; //TODO remove unsafe conversion

await this.refreshSkills(cwd, request._meta);
if (cancellation) {
const refreshResult = await Promise.race([
this.refreshSkills(cwd, request._meta).then(() => null),
cancellation,
]);
if (refreshResult !== null) {
return refreshResult;
}
} else {
await this.refreshSkills(cwd, request._meta);
}
if (isCancelled?.()) {
return createInterruptedTurnCompleted(request.sessionId, "cancelled-before-turn");
}
onTurnStartRequested?.();
return await this.codexClient.runTurn({
threadId: request.sessionId,
input: input,
Expand All @@ -399,7 +443,7 @@ export class CodexAcpClient {
effort: effort,
model: modelId.model,
serviceTier: serviceTier,
});
}, onTurnStarted, cancellation);
}

async listSkills(params?: SkillsListParams): Promise<SkillsListResponse> {
Expand Down Expand Up @@ -524,6 +568,14 @@ export class CodexAcpClient {
});
}

resolveInterruptedTurn(params: { threadId: string, turnId: string }): void {
this.codexClient.resolveTurnInterrupted(params.threadId, params.turnId);
}

fenceCancelledTurn(params: { threadId: string, turnId: string }): void {
this.codexClient.fenceCancelledTurn(params.threadId, params.turnId);
}

async fetchAvailableModels(): Promise<Model[]> {
const models: Model[] = [];
let cursor: string | null = null;
Expand Down Expand Up @@ -575,6 +627,21 @@ export type SessionMetadataWithThread = SessionMetadata & {
thread: Thread,
}

function createInterruptedTurnCompleted(threadId: string, turnId: string): TurnCompletedNotification {
return {
threadId,
turn: {
id: turnId,
items: [],
status: "interrupted",
error: null,
startedAt: null,
completedAt: null,
durationMs: null,
},
};
}

function buildPromptItems(prompt: acp.ContentBlock[]): UserInput[] {
return prompt.map((block): UserInput | null => {
switch (block.type) {
Expand Down
Loading