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>
110 lines
3.7 KiB
Svelte
110 lines
3.7 KiB
Svelte
<script lang="ts">
|
|
// Phase 3 — Energy tag chip. Cycles a task's energy level through
|
|
// the spec's four states: unset → High → Medium → Zero → unset.
|
|
//
|
|
// Visual discipline: when energy is unset, the chip renders at a
|
|
// muted but still legible opacity so users can discover the affordance
|
|
// without first hovering the row. The previous "0% until hover"
|
|
// behaviour hid the control entirely, which read as broken on first
|
|
// contact. Once set, the chip is always fully visible because the
|
|
// colour IS the signal for the match-my-energy sort.
|
|
//
|
|
// Colour choices borrow the existing design tokens:
|
|
// High → accent (warm, on-brand, attention-ready)
|
|
// Medium → warning (amber, unforced)
|
|
// Zero → text-tertiary (low-energy grey, not danger red — the
|
|
// brief is explicit that this state must not feel
|
|
// pathologised; the internal enum value remains
|
|
// `brain_dead` to avoid a DB migration churn)
|
|
//
|
|
// Callers pass the task's current energy and a setter. This component
|
|
// owns no state — the task store is the source of truth.
|
|
|
|
import type { EnergyLevel } from "$lib/types/app";
|
|
import { Zap } from "lucide-svelte";
|
|
|
|
let {
|
|
energy = null as EnergyLevel | null,
|
|
onSelect,
|
|
size = "sm",
|
|
reduceMotion = false,
|
|
}: {
|
|
energy: EnergyLevel | null;
|
|
onSelect: (next: EnergyLevel | null) => void;
|
|
size?: "sm" | "md";
|
|
reduceMotion?: boolean;
|
|
} = $props();
|
|
|
|
// Cycle order lives here so the chip is the single authority on what
|
|
// "next" means. Tap once to tag, tap again to move up, tap past
|
|
// Zero to clear. Keyboard-equivalent via the <button> element.
|
|
const CYCLE: (EnergyLevel | null)[] = [null, "high", "medium", "brain_dead"];
|
|
|
|
function next(): EnergyLevel | null {
|
|
const idx = CYCLE.indexOf(energy);
|
|
return CYCLE[(idx + 1) % CYCLE.length];
|
|
}
|
|
|
|
function labelFor(level: EnergyLevel | null): string {
|
|
switch (level) {
|
|
case "high": return "High";
|
|
case "medium": return "Medium";
|
|
case "brain_dead": return "Zero";
|
|
default: return "No energy set";
|
|
}
|
|
}
|
|
|
|
let tooltip = $derived(
|
|
energy === null
|
|
? "Tag energy (click to set)"
|
|
: `Energy: ${labelFor(energy)} — click to change`
|
|
);
|
|
|
|
// Icon dimensions. `md` is the one used on the Tasks-page main rows;
|
|
// `sm` is for the compact WIP list rows and micro-step children.
|
|
let iconSize = $derived(size === "md" ? 13 : 10);
|
|
let chipSize = $derived(size === "md" ? "h-5" : "h-4");
|
|
</script>
|
|
|
|
<button
|
|
type="button"
|
|
class="energy-chip inline-flex items-center justify-center rounded-md border px-1 {chipSize} text-[10px] font-medium
|
|
{energy === null
|
|
? 'opacity-60 group-hover:opacity-100 text-text-tertiary border-border-subtle hover:border-accent hover:text-text-secondary'
|
|
: ''}
|
|
{energy === 'high'
|
|
? 'text-accent border-accent bg-accent/10'
|
|
: ''}
|
|
{energy === 'medium'
|
|
? 'text-warning border-warning bg-warning/10'
|
|
: ''}
|
|
{energy === 'brain_dead'
|
|
? 'text-text-tertiary border-border bg-hover'
|
|
: ''}"
|
|
onclick={() => onSelect(next())}
|
|
aria-label={tooltip}
|
|
title={tooltip}
|
|
data-energy={energy ?? 'unset'}
|
|
style={reduceMotion
|
|
? ''
|
|
: 'transition: opacity var(--duration-ui), color var(--duration-ui), border-color var(--duration-ui), background-color var(--duration-ui)'}
|
|
>
|
|
<Zap size={iconSize} aria-hidden="true" />
|
|
{#if energy !== null && size === "md"}
|
|
<span class="ml-1">{labelFor(energy)}</span>
|
|
{/if}
|
|
</button>
|
|
|
|
<style>
|
|
.energy-chip {
|
|
cursor: pointer;
|
|
line-height: 1;
|
|
font-family: var(--font-family-body);
|
|
}
|
|
|
|
.energy-chip:focus-visible {
|
|
outline: 2px solid var(--color-accent);
|
|
outline-offset: 1px;
|
|
}
|
|
</style>
|