@scemoon/cdh 1.0.4 → 1.0.6

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 (269) hide show
  1. package/ai-dlc-skill/SKILL.md +4 -2
  2. package/ai-dlc-skill/agents/master.md +6 -0
  3. package/ai-dlc-skill/agents/plan-agent.md +2 -2
  4. package/ai-dlc-skill/agents/understand-agent.md +4 -4
  5. package/ai-dlc-skill/agents/verify-agent.md +1 -1
  6. package/ai-dlc-skill/architecture/project-structure.md +10 -10
  7. package/ai-dlc-skill/brownfield/README.md +2 -2
  8. package/ai-dlc-skill/brownfield/generate-context.sh +3 -3
  9. package/ai-dlc-skill/brownfield/scripts/extract-api.sh +5 -5
  10. package/ai-dlc-skill/components/backend.md +1 -1
  11. package/ai-dlc-skill/components/web.md +2 -2
  12. package/ai-dlc-skill/contracts/README.md +3 -3
  13. package/ai-dlc-skill/cross-tool/cline/export.sh +1 -1
  14. package/ai-dlc-skill/cross-tool/copilot/export.sh +1 -1
  15. package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +7 -3
  16. package/ai-dlc-skill/cross-tool/onecode/export.sh +14 -8
  17. package/ai-dlc-skill/phases/deliver/lifecycle.md +6 -6
  18. package/ai-dlc-skill/phases/deliver/prompt.md +2 -2
  19. package/ai-dlc-skill/phases/deliver/rules.md +1 -1
  20. package/ai-dlc-skill/phases/plan/lifecycle.md +11 -11
  21. package/ai-dlc-skill/phases/plan/prompt.md +6 -6
  22. package/ai-dlc-skill/phases/plan/rules.md +1 -1
  23. package/ai-dlc-skill/phases/understand/entry.md +1 -1
  24. package/ai-dlc-skill/phases/understand/lifecycle.md +7 -7
  25. package/ai-dlc-skill/phases/understand/prompt.md +8 -8
  26. package/ai-dlc-skill/phases/understand/rules.md +5 -5
  27. package/ai-dlc-skill/phases/verify/lifecycle.md +24 -24
  28. package/ai-dlc-skill/phases/verify/prompt.md +8 -8
  29. package/ai-dlc-skill/phases/verify/rules.md +9 -9
  30. package/ai-dlc-skill/providers/README.md +2 -2
  31. package/ai-dlc-skill/providers/aliyun/preview.yaml +2 -2
  32. package/ai-dlc-skill/providers/tcb/preview.yaml +2 -2
  33. package/ai-dlc-skill/requirements.md +7 -7
  34. package/ai-dlc-skill/skill.yaml +4 -4
  35. package/ai-dlc-skill/templates/integration/CHANGELOG.md +2 -2
  36. package/ai-dlc-skill/templates/integration/contract-diff.md +7 -7
  37. package/ai-dlc-skill/templates/integration/contract-spec.md +7 -7
  38. package/ai-dlc-skill/templates/integration/runtime-contract.yaml +1 -1
  39. package/ai-dlc-skill/templates/project/README.md +18 -19
  40. package/ai-dlc-skill/templates/project/template.md +21 -21
  41. package/ai-dlc-skill/walkthrough/collect.sh +1 -1
  42. package/ai-dlc-skill/walkthrough/template.md +2 -2
  43. package/ai-dlc-skill/workflows/ai-dlc.yaml +12 -12
  44. package/ai-dlc-skill/workflows/deployment.yaml +4 -4
  45. package/cdh/__pycache__/cdh_mcp_injector.cpython-314.pyc +0 -0
  46. package/cdh/__pycache__/cdh_mcp_loader.cpython-314.pyc +0 -0
  47. package/cdh/__pycache__/cdh_mcp_manager.cpython-314.pyc +0 -0
  48. package/cdh/__pycache__/cdh_session_aggregator.cpython-314.pyc +0 -0
  49. package/cdh/__pycache__/cdh_skill_loader.cpython-314.pyc +0 -0
  50. package/cdh/__pycache__/cdh_skill_manager.cpython-314.pyc +0 -0
  51. package/cdh/__pycache__/cli.cpython-313.pyc +0 -0
  52. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  53. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  54. package/cdh/cdh_mcp_injector.py +115 -0
  55. package/cdh/cdh_mcp_loader.py +67 -0
  56. package/cdh/cdh_mcp_manager.py +73 -0
  57. package/cdh/cdh_session_aggregator.py +258 -0
  58. package/cdh/cdh_skill_manager.py +105 -0
  59. package/cdh/cli.py +64 -4
  60. package/cdh/scaffold.py +192 -41
  61. package/onecode/__init__.py +1 -1
  62. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  63. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  64. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  65. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  66. package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
  67. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  68. package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
  69. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  70. package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
  71. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  72. package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
  73. package/onecode/agent/__pycache__/project_doc.cpython-314.pyc +0 -0
  74. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  75. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  76. package/onecode/agent/agents/types.py +47 -164
  77. package/onecode/agent/attachments.py +0 -1
  78. package/onecode/agent/cdh_loader.py +17 -25
  79. package/onecode/agent/context.py +8 -3
  80. package/onecode/agent/engine.py +835 -220
  81. package/onecode/agent/hooks.py +1 -1
  82. package/onecode/agent/onecode_agent_acp.py +460 -47
  83. package/onecode/agent/onecode_agent_acp.py.bak +2447 -0
  84. package/onecode/agent/permissions.py +0 -1
  85. package/onecode/agent/project_doc.py +19 -0
  86. package/onecode/agent/session.py +17 -3
  87. package/onecode/agent/snapshot.py +3 -4
  88. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  89. package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
  90. package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
  91. package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
  92. package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
  93. package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
  94. package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
  95. package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
  96. package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
  97. package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
  98. package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
  99. package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
  100. package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
  101. package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
  102. package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
  103. package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
  104. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  105. package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
  106. package/onecode/agent/tools/agent_tools.py +4 -6
  107. package/onecode/agent/tools/apply_patch_tool.py +2 -8
  108. package/onecode/agent/tools/bash_tool.py +32 -12
  109. package/onecode/agent/tools/communication_tools.py +4 -5
  110. package/onecode/agent/tools/config_tool.py +3 -3
  111. package/onecode/agent/tools/cron_tools.py +4 -5
  112. package/onecode/agent/tools/file_ops.py +37 -3
  113. package/onecode/agent/tools/file_tools.py +10 -11
  114. package/onecode/agent/tools/git_tools.py +2 -2
  115. package/onecode/agent/tools/lsp_tools.py +2 -2
  116. package/onecode/agent/tools/mcp_tools.py +3 -5
  117. package/onecode/agent/tools/permission_handler.py +1 -1
  118. package/onecode/agent/tools/protocol.py +1 -1
  119. package/onecode/agent/tools/registry.py +69 -2
  120. package/onecode/agent/tools/sandbox.py +110 -16
  121. package/onecode/agent/tools/skill_tools.py +2 -2
  122. package/onecode/agent/tools/todo_tools.py +38 -19
  123. package/onecode/agent/tools/web_tools.py +6 -7
  124. package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
  125. package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
  126. package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
  127. package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
  128. package/onecode/cli.py +186 -6
  129. package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
  130. package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
  131. package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
  132. package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
  133. package/onecode/codebase/chunker.py +0 -5
  134. package/onecode/codebase/indexer.py +14 -1
  135. package/onecode/codebase/retriever.py +13 -2
  136. package/onecode/codebase/storage.py +5 -4
  137. package/onecode/commands.py +81 -6
  138. package/onecode/config.py +41 -12
  139. package/onecode/config_screen.py +7 -3
  140. package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
  141. package/onecode/mcp/manager.py +2 -3
  142. package/onecode/memory/__init__.py +7 -53
  143. package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
  144. package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
  145. package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
  146. package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
  147. package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
  148. package/onecode/memory/backend.py +13 -39
  149. package/onecode/memory/pyramid.py +2 -71
  150. package/onecode/memory/recall.py +38 -3
  151. package/onecode/migrate.py +122 -0
  152. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  153. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  154. package/onecode/models/messages.py +58 -0
  155. package/onecode/models/provider.py +20 -2
  156. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  157. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  158. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  159. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  160. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  161. package/onecode/models/providers/anthropic_provider.py +59 -12
  162. package/onecode/models/providers/glm_provider.py +40 -0
  163. package/onecode/models/providers/minimax_provider.py +3 -0
  164. package/onecode/models/providers/minimaxi_provider.py +3 -0
  165. package/onecode/models/providers/ollama_provider.py +3 -0
  166. package/onecode/server/app.py +1 -1
  167. package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
  168. package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
  169. package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
  170. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  171. package/onecode/skills/argument_substitution.py +0 -1
  172. package/onecode/skills/bootstrap.py +124 -0
  173. package/onecode/skills/loader.py +35 -53
  174. package/onecode/skills/manager.py +2 -2
  175. package/onecode/storage/activity.py +3 -4
  176. package/onecode/storage/session.py +3 -4
  177. package/onecode/tasks/manager.py +23 -10
  178. package/onecode/trace/tracer.py +2 -3
  179. package/package.json +1 -1
  180. package/pyproject.toml +5 -1
  181. package/run.js +68 -14
  182. package/tui/__pycache__/about.cpython-314.pyc +0 -0
  183. package/tui/__pycache__/app.cpython-313.pyc +0 -0
  184. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  185. package/tui/__pycache__/danger.cpython-314.pyc +0 -0
  186. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  187. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  188. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  189. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  190. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  191. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  192. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  193. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  194. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  195. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  196. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  197. package/tui/acp/agent.py +125 -58
  198. package/tui/acp/api.py +12 -0
  199. package/tui/acp/messages.py +3 -1
  200. package/tui/acp/prompt.py +0 -57
  201. package/tui/acp/protocol.py +6 -0
  202. package/tui/app.py +11 -33
  203. package/tui/messages.py +2 -0
  204. package/tui/path_complete.py +1 -1
  205. package/tui/paths.py +16 -0
  206. package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
  207. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  208. package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
  209. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  210. package/tui/screens/__pycache__/main.cpython-313.pyc +0 -0
  211. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  212. package/tui/screens/__pycache__/permissions.cpython-314.pyc +0 -0
  213. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  214. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  215. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  216. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  217. package/tui/screens/__pycache__/tool_content_screen.cpython-314.pyc +0 -0
  218. package/tui/screens/component_picker.py +35 -15
  219. package/tui/screens/diff_screen.py +84 -0
  220. package/tui/screens/log.py +25 -25
  221. package/tui/screens/main.py +48 -26
  222. package/tui/screens/projects.tcss +5 -1
  223. package/tui/screens/projects_app.py +6 -6
  224. package/tui/screens/projects_screen.py +19 -39
  225. package/tui/screens/sessions.py +3 -5
  226. package/tui/session_tracker.py +3 -0
  227. package/tui/settings_schema.py +1 -1
  228. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  229. package/tui/visuals/columns.py +1 -2
  230. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  231. package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
  232. package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
  233. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  234. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  235. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  236. package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
  237. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  238. package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
  239. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  240. package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
  241. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  242. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  243. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  244. package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
  245. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  246. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  247. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  248. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  249. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  250. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  251. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  252. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  253. package/tui/widgets/agent_response.py +0 -1
  254. package/tui/widgets/agent_thought.py +0 -1
  255. package/tui/widgets/conversation.py +157 -14
  256. package/tui/widgets/diff_view.py +10 -0
  257. package/tui/widgets/load_more.py +24 -0
  258. package/tui/widgets/modified_files.py +86 -12
  259. package/tui/widgets/plan.py +1 -1
  260. package/tui/widgets/project_grid_select.py +9 -2
  261. package/tui/widgets/session_grid_select.py +6 -0
  262. package/tui/widgets/side_bar.py +10 -0
  263. package/tui/widgets/subagent.py +92 -58
  264. package/tui/widgets/subagent_screen.py +638 -115
  265. package/tui/widgets/terminal.py +1 -2
  266. package/tui/widgets/terminal_tool.py +11 -2
  267. package/tui/widgets/tool_call.py +35 -2
  268. package/onecode/memory/offload.py +0 -114
  269. package/onecode/memory/symbolic.py +0 -164
@@ -43,16 +43,31 @@ def debug_log(*args, **kwargs):
43
43
  print("[onecode]", *args, file=sys.stderr, flush=True)
44
44
 
45
45
 
46
- _CRASH_LOG_DIR = Path.home() / ".cdh" / "logs"
46
+ def info_log(*args, **kwargs):
47
+ """INFO-level lifecycle log so critical events survive even default log_level=info.
48
+
49
+ ``debug_log`` is filtered out at INFO; previously all ACP/subagent
50
+ diagnostic markers were DEBUG, so users running with the default
51
+ ``log_level: info`` saw an empty log and could not diagnose the
52
+ subagent hang. Use ``info_log`` for critical lifecycle boundaries
53
+ (adapter init, cancel, subagent dispatch markers).
54
+ """
55
+ if logger.handlers or logging.getLogger().handlers:
56
+ logger.info(" ".join(str(a) for a in args), **kwargs)
57
+ else:
58
+ print("[onecode]", *args, file=sys.stderr, flush=True)
59
+
60
+
61
+ _CRASH_LOG_DIR = Path.home() / ".onecode" / "logs"
47
62
 
48
63
 
49
64
  def _dump_crash(context: str) -> None:
50
- """Write traceback to ``~/.cdh/logs/cdh_crash.log`` so it survives subprocess exit."""
65
+ """Write traceback to ``~/.onecode/logs/onecode_crash.log`` so it survives subprocess exit."""
51
66
  import os
52
67
  import traceback
53
68
  now = __import__("datetime").datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
54
69
  _CRASH_LOG_DIR.mkdir(parents=True, exist_ok=True)
55
- path = _CRASH_LOG_DIR / "cdh_crash.log"
70
+ path = _CRASH_LOG_DIR / "onecode_crash.log"
56
71
  try:
57
72
  lines = [f"=== crash at {now} context={context} ===\n"]
58
73
  for tb_line in traceback.format_exc().splitlines(True):
@@ -73,7 +88,6 @@ from onecode.models.registry import ModelRegistry
73
88
  from onecode.models.messages import (
74
89
  StreamEventType,
75
90
  ToolCategory,
76
- AgentMessage,
77
91
  TextBlock,
78
92
  ThinkBlock,
79
93
  ToolCall as MsgToolCall,
@@ -82,14 +96,8 @@ from onecode.models.messages import (
82
96
  LifecycleStatus,
83
97
  get_tool_category,
84
98
  )
99
+ import onecode.models.providers # noqa: F401 trigger ProviderRegistry.register()
85
100
 
86
- from onecode.models.providers.minimaxi_provider import MiniMaxiProvider
87
- from onecode.models.providers.minimax_provider import MiniMaxProvider
88
- from onecode.models.providers.anthropic_provider import AnthropicProvider
89
- from onecode.models.providers.openai_provider import OpenAIProvider
90
- from onecode.models.providers.deepseek_provider import DeepSeekProvider
91
- from onecode.models.providers.glm_provider import GLMProvider
92
- from onecode.models.providers.ollama_provider import OllamaProvider
93
101
 
94
102
 
95
103
  _CATEGORY_TO_ACP_KIND: dict[str, str] = {
@@ -325,8 +333,8 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
325
333
  (Markdown / Static). JSON-fence fallbacks only kick in for tools we
326
334
  don't recognise, so known tools render as their intended visual.
327
335
 
328
- Write emits a Markdown block (`path` header + code fence) so the
329
- content reads as code rather than a noisy all-additions diff.
336
+ Write emits a code fence so the content reads as proper code
337
+ rather than a noisy all-additions diff view.
330
338
  Edit keeps the diff block because there are real before/after changes.
331
339
  """
332
340
  if not arguments:
@@ -336,8 +344,8 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
336
344
  return []
337
345
 
338
346
  _HEADER_ONLY_TOOLS = frozenset({
339
- "TodoCreate", "TodoGet", "TodoList", "TodoUpdate", "TodoOutput", "TodoStop",
340
- "Glob", "Spawn",
347
+ "TodoCreate", "TodoGet", "TodoList", "TodoUpdate", "TodoOutput", "TodoStop", "TodoClear",
348
+ "Glob", "List", "Spawn",
341
349
  })
342
350
  if name in _HEADER_ONLY_TOOLS:
343
351
  return []
@@ -351,7 +359,7 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
351
359
  "type": "content",
352
360
  "content": {
353
361
  "type": "text",
354
- "text": f"📄 {path}\n```{lang}\n{content}\n```",
362
+ "text": f"```{lang}\n{content}\n```",
355
363
  },
356
364
  }]
357
365
 
@@ -378,6 +386,21 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
378
386
  },
379
387
  }]
380
388
 
389
+ if name == "Grep":
390
+ pattern = str(arguments.get("pattern", ""))
391
+ include = arguments.get("include")
392
+ if pattern:
393
+ text = f"🔍 Pattern: `{pattern}`"
394
+ if include:
395
+ text += f"\n📁 Filter: `{include}`"
396
+ return [{
397
+ "type": "content",
398
+ "content": {"type": "text", "text": text},
399
+ }]
400
+
401
+ if name == "ToolSearch":
402
+ return []
403
+
381
404
  if name == "ApplyPatch":
382
405
  patch = str(arguments.get("patch", ""))
383
406
  if patch:
@@ -424,11 +447,7 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
424
447
  "content": {"type": "text", "text": "\n".join(lines)},
425
448
  }] if lines else []
426
449
 
427
- args_text = json.dumps(arguments, indent=2, ensure_ascii=False)
428
- return [{
429
- "type": "content",
430
- "content": {"type": "text", "text": f"```json\n{args_text}\n```"},
431
- }]
450
+ return []
432
451
 
433
452
 
434
453
  def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
@@ -456,6 +475,19 @@ def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
456
475
  return result_text
457
476
  if not isinstance(parsed, dict):
458
477
  return result_text
478
+ if tool_name == "ToolSearch":
479
+ matches = parsed.get("matches", [])
480
+ if matches:
481
+ lines = [f"🔍 Found {len(matches)} tool(s):", ""]
482
+ for m in matches:
483
+ name = m.get("name", "")
484
+ desc = m.get("description", "")
485
+ if desc:
486
+ lines.append(f" • **{name}** — {desc}")
487
+ else:
488
+ lines.append(f" • **{name}**")
489
+ return "\n".join(lines)
490
+ return "No matching tools found."
459
491
  if "error" in parsed:
460
492
  return str(parsed["error"])
461
493
  # Read: show actual file content (code fence + language wrapping in _emit_tool_result)
@@ -464,10 +496,12 @@ def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
464
496
  # Todo tools: collapse verbose state echoes to one-liner.
465
497
  if tool_name in _STATUS_ONLY_TOOLS:
466
498
  return "✓ updated"
499
+ if tool_name == "Write":
500
+ return ""
467
501
  if parsed.get("success") is True:
468
502
  if path := parsed.get("path"):
469
503
  return f"✓ {path}"
470
- visible = {k: v for k, v in parsed.items() if k != "success"}
504
+ visible = {k: v for k, v in parsed.items() if k != "success" and v not in ("", None)}
471
505
  if not visible:
472
506
  return "✓ done"
473
507
  try:
@@ -484,6 +518,7 @@ _STATUS_ONLY_TOOLS = frozenset({
484
518
  "TodoUpdate",
485
519
  "TodoOutput",
486
520
  "TodoStop",
521
+ "TodoClear",
487
522
  })
488
523
 
489
524
 
@@ -661,6 +696,14 @@ class CDHACPAdapter:
661
696
  self._request_seq = 0
662
697
  self._perm_store = PermissionStore()
663
698
  self._ask_user_future: asyncio.Future[dict] | None = None
699
+ # Diagnostics: per-subagent ACP forward counters (id → int)
700
+ self._subagent_fwd_count: dict[str, int] = {}
701
+ self._subagent_fwd_bytes: dict[str, int] = {}
702
+ # Display state for tools invoked *inside* a subagent, keyed by a
703
+ # namespaced id ``"sa:{subagent_id}:{inner_tool_id}"`` (kept separate
704
+ # from ``self.tool_calls`` so subagent tool cards never collide with
705
+ # main-agent tool ids nor get persisted into the main session view).
706
+ self._subagent_tool_calls: dict[str, dict] = {}
664
707
 
665
708
  def _perm_store_load(self, cwd: str | None = None) -> None:
666
709
  """Load permission overrides from ``.cdh/permissions.json``."""
@@ -837,9 +880,23 @@ class CDHACPAdapter:
837
880
  def cancel_prompt(self):
838
881
  """Synchronously cancel the current prompt (called from main loop)."""
839
882
  if self.agent:
883
+ n_children = len(self.agent._child_engines)
884
+ child_ids = [hex(id(c)) for c in self.agent._child_engines]
885
+ info_log(
886
+ "[ACP-CANCEL] cancel_prompt called; marking agent._cancelled=True; "
887
+ "children=%d ids=%s already_cancelled=%s",
888
+ n_children, child_ids, self.agent._cancelled,
889
+ )
840
890
  self.agent._cancelled = True
841
891
  for child in self.agent._child_engines:
842
892
  child._cancelled = True
893
+ info_log(
894
+ "[ACP-CANCEL] after mark: agent._cancelled=%s subagent_fwd=%s",
895
+ self.agent._cancelled,
896
+ dict(self._subagent_fwd_count),
897
+ )
898
+ else:
899
+ info_log("[ACP-CANCEL] cancel_prompt called but self.agent is None")
843
900
 
844
901
  def resolve_ask_user(self, answer: str, cancelled: bool) -> dict:
845
902
  """Resolve the pending ask_user request with the user's answer.
@@ -857,6 +914,156 @@ class CDHACPAdapter:
857
914
  "update": update,
858
915
  })
859
916
 
917
+ def _subagent_tool_ns_id(self, subagent_id: str, inner_tool_id: str) -> str:
918
+ """Namespaced toolCallId so subagent tool cards never collide with
919
+ main-agent tool ids in the TUI or in ``self.tool_calls``."""
920
+ return f"sa:{subagent_id}:{inner_tool_id}"
921
+
922
+ def _handle_subagent_tool_call(self, event) -> None:
923
+ """Render a tool invoked *inside* a subagent as a real tool_call /
924
+ tool_call_update sessionUpdate tagged with ``subagentId`` so the TUI
925
+ mounts a ToolCall card inside the owning SubAgent widget (same style
926
+ as main-conversation tools) instead of a ``[Tool: X]`` text line."""
927
+ sa_id = event.subagent_id
928
+ ns_id = self._subagent_tool_ns_id(sa_id, event.tool_id)
929
+ tname = event.tool_name or "Tool"
930
+ if tname == "Spawn":
931
+ # Nested spawn is disallowed (depth limit); skip rendering.
932
+ return
933
+ tool_kind = _kind_for_category(event.tool_category)
934
+
935
+ if event.subagent_tool_phase != "complete":
936
+ # start: in_progress card with the tool name as title.
937
+ entry = self._subagent_tool_calls.get(ns_id)
938
+ if entry is None:
939
+ entry = {
940
+ "sessionUpdate": "tool_call",
941
+ "toolCallId": ns_id,
942
+ "subagentId": sa_id,
943
+ "title": tname,
944
+ "kind": tool_kind,
945
+ "status": "in_progress",
946
+ "content": [],
947
+ "_tool_name": tname,
948
+ "_tool_args": {},
949
+ }
950
+ self._subagent_tool_calls[ns_id] = entry
951
+ else:
952
+ entry["subagentId"] = sa_id
953
+ entry["kind"] = tool_kind
954
+ entry["status"] = "in_progress"
955
+ entry["_tool_name"] = tname
956
+ self.send_session_update(entry)
957
+ return
958
+
959
+ # complete: enrich title from args + attach args content.
960
+ args = event.tool_args or {}
961
+ title = tname
962
+ if args:
963
+ if args.get("path", ""):
964
+ title = f"{tname}: {_short_path(args.get('path', ''), self.agent._project_dir)}"
965
+ elif tname == "Bash":
966
+ cmd = str(args.get("command", ""))
967
+ if cmd:
968
+ title = f"Bash: {cmd}"
969
+ elif tname in ("TodoCreate", "TodoUpdate"):
970
+ subject = str(args.get("subject", ""))
971
+ if subject:
972
+ title = f"{tname}: {subject}"
973
+ elif tname in ("TodoGet", "TodoStop", "TodoOutput"):
974
+ tid = str(args.get("taskId", args.get("task_id", "")))
975
+ if tid:
976
+ title = f"{tname}: {tid}"
977
+ elif tname == "Glob":
978
+ pattern = str(args.get("pattern", ""))
979
+ if pattern:
980
+ title = f"Glob: {pattern}"
981
+ elif tname == "ToolSearch":
982
+ query = str(args.get("query", ""))
983
+ if query:
984
+ title = f"ToolSearch: {query}"
985
+ else:
986
+ for key in ("query", "command", "pattern", "name", "subject"):
987
+ val = str(args.get(key, ""))
988
+ if val:
989
+ title = f"{tname}: {val}"
990
+ break
991
+ content = _build_tool_call_content(tname, args)
992
+ entry = self._subagent_tool_calls.get(ns_id)
993
+ if entry is None:
994
+ entry = {
995
+ "sessionUpdate": "tool_call",
996
+ "toolCallId": ns_id,
997
+ "subagentId": sa_id,
998
+ "title": tname,
999
+ "kind": tool_kind,
1000
+ "status": "in_progress",
1001
+ "content": [],
1002
+ "_tool_name": tname,
1003
+ "_tool_args": {},
1004
+ }
1005
+ self._subagent_tool_calls[ns_id] = entry
1006
+ entry.update({
1007
+ "sessionUpdate": "tool_call_update",
1008
+ "toolCallId": ns_id,
1009
+ "subagentId": sa_id,
1010
+ "title": title,
1011
+ "status": "in_progress",
1012
+ "content": content,
1013
+ "_tool_name": tname,
1014
+ "_tool_args": args,
1015
+ })
1016
+ self.send_session_update(entry)
1017
+
1018
+ def _handle_subagent_tool_result(self, event) -> None:
1019
+ """Attach a subagent tool's result to its card (completed/failed)."""
1020
+ sa_id = event.subagent_id
1021
+ ns_id = self._subagent_tool_ns_id(sa_id, event.tool_id)
1022
+ entry = self._subagent_tool_calls.get(ns_id)
1023
+ tname = (entry or {}).get("_tool_name", "") or event.tool_name or "Tool"
1024
+ targs = (entry or {}).get("_tool_args", {}) or {}
1025
+ if tname == "Spawn":
1026
+ return
1027
+ status = "failed" if event.result_is_error else "completed"
1028
+ display_text = _format_tui_display_text(event.result_content or "", tname)
1029
+ # Fence multi-line results so the TUI renders a code block (mirrors
1030
+ # the main TOOL_RESULT handler).
1031
+ if display_text and "\n" in display_text and "```" not in display_text:
1032
+ lang = ""
1033
+ if tname in ("Bash", "Glob"):
1034
+ lang = "bash"
1035
+ elif tname == "Read" and isinstance(targs, dict):
1036
+ path = str(targs.get("path", ""))
1037
+ if path:
1038
+ lang = _language_for_path(path)
1039
+ display_text = f"```{lang}\n{display_text}\n```" if lang else f"```\n{display_text}\n```"
1040
+ content_block = [{
1041
+ "type": "content",
1042
+ "content": {"type": "text", "text": display_text},
1043
+ }] if display_text else []
1044
+ existing_content = (entry or {}).get("content", [])
1045
+ if not isinstance(existing_content, list):
1046
+ existing_content = []
1047
+ if entry is None:
1048
+ entry = {
1049
+ "sessionUpdate": "tool_call_update",
1050
+ "toolCallId": ns_id,
1051
+ "subagentId": sa_id,
1052
+ "title": tname,
1053
+ "kind": _kind_for_category(get_tool_category(tname)),
1054
+ "status": status,
1055
+ "content": existing_content + content_block,
1056
+ "_tool_name": tname,
1057
+ "_tool_args": targs,
1058
+ }
1059
+ self._subagent_tool_calls[ns_id] = entry
1060
+ else:
1061
+ entry["subagentId"] = sa_id
1062
+ entry["status"] = status
1063
+ entry["content"] = existing_content + content_block
1064
+ entry["sessionUpdate"] = "tool_call_update"
1065
+ self.send_session_update(entry)
1066
+
860
1067
  def _emit_tool_result(self, block: ToolResult) -> None:
861
1068
  """Send a tool_result as a tool_call_update, accumulating with existing content."""
862
1069
  # Spawn results are rendered as SubAgent widgets via
@@ -870,6 +1077,11 @@ class CDHACPAdapter:
870
1077
  tool_name = tool_name.split(":", 1)[0].strip()
871
1078
  display_text = _format_tui_display_text(block.content, tool_name)
872
1079
 
1080
+ # For edit/write tools the diff/code block + header ✔ status is
1081
+ # sufficient — skip the redundant "✓ /path" result text.
1082
+ if display_text and self.tool_calls.get(block.tool_use_id, {}).get("kind") == "edit":
1083
+ display_text = ""
1084
+
873
1085
  # Update title with path/subject from result if not already set
874
1086
  if block.content and block.tool_use_id in self.tool_calls:
875
1087
  try:
@@ -1015,6 +1227,8 @@ class CDHACPAdapter:
1015
1227
 
1016
1228
  async def session_new(self, cwd: str, mcp_servers: list):
1017
1229
  """Create new session."""
1230
+ if self.agent is not None:
1231
+ await self.agent.shutdown()
1018
1232
  cfg = load_config()
1019
1233
  self.agent = _create_engine(cwd, perm_store=self._perm_store)
1020
1234
  self.agent.set_agent(cfg.default_mode)
@@ -1031,9 +1245,14 @@ class CDHACPAdapter:
1031
1245
  self.agent.attach_session(session)
1032
1246
  self.session_id = session.id
1033
1247
 
1034
- # Restore tasks from project .cdh/ if available
1035
- self.agent.load_todos_from_project()
1036
-
1248
+ # New sessions start with a blank plan — inject a hint so the user
1249
+ # knows previous session todos are not restored.
1250
+ self.agent.context.add_system(
1251
+ "<!-- NEW_SESSION_HINT -->\n"
1252
+ "This is a new session with a blank plan. Previous session todos "
1253
+ "are not automatically restored. Use `/resume <session_id>` to "
1254
+ "recover a previous session's plan if needed.\n"
1255
+ )
1037
1256
  self._send_available_commands()
1038
1257
 
1039
1258
  return {
@@ -1046,6 +1265,8 @@ class CDHACPAdapter:
1046
1265
 
1047
1266
  async def session_load(self, cwd: str, mcp_servers: list, session_id: str):
1048
1267
  """Load existing session."""
1268
+ if self.agent is not None:
1269
+ await self.agent.shutdown()
1049
1270
  self._perm_store_load(cwd)
1050
1271
  self.agent = _create_engine(cwd, perm_store=self._perm_store)
1051
1272
  self.session_id = session_id
@@ -1064,13 +1285,44 @@ class CDHACPAdapter:
1064
1285
  self._emit_plan_update_to_tui()
1065
1286
 
1066
1287
  if not loaded:
1067
- return {"modes": _DEFAULT_MODES}
1288
+ return {
1289
+ "modes": _DEFAULT_MODES,
1290
+ "error": f"Session {session_id} not found at {self.agent._session.storage_path}",
1291
+ }
1068
1292
 
1069
1293
  # Walk the in-memory context (preserves list-of-blocks structure)
1070
1294
  # instead of the raw _session.messages dicts, which only carry
1071
1295
  # `content: str`. Engine.chat_stream() stores tool_use / tool_result
1072
1296
  # blocks in context.Message.content, and we need them at replay time.
1073
- for ctx_msg in self.agent.context.messages:
1297
+ # Only replay the most recent MAX_VISIBLE messages; older ones are
1298
+ # fetched on demand via session/load_earlier.
1299
+ MAX_VISIBLE = 50
1300
+ total_messages = len(self.agent.context.messages)
1301
+ visible_start = max(0, total_messages - MAX_VISIBLE)
1302
+ self._replay_messages(self.agent.context.messages[visible_start:])
1303
+
1304
+ return {
1305
+ "modes": {
1306
+ "currentModeId": cfg.default_mode,
1307
+ "availableModes": _DEFAULT_MODES["availableModes"],
1308
+ },
1309
+ "_total_messages": total_messages,
1310
+ "_visible_start": visible_start,
1311
+ }
1312
+
1313
+ async def session_load_earlier(self, session_id: str, offset: int, limit: int) -> dict:
1314
+ """Replay a range of earlier messages (used by 'load earlier messages')."""
1315
+ if self.agent is None or self.agent.context is None:
1316
+ return {"count": 0, "remaining": 0}
1317
+ messages = self.agent.context.messages
1318
+ end = min(offset + limit, len(messages))
1319
+ segment = messages[offset:end]
1320
+ self._replay_messages(segment)
1321
+ return {"count": end - offset, "remaining": offset}
1322
+
1323
+ def _replay_messages(self, messages: list) -> None:
1324
+ """Replay a list of context messages as session/update notifications."""
1325
+ for ctx_msg in messages:
1074
1326
  role = ctx_msg.role
1075
1327
  content = ctx_msg.content
1076
1328
  if role == "user":
@@ -1146,10 +1398,6 @@ class CDHACPAdapter:
1146
1398
  elif isinstance(block, ToolResult):
1147
1399
  self._emit_tool_result(block)
1148
1400
  elif isinstance(block, SubAgentBlock):
1149
- content_block = [{
1150
- "type": "content",
1151
- "content": {"type": "text", "text": block.result},
1152
- }] if block.result else []
1153
1401
  self.send_session_update({
1154
1402
  "sessionUpdate": "subagent_start",
1155
1403
  "subagentId": block.id,
@@ -1183,13 +1431,6 @@ class CDHACPAdapter:
1183
1431
  content.get("is_error", False),
1184
1432
  )
1185
1433
 
1186
- return {
1187
- "modes": {
1188
- "currentModeId": cfg.default_mode,
1189
- "availableModes": _DEFAULT_MODES["availableModes"],
1190
- },
1191
- }
1192
-
1193
1434
  def _make_stream_callback(self):
1194
1435
  """Create a thinking-aware streaming callback for real-time token output.
1195
1436
 
@@ -1484,6 +1725,13 @@ class CDHACPAdapter:
1484
1725
  # Fresh per-turn tracking (tool_calls accumulates across one turn only)
1485
1726
  self.tool_calls = {}
1486
1727
  self.agent._cancelled = False
1728
+ self._subagent_fwd_count = {}
1729
+ self._subagent_fwd_bytes = {}
1730
+ self._subagent_tool_calls = {}
1731
+ info_log(
1732
+ "[ACP-PROMPT] entering chat_stream loop; agent._cancelled reset to "
1733
+ "False; session=%s", session_id,
1734
+ )
1487
1735
 
1488
1736
  # Create chunkers for agent_message_chunk and agent_thought_chunk
1489
1737
  self._text_chunker = TextChunker(
@@ -1589,6 +1837,20 @@ class CDHACPAdapter:
1589
1837
  pattern = str(event.tool_args.get("pattern", ""))
1590
1838
  if pattern:
1591
1839
  title = f"Glob: {pattern}"
1840
+ elif event.tool_name == "Grep":
1841
+ pattern = str(event.tool_args.get("pattern", ""))
1842
+ if pattern:
1843
+ title = f"Grep: {pattern}"
1844
+ elif event.tool_name == "ToolSearch":
1845
+ query = str(event.tool_args.get("query", ""))
1846
+ if query:
1847
+ title = f"ToolSearch: {query}"
1848
+ else:
1849
+ for key in ("query", "command", "pattern", "name", "subject"):
1850
+ val = str(event.tool_args.get(key, ""))
1851
+ if val:
1852
+ title = f"{event.tool_name}: {val}"
1853
+ break
1592
1854
  content = _build_tool_call_content(
1593
1855
  event.tool_name, event.tool_args
1594
1856
  )
@@ -1633,6 +1895,11 @@ class CDHACPAdapter:
1633
1895
  event.result_content or "", tname,
1634
1896
  )
1635
1897
 
1898
+ # For edit/write tools the diff/code block + header ✔ status is
1899
+ # sufficient — skip the redundant "✓ /path" result text.
1900
+ if display_text and self.tool_calls.get(event.tool_id, {}).get("kind") == "edit":
1901
+ display_text = ""
1902
+
1636
1903
  # Update title with path/subject from result if not already set
1637
1904
  if event.result_content and event.tool_id in self.tool_calls:
1638
1905
  try:
@@ -1662,14 +1929,14 @@ class CDHACPAdapter:
1662
1929
  # Wrap multi-line results in a fenced code block so they
1663
1930
  # display as a code block rather than a raw paragraph.
1664
1931
  # For Read results, detect language from the file path.
1665
- # For Bash/Glob, use bash-style fence.
1932
+ # For Bash/Glob/Grep/List, use bash-style fence.
1666
1933
  # Skip if already contains a code fence to avoid nesting.
1667
- if display_text and "\n" in display_text and "```" not in display_text:
1934
+ if display_text and "\n" in display_text and "```" not in display_text and tname != "ToolSearch":
1668
1935
  lang = ""
1669
1936
  tool_info = self.tool_calls.get(event.tool_id, {})
1670
1937
  tname = tool_info.get("_tool_name", "")
1671
1938
  targs = tool_info.get("_tool_args", {})
1672
- if tname in ("Bash", "Glob"):
1939
+ if tname in ("Bash", "Glob", "Grep", "List"):
1673
1940
  lang = "bash"
1674
1941
  elif tname == "Read" and isinstance(targs, dict):
1675
1942
  path = str(targs.get("path", ""))
@@ -1729,25 +1996,66 @@ class CDHACPAdapter:
1729
1996
  elif event.type == StreamEventType.SUBAGENT_START:
1730
1997
  self._text_chunker.flush()
1731
1998
  self._thought_chunker.flush()
1999
+ info_log(
2000
+ "[ACP-SUBagent] SUBAGENT_START id=%s type=%s prompt_len=%d",
2001
+ event.subagent_id, event.subagent_type,
2002
+ len(event.subagent_prompt or ""),
2003
+ )
1732
2004
  self.send_session_update({
1733
2005
  "sessionUpdate": "subagent_start",
1734
2006
  "subagentId": event.subagent_id,
1735
2007
  "agentType": event.subagent_type,
1736
2008
  "prompt": event.subagent_prompt,
1737
2009
  })
2010
+ self._subagent_fwd_count[event.subagent_id] = 0
2011
+ self._subagent_fwd_bytes[event.subagent_id] = 0
1738
2012
  elif event.type == StreamEventType.SUBAGENT_CHUNK:
2013
+ _cnt = self._subagent_fwd_count.get(event.subagent_id, 0) + 1
2014
+ self._subagent_fwd_count[event.subagent_id] = _cnt
2015
+ _by = self._subagent_fwd_bytes.get(event.subagent_id, 0) + len(event.subagent_text or "")
2016
+ self._subagent_fwd_bytes[event.subagent_id] = _by
2017
+ if _cnt <= 5 or _cnt % 50 == 0:
2018
+ debug_log(
2019
+ "[ACP-SUBagent] SUBAGENT_CHUNK id=%s chunk#%d bytes=%d total=%d",
2020
+ event.subagent_id, _cnt, len(event.subagent_text or ""),
2021
+ _by,
2022
+ )
1739
2023
  self.send_session_update({
1740
2024
  "sessionUpdate": "subagent_chunk",
1741
2025
  "subagentId": event.subagent_id,
1742
2026
  "text": event.subagent_text,
1743
2027
  })
1744
2028
  elif event.type == StreamEventType.SUBAGENT_THINKING:
2029
+ debug_log(
2030
+ "[ACP-SUBagent] SUBAGENT_THINKING id=%s bytes=%d",
2031
+ event.subagent_id, len(event.subagent_thinking_text or ""),
2032
+ )
1745
2033
  self.send_session_update({
1746
2034
  "sessionUpdate": "subagent_thinking",
1747
2035
  "subagentId": event.subagent_id,
1748
2036
  "text": event.subagent_thinking_text,
1749
2037
  })
1750
2038
  elif event.type == StreamEventType.SUBAGENT_END:
2039
+ chunk_count = self._subagent_fwd_count.pop(event.subagent_id, None)
2040
+ self._subagent_fwd_bytes.pop(event.subagent_id, None)
2041
+ info_log(
2042
+ "[ACP-SUBagent] SUBAGENT_END id=%s type=%s status=%s "
2043
+ "err=%r chunk_count=%s",
2044
+ event.subagent_id, event.subagent_type,
2045
+ event.subagent_status, (event.subagent_error or "")[:100],
2046
+ chunk_count,
2047
+ )
2048
+ # If there's no active subagent for this id, the engine
2049
+ # rejected a nested Spawn (depth exceeded). Drop the
2050
+ # orphan end event so the TUI doesn't get a stale
2051
+ # subagent_end without a matching subagent_start.
2052
+ if chunk_count is None:
2053
+ debug_log(
2054
+ "[ACP-SUBagent] DROP orphan SUBAGENT_END id=%s "
2055
+ "(no matching start — likely nested Spawn rejection)",
2056
+ event.subagent_id,
2057
+ )
2058
+ continue
1751
2059
  self.send_session_update({
1752
2060
  "sessionUpdate": "subagent_end",
1753
2061
  "subagentId": event.subagent_id,
@@ -1755,6 +2063,10 @@ class CDHACPAdapter:
1755
2063
  "status": event.subagent_status,
1756
2064
  "error": event.subagent_error,
1757
2065
  })
2066
+ elif event.type == StreamEventType.SUBAGENT_TOOL_CALL:
2067
+ self._handle_subagent_tool_call(event)
2068
+ elif event.type == StreamEventType.SUBAGENT_TOOL_RESULT:
2069
+ self._handle_subagent_tool_result(event)
1758
2070
  elif event.type == StreamEventType.PLAN:
1759
2071
  self.send_session_update({
1760
2072
  "sessionUpdate": "plan",
@@ -1947,6 +2259,8 @@ class CDHACPAdapter:
1947
2259
  "content": result_str, "is_error": is_error}],
1948
2260
  name=tid,
1949
2261
  )
2262
+ # Sync plan after approval — the tool may have mutated todos
2263
+ self._emit_plan_update_to_tui()
1950
2264
  verb = "Approved" if approved else "Rejected"
1951
2265
  self.send_session_update({
1952
2266
  "sessionUpdate": "agent_message_chunk",
@@ -1966,7 +2280,7 @@ class CDHACPAdapter:
1966
2280
  self._thought_chunker.flush()
1967
2281
  self.send_session_update({
1968
2282
  "sessionUpdate": "agent_message_chunk",
1969
- "content": {"type": "text", "text": f"Error: internal error"},
2283
+ "content": {"type": "text", "text": "Error: internal error"},
1970
2284
  })
1971
2285
  try:
1972
2286
  self.agent.save_session()
@@ -1992,6 +2306,9 @@ class CDHACPAdapter:
1992
2306
  except Exception:
1993
2307
  debug_log("Failed to save tasks to .cdh at turn end", exc_info=True)
1994
2308
 
2309
+ # Sync plan to TUI so sidebar shows final todo state
2310
+ self._emit_plan_update_to_tui()
2311
+
1995
2312
  # Send context usage stats to TUI sidebar
1996
2313
  ctx = self.agent.context
1997
2314
  used = ctx._token_count if ctx else 0
@@ -2080,6 +2397,7 @@ class CDHACPAdapter:
2080
2397
  """Cancel current session."""
2081
2398
  if self.agent:
2082
2399
  await self.agent.cancel()
2400
+ await self.agent.shutdown()
2083
2401
  try:
2084
2402
  self.agent.save_session()
2085
2403
  except Exception:
@@ -2103,6 +2421,16 @@ class CDHACPAdapter:
2103
2421
  self._send_available_commands()
2104
2422
  return {"modeId": mode_id}
2105
2423
 
2424
+ async def session_clear_todos(self, session_id: str):
2425
+ """Clear all todos in the current session."""
2426
+ if self.agent:
2427
+ tm = getattr(self.agent, "_todo_manager", None)
2428
+ if tm:
2429
+ tm.clear_todos()
2430
+ self.agent.save_todos_to_project()
2431
+ self._emit_plan_update_to_tui()
2432
+ return {"cleared": True}
2433
+
2106
2434
  # ── Terminal RPC stubs ──────────────────────────────────────────
2107
2435
  async def terminal_create(
2108
2436
  self, session_id: str, terminal_id: str, command: str, args: list[str] | None = None,
@@ -2134,6 +2462,8 @@ class JSONRPCServer:
2134
2462
  "session/cancel": self._handle_session_cancel,
2135
2463
  "session/save": self._handle_session_save,
2136
2464
  "session/set_mode": self._handle_session_set_mode,
2465
+ "session/clear_todos": self._handle_session_clear_todos,
2466
+ "session/load_earlier": self._handle_session_load_earlier,
2137
2467
  "session/ask_user_answer": self._handle_ask_user_answer,
2138
2468
  "terminal/create": self._handle_terminal_create,
2139
2469
  "terminal/kill": self._handle_terminal_kill,
@@ -2208,6 +2538,18 @@ class JSONRPCServer:
2208
2538
  params.get("modeId"),
2209
2539
  )
2210
2540
 
2541
+ async def _handle_session_clear_todos(self, params: dict):
2542
+ return await self.adapter.session_clear_todos(
2543
+ params.get("sessionId"),
2544
+ )
2545
+
2546
+ async def _handle_session_load_earlier(self, params: dict):
2547
+ return await self.adapter.session_load_earlier(
2548
+ params.get("sessionId"),
2549
+ params.get("offset", 0),
2550
+ params.get("limit", 50),
2551
+ )
2552
+
2211
2553
  async def _handle_terminal_create(self, params: dict):
2212
2554
  return await self.adapter.terminal_create(
2213
2555
  params.get("sessionId"),
@@ -2247,12 +2589,18 @@ class JSONRPCServer:
2247
2589
  async def _main():
2248
2590
  adapter = CDHACPAdapter()
2249
2591
  server = JSONRPCServer(adapter)
2250
- prompt_task: asyncio.Task | None = None
2592
+
2593
+ # Track in-flight prompt tasks by request id so that:
2594
+ # - session/cancel can target the correct task
2595
+ # - EOF can cancel & await all remaining tasks
2596
+ prompt_tasks: dict[str | int, asyncio.Task] = {}
2251
2597
 
2252
2598
  async def _run_prompt(req: dict):
2253
2599
  try:
2254
2600
  result = await server._handle_session_prompt(req.get("params", {}))
2255
2601
  return {"jsonrpc": "2.0", "result": result, "id": req.get("id")}
2602
+ except asyncio.CancelledError:
2603
+ raise
2256
2604
  except Exception as e:
2257
2605
  import traceback
2258
2606
  tb = traceback.format_exc()
@@ -2264,6 +2612,13 @@ async def _main():
2264
2612
  while True:
2265
2613
  line = await asyncio.to_thread(sys.stdin.readline)
2266
2614
  if not line:
2615
+ # EOF: cancel all in-flight prompts and exit
2616
+ for p_req_id, p_task in prompt_tasks.items():
2617
+ if not p_task.done():
2618
+ p_task.cancel()
2619
+ if prompt_tasks:
2620
+ await asyncio.gather(*prompt_tasks.values(), return_exceptions=True)
2621
+ prompt_tasks.clear()
2267
2622
  break
2268
2623
 
2269
2624
  line = line.strip()
@@ -2294,10 +2649,34 @@ async def _main():
2294
2649
  if method == "session/prompt":
2295
2650
  # Run prompt in background so main loop stays responsive
2296
2651
  # to cancel notifications on stdin
2297
- prompt_task = asyncio.create_task(_run_prompt(req))
2298
- def _on_prompt_done(t, req=req):
2652
+ task = asyncio.create_task(_run_prompt(req))
2653
+ prompt_tasks[req_id] = task
2654
+
2655
+ def _on_prompt_done(t, req=req, rid=req_id):
2656
+ prompt_tasks.pop(rid, None)
2299
2657
  try:
2300
2658
  resp = t.result()
2659
+ except asyncio.CancelledError:
2660
+ # Prompt was cancelled (session/cancel). Send a
2661
+ # stopReason=cancelled response so the TUI's pending
2662
+ # session/prompt request resolves and the turn ends
2663
+ # cleanly — otherwise the TUI hangs forever waiting
2664
+ # for a response that never comes.
2665
+ info_log(
2666
+ "[ACP-CANCEL] _on_prompt_done CancelledError raised; "
2667
+ "req_id=%s subagent_fwd=%s",
2668
+ req.get("id"), dict(adapter._subagent_fwd_count),
2669
+ )
2670
+ if req.get("id") is not None:
2671
+ try:
2672
+ print(json.dumps({
2673
+ "jsonrpc": "2.0",
2674
+ "result": {"stopReason": "cancelled"},
2675
+ "id": req["id"],
2676
+ }), flush=True)
2677
+ except (TypeError, ValueError, OSError):
2678
+ pass
2679
+ return
2301
2680
  except Exception as exc:
2302
2681
  _dump_crash("_on_prompt_done")
2303
2682
  debug_log(f"Prompt task raised unhandled exception: {exc}", exc_info=True)
@@ -2308,10 +2687,30 @@ async def _main():
2308
2687
  except (TypeError, ValueError, OSError) as e:
2309
2688
  _dump_crash("_on_prompt_done_send")
2310
2689
  debug_log("_on_prompt_done send failed: %s", e)
2311
- prompt_task.add_done_callback(_on_prompt_done)
2690
+ task.add_done_callback(_on_prompt_done)
2312
2691
  elif method == "session/cancel":
2313
- if prompt_task and not prompt_task.done():
2692
+ # session/cancel is a notification (no id), so req_id is None.
2693
+ # prompt_tasks is keyed by request id; find any in-flight task.
2694
+ if req_id is not None:
2695
+ target_task = prompt_tasks.get(req_id)
2696
+ else:
2697
+ target_task = next(
2698
+ (t for t in prompt_tasks.values() if not t.done()),
2699
+ None,
2700
+ )
2701
+ info_log(
2702
+ "[ACP-CANCEL] received session/cancel; "
2703
+ "req_id=%s task_exists=%s task_done=%s",
2704
+ req_id, "yes" if target_task else "no",
2705
+ target_task.done() if target_task else "n/a",
2706
+ )
2707
+ if target_task and not target_task.done():
2314
2708
  adapter.cancel_prompt()
2709
+ info_log("[ACP-CANCEL] calling prompt_task.cancel() now")
2710
+ target_task.cancel()
2711
+ info_log("[ACP-CANCEL] prompt_task.cancel() returned")
2712
+ else:
2713
+ info_log("[ACP-CANCEL] no in-flight prompt_task to cancel")
2315
2714
  # Notification (no id) — nothing to respond with
2316
2715
  else:
2317
2716
  response = await server.handle_request(req)
@@ -2324,6 +2723,20 @@ async def _main():
2324
2723
 
2325
2724
 
2326
2725
  def main():
2726
+ # Initialise file logging ONCE at adapter entry. Previously the ACP
2727
+ # subprocess never called ``setup_logging`` (only the ``cli`` command
2728
+ # path did), so every ``logger.debug(...)`` / ``debug_log(...)`` call
2729
+ # in the engine/provider/ACP layers was silently dropped and
2730
+ # ``~/.onecode/logs/onecode.log`` stayed empty during live reproductions.
2731
+ # Honour the same ``CDH_LOG_LEVEL`` env var as the CLI so users can
2732
+ # turn on DEBUG diagnostics without code changes.
2733
+ import os
2734
+ from onecode.cli import setup_logging
2735
+ setup_logging(os.environ.get("CDH_LOG_LEVEL", "INFO"))
2736
+ info_log(
2737
+ "[ACP-INIT] cdh_acp adapter starting; log_level=%s pid=%d",
2738
+ os.environ.get("CDH_LOG_LEVEL", "INFO"), os.getpid(),
2739
+ )
2327
2740
  asyncio.run(_main())
2328
2741
 
2329
2742
  if __name__ == "__main__":