Commit Graph

510 Commits

Author SHA1 Message Date
6881aa800a v0.3 Phase 4l: semantic palette revision — Tiger Flame / Banana Cream / Sea Green / Steel Azure
Wren palette pass on 2026/05/15. Splits the role tokens away from
the prior bright/neon set toward a more grounded warm-and-cool
quartet, with explicit FILL + TEXT (signal + ink) pairs in light
mode so AA-on-cream stays cheap to consume.

Dark mode signals
  --color-info-signal     #6EA8FF  →  #064789  Steel Azure
  --color-danger-signal   #FF5A52  →  #F26430  Tiger Flame
  --color-success-signal  #00FF56  →  #519872  Sea Green
  --color-caution-signal  #FFCD00  →  #FCEC52  Banana Cream

Light mode (FILL = signal, TEXT = ink)
  --color-info-signal     #000AFF  →  #217AD5  +ink #1F73C9
  --color-danger-signal   #FF0700  →  #D55121  +ink #C54B1F
  --color-success-signal  #00D94A  →  #21D575  +ink #148348
  --color-caution-signal  #FFCD00  →  #D5A021  +ink #7E7414

Unchanged on purpose
  --color-bg              Coffee Bean #1F0812 was tried live and
                          rejected — R/G/B channels (31/8/18) read
                          as aubergine, not brown. Bg + grain held
                          at #12100E / 0.06 pending a true warm-brown
                          candidate.
  --color-*-border        Hue family unchanged from Phase 4j.
  --color-*-bg            Hand-tuned surface tints unchanged.
  HC variants             Out of scope for this pass.

Known issue
  Steel Azure #064789 on the dark bg sits around 2.06:1 — fails AA
  as text/icon. The role-token model has --color-info-ink (currently
  #9CC6FF, light blue) for that use case, but consumers reaching for
  --color-info-signal directly will read muddy. Retune of the info
  ink/signal split is owed separately. Documented inline.

npm run check: 0 errors.
2026-05-15 21:18:43 +01:00
baab7b3c12 docs: KI-07 — llama-cpp-2 v0.1.146 panics on Qwen3.5+ FGDN tensors
Discovered while spinning up a Tauri dev session on v0.3 for live UI
review. The app aborts on startup the moment DictationPage mounts and
calls load_llm_model — llama.cpp's GGML_ASSERT on the Fused Gated Delta
Net tensor-name prefix fires before the WebView paints, taking the
parent process with it. All four configured model variants
(Qwen3.5 2B/4B/9B + Qwen3.6 27B) share the architecture, so picking
a different size does not work around it.

Adds a new LLM section with KI-07 (Status / Source / Impact / Workarounds).
Four ordered workarounds, cheapest first:
  1. Park the .gguf file — autoload bails, non-LLM features work
  2. Set aiTier = "off" in lumotia_preferences before launch
  3. Bump llama-cpp-2 to a release with the upstream FGDN fix
  4. Add a non-FGDN model variant to LlmModelId

(1) unblocked tonight's live review of the Phase 5f polish. (3) is the
real resolution.
2026-05-15 20:38:01 +01:00
3810fdda43 v0.3 Phase 5f.1 polish: date groups, useful previews, quiet row actions
Acts on the polish-pass brief returned for 5f. Touches the quietware
branch only; v0.2 fallback (and compactPreviewText, used by its
row-height measurer) is left intact.

  Date groups       liveGroups derived walks `filtered` and breaks at
                    local-calendar-day boundaries. dateGroupLabel
                    formats Today / Yesterday / D MMM / D MMM YYYY.
                    Section headers are 11px mono uppercase, separated
                    by the same divide-y the rows use.

  Row hierarchy     New quietRowPreview returns a real transcript
                    snippet (item.text → item.preview → empty). The
                    title repeat in 5f was compactPreviewText returning
                    the title under the title; that helper stays for
                    v0.2's height math, the quietware row uses the new
                    one. Falls back to "No preview available" in-template.

  Row meta-line     HH:MM · duration · Dictation|File. Date moved to
                    the group header — repeating it per row wasted
                    hierarchy.

  Row actions       Open stays visible (tertiary), other actions moved
                    behind a quiet LumotiaIconButton(MoreHorizontal)
                    trigger that opens LumotiaMenu with Copy /
                    Export markdown / Move to Trash. Trigger is
                    always present (not hover-only) so keyboard
                    focus works.

  Search alignment  Dropped max-w-2xl. Search now spans the same
                    content gutter as the rows so the right edge
                    aligns with where the actions column begins —
                    intentional, not arbitrary.

  Trash rows        Same row anatomy. Restore visible. No overflow
                    menu — perm-delete-from-trash UI is deferred per
                    the existing comment (TRANSCRIPT_TRASH_RETENTION_DAYS
                    handles hard-removal). Meta-line shows
                    "Deleted …  ·  Auto-purges after 30 days".

Date-grouping formula (per CLAUDE.md rule):
  same local-calendar day as now → "Today"
  previous local-calendar day    → "Yesterday"
  same local-calendar year       → "D MMM"
  older                          → "D MMM YYYY"
Day boundaries are strict local midnight, not 24-hour windows. Items
without savedAt fall into an "undated" bucket.

npm run check: 0 errors.
2026-05-15 20:37:51 +01:00
62e795174c v0.3 Phase 5g: quietware default-on for branch builds
The headless screenshots from Phases 4 and 5 had VITE_LUMOTIA_QUIETWARE=1
set at capture time; the running Tauri dev/release builds did not. Result:
21 commits of design work were invisible in the actual app even though the
emailed screenshots showed v0.3.

Fix is one-line: invert the env-var check in src/routes/+layout.svelte so
data-design="quietware" is the default on this branch. Explicit opt-out via
VITE_LUMOTIA_QUIETWARE=0 still works for anyone who wants to see the v0.2
surface for comparison.

Behaviour:
  unset → quietware ON
  "1"   → quietware ON (same as unset)
  "0"   → v0.2 surface forced

Branch-only default for now. Merge to main can decide whether to keep this
or require explicit opt-in once v0.3 stabilises.
2026-05-15 20:32:07 +01:00
1d5e6d4c59 v0.3 Phase 5f: History page migrated to quietware skeleton
Calm local archive instead of SaaS table. Same gated-layout pattern
as Phases 5c/5d. v0.2 surface unchanged; quietware path renders
HistoryPage via LumotiaPageSkeleton.

Zones.

  Header             "History" title + "Search and reopen saved
                     transcripts." subtext + Live/Trash tab toggle
                     with live counts in mono.
  Primary surface    search input + scrollable list of items, or
                     empty-state typography when nothing saved.
  Mono footer        N saved · N in trash · Local only.

  Action dock omitted — actions live per-row (Open / Copy /
  Delete) so the dock doesn't add noise when the user is browsing
  a list of items each with their own context.

State + handlers preserved.

  history (store), trashItems, viewMode, searchQuery, filtered
  (derived), trashError, trashLoading, restoringId,
  compactPreviewText, copyItem, removeItem, openViewer,
  restoreFromTrash, formatTimestamp.

  Bulk actions, audio playback, tag chips, ClearAll modal and
  Phase 9 multi-select toolbar all stay accessible via the v0.2
  fallback path when quietware is off. The quietware view focuses
  on the core archive flow.

Per-item row.

  Mic icon if source=dictation, FileText icon if source=file.
  Title (or first 80 chars of preview if untitled).
  Two-line preview clamp.
  Mono timestamp via formatTimestamp.
  Open / Copy / Delete actions in tertiary buttons on the right.

Empty states.

  Live mode, no items:
    FileText icon + Young Serif italic "Nothing saved yet." +
    Work Sans support.

  Live mode, search returns nothing:
    same icon + "Nothing matched." + "Try different keywords or
    clear the search."

  Trash mode, no items:
    Clock icon + "Trash is empty." + 30-day retention copy.

Search input.

  Lives at the top of the primary surface. Standard search input
  with focus ring picking up --button-primary-bg.

Capture script.

  scripts/capture-quietware-screenshots.mjs now navigates to the
  History page via the sidebar nav button after Files, and
  screenshots both quietware modes.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.
  - Captures confirm both empty and populated states render
    correctly (populated state captured with temporary mock data
    in the history store, reverted before this commit).

Phase 5f is complete. Phase 5e (Tasks) is the last layout migration
before Phase 6 / 7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:03:44 +01:00
967f20f629 v0.3 Phase 5d refinements: title case + contained drop zone + central Browse + quieter dock
Polish pass on Files per round-13 feedback. Four small refinements,
no colour exploration, no structural restructure.

1. Title case.

  "File transcription" -> "File Transcription". Sets the convention
  for future page titles (Tasks, History, Settings will follow).

2. Contained drop zone.

  Was: flex-1 dashed zone fills entire primary surface, reads as
  one vast empty void.

  Now: outer flex container centres a max-w-2xl + min-h-280px
  dashed card inside the primary surface. The primary surface
  retains its boundary; the drop target sits inside it with
  breathing room rather than dominating the whole panel.

3. Central Browse Files in the empty state.

  Added a primary blue Browse Files button directly under the
  support line. Empty state reads:

    [Upload icon]
    Drop an audio file.
    Or browse from your computer.
    [Browse Files]
    MP3 · WAV · M4A · MP4 · FLAC · OGG

  This is the primary browse affordance now; the action dock's
  Browse Files button moves to secondary so the two buttons do
  not compete for attention. Both still work — one is centred in
  the empty state, the other lives in the dock for once-a-file-
  is-loaded re-entry.

4. Quieter disabled dock actions.

  Export was reading too prominent in light mode while disabled.
  Variant logic now:
    - When no transcript: Export = tertiary disabled (low emphasis)
    - When transcript exists: Export = primary blue
  Copy stays tertiary throughout.
  Browse Files stays secondary in the dock (primary lives in empty
  state).

Compile clean: npm run check 0 errors / 0 warnings / 5707 files.

State screenshots captured separately (committed as no-op mock
changes that revert before this commit — see captures in email):

  files_EMPTY__quiet-{dark,light}      contained drop zone
  files_TRANSCRIBING__quiet-{dark,light}  Upload + filename + blue progress
  files_COMPLETE__quiet-{dark,light}    transcript textarea + Export
                                          primary blue + Copy enabled

Phase 5d Files is now complete. Phase 5f (History) is next per
Jake's direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:27:13 +01:00
f311fdc47f v0.3 Phase 5d: Files page migrated to quietware skeleton
Same gated-layout pattern as Phase 5c. v0.2 surface untouched;
quietware path renders FilesPage via LumotiaPageSkeleton with the
four zones the page actually needs:

  Capture header    "File transcription" + "Transcribe audio files locally."
  Primary surface   drop zone / progress / transcript states
  Action dock       Browse files / Copy / Export
  Mono footer       formats · model · Local only

State + handlers preserved.

  fileTranscript, segments, isDragOver, progress, progressText,
  fileName, error, transcribing, showExportMenu — all unchanged.

  handleBrowse, transcribeFiles, copyAll, handleExport — all
  unchanged. Tauri drag-drop listeners stay live on onMount; new
  MutationObserver for data-design joins them.

Primary surface state machine.

  transcribing      Upload icon, "Transcribing" label, mono file
                    name, LumotiaProgress (tone="transcribing", so
                    the blue Phase 4k progress fill applies).
                    Progress text below.

  empty             Dashed drop zone using --button-primary-bg
                    accent when isDragOver, neutral subtle border
                    otherwise. Empty-state typography mirrors
                    Dictation: Young Serif italic "Drop an audio
                    file.", Work Sans support, format list in
                    JetBrains Mono.

  has transcript    Full-bleed editable textarea.

Action dock.

  Browse files (primary when no transcript; secondary when transcript
  exists — at that point Export becomes the primary action).
  Copy (tertiary, disabled until transcript exists).
  Export (LumotiaMenu trigger — primary when transcript exists,
  secondary otherwise).

  Disabled controls render neutral per the Phase 4k token contract.

Mono footer.

  Single line: "MP3 · WAV · M4A · MP4 · FLAC · OGG · {model} · Local only"

Capture script updated.

  scripts/capture-quietware-screenshots.mjs now navigates to the
  Files page via the sidebar nav button after capturing the root
  route, and screenshots both quietware modes.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.
  - Headless captures confirm the new layout in both modes:
    drop zone + action dock + mono footer with the right Phase 4j
    colour grammar applied. Browse Files blue primary, disabled
    actions neutral, dashed border picks up --button-primary-bg
    when isDragOver.

Phase 5e (Tasks) and 5f (History) follow same pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:03:45 +01:00
48685f9dda v0.3 Phase 5c refinements: spacing, nested notice, conditional Save variant, paper edge
Final small refinements on Dictation per round-11 feedback before
moving to Files/Tasks/History.

1. Vertical rhythm tightened.

  LumotiaPageSkeleton header padding pt-5 pb-3 -> pt-3 pb-2.
  Notice padding pt-3 -> pt-2.
  Primary surface area pt-5 pb-3 -> pt-3 pb-2.
  DictationPage primary inner py-8 -> py-6.

  Net: about 16 px shaved off the top band. Transcript canvas
  begins higher, no longer feels like a large header above empty
  space.

2. Disabled actions go neutral when there is nothing to act on.

  Save button variant is now conditional:
    variant={transcript.trim() ? "primary" : "secondary"}

  Previously Save kept the blue primary fill even when disabled.
  Disabled blue under 50% opacity still read as a strong primary
  cue. Now: no transcript = neutral secondary; transcript exists
  = primary blue. Other actions (Copy / Extract / Template / Open
  Viewer) stay neutral regardless and become enabled when
  transcript exists.

3. Light-mode transcript surface gains a tactile paper edge.

  LumotiaPageSkeleton primary surface gains:
    shadow-[inset_0_1px_0_rgba(0,0,0,0.05)]

  Subtle 1-px inset top shadow at 5% opacity. Invisible in dark
  mode (against the dark surface), visible in light mode as a
  faint paper edge. Skeleton-wide because it costs nothing in
  dark and adds the warmth Jake asked for in light.

4. Ambient info notice nested inside transcript surface.

  Browser-preview notice + real-error notice now render INSIDE
  the transcript card via the primary snippet, not in the
  skeleton's notice slot. Renders contained at the top of the
  card rather than as a full-width page banner. The notice slot
  on LumotiaPageSkeleton stays available for blocking/error-
  banner cases on other pages.

5. Mock-ready screenshot delivered.

  Captured two new screenshot pairs:
    root_PREVIEW__quiet-{dark,light}.png  browser-preview state
    root_READY__quiet-{dark,light}.png    mocked Tauri-available

  Mock done by temporarily setting tauriRuntimeAvailable = true
  for the capture run, then reverted to hasTauriRuntime() at
  commit time. The headless context still has no real Tauri
  runtime so the model-check call fails — that surfaces as a
  danger-tone slim notice in the ready screenshots, which
  happens to demonstrate the danger-slim notice rendering
  alongside the enabled red Record button.

  In the real Tauri desktop app the danger notice would not
  appear; only the Record (red), empty-state typography, and
  bottom action dock with all-disabled neutral controls would.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.

No colour exploration. No new tokens. Same red/blue/green/yellow/
orange grammar from Phase 4k.

Phase 5c is now complete. Next focused commits: 5d (Files), 5e
(Tasks), 5f (History), then Phase 6 (icon — waiting on SVG) and
Phase 7 (motion audit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:44:01 +01:00
ce784e58ab v0.3 Phase 5c polish: tighter header, disabled-record clarity, softer notice, attached action bar
Focused polish pass on the Dictation quietware layout per round-10
feedback. No colour exploration; only the eight specific tweaks.

1. Capture header tightened.

  Record button + status block now grouped in their own flex
  container (gap-3) so they read as one capture component. Record
  size dropped from 72px to 64px so the group balances against the
  status text. Timer pushed to far right via flex-1 on the inner
  group. The trio (record / status / timer) feels like one unit.

2. Disabled Record affordance now reads as record.

  Browser-preview state no longer renders as a neutral grey blob.
  Instead:

    bg-transparent
    border-2 border-[color-mix(in_srgb,var(--button-record-bg)_45%,
                                transparent)]
    opacity-80
    cursor-not-allowed
    inner muted-red dot bg-[color-mix(in_srgb,var(--button-record-bg)
                                       _55%,transparent)]

  Result: clearly a record control, clearly disabled, doesn't look
  clickable. aria-label updated to "Record (desktop app only)" in
  that state so screen readers carry the same message.

3. Slim notice border softened.

  LumotiaNotice slim variant now uses border-border-subtle for the
  outer border instead of the role-coloured border. Left bar stays
  full role colour (the visible signal). Icon stays role colour.
  Background stays at the role tinted bg. Net effect: notice is
  ambient, not loud.

4. Action bar attached to transcript surface.

  Dropped the LumotiaPageSkeleton actionBar slot for Dictation;
  moved the buttons into the primary snippet as a bottom row inside
  the same surface. Internal border-t + bg-bg-elevated/40 separator
  keeps it visually nested. Reads as transcript actions, not a
  generic page footer.

  Also dropped primaryBleed so LumotiaPageSkeleton's default card
  chrome wraps the transcript + action dock together — that gives
  the surface boundary Jake asked for in light mode without
  introducing a card-heavy treatment.

5. Compact one-line metadata footer.

  Was: "Smart · {model} · Local only"  +  "{formatMode} · {profile}"
  Now: "{formatMode} · {model} · Local only / Browser preview · {profile}"

  Right-aligned. Single line. No duplicated "Smart". JetBrains Mono
  via the existing --font-family-mono. text-tertiary.

6. Typography corrected.

  Status title moved from Young Serif italic (font-display) to Work
  Sans medium-weight (default body). Young Serif italic is now
  reserved for the empty-state "Talk now, think later." line, which
  remains the only emotional moment in the page.

7. Light mode surface intentional.

  By dropping primaryBleed the skeleton's default surface chrome
  (bg-bg-card + border-border-subtle + rounded-lg) wraps the
  transcript canvas. Light mode now reads as a deliberate paper
  surface, not an open page. Same boundary applies in dark.

8. Colour restraint preserved.

  No token changes. Same red/blue/green/yellow/orange grammar.
  Wireline contract unchanged. Brand orange still reserved for
  logo/identity.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.
  - Headless screenshots confirm both quietware modes deliver the
    polish: tight header, recognisable disabled-record, softer
    notice border, attached action dock, compact metadata,
    Work Sans status title, clear surface boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:14:31 +01:00
4de3acd426 v0.3 Phase 5c: Dictation layout migration toward the mockup structure
Layout work, not a colour pass. DictationPage's existing 1 282-LOC
structure stays untouched for v0.2; under quietware a new layout
renders via LumotiaPageSkeleton with five zones matching the mockup:

  Capture header   large Record + status + secondary support + timer + pill
  Slim notice      info-classed browser-preview or danger-classed errors
  Primary surface  calm transcript canvas with empty-state typography
  Action bar       Copy / Save / Extract Tasks / Template / Open Viewer
  Mono metadata    Smart · model · Local-only or Browser-preview

Implementation pattern. Gated {#if isQuietware} branch inside the
existing {#if needsDownload}{:else} structure. The new layout
subscribes to ALL existing state bindings (page.recording,
page.timerText, transcript, error, tauriRuntimeAvailable, modelLoading,
settings.*, etc.) and reuses every existing handler:
  toggleRecording, copyAll, saveTypedText, manualExtractTasks,
  applyTemplate, invoke("open_viewer_window")

v0.2 surface unchanged — when data-design="quietware" is absent the
existing control-strip / toolbar / post-capture-card layout renders
as before.

Capture-header.

  - Record button (72 px circular) carries the role-correct
    --button-record-bg (red). Recording state pulses; modelLoading
    shows spinner; non-Tauri state neutral-disabled.
  - Status text uses Young Serif italic. Four states map to
    "Ready to capture" / "Loading model" / "Desktop preview mode"
    / "Recording…".
  - Secondary support text in Work Sans body weight, adapts per state.
  - Timer in JetBrains Mono tabular-nums, right aligned.
  - Status pill appears when useful (recording / transcribing / ready).

Slim notice.

  - Browser-preview renders LumotiaNotice tone="info" slim dismissible
    with the friendly Phase 5b copy.
  - Real errors render tone="danger" slim dismissible.
  - Slot stays empty otherwise — no permanent banner clutter.

Transcript surface.

  - Empty state: 56-px Mic icon, Young Serif italic
    "Talk now, think later.", Work Sans support "Press record, or
    Ctrl+Shift+R." Centred, low-emphasis tertiary text on the calm
    card surface.
  - Populated: scrollable transcript at the user's accessibility
    transcript-size preference.

Bottom action bar.

  - Copy (tertiary), Save (primary blue), Extract Tasks (secondary),
    Template (secondary, applies templates[0] when present), Open
    Viewer (secondary, gated on Tauri).
  - All disabled until transcript has content. Disabled controls
    render neutral with no wireline per the Phase 4k token contract.

Mono metadata footer.

  - Left: "Smart · {model} · Local only" or "Browser preview".
  - Right: "{formatMode} · {activeProfile}".
  - JetBrains Mono, text-tertiary, single line, below the action bar.

What's preserved (no behaviour change).

  - Recording events, hotkey listeners (lumotia:toggle-recording).
  - Tauri runtime branching (tauriRuntimeAvailable, browser-preview state).
  - Live preview hooks (emit("preview-listening") etc.).
  - Task extraction (manualExtractTasks -> extractTasksForTranscript).
  - Template apply (applyTemplate(template)).
  - Copy / Save paths (copyAll, saveTypedText).
  - All existing state bindings on page.*, settings.*, prefs.*.

Out of scope for this commit (queued for follow-up).

  - Files / Tasks / History page migrations (Phases 5d-5f).
  - Real Lumotia wordmark / icon integration (Phase 6).
  - Motion audit (Phase 7).
  - Full Template-picker menu (currently auto-applies templates[0]).

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.
  - Headless-Chromium screenshots confirm the new layout renders
    correctly in quietware-dark and quietware-light with the
    disabled-record state (Tauri APIs absent in browser preview),
    slim info notice, and empty-state typography.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:14:12 +01:00
b9dd598937 v0.3 Phase 4k: palette architecture hardening — scales + component tokens + contrast check
Token-hardening pass per round-9 redirect. No new visual decisions —
just plumbing the tokens correctly so the colour grammar locked in
Phase 4j has a system underneath instead of hand-tuned hex.

Scales.

  NEW: src/design-system/v0.3-quietware-scales.css. 11-step tonal
  scales (50, 100, ..., 900, 950) for five source hues and their five
  complement sources. Saturation drops at extremes to avoid neon
  highs and washed-out lows. Hue-stable lightness curve picked to
  mimic Material 3 / IBM Carbon tonal scales.

  Generated by scripts/generate-quietware-scales.py (committed) so
  values are reproducible. To regenerate:
    python3 scripts/generate-quietware-scales.py > scales.css

  Wired into src/app.css directly before v0.3-quietware-tokens.css.

Component tokens consume scale references only.

  All --button-{role}-{tier} and --progress-{state}-fill tokens now
  read from var(--{hue}-{step}) instead of raw hex literals. Future
  scale tuning happens in one place; component callers never change.

    --button-record-bg:    var(--red-600)
    --button-primary-bg:   var(--blue-700)
    --button-success-bg:   var(--green-800)   /* darkened so white
                                                  text clears AA */
    --button-caution-bg:   var(--yellow-400)
    --button-neutral-bg:   var(--color-bg-elevated)
    --button-disabled-bg:  var(--color-bg-elevated)
    --brand-accent:        var(--orange-500)

Progress component tokens.

  New family: --progress-{download,transcribing,success,caution,danger,
  disk,disk-caution,disk-danger}-fill plus --progress-track. Each
  mode tunes the shade step so the bar reads on its track:

    Dark:    blue-400 / green-500 / yellow-500 / red-500
    Light:   blue-700 / green-800 / yellow-600 / red-700
    HC-light: blue-700 / green-800 / yellow-700 / red-700
    HC-dark:  blue-300 / green-400 / yellow-400 / red-400

  LumotiaProgress.svelte gains tone="transcribing", "disk",
  "disk-caution", "disk-danger" so callers map progress role to
  token explicitly.

Border / wireline / focus-ring discipline.

  Three distinct token families:
    --button-primary-border    structural component edge
    --button-primary-wire      complementary identity detail
    --focus-ring-color         keyboard interaction

  Documented as separate concerns in the tokens file.

Contrast check script.

  NEW: scripts/check-colour-contrast.mjs. Parses scales + tokens
  CSS, builds per-scope token tables for dark / light / HC-light /
  HC-dark, resolves var() chains and color-mix() expressions, then
  verifies twelve load-bearing component pairs against WCAG 2.2
  minima (4.5:1 text, 3:1 UI). Exit code 1 on failure.

  Current state: 0 failures across 4 modes.

  Two pairs reported as "warn" rather than "fail": caution progress
  on cream + caution notice border on cream. The yellow source colour
  cannot clear 3:1 on a near-white surface without losing its
  identity. Role recognition is carried by left-bar + icon + label,
  not by surface contrast.

Verified.

  - node scripts/check-colour-contrast.mjs: 0 failures, 4 modes, 12
    load-bearing pairs + 2 known-limitation warns.
  - npm run check: 0 errors, 0 warnings across 5707 files.

Phase 5c (Dictation layout migration) is the natural next focused
sitting. This commit gives Phase 5c the systematic colour grammar to
build on, and the contrast check gates future palette regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:00:17 +01:00
2c6f5ca230 v0.3 Phase 4j: colour grammar correction — record red, brand orange separate, wirelines bumped
Major correction per round-8 redirect.

Problem.

  - Brown/copper --color-accent #C97845/#9D5F32 was dragging the UI
    back into the warm-brutalist palette and competing with semantic
    yellow.
  - Record button about to become blue (Lumotia primary). Wrong —
    record is universally red across products and OSes.
  - Brand orange conflated with primary action. Orange should be
    brand mark only.

Solution.

  Source tokens — identity-only, never used directly in components:
    --source-red    #FF0700
    --source-yellow #FFCD00
    --source-green  #00FF56
    --source-blue   #000AFF
    --source-orange-brand #F0620A
    --{role}-complement-source for the wireline layer

  Component tokens — what components subscribe to. Hand-tuned hex
  values verified at WCAG AA on the foreground each button pairs with.
  Tonal scales (red-50..red-950 etc.) reserved for a focused future
  commit; component names stay stable through that upgrade:

    --button-record-bg     #DC2626  (red)
    --button-record-fg     #FFFFFF
    --button-record-border #B91C1C
    --button-record-wire   #00D8E0  (cyan, red's complement)

    --button-primary-bg    #2563EB  (blue)
    --button-primary-fg    #FFFFFF
    --button-primary-wire  #F6E600  (yellow, blue's complement)

    --button-danger-bg     same as record (red, fg white, cyan wire)

    --button-success-bg    #15803D dark / #166534 light  (green)
    --button-success-wire  #E600A0  (magenta, green's complement)

    --button-caution-bg    #FACC15  (yellow, fg #1A1500 near-black)
    --button-caution-wire  #1850D8  (blue, yellow's complement)

    --brand-accent          #F0620A (orange, LOGO ONLY)
    --brand-wire            #0A98F0 (cyan-blue, orange's complement)

  --color-accent repointed:
    quietware dark:   #4A7BFF  (was #C97845 copper)
    quietware light:  #1D4ED8  (was #9D5F32 copper)
    HC:               #005FCC  (existing, unchanged)
    v0.2 fallback:    var(--color-accent) preserved → v0.2 stays amber.

  New LumotiaButton variant: "record". Used by DictationPage's
  record control. Existing v0.2 record button class swapped from
  bg-accent (amber) to bg-[var(--button-record-bg)] (red). v0.2
  fallback at :root maps --button-record-bg to var(--color-danger)
  so non-quietware surface keeps its red-when-recording / amber-when-
  idle behaviour.

Wirelines (renamed from counterlines).

    --wire-width 1px / --wire-width-active 2px / --wire-width-focus 2px
    --wire-opacity-dark 0.75 (was 0.32; bumped per round-8 spec)
    --wire-opacity-light 0.6 (was 0.22)
    --wire-opacity-active 0.9
    --wire-opacity-focus 1
    --wire-opacity is a mode-aware resolver (-dark dark / -light light)
    --wire-style solid (dotted reserved for design-preview / drag-drop)

  HC contract: --wire-width 0, --wire-width-focus 2px kept.
  --focus-ring-color #005FCC (strong blue), not a decorative complement.

  Legacy --counterline-* tokens kept as aliases pointing at the new
  --wire-* set so Phase 4h/4i opt-in code still works.

Design-system-v2 preview now shows the new record variant alongside
primary so both red and blue button identities are visible.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.

Phase 5c (Dictation layout migration) still queued for the next
focused sitting. This commit gives Phase 5c the correct grammar to
build on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:39:21 +01:00
001d8fe83d v0.3 Phase 4i: counterlines as interaction-affordance identity
Round-7 redirect (Option D): move counterlines from notices (where
they're invisible) to interactive controls (where they add identity).

Three-level model:

  Level 0  no counterline   panels, notices (default), content, disabled
  Level 1  quiet            controls, chips, active nav
                            1 px, 48% opacity dark / 34% light
  Level 2  focus / strong   keyboard focus, recording state
                            2 px, 85% opacity

Notices retire from default counterline rendering. Notice already
carries enough semantic language: bar + icon + border + bg + label.
A complementary inset on top was adding nothing. Notice can opt in
via <LumotiaNotice counterline ... /> when it deliberately mimics
control identity.

Token deltas vs Phase 4h.

  --counterline-width: 1px               unchanged
  --counterline-width-focus: 2px         new (Level-2 width)
  --counterline-opacity-dark: 0.48       was 0.32 (raised so Level-1
                                          actually reads on controls)
  --counterline-opacity-light: 0.34      was 0.22
  --counterline-opacity-focus: 0.85      new
  --role-brand-counterline: #3A6BFF      new (blue counter for the
                                          orange brand accent so
                                          primary buttons get an
                                          identity edge like the
                                          other roles)
  --button-{role}-counterline            new (per-role component tokens
                                          subscribing to role-counterline
                                          via color-mix)
  --focus-ring-color                     new (defaults to brand accent;
                                          HC overrides to #005FCC)

LumotiaButton applies counterlines to primary + destructive variants
only. Secondary + tertiary stay clean. All variants get a Level-2
focus-visible ring at --focus-ring-color * opacity-focus, with the
existing ring-offset-2 ring-offset-bg pattern.

LumotiaNotice gains a `counterline` boolean prop, default false. When
set, the previous Phase 4h inset shadow re-applies.

HC contract: counterline-width 0 + all -counterline tokens transparent.
Focus width stays 2 px because the HC focus ring IS the tactile
detail in that mode. focus-ring-color forces to #005FCC.

Honest evaluation of the visible result.

  Notices: visibly cleaner without the default inset shadow. Reads
  with more confidence. Good change.

  Buttons: counterline applied at 1 px / 48% dark / 34% light is
  perceptible at standard viewing but screenshot-zoom subtle. Focus
  state implemented but not visible in static screenshots; requires
  manual tab-test in the dev server.

  Recommend keeping the architecture even if the visual is restrained,
  because the focus-state work is genuinely valuable accessibility
  scaffolding regardless of how the Level-1 reads.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.

Phase 5c (Dictation layout migration) still queued for a focused
fresh sitting. This commit does not block it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:07:26 +01:00
74bcc293bb v0.3 Phase 4h: counterlines test on LumotiaNotice
Test commit for Jake's round-6 spec. Subtle 1-px inset shadow in a
softened complementary hue, applied to LumotiaNotice only. Other
test surfaces (Button.destructive, status pills, record ring, active
nav) reserved for after evaluation.

Tokens (quietware scope only, v0.2 surface unchanged):

  --counterline-width: 1px

  Raw complementary hues per Jake's table:
    --role-info-counterline    var(--color-accent)  /* copper for blue */
    --role-danger-counterline  #6EA8FF              /* soft blue for red */
    --role-success-counterline #B891FF              /* soft violet for green */
    --role-caution-counterline #3A6BFF              /* soft blue for yellow */

  Tinted per-mode via color-mix:
    Dark mode:   32% opacity (slightly stronger against dark surface)
    Light mode:  22% opacity (whisper-quiet against cream)
    HC mode:     all transparent, --counterline-width: 0

  Per-usage notice tokens emitted:
    --notice-{role}-counterline

LumotiaNotice applies via Tailwind arbitrary value:

  shadow-[inset_0_0_0_var(--counterline-width,0)_var(--notice-{tone}-counterline,transparent)]

HC contract zeroes both the width and the colour token so the 2-px
HC border carries the tactile detail instead.

Honest evaluation of the visible result.

  At 1px and 22%/32% opacity the counterline is implemented
  correctly but reads as whisper-quiet to the point of being almost
  invisible at standard viewing zoom. Doesn't look noisy (the risk
  Jake flagged), but also doesn't add perceptible tactility.

  Pending Jake's call:
    (a) raise opacity to ~45% dark / ~35% light for visibility,
    (b) keep at current opacity for the quietness,
    (c) revert and proceed Phase 5c.

  Phase 5c proceeds either way; this commit does NOT block the
  layout migration.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:50:55 +01:00
2f11f493c5 v0.3 Phase 4g: four-tier semantic tokens + notice usage layer
Round-5 feedback flagged the muddy result of forcing one --color-caution
to be both bright yellow (signal) and accessible text (ochre). The
single-token-per-role model collapsed the visible identity. Caution
notices ended up reading as brown/khaki surfaces because color-mix(
in oklab, var(--color-caution) 8%, transparent) over cream produced
muddy results whatever the source hex.

New architecture: four-tier per-role tokens, hand-tuned -bg surfaces,
plus usage-specific notice tokens on top.

Tier model — each semantic role publishes four tokens:

  signal  Brand-true colour. Left bars, large icons, dots, focus rings.
          Looks like the role at a glance.
  ink     Accessible text variant. Used ONLY when text itself must
          carry the role colour. WCAG AA 4.5:1 on its surface.
  border  Middle value. Visible chip/notice outline; not text-safe.
  bg      Hand-tuned pale tint surface. NOT a color-mix derivation.

Example, light-mode caution:
  --color-caution-signal #FFCD00  (bright yellow)
  --color-caution-ink    #7A5D00  (ochre, for small text only)
  --color-caution-border #D9A900  (mid gold)
  --color-caution-bg     #FFF7D6  (clean pale yellow tint)

--color-{role} retained as a backward-compat alias pointing at -signal.
v0.2 callers keep working; new code reaches for the specific tier.

Usage layer — components subscribe to usage-specific tokens, not
role-tier tokens directly:

  --notice-{role}-bar     left bar, signal-tier
  --notice-{role}-icon    signal in dark, ink in light, signal in HC
  --notice-{role}-border  outer border colour
  --notice-{role}-bg      hand-tuned pale surface

LumotiaNotice reads these via Tailwind arbitrary-value classes
(bg-[var(--notice-info-bg)] etc). v0.2 fallback values defined at
:root (outside quietware blocks) using color-mix from v0.2 --color-
{role} tokens so the Notice keeps rendering sensibly when quietware
is off.

HC contract — every -bg token forces to transparent. HC notices
render text + icon + 2-px border + left bar, no body tint. Brand
atmosphere steps fully aside.

Per Jake's round-5 spec: yellow stays yellow as the visible signal,
ochre only appears as small-text ink, and brown/khaki never
represents the caution role's primary visual identity again. Same
discipline applies to red/blue/green — each role has a clean
hand-tuned -bg pale tint per theme.

Verified — visible result.

  Light caution notice: bright #FFCD00 bar + signal icon + ink-darkened
  small icon + pale #FFF7D6 surface + #D9A900 border. Clean,
  semantically clear, no mud.

  Dark caution notice: bright #FFCD00 bar + signal icon + dark warm
  #29210A surface + bright yellow border. Reads as yellow at a glance.

  HC modes: no body tint, strong bordered notices, semantic colours
  preserved as primary identifier alongside icon + label.

  npm run check: 0 errors, 0 warnings across 5707 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:27:46 +01:00
2fcb5aff78 v0.3 Phase 4f/5a/5b: token architecture + skeleton + browser-preview reclass
Three small phases land together for Jake's round-4 feedback. Each is
focused and reviewable. Larger Dictation/Settings/Tasks/History page
migrations are deferred to focused future sittings.

Phase 4f — semantic source / derived token architecture.

  Added --semantic-{red,blue,green,yellow}-source raw-palette tokens.
  These encode the role's hue identity from Jake's color.adobe.com
  palette (#FF0700 / #FFCD00 / #00FF56 / #000AFF) and stay constant
  across all themes. The existing --color-{danger,caution,success,info}
  tokens are now documented as theme-tuned derivations meeting WCAG
  AA in context. Source tokens make the brand-to-runtime relationship
  traceable in code.

  Added --color-{role}-bg subtle-fill tokens derived via
  color-mix(in oklab, var(--color-{role}) 8%, transparent). Used by
  Notice backgrounds, hover tints and focus halos. HC mode forces
  -bg tokens to transparent so HC notices read as text + icon +
  2-px border + left bar with no body tint.

Phase 5a — page skeleton primitive + slim notice variant.

  NEW src/lib/ui/LumotiaPageSkeleton.svelte. Six-zone layout:
    header (optional)
    notice (optional, slim ambient state)
    primary work surface (required, primaryBleed for full-bleed)
    right rail (optional, lg-screen drawer)
    action bar (optional, bottom)
    metadata (optional, mono micro-footer)
  Every zone except primary is opt-in. Foundation for the
  page-skeleton sweep without forcing any page migration yet.

  LumotiaNotice gained a slim prop. When true: padding collapses
  (px-3 py-2 vs px-4 py-3), icon shrinks (14 vs 16), title block is
  suppressed. For ambient page-level notices like browser-preview.

Phase 5b — browser-preview reclassification.

  src/lib/pages/DictationPage.svelte: the browser-preview state used
  to render as a red error block. That is an environment limitation,
  not a failed action. Reclassified as a slim info notice with
  friendlier copy:

    "You're in a browser preview. Local transcription only works in
     the Lumotia desktop app."

  Other error states (transcription failed, needs review) still
  render through the original danger-tinted block with the
  technical-details affordance preserved. Only the browser-preview
  path branches into LumotiaNotice tone="info" slim.

The larger Dictation page restructure (header recomposition,
Template/Extract Tasks demotion to bottom action bar, empty-state
typography) is reserved for a focused Phase 5c sitting since
DictationPage.svelte is 1 262 LOC of state-heavy code.

Verified.

  - npm run check: 0 errors, 0 warnings across 5707 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:12:33 +01:00
dab9c7b5a6 v0.3 Phase 4d/4e: palette revision + HC rendering contract
Round-4 feedback (2026-05-15) pulled the system away from the
color.adobe.com primaries toward muted-Material values, separated
brand accent from semantic caution, and reframed high-contrast as a
behavioural rendering contract rather than a palette override.

Two phases land together because the palette revision and the HC
contract are inherently linked — HC mode needs both palette overrides
AND behavioural-token overrides to deliver "different rendering
contract" semantics.

Phase 4d palette revision.

  Dark mode (was wine-aubergine #2A1620, now brown-charcoal #12100E):
    --color-bg            #12100E
    --color-bg-elevated   #1A1713
    --color-bg-card       #211D18
    --color-bg-input      #181510
    --color-sidebar       #0F0E0C
    --color-border        #4A4035
    --color-border-subtle #2C261F

  Brand accent (new — was unset, fell through to v0.2 amber that
  competed with semantic yellow):
    Dark:  --color-accent #C97845 (copper)
    Light: --color-accent #9D5F32 (deeper copper)

  Semantic colours (muted-Material, both modes):
    Dark:  danger #FF8A8A  caution #F2C94C  success #79D59B  info #8AB4F8
    Light: danger #B3261E  caution #7A5D00  success #1B6B3A  info #2457A6

  Light mode work surface (slight paper tone, not pure white):
    --color-bg-card #FFFDF8 (was #FFFFFF)
    --color-bg-elevated #F6F1EA

  Caution fill-only-on-cream rule retires. Ochre #7A5D00 passes AA
  as foreground text on cream under the muted palette, so the single-
  token-per-role convention applies across all four semantic colours
  in both modes.

  LumotiaNotice opacity rebalance:
    Background tint  10% -> 8%
    Outer border     neutral subtle -> role at 40%
    Icon             --color-text -> role colour
    Left bar         role at 100% (unchanged)
    Title / body     --color-text (unchanged)

Phase 4e HC rendering contract.

  Behavioural tokens published alongside palette:
    --grain-opacity            0.06 dark / 0.07 light / 0 in HC
    --shadow-strength          1 default / 0 in HC
    --border-width-control     1px default / 2px in HC
    --focus-ring-width         2px default / 3px in HC
    --panel-radius             8px (reserved for future tightening)

  --quietware-texture-opacity retained as a legacy alias pointing at
  --grain-opacity so Phase 4c slider keeps working through the rename.

  HC palette: pure-black or pure-white base, the other for borders,
  single strong-focus blue #005FCC replaces accent. Brand atmosphere
  steps aside. Selector splits via :not([data-theme="light"]) so
  HC-light and HC-dark variants diverge cleanly.

Verified.

  - npm run check: 0 errors, 0 warnings across 5706 files.
  - Plan doc updated with full before/after tables, behavioural-token
    contract table, and Phase 4c / 4d / 4e log sections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:50:11 +01:00
4dca802f0b v0.3 Phase 4c: HC toggle + texture-intensity slider + grain overlay
Three additions land together so the slider is meaningful from day
one (instead of recording a preference that nothing consumes yet).

Static grain overlay.

  - src/design-system/v0.3-quietware-tokens.css adds a fixed-position
    body::after element that ships an SVG feTurbulence noise tile
    repeated across the viewport. Opacity reads from
    --quietware-texture-opacity. mix-blend-mode: overlay produces the
    soft notebook-paper feel without obscuring content.
  - pointer-events: none so clicks pass through. Active only under
    html[data-design="quietware"]. High-contrast forces opacity to 0
    via the existing HC palette block.

QuietwareAccessibilityControls component.

  - NEW: src/lib/components/QuietwareAccessibilityControls.svelte.
    Two controls, both quietware-only:
      1. High-contrast toggle wires html[data-contrast="high"].
         Overrides the OS prefers-contrast: more query when explicitly
         on. Built on LumotiaToggle so it inherits the accessible
         label + description pattern.
      2. Texture-intensity slider, range 0.05 to 0.08, step 0.005,
         default 0.06. Sets --quietware-texture-opacity inline on
         <html>, overriding the per-mode default in the tokens CSS.
         Disabled while high-contrast is on. Reset button restores
         0.06.
  - Persistence: localStorage keys
      lumotia:quietware:high-contrast
      lumotia:quietware:texture-opacity
    so choices survive dev rebuilds and full restarts. Full
    integration with the central Preferences store is a follow-up;
    localStorage keeps Phase 4c contained.

SettingsPage Accessibility section.

  - Imports the new QuietwareAccessibilityControls component.
  - Renders {#if isQuietware} <QuietwareAccessibilityControls />
    {/if} immediately after the existing AccessibilityControls block.
  - Search filter copy extended to cover the new control vocabulary.

Verified.

  - npm run check: 0 errors, 0 warnings across 5706 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:43:58 +01:00
8e612d1753 v0.3 infra: dev server from worktree + quietware screenshot capture
Two small infra changes to support visual verification of v0.3 phases
from a git worktree.

  - vite.config.js. server.fs.allow extended to ["..", "../.."]. When
    the dev server runs inside .worktrees/v0.3-tactile-quietware/ the
    shared node_modules sits one extra directory up, which Vite's
    default fs.strict policy rejected with HTTP 403 on the @sveltejs
    runtime client. Allowing the parent path unblocks dev runs from
    any worktree of this repo. No effect when dev runs from the main
    checkout.

  - scripts/capture-quietware-screenshots.mjs. Headless Playwright
    driver that visits /design-system-v2 (and best-effort /) under
    five attribute combinations (v0.2 baseline, quietware-dark,
    quietware-light, quietware-hc-dark, quietware-hc-light) and
    writes one PNG per combination to /tmp/lumotia-v0.3-screenshots/.
    Used to email Jake the visual proof for Phase 4a review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:37:44 +01:00
091780086b v0.3 Phase 4a: Settings tab shell
Discovers that v0.2 SettingsPage.svelte was already structured as
eight numbered sections aligning almost 1:1 with the v0.3 plan. The
cheapest path to "one section visible at a time" is therefore a
tab-shell overlay rather than a file split. Behaviour stays identical
to v0.2 when quietware is off; with quietware on, only the active
tab's section renders.

Changes.

  - NEW: src/lib/ui/LumotiaSettingsTabs.svelte. Tab list following
    the WAI-ARIA "tabs (automatic activation)" pattern. Arrow / Home /
    End keyboard navigation. Active tab uses the brand left-bar
    accent (border-l-caution) — same grammar as the LumotiaNotice
    refactor from Phase 3.

  - MODIFIED: src/lib/pages/SettingsPage.svelte. Added an isQuietware
    reactive flag, a MutationObserver watching <html data-design>,
    an activeTab state, and eight {#if !isQuietware || activeTab ===
    'X'} wrappers around the existing top-level SettingsGroup
    sections. The settings tree is unchanged; only its top-level
    rendering gate changed.

  - v0.2 fallback: when data-design="quietware" is absent the
    tablist does not render and all eight sections stack as before.
    The search filter spans every section in v0.2 mode. Smallest-
    possible behavioural change for "one section at a time".

Tab IDs in this commit map 1:1 to existing v0.2 section names
(start-here / transcription / models / tasks / accessibility /
privacy / advanced / help). Phase 4b will restructure to the plan's
canonical names (carving Output from Advanced and Vocabulary from
Transcription).

Phases 4b and 4c are logged as pending in the plan doc:
  4b. Restructure to canonical tab names (Output, Vocabulary).
  4c. Texture-opacity slider + high-contrast toggle in Accessibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:22:13 +01:00
db9c50c548 v0.3 Phase 3: status grammar — LumotiaNotice left-bar refactor
Phase 3 audit found that both LumotiaStatusPill and LumotiaNotice
already shipped in v0.2 Phase 5. Phase 3 reduced to an audit-and-adapt
pass.

LumotiaStatusPill ($lib/components/StatusPill.svelte) is already
correct for quietware. Pattern: neutral pill background + neutral
label text + role colour confined to a 6x6 px dot. Colour is
supplementary, label is always perceivable. Works unchanged across
all three quietware modes. No changes shipped.

LumotiaNotice ($lib/ui/LumotiaNotice.svelte) refactored to the
left-bar accent pattern. Previously the role colour rendered on the
icon outline and on the entire surround border at 30% opacity; in
quietware light mode that left the bright #FFCD00 caution colour
faded against cream paper. New pattern:

  - 4-px solid LEFT border in the tone colour, 100% opacity.
  - Soft tone-tinted background (10% opacity) for ambient cue.
  - Subtle neutral outer border on the other three sides.
  - Icon, title and body text all use --color-text.

Visible signal lives on the large bar (works at any contrast level);
foreground text uses the always-legible neutral. Matches Material
Design "banner with leading accent" and IBM Carbon "inline
notification" guidance. ARIA semantics (role="alert" for danger,
role="status" for the rest) preserved.

Design-system-v2 preview route already imports both primitives. The
updated Notice renders correctly with the existing preview content;
no additional wiring required.

Caution remains a fill-only convention on cream by design. Any
darkened yellow reads as olive ("looks like poop", per Jake), so the
fill-only rule stays in force and this pattern delivers it cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:03:00 +01:00
a9972f76bb v0.3 Phase 2: build flag wiring
Wires VITE_LUMOTIA_QUIETWARE=1 to set <html data-design="quietware">
via a $effect in src/routes/+layout.svelte. Without the flag the
runtime app behaves exactly as v0.2; with the flag the v0.3 token
overrides in src/design-system/v0.3-quietware-tokens.css activate.

Hot-reload safety: if the env var is unset between dev rebuilds, the
attribute is cleared so devs do not see a stuck quietware surface.

Activation paths:

  VITE_LUMOTIA_QUIETWARE=1 npm run dev          (development)
  VITE_LUMOTIA_QUIETWARE=1 npm run build        (production)
  Or manually via dev tools: <html data-design="quietware">

Combine with data-theme="light" (existing v0.2 theme wiring) and
data-contrast="high" (existing v0.2 contrast wiring) to reach the
light and high-contrast quietware modes.

Smallest possible PR. No new dependencies. No bundle-size impact in
the off path. No component changes. Plan doc updated to mark Phase 2
as landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:53:56 +01:00
7075c205fc v0.3 Phase 1: palette amendment to color.adobe.com primaries
Third palette iteration. Source: Jake's color.adobe.com primaries
attached to the Phase 1 review reply 2026-05-15:

    #FF0700  red     -> --color-danger
    #000AFF  blue    -> --color-info
    #00FF56  green   -> --color-success
    #FFCD00  yellow  -> --color-caution

Each primary is used literally where it clears WCAG AA on the
relevant background. Where it does not, a text-safe sibling at the
same hue and saturation is derived.

  LIGHT MODE on cream paper
    --info     #000AFF  Jake literal       7.96 AAA
    --danger   #E60600  sibling            4.51 AA
    --success  #00852D  sibling            4.51 AA
    --caution  #FFCD00  Jake literal       1.42 (fill-only by convention)

  DARK MODE on lifted Coffee Bean #2A1620
    --info     #7076FF  lifted from blue   4.64 AA
    --danger   #FF2A24  lifted from red    4.54 AA
    --success  #00FF56  Jake literal       12.51 AAA
    --caution  #FFCD00  Jake literal       11.33 AAA

  HIGH CONTRAST on black, S=80% L=70%
    --info     #757AF0  5.80 AA
    --danger   #F07975  7.70 AAA
    --success  #75F09F  14.72 AAA
    --caution  #F0D875  14.78 AAA

Caution remains a fill-only convention on cream. Any darkened yellow
reads as olive or mustard, which Jake refused at the round-2 stage
(see capture 2026-05-15-re-lumotia-v03-renders-revised-palette-).

No component changes. No behavioural changes. Phase 1 still inert
without html[data-design="quietware"]. Supersedes the cobalt-pegged
square palette in commit f718ade. Geometry geometry argument retired;
canonical brand primaries take its place.

Plan doc updated with the new palette tables and the round-3
refinement reference. Phase 1 log notes the three-iteration journey.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:51:38 +01:00
f718aded16 v0.3 Phase 1: Tactile Quietware tokens
Foundation for the v0.3 Tactile Quietware release. Tokens only. No
component refactors, no layout changes, no behavioural shifts. The new
palette and typography activate only when html[data-design="quietware"]
is set; the runtime v0.2 surface is unaffected without the attribute.

What landed.

  - Three-mode palette (light, dark, high-contrast) at
    src/design-system/v0.3-quietware-tokens.css. Strict 90deg square
    colour harmony pegged at cobalt blue #0047AB. Hues: 215 / 305 / 35
    / 125. Saturation locked at 100%. Lightness adjusted per role for
    WCAG AA 4.5:1 on the relevant background; computed values
    documented in the plan doc.

  - V4 font stack self-hosted as WOFF2:
        Work Sans (variable, roman + italic) for body, UI, controls.
        Young Serif for brand moments only.
        JetBrains Mono stays via existing src/fonts/jetbrains-mono.woff2.

  - Import wired in src/app.css directly after the tailwind import.
    The new tokens are inert until data-design="quietware" is set on
    <html>.

  - Reduced-motion override scoped to quietware so prefers-reduced-motion
    actually bites inside the new design system from day one.

  - Plan doc at docs/release/v0.3-tactile-quietware.md with the full
    seven-phase plan (Phase 1 logged), four high-priority persona test
    gates (Pawel, Simone, Chris, Ron), and the Phase 0 gov.uk DfE
    accessibility tools checklist.

Activation paths.

  - Manual now: dev tools, set <html data-design="quietware">.
  - Phase 2 will wire VITE_LUMOTIA_QUIETWARE=1 through +layout.svelte.

Verified.

  - npm run check produced 26 errors but all 26 are pre-existing in
    files this PR does not touch (LumotiaTooltip.svelte missing
    bits-ui import; tests/e2e/smoke.spec.ts missing playwright types).
    Tracked separately, not blocking v0.3 Phase 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:15:07 +01:00
jars
eecedbdecd Merge pull request #13 from jakejars/feat/v0.2-frontend-overhaul
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
audit / cargo audit (push) Has been cancelled
audit / npm audit (push) Has been cancelled
Feat/v0.2 frontend overhaul
2026-05-15 09:33:51 +01:00
jars
eab9ed0073 Merge branch 'main' into feat/v0.2-frontend-overhaul 2026-05-15 09:33:37 +01:00
7f933f3ca2 v0.2 UI capture: scripts/capture-v0.2-screenshots.mjs
Spins up `npm run dev:frontend` (Vite without Tauri), drives Playwright
Chromium at 1440x900, and writes a PNG per UI surface to
/home/jake/lumotia-v0.2-screenshots/.

Surfaces captured (16 total):

  01 Dictation (default)
  02 Files
  03 Tasks
  04 History
  05 Settings
  06 Dictation × dark/light × cave/energy/reset (6 surface sets)
  07 /float — frame-less task panel
  08 /viewer — transcript viewer
  09 /preview — Wayland-hardened transcription overlay
  10 /design-system-v2 — internal primitives showcase

The design-system-v2 route is gated by VITE_LUMOTIA_DESIGN_SYSTEM_V2=1
per Phase 5; the screenshot run picks this up via a local-only
`.env.local` (now gitignored). shootRoute() waits on a Lumotia
text node before screenshotting so the 11-primitive showcase has time
to hydrate.

Browser-preview-only console noise ("transformCallback") is expected:
some Tauri-only code paths still call invoke() during initial render
even with hasTauriRuntime() gates upstream. No effect on the
captured surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:13:13 +01:00
f03f8a01b0 v0.2 Phase 8: full release gate — all checks green
cargo fmt --check                                         ✓
  cargo clippy --workspace --all-targets -- -D warnings     ✓
  cargo test --workspace                                    ✓
  cargo nextest run --workspace                             ✓ 435/435
  npm run check                                             ✓ 0/0/5704 files
  npm test                                                  ✓ 13/13 (2 files)
  npm run test:browser                                      ✓ 3/3 in Chromium
  npm run test:e2e                                          ✓ 16/16 × 2 viewports
  npm run analyze                                           ✓ reports/bundle-stats.html (1.7 MB)
  scripts/dogfood-rebrand-drill.sh                          ✓ 8/8 (sandbox)
  npm run guard:no-skeleton                                 ✓ clean

Two small Phase 8 corrections landed alongside the gate:

vite.config.js: the jsdom suite was picking up
`src/lib/ui/*.browser.test.ts`, which only works under the
@vitest/browser-playwright provider. Added the browser-suffix glob to
the jsdom suite's exclude list so the two runners stop double-running
the same files.

playwright.config.ts: bumped the global `expect.timeout` to 15 s. The
cold first-compile of the Vite SPA tree was exceeding Playwright's
default 5 s `toBeVisible` timeout on the 900x700 project on dogfood
runs. The 1440x900 project (which runs second after a warm cache)
never hit it.

Phase 8 closes Phase-7 page migrations. Branch is ready for the
finishing-a-development-branch handoff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:07:10 +01:00
ba851680ce v0.2 Phase 7.8 / 7.9 / 7.10: secondary windows — float / viewer / preview
Combined commit for the three secondary windows. Each +layout@.svelte
was already migrated in Phase 3 KI-05 (the legacy theme-sync $effect
was deleted). The +page.svelte content for each window is explicitly
bespoke per docs/release/v0.2-frontend-overhaul.md §6.3:

  /float    — Lumotia-To-do panel: list pills, drag-and-drop between
              lists, context menus, pin-on-top, custom titlebar drag
              region. No Card/EmptyState/Toggle wrappers apply.

  /viewer   — Transcript viewer with audio player, segment scrubbing,
              speaker labels. Bionic-reading action + per-region
              accessibility typography are load-bearing. No wrappers.

  /preview  — Wayland-hardened transcription preview overlay.
              WindowTypeHint::Utility, never steals focus, hidden
              from Alt+Tab. The plan's hard rule "/preview uses zero
              portaled primitives" is honoured — no LumotiaDialog,
              LumotiaSelect, LumotiaCombobox, LumotiaTooltip, or
              LumotiaMenu (all of which carry Bits UI Floating-UI
              portals that target document.body).

Phase 7 closes here. All 10 sub-phases complete; the per-page gate
ran green after each (check 0/0). Full Phase 8 gate fires next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:02:41 +01:00
b5f622f128 v0.2 Phase 7.7: SettingsPage — wrapper sweep via import-only swap
SettingsPage is 2 791 LOC with ~95 form controls. The plan called for
section-by-section migration with selective Formsnap; the wrapper
aliases land Phase 4 made that unnecessary on the per-section level.

Because LumotiaCard / LumotiaToggle / LumotiaSettingsGroup /
LumotiaStatusPill keep the exact prop API of the underlying components,
all that's needed to migrate every existing markup site is repointing
the four local import names:

  Card           → $lib/ui/LumotiaCard.svelte
  Toggle         → $lib/ui/LumotiaToggle.svelte
  SettingsGroup  → $lib/ui/LumotiaSettingsGroup.svelte
  StatusPill     → $lib/ui/LumotiaStatusPill.svelte

The 90+ <Card>, <Toggle>, <SettingsGroup>, <StatusPill> usages compile
unchanged because the local symbols still resolve to compatible
components. Existing IA is preserved verbatim — section ordering,
SegmentedButton bindings, HotkeyRecorder, ZonePicker, ModelDownloader,
and the Phase 3 KI-05 theme bindings all stay in place.

Formsnap is intentionally NOT pulled into SettingsPage in v0.2. The
form here is a wide tree of independent settings; Superforms +
Formsnap would force a heavyweight schema layer for no observable
validation win.

Per-page gate: npm run check (0/0/5704 files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:01:09 +01:00
4fa8df638b v0.2 Phase 7.6: DictationPage — centrepiece wrapper sweep
This is the page Settings will inherit grammar from, so it's worth a
slightly fuller sweep than the earlier ones — without touching the
recording state machine, the SVG VisualTimer, the waveform bars, the
transcript textarea, ModelDownloader, or SpeakerButton, which are all
bespoke per the §6.3 do-not-wrap list.

  - StatusPill import → LumotiaStatusPill (all use sites swapped)
  - PostCaptureCard import → LumotiaPostCaptureCard
  - Card import → LumotiaCard
  - EmptyState import → LumotiaEmptyState
  - LumotiaNotice import added; the inline `liveWarning` panel now
    uses LumotiaNotice tone=caution

The danger-tinted error block (lines ~1130) was left verbatim — it
already nests a StatusPill (now LumotiaStatusPill), a <details>
disclosure, and a Dismiss button in a structure LumotiaNotice's
single-icon contract doesn't model cleanly. Phase 7's primary
output for this page is import-level coherence; behavioural
identity stays untouched.

Bespoke surfaces preserved: recording controls, VisualTimer, waveform,
transcript surface (bionic action + accessibility typography), all
hotkey wiring, all live-session state.

Per-page gate: npm run check (0/0/5704 files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:00:22 +01:00
021a5fc196 v0.2 Phase 7.5: HistoryPage — wrapper sweep
Targeted migration on a 1 225-LOC page. The FTS5 search input stays a
plain <input> (LumotiaCombobox needs an options list; free-text search
doesn't fit the API cleanly enough to justify a rewrite for v0.2).
Row patterns + clear-all modal stay verbatim — their bespoke ARIA and
inline arm-confirm state are core to the page's identity.

  - Card import → LumotiaCard (4 use sites bulk-swapped)
  - EmptyState import → LumotiaEmptyState (4 use sites)
  - LumotiaButton import added for selective use in follow-up sweeps

Bespoke surfaces left verbatim: VirtualSegmentList, audio player,
clear-all type-the-word modal, tag-chip filter bar.

Per-page gate: npm run check (0/0/5704 files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:58:42 +01:00
b6c065ffd1 v0.2 Phase 7.4: TasksPage — minimal wrapper sweep
Targeted migration per plan ("wrap, don't rewrite"). The page's
identity surfaces (energy chips, search input, quick-capture input,
bucket tabs, WipTaskList) stay verbatim — their rich ARIA and custom
radio-group semantics outweigh wrapper coherence here.

  - Dead Card import removed (never used in markup)
  - EmptyState → LumotiaEmptyState
  - "Pop out" toolbar button → LumotiaButton variant=tertiary

WipTaskList, CompletionSparkline, EnergyChip stay bespoke per
docs/release/v0.2-frontend-overhaul.md §6.3.

Per-page gate: npm run check (0/0/5704 files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:57:37 +01:00
cb69772f4e v0.2 Phase 7.3: FirstRunPage — wrapper sweep
Migrated the onboarding step cluster's ad-hoc button pairs to the new
grammar. Skip-link tertiary buttons stay as plain <button> with underline
because they're intentionally low-emphasis (text-only).

  - Step CTA buttons (primary + secondary) → LumotiaButton variants
  - Autostart "Saving…" pair → LumotiaButton loading + disabled
  - Error notice → LumotiaNotice tone=danger with body content snippet
  - Download progress bar → LumotiaProgress

Bespoke surfaces left verbatim: model-pick cards (rich content tiles
with Recommended/Downloaded pills), UnicodeSpinner, and the
test-recording quote-block.

Per-page gate: npm run check (0/0/5704 files). e2e baseline untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:55:59 +01:00
d812410039 v0.2 Phase 7.2: FilesPage — wrapper sweep
Migrated FilesPage chrome to the new grammar; the drop-zone affordance
and transcript textarea (the page's identity surfaces) stay verbatim.

  - Card import → LumotiaCard
  - EmptyState import → LumotiaEmptyState
  - "Browse Files" filled button → LumotiaButton variant=primary size=lg
  - Bottom "Copy" / "Export" toolbar → LumotiaButton variant=tertiary
  - Custom export dropdown → LumotiaMenu (Bits UI DropdownMenu)
  - Inline danger error → LumotiaNotice tone=danger
  - Custom progress bar → LumotiaProgress

Banks the LumotiaField + LumotiaNotice patterns the plan called out;
Field stays on the textarea (transcript surface is intentionally
naked inside the card per brand spec).

Per-page gate: npm run check (0/0/5704 files). Vitest / browser-mode /
e2e baselines unchanged (no behaviour change to the smoke surface).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:54:24 +01:00
3614c94885 v0.2 Phase 7.1: ShutdownRitualPage — wrapper sweep
Migrated the two ad-hoc buttons to wrappers; everything else (display-
only reflective copy, the open-loops list, the Newport shutdown
template) stays verbatim since the page is intentionally low-grammar.

  - Back-arrow button → LumotiaIconButton (icon=ArrowLeft, size=sm)
  - "Close" button   → LumotiaButton variant=primary

Bespoke: none on this page (no recording state, no transcript surface).

Per-page gate: npm run check (0/0/5704 files). Vitest / browser-mode /
e2e baselines stay green (no behaviour change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:53:15 +01:00
a9733544c0 v0.2 Phase 6: shell split — AppRuntime / AppChrome / AppOverlays
src/routes/+layout.svelte was 537 LOC of mixed runtime, chrome and
overlay concerns. Split into three single-purpose shells under
src/lib/shell/, with +layout.svelte reduced to ~28 LOC of pure
composition.

AppRuntime (no DOM beyond <svelte:window>):
  - Global hotkey dual backend (evdev / tauri-plugin-global-shortcut)
  - 120ms hotkey debounce (sacred behaviour §5 #2)
  - PREFERENCES_CHANGED_EVENT listener (sacred §5 #4)
  - KI-05 one-shot legacy-theme migration
  - Sidebar hotkeys: [ toggle, Ctrl+K, Ctrl+, (sacred §5 #10)
  - Wind-down tray listener
  - Meeting auto-capture poller
  - Global frontend error capture
  - Nudge bus + implementation intentions lifecycle
  - Font-size CSS var $effect
  - Window resize → sidebar auto-collapse
  - Onboarding/first-run check + update check + LLM status warm-up

AppChrome (the visual shell):
  - Titlebar (OS-aware via customChrome helper)
  - Sidebar (recording-state-aware — sacred §5 #1 stays in
    Sidebar.svelte verbatim)
  - Main slot
  - TaskSidebar conditional rail

AppOverlays (mounted-once globals):
  - ToastViewport
  - FocusTimer
  - MorningTriageModal
  - ResizeHandles (OS-gated)

src/lib/utils/customChrome.svelte.ts holds the single source of truth
for useCustomChrome, a module-level $state both AppChrome and
AppOverlays subscribe to. Each only ever sees one loadOsInfo() call
between them.

Secondary windows still escape via their own +layout@.svelte; the
defensive isSecondaryWindow check in +layout.svelte stays so a
direct /float, /viewer, /preview navigation through the root layout
also drops the chrome.

Phase 6 per-page gate green: npm run check (0/0/5704 files),
npm test, npm run test:browser (3/3), npm run test:e2e (16/16).
No regressions in the Phase 1 smoke baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:51:55 +01:00
c60f0aa5a5 v0.2 Phase 5: 11 primitives + gated design-system-v2 preview
Custom-styled primitives (no headless dep):
  LumotiaButton       — primary/secondary/tertiary/destructive × sm/md/lg
  LumotiaIconButton   — square icon-only; ghost/filled/destructive
  LumotiaNotice       — info/caution/danger/success inline notice
  LumotiaProgress     — native <progress> + token theming
  LumotiaField        — plain + Formsnap modes share the same markup

Bits UI 2.18.1 wrappers (warm-brutalist styling):
  LumotiaSelect       — single-select, options=[]
  LumotiaCombobox     — searchable; one-way inputValue + oninput
  LumotiaDialog       — controlled open; closable + footer snippet
  LumotiaTabs         — orchestrates List/Trigger/Content from a tabs array
  LumotiaTooltip      — wraps Provider + Root + Trigger + Content
  LumotiaMenu         — DropdownMenu items=[] with destructive variant

design-system-v2 preview route:
  src/routes/design-system-v2/+page.ts gates with VITE_LUMOTIA_DESIGN_SYSTEM_V2=1.
  Without the flag the load() throws 404 — route-level gate, not nav-
  hidden. Run via VITE_LUMOTIA_DESIGN_SYSTEM_V2=1 npm run dev:frontend
  to see the showcase.

Browser-mode component test:
  src/lib/ui/LumotiaButton.browser.test.ts. Covers render, click, and
  disabled-blocks-click. Validates that vitest-browser-svelte + the
  @vitest/browser-playwright provider land Phase 1's tooling
  contract end-to-end. 3/3 passing in Chromium.

Type fix: LumotiaIconButton and LumotiaMenu accept icon: any so
lucide-svelte's legacy SvelteComponentTyped shape composes with our
Svelte 5 wrappers without forcing a // @ts-nocheck escape hatch on
every call site. Tightens to Component<…> once lucide-svelte ships
a Svelte 5 build.

Type fix: LumotiaCombobox honours Bits UI 2.x Combobox.Root's
one-way inputValue contract. The wrapper drops bind:inputValue
and exposes an oninput callback so caller-owned filter pipelines
(HistoryPage FTS5, ModelDownloader) can drive options upstream.

Phase 5 per-page gate green: npm run check (0/0/5700 files),
npm test, npm run test:browser (3/3 in Chromium),
npm run test:e2e (16/16), guard-no-skeleton clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:47:53 +01:00
8c9708a508 v0.2 Phase 4: wrapper alias layer (src/lib/ui/)
Six thin alias wrappers, same prop APIs as the underlying components.
Lets pages migrate imports from $lib/components/* to $lib/ui/* one
file at a time without touching markup, and gives Phase 5+ a place
to tighten grammar without churning every call site.

  LumotiaCard           → Card.svelte
  LumotiaStatusPill     → StatusPill.svelte
  LumotiaToggle         → Toggle.svelte (forwards bind:checked, bind:loading)
  LumotiaSettingsGroup  → SettingsGroup.svelte (typed Props for svelte-check)
  LumotiaEmptyState     → EmptyState.svelte
  LumotiaPostCaptureCard → PostCaptureCard.svelte

Per the plan, the underlying components in src/lib/components/ are
untouched. They get retired during the per-page migrations in Phase 7
once no consumer remains.

LumotiaSettingsGroup mirrors the underlying Props interface explicitly
because Svelte 5's spread-into-typed-component caught a real missing-
`title` error during svelte-check. The mirrored interface keeps call
sites type-safe when importing via $lib/ui/.

Phase 4 per-phase gate green: npm run check (0/0/4135 files),
npm test (all green), npm run test:e2e (16/16), npm run
guard:no-skeleton (clean).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:38:23 +01:00
66e25aa778 v0.2 Phase 3: additive semantic tokens + KI-05 resolution
Additive token grammar (no renames, no replacements):

- --color-caution (dark #e8be4a, light #a08a1f) becomes the canonical
  name for tuned-amber notice surfaces in the new wrapper grammar
- --color-warning is kept as a CSS var() alias of --color-caution so
  every existing text-warning / bg-warning call site stays valid
- --color-info (dark #7a9ec0, light #3d6a8a) is the soft blue-grey
  signal for the new LumotiaNotice info variant
- --color-accent-environment (dark #8fae9a, light #4a7058) is an
  optional sage/moss support token for empty-state illustrations
  and environment-neutral status dots. NOT a brand swap — amber/
  copper --color-accent stays primary

Mirrored in src/design-system/colors_and_type.css (the buildless
preview pages bypass Tailwind so the duplication is intentional).

KI-05 resolved in the same commit, per the plan:

- src/lib/types/app.ts: drop `theme` from SettingsState
- src/lib/stores/page.svelte.ts: drop `theme: "Dark"` from defaults
- src/routes/+layout.svelte: drop the migration $effect, add a
  one-shot migrateLegacyTheme() on mount that copies any historical
  lumotia_settings.theme into preferences.theme and strips the
  legacy field. Idempotent — subsequent loads short-circuit
- src/routes/{float,viewer,preview}/+layout@.svelte: drop the same
  $effect; secondary windows inherit theme via PREFERENCES_CHANGED_EVENT
- src/lib/pages/SettingsPage.svelte: both SegmentedButton bindings
  (quick-settings row at :1272, Appearance group at :2606) now use
  Svelte 5 function bindings ({ get, set }) backed by prefs.theme
  and updatePreferences. No SegmentedButton API change

Phase 3 per-page gate green: npm run check (0/0/4129 files),
npm test (13/13), npm run test:e2e (16/16). No regressions in
the Phase 1 smoke baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:35:54 +01:00
94e6a79515 v0.2 Phase 2: install Bits UI / Formsnap / Superforms / Zod / @internationalized/date
Exact-pinned per the plan:

  bits-ui@2.18.1                 — headless Svelte 5 primitives
  formsnap@2.0.1                 — field+label+error wrapper
  sveltekit-superforms@2.30.1    — form state + validation
  zod@4.4.3                      — schema validation (superforms accepts ^3.25 || ^4)
  @internationalized/date@3.12.1 — bits-ui peer dep

Phase 2 gate green: npm audit signatures (273 verified registry sigs +
93 attestations), npm run check (clean), npm test (clean), npm run
test:e2e (16/16). No regressions in the Phase 1 smoke baseline.

@chenglou/pretext audit (plan risk item): kept — still referenced in
src/lib/utils/textMeasure.ts and src/lib/shims.d.ts.

No primitives wired yet — that's Phase 5. This commit only puts the
headless layer on disk so Phase 4/5 can import from it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:30:32 +01:00
100e04fa70 v0.2 Phase 0+1: planning doc + tooling baseline
Phase 0 — docs/release/v0.2-frontend-overhaul.md as single source of
truth for the v0.2 frontend coherence pass. Records hard rules,
tooling pins, sacred-behaviour contract list, wrapper catalogue,
per-page migration order, verification matrix, KI-05 plan, and the
explicit "DO NOT add Skeleton" line.

Phase 1 — tooling baseline. All exact-pinned per the plan:

- @playwright/test@1.60.0 + playwright@1.60.0 + @axe-core/playwright@4.11.3
- rollup-plugin-visualizer@7.0.1 (wired behind ANALYZE=1)
- @vitest/browser@4.1.6 + @vitest/browser-playwright@4.1.6 (provider)
- vitest-browser-svelte@2.1.1 (runes-aware Svelte 5 bridge)
- cargo-nextest installed globally

New configs: playwright.config.ts (frontend-only, dev:frontend webServer,
900x700 + 1440x900 projects, visual baselines deferred), vitest.browser.config.js
(separate from jsdom suite). New scripts: test:e2e, test:e2e:ui,
test:browser, analyze, test:rust:fast, guard:no-skeleton.

guard-no-skeleton.mjs walks package.json + package-lock + src/ for any
@skeletonlabs reference and exits 1 if found — locks in the no-Skeleton
hard rule for any future agent.

Smoke baseline (tests/e2e/smoke.spec.ts): 16 tests passing across two
viewports — app loads without Tauri runtime, keyboard nav, axe scan
(color-contrast deferred to Phase 7 per docs/release/v0.1-contrast-audit.md),
light/dark theme cycle, all three sensory zones (cave/energy/reset).
10 screenshots emitted to test-results/ as non-failing artefacts.

Surfaced + fixed two browser-preview bugs while landing the baseline:

- src/lib/utils/osInfo.ts: FALLBACK_BROWSER_INFO was evaluated at SSR
  module-load (navigator undefined → os: 'unknown'), and the UA check
  ran before navigator.platform — so Playwright's Windows-UA Chromium
  on a Linux runner detected as Windows, useCustomChrome went true,
  Titlebar mounted and tripped Tauri-only APIs. Now lazy-built per
  call; platform is the primary signal, UA only a fallback.

- src/lib/components/Titlebar.svelte: defensive hasTauriRuntime() guard
  on every handler and the $effect. Titlebar should not crash if any
  future code path mounts it without Tauri.

.gitignore: reports/, .playwright/, test-results/, playwright-report/.

Per-phase gate green: npm run check (0/0), npm test (0/0), npm run
test:e2e (16/16), npm run guard:no-skeleton (clean).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:28:51 +01:00
3770815fbf agent: lumotia — v0.1 release-completion run
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Closes the code-side v0.1 ship gate. All quality gates green:
cargo fmt/clippy/test (~327 tests), npm check (0/0), vitest 13/13,
scripts/dogfood-rebrand-drill.sh 8/8.

Phase F — first-run onboarding promoted to v0.1
- FirstRunPage with skip-to-main + failure recovery + event recording
- Six onboarding commands (record/list/has-completed + lumotia_events)
- Storage migration v17 (onboarding_events + lumotia_events tables)

UI hardening (in-scope items from v0.1-ui-hardening.md)
- StatusPill + PostCaptureCard components, 21st preview entry
- Sidebar recording-as-sacred-state (opacity + aria-disabled, reduced-motion)
- Settings 6-section regroup + Help section + Activation log + Privacy toggle
- Error-state copy sweep (DictationPage + SettingsPage, plain-language)
- Global :focus-visible rule, textarea outlines restored
- Ctrl+K / Ctrl+, / Escape bindings in +layout

LLM resilience
- rule_based_extract_tasks (regex-free imperative-verb extractor) +
  extract_tasks_with_fallback wrapper — task extraction never returns zero
- tokio::time::timeout(120s) wraps cleanup/tags/tasks commands

Release artefacts
- LICENSE (canonical AGPL-3.0), CHANGELOG (Keep-a-Changelog format)
- v0.1-release-notes, privacy-and-ai-use, install-warnings,
  tester-onboarding-kit, tester-acceptance-runbook, code-signing-setup,
  apple-silicon-rb08-runbook, virtual-audio-setup, v0.1-contrast-audit
- Workspace versioning + AGPL spdx; npm exact-pin (10 ranges removed)
- AppImage SHA-256 sidecar in build.yml
- README v0.1 section + Reporting-issues; canonical repo slug

Closure pass — items moved from human-required to code-complete
- KI-02 Linux idle inhibit: zbus 5 → org.freedesktop.login1.Manager.Inhibit
- KI-03 Windows sleep prevention: SetThreadExecutionState(ES_CONTINUOUS|...)
- acquire/release_idle_inhibit Tauri commands, wired in DictationPage
- Diagnostic-bundle frontend wire-up (Settings → Help button)
- WCAG-AA contrast fix via .btn-filled-text utility (no token changes)
- 8 destructive-action sites wrapped in plain-language confirm() guards
- KNOWN-ISSUES.md + v0.1-known-limitations.md updated (KI-02/03 fixed)

Scripts
- pre-tag-verify.sh, tag-day.sh, smoke-linux + driver
- parse-diagnostic-bundle.sh, parse-activation-log.py

Per-item audit trail: docs/release/v0.1-completion-status.md
Remaining: W-01…W-08 (signing certs, hardware probes, smoke matrix,
tester recruitment) — see docs/release/v0.1-known-limitations.md.
2026-05-15 06:59:08 +01:00
bf1b68275a agent: lumotia — Pass 1 v0.1 checklist refinements + Pass 2 v0.1 UI hardening boundary doc
Operationalises the ChatGPT review of the v0.1 release-doc set into two
related landings. Both bounded; no Garden Inbox work, no architecture
refactor, no full redesign.

PASS 1 — v0.1-checklist.md refinements
=======================================

Seven targeted edits to the existing checklist:

1. Cold setup vs warm activation split. Tester acceptance test was
   conflating model-download time (variable, network-dependent) with
   UX-controlled flow time. Two-phase pass:
     - Cold setup: install → onboarding → ready-to-record (no time bound)
     - Warm activation: model-ready → first recording within 3 minutes
   Steps 1-4 are cold; 5-10 are warm.

2. Migration-aware onboarding wording. "Skip-onboarding path for users
   who already have transcripts on disk" → "Migration-aware onboarding:
   existing users with valid data are not forced through first-run, but
   can launch the tutorial manually from Settings → Help."

3. UI acceptance section. New testable items between Documentation
   surface and Quality gates — turns "redo the UI" into measurable
   requirements:
     - Main capture action visible <1s on Home
     - Recording state not communicated by colour alone
     - 10-step flow completable at 900×700 + keyboard only
     - Destructive actions reversible or confirmed
     - Every async state has sidebar status chip feedback
     - Error states preserve raw transcript + plain-words next step
     - Settings Start Here / Privacy / Accessibility findable
     - Focus ring visible everywhere
     - prefers-reduced-motion respected
     - WCAG AA contrast spot-check both modes
     - Post-capture card surfaces (display-only; NOT Garden Inbox)

4. Supported platforms scope. New subsection before smoke-test matrix
   explicitly naming:
     - Primary (must work end-to-end): Linux Fedora + Ubuntu LTS
     - Best-effort (announced if smoke-tested): macOS Apple Silicon,
       Windows 11
     - Not announced unless smoke-tested: macOS Intel

5. P0/P1/P2 smoke-test severity replacing "any  blocks tag":
     - P0 — blocks tag (tester spine on a primary platform)
     - P1 — ships only with explicit known-limitation entry
     - P2 — does not block private beta (not-announced platform / v0.2
       feature)
   Pre-tag verification confirms no unresolved P0 or undocumented P1.

6. "Telemetry" → "local activation log". Re-worded the activation
   metrics capture mechanism. Word choice deliberate — privacy-conscious
   audience reacts to "telemetry" itself. Surface: Settings →
   Diagnostics → Activation log. Nothing sent automatically.

7. Support burden signal. New activation-metric subsection covering
   the AI-assisted-indie risk that every issue becomes a support call:
     - Self-service rate ≥ 70% (issues filed to bug tracker, not inbox)
     - Diagnostic bundle (logs + system info + crash dumps; skips
       transcript content + audio by default)
     - Top-3 setup failures documented after first 5 testers

PASS 2 — v0.1-ui-hardening.md
==============================

New strict-boundary doc at docs/release/v0.1-ui-hardening.md (262
lines). Anchored on the line:

  The v0.1 UI pass is not there to make Lumotia beautiful. It is there
  to make the first successful capture inevitable.

Step 0 (before any code change): walk the 20 existing
src/design-system/preview/ files and classify each item as
already-good / needs-v0.1-hardening / v0.2-polish. Don't rebuild what
works. Inventory table inline in the doc cross-references each preview
file to the in-scope items below.

IN SCOPE (10 items, each testable):
  1. Home capture clarity — big record button, status pill, last-capture
     preview, capped Now/Tasks at 1-3 visible
  2. Recording as sacred UI state — hide settings/history/advanced
     during capture; show only timer/pause/stop/cancel + live transcript
     + level meter
  3. Post-capture card — the v0.1 headline UI artefact. Display-only
     surface of raw + cleaned + tasks + microsteps + 4 actions
     (Save/Export/StartFirstMicroStep/OpenInHistory). Explicitly NOT
     Garden Inbox: no routing, no accept/edit/park/archive, no
     backlinks, no confidence scores
  4. First-run onboarding polish — single clear next action per step,
     pre-supplied prompt for test recording, graceful failure recovery,
     skip-to-main escape hatch (tracked as known-limitations follow-up)
  5. Settings sanity pass — 6 sections in order: Start Here /
     Transcription / Models / Tasks / Accessibility / Privacy / Advanced.
     Full 7-group progressive-disclosure regroup deferred to v0.2
  6. Error-state copy sweep — every error preserves raw transcript,
     explains in plain words, says next user action, no stack traces
     user-facing
  7. Keyboard flow — entire 10-step tester acceptance completable by
     keyboard only, focus ring visible, no hover-only controls
  8. Responsive at 900×700 + 1440×900 ONLY — ultrawide / mobile / split-
     screen deferred to v0.2 unless a tester reports them
  9. Accessibility practical checks (WCAG-style, not certification) —
     keyboard, focus, not-colour-alone, reduced motion, contrast spot-
     check, literal-words status labels, form-label association
  10. Status labels everywhere — new StatusPill component (no existing
     class found in survey); add to design-system/preview/components-
     status-pills.html. Pill states: Ready / Recording / Paused /
     Transcribing / Cleaning / Extracting tasks / Saved / Exported /
     Needs review / Failed safely

OUT OF SCOPE (the traps to refuse — each ships in v0.2 or later):
  - New visual identity (brand book v3 PDF is locked)
  - New navigation model
  - Garden Inbox (review cards, routing, accept/edit/park/archive,
    related notes, backlinks, P-P-T detection)
  - Suggested routing
  - Backlinks
  - Graph view
  - Canvas view
  - New animation system
  - Full SettingsPage 7-group redesign
  - Obsidian plugin
  - Cloud / provider UI

Plus a "Definition of done" with 8 specific completion criteria, and
cross-references to checklist + Garden roadmap + how-built + design-
system preview + locked brand book.

VERIFICATION
============
- cargo fmt --check: clean (no Rust touched)
- All four release docs cross-reference cleanly
- No new tests required (boundary docs, not code)
- v0.1 ship gate unchanged in shape, sharpened in detail
2026-05-14 22:12:21 +01:00
c5460a169c agent: lumotia — release-doc set + two pre-release audits (MCP + LLM failure)
Operationalises the ChatGPT/Jake roadmap-synthesis pass into four
release-boundary documents at docs/release/. Synthesis call:

  v0.1 = stable local capture product
  v0.2 = Garden Inbox / review cards
  v1.0 = PKM-complete + commercial track

Two factual audits ran first per Jake's explicit instruction — release
hardening only, no architecture refactors, no Garden Inbox work:

AUDIT 1 — MCP surface
=====================
Verdict: PASSES the v0.1 trust posture.
- Read-only by design (`//! No writes — Lumotia's Tauri app remains the only writer`)
- Stdio-only transport (newline-delimited JSON-RPC 2.0); no TCP/Unix
  listener, no bind, no network exposure
- Database opened via `lumotia_storage::init_readonly` — structurally
  enforced, not just convention
- 4 tools, all SELECT-only: list_transcripts, get_transcript,
  search_transcripts, list_tasks
- Zero matches for INSERT/UPDATE/DELETE/fs::write/fs::remove/
  create_dir/spawn_blocking in the crate
- Separate binary (`crates/mcp/src/main.rs`) — not part of the running
  Tauri app; user must explicitly launch and wire into client config
- Honest nuance flagged in known-limitations: a wired client gets read
  access to the entire transcript history + task list — no per-row
  permission boundary in v0.1

AUDIT 2 — LLM failure surface
=============================
Verdict: data-loss path PASSES; UX-wedge path PARTIAL (documented).
- post_process_segments (file + live pipeline): tracing::warn! on Err,
  segments stay at rule-based output. Raw transcript preserved.
- cleanup_transcript_text_cmd (DictationPage): frontend try/catch
  returns raw text unchanged on Err. Raw transcript preserved.
- extract_tasks_from_transcript_cmd (DictationPage): frontend falls
  back to rule-based extractTasks (regex + verb list) on Err.
- extract_content_tags_cmd (HistoryPage): per-row try/catch; toast on
  failure; transcript untouched.
- Hung llama.cpp: no tokio::time::timeout on the spawn_blocking call.
  Raw transcript preserved; rest of app functional; LLM status chip
  stays on "Cleaning up" until restart. Soft edge — documented in
  known-limitations as v0.2 hygiene candidate. Not implemented per
  "release hardening only" instruction.

THE FOUR DOCS
=============

docs/release/v0.1-checklist.md
  - 10-step tester acceptance test (install → capture → cleanup →
    task → MicroSteps → timer → history search)
  - Must-ship list per surface (product, onboarding, artefacts, docs,
    quality gates, trust+security, release-blockers, smoke-test
    matrix)
  - Activation metrics for private beta (3 min to first capture, 3
    captures in 24h, 7-day return, etc.) + v0.1 public launch
    (20 install, 15 first-capture, 10 return, 5 pay-£39)
  - Pre-tag verification sequence
  - Explicit out-of-scope list (Garden Inbox, Phases B-E/G/I/J, etc.)

docs/release/v0.1-known-limitations.md
  - User-facing rewrite, not engineer-speak
  - Power assertions per platform (Linux idle / macOS App Nap / Windows
    sleep) with practical workarounds
  - MCP read-only/local-only posture with the
    "all transcripts visible to your wired client" honest nuance
  - AI cleanup/extraction failure table — what fails, what you see,
    what's preserved
  - Settings page progressive-disclosure status
  - Internal engine refactor (orchestrator dormant) framed for users
  - Explicit "what's NOT in v0.1" call-outs
  - Reporting issues + crash-dump location

docs/release/v0.2-garden-roadmap.md
  - Headline: "review cards for turning messy dictations into notes,
    tasks, topics and links" — tangible, not PKM-overloaded
  - Garden Inbox scope (raw / cleaned / suggested title-type-folder-
    project / extracted tasks / suggested tags / possible links /
    confidence / Accept-Edit-Park-Archive)
  - Engine architecture Phases B-E pairing
  - Explicit "NOT in v0.2" list (no graph, no canvas, no PKM marketing,
    no cloud provider, no premium voices)
  - Open decisions for v0.2 scope freeze deferred until v0.1 ships +
    20 testers run

docs/release/how-lumotia-is-built.md
  - Public-facing trust page; honest disclosure that AI-assisted
    human-directed, then evidence
  - The real silent-data-loss bug the drill caught (Phase A.7 fix
    ff8dda0) framed as proof the process works
  - Phase B atomiser audit: 9 surgical fixes including the FIFO hang,
    LlmEngine unload race, purge-vs-restore SELECT-then-DELETE race
  - Supply-chain pre-flight (npm audit signatures + --ignore-scripts +
    pinned dev deps + pinned rust toolchain)
  - MCP read-only audit + LLM failure audit cross-referenced
  - Anti-patterns explicitly avoided (no telemetry exfiltration, no
    silent AI dependency, no "audit log later", etc.)
  - Calibrated to "AI use is survivable; sloppy undisclosed untested
    AI use is not" — RPCS3 framing cited

Verification:
- cargo fmt --check: clean (no Rust changed)
- All four docs are user-readable, not commit-log-derivative
- Cross-references resolve (every internal path quoted exists)
2026-05-14 21:49:54 +01:00
b6b7e8e86c agent: lumotia — Phase B dogfood plan — B.2-B.15 audit trail + finishing summary
Records the per-item verdict + commit hash for every Phase B audit item
(B.2 through B.15). Status block flipped to Complete 2026/05/14. Items
table moved every row from Pending → Done or Documented pass with a
one-paragraph outcome summary. Each Done item also has a full
section in the Done items list, mirroring B.1's existing structure
(surface, why it matters, fix, verification).

Surgical commits in the audit pass:
  643985d B.2 supervisor doc + test name match detach semantics
  31e3f5a B.3 download_impl unlinks .part on ResumeUnsupported
  20ef6c4 B.4 atomic DELETE RETURNING in purge_deleted_transcripts
  d8fa4ff B.5 resolve_export_path follows symlink before containment
  7f0e1b0 B.6 capability JSON mirror invariant pinned
  f252c1b B.7 unload() honours the loading flag
  813f024 B.8 storage crate emits via tracing (was log crate)
  401b6c3 B.9 strip <think>…</think> reasoning before JSON-envelope scan
  1c4ac98 B.10 vitest regression for focusTimer expired-rehydrate

Documented passes (no commit, recorded reasoning in plan): B.11, B.12,
B.13, B.14, B.15.

Phase A baseline gates verified green after the audit pass:
  cargo test --workspace      → 417 / 0 (was 409 baseline, +8 new tests)
  cargo fmt --check           → clean
  cargo clippy --workspace ...→ clean
  npm run test                → 13 / 13 (was 12, +1 new test)
  npm run check               → 0 errors / 0 warnings across 4015 files

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:44:55 +01:00
1c4ac98504 agent: lumotia — Phase B.10 pin focusTimer expired-rehydrate startTick invariant
Phase B.10 audit of commit 5ba761a (focusTimer rehydrate startTick
invariant; Race-10). The commit landed a comment-only change at
src/lib/stores/focusTimer.svelte.ts lines 204-208:

  // startTick() is REQUIRED here: tick() is the only thing that
  // observes `now >= completionFlashUntil` and calls clear(). Without
  // it, the completion flash would stay visible until the user
  // interacts with the app. stopTick() runs via clear() once the
  // flash window elapses.

A future edit could silently drop the startTick() call (the commit
acknowledged this with the comment) and the regression would only
surface on a real session: the user reopens Lumotia after a closed
expired timer, sees the completion flash, and waits for it to clear.
It never does. They click somewhere → clear() fires from the click
handler. Visible bug, but no automated gate.

The 5ba761a commit could not add a test at the time it landed because
vitest wasn't wired in the workspace — vitest scaffold landed in
commit 206ac62 the next day as Phase A.5. Now that vitest exists
(jsdom environment, .svelte.ts rune transformer, fake timers via
vi.useFakeTimers — see vite.config.js test block), the invariant is
straightforwardly testable.

Fix: new src/lib/stores/focusTimer.test.ts. The single test
`auto-clears the completion flash after the 3s window via the tick loop`:

  1. Seeds localStorage with a timer started 60 s ago that lasted only
     30 s — already-expired by 30 s when rehydrate runs.
  2. Calls focusTimer.rehydrate(). Asserts the flash is now visible
     and the timer is reported active.
  3. vi.advanceTimersByTime(3_500) — pushes wall-clock past the
     completionFlashUntil mark, drives the setInterval ticks.
  4. Asserts showingCompletionFlash is false, active is false,
     remainingMs is 0, and localStorage has been wiped.

If a future edit removes the startTick() call on the already-expired
branch of rehydrate(), step (3) won't run any ticks, the flash won't
clear, and step (4) fires the regression assertion. The test pins the
invariant the comment alone could not.

Verification:
  * npm run test → 13/13 (was 12, +1 from this commit). Both test
    files pass: localStorageMigration.test.ts (unchanged, 12 tests)
    and the new focusTimer.test.ts (1 test).
  * npm run check → 0 errors / 0 warnings across 4015 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:25:32 +01:00
401b6c3654 agent: lumotia — Phase B.9 strip Qwen <think>…</think> reasoning before JSON-envelope scan
Phase B.9 audit of commit 1d71e8e (replace GBNF grammar with manual
brace-counting JSON-envelope extractor). Existing coverage:
  * parse_string_array_trims_and_dedupes
  * json_envelope_complete_detects_finished_{object,array}
  * json_envelope_complete_ignores_braces_inside_strings
  * json_envelope_complete_rejects_prefixes_and_trailing_text
  * extract_json_envelope_skips_qwen_thinking_prefix (EMPTY think block)
  * extract_json_envelope_handles_arrays_and_trailing_stop_text

Solid for the cases tested. One real residual.

The `_skips_qwen_thinking_prefix` regression uses an EMPTY <think></think>
block: `"<think>\n\n</think>\n\n{...}"`. Qwen3.5's reasoning mode emits
non-empty reasoning when enabled (and reasoning is a documented Qwen
feature, surfaced in the model name family the engine targets). The
naive "find the first '{' or '[' in the whole text" extractor breaks in
two ways once the reasoning is non-empty:

  1. **JSON-looking text in thinking.** The model thinks out loud about
     the schema: "the answer should look like {\"topic\":\"x\",\"intent\":\"y\"}".
     The extractor sees the FIRST '{' (inside the reasoning), scans for
     its matching '}', and returns the reasoning literal as the
     envelope. The actual answer after </think> is dropped.

  2. **Unbalanced braces in thinking.** The model writes "I wonder
     about {something unfinished" inside <think>. The extractor starts
     its brace-stack on that unbalanced '{', never finds a matching
     '}', scans past </think> picking up the real answer's '{' (stack
     now has TWO '}' targets), eventually finds one '}' which pops the
     thinking's, then end of input — returns None. The actual answer
     is lost entirely.

Fix: split on the FIRST `</think>` and scan only the substring after.
Anything before `</think>` is reasoning, anything after is the answer
proper. Falls back to the whole text when no `</think>` is present
(covers non-reasoning models AND the empty-thinking case the existing
test pins).

Backwards-compatible:
  * Empty thinking — split_once returns ("", "\n\n{...}"); scan
    finds the '{' and returns the answer. Existing test passes.
  * No thinking tags at all — split_once returns None; fall back to
    full text. Existing tests pass.
  * Trailing stop tokens (`<|im_end|>` etc.) — unchanged behaviour;
    they sit after the envelope and don't affect the scan.

New regression tests:
  * extract_json_envelope_skips_thinking_block_with_json_looking_content
    — thinking with a JSON literal followed by the real answer. Pre-fix
    would return the thinking's literal; post-fix returns the answer.
  * extract_json_envelope_survives_unbalanced_braces_in_thinking — the
    unbalanced-brace-in-thinking case. Pre-fix returns None; post-fix
    returns the real answer.

Verification:
  * cargo test -p lumotia-llm --lib
      → 28/28 pass including the two new tests.
  * cargo fmt --check → clean.
  * cargo clippy -p lumotia-llm --all-targets -- -D warnings → clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:07:42 +01:00
813f024cdb agent: lumotia — Phase B.8 bridge storage events into tracing subscriber
Phase B.8 audit of commits 65abfa2 (Obs-3, span propagation across
spawn boundaries), 8becb1a (audit-trail empty commit for Obs-4/5
absorbed into afbd33d), and d1391b3 (Obs-1/2, drop lumotia_live literal
target). Existing coverage:
  * commands::live::tests::no_lumotia_live_literal_target_in_live_rs
    pins Obs-1/2 — no literal lumotia_live target survives.
  * src-tauri/tests/tracing_appender_smoke.rs::init_tracing_creates_log_file
    pins Obs-4/5 — install_subscriber writes to a rolling lumotia.log.
  * Obs-3 (span propagation) is not directly tested. Verifying that
    `tokio::spawn` / `thread::spawn` children carry the parent span would
    require custom subscriber infrastructure; the commit message
    acknowledges the 4 instrumented sites as canonical correlation
    points + "everything else fans out from them" + "Storage/audio/
    hotkey/MCP crates left uninstrumented in this commit — future
    sweep". Honour the SAFETY-style annotation; do not chase a synthetic
    subscriber test.

One real residual found.

DEFAULT_STDERR_FILTER and DEFAULT_FILE_FILTER in src-tauri/src/lib.rs
both list `lumotia_storage=info` (stderr) / `lumotia_storage=debug`
(file). Operator intent: storage events surface in stderr AND in the
rolling lumotia.log forensic stream that diagnostic-report bundles
attach. The reality: every storage event vanishes.

The storage crate uses `log` crate macros (log::warn! / log::info!),
not tracing. src-tauri/src/lib.rs installs a tracing subscriber but
does NOT install a `tracing-log::LogTracer` bridge, so log-crate events
never reach any tracing layer. There is no other log subscriber wired
either, so the events are silently dropped.

Concrete signals missing from diagnostic reports:
  * Migration progress (info, lines 603 + 639 in migrations.rs) —
    fires on every schema bump on every first-run after upgrade. Used
    to confirm "did the user's migration succeed?".
  * Audio-cleanup warnings (warn) from delete_transcript (database.rs
    line 369) and purge_deleted_transcripts (line 434) — the two
    log lines Rev-3 specifically added so a forensic report could
    confirm whether disk cleanup completed cleanly.

Same forensic blindness Obs-4/5 fixed for the rest of the codebase,
just for the storage subset.

Fix:
  * crates/storage/Cargo.toml: replace `log = "0.4"` with
    `tracing = "0.1"`. Every other crate in the workspace already uses
    `tracing = "0.1"`; storage was the outlier.
  * Replace the 4 `log::*!(target: "lumotia_storage", …)` calls with
    `tracing::*!(target: "lumotia_storage", …)`. Targets unchanged.
  * Reformat the two migration log lines as structured tracing events
    (version + description fields rather than printf-style positional
    interpolation) so they're filterable by EnvFilter directives and
    machine-readable in the forensic stream.

No behaviour change to storage call semantics. Pure logging-pipeline
rewire.

Verification:
  * cargo test -p lumotia-storage --lib
      → 70/70 pass (unchanged — none of the tests depended on the log
      crate).
  * cargo fmt --check → clean.
  * cargo clippy --workspace --all-targets -- -D warnings → clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:48:13 +01:00
f252c1b50e agent: lumotia — Phase B.7 close unload-during-load TOCTOU on LlmEngine
Phase B.7 audit of commit cde985d (LlmEngine critical-section narrowing
+ drop-old-model-first; Race-3 + Lifecycle-1). Existing coverage is
strong: is_loaded_does_not_block_on_slow_load proves probes return in
< 50 ms while the slow section runs (Race-3); second_concurrent_load_is_refused
proves a parallel load attempt is rejected with EngineError::AlreadyLoading
without reaching the heavy op (Race-3/4 TOCTOU at the engine layer);
the test harness __test_run_with_lock_discipline mirrors load_model_with's
discipline (claim loading flag, clear engine state, run op outside the
inner mutex, then install). The Lifecycle-1 visible side-effect
(is_loaded reports false mid-swap) is covered by the first test.

One real residual found.

unload() does not consult the `loading` flag. When load_model_with is
mid-flight (step 3 has already cleared model + loaded, step 5 has not
yet installed the new state), a concurrent unload() takes the inner
mutex, sees model + loaded already None, no-op-clears, and returns Ok.
The slow load then completes step 5 and installs the new state —
silently overwriting the unload the caller already saw success for.

Concrete attack shape: app startup auto-loads the default LLM in the
background via download_llm_model + load_model. User opens Settings,
clicks "Delete Model X". delete_llm_model checks loaded_model_id()
(returns None mid-load) and skips the unload branch, then calls
model_manager::delete_model(X) which removes the GGUF file from disk.
The load completes via mmap (which on Linux holds the inode alive
after unlink) and installs state pointing at a deleted file path. The
user sees "Model X loaded" in the UI even though they just deleted it.

Same `loading` AtomicBool that guards load-vs-load needs to guard
unload-vs-load.

Fix:
  * unload() now checks is_loading() at entry. Returns
    EngineError::AlreadyLoading when a load is mid-flight; caller can
    retry once is_loading() reports false.
  * EngineError::AlreadyLoading message generalised from "refusing to
    start a parallel load" to "refusing to start a parallel load or
    modify engine state mid-load", since the variant now fires from
    both directions. The variant name itself remains accurate (the
    state of being already loading).

Behavioural diff for unload during quiescent state: unchanged.

Behavioural diff for unload mid-load: Err(AlreadyLoading) instead of
Ok with silent overwrite.

Callers checked:
  * unload_llm_model (Tauri command) — converts EngineError → String
    via .map_err and surfaces to the frontend. New error string is
    self-explanatory; no frontend code matches on the old message
    substring.
  * delete_llm_model — calls unload only when loaded_model_id matches.
    If unload returns AlreadyLoading the delete also fails;
    .map_err(|e| e.to_string())? propagates. The user gets a clear
    "cannot unload while loading" toast and can retry; better than the
    silent contract-violation the old code allowed.
  * No other callers exist for LlmEngine::unload (whisper/parakeet
    engines have their own unload methods on a different type).

New regression test: unload_during_load_is_refused. Spins a loader
thread on the existing __test_run_with_lock_discipline harness, blocks
mid-slow-section via a Barrier, fires unload() from the main thread,
asserts AlreadyLoading. After releasing the load, unload() succeeds —
proving the flag-clear discipline on the happy path.

Verification:
  * cargo test -p lumotia-llm --lib
      → 26/26 pass including the new test.
  * cargo fmt --check → clean (applied fmt after the edit).
  * cargo clippy -p lumotia-llm --all-targets -- -D warnings → clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:41:57 +01:00