Merge pull request #48 from getpaseo/feature/commands-in-draft-screen-clean
Make slash commands available in the new-agent draft screen
This commit is contained in:
@@ -769,6 +769,139 @@ describe("DaemonClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("lists commands with draft config via RPC", async () => {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const promise = client.listCommands("__new_agent__", {
|
||||
draftConfig: {
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
modeId: "bypassPermissions",
|
||||
model: "gpt-5",
|
||||
thinkingOptionId: "off",
|
||||
},
|
||||
});
|
||||
expect(mock.sent).toHaveLength(1);
|
||||
|
||||
const request = JSON.parse(mock.sent[0]) as {
|
||||
type: "session";
|
||||
message: {
|
||||
type: "list_commands_request";
|
||||
agentId: string;
|
||||
draftConfig?: {
|
||||
provider: string;
|
||||
cwd: string;
|
||||
modeId?: string;
|
||||
model?: string;
|
||||
thinkingOptionId?: string;
|
||||
};
|
||||
requestId: string;
|
||||
};
|
||||
};
|
||||
expect(request.message.type).toBe("list_commands_request");
|
||||
expect(request.message.agentId).toBe("__new_agent__");
|
||||
expect(request.message.draftConfig).toEqual({
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
modeId: "bypassPermissions",
|
||||
model: "gpt-5",
|
||||
thinkingOptionId: "off",
|
||||
});
|
||||
|
||||
mock.triggerMessage(
|
||||
JSON.stringify({
|
||||
type: "session",
|
||||
message: {
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId: "__new_agent__",
|
||||
commands: [
|
||||
{ name: "help", description: "Show help", argumentHint: "" },
|
||||
],
|
||||
error: null,
|
||||
requestId: request.message.requestId,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await expect(promise).resolves.toEqual({
|
||||
agentId: "__new_agent__",
|
||||
commands: [{ name: "help", description: "Show help", argumentHint: "" }],
|
||||
error: null,
|
||||
requestId: request.message.requestId,
|
||||
});
|
||||
});
|
||||
|
||||
test("lists commands with legacy requestId signature via RPC", async () => {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen();
|
||||
await connectPromise;
|
||||
|
||||
const promise = client.listCommands("agent-1", "req-legacy");
|
||||
expect(mock.sent).toHaveLength(1);
|
||||
|
||||
const request = JSON.parse(mock.sent[0]) as {
|
||||
type: "session";
|
||||
message: {
|
||||
type: "list_commands_request";
|
||||
agentId: string;
|
||||
draftConfig?: unknown;
|
||||
requestId: string;
|
||||
};
|
||||
};
|
||||
expect(request.message.type).toBe("list_commands_request");
|
||||
expect(request.message.agentId).toBe("agent-1");
|
||||
expect(request.message.requestId).toBe("req-legacy");
|
||||
expect(request.message.draftConfig).toBeUndefined();
|
||||
|
||||
mock.triggerMessage(
|
||||
JSON.stringify({
|
||||
type: "session",
|
||||
message: {
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId: "agent-1",
|
||||
commands: [],
|
||||
error: null,
|
||||
requestId: "req-legacy",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await expect(promise).resolves.toEqual({
|
||||
agentId: "agent-1",
|
||||
commands: [],
|
||||
error: null,
|
||||
requestId: "req-legacy",
|
||||
});
|
||||
});
|
||||
|
||||
test("auto-acks terminal stream chunks after delivery", async () => {
|
||||
const logger = createMockLogger();
|
||||
const mock = createMockTransport();
|
||||
|
||||
@@ -209,6 +209,14 @@ type SpeechModelsListPayload = SpeechModelsListResponse["payload"];
|
||||
type SpeechModelsDownloadPayload = SpeechModelsDownloadResponse["payload"];
|
||||
type ListCommandsPayload = ListCommandsResponse["payload"];
|
||||
type ExecuteCommandPayload = ExecuteCommandResponse["payload"];
|
||||
type ListCommandsDraftConfig = Pick<
|
||||
AgentSessionConfig,
|
||||
"provider" | "cwd" | "modeId" | "model" | "thinkingOptionId"
|
||||
>;
|
||||
type ListCommandsOptions = {
|
||||
requestId?: string;
|
||||
draftConfig?: ListCommandsDraftConfig;
|
||||
};
|
||||
type SetVoiceModePayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
{ type: "set_voice_mode_response" }
|
||||
@@ -2171,15 +2179,30 @@ export class DaemonClient {
|
||||
});
|
||||
}
|
||||
|
||||
async listCommands(agentId: string, requestId?: string): Promise<ListCommandsPayload>;
|
||||
async listCommands(
|
||||
agentId: string,
|
||||
requestId?: string
|
||||
options?: ListCommandsOptions
|
||||
): Promise<ListCommandsPayload>;
|
||||
async listCommands(
|
||||
agentId: string,
|
||||
requestIdOrOptions?: string | ListCommandsOptions
|
||||
): Promise<ListCommandsPayload> {
|
||||
const requestId =
|
||||
typeof requestIdOrOptions === "string"
|
||||
? requestIdOrOptions
|
||||
: requestIdOrOptions?.requestId;
|
||||
const draftConfig =
|
||||
typeof requestIdOrOptions === "string"
|
||||
? undefined
|
||||
: requestIdOrOptions?.draftConfig;
|
||||
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId,
|
||||
message: {
|
||||
type: "list_commands_request",
|
||||
agentId,
|
||||
...(draftConfig ? { draftConfig } : {}),
|
||||
},
|
||||
responseType: "list_commands_response",
|
||||
timeout: 30000,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { z } from "zod";
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentSlashCommand,
|
||||
AgentMode,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionResponse,
|
||||
@@ -437,6 +438,36 @@ export class AgentManager {
|
||||
return Promise.all(checks);
|
||||
}
|
||||
|
||||
async listDraftCommands(config: AgentSessionConfig): Promise<AgentSlashCommand[]> {
|
||||
const normalizedConfig = await this.normalizeConfig(config);
|
||||
const client = this.requireClient(normalizedConfig.provider);
|
||||
const available = await client.isAvailable();
|
||||
if (!available) {
|
||||
throw new Error(
|
||||
`Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`
|
||||
);
|
||||
}
|
||||
|
||||
const session = await client.createSession(normalizedConfig);
|
||||
try {
|
||||
if (!session.listCommands) {
|
||||
throw new Error(
|
||||
`Provider '${normalizedConfig.provider}' does not support listing commands`
|
||||
);
|
||||
}
|
||||
return await session.listCommands();
|
||||
} finally {
|
||||
try {
|
||||
await session.close();
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
{ err: error, provider: normalizedConfig.provider },
|
||||
"Failed to close draft command listing session"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAgent(id: string): ManagedAgent | null {
|
||||
const agent = this.agents.get(id);
|
||||
return agent ? { ...agent } : null;
|
||||
|
||||
@@ -1524,7 +1524,7 @@ export class Session {
|
||||
}
|
||||
|
||||
case "list_commands_request":
|
||||
await this.handleListCommandsRequest(msg.agentId, msg.requestId);
|
||||
await this.handleListCommandsRequest(msg);
|
||||
break;
|
||||
|
||||
case "execute_command_request":
|
||||
@@ -3473,9 +3473,12 @@ export class Session {
|
||||
/**
|
||||
* Handle list commands request for an agent
|
||||
*/
|
||||
private async handleListCommandsRequest(agentId: string, requestId: string): Promise<void> {
|
||||
private async handleListCommandsRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "list_commands_request" }>
|
||||
): Promise<void> {
|
||||
const { agentId, requestId, draftConfig } = msg;
|
||||
this.sessionLogger.debug(
|
||||
{ agentId },
|
||||
{ agentId, draftConfig },
|
||||
`Handling list commands request for agent ${agentId}`
|
||||
);
|
||||
|
||||
@@ -3483,47 +3486,59 @@ export class Session {
|
||||
const agents = this.agentManager.listAgents();
|
||||
const agent = agents.find((a) => a.id === agentId);
|
||||
|
||||
if (!agent) {
|
||||
if (agent?.session?.listCommands) {
|
||||
const commands = await agent.session.listCommands();
|
||||
this.emit({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId,
|
||||
commands: [],
|
||||
error: `Agent not found: ${agentId}`,
|
||||
commands,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const session = agent.session;
|
||||
if (!session || !session.listCommands) {
|
||||
if (!agent && draftConfig) {
|
||||
const sessionConfig: AgentSessionConfig = {
|
||||
provider: draftConfig.provider,
|
||||
cwd: expandTilde(draftConfig.cwd),
|
||||
...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
|
||||
...(draftConfig.model ? { model: draftConfig.model } : {}),
|
||||
...(draftConfig.thinkingOptionId
|
||||
? { thinkingOptionId: draftConfig.thinkingOptionId }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const commands = await this.agentManager.listDraftCommands(sessionConfig);
|
||||
this.emit({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId,
|
||||
commands: [],
|
||||
error: `Agent does not support listing commands`,
|
||||
commands,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = await session.listCommands();
|
||||
|
||||
this.emit({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId,
|
||||
commands,
|
||||
error: null,
|
||||
commands: [],
|
||||
error: agent
|
||||
? `Agent does not support listing commands`
|
||||
: `Agent not found: ${agentId}`,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
this.sessionLogger.error(
|
||||
{ err: error, agentId },
|
||||
{ err: error, agentId, draftConfig },
|
||||
"Failed to list commands"
|
||||
);
|
||||
this.emit({
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { SessionInboundMessageSchema } from "./messages.js";
|
||||
|
||||
describe("list_commands_request schema", () => {
|
||||
test("accepts legacy agent-only payload", () => {
|
||||
const parsed = SessionInboundMessageSchema.parse({
|
||||
type: "list_commands_request",
|
||||
agentId: "agent-123",
|
||||
requestId: "req-123",
|
||||
});
|
||||
|
||||
expect(parsed.type).toBe("list_commands_request");
|
||||
if (parsed.type !== "list_commands_request") {
|
||||
throw new Error("Expected list_commands_request message");
|
||||
}
|
||||
expect(parsed.agentId).toBe("agent-123");
|
||||
expect(parsed.draftConfig).toBeUndefined();
|
||||
});
|
||||
|
||||
test("accepts draft command context payload", () => {
|
||||
const parsed = SessionInboundMessageSchema.parse({
|
||||
type: "list_commands_request",
|
||||
agentId: "__new_agent__",
|
||||
draftConfig: {
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
modeId: "bypassPermissions",
|
||||
model: "gpt-5",
|
||||
thinkingOptionId: "off",
|
||||
},
|
||||
requestId: "req-456",
|
||||
});
|
||||
|
||||
expect(parsed.type).toBe("list_commands_request");
|
||||
if (parsed.type !== "list_commands_request") {
|
||||
throw new Error("Expected list_commands_request message");
|
||||
}
|
||||
expect(parsed.draftConfig).toEqual({
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
modeId: "bypassPermissions",
|
||||
model: "gpt-5",
|
||||
thinkingOptionId: "off",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -926,9 +926,18 @@ export const PingMessageSchema = z.object({
|
||||
clientSentAt: z.number().int().optional(),
|
||||
});
|
||||
|
||||
const ListCommandsDraftConfigSchema = z.object({
|
||||
provider: AgentProviderSchema,
|
||||
cwd: z.string(),
|
||||
modeId: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
thinkingOptionId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ListCommandsRequestSchema = z.object({
|
||||
type: z.literal("list_commands_request"),
|
||||
agentId: z.string(),
|
||||
draftConfig: ListCommandsDraftConfigSchema.optional(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user