Fix commands not loading for Pi agent (#225)

This commit is contained in:
Benjamin Kitt
2026-04-08 21:10:11 -05:00
committed by GitHub
parent 6bf8da8087
commit d98d5457ed
3 changed files with 119 additions and 2 deletions

View File

@@ -415,10 +415,39 @@ describe("transformPiModels", () => {
});
describe("ACPAgentSession slash commands", () => {
test("caches ACP available commands for listCommands", async () => {
test("returns immediately for ACP sessions that do not wait for async command discovery", async () => {
const session = createSession();
expect(await session.listCommands()).toEqual([]);
await expect(session.listCommands()).resolves.toEqual([]);
});
test("waits for async available_commands_update when enabled", async () => {
const session = new ACPAgentSession(
{
provider: "pi",
cwd: "/tmp/paseo-acp-test",
},
{
provider: "pi",
logger: createTestLogger(),
defaultCommand: ["pi-acp"],
defaultModes: [],
modelTransformer: transformPiModels,
sessionResponseTransformer: transformPiSessionResponse,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: false,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
waitForInitialCommands: true,
initialCommandsWaitTimeoutMs: 1500,
},
);
const listCommandsPromise = session.listCommands();
(session as any).translateSessionUpdate({
sessionUpdate: "available_commands_update",
@@ -434,6 +463,19 @@ describe("ACPAgentSession slash commands", () => {
],
});
expect(await listCommandsPromise).toEqual([
{
name: "research_codebase",
description: "Search the workspace for relevant files",
argumentHint: "",
},
{
name: "create_plan",
description: "Draft a plan for the requested work",
argumentHint: "",
},
]);
expect(await session.listCommands()).toEqual([
{
name: "research_codebase",

View File

@@ -125,6 +125,8 @@ type ACPAgentClientOptions = {
thinkingOptionId: string,
) => Promise<void>;
capabilities?: AgentCapabilityFlags;
waitForInitialCommands?: boolean;
initialCommandsWaitTimeoutMs?: number;
};
type ACPAgentSessionOptions = {
@@ -144,6 +146,8 @@ type ACPAgentSessionOptions = {
capabilities: AgentCapabilityFlags;
handle?: AgentPersistenceHandle;
launchEnv?: Record<string, string>;
waitForInitialCommands?: boolean;
initialCommandsWaitTimeoutMs?: number;
};
type SpawnedACPProcess = {
@@ -302,6 +306,8 @@ export class ACPAgentClient implements AgentClient {
sessionId: string,
thinkingOptionId: string,
) => Promise<void>;
private readonly waitForInitialCommands: boolean;
private readonly initialCommandsWaitTimeoutMs: number;
constructor(options: ACPAgentClientOptions) {
this.provider = options.provider;
@@ -314,6 +320,8 @@ export class ACPAgentClient implements AgentClient {
this.sessionResponseTransformer = options.sessionResponseTransformer;
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
this.thinkingOptionWriter = options.thinkingOptionWriter;
this.waitForInitialCommands = options.waitForInitialCommands ?? false;
this.initialCommandsWaitTimeoutMs = options.initialCommandsWaitTimeoutMs ?? 1500;
}
async createSession(
@@ -335,6 +343,8 @@ export class ACPAgentClient implements AgentClient {
thinkingOptionWriter: this.thinkingOptionWriter,
capabilities: this.capabilities,
launchEnv: launchContext?.env,
waitForInitialCommands: this.waitForInitialCommands,
initialCommandsWaitTimeoutMs: this.initialCommandsWaitTimeoutMs,
},
);
await session.initializeNewSession();
@@ -375,6 +385,8 @@ export class ACPAgentClient implements AgentClient {
capabilities: this.capabilities,
handle,
launchEnv: launchContext?.env,
waitForInitialCommands: this.waitForInitialCommands,
initialCommandsWaitTimeoutMs: this.initialCommandsWaitTimeoutMs,
});
await session.initializeResumedSession();
return session;
@@ -617,6 +629,10 @@ export class ACPAgentSession implements AgentSession, ACPClient {
private lastActivityAt: string | null = null;
private configOptions: SessionConfigOption[] = [];
private cachedCommands: AgentSlashCommand[] = [];
private commandsReadyDeferred: { promise: Promise<void>; resolve: () => void } | null = null;
private commandsReadySettled = false;
private waitForInitialCommands: boolean;
private initialCommandsWaitTimeoutMs: number;
private currentTurnUsage: AgentUsage | undefined;
private activeForegroundTurnId: string | null = null;
private closed = false;
@@ -645,6 +661,8 @@ export class ACPAgentSession implements AgentSession, ACPClient {
this.currentModel = config.model ?? null;
this.thinkingOptionId = config.thinkingOptionId ?? null;
this.currentTitle = config.title ?? null;
this.waitForInitialCommands = options.waitForInitialCommands ?? false;
this.initialCommandsWaitTimeoutMs = options.initialCommandsWaitTimeoutMs ?? 1500;
}
get id(): string | null {
@@ -876,7 +894,59 @@ export class ACPAgentSession implements AgentSession, ACPClient {
return this.currentMode;
}
private ensureCommandsReadyDeferred(): void {
if (this.commandsReadyDeferred || this.commandsReadySettled || this.cachedCommands.length > 0) {
return;
}
let resolve!: () => void;
const promise = new Promise<void>((r) => {
resolve = r;
});
this.commandsReadyDeferred = { promise, resolve };
}
private settleCommandsReady(): void {
if (this.commandsReadySettled) {
return;
}
this.commandsReadySettled = true;
this.commandsReadyDeferred?.resolve();
this.commandsReadyDeferred = null;
}
private async waitForCommandsReady(): Promise<void> {
const deferred = this.commandsReadyDeferred;
if (!deferred) {
return;
}
let timer: ReturnType<typeof setTimeout> | null = null;
try {
await Promise.race([
deferred.promise,
new Promise<void>((resolve) => {
timer = setTimeout(resolve, this.initialCommandsWaitTimeoutMs);
}),
]);
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
async listCommands(): Promise<AgentSlashCommand[]> {
if (this.cachedCommands.length > 0) {
return this.cachedCommands;
}
if (!this.waitForInitialCommands || this.closed) {
return this.cachedCommands;
}
this.ensureCommandsReadyDeferred();
await this.waitForCommandsReady();
this.settleCommandsReady();
return this.cachedCommands;
}
@@ -1046,6 +1116,8 @@ export class ACPAgentSession implements AgentSession, ACPClient {
}
this.closed = true;
this.settleCommandsReady();
for (const pending of this.pendingPermissions.values()) {
pending.resolve({ outcome: { outcome: "cancelled" } });
}
@@ -1392,6 +1464,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
description: command.description,
argumentHint: "",
}));
this.settleCommandsReady();
return [];
default:
return [];

View File

@@ -298,6 +298,8 @@ export class PiACPAgentClient extends ACPAgentClient {
await connection.setSessionMode({ sessionId, modeId: thinkingOptionId });
},
capabilities: PI_CAPABILITIES,
waitForInitialCommands: true,
initialCommandsWaitTimeoutMs: 1500,
});
}