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
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ ADR-147:Terminal正常实际提交“你”及wq前缀确认后的failure/INT/
- [ ] **2026-10-27 MVP 正式发布(目标 `1.0.0`,ADR-298)**:阶段 A 在 0.1.16/26 上演练 IT-22(`docs/tasks/T-ITER-22.md`)→ 修复入库 → 阶段 B 递增 1.0.0(build 27)并双机装同一包(同时闭环「更新器自更新」)→ 阶段 C 只在 1.0.0 上做正式 IT-22、IT-15 独立重建、IT-23 五工作日 → 阶段 D `tag v1.0.0` 打在验收候选提交 → Release → Pages。
- [x] 2026-10-28 **锁定候选 1.0.0 / build 29**(报告217):提交 `11abece3`、ZIP `aca4ed5c…`、宿主执行 `4ad949e6…`,air 已装同包。装机定向复验:2.1 多词拼接 PASS、9.1 编码播报 PASS、11.1 候选跟随(6 张截图:下方/镜像/底部翻上,无压字)PASS。**IT-22 限定范围通过**(必查 11 + 抽查 8 全 PASS,其余沿用报告216 基线)。
- [x] 2026-10-28 **IT-15 独立离线重建通过(报告218)**:用户在 air 上从候选自带源码包离线重建 1.0.0/build 29,身份链与逐架构网络导入 PASS、**默认数据 4/4 逐字节一致**、空闲 0 外联;可执行文件因本机 SDK 27.0 vs 发布 26.5 不同(ADR-033 边界)。顺带修掉 `run-repro.sh` 的缺陷:原来 `cmp` 可执行文件失败会中断,导致数据表比对根本没跑,且漏比 `variant-s2t.tsv`。
- [ ] 2026-10-29 IT-23 五工作日试用(**1.0.0 / build 32**,air 与 pro 执行 SHA 均为 `ddd33b7a…`):两台设备已装同一包(报告217 §8)。29→30 修「候选窗跟随鼠标」,30→31 加 HUD 诊断,31→32 改「组合态切换不丢字」(ADR-302);IT-23 尚未起算。
- [ ] 2026-10-29 IT-23 五工作日试用(**1.0.0 / build 33**,air 与 pro 执行 SHA 均为 `8a94ca33…`):两台设备已装同一包(报告217 §8)。31→32 改核心语义,32→33 补宿主两条路径(Shift/⌥M);IT-23 尚未起算。
- [x] 2026-10-29 **组合态切换三条路径全部通过(报告222,build 33)**:Shift(`flagsChanged` 分支原样上屏编码)、⌥M(去掉 `applyInputMode` 里的取消,保留缓冲并重算)、⌥F(保留组合);教训是同一动作有多个入口,离线探针覆盖不到,必须装机按键复验。
- [x] 2026-10-29 **组合态切换不再丢字(ADR-302,报告221,build 32)**:`toggle_width`/`set_mode` 去掉 `cancel()`(保留组合、按新模式重算且不自动上屏),Shift 切英文改为**原样上屏编码**;core 新增 3 个单测,`ime-ffi` 旧用例改为先 Esc;两台设备已装 build 32。
- [x] 2026-10-29 **「HUD 不显示」结案(报告220)**:三次报告分别是诊断开关未生效无法取证、候选窗跟随鼠标(真缺陷,build 30 修)、**换包后旧进程仍在运行**。build 31 把诊断改为文件开关并给 `showStateFeedback`/`hudCaretRect` 各分支打点;`iterate-macos-ime` 收尾清单新增「`lsof … | grep -c deleted` 必须为 0」。
- [x] 2026-10-29 **候选窗跟着鼠标而不是插入点(报告219,build 30)**:`caretRect` 给解析器传了 `allowsEmptyCaret: false`,`length()==0` 时解析直接返回 nil → 落到鼠标兜底;改为 `true`,并把回退链收敛为「本轮 → 上次成功位置 → 鼠标」,新增 3 条断言。两台设备已装 build 30。
Expand Down
10 changes: 6 additions & 4 deletions docs/reviews/217-1.0.0-freeze.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ platforms/macos/tests/run-repro.sh \
- **静态零外联**:`./scripts/verify-no-network.sh`(或 `make verify-no-network`)通过。
- **运行时零外联**:一边打字一边 `lsof -i -a -p "$(pgrep -x Lampyris)"`,预期**无输出**。

## 8. IT-23 试用基线:两台设备装同一个包(2026-10-29 更新为 build 32)
## 8. IT-23 试用基线:两台设备装同一个包(2026-10-29 更新为 build 33)

| 项 | air(本机,arm64 / macOS 27.0.1) | pro(x86_64 / macOS 13.7.8) |
|---|---|---|
| 版本 / 构建 | **1.0.0 / 32** | **1.0.0 / 32** |
| 宿主执行 SHA-256 | `ddd33b7aa2962a6b4e5cc3f662624931b67f713c6428201d61979edec17dd66e` | **同值**(同一个二进制) |
| 更新程序构建号 | 32 | 32 |
| 版本 / 构建 | **1.0.0 / 33** | **1.0.0 / 33** |
| 宿主执行 SHA-256 | `8a94ca339c1c02d98b9472be1fcd17e7ebecd30a2f99ee799ed6430364e587e2` | **同值**(同一个二进制) |
| 更新程序构建号 | 33 | 33 |
| 签名 / 公证 | Developer ID + `spctl` Notarized | 同(`codesign --strict`、`stapler validate` 均 PASS) |
| 用户数据 | config `cfc86cff…`、词库 800+ 行 | config `cb8e7d05…`、词库 **727 行**(build 32 装前装后一致) |
| 旧版本备份 | `updates/`、`uninstalled/` 历史项 | `uninstalled/bundle.aPZKev`(0.1.16) |
Expand All @@ -147,3 +147,5 @@ pro 的安装记录:`ev-95-pro-stage.log`(ZIP 校验 → 解包 → 停宿
> 实为**旧进程仍在运行**(报告 220),不是产品缺陷。诊断文件已删除,无文件时零开销;两台设备均已在 31。
> **31 → 32 的原因**:组合态下按 Shift/⌥M/⌥F 会丢字(报告 221 / ADR-302),改为「Shift 原样上屏编码、⌥M/⌥F 保留组合」。
> IT-23 仍未起算,基线跟随最终行为。
> **32 → 33 的原因**:build 32 只修到核心层,Shift(`flagsChanged` 路径)与 ⌥M(`applyInputMode` 先取消组合)
> 都走宿主分支,装机复验只有 ⌥F 通过(报告 222)。33 把这两条也改到「保留/原样上屏」,两台设备均已安装。
2 changes: 1 addition & 1 deletion docs/reviews/221-switch-keeps-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
- core 新增 3 个单测(全角保留组合与候选、切模式保留缓冲区并重算、新模式无候选时保留编码且 Esc 仍可取消)。
- `ime-ffi` 的 `candidates_expose_source_codes` 原先依赖"切模式会清空缓冲",改为先 Esc 再切模式。
- 离线回归 56 文件 / 303 例、`make verify` 全绿;实机复验见 §5。
- 装机:air 与 pro 均为 1.0.0/build 32(执行 `ddd33b7a…`),待用户按 §5 复验三条快捷键。
- 装机:build 32 只有 ⌥F 通过,Shift/⌥M 未生效(详见报告 222);build 33 修好后三条全部通过。

## 5. 遗留

Expand Down
52 changes: 52 additions & 0 deletions docs/reviews/222-switch-paths-in-host.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 222 组合态切换:核心改对了,但宿主两条路径没走到(build 32 → 33)

> 承接报告 [221](221-switch-keeps-composition.md)。build 32 装机复验:**⌥F 通过,Shift 与 ⌥M 仍丢字**。
> 本报告记录这两条为什么没被上一版修到,以及修法与最终复验。

## 1. 为什么 build 32 只修好了 ⌥F

| 快捷键 | 实际入口 | build 32 改在哪 | 结果 |
|---|---|---|---|
| ⌥F | `toggleWidth()` → `session.toggleWidth()` → `refreshAfterStateChange()` | **核心** `Session::toggle_width`(去掉 `cancel()`) | ✅ 生效 |
| Shift | `handle(_:)` 的 `.flagsChanged` 分支 → `session.shiftTransition(...)` → `clearMarked` + `hide()` | 宿主的 `toggleEnglish(_:)`(**菜单/快捷键**入口) | ❌ 键盘走不到 |
| ⌥M | `applyInputMode()` → **先** `cancelClientComposition()` **再** `setMode()` | 核心 `Session::set_mode`(保留组合) | ❌ 取消发生在调用核心之前 |

两条都不是「改错了」,而是**改在了另一条等价入口上**——截图/探针都看不出来,只有真机按键能暴露。

## 2. 修法(build 33)

**Shift**:在 `shiftTransition` 之前把编码取出来(它内部会 `cancel()`),切换后 `clearMarked` + `insertText(raw)`:

```swift
let pendingRaw = session.isComposing && !session.isEnglish ? session.buffer : nil
let toggled = session.shiftTransition(isDown: flags.contains(.shift), otherModifier: other)
if toggled {
if let client { clearMarked(client) }
if let raw = pendingRaw, !raw.isEmpty, let client { client.insertText(raw, ...) }
candidateWindow?.hide()
showStateFeedback(StateFeedback.language(session.isEnglish))
}
```

**⌥M**:去掉 `applyInputMode()` 里的 `cancelClientComposition()`,改为

```swift
session?.setMode(next) // 核心保留缓冲区并按新模式重算
showStateFeedback(StateFeedback.mode(next))
refreshAfterStateChange() // 预编辑与候选跟上新模式
```

## 3. 验证

| 项 | 结果 |
|---|---|
| `test-controller-menu`(真实 IMK 派发探针) | PASS |
| `make verify` | PASS |
| `make macos-offline-tests` | 56 文件 / 303 例 PASS |
| **实机(build 33,用户)** | ① 打字出候选 → Shift:文档落下 `nihao` 字母并切英文 **PASS**;② 打字出候选 → ⌥M:候选框不消失、候选按新模式刷新 **PASS**;③ ⌥F 保持 PASS |

## 4. 教训

1. **同一个动作常有多个入口**(键盘 / 菜单 / 快捷键 / 探针),改行为时要先把入口列全再动手;本次 `toggleEnglish` 有两条,`setMode` 有三个调用点。
2 这两条路径属 AppKit 事件处理,**离线探针覆盖不到**,必须装机按键复验——build 32 的"已改完"结论下得太早。
3. 已在 `iterate-macos-ime` 的收尾清单里强调:换包后先确认跑的是新二进制(报告 220),再做行为判断。
32 changes: 32 additions & 0 deletions docs/reviews/223-drop-language-chip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 223 候选窗去掉「中文/英文」芯片(ADR-303)

> 用户观察:候选框出现时中英状态始终显示「中文」,而按 Shift 切英文时候选框直接消失——
> 那么这枚芯片就恒为「中文」,属于多余信息。要求确认后取消,以节省候选框空间。

## 1. 确认:这是结构性的,不是显示 bug

| 证据 | 结论 |
|---|---|
| `Session::handle_key` 首行 `if self.english { commit; return }` | 英文模式每键立即上屏,**不建缓冲区** |
| `Session::composing()` 仅在 `handle_key` 的非英文分支置真 | 英文模式下永远 false |
| `InputController.showCandidates` 只在 `isComposing` 分支被调用 | 候选窗**不可能**在英文态出现 |
| ADR-302 后 Shift 切英文会先提交编码再切 | 切换瞬间组合即结束,窗口随之隐藏 |

即:**候选窗可见 ⇒ 一定处于中文输入**,芯片恒为「中文」。

## 2. 改动

- `CandidateStatus.chips`:5 枚 → 4 枚(模式 / 全半角 / 简繁 / 标点)
- `CandidateStatus.english` 字段删除;`accessibilityLabel` 去掉「语言:」分支
- 构造点同步:`InputController.candidateStatus()`、`Preview/main.swift`
- 断言同步:`candidate-presentation`(两次芯片列表断言 + 文案)、`ui-snapshots` 4 处

## 3. 验证

- `make macos-offline-tests`:56 文件 / 303 例 PASS
- 实机(build 34):待用户确认外观

## 4. 不丢的信息

- 中英切换时弹 HUD(约 1.15 秒),VoiceOver 仍通过 `StateFeedback.language` 播报「语言:中文/英文」
- 模式 / 全半角 / 简繁 / 标点仍有芯片内联反馈
9 changes: 9 additions & 0 deletions docs/决策记录.md
Original file line number Diff line number Diff line change
Expand Up @@ -2747,6 +2747,15 @@
- **验证**:`crates/ime-core` 新增 3 个单测(`toggle_width_keeps_the_composition`、`set_mode_keeps_the_buffer_and_recomputes`、`set_mode_without_candidates_keeps_the_buffer_for_escape`);`crates/ime-ffi` 的 `candidates_expose_source_codes` 改为先显式 Esc 再切模式(旧用例依赖了已删除的取消语义);`test-controller-menu`、`test-composition-navigation`、`make verify`、`make macos-offline-tests`(56 文件/303 例)全绿;实机复验见报告 221。
- **关联**:ADR-062(模式/限长)、ADR-211(状态反馈)、`crates/ime-core/src/lib.rs`、`platforms/macos/Sources/InputController.swift`、报告 221。

## ADR-303:候选窗不再显示「中文/英文」芯片

- **编号**:303
- **决策**:`CandidateStatus.chips` 移除中英芯片(原 5 枚降为 4 枚:模式 / 全半角 / 简繁 / 标点),`CandidateStatus.english` 字段一并删除。
- **理由**:用户观察「候选框出现时中英状态永远是中文,切英文候选框直接消失」。代码确认这是结构性的:英文模式下 `handle_key` 每键立即 `commit`,`composing` 永远为 false,候选窗不可能出现。因此这枚芯片恒为「中文」,只是占宽度(约 36pt)与一行视觉噪音。
- **边界**:中英切换的反馈不丢——候选窗不可见时本来就走 HUD(ADR-211),屏幕阅读器仍有 `StateFeedback.language` 的播报;不新增配置项;不改变其它三枚芯片与标点符号芯片。
- **验证**:`candidate-presentation` 芯片断言改为 4 枚(不含中英)、`ui-snapshots` / `Preview` 构造点同步;`make macos-offline-tests` 56 文件 / 303 例全绿;实机外观见报告 223。
- **关联**:ADR-211(内联芯片与 HUD 分工)、ADR-291/300(播报)、报告 223、`platforms/macos/Sources/CandidateWindow.swift`。

## 待定项

| 编号 | 议题 | 说明 |
Expand Down
2 changes: 1 addition & 1 deletion docs/发布说明.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
| **`⌥A`** | 关于 | `hotkey.open_about`;— |
| **`,` / `.`**(组合态) | 上一页 / 下一页候选 | `candidate.page_with_comma`;否 |

> 每次状态变化(中英 / 全角半角 / 简体繁体 / 标点 / 模式 / 学习区)都会即时反馈:候选框可见时在顶部右侧(翻页区左边)更新两字状态芯片(混输/中文/半角/简体 + 标点);候选框未显示时在光标上方弹出约 1.15 秒的提示(仅在聚焦文本输入框时)。两者都不抢焦点、不写配置。
> 每次状态变化(中英 / 全角半角 / 简体繁体 / 标点 / 模式 / 学习区)都会即时反馈:候选框可见时在顶部右侧(翻页区左边)更新两字状态芯片(混输/五笔/拼音、全角/半角、简体/繁体 + 标点;中英不显示,见 ADR-303);候选框未显示时在光标上方弹出约 1.15 秒的提示(仅在聚焦文本输入框时)。两者都不抢焦点、不写配置。

> 各组快捷键都可在 `config.toml` 改:组合键写法 `ctrl|shift|alt|cmd + 键`(空格写 `space`);`hotkey.toggle_english = "shift"` 表示“单独按 Shift”;任一项设为 `"none"` 即关闭。输入法菜单的状态项(混输/五笔/拼音、中文/英文、全角/半角、简体/繁体、中文标点/英文标点)**直接显示当前值**并在右侧列出快捷键(单按 Shift 切中英时在快捷键位显示 `⇧`);菜单「偏好设置」打开设置面板(设置项即列出全部可调项与快捷键);「版本信息」窗口含诊断,可「复制信息」或点「反馈问题」/「反馈漏洞」跳转。

Expand Down
2 changes: 1 addition & 1 deletion packaging/build-number
Original file line number Diff line number Diff line change
@@ -1 +1 @@
33
34
6 changes: 3 additions & 3 deletions platforms/macos/Preview/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ final class PreviewInput: NSTextView {
let summary = values.map { "\(CandidateTextPresentation.display($0.text))[\($0.source.label)]" }.joined(separator: " · ")
status?("\(session.variantErrorMessage.map { $0 + " " } ?? "")词库:\(session.dictionaryError == IME_OK ? "正常" : "读写失败") 页:\(session.pageIndex + 1)/\(session.pageCount) 组合:\(session.buffer.isEmpty ? "(空)" : session.buffer) 候选:\(summary)")
let status = CandidateStatus(
mode: session.mode, english: session.isEnglish, fullWidth: session.isFullWidth,
mode: session.mode, fullWidth: session.isFullWidth,
traditional: session.isTraditional,
chinesePunctuation: (ImeConfig.string("input.punctuation") ?? "chinese") == "chinese")
let caret = firstRect(forCharacterRange: NSRange(location: content.length, length: 0), actualRange: nil)
let feedback = (status.mode, status.english, status.fullWidth, status.traditional,
let feedback = (status.mode, session.isEnglish, status.fullWidth, status.traditional,
status.chinesePunctuation, ImeConfig.bool("candidate.show_wubi_learning"))
if let last = lastFeedback, last != feedback {
if session.isComposing {
Expand All @@ -161,7 +161,7 @@ final class PreviewInput: NSTextView {
if last.0 != feedback.0 {
message = StateFeedback.mode(status.mode)
} else if last.1 != feedback.1 {
message = StateFeedback.language(status.english)
message = StateFeedback.language(session.isEnglish)
} else if last.2 != feedback.2 {
message = StateFeedback.width(status.fullWidth)
} else if last.3 != feedback.3 {
Expand Down
7 changes: 4 additions & 3 deletions platforms/macos/Sources/CandidateWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -509,16 +509,18 @@ private final class LearningDetailsView: NSView {
/// 候选窗顶部右侧的状态摘要;单字芯片,压缩到不抢候选与翻页的注意力。
struct CandidateStatus: Equatable {
var mode: String
var english: Bool
var fullWidth: Bool
var traditional: Bool
var chinesePunctuation: Bool

/// 两字状态 + 标点符号;学习区不在这里显示(下方学习区本身就是状态)。
///
/// 不显示中英芯片(ADR-303):候选窗只在组合态出现,而组合只可能在中文输入下产生
/// (英文模式每键立即上屏、不建缓冲区),所以这枚芯片永远是「中文」,纯占位置。
/// 中英切换的反馈由 HUD 承担,屏幕阅读器仍能听到「语言:中文/英文」。
var chips: [(text: String, active: Bool)] {
[
(mode == "wubi" ? "五笔" : mode == "pinyin" ? "拼音" : "混输", mode != "mixed"),
(english ? "英文" : "中文", english),
(fullWidth ? "全角" : "半角", fullWidth),
(traditional ? "繁体" : "简体", traditional),
(chinesePunctuation ? "。" : ".", !chinesePunctuation),
Expand All @@ -528,7 +530,6 @@ struct CandidateStatus: Equatable {
func accessibilityLabel(for text: String) -> String {
switch text {
case "混输", "五笔", "拼音": return "输入模式:" + text
case "中文", "英文": return "语言:" + text
case "全角", "半角": return "宽度:" + text
case "简体", "繁体": return "字形:" + text
case "。": return "标点:中文标点"
Expand Down
1 change: 0 additions & 1 deletion platforms/macos/Sources/InputController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,6 @@ final class LampyrisInputController: IMKInputController {
private func candidateStatus() -> CandidateStatus {
CandidateStatus(
mode: session?.mode ?? ImeConfig.string("mode") ?? "mixed",
english: session?.isEnglish ?? false,
fullWidth: session?.isFullWidth ?? false,
traditional: session?.isTraditional ?? false,
chinesePunctuation: (ImeConfig.string("input.punctuation") ?? "chinese") == "chinese")
Expand Down
Loading
Loading