@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
@@ -226,13 +226,24 @@ class ProjectsApp(App):
226
226
  except Exception:
227
227
  ws = Path.cwd().resolve()
228
228
  name = ws.name
229
- from cdh.scaffold import scaffold_dlc_project
230
- scaffold_dlc_project(ws, name)
231
- CdhProjectLoader.init_project(ws, name)
232
229
  projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
233
230
  projects_dir.mkdir(parents=True, exist_ok=True)
231
+ proj_file = projects_dir / f"{name}.yaml"
232
+ if proj_file.exists():
233
+ self.notify(
234
+ f"Project '{name}' already exists in the project list",
235
+ severity="error",
236
+ )
237
+ return
238
+ from cdh.scaffold import scaffold_dlc_project
239
+ try:
240
+ scaffold_dlc_project(ws, name)
241
+ except (ValueError, RuntimeError) as e:
242
+ self.notify(str(e), severity="error")
243
+ return
244
+ CdhProjectLoader.init_project(ws, name)
234
245
  proj_data = {"name": name, "path": str(ws), "description": ""}
235
- (projects_dir / f"{name}.yaml").write_text(yaml.dump(proj_data))
246
+ proj_file.write_text(yaml.dump(proj_data))
236
247
  cfg = load_config()
237
248
  cfg.current_project = name
238
249
  cfg.current_project_path = str(ws)
@@ -262,9 +273,26 @@ class ProjectsApp(App):
262
273
  self.notify(f".cdh already exists at {existing}", severity="warning")
263
274
  return
264
275
  name = target.name
276
+ projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
277
+ projects_dir.mkdir(parents=True, exist_ok=True)
278
+ proj_file = projects_dir / f"{name}.yaml"
279
+ if proj_file.exists():
280
+ self.notify(
281
+ f"Project '{name}' already exists in the project list",
282
+ severity="error",
283
+ )
284
+ return
265
285
  from cdh.scaffold import init_dlc_project
266
- init_dlc_project(target, name)
286
+ try:
287
+ init_dlc_project(target, name)
288
+ except (ValueError, RuntimeError) as e:
289
+ self.notify(str(e), severity="error")
290
+ return
267
291
  CdhProjectLoader.init_project(target, name)
292
+ import yaml
293
+ proj_data = {"name": name, "path": str(target), "description": ""}
294
+ proj_file.write_text(yaml.dump(proj_data))
295
+ self._refresh()
268
296
  self.notify(f"Initialized .cdh in {target}")
269
297
 
270
298
  def _load_project(self) -> None:
@@ -2,7 +2,6 @@ from time import monotonic
2
2
 
3
3
  from textual.app import ComposeResult
4
4
  from textual.binding import Binding
5
- from textual.events import ScreenResume
6
5
  from textual.screen import ModalScreen
7
6
  from textual import getters
8
7
  from textual.widget import Widget
@@ -17,12 +16,26 @@ from tui.app import A2TUIApp
17
16
  from tui.widgets.grid_select import GridSelect
18
17
  from tui.widgets.project_grid_select import ProjectGridSelect
19
18
  from tui.widgets.project_summary import ProjectSummary
19
+ from tui.screens.component_picker import ComponentPickerScreen
20
20
 
21
21
 
22
22
  CONFIRM_TIMEOUT = 5.0
23
23
  INSTRUCTIONS_NO_PROJECTS = "Your projects will be shown here."
24
24
 
25
25
 
26
+ def _project_db_path(name: str):
27
+ """Return the path of the project entry in ``~/.cdh/projects/``
28
+ for *name* across all supported extensions, or ``None`` if the
29
+ project is not registered yet.
30
+ """
31
+ projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
32
+ for ext in ("yaml", "yml", "json"):
33
+ pf = projects_dir / f"{name}.{ext}"
34
+ if pf.exists():
35
+ return pf
36
+ return None
37
+
38
+
26
39
  class ProjectsScreen(ModalScreen[str]):
27
40
  CSS_PATH = "projects.tcss"
28
41
  BINDINGS = [
@@ -41,7 +54,7 @@ class ProjectsScreen(ModalScreen[str]):
41
54
  yield widgets.Label("Projects")
42
55
  yield widgets.Static(INSTRUCTIONS_NO_PROJECTS, classes="instructions")
43
56
  yield ProjectGridSelect()
44
- with containers.Center():
57
+ with containers.Horizontal(id="actions"):
45
58
  yield widgets.Button("+ New Project", id="new-project", variant="primary")
46
59
  yield widgets.Button("Init .cdh (i)", id="init-project")
47
60
  yield widgets.Footer()
@@ -89,12 +102,95 @@ class ProjectsScreen(ModalScreen[str]):
89
102
  self.notify(f"Deleted project: {project_name}")
90
103
 
91
104
  def action_new_project(self) -> None:
92
- self.dismiss("__new__")
105
+ from pathlib import Path
106
+ from onecode.config_screen import EditFieldScreen
107
+
108
+ default_path = str(Path.cwd().resolve())
109
+ self.app.push_screen(
110
+ EditFieldScreen("Project path", default_path),
111
+ self._on_new_project_path,
112
+ )
113
+
114
+ def _on_new_project_path(self, path_str: str | None) -> None:
115
+ from pathlib import Path
116
+
117
+ if not path_str:
118
+ return
119
+ try:
120
+ target = Path(path_str).expanduser().resolve()
121
+ except Exception:
122
+ self.notify("Invalid path", severity="error")
123
+ return
124
+ if _project_db_path(target.name) is not None:
125
+ self.notify(
126
+ f"Project '{target.name}' is already in the project list",
127
+ severity="warning",
128
+ )
129
+ return
130
+ self.app.push_screen(
131
+ ComponentPickerScreen(
132
+ title="New Project — Select Components",
133
+ subtitle=(
134
+ f"Project path: {target}\n"
135
+ "Cross-cutting items (contracts, shared types, etc.) are created automatically."
136
+ ),
137
+ allow_empty=False,
138
+ ),
139
+ lambda picked: self._on_new_project_components(target, picked),
140
+ )
141
+
142
+ def _on_new_project_components(
143
+ self,
144
+ target: "Path",
145
+ picked: list[str] | None,
146
+ ) -> None:
147
+ if picked is None:
148
+ self.notify("New project cancelled", severity="warning")
149
+ return
150
+ if not picked:
151
+ return
152
+ self._do_new_project(target, picked)
153
+
154
+ def _do_new_project(
155
+ self,
156
+ target: "Path",
157
+ components: list[str],
158
+ ) -> None:
159
+ from pathlib import Path
160
+ from onecode.agent.cdh_loader import CdhProjectLoader
161
+
162
+ name = target.name
163
+ from cdh.scaffold import scaffold_dlc_project
164
+
165
+ try:
166
+ scaffold_dlc_project(target, name, components=components)
167
+ except (ValueError, RuntimeError) as e:
168
+ self.notify(str(e), severity="error")
169
+ return
170
+ if _project_db_path(name) is not None:
171
+ self.notify(
172
+ f"Project '{name}' is already in the project list",
173
+ severity="error",
174
+ )
175
+ return
176
+ projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
177
+ projects_dir.mkdir(parents=True, exist_ok=True)
178
+ proj_file = projects_dir / f"{name}.yaml"
179
+ CdhProjectLoader.init_project(target, name)
180
+ import yaml
181
+ proj_data = {"name": name, "path": str(target), "description": ""}
182
+ proj_file.write_text(yaml.dump(proj_data))
183
+ widget = ProjectSummary(name, str(target), id=name)
184
+ self.project_grid_select.mount(widget)
185
+ self.project_grid_select.highlighted = len(self.project_grid_select.children) - 1
186
+ self.notify(
187
+ f"Created project '{name}' at {target} "
188
+ f"(components: {', '.join(components)})"
189
+ )
93
190
 
94
191
  def action_init_project(self) -> None:
95
192
  from pathlib import Path
96
193
  from onecode.config_screen import EditFieldScreen
97
- from onecode.agent.cdh_loader import CdhProjectLoader
98
194
 
99
195
  default_path = str(Path.cwd().resolve())
100
196
  self.app.push_screen(
@@ -117,14 +213,80 @@ class ProjectsScreen(ModalScreen[str]):
117
213
  self.notify("Invalid path", severity="error")
118
214
  return
119
215
  existing = CdhProjectLoader.find_cdh_dir(target)
120
- if existing is not None:
216
+ if existing is not None and existing.parent == target:
121
217
  self.notify(f".cdh already exists at {existing}", severity="warning")
122
218
  return
219
+ if _project_db_path(target.name) is not None:
220
+ self.notify(
221
+ f"Project '{target.name}' is already in the project list",
222
+ severity="warning",
223
+ )
224
+ return
225
+ self.app.push_screen(
226
+ ComponentPickerScreen(
227
+ title="Init .cdh — Select Components",
228
+ subtitle=(
229
+ f"Directory: {target}\n"
230
+ "Pick the components already present in this directory. "
231
+ "Empty selection is allowed (no apps/ created)."
232
+ ),
233
+ allow_empty=True,
234
+ ),
235
+ lambda picked: self._on_init_components(target, picked),
236
+ )
237
+
238
+ def _on_init_components(
239
+ self,
240
+ target: "Path",
241
+ picked: list[str] | None,
242
+ ) -> None:
243
+ if picked is None:
244
+ self.notify("Init cancelled", severity="warning")
245
+ return
246
+ self._do_init_project(target, picked)
247
+
248
+ def _do_init_project(
249
+ self,
250
+ target: "Path",
251
+ components: list[str],
252
+ ) -> None:
253
+ from pathlib import Path
254
+ from onecode.agent.cdh_loader import CdhProjectLoader
255
+ from cdh.scaffold import add_component, init_dlc_project
256
+
123
257
  name = target.name
124
- from cdh.scaffold import scaffold_dlc_project
125
- scaffold_dlc_project(target, name)
258
+ if _project_db_path(name) is not None:
259
+ self.notify(
260
+ f"Project '{name}' is already in the project list",
261
+ severity="error",
262
+ )
263
+ return
264
+ try:
265
+ init_dlc_project(target, name)
266
+ except (ValueError, RuntimeError) as e:
267
+ self.notify(str(e), severity="error")
268
+ return
269
+ for cid in components:
270
+ try:
271
+ add_component(target, cid)
272
+ except (ValueError, FileNotFoundError) as e:
273
+ self.notify(str(e), severity="error")
126
274
  CdhProjectLoader.init_project(target, name)
127
- self.notify(f"Initialized .cdh in {target}")
275
+ import yaml
276
+ projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
277
+ projects_dir.mkdir(parents=True, exist_ok=True)
278
+ proj_file = projects_dir / f"{name}.yaml"
279
+ proj_data = {"name": name, "path": str(target), "description": ""}
280
+ proj_file.write_text(yaml.dump(proj_data))
281
+ widget = ProjectSummary(name, str(target), id=name)
282
+ self.project_grid_select.mount(widget)
283
+ self.project_grid_select.highlighted = len(self.project_grid_select.children) - 1
284
+ suffix = (
285
+ f" (components: {', '.join(components)})"
286
+ if components
287
+ else " (no components)"
288
+ )
289
+ self.notify(f"Initialized .cdh in {target}{suffix}")
128
290
 
129
291
  @on(GridSelect.Selected)
130
292
  def on_selected(self, event: GridSelect.Selected) -> None:
@@ -135,7 +297,7 @@ class ProjectsScreen(ModalScreen[str]):
135
297
 
136
298
  @on(widgets.Button.Pressed, "#new-project")
137
299
  def on_new_project(self) -> None:
138
- self.dismiss("__new__")
300
+ self.action_new_project()
139
301
 
140
302
  @on(widgets.Button.Pressed, "#init-project")
141
303
  def on_init_project(self) -> None:
@@ -260,5 +260,5 @@ class AskUserWidget(VerticalGroup):
260
260
  def _finish(self, value: str) -> None:
261
261
  self._answer = value
262
262
  self._done = True
263
- self.refresh(recompose=True, layout=True)
263
+ self.refresh(recompose=True)
264
264
  self.post_message(AskUserSubmitted(value, self._tool_id))
@@ -267,6 +267,16 @@ This is a view of your conversation with the agent.
267
267
  pass
268
268
 
269
269
 
270
+ def _plan_entries_from_dicts(raw_entries):
271
+ """Deprecated thin wrapper around the shared ``entries_from_dicts`` helper.
272
+
273
+ Kept for backwards compatibility with any external callers; new code
274
+ should import ``entries_from_dicts`` directly from ``tui.widgets.plan``.
275
+ """
276
+ from tui.widgets.plan import entries_from_dicts
277
+ return entries_from_dicts(raw_entries)
278
+
279
+
270
280
  class Conversation(containers.Vertical):
271
281
  """Holds the agent conversation (input, output, and various controls / information)."""
272
282
 
@@ -378,6 +388,7 @@ class Conversation(containers.Vertical):
378
388
  self._agent_thought: AgentThought | None = None
379
389
  self._replay: bool = False
380
390
  self._replay_buffer: list[dict] = []
391
+ self._active_subagents: dict = {}
381
392
  self._last_escape_time: float = monotonic()
382
393
  self._agent_data = agent
383
394
  self._agent_session_id = agent_session_id
@@ -404,6 +415,7 @@ class Conversation(containers.Vertical):
404
415
 
405
416
  self._directory_changed = False
406
417
  self._directory_watcher: DirectoryWatcher | None = None
418
+ self._suppress_directory_changed = False
407
419
 
408
420
  self._initial_prompt = initial_prompt
409
421
 
@@ -559,7 +571,34 @@ class Conversation(containers.Vertical):
559
571
  @on(DirectoryChanged)
560
572
  def on_directory_changed(self, event: DirectoryChanged) -> None:
561
573
  event.stop()
562
- self.post_message(messages.ProjectDirectoryUpdated())
574
+ self._directory_changed = True
575
+ if self._suppress_directory_changed:
576
+ return
577
+ self._suppress_directory_changed = True
578
+ self.post_message(messages.ProjectDirectoryUpdated(project_dir=self.app.project_dir))
579
+ self.set_timer(0.5, self._release_directory_changed_suppress)
580
+
581
+ def _release_directory_changed_suppress(self) -> None:
582
+ self._suppress_directory_changed = False
583
+
584
+ def directory_watcher_swap(self, project_path: Path) -> None:
585
+ """Stop the current DirectoryWatcher (if any) and start a new one
586
+ rooted at *project_path*.
587
+
588
+ Used by ``MainScreen`` when the user switches projects so we don't
589
+ keep receiving events for the old tree.
590
+ """
591
+ if self._directory_watcher is not None:
592
+ self._directory_watcher.stop()
593
+ self._directory_watcher = None
594
+ if project_path is None:
595
+ return
596
+ try:
597
+ watcher = DirectoryWatcher(project_path, self)
598
+ except Exception:
599
+ return
600
+ watcher.start()
601
+ self._directory_watcher = watcher
563
602
 
564
603
  @on(Terminal.Finalized)
565
604
  def on_terminal_finalized(self, event: Terminal.Finalized) -> None:
@@ -572,7 +611,7 @@ class Conversation(containers.Vertical):
572
611
  if self._directory_changed or not self.is_watching_directory:
573
612
  self.prompt.project_directory_updated()
574
613
  self._directory_changed = False
575
- self.post_message(messages.ProjectDirectoryUpdated())
614
+ self.post_message(messages.ProjectDirectoryUpdated(project_dir=self.app.project_dir))
576
615
 
577
616
  @on(Terminal.LongRunning)
578
617
  def on_terminal_long_running(self, event: Terminal.LongRunning) -> None:
@@ -934,7 +973,7 @@ class Conversation(containers.Vertical):
934
973
 
935
974
  if self._directory_changed or not self.is_watching_directory:
936
975
  self._directory_changed = False
937
- self.post_message(messages.ProjectDirectoryUpdated())
976
+ self.post_message(messages.ProjectDirectoryUpdated(project_dir=self.app.project_dir))
938
977
  self.prompt.project_directory_updated()
939
978
 
940
979
  self._turn_count += 1
@@ -1102,12 +1141,12 @@ class Conversation(containers.Vertical):
1102
1141
  case "plan":
1103
1142
  current_thought = None
1104
1143
  self.new_block()
1105
- widgets.append(Plan(entry["entries"]))
1144
+ widgets.append(Plan(_plan_entries_from_dicts(entry["entries"])))
1106
1145
 
1107
1146
  case "subagent_start":
1108
1147
  current_thought = None
1109
1148
  self.new_block()
1110
- sa = SubAgent(entry["agent_type"], tool_id=entry["id"])
1149
+ sa = SubAgent(entry["agent_type"], tool_id=entry["id"], prompt=entry.get("prompt", ""))
1111
1150
  created_subagents[entry["id"]] = sa
1112
1151
  widgets.append(sa)
1113
1152
 
@@ -1116,10 +1155,18 @@ class Conversation(containers.Vertical):
1116
1155
  if sa is not None:
1117
1156
  sa.append_chunk(entry["text"])
1118
1157
 
1158
+ case "subagent_thinking":
1159
+ sa = created_subagents.get(entry["id"])
1160
+ if sa is not None:
1161
+ sa.append_thinking(entry["text"])
1162
+
1119
1163
  case "subagent_end":
1120
1164
  sa = created_subagents.get(entry["id"])
1121
1165
  if sa is not None:
1122
- sa.complete()
1166
+ sa.complete(
1167
+ entry.get("status", "completed"),
1168
+ entry.get("error", ""),
1169
+ )
1123
1170
 
1124
1171
  case "ask_user":
1125
1172
  current_thought = None
@@ -1192,14 +1239,7 @@ class Conversation(containers.Vertical):
1192
1239
  return
1193
1240
  from tui.widgets.plan import Plan
1194
1241
 
1195
- entries = [
1196
- Plan.Entry(
1197
- Content(entry["content"]),
1198
- entry.get("priority", "medium"),
1199
- entry.get("status", "pending"),
1200
- )
1201
- for entry in message.entries
1202
- ]
1242
+ entries = _plan_entries_from_dicts(message.entries)
1203
1243
 
1204
1244
  # Look up any existing Plan widget anywhere in the conversation
1205
1245
  # tree — checking only ``children[-1]`` would create duplicates if
@@ -1310,46 +1350,62 @@ class Conversation(containers.Vertical):
1310
1350
 
1311
1351
  message.stop()
1312
1352
  if self._replay:
1313
- self._replay_buffer.append({"kind": "subagent_start", "agent_type": message.agent_type, "id": message.subagent_id})
1353
+ self._replay_buffer.append({"kind": "subagent_start", "agent_type": message.agent_type, "id": message.subagent_id, "prompt": message.prompt})
1314
1354
  return
1315
- sa = SubAgent(message.agent_type, tool_id=message.subagent_id)
1355
+ sa = SubAgent(message.agent_type, tool_id=message.subagent_id, prompt=message.prompt)
1356
+ self._active_subagents[message.subagent_id] = sa
1316
1357
  await self.post(sa, new_block=True)
1317
1358
 
1318
- @on(acp_messages.SubAgentChunk)
1319
- async def on_acp_subagent_chunk(self, message: acp_messages.SubAgentChunk):
1359
+ def _get_subagent(self, subagent_id: str):
1320
1360
  from tui.widgets.subagent import SubAgent
1321
1361
 
1362
+ sa = self._active_subagents.get(subagent_id)
1363
+ if sa is not None:
1364
+ return sa
1365
+ try:
1366
+ return self.contents.get_child_by_id(subagent_id, SubAgent)
1367
+ except NoMatches:
1368
+ return None
1369
+
1370
+ @on(acp_messages.SubAgentChunk)
1371
+ async def on_acp_subagent_chunk(self, message: acp_messages.SubAgentChunk):
1322
1372
  message.stop()
1323
1373
  if self._replay:
1324
1374
  self._replay_buffer.append({"kind": "subagent_chunk", "id": message.subagent_id, "text": message.text})
1325
1375
  return
1326
- try:
1327
- sa: SubAgent | None = self.contents.get_child_by_id(
1328
- message.subagent_id, SubAgent
1329
- )
1330
- except NoMatches:
1376
+ sa = self._get_subagent(message.subagent_id)
1377
+ if sa is not None:
1378
+ sa.append_chunk(message.text)
1379
+
1380
+ @on(acp_messages.SubAgentThinking)
1381
+ async def on_acp_subagent_thinking(self, message: acp_messages.SubAgentThinking):
1382
+ message.stop()
1383
+ if self._replay:
1384
+ self._replay_buffer.append({
1385
+ "kind": "subagent_thinking",
1386
+ "id": message.subagent_id,
1387
+ "text": message.text,
1388
+ })
1331
1389
  return
1332
- else:
1333
- if sa is not None:
1334
- sa.append_chunk(message.text)
1390
+ sa = self._get_subagent(message.subagent_id)
1391
+ if sa is not None:
1392
+ sa.append_thinking(message.text)
1335
1393
 
1336
1394
  @on(acp_messages.SubAgentEnd)
1337
1395
  async def on_acp_subagent_end(self, message: acp_messages.SubAgentEnd):
1338
- from tui.widgets.subagent import SubAgent
1339
-
1340
1396
  message.stop()
1341
1397
  if self._replay:
1342
- self._replay_buffer.append({"kind": "subagent_end", "id": message.subagent_id})
1343
- return
1344
- try:
1345
- sa: SubAgent | None = self.contents.get_child_by_id(
1346
- message.subagent_id, SubAgent
1347
- )
1348
- except NoMatches:
1398
+ self._replay_buffer.append({
1399
+ "kind": "subagent_end",
1400
+ "id": message.subagent_id,
1401
+ "status": message.status,
1402
+ "error": message.error,
1403
+ })
1349
1404
  return
1350
- else:
1351
- if sa is not None:
1352
- sa.complete()
1405
+ sa = self._get_subagent(message.subagent_id)
1406
+ if sa is not None:
1407
+ sa.complete(message.status, message.error)
1408
+ self._active_subagents.pop(message.subagent_id, None)
1353
1409
 
1354
1410
  @on(acp_messages.AvailableCommandsUpdate)
1355
1411
  async def on_acp_available_commands_update(
@@ -1929,12 +1985,13 @@ class Conversation(containers.Vertical):
1929
1985
  self.cursor_offset = -1
1930
1986
  self.cursor.visible = False
1931
1987
  self.cursor.follow(None)
1932
- contents.refresh(layout=True)
1988
+ contents.refresh()
1933
1989
 
1934
1990
  if prune_children:
1935
1991
  await contents.remove_children(prune_children)
1936
1992
 
1937
- self.call_later(self.window.anchor)
1993
+ if self.window.scroll_y >= self.window.max_scroll_y:
1994
+ self.call_later(self.window.anchor)
1938
1995
 
1939
1996
  async def new_terminal(self) -> Terminal:
1940
1997
  """Create a new interactive Terminal.
@@ -2401,7 +2458,7 @@ class Conversation(containers.Vertical):
2401
2458
  save_config(cfg)
2402
2459
  new_project_dir = Path(project_path) if project_path else Path.cwd()
2403
2460
  self.app.project_dir = new_project_dir
2404
- self.post_message(messages.ProjectDirectoryUpdated())
2461
+ self.post_message(messages.ProjectDirectoryUpdated(project_dir=new_project_dir))
2405
2462
  self.flash(f"Switched to project: {name}", style="success")
2406
2463
  return True
2407
2464
  elif sub_cmd == "new":
@@ -2427,7 +2484,7 @@ class Conversation(containers.Vertical):
2427
2484
  cfg.current_project_path = str(ws)
2428
2485
  save_config(cfg)
2429
2486
  self.app.project_dir = ws
2430
- self.post_message(messages.ProjectDirectoryUpdated())
2487
+ self.post_message(messages.ProjectDirectoryUpdated(project_dir=ws))
2431
2488
  self.flash(f"Created and switched to project: {name}", style="success")
2432
2489
  return True
2433
2490
  else: