Settings → Energy: edit global energy labels (label + description per
level) with reset-to-defaults. Per-profile override toggle in the
Profiles section ("Customise for this profile" / "Use global").
EnergyChip and Tasks-page energy controls now resolve labels via
resolveEnergyLabels(profile, settings) — global by default, profile
override when present.
First-time prompt: on first energy-chip tap of the day (per profile)
or first-ever tap to Zero, an inline non-modal prompt asks "What does
Zero mean for you today?" with example chips. Per-day gate via
energyPromptLastShownDate; one-shot first-Zero gate via
energyPromptZeroSeen; Settings escape resets both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Storage: 5 task_lists CRUD functions (list/create/update/delete/
import) and 4 archive functions (list_archived_tasks/archive_task/
unarchive_task/archive_inbox_older_than). 8 new tests cover CRUD,
import idempotency, atomic-failure rollback, dependent-task null-out,
archive scope, archive_inbox_older_than Inbox-only invariant, and
unarchive round-trip.
Tauri: 9 new commands (5 task_lists + 4 archive) registered in the
invoke_handler. New file commands/task_lists.rs mirroring tasks.rs
structure. TaskDto extended with archived/archivedAt.
Frontend: loadTaskLists rewired to read from SQLite via
list_task_lists_cmd on Tauri runtime, with a one-time migration
that imports kon_task_lists localStorage into SQLite via
import_task_lists_cmd and clears localStorage only on success.
mapTaskRow handles the new archived fields. TasksPage gets an
Archived filter pill (hidden when empty) and a per-row archive
icon. moveTaskList/sortTaskLists kept local pending a future
order-column migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flips the Tasks landing bucket from All to Today so the cold-open answer to
"what should I do now?" is scoped to today's commitments rather than the
full backlog. Refactors WipTaskList to accept a tasks prop instead of
reading the global store, and wires it into TasksPage so the Now lane and
the bucket list below share one filtered set as their source of truth —
the lane consumes the first three top-level rows and the bucket list
excludes those IDs, so no task is ever rendered in both panels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add cue-anchored "When [cue], [action]" framing to the task-decomposition
prompt where natural cues are present (Gollwitzer-style implementation
intentions, d=0.65 effect size). Soften Bionic Reading and accessibility-
font copy to honest preference framing per the v3 audit (Strukelj 2024;
Doyon n=2,074). Update timer nudge from "Still on that timer?" (which
read as judgmental) to "Timer's still running." Replace stale Tasks
page header copy promising automatic extraction.
Audio envelopes (focusTimer 20ms ramp, sounds.ts 10ms attack) verified
correct per memo §B; no code change needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Sparkline: friendlier aria-label ("3 completed today. 14 total over
the last 7 days." rather than a numeric list), per-bar <title>
tooltips with absolute date + count, 30 ms stagger entrance via
scaleY animation. Badge: 180 ms opacity + translateY entrance on
mount; conditional render means each new badge re-fires the
animation. Both animations respect prefers-reduced-motion.
The earlier draft tabindex=0 on the SVG was correctly flagged by
svelte-check as noninteractive_tabindex; SVG role="img" + aria-label
is sufficient for SR navigation without putting it in the keyboard
tab order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Badge renders when today's count > 0. Sparkline renders when the
setting is enabled and any of the last 7 days has a completion.
Wrapped in a narrow aria-live region so increments announce without
re-reading the rest of the header.
Fix: converted todayCount from $derived module export to a getter
function (Svelte 5 derived_invalid_export constraint).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex post-implementation review flagged one MAJOR: the energy segmented
control declared `role="radiogroup"` / `role="radio"` but only wired
`onclick`. No arrow-key navigation, no Home/End, no roving tabindex.
Keyboard users got four independent tab stops while assistive tech was
told it was a single radio group — a broken ARIA contract.
Fix (W3C APG Radio Group pattern):
- Extract the options list as `energyOptions` so the render loop and
the keyboard handler share one source of truth.
- `energyRadioKeydown` handles ArrowLeft/Right/Up/Down (cycle wraps),
Home (first), End (last).
- Roving tabindex: the currently-checked button gets `tabindex=0`,
the rest get `tabindex=-1`, matching the APG recipe. Focus moves
with selection.
- The radiogroup container gets `tabindex="-1"` to satisfy the
svelte-check a11y rule without creating its own tab stop.
All green: 251 tests, clippy -D warnings, fmt, svelte-check 0/0, build.
Closes Phase 3 of the 2026-04-23 feature-complete roadmap. Incorporates
the Codex plan-review fixes from this session: profile-free index, tri-
state update command, and de-prioritise-not-hide semantics.
Storage (kon-storage):
- Migration v11 adds `energy TEXT` to `tasks` with a CHECK constraint on
`high | medium | brain_dead | NULL`. Index `(energy, created_at DESC)`
— deliberately not per-profile because the tasks table carries no
profile_id column yet (tracked as a separate gap in HANDOVER).
- `TaskRow.energy: Option<String>` plus `task_row_from` read.
- `insert_task` signature grows by one optional arg (`energy`). Allowed
`too_many_arguments` with a rationale comment — the positional shape
matches the column order and flipping to a params struct would have
rippled through every caller for cosmetic benefit only.
- New `set_task_energy(pool, id, Option<&str>) -> TaskRow`. Lives as its
own function because `update_task` uses COALESCE to let `None` mean
"preserve" — which would make clearing the tag impossible.
- Two new tests: round-trip including explicit NULL clear, and CHECK
constraint rejection of unknown values.
- Tests updated for the v10 → v11 version bump.
Tauri (src-tauri):
- `TaskDto.energy`. `CreateTaskRequest.energy` (optional). Inline
validation against the allowed set before hitting the DB, so frontend
bugs surface as friendly errors instead of CHECK-constraint failures.
- New `set_task_energy_cmd` command mirroring the storage tri-state API.
Frontend (svelte):
- `EnergyLevel` type added to `types/app.ts`. `TaskDto`, `TaskEntry`, and
`TaskDraft` grow an `energy` field.
- `SettingsState.currentEnergy` (persisted) + `matchMyEnergy` (persisted
toggle). Defaults: null + false — no surface change until user opts in.
- `setTaskEnergy(id, EnergyLevel | null)` action on the task store.
Calls the dedicated Tauri command, updates local state, broadcasts to
sibling windows.
- `EnergyChip.svelte` — new component. Cycles unset → High → Medium →
Brain-Dead → unset on click. Colour tokens: accent / warning /
text-tertiary (deliberately not danger-red for Brain-Dead — the brief
is explicit that this state must not feel pathologised).
- Chip rendered on every task row in TasksPage and every row in
WipTaskList. Hidden-until-hover when energy is unset so untagged rows
stay calm; always visible once tagged because the colour is the signal.
- Tasks page header gains a "I feel" segmented control and a
"Match my energy" toggle. When both are active, matching tasks sort
to the top — unset tasks are treated as Medium-equivalent. Nothing is
ever hidden; this is a de-prioritisation, not a filter.
Deferred / out of scope:
- LLM-driven surfacing (brief says "The AI surfaces...") — deterministic
client-side sort is v1; LLM layer is a later phase.
- tasks.profile_id + per-profile energy sort — separate migration.
All green: cargo build + 251 tests + clippy -D warnings (0 warnings)
+ fmt + svelte-check (0/0) + npm run build.
Wholesale JS -> TS migration of the frontend — stores, utils, actions,
and all Svelte component scripts adopt type annotations. Compile-time
surfaces (app.d.ts, lib/types/) added for shared DTO types.
Build plumbing:
- package.json: dev:frontend script that runs svelte-kit sync first
- tauri.conf.json: beforeDevCommand points at dev:frontend
- run.sh: dropped the sed-hack that temporarily blanked beforeDevCommand;
now relies on npm run dev:frontend to avoid double-Vite
- jsconfig.json: allowImportingTsExtensions
Preserves all Group 1 behaviour:
- page.svelte.ts keeps loadHistory / loadTasks Tauri-first, no
localStorage; saveTranscriptMeta + mapTranscriptRow + mapTaskRow
intact; update_task_cmd and update_transcript_meta_cmd invocations
carry the correct payload shape.
- Toasts, preferences stores typed without behaviour change.
- Viewer still routes segment edits through saveTranscriptMeta; the
Task 1.5 TODO markers are gone.
taskExtractor.ts is functionally improved during the migration:
- multi-task matches in the same sentence
- list-style shopping-verb expansion (get bread, milk, and cheese)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>