@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,360 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+ from textual.app import ComposeResult
7
+ from textual import getters
8
+ from textual import events, on
9
+ from textual.binding import Binding
10
+ from textual import containers
11
+ from textual.content import Content
12
+ from textual.reactive import var, reactive
13
+ from textual.message import Message
14
+ from textual.widget import Widget
15
+
16
+ from textual import widgets
17
+
18
+ from tui.answer import Answer
19
+
20
+ type Options = list[Answer]
21
+
22
+
23
+ @dataclass
24
+ class Ask:
25
+ """Data for Question."""
26
+
27
+ question: str
28
+ options: Options
29
+ get_content: Callable[[], Widget] | None = None
30
+ callback: Callable[[Answer], Any] | None = None
31
+
32
+
33
+ class NonSelectableLabel(widgets.Label):
34
+ ALLOW_SELECT = False
35
+
36
+
37
+ class Option(containers.HorizontalGroup):
38
+ ALLOW_SELECT = False
39
+ DEFAULT_CSS = """
40
+ Option {
41
+
42
+ &:hover {
43
+ background: $boost;
44
+ }
45
+ color: $text-muted;
46
+ #caret {
47
+ visibility: hidden;
48
+ padding: 0 1;
49
+ }
50
+ #index {
51
+ padding-right: 1;
52
+ }
53
+ #label {
54
+ width: 1fr;
55
+ }
56
+ &.-active {
57
+ color: $text-accent;
58
+ #caret {
59
+ visibility: visible;
60
+ }
61
+ }
62
+ &.-selected {
63
+ opacity: 0.5;
64
+ }
65
+ &.-active.-selected {
66
+ opacity: 1.0;
67
+ background: transparent;
68
+ color: $text-accent;
69
+ #label {
70
+ text-style: underline;
71
+ }
72
+ #caret {
73
+ visibility: hidden;
74
+ }
75
+ }
76
+ }
77
+ """
78
+
79
+ @dataclass
80
+ class Selected(Message):
81
+ """The option was selected."""
82
+
83
+ index: int
84
+
85
+ selected: reactive[bool] = reactive(False, toggle_class="-selected")
86
+
87
+ def __init__(
88
+ self, index: int, content: Content, key: str | None, classes: str = ""
89
+ ) -> None:
90
+ super().__init__(classes=classes)
91
+ self.index = index
92
+ self.content = content
93
+ self.key = key
94
+
95
+ def compose(self) -> ComposeResult:
96
+ key = self.key
97
+ yield NonSelectableLabel("❯", id="caret")
98
+ if key:
99
+ yield NonSelectableLabel(Content.styled(f"{key}", "b"), id="index")
100
+ else:
101
+ yield NonSelectableLabel(Content(" "), id="index")
102
+
103
+ yield NonSelectableLabel(self.content, id="label")
104
+
105
+ def on_click(self, event: events.Click) -> None:
106
+ event.stop()
107
+ self.post_message(self.Selected(self.index))
108
+
109
+
110
+ class Question(containers.VerticalGroup, can_focus=True):
111
+ """A text question with a menu of responses."""
112
+
113
+ BINDING_GROUP_TITLE = "Question"
114
+ ALLOW_SELECT = False
115
+ CURSOR_GROUP = Binding.Group("Cursor", compact=True)
116
+ ALLOW_GROUP = Binding.Group("Allow once/always", compact=True)
117
+ REJECT_GROUP = Binding.Group("Reject once/always", compact=True)
118
+ BINDINGS = [
119
+ Binding(
120
+ "up",
121
+ "selection_up",
122
+ "Up",
123
+ group=CURSOR_GROUP,
124
+ ),
125
+ Binding(
126
+ "down",
127
+ "selection_down",
128
+ "Down",
129
+ group=CURSOR_GROUP,
130
+ ),
131
+ Binding(
132
+ "enter",
133
+ "select",
134
+ "Select",
135
+ ),
136
+ Binding(
137
+ "a",
138
+ "select_kind(('allow_once', 'allow'))",
139
+ "Allow once",
140
+ group=ALLOW_GROUP,
141
+ ),
142
+ Binding(
143
+ "A",
144
+ "select_kind('allow_always')",
145
+ "Allow always",
146
+ group=ALLOW_GROUP,
147
+ ),
148
+ Binding(
149
+ "r",
150
+ "select_kind(('reject_once', 'reject'))",
151
+ "Reject once",
152
+ group=REJECT_GROUP,
153
+ ),
154
+ Binding(
155
+ "R",
156
+ "select_kind('reject_always')",
157
+ "Reject always",
158
+ group=REJECT_GROUP,
159
+ ),
160
+ ]
161
+
162
+ DEFAULT_CSS = """
163
+ Question {
164
+ width: 1fr;
165
+ height: auto;
166
+ padding: 0 1;
167
+ background: transparent;
168
+ #title {
169
+ margin-bottom: 1;
170
+ color: $text-primary;
171
+ }
172
+ #question-container {
173
+ margin-bottom: 1;
174
+ }
175
+
176
+
177
+
178
+ #option-container.-blink Option.-active #caret {
179
+ opacity: 0.2;
180
+ }
181
+
182
+ &:blur {
183
+ #index {
184
+ opacity: 0.3;
185
+ }
186
+ #caret {
187
+ opacity: 0.3;
188
+ }
189
+ }
190
+ }
191
+ """
192
+
193
+ title: var[str] = var("")
194
+ options: var[Options] = var(list)
195
+
196
+ selection: reactive[int] = reactive(0, init=False)
197
+ selected: var[bool] = var(False, toggle_class="-selected")
198
+ blink: var[bool] = var(False)
199
+
200
+ option_container = getters.query_one("#option-container", containers.VerticalGroup)
201
+
202
+ DEFAULT_KINDS = {
203
+ "allow_once": "a",
204
+ "allow_always": "A",
205
+ "reject_once": "r",
206
+ "reject_always": "R",
207
+ }
208
+
209
+ @dataclass
210
+ class Answer(Message):
211
+ """User selected a response."""
212
+
213
+ index: int
214
+ answer: Answer
215
+
216
+ def __init__(
217
+ self,
218
+ title: str = "Ask and you will receive",
219
+ get_content: Callable[[], Widget] | None = None,
220
+ options: Options | None = None,
221
+ name: str | None = None,
222
+ id: str | None = None,
223
+ classes: str | None = None,
224
+ disabled: bool = False,
225
+ ):
226
+ super().__init__(name=name, id=id, classes=classes, disabled=disabled)
227
+ self.set_reactive(Question.title, title)
228
+ self._get_content = get_content
229
+ self.set_reactive(Question.options, options or [])
230
+
231
+ def on_mount(self) -> None:
232
+ def toggle_blink() -> None:
233
+ if self.has_focus:
234
+ self.blink = not self.blink
235
+ else:
236
+ self.blink = False
237
+
238
+ self._blink_timer = self.set_interval(0.5, toggle_blink)
239
+
240
+ def _reset_blink(self) -> None:
241
+ self.blink = False
242
+ self._blink_timer.reset()
243
+
244
+ def update(self, ask: Ask) -> None:
245
+ self.title = ask.question
246
+ self._get_content = ask.get_content
247
+ self.options = ask.options
248
+ self.selection = 0
249
+ self.selected = False
250
+ self.refresh(recompose=True, layout=True)
251
+
252
+ def compose(self) -> ComposeResult:
253
+
254
+ with containers.VerticalGroup(id="contents"):
255
+ if self.title:
256
+ yield widgets.Label(self.title, id="title", markup=False)
257
+ if self._get_content is not None:
258
+ yield self._get_content()
259
+
260
+ with containers.VerticalGroup(id="option-container"):
261
+ kinds: set[str] = set()
262
+ for index, answer in enumerate(self.options):
263
+ active = index == self.selection
264
+ key = (
265
+ answer.kind
266
+ if (answer.kind and answer.kind not in kinds
267
+ and answer.kind not in self.DEFAULT_KINDS)
268
+ else (
269
+ self.DEFAULT_KINDS.get(answer.kind)
270
+ if (answer.kind and answer.kind not in kinds)
271
+ else None
272
+ )
273
+ )
274
+ yield Option(
275
+ index,
276
+ Content(answer.text),
277
+ key,
278
+ classes="-active" if active else "",
279
+ ).data_bind(Question.selected)
280
+ if answer.kind is not None:
281
+ kinds.add(answer.kind)
282
+
283
+ def watch_selection(self, old_selection: int, new_selection: int) -> None:
284
+ self.query("#option-container > .-active").remove_class("-active")
285
+ if new_selection >= 0:
286
+ self.query_one("#option-container").children[new_selection].add_class(
287
+ "-active"
288
+ )
289
+
290
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
291
+ if self.selected and action in ("selection_up", "selection_down"):
292
+ return False
293
+ if action == "select_kind":
294
+ kinds = {answer.kind for answer in self.options if answer.kind is not None}
295
+ check_kinds = set()
296
+ for parameter in parameters:
297
+ if isinstance(parameter, str):
298
+ check_kinds.add(parameter)
299
+ elif isinstance(parameter, tuple):
300
+ check_kinds.update(parameter)
301
+
302
+ return any(kind in kinds for kind in check_kinds)
303
+
304
+ return True
305
+
306
+ def watch_blink(self, blink: bool) -> None:
307
+ self.option_container.set_class(blink, "-blink")
308
+
309
+ def action_selection_up(self) -> None:
310
+ self._reset_blink()
311
+ self.selection = max(0, self.selection - 1)
312
+
313
+ def action_selection_down(self) -> None:
314
+ self._reset_blink()
315
+ self.selection = min(len(self.options) - 1, self.selection + 1)
316
+
317
+ def action_select(self) -> None:
318
+ self._reset_blink()
319
+ self.post_message(
320
+ self.Answer(
321
+ index=self.selection,
322
+ answer=self.options[self.selection],
323
+ )
324
+ )
325
+ self.selected = True
326
+
327
+ def action_select_kind(self, kind: str | tuple[str]) -> None:
328
+ kinds = kind if isinstance(kind, tuple) else (kind,)
329
+ for kind in kinds:
330
+ for index, answer in enumerate(self.options):
331
+ if answer.kind == kind:
332
+ self.selection = index
333
+ self.action_select()
334
+ break
335
+
336
+ @on(Option.Selected)
337
+ def on_option_selected(self, event: Option.Selected) -> None:
338
+ event.stop()
339
+ self._reset_blink()
340
+ if not self.selected:
341
+ self.selection = event.index
342
+
343
+
344
+ if __name__ == "__main__":
345
+ from textual.app import App
346
+ from textual.widgets import Footer
347
+
348
+ OPTIONS = [
349
+ Answer("Yes, allow once", "proceed_always", kind="allow_once"),
350
+ Answer("Yes, allow always", "allow_always", kind="allow_always"),
351
+ Answer("Modify with external editor", "modify", kind="allow_once"),
352
+ Answer("No, suggest changes (esc)", "reject"),
353
+ ]
354
+
355
+ class QuestionApp(App):
356
+ def compose(self) -> ComposeResult:
357
+ yield Question("Apply this change?", OPTIONS)
358
+ yield Footer()
359
+
360
+ QuestionApp().run()
@@ -0,0 +1,224 @@
1
+ from textual.app import ComposeResult
2
+ from textual import getters
3
+ from textual.binding import Binding
4
+
5
+ from tui.app import A2TUIApp
6
+ from tui.constants import SESSION_GRID_PAGE_SIZE
7
+ from tui.widgets.grid_select import GridSelect
8
+ from tui.widgets.session_summary import SessionSummary
9
+ from tui.session_tracker import SessionTracker, SessionDetails
10
+
11
+
12
+ PAGE_INDICATOR_ID = "session-grid-page-indicator"
13
+
14
+
15
+ class SessionGridSelect(GridSelect):
16
+ FOCUS_ON_CLICK = True
17
+ CURSOR_GROUP = Binding.Group("Select")
18
+ BINDINGS = [
19
+ Binding("]", "next_page", "Next page"),
20
+ Binding("[", "prev_page", "Prev page"),
21
+ ]
22
+ app: getters.app[A2TUIApp] = getters.app(A2TUIApp)
23
+
24
+ def __init__(
25
+ self,
26
+ session_tracker: SessionTracker,
27
+ id: str | None = None,
28
+ classes: str | None = None,
29
+ ) -> None:
30
+ self.session_tracker = session_tracker
31
+ self._all_sessions: list[SessionDetails] = []
32
+ self._current_page: int = 0
33
+ super().__init__(
34
+ id=id,
35
+ classes=classes,
36
+ min_column_width=36,
37
+ )
38
+
39
+ @property
40
+ def page_size(self) -> int:
41
+ return SESSION_GRID_PAGE_SIZE
42
+
43
+ @property
44
+ def page_count(self) -> int:
45
+ total = len(self._all_sessions)
46
+ if total == 0:
47
+ return 0
48
+ return (total + self.page_size - 1) // self.page_size
49
+
50
+ @property
51
+ def current_page(self) -> int:
52
+ return self._current_page
53
+
54
+ def allow_focus(self) -> bool:
55
+ return True
56
+
57
+ def on_mount(self) -> None:
58
+ self.app.session_update_signal.subscribe(
59
+ self, self.handle_session_update_signal
60
+ )
61
+ if self._all_sessions:
62
+ self._render_page()
63
+ self.call_after_refresh(self._ensure_focus)
64
+
65
+ def _ensure_focus(self) -> None:
66
+ if (
67
+ self.is_mounted
68
+ and not self._closing
69
+ and self.focusable
70
+ and self.screen is not None
71
+ and self.screen.is_mounted
72
+ ):
73
+ self.focus()
74
+
75
+ def update_current(self, current_mode: str) -> None:
76
+ for index, child in enumerate(self.query_children(SessionSummary)):
77
+ if child.session_details is None:
78
+ continue
79
+ is_current = child.session_details.mode_name == current_mode
80
+ child.current = is_current
81
+ if is_current:
82
+ self.highlighted = index
83
+
84
+ async def handle_session_update_signal(
85
+ self, update: tuple[str, SessionDetails | None]
86
+ ) -> None:
87
+ mode_name, details = update
88
+ if details is None:
89
+ if session_summary := self.query_one_optional(
90
+ f"#{mode_name}", SessionSummary
91
+ ):
92
+ await session_summary.remove()
93
+ for i, existing in enumerate(self._all_sessions):
94
+ if existing.mode_name == mode_name:
95
+ self._all_sessions.pop(i)
96
+ break
97
+ self._clamp_page()
98
+ self._render_page()
99
+ return
100
+
101
+ if session_summary := self.query_one_optional(
102
+ f"#{mode_name}", SessionSummary
103
+ ):
104
+ session_summary.session_details = details
105
+ for existing in self._all_sessions:
106
+ if existing.mode_name == mode_name:
107
+ existing.title = details.title
108
+ existing.subtitle = details.subtitle
109
+ existing.path = details.path
110
+ existing.state = details.state
111
+ break
112
+ return
113
+
114
+ if details.session_pk is not None:
115
+ for i, existing in enumerate(self._all_sessions):
116
+ if existing.session_pk == details.session_pk:
117
+ self._all_sessions[i] = details
118
+ self._render_page()
119
+ return
120
+
121
+ self._all_sessions.append(details)
122
+ self._render_page()
123
+
124
+ def compose(self) -> ComposeResult:
125
+ yield from ()
126
+
127
+ def set_sessions(self, sessions: list[SessionDetails]) -> None:
128
+ deduped: list[SessionDetails] = []
129
+ seen: set[int] = set()
130
+ for session in sessions:
131
+ if session.session_pk is not None:
132
+ if session.session_pk in seen:
133
+ continue
134
+ seen.add(session.session_pk)
135
+ deduped.append(session)
136
+ self._all_sessions = deduped
137
+ self._current_page = 0
138
+ if self.is_mounted:
139
+ self._render_page()
140
+
141
+ def _current_page_items(self) -> list[SessionDetails]:
142
+ if not self._all_sessions:
143
+ return []
144
+ start = self._current_page * self.page_size
145
+ end = start + self.page_size
146
+ return self._all_sessions[start:end]
147
+
148
+ def _clamp_page(self) -> None:
149
+ if self._current_page >= self.page_count and self.page_count > 0:
150
+ self._current_page = self.page_count - 1
151
+
152
+ def _render_page(self) -> None:
153
+ if not self.is_mounted or self._closing or self._closed:
154
+ return
155
+ if self.screen is None or not self.screen.is_mounted:
156
+ return
157
+
158
+ current_items = self._current_page_items()
159
+ seen: dict[str, SessionDetails] = {}
160
+ for session in current_items:
161
+ seen[session.mode_name] = session
162
+ unique_items = list(seen.values())
163
+
164
+ for child in list(self.query_children(SessionSummary)):
165
+ if child.id not in seen:
166
+ try:
167
+ child.remove()
168
+ except Exception:
169
+ pass
170
+
171
+ existing_ids = {c.id for c in self.query_children(SessionSummary)}
172
+ for session in unique_items:
173
+ if session.mode_name not in existing_ids:
174
+ try:
175
+ self.mount(SessionSummary(session, id=session.mode_name))
176
+ except Exception:
177
+ pass
178
+
179
+ self._apply_current()
180
+ self._update_page_indicator()
181
+ self.highlighted = 0 if unique_items else None
182
+
183
+ if unique_items:
184
+ self.call_later(self._ensure_focus)
185
+
186
+ def _apply_current(self) -> None:
187
+ current_mode = self._current_screen_mode_name()
188
+ for index, child in enumerate(self.query_children(SessionSummary)):
189
+ if child.session_details is None:
190
+ continue
191
+ is_current = child.session_details.mode_name == current_mode
192
+ child.current = is_current
193
+ if is_current:
194
+ self.highlighted = index
195
+
196
+ def _current_screen_mode_name(self) -> str | None:
197
+ try:
198
+ screen = self.app.screen_stack[0]
199
+ except (IndexError, AttributeError):
200
+ return None
201
+ return getattr(screen, "id", None)
202
+
203
+ def _update_page_indicator(self) -> None:
204
+ try:
205
+ indicator = self.app.query_one(f"#{PAGE_INDICATOR_ID}")
206
+ except Exception:
207
+ return
208
+ total = len(self._all_sessions)
209
+ if total == 0:
210
+ indicator.update("")
211
+ return
212
+ indicator.update(
213
+ f"Page {self._current_page + 1} / {self.page_count} · 共 {total} 项"
214
+ )
215
+
216
+ def action_next_page(self) -> None:
217
+ if self._current_page + 1 < self.page_count:
218
+ self._current_page += 1
219
+ self._render_page()
220
+
221
+ def action_prev_page(self) -> None:
222
+ if self._current_page > 0:
223
+ self._current_page -= 1
224
+ self._render_page()
@@ -0,0 +1,115 @@
1
+ from textual.app import ComposeResult
2
+ from textual import getters
3
+ from textual import containers
4
+ from textual import widgets
5
+ from textual.reactive import reactive
6
+ from textual.timer import Timer
7
+
8
+
9
+ from tui.widgets.condensed_path import CondensedPath
10
+ from tui.widgets.throbber import ThrobberVisual
11
+ from tui.session_tracker import SessionDetails
12
+
13
+
14
+ class BusyIndicator(widgets.Static):
15
+ def render(self) -> ThrobberVisual:
16
+ return ThrobberVisual("▔")
17
+
18
+ def on_mount(self) -> None:
19
+ self.auto_refresh = 1 / 4
20
+
21
+
22
+ class SessionSummary(containers.VerticalGroup):
23
+ session_details: reactive[SessionDetails | None] = reactive(
24
+ None, always_update=True, recompose=True
25
+ )
26
+
27
+ title = getters.query_one(".title", widgets.Label)
28
+ subtitle = getters.query_one(".subtitle", widgets.Label)
29
+ current = reactive(False, toggle_class="-current", init=False, recompose=True)
30
+ blink = reactive(False, toggle_class="-blink")
31
+
32
+ def __init__(
33
+ self,
34
+ session_details: SessionDetails | None,
35
+ id: str | None = None,
36
+ classes: str | None = None,
37
+ ) -> None:
38
+ super().__init__(id=id, classes=classes)
39
+ self.blink_timer: Timer | None = None
40
+ self.set_reactive(SessionSummary.session_details, session_details)
41
+
42
+ def on_mount(self) -> None:
43
+
44
+ def do_blink() -> None:
45
+ self.blink = not self.blink
46
+
47
+ self.blink_timer = self.set_interval(0.5, do_blink, pause=False)
48
+
49
+ def watch_current(self, current: bool) -> None:
50
+ if (session_details := self.session_details) is not None:
51
+ if current:
52
+ self.title.update(f"✱{session_details.title}")
53
+ else:
54
+ self.title.update(session_details.title)
55
+
56
+ def compose(self) -> ComposeResult:
57
+ if (session_details := self.session_details) is not None:
58
+ self.remove_class(
59
+ "-state-notready",
60
+ "-state-busy",
61
+ "-state-asking",
62
+ "-state-idle",
63
+ update=False,
64
+ )
65
+ self.add_class(f"-state-{session_details.state}")
66
+
67
+ yield BusyIndicator()
68
+ # yield widgets.Rule(line_style="heavy")
69
+ with containers.HorizontalGroup():
70
+ yield widgets.Label("❯", classes="icon")
71
+ with containers.VerticalGroup():
72
+ if session_details is not None:
73
+ yield widgets.Label(
74
+ (
75
+ f"✱{session_details.title}"
76
+ if self.current
77
+ else session_details.title
78
+ ),
79
+ classes="title",
80
+ markup=False,
81
+ )
82
+ yield widgets.Label(
83
+ session_details.subtitle,
84
+ classes="subtitle",
85
+ markup=False,
86
+ )
87
+ with containers.HorizontalGroup():
88
+ yield widgets.Label("📁 ")
89
+ yield CondensedPath(session_details.path)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ from textual.app import App
94
+
95
+ session_details = SessionDetails(
96
+ 0,
97
+ "mode",
98
+ title="Building BMI calculator",
99
+ subtitle="Claude Code",
100
+ )
101
+
102
+ class SessionApp(App):
103
+ CSS_PATH = "../tui.tcss"
104
+ CSS = """
105
+ Screen {
106
+ layout: horizontal;
107
+ }
108
+ """
109
+
110
+ def compose(self) -> ComposeResult:
111
+ yield SessionSummary(session_details, classes="-state-busy")
112
+ yield SessionSummary(session_details, classes="-state-asking")
113
+ yield SessionSummary(session_details, classes="-state-idle")
114
+
115
+ SessionApp().run()