From 3e95ef48136276afd1280e7b9b26ec3c9b5ab823 Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Wed, 26 Aug 2026 00:28:43 +0800 Subject: [PATCH 1/9] fix(selection-voice): auto-hide capsule when begin fails without selection Schedule the standard 2s capsule idle timer after no-selection begin errors so the Siri-style prompt does not stay on screen indefinitely. Co-authored-by: Cursor --- .../src/coordinator/selection_voice_session.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openless-all/app/src-tauri/src/coordinator/selection_voice_session.rs b/openless-all/app/src-tauri/src/coordinator/selection_voice_session.rs index 4db36c85..357e8575 100644 --- a/openless-all/app/src-tauri/src/coordinator/selection_voice_session.rs +++ b/openless-all/app/src-tauri/src/coordinator/selection_voice_session.rs @@ -14,6 +14,7 @@ use super::{ answer_qa_question_text, capture_external_focus_target, close_qa_panel, emit_capsule, open_qa_panel, polish_text, qa_event_target, qa_session, restore_focus_target_if_possible, schedule_capsule_idle, translate_text, CapsuleFeedback, Coordinator, Inner, QaPhase, + CAPSULE_AUTO_HIDE_DELAY_MS, }; use crate::coordinator_state::{initial_session_id, new_session_id, SessionId}; use crate::edit_plan::{apply_edit_plan, parse_edit_plan, EditOperation, EditPlan}; @@ -64,6 +65,18 @@ fn emit_selection_voice_begin_error(inner: &Arc, error: &str) { Some(selection_voice_user_message(error)), None, ); + schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS); + // #region agent log + crate::agent_debug::agent_debug_log( + "H8", + "selection_voice_session.rs:begin_error", + "scheduled capsule auto-hide after begin failure", + serde_json::json!({ + "error": error, + "delayMs": CAPSULE_AUTO_HIDE_DELAY_MS, + }), + ); + // #endregion } fn emit_selection_voice_end_error(inner: &Arc, error: &str) { From affb5ca331d431effb08d8e9775774e29dec9fcb Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Wed, 26 Aug 2026 00:53:06 +0800 Subject: [PATCH 2/9] fix(selection): log why selection capture missed for voice/polish Distinguish clipboard sentinel unchanged, empty copy, shortcut failure, and prefetch vs live capture so user-exported logs can diagnose false no-selection reports. Co-authored-by: Cursor --- .../src/coordinator/selection_polish.rs | 7 +- .../coordinator/selection_voice_session.rs | 35 ++- openless-all/app/src-tauri/src/selection.rs | 295 +++++++++++++++--- 3 files changed, 279 insertions(+), 58 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator/selection_polish.rs b/openless-all/app/src-tauri/src/coordinator/selection_polish.rs index df8bf1ec..80bc9d46 100644 --- a/openless-all/app/src-tauri/src/coordinator/selection_polish.rs +++ b/openless-all/app/src-tauri/src/coordinator/selection_polish.rs @@ -145,9 +145,14 @@ pub(super) async fn run_selection_polish(inner: &Arc) -> Result<(), Strin // final check below deliberately does *not* restore this target: a user who // changed windows made an intentional context switch, so the safe behavior // is to leave both apps untouched. - let (selection_opt, insertion_target) = crate::selection::resolve_selection_workspace_capture(); + let (selection_opt, insertion_target, capture_diag) = + crate::selection::resolve_selection_workspace_capture_with_diag(); if selection_polish_plan(selection_opt.as_ref()) == SelectionPolishPlan::NoSelection { let code = "selectionPolishNoSelection"; + log::warn!( + "[selection-polish] no selection ({})", + capture_diag.summary() + ); finish_selection_polish_capsule( inner, CapsuleState::Cancelled, diff --git a/openless-all/app/src-tauri/src/coordinator/selection_voice_session.rs b/openless-all/app/src-tauri/src/coordinator/selection_voice_session.rs index 357e8575..43c15bbb 100644 --- a/openless-all/app/src-tauri/src/coordinator/selection_voice_session.rs +++ b/openless-all/app/src-tauri/src/coordinator/selection_voice_session.rs @@ -65,18 +65,11 @@ fn emit_selection_voice_begin_error(inner: &Arc, error: &str) { Some(selection_voice_user_message(error)), None, ); + // 无选区等 begin 失败时胶囊会停在 Error;与听写 Done/Error 同口径,2s 后自动收回。 schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS); - // #region agent log - crate::agent_debug::agent_debug_log( - "H8", - "selection_voice_session.rs:begin_error", - "scheduled capsule auto-hide after begin failure", - serde_json::json!({ - "error": error, - "delayMs": CAPSULE_AUTO_HIDE_DELAY_MS, - }), + log::info!( + "[selection-voice] begin error capsule shown error={error} auto_hide_ms={CAPSULE_AUTO_HIDE_DELAY_MS}" ); - // #endregion } fn emit_selection_voice_end_error(inner: &Arc, error: &str) { @@ -401,9 +394,27 @@ async fn begin_selection_voice_session(inner: &Arc) -> Result<(), String> return Err("selectionVoiceBusy".into()); } - let (selection_opt, insertion_target) = crate::selection::resolve_selection_workspace_capture(); - let selection = selection_opt.ok_or_else(|| "selectionVoiceNoSelection".to_string())?; + let (selection_opt, insertion_target, capture_diag) = + crate::selection::resolve_selection_workspace_capture_with_diag(); + log::info!( + "[selection-voice] begin capture diag={}", + capture_diag.summary() + ); + let selection = match selection_opt { + Some(selection) => selection, + None => { + log::warn!( + "[selection-voice] begin failed: selectionVoiceNoSelection ({})", + capture_diag.summary() + ); + return Err("selectionVoiceNoSelection".into()); + } + }; if !crate::selection::selection_insertion_target_is_captured(&insertion_target) { + log::warn!( + "[selection-voice] begin failed: selectionVoiceTargetUnavailable ({})", + capture_diag.summary() + ); return Err("selectionVoiceTargetUnavailable".into()); } diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index 548e256e..f185dde7 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -106,6 +106,63 @@ struct PrefetchedSelectionWorkspace { insertion_target: SelectionInsertionTarget, } +/// 选区抓取失败 / 命中原因,写入用户可导出的 openless.log,便于区分 +/// 「真的没选区」vs「模拟复制未覆盖剪贴板」vs「剪贴板 API 失败」。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SelectionCaptureMissReason { + Ok, + PrefetchHit, + PrefetchMissLiveOk, + ClipboardInitFailed, + CopyShortcutFailed, + ClipboardUnchangedSentinel, + ClipboardEmptyAfterCopy, + ClipboardReadFailed, + EmptyTrimmed, + NoCapturePath, +} + +impl SelectionCaptureMissReason { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Ok => "ok", + Self::PrefetchHit => "prefetch_hit", + Self::PrefetchMissLiveOk => "prefetch_miss_live_ok", + Self::ClipboardInitFailed => "clipboard_init_failed", + Self::CopyShortcutFailed => "copy_shortcut_failed", + Self::ClipboardUnchangedSentinel => "clipboard_unchanged_sentinel", + Self::ClipboardEmptyAfterCopy => "clipboard_empty_after_copy", + Self::ClipboardReadFailed => "clipboard_read_failed", + Self::EmptyTrimmed => "empty_trimmed", + Self::NoCapturePath => "no_capture_path", + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct SelectionCaptureDiag { + pub reason: SelectionCaptureMissReason, + pub front_app: Option, + pub used_prefetch: bool, + pub target_captured: bool, + pub char_count: Option, +} + +impl SelectionCaptureDiag { + pub(crate) fn summary(&self) -> String { + format!( + "reason={} used_prefetch={} target_captured={} chars={} front_app={}", + self.reason.as_str(), + self.used_prefetch, + self.target_captured, + self.char_count + .map(|n| n.to_string()) + .unwrap_or_else(|| "-".into()), + self.front_app.as_deref().unwrap_or("-") + ) + } +} + #[cfg(any(target_os = "macos", target_os = "windows"))] static PREFETCHED_SELECTION_WORKSPACE: std::sync::Mutex> = std::sync::Mutex::new(None); @@ -114,7 +171,7 @@ static PREFETCHED_SELECTION_WORKSPACE: std::sync::Mutex { let chars = selection.text.chars().count(); log::info!( - "[selection] prefetched workspace selection ({} chars)", - chars + "[selection] prefetched workspace selection ({} chars) reason={} front_app={}", + chars, + reason.as_str(), + selection.source_app.as_deref().unwrap_or("-") ); *guard = Some(PrefetchedSelectionWorkspace { selection, @@ -131,7 +190,12 @@ pub(crate) fn prefetch_selection_workspace_capture() { }); } None => { - log::info!("[selection] prefetch missed (no selection at hotkey edge)"); + log::info!( + "[selection] prefetch missed (no selection at hotkey edge) reason={} front_app={} target_captured={}", + reason.as_str(), + capture_selection_source_app_hint(), + selection_insertion_target_is_captured(&insertion_target) + ); guard.take(); } } @@ -160,20 +224,94 @@ pub(crate) fn take_prefetched_selection_workspace( #[cfg(any(target_os = "macos", target_os = "windows"))] pub(crate) fn resolve_selection_workspace_capture( ) -> (Option, SelectionInsertionTarget) { + let (selection, target, _) = resolve_selection_workspace_capture_with_diag(); + (selection, target) +} + +#[cfg(any(target_os = "macos", target_os = "windows"))] +pub(crate) fn resolve_selection_workspace_capture_with_diag() -> ( + Option, + SelectionInsertionTarget, + SelectionCaptureDiag, +) { if let Some((selection, insertion_target)) = take_prefetched_selection_workspace() { - return (Some(selection), insertion_target); + let char_count = selection.text.chars().count(); + let front_app = selection.source_app.clone(); + let target_captured = selection_insertion_target_is_captured(&insertion_target); + let diag = SelectionCaptureDiag { + reason: SelectionCaptureMissReason::PrefetchHit, + front_app, + used_prefetch: true, + target_captured, + char_count: Some(char_count), + }; + log::info!("[selection] resolve used prefetch {}", diag.summary()); + return (Some(selection), insertion_target, diag); } let insertion_target = capture_selection_insertion_target(); - let capture = capture_selection_with_status(); - (capture.selection, insertion_target) + let (capture, reason) = capture_selection_with_status_diag(); + let target_captured = selection_insertion_target_is_captured(&insertion_target); + let (front_app, char_count) = match &capture.selection { + Some(selection) => ( + selection.source_app.clone(), + Some(selection.text.chars().count()), + ), + None => (capture_selection_source_app_hint(), None), + }; + let reason = if capture.selection.is_some() + && reason == SelectionCaptureMissReason::Ok + { + SelectionCaptureMissReason::PrefetchMissLiveOk + } else { + reason + }; + let diag = SelectionCaptureDiag { + reason, + front_app, + used_prefetch: false, + target_captured, + char_count, + }; + log::info!("[selection] resolve live capture {}", diag.summary()); + (capture.selection, insertion_target, diag) } #[cfg(not(any(target_os = "macos", target_os = "windows")))] pub(crate) fn resolve_selection_workspace_capture( ) -> (Option, SelectionInsertionTarget) { + let (selection, target, _) = resolve_selection_workspace_capture_with_diag(); + (selection, target) +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +pub(crate) fn resolve_selection_workspace_capture_with_diag() -> ( + Option, + SelectionInsertionTarget, + SelectionCaptureDiag, +) { let insertion_target = capture_selection_insertion_target(); - let capture = capture_selection_with_status(); - (capture.selection, insertion_target) + let (capture, reason) = capture_selection_with_status_diag(); + let target_captured = selection_insertion_target_is_captured(&insertion_target); + let (front_app, char_count) = match &capture.selection { + Some(selection) => ( + selection.source_app.clone(), + Some(selection.text.chars().count()), + ), + None => (capture_selection_source_app_hint(), None), + }; + let diag = SelectionCaptureDiag { + reason, + front_app, + used_prefetch: false, + target_captured, + char_count, + }; + log::info!("[selection] resolve live capture {}", diag.summary()); + (capture.selection, insertion_target, diag) +} + +fn capture_selection_source_app_hint() -> Option { + current_front_app() } /// Snapshot the insertion target before starting an asynchronous Selection @@ -396,6 +534,10 @@ fn activate_app_by_pid(pid: i32) { /// 捕获选区。Linux 只通过 fcitx5 DBus 读取 PRIMARY 选区,失败统一视为无选区。 pub fn capture_selection_with_status() -> SelectionCaptureOutcome { + capture_selection_with_status_diag().0 +} + +fn capture_selection_with_status_diag() -> (SelectionCaptureOutcome, SelectionCaptureMissReason) { let source_app = current_front_app(); // 1. macOS AX 直读 @@ -411,34 +553,73 @@ pub fn capture_selection_with_status() -> SelectionCaptureOutcome { .map(|a| format!(" front_app={a}")) .unwrap_or_default() ); - return SelectionCaptureOutcome { - selection: Some(SelectionContext { - text: truncate_selection(trimmed), - source_app, - }), - }; + return ( + SelectionCaptureOutcome { + selection: Some(SelectionContext { + text: truncate_selection(trimmed), + source_app, + }), + }, + SelectionCaptureMissReason::Ok, + ); } + log::info!( + "[selection] AX read returned empty/whitespace{}", + source_app + .as_deref() + .map(|a| format!(" front_app={a}")) + .unwrap_or_default() + ); } // 2. 模拟复制 fallback(macOS / Windows) #[cfg(any(target_os = "macos", target_os = "windows"))] - if let Some(text) = simulate_copy_and_read() { - let trimmed = text.trim(); - if !trimmed.is_empty() { - log::info!( - "[selection] simulate-copy fallback OK ({} chars){}", - trimmed.chars().count(), - source_app - .as_deref() - .map(|a| format!(" front_app={a}")) - .unwrap_or_default() - ); - return SelectionCaptureOutcome { - selection: Some(SelectionContext { - text: truncate_selection(trimmed), - source_app, - }), - }; + { + match simulate_copy_and_read_diag() { + Ok(text) => { + let trimmed = text.trim(); + if !trimmed.is_empty() { + log::info!( + "[selection] simulate-copy fallback OK ({} chars){}", + trimmed.chars().count(), + source_app + .as_deref() + .map(|a| format!(" front_app={a}")) + .unwrap_or_default() + ); + return ( + SelectionCaptureOutcome { + selection: Some(SelectionContext { + text: truncate_selection(trimmed), + source_app, + }), + }, + SelectionCaptureMissReason::Ok, + ); + } + log::info!( + "[selection] simulate-copy returned whitespace-only{}", + source_app + .as_deref() + .map(|a| format!(" front_app={a}")) + .unwrap_or_default() + ); + return ( + SelectionCaptureOutcome { selection: None }, + SelectionCaptureMissReason::EmptyTrimmed, + ); + } + Err(reason) => { + log::info!( + "[selection] simulate-copy miss reason={}{}", + reason.as_str(), + source_app + .as_deref() + .map(|a| format!(" front_app={a}")) + .unwrap_or_default() + ); + return (SelectionCaptureOutcome { selection: None }, reason); + } } } @@ -455,17 +636,28 @@ pub fn capture_selection_with_status() -> SelectionCaptureOutcome { .map(|a| format!(" front_app={a}")) .unwrap_or_default() ); - return SelectionCaptureOutcome { - selection: Some(SelectionContext { - text: truncate_selection(trimmed), - source_app, - }), - }; + return ( + SelectionCaptureOutcome { + selection: Some(SelectionContext { + text: truncate_selection(trimmed), + source_app, + }), + }, + SelectionCaptureMissReason::Ok, + ); + } + linux_selection::LinuxSelectionRead::NoSelection => { + return ( + SelectionCaptureOutcome { selection: None }, + SelectionCaptureMissReason::EmptyTrimmed, + ); } - linux_selection::LinuxSelectionRead::NoSelection => {} } - SelectionCaptureOutcome { selection: None } + ( + SelectionCaptureOutcome { selection: None }, + SelectionCaptureMissReason::NoCapturePath, + ) } /// 长度截断到首 + 尾 + 标记。 @@ -484,12 +676,17 @@ fn truncate_selection(text: &str) -> String { #[cfg(any(target_os = "macos", target_os = "windows"))] fn simulate_copy_and_read() -> Option { + simulate_copy_and_read_diag().ok() +} + +#[cfg(any(target_os = "macos", target_os = "windows"))] +fn simulate_copy_and_read_diag() -> Result { // a) snapshot 当前剪贴板(用作还原原状态的备份) let mut clipboard = match arboard::Clipboard::new() { Ok(c) => c, Err(e) => { log::warn!("[selection] clipboard init failed: {e}"); - return None; + return Err(SelectionCaptureMissReason::ClipboardInitFailed); } }; let original = match clipboard.get_text() { @@ -533,11 +730,19 @@ fn simulate_copy_and_read() -> Option { } } - let captured = captured?; - if captured == sentinel || captured.is_empty() { - return None; + if !post_ok { + return Err(SelectionCaptureMissReason::CopyShortcutFailed); + } + let Some(captured) = captured else { + return Err(SelectionCaptureMissReason::ClipboardReadFailed); + }; + if captured == sentinel { + return Err(SelectionCaptureMissReason::ClipboardUnchangedSentinel); + } + if captured.is_empty() { + return Err(SelectionCaptureMissReason::ClipboardEmptyAfterCopy); } - Some(captured) + Ok(captured) } /// Read the current selection in the same normalized/truncated form stored by From 8a0005caed0a4fea59462b0bd241aebad358a80a Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Wed, 26 Aug 2026 01:12:49 +0800 Subject: [PATCH 3/9] fix(selection): fix capture diag logging compile errors Format optional front_app in prefetch miss logs and gate the NoCapturePath fallback to non-desktop targets so macOS/Windows/Linux builds compile cleanly. Co-authored-by: Cursor --- openless-all/app/src-tauri/src/selection.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index f185dde7..b4b4d1f5 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -193,7 +193,7 @@ pub(crate) fn prefetch_selection_workspace_capture() { log::info!( "[selection] prefetch missed (no selection at hotkey edge) reason={} front_app={} target_captured={}", reason.as_str(), - capture_selection_source_app_hint(), + capture_selection_source_app_hint().as_deref().unwrap_or("-"), selection_insertion_target_is_captured(&insertion_target) ); guard.take(); @@ -654,6 +654,7 @@ fn capture_selection_with_status_diag() -> (SelectionCaptureOutcome, SelectionCa } } + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] ( SelectionCaptureOutcome { selection: None }, SelectionCaptureMissReason::NoCapturePath, From f6fd7828d486d475bac7dad6de9f03f7f539502d Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Wed, 26 Aug 2026 12:25:28 +0800 Subject: [PATCH 4/9] debug(selection): add sentinel/post_copy DEBUG logs to diagnose Chrome capture Co-authored-by: Cursor --- openless-all/app/src-tauri/src/selection.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index b4b4d1f5..741a37b3 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -708,6 +708,11 @@ fn simulate_copy_and_read_diag() -> Result { // c) 模拟 Cmd+C / Ctrl+C let post_ok = post_copy_shortcut(); + log::info!( + "[selection] DEBUG post_copy: post_ok={} original_was_some={}", + post_ok, + original.is_some() + ); if !post_ok { log::warn!("[selection] post_copy_shortcut failed"); // 不立刻 return:剪贴板可能已经被某些路径污染,按下方还原流程恢复。 @@ -720,7 +725,7 @@ fn simulate_copy_and_read_diag() -> Result { let captured = clipboard.get_text().ok(); // f) 还原原剪贴板 - if let Some(prev) = original { + if let Some(ref prev) = original { if let Err(e) = clipboard.set_text(prev) { log::warn!("[selection] clipboard restore failed: {e}"); } @@ -738,8 +743,20 @@ fn simulate_copy_and_read_diag() -> Result { return Err(SelectionCaptureMissReason::ClipboardReadFailed); }; if captured == sentinel { + log::info!( + "[selection] DEBUG sentinel_check: sentinel_eq_captured=true sentinel_prefix='{}' captured_len={} original_was_some={}", + &sentinel[..32.min(sentinel.len())], + captured.len(), + original.is_some() + ); return Err(SelectionCaptureMissReason::ClipboardUnchangedSentinel); } + log::info!( + "[selection] DEBUG sentinel_check: sentinel_eq_captured=false sentinel_prefix='{}' captured_len={} captured_preview='{}'", + &sentinel[..32.min(sentinel.len())], + captured.len(), + &captured[..captured.len().min(50)] + ); if captured.is_empty() { return Err(SelectionCaptureMissReason::ClipboardEmptyAfterCopy); } From 14d5aece89f5ce684fc5251e3da39227f23f4da8 Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Wed, 26 Aug 2026 15:47:31 +0800 Subject: [PATCH 5/9] fix(selection): Windows content-change check instead of sentinel for Ctrl+C validation Co-authored-by: Cursor --- openless-all/app/src-tauri/src/selection.rs | 84 ++++++++++++++++++--- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index 741a37b3..2036837d 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -742,21 +742,85 @@ fn simulate_copy_and_read_diag() -> Result { let Some(captured) = captured else { return Err(SelectionCaptureMissReason::ClipboardReadFailed); }; - if captured == sentinel { + + #[cfg(target_os = "macos")] + { + // macOS:无法确认 Ctrl+C 是否真的生效(Sentinel 方案)。 + // captured == sentinel → 应用没写剪贴板(没选区 / Ctrl+C 未生效) + // captured != sentinel → 应用写了剪贴板 + if captured == sentinel { + log::info!( + "[selection] DEBUG sentinel_check: sentinel_eq_captured=true sentinel_prefix='{}' captured_len={} original_was_some={}", + &sentinel[..32.min(sentinel.len())], + captured.len(), + original.is_some() + ); + return Err(SelectionCaptureMissReason::ClipboardUnchangedSentinel); + } log::info!( - "[selection] DEBUG sentinel_check: sentinel_eq_captured=true sentinel_prefix='{}' captured_len={} original_was_some={}", + "[selection] DEBUG sentinel_check: sentinel_eq_captured=false sentinel_prefix='{}' captured_len={} captured_preview='{}'", &sentinel[..32.min(sentinel.len())], captured.len(), - original.is_some() + &captured[..captured.len().min(50)] ); - return Err(SelectionCaptureMissReason::ClipboardUnchangedSentinel); } - log::info!( - "[selection] DEBUG sentinel_check: sentinel_eq_captured=false sentinel_prefix='{}' captured_len={} captured_preview='{}'", - &sentinel[..32.min(sentinel.len())], - captured.len(), - &captured[..captured.len().min(50)] - ); + + #[cfg(target_os = "windows")] + { + // Windows:用内容变化检测替代 Sentinel 检查。 + // Sentinel 检查过于严格——Chrome/VSCode/Notepad 等应用在焦点变化、UI 状态、 + // 安全沙箱限制下,SendInput(Ctrl+C) 可能被应用忽略,导致剪贴板不更新。 + // 但 `captured != sentinel && captured != original` 说明 Ctrl+C 确实触发了剪贴板写入。 + // + // 场景分析(captured vs original vs sentinel): + // - captured=sentinel, original=Some("foo") → sentinel 未覆盖,应用无选区/未响应 + // - captured=sentinel, original=None → sentinel 未覆盖,应用无选区/未响应 + // - captured="foo", original=Some("foo") → 内容未变,应用可能有选区但内容相同 + // - captured="bar", original=Some("foo") → 应用写了新内容,选区有效 + // - captured="bar", original=None → 应用写了新内容,选区有效 + // + // 核心判断: + // captured == sentinel → 失败(应用未响应) + // captured != sentinel && captured != original → 成功(应用写了新内容) + // captured != sentinel && captured == original → 失败(应用未写新内容) + let original_str = original.as_ref().map(|s| s.as_str()).unwrap_or(""); + let captured_changed = captured != sentinel && captured != original_str; + let app_responded = captured != sentinel; + + if !app_responded { + // 应用未响应 Ctrl+C + log::info!( + "[selection] DEBUG windows_content_check: app_responded=false captured_is_sentinel=true original_was_some={}", + original.is_some() + ); + return Err(SelectionCaptureMissReason::ClipboardUnchangedSentinel); + } + + if captured_changed { + // 应用写了新内容——选区有效 + log::info!( + "[selection] DEBUG windows_content_check: app_responded=true captured_changed=true captured_len={} captured_preview='{}'", + captured.len(), + &captured[..captured.len().min(50)] + ); + } else { + // 应用未写新内容(内容与原内容相同或为空) + log::info!( + "[selection] DEBUG windows_content_check: app_responded=true captured_changed=false captured_len={} captured_preview='{}' original_was_some={}", + captured.len(), + &captured[..captured.len().min(50)], + original.is_some() + ); + return Err(SelectionCaptureMissReason::ClipboardEmptyAfterCopy); + } + } + + #[cfg(target_os = "linux")] + { + // Linux 不走此函数 + unreachable!(); + } + if captured.is_empty() { return Err(SelectionCaptureMissReason::ClipboardEmptyAfterCopy); } From 1bb3a957a980583a590f4a855133f2256794ff9f Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Tue, 1 Sep 2026 08:59:26 +0800 Subject: [PATCH 6/9] fix(selection): settle clipboard after sentinel for Windows history race Clipboard History async listeners can hold the clipboard after we write the sentinel, so target-app Ctrl+C silently fails. Wait for settle, poll, and optionally resend Ctrl+C. Bump to 1.3.18-Beta.10. Co-authored-by: Cursor --- openless-all/app/package-lock.json | 4 +- openless-all/app/package.json | 2 +- openless-all/app/src-tauri/Cargo.toml | 3 +- openless-all/app/src-tauri/src/selection.rs | 335 +++++++++++++++++--- openless-all/app/src-tauri/tauri.conf.json | 2 +- 5 files changed, 296 insertions(+), 50 deletions(-) diff --git a/openless-all/app/package-lock.json b/openless-all/app/package-lock.json index 3887cb99..5fa63756 100644 --- a/openless-all/app/package-lock.json +++ b/openless-all/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "openless-app", - "version": "1.3.18-Beta.7", + "version": "1.3.18-Beta.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openless-app", - "version": "1.3.18-Beta.7", + "version": "1.3.18-Beta.10", "dependencies": { "@base-ui/react": "^1.6.0", "@formkit/auto-animate": "^0.9.0", diff --git a/openless-all/app/package.json b/openless-all/app/package.json index c353dcb4..90454ee5 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -1,7 +1,7 @@ { "name": "openless-app", "private": true, - "version": "1.3.18-Beta.7", + "version": "1.3.18-Beta.10", "type": "module", "scripts": { "pretest": "npm run build", diff --git a/openless-all/app/src-tauri/Cargo.toml b/openless-all/app/src-tauri/Cargo.toml index 2b7617cb..3b541dd7 100644 --- a/openless-all/app/src-tauri/Cargo.toml +++ b/openless-all/app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openless" -version = "1.3.18-Beta.7" +version = "1.3.18-Beta.10" description = "OpenLess — local voice input that types where your cursor is" authors = ["OpenLess"] edition = "2021" @@ -149,6 +149,7 @@ windows = { version = "0.58", features = [ "Win32_Media_Audio_Endpoints", "Win32_Storage_FileSystem", "Win32_System_Com", + "Win32_System_DataExchange", "Win32_System_Ole", "Win32_System_Registry", "Win32_System_Threading", diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index 2036837d..3596de84 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -706,8 +706,16 @@ fn simulate_copy_and_read_diag() -> Result { // 即使设置 sentinel 失败,也尝试发 Cmd+C 看能不能直接拿到东西 } + // Windows + Clipboard History: writing the sentinel wakes an async listener that may + // briefly own/open the clipboard. Target-app Ctrl+C then fails silently and leaves + // the sentinel in place (user A/B: history OFF → OK, history ON → fail). Settle first. + #[cfg(target_os = "windows")] + let settle = windows_wait_clipboard_settle_after_write(); + // c) 模拟 Cmd+C / Ctrl+C - let post_ok = post_copy_shortcut(); + #[cfg(target_os = "windows")] + let pre_copy_env = windows_copy_env_snapshot("pre_send"); + let mut post_ok = post_copy_shortcut(); log::info!( "[selection] DEBUG post_copy: post_ok={} original_was_some={}", post_ok, @@ -718,12 +726,57 @@ fn simulate_copy_and_read_diag() -> Result { // 不立刻 return:剪贴板可能已经被某些路径污染,按下方还原流程恢复。 } - // d) 等剪贴板更新(macOS / Windows 都需要少量时间让目标 app 把数据 put 进去) - std::thread::sleep(Duration::from_millis(80)); + // d) 等剪贴板更新;Windows 在开启剪贴板历史时需要轮询 + 必要时重发 Ctrl+C。 + #[cfg(not(target_os = "windows"))] + { + std::thread::sleep(Duration::from_millis(80)); + } - // e) 读新值 + #[cfg(target_os = "windows")] + let (captured, copy_poll) = { + let poll = windows_poll_clipboard_after_copy(&mut clipboard, &sentinel, &mut post_ok); + (poll.captured.clone(), poll) + }; + #[cfg(not(target_os = "windows"))] let captured = clipboard.get_text().ok(); + #[cfg(target_os = "windows")] + let post_wait_env = windows_copy_env_snapshot("post_wait"); + + // #region agent log + #[cfg(target_os = "windows")] + { + let seq_delta = post_wait_env + .clipboard_seq + .zip(pre_copy_env.clipboard_seq) + .map(|(after, before)| after.wrapping_sub(before)); + let payload = serde_json::json!({ + "sessionId": "ddfc8d", + "runId": "post-fix", + "hypothesisId": "A", + "location": "selection.rs:simulate_copy_and_read_diag", + "message": "windows copy after clipboard-history settle", + "data": { + "post_ok": post_ok, + "captured_is_sentinel": captured.as_ref() == Some(&sentinel), + "captured_len": captured.as_ref().map(|s| s.len()), + "seq_delta": seq_delta, + "settle_ms": settle.waited_ms, + "settle_open_cleared": settle.open_cleared, + "settle_seq_stable": settle.seq_stable, + "poll_ms": copy_poll.waited_ms, + "poll_attempts": copy_poll.attempts, + "resent_ctrl_c": copy_poll.resent_ctrl_c, + "pre": pre_copy_env, + "post_wait": post_wait_env, + }, + "timestamp": chrono::Utc::now().timestamp_millis(), + }); + log::info!("[selection] DEBUG agent_env {}", payload); + agent_debug_ndjson(&payload); + } + // #endregion + // f) 还原原剪贴板 if let Some(ref prev) = original { if let Err(e) = clipboard.set_text(prev) { @@ -767,52 +820,20 @@ fn simulate_copy_and_read_diag() -> Result { #[cfg(target_os = "windows")] { - // Windows:用内容变化检测替代 Sentinel 检查。 - // Sentinel 检查过于严格——Chrome/VSCode/Notepad 等应用在焦点变化、UI 状态、 - // 安全沙箱限制下,SendInput(Ctrl+C) 可能被应用忽略,导致剪贴板不更新。 - // 但 `captured != sentinel && captured != original` 说明 Ctrl+C 确实触发了剪贴板写入。 - // - // 场景分析(captured vs original vs sentinel): - // - captured=sentinel, original=Some("foo") → sentinel 未覆盖,应用无选区/未响应 - // - captured=sentinel, original=None → sentinel 未覆盖,应用无选区/未响应 - // - captured="foo", original=Some("foo") → 内容未变,应用可能有选区但内容相同 - // - captured="bar", original=Some("foo") → 应用写了新内容,选区有效 - // - captured="bar", original=None → 应用写了新内容,选区有效 - // - // 核心判断: - // captured == sentinel → 失败(应用未响应) - // captured != sentinel && captured != original → 成功(应用写了新内容) - // captured != sentinel && captured == original → 失败(应用未写新内容) - let original_str = original.as_ref().map(|s| s.as_str()).unwrap_or(""); - let captured_changed = captured != sentinel && captured != original_str; - let app_responded = captured != sentinel; - - if !app_responded { - // 应用未响应 Ctrl+C + // Runtime evidence (Beta.9 / ChatGPT+Notepad+Chrome): when Ctrl+C is ignored, + // captured stays equal to sentinel. Content-vs-original checks do not recover that + // case; keep the same sentinel gate as macOS and rely on agent_env probes instead. + if captured == sentinel { log::info!( - "[selection] DEBUG windows_content_check: app_responded=false captured_is_sentinel=true original_was_some={}", + "[selection] DEBUG sentinel_check: sentinel_eq_captured=true original_was_some={}", original.is_some() ); return Err(SelectionCaptureMissReason::ClipboardUnchangedSentinel); } - - if captured_changed { - // 应用写了新内容——选区有效 - log::info!( - "[selection] DEBUG windows_content_check: app_responded=true captured_changed=true captured_len={} captured_preview='{}'", - captured.len(), - &captured[..captured.len().min(50)] - ); - } else { - // 应用未写新内容(内容与原内容相同或为空) - log::info!( - "[selection] DEBUG windows_content_check: app_responded=true captured_changed=false captured_len={} captured_preview='{}' original_was_some={}", - captured.len(), - &captured[..captured.len().min(50)], - original.is_some() - ); - return Err(SelectionCaptureMissReason::ClipboardEmptyAfterCopy); - } + log::info!( + "[selection] DEBUG sentinel_check: sentinel_eq_captured=false captured_len={}", + captured.len() + ); } #[cfg(target_os = "linux")] @@ -1186,6 +1207,230 @@ mod macos_paste { // ─────────────────────────── Windows Ctrl+C send ─────────────────────────── +#[cfg(target_os = "windows")] +struct WindowsClipboardSettle { + waited_ms: u64, + open_cleared: bool, + seq_stable: bool, +} + +#[cfg(target_os = "windows")] +struct WindowsCopyPollResult { + captured: Option, + waited_ms: u64, + attempts: u32, + resent_ctrl_c: bool, +} + +/// After we write a sentinel, Clipboard History (and other listeners) may briefly open +/// the clipboard. Wait until nobody holds it and the sequence number stops moving. +#[cfg(target_os = "windows")] +fn windows_wait_clipboard_settle_after_write() -> WindowsClipboardSettle { + use windows::Win32::System::DataExchange::{GetClipboardSequenceNumber, GetOpenClipboardWindow}; + + const MAX_MS: u64 = 250; + const STEP_MS: u64 = 10; + let start = std::time::Instant::now(); + let mut last_seq = unsafe { GetClipboardSequenceNumber() }; + let mut stable_rounds = 0u32; + let mut open_cleared = false; + let mut seq_stable = false; + + while start.elapsed().as_millis() as u64 < MAX_MS { + let open = unsafe { GetOpenClipboardWindow() }; + let seq = unsafe { GetClipboardSequenceNumber() }; + if open.0.is_null() { + open_cleared = true; + if seq == last_seq { + stable_rounds += 1; + // ~30ms of unchanged seq with clipboard free → history likely caught up. + if stable_rounds >= 3 { + seq_stable = true; + break; + } + } else { + stable_rounds = 0; + last_seq = seq; + } + } else { + open_cleared = false; + stable_rounds = 0; + last_seq = seq; + } + std::thread::sleep(Duration::from_millis(STEP_MS)); + } + + let waited_ms = start.elapsed().as_millis() as u64; + log::info!( + "[selection] DEBUG clipboard_settle: waited_ms={} open_cleared={} seq_stable={}", + waited_ms, + open_cleared, + seq_stable + ); + WindowsClipboardSettle { + waited_ms, + open_cleared, + seq_stable, + } +} + +/// Poll until clipboard leaves the sentinel (selection copy landed). One Ctrl+C resend +/// mid-window covers the case where the first chord hit a still-busy clipboard. +#[cfg(target_os = "windows")] +fn windows_poll_clipboard_after_copy( + clipboard: &mut arboard::Clipboard, + sentinel: &str, + post_ok: &mut bool, +) -> WindowsCopyPollResult { + const MAX_MS: u64 = 450; + const STEP_MS: u64 = 25; + let start = std::time::Instant::now(); + let mut attempts = 0u32; + let mut resent_ctrl_c = false; + let mut captured = None; + + while start.elapsed().as_millis() as u64 < MAX_MS { + attempts += 1; + match clipboard.get_text() { + Ok(text) if text != sentinel => { + captured = Some(text); + break; + } + Ok(text) => { + captured = Some(text); + } + Err(_) => { + captured = None; + } + } + + // Halfway through: if still on sentinel, resend Ctrl+C once. + if !resent_ctrl_c && start.elapsed().as_millis() as u64 >= 150 { + if post_copy_shortcut() { + *post_ok = true; + resent_ctrl_c = true; + log::info!("[selection] DEBUG clipboard_poll: resent Ctrl+C after settle miss"); + } + } + std::thread::sleep(Duration::from_millis(STEP_MS)); + } + + let waited_ms = start.elapsed().as_millis() as u64; + log::info!( + "[selection] DEBUG clipboard_poll: waited_ms={} attempts={} resent={} still_sentinel={}", + waited_ms, + attempts, + resent_ctrl_c, + captured.as_deref() == Some(sentinel) + ); + WindowsCopyPollResult { + captured, + waited_ms, + attempts, + resent_ctrl_c, + } +} + +#[cfg(target_os = "windows")] +#[derive(Debug, serde::Serialize)] +struct WindowsCopyEnvSnapshot { + phase: &'static str, + ctrl_down: bool, + alt_down: bool, + shift_down: bool, + win_down: bool, + fg_hwnd: usize, + focus_hwnd: usize, + fg_class: String, + focus_class: String, + clipboard_seq: Option, + open_clipboard_hwnd: usize, +} + +#[cfg(target_os = "windows")] +fn windows_copy_env_snapshot(phase: &'static str) -> WindowsCopyEnvSnapshot { + use windows::Win32::System::DataExchange::{GetClipboardSequenceNumber, GetOpenClipboardWindow}; + use windows::Win32::UI::Input::KeyboardAndMouse::{ + GetAsyncKeyState, VK_CONTROL, VK_LWIN, VK_MENU, VK_RWIN, VK_SHIFT, + }; + use windows::Win32::UI::WindowsAndMessaging::{ + GetClassNameW, GetForegroundWindow, GetGUIThreadInfo, GetWindowThreadProcessId, + GUITHREADINFO, + }; + + unsafe { + let key_down = |vk| (GetAsyncKeyState(vk.0 as i32) as u16) & 0x8000 != 0; + let ctrl_down = key_down(VK_CONTROL); + let alt_down = key_down(VK_MENU); + let shift_down = key_down(VK_SHIFT); + let win_down = key_down(VK_LWIN) || key_down(VK_RWIN); + + let foreground = GetForegroundWindow(); + let mut gui_info = GUITHREADINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let focus = if !foreground.0.is_null() { + let tid = GetWindowThreadProcessId(foreground, None); + if tid != 0 && GetGUIThreadInfo(tid, &mut gui_info).is_ok() && !gui_info.hwndFocus.0.is_null() + { + gui_info.hwndFocus + } else { + foreground + } + } else { + foreground + }; + + let class_of = |hwnd: windows::Win32::Foundation::HWND| -> String { + if hwnd.0.is_null() { + return String::new(); + } + let mut buf = [0u16; 256]; + let n = GetClassNameW(hwnd, &mut buf); + if n <= 0 { + return String::new(); + } + String::from_utf16_lossy(&buf[..n as usize]) + }; + + WindowsCopyEnvSnapshot { + phase, + ctrl_down, + alt_down, + shift_down, + win_down, + fg_hwnd: foreground.0 as usize, + focus_hwnd: focus.0 as usize, + fg_class: class_of(foreground), + focus_class: class_of(focus), + clipboard_seq: Some(GetClipboardSequenceNumber()), + open_clipboard_hwnd: GetOpenClipboardWindow().0 as usize, + } + } +} + +// #region agent log +#[cfg(target_os = "windows")] +fn agent_debug_ndjson(payload: &serde_json::Value) { + use std::io::Write; + let line = payload.to_string(); + for path in [ + std::path::Path::new("debug-ddfc8d.log"), + std::path::Path::new(r"f:\编程\openless-agent-eval-960\debug-ddfc8d.log"), + ] { + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + { + let _ = writeln!(f, "{line}"); + break; + } + } +} +// #endregion + #[cfg(target_os = "windows")] mod windows_paste { use windows::Win32::UI::Input::KeyboardAndMouse::{ diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index cb2bc965..de878d61 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenLess", - "version": "1.3.18-Beta.7", + "version": "1.3.18-Beta.10", "identifier": "com.openless.app", "build": { "beforeDevCommand": "npm run dev", From b7294c58d4a5261a5d576724b442fa48997a9519 Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Tue, 1 Sep 2026 09:09:57 +0800 Subject: [PATCH 7/9] fix(selection): parenthesize duration cast comparisons for Rust parse Bare `as u64 <` is parsed as a generic; cfg(windows) code still must parse on Linux CI. Co-authored-by: Cursor --- openless-all/app/src-tauri/src/selection.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index 3596de84..1d051803 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -1236,7 +1236,7 @@ fn windows_wait_clipboard_settle_after_write() -> WindowsClipboardSettle { let mut open_cleared = false; let mut seq_stable = false; - while start.elapsed().as_millis() as u64 < MAX_MS { + while (start.elapsed().as_millis() as u64) < MAX_MS { let open = unsafe { GetOpenClipboardWindow() }; let seq = unsafe { GetClipboardSequenceNumber() }; if open.0.is_null() { @@ -1289,7 +1289,7 @@ fn windows_poll_clipboard_after_copy( let mut resent_ctrl_c = false; let mut captured = None; - while start.elapsed().as_millis() as u64 < MAX_MS { + while (start.elapsed().as_millis() as u64) < MAX_MS { attempts += 1; match clipboard.get_text() { Ok(text) if text != sentinel => { @@ -1305,7 +1305,7 @@ fn windows_poll_clipboard_after_copy( } // Halfway through: if still on sentinel, resend Ctrl+C once. - if !resent_ctrl_c && start.elapsed().as_millis() as u64 >= 150 { + if !resent_ctrl_c && (start.elapsed().as_millis() as u64) >= 150 { if post_copy_shortcut() { *post_ok = true; resent_ctrl_c = true; From b50b9faf6df4efced91b4123de86a15a1fc321ff Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Tue, 1 Sep 2026 09:21:36 +0800 Subject: [PATCH 8/9] fix(selection): handle GetOpenClipboardWindow Result and type key_down windows 0.58 returns Result; annotate VIRTUAL_KEY for GetAsyncKeyState. Co-authored-by: Cursor --- openless-all/app/src-tauri/src/selection.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index 1d051803..46c423f2 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -1237,9 +1237,12 @@ fn windows_wait_clipboard_settle_after_write() -> WindowsClipboardSettle { let mut seq_stable = false; while (start.elapsed().as_millis() as u64) < MAX_MS { - let open = unsafe { GetOpenClipboardWindow() }; + let open_is_free = match unsafe { GetOpenClipboardWindow() } { + Ok(hwnd) => hwnd.0.is_null(), + Err(_) => true, + }; let seq = unsafe { GetClipboardSequenceNumber() }; - if open.0.is_null() { + if open_is_free { open_cleared = true; if seq == last_seq { stable_rounds += 1; @@ -1359,7 +1362,8 @@ fn windows_copy_env_snapshot(phase: &'static str) -> WindowsCopyEnvSnapshot { }; unsafe { - let key_down = |vk| (GetAsyncKeyState(vk.0 as i32) as u16) & 0x8000 != 0; + use windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY; + let key_down = |vk: VIRTUAL_KEY| ((GetAsyncKeyState(vk.0 as i32) as u16) & 0x8000) != 0; let ctrl_down = key_down(VK_CONTROL); let alt_down = key_down(VK_MENU); let shift_down = key_down(VK_SHIFT); @@ -1394,6 +1398,11 @@ fn windows_copy_env_snapshot(phase: &'static str) -> WindowsCopyEnvSnapshot { String::from_utf16_lossy(&buf[..n as usize]) }; + let open_clipboard_hwnd = GetOpenClipboardWindow() + .ok() + .map(|hwnd| hwnd.0 as usize) + .unwrap_or(0); + WindowsCopyEnvSnapshot { phase, ctrl_down, @@ -1405,7 +1414,7 @@ fn windows_copy_env_snapshot(phase: &'static str) -> WindowsCopyEnvSnapshot { fg_class: class_of(foreground), focus_class: class_of(focus), clipboard_seq: Some(GetClipboardSequenceNumber()), - open_clipboard_hwnd: GetOpenClipboardWindow().0 as usize, + open_clipboard_hwnd, } } } From d537dd003937b2480dabae51d46d0d70c085c070 Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Wed, 2 Sep 2026 08:11:29 +0000 Subject: [PATCH 9/9] =?UTF-8?q?chore(selection):=20=E4=BB=8E=20PR=20?= =?UTF-8?q?=E4=B8=AD=E7=A7=BB=E9=99=A4=20fork=20=E4=B8=93=E7=94=A8?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=E4=B8=8E=20release=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上游 PR 只保留选区捕获 / 剪贴板 sentinel 修复。版本号仍由上游发布流程管理。 Co-authored-by: Cursor --- openless-all/app/package-lock.json | 4 ++-- openless-all/app/package.json | 2 +- openless-all/app/src-tauri/Cargo.toml | 2 +- openless-all/app/src-tauri/tauri.conf.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openless-all/app/package-lock.json b/openless-all/app/package-lock.json index 5fa63756..3887cb99 100644 --- a/openless-all/app/package-lock.json +++ b/openless-all/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "openless-app", - "version": "1.3.18-Beta.10", + "version": "1.3.18-Beta.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openless-app", - "version": "1.3.18-Beta.10", + "version": "1.3.18-Beta.7", "dependencies": { "@base-ui/react": "^1.6.0", "@formkit/auto-animate": "^0.9.0", diff --git a/openless-all/app/package.json b/openless-all/app/package.json index 90454ee5..c353dcb4 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -1,7 +1,7 @@ { "name": "openless-app", "private": true, - "version": "1.3.18-Beta.10", + "version": "1.3.18-Beta.7", "type": "module", "scripts": { "pretest": "npm run build", diff --git a/openless-all/app/src-tauri/Cargo.toml b/openless-all/app/src-tauri/Cargo.toml index 3b541dd7..6d6f930e 100644 --- a/openless-all/app/src-tauri/Cargo.toml +++ b/openless-all/app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openless" -version = "1.3.18-Beta.10" +version = "1.3.18-Beta.7" description = "OpenLess — local voice input that types where your cursor is" authors = ["OpenLess"] edition = "2021" diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index de878d61..cb2bc965 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenLess", - "version": "1.3.18-Beta.10", + "version": "1.3.18-Beta.7", "identifier": "com.openless.app", "build": { "beforeDevCommand": "npm run dev",