@scemoon/cdh 1.0.0

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 (528) hide show
  1. package/ai-dlc-skill/SKILL.md +75 -0
  2. package/ai-dlc-skill/agents/deliver-agent.md +24 -0
  3. package/ai-dlc-skill/agents/master.md +44 -0
  4. package/ai-dlc-skill/agents/plan-agent.md +23 -0
  5. package/ai-dlc-skill/agents/understand-agent.md +23 -0
  6. package/ai-dlc-skill/agents/verify-agent.md +27 -0
  7. package/ai-dlc-skill/architecture/project-structure.md +78 -0
  8. package/ai-dlc-skill/brownfield/README.md +30 -0
  9. package/ai-dlc-skill/brownfield/generate-context.sh +64 -0
  10. package/ai-dlc-skill/brownfield/scripts/deps.sh +27 -0
  11. package/ai-dlc-skill/brownfield/scripts/discover.sh +34 -0
  12. package/ai-dlc-skill/brownfield/scripts/extract-api.sh +45 -0
  13. package/ai-dlc-skill/components/backend.md +34 -0
  14. package/ai-dlc-skill/components/desktop.md +30 -0
  15. package/ai-dlc-skill/components/mya.md +29 -0
  16. package/ai-dlc-skill/components/native.md +30 -0
  17. package/ai-dlc-skill/components/tta.md +29 -0
  18. package/ai-dlc-skill/components/web.md +31 -0
  19. package/ai-dlc-skill/components/wxa.md +29 -0
  20. package/ai-dlc-skill/contracts/README.md +25 -0
  21. package/ai-dlc-skill/core/adaptive-flow.md +36 -0
  22. package/ai-dlc-skill/core/security.md +29 -0
  23. package/ai-dlc-skill/cross-tool/cline/export.sh +31 -0
  24. package/ai-dlc-skill/cross-tool/copilot/export.sh +47 -0
  25. package/ai-dlc-skill/cross-tool/cursor/export.sh +32 -0
  26. package/ai-dlc-skill/cross-tool/export.sh +22 -0
  27. package/ai-dlc-skill/cross-tool/onecode/config.yaml.tpl +17 -0
  28. package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +31 -0
  29. package/ai-dlc-skill/cross-tool/onecode/export.sh +48 -0
  30. package/ai-dlc-skill/phases/deliver/entry.md +26 -0
  31. package/ai-dlc-skill/phases/deliver/lifecycle.md +185 -0
  32. package/ai-dlc-skill/phases/deliver/prompt.md +40 -0
  33. package/ai-dlc-skill/phases/deliver/rules.md +61 -0
  34. package/ai-dlc-skill/phases/plan/entry.md +25 -0
  35. package/ai-dlc-skill/phases/plan/lifecycle.md +204 -0
  36. package/ai-dlc-skill/phases/plan/prompt.md +38 -0
  37. package/ai-dlc-skill/phases/plan/rules.md +25 -0
  38. package/ai-dlc-skill/phases/understand/entry.md +26 -0
  39. package/ai-dlc-skill/phases/understand/lifecycle.md +184 -0
  40. package/ai-dlc-skill/phases/understand/prompt.md +41 -0
  41. package/ai-dlc-skill/phases/understand/rules.md +37 -0
  42. package/ai-dlc-skill/phases/verify/entry.md +26 -0
  43. package/ai-dlc-skill/phases/verify/lifecycle.md +192 -0
  44. package/ai-dlc-skill/phases/verify/prompt.md +43 -0
  45. package/ai-dlc-skill/phases/verify/rules.md +73 -0
  46. package/ai-dlc-skill/practices/bdd.md +91 -0
  47. package/ai-dlc-skill/practices/sdd.md +84 -0
  48. package/ai-dlc-skill/practices/tdd.md +95 -0
  49. package/ai-dlc-skill/providers/README.md +114 -0
  50. package/ai-dlc-skill/providers/aliyun/deployment.yaml +66 -0
  51. package/ai-dlc-skill/providers/aliyun/preview.yaml +84 -0
  52. package/ai-dlc-skill/providers/aliyun/provider.yaml +160 -0
  53. package/ai-dlc-skill/providers/tcb/deployment.yaml +69 -0
  54. package/ai-dlc-skill/providers/tcb/preview.yaml +83 -0
  55. package/ai-dlc-skill/providers/tcb/provider.yaml +172 -0
  56. package/ai-dlc-skill/requirements.md +140 -0
  57. package/ai-dlc-skill/skill.yaml +147 -0
  58. package/ai-dlc-skill/templates/integration/CHANGELOG.md +41 -0
  59. package/ai-dlc-skill/templates/integration/asyncapi.yaml +43 -0
  60. package/ai-dlc-skill/templates/integration/contract-diff.md +63 -0
  61. package/ai-dlc-skill/templates/integration/contract-spec.md +49 -0
  62. package/ai-dlc-skill/templates/integration/openapi.yaml +18 -0
  63. package/ai-dlc-skill/templates/integration/runtime-contract.yaml +36 -0
  64. package/ai-dlc-skill/templates/plan/design.md +49 -0
  65. package/ai-dlc-skill/templates/plan/task-list.md +32 -0
  66. package/ai-dlc-skill/templates/project/README.md +127 -0
  67. package/ai-dlc-skill/templates/project/template.md +119 -0
  68. package/ai-dlc-skill/templates/understand/feature.feature +23 -0
  69. package/ai-dlc-skill/templates/understand/intent.md +29 -0
  70. package/ai-dlc-skill/templates/understand/spec-delta.md +47 -0
  71. package/ai-dlc-skill/templates/verify/test-unit.py +27 -0
  72. package/ai-dlc-skill/walkthrough/collect.sh +50 -0
  73. package/ai-dlc-skill/walkthrough/template.md +54 -0
  74. package/ai-dlc-skill/workflows/ai-dlc.yaml +110 -0
  75. package/ai-dlc-skill/workflows/deployment.yaml +199 -0
  76. package/cdh/__init__.py +0 -0
  77. package/cdh/__main__.py +4 -0
  78. package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
  79. package/cdh/__pycache__/__init__.cpython-314.pyc +0 -0
  80. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  81. package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
  82. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  83. package/cdh/cli.py +361 -0
  84. package/cdh/scaffold.py +312 -0
  85. package/cli.js +2 -0
  86. package/onecode/__init__.py +1 -0
  87. package/onecode/__main__.py +4 -0
  88. package/onecode/__pycache__/__init__.cpython-314.pyc +0 -0
  89. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  90. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  91. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  92. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  93. package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
  94. package/onecode/agent/__init__.py +0 -0
  95. package/onecode/agent/__pycache__/__init__.cpython-314.pyc +0 -0
  96. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
  97. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
  98. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  99. package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
  100. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  101. package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
  102. package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
  103. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  104. package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
  105. package/onecode/agent/__pycache__/permissions_store.cpython-314.pyc +0 -0
  106. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  107. package/onecode/agent/agents/__init__.py +29 -0
  108. package/onecode/agent/agents/__pycache__/__init__.cpython-314.pyc +0 -0
  109. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  110. package/onecode/agent/agents/types.py +662 -0
  111. package/onecode/agent/attachments.py +113 -0
  112. package/onecode/agent/cdh_loader.py +262 -0
  113. package/onecode/agent/context.py +338 -0
  114. package/onecode/agent/engine.py +1894 -0
  115. package/onecode/agent/hooks.py +86 -0
  116. package/onecode/agent/onecode_agent_acp.py +2203 -0
  117. package/onecode/agent/permissions.py +151 -0
  118. package/onecode/agent/permissions_store.py +62 -0
  119. package/onecode/agent/session.py +186 -0
  120. package/onecode/agent/snapshot.py +145 -0
  121. package/onecode/agent/tools/__init__.py +17 -0
  122. package/onecode/agent/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  123. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  124. package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
  125. package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
  126. package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
  127. package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
  128. package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
  129. package/onecode/agent/tools/__pycache__/errors.cpython-314.pyc +0 -0
  130. package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
  131. package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
  132. package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
  133. package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
  134. package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
  135. package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
  136. package/onecode/agent/tools/__pycache__/permissions.cpython-314.pyc +0 -0
  137. package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
  138. package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
  139. package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
  140. package/onecode/agent/tools/__pycache__/schema_validation.cpython-314.pyc +0 -0
  141. package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
  142. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  143. package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
  144. package/onecode/agent/tools/agent_tools.py +134 -0
  145. package/onecode/agent/tools/apply_patch_tool.py +144 -0
  146. package/onecode/agent/tools/bash_tool.py +91 -0
  147. package/onecode/agent/tools/communication_tools.py +149 -0
  148. package/onecode/agent/tools/config_tool.py +130 -0
  149. package/onecode/agent/tools/cron_tools.py +179 -0
  150. package/onecode/agent/tools/errors.py +17 -0
  151. package/onecode/agent/tools/file_ops.py +187 -0
  152. package/onecode/agent/tools/file_tools.py +226 -0
  153. package/onecode/agent/tools/git_tools.py +88 -0
  154. package/onecode/agent/tools/lsp_tools.py +404 -0
  155. package/onecode/agent/tools/mcp_tools.py +129 -0
  156. package/onecode/agent/tools/permission_handler.py +46 -0
  157. package/onecode/agent/tools/permissions.py +77 -0
  158. package/onecode/agent/tools/protocol.py +20 -0
  159. package/onecode/agent/tools/registry.py +134 -0
  160. package/onecode/agent/tools/sandbox.py +264 -0
  161. package/onecode/agent/tools/schema_validation.py +100 -0
  162. package/onecode/agent/tools/skill_tools.py +52 -0
  163. package/onecode/agent/tools/task_tools.py +249 -0
  164. package/onecode/agent/tools/web_tools.py +170 -0
  165. package/onecode/builtin_skills/git/SKILL.md +64 -0
  166. package/onecode/builtin_skills/git/skill.yaml +7 -0
  167. package/onecode/builtin_skills/shell/SKILL.md +58 -0
  168. package/onecode/builtin_skills/shell/skill.yaml +8 -0
  169. package/onecode/cli.py +755 -0
  170. package/onecode/cloud/__init__.py +0 -0
  171. package/onecode/cloud/base.py +20 -0
  172. package/onecode/codebase/__init__.py +69 -0
  173. package/onecode/codebase/__pycache__/__init__.cpython-314.pyc +0 -0
  174. package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
  175. package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
  176. package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
  177. package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
  178. package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
  179. package/onecode/codebase/__pycache__/tools.cpython-314.pyc +0 -0
  180. package/onecode/codebase/chunker.py +130 -0
  181. package/onecode/codebase/indexer.py +136 -0
  182. package/onecode/codebase/retriever.py +192 -0
  183. package/onecode/codebase/storage.py +130 -0
  184. package/onecode/codebase/tools.py +59 -0
  185. package/onecode/commands.py +276 -0
  186. package/onecode/config.py +285 -0
  187. package/onecode/config_screen.py +594 -0
  188. package/onecode/cron/__init__.py +3 -0
  189. package/onecode/lsp/__init__.py +3 -0
  190. package/onecode/mcp/__init__.py +4 -0
  191. package/onecode/mcp/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/onecode/mcp/__pycache__/client.cpython-314.pyc +0 -0
  193. package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
  194. package/onecode/mcp/client.py +193 -0
  195. package/onecode/mcp/manager.py +191 -0
  196. package/onecode/memory/__init__.py +95 -0
  197. package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
  199. package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
  200. package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
  201. package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
  202. package/onecode/memory/__pycache__/symbolic.cpython-314.pyc +0 -0
  203. package/onecode/memory/backend.py +185 -0
  204. package/onecode/memory/offload.py +114 -0
  205. package/onecode/memory/pyramid.py +202 -0
  206. package/onecode/memory/recall.py +168 -0
  207. package/onecode/memory/symbolic.py +164 -0
  208. package/onecode/models/__init__.py +4 -0
  209. package/onecode/models/__pycache__/__init__.cpython-314.pyc +0 -0
  210. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  211. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  212. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  213. package/onecode/models/__pycache__/registry.cpython-314.pyc +0 -0
  214. package/onecode/models/errors.py +94 -0
  215. package/onecode/models/messages.py +662 -0
  216. package/onecode/models/provider.py +450 -0
  217. package/onecode/models/providers/__init__.py +17 -0
  218. package/onecode/models/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  219. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  220. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  221. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  222. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  223. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  224. package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
  225. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  226. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  227. package/onecode/models/providers/anthropic_provider.py +224 -0
  228. package/onecode/models/providers/deepseek_provider.py +210 -0
  229. package/onecode/models/providers/glm_provider.py +154 -0
  230. package/onecode/models/providers/minimax_provider.py +154 -0
  231. package/onecode/models/providers/minimaxi_provider.py +363 -0
  232. package/onecode/models/providers/ollama_provider.py +96 -0
  233. package/onecode/models/providers/openai_provider.py +233 -0
  234. package/onecode/models/registry.py +212 -0
  235. package/onecode/server/__init__.py +3 -0
  236. package/onecode/server/app.py +224 -0
  237. package/onecode/skills/__init__.py +11 -0
  238. package/onecode/skills/__pycache__/__init__.cpython-314.pyc +0 -0
  239. package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
  240. package/onecode/skills/__pycache__/create.cpython-314.pyc +0 -0
  241. package/onecode/skills/__pycache__/frontmatter.cpython-314.pyc +0 -0
  242. package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
  243. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  244. package/onecode/skills/__pycache__/model.cpython-314.pyc +0 -0
  245. package/onecode/skills/argument_substitution.py +39 -0
  246. package/onecode/skills/create.py +56 -0
  247. package/onecode/skills/frontmatter.py +33 -0
  248. package/onecode/skills/loader.py +157 -0
  249. package/onecode/skills/manager.py +75 -0
  250. package/onecode/skills/model.py +43 -0
  251. package/onecode/storage/__init__.py +0 -0
  252. package/onecode/storage/activity.py +64 -0
  253. package/onecode/storage/project.py +54 -0
  254. package/onecode/storage/session.py +113 -0
  255. package/onecode/tasks/__init__.py +26 -0
  256. package/onecode/tasks/manager.py +219 -0
  257. package/onecode/tasks/models.py +240 -0
  258. package/onecode/trace/__init__.py +0 -0
  259. package/onecode/trace/tracer.py +174 -0
  260. package/onecode/utils/__init__.py +0 -0
  261. package/onecode/utils/helpers.py +20 -0
  262. package/onecode-cli.js +2 -0
  263. package/package.json +32 -0
  264. package/pyproject.toml +60 -0
  265. package/run.js +86 -0
  266. package/tui/__init__.py +46 -0
  267. package/tui/__main__.py +4 -0
  268. package/tui/__pycache__/__init__.cpython-314.pyc +0 -0
  269. package/tui/__pycache__/_loop.cpython-314.pyc +0 -0
  270. package/tui/__pycache__/_path_fuzzy_search.cpython-314.pyc +0 -0
  271. package/tui/__pycache__/_path_match.cpython-314.pyc +0 -0
  272. package/tui/__pycache__/agent.cpython-314.pyc +0 -0
  273. package/tui/__pycache__/agent_schema.cpython-314.pyc +0 -0
  274. package/tui/__pycache__/agents.cpython-314.pyc +0 -0
  275. package/tui/__pycache__/answer.cpython-314.pyc +0 -0
  276. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  277. package/tui/__pycache__/atomic.cpython-314.pyc +0 -0
  278. package/tui/__pycache__/complete.cpython-314.pyc +0 -0
  279. package/tui/__pycache__/constants.cpython-314.pyc +0 -0
  280. package/tui/__pycache__/conversation_markdown.cpython-314.pyc +0 -0
  281. package/tui/__pycache__/danger.cpython-314.pyc +0 -0
  282. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  283. package/tui/__pycache__/dec.cpython-314.pyc +0 -0
  284. package/tui/__pycache__/directory.cpython-314.pyc +0 -0
  285. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  286. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  287. package/tui/__pycache__/format_path.cpython-314.pyc +0 -0
  288. package/tui/__pycache__/fuzzy.cpython-314.pyc +0 -0
  289. package/tui/__pycache__/fuzzy_index.cpython-314.pyc +0 -0
  290. package/tui/__pycache__/history.cpython-314.pyc +0 -0
  291. package/tui/__pycache__/jsonrpc.cpython-314.pyc +0 -0
  292. package/tui/__pycache__/menus.cpython-314.pyc +0 -0
  293. package/tui/__pycache__/message_log.cpython-314.pyc +0 -0
  294. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  295. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  296. package/tui/__pycache__/path_filter.cpython-314.pyc +0 -0
  297. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  298. package/tui/__pycache__/pill.cpython-314.pyc +0 -0
  299. package/tui/__pycache__/platform_commands.cpython-314.pyc +0 -0
  300. package/tui/__pycache__/protocol.cpython-314.pyc +0 -0
  301. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  302. package/tui/__pycache__/settings.cpython-314.pyc +0 -0
  303. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  304. package/tui/__pycache__/shell.cpython-314.pyc +0 -0
  305. package/tui/__pycache__/shell_read.cpython-314.pyc +0 -0
  306. package/tui/__pycache__/slash_command.cpython-314.pyc +0 -0
  307. package/tui/__pycache__/version.cpython-314.pyc +0 -0
  308. package/tui/_loop.py +86 -0
  309. package/tui/_path_fuzzy_search.py +48 -0
  310. package/tui/_path_match.py +141 -0
  311. package/tui/about.py +95 -0
  312. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  313. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  314. package/tui/acp/__pycache__/encode_tool_call_id.cpython-314.pyc +0 -0
  315. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  316. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  317. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  318. package/tui/acp/agent.py +886 -0
  319. package/tui/acp/api.py +55 -0
  320. package/tui/acp/encode_tool_call_id.py +12 -0
  321. package/tui/acp/messages.py +190 -0
  322. package/tui/acp/prompt.py +110 -0
  323. package/tui/acp/protocol.py +462 -0
  324. package/tui/agent.py +70 -0
  325. package/tui/agent_schema.py +76 -0
  326. package/tui/agents.py +45 -0
  327. package/tui/ansi/__init__.py +1 -0
  328. package/tui/ansi/__pycache__/__init__.cpython-314.pyc +0 -0
  329. package/tui/ansi/__pycache__/_ansi.cpython-314.pyc +0 -0
  330. package/tui/ansi/__pycache__/_ansi_colors.cpython-314.pyc +0 -0
  331. package/tui/ansi/__pycache__/_control_codes.cpython-314.pyc +0 -0
  332. package/tui/ansi/__pycache__/_keys.cpython-314.pyc +0 -0
  333. package/tui/ansi/__pycache__/_sgr_styles.cpython-314.pyc +0 -0
  334. package/tui/ansi/__pycache__/_stream_parser.cpython-314.pyc +0 -0
  335. package/tui/ansi/_ansi.py +1689 -0
  336. package/tui/ansi/_ansi_colors.py +264 -0
  337. package/tui/ansi/_control_codes.py +37 -0
  338. package/tui/ansi/_keys.py +251 -0
  339. package/tui/ansi/_sgr_styles.py +64 -0
  340. package/tui/ansi/_stream_parser.py +418 -0
  341. package/tui/answer.py +12 -0
  342. package/tui/app.py +1236 -0
  343. package/tui/atomic.py +37 -0
  344. package/tui/cli.py +313 -0
  345. package/tui/code_analyze.py +27 -0
  346. package/tui/complete.py +38 -0
  347. package/tui/constants.py +61 -0
  348. package/tui/conversation_markdown.py +30 -0
  349. package/tui/danger.py +372 -0
  350. package/tui/data/agents/ampcode.com.toml +51 -0
  351. package/tui/data/agents/augmentcode.com.toml +40 -0
  352. package/tui/data/agents/cdh.cloud-dev-harness.toml +22 -0
  353. package/tui/data/agents/claude.com.toml +41 -0
  354. package/tui/data/agents/copilot.github.com.toml +29 -0
  355. package/tui/data/agents/crowai.dev.toml +35 -0
  356. package/tui/data/agents/cursor.com.toml +38 -0
  357. package/tui/data/agents/deepagents.langchain.com +33 -0
  358. package/tui/data/agents/docker.com.toml +59 -0
  359. package/tui/data/agents/geminicli.com.toml +28 -0
  360. package/tui/data/agents/goose.ai.toml +51 -0
  361. package/tui/data/agents/inference.huggingface.co.toml +41 -0
  362. package/tui/data/agents/kimi.com.toml +35 -0
  363. package/tui/data/agents/kiro.dev.toml +42 -0
  364. package/tui/data/agents/openai.com.toml +53 -0
  365. package/tui/data/agents/openclaw.ai.toml +37 -0
  366. package/tui/data/agents/opencode.ai.toml +39 -0
  367. package/tui/data/agents/openhands.dev.toml +45 -0
  368. package/tui/data/agents/stakpak.dev.toml +61 -0
  369. package/tui/data/agents/vibe.mistral.ai.toml +30 -0
  370. package/tui/data/agents/vtcode.dev.toml +62 -0
  371. package/tui/data/images/frog.png +0 -0
  372. package/tui/data/sounds/question.wav +0 -0
  373. package/tui/data/sounds/turn-over.wav +0 -0
  374. package/tui/db.py +190 -0
  375. package/tui/dec.py +332 -0
  376. package/tui/directory.py +234 -0
  377. package/tui/directory_suggester.py +93 -0
  378. package/tui/directory_watcher.py +197 -0
  379. package/tui/format_path.py +25 -0
  380. package/tui/fuzzy.py +140 -0
  381. package/tui/fuzzy_index.py +275 -0
  382. package/tui/gist.py +33 -0
  383. package/tui/history.py +138 -0
  384. package/tui/jsonrpc.py +578 -0
  385. package/tui/menus.py +14 -0
  386. package/tui/message_log.py +69 -0
  387. package/tui/messages.py +139 -0
  388. package/tui/option_content.py +51 -0
  389. package/tui/os.py +0 -0
  390. package/tui/path_complete.py +217 -0
  391. package/tui/path_filter.py +124 -0
  392. package/tui/paths.py +71 -0
  393. package/tui/pill.py +23 -0
  394. package/tui/platform_commands.py +52 -0
  395. package/tui/prompt/__pycache__/extract.cpython-314.pyc +0 -0
  396. package/tui/prompt/__pycache__/resource.cpython-314.pyc +0 -0
  397. package/tui/prompt/extract.py +19 -0
  398. package/tui/prompt/resource.py +68 -0
  399. package/tui/protocol.py +28 -0
  400. package/tui/screens/__init__.py +0 -0
  401. package/tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
  402. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  403. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  404. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  405. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  406. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  407. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  408. package/tui/screens/action_modal.py +94 -0
  409. package/tui/screens/agent_modal.py +175 -0
  410. package/tui/screens/command_edit_modal.py +74 -0
  411. package/tui/screens/log.py +553 -0
  412. package/tui/screens/main.py +411 -0
  413. package/tui/screens/main.tcss +655 -0
  414. package/tui/screens/permissions.py +404 -0
  415. package/tui/screens/permissions.tcss +72 -0
  416. package/tui/screens/projects.tcss +66 -0
  417. package/tui/screens/projects_app.py +319 -0
  418. package/tui/screens/projects_screen.py +142 -0
  419. package/tui/screens/session_resume_modal.py +172 -0
  420. package/tui/screens/session_resume_modal.tcss +35 -0
  421. package/tui/screens/sessions.py +166 -0
  422. package/tui/screens/sessions.tcss +119 -0
  423. package/tui/screens/settings.py +254 -0
  424. package/tui/screens/settings.tcss +101 -0
  425. package/tui/screens/store.py +500 -0
  426. package/tui/screens/store.tcss +274 -0
  427. package/tui/screens/tool_content_screen.py +59 -0
  428. package/tui/session_tracker.py +120 -0
  429. package/tui/settings.py +353 -0
  430. package/tui/settings_schema.py +431 -0
  431. package/tui/shell.py +304 -0
  432. package/tui/shell_read.py +42 -0
  433. package/tui/slash_command.py +34 -0
  434. package/tui/tui.tcss +246 -0
  435. package/tui/twitter.py +38 -0
  436. package/tui/version.py +80 -0
  437. package/tui/visuals/__init__.py +0 -0
  438. package/tui/visuals/__pycache__/__init__.cpython-314.pyc +0 -0
  439. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  440. package/tui/visuals/columns.py +273 -0
  441. package/tui/widgets/__init__.py +0 -0
  442. package/tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
  443. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  444. package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
  445. package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
  446. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  447. package/tui/widgets/__pycache__/condensed_path.cpython-314.pyc +0 -0
  448. package/tui/widgets/__pycache__/context_stats.cpython-314.pyc +0 -0
  449. package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
  450. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  451. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  452. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  453. package/tui/widgets/__pycache__/flash.cpython-314.pyc +0 -0
  454. package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
  455. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  456. package/tui/widgets/__pycache__/highlighted_textarea.cpython-314.pyc +0 -0
  457. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  458. package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
  459. package/tui/widgets/__pycache__/menu.cpython-314.pyc +0 -0
  460. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  461. package/tui/widgets/__pycache__/non_selectable_label.cpython-314.pyc +0 -0
  462. package/tui/widgets/__pycache__/note.cpython-314.pyc +0 -0
  463. package/tui/widgets/__pycache__/path_search.cpython-314.pyc +0 -0
  464. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  465. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  466. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  467. package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
  468. package/tui/widgets/__pycache__/prompt.cpython-314.pyc +0 -0
  469. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  470. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  471. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  472. package/tui/widgets/__pycache__/session_tabs.cpython-314.pyc +0 -0
  473. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  474. package/tui/widgets/__pycache__/shell_terminal.cpython-314.pyc +0 -0
  475. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  476. package/tui/widgets/__pycache__/slash_complete.cpython-314.pyc +0 -0
  477. package/tui/widgets/__pycache__/strike_text.cpython-314.pyc +0 -0
  478. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  479. package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
  480. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  481. package/tui/widgets/__pycache__/throbber.cpython-314.pyc +0 -0
  482. package/tui/widgets/__pycache__/tool_call.cpython-313.pyc +0 -0
  483. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  484. package/tui/widgets/__pycache__/user_input.cpython-314.pyc +0 -0
  485. package/tui/widgets/acp_content.py +70 -0
  486. package/tui/widgets/agent_response.py +77 -0
  487. package/tui/widgets/agent_thought.py +168 -0
  488. package/tui/widgets/ask_user.py +264 -0
  489. package/tui/widgets/command_pane.py +223 -0
  490. package/tui/widgets/condensed_path.py +115 -0
  491. package/tui/widgets/context_stats.py +66 -0
  492. package/tui/widgets/conversation.py +2441 -0
  493. package/tui/widgets/danger_warning.py +65 -0
  494. package/tui/widgets/diff_view.py +59 -0
  495. package/tui/widgets/directory_input.py +8 -0
  496. package/tui/widgets/flash.py +81 -0
  497. package/tui/widgets/future_text.py +126 -0
  498. package/tui/widgets/grid_select.py +234 -0
  499. package/tui/widgets/highlighted_textarea.py +180 -0
  500. package/tui/widgets/mandelbrot.py +294 -0
  501. package/tui/widgets/markdown_note.py +13 -0
  502. package/tui/widgets/menu.py +147 -0
  503. package/tui/widgets/modified_files.py +106 -0
  504. package/tui/widgets/non_selectable_label.py +5 -0
  505. package/tui/widgets/note.py +18 -0
  506. package/tui/widgets/path_search.py +492 -0
  507. package/tui/widgets/plan.py +263 -0
  508. package/tui/widgets/project_directory_tree.py +90 -0
  509. package/tui/widgets/project_grid_select.py +35 -0
  510. package/tui/widgets/project_summary.py +33 -0
  511. package/tui/widgets/prompt.py +795 -0
  512. package/tui/widgets/question.py +360 -0
  513. package/tui/widgets/session_grid_select.py +224 -0
  514. package/tui/widgets/session_summary.py +115 -0
  515. package/tui/widgets/session_tabs.py +188 -0
  516. package/tui/widgets/shell_result.py +35 -0
  517. package/tui/widgets/shell_terminal.py +40 -0
  518. package/tui/widgets/side_bar.py +114 -0
  519. package/tui/widgets/slash_complete.py +235 -0
  520. package/tui/widgets/strike_text.py +70 -0
  521. package/tui/widgets/subagent.py +87 -0
  522. package/tui/widgets/terminal.py +577 -0
  523. package/tui/widgets/terminal_tool.py +351 -0
  524. package/tui/widgets/throbber.py +100 -0
  525. package/tui/widgets/tool_call.py +368 -0
  526. package/tui/widgets/user_input.py +23 -0
  527. package/tui/widgets/version.py +5 -0
  528. package/tui/widgets/welcome.py +31 -0
@@ -0,0 +1,886 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+
5
+ logger = logging.getLogger("tui.acp.agent")
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any, cast, NamedTuple
11
+ from copy import deepcopy
12
+
13
+ import rich.repr
14
+
15
+ from textual.content import Content
16
+ from textual.message import Message
17
+ from textual.message_pump import MessagePump
18
+
19
+
20
+ from tui import jsonrpc
21
+ import tui
22
+ from tui.agent_schema import Agent as AgentData
23
+ from tui.agent import AgentBase, AgentReady, AgentFail
24
+ from tui.acp import protocol
25
+ from tui.acp import api
26
+ from tui.acp.api import API
27
+ from tui.acp import messages
28
+ from tui.acp.prompt import build as build_prompt
29
+ from tui import messages as tui_messages
30
+ from tui.db import DB
31
+ from tui import paths
32
+ from tui import constants
33
+ from tui.answer import Answer
34
+
35
+ PROTOCOL_VERSION = 1
36
+
37
+
38
+ class Mode(NamedTuple):
39
+ """An agent mode."""
40
+
41
+ id: str
42
+ name: str
43
+ description: str | None
44
+
45
+
46
+ @rich.repr.auto
47
+ class Agent(AgentBase):
48
+ """An agent that speaks the APC (https://agentclientprotocol.com/overview/introduction) protocol."""
49
+
50
+ def __init__(
51
+ self,
52
+ project_root: Path,
53
+ agent: AgentData,
54
+ session_id: str | None,
55
+ session_pk: int | None = None,
56
+ ) -> None:
57
+ """
58
+
59
+ Args:
60
+ project_root: Project root path.
61
+ command: Command to launch agent.
62
+ """
63
+ super().__init__(project_root)
64
+
65
+ self._agent_data = agent
66
+ self.session_id = session_id
67
+
68
+ self.server = jsonrpc.Server()
69
+ self.server.expose_instance(self)
70
+
71
+ self._agent_task: asyncio.Task | None = None
72
+ self._task: asyncio.Task | None = None
73
+ self._process: asyncio.subprocess.Process | None = None
74
+ self.done_event = asyncio.Event()
75
+
76
+ self.agent_capabilities: protocol.AgentCapabilities = {
77
+ "loadSession": False,
78
+ "promptCapabilities": {
79
+ "audio": False,
80
+ "embeddedContent": True,
81
+ "image": True,
82
+ },
83
+ }
84
+ self.auth_methods: list[protocol.AuthMethod] = []
85
+ self.session_pk: int | None = session_pk
86
+ self._pending_session_data: dict | None = None
87
+ self.tool_calls: dict[str, protocol.ToolCall] = {}
88
+ self._message_target: MessagePump | None = None
89
+
90
+ self._terminal_count: int = 0
91
+
92
+ @property
93
+ def command(self) -> str | None:
94
+ """The command used to launch the agent, or `None` if there isn't one."""
95
+ acp_command = tui.get_os_matrix(self._agent_data["run_command"])
96
+ return acp_command
97
+
98
+ @property
99
+ def supports_load_session(self) -> bool:
100
+ """Does the agent support loading sessions?"""
101
+ return self.agent_capabilities.get("loadSession", False)
102
+
103
+ def __rich_repr__(self) -> rich.repr.Result:
104
+ yield self.project_root_path
105
+ yield self.command
106
+
107
+ def get_info(self) -> Content:
108
+ agent_name = self._agent_data["name"]
109
+ return Content(agent_name)
110
+
111
+ async def start(self, message_target: MessagePump | None = None) -> None:
112
+ """Start the agent."""
113
+ self._message_target = message_target
114
+ self._agent_task = asyncio.create_task(self._run_agent())
115
+
116
+ def send(self, request: jsonrpc.Request) -> None:
117
+ """Send a request to the agent.
118
+
119
+ This is called automatically, if you go through `self.request`.
120
+
121
+ Args:
122
+ request: JSONRPC request object.
123
+
124
+ """
125
+ assert self._process is not None, "Process should be present here"
126
+
127
+ logger.debug("[client] %s", request.body)
128
+ if (stdin := self._process.stdin) is not None:
129
+ stdin.write(b"%s\n" % request.body_json)
130
+
131
+ def request(self) -> jsonrpc.Request:
132
+ """Create a request object."""
133
+ return API.request(self.send)
134
+
135
+ def post_message(self, message: Message) -> bool:
136
+ """Post a message to the message target (the Conversation).
137
+
138
+ Args:
139
+ message: Message object.
140
+
141
+ Returns:
142
+ `True` if the message was posted successfully, or `False` if it wasn't.
143
+ """
144
+ if (message_target := self._message_target) is None:
145
+ return False
146
+ return message_target.post_message(message)
147
+
148
+ @jsonrpc.expose("session/update")
149
+ def rpc_session_update(
150
+ self,
151
+ sessionId: str,
152
+ update: protocol.SessionUpdate,
153
+ _meta: dict[str, Any] | None = None,
154
+ ):
155
+ """Agent requests an update.
156
+
157
+ https://agentclientprotocol.com/protocol/schema
158
+ """
159
+ status_line: str | None = None
160
+ if _meta and (field_meta := _meta.get("field_meta")) is not None:
161
+ if (
162
+ open_hands_metrics := field_meta.get("openhands.dev/metrics")
163
+ ) is not None:
164
+ status_line = open_hands_metrics.get("status_line")
165
+
166
+ match update:
167
+ case {
168
+ "sessionUpdate": "user_message_chunk",
169
+ "content": {"type": type, "text": text},
170
+ }:
171
+ if text:
172
+ self.post_message(messages.UserMessage(type, text))
173
+
174
+ case {
175
+ "sessionUpdate": "agent_message_chunk",
176
+ "content": {"type": type, "text": text},
177
+ }:
178
+ self.post_message(messages.Update(type, text))
179
+
180
+ case {
181
+ "sessionUpdate": "agent_thought_chunk",
182
+ "content": {"type": type, "text": text},
183
+ }:
184
+ self.post_message(messages.Thinking(type, text))
185
+
186
+ case {
187
+ "sessionUpdate": "tool_call",
188
+ "toolCallId": tool_call_id,
189
+ }:
190
+ logger.debug(
191
+ "session/update: tool_call id=%s title=%s kind=%s status=%s content_blocks=%d",
192
+ tool_call_id,
193
+ update.get('title','?'), update.get('kind','?'),
194
+ update.get('status','?'), len(update.get('content',[]) or []),
195
+ )
196
+ self.tool_calls[tool_call_id] = deepcopy(update)
197
+ self.post_message(messages.ToolCall(deepcopy(update)))
198
+
199
+ case {"sessionUpdate": "plan", "entries": entries}:
200
+ self.post_message(messages.Plan(entries))
201
+
202
+ case {
203
+ "sessionUpdate": "tool_call_update",
204
+ "toolCallId": tool_call_id,
205
+ }:
206
+ if tool_call_id in self.tool_calls:
207
+ current_tool_call = self.tool_calls[tool_call_id]
208
+ for key, value in update.items():
209
+ if value is not None:
210
+ current_tool_call[key] = value
211
+
212
+ logger.debug(
213
+ "session/update: tool_call_update id=%s status=%s content_blocks=%d",
214
+ tool_call_id,
215
+ update.get('status','?'),
216
+ len(update.get('content',[]) or []),
217
+ )
218
+
219
+ self.post_message(
220
+ messages.ToolCallUpdate(deepcopy(current_tool_call), update)
221
+ )
222
+ else:
223
+ logger.debug(
224
+ "session/update: tool_call_update ORPHAN id=%s "
225
+ "(no prior tool_call, creating widget from update)",
226
+ tool_call_id,
227
+ )
228
+ current_tool_call: protocol.ToolCall = {
229
+ "sessionUpdate": "tool_call",
230
+ "toolCallId": tool_call_id,
231
+ "title": "Tool call",
232
+ }
233
+ for key, value in update.items():
234
+ if value is not None:
235
+ current_tool_call[key] = value
236
+
237
+ self.tool_calls[tool_call_id] = current_tool_call
238
+ self.post_message(messages.ToolCall(deepcopy(current_tool_call)))
239
+
240
+ case {
241
+ "sessionUpdate": "usage_update",
242
+ }:
243
+ self.post_message(messages.ContextUpdate(
244
+ used=update.get("used", 0),
245
+ size=update.get("size", 0),
246
+ ))
247
+
248
+ case {
249
+ "sessionUpdate": "ask_user",
250
+ "question": question,
251
+ }:
252
+ self.post_message(messages.AskUser(
253
+ question=question,
254
+ context=update.get("context", ""),
255
+ options=update.get("options", []),
256
+ questions=update.get("questions", []),
257
+ tool_id=update.get("toolId", ""),
258
+ ))
259
+
260
+ case {
261
+ "sessionUpdate": "ask_user",
262
+ "questions": questions,
263
+ } if questions:
264
+ self.post_message(messages.AskUser(
265
+ questions=questions,
266
+ context=update.get("context", ""),
267
+ tool_id=update.get("toolId", ""),
268
+ ))
269
+
270
+ case {
271
+ "sessionUpdate": "available_commands_update",
272
+ "availableCommands": available_commands,
273
+ }:
274
+ self.post_message(messages.AvailableCommandsUpdate(available_commands))
275
+
276
+ case {
277
+ "sessionUpdate": "subagent_start",
278
+ "subagentId": subagent_id,
279
+ "agentType": agent_type,
280
+ }:
281
+ self.post_message(messages.SubAgentStart(subagent_id, agent_type))
282
+
283
+ case {
284
+ "sessionUpdate": "subagent_chunk",
285
+ "subagentId": subagent_id,
286
+ "text": text,
287
+ }:
288
+ self.post_message(messages.SubAgentChunk(subagent_id, text))
289
+
290
+ case {
291
+ "sessionUpdate": "subagent_end",
292
+ "subagentId": subagent_id,
293
+ "agentType": agent_type,
294
+ }:
295
+ self.post_message(messages.SubAgentEnd(subagent_id, agent_type))
296
+
297
+ if status_line is not None:
298
+ self.post_message(messages.UpdateStatusLine(status_line))
299
+
300
+ @jsonrpc.expose("session/request_permission")
301
+ async def rpc_request_permission(
302
+ self,
303
+ sessionId: str,
304
+ options: list[protocol.PermissionOption],
305
+ toolCall: protocol.ToolCallUpdatePermissionRequest,
306
+ _meta: dict | None = None,
307
+ ) -> protocol.RequestPermissionResponse:
308
+ """Agent requests permission to make a tool call.
309
+
310
+ Args:
311
+ sessionId: The session ID.
312
+ options: A list of permission options (potential replies).
313
+ toolCall: The tool or tools the agent is requesting permission to call.
314
+ _meta: Optional meta information.
315
+
316
+ Returns:
317
+ The response to the permission request.
318
+ """
319
+ result_future: asyncio.Future[Answer] = asyncio.Future()
320
+ tool_call_id = toolCall["toolCallId"]
321
+
322
+ permission_tool_call = toolCall.copy()
323
+ permission_tool_call.pop("sessionUpdate", None)
324
+ tool_call = cast(protocol.ToolCall, permission_tool_call)
325
+ if tool_call_id in self.tool_calls:
326
+ self.tool_calls[tool_call_id] |= tool_call
327
+ else:
328
+ self.tool_calls[tool_call_id] = deepcopy(tool_call)
329
+
330
+ # Forward content to the ToolCall widget so the user can see what
331
+ # the agent is requesting permission for (e.g. the bash command).
332
+ merged = deepcopy(self.tool_calls[tool_call_id])
333
+ self.post_message(messages.ToolCallUpdate(merged, tool_call))
334
+
335
+ message = messages.RequestPermission(options, merged, result_future)
336
+ self.post_message(message)
337
+ await result_future
338
+ ask_result = result_future.result()
339
+
340
+ request_permission_outcome: protocol.OutcomeSelected = {
341
+ "optionId": ask_result.id,
342
+ "outcome": "selected",
343
+ }
344
+ result: protocol.RequestPermissionResponse = {
345
+ "outcome": request_permission_outcome
346
+ }
347
+ return result
348
+
349
+ def send_ask_user_answer(self, answer: str, cancelled: bool) -> None:
350
+ """Send the user's answer back to the CDHA agent."""
351
+ import uuid
352
+
353
+ request = {
354
+ "jsonrpc": "2.0",
355
+ "method": "session/ask_user_answer",
356
+ "params": {"answer": answer, "cancelled": cancelled},
357
+ "id": str(uuid.uuid4()),
358
+ }
359
+ if self._process is not None and self._process.stdin is not None:
360
+ self._process.stdin.write(b"%s\n" % json.dumps(request).encode("utf-8"))
361
+
362
+ @jsonrpc.expose("fs/read_text_file")
363
+ def rpc_read_text_file(
364
+ self,
365
+ sessionId: str,
366
+ path: str,
367
+ line: int | None = None,
368
+ limit: int | None = None,
369
+ ) -> dict[str, str]:
370
+ """Read a file in the project."""
371
+ # TODO: what if the read is outside of the project path?
372
+ # https://agentclientprotocol.com/protocol/file-system#reading-files
373
+ read_path = self.project_root_path / path
374
+ try:
375
+ text = read_path.read_text(encoding="utf-8", errors="ignore")
376
+ except IOError:
377
+ text = ""
378
+ if line is not None:
379
+ line = max(0, line - 1)
380
+ if limit is None:
381
+ text = "\n".join(text.splitlines()[line:])
382
+ else:
383
+ text = "\n".join(text.splitlines()[line : line + limit])
384
+ return {"content": text}
385
+
386
+ @jsonrpc.expose("fs/write_text_file")
387
+ def rpc_write_text_file(self, sessionId: str, path: str, content: str) -> None:
388
+ # TODO: What if the agent wants to write outside of the project path?
389
+ # https://agentclientprotocol.com/protocol/file-system#writing-files
390
+
391
+ write_path = self.project_root_path / path
392
+ write_path.write_text(content, encoding="utf-8", errors="ignore")
393
+
394
+ # https://agentclientprotocol.com/protocol/schema#createterminalrequest
395
+ @jsonrpc.expose("terminal/create")
396
+ async def rpc_terminal_create(
397
+ self,
398
+ command: str,
399
+ _meta: dict | None = None,
400
+ args: list[str] | None = None,
401
+ cwd: str | None = None,
402
+ env: list[protocol.EnvVariable] | None = None,
403
+ outputByteLimit: int | None = None,
404
+ sessionId: str | None = None,
405
+ ) -> protocol.CreateTerminalResponse:
406
+ # Assign a terminal id
407
+ self._terminal_count = self._terminal_count + 1
408
+ terminal_id = f"terminal-{self._terminal_count}"
409
+
410
+ terminal_env = (
411
+ {variable["name"]: variable["value"] for variable in env} if env else {}
412
+ )
413
+ result_future: asyncio.Future[bool] = asyncio.Future()
414
+ self.post_message(
415
+ messages.CreateTerminal(
416
+ terminal_id,
417
+ command=command,
418
+ args=args,
419
+ cwd=cwd,
420
+ env=terminal_env,
421
+ output_byte_limit=outputByteLimit,
422
+ result_future=result_future,
423
+ )
424
+ )
425
+ await result_future
426
+ if not result_future.result():
427
+ raise jsonrpc.JSONRPCError("Failed to create a terminal.")
428
+ return {"terminalId": terminal_id}
429
+
430
+ # https://agentclientprotocol.com/protocol/schema#killterminalcommandrequest
431
+ @jsonrpc.expose("terminal/kill")
432
+ def rpc_terminal_kill(
433
+ self, sessionID: str, terminalId: str, _meta: dict | None = None
434
+ ) -> protocol.KillTerminalCommandResponse:
435
+ self.post_message(messages.KillTerminal(terminalId))
436
+ return {}
437
+
438
+ # https://agentclientprotocol.com/protocol/schema#terminal%2Foutput
439
+ @jsonrpc.expose("terminal/output")
440
+ async def rpc_terminal_output(
441
+ self, sessionId: str, terminalId: str, _meta: dict | None = None
442
+ ) -> protocol.TerminalOutputResponse:
443
+ from tui.widgets.terminal_tool import ToolState
444
+
445
+ result_future: asyncio.Future[ToolState] = asyncio.Future()
446
+
447
+ if not self.post_message(messages.GetTerminalState(terminalId, result_future)):
448
+ raise RuntimeError("Unable to get terminal output")
449
+
450
+ await result_future
451
+ terminal_state = result_future.result()
452
+
453
+ result: protocol.TerminalOutputResponse = {
454
+ "output": terminal_state.output,
455
+ "truncated": terminal_state.truncated,
456
+ }
457
+ if (return_code := terminal_state.return_code) is not None:
458
+ result["exitStatus"] = {"exitCode": return_code}
459
+ return result
460
+
461
+ # https://agentclientprotocol.com/protocol/schema#terminal%2Frelease
462
+ @jsonrpc.expose("terminal/release")
463
+ def rpc_terminal_release(
464
+ self, sessionId: str, terminalId: str, _meta: dict | None = None
465
+ ) -> protocol.ReleaseTerminalResponse:
466
+ self.post_message(messages.ReleaseTerminal(terminalId))
467
+ return {}
468
+
469
+ # https://agentclientprotocol.com/protocol/schema#terminal%2Fwait-for-exit
470
+ @jsonrpc.expose("terminal/wait_for_exit")
471
+ async def rpc_terminal_wait_for_exit(
472
+ self, sessionId: str, terminalId: str, _meta: dict | None = None
473
+ ) -> protocol.WaitForTerminalExitResponse:
474
+ result_future: asyncio.Future[tuple[int, str | None]] = asyncio.Future()
475
+ if not self.post_message(
476
+ messages.WaitForTerminalExit(terminalId, result_future)
477
+ ):
478
+ raise RuntimeError("Unable to wait for terminal exit; no terminal found")
479
+
480
+ await result_future
481
+ return_code, signal = result_future.result()
482
+ return {"exitCode": return_code, "signal": signal}
483
+
484
+ async def _run_agent(self) -> None:
485
+ """Task to communicate with the agent subprocess."""
486
+
487
+ PIPE = asyncio.subprocess.PIPE
488
+ env = os.environ.copy()
489
+ env["TOAD_CWD"] = str(self.project_root_path)
490
+ python_bin = str(Path(sys.executable).parent)
491
+ env["PATH"] = f"{python_bin}:{env.get('PATH', '')}"
492
+
493
+ if (command := self.command) is None:
494
+ self.post_message(
495
+ AgentFail("Failed to start agent; no run command for this OS")
496
+ )
497
+ return
498
+ try:
499
+ process = self._process = await asyncio.create_subprocess_shell(
500
+ command,
501
+ stdin=PIPE,
502
+ stdout=PIPE,
503
+ stderr=PIPE,
504
+ env=env,
505
+ cwd=str(self.project_root_path),
506
+ limit=10 * 1024 * 1024,
507
+ )
508
+ except Exception as error:
509
+ self.post_message(AgentFail("Failed to start agent", details=str(error)))
510
+ return
511
+
512
+ self._task = asyncio.create_task(self.run())
513
+
514
+ assert process.stdout is not None
515
+ assert process.stdin is not None
516
+
517
+ tasks: set[asyncio.Task] = set()
518
+
519
+ async def call_jsonrpc(request: jsonrpc.JSONObject | jsonrpc.JSONList) -> None:
520
+ try:
521
+ if (result := await self.server.call(request)) is not None:
522
+ result_json = json.dumps(result).encode("utf-8")
523
+ if process.stdin is not None:
524
+ process.stdin.write(b"%s\n" % result_json)
525
+ finally:
526
+ if (task := asyncio.current_task()) is not None:
527
+ tasks.discard(task)
528
+
529
+ while line := await process.stdout.readline():
530
+ # This line should contain JSON, which may be:
531
+ # A) a JSONRPC request
532
+ # B) a JSONRPC response to a previous request
533
+ if not line.strip():
534
+ continue
535
+
536
+ try:
537
+ line_str = line.decode("utf-8")
538
+ except Exception as error:
539
+ logger.error("Unable to decode utf-8 from agent: %s", error)
540
+ continue
541
+
542
+ logger.debug("[agent] %s", line_str.rstrip())
543
+ try:
544
+ agent_data: jsonrpc.JSONType = json.loads(line_str)
545
+ except Exception as error:
546
+ logger.error("Failed to decode JSON from agent: %s", error)
547
+ continue
548
+
549
+ if isinstance(agent_data, dict):
550
+ if "result" in agent_data or "error" in agent_data:
551
+ # Wait for pending notification dispatch tasks so all ACP
552
+ # messages are queued before the response future resolves.
553
+ if tasks:
554
+ await asyncio.gather(*tasks, return_exceptions=True)
555
+ tasks.clear()
556
+ API.process_response(agent_data)
557
+ continue
558
+
559
+ elif isinstance(agent_data, list):
560
+ if not all(isinstance(datum, dict) for datum in agent_data):
561
+ logger.error("Agent sent invalid data: %r", agent_data)
562
+ continue
563
+
564
+ if not isinstance(agent_data, dict):
565
+ logger.error("Invalid JSON from agent: %r", agent_data)
566
+ continue
567
+
568
+ if not isinstance(agent_data, dict):
569
+ logger.error("Invalid JSON from agent: %r", agent_data)
570
+ continue
571
+
572
+ # By this point we know it is a JSON RPC call
573
+ assert isinstance(agent_data, dict)
574
+ tasks.add(asyncio.create_task(call_jsonrpc(agent_data)))
575
+ await asyncio.sleep(0)
576
+
577
+ # Cancel all remaining tasks and wait for them to finish
578
+ for task in tasks:
579
+ task.cancel()
580
+
581
+ # Wait for all tasks to complete cancellation
582
+ if tasks:
583
+ await asyncio.gather(*tasks, return_exceptions=True)
584
+
585
+ if process.returncode:
586
+ assert process.stderr is not None
587
+ fail_details = (await process.stderr.read()).decode("utf-8", "replace")
588
+ self.post_message(
589
+ AgentFail(
590
+ f"Agent returned a failure code: [b]{process.returncode}",
591
+ details=fail_details,
592
+ )
593
+ )
594
+
595
+ self._process = None
596
+
597
+ async def stop(self) -> None:
598
+ """Gracefully stop the process."""
599
+ if self.session_pk is not None:
600
+ db = DB()
601
+ await db.session_update_last_used(self.session_pk)
602
+
603
+ if self._process is not None:
604
+ try:
605
+ self._process.terminate()
606
+ except OSError:
607
+ pass
608
+
609
+ async def run(self) -> None:
610
+ """The main logic of the Agent."""
611
+ if constants.ACP_INITIALIZE:
612
+ try:
613
+ await self.acp_initialize()
614
+
615
+ if self.session_id is None:
616
+ await self.acp_new_session()
617
+ else:
618
+ if not self.agent_capabilities.get("loadSession", False):
619
+ self.post_message(
620
+ AgentFail(
621
+ "Resume not supported",
622
+ f"{self._agent_data['name']} does not currently support resuming sessions.",
623
+ help="no_resume",
624
+ )
625
+ )
626
+ return
627
+ await self.acp_load_session()
628
+ if self.session_pk is not None:
629
+ db = DB()
630
+ await db.session_update_last_used(self.session_pk)
631
+ except jsonrpc.APIError as error:
632
+ if isinstance(error.data, dict):
633
+ reason = str(
634
+ error.data.get("reason") or "Failed to initialize agent"
635
+ )
636
+ details = str(
637
+ error.data.get("details") or error.data.get("error") or ""
638
+ )
639
+ else:
640
+ reason = "Failed to initialize agent"
641
+ details = ""
642
+ self.post_message(AgentFail(reason, details))
643
+ elif self.session_id is None:
644
+ await self.acp_new_session()
645
+ if self.session_pk is not None:
646
+ db = DB()
647
+ await db.session_update_last_used(self.session_pk)
648
+
649
+ self.post_message(AgentReady())
650
+
651
+ async def _ensure_db_session(self) -> None:
652
+ if self.session_pk is not None:
653
+ return
654
+ if not self.supports_load_session:
655
+ return
656
+ data = getattr(self, "_pending_session_data", None)
657
+ if data is None:
658
+ return
659
+
660
+ db = DB()
661
+ self.session_pk = await db.session_new(
662
+ data["session_name"],
663
+ data["agent_name"],
664
+ data["agent_identity"],
665
+ data["session_id"],
666
+ protocol=data["protocol"],
667
+ meta=data["meta"],
668
+ )
669
+ self._pending_session_data = None
670
+ await self._save_last_session_to_cdh()
671
+
672
+ async def _save_last_session_to_cdh(self) -> None:
673
+ if self.session_pk is None:
674
+ return
675
+ try:
676
+ from onecode.agent.cdh_loader import CdhProjectLoader
677
+
678
+ cdh_dir = CdhProjectLoader.find_cdh_dir(self.project_root_path)
679
+ if cdh_dir is None:
680
+ return
681
+ CdhProjectLoader.save_last_session(
682
+ cdh_dir,
683
+ {
684
+ "agent_session_id": self.session_id,
685
+ "session_pk": self.session_pk,
686
+ "agent_identity": self._agent_data["identity"],
687
+ },
688
+ )
689
+ except Exception:
690
+ pass
691
+
692
+ async def send_prompt(self, prompt: str) -> str | None:
693
+ """Send a prompt to the agent.
694
+
695
+ !!! note
696
+ This method blocks as it may defer to a thread to read resources.
697
+
698
+ Args:
699
+ prompt: Prompt text.
700
+ """
701
+ await self._ensure_db_session()
702
+ prompt_content_blocks = await asyncio.to_thread(
703
+ build_prompt, self.project_root_path, prompt
704
+ )
705
+ return await self.acp_session_prompt(prompt_content_blocks)
706
+
707
+ async def acp_initialize(self):
708
+ """Initialize agent."""
709
+ with self.request():
710
+ initialize_response = api.initialize(
711
+ PROTOCOL_VERSION,
712
+ {
713
+ "fs": {
714
+ "readTextFile": True,
715
+ "writeTextFile": True,
716
+ },
717
+ "terminal": True,
718
+ },
719
+ {
720
+ "name": tui.NAME,
721
+ "title": tui.TITLE,
722
+ "version": tui.get_version(),
723
+ },
724
+ )
725
+
726
+ response = await initialize_response.wait()
727
+ assert response is not None
728
+
729
+ # Store agents capabilities
730
+ if agent_capabilities := response.get("agentCapabilities"):
731
+ self.agent_capabilities = agent_capabilities
732
+ if auth_methods := response.get("authMethods"):
733
+ self.auth_methods = auth_methods
734
+
735
+ async def acp_new_session(self) -> None:
736
+ """Create a new session."""
737
+ with self.request():
738
+ session_new_response = api.session_new(
739
+ str(self.project_root_path),
740
+ [],
741
+ )
742
+ response = await session_new_response.wait()
743
+ assert response is not None
744
+ self.session_id = response["sessionId"]
745
+
746
+ if self.supports_load_session:
747
+ self._pending_session_data = {
748
+ "session_name": "New Session",
749
+ "agent_name": self._agent_data["name"],
750
+ "agent_identity": self._agent_data["identity"],
751
+ "session_id": self.session_id,
752
+ "protocol": "acp",
753
+ "meta": {
754
+ "cwd": str(self.project_root_path),
755
+ "agent_data": self._agent_data,
756
+ },
757
+ }
758
+
759
+ if (modes := response.get("modes", None)) is not None:
760
+ current_mode = modes["currentModeId"]
761
+ available_modes = modes["availableModes"]
762
+ modes_update = {
763
+ mode["id"]: Mode(
764
+ mode["id"], mode["name"], mode.get("description", None)
765
+ )
766
+ for mode in available_modes
767
+ }
768
+ self.post_message(messages.SetModes(current_mode, modes_update))
769
+
770
+ async def acp_load_session(self) -> None:
771
+ assert self.session_id is not None, "Session id must be set"
772
+ cwd = str(self.project_root_path)
773
+ session_title = None
774
+ if self.session_pk is not None:
775
+ db = DB()
776
+ if (session := await db.session_get(self.session_pk)) is not None:
777
+ session_title = session.get("title") or "Untitled"
778
+ if session["meta_json"]:
779
+ meta = json.loads(session["meta_json"])
780
+ if session_cwd := meta.get("cwd", None):
781
+ cwd = session_cwd
782
+ if agent_data := meta.get("agent_data"):
783
+ self._agent_data = agent_data
784
+
785
+ with self.request():
786
+ session_load_response = api.session_load(cwd, [], self.session_id)
787
+ self.post_message(messages.SessionReplay(active=True))
788
+ try:
789
+ response = await session_load_response.wait()
790
+ finally:
791
+ self.post_message(messages.SessionReplay(active=False))
792
+
793
+ if (modes := response.get("modes", None)) is not None:
794
+ current_mode = modes["currentModeId"]
795
+ available_modes = modes["availableModes"]
796
+ modes_update = {
797
+ mode["id"]: Mode(
798
+ mode["id"], mode["name"], mode.get("description", None)
799
+ )
800
+ for mode in available_modes
801
+ }
802
+ self.post_message(messages.SetModes(current_mode, modes_update))
803
+
804
+ if session_title:
805
+ self.post_message(tui_messages.SessionUpdate(name=session_title))
806
+
807
+ await self._save_last_session_to_cdh()
808
+
809
+ async def acp_session_prompt(
810
+ self, prompt: list[protocol.ContentBlock]
811
+ ) -> str | None:
812
+ """Send the prompt to the agent.
813
+
814
+ Returns:
815
+ The stop reason.
816
+
817
+ """
818
+ with self.request():
819
+ session_prompt = api.session_prompt(prompt, self.session_id)
820
+ try:
821
+ result = await session_prompt.wait()
822
+ except jsonrpc.APIError as error:
823
+ details = ""
824
+ match error.data:
825
+ case {"details": details}:
826
+ pass
827
+
828
+ self.post_message(
829
+ AgentFail(
830
+ "Failed to send prompt" or error.message,
831
+ (
832
+ str(details)
833
+ if details
834
+ else f"{self._agent_data['name']} returned an error"
835
+ ),
836
+ )
837
+ )
838
+ return None
839
+ except jsonrpc.JSONRPCError as error:
840
+ self.post_message(
841
+ AgentFail(
842
+ "Failed to send prompt" or error.message,
843
+ (error.message or f"{self._agent_data['name']} returned an error"),
844
+ )
845
+ )
846
+ return None
847
+
848
+ assert result is not None
849
+ return result.get("stopReason")
850
+
851
+ async def acp_session_set_mode(self, mode_id: str) -> str | None:
852
+ """Update the current mode with the agent."""
853
+ with self.request():
854
+ response = api.session_set_mode(self.session_id, mode_id)
855
+ try:
856
+ await response.wait()
857
+ except jsonrpc.APIError as error:
858
+ match error.data:
859
+ case {"details": details}:
860
+ return details if isinstance(details, str) else "Failed to set mode"
861
+ return "Failed to set mode"
862
+ else:
863
+ return None
864
+
865
+ async def set_mode(self, mode_id: str) -> str | None:
866
+ return await self.acp_session_set_mode(mode_id)
867
+
868
+ async def set_session_name(self, name: str) -> None:
869
+ if self.session_pk is None:
870
+ return
871
+ db = DB()
872
+ await db.session_update_title(self.session_pk, name)
873
+ self.post_message(tui_messages.SessionUpdate(name=name))
874
+
875
+ async def acp_session_cancel(self) -> bool:
876
+ with self.request():
877
+ response = api.session_cancel(self.session_id, {})
878
+ try:
879
+ await response.wait()
880
+ except jsonrpc.APIError:
881
+ # No-op if there is nothing to cancel
882
+ return False
883
+ return True
884
+
885
+ async def cancel(self) -> bool:
886
+ return await self.acp_session_cancel()