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,161 @@
1
+ """Popup widget for injection autocomplete suggestions.
2
+
3
+ This is a "dumb" display widget - it shows suggestions and highlights one,
4
+ but all keyboard handling is done by ChatInput which controls this popup.
5
+ """
6
+
7
+ from textual.app import ComposeResult
8
+ from textual.containers import Vertical
9
+ from textual.widget import Widget
10
+ from textual.widgets import OptionList, Static
11
+ from textual.widgets.option_list import Option
12
+
13
+ from cli.tui.chat.services.injection import InjectionCompletion
14
+
15
+
16
+ class InjectionSuggestionPopup(Widget):
17
+ """Display-only popup for autocomplete suggestions.
18
+
19
+ This widget is controlled entirely by ChatInput:
20
+ - ChatInput calls show_suggestions() to display options
21
+ - ChatInput calls highlight_next/previous() to change selection
22
+ - ChatInput calls get_selected() when user accepts
23
+ - ChatInput calls hide() to close
24
+
25
+ The popup never takes focus - input stays in the text area.
26
+ """
27
+
28
+ DEFAULT_CSS = """
29
+ InjectionSuggestionPopup {
30
+ display: none;
31
+ }
32
+
33
+ InjectionSuggestionPopup.visible {
34
+ display: block;
35
+ }
36
+ """
37
+
38
+ # This widget should never take focus
39
+ can_focus = False
40
+
41
+ def __init__(self, id: str | None = None) -> None:
42
+ super().__init__(id=id)
43
+ self._suggestions: list[InjectionCompletion] = []
44
+ self._injection_type: str = ""
45
+ self._filter_text: str = ""
46
+ self._highlighted_index: int = 0
47
+
48
+ def compose(self) -> ComposeResult:
49
+ """Compose the popup layout."""
50
+ with Vertical():
51
+ yield Static("@fragment()", id="popup-header")
52
+ yield Static(
53
+ "[dim]Type to filter • Tab accept • ↑↓ navigate • Esc dismiss[/]",
54
+ id="popup-hint",
55
+ )
56
+ yield OptionList(id="popup-list")
57
+ yield Static("No matches found", id="empty-hint")
58
+
59
+ def on_mount(self) -> None:
60
+ """Initialize popup state."""
61
+ self.query_one("#empty-hint", Static).display = False
62
+ # Disable focus on the option list too
63
+ self.query_one("#popup-list", OptionList).can_focus = False
64
+
65
+ def show_suggestions(
66
+ self,
67
+ suggestions: list[InjectionCompletion],
68
+ injection_type: str,
69
+ filter_text: str = "",
70
+ ) -> None:
71
+ """Display suggestions in the popup.
72
+
73
+ Args:
74
+ suggestions: List of completion suggestions.
75
+ injection_type: Type of injection (fragment, text, json, schema).
76
+ filter_text: Current filter/partial identifier typed by user.
77
+ """
78
+ self._suggestions = suggestions
79
+ self._injection_type = injection_type
80
+ self._filter_text = filter_text
81
+ self._highlighted_index = 0
82
+
83
+ # Update header to show current input state
84
+ header = self.query_one("#popup-header", Static)
85
+ if filter_text:
86
+ header.update(f"@{injection_type}([bold]{filter_text}[/bold])")
87
+ else:
88
+ header.update(f"@{injection_type}()")
89
+
90
+ # Update option list
91
+ option_list = self.query_one("#popup-list", OptionList)
92
+ option_list.clear_options()
93
+
94
+ empty_hint = self.query_one("#empty-hint", Static)
95
+
96
+ if suggestions:
97
+ for i, suggestion in enumerate(suggestions):
98
+ label = (
99
+ f"{suggestion.display_name} [dim]{suggestion.description}[/dim]"
100
+ )
101
+ option_list.add_option(Option(label, id=str(i)))
102
+
103
+ option_list.display = True
104
+ empty_hint.display = False
105
+ option_list.highlighted = 0
106
+ else:
107
+ option_list.display = False
108
+ empty_hint.display = True
109
+ if filter_text:
110
+ empty_hint.update(f"No matches for '{filter_text}'")
111
+ else:
112
+ empty_hint.update("No items available")
113
+
114
+ self.add_class("visible")
115
+
116
+ def hide(self) -> None:
117
+ """Hide the popup."""
118
+ self.remove_class("visible")
119
+ self._suggestions = []
120
+ self._filter_text = ""
121
+ self._highlighted_index = 0
122
+
123
+ @property
124
+ def is_visible(self) -> bool:
125
+ """Check if popup is currently visible."""
126
+ return self.has_class("visible")
127
+
128
+ @property
129
+ def has_suggestions(self) -> bool:
130
+ """Check if there are any suggestions to select."""
131
+ return bool(self._suggestions)
132
+
133
+ def highlight_next(self) -> None:
134
+ """Move highlight to next suggestion."""
135
+ if not self._suggestions:
136
+ return
137
+ if self._highlighted_index < len(self._suggestions) - 1:
138
+ self._highlighted_index += 1
139
+ option_list = self.query_one("#popup-list", OptionList)
140
+ option_list.highlighted = self._highlighted_index
141
+
142
+ def highlight_previous(self) -> None:
143
+ """Move highlight to previous suggestion."""
144
+ if not self._suggestions:
145
+ return
146
+ if self._highlighted_index > 0:
147
+ self._highlighted_index -= 1
148
+ option_list = self.query_one("#popup-list", OptionList)
149
+ option_list.highlighted = self._highlighted_index
150
+
151
+ def get_selected(self) -> InjectionCompletion | None:
152
+ """Get the currently highlighted suggestion.
153
+
154
+ Returns:
155
+ The selected completion, or None if no suggestions.
156
+ """
157
+ if not self._suggestions:
158
+ return None
159
+ if 0 <= self._highlighted_index < len(self._suggestions):
160
+ return self._suggestions[self._highlighted_index]
161
+ return None
@@ -0,0 +1,287 @@
1
+ """Message display widget for rendering chat message history."""
2
+
3
+ from typing import TYPE_CHECKING, Callable
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.binding import Binding
7
+ from textual.containers import VerticalScroll
8
+ from textual.widget import Widget
9
+ from textual.widgets import Static
10
+
11
+ from cli.tui.chat.messages import CopyToClipboardRequested
12
+ from cli.tui.chat.renderers.markdown import MarkdownRenderer
13
+ from cli.tui.chat.renderers.plain import PlainTextRenderer
14
+ from cli.tui.chat.store import ChatStore
15
+ from cli.tui.chat.types import (
16
+ ChatPhase,
17
+ ChatState,
18
+ MessageRenderer,
19
+ RenderMode,
20
+ extract_message_text,
21
+ )
22
+ from cli.tui.chat.widgets.welcome_screen import WelcomeScreen
23
+
24
+ if TYPE_CHECKING:
25
+ from cli.tui.app import AlloyRuntimeApp
26
+
27
+
28
+ class MessageDisplay(Widget):
29
+ """Widget for displaying chat message history.
30
+
31
+ Features:
32
+ - Renders message history with timestamps
33
+ - Shows streaming content in real-time
34
+ - Auto-scrolls to bottom on new content (unless user scrolls up)
35
+ - Supports copying last response
36
+ - Pluggable renderer for different output formats
37
+
38
+ Posts Messages:
39
+ - CopyToClipboardRequested: When user triggers copy action
40
+
41
+ Auto-scroll behavior:
42
+ - During streaming, auto-scrolls to bottom unless user has scrolled up
43
+ - User scrolling up during streaming disables auto-scroll
44
+ - User scrolling back to bottom (or pressing G) re-enables auto-scroll
45
+ - Auto-scroll is always re-enabled when streaming ends
46
+ """
47
+
48
+ app: "AlloyRuntimeApp"
49
+
50
+ BINDINGS = [
51
+ Binding("ctrl+y", "copy_last", "Copy Last", show=False),
52
+ Binding("ctrl+shift+y", "copy_all", "Copy All", show=False),
53
+ Binding("g", "scroll_top", "Top", show=False),
54
+ Binding("G", "scroll_bottom", "Bottom", show=False),
55
+ ]
56
+
57
+ def __init__(
58
+ self,
59
+ store: ChatStore,
60
+ renderer: MessageRenderer | None = None,
61
+ ) -> None:
62
+ """Initialize the message display.
63
+
64
+ Args:
65
+ store: ChatStore instance for state management.
66
+ renderer: Optional renderer for message formatting. Defaults to MarkdownRenderer.
67
+ """
68
+ super().__init__()
69
+ self._store = store
70
+ self._custom_renderer: MessageRenderer | None = renderer
71
+ self._markdown_renderer = MarkdownRenderer()
72
+ self._plain_renderer = PlainTextRenderer()
73
+ self._unsubscribe: Callable[[], None] | None = None
74
+ # Auto-scroll is enabled by default; disabled when user scrolls up during streaming
75
+ self._auto_scroll_enabled = True
76
+ # Track whether welcome screen is currently shown
77
+ self._welcome_screen_visible = False
78
+
79
+ def _get_renderer(self, state: ChatState) -> MessageRenderer:
80
+ """Get the appropriate renderer based on state."""
81
+ if self._custom_renderer:
82
+ return self._custom_renderer
83
+ if state.render_mode == RenderMode.PLAIN:
84
+ return self._plain_renderer
85
+ return self._markdown_renderer
86
+
87
+ def compose(self) -> ComposeResult:
88
+ """Compose the message display layout."""
89
+ with VerticalScroll(id="message-scroll"):
90
+ # Start with welcome screen visible, messages hidden
91
+ yield WelcomeScreen(id="welcome-screen")
92
+ messages_widget = Static("", id="messages")
93
+ messages_widget.display = False
94
+ yield messages_widget
95
+ self._welcome_screen_visible = True
96
+
97
+ def on_mount(self) -> None:
98
+ """Subscribe to store on mount."""
99
+ self._unsubscribe = self._store.subscribe(self._on_state_change)
100
+
101
+ def on_unmount(self) -> None:
102
+ """Unsubscribe from store on unmount."""
103
+ if self._unsubscribe:
104
+ self._unsubscribe()
105
+
106
+ def _on_state_change(self, state: ChatState) -> None:
107
+ """React to store state changes."""
108
+ self._render_messages(state)
109
+
110
+ def _is_at_bottom(self) -> bool:
111
+ """Check if scroll is at or near the bottom."""
112
+ scroll = self.query_one("#message-scroll", VerticalScroll)
113
+ # Consider "at bottom" if within 50 pixels of the end
114
+ # Also consider at bottom if max_scroll is 0 (content fits in view)
115
+ return scroll.max_scroll_y <= 0 or scroll.scroll_y >= scroll.max_scroll_y - 50
116
+
117
+ def _render_messages(self, state: ChatState) -> None:
118
+ """Render messages to the display using the configured renderer."""
119
+ messages_widget = self.query_one("#messages", Static)
120
+ messages = state.messages
121
+ streaming_content = state.streaming_content
122
+ streaming_thinking_content = state.streaming_thinking_content
123
+ pending_user_message = state.pending_user_message
124
+ is_streaming = state.phase == ChatPhase.STREAMING
125
+
126
+ # Check scroll position BEFORE rendering to detect user scroll-up
127
+ # If streaming and not at bottom, user must have scrolled up
128
+ if is_streaming and not self._is_at_bottom():
129
+ self._auto_scroll_enabled = False
130
+
131
+ # Get assistant name from session context
132
+ assistant_name = "Assistant"
133
+ if state.session:
134
+ assistant_name = state.session.display_name
135
+
136
+ # Check if we have anything to show
137
+ has_content = (
138
+ messages
139
+ or streaming_content
140
+ or streaming_thinking_content
141
+ or pending_user_message
142
+ )
143
+
144
+ # Determine if welcome screen should be visible
145
+ should_show_welcome = state.session is None and not has_content
146
+
147
+ # Switch between welcome screen and messages display
148
+ if should_show_welcome != self._welcome_screen_visible:
149
+ self._toggle_welcome_screen(should_show_welcome)
150
+
151
+ if should_show_welcome:
152
+ # Welcome screen handles its own content
153
+ return
154
+
155
+ if not has_content:
156
+ # Session exists but no messages yet
157
+ messages_widget.update(
158
+ "[dim]No messages yet. Type below and press Ctrl+D to send.[/]"
159
+ )
160
+ return
161
+
162
+ # Delegate rendering to the appropriate renderer based on state
163
+ renderer = self._get_renderer(state)
164
+ rendered_content = renderer.render(
165
+ messages,
166
+ streaming_content=streaming_content,
167
+ streaming_thinking_content=streaming_thinking_content,
168
+ pending_user_message=pending_user_message,
169
+ assistant_name=assistant_name,
170
+ is_streaming=is_streaming,
171
+ thinking_collapsed=state.thinking_collapsed,
172
+ )
173
+ messages_widget.update(rendered_content)
174
+
175
+ # Auto-scroll if enabled
176
+ if self._auto_scroll_enabled:
177
+ self._scroll_to_bottom()
178
+
179
+ # Re-enable auto-scroll when streaming ends
180
+ if not is_streaming:
181
+ self._auto_scroll_enabled = True
182
+
183
+ def _scroll_to_bottom(self) -> None:
184
+ """Scroll the message view to the bottom."""
185
+ scroll = self.query_one("#message-scroll", VerticalScroll)
186
+ scroll.scroll_end(animate=False)
187
+
188
+ def _toggle_welcome_screen(self, show_welcome: bool) -> None:
189
+ """Toggle between welcome screen and messages display.
190
+
191
+ Args:
192
+ show_welcome: If True, show welcome screen; if False, show messages.
193
+ """
194
+ try:
195
+ welcome_screen = self.query_one("#welcome-screen", WelcomeScreen)
196
+ messages_widget = self.query_one("#messages", Static)
197
+
198
+ welcome_screen.display = show_welcome
199
+ messages_widget.display = not show_welcome
200
+ self._welcome_screen_visible = show_welcome
201
+
202
+ # Refresh welcome screen data when showing it
203
+ if show_welcome:
204
+ welcome_screen.refresh_data()
205
+ except Exception:
206
+ # Widgets may not be mounted yet during initial compose
207
+ pass
208
+
209
+ def _get_welcome_message(self) -> str:
210
+ """Get the welcome message for new sessions."""
211
+ return """[bold]Welcome to Alloy Runtime![/]
212
+
213
+ [dim]Select a session from the sidebar or press Ctrl+N to start a new chat.[/]
214
+
215
+ [bold cyan]Features:[/]
216
+ - Browse and resume previous chat sessions
217
+ - Start new conversations with agents or models
218
+ - Stream responses in real-time
219
+ - Compose messages in your $EDITOR
220
+
221
+ [bold cyan]Keyboard shortcuts:[/]
222
+ Ctrl+D Send message
223
+ Ctrl+N New chat session
224
+ Ctrl+E Open message in $EDITOR
225
+ Ctrl+B Toggle sidebar
226
+ Ctrl+R Regenerate last response
227
+ Ctrl+U Undo last turn
228
+ Ctrl+Y Copy last response
229
+ Ctrl+Shift+Y Copy all messages
230
+ / Search sessions
231
+
232
+ [bold cyan]Tip:[/] Hold Shift while clicking to use native terminal text selection.
233
+ """
234
+
235
+ def action_copy_last(self) -> None:
236
+ """Copy the last assistant response to clipboard."""
237
+ messages = self._store.state.messages
238
+
239
+ if not messages:
240
+ self.app.notify("No messages to copy", severity="warning")
241
+ return
242
+
243
+ # Find last assistant message
244
+ for msg in reversed(messages):
245
+ if msg.role == "assistant":
246
+ text = extract_message_text(msg)
247
+ if text:
248
+ self.post_message(CopyToClipboardRequested(text, "Last response"))
249
+ return
250
+
251
+ self.app.notify("No assistant message to copy", severity="warning")
252
+
253
+ def action_copy_all(self) -> None:
254
+ """Copy all messages to clipboard as plain text."""
255
+ messages = self._store.state.messages
256
+
257
+ if not messages:
258
+ self.app.notify("No messages to copy", severity="warning")
259
+ return
260
+
261
+ # Build plain text version of all messages
262
+ lines: list[str] = []
263
+ for msg in messages:
264
+ role = msg.role.capitalize()
265
+ text = extract_message_text(msg)
266
+ if text:
267
+ lines.append(f"{role}:\n{text}\n")
268
+
269
+ if lines:
270
+ full_text = "\n".join(lines)
271
+ self.post_message(CopyToClipboardRequested(full_text, "All messages"))
272
+ else:
273
+ self.app.notify("No message content to copy", severity="warning")
274
+
275
+ def action_scroll_top(self) -> None:
276
+ """Scroll to the top of messages."""
277
+ scroll = self.query_one("#message-scroll", VerticalScroll)
278
+ scroll.scroll_home(animate=False)
279
+
280
+ def action_scroll_bottom(self) -> None:
281
+ """Scroll to the bottom of messages and resume auto-scroll."""
282
+ self._auto_scroll_enabled = True
283
+ self._scroll_to_bottom()
284
+
285
+ def show_welcome(self) -> None:
286
+ """Show the welcome screen."""
287
+ self._toggle_welcome_screen(show_welcome=True)
@@ -0,0 +1,214 @@
1
+ """Session sidebar widget for listing and searching chat sessions."""
2
+
3
+ from typing import TYPE_CHECKING, Callable
4
+
5
+ from textual import on, work
6
+ from textual.app import ComposeResult
7
+ from textual.containers import Horizontal, Vertical
8
+ from textual.widget import Widget
9
+ from textual.widgets import Checkbox, DataTable, Input, Static
10
+
11
+ from alloy_runtime_sdk.api_client.client import ApiClient
12
+ from alloy_runtime_types.dtos.sessions import SessionSummary
13
+ from alloy_runtime_sdk.logging.config import get_logger
14
+
15
+ from cli.tui.chat.messages import SessionSelected, SessionsLoaded, SessionsLoadError
16
+ from cli.tui.chat.store import ChatStore
17
+ from cli.tui.chat.types import ChatPhase, ChatState, format_session_time
18
+
19
+ logger = get_logger(__name__)
20
+
21
+ if TYPE_CHECKING:
22
+ from cli.tui.app import AlloyRuntimeApp
23
+
24
+
25
+ class SessionSidebar(Widget):
26
+ """Sidebar widget showing session list with search.
27
+
28
+ Features:
29
+ - Search sessions with debounced API calls
30
+ - Display session list in a DataTable
31
+ - Select sessions to load their messages
32
+ - Toggle between named-only and all sessions
33
+
34
+ Posts Messages:
35
+ - SessionSelected: When user selects a session
36
+ - SessionsLoaded: After successfully loading sessions
37
+ - SessionsLoadError: When session loading fails
38
+ """
39
+
40
+ app: "AlloyRuntimeApp"
41
+
42
+ # No bindings on widget - parent Screen handles all keybindings
43
+ # and delegates to public methods on this widget
44
+
45
+ DEFAULT_CSS = """
46
+ SessionSidebar #show-untitled-row {
47
+ height: auto;
48
+ padding: 0 1;
49
+ }
50
+
51
+ SessionSidebar #show-untitled {
52
+ height: auto;
53
+ min-height: 1;
54
+ padding: 0;
55
+ margin: 0;
56
+ }
57
+ """
58
+
59
+ def __init__(self, store: ChatStore) -> None:
60
+ """Initialize the session sidebar.
61
+
62
+ Args:
63
+ store: ChatStore instance for state management.
64
+ """
65
+ super().__init__()
66
+ self._store = store
67
+ self._last_search = ""
68
+ self._show_untitled = False # Default: show only named sessions
69
+ self._unsubscribe: Callable[[], None] | None = None
70
+ # Track visibility to refresh when sidebar is opened
71
+ self._was_visible = False
72
+ # Guard against re-entrant state changes
73
+ self._refreshing = False
74
+
75
+ def compose(self) -> ComposeResult:
76
+ """Compose the sidebar layout."""
77
+ with Vertical(id="sidebar-root"):
78
+ with Vertical(id="session-header"):
79
+ yield Input(
80
+ placeholder="Search sessions...",
81
+ id="session-search",
82
+ )
83
+ with Horizontal(id="show-untitled-row"):
84
+ yield Checkbox("Show untitled", id="show-untitled", value=False)
85
+ yield Static("Loading...", id="session-status")
86
+ yield DataTable(id="session-table", cursor_type="row")
87
+
88
+ def on_mount(self) -> None:
89
+ """Initialize table and subscribe to store on mount."""
90
+ table = self.query_one("#session-table", DataTable[str])
91
+ table.add_columns("Session", "Time", "Msgs")
92
+
93
+ # Subscribe to store changes
94
+ self._unsubscribe = self._store.subscribe(self._on_state_change)
95
+
96
+ # Load initial sessions
97
+ self._load_sessions("")
98
+
99
+ def on_unmount(self) -> None:
100
+ """Unsubscribe from store on unmount."""
101
+ if self._unsubscribe:
102
+ self._unsubscribe()
103
+
104
+ def _on_state_change(self, state: ChatState) -> None:
105
+ """React to store state changes."""
106
+ # Handle sidebar visibility
107
+ self.display = state.sidebar_visible
108
+
109
+ # Refresh sessions when sidebar becomes visible (with guard against recursion)
110
+ becoming_visible = state.sidebar_visible and not self._was_visible
111
+ self._was_visible = state.sidebar_visible
112
+
113
+ if becoming_visible and not self._refreshing:
114
+ self._refreshing = True
115
+ self._load_sessions(self._last_search)
116
+
117
+ # Update table when sessions change
118
+ self._update_table(state.sessions)
119
+
120
+ # Update status based on phase
121
+ status = self.query_one("#session-status", Static)
122
+ if state.phase == ChatPhase.LOADING_SESSIONS:
123
+ status.update("Loading...")
124
+ elif state.sessions:
125
+ status.update(f"{len(state.sessions)} sessions")
126
+ else:
127
+ status.update("No sessions found")
128
+
129
+ def _update_table(self, sessions: tuple[SessionSummary, ...]) -> None:
130
+ """Update the DataTable with session data."""
131
+ table = self.query_one("#session-table", DataTable[str])
132
+ table.clear()
133
+
134
+ # Populate table with sessions (filtering is done server-side via named_only)
135
+ for session in sessions:
136
+ name = session.name or "[dim]Untitled[/]"
137
+ if len(name) > 15:
138
+ name = name[:12] + "..."
139
+ table.add_row(
140
+ name,
141
+ format_session_time(session.last_activity_at),
142
+ str(session.message_count),
143
+ )
144
+
145
+ @on(Input.Changed, "#session-search")
146
+ def on_search_changed(self, event: Input.Changed) -> None:
147
+ """Handle search input changes with debouncing."""
148
+ query = event.value.strip()
149
+ if query != self._last_search:
150
+ self._last_search = query
151
+ self._store.set_session_search_query(query)
152
+ self._load_sessions(query)
153
+
154
+ @on(Checkbox.Changed, "#show-untitled")
155
+ def on_show_untitled_changed(self, event: Checkbox.Changed) -> None:
156
+ """Handle show untitled checkbox toggle - triggers a reload from server."""
157
+ self._show_untitled = event.value
158
+ # Reload sessions from server with new filter setting
159
+ self._load_sessions(self._last_search)
160
+
161
+ @on(DataTable.RowSelected, "#session-table")
162
+ def on_row_selected(self, event: DataTable.RowSelected) -> None:
163
+ """Handle session selection from table."""
164
+ row_index = event.cursor_row
165
+ sessions = self._store.state.sessions
166
+ if 0 <= row_index < len(sessions):
167
+ session = sessions[row_index]
168
+ logger.debug(
169
+ "sidebar_session_selected",
170
+ session_id=str(session.id),
171
+ session_name=session.name,
172
+ )
173
+ self.post_message(SessionSelected(session.id))
174
+
175
+ @work(exclusive=True)
176
+ async def _load_sessions(self, query: str) -> None:
177
+ """Load sessions from API."""
178
+ logger.debug("sidebar_loading_sessions", query=query or None)
179
+ self._store.set_phase(ChatPhase.LOADING_SESSIONS)
180
+ status = self.query_one("#session-status", Static)
181
+ status.update("Loading...")
182
+
183
+ try:
184
+ client = await self._get_client()
185
+ response = await client.list_sessions(
186
+ search=query if query else None,
187
+ named_only=not self._show_untitled,
188
+ limit=50,
189
+ )
190
+
191
+ logger.debug("sidebar_sessions_loaded", count=len(response.sessions))
192
+ self._store.set_sessions(list(response.sessions))
193
+ self._store.set_phase(ChatPhase.IDLE)
194
+ self.post_message(SessionsLoaded(list(response.sessions)))
195
+
196
+ except Exception as e:
197
+ logger.warning("sidebar_sessions_load_error", error=str(e))
198
+ self._store.set_phase(ChatPhase.IDLE)
199
+ status.update("[red]Error[/]")
200
+ self.post_message(SessionsLoadError(str(e)))
201
+ finally:
202
+ self._refreshing = False
203
+
204
+ async def _get_client(self) -> ApiClient:
205
+ """Get the shared server client."""
206
+ return await self.app.store.get_client()
207
+
208
+ def action_focus_search(self) -> None:
209
+ """Focus the search input."""
210
+ self.query_one("#session-search", Input).focus()
211
+
212
+ def refresh_sessions(self) -> None:
213
+ """Refresh the session list with current search query."""
214
+ self._load_sessions(self._last_search)