@scemoon/cdh 1.0.0 → 1.0.3

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 (116) hide show
  1. package/cdh/__pycache__/__main__.cpython-314.pyc +0 -0
  2. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  3. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  4. package/cdh/cli.py +473 -87
  5. package/cdh/scaffold.py +386 -121
  6. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  7. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  8. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  9. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  10. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  11. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  12. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  13. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  14. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  15. package/onecode/agent/agents/types.py +123 -79
  16. package/onecode/agent/cdh_loader.py +26 -13
  17. package/onecode/agent/engine.py +426 -235
  18. package/onecode/agent/onecode_agent_acp.py +200 -73
  19. package/onecode/agent/session.py +8 -13
  20. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  21. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  22. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  23. package/onecode/agent/tools/agent_tools.py +4 -4
  24. package/onecode/agent/tools/todo_tools.py +188 -0
  25. package/onecode/cli.py +3 -3
  26. package/onecode/commands.py +439 -194
  27. package/onecode/config.py +3 -3
  28. package/onecode/config_screen.py +4 -2
  29. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  30. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  31. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  32. package/onecode/models/errors.py +11 -1
  33. package/onecode/models/messages.py +35 -16
  34. package/onecode/models/provider.py +243 -12
  35. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  36. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  37. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  38. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  39. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  40. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  41. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  42. package/onecode/models/providers/anthropic_provider.py +4 -2
  43. package/onecode/models/providers/deepseek_provider.py +2 -0
  44. package/onecode/models/providers/glm_provider.py +2 -1
  45. package/onecode/models/providers/minimax_provider.py +2 -1
  46. package/onecode/models/providers/minimaxi_provider.py +76 -25
  47. package/onecode/models/providers/ollama_provider.py +2 -1
  48. package/onecode/models/providers/openai_provider.py +4 -0
  49. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  50. package/onecode/tasks/__pycache__/__init__.cpython-314.pyc +0 -0
  51. package/onecode/tasks/__pycache__/manager.cpython-314.pyc +0 -0
  52. package/onecode/tasks/__pycache__/models.cpython-314.pyc +0 -0
  53. package/onecode/tasks/manager.py +1 -1
  54. package/package.json +1 -1
  55. package/pyproject.toml +10 -0
  56. package/run.js +24 -7
  57. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  58. package/tui/__pycache__/cli.cpython-314.pyc +0 -0
  59. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  60. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  61. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  62. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  63. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  64. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  65. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  66. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  67. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  68. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  69. package/tui/acp/agent.py +60 -11
  70. package/tui/acp/api.py +6 -0
  71. package/tui/acp/messages.py +11 -0
  72. package/tui/acp/prompt.py +1 -1
  73. package/tui/acp/protocol.py +31 -0
  74. package/tui/app.py +60 -8
  75. package/tui/db.py +23 -0
  76. package/tui/directory_watcher.py +35 -1
  77. package/tui/messages.py +4 -1
  78. package/tui/paths.py +3 -3
  79. package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
  80. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  81. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  82. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  83. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  84. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  85. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  86. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  87. package/tui/screens/component_picker.py +148 -0
  88. package/tui/screens/main.py +47 -95
  89. package/tui/screens/projects.tcss +10 -0
  90. package/tui/screens/projects_app.py +33 -5
  91. package/tui/screens/projects_screen.py +171 -9
  92. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  93. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  94. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  95. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  96. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  97. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  98. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  99. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  100. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  101. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  102. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  103. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  104. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  105. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  106. package/tui/widgets/ask_user.py +1 -1
  107. package/tui/widgets/conversation.py +99 -42
  108. package/tui/widgets/modified_files.py +155 -23
  109. package/tui/widgets/plan.py +22 -0
  110. package/tui/widgets/project_directory_tree.py +22 -10
  111. package/tui/widgets/project_grid_select.py +23 -1
  112. package/tui/widgets/question.py +1 -1
  113. package/tui/widgets/subagent.py +202 -36
  114. package/tui/widgets/subagent_screen.py +186 -0
  115. package/tui/widgets/tool_call.py +5 -8
  116. package/onecode/agent/tools/task_tools.py +0 -249
@@ -42,6 +42,27 @@ def debug_log(*args, **kwargs):
42
42
  else:
43
43
  print("[onecode]", *args, file=sys.stderr, flush=True)
44
44
 
45
+
46
+ _CRASH_LOG_DIR = Path.home() / ".cdh" / "logs"
47
+
48
+
49
+ def _dump_crash(context: str) -> None:
50
+ """Write traceback to ``~/.cdh/logs/cdh_crash.log`` so it survives subprocess exit."""
51
+ import os
52
+ import traceback
53
+ now = __import__("datetime").datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
54
+ _CRASH_LOG_DIR.mkdir(parents=True, exist_ok=True)
55
+ path = _CRASH_LOG_DIR / "cdh_crash.log"
56
+ try:
57
+ lines = [f"=== crash at {now} context={context} ===\n"]
58
+ for tb_line in traceback.format_exc().splitlines(True):
59
+ lines.append(tb_line)
60
+ with open(os.fspath(path), "a", encoding="utf-8") as f:
61
+ f.writelines(lines)
62
+ except Exception:
63
+ pass
64
+
65
+
45
66
  from onecode.agent.cdh_loader import CdhProjectLoader
46
67
  from onecode.agent.engine import AgentEngine
47
68
  from onecode.agent.permissions_store import PermissionStore
@@ -82,7 +103,7 @@ _CATEGORY_TO_ACP_KIND: dict[str, str] = {
82
103
  ToolCategory.WEB_FETCH: "fetch",
83
104
  ToolCategory.WEB_SEARCH: "search",
84
105
  ToolCategory.TASK: "other",
85
- ToolCategory.TASK_MGMT: "other",
106
+ ToolCategory.TODO_MGMT: "other",
86
107
  ToolCategory.INTERACTION: "other",
87
108
  ToolCategory.UNKNOWN: "other",
88
109
  }
@@ -315,8 +336,8 @@ def _build_tool_call_content(name: str | None, arguments: dict) -> list:
315
336
  return []
316
337
 
317
338
  _HEADER_ONLY_TOOLS = frozenset({
318
- "TaskCreate", "TaskGet", "TaskList", "TaskUpdate", "TaskOutput", "TaskStop",
319
- "TodoCreate", "TodoList", "TodoComplete", "Glob",
339
+ "TodoCreate", "TodoGet", "TodoList", "TodoUpdate", "TodoOutput", "TodoStop",
340
+ "Glob", "Spawn",
320
341
  })
321
342
  if name in _HEADER_ONLY_TOOLS:
322
343
  return []
@@ -421,11 +442,11 @@ def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
421
442
  other success results fall through to a compact view of the rest
422
443
  of the dict.
423
444
 
424
- Task / Todo tools (TaskCreate, TaskUpdate, TaskList, …) are special-
445
+ Todo tools (TodoCreate, TodoUpdate, TodoList, …) are special-
425
446
  cased: their verbose ``{"task": {...}}`` / ``{"tasks": [...]}`` results
426
447
  are collapsed to a single concise marker (``✓ updated``) since the
427
448
  LLM's tool-call args already show what the agent did, and re-printing
428
- the full task object after every call clutters the conversation view.
449
+ the full todo object after every call clutters the conversation view.
429
450
  """
430
451
  if not result_text:
431
452
  return ""
@@ -440,9 +461,9 @@ def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
440
461
  # Read: show actual file content (code fence + language wrapping in _emit_tool_result)
441
462
  if tool_name == "Read":
442
463
  return parsed.get("content", "")
443
- # Task / Todo tools: collapse verbose state echoes to one-liner.
464
+ # Todo tools: collapse verbose state echoes to one-liner.
444
465
  if tool_name in _STATUS_ONLY_TOOLS:
445
- return ""
466
+ return "✓ updated"
446
467
  if parsed.get("success") is True:
447
468
  if path := parsed.get("path"):
448
469
  return f"✓ {path}"
@@ -457,11 +478,12 @@ def _format_tui_display_text(result_text: str, tool_name: str = "") -> str:
457
478
 
458
479
 
459
480
  _STATUS_ONLY_TOOLS = frozenset({
460
- "TaskCreate",
461
- "TaskUpdate",
462
- "TaskStop",
463
481
  "TodoCreate",
464
- "TodoComplete",
482
+ "TodoGet",
483
+ "TodoList",
484
+ "TodoUpdate",
485
+ "TodoOutput",
486
+ "TodoStop",
465
487
  })
466
488
 
467
489
 
@@ -781,8 +803,12 @@ class CDHACPAdapter:
781
803
 
782
804
  def send_notification(self, method: str, params: dict):
783
805
  """Send a JSONRPC notification to A2TUI."""
784
- notification = {"jsonrpc": "2.0", "method": method, "params": params}
785
- print(json.dumps(notification), flush=True)
806
+ try:
807
+ notification = {"jsonrpc": "2.0", "method": method, "params": params}
808
+ print(json.dumps(notification), flush=True)
809
+ except (TypeError, ValueError, OSError) as e:
810
+ _dump_crash("send_notification")
811
+ debug_log("send_notification failed: %s", e)
786
812
 
787
813
  def send_request(self, method: str, params: dict) -> asyncio.Future[dict]:
788
814
  """Send a JSONRPC request to A2TUI and return a Future for the response."""
@@ -791,7 +817,13 @@ class CDHACPAdapter:
791
817
  future: asyncio.Future[dict] = asyncio.Future()
792
818
  self._pending_requests[req_id] = future
793
819
  request = {"jsonrpc": "2.0", "method": method, "params": params, "id": req_id}
794
- print(json.dumps(request), flush=True)
820
+ try:
821
+ print(json.dumps(request), flush=True)
822
+ except (TypeError, ValueError, OSError) as e:
823
+ _dump_crash("send_request")
824
+ debug_log("send_request failed: %s", e)
825
+ if not future.done():
826
+ future.set_exception(e)
795
827
  return future
796
828
 
797
829
  def resolve_pending_request(self, req_id: str, result: dict) -> bool:
@@ -827,6 +859,10 @@ class CDHACPAdapter:
827
859
 
828
860
  def _emit_tool_result(self, block: ToolResult) -> None:
829
861
  """Send a tool_result as a tool_call_update, accumulating with existing content."""
862
+ # Spawn results are rendered as SubAgent widgets via
863
+ # _replay_tool_or_subagent → subagent_start/chunk/end events.
864
+ if self.tool_calls.get(block.tool_use_id, {}).get("_tool_name") == "Spawn":
865
+ return
830
866
  # Look up tool name from the tracked tool_calls entry
831
867
  tool_name = self.tool_calls.get(block.tool_use_id, {}).get("title", "")
832
868
  # Title may be like "Bash: ls -la" — keep just the leading tool name
@@ -844,7 +880,7 @@ class CDHACPAdapter:
844
880
  if isinstance(parsed, dict) and parsed.get("success") is True:
845
881
  current_title = self.tool_calls[block.tool_use_id].get("title", "")
846
882
  if ":" not in current_title:
847
- if tool_name in ("TaskCreate", "TaskUpdate"):
883
+ if tool_name in ("TodoCreate", "TodoUpdate"):
848
884
  task_info = parsed.get("task", {})
849
885
  if isinstance(task_info, dict):
850
886
  if subject := task_info.get("subject", ""):
@@ -895,6 +931,58 @@ class CDHACPAdapter:
895
931
  }
896
932
  self.send_session_update(self.tool_calls[block.tool_use_id])
897
933
 
934
+ def _replay_tool_or_subagent(self, tool_use_id: str, content: str, is_error: bool) -> None:
935
+ """Replay a tool result — subagent events for Task, tool_call_update otherwise."""
936
+ tc = self.tool_calls.get(tool_use_id)
937
+ if tc and tc.get("_tool_name") == "Spawn":
938
+ args = tc.get("_tool_args", {})
939
+ agent_type = args.get("agent_type", "general")
940
+ prompt = args.get("prompt", "")
941
+ self.send_session_update({
942
+ "sessionUpdate": "subagent_start",
943
+ "subagentId": tool_use_id,
944
+ "agentType": agent_type,
945
+ "prompt": prompt,
946
+ })
947
+ if content:
948
+ self.send_session_update({
949
+ "sessionUpdate": "subagent_chunk",
950
+ "subagentId": tool_use_id,
951
+ "text": content,
952
+ })
953
+ self.send_session_update({
954
+ "sessionUpdate": "subagent_end",
955
+ "subagentId": tool_use_id,
956
+ "agentType": agent_type,
957
+ })
958
+ else:
959
+ tr = ToolResult(
960
+ tool_use_id=tool_use_id,
961
+ content=content,
962
+ is_error=is_error,
963
+ )
964
+ self._emit_tool_result(tr)
965
+
966
+ def _emit_plan_update_to_tui(self) -> None:
967
+ """Send a `plan` session update built from current todo state.
968
+
969
+ Used right after ``session_load`` so the TUI Plan widget shows the
970
+ resumed todos immediately, without waiting for the user to
971
+ type a new prompt. Delegates dedupe to the engine's
972
+ ``_emit_plan_update`` so the session-resume snapshot and the
973
+ per-turn snapshot share a single cache.
974
+ """
975
+ tm = getattr(self.agent, "_todo_manager", None)
976
+ if tm is None:
977
+ return
978
+ if not tm.list_todos():
979
+ return
980
+ for event in self.agent._emit_plan_update():
981
+ self.send_session_update({
982
+ "sessionUpdate": "plan",
983
+ "entries": event.plan_entries,
984
+ })
985
+
898
986
  async def initialize(self, protocol_version: int, client_capabilities: dict, client_info: dict):
899
987
  """Handle ACP initialize."""
900
988
  return {
@@ -944,7 +1032,7 @@ class CDHACPAdapter:
944
1032
  self.session_id = session.id
945
1033
 
946
1034
  # Restore tasks from project .cdh/ if available
947
- self.agent.load_tasks_from_project()
1035
+ self.agent.load_todos_from_project()
948
1036
 
949
1037
  self._send_available_commands()
950
1038
 
@@ -969,7 +1057,11 @@ class CDHACPAdapter:
969
1057
  self._send_available_commands()
970
1058
 
971
1059
  # Also restore tasks from project .cdh/ as fallback / supplement
972
- self.agent.load_tasks_from_project()
1060
+ self.agent.load_todos_from_project()
1061
+
1062
+ # Surface loaded tasks to the TUI Plan widget so the user sees
1063
+ # pending work without having to send a new message first.
1064
+ self._emit_plan_update_to_tui()
973
1065
 
974
1066
  if not loaded:
975
1067
  return {"modes": _DEFAULT_MODES}
@@ -1020,22 +1112,18 @@ class CDHACPAdapter:
1020
1112
  elif block.name == "Bash":
1021
1113
  cmd = str(args.get("command", ""))
1022
1114
  title = f"Bash: {cmd}"
1023
- elif block.name == "TaskCreate":
1115
+ elif block.name == "TodoCreate":
1024
1116
  subject = str(args.get("subject", ""))
1025
1117
  if subject:
1026
- title = f"TaskCreate: {subject}"
1027
- elif block.name == "TaskUpdate":
1118
+ title = f"TodoCreate: {subject}"
1119
+ elif block.name == "TodoUpdate":
1028
1120
  subject = str(args.get("subject", ""))
1029
1121
  if subject:
1030
- title = f"TaskUpdate: {subject}"
1031
- elif block.name in ("TaskGet", "TaskStop", "TaskOutput"):
1122
+ title = f"TodoUpdate: {subject}"
1123
+ elif block.name in ("TodoGet", "TodoStop", "TodoOutput"):
1032
1124
  tid = str(args.get("taskId", args.get("task_id", "")))
1033
1125
  if tid:
1034
1126
  title = f"{block.name}: {tid}"
1035
- elif block.name == "TodoCreate":
1036
- text = str(args.get("text", ""))
1037
- if text:
1038
- title = f"TodoCreate: {text[:40]}"
1039
1127
  elif block.name == "Glob":
1040
1128
  pattern = str(args.get("pattern", ""))
1041
1129
  if pattern:
@@ -1050,7 +1138,11 @@ class CDHACPAdapter:
1050
1138
  "_tool_name": block.name,
1051
1139
  "_tool_args": block.arguments or {},
1052
1140
  }
1053
- self.send_session_update(self.tool_calls[block.id])
1141
+ # Skip sending tool_call for Task tools — SubAgent widget
1142
+ # is rendered via subagent_start/subagent_chunk/subagent_end
1143
+ # when the corresponding tool_result is processed below.
1144
+ if block.name != "Spawn":
1145
+ self.send_session_update(self.tool_calls[block.id])
1054
1146
  elif isinstance(block, ToolResult):
1055
1147
  self._emit_tool_result(block)
1056
1148
  elif isinstance(block, SubAgentBlock):
@@ -1062,6 +1154,7 @@ class CDHACPAdapter:
1062
1154
  "sessionUpdate": "subagent_start",
1063
1155
  "subagentId": block.id,
1064
1156
  "agentType": block.agent_type,
1157
+ "prompt": block.prompt,
1065
1158
  })
1066
1159
  if block.result:
1067
1160
  self.send_session_update({
@@ -1078,19 +1171,17 @@ class CDHACPAdapter:
1078
1171
  if isinstance(content, list):
1079
1172
  for item in content:
1080
1173
  if isinstance(item, dict) and item.get("type") == "tool_result":
1081
- tr = ToolResult(
1082
- tool_use_id=item.get("tool_use_id", ""),
1083
- content=item.get("content", ""),
1084
- is_error=item.get("is_error", False),
1174
+ self._replay_tool_or_subagent(
1175
+ item.get("tool_use_id", ""),
1176
+ item.get("content", ""),
1177
+ item.get("is_error", False),
1085
1178
  )
1086
- self._emit_tool_result(tr)
1087
1179
  elif isinstance(content, dict) and content.get("type") == "tool_result":
1088
- tr = ToolResult(
1089
- tool_use_id=content.get("tool_use_id", ""),
1090
- content=content.get("content", ""),
1091
- is_error=content.get("is_error", False),
1180
+ self._replay_tool_or_subagent(
1181
+ content.get("tool_use_id", ""),
1182
+ content.get("content", ""),
1183
+ content.get("is_error", False),
1092
1184
  )
1093
- self._emit_tool_result(tr)
1094
1185
 
1095
1186
  return {
1096
1187
  "modes": {
@@ -1422,6 +1513,9 @@ class CDHACPAdapter:
1422
1513
  _ARGS_THROTTLE_CHARS = 50
1423
1514
 
1424
1515
  def _on_tool_call_delta(call_id: str, name: str, args_delta: str) -> None:
1516
+ # Skip Spawn — SubAgent widget handles display via subagent_* events
1517
+ if name == "Spawn":
1518
+ return
1425
1519
  if args_delta:
1426
1520
  _incremental_args[call_id] = _incremental_args.get(call_id, "") + args_delta
1427
1521
  display_args = _incremental_args[call_id]
@@ -1466,7 +1560,10 @@ class CDHACPAdapter:
1466
1560
  "content": existing.get("content", []),
1467
1561
  "_tool_name": event.tool_name,
1468
1562
  }
1469
- self.send_session_update(self.tool_calls[event.tool_id])
1563
+ # Skip sending tool_call for Spawn tools — SubAgent widget
1564
+ # handles the display via subagent_start events.
1565
+ if event.tool_name != "Spawn":
1566
+ self.send_session_update(self.tool_calls[event.tool_id])
1470
1567
  elif event.type == StreamEventType.TOOL_CALL_COMPLETE:
1471
1568
  if event.tool_id in self.tool_calls:
1472
1569
  title = event.tool_name
@@ -1476,22 +1573,18 @@ class CDHACPAdapter:
1476
1573
  elif event.tool_name == "Bash":
1477
1574
  cmd = str(event.tool_args.get("command", ""))
1478
1575
  title = f"Bash: {cmd}"
1479
- elif event.tool_name == "TaskCreate":
1576
+ elif event.tool_name == "TodoCreate":
1480
1577
  subject = str(event.tool_args.get("subject", ""))
1481
1578
  if subject:
1482
- title = f"TaskCreate: {subject}"
1483
- elif event.tool_name == "TaskUpdate":
1579
+ title = f"TodoCreate: {subject}"
1580
+ elif event.tool_name == "TodoUpdate":
1484
1581
  subject = str(event.tool_args.get("subject", ""))
1485
1582
  if subject:
1486
- title = f"TaskUpdate: {subject}"
1487
- elif event.tool_name in ("TaskGet", "TaskStop", "TaskOutput"):
1583
+ title = f"TodoUpdate: {subject}"
1584
+ elif event.tool_name in ("TodoGet", "TodoStop", "TodoOutput"):
1488
1585
  tid = str(event.tool_args.get("taskId", event.tool_args.get("task_id", "")))
1489
1586
  if tid:
1490
1587
  title = f"{event.tool_name}: {tid}"
1491
- elif event.tool_name == "TodoCreate":
1492
- text = str(event.tool_args.get("text", ""))
1493
- if text:
1494
- title = f"TodoCreate: {text[:40]}"
1495
1588
  elif event.tool_name == "Glob":
1496
1589
  pattern = str(event.tool_args.get("pattern", ""))
1497
1590
  if pattern:
@@ -1520,10 +1613,22 @@ class CDHACPAdapter:
1520
1613
  "_tool_name": event.tool_name,
1521
1614
  "_tool_args": event.tool_args,
1522
1615
  })
1523
- self.send_session_update(self.tool_calls[event.tool_id])
1616
+ # Skip sending tool_call_update for Spawn — SubAgent widget
1617
+ # handles the display via subagent_start/chunk/end events.
1618
+ if event.tool_name != "Spawn":
1619
+ self.send_session_update(self.tool_calls[event.tool_id])
1524
1620
  elif event.type == StreamEventType.TOOL_RESULT:
1525
- status = "failed" if event.result_is_error else "completed"
1621
+ # Spawn tool results: subagent_start/chunk/end events were
1622
+ # already sent during subagent execution — just skip the
1623
+ # tool_call_update (no duplicate SubAgent widget needed).
1526
1624
  tname = self.tool_calls.get(event.tool_id, {}).get("_tool_name", "")
1625
+ if tname == "Spawn":
1626
+ try:
1627
+ self.agent.save_session()
1628
+ except Exception:
1629
+ debug_log("Failed to save session after subagent", exc_info=True)
1630
+ continue
1631
+ status = "failed" if event.result_is_error else "completed"
1527
1632
  display_text = _format_tui_display_text(
1528
1633
  event.result_content or "", tname,
1529
1634
  )
@@ -1539,7 +1644,7 @@ class CDHACPAdapter:
1539
1644
  current_title = self.tool_calls[event.tool_id].get("title", "")
1540
1645
  if ":" not in current_title:
1541
1646
  tname = self.tool_calls[event.tool_id].get("_tool_name", "")
1542
- if tname in ("TaskCreate", "TaskUpdate"):
1647
+ if tname in ("TodoCreate", "TodoUpdate"):
1543
1648
  task_info = parsed.get("task", {})
1544
1649
  if isinstance(task_info, dict):
1545
1650
  if subject := task_info.get("subject", ""):
@@ -1578,9 +1683,8 @@ class CDHACPAdapter:
1578
1683
  "type": "content",
1579
1684
  "content": {"type": "text", "text": display_text},
1580
1685
  }] if display_text else []
1581
- if event.tool_id in self.tool_calls:
1582
- existing_content = self.tool_calls[event.tool_id].get("content", [])
1583
- else:
1686
+ existing_content = self.tool_calls.get(event.tool_id, {}).get("content", [])
1687
+ if not isinstance(existing_content, list):
1584
1688
  existing_content = []
1585
1689
  debug_log(
1586
1690
  "TOOL_RESULT id=%s status=%s result_len=%d display_len=%d existing_blocks=%d new_blocks=%d",
@@ -1629,6 +1733,7 @@ class CDHACPAdapter:
1629
1733
  "sessionUpdate": "subagent_start",
1630
1734
  "subagentId": event.subagent_id,
1631
1735
  "agentType": event.subagent_type,
1736
+ "prompt": event.subagent_prompt,
1632
1737
  })
1633
1738
  elif event.type == StreamEventType.SUBAGENT_CHUNK:
1634
1739
  self.send_session_update({
@@ -1636,11 +1741,19 @@ class CDHACPAdapter:
1636
1741
  "subagentId": event.subagent_id,
1637
1742
  "text": event.subagent_text,
1638
1743
  })
1744
+ elif event.type == StreamEventType.SUBAGENT_THINKING:
1745
+ self.send_session_update({
1746
+ "sessionUpdate": "subagent_thinking",
1747
+ "subagentId": event.subagent_id,
1748
+ "text": event.subagent_thinking_text,
1749
+ })
1639
1750
  elif event.type == StreamEventType.SUBAGENT_END:
1640
1751
  self.send_session_update({
1641
1752
  "sessionUpdate": "subagent_end",
1642
1753
  "subagentId": event.subagent_id,
1643
1754
  "agentType": event.subagent_type,
1755
+ "status": event.subagent_status,
1756
+ "error": event.subagent_error,
1644
1757
  })
1645
1758
  elif event.type == StreamEventType.PLAN:
1646
1759
  self.send_session_update({
@@ -1846,6 +1959,7 @@ class CDHACPAdapter:
1846
1959
  except Exception:
1847
1960
  import traceback
1848
1961
  tb = traceback.format_exc()
1962
+ _dump_crash("chat_stream")
1849
1963
  debug_log("Unhandled exception in chat_stream:\n%s", tb, exc_info=True)
1850
1964
  print(f"[cdh-agent-acp] ERROR in chat_stream:\n{tb}", file=sys.stderr, flush=True)
1851
1965
  self._text_chunker.flush()
@@ -1859,7 +1973,7 @@ class CDHACPAdapter:
1859
1973
  except Exception:
1860
1974
  pass
1861
1975
  try:
1862
- self.agent.save_tasks_to_project()
1976
+ self.agent.save_todos_to_project()
1863
1977
  except Exception:
1864
1978
  pass
1865
1979
  return {"stopReason": "error", "message": "Internal agent error"}
@@ -1874,7 +1988,7 @@ class CDHACPAdapter:
1874
1988
 
1875
1989
  # Persist tasks to project .cdh/ so they survive Ctrl+C
1876
1990
  try:
1877
- self.agent.save_tasks_to_project()
1991
+ self.agent.save_todos_to_project()
1878
1992
  except Exception:
1879
1993
  debug_log("Failed to save tasks to .cdh at turn end", exc_info=True)
1880
1994
 
@@ -1888,21 +2002,8 @@ class CDHACPAdapter:
1888
2002
  "size": size,
1889
2003
  })
1890
2004
 
1891
- # Send final action summary if tool calls were made this turn
1892
- if self.tool_calls:
1893
- summaries: list[str] = []
1894
- for tid, tc in self.tool_calls.items():
1895
- action = tc.get("title", "")
1896
- status = tc.get("status", "")
1897
- if action:
1898
- emoji = "✅" if status == "completed" else "❌" if status == "failed" else "⏳"
1899
- summaries.append(f"{emoji} {action}")
1900
- if summaries:
1901
- summary_text = "\n".join(summaries)
1902
- self.send_session_update({
1903
- "sessionUpdate": "agent_message_chunk",
1904
- "content": {"type": "text", "text": f"```output\n{summary_text}\n```"},
1905
- })
2005
+ # Tool call summary suppressed internal step tracking
2006
+ # is not shown to avoid cluttering conversation output.
1906
2007
 
1907
2008
  stop_reason = "cancelled" if self.agent._cancelled else "end_turn"
1908
2009
  usage = self._build_session_usage()
@@ -1966,6 +2067,15 @@ class CDHACPAdapter:
1966
2067
  "output_tokens": total_out,
1967
2068
  }
1968
2069
 
2070
+ async def session_save(self, session_id: str, _meta: dict):
2071
+ """Save current session state."""
2072
+ if self.agent:
2073
+ try:
2074
+ self.agent.save_session()
2075
+ except Exception:
2076
+ debug_log("Failed to save session on save", exc_info=True)
2077
+ return {}
2078
+
1969
2079
  async def session_cancel(self, session_id: str, _meta: dict):
1970
2080
  """Cancel current session."""
1971
2081
  if self.agent:
@@ -1976,7 +2086,7 @@ class CDHACPAdapter:
1976
2086
  debug_log("Failed to save session on cancel", exc_info=True)
1977
2087
  # Persist tasks to .cdh so they survive Ctrl+C and re-entry
1978
2088
  try:
1979
- self.agent.save_tasks_to_project()
2089
+ self.agent.save_todos_to_project()
1980
2090
  except Exception:
1981
2091
  debug_log("Failed to save tasks to .cdh on cancel", exc_info=True)
1982
2092
  return {}
@@ -2022,6 +2132,7 @@ class JSONRPCServer:
2022
2132
  "session/load": self._handle_session_load,
2023
2133
  "session/prompt": self._handle_session_prompt,
2024
2134
  "session/cancel": self._handle_session_cancel,
2135
+ "session/save": self._handle_session_save,
2025
2136
  "session/set_mode": self._handle_session_set_mode,
2026
2137
  "session/ask_user_answer": self._handle_ask_user_answer,
2027
2138
  "terminal/create": self._handle_terminal_create,
@@ -2067,6 +2178,12 @@ class JSONRPCServer:
2067
2178
  params.get("sessionId"),
2068
2179
  )
2069
2180
 
2181
+ async def _handle_session_save(self, params: dict):
2182
+ return await self.adapter.session_save(
2183
+ params.get("sessionId"),
2184
+ params.get("_meta", {}),
2185
+ )
2186
+
2070
2187
  async def _handle_session_prompt(self, params: dict):
2071
2188
  return await self.adapter.session_prompt(
2072
2189
  params.get("prompt", []),
@@ -2139,6 +2256,7 @@ async def _main():
2139
2256
  except Exception as e:
2140
2257
  import traceback
2141
2258
  tb = traceback.format_exc()
2259
+ _dump_crash("_run_prompt")
2142
2260
  print(f"[cdh-agent-acp] ERROR in _run_prompt:\n{tb}", file=sys.stderr, flush=True)
2143
2261
  debug_log("_run_prompt failed: %s\n%s", e, tb, exc_info=True)
2144
2262
  return {"jsonrpc": "2.0", "error": {"code": -32603, "message": str(e)}, "id": req.get("id")}
@@ -2181,10 +2299,15 @@ async def _main():
2181
2299
  try:
2182
2300
  resp = t.result()
2183
2301
  except Exception as exc:
2302
+ _dump_crash("_on_prompt_done")
2184
2303
  debug_log(f"Prompt task raised unhandled exception: {exc}", exc_info=True)
2185
2304
  resp = {"jsonrpc": "2.0", "error": {"code": -32603, "message": str(exc)}, "id": req.get("id")}
2186
2305
  if resp.get("id") is not None:
2187
- print(json.dumps(resp), flush=True)
2306
+ try:
2307
+ print(json.dumps(resp), flush=True)
2308
+ except (TypeError, ValueError, OSError) as e:
2309
+ _dump_crash("_on_prompt_done_send")
2310
+ debug_log("_on_prompt_done send failed: %s", e)
2188
2311
  prompt_task.add_done_callback(_on_prompt_done)
2189
2312
  elif method == "session/cancel":
2190
2313
  if prompt_task and not prompt_task.done():
@@ -2193,7 +2316,11 @@ async def _main():
2193
2316
  else:
2194
2317
  response = await server.handle_request(req)
2195
2318
  if response.get("id") is not None:
2196
- print(json.dumps(response), flush=True)
2319
+ try:
2320
+ print(json.dumps(response), flush=True)
2321
+ except (TypeError, ValueError, OSError) as e:
2322
+ _dump_crash("main_loop_send")
2323
+ debug_log("main loop send failed: %s", e)
2197
2324
 
2198
2325
 
2199
2326
  def main():
@@ -23,7 +23,6 @@ class SessionData:
23
23
  lifecycle_state: dict = field(default_factory=dict)
24
24
  created_at: Optional[str] = None
25
25
  updated_at: Optional[str] = None
26
- tasks: list[dict] = field(default_factory=list)
27
26
  todos: list[dict] = field(default_factory=list)
28
27
 
29
28
  def to_dict(self) -> dict:
@@ -38,12 +37,18 @@ class SessionData:
38
37
  "lifecycle_state": self.lifecycle_state,
39
38
  "created_at": self.created_at,
40
39
  "updated_at": self.updated_at,
41
- "tasks": self.tasks,
42
40
  "todos": self.todos,
43
41
  }
44
42
 
45
43
  @classmethod
46
44
  def from_dict(cls, data: dict) -> SessionData:
45
+ # Merge legacy ``tasks`` and ``todos`` fields into the unified
46
+ # ``todos`` list. Older sessions stored them as two separate lists;
47
+ # TodoManager.from_dict already handles the legacy entry shape, so
48
+ # we just concatenate here.
49
+ legacy_todos: list[dict] = []
50
+ legacy_todos.extend(data.get("tasks") or [])
51
+ legacy_todos.extend(data.get("todos") or [])
47
52
  return cls(
48
53
  id=data.get("id", str(uuid.uuid4())),
49
54
  name=data.get("name", "Untitled"),
@@ -55,8 +60,7 @@ class SessionData:
55
60
  lifecycle_state=data.get("lifecycle_state", {}),
56
61
  created_at=data.get("created_at"),
57
62
  updated_at=data.get("updated_at"),
58
- tasks=data.get("tasks", []),
59
- todos=data.get("todos", []),
63
+ todos=legacy_todos,
60
64
  )
61
65
 
62
66
 
@@ -66,7 +70,6 @@ class AgentSession:
66
70
  self._data: SessionData = SessionData()
67
71
  if session_id:
68
72
  self._data.id = session_id
69
- self.load()
70
73
 
71
74
  def _default_storage_path(self) -> Path:
72
75
  from onecode.config import CLOUD_DEV_HARNESS_DIR
@@ -96,14 +99,6 @@ class AgentSession:
96
99
  def lifecycle_state(self) -> dict:
97
100
  return self._data.lifecycle_state
98
101
 
99
- @property
100
- def tasks(self) -> list[dict]:
101
- return self._data.tasks
102
-
103
- @tasks.setter
104
- def tasks(self, value: list[dict]) -> None:
105
- self._data.tasks = value
106
-
107
102
  @property
108
103
  def todos(self) -> list[dict]:
109
104
  return self._data.todos
@@ -87,7 +87,7 @@ class TaskTool:
87
87
 
88
88
  def spec(self) -> ToolSpec:
89
89
  return ToolSpec(
90
- name="Task",
90
+ name="Spawn",
91
91
  description="Delegate a subtask to a specialized subagent. Use for large, independent, or specialized work that benefits from a focused sub-agent.",
92
92
  input_schema={
93
93
  "type": "object",
@@ -108,7 +108,7 @@ class TaskTool:
108
108
  prompt = tool_input.get("prompt", "")
109
109
  if agent_type not in self.VALID_AGENT_TYPES:
110
110
  return ToolResult(
111
- name="Task",
111
+ name="Spawn",
112
112
  output={
113
113
  "error": (
114
114
  f"Unknown agent_type '{agent_type}'. "
@@ -129,6 +129,6 @@ class TaskTool:
129
129
  else:
130
130
  result = asyncio.run(self._spawn(agent_type, prompt))
131
131
  except Exception as e:
132
- return ToolResult(name="Task", output={"error": str(e)}, is_error=True)
133
- return ToolResult(name="Task", output=str(result))
132
+ return ToolResult(name="Spawn", output={"error": str(e)}, is_error=True)
133
+ return ToolResult(name="Spawn", output=str(result))
134
134