@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
@@ -65,9 +65,10 @@ from tui.protocol import BlockProtocol, MenuProtocol, ExpandProtocol
65
65
  from tui.menus import MenuItem
66
66
  from tui.widgets.shell_terminal import ShellTerminal
67
67
  from tui.widgets.ask_user import AskUserWidget, AskUserSubmitted
68
+ from tui.widgets.load_more import LoadMoreIndicator
68
69
 
69
70
  if TYPE_CHECKING:
70
- from collections.abc import Awaitable, Callable
71
+ from collections.abc import Callable
71
72
  from tui.acp.agent import Mode
72
73
  from tui.widgets.terminal import Terminal
73
74
  from tui.widgets.agent_response import AgentResponse
@@ -183,7 +184,12 @@ class Cursor(Static):
183
184
  self.blink_timer = self.set_interval(0.5, self._update_blink, pause=True)
184
185
 
185
186
  def _update_blink(self) -> None:
186
- if self.query_ancestor(Window).has_focus and self.screen.is_active:
187
+ try:
188
+ window = self.query_ancestor(Window)
189
+ except NoMatches:
190
+ self.blink = True
191
+ return
192
+ if window.has_focus and self.screen.is_active:
187
193
  self.blink = not self.blink
188
194
  else:
189
195
  self.blink = False
@@ -263,9 +269,19 @@ This is a view of your conversation with the agent.
263
269
  BINDING_GROUP_TITLE = "View"
264
270
  BINDINGS = [Binding("end", "screen.focus_prompt", "Prompt")]
265
271
 
272
+ auto_follow: var[bool] = var(True)
273
+
266
274
  def update_node_styles(self, animate: bool = True) -> None:
267
275
  pass
268
276
 
277
+ def watch_scroll_y(self, old_value: float, new_value: float) -> None:
278
+ self.auto_follow = new_value >= self.max_scroll_y
279
+ super().watch_scroll_y(old_value, new_value)
280
+
281
+ def scroll_end_if_following(self, animate: bool = True) -> None:
282
+ if self.auto_follow:
283
+ self.scroll_end(animate=animate)
284
+
269
285
 
270
286
  def _plan_entries_from_dicts(raw_entries):
271
287
  """Deprecated thin wrapper around the shared ``entries_from_dicts`` helper.
@@ -316,6 +332,7 @@ class Conversation(containers.Vertical):
316
332
  "cancel",
317
333
  "Cancel",
318
334
  tooltip="Cancel agent's turn",
335
+ priority=True,
319
336
  ),
320
337
  Binding(
321
338
  "ctrl+f",
@@ -330,6 +347,12 @@ class Conversation(containers.Vertical):
330
347
  "Modes",
331
348
  tooltip="Open the mode switcher",
332
349
  ),
350
+ Binding(
351
+ "ctrl+n",
352
+ "fullscreen_block",
353
+ "Full screen",
354
+ tooltip="Open full-screen view of the cursor block",
355
+ ),
333
356
  Binding(
334
357
  "ctrl+c",
335
358
  "interrupt",
@@ -424,6 +447,12 @@ class Conversation(containers.Vertical):
424
447
  self._post_lock = asyncio.Lock()
425
448
  self._auto_named_session = False
426
449
 
450
+ self._total_messages: int = 0
451
+ self._visible_count: int = 0
452
+ self._loaded_up_to: int = 0
453
+ self._load_more_widget: LoadMoreIndicator | None = None
454
+ self._flush_append_at: int | None = None
455
+
427
456
  def update_title(self) -> None:
428
457
  """Update the screen title."""
429
458
  screen = self.screen
@@ -474,6 +503,7 @@ class Conversation(containers.Vertical):
474
503
  self.prompt.prompt_text_area.insert(" ")
475
504
 
476
505
  def watch_project_path(self, path: Path) -> None:
506
+ self.working_directory = str(path)
477
507
  self.post_message(messages.SessionUpdate(path=str(path)))
478
508
 
479
509
  async def watch_shell_history_index(self, previous_index: int, index: int) -> None:
@@ -721,6 +751,13 @@ class Conversation(containers.Vertical):
721
751
  self.refresh_bindings()
722
752
  self.call_after_refresh(self.cursor.follow, cursor_block)
723
753
 
754
+ async def action_fullscreen_block(self) -> None:
755
+ if (cursor_block := self.cursor_block) is not None:
756
+ fullscreen = getattr(cursor_block, "action_fullscreen", None)
757
+ if fullscreen:
758
+ fullscreen()
759
+ self.call_after_refresh(self.cursor.follow, cursor_block)
760
+
724
761
  async def post_agent_response(self, fragment: str = "") -> AgentResponse | None:
725
762
  """Get or create an agent response widget."""
726
763
  from tui.widgets.agent_response import AgentResponse
@@ -926,6 +963,9 @@ class Conversation(containers.Vertical):
926
963
  self._auto_named_session = True
927
964
  return
928
965
 
966
+ if self.agent.session_pk is None:
967
+ return
968
+
929
969
  self._auto_named_session = True
930
970
  name = prompt.strip().split("\n")[0][:60].strip()
931
971
  if name:
@@ -957,6 +997,7 @@ class Conversation(containers.Vertical):
957
997
  )
958
998
  finally:
959
999
  self.busy_count -= 1
1000
+ await self._auto_name_session(prompt)
960
1001
  self.call_later(self.agent_turn_over, stop_reason)
961
1002
 
962
1003
  async def agent_turn_over(self, stop_reason: str | None) -> None:
@@ -971,6 +1012,14 @@ class Conversation(containers.Vertical):
971
1012
  self._agent_response = None
972
1013
  self._complete_thought()
973
1014
 
1015
+ # Safety net: mark any still-active subagents as cancelled.
1016
+ # Normal flow emits subagent_end before getting here, but
1017
+ # cancellation or errors can leave widgets stuck in "running".
1018
+ if self._active_subagents:
1019
+ for sa in self._active_subagents.values():
1020
+ sa.complete(status="cancelled", error="Turn ended")
1021
+ self._active_subagents.clear()
1022
+
974
1023
  if self._directory_changed or not self.is_watching_directory:
975
1024
  self._directory_changed = False
976
1025
  self.post_message(messages.ProjectDirectoryUpdated(project_dir=self.app.project_dir))
@@ -1094,6 +1143,10 @@ class Conversation(containers.Vertical):
1094
1143
  self._replay = message.active
1095
1144
  # When the replay just ended, flush buffer and complete any pending thought.
1096
1145
  if was_replay and not message.active:
1146
+ if message.total_messages > 0:
1147
+ self._total_messages = message.total_messages
1148
+ self._visible_count = message.visible_count
1149
+ self._loaded_up_to = message.total_messages - message.visible_count
1097
1150
  self.call_after_refresh(self._flush_replay_buffer)
1098
1151
  self._complete_thought()
1099
1152
 
@@ -1168,6 +1221,13 @@ class Conversation(containers.Vertical):
1168
1221
  entry.get("error", ""),
1169
1222
  )
1170
1223
 
1224
+ case "subagent_tool_call":
1225
+ sa = created_subagents.get(entry["id"])
1226
+ if sa is not None:
1227
+ await sa.add_or_update_tool_call(
1228
+ entry["tool_id"], entry["tool_call"]
1229
+ )
1230
+
1171
1231
  case "ask_user":
1172
1232
  current_thought = None
1173
1233
  self.new_block()
@@ -1178,11 +1238,50 @@ class Conversation(containers.Vertical):
1178
1238
  questions=entry.get("questions", []),
1179
1239
  ))
1180
1240
 
1241
+ if self._loaded_up_to > 0 and self._flush_append_at is None:
1242
+ load_more = LoadMoreIndicator(count=self._loaded_up_to)
1243
+ widgets.insert(0, load_more)
1244
+ self._load_more_widget = load_more
1245
+
1181
1246
  if widgets:
1182
- await self.contents.mount(*widgets)
1247
+ insert_before = self._flush_append_at
1248
+ if insert_before is not None:
1249
+ await self.contents.mount(*widgets, before=insert_before)
1250
+ self._loaded_up_to = max(0, self._loaded_up_to - len(widgets))
1251
+ if self._loaded_up_to > 0:
1252
+ lm = LoadMoreIndicator(count=self._loaded_up_to)
1253
+ self._load_more_widget = lm
1254
+ await self.contents.mount(lm, before=len(widgets))
1255
+ self._flush_append_at = None
1256
+ else:
1257
+ await self.contents.mount(*widgets)
1183
1258
  self._replay_buffer.clear()
1184
1259
  self._complete_thought()
1185
- self.window.scroll_end(animate=False)
1260
+ self.window.scroll_end_if_following(animate=False)
1261
+
1262
+ @on(LoadMoreIndicator.LoadMorePressed)
1263
+ async def on_load_more_pressed(self, message: LoadMoreIndicator.LoadMorePressed) -> None:
1264
+ message.stop()
1265
+ if self._loaded_up_to <= 0:
1266
+ return
1267
+ batch_size = 50
1268
+ start = max(0, self._loaded_up_to - batch_size)
1269
+ limit = self._loaded_up_to - start
1270
+ if limit <= 0:
1271
+ return
1272
+
1273
+ if self._load_more_widget is not None:
1274
+ await self._load_more_widget.remove()
1275
+ self._load_more_widget = None
1276
+
1277
+ loading = Static(" Loading earlier messages... ")
1278
+ await self.contents.mount(loading, before=0)
1279
+
1280
+ self._flush_append_at = 0
1281
+ self.post_message(acp_messages.SessionReplay(active=True))
1282
+ await self.agent.acp_load_earlier_messages(offset=start, limit=limit)
1283
+ self.post_message(acp_messages.SessionReplay(active=False))
1284
+ await loading.remove()
1186
1285
 
1187
1286
  @on(acp_messages.RequestPermission)
1188
1287
  async def on_acp_request_permission(self, message: acp_messages.RequestPermission):
@@ -1275,6 +1374,25 @@ class Conversation(containers.Vertical):
1275
1374
  return
1276
1375
  tool_id = message.tool_id
1277
1376
 
1377
+ # Tools invoked *inside* a subagent carry a ``subagentId`` (the parent
1378
+ # Spawn toolCallId). Render them as ToolCall cards INSIDE the owning
1379
+ # SubAgent widget (same style as main-conversation tools) instead of
1380
+ # the main conversation.
1381
+ subagent_id = tool_call.get("subagentId")
1382
+ if subagent_id:
1383
+ if self._replay:
1384
+ self._replay_buffer.append({
1385
+ "kind": "subagent_tool_call",
1386
+ "id": subagent_id,
1387
+ "tool_id": tool_id,
1388
+ "tool_call": tool_call,
1389
+ })
1390
+ return
1391
+ sa = self._get_subagent(subagent_id)
1392
+ if sa is not None:
1393
+ await sa.add_or_update_tool_call(tool_id, tool_call)
1394
+ return
1395
+
1278
1396
  if self._replay:
1279
1397
  if isinstance(message, acp_messages.ToolCall):
1280
1398
  self._replay_buffer.append({"kind": "tool_call", "tool_call": tool_call, "tool_id": tool_id})
@@ -1349,12 +1467,18 @@ class Conversation(containers.Vertical):
1349
1467
  from tui.widgets.subagent import SubAgent
1350
1468
 
1351
1469
  message.stop()
1470
+ logger.debug(
1471
+ "[TUI-SUBagent] on_start id=%s type=%s prompt_len=%d replay=%s",
1472
+ message.subagent_id, message.agent_type,
1473
+ len(message.prompt or ""), self._replay,
1474
+ )
1352
1475
  if self._replay:
1353
1476
  self._replay_buffer.append({"kind": "subagent_start", "agent_type": message.agent_type, "id": message.subagent_id, "prompt": message.prompt})
1354
1477
  return
1355
1478
  sa = SubAgent(message.agent_type, tool_id=message.subagent_id, prompt=message.prompt)
1356
1479
  self._active_subagents[message.subagent_id] = sa
1357
1480
  await self.post(sa, new_block=True)
1481
+ logger.debug("[TUI-SUBagent] on_start posted SubAgent id=%s", message.subagent_id)
1358
1482
 
1359
1483
  def _get_subagent(self, subagent_id: str):
1360
1484
  from tui.widgets.subagent import SubAgent
@@ -1370,10 +1494,19 @@ class Conversation(containers.Vertical):
1370
1494
  @on(acp_messages.SubAgentChunk)
1371
1495
  async def on_acp_subagent_chunk(self, message: acp_messages.SubAgentChunk):
1372
1496
  message.stop()
1497
+ sa = self._get_subagent(message.subagent_id)
1498
+ _cnt = (sa._chunk_count_log + 1) if sa else 0
1499
+ if sa:
1500
+ sa._chunk_count_log = _cnt
1501
+ if _cnt <= 5 or _cnt % 50 == 0:
1502
+ logger.debug(
1503
+ "[TUI-SUBagent] on_chunk id=%s chunk#%d bytes=%d sa=%s",
1504
+ message.subagent_id, _cnt, len(message.text or ""),
1505
+ "yes" if sa else "MISSING",
1506
+ )
1373
1507
  if self._replay:
1374
1508
  self._replay_buffer.append({"kind": "subagent_chunk", "id": message.subagent_id, "text": message.text})
1375
1509
  return
1376
- sa = self._get_subagent(message.subagent_id)
1377
1510
  if sa is not None:
1378
1511
  sa.append_chunk(message.text)
1379
1512
 
@@ -1394,6 +1527,16 @@ class Conversation(containers.Vertical):
1394
1527
  @on(acp_messages.SubAgentEnd)
1395
1528
  async def on_acp_subagent_end(self, message: acp_messages.SubAgentEnd):
1396
1529
  message.stop()
1530
+ logger.debug(
1531
+ "[TUI-SUBagent] on_end id=%s status=%s err=%r active=%s "
1532
+ "chunks=%d bytes=%d",
1533
+ message.subagent_id, message.status, (message.error or "")[:80],
1534
+ message.subagent_id in self._active_subagents,
1535
+ (self._get_subagent(message.subagent_id)._chunk_count_log
1536
+ if self._get_subagent(message.subagent_id) else 0),
1537
+ (self._get_subagent(message.subagent_id)._byte_count_log
1538
+ if self._get_subagent(message.subagent_id) else 0),
1539
+ )
1397
1540
  if self._replay:
1398
1541
  self._replay_buffer.append({
1399
1542
  "kind": "subagent_end",
@@ -2132,17 +2275,18 @@ class Conversation(containers.Vertical):
2132
2275
  cursor_block.block_cursor_down()
2133
2276
  self.refresh_block_cursor()
2134
2277
 
2135
- @work
2136
2278
  async def action_cancel(self) -> None:
2137
- if monotonic() - self._last_escape_time < 3:
2279
+ now = monotonic()
2280
+ if now - self._last_escape_time < 3:
2138
2281
  if (agent := self.agent) is not None:
2282
+ self._last_escape_time = 0.0
2139
2283
  if await agent.cancel():
2140
2284
  self.flash("Turn cancelled", style="success")
2141
2285
  else:
2142
2286
  self.flash("Agent declined to cancel. Please wait.", style="error")
2143
2287
  else:
2144
2288
  self.flash("Press [b]esc[/] again to cancel agent's turn")
2145
- self._last_escape_time = monotonic()
2289
+ self._last_escape_time = now
2146
2290
 
2147
2291
  def focus_prompt(self, reset_cursor: bool = True, scroll_end: bool = True) -> None:
2148
2292
  """Focus the prompt input.
@@ -2419,8 +2563,7 @@ class Conversation(containers.Vertical):
2419
2563
 
2420
2564
  if sub_cmd == "list":
2421
2565
  from pathlib import Path
2422
- from onecode.config import CLOUD_DEV_HARNESS_DIR
2423
- projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
2566
+ projects_dir = Path.home() / ".cdh" / "projects"
2424
2567
  if not projects_dir.exists():
2425
2568
  self.notify("No projects found", title="/project list")
2426
2569
  return True
@@ -2438,8 +2581,8 @@ class Conversation(containers.Vertical):
2438
2581
  self.notify("Project name required", title="/project load", severity="error")
2439
2582
  return True
2440
2583
  from pathlib import Path
2441
- from onecode.config import load_config, save_config, CLOUD_DEV_HARNESS_DIR
2442
- projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
2584
+ from onecode.config import load_config, save_config
2585
+ projects_dir = Path.home() / ".cdh" / "projects"
2443
2586
  project_file = None
2444
2587
  for ext in ["yaml", "yml", "json"]:
2445
2588
  pf = projects_dir / f"{name}.{ext}"
@@ -2466,11 +2609,11 @@ class Conversation(containers.Vertical):
2466
2609
  self.notify("Usage: /project new <name> [path]", title="/project new", severity="error")
2467
2610
  return True
2468
2611
  from pathlib import Path
2469
- from onecode.config import load_config, save_config, CLOUD_DEV_HARNESS_DIR
2612
+ from onecode.config import load_config, save_config
2470
2613
  from onecode.agent.cdh_loader import CdhProjectLoader
2471
2614
  from cdh.scaffold import scaffold_dlc_project
2472
2615
  import yaml
2473
- projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
2616
+ projects_dir = Path.home() / ".cdh" / "projects"
2474
2617
  projects_dir.mkdir(parents=True, exist_ok=True)
2475
2618
  project_path = path or str(Path.cwd())
2476
2619
  ws = Path(project_path).expanduser().resolve()
@@ -1,5 +1,15 @@
1
1
  from textual_diff_view import DiffView
2
2
 
3
+ # Workaround for textual_diff_view crash: DiffCode has ALLOW_SELECT = True
4
+ # but its parent chain is not a proper Widget, causing Textual's
5
+ # _forward_event to assert-fail on mouse move.
6
+ try:
7
+ from textual_diff_view._diff_view import DiffCode
8
+
9
+ DiffCode.ALLOW_SELECT = False
10
+ except ImportError:
11
+ pass
12
+
3
13
  from tui.app import A2TUIApp
4
14
 
5
15
 
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from textual.binding import Binding
4
+ from textual.message import Message
5
+ from textual.widgets import Static
6
+
7
+
8
+ class LoadMoreIndicator(Static, can_focus=True):
9
+ """Clickable 'load earlier messages' indicator at top of conversation."""
10
+
11
+ class LoadMorePressed(Message):
12
+ pass
13
+
14
+ BINDINGS = [
15
+ Binding("enter", "press", "Load"),
16
+ ]
17
+
18
+ def __init__(self, count: int, batch: int = 50) -> None:
19
+ self.count = count
20
+ label = f" ▲ Load {min(count, batch)} earlier messages ({count} remaining) ▲ "
21
+ super().__init__(label)
22
+
23
+ def action_press(self) -> None:
24
+ self.post_message(self.LoadMorePressed())
@@ -5,11 +5,16 @@ import subprocess
5
5
 
6
6
  from textual import work
7
7
  from textual.app import ComposeResult
8
+ from textual.binding import Binding
9
+ from textual.message import Message
8
10
  from textual.reactive import reactive
9
11
  from textual.timer import Timer
12
+ from textual.widget import Widget
10
13
  from textual.widgets import Static
11
14
  from textual import containers
12
15
 
16
+ from dataclasses import dataclass
17
+
13
18
 
14
19
  CONTENT_WIDTH = 35
15
20
 
@@ -55,13 +60,17 @@ def _tag_color(added: int, deleted: int) -> str:
55
60
  return DIFF_COLORS["dim"]
56
61
 
57
62
 
58
- class ModifiedFiles(containers.Vertical):
63
+ class ModifiedFiles(containers.Vertical, can_focus=True):
59
64
  DEFAULT_CSS = """
60
65
  ModifiedFiles {
61
66
  height: auto;
62
67
  overflow-y: auto;
63
68
  padding: 0 0 0 0;
64
69
 
70
+ &:focus {
71
+ background-tint: $surface 10%;
72
+ }
73
+
65
74
  #mf-files {
66
75
  height: auto;
67
76
  padding: 0 0 0 1;
@@ -69,8 +78,24 @@ class ModifiedFiles(containers.Vertical):
69
78
  }
70
79
  """
71
80
 
81
+ BINDINGS = [
82
+ Binding("up", "cursor_up", "Up", group="Files"),
83
+ Binding("down", "cursor_down", "Down", group="Files"),
84
+ Binding("enter", "select", "Select"),
85
+ ]
86
+
87
+ @dataclass
88
+ class FileSelected(Message):
89
+ filepath: str
90
+ modified_files: "ModifiedFiles"
91
+
92
+ @property
93
+ def control(self) -> Widget:
94
+ return self.modified_files
95
+
72
96
  path: reactive[Path | None] = reactive(None)
73
97
  files: reactive[list[str]] = reactive(list, recompose=False)
98
+ highlighted: reactive[int | None] = reactive(None)
74
99
 
75
100
  def __init__(
76
101
  self,
@@ -84,6 +109,8 @@ class ModifiedFiles(containers.Vertical):
84
109
  self._status_kind: str = "no-changes"
85
110
  self._status_text: str = NO_CHANGES_TEXT
86
111
  self._initial_path: Path | None = path
112
+ self._filepaths: list[str] = []
113
+ self._lines_data: tuple[list[str], dict[str, tuple[int, int]]] | None = None
87
114
 
88
115
  def on_mount(self) -> None:
89
116
  initial = self._initial_path
@@ -93,17 +120,12 @@ class ModifiedFiles(containers.Vertical):
93
120
 
94
121
  def watch_path(self, path: Path | None) -> None:
95
122
  self._cancel_debounce()
96
- if path is None:
97
- self._show_status("no-changes", NO_CHANGES_TEXT)
98
- return
99
- self._show_status("-loading", LOADING_TEXT)
100
- self._schedule_run(path)
123
+ self._schedule_run(path or Path.cwd())
101
124
 
102
125
  def refresh_files(self) -> None:
103
- if self.path is not None:
104
- self._cancel_debounce()
105
- self._show_status("-loading", LOADING_TEXT)
106
- self._run_git_status(self.path)
126
+ target = self.path or Path.cwd()
127
+ self._cancel_debounce()
128
+ self._run_git_status(target)
107
129
 
108
130
  def _schedule_run(self, path: Path) -> None:
109
131
  timer = self.set_timer(
@@ -184,13 +206,33 @@ class ModifiedFiles(containers.Vertical):
184
206
  def _render_lines(self, data: tuple[list[str], dict[str, tuple[int, int]]]) -> None:
185
207
  lines, diffmap = data
186
208
  self.files = list(lines)
209
+ self._filepaths = [l[3:] for l in lines]
187
210
  if not lines:
188
211
  self._show_status("no-changes", NO_CHANGES_TEXT)
212
+ self._lines_data = None
213
+ self.highlighted = None
189
214
  return
190
215
  self._show_status("file-modified", "")
216
+ self._lines_data = data
217
+ if self.highlighted is None or self.highlighted >= len(self._filepaths):
218
+ self.highlighted = 0
219
+ self._apply_rendering()
191
220
 
221
+ def on_focus(self) -> None:
222
+ if self.highlighted is None and self._filepaths:
223
+ self.highlighted = 0
224
+
225
+ def watch_highlighted(
226
+ self, old: int | None, highlighted: int | None
227
+ ) -> None:
228
+ if self._lines_data is not None:
229
+ self._apply_rendering()
230
+
231
+ def _apply_rendering(self) -> None:
232
+ assert self._lines_data is not None
233
+ lines, diffmap = self._lines_data
192
234
  out: list[str] = []
193
- for raw_line in lines:
235
+ for idx, raw_line in enumerate(lines):
194
236
  filepath = raw_line[3:]
195
237
  icon, sc = _status_info(raw_line[:2])
196
238
 
@@ -216,10 +258,42 @@ class ModifiedFiles(containers.Vertical):
216
258
  disp = filepath[:max_fn - 1] + "…" if len(filepath) > max_fn else filepath
217
259
  pad = CONTENT_WIDTH - 2 - len(disp) - tag_len
218
260
  pad_s = " " * pad if pad > 0 else ""
219
- out.append(f"[{sc}]{icon} {disp}[/]{pad_s}{tag_s}")
261
+ line = f"[{sc}]{icon} {disp}[/]{pad_s}{tag_s}"
262
+
263
+ if idx == self.highlighted:
264
+ line = f"[reverse]{line}[/]"
265
+ out.append(line)
220
266
 
221
267
  files_widget = self.query_one("#mf-files", Static)
222
268
  files_widget.update("\n".join(out))
269
+ self.call_after_refresh(self._scroll_to_highlighted)
270
+
271
+ def _scroll_to_highlighted(self) -> None:
272
+ if self.highlighted is not None:
273
+ self.scroll_to(y=self.highlighted, animate=False)
274
+
275
+ def action_cursor_up(self) -> None:
276
+ if not self._filepaths:
277
+ return
278
+ if self.highlighted is None or self.highlighted <= 0:
279
+ self.highlighted = 0
280
+ else:
281
+ self.highlighted -= 1
282
+
283
+ def action_cursor_down(self) -> None:
284
+ if not self._filepaths:
285
+ return
286
+ if self.highlighted is None:
287
+ self.highlighted = 0
288
+ elif self.highlighted >= len(self._filepaths) - 1:
289
+ self.highlighted = len(self._filepaths) - 1
290
+ else:
291
+ self.highlighted += 1
292
+
293
+ def action_select(self) -> None:
294
+ if self.highlighted is not None and self._filepaths:
295
+ filepath = self._filepaths[self.highlighted]
296
+ self.post_message(self.FileSelected(filepath, self))
223
297
 
224
298
  def _show_status(self, kind: str, text: str) -> None:
225
299
  self._status_kind = kind
@@ -182,7 +182,7 @@ class Plan(containers.Grid):
182
182
  elif status == "pending":
183
183
  return Content(" • ")
184
184
  elif status == "in_progress":
185
- return Content("👉 ")
185
+ return Content(" 👉")
186
186
  return Content()
187
187
 
188
188
 
@@ -7,7 +7,6 @@ from textual import getters
7
7
  from tui.app import A2TUIApp
8
8
  from tui.widgets.grid_select import GridSelect
9
9
  from tui.widgets.project_summary import ProjectSummary
10
- from onecode.config import CLOUD_DEV_HARNESS_DIR
11
10
 
12
11
 
13
12
  def _read_project_path(pf: Path) -> str:
@@ -47,11 +46,19 @@ class ProjectGridSelect(GridSelect):
47
46
  return True
48
47
 
49
48
  def compose(self) -> ComposeResult:
50
- projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
49
+ projects_dir = Path.home() / ".cdh" / "projects"
51
50
  projects_dir.mkdir(parents=True, exist_ok=True)
52
51
  project_files = list(projects_dir.glob("*.yaml")) + list(projects_dir.glob("*.json"))
53
52
  for pf in sorted(project_files):
54
53
  yield ProjectSummary(pf.stem, _read_project_path(pf), id=pf.stem)
55
54
 
55
+ async def reload(self) -> None:
56
+ await self.remove_children()
57
+ projects_dir = Path.home() / ".cdh" / "projects"
58
+ projects_dir.mkdir(parents=True, exist_ok=True)
59
+ project_files = list(projects_dir.glob("*.yaml")) + list(projects_dir.glob("*.json"))
60
+ for pf in sorted(project_files):
61
+ await self.mount(ProjectSummary(pf.stem, _read_project_path(pf), id=pf.stem))
62
+
56
63
  def refresh(self, **kwargs) -> None:
57
64
  super().refresh(**kwargs)
@@ -111,6 +111,12 @@ class SessionGridSelect(GridSelect):
111
111
  break
112
112
  return
113
113
 
114
+ for i, existing in enumerate(self._all_sessions):
115
+ if existing.mode_name == mode_name:
116
+ self._all_sessions[i] = details
117
+ self._render_page()
118
+ return
119
+
114
120
  if details.session_pk is not None:
115
121
  for i, existing in enumerate(self._all_sessions):
116
122
  if existing.session_pk == details.session_pk:
@@ -1,6 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
 
3
3
  from textual.app import ComposeResult
4
+ from textual.binding import Binding
4
5
  from textual.widget import Widget
5
6
  from textual import containers
6
7
  from textual import widgets
@@ -9,6 +10,9 @@ from textual.message import Message
9
10
 
10
11
  class SideBarCollapsible(widgets.Collapsible):
11
12
  BINDING_GROUP_TITLE = "Sidebar collapsible"
13
+ BINDINGS = [
14
+ Binding("down", "focus_content", "Focus content", show=False),
15
+ ]
12
16
  HELP = """\
13
17
  ## Sidebar
14
18
 
@@ -20,6 +24,12 @@ The Sidebar contains additonal information associated with the conversation.
20
24
  - **enter** expand or collapse secions
21
25
  """
22
26
 
27
+ def action_focus_content(self) -> None:
28
+ """Move focus into the content area when expanded."""
29
+ if self.collapsed:
30
+ return
31
+ self.screen.focus_next()
32
+
23
33
 
24
34
  class SideBar(containers.Vertical):
25
35
  BINDING_GROUP_TITLE = "Sidebar"