feat: new drafts feature and sound spam fix (#174)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 58s
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
CI / Lint & Test (push) Has been cancelled

## Summary

- **Saved Drafts feature**: Users can now save input content as drafts for later use, and manage them from a new panel
- **Sound spam fix**: The "Working on it!" sound no longer plays repeatedly when Claude makes multiple tool calls in a row

## Details

### Drafts feature
- Rust backend (`drafts.rs`) with `list_drafts`, `save_draft`, `delete_draft`, and `delete_all_drafts` commands, persisted to `hikari-drafts.json` via the Tauri Store plugin
- `draftsStore` wrapping all four commands with timestamp formatting
- `DraftPanel` overlay with insert, per-item two-step delete confirmation, delete-all with confirmation, empty state, and slide-in animation
- **Drafts** button in the top control row (pencil icon)
- **Save as Draft** floppy-disk icon button in the button wrapper (disabled when input is empty)

### Sound spam fix
- Root cause: `resetSoundState` was called on **every** `thinking` state transition, including mid-task transitions (`coding → thinking → coding`)
- Fix: only reset sound state when entering `thinking` from a clean-slate state (`idle`, `success`, or `error`) — states that genuinely mark the end of one task and the start of a new one

## Test plan
- [ ] Save a draft and verify it persists across app restarts
- [ ] Insert a draft and verify it populates the input
- [ ] Delete individual drafts and verify delete-all works
- [ ] Verify "Working on it!" plays once per user message regardless of how many tools are called

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #174
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #174.
This commit is contained in:
2026-02-27 15:07:10 -08:00
committed by Naomi Carrigan
parent fe7027c585
commit 7ebd9dc97a
8 changed files with 807 additions and 4 deletions
+87
View File
@@ -0,0 +1,87 @@
import { writable } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
export interface Draft {
id: string;
content: string;
saved_at: string;
}
function createDraftsStore() {
const drafts = writable<Draft[]>([]);
const isLoading = writable(false);
async function loadDrafts(): Promise<void> {
isLoading.set(true);
try {
const list = await invoke<Draft[]>("list_drafts");
drafts.set(list);
} catch (error) {
console.error("Failed to load drafts:", error);
} finally {
isLoading.set(false);
}
}
async function saveDraft(content: string): Promise<Draft | null> {
try {
const draft = await invoke<Draft>("save_draft", { content });
await loadDrafts();
return draft;
} catch (error) {
console.error("Failed to save draft:", error);
return null;
}
}
async function deleteDraft(draftId: string): Promise<boolean> {
try {
await invoke("delete_draft", { draftId });
await loadDrafts();
return true;
} catch (error) {
console.error("Failed to delete draft:", error);
return false;
}
}
async function deleteAllDrafts(): Promise<boolean> {
try {
await invoke("delete_all_drafts");
drafts.set([]);
return true;
} catch (error) {
console.error("Failed to delete all drafts:", error);
return false;
}
}
function formatTimestamp(isoString: string): string {
const date = new Date(isoString);
if (isNaN(date.getTime())) {
return isoString;
}
try {
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
} catch {
return isoString;
}
}
return {
drafts: { subscribe: drafts.subscribe },
isLoading: { subscribe: isLoading.subscribe },
loadDrafts,
saveDraft,
deleteDraft,
deleteAllDrafts,
formatTimestamp,
};
}
export const draftsStore = createDraftsStore();