nonoka-cli 0.2.3__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/SKILL.md +413 -0
  2. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/references/checklist.md +48 -0
  3. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/scripts/analyze-timeline.py +100 -0
  4. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/scripts/capture-screenshots.py +191 -0
  5. nonoka_cli-0.2.5/.agents/skills/nonoka-tui-testing/scripts/setup-sandbox.sh +237 -0
  6. nonoka_cli-0.2.5/.agents/skills/tui-testing/SKILL.md +60 -0
  7. nonoka_cli-0.2.5/.agents/skills/tui-testing/scripts/opencode_tui.py +288 -0
  8. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/.gitignore +5 -0
  9. nonoka_cli-0.2.5/.nonoka/plugin.json.example +37 -0
  10. nonoka_cli-0.2.5/.nonoka/repo_map.jsonl +85 -0
  11. nonoka_cli-0.2.5/PKG-INFO +559 -0
  12. nonoka_cli-0.2.5/README.md +530 -0
  13. nonoka_cli-0.2.5/TODO.md +43 -0
  14. nonoka_cli-0.2.5/docs/external-mcp-skill-design.md +542 -0
  15. nonoka_cli-0.2.5/docs/nonoka-cli-complex-task-evaluation.md +254 -0
  16. nonoka_cli-0.2.5/docs/nonoka-cli-refactor-design.md +735 -0
  17. nonoka_cli-0.2.5/docs/subagent-refactor-design.md +162 -0
  18. nonoka_cli-0.2.5/docs/subagent-refactor-plan.md +134 -0
  19. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/install.sh +73 -6
  20. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/nonoka.yaml.example +1 -0
  21. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/README.md +12 -0
  22. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/package.json +1 -1
  23. nonoka_cli-0.2.5/packages/nonoka-opencode-provider/src/nonoka-language-model.ts +654 -0
  24. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/src/protocol.ts +41 -0
  25. nonoka_cli-0.2.5/packages/nonoka-opencode-provider/src/stream.ts +275 -0
  26. nonoka_cli-0.2.5/packages/nonoka-opencode-provider/tests/language-model.test.ts +251 -0
  27. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/tests/protocol.test.ts +40 -0
  28. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/tests/stream.test.ts +15 -0
  29. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/pyproject.toml +6 -2
  30. nonoka_cli-0.2.5/scripts/quick-install.sh +23 -0
  31. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/bridge/events.py +58 -7
  32. nonoka_cli-0.2.5/src/nonoka_cli/bridge/handler.py +507 -0
  33. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/bridge/protocol.py +72 -13
  34. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/bridge/server.py +4 -23
  35. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/cli.py +14 -5
  36. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/commands/config_cmd.py +6 -1
  37. nonoka_cli-0.2.5/src/nonoka_cli/commands/opencode_cmd.py +350 -0
  38. nonoka_cli-0.2.5/src/nonoka_cli/commands/plugin_cmd.py +86 -0
  39. nonoka_cli-0.2.5/src/nonoka_cli/commands/run_cmd.py +110 -0
  40. nonoka_cli-0.2.5/src/nonoka_cli/config/models.py +172 -0
  41. nonoka_cli-0.2.5/src/nonoka_cli/core/agent_factory.py +638 -0
  42. nonoka_cli-0.2.5/src/nonoka_cli/core/context.py +28 -0
  43. nonoka_cli-0.2.5/src/nonoka_cli/core/context_trimmer.py +91 -0
  44. nonoka_cli-0.2.5/src/nonoka_cli/core/git_service.py +278 -0
  45. nonoka_cli-0.2.5/src/nonoka_cli/core/namespaces.py +78 -0
  46. nonoka_cli-0.2.5/src/nonoka_cli/core/orchestrator.py +776 -0
  47. nonoka_cli-0.2.5/src/nonoka_cli/core/planning_service.py +192 -0
  48. nonoka_cli-0.2.5/src/nonoka_cli/core/plugin_manifest.py +181 -0
  49. nonoka_cli-0.2.5/src/nonoka_cli/core/plugin_manifest_converter.py +71 -0
  50. nonoka_cli-0.2.5/src/nonoka_cli/core/prompt_builder.py +286 -0
  51. nonoka_cli-0.2.5/src/nonoka_cli/core/repo_map_service.py +111 -0
  52. nonoka_cli-0.2.5/src/nonoka_cli/core/review_service.py +293 -0
  53. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/core/runner_service.py +28 -0
  54. nonoka_cli-0.2.5/src/nonoka_cli/core/subagent_service.py +184 -0
  55. nonoka_cli-0.2.5/src/nonoka_cli/core/task_state.py +206 -0
  56. nonoka_cli-0.2.5/src/nonoka_cli/core/tool_output_policy.py +233 -0
  57. nonoka_cli-0.2.5/src/nonoka_cli/mcp/manager.py +116 -0
  58. nonoka_cli-0.2.5/src/nonoka_cli/skills/__init__.py +9 -0
  59. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/tools/builtins/__init__.py +2 -2
  60. nonoka_cli-0.2.5/src/nonoka_cli/tools/builtins/agent_tools.py +31 -0
  61. nonoka_cli-0.2.5/src/nonoka_cli/utils/trace_logger.py +81 -0
  62. nonoka_cli-0.2.5/tests/integration/test_server_external_capabilities.py +247 -0
  63. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/tests/unit/bridge/test_events.py +34 -0
  64. nonoka_cli-0.2.5/tests/unit/bridge/test_handler.py +303 -0
  65. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/tests/unit/bridge/test_protocol.py +40 -0
  66. nonoka_cli-0.2.5/tests/unit/commands/test_opencode_cmd.py +178 -0
  67. nonoka_cli-0.2.5/tests/unit/commands/test_run_cmd.py +113 -0
  68. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/tests/unit/config/test_models.py +27 -0
  69. nonoka_cli-0.2.5/tests/unit/core/test_agent_factory.py +463 -0
  70. nonoka_cli-0.2.5/tests/unit/core/test_context_trimmer.py +93 -0
  71. nonoka_cli-0.2.5/tests/unit/core/test_git_service.py +262 -0
  72. nonoka_cli-0.2.5/tests/unit/core/test_orchestrator.py +205 -0
  73. nonoka_cli-0.2.5/tests/unit/core/test_planning_service.py +174 -0
  74. nonoka_cli-0.2.5/tests/unit/core/test_plugin_manifest.py +66 -0
  75. nonoka_cli-0.2.5/tests/unit/core/test_plugin_manifest_converter.py +63 -0
  76. nonoka_cli-0.2.5/tests/unit/core/test_repo_map_service.py +41 -0
  77. nonoka_cli-0.2.5/tests/unit/core/test_review_service.py +251 -0
  78. nonoka_cli-0.2.5/tests/unit/core/test_task_state.py +83 -0
  79. nonoka_cli-0.2.5/tests/unit/core/test_tool_output_policy.py +123 -0
  80. nonoka_cli-0.2.5/tests/unit/utils/test_trace_logger.py +49 -0
  81. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/uv.lock +218 -9
  82. nonoka_cli-0.2.3/PKG-INFO +0 -274
  83. nonoka_cli-0.2.3/README.md +0 -247
  84. nonoka_cli-0.2.3/TODO.md +0 -77
  85. nonoka_cli-0.2.3/packages/nonoka-opencode-provider/nonoka-opencode-provider-0.2.0.tgz +0 -0
  86. nonoka_cli-0.2.3/packages/nonoka-opencode-provider/src/nonoka-language-model.ts +0 -378
  87. nonoka_cli-0.2.3/packages/nonoka-opencode-provider/src/stream.ts +0 -164
  88. nonoka_cli-0.2.3/packages/nonoka-opencode-provider/tests/language-model.test.ts +0 -38
  89. nonoka_cli-0.2.3/src/nonoka_cli/bridge/handler.py +0 -252
  90. nonoka_cli-0.2.3/src/nonoka_cli/commands/opencode_cmd.py +0 -149
  91. nonoka_cli-0.2.3/src/nonoka_cli/config/models.py +0 -53
  92. nonoka_cli-0.2.3/src/nonoka_cli/core/agent_factory.py +0 -220
  93. nonoka_cli-0.2.3/src/nonoka_cli/core/context.py +0 -17
  94. nonoka_cli-0.2.3/src/nonoka_cli/core/orchestrator.py +0 -399
  95. nonoka_cli-0.2.3/src/nonoka_cli/mcp/manager.py +0 -373
  96. nonoka_cli-0.2.3/src/nonoka_cli/skills/__init__.py +0 -7
  97. nonoka_cli-0.2.3/src/nonoka_cli/skills/manager.py +0 -204
  98. nonoka_cli-0.2.3/tests/unit/bridge/test_handler.py +0 -79
  99. nonoka_cli-0.2.3/tests/unit/commands/test_opencode_cmd.py +0 -51
  100. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/LICENSE +0 -0
  101. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/NOTICE +0 -0
  102. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/nonoka.yaml +0 -0
  103. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/.gitignore +0 -0
  104. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/LICENSE +0 -0
  105. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/bun.lock +0 -0
  106. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/src/index.ts +0 -0
  107. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/packages/nonoka-opencode-provider/tsconfig.json +0 -0
  108. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/__init__.py +0 -0
  109. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/__main__.py +0 -0
  110. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/bridge/__init__.py +0 -0
  111. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/commands/__init__.py +0 -0
  112. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/commands/doctor_cmd.py +0 -0
  113. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/config/__init__.py +0 -0
  114. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/config/loader.py +0 -0
  115. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/config/manager.py +0 -0
  116. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/core/__init__.py +0 -0
  117. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/core/mcp_service.py +0 -0
  118. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/core/session_service.py +0 -0
  119. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/core/tool_service.py +0 -0
  120. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/mcp/__init__.py +0 -0
  121. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/mcp/models.py +0 -0
  122. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/sessions/__init__.py +0 -0
  123. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/sessions/manager.py +0 -0
  124. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/sessions/models.py +0 -0
  125. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/tools/__init__.py +0 -0
  126. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/tools/builtins/file_tools.py +0 -0
  127. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/tools/loader.py +0 -0
  128. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/utils/__init__.py +0 -0
  129. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/utils/errors.py +0 -0
  130. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/src/nonoka_cli/utils/logging.py +0 -0
  131. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/tests/conftest.py +0 -0
  132. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/tests/unit/commands/test_config_cmd.py +0 -0
  133. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/tests/unit/commands/test_doctor_cmd.py +0 -0
  134. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/tests/unit/config/test_loader.py +0 -0
  135. {nonoka_cli-0.2.3 → nonoka_cli-0.2.5}/tests/unit/sessions/test_manager.py +0 -0
@@ -0,0 +1,413 @@
1
+ ---
2
+ name: nonoka-tui-testing
3
+ description: |
4
+ nonoka-cli 的端到端 TUI 与功能测试流程。用于在隔离沙箱中搭建最新
5
+ nonoka-cli + nonoka-agent + nonoka-opencode-provider,通过 OpenCode
6
+ 实际运行对话与开发任务,截图检查 TUI 渲染,并按分层策略定位根因。
7
+ Use when:
8
+ (1) 验证 nonoka-cli 基础对话是否卡住;
9
+ (2) 验证复杂任务(Next.js 应用等)的工具调用链与 HITL;
10
+ (3) 检查 OpenCode TUI 文字分段、工具卡片、HITL 弹窗等渲染细节;
11
+ (4) 排查 nonoka-cli / adapter / nonoka 框架边界问题。
12
+ license: MIT
13
+ metadata:
14
+ author: Kimi Code CLI
15
+ version: "1.1.0"
16
+ ---
17
+
18
+ # nonoka-cli TUI 与功能测试 Skill
19
+
20
+ ## 1. 快速开始
21
+
22
+ 运行项目内的 setup 脚本,一键准备 `/tmp/nonoka-test` 沙箱:
23
+
24
+ ```bash
25
+ cd /home/fyerfyer/fyerfyer/Projects/nonoka-cli
26
+ bash .agents/skills/nonoka-tui-testing/scripts/setup-sandbox.sh
27
+ ```
28
+
29
+ 如果项目根目录存在 `.env`(例如 `OPENAI_API_KEY=...`),脚本会自动 source 并生成
30
+ `nonoka.yaml` / `opencode.json`。完成后启动 OpenCode:
31
+
32
+ ```bash
33
+ cd /tmp/nonoka-test
34
+ ./run-opencode.sh # 前台运行
35
+ # 或
36
+ ./run-opencode-tmux.sh # 后台 tmux,然后 tmux attach -t nonoka-test
37
+ ```
38
+
39
+ ## 2. 测试目标
40
+
41
+ - 基础对话不卡住(`hello`、`what is 2+2?`)。
42
+ - 复杂开发任务能完成工具调用链(生成 Next.js 应用)。
43
+ - HITL 审批正常弹出、批准/拒绝后流程正确。
44
+ - TUI 渲染细节:文字分段、工具卡片、卡片后文字续接、HITL 弹窗布局。
45
+ - Session 管理:新建、切换、重命名、删除、持久化。
46
+ - 工具实际执行效果:文件读写、命令执行。
47
+
48
+ ## 3. 测试分层
49
+
50
+ | 层级 | 名称 | 验证方式 | 问题归属 |
51
+ | ---- | ------------ | ----------------------------------------- | ------------------------------------- |
52
+ | L1 | 协议层 | 直接对 `nonoka-cli --server` 喂 NDJSON | nonoka-cli bridge / nonoka 框架 |
53
+ | L2 | Provider 层 | 检查 `/tmp/nonoka-provider.log`、timeline | nonoka-opencode-provider |
54
+ | L3 | TUI E2E | Playwright/tmux 截屏 + UI 检查清单 | OpenCode / Ink / provider stream 转换 |
55
+ | L4 | Session 管理 | `nonoka-cli` 子命令 + SQLite 检查 | nonoka-cli sessions |
56
+ | L5 | 产物评估 | 验证生成项目能否 build | nonoka 框架 / tool 实现 |
57
+
58
+ ## 4. 环境搭建(手动版)
59
+
60
+ setup 脚本已经把这些步骤自动化;下面是等价的手动流程,便于排错。
61
+
62
+ ### 4.1 沙箱目录
63
+
64
+ ```bash
65
+ export NONOKA_TEST_ROOT=/tmp/nonoka-test
66
+ rm -rf "$NONOKA_TEST_ROOT"
67
+ mkdir -p "$NONOKA_TEST_ROOT"
68
+ cd "$NONOKA_TEST_ROOT"
69
+ ```
70
+
71
+ ### 4.2 复制/链接源码
72
+
73
+ ```bash
74
+ cd "$NONOKA_TEST_ROOT"
75
+ cp -r /home/fyerfyer/fyerfyer/Projects/nonoka-cli ./nonoka-cli
76
+ ln -s /home/fyerfyer/fyerfyer/Projects/nonoka-agent ./nonoka-agent
77
+ ```
78
+
79
+ ### 4.3 创建 Python 虚拟环境并 editable 安装
80
+
81
+ ```bash
82
+ cd "$NONOKA_TEST_ROOT/nonoka-cli"
83
+ uv venv .venv
84
+ source .venv/bin/activate
85
+ uv pip install -e ./../nonoka-agent -e .
86
+ ```
87
+
88
+ > 测试阶段使用 editable 安装 `nonoka-agent`,修复完成后再 `uv run twine publish` 发布新版到 PyPI。
89
+
90
+ ### 4.4 启动本地 verdaccio 并发布 provider
91
+
92
+ ```bash
93
+ cd "$NONOKA_TEST_ROOT"
94
+ cat > verdaccio.yaml <<'EOF'
95
+ storage: ./verdaccio-storage
96
+ packages:
97
+ 'nonoka-opencode-provider':
98
+ access: $all
99
+ publish: $all
100
+ '**':
101
+ access: $all
102
+ publish: $all
103
+ proxy: npmjs
104
+ EOF
105
+
106
+ nohup npx --yes verdaccio --config verdaccio.yaml > verdaccio.log 2>&1 &
107
+ for i in {1..30}; do
108
+ if curl -fsSL http://localhost:4873/ >/dev/null 2>&1; then break; fi
109
+ sleep 1
110
+ done
111
+
112
+ cd "$NONOKA_TEST_ROOT/nonoka-cli/packages/nonoka-opencode-provider"
113
+ # 只让当前目录使用本地 registry,不污染全局 npm 配置
114
+ cat > .npmrc <<EOF
115
+ registry=http://localhost:4873
116
+ EOF
117
+ npm install
118
+ npm run build
119
+ npm publish --access public
120
+ ```
121
+
122
+ ### 4.5 清理 OpenCode provider 缓存
123
+
124
+ OpenCode 会把 provider 缓存到 `~/.cache/opencode/packages/nonoka-opencode-provider`,
125
+ 如果里面是从 npmjs 下载的旧版本,就会覆盖 verdaccio 的新版本。每次重新 setup 时都要清理:
126
+
127
+ ```bash
128
+ rm -rf "$HOME/.cache/opencode/packages/nonoka-opencode-provider"
129
+ ```
130
+
131
+ ### 4.6 准备 OpenCode
132
+
133
+ 确保 `opencode` 已在 PATH 中:
134
+
135
+ ```bash
136
+ which opencode
137
+ ```
138
+
139
+ ### 4.7 生成配置
140
+
141
+ ```bash
142
+ cd "$NONOKA_TEST_ROOT"
143
+ export DEEPSEEK_API_KEY="your-key"
144
+ nonoka-cli config init --yes --model deepseek-chat --config "$NONOKA_TEST_ROOT/nonoka.yaml"
145
+ nonoka-cli opencode init --config "$NONOKA_TEST_ROOT/nonoka.yaml"
146
+ ```
147
+
148
+ `opencode init` 写入的 `serverCommand` 会通过 PATH 找到沙箱内的 `nonoka-cli`,并把 stderr
149
+ 重定向到 `/tmp/nonoka-server.log`。
150
+
151
+ ## 5. 日志策略
152
+
153
+ 所有日志都写文件,不再混进 OpenCode TUI:
154
+
155
+ | 日志文件 | 来源 | 用途 |
156
+ | -------- | ---- | ---- |
157
+ | `/tmp/nonoka-server-{cwdhash}.log` | `nonoka-cli --server` 的 stderr | 框架/LLM 调试、消息顺序 |
158
+ | `/tmp/nonoka-provider.log` | `nonoka-opencode-provider` | adapter 输入/输出摘要 |
159
+ | `/tmp/nonoka-trace/trace-YYYYMMDD.jsonl` | `nonoka-cli` bridge | 结构化请求/事件追踪 |
160
+ | `/tmp/nonoka-tui-timeline.ndjson` | provider + bridge | 渲染时间线对齐 |
161
+
162
+ 启动 OpenCode 前设置:
163
+
164
+ ```bash
165
+ export NONOKA_LOG_LEVEL=DEBUG
166
+ ```
167
+
168
+ ## 6. TUI 渲染时间线日志
169
+
170
+ TUI 问题不能仅靠 stdout/stderr 日志定位,需要**事件时间线 + 截图**对齐。
171
+
172
+ provider 和 bridge 共同写 `/tmp/nonoka-tui-timeline.ndjson`,每行一个事件:
173
+
174
+ ```json
175
+ {"ts":"2026-07-04T09:00:01.123Z","source":"bridge","type":"text_delta","len":42,"has_newline":true,"preview":"Hello! How can I..."}
176
+ {"ts":"2026-07-04T09:00:01.145Z","source":"provider","type":"text-delta","len":42,"preview":"Hello! How can I..."}
177
+ {"ts":"2026-07-04T09:00:01.200Z","source":"provider","type":"tool-call","toolName":"write_file"}
178
+ ```
179
+
180
+ 字段说明:
181
+
182
+ - `ts`: ISO 8601 时间戳,精确到毫秒。
183
+ - `source`: `bridge`(nonoka-cli 发出)、`provider`(adapter 转换后)、`test`(测试脚本标记)。
184
+ - `type`: 事件类型。
185
+ - `len`/`has_newline`/`preview`: text 类事件的关键字段,用于判断文字堆叠是否因为后端没发换行。
186
+
187
+ 截屏脚本在每次截图前写入一行 `test` 事件:
188
+
189
+ ```json
190
+ {
191
+ "ts": "2026-07-04T09:00:02.500Z",
192
+ "source": "test",
193
+ "type": "screenshot",
194
+ "file": "01_hello.png"
195
+ }
196
+ ```
197
+
198
+ 事后可用 `scripts/analyze-timeline.py` 把截图与前后 500ms 的事件对齐:
199
+
200
+ ```bash
201
+ python .agents/skills/nonoka-tui-testing/scripts/analyze-timeline.py --root /tmp/nonoka-test
202
+ ```
203
+
204
+ 快速判断:
205
+
206
+ - 如果 bridge 发了 `\n` 但 UI 没分段 → OpenCode / Ink 渲染问题。
207
+ - 如果 bridge 没发 `\n` → nonoka-cli / nonoka 框架问题。
208
+ - 如果 provider 没把 `text_delta` 传给 OpenCode → adapter 问题。
209
+
210
+ ## 7. 文本拼接快照
211
+
212
+ 在 bridge 端维护一个当前 turn 的文本缓冲区,每次 `text_delta` 后把完整文本写入 `/tmp/nonoka-rendered-text.txt`。截图后对比:
213
+
214
+ - 文件里的文本应该是 UI 上应该显示的内容。
215
+ - 如果文件有分段但 UI 没有 → 渲染问题。
216
+ - 如果文件本身就没分段 → 后端问题。
217
+
218
+ ## 8. 辅助脚本
219
+
220
+ | 脚本 | 用途 |
221
+ | ---- | ---- |
222
+ | `scripts/setup-sandbox.sh` | 搭建 `/tmp/nonoka-test` |
223
+ | `scripts/capture-screenshots.py` | 驱动 tmux 输入并触发截图 |
224
+ | `scripts/analyze-timeline.py` | 对齐截图与 timeline |
225
+
226
+ setup 完成后,沙箱内还会生成:
227
+
228
+ - `start-verdaccio.sh`:重新启动 verdaccio。
229
+ - `run-opencode.sh`:前台启动 OpenCode。
230
+ - `run-opencode-tmux.sh`:后台 tmux 启动 OpenCode。
231
+ - `stop-sandbox.sh`:停止 verdaccio 与 tmux session。
232
+
233
+ ## 9. 测试用例
234
+
235
+ ### 9.1 基础对话
236
+
237
+ | ID | 输入 | 通过标准 |
238
+ | ----- | ---------------------- | ----------------------------------------- |
239
+ | TC-01 | `hello` | 3 秒内出现 assistant 回复,无 error 日志 |
240
+ | TC-02 | `what is 2+2?` | 截图包含答案(如 4) |
241
+ | TC-03 | 连续 3 轮简单问答 | provider 日志 session_id 一致,上下文不丢 |
242
+ | TC-04 | 发送一段多段落文本请求 | assistant 回复段落分明,无堆叠 |
243
+
244
+ ### 9.2 复杂任务
245
+
246
+ | ID | 输入 | 通过标准 |
247
+ | ----- | ---------------------------------- | ---------------------------------------------------- |
248
+ | TC-10 | `create a Next.js app in ./my-app` | 出现 write_file / execute_command 工具卡片 |
249
+ | TC-11 | 全部批准 | `my-app/package.json` 存在,且 `npm run build` 成功 |
250
+ | TC-12 | 拒绝一个危险工具 | provider 日志出现 `approved: false`,agent 正确处理 |
251
+ | TC-13 | 多步工具链任务 | server 日志显示 tool_call / tool_result 交替,无死锁 |
252
+ | TC-14 | 让 agent 自己读取它刚写的文件 | 返回内容与文件实际内容一致 |
253
+
254
+ ### 9.3 TUI 专项
255
+
256
+ | ID | 场景 | 通过标准 |
257
+ | ----- | ----------------------------- | -------------------------------------------- |
258
+ | TC-20 | 单轮短回复 | 文字分段,不是连续一团 |
259
+ | TC-21 | 工具调用后 assistant 继续输出 | 文字接在卡片后,不堆叠 |
260
+ | TC-22 | HITL 弹窗 | 工具名、参数、Approve/Decline 完整可见 |
261
+ | TC-23 | 长回复滚动 | 无重影、无截断 |
262
+ | TC-24 | 中文/多语言输出 | 字符宽度正确,不散落/重叠 |
263
+ | TC-25 | 代码块输出 | 渲染为代码块,不是原始 markdown(如 ```) |
264
+ | TC-26 | ANSI / 终端序列 | 无 `[14;4H` 这类裸 ANSI 字符出现在文字中 |
265
+ | TC-27 | 工具卡片展开/折叠 | 点击展开后显示完整参数,折叠后不影响后续文字 |
266
+
267
+ ### 9.4 Session 管理
268
+
269
+ | ID | 操作 | 通过标准 |
270
+ | ----- | ----------------- | -------------------------------------- |
271
+ | TC-30 | `/new` 后发送消息 | 新 session_id,旧会话保留在 SQLite |
272
+ | TC-31 | 切换历史会话 | 能恢复上下文并继续对话 |
273
+ | TC-32 | 重命名会话 | `cli_sessions` 表 `name` 字段更新 |
274
+ | TC-33 | 删除非当前会话 | 删除后列表中不再出现,当前会话不受影响 |
275
+
276
+ ### 9.5 工具实际执行
277
+
278
+ | ID | 操作 | 通过标准 |
279
+ | ----- | --------------------------- | --------------------------------------------------- |
280
+ | TC-40 | `write_file` 创建文件 | 文件存在且内容与参数一致 |
281
+ | TC-41 | `edit_file` 修改文件 | 文件内容被精确替换 |
282
+ | TC-42 | `execute_command echo test` | 返回 `test` |
283
+ | TC-43 | `delete_file` 删除文件 | 文件不存在 |
284
+ | TC-44 | 路径解析 | 相对路径基于 OpenCode cwd,不是 nonoka-cli 安装目录 |
285
+
286
+ ## 10. TUI 测试方法
287
+
288
+ ### 10.1 用 tmux + Playwright 截屏
289
+
290
+ 启动后台 tmux session:
291
+
292
+ ```bash
293
+ cd /tmp/nonoka-test
294
+ ./run-opencode-tmux.sh
295
+ ```
296
+
297
+ 获取 pane id:
298
+
299
+ ```bash
300
+ tmux list-panes -t nonoka-test -F '#{pane_id}'
301
+ ```
302
+
303
+ 输入消息并截图:
304
+
305
+ ```bash
306
+ tmux read -t %0 -l 20
307
+ tmux type -t %0 'hello'
308
+ tmux keys -t %0 Enter
309
+ tmux read -t %0 -l 50
310
+ ```
311
+
312
+ 用 Playwright 截取终端窗口,保存到 `/tmp/nonoka-test/screenshots/`。
313
+
314
+ ### 10.2 UI 检查清单
315
+
316
+ 每次截图后检查:
317
+
318
+ - [ ] 用户输入完整显示。
319
+ - [ ] assistant 回复分段清晰,段落间有空行或缩进。
320
+ - [ ] 工具调用卡片独立渲染,不与文字混排。
321
+ - [ ] 工具调用后文字接在卡片下方,不紧跟卡片右侧。
322
+ - [ ] HITL 弹窗完整,工具名、参数、Approve/Decline 按钮可见。
323
+ - [ ] 滚动后无重影、无截断。
324
+ - [ ] 输入框光标位置正确,无明显延迟。
325
+ - [ ] 中文/日文/韩文等宽字符宽度正确,无散落。
326
+ - [ ] 代码块有语法高亮或独立背景,不是裸 markdown。
327
+ - [ ] 无裸 ANSI 转义序列(如 `[14;4H`)出现在文字中。
328
+
329
+ ## 11. 问题定位决策树
330
+
331
+ ```
332
+ 发现问题
333
+
334
+ ├─ 是否卡住?
335
+ │ ├─ 是 → 查看 /tmp/nonoka-provider.log
336
+ │ │ ├─ 无 doStream start → OpenCode 未调用 provider
337
+ │ │ ├─ spawned child 后无响应 → server 启动失败,看 /tmp/nonoka-server.log
338
+ │ │ ├─ 收到 finish/error 但未 close → provider bug
339
+ │ │ └─ 无 finish 且 server 也无 final → nonoka 框架 / runner 层
340
+ │ └─ 否 → 看 TUI / 结果
341
+
342
+ ├─ 是否结果错误?
343
+ │ ├─ 先隔离 nonoka-cli --server:直接喂 NDJSON,看 stdout 是否正确
344
+ │ ├─ stdout 正确 → adapter 或 OpenCode 问题
345
+ │ └─ stdout 错误 → nonoka-cli / nonoka 框架问题
346
+
347
+ ├─ 是否 UI 异常?
348
+ │ ├─ 查看 /tmp/nonoka-tui-timeline.ndjson
349
+ │ │ ├─ bridge text_delta 含 \n 但 UI 未分段 → OpenCode / Ink 渲染问题
350
+ │ │ ├─ bridge text_delta 不含 \n → nonoka-cli / nonoka 框架问题
351
+ │ │ └─ provider 未转发 text_delta → adapter 问题
352
+ │ ├─ 对比 /tmp/nonoka-rendered-text.txt 与截图
353
+ │ └─ 检查 CJK/emoji 宽度、ANSI 泄漏、代码块 markdown
354
+
355
+ └─ 是否 Session / 工具执行异常?
356
+ ├─ 检查 SQLite `cli_sessions` 表
357
+ ├─ 检查 tool 实际副作用(文件是否存在、命令是否执行)
358
+ └─ 检查 tool 的路径解析是否基于 cwd
359
+ ```
360
+
361
+ ## 12. 回归与发布
362
+
363
+ 修复后:
364
+
365
+ 1. 重新跑 TC-01 ~ TC-44,全部通过。
366
+ 2. 如果修改了 `nonoka-agent`:
367
+ - 更新 `nonoka-agent/pyproject.toml` 版本号。
368
+ - `uv run twine publish` 发布到 PyPI。
369
+ - 更新 `nonoka-cli/pyproject.toml` 的 `nonoka` 依赖版本。
370
+ 3. 如果修改了 `nonoka-opencode-provider`:
371
+ - 更新 `package.json` 版本号。
372
+ - `npm run build && npm publish`。
373
+ 4. 如果修改了 `nonoka-cli`:
374
+ - 更新 `pyproject.toml` 版本号。
375
+ - 跑 `uv run pytest tests/unit`。
376
+ - 必要时发布新版到 PyPI。
377
+
378
+ ## 13. 常见问题
379
+
380
+ ### 13.1 OpenCode 仍然加载旧 provider
381
+
382
+ 症状:timeline 里没有 `session_init`,或行为不像最新代码。
383
+
384
+ 解决:
385
+
386
+ ```bash
387
+ rm -rf ~/.cache/opencode/packages/nonoka-opencode-provider
388
+ # 然后重启 OpenCode
389
+ ```
390
+
391
+ ### 13.2 verdaccio 启动后 npm publish 报 ECONNREFUSED
392
+
393
+ 症状:`npm publish --registry http://localhost:4873` 失败。
394
+
395
+ 原因:脚本里的 `npx verdaccio &` 在 `bash -c` 包装下可能拿到错误的 PID,导致 verdaccio 没真正启动。
396
+
397
+ 解决:使用沙箱内生成的 `start-verdaccio.sh`(它用 `nohup` 启动并等待端口可用)。
398
+
399
+ ### 13.3 沙箱内的 nonoka-cli 找不到
400
+
401
+ 症状:OpenCode 启动后 provider 子进程报 `nonoka-cli: command not found`。
402
+
403
+ 原因:`opencode.json` 的 `serverCommand` 依赖 PATH,而 OpenCode 的环境没有继承 `env.sh`。
404
+
405
+ 解决:始终通过 `run-opencode.sh` 或 `run-opencode-tmux.sh` 启动,它们会先 `source env.sh`。
406
+
407
+ ### 13.4 API key 未生效
408
+
409
+ 症状:`nonoka-cli config init` 提示 API key 来自环境变量,但值为空。
410
+
411
+ 原因:`.env` 里用的是 `OPENAI_API_KEY`,而 deepseek-chat 需要 `DEEPSEEK_API_KEY`。
412
+
413
+ 解决:`env.sh` 会自动把 `OPENAI_API_KEY` 同步到 `DEEPSEEK_API_KEY`。手动运行时执行 `source env.sh` 即可。
@@ -0,0 +1,48 @@
1
+ # nonoka-cli TUI 检查清单速查表
2
+
3
+ 每次截屏后逐项检查,勾选通过项,对未通过项记录截图文件名与现象。
4
+
5
+ ## 基础布局
6
+
7
+ - [ ] 终端整体布局未崩溃,边界对齐。
8
+ - [ ] 顶部/底部状态栏信息完整,不被内容覆盖。
9
+ - [ ] 输入框始终可见,光标位置正确。
10
+ - [ ] 滚动后无重影、无残影、无内容覆盖。
11
+
12
+ ## 文字渲染
13
+
14
+ - [ ] 用户输入的消息完整显示。
15
+ - [ ] Assistant 回复按段落分段,段间有视觉分隔。
16
+ - [ ] 长文本自动换行,不超出容器边界。
17
+ - [ ] 代码块渲染为代码块(独立背景/语法高亮),不是裸 markdown(如 ```)。
18
+ - [ ] 内联代码、粗体、斜体等 markdown 样式正确。
19
+ - [ ] 工具调用后,后续文字出现在卡片下方,不紧跟卡片右侧。
20
+
21
+ ## 多语言与特殊字符
22
+
23
+ - [ ] 中文、日文、韩文等 CJK 字符宽度正确,不散落或重叠。
24
+ - [ ] Emoji 宽度正确,不导致后续文字错位。
25
+ - [ ] 无裸 ANSI 转义序列(如 `[14;4H`)出现在可见文字中。
26
+
27
+ ## 工具调用与 HITL
28
+
29
+ - [ ] 工具调用卡片独立渲染,有图标/工具名/参数。
30
+ - [ ] 工具调用卡片与文字有明显分隔。
31
+ - [ ] 工具执行结果(成功/失败)状态可见。
32
+ - [ ] HITL 弹窗完整显示工具名、参数、Approve/Decline/Modify 按钮。
33
+ - [ ] HITL 弹窗不遮挡输入框或历史消息。
34
+ - [ ] 批准/拒绝后 UI 及时更新,无残留弹窗。
35
+
36
+ ## 交互响应
37
+
38
+ - [ ] 输入时无明显延迟。
39
+ - [ ] Enter 发送后输入框清空。
40
+ - [ ] 等待 AI 回复时有 loading / thinking 指示。
41
+ - [ ] 多轮对话上下文连续,无会话重置错觉。
42
+
43
+ ## 长任务与复杂输出
44
+
45
+ - [ ] 长回复滚动流畅,无跳变。
46
+ - [ ] 多个工具调用按时间顺序排列。
47
+ - [ ] 工具调用失败时错误信息可读。
48
+ - [ ] 任务完成后有清晰的总结输出。
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ """Align screenshots with the TUI event timeline.
3
+
4
+ Usage:
5
+ python analyze-timeline.py --root /tmp/nonoka-test --window 0.5
6
+
7
+ Output:
8
+ For each screenshot, prints the bridge/provider events within +/- window
9
+ seconds of the screenshot timestamp. Helps determine whether a rendering
10
+ issue originates in the backend or the frontend.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ from datetime import datetime, timedelta, timezone
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+
22
+ def parse_ts(ts: str) -> datetime:
23
+ """Parse an ISO 8601 timestamp with naive or explicit timezone."""
24
+ # Replace trailing Z with +00:00 for fromisoformat compatibility.
25
+ if ts.endswith("Z"):
26
+ ts = ts[:-1] + "+00:00"
27
+ dt = datetime.fromisoformat(ts)
28
+ if dt.tzinfo is None:
29
+ dt = dt.replace(tzinfo=timezone.utc)
30
+ return dt
31
+
32
+
33
+ def load_timeline(root: Path) -> list[dict[str, Any]]:
34
+ timeline = root / "nonoka-tui-timeline.ndjson"
35
+ events: list[dict[str, Any]] = []
36
+ if not timeline.exists():
37
+ return events
38
+ for line in timeline.read_text(encoding="utf-8").splitlines():
39
+ line = line.strip()
40
+ if not line:
41
+ continue
42
+ try:
43
+ events.append(json.loads(line))
44
+ except json.JSONDecodeError:
45
+ continue
46
+ return events
47
+
48
+
49
+ def describe_event(event: dict[str, Any]) -> str:
50
+ parts = [event.get("source", "?"), event.get("type", "?")]
51
+ for key in ("toolName", "tool_name", "step", "file", "len", "has_newline"):
52
+ if key in event:
53
+ parts.append(f"{key}={event[key]}")
54
+ preview = event.get("preview")
55
+ if preview is not None:
56
+ parts.append(f"preview={preview[:60]!r}")
57
+ return " ".join(parts)
58
+
59
+
60
+ def analyze(root: Path, window_seconds: float) -> None:
61
+ events = load_timeline(root)
62
+ if not events:
63
+ print("No timeline events found.")
64
+ return
65
+
66
+ screenshots = [e for e in events if e.get("type") == "screenshot"]
67
+ if not screenshots:
68
+ print("No screenshot events found.")
69
+ return
70
+
71
+ for shot in screenshots:
72
+ shot_ts = parse_ts(shot["ts"])
73
+ shot_file = shot.get("file", "unknown")
74
+ print(f"\n=== {Path(shot_file).name} @ {shot['ts']} ===")
75
+ for event in events:
76
+ if event.get("type") == "screenshot":
77
+ continue
78
+ event_ts = parse_ts(event["ts"])
79
+ delta = abs((event_ts - shot_ts).total_seconds())
80
+ if delta <= window_seconds:
81
+ marker = " "
82
+ if event_ts < shot_ts:
83
+ marker = "<-"
84
+ elif event_ts > shot_ts:
85
+ marker = "->"
86
+ print(f" {marker} {delta:.3f}s {describe_event(event)}")
87
+
88
+
89
+ def main() -> int:
90
+ parser = argparse.ArgumentParser(description="Align screenshots with TUI timeline")
91
+ parser.add_argument("--root", default="/tmp/nonoka-test", help="Sandbox root")
92
+ parser.add_argument("--window", type=float, default=0.5, help="Time window in seconds around each screenshot")
93
+ args = parser.parse_args()
94
+
95
+ analyze(Path(args.root), args.window)
96
+ return 0
97
+
98
+
99
+ if __name__ == "__main__":
100
+ raise SystemExit(main())