Skip to content

Commit d332389

Browse files
authored
Add support for listing active and loaded sessions via listSessions (#37)
Enable session history playback during `loadSession`. Stream historical updates such as user/agent messages, reasoning, file changes, and tool calls.
1 parent 0478bfb commit d332389

11 files changed

Lines changed: 1063 additions & 195 deletions

src/CodexAcpClient.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ import type {
1414
import type {JsonValue} from "./app-server/serde_json/JsonValue";
1515
import {ModelId} from "./ModelId";
1616
import {AgentMode} from "./AgentMode";
17+
import path from "node:path";
18+
import {logger} from "./Logger";
1719
import type {
1820
GetAccountResponse,
1921
ListMcpServerStatusParams,
2022
ListMcpServerStatusResponse,
2123
Model,
2224
SkillsListParams,
2325
SkillsListResponse,
26+
Thread,
27+
ThreadSourceKind,
2428
TurnCompletedNotification,
2529
UserInput,
2630
} from "./app-server/v2";
@@ -198,6 +202,31 @@ export class CodexAcpClient {
198202
}
199203
}
200204

205+
async loadSession(request: acp.LoadSessionRequest): Promise<SessionMetadataWithThread> {
206+
const response = await this.codexClient.threadResume({
207+
approvalPolicy: null,
208+
sandbox: null,
209+
baseInstructions: null,
210+
config: this.createSessionConfig(request.cwd, request.mcpServers ?? []),
211+
cwd: request.cwd,
212+
developerInstructions: null,
213+
history: null,
214+
model: null,
215+
modelProvider: this.getModelProvider(),
216+
path: null,
217+
personality: null,
218+
threadId: request.sessionId,
219+
});
220+
const codexModels = await this.fetchAvailableModels();
221+
const currentModelId = this.createModelId(codexModels, response.model, response.reasoningEffort).toString();
222+
return {
223+
sessionId: request.sessionId,
224+
currentModelId: currentModelId,
225+
models: codexModels,
226+
thread: response.thread,
227+
};
228+
}
229+
201230
async newSession(request: acp.NewSessionRequest): Promise<SessionMetadata> {
202231
const response = await this.codexClient.threadStart({
203232
config: this.createSessionConfig(request.cwd, request.mcpServers),
@@ -336,6 +365,73 @@ export class CodexAcpClient {
336365
return this.codexClient.listMcpServerStatus(params);
337366
}
338367

368+
async listSessions(request: acp.ListSessionsRequest): Promise<acp.ListSessionsResponse> {
369+
const sourceKinds: ThreadSourceKind[] = [
370+
"cli",
371+
"vscode",
372+
"exec",
373+
"appServer",
374+
"subAgent",
375+
"subAgentReview",
376+
"subAgentCompact",
377+
"subAgentThreadSpawn",
378+
"subAgentOther",
379+
"unknown",
380+
];
381+
const requestedCwd = request.cwd?.trim() ?? null;
382+
const filterByCwd = (thread: Thread): boolean => {
383+
if (!requestedCwd) return true;
384+
if (path.isAbsolute(requestedCwd)) {
385+
return thread.cwd === requestedCwd;
386+
}
387+
const requestedBase = path.basename(requestedCwd);
388+
return path.basename(thread.cwd) === requestedBase;
389+
};
390+
391+
const preferredProvider = this.getModelProvider();
392+
const modelProviders = preferredProvider ? [preferredProvider] : [];
393+
const listResponse = await this.codexClient.threadList({
394+
cursor: request.cursor ?? null,
395+
limit: null,
396+
sortKey: null,
397+
modelProviders: modelProviders,
398+
sourceKinds: sourceKinds,
399+
archived: null,
400+
});
401+
402+
if (listResponse.data.length === 0) {
403+
const diagnostics = await this.runSessionListDiagnostics();
404+
logger.log("Session list diagnostics", diagnostics);
405+
}
406+
407+
let sessions = listResponse.data.map((thread) => ({
408+
sessionId: thread.id,
409+
cwd: thread.cwd,
410+
title: thread.preview || null,
411+
updatedAt: new Date(thread.updatedAt * 1000).toISOString(),
412+
}));
413+
if (requestedCwd) {
414+
const filtered = listResponse.data
415+
.filter(filterByCwd)
416+
.map((thread) => ({
417+
sessionId: thread.id,
418+
cwd: thread.cwd,
419+
title: thread.preview || null,
420+
updatedAt: new Date(thread.updatedAt * 1000).toISOString(),
421+
}));
422+
if (filtered.length > 0 || path.isAbsolute(requestedCwd)) {
423+
sessions = filtered;
424+
} else {
425+
logger.log("Ignoring non-absolute cwd filter for session/list", {cwd: requestedCwd});
426+
}
427+
}
428+
429+
return {
430+
sessions,
431+
nextCursor: listResponse.nextCursor ?? null,
432+
};
433+
}
434+
339435
async turnInterrupt(params: { threadId: string, turnId: string }): Promise<void> {
340436
await this.codexClient.turnInterrupt({
341437
threadId: params.threadId,
@@ -355,6 +451,51 @@ export class CodexAcpClient {
355451

356452
return models;
357453
}
454+
455+
private async runSessionListDiagnostics(): Promise<Record<string, unknown>> {
456+
const [allProviders, archivedAllProviders, customGateway] = await Promise.all([
457+
this.codexClient.threadList({
458+
cursor: null,
459+
limit: null,
460+
sortKey: null,
461+
modelProviders: [],
462+
sourceKinds: null,
463+
archived: null,
464+
}),
465+
this.codexClient.threadList({
466+
cursor: null,
467+
limit: null,
468+
sortKey: null,
469+
modelProviders: [],
470+
sourceKinds: null,
471+
archived: true,
472+
}),
473+
this.codexClient.threadList({
474+
cursor: null,
475+
limit: null,
476+
sortKey: null,
477+
modelProviders: ["custom-gateway"],
478+
sourceKinds: null,
479+
archived: null,
480+
}),
481+
]);
482+
483+
return {
484+
allProviders: {
485+
count: allProviders.data.length,
486+
nextCursor: allProviders.nextCursor ?? null,
487+
},
488+
archivedAllProviders: {
489+
count: archivedAllProviders.data.length,
490+
nextCursor: archivedAllProviders.nextCursor ?? null,
491+
},
492+
customGateway: {
493+
count: customGateway.data.length,
494+
nextCursor: customGateway.nextCursor ?? null,
495+
},
496+
};
497+
}
498+
358499
}
359500

360501
export type JsonObject = { [key in string]?: JsonValue }
@@ -365,6 +506,10 @@ export type SessionMetadata = {
365506
models: Model[],
366507
}
367508

509+
export type SessionMetadataWithThread = SessionMetadata & {
510+
thread: Thread,
511+
}
512+
368513
function buildPromptItems(prompt: acp.ContentBlock[]): UserInput[] {
369514
return prompt.map((block): UserInput | null => {
370515
switch (block.type) {

0 commit comments

Comments
 (0)