Add command autocomplete and stream spacing updates
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
createDaemonTestContext,
|
||||
type DaemonTestContext,
|
||||
} from "../../test-utils/index.js";
|
||||
import type { AgentSlashCommand } from "../agent-sdk-types.js";
|
||||
|
||||
describe("claude agent commands E2E", () => {
|
||||
let ctx: DaemonTestContext;
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx = await createDaemonTestContext();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx.cleanup();
|
||||
}, 60000);
|
||||
|
||||
test("lists available slash commands for a claude agent", async () => {
|
||||
// Create a Claude agent
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "claude",
|
||||
cwd: "/tmp",
|
||||
title: "Commands Test Agent",
|
||||
});
|
||||
|
||||
expect(agent.id).toBeTruthy();
|
||||
expect(agent.provider).toBe("claude");
|
||||
expect(agent.status).toBe("idle");
|
||||
|
||||
// List commands
|
||||
const result = await ctx.client.listCommands(agent.id);
|
||||
|
||||
// Should have no error
|
||||
expect(result.error).toBeNull();
|
||||
|
||||
// Should have commands
|
||||
expect(result.commands.length).toBeGreaterThan(0);
|
||||
|
||||
// Each command should have required fields
|
||||
for (const cmd of result.commands) {
|
||||
expect(cmd.name).toBeTruthy();
|
||||
expect(typeof cmd.description).toBe("string");
|
||||
expect(typeof cmd.argumentHint).toBe("string");
|
||||
}
|
||||
|
||||
// Should include some well-known commands
|
||||
const commandNames = result.commands.map((c) => c.name);
|
||||
// These are skills that come from CLAUDE.md configurations
|
||||
// At minimum we should have some commands available
|
||||
console.log("Available commands:", commandNames);
|
||||
expect(commandNames.length).toBeGreaterThan(0);
|
||||
}, 120000);
|
||||
|
||||
test("returns error for non-existent agent", async () => {
|
||||
const result = await ctx.client.listCommands("non-existent-agent-id");
|
||||
|
||||
expect(result.error).toBeTruthy();
|
||||
expect(result.error).toContain("Agent not found");
|
||||
expect(result.commands).toEqual([]);
|
||||
}, 30000);
|
||||
});
|
||||
@@ -523,6 +523,12 @@ export const ClearAgentAttentionMessageSchema = z.object({
|
||||
agentId: z.union([z.string(), z.array(z.string())]),
|
||||
});
|
||||
|
||||
export const ListCommandsRequestSchema = z.object({
|
||||
type: z.literal("list_commands_request"),
|
||||
agentId: z.string(),
|
||||
requestId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
UserTextMessageSchema,
|
||||
RealtimeAudioChunkMessageSchema,
|
||||
@@ -550,6 +556,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
FileDownloadTokenRequestSchema,
|
||||
GitRepoInfoRequestMessageSchema,
|
||||
ClearAgentAttentionMessageSchema,
|
||||
ListCommandsRequestSchema,
|
||||
]);
|
||||
|
||||
export type SessionInboundMessage = z.infer<typeof SessionInboundMessageSchema>;
|
||||
@@ -805,6 +812,22 @@ export const ListProviderModelsResponseMessageSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const AgentSlashCommandSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
argumentHint: z.string(),
|
||||
});
|
||||
|
||||
export const ListCommandsResponseSchema = z.object({
|
||||
type: z.literal("list_commands_response"),
|
||||
payload: z.object({
|
||||
agentId: z.string(),
|
||||
commands: z.array(AgentSlashCommandSchema),
|
||||
error: z.string().nullable(),
|
||||
requestId: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
ActivityLogMessageSchema,
|
||||
AssistantChunkMessageSchema,
|
||||
@@ -830,6 +853,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
FileDownloadTokenResponseSchema,
|
||||
GitRepoInfoResponseSchema,
|
||||
ListProviderModelsResponseMessageSchema,
|
||||
ListCommandsResponseSchema,
|
||||
]);
|
||||
|
||||
export type SessionOutboundMessage = z.infer<
|
||||
@@ -888,6 +912,8 @@ export type FileDownloadTokenRequest = z.infer<typeof FileDownloadTokenRequestSc
|
||||
export type FileDownloadTokenResponse = z.infer<typeof FileDownloadTokenResponseSchema>;
|
||||
export type RestartServerRequestMessage = z.infer<typeof RestartServerRequestMessageSchema>;
|
||||
export type ClearAgentAttentionMessage = z.infer<typeof ClearAgentAttentionMessageSchema>;
|
||||
export type ListCommandsRequest = z.infer<typeof ListCommandsRequestSchema>;
|
||||
export type ListCommandsResponse = z.infer<typeof ListCommandsResponseSchema>;
|
||||
|
||||
// ============================================================================
|
||||
// WebSocket Level Messages (wraps session messages)
|
||||
|
||||
@@ -794,6 +794,10 @@ export class Session {
|
||||
case "clear_agent_attention":
|
||||
await this.handleClearAgentAttention(msg.agentId);
|
||||
break;
|
||||
|
||||
case "list_commands_request":
|
||||
await this.handleListCommandsRequest(msg.agentId, msg.requestId);
|
||||
break;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
@@ -1818,6 +1822,73 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle list commands request for an agent
|
||||
*/
|
||||
private async handleListCommandsRequest(agentId: string, requestId?: string): Promise<void> {
|
||||
console.log(
|
||||
`[Session ${this.clientId}] Handling list commands request for agent ${agentId}`
|
||||
);
|
||||
|
||||
try {
|
||||
const agents = this.agentManager.listAgents();
|
||||
const agent = agents.find((a) => a.id === agentId);
|
||||
|
||||
if (!agent) {
|
||||
this.emit({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId,
|
||||
commands: [],
|
||||
error: `Agent not found: ${agentId}`,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const session = agent.session;
|
||||
if (!session || !session.listCommands) {
|
||||
this.emit({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId,
|
||||
commands: [],
|
||||
error: `Agent does not support listing commands`,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = await session.listCommands();
|
||||
|
||||
this.emit({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId,
|
||||
commands,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`[Session ${this.clientId}] Failed to list commands:`,
|
||||
error
|
||||
);
|
||||
this.emit({
|
||||
type: "list_commands_response",
|
||||
payload: {
|
||||
agentId,
|
||||
commands: [],
|
||||
error: error.message,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agent permission response from user
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
AgentPermissionResponse,
|
||||
AgentPersistenceHandle,
|
||||
AgentProvider,
|
||||
AgentSlashCommand,
|
||||
} from "../agent/agent-sdk-types.js";
|
||||
import { getAgentProviderDefinition } from "../agent/provider-registry.js";
|
||||
|
||||
@@ -616,6 +617,41 @@ export class DaemonClient {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Commands
|
||||
// ============================================================================
|
||||
|
||||
async listCommands(agentId: string): Promise<{
|
||||
agentId: string;
|
||||
commands: AgentSlashCommand[];
|
||||
error: string | null;
|
||||
}> {
|
||||
const startPosition = this.messageQueue.length;
|
||||
|
||||
this.send({
|
||||
type: "list_commands_request",
|
||||
agentId,
|
||||
});
|
||||
|
||||
return this.waitFor(
|
||||
(msg) => {
|
||||
if (
|
||||
msg.type === "list_commands_response" &&
|
||||
msg.payload.agentId === agentId
|
||||
) {
|
||||
return {
|
||||
agentId: msg.payload.agentId,
|
||||
commands: msg.payload.commands ?? [],
|
||||
error: msg.payload.error ?? null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
30000,
|
||||
{ skipQueueBefore: startPosition }
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Permissions
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user