Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/iterate-macos-ime/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ description: 在已装 macOS 输入法上做「换包 → 验收 → 还原」

> `make macos-dev` 使用私有配置/词库根,可在非 TTY(Claude Code `!`)下运行:**关闭窗口即结束**并清理临时目录;
> 真实 `user-dictionary.tsv` 若在会话中变化,通常来自已装输入法在其它窗口的学习,不是预览越界(报告 287)。
> **测 ⌥ 快捷键前先切到 ABC**:输入源是已装萤火虫时,IMK 会在系统层先消费 ⌥M/⌥F/⌥P/⌥T,预览收不到。
> `make macos-dev` 会自动切到 ABC 并在结束时还原;手动流程记得自己切回(报告 289)。

既有`macos-fast-reinstall`路径的前提只有三条,缺一就停:**输入源已移除**、**宿主已退出**、**包装前校验签名/公证**。

Expand Down
6 changes: 6 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ make macos-offline-tests

## 9. 会话日志(最近)

- 2026-11-15 / Claude Code(air):**预览自动切输入源**。`make macos-dev` 启动时若当前是萤火虫,
自动切到 ABC 并在退出(含异常)后切回;实测双向 `tissrc select` 均生效,会话后已还原 Lampyris。
预览冒烟新增 ⌥M/⌥F/⌥P/⌥T 状态断言。
- 2026-11-15 / Claude Code(air):**人工验收三项修复(ADR-361,报告 289)**。指南窗口 380→260 并改两栏
(右侧步骤图示)+ 新增离屏快照;隔离预览补齐 `symbols/emoji/B0-runtime/wubi86-core/level3` 资源,符号面板可搜可插;
练习新增「三级简码」题型(只用 D17-ter 权威表,3 格作答)。离线回归 68 文件/337 例、`make verify` 全绿。
- 2026-11-15 / Claude Code(air):**输入法菜单固定顺序 + 符号快捷键(ADR-360,报告 288)**。菜单动作区固定为
「学习区 → 练习 → 符号与表情 → 使用指南 → 检查更新 → 设置 → 关于」,正式/预览共用 `InputMenuAction.sections`;
新增学习区/练习/符号入口;`hotkey.open_symbols` 默认 `alt+s` → `alt+c`(C = Character;保持单修饰键队形,已有显式配置不覆盖)。
Expand Down
88 changes: 88 additions & 0 deletions docs/reviews/289-manual-acceptance-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 289 — 人工验收三项修复:指南布局 / 预览数据 / 三级简码练习

> 2026-11-15。用户在隔离预览窗口验收时反馈的三项问题。

## 1. 使用指南下半部分空白

**现象**:指南窗口 560×380,内容都在上半部,下半部大片空白。

**处理**:

- 窗口高度 380 → **260**;
- 布局改为两栏:左侧图标/标题/正文,右侧 132×132 的步骤图示卡片(强调色 10% 底、
56pt SF Symbol),把原来的空白变成视觉锚点;
- 新增 `OnboardingWindow.renderForTesting` 与 `guide-{light,dark}-step{1,4}.png` 快照,
以后改布局可直接对比。

## 2. 符号 / emoji 面板「缺少 symbols.tsv」

**现象**:预览的符号面板只有搜索框和「其他」分类,底部报「缺少 symbols.tsv」,
无法搜索或插入。

**原因**:隔离预览 App 只软链了 `Resources/data`,但 `SymbolCatalog` 按正式包的方式从
`Contents/Resources/symbols.tsv`、`emoji.tsv` 读取;学习提示(`B0-runtime.tsv`)、
词组题库(`wubi86-core.tsv`)同样缺失。

**处理**:

- `run-panel-interactive.py` 把预览包 `Contents/Resources/` 下的数据文件**全部软链**到隔离 App,
以后新增资源不会再漏;
- `dev-preview.sh` 补齐 `symbols.tsv`、`emoji.tsv`、`wubi86-core.tsv`、`wubi86-level3-codes.tsv`;
- 快照/预览仍只读仓库数据,不写真实配置。

## 3. 练习没有三级简码

**现象**:练习分段只有「字根 / 一级 / 二级 / 完整码 / 词组 / 识别码」。

**处理**(ADR-361):

- 新增 `PracticeKind.code3`,插在二级与完整码之间;
- 题库只对 **D17-ter 权威表**里明确收录的 `full` 字出题,绝不把「完整码前 3 位」当简码;
- 作答框 3 格,拆解说明追加「三级 XXX」;
- `wubi86-level3-codes.tsv` 随包分发;预览/隔离 App 也已带上;
- 指南第 4 步的题型顺序同步。

## 4. 预览输入区不消费打开类快捷键

**现象**:在 `make macos-dev` 的输入框里按 `⌥C`/`⌥K`/`⌥G`,快捷键不生效,反而把 Option 字符插入文本。

**原因**:正式宿主在 `handleHotkey` 里先处理 `open_update/open_practice/open_symbols`,再交给
`session.shortcut`;预览的 `PreviewInput.handleShortcut` 只处理了设置/关于,缺少这三个入口。

**处理**:

- 预览补齐 `onOpenUpdate` / `onOpenPractice` / `onOpenSymbols`,与菜单回调共用同一个符号插入闭包;
- 预览冒烟新增断言:合成 `⌥C`/`⌥K`/`⌥G` 事件必须分别触发对应回调,不能被当作文本插入。

## 5. 预览里 ⌥M/⌥F/⌥P/⌥T 仍不生效

**现象**:补齐预览的打开类快捷键后,`⌥C/⌥K/⌥G` 正常,但 `⌥M/⌥F/⌥P/⌥T` 仍无反应。

**定位**:

- 预览冒烟(合成事件 + 私有配置)里四个状态键**全部通过**:`⌥M` 切模式、`⌥F` 全半角、
`⌥P` 标点、`⌥T` 简繁;说明预览代码路径与持久化写入都正常。
- 当前系统输入源是 `cn.jokerhub.inputmethod.Lampyris.default`(已装输入法)。IMK 会在系统层
先把这些 ⌥ 快捷键交给已装宿主;预览窗口收不到。会话期间真实 `config.toml` 被改成
`punctuation=english / full_width=full / chinese_variant=traditional`,正是这四个键的效果。
- `⌥C` 之所以正常,是因为已装 1.3.0 的符号键还是 `⌥S`,不认识 `⌥C`,事件才落到预览。

**处理**:

- `make macos-dev` 启动时查询当前输入源;若是萤火虫,**自动切到 ABC**,关闭窗口/异常退出时
在 `finally` 里切回原输入源(实测双向 `tissrc select` 均生效);切换失败时打印手动提示;
- 预览冒烟新增四个状态键的断言(模式/全半角/标点/简繁各切换一次),防止回归;
- 这是输入法系统的预期行为,不是预览或产品的 bug;自动化只改变本机输入源并在会话结束时还原。

## 6. 验证

- `test-wubi-learning`:新增 `.code3` 答案 3 位与判分断言,题库非空;
- `test-symbol-picker`、`test-ui-snapshots`、三处菜单探针 PASS;
- `make macos-ui-snapshots`:新增指南快照(亮/暗 × 第 1/4 步);
- 预览冒烟新增状态快捷键断言;`make macos-offline-tests` 68 文件 / 337 例 PASS;`make verify` PASS。

## 7. 关联

ADR-361、ADR-345/314、报告 288、`OnboardingWindow.swift`、`Practice.swift`、
`PracticeWindow.swift`、`PracticeExplanationView.swift`、`Preview/main.swift`、
`run-panel-interactive.py`、`dev-preview.sh`。
19 changes: 19 additions & 0 deletions docs/决策记录.md
Original file line number Diff line number Diff line change
Expand Up @@ -3988,3 +3988,22 @@
- **状态**:采纳(2026-11-15)。
- **关联**:ADR-209/313/349/351、报告 288、`InputMenu.swift`、`InputController.swift`、
`Preview/InputMenuPreview.swift`、`crates/ime-config/src/schema.rs`、`docs/配置架构.md`。

### ADR-361:练习新增「三级简码」题型(使用 D17-ter 权威表)

- **背景**:ADR-314 当时关闭了三级简码练习,原因是引擎侧没有「哪些字真有三级简码」的码表,
只能把「前 3 码」当简码教,容易教错。ADR-345 后来引入 D17-ter 权威表(3,362 条,已随包用于
候选命中),练习现在有条件按同一张表出题。
- **决策**:
1. 新增 `PracticeKind.code3`,顺序放在「二级简码」与「完整码」之间:
字根 → 一级 → 二级 → 三级 → 完整码 → 词组 → 识别码。
2. 题库只取 `LearningHints` 的 `full` 条目,并且**该字必须出现在 D17-ter 表里**;不在表里的字不出题,
绝不把「完整码前 3 位」当三级简码。
3. 三级简码表随包分发(`wubi86-level3-codes.tsv`,LGPL-3.0,来源 D9/D17-ter),
练习按 `Resources/` 读取;缺失时该题型题库为空,不报错。
4. 作答框 3 格;拆解说明在「完整码 / 一级 / 二级」之后追加「三级」。
- **结果**:练习页多一个「三级简码」分段;题型探针覆盖答案 3 位与判分。
- **边界**:不改变候选排序与自动上屏规则;表里没有的字不猜;旧进度文件按题型 raw value 读取,不受影响。
- **状态**:采纳(2026-11-15)。
- **关联**:ADR-314/345、报告 289、`Practice.swift`、`PracticeWindow.swift`、`PracticeExplanationView.swift`、
`platforms/macos/build.sh` 的 `wubi86-level3-codes.tsv` 资源。
58 changes: 52 additions & 6 deletions platforms/macos/Preview/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ final class PreviewInput: NSTextView {
var persistModeChanges = false
var onOpenSettings: (() -> Void)?
var onOpenAbout: (() -> Void)?
var onOpenUpdate: (() -> Void)?
var onOpenPractice: (() -> Void)?
var onOpenSymbols: (() -> Void)?

init(session: ImeSession) {
self.session = session
Expand Down Expand Up @@ -51,6 +54,15 @@ final class PreviewInput: NSTextView {
if session.matches("hotkey.open_about", event), let onOpenAbout {
cancelComposition(); onOpenAbout(); return true
}
if session.matches("hotkey.open_update", event), let onOpenUpdate {
cancelComposition(); onOpenUpdate(); return true
}
if session.matches("hotkey.open_practice", event), let onOpenPractice {
cancelComposition(); onOpenPractice(); return true
}
if session.matches("hotkey.open_symbols", event), let onOpenSymbols {
cancelComposition(); onOpenSymbols(); return true
}
let shortcut = session.shortcut(event, persistMode: persistModeChanges)
if shortcut.consumed {
session.markKeyWhileShift()
Expand Down Expand Up @@ -225,6 +237,15 @@ final class PreviewDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
input.persistModeChanges = isolated
input.onOpenSettings = { [weak self] in self?.openSettings() }
input.onOpenAbout = { AboutUI.show() }
input.onOpenUpdate = { UpdateEntry.open() }
input.onOpenPractice = { PracticeWindow.shared.show() }
let openSymbols: () -> Void = { [weak input] in
guard let input else { return }
SymbolPickerWindow.shared.show { [weak input] text in
input?.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0))
}
}
input.onOpenSymbols = openSymbols
self.input = input
let scroll = NSScrollView()
scroll.hasVerticalScroller = true
Expand Down Expand Up @@ -269,12 +290,7 @@ final class PreviewDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
}
menu.onPractice = { PracticeWindow.shared.show() }
menu.onGuide = { OnboardingWindow.shared.show() }
menu.onSymbols = { [weak self] in
guard let input = self?.input else { return }
SymbolPickerWindow.shared.show { [weak input] text in
input?.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0))
}
}
menu.onSymbols = openSymbols
previewMenu = menu
input.stateChanged = { [weak self] in self?.syncMenuControls() }
let menuButton = NSButton(title: "输入法菜单", target: self, action: #selector(openInputMenu(_:)))
Expand Down Expand Up @@ -1094,6 +1110,36 @@ if CommandLine.arguments.contains("--smoke-test") {
else {
fputs("诊断信息缺少版本/系统/架构。\n", stderr); exit(1)
}
// ADR-360:预览输入区必须与正式宿主一样消费打开类快捷键,不能把 ⌥C/⌥K/⌥G 当文本插入。
let shortcutProbe = PreviewInput(session: session)
var opened: [String] = []
shortcutProbe.onOpenUpdate = { opened.append("update") }
shortcutProbe.onOpenPractice = { opened.append("practice") }
shortcutProbe.onOpenSymbols = { opened.append("symbols") }
for (key, character) in [(UInt16(8), "c"), (UInt16(40), "k"), (UInt16(5), "g")] {
shortcutProbe.keyDown(with: navigationEvent(key, .option, characters: character))
}
guard opened == ["symbols", "practice", "update"] else {
fputs("预览未消费打开类快捷键:\(opened)\n", stderr); exit(1)
}
// 状态类快捷键必须真正改变会话(预览没有 HUD,用户只能从行为看到结果)。
// 前面的快捷键校验把 toggle_mode 改成了 none;恢复默认并测试持久化路径。
guard ImeConfig.setString("hotkey.toggle_mode", "alt+m") else { exit(1) }
shortcutProbe.persistModeChanges = true
session.setMode("mixed")
let mEvent = navigationEvent(46, .option, characters: "m")
shortcutProbe.keyDown(with: mEvent)
guard session.mode == "wubi" else { fputs("⌥M 未切换模式:\(session.mode)\n", stderr); exit(1) }
let widthBefore = session.isFullWidth
shortcutProbe.keyDown(with: navigationEvent(3, .option, characters: "f"))
guard session.isFullWidth != widthBefore else { fputs("⌥F 未切换全半角\n", stderr); exit(1) }
let shortcutPunctuationBefore = ImeConfig.string("input.punctuation")
shortcutProbe.keyDown(with: navigationEvent(35, .option, characters: "p"))
guard ImeConfig.string("input.punctuation") != shortcutPunctuationBefore else { fputs("⌥P 未切换标点\n", stderr); exit(1) }
let shortcutVariantBefore = session.isTraditional
shortcutProbe.keyDown(with: navigationEvent(17, .option, characters: "t"))
guard session.isTraditional != shortcutVariantBefore else { fputs("⌥T 未切换简繁\n", stderr); exit(1) }
session.setMode("mixed")
try? FileManager.default.removeItem(at: smokeDirectory)
print("视觉证据:\(qaDirectory.path)")
print("✅ 预览冒烟通过:文本渲染、候选、五笔/拼音、数字选字/回车字母、退格/Esc、清空。")
Expand Down
Loading
Loading