Skip to content
Open
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
178 changes: 175 additions & 3 deletions openless-all/app/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ use crate::persistence::{CredentialAccount, CredentialsSnapshot, CredentialsVaul
use crate::polish::{LLMError, OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
use crate::recorder::{AudioConsumer, Recorder};
use crate::types::{
ChineseScriptPreference, ComboBinding, CredentialsStatus, DictationSession, DictionaryEntry,
HotkeyCapability, HotkeyStatus, OutputLanguagePreference, PolishMode, ShortcutBinding,
UpdateChannel, UserPreferences, VocabPresetStore, WindowsImeStatus,
AppModeOverride, ChineseScriptPreference, ComboBinding, CredentialsStatus, CustomMode,
DictationSession, DictionaryEntry, HotkeyCapability, HotkeyStatus, OutputLanguagePreference,
PolishMode, ShortcutBinding, UpdateChannel, UserPreferences, VocabPresetStore,
WindowsImeStatus,
};

type CoordinatorState<'a> = State<'a, Arc<Coordinator>>;
Expand Down Expand Up @@ -590,6 +591,7 @@ async fn validate_llm_provider() -> Result<(), String> {
OutputLanguagePreference::Auto,
None,
&[],
None,
)
.await
.map(|_| ())
Expand Down Expand Up @@ -926,6 +928,12 @@ pub fn set_default_polish_mode(
mode: PolishMode,
) -> Result<(), String> {
let mut prefs = coord.prefs().get();
// Custom mode の場合は id が custom_modes に存在することを検証
if let PolishMode::Custom(id) = &mode {
if !prefs.custom_modes.iter().any(|m| m.id == *id) {
return Err(format!("custom mode id '{id}' not found"));
}
}
prefs.default_mode = mode;
coord.prefs().set(prefs).map_err(|e| e.to_string())
}
Expand All @@ -937,7 +945,13 @@ pub fn set_style_enabled(
enabled: bool,
) -> Result<(), String> {
let mut prefs = coord.prefs().get();
// Custom mode を有効化する場合は id が custom_modes に存在することを検証
if enabled {
if let PolishMode::Custom(id) = &mode {
if !prefs.custom_modes.iter().any(|m| m.id == *id) {
return Err(format!("custom mode id '{id}' not found"));
}
}
if !prefs.enabled_modes.contains(&mode) {
prefs.enabled_modes.push(mode);
}
Expand Down Expand Up @@ -1152,6 +1166,164 @@ pub fn set_translation_hotkey(
Ok(())
}

/// 翻訳機能のグローバル on/off。OFF にすると hotkey が誤発火しても
/// 翻訳パイプラインと UI overlay は起動しない。設定はそのまま保持される。
#[tauri::command]
pub fn set_translate_enabled(
coord: CoordinatorState<'_>,
app: AppHandle,
enabled: bool,
) -> Result<(), String> {
let mut prefs = coord.prefs().get();
if prefs.translate_enabled == enabled {
return Ok(());
}
prefs.translate_enabled = enabled;
persist_settings(&*coord, prefs.clone())?;
let _ = app.emit("prefs:changed", &prefs);
Ok(())
}

// ─────────────────────────── ビルトインprompt表示 + カスタムスタイル CRUD ───────────────────────────
//
// `get_default_polish_prompt` はビルトイン4 mode(raw/light/structured/formal)の既定 prompt を返す。
// Settings → Style ページで「prompt を見る」を押した時の参考表示用。Custom mode は対象外。
//
// CustomMode 系コマンド:
// - `add_custom_mode(id, name, prompt)` — 末尾に追加。重複 id はエラー。
// - `update_custom_mode(id, name, prompt)` — 既存 id の name/prompt を更新。
// - `delete_custom_mode(id)` — 削除 + enabled_modes / default_mode のフォールバック。

#[tauri::command]
pub fn get_default_polish_prompt(mode: String) -> Result<String, String> {
let parsed: PolishMode = mode
.clone()
.try_into()
.map_err(|e: String| e)?;
if let PolishMode::Custom(_) = parsed {
return Err("get_default_polish_prompt is for builtin modes only".to_string());
}
Ok(crate::polish::prompts::system_prompt(parsed, None))
}

#[tauri::command]
pub fn add_custom_mode(
coord: CoordinatorState<'_>,
app: AppHandle,
id: String,
name: String,
prompt: String,
) -> Result<(), String> {
let id_trim = id.trim().to_string();
if id_trim.is_empty() {
return Err("custom mode id is empty".to_string());
}
if id_trim.contains(':') {
return Err("custom mode id cannot contain ':'".to_string());
}
let mut prefs = coord.prefs().get();
if prefs.custom_modes.iter().any(|m| m.id == id_trim) {
return Err(format!("custom mode id '{id_trim}' already exists"));
}
prefs.custom_modes.push(CustomMode {
id: id_trim,
name,
prompt,
});
persist_settings(&*coord, prefs.clone())?;
let _ = app.emit("prefs:changed", &prefs);
Ok(())
}

#[tauri::command]
pub fn update_custom_mode(
coord: CoordinatorState<'_>,
app: AppHandle,
id: String,
name: String,
prompt: String,
) -> Result<(), String> {
let mut prefs = coord.prefs().get();
let entry = prefs
.custom_modes
.iter_mut()
.find(|m| m.id == id)
.ok_or_else(|| format!("custom mode id '{id}' not found"))?;
entry.name = name;
entry.prompt = prompt;
persist_settings(&*coord, prefs.clone())?;
let _ = app.emit("prefs:changed", &prefs);
Ok(())
}

#[tauri::command]
pub fn delete_custom_mode(
coord: CoordinatorState<'_>,
app: AppHandle,
id: String,
) -> Result<(), String> {
let mut prefs = coord.prefs().get();
let before = prefs.custom_modes.len();
prefs.custom_modes.retain(|m| m.id != id);
if prefs.custom_modes.len() == before {
return Err(format!("custom mode id '{id}' not found"));
}
// default_mode が消したカスタムmodeを参照していたら Light にフォールバック
if matches!(&prefs.default_mode, PolishMode::Custom(cur_id) if *cur_id == id) {
prefs.default_mode = PolishMode::Light;
}
// enabled_modes から該当 Custom を除去
prefs.enabled_modes.retain(|m| !matches!(m, PolishMode::Custom(cur_id) if *cur_id == id));
// app_mode_overrides からも該当 Custom を参照しているルールを除去。
// 残しておくと、app パターンマッチ時に存在しない custom id を mode として
// 採用してしまい、polish パイプライン側で id 文字列をそのまま prompt 名と
// して扱う / fallback もしないという dangling 状態になる。削除と同時に
// ルール自体を消す方がユーザー期待にも近い(カスタムスタイル消したのに
// そのアプリだけ「不明な mode」が残る、という状況を避ける)。
prefs
.app_mode_overrides
.retain(|o| !matches!(&o.mode, PolishMode::Custom(cur_id) if *cur_id == id));
persist_settings(&*coord, prefs.clone())?;
let _ = app.emit("prefs:changed", &prefs);
Ok(())
}

// ─────────────────────────── アプリ別自動 mode 切替 ───────────────────────────
//
// `set_app_mode_overrides(overrides)` — 一括置換。フロントは編集中の配列丸ごとを
// 送ってくる前提。各 override の `mode` が `Custom(id)` なら `custom_modes` に
// 該当 id が存在することを検証してから保存する(dangling 参照を弾く)。
// 空 `app_pattern`(trim 後)はそのまま保存可(UI 側で「未入力行」を持ったまま
// 別行を編集するケースを許容したいため)。`pick_mode_for_app` は空パターンを
// スキップするため実害は無い。

#[tauri::command]
pub fn set_app_mode_overrides(
coord: CoordinatorState<'_>,
app: AppHandle,
overrides: Vec<AppModeOverride>,
) -> Result<(), String> {
let mut prefs = coord.prefs().get();
for ov in &overrides {
if let PolishMode::Custom(id) = &ov.mode {
if !prefs.custom_modes.iter().any(|m| m.id == *id) {
return Err(format!(
"app_mode_override references unknown custom mode id '{id}'"
));
}
}
}
prefs.app_mode_overrides = overrides;
persist_settings(&*coord, prefs.clone())?;
let _ = app.emit("prefs:changed", &prefs);
Ok(())
}

#[tauri::command]
pub fn get_app_mode_overrides(coord: CoordinatorState<'_>) -> Vec<AppModeOverride> {
coord.prefs().get().app_mode_overrides
}

#[tauri::command]
pub fn set_switch_style_hotkey(
coord: CoordinatorState<'_>,
Expand Down
70 changes: 53 additions & 17 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,16 @@ impl Coordinator {
let working_languages = prefs.working_languages;
let chinese_script_preference = prefs.chinese_script_preference;
let output_language_preference = prefs.output_language_preference;
// Custom mode のときだけ custom_modes から prompt を引いてくる。
// 既存4 mode ではハードコード prompt(system_prompt 側でハンドリング)。
let prompt_override_owned: Option<String> = match &mode {
PolishMode::Custom(id) => prefs
.custom_modes
.iter()
.find(|m| m.id == *id)
.map(|m| m.prompt.clone()),
_ => None,
};
// repolish 是历史记录里手动重新润色,不再绑定原 session 的前台 app;
// 当下用户调起的 app 才是相关上下文(如果可拿)。
let front_app = capture_frontmost_app();
Expand All @@ -859,6 +869,7 @@ impl Coordinator {
output_language_preference,
front_app.as_deref(),
&[],
prompt_override_owned.as_deref(),
)
.await
.map_err(|e| e.to_string())
Expand Down Expand Up @@ -1346,16 +1357,9 @@ fn handle_action_hotkey_pressed(inner: &Arc<Inner>, kind: ActionHotkeyKind) {

fn switch_to_previous_style(inner: &Arc<Inner>) {
let mut prefs = inner.prefs.get();
let order = [
PolishMode::Raw,
PolishMode::Light,
PolishMode::Structured,
PolishMode::Formal,
];
let enabled: Vec<PolishMode> = order
.into_iter()
.filter(|mode| prefs.enabled_modes.contains(mode))
.collect();
// 既存4 mode + Custom mode を含む、enabled_modes の順番をそのまま使う。
// Custom mode は `custom_modes` の登録順に enabled_modes へ並ぶ前提。
let enabled: Vec<PolishMode> = prefs.enabled_modes.clone();
if enabled.len() <= 1 {
log::info!("[coord] switch style hotkey ignored: enabled style count <= 1");
return;
Expand All @@ -1369,14 +1373,14 @@ fn switch_to_previous_style(inner: &Arc<Inner>) {
} else {
current_index - 1
};
prefs.default_mode = enabled[next_index];
prefs.default_mode = enabled[next_index].clone();
let next_label = prefs
.default_mode
.display_name_with_customs(&prefs.custom_modes);
if let Err(e) = inner.prefs.set(prefs.clone()) {
log::warn!("[coord] switch style hotkey 保存失败: {e}");
} else {
log::info!(
"[coord] switch style hotkey changed default mode to {}",
prefs.default_mode.display_name()
);
log::info!("[coord] switch style hotkey changed default mode to {next_label}");
}
}

Expand Down Expand Up @@ -2701,12 +2705,27 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
emit_capsule(inner, CapsuleState::Polishing, 0.0, elapsed, None, None);

let prefs = inner.prefs.get();
let mode = prefs.default_mode;
let hotword_strs = enabled_phrases(inner);
let working_languages = prefs.working_languages.clone();
let chinese_script_preference = prefs.chinese_script_preference;
let output_language_preference = prefs.output_language_preference;
let front_app = inner.state.lock().front_app.clone();
// アプリ別自動 mode 切替:app_mode_overrides を順次評価して最初にマッチした mode を採用。
// どのルールにもマッチしないなら default_mode(既存挙動)。
let mode = match crate::types::pick_mode_for_app(
front_app.as_deref(),
&prefs.app_mode_overrides,
) {
Some(m) => {
log::info!(
"[coord] app override matched: front_app={:?} → mode={}",
front_app,
m.as_serde_string()
);
m
}
None => prefs.default_mode.clone(),
};
let translation_target = prefs.translation_target_language.trim().to_string();
let translation_active =
inner.translation_modifier_seen.load(Ordering::SeqCst) && !translation_target.is_empty();
Expand Down Expand Up @@ -2754,15 +2773,26 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
)
.await
} else {
// Custom mode のときだけ custom_modes から prompt 本文を引いてくる。
// 既存4 mode ではハードコード prompt を使うため None を渡す。
let prompt_override_owned: Option<String> = match &mode {
PolishMode::Custom(id) => prefs
.custom_modes
.iter()
.find(|m| m.id == *id)
.map(|m| m.prompt.clone()),
_ => None,
};
polish_or_passthrough(
&raw,
mode,
mode.clone(),
&hotword_strs,
&working_languages,
chinese_script_preference,
output_language_preference,
front_app.as_deref(),
&prior_turns,
prompt_override_owned.as_deref(),
)
.await
};
Expand Down Expand Up @@ -3355,6 +3385,7 @@ fn ensure_qa_volcengine_credentials() -> Result<(), String> {

/// 润色文本;失败时返回原文 + 失败原因,调用方据此弹错误胶囊 + 写历史 error_code。
/// 之前固定返回 String,调用方拿不到失败信号 → 用户感知"为什么风格设置没生效"。issue #57。
#[allow(clippy::too_many_arguments)]
async fn polish_or_passthrough(
raw: &RawTranscript,
mode: PolishMode,
Expand All @@ -3364,6 +3395,7 @@ async fn polish_or_passthrough(
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
prior_turns: &[(String, String)],
prompt_override: Option<&str>,
) -> (String, Option<String>) {
if mode == PolishMode::Raw {
return (raw.text.clone(), None);
Expand All @@ -3377,6 +3409,7 @@ async fn polish_or_passthrough(
output_language_preference,
front_app,
prior_turns,
prompt_override,
)
.await
{
Expand All @@ -3389,6 +3422,7 @@ async fn polish_or_passthrough(
}
}

#[allow(clippy::too_many_arguments)]
async fn polish_text(
raw: &str,
mode: PolishMode,
Expand All @@ -3398,6 +3432,7 @@ async fn polish_text(
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
prior_turns: &[(String, String)],
prompt_override: Option<&str>,
) -> anyhow::Result<String> {
let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
let model = CredentialsVault::get(CredentialAccount::ArkModelId)?
Expand All @@ -3421,6 +3456,7 @@ async fn polish_text(
output_language_preference,
front_app,
prior_turns,
prompt_override,
)
.await?)
}
Expand Down
7 changes: 7 additions & 0 deletions openless-all/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,13 @@ pub fn run() {
commands::validate_shortcut_binding,
commands::set_dictation_hotkey,
commands::set_translation_hotkey,
commands::set_translate_enabled,
commands::get_default_polish_prompt,
commands::add_custom_mode,
commands::update_custom_mode,
commands::delete_custom_mode,
commands::set_app_mode_overrides,
commands::get_app_mode_overrides,
commands::set_switch_style_hotkey,
commands::set_open_app_hotkey,
commands::qa_window_dismiss,
Expand Down
Loading
Loading