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
@@ -0,0 +1,734 @@
1
+ """Templates screen for browsing and managing templates.
2
+
3
+ This screen provides:
4
+ - Search and browse templates
5
+ - View template content in preview panel
6
+ - Edit templates and create versions
7
+ - Copy template names and content
8
+ """
9
+
10
+ import os
11
+ import subprocess
12
+ import tempfile
13
+ from dataclasses import dataclass, field
14
+ from typing import TYPE_CHECKING, Any
15
+ from uuid import UUID
16
+
17
+ from textual import on, work
18
+ from textual.app import ComposeResult
19
+ from textual.binding import Binding
20
+ from textual.containers import Horizontal, Vertical, VerticalScroll
21
+ from textual.widgets import Button, DataTable, Input, Static
22
+
23
+ from alloy_runtime_types.dtos.templates import (
24
+ CreateTemplateVersionRequest,
25
+ RenderTemplateRequest,
26
+ TemplateListItemResponse,
27
+ UpdateTemplateRequest,
28
+ )
29
+
30
+ from cli.infrastructure.forms.resolution_modal import ResolutionModal
31
+ from cli.infrastructure.macro_parser import (
32
+ MacroMatch,
33
+ build_jinja_replacement,
34
+ compile_content,
35
+ deduplicate_macros,
36
+ parse_macros,
37
+ )
38
+ from cli.infrastructure.tui.clipboard import copy_to_clipboard
39
+ from cli.infrastructure.tui.formatters import format_datetime, format_tags_list
40
+ from cli.tui.screens.base import BrowserWidget
41
+ from cli.tui.screens.nav_screen import NavScreen
42
+ from cli.tui.widgets.template_create_modal import (
43
+ TemplateCreateModal,
44
+ TemplateCreateResult,
45
+ )
46
+ from cli.tui.widgets.template_update_modal import (
47
+ TemplateUpdateModal,
48
+ TemplateUpdateResult,
49
+ )
50
+
51
+ if TYPE_CHECKING:
52
+ from cli.tui.app import AlloyRuntimeApp
53
+
54
+
55
+ def _macro_list_factory() -> list[MacroMatch]:
56
+ return []
57
+
58
+
59
+ def _replacements_factory() -> dict[str, str]:
60
+ return {}
61
+
62
+
63
+ @dataclass
64
+ class PendingEdit:
65
+ """State for a template edit waiting on macro resolution."""
66
+
67
+ template_id: str
68
+ content: str
69
+ is_new_version: bool
70
+ all_macros: list[MacroMatch] = field(default_factory=_macro_list_factory)
71
+ pending_macros: list[MacroMatch] = field(default_factory=_macro_list_factory)
72
+ replacements: dict[str, str] = field(default_factory=_replacements_factory)
73
+ current_macro_index: int = 0
74
+
75
+
76
+ class TemplatesWidget(BrowserWidget[TemplateListItemResponse]):
77
+ """Widget for browsing and managing templates."""
78
+
79
+ app: "AlloyRuntimeApp"
80
+
81
+ TITLE = "Templates"
82
+ TABLE_COLUMNS = ["Type", "Name", "Visibility", "Version", "Tags", "Updated"]
83
+
84
+ def __init__(self, **kwargs: Any) -> None:
85
+ super().__init__(**kwargs)
86
+ self._pending_edit: PendingEdit | None = None
87
+
88
+ def compose(self) -> ComposeResult:
89
+ """Compose the browser layout with a New Template button."""
90
+ with Vertical(id="browser-root"):
91
+ with Vertical(id="search-container"):
92
+ with Horizontal(id="search-row"):
93
+ yield Input(
94
+ placeholder=f"Search {self.TITLE.lower()}...",
95
+ id="search-input",
96
+ )
97
+ yield Button("+ New", id="new-template-btn", variant="primary")
98
+ yield Static("Loading...", id="status-bar")
99
+
100
+ with Horizontal(id="main-container"):
101
+ with Vertical(id="list-container"):
102
+ yield DataTable(id="data-table", cursor_type="row")
103
+
104
+ with Vertical(id="detail-container"):
105
+ yield Static("Preview", id="preview-title")
106
+ with VerticalScroll(id="preview-scroll"):
107
+ yield Static(
108
+ "Select an item to view details",
109
+ id="preview-content",
110
+ )
111
+
112
+ @on(Button.Pressed, "#new-template-btn")
113
+ def _on_new_template_btn_pressed(self, event: Button.Pressed) -> None:
114
+ """Handle New Template button press."""
115
+ self.open_new_template_modal()
116
+
117
+ async def _fetch_items(
118
+ self, query: str
119
+ ) -> tuple[list[TemplateListItemResponse], int]:
120
+ """Fetch templates from the API."""
121
+ client = await self._ensure_client()
122
+ response = await client.list_templates(
123
+ search=query if query else None,
124
+ limit=50,
125
+ include_content=True,
126
+ )
127
+ return response.templates, response.total_count
128
+
129
+ def _format_row(self, item: TemplateListItemResponse) -> tuple[str, ...]:
130
+ """Format a template as a table row."""
131
+ tags_dicts = [{"tag_path": t.tag_path} for t in item.tags]
132
+ name = item.name[:27] + "..." if len(item.name) > 30 else item.name
133
+ return (
134
+ item.content_type_id,
135
+ name,
136
+ item.visibility_id,
137
+ f"v{item.latest_version_number}",
138
+ format_tags_list(tags_dicts, max_display=2),
139
+ format_datetime(item.updated_at),
140
+ )
141
+
142
+ def _update_preview(self, item: TemplateListItemResponse) -> None:
143
+ """Update the preview pane with template details."""
144
+ preview = self.query_one("#preview-content", Static)
145
+
146
+ tags_display = ", ".join(t.tag_path for t in item.tags) if item.tags else "-"
147
+ vars_display = (
148
+ ", ".join(item.required_variables) if item.required_variables else "-"
149
+ )
150
+
151
+ created = format_datetime(item.created_at)
152
+ updated = format_datetime(item.updated_at)
153
+ version_created = format_datetime(item.latest_version_created_at)
154
+
155
+ content_display = item.latest_version_content or "(content not available)"
156
+ max_content_length = 2000
157
+ if len(content_display) > max_content_length:
158
+ content_display = content_display[:max_content_length] + "\n... (truncated)"
159
+
160
+ preview_text = f"""[bold]Template ID:[/] {item.id}
161
+ [bold]Version ID:[/] {item.latest_version_id}
162
+ [bold]Name:[/] {item.name}
163
+ [bold]Type:[/] {item.content_type_id}
164
+ [bold]Visibility:[/] {item.visibility_id}
165
+ [bold]Version:[/] v{item.latest_version_number}
166
+
167
+ [bold]Description:[/]
168
+ {item.description or "(no description)"}
169
+
170
+ [bold]Required Variables:[/] {vars_display}
171
+ [bold]Tags:[/] {tags_display}
172
+
173
+ [bold]Created:[/] {created}
174
+ [bold]Updated:[/] {updated}
175
+ [bold]Version Created:[/] {version_created}
176
+
177
+ [bold]Content:[/]
178
+ {content_display}"""
179
+
180
+ preview.update(preview_text)
181
+
182
+ # =========================================================================
183
+ # Public methods for parent Screen to call
184
+ # =========================================================================
185
+
186
+ def copy_template_name(self) -> None:
187
+ """Copy the template name to clipboard."""
188
+ template = self.get_selected_item()
189
+ if not template:
190
+ self.app.notify("No template selected", severity="warning")
191
+ return
192
+
193
+ copy_to_clipboard(self.app, template.name, "Template name")
194
+
195
+ def copy_template_content(self) -> None:
196
+ """Copy the template content to clipboard."""
197
+ template = self.get_selected_item()
198
+ if not template:
199
+ self.app.notify("No template to copy", severity="warning")
200
+ return
201
+
202
+ content = template.latest_version_content
203
+ if content:
204
+ copy_to_clipboard(self.app, content, "Template content")
205
+ else:
206
+ self.app.notify("No content available", severity="warning")
207
+
208
+ def edit_template_content(self) -> None:
209
+ """Open template content in $EDITOR for editing.
210
+
211
+ For single-version templates, updates the existing version.
212
+ For multi-version templates, automatically creates a new version.
213
+ If content changes and contains macros, opens macro resolution flow before saving.
214
+ """
215
+ template = self.get_selected_item()
216
+ if not template:
217
+ self.app.notify("No template to edit", severity="warning")
218
+ return
219
+
220
+ # Check if template has multiple versions
221
+ is_multi_version = template.latest_version_number > 1
222
+
223
+ if is_multi_version:
224
+ # For multi-version templates, edit will create a new version
225
+ self.app.notify(
226
+ f"Template has {template.latest_version_number} versions - edit will create v{template.latest_version_number + 1}",
227
+ severity="information",
228
+ )
229
+
230
+ new_content = self._open_editor_for_template(
231
+ template.name,
232
+ template.latest_version_content or "",
233
+ )
234
+ if new_content is None:
235
+ return
236
+
237
+ if new_content == (template.latest_version_content or ""):
238
+ self.app.notify("No changes made", severity="information")
239
+ return
240
+
241
+ self._handle_content_after_edit(
242
+ template_id=str(template.id),
243
+ content=new_content,
244
+ is_new_version=is_multi_version, # Use versioning flow for multi-version templates
245
+ )
246
+
247
+ def create_template_version(self) -> None:
248
+ """Create a new version of the template in $EDITOR.
249
+
250
+ Opens the current content in the editor. If content changes and
251
+ contains macros, opens macro resolution flow before creating version.
252
+ """
253
+ template = self.get_selected_item()
254
+ if not template:
255
+ self.app.notify("No template selected", severity="warning")
256
+ return
257
+
258
+ version_hint = f"_v{template.latest_version_number + 1}"
259
+ new_content = self._open_editor_for_template(
260
+ f"{template.name}{version_hint}",
261
+ template.latest_version_content or "",
262
+ )
263
+ if new_content is None:
264
+ return
265
+
266
+ if new_content == (template.latest_version_content or ""):
267
+ self.app.notify(
268
+ "No changes made - version not created", severity="information"
269
+ )
270
+ return
271
+
272
+ self._handle_content_after_edit(
273
+ template_id=str(template.id),
274
+ content=new_content,
275
+ is_new_version=True,
276
+ )
277
+
278
+ def _open_editor_for_template(
279
+ self, name_hint: str, initial_content: str
280
+ ) -> str | None:
281
+ """Open external editor with template content.
282
+
283
+ Args:
284
+ name_hint: Name to include in temp file name
285
+ initial_content: Initial content for the editor
286
+
287
+ Returns:
288
+ Edited content, or None if editor failed
289
+ """
290
+ editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "vi"
291
+
292
+ try:
293
+ with tempfile.NamedTemporaryFile(
294
+ mode="w",
295
+ suffix=".jinja2",
296
+ prefix=f"template_{name_hint}_",
297
+ delete=False,
298
+ encoding="utf-8",
299
+ ) as f:
300
+ f.write(initial_content)
301
+ temp_path = f.name
302
+
303
+ with self.app.suspend():
304
+ subprocess.run([editor, temp_path], check=False)
305
+
306
+ with open(temp_path, encoding="utf-8") as f:
307
+ new_content = f.read()
308
+
309
+ try:
310
+ os.unlink(temp_path)
311
+ except OSError:
312
+ pass
313
+
314
+ return new_content
315
+
316
+ except Exception as e:
317
+ self.app.notify(f"Failed to open editor: {e}", severity="error")
318
+ return None
319
+
320
+ def _handle_content_after_edit(
321
+ self, template_id: str, content: str, is_new_version: bool
322
+ ) -> None:
323
+ """Handle content after editor closes - check for macros and save.
324
+
325
+ If the content contains macros, starts the macro resolution flow.
326
+ Otherwise, saves/creates version immediately.
327
+ """
328
+ all_macros = parse_macros(content)
329
+
330
+ if not all_macros:
331
+ # No macros - save immediately
332
+ if is_new_version:
333
+ self._create_template_version(template_id, content)
334
+ else:
335
+ self._save_template_content(template_id, content)
336
+ return
337
+
338
+ # Has macros - set up resolution flow
339
+ unique_macros = deduplicate_macros(all_macros)
340
+ self._pending_edit = PendingEdit(
341
+ template_id=template_id,
342
+ content=content,
343
+ is_new_version=is_new_version,
344
+ all_macros=all_macros,
345
+ pending_macros=unique_macros,
346
+ replacements={},
347
+ current_macro_index=0,
348
+ )
349
+
350
+ self.app.notify(
351
+ f"Found {len(unique_macros)} macro(s) to resolve",
352
+ severity="information",
353
+ )
354
+ self._start_macro_resolution()
355
+
356
+ def _start_macro_resolution(self) -> None:
357
+ """Start or continue the macro resolution flow."""
358
+ if self._pending_edit is None:
359
+ return
360
+
361
+ self._process_next_macro()
362
+
363
+ def _process_next_macro(self) -> None:
364
+ """Process the next macro in the resolution queue."""
365
+ if self._pending_edit is None:
366
+ return
367
+
368
+ if self._pending_edit.current_macro_index >= len(
369
+ self._pending_edit.pending_macros
370
+ ):
371
+ # All macros processed - finalize
372
+ self._finalize_edit()
373
+ return
374
+
375
+ macro = self._pending_edit.pending_macros[
376
+ self._pending_edit.current_macro_index
377
+ ]
378
+ self._open_resolution_modal_for_macro(macro)
379
+
380
+ @work(exclusive=True)
381
+ async def _open_resolution_modal_for_macro(self, macro: MacroMatch) -> None:
382
+ """Open the resolution modal for a specific macro."""
383
+ try:
384
+ client = await self._ensure_client()
385
+ self.app.push_screen(
386
+ ResolutionModal(
387
+ client=client,
388
+ macro_type=macro.macro_type,
389
+ query=macro.query,
390
+ ),
391
+ self._on_macro_resolved,
392
+ )
393
+ except Exception as e:
394
+ self.app.notify(f"Failed to open resolution modal: {e}", severity="error")
395
+ self._pending_edit = None
396
+
397
+ def _on_macro_resolved(self, result: dict[str, Any] | None) -> None:
398
+ """Callback when a macro resolution modal is dismissed."""
399
+ if self._pending_edit is None:
400
+ return
401
+
402
+ macro = self._pending_edit.pending_macros[
403
+ self._pending_edit.current_macro_index
404
+ ]
405
+
406
+ if result is not None:
407
+ # User selected a value - build jinja replacement
408
+ resolved_value = result["value"]
409
+ jinja_replacement = build_jinja_replacement(
410
+ macro.macro_type, resolved_value
411
+ )
412
+
413
+ # Apply to all matching macros (same type and query)
414
+ matching_count = 0
415
+ for m in self._pending_edit.all_macros:
416
+ if m.macro_type == macro.macro_type and m.query == macro.query:
417
+ self._pending_edit.replacements[m.original_text] = jinja_replacement
418
+ matching_count += 1
419
+
420
+ display = result.get("display", resolved_value)
421
+ if matching_count > 1:
422
+ self.app.notify(
423
+ f"Resolved {matching_count}x @{macro.macro_type}({macro.query}) -> {display}",
424
+ severity="information",
425
+ )
426
+ else:
427
+ self.app.notify(
428
+ f"Resolved @{macro.macro_type}({macro.query}) -> {display}",
429
+ severity="information",
430
+ )
431
+ else:
432
+ # User skipped - macro will remain unchanged
433
+ self.app.notify(
434
+ f"Skipped @{macro.macro_type}({macro.query})",
435
+ severity="warning",
436
+ )
437
+
438
+ # Move to next macro
439
+ self._pending_edit.current_macro_index += 1
440
+ self._process_next_macro()
441
+
442
+ def _finalize_edit(self) -> None:
443
+ """Compile content with replacements and save."""
444
+ if self._pending_edit is None:
445
+ return
446
+
447
+ # Apply all replacements to get final content
448
+ final_content = compile_content(
449
+ self._pending_edit.content,
450
+ self._pending_edit.replacements,
451
+ )
452
+
453
+ resolved_count = len(
454
+ {
455
+ (m.macro_type, m.query)
456
+ for m in self._pending_edit.all_macros
457
+ if m.original_text in self._pending_edit.replacements
458
+ }
459
+ )
460
+ total_unique = len(self._pending_edit.pending_macros)
461
+
462
+ if resolved_count < total_unique:
463
+ self.app.notify(
464
+ f"Resolved {resolved_count}/{total_unique} macros (skipped macros unchanged)",
465
+ severity="information",
466
+ )
467
+
468
+ # Save based on edit type
469
+ if self._pending_edit.is_new_version:
470
+ self._create_template_version(self._pending_edit.template_id, final_content)
471
+ else:
472
+ self._save_template_content(self._pending_edit.template_id, final_content)
473
+
474
+ # Clear pending state
475
+ self._pending_edit = None
476
+
477
+ def open_new_template_modal(self) -> None:
478
+ """Open the template creation modal."""
479
+ self._open_template_create_modal()
480
+
481
+ def update_template_metadata(self) -> None:
482
+ """Open the template update modal for editing metadata."""
483
+ template = self.get_selected_item()
484
+ if not template:
485
+ self.app.notify("No template selected", severity="warning")
486
+ return
487
+ self._open_template_update_modal(template)
488
+
489
+ def render_and_open(self) -> None:
490
+ """Render the template and open the result in $EDITOR.
491
+
492
+ Renders the selected template with empty variables and opens
493
+ the rendered output in the user's editor. No action is taken
494
+ on editor exit.
495
+ """
496
+ template = self.get_selected_item()
497
+ if not template:
498
+ self.app.notify("No template selected", severity="warning")
499
+ return
500
+
501
+ # Warn if template has required variables
502
+ if template.required_variables:
503
+ vars_list = ", ".join(template.required_variables)
504
+ self.app.notify(
505
+ f"Note: template has variables ({vars_list}) - rendering with empty values",
506
+ severity="warning",
507
+ )
508
+
509
+ # Delegate to async worker
510
+ self._render_and_open_template(str(template.id), template.name)
511
+
512
+ # =========================================================================
513
+ # Internal async operations
514
+ # =========================================================================
515
+
516
+ @work(exclusive=True)
517
+ async def _save_template_content(self, template_id: str, content: str) -> None:
518
+ """Save updated template content via API."""
519
+ try:
520
+ client = await self._ensure_client()
521
+ request = UpdateTemplateRequest(content=content)
522
+ await client.update_template(template_id, request)
523
+
524
+ self.app.notify("Template updated successfully", severity="information")
525
+ self._do_search(self._last_search)
526
+
527
+ except Exception as e:
528
+ error_msg = str(e)
529
+ if "multiple versions" in error_msg.lower() or "409" in error_msg:
530
+ self.app.notify(
531
+ "Cannot edit: template has multiple versions. Use Ctrl+V to create new version.",
532
+ severity="error",
533
+ )
534
+ else:
535
+ self.app.notify(f"Failed to update template: {e}", severity="error")
536
+
537
+ @work(exclusive=True)
538
+ async def _create_template_version(self, template_id: str, content: str) -> None:
539
+ """Create a new template version via API."""
540
+ try:
541
+ client = await self._ensure_client()
542
+ request = CreateTemplateVersionRequest(content=content)
543
+ await client.create_template_version(UUID(template_id), request)
544
+
545
+ self.app.notify("New version created successfully", severity="information")
546
+ self._do_search(self._last_search)
547
+
548
+ except Exception as e:
549
+ self.app.notify(f"Failed to create version: {e}", severity="error")
550
+
551
+ @work(exclusive=True)
552
+ async def _open_template_create_modal(self) -> None:
553
+ """Open the template creation modal."""
554
+ try:
555
+ client = await self._ensure_client()
556
+ self.app.push_screen(
557
+ TemplateCreateModal(client=client),
558
+ self._on_template_created,
559
+ )
560
+ except Exception as e:
561
+ self.app.notify(f"Failed to open template creator: {e}", severity="error")
562
+
563
+ def _on_template_created(self, result: TemplateCreateResult | None) -> None:
564
+ """Callback when template creation modal is dismissed."""
565
+ if result is not None:
566
+ self.app.notify(
567
+ f"Created template: {result.template_name} (v{result.version_number})",
568
+ severity="information",
569
+ )
570
+ # Refresh the template list
571
+ self._do_search(self._last_search)
572
+
573
+ @work(exclusive=True)
574
+ async def _open_template_update_modal(
575
+ self, template: TemplateListItemResponse
576
+ ) -> None:
577
+ """Open the template update modal."""
578
+ try:
579
+ client = await self._ensure_client()
580
+ self.app.push_screen(
581
+ TemplateUpdateModal(client=client, template=template),
582
+ self._on_template_updated,
583
+ )
584
+ except Exception as e:
585
+ self.app.notify(f"Failed to open template editor: {e}", severity="error")
586
+
587
+ def _on_template_updated(self, result: TemplateUpdateResult | None) -> None:
588
+ """Callback when template update modal is dismissed."""
589
+ if result is not None:
590
+ self.app.notify(
591
+ f"Updated template: {result.template_name}",
592
+ severity="information",
593
+ )
594
+ # Refresh the template list
595
+ self._do_search(self._last_search)
596
+
597
+ @work(exclusive=True)
598
+ async def _render_and_open_template(
599
+ self, template_id: str, template_name: str
600
+ ) -> None:
601
+ """Render template and open in $EDITOR."""
602
+ try:
603
+ client = await self._ensure_client()
604
+ request = RenderTemplateRequest(variables={})
605
+ response = await client.render_template(UUID(template_id), request)
606
+
607
+ rendered_text = response.rendered_text
608
+ editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "vi"
609
+
610
+ # Create temp file with rendered content
611
+ with tempfile.NamedTemporaryFile(
612
+ mode="w",
613
+ suffix=".txt",
614
+ prefix=f"rendered_{template_name}_",
615
+ delete=False,
616
+ encoding="utf-8",
617
+ ) as f:
618
+ f.write(rendered_text)
619
+ temp_path = f.name
620
+
621
+ # Suspend TUI and open editor
622
+ with self.app.suspend():
623
+ subprocess.run([editor, temp_path], check=False)
624
+
625
+ # Clean up temp file (no action on exit)
626
+ try:
627
+ os.unlink(temp_path)
628
+ except OSError:
629
+ pass
630
+
631
+ except Exception as e:
632
+ self.app.notify(f"Failed to render template: {e}", severity="error")
633
+
634
+
635
+ class TemplatesScreen(NavScreen):
636
+ """Screen for browsing and managing templates.
637
+
638
+ This is a proper Textual Screen that wraps the TemplatesWidget
639
+ and provides keybindings that work regardless of focus.
640
+ """
641
+
642
+ app: "AlloyRuntimeApp"
643
+
644
+ SCREEN_ID = "templates"
645
+
646
+ DEFAULT_CSS = """
647
+ TemplatesScreen #screen-root {
648
+ height: 1fr;
649
+ }
650
+
651
+ TemplatesScreen .browser-widget {
652
+ height: 100%;
653
+ width: 100%;
654
+ }
655
+ """
656
+
657
+ # Screen-specific bindings (Ctrl+ prefixed)
658
+ BINDINGS = NavScreen.BINDINGS + [
659
+ Binding("ctrl+f", "focus_search", "Search", show=True),
660
+ Binding("ctrl+r", "refresh", "Refresh", show=True),
661
+ Binding("ctrl+y", "copy_name", "Copy Name", show=True),
662
+ Binding("ctrl+shift+y", "copy_content", "Copy Content", show=True),
663
+ Binding("ctrl+e", "edit_content", "Edit", show=True),
664
+ Binding("ctrl+u", "update_metadata", "Edit Metadata", show=True),
665
+ Binding("ctrl+v", "create_version", "New Version", show=True),
666
+ Binding("ctrl+n", "new_template", "New", show=True),
667
+ Binding("ctrl+o", "render_and_open", "Render & Open", show=True),
668
+ Binding("escape", "focus_table", "Focus List", show=False),
669
+ Binding("enter", "copy_name", "Copy Name", show=False),
670
+ ]
671
+
672
+ def compose_content(self) -> ComposeResult:
673
+ """Compose the screen content."""
674
+ yield TemplatesWidget(id="templates-widget")
675
+
676
+ def on_screen_resume(self) -> None:
677
+ """Focus the data table when screen becomes active."""
678
+ self.call_after_refresh(self._focus_table)
679
+
680
+ def _focus_table(self) -> None:
681
+ """Focus the data table."""
682
+ try:
683
+ widget = self.query_one("#templates-widget", TemplatesWidget)
684
+ widget.focus_table()
685
+ except Exception:
686
+ pass
687
+
688
+ def _get_widget(self) -> TemplatesWidget:
689
+ """Get the templates widget."""
690
+ return self.query_one("#templates-widget", TemplatesWidget)
691
+
692
+ # =========================================================================
693
+ # Actions (keybinding handlers)
694
+ # =========================================================================
695
+
696
+ def action_focus_search(self) -> None:
697
+ """Focus the search input."""
698
+ self._get_widget().focus_search()
699
+
700
+ def action_focus_table(self) -> None:
701
+ """Focus the data table."""
702
+ self._get_widget().focus_table()
703
+
704
+ def action_refresh(self) -> None:
705
+ """Refresh the current search results."""
706
+ self._get_widget().refresh_data()
707
+
708
+ def action_copy_name(self) -> None:
709
+ """Copy the template name to clipboard."""
710
+ self._get_widget().copy_template_name()
711
+
712
+ def action_copy_content(self) -> None:
713
+ """Copy the template content to clipboard."""
714
+ self._get_widget().copy_template_content()
715
+
716
+ def action_edit_content(self) -> None:
717
+ """Edit the template content in $EDITOR."""
718
+ self._get_widget().edit_template_content()
719
+
720
+ def action_update_metadata(self) -> None:
721
+ """Open the template update modal for editing metadata."""
722
+ self._get_widget().update_template_metadata()
723
+
724
+ def action_create_version(self) -> None:
725
+ """Create a new version of the template."""
726
+ self._get_widget().create_template_version()
727
+
728
+ def action_new_template(self) -> None:
729
+ """Open the template creation modal."""
730
+ self._get_widget().open_new_template_modal()
731
+
732
+ def action_render_and_open(self) -> None:
733
+ """Render the template and open in $EDITOR."""
734
+ self._get_widget().render_and_open()