feat(ux): dogfood pass — onboarding, tasks, LLM chip, float popout

Bundles a session of dogfood UX feedback plus the two Cursor Bugbot
findings on the auto-titles branch.

Onboarding (FirstRunPage):
- Welcome leads with "Set up automatically"; system breakdown and the
  full model list move behind a "Choose manually" disclosure
- Morning / evening / autostart modal copy trimmed to one short
  sentence each; CTAs shortened
- "Corbie" autostart string reverted to "Kon"
- Already-downloaded models are clickable in the picker so the
  Settings → About → Replay onboarding flow doesn't re-download
- Autostart "No thanks" now does isEnabled() → disable() to actually
  remove the OS login item when replaying after a previous "Yes"

Tasks page:
- Bucket nav (All / Inbox / Today / Soon / Later) now a horizontal
  pill row; was stacking because nav was block-level
- List sidebar sized to content via self-start max-h-full instead of
  stretching to viewport when sparse
- Energy chip surfaces at opacity-60 when unset (was opacity-0,
  hidden until hover) so the affordance is discoverable
- "Brain-Dead" energy label → "Zero" everywhere user-facing; enum
  stays brain_dead to avoid a destructive DB migration

LLM status chip (llmStatus.svelte.ts + Dictation/Settings):
- Chip no longer auto-warms when the engine isn't loaded; it's hidden
  unless ready / generating / loading / error
- refreshLlmStatus takes { force: true } so post-load reconcile clears
  stale "warming"; ambient refreshes still preserve in-flight state
- markError exported; failed loads surface "AI error" with detail
  rather than silently going to off
- check_llm_model is the source of truth (replaces the bool-only
  get_llm_status path in the store)

Float popout window:
- Native decorations off — was stacking two titlebars + two close X's
  on KWin, one of which silently failed
- ResizeHandles mounted outside the animate-float-enter wrapper so
  fixed-position handles anchor to the viewport, not the transformed
  root; secondary-windows capability gains
  core:window:allow-start-resize-dragging for tasks-float
- GTK Utility WindowTypeHint applied pre-map (mirroring the preview
  window) so KWin Wayland honours always-on-top reliably
- visible_on_all_workspaces(true) so the pinned tasks list follows
  workspace switches
- togglePin does hide()+show()+focus() on re-pin to nudge the
  compositor into re-evaluating window state
- Pop-out / Edit / Open viewer buttons hidden on Android via
  isAndroid() — the multi-window Tauri commands stub out there

Build / Bugbot:
- src-tauri Cargo.toml: whisper feature now chains whisper-vulkan, so
  the dev runner's --no-default-features --features whisper
  invocation actually pulls Vulkan acceleration instead of silently
  falling back to CPU-only
- jsconfig.json's inherited "types": ["node"] fixed by adding
  @types/node; corresponding @ts-expect-error in vite.config.js
  removed now that process is a known global

Verification: svelte-check + cargo check pass clean. Manual
device-side validation still pending for float resize and replay
autostart "No thanks" — those are the only remaining confidence items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 23:43:00 +01:00
parent ce849a15ab
commit a15167c44e
18 changed files with 403 additions and 174 deletions

View File

@@ -5,7 +5,7 @@
import { emit } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { page, settings, templates, profiles, addToHistory, addTask, tasks } from "$lib/stores/page.svelte.js";
import { markGenerating, markGenerationDone } from "$lib/stores/llmStatus.svelte.js";
import { markError, markGenerating, markGenerationDone, markLoading, refreshLlmStatus } from "$lib/stores/llmStatus.svelte.js";
import { playStartCue, playStopCue, playCompleteCue } from "$lib/utils/sounds.js";
import { profilesStore } from "$lib/stores/profiles.svelte.ts";
import { toasts } from "$lib/stores/toasts.svelte.js";
@@ -22,7 +22,8 @@
import { bionicReading } from '$lib/actions/bionicReading.js';
import { measurePreWrap } from '$lib/utils/textMeasure.js';
import { transcriptPretextFont, transcriptPretextLineHeight } from '$lib/utils/accessibilityTypography.js';
import { hasTauriRuntime } from '$lib/utils/runtime.js';
import { hasTauriRuntime, isAndroid } from '$lib/utils/runtime.js';
import { errorMessage } from '$lib/utils/errors.js';
const prefs = getPreferences();
const tauriRuntimeAvailable = hasTauriRuntime();
const browserPreviewMessage = "You're viewing Kon in a normal browser. Local transcription only works in the Tauri desktop app window.";
@@ -240,16 +241,22 @@
try {
const status = await invoke("check_llm_model", { modelId: settings.llmModelId });
if (status?.downloaded && !status.loaded) {
await invoke("load_llm_model", {
modelId: settings.llmModelId,
// Sequential-GPU guard (brief item A.1 #28): frees whisper
// before loading the LLM on tight-VRAM setups. "parallel"
// keeps both resident (default, safe on multi-GB cards).
concurrent: settings.aiGpuConcurrency === "parallel",
});
markLoading("Loading AI model");
try {
await invoke("load_llm_model", {
modelId: settings.llmModelId,
// Sequential-GPU guard (brief item A.1 #28): frees whisper
// before loading the LLM on tight-VRAM setups. "parallel"
// keeps both resident (default, safe on multi-GB cards).
concurrent: settings.aiGpuConcurrency === "parallel",
});
} finally {
await refreshLlmStatus(settings.aiTier, settings.llmModelId, { force: true });
}
}
} catch (err) {
console.warn("ensureLlmModelLoaded failed", err);
markError(errorMessage(err));
}
}
@@ -377,7 +384,11 @@
// Preview overlay: if the user opted in, reset any leftover state
// from a prior run and open the window when the main window is not
// focused (i.e. the user is dictating into some other app).
if (settings.transcriptionPreview && tauriRuntimeAvailable) {
if (settings.transcriptionPreview && tauriRuntimeAvailable && !isAndroid()) {
// open_preview_window is a desktop-only multi-window command; the
// Android Tauri stub returns an error. Skip it so we don't surface
// a noisy console error every time the user starts recording on
// a mobile build.
emit("preview-listening").catch(() => {});
try {
const focused = await getCurrentWindow().isFocused();