@scemoon/cdh 1.0.3 → 1.0.5

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 (294) 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 +14 -3
  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 +462 -49
  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/tui/__pycache__/about.cpython-314.pyc +0 -0
  182. package/tui/__pycache__/agents.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__/messages.cpython-314.pyc +0 -0
  187. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  188. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  189. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  190. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  191. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  192. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  193. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  194. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  195. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  196. package/tui/acp/agent.py +125 -58
  197. package/tui/acp/api.py +12 -0
  198. package/tui/acp/messages.py +3 -1
  199. package/tui/acp/prompt.py +0 -57
  200. package/tui/acp/protocol.py +6 -0
  201. package/tui/agents.py +8 -7
  202. package/tui/app.py +11 -33
  203. package/tui/data/agents/ampcode.com.toml +1 -0
  204. package/tui/data/agents/augmentcode.com.toml +1 -0
  205. package/tui/data/agents/cdh.cloud-dev-harness.toml +3 -2
  206. package/tui/data/agents/crowai.dev.toml +1 -0
  207. package/tui/data/agents/deepagents.langchain.com +1 -0
  208. package/tui/data/agents/docker.com.toml +1 -0
  209. package/tui/data/agents/goose.ai.toml +1 -0
  210. package/tui/data/agents/inference.huggingface.co.toml +1 -1
  211. package/tui/data/agents/kiro.dev.toml +1 -0
  212. package/tui/data/agents/onecode.dev.toml +22 -0
  213. package/tui/data/agents/openclaw.ai.toml +1 -1
  214. package/tui/data/agents/openhands.dev.toml +1 -0
  215. package/tui/data/agents/stakpak.dev.toml +1 -0
  216. package/tui/data/agents/vibe.mistral.ai.toml +1 -0
  217. package/tui/data/agents/vtcode.dev.toml +1 -0
  218. package/tui/messages.py +2 -0
  219. package/tui/path_complete.py +1 -1
  220. package/tui/paths.py +16 -0
  221. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  222. package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
  223. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  224. package/tui/screens/__pycache__/main.cpython-313.pyc +0 -0
  225. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  226. package/tui/screens/__pycache__/permissions.cpython-314.pyc +0 -0
  227. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  228. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  229. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  230. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  231. package/tui/screens/__pycache__/tool_content_screen.cpython-314.pyc +0 -0
  232. package/tui/screens/component_picker.py +35 -15
  233. package/tui/screens/diff_screen.py +84 -0
  234. package/tui/screens/log.py +25 -25
  235. package/tui/screens/main.py +48 -26
  236. package/tui/screens/projects.tcss +5 -1
  237. package/tui/screens/projects_app.py +6 -6
  238. package/tui/screens/projects_screen.py +19 -39
  239. package/tui/screens/sessions.py +3 -5
  240. package/tui/screens/store.py +1 -1
  241. package/tui/session_tracker.py +3 -0
  242. package/tui/settings_schema.py +2 -2
  243. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  244. package/tui/visuals/columns.py +1 -2
  245. package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
  246. package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
  247. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  248. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  249. package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
  250. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  251. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  252. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  253. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  254. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  255. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  256. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  257. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  258. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  259. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  260. package/tui/widgets/agent_response.py +0 -1
  261. package/tui/widgets/agent_thought.py +0 -1
  262. package/tui/widgets/conversation.py +157 -14
  263. package/tui/widgets/diff_view.py +10 -0
  264. package/tui/widgets/load_more.py +24 -0
  265. package/tui/widgets/modified_files.py +173 -105
  266. package/tui/widgets/plan.py +1 -1
  267. package/tui/widgets/project_grid_select.py +9 -2
  268. package/tui/widgets/session_grid_select.py +6 -0
  269. package/tui/widgets/side_bar.py +10 -0
  270. package/tui/widgets/subagent.py +92 -58
  271. package/tui/widgets/subagent_screen.py +638 -115
  272. package/tui/widgets/terminal.py +1 -2
  273. package/tui/widgets/terminal_tool.py +11 -2
  274. package/tui/widgets/tool_call.py +35 -2
  275. package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
  276. package/cdh/__pycache__/__main__.cpython-314.pyc +0 -0
  277. package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
  278. package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
  279. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
  280. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
  281. package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
  282. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  283. package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
  284. package/onecode/memory/offload.py +0 -114
  285. package/onecode/memory/symbolic.py +0 -164
  286. package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
  287. package/onecode/tasks/__pycache__/__init__.cpython-314.pyc +0 -0
  288. package/onecode/tasks/__pycache__/manager.cpython-314.pyc +0 -0
  289. package/onecode/tasks/__pycache__/models.cpython-314.pyc +0 -0
  290. package/tui/__pycache__/cli.cpython-314.pyc +0 -0
  291. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  292. package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
  293. package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
  294. package/tui/widgets/__pycache__/tool_call.cpython-313.pyc +0 -0
@@ -5,11 +5,18 @@ 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
+
18
+
19
+ CONTENT_WIDTH = 35
13
20
 
14
21
  GIT_STATUS_TIMEOUT = 5.0
15
22
  GIT_STATUS_DEBOUNCE = 0.2
@@ -33,53 +40,62 @@ IGNORED_DIRS = frozenset(
33
40
  }
34
41
  )
35
42
 
43
+ STATUS_ICONS = {"?": "?", "M": "M", "A": "A", "D": "D", "R": "R", "C": "C"}
44
+ STATUS_COLORS = {"?": "green", "M": "yellow", "A": "green", "D": "red", "R": "#888888", "C": "green"}
45
+ DIFF_COLORS: dict[str, str] = {"add": "green", "del": "red", "mix": "yellow", "dim": "#555555"}
36
46
 
37
- class ModifiedFiles(containers.Vertical):
38
- DEFAULT_CSS = """
39
- ModifiedFiles {
40
- height: auto;
41
- max-height: 15;
42
- overflow-y: auto;
43
47
 
44
- Static {
45
- padding: 0 1;
46
- }
48
+ def _status_info(raw: str) -> tuple[str, str]:
49
+ ch = raw.strip()[:1] or "M"
50
+ return STATUS_ICONS.get(ch, "M"), STATUS_COLORS.get(ch, "yellow")
47
51
 
48
- .file-added {
49
- color: $text-success;
50
- }
51
-
52
- .file-modified {
53
- color: $text-warning;
54
- }
55
52
 
56
- .file-deleted {
57
- color: $text-error;
58
- }
53
+ def _tag_color(added: int, deleted: int) -> str:
54
+ if added and deleted:
55
+ return DIFF_COLORS["mix"]
56
+ if added:
57
+ return DIFF_COLORS["add"]
58
+ if deleted:
59
+ return DIFF_COLORS["del"]
60
+ return DIFF_COLORS["dim"]
59
61
 
60
- .file-renamed {
61
- color: $text-secondary;
62
- }
63
62
 
64
- .no-changes {
65
- color: $text-secondary;
66
- text-style: italic;
67
- }
63
+ class ModifiedFiles(containers.Vertical, can_focus=True):
64
+ DEFAULT_CSS = """
65
+ ModifiedFiles {
66
+ height: auto;
67
+ overflow-y: auto;
68
+ padding: 0 0 0 0;
68
69
 
69
- .not-a-repo {
70
- color: $text-secondary;
71
- text-style: italic;
70
+ &:focus {
71
+ background-tint: $surface 10%;
72
72
  }
73
73
 
74
- .-loading {
75
- color: $text-secondary;
76
- text-style: italic;
74
+ #mf-files {
75
+ height: auto;
76
+ padding: 0 0 0 1;
77
77
  }
78
78
  }
79
79
  """
80
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
+
81
96
  path: reactive[Path | None] = reactive(None)
82
97
  files: reactive[list[str]] = reactive(list, recompose=False)
98
+ highlighted: reactive[int | None] = reactive(None)
83
99
 
84
100
  def __init__(
85
101
  self,
@@ -92,8 +108,9 @@ class ModifiedFiles(containers.Vertical):
92
108
  self._debounce_timer: Timer | None = None
93
109
  self._status_kind: str = "no-changes"
94
110
  self._status_text: str = NO_CHANGES_TEXT
95
- self._file_widgets: list[Static] = []
96
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
97
114
 
98
115
  def on_mount(self) -> None:
99
116
  initial = self._initial_path
@@ -102,21 +119,13 @@ class ModifiedFiles(containers.Vertical):
102
119
  self.path = initial
103
120
 
104
121
  def watch_path(self, path: Path | None) -> None:
105
- """Schedule a (debounced, async) git status when path changes."""
106
122
  self._cancel_debounce()
107
- if path is None:
108
- self._show_status("no-changes", NO_CHANGES_TEXT)
109
- self._clear_file_widgets()
110
- return
111
- self._show_status("-loading", LOADING_TEXT)
112
- self._schedule_run(path)
123
+ self._schedule_run(path or Path.cwd())
113
124
 
114
125
  def refresh_files(self) -> None:
115
- """Re-run git status manually (e.g. after file system changes)."""
116
- if self.path is not None:
117
- self._cancel_debounce()
118
- self._show_status("-loading", LOADING_TEXT)
119
- self._run_git_status(self.path)
126
+ target = self.path or Path.cwd()
127
+ self._cancel_debounce()
128
+ self._run_git_status(target)
120
129
 
121
130
  def _schedule_run(self, path: Path) -> None:
122
131
  timer = self.set_timer(
@@ -136,8 +145,6 @@ class ModifiedFiles(containers.Vertical):
136
145
 
137
146
  @work(exclusive=True, group="modified-files-git-status", thread=True)
138
147
  def _run_git_status(self, path: Path) -> None:
139
- """Worker entry: do the slow ``git status`` off the UI thread,
140
- then marshal results back via ``app.call_from_thread``."""
141
148
  app = self.app
142
149
  outcome = self._do_git_status(path)
143
150
  if outcome is None:
@@ -145,24 +152,18 @@ class ModifiedFiles(containers.Vertical):
145
152
  kind, text, lines = outcome
146
153
  if lines is None:
147
154
  app.call_from_thread(self._show_status, kind, text)
148
- app.call_from_thread(self._clear_file_widgets)
149
155
  else:
150
156
  app.call_from_thread(self._render_lines, lines)
151
157
 
152
158
  def _do_git_status(
153
159
  self, path: Path
154
160
  ) -> tuple[str, str, list[str] | None] | None:
155
- """Pure helper for ``git status``. Returns
156
- ``(kind, text, lines_or_None)`` for the UI to render, or ``None``
157
- if the call should silently no-op (e.g. ignored path).
158
- """
159
161
  if any(part in IGNORED_DIRS for part in path.parts):
160
162
  return ("not-a-repo", NOT_A_REPO_TEXT, None)
161
163
  try:
162
- result = subprocess.run(
164
+ status = subprocess.run(
163
165
  ["git", "-C", str(path), "status", "--porcelain"],
164
- capture_output=True,
165
- text=True,
166
+ capture_output=True, text=True,
166
167
  timeout=GIT_STATUS_TIMEOUT,
167
168
  )
168
169
  except subprocess.TimeoutExpired:
@@ -171,68 +172,135 @@ class ModifiedFiles(containers.Vertical):
171
172
  return ("not-a-repo", NOT_A_REPO_TEXT, None)
172
173
  except Exception:
173
174
  return ("no-changes", NO_CHANGES_TEXT, None)
174
- if result.returncode != 0:
175
+ if status.returncode != 0:
175
176
  return ("not-a-repo", NOT_A_REPO_TEXT, None)
176
- lines = [
177
- line for line in result.stdout.splitlines() if line.strip()
178
- ]
177
+
178
+ lines = [l for l in status.stdout.splitlines() if l.strip()]
179
179
  if not lines:
180
- return ("no-changes", NO_CHANGES_TEXT, [])
181
- return ("file-modified", "", lines)
180
+ return ("no-changes", NO_CHANGES_TEXT, ([], {}))
181
+
182
+ diffmap: dict[str, tuple[int, int]] = {}
183
+ for diff_cmd in (
184
+ ["git", "-C", str(path), "diff", "--numstat"],
185
+ ["git", "-C", str(path), "diff", "--cached", "--numstat"],
186
+ ):
187
+ try:
188
+ r = subprocess.run(
189
+ diff_cmd, capture_output=True, text=True,
190
+ timeout=GIT_STATUS_TIMEOUT,
191
+ )
192
+ except Exception:
193
+ continue
194
+ for dl in r.stdout.splitlines():
195
+ parts = dl.split("\t", 2)
196
+ if len(parts) == 3:
197
+ added, deleted = parts[0], parts[1]
198
+ if added != "-":
199
+ a, d = int(added), int(deleted)
200
+ fp = parts[2]
201
+ prev_a, prev_d = diffmap.get(fp, (0, 0))
202
+ diffmap[fp] = (prev_a + a, prev_d + d)
203
+
204
+ return ("file-modified", "", (lines, diffmap))
182
205
 
183
- def _render_lines(self, lines: list[str]) -> None:
206
+ def _render_lines(self, data: tuple[list[str], dict[str, tuple[int, int]]]) -> None:
207
+ lines, diffmap = data
184
208
  self.files = list(lines)
209
+ self._filepaths = [l[3:] for l in lines]
185
210
  if not lines:
186
211
  self._show_status("no-changes", NO_CHANGES_TEXT)
187
- self._clear_file_widgets()
212
+ self._lines_data = None
213
+ self.highlighted = None
188
214
  return
189
215
  self._show_status("file-modified", "")
190
- self._clear_file_widgets()
191
- try:
192
- header = self.query_one("#mf-status", Static)
193
- except Exception:
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()
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
234
+ out: list[str] = []
235
+ for idx, raw_line in enumerate(lines):
236
+ filepath = raw_line[3:]
237
+ icon, sc = _status_info(raw_line[:2])
238
+
239
+ added, deleted = diffmap.get(filepath, (0, 0))
240
+ if "?" in raw_line[:2]:
241
+ tag = "new"
242
+ tc = DIFF_COLORS["dim"]
243
+ elif "D" in raw_line[:2]:
244
+ tag = "del"
245
+ tc = DIFF_COLORS["del"]
246
+ elif added or deleted:
247
+ tag = f"+{added}/-{deleted}"
248
+ tc = _tag_color(added, deleted)
249
+ else:
250
+ tag = ""
251
+ tc = ""
252
+
253
+ tag_s = f" [{tc}]{tag}[/]" if tag else ""
254
+ tag_len = len(tag) + 1 if tag else 0
255
+ max_fn = CONTENT_WIDTH - 2 - tag_len
256
+ if max_fn < 3:
257
+ max_fn = 3
258
+ disp = filepath[:max_fn - 1] + "…" if len(filepath) > max_fn else filepath
259
+ pad = CONTENT_WIDTH - 2 - len(disp) - tag_len
260
+ pad_s = " " * pad if pad > 0 else ""
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)
266
+
267
+ files_widget = self.query_one("#mf-files", Static)
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:
194
277
  return
195
- for line in lines:
196
- status = line[:2]
197
- filepath = line[3:]
198
- cls = self._class_for_status(status)
199
- widget = Static(f" {filepath}", classes=cls)
200
- self._file_widgets.append(widget)
201
- self.mount(widget, after=header)
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))
202
297
 
203
298
  def _show_status(self, kind: str, text: str) -> None:
204
299
  self._status_kind = kind
205
300
  self._status_text = text
206
- try:
207
- header = self.query_one("#mf-status", Static)
208
- except Exception:
209
- return
210
- header.set_classes(kind)
211
- header.update(text)
212
- if kind != "file-modified":
213
- self._clear_file_widgets()
214
-
215
- def _clear_file_widgets(self) -> None:
216
- for w in self._file_widgets:
217
- try:
218
- w.remove()
219
- except Exception:
220
- pass
221
- self._file_widgets = []
301
+ status = self.query_one("#mf-status", Static)
302
+ status.update(text)
222
303
 
223
304
  def compose(self) -> ComposeResult:
224
- yield Static(self._status_text, classes=self._status_kind, id="mf-status")
225
-
226
- @staticmethod
227
- def _class_for_status(status: str) -> str:
228
- if "?" in status:
229
- return "file-added"
230
- if "D" in status:
231
- return "file-deleted"
232
- if "A" in status or "C" in status:
233
- return "file-added"
234
- if "R" in status:
235
- return "file-renamed"
236
- if "M" in status:
237
- return "file-modified"
238
- return "file-modified"
305
+ yield Static(self._status_text, id="mf-status")
306
+ yield Static("", id="mf-files")
@@ -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"