feat(auto-title): auto on save + per-row + bulk in History

Wire generate_title_cmd into the user-visible flows. Three entry points,
matching the agreed hybrid trigger (auto when conditions are met, plus
on-demand for retroactive titling of old transcripts):

1. Auto on save — addToHistory in src/lib/stores/page.svelte.ts now
   fires `maybeAutoGenerateTitle` fire-and-forget after the SQLite
   write succeeds. Gate: `aiTier !== "off" && formatMode !== "Raw"`,
   piggybacking on the same Settings choice that drives auto-cleanup.
   Per the design principle "Every new setting must earn its mental
   real estate" (README.md:22), no new settings flag. Skipped silently
   when LLM isn't loaded; user retries via the per-row button. Never
   overwrites a title the caller already set.

2. Per-row "Title" button in HistoryPage — mirrors the existing Tag
   button shape exactly (Sparkles icon vs Tag icon). In-flight tracked
   via a `titling: Set<string>` so the same row can't fire twice; the
   row stays disabled with "Titling…" copy while in flight. Persists
   via the existing `renameHistoryEntry` (which already calls
   `update_transcript`) — no need to extend `saveTranscriptMeta`.

3. Bulk "Title all untitled" toolbar action — same shape as
   "Tag all untagged". Filters `history` to entries where
   `!item.title || !item.title.trim()`, iterates with progress
   text, surfaces a success toast at the end with the count actually
   written (not iterated — empty model responses count as skipped).

Visible with no schema changes, no migration: transcripts.title has
been nullable since v1 (migrations.rs:12-29).

Verification:
- `npm run check`: 0 errors, 0 warnings across 3957 files.
- `cargo test --workspace --lib`: 280 passing (was 277; +3 from the
  sanitize_title tests landed in the kon-llm commit).

Together with the prior two commits this closes the "auto-titles"
plan from /home/jake/.claude/plans/delightful-meandering-moth.md.
Out of scope per that plan: settings toggle, regenerate-on-edit,
i18n-locale-matching titles.
This commit is contained in:
2026-04-25 19:48:29 +01:00
parent be49bc4374
commit ce849a15ab
2 changed files with 116 additions and 1 deletions

View File

@@ -251,11 +251,44 @@ export async function addToHistory(entry: TranscriptWriteEntry) {
antiHallucination: !!entry.antiHallucination,
},
});
// Fire-and-forget auto-title. Same gate as the existing auto-cleanup
// path (`aiTier !== "off" && formatMode !== "Raw"`) so users opt in
// by the same Settings choice — no new toggle. Skipped silently if
// the LLM isn't loaded; HistoryPage's per-row "Title" button is the
// retry path. Never overwrites a title the caller already set.
void maybeAutoGenerateTitle(normalised.id, normalised.text, normalised.title);
} catch (err) {
console.warn("addToHistory: SQLite write failed, entry will not persist past restart", err);
}
}
/// Run the auto-title pass when the same gate that drives auto-cleanup
/// is met. Best-effort: any failure (LLM not loaded, model rejected the
/// transcript, sanitisation produced an empty string) is swallowed
/// because the user can still title the row manually via the per-row
/// button in History.
async function maybeAutoGenerateTitle(
id: string,
text: string,
existingTitle: string | undefined | null,
): Promise<void> {
if (!hasTauriRuntime()) return;
if (settings.aiTier === "off" || settings.formatMode === "Raw") return;
if (!text.trim()) return;
if (existingTitle && existingTitle.trim()) return;
try {
const title = await invoke<string>("generate_title_cmd", { transcript: text });
if (title && title.trim()) {
await renameHistoryEntry(id, { title: title.trim() });
}
} catch (err) {
// Auto-title is a nice-to-have, not core flow. The most common
// failure (LLM not loaded yet) is exactly the case where the
// user can re-trigger via the per-row "Title" button.
console.warn("maybeAutoGenerateTitle skipped", err);
}
}
export async function renameHistoryEntry(
id: string,
updates: { title?: string; text?: string },