Commit Graph
758 Commits
Author SHA1 Message Date
baiqing ea1b18ee08 chore(release): merge beta for Stable 1.3.0 2026-05-12 10:28:19 +08:00
baiqing 8ea4e8932f chore(release): bump version to 1.3.0 (Stable) 2026-05-12 10:28:07 +08:00
baiqing e90fe9f7b1 chore(release): correct beta version to 1.3.0-1 (MSI requires numeric pre-release)
上一条 1.3.0-BETA 在 Windows MSI 打包阶段被 Tauri 拒绝:MSI ProductVersion
只接受 \"MAJOR.MINOR.BUILD\" + 数字 pre-release(≤ 65535)。BETA 字面被
canonicalize 时报错 \"optional pre-release identifier in app version must be
numeric-only\"。

按仓库历史 (1.2.24-7/8/9) 同体例改为 1.3.0-1,表示 1.3.0 的第 1 个 Beta。
2026-05-12 09:56:20 +08:00
baiqing 7136cf55cc chore(release): bump version to 1.3.0-BETA 2026-05-12 02:04:32 +08:00
TRIP d4162cf9cc Merge pull request #412 from Open-Less/pr/streaming-cross-platform
feat: cross-platform streaming polish output (Win/Linux/macOS)
2026-05-12 02:02:57 +08:00
baiqing ceda94e73b fix(streaming): clipboard saves the final_text users actually saw
pr-agent #412 二次反馈 \"Clipboard Mismatch\":原来无条件先写 polish 完整 text
到剪贴板,再决定 typer 是否中途失败 → typer 中途失败时屏幕 / history 是
partial typed_text,但剪贴板是完整 polish,Cmd+V 会粘出用户从未见过的内容。

修复:先把 final_text 算出来(typer 失败时 = typed_text,正常时 = 完整 polish),
然后剪贴板、history、返回值统统用同一个 final_text,三处保持一致。

测试:cargo test --lib 222 passed。
2026-05-12 01:33:41 +08:00
baiqing a8ae91c63b fix(streaming): keep history = typed_text + fallback when zero chars typed
pr-agent #412 反馈两条都修:

1. \"Missing fallback\" —— typer 在第一字就失败(最常见:session 开始时已处
   于 macOS Secure Input,或 SendInput / enigo 拒绝),已往返流式但 typed_chars=0。
   旧逻辑仍返 already_streamed=true → 上层跳过 inserter → 用户看不到任何内容。
   新逻辑:typed_chars=0 且 typer_failure 存在时,回退到一次性 inserter 路径
   兜底,让 polish 文本通过正常 insertion 写出。

2. \"Wrong final text\" —— SSE 中途失败但已落字(typed_chars > 0),旧逻辑给
   history 写 raw.text 而屏幕显示 partial polish,二者分叉。新逻辑:typer 持续
   累积 typed_text(屏幕实际落字内容),history 与屏幕保持一致 —— 写 typed_text,
   polish_error 注明 \"streaming polish failed mid-stream after N chars\"。

实现:typer 任务返回值由 (typed_chars, first_failure) 变为 (typed_text, first_failure),
顶层用 typed_text.chars().count() 派生 typed_chars,并把 typed_text 在两个分支
都用上。

测试:cargo test --lib 222 passed。
2026-05-12 01:33:41 +08:00
baiqing 44f1b94c32 feat: cross-platform streaming polish output (Win/Linux/macOS)
让润色 LLM 的 SSE chunk 边到达边逐字模拟键盘事件落到光标,用户感知到的
处理时延显著降低(第一个 token 即开始落字)。Opt-in,不开等价于历史行为。

后端(src-tauri/src/)
- unicode_keystroke.rs(新):跨平台 Unicode 按键合成
  - macOS:CGEvent + 切到 ABC 输入源(规避 CJK/日文 IME 拦截 Unicode 事件)+
    Secure Input 探测;TIS 调用强制调度到主线程,规避 macOS 14+ TSM 断言。
  - Windows:SendInput(KEYEVENTF_UNICODE),绕过 TSF / IME,不切输入法。
  - Linux(实验):enigo Keyboard::text。X11 / XTest 稳定;Wayland 看 compositor
    给不给 libei 权限,失败回落由调用方处理。
- polish.rs:OpenAI-compatible provider 新增 polish_streaming + 共用 SSE 解析
  skeleton(与 QA streaming 同款);Codex provider 返 not-implemented 让上层降级
- coordinator.rs:StreamingPolishOutcome { Streamed, UnsupportedFallback, Failed }
  + polish_or_passthrough_streaming 分发器
- coordinator/dictation.rs::run_streaming_polish:
  · 切到 ABC(macOS)→ spawn_blocking typer 串行读 mpsc → 调流式润色 →
    drop tx → typer drain 退出 → 恢复输入源
  · 中途取消 / typer 失败 / Secure Input 启用:drain 不爆,已流出去的字保留
  · 流式成功:可选写回剪贴板(streaming_insert_save_clipboard,默认 on)
  · 流式失败:已流的部分保留,未流时回落到 raw 一次性
  · end_session 的 cancel 检查识别 already_streamed=true 走完收尾(撤销不掉)
- types.rs:UserPreferences 新增 streaming_insert(默认 false)+
  streaming_insert_save_clipboard(默认 true),Wire/Deserialize/Default 三处同步
- lib.rs:mod unicode_keystroke

前端(src/)
- lib/types.ts + ipc.ts:UserPreferences 同步两字段 + mock 默认值
- lib/stylePrefs.test.ts:测试 fixture previousPrefs 补两字段
- pages/Settings.tsx:AdvancedSection 加流式 Card(Toggle ×2),按平台分发 hint
  文案(mac / win / linux 三套)
- i18n/{zh-CN,en,ja,ko,zh-TW}:advanced 新增 8 个 streamingInsert* key,5 个
  locale 完全对齐

验证范围
- macOS host:cargo check + cargo test --lib (222 pass) + npm run build (tsc + vite)
- Windows/Linux cfg 分支只走过 cargo check(cross-target 验证依赖 CI runner)
2026-05-12 01:33:41 +08:00
TRIP af07795e25 Merge pull request #407 from Open-Less/pr/settings-vocab-collapsible
refactor(ui): Collapsible groups in Settings & Vocab
2026-05-12 01:25:41 +08:00
baiqing 50daab11e2 fix(a11y): expose Collapsible expanded state + keyboard focus
pr-agent #407 二次反馈:toggle button 缺 aria-expanded(屏幕阅读器无法判断
当前组是否展开),并且 outline: 'none' 把浏览器默认 keyboard focus indicator
也抹掉了(Tab 切换时丢失视觉焦点)。

修复:
- 加 aria-expanded={open}
- 移除 outline: 'none',让浏览器默认 focus ring 显示出来
2026-05-12 00:55:26 +08:00
TRIP 1f11e2574b Merge pull request #408 from Open-Less/pr/capsule-thinking-shine
feat(capsule): animated thinking shine indicator
2026-05-12 00:32:33 +08:00
TRIP 6a1986e0c5 Merge pull request #409 from Open-Less/pr/hotkey-debounce-250ms
fix(hotkey): 250ms debounce on pressed-edge dispatch
2026-05-12 00:32:13 +08:00
TRIP 080fd9c155 Merge pull request #411 from Open-Less/pr/docs-and-empty-asr-zh
docs: refresh CLAUDE.md + localize empty-ASR capsule message
2026-05-12 00:31:55 +08:00
TRIP 051e058838 Merge pull request #410 from Open-Less/pr/polish-role-block-refine
polish: refine ROLE_BLOCK to preserve user intent
2026-05-12 00:31:13 +08:00
baiqing e44dcbc7f1 fix(a11y): make collapsed Collapsible content non-tabbable
pr-agent #407 反馈:grid-template-rows: 0fr 把内容视觉裁掉,但内部的 input /
button / Toggle 仍 mounted 且 tabbable —— 键盘用户折叠 Section 之后仍能 tab
到不可见的控件,屏幕阅读器也会朗读。

修复:内容容器 inert={当未展开} + aria-hidden={当未展开}:
- inert 把整个子树从 tab 顺序、点击事件、a11y 树里同时移除
- aria-hidden 配合 inert 给保守的 AT 兜底

React 18 types 没收 inert,用 spread { inert: '' } 绕过 TS。Chromium 102+ /
Safari 15.4+ 支持,Tauri WebView 远高于此基线。
2026-05-12 00:27:14 +08:00
baiqing f5d9288716 refactor(ui): Collapsible groups in Settings & Vocab
把 Settings/Recording 与 Vocab 三段从单一长 Card 切成可折叠分组,缓解
内容下沉时整段 reflow / overflow 的拥挤感。

新组件:
- _atoms.tsx::Collapsible — grid-template-rows 0fr↔1fr 高度过渡(Chromium
  117+ 解析 1fr 为内容真实高度,比 max-height 固定大值稳);右上角 chevron
  旋转 90° 标示展开
- embedded prop:嵌在 padding=0 的父 Card 里时只画底部分隔线,无独立 border

Settings/RecordingSection:
- 原单 Card 拆为 1 个主 Card + 3 个 Collapsible(插入与剪贴板 / 历史与上下文 /
  启动)
- 滚动容器加 paddingBottom: 16 防 Card 底部 shadow 被裁
- 同步把若干 desc 文案缩短一句,避免 Collapsible 折叠展开时的高度抖动

Vocab:
- 原 3 段 div padding=18 改用 Collapsible(embedded),词条区域 defaultOpen
- 标题从 strong + tip div 改为 Collapsible 自带的 title + desc props

i18n(5 个 locale 全平等更新):
- 新增 4 个 key:vocab.sectionTitle / settings.recording.{insertGroupTitle,
  historyGroupTitle, startupGroupTitle}
- 同步缩短若干 recording 区 desc 文案
2026-05-11 23:45:23 +08:00
baiqing d52c6cda05 feat(capsule): animated thinking shine indicator
把 transcribing / polishing 阶段的胶囊从原来的"三个圆点 + 文案"换成
扫光的 thinking 文字效果:

- 移除 ProcessingDots 组件
- 文字字号 11 → 14,字重 500,扫光 linear-gradient + background-clip: text
- 进入 transcribing / polishing 的头 2 秒走 0.9s/cycle 快速扫光(提示「刚开始」),
  之后切回 2.4s 稳态。React 副作用切 duration 不重启动画,会平滑减速
- 切换 state 时多一道 220ms 淡入(cap-state-enter)让用户更容易感知
- 5 个 locale 的 capsule.thinking 文案统一为 'thinking'(小写英文),与扫光字面
  视觉一致,避免不同语言长度差影响排版

CSS keyframes 改名 cap-dot → cap-shine,并加 cap-state-enter。
2026-05-11 23:41:29 +08:00
baiqing 60ab2418bd fix(hotkey): 250ms debounce on pressed-edge dispatch
避免微动开关回弹 / 用户手抖双击造成的空转写报错和 ASR session 抢资源:
相邻 < 250ms 的 press 边沿直接丢弃,记 log。

- coordinator.rs: Inner 新增 last_hotkey_dispatch_at: Mutex<Option<Instant>>
- coordinator/dictation.rs: HOTKEY_DEBOUNCE 常量 + handle_pressed_edge 入口检查

与 hotkey_trigger_held 互补:held 防 press-without-release,本检查防
press-release-press 三连过快。每个有效边沿都会更新时间戳;用户正常使用
(不会快于 4 次/秒)不会触发。
2026-05-11 23:39:43 +08:00
baiqing 2384351ad8 docs: refresh CLAUDE.md + localize empty-ASR capsule message
CLAUDE.md:
- 模块表加上 coordinator 拆分(dictation/qa/resources)、coordinator_state、qa_hotkey
  与三套并行 hotkey 监听(hotkey / combo_hotkey / qa_hotkey + global_hotkey_runtime)
- 新增 Q&A pipeline 章节,记录 panel 路由、capsule sweep、selection 3-tier fallback、
  selection 4000 char 截断与多轮 messages 行为
- 新增 Insertion paths 章节,区分跨平台默认路径与 Windows IME-IPC 路径
- 顶部 adjacent docs 段落补 AGENTS.md / README.zh.md / USAGE.md 指引

dictation.rs:
- 空转写场景 capsule 文案 "ASR returned empty transcript" → "没有识别到语音",
  与界面其他位置一致。line 907 的内部 Err 字符串保留英文(属于上层日志/调用方
  消费的内部 error code,不直接对用户显示)
2026-05-11 23:37:42 +08:00
baiqing 7e60bdb511 polish: refine ROLE_BLOCK to preserve user intent
把 polish 提示词从"严苛不允许"语气改回贴近用户原意的整理姿态:

- 先分析用户意图,再贴合原本句子做语法整理与必要的结构化
- 措辞优先用原句字面词;理解到的意图用来贴近原话,而不是替用户重写或扩写
- 不创作 / 不补充用户没说过的内容
- 转写里若有未解决问题或待确认事项,全部列为条目保留,不省略也不替用户判断
- 用户意图难以判断时不强行推断,改为只做句子层面整理(标点 / 断句 / 口癖去除)
- 不引用任何会话历史 / 外部知识 / 模型记忆,每次请求独立任务

行为差异:相对于已发布版本,新增"列出未解决问题"规则;移除"补充功能清单"
那一条(与新的"列为条目保留"语义已经覆盖)。
2026-05-11 23:35:04 +08:00
Chris233 e3a6c44d4d Merge pull request #405 from weikeyi/fix/issue-298-wix-glob
fix(windows-ci): WiX 工具按 WixTools* 通配查找,避免 Tauri 升级后路径失效
2026-05-11 17:23:48 +08:00
weikeyi 58c229dace fix(windows-ci): WiX 工具按 WixTools* 通配查找,避免 Tauri 升级后路径失效
Windows MSI Repair 之前硬编码 `C:\Users\22933\AppData\Local\tauri\WixTools314\light.exe`,Tauri 升级到新 WiX 版本(如 WixTools315)后路径不存在,Repair 步骤直接挂。

改为 WixTools* glob,按数字后缀降序回退到名称降序,取最新可用版本。同步更新 windows-package-msvc.ps1::Find-WixTool 和 release-tauri.yml 的 Repair 步骤;保留 -sice:ICE80、NSIS/MSI 两轮 build、bash shell 等 Windows CI 红线不变。

补 4 条 windows-package-msvc.test.mjs 防回归断言:禁止 WixTools314 出现在脚本/workflow,要求两边都按 WixTools* glob 查找。

Refs: #298
2026-05-11 16:50:20 +08:00
baiqing cc215fdce3 chore(release): bump version to 1.2.24-9 (Beta-9) 2026-05-11 13:19:17 +08:00
Chris233 69978a3c47 Merge pull request #404 from H-Chris233/fix/issue-402-llm-thinking-toggle
feat: 增加渠道级 LLM 思考模式开关
2026-05-11 11:48:13 +08:00
H-Chris233 8fbb965635 Let users control LLM thinking at the provider channel
Issue #402 needs a Settings toggle that applies to every LLM runtime path without changing prompt behavior. The implementation keeps the preference in user settings, threads it through polish, translate, QA, and validation, and sends only provider-channel request fields documented by each backend instead of prompt instructions or model-name heuristics.

Constraint: Thinking controls are provider-specific request fields, not a portable prompt convention

Rejected: Prompt-level thinking instructions | changes model-facing prompts and cannot guarantee provider thinking mode

Rejected: Per-model allow/deny lists | user requested channel-level behavior and provider-side handling for unsupported models

Confidence: medium

Scope-risk: moderate

Directive: Do not add prompt injection or model-name thinking adapters for #402 without a new explicit product decision

Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib openai_chat_body -- --nocapture

Tested: cd openless-all/app && cargo test --manifest-path src-tauri/Cargo.toml --lib disabled_thinking_config -- --nocapture

Tested: cd openless-all/app && cargo test --manifest-path src-tauri/Cargo.toml --lib build_generate_body -- --nocapture

Tested: cd openless-all/app && cargo check --manifest-path src-tauri/Cargo.toml

Tested: cd openless-all/app && npm run build

Tested: git diff --check

Related: #402
2026-05-11 11:20:35 +08:00
baiqing 47bda0d0ac chore(release): bump version to 1.2.24-8 (Beta-8) 2026-05-11 07:10:53 +08:00
TRIP e7a0ad5971 Merge pull request #401 from BBossss/beta
Merging Codex OAuth LLM provider integration from external contributor.
2026-05-11 07:10:15 +08:00
TRIPandbaiqing 3125ff4d20 feat(settings): cross-platform parity for local ASR (#403)
* feat(settings): cross-platform parity for local ASR

- AdvancedSection: warning collapses to inline 11px amber badge in
  the title row's top-right; the enable-confirm popup is now a
  centered fixed modal with backdrop blur(8px) and click-outside-
  to-cancel (busy-guarded).
- Symmetric rows by platform — fixes the #400 bug where the Qwen3
  row was rendered on Windows even though all qwen_engine /
  local_provider / cache modules are #[cfg(target_os = "macos")],
  so Enable on Windows would always error:
    macOS:   Qwen3 row enabled / Foundry row hidden
    Windows: Foundry row enabled / Qwen3 row disabled with
             notSupportedHere desc
- Providers ASR Card: replaces the plain-text localAsrActiveNotice
  branch with an always-rendered dropdown that includes the
  platform-native local engine (macOS=Qwen3, Windows=Foundry) as a
  disabled option, plus a takeover hint underneath. Entire
  dropdown locks when a local engine is active, so it can only be
  released via Advanced (matches "启动之后供应商将被接管" UX).
- onLlmProviderChange / onAsrProviderChange now emit saving →
  saved through emitSaved. Data was already being persisted; the
  saved-toast was missing.
- LocalAsr embedded: hide Qwen3 model management (mirror /
  engine-status / model list) on Windows — dead UI without the
  Qwen3 backend. The shared error Card is kept unconditional
  since Foundry handlers also call setError.
- i18n × 5 (zh-CN/zh-TW/en/ja/ko): shorten localAsrWarningShort;
  rewrite qwen3Desc and foundryDesc to '启动之后,ASR 提供商将被接管';
  add notSupportedHere and localAsrTakeoverHint.

* fix(settings): clear saving toast on provider-switch failure

pr_agent #403 caught: emitSaved('saving') with no try/catch — if
setActiveLlmProvider / setActiveAsrProvider / any later credential
write throws, the right-corner toast stays stuck on saving forever.

Wrap both onLlmProviderChange and onAsrProviderChange bodies in
try/catch. On error, emit 'failed' (and re-throw) only if seq still
matches — stale calls superseded by a newer switch shouldn't
overwrite the newer call's own saving state.

* fix(settings): provider-card visual cleanup + log-export permission + pr_agent #403

Fixes piled up in user feedback during local verify of the previous
commit; bundled into one commit to keep #403 small.

- capabilities/default.json: add `dialog:default` permission. The
  About → Export error log button always failed because the Tauri 2
  capabilities did not allow `tauri-plugin-dialog`'s `save()` command;
  the JS rejected before export_error_log was ever invoked.

- ProvidersSection ASR Card (pr_agent #403 'Wrong active label'):
  when committedAsrProvider is a local engine, selectedValue now
  reflects the actually-active engine instead of being forced to
  platformLocalAsr. Cross-machine config sync (macOS profile with
  foundry-local-whisper / Windows profile with local-qwen3) used to
  make the dropdown lie about which backend is running. Anomalous
  local engines are added as disabled <option>s so the controlled
  <select> still finds its value and doesn't fall back to the first
  option.

- ProvidersSection: drop verbose desc on LLM and ASR provider rows.
  '选择后将自动填入 Base URL 默认值。' is ~16 Chinese chars and
  always wraps inside the 180px label column, leaving a stranded
  second line on the left — the dropdown itself already conveys the
  semantic, the desc was redundant.

- AdvancedSection: replace the [启用] / [已启用] Btn with the
  shared Toggle component for Qwen3 and Foundry rows. Toggle off
  switches back to volcengine. The Qwen3 row's Toggle truthfully
  reflects committed state on Windows (so a cross-platform anomaly
  shows ON even though it is non-interactive there). The standalone
  '禁用本地 ASR' escape row is now only rendered when no inline
  Toggle can disable the active engine — i.e., Linux / unsupported
  platform, or anomaly where the row for the foreign engine is
  hidden / non-interactive.

- PermissionsSection: every status pill (microphone, accessibility,
  hotkey, Windows IME, network) now right-aligns inside its row via
  `justifyContent: flex-end` + `width: 100%`. Pill text moved to the
  right of inline messages so the green '已授权' / 'Authorized'
  reads as a right-edge badge — closer to macOS System Settings
  convention.

* fix(settings): keep Windows Qwen3 toggle visually OFF (pr_agent #403)

pr_agent 'Stale Windows state' caught: previous commit had `on=
{isOnLocalQwen3}` which truthfully reflected committed state, so a
profile synced from macOS with activeAsrProvider=local-qwen3 would
render Toggle ON next to a desc that says '本平台暂不支持'. The
visual contradiction was the concern.

Switch to `on={isMac && isOnLocalQwen3}` — Windows always renders
the row OFF + non-interactive, matching the unsupported desc. The
cross-platform anomaly state surfaces via the standalone '禁用本地
ASR' escape row, which is rendered exactly when no inline Toggle
can disable the active engine.

Comment updated to match the new behavior and document the
tradeoff.

* fix(localasr): scope Qwen3 mgmt UI to macOS only (pr_agent #403)

pr_agent 'Linux regression' caught: previous wrap was `!IS_WINDOWS`,
so a hypothetical Linux render path would still expose the Qwen3
mirror / model-list / engine-status UI even though Qwen3's backend
is `#[cfg(target_os = "macos")]`.

Swap the two wrappers to `IS_MAC` so the management cards only
materialize on macOS. Today's AdvancedSection guards LocalAsr
embedded with `platformSupported = isMac || isWin`, so Linux already
short-circuits before LocalAsr renders — but defense in depth here
costs nothing and keeps the comment accurate.

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-11 07:01:00 +08:00
bbossssandOmX 8391e566c6 Let OpenLess reuse Codex auth for low-latency polishing
Codex OAuth should let users spend their existing Codex subscription on short dictation polish, translation, and QA without storing another API key in OpenLess. The provider reads the local Codex auth file, keeps OpenAI-compatible providers on their existing path, uses a lighter Spark default for latency, and broadens Codex auth discovery for Windows GUI environments.

The same change keeps Volcengine on the duration-priced BigASR resource validated locally and preserves proxy behavior so Codex network paths can use system proxy settings while loopback/local provider checks avoid the proxy.

Constraint: Codex OAuth credentials must remain sourced from the Codex auth file and must not be copied into OpenLess credential storage.

Constraint: PR #401 is a fork PR from BBossss/openless:beta to appergb/openless:beta.

Rejected: Require a separate OpenAI API key | the requested path was to reuse the existing Codex subscription.

Rejected: Keep gpt-5.5 as the default | it is unnecessarily heavy for short dictation polish and was observed timing out.

Rejected: Use only HOME for Codex auth lookup | Windows GUI sessions commonly expose USERPROFILE instead.

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Do not persist Codex OAuth tokens into OpenLess credentials; keep using the Codex auth file as the source of truth.

Directive: Keep Codex OAuth defaults optimized for short low-latency dictation polishing unless product requirements change.

Tested: git diff --cached --check

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

Tested: npm run build

Tested: PR #401 CI passed on Windows, macOS, and Linux before this history-only squash.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-10 22:04:25 +08:00
Chris233 69081e0d42 Merge pull request #397 from appergb/docs/research-first-subagent-mechanism
docs: codify subagent-mediated API / library / platform research workflow
2026-05-10 19:08:26 +08:00
TRIPandbaiqing f3894c1f7b feat(llm): Google Gemini provider + fix LLM provider switch (#398)
* chore: ignore video-materials/ promo recordings

video-materials/ holds ~2.2 GB of promo recordings (promo /
promo-materials / promo-openless / promo-openless-v2 / SC). These
are local working assets that don't belong in the repo. Add to
.gitignore so future stash / commit operations can't accidentally
pull the binary blob in.

* feat(llm): Google Gemini provider with forced thinking-off + fix provider-switch credentials

Add Google Gemini as a native LLM provider hitting v1beta
:generateContent (single-shot) and :streamGenerateContent?alt=sse
(QA chat) with x-goog-api-key auth. Per-model thinkingConfig is
injected to keep thinking off for every supported ID — exact
matrix verified against ai.google.dev/gemini-api/docs/thinking:

  gemini-2.5-flash, gemini-2.5-flash-lite     thinkingBudget: 0
  gemini-2.5-pro                              field omitted (Google: cannot disable)
  gemini-3*-pro*                              thinkingLevel: "low"
  gemini-3*-flash*, unknown future IDs        thinkingLevel: "minimal"

Why native, not the OpenAI-compat shim: shim is beta and for 3.x
needs an extra_body workaround for thinking control; auth header
and contents shape differ enough that reusing
OpenAICompatibleLLMProvider would have been the worse compromise.

fix(settings): switching LLM preset previously only auto-filled
endpoint/model when those slots were empty. All providers share
ark.endpoint / ark.model_id (per-provider entries were never
introduced — see persistence.rs lookup_account), so for returning
users with non-empty slots, switching the dropdown updated active
provider but kept the OLD endpoint — polish silently kept calling
the previous provider. Now switching to any non-custom preset
overwrites endpoint/model to that preset's defaults. Trade-off:
user-customized endpoints within a preset are dropped on switch;
silent wrong-routing was a worse failure mode.

Backend:
- New module llm_gemini.rs (15 unit tests for thinking-config table,
  URL builder, response parser, SSE shape, history role mapping)
- polish.rs extracts compose_polish_prompts / compose_translate_prompts
  / compose_qa_system_prompt as pub(crate) helpers shared by both
  LLM paths so prompt assembly can't drift
- coordinator.rs's polish_text / translate_text / answer_chat_dispatch
  open with `if get_active_llm() == "gemini"` early-return; existing
  OpenAI-compat callsite stays untouched

Frontend:
- LLM_PRESETS gains gemini entry (baseUrl v1beta, model placeholder
  gemini-2.5-flash)
- ProviderTools hidden when committedLlmProvider === 'gemini'
  (its "Fetch models" button assumes OpenAI /models shape; native
  /v1beta/models response shape doesn't match)
- Static geminiModelHint lists the 7 available IDs so users pick
  correctly without auto-fetch
- 5 locales (zh-CN, zh-TW, en, ja, ko) gain presets.gemini and
  geminiModelHint

Test plan:
- cargo test --lib                          200/200 pass
- npm run build (tsc + vite)                green
- Manual: switch to Gemini in Settings,
  paste Google AI Studio key, dictate;
  verify polish reaches v1beta and respects
  the per-model thinking-off contract.

* docs: 2026-05-10 audit + beta logic review snapshots

Snapshot two audit artifacts produced 2026-05-10:

- docs/audit-2026-05-10-validated.md — 21 audit items run against
  the live source tree at openless-all/app/src-tauri/src/ with a
  CONFIRMED / FALSE_POSITIVE / PARTIAL verdict per item plus the
  exact file:line range that supports each verdict.

- docs/logic-review-2026-05-10.md — 5-path logic review of
  origin/beta @ 400097ad covering startup, press/release, end-of-
  session, capsule emit, and shutdown. Notable: at 400097ad the
  emit_capsule fix from PR #389 was NOT yet an ancestor, so the
  cpal audio-callback → AppKit/Win32 SIGTRAP risk was still live
  on that snapshot (subsequent merges have addressed it; this doc
  remains the dated record).

Both files are pure snapshots — no code changes here.

* fix(llm-gemini): buffer SSE bytes across chunks to handle multibyte UTF-8 split

PR #398 pr_agent flagged: reqwest::chunk() can split a multi-byte
UTF-8 codepoint (CJK / emoji) across two network chunks, but the
streaming code decoded each chunk independently with from_utf8 →
half a 你 in the tail of one chunk would surface as
"non-utf8 SSE chunk" and abort the whole stream. This breaks
Gemini QA streaming for any Chinese-language answer that doesn't
happen to align with packet boundaries.

Fix: buffer raw bytes (Vec<u8>) until we see the SSE delimiter
b"\n\n", and only then decode the complete event content as UTF-8.
The delimiter \n\n is two ASCII bytes (0x0A 0x0A) which can never
appear inside a multi-byte UTF-8 sequence, so byte-level splitting
at that boundary is always safe.

Extracted the parsing as drain_complete_sse_events(&mut Vec<u8>) so
the invariant is testable without a TcpListener. Three new unit
tests cover:
- normal split-on-delimiter + remainder buffering
- multibyte UTF-8 split across chunks (the actual regression)
- invalid-UTF-8 event content gracefully skipped (warn, don't crash)

The same byte-level pattern fix applies to polish.rs's
chat_completion_history_streaming — left untouched here to keep
this PR scoped to the Gemini diff pr_agent reviewed. polish.rs
should get the same fix in a follow-up.

* feat(llm-gemini): teach ProviderTools to fetch native Gemini /v1beta/models

Drop the verbose static geminiModelHint paragraph and restore the
normal ProviderTools "Fetch models" button when the active provider
is Gemini. Users now read live model IDs from Google instead of
staring at a wall of i18n text below their model field.

Backend fetch_provider_models now branches on the base URL:
- Google's generativelanguage.googleapis.com → x-goog-api-key auth +
  parse_gemini_model_ids() that walks `models[].name` and strips the
  `models/` prefix so the result is a plain `gemini-2.5-flash` style
  ID directly writable to ark.model_id.
- Anything else → unchanged Bearer auth + OpenAI `data[].id` shape.

Tests: parse_gemini_model_ids_strips_models_prefix_and_dedups +
is_gemini_base_url_matches_official_domain. cargo test --lib green
(32/32 in commands::tests subset).

i18n: settings.providers.geminiModelHint removed from all 5 locales.
The TS `typeof zhCN` constraint enforces the consistent removal.

* fix(llm-gemini): pr_agent #398 follow-ups — filter non-chat models + CRLF SSE delim

Two follow-up fixes for pr_agent advisories on PR #398:

1. parse_gemini_model_ids now filters by `supportedGenerationMethods`.
   Google's /v1beta/models lists every family — embeddings,
   TTS-image, predict-only — alongside generateContent-capable
   chat models. Without filtering, a "Fetch models" click would
   surface ids like `gemini-embedding-2`, and a user picking one
   would write it into ark.model_id and have polish silently fail
   on every dictation. Field is conservatively included when
   missing (some preview models don't expose it yet).

2. drain_complete_sse_events now accepts both `\n\n` and `\r\n\r\n`
   as event delimiters. Some HTTP/2 middleware and CDNs normalize
   line endings to CRLF; the previous LF-only implementation would
   never drain a complete event from those servers and report the
   stream as empty even when data was received. Tied positions
   can't happen by construction (\r\n\r\n contains no \n\n
   substring), so "earliest match wins" is unambiguous.

Tests:
- parse_gemini_model_ids_filters_out_non_generate_content_families
- drain_complete_sse_events_handles_crlf_delimiter
- drain_complete_sse_events_picks_earliest_delimiter_when_mixed

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-10 18:18:24 +08:00
baiqing a74a5472b0 docs: 2026-05-10 audit + beta logic review snapshots
Snapshot two audit artifacts produced 2026-05-10:

- docs/audit-2026-05-10-validated.md — 21 audit items run against
  the live source tree at openless-all/app/src-tauri/src/ with a
  CONFIRMED / FALSE_POSITIVE / PARTIAL verdict per item plus the
  exact file:line range that supports each verdict.

- docs/logic-review-2026-05-10.md — 5-path logic review of
  origin/beta @ 400097ad covering startup, press/release, end-of-
  session, capsule emit, and shutdown. Notable: at 400097ad the
  emit_capsule fix from PR #389 was NOT yet an ancestor, so the
  cpal audio-callback → AppKit/Win32 SIGTRAP risk was still live
  on that snapshot (subsequent merges have addressed it; this doc
  remains the dated record).

Both files are pure snapshots — no code changes here.
2026-05-10 18:13:46 +08:00
baiqing fcb13f5c23 chore: ignore video-materials/ promo recordings
video-materials/ holds ~2.2 GB of promo recordings (promo /
promo-materials / promo-openless / promo-openless-v2 / SC). These
are local working assets that don't belong in the repo. Add to
.gitignore so future stash / commit operations can't accidentally
pull the binary blob in.
2026-05-10 18:13:46 +08:00
TRIPandbaiqing ce21d99f05 feat(settings): hide local ASR behind new Advanced tab + confirm dialog (#400)
* feat(settings): hide local ASR behind new Advanced tab + system confirm dialog

Move local ASR options (local-qwen3 / foundry-local-whisper) out of
the main Providers → ASR dropdown into a new "Advanced" Settings tab,
so first-time users can't accidentally enable CPU inference and get
slower / less-accurate transcription than they expected. Enabling
local ASR now requires reading a strong yellow/amber warning and
confirming a system-level dialog (@tauri-apps/plugin-dialog ask()
with kind:'warning') — the in-page modal pattern wasn't loud enough.

Behavior changes:
- visibleAsrPresets now excludes local-qwen3 and foundry-local-whisper
  (the dropdown is cloud-only)
- useEffect lookup uses ASR_PRESETS (all) so committedAsrProvider can
  still resolve to a local provider when one is active
- When committedAsrProvider is local, the ASR Card replaces the
  dropdown with a plain notice ("Local ASR (X) is currently active —
  switch / disable in the Advanced tab"); existing
  LocalAsrProviderHint rendering for the Card body is preserved
- New AdvancedSection component:
  - Strong warning block (amber background + amber border + amber text,
    fontWeight 500, ⚠️ icon) — meets user's "yellow/red obvious" ask
  - Per-platform engine selector:
      macOS  : Qwen3-ASR only
      Windows: Qwen3-ASR + Foundry Local Whisper
      Linux  : "Local ASR model integration is not supported on this platform"
  - Enable buttons trigger Tauri ask() confirm dialog before switching
  - When already on local: shows a "Disable local ASR" button
    (switches to volcengine, no confirm needed) plus the existing
    LocalAsrProviderHint for model status / download navigation
- SettingsSectionId gains 'advanced'; SECTION_ORDER appends it after
  language so it sits last in the sidebar (matches macOS conventions
  for Advanced/Experimental panes)

i18n: 5 locales (zh-CN, zh-TW, en, ja, ko) gain
- settings.sections.advanced
- settings.providers.localAsrActiveNotice
- settings.advanced.{localAsrTitle, localAsrDesc, localAsrWarningStrong,
  qwen3Desc, foundryDesc, enable, alreadyActive, disableLocalLabel,
  disableLocalDesc, disable, platformNotSupported,
  confirmEnableLocalTitle, confirmEnableLocalBody}

Test plan:
- npm run build (tsc + vite) — green; the typeof zhCN constraint
  catches any locale that drifted out of shape
- Manual: switch Settings tab to "Advanced", verify warning is
  amber-on-amber and clearly visible, click "Enable" on Qwen3-ASR,
  verify a system dialog (not a React modal) appears with kind:warning
  iconography, cancel — verify nothing changed; confirm — verify the
  Providers tab now shows "Local ASR is currently active" instead of
  the dropdown
- Manual on macOS: confirm Foundry option does NOT appear
- Manual on Linux (if available): confirm "platform not supported"
  message renders

* feat(advanced): inline LocalAsr model UI + page-top confirm popup + concise warning

User feedback: the standalone "Models" page (LocalAsr.tsx as a main
nav tab) was redundant once Advanced settings exists, the inline
amber warning was a wall of text, and clicking Enable triggered an
OS-native dialog that didn't feel like part of the app. Refactor
the whole local-ASR surface around what users actually need:

- "Models" main-nav tab removed (FloatingShell + NAVIGATE_LOCAL_ASR_EVENT
  listener gone). Model management is now exclusively reachable via
  Settings → Advanced.
- LocalAsr.tsx gains an `embedded` prop. When embedded, the outer
  page padding/height, PageHeader, and standalone warning Card are
  skipped — the host (AdvancedSection) supplies the chrome.
- AdvancedSection no longer calls @tauri-apps/plugin-dialog ask().
  Enabling local ASR sets pendingTarget which renders a single
  amber-bordered Card at the very top of the section with
  Confirm / Cancel buttons. One box, in-app, dismissable. Disable
  is direct (no confirm needed — going back to cloud is safe).
- Persistent inline warning shrunk to one line. The longer wording
  (including the new "may drop words on under-spec hardware" risk
  the user asked for) lives in the popup body so it's only seen
  when actually consequential.
- ProvidersSection ASR Card body returns null when active is
  local — the dropdown row's localAsrActiveNotice already points
  to Advanced, no need to duplicate manage UI here.

i18n (5 locales): localAsrWarningStrong replaced by
localAsrWarningShort; confirmEnableLocalBody / Title shortened;
new `confirm` key for the popup CTA.

LocalAsrProviderHint and NAVIGATE_LOCAL_ASR_EVENT in Settings.tsx
are now unused but left in place to keep the diff focused; will
clean up in a follow-up commit if pr_agent flags them.

Test plan:
- npm run build (tsc + vite) — green
- Manual on macOS: Settings → Advanced shows one short amber line +
  Enable Qwen3-ASR button + LocalAsr model UI inline. Click Enable
  → page-top popup appears (no OS dialog), with Confirm / Cancel.
  Cancel → no change. Confirm → switches; Disable button appears,
  still single-click no confirm.
- Manual: Providers → ASR with active=local-qwen3 shows only the
  "Local ASR is currently active — switch in Advanced" notice,
  no manage UI.

* fix(advanced): pr_agent #400 follow-up — keep disable accessible on unsupported platform

If a user enables local-qwen3 (or foundry-local-whisper) on a machine
that supports it, then their preferences sync to a Linux/unsupported
machine, the previous Advanced UI rendered only the
"platformNotSupported" message and hid the disable button — they had
no in-app path to switch back to cloud and dictation would fail
silently on every press.

Move the disable SettingRow out of the platformSupported branch.
It's gated only on isOnAnyLocal now, so:
- Supported platform + on local → enable Qwen3/Foundry rows + disable row
- Supported platform + on cloud → enable rows only
- Unsupported platform + on local → "not supported" notice + disable row
- Unsupported platform + on cloud → "not supported" notice only

This is the pr_agent advisory from PR #400 review @ d0a425a1.

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-10 18:06:28 +08:00
Chris233 a47e27e2c5 Merge pull request #396 from H-Chris233/fix/issue-372-correction-rules
支持自定义听写纠正规则
2026-05-10 14:34:28 +08:00
baiqing 0cc3a2412d docs: codify subagent-mediated API research workflow
User-requested mechanism (/init args): when implementing against any
external surface — third-party HTTP APIs, unfamiliar Rust crates / npm
packages, or platform APIs (Apple Security framework / Win32 / Carbon
/ AppKit) — Claude must dispatch a dedicated subagent to fetch
authoritative documentation rather than write integration code from
training memory. The subagent acts as a network-search-and-retrieval
layer that returns distilled, version-aware usage data so the main
context stays uncluttered and the implementation always tracks the
current upstream surface.

Two surgical edits:

1. AGENTS.md §"Third-party service integrations" — heading widened to
   "Third-party service integrations & library / platform API research"
   so the existing 5-step research-first workflow explicitly covers
   library + platform APIs, not just HTTP service integrations.
   Rephrased the lead-in paragraph to enumerate the broader scope.

2. CLAUDE.md — new top-level section "调研先于编码:派子 agent 查 API
   / 库 / 平台文档" pointing back to AGENTS.md as the canonical rules
   and listing the concrete tooling Claude Code has available:
   Context7 MCP (resolve-library-id + query-docs), the
   documentation-lookup skill, and Agent dispatch with
   subagent_type=general-purpose. Specifies trigger conditions,
   non-trigger conditions (when grep is enough), the prompt fields
   every subagent dispatch must include, and the four canonical
   anti-patterns.

No code changes; pure docs.
2026-05-10 11:05:50 +08:00
H-Chris233andOmX 95237d2c49 Let users repair recurring ASR classifier mistakes
Issue #372 reports repeated classifier errors such as 2粒 being intended as 2例. The existing vocabulary hotword path is probabilistic, so this adds a separate deterministic correction-rules store and applies supported literal/{num} rules in the coordinator before polish and before insertion.

The review follow-up keeps the UI refresh path consistent, handles remove failures, rejects rules that would otherwise save as enabled but never run, and fixes the Traditional Chinese wording.

Constraint: Keep correction rules separate from dictionary.json and vocab presets.

Constraint: Only support literal replacement and one {num} wildcard; do not expose arbitrary regex.

Rejected: Add import/export or a new top-level page | outside the approved scheme-C scope.

Rejected: Change history rawTranscript schema | product/data-model decision not approved in this review round.

Confidence: high

Scope-risk: moderate

Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib correction

Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib correction_rule_syntax_rejects_silent_noops

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

Tested: cd openless-all/app && npm run build

Tested: git diff --check

Related: #372

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-10 10:59:07 +08:00
TRIP 225859e18d Merge pull request #395 from appergb/chore/bump-1.2.24-7
chore: bump version to 1.2.24-7 (Beta-7)
2026-05-10 10:53:02 +08:00
baiqing 049147c481 chore(release): sync package-lock.json version to 1.2.24-7
CI ci.yml "Verify version sync across all 5 files" 步骤要求 5 处版本号一致:
package.json / package-lock.json (root + nested) / tauri.conf.json /
Cargo.toml / Cargo.lock 的 [openless] 包。上一个 commit (568d30d) 漏改了
package-lock.json 两处 → Linux + macOS CI fail。

修法跟 PR #383 后续 commit 941542c6 同形:手动同步两处 'version'
字段到 1.2.24-7。下次正确流程是用 scripts/bump-version.sh 一把过。
2026-05-10 10:42:20 +08:00
baiqing 568d30d079 chore: bump version to 1.2.24-7 (Beta-7)
10 PRs landed on beta this cycle:
- #377 paste shortcut configurable (issue #360)
- #386 TS UserPreferences updateChannel alignment
- #387 focus_target leak on Processing-phase cancel
- #388 [严重] MacHotkeyAdapter::shutdown stops CFRunLoop + tap
- #389 emit_capsule window.show/hide off audio thread
- #390 QA / dictation hotkey routing race
- #391 audio-mute spawn_blocking (async hygiene)
- #392 hotkey supervisor + global dispatcher exit signal
- #393 post-audit logic-review hotfixes (QA mute .await + focus_target Processing branch)
- #394 in-process credentials cache (kills repeated Keychain prompts)

Bump 4 files: package.json, tauri.conf.json, Cargo.toml, Cargo.lock.
2026-05-10 10:34:08 +08:00
TRIP 0f7961963b Merge pull request #394 from appergb/fix/keychain-cache
fix(vault): cache CredsRoot in process to stop repeated Keychain prompts
2026-05-10 10:12:27 +08:00
baiqing 8a9fb4f4f8 fix(vault): don't cache fallback creds when keyring read fails
pr_agent on PR #394 flagged a "Stale cache on read error" issue:
both load_credentials / load_credentials_for_update were
unconditionally writing the result into CREDENTIALS_CACHE, including
the legacy-file fallback returned when load_keyring_credentials() hit
an Err (e.g. user hasn't yet clicked "Always Allow" on the first
keychain dialog, login keychain locked, DataProtection error).

Effect: after a transient keyring failure the process pinned the
fallback (or default) creds in cache for its entire lifetime — even
after the user granted access, subsequent reads would never retry
the keyring and would keep returning stale defaults / legacy content
until the app is restarted.

Fix: cache only on the Ok(Some) and Ok(None)→migrate paths. The Err
path now returns the legacy fallback without populating the cache, so
the next call retries the keyring and picks up the user's just-granted
access.

185/185 lib tests pass; cargo check clean.
2026-05-10 10:01:00 +08:00
baiqing 2dc7ac59cf ci: re-trigger pr_agent after PR description cleanup 2026-05-10 09:41:07 +08:00
baiqing b0da8e01b9 fix(vault): cache CredsRoot in process to stop repeated Keychain prompts
Without an in-process cache, every CredentialsVault::get_* /
get_active_asr / snapshot call drove load_credentials() →
load_keyring_credentials(), which reads the manifest entry plus every
chunk entry from the OS keyring. On macOS each distinct keychain entry
has its own ACL — so an ad-hoc-signed binary (or any binary whose ACL
grants haven't yet been "Always Allow"-ed) prompts the user on every
read of every entry. A single dictation cycle reads credentials 5–10
times, multiplied by (1 manifest + N chunks), produces tens of
"OpenLess wants to use the keychain" prompts per recording.

Add a process-wide CREDENTIALS_CACHE: OnceLock<Mutex<Option<CredsRoot>>>:
- load_credentials / load_credentials_for_update consult the cache
  before falling through to Keychain. First read populates the cache;
  every subsequent read in the same process is silent.
- save_credentials populates the cache with the cleaned root after
  successful Keychain write, keeping Settings → Recording credential
  edits visible immediately to dictation.
- migrate_legacy_sources{,_for_update} call save_credentials internally
  so legacy-migration paths inherit the cache update for free.

Trade-off: cross-process changes (e.g. user runs `security` CLI
manually, or a second instance of the app — single-instance is enforced
but defense in depth) are invisible until next launch. Acceptable per
the credential vault contract: the keyring is owned by this app.

Note: this does NOT fix the underlying problem that ad-hoc-signed
builds change cdhash on every rebuild and lose "Always Allow" grants.
For that the build must use a stable Apple Developer ID signature
(APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / APPLE_TEAM_ID env
vars). The cache only ensures that within a single process lifetime,
the user is prompted at most once per Keychain entry.

185/185 lib tests pass. cargo check clean.
2026-05-10 09:23:29 +08:00
TRIP 618fc7f0b1 Merge pull request #393 from appergb/fix/post-audit-logic-review
fix(coordinator): post-audit logic-review hotfixes
2026-05-10 09:11:20 +08:00
baiqing d40b08ab44 fix(coordinator): post-audit logic-review hotfixes
Two real bugs surfaced by the 2026-05-10 end-to-end logic review of
beta after the 8-PR audit-fix wave landed.

1. coordinator.rs:2313 — `acquire_recording_mute(inner, "qa");` was
   missing `.await`. PR #391 (`6171df61`) made the function `async fn`
   and updated the dictation call site at coordinator/dictation.rs:451,
   but the QA call site was missed. Effect: when a user has
   `mute_during_recording = true` and triggers QA via Right Option, the
   returned Future is dropped on the next line, `spawn_blocking` is
   never scheduled, holders never increments, and system audio is NOT
   muted (e.g. YouTube playback continues into the QA recording). Both
   the matching `release_recording_mute(inner, "qa")` calls become
   no-ops (early return at holders == 0). The compiler was emitting
   `unused_must_use` for this site (verified before this commit).
   Fix: add `.await`.

2. coordinator/dictation.rs:843-849 — PR #387 (`ce82fcd9`) was framed
   as "clear focus_target on cancel regardless of phase", but the only
   code path that gained the unconditional clear was
   `finish_cancel_session_state`. cancel_session deliberately skips
   that helper for Processing (so end_session can drive its own
   teardown), and end_session's "ASR-finished, cancelled" exit at
   dictation.rs:843-849 set phase=Idle but never touched focus_target.
   Result: cancel-during-Processing leaves a stale `usize` focus slot
   in state.focus_target until the next begin_session_state overwrites
   it. Today the leak is bounded (no documented reader between cancel
   and next begin), but it violates PR #387's stated contract and is a
   silent footgun for any future reader of focus_target on that
   interval. Fix: in the cancelled-after-ASR exit branch, take the
   state lock once and clear both phase and focus_target together.

Source: docs/logic-review-2026-05-10.md (subagent end-to-end review).
185/185 lib tests pass; cargo check clean (unused_must_use gone).
Manual verification checklist for both fixes in the review doc.
2026-05-10 08:42:23 +08:00
TRIP 892f3c4bfe Merge pull request #389 from appergb/fix/audit-emit-capsule-mainthread
fix(coordinator): marshal capsule show/hide to main thread [高]
2026-05-10 08:41:01 +08:00
baiqing e45860538d ci: re-trigger pr_agent after PR description cleanup (no code change) 2026-05-10 08:24:04 +08:00
TRIP 400097adbf Merge pull request #391 from appergb/fix/audit-async-hygiene
fix(coordinator): wrap audio-mute shellouts in spawn_blocking [中]
2026-05-10 08:22:53 +08:00
TRIP ff5e8f8ee1 Merge pull request #377 from appergb/fix/issue-360-paste-shortcut
fix(insertion): make Windows/Linux paste shortcut configurable (#360)
2026-05-10 08:10:15 +08:00