generated from nhcarrigan/template
feat: CLI v2.1.68–v2.1.74 compatibility updates (#221)
## Summary This PR brings Hikari Desktop up to full compatibility with Claude Code CLI versions v2.1.68 through v2.1.74, implementing all changelog items audited in issues #200–#218. ## Changes ### Bug Fixes - Remove deprecated Claude Opus 4.0 and 4.1 models from the model selector - Auto-migrate users pinned to deprecated models to Opus 4.6 ### New Features - Add cron tool support (`CronCreate`, `CronDelete`, `CronList`) with character state mapping and `CLAUDE_CODE_DISABLE_CRON` settings toggle - Handle `EnterWorktree` and `ExitWorktree` tools in character state mapping and tool display - Add CLI update check with npm registry indicator in the version bar - Add `agent_type` field and support the Agent tool rename from CLI v2.1.69 - Consume `worktree` field from status line hook events - Display per-agent model override in the agent monitor tree - Expose Claude Code CLI built-in slash commands (`/simplify`, `/loop`, `/batch`, `/memory`, `/context`) in the command menu with CLI badges - Add `includeGitInstructions` toggle in settings - Add `ENABLE_CLAUDEAI_MCP_SERVERS` opt-out setting - Linkify MCP binary file paths (PDFs, audio, Office docs) in markdown output - Add auto-memory panel, `/memory` slash command shortcut, and unified toast notification system - Toast notifications for `WorktreeCreate` and `WorktreeRemove` hook events - Sort session resume list by most recent activity, with most recent user message as preview - Convert WSL Linux paths to Windows UNC paths when opening binary files via `open_binary_file` command - Expose `autoMemoryDirectory` setting in ConfigSidebar (Agent Settings section) - Add `/context` as a CLI built-in in the slash command menu - Expose `modelOverrides` setting as a JSON textarea in ConfigSidebar (for AWS Bedrock, Google Vertex, etc.) > **Note:** The CLI update check commit does not have a corresponding issue — it was a bonus addition during the audit sprint. ## Closes Closes #200 Closes #201 Closes #202 Closes #205 Closes #206 Closes #207 Closes #208 Closes #209 Closes #210 Closes #211 Closes #212 Closes #213 Closes #214 Closes #215 Closes #216 Closes #217 Closes #218 Reviewed-on: #221 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #221.
This commit is contained in:
@@ -65,6 +65,10 @@ vi.mock("$lib/stores/config", () => ({
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
max_output_tokens: null,
|
||||
include_git_instructions: true,
|
||||
enable_claudeai_mcp_servers: true,
|
||||
auto_memory_directory: null,
|
||||
model_overrides: null,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
@@ -87,10 +91,15 @@ 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");
|
||||
expect(commandNames).toContain("memory");
|
||||
expect(commandNames).toContain("context");
|
||||
});
|
||||
|
||||
it("has 7 commands total", () => {
|
||||
expect(slashCommands.length).toBe(7);
|
||||
it("has 12 commands total", () => {
|
||||
expect(slashCommands.length).toBe(12);
|
||||
});
|
||||
|
||||
it("each command has required properties", () => {
|
||||
@@ -160,6 +169,52 @@ 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("context command has correct metadata and source", () => {
|
||||
const contextCmd = slashCommands.find((cmd) => cmd.name === "context");
|
||||
expect(contextCmd).toBeDefined();
|
||||
expect(contextCmd!.source).toBe("cli");
|
||||
expect(contextCmd!.usage).toBe("/context");
|
||||
});
|
||||
|
||||
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", "memory", "context"];
|
||||
cliCommandNames.forEach((name) => {
|
||||
const cmd = slashCommands.find((c) => c.name === name);
|
||||
expect(cmd).toBeDefined();
|
||||
expect(cmd!.source).toBe("cli");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSlashCommand", () => {
|
||||
@@ -342,6 +397,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 +480,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 +796,125 @@ 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("/memory execute", () => {
|
||||
it("opens the memory browser panel without requiring an active conversation", () => {
|
||||
getMock.mockReturnValue(null);
|
||||
const memoryCmd = slashCommands.find((cmd) => cmd.name === "memory")!;
|
||||
memoryCmd.execute("");
|
||||
expect(claudeStore.addLine).not.toHaveBeenCalled();
|
||||
expect(invokeMock).not.toHaveBeenCalledWith("send_prompt", expect.anything());
|
||||
});
|
||||
|
||||
it("does not send a prompt to Claude when executed", () => {
|
||||
getMock.mockReturnValue("conv-123");
|
||||
const memoryCmd = slashCommands.find((cmd) => cmd.name === "memory")!;
|
||||
memoryCmd.execute("");
|
||||
expect(invokeMock).not.toHaveBeenCalledWith("send_prompt", expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe("/context execute", () => {
|
||||
it("shows error when no active conversation", async () => {
|
||||
getMock.mockReturnValue(null);
|
||||
const contextCmd = slashCommands.find((cmd) => cmd.name === "context")!;
|
||||
await contextCmd.execute("");
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation");
|
||||
});
|
||||
|
||||
it("sends /context prompt to Claude when there is an active conversation", async () => {
|
||||
getMock.mockReturnValue("conv-123");
|
||||
invokeMock.mockResolvedValue(undefined);
|
||||
const contextCmd = slashCommands.find((cmd) => cmd.name === "context")!;
|
||||
await contextCmd.execute("");
|
||||
expect(invokeMock).toHaveBeenCalledWith("send_prompt", {
|
||||
conversationId: "conv-123",
|
||||
message: "/context",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/cd success path", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
Reference in New Issue
Block a user