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:
@@ -59,12 +59,52 @@ const makeConversation = () => ({
|
||||
|
||||
describe("sessionsStore - loadSessions", () => {
|
||||
it("loads sessions from backend and updates the store", async () => {
|
||||
const sessionList = [{ id: "session-1", name: "Test", message_count: 1, preview: "..." }];
|
||||
const sessionList = [
|
||||
{
|
||||
id: "session-1",
|
||||
name: "Test",
|
||||
message_count: 1,
|
||||
preview: "...",
|
||||
last_activity_at: "2026-03-03T11:00:00.000Z",
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("list_sessions", sessionList);
|
||||
await sessionsStore.loadSessions();
|
||||
expect(get(sessionsStore.sessions)).toEqual(sessionList);
|
||||
});
|
||||
|
||||
it("sorts sessions by last_activity_at descending", async () => {
|
||||
const sessionList = [
|
||||
{
|
||||
id: "older",
|
||||
name: "Older",
|
||||
message_count: 1,
|
||||
preview: "...",
|
||||
last_activity_at: "2026-03-01T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "newest",
|
||||
name: "Newest",
|
||||
message_count: 1,
|
||||
preview: "...",
|
||||
last_activity_at: "2026-03-03T12:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "middle",
|
||||
name: "Middle",
|
||||
message_count: 1,
|
||||
preview: "...",
|
||||
last_activity_at: "2026-03-02T10:00:00.000Z",
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("list_sessions", sessionList);
|
||||
await sessionsStore.loadSessions();
|
||||
const sorted = get(sessionsStore.sessions);
|
||||
expect(sorted[0].id).toBe("newest");
|
||||
expect(sorted[1].id).toBe("middle");
|
||||
expect(sorted[2].id).toBe("older");
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("list_sessions", new Error("Backend error"));
|
||||
@@ -128,12 +168,44 @@ describe("sessionsStore - searchSessions", () => {
|
||||
});
|
||||
|
||||
it("searches with the given query", async () => {
|
||||
const results = [{ id: "session-1", name: "Test", message_count: 1, preview: "..." }];
|
||||
const results = [
|
||||
{
|
||||
id: "session-1",
|
||||
name: "Test",
|
||||
message_count: 1,
|
||||
preview: "...",
|
||||
last_activity_at: "2026-03-03T11:00:00.000Z",
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("search_sessions", results);
|
||||
await sessionsStore.searchSessions("test");
|
||||
expect(get(sessionsStore.sessions)).toEqual(results);
|
||||
});
|
||||
|
||||
it("sorts search results by last_activity_at descending", async () => {
|
||||
const results = [
|
||||
{
|
||||
id: "older",
|
||||
name: "Older",
|
||||
message_count: 1,
|
||||
preview: "...",
|
||||
last_activity_at: "2026-03-01T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "newest",
|
||||
name: "Newest",
|
||||
message_count: 1,
|
||||
preview: "...",
|
||||
last_activity_at: "2026-03-03T12:00:00.000Z",
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("search_sessions", results);
|
||||
await sessionsStore.searchSessions("query");
|
||||
const sorted = get(sessionsStore.sessions);
|
||||
expect(sorted[0].id).toBe("newest");
|
||||
expect(sorted[1].id).toBe("older");
|
||||
});
|
||||
|
||||
it("updates searchQuery store", async () => {
|
||||
setMockInvokeResult("search_sessions", []);
|
||||
await sessionsStore.searchSessions("hello");
|
||||
@@ -187,6 +259,94 @@ describe("sessionsStore - saveConversation", () => {
|
||||
const conv = { ...makeConversation(), terminalLines: [] };
|
||||
await sessionsStore.saveConversation(conv as never);
|
||||
});
|
||||
|
||||
it("uses the most recent user message as the preview", async () => {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
setMockInvokeResult("save_session", undefined);
|
||||
setMockInvokeResult("list_sessions", []);
|
||||
|
||||
const conv = {
|
||||
...makeConversation(),
|
||||
terminalLines: [
|
||||
{
|
||||
id: "1",
|
||||
type: "user",
|
||||
content: "First message",
|
||||
timestamp: new Date(),
|
||||
toolName: undefined,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "assistant",
|
||||
content: "Reply one",
|
||||
timestamp: new Date(),
|
||||
toolName: undefined,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "user",
|
||||
content: "Most recent prompt",
|
||||
timestamp: new Date(),
|
||||
toolName: undefined,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "assistant",
|
||||
content: "Reply two",
|
||||
timestamp: new Date(),
|
||||
toolName: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
await sessionsStore.saveConversation(conv as never);
|
||||
|
||||
const saveCall = vi.mocked(invoke).mock.calls.find((call) => call[0] === "save_session");
|
||||
const capturedSession = (saveCall![1] as { session: SavedSession }).session;
|
||||
expect(capturedSession.preview).toBe("Most recent prompt");
|
||||
});
|
||||
|
||||
it("truncates long preview text at 150 characters", async () => {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
setMockInvokeResult("save_session", undefined);
|
||||
setMockInvokeResult("list_sessions", []);
|
||||
|
||||
const longContent = "A".repeat(200);
|
||||
const conv = {
|
||||
...makeConversation(),
|
||||
terminalLines: [
|
||||
{ id: "1", type: "user", content: longContent, timestamp: new Date(), toolName: undefined },
|
||||
],
|
||||
};
|
||||
await sessionsStore.saveConversation(conv as never);
|
||||
|
||||
const saveCall = vi.mocked(invoke).mock.calls.find((call) => call[0] === "save_session");
|
||||
const capturedSession = (saveCall![1] as { session: SavedSession }).session;
|
||||
expect(capturedSession.preview).toBe("A".repeat(150) + "...");
|
||||
});
|
||||
|
||||
it("uses 'Empty conversation' as preview when there are no user messages", async () => {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
setMockInvokeResult("save_session", undefined);
|
||||
setMockInvokeResult("list_sessions", []);
|
||||
|
||||
const conv = {
|
||||
...makeConversation(),
|
||||
terminalLines: [
|
||||
{
|
||||
id: "1",
|
||||
type: "assistant",
|
||||
content: "Only assistant message",
|
||||
timestamp: new Date(),
|
||||
toolName: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
await sessionsStore.saveConversation(conv as never);
|
||||
|
||||
const saveCall = vi.mocked(invoke).mock.calls.find((call) => call[0] === "save_session");
|
||||
const capturedSession = (saveCall![1] as { session: SavedSession }).session;
|
||||
expect(capturedSession.preview).toBe("Empty conversation");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionsStore - scheduleAutoSave and cancelAutoSave", () => {
|
||||
|
||||
Reference in New Issue
Block a user