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: 3 additions & 0 deletions .agents/skills/iterate-macos-ime/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ description: 在已装 macOS 输入法上做「换包 → 验收 → 还原」
| 发布验收(用户正常卸载→安装→添加源) | 注销:`make macos-clean-install` | 一次注销(会终止 agent 会话) |
| 只要看 UI 观感 | `make macos-ui-snapshots` / `make macos-dev` | 秒级,不碰安装目录 |

> `make macos-dev` 使用私有配置/词库根,可在非 TTY(Claude Code `!`)下运行:**关闭窗口即结束**并清理临时目录;
> 真实 `user-dictionary.tsv` 若在会话中变化,通常来自已装输入法在其它窗口的学习,不是预览越界(报告 287)。

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

## 1. 不注销换包(推荐)
Expand Down
3 changes: 3 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ make macos-offline-tests

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

- 2026-11-15 / Claude Code(air):**交互式面板验收修复(报告 287)**。`make macos-dev` 在 Claude Code 的
非 TTY stdin 下 `selectors.register` 崩溃;改为非 TTY 时只轮询子进程、关闭窗口即结束。交互会话中真实数据
变化降为 WARN(预览只写私有根);测试期间真实词库 2,046→2,060 行属已装输入法正常学习,未回滚。
- 2026-11-15 / Claude Code(air):**快速 UI 回路修复(报告 286)**。`make macos-ui-snapshots` 因
`CandidateWindow` 新增 `CaretPlacement`/`LearningHints` 依赖而编译失败;补全源码清单,新增
`test-ui-snapshots.py` 防清单漂移。离线回归 68 文件/337 例 PASS;快照可重新产出(候选 6 / 面板 14 / HUD 6)。
Expand Down
39 changes: 39 additions & 0 deletions docs/reviews/287-interactive-panel-non-tty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 287 — 交互式面板验收在非 TTY 下崩溃修复

> 2026-11-15。用户直接执行 `make macos-dev` 时脚本在 `selectors.register(sys.stdin)` 崩溃(EINVAL),窗口来不及使用。

## 1. 现象

```
File "run-panel-interactive.py", line 45, in <module>
reader.register(sys.stdin, selectors.EVENT_READ)
OSError: [Errno 22] Invalid argument
```

Claude Code 的 `!` / `Bash` 环境里 stdin 不是可 `select()` 的终端(管道/`/dev/null`),
而脚本假定了一个可轮询的 `sys.stdin`。崩溃发生在 `try` 内,`finally` 已终止隔离进程并把
临时目录标为 `RETAINED`,但窗口已经关闭,无法验收。

## 2. 修复

- 用 `sys.stdin.isatty()` 区分:TTY 走原来的「输入 `close` 结束」;非 TTY 不注册 stdin,
只轮询子进程,**关闭窗口即结束**,并打印对应提示。
- 交互会话中若真实 `config.toml` / `user-dictionary.tsv` 有变化,改为 **WARN** 而不是
「protected files changed」异常:预览进程只写私有根(`ime_config_init(work/config.toml)`),
人工会话期间已装输入法可能在其它窗口正常学习(写真实词库),不能据此判定预览越界。
自动化模式的严格隔离断言仍由 `run-panel-isolation.py` 负责。

## 3. 验证

- 非 TTY 启动 → 关闭窗口 → `PASS interactive cleanup: owned process exited; real files unchanged`,
退出码 0,临时目录已删除;
- `run-panel-acceptance.sh --isolation-probe` 4 项仍 PASS。

## 4. 关于真实数据

首次崩溃后的手工测试期间,真实 `user-dictionary.tsv` 从 2,046 行变为 2,060 行(新增 14 条五笔词条),
`config.toml` 未变。该变化与已装输入法在其它窗口的正常学习一致,**未回滚**(属于用户真实数据)。

## 5. 关联

`platforms/macos/tests/run-panel-interactive.py`、报告 286。
22 changes: 17 additions & 5 deletions platforms/macos/tests/run-panel-interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,34 @@ def stamp(path):
process = subprocess.Popen([str(contents / 'MacOS/LampyrisIsolated'),
'--panel-isolation-probe', 'interactive', str(work), str(ROOT / 'data/generated')])
print(f'OWNED_APP={bundle}\nOWNED_PID={process.pid}\nOWNED_ROOT={work}', flush=True)
print('请在隔离窗口点击“输入法菜单”或“偏好设置”;关闭窗口或在此输入 close 结束。', flush=True)
interactive_stdin = sys.stdin.isatty()
if interactive_stdin:
print('请在隔离窗口点击“输入法菜单”或“偏好设置”;关闭窗口或在此输入 close 结束。', flush=True)
else:
print('请在隔离窗口点击“输入法菜单”或“偏好设置”;关闭窗口即结束(当前终端不可输入 close)。', flush=True)
reader = selectors.DefaultSelector()
completed = False
try:
reader.register(sys.stdin, selectors.EVENT_READ)
if interactive_stdin:
reader.register(sys.stdin, selectors.EVENT_READ)
deadline = time.monotonic() + 3600
while process.poll() is None and time.monotonic() < deadline:
if reader.select(timeout=0.2):
if interactive_stdin and reader.select(timeout=0.2):
if sys.stdin.readline().strip() == 'close':
completed = True
break
else:
time.sleep(0.2)
if process.poll() is None:
process.terminate()
process.wait(timeout=5)
if [stamp(path) for path in protected] != before:
raise RuntimeError('protected files changed; no restoration attempted')
changed = [path for path, old in zip(protected, before) if stamp(path) != old]
if changed:
# 交互窗口是人工会话:已装输入法可能在别的应用里学习(写 user-dictionary.tsv),
# 或用户自己改了设置。预览进程只写私有根(`ime_config_init(work/config.toml)`),
# 这里不能认定是预览越界;自动化隔离仍由 run-panel-isolation.py 严格断言。
print('WARN 交互会话期间真实数据有变化(通常来自已装输入法的正常学习,'
'不是预览写入):' + '、'.join(path.name for path in changed), flush=True)
if completed or process.returncode == 0:
shutil.rmtree(work)
print('PASS interactive cleanup: owned process exited; real files unchanged', flush=True)
Expand Down
Loading