generated from nhcarrigan/template
feat(slash-commands): expose Claude Code CLI built-ins in command menu
Adds /simplify, /loop, and /batch as CLI-sourced slash commands so they appear in the autocomplete menu with a 'CLI' badge and forward correctly to Claude Code instead of triggering 'Unknown command' errors. Introduces source?: 'cli' field to the SlashCommand interface to distinguish app-level commands from CLI pass-through entries. Resolves #208.
This commit is contained in:
@@ -87,10 +87,13 @@ describe("slashCommands", () => {
|
||||
expect(commandNames).toContain("search");
|
||||
expect(commandNames).toContain("summarise");
|
||||
expect(commandNames).toContain("skill");
|
||||
expect(commandNames).toContain("simplify");
|
||||
expect(commandNames).toContain("loop");
|
||||
expect(commandNames).toContain("batch");
|
||||
});
|
||||
|
||||
it("has 7 commands total", () => {
|
||||
expect(slashCommands.length).toBe(7);
|
||||
it("has 10 commands total", () => {
|
||||
expect(slashCommands.length).toBe(10);
|
||||
});
|
||||
|
||||
it("each command has required properties", () => {
|
||||
@@ -160,6 +163,45 @@ describe("slashCommands", () => {
|
||||
expect(skillCmd!.description).toBe("Invoke a Claude Code skill from ~/.claude/skills/");
|
||||
expect(skillCmd!.usage).toBe("/skill [name] [data]");
|
||||
});
|
||||
|
||||
it("simplify command has correct metadata and source", () => {
|
||||
const simplifyCmd = slashCommands.find((cmd) => cmd.name === "simplify");
|
||||
expect(simplifyCmd).toBeDefined();
|
||||
expect(simplifyCmd!.source).toBe("cli");
|
||||
expect(simplifyCmd!.usage).toBe("/simplify");
|
||||
});
|
||||
|
||||
it("loop command has correct metadata and source", () => {
|
||||
const loopCmd = slashCommands.find((cmd) => cmd.name === "loop");
|
||||
expect(loopCmd).toBeDefined();
|
||||
expect(loopCmd!.source).toBe("cli");
|
||||
expect(loopCmd!.usage).toBe("/loop [interval] [command]");
|
||||
});
|
||||
|
||||
it("batch command has correct metadata and source", () => {
|
||||
const batchCmd = slashCommands.find((cmd) => cmd.name === "batch");
|
||||
expect(batchCmd).toBeDefined();
|
||||
expect(batchCmd!.source).toBe("cli");
|
||||
expect(batchCmd!.usage).toBe("/batch [tasks]");
|
||||
});
|
||||
|
||||
it("app commands do not have source set", () => {
|
||||
const appCommandNames = ["cd", "clear", "new", "help", "search", "summarise", "skill"];
|
||||
appCommandNames.forEach((name) => {
|
||||
const cmd = slashCommands.find((c) => c.name === name);
|
||||
expect(cmd).toBeDefined();
|
||||
expect(cmd!.source).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("cli commands have source set to 'cli'", () => {
|
||||
const cliCommandNames = ["simplify", "loop", "batch"];
|
||||
cliCommandNames.forEach((name) => {
|
||||
const cmd = slashCommands.find((c) => c.name === name);
|
||||
expect(cmd).toBeDefined();
|
||||
expect(cmd!.source).toBe("cli");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSlashCommand", () => {
|
||||
@@ -342,6 +384,19 @@ describe("slashCommands", () => {
|
||||
expect(names).toContain("search");
|
||||
expect(names).toContain("summarise");
|
||||
expect(names).toContain("skill");
|
||||
expect(names).toContain("simplify");
|
||||
});
|
||||
|
||||
it("returns /loop for /l prefix", () => {
|
||||
const result = getMatchingCommands("/l");
|
||||
const names = result.map((cmd) => cmd.name);
|
||||
expect(names).toContain("loop");
|
||||
});
|
||||
|
||||
it("returns /batch for /b prefix", () => {
|
||||
const result = getMatchingCommands("/b");
|
||||
const names = result.map((cmd) => cmd.name);
|
||||
expect(names).toContain("batch");
|
||||
});
|
||||
|
||||
it("is case insensitive", () => {
|
||||
@@ -412,6 +467,19 @@ describe("slashCommands", () => {
|
||||
expect(testCommand.description).toBe("A test command");
|
||||
expect(testCommand.usage).toBe("/test [arg]");
|
||||
expect(typeof testCommand.execute).toBe("function");
|
||||
expect(testCommand.source).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can create a cli-sourced slash command object", () => {
|
||||
const cliCommand: SlashCommand = {
|
||||
name: "cli-test",
|
||||
description: "A CLI command",
|
||||
usage: "/cli-test",
|
||||
source: "cli",
|
||||
execute: vi.fn(),
|
||||
};
|
||||
|
||||
expect(cliCommand.source).toBe("cli");
|
||||
});
|
||||
|
||||
it("execute can be async function", () => {
|
||||
@@ -715,6 +783,88 @@ describe("slashCommands", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("/simplify execute", () => {
|
||||
it("shows error when no active conversation", async () => {
|
||||
getMock.mockReturnValue(null);
|
||||
const simplifyCmd = slashCommands.find((cmd) => cmd.name === "simplify")!;
|
||||
await simplifyCmd.execute("");
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation");
|
||||
});
|
||||
|
||||
it("sends /simplify prompt to Claude when there is an active conversation", async () => {
|
||||
getMock.mockReturnValue("conv-123");
|
||||
invokeMock.mockResolvedValue(undefined);
|
||||
const simplifyCmd = slashCommands.find((cmd) => cmd.name === "simplify")!;
|
||||
await simplifyCmd.execute("");
|
||||
expect(invokeMock).toHaveBeenCalledWith("send_prompt", {
|
||||
conversationId: "conv-123",
|
||||
message: "/simplify",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/loop execute", () => {
|
||||
it("shows error when no active conversation", async () => {
|
||||
getMock.mockReturnValue(null);
|
||||
const loopCmd = slashCommands.find((cmd) => cmd.name === "loop")!;
|
||||
await loopCmd.execute("5m /help");
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation");
|
||||
});
|
||||
|
||||
it("sends /loop with args when args are provided", async () => {
|
||||
getMock.mockReturnValue("conv-123");
|
||||
invokeMock.mockResolvedValue(undefined);
|
||||
const loopCmd = slashCommands.find((cmd) => cmd.name === "loop")!;
|
||||
await loopCmd.execute("5m /help");
|
||||
expect(invokeMock).toHaveBeenCalledWith("send_prompt", {
|
||||
conversationId: "conv-123",
|
||||
message: "/loop 5m /help",
|
||||
});
|
||||
});
|
||||
|
||||
it("sends /loop without args when no args provided", async () => {
|
||||
getMock.mockReturnValue("conv-123");
|
||||
invokeMock.mockResolvedValue(undefined);
|
||||
const loopCmd = slashCommands.find((cmd) => cmd.name === "loop")!;
|
||||
await loopCmd.execute("");
|
||||
expect(invokeMock).toHaveBeenCalledWith("send_prompt", {
|
||||
conversationId: "conv-123",
|
||||
message: "/loop",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/batch execute", () => {
|
||||
it("shows error when no active conversation", async () => {
|
||||
getMock.mockReturnValue(null);
|
||||
const batchCmd = slashCommands.find((cmd) => cmd.name === "batch")!;
|
||||
await batchCmd.execute("task1, task2");
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation");
|
||||
});
|
||||
|
||||
it("sends /batch with args when args are provided", async () => {
|
||||
getMock.mockReturnValue("conv-123");
|
||||
invokeMock.mockResolvedValue(undefined);
|
||||
const batchCmd = slashCommands.find((cmd) => cmd.name === "batch")!;
|
||||
await batchCmd.execute("task1, task2");
|
||||
expect(invokeMock).toHaveBeenCalledWith("send_prompt", {
|
||||
conversationId: "conv-123",
|
||||
message: "/batch task1, task2",
|
||||
});
|
||||
});
|
||||
|
||||
it("sends /batch without args when no args provided", async () => {
|
||||
getMock.mockReturnValue("conv-123");
|
||||
invokeMock.mockResolvedValue(undefined);
|
||||
const batchCmd = slashCommands.find((cmd) => cmd.name === "batch")!;
|
||||
await batchCmd.execute("");
|
||||
expect(invokeMock).toHaveBeenCalledWith("send_prompt", {
|
||||
conversationId: "conv-123",
|
||||
message: "/batch",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/cd success path", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
Reference in New Issue
Block a user