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,424 @@
1
+ """Slash command system for the chat interface.
2
+
3
+ Provides a registry-based system for defining and executing slash commands
4
+ like /clear, /help, /copy, etc. Commands are parsed from user input and
5
+ executed with access to the chat context.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import TYPE_CHECKING, Protocol
10
+
11
+ from cli.tui.chat.store import ChatStore
12
+
13
+ if TYPE_CHECKING:
14
+ from cli.tui.app import AlloyRuntimeApp
15
+
16
+
17
+ @dataclass
18
+ class SlashContext:
19
+ """Context provided to slash commands during execution.
20
+
21
+ Contains all the resources a command might need to perform its action.
22
+ """
23
+
24
+ store: ChatStore
25
+ app: "AlloyRuntimeApp"
26
+
27
+ def notify(self, message: str, severity: str = "information") -> None:
28
+ """Notify the user with a message.
29
+
30
+ Args:
31
+ message: The message to display.
32
+ severity: Notification severity (information, warning, error).
33
+ """
34
+ self.app.notify(message, severity=severity) # type: ignore[arg-type]
35
+
36
+
37
+ class SlashCommand(Protocol):
38
+ """Protocol for slash commands.
39
+
40
+ All slash commands must implement this protocol to be registered.
41
+ """
42
+
43
+ @property
44
+ def name(self) -> str:
45
+ """Primary command name (without slash)."""
46
+ ...
47
+
48
+ @property
49
+ def description(self) -> str:
50
+ """Short description shown in /help."""
51
+ ...
52
+
53
+ @property
54
+ def aliases(self) -> tuple[str, ...]:
55
+ """Alternative names for the command."""
56
+ ...
57
+
58
+ def execute(self, args: str, context: SlashContext) -> None:
59
+ """Execute the command.
60
+
61
+ Args:
62
+ args: Arguments passed after the command name.
63
+ context: Execution context with store, app, and notify.
64
+ """
65
+ ...
66
+
67
+
68
+ @dataclass
69
+ class ParseResult:
70
+ """Result of parsing user input for slash commands."""
71
+
72
+ is_command: bool
73
+ command: SlashCommand | None = None
74
+ args: str = ""
75
+ raw_input: str = ""
76
+
77
+
78
+ class SlashCommandRegistry:
79
+ """Registry of available slash commands.
80
+
81
+ Manages command registration, lookup by name/alias, and parsing.
82
+ """
83
+
84
+ def __init__(self) -> None:
85
+ self._commands: dict[str, SlashCommand] = {}
86
+ self._all_commands: list[SlashCommand] = []
87
+
88
+ def register(self, command: SlashCommand) -> None:
89
+ """Register a slash command.
90
+
91
+ Args:
92
+ command: The command to register.
93
+ """
94
+ self._commands[command.name] = command
95
+ for alias in command.aliases:
96
+ self._commands[alias] = command
97
+ self._all_commands.append(command)
98
+
99
+ def get(self, name: str) -> SlashCommand | None:
100
+ """Get a command by name or alias.
101
+
102
+ Args:
103
+ name: Command name (without slash).
104
+
105
+ Returns:
106
+ The command if found, None otherwise.
107
+ """
108
+ return self._commands.get(name.lower())
109
+
110
+ def list_commands(self) -> list[SlashCommand]:
111
+ """Get all registered commands (no duplicates from aliases)."""
112
+ return list(self._all_commands)
113
+
114
+ def parse(self, text: str) -> ParseResult:
115
+ """Parse user input to detect slash commands.
116
+
117
+ Args:
118
+ text: Raw user input.
119
+
120
+ Returns:
121
+ ParseResult indicating if input is a command and which one.
122
+ """
123
+ text = text.strip()
124
+
125
+ if not text.startswith("/"):
126
+ return ParseResult(is_command=False, raw_input=text)
127
+
128
+ # Split into command and args
129
+ parts = text[1:].split(maxsplit=1)
130
+ name = parts[0].lower() if parts else ""
131
+ args = parts[1] if len(parts) > 1 else ""
132
+
133
+ command = self.get(name)
134
+ return ParseResult(
135
+ is_command=True,
136
+ command=command,
137
+ args=args,
138
+ raw_input=text,
139
+ )
140
+
141
+
142
+ # =============================================================================
143
+ # Built-in Commands
144
+ # =============================================================================
145
+
146
+
147
+ @dataclass
148
+ class ClearCommand:
149
+ """Clear the message display and show welcome."""
150
+
151
+ name: str = field(default="clear", init=False)
152
+ description: str = field(default="Clear the message display", init=False)
153
+ aliases: tuple[str, ...] = field(default=("c", "cls"), init=False)
154
+
155
+ def execute(self, args: str, context: SlashContext) -> None:
156
+ """Clear display - args are ignored."""
157
+ del args # Unused
158
+ context.store.clear_session()
159
+ context.notify("Display cleared", "information")
160
+
161
+
162
+ @dataclass
163
+ class HelpCommand:
164
+ """Show available slash commands."""
165
+
166
+ name: str = field(default="help", init=False)
167
+ description: str = field(default="Show available commands", init=False)
168
+ aliases: tuple[str, ...] = field(default=("h", "?"), init=False)
169
+ registry: SlashCommandRegistry | None = None
170
+
171
+ def execute(self, args: str, context: SlashContext) -> None:
172
+ """Display help - args can filter to specific command."""
173
+ del args # Could be used for command-specific help later
174
+
175
+ if not self.registry:
176
+ context.notify("Help unavailable", "warning")
177
+ return
178
+
179
+ # Build help text
180
+ lines = ["[bold]Available Commands:[/]", ""]
181
+ for cmd in self.registry.list_commands():
182
+ alias_str = ""
183
+ if cmd.aliases:
184
+ alias_str = (
185
+ f" [dim](aliases: {', '.join('/' + a for a in cmd.aliases)})[/]"
186
+ )
187
+ lines.append(f" [cyan]/{cmd.name}[/] - {cmd.description}{alias_str}")
188
+
189
+ lines.append("")
190
+ lines.append(
191
+ "[dim]Keyboard shortcuts also available: Ctrl+N (new), Ctrl+R (regenerate), etc.[/]"
192
+ )
193
+
194
+ # Show as notification - in a real impl could show in a modal
195
+ context.notify("\n".join(lines), "information")
196
+
197
+
198
+ @dataclass
199
+ class CopyCommand:
200
+ """Copy the last assistant response to clipboard."""
201
+
202
+ name: str = field(default="copy", init=False)
203
+ description: str = field(default="Copy last response to clipboard", init=False)
204
+ aliases: tuple[str, ...] = field(default=("cp", "y"), init=False)
205
+
206
+ def execute(self, args: str, context: SlashContext) -> None:
207
+ """Copy last assistant message - args are ignored."""
208
+ del args # Unused
209
+
210
+ from cli.tui.chat.types import extract_message_text
211
+
212
+ messages = context.store.state.messages
213
+ if not messages:
214
+ context.notify("No messages to copy", "warning")
215
+ return
216
+
217
+ # Find last assistant message
218
+ for msg in reversed(messages):
219
+ if msg.role == "assistant":
220
+ text = extract_message_text(msg)
221
+ if text:
222
+ from cli.infrastructure.tui.clipboard import copy_to_clipboard
223
+
224
+ copy_to_clipboard(context.app, text, "Last response")
225
+ return
226
+
227
+ context.notify("No assistant message to copy", "warning")
228
+
229
+
230
+ @dataclass
231
+ class NewCommand:
232
+ """Request a new chat session."""
233
+
234
+ name: str = field(default="new", init=False)
235
+ description: str = field(default="Start a new chat session", init=False)
236
+ aliases: tuple[str, ...] = field(default=("n",), init=False)
237
+
238
+ def execute(self, args: str, context: SlashContext) -> None:
239
+ """Request new session - signals to screen to open modal."""
240
+ del args, context # Command handled by screen via message
241
+
242
+
243
+ @dataclass
244
+ class SessionsCommand:
245
+ """Focus the session sidebar for browsing."""
246
+
247
+ name: str = field(default="sessions", init=False)
248
+ description: str = field(default="Focus session sidebar", init=False)
249
+ aliases: tuple[str, ...] = field(default=("s", "list"), init=False)
250
+
251
+ def execute(self, args: str, context: SlashContext) -> None:
252
+ """Focus sessions - signals to screen to focus sidebar."""
253
+ del args, context # Command handled by screen via message
254
+
255
+
256
+ @dataclass
257
+ class UndoCommand:
258
+ """Undo the last conversation turn."""
259
+
260
+ name: str = field(default="undo", init=False)
261
+ description: str = field(default="Undo last conversation turn", init=False)
262
+ aliases: tuple[str, ...] = field(default=("u", "z"), init=False)
263
+
264
+ def execute(self, args: str, context: SlashContext) -> None:
265
+ """Undo last turn - signals to screen to perform undo."""
266
+ del args, context # Command handled by screen via message
267
+
268
+
269
+ @dataclass
270
+ class RegenerateCommand:
271
+ """Regenerate the last AI response."""
272
+
273
+ name: str = field(default="regenerate", init=False)
274
+ description: str = field(default="Regenerate last response", init=False)
275
+ aliases: tuple[str, ...] = field(default=("r", "regen"), init=False)
276
+
277
+ def execute(self, args: str, context: SlashContext) -> None:
278
+ """Regenerate - signals to screen to perform regeneration."""
279
+ del args, context # Command handled by screen via message
280
+
281
+
282
+ @dataclass
283
+ class AutoNameCommand:
284
+ """Toggle automatic session naming."""
285
+
286
+ name: str = field(default="autoname", init=False)
287
+ description: str = field(default="Toggle automatic session naming", init=False)
288
+ aliases: tuple[str, ...] = field(default=("an",), init=False)
289
+
290
+ # Storage key for the preference
291
+ _PREF_KEY: str = field(default="auto_name_sessions", init=False)
292
+
293
+ def execute(self, args: str, context: SlashContext) -> None:
294
+ """Toggle auto-naming or set to specific value.
295
+
296
+ Args can be:
297
+ - (empty): Toggle the current setting
298
+ - "on" / "true" / "1": Enable auto-naming
299
+ - "off" / "false" / "0": Disable auto-naming
300
+ """
301
+ from cli.infrastructure.local_storage import get_storage
302
+
303
+ storage = get_storage()
304
+ current = storage.get("preferences", self._PREF_KEY, True)
305
+
306
+ # Parse args to determine new value
307
+ args_lower = args.strip().lower()
308
+ if args_lower in ("on", "true", "1", "enable", "yes"):
309
+ new_value = True
310
+ elif args_lower in ("off", "false", "0", "disable", "no"):
311
+ new_value = False
312
+ else:
313
+ # Toggle
314
+ new_value = not current
315
+
316
+ storage.set("preferences", self._PREF_KEY, new_value)
317
+
318
+ status = "enabled" if new_value else "disabled"
319
+ context.notify(f"Automatic session naming {status}", "information")
320
+
321
+
322
+ @dataclass
323
+ class SplitCommand:
324
+ """Toggle split-pane view for comparing models."""
325
+
326
+ name: str = field(default="split", init=False)
327
+ description: str = field(default="Toggle split-pane view", init=False)
328
+ aliases: tuple[str, ...] = field(default=(), init=False)
329
+
330
+ def execute(self, args: str, context: SlashContext) -> None:
331
+ """Toggle split view - handled by screen via message."""
332
+ del args, context # Command handled by screen via message
333
+
334
+
335
+ @dataclass
336
+ class SyncCommand:
337
+ """Toggle synchronized input mode in split view."""
338
+
339
+ name: str = field(default="sync", init=False)
340
+ description: str = field(
341
+ default="Toggle sync mode (send to both panes)", init=False
342
+ )
343
+ aliases: tuple[str, ...] = field(default=(), init=False)
344
+
345
+ def execute(self, args: str, context: SlashContext) -> None:
346
+ """Toggle sync mode - handled by screen via message."""
347
+ del args, context # Command handled by screen via message
348
+
349
+
350
+ @dataclass
351
+ class SwitchPaneCommand:
352
+ """Switch focus between panes in split view."""
353
+
354
+ name: str = field(default="switch", init=False)
355
+ description: str = field(default="Switch between panes in split view", init=False)
356
+ aliases: tuple[str, ...] = field(default=("sw",), init=False)
357
+
358
+ def execute(self, args: str, context: SlashContext) -> None:
359
+ """Switch pane focus - handled by screen via message."""
360
+ del args, context # Command handled by screen via message
361
+
362
+
363
+ @dataclass
364
+ class MarkdownToggleCommand:
365
+ """Toggle markdown rendering on/off."""
366
+
367
+ name: str = field(default="markdown", init=False)
368
+ description: str = field(default="Toggle markdown rendering", init=False)
369
+ aliases: tuple[str, ...] = field(default=("md", "plain"), init=False)
370
+
371
+ def execute(self, args: str, context: SlashContext) -> None:
372
+ """Toggle render mode between markdown and plain text."""
373
+ del args # Unused
374
+
375
+ from cli.tui.chat.types import RenderMode
376
+
377
+ old_mode = context.store.state.render_mode
378
+ context.store.toggle_render_mode()
379
+ new_mode = context.store.state.render_mode
380
+ mode_name = "Markdown" if new_mode == RenderMode.MARKDOWN else "Plain text"
381
+ context.notify(f"Render mode: {mode_name} (was {old_mode.name})", "information")
382
+
383
+
384
+ def create_default_registry() -> SlashCommandRegistry:
385
+ """Create a registry with all built-in commands.
386
+
387
+ Returns:
388
+ SlashCommandRegistry populated with default commands.
389
+ """
390
+ registry = SlashCommandRegistry()
391
+
392
+ # Create help command with registry reference
393
+ help_cmd = HelpCommand()
394
+ help_cmd.registry = registry
395
+
396
+ # Register all commands
397
+ registry.register(ClearCommand())
398
+ registry.register(help_cmd)
399
+ registry.register(CopyCommand())
400
+ registry.register(NewCommand())
401
+ registry.register(SessionsCommand())
402
+ registry.register(UndoCommand())
403
+ registry.register(RegenerateCommand())
404
+ registry.register(AutoNameCommand())
405
+ # Split view commands
406
+ registry.register(SplitCommand())
407
+ registry.register(SyncCommand())
408
+ registry.register(SwitchPaneCommand())
409
+ # Rendering commands
410
+ registry.register(MarkdownToggleCommand())
411
+
412
+ return registry
413
+
414
+
415
+ # Default global registry
416
+ _default_registry: SlashCommandRegistry | None = None
417
+
418
+
419
+ def get_default_registry() -> SlashCommandRegistry:
420
+ """Get the default slash command registry (lazy-initialized singleton)."""
421
+ global _default_registry
422
+ if _default_registry is None:
423
+ _default_registry = create_default_registry()
424
+ return _default_registry
cli/tui/chat/store.py ADDED
@@ -0,0 +1,280 @@
1
+ """Reactive state store for the chat module.
2
+
3
+ Provides centralized state management with subscriber notifications.
4
+ Widgets subscribe to state changes and react accordingly.
5
+ """
6
+
7
+ from dataclasses import replace
8
+ from typing import Callable
9
+ from uuid import UUID
10
+
11
+ from alloy_runtime_types.dtos.sessions import MessageResponse, SessionSummary
12
+ from alloy_runtime_sdk.logging.config import get_logger
13
+
14
+ from cli.tui.chat.types import ChatPhase, ChatState, RenderMode, SessionContext
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class ChatStore:
20
+ """Reactive state store for chat screen.
21
+
22
+ Follows the observable store pattern:
23
+ - State is accessed via .state property (immutable snapshot)
24
+ - Mutations happen through methods that create new state
25
+ - Subscribers are notified on every state change
26
+
27
+ Usage:
28
+ store = ChatStore()
29
+
30
+ # Subscribe to changes
31
+ def on_change(state: ChatState):
32
+ print(f"Phase: {state.phase}")
33
+
34
+ unsubscribe = store.subscribe(on_change)
35
+
36
+ # Mutate state
37
+ store.set_phase(ChatPhase.STREAMING) # triggers on_change
38
+
39
+ # Unsubscribe when done
40
+ unsubscribe()
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ """Initialize with default state."""
45
+ self._state = ChatState()
46
+ self._subscribers: list[Callable[[ChatState], None]] = []
47
+
48
+ @property
49
+ def state(self) -> ChatState:
50
+ """Get current immutable state snapshot."""
51
+ return self._state
52
+
53
+ def subscribe(self, callback: Callable[[ChatState], None]) -> Callable[[], None]:
54
+ """Subscribe to state changes.
55
+
56
+ Args:
57
+ callback: Function called with new state on every change.
58
+
59
+ Returns:
60
+ Unsubscribe function - call to stop receiving updates.
61
+ """
62
+ self._subscribers.append(callback)
63
+ return lambda: self._subscribers.remove(callback)
64
+
65
+ def _notify(self) -> None:
66
+ """Notify all subscribers of state change."""
67
+ for subscriber in self._subscribers:
68
+ subscriber(self._state)
69
+
70
+ def _update(self, **changes: object) -> None:
71
+ """Update state with changes and notify subscribers.
72
+
73
+ Creates a new ChatState with the given field changes.
74
+ """
75
+ self._state = replace(self._state, **changes)
76
+ self._notify()
77
+
78
+ # === Phase Management ===
79
+
80
+ def set_phase(self, phase: ChatPhase) -> None:
81
+ """Set the current chat phase."""
82
+ old_phase = self._state.phase
83
+ if old_phase != phase:
84
+ logger.debug(
85
+ "store_phase_changed", old_phase=old_phase.name, new_phase=phase.name
86
+ )
87
+ self._update(phase=phase)
88
+
89
+ def is_busy(self) -> bool:
90
+ """Check if the chat is in a busy state (can't accept new actions)."""
91
+ return self._state.phase not in (ChatPhase.IDLE,)
92
+
93
+ # === Session Management ===
94
+
95
+ def set_session(
96
+ self,
97
+ session_id: UUID,
98
+ session_name: str | None,
99
+ agent_id: UUID | None,
100
+ agent_name: str | None,
101
+ message_count: int,
102
+ provider_key: str | None = None,
103
+ model_name: str | None = None,
104
+ system_instruction: str | None = None,
105
+ ) -> None:
106
+ """Set the active session context.
107
+
108
+ Args:
109
+ session_id: The session ID.
110
+ session_name: Optional session name.
111
+ agent_id: Agent ID (for agent mode).
112
+ agent_name: Agent name (for agent mode).
113
+ message_count: Number of messages in session.
114
+ provider_key: Provider key (for direct model mode).
115
+ model_name: Model name (for direct model mode).
116
+ system_instruction: System instruction (for direct model mode).
117
+ """
118
+ session = SessionContext(
119
+ session_id=session_id,
120
+ session_name=session_name,
121
+ agent_id=agent_id,
122
+ agent_name=agent_name,
123
+ message_count=message_count,
124
+ provider_key=provider_key,
125
+ model_name=model_name,
126
+ system_instruction=system_instruction,
127
+ )
128
+ logger.info(
129
+ "store_session_set",
130
+ session_id=str(session_id),
131
+ session_name=session_name,
132
+ agent_name=agent_name,
133
+ model_name=model_name,
134
+ message_count=message_count,
135
+ )
136
+ self._update(session=session)
137
+
138
+ def clear_session(self) -> None:
139
+ """Clear the active session."""
140
+ logger.debug("store_session_cleared")
141
+ self._update(session=None, messages=())
142
+
143
+ def set_sessions(self, sessions: list[SessionSummary]) -> None:
144
+ """Set the session list."""
145
+ self._update(sessions=tuple(sessions))
146
+
147
+ def set_session_search_query(self, query: str) -> None:
148
+ """Set the session search query."""
149
+ self._update(session_search_query=query)
150
+
151
+ # === Message Management ===
152
+
153
+ def set_messages(self, messages: list[MessageResponse]) -> None:
154
+ """Set the message list for current session."""
155
+ logger.debug("store_messages_set", message_count=len(messages))
156
+ self._update(messages=tuple(messages))
157
+
158
+ def append_streaming_content(self, chunk: str) -> None:
159
+ """Append content to streaming buffer."""
160
+ new_content = self._state.streaming_content + chunk
161
+ self._update(streaming_content=new_content)
162
+
163
+ def append_streaming_thinking_content(self, chunk: str) -> None:
164
+ """Append content to streaming thinking/reasoning buffer."""
165
+ new_content = self._state.streaming_thinking_content + chunk
166
+ self._update(streaming_thinking_content=new_content)
167
+
168
+ def clear_streaming_content(self) -> None:
169
+ """Clear the streaming content buffer."""
170
+ self._update(streaming_content="", streaming_thinking_content="")
171
+
172
+ # === Error Management ===
173
+
174
+ def set_error(self, error: str | None) -> None:
175
+ """Set or clear error state."""
176
+ if error:
177
+ logger.warning("store_error_set", error=error)
178
+ self._update(error=error)
179
+
180
+ def clear_error(self) -> None:
181
+ """Clear error state."""
182
+ self._update(error=None)
183
+
184
+ # === Convenience Methods ===
185
+
186
+ def start_streaming(self, user_message: str | None = None) -> None:
187
+ """Transition to streaming state with optional pending user message."""
188
+ logger.info(
189
+ "store_streaming_started",
190
+ has_user_message=user_message is not None,
191
+ message_preview=user_message[:50] if user_message else None,
192
+ )
193
+ self._update(
194
+ phase=ChatPhase.STREAMING,
195
+ streaming_content="",
196
+ streaming_thinking_content="",
197
+ error=None,
198
+ pending_user_message=user_message,
199
+ cancel_requested=False, # Reset cancellation flag on new stream
200
+ )
201
+
202
+ def finish_streaming(self, cancelled: bool = False) -> None:
203
+ """Transition from streaming back to idle.
204
+
205
+ Args:
206
+ cancelled: If True, keep streaming content visible for cancelled streams.
207
+ """
208
+ content_length = len(self._state.streaming_content)
209
+ logger.info(
210
+ "store_streaming_finished",
211
+ cancelled=cancelled,
212
+ content_length=content_length,
213
+ )
214
+ if cancelled:
215
+ # Keep the partial content visible when cancelled
216
+ self._update(
217
+ phase=ChatPhase.IDLE,
218
+ cancel_requested=False,
219
+ )
220
+ else:
221
+ # Normal completion - clear streaming content
222
+ self._update(
223
+ phase=ChatPhase.IDLE,
224
+ streaming_content="",
225
+ streaming_thinking_content="",
226
+ pending_user_message=None,
227
+ cancel_requested=False,
228
+ )
229
+
230
+ def request_cancel(self) -> None:
231
+ """Request cancellation of current streaming operation."""
232
+ if self._state.phase == ChatPhase.STREAMING:
233
+ logger.info("store_cancel_requested")
234
+ self._update(cancel_requested=True)
235
+
236
+ def toggle_thinking_collapsed(self) -> None:
237
+ """Toggle whether thinking blocks are collapsed."""
238
+ self._update(thinking_collapsed=not self._state.thinking_collapsed)
239
+
240
+ def set_thinking_collapsed(self, collapsed: bool) -> None:
241
+ """Set whether thinking blocks are collapsed."""
242
+ self._update(thinking_collapsed=collapsed)
243
+
244
+ def set_pending_user_message(self, message: str | None) -> None:
245
+ """Set the pending user message (shown optimistically before server confirms)."""
246
+ self._update(pending_user_message=message)
247
+
248
+ def clear_pending_user_message(self) -> None:
249
+ """Clear the pending user message."""
250
+ self._update(pending_user_message=None)
251
+
252
+ def reset(self) -> None:
253
+ """Reset to initial state."""
254
+ self._state = ChatState()
255
+ self._notify()
256
+
257
+ # === UI State ===
258
+
259
+ def toggle_sidebar(self) -> None:
260
+ """Toggle sidebar visibility."""
261
+ self._update(sidebar_visible=not self._state.sidebar_visible)
262
+
263
+ def set_sidebar_visible(self, visible: bool) -> None:
264
+ """Set sidebar visibility."""
265
+ self._update(sidebar_visible=visible)
266
+
267
+ def toggle_render_mode(self) -> None:
268
+ """Toggle between markdown and plain text rendering."""
269
+ current = self._state.render_mode
270
+ new_mode = (
271
+ RenderMode.PLAIN if current == RenderMode.MARKDOWN else RenderMode.MARKDOWN
272
+ )
273
+ logger.debug(
274
+ "store_render_mode_toggled", old_mode=current.name, new_mode=new_mode.name
275
+ )
276
+ self._update(render_mode=new_mode)
277
+
278
+ def set_render_mode(self, mode: RenderMode) -> None:
279
+ """Set the render mode."""
280
+ self._update(render_mode=mode)