agent: dogfood polish 2026/04/19 — Linux native chrome + History redesign + mic picker cleanup
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

Second dogfood sprint. Headline fix: Linux now uses native KWin/Mutter
decorations instead of fragile frameless `startResizeDragging`, which
collapsed diagonal corner resize to a single axis and made drag feel
laggy. macOS / Windows keep custom chrome via `useCustomChrome` gate.

Other changes:

- Cross-window preferences sync via `kon:preferences-changed` Tauri
  event — theme and font changes propagate live to float/viewer.
- Hotkey recorder rewritten to use capture-phase document listener
  gated by $effect; button focus was unreliable in webkit2gtk.
- History page redesigned for cognitive-load hygiene: title-first
  compact row, inline title input, Edit popout opening /viewer in
  edit mode, clipboard export as .md with YAML frontmatter, manual
  tag chips + + Add tag input, header tag filter (cap 7), global
  Starred filter, `tag:xyz` search syntax.
- `deriveAutoTags` kept as empty hook for post-Task-7 LLM topic tags;
  research found all previous auto-tag chips redundant with row UI.
- Viewer window adds edit mode with debounced-save textarea; native
  title renamed to "Kon - Transcription Editor".
- Window minimums updated per GNOME HIG + WCAG reflow research:
  main 960x600, float 360x480, editor 560x520.
- Microphone picker filters raw ALSA strings (hw:, plughw:, front:,
  sysdefault:, null) and dedupes by CARD=X. New `description` field
  on DeviceInfo reads /proc/asound/cards so Blue Yeti shows as "Blue
  Microphones" instead of the short "Microphones" card name.
- GPU reporting fixed: get_runtime_capabilities now returns
  accelerators=[cpu,vulkan] and whisper.supports_gpu=true, matching
  the transcribe-rs whisper-vulkan feature linked unconditionally.
- ResizeHandles kept for macOS/Windows frameless: 12px edges, 20px
  corners via CSS vars, pointerdown + setPointerCapture, corners
  above edges in z-order, rendered as sibling (not child) of the
  animated layout root so `position: fixed` is viewport-relative.
- Dueling drag-region handlers removed — `data-tauri-drag-region` and
  manual `startDragging()` were stacked on the same elements; kept
  the manual handler which has the button/input early-return logic.

See HANDOVER.md for the full session log and deferred items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 14:30:42 +01:00
parent 8c9c9390d8
commit ea48d03cee
21 changed files with 1079 additions and 157 deletions

View File

@@ -0,0 +1,141 @@
<script>
// Invisible resize-handle overlays around each Kon window edge.
// Needed because Kon runs with `decorations: false`, so the WM
// provides no resize affordance on KDE/GNOME Wayland.
//
// Architecture: eight fixed-position divs are the click target for
// resize. Because the divs themselves are the event target (not any
// ancestor), Tauri's `data-tauri-drag-region` delegated handler walks
// the div's ancestors, finds no drag-region, and does not start a
// window move. This gives us a clean separation: resize wins on the
// edges, drag wins on the titlebar.
//
// Hit zone sizes come from CSS custom properties so every Kon window
// across the app stays identical. See `--kon-resize-edge` and
// `--kon-resize-corner` in app.css.
//
// Placement requirement: instances of this component MUST be mounted
// as a sibling of the animated/transformed layout root (not a child),
// or `position: fixed` becomes relative to that transformed ancestor
// instead of the viewport.
import { getCurrentWindow } from "@tauri-apps/api/window";
import { hasTauriRuntime } from "$lib/utils/runtime.js";
const enabled = hasTauriRuntime();
async function startResize(e, direction) {
if (!enabled) return;
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
// Lock the pointer to this handle BEFORE asking Wayland to begin a
// resize. Without capture, KWin's xdg_toplevel.resize grab arrives
// between mousedown synthesis and pointer movement, often losing the
// diagonal component and collapsing a corner drag to a single axis.
// pointerdown + setPointerCapture keeps the direction pinned.
try {
e.currentTarget?.setPointerCapture?.(e.pointerId);
} catch {}
try {
await getCurrentWindow().startResizeDragging(direction);
} catch {}
}
</script>
{#if enabled}
<!-- Edge strips. Inset by corner size so edge and corner zones don't overlap. -->
<div class="kon-rh kon-rh-n" onpointerdown={(e) => startResize(e,"North")}></div>
<div class="kon-rh kon-rh-s" onpointerdown={(e) => startResize(e,"South")}></div>
<div class="kon-rh kon-rh-w" onpointerdown={(e) => startResize(e,"West")}></div>
<div class="kon-rh kon-rh-e" onpointerdown={(e) => startResize(e,"East")}></div>
<!-- Corner hit zones, larger for easier diagonal targeting. -->
<div class="kon-rh kon-rh-nw" onpointerdown={(e) => startResize(e,"NorthWest")}></div>
<div class="kon-rh kon-rh-ne" onpointerdown={(e) => startResize(e,"NorthEast")}></div>
<div class="kon-rh kon-rh-sw" onpointerdown={(e) => startResize(e,"SouthWest")}></div>
<div class="kon-rh kon-rh-se" onpointerdown={(e) => startResize(e,"SouthEast")}></div>
{/if}
<style>
.kon-rh {
position: fixed;
z-index: 2147483646;
background: transparent;
pointer-events: auto;
/* Prevent the browser from consuming the pointerdown for its own
defaults (text selection, drag scroll) before we capture it. */
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
/* Corners stack above edges so, at the 1-2 px of overlap on some
compositors, a corner click is never mistaken for an edge click. */
.kon-rh-nw,
.kon-rh-ne,
.kon-rh-sw,
.kon-rh-se {
z-index: 2147483647;
}
/* Edges */
.kon-rh-n {
top: 0;
left: var(--kon-resize-corner);
right: var(--kon-resize-corner);
height: var(--kon-resize-edge);
cursor: n-resize;
}
.kon-rh-s {
bottom: 0;
left: var(--kon-resize-corner);
right: var(--kon-resize-corner);
height: var(--kon-resize-edge);
cursor: s-resize;
}
.kon-rh-w {
top: var(--kon-resize-corner);
bottom: var(--kon-resize-corner);
left: 0;
width: var(--kon-resize-edge);
cursor: w-resize;
}
.kon-rh-e {
top: var(--kon-resize-corner);
bottom: var(--kon-resize-corner);
right: 0;
width: var(--kon-resize-edge);
cursor: e-resize;
}
/* Corners */
.kon-rh-nw {
top: 0;
left: 0;
width: var(--kon-resize-corner);
height: var(--kon-resize-corner);
cursor: nw-resize;
}
.kon-rh-ne {
top: 0;
right: 0;
width: var(--kon-resize-corner);
height: var(--kon-resize-corner);
cursor: ne-resize;
}
.kon-rh-sw {
bottom: 0;
left: 0;
width: var(--kon-resize-corner);
height: var(--kon-resize-corner);
cursor: sw-resize;
}
.kon-rh-se {
bottom: 0;
right: 0;
width: var(--kon-resize-corner);
height: var(--kon-resize-corner);
cursor: se-resize;
}
</style>