generated from nhcarrigan/template
452fe185df
## 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>
271 lines
8.8 KiB
TypeScript
271 lines
8.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
BINARY_FILE_EXTENSIONS,
|
|
getFileExtension,
|
|
getFileTypeIcon,
|
|
isBinaryFilePath,
|
|
linkifyFilePaths,
|
|
} from "./filePaths";
|
|
|
|
describe("getFileExtension", () => {
|
|
it("returns the lowercase extension of a simple path", () => {
|
|
expect(getFileExtension("/tmp/report.pdf")).toBe("pdf");
|
|
});
|
|
|
|
it("returns the lowercase extension for uppercase file names", () => {
|
|
expect(getFileExtension("/tmp/AUDIO.MP3")).toBe("mp3");
|
|
});
|
|
|
|
it("returns the extension for a path with multiple dots", () => {
|
|
expect(getFileExtension("/tmp/my.file.docx")).toBe("docx");
|
|
});
|
|
|
|
it("returns an empty string when there is no extension", () => {
|
|
expect(getFileExtension("/tmp/noextension")).toBe("");
|
|
});
|
|
|
|
it("returns an empty string for an empty string input", () => {
|
|
expect(getFileExtension("")).toBe("");
|
|
});
|
|
|
|
it("returns the extension for a home-relative path", () => {
|
|
expect(getFileExtension("~/downloads/track.wav")).toBe("wav");
|
|
});
|
|
});
|
|
|
|
describe("getFileTypeIcon", () => {
|
|
it("returns the PDF icon for .pdf files", () => {
|
|
expect(getFileTypeIcon("/tmp/doc.pdf")).toBe("📄");
|
|
});
|
|
|
|
it("returns the Word icon for .docx files", () => {
|
|
expect(getFileTypeIcon("/tmp/report.docx")).toBe("📝");
|
|
});
|
|
|
|
it("returns the Word icon for .doc files", () => {
|
|
expect(getFileTypeIcon("/tmp/old.doc")).toBe("📝");
|
|
});
|
|
|
|
it("returns the spreadsheet icon for .xlsx files", () => {
|
|
expect(getFileTypeIcon("/tmp/data.xlsx")).toBe("📊");
|
|
});
|
|
|
|
it("returns the spreadsheet icon for .xls files", () => {
|
|
expect(getFileTypeIcon("/tmp/data.xls")).toBe("📊");
|
|
});
|
|
|
|
it("returns the presentation icon for .pptx files", () => {
|
|
expect(getFileTypeIcon("/tmp/slides.pptx")).toBe("📽️");
|
|
});
|
|
|
|
it("returns the presentation icon for .ppt files", () => {
|
|
expect(getFileTypeIcon("/tmp/slides.ppt")).toBe("📽️");
|
|
});
|
|
|
|
it("returns the audio icon for .mp3 files", () => {
|
|
expect(getFileTypeIcon("/tmp/song.mp3")).toBe("🎵");
|
|
});
|
|
|
|
it("returns the audio icon for .wav files", () => {
|
|
expect(getFileTypeIcon("/tmp/sound.wav")).toBe("🎵");
|
|
});
|
|
|
|
it("returns the audio icon for .ogg files", () => {
|
|
expect(getFileTypeIcon("/tmp/audio.ogg")).toBe("🎵");
|
|
});
|
|
|
|
it("returns the audio icon for .flac files", () => {
|
|
expect(getFileTypeIcon("/tmp/lossless.flac")).toBe("🎵");
|
|
});
|
|
|
|
it("returns the audio icon for .aac files", () => {
|
|
expect(getFileTypeIcon("/tmp/compressed.aac")).toBe("🎵");
|
|
});
|
|
|
|
it("returns the audio icon for .m4a files", () => {
|
|
expect(getFileTypeIcon("/tmp/itunes.m4a")).toBe("🎵");
|
|
});
|
|
|
|
it("returns the video icon for .mp4 files", () => {
|
|
expect(getFileTypeIcon("/tmp/video.mp4")).toBe("🎬");
|
|
});
|
|
|
|
it("returns the video icon for .avi files", () => {
|
|
expect(getFileTypeIcon("/tmp/old.avi")).toBe("🎬");
|
|
});
|
|
|
|
it("returns the video icon for .mov files", () => {
|
|
expect(getFileTypeIcon("/tmp/clip.mov")).toBe("🎬");
|
|
});
|
|
|
|
it("returns the video icon for .mkv files", () => {
|
|
expect(getFileTypeIcon("/tmp/film.mkv")).toBe("🎬");
|
|
});
|
|
|
|
it("returns the video icon for .webm files", () => {
|
|
expect(getFileTypeIcon("/tmp/stream.webm")).toBe("🎬");
|
|
});
|
|
|
|
it("returns the archive icon for .zip files", () => {
|
|
expect(getFileTypeIcon("/tmp/bundle.zip")).toBe("📦");
|
|
});
|
|
|
|
it("returns the archive icon for .tar files", () => {
|
|
expect(getFileTypeIcon("/tmp/archive.tar")).toBe("📦");
|
|
});
|
|
|
|
it("returns the archive icon for .gz files", () => {
|
|
expect(getFileTypeIcon("/tmp/compressed.gz")).toBe("📦");
|
|
});
|
|
|
|
it("returns the disk icon for .bin files", () => {
|
|
expect(getFileTypeIcon("/tmp/firmware.bin")).toBe("💿");
|
|
});
|
|
|
|
it("returns the disk icon for .iso files", () => {
|
|
expect(getFileTypeIcon("/tmp/image.iso")).toBe("💿");
|
|
});
|
|
|
|
it("returns the generic folder icon for an unknown extension", () => {
|
|
expect(getFileTypeIcon("/tmp/file.unknown")).toBe("📁");
|
|
});
|
|
|
|
it("returns the generic folder icon for a file with no extension", () => {
|
|
expect(getFileTypeIcon("/tmp/noext")).toBe("📁");
|
|
});
|
|
});
|
|
|
|
describe("isBinaryFilePath", () => {
|
|
it("returns true for a PDF path", () => {
|
|
expect(isBinaryFilePath("/tmp/report.pdf")).toBe(true);
|
|
});
|
|
|
|
it("returns true for an audio path", () => {
|
|
expect(isBinaryFilePath("/tmp/song.mp3")).toBe(true);
|
|
});
|
|
|
|
it("returns true for a video path", () => {
|
|
expect(isBinaryFilePath("/tmp/clip.mp4")).toBe(true);
|
|
});
|
|
|
|
it("returns true for a document path", () => {
|
|
expect(isBinaryFilePath("/tmp/doc.docx")).toBe(true);
|
|
});
|
|
|
|
it("returns false for a TypeScript file", () => {
|
|
expect(isBinaryFilePath("/src/index.ts")).toBe(false);
|
|
});
|
|
|
|
it("returns false for a text file", () => {
|
|
expect(isBinaryFilePath("/tmp/output.txt")).toBe(false);
|
|
});
|
|
|
|
it("returns false for a path with no extension", () => {
|
|
expect(isBinaryFilePath("/tmp/file")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("BINARY_FILE_EXTENSIONS", () => {
|
|
it("includes pdf", () => {
|
|
expect(BINARY_FILE_EXTENSIONS).toContain("pdf");
|
|
});
|
|
|
|
it("includes common audio extensions", () => {
|
|
expect(BINARY_FILE_EXTENSIONS).toContain("mp3");
|
|
expect(BINARY_FILE_EXTENSIONS).toContain("wav");
|
|
});
|
|
|
|
it("includes common video extensions", () => {
|
|
expect(BINARY_FILE_EXTENSIONS).toContain("mp4");
|
|
});
|
|
|
|
it("includes common document extensions", () => {
|
|
expect(BINARY_FILE_EXTENSIONS).toContain("docx");
|
|
expect(BINARY_FILE_EXTENSIONS).toContain("xlsx");
|
|
});
|
|
});
|
|
|
|
describe("linkifyFilePaths", () => {
|
|
it("converts a PDF path in plain text to a file link", () => {
|
|
const html = "<p>Saved to /tmp/report.pdf successfully.</p>";
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).toContain('data-filepath="/tmp/report.pdf"');
|
|
expect(result).toContain("📄");
|
|
expect(result).toContain('class="file-link"');
|
|
});
|
|
|
|
it("converts an audio path to a file link", () => {
|
|
const html = "<p>Audio saved to /tmp/output.mp3</p>";
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).toContain('data-filepath="/tmp/output.mp3"');
|
|
expect(result).toContain("🎵");
|
|
});
|
|
|
|
it("does not linkify paths inside code blocks", () => {
|
|
const html = "<p>Example:</p><pre><code>/tmp/file.pdf</code></pre>";
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).not.toContain('data-filepath="/tmp/file.pdf"');
|
|
expect(result).toContain("/tmp/file.pdf");
|
|
});
|
|
|
|
it("does not linkify paths inside inline code", () => {
|
|
const html = "<p>Use <code>/tmp/file.pdf</code> to open it.</p>";
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).not.toContain('data-filepath="/tmp/file.pdf"');
|
|
expect(result).toContain("/tmp/file.pdf");
|
|
});
|
|
|
|
it("does not modify HTML that has no binary file paths", () => {
|
|
const html = "<p>Hello, this is regular text with /tmp/script.sh</p>";
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).toBe(html);
|
|
});
|
|
|
|
it("does not linkify text file paths", () => {
|
|
const html = "<p>Saved to /tmp/output.txt</p>";
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).not.toContain("data-filepath");
|
|
});
|
|
|
|
it("handles a home-relative path", () => {
|
|
const html = "<p>Saved to ~/downloads/audio.flac</p>";
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).toContain('data-filepath="~/downloads/audio.flac"');
|
|
expect(result).toContain("🎵");
|
|
});
|
|
|
|
it("handles multiple file paths in the same HTML", () => {
|
|
const html = "<p>Files: /tmp/a.pdf and /tmp/b.mp3</p>";
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).toContain('data-filepath="/tmp/a.pdf"');
|
|
expect(result).toContain('data-filepath="/tmp/b.mp3"');
|
|
});
|
|
|
|
it("does not linkify paths that contain double quotes (invalid path character)", () => {
|
|
// Double quotes are excluded from path chars so the path is not matched
|
|
const html = `<p>Saved to /tmp/my"file.pdf</p>`;
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).not.toContain("data-filepath");
|
|
});
|
|
|
|
it("preserves existing HTML tags and attributes", () => {
|
|
const html = '<p class="foo">Saved to /tmp/report.pdf</p>';
|
|
const result = linkifyFilePaths(html);
|
|
expect(result).toContain('class="foo"');
|
|
expect(result).toContain('data-filepath="/tmp/report.pdf"');
|
|
});
|
|
|
|
it("does not double-linkify a path already inside an anchor tag", () => {
|
|
const html = '<a href="/tmp/file.pdf">/tmp/file.pdf</a>';
|
|
const result = linkifyFilePaths(html);
|
|
// The href is inside a tag (placeholder), the text content IS linkified
|
|
// but the href itself should not be modified
|
|
const hrefMatches = result.match(/href="[^"]*\/tmp\/file\.pdf[^"]*"/g) ?? [];
|
|
expect(hrefMatches.length).toBe(1);
|
|
});
|
|
|
|
it("returns the input unchanged when html is empty", () => {
|
|
expect(linkifyFilePaths("")).toBe("");
|
|
});
|
|
});
|