Feat/UI fixes merged with main#1
Conversation
Consuming the LWin keyup left Windows thinking Win was still held, so PasteEngine's SendInput(Ctrl+V) read as Win+Ctrl+V (Action Center / Quick Settings) and every subsequent keystroke became a Win+key system shortcut. The Ctrl-tap injection (AHK #MenuMaskKey idiom) still runs to suppress the Start menu; we just let the real LWin keyup reach the OS so its key-held state clears. Spec §13 prescribes the old (buggy) behavior; flagged for revision in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface (docs/PILL_DESIGN.md §1, §3): - 200×56 with 8 px DWM-rounded corners, Mica backdrop on Win11 22H2+ (DWMWA_SYSTEMBACKDROP_TYPE = DWMSBT_TRANSIENTWINDOW), dark-tinted via DWMWA_USE_IMMERSIVE_DARK_MODE. Falls back to the §3.1 dark gradient on older Windows. - §3.3 1 px hairline border + drop shadow + inner top highlight. Five-state machine (§2): - Recording: 20-bar visualizer + RECORDING micro-label. - Transcribing: 14 px ¾-arc spinner (0.9 s loop, rotated via direct BeginAnimation on the RotateTransform) + "Transcribing…" text. - Confirmed: "Pasted into <App>" with the bold app name, 1 s hold. - Error: 5 px red dot + reason text, 2 s hold, instant accent shift to red (§5). - Idle: PRD G4 dev override — pill stays visible between dictations showing the app icon + "KusPus" label. Will revert to spec §6.1 hidden-when-not-in-use once Settings exposes the close path. Visualizer (§4): - 20 bars × 3 px wide × 4 px gap × 4–26 px tall (136 px track). - Damped target/value motion model per §4.2: center-weighted speak envelope, per-bar damp rates, real audio levels from IAudioRecorder override the simulation when present. Runs on CompositionTarget. Rendering for display-refresh smoothness. Accent line (§3.4): - 136 × 1.5 mint gradient with glow, opacity per state. Motion (§5): - 120 ms pill appear/disappear, 150 ms content crossfade between states. Cubic easing. Hover-extend override (PILL_DESIGN.md §10): - Width animates 200 → 280 over 150 ms on hover, Settings + Close buttons fade in. WS_EX_TRANSPARENT intentionally NOT applied (overrides §1.2 click-through) so buttons work; WS_EX_NOACTIVATE preserved so focus doesn't move. Draggable pill (beyond spec): - MouseLeftButtonDown anywhere on the pill body → DragMove. Skips when click is on a Button (Settings / Close). - Session-only per-monitor remembered positions via Dictionary<deviceName, Point> keyed by MONITORINFOEX.szDevice. Cleared on every fresh process start. Multi-monitor option C (hybrid sticky): - On state transition into Armed/Recording, jump to the foreground window's monitor at its remembered position (or default bottom-center if first time). No-op when pill is already on the right monitor or while user is dragging. Coordinator snapshot extension: - CoordinatorSnapshot.PostPaste:PostPasteInfo carries (Pasted, TargetApp, ErrorReason). AppCoordinator emits one post-paste snapshot from DeliverAsync / HandleFailureAsync so the pill knows whether to show Confirmed or Error. Icon pipeline: - tools/IconBuilder: one-shot SVG → multi-frame ICO converter. - icons/icon.svg as the single source of truth. ViewBox tightened to "272 246 480 480" so the 5-bar content fills ~94 % of every rendered frame (tray, taskbar, Task Manager, .exe icon). - icons/icon.ico regenerated, embedded as ApplicationIcon + WPF Resource. TrayManager loads via pack URI. - SharpVectors.Wpf 1.8.5 renders the SVG directly inside the pill Idle state — no hand-converted XAML to drift from the source. Deferred from spec (not blockers): - §3.2 light theme + WM_SETTINGCHANGE live switching. - §3.4 accent variants beyond Mint (needs Settings UI from Phase 9). - §5.3 reduced-motion gating of fades. - Win10 acrylic fallback. Tests: 117/117 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface: - New MainWindow per docs/APP_DESIGN.md §3. 880×620 (820×600 min) system- chromed window. Sidebar nav (6 RadioButton tabs styled per §3.2 — mint stripe + elevated bg on select). Close hides (§3.1 / §8.5); only the tray's Quit fully exits. Tray menu gains "Preferences…" → MainWindow. - Dark title-bar tint via DwmSetWindowAttribute DWMWA_USE_IMMERSIVE_DARK_MODE + SetWindowPos(SWP_FRAMECHANGED) to force the non-client repaint on runtime theme flips (validated against Microsoft Q&A "DWMWA_USE_ IMMERSIVE_DARK_MODE won't update"). Six tabs (§3.3): - General: Hotkey hero card with live listen-mode rebind (suspends LL hook, captures held-keys snapshot, commits on full release, conflict warning against known Windows shortcuts), Startup toggle → HKCU\Run, Appearance Auto/Light/Dark segmented control. - Audio: device label + Discord-style 200×6 track+fill meter (validated fix against naudio/NAudio#160 #347 #507 — MMDevice.AudioMeterInformation reports zero without an active capture session, so we open a WasapiCapture and compute peak from samples in DataAvailable). Peak-hold tick that decays slower than fill. - Models: active-model row + manifest list with install state (file existence per device), radio-select writes ActiveModelId to PrefsStore; download wiring deferred to Phase 11. - History: last 50 transcripts via IHistoryStore.SearchAsync; status dot (mint = ok, red = failed), relative time, app name, model + duration. - Privacy: offline + crash-reports toggles to PrefsStore, logs path + Open in Explorer, local-first mint promise card. - About: 80px brand mark + version line (AssemblyInformationalVersion) + Cascadia-Mono build line, Resources card group (GitHub link + logs + Re-run onboarding placeholder), MIT/local-first license blurb. Theming infrastructure: - ThemeApply (new) resolves "auto"/"light"/"dark" against AppsUseLightTheme registry, applies DWM dark-mode + SWP_FRAMECHANGED. - ThemeTokens (new) — 23-entry map of (dark, light) Color pairs covering AppBg, Sidebar, Surface, SurfaceElevated, BorderSubtle/Strong/Divider, Primary/Secondary/Muted/DisabledText, HoverSubtle, KeycapBg/Border, Mint/MintTint/MintBorder, ErrorRed, WarningAmber/Tint/Border, plus pill-specific PillBorder/PillInnerHighlight/VisualizerBarActive/Idle and MeterTrack/ButtonHoverBg. Plus a LinearGradientBrush builder for the pill's two-stop surface gradient. - ThemeTokens.Apply uses REPLACEMENT (not mutation) — WPF freezes Freezable resources in Application.Resources (x:Shared semantics) so brush.Color mutation throws InvalidOperationException at startup. Replacement fires ResourcesChanged; every {DynamicResource} consumer re-resolves. - MainWindow.xaml + FloatingPillWindow.xaml refactored end-to-end to use {DynamicResource Token} for every brush/foreground/border. Code- built UI (Models rows, History rows, hotkey keycaps) uses a Theme(key) helper that returns the current resource brush; theme changes re- render visible dynamic tabs so they pull fresh brushes. - Pill visualizer bars use SetResourceReference(FillProperty) instead of a frozen explicit brush so bars re-theme on switch. Deviations from spec — flagged in CLAUDE.md (no MainWindow.xaml hex literals migrated; everything is now token-based). Body theming for the pill required new PillSurface gradient resource installed per- theme. Multiple WPF parse-time gotchas worked around: IsChecked="True" on TabGeneral triggers Checked event before content panels are bound to fields — fixed with a _loaded guard in OnTabChecked. Tests: 117/117 pass. docs/APP_DESIGN.md: new authoritative UI spec from the user, referenced from CLAUDE.md source-of-truth list. Where it conflicts with PILL_DESIGN §2.1 (click-through) the §10 hover-extend override still wins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OnboardingWindow per docs/APP_DESIGN.md §4. 720×520 chromeless rounded card, 12 px corners, draggable from the progress-dots header. Theme-aware via the same ThemeApply + DynamicResource brushes that flip MainWindow / pill. Seven steps (linear nav with Back / Next / Skip / Finish): - 1 Welcome: stylized desktop preview + 3-column value-prop grid. - 2 Hotkey: listen-mode + capture + commit to PrefsStore.Hotkey, with the same Win+L-class conflict warning. Duplicates MainWindow's listen-mode state machine (extract to UserControl when there's a third consumer). - 3 Mic check: WasapiCapture on default device, Discord-style track+fill meter, success/error variants, Open Settings → ms-settings:privacy-microphone. - 4 Autostart: clickable ToggleCard → HKCU\Run via AutostartRegistry.Set. - 5 Crash reports: ToggleCard + Local-first promise card. - 6 Try it: 120-DIP transcript surface, "Simulate dictation" runs 1.8 s Listening… then surfaces one of three canned sentences with mint border. - 7 Done: corner-of-screen tray-diagram + Finish. Finish path writes PrefsStore.Onboarding.Completed = true. Skip / Esc / window-close leave Completed = false so the modal re-pops on next launch. Re-runnable any time from About → "Run again" (replaces the disabled Phase 9 placeholder button). First-launch trigger in App.OnStartup: after _coordinator.Start(), queue ShowDialog at DispatcherPriority.Background so OnStartup returns before the modal's nested dispatcher frame begins. Pill + coordinator already running by then, so the modal's hotkey picker can suspend the live LL hook cleanly. Deferred to a polish cluster: §4.1 dimmed-desktop backdrop, hotkey-picker UserControl extraction, value-card hover states. Tests: 117/117 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nding Phase 11 (TECH_SPEC §19, PRD §10.2/§10.3) - CrashReporter: Sentry init/shutdown gated on (CrashReportsOptIn && !OfflineMode). Embeds project DSN (env-var override) and routes Sentry's own transport through EgressAllowlistHandler so PRD §10.2 holds for SDK uploads too. - EgressAllowlistHandler + EgressPolicy: v1 allowlist. Accepts huggingface.co and regional Sentry ingest hosts (*.ingest[.<region>].sentry.io); Offline Mode and non-HTTPS block everything. Pure policy decision in Core, IO handler in App. - CrashScrubber: drops events whose Tags/Extra contain transcript/clipboard/text/ password/target_app/hwnd keys; replaces %TEMP%, %LOCALAPPDATA%, %APPDATA%, %USERPROFILE% prefixes (start-anchored) and Environment.UserName occurrences (mid-string) in messages, exception text, stack-frame paths, breadcrumbs. - 30 new unit tests (167/167 total). UI/UX W1 — "stop lying" (APP_DESIGN §13 audit findings) - Crash Reports toggle visually disables when Offline Mode is on; subtitle swaps to "Disabled while Offline Mode is on." The toggle's IsChecked is preserved. - Replace stale "Phase X" + Win32 jargon copy across General/Audio/Models/About. - Remove permanently-disabled "Test transcription" section; restore in W3 when wired. - Sidebar footer bound live: status label from AppCoordinator snapshots, chord glyph from PrefsStore (compact short form: "Ctrl+Win", not unicode soup). - Inline [ESC] keycap in hotkey-listen hint (APP_DESIGN §13.4). - Audit findings appended to docs/APP_DESIGN.md as §13 with a progress ledger. Build: 0/0. Tests: 157 + 10 new CrashScrubber + extended EgressPolicy = 167/167. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, spacing) APP_DESIGN.md §13.5 P1-6/-7/-1/-9/-8/-P2-5. No user-visible scope change — this is pure styling consolidation. Five new shared style sets, four inline-styled buttons replaced, one fragile negative-margin layout fixed. New: src/KusPus.App/Styles/ - Typography.xaml — SectionHeader, Type.RowTitle, Type.RowSubtitle, Type.Eyebrow, Type.MonoSm, Type.MonoXs, Type.Display, Type.WarningEmphasis. Replaces ~25 inline TextBlock FontFamily / FontSize / FontWeight / Foreground quadruplets across MainWindow.xaml. - Dot.xaml — Dot.Mint / Dot.Amber / Dot.Red ellipse styles with the spec's 7 px + 6 px coloured glow. Replaces 4 hand-rolled ellipses in MainWindow.xaml + 1 code-behind ellipse in the history row renderer. - Buttons.xaml — Btn.Primary / Secondary / Ghost / Danger × Sm / Md / Lg per APP_DESIGN §3.4. All buttons now share one template (border + content + hover opacity ladder + disabled-state); inline Padding / Background / Foreground / BorderThickness gone from every call site. - Focus.xaml — Focus.Mint: 1.5 px mint dashed outline at 2 px inset. Wired into SidebarTab, Toggle, SegmentButton, and Btn.Base — keyboard-nav now has a visible focus ring that reads as a design choice rather than the WPF default dotted-Aero ring. Modified: src/KusPus.App/MainWindow.xaml - All TextBlock declarations matching repeated patterns use a Style key. - All Buttons use Btn.Secondary (only kind currently needed; the rest of the set arrives when W3's purge / download flows land). - ConflictRow refactored: wraps HotkeyCard + ConflictRow in a single 440 px StackPanel with bottom Margin 28. ConflictRow gets `Margin="0,1,0,0"` (1 px gap below HotkeyCard) instead of the previous `Margin="0,-20,0,28"` negative- margin tuck. Section gap now lives on the parent, not on each child. Modified: src/KusPus.App/App.xaml - Application.Resources now merges the four Styles/*.xaml dictionaries so every keyed style is reachable app-wide (including future OnboardingWindow reuse). Build: 0/0. Tests: 167/167 still green. Smoke: clean launch. P1-10 (per-tab UserControl extraction) deferred to a post-W3 cleanup — doing it now would mean reshuffling every W3 addition into newly-created files. The ledger in docs/APP_DESIGN.md §13.5 reflects this. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…loads, contrast
APP_DESIGN.md §13.5 P0-3, P0-5, P1-2, P1-3, P1-4. Ships the four user-facing
gaps the audit flagged. All wired against existing services — no new layers.
P0-3 — DisabledText contrast
- ThemeTokens.cs: dark #5CFFFFFF → #80FFFFFF (~3.0:1 → ~5.1:1 vs AppBg).
- Light #50141414 → #A0141414 (~1.9:1 → ~4.7:1 vs AppBg).
- Both clear WCAG AA normal-text 4.5:1. Used for hotkey hint, model "Not
installed" label, disabled buttons.
P0-5 — Mic-active disclosure (already in W1)
- The Audio "Live level" subtitle reads "Active only while this tab is open.
Audio is never recorded." Tracking the ledger to "done" — no code change.
P1-3 — Privacy Logs row
- Two-row card: "Log size · {size}" with Clear logs ghost-danger button, then
"Log folder · {path}" with Open in Explorer secondary button.
- RefreshLogsSize enumerates LOCALAPPDATA\KusPus\logs\*.log on Loaded.
- OnClearLogsClick confirms via MessageBox (No default), then File.Delete each
*.log. Today's open log is held by Serilog's FileSink — skipped without
error, count reported in log.
- FormatBytes handles bytes / KB / MB.
P1-2 — History search + bulk footer
- Search box at top with Segoe Fluent icon, placeholder overlay, clear "×"
button. 250 ms DispatcherTimer debounces TextChanged → HistoryStore.SearchAsync
(FTS5 backing). Empty query reverts to "most recent first".
- Footer above 1px divider: live row count, "{n} matches for '…'" when filtered,
Purge all history Btn.Danger with MessageBox confirm. Calls HistoryStore.PurgeAllAsync.
- PurgeAllButton.IsEnabled gates on row count > 0 || query is not null.
P1-4 — Models download flow
- Per-model state machine in _modelDownloads dictionary keyed by id.
- BuildModelStatusRegion dispatches on state: Active / Installed / Downloading
(180 × 4 px mint progress bar + percent in mono + Cancel ghost) / Error
(red message + Retry secondary) / Not installed (Download secondary).
- OnModelDownloadClick: pre-checks OfflineMode (clearer message than letting
EgressAllowlistHandler throw mid-stream), spawns Task.Run with cancellable
cts. IProgress<DownloadProgress> marshals to UI thread, throttled to 0.5 %
steps so the StackPanel rebuild doesn't dominate CPU on a fast link.
- OnModelCancelClick: cts.Cancel(). Completion continuation handles cleanup
for both cancellation (silent) and failure (sticky error + Retry).
- ShortenDownloadError strips ModelManager's "HTTP error downloading …:" prefix.
Build: 0/0. Tests: 167/167.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing rhythm
History tab — table layout (audit follow-up)
- Replace card-list rendering with a Grid-based table: header row in
Type.Eyebrow style above, 6-column body rows below. Columns: status (14) /
time (78) / app (110) / transcript (*) / model (72) / duration (52).
- Per the skill's number-tabular rule: time, model, duration use Cascadia
Mono so columns stay aligned across rows.
- Per truncation-strategy: transcript and app truncate with ellipsis +
ToolTip exposing the full text on hover. Time column ToolTip shows the
full timestamp.
- Per gridline-subtle: 1 px BorderDivider between body rows, 1 px
BorderSubtle between header and body. No row striping.
- Row hover: HoverSubtle background via Style trigger on the HistoryRow
Border (Cursor=Hand for affordance).
- Right-click context menu per row: "Copy text" (Clipboard.SetText) and
"Delete" (HistoryStore.DeleteAsync + ReloadHistoryAsync).
- ShortModelId strips "ggml-" prefix to match the sidebar's compact form.
- Failed transcripts: red dot + italic red transcript column; rest of the
row stays normal so the failure mode reads as one cell, not the row.
Pill Settings button wired to Preferences modal
- Add SetSettingsAction(Action) to FloatingPillWindow, mirroring
SetCloseAction's pattern.
- App.OnStartup wires it to _mainWindow.ShowOn("general") AFTER MainWindow
is constructed (the existing pill setup runs before MainWindow exists).
- Tooltip "Settings — coming soon" → "Open Preferences".
Spacing rhythm normalization
- Models tab: list bottom margin 12 → 16 (align with the 8-grid inter-block
rhythm; "16 = block gap" is now consistent across History search bar and
Models list).
- About tab: header StackPanel bottom 32 → 28 (matches the section gap
rhythm used everywhere else, instead of being one-off heavier).
- History footer: top margin 18 → 16 (16 is the canonical block gap).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…le-card Parallelised Phase 1 (5 sub-agents wrote disjoint style files concurrently), then serial Phase 2 sweep of MainWindow + App.xaml + APP_DESIGN.md. NEW style files (Phase 1, parallel) - Styles/Tokens.xaml — Space.xs..xxl + Pad.Tight/Default/Hero + Radius.Sm/Md/Lg - Styles/Surfaces.xaml — Surface.Default/Hero/Tight/Warning/Mint (5 Border styles) - Styles/Inputs.xaml — Input.Search TextBox style - Styles/Typography.xaml extended — 10 new Type.* roles, dead Type.MonoXs removed - Styles/Buttons.xaml extended — new Btn.IconGhost; header docs call-site inventory Phase 2 — App.xaml wires the 3 new dictionaries (Tokens first so others can reference its tokens), then sweep MainWindow.xaml + MainWindow.xaml.cs: MainWindow.xaml migrations - Hotkey card Border → Surface.Hero (drops inline Padding 22,20 override) - ConflictRow Border → Surface.Warning (collapses 5 inline attrs) - Local-first Border → Surface.Mint (collapses 5 inline attrs) - HotkeyHint TextBlock → Type.HintItalic - ConflictText TextBlock → Type.WarningBody - AboutVersion TextBlock → Type.Body - AboutBuildLine Margin 0,4,0,0 → 0,3,0,0 (matches Type.RowSubtitle rhythm) - Local-first head TextBlock → Type.MintHeadline - Local-first body TextBlock → Type.BodySmall - MIT licensed TextBlock → Type.Footnote - "Press a hotkey" TextBlock → Type.HintItalic - StatusLabel TextBlock → Type.SidebarStatus (was Type.MonoSm-with-override misuse) - History search bar magnifier → Type.IconSm - History search box TextBox → Input.Search - History search clear Button → Btn.IconGhost - About re-run card Margin 0,0,0,32 → 0,0,0,28 (matches section gap) - Sidebar footer Grid Margin 18,8,18,14 → 14,8,14,14 (matches sidebar 14) History tab — unified single composed card (Q3 from user audit decisions) - Outer RowCard Padding="0" wraps a StackPanel of inner Borders. - Search bar (Padding 14,8, bottom 1 px divider), table header (Padding 14,10, bottom 1 px divider), HistoryList (HistoryRow style provides per-row bottom divider), bulk footer (Padding 14,12, no top border — last row's bottom border IS the separator → no double line). - Reads as one "history widget" instead of four separately-styled blocks. MainWindow.xaml.cs code-behind sweep - New TypeStyle(string) helper (mirrors Theme()) to pull Type.* styles from Application.Resources for code-built TextBlocks. - BuildModelRow title/subtitle → Type.RowTitle / Type.RowSubtitle - BuildBundledBadge child TextBlock → Type.BadgeMint - BuildModelDownloadingRegion percent → Type.MonoSm - BuildModelErrorRegion error text → Type.ErrorInline - BuildHistoryRow TIME / MODEL / DUR columns → Type.MonoSm (transcript + app columns + Installed/Active status stay inline because Foreground flips per state — no single Type.* role covers both colour states). - Empty-state TextBlock in ReloadHistoryAsync → Type.HintItalic - Dropped unreachable "(none)" branch in RenderModelsTab (audit P2-8). Docs: APP_DESIGN.md §13.5 ledger updated (P2-8/-9 done, P2-10 won't-fix with reason); new §13.6 documents the Round 2 work — token system, surface variants, typography role catalogue, inputs/IconGhost, spacing fixes, History unification, code-behind cleanup, dead-code policy. Parallelisation safety - 5 sub-agents in Phase 1 each owned exactly one file (disjoint writes — no lost-update risk). None ran dotnet build (avoids bin/obj corruption). The orchestrator does all building serially in Phase 3 after killing any running KusPus.exe to avoid output-DLL locks. Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author byline (About → bottom-right)
- "Made by Devang Kumawat" + LinkedIn / X / GitHub / Portfolio icon row at
the bottom-right of the About tab, sitting directly on AppBg (no card —
reads as a personal touch, not part of the design-system surface inventory).
- Text uses Type.Footnote (12 Medium SecondaryText) — matches the existing
"MIT licensed" line on the left. Per UI UX Pro Max rule weight-hierarchy:
text carries the byline weight, icons are visually subordinate.
- Each icon: 14×14 Viewbox inside a Btn.IconGhost (28×28 click target).
Aspect ratio locked by the Viewbox's default Uniform Stretch and the
underlying 24×24 viewBox.
- Theme tinting: Fill="{DynamicResource MutedText}" (filled paths) or
Stroke="{DynamicResource MutedText}" (Lucide globe) — no per-theme assets,
one shared rendering for dark+light.
Icon sources (saved to icons/social/ + LICENSE.md attribution)
- LinkedIn / X / GitHub: Simple Icons (CC0) via jsDelivr simple-icons@v11
and raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/.
- Portfolio (globe): Lucide (ISC) from raw.githubusercontent.com/lucide-icons/lucide.
- Saved as .svg files for documentation/license tracking; actual rendering
inlines the path data in MainWindow.xaml so the fill binds to theme tokens
(SharpVectors SvgViewbox can't easily theme-tint).
Each icon button OpenUrl(...) → Process.Start with UseShellExecute=true.
Single helper handles Win32Exception + FileNotFoundException for missing
default browser without crashing.
Links wired
- LinkedIn → https://www.linkedin.com/in/devangk003/
- X → https://x.com/devang_kumawat
- GitHub → https://github.com/devangk003
- Portfolio → https://lnk.bio/devangk003
Spacing (user audit feedback — 1 px stacking felt cramped)
- Models tab BuildModelRow inter-row Margin 1 → 8 (per model row reads as
its own card now, not a grouped 1-px-divider stack).
- About tab Resources/Log-folder cards Margin 0,0,0,1 → 0,0,0,8. Re-run
card already at 0,0,0,28 (section gap below it).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit gap closed. Previously the three top-level handlers (OnUnhandledException / OnDispatcherUnhandled / OnUnobservedTask) only wrote to Serilog — Sentry's own AppDomain auto-hook fires in parallel but WPF dispatcher exceptions were swallowed by e.Handled=true before Sentry could see them. Now each handler logs first, then forwards via the new TryReportToSentry helper. TryReportToSentry is gated on _crashReporter?.IsActive so the call no-ops when the user hasn't opted in (or Offline Mode killed the SDK). The Sentry call itself is wrapped in try/catch so a Sentry failure can't recurse into another unhandled exception. Behaviour summary - Crash Reports OFF: handlers log locally, no network. Same as before. - Crash Reports ON, Offline Mode OFF: every unhandled exception (AppDomain, WPF dispatcher, unobserved Task) reaches your Sentry EU project with the scrubbing pipeline applied. - Crash Reports ON, Offline Mode ON: CrashReporter shuts the SDK down → IsActive==false → handlers skip the Sentry call. Local logging still runs. Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…F commit
Onboarding rounded corners (APP_DESIGN §13.7)
- Root cause: WindowStyle=None + AllowsTransparency=False + Background=Transparent
renders the area outside the inner Border's CornerRadius as black. The
inner <Border CornerRadius=12> shows but the 4 corner triangles around it
fill with WPF's solid black for "transparent-but-not-actually-transparent".
- Fix per Microsoft's "Apply rounded corners in desktop apps for Windows 11"
guidance:
- XAML: Background="Transparent" → Background="{DynamicResource AppBg}".
Corners blend on Win10 fallback.
- Code-behind: DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE=33,
DWMWCP_ROUND=2, sizeof(int)) in OnSourceInitialized. Win11 rounds the
OS-level window edge; Win10 is a silent no-op.
- Corner-radius spec deviation 12 → 8 px (Radius.Lg). The DWM API only
supports DWMWCP_ROUND (~8 px) or DWMWCP_ROUNDSMALL (~4 px) — no path to
custom 12 px without AllowsTransparency=True (loses Mica + reintroduces
the cutout bug). 8 px is also MainWindow's curvature → both surfaces share
one canonical radius via the Radius.Lg token. APP_DESIGN §4.1 updated;
full rationale in §13.7.
Model download pinned + verified
- Replaced models.json placeholders (TODO_PIN commit + TODO_FILL SHAs) with
real values fetched from HuggingFace's tree API:
commit = 5359861c739e955e79d9a303bcbc70fb988958b1 (2024-10-29)
sha256 = LFS digests pulled per file from /api/models/.../tree/<commit>
sizeBytes = corrected to HF's actual sizes (placeholder bytes were
slightly off, would have shown wrong progress-bar totals)
- 5 models wired: ggml-tiny.en (77.7 MB · bundled), ggml-base.en (148 MB),
ggml-small.en (488 MB), ggml-medium.en (1.53 GB), ggml-large-v3 (3.10 GB).
- Models tab Download button now hits real URLs → HuggingFace serves the
.bin → ModelManager verifies SHA-256 against the manifest entry → File.Move
to %LOCALAPPDATA%\KusPus\models\. Behaviour matches TECH_SPEC §18.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…scription P0 mic-always-on bug - Root cause: SelectTab's StartAudioMeter/StopAudioMeter only ran on tab switches. Closing the Preferences window with X (hide-instead-of-close per §3.1) left the WasapiCapture open → mic icon stayed in the system tray indefinitely. - Fix: hooked Window.IsVisibleChanged. When IsVisible=false → StopAudioMeter(). When IsVisible=true AND Audio tab is currently showing → StartAudioMeter() to resume. - Also added StopAudioMeter() to the OnClosing _allowClose path so app exit releases the mic too. StopAudioMeter now resets meter visuals (fill width + peak tick opacity) so a paused meter doesn't show stale levels on resume. ● LIVE indicator (privacy affordance — UI UX Pro Max progressive-disclosure) - Small mint dot + LIVE eyebrow shown next to "Microphone level" only while the WasapiCapture is open. Toggled in StartAudioMeter / StopAudioMeter. Test transcription — fully functional (restored from W1 placeholder) - State machine: Idle → Recording (5 s countdown) → Transcribing (spinner) → Result (transcript shown inline) or Error (red message + Retry). - Single button doubles as Cancel mid-flight (CancellationTokenSource). - Mic contention handled: StopAudioMeter() before AudioRecorder.StartAsync; StartAudioMeter() resumes after completion / cancellation IF window is still visible AND Audio tab is still showing. - MainWindow constructor now takes IAudioRecorder + IWhisperRunner (added to the App.xaml.cs DI wire-up). The active model is resolved via IModelManager.Resolve before the mic opens — fast-fail if the model is missing. - Result text rendered in a SurfaceInput-tinted Border with BodySmall typography; error text overrides Foreground to ErrorRed. - Temp WAV from AudioRecorder.StopAsync deleted after transcription (best-effort; IOException swallowed). - CA1001 suppression added to MainWindow with rationale (mirrors App's suppression — Window owns its lifecycle, _testCts disposed in OnClosing). Build: 0/0. Tests: 167/167. Smoke: clean launch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
History tab — hover-revealed row actions
- Per UI UX Pro Max convention for productivity data tables (Gmail / Notion /
Linear pattern): on row hover, the model + duration cells are replaced
with Copy + Delete Btn.IconGhost buttons. No permanently-visible button
clutter in the read-heavy table.
- Border MouseEnter / MouseLeave toggles the action StackPanel Visibility;
Background=Surface paints over the model+duration columns when shown.
- Right-click ContextMenu retained as the keyboard / power-user path. Both
paths now route through shared helpers CopyTranscriptToClipboard +
DeleteTranscriptAsync, eliminating duplicated try/catch blocks.
- Icons: Segoe Fluent Icons "Copy" (E8C8) + "Delete" (E74D). Delete icon
tinted ErrorRed.
Models tab — radio buttons replaced with state-driven action CTAs
- New row layout: 4 px left-edge accent strip + title row (name + Bundled +
ACTIVE badge if applicable) + state-driven button on the right.
- Five visual states per UI UX Pro Max state-clarity rule:
Active — MintTint card bg + mint accent + ACTIVE badge, no button
(action already performed — primary-action rule).
Installed — neutral card, no accent, "Use this model" Btn.Primary.
Not installed — neutral card, no accent, "Download" Btn.Secondary
(heavier commitment than primary).
Downloading — neutral card, mint accent, progress + percent + Cancel
Btn.Ghost (existing BuildModelDownloadingRegion reused).
Failed — neutral card, red accent, error text + Retry Btn.Secondary
(existing BuildModelErrorRegion reused).
- ACTIVE badge: small mint-tinted Border with dark "ACTIVE" text — pulls the
user's eye to the in-use model at a glance.
- Dead code removed: OnModelRadioChecked (radio gone), BuildModelStatusRegion
(replaced by BuildModelActionRegion). BuildActiveBadge marked static
(CA1822 compliance).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-facing: a styled ComboBox sits next to the Microphone row in the Audio tab. First entry is "Default device (follows Windows)"; remaining entries are every active capture endpoint enumerated via MMDeviceEnumerator. Selection persists as Audio.InputDeviceId in settings.json and takes effect immediately for the live meter, Test transcription, and live dictation. Wiring (no new layer dependencies): - IAudioRecorder gains SetInputDeviceId(string?). AudioRecorder holds the preferred id in a volatile field. StartAsync now goes through a ResolveCaptureDevice helper: look up the preferred id; if it's missing / inactive / not a capture endpoint, log a warning and fall back to the OS default. KusPus.Audio still doesn't reference KusPus.Persistence. - App.OnStartup pushes the initial id from PrefsStore + subscribes to PrefsStore.Changes to propagate further updates. Composition-root pattern. - MainWindow's level meter (separate WasapiCapture from AudioRecorder) gets the same ResolveLevelMeterDevice helper so the meter shows the picked device's levels, not the OS default's. Restarts on selection change. UI (Styles/Inputs.xaml + MainWindow.xaml + .xaml.cs): - New ComboBox.Surface style — SurfaceInput bg + BorderStrong border + 7 px radius matching the SegmentButton wrapper aesthetic. Fully restyled ToggleButton template (Fluent Icons chevron) and Popup template (dark/ light-themed Surface + DropShadowEffect) so the default WPF chrome doesn't leak through. Items use MintTint for the selected row + HoverSubtle for hover, matching the rest of the design system. - AudioDeviceTitle TextBlock removed; replaced by the ComboBox + a new AudioDeviceSubtitle that doubles as the error surface for "no mic" / "mic busy" states (writes to subtitle instead of overwriting the title). - PopulateInputDeviceCombo runs on tab open + every dropdown open — cheap enumeration picks up hot-plug USB mics without restarting the app. Combo selection matched to the persisted preference; falls back to "Default" silently if the saved id is no longer present. Build: 0/0. Tests: 167/167. Smoke: clean launch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n re-enum) Two root causes per Microsoft Learn "Optimize control performance" + dotnet/wpf#9881: 1. DropShadowEffect on the Popup's inner Border (BlurRadius=14) was the dominant cost — every dropdown open triggered a per-pixel blur pass. Removed; replaced with the existing BorderStrong stroke + Surface tint which read as elevation without the GPU work. 2. MainWindow.OnInputDeviceDropDownOpened was re-enumerating MMDevices via MMDeviceEnumerator.EnumerateAudioEndPoints on every open — a Win32 COM round-trip + a full ItemsSource rebuild + a visual-tree teardown. Removed the handler. Population now happens ONCE when the Audio tab opens (already wired). Hot-plugged devices appear on next tab visit, which is an acceptable trade-off vs the 150 ms perceptual lag every open. Belt-and-suspenders: ComboBox.Surface now declares VirtualizingStackPanel as its ItemsPanel + IsVirtualizing=True + VirtualizationMode=Recycling. Negligible for 5-10 mics but bombproof if someone has 20+ capture devices. Build: 0/0. Smoke: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tons Restructures the floating pill per the Organic Pill spec (Phase 1 — chrome only; halo, hue-drift, breath, hover-visualizer arrive in Phases 2-4). Geometry — dynamic window size - Collapsed: 200×56 (pill only). - Open / pinned: 320×78 (pill 320 wide + 22 px dock peek). Mica stays tight to the visible chrome so the area around the pill doesn't render a rectangular Mica frame. Window animates both Width and Height on hover. - Pill anchor stays on base width 200 so the position math (multi-monitor sticky, bottom-center default) doesn't drift center on expand. New chrome - Pin button (top-right corner of pill, 18×18). Hidden by default at -12° rotation. On pill hover: fades in + rotates to 0° (180 ms / 220 ms). Click toggles "pinned" — dock + corner buttons stay visible after the cursor leaves, glyph + bg tint to mint. - Magic-wand button (top-right, left of Pin, 18×18). Dormant — ToolTip "Refine text", no Click handler. We will wire it next iteration. - Dock drawer (22 px row below the pill, slides down + fades in on hover). Background matches the pill so the two read as one continuous chrome. Border CornerRadius=0,0,8,8 to share the pill's bottom rounding. Dock contents (left → right) - Record toggle (22×18). Red dot glyph. Click currently logs a TODO — the real wire-up needs a public AppCoordinator.ToggleTapMode() that doesn't exist yet; the hotkey chord remains the canonical entry point for v1. - Mic chooser (flex-grow). [mic icon] [device name] [chevron-down] on a subtle button bg. Click opens a real popup picker — a styled <Popup> containing a ScrollViewer + a StackPanel of per-device <Button>s. Click a device → SetInputDeviceIdAsync via the bridge → popup closes → label updates. Mint-tinted selected item. - Settings (22×18). Fluent gear, opens Preferences (existing wire). - Dismiss (22×18). Fluent X, red hover bg, calls _onClose → Shutdown. Layer-friendly bridges - FloatingPillWindow defines two tiny interfaces (IPrefsStoreBridge, IAudioRecorderBridge) and a SetBridges(prefs, audio) hook. App.xaml.cs implements them via PrefsStoreBridge (wraps IPrefsStore for the device id get/set) and AudioDeviceBridge (calls MMDeviceEnumerator). Keeps KusPus.App as the only layer that knows about both Persistence and NAudio. Removed - Old side-only hover-extend (ButtonPanel + AnimateWidth/AnimateButtonPanel). Replaced by the dock drawer + corner-button animation pair. Build: 0/0. Tests: 167/167. Smoke: clean launch + pill transitions to Idle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default idle (no hover) — unchanged: SVG voice-stack icon + "KusPus" wordmark,
just like today. The pill reads as a tiny brand mark when the user isn't
intentionally interacting with it.
On hover (still Idle) — swap to:
- 20-bar visualizer running the low-amplitude traveling-sine motion model
from the Organic Pill §3 idle-visualizer cue: amplitude 0.06-0.14,
per-bar phase offset 0.18 rad, damping k≈3.5/s, full traversal every
~2.4 s. Quiet and slow enough to disappear from peripheral vision.
- Label "IDLE · HOLD TO DICTATE" replaces "RECORDING" in the same slot.
State + hover form an orthogonal grid:
(Idle, !hover) → IdleContent (SVG + KusPus) · viz Off
(Idle, hover) → VisualizerContent (bars + IDLE) · viz HoverIdle
(Recording, *) → VisualizerContent (bars + RECORDING) · viz Recording
(other states, *) → that state's panel · viz Off
Refactor
- RecordingContent renamed to VisualizerContent (now serves both Recording
and HoverIdle modes — same Canvas, label swaps).
- New VisualizerLabel x:Name so the label text can change per mode.
- FadeContent + new ApplyIdleContent + small static FadeElement helper:
TransitionTo delegates idle-content rendering to ApplyIdleContent, which
re-evaluates IsMouseOver every time it's called.
- OnPillMouseEnter / OnPillMouseLeave call ApplyIdleContent so the swap
happens on every hover transition while in Idle.
- New VisualizerMode enum (Off / Recording / HoverIdle). OnVisualizerTick
switches motion math by mode — HoverIdle runs the sine wave; Recording
keeps the existing voice-envelope target-rolling; Off targets all bars
to 0.05 (silent).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Personality animations - Breath: ±0.6% scale pulse on PillSurface via ScaleTransform, 4 s sine cycle (2 s in + 2 s out, AutoReverse + RepeatForever, SineEase). Subtle enough to disappear from peripheral vision — gives the pill a "living organism" presence without intruding. - Hue drift: AccentBrush's middle gradient stop cycles mint #4DDBA6 → seafoam #4DCDC2 → soft cyan #4DB8DB → back over 14 s, constant R=0x4D band so perceived brightness stays flat (manual approximation of the spec's OKLCH constant-L=0.84/C=0.14 constraint; WPF has no native OKLCH). - Both wired as long-lived Storyboards (built once on Loaded, Begin/Stop via SetReduceAnimations) so toggling is cheap. Deferred to follow-up - Halo: needs a backbuffer larger than the pill bounds — incompatible with the current Mica setup (Mica would paint a rectangular tint around the halo area). Decision point: keep Mica + skip halo, OR drop Mica for AllowsTransparency=true + custom translucent gradient. - Heartbeat blink: depends on accent-line opacity which is state-driven (TransitionTo sets it per state). Multiplying onto state-driven base needs a layered opacity model — deferred until heartbeat semantics are pinned down. Accessibility toggle (new Settings.Privacy.ReducePillAnimations field) - New Accessibility section in Privacy tab: "Reduce pill animations" Toggle. Default off. Saves to settings.json on flip. - App.UpdatePillReduceAnimations combines the user toggle with SystemParameters.ClientAreaAnimation — if either says reduce, pill pauses personality animations (state transitions + dock slide remain active). - Initial state applied at startup + on every PrefsStore.Changes emit. Build: 0/0. Tests: 167/167. Smoke: clean launch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per user audit feedback that the bars should echo the icon.svg's pearly- mint gradient. Dark theme: unchanged — solid #EBFFFFFF SolidColorBrush (the historical token). A mint gradient over a dark pill surface would lose the visualizer's "voice on top" reading. Light theme: three-stop vertical LinearGradientBrush, alpha climbs top→ bottom so each bar reads as "lit from below": 0.0 → #664DDBA6 (subtle mint, 40% alpha) 0.5 → #994DDBA6 (mid mint, 60% alpha) 1.0 → #CC1F8762 (deeper mint, 80% alpha — bottom anchors) Implementation: VisualizerBarActive is removed from the ThemeTokens.Map dictionary and installed via a dedicated BuildVisualizerBarActive(mode) helper alongside the existing BuildPillSurfaceGradient. ThemeTokens.Apply now calls both special-case builders after the simple-Color-pair loop. The bars in FloatingPillWindow use SetResourceReference for their Fill, so the swap fires on theme flip with no other code touching needed. Build: 0/0. Smoke: clean launch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… theming, inset) 1. Center-expand on hover — Width + Left animate together (Left -= ΔW/2) so the pill grows symmetrically instead of right-only. 2. Height recovery on dock close — DoubleAnimations use FillBehavior.Stop and on Completed call BeginAnimation(prop, null) + SetValue(prop, to), freeing the animated values so the pill collapses cleanly with no black gap underneath. 3. Mic picker now design-system styled — Popup uses Surface/BorderStrong tokens with a 4-px-padded ScrollViewer (PanningMode=VerticalOnly, HorizontalScrollBarVisibility=Disabled). Item template adds a hover trigger that paints HoverSubtle on each row. 4. Picker pins the dock open — _pickerOpen flag gates OnPillMouseLeave so the dock stays open while the picker popup is open; OnMicChooserPopupClosed restores normal hover behavior afterward. 5. Light-theme pill carries the icon's mint — BuildPillSurfaceGradient light stops shift from #F8F8FA/#EEEEEF2 to #F4F8F4/#E0F0E6 (subtle top shift + slightly mintier bottom), echoing icon.svg's pearl-to-mint gradient without changing dark-theme look. 6. Dock visually narrower than pill — DockDrawer carries Margin="24,0,24,0" so it reads as a nested sub-element instead of a flush continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Black strips beside dock — DockDrawer.Margin removed. The pill window is AllowsTransparency=False because Mica (DWMWA_SYSTEMBACKDROP_TYPE) requires it, so any inset between the dock and the window edge renders opaque window-background black instead of click-through. The prior 24px margin was the "narrower than pill" aesthetic from the last batch — reverting it here since the side-effect (black strips) is worse than the cohesive look. 2. Pill mic-picker lag — cache the device list in FloatingPillWindow. On SetBridges we warm the cache via Task.Run + Dispatcher.BeginInvoke; each subsequent OnMicChooserClick reads from cache (instant) and fires a background RefreshMicCacheAsync so hot-plugged devices appear on next open. Same root cause as the audio-tab combo lag fixed in f4d2413: MMDeviceEnumerator .EnumerateAudioEndPoints is a synchronous Win32 COM round-trip (~150ms). UpdateMicChooserLabel uses the same cache fall-through. 3. Audio tab loading lag — OpenAudioTabAsync runs the heavy init off the dispatcher. EnumerateInputDeviceItems (COM) and the WasapiCapture ctor (driver shared-mode negotiation, ~150-500ms on some hardware) both await Task.Run, then the combo's ItemsSource is set + StartRecording fires on the UI thread. The Audio panel paints immediately; the device combo + LIVE meter populate as each piece completes. Surface kept stable: synchronous StartAudioMeter() façade still exists so the 3 non-tab-open callers (visibility change, mid-test resume, device-change restart) read unchanged. Build: 0/0. Smoke: pill places + hook installs cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-spec rewire of the pin semantics. Previously "latch dock open"; now
"compact mode" — clicking pin contracts the pill back to 200×56, slides
the dock back, but keeps the pin button visible at all times so the user
can unpin.
Behavior matrix:
Pinned OFF (default):
hover → expand 200→320, slide dock down, fade pin+wand in
leave → contract, slide dock up, fade pin+wand out
pin click → enter pinned + contract immediately (if already expanded)
Pinned ON:
hover → swap SVG+wordmark → visualizer+IDLE label (NO resize, NO dock)
leave → swap visualizer → SVG+wordmark (NO resize, NO dock)
pin click → exit pinned; if currently hovered, expand back to hover view
pin button stays visible the entire time (mint-tinted)
Implementation:
- OnPinClick — inverted: becoming pinned calls CloseDock; becoming unpinned
+ hovered calls OpenDock. Unpinned + not-hovered stays put.
- OnPillMouseEnter/Leave — gate OpenDock/CloseDock on !_isPinned so hover
doesn't trigger the expand/contract while pinned. ApplyIdleContent still
runs in both branches so the content swap (SVG ↔ visualizer) works.
- AnimateCornerButtons — effectiveVisible = visible || _isPinned. Keeps
the pin button at opacity=1 and angle=0 while pinned regardless of what
the caller asked for.
Plus: idle KusPus wordmark now Mint instead of MutedText — picks up the
brand accent so the resting pill carries the product's color cue.
Build: 0/0. Smoke: pill places + hook installs clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three landed-together changes for the tray + pill experience.
1. Pill record-toggle wired (Cluster A)
- SetRecordToggleAction(Action) on FloatingPillWindow; App binds to
AppCoordinator.ToggleFromTray. Per user spec the toggle does NOT
auto-capture a foreground target — the post-transcribe paste lands
wherever focus happens to be at the time.
- On toggle-start a RecordNudgePopup balloon appears above the RecordButton
("Click into your text field") for 6s. Auto-dismisses when state moves
to Recording. Previous 3s window was too short to read — user feedback.
- RecordGlyph changed from Ellipse to Rectangle that morphs dot ↔ rounded
square depending on FSM state.
2. Custom WPF tray right-click menu (Cluster B)
Replaces WinForms ContextMenuStrip with TrayMenuWindow.xaml — a
borderless, transparent, design-system-styled popup matching
Tray_light.png / Tray_dark.png:
- KusPus header with state-aware "Version 1.0.0 · {Idle|Recording|Transcribing}"
- Toggle recorder row with hotkey keycap (live-bound to PrefsStore.Hotkey)
- Active model: <name> row with chevron, opens models tab
- Preferences… opens general tab
- History… opens history tab
- Quit in ErrorRed
Shows at cursor on NotifyIcon.MouseClick (right). Closes on Deactivated
(focus lost) or any item click. WS_EX_TOOLWINDOW so it's hidden from
Alt-Tab/taskbar.
3. State-aware tray icons (Cluster C)
icons/icon-{idle,recording,error}.svg generated to .ico via tools/IconBuilder.
Recording overlays a red dot + glow on the bars; Error overlays a red
warning triangle. TrayManager subscribes to AppCoordinator.State and
swaps NotifyIcon.Icon based on the FSM state (treating a failed PostPaste
snapshot as Error for its hold duration). All three .ico files are
Resources in KusPus.App.csproj.
Build: 0/0. Smoke: pill places + tray icon visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three concerns folded into one commit because they share the pill XAML/code-behind:
1. UX pass (per user spec):
- Pill record button + tray menu both labelled "Toggle Recording [BETA]"
(verb-form + dogfood expectation-setting). Tray chip is mint-coloured.
- Magic wand is dormant — rendered at 0.35 opacity with Arrow cursor +
tooltip "Refine text — coming soon" so the disabled state is legible
visually, not just in the tooltip.
- Pin semantics extended: now also locks the pill's screen position. Drag
short-circuits when _isPinned. Drag cursor (SizeAll) flips to Arrow when
pinned so the lock is telegraphed.
- Added CompactRecordButton at the pill's top-LEFT corner, visible only
when pinned. Pinned mode hides the dock, so without this the user would
have to unpin just to record. Sits opposite Pin/Wand on the right for
visual balance.
2. Nudge bug fix (the 6s timer was a red herring — TransitionTo's
"dismiss-on-Recording" rule was firing within ~ms of click since the FSM
moves to Recording immediately after ToggleFromTray. Dropped that rule;
timer bumped 6s→10s as the sole dismissal path. Comment explains why.
3. Full UX audit pass (10 items from the 2026-05-17 self-audit):
- #1 CompactRecord 22×18 r=5 → 18×18 r=4 (matches Pin/Wand cluster)
- #2 Design-system icon size tokens added to Styles/Tokens.xaml:
Icon.Glyph=11, Icon.Chevron=9. Bound on Wand, Pin, Settings, Close,
MicChooser icon (was 10), MicChooser chevron (was 8).
- #3 MicChooser hover: Opacity=1.4 (silent no-op — WPF clamps at 1) →
Background=SurfaceElevated. Real, theme-aware lift.
- #4 Error text margin 6→8 px (matches Idle/Transcribing rhythm)
- #5 Dock vertical centering moved to parent Grid (Margin=6,2,6,2);
mic chooser drops its per-button vertical compensation.
- #6 Wand opacity 0.5→0.35 (clearer subordinate read)
- #7 Dropped the 1px PillInnerHighlight — only existed on the pill, not
the dock, creating a seam at the drawer junction.
- #8 PillSurface Cursor=SizeAll at rest (telegraphs drag), Arrow when pinned
- #9 Drop shadow: Direction=270 Depth=2 Blur=32 Op=0.45 →
Depth=0 Blur=14 Op=0.25. Omnidirectional soft halo, no dock bleed.
- #10 All chrome gutters standardized at 6 px (was 5 on corners, 6 on dock).
Build: 0/0. Smoke: clean (verified locally with real recording + paste).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three changes from the 2026-05-17 dogfood batch: 1. Default theme flipped "auto" → "dark" (AppSettings.UiSettings.Theme). Light theme is still in beta polish, so new installs land on the polished dark surface. DefaultSettingsTests assertion updated with rationale. 2. Preferences theme picker: "Light" → "Light [BETA]" with tooltip explaining the beta state. Sets dogfood expectations that light surfaces may not be fully tuned yet. 3. Onboarding step 6 (Try it) replaced fake SimulatedSentences random-pick with a real IAudioRecorder + IWhisperRunner pipeline. 5 s countdown recording → transcribe with active model → render actual transcript (or error if mic/model missing). Mirrors the existing Test Transcription pattern from the Audio tab. Threaded audio/whisper/models services through the OnboardingWindow constructor + both call sites (App.OnStartup + MainWindow.OnRerunOnboarding). Prior behaviour was misleading — onboarding "tested" dictation by picking a canned sentence from a hard-coded list, so a broken mic / missing model didn't surface until after onboarding finished. Now the failure modes surface during setup where the user can act on them. Build: 0/0. Core/Persistence/Whisper/Audio test suites all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User dogfood feedback (2026-05-17) asked for continuous "speak-pause-paste"
loop on top of the existing push-to-talk model. Researched 3 architectures
(sliding-window, chunk-on-VAD, library-binding); recommended chunk-on-VAD
+ second hotkey ("Option B") to keep the existing UX intact while giving
power users opt-in long-mode dictation.
Deferred to v1.2 per user choice — ~2 weeks build + 1 week dogfood, too
large for the current pre-v1 polish window. Entry captures full 8-cluster
plan, top-3 risks (hallucination on silence, mid-word VAD cuts, paste-into-
wrong-app race), realistic latency (~0.7-1 s per pause with tiny.en), and
the rejected-for-now soft-cap alternative.
Also clarified LT-07 (streaming partial results) as a distinct UX
hypothesis — sliding-window for visible live caption in the pill, NOT a
paste pipeline. Different architecture from R1.2-10; both can ship in
principle but R1.2-10 lands first because it answers a real dogfood ask.
Per CLAUDE.md, this edit to docs/ROADMAP.md is authorized — user
explicitly said "keep it in the roadmap for later versions".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR bundles a broad set of UI polish, reliability, and tooling changes for the KusPus Windows dictation app: it adds a Sentry-based crash reporter with a strict egress allowlist and PII scrubbing, replaces the default tray context menu with a custom WPF window, introduces an onboarding window, fixes a Start-menu-popping bug in the LWin hotkey handling, pins the Hugging Face model commit/hashes, flips the default UI theme to dark, and adds a one-shot IconBuilder tool for generating multi-resolution .ico files from SVG.
Changes:
- Bug fix + matching test/doc updates:
HotkeyEngineno longer consumes the LWin keyup (prevents Win-stuck-down state that turnedSendInput(Ctrl+V)intoWin+Ctrl+V). - New crash/telemetry plumbing:
CrashScrubber,EgressPolicy,EgressAllowlistHandler,CrashReporter, plus comprehensive unit tests. - UI/UX work: custom
TrayMenuWindow,OnboardingWindow, theming/token refactors, default theme flipped todark, pinned Whisper model metadata, and a newtools/IconBuilderproject.
Reviewed changes
Copilot reviewed 44 out of 56 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/KusPus.Native/HotkeyEngine.cs |
Stops consuming LWin keyup; injects Ctrl mask-key tap only. |
test/KusPus.Native.Tests/HotkeyEngineTests.cs |
Test renamed and assertion flipped to match new LWin behavior. |
src/KusPus.Core/Telemetry/CrashScrubber.cs |
New scrubber for sensitive keys, paths, and usernames. |
test/KusPus.Core.Tests/Telemetry/CrashScrubberTests.cs |
Coverage for sensitive keys, path prefix rewriting, username redaction. |
src/KusPus.Core/Networking/EgressPolicy.cs |
HTTPS allowlist for huggingface.co and *.ingest.*sentry.io. |
test/KusPus.Core.Tests/Networking/EgressPolicyTests.cs |
Allow/deny coverage including lookalike-host and offline-mode cases. |
src/KusPus.Core/Settings/AppSettings.cs |
Default theme switched to dark; adds ReducePillAnimations. |
test/KusPus.Core.Tests/DefaultSettingsTests.cs |
Asserts new dark default. |
src/KusPus.Whisper/Resources/models.json |
Pins HF commit, file sizes, and SHA-256 hashes for all models. |
src/KusPus.Core/State/CoordinatorSnapshot.cs |
Adds PostPasteInfo. |
src/KusPus.Audio/IAudioRecorder.cs, AudioRecorder.cs |
Adds SetInputDeviceId and device resolution path. |
src/KusPus.App/TrayManager.cs, TrayMenuWindow.xaml(.cs) |
New custom WPF tray menu replacing default context menu. |
src/KusPus.App/OnboardingWindow.xaml(.cs) |
New first-run onboarding window with hotkey picker. |
src/KusPus.App/CrashReporter.cs, EgressAllowlistHandler.cs |
Wires Sentry through the egress allowlist and scrubber. |
src/KusPus.App/AppCoordinator.cs, App.xaml(.cs) |
Composition updates for new subsystems and settings. |
src/KusPus.App/Styles/*.xaml, ThemeTokens.cs, ThemeApply.cs, FloatingPillWindow.xaml |
Theme token refactor and UI polish. |
src/KusPus.App/AutostartRegistry.cs |
Autostart registration helper updates. |
tools/IconBuilder/Program.cs, IconBuilder.csproj |
New SVG→multi-resolution .ico build tool. |
icons/*, docs/ROADMAP.md, docs/PILL_DESIGN.md, CLAUDE.md |
New SVG assets and documentation updates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private static System.Drawing.Icon LoadIcon(string resourceName) | ||
| { | ||
| // Resolve the WPF Resource via pack URI and hand the stream to | ||
| // System.Drawing.Icon. resourceName is the per-state file name (e.g. | ||
| // "icon-recording.ico"). Falls back to the system app icon if the | ||
| // resource is missing — defensive, shouldn't fire in a normal build. | ||
| var info = System.Windows.Application.GetResourceStream( | ||
| new Uri($"pack://application:,,,/{resourceName}")); | ||
| if (info is null) | ||
| { | ||
| return System.Drawing.SystemIcons.Application; | ||
| } | ||
| using var stream = info.Stream; | ||
| return new System.Drawing.Icon(stream); | ||
| } |
| private MMDevice ResolveCaptureDevice(MMDeviceEnumerator enumerator, string? preferredId) | ||
| { | ||
| if (!string.IsNullOrEmpty(preferredId)) | ||
| { | ||
| try | ||
| { | ||
| var device = enumerator.GetDevice(preferredId); | ||
| if (device is not null && device.State == DeviceState.Active && device.DataFlow == DataFlow.Capture) | ||
| { | ||
| return device; | ||
| } | ||
| device?.Dispose(); | ||
| _logger.LogWarning( | ||
| "Preferred input device {Id} not active/capture; falling back to OS default.", | ||
| preferredId); | ||
| } | ||
| catch (COMException ex) | ||
| { | ||
| _logger.LogWarning(ex, | ||
| "Preferred input device {Id} could not be resolved; falling back to OS default.", | ||
| preferredId); | ||
| } | ||
| } | ||
| return enumerator.GetDefaultAudioEndpoint(DataFlow.Capture, Role.Console); | ||
| } |
| private static System.Drawing.Icon LoadIcon(string resourceName) | ||
| { | ||
| // Resolve the WPF Resource via pack URI and hand the stream to | ||
| // System.Drawing.Icon. resourceName is the per-state file name (e.g. | ||
| // "icon-recording.ico"). Falls back to the system app icon if the | ||
| // resource is missing — defensive, shouldn't fire in a normal build. | ||
| var info = System.Windows.Application.GetResourceStream( | ||
| new Uri($"pack://application:,,,/{resourceName}")); | ||
| if (info is null) | ||
| { | ||
| return System.Drawing.SystemIcons.Application; | ||
| } | ||
| using var stream = info.Stream; | ||
| return new System.Drawing.Icon(stream); | ||
| } |
User dogfood ask: let the user pick their mic during onboarding (not just see the meter for the OS default), and persist that choice until they change it from Preferences. Step 3 gets an OnbInputDeviceCombo above the live meter card. It writes to PrefsStore.Audio.InputDeviceId — the same field Preferences → Audio uses — so the selection survives onboarding-exit and stays put until the user changes it from either surface. ResolveOnbMicDevice mirrors MainWindow.ResolveLevelMeterDevice: looks up by saved id, falls back to the OS default if the device is unplugged. SelectionChanged restarts the meter capture so the user sees the level for whichever mic they just picked. No shared base class with MainWindow's combo — onboarding is short-lived and a single helper would pull in more ceremony than it removes. Logic is a faithful mirror; if a future refactor extracts a shared HotkeyPickerControl / InputDevicePickerControl UserControl, this and the MainWindow combo + the Audio-tab one would all collapse to one consumer. Also updated CLAUDE.md "Deviations" with 11 new entries covering this session's UX work (pin = compact mode + position lock, BETA labels, tray menu redesign + state-aware icons, dark default theme, real onboarding dictation, mic chooser in onboarding, icon-size tokens, shadow softening, mic-chooser hover fix, roadmap R1.2-10 entry). Build: 0/0. Smoke: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(hotkey): don't consume LWin keyup — kept Win stuck-down in OS state
Consuming the LWin keyup left Windows thinking Win was still held, so
PasteEngine's SendInput(Ctrl+V) read as Win+Ctrl+V (Action Center / Quick
Settings) and every subsequent keystroke became a Win+key system shortcut.
The Ctrl-tap injection (AHK #MenuMaskKey idiom) still runs to suppress the
Start menu; we just let the real LWin keyup reach the OS so its key-held
state clears.
Spec §13 prescribes the old (buggy) behavior; flagged for revision in
CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 8 pill polish: full PILL_DESIGN.md + drag + multi-monitor sticky
Surface (docs/PILL_DESIGN.md §1, §3):
- 200×56 with 8 px DWM-rounded corners, Mica backdrop on Win11 22H2+
(DWMWA_SYSTEMBACKDROP_TYPE = DWMSBT_TRANSIENTWINDOW), dark-tinted via
DWMWA_USE_IMMERSIVE_DARK_MODE. Falls back to the §3.1 dark gradient
on older Windows.
- §3.3 1 px hairline border + drop shadow + inner top highlight.
Five-state machine (§2):
- Recording: 20-bar visualizer + RECORDING micro-label.
- Transcribing: 14 px ¾-arc spinner (0.9 s loop, rotated via direct
BeginAnimation on the RotateTransform) + "Transcribing…" text.
- Confirmed: "Pasted into <App>" with the bold app name, 1 s hold.
- Error: 5 px red dot + reason text, 2 s hold, instant accent shift to
red (§5).
- Idle: PRD G4 dev override — pill stays visible between dictations
showing the app icon + "KusPus" label. Will revert to spec §6.1
hidden-when-not-in-use once Settings exposes the close path.
Visualizer (§4):
- 20 bars × 3 px wide × 4 px gap × 4–26 px tall (136 px track).
- Damped target/value motion model per §4.2: center-weighted speak
envelope, per-bar damp rates, real audio levels from IAudioRecorder
override the simulation when present. Runs on CompositionTarget.
Rendering for display-refresh smoothness.
Accent line (§3.4):
- 136 × 1.5 mint gradient with glow, opacity per state.
Motion (§5):
- 120 ms pill appear/disappear, 150 ms content crossfade between
states. Cubic easing.
Hover-extend override (PILL_DESIGN.md §10):
- Width animates 200 → 280 over 150 ms on hover, Settings + Close
buttons fade in. WS_EX_TRANSPARENT intentionally NOT applied
(overrides §1.2 click-through) so buttons work; WS_EX_NOACTIVATE
preserved so focus doesn't move.
Draggable pill (beyond spec):
- MouseLeftButtonDown anywhere on the pill body → DragMove. Skips
when click is on a Button (Settings / Close).
- Session-only per-monitor remembered positions via
Dictionary<deviceName, Point> keyed by MONITORINFOEX.szDevice.
Cleared on every fresh process start.
Multi-monitor option C (hybrid sticky):
- On state transition into Armed/Recording, jump to the foreground
window's monitor at its remembered position (or default
bottom-center if first time). No-op when pill is already on the
right monitor or while user is dragging.
Coordinator snapshot extension:
- CoordinatorSnapshot.PostPaste:PostPasteInfo carries (Pasted,
TargetApp, ErrorReason). AppCoordinator emits one post-paste
snapshot from DeliverAsync / HandleFailureAsync so the pill knows
whether to show Confirmed or Error.
Icon pipeline:
- tools/IconBuilder: one-shot SVG → multi-frame ICO converter.
- icons/icon.svg as the single source of truth. ViewBox tightened to
"272 246 480 480" so the 5-bar content fills ~94 % of every
rendered frame (tray, taskbar, Task Manager, .exe icon).
- icons/icon.ico regenerated, embedded as ApplicationIcon + WPF
Resource. TrayManager loads via pack URI.
- SharpVectors.Wpf 1.8.5 renders the SVG directly inside the pill
Idle state — no hand-converted XAML to drift from the source.
Deferred from spec (not blockers):
- §3.2 light theme + WM_SETTINGCHANGE live switching.
- §3.4 accent variants beyond Mint (needs Settings UI from Phase 9).
- §5.3 reduced-motion gating of fades.
- Win10 acrylic fallback.
Tests: 117/117 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 9: MainWindow + 6 tabs + theming + pill flips with theme
Surface:
- New MainWindow per docs/APP_DESIGN.md §3. 880×620 (820×600 min) system-
chromed window. Sidebar nav (6 RadioButton tabs styled per §3.2 — mint
stripe + elevated bg on select). Close hides (§3.1 / §8.5); only the
tray's Quit fully exits. Tray menu gains "Preferences…" → MainWindow.
- Dark title-bar tint via DwmSetWindowAttribute DWMWA_USE_IMMERSIVE_DARK_MODE
+ SetWindowPos(SWP_FRAMECHANGED) to force the non-client repaint on
runtime theme flips (validated against Microsoft Q&A "DWMWA_USE_
IMMERSIVE_DARK_MODE won't update").
Six tabs (§3.3):
- General: Hotkey hero card with live listen-mode rebind (suspends LL hook,
captures held-keys snapshot, commits on full release, conflict warning
against known Windows shortcuts), Startup toggle → HKCU\Run, Appearance
Auto/Light/Dark segmented control.
- Audio: device label + Discord-style 200×6 track+fill meter (validated
fix against naudio/NAudio#160 #347 #507 — MMDevice.AudioMeterInformation
reports zero without an active capture session, so we open a WasapiCapture
and compute peak from samples in DataAvailable). Peak-hold tick that
decays slower than fill.
- Models: active-model row + manifest list with install state (file
existence per device), radio-select writes ActiveModelId to PrefsStore;
download wiring deferred to Phase 11.
- History: last 50 transcripts via IHistoryStore.SearchAsync; status dot
(mint = ok, red = failed), relative time, app name, model + duration.
- Privacy: offline + crash-reports toggles to PrefsStore, logs path +
Open in Explorer, local-first mint promise card.
- About: 80px brand mark + version line (AssemblyInformationalVersion) +
Cascadia-Mono build line, Resources card group (GitHub link + logs +
Re-run onboarding placeholder), MIT/local-first license blurb.
Theming infrastructure:
- ThemeApply (new) resolves "auto"/"light"/"dark" against AppsUseLightTheme
registry, applies DWM dark-mode + SWP_FRAMECHANGED.
- ThemeTokens (new) — 23-entry map of (dark, light) Color pairs covering
AppBg, Sidebar, Surface, SurfaceElevated, BorderSubtle/Strong/Divider,
Primary/Secondary/Muted/DisabledText, HoverSubtle, KeycapBg/Border,
Mint/MintTint/MintBorder, ErrorRed, WarningAmber/Tint/Border, plus
pill-specific PillBorder/PillInnerHighlight/VisualizerBarActive/Idle
and MeterTrack/ButtonHoverBg. Plus a LinearGradientBrush builder for
the pill's two-stop surface gradient.
- ThemeTokens.Apply uses REPLACEMENT (not mutation) — WPF freezes
Freezable resources in Application.Resources (x:Shared semantics) so
brush.Color mutation throws InvalidOperationException at startup.
Replacement fires ResourcesChanged; every {DynamicResource} consumer
re-resolves.
- MainWindow.xaml + FloatingPillWindow.xaml refactored end-to-end to
use {DynamicResource Token} for every brush/foreground/border. Code-
built UI (Models rows, History rows, hotkey keycaps) uses a Theme(key)
helper that returns the current resource brush; theme changes re-
render visible dynamic tabs so they pull fresh brushes.
- Pill visualizer bars use SetResourceReference(FillProperty) instead
of a frozen explicit brush so bars re-theme on switch.
Deviations from spec — flagged in CLAUDE.md (no MainWindow.xaml hex
literals migrated; everything is now token-based). Body theming for
the pill required new PillSurface gradient resource installed per-
theme. Multiple WPF parse-time gotchas worked around: IsChecked="True"
on TabGeneral triggers Checked event before content panels are bound
to fields — fixed with a _loaded guard in OnTabChecked.
Tests: 117/117 pass.
docs/APP_DESIGN.md: new authoritative UI spec from the user, referenced
from CLAUDE.md source-of-truth list. Where it conflicts with PILL_DESIGN
§2.1 (click-through) the §10 hover-extend override still wins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 10: Onboarding modal — 7 steps + first-launch trigger
OnboardingWindow per docs/APP_DESIGN.md §4. 720×520 chromeless rounded card,
12 px corners, draggable from the progress-dots header. Theme-aware via the
same ThemeApply + DynamicResource brushes that flip MainWindow / pill.
Seven steps (linear nav with Back / Next / Skip / Finish):
- 1 Welcome: stylized desktop preview + 3-column value-prop grid.
- 2 Hotkey: listen-mode + capture + commit to PrefsStore.Hotkey, with the
same Win+L-class conflict warning. Duplicates MainWindow's listen-mode
state machine (extract to UserControl when there's a third consumer).
- 3 Mic check: WasapiCapture on default device, Discord-style track+fill
meter, success/error variants, Open Settings → ms-settings:privacy-microphone.
- 4 Autostart: clickable ToggleCard → HKCU\Run via AutostartRegistry.Set.
- 5 Crash reports: ToggleCard + Local-first promise card.
- 6 Try it: 120-DIP transcript surface, "Simulate dictation" runs 1.8 s
Listening… then surfaces one of three canned sentences with mint border.
- 7 Done: corner-of-screen tray-diagram + Finish.
Finish path writes PrefsStore.Onboarding.Completed = true. Skip / Esc /
window-close leave Completed = false so the modal re-pops on next launch.
Re-runnable any time from About → "Run again" (replaces the disabled
Phase 9 placeholder button).
First-launch trigger in App.OnStartup: after _coordinator.Start(), queue
ShowDialog at DispatcherPriority.Background so OnStartup returns before
the modal's nested dispatcher frame begins. Pill + coordinator already
running by then, so the modal's hotkey picker can suspend the live LL
hook cleanly.
Deferred to a polish cluster: §4.1 dimmed-desktop backdrop, hotkey-picker
UserControl extraction, value-card hover states.
Tests: 117/117 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 11 + UX audit W1: crash reporter, egress killswitch, sidebar binding
Phase 11 (TECH_SPEC §19, PRD §10.2/§10.3)
- CrashReporter: Sentry init/shutdown gated on (CrashReportsOptIn && !OfflineMode).
Embeds project DSN (env-var override) and routes Sentry's own transport through
EgressAllowlistHandler so PRD §10.2 holds for SDK uploads too.
- EgressAllowlistHandler + EgressPolicy: v1 allowlist. Accepts huggingface.co and
regional Sentry ingest hosts (*.ingest[.<region>].sentry.io); Offline Mode and
non-HTTPS block everything. Pure policy decision in Core, IO handler in App.
- CrashScrubber: drops events whose Tags/Extra contain transcript/clipboard/text/
password/target_app/hwnd keys; replaces %TEMP%, %LOCALAPPDATA%, %APPDATA%,
%USERPROFILE% prefixes (start-anchored) and Environment.UserName occurrences
(mid-string) in messages, exception text, stack-frame paths, breadcrumbs.
- 30 new unit tests (167/167 total).
UI/UX W1 — "stop lying" (APP_DESIGN §13 audit findings)
- Crash Reports toggle visually disables when Offline Mode is on; subtitle swaps
to "Disabled while Offline Mode is on." The toggle's IsChecked is preserved.
- Replace stale "Phase X" + Win32 jargon copy across General/Audio/Models/About.
- Remove permanently-disabled "Test transcription" section; restore in W3 when wired.
- Sidebar footer bound live: status label from AppCoordinator snapshots, chord
glyph from PrefsStore (compact short form: "Ctrl+Win", not unicode soup).
- Inline [ESC] keycap in hotkey-listen hint (APP_DESIGN §13.4).
- Audit findings appended to docs/APP_DESIGN.md as §13 with a progress ledger.
Build: 0/0. Tests: 157 + 10 new CrashScrubber + extended EgressPolicy = 167/167.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit W2: design-system extraction (typography, dot, button, focus, spacing)
APP_DESIGN.md §13.5 P1-6/-7/-1/-9/-8/-P2-5. No user-visible scope change —
this is pure styling consolidation. Five new shared style sets, four
inline-styled buttons replaced, one fragile negative-margin layout fixed.
New: src/KusPus.App/Styles/
- Typography.xaml — SectionHeader, Type.RowTitle, Type.RowSubtitle, Type.Eyebrow,
Type.MonoSm, Type.MonoXs, Type.Display, Type.WarningEmphasis. Replaces ~25
inline TextBlock FontFamily / FontSize / FontWeight / Foreground quadruplets
across MainWindow.xaml.
- Dot.xaml — Dot.Mint / Dot.Amber / Dot.Red ellipse styles with the spec's
7 px + 6 px coloured glow. Replaces 4 hand-rolled ellipses in MainWindow.xaml
+ 1 code-behind ellipse in the history row renderer.
- Buttons.xaml — Btn.Primary / Secondary / Ghost / Danger × Sm / Md / Lg per
APP_DESIGN §3.4. All buttons now share one template (border + content + hover
opacity ladder + disabled-state); inline Padding / Background / Foreground /
BorderThickness gone from every call site.
- Focus.xaml — Focus.Mint: 1.5 px mint dashed outline at 2 px inset. Wired into
SidebarTab, Toggle, SegmentButton, and Btn.Base — keyboard-nav now has a
visible focus ring that reads as a design choice rather than the WPF default
dotted-Aero ring.
Modified: src/KusPus.App/MainWindow.xaml
- All TextBlock declarations matching repeated patterns use a Style key.
- All Buttons use Btn.Secondary (only kind currently needed; the rest of the
set arrives when W3's purge / download flows land).
- ConflictRow refactored: wraps HotkeyCard + ConflictRow in a single 440 px
StackPanel with bottom Margin 28. ConflictRow gets `Margin="0,1,0,0"` (1 px
gap below HotkeyCard) instead of the previous `Margin="0,-20,0,28"` negative-
margin tuck. Section gap now lives on the parent, not on each child.
Modified: src/KusPus.App/App.xaml
- Application.Resources now merges the four Styles/*.xaml dictionaries so
every keyed style is reachable app-wide (including future OnboardingWindow
reuse).
Build: 0/0. Tests: 167/167 still green. Smoke: clean launch.
P1-10 (per-tab UserControl extraction) deferred to a post-W3 cleanup — doing
it now would mean reshuffling every W3 addition into newly-created files. The
ledger in docs/APP_DESIGN.md §13.5 reflects this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit W3: missing UX — history search/purge, log clear, model downloads, contrast
APP_DESIGN.md §13.5 P0-3, P0-5, P1-2, P1-3, P1-4. Ships the four user-facing
gaps the audit flagged. All wired against existing services — no new layers.
P0-3 — DisabledText contrast
- ThemeTokens.cs: dark #5CFFFFFF → #80FFFFFF (~3.0:1 → ~5.1:1 vs AppBg).
- Light #50141414 → #A0141414 (~1.9:1 → ~4.7:1 vs AppBg).
- Both clear WCAG AA normal-text 4.5:1. Used for hotkey hint, model "Not
installed" label, disabled buttons.
P0-5 — Mic-active disclosure (already in W1)
- The Audio "Live level" subtitle reads "Active only while this tab is open.
Audio is never recorded." Tracking the ledger to "done" — no code change.
P1-3 — Privacy Logs row
- Two-row card: "Log size · {size}" with Clear logs ghost-danger button, then
"Log folder · {path}" with Open in Explorer secondary button.
- RefreshLogsSize enumerates LOCALAPPDATA\KusPus\logs\*.log on Loaded.
- OnClearLogsClick confirms via MessageBox (No default), then File.Delete each
*.log. Today's open log is held by Serilog's FileSink — skipped without
error, count reported in log.
- FormatBytes handles bytes / KB / MB.
P1-2 — History search + bulk footer
- Search box at top with Segoe Fluent icon, placeholder overlay, clear "×"
button. 250 ms DispatcherTimer debounces TextChanged → HistoryStore.SearchAsync
(FTS5 backing). Empty query reverts to "most recent first".
- Footer above 1px divider: live row count, "{n} matches for '…'" when filtered,
Purge all history Btn.Danger with MessageBox confirm. Calls HistoryStore.PurgeAllAsync.
- PurgeAllButton.IsEnabled gates on row count > 0 || query is not null.
P1-4 — Models download flow
- Per-model state machine in _modelDownloads dictionary keyed by id.
- BuildModelStatusRegion dispatches on state: Active / Installed / Downloading
(180 × 4 px mint progress bar + percent in mono + Cancel ghost) / Error
(red message + Retry secondary) / Not installed (Download secondary).
- OnModelDownloadClick: pre-checks OfflineMode (clearer message than letting
EgressAllowlistHandler throw mid-stream), spawns Task.Run with cancellable
cts. IProgress<DownloadProgress> marshals to UI thread, throttled to 0.5 %
steps so the StackPanel rebuild doesn't dominate CPU on a fast link.
- OnModelCancelClick: cts.Cancel(). Completion continuation handles cleanup
for both cancellation (silent) and failure (sticky error + Retry).
- ShortenDownloadError strips ModelManager's "HTTP error downloading …:" prefix.
Build: 0/0. Tests: 167/167.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX follow-up: History as a table · pill Settings → Preferences · spacing rhythm
History tab — table layout (audit follow-up)
- Replace card-list rendering with a Grid-based table: header row in
Type.Eyebrow style above, 6-column body rows below. Columns: status (14) /
time (78) / app (110) / transcript (*) / model (72) / duration (52).
- Per the skill's number-tabular rule: time, model, duration use Cascadia
Mono so columns stay aligned across rows.
- Per truncation-strategy: transcript and app truncate with ellipsis +
ToolTip exposing the full text on hover. Time column ToolTip shows the
full timestamp.
- Per gridline-subtle: 1 px BorderDivider between body rows, 1 px
BorderSubtle between header and body. No row striping.
- Row hover: HoverSubtle background via Style trigger on the HistoryRow
Border (Cursor=Hand for affordance).
- Right-click context menu per row: "Copy text" (Clipboard.SetText) and
"Delete" (HistoryStore.DeleteAsync + ReloadHistoryAsync).
- ShortModelId strips "ggml-" prefix to match the sidebar's compact form.
- Failed transcripts: red dot + italic red transcript column; rest of the
row stays normal so the failure mode reads as one cell, not the row.
Pill Settings button wired to Preferences modal
- Add SetSettingsAction(Action) to FloatingPillWindow, mirroring
SetCloseAction's pattern.
- App.OnStartup wires it to _mainWindow.ShowOn("general") AFTER MainWindow
is constructed (the existing pill setup runs before MainWindow exists).
- Tooltip "Settings — coming soon" → "Open Preferences".
Spacing rhythm normalization
- Models tab: list bottom margin 12 → 16 (align with the 8-grid inter-block
rhythm; "16 = block gap" is now consistent across History search bar and
Models list).
- About tab: header StackPanel bottom 32 → 28 (matches the section gap
rhythm used everywhere else, instead of being one-off heavier).
- History footer: top margin 18 → 16 (16 is the canonical block gap).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit Round 2: tokens, surfaces, expanded typography, History single-card
Parallelised Phase 1 (5 sub-agents wrote disjoint style files concurrently),
then serial Phase 2 sweep of MainWindow + App.xaml + APP_DESIGN.md.
NEW style files (Phase 1, parallel)
- Styles/Tokens.xaml — Space.xs..xxl + Pad.Tight/Default/Hero + Radius.Sm/Md/Lg
- Styles/Surfaces.xaml — Surface.Default/Hero/Tight/Warning/Mint (5 Border styles)
- Styles/Inputs.xaml — Input.Search TextBox style
- Styles/Typography.xaml extended — 10 new Type.* roles, dead Type.MonoXs removed
- Styles/Buttons.xaml extended — new Btn.IconGhost; header docs call-site inventory
Phase 2 — App.xaml wires the 3 new dictionaries (Tokens first so others can
reference its tokens), then sweep MainWindow.xaml + MainWindow.xaml.cs:
MainWindow.xaml migrations
- Hotkey card Border → Surface.Hero (drops inline Padding 22,20 override)
- ConflictRow Border → Surface.Warning (collapses 5 inline attrs)
- Local-first Border → Surface.Mint (collapses 5 inline attrs)
- HotkeyHint TextBlock → Type.HintItalic
- ConflictText TextBlock → Type.WarningBody
- AboutVersion TextBlock → Type.Body
- AboutBuildLine Margin 0,4,0,0 → 0,3,0,0 (matches Type.RowSubtitle rhythm)
- Local-first head TextBlock → Type.MintHeadline
- Local-first body TextBlock → Type.BodySmall
- MIT licensed TextBlock → Type.Footnote
- "Press a hotkey" TextBlock → Type.HintItalic
- StatusLabel TextBlock → Type.SidebarStatus (was Type.MonoSm-with-override misuse)
- History search bar magnifier → Type.IconSm
- History search box TextBox → Input.Search
- History search clear Button → Btn.IconGhost
- About re-run card Margin 0,0,0,32 → 0,0,0,28 (matches section gap)
- Sidebar footer Grid Margin 18,8,18,14 → 14,8,14,14 (matches sidebar 14)
History tab — unified single composed card (Q3 from user audit decisions)
- Outer RowCard Padding="0" wraps a StackPanel of inner Borders.
- Search bar (Padding 14,8, bottom 1 px divider), table header (Padding 14,10,
bottom 1 px divider), HistoryList (HistoryRow style provides per-row bottom
divider), bulk footer (Padding 14,12, no top border — last row's bottom
border IS the separator → no double line).
- Reads as one "history widget" instead of four separately-styled blocks.
MainWindow.xaml.cs code-behind sweep
- New TypeStyle(string) helper (mirrors Theme()) to pull Type.* styles from
Application.Resources for code-built TextBlocks.
- BuildModelRow title/subtitle → Type.RowTitle / Type.RowSubtitle
- BuildBundledBadge child TextBlock → Type.BadgeMint
- BuildModelDownloadingRegion percent → Type.MonoSm
- BuildModelErrorRegion error text → Type.ErrorInline
- BuildHistoryRow TIME / MODEL / DUR columns → Type.MonoSm (transcript + app
columns + Installed/Active status stay inline because Foreground flips per
state — no single Type.* role covers both colour states).
- Empty-state TextBlock in ReloadHistoryAsync → Type.HintItalic
- Dropped unreachable "(none)" branch in RenderModelsTab (audit P2-8).
Docs: APP_DESIGN.md §13.5 ledger updated (P2-8/-9 done, P2-10 won't-fix with
reason); new §13.6 documents the Round 2 work — token system, surface variants,
typography role catalogue, inputs/IconGhost, spacing fixes, History
unification, code-behind cleanup, dead-code policy.
Parallelisation safety
- 5 sub-agents in Phase 1 each owned exactly one file (disjoint writes — no
lost-update risk). None ran dotnet build (avoids bin/obj corruption). The
orchestrator does all building serially in Phase 3 after killing any running
KusPus.exe to avoid output-DLL locks.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* About tab: byline + 4 social icons · Models/About card spacing · 1px→8px
Author byline (About → bottom-right)
- "Made by Devang Kumawat" + LinkedIn / X / GitHub / Portfolio icon row at
the bottom-right of the About tab, sitting directly on AppBg (no card —
reads as a personal touch, not part of the design-system surface inventory).
- Text uses Type.Footnote (12 Medium SecondaryText) — matches the existing
"MIT licensed" line on the left. Per UI UX Pro Max rule weight-hierarchy:
text carries the byline weight, icons are visually subordinate.
- Each icon: 14×14 Viewbox inside a Btn.IconGhost (28×28 click target).
Aspect ratio locked by the Viewbox's default Uniform Stretch and the
underlying 24×24 viewBox.
- Theme tinting: Fill="{DynamicResource MutedText}" (filled paths) or
Stroke="{DynamicResource MutedText}" (Lucide globe) — no per-theme assets,
one shared rendering for dark+light.
Icon sources (saved to icons/social/ + LICENSE.md attribution)
- LinkedIn / X / GitHub: Simple Icons (CC0) via jsDelivr simple-icons@v11
and raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/.
- Portfolio (globe): Lucide (ISC) from raw.githubusercontent.com/lucide-icons/lucide.
- Saved as .svg files for documentation/license tracking; actual rendering
inlines the path data in MainWindow.xaml so the fill binds to theme tokens
(SharpVectors SvgViewbox can't easily theme-tint).
Each icon button OpenUrl(...) → Process.Start with UseShellExecute=true.
Single helper handles Win32Exception + FileNotFoundException for missing
default browser without crashing.
Links wired
- LinkedIn → https://www.linkedin.com/in/devangk003/
- X → https://x.com/devang_kumawat
- GitHub → https://github.com/devangk003
- Portfolio → https://lnk.bio/devangk003
Spacing (user audit feedback — 1 px stacking felt cramped)
- Models tab BuildModelRow inter-row Margin 1 → 8 (per model row reads as
its own card now, not a grouped 1-px-divider stack).
- About tab Resources/Log-folder cards Margin 0,0,0,1 → 0,0,0,8. Re-run
card already at 0,0,0,28 (section gap below it).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Sentry: forward unhandled exceptions to CaptureException
Audit gap closed. Previously the three top-level handlers
(OnUnhandledException / OnDispatcherUnhandled / OnUnobservedTask) only
wrote to Serilog — Sentry's own AppDomain auto-hook fires in parallel but
WPF dispatcher exceptions were swallowed by e.Handled=true before Sentry
could see them. Now each handler logs first, then forwards via the new
TryReportToSentry helper.
TryReportToSentry is gated on _crashReporter?.IsActive so the call no-ops
when the user hasn't opted in (or Offline Mode killed the SDK). The Sentry
call itself is wrapped in try/catch so a Sentry failure can't recurse into
another unhandled exception.
Behaviour summary
- Crash Reports OFF: handlers log locally, no network. Same as before.
- Crash Reports ON, Offline Mode OFF: every unhandled exception (AppDomain,
WPF dispatcher, unobserved Task) reaches your Sentry EU project with the
scrubbing pipeline applied.
- Crash Reports ON, Offline Mode ON: CrashReporter shuts the SDK down →
IsActive==false → handlers skip the Sentry call. Local logging still runs.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix: OnboardingWindow black corners + model download pinned to real HF commit
Onboarding rounded corners (APP_DESIGN §13.7)
- Root cause: WindowStyle=None + AllowsTransparency=False + Background=Transparent
renders the area outside the inner Border's CornerRadius as black. The
inner <Border CornerRadius=12> shows but the 4 corner triangles around it
fill with WPF's solid black for "transparent-but-not-actually-transparent".
- Fix per Microsoft's "Apply rounded corners in desktop apps for Windows 11"
guidance:
- XAML: Background="Transparent" → Background="{DynamicResource AppBg}".
Corners blend on Win10 fallback.
- Code-behind: DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE=33,
DWMWCP_ROUND=2, sizeof(int)) in OnSourceInitialized. Win11 rounds the
OS-level window edge; Win10 is a silent no-op.
- Corner-radius spec deviation 12 → 8 px (Radius.Lg). The DWM API only
supports DWMWCP_ROUND (~8 px) or DWMWCP_ROUNDSMALL (~4 px) — no path to
custom 12 px without AllowsTransparency=True (loses Mica + reintroduces
the cutout bug). 8 px is also MainWindow's curvature → both surfaces share
one canonical radius via the Radius.Lg token. APP_DESIGN §4.1 updated;
full rationale in §13.7.
Model download pinned + verified
- Replaced models.json placeholders (TODO_PIN commit + TODO_FILL SHAs) with
real values fetched from HuggingFace's tree API:
commit = 5359861c739e955e79d9a303bcbc70fb988958b1 (2024-10-29)
sha256 = LFS digests pulled per file from /api/models/.../tree/<commit>
sizeBytes = corrected to HF's actual sizes (placeholder bytes were
slightly off, would have shown wrong progress-bar totals)
- 5 models wired: ggml-tiny.en (77.7 MB · bundled), ggml-base.en (148 MB),
ggml-small.en (488 MB), ggml-medium.en (1.53 GB), ggml-large-v3 (3.10 GB).
- Models tab Download button now hits real URLs → HuggingFace serves the
.bin → ModelManager verifies SHA-256 against the manifest entry → File.Move
to %LOCALAPPDATA%\KusPus\models\. Behaviour matches TECH_SPEC §18.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio tab: fix mic-always-on + add LIVE indicator + restore Test transcription
P0 mic-always-on bug
- Root cause: SelectTab's StartAudioMeter/StopAudioMeter only ran on tab
switches. Closing the Preferences window with X (hide-instead-of-close per
§3.1) left the WasapiCapture open → mic icon stayed in the system tray
indefinitely.
- Fix: hooked Window.IsVisibleChanged. When IsVisible=false → StopAudioMeter().
When IsVisible=true AND Audio tab is currently showing → StartAudioMeter()
to resume.
- Also added StopAudioMeter() to the OnClosing _allowClose path so app exit
releases the mic too. StopAudioMeter now resets meter visuals (fill width +
peak tick opacity) so a paused meter doesn't show stale levels on resume.
● LIVE indicator (privacy affordance — UI UX Pro Max progressive-disclosure)
- Small mint dot + LIVE eyebrow shown next to "Microphone level" only while
the WasapiCapture is open. Toggled in StartAudioMeter / StopAudioMeter.
Test transcription — fully functional (restored from W1 placeholder)
- State machine: Idle → Recording (5 s countdown) → Transcribing (spinner) →
Result (transcript shown inline) or Error (red message + Retry).
- Single button doubles as Cancel mid-flight (CancellationTokenSource).
- Mic contention handled: StopAudioMeter() before AudioRecorder.StartAsync;
StartAudioMeter() resumes after completion / cancellation IF window is
still visible AND Audio tab is still showing.
- MainWindow constructor now takes IAudioRecorder + IWhisperRunner (added to
the App.xaml.cs DI wire-up). The active model is resolved via
IModelManager.Resolve before the mic opens — fast-fail if the model is
missing.
- Result text rendered in a SurfaceInput-tinted Border with BodySmall
typography; error text overrides Foreground to ErrorRed.
- Temp WAV from AudioRecorder.StopAsync deleted after transcription
(best-effort; IOException swallowed).
- CA1001 suppression added to MainWindow with rationale (mirrors App's
suppression — Window owns its lifecycle, _testCts disposed in OnClosing).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* History hover-actions + Models redesign (action buttons, no radios)
History tab — hover-revealed row actions
- Per UI UX Pro Max convention for productivity data tables (Gmail / Notion /
Linear pattern): on row hover, the model + duration cells are replaced
with Copy + Delete Btn.IconGhost buttons. No permanently-visible button
clutter in the read-heavy table.
- Border MouseEnter / MouseLeave toggles the action StackPanel Visibility;
Background=Surface paints over the model+duration columns when shown.
- Right-click ContextMenu retained as the keyboard / power-user path. Both
paths now route through shared helpers CopyTranscriptToClipboard +
DeleteTranscriptAsync, eliminating duplicated try/catch blocks.
- Icons: Segoe Fluent Icons "Copy" (E8C8) + "Delete" (E74D). Delete icon
tinted ErrorRed.
Models tab — radio buttons replaced with state-driven action CTAs
- New row layout: 4 px left-edge accent strip + title row (name + Bundled +
ACTIVE badge if applicable) + state-driven button on the right.
- Five visual states per UI UX Pro Max state-clarity rule:
Active — MintTint card bg + mint accent + ACTIVE badge, no button
(action already performed — primary-action rule).
Installed — neutral card, no accent, "Use this model" Btn.Primary.
Not installed — neutral card, no accent, "Download" Btn.Secondary
(heavier commitment than primary).
Downloading — neutral card, mint accent, progress + percent + Cancel
Btn.Ghost (existing BuildModelDownloadingRegion reused).
Failed — neutral card, red accent, error text + Retry Btn.Secondary
(existing BuildModelErrorRegion reused).
- ACTIVE badge: small mint-tinted Border with dark "ACTIVE" text — pulls the
user's eye to the in-use model at a glance.
- Dead code removed: OnModelRadioChecked (radio gone), BuildModelStatusRegion
(replaced by BuildModelActionRegion). BuildActiveBadge marked static
(CA1822 compliance).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio: input-device picker — user-selectable microphone
User-facing: a styled ComboBox sits next to the Microphone row in the Audio
tab. First entry is "Default device (follows Windows)"; remaining entries are
every active capture endpoint enumerated via MMDeviceEnumerator. Selection
persists as Audio.InputDeviceId in settings.json and takes effect immediately
for the live meter, Test transcription, and live dictation.
Wiring (no new layer dependencies):
- IAudioRecorder gains SetInputDeviceId(string?). AudioRecorder holds the
preferred id in a volatile field. StartAsync now goes through a
ResolveCaptureDevice helper: look up the preferred id; if it's missing /
inactive / not a capture endpoint, log a warning and fall back to the OS
default. KusPus.Audio still doesn't reference KusPus.Persistence.
- App.OnStartup pushes the initial id from PrefsStore + subscribes to
PrefsStore.Changes to propagate further updates. Composition-root pattern.
- MainWindow's level meter (separate WasapiCapture from AudioRecorder) gets
the same ResolveLevelMeterDevice helper so the meter shows the picked
device's levels, not the OS default's. Restarts on selection change.
UI (Styles/Inputs.xaml + MainWindow.xaml + .xaml.cs):
- New ComboBox.Surface style — SurfaceInput bg + BorderStrong border + 7 px
radius matching the SegmentButton wrapper aesthetic. Fully restyled
ToggleButton template (Fluent Icons chevron) and Popup template (dark/
light-themed Surface + DropShadowEffect) so the default WPF chrome doesn't
leak through. Items use MintTint for the selected row + HoverSubtle for
hover, matching the rest of the design system.
- AudioDeviceTitle TextBlock removed; replaced by the ComboBox + a new
AudioDeviceSubtitle that doubles as the error surface for "no mic" / "mic
busy" states (writes to subtitle instead of overwriting the title).
- PopulateInputDeviceCombo runs on tab open + every dropdown open — cheap
enumeration picks up hot-plug USB mics without restarting the app. Combo
selection matched to the persisted preference; falls back to "Default"
silently if the saved id is no longer present.
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio mic picker: fix dropdown lag (remove DropShadowEffect + per-open re-enum)
Two root causes per Microsoft Learn "Optimize control performance" +
dotnet/wpf#9881:
1. DropShadowEffect on the Popup's inner Border (BlurRadius=14) was the
dominant cost — every dropdown open triggered a per-pixel blur pass.
Removed; replaced with the existing BorderStrong stroke + Surface tint
which read as elevation without the GPU work.
2. MainWindow.OnInputDeviceDropDownOpened was re-enumerating MMDevices via
MMDeviceEnumerator.EnumerateAudioEndPoints on every open — a Win32 COM
round-trip + a full ItemsSource rebuild + a visual-tree teardown. Removed
the handler. Population now happens ONCE when the Audio tab opens
(already wired). Hot-plugged devices appear on next tab visit, which is
an acceptable trade-off vs the 150 ms perceptual lag every open.
Belt-and-suspenders: ComboBox.Surface now declares VirtualizingStackPanel
as its ItemsPanel + IsVirtualizing=True + VirtualizationMode=Recycling.
Negligible for 5-10 mics but bombproof if someone has 20+ capture devices.
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 1: chrome restructure — dock drawer + pin + magic-wand buttons
Restructures the floating pill per the Organic Pill spec (Phase 1 — chrome
only; halo, hue-drift, breath, hover-visualizer arrive in Phases 2-4).
Geometry — dynamic window size
- Collapsed: 200×56 (pill only).
- Open / pinned: 320×78 (pill 320 wide + 22 px dock peek). Mica stays tight
to the visible chrome so the area around the pill doesn't render a
rectangular Mica frame. Window animates both Width and Height on hover.
- Pill anchor stays on base width 200 so the position math (multi-monitor
sticky, bottom-center default) doesn't drift center on expand.
New chrome
- Pin button (top-right corner of pill, 18×18). Hidden by default at -12°
rotation. On pill hover: fades in + rotates to 0° (180 ms / 220 ms). Click
toggles "pinned" — dock + corner buttons stay visible after the cursor
leaves, glyph + bg tint to mint.
- Magic-wand button (top-right, left of Pin, 18×18). Dormant — ToolTip
"Refine text", no Click handler. We will wire it next iteration.
- Dock drawer (22 px row below the pill, slides down + fades in on hover).
Background matches the pill so the two read as one continuous chrome.
Border CornerRadius=0,0,8,8 to share the pill's bottom rounding.
Dock contents (left → right)
- Record toggle (22×18). Red dot glyph. Click currently logs a TODO — the
real wire-up needs a public AppCoordinator.ToggleTapMode() that doesn't
exist yet; the hotkey chord remains the canonical entry point for v1.
- Mic chooser (flex-grow). [mic icon] [device name] [chevron-down] on a
subtle button bg. Click opens a real popup picker — a styled <Popup>
containing a ScrollViewer + a StackPanel of per-device <Button>s. Click a
device → SetInputDeviceIdAsync via the bridge → popup closes → label
updates. Mint-tinted selected item.
- Settings (22×18). Fluent gear, opens Preferences (existing wire).
- Dismiss (22×18). Fluent X, red hover bg, calls _onClose → Shutdown.
Layer-friendly bridges
- FloatingPillWindow defines two tiny interfaces (IPrefsStoreBridge,
IAudioRecorderBridge) and a SetBridges(prefs, audio) hook. App.xaml.cs
implements them via PrefsStoreBridge (wraps IPrefsStore for the device id
get/set) and AudioDeviceBridge (calls MMDeviceEnumerator). Keeps
KusPus.App as the only layer that knows about both Persistence and NAudio.
Removed
- Old side-only hover-extend (ButtonPanel + AnimateWidth/AnimateButtonPanel).
Replaced by the dock drawer + corner-button animation pair.
Build: 0/0. Tests: 167/167. Smoke: clean launch + pill transitions to Idle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 2: idle content swaps to visualizer + IDLE label on hover
Default idle (no hover) — unchanged: SVG voice-stack icon + "KusPus" wordmark,
just like today. The pill reads as a tiny brand mark when the user isn't
intentionally interacting with it.
On hover (still Idle) — swap to:
- 20-bar visualizer running the low-amplitude traveling-sine motion model
from the Organic Pill §3 idle-visualizer cue: amplitude 0.06-0.14,
per-bar phase offset 0.18 rad, damping k≈3.5/s, full traversal every
~2.4 s. Quiet and slow enough to disappear from peripheral vision.
- Label "IDLE · HOLD TO DICTATE" replaces "RECORDING" in the same slot.
State + hover form an orthogonal grid:
(Idle, !hover) → IdleContent (SVG + KusPus) · viz Off
(Idle, hover) → VisualizerContent (bars + IDLE) · viz HoverIdle
(Recording, *) → VisualizerContent (bars + RECORDING) · viz Recording
(other states, *) → that state's panel · viz Off
Refactor
- RecordingContent renamed to VisualizerContent (now serves both Recording
and HoverIdle modes — same Canvas, label swaps).
- New VisualizerLabel x:Name so the label text can change per mode.
- FadeContent + new ApplyIdleContent + small static FadeElement helper:
TransitionTo delegates idle-content rendering to ApplyIdleContent, which
re-evaluates IsMouseOver every time it's called.
- OnPillMouseEnter / OnPillMouseLeave call ApplyIdleContent so the swap
happens on every hover transition while in Idle.
- New VisualizerMode enum (Off / Recording / HoverIdle). OnVisualizerTick
switches motion math by mode — HoverIdle runs the sine wave; Recording
keeps the existing voice-envelope target-rolling; Off targets all bars
to 0.05 (silent).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 3: breath + hue drift animations · Accessibility toggle
Personality animations
- Breath: ±0.6% scale pulse on PillSurface via ScaleTransform, 4 s sine cycle
(2 s in + 2 s out, AutoReverse + RepeatForever, SineEase). Subtle enough
to disappear from peripheral vision — gives the pill a "living organism"
presence without intruding.
- Hue drift: AccentBrush's middle gradient stop cycles mint #4DDBA6 →
seafoam #4DCDC2 → soft cyan #4DB8DB → back over 14 s, constant R=0x4D
band so perceived brightness stays flat (manual approximation of the
spec's OKLCH constant-L=0.84/C=0.14 constraint; WPF has no native OKLCH).
- Both wired as long-lived Storyboards (built once on Loaded, Begin/Stop
via SetReduceAnimations) so toggling is cheap.
Deferred to follow-up
- Halo: needs a backbuffer larger than the pill bounds — incompatible with
the current Mica setup (Mica would paint a rectangular tint around the
halo area). Decision point: keep Mica + skip halo, OR drop Mica for
AllowsTransparency=true + custom translucent gradient.
- Heartbeat blink: depends on accent-line opacity which is state-driven
(TransitionTo sets it per state). Multiplying onto state-driven base
needs a layered opacity model — deferred until heartbeat semantics are
pinned down.
Accessibility toggle (new Settings.Privacy.ReducePillAnimations field)
- New Accessibility section in Privacy tab: "Reduce pill animations" Toggle.
Default off. Saves to settings.json on flip.
- App.UpdatePillReduceAnimations combines the user toggle with
SystemParameters.ClientAreaAnimation — if either says reduce, pill pauses
personality animations (state transitions + dock slide remain active).
- Initial state applied at startup + on every PrefsStore.Changes emit.
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 4: light-theme mint gradient on visualizer bars
Per user audit feedback that the bars should echo the icon.svg's pearly-
mint gradient.
Dark theme: unchanged — solid #EBFFFFFF SolidColorBrush (the historical
token). A mint gradient over a dark pill surface would lose the
visualizer's "voice on top" reading.
Light theme: three-stop vertical LinearGradientBrush, alpha climbs top→
bottom so each bar reads as "lit from below":
0.0 → #664DDBA6 (subtle mint, 40% alpha)
0.5 → #994DDBA6 (mid mint, 60% alpha)
1.0 → #CC1F8762 (deeper mint, 80% alpha — bottom anchors)
Implementation: VisualizerBarActive is removed from the ThemeTokens.Map
dictionary and installed via a dedicated BuildVisualizerBarActive(mode)
helper alongside the existing BuildPillSurfaceGradient. ThemeTokens.Apply
now calls both special-case builders after the simple-Color-pair loop.
The bars in FloatingPillWindow use SetResourceReference for their Fill, so
the swap fires on theme flip with no other code touching needed.
Build: 0/0. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill follow-ups: 6 bug fixes (center-expand, height recovery, picker, theming, inset)
1. Center-expand on hover — Width + Left animate together (Left -= ΔW/2)
so the pill grows symmetrically instead of right-only.
2. Height recovery on dock close — DoubleAnimations use FillBehavior.Stop
and on Completed call BeginAnimation(prop, null) + SetValue(prop, to),
freeing the animated values so the pill collapses cleanly with no black
gap underneath.
3. Mic picker now design-system styled — Popup uses Surface/BorderStrong
tokens with a 4-px-padded ScrollViewer (PanningMode=VerticalOnly,
HorizontalScrollBarVisibility=Disabled). Item template adds a hover
trigger that paints HoverSubtle on each row.
4. Picker pins the dock open — _pickerOpen flag gates OnPillMouseLeave so
the dock stays open while the picker popup is open; OnMicChooserPopupClosed
restores normal hover behavior afterward.
5. Light-theme pill carries the icon's mint — BuildPillSurfaceGradient
light stops shift from #F8F8FA/#EEEEEF2 to #F4F8F4/#E0F0E6 (subtle top
shift + slightly mintier bottom), echoing icon.svg's pearl-to-mint
gradient without changing dark-theme look.
6. Dock visually narrower than pill — DockDrawer carries Margin="24,0,24,0"
so it reads as a nested sub-element instead of a flush continuation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill follow-ups round 2: black strips + picker lag + audio tab lag
1. Black strips beside dock — DockDrawer.Margin removed. The pill window is
AllowsTransparency=False because Mica (DWMWA_SYSTEMBACKDROP_TYPE) requires
it, so any inset between the dock and the window edge renders opaque
window-background black instead of click-through. The prior 24px margin
was the "narrower than pill" aesthetic from the last batch — reverting it
here since the side-effect (black strips) is worse than the cohesive look.
2. Pill mic-picker lag — cache the device list in FloatingPillWindow. On
SetBridges we warm the cache via Task.Run + Dispatcher.BeginInvoke; each
subsequent OnMicChooserClick reads from cache (instant) and fires a
background RefreshMicCacheAsync so hot-plugged devices appear on next open.
Same root cause as the audio-tab combo lag fixed in f4d2413: MMDeviceEnumerator
.EnumerateAudioEndPoints is a synchronous Win32 COM round-trip (~150ms).
UpdateMicChooserLabel uses the same cache fall-through.
3. Audio tab loading lag — OpenAudioTabAsync runs the heavy init off the
dispatcher. EnumerateInputDeviceItems (COM) and the WasapiCapture
ctor (driver shared-mode negotiation, ~150-500ms on some hardware) both
await Task.Run, then the combo's ItemsSource is set + StartRecording
fires on the UI thread. The Audio panel paints immediately; the device
combo + LIVE meter populate as each piece completes.
Surface kept stable: synchronous StartAudioMeter() façade still exists
so the 3 non-tab-open callers (visibility change, mid-test resume,
device-change restart) read unchanged.
Build: 0/0. Smoke: pill places + hook installs cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill: pin = compact-mode toggle + mint idle wordmark
User-spec rewire of the pin semantics. Previously "latch dock open"; now
"compact mode" — clicking pin contracts the pill back to 200×56, slides
the dock back, but keeps the pin button visible at all times so the user
can unpin.
Behavior matrix:
Pinned OFF (default):
hover → expand 200→320, slide dock down, fade pin+wand in
leave → contract, slide dock up, fade pin+wand out
pin click → enter pinned + contract immediately (if already expanded)
Pinned ON:
hover → swap SVG+wordmark → visualizer+IDLE label (NO resize, NO dock)
leave → swap visualizer → SVG+wordmark (NO resize, NO dock)
pin click → exit pinned; if currently hovered, expand back to hover view
pin button stays visible the entire time (mint-tinted)
Implementation:
- OnPinClick — inverted: becoming pinned calls CloseDock; becoming unpinned
+ hovered calls OpenDock. Unpinned + not-hovered stays put.
- OnPillMouseEnter/Leave — gate OpenDock/CloseDock on !_isPinned so hover
doesn't trigger the expand/contract while pinned. ApplyIdleContent still
runs in both branches so the content swap (SVG ↔ visualizer) works.
- AnimateCornerButtons — effectiveVisible = visible || _isPinned. Keeps
the pin button at opacity=1 and angle=0 while pinned regardless of what
the caller asked for.
Plus: idle KusPus wordmark now Mint instead of MutedText — picks up the
brand accent so the resting pill carries the product's color cue.
Build: 0/0. Smoke: pill places + hook installs clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Tray redesign + record-toggle wiring + nudge fix
Three landed-together changes for the tray + pill experience.
1. Pill record-toggle wired (Cluster A)
- SetRecordToggleAction(Action) on FloatingPillWindow; App binds to
AppCoordinator.ToggleFromTray. Per user spec the toggle does NOT
auto-capture a foreground target — the post-transcribe paste lands
wherever focus happens to be at the time.
- On toggle-start a RecordNudgePopup balloon appears above the RecordButton
("Click into your text field") for 6s. Auto-dismisses when state moves
to Recording. Previous 3s window was too short to read — user feedback.
- RecordGlyph changed from Ellipse to Rectangle that morphs dot ↔ rounded
square depending on FSM state.
2. Custom WPF tray right-click menu (Cluster B)
Replaces WinForms ContextMenuStrip with TrayMenuWindow.xaml — a
borderless, transparent, design-system-styled popup matching
Tray_light.png / Tray_dark.png:
- KusPus header with state-aware "Version 1.0.0 · {Idle|Recording|Transcribing}"
- Toggle recorder row with hotkey keycap (live-bound to PrefsStore.Hotkey)
- Active model: <name> row with chevron, opens models tab
- Preferences… opens general tab
- History… opens history tab
- Quit in ErrorRed
Shows at cursor on NotifyIcon.MouseClick (right). Closes on Deactivated
(focus lost) or any item click. WS_EX_TOOLWINDOW so it's hidden from
Alt-Tab/taskbar.
3. State-aware tray icons (Cluster C)
icons/icon-{idle,recording,error}.svg generated to .ico via tools/IconBuilder.
Recording overlays a red dot + glow on the bars; Error overlays a red
warning triangle. TrayManager subscribes to AppCoordinator.State and
swaps NotifyIcon.Icon based on the FSM state (treating a failed PostPaste
snapshot as Error for its hold duration). All three .ico files are
Resources in KusPus.App.csproj.
Build: 0/0. Smoke: pill places + tray icon visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill UX polish: BETA labels + pin lock + audit pass
Three concerns folded into one commit because they share the pill XAML/code-behind:
1. UX pass (per user spec):
- Pill record button + tray menu both labelled "Toggle Recording [BETA]"
(verb-form + dogfood expectation-setting). Tray chip is mint-coloured.
- Magic wand is dormant — rendered at 0.35 opacity with Arrow cursor +
tooltip "Refine text — coming soon" so the disabled state is legible
visually, not just in the tooltip.
- Pin semantics extended: now also locks the pill's screen position. Drag
short-circuits when _isPinned. Drag cursor (SizeAll) flips to Arrow when
pinned so the lock is telegraphed.
- Added CompactRecordButton at the pill's top-LEFT corner, visible only
when pinned. Pinned mode hides the dock, so without this the user would
have to unpin just to record. Sits opposite Pin/Wand on the right for
visual balance.
2. Nudge bug fix (the 6s timer was a red herring — TransitionTo's
"dismiss-on-Recording" rule was firing within ~ms of click since the FSM
moves to Recording immediately after ToggleFromTray. Dropped that rule;
timer bumped 6s→10s as the sole dismissal path. Comment explains why.
3. Full UX audit pass (10 items from the 2026-05-17 self-audit):
- #1 CompactRecord 22×18 r=5 → 18×18 r=4 (matches Pin/Wand cluster)
- #2 Design-system icon size tokens added to Styles/Tokens.xaml:
Icon.Glyph=11, Icon.Chevron=9. Bound on Wand, Pin, Settings, Close,
MicChooser icon (was 10), MicChooser chevron (was 8).
- #3 MicChooser hover: Opacity=1.4 (silent no-op — WPF clamps at 1) →
Background=SurfaceElevated. Real, theme-aware lift.
- #4 Error text margin 6→8 px (matches Idle/Transcribing rhythm)
- #5 Dock vertical centering moved to parent Grid (Margin=6,2,6,2);
mic chooser drops its per-button vertical compensation.
- #6 Wand opacity 0.5→0.35 (clearer subordinate read)
- #7 Dropped the 1px PillInnerHighlight — only existed on the pill, not
the dock, creating a seam at the drawer junction.
- #8 PillSurface Cursor=SizeAll at rest (telegraphs drag), Arrow when pinned
- #9 Drop shadow: Direction=270 Depth=2 Blur=32 Op=0.45 →
Depth=0 Blur=14 Op=0.25. Omnidirectional soft halo, no dock bleed.
- #10 All chrome gutters standardized at 6 px (was 5 on corners, 6 on dock).
Build: 0/0. Smoke: clean (verified locally with real recording + paste).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Default theme dark + Light [BETA] + onboarding step 6 = real dictation
Three changes from the 2026-05-17 dogfood batch:
1. Default theme flipped "auto" → "dark" (AppSettings.UiSettings.Theme).
Light theme is still in beta polish, so new installs land on the polished
dark surface. DefaultSettingsTests assertion updated with rationale.
2. Preferences theme picker: "Light" → "Light [BETA]" with tooltip explaining
the beta state. Sets dogfood expectations that light surfaces may not be
fully tuned yet.
3. Onboarding step 6 (Try it) replaced fake SimulatedSentences random-pick
with a real IAudioRecorder + IWhisperRunner pipeline. 5 s countdown
recording → transcribe with active model → render actual transcript (or
error if mic/model missing). Mirrors the existing Test Transcription
pattern from the Audio tab. Threaded audio/whisper/models services through
the OnboardingWindow constructor + both call sites (App.OnStartup +
MainWindow.OnRerunOnboarding).
Prior behaviour was misleading — onboarding "tested" dictation by picking a
canned sentence from a hard-coded list, so a broken mic / missing model
didn't surface until after onboarding finished. Now the failure modes
surface during setup where the user can act on them.
Build: 0/0. Core/Persistence/Whisper/Audio test suites all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Roadmap: add R1.2-10 long-mode chunk-on-VAD streaming on second hotkey
User dogfood feedback (2026-05-17) asked for continuous "speak-pause-paste"
loop on top of the existing push-to-talk model. Researched 3 architectures
(sliding-window, chunk-on-VAD, library-binding); recommended chunk-on-VAD
+ second hotkey ("Option B") to keep the existing UX intact while giving
power users opt-in long-mode dictation.
Deferred to v1.2 per user choice — ~2 weeks build + 1 week dogfood, too
large for the current pre-v1 polish window. Entry captures full 8-cluster
plan, top-3 risks (hallucination on silence, mid-word VAD cuts, paste-into-
wrong-app race), realistic latency (~0.7-1 s per pause with tiny.en), and
the rejected-for-now soft-cap alternative.
Also clarified LT-07 (streaming partial results) as a distinct UX
hypothesis — sliding-window for visible live caption in the pill, NOT a
paste pipeline. Different architecture from R1.2-10; both can ship in
principle but R1.2-10 lands first because it answers a real dogfood ask.
Per CLAUDE.md, this edit to docs/ROADMAP.md is authorized — user
explicitly said "keep it in the roadmap for later versions".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Onboarding step 3: mic chooser dropdown + CLAUDE.md deviation log update
User dogfood ask: let the user pick their mic during onboarding (not just
see the meter for the OS default), and persist that choice until they
change it from Preferences.
Step 3 gets an OnbInputDeviceCombo above the live meter card. It writes
to PrefsStore.Audio.InputDeviceId — the same field Preferences → Audio
uses — so the selection survives onboarding-exit and stays put until the
user changes it from either surface. ResolveOnbMicDevice mirrors
MainWindow.ResolveLevelMeterDevice: looks up by saved id, falls back to
the OS default if the device is unplugged. SelectionChanged restarts the
meter capture so the user sees the level for whichever mic they just
picked.
No shared base class with MainWindow's combo — onboarding is short-lived
and a single helper would pull in more ceremony than it removes. Logic
is a faithful mirror; if a future refactor extracts a shared
HotkeyPickerControl / InputDevicePickerControl UserControl, this and the
MainWindow combo + the Audio-tab one would all collapse to one consumer.
Also updated CLAUDE.md "Deviations" with 11 new entries covering this
session's UX work (pin = compact mode + position lock, BETA labels,
tray menu redesign + state-aware icons, dark default theme, real
onboarding dictation, mic chooser in onboarding, icon-size tokens,
shadow softening, mic-chooser hover fix, roadmap R1.2-10 entry).
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Onboarding fixes: Skip=Completed, pill defer, step 3 async, apartment-marshalling bug
Four bugs / behaviors corrected in one session of dogfood feedback.
1. Skip now marks Completed=true (was: Completed=false). Onboarding modal
opens once-ever per install. Closing via Skip is honoured the same as
Finish — modal does not re-appear on next launch. Re-runnable via
About → "Run again". Prior "skip-on-skip keeps Completed=false" rule
was hostile (the user just wanted to dismiss); replaced with show-once-
ever.
2. Pill is now invisible while onboarding is open. Bind() / BindLevels()
moved out of App.OnStartup inline construction and into a new
BindPillAndShow() helper that runs AFTER ShowDialog() returns (or
immediately if no onboarding). The first BehaviorSubject snapshot
subscribes to coordinator.State which triggers the pill's FadePillIn
→ Show(), so deferring Bind is what hides the pill. Existing users
(Completed=true) get pill instantly; new users get pill after Finish/Skip.
3. Step 3 mic now loads async (mirrors MainWindow.OpenAudioTabAsync).
New OpenMicStepAsync orchestrator: page paints immediately with
"Loading microphones…" placeholder + "LOADING…" label; MMDevice enum
and WasapiCapture init run on Task.Run; UI populates when ready.
Previously the entire dispatcher blocked for ~250 ms on first step 3
entry (driver shared-mode negotiation).
4. Cross-apartment MMDevice access bug (fix-of-fix). The first async pass
returned the MMDevice from the Task.Run lambda and then read
.FriendlyName on the dispatcher — NAudio's IMMDevice doesn't support
standard COM cross-apartment proxy marshalling, so the property getter
threw InvalidCastException → E_NOINTERFACE. That landed in
UnobservedTaskException (silent) and the user saw "Microphone blocked"
even though nothing was using the mic. Fix: read FriendlyName INSIDE
the Task.Run lambda (MTA where the device was created), return only
the string + WasapiCapture across the await. MMDevice never crosses
thread boundaries. WasapiCapture is fine cross-thread because it
caches its WaveFormat internally before its ctor returns — that's
why MainWindow.OpenAudioTabAsync (which only returns the capture)
never had this bug.
Validated from the live log:
System.InvalidCastException: Unable to cast COM object ...
to interface type IMMDevice ... E_NOINTERFACE
at NAudio.CoreAudioApi.MMDevice.GetPropertyInformation
at OnboardingWindow.StartMicCheckAsync() line 641
Also broadened the Task.Run catch from
COMException + MmException
to general Exception, since NAudio's WasapiCapture can throw a wider
set (InvalidOperationException on busy device, ArgumentException on
malformed format, etc.). Added an outer try/catch on OpenMicStepAsync
so any unhandled error surfaces as ShowMicError instead of silent
stuck-Loading.
Build: 0/0. Cross-apartment fix validated by code trace + matched
against MainWindow.OpenAudioTabAsync (which doesn't return MMDevice
across threads and works correctly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* About tab: globe icon matches LinkedIn visual size
The four social icons (LinkedIn, X, GitHub, Portfolio-globe) all use a
14×14 Viewbox wrapping 24×24 vector content. LinkedIn/X/GitHub paths
fill their viewBox edge-to-edge (0–24 on both axes), so they render at
the full 14×14 visual size. The globe was drawn in a 24×24 Canvas with
the ellipse at (2,2) W=20 H=20 plus a stroke=2 outline — that left 2 px
of padding around the geometry, so the globe rendered at ~20/24 ≈ 83%
of the other icons' visual size.
Fix: expand the geometry to fill the full 24×24 box.
Ellipse: (1,1) W=22 H=22 + stroke=2 → visible ink spans 0–24.
Meridian arc: radius 14.5 → 15.95 (×22/20 scale factor); endpoints
move from y=2/22 to y=1/23.
Equator line: M2 12 h20 → M1 12 h22.
All four icons now render at the same effective 14×14 visual size. No
aspect-ratio change — geometry preserved, only the bounds expanded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill compact-record grey + nudge 2s + social icon size parity
Four small UX tweaks from dogfood feedback (2026-05-17).
1. CompactRecord glyph (the corner record button visible while pinned) is
now grey (MutedText) when idle, red (#EF5350) when recording. Previously
it was always red — looked like "recording in progress" even at rest.
Grey reads as "available, tap to start"; red reserved for active state.
2. CompactRecord glyph bumped 8×8 → 10×10 (RadiusX 4 → 5 idle, 1.5 → 2
recording). The visible footprint now roughly matches the pin glyph's
ascent at FontSize=Icon.Glyph (11), so left/right corner clusters look
visually balanced. Button itself stays 18×18 with the same 6 px margin
from the pill edge as the pin StackPanel — positions were already
symmetric; the parity issue was glyph size.
3. UpdateRecordGlyph now swaps CompactRecord.Fill on state change (grey
↔ red) in addition to the existing radius morph. Dock RecordGlyph
stays always-red (it's the dock's record identifier; grey would lose
its affordance).
4. Nudge timer 10 s → 2 s. The "Click into your text field" hint is now
a brief flash, not a lingering popup. User feedback: 10 s sat there
long after they had already moved on.
5. About-tab social icons: wrapped each Path in a fixed-size 24×24 Canvas
so the Viewbox uses the canvas bounds (always 24×24) rather than the
path's computed bbox. Path bboxes vary subtly — GitHub's "M12 .297"
start offset, Bezier control points extending past the visible curve,
X's 0.258 left edge — which caused uneven rendered sizes when Viewbox
uniformly stretched each to 14×14. With the Canvas wrapper, all four
(LinkedIn, X, GitHub, Globe) are guaranteed to render at exactly the
same effective visual size.
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* About tagline + pill bottom-corners squared while dock is open
Two surgical fixes.
1. About-tab tagline changed from "Press a hotkey. Speak. Get pasted."
to "Local Privacy First" — direct product-pillar wording per user
ask. Same Type.HintItalic style, same position; just the string.
2. PillSurface.CornerRadius drops from 8 → (8,8,0,0) when the dock
slides into view, and back to 8 when the dock slides away. The
pill's bottom edge is flat while the dock is visible, so the seam
between pill bottom and dock top (which has CornerRadius=0,0,8,8)
reads as one continuous shape instead of two stacked rounded
rectangles with visible "ears" at the seam.
Implementation: the corner-radius swap lives inside OpenDock() and
CloseDock(). OnPillMouseEnter/Leave + OnPinClick already gate
OpenDock/CloseDock on !_isPinned (pinned mode uses content-swap
without expanding), so pinned compact-mode never enters OpenDock
and the pill keeps its full 8 px rounded corners — exactly the
"no corner-radius changes in pinned state" constraint.
Snap (not animate) since WPF's CornerRadius isn't a natively
animatable DependencyProperty. The snap happens at the START of
each method so the bottom edge is flat the full time the dock is
becoming visible (OpenDock case) and the round-back happens just
as the dock starts going away (CloseDock case — the brief
round-bottom-over-still-visible-dock artifact is during the
subordinate "going away" animation).
No XAML changes to the PillSurface element; the default
CornerRadius="8" stays as the initial / fully-collapsed value.
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@…
* fix(hotkey): don't consume LWin keyup — kept Win stuck-down in OS state
Consuming the LWin keyup left Windows thinking Win was still held, so
PasteEngine's SendInput(Ctrl+V) read as Win+Ctrl+V (Action Center / Quick
Settings) and every subsequent keystroke became a Win+key system shortcut.
The Ctrl-tap injection (AHK #MenuMaskKey idiom) still runs to suppress the
Start menu; we just let the real LWin keyup reach the OS so its key-held
state clears.
Spec §13 prescribes the old (buggy) behavior; flagged for revision in
CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 8 pill polish: full PILL_DESIGN.md + drag + multi-monitor sticky
Surface (docs/PILL_DESIGN.md §1, §3):
- 200×56 with 8 px DWM-rounded corners, Mica backdrop on Win11 22H2+
(DWMWA_SYSTEMBACKDROP_TYPE = DWMSBT_TRANSIENTWINDOW), dark-tinted via
DWMWA_USE_IMMERSIVE_DARK_MODE. Falls back to the §3.1 dark gradient
on older Windows.
- §3.3 1 px hairline border + drop shadow + inner top highlight.
Five-state machine (§2):
- Recording: 20-bar visualizer + RECORDING micro-label.
- Transcribing: 14 px ¾-arc spinner (0.9 s loop, rotated via direct
BeginAnimation on the RotateTransform) + "Transcribing…" text.
- Confirmed: "Pasted into <App>" with the bold app name, 1 s hold.
- Error: 5 px red dot + reason text, 2 s hold, instant accent shift to
red (§5).
- Idle: PRD G4 dev override — pill stays visible between dictations
showing the app icon + "KusPus" label. Will revert to spec §6.1
hidden-when-not-in-use once Settings exposes the close path.
Visualizer (§4):
- 20 bars × 3 px wide × 4 px gap × 4–26 px tall (136 px track).
- Damped target/value motion model per §4.2: center-weighted speak
envelope, per-bar damp rates, real audio levels from IAudioRecorder
override the simulation when present. Runs on CompositionTarget.
Rendering for display-refresh smoothness.
Accent line (§3.4):
- 136 × 1.5 mint gradient with glow, opacity per state.
Motion (§5):
- 120 ms pill appear/disappear, 150 ms content crossfade between
states. Cubic easing.
Hover-extend override (PILL_DESIGN.md §10):
- Width animates 200 → 280 over 150 ms on hover, Settings + Close
buttons fade in. WS_EX_TRANSPARENT intentionally NOT applied
(overrides §1.2 click-through) so buttons work; WS_EX_NOACTIVATE
preserved so focus doesn't move.
Draggable pill (beyond spec):
- MouseLeftButtonDown anywhere on the pill body → DragMove. Skips
when click is on a Button (Settings / Close).
- Session-only per-monitor remembered positions via
Dictionary<deviceName, Point> keyed by MONITORINFOEX.szDevice.
Cleared on every fresh process start.
Multi-monitor option C (hybrid sticky):
- On state transition into Armed/Recording, jump to the foreground
window's monitor at its remembered position (or default
bottom-center if first time). No-op when pill is already on the
right monitor or while user is dragging.
Coordinator snapshot extension:
- CoordinatorSnapshot.PostPaste:PostPasteInfo carries (Pasted,
TargetApp, ErrorReason). AppCoordinator emits one post-paste
snapshot from DeliverAsync / HandleFailureAsync so the pill knows
whether to show Confirmed or Error.
Icon pipeline:
- tools/IconBuilder: one-shot SVG → multi-frame ICO converter.
- icons/icon.svg as the single source of truth. ViewBox tightened to
"272 246 480 480" so the 5-bar content fills ~94 % of every
rendered frame (tray, taskbar, Task Manager, .exe icon).
- icons/icon.ico regenerated, embedded as ApplicationIcon + WPF
Resource. TrayManager loads via pack URI.
- SharpVectors.Wpf 1.8.5 renders the SVG directly inside the pill
Idle state — no hand-converted XAML to drift from the source.
Deferred from spec (not blockers):
- §3.2 light theme + WM_SETTINGCHANGE live switching.
- §3.4 accent variants beyond Mint (needs Settings UI from Phase 9).
- §5.3 reduced-motion gating of fades.
- Win10 acrylic fallback.
Tests: 117/117 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 9: MainWindow + 6 tabs + theming + pill flips with theme
Surface:
- New MainWindow per docs/APP_DESIGN.md §3. 880×620 (820×600 min) system-
chromed window. Sidebar nav (6 RadioButton tabs styled per §3.2 — mint
stripe + elevated bg on select). Close hides (§3.1 / §8.5); only the
tray's Quit fully exits. Tray menu gains "Preferences…" → MainWindow.
- Dark title-bar tint via DwmSetWindowAttribute DWMWA_USE_IMMERSIVE_DARK_MODE
+ SetWindowPos(SWP_FRAMECHANGED) to force the non-client repaint on
runtime theme flips (validated against Microsoft Q&A "DWMWA_USE_
IMMERSIVE_DARK_MODE won't update").
Six tabs (§3.3):
- General: Hotkey hero card with live listen-mode rebind (suspends LL hook,
captures held-keys snapshot, commits on full release, conflict warning
against known Windows shortcuts), Startup toggle → HKCU\Run, Appearance
Auto/Light/Dark segmented control.
- Audio: device label + Discord-style 200×6 track+fill meter (validated
fix against naudio/NAudio#160 #347 #507 — MMDevice.AudioMeterInformation
reports zero without an active capture session, so we open a WasapiCapture
and compute peak from samples in DataAvailable). Peak-hold tick that
decays slower than fill.
- Models: active-model row + manifest list with install state (file
existence per device), radio-select writes ActiveModelId to PrefsStore;
download wiring deferred to Phase 11.
- History: last 50 transcripts via IHistoryStore.SearchAsync; status dot
(mint = ok, red = failed), relative time, app name, model + duration.
- Privacy: offline + crash-reports toggles to PrefsStore, logs path +
Open in Explorer, local-first mint promise card.
- About: 80px brand mark + version line (AssemblyInformationalVersion) +
Cascadia-Mono build line, Resources card group (GitHub link + logs +
Re-run onboarding placeholder), MIT/local-first license blurb.
Theming infrastructure:
- ThemeApply (new) resolves "auto"/"light"/"dark" against AppsUseLightTheme
registry, applies DWM dark-mode + SWP_FRAMECHANGED.
- ThemeTokens (new) — 23-entry map of (dark, light) Color pairs covering
AppBg, Sidebar, Surface, SurfaceElevated, BorderSubtle/Strong/Divider,
Primary/Secondary/Muted/DisabledText, HoverSubtle, KeycapBg/Border,
Mint/MintTint/MintBorder, ErrorRed, WarningAmber/Tint/Border, plus
pill-specific PillBorder/PillInnerHighlight/VisualizerBarActive/Idle
and MeterTrack/ButtonHoverBg. Plus a LinearGradientBrush builder for
the pill's two-stop surface gradient.
- ThemeTokens.Apply uses REPLACEMENT (not mutation) — WPF freezes
Freezable resources in Application.Resources (x:Shared semantics) so
brush.Color mutation throws InvalidOperationException at startup.
Replacement fires ResourcesChanged; every {DynamicResource} consumer
re-resolves.
- MainWindow.xaml + FloatingPillWindow.xaml refactored end-to-end to
use {DynamicResource Token} for every brush/foreground/border. Code-
built UI (Models rows, History rows, hotkey keycaps) uses a Theme(key)
helper that returns the current resource brush; theme changes re-
render visible dynamic tabs so they pull fresh brushes.
- Pill visualizer bars use SetResourceReference(FillProperty) instead
of a frozen explicit brush so bars re-theme on switch.
Deviations from spec — flagged in CLAUDE.md (no MainWindow.xaml hex
literals migrated; everything is now token-based). Body theming for
the pill required new PillSurface gradient resource installed per-
theme. Multiple WPF parse-time gotchas worked around: IsChecked="True"
on TabGeneral triggers Checked event before content panels are bound
to fields — fixed with a _loaded guard in OnTabChecked.
Tests: 117/117 pass.
docs/APP_DESIGN.md: new authoritative UI spec from the user, referenced
from CLAUDE.md source-of-truth list. Where it conflicts with PILL_DESIGN
§2.1 (click-through) the §10 hover-extend override still wins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 10: Onboarding modal — 7 steps + first-launch trigger
OnboardingWindow per docs/APP_DESIGN.md §4. 720×520 chromeless rounded card,
12 px corners, draggable from the progress-dots header. Theme-aware via the
same ThemeApply + DynamicResource brushes that flip MainWindow / pill.
Seven steps (linear nav with Back / Next / Skip / Finish):
- 1 Welcome: stylized desktop preview + 3-column value-prop grid.
- 2 Hotkey: listen-mode + capture + commit to PrefsStore.Hotkey, with the
same Win+L-class conflict warning. Duplicates MainWindow's listen-mode
state machine (extract to UserControl when there's a third consumer).
- 3 Mic check: WasapiCapture on default device, Discord-style track+fill
meter, success/error variants, Open Settings → ms-settings:privacy-microphone.
- 4 Autostart: clickable ToggleCard → HKCU\Run via AutostartRegistry.Set.
- 5 Crash reports: ToggleCard + Local-first promise card.
- 6 Try it: 120-DIP transcript surface, "Simulate dictation" runs 1.8 s
Listening… then surfaces one of three canned sentences with mint border.
- 7 Done: corner-of-screen tray-diagram + Finish.
Finish path writes PrefsStore.Onboarding.Completed = true. Skip / Esc /
window-close leave Completed = false so the modal re-pops on next launch.
Re-runnable any time from About → "Run again" (replaces the disabled
Phase 9 placeholder button).
First-launch trigger in App.OnStartup: after _coordinator.Start(), queue
ShowDialog at DispatcherPriority.Background so OnStartup returns before
the modal's nested dispatcher frame begins. Pill + coordinator already
running by then, so the modal's hotkey picker can suspend the live LL
hook cleanly.
Deferred to a polish cluster: §4.1 dimmed-desktop backdrop, hotkey-picker
UserControl extraction, value-card hover states.
Tests: 117/117 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 11 + UX audit W1: crash reporter, egress killswitch, sidebar binding
Phase 11 (TECH_SPEC §19, PRD §10.2/§10.3)
- CrashReporter: Sentry init/shutdown gated on (CrashReportsOptIn && !OfflineMode).
Embeds project DSN (env-var override) and routes Sentry's own transport through
EgressAllowlistHandler so PRD §10.2 holds for SDK uploads too.
- EgressAllowlistHandler + EgressPolicy: v1 allowlist. Accepts huggingface.co and
regional Sentry ingest hosts (*.ingest[.<region>].sentry.io); Offline Mode and
non-HTTPS block everything. Pure policy decision in Core, IO handler in App.
- CrashScrubber: drops events whose Tags/Extra contain transcript/clipboard/text/
password/target_app/hwnd keys; replaces %TEMP%, %LOCALAPPDATA%, %APPDATA%,
%USERPROFILE% prefixes (start-anchored) and Environment.UserName occurrences
(mid-string) in messages, exception text, stack-frame paths, breadcrumbs.
- 30 new unit tests (167/167 total).
UI/UX W1 — "stop lying" (APP_DESIGN §13 audit findings)
- Crash Reports toggle visually disables when Offline Mode is on; subtitle swaps
to "Disabled while Offline Mode is on." The toggle's IsChecked is preserved.
- Replace stale "Phase X" + Win32 jargon copy across General/Audio/Models/About.
- Remove permanently-disabled "Test transcription" section; restore in W3 when wired.
- Sidebar footer bound live: status label from AppCoordinator snapshots, chord
glyph from PrefsStore (compact short form: "Ctrl+Win", not unicode soup).
- Inline [ESC] keycap in hotkey-listen hint (APP_DESIGN §13.4).
- Audit findings appended to docs/APP_DESIGN.md as §13 with a progress ledger.
Build: 0/0. Tests: 157 + 10 new CrashScrubber + extended EgressPolicy = 167/167.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit W2: design-system extraction (typography, dot, button, focus, spacing)
APP_DESIGN.md §13.5 P1-6/-7/-1/-9/-8/-P2-5. No user-visible scope change —
this is pure styling consolidation. Five new shared style sets, four
inline-styled buttons replaced, one fragile negative-margin layout fixed.
New: src/KusPus.App/Styles/
- Typography.xaml — SectionHeader, Type.RowTitle, Type.RowSubtitle, Type.Eyebrow,
Type.MonoSm, Type.MonoXs, Type.Display, Type.WarningEmphasis. Replaces ~25
inline TextBlock FontFamily / FontSize / FontWeight / Foreground quadruplets
across MainWindow.xaml.
- Dot.xaml — Dot.Mint / Dot.Amber / Dot.Red ellipse styles with the spec's
7 px + 6 px coloured glow. Replaces 4 hand-rolled ellipses in MainWindow.xaml
+ 1 code-behind ellipse in the history row renderer.
- Buttons.xaml — Btn.Primary / Secondary / Ghost / Danger × Sm / Md / Lg per
APP_DESIGN §3.4. All buttons now share one template (border + content + hover
opacity ladder + disabled-state); inline Padding / Background / Foreground /
BorderThickness gone from every call site.
- Focus.xaml — Focus.Mint: 1.5 px mint dashed outline at 2 px inset. Wired into
SidebarTab, Toggle, SegmentButton, and Btn.Base — keyboard-nav now has a
visible focus ring that reads as a design choice rather than the WPF default
dotted-Aero ring.
Modified: src/KusPus.App/MainWindow.xaml
- All TextBlock declarations matching repeated patterns use a Style key.
- All Buttons use Btn.Secondary (only kind currently needed; the rest of the
set arrives when W3's purge / download flows land).
- ConflictRow refactored: wraps HotkeyCard + ConflictRow in a single 440 px
StackPanel with bottom Margin 28. ConflictRow gets `Margin="0,1,0,0"` (1 px
gap below HotkeyCard) instead of the previous `Margin="0,-20,0,28"` negative-
margin tuck. Section gap now lives on the parent, not on each child.
Modified: src/KusPus.App/App.xaml
- Application.Resources now merges the four Styles/*.xaml dictionaries so
every keyed style is reachable app-wide (including future OnboardingWindow
reuse).
Build: 0/0. Tests: 167/167 still green. Smoke: clean launch.
P1-10 (per-tab UserControl extraction) deferred to a post-W3 cleanup — doing
it now would mean reshuffling every W3 addition into newly-created files. The
ledger in docs/APP_DESIGN.md §13.5 reflects this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit W3: missing UX — history search/purge, log clear, model downloads, contrast
APP_DESIGN.md §13.5 P0-3, P0-5, P1-2, P1-3, P1-4. Ships the four user-facing
gaps the audit flagged. All wired against existing services — no new layers.
P0-3 — DisabledText contrast
- ThemeTokens.cs: dark #5CFFFFFF → #80FFFFFF (~3.0:1 → ~5.1:1 vs AppBg).
- Light #50141414 → #A0141414 (~1.9:1 → ~4.7:1 vs AppBg).
- Both clear WCAG AA normal-text 4.5:1. Used for hotkey hint, model "Not
installed" label, disabled buttons.
P0-5 — Mic-active disclosure (already in W1)
- The Audio "Live level" subtitle reads "Active only while this tab is open.
Audio is never recorded." Tracking the ledger to "done" — no code change.
P1-3 — Privacy Logs row
- Two-row card: "Log size · {size}" with Clear logs ghost-danger button, then
"Log folder · {path}" with Open in Explorer secondary button.
- RefreshLogsSize enumerates LOCALAPPDATA\KusPus\logs\*.log on Loaded.
- OnClearLogsClick confirms via MessageBox (No default), then File.Delete each
*.log. Today's open log is held by Serilog's FileSink — skipped without
error, count reported in log.
- FormatBytes handles bytes / KB / MB.
P1-2 — History search + bulk footer
- Search box at top with Segoe Fluent icon, placeholder overlay, clear "×"
button. 250 ms DispatcherTimer debounces TextChanged → HistoryStore.SearchAsync
(FTS5 backing). Empty query reverts to "most recent first".
- Footer above 1px divider: live row count, "{n} matches for '…'" when filtered,
Purge all history Btn.Danger with MessageBox confirm. Calls HistoryStore.PurgeAllAsync.
- PurgeAllButton.IsEnabled gates on row count > 0 || query is not null.
P1-4 — Models download flow
- Per-model state machine in _modelDownloads dictionary keyed by id.
- BuildModelStatusRegion dispatches on state: Active / Installed / Downloading
(180 × 4 px mint progress bar + percent in mono + Cancel ghost) / Error
(red message + Retry secondary) / Not installed (Download secondary).
- OnModelDownloadClick: pre-checks OfflineMode (clearer message than letting
EgressAllowlistHandler throw mid-stream), spawns Task.Run with cancellable
cts. IProgress<DownloadProgress> marshals to UI thread, throttled to 0.5 %
steps so the StackPanel rebuild doesn't dominate CPU on a fast link.
- OnModelCancelClick: cts.Cancel(). Completion continuation handles cleanup
for both cancellation (silent) and failure (sticky error + Retry).
- ShortenDownloadError strips ModelManager's "HTTP error downloading …:" prefix.
Build: 0/0. Tests: 167/167.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX follow-up: History as a table · pill Settings → Preferences · spacing rhythm
History tab — table layout (audit follow-up)
- Replace card-list rendering with a Grid-based table: header row in
Type.Eyebrow style above, 6-column body rows below. Columns: status (14) /
time (78) / app (110) / transcript (*) / model (72) / duration (52).
- Per the skill's number-tabular rule: time, model, duration use Cascadia
Mono so columns stay aligned across rows.
- Per truncation-strategy: transcript and app truncate with ellipsis +
ToolTip exposing the full text on hover. Time column ToolTip shows the
full timestamp.
- Per gridline-subtle: 1 px BorderDivider between body rows, 1 px
BorderSubtle between header and body. No row striping.
- Row hover: HoverSubtle background via Style trigger on the HistoryRow
Border (Cursor=Hand for affordance).
- Right-click context menu per row: "Copy text" (Clipboard.SetText) and
"Delete" (HistoryStore.DeleteAsync + ReloadHistoryAsync).
- ShortModelId strips "ggml-" prefix to match the sidebar's compact form.
- Failed transcripts: red dot + italic red transcript column; rest of the
row stays normal so the failure mode reads as one cell, not the row.
Pill Settings button wired to Preferences modal
- Add SetSettingsAction(Action) to FloatingPillWindow, mirroring
SetCloseAction's pattern.
- App.OnStartup wires it to _mainWindow.ShowOn("general") AFTER MainWindow
is constructed (the existing pill setup runs before MainWindow exists).
- Tooltip "Settings — coming soon" → "Open Preferences".
Spacing rhythm normalization
- Models tab: list bottom margin 12 → 16 (align with the 8-grid inter-block
rhythm; "16 = block gap" is now consistent across History search bar and
Models list).
- About tab: header StackPanel bottom 32 → 28 (matches the section gap
rhythm used everywhere else, instead of being one-off heavier).
- History footer: top margin 18 → 16 (16 is the canonical block gap).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit Round 2: tokens, surfaces, expanded typography, History single-card
Parallelised Phase 1 (5 sub-agents wrote disjoint style files concurrently),
then serial Phase 2 sweep of MainWindow + App.xaml + APP_DESIGN.md.
NEW style files (Phase 1, parallel)
- Styles/Tokens.xaml — Space.xs..xxl + Pad.Tight/Default/Hero + Radius.Sm/Md/Lg
- Styles/Surfaces.xaml — Surface.Default/Hero/Tight/Warning/Mint (5 Border styles)
- Styles/Inputs.xaml — Input.Search TextBox style
- Styles/Typography.xaml extended — 10 new Type.* roles, dead Type.MonoXs removed
- Styles/Buttons.xaml extended — new Btn.IconGhost; header docs call-site inventory
Phase 2 — App.xaml wires the 3 new dictionaries (Tokens first so others can
reference its tokens), then sweep MainWindow.xaml + MainWindow.xaml.cs:
MainWindow.xaml migrations
- Hotkey card Border → Surface.Hero (drops inline Padding 22,20 override)
- ConflictRow Border → Surface.Warning (collapses 5 inline attrs)
- Local-first Border → Surface.Mint (collapses 5 inline attrs)
- HotkeyHint TextBlock → Type.HintItalic
- ConflictText TextBlock → Type.WarningBody
- AboutVersion TextBlock → Type.Body
- AboutBuildLine Margin 0,4,0,0 → 0,3,0,0 (matches Type.RowSubtitle rhythm)
- Local-first head TextBlock → Type.MintHeadline
- Local-first body TextBlock → Type.BodySmall
- MIT licensed TextBlock → Type.Footnote
- "Press a hotkey" TextBlock → Type.HintItalic
- StatusLabel TextBlock → Type.SidebarStatus (was Type.MonoSm-with-override misuse)
- History search bar magnifier → Type.IconSm
- History search box TextBox → Input.Search
- History search clear Button → Btn.IconGhost
- About re-run card Margin 0,0,0,32 → 0,0,0,28 (matches section gap)
- Sidebar footer Grid Margin 18,8,18,14 → 14,8,14,14 (matches sidebar 14)
History tab — unified single composed card (Q3 from user audit decisions)
- Outer RowCard Padding="0" wraps a StackPanel of inner Borders.
- Search bar (Padding 14,8, bottom 1 px divider), table header (Padding 14,10,
bottom 1 px divider), HistoryList (HistoryRow style provides per-row bottom
divider), bulk footer (Padding 14,12, no top border — last row's bottom
border IS the separator → no double line).
- Reads as one "history widget" instead of four separately-styled blocks.
MainWindow.xaml.cs code-behind sweep
- New TypeStyle(string) helper (mirrors Theme()) to pull Type.* styles from
Application.Resources for code-built TextBlocks.
- BuildModelRow title/subtitle → Type.RowTitle / Type.RowSubtitle
- BuildBundledBadge child TextBlock → Type.BadgeMint
- BuildModelDownloadingRegion percent → Type.MonoSm
- BuildModelErrorRegion error text → Type.ErrorInline
- BuildHistoryRow TIME / MODEL / DUR columns → Type.MonoSm (transcript + app
columns + Installed/Active status stay inline because Foreground flips per
state — no single Type.* role covers both colour states).
- Empty-state TextBlock in ReloadHistoryAsync → Type.HintItalic
- Dropped unreachable "(none)" branch in RenderModelsTab (audit P2-8).
Docs: APP_DESIGN.md §13.5 ledger updated (P2-8/-9 done, P2-10 won't-fix with
reason); new §13.6 documents the Round 2 work — token system, surface variants,
typography role catalogue, inputs/IconGhost, spacing fixes, History
unification, code-behind cleanup, dead-code policy.
Parallelisation safety
- 5 sub-agents in Phase 1 each owned exactly one file (disjoint writes — no
lost-update risk). None ran dotnet build (avoids bin/obj corruption). The
orchestrator does all building serially in Phase 3 after killing any running
KusPus.exe to avoid output-DLL locks.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* About tab: byline + 4 social icons · Models/About card spacing · 1px→8px
Author byline (About → bottom-right)
- "Made by Devang Kumawat" + LinkedIn / X / GitHub / Portfolio icon row at
the bottom-right of the About tab, sitting directly on AppBg (no card —
reads as a personal touch, not part of the design-system surface inventory).
- Text uses Type.Footnote (12 Medium SecondaryText) — matches the existing
"MIT licensed" line on the left. Per UI UX Pro Max rule weight-hierarchy:
text carries the byline weight, icons are visually subordinate.
- Each icon: 14×14 Viewbox inside a Btn.IconGhost (28×28 click target).
Aspect ratio locked by the Viewbox's default Uniform Stretch and the
underlying 24×24 viewBox.
- Theme tinting: Fill="{DynamicResource MutedText}" (filled paths) or
Stroke="{DynamicResource MutedText}" (Lucide globe) — no per-theme assets,
one shared rendering for dark+light.
Icon sources (saved to icons/social/ + LICENSE.md attribution)
- LinkedIn / X / GitHub: Simple Icons (CC0) via jsDelivr simple-icons@v11
and raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/.
- Portfolio (globe): Lucide (ISC) from raw.githubusercontent.com/lucide-icons/lucide.
- Saved as .svg files for documentation/license tracking; actual rendering
inlines the path data in MainWindow.xaml so the fill binds to theme tokens
(SharpVectors SvgViewbox can't easily theme-tint).
Each icon button OpenUrl(...) → Process.Start with UseShellExecute=true.
Single helper handles Win32Exception + FileNotFoundException for missing
default browser without crashing.
Links wired
- LinkedIn → https://www.linkedin.com/in/devangk003/
- X → https://x.com/devang_kumawat
- GitHub → https://github.com/devangk003
- Portfolio → https://lnk.bio/devangk003
Spacing (user audit feedback — 1 px stacking felt cramped)
- Models tab BuildModelRow inter-row Margin 1 → 8 (per model row reads as
its own card now, not a grouped 1-px-divider stack).
- About tab Resources/Log-folder cards Margin 0,0,0,1 → 0,0,0,8. Re-run
card already at 0,0,0,28 (section gap below it).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Sentry: forward unhandled exceptions to CaptureException
Audit gap closed. Previously the three top-level handlers
(OnUnhandledException / OnDispatcherUnhandled / OnUnobservedTask) only
wrote to Serilog — Sentry's own AppDomain auto-hook fires in parallel but
WPF dispatcher exceptions were swallowed by e.Handled=true before Sentry
could see them. Now each handler logs first, then forwards via the new
TryReportToSentry helper.
TryReportToSentry is gated on _crashReporter?.IsActive so the call no-ops
when the user hasn't opted in (or Offline Mode killed the SDK). The Sentry
call itself is wrapped in try/catch so a Sentry failure can't recurse into
another unhandled exception.
Behaviour summary
- Crash Reports OFF: handlers log locally, no network. Same as before.
- Crash Reports ON, Offline Mode OFF: every unhandled exception (AppDomain,
WPF dispatcher, unobserved Task) reaches your Sentry EU project with the
scrubbing pipeline applied.
- Crash Reports ON, Offline Mode ON: CrashReporter shuts the SDK down →
IsActive==false → handlers skip the Sentry call. Local logging still runs.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix: OnboardingWindow black corners + model download pinned to real HF commit
Onboarding rounded corners (APP_DESIGN §13.7)
- Root cause: WindowStyle=None + AllowsTransparency=False + Background=Transparent
renders the area outside the inner Border's CornerRadius as black. The
inner <Border CornerRadius=12> shows but the 4 corner triangles around it
fill with WPF's solid black for "transparent-but-not-actually-transparent".
- Fix per Microsoft's "Apply rounded corners in desktop apps for Windows 11"
guidance:
- XAML: Background="Transparent" → Background="{DynamicResource AppBg}".
Corners blend on Win10 fallback.
- Code-behind: DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE=33,
DWMWCP_ROUND=2, sizeof(int)) in OnSourceInitialized. Win11 rounds the
OS-level window edge; Win10 is a silent no-op.
- Corner-radius spec deviation 12 → 8 px (Radius.Lg). The DWM API only
supports DWMWCP_ROUND (~8 px) or DWMWCP_ROUNDSMALL (~4 px) — no path to
custom 12 px without AllowsTransparency=True (loses Mica + reintroduces
the cutout bug). 8 px is also MainWindow's curvature → both surfaces share
one canonical radius via the Radius.Lg token. APP_DESIGN §4.1 updated;
full rationale in §13.7.
Model download pinned + verified
- Replaced models.json placeholders (TODO_PIN commit + TODO_FILL SHAs) with
real values fetched from HuggingFace's tree API:
commit = 5359861c739e955e79d9a303bcbc70fb988958b1 (2024-10-29)
sha256 = LFS digests pulled per file from /api/models/.../tree/<commit>
sizeBytes = corrected to HF's actual sizes (placeholder bytes were
slightly off, would have shown wrong progress-bar totals)
- 5 models wired: ggml-tiny.en (77.7 MB · bundled), ggml-base.en (148 MB),
ggml-small.en (488 MB), ggml-medium.en (1.53 GB), ggml-large-v3 (3.10 GB).
- Models tab Download button now hits real URLs → HuggingFace serves the
.bin → ModelManager verifies SHA-256 against the manifest entry → File.Move
to %LOCALAPPDATA%\KusPus\models\. Behaviour matches TECH_SPEC §18.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio tab: fix mic-always-on + add LIVE indicator + restore Test transcription
P0 mic-always-on bug
- Root cause: SelectTab's StartAudioMeter/StopAudioMeter only ran on tab
switches. Closing the Preferences window with X (hide-instead-of-close per
§3.1) left the WasapiCapture open → mic icon stayed in the system tray
indefinitely.
- Fix: hooked Window.IsVisibleChanged. When IsVisible=false → StopAudioMeter().
When IsVisible=true AND Audio tab is currently showing → StartAudioMeter()
to resume.
- Also added StopAudioMeter() to the OnClosing _allowClose path so app exit
releases the mic too. StopAudioMeter now resets meter visuals (fill width +
peak tick opacity) so a paused meter doesn't show stale levels on resume.
● LIVE indicator (privacy affordance — UI UX Pro Max progressive-disclosure)
- Small mint dot + LIVE eyebrow shown next to "Microphone level" only while
the WasapiCapture is open. Toggled in StartAudioMeter / StopAudioMeter.
Test transcription — fully functional (restored from W1 placeholder)
- State machine: Idle → Recording (5 s countdown) → Transcribing (spinner) →
Result (transcript shown inline) or Error (red message + Retry).
- Single button doubles as Cancel mid-flight (CancellationTokenSource).
- Mic contention handled: StopAudioMeter() before AudioRecorder.StartAsync;
StartAudioMeter() resumes after completion / cancellation IF window is
still visible AND Audio tab is still showing.
- MainWindow constructor now takes IAudioRecorder + IWhisperRunner (added to
the App.xaml.cs DI wire-up). The active model is resolved via
IModelManager.Resolve before the mic opens — fast-fail if the model is
missing.
- Result text rendered in a SurfaceInput-tinted Border with BodySmall
typography; error text overrides Foreground to ErrorRed.
- Temp WAV from AudioRecorder.StopAsync deleted after transcription
(best-effort; IOException swallowed).
- CA1001 suppression added to MainWindow with rationale (mirrors App's
suppression — Window owns its lifecycle, _testCts disposed in OnClosing).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* History hover-actions + Models redesign (action buttons, no radios)
History tab — hover-revealed row actions
- Per UI UX Pro Max convention for productivity data tables (Gmail / Notion /
Linear pattern): on row hover, the model + duration cells are replaced
with Copy + Delete Btn.IconGhost buttons. No permanently-visible button
clutter in the read-heavy table.
- Border MouseEnter / MouseLeave toggles the action StackPanel Visibility;
Background=Surface paints over the model+duration columns when shown.
- Right-click ContextMenu retained as the keyboard / power-user path. Both
paths now route through shared helpers CopyTranscriptToClipboard +
DeleteTranscriptAsync, eliminating duplicated try/catch blocks.
- Icons: Segoe Fluent Icons "Copy" (E8C8) + "Delete" (E74D). Delete icon
tinted ErrorRed.
Models tab — radio buttons replaced with state-driven action CTAs
- New row layout: 4 px left-edge accent strip + title row (name + Bundled +
ACTIVE badge if applicable) + state-driven button on the right.
- Five visual states per UI UX Pro Max state-clarity rule:
Active — MintTint card bg + mint accent + ACTIVE badge, no button
(action already performed — primary-action rule).
Installed — neutral card, no accent, "Use this model" Btn.Primary.
Not installed — neutral card, no accent, "Download" Btn.Secondary
(heavier commitment than primary).
Downloading — neutral card, mint accent, progress + percent + Cancel
Btn.Ghost (existing BuildModelDownloadingRegion reused).
Failed — neutral card, red accent, error text + Retry Btn.Secondary
(existing BuildModelErrorRegion reused).
- ACTIVE badge: small mint-tinted Border with dark "ACTIVE" text — pulls the
user's eye to the in-use model at a glance.
- Dead code removed: OnModelRadioChecked (radio gone), BuildModelStatusRegion
(replaced by BuildModelActionRegion). BuildActiveBadge marked static
(CA1822 compliance).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio: input-device picker — user-selectable microphone
User-facing: a styled ComboBox sits next to the Microphone row in the Audio
tab. First entry is "Default device (follows Windows)"; remaining entries are
every active capture endpoint enumerated via MMDeviceEnumerator. Selection
persists as Audio.InputDeviceId in settings.json and takes effect immediately
for the live meter, Test transcription, and live dictation.
Wiring (no new layer dependencies):
- IAudioRecorder gains SetInputDeviceId(string?). AudioRecorder holds the
preferred id in a volatile field. StartAsync now goes through a
ResolveCaptureDevice helper: look up the preferred id; if it's missing /
inactive / not a capture endpoint, log a warning and fall back to the OS
default. KusPus.Audio still doesn't reference KusPus.Persistence.
- App.OnStartup pushes the initial id from PrefsStore + subscribes to
PrefsStore.Changes to propagate further updates. Composition-root pattern.
- MainWindow's level meter (separate WasapiCapture from AudioRecorder) gets
the same ResolveLevelMeterDevice helper so the meter shows the picked
device's levels, not the OS default's. Restarts on selection change.
UI (Styles/Inputs.xaml + MainWindow.xaml + .xaml.cs):
- New ComboBox.Surface style — SurfaceInput bg + BorderStrong border + 7 px
radius matching the SegmentButton wrapper aesthetic. Fully restyled
ToggleButton template (Fluent Icons chevron) and Popup template (dark/
light-themed Surface + DropShadowEffect) so the default WPF chrome doesn't
leak through. Items use MintTint for the selected row + HoverSubtle for
hover, matching the rest of the design system.
- AudioDeviceTitle TextBlock removed; replaced by the ComboBox + a new
AudioDeviceSubtitle that doubles as the error surface for "no mic" / "mic
busy" states (writes to subtitle instead of overwriting the title).
- PopulateInputDeviceCombo runs on tab open + every dropdown open — cheap
enumeration picks up hot-plug USB mics without restarting the app. Combo
selection matched to the persisted preference; falls back to "Default"
silently if the saved id is no longer present.
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio mic picker: fix dropdown lag (remove DropShadowEffect + per-open re-enum)
Two root causes per Microsoft Learn "Optimize control performance" +
dotnet/wpf#9881:
1. DropShadowEffect on the Popup's inner Border (BlurRadius=14) was the
dominant cost — every dropdown open triggered a per-pixel blur pass.
Removed; replaced with the existing BorderStrong stroke + Surface tint
which read as elevation without the GPU work.
2. MainWindow.OnInputDeviceDropDownOpened was re-enumerating MMDevices via
MMDeviceEnumerator.EnumerateAudioEndPoints on every open — a Win32 COM
round-trip + a full ItemsSource rebuild + a visual-tree teardown. Removed
the handler. Population now happens ONCE when the Audio tab opens
(already wired). Hot-plugged devices appear on next tab visit, which is
an acceptable trade-off vs the 150 ms perceptual lag every open.
Belt-and-suspenders: ComboBox.Surface now declares VirtualizingStackPanel
as its ItemsPanel + IsVirtualizing=True + VirtualizationMode=Recycling.
Negligible for 5-10 mics but bombproof if someone has 20+ capture devices.
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 1: chrome restructure — dock drawer + pin + magic-wand buttons
Restructures the floating pill per the Organic Pill spec (Phase 1 — chrome
only; halo, hue-drift, breath, hover-visualizer arrive in Phases 2-4).
Geometry — dynamic window size
- Collapsed: 200×56 (pill only).
- Open / pinned: 320×78 (pill 320 wide + 22 px dock peek). Mica stays tight
to the visible chrome so the area around the pill doesn't render a
rectangular Mica frame. Window animates both Width and Height on hover.
- Pill anchor stays on base width 200 so the position math (multi-monitor
sticky, bottom-center default) doesn't drift center on expand.
New chrome
- Pin button (top-right corner of pill, 18×18). Hidden by default at -12°
rotation. On pill hover: fades in + rotates to 0° (180 ms / 220 ms). Click
toggles "pinned" — dock + corner buttons stay visible after the cursor
leaves, glyph + bg tint to mint.
- Magic-wand button (top-right, left of Pin, 18×18). Dormant — ToolTip
"Refine text", no Click handler. We will wire it next iteration.
- Dock drawer (22 px row below the pill, slides down + fades in on hover).
Background matches the pill so the two read as one continuous chrome.
Border CornerRadius=0,0,8,8 to share the pill's bottom rounding.
Dock contents (left → right)
- Record toggle (22×18). Red dot glyph. Click currently logs a TODO — the
real wire-up needs a public AppCoordinator.ToggleTapMode() that doesn't
exist yet; the hotkey chord remains the canonical entry point for v1.
- Mic chooser (flex-grow). [mic icon] [device name] [chevron-down] on a
subtle button bg. Click opens a real popup picker — a styled <Popup>
containing a ScrollViewer + a StackPanel of per-device <Button>s. Click a
device → SetInputDeviceIdAsync via the bridge → popup closes → label
updates. Mint-tinted selected item.
- Settings (22×18). Fluent gear, opens Preferences (existing wire).
- Dismiss (22×18). Fluent X, red hover bg, calls _onClose → Shutdown.
Layer-friendly bridges
- FloatingPillWindow defines two tiny interfaces (IPrefsStoreBridge,
IAudioRecorderBridge) and a SetBridges(prefs, audio) hook. App.xaml.cs
implements them via PrefsStoreBridge (wraps IPrefsStore for the device id
get/set) and AudioDeviceBridge (calls MMDeviceEnumerator). Keeps
KusPus.App as the only layer that knows about both Persistence and NAudio.
Removed
- Old side-only hover-extend (ButtonPanel + AnimateWidth/AnimateButtonPanel).
Replaced by the dock drawer + corner-button animation pair.
Build: 0/0. Tests: 167/167. Smoke: clean launch + pill transitions to Idle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 2: idle content swaps to visualizer + IDLE label on hover
Default idle (no hover) — unchanged: SVG voice-stack icon + "KusPus" wordmark,
just like today. The pill reads as a tiny brand mark when the user isn't
intentionally interacting with it.
On hover (still Idle) — swap to:
- 20-bar visualizer running the low-amplitude traveling-sine motion model
from the Organic Pill §3 idle-visualizer cue: amplitude 0.06-0.14,
per-bar phase offset 0.18 rad, damping k≈3.5/s, full traversal every
~2.4 s. Quiet and slow enough to disappear from peripheral vision.
- Label "IDLE · HOLD TO DICTATE" replaces "RECORDING" in the same slot.
State + hover form an orthogonal grid:
(Idle, !hover) → IdleContent (SVG + KusPus) · viz Off
(Idle, hover) → VisualizerContent (bars + IDLE) · viz HoverIdle
(Recording, *) → VisualizerContent (bars + RECORDING) · viz Recording
(other states, *) → that state's panel · viz Off
Refactor
- RecordingContent renamed to VisualizerContent (now serves both Recording
and HoverIdle modes — same Canvas, label swaps).
- New VisualizerLabel x:Name so the label text can change per mode.
- FadeContent + new ApplyIdleContent + small static FadeElement helper:
TransitionTo delegates idle-content rendering to ApplyIdleContent, which
re-evaluates IsMouseOver every time it's called.
- OnPillMouseEnter / OnPillMouseLeave call ApplyIdleContent so the swap
happens on every hover transition while in Idle.
- New VisualizerMode enum (Off / Recording / HoverIdle). OnVisualizerTick
switches motion math by mode — HoverIdle runs the sine wave; Recording
keeps the existing voice-envelope target-rolling; Off targets all bars
to 0.05 (silent).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 3: breath + hue drift animations · Accessibility toggle
Personality animations
- Breath: ±0.6% scale pulse on PillSurface via ScaleTransform, 4 s sine cycle
(2 s in + 2 s out, AutoReverse + RepeatForever, SineEase). Subtle enough
to disappear from peripheral vision — gives the pill a "living organism"
presence without intruding.
- Hue drift: AccentBrush's middle gradient stop cycles mint #4DDBA6 →
seafoam #4DCDC2 → soft cyan #4DB8DB → back over 14 s, constant R=0x4D
band so perceived brightness stays flat (manual approximation of the
spec's OKLCH constant-L=0.84/C=0.14 constraint; WPF has no native OKLCH).
- Both wired as long-lived Storyboards (built once on Loaded, Begin/Stop
via SetReduceAnimations) so toggling is cheap.
Deferred to follow-up
- Halo: needs a backbuffer larger than the pill bounds — incompatible with
the current Mica setup (Mica would paint a rectangular tint around the
halo area). Decision point: keep Mica + skip halo, OR drop Mica for
AllowsTransparency=true + custom translucent gradient.
- Heartbeat blink: depends on accent-line opacity which is state-driven
(TransitionTo sets it per state). Multiplying onto state-driven base
needs a layered opacity model — deferred until heartbeat semantics are
pinned down.
Accessibility toggle (new Settings.Privacy.ReducePillAnimations field)
- New Accessibility section in Privacy tab: "Reduce pill animations" Toggle.
Default off. Saves to settings.json on flip.
- App.UpdatePillReduceAnimations combines the user toggle with
SystemParameters.ClientAreaAnimation — if either says reduce, pill pauses
personality animations (state transitions + dock slide remain active).
- Initial state applied at startup + on every PrefsStore.Changes emit.
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 4: light-theme mint gradient on visualizer bars
Per user audit feedback that the bars should echo the icon.svg's pearly-
mint gradient.
Dark theme: unchanged — solid #EBFFFFFF SolidColorBrush (the historical
token). A mint gradient over a dark pill surface would lose the
visualizer's "voice on top" reading.
Light theme: three-stop vertical LinearGradientBrush, alpha climbs top→
bottom so each bar reads as "lit from below":
0.0 → #664DDBA6 (subtle mint, 40% alpha)
0.5 → #994DDBA6 (mid mint, 60% alpha)
1.0 → #CC1F8762 (deeper mint, 80% alpha — bottom anchors)
Implementation: VisualizerBarActive is removed from the ThemeTokens.Map
dictionary and installed via a dedicated BuildVisualizerBarActive(mode)
helper alongside the existing BuildPillSurfaceGradient. ThemeTokens.Apply
now calls both special-case builders after the simple-Color-pair loop.
The bars in FloatingPillWindow use SetResourceReference for their Fill, so
the swap fires on theme flip with no other code touching needed.
Build: 0/0. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill follow-ups: 6 bug fixes (center-expand, height recovery, picker, theming, inset)
1. Center-expand on hover — Width + Left animate together (Left -= ΔW/2)
so the pill grows symmetrically instead of right-only.
2. Height recovery on dock close — DoubleAnimations use FillBehavior.Stop
and on Completed call BeginAnimation(prop, null) + SetValue(prop, to),
freeing the animated values so the pill collapses cleanly with no black
gap underneath.
3. Mic picker now design-system styled — Popup uses Surface/BorderStrong
tokens with a 4-px-padded ScrollViewer (PanningMode=VerticalOnly,
HorizontalScrollBarVisibility=Disabled). Item template adds a hover
trigger that paints HoverSubtle on each row.
4. Picker pins the dock open — _pickerOpen flag gates OnPillMouseLeave so
the dock stays open while the picker popup is open; OnMicChooserPopupClosed
restores normal hover behavior afterward.
5. Light-theme pill carries the icon's mint — BuildPillSurfaceGradient
light stops shift from #F8F8FA/#EEEEEF2 to #F4F8F4/#E0F0E6 (subtle top
shift + slightly mintier bottom), echoing icon.svg's pearl-to-mint
gradient without changing dark-theme look.
6. Dock visually narrower than pill — DockDrawer carries Margin="24,0,24,0"
so it reads as a nested sub-element instead of a flush continuation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill follow-ups round 2: black strips + picker lag + audio tab lag
1. Black strips beside dock — DockDrawer.Margin removed. The pill window is
AllowsTransparency=False because Mica (DWMWA_SYSTEMBACKDROP_TYPE) requires
it, so any inset between the dock and the window edge renders opaque
window-background black instead of click-through. The prior 24px margin
was the "narrower than pill" aesthetic from the last batch — reverting it
here since the side-effect (black strips) is worse than the cohesive look.
2. Pill mic-picker lag — cache the device list in FloatingPillWindow. On
SetBridges we warm the cache via Task.Run + Dispatcher.BeginInvoke; each
subsequent OnMicChooserClick reads from cache (instant) and fires a
background RefreshMicCacheAsync so hot-plugged devices appear on next open.
Same root cause as the audio-tab combo lag fixed in f834cc5: MMDeviceEnumerator
.EnumerateAudioEndPoints is a synchronous Win32 COM round-trip (~150ms).
UpdateMicChooserLabel uses the same cache fall-through.
3. Audio tab loading lag — OpenAudioTabAsync runs the heavy init off the
dispatcher. EnumerateInputDeviceItems (COM) and the WasapiCapture
ctor (driver shared-mode negotiation, ~150-500ms on some hardware) both
await Task.Run, then the combo's ItemsSource is set + StartRecording
fires on the UI thread. The Audio panel paints immediately; the device
combo + LIVE meter populate as each piece completes.
Surface kept stable: synchronous StartAudioMeter() façade still exists
so the 3 non-tab-open callers (visibility change, mid-test resume,
device-change restart) read unchanged.
Build: 0/0. Smoke: pill places + hook installs cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill: pin = compact-mode toggle + mint idle wordmark
User-spec rewire of the pin semantics. Previously "latch dock open"; now
"compact mode" — clicking pin contracts the pill back to 200×56, slides
the dock back, but keeps the pin button visible at all times so the user
can unpin.
Behavior matrix:
Pinned OFF (default):
hover → expand 200→320, slide dock down, fade pin+wand in
leave → contract, slide dock up, fade pin+wand out
pin click → enter pinned + contract immediately (if already expanded)
Pinned ON:
hover → swap SVG+wordmark → visualizer+IDLE label (NO resize, NO dock)
leave → swap visualizer → SVG+wordmark (NO resize, NO dock)
pin click → exit pinned; if currently hovered, expand back to hover view
pin button stays visible the entire time (mint-tinted)
Implementation:
- OnPinClick — inverted: becoming pinned calls CloseDock; becoming unpinned
+ hovered calls OpenDock. Unpinned + not-hovered stays put.
- OnPillMouseEnter/Leave — gate OpenDock/CloseDock on !_isPinned so hover
doesn't trigger the expand/contract while pinned. ApplyIdleContent still
runs in both branches so the content swap (SVG ↔ visualizer) works.
- AnimateCornerButtons — effectiveVisible = visible || _isPinned. Keeps
the pin button at opacity=1 and angle=0 while pinned regardless of what
the caller asked for.
Plus: idle KusPus wordmark now Mint instead of MutedText — picks up the
brand accent so the resting pill carries the product's color cue.
Build: 0/0. Smoke: pill places + hook installs clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Tray redesign + record-toggle wiring + nudge fix
Three landed-together changes for the tray + pill experience.
1. Pill record-toggle wired (Cluster A)
- SetRecordToggleAction(Action) on FloatingPillWindow; App binds to
AppCoordinator.ToggleFromTray. Per user spec the toggle does NOT
auto-capture a foreground target — the post-transcribe paste lands
wherever focus happens to be at the time.
- On toggle-start a RecordNudgePopup balloon appears above the RecordButton
("Click into your text field") for 6s. Auto-dismisses when state moves
to Recording. Previous 3s window was too short to read — user feedback.
- RecordGlyph changed from Ellipse to Rectangle that morphs dot ↔ rounded
square depending on FSM state.
2. Custom WPF tray right-click menu (Cluster B)
Replaces WinForms ContextMenuStrip with TrayMenuWindow.xaml — a
borderless, transparent, design-system-styled popup matching
Tray_light.png / Tray_dark.png:
- KusPus header with state-aware "Version 1.0.0 · {Idle|Recording|Transcribing}"
- Toggle recorder row with hotkey keycap (live-bound to PrefsStore.Hotkey)
- Active model: <name> row with chevron, opens models tab
- Preferences… opens general tab
- History… opens history tab
- Quit in ErrorRed
Shows at cursor on NotifyIcon.MouseClick (right). Closes on Deactivated
(focus lost) or any item click. WS_EX_TOOLWINDOW so it's hidden from
Alt-Tab/taskbar.
3. State-aware tray icons (Cluster C)
icons/icon-{idle,recording,error}.svg generated to .ico via tools/IconBuilder.
Recording overlays a red dot + glow on the bars; Error overlays a red
warning triangle. TrayManager subscribes to AppCoordinator.State and
swaps NotifyIcon.Icon based on the FSM state (treating a failed PostPaste
snapshot as Error for its hold duration). All three .ico files are
Resources in KusPus.App.csproj.
Build: 0/0. Smoke: pill places + tray icon visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill UX polish: BETA labels + pin lock + audit pass
Three concerns folded into one commit because they share the pill XAML/code-behind:
1. UX pass (per user spec):
- Pill record button + tray menu both labelled "Toggle Recording [BETA]"
(verb-form + dogfood expectation-setting). Tray chip is mint-coloured.
- Magic wand is dormant — rendered at 0.35 opacity with Arrow cursor +
tooltip "Refine text — coming soon" so the disabled state is legible
visually, not just in the tooltip.
- Pin semantics extended: now also locks the pill's screen position. Drag
short-circuits when _isPinned. Drag cursor (SizeAll) flips to Arrow when
pinned so the lock is telegraphed.
- Added CompactRecordButton at the pill's top-LEFT corner, visible only
when pinned. Pinned mode hides the dock, so without this the user would
have to unpin just to record. Sits opposite Pin/Wand on the right for
visual balance.
2. Nudge bug fix (the 6s timer was a red herring — TransitionTo's
"dismiss-on-Recording" rule was firing within ~ms of click since the FSM
moves to Recording immediately after ToggleFromTray. Dropped that rule;
timer bumped 6s→10s as the sole dismissal path. Comment explains why.
3. Full UX audit pass (10 items from the 2026-05-17 self-audit):
- #1 CompactRecord 22×18 r=5 → 18×18 r=4 (matches Pin/Wand cluster)
- #2 Design-system icon size tokens added to Styles/Tokens.xaml:
Icon.Glyph=11, Icon.Chevron=9. Bound on Wand, Pin, Settings, Close,
MicChooser icon (was 10), MicChooser chevron (was 8).
- #3 MicChooser hover: Opacity=1.4 (silent no-op — WPF clamps at 1) →
Background=SurfaceElevated. Real, theme-aware lift.
- #4 Error text margin 6→8 px (matches Idle/Transcribing rhythm)
- #5 Dock vertical centering moved to parent Grid (Margin=6,2,6,2);
mic chooser drops its per-button vertical compensation.
- #6 Wand opacity 0.5→0.35 (clearer subordinate read)
- #7 Dropped the 1px PillInnerHighlight — only existed on the pill, not
the dock, creating a seam at the drawer junction.
- #8 PillSurface Cursor=SizeAll at rest (telegraphs drag), Arrow when pinned
- #9 Drop shadow: Direction=270 Depth=2 Blur=32 Op=0.45 →
Depth=0 Blur=14 Op=0.25. Omnidirectional soft halo, no dock bleed.
- #10 All chrome gutters standardized at 6 px (was 5 on corners, 6 on dock).
Build: 0/0. Smoke: clean (verified locally with real recording + paste).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Default theme dark + Light [BETA] + onboarding step 6 = real dictation
Three changes from the 2026-05-17 dogfood batch:
1. Default theme flipped "auto" → "dark" (AppSettings.UiSettings.Theme).
Light theme is still in beta polish, so new installs land on the polished
dark surface. DefaultSettingsTests assertion updated with rationale.
2. Preferences theme picker: "Light" → "Light [BETA]" with tooltip explaining
the beta state. Sets dogfood expectations that light surfaces may not be
fully tuned yet.
3. Onboarding step 6 (Try it) replaced fake SimulatedSentences random-pick
with a real IAudioRecorder + IWhisperRunner pipeline. 5 s countdown
recording → transcribe with active model → render actual transcript (or
error if mic/model missing). Mirrors the existing Test Transcription
pattern from the Audio tab. Threaded audio/whisper/models services through
the OnboardingWindow constructor + both call sites (App.OnStartup +
MainWindow.OnRerunOnboarding).
Prior behaviour was misleading — onboarding "tested" dictation by picking a
canned sentence from a hard-coded list, so a broken mic / missing model
didn't surface until after onboarding finished. Now the failure modes
surface during setup where the user can act on them.
Build: 0/0. Core/Persistence/Whisper/Audio test suites all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Roadmap: add R1.2-10 long-mode chunk-on-VAD streaming on second hotkey
User dogfood feedback (2026-05-17) asked for continuous "speak-pause-paste"
loop on top of the existing push-to-talk model. Researched 3 architectures
(sliding-window, chunk-on-VAD, library-binding); recommended chunk-on-VAD
+ second hotkey ("Option B") to keep the existing UX intact while giving
power users opt-in long-mode dictation.
Deferred to v1.2 per user choice — ~2 weeks build + 1 week dogfood, too
large for the current pre-v1 polish window. Entry captures full 8-cluster
plan, top-3 risks (hallucination on silence, mid-word VAD cuts, paste-into-
wrong-app race), realistic latency (~0.7-1 s per pause with tiny.en), and
the rejected-for-now soft-cap alternative.
Also clarified LT-07 (streaming partial results) as a distinct UX
hypothesis — sliding-window for visible live caption in the pill, NOT a
paste pipeline. Different architecture from R1.2-10; both can ship in
principle but R1.2-10 lands first because it answers a real dogfood ask.
Per CLAUDE.md, this edit to docs/ROADMAP.md is authorized — user
explicitly said "keep it in the roadmap for later versions".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Onboarding step 3: mic chooser dropdown + CLAUDE.md deviation log update
User dogfood ask: let the user pick their mic during onboarding (not just
see the meter for the OS default), and persist that choice until they
change it from Preferences.
Step 3 gets an OnbInputDeviceCombo above the live meter card. It writes
to PrefsStore.Audio.InputDeviceId — the same field Preferences → Audio
uses — so the selection survives onboarding-exit and stays put until the
user changes it from either surface. ResolveOnbMicDevice mirrors
MainWindow.ResolveLevelMeterDevice: looks up by saved id, falls back to
the OS default if the device is unplugged. SelectionChanged restarts the
meter capture so the user sees the level for whichever mic they just
picked.
No shared base class with MainWindow's combo — onboarding is short-lived
and a single helper would pull in more ceremony than it removes. Logic
is a faithful mirror; if a future refactor extracts a shared
HotkeyPickerControl / InputDevicePickerControl UserControl, this and the
MainWindow combo + the Audio-tab one would all collapse to one consumer.
Also updated CLAUDE.md "Deviations" with 11 new entries covering this
session's UX work (pin = compact mode + position lock, BETA labels,
tray menu redesign + state-aware icons, dark default theme, real
onboarding dictation, mic chooser in onboarding, icon-size tokens,
shadow softening, mic-chooser hover fix, roadmap R1.2-10 entry).
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(hotkey): don't consume LWin keyup — kept Win stuck-down in OS state
Consuming the LWin keyup left Windows thinking Win was still held, so
PasteEngine's SendInput(Ctrl+V) read as Win+Ctrl+V (Action Center / Quick
Settings) and every subsequent keystroke became a Win+key system shortcut.
The Ctrl-tap injection (AHK #MenuMaskKey idiom) still runs to suppress the
Start menu; we just let the real LWin keyup reach the OS so its key-held
state clears.
Spec §13 prescribes the old (buggy) behavior; flagged for revision in
CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 8 pill polish: full PILL_DESIGN.md + drag + multi-monitor sticky
Surface (docs/PILL_DESIGN.md §1, §3):
- 200×56 with 8 px DWM-rounded corners, Mica backdrop on Win11 22H2+
(DWMWA_SYSTEMBACKDROP_TYPE = DWMSBT_TRANSIENTWINDOW), dark-tinted via
DWMWA_USE_IMMERSIVE_DARK_MODE. Falls back to the §3.1 dark gradient
on older Windows.
- §3.3 1 px hairline border + drop shadow + inner top highlight.
Five-state machine (§2):
- Recording: 20-bar visualizer + RECORDING micro-label.
- Transcribing: 14 px ¾-arc spinner (0.9 s loop, rotated via direct
BeginAnimation on the RotateTransform) + "Transcribing…" text.
- Confirmed: "Pasted into <App>" with the bold app name, 1 s hold.
- Error: 5 px red dot + reason text, 2 s hold, instant accent shift to
red (§5).
- Idle: PRD G4 dev override — pill stays visible between dictations
showing the app icon + "KusPus" label. Will revert to spec §6.1
hidden-when-not-in-use once Settings exposes the close path.
Visualizer (§4):
- 20 bars × 3 px wide × 4 px gap × 4–26 px tall (136 px track).
- Damped target/value motion model per §4.2: center-weighted speak
envelope, per-bar damp rates, real audio levels from IAudioRecorder
override the simulation when present. Runs on CompositionTarget.
Rendering for display-refresh smoothness.
Accent line (§3.4):
- 136 × 1.5 mint gradient with glow, opacity per state.
Motion (§5):
- 120 ms pill appear/disappear, 150 ms content crossfade between
states. Cubic easing.
Hover-extend override (PILL_DESIGN.md §10):
- Width animates 200 → 280 over 150 ms on hover, Settings + Close
buttons fade in. WS_EX_TRANSPARENT intentionally NOT applied
(overrides §1.2 click-through) so buttons work; WS_EX_NOACTIVATE
preserved so focus doesn't move.
Draggable pill (beyond spec):
- MouseLeftButtonDown anywhere on the pill body → DragMove. Skips
when click is on a Button (Settings / Close).
- Session-only per-monitor remembered positions via
Dictionary<deviceName, Point> keyed by MONITORINFOEX.szDevice.
Cleared on every fresh process start.
Multi-monitor option C (hybrid sticky):
- On state transition into Armed/Recording, jump to the foreground
window's monitor at its remembered position (or default
bottom-center if first time). No-op when pill is already on the
right monitor or while user is dragging.
Coordinator snapshot extension:
- CoordinatorSnapshot.PostPaste:PostPasteInfo carries (Pasted,
TargetApp, ErrorReason). AppCoordinator emits one post-paste
snapshot from DeliverAsync / HandleFailureAsync so the pill knows
whether to show Confirmed or Error.
Icon pipeline:
- tools/IconBuilder: one-shot SVG → multi-frame ICO converter.
- icons/icon.svg as the single source of truth. ViewBox tightened to
"272 246 480 480" so the 5-bar content fills ~94 % of every
rendered frame (tray, taskbar, Task Manager, .exe icon).
- icons/icon.ico regenerated, embedded as ApplicationIcon + WPF
Resource. TrayManager loads via pack URI.
- SharpVectors.Wpf 1.8.5 renders the SVG directly inside the pill
Idle state — no hand-converted XAML to drift from the source.
Deferred from spec (not blockers):
- §3.2 light theme + WM_SETTINGCHANGE live switching.
- §3.4 accent variants beyond Mint (needs Settings UI from Phase 9).
- §5.3 reduced-motion gating of fades.
- Win10 acrylic fallback.
Tests: 117/117 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 9: MainWindow + 6 tabs + theming + pill flips with theme
Surface:
- New MainWindow per docs/APP_DESIGN.md §3. 880×620 (820×600 min) system-
chromed window. Sidebar nav (6 RadioButton tabs styled per §3.2 — mint
stripe + elevated bg on select). Close hides (§3.1 / §8.5); only the
tray's Quit fully exits. Tray menu gains "Preferences…" → MainWindow.
- Dark title-bar tint via DwmSetWindowAttribute DWMWA_USE_IMMERSIVE_DARK_MODE
+ SetWindowPos(SWP_FRAMECHANGED) to force the non-client repaint on
runtime theme flips (validated against Microsoft Q&A "DWMWA_USE_
IMMERSIVE_DARK_MODE won't update").
Six tabs (§3.3):
- General: Hotkey hero card with live listen-mode rebind (suspends LL hook,
captures held-keys snapshot, commits on full release, conflict warning
against known Windows shortcuts), Startup toggle → HKCU\Run, Appearance
Auto/Light/Dark segmented control.
- Audio: device label + Discord-style 200×6 track+fill meter (validated
fix against naudio/NAudio#160 #347 #507 — MMDevice.AudioMeterInformation
reports zero without an active capture session, so we open a WasapiCapture
and compute peak from samples in DataAvailable). Peak-hold tick that
decays slower than fill.
- Models: active-model row + manifest list with install state (file
existence per device), radio-select writes ActiveModelId to PrefsStore;
download wiring deferred to Phase 11.
- History: last 50 transcripts via IHistoryStore.SearchAsync; status dot
(mint = ok, red = failed), relative time, app name, model + duration.
- Privacy: offline + crash-reports toggles to PrefsStore, logs path +
Open in Explorer, local-first mint promise card.
- About: 80px brand mark + version line (AssemblyInformationalVersion) +
Cascadia-Mono build line, Resources card group (GitHub link + logs +
Re-run onboarding placeholder), MIT/local-first license blurb.
Theming infrastructure:
- ThemeApply (new) resolves "auto"/"light"/"dark" against AppsUseLightTheme
registry, applies DWM dark-mode + SWP_FRAMECHANGED.
- ThemeTokens (new) — 23-entry map of (dark, light) Color pairs covering
AppBg, Sidebar, Surface, SurfaceElevated, BorderSubtle/Strong/Divider,
Primary/Secondary/Muted/DisabledText, HoverSubtle, KeycapBg/Border,
Mint/MintTint/MintBorder, ErrorRed, WarningAmber/Tint/Border, plus
pill-specific PillBorder/PillInnerHighlight/VisualizerBarActive/Idle
and MeterTrack/ButtonHoverBg. Plus a LinearGradientBrush builder for
the pill's two-stop surface gradient.
- ThemeTokens.Apply uses REPLACEMENT (not mutation) — WPF freezes
Freezable resources in Application.Resources (x:Shared semantics) so
brush.Color mutation throws InvalidOperationException at startup.
Replacement fires ResourcesChanged; every {DynamicResource} consumer
re-resolves.
- MainWindow.xaml + FloatingPillWindow.xaml refactored end-to-end to
use {DynamicResource Token} for every brush/foreground/border. Code-
built UI (Models rows, History rows, hotkey keycaps) uses a Theme(key)
helper that returns the current resource brush; theme changes re-
render visible dynamic tabs so they pull fresh brushes.
- Pill visualizer bars use SetResourceReference(FillProperty) instead
of a frozen explicit brush so bars re-theme on switch.
Deviations from spec — flagged in CLAUDE.md (no MainWindow.xaml hex
literals migrated; everything is now token-based). Body theming for
the pill required new PillSurface gradient resource installed per-
theme. Multiple WPF parse-time gotchas worked around: IsChecked="True"
on TabGeneral triggers Checked event before content panels are bound
to fields — fixed with a _loaded guard in OnTabChecked.
Tests: 117/117 pass.
docs/APP_DESIGN.md: new authoritative UI spec from the user, referenced
from CLAUDE.md source-of-truth list. Where it conflicts with PILL_DESIGN
§2.1 (click-through) the §10 hover-extend override still wins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 10: Onboarding modal — 7 steps + first-launch trigger
OnboardingWindow per docs/APP_DESIGN.md §4. 720×520 chromeless rounded card,
12 px corners, draggable from the progress-dots header. Theme-aware via the
same ThemeApply + DynamicResource brushes that flip MainWindow / pill.
Seven steps (linear nav with Back / Next / Skip / Finish):
- 1 Welcome: stylized desktop preview + 3-column value-prop grid.
- 2 Hotkey: listen-mode + capture + commit to PrefsStore.Hotkey, with the
same Win+L-class conflict warning. Duplicates MainWindow's listen-mode
state machine (extract to UserControl when there's a third consumer).
- 3 Mic check: WasapiCapture on default device, Discord-style track+fill
meter, success/error variants, Open Settings → ms-settings:privacy-microphone.
- 4 Autostart: clickable ToggleCard → HKCU\Run via AutostartRegistry.Set.
- 5 Crash reports: ToggleCard + Local-first promise card.
- 6 Try it: 120-DIP transcript surface, "Simulate dictation" runs 1.8 s
Listening… then surfaces one of three canned sentences with mint border.
- 7 Done: corner-of-screen tray-diagram + Finish.
Finish path writes PrefsStore.Onboarding.Completed = true. Skip / Esc /
window-close leave Completed = false so the modal re-pops on next launch.
Re-runnable any time from About → "Run again" (replaces the disabled
Phase 9 placeholder button).
First-launch trigger in App.OnStartup: after _coordinator.Start(), queue
ShowDialog at DispatcherPriority.Background so OnStartup returns before
the modal's nested dispatcher frame begins. Pill + coordinator already
running by then, so the modal's hotkey picker can suspend the live LL
hook cleanly.
Deferred to a polish cluster: §4.1 dimmed-desktop backdrop, hotkey-picker
UserControl extraction, value-card hover states.
Tests: 117/117 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 11 + UX audit W1: crash reporter, egress killswitch, sidebar binding
Phase 11 (TECH_SPEC §19, PRD §10.2/§10.3)
- CrashReporter: Sentry init/shutdown gated on (CrashReportsOptIn && !OfflineMode).
Embeds project DSN (env-var override) and routes Sentry's own transport through
EgressAllowlistHandler so PRD §10.2 holds for SDK uploads too.
- EgressAllowlistHandler + EgressPolicy: v1 allowlist. Accepts huggingface.co and
regional Sentry ingest hosts (*.ingest[.<region>].sentry.io); Offline Mode and
non-HTTPS block everything. Pure policy decision in Core, IO handler in App.
- CrashScrubber: drops events whose Tags/Extra contain transcript/clipboard/text/
password/target_app/hwnd keys; replaces %TEMP%, %LOCALAPPDATA%, %APPDATA%,
%USERPROFILE% prefixes (start-anchored) and Environment.UserName occurrences
(mid-string) in messages, exception text, stack-frame paths, breadcrumbs.
- 30 new unit tests (167/167 total).
UI/UX W1 — "stop lying" (APP_DESIGN §13 audit findings)
- Crash Reports toggle visually disables when Offline Mode is on; subtitle swaps
to "Disabled while Offline Mode is on." The toggle's IsChecked is preserved.
- Replace stale "Phase X" + Win32 jargon copy across General/Audio/Models/About.
- Remove permanently-disabled "Test transcription" section; restore in W3 when wired.
- Sidebar footer bound live: status label from AppCoordinator snapshots, chord
glyph from PrefsStore (compact short form: "Ctrl+Win", not unicode soup).
- Inline [ESC] keycap in hotkey-listen hint (APP_DESIGN §13.4).
- Audit findings appended to docs/APP_DESIGN.md as §13 with a progress ledger.
Build: 0/0. Tests: 157 + 10 new CrashScrubber + extended EgressPolicy = 167/167.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit W2: design-system extraction (typography, dot, button, focus, spacing)
APP_DESIGN.md §13.5 P1-6/-7/-1/-9/-8/-P2-5. No user-visible scope change —
this is pure styling consolidation. Five new shared style sets, four
inline-styled buttons replaced, one fragile negative-margin layout fixed.
New: src/KusPus.App/Styles/
- Typography.xaml — SectionHeader, Type.RowTitle, Type.RowSubtitle, Type.Eyebrow,
Type.MonoSm, Type.MonoXs, Type.Display, Type.WarningEmphasis. Replaces ~25
inline TextBlock FontFamily / FontSize / FontWeight / Foreground quadruplets
across MainWindow.xaml.
- Dot.xaml — Dot.Mint / Dot.Amber / Dot.Red ellipse styles with the spec's
7 px + 6 px coloured glow. Replaces 4 hand-rolled ellipses in MainWindow.xaml
+ 1 code-behind ellipse in the history row renderer.
- Buttons.xaml — Btn.Primary / Secondary / Ghost / Danger × Sm / Md / Lg per
APP_DESIGN §3.4. All buttons now share one template (border + content + hover
opacity ladder + disabled-state); inline Padding / Background / Foreground /
BorderThickness gone from every call site.
- Focus.xaml — Focus.Mint: 1.5 px mint dashed outline at 2 px inset. Wired into
SidebarTab, Toggle, SegmentButton, and Btn.Base — keyboard-nav now has a
visible focus ring that reads as a design choice rather than the WPF default
dotted-Aero ring.
Modified: src/KusPus.App/MainWindow.xaml
- All TextBlock declarations matching repeated patterns use a Style key.
- All Buttons use Btn.Secondary (only kind currently needed; the rest of the
set arrives when W3's purge / download flows land).
- ConflictRow refactored: wraps HotkeyCard + ConflictRow in a single 440 px
StackPanel with bottom Margin 28. ConflictRow gets `Margin="0,1,0,0"` (1 px
gap below HotkeyCard) instead of the previous `Margin="0,-20,0,28"` negative-
margin tuck. Section gap now lives on the parent, not on each child.
Modified: src/KusPus.App/App.xaml
- Application.Resources now merges the four Styles/*.xaml dictionaries so
every keyed style is reachable app-wide (including future OnboardingWindow
reuse).
Build: 0/0. Tests: 167/167 still green. Smoke: clean launch.
P1-10 (per-tab UserControl extraction) deferred to a post-W3 cleanup — doing
it now would mean reshuffling every W3 addition into newly-created files. The
ledger in docs/APP_DESIGN.md §13.5 reflects this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit W3: missing UX — history search/purge, log clear, model downloads, contrast
APP_DESIGN.md §13.5 P0-3, P0-5, P1-2, P1-3, P1-4. Ships the four user-facing
gaps the audit flagged. All wired against existing services — no new layers.
P0-3 — DisabledText contrast
- ThemeTokens.cs: dark #5CFFFFFF → #80FFFFFF (~3.0:1 → ~5.1:1 vs AppBg).
- Light #50141414 → #A0141414 (~1.9:1 → ~4.7:1 vs AppBg).
- Both clear WCAG AA normal-text 4.5:1. Used for hotkey hint, model "Not
installed" label, disabled buttons.
P0-5 — Mic-active disclosure (already in W1)
- The Audio "Live level" subtitle reads "Active only while this tab is open.
Audio is never recorded." Tracking the ledger to "done" — no code change.
P1-3 — Privacy Logs row
- Two-row card: "Log size · {size}" with Clear logs ghost-danger button, then
"Log folder · {path}" with Open in Explorer secondary button.
- RefreshLogsSize enumerates LOCALAPPDATA\KusPus\logs\*.log on Loaded.
- OnClearLogsClick confirms via MessageBox (No default), then File.Delete each
*.log. Today's open log is held by Serilog's FileSink — skipped without
error, count reported in log.
- FormatBytes handles bytes / KB / MB.
P1-2 — History search + bulk footer
- Search box at top with Segoe Fluent icon, placeholder overlay, clear "×"
button. 250 ms DispatcherTimer debounces TextChanged → HistoryStore.SearchAsync
(FTS5 backing). Empty query reverts to "most recent first".
- Footer above 1px divider: live row count, "{n} matches for '…'" when filtered,
Purge all history Btn.Danger with MessageBox confirm. Calls HistoryStore.PurgeAllAsync.
- PurgeAllButton.IsEnabled gates on row count > 0 || query is not null.
P1-4 — Models download flow
- Per-model state machine in _modelDownloads dictionary keyed by id.
- BuildModelStatusRegion dispatches on state: Active / Installed / Downloading
(180 × 4 px mint progress bar + percent in mono + Cancel ghost) / Error
(red message + Retry secondary) / Not installed (Download secondary).
- OnModelDownloadClick: pre-checks OfflineMode (clearer message than letting
EgressAllowlistHandler throw mid-stream), spawns Task.Run with cancellable
cts. IProgress<DownloadProgress> marshals to UI thread, throttled to 0.5 %
steps so the StackPanel rebuild doesn't dominate CPU on a fast link.
- OnModelCancelClick: cts.Cancel(). Completion continuation handles cleanup
for both cancellation (silent) and failure (sticky error + Retry).
- ShortenDownloadError strips ModelManager's "HTTP error downloading …:" prefix.
Build: 0/0. Tests: 167/167.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX follow-up: History as a table · pill Settings → Preferences · spacing rhythm
History tab — table layout (audit follow-up)
- Replace card-list rendering with a Grid-based table: header row in
Type.Eyebrow style above, 6-column body rows below. Columns: status (14) /
time (78) / app (110) / transcript (*) / model (72) / duration (52).
- Per the skill's number-tabular rule: time, model, duration use Cascadia
Mono so columns stay aligned across rows.
- Per truncation-strategy: transcript and app truncate with ellipsis +
ToolTip exposing the full text on hover. Time column ToolTip shows the
full timestamp.
- Per gridline-subtle: 1 px BorderDivider between body rows, 1 px
BorderSubtle between header and body. No row striping.
- Row hover: HoverSubtle background via Style trigger on the HistoryRow
Border (Cursor=Hand for affordance).
- Right-click context menu per row: "Copy text" (Clipboard.SetText) and
"Delete" (HistoryStore.DeleteAsync + ReloadHistoryAsync).
- ShortModelId strips "ggml-" prefix to match the sidebar's compact form.
- Failed transcripts: red dot + italic red transcript column; rest of the
row stays normal so the failure mode reads as one cell, not the row.
Pill Settings button wired to Preferences modal
- Add SetSettingsAction(Action) to FloatingPillWindow, mirroring
SetCloseAction's pattern.
- App.OnStartup wires it to _mainWindow.ShowOn("general") AFTER MainWindow
is constructed (the existing pill setup runs before MainWindow exists).
- Tooltip "Settings — coming soon" → "Open Preferences".
Spacing rhythm normalization
- Models tab: list bottom margin 12 → 16 (align with the 8-grid inter-block
rhythm; "16 = block gap" is now consistent across History search bar and
Models list).
- About tab: header StackPanel bottom 32 → 28 (matches the section gap
rhythm used everywhere else, instead of being one-off heavier).
- History footer: top margin 18 → 16 (16 is the canonical block gap).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* UX audit Round 2: tokens, surfaces, expanded typography, History single-card
Parallelised Phase 1 (5 sub-agents wrote disjoint style files concurrently),
then serial Phase 2 sweep of MainWindow + App.xaml + APP_DESIGN.md.
NEW style files (Phase 1, parallel)
- Styles/Tokens.xaml — Space.xs..xxl + Pad.Tight/Default/Hero + Radius.Sm/Md/Lg
- Styles/Surfaces.xaml — Surface.Default/Hero/Tight/Warning/Mint (5 Border styles)
- Styles/Inputs.xaml — Input.Search TextBox style
- Styles/Typography.xaml extended — 10 new Type.* roles, dead Type.MonoXs removed
- Styles/Buttons.xaml extended — new Btn.IconGhost; header docs call-site inventory
Phase 2 — App.xaml wires the 3 new dictionaries (Tokens first so others can
reference its tokens), then sweep MainWindow.xaml + MainWindow.xaml.cs:
MainWindow.xaml migrations
- Hotkey card Border → Surface.Hero (drops inline Padding 22,20 override)
- ConflictRow Border → Surface.Warning (collapses 5 inline attrs)
- Local-first Border → Surface.Mint (collapses 5 inline attrs)
- HotkeyHint TextBlock → Type.HintItalic
- ConflictText TextBlock → Type.WarningBody
- AboutVersion TextBlock → Type.Body
- AboutBuildLine Margin 0,4,0,0 → 0,3,0,0 (matches Type.RowSubtitle rhythm)
- Local-first head TextBlock → Type.MintHeadline
- Local-first body TextBlock → Type.BodySmall
- MIT licensed TextBlock → Type.Footnote
- "Press a hotkey" TextBlock → Type.HintItalic
- StatusLabel TextBlock → Type.SidebarStatus (was Type.MonoSm-with-override misuse)
- History search bar magnifier → Type.IconSm
- History search box TextBox → Input.Search
- History search clear Button → Btn.IconGhost
- About re-run card Margin 0,0,0,32 → 0,0,0,28 (matches section gap)
- Sidebar footer Grid Margin 18,8,18,14 → 14,8,14,14 (matches sidebar 14)
History tab — unified single composed card (Q3 from user audit decisions)
- Outer RowCard Padding="0" wraps a StackPanel of inner Borders.
- Search bar (Padding 14,8, bottom 1 px divider), table header (Padding 14,10,
bottom 1 px divider), HistoryList (HistoryRow style provides per-row bottom
divider), bulk footer (Padding 14,12, no top border — last row's bottom
border IS the separator → no double line).
- Reads as one "history widget" instead of four separately-styled blocks.
MainWindow.xaml.cs code-behind sweep
- New TypeStyle(string) helper (mirrors Theme()) to pull Type.* styles from
Application.Resources for code-built TextBlocks.
- BuildModelRow title/subtitle → Type.RowTitle / Type.RowSubtitle
- BuildBundledBadge child TextBlock → Type.BadgeMint
- BuildModelDownloadingRegion percent → Type.MonoSm
- BuildModelErrorRegion error text → Type.ErrorInline
- BuildHistoryRow TIME / MODEL / DUR columns → Type.MonoSm (transcript + app
columns + Installed/Active status stay inline because Foreground flips per
state — no single Type.* role covers both colour states).
- Empty-state TextBlock in ReloadHistoryAsync → Type.HintItalic
- Dropped unreachable "(none)" branch in RenderModelsTab (audit P2-8).
Docs: APP_DESIGN.md §13.5 ledger updated (P2-8/-9 done, P2-10 won't-fix with
reason); new §13.6 documents the Round 2 work — token system, surface variants,
typography role catalogue, inputs/IconGhost, spacing fixes, History
unification, code-behind cleanup, dead-code policy.
Parallelisation safety
- 5 sub-agents in Phase 1 each owned exactly one file (disjoint writes — no
lost-update risk). None ran dotnet build (avoids bin/obj corruption). The
orchestrator does all building serially in Phase 3 after killing any running
KusPus.exe to avoid output-DLL locks.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* About tab: byline + 4 social icons · Models/About card spacing · 1px→8px
Author byline (About → bottom-right)
- "Made by Devang Kumawat" + LinkedIn / X / GitHub / Portfolio icon row at
the bottom-right of the About tab, sitting directly on AppBg (no card —
reads as a personal touch, not part of the design-system surface inventory).
- Text uses Type.Footnote (12 Medium SecondaryText) — matches the existing
"MIT licensed" line on the left. Per UI UX Pro Max rule weight-hierarchy:
text carries the byline weight, icons are visually subordinate.
- Each icon: 14×14 Viewbox inside a Btn.IconGhost (28×28 click target).
Aspect ratio locked by the Viewbox's default Uniform Stretch and the
underlying 24×24 viewBox.
- Theme tinting: Fill="{DynamicResource MutedText}" (filled paths) or
Stroke="{DynamicResource MutedText}" (Lucide globe) — no per-theme assets,
one shared rendering for dark+light.
Icon sources (saved to icons/social/ + LICENSE.md attribution)
- LinkedIn / X / GitHub: Simple Icons (CC0) via jsDelivr simple-icons@v11
and raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/.
- Portfolio (globe): Lucide (ISC) from raw.githubusercontent.com/lucide-icons/lucide.
- Saved as .svg files for documentation/license tracking; actual rendering
inlines the path data in MainWindow.xaml so the fill binds to theme tokens
(SharpVectors SvgViewbox can't easily theme-tint).
Each icon button OpenUrl(...) → Process.Start with UseShellExecute=true.
Single helper handles Win32Exception + FileNotFoundException for missing
default browser without crashing.
Links wired
- LinkedIn → https://www.linkedin.com/in/devangk003/
- X → https://x.com/devang_kumawat
- GitHub → https://github.com/devangk003
- Portfolio → https://lnk.bio/devangk003
Spacing (user audit feedback — 1 px stacking felt cramped)
- Models tab BuildModelRow inter-row Margin 1 → 8 (per model row reads as
its own card now, not a grouped 1-px-divider stack).
- About tab Resources/Log-folder cards Margin 0,0,0,1 → 0,0,0,8. Re-run
card already at 0,0,0,28 (section gap below it).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Sentry: forward unhandled exceptions to CaptureException
Audit gap closed. Previously the three top-level handlers
(OnUnhandledException / OnDispatcherUnhandled / OnUnobservedTask) only
wrote to Serilog — Sentry's own AppDomain auto-hook fires in parallel but
WPF dispatcher exceptions were swallowed by e.Handled=true before Sentry
could see them. Now each handler logs first, then forwards via the new
TryReportToSentry helper.
TryReportToSentry is gated on _crashReporter?.IsActive so the call no-ops
when the user hasn't opted in (or Offline Mode killed the SDK). The Sentry
call itself is wrapped in try/catch so a Sentry failure can't recurse into
another unhandled exception.
Behaviour summary
- Crash Reports OFF: handlers log locally, no network. Same as before.
- Crash Reports ON, Offline Mode OFF: every unhandled exception (AppDomain,
WPF dispatcher, unobserved Task) reaches your Sentry EU project with the
scrubbing pipeline applied.
- Crash Reports ON, Offline Mode ON: CrashReporter shuts the SDK down →
IsActive==false → handlers skip the Sentry call. Local logging still runs.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix: OnboardingWindow black corners + model download pinned to real HF commit
Onboarding rounded corners (APP_DESIGN §13.7)
- Root cause: WindowStyle=None + AllowsTransparency=False + Background=Transparent
renders the area outside the inner Border's CornerRadius as black. The
inner <Border CornerRadius=12> shows but the 4 corner triangles around it
fill with WPF's solid black for "transparent-but-not-actually-transparent".
- Fix per Microsoft's "Apply rounded corners in desktop apps for Windows 11"
guidance:
- XAML: Background="Transparent" → Background="{DynamicResource AppBg}".
Corners blend on Win10 fallback.
- Code-behind: DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE=33,
DWMWCP_ROUND=2, sizeof(int)) in OnSourceInitialized. Win11 rounds the
OS-level window edge; Win10 is a silent no-op.
- Corner-radius spec deviation 12 → 8 px (Radius.Lg). The DWM API only
supports DWMWCP_ROUND (~8 px) or DWMWCP_ROUNDSMALL (~4 px) — no path to
custom 12 px without AllowsTransparency=True (loses Mica + reintroduces
the cutout bug). 8 px is also MainWindow's curvature → both surfaces share
one canonical radius via the Radius.Lg token. APP_DESIGN §4.1 updated;
full rationale in §13.7.
Model download pinned + verified
- Replaced models.json placeholders (TODO_PIN commit + TODO_FILL SHAs) with
real values fetched from HuggingFace's tree API:
commit = 5359861c739e955e79d9a303bcbc70fb988958b1 (2024-10-29)
sha256 = LFS digests pulled per file from /api/models/.../tree/<commit>
sizeBytes = corrected to HF's actual sizes (placeholder bytes were
slightly off, would have shown wrong progress-bar totals)
- 5 models wired: ggml-tiny.en (77.7 MB · bundled), ggml-base.en (148 MB),
ggml-small.en (488 MB), ggml-medium.en (1.53 GB), ggml-large-v3 (3.10 GB).
- Models tab Download button now hits real URLs → HuggingFace serves the
.bin → ModelManager verifies SHA-256 against the manifest entry → File.Move
to %LOCALAPPDATA%\KusPus\models\. Behaviour matches TECH_SPEC §18.
Build: 0/0. Tests: 167/167. Smoke: clean launch + Sentry init OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio tab: fix mic-always-on + add LIVE indicator + restore Test transcription
P0 mic-always-on bug
- Root cause: SelectTab's StartAudioMeter/StopAudioMeter only ran on tab
switches. Closing the Preferences window with X (hide-instead-of-close per
§3.1) left the WasapiCapture open → mic icon stayed in the system tray
indefinitely.
- Fix: hooked Window.IsVisibleChanged. When IsVisible=false → StopAudioMeter().
When IsVisible=true AND Audio tab is currently showing → StartAudioMeter()
to resume.
- Also added StopAudioMeter() to the OnClosing _allowClose path so app exit
releases the mic too. StopAudioMeter now resets meter visuals (fill width +
peak tick opacity) so a paused meter doesn't show stale levels on resume.
● LIVE indicator (privacy affordance — UI UX Pro Max progressive-disclosure)
- Small mint dot + LIVE eyebrow shown next to "Microphone level" only while
the WasapiCapture is open. Toggled in StartAudioMeter / StopAudioMeter.
Test transcription — fully functional (restored from W1 placeholder)
- State machine: Idle → Recording (5 s countdown) → Transcribing (spinner) →
Result (transcript shown inline) or Error (red message + Retry).
- Single button doubles as Cancel mid-flight (CancellationTokenSource).
- Mic contention handled: StopAudioMeter() before AudioRecorder.StartAsync;
StartAudioMeter() resumes after completion / cancellation IF window is
still visible AND Audio tab is still showing.
- MainWindow constructor now takes IAudioRecorder + IWhisperRunner (added to
the App.xaml.cs DI wire-up). The active model is resolved via
IModelManager.Resolve before the mic opens — fast-fail if the model is
missing.
- Result text rendered in a SurfaceInput-tinted Border with BodySmall
typography; error text overrides Foreground to ErrorRed.
- Temp WAV from AudioRecorder.StopAsync deleted after transcription
(best-effort; IOException swallowed).
- CA1001 suppression added to MainWindow with rationale (mirrors App's
suppression — Window owns its lifecycle, _testCts disposed in OnClosing).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* History hover-actions + Models redesign (action buttons, no radios)
History tab — hover-revealed row actions
- Per UI UX Pro Max convention for productivity data tables (Gmail / Notion /
Linear pattern): on row hover, the model + duration cells are replaced
with Copy + Delete Btn.IconGhost buttons. No permanently-visible button
clutter in the read-heavy table.
- Border MouseEnter / MouseLeave toggles the action StackPanel Visibility;
Background=Surface paints over the model+duration columns when shown.
- Right-click ContextMenu retained as the keyboard / power-user path. Both
paths now route through shared helpers CopyTranscriptToClipboard +
DeleteTranscriptAsync, eliminating duplicated try/catch blocks.
- Icons: Segoe Fluent Icons "Copy" (E8C8) + "Delete" (E74D). Delete icon
tinted ErrorRed.
Models tab — radio buttons replaced with state-driven action CTAs
- New row layout: 4 px left-edge accent strip + title row (name + Bundled +
ACTIVE badge if applicable) + state-driven button on the right.
- Five visual states per UI UX Pro Max state-clarity rule:
Active — MintTint card bg + mint accent + ACTIVE badge, no button
(action already performed — primary-action rule).
Installed — neutral card, no accent, "Use this model" Btn.Primary.
Not installed — neutral card, no accent, "Download" Btn.Secondary
(heavier commitment than primary).
Downloading — neutral card, mint accent, progress + percent + Cancel
Btn.Ghost (existing BuildModelDownloadingRegion reused).
Failed — neutral card, red accent, error text + Retry Btn.Secondary
(existing BuildModelErrorRegion reused).
- ACTIVE badge: small mint-tinted Border with dark "ACTIVE" text — pulls the
user's eye to the in-use model at a glance.
- Dead code removed: OnModelRadioChecked (radio gone), BuildModelStatusRegion
(replaced by BuildModelActionRegion). BuildActiveBadge marked static
(CA1822 compliance).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio: input-device picker — user-selectable microphone
User-facing: a styled ComboBox sits next to the Microphone row in the Audio
tab. First entry is "Default device (follows Windows)"; remaining entries are
every active capture endpoint enumerated via MMDeviceEnumerator. Selection
persists as Audio.InputDeviceId in settings.json and takes effect immediately
for the live meter, Test transcription, and live dictation.
Wiring (no new layer dependencies):
- IAudioRecorder gains SetInputDeviceId(string?). AudioRecorder holds the
preferred id in a volatile field. StartAsync now goes through a
ResolveCaptureDevice helper: look up the preferred id; if it's missing /
inactive / not a capture endpoint, log a warning and fall back to the OS
default. KusPus.Audio still doesn't reference KusPus.Persistence.
- App.OnStartup pushes the initial id from PrefsStore + subscribes to
PrefsStore.Changes to propagate further updates. Composition-root pattern.
- MainWindow's level meter (separate WasapiCapture from AudioRecorder) gets
the same ResolveLevelMeterDevice helper so the meter shows the picked
device's levels, not the OS default's. Restarts on selection change.
UI (Styles/Inputs.xaml + MainWindow.xaml + .xaml.cs):
- New ComboBox.Surface style — SurfaceInput bg + BorderStrong border + 7 px
radius matching the SegmentButton wrapper aesthetic. Fully restyled
ToggleButton template (Fluent Icons chevron) and Popup template (dark/
light-themed Surface + DropShadowEffect) so the default WPF chrome doesn't
leak through. Items use MintTint for the selected row + HoverSubtle for
hover, matching the rest of the design system.
- AudioDeviceTitle TextBlock removed; replaced by the ComboBox + a new
AudioDeviceSubtitle that doubles as the error surface for "no mic" / "mic
busy" states (writes to subtitle instead of overwriting the title).
- PopulateInputDeviceCombo runs on tab open + every dropdown open — cheap
enumeration picks up hot-plug USB mics without restarting the app. Combo
selection matched to the persisted preference; falls back to "Default"
silently if the saved id is no longer present.
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Audio mic picker: fix dropdown lag (remove DropShadowEffect + per-open re-enum)
Two root causes per Microsoft Learn "Optimize control performance" +
dotnet/wpf#9881:
1. DropShadowEffect on the Popup's inner Border (BlurRadius=14) was the
dominant cost — every dropdown open triggered a per-pixel blur pass.
Removed; replaced with the existing BorderStrong stroke + Surface tint
which read as elevation without the GPU work.
2. MainWindow.OnInputDeviceDropDownOpened was re-enumerating MMDevices via
MMDeviceEnumerator.EnumerateAudioEndPoints on every open — a Win32 COM
round-trip + a full ItemsSource rebuild + a visual-tree teardown. Removed
the handler. Population now happens ONCE when the Audio tab opens
(already wired). Hot-plugged devices appear on next tab visit, which is
an acceptable trade-off vs the 150 ms perceptual lag every open.
Belt-and-suspenders: ComboBox.Surface now declares VirtualizingStackPanel
as its ItemsPanel + IsVirtualizing=True + VirtualizationMode=Recycling.
Negligible for 5-10 mics but bombproof if someone has 20+ capture devices.
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 1: chrome restructure — dock drawer + pin + magic-wand buttons
Restructures the floating pill per the Organic Pill spec (Phase 1 — chrome
only; halo, hue-drift, breath, hover-visualizer arrive in Phases 2-4).
Geometry — dynamic window size
- Collapsed: 200×56 (pill only).
- Open / pinned: 320×78 (pill 320 wide + 22 px dock peek). Mica stays tight
to the visible chrome so the area around the pill doesn't render a
rectangular Mica frame. Window animates both Width and Height on hover.
- Pill anchor stays on base width 200 so the position math (multi-monitor
sticky, bottom-center default) doesn't drift center on expand.
New chrome
- Pin button (top-right corner of pill, 18×18). Hidden by default at -12°
rotation. On pill hover: fades in + rotates to 0° (180 ms / 220 ms). Click
toggles "pinned" — dock + corner buttons stay visible after the cursor
leaves, glyph + bg tint to mint.
- Magic-wand button (top-right, left of Pin, 18×18). Dormant — ToolTip
"Refine text", no Click handler. We will wire it next iteration.
- Dock drawer (22 px row below the pill, slides down + fades in on hover).
Background matches the pill so the two read as one continuous chrome.
Border CornerRadius=0,0,8,8 to share the pill's bottom rounding.
Dock contents (left → right)
- Record toggle (22×18). Red dot glyph. Click currently logs a TODO — the
real wire-up needs a public AppCoordinator.ToggleTapMode() that doesn't
exist yet; the hotkey chord remains the canonical entry point for v1.
- Mic chooser (flex-grow). [mic icon] [device name] [chevron-down] on a
subtle button bg. Click opens a real popup picker — a styled <Popup>
containing a ScrollViewer + a StackPanel of per-device <Button>s. Click a
device → SetInputDeviceIdAsync via the bridge → popup closes → label
updates. Mint-tinted selected item.
- Settings (22×18). Fluent gear, opens Preferences (existing wire).
- Dismiss (22×18). Fluent X, red hover bg, calls _onClose → Shutdown.
Layer-friendly bridges
- FloatingPillWindow defines two tiny interfaces (IPrefsStoreBridge,
IAudioRecorderBridge) and a SetBridges(prefs, audio) hook. App.xaml.cs
implements them via PrefsStoreBridge (wraps IPrefsStore for the device id
get/set) and AudioDeviceBridge (calls MMDeviceEnumerator). Keeps
KusPus.App as the only layer that knows about both Persistence and NAudio.
Removed
- Old side-only hover-extend (ButtonPanel + AnimateWidth/AnimateButtonPanel).
Replaced by the dock drawer + corner-button animation pair.
Build: 0/0. Tests: 167/167. Smoke: clean launch + pill transitions to Idle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 2: idle content swaps to visualizer + IDLE label on hover
Default idle (no hover) — unchanged: SVG voice-stack icon + "KusPus" wordmark,
just like today. The pill reads as a tiny brand mark when the user isn't
intentionally interacting with it.
On hover (still Idle) — swap to:
- 20-bar visualizer running the low-amplitude traveling-sine motion model
from the Organic Pill §3 idle-visualizer cue: amplitude 0.06-0.14,
per-bar phase offset 0.18 rad, damping k≈3.5/s, full traversal every
~2.4 s. Quiet and slow enough to disappear from peripheral vision.
- Label "IDLE · HOLD TO DICTATE" replaces "RECORDING" in the same slot.
State + hover form an orthogonal grid:
(Idle, !hover) → IdleContent (SVG + KusPus) · viz Off
(Idle, hover) → VisualizerContent (bars + IDLE) · viz HoverIdle
(Recording, *) → VisualizerContent (bars + RECORDING) · viz Recording
(other states, *) → that state's panel · viz Off
Refactor
- RecordingContent renamed to VisualizerContent (now serves both Recording
and HoverIdle modes — same Canvas, label swaps).
- New VisualizerLabel x:Name so the label text can change per mode.
- FadeContent + new ApplyIdleContent + small static FadeElement helper:
TransitionTo delegates idle-content rendering to ApplyIdleContent, which
re-evaluates IsMouseOver every time it's called.
- OnPillMouseEnter / OnPillMouseLeave call ApplyIdleContent so the swap
happens on every hover transition while in Idle.
- New VisualizerMode enum (Off / Recording / HoverIdle). OnVisualizerTick
switches motion math by mode — HoverIdle runs the sine wave; Recording
keeps the existing voice-envelope target-rolling; Off targets all bars
to 0.05 (silent).
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 3: breath + hue drift animations · Accessibility toggle
Personality animations
- Breath: ±0.6% scale pulse on PillSurface via ScaleTransform, 4 s sine cycle
(2 s in + 2 s out, AutoReverse + RepeatForever, SineEase). Subtle enough
to disappear from peripheral vision — gives the pill a "living organism"
presence without intruding.
- Hue drift: AccentBrush's middle gradient stop cycles mint #4DDBA6 →
seafoam #4DCDC2 → soft cyan #4DB8DB → back over 14 s, constant R=0x4D
band so perceived brightness stays flat (manual approximation of the
spec's OKLCH constant-L=0.84/C=0.14 constraint; WPF has no native OKLCH).
- Both wired as long-lived Storyboards (built once on Loaded, Begin/Stop
via SetReduceAnimations) so toggling is cheap.
Deferred to follow-up
- Halo: needs a backbuffer larger than the pill bounds — incompatible with
the current Mica setup (Mica would paint a rectangular tint around the
halo area). Decision point: keep Mica + skip halo, OR drop Mica for
AllowsTransparency=true + custom translucent gradient.
- Heartbeat blink: depends on accent-line opacity which is state-driven
(TransitionTo sets it per state). Multiplying onto state-driven base
needs a layered opacity model — deferred until heartbeat semantics are
pinned down.
Accessibility toggle (new Settings.Privacy.ReducePillAnimations field)
- New Accessibility section in Privacy tab: "Reduce pill animations" Toggle.
Default off. Saves to settings.json on flip.
- App.UpdatePillReduceAnimations combines the user toggle with
SystemParameters.ClientAreaAnimation — if either says reduce, pill pauses
personality animations (state transitions + dock slide remain active).
- Initial state applied at startup + on every PrefsStore.Changes emit.
Build: 0/0. Tests: 167/167. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill Phase 4: light-theme mint gradient on visualizer bars
Per user audit feedback that the bars should echo the icon.svg's pearly-
mint gradient.
Dark theme: unchanged — solid #EBFFFFFF SolidColorBrush (the historical
token). A mint gradient over a dark pill surface would lose the
visualizer's "voice on top" reading.
Light theme: three-stop vertical LinearGradientBrush, alpha climbs top→
bottom so each bar reads as "lit from below":
0.0 → #664DDBA6 (subtle mint, 40% alpha)
0.5 → #994DDBA6 (mid mint, 60% alpha)
1.0 → #CC1F8762 (deeper mint, 80% alpha — bottom anchors)
Implementation: VisualizerBarActive is removed from the ThemeTokens.Map
dictionary and installed via a dedicated BuildVisualizerBarActive(mode)
helper alongside the existing BuildPillSurfaceGradient. ThemeTokens.Apply
now calls both special-case builders after the simple-Color-pair loop.
The bars in FloatingPillWindow use SetResourceReference for their Fill, so
the swap fires on theme flip with no other code touching needed.
Build: 0/0. Smoke: clean launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill follow-ups: 6 bug fixes (center-expand, height recovery, picker, theming, inset)
1. Center-expand on hover — Width + Left animate together (Left -= ΔW/2)
so the pill grows symmetrically instead of right-only.
2. Height recovery on dock close — DoubleAnimations use FillBehavior.Stop
and on Completed call BeginAnimation(prop, null) + SetValue(prop, to),
freeing the animated values so the pill collapses cleanly with no black
gap underneath.
3. Mic picker now design-system styled — Popup uses Surface/BorderStrong
tokens with a 4-px-padded ScrollViewer (PanningMode=VerticalOnly,
HorizontalScrollBarVisibility=Disabled). Item template adds a hover
trigger that paints HoverSubtle on each row.
4. Picker pins the dock open — _pickerOpen flag gates OnPillMouseLeave so
the dock stays open while the picker popup is open; OnMicChooserPopupClosed
restores normal hover behavior afterward.
5. Light-theme pill carries the icon's mint — BuildPillSurfaceGradient
light stops shift from #F8F8FA/#EEEEEF2 to #F4F8F4/#E0F0E6 (subtle top
shift + slightly mintier bottom), echoing icon.svg's pearl-to-mint
gradient without changing dark-theme look.
6. Dock visually narrower than pill — DockDrawer carries Margin="24,0,24,0"
so it reads as a nested sub-element instead of a flush continuation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill follow-ups round 2: black strips + picker lag + audio tab lag
1. Black strips beside dock — DockDrawer.Margin removed. The pill window is
AllowsTransparency=False because Mica (DWMWA_SYSTEMBACKDROP_TYPE) requires
it, so any inset between the dock and the window edge renders opaque
window-background black instead of click-through. The prior 24px margin
was the "narrower than pill" aesthetic from the last batch — reverting it
here since the side-effect (black strips) is worse than the cohesive look.
2. Pill mic-picker lag — cache the device list in FloatingPillWindow. On
SetBridges we warm the cache via Task.Run + Dispatcher.BeginInvoke; each
subsequent OnMicChooserClick reads from cache (instant) and fires a
background RefreshMicCacheAsync so hot-plugged devices appear on next open.
Same root cause as the audio-tab combo lag fixed in f834cc5: MMDeviceEnumerator
.EnumerateAudioEndPoints is a synchronous Win32 COM round-trip (~150ms).
UpdateMicChooserLabel uses the same cache fall-through.
3. Audio tab loading lag — OpenAudioTabAsync runs the heavy init off the
dispatcher. EnumerateInputDeviceItems (COM) and the WasapiCapture
ctor (driver shared-mode negotiation, ~150-500ms on some hardware) both
await Task.Run, then the combo's ItemsSource is set + StartRecording
fires on the UI thread. The Audio panel paints immediately; the device
combo + LIVE meter populate as each piece completes.
Surface kept stable: synchronous StartAudioMeter() façade still exists
so the 3 non-tab-open callers (visibility change, mid-test resume,
device-change restart) read unchanged.
Build: 0/0. Smoke: pill places + hook installs cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill: pin = compact-mode toggle + mint idle wordmark
User-spec rewire of the pin semantics. Previously "latch dock open"; now
"compact mode" — clicking pin contracts the pill back to 200×56, slides
the dock back, but keeps the pin button visible at all times so the user
can unpin.
Behavior matrix:
Pinned OFF (default):
hover → expand 200→320, slide dock down, fade pin+wand in
leave → contract, slide dock up, fade pin+wand out
pin click → enter pinned + contract immediately (if already expanded)
Pinned ON:
hover → swap SVG+wordmark → visualizer+IDLE label (NO resize, NO dock)
leave → swap visualizer → SVG+wordmark (NO resize, NO dock)
pin click → exit pinned; if currently hovered, expand back to hover view
pin button stays visible the entire time (mint-tinted)
Implementation:
- OnPinClick — inverted: becoming pinned calls CloseDock; becoming unpinned
+ hovered calls OpenDock. Unpinned + not-hovered stays put.
- OnPillMouseEnter/Leave — gate OpenDock/CloseDock on !_isPinned so hover
doesn't trigger the expand/contract while pinned. ApplyIdleContent still
runs in both branches so the content swap (SVG ↔ visualizer) works.
- AnimateCornerButtons — effectiveVisible = visible || _isPinned. Keeps
the pin button at opacity=1 and angle=0 while pinned regardless of what
the caller asked for.
Plus: idle KusPus wordmark now Mint instead of MutedText — picks up the
brand accent so the resting pill carries the product's color cue.
Build: 0/0. Smoke: pill places + hook installs clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Tray redesign + record-toggle wiring + nudge fix
Three landed-together changes for the tray + pill experience.
1. Pill record-toggle wired (Cluster A)
- SetRecordToggleAction(Action) on FloatingPillWindow; App binds to
AppCoordinator.ToggleFromTray. Per user spec the toggle does NOT
auto-capture a foreground target — the post-transcribe paste lands
wherever focus happens to be at the time.
- On toggle-start a RecordNudgePopup balloon appears above the RecordButton
("Click into your text field") for 6s. Auto-dismisses when state moves
to Recording. Previous 3s window was too short to read — user feedback.
- RecordGlyph changed from Ellipse to Rectangle that morphs dot ↔ rounded
square depending on FSM state.
2. Custom WPF tray right-click menu (Cluster B)
Replaces WinForms ContextMenuStrip with TrayMenuWindow.xaml — a
borderless, transparent, design-system-styled popup matching
Tray_light.png / Tray_dark.png:
- KusPus header with state-aware "Version 1.0.0 · {Idle|Recording|Transcribing}"
- Toggle recorder row with hotkey keycap (live-bound to PrefsStore.Hotkey)
- Active model: <name> row with chevron, opens models tab
- Preferences… opens general tab
- History… opens history tab
- Quit in ErrorRed
Shows at cursor on NotifyIcon.MouseClick (right). Closes on Deactivated
(focus lost) or any item click. WS_EX_TOOLWINDOW so it's hidden from
Alt-Tab/taskbar.
3. State-aware tray icons (Cluster C)
icons/icon-{idle,recording,error}.svg generated to .ico via tools/IconBuilder.
Recording overlays a red dot + glow on the bars; Error overlays a red
warning triangle. TrayManager subscribes to AppCoordinator.State and
swaps NotifyIcon.Icon based on the FSM state (treating a failed PostPaste
snapshot as Error for its hold duration). All three .ico files are
Resources in KusPus.App.csproj.
Build: 0/0. Smoke: pill places + tray icon visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill UX polish: BETA labels + pin lock + audit pass
Three concerns folded into one commit because they share the pill XAML/code-behind:
1. UX pass (per user spec):
- Pill record button + tray menu both labelled "Toggle Recording [BETA]"
(verb-form + dogfood expectation-setting). Tray chip is mint-coloured.
- Magic wand is dormant — rendered at 0.35 opacity with Arrow cursor +
tooltip "Refine text — coming soon" so the disabled state is legible
visually, not just in the tooltip.
- Pin semantics extended: now also locks the pill's screen position. Drag
short-circuits when _isPinned. Drag cursor (SizeAll) flips to Arrow when
pinned so the lock is telegraphed.
- Added CompactRecordButton at the pill's top-LEFT corner, visible only
when pinned. Pinned mode hides the dock, so without this the user would
have to unpin just to record. Sits opposite Pin/Wand on the right for
visual balance.
2. Nudge bug fix (the 6s timer was a red herring — TransitionTo's
"dismiss-on-Recording" rule was firing within ~ms of click since the FSM
moves to Recording immediately after ToggleFromTray. Dropped that rule;
timer bumped 6s→10s as the sole dismissal path. Comment explains why.
3. Full UX audit pass (10 items from the 2026-05-17 self-audit):
- #1 CompactRecord 22×18 r=5 → 18×18 r=4 (matches Pin/Wand cluster)
- #2 Design-system icon size tokens added to Styles/Tokens.xaml:
Icon.Glyph=11, Icon.Chevron=9. Bound on Wand, Pin, Settings, Close,
MicChooser icon (was 10), MicChooser chevron (was 8).
- #3 MicChooser hover: Opacity=1.4 (silent no-op — WPF clamps at 1) →
Background=SurfaceElevated. Real, theme-aware lift.
- #4 Error text margin 6→8 px (matches Idle/Transcribing rhythm)
- #5 Dock vertical centering moved to parent Grid (Margin=6,2,6,2);
mic chooser drops its per-button vertical compensation.
- #6 Wand opacity 0.5→0.35 (clearer subordinate read)
- #7 Dropped the 1px PillInnerHighlight — only existed on the pill, not
the dock, creating a seam at the drawer junction.
- #8 PillSurface Cursor=SizeAll at rest (telegraphs drag), Arrow when pinned
- #9 Drop shadow: Direction=270 Depth=2 Blur=32 Op=0.45 →
Depth=0 Blur=14 Op=0.25. Omnidirectional soft halo, no dock bleed.
- #10 All chrome gutters standardized at 6 px (was 5 on corners, 6 on dock).
Build: 0/0. Smoke: clean (verified locally with real recording + paste).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Default theme dark + Light [BETA] + onboarding step 6 = real dictation
Three changes from the 2026-05-17 dogfood batch:
1. Default theme flipped "auto" → "dark" (AppSettings.UiSettings.Theme).
Light theme is still in beta polish, so new installs land on the polished
dark surface. DefaultSettingsTests assertion updated with rationale.
2. Preferences theme picker: "Light" → "Light [BETA]" with tooltip explaining
the beta state. Sets dogfood expectations that light surfaces may not be
fully tuned yet.
3. Onboarding step 6 (Try it) replaced fake SimulatedSentences random-pick
with a real IAudioRecorder + IWhisperRunner pipeline. 5 s countdown
recording → transcribe with active model → render actual transcript (or
error if mic/model missing). Mirrors the existing Test Transcription
pattern from the Audio tab. Threaded audio/whisper/models services through
the OnboardingWindow constructor + both call sites (App.OnStartup +
MainWindow.OnRerunOnboarding).
Prior behaviour was misleading — onboarding "tested" dictation by picking a
canned sentence from a hard-coded list, so a broken mic / missing model
didn't surface until after onboarding finished. Now the failure modes
surface during setup where the user can act on them.
Build: 0/0. Core/Persistence/Whisper/Audio test suites all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Roadmap: add R1.2-10 long-mode chunk-on-VAD streaming on second hotkey
User dogfood feedback (2026-05-17) asked for continuous "speak-pause-paste"
loop on top of the existing push-to-talk model. Researched 3 architectures
(sliding-window, chunk-on-VAD, library-binding); recommended chunk-on-VAD
+ second hotkey ("Option B") to keep the existing UX intact while giving
power users opt-in long-mode dictation.
Deferred to v1.2 per user choice — ~2 weeks build + 1 week dogfood, too
large for the current pre-v1 polish window. Entry captures full 8-cluster
plan, top-3 risks (hallucination on silence, mid-word VAD cuts, paste-into-
wrong-app race), realistic latency (~0.7-1 s per pause with tiny.en), and
the rejected-for-now soft-cap alternative.
Also clarified LT-07 (streaming partial results) as a distinct UX
hypothesis — sliding-window for visible live caption in the pill, NOT a
paste pipeline. Different architecture from R1.2-10; both can ship in
principle but R1.2-10 lands first because it answers a real dogfood ask.
Per CLAUDE.md, this edit to docs/ROADMAP.md is authorized — user
explicitly said "keep it in the roadmap for later versions".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Onboarding step 3: mic chooser dropdown + CLAUDE.md deviation log update
User dogfood ask: let the user pick their mic during onboarding (not just
see the meter for the OS default), and persist that choice until they
change it from Preferences.
Step 3 gets an OnbInputDeviceCombo above the live meter card. It writes
to PrefsStore.Audio.InputDeviceId — the same field Preferences → Audio
uses — so the selection survives onboarding-exit and stays put until the
user changes it from either surface. ResolveOnbMicDevice mirrors
MainWindow.ResolveLevelMeterDevice: looks up by saved id, falls back to
the OS default if the device is unplugged. SelectionChanged restarts the
meter capture so the user sees the level for whichever mic they just
picked.
No shared base class with MainWindow's combo — onboarding is short-lived
and a single helper would pull in more ceremony than it removes. Logic
is a faithful mirror; if a future refactor extracts a shared
HotkeyPickerControl / InputDevicePickerControl UserControl, this and the
MainWindow combo + the Audio-tab one would all collapse to one consumer.
Also updated CLAUDE.md "Deviations" with 11 new entries covering this
session's UX work (pin = compact mode + position lock, BETA labels,
tray menu redesign + state-aware icons, dark default theme, real
onboarding dictation, mic chooser in onboarding, icon-size tokens,
shadow softening, mic-chooser hover fix, roadmap R1.2-10 entry).
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Onboarding fixes: Skip=Completed, pill defer, step 3 async, apartment-marshalling bug
Four bugs / behaviors corrected in one session of dogfood feedback.
1. Skip now marks Completed=true (was: Completed=false). Onboarding modal
opens once-ever per install. Closing via Skip is honoured the same as
Finish — modal does not re-appear on next launch. Re-runnable via
About → "Run again". Prior "skip-on-skip keeps Completed=false" rule
was hostile (the user just wanted to dismiss); replaced with show-once-
ever.
2. Pill is now invisible while onboarding is open. Bind() / BindLevels()
moved out of App.OnStartup inline construction and into a new
BindPillAndShow() helper that runs AFTER ShowDialog() returns (or
immediately if no onboarding). The first BehaviorSubject snapshot
subscribes to coordinator.State which triggers the pill's FadePillIn
→ Show(), so deferring Bind is what hides the pill. Existing users
(Completed=true) get pill instantly; new users get pill after Finish/Skip.
3. Step 3 mic now loads async (mirrors MainWindow.OpenAudioTabAsync).
New OpenMicStepAsync orchestrator: page paints immediately with
"Loading microphones…" placeholder + "LOADING…" label; MMDevice enum
and WasapiCapture init run on Task.Run; UI populates when ready.
Previously the entire dispatcher blocked for ~250 ms on first step 3
entry (driver shared-mode negotiation).
4. Cross-apartment MMDevice access bug (fix-of-fix). The first async pass
returned the MMDevice from the Task.Run lambda and then read
.FriendlyName on the dispatcher — NAudio's IMMDevice doesn't support
standard COM cross-apartment proxy marshalling, so the property getter
threw InvalidCastException → E_NOINTERFACE. That landed in
UnobservedTaskException (silent) and the user saw "Microphone blocked"
even though nothing was using the mic. Fix: read FriendlyName INSIDE
the Task.Run lambda (MTA where the device was created), return only
the string + WasapiCapture across the await. MMDevice never crosses
thread boundaries. WasapiCapture is fine cross-thread because it
caches its WaveFormat internally before its ctor returns — that's
why MainWindow.OpenAudioTabAsync (which only returns the capture)
never had this bug.
Validated from the live log:
System.InvalidCastException: Unable to cast COM object ...
to interface type IMMDevice ... E_NOINTERFACE
at NAudio.CoreAudioApi.MMDevice.GetPropertyInformation
at OnboardingWindow.StartMicCheckAsync() line 641
Also broadened the Task.Run catch from
COMException + MmException
to general Exception, since NAudio's WasapiCapture can throw a wider
set (InvalidOperationException on busy device, ArgumentException on
malformed format, etc.). Added an outer try/catch on OpenMicStepAsync
so any unhandled error surfaces as ShowMicError instead of silent
stuck-Loading.
Build: 0/0. Cross-apartment fix validated by code trace + matched
against MainWindow.OpenAudioTabAsync (which doesn't return MMDevice
across threads and works correctly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* About tab: globe icon matches LinkedIn visual size
The four social icons (LinkedIn, X, GitHub, Portfolio-globe) all use a
14×14 Viewbox wrapping 24×24 vector content. LinkedIn/X/GitHub paths
fill their viewBox edge-to-edge (0–24 on both axes), so they render at
the full 14×14 visual size. The globe was drawn in a 24×24 Canvas with
the ellipse at (2,2) W=20 H=20 plus a stroke=2 outline — that left 2 px
of padding around the geometry, so the globe rendered at ~20/24 ≈ 83%
of the other icons' visual size.
Fix: expand the geometry to fill the full 24×24 box.
Ellipse: (1,1) W=22 H=22 + stroke=2 → visible ink spans 0–24.
Meridian arc: radius 14.5 → 15.95 (×22/20 scale factor); endpoints
move from y=2/22 to y=1/23.
Equator line: M2 12 h20 → M1 12 h22.
All four icons now render at the same effective 14×14 visual size. No
aspect-ratio change — geometry preserved, only the bounds expanded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pill compact-record grey + nudge 2s + social icon size parity
Four small UX tweaks from dogfood feedback (2026-05-17).
1. CompactRecord glyph (the corner record button visible while pinned) is
now grey (MutedText) when idle, red (#EF5350) when recording. Previously
it was always red — looked like "recording in progress" even at rest.
Grey reads as "available, tap to start"; red reserved for active state.
2. CompactRecord glyph bumped 8×8 → 10×10 (RadiusX 4 → 5 idle, 1.5 → 2
recording). The visible footprint now roughly matches the pin glyph's
ascent at FontSize=Icon.Glyph (11), so left/right corner clusters look
visually balanced. Button itself stays 18×18 with the same 6 px margin
from the pill edge as the pin StackPanel — positions were already
symmetric; the parity issue was glyph size.
3. UpdateRecordGlyph now swaps CompactRecord.Fill on state change (grey
↔ red) in addition to the existing radius morph. Dock RecordGlyph
stays always-red (it's the dock's record identifier; grey would lose
its affordance).
4. Nudge timer 10 s → 2 s. The "Click into your text field" hint is now
a brief flash, not a lingering popup. User feedback: 10 s sat there
long after they had already moved on.
5. About-tab social icons: wrapped each Path in a fixed-size 24×24 Canvas
so the Viewbox uses the canvas bounds (always 24×24) rather than the
path's computed bbox. Path bboxes vary subtly — GitHub's "M12 .297"
start offset, Bezier control points extending past the visible curve,
X's 0.258 left edge — which caused uneven rendered sizes when Viewbox
uniformly stretched each to 14×14. With the Canvas wrapper, all four
(LinkedIn, X, GitHub, Globe) are guaranteed to render at exactly the
same effective visual size.
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* About tagline + pill bottom-corners squared while dock is open
Two surgical fixes.
1. About-tab tagline changed from "Press a hotkey. Speak. Get pasted."
to "Local Privacy First" — direct product-pillar wording per user
ask. Same Type.HintItalic style, same position; just the string.
2. PillSurface.CornerRadius drops from 8 → (8,8,0,0) when the dock
slides into view, and back to 8 when the dock slides away. The
pill's bottom edge is flat while the dock is visible, so the seam
between pill bottom and dock top (which has CornerRadius=0,0,8,8)
reads as one continuous shape instead of two stacked rounded
rectangles with visible "ears" at the seam.
Implementation: the corner-radius swap lives inside OpenDock() and
CloseDock(). OnPillMouseEnter/Leave + OnPinClick already gate
OpenDock/CloseDock on !_isPinned (pinned mode uses content-swap
without expanding), so pinned compact-mode never enters OpenDock
and the pill keeps its full 8 px rounded corners — exactly the
"no corner-radius changes in pinned state" constraint.
Snap (not animate) since WPF's CornerRadius isn't a natively
animatable DependencyProperty. The snap happens at the START of
each method so the bottom edge is flat the full time the dock is
becoming visible (OpenDock case) and the round-back happens just
as the dock starts going away (CloseDock case — the brief
round-bottom-over-still-visible-dock artifact is during the
subordinate "going away" animation).
No XAML changes to the PillSurface element; the default
CornerRadius="8" stays as the initial / fully-collapsed value.
Build: 0/0. Smoke: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@…
No description provided.