Skip to content

feat(style): per-app auto polish-mode override#367

Closed
katanumahotori wants to merge 2 commits intoOpen-Less:betafrom
katanumahotori:feat/per-app-mode-override
Closed

feat(style): per-app auto polish-mode override#367
katanumahotori wants to merge 2 commits intoOpen-Less:betafrom
katanumahotori:feat/per-app-mode-override

Conversation

@katanumahotori
Copy link
Copy Markdown
Contributor

@katanumahotori katanumahotori commented May 8, 2026

User description

Why

When dictating into different apps, users typically want different polish styles (formal for email, raw for code, casual for chat). Today the only mechanism is to manually cycle Ctrl+Shift+S each time. coordinator.rs already captures the foreground app name ('front_app captured: ...'), so we just need to wire that into mode selection.

What

  • AppModeOverride { app_pattern: String, mode: PolishMode } and UserPreferences.app_mode_overrides: Vec<...>. Order is preserved so the user can prioritize.
  • pick_mode_for_app(app, &overrides) - case-insensitive substring match, first hit wins, empty patterns are skipped (so an in-progress edit doesn't apply globally). 9 unit tests.
  • coordinator.rs: just before kicking off polish, override the effective mode if any rule matches the foreground app, otherwise fall back to default_mode. A [coord] app override matched log line is emitted for diagnosis.
  • set_app_mode_overrides IPC command does dangling-id validation for Custom(...) references.
  • Style page: a new 'Per-app override' section under Custom styles with add / edit / delete rows; each row picks among built-ins + the user's custom modes. Saves are debounced 500ms.

Empty overrides Vec means existing behavior is unchanged.

Note

types.rs / commands.rs / coordinator.rs / Style.tsx overlap heavily with #366 (custom polish styles). The overlap is intentional - both features sit on the same UserPreferences schema and share the Style page. If #366 lands first, this PR will rebase to a much smaller diff (just the AppModeOverride struct + pick_mode_for_app + the override section UI).

Depends on #366 (custom modes) for PolishMode::Custom and the custom-mode picker dropdown in the override row.


PR Type

Enhancement, Documentation, Tests


Description

  • Add custom style CRUD and lookup

  • Resolve per-app overrides before polishing

  • Thread prompt overrides through LLM pipeline

  • Expand i18n, IPC, and tests


Diagram Walkthrough

flowchart LR
  A["UserPreferences schema"] -- "custom modes / app overrides / translate flag" --> B["Coordinator and prompt selection"]
  B -- "IPC commands" --> C["commands.rs / ipc.ts"]
  C -- "style settings UI" --> D["Style.tsx"]
  D -- "localized labels" --> E["i18n files"]
Loading

File Walkthrough

Relevant files
Enhancement
6 files
types.rs
Add custom modes and override types                                           
+226/-8 
polish.rs
Allow prompt overrides in system prompts                                 
+72/-9   
coordinator.rs
Apply app overrides during session polishing                         
+53/-17 
commands.rs
Add CRUD commands and validation                                                 
+166/-3 
Style.tsx
Build custom styles and override UI                                           
+670/-74
types.ts
Mirror custom mode and override types                                       
+39/-2   
Configuration changes
2 files
lib.rs
Register new Tauri commands                                                           
+7/-0     
ipc.ts
Expose new IPC helpers to frontend                                             
+37/-0   
Documentation
5 files
ja.ts
Add Japanese labels for new settings                                         
+54/-2   
ko.ts
Add Korean labels for new settings                                             
+52/-0   
zh-TW.ts
Add Traditional Chinese labels                                                     
+52/-0   
zh-CN.ts
Add Simplified Chinese labels                                                       
+52/-0   
en.ts
Add English labels for new settings                                           
+52/-0   
Tests
1 files
stylePrefs.test.ts
Update mock preferences for new fields                                     
+3/-0     

When dictating into different apps, users typically want different
polish styles (formal for email, raw for code, casual for chat). Today
the only mechanism is to manually cycle Ctrl+Shift+S each time.
coordinator.rs already captures the foreground app name
("front_app captured: ..."), so we just need to wire that into mode
selection.

Adds:

- AppModeOverride { app_pattern: String, mode: PolishMode } and
  UserPreferences.app_mode_overrides: Vec<...>. Order is preserved so
  the user can prioritize.
- pick_mode_for_app(app, &overrides) - case-insensitive substring
  match, first hit wins, empty patterns are skipped (so an in-progress
  edit doesn't apply globally). 9 unit tests.
- coordinator.rs: just before kicking off polish, override the
  effective mode if any rule matches the foreground app, otherwise
  fall back to default_mode. A "[coord] app override matched" log line
  is emitted for diagnosis.
- set_app_mode_overrides IPC command does dangling-id validation for
  Custom(...) references.
- Style page: a new "Per-app override" section under Custom styles
  with add / edit / delete rows; each row picks among built-ins + the
  user's custom modes. Saves are debounced 500ms.

Empty overrides Vec means existing behavior is unchanged.

Note: this PR's types.rs / commands.rs / coordinator.rs / Style.tsx
overlap heavily with the previous PR (custom polish styles). The
overlap is intentional - both features sit on the same UserPreferences
schema and share the Style page. If Open-Less#366 lands first, this PR will
rebase to a much smaller diff (just the AppModeOverride struct +
pick_mode_for_app + the override section UI).
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

katanumahotori pushed a commit to katanumahotori/openless that referenced this pull request May 8, 2026
…Mode::Custom

Without these the PolishMode::Custom variant added to types.rs in this
PR makes the existing match arms in polish.rs / coordinator.rs
non-exhaustive and the crate fails to compile. Same scope-leakage
caveat from the previous commit applies: these will be the canonical
implementations once Open-Less#366 / Open-Less#367 land.
Without this the PolishMode::Custom variant added in this PR's
types.rs makes the existing match arms in polish.rs non-exhaustive.
Same scope-leakage caveat as the rest of this PR.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

PR Reviewer Guide 🔍

(Review updated until commit 8abeaff)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

366 - Partially compliant

Compliant requirements:

  • Custom styles are stored in preferences and can be created, edited, and deleted.
  • Custom styles can be selected as the default mode and enabled/disabled like built-ins.
  • Per-app override rules are persisted, ordered, and resolved with first-match substring matching.
  • IPC commands and Style page controls were added for override management.
  • The translation enable flag is added to preferences with an IPC setter.
  • New i18n strings were added for the updated style and translation UI.

Non-compliant requirements:

  • None found from code review.

Requires further human verification:

  • Foreground-app matching should be exercised on each supported OS to confirm process names are captured as expected.
  • The debounced override persistence and custom-style editing flow should be checked interactively in the UI.
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Dangling override

Deleting a custom style only removes it from custom_modes, enabled_modes, and default_mode. Any existing app_mode_overrides entries that still point at that deleted custom id are left behind, so the next time one of those rules matches the foreground app the coordinator will resolve a custom mode with no prompt text and send an empty system prompt.

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));
persist_settings(&*coord, prefs.clone())?;

@appergb
Copy link
Copy Markdown
Collaborator

appergb commented May 8, 2026

@claude 它具体的实现方式是怎样的?实现是否在全平台可以正常通过?

@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

Claude finished @appergb's task in 1m 40s —— View job


分析 PR #367 实现方式及跨平台兼容性

  • 读取核心 Rust 文件(types.rs, coordinator.rs, polish.rs, commands.rs)
  • 读取前端文件(Style.tsx, ipc.ts, types.ts)
  • 分析跨平台兼容性
  • 整理实现细节并输出分析报告

具体实现方式

整个功能由四层串联实现,以下是每层的核心逻辑:

1. 数据模型(types.rs

新增两个结构体并扩展了 UserPreferences

// 用户自定义整形风格(prefs.custom_modes)
pub struct CustomMode { pub id: String, pub name: String, pub prompt: String }

// 应用级自动切换规则(prefs.app_mode_overrides,顺序即优先级)
pub struct AppModeOverride { pub app_pattern: String, pub mode: PolishMode }

核心匹配函数 pick_mode_for_apptypes.rs:117-136):

  • app 字符串和 app_pattern 都做 to_lowercase(),然后用 contains 做子字符串匹配
  • 顺序优先:第一个命中的规则获胜
  • 空 pattern 跳过(避免误匹配所有 app)
  • 共有 9 个单元测试 覆盖各种边界

2. 会话执行层(coordinator.rs

触发时机:在会话开始时(hotkey edge)调用 capture_frontmost_app() 记录当前前台 app(coordinator.rs:1975);在 polish 开始前(coordinator.rs:2712-2728)做 override 查找:

let front_app = inner.state.lock().front_app.clone();
let mode = match pick_mode_for_app(front_app.as_deref(), &prefs.app_mode_overrides) {
    Some(m) => { log::info!("[coord] app override matched: ..."); m }
    None => prefs.default_mode.clone(),  // 无匹配 → 沿用默认
};

若命中 Custom(id) 模式,则从 custom_modes 里查出 prompt 文本,作为 prompt_override 传入 LLM(coordinator.rs:2778-2785)。

3. LLM 层(polish.rs

compose_system_prompt(mode, hotwords, override_text) 新增了 override_text 参数(polish.rs:564-587):若非空则直接返回,完全跳过内置 prompt——这就是自定义风格如何接管的机制。

4. 前端(Style.tsx

  • 自定义风格:新增 Add/Edit/Delete 表单,id 通过正则 ^[A-Za-z0-9-]+$ 校验
  • Per-app 规则:每行有 app pattern 输入框 + mode 下拉选择器(含内置4种 + 所有自定义)
  • 所有 override 编辑走 500ms debounce,之后调用 setAppModeOverrides(next)Style.tsx:85-100

跨平台兼容性分析

平台 capture_frontmost_app() 实现 返回内容 规则匹配对象
macOS NSWorkspace.frontmostApplication via objc2 "Slack (com.tinyspeck.slackmacgap)" App 名称 + Bundle ID
Windows GetForegroundWindow + GetWindowTextW "Inbox - Gmail — Google Chrome" 窗口标题(随 tab/文档变化)
Linux 直接返回 None 永远不触发任何 override

macOS ✅ 正常工作

NSWorkspace.frontmostApplication 返回稳定的 app 名称 + Bundle ID,如 "Google Chrome (com.google.Chrome)"。用户写 "chrome" 就能可靠命中 Chrome,与当前打开的 tab 无关。

Windows ⚠️ 行为与文档描述不符

GetWindowTextW 返回的是当前窗口标题,例如:

  • Chrome 打开 Gmail → "Inbox - Gmail — Google Chrome"
  • VS Code 编辑 foo.ts → "foo.ts - Visual Studio Code"
  • 空白新 tab → "New Tab - Google Chrome"

这与 PR 描述的"进程名 substring 匹配"存在偏差:写 "chrome" 在大多数 Chrome 标题里能命中,但写 "visual studio code" 只在标题末尾固定时有效。PR 中没有文档说明这一区别,用户可能对"为什么有时不生效"感到困惑。

建议:Windows 下应优先使用 GetWindowThreadProcessId + QueryFullProcessImageNameW 获取进程路径/名称,或至少在 UI 里说明 Windows 匹配的是窗口标题。

Linux ❌ 功能完全静默禁用

capture_frontmost_app() 在 non-macOS/non-Windows 平台直接返回 Nonecoordinator.rs:4739-4741)。所有 override 规则永远不匹配。PR 描述和 UI 均未提及此限制。


已知 Bug(与 PR Reviewer 指出问题一致)

删除自定义 mode 后 override 悬空commands.rs:1260-1279):

delete_custom_mode 只清理了 default_modeenabled_modes,但没有清理 app_mode_overrides 里引用该 id 的规则。下次 dictation 时:

  1. pick_mode_for_app 命中悬空规则,返回 Custom("deleted-id")
  2. prefs.custom_modes.iter().find(|m| m.id == "deleted-id") 返回 None
  3. prompt_override_owned = None,LLM 收到空 system prompt,polish 静默退化

set_app_mode_overrides 会校验 Custom id 存在性(防止新建悬空),但删除路径没有对称地清理,逻辑不一致。

Fix this →


总结

方面 状态
macOS 功能完整性 ✅ 核心逻辑正确,测试覆盖充分
Windows 基本可用 ⚠️ 匹配的是窗口标题而非进程名,与文档不符
Linux ❌ 功能静默禁用,无任何提示
删除 Custom mode 后悬空 override ❌ 导致空 system prompt,静默降级
IPC 命令一致性 ⚠️ set_app_mode_overrides 做了 id 校验,delete_custom_mode 没有对称清理

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Persistent review updated to latest commit 8abeaff

@H-Chris233 H-Chris233 closed this May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants