feat: multiple UI improvements, font settings, and memory file display names (#175)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 57s
CI / Lint & Test (push) Has been cancelled
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled

## Summary

- **fix**: `show_thinking_blocks` setting now persists across sessions — it was defined on the TypeScript side but missing from the Rust `HikariConfig` struct, so serde silently dropped it on every save/load
- **feat**: Tool calls are now rendered as collapsible blocks matching the Extended Thinking block aesthetic, replacing the old inline dropdown approach
- **feat**: Add configurable max output tokens setting
- **feat**: Use random creative names for conversation tabs
- **test**: Significantly expanded frontend unit test coverage
- **docs**: Require tests for all changes in CLAUDE.md
- **feat**: Allow users to specify a custom terminal font (Closes #176)
- **feat**: Display friendly names for memory files derived from the first heading (Closes #177)
- **feat**: Add custom UI font support for the app chrome (buttons, labels, tabs)
- **fix**: Apply custom UI font to the full app interface — `.app-container` was hardcoded, blocking inheritance from `body`; also renamed "Custom Font" to "Custom Terminal Font" for clarity

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #175
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #175.
This commit is contained in:
2026-03-03 20:21:58 -08:00
committed by Naomi Carrigan
parent 97b8243d24
commit fa906684c2
48 changed files with 7148 additions and 101 deletions
@@ -232,6 +232,53 @@ describe("notifications", () => {
// Should not throw
await expect(soundPlayer.play(NotificationType.SUCCESS)).resolves.toBeUndefined();
});
it("play warns when audio type is not in the cache", async () => {
const { soundPlayer } = await import("./soundPlayer");
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
soundPlayer.setEnabled(true);
await soundPlayer.play("nonexistent" as NotificationType);
expect(warnSpy).toHaveBeenCalledWith("No audio found for notification type: nonexistent");
warnSpy.mockRestore();
});
it("play catches errors from audio playback", async () => {
vi.resetModules();
class FailingAudio {
volume = 1;
preload = "auto";
cloneNode() {
const clone = new FailingAudio();
clone.volume = this.volume;
return clone;
}
async play(): Promise<void> {
throw new Error("Playback blocked by browser");
}
}
const originalAudio = globalThis.Audio;
globalThis.Audio = FailingAudio as unknown as typeof Audio;
const { soundPlayer: freshPlayer } = await import("./soundPlayer");
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
freshPlayer.setEnabled(true);
await freshPlayer.play(NotificationType.SUCCESS);
expect(errorSpy).toHaveBeenCalledWith(
"Failed to play notification sound:",
expect.any(Error)
);
errorSpy.mockRestore();
globalThis.Audio = originalAudio;
});
});
describe("NotificationManager class", () => {