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:
@@ -14,6 +14,7 @@
|
||||
import { getPreferences, updatePreferences } from "$lib/stores/preferences.svelte.js";
|
||||
import { profilesStore, DEFAULT_PROFILE_ID } from "$lib/stores/profiles.svelte.ts";
|
||||
import { toasts } from "$lib/stores/toasts.svelte.js";
|
||||
import { errorMessage } from "$lib/utils/errors.js";
|
||||
import { clampTextLines } from "$lib/utils/textMeasure.js";
|
||||
import { bodyPretextLineHeight, pretextFontShorthand } from "$lib/utils/accessibilityTypography.js";
|
||||
import { Check, ChevronRight } from "lucide-svelte";
|
||||
@@ -22,7 +23,7 @@
|
||||
// Aliased because SettingsPage has its own local refreshLlmStatus
|
||||
// that just mutates the page-local llmLoaded bool. The store
|
||||
// version drives the sidebar chip (brief item #31).
|
||||
import { refreshLlmStatus as refreshGlobalLlmStatus } from "$lib/stores/llmStatus.svelte.js";
|
||||
import { refreshLlmStatus as refreshGlobalLlmStatus, markError as markGlobalLlmError, markLoading as markGlobalLlmLoading } from "$lib/stores/llmStatus.svelte.js";
|
||||
|
||||
const prefs = getPreferences();
|
||||
|
||||
@@ -520,7 +521,7 @@
|
||||
await invoke("download_llm_model", { modelId });
|
||||
llmDownloadingModel = "";
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId);
|
||||
llmStatus = "Download complete";
|
||||
} catch (err) {
|
||||
llmDownloadingModel = "";
|
||||
@@ -531,6 +532,7 @@
|
||||
async function loadSelectedLlmModel() {
|
||||
const modelId = selectedLlmModelId();
|
||||
llmStatus = "Loading...";
|
||||
markGlobalLlmLoading("Loading AI model");
|
||||
try {
|
||||
await invoke("load_llm_model", {
|
||||
modelId,
|
||||
@@ -538,9 +540,12 @@
|
||||
concurrent: settings.aiGpuConcurrency === "parallel",
|
||||
});
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId, { force: true });
|
||||
} catch (err) {
|
||||
llmStatus = typeof err === "string" ? err : "LLM load failed";
|
||||
const message = errorMessage(err);
|
||||
llmStatus = message || "LLM load failed";
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId, { force: true });
|
||||
markGlobalLlmError(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +553,7 @@
|
||||
try {
|
||||
await invoke("unload_llm_model");
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId);
|
||||
llmStatus = "Model unloaded";
|
||||
} catch (err) {
|
||||
llmStatus = typeof err === "string" ? err : "LLM unload failed";
|
||||
@@ -560,7 +565,7 @@
|
||||
try {
|
||||
await invoke("delete_llm_model", { modelId });
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId);
|
||||
llmStatus = "Downloaded model removed";
|
||||
} catch (err) {
|
||||
llmStatus = typeof err === "string" ? err : "Delete failed";
|
||||
@@ -589,7 +594,7 @@
|
||||
// already loaded) — refresh both Settings-local and global
|
||||
// status so the sidebar chip and download/load buttons react.
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId);
|
||||
} catch (err) {
|
||||
llmStatus = typeof err === "string" ? err : "Test failed";
|
||||
llmTestHint = "";
|
||||
@@ -618,7 +623,7 @@
|
||||
await unloadLlmModel();
|
||||
} else {
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId);
|
||||
}
|
||||
llmStatus = llmModelDownloaded(modelId)
|
||||
? "Selected model changed. Load it to enable AI features."
|
||||
@@ -630,7 +635,7 @@
|
||||
if (openSection === 'ai') {
|
||||
await ensureRecommendedLlmTier();
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +724,7 @@
|
||||
systemInfo = await invoke("probe_system").catch(() => null);
|
||||
await ensureRecommendedLlmTier();
|
||||
await refreshLlmStatus();
|
||||
await refreshGlobalLlmStatus(settings.aiTier);
|
||||
await refreshGlobalLlmStatus(settings.aiTier, settings.llmModelId);
|
||||
const loaded = await invoke("check_engine");
|
||||
engineOk = loaded;
|
||||
engineStatus = loaded ? "Model loaded" : "No model loaded";
|
||||
@@ -2322,6 +2327,26 @@
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Replay onboarding — useful for testing first-run flow without
|
||||
wiping data. Resets the rituals-prompt-seen flag and routes
|
||||
the main shell back to the FirstRunPage. Already-downloaded
|
||||
models stay clickable there, so no re-download is needed. -->
|
||||
<div class="mt-6 pt-5 border-t border-border-subtle">
|
||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Onboarding</p>
|
||||
<p class="text-[11px] text-text-tertiary mb-3">
|
||||
Replay the first-run welcome and the morning / evening / autostart prompts. Your downloaded models, transcripts, and tasks are kept.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
settings.ritualsPromptSeen = false;
|
||||
saveSettings();
|
||||
page.current = "first-run";
|
||||
}}
|
||||
class="px-3 py-2 text-[12px] text-text border border-border rounded-lg hover:border-accent hover:text-accent"
|
||||
>Replay onboarding</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user