Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion openless-all/app/src-tauri/src/commands/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,13 @@ fn apply_retranscription(
mod retranscribe_tests {
use super::apply_retranscription;
use crate::coordinator::AsrCallLabel;
use crate::types::{DictationSession, InsertStatus, PolishMode};
use crate::types::{DictationSession, HistorySource, InsertStatus, PolishMode};

fn failed_entry() -> DictationSession {
DictationSession {
id: "s1".into(),
created_at: "2026-07-15T00:00:00Z".into(),
source: HistorySource::Voice,
raw_transcript: String::new(),
final_text: String::new(),
mode: PolishMode::Light,
Expand Down
137 changes: 137 additions & 0 deletions openless-all/app/src-tauri/src/commands/hotkeys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub fn set_dictation_hotkey(
if let Some(less_computer) = prefs.coding_agent_voice_hotkey.as_ref() {
reject_dictation_less_computer_hotkey_overlap(&binding, less_computer)?;
}
reject_existing_selection_polish_hotkey_overlap(&binding, &prefs)?;
prefs.dictation_hotkey = binding;
sync_dictation_hotkey_legacy_fields(&mut prefs);
coord.prefs().set(prefs).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -55,6 +56,7 @@ pub fn set_translation_hotkey(
if let Some(less_computer) = previous.coding_agent_voice_hotkey.as_ref() {
reject_translation_less_computer_hotkey_overlap(&binding, less_computer)?;
}
reject_existing_selection_polish_hotkey_overlap(&binding, &previous)?;
let mut prefs = previous.clone();
prefs.translation_hotkey = binding;
coord.prefs().set(prefs).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -93,6 +95,7 @@ pub fn set_switch_style_hotkey(
if let Some(less_computer) = prefs.coding_agent_voice_hotkey.as_ref() {
reject_less_computer_switch_style_hotkey_overlap(less_computer, binding)?;
}
reject_existing_selection_polish_hotkey_overlap(binding, &prefs)?;
}
prefs.switch_style_hotkey = binding;
coord.prefs().set(prefs).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -124,13 +127,48 @@ pub fn set_open_app_hotkey(
if let Some(less_computer) = prefs.coding_agent_voice_hotkey.as_ref() {
reject_less_computer_open_app_hotkey_overlap(less_computer, binding)?;
}
reject_existing_selection_polish_hotkey_overlap(binding, &prefs)?;
}
prefs.open_app_hotkey = binding;
coord.prefs().set(prefs).map_err(|e| e.to_string())?;
coord.update_open_app_hotkey_binding();
Ok(())
}

/// Set the Selection Polish global shortcut. The new binding is persisted first
/// so the coordinator sees it during registration; a registration failure
/// restores the exact previous preferences and listener state before returning.
/// 选区润色为桌面(Windows-first)工作流,mobile 不注册。
#[cfg(not(mobile))]
#[tauri::command]
pub fn set_selection_polish_hotkey(
coord: CoordinatorState<'_>,
binding: Option<ShortcutBinding>,
) -> Result<(), String> {
if let Some(binding) = binding.as_ref() {
crate::shortcut_binding::validate_binding(binding).map_err(|e| e.to_string())?;
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
reject_bare_shift_dictation_shortcut(binding)?;
}
let previous = coord.prefs().get();
if let Some(binding) = binding.as_ref() {
reject_selection_polish_hotkey_collisions(binding, &previous)?;
}
let mut next = previous.clone();
next.selection_polish_hotkey = binding;
coord.prefs().set(next).map_err(|e| e.to_string())?;
if let Err(error) = coord.try_update_selection_polish_hotkey_binding() {
if let Err(rollback_error) = coord.prefs().set(previous) {
return Err(format!(
"{error}; additionally failed to restore previous Selection Polish shortcut: {rollback_error}"
));
}
coord.update_selection_polish_hotkey_binding();
return Err(error);
}
Ok(())
}

fn reject_modifier_only_action_shortcut(binding: &ShortcutBinding) -> Result<(), String> {
if binding.modifiers.is_empty()
&& (binding.primary.eq_ignore_ascii_case("shift")
Expand Down Expand Up @@ -174,6 +212,7 @@ pub fn set_combo_hotkey(coord: CoordinatorState<'_>, binding: ComboBinding) -> R
if let Some(less_computer) = prefs.coding_agent_voice_hotkey.as_ref() {
reject_dictation_less_computer_hotkey_overlap(&shortcut, less_computer)?;
}
reject_existing_selection_polish_hotkey_overlap(&shortcut, &prefs)?;
prefs.custom_combo_hotkey = Some(binding);
prefs.dictation_hotkey = shortcut;
sync_dictation_hotkey_legacy_fields(&mut prefs);
Expand Down Expand Up @@ -275,6 +314,64 @@ pub(crate) fn reject_hotkey_collisions(prefs: &UserPreferences) -> Result<(), St
if let (Some(switch_style), Some(open_app)) = (switch_style, open_app) {
reject_switch_style_open_app_hotkey_overlap(switch_style, open_app)?;
}
if let Some(selection_polish) = prefs.selection_polish_hotkey.as_ref() {
reject_selection_polish_hotkey_collisions(selection_polish, prefs)?;
}
Ok(())
}

pub(crate) fn reject_selection_polish_hotkey_collisions(
selection_polish: &ShortcutBinding,
prefs: &UserPreferences,
) -> Result<(), String> {
reject_hotkey_overlap(
selection_polish,
&prefs.dictation_hotkey,
"选区润色快捷键不能和听写快捷键相同",
)?;
reject_hotkey_overlap(
selection_polish,
&prefs.translation_hotkey,
"选区润色快捷键不能和翻译快捷键相同",
)?;
if let Some(qa) = prefs.qa_hotkey.as_ref() {
reject_hotkey_overlap(selection_polish, qa, "选区润色快捷键不能和 QA 快捷键相同")?;
}
if let Some(switch_style) = prefs.switch_style_hotkey.as_ref() {
reject_hotkey_overlap(
selection_polish,
switch_style,
"选区润色快捷键不能和切换风格快捷键相同",
)?;
}
if let Some(open_app) = prefs.open_app_hotkey.as_ref() {
reject_hotkey_overlap(
selection_polish,
open_app,
"选区润色快捷键不能和打开应用快捷键相同",
)?;
}
if let Some(less_computer) = prefs.coding_agent_voice_hotkey.as_ref() {
reject_hotkey_overlap(
selection_polish,
less_computer,
"选区润色快捷键不能和 Less Computer 快捷键相同",
)?;
}
Ok(())
}

pub(crate) fn reject_existing_selection_polish_hotkey_overlap(
binding: &ShortcutBinding,
prefs: &UserPreferences,
) -> Result<(), String> {
if let Some(selection_polish) = prefs.selection_polish_hotkey.as_ref() {
reject_hotkey_overlap(
binding,
selection_polish,
"该快捷键不能和选区润色快捷键相同",
)?;
}
Ok(())
}

Expand All @@ -291,6 +388,11 @@ pub(crate) fn reject_non_dictation_side_specific_shortcuts(
if let Some(binding) = prefs.open_app_hotkey.as_ref() {
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
}
if let Some(binding) = prefs.selection_polish_hotkey.as_ref() {
crate::shortcut_binding::validate_binding(binding).map_err(|e| e.to_string())?;
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
reject_bare_shift_dictation_shortcut(binding)?;
}
if let Some(binding) = prefs.coding_agent_voice_hotkey.as_ref() {
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
}
Expand Down Expand Up @@ -481,6 +583,29 @@ mod tests {
assert!(reject_hotkey_collisions(&prefs).is_ok());
}

#[test]
fn selection_polish_hotkey_collides_with_existing_shortcuts() {
let binding = key("RightControl");
let prefs = UserPreferences {
dictation_hotkey: binding.clone(),
selection_polish_hotkey: Some(binding),
..Default::default()
};
assert!(reject_hotkey_collisions(&prefs).is_err());
}

#[test]
fn existing_selection_polish_hotkey_rejects_another_action_binding() {
let selection = key("RightControl");
let prefs = UserPreferences {
selection_polish_hotkey: Some(selection.clone()),
..Default::default()
};

assert!(reject_existing_selection_polish_hotkey_overlap(&selection, &prefs).is_err());
assert!(reject_existing_selection_polish_hotkey_overlap(&key("P"), &prefs).is_ok());
}

#[test]
fn side_specific_dictation_overlaps_generic_qa_hotkey() {
let mut prefs = UserPreferences {
Expand Down Expand Up @@ -526,6 +651,18 @@ mod tests {
assert!(reject_non_dictation_side_specific_shortcuts(&prefs).is_err());
}

#[test]
fn rejects_side_specific_selection_polish_hotkey_on_save() {
let prefs = UserPreferences {
selection_polish_hotkey: Some(ShortcutBinding {
primary: "D".into(),
modifiers: vec!["cmd-right".into()],
}),
..Default::default()
};
assert!(reject_non_dictation_side_specific_shortcuts(&prefs).is_err());
}

#[test]
fn accepts_side_specific_dictation_hotkey_on_save() {
let prefs = UserPreferences {
Expand Down
14 changes: 14 additions & 0 deletions openless-all/app/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ mod remote_input;
mod settings;
#[cfg(not(mobile))]
mod sherpa_asr;
#[cfg(all(not(mobile), debug_assertions))]
mod selection_polish;
#[cfg(not(mobile))]
mod selection_polish_preview;
mod style_packs;

pub use credentials::*;
Expand All @@ -110,6 +114,10 @@ pub use settings::*;
#[cfg(not(mobile))]
#[allow(unused_imports)]
pub use sherpa_asr::*;
#[cfg(all(not(mobile), debug_assertions))]
pub use selection_polish::*;
#[cfg(not(mobile))]
pub use selection_polish_preview::*;
pub use style_packs::*;

pub(crate) type CoordinatorState<'a> = State<'a, Arc<Coordinator>>;
Expand Down Expand Up @@ -676,6 +684,8 @@ mod tests {
*self.open_app_refreshes.lock().unwrap() += 1;
}

fn refresh_selection_polish_hotkey(&self) {}

fn refresh_coding_agent_hotkey(&self) {
*self.coding_agent_refreshes.lock().unwrap() += 1;
}
Expand Down Expand Up @@ -820,6 +830,10 @@ mod tests {
primary: "RightControl".to_string(),
modifiers: vec![],
}),
// This fixture deliberately assigns Right Control to Less Computer.
// Keep selection polish disabled so the test exercises the intended
// independent refresh paths.
selection_polish_hotkey: None,
hotkey: HotkeyBinding {
trigger: HotkeyTrigger::Custom,
mode: HotkeyMode::Hold,
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src-tauri/src/commands/qa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub fn set_qa_hotkey(
if let Some(less_computer) = prefs.coding_agent_voice_hotkey.as_ref() {
reject_qa_less_computer_hotkey_overlap(binding, less_computer)?;
}
reject_existing_selection_polish_hotkey_overlap(binding, &prefs)?;
}
prefs.qa_hotkey = binding;
coord.prefs().set(prefs).map_err(|e| e.to_string())?;
Expand Down
7 changes: 7 additions & 0 deletions openless-all/app/src-tauri/src/commands/selection_polish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use super::*;

/// Development-only entry point for exercising the selection-polish workflow.
#[tauri::command]
pub async fn run_selection_polish_for_dev(coord: CoordinatorState<'_>) -> Result<(), String> {
coord.trigger_selection_polish_for_dev().await
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use super::*;
use crate::coordinator::selection_polish::SelectionPolishPreviewPayload;

#[tauri::command]
pub fn get_selection_polish_preview(
coord: CoordinatorState<'_>,
) -> Option<SelectionPolishPreviewPayload> {
coord.selection_polish_preview()
}

#[tauri::command]
pub fn confirm_selection_polish_preview(
coord: CoordinatorState<'_>,
text: String,
) -> Result<(), String> {
coord.confirm_selection_polish_preview(text)
}

#[tauri::command]
pub fn cancel_selection_polish_preview(coord: CoordinatorState<'_>) {
coord.cancel_selection_polish_preview();
}
20 changes: 20 additions & 0 deletions openless-all/app/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub(crate) trait SettingsWriter {
fn refresh_translation_hotkey(&self);
fn refresh_switch_style_hotkey(&self);
fn refresh_open_app_hotkey(&self);
fn refresh_selection_polish_hotkey(&self);
fn refresh_coding_agent_hotkey(&self);
}

Expand Down Expand Up @@ -77,6 +78,14 @@ impl SettingsWriter for Coordinator {
self.update_open_app_hotkey_binding();
}

#[cfg(not(mobile))]
fn refresh_selection_polish_hotkey(&self) {
self.update_selection_polish_hotkey_binding();
}

#[cfg(mobile)]
fn refresh_selection_polish_hotkey(&self) {}

fn refresh_coding_agent_hotkey(&self) {
self.update_coding_agent_hotkey_binding();
}
Expand Down Expand Up @@ -126,6 +135,10 @@ impl<T: SettingsWriter + ?Sized> SettingsWriter for Arc<T> {
(**self).refresh_open_app_hotkey();
}

fn refresh_selection_polish_hotkey(&self) {
(**self).refresh_selection_polish_hotkey();
}

fn refresh_coding_agent_hotkey(&self) {
(**self).refresh_coding_agent_hotkey();
}
Expand Down Expand Up @@ -157,6 +170,8 @@ pub(crate) fn persist_settings_with_keyboard_apply<T: SettingsWriter>(
let translation_changed = previous.translation_hotkey != prefs.translation_hotkey;
let switch_style_changed = previous.switch_style_hotkey != prefs.switch_style_hotkey;
let open_app_changed = previous.open_app_hotkey != prefs.open_app_hotkey;
let selection_polish_changed =
previous.selection_polish_hotkey != prefs.selection_polish_hotkey;
let coding_agent_changed = previous.coding_agent_enabled != prefs.coding_agent_enabled
|| previous.coding_agent_voice_hotkey != prefs.coding_agent_voice_hotkey;
let windows_keyboard_list_changed = previous.windows_sendinput_insertion_only
Expand Down Expand Up @@ -245,6 +260,9 @@ pub(crate) fn persist_settings_with_keyboard_apply<T: SettingsWriter>(
if open_app_changed {
coord.refresh_open_app_hotkey();
}
if selection_polish_changed {
coord.refresh_selection_polish_hotkey();
}
if coding_agent_changed {
coord.refresh_coding_agent_hotkey();
}
Expand Down Expand Up @@ -360,6 +378,7 @@ mod tests {
fn refresh_switch_style_hotkey(&self) {}

fn refresh_open_app_hotkey(&self) {}
fn refresh_selection_polish_hotkey(&self) {}

fn refresh_coding_agent_hotkey(&self) {}
}
Expand Down Expand Up @@ -738,6 +757,7 @@ mod persist_settings_tests {
fn refresh_translation_hotkey(&self) {}
fn refresh_switch_style_hotkey(&self) {}
fn refresh_open_app_hotkey(&self) {}
fn refresh_selection_polish_hotkey(&self) {}
fn refresh_coding_agent_hotkey(&self) {}
}

Expand Down
Loading
Loading