feat(history+polish): time-based retention + conversation-aware polish#355
Conversation
回应用户「历史记录功能 + 对话上下文 + 提示词调优」需求。
新增 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。
…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。
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
PR Reviewer Guide 🔍(Review updated until commit d55298e)Here are some key observations to aid the review process:
|
…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 新增),完全无回归。
|
Persistent review updated to latest commit d55298e |
|
@claude 检查一下这段代码是否能正常合并。 |
|
Claude finished @appergb's task in 3m 17s —— View job 合并检查完成 ✓
合并可行性:✅ 可以合并分支干净,基于最新 beta(merge-base = persistence.rs
一个微小边角情况:若 sessions 中间有 types.rs
coordinator.rs
polish.rs
小样式问题(不影响合并): 前端(Settings.tsx / types.ts / ipc.ts)
微小 UX 注意(不影响功能):用户清空输入框再输入新值时,每次 i18nzh-CN、zh-TW、en、ja、ko 五个文件均添加了 4 个新 key,各语言翻译均为实质性翻译(非复制),位置插入到正确的 测试
总结代码架构清晰,沿用现有模块纪律(leaf modules 不跨模块、新逻辑通过 coordinator 串联),两大功能(时间保留 + 多轮上下文)均有充分的单测覆盖,边角情况处理保守稳妥,无安全风险,无合并冲突。可以合并到 beta。 |
* 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>
…ound mode) 回应用户「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 干净。
…tart) (#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>
User description
Summary
回应「历史记录功能 + 对话感知 polish」需求。
设计
历史保留
对话感知 polish
提取小重构
顺手修的预存 bug(独立 commit `01e9f00`)
`types::tests::legacy_hotkey_trigger_still_produces_effective_key_codes` 跑测时 fail。跟本 PR 无关,但用户要求中途发现就修:
`HotkeyBinding` `#[serde(default)]` 结构级 default 让旧 prefs(无 `keys` 字段)反序列化后 trigger 跟 keys 不一致——trigger 被 JSON 覆盖成 RightControl,但 keys 仍是 Default 残留的 ["AltRight"]。effective_codes() 信任 keys 不 fallback 到 trigger,于是实际生效的快捷键跟用户当年选的对不上。
修法:`Default::keys = None`,让 effective_codes() 走 `legacy_trigger_code(trigger)` 跟 trigger 自动同步。
验证
不在本 PR 的部分
用户原 ask 还提了「连接速度优化 + 提示词排布工程」。这两块需要先 measure 再动(盲改可能让 polish 慢的更慢、prompt 改坏直接影响输出质量),留独立 PR。
PR Type
Enhancement, Bug fix, Tests
Description
Add history retention preferences
Feed recent turns into polish
Add recording settings controls
Fix compatibility and tests
Diagram Walkthrough
File Walkthrough
7 files
Pass empty context to validation polishWire retention and context-aware polish flowPrune old history during appendBuild multi-turn polish chat requestsAdd new user preference fieldsExtend frontend preferences type definitionsAdd numeric controls for new settings5 files
Localize new recording settings stringsLocalize new recording settings stringsLocalize new recording settings stringsLocalize new recording settings stringsLocalize new recording settings strings1 files
Seed mock preferences with new defaults1 files
Update preference fixture defaults for tests