Bundles a session of dogfood UX feedback plus the two Cursor Bugbot
findings on the auto-titles branch.
Onboarding (FirstRunPage):
- Welcome leads with "Set up automatically"; system breakdown and the
full model list move behind a "Choose manually" disclosure
- Morning / evening / autostart modal copy trimmed to one short
sentence each; CTAs shortened
- "Corbie" autostart string reverted to "Kon"
- Already-downloaded models are clickable in the picker so the
Settings → About → Replay onboarding flow doesn't re-download
- Autostart "No thanks" now does isEnabled() → disable() to actually
remove the OS login item when replaying after a previous "Yes"
Tasks page:
- Bucket nav (All / Inbox / Today / Soon / Later) now a horizontal
pill row; was stacking because nav was block-level
- List sidebar sized to content via self-start max-h-full instead of
stretching to viewport when sparse
- Energy chip surfaces at opacity-60 when unset (was opacity-0,
hidden until hover) so the affordance is discoverable
- "Brain-Dead" energy label → "Zero" everywhere user-facing; enum
stays brain_dead to avoid a destructive DB migration
LLM status chip (llmStatus.svelte.ts + Dictation/Settings):
- Chip no longer auto-warms when the engine isn't loaded; it's hidden
unless ready / generating / loading / error
- refreshLlmStatus takes { force: true } so post-load reconcile clears
stale "warming"; ambient refreshes still preserve in-flight state
- markError exported; failed loads surface "AI error" with detail
rather than silently going to off
- check_llm_model is the source of truth (replaces the bool-only
get_llm_status path in the store)
Float popout window:
- Native decorations off — was stacking two titlebars + two close X's
on KWin, one of which silently failed
- ResizeHandles mounted outside the animate-float-enter wrapper so
fixed-position handles anchor to the viewport, not the transformed
root; secondary-windows capability gains
core:window:allow-start-resize-dragging for tasks-float
- GTK Utility WindowTypeHint applied pre-map (mirroring the preview
window) so KWin Wayland honours always-on-top reliably
- visible_on_all_workspaces(true) so the pinned tasks list follows
workspace switches
- togglePin does hide()+show()+focus() on re-pin to nudge the
compositor into re-evaluating window state
- Pop-out / Edit / Open viewer buttons hidden on Android via
isAndroid() — the multi-window Tauri commands stub out there
Build / Bugbot:
- src-tauri Cargo.toml: whisper feature now chains whisper-vulkan, so
the dev runner's --no-default-features --features whisper
invocation actually pulls Vulkan acceleration instead of silently
falling back to CPU-only
- jsconfig.json's inherited "types": ["node"] fixed by adding
@types/node; corresponding @ts-expect-error in vite.config.js
removed now that process is a known global
Verification: svelte-check + cargo check pass clean. Manual
device-side validation still pending for float resize and replay
autostart "No thanks" — those are the only remaining confidence items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
232 lines
9.2 KiB
Rust
232 lines
9.2 KiB
Rust
// Multi-window support is desktop-only. Android Tauri apps run as a
|
|
// single Activity, so `WebviewWindowBuilder` is not available there.
|
|
// All four commands below have an Android stub that returns a clear
|
|
// error so frontend invokes don't panic — the frontend itself is
|
|
// expected to detect Android via `isAndroid()` and route the previously-
|
|
// secondary content (preview overlay, transcript viewer, task float)
|
|
// into routes inside the main window instead.
|
|
#[cfg(not(target_os = "android"))]
|
|
use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
use crate::PreferencesScript;
|
|
|
|
#[cfg(target_os = "android")]
|
|
const ANDROID_MULTIWINDOW_ERR: &str =
|
|
"Multi-window is not supported on Android; this command is desktop-only";
|
|
|
|
#[cfg(target_os = "android")]
|
|
#[tauri::command]
|
|
pub async fn open_task_window(_app: tauri::AppHandle) -> Result<(), String> {
|
|
Err(ANDROID_MULTIWINDOW_ERR.into())
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
#[tauri::command]
|
|
pub async fn open_preview_window(_app: tauri::AppHandle) -> Result<(), String> {
|
|
Err(ANDROID_MULTIWINDOW_ERR.into())
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
#[tauri::command]
|
|
pub async fn close_preview_window(_app: tauri::AppHandle) -> Result<(), String> {
|
|
Err(ANDROID_MULTIWINDOW_ERR.into())
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
#[tauri::command]
|
|
pub async fn open_viewer_window(_app: tauri::AppHandle) -> Result<(), String> {
|
|
Err(ANDROID_MULTIWINDOW_ERR.into())
|
|
}
|
|
|
|
/// Open a floating always-on-top task window.
|
|
#[cfg(not(target_os = "android"))]
|
|
#[tauri::command]
|
|
pub async fn open_task_window(app: tauri::AppHandle) -> Result<(), String> {
|
|
if let Some(window) = app.get_webview_window("tasks-float") {
|
|
window.show().map_err(|e| e.to_string())?;
|
|
window.set_focus().map_err(|e| e.to_string())?;
|
|
app.emit("task-window-focus", ())
|
|
.map_err(|e| e.to_string())?;
|
|
return Ok(());
|
|
}
|
|
|
|
// On Linux we use native KWin/Mutter decorations so resize and drag
|
|
// are handled by the compositor. Tauri's frameless path on Wayland
|
|
// doesn't honour diagonal resize reliably and Tauri's own drag
|
|
// region adds latency on webkit2gtk. macOS and Windows keep the
|
|
// custom frameless chrome drawn by the Titlebar component.
|
|
let use_native_decorations = cfg!(target_os = "linux");
|
|
|
|
// Built hidden so the GTK utility type hint can be applied pre-map on
|
|
// Linux — see the preview window for the same pattern. KWin/Mutter on
|
|
// Wayland reliably keep utility-class windows above normal windows
|
|
// (and respect runtime keep-above toggles for them); for normal
|
|
// windows the same hint requests are flaky post-map.
|
|
//
|
|
// Decorations are off for this window: the float route renders its own
|
|
// titlebar with the pin / close controls, and stacking native KDE
|
|
// decorations on top of that produced two titlebars and two close X's
|
|
// (the native one didn't even close the window because the in-page
|
|
// chrome captured the click first). Custom drag is wired via
|
|
// handleDragStart (startDragging) and ResizeHandles in the route.
|
|
let _ = use_native_decorations; // keep the OS detection for future use
|
|
let mut builder =
|
|
WebviewWindowBuilder::new(&app, "tasks-float", WebviewUrl::App("/float".into()))
|
|
.title("Kon Tasks")
|
|
.inner_size(480.0, 520.0)
|
|
.min_inner_size(360.0, 480.0)
|
|
.always_on_top(true)
|
|
// Pin across virtual desktops so users who flip workspaces
|
|
// mid-task don't lose the floating tasks list. Combined with
|
|
// always_on_top + utility hint, this matches what users
|
|
// expect from a "pin" button on KDE Plasma and GNOME.
|
|
.visible_on_all_workspaces(true)
|
|
.decorations(false)
|
|
.resizable(true)
|
|
.visible(false);
|
|
|
|
// Inject preferences before Svelte mounts
|
|
if let Some(script) = app.try_state::<PreferencesScript>() {
|
|
if !script.0.is_empty() {
|
|
builder = builder.initialization_script(&script.0);
|
|
}
|
|
}
|
|
|
|
let window = builder.build().map_err(|e| e.to_string())?;
|
|
|
|
// Apply the GTK Utility type hint before the window maps. On X11 and
|
|
// XWayland the hint is honoured immediately; on native Wayland-only
|
|
// compositors GTK uses the closest semantic equivalent. Must happen
|
|
// before show() per GTK3 docs.
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use gdk::WindowTypeHint;
|
|
use gtk::prelude::GtkWindowExt;
|
|
if let Ok(gtk_window) = window.gtk_window() {
|
|
gtk_window.set_type_hint(WindowTypeHint::Utility);
|
|
}
|
|
}
|
|
|
|
window.show().map_err(|e| e.to_string())?;
|
|
window.set_focus().map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Open the always-on-top transcription preview window. Idempotent — an
|
|
/// existing window is shown and focused; otherwise a new one is built.
|
|
/// The preview is passive: it subscribes to the `transcription-result`
|
|
/// event and the cross-window `preview-*` events that DictationPage fires
|
|
/// as it moves through the phases of a dictation run.
|
|
#[cfg(not(target_os = "android"))]
|
|
#[tauri::command]
|
|
pub async fn open_preview_window(app: tauri::AppHandle) -> Result<(), String> {
|
|
if let Some(window) = app.get_webview_window("transcription-preview") {
|
|
window.show().map_err(|e| e.to_string())?;
|
|
// Intentionally NOT set_focus — the preview window is meant to
|
|
// appear next to whatever the user is typing into; stealing focus
|
|
// defeats the whole point.
|
|
return Ok(());
|
|
}
|
|
|
|
let use_native_decorations = cfg!(target_os = "linux");
|
|
|
|
// Preview is a transient helper overlay, not a primary surface. It
|
|
// should (a) follow the user across virtual desktops — otherwise the
|
|
// overlay vanishes the moment they switch workspace mid-dictation —
|
|
// and (b) stay out of the Alt+Tab / taskbar lists. skip_taskbar covers
|
|
// both on KWin (KWin's default Alt+Tab list reads _NET_WM_STATE_SKIP_TASKBAR);
|
|
// visible_on_all_workspaces sets _NET_WM_STATE_STICKY via GTK on X11
|
|
// / XWayland so the overlay is pinned. Matches the ergonomic OpenWhispr
|
|
// PR #183 shipped for KDE Plasma Wayland.
|
|
//
|
|
// Built hidden so we can set the Linux WindowTypeHint before the
|
|
// window maps — GTK3 only honours the hint pre-realize.
|
|
let mut builder = WebviewWindowBuilder::new(
|
|
&app,
|
|
"transcription-preview",
|
|
WebviewUrl::App("/preview".into()),
|
|
)
|
|
.title("Kon — Preview")
|
|
.inner_size(420.0, 200.0)
|
|
.min_inner_size(360.0, 140.0)
|
|
.max_inner_size(520.0, 360.0)
|
|
.always_on_top(true)
|
|
.skip_taskbar(true)
|
|
.visible_on_all_workspaces(true)
|
|
.focused(false)
|
|
.visible(false)
|
|
.decorations(use_native_decorations)
|
|
.resizable(true);
|
|
|
|
if let Some(script) = app.try_state::<PreferencesScript>() {
|
|
if !script.0.is_empty() {
|
|
builder = builder.initialization_script(&script.0);
|
|
}
|
|
}
|
|
|
|
let window = builder.build().map_err(|e| e.to_string())?;
|
|
|
|
// Defence-in-depth for non-KDE compositors (Hyprland, Sway, GNOME
|
|
// Mutter) where SKIP_TASKBAR alone may not hide a window from the
|
|
// alt-tab switcher. Classifying as Utility signals to the compositor
|
|
// that this is an assistive auxiliary window — switchers and tilers
|
|
// treat it accordingly. Noop on KWin (already handled by skip_taskbar)
|
|
// but harmless. Must happen before show() per GTK3 docs.
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use gdk::WindowTypeHint;
|
|
use gtk::prelude::GtkWindowExt;
|
|
if let Ok(gtk_window) = window.gtk_window() {
|
|
gtk_window.set_type_hint(WindowTypeHint::Utility);
|
|
}
|
|
}
|
|
|
|
window.show().map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Hide the transcription preview window without destroying it so the next
|
|
/// open is instant. Returns Ok even when no preview window exists.
|
|
#[cfg(not(target_os = "android"))]
|
|
#[tauri::command]
|
|
pub async fn close_preview_window(app: tauri::AppHandle) -> Result<(), String> {
|
|
if let Some(window) = app.get_webview_window("transcription-preview") {
|
|
window.hide().map_err(|e| e.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Open the transcript viewer window.
|
|
#[cfg(not(target_os = "android"))]
|
|
#[tauri::command]
|
|
pub async fn open_viewer_window(app: tauri::AppHandle) -> Result<(), String> {
|
|
if let Some(window) = app.get_webview_window("transcript-viewer") {
|
|
window.show().map_err(|e| e.to_string())?;
|
|
window.set_focus().map_err(|e| e.to_string())?;
|
|
return Ok(());
|
|
}
|
|
|
|
// See note in open_task_window for the Linux-vs-other platform split.
|
|
let use_native_decorations = cfg!(target_os = "linux");
|
|
|
|
let mut builder =
|
|
WebviewWindowBuilder::new(&app, "transcript-viewer", WebviewUrl::App("/viewer".into()))
|
|
.title("Kon - Transcription Editor")
|
|
.inner_size(600.0, 700.0)
|
|
.min_inner_size(560.0, 520.0)
|
|
.decorations(use_native_decorations)
|
|
.resizable(true);
|
|
|
|
// Inject preferences before Svelte mounts
|
|
if let Some(script) = app.try_state::<PreferencesScript>() {
|
|
if !script.0.is_empty() {
|
|
builder = builder.initialization_script(&script.0);
|
|
}
|
|
}
|
|
|
|
builder.build().map_err(|e| e.to_string())?;
|
|
|
|
Ok(())
|
|
}
|