HistoryPage previously serialised the full TranscriptDto — text, segments,
manual + LLM tags, audio path — into `localStorage["kon_viewer_item"]` so
the viewer window could pick it up on mount. On a multi-hour transcript
that's MB-scale of user voice content sitting in storage that any
same-origin script in any open Kon window can read.
Hand off only `{ id }` (and a timestamp on re-saves). The viewer fetches
the canonical row from SQLite via the existing `get_transcript` Tauri
command and hydrates via the now-exported `mapTranscriptRow`. Cross-window
sync via the `storage` event still works — the receiving window re-fetches
on event instead of trusting the payload.
- HistoryPage `openViewer` + `openEditor`: write `{ id }` only.
- viewer `onMount` + `handleStorageChange`: route through new
`loadFromHandoff` which calls `invoke("get_transcript", { id })`.
- viewer `saveItemToHistory`: re-stamp localStorage with `{ id, stamp }`
to retrigger the storage event in sibling windows without leaking
content.
- `mapTranscriptRow` exported from page.svelte.ts for the viewer's use.
Backward-compatible at the parse layer: the `{ id }` shape extracts cleanly
from a stale full-DTO payload (TranscriptEntry already carries `id` at top
level), so a session that survives the upgrade picks up the new path on
next handoff without manual cleanup.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
`detect_meeting_processes` is called every 15 s when meeting-auto-capture
is enabled. The previous `list_running_process_names` allocated a fresh
`sysinfo::System` per call and walked /proc cold; on a busy host
(~300 processes) that's ~50–100 ms of work, every poll, forever.
Add `kon_core::process_watch::ProcessLister`, a thin wrapper around a
long-lived `System` whose process table is refreshed in place. The Tauri
host holds one behind a `Mutex<ProcessLister>` in a new `MeetingState`
managed at app setup. The free `list_running_process_names` is kept as a
convenience that constructs a fresh `ProcessLister` per call — its only
remaining caller is the existing smoke test.
- ProcessLister + Default in crates/core/src/process_watch.rs.
- MeetingState in src-tauri/src/commands/meeting.rs; the command takes
it via `tauri::State` and locks for the duration of the snapshot.
- src-tauri/src/lib.rs registers MeetingState alongside the other
managed states.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
Migration v15 adds a composite index covering the dominant transcripts
query path:
SELECT ... FROM transcripts
WHERE profile_id = ? ORDER BY created_at DESC LIMIT ?
Previously SQLite had to choose between idx_transcripts_profile_id
(filter by profile, then in-memory sort by date) and idx_transcripts_created
(scan dates and filter on profile). Both work fine at hundreds of rows
and degrade past a few thousand.
`migration_v15_creates_profile_created_index` asserts (a) the index exists
and (b) `EXPLAIN QUERY PLAN` shows the planner picks it for the canonical
profile-scoped, date-ordered list query.
Test count assertions in `test_migrations_run_on_empty_db` and
`test_migrations_idempotent` bumped 14 → 15.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
The kon-mcp stdio server is documented as "read-only, no auth, local-only"
but until now opened the SQLite store via `kon_storage::init`, which returns
a writable pool and runs migrations. Read-only-ness was enforced only by the
exposed tool surface (list_transcripts, get_transcript, search_transcripts,
list_tasks); a future bug or a malformed dispatch could escape into a write
against the user's primary database.
Add `kon_storage::init_readonly` that opens with `SqliteConnectOptions
::read_only(true)` and `create_if_missing(false)`, no migrations. The
constraint is now structural — SQLite rejects writes at the connection
level regardless of which handler runs.
- New `init_readonly(path)` in crates/storage/src/database.rs.
- Re-exported from kon_storage.
- crates/mcp/src/main.rs switched over and updated startup banner.
- Two tests: writes fail on the read-only pool, reads succeed; opening a
non-existent DB returns an error instead of silently creating one.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
The Phase 9 bulk-delete path passed UUID strings to deleteFromHistory(index),
which expected an integer; JS coerced the string to NaN and splice(NaN, 1)
collapsed to splice(0, 1), so bulk-delete silently removed the first N visible
rows instead of the selected ones, then fired delete_transcript against the
wrong IDs.
Clear-all called saveHistory(), which was a no-op stub left over from the
same incomplete-refactor pattern that produced the manualTags persistence bug
fixed in 7eb52d9. The in-memory array was emptied, but SQLite still held
every transcript, so they reappeared on next loadHistory().
- Add deleteFromHistoryById(id) next to the index-keyed deleteFromHistory.
- bulkDelete now calls deleteFromHistoryById.
- clearAll now awaits an explicit per-id delete loop and surfaces a toast on
partial failure.
- Remove the saveHistory() stub and its sole caller's import.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
Captures the agent-runnable portion of Phase 10a ahead of Jake's
manual walkthrough and feedback-document pass:
- a11y baseline confirmed clean (svelte-check 0/0; consistent
aria-label + aria-hidden patterns across icon buttons; global
:focus-visible ring set in design tokens; prefers-reduced-motion
guards present where motion warrants them)
- WCAG 2.1 AA contrast tables for both themes computed from the
token list at design-system/colors_and_type.css. Nine pairs miss
AA-normal; light-theme warning misses AA-large too. Severity
ranked, suggested token shifts noted as starting points
- CI matrix state: check.yml runs on every push, build.yml has
never been end-to-end exercised - recommend manual workflow_
dispatch before tagging v0.1.0
- Clean-install test plan and the Phase 9d walkthrough checklist
consolidated for the testing session
Phase 9 export plumbing, LLM content tags (with migration v14 + storage
+ Tauri-command extension that picks up the latent manualTags
persistence bug as a side effect), and sparkline polish all on main.
SettingsPage deeper restructure and walkthrough-driven a11y sweeps
deferred to a follow-up polish session and Phase 10a QC respectively.
Roadmap Phase 9 entry updated with shipped + deferred notes. HANDOVER
captures the full session including the plan-correction discovery and
the Codex-blocked retries. Phase 8 handover preserved as the dated
archive HANDOVER-2026-04-24.md.
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>
Phase 8 carryover backlog: the showMomentumSparkline toggle was
sitting under Rituals and visually claimed by the Launch-at-login
border-t subgroup. New top-level Tasks section hosts it, ready to
absorb future task-page settings (energy default, WIP limit, etc.).
The deeper Phase 9 SettingsPage restructure (search box + 7-group
progressive disclosure via the new SettingsGroup component) is
deferred to a follow-up polish session: the existing 2309-line file
uses a hand-rolled accordion that needs careful unwinding, and is
not in this session's scope. SettingsGroup component remains
available for that future pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reusable progressive-disclosure wrapper around the native <details>
element. Animated chevron, hover + focus-visible affordances, and
prefers-reduced-motion guard. Designed to host the six collapsed
groups in the Phase 9 SettingsPage restructure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-row Tag button calls extract_content_tags_cmd, persists via
saveTranscriptMeta. Dashed-italic chips render the AI tags distinct
from manual; clicking a chip promotes it into manualTags (the
LLM tag disappears, the manual one stays). Top toolbar gains "Tag all
untagged" for batch tagging across the corpus, with progress text.
Existing addManualTag / removeManualTag handlers swap their no-op
saveHistory() calls for saveTranscriptMeta — picks up the latent
manualTags persistence bug as a side effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TranscriptEntry gains llmTags: string[]; TranscriptRow gains the
storage-shape llmTags: string. ContentTags type added. mapTranscriptRow
hydrates llmTags from the comma-joined column. saveTranscriptMeta
forwards llmTags through to update_transcript_meta_cmd, mirroring the
existing manualTags handling.
buildFrontmatter unions auto + manual + llm tags so exported markdown
surfaces every source in one tags: list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds llm_tags TEXT NOT NULL DEFAULT '' to the transcripts table via
new migration v14. SELECT statements + TranscriptRow + transcript_row_from
now carry the column. update_transcript_meta gains a sixth Option for
llm_tags following the existing COALESCE pattern; an
#[allow(too_many_arguments)] keeps clippy happy without inverting the
signature into a struct that would just shift the indirection.
The Tauri-side TranscriptDto + UpdateTranscriptMetaRequest + the
update_transcript_meta_cmd command pass llm_tags through unchanged.
Pre-existing manualTags persistence path now has a sibling for
llmTags ready for the frontend to call.
Phase 8 brittle test fix included: list_recent_completions_uses_local_day_boundary
was anchoring its "-2 days" UTC offset against the local-day spine,
which drifted across UTC midnight. Anchored to the local date directly
so it matches the spine regardless of clock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bridges LlmEngine::extract_content_tags to the frontend with the same
spawn_blocking + PowerAssertion guard the cleanup_text command uses.
Returns a ContentTags object serialised to camelCase JSON. Errors
surface as readable strings so the frontend toast shows actionable
text on the rare grammar-bypass path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added as a method on LlmEngine alongside cleanup_text and
extract_tasks; same render_chat_prompt -> generate -> parse pattern.
Truncates the transcript to its trailing 2000 chars on a UTF-8 char
boundary, runs at temperature 0.0 with the CONTENT_TAGS_GRAMMAR GBNF,
and re-validates intent against INTENT_CLOSED_SET to catch the
unlikely grammar bypass case. max_tokens 96 is enough for the JSON
envelope. Smoke test gated on KON_LLM_TEST_MODEL like the existing
smoke.rs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ContentTags serde-serialisable. CONTENT_TAGS_SYSTEM is the system
message rendered at extraction time; INTENT_CLOSED_SET is the single
source of truth for the enum values the grammar restricts. Grammar is
strict: lowercase hyphen-joined topic 3+ chars (max enforced by
max_tokens at call site), intent from the closed set, JSON-only
output. Recursive topic-rest matches the existing GBNF style in this
file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slim leading checkbox on every row, tinted-row state when selected.
Bulk-action toolbar appears only when selection is non-empty: select
all (visible), clear, export selected (via exportTranscriptsToDir),
delete selected (single confirm). Esc clears selection. Cmd/Ctrl+A
selects all visible when focus is inside the list and not in a text
input. Stop-propagation on the checkbox keeps the click off the
row-expand toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the clipboard-only path with saveTranscriptAsMarkdown. User
picks the location via the OS save dialog; cancellation leaves no side
effect, no toast, no fallback. Toast on success names the file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Centralises the save-dialog plus write-file plumbing. suggestedFilename
slugs the title into "<slug>-<YYYY-MM-DD>.md". saveTranscriptAsMarkdown
opens the system save dialog and writes via write_text_file_cmd; on
cancel returns null with no toast or fallback. exportTranscriptsToDir
writes one .md per item to a chosen folder, in-batch collision suffix
" (2)" etc. Documents the deliberate non-check of pre-existing files
in the chosen directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin UTF-8 writer used by the new save-dialog path. Caller owns path
safety; the source path is always OS-dialog-provided. Two unit tests:
roundtrips a small UTF-8 string with non-ASCII chars and asserts a
nonexistent parent path returns an actionable error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-execution review against the actual codebase (Codex unavailable
this session) surfaced three mismatches: kon-llm uses LlmEngine with a
synchronous generate(prompt, config), AppState exposes llm_engine as a
direct Arc not behind RwLock, and llmTags persistence requires a real
SQLite migration plus Tauri command extension because saveHistory()
is a no-op stub today (which also means manualTags edits weren't
persisting). Plan now adds Task 8.5 for migration v14 + storage and
Tauri-command extension. Spec data-model section corrected to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sixteen tasks across four sub-phases (9a export plumbing, 9b LLM tags,
9b Settings restructure, 9d polish and a11y). TDD task structure for
the concrete items (save dialog, LLM extraction, types/persistence);
discovery-and-checklist structure for the polish items where an
a-priori test is not meaningful. Commit cadence matches Phase 8 (one
commit per task, feat/fix/polish prefix tags).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Downstream effect of the Task 4 commit (83bd338) adding
serde = "1" to crates/storage/Cargo.toml. Lockfile change is a
single line recording the new dependency on the pinned crates.io
version; no other crates affected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roadmap: Phase 8 header now carries SHIPPED 2026/04/24 alongside the
REVISED 2026/04/23 marker. Added a shipped note summarising the
landing commits, architectural deltas, and verification state.
Pre-Phase-10 Cargo.lock decision updated to RESOLVED since Jake's
hardening pass (commit b333c62) committed the lockfile.
HANDOVER rewritten for today's state. Covers Phase 8 end-to-end,
counting semantics, three architectural notes worth carrying forward
(serde in kon-storage; no module-scope $derived export; tuple FromRow
pattern), full verification counts, the manual dogfood walkthrough
still owed to Jake when he next opens Corbie, and a Phase 9 polish
backlog surfaced during review (sparkline aria-label summary form,
sparkline toggle placement inside Rituals section).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default on. Controls only the sparkline; the "N today" badge is
unconditional. Copy kept in the zero-loss register: "Never counts
against you." Co-located with the Rituals section for Phase 8.
Can move to a dedicated section in Phase 9 polish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 8 completionStats store listens on these events to refresh the
daily count. Keeps the badge + sparkline accurate after un-tick / delete
paths, which don't currently fire kon:task-completed.
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>
Tiny inline SVG. Seven bars, zero-days render as 1 px baseline stubs.
fill=currentColor so the parent's text colour (tertiary ink on the
Tasks header) drives it. Self-hides if all 7 days are zero.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owns the last-7-days completion series. Refreshes on task-completed /
step-completed / task-uncompleted / task-deleted window events and on
window focus (for day rollover). Derives todayCount from the newest
entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Phase 8 frontend types. New setting defaults to true (sparkline
on for everyone on upgrade) and persists via the existing settings
envelope; no migration needed because missing keys spread over the
defaults object on load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin wrapper over kon_storage::list_recent_completions, parameterised
by day count. Serialises to camelCase JSON (day, count).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns a fixed-length, oldest-first series of daily completion counts
for the last N local-time days. Excludes cascade parents and
uncompleted rows. Empty days are explicit zeros, not missing entries,
so the Phase 8 sparkline can render a fixed 7 bars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
uncomplete_task now clears auto_completed alongside done / done_at on
both the target row and the cascaded-parent reopen. Keeps the flag
accurate so a later re-completion via a different path is counted
correctly by the Phase 8 daily-count query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the project's style preference for full stops over em/en dashes.
No functional change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
complete_subtask_and_check_parent now sets auto_completed = 1 on the
parent when it closes via the cascade. The subtask UPDATE itself
remains at the default 0, so explicit user taps still count toward
the Phase 8 daily total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a flag on tasks to distinguish manual completions from the
cascade auto-completion performed by complete_subtask_and_check_parent.
Partial index on (done_at, auto_completed) supports the Phase 8
daily-count query without bloating the tasks index footprint.
Forward-only: pre-migration completed rows default to 0 (they count).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 bite-sized tasks with TDD on the Rust side (migrations +
cascade flag + list_recent_completions query + 4 storage tests) and
incremental frontend wiring (store, sparkline component, Tasks header
badge, event dispatches, settings toggle). Verification pass + roadmap
and HANDOVER update as final tasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers today's completion count badge + 7-day momentum sparkline on the
Tasks header. Chose to exclude auto-cascade parents from the count via a
new auto_completed column (migration v13), to respect the zero-loss
framing. Sparkline is a new settings toggle, default on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Small if-then automation layer. Rules persist in SQLite; the runner
lives on the frontend and binds to the Phase 6 event bus so the rule
pipeline reuses the same delivery primitives (timer events, TTS,
Tasks navigation).
Storage:
- Migration v12 adds implementation_rules (id, enabled, trigger_kind,
trigger_value, actions_json, last_fired_key, created_at,
updated_at) with enabled+trigger_kind index for the runner's hot
path.
- CRUD helpers: insert / list / get / set-enabled / mark-fired /
delete, plus a round-trip test.
Commands (all main-window-guarded via ensure_main_window):
- list_implementation_rules
- create_implementation_rule — validates HH:MM, checks the target
task exists at save time for surface-task actions, caps
speak-line at 240 chars, pins v1 timers to 5 minutes.
- set_implementation_rule_enabled
- mark_implementation_rule_fired — main-thread idempotency shim so
the runner can atomically claim a fire.
- delete_implementation_rule
Runner (implementationIntentions.svelte.ts):
- Subscribes to kon:task-completed and kon:morning-triage-finished
(MorningTriageModal now emits on all three exit paths — empty,
skipped, picked — so skip counts as finishing).
- 30 s poll for time-of-day rules, plus an immediate check on startup
so a rule whose time has already passed today catches up once.
- Idempotency via last_fired_key composed as YYYY-MM-DD@HH:MM for
time rules; new time rules whose HH:MM has already passed today
are pre-seeded so they don't fire retroactively on save.
- Rules are paused when Nudges "Mute for now" is on — a hard mute
stops all rule delivery in addition to OS notifications.
- Stale-task safety: if a surface-task action's target has been
deleted, the runner opens Tasks and warns clearly rather than
pretending to surface something that's gone.
Editor (ImplementationRulesEditor.svelte):
- Lives in Settings under a new "If-then rules" accordion section.
- `If` picker: time of day (with time input), a task completes,
morning triage finishes.
- `Then` composer: optional surface (inbox / today / all tasks /
specific task), optional 5-min timer, optional speak-aloud line.
- Saved rules list with enable toggle + delete.
Rules table integration for Phase 10b rename sweep: add
implementation_rules to the kon.db → corbie.db migration shim when
that phase lands.
Gates: fmt, clippy -D warnings, cargo test 265/0, svelte-check
0/0, npm build green. Pre-existing Vite chunk warning on sounds.ts
is unrelated to Phase 7.
Frontend-owned nudge bus that consumes in-app signals Corbie already
produces, applies suppression, and fans out to OS notification + an
optional TTS read-aloud. OS-wide keyboard/window activity detection
stays deferred per the revised roadmap — the plan before rewrite
would have been brittle on Wayland, permission-heavy on macOS, and
low-quality everywhere.
Triggers (v1, all in-app signals):
- inactivity_with_active_timer — timer running, window blurred ≥ 90 s,
at least 60 s into the timer.
- pending_morning_triage — ritual enabled, past 10:00 local, last
shown ≠ today. Polls every 5 min while focused.
- micro_step_idle — micro-step decomposition created, no child step
or parent task completed within 15 min.
Suppression:
- Respects nudgesEnabled + nudgesMuted.
- No nudge while the app has focus (document.hasFocus).
- Hard cap 3 per rolling hour.
- Permission requested via @tauri-apps/plugin-notification on first
delivery; denial is silently respected.
Rust side:
- tauri-plugin-notification registered + ACL entries on the main-
window capability only (secondary windows can't fire nudges).
- commands::nudges::deliver_nudge — thin wrapper, security-guarded
via ensure_main_window, delegates to the plugin. No DB writes —
the roadmap's nudges-audit table is deferred until a concrete need
emerges.
Frontend glue:
- nudgeBus.svelte.ts — subscribes to window events, applies
suppression, calls deliver_nudge (+ tts_speak when speakAloud is
on).
- kon:task-completed now dispatched on complete_task_cmd success.
- kon:microstep-generated + kon:step-completed dispatched from
MicroSteps so the idle trigger can clear itself on any engagement.
- kon:focus-timer-cancelled added to focusTimer so the bus can reset
its inactivity state on cancel, not only on natural completion.
- nudgeBus started from +layout.svelte onMount, stopped on destroy.
Settings:
- New "Nudges" section with three toggles: Enable nudges, Mute for
now (separate so a hard mute doesn't lose preferences),
Speak nudges aloud (reuses Phase 4 TTS).
- All default OFF. No first-run prompt — nudges are a Settings-found
feature rather than a walkthrough step.
Out of scope per the revised Phase 6 spec: OS-wide keyboard/window
hooks, biometric signals, custom trigger editor (Phase 7), notification
sound (platform variance too high for Layer-1 — revisit in Phase 9
polish with a bundled .wav).
Gates: fmt clean, clippy -D warnings clean, cargo test 262/0 (4
existing + 262 current), npm run check 0/0, npm run build green.
Three opt-in rituals, all default OFF. Research-anchored (Barkley's
point-of-performance, Sweller cognitive-load theory, Newport shutdown
ritual, Gollwitzer implementation intentions, Thaler/Sunstein nudge
with informed consent for the ADHD audience).
Morning triage: modal gated on ritualsMorning toggle, configurable
trigger time (default 08:00 to respect ADHD sleep inertia rather than
the spec's 06:00), "pick up to three for today" with a gentle swap
message on the fourth attempt. Skip sets last-shown-today so it never
re-prompts the same calendar day. last-shown persists via kon_storage.
Evening wind-down: dedicated page, user-triggered only (tray menu +
Settings button). Mechanical closure + physical reset + intentional
cue — the whole Newport template. Open loops are read-only reflection;
Tasks page owns transactions. Copy is additive throughout: "you
finished X today", never "you didn't finish Y".
Autostart: tauri-plugin-autostart registered (LaunchAgent on macOS,
.desktop on Linux, registry Run on Windows). No bespoke Rust commands
— frontend calls the plugin's invoke-handlers directly. Toggle in
Settings is one-way (click → OS call → state update) to avoid the UI
lying during the round-trip. First-run presents a forced-choice prompt
for all three options, with "skip all" escape hatches per step.
Copy audit against RSD literature: no "overdue", "failed", or
day-to-day comparison framing anywhere in ritual surfaces.
Post-v0.1 ideas captured in the roadmap: calendar integration
(read-only ICS as interim, cloud sync parked) and right-click-to-task
(in-app simple, system-wide a separate phase).
Platform-dispatched TTS (spd-say + espeak-ng fallback on Linux, say on
macOS, PowerShell System.Speech on Windows) with a shared SpeakerButton
component. Tap to speak, tap again to stop; only one button speaks at
a time so two surfaces don't talk over each other. Text always travels
via argv (or a PowerShell here-string delivered through -EncodedCommand
on Windows) so user content never enters a shell string.
Mount points: DictationPage transcript footer, transcript viewer header,
per-step in MicroSteps. Settings gains a "Read aloud" accordion with
voice picker (lazy-loaded from the OS synth), rate slider 0.5-2.0x,
and a British-English test utterance.
Rust tests cover rate mapping, NaN handling, and Windows here-string
terminator safety. No pause/resume, no SSML, no cloud voices — that
stays out of scope per the Layer-1 roadmap.
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.
Independent review surfaced three majors and one nit. All actioned.
MAJOR 1 — profile scoping:
`decompose_and_store` and `extract_tasks_from_transcript_cmd` now
accept an optional `profile_id` (wired from `profilesStore.activeProfileId`
in MicroSteps.svelte and DictationPage.svelte), and thread it into the
feedback-retrieval query so per-profile decomposition styles do not
leak into each other. `record_feedback` gets the same treatment.
MAJOR 2 — prompt-budget regression on long inputs:
New `trim_to_budget` helper + `FEW_SHOT_CHAR_BUDGET = 2000` char cap
in `src-tauri/src/commands/tasks.rs`. Retrieval still pulls up to 5
rows but they are char-counted and truncated against the budget
before being sent to the LLM. Char cost matches the `Input: ...\n
Good output: ...` render path so the budget maps cleanly to ~570
Qwen3 tokens, well inside the 8192-context reserve after the 512-
or 768-token response allocation. Oldest-first drop order (iteration
stops at cost exceeded) preserves the most recent correction which
is the one carrying the user's live preference.
MAJOR 3 — inline edit stale-rollback race:
`saveEdit` in MicroSteps.svelte now stamps a monotonic per-step
`saveToken`. Each edit bumps the token; on failure the rollback
only fires if `saveToken[step.id] === myToken`, so a slow-failing
first save can no longer overwrite a faster successful second save.
NIT — retrieval ordering stability:
`list_feedback_examples` ORDER BY now `created_at DESC, id DESC`.
SQLite timestamp precision is one second; without the secondary
key, bursty feedback within the same second would select
non-deterministically.
Also: malformed `context_json` now warns via eprintln! rather than
disappearing silently — Codex minor.
All green: cargo build + 249 tests + clippy -D warnings + fmt
+ svelte-check (0/0) + npm run build.