generated from nhcarrigan/template
7ebd9dc97a
## 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>
88 lines
2.0 KiB
TypeScript
88 lines
2.0 KiB
TypeScript
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();
|