Commit Graph
866 Commits
Author SHA1 Message Date
aeoformandClaude Opus 4.7 088e2ab3d9 fix(ci): derive RPM fcitx5 plugin path from cmake detection
Map Debian multiarch addon dir to /usr/lib64 for RPM packages.
Uses FCITX_RPM_ADDON_DIR derived from cmake output instead of
hardcoded paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:38:21 +08:00
aeoformandClaude Opus 4.7 60250c839d fix(linux): support custom combo hotkeys and surface fcitx5 errors
- Add SetCustomDictationTrigger(s) to plugin for custom key combos
- Add fcitx5 availability check before hotkey install, surface error
- Sync custom dictation combo to fcitx5 plugin from coordinator

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:32:12 +08:00
aeoformandClaude Opus 4.7 323dbed401 fix(linux): add QA and translation modifier hotkey support to fcitx5 plugin
Extend the fcitx5 plugin with SetQaHotkeyRaw/SetTranslationHotkeyRaw
DBus methods and corresponding signals, so that QA panel toggle and
translation modifier hotkeys work on Linux (not just the main dictation
hotkey). Built-in Shift key translation modifier also handled by the
plugin. Document DBus security model in plugin header.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:21:38 +08:00
aeoformandClaude Opus 4.7 b349a4dcba fix(linux): remove dead rdev code and leftover Wayland references
Remove 290 lines of unused rdev listener code from the Linux hotkey
platform module, drop the rdev dependency from Cargo.toml, and clean up
capsule_window_visible init leftover. Update doc comments and frontend
i18n to reflect the unified fcitx5-only path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:01:56 +08:00
aeoformandClaude Opus 4.7 acfdd39481 fix(linux): unify on fcitx5 for both Wayland and X11, drop enigo
fcitx5 插件 Wayland/X11 都可使用,不再为 X11 单独维护 enigo XTest。
Linux 统一走 fcitx5 CommitText 直写,插件不可用时降级到剪贴板拷贝。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:53:06 +08:00
aeoformandClaude Opus 4.7 dbae3b7a5f fix(linux): correct fcitx5 InputContext::destroyed signal connection syntax
ic->connect(ic->destroyed, ...) is invalid — InputContext has no two-arg
connect overload. Use ic->destroyed.connect(callback) directly, which
calls Signal<void()>::connect. Also simplified ScopedConnection storage
from unique_ptr to direct member (ScopedConnection is move-assignable).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:53:06 +08:00
aeoformandClaude Opus 4.7 07132c2087 fix(linux): track fcitx5 InputContext lifetime via destroyed signal
Prevents savedIc_ from becoming a dangling pointer by connecting to the
InputContext::destroyed signal when the IC is saved. On destruction the
pointer is cleared automatically, and commitText falls through to
foreachFocused for the current focused IC.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:53:06 +08:00
aeoformandClaude Opus 4.7 85c2cafcad fix(ci): use cmake-determined plugin install path in deb
FCITX_INSTALL_ADDONDIR varies by distro (multiarch Ubuntu →
/usr/lib/x86_64-linux-gnu/fcitx5/; Fedora → /usr/lib64/fcitx5/;
Arch → /usr/lib/fcitx5/). Extract from cmake cache at build time
via GITHUB_ENV instead of hardcoding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:53:06 +08:00
aeoformandClaude Opus 4.7 dfced4ff1d fix(linux): sync fcitx5 plugin hotkey binding on update
When ensure_modifier_hotkey_monitor finds an existing monitor and
updates the rdev/CGEventTap binding, it must also sync the new binding
to the fcitx5 plugin on Wayland. Previously the early return skipped
this, leaving plugin and coordinator out of sync after a hotkey change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:53:06 +08:00
aeoformandClaude Opus 4.7 4f1e457cfa feat(ci): build fcitx5 plugin into Linux deb/rpm packages
- Add "Build fcitx5 plugin" step to release-tauri.yml: compiles
  the C++ plugin via cmake, copies .so + .conf to
  src-tauri/linux-fcitx5-plugin/ for the Tauri bundler.
- Override tauri build --config to include deb.files / rpm.files
  that place the plugin at /usr/lib/fcitx5/ so it's auto-detected
  after install.
- Add fcitx5, fcitx5-module-dbus as deb/rpm dependencies.
- Local builds unaffected (config is CI-only via --config flag).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:53:06 +08:00
aeoformandClaude Opus 4.7 a1b7a2a1f9 fix(linux): cfg-gate linux_fcitx module + skip capsule window on Wayland
- Add #[cfg(target_os = "linux")] to mod linux_fcitx to fix macOS/Windows
  CI compilation (dbus crate is Linux-only).
- Wayland: skip capsule window show/hide entirely in emit_capsule so the
  target app never loses keyboard focus. Text is committed via fcitx5
  plugin commit_string — no window means the compositor forwards the
  commit to the right app.
- X11 keeps existing behavior (show capsule window once per session).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:53:06 +08:00
aeoformandClaude Opus 4.7 2912aefed5 feat(linux): replace enigo with fcitx5 plugin for Wayland input
- fcitx5 C++ plugin (scripts/linux-fcitx5-plugin/) with DBus interface:
  CommitText, SetHotkey/SetHotkeyRaw, DictationKeyEvent signal
- linux_fcitx.rs: DBus client to call plugin from Rust
- coordinator/dictation.rs: Wayland insertion via fcitx5 commit_text,
  streaming insert enabled on Wayland
- insertion.rs: fcitx5 commit_text on Linux with clipboard fallback
- unicode_keystroke.rs: Linux path uses fcitx5 commit_text
- Capsule window show-once on Linux to avoid stealing focus

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 08:53:06 +08:00
TRIP e8d0ccf303 Merge pull request #449 from Open-Less/chore/marketplace-disable-stable
chore(stable): disable marketplace entry + strengthen ASR correction rules
2026-05-15 22:44:48 +08:00
baiqing 461a4f3fa7 fix(prompt): keep '5) 自动纠错' prefix so guard test still matches
CI 测 polish::tests::common_rules_include_auto_correction_and_natural_organization
hard-codes 'prompt.contains("5) 自动纠错")' 作为 guardrail;上一 commit 把前缀
改为「ASR 主动纠错」导致 4 个 mode 都失败。前缀回到「自动纠错」,新增说明
夹在括号里:'5) 自动纠错(ASR 主动纠错,按置信度分级处理)'。
2026-05-15 22:31:33 +08:00
baiqing 2b6f478018 chore(stable): disable marketplace entry + strengthen ASR correction rules
Marketplace entry → 灰色 pill + 点击 toast「暂时未开放」
- 风格市场云端服务尚未上线,先在 UI 层禁用入口
- 真正功能(Marketplace 组件 / IPC / backend client / MarketplaceModal)全部保留
- 后续云端就绪时改回 onClick={() => setMarketplaceOpen(true)} 即可恢复
- 配色从蓝色 pill 改为灰色,明确视觉禁用语义

ASR 主动纠错升级(吸收「完全重写」社区 prompt 的优点)
- types.rs::COMMON_RULES 规则 5 重写:
  - 三级置信度策略(高 → 直接换;中 → 最优候选;低 → 保留)
  - 中文音译 → 英文技术词还原(脱肯/西克瑞特/埃克塞斯 Token...)
  - 技术字段大小写规范化(API/App ID/Access Key/Secret Key/OAuth/JWT/UUID 等 12+ 字段)
  - 大小写敏感场景例外(代码变量 / Bash / 路径 / URL 段保留)
- 新增规则 6:禁止输出修改说明 / 原文对比 / 编造字段,所有模式无例外
- 4 个 builtin pack(Raw/Light/Structured/Formal)共享 COMMON_RULES 自动获益
2026-05-15 22:18:58 +08:00
Chris233 1ab231df65 Merge pull request #446 from H-Chris233/fix/issue-440-streaming-default-migration
Keep streaming insertion enabled for migrated prefs
2026-05-15 19:30:48 +08:00
baiqing 3cedb72a1d feat(marketplace): derivative version + like toggle + my-pack management + UX polish
客户端配套 backend marketplace 更新:

衍生版本链路:
- StylePack 新增 originPackId / originAuthorLogin(仅 set_origin 通道写入,
  普通 save 路径不会清掉)
- marketplace_install 拉 detail → 安装后写 origin 绑定
- marketplace_upload 把 originPackId 塞到 multipart;本地原创首发后回写自己
  的远端 id 当 origin,让同设备后续走 supersede
- 卡片 / 详情 / 本地列表都加「衍生自 @<author>」绿色 pill;过滤掉自己 != 衍生

点赞 + 我发布的:
- 新增 marketplace_my_likes IPC(GET /me/likes)→ likedIds Set
- 心形 ♥/♡ + 红色已赞 + spring 缩放动画
- 排序栏第 3/4 项「我赞过的」/「我发布的」客户端 filter

发布流程:
- 编辑器 dirty 时自动 save 再 upload,不再阻塞
- 内置 pack 双层禁发(按钮 disabled + handler early return)
- 上传 picker 过滤 builtin
- 失败 toast 6s 自动消失
- Style 页 toast 改右下角,避开「导入 ZIP」按钮

风格市场弹窗:
- 整合到 Style 页面 modal(左侧 nav 去掉 marketplace tab)
- 右上 32×32 圆角 X 按钮;左上「@<login>」登录身份 pill
- 内容区右下角小 toast,从右滑入 / 停留 / 向右滑出
- 详情页右下「🗑 撤回发布」(仅作者可见,confirm 后 DELETE /packs/:id)
2026-05-15 19:14:46 +08:00
H-Chris233 b0c469dbd4 Keep streaming insertion enabled for migrated prefs
Older builds could persist the old default streamingInsert=false into preferences.json, which is indistinguishable from a user opt-out if the value is treated as an ordinary bool. Add a one-time migration marker, persist normalized legacy prefs on load, and keep later user opt-outs durable after migration.

Constraint: issue #440 requests streaming input/output defaults on for fresh installs and updates, plus release notes for the behavior change.

Rejected: treating any stored false as manual opt-out | old default writes would keep update users off forever.

Rejected: overriding every false forever | would erase a user's post-migration manual opt-out.

Confidence: high

Scope-risk: moderate

Directive: Keep UserPreferencesWire serde defaults and migration markers aligned with persisted UserPreferences fields.

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

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

Tested: npm run build

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

Tested: git diff --check

Not-tested: cargo fmt --check fails on pre-existing unrelated formatting drift in volcengine.rs and commands.rs.
2026-05-15 18:59:45 +08:00
TRIP 74208322bb Merge pull request #444 from Open-Less/feat/marketplace-tab
feat: marketplace tab + 一键发布 + Settings 风格市场配置
2026-05-15 15:00:34 +08:00
baiqing 9e119e2a2f fix(marketplace): stale detail response guard (pr_agent #444 round 3)
跟 round 2 同类 race condition:openDetail 用户快速点两张卡片时,先点 A 的
detail response 可能晚于 B 到达 → 覆盖 B 的 detail / Install / Like 作用错对象。

修:复用 useRef seq token 模式,detailSeqRef 单调递增,response 比较 seq 丢
弃 stale。逻辑跟 refresh() 完全对称。
2026-05-15 14:52:22 +08:00
baiqing 4af7fec82f fix(marketplace): UUID-v4 whitelist on pack_id + stale-response guard (pr_agent #444 round 2)
pr_agent 提的两个真问题:

1. Security: arbitrary file write —— marketplace_install 用 backend-controlled
   pack_id 拼临时文件路径。若 backend 被攻陷返回路径遍历 id,可写客户端任意位置。
   修:4 个 marketplace_* IPC 全部加 is_valid_session_id() UUID-v4 白名单
   校验(跟 read_audio_recording 同套 boundary 校验)。

2. Race condition: 搜索 refresh 旧请求晚到覆盖新结果。用户连续输入时体验差。
   修:useRef 单调递增 seq token,response 到了 check seq 是否最新,stale 直接丢。

cargo test 263 全过;tsc 0 error。
2026-05-15 14:48:05 +08:00
baiqing 242f7ef8cd feat(marketplace): one-click publish + Settings marketplace config (goal 2.e)
Goal 2.e 一键发版到云端:
- Style.tsx 详情页加「发布到风格市场」按钮(在导出 ZIP 旁),
  调既有 marketplace_upload IPC。disabled 直到用户在 Settings 配 GitHub login。
- Settings.tsx 加「风格市场」折叠区:marketplaceBaseUrl + marketplaceDevLogin
  两个 input。base URL 空 = localhost:8090 dev 默认;生产填 https://api.<domain>。
- 5 语言 i18n(zh-CN/en/ja/ko/zh-TW)补全 marketplace 相关 settings 文案

cargo test 263 全过;tsc 0 error。
2026-05-15 14:25:51 +08:00
baiqing c9c126730c feat(marketplace): Phase A — 客户端 Marketplace tab (Goal 1)
围绕 user goal 1 (a-e) 实现:
- (a) 后端验证:5 个 marketplace_* IPC,HTTP 通过 reqwest 调 backend
- (b) 上传与拉取无问题:marketplace_install / marketplace_upload 复用本地 ZIP IPC
- (c) 单独弹窗:详情 + 上传选包器走 Modal 半透明背景中央卡片
- (d) 搜索框:顶部 input 300ms 防抖 + server-side ?q= 过滤
- (e) 按排名推荐:默认 sort=popular(按 like_count DESC)

后端 IPC(commands.rs):
- marketplace_list / detail / install / upload / like
- HTTP base URL 走 prefs.marketplace_base_url(默认 http://127.0.0.1:8090)
- dev-mode auth 走 prefs.marketplace_dev_login → X-Dev-User header
- install 路径:下载 ZIP 到 temp → 调既有 import_from_zip → 删 temp
- upload 路径:export 本地 pack → multipart POST → 后端入审核队列

UserPreferences 加两个字段(5 处 wiring:struct/Wire/Default×2/Deserialize):
- marketplace_base_url (空 = localhost:8090 默认)
- marketplace_dev_login (空 = 上传按钮 disabled)

前端:
- types.ts 加 MarketplaceListItem / MarketplaceDetail
- ipc.ts 加 5 个 wrapper + mock fixture(vite dev 模式仍可演示 UI)
- pages/Marketplace.tsx (~350 行) UI:搜索 / 排序 toggle / 卡片 grid / 详情弹窗 / 上传选包器
- FloatingShell.tsx + state/useAppState.ts 加 marketplace AppTab
- 5 语言 i18n 补全 nav.marketplace + marketplace.* 子树(en/zh-CN/zh-TW 完整,ja/ko nav 本地化 + 子树 fallback en)

cargo test 263 全过;tsc 0 error。
2026-05-15 14:02:55 +08:00
baiqing efa94afc27 chore(release): bump version to 1.3.2-3 (Beta) 2026-05-15 12:35:02 +08:00
TRIP ec93a4a87a Merge pull request #443 from Open-Less/fix/polish-network-retry
fix(llm): retry transient network errors once before failing polish
2026-05-15 12:33:30 +08:00
baiqing 55c0c64671 fix(llm): don't retry on timeout to avoid duplicate billing (pr_agent #443 round 2)
pr_agent #443 review 指出 Duplicate Request 风险:

> Retrying after send() fails can submit the same LLM request twice if the
> first attempt already reached the provider but the connection dropped
> before a response was returned. For non-idempotent completion calls, that
> can mean duplicate billing and two completions for one user action.

事实分析 reqwest::Error 各 variant:
- is_connect()  → TCP 握手没建立,server 不可能收到 → 安全 retry
- is_request()  → HTTP 请求层错误(构造问题),server 没收到完整请求 → 安全 retry
- is_timeout()  → client 设置的 timeout 到了,server 可能已经收到并在处理
                  (非幂等 completion 调用)→ 不安全 retry,会重复 billing

修法:从 retry 触发条件移除 `|| e.is_timeout()`。timeout 直接返回
LLMError::Timeout,不再重试。

user 实际看到的失败模式是 "error sending request"(reqwest::is_connect),不在
被移除范围内,覆盖率不变。

注:pr_agent 同轮提的 "Ticket compliance ❌ Not compliant 442" 是 false positive
—— pr_agent 把 PR description 里提到的 #442 当成本 PR 的 ticket id;#442 是另一
个独立 PR(A-D 默认值 / 提示词重构),已 merge。

cargo test 263 全过。
2026-05-15 11:43:39 +08:00
TRIP 1f5051d8e0 Merge pull request #442 from Open-Less/fix/streaming-default-prompt-restructure
feat: streaming-on by default, structured default, agent-style hotword block middle
2026-05-15 11:41:14 +08:00
baiqing eb5430dc4a fix(llm): retry transient network errors once before failing polish
用户反馈日志显示 streaming polish 偶发失败:
  [coord] streaming polish FAILED: network error:
          error sending request for url (https://api.deepseek.com/v1/chat/completions)

诊断:失败发生在 reqwest `request.send().await` 阶段(不是 HTTP 4xx/5xx),
属于 connect / request / timeout 三类 transient 错误。同一 session 内大量
polish 调用成功,证明网络没断、是间歇性抖动。

修法:抽 `send_with_transient_retry` helper,首次失败 + 错误是 transient
(is_connect / is_request / is_timeout)→ sleep 500ms 重试一次。retry 第二次
失败按原 LLMError::Timeout / Network 返回。HTTP 4xx/5xx 走 response.status()
分支不受影响。

适用范围:3 处 reqwest send 都用 helper:
- chat_completion_messages_streaming (SSE 流式,line 720)
- chat_completion_messages_streaming 的非流式兄弟 (line 591)
- send_chat_request 一次性 chat 调用 (line 517)

retry 安全前提:传入 RequestBuilder body 必须是内存型(json/form),用
`try_clone()` 复制;3 处都满足。对流式路径 retry 安全是因为失败发生在 SSE
开始前 → on_delta 必然未被调用 → 不会重复输出。

注:Codex OAuth 路径(line 1085)使用独立 client,结构不同,本 PR 不动;
后续如有相同抖动反馈再扩展。

cargo test 263 全过。
2026-05-15 11:37:34 +08:00
baiqing 0db0ce4e5e feat: streaming-on by default, structured default mode, agent-style hotword block in middle
围绕 user 反馈(用户主动测试后给的迭代清单)做 4 项调整:

A. streaming_insert 默认 false → true
   - 流式落字感知延迟低,所有 fallback case 都已经接好
   - CJK IME / Codex / Gemini provider 自动回落到一次性路径,对存量用户无感
   - 影响 mock + test fixture 一并更新

B. default_mode: PolishMode::Light → PolishMode::Structured
   - 新装用户开箱默认走清晰结构 mode,对多事项口述场景更合适

C. 热词与纠错模块从「prompt 末尾」移到「ROLE_BLOCK 之后」
   - 新增 types.rs::HOTWORDS_PLACEHOLDER = "{{HOTWORDS}}"
   - default_style_system_prompt_for_mode 拼接顺序:
     ROLE_BLOCK + {{HOTWORDS}} + task_and_example + COMMON_RULES + OUTPUT_BLOCK
   - polish.rs::build_hotword_block 统一生成 agent-style 错别字纠正 + 热词列表文本
     (明确告诉模型「转写来自 ASR 可能含错别字」,比旧的「热词:...」措辞更强)
   - polish.rs::compose_system_prompt 找占位符替换;用户自定义 prompt 无占位符 + 热词
     非空 → 末尾追加(兼容历史);空热词 → 原 prompt 不变(保留旧测试断言)
   - preview 跟 system prompt 用同一段文本,消除「设置看一段,发给 LLM 是另一段」不一致

D. Style Pack 新建预填示例 prompt
   - Style.tsx 新增 NEW_PACK_PROMPT_TEMPLATE 含 # 角色 / {{HOTWORDS}} / # 任务 /
     # 通用规则 / # 输出 4 段结构,用户照着改即可
   - 让用户看到 placeholder 怎么用,决定是否保留/移动/删除

cargo test 263 全过;tsc 0 error。

测试反馈:本地构建 1.3.2-2 装到 /Applications 跑通 A-D 全部功能,无回归。
2026-05-15 11:26:49 +08:00
TRIP e297bfdd6e Merge pull request #438 from Open-Less/fix/volcengine-403-msg
fix(asr): surface Volcengine 403 as 'credentials rejected'
2026-05-15 09:45:56 +08:00
baiqing 1234dcd9c0 fix(asr): shorten 403 message to '凭据被拒(403)'
原文案带 Settings 引导 + SAUC 资源说明,capsule 上显得拥挤。简化到 5 个字 + 状态码,原因留在错误类型 doc comment 里。
2026-05-15 09:28:07 +08:00
Chris233 b792caabf9 Merge pull request #437 from H-Chris233/issue-420-wayland-p1
Prevent Wayland dictation text loss
2026-05-15 09:23:06 +08:00
baiqing 5c9ebe1930 fix(asr): surface Volcengine 403 as 'credentials rejected' instead of raw HTTP error
用户反馈"豆包云端语音连接不了",日志显示 9 次相同的:
  [coord] open ASR session failed: connection failed: HTTP error: 403 Forbidden

原因不是网络不通(网络通到 Volcengine 才会有 HTTP 状态码),而是凭据被拒:
App ID / Access Token 错、或账号没开通 SAUC bigmodel 资源。当前 capsule
显示原始 `HTTP error: 403 Forbidden`,用户看不懂、客服需要解释一遍。

修法:在 volcengine.rs 加 classify_connect_error 函数,握手收到 401/403 时
转成新的 VolcengineASRError::AuthRejected(status) variant;它的 thiserror
display 直接是中文「凭据被拒(HTTP 403):请检查 Settings → 凭据 → Volcengine
的 App ID 和 Access Token,或确认账号已开通 SAUC bigmodel 资源」。
coordinator 沿用原有错误透传路径,capsule 文案自动从英文变中文具体引导。

其它握手错误(DNS / TLS / connection refused)仍走 ConnectionFailed,文案不变。

cargo test 258 全过。
2026-05-15 09:20:43 +08:00
baiqing 5695d8c467 chore(release): bump version to 1.3.2-2 (Beta) 2026-05-15 08:42:08 +08:00
TRIP a0c239a716 Merge pull request #436 from Open-Less/feat/voice-quality-audio-archive
feat: polish hotword overrides, audio archive, auto-update check
2026-05-15 08:40:35 +08:00
H-Chris233 c4cfad3632 Align Wayland trigger guidance
Wayland now uses CLI-triggered dictation and copy-only output safety, so user-visible Linux hints should not imply that Wayland streaming insertion or global hotkeys are still attempted inside OpenLess.

Constraint: Wayland trigger support is currently delivered through desktop shortcuts invoking openless CLI flags.

Rejected: Leaving old best-effort Wayland wording in Settings | It conflicts with Phase 1 copy-only behavior and the CLI trigger path.

Confidence: high

Scope-risk: narrow

Directive: Keep future portal/libei language in research docs, not current-product Settings hints.

Tested: npm run build; cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml; git diff --check

Not-tested: Live Wayland Settings rendering on GNOME/KDE/Hyprland/sway.
2026-05-15 07:44:44 +08:00
H-Chris233 5fd991ae1f Prevent Wayland dictation text loss
Wayland cannot prove synthetic typing or paste delivery, so Linux Wayland now bypasses streaming success semantics and keeps completed dictation text in the clipboard with explicit fallback status. The Phase 1 plan is recorded at repo root to keep follow-up work aligned with the current CLI trigger decision.

Constraint: Wayland security model does not provide X11-style global key or cross-app text injection guarantees.

Rejected: Treating enigo or simulated paste success as inserted on Wayland | It can report success while the target app receives no text.

Confidence: high

Scope-risk: narrow

Directive: Keep Wayland automatic input as an explicitly probed future enhancement; do not silently restore fake insertion success.

Tested: cargo fmt --manifest-path openless-all/app/src-tauri/Cargo.toml; cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib coordinator::dictation; cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib focus_restore_failure_uses_specific_error_code; git diff --check; cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml

Not-tested: Real Wayland compositor clipboard behavior and Windows TSF runtime behavior on native hosts.
2026-05-15 07:41:08 +08:00
baiqing 3125de88fb fix: normalize TOCTOU NotFound in read_audio_recording (round 3)
pr_agent round 3 提的 Missing-file handling 兜底:

read_audio_recording 在 exists() 检查后到 tokio::fs::read 之间,文件可能因
prune(条数 cap / retention 清理 / 用户手动删)而消失。原实现把 NotFound 错
格式化成 'read wav failed: No such file...' 这种 OS 原文,前端字符串匹配
'recording not found' 接不住,UI 显示一次 generic error 而不是隐藏按钮。

修法:read 失败时显式判 ErrorKind::NotFound → 返回跟 exists() 失败同样的
'recording not found' 字符串。前端单条 catch 就稳,不依赖本地化 OS 错误。

注:pr_agent 同轮提的 "Build break"(arch.lock() 返回 Result)是 false positive
—— recorder.rs 用 parking_lot::Mutex 而非 std::sync::Mutex,.lock() 直接返回
MutexGuard 无 Result。本地 cargo test 258 全过、3 平台 CI build 都绿可证。

cargo test 258 全过。
2026-05-14 21:31:47 +08:00
baiqing 67d1277361 fix: address pr_agent Stale closure + Missing file check (round 2)
pr_agent 二轮 review 又提了 2 个 issue,本 commit 解决:

Stale closure (AutoUpdateGate.tsx)
- 原 useEffect 依赖 [enabled],tick 闭包捕获渲染时的 u;u.status 变化时 tick
  读不到新值,极端时序下 60min interval 触发的 tick 可能在用户已经手动打开
  UpdateDialog 的情况下错过 busy 检查,并发触发第二次 checkForUpdates。
- 修:useRef(u) + 每次渲染同步 uRef.current = u;tick 读 uRef.current 始终拿
  最新 useAutoUpdate 返回值,避免 stale 状态判断。

Missing file check (History.tsx)
- hasAudioRecording 只代表「录音时 wav 写盘成功」,但 audioRecordingMaxEntries
  / historyRetentionDays 之后 prune 可能已删 wav。前端无条件渲染按钮 → click
  → 后端返回 'recording not found'。属于 UX 不优雅(功能有 error UI 兜住)。
- 修:父组件加 audioMissingIds: Set<string>;AudioRecordingPlayer 加 onMissing
  回调,遇 'not found' 错误时调用父让 id 加入 set;导出按钮 catch 同样错误
  也 mark missing;按钮组渲染条件改为 hasAudioRecording && !audioMissingIds.
  has(id),一次点击后永久隐藏,不再让用户撞同样的 error。

cargo test 258 / tsc 0 error。
2026-05-14 21:15:18 +08:00
baiqing 13839a2fd4 fix: address pr_agent Wrong Flag + Missing Audio review
pr_agent review #436 提了两个跟 has_audio_recording 字段相关的 issue,本 commit
全部修复:

Wrong Flag (dictation.rs:1551 polish 收尾路径)
- 原实现:has_audio_recording: Some(prefs.record_audio_for_debug)
  以「开关是否打开」当真值。但 Recorder::start 内部 WavArchiver::create 可能失败
  (recordings/ 创建不出 / 权限不足 / 磁盘满),开关 on 但 wav 没真写盘时,前端会
  渲染播放按钮、点击得到 'recording not found'。
- 修法:Recorder::start 返回值加第三个 bool = archive_active。Inner 加
  audio_archive_active: AtomicBool 字段,begin_session 时写入。history 写入路径
  读这个字段而不是 prefs,反映真实写盘状态。

Missing Audio (dictation.rs:1233 empty-transcript 失败路径)
- 原实现:has_audio_recording: None,即使开关 on 也强行 None。但磁盘上 wav 仍存在
  (recorder 已走完整段录音)。讽刺的是:ASR 没识别到文字恰好是用户最想听原始录音
  诊断的场景。
- 修法:empty-transcript 路径同样用 inner.audio_archive_active.load(...)。
  与 polish 路径一致。

副作用最小:
- SessionState 不动(不污染 10 个构造点)
- 改 Inner 加一个 AtomicBool(2 个构造点已补)
- Recorder::start 返回元组扩第 3 项;3 个调用点(dictation/QA/preview)都已更新

cargo test 258 全过。
2026-05-14 20:56:05 +08:00
baiqing 366f6c38eb feat: polish hotword overrides, audio archive, auto-update check
围绕"ZIP 误识别 + AI 日报反馈"一波集中改动,共 7 项:

polish 提示词
- COMMON_RULES 规则 2 加热词例外(热词列表正确写法优先于"原样保留")
- 规则 5 自动纠错补英文短词同音示例(ZIP/VIP)
- 规则 2 显式举例带次版本号产品名(GPT-5.6 / Claude 4.7 / iOS 26.1)不省略小数
- Structured mode 主题数上限放宽给"多条独立新闻/日报"场景
- Structured mode 要求主题标题保留关键实体名
- 热词块尾部加「优先于规则 2」强约束

History 详情面板
- 修复复制按钮没 await 没反馈的 bug(1.5s 显示"已复制")
- 加 AudioRecordingPlayer(按需加载 wav → Blob → 原生 audio controls)
- 加导出录音按钮(anchor.download 触发浏览器下载)

Settings 新增
- 自动检查更新开关(默认 on,启动延 4s + 60min interval)
- 历史条数上限输入框(5–200,留空 = 200)
- 保留原始录音开关 + 录音条数上限输入框

录音归档(debug 开关版)
- recorder.rs WavArchiver(Drop 时回填 RIFF/data header)
- DictationSession.has_audio_recording 字段;history.id 与 SessionId 对齐
- persistence.rs 加 recordings_root / recording_path_for_session
- prune_recordings 双重 cap(days + count),仅扫 .wav,metadata 失败按过期处理

IPC 安全 + 性能
- read_audio_recording 改 async + tokio::fs::read 防阻塞主循环
- session_id 白名单 UUID-v4 字面校验 + 2 个单测覆盖路径越界

后续待优化(review 标记 H2 / M1 / M3):
- prune 在录音前调用,cap 边界少裁 1 条(最多多 1 个文件,不爆盘)
- WavArchiver Drop unwinding 时 header 长度兜底(QuickTime 可能拒播)
- has_audio_recording 跟磁盘脱钩(前端 catch 'recording not found' 已兜住)

cargo test 258 全过;tsc 0 error;5 语言 i18n 完整。
2026-05-14 20:27:05 +08:00
TRIP 0aa3ee64d3 Merge pull request #435 from Open-Less/feat/style-pack-ui-refresh
feat(style-pack): rework UI + 模板新建 + 清理 PR #429 遗留死代码
2026-05-14 15:01:38 +08:00
baiqing e0a48ce801 chore(release): bump version to 1.3.2-1 (Beta) 2026-05-14 15:00:15 +08:00
baiqing 883ff74cf4 docs(marketplace): add style pack marketplace planning doc
预留风格包市场的 HTTP API + IPC 契约 + DTO + 鉴权/缓存/安全策略。
IPC stub 未实装;后端 endpoint 选型 + 服务端工程化待定。
2026-05-14 14:59:36 +08:00
baiqing 477e6da93d feat(style-pack): rework UI + template-create path
UI:
- 「原文」改成标题旁的 pill 切换器,与 builtin 卡分离
- builtin 卡片按 light → structured → formal 排序,followed by imported 包
- 「+ 新建风格包」tile 固定在网格末位
- imported 卡片右上角换成红色 trash 删除按钮(builtin 显示装饰 sparkle 图标,无 delete)
- builtin 卡片背景做灰底处理;编辑按钮 disabled,"只读"
- 编辑按钮挪到底部,紧接「导出」右边

后端:
- 新增 IPC create_style_pack_from_template / persistence.create_from_template,「+」走这条路径直接落盘
- 编辑面板出场加 modal-backdrop-out / modal-drawer-out 两个 keyframe

清理:
- 删除 Settings.tsx 里 PR #429 留下的"润色 System Prompt 已迁移"死告示卡
- 删除 11 个相关 i18n key 跨 5 种语言
- 删除卡片上的「轮换 ON/OFF」按钮 + 编辑面板里同款 + metaStatus item
- BusyAction 移除 'toggling' 变体
- 删除 BUILTIN 选中态与 active 共用 highlight 的 bug
- SavedToast 统一替换 inline notice/error banner
2026-05-14 14:59:31 +08:00
baiqing eca6fae8c0 chore(release): bump version to 1.3.1 (Stable) 2026-05-14 12:58:30 +08:00
Chris233 4f644505f6 Merge pull request #429 from Cooper-X-Oak/cooper/style-pack-store-v1-beta
feat: add style pack store v1
2026-05-14 11:59:30 +08:00
H-Chris233 426bd8fe38 Preserve legacy style mode during migration
Constraint: legacy preferences may have defaultMode without activeStylePackId, and Raw style packs may still use the LLM path
Rejected: defaulting missing activeStylePackId to builtin.light | silently changes Raw, Structured, and Formal users during upgrade
Rejected: skipping script normalization for Raw LLM output | lets Simplified/Traditional preferences be bypassed by raw style packs
Confidence: high
Scope-risk: narrow
Directive: treat missing activeStylePackId as a migration case derived from defaultMode, not as the new-user default
Tested: cargo fmt --manifest-path src-tauri/Cargo.toml; git diff --check; cargo test --manifest-path src-tauri/Cargo.toml --lib types::tests::missing_active_style_pack_id_uses_legacy_default_mode; cargo test --manifest-path src-tauri/Cargo.toml --lib types::tests::explicit_active_style_pack_id_is_preserved; cargo test --manifest-path src-tauri/Cargo.toml --lib coordinator::dictation::tests::raw_llm_output_still_applies_script_preference; cargo test --manifest-path src-tauri/Cargo.toml --lib
Not-tested: full app build; desktop runtime smoke on macOS/Windows
2026-05-14 11:16:52 +08:00
H-Chris233 b3a2bc393d Make hotword misrecognition wording natural
Constraint: hotword wording appears in both effective system prompts and preview diagnostics
Rejected: keeping the slash form | less natural and leaves preview wording harder to read
Confidence: high
Scope-risk: narrow
Directive: keep hotword preview wording aligned with the system prompt wording
Tested: cargo fmt --manifest-path src-tauri/Cargo.toml; git diff --check; cargo test --manifest-path src-tauri/Cargo.toml --lib polish::tests::
Not-tested: full app build; desktop runtime smoke on macOS/Windows
2026-05-14 11:09:35 +08:00
H-Chris233 97e91ecdb6 Keep streaming history aligned with partial typing
Constraint: type_unicode_chunk returns the actual count of inserted characters, including partial failures
Rejected: appending the full delta after every successful call | hides short writes and makes typed_text diverge from the screen
Confidence: high
Scope-risk: narrow
Directive: preserve typed_text as the source of truth for what actually reached the focused field during streaming insertion
Tested: cargo fmt --manifest-path src-tauri/Cargo.toml; git diff --check; cargo check --manifest-path src-tauri/Cargo.toml; cargo test --manifest-path src-tauri/Cargo.toml --lib
Not-tested: desktop runtime smoke on macOS/Windows
2026-05-14 10:46:28 +08:00