From 6c74bfa7ec85f6c457cce903d6d69f70952bc9df Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 1 May 2026 22:25:37 +0800 Subject: [PATCH 1/5] Make hotword prompt wording more directive The hotword injection copy now tells the model to prefer the user's spelling proactively while still avoiding unrelated mechanical replacements. That keeps the intent of issue #63 aligned with the existing prompt structure and only changes the wording that users see indirectly through polish behavior. Constraint: Keep the change minimal and avoid changing the surrounding prompt architecture. Rejected: Broaden the prompt or add new prompt sections | unnecessary for this issue. Confidence: high Scope-risk: narrow Directive: Keep future hotword guidance user-facing and proactive, but still avoid blanket replacement language. Tested: cargo test --manifest-path src-tauri/Cargo.toml polish::tests -- --nocapture; cargo check --manifest-path src-tauri/Cargo.toml Not-tested: Live LLM output quality against the updated prompt wording. --- openless-all/app/src-tauri/src/polish.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/polish.rs b/openless-all/app/src-tauri/src/polish.rs index 0004e1f7..d207bf33 100644 --- a/openless-all/app/src-tauri/src/polish.rs +++ b/openless-all/app/src-tauri/src/polish.rs @@ -411,7 +411,7 @@ fn compose_system_prompt(mode: PolishMode, hotwords: &[String]) -> String { .collect::>() .join("\n"); format!( - "{}\n\n热词(用户提供的正确写法,仅当原始转写明显是其误识别时才纠正,不做机械替换):\n{}", + "{}\n\n热词(用户希望以下写法在输出中保持准确;当转写中出现这些词的同音 / 近形误识别时,优先按上述写法输出,不做无关词的机械替换):\n{}", base, bullets ) } @@ -888,4 +888,14 @@ mod tests { // 防回归:旧版"另外:"标签写法不能再出现在示例输出里。 assert!(!prompt.contains("另外:检查一下当前还有哪些 issues")); } + + #[test] + fn compose_system_prompt_prefers_correct_spelling_for_hotwords() { + let prompt = compose_system_prompt(PolishMode::Light, &["GitHub".into(), "OpenLess".into()]); + + assert!(prompt.contains("用户希望以下写法在输出中保持准确")); + assert!(prompt.contains("同音 / 近形误识别时,优先按上述写法输出")); + assert!(prompt.contains("- GitHub")); + assert!(prompt.contains("- OpenLess")); + } } From f62762a17f37f123fce15853a4ae9bffe945f0c9 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 1 May 2026 22:38:15 +0800 Subject: [PATCH 2/5] Keep capsule hitbox aligned with visible UI The capsule window used a fixed oversized frame even when only the pill was visible, so transparent padding could block clicks on underlying apps. The runtime now resizes and repositions the capsule to the actual visible content, expanding only when the translation badge is shown. Constraint: Keep the fix minimal and preserve the translation badge when it is actually visible. Rejected: Add click-through behavior to transparent pixels | too risky and platform-specific for this narrow fix. Confidence: high Scope-risk: moderate Directive: Keep capsule window geometry in sync with rendered content; do not reintroduce a larger transparent hitbox without a matching click-through strategy. Tested: cargo test --manifest-path src-tauri/Cargo.toml --lib -- --nocapture; npm ci; npm run build; git diff --check Not-tested: Live mouse hit-testing on Windows/macOS/Linux after the resize change. --- openless-all/app/src-tauri/src/coordinator.rs | 1 + openless-all/app/src-tauri/src/lib.rs | 51 +++++++++++++++++-- openless-all/app/src/components/Capsule.tsx | 2 +- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index fdc95d07..999e1831 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -2451,6 +2451,7 @@ fn emit_capsule( let show_capsule = inner.prefs.get().show_capsule; if let Some(window) = app.get_webview_window("capsule") { let visible = !matches!(state, CapsuleState::Idle); + let _ = crate::position_capsule_bottom_center(&window, payload.translation); if show_capsule && visible { if cfg!(target_os = "windows") { if !show_capsule_window_no_activate() { diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 5e81af15..a20b1546 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -35,7 +35,7 @@ use std::time::Duration; static QA_WINDOW_POSITIONED: AtomicBool = AtomicBool::new(false); use tauri::menu::{MenuBuilder, MenuItemBuilder}; use tauri::tray::{MouseButton, TrayIconBuilder, TrayIconEvent}; -use tauri::{AppHandle, Emitter, LogicalPosition, Manager, RunEvent, Runtime}; +use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, RunEvent, Runtime}; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -61,7 +61,7 @@ pub fn run() { // Capsule 启动时定位到屏幕底部居中并隐藏;coordinator 按需显示。 // 与 Swift `CapsuleWindowController.repositionToBottomCenter` 同语义。 if let Some(capsule) = app.get_webview_window("capsule") { - if let Err(e) = position_capsule_bottom_center(&capsule) { + if let Err(e) = position_capsule_bottom_center(&capsule, false) { log::warn!("[capsule] position failed: {e}"); } let _ = capsule.hide(); @@ -563,21 +563,62 @@ pub(crate) fn hide_qa_window(app: &AppHandle) { /// 把 capsule 窗口移到屏幕底部居中,与 Swift `CapsuleWindowController.repositionToBottomCenter` 同效。 /// 留 80pt 给 macOS Dock;Windows 任务栏一般在底部 48pt 以内,整体也合适。 -fn position_capsule_bottom_center( +pub(crate) fn position_capsule_bottom_center( window: &tauri::WebviewWindow, + translation_active: bool, ) -> tauri::Result<()> { let monitor = match window.current_monitor()? { Some(m) => m, None => return Ok(()), }; + let (cap_w, cap_h) = capsule_window_size(translation_active); + window.set_size(LogicalSize::new(cap_w, cap_h))?; + let scale = monitor.scale_factor(); let size = monitor.size(); let logical_w = size.width as f64 / scale; let logical_h = size.height as f64 / scale; - let cap_w = 220.0_f64; - let cap_h = 96.0_f64; let x = ((logical_w - cap_w) / 2.0).max(0.0); let y = (logical_h - cap_h - 80.0).max(0.0); window.set_position(LogicalPosition::new(x, y))?; Ok(()) } + +fn capsule_window_size(translation_active: bool) -> (f64, f64) { + #[cfg(target_os = "windows")] + { + let height = if translation_active { 110.0 } else { 52.0 }; + (196.0, height) + } + + #[cfg(not(target_os = "windows"))] + { + let height = if translation_active { 110.0 } else { 42.0 }; + (176.0, height) + } +} + +#[cfg(test)] +mod tests { + use super::capsule_window_size; + + #[test] + fn capsule_window_size_matches_visible_pill_when_not_translating() { + let (width, height) = capsule_window_size(false); + #[cfg(target_os = "windows")] + assert_eq!((width, height), (196.0, 52.0)); + + #[cfg(not(target_os = "windows"))] + assert_eq!((width, height), (176.0, 42.0)); + } + + #[test] + fn capsule_window_size_expands_for_translation_badge() { + let (width, height) = capsule_window_size(true); + #[cfg(target_os = "windows")] + assert_eq!((width, height), (196.0, 110.0)); + + #[cfg(not(target_os = "windows"))] + assert_eq!((width, height), (176.0, 110.0)); + } +} diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index 8e518fef..e7b2044b 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -323,7 +323,7 @@ export function Capsule() { position: 'absolute', left: '50%', // bottom = 50%(pill 中线)+ pill 半高 21px(capsuleLayout mac=42)+ 8px 间隔。 - // 胶囊窗口高度 110(tauri.conf.json)刚好装下 badge + 间隔 + pill。 + // 只有翻译徽章可见时才需要额外高度;普通录音/转写状态由后端缩到 pill 本体,避免透明死区。 bottom: 'calc(50% + 21px + 8px)', transform: 'translateX(-50%)', pointerEvents: 'none', From 320c97485cdce4d47421500ca8acd6e8c9361d5a Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 1 May 2026 22:58:23 +0800 Subject: [PATCH 3/5] Reuse capsule height for QA anchoring QA positioning had kept its own fixed capsule height constant after the capsule window was resized to match the visible pill. Reusing the shared capsule height source keeps the documented gap intact without widening the change surface. Constraint: Keep the change minimal and avoid a separate geometry model for QA. Rejected: Keep the old QA constant | would drift from the new capsule geometry again. Confidence: high Scope-risk: narrow Directive: If capsule geometry changes again, update QA anchoring from the shared capsule source instead of introducing another fixed constant. Tested: cargo test --manifest-path src-tauri/Cargo.toml --lib -- --nocapture Not-tested: Live QA window placement on each platform. --- openless-all/app/src-tauri/src/lib.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index a20b1546..12056a11 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -436,8 +436,6 @@ const QA_WINDOW_WIDTH: f64 = 380.0; const QA_WINDOW_HEIGHT: f64 = 440.0; /// 胶囊与 QA 窗口的间距,与设计稿一致。 const QA_WINDOW_GAP_TO_CAPSULE: f64 = 8.0; -/// 胶囊高度(与 `position_capsule_bottom_center` 中一致)。 -const CAPSULE_HEIGHT_FOR_QA: f64 = 96.0; /// 给 macOS Dock 留的下边距(与 capsule 同源)。 const DOCK_BOTTOM_PADDING_FOR_QA: f64 = 80.0; @@ -452,10 +450,11 @@ fn position_qa_window(window: &tauri::WebviewWindow) -> ta let size = monitor.size(); let logical_w = size.width as f64 / scale; let logical_h = size.height as f64 / scale; + let capsule_height = capsule_window_size(false).1; let x = ((logical_w - QA_WINDOW_WIDTH) / 2.0).max(0.0); let y = (logical_h - DOCK_BOTTOM_PADDING_FOR_QA - - CAPSULE_HEIGHT_FOR_QA + - capsule_height - QA_WINDOW_GAP_TO_CAPSULE - QA_WINDOW_HEIGHT) .max(0.0); @@ -598,9 +597,13 @@ fn capsule_window_size(translation_active: bool) -> (f64, f64) { } } +fn capsule_height_for_qa() -> f64 { + capsule_window_size(false).1 +} + #[cfg(test)] mod tests { - use super::capsule_window_size; + use super::{capsule_height_for_qa, capsule_window_size}; #[test] fn capsule_window_size_matches_visible_pill_when_not_translating() { @@ -621,4 +624,13 @@ mod tests { #[cfg(not(target_os = "windows"))] assert_eq!((width, height), (176.0, 110.0)); } + + #[test] + fn qa_anchor_uses_normal_capsule_height_source() { + #[cfg(target_os = "windows")] + assert_eq!(capsule_height_for_qa(), 52.0); + + #[cfg(not(target_os = "windows"))] + assert_eq!(capsule_height_for_qa(), 42.0); + } } From 1dfcf7f3026c5d362dac8a1aeb4919b098c47e84 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 1 May 2026 23:33:08 +0800 Subject: [PATCH 4/5] Avoid relayouting capsule on every audio tick The capsule window now tracks its last applied geometry and only resizes/repositions when the monitor or translation state actually changes. That keeps steady-state level updates from churning the native window manager while preserving the current placement behavior when geometry really does change. Constraint: Keep the fix minimal and avoid introducing a broader window-management abstraction. Rejected: Leave the unconditional relayout in place | it adds avoidable compositor churn during recording. Confidence: high Scope-risk: narrow Directive: Any future capsule geometry change should update the cached geometry key, not reintroduce unconditional relayout. Tested: cargo test --manifest-path src-tauri/Cargo.toml --lib -- --nocapture Not-tested: Live compositor/jitter behavior on Windows/macOS/Linux. --- openless-all/app/src-tauri/src/coordinator.rs | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 999e1831..12a32ee5 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -111,6 +111,9 @@ struct Inner { qa_hotkey: Mutex>, /// QA 单独的 session 状态,与 dictation 的 SessionPhase 不冲突。 qa_state: Mutex, + /// 最近一次应用到 capsule 窗口的几何状态。避免录音 level tick 反复触发 + /// resize / reposition。 + capsule_layout: Mutex>, /// QA 用的 ASR 句柄(始终是 Volcengine 流式)。 qa_asr: Mutex>>, /// QA 用的 Recorder 句柄。 @@ -181,6 +184,7 @@ impl Coordinator { translation_modifier_seen: AtomicBool::new(false), qa_hotkey: Mutex::new(None), qa_state: Mutex::new(QaSessionState::default()), + capsule_layout: Mutex::new(None), qa_asr: Mutex::new(None), qa_recorder: Mutex::new(None), }), @@ -2451,7 +2455,7 @@ fn emit_capsule( let show_capsule = inner.prefs.get().show_capsule; if let Some(window) = app.get_webview_window("capsule") { let visible = !matches!(state, CapsuleState::Idle); - let _ = crate::position_capsule_bottom_center(&window, payload.translation); + maybe_position_capsule_bottom_center(inner, &window, payload.translation); if show_capsule && visible { if cfg!(target_os = "windows") { if !show_capsule_window_no_activate() { @@ -2472,6 +2476,41 @@ fn emit_capsule( let _ = app.emit_to("capsule", "capsule:state", payload); } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct CapsuleLayoutState { + translation_active: bool, + monitor_x: i32, + monitor_y: i32, + monitor_width: u32, + monitor_height: u32, + scale_bits: u64, +} + +fn maybe_position_capsule_bottom_center( + inner: &Arc, + window: &tauri::WebviewWindow, + translation_active: bool, +) { + let Some(monitor) = window.current_monitor().ok().flatten() else { + return; + }; + let next = CapsuleLayoutState { + translation_active, + monitor_x: monitor.position().x, + monitor_y: monitor.position().y, + monitor_width: monitor.size().width, + monitor_height: monitor.size().height, + scale_bits: monitor.scale_factor().to_bits(), + }; + let mut last = inner.capsule_layout.lock(); + if last.as_ref() == Some(&next) { + return; + } + *last = Some(next); + drop(last); + let _ = crate::position_capsule_bottom_center(window, translation_active); +} + // ─────────────────────────── audio bridge ─────────────────────────── struct DeferredAsrBridge { From e8ba67a45f35e94c180bb637a18d84a642580d74 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 1 May 2026 23:38:24 +0800 Subject: [PATCH 5/5] Only cache capsule geometry after a successful relayout The capsule geometry cache must reflect only applied layout, otherwise a transient resize or position failure would permanently suppress future retries for the same monitor and translation state. The cache is now written only after the relayout call succeeds, so a failed attempt can be retried on later emits. Constraint: Keep the fix local to the capsule relayout path. Rejected: Cache before calling relayout | can strand the capsule on stale geometry after a transient failure. Confidence: high Scope-risk: narrow Directive: Never treat capsule geometry as applied until the window manager has accepted the resize/reposition. Tested: cargo test --manifest-path src-tauri/Cargo.toml --lib -- --nocapture Not-tested: Live retry behavior under actual window-manager failures. --- openless-all/app/src-tauri/src/coordinator.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 12a32ee5..cca83683 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -2502,13 +2502,17 @@ fn maybe_position_capsule_bottom_center( monitor_height: monitor.size().height, scale_bits: monitor.scale_factor().to_bits(), }; - let mut last = inner.capsule_layout.lock(); - if last.as_ref() == Some(&next) { + { + let last = inner.capsule_layout.lock(); + if last.as_ref() == Some(&next) { + return; + } + } + if crate::position_capsule_bottom_center(window, translation_active).is_ok() { + let mut last = inner.capsule_layout.lock(); + *last = Some(next); return; } - *last = Some(next); - drop(last); - let _ = crate::position_capsule_bottom_center(window, translation_active); } // ─────────────────────────── audio bridge ───────────────────────────