alloy-runtime-cli 0.1.0__py3-none-any.whl

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 (451) hide show
  1. alloy_runtime_cli-0.1.0.dist-info/METADATA +61 -0
  2. alloy_runtime_cli-0.1.0.dist-info/RECORD +451 -0
  3. alloy_runtime_cli-0.1.0.dist-info/WHEEL +5 -0
  4. alloy_runtime_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. alloy_runtime_cli-0.1.0.dist-info/top_level.txt +1 -0
  6. cli/__init__.py +0 -0
  7. cli/commands/__init__.py +0 -0
  8. cli/commands/admin/__init__.py +0 -0
  9. cli/commands/admin/bootstrap_command.py +118 -0
  10. cli/commands/admin/credentials/__init__.py +0 -0
  11. cli/commands/admin/credentials/create/__init__.py +0 -0
  12. cli/commands/admin/credentials/create/command.py +148 -0
  13. cli/commands/admin/credentials/create/presenter.py +16 -0
  14. cli/commands/admin/credentials/grant/__init__.py +0 -0
  15. cli/commands/admin/credentials/grant/command.py +119 -0
  16. cli/commands/admin/credentials/grant/fields.py +33 -0
  17. cli/commands/admin/credentials/grant/presenter.py +23 -0
  18. cli/commands/agents/__init__.py +0 -0
  19. cli/commands/agents/create/__init__.py +0 -0
  20. cli/commands/agents/create/command.py +475 -0
  21. cli/commands/agents/create/fields.py +64 -0
  22. cli/commands/agents/create/presenter.py +68 -0
  23. cli/commands/agents/delete/__init__.py +0 -0
  24. cli/commands/agents/delete/command.py +47 -0
  25. cli/commands/agents/delete/presenter.py +16 -0
  26. cli/commands/agents/get/command.py +37 -0
  27. cli/commands/agents/get/presenter.py +32 -0
  28. cli/commands/agents/list/__init__.py +1 -0
  29. cli/commands/agents/list/command.py +54 -0
  30. cli/commands/agents/list/presenter.py +82 -0
  31. cli/commands/agents/update/__init__.py +0 -0
  32. cli/commands/agents/update/command.py +435 -0
  33. cli/commands/agents/update/fields.py +40 -0
  34. cli/commands/agents/update/presenter.py +68 -0
  35. cli/commands/audio/__init__.py +0 -0
  36. cli/commands/audio/transcribe/__init__.py +0 -0
  37. cli/commands/audio/transcribe/command.py +144 -0
  38. cli/commands/audio/transcribe/presenter.py +15 -0
  39. cli/commands/auth/__init__.py +0 -0
  40. cli/commands/auth/login/__init__.py +0 -0
  41. cli/commands/auth/login/command.py +80 -0
  42. cli/commands/auth/signup/__init__.py +0 -0
  43. cli/commands/auth/signup/command.py +115 -0
  44. cli/commands/billing/__init__.py +1 -0
  45. cli/commands/billing/costs/__init__.py +1 -0
  46. cli/commands/billing/costs/by_agent/__init__.py +1 -0
  47. cli/commands/billing/costs/by_agent/command.py +57 -0
  48. cli/commands/billing/costs/by_agent/presenter.py +81 -0
  49. cli/commands/billing/costs/by_model/__init__.py +1 -0
  50. cli/commands/billing/costs/by_model/command.py +57 -0
  51. cli/commands/billing/costs/by_model/presenter.py +80 -0
  52. cli/commands/billing/costs/daily/__init__.py +1 -0
  53. cli/commands/billing/costs/daily/command.py +55 -0
  54. cli/commands/billing/costs/daily/presenter.py +75 -0
  55. cli/commands/billing/costs/summary/__init__.py +1 -0
  56. cli/commands/billing/costs/summary/command.py +57 -0
  57. cli/commands/billing/costs/summary/presenter.py +42 -0
  58. cli/commands/billing/projects/__init__.py +1 -0
  59. cli/commands/billing/projects/create/__init__.py +1 -0
  60. cli/commands/billing/projects/create/command.py +60 -0
  61. cli/commands/billing/projects/create/presenter.py +26 -0
  62. cli/commands/billing/projects/get/__init__.py +1 -0
  63. cli/commands/billing/projects/get/command.py +33 -0
  64. cli/commands/billing/projects/get/presenter.py +32 -0
  65. cli/commands/billing/projects/list/__init__.py +1 -0
  66. cli/commands/billing/projects/list/command.py +40 -0
  67. cli/commands/billing/projects/list/presenter.py +57 -0
  68. cli/commands/content/__init__.py +1 -0
  69. cli/commands/content/delete/__init__.py +0 -0
  70. cli/commands/content/delete/command.py +49 -0
  71. cli/commands/content/delete/presenter.py +18 -0
  72. cli/commands/content/edit/__init__.py +1 -0
  73. cli/commands/content/edit/command.py +155 -0
  74. cli/commands/content/edit/editor.py +150 -0
  75. cli/commands/content/edit/presenter.py +146 -0
  76. cli/commands/content/get/__init__.py +1 -0
  77. cli/commands/content/get/command.py +39 -0
  78. cli/commands/content/get/presenter.py +176 -0
  79. cli/commands/content/list/__init__.py +1 -0
  80. cli/commands/content/list/command.py +347 -0
  81. cli/commands/content/list/export_formatters.py +409 -0
  82. cli/commands/content/list/export_handler.py +165 -0
  83. cli/commands/content/list/presenter.py +190 -0
  84. cli/commands/credentials/__init__.py +0 -0
  85. cli/commands/credentials/create/__init__.py +0 -0
  86. cli/commands/credentials/create/command.py +165 -0
  87. cli/commands/credentials/create/fields.py +38 -0
  88. cli/commands/credentials/create/presenter.py +20 -0
  89. cli/commands/credentials/update/__init__.py +0 -0
  90. cli/commands/credentials/update/command.py +53 -0
  91. cli/commands/credentials/update/fields.py +71 -0
  92. cli/commands/credentials/update/presenter.py +16 -0
  93. cli/commands/flag_utils.py +366 -0
  94. cli/commands/generate/__init__.py +0 -0
  95. cli/commands/generate/cancel/__init__.py +1 -0
  96. cli/commands/generate/cancel/command.py +44 -0
  97. cli/commands/generate/cancel/presenter.py +26 -0
  98. cli/commands/generate/status/__init__.py +1 -0
  99. cli/commands/generate/status/command.py +58 -0
  100. cli/commands/generate/status/presenter.py +78 -0
  101. cli/commands/generate/text/__init__.py +0 -0
  102. cli/commands/generate/text/command.py +1325 -0
  103. cli/commands/generate/text/concurrent_renderer.py +355 -0
  104. cli/commands/generate/text/presenter.py +287 -0
  105. cli/commands/generate/text/stream_renderer.py +129 -0
  106. cli/commands/knowledge/__init__.py +0 -0
  107. cli/commands/knowledge/collections/__init__.py +0 -0
  108. cli/commands/knowledge/collections/cluster/__init__.py +0 -0
  109. cli/commands/knowledge/collections/cluster/command.py +64 -0
  110. cli/commands/knowledge/collections/cluster/presenter.py +74 -0
  111. cli/commands/knowledge/collections/cluster_status/__init__.py +0 -0
  112. cli/commands/knowledge/collections/cluster_status/command.py +46 -0
  113. cli/commands/knowledge/collections/cluster_status/presenter.py +10 -0
  114. cli/commands/knowledge/collections/create/__init__.py +0 -0
  115. cli/commands/knowledge/collections/create/command.py +137 -0
  116. cli/commands/knowledge/collections/create/presenter.py +38 -0
  117. cli/commands/knowledge/collections/delete/__init__.py +1 -0
  118. cli/commands/knowledge/collections/delete/command.py +47 -0
  119. cli/commands/knowledge/collections/delete/presenter.py +20 -0
  120. cli/commands/knowledge/collections/get/__init__.py +1 -0
  121. cli/commands/knowledge/collections/get/command.py +30 -0
  122. cli/commands/knowledge/collections/get/presenter.py +44 -0
  123. cli/commands/knowledge/collections/list/__init__.py +1 -0
  124. cli/commands/knowledge/collections/list/command.py +41 -0
  125. cli/commands/knowledge/collections/list/presenter.py +68 -0
  126. cli/commands/knowledge/collections/update/__init__.py +0 -0
  127. cli/commands/knowledge/collections/update/command.py +97 -0
  128. cli/commands/knowledge/collections/update/presenter.py +42 -0
  129. cli/commands/knowledge/documents/__init__.py +0 -0
  130. cli/commands/knowledge/documents/bulk_metadata/__init__.py +0 -0
  131. cli/commands/knowledge/documents/bulk_metadata/command.py +119 -0
  132. cli/commands/knowledge/documents/bulk_metadata/presenter.py +36 -0
  133. cli/commands/knowledge/documents/delete/__init__.py +0 -0
  134. cli/commands/knowledge/documents/delete/command.py +47 -0
  135. cli/commands/knowledge/documents/delete/presenter.py +20 -0
  136. cli/commands/knowledge/documents/get/__init__.py +0 -0
  137. cli/commands/knowledge/documents/get/command.py +39 -0
  138. cli/commands/knowledge/documents/get/presenter.py +78 -0
  139. cli/commands/knowledge/documents/ingest/__init__.py +0 -0
  140. cli/commands/knowledge/documents/ingest/command.py +222 -0
  141. cli/commands/knowledge/documents/ingest/presenter.py +41 -0
  142. cli/commands/knowledge/documents/list/__init__.py +0 -0
  143. cli/commands/knowledge/documents/list/command.py +69 -0
  144. cli/commands/knowledge/documents/list/presenter.py +86 -0
  145. cli/commands/knowledge/documents/reingest/__init__.py +0 -0
  146. cli/commands/knowledge/documents/reingest/command.py +102 -0
  147. cli/commands/knowledge/documents/reingest/presenter.py +70 -0
  148. cli/commands/knowledge/documents/update/__init__.py +0 -0
  149. cli/commands/knowledge/documents/update/command.py +85 -0
  150. cli/commands/knowledge/documents/update/presenter.py +37 -0
  151. cli/commands/knowledge/recover/__init__.py +0 -0
  152. cli/commands/knowledge/recover/command.py +46 -0
  153. cli/commands/knowledge/recover/presenter.py +79 -0
  154. cli/commands/knowledge/search/__init__.py +0 -0
  155. cli/commands/knowledge/search/command.py +218 -0
  156. cli/commands/knowledge/search/presenter.py +111 -0
  157. cli/commands/knowledge/synthesis/__init__.py +0 -0
  158. cli/commands/knowledge/synthesis/create/__init__.py +0 -0
  159. cli/commands/knowledge/synthesis/create/command.py +127 -0
  160. cli/commands/knowledge/synthesis/create/presenter.py +33 -0
  161. cli/commands/knowledge/synthesis/delete/__init__.py +0 -0
  162. cli/commands/knowledge/synthesis/delete/command.py +53 -0
  163. cli/commands/knowledge/synthesis/delete/presenter.py +31 -0
  164. cli/commands/knowledge/synthesis/get/__init__.py +0 -0
  165. cli/commands/knowledge/synthesis/get/command.py +55 -0
  166. cli/commands/knowledge/synthesis/get/presenter.py +114 -0
  167. cli/commands/knowledge/synthesis/list/__init__.py +0 -0
  168. cli/commands/knowledge/synthesis/list/command.py +132 -0
  169. cli/commands/knowledge/synthesis/list/presenter.py +84 -0
  170. cli/commands/knowledge/synthesis/refresh/__init__.py +0 -0
  171. cli/commands/knowledge/synthesis/refresh/command.py +42 -0
  172. cli/commands/knowledge/synthesis/refresh/presenter.py +33 -0
  173. cli/commands/knowledge/synthesis/update/__init__.py +0 -0
  174. cli/commands/knowledge/synthesis/update/command.py +76 -0
  175. cli/commands/knowledge/synthesis/update/presenter.py +41 -0
  176. cli/commands/models/__init__.py +0 -0
  177. cli/commands/models/list/__init__.py +0 -0
  178. cli/commands/models/list/command.py +84 -0
  179. cli/commands/models/list/presenter.py +114 -0
  180. cli/commands/organizations/__init__.py +0 -0
  181. cli/commands/organizations/create/command.py +32 -0
  182. cli/commands/organizations/create/presenter.py +9 -0
  183. cli/commands/pipelines/__init__.py +1 -0
  184. cli/commands/pipelines/approvals/__init__.py +1 -0
  185. cli/commands/pipelines/approvals/decide_command.py +77 -0
  186. cli/commands/pipelines/approvals/get_command.py +44 -0
  187. cli/commands/pipelines/approvals/presenter.py +56 -0
  188. cli/commands/pipelines/costs/__init__.py +1 -0
  189. cli/commands/pipelines/costs/command.py +57 -0
  190. cli/commands/pipelines/costs/daily_command.py +54 -0
  191. cli/commands/pipelines/costs/daily_presenter.py +59 -0
  192. cli/commands/pipelines/costs/presenter.py +37 -0
  193. cli/commands/pipelines/create/__init__.py +1 -0
  194. cli/commands/pipelines/create/command.py +103 -0
  195. cli/commands/pipelines/create/presenter.py +22 -0
  196. cli/commands/pipelines/env_vars/__init__.py +1 -0
  197. cli/commands/pipelines/env_vars/command.py +51 -0
  198. cli/commands/pipelines/env_vars/presenter.py +16 -0
  199. cli/commands/pipelines/execute/__init__.py +1 -0
  200. cli/commands/pipelines/execute/command.py +142 -0
  201. cli/commands/pipelines/execute/presenter.py +47 -0
  202. cli/commands/pipelines/executions/__init__.py +1 -0
  203. cli/commands/pipelines/executions/costs/__init__.py +1 -0
  204. cli/commands/pipelines/executions/costs/command.py +48 -0
  205. cli/commands/pipelines/executions/costs/presenter.py +29 -0
  206. cli/commands/pipelines/executions/costs_by_model/__init__.py +1 -0
  207. cli/commands/pipelines/executions/costs_by_model/command.py +50 -0
  208. cli/commands/pipelines/executions/costs_by_model/presenter.py +78 -0
  209. cli/commands/pipelines/executions/costs_by_step/__init__.py +1 -0
  210. cli/commands/pipelines/executions/costs_by_step/command.py +50 -0
  211. cli/commands/pipelines/executions/costs_by_step/presenter.py +72 -0
  212. cli/commands/pipelines/executions/get_command.py +38 -0
  213. cli/commands/pipelines/executions/list_command.py +123 -0
  214. cli/commands/pipelines/executions/presenter.py +131 -0
  215. cli/commands/pipelines/executions/rerun_command.py +41 -0
  216. cli/commands/pipelines/executions/update/__init__.py +1 -0
  217. cli/commands/pipelines/executions/update/command.py +110 -0
  218. cli/commands/pipelines/executions/update/presenter.py +28 -0
  219. cli/commands/pipelines/get/__init__.py +1 -0
  220. cli/commands/pipelines/get/command.py +33 -0
  221. cli/commands/pipelines/get/presenter.py +48 -0
  222. cli/commands/pipelines/list/__init__.py +1 -0
  223. cli/commands/pipelines/list/command.py +53 -0
  224. cli/commands/pipelines/list/presenter.py +66 -0
  225. cli/commands/pipelines/schedules/__init__.py +1 -0
  226. cli/commands/pipelines/schedules/create_command.py +119 -0
  227. cli/commands/pipelines/schedules/create_presenter.py +35 -0
  228. cli/commands/pipelines/schedules/delete_command.py +52 -0
  229. cli/commands/pipelines/schedules/env_vars_command.py +59 -0
  230. cli/commands/pipelines/schedules/env_vars_presenter.py +16 -0
  231. cli/commands/pipelines/schedules/get_command.py +38 -0
  232. cli/commands/pipelines/schedules/list_command.py +33 -0
  233. cli/commands/pipelines/schedules/once_command.py +90 -0
  234. cli/commands/pipelines/schedules/once_presenter.py +30 -0
  235. cli/commands/pipelines/schedules/presenter.py +104 -0
  236. cli/commands/pipelines/schedules/update_command.py +139 -0
  237. cli/commands/pipelines/schedules/update_presenter.py +29 -0
  238. cli/commands/render/__init__.py +0 -0
  239. cli/commands/render/html_to_image/__init__.py +0 -0
  240. cli/commands/render/html_to_image/command.py +170 -0
  241. cli/commands/schemas/__init__.py +0 -0
  242. cli/commands/schemas/create/__init__.py +0 -0
  243. cli/commands/schemas/create/command.py +122 -0
  244. cli/commands/schemas/create/presenter.py +53 -0
  245. cli/commands/schemas/delete/command.py +45 -0
  246. cli/commands/schemas/delete/presenter.py +9 -0
  247. cli/commands/schemas/get/__init__.py +0 -0
  248. cli/commands/schemas/get/command.py +56 -0
  249. cli/commands/schemas/get/presenter.py +129 -0
  250. cli/commands/schemas/list/__init__.py +0 -0
  251. cli/commands/schemas/list/command.py +64 -0
  252. cli/commands/schemas/list/presenter.py +133 -0
  253. cli/commands/schemas/update/__init__.py +0 -0
  254. cli/commands/schemas/update/command.py +369 -0
  255. cli/commands/schemas/update/presenter.py +53 -0
  256. cli/commands/sessions/__init__.py +1 -0
  257. cli/commands/sessions/delete/__init__.py +1 -0
  258. cli/commands/sessions/delete/command.py +47 -0
  259. cli/commands/sessions/delete/presenter.py +10 -0
  260. cli/commands/sessions/get/__init__.py +1 -0
  261. cli/commands/sessions/get/command.py +42 -0
  262. cli/commands/sessions/get/presenter.py +59 -0
  263. cli/commands/sessions/list/__init__.py +1 -0
  264. cli/commands/sessions/list/command.py +61 -0
  265. cli/commands/sessions/list/presenter.py +68 -0
  266. cli/commands/sessions/messages/__init__.py +1 -0
  267. cli/commands/sessions/messages/command.py +78 -0
  268. cli/commands/sessions/messages/presenter.py +79 -0
  269. cli/commands/shared_flags.py +500 -0
  270. cli/commands/sync/__init__.py +0 -0
  271. cli/commands/sync/command.py +45 -0
  272. cli/commands/sync/presenter.py +49 -0
  273. cli/commands/tags/__init__.py +1 -0
  274. cli/commands/tags/create/__init__.py +1 -0
  275. cli/commands/tags/create/command.py +60 -0
  276. cli/commands/tags/delete/__init__.py +1 -0
  277. cli/commands/tags/delete/command.py +47 -0
  278. cli/commands/tags/delete/presenter.py +10 -0
  279. cli/commands/tags/get/command.py +31 -0
  280. cli/commands/tags/get/presenter.py +23 -0
  281. cli/commands/tags/list/__init__.py +1 -0
  282. cli/commands/tags/list/command.py +52 -0
  283. cli/commands/tags/list/presenter.py +49 -0
  284. cli/commands/tags/update/command.py +64 -0
  285. cli/commands/tags/update/presenter.py +9 -0
  286. cli/commands/templates/__init__.py +0 -0
  287. cli/commands/templates/create/__init__.py +0 -0
  288. cli/commands/templates/create/command.py +152 -0
  289. cli/commands/templates/create/presenter.py +86 -0
  290. cli/commands/templates/delete/__init__.py +0 -0
  291. cli/commands/templates/delete/command.py +47 -0
  292. cli/commands/templates/delete/presenter.py +16 -0
  293. cli/commands/templates/get/__init__.py +0 -0
  294. cli/commands/templates/get/command.py +52 -0
  295. cli/commands/templates/get/presenter.py +233 -0
  296. cli/commands/templates/get_by_version/command.py +32 -0
  297. cli/commands/templates/get_by_version/presenter.py +30 -0
  298. cli/commands/templates/list/__init__.py +1 -0
  299. cli/commands/templates/list/command.py +102 -0
  300. cli/commands/templates/list/presenter.py +93 -0
  301. cli/commands/templates/render/__init__.py +0 -0
  302. cli/commands/templates/render/command.py +115 -0
  303. cli/commands/templates/render/presenter.py +276 -0
  304. cli/commands/templates/update/__init__.py +0 -0
  305. cli/commands/templates/update/command.py +199 -0
  306. cli/commands/templates/update/presenter.py +94 -0
  307. cli/commands/templates/version/__init__.py +1 -0
  308. cli/commands/templates/version/command.py +116 -0
  309. cli/commands/templates/version/presenter.py +100 -0
  310. cli/commands/tool_configs/__init__.py +0 -0
  311. cli/commands/tool_configs/create/__init__.py +0 -0
  312. cli/commands/tool_configs/create/command.py +118 -0
  313. cli/commands/tool_configs/create/presenter.py +53 -0
  314. cli/commands/tool_configs/delete/__init__.py +0 -0
  315. cli/commands/tool_configs/delete/command.py +47 -0
  316. cli/commands/tool_configs/delete/presenter.py +18 -0
  317. cli/commands/tool_configs/get/__init__.py +0 -0
  318. cli/commands/tool_configs/get/command.py +31 -0
  319. cli/commands/tool_configs/get/presenter.py +62 -0
  320. cli/commands/tool_configs/list/__init__.py +0 -0
  321. cli/commands/tool_configs/list/command.py +59 -0
  322. cli/commands/tool_configs/list/presenter.py +60 -0
  323. cli/commands/tool_configs/update/__init__.py +0 -0
  324. cli/commands/tool_configs/update/command.py +128 -0
  325. cli/commands/tool_configs/update/presenter.py +53 -0
  326. cli/commands/tools/__init__.py +1 -0
  327. cli/commands/tools/get/__init__.py +1 -0
  328. cli/commands/tools/get/command.py +42 -0
  329. cli/commands/tools/get/presenter.py +45 -0
  330. cli/commands/tools/list/__init__.py +1 -0
  331. cli/commands/tools/list/command.py +56 -0
  332. cli/commands/tools/list/presenter.py +44 -0
  333. cli/commands/users/__init__.py +0 -0
  334. cli/commands/users/create/command.py +53 -0
  335. cli/commands/users/create/presenter.py +9 -0
  336. cli/commands/whoami/__init__.py +0 -0
  337. cli/commands/whoami/command.py +42 -0
  338. cli/infrastructure/__init__.py +0 -0
  339. cli/infrastructure/auth_storage.py +71 -0
  340. cli/infrastructure/client_factory.py +36 -0
  341. cli/infrastructure/command.py +75 -0
  342. cli/infrastructure/config.py +188 -0
  343. cli/infrastructure/console.py +27 -0
  344. cli/infrastructure/editor.py +138 -0
  345. cli/infrastructure/error_display.py +178 -0
  346. cli/infrastructure/field_extractor.py +360 -0
  347. cli/infrastructure/file_content.py +210 -0
  348. cli/infrastructure/filter_parser.py +256 -0
  349. cli/infrastructure/formatters/__init__.py +0 -0
  350. cli/infrastructure/formatters/base.py +99 -0
  351. cli/infrastructure/formatters/compact_formatter.py +245 -0
  352. cli/infrastructure/formatters/json_formatter.py +84 -0
  353. cli/infrastructure/formatters/lines_formatter.py +102 -0
  354. cli/infrastructure/formatting/__init__.py +0 -0
  355. cli/infrastructure/formatting/fields.py +193 -0
  356. cli/infrastructure/forms/__init__.py +0 -0
  357. cli/infrastructure/forms/agent_picker.py +123 -0
  358. cli/infrastructure/forms/agent_tool_editor.py +384 -0
  359. cli/infrastructure/forms/agent_tools_manager.py +212 -0
  360. cli/infrastructure/forms/base_picker.py +469 -0
  361. cli/infrastructure/forms/components.py +126 -0
  362. cli/infrastructure/forms/json_schema_builder.py +149 -0
  363. cli/infrastructure/forms/model_picker.py +134 -0
  364. cli/infrastructure/forms/parsers.py +173 -0
  365. cli/infrastructure/forms/resolution_modal.py +302 -0
  366. cli/infrastructure/forms/schema_picker.py +137 -0
  367. cli/infrastructure/forms/tag_management_modal.py +103 -0
  368. cli/infrastructure/forms/tag_picker.py +207 -0
  369. cli/infrastructure/forms/template_picker.py +131 -0
  370. cli/infrastructure/forms/tool_config_picker.py +130 -0
  371. cli/infrastructure/forms/tool_picker.py +103 -0
  372. cli/infrastructure/injection/__init__.py +0 -0
  373. cli/infrastructure/injection/parser.py +302 -0
  374. cli/infrastructure/injection/resolver.py +399 -0
  375. cli/infrastructure/kv_parser.py +130 -0
  376. cli/infrastructure/local_storage.py +227 -0
  377. cli/infrastructure/macro_parser.py +215 -0
  378. cli/infrastructure/output.py +192 -0
  379. cli/infrastructure/provider_setup.py +81 -0
  380. cli/infrastructure/renderers/__init__.py +0 -0
  381. cli/infrastructure/renderers/entity_renderer.py +77 -0
  382. cli/infrastructure/renderers/list_renderer.py +114 -0
  383. cli/infrastructure/scope_utils.py +47 -0
  384. cli/infrastructure/spinner.py +101 -0
  385. cli/infrastructure/tui/__init__.py +0 -0
  386. cli/infrastructure/tui/clipboard.py +41 -0
  387. cli/infrastructure/tui/formatters.py +105 -0
  388. cli/infrastructure/tui/preview.py +14 -0
  389. cli/infrastructure/tui/selectable.py +198 -0
  390. cli/infrastructure/validation/__init__.py +0 -0
  391. cli/infrastructure/validation/tag_validation.py +74 -0
  392. cli/main.py +759 -0
  393. cli/tui/__init__.py +0 -0
  394. cli/tui/app.py +199 -0
  395. cli/tui/app_store.py +73 -0
  396. cli/tui/chat/__init__.py +0 -0
  397. cli/tui/chat/commands/__init__.py +0 -0
  398. cli/tui/chat/commands/base.py +65 -0
  399. cli/tui/chat/commands/create_session.py +135 -0
  400. cli/tui/chat/commands/load_session.py +119 -0
  401. cli/tui/chat/commands/regenerate.py +120 -0
  402. cli/tui/chat/commands/reload_session.py +63 -0
  403. cli/tui/chat/commands/send_message.py +190 -0
  404. cli/tui/chat/commands/undo.py +66 -0
  405. cli/tui/chat/editor.py +71 -0
  406. cli/tui/chat/messages.py +223 -0
  407. cli/tui/chat/pane.py +141 -0
  408. cli/tui/chat/renderers/__init__.py +0 -0
  409. cli/tui/chat/renderers/base.py +72 -0
  410. cli/tui/chat/renderers/markdown.py +250 -0
  411. cli/tui/chat/renderers/plain.py +83 -0
  412. cli/tui/chat/screen.py +1155 -0
  413. cli/tui/chat/services/__init__.py +0 -0
  414. cli/tui/chat/services/injection.py +386 -0
  415. cli/tui/chat/services/name_generator.py +256 -0
  416. cli/tui/chat/slash_commands.py +424 -0
  417. cli/tui/chat/store.py +280 -0
  418. cli/tui/chat/types.py +220 -0
  419. cli/tui/chat/widgets/__init__.py +0 -0
  420. cli/tui/chat/widgets/chat_header.py +75 -0
  421. cli/tui/chat/widgets/chat_input.py +362 -0
  422. cli/tui/chat/widgets/injection_popup.py +161 -0
  423. cli/tui/chat/widgets/message_display.py +287 -0
  424. cli/tui/chat/widgets/session_sidebar.py +214 -0
  425. cli/tui/chat/widgets/welcome_screen.py +290 -0
  426. cli/tui/screens/__init__.py +0 -0
  427. cli/tui/screens/agents.py +344 -0
  428. cli/tui/screens/base.py +301 -0
  429. cli/tui/screens/content.py +508 -0
  430. cli/tui/screens/dashboard.py +89 -0
  431. cli/tui/screens/models.py +96 -0
  432. cli/tui/screens/nav_screen.py +186 -0
  433. cli/tui/screens/schemas.py +522 -0
  434. cli/tui/screens/templates.py +734 -0
  435. cli/tui/screens/tool_configs.py +335 -0
  436. cli/tui/styles/__init__.py +0 -0
  437. cli/tui/widgets/__init__.py +0 -0
  438. cli/tui/widgets/agent_create_modal.py +139 -0
  439. cli/tui/widgets/agent_form_modal.py +659 -0
  440. cli/tui/widgets/agent_update_modal.py +299 -0
  441. cli/tui/widgets/base_form_modal.py +77 -0
  442. cli/tui/widgets/confirm_modal.py +75 -0
  443. cli/tui/widgets/help_modal.py +145 -0
  444. cli/tui/widgets/new_session_modal.py +328 -0
  445. cli/tui/widgets/schema_create_modal.py +271 -0
  446. cli/tui/widgets/schema_update_modal.py +188 -0
  447. cli/tui/widgets/status_footer.py +147 -0
  448. cli/tui/widgets/template_create_modal.py +502 -0
  449. cli/tui/widgets/template_update_modal.py +308 -0
  450. cli/tui/widgets/tool_config_create_modal.py +216 -0
  451. cli/tui/widgets/tool_config_update_modal.py +208 -0
cli/tui/chat/types.py ADDED
@@ -0,0 +1,220 @@
1
+ """Type definitions for the chat module.
2
+
3
+ Contains enums, protocols, and dataclasses used throughout the chat system.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from enum import Enum, auto
9
+ from typing import Protocol, Union
10
+ from uuid import UUID
11
+
12
+ from rich.console import RenderableType
13
+ from alloy_runtime_types.dtos.sessions import MessageResponse, SessionSummary
14
+
15
+
16
+ class ChatPhase(Enum):
17
+ """Current phase of chat interaction.
18
+
19
+ Used to control UI state and prevent conflicting operations.
20
+ """
21
+
22
+ IDLE = auto()
23
+ SENDING = auto()
24
+ STREAMING = auto()
25
+ LOADING_SESSION = auto()
26
+ LOADING_SESSIONS = auto()
27
+ CREATING_SESSION = auto()
28
+
29
+
30
+ class RenderMode(Enum):
31
+ """Rendering mode for message display."""
32
+
33
+ MARKDOWN = auto()
34
+ PLAIN = auto()
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class SessionContext:
39
+ """Immutable context for the active chat session.
40
+
41
+ Supports two modes:
42
+ - Agent mode: agent_id and agent_name are set, model comes from agent config
43
+ - Model mode: provider_key and model_name are set, with optional system_instruction
44
+ """
45
+
46
+ session_id: UUID
47
+ session_name: str | None
48
+ agent_id: UUID | None
49
+ agent_name: str | None
50
+ message_count: int
51
+ # Model info for direct model mode (no agent)
52
+ provider_key: str | None = None
53
+ model_name: str | None = None
54
+ system_instruction: str | None = None
55
+
56
+ @property
57
+ def is_model_mode(self) -> bool:
58
+ """Check if this session uses direct model mode."""
59
+ return self.provider_key is not None and self.model_name is not None
60
+
61
+ @property
62
+ def is_agent_mode(self) -> bool:
63
+ """Check if this session uses agent mode."""
64
+ return self.agent_id is not None
65
+
66
+ @property
67
+ def display_name(self) -> str:
68
+ """Get the display name for the assistant (agent name or model identifier)."""
69
+ if self.agent_name:
70
+ return self.agent_name
71
+ if self.provider_key and self.model_name:
72
+ return f"{self.provider_key}:{self.model_name}"
73
+ if self.model_name:
74
+ return self.model_name
75
+ return "Assistant"
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class ChatState:
80
+ """Immutable snapshot of chat state.
81
+
82
+ All fields are immutable (frozen dataclass with tuple for collections).
83
+ State transitions create new instances rather than mutating.
84
+ """
85
+
86
+ phase: ChatPhase = ChatPhase.IDLE
87
+ session: SessionContext | None = None
88
+ messages: tuple[MessageResponse, ...] = ()
89
+ streaming_content: str = ""
90
+ streaming_thinking_content: str = "" # Reasoning/thinking content during streaming
91
+ error: str | None = None
92
+
93
+ # Optimistic UI: user message shown before server confirms
94
+ pending_user_message: str | None = None
95
+
96
+ # Session sidebar state
97
+ sessions: tuple[SessionSummary, ...] = ()
98
+ session_search_query: str = ""
99
+
100
+ # UI state
101
+ sidebar_visible: bool = False # Default: hidden for cleaner initial view
102
+ thinking_collapsed: bool = False # Whether thinking blocks are collapsed
103
+
104
+ # Stream cancellation
105
+ cancel_requested: bool = False # Set to True to cancel ongoing stream
106
+
107
+ # Rendering
108
+ render_mode: RenderMode = RenderMode.MARKDOWN
109
+
110
+
111
+ class MessageRenderer(Protocol):
112
+ """Protocol for message rendering strategies.
113
+
114
+ Implement this to create custom renderers (markdown, plain text, etc.).
115
+ """
116
+
117
+ def render(
118
+ self,
119
+ messages: tuple[MessageResponse, ...],
120
+ streaming_content: str = "",
121
+ streaming_thinking_content: str = "",
122
+ pending_user_message: str | None = None,
123
+ assistant_name: str = "Assistant",
124
+ is_streaming: bool = False,
125
+ thinking_collapsed: bool = False,
126
+ ) -> Union[str, RenderableType]:
127
+ """Render messages to a displayable format.
128
+
129
+ Args:
130
+ messages: Tuple of message responses to render.
131
+ streaming_content: Partial content being streamed (appended to end).
132
+ streaming_thinking_content: Partial thinking/reasoning content being streamed.
133
+ pending_user_message: User message shown optimistically before server confirms.
134
+ assistant_name: Name to display for assistant (agent name or model identifier).
135
+ is_streaming: Whether currently streaming a response (shows spinner).
136
+ thinking_collapsed: Whether thinking blocks should be collapsed.
137
+
138
+ Returns:
139
+ Formatted content suitable for display in a Static widget.
140
+ Can be a string (for plain text) or a Rich renderable (for markdown).
141
+ """
142
+ ...
143
+
144
+
145
+ def format_time(dt: datetime) -> str:
146
+ """Format datetime for message display."""
147
+ return dt.strftime("%H:%M")
148
+
149
+
150
+ def format_session_time(dt: datetime) -> str:
151
+ """Format datetime for session sidebar display."""
152
+ now = datetime.now(dt.tzinfo)
153
+ if dt.date() == now.date():
154
+ return dt.strftime("%H:%M")
155
+ return dt.strftime("%m/%d")
156
+
157
+
158
+ def extract_message_text(msg: MessageResponse) -> str:
159
+ """Extract text content from a message's content parts (excludes thinking content)."""
160
+ text_parts: list[str] = []
161
+ for part in msg.content_parts:
162
+ # Skip thinking content - it's extracted separately
163
+ if part.content_type_id == "thinking":
164
+ continue
165
+ if part.content_text:
166
+ text_parts.append(part.content_text)
167
+ return "".join(text_parts)
168
+
169
+
170
+ def extract_message_text_for_display(
171
+ msg: MessageResponse,
172
+ *,
173
+ truncate_threshold: int = 2000,
174
+ ) -> tuple[str, bool]:
175
+ """Extract text content from a message, truncating long content for display.
176
+
177
+ Very long content (e.g., from rendered @fragment() injections) can overwhelm
178
+ the TUI. This function truncates such content while preserving the head and
179
+ tail so users can see what was injected.
180
+
181
+ Args:
182
+ msg: The message to extract text from
183
+ truncate_threshold: Character count above which to truncate each part
184
+
185
+ Returns:
186
+ Tuple of (text, was_any_truncated)
187
+ - text: The extracted text with long parts truncated
188
+ - was_any_truncated: True if any content was truncated
189
+ """
190
+ from cli.infrastructure.tui.formatters import truncate_long_content
191
+
192
+ text_parts: list[str] = []
193
+ any_truncated = False
194
+
195
+ for part in msg.content_parts:
196
+ # Skip thinking content - it's extracted separately
197
+ if part.content_type_id == "thinking":
198
+ continue
199
+ if part.content_text:
200
+ truncated_text, was_truncated = truncate_long_content(
201
+ part.content_text,
202
+ threshold=truncate_threshold,
203
+ )
204
+ text_parts.append(truncated_text)
205
+ if was_truncated:
206
+ any_truncated = True
207
+
208
+ return "".join(text_parts), any_truncated
209
+
210
+
211
+ def extract_thinking_content(msg: MessageResponse) -> str | None:
212
+ """Extract thinking/reasoning content from a message's content parts.
213
+
214
+ Returns:
215
+ The thinking content if present, None otherwise.
216
+ """
217
+ for part in msg.content_parts:
218
+ if part.content_type_id == "thinking" and part.content_text:
219
+ return part.content_text
220
+ return None
File without changes
@@ -0,0 +1,75 @@
1
+ """Chat header widget for displaying session info."""
2
+
3
+ from typing import Callable
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Vertical
7
+ from textual.widget import Widget
8
+ from textual.widgets import Static
9
+
10
+ from cli.tui.chat.store import ChatStore
11
+ from cli.tui.chat.types import ChatState
12
+
13
+
14
+ class ChatHeader(Widget):
15
+ """Widget for displaying current session information.
16
+
17
+ Shows:
18
+ - Session name
19
+ - Agent name (if configured)
20
+ - Message count
21
+ """
22
+
23
+ def __init__(self, store: ChatStore) -> None:
24
+ """Initialize the chat header.
25
+
26
+ Args:
27
+ store: ChatStore instance for state management.
28
+ """
29
+ super().__init__()
30
+ self._store = store
31
+ self._unsubscribe: Callable[[], None] | None = None
32
+
33
+ def compose(self) -> ComposeResult:
34
+ """Compose the header layout."""
35
+ with Vertical(id="chat-header"):
36
+ yield Static(
37
+ "[bold]Select a session[/] or press [cyan]Ctrl+N[/] to start a new chat",
38
+ id="session-info",
39
+ )
40
+
41
+ def on_mount(self) -> None:
42
+ """Subscribe to store on mount."""
43
+ self._unsubscribe = self._store.subscribe(self._on_state_change)
44
+
45
+ def on_unmount(self) -> None:
46
+ """Unsubscribe from store on unmount."""
47
+ if self._unsubscribe:
48
+ self._unsubscribe()
49
+
50
+ def _on_state_change(self, state: ChatState) -> None:
51
+ """React to store state changes."""
52
+ session_info = self.query_one("#session-info", Static)
53
+
54
+ if state.session is None:
55
+ session_info.update(
56
+ "[bold]Select a session[/] or press [cyan]Ctrl+N[/] to start a new chat"
57
+ )
58
+ else:
59
+ name = state.session.session_name or "Untitled Chat"
60
+ agent_info = (
61
+ f" | Agent: {state.session.agent_name}"
62
+ if state.session.agent_name
63
+ else ""
64
+ )
65
+ msg_count = state.session.message_count
66
+ session_info.update(
67
+ f"[bold]{name}[/]{agent_info} | [dim]{msg_count} messages[/]"
68
+ )
69
+
70
+ def reset(self) -> None:
71
+ """Reset header to default state."""
72
+ session_info = self.query_one("#session-info", Static)
73
+ session_info.update(
74
+ "[bold]Select a session[/] or press [cyan]Ctrl+N[/] to start a new chat"
75
+ )
@@ -0,0 +1,362 @@
1
+ """Chat input widget for composing and sending messages."""
2
+
3
+ from typing import TYPE_CHECKING, Callable
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.binding import Binding
8
+ from textual.containers import Vertical
9
+ from textual.events import Key
10
+ from textual.widget import Widget
11
+ from textual.widgets import TextArea
12
+
13
+ from cli.infrastructure.injection.parser import detect_partial_injection, has_injections
14
+ from cli.infrastructure.tui.selectable import CopyOnSelectTextArea
15
+ from cli.tui.chat.messages import (
16
+ CopyToClipboardRequested,
17
+ OpenEditorRequested,
18
+ SendMessageRequested,
19
+ SlashCommandExecuted,
20
+ UnknownSlashCommand,
21
+ )
22
+ from cli.tui.chat.services.injection import InjectionService
23
+ from cli.tui.chat.slash_commands import SlashContext, get_default_registry
24
+ from cli.tui.chat.store import ChatStore
25
+ from cli.tui.chat.types import ChatPhase, ChatState
26
+ from cli.tui.chat.widgets.injection_popup import InjectionSuggestionPopup
27
+ from alloy_runtime_sdk.logging.config import get_logger
28
+
29
+ logger = get_logger(__name__)
30
+
31
+ if TYPE_CHECKING:
32
+ from cli.tui.app import AlloyRuntimeApp
33
+
34
+
35
+ class ChatTextArea(CopyOnSelectTextArea):
36
+ """Custom TextArea with send key bindings and copy-on-select.
37
+
38
+ Key handling for injection popup is done by the parent ChatInput widget.
39
+ """
40
+
41
+ BINDINGS = [
42
+ Binding("ctrl+d", "submit", "Send", show=False, priority=True),
43
+ Binding("ctrl+e", "open_editor", "Editor", show=False, priority=True),
44
+ Binding("ctrl+y", "copy_last", "Copy Last", show=False, priority=True),
45
+ ]
46
+
47
+ def action_submit(self) -> None:
48
+ """Post a submit request to parent."""
49
+ self.post_message(SendMessageRequested(self.text.strip()))
50
+
51
+ def action_open_editor(self) -> None:
52
+ """Request to open current text in external editor."""
53
+ self.post_message(OpenEditorRequested(self.text))
54
+
55
+ def action_copy_last(self) -> None:
56
+ """Request to copy last assistant message - bubbles up to ChatScreen."""
57
+ self.post_message(CopyToClipboardRequested("", "_copy_last_request_"))
58
+
59
+ async def _on_key(self, event: Key) -> None:
60
+ """Handle keys, letting parent intercept for popup control."""
61
+ # Let the parent handle the key first
62
+ await super()._on_key(event)
63
+ # Then ensure cursor is visible
64
+ self.call_after_refresh(self._ensure_cursor_visible)
65
+
66
+ def _ensure_cursor_visible(self) -> None:
67
+ """Scroll the TextArea to ensure the cursor is visible."""
68
+ self.scroll_cursor_visible(animate=False)
69
+
70
+
71
+ class ChatInput(Widget):
72
+ """Widget for composing chat messages with injection autocomplete.
73
+
74
+ This widget controls the injection popup - the popup is just a display,
75
+ all keyboard handling happens here. Focus stays on the text area.
76
+
77
+ Features:
78
+ - Multi-line text input with auto-grow
79
+ - Send with Ctrl+D
80
+ - Open in external editor with Ctrl+E
81
+ - Slash command parsing (/help, /clear, /copy, etc.)
82
+ - Injection autocomplete (@fragment, @text, @json, @schema)
83
+ - Arrow keys navigate popup while focus stays in input
84
+ """
85
+
86
+ app: "AlloyRuntimeApp"
87
+
88
+ DEFAULT_CSS = """
89
+ ChatInput {
90
+ height: auto;
91
+ }
92
+
93
+ ChatInput #input-container {
94
+ height: auto;
95
+ }
96
+ """
97
+
98
+ # Commands that are handled directly in ChatInput (don't need screen)
99
+ _IMMEDIATE_COMMANDS = {
100
+ "clear",
101
+ "c",
102
+ "cls",
103
+ "help",
104
+ "h",
105
+ "?",
106
+ "copy",
107
+ "cp",
108
+ "y",
109
+ "markdown",
110
+ "md",
111
+ "plain", # Render mode toggle
112
+ }
113
+
114
+ def __init__(
115
+ self,
116
+ store: ChatStore,
117
+ injection_service: InjectionService | None = None,
118
+ ) -> None:
119
+ """Initialize the chat input."""
120
+ super().__init__()
121
+ self._store = store
122
+ self._injection_service = injection_service
123
+ self._registry = get_default_registry()
124
+ self._unsubscribe: Callable[[], None] | None = None
125
+ # Track current injection trigger for replacement
126
+ self._current_trigger_start: int = 0
127
+ self._current_trigger_end: int = 0
128
+ # Track if popup was dismissed to prevent re-triggering
129
+ self._popup_dismissed_text: str = ""
130
+
131
+ def compose(self) -> ComposeResult:
132
+ """Compose the input layout."""
133
+ with Vertical(id="input-container"):
134
+ yield ChatTextArea(id="chat-input")
135
+
136
+ def on_mount(self) -> None:
137
+ """Subscribe to store on mount."""
138
+ self._unsubscribe = self._store.subscribe(self._on_state_change)
139
+
140
+ def on_unmount(self) -> None:
141
+ """Unsubscribe from store on unmount."""
142
+ if self._unsubscribe:
143
+ self._unsubscribe()
144
+
145
+ def _on_state_change(self, state: ChatState) -> None:
146
+ """React to store state changes."""
147
+ # State changes are handled by store subscribers
148
+ # No local status bar to update anymore
149
+ pass
150
+
151
+ def _get_popup(self) -> InjectionSuggestionPopup | None:
152
+ """Get the injection popup from the parent ChatPane."""
153
+ try:
154
+ return self.screen.query_one("#injection-popup", InjectionSuggestionPopup)
155
+ except Exception:
156
+ return None
157
+
158
+ def on_key(self, event: Key) -> None:
159
+ """Intercept keys when popup is visible for navigation."""
160
+ popup = self._get_popup()
161
+ if not popup or not popup.is_visible:
162
+ return # Let key propagate normally
163
+
164
+ # Handle popup navigation keys
165
+ if event.key == "up":
166
+ event.stop()
167
+ popup.highlight_previous()
168
+ elif event.key == "down":
169
+ event.stop()
170
+ popup.highlight_next()
171
+ elif event.key == "tab" or event.key == "enter":
172
+ event.stop()
173
+ self._accept_suggestion(popup)
174
+ elif event.key == "escape":
175
+ event.stop()
176
+ self._dismiss_popup(popup)
177
+
178
+ def _accept_suggestion(self, popup: InjectionSuggestionPopup) -> None:
179
+ """Accept the currently highlighted suggestion."""
180
+ completion = popup.get_selected()
181
+ if not completion:
182
+ self._dismiss_popup(popup)
183
+ return
184
+
185
+ text_area = self.query_one("#chat-input", ChatTextArea)
186
+ current_text = text_area.text
187
+
188
+ # Replace the partial injection with the completed one
189
+ new_text = (
190
+ current_text[: self._current_trigger_start]
191
+ + completion.text
192
+ + current_text[self._current_trigger_end :]
193
+ )
194
+
195
+ # Update text area
196
+ text_area.text = new_text
197
+
198
+ # Move cursor to end of inserted text
199
+ new_cursor_pos = self._current_trigger_start + len(completion.text)
200
+ lines = new_text.split("\n")
201
+ row = 0
202
+ remaining = new_cursor_pos
203
+ for i, line in enumerate(lines):
204
+ if remaining <= len(line):
205
+ row = i
206
+ break
207
+ remaining -= len(line) + 1
208
+ row = i + 1
209
+
210
+ col = min(remaining, len(lines[row]) if row < len(lines) else 0)
211
+ text_area.move_cursor((row, col))
212
+
213
+ # Hide popup
214
+ popup.hide()
215
+ self._popup_dismissed_text = new_text # Prevent re-trigger
216
+
217
+ def _dismiss_popup(self, popup: InjectionSuggestionPopup) -> None:
218
+ """Dismiss the popup without accepting."""
219
+ popup.hide()
220
+ text_area = self.query_one("#chat-input", ChatTextArea)
221
+ self._popup_dismissed_text = text_area.text # Prevent re-trigger on same text
222
+
223
+ @on(TextArea.Changed, "#chat-input")
224
+ def _on_text_changed(self, event: TextArea.Changed) -> None:
225
+ """Check for injection triggers when text changes."""
226
+ text_area = event.text_area
227
+ text = text_area.text
228
+
229
+ # Scroll to keep cursor visible
230
+ text_area.call_after_refresh(
231
+ lambda: text_area.scroll_cursor_visible(animate=False)
232
+ )
233
+
234
+ # If text changed significantly, allow popup to trigger again
235
+ if text != self._popup_dismissed_text:
236
+ self._popup_dismissed_text = ""
237
+
238
+ # Check for injection trigger
239
+ if not self._injection_service:
240
+ return
241
+
242
+ # Get cursor position
243
+ cursor_location = text_area.cursor_location
244
+ lines = text.split("\n")
245
+ cursor_pos = sum(len(lines[i]) + 1 for i in range(cursor_location[0]))
246
+ cursor_pos += cursor_location[1]
247
+
248
+ partial = detect_partial_injection(text, cursor_pos)
249
+ popup = self._get_popup()
250
+
251
+ if partial and text != self._popup_dismissed_text:
252
+ # Store trigger position for replacement
253
+ self._current_trigger_start = partial.start
254
+ self._current_trigger_end = partial.end
255
+
256
+ # Get completions
257
+ completions = self._injection_service.get_completions(
258
+ partial.injection_type,
259
+ partial.partial_identifier,
260
+ )
261
+
262
+ # Show popup
263
+ if popup:
264
+ popup.show_suggestions(
265
+ completions,
266
+ partial.injection_type,
267
+ filter_text=partial.partial_identifier,
268
+ )
269
+ elif popup and popup.is_visible:
270
+ # No trigger detected, hide popup if visible
271
+ popup.hide()
272
+
273
+ def on_send_message_requested(self, event: SendMessageRequested) -> None:
274
+ """Handle send request from TextArea - check for slash commands first."""
275
+ content = event.content
276
+
277
+ # Hide popup if visible
278
+ popup = self._get_popup()
279
+ if popup and popup.is_visible:
280
+ popup.hide()
281
+
282
+ # Check for slash command
283
+ parse_result = self._registry.parse(content)
284
+
285
+ if parse_result.is_command:
286
+ event.stop()
287
+ text_area = self.query_one("#chat-input", ChatTextArea)
288
+ text_area.clear()
289
+
290
+ if parse_result.command is None:
291
+ self.post_message(UnknownSlashCommand(content))
292
+ return
293
+
294
+ cmd_name = parse_result.command.name
295
+ is_immediate = cmd_name in self._IMMEDIATE_COMMANDS
296
+ logger.debug(
297
+ "slash_command_parsed",
298
+ command=cmd_name,
299
+ is_immediate=is_immediate,
300
+ args=parse_result.args or None,
301
+ )
302
+
303
+ if is_immediate:
304
+ context = SlashContext(store=self._store, app=self.app)
305
+ parse_result.command.execute(parse_result.args, context)
306
+ logger.debug("slash_command_executed_immediate", command=cmd_name)
307
+ else:
308
+ self.post_message(
309
+ SlashCommandExecuted(parse_result.command.name, parse_result.args)
310
+ )
311
+ return
312
+
313
+ # Regular message - validate and process
314
+ state = self._store.state
315
+ session = state.session
316
+
317
+ if state.phase != ChatPhase.IDLE:
318
+ self.app.notify("Please wait for the current operation", severity="warning")
319
+ event.stop()
320
+ return
321
+
322
+ if not session:
323
+ self.app.notify(
324
+ "No active session. Press Ctrl+N to create one.", severity="warning"
325
+ )
326
+ event.stop()
327
+ return
328
+
329
+ if not session.is_agent_mode and not session.is_model_mode:
330
+ self.app.notify(
331
+ "No agent or model configured for this session", severity="warning"
332
+ )
333
+ event.stop()
334
+ return
335
+
336
+ if not content:
337
+ event.stop()
338
+ return
339
+
340
+ # Clear input and let message bubble to parent
341
+ text_area = self.query_one("#chat-input", ChatTextArea)
342
+ text_area.clear()
343
+
344
+ def focus_input(self) -> None:
345
+ """Focus the text input."""
346
+ self.query_one("#chat-input", ChatTextArea).focus()
347
+
348
+ def clear_input(self) -> None:
349
+ """Clear the text input."""
350
+ self.query_one("#chat-input", ChatTextArea).clear()
351
+
352
+ def get_text(self) -> str:
353
+ """Get the current text content."""
354
+ return self.query_one("#chat-input", ChatTextArea).text
355
+
356
+ def set_injection_service(self, service: InjectionService) -> None:
357
+ """Set the injection service for autocomplete."""
358
+ self._injection_service = service
359
+
360
+ def has_injections(self) -> bool:
361
+ """Check if current text contains injection patterns."""
362
+ return has_injections(self.get_text())