Commit Graph
671 Commits
Author SHA1 Message Date
baiqing eaca99bdf4 fix(insertion): make Windows/Linux paste shortcut configurable (#360)
模拟粘贴硬编码 Ctrl+V,kitty / alacritty / wezterm / gnome-terminal /
foot 等 Linux 终端只接受 Ctrl+Shift+V,听写文本只剩在剪贴板里,看起来
像"什么也没插入"。issue #360 报告了 kitty 这个具体场景。

新增 `PasteShortcut` 枚举(`CtrlV` / `CtrlShiftV` / `ShiftInsert`)作为
UserPreferences 字段,串到 simulate_paste 的 enigo 路径。Settings →
录音 多了"模拟粘贴快捷键"一行,仅在非 macOS 显示(macOS 走 AX 直写
不受影响)。默认 `CtrlV` 与历史行为一致,老配置文件没有 pasteShortcut
字段时 `#[serde(default)]` 回退到默认值,不破坏既有用户。

涵盖:
- types.rs:新枚举 + 字段 + UserPreferencesWire 串接
- insertion.rs:simulate_paste(shortcut),paste_keys 把枚举拆成
  (modifiers, primary),按下/释放严格对称,中途出错也反向释放避免卡键
- coordinator.rs:从 prefs 读取,串到三个 insert 调用点 + Windows IME 链
- 前端 TS 类型 / Settings 选择器 / 5 种语言 i18n
- 单元测试:默认 CtrlV、JSON 反序列化、paste_keys 三种映射

cargo test --lib 全部 181 通过;npm run build 干净。
2026-05-09 13:15:33 +08:00
Chris233 15442b5aad Merge pull request #374 from H-Chris233/fix/backend-rust-unit-tests
让 Windows CI 实际运行 Rust-only 后端单测
2026-05-09 12:42:29 +08:00
Chris233 9db6de34dc Merge pull request #375 from katanumahotori/feat/whisper-prompt-vocabulary
feat(asr): forward dictionary entries to Whisper prompt
2026-05-09 12:06:15 +08:00
H-Chris233 c63c69fb2f Lock recorder resampling and RMS coverage
The backend-only test PR needs to cover the concrete recorder paths named in the ticket, not just empty input handling. This adds focused Rust unit coverage for non-target sample-rate resampling, emitted PCM shape, scaled RMS levels, and peak tracking while keeping the production recorder unchanged.\n\nConstraint: Scope is Rust-only backend tests; do not launch or link extra Tauri runtime paths for Windows CI.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml recorder:: -- --nocapture\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 11:40:39 +08:00
H-Chris233 044799e705 Cover backend edge tests in Rust-only harness
The Rust-only backend harness already ran existing hotkey, recorder, and insertion unit tests, but the review could not see target edge coverage directly in this diff. Add focused edge tests for modifier trigger filtering/re-emission, recorder callback no-op inputs, and insertion fallback status so the coverage is explicit in the changed files.\n\nConstraint: Keep scope to backend Rust unit tests and avoid app/runtime integration.\nRejected: Add UI or Tauri runtime tests | conflicts with the Rust-only backend boundary.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nTested: git diff --check\nNot-tested: GitHub Windows runner result after push.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 10:07:38 +08:00
lightnovel0 403985208d feat(asr): forward dictionary entries to Whisper prompt parameter
The Vocabulary screen advertises that entries are passed to "both ASR
hotword and post-processing model context", and that contract holds for
Volcengine ASR (which receives them as hotwords) and the Polish LLM
(which receives them as a system-prompt block). Whisper-compatible
providers (whisper / siliconflow / zhipu / groq) were the gap: their
client never set the OpenAI `prompt` field, so the dictionary had no
effect on the STT stage at all.

That gap shows up as inconsistent transliteration of registered proper
nouns. A user who registers "梁山泊" still gets "両山泊" / "良山泊" /
"りょうざんぱく" from Whisper, and the Polish LLM cannot always recover
once the script choice has already drifted.

This change:

- Adds an optional `prompt: Option<String>` to `WhisperBatchASR` and
  attaches it as a `prompt` multipart field when non-blank. Empty/blank
  prompts are skipped defensively so they cannot trigger
  provider-specific 4xx responses.
- Adds `build_prompt_from_phrases`, a pure helper that joins enabled
  vocabulary phrases with `, ` and a trailing period, capped at 240
  characters (Whisper's published limit is 244 tokens; CJK is worst-case
  ~1 token/char, so 240 chars is the safe cap). Over-budget entries are
  skipped individually rather than truncating mid-list, which preserves
  registration order and lets short later entries still ride along when
  one earlier entry is unusually long.
- Wires `enabled_phrases(inner)` through the helper at the existing
  Whisper-compatible branch in `coordinator.rs`, mirroring how the
  Volcengine branch already builds its hotword list.

Existing call sites unchanged in behavior when the dictionary is empty
(`None` prompt → no `prompt` field on the request). No new dependencies.

Tests in `asr::whisper::tests` cover: empty / all-blank input returns
None; single and multi phrase joining; per-entry trimming; mid-list
blanks are skipped without breaking the join; an over-long single entry
is dropped while shorter entries that follow it are kept; the resulting
string never exceeds the budget; truncation prefers earlier entries.
2026-05-09 10:00:18 +09:00
H-Chris233 02ccac2dd5 Make Rust-only backend test target explicit
The backend-tests crate is intentionally an integration-test harness, but declaring the target explicitly makes Cargo discovery and review intent unambiguous. The harness comment also records that path-included modules run their own cfg(test) unit tests for hotkey, recorder, and insertion without linking the full Tauri library.\n\nConstraint: Stay within Rust-only backend tests; do not add a Tauri lib/runtime target.\nRejected: Add src/lib.rs just to satisfy target discovery | would introduce an unused crate surface instead of the actual test harness.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml -- --list | grep -E '^(hotkey|recorder|insertion)::'\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: git diff --check\nNot-tested: GitHub Windows runner rerun result pending after push.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 08:30:23 +08:00
H-Chris233 f339209394 Align shortcut tests with platform modifier semantics
Windows intentionally normalizes cmd/command shortcut modifiers to ctrl, while macOS and Linux keep the super bit. The Rust-only backend test crate now executes these source tests on Windows, so the assertion must follow the same platform contract as production parsing instead of assuming the non-Windows result.\n\nConstraint: Keep the fix inside Rust backend tests and avoid changing shortcut behavior.\nRejected: Change normalize_modifier_tag to keep cmd as super on Windows | would alter user-facing hotkey semantics.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nNot-tested: GitHub Windows runner rerun result pending after push.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 08:00:21 +08:00
H-Chris233andOmX 029232a269 Make backend state tests runnable without Tauri
Windows cannot reliably execute the full Tauri lib test harness in CI because the process exits before Rust tests start in the desktop runtime loader path. Split the coordinator session state transitions into a Tauri-free Rust module used by coordinator.rs, and add a small Rust-only backend test crate that path-includes coordinator/hotkey/recorder/insertion logic for real Windows execution.

Constraint: User scoped this follow-up to backend-only Rust-to-Rust tests, not app/runtime smoke tests.
Constraint: Existing Windows CI still keeps full lib --no-run cfg/link coverage.
Rejected: Keep trying to execute cargo test --lib on Windows | it starts the full Tauri-linked test binary and fails before unit tests run.
Rejected: Rework release/runtime DLL handling | outside the backend unit-test scope.
Confidence: high
Scope-risk: moderate
Directive: Keep Windows actual unit execution on the Rust-only backend-tests manifest unless the full Tauri lib harness is proven runnable on clean Windows CI.
Tested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml
Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib
Tested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml
Tested: python yaml parse for .github/workflows/ci.yml; git diff --check
Not-tested: Windows runner execution; intended to be verified by GitHub Actions.
Related: #295
Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 07:44:57 +08:00
Chris233 1aa4f9b494 Merge pull request #371 from millionart/codex/windows-tray-style-sync
fix: sync Windows tray style selection
2026-05-08 21:14:10 +08:00
millionart 19a87a4410 fix: sync Windows tray style selection 2026-05-08 20:30:23 +08:00
Chris233 7d77044b97 Merge pull request #361 from H-Chris233/fix/issue-295-rust-unit-tests
为 Rust 后端补充单元测试门禁
2026-05-08 20:19:58 +08:00
TRIPandbaiqing b44cb998af fix(installer): unregister TSF IME before replacing OpenLessIme.dll on upgrade (#321) (#369)
Auto-update was failing on Windows because OpenLessIme.dll is a TSF input-method
DLL: once registered, Windows maps it into every text-input client process
(browsers, IDEs, chat apps...). The installer's PREINSTALL hook tried to
overwrite the file directly while it was still mapped, hitting a sharing
violation and aborting the update.

NSIS hook (openless-ime-hooks.nsh):
- PREINSTALL now calls OPENLESS_IME_UNREGISTER_X86/X64 first to release the
  COM/TSF registration. On a fresh install this is a harmless no-op (existing
  unregister macros log the exit code without aborting).
- DLL bytes are written to OpenLessIme.dll.new and atomically renamed into
  place. If the old DLL is still locked by a TSF client process, Delete fails
  and we fall back to Rename /REBOOTOK + SetRebootFlag — Windows queues the
  replacement via PendingFileRenameOperations and the new bytes activate after
  the next reboot. Same TSF registration path is preserved, so re-registration
  in POSTINSTALL stays valid across the reboot.

WiX (openless-ime.wxs):
- UnregisterOpenLessImeX86/X64 now also fire Before InstallFiles when a
  previous version is installed (Installed AND NOT REMOVE). Original condition
  (REMOVE="ALL", Before RemoveFiles) only covered the dedicated uninstall
  sequence; auto-update / minor-upgrade skipped it entirely and ran straight
  into the same sharing violation as NSIS.

Bumps to 1.2.24-5 (Beta-5).

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 19:58:05 +08:00
H-Chris233 ffeaa243d9 Keep backend test CI executable on Windows
The #295 unit suite now runs in CI, but the Windows clean runner exits before the lib test harness starts with STATUS_ENTRYPOINT_NOT_FOUND when optional native runtime entrypoints are absent. Windows still needs cfg/link coverage, while macOS and Linux can execute the shared Rust unit suite.

Constraint: Preserve the new Rust test gate without making optional Windows native runtime installation a CI prerequisite.
Rejected: Install Foundry/Windows App Runtime into every CI run | broader, slower, and unrelated to backend unit-test coverage.
Confidence: high
Scope-risk: narrow
Directive: Do not switch Windows back to executing the lib test binary until the optional native runtime dependency is present on the runner.
Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib
Not-tested: Windows CI rerun before push.
2026-05-08 19:15:54 +08:00
H-Chris233 b082805158 Close backend test blind spots from review
The first #295 pass made cargo tests visible but still left the reviewer-requested state-machine, modifier-edge, and insertion fallback contracts too implicit. This adds narrow unit coverage around those contracts without refactoring runtime flow.\n\nConstraint: Address review compliance with minimal production changes.\nRejected: Split coordinator or hotkey adapters for testability | broader than the issue and riskier for the active PR.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib; cargo check --manifest-path src-tauri/Cargo.toml; npm run build\nNot-tested: Live macOS/Windows native hotkey hooks and clipboard/AX behavior.
2026-05-08 15:35:34 +08:00
H-Chris233 88e8418982 Make backend regressions visible in CI
Issue #295 calls out that core Rust paths have been too dependent on manual app runs. Keep the change narrow by covering existing pure state/audio/clipboard helpers instead of refactoring coordinator or touching hardware-facing code, then wire the existing lib test suite into the lightweight CI lane.

Constraint: Global hotkey, recorder, and insertion integration still require real OS permissions and devices.

Rejected: Split coordinator before adding tests | broader refactor would raise review risk and is tracked separately.

Confidence: high

Scope-risk: narrow

Directive: Keep Rust CI on --lib unless integration tests get explicit OS fixtures.

Tested: npm run build

Tested: cargo test --manifest-path src-tauri/Cargo.toml --lib

Tested: cargo check --manifest-path src-tauri/Cargo.toml

Not-tested: Live microphone, global hotkey, clipboard, and insertion device paths.

Related: #295
2026-05-08 15:18:40 +08:00
Chris233 d51f6ee59e Merge pull request #359 from H-Chris233/fix/issue-226-tauri-csp
security: 为 Tauri WebView 启用最小 CSP
2026-05-08 15:01:06 +08:00
H-Chris233 717e694370 Constrain WebView loading with a minimal CSP
The desktop WebView no longer runs with an unexplained disabled CSP. The policy keeps scripts self-hosted, permits Tauri IPC and local Vite development connections, keeps provider/update/model networking on the Rust side, and documents the only unsafe allowance for React inline styles used by the existing pixel-aligned UI.

Constraint: Issue #226 requires csp:null to be replaced or explicitly bounded

Constraint: Provider validation, updater checks, and model downloads are Rust-side paths, not frontend fetch allowlist entries

Rejected: Keep csp:null with documentation only | weaker defense-in-depth and leaves the WebView effectively unconstrained

Confidence: high

Scope-risk: narrow

Directive: Do not add external provider domains to WebView connect-src unless a frontend fetch path is introduced

Tested: npm run build

Tested: npm run tauri -- info

Tested: cargo check --manifest-path src-tauri/Cargo.toml
2026-05-08 12:00:09 +08:00
TRIPandbaiqing 29d3a79eb6 chore: bump version to 1.2.24-4 (Beta-4) (#358)
Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 11:57:36 +08:00
TRIPandbaiqing aee0d82a59 feat(startup): add "start minimized" toggle (Windows background autostart) (#357)
回应用户「Windows 端开机启动时不弹软件界面、自动后台启动」需求。

新 pref start_minimized: bool,默认 false 不破坏现有行为。打开后所有
启动路径都不弹主窗口(包括开机自启 + 手动启动),App 仅在菜单栏 / 托盘
运行;用户从托盘进入主窗口。Windows 开机自启场景用得多——本来想要
后台 + 托盘,不想被主窗口打扰。

设计选择:不区分"自启动 vs 手动启动"——开关一开则所有启动统一静默。
理由:避免 argv parsing + 重新注册 autostart 带来的复杂性(tauri-plugin-
autostart 的 args 是 init() 时静态设的、动态切需要重注册)。开关明确
打开 = 接受所有启动都从托盘进。

实现:
- types.rs: UserPreferences 加 start_minimized 字段(含 UserPreferencesWire +
  Default + Deserialize 全套兼容)
- lib.rs setup hook: 读 coordinator.prefs().get().start_minimized;为 true
  时跳过 main.show()。OPENLESS_SHOW_MAIN_ON_START=1 仍保留老的强制 show
  路径(dev 用),优先级高于 prefs
- 前端 types.ts / ipc.ts mockSettings / stylePrefs.test.ts: 加默认 false
- Settings.tsx Recording 区在 AutostartRow 下加 Toggle
- i18n 5 locale (zh-CN / zh-TW / en / ja / ko) 加 startMinimizedLabel +
  startMinimizedDesc 两个 key

顺手修预存 bug(跟 PR #355 副 commit 同源——本 branch 没含那个修复):
HotkeyBinding default keys=None,避免旧 prefs 反序列化时 trigger / keys
不一致。types::tests 8/8 pass。

不在本 PR:Windows IME 切换还原偶发不还原 bug——需要 Windows 日志
+ 复现条件才能定位,盲改可能引入新 bug。已请求用户提供
%LOCALAPPDATA%\OpenLess\Logs\openless.log + 系统版本 + IME 信息。

cargo test --lib: 135/135 pass。npm run build: tsc + vite 干净。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 11:49:13 +08:00
TRIPandbaiqing 375456f834 fix(icons): pad bundle icon to Apple HIG ~10% margin (#356)
* feat(history+polish): time-based retention + conversation-aware polish

回应用户「历史记录功能 + 对话上下文 + 提示词调优」需求。

新增 2 个用户偏好(types.rs + UserPreferencesWire + Default + Deserialize 全套兼容):
- history_retention_days: u32(默认 7;0=不按时间清理仅受 200 上限)
- polish_context_window_minutes: u32(默认 5;0=关闭走原有单轮 polish)

后端:
- persistence.rs:
  - HistoryStore::append_with_retention(session, retention_days):写入新条目时
    顺手把超过 N 天的会话裁掉,不需要后台轮询
  - HistoryStore::recent_within_minutes(minutes):返回最近 N 分钟内的会话(newest-
    first,take_while 利用 newest-first 顺序提前终止)
  - 老 append() 保留,等价 append_with_retention(s, 0)
- polish.rs:
  - polish() 多一个 prior_turns 参数;不为空时走 chat_completion_with_polish_history
    构造 system + (user/assistant)... + 当前 user 多轮 chat completion
  - 把 chat_completion 的 send/parse 抽成 send_chat_request 复用
  - prompts::polish_context_instruction():明确告诉 LLM 历史 turns 仅作语义上下文,
    不要复读已经被插入到用户文档里的 assistant 输出
- coordinator.rs:
  - 主流水线在 Polishing 阶段拉 recent_within_minutes(prefs.window) 作为 prior_turns,
    过滤掉失败的 session(error_code/empty final_text)避免喂噪声
  - 翻译路径 / Raw 模式 / repolish 主动跳过上下文(语义不合适)
  - 3 个 history.append 改成 append_with_retention(session, prefs.history_retention_days)
- commands.rs / coordinator.rs:repolish / polish.rs::tests / commands.rs::testProvider:
  原 polish() 调用全部加 &[] 空 prior_turns 参数

前端:
- types.ts:UserPreferences 加 historyRetentionDays / polishContextWindowMinutes
- ipc.ts mockSettings + stylePrefs.test.ts mockPrefs:补默认值
- Settings.tsx Recording 区加 2 个 number input(裸 input + clamp,retention 0-365 天,
  context window 0-60 分钟)
- i18n 5 locale(zh-CN / zh-TW / en / ja / ko)补 4 个 key:
  historyRetentionLabel / Desc + polishContextWindowLabel / Desc

性能注意:retention 在 append 时 O(n) 一次扫描裁切;recent_within_minutes 使用
take_while 利用 newest-first 顺序提前终止,平均 O(window 内条目数) 不是 O(总历史)。

cargo check + commands::tests (30/30) + polish::tests (10/10) + npm run build 全 pass。
预先存在的 types::tests::legacy_hotkey_trigger 失败跟本 PR 无关(macOS 平台特有)。

不包含:连接 / 厂商 / 提示词排布优化(用户也提了)——那些需要先 measure 再动,
不能盲改,留给独立 PR。

* fix(types): HotkeyBinding default keys = None (legacy trigger / keys mismatch)

跑测试时发现 types::tests::legacy_hotkey_trigger_still_produces_effective_key_codes
fail,原因跟本 PR 无关,但用户要求中途发现 bug 顺手修。

Bug:HotkeyBinding 用 `#[serde(default)]` 结构级 default。反序列化时整个 struct
先按 Default 填充,JSON 字段覆盖。如果 Default 里 keys 预填具体 codes(macOS
Default ["AltRight"] / Windows ["ControlRight"]),那么旧 prefs 写
`{"trigger":"rightControl","mode":"toggle"}`(无 keys)反序列化后会变成:
  trigger = RightControl  ← JSON 覆盖
  keys = Some([HotkeyKey { code: "AltRight" }])  ← Default 残留,跟 trigger 不一致

effective_codes() 在 keys 是 Some 时直接信任 keys、不再 fallback 到
legacy_trigger_code(trigger),于是用户选的 trigger 跟实际生效的 keys 对不上。

修法:Default::keys = None。这样 keys 缺省时 effective_codes() 走 legacy_trigger_code
路径,跟 trigger 自动同步;JSON 显式写 keys 时仍可以覆盖。

types::tests 8/8 pass,全 lib 测试 135/135 pass。

* test(polish): lock down "no echo prior context" with 4 explicit unit tests

用户的核心顾虑:上下文感知 polish 不能让 LLM 把上文吐出来。PR #355 设计了
两层防御(chat structure + system prompt 显式指令),但**没写过 explicit 测试**
盯死这条不变量——回归就听天由命。这次把它锁住。

把 chat_completion_with_polish_history 里 inline 的 messages 构造抽成纯函数
build_polish_history_messages(system_prompt, prior_turns, user_prompt) → Vec<Value>,
方便可测,行为零变化。

新增 4 个 polish::tests:

1. build_polish_history_messages_empty_prior_falls_back_to_two_messages
   prior 空时只剩 [system, user],跟单轮 chat_completion 等价

2. build_polish_history_messages_orders_prior_oldest_to_newest_then_current
   入参 prior_turns 是 newest-first(match HistoryStore::recent_within_minutes
   返回顺序),chat 需要 oldest-first 时间序——验证 reverse 没漏。
   顺序错了 LLM 会看到「未来→过去→当前」错乱时间轴,对话就废了。

3. build_polish_history_messages_keeps_polished_text_at_assistant_role
   关键不变量:历史 polish 必须挂 role=assistant,**不能**误挂 user。
   typo 把它放进 user role 会让 LLM 当成新输入再润色 → 输出复读上文,
   直接违反"不复读"目标。这个 test 把这条锁死,重构时一改就 fail。

4. polish_context_instruction_explicitly_forbids_repeating_prior_assistant_output
   第二层防御:仅靠 chat structure 不够(一些模型在长上下文里仍可能 echo prior turns)。
   验证 system prompt 末尾追加的指令含「不要 / 复读 / assistant-or-已经整理 /
   当前最新」四组关键词,文案可以改但语义不能丢。

cargo test --lib:139/139 pass(135 之前 + 4 新增),完全无回归。

* fix(icons): pad bundle icon to Apple HIG ~10% transparent margin (#354)

issue #354:在 macOS 26 以下系统,Dock 里的 OpenLess 图标比其他 app 大约 25%。

根因:原 icon.png 内容 100% 填满 1024×1024 画布,没留 Apple HIG 推荐的
~10% 透明 padding。macOS 26 自带 Liquid Glass squircle 容器会自动套圆角 +
裁切,掩盖了这个问题;但 macOS 11-15 把 icon PNG 原样渲染到 Dock,
所以视觉尺寸明显比标准 app 大一圈。

修法:
- 用 PIL 把 1024×1024 master 缩到 820×820 居中、四周各 102px 透明 padding
  (内容占比 ≈ 80%,符合 Apple HIG 约定)
- 重新生成 Tauri bundle 的 5 个 PNG:icon.png / 32×32 / 64×64 / 128×128 / 128×128@2x
- 用 iconutil 重新打包 icon.icns(含 16/32/128/256/512 + @2x 共 10 张)
- 不动 frontend public/AppIcon.png——那个是 React UI 内部用的小图(14-56px +
  CSS borderRadius),padding 反而会让它在固定容器里显得偏小

校验:python PIL 读新 icon.png,bbox 显示画布 512×512、内容 416×416 居中、
四周 padding 各 48px,内容占比 81.2%——接近 80% 推荐值。

不影响 macOS 26 渲染:Liquid Glass 看到的视觉内容仍是同一张图,
只是周围多了透明像素,Apple 自动 squircle 会无视它。

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 11:43:33 +08:00
TRIPandbaiqing b86c8d7966 feat(history+polish): time-based retention + conversation-aware polish (#355)
* feat(history+polish): time-based retention + conversation-aware polish

回应用户「历史记录功能 + 对话上下文 + 提示词调优」需求。

新增 2 个用户偏好(types.rs + UserPreferencesWire + Default + Deserialize 全套兼容):
- history_retention_days: u32(默认 7;0=不按时间清理仅受 200 上限)
- polish_context_window_minutes: u32(默认 5;0=关闭走原有单轮 polish)

后端:
- persistence.rs:
  - HistoryStore::append_with_retention(session, retention_days):写入新条目时
    顺手把超过 N 天的会话裁掉,不需要后台轮询
  - HistoryStore::recent_within_minutes(minutes):返回最近 N 分钟内的会话(newest-
    first,take_while 利用 newest-first 顺序提前终止)
  - 老 append() 保留,等价 append_with_retention(s, 0)
- polish.rs:
  - polish() 多一个 prior_turns 参数;不为空时走 chat_completion_with_polish_history
    构造 system + (user/assistant)... + 当前 user 多轮 chat completion
  - 把 chat_completion 的 send/parse 抽成 send_chat_request 复用
  - prompts::polish_context_instruction():明确告诉 LLM 历史 turns 仅作语义上下文,
    不要复读已经被插入到用户文档里的 assistant 输出
- coordinator.rs:
  - 主流水线在 Polishing 阶段拉 recent_within_minutes(prefs.window) 作为 prior_turns,
    过滤掉失败的 session(error_code/empty final_text)避免喂噪声
  - 翻译路径 / Raw 模式 / repolish 主动跳过上下文(语义不合适)
  - 3 个 history.append 改成 append_with_retention(session, prefs.history_retention_days)
- commands.rs / coordinator.rs:repolish / polish.rs::tests / commands.rs::testProvider:
  原 polish() 调用全部加 &[] 空 prior_turns 参数

前端:
- types.ts:UserPreferences 加 historyRetentionDays / polishContextWindowMinutes
- ipc.ts mockSettings + stylePrefs.test.ts mockPrefs:补默认值
- Settings.tsx Recording 区加 2 个 number input(裸 input + clamp,retention 0-365 天,
  context window 0-60 分钟)
- i18n 5 locale(zh-CN / zh-TW / en / ja / ko)补 4 个 key:
  historyRetentionLabel / Desc + polishContextWindowLabel / Desc

性能注意:retention 在 append 时 O(n) 一次扫描裁切;recent_within_minutes 使用
take_while 利用 newest-first 顺序提前终止,平均 O(window 内条目数) 不是 O(总历史)。

cargo check + commands::tests (30/30) + polish::tests (10/10) + npm run build 全 pass。
预先存在的 types::tests::legacy_hotkey_trigger 失败跟本 PR 无关(macOS 平台特有)。

不包含:连接 / 厂商 / 提示词排布优化(用户也提了)——那些需要先 measure 再动,
不能盲改,留给独立 PR。

* fix(types): HotkeyBinding default keys = None (legacy trigger / keys mismatch)

跑测试时发现 types::tests::legacy_hotkey_trigger_still_produces_effective_key_codes
fail,原因跟本 PR 无关,但用户要求中途发现 bug 顺手修。

Bug:HotkeyBinding 用 `#[serde(default)]` 结构级 default。反序列化时整个 struct
先按 Default 填充,JSON 字段覆盖。如果 Default 里 keys 预填具体 codes(macOS
Default ["AltRight"] / Windows ["ControlRight"]),那么旧 prefs 写
`{"trigger":"rightControl","mode":"toggle"}`(无 keys)反序列化后会变成:
  trigger = RightControl  ← JSON 覆盖
  keys = Some([HotkeyKey { code: "AltRight" }])  ← Default 残留,跟 trigger 不一致

effective_codes() 在 keys 是 Some 时直接信任 keys、不再 fallback 到
legacy_trigger_code(trigger),于是用户选的 trigger 跟实际生效的 keys 对不上。

修法:Default::keys = None。这样 keys 缺省时 effective_codes() 走 legacy_trigger_code
路径,跟 trigger 自动同步;JSON 显式写 keys 时仍可以覆盖。

types::tests 8/8 pass,全 lib 测试 135/135 pass。

* test(polish): lock down "no echo prior context" with 4 explicit unit tests

用户的核心顾虑:上下文感知 polish 不能让 LLM 把上文吐出来。PR #355 设计了
两层防御(chat structure + system prompt 显式指令),但**没写过 explicit 测试**
盯死这条不变量——回归就听天由命。这次把它锁住。

把 chat_completion_with_polish_history 里 inline 的 messages 构造抽成纯函数
build_polish_history_messages(system_prompt, prior_turns, user_prompt) → Vec<Value>,
方便可测,行为零变化。

新增 4 个 polish::tests:

1. build_polish_history_messages_empty_prior_falls_back_to_two_messages
   prior 空时只剩 [system, user],跟单轮 chat_completion 等价

2. build_polish_history_messages_orders_prior_oldest_to_newest_then_current
   入参 prior_turns 是 newest-first(match HistoryStore::recent_within_minutes
   返回顺序),chat 需要 oldest-first 时间序——验证 reverse 没漏。
   顺序错了 LLM 会看到「未来→过去→当前」错乱时间轴,对话就废了。

3. build_polish_history_messages_keeps_polished_text_at_assistant_role
   关键不变量:历史 polish 必须挂 role=assistant,**不能**误挂 user。
   typo 把它放进 user role 会让 LLM 当成新输入再润色 → 输出复读上文,
   直接违反"不复读"目标。这个 test 把这条锁死,重构时一改就 fail。

4. polish_context_instruction_explicitly_forbids_repeating_prior_assistant_output
   第二层防御:仅靠 chat structure 不够(一些模型在长上下文里仍可能 echo prior turns)。
   验证 system prompt 末尾追加的指令含「不要 / 复读 / assistant-or-已经整理 /
   当前最新」四组关键词,文案可以改但语义不能丢。

cargo test --lib:139/139 pass(135 之前 + 4 新增),完全无回归。

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 11:42:47 +08:00
millionart aa5add1188 [codex] fix Foundry Windows App Runtime prompt (#340) 2026-05-08 11:42:07 +08:00
Chris233 4aa3f6c58d Merge pull request #352 from H-Chris233/fix/issue-297-session-uuid
修复 session_id 回绕导致的迟到事件误匹配
2026-05-08 11:08:56 +08:00
H-Chris233 e95519a6b4 Prevent stale recorder events from matching wrapped session ids
The dictation coordinator used a wrapping u64 counter to guard delayed recorder and startup events. Replacing that guard with UUID-backed session ids removes the theoretical wraparound collision while keeping the existing stale-event comparison model.

Constraint: Issue #297 requests the UUID-based fix rather than checked arithmetic.\nRejected: checked_add with warning | still preserves a numeric boundary condition instead of eliminating the collision class.\nConfidence: high\nScope-risk: narrow\nTested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml coordinator::tests:: --lib\nNot-tested: Full cargo test --lib still has unrelated existing failure in types::tests::legacy_hotkey_trigger_still_produces_effective_key_codes.\nRelated: #297
2026-05-08 10:39:33 +08:00
Chris233 6c5eee6b78 Merge pull request #351 from H-Chris233/fix/issue-316-hotkey-settings-error
修复热键设置加载失败后永久 loading
2026-05-08 10:31:37 +08:00
H-Chris233 98f25bd9bb Keep hotkey settings load failures recoverable
Hotkey settings are a shared context for multiple pages, so one rejected IPC call must not leave the provider in a permanent loading state. The refresh path now records a localized recoverable error, keeps any successfully loaded half of the state, and always releases loading so consumers can render retry UI instead of staying blank.

Constraint: Issue #316 requires local load failures to stay visible and non-fatal.
Rejected: Keep Promise.all with one catch | it would still discard partial success from the other IPC call.
Confidence: high
Scope-risk: narrow
Tested: git diff --check; npm run build; cargo check --manifest-path src-tauri/Cargo.toml
Not-tested: Manual Tauri UI session with a deliberately corrupted preferences.json.
Related: https://github.com/appergb/openless/issues/316
2026-05-08 10:05:17 +08:00
Chris233 29c9160b9f Merge pull request #346 from H-Chris233/fix/issue-300-linux-ci-gate
为 PR CI 增加 Linux cargo check 门禁
2026-05-08 09:48:47 +08:00
TRIPandbaiqing 10440c7f72 chore: bump version to 1.2.24-3 (Beta-3) (#349)
Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:47:30 +08:00
TRIPandbaiqing e358c20b20 fix(updater): use GitHub atom feed for Beta lookup, dodge API rate limit (#350)
用户报「获取 Beta 版本信息失败」根因:fetch_latest_beta_release 调
api.github.com/repos/.../releases,未认证 60 req/h/IP。多次切 Beta toggle
+ 同 IP 共享配额(公司/家庭网络后),轻易撞 403 rate limit exceeded。
现场实测 IP 已耗尽:x-ratelimit-remaining: 0。

修法:换成 GitHub releases.atom(公开 RSS feed,CDN cache,无 API rate limit)。

具体改动:
- fetch_latest_beta_release 端点从 api.github.com 改成 github.com/.../releases.atom
- 提取一个纯函数 parse_latest_beta_from_atom 做字符串解析(不引 XML 库——
  feed 格式稳定,找 /releases/tag/ 锚点抓 tag 即可),方便单测
- atom feed 不显式标 prerelease,但项目约定 tag 后缀 `-beta-tauri` 就是 Beta,
  过滤后缀已足够
- timeout 从 10s 提到 15s,给跨境网络更多余地
- 加 2 个 unit test:(1) 混合 stable+beta 时返回 Beta entry;(2) 全 stable
  时返回 None。两个 test 都已 pass

不改前端:JS 端调用签名 / 错误展示路径不变,下次 Beta-3 release 出来后用户
装上即生效。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:47:18 +08:00
TRIPandbaiqing 8b55469d8e feat(ui): drop sidebar version chip; show BETA tag only on beta builds (#348)
用户反馈:左上角 OpenLess logo 旁边的 v1.2.24-2 chip 在窄侧栏下被截断换行
(视觉上读作 "v1.2.24-/2"),太丑且没有信息价值——版本号在底部 footer
和「设置 → 关于」里都已经展示,不需要这第三处。

同时 BETA 标签当前是无条件渲染的,正式版用户也会看到,违反"Beta 不
泄漏到正式版"原则。

改动:
- appVersion.ts:新增 IS_BETA_BUILD 常量。约定:semver prerelease 段
  (含 `-`)= Beta(如 1.2.24-1 / 1.2.24-2);不含 `-` = 正式版
  (如 1.2.23 / 1.2.24)。
- FloatingShell.tsx:
  - 删左上 brand 行里的 version chip <span>(保留 logo + "OpenLess" 文字)
  - BETA 区域用 {IS_BETA_BUILD && (...)} 包条件渲染。正式版 build 直接
    不渲染整块,避免普通用户误以为正式版是 Beta

footer 里的 "版本 v1.2.24-2 检查" 和 设置 → 关于的 AboutUpdateControl
保留不动——那两处是用户主动想看的版本信息,本次只删左上的冗余 chip。

npm run build 干净通过。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:39:14 +08:00
TRIPandbaiqing b7685cadda docs(release): make macOS xattr workaround discoverable (#347)
发现的真因:v1.2.23 Stable 和 v1.2.24-1 Beta 的 macOS DMG 都是 ad-hoc 签名 + 未经 Apple 公证(仓库没配 APPLE_CERTIFICATE 等 secrets)。macOS 14+ Gatekeeper 默认拒绝运行未公证的 app,弹"OpenLess 已损坏"。
build-mac.sh:40-41 注释已经写明这是已知限制:
  "用户免手工 xattr 的根本方案是 Developer ID 签名 + Apple notarization"

之前 README 只在 brew install 段写了 xattr -cr 指令,DMG 直装路径没写。
用户首次装 Beta 不知道要做这一步,看到"已损坏"以为 release 坏了。

本 PR:
- README.md / README.zh.md 的「下载与安装」段,DMG 直装路径加上 xattr -cr 指令 + 一句解释为什么需要(ad-hoc 签名、未公证)
- release-tauri.yml 的 Create release step 加 body 模板,所有未来 release 自动 prepend 一段:macOS xattr 指令 + Beta/Stable 渠道说明。用 append_body: true 让它放在 generated notes 之前

不在本 PR:配 Apple Developer 证书 + secrets——那是根治方案,需要 $99/年 Apple Developer 账号 + 用户决定要不要走那条路。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:27:16 +08:00
H-Chris233 c8fa63e30e Catch Linux cfg drift before release
CI only checked macOS and Windows before merge, so Linux-only Rust paths could fail cargo check for the first time during release packaging. The matrix now includes an Ubuntu lane that runs the same frontend build and backend cargo check, with only the Linux libraries needed for checking Tauri dependencies and no bundle step.

Constraint: Issue #300 asks for PR-time Linux validation while skipping Tauri bundle work.\nRejected: Add Linux release bundle to CI | too slow and duplicates release-tauri.yml.\nConfidence: high\nScope-risk: narrow\nTested: python ci.yml textual assertions; git diff --check; cd openless-all/app && npm run build; cd openless-all/app && cargo check --manifest-path src-tauri/Cargo.toml\nNot-tested: Full GitHub Actions ubuntu-latest runner execution before PR.\nRelated: https://github.com/appergb/openless/issues/300
2026-05-08 09:16:46 +08:00
Chris233 66e0efa7de Merge pull request #343 from H-Chris233/fix/issue-310-history-load-errors
修复 History IPC 失败时无反馈和卡 loading
2026-05-08 09:12:05 +08:00
TRIPandbaiqing 2a99d6bf6e chore: bump version to 1.2.24-2 (Beta-2) (#345)
Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:08:23 +08:00
TRIPandbaiqing 58a0ad9e62 fix(release): use tagged URL in beta manifest, not releases/latest (#344)
修复 v1.2.24-1-beta-tauri release verification 时发现的 latent bug:
Beta manifest 里 url 字段指向 https://github.com/.../releases/latest/download/<asset>,
但 releases/latest 永远是最新非-prerelease(即 Stable),Beta 用户按这个 url
auto-update 会拉到 Stable 包:

- macOS Beta: 文件名 OpenLess_aarch64.app.tar.gz 跟 Stable 重名 → 下载 Stable 包,
  装上的是 1.2.23 但 manifest 声称 1.2.24-1,version drift
- Windows/Linux Beta: 文件名带版本号 OpenLess_1.2.24-1_x64-setup.exe / .AppImage,
  releases/latest 路径下没这个文件 → 直接 404

Stable 渠道沿用 releases/latest 是对的(latest = Stable)。

修法:write-updater-manifest.mjs 多读一个 OPENLESS_RELEASE_TAG env,beta 时
url 拼成 releases/download/<tag>/<asset>(指定具体 tag,不被 prerelease 折叠
影响);stable 不变。脚本对 beta 缺 tag 做 fail-fast。

release-tauri.yml 在 manifest step 多注入 OPENLESS_RELEASE_TAG: ${{ github.ref_name }}。

为什么不重发 v1.2.24-1:当前走 Path A 手动下载式 opt-in,前端调
fetch_latest_beta_release 拿 GitHub release 页 html_url,用户跳浏览器手动选包,
不消费 manifest.url。所以 v1.2.24-1 已发布的 manifest 里 url 错也是 dormant。
下一轮 Beta tag(v1.2.24-2-beta-tauri 或 stable cut 后再做 Beta)会用修复后
的脚本产出正确 URL。

本地 smoke test:
- channel=beta + 缺 OPENLESS_RELEASE_TAG → fail-fast 抛 "is required"
- channel=beta + 有 tag → env 检查通过,进入 artifact 解析
- channel=stable + 不传 tag → 维持原行为
- python yaml.safe_load() 解析 release-tauri.yml 通过

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:04:14 +08:00
H-Chris233 f79a8dc089 Avoid restoring stale History snapshots
The History mutation handlers already wait for backend success before changing visible list state, so failure recovery does not need to write back the request-time snapshot. Removing that restoration avoids overwriting a newer refresh result, and delete success now updates from the latest list state instead of the closure captured before the IPC call.

Constraint: PR #343 review identified stale snapshot writes during overlapping refresh and mutation requests.\nRejected: Add action-wide locking | more UI state than needed once failed mutations stop writing stale snapshots.\nConfidence: high\nScope-risk: narrow\nTested: cd openless-all/app && git diff --check && npm run build\nNot-tested: Manual overlapping refresh/delete in the Tauri app.
2026-05-08 08:50:47 +08:00
H-Chris233 1676557a75 Keep History usable after IPC failures
History actions previously assumed list, clear, and delete IPC calls would resolve. A rejected load could leave the page stuck in loading, while failed mutations gave no user-visible feedback. The page now clears loading in finally, shows retryable inline load errors, and reports clear/delete failures while restoring the prior local selection and items.

Constraint: Issue #310 asks for inline card feedback without changing Rust handlers or introducing a toast system.\nRejected: New shared error component | broader UI surface than needed for this single-page failure path.\nConfidence: high\nScope-risk: narrow\nTested: cd openless-all/app && git diff --check && npm run build\nNot-tested: Live Tauri IPC failure injection.\nRelated: https://github.com/appergb/openless/issues/310
2026-05-08 08:06:29 +08:00
Chris233 13937d21fd Merge pull request #342 from H-Chris233/fix/issue-311-overview-load-errors
修复 Overview IPC 加载失败时误显示空数据
2026-05-08 08:02:27 +08:00
H-Chris233 d32da5c36c Expose Overview IPC load failures inline
Overview previously treated rejected history and credential IPC calls like empty data, which made backend read failures indistinguishable from a clean first run. The page now keeps separate error flags, shows provider status as unreadable when credential status cannot load, and replaces history-derived metrics/recent rows with retryable inline failure states.

Constraint: Issue #311 asks for per-card inline feedback without changing backend handler signatures or adding global toast UI.\nRejected: Global error boundary or toast | broader UI behavior than needed for the Overview failure path.\nConfidence: high\nScope-risk: narrow\nTested: cd openless-all/app && git diff --check && npm run build\nNot-tested: Live Tauri IPC failure injection.\nRelated: https://github.com/appergb/openless/issues/311
2026-05-08 07:25:38 +08:00
Chris233 c4a7ebe62c Merge pull request #338 from H-Chris233/fix/issue-315-selectionask-save-feedback
修复划词追问保存失败无反馈
2026-05-08 07:16:52 +08:00
H-Chris233 b0f481c78e Block shortcut recording during Selection Ask saves
The Selection Ask page now disables its card toggles while a save is in flight, but the nested shortcut recorder still accepted new recordings. That left a small overlap window where a second QA hotkey save could race the current setQaHotkey/savePrefs pair.\n\nThis keeps the recorder default behavior unchanged for other pages by adding an optional disabled prop and only wiring it from Selection Ask during its saving state.\n\nConstraint: Review feedback on PR #338 requested preventing overlapping QA shortcut saves without broad settings-queue refactoring.\nRejected: Global serialization inside ShortcutRecorder | unnecessary for pages that already own their own persistence state.\nConfidence: high\nScope-risk: narrow\nTested: cd openless-all/app && npm run build\nTested: git diff --check\nNot-tested: Manual browser interaction against a slow Tauri IPC response.\nRelated: https://github.com/appergb/openless/issues/315\nRelated: https://github.com/appergb/openless/pull/338
2026-05-08 06:47:36 +08:00
Chris233 50cd1ae583 Merge pull request #341 from Cooper-X-Oak/docs/backfill-cooper-developer-entry-beta
docs: add Cooper's circular avatar
2026-05-08 06:44:40 +08:00
cooper e331784b3b docs: add Cooper's circular avatar image
- Add assets/people/cooper.png (circular avatar)
- Replace GitHub API link with local image file
- Match TRIP and Chris233's avatar style
2026-05-08 00:34:48 +08:00
cooper 620e817939 docs: make Cooper avatar circular
Add border-radius: 50% to Cooper's avatar to match TRIP and Chris233's circular style.
2026-05-08 00:32:29 +08:00
cooper dd96d64672 docs: add website URLs back to developer section
- Add website URLs below each developer's name
- Increase cell width from 110 to 140 to accommodate URLs
- Keep circular avatars and table layout
2026-05-08 00:27:31 +08:00
cooper 712e9564f2 docs: refine developer section 2026-05-08 00:02:23 +08:00
H-Chris233 b027d8b210 Expose Selection Ask save failures before state drifts
Selection Ask reused the same QA hotkey and history persistence paths as other settings pages, but the page did not surface pending, success, or failure states. A rejected hotkey registration or settings write could therefore leave users believing the feature or history option was enabled when the backend had not accepted the change.\n\nThis keeps the existing persistence contract and mirrors the Translation page status treatment: register QA hotkeys before saving prefs, stop on registration errors, refresh after write failures, and add localized feedback for all supported UI languages.\n\nConstraint: Issue #315 requests minimal UI feedback and rollback behavior without refactoring the shared settings queue.\nRejected: Refactor HotkeySettingsContext persistence semantics | broader cross-page behavior change than needed for this bug.\nConfidence: high\nScope-risk: narrow\nTested: cd openless-all/app && npm run build\nTested: git diff --check\nNot-tested: Manual Tauri hotkey conflict flow on macOS/Windows.\nRelated: https://github.com/appergb/openless/issues/315
2026-05-07 23:32:41 +08:00
Chris233 fc24d37fdd Merge pull request #331 from H-Chris233/fix/issue-313-style-save-feedback
修复 Style 页保存失败后的状态回滚
2026-05-07 22:57:22 +08:00
TRIPandbaiqing 4962a661a3 chore: bump version to 1.2.24-1 (#337)
将 Beta 版本号从 1.2.24-beta.1 改为 1.2.24-1。

原因:Tauri MSI bundler 对 prerelease 段的限制比 npm/Cargo semver 严格,
要求 prerelease identifier 必须是「numeric-only and ≤ 65535」。
1.2.24-beta.1(含字母 + 多段)会让 windows MSI 构建直接报:
  failed to bundle project: optional pre-release identifier in app version
  must be numeric-only and cannot be greater than 65535 for msi target

实测后果:上次 v1.2.24-beta.1-beta-tauri tag 触发的 CI run 25501471895,
macOS arm64 / macOS x64 / Linux 都成功,只有 Windows MSI 卡在 candle 之前。

修复:
- 1.2.24-1 是合法的 npm/Cargo semver prerelease 写法( id="1" 纯数字)
- Tauri MSI 接受这种格式
- semver 排序仍然 1.2.23 < 1.2.24-1 < 1.2.24,符合渠道隔离要求
- 后续 Beta 迭代版本号: 1.2.24-2 / 1.2.24-3 / ...
- Beta 身份不靠版本字面,靠 tag 后缀 `-beta-tauri`(release-tauri.yml 检测)

5 处版本号同步:package.json、package-lock.json (root + nested)、
tauri.conf.json、Cargo.toml、Cargo.lock。CI 用的同一段 verify 通过。

合入后由维护者删旧 tag/release(已删)+ 推 v1.2.24-1-beta-tauri 触发新 CI。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-07 22:40:18 +08:00