agent: dogfood polish 2026/04/19 — Linux native chrome + History redesign + mic picker cleanup
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:
@@ -6,16 +6,17 @@
|
||||
|
||||
const modifierKeys = new Set(["Control", "Shift", "Alt", "Meta"]);
|
||||
|
||||
function startRecording() {
|
||||
recording = true;
|
||||
captured = false;
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
// Capture-phase listener; guard still belts-and-braces.
|
||||
if (!recording) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === "Escape") {
|
||||
recording = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for a non-modifier key
|
||||
if (modifierKeys.has(e.key)) return;
|
||||
|
||||
@@ -39,8 +40,21 @@
|
||||
setTimeout(() => { captured = false; }, 1500);
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
recording = false;
|
||||
// Register the listener only while recording, at the capture phase so no
|
||||
// descendant handler (or the parent layout's svelte:window keydown) can
|
||||
// swallow the event first. Button-level onkeydown would require the
|
||||
// button to hold keyboard focus after a click, which webkit2gtk on Linux
|
||||
// does not guarantee.
|
||||
$effect(() => {
|
||||
if (!recording) return;
|
||||
const handler = handleKeyDown;
|
||||
document.addEventListener("keydown", handler, { capture: true });
|
||||
return () => document.removeEventListener("keydown", handler, { capture: true });
|
||||
});
|
||||
|
||||
function startRecording() {
|
||||
recording = true;
|
||||
captured = false;
|
||||
}
|
||||
|
||||
let chips = $derived(settings.globalHotkey.split("+"));
|
||||
@@ -55,8 +69,6 @@
|
||||
: 'bg-bg-input border-border hover:border-border'}
|
||||
border transition-all"
|
||||
onclick={startRecording}
|
||||
onkeydown={handleKeyDown}
|
||||
onblur={handleBlur}
|
||||
aria-label="Record hotkey"
|
||||
>
|
||||
{#if recording}
|
||||
|
||||
141
src/lib/components/ResizeHandles.svelte
Normal file
141
src/lib/components/ResizeHandles.svelte
Normal 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>
|
||||
@@ -22,6 +22,7 @@
|
||||
function handleDragStart(e) {
|
||||
if (e.button !== 0) return;
|
||||
if (e.target.closest("button")) return;
|
||||
try { e.currentTarget?.setPointerCapture?.(e.pointerId); } catch {}
|
||||
getCurrentWindow().startDragging();
|
||||
}
|
||||
|
||||
@@ -38,20 +39,18 @@
|
||||
<div
|
||||
class="flex items-center select-none bg-sidebar border-b border-border-subtle
|
||||
{compact ? 'h-[28px]' : 'h-[32px]'}"
|
||||
onmousedown={handleDragStart}
|
||||
data-tauri-drag-region
|
||||
onpointerdown={handleDragStart}
|
||||
>
|
||||
{#if !compact}
|
||||
<!-- Left spacer: aligns with sidebar width -->
|
||||
<div
|
||||
class="transition-all {settings.sidebarCollapsed ? 'w-[48px] min-w-[48px]' : 'w-[210px] min-w-[210px]'}"
|
||||
style="transition-duration: var(--duration-ui)"
|
||||
data-tauri-drag-region
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<!-- Centre: drag area -->
|
||||
<div class="flex-1" data-tauri-drag-region></div>
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- Window controls -->
|
||||
<div class="flex items-center h-full">
|
||||
|
||||
Reference in New Issue
Block a user