@pheem49/mint 1.17.0 โ†’ 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (421) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +2 -2
  3. package/README.md +8 -2
  4. package/Release_Note.md +222 -71
  5. package/crates/mint-cli/src/agent/diff_render.rs +66 -44
  6. package/crates/mint-cli/src/agent/live_status.rs +21 -4
  7. package/crates/mint-cli/src/agent/markdown_render.rs +342 -5
  8. package/crates/mint-cli/src/agent.rs +25 -10
  9. package/crates/mint-cli/src/commands/agent.rs +4 -92
  10. package/crates/mint-cli/src/commands/browser.rs +2 -15
  11. package/crates/mint-cli/src/commands/integrations.rs +3 -3
  12. package/crates/mint-cli/src/commands/mod.rs +0 -3
  13. package/crates/mint-cli/src/commands/tailnet_web.rs +1 -0
  14. package/crates/mint-cli/src/interactive/chat_tui/dialog.rs +572 -0
  15. package/crates/mint-cli/src/interactive/chat_tui/mod.rs +1427 -0
  16. package/crates/mint-cli/src/interactive/chat_tui/render.rs +1524 -0
  17. package/crates/mint-cli/src/interactive/chat_tui/selection.rs +401 -0
  18. package/crates/mint-cli/src/interactive/chat_tui/state.rs +1833 -0
  19. package/crates/mint-cli/src/interactive/chat_tui/tests.rs +3414 -0
  20. package/crates/mint-cli/src/interactive/chat_tui/thought.rs +202 -0
  21. package/crates/mint-cli/src/interactive/chat_tui/transcript.rs +728 -0
  22. package/crates/mint-cli/src/interactive/chat_tui/watermark.rs +148 -0
  23. package/crates/mint-cli/src/interactive/command_ui.rs +53 -5
  24. package/crates/mint-cli/src/interactive/composer.rs +186 -0
  25. package/crates/mint-cli/src/interactive/format.rs +78 -3
  26. package/crates/mint-cli/src/interactive/input_box.rs +61 -1
  27. package/crates/mint-cli/src/interactive/resume_picker.rs +367 -25
  28. package/crates/mint-cli/src/interactive/slash_commands.rs +648 -371
  29. package/crates/mint-cli/src/interactive/tool_activity.rs +93 -20
  30. package/crates/mint-cli/src/interactive.rs +37 -0
  31. package/crates/mint-cli/src/main.rs +6 -3
  32. package/crates/mint-cli/src/markdown.rs +2 -37
  33. package/crates/mint-cli/src/onboard.rs +700 -282
  34. package/crates/mint-cli/src/setup.rs +1342 -624
  35. package/crates/mint-cli/src/terminal_theme.rs +159 -35
  36. package/crates/mint-cli/tests/library_intake.rs +1 -0
  37. package/crates/mint-core/src/agent/chat/anthropic.rs +318 -0
  38. package/crates/mint-core/src/agent/chat/custom.rs +149 -0
  39. package/crates/mint-core/src/agent/chat/gemini.rs +518 -0
  40. package/crates/mint-core/src/agent/chat/mod.rs +857 -0
  41. package/crates/mint-core/src/agent/chat/ollama.rs +321 -0
  42. package/crates/mint-core/src/agent/chat/openai.rs +582 -0
  43. package/crates/mint-core/src/agent/chat/streaming.rs +469 -0
  44. package/crates/mint-core/src/agent/chat/tests.rs +1479 -0
  45. package/crates/mint-core/src/agent/memory.rs +56 -2
  46. package/crates/mint-core/src/agent/tasks.rs +2 -2
  47. package/crates/mint-core/src/api_server/routes/chat.rs +24 -2
  48. package/crates/mint-core/src/api_server/routes/gemini_live.rs +14 -1
  49. package/crates/mint-core/src/api_server/routes/html_preview.rs +1 -8
  50. package/crates/mint-core/src/api_server/routes/sessions.rs +21 -0
  51. package/crates/mint-core/src/avatar_bridge.rs +0 -22
  52. package/crates/mint-core/src/browser/cdp.rs +10 -4
  53. package/crates/mint-core/src/browser/headless.rs +582 -0
  54. package/crates/mint-core/src/browser/input.rs +7 -62
  55. package/crates/mint-core/src/browser/interact.rs +0 -14
  56. package/crates/mint-core/src/browser/mod.rs +8 -29
  57. package/crates/mint-core/src/browser/navigate.rs +2 -36
  58. package/crates/mint-core/src/browser/runtime.rs +2 -4
  59. package/crates/mint-core/src/browser/session.rs +50 -466
  60. package/crates/mint-core/src/browser/workflow.rs +142 -32
  61. package/crates/mint-core/src/cron/store.rs +2 -2
  62. package/crates/mint-core/src/integrations/mcp.rs +6 -8
  63. package/crates/mint-core/src/lib.rs +0 -5
  64. package/crates/mint-core/src/media/gemini_live.rs +636 -86
  65. package/crates/mint-core/src/media/library/migration.rs +26 -0
  66. package/crates/mint-core/src/media/library/tests.rs +1 -1
  67. package/crates/mint-core/src/media/library/thumbnails.rs +8 -2
  68. package/crates/mint-core/src/media/pictures.rs +1 -1
  69. package/crates/mint-core/src/media/tool_images.rs +92 -1
  70. package/crates/mint-core/src/mods/manager.rs +101 -35
  71. package/crates/mint-core/src/orchestration/context_render.rs +441 -24
  72. package/crates/mint-core/src/orchestration/decision_parsing.rs +531 -16
  73. package/crates/mint-core/src/orchestration/full_skill_result_tests.rs +12 -0
  74. package/crates/mint-core/src/orchestration/loop_guard.rs +145 -0
  75. package/crates/mint-core/src/orchestration/mcp_outcome_tests.rs +864 -0
  76. package/crates/mint-core/src/orchestration/memory_skill.rs +88 -28
  77. package/crates/mint-core/src/orchestration/mod.rs +320 -4249
  78. package/crates/mint-core/src/orchestration/reliability.rs +67 -27
  79. package/crates/mint-core/src/orchestration/review_regressions.rs +5 -0
  80. package/crates/mint-core/src/orchestration/tests.rs +1665 -0
  81. package/crates/mint-core/src/orchestration/tools/dispatch.rs +967 -0
  82. package/crates/mint-core/src/orchestration/tools/files.rs +1 -0
  83. package/crates/mint-core/src/orchestration/tools/mod.rs +5 -1
  84. package/crates/mint-core/src/orchestration/tools/planning.rs +102 -42
  85. package/crates/mint-core/src/orchestration/tools/shell.rs +129 -58
  86. package/crates/mint-core/src/orchestration/truncation.rs +170 -13
  87. package/crates/mint-core/src/orchestration/types.rs +648 -0
  88. package/crates/mint-core/src/orchestration/verification.rs +0 -3
  89. package/crates/mint-core/src/orchestration/workspace_helpers.rs +24 -6
  90. package/crates/mint-core/src/prompts/agent.rs +52 -76
  91. package/crates/mint-core/src/prompts/tool_catalog.rs +8 -151
  92. package/crates/mint-core/src/search/files.rs +20 -0
  93. package/crates/mint-core/src/slash/mod.rs +228 -265
  94. package/crates/mint-core/src/slash/models.rs +6 -1
  95. package/crates/mint-core/src/system/bg_shell.rs +1 -0
  96. package/crates/mint-core/src/system/config.rs +176 -34
  97. package/crates/mint-mod-runtime/src/lib.rs +37 -23
  98. package/out/web/assets/ArtifactPreviewPanel-Cx6BVOOF.js +2 -0
  99. package/out/web/assets/CalculationCard-D2E74u5q.js +1 -0
  100. package/out/web/assets/CliSessionsView-ZbdtSjvK.css +1 -0
  101. package/out/web/assets/{CliSessionsView-MI8kXMAb.js โ†’ CliSessionsView-z5i6qGyb.js} +1 -1
  102. package/out/web/assets/CodeReviewPage-BK2bOScd.css +1 -0
  103. package/out/web/assets/CodeReviewPage-Dp6zHuHu.js +2 -0
  104. package/out/web/assets/{CommandPalette-BD8SlTE5.js โ†’ CommandPalette-DaKlzbKs.js} +1 -1
  105. package/out/web/assets/ImageGenCard-BMCJHP9O.js +1 -0
  106. package/out/web/assets/ImageSearchCard-Cld_SOUg.js +1 -0
  107. package/out/web/assets/{ImageStudioPanel-BP7N4rLr.js โ†’ ImageStudioPanel-B5N7TjeB.js} +1 -1
  108. package/out/web/assets/LibraryPage-CMB89rJ0.css +1 -0
  109. package/out/web/assets/LibraryPage-DbZEi4W_.js +2 -0
  110. package/out/web/assets/LinkedFoldersView-9B9DIPoZ.js +1 -0
  111. package/out/web/assets/{McpRegistryPicker-CoS83vtc.js โ†’ McpRegistryPicker-DnXPlzI7.js} +1 -1
  112. package/out/web/assets/McpServersView-CiNBNUl4.js +1 -0
  113. package/out/web/assets/MermaidCard-BBDNEXbY.js +2 -0
  114. package/out/web/assets/MintDashboard-C_UYPoNV.js +119 -0
  115. package/out/web/assets/MintDashboard-DTd8C_c8.css +1 -0
  116. package/out/web/assets/PlanCard-C2x5pajq.js +1 -0
  117. package/out/web/assets/{PluginsView-jBYmcKh4.js โ†’ PluginsView-DoTg9MVa.js} +1 -1
  118. package/out/web/assets/RewindModal-B5oowrR0.js +1 -0
  119. package/out/web/assets/{ScheduledTasksView-5uw9-V9H.js โ†’ ScheduledTasksView-BD5DVPkZ.js} +1 -1
  120. package/out/web/assets/{SelectField-DXBQtrUK.js โ†’ SelectField-Cy6CghrU.js} +1 -1
  121. package/out/web/assets/SettingsWindow-BYl_oCFh.js +4 -0
  122. package/out/web/assets/SettingsWindow-Ckcgnjt8.css +1 -0
  123. package/out/web/assets/{SkillsView-C-gp9J7m.js โ†’ SkillsView-CIeWwq5z.js} +1 -1
  124. package/out/web/assets/StockCard-DxUciKyp.js +1 -0
  125. package/out/web/assets/ToolSurfacePage-qBgs_VHq.css +1 -0
  126. package/out/web/assets/ToolSurfacePage-saVbVrkE.js +38 -0
  127. package/out/web/assets/{UiFeatureCard-6BwxTOCe.js โ†’ UiFeatureCard-CdY0hGWy.js} +1 -1
  128. package/out/web/assets/{UiGridCard-DvZcLNxb.js โ†’ UiGridCard-BBK3I5hC.js} +1 -1
  129. package/out/web/assets/UiMockupWidget-CpR9EDuc.js +1 -0
  130. package/out/web/assets/{VeoStudioPanel-DOj7_WR3.css โ†’ VeoStudioPanel-9kU9YLEN.css} +1 -1
  131. package/out/web/assets/{VeoStudioPanel-DfsOJKv0.js โ†’ VeoStudioPanel-Co6-wEWM.js} +2 -2
  132. package/out/web/assets/WeatherCard-Buzi7Vfu.js +1 -0
  133. package/out/web/assets/WorkspacePdfPreview-Bi8Zzyir.js +2 -0
  134. package/out/web/assets/{abnfDiagram-VCTEODGH-BOo3Ckng.js โ†’ abnfDiagram-VCTEODGH-CMfR95N2.js} +1 -1
  135. package/out/web/assets/{architecture-7GRP2DOG-ZOwheW3n.js โ†’ architecture-7GRP2DOG-DhAM4HyO.js} +1 -1
  136. package/out/web/assets/{architectureDiagram-5GKGNRK7-DC2FDzVd.js โ†’ architectureDiagram-5GKGNRK7-Yk3TUJ_F.js} +1 -1
  137. package/out/web/assets/{blockDiagram-I7D4REHJ-BrKFWdAH.js โ†’ blockDiagram-I7D4REHJ-X9fBgJik.js} +1 -1
  138. package/out/web/assets/{c4Diagram-7LVT6UL2-BHTHK1Yd.js โ†’ c4Diagram-7LVT6UL2-Bk9x7N-8.js} +1 -1
  139. package/out/web/assets/{chunk-4HAMMTFA-BYFkZwNj.js โ†’ chunk-4HAMMTFA-Dx5fk1_0.js} +1 -1
  140. package/out/web/assets/{chunk-75Z2AOVW-BSKfSKSA.js โ†’ chunk-75Z2AOVW-BszTQoM4.js} +1 -1
  141. package/out/web/assets/{chunk-DU6HZSFF-vIhsiW6n.js โ†’ chunk-DU6HZSFF-DK-As3rk.js} +1 -1
  142. package/out/web/assets/{chunk-F27PBJKO-BlYpW9xq.js โ†’ chunk-F27PBJKO-LmncZZIS.js} +1 -1
  143. package/out/web/assets/{chunk-GMAD6QVW-BIBz_Ug5.js โ†’ chunk-GMAD6QVW-DSQ_sDRJ.js} +1 -1
  144. package/out/web/assets/{chunk-GVQU2GXP-NPSHxeqC.js โ†’ chunk-GVQU2GXP-Cno3ulje.js} +1 -1
  145. package/out/web/assets/{chunk-IMKFNOWR-YlJK8_Fp.js โ†’ chunk-IMKFNOWR-bIzfY9Jc.js} +1 -1
  146. package/out/web/assets/{chunk-L3NEJ4N5-DMYi4-af.js โ†’ chunk-L3NEJ4N5-Dl6eOa0D.js} +1 -1
  147. package/out/web/assets/{chunk-OSK3NFVY-Begd3-jA.js โ†’ chunk-OSK3NFVY-BaUGJ-cM.js} +1 -1
  148. package/out/web/assets/{chunk-P2QGCYS3-D1I4xFHN.js โ†’ chunk-P2QGCYS3-_066ndhH.js} +1 -1
  149. package/out/web/assets/{chunk-POPQ4Y6H-CafZZzNE.js โ†’ chunk-POPQ4Y6H-YT1AKhTv.js} +1 -1
  150. package/out/web/assets/{chunk-PWAF6VOD-Dwh6_Lnt.js โ†’ chunk-PWAF6VOD-CrEgDsxh.js} +1 -1
  151. package/out/web/assets/{chunk-SHT3W25Y-B0XrCSEb.js โ†’ chunk-SHT3W25Y-_KwaRLOl.js} +1 -1
  152. package/out/web/assets/{chunk-SVP7TREG-CxLsddxC.js โ†’ chunk-SVP7TREG-BQ1qb32L.js} +1 -1
  153. package/out/web/assets/{chunk-TICWLB2K-y156QInb.js โ†’ chunk-TICWLB2K-BDyehCtg.js} +1 -1
  154. package/out/web/assets/classDiagram-ZZMXUADV-BoSpzRCJ.js +1 -0
  155. package/out/web/assets/classDiagram-v2-VYDZK3BY-BoSpzRCJ.js +1 -0
  156. package/out/web/assets/{config-DqXJXeZM.css โ†’ config-2suVFPZg.css} +1 -1
  157. package/out/web/assets/config-CkRCdkxn.js +2 -0
  158. package/out/web/assets/{cynefin-OW5HDTMX-SMVvrLf-.js โ†’ cynefin-OW5HDTMX-Dl_31aaN.js} +1 -1
  159. package/out/web/assets/{cynefinDiagram-5FMLGOSQ-rZznN24T.js โ†’ cynefinDiagram-5FMLGOSQ-Bpir8jha.js} +1 -1
  160. package/out/web/assets/{dagre-GXQ25YYZ-BOToydFu.js โ†’ dagre-GXQ25YYZ-D9xu3nsq.js} +1 -1
  161. package/out/web/assets/{diagram-S7CK7UJ4-TMVaGQEJ.js โ†’ diagram-S7CK7UJ4-DeveoEeB.js} +1 -1
  162. package/out/web/assets/{diagram-UQ7AKVKN-C2JK-GP2.js โ†’ diagram-UQ7AKVKN-p4xWRESV.js} +1 -1
  163. package/out/web/assets/{diagram-VSXAHHWV-Cw8VSi2K.js โ†’ diagram-VSXAHHWV-C7qz9NDF.js} +1 -1
  164. package/out/web/assets/{diagram-VX7I27RA-DXp4fKk9.js โ†’ diagram-VX7I27RA-DRiCa3Vd.js} +1 -1
  165. package/out/web/assets/{diagram-Z3DM3KII-CzSicYiW.js โ†’ diagram-Z3DM3KII-Bt7zduQX.js} +1 -1
  166. package/out/web/assets/{ebnfDiagram-PWID7BFC-Dqj-zqPu.js โ†’ ebnfDiagram-PWID7BFC-DV9WpwdR.js} +1 -1
  167. package/out/web/assets/{erDiagram-RLTQ6QDP-DZwFZAuD.js โ†’ erDiagram-RLTQ6QDP-BJ_nwG2m.js} +1 -1
  168. package/out/web/assets/{eventmodeling-NTZA5JFV-C0O-Lvgp.js โ†’ eventmodeling-NTZA5JFV-BTBMrfVa.js} +1 -1
  169. package/out/web/assets/flowDiagram-HODETNUW-Cdlq3cJL.js +1 -0
  170. package/out/web/assets/{ganttDiagram-EL5Y4UJY-Da4ULXTl.js โ†’ ganttDiagram-EL5Y4UJY-DQLdsXrC.js} +1 -1
  171. package/out/web/assets/{gitGraph-4MIJSDKK-B9wNFZIP.js โ†’ gitGraph-4MIJSDKK-CL1OEOQ_.js} +1 -1
  172. package/out/web/assets/{gitGraphDiagram-WWUBYQGX-RjrhB0MP.js โ†’ gitGraphDiagram-WWUBYQGX-8cSi42H7.js} +1 -1
  173. package/out/web/assets/index-BocxnrDG.js +168 -0
  174. package/out/web/assets/index-yeh_r55a.css +1 -0
  175. package/out/web/assets/{info-A6RAGUB7-B3dfCpeY.js โ†’ info-A6RAGUB7-C1zsX5sc.js} +1 -1
  176. package/out/web/assets/{infoDiagram-27XIBGKW-QVWkLVx7.js โ†’ infoDiagram-27XIBGKW-DsRYtIyI.js} +1 -1
  177. package/out/web/assets/{ishikawaDiagram-5VMMS53U-CLFNIRI9.js โ†’ ishikawaDiagram-5VMMS53U-B2crwIXj.js} +1 -1
  178. package/out/web/assets/{journeyDiagram-3NMN7TZE-fgOQadYF.js โ†’ journeyDiagram-3NMN7TZE-BTH-GyVR.js} +1 -1
  179. package/out/web/assets/{kanban-definition-UXKFOSKX-G3Q_9g9O.js โ†’ kanban-definition-UXKFOSKX-BeRfCnuA.js} +1 -1
  180. package/out/web/assets/{mermaid-parser.core-v3Jm7nco.js โ†’ mermaid-parser.core-BJwgCBBc.js} +3 -3
  181. package/out/web/assets/{mermaid.core-DTEjLUA1.js โ†’ mermaid.core-CsDDodN8.js} +4 -4
  182. package/out/web/assets/{mindmap-definition-YA3MSWOX-C_D42_fF.js โ†’ mindmap-definition-YA3MSWOX-B42efOQ1.js} +1 -1
  183. package/out/web/assets/{modelManager-BPLycQrN.js โ†’ modelManager-B3ZFMh8v.js} +2 -2
  184. package/out/web/assets/{oauthManager-BUK7ic7q.js โ†’ oauthManager-Ch5Du2fF.js} +1 -1
  185. package/out/web/assets/{packet-AYTQ26CC-B-R-SHlF.js โ†’ packet-AYTQ26CC-B6xUyE6r.js} +1 -1
  186. package/out/web/assets/{pegDiagram-XKGWAZYB-Da2q_giz.js โ†’ pegDiagram-XKGWAZYB-1LabVycL.js} +1 -1
  187. package/out/web/assets/{pie-WAS4IAKB-BC8F0N7-.js โ†’ pie-WAS4IAKB-DJwo-nf_.js} +1 -1
  188. package/out/web/assets/{pieDiagram-E7YTZNPT-Iy9REJNC.js โ†’ pieDiagram-E7YTZNPT-icmAqzy4.js} +1 -1
  189. package/out/web/assets/{quadrantDiagram-AXDQQJYC-ztsiYw7h.js โ†’ quadrantDiagram-AXDQQJYC-CZ-wEx41.js} +1 -1
  190. package/out/web/assets/{radar-RG4KPBEZ-B_1P8Z_z.js โ†’ radar-RG4KPBEZ-D7d-Xriw.js} +1 -1
  191. package/out/web/assets/{railroad-74A4TZTK-DuTqczSo.js โ†’ railroad-74A4TZTK-viuhdYbs.js} +1 -1
  192. package/out/web/assets/{railroad-abnf-HS5TGJTU-Df29WLmp.js โ†’ railroad-abnf-HS5TGJTU-DNXiCNea.js} +1 -1
  193. package/out/web/assets/{railroad-ebnf-LZEXJU2U-ePFMo-qp.js โ†’ railroad-ebnf-LZEXJU2U-nkSu1-0C.js} +1 -1
  194. package/out/web/assets/{railroad-peg-WCYAUIDC-DYXLW4_E.js โ†’ railroad-peg-WCYAUIDC-BhbMeQzz.js} +1 -1
  195. package/out/web/assets/{railroadDiagram-O6MQD6OU-LStnZf2S.js โ†’ railroadDiagram-O6MQD6OU-CLpG1jUX.js} +1 -1
  196. package/out/web/assets/{requirementDiagram-BXWQKSXE-DmzonMH-.js โ†’ requirementDiagram-BXWQKSXE-kRDrvWrc.js} +1 -1
  197. package/out/web/assets/{sankeyDiagram-P5KCCOFB-B8vjmUU1.js โ†’ sankeyDiagram-P5KCCOFB-DxAwmq4x.js} +1 -1
  198. package/out/web/assets/{sequenceDiagram-WJ2MYXX4-CTnplOAX.js โ†’ sequenceDiagram-WJ2MYXX4-aLgY-6Wz.js} +1 -1
  199. package/out/web/assets/{stateDiagram-D77RDMKH-BFrIaLdI.js โ†’ stateDiagram-D77RDMKH-D5QWfOhp.js} +1 -1
  200. package/out/web/assets/stateDiagram-v2-MP3YSRHH-D6OiZoAP.js +1 -0
  201. package/out/web/assets/{swimlanes-42K2YHIH-BXGngUNU.js โ†’ swimlanes-42K2YHIH-CRs5UyA-.js} +1 -1
  202. package/out/web/assets/swimlanesDiagram-VR7AAH4N-UgEXIsV0.js +8 -0
  203. package/out/web/assets/syntaxHighlight-C-wAGT84.js +3 -0
  204. package/out/web/assets/{timeline-definition-24CTP7MA-CegH7n43.js โ†’ timeline-definition-24CTP7MA-Denqd9qb.js} +1 -1
  205. package/out/web/assets/{treeView-Q6P3EWNA-DTpJv2Tw.js โ†’ treeView-Q6P3EWNA-0my3Z3s4.js} +1 -1
  206. package/out/web/assets/{treemap-WGGIJYW6-i8M9iJU6.js โ†’ treemap-WGGIJYW6-B_EjkLYq.js} +1 -1
  207. package/out/web/assets/{vennDiagram-4TSXK5OY-Dpzo1S8U.js โ†’ vennDiagram-4TSXK5OY-DxVg3Ewr.js} +1 -1
  208. package/out/web/assets/{wardley-WFR3VGLG-BrNsgtfw.js โ†’ wardley-WFR3VGLG-CGD-Tj4p.js} +1 -1
  209. package/out/web/assets/{wardleyDiagram-VM6X3IG4-lQ92LFE1.js โ†’ wardleyDiagram-VM6X3IG4-CmfS39Xm.js} +1 -1
  210. package/out/web/assets/{xychartDiagram-S5SC5T6Z-BaOaEoBd.js โ†’ xychartDiagram-S5SC5T6Z-BVM9bKis.js} +1 -1
  211. package/out/web/index-web.html +2 -2
  212. package/out/web/index.html +2 -2
  213. package/package.json +2 -2
  214. package/shared-modules/CODEOWNERS +3 -3
  215. package/shared-modules/SDL2/SDL2_mixer.json +27 -0
  216. package/shared-modules/appstream/README.md +13 -0
  217. package/shared-modules/appstream/appstream-compose.json +111 -0
  218. package/shared-modules/dbus-glib/dbus-glib.json +2 -2
  219. package/shared-modules/libportal/libportal.json +23 -0
  220. package/shared-modules/linux-audio/fluidsynth2-static.json +2 -2
  221. package/shared-modules/linux-audio/fluidsynth2.json +2 -2
  222. package/shared-modules/linux-audio/libinstpatch.json +2 -2
  223. package/shared-modules/linux-audio/liblo-static.json +3 -0
  224. package/shared-modules/luajit/luajit.json +2 -2
  225. package/shared-modules/squashfs-tools/squashfs-tools.json +0 -3
  226. package/slash-commands.json +7 -2
  227. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  228. package/src/renderer/shared/agentProgress.ts +51 -3
  229. package/src/renderer/shared/components/Settings/AutomationTab.tsx +2 -56
  230. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +15 -1
  231. package/src/renderer/shared/components/Settings/GeneralTab.tsx +25 -12
  232. package/src/renderer/shared/components/Settings/ThemeTab.test.tsx +45 -0
  233. package/src/renderer/shared/components/Settings/ThemeTab.tsx +106 -3
  234. package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +67 -17
  235. package/src/renderer/shared/components/activity/AgentActivityTable.tsx +376 -134
  236. package/src/renderer/shared/components/activity/AskUserActivityCard.test.tsx +122 -0
  237. package/src/renderer/shared/components/activity/AskUserActivityCard.tsx +70 -0
  238. package/src/renderer/shared/components/activity/McpToolResults.tsx +39 -2
  239. package/src/renderer/shared/components/activity/SubagentDagView.tsx +7 -7
  240. package/src/renderer/shared/components/activity/ViewedImages.tsx +41 -2
  241. package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +3 -3
  242. package/src/renderer/shared/components/app/CommandPalette.tsx +7 -7
  243. package/src/renderer/shared/components/app/DesktopTitlebar.tsx +2 -2
  244. package/src/renderer/shared/components/app/MintDashboard.tsx +27 -17
  245. package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +3 -3
  246. package/src/renderer/shared/components/cards/CalculationCard.tsx +3 -3
  247. package/src/renderer/shared/components/cards/ImageGenCard.tsx +2 -2
  248. package/src/renderer/shared/components/cards/ImageSearchCard.tsx +2 -2
  249. package/src/renderer/shared/components/cards/MermaidCard.tsx +1 -1
  250. package/src/renderer/shared/components/cards/PlanCard.test.tsx +41 -0
  251. package/src/renderer/shared/components/cards/PlanCard.tsx +32 -0
  252. package/src/renderer/shared/components/cards/ReleaseNotesCard.test.tsx +29 -0
  253. package/src/renderer/shared/components/cards/ReleaseNotesCard.tsx +137 -0
  254. package/src/renderer/shared/components/cards/StockCard.tsx +7 -7
  255. package/src/renderer/shared/components/cards/ToolResultCards.tsx +11 -3
  256. package/src/renderer/shared/components/cards/UiMockupWidget.tsx +4 -4
  257. package/src/renderer/shared/components/cards/WeatherCard.tsx +5 -5
  258. package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +11 -0
  259. package/src/renderer/shared/components/chat/ApprovalCard.tsx +21 -2
  260. package/src/renderer/shared/components/chat/ArtifactCards.test.tsx +69 -0
  261. package/src/renderer/shared/components/chat/ArtifactCards.tsx +238 -0
  262. package/src/renderer/shared/components/chat/AttachedDocumentCard.test.tsx +39 -0
  263. package/src/renderer/shared/components/chat/AttachedDocumentCard.tsx +220 -0
  264. package/src/renderer/shared/components/chat/ChatCodeBlock.test.tsx +33 -0
  265. package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +15 -1
  266. package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +40 -2
  267. package/src/renderer/shared/components/chat/ChatMessageItem.tsx +180 -34
  268. package/src/renderer/shared/components/chat/ChatModeSelector.css +227 -0
  269. package/src/renderer/shared/components/chat/ChatModeSelector.test.tsx +45 -0
  270. package/src/renderer/shared/components/chat/ChatModeSelector.tsx +198 -0
  271. package/src/renderer/shared/components/chat/ChatPanel.test.tsx +22 -0
  272. package/src/renderer/shared/components/chat/ChatPanel.tsx +227 -53
  273. package/src/renderer/shared/components/chat/DiffReviewModal.tsx +6 -6
  274. package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +142 -1
  275. package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +45 -3
  276. package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +42 -14
  277. package/src/renderer/shared/components/chat/RewindModal.tsx +10 -10
  278. package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +14 -0
  279. package/src/renderer/shared/components/chat/SourcesBlock.test.tsx +53 -0
  280. package/src/renderer/shared/components/chat/SourcesBlock.tsx +236 -90
  281. package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +45 -1
  282. package/src/renderer/shared/components/chat/TurnFileChanges.tsx +202 -169
  283. package/src/renderer/shared/components/extensions/CliSessionsView.tsx +5 -4
  284. package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +3 -3
  285. package/src/renderer/shared/components/studio/LibraryPage.tsx +48 -5
  286. package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +125 -13
  287. package/src/renderer/shared/components/workspace/CodeReviewPage.test.tsx +50 -0
  288. package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +77 -16
  289. package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +78 -22
  290. package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +13 -0
  291. package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +48 -28
  292. package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +266 -47
  293. package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +18 -6
  294. package/src/renderer/shared/components/workspace/background-terminals.css +11 -11
  295. package/src/renderer/shared/constants/config.ts +1 -5
  296. package/src/renderer/shared/constants/models.test.ts +51 -0
  297. package/src/renderer/shared/constants/models.ts +139 -32
  298. package/src/renderer/shared/conversation/lifecycle.test.ts +91 -3
  299. package/src/renderer/shared/conversation/lifecycle.ts +104 -5
  300. package/src/renderer/shared/conversation/progressiveStream.test.ts +83 -0
  301. package/src/renderer/shared/conversation/progressiveStream.ts +219 -0
  302. package/src/renderer/shared/css/agent-observability.css +133 -0
  303. package/src/renderer/shared/css/chat-ui-cards.css +13 -13
  304. package/src/renderer/shared/css/code-review.css +63 -34
  305. package/src/renderer/shared/css/code-sessions.css +47 -18
  306. package/src/renderer/shared/css/collapsed-paste.css +1 -1
  307. package/src/renderer/shared/css/error-notice.css +1 -1
  308. package/src/renderer/shared/css/file-changes.css +165 -80
  309. package/src/renderer/shared/css/library.css +105 -10
  310. package/src/renderer/shared/css/management-views.css +1 -1
  311. package/src/renderer/shared/css/mcp-results.css +5 -5
  312. package/src/renderer/shared/css/model-selector.css +18 -18
  313. package/src/renderer/shared/css/mods.css +7 -7
  314. package/src/renderer/shared/css/notification-notice.css +2 -2
  315. package/src/renderer/shared/css/queued-messages.css +5 -5
  316. package/src/renderer/shared/css/settings/general.css +18 -13
  317. package/src/renderer/shared/css/settings/profile.css +5 -5
  318. package/src/renderer/shared/css/settings/theme.css +5 -5
  319. package/src/renderer/shared/css/slash-choice.css +2 -2
  320. package/src/renderer/shared/css/slash-suggestions.css +3 -3
  321. package/src/renderer/shared/css/sources-block.css +41 -2
  322. package/src/renderer/shared/css/tools-panel.css +2 -2
  323. package/src/renderer/shared/css/ui-tailwind.css +8 -0
  324. package/src/renderer/shared/css/veo-studio.css +2 -2
  325. package/src/renderer/shared/css/viewed-images.css +7 -7
  326. package/src/renderer/shared/platform.ts +2 -1
  327. package/src/renderer/shared/theme/themeManager.test.ts +17 -0
  328. package/src/renderer/shared/theme/themeManager.ts +21 -4
  329. package/src/renderer/shared/theme/themes.ts +54 -0
  330. package/src/renderer/shared/theme/tokens.css +6 -4
  331. package/src/renderer/shared/types.ts +1 -1
  332. package/src/renderer/shared/utils/agentActivity.test.ts +468 -2
  333. package/src/renderer/shared/utils/agentActivity.ts +602 -35
  334. package/src/renderer/shared/utils/artifactBrowser.test.ts +37 -32
  335. package/src/renderer/shared/utils/artifactBrowser.ts +17 -19
  336. package/src/renderer/shared/utils/attachedDocument.test.ts +73 -0
  337. package/src/renderer/shared/utils/attachedDocument.ts +49 -0
  338. package/src/renderer/shared/utils/fileIcons.test.ts +59 -0
  339. package/src/renderer/shared/utils/fileIcons.ts +149 -16
  340. package/src/renderer/shared/utils/markdown.tsx +72 -19
  341. package/src/renderer/shared/utils/markdown.webImages.test.tsx +16 -0
  342. package/src/renderer/shared/utils/mcpResults.test.tsx +29 -0
  343. package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
  344. package/src/renderer/shared/utils/syntaxHighlight.test.tsx +11 -1
  345. package/src/renderer/shared/utils/syntaxHighlight.tsx +51 -3
  346. package/src/renderer/shared/utils/useGeminiLiveVoice.ts +324 -51
  347. package/src/renderer/shared/utils/workspaceDrag.test.ts +153 -0
  348. package/src/renderer/shared/utils/workspaceDrag.ts +142 -0
  349. package/src/renderer/shared/utils/workspaceMedia.ts +10 -2
  350. package/src/renderer/src/App.tsx +14 -8
  351. package/src/renderer/src/agentProgress.ts +1 -0
  352. package/src/renderer/src/components/ScreenPicker.css +1 -1
  353. package/src/renderer/src/components/WorkspacePanel.tsx +6 -5
  354. package/src/renderer/src/css/app-shell.css +4 -4
  355. package/src/renderer/src/css/chat.css +1261 -241
  356. package/src/renderer/src/css/desktop-tools.css +11 -11
  357. package/src/renderer/src/css/sidebar-workspace.css +2 -2
  358. package/src/renderer/src/css/tool-surfaces.css +12 -12
  359. package/src/renderer/src/index.css +38 -20
  360. package/src/renderer/src/tauri.ts +22 -2
  361. package/src/renderer/src-web/App.tsx +14 -0
  362. package/src/renderer/src-web/agentProgress.ts +1 -0
  363. package/src/renderer/src-web/css/app-shell.css +4 -4
  364. package/src/renderer/src-web/css/chat.css +1278 -190
  365. package/src/renderer/src-web/css/mobile.css +29 -13
  366. package/src/renderer/src-web/css/settings/mobile.css +2 -2
  367. package/src/renderer/src-web/css/sidebar-workspace.css +1 -1
  368. package/src/renderer/src-web/index.css +38 -20
  369. package/src/renderer/src-web/tauri.ts +22 -2
  370. package/src/renderer/tests/browserArtifacts.browser.tsx +1 -1
  371. package/src/renderer/tests/library.browser.tsx +3 -0
  372. package/src/renderer/tests/liveVoice.browser.html +1 -0
  373. package/src/renderer/tests/workspaceDrop.browser.tsx +1 -1
  374. package/src-tauri/src/app/lifecycle.rs +1 -14
  375. package/src-tauri/src/background/headless.rs +14 -8
  376. package/src-tauri/src/commands/conversation.rs +38 -0
  377. package/src-tauri/src/native/mod.rs +0 -1
  378. package/src-tauri/src/native/system.rs +4 -47
  379. package/crates/mint-cli/src/interactive/chat_tui.rs +0 -8855
  380. package/crates/mint-core/src/agent/chat.rs +0 -4394
  381. package/crates/mint-core/src/browser/lifecycle.rs +0 -291
  382. package/crates/mint-core/src/browser/logging.rs +0 -20
  383. package/crates/mint-core/src/orchestration/tools/browser.rs +0 -31
  384. package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +0 -2
  385. package/out/web/assets/CalculationCard-CRjP3E1j.js +0 -1
  386. package/out/web/assets/CliSessionsView-C3Br2v5Y.css +0 -1
  387. package/out/web/assets/CodeReviewPage-CUJceQme.css +0 -1
  388. package/out/web/assets/CodeReviewPage-VCUgPla_.js +0 -2
  389. package/out/web/assets/ImageGenCard-BCXxkVJX.js +0 -1
  390. package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +0 -1
  391. package/out/web/assets/LibraryPage-DAxUTAg7.css +0 -1
  392. package/out/web/assets/LibraryPage-WlviRdR8.js +0 -2
  393. package/out/web/assets/LinkedFoldersView-CKRgroWK.js +0 -1
  394. package/out/web/assets/McpServersView-D3wHnuHg.js +0 -1
  395. package/out/web/assets/MermaidCard-NqrHdyJW.js +0 -2
  396. package/out/web/assets/MintDashboard-B2DJsDDO.css +0 -1
  397. package/out/web/assets/MintDashboard-COw_xol7.js +0 -63
  398. package/out/web/assets/RewindModal-DempuDod.js +0 -1
  399. package/out/web/assets/SettingsWindow-2YP3yj7s.js +0 -4
  400. package/out/web/assets/SettingsWindow-Dz8LQVgD.css +0 -1
  401. package/out/web/assets/StockCard-BDMCe7mx.js +0 -1
  402. package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +0 -38
  403. package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +0 -1
  404. package/out/web/assets/UiMockupWidget-DD73QWg1.js +0 -1
  405. package/out/web/assets/WeatherCard-CPucIfq8.js +0 -1
  406. package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +0 -2
  407. package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +0 -1
  408. package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +0 -1
  409. package/out/web/assets/config-CvIO5hkn.js +0 -2
  410. package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +0 -1
  411. package/out/web/assets/index-BmpulIkg.js +0 -168
  412. package/out/web/assets/index-CkvBwhX_.css +0 -1
  413. package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +0 -1
  414. package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +0 -8
  415. package/shared-modules/libsecret/libsecret.json +0 -23
  416. package/shared-modules/libusb/libusb.json +0 -27
  417. package/shared-modules/lzo/lzo.json +0 -27
  418. package/src/renderer/src/components/BrowserShell.tsx +0 -108
  419. package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +0 -91
  420. package/src/renderer/src/components/browser-shell.css +0 -185
  421. package/src-tauri/src/native/browser.rs +0 -252
@@ -159,14 +159,46 @@ fn first_line_preview(path: &Path) -> String {
159
159
  .map(str::trim)
160
160
  .find(|l| !l.is_empty())
161
161
  .unwrap_or("");
162
- if first.chars().count() > 80 {
163
- let short: String = first.chars().take(80).collect();
162
+ let clean = first.trim_start_matches('#').trim();
163
+ if clean.chars().count() > 80 {
164
+ let short: String = clean.chars().take(80).collect();
164
165
  format!("{short}โ€ฆ")
165
166
  } else {
166
- first.to_string()
167
+ clean.to_string()
167
168
  }
168
169
  }
169
170
 
171
+ fn format_plan_meta(path: &Path) -> String {
172
+ let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
173
+ if stem.len() >= 15 && stem.as_bytes().get(8) == Some(&b'-') {
174
+ let y = &stem[0..4];
175
+ let m = &stem[4..6];
176
+ let d = &stem[6..8];
177
+ let hh = &stem[9..11];
178
+ let mm = &stem[11..13];
179
+ let ss = &stem[13..15];
180
+ if y.chars().all(|c| c.is_ascii_digit())
181
+ && m.chars().all(|c| c.is_ascii_digit())
182
+ && d.chars().all(|c| c.is_ascii_digit())
183
+ && hh.chars().all(|c| c.is_ascii_digit())
184
+ && mm.chars().all(|c| c.is_ascii_digit())
185
+ && ss.chars().all(|c| c.is_ascii_digit())
186
+ {
187
+ let date_str = format!("{y}-{m}-{d} {hh}:{mm}:{ss}");
188
+ if stem.len() > 16 {
189
+ let slug = &stem[16..];
190
+ return format!("{date_str} ยท {slug}");
191
+ } else {
192
+ return date_str;
193
+ }
194
+ }
195
+ }
196
+ path.file_name()
197
+ .and_then(|n| n.to_str())
198
+ .unwrap_or("")
199
+ .to_string()
200
+ }
201
+
170
202
  async fn execute_core_slash(
171
203
  session: &mut InteractiveSession,
172
204
  ui: &mut dyn CommandUi,
@@ -205,7 +237,19 @@ async fn execute_core_slash(
205
237
  match response {
206
238
  SlashResponse::Message { markdown } => {
207
239
  if !markdown.is_empty() {
208
- ui.push_command_output(markdown);
240
+ if matches!(
241
+ cmd,
242
+ "/release-notes"
243
+ | "/release_notes"
244
+ | "/release-note"
245
+ | "/release_note"
246
+ | "/releasenotes"
247
+ | "/releasenote"
248
+ ) {
249
+ ui.push_release_notes(&markdown);
250
+ } else {
251
+ ui.push_command_output(markdown);
252
+ }
209
253
  }
210
254
  Some(SlashResult::Handled)
211
255
  }
@@ -427,6 +471,34 @@ pub async fn handle_slash_command(
427
471
  mint_core::slash::INIT_AGENTS_MD_PROMPT.to_string(),
428
472
  )),
429
473
 
474
+ "/edit" | "/retry" => {
475
+ let scoped = mint_core::scoped_chat_id(
476
+ &session.chat_id,
477
+ Some(&session.current_dir.to_string_lossy()),
478
+ );
479
+ if let Ok(memory) = MemoryStore::open_default() {
480
+ if let Ok(rows) = memory.interactions_for_chat(&scoped) {
481
+ if let Some(last) = rows.last() {
482
+ let turn_id = last.id;
483
+ let prompt_to_run = if rest.is_empty() {
484
+ last.user_text.clone()
485
+ } else {
486
+ rest.to_string()
487
+ };
488
+ let _ = memory.truncate_interactions_from(&scoped, turn_id);
489
+ ui.push_notice(format!(
490
+ "Truncated turn #{} from history. Resending with prompt:\n{}",
491
+ rows.len(),
492
+ prompt_to_run
493
+ ));
494
+ return Some(SlashResult::ForwardToAgent(prompt_to_run));
495
+ }
496
+ }
497
+ }
498
+ ui.push_notice("No previous turn found to edit or retry.");
499
+ Some(SlashResult::Handled)
500
+ }
501
+
430
502
  "/fast" => {
431
503
  let choice = if rest.is_empty() {
432
504
  let options = vec![
@@ -531,14 +603,15 @@ pub async fn handle_slash_command(
531
603
  };
532
604
  if rest.is_empty() {
533
605
  if checkpoints.is_empty() {
534
- println!("\n{DIM}No git checkpoints recorded for this CLI session yet.{RESET}");
535
- println!(
536
- "{DIM}Checkpoints are created automatically before mutating file actions (write_file, apply_patch).{RESET}\n"
606
+ ui.push_notice(
607
+ "No git checkpoints recorded for this CLI session yet.\nCheckpoints are created automatically before mutating file actions (write_file, apply_patch).",
537
608
  );
538
609
  } else {
539
- println!("\n{BLUE}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{RESET}");
540
- println!("{MINT} โ†บ Available Git Checkpoints (Time Machine){RESET}");
541
- println!("{BLUE}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{RESET}");
610
+ let mut lines = vec![
611
+ format!("{BLUE}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{RESET}"),
612
+ format!("{MINT} โ†บ Available Git Checkpoints (Time Machine){RESET}"),
613
+ format!("{BLUE}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{RESET}"),
614
+ ];
542
615
  for cp in checkpoints.iter().rev() {
543
616
  let hash_short = if cp.commit_hash.len() >= 7 {
544
617
  &cp.commit_hash[..7]
@@ -546,24 +619,21 @@ pub async fn handle_slash_command(
546
619
  &cp.commit_hash
547
620
  };
548
621
  let target = cp.target_path.as_deref().unwrap_or("-");
549
- println!(
622
+ lines.push(format!(
550
623
  " {MINT}Step {:<3}{RESET} {DIM}[{}]{RESET} {:<20} {DIM}({}){RESET}",
551
624
  cp.step, hash_short, cp.action, target
552
- );
625
+ ));
553
626
  }
554
- println!(
555
- "\n{DIM}Use {RESET}{MINT}/rewind <step>{RESET}{DIM} to restore workspace to before that step, or {RESET}{MINT}/rewind undo{RESET}{DIM} to recover from rescue snapshot.{RESET}\n"
556
- );
627
+ lines.push(format!(
628
+ "\n{DIM}Use {RESET}{MINT}/rewind <step>{RESET}{DIM} to restore workspace to before that step, or {RESET}{MINT}/rewind undo{RESET}{DIM} to recover from rescue snapshot.{RESET}"
629
+ ));
630
+ ui.push_command_output(lines.join("\n"));
557
631
  }
558
632
  Some(SlashResult::Handled)
559
633
  } else if rest.trim().eq_ignore_ascii_case("undo") {
560
634
  match mint_core::git::undo_rollback(&session.current_dir) {
561
- Ok(msg) => {
562
- println!("\n{MINT}{msg}{RESET}\n");
563
- }
564
- Err(err) => {
565
- println!("\n{ERROR}Failed to undo rewind:{RESET} {err}\n");
566
- }
635
+ Ok(msg) => ui.push_notice(msg),
636
+ Err(err) => ui.push_notice(format!("Failed to undo rewind: {err}")),
567
637
  }
568
638
  Some(SlashResult::Handled)
569
639
  } else {
@@ -574,18 +644,14 @@ pub async fn handle_slash_command(
574
644
  &target_rewind_chat_id,
575
645
  step,
576
646
  ) {
577
- Ok(msg) => {
578
- println!("\n{MINT}{msg}{RESET}\n");
579
- }
580
- Err(err) => {
581
- println!("\n{ERROR}Failed to rollback:{RESET} {err}\n");
582
- }
647
+ Ok(msg) => ui.push_notice(msg),
648
+ Err(err) => ui.push_notice(format!("Failed to rollback: {err}")),
583
649
  }
584
650
  }
585
651
  Err(_) => {
586
- println!(
587
- "\n{ERROR}Invalid step number:{RESET} \"{rest}\". Usage: /rewind <step> or /rewind undo\n"
588
- );
652
+ ui.push_notice(format!(
653
+ "Invalid step number: \"{rest}\". Usage: /rewind <step> or /rewind undo"
654
+ ));
589
655
  }
590
656
  }
591
657
  Some(SlashResult::Handled)
@@ -702,28 +768,75 @@ pub async fn handle_slash_command(
702
768
  }
703
769
 
704
770
  "/thought" | "/think" => {
705
- if let Some(record) = get_last_thought() {
706
- let _ = show_thought_viewer(&record.thought, &record.elapsed_str);
707
- } else {
708
- ui.push_notice("No recent thought process recorded for this turn.");
771
+ if let Err(err) = ui.open_thought_viewer() {
772
+ ui.push_notice(format!("Could not open thought viewer: {err}"));
709
773
  }
710
774
  Some(SlashResult::Handled)
711
775
  }
712
776
 
713
- "/plan" if rest == "list" => {
777
+ "/plan" if rest == "list" || rest.starts_with("list ") => {
778
+ let arg = rest.trim_start_matches("list").trim();
714
779
  let files = list_plan_files(&session.current_dir);
715
780
  if files.is_empty() {
716
- ui.push_notice("No saved plans yet.");
781
+ ui.push_notice("No saved plans yet under .agents/plans/");
782
+ } else if !arg.is_empty() {
783
+ let found = if let Ok(idx) = arg.parse::<usize>() {
784
+ idx.checked_sub(1).and_then(|i| files.get(i))
785
+ } else {
786
+ files.iter().find(|p| {
787
+ p.file_name()
788
+ .and_then(|n| n.to_str())
789
+ .is_some_and(|n| n.contains(arg))
790
+ })
791
+ };
792
+ match found {
793
+ Some(path) => match std::fs::read_to_string(path) {
794
+ Ok(content) => ui.push_plan(&content),
795
+ Err(e) => ui.push_notice(format!("Could not read plan file: {e}")),
796
+ },
797
+ None => ui.push_notice(format!(
798
+ "No saved plan matching \"{arg}\" โ€” run /plan list to see saved plans."
799
+ )),
800
+ }
717
801
  } else {
718
- let mut lines = vec![format!("{BLUE}Saved plans:{RESET}")];
719
- for (idx, path) in files.iter().enumerate() {
720
- lines.push(format!(
721
- " {DIM}[{}]{RESET} {BLUE}{}{RESET}",
722
- idx + 1,
723
- first_line_preview(path)
724
- ));
802
+ let options = files
803
+ .iter()
804
+ .enumerate()
805
+ .map(|(idx, path)| {
806
+ let preview = first_line_preview(path);
807
+ let display_title = if preview.is_empty() {
808
+ path.file_name()
809
+ .and_then(|n| n.to_str())
810
+ .unwrap_or("Untitled Plan")
811
+ } else {
812
+ &preview
813
+ };
814
+ let meta = format_plan_meta(path);
815
+ ChoiceItem::with_description(
816
+ format!("[{}] {}", idx + 1, display_title),
817
+ meta,
818
+ path.to_string_lossy().to_string(),
819
+ )
820
+ })
821
+ .collect::<Vec<_>>();
822
+
823
+ match ui.prompt_choice(
824
+ "Select Saved Plan",
825
+ "Choose a plan to view its content (Press Enter to view, Esc to cancel)",
826
+ &options,
827
+ ) {
828
+ Ok(Some(idx)) => {
829
+ if let Some(path) = files.get(idx) {
830
+ match std::fs::read_to_string(path) {
831
+ Ok(content) => {
832
+ ui.push_plan(&content);
833
+ }
834
+ Err(e) => ui.push_notice(format!("Could not read plan file: {e}")),
835
+ }
836
+ }
837
+ }
838
+ _ => {}
725
839
  }
726
- ui.push_command_output(lines.join("\n"));
727
840
  }
728
841
  Some(SlashResult::Handled)
729
842
  }
@@ -742,7 +855,7 @@ pub async fn handle_slash_command(
742
855
  };
743
856
  match found {
744
857
  Some(path) => match std::fs::read_to_string(path) {
745
- Ok(content) => ui.push_command_output(content),
858
+ Ok(content) => ui.push_plan(&content),
746
859
  Err(e) => ui.push_notice(format!("Could not read plan file: {e}")),
747
860
  },
748
861
  None => ui.push_notice(format!(
@@ -789,18 +902,18 @@ pub async fn handle_slash_command(
789
902
  "/models" => {
790
903
  let selected_provider = if rest.is_empty() {
791
904
  let providers = session.config.available_providers();
792
- match prompt_interactive_select(
793
- "Select AI provider",
794
- &providers,
795
- &session.config.ai_provider,
796
- ) {
797
- Ok(Some(p)) => Some(p),
905
+ let choices: Vec<ChoiceItem> = providers
906
+ .iter()
907
+ .map(|p| ChoiceItem::new(p.clone(), p.clone()))
908
+ .collect();
909
+ match ui.prompt_choice("Select AI provider", "", &choices) {
910
+ Ok(Some(idx)) => providers.get(idx).cloned(),
798
911
  Ok(None) => {
799
- println!("Cancelled provider selection.\n");
912
+ ui.push_notice("Cancelled provider selection.");
800
913
  None
801
914
  }
802
915
  Err(e) => {
803
- println!("{ERROR}Error selecting provider:{RESET} {e}\n");
916
+ ui.push_notice(format!("Error selecting provider: {e}"));
804
917
  None
805
918
  }
806
919
  }
@@ -809,25 +922,16 @@ pub async fn handle_slash_command(
809
922
  };
810
923
 
811
924
  if let Some(provider) = selected_provider {
812
- // Switch provider first so `active_model()` reflects that
813
- // provider's own default/last-used model, then offer a
814
- // second picker (when a model list is known for it) so the
815
- // user isn't stuck with the default after choosing a provider.
816
925
  match session.config.set_active_model(&provider, None) {
817
926
  Ok(mut display_name) => {
818
- // Try live fetch from the provider API; fall back to
819
- // static presets if the network is unavailable or the
820
- // user hasn't entered an API key yet.
821
- print!("{DIM}Fetching available models for {provider}...{RESET}\r");
822
- let _ = std::io::Write::flush(&mut std::io::stdout());
927
+ ui.set_status(vec![format!("Fetching available models for {provider}...")]);
823
928
  let mut model_options =
824
929
  mint_core::slash::models::model_options_for_provider_async(
825
930
  &session.config,
826
931
  &provider,
827
932
  )
828
933
  .await;
829
- print!("\r\x1b[2K");
830
- let _ = std::io::Write::flush(&mut std::io::stdout());
934
+ ui.clear_status();
831
935
  let current_model = session.config.active_model().to_string();
832
936
  if !current_model.is_empty()
833
937
  && !model_options.iter().any(|m| m == &current_model)
@@ -836,32 +940,40 @@ pub async fn handle_slash_command(
836
940
  }
837
941
 
838
942
  if !model_options.is_empty() {
839
- match prompt_interactive_select(
943
+ let choices: Vec<ChoiceItem> = model_options
944
+ .iter()
945
+ .map(|m| ChoiceItem::new(m.clone(), m.clone()))
946
+ .collect();
947
+ match ui.prompt_choice(
840
948
  &format!("Select {provider} model"),
841
- &model_options,
842
- &current_model,
949
+ "",
950
+ &choices,
843
951
  ) {
844
- Ok(Some(model)) if model != current_model => {
845
- match session.config.set_active_model(&provider, Some(&model)) {
846
- Ok(name) => display_name = name,
847
- Err(error) => {
848
- println!("{ERROR}Config error:{RESET} {error}")
952
+ Ok(Some(idx)) => {
953
+ if let Some(model) = model_options.get(idx)
954
+ && model != &current_model
955
+ {
956
+ match session
957
+ .config
958
+ .set_active_model(&provider, Some(model))
959
+ {
960
+ Ok(name) => display_name = name,
961
+ Err(error) => {
962
+ ui.push_notice(format!("Config error: {error}"));
963
+ }
849
964
  }
850
965
  }
851
966
  }
852
- Ok(_) => {}
967
+ Ok(None) => {}
853
968
  Err(e) => {
854
- println!("{ERROR}Error selecting model:{RESET} {e}\n")
969
+ ui.push_notice(format!("Error selecting model: {e}"));
855
970
  }
856
971
  }
857
972
  }
858
973
 
859
- println!(
860
- "\n{DIM}โ”€โ”€โ”€{RESET} {MINT}{}{RESET} {DIM}โ”€โ”€โ”€{RESET}\n",
861
- display_name
862
- );
974
+ ui.push_notice(format!("Active model: {display_name}"));
863
975
  }
864
- Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
976
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
865
977
  }
866
978
  }
867
979
  Some(SlashResult::Handled)
@@ -871,29 +983,31 @@ pub async fn handle_slash_command(
871
983
  if rest == "on" || rest == "off" {
872
984
  session.config.enable_agent_collaboration = rest == "on";
873
985
  match mint_core::save_config(&session.config) {
874
- Ok(()) => println!(
875
- "{DIM}Multi-Agent collaboration set to: {}{RESET}\n",
986
+ Ok(()) => ui.push_notice(format!(
987
+ "Multi-Agent collaboration set to: {}",
876
988
  if session.config.enable_agent_collaboration {
877
989
  "Enabled"
878
990
  } else {
879
991
  "Disabled"
880
992
  }
881
- ),
882
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
993
+ )),
994
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
883
995
  }
884
996
  } else if !rest.is_empty() {
885
- println!("{WARN}Usage: /multi-agent [on|off]{RESET}\n");
997
+ ui.push_notice("Usage: /multi-agent [on|off]");
886
998
  } else {
887
- println!("\n{BLUE}Multi-Agent System Settings:{RESET}");
888
999
  let collab_status = if session.config.enable_agent_collaboration {
889
1000
  format!("{MINT}Enabled (on){RESET}")
890
1001
  } else {
891
1002
  format!("{DIM}Disabled (off){RESET}")
892
1003
  };
893
- println!(" Global Collaboration: {collab_status}");
894
- println!("\n{BLUE}Configured Agents:{RESET}");
1004
+ let mut lines = vec![
1005
+ format!("{BLUE}Multi-Agent System Settings:{RESET}"),
1006
+ format!(" Global Collaboration: {collab_status}"),
1007
+ format!("\n{BLUE}Configured Agents:{RESET}"),
1008
+ ];
895
1009
  if session.config.agents.is_empty() {
896
- println!(" No agents configured.");
1010
+ lines.push(" No agents configured.".to_string());
897
1011
  } else {
898
1012
  for agent in &session.config.agents {
899
1013
  let status_label = if agent.enabled {
@@ -901,10 +1015,10 @@ pub async fn handle_slash_command(
901
1015
  } else {
902
1016
  format!("{DIM}[Disabled]{RESET}")
903
1017
  };
904
- println!(
1018
+ lines.push(format!(
905
1019
  " {:<15} {} Provider: {:<12} Model: {}",
906
1020
  agent.name, status_label, agent.provider, agent.model
907
- );
1021
+ ));
908
1022
  let truncated_instruction = if agent.system_instruction.chars().count() > 60
909
1023
  {
910
1024
  let head: String = agent.system_instruction.chars().take(60).collect();
@@ -912,38 +1026,34 @@ pub async fn handle_slash_command(
912
1026
  } else {
913
1027
  agent.system_instruction.replace('\n', " ")
914
1028
  };
915
- println!(" {DIM}Instruction: {truncated_instruction}{RESET}");
1029
+ lines.push(format!(
1030
+ " {DIM}Instruction: {truncated_instruction}{RESET}"
1031
+ ));
916
1032
  }
917
1033
  }
918
- println!();
1034
+ ui.push_command_output(lines.join("\n"));
919
1035
 
920
- let options = vec![
921
- "Keep current status".to_string(),
922
- "on (enable collaboration)".to_string(),
923
- "off (disable collaboration)".to_string(),
1036
+ let choices = vec![
1037
+ ChoiceItem::new("Keep current status", "keep"),
1038
+ ChoiceItem::new("on (enable collaboration)", "on"),
1039
+ ChoiceItem::new("off (disable collaboration)", "off"),
924
1040
  ];
925
- let current = &options[0];
926
- match prompt_interactive_select("Toggle Collaboration Status?", &options, current) {
927
- Ok(Some(sel)) => {
928
- if sel.starts_with("on") {
929
- session.config.enable_agent_collaboration = true;
930
- match mint_core::save_config(&session.config) {
931
- Ok(()) => println!(
932
- "{DIM}Multi-Agent collaboration set to: Enabled{RESET}\n"
933
- ),
934
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
935
- }
936
- } else if sel.starts_with("off") {
937
- session.config.enable_agent_collaboration = false;
938
- match mint_core::save_config(&session.config) {
939
- Ok(()) => println!(
940
- "{DIM}Multi-Agent collaboration set to: Disabled{RESET}\n"
941
- ),
942
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
943
- }
1041
+ if let Ok(Some(idx)) =
1042
+ ui.prompt_choice("Toggle Collaboration Status?", "", &choices)
1043
+ {
1044
+ if idx == 1 {
1045
+ session.config.enable_agent_collaboration = true;
1046
+ match mint_core::save_config(&session.config) {
1047
+ Ok(()) => ui.push_notice("Multi-Agent collaboration set to: Enabled"),
1048
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
1049
+ }
1050
+ } else if idx == 2 {
1051
+ session.config.enable_agent_collaboration = false;
1052
+ match mint_core::save_config(&session.config) {
1053
+ Ok(()) => ui.push_notice("Multi-Agent collaboration set to: Disabled"),
1054
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
944
1055
  }
945
1056
  }
946
- _ => {}
947
1057
  }
948
1058
  }
949
1059
  Some(SlashResult::Handled)
@@ -954,19 +1064,21 @@ pub async fn handle_slash_command(
954
1064
 
955
1065
  // Soft toggle: `avatar_signal_disabled` hides the tool while keeping
956
1066
  // the token so the paired viewer never needs re-linking.
957
- let set_signal_disabled = |session: &mut InteractiveSession, disabled: bool| {
1067
+ let set_signal_disabled = |session: &mut InteractiveSession,
1068
+ ui: &mut dyn CommandUi,
1069
+ disabled: bool| {
958
1070
  session.config.avatar_signal_disabled = disabled;
959
1071
  match mint_core::save_config(&session.config) {
960
1072
  Ok(()) => {
961
1073
  if disabled {
962
- println!(
963
- "{DIM}[Avatar] avatar_signal disabled (token kept โ€” run /avatar to re-enable).{RESET}\n"
964
- );
1074
+ ui.push_notice(
1075
+ "[Avatar] avatar_signal disabled (token kept โ€” run /avatar to re-enable).",
1076
+ );
965
1077
  } else {
966
- println!("{DIM}[Avatar] avatar_signal enabled.{RESET}\n");
1078
+ ui.push_notice("[Avatar] avatar_signal enabled.");
967
1079
  }
968
1080
  }
969
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1081
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
970
1082
  }
971
1083
  };
972
1084
 
@@ -976,33 +1088,33 @@ pub async fn handle_slash_command(
976
1088
  "web" | "link web" => "web",
977
1089
  "desktop" | "link desktop" => "desktop",
978
1090
  "on" | "enable" => {
979
- set_signal_disabled(session, false);
1091
+ set_signal_disabled(session, ui, false);
980
1092
  return Some(SlashResult::Handled);
981
1093
  }
982
1094
  "off" | "disable" => {
983
- set_signal_disabled(session, true);
1095
+ set_signal_disabled(session, ui, true);
984
1096
  return Some(SlashResult::Handled);
985
1097
  }
986
1098
  "" | "link" => {
987
1099
  let options = vec![
988
- "No".to_string(),
989
- "Off (disable avatar_signal)".to_string(),
990
- "Web browser".to_string(),
991
- "Desktop app".to_string(),
1100
+ ChoiceItem::new("Web browser", "web"),
1101
+ ChoiceItem::new("Desktop app", "desktop"),
1102
+ ChoiceItem::new("Off (disable avatar_signal)", "off"),
1103
+ ChoiceItem::new("No", "no"),
992
1104
  ];
993
- match prompt_interactive_select("Avatar output", &options, &options[2]) {
994
- Ok(Some(sel)) if sel == "No" => return Some(SlashResult::Handled),
995
- Ok(Some(sel)) if sel.starts_with("Off") => {
996
- set_signal_disabled(session, true);
997
- return Some(SlashResult::Handled);
998
- }
999
- Ok(Some(sel)) if sel.starts_with("Desktop") => "desktop",
1000
- Ok(Some(_)) => "web",
1001
- // Cancelled / no TTY โ€” the picker has an explicit "No",
1002
- // so treat a cancel as "do nothing".
1105
+ match ui.prompt_choice("Avatar output", "", &options) {
1106
+ Ok(Some(idx)) => match options[idx].value.as_str() {
1107
+ "no" => return Some(SlashResult::Handled),
1108
+ "off" => {
1109
+ set_signal_disabled(session, ui, true);
1110
+ return Some(SlashResult::Handled);
1111
+ }
1112
+ "desktop" => "desktop",
1113
+ _ => "web",
1114
+ },
1003
1115
  Ok(None) => return Some(SlashResult::Handled),
1004
1116
  Err(e) => {
1005
- println!("{ERROR}Error selecting target:{RESET} {e}\n");
1117
+ ui.push_notice(format!("Error selecting target: {e}"));
1006
1118
  return Some(SlashResult::Handled);
1007
1119
  }
1008
1120
  }
@@ -1010,44 +1122,41 @@ pub async fn handle_slash_command(
1010
1122
  "status" => {
1011
1123
  let cfg = AvatarBridgeConfig::from_mint_config(&session.config);
1012
1124
  if !cfg.enabled {
1013
- println!("{WARN}[Avatar] No token yet โ€” run /avatar link first.{RESET}\n");
1125
+ ui.push_notice("[Avatar] No token yet โ€” run /avatar link first.");
1014
1126
  } else {
1015
- println!(
1016
- "{DIM}[Avatar] avatar_signal tool: {}{RESET}",
1017
- if session.config.avatar_signal_disabled {
1018
- "disabled"
1019
- } else {
1020
- "enabled"
1021
- }
1022
- );
1127
+ let tool_state = if session.config.avatar_signal_disabled {
1128
+ "disabled"
1129
+ } else {
1130
+ "enabled"
1131
+ };
1023
1132
  match fetch_channel_state(&cfg).await {
1024
1133
  Ok(state) => {
1025
1134
  let now_ms = std::time::SystemTime::now()
1026
1135
  .duration_since(std::time::UNIX_EPOCH)
1027
1136
  .map(|d| d.as_millis() as i64)
1028
1137
  .unwrap_or(0);
1029
- println!("\n{BLUE}[Avatar] Channel status:{RESET}");
1030
- println!(
1031
- " Model: {}",
1032
- state.model.unwrap_or_else(|| "not selected".into())
1033
- );
1034
- println!(" Viewers: {}", state.connected_clients);
1035
- println!(
1036
- " Last event: {}\n",
1037
- state
1038
- .last_agent_event_at
1039
- .map(|t| format!("{}s ago", (now_ms - t).max(0) / 1000))
1040
- .unwrap_or_else(|| "never".into())
1041
- );
1138
+ let last_ev = state
1139
+ .last_agent_event_at
1140
+ .map(|t| format!("{}s ago", (now_ms - t).max(0) / 1000))
1141
+ .unwrap_or_else(|| "never".into());
1142
+ ui.push_command_output(format!(
1143
+ "{DIM}[Avatar] avatar_signal tool: {tool_state}{RESET}\n\
1144
+ {BLUE}[Avatar] Channel status:{RESET}\n \
1145
+ Model: {}\n \
1146
+ Viewers: {}\n \
1147
+ Last event: {last_ev}",
1148
+ state.model.unwrap_or_else(|| "not selected".into()),
1149
+ state.connected_clients
1150
+ ));
1042
1151
  }
1043
- Err(error) => println!("{ERROR}[Avatar] {error}{RESET}\n"),
1152
+ Err(error) => ui.push_notice(format!("[Avatar] {error}")),
1044
1153
  }
1045
1154
  }
1046
1155
  return Some(SlashResult::Handled);
1047
1156
  }
1048
1157
  _ => {
1049
- println!(
1050
- "{WARN}Usage: /avatar [web|desktop|on|off|status] โ€” connects agent activity to Project Avatar{RESET}\n"
1158
+ ui.push_notice(
1159
+ "Usage: /avatar [web|desktop|on|off|status] โ€” connects agent activity to Project Avatar",
1051
1160
  );
1052
1161
  return Some(SlashResult::Handled);
1053
1162
  }
@@ -1060,179 +1169,185 @@ pub async fn handle_slash_command(
1060
1169
  session.config.avatar_token = AvatarBridgeConfig::generate_token();
1061
1170
  }
1062
1171
  if let Err(error) = mint_core::save_config(&session.config) {
1063
- println!("{ERROR}Config error:{RESET} {error}\n");
1172
+ ui.push_notice(format!("Config error: {error}"));
1064
1173
  return Some(SlashResult::Handled);
1065
1174
  }
1066
1175
  let cfg = AvatarBridgeConfig::from_mint_config(&session.config);
1067
1176
  let token = session.config.avatar_token.clone();
1068
1177
  if target == "desktop" {
1069
- println!(
1070
- "\n{MINT}[Avatar] Desktop app:{RESET}\n\
1178
+ ui.push_command_output(format!(
1179
+ "{MINT}[Avatar] Desktop app:{RESET}\n\
1071
1180
  1. Open the Project Avatar desktop app.\n\
1072
1181
  2. It has no address bar, so on first launch it generates its \
1073
1182
  own token โ€” paste this one into its \"Paste existing token...\" \
1074
1183
  field instead so it connects to the same channel Mint pushes to:\n\
1075
1184
  {DIM}{token}{RESET}\n\
1076
- It remembers the token after that; you won't need to paste it again.\n"
1077
- );
1185
+ It remembers the token after that; you won't need to paste it again."
1186
+ ));
1078
1187
  } else {
1079
- println!(
1080
- "\n{MINT}[Avatar] Share link:{RESET}\n{}\n",
1188
+ ui.push_command_output(format!(
1189
+ "{MINT}[Avatar] Share link:{RESET}\n{}",
1081
1190
  cfg.share_link().expect("token was just ensured non-empty")
1082
- );
1191
+ ));
1083
1192
  }
1084
1193
  Some(SlashResult::Handled)
1085
1194
  }
1086
1195
 
1087
1196
  "/autoskill" => {
1088
- if rest == "on" || rest == "off" {
1089
- session.config.auto_skill_writing = rest == "on";
1197
+ let apply = |session: &mut InteractiveSession, ui: &mut dyn CommandUi, on: bool| {
1198
+ session.config.auto_skill_writing = on;
1090
1199
  match mint_core::save_config(&session.config) {
1091
- Ok(()) => println!(
1092
- "{DIM}Skill suggestions (approval required to save) set to: {}{RESET}\n",
1093
- if session.config.auto_skill_writing {
1094
- "Enabled"
1095
- } else {
1096
- "Disabled"
1097
- }
1098
- ),
1099
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1200
+ Ok(()) => ui.push_notice(format!(
1201
+ "Skill suggestions (approval required to save) set to: {}",
1202
+ if on { "Enabled" } else { "Disabled" }
1203
+ )),
1204
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
1100
1205
  }
1206
+ };
1207
+ if rest == "on" || rest == "off" {
1208
+ apply(session, ui, rest == "on");
1101
1209
  } else if !rest.is_empty() {
1102
- println!("{WARN}Usage: /autoskill [on|off]{RESET}\n");
1210
+ ui.push_notice("Usage: /autoskill [on|off]");
1103
1211
  } else {
1104
- println!(
1105
- "{DIM}When enabled, the agent proposes a skill after a reusable task. Each proposed diff needs your approval before saving.{RESET}"
1106
- );
1107
1212
  let options = if session.config.auto_skill_writing {
1108
- vec!["on (current)".to_string(), "off".to_string()]
1109
- } else {
1110
- vec!["on".to_string(), "off (current)".to_string()]
1111
- };
1112
- let current = if session.config.auto_skill_writing {
1113
- &options[0]
1213
+ vec![
1214
+ ChoiceItem::new("on (current)", "on"),
1215
+ ChoiceItem::new("off", "off"),
1216
+ ]
1114
1217
  } else {
1115
- &options[1]
1218
+ vec![
1219
+ ChoiceItem::new("off (current)", "off"),
1220
+ ChoiceItem::new("on", "on"),
1221
+ ]
1116
1222
  };
1117
- match prompt_interactive_select("Skill Suggestions", &options, current) {
1118
- Ok(Some(sel)) => {
1119
- session.config.auto_skill_writing = sel.starts_with("on");
1120
- match mint_core::save_config(&session.config) {
1121
- Ok(()) => println!(
1122
- "{DIM}Skill suggestions (approval required to save) set to: {}{RESET}\n",
1123
- if session.config.auto_skill_writing {
1124
- "Enabled"
1125
- } else {
1126
- "Disabled"
1127
- }
1128
- ),
1129
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1130
- }
1131
- }
1132
- Ok(None) => println!("{DIM}Cancelled.{RESET}\n"),
1133
- Err(e) => println!("{ERROR}Error selecting option:{RESET} {e}\n"),
1223
+ match ui.prompt_choice(
1224
+ "Skill Suggestions",
1225
+ "When enabled, the agent proposes a skill after a reusable task. Each proposed diff needs your approval before saving.",
1226
+ &options,
1227
+ ) {
1228
+ Ok(Some(idx)) => apply(session, ui, options[idx].value == "on"),
1229
+ Ok(None) => ui.push_notice("Cancelled."),
1230
+ Err(e) => ui.push_notice(format!("Error selecting option: {e}")),
1134
1231
  }
1135
1232
  }
1136
1233
  Some(SlashResult::Handled)
1137
1234
  }
1138
1235
 
1139
1236
  "/autorecall" => {
1140
- let apply = |session: &mut InteractiveSession, on: bool| {
1237
+ let apply = |session: &mut InteractiveSession, ui: &mut dyn CommandUi, on: bool| {
1141
1238
  session.config.memory_recall = on;
1142
1239
  match mint_core::save_config(&session.config) {
1143
- Ok(()) => println!(
1144
- "{DIM}Memory recall set to: {}{RESET}\n",
1240
+ Ok(()) => ui.push_notice(format!(
1241
+ "Memory recall set to: {}",
1145
1242
  if on { "Enabled" } else { "Disabled" }
1146
- ),
1147
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1243
+ )),
1244
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
1148
1245
  }
1149
1246
  };
1150
1247
  if rest == "on" || rest == "off" {
1151
- apply(session, rest == "on");
1248
+ apply(session, ui, rest == "on");
1152
1249
  } else if !rest.is_empty() {
1153
- println!("{WARN}Usage: /autorecall [on|off]{RESET}\n");
1250
+ ui.push_notice("Usage: /autorecall [on|off]");
1154
1251
  } else {
1155
- println!(
1156
- "{DIM}When enabled, each turn full-text-searches this conversation's older messages and injects the few most relevant to what you just said.{RESET}"
1157
- );
1158
- let options = vec!["on (enable)".to_string(), "off (disable)".to_string()];
1159
- let current = if session.config.memory_recall {
1160
- &options[0]
1252
+ let options = if session.config.memory_recall {
1253
+ vec![
1254
+ ChoiceItem::new("on (enable)", "on"),
1255
+ ChoiceItem::new("off (disable)", "off"),
1256
+ ]
1161
1257
  } else {
1162
- &options[1]
1258
+ vec![
1259
+ ChoiceItem::new("off (disable)", "off"),
1260
+ ChoiceItem::new("on (enable)", "on"),
1261
+ ]
1163
1262
  };
1164
- match prompt_interactive_select("Memory Recall", &options, current) {
1165
- Ok(Some(sel)) => apply(session, sel.starts_with("on")),
1166
- Ok(None) => println!("{DIM}Cancelled.{RESET}\n"),
1167
- Err(e) => println!("{ERROR}Error selecting option:{RESET} {e}\n"),
1263
+ match ui.prompt_choice(
1264
+ "Memory Recall",
1265
+ "When enabled, each turn full-text-searches this conversation's older messages and injects the few most relevant to what you just said.",
1266
+ &options,
1267
+ ) {
1268
+ Ok(Some(idx)) => apply(session, ui, options[idx].value == "on"),
1269
+ Ok(None) => ui.push_notice("Cancelled."),
1270
+ Err(e) => ui.push_notice(format!("Error selecting option: {e}")),
1168
1271
  }
1169
1272
  }
1170
1273
  Some(SlashResult::Handled)
1171
1274
  }
1172
1275
 
1173
1276
  "/autofacts" => {
1174
- let apply = |session: &mut InteractiveSession, on: bool| {
1277
+ let apply = |session: &mut InteractiveSession, ui: &mut dyn CommandUi, on: bool| {
1175
1278
  session.config.auto_fact_extraction = on;
1176
1279
  match mint_core::save_config(&session.config) {
1177
- Ok(()) => println!(
1178
- "{DIM}Auto fact extraction set to: {}{RESET}\n",
1280
+ Ok(()) => ui.push_notice(format!(
1281
+ "Auto fact extraction set to: {}",
1179
1282
  if on { "Enabled" } else { "Disabled" }
1180
- ),
1181
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1283
+ )),
1284
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
1182
1285
  }
1183
1286
  };
1184
1287
  if rest == "on" || rest == "off" {
1185
- apply(session, rest == "on");
1288
+ apply(session, ui, rest == "on");
1186
1289
  } else if !rest.is_empty() {
1187
- println!("{WARN}Usage: /autofacts [on|off]{RESET}\n");
1290
+ ui.push_notice("Usage: /autofacts [on|off]");
1188
1291
  } else {
1189
- println!(
1190
- "{DIM}When enabled, a turn that states a durable preference, naming, or standing instruction triggers a background pass that saves it to long-term memory.{RESET}"
1191
- );
1192
- let options = vec!["on (enable)".to_string(), "off (disable)".to_string()];
1193
- let current = if session.config.auto_fact_extraction {
1194
- &options[0]
1292
+ let options = if session.config.auto_fact_extraction {
1293
+ vec![
1294
+ ChoiceItem::new("on (enable)", "on"),
1295
+ ChoiceItem::new("off (disable)", "off"),
1296
+ ]
1195
1297
  } else {
1196
- &options[1]
1298
+ vec![
1299
+ ChoiceItem::new("off (disable)", "off"),
1300
+ ChoiceItem::new("on (enable)", "on"),
1301
+ ]
1197
1302
  };
1198
- match prompt_interactive_select("Auto Fact Extraction", &options, current) {
1199
- Ok(Some(sel)) => apply(session, sel.starts_with("on")),
1200
- Ok(None) => println!("{DIM}Cancelled.{RESET}\n"),
1201
- Err(e) => println!("{ERROR}Error selecting option:{RESET} {e}\n"),
1303
+ match ui.prompt_choice(
1304
+ "Auto Fact Extraction",
1305
+ "When enabled, a turn that states a durable preference, naming, or standing instruction triggers a background pass that saves it to long-term memory.",
1306
+ &options,
1307
+ ) {
1308
+ Ok(Some(idx)) => apply(session, ui, options[idx].value == "on"),
1309
+ Ok(None) => ui.push_notice("Cancelled."),
1310
+ Err(e) => ui.push_notice(format!("Error selecting option: {e}")),
1202
1311
  }
1203
1312
  }
1204
1313
  Some(SlashResult::Handled)
1205
1314
  }
1206
1315
 
1207
1316
  "/factrecall" => {
1208
- let apply = |session: &mut InteractiveSession, on: bool| {
1317
+ let apply = |session: &mut InteractiveSession, ui: &mut dyn CommandUi, on: bool| {
1209
1318
  session.config.semantic_fact_recall = on;
1210
1319
  match mint_core::save_config(&session.config) {
1211
- Ok(()) => println!(
1212
- "{DIM}Semantic fact recall set to: {}{RESET}\n",
1320
+ Ok(()) => ui.push_notice(format!(
1321
+ "Semantic fact recall set to: {}",
1213
1322
  if on { "Enabled" } else { "Disabled" }
1214
- ),
1215
- Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1323
+ )),
1324
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
1216
1325
  }
1217
1326
  };
1218
1327
  if rest == "on" || rest == "off" {
1219
- apply(session, rest == "on");
1328
+ apply(session, ui, rest == "on");
1220
1329
  } else if !rest.is_empty() {
1221
- println!("{WARN}Usage: /factrecall [on|off]{RESET}\n");
1330
+ ui.push_notice("Usage: /factrecall [on|off]");
1222
1331
  } else {
1223
- println!(
1224
- "{DIM}When your stored facts overflow their per-turn budget, this fills the overflow with the facts most relevant to your message instead of just the newest ones (the newest few are always kept).{RESET}"
1225
- );
1226
- let options = vec!["on (enable)".to_string(), "off (disable)".to_string()];
1227
- let current = if session.config.semantic_fact_recall {
1228
- &options[0]
1332
+ let options = if session.config.semantic_fact_recall {
1333
+ vec![
1334
+ ChoiceItem::new("on (enable)", "on"),
1335
+ ChoiceItem::new("off (disable)", "off"),
1336
+ ]
1229
1337
  } else {
1230
- &options[1]
1338
+ vec![
1339
+ ChoiceItem::new("off (disable)", "off"),
1340
+ ChoiceItem::new("on (enable)", "on"),
1341
+ ]
1231
1342
  };
1232
- match prompt_interactive_select("Semantic Fact Recall", &options, current) {
1233
- Ok(Some(sel)) => apply(session, sel.starts_with("on")),
1234
- Ok(None) => println!("{DIM}Cancelled.{RESET}\n"),
1235
- Err(e) => println!("{ERROR}Error selecting option:{RESET} {e}\n"),
1343
+ match ui.prompt_choice(
1344
+ "Semantic Fact Recall",
1345
+ "When your stored facts overflow their per-turn budget, this fills the overflow with the facts most relevant to your message instead of just the newest ones (the newest few are always kept).",
1346
+ &options,
1347
+ ) {
1348
+ Ok(Some(idx)) => apply(session, ui, options[idx].value == "on"),
1349
+ Ok(None) => ui.push_notice("Cancelled."),
1350
+ Err(e) => ui.push_notice(format!("Error selecting option: {e}")),
1236
1351
  }
1237
1352
  }
1238
1353
  Some(SlashResult::Handled)
@@ -1253,25 +1368,25 @@ pub async fn handle_slash_command(
1253
1368
  ("user", None, rest)
1254
1369
  };
1255
1370
  if body.is_empty() {
1256
- println!("{WARN}Usage: /remember [here] <text>{RESET}\n");
1371
+ ui.push_notice("Usage: /remember [here] <text>");
1257
1372
  return Some(SlashResult::Handled);
1258
1373
  }
1259
1374
  match MemoryStore::open_default() {
1260
1375
  Ok(memory) => {
1261
1376
  match memory.add_fact(scope, project_path.as_deref(), body, None, None) {
1262
- Ok(Some(_)) => println!(
1263
- "{DIM}Remembered{}.{RESET}\n",
1377
+ Ok(Some(_)) => ui.push_notice(format!(
1378
+ "Remembered{}.",
1264
1379
  if scope == "project" {
1265
1380
  " (this project)"
1266
1381
  } else {
1267
1382
  ""
1268
1383
  }
1269
- ),
1270
- Ok(None) => println!("{DIM}Already remembered.{RESET}\n"),
1271
- Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
1384
+ )),
1385
+ Ok(None) => ui.push_notice("Already remembered."),
1386
+ Err(e) => ui.push_notice(format!("Error: {e}")),
1272
1387
  }
1273
1388
  }
1274
- Err(e) => println!("{ERROR}Memory error:{RESET} {e}\n"),
1389
+ Err(e) => ui.push_notice(format!("Memory error: {e}")),
1275
1390
  }
1276
1391
  Some(SlashResult::Handled)
1277
1392
  }
@@ -1301,42 +1416,45 @@ pub async fn handle_slash_command(
1301
1416
  }
1302
1417
 
1303
1418
  let selected_provider = if rest.is_empty() {
1304
- let options: Vec<String> = available.iter().map(|s| s.to_string()).collect();
1305
- match prompt_interactive_select(
1306
- "Select Image Generation provider",
1307
- &options,
1308
- &session.config.image_gen_provider,
1309
- ) {
1310
- Ok(Some(p)) => Some(p),
1419
+ let mut options: Vec<ChoiceItem> =
1420
+ available.iter().map(|s| ChoiceItem::new(*s, *s)).collect();
1421
+ if let Some(pos) = options
1422
+ .iter()
1423
+ .position(|o| o.value == session.config.image_gen_provider)
1424
+ && pos > 0
1425
+ {
1426
+ let item = options.remove(pos);
1427
+ options.insert(0, item);
1428
+ }
1429
+ match ui.prompt_choice("Select Image Generation provider", "", &options) {
1430
+ Ok(Some(idx)) => Some(options[idx].value.clone()),
1311
1431
  Ok(None) => {
1312
- println!("Cancelled image provider selection.\n");
1432
+ ui.push_notice("Cancelled image provider selection.");
1313
1433
  None
1314
1434
  }
1315
1435
  Err(e) => {
1316
- println!("{ERROR}Error selecting provider:{RESET} {e}\n");
1436
+ ui.push_notice(format!("Error selecting provider: {e}"));
1317
1437
  None
1318
1438
  }
1319
1439
  }
1440
+ } else if available.contains(&rest) {
1441
+ Some(rest.to_owned())
1320
1442
  } else {
1321
- if available.contains(&rest) {
1322
- Some(rest.to_owned())
1323
- } else {
1324
- println!("{ERROR}Provider '{rest}' is not configured or invalid.{RESET}\n");
1325
- None
1326
- }
1443
+ ui.push_notice(format!("Provider '{rest}' is not configured or invalid."));
1444
+ None
1327
1445
  };
1328
1446
 
1329
1447
  if let Some(provider) = selected_provider {
1330
- print!("{DIM}Fetching available image models for {provider}...{RESET}\r");
1331
- let _ = std::io::Write::flush(&mut std::io::stdout());
1448
+ ui.set_status(vec![format!(
1449
+ "Fetching available image models for {provider}..."
1450
+ )]);
1332
1451
  let mut model_options =
1333
1452
  mint_core::media::image_models::image_model_options_for_provider_async(
1334
1453
  &session.config,
1335
1454
  &provider,
1336
1455
  )
1337
1456
  .await;
1338
- print!("\r\x1b[2K");
1339
- let _ = std::io::Write::flush(&mut std::io::stdout());
1457
+ ui.clear_status();
1340
1458
 
1341
1459
  let current_model =
1342
1460
  mint_core::media::image_models::active_image_model_for_provider(
@@ -1349,19 +1467,29 @@ pub async fn handle_slash_command(
1349
1467
  }
1350
1468
 
1351
1469
  if !model_options.is_empty() {
1352
- match prompt_interactive_select(
1470
+ let mut choices: Vec<ChoiceItem> = model_options
1471
+ .iter()
1472
+ .map(|m| ChoiceItem::new(m.clone(), m.clone()))
1473
+ .collect();
1474
+ if let Some(pos) = choices.iter().position(|c| c.value == current_model)
1475
+ && pos > 0
1476
+ {
1477
+ let item = choices.remove(pos);
1478
+ choices.insert(0, item);
1479
+ }
1480
+ match ui.prompt_choice(
1353
1481
  &format!(
1354
1482
  "Select {} model",
1355
1483
  mint_core::media::image_models::image_provider_display_name(&provider)
1356
1484
  ),
1357
- &model_options,
1358
- &current_model,
1485
+ "",
1486
+ &choices,
1359
1487
  ) {
1360
- Ok(Some(model)) => {
1488
+ Ok(Some(idx)) => {
1361
1489
  mint_core::media::image_models::set_active_image_provider_model(
1362
1490
  &mut session.config,
1363
1491
  &provider,
1364
- Some(&model),
1492
+ Some(&choices[idx].value),
1365
1493
  );
1366
1494
  }
1367
1495
  Ok(None) => {
@@ -1372,7 +1500,7 @@ pub async fn handle_slash_command(
1372
1500
  );
1373
1501
  }
1374
1502
  Err(e) => {
1375
- println!("{ERROR}Error selecting image model:{RESET} {e}\n");
1503
+ ui.push_notice(format!("Error selecting image model: {e}"));
1376
1504
  }
1377
1505
  }
1378
1506
  } else {
@@ -1390,13 +1518,13 @@ pub async fn handle_slash_command(
1390
1518
  &session.config,
1391
1519
  &provider,
1392
1520
  );
1393
- println!(
1394
- "{DIM}Switched default image provider to: {} โ€ข {}{RESET}\n",
1521
+ ui.push_notice(format!(
1522
+ "Switched default image provider to: {} โ€ข {}",
1395
1523
  mint_core::media::image_models::image_provider_display_name(&provider),
1396
1524
  active
1397
- );
1525
+ ));
1398
1526
  }
1399
- Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
1527
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
1400
1528
  }
1401
1529
  }
1402
1530
  Some(SlashResult::Handled)
@@ -1412,49 +1540,24 @@ pub async fn handle_slash_command(
1412
1540
  available.push("veo");
1413
1541
  }
1414
1542
 
1415
- let current_provider = session
1416
- .config
1417
- .extra
1418
- .get("videoGenProvider")
1419
- .and_then(|v| v.as_str())
1420
- .unwrap_or("veo")
1421
- .to_string();
1422
-
1423
1543
  let selected_provider = if rest.is_empty() {
1424
- let options = vec!["Google Veo (Gemini Videos)".to_string()];
1425
- let current_display = if current_provider == "veo" {
1426
- "Google Veo (Gemini Videos)"
1427
- } else {
1428
- &current_provider
1429
- };
1430
- match prompt_interactive_select(
1431
- "Select Video Generation provider",
1432
- &options,
1433
- current_display,
1434
- ) {
1435
- Ok(Some(p)) => {
1436
- if p == "Google Veo (Gemini Videos)" {
1437
- Some("veo".to_string())
1438
- } else {
1439
- Some(p)
1440
- }
1441
- }
1544
+ let options = vec![ChoiceItem::new("Google Veo (Gemini Videos)", "veo")];
1545
+ match ui.prompt_choice("Select Video Generation provider", "", &options) {
1546
+ Ok(Some(idx)) => Some(options[idx].value.clone()),
1442
1547
  Ok(None) => {
1443
- println!("Cancelled video provider selection.\n");
1548
+ ui.push_notice("Cancelled video provider selection.");
1444
1549
  None
1445
1550
  }
1446
1551
  Err(e) => {
1447
- println!("{ERROR}Error selecting provider:{RESET} {e}\n");
1552
+ ui.push_notice(format!("Error selecting provider: {e}"));
1448
1553
  None
1449
1554
  }
1450
1555
  }
1556
+ } else if rest == "veo" || rest == "Google Veo (Gemini Videos)" {
1557
+ Some("veo".to_string())
1451
1558
  } else {
1452
- if rest == "veo" || rest == "Google Veo (Gemini Videos)" {
1453
- Some("veo".to_string())
1454
- } else {
1455
- println!("{ERROR}Provider '{rest}' is not configured or invalid.{RESET}\n");
1456
- None
1457
- }
1559
+ ui.push_notice(format!("Provider '{rest}' is not configured or invalid."));
1560
+ None
1458
1561
  };
1459
1562
 
1460
1563
  if let Some(provider) = selected_provider {
@@ -1469,12 +1572,11 @@ pub async fn handle_slash_command(
1469
1572
  } else {
1470
1573
  &provider
1471
1574
  };
1472
- println!(
1473
- "{DIM}Switched default video provider to: {}{RESET}\n",
1474
- display_name
1475
- );
1575
+ ui.push_notice(format!(
1576
+ "Switched default video provider to: {display_name}"
1577
+ ));
1476
1578
  }
1477
- Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
1579
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
1478
1580
  }
1479
1581
  }
1480
1582
  Some(SlashResult::Handled)
@@ -1517,8 +1619,8 @@ pub async fn handle_slash_command(
1517
1619
  }
1518
1620
 
1519
1621
  if available.is_empty() {
1520
- println!(
1521
- "{ERROR}No search provider is configured yet.{RESET} Run onboarding or set a Google/Brave key or SearXNG URL in Settings first.\n"
1622
+ ui.push_notice(
1623
+ "No search provider is configured yet. Run onboarding or set a Google/Brave key or SearXNG URL in Settings first.",
1522
1624
  );
1523
1625
  return Some(SlashResult::Handled);
1524
1626
  }
@@ -1532,30 +1634,24 @@ pub async fn handle_slash_command(
1532
1634
  .to_string();
1533
1635
 
1534
1636
  let selected_provider = if rest.is_empty() {
1535
- let options: Vec<String> = available
1637
+ let mut options: Vec<ChoiceItem> = available
1536
1638
  .iter()
1537
- .map(|(_, label)| label.to_string())
1639
+ .map(|(key, label)| ChoiceItem::new(*label, *key))
1538
1640
  .collect();
1539
- let current_display = available
1540
- .iter()
1541
- .find(|(key, _)| *key == current_provider)
1542
- .map(|(_, label)| *label)
1543
- .unwrap_or(available[0].1);
1544
- match prompt_interactive_select(
1545
- "Select Web Search provider",
1546
- &options,
1547
- current_display,
1548
- ) {
1549
- Ok(Some(label)) => available
1550
- .iter()
1551
- .find(|(_, l)| *l == label)
1552
- .map(|(key, _)| key.to_string()),
1641
+ if let Some(pos) = options.iter().position(|o| o.value == current_provider)
1642
+ && pos > 0
1643
+ {
1644
+ let item = options.remove(pos);
1645
+ options.insert(0, item);
1646
+ }
1647
+ match ui.prompt_choice("Select Web Search provider", "", &options) {
1648
+ Ok(Some(idx)) => Some(options[idx].value.clone()),
1553
1649
  Ok(None) => {
1554
- println!("Cancelled search provider selection.\n");
1650
+ ui.push_notice("Cancelled search provider selection.");
1555
1651
  None
1556
1652
  }
1557
1653
  Err(e) => {
1558
- println!("{ERROR}Error selecting provider:{RESET} {e}\n");
1654
+ ui.push_notice(format!("Error selecting provider: {e}"));
1559
1655
  None
1560
1656
  }
1561
1657
  }
@@ -1565,7 +1661,7 @@ pub async fn handle_slash_command(
1565
1661
  {
1566
1662
  Some(key.to_string())
1567
1663
  } else {
1568
- println!("{ERROR}Provider '{rest}' is not configured or invalid.{RESET}\n");
1664
+ ui.push_notice(format!("Provider '{rest}' is not configured or invalid."));
1569
1665
  None
1570
1666
  };
1571
1667
 
@@ -1581,12 +1677,11 @@ pub async fn handle_slash_command(
1581
1677
  .find(|(key, _)| *key == provider)
1582
1678
  .map(|(_, label)| *label)
1583
1679
  .unwrap_or(&provider);
1584
- println!(
1585
- "{DIM}Switched default web search provider to: {}{RESET}\n",
1586
- display_name
1587
- );
1680
+ ui.push_notice(format!(
1681
+ "Switched default web search provider to: {display_name}"
1682
+ ));
1588
1683
  }
1589
- Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
1684
+ Err(error) => ui.push_notice(format!("Config error: {error}")),
1590
1685
  }
1591
1686
  }
1592
1687
  Some(SlashResult::Handled)
@@ -2665,11 +2760,9 @@ pub async fn handle_slash_command(
2665
2760
  Some(SlashResult::Handled)
2666
2761
  }
2667
2762
 
2668
- "/release-notes" => {
2669
- ui.push_command_output(format!(
2670
- "\n{BLUE}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{RESET}\n{}\n{BLUE}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{RESET}",
2671
- mint_core::release_notes::text()
2672
- ));
2763
+ "/release-notes" | "/release_notes" | "/release-note" | "/release_note"
2764
+ | "/releasenotes" | "/releasenote" => {
2765
+ ui.push_release_notes(&mint_core::release_notes::text());
2673
2766
  Some(SlashResult::Handled)
2674
2767
  }
2675
2768
 
@@ -3101,6 +3194,74 @@ pub async fn handle_slash_command(
3101
3194
  }
3102
3195
  }
3103
3196
 
3197
+ "/voice" => {
3198
+ let sub = rest.trim().to_ascii_lowercase();
3199
+ if sub == "stop" || sub == "cancel" {
3200
+ let _ = super::stop_cli_voice_recording();
3201
+ ui.push_notice("Voice recording stopped.");
3202
+ return Some(SlashResult::Handled);
3203
+ }
3204
+ if !super::is_cli_voice_recording()
3205
+ && let Err(err) = super::start_cli_voice_recording()
3206
+ {
3207
+ ui.push_notice(format!("Microphone error: {err}"));
3208
+ return Some(SlashResult::Handled);
3209
+ }
3210
+ let choices = vec![
3211
+ ChoiceItem::with_description(
3212
+ "Transcribe & Send",
3213
+ "Stop recording, transcribe speech, and send prompt to Mint",
3214
+ "send",
3215
+ ),
3216
+ ChoiceItem::with_description("Cancel", "Discard current voice recording", "cancel"),
3217
+ ];
3218
+ match ui.prompt_choice(
3219
+ "๐ŸŽ™ Recording Voice",
3220
+ "Speak into your microphone, then select an action:",
3221
+ &choices,
3222
+ ) {
3223
+ Ok(Some(0)) => {
3224
+ let wav_bytes = match super::stop_cli_voice_recording() {
3225
+ Ok(Some(bytes)) => bytes,
3226
+ Ok(None) => {
3227
+ ui.push_notice("No active voice recording found.");
3228
+ return Some(SlashResult::Handled);
3229
+ }
3230
+ Err(err) => {
3231
+ ui.push_notice(format!("Voice recording error: {err}"));
3232
+ return Some(SlashResult::Handled);
3233
+ }
3234
+ };
3235
+ ui.push_notice("Transcribing voice recordingโ€ฆ");
3236
+ match mint_core::mic_transcribe::transcribe_recording(
3237
+ &session.config,
3238
+ wav_bytes,
3239
+ )
3240
+ .await
3241
+ {
3242
+ Ok(text) if !text.trim().is_empty() => {
3243
+ let cleaned = text.trim().to_string();
3244
+ ui.push_notice(format!("๐ŸŽ™ Heard: {cleaned}"));
3245
+ Some(SlashResult::ForwardToAgent(cleaned))
3246
+ }
3247
+ Ok(_) => {
3248
+ ui.push_notice("No speech detected in voice recording.");
3249
+ Some(SlashResult::Handled)
3250
+ }
3251
+ Err(err) => {
3252
+ ui.push_notice(format!("Voice transcription failed: {err}"));
3253
+ Some(SlashResult::Handled)
3254
+ }
3255
+ }
3256
+ }
3257
+ _ => {
3258
+ let _ = super::stop_cli_voice_recording();
3259
+ ui.push_notice("Voice recording cancelled.");
3260
+ Some(SlashResult::Handled)
3261
+ }
3262
+ }
3263
+ }
3264
+
3104
3265
  _ => {
3105
3266
  // Unknown slash command โ€” treat as normal message to the agent
3106
3267
  None
@@ -4263,4 +4424,120 @@ mod tests {
4263
4424
  assert!(matches!(res, Some(SlashResult::Handled)));
4264
4425
  assert!(session.fast_mode);
4265
4426
  }
4427
+
4428
+ #[tokio::test]
4429
+ async fn slash_release_notes_routes_to_push_release_notes() {
4430
+ let mut session = InteractiveSession {
4431
+ chat_id: mint_core::CHAT_CLI_ID.to_string(),
4432
+ config: MintConfig::default(),
4433
+ current_dir: std::env::current_dir().unwrap(),
4434
+ fast_mode: false,
4435
+ plan_mode: false,
4436
+ pending_image: None,
4437
+ history: Vec::new(),
4438
+ jobs: BackgroundJobs::new(),
4439
+ };
4440
+
4441
+ for cmd in [
4442
+ "/release-notes",
4443
+ "/release_notes",
4444
+ "/release-note",
4445
+ "/release_note",
4446
+ ] {
4447
+ let mut ui = MockCommandUi::new();
4448
+ let res = handle_slash_command(&mut session, &mut ui, cmd).await;
4449
+ assert!(matches!(res, Some(SlashResult::Handled)));
4450
+ assert!(
4451
+ ui.command_outputs.is_empty(),
4452
+ "must not route {cmd} to push_command_output"
4453
+ );
4454
+ assert_eq!(
4455
+ ui.release_notes.len(),
4456
+ 1,
4457
+ "must route {cmd} to push_release_notes"
4458
+ );
4459
+ assert!(ui.release_notes[0].contains("Release Notes"));
4460
+ }
4461
+ }
4462
+
4463
+ #[test]
4464
+ fn format_plan_meta_parses_timestamp() {
4465
+ let p1 = Path::new("/tmp/plans/20261102-182231-my-plan.md");
4466
+ assert_eq!(format_plan_meta(p1), "2026-11-02 18:22:31 ยท my-plan");
4467
+
4468
+ let p2 = Path::new("/tmp/plans/20261102-180001.md");
4469
+ assert_eq!(format_plan_meta(p2), "2026-11-02 18:00:01");
4470
+
4471
+ let p3 = Path::new("/tmp/plans/custom-plan.md");
4472
+ assert_eq!(format_plan_meta(p3), "custom-plan.md");
4473
+ }
4474
+
4475
+ #[tokio::test]
4476
+ async fn slash_plan_list_interactive_selection_and_show() {
4477
+ let temp = std::env::temp_dir().join(format!("mint-plan-test-{}", std::process::id()));
4478
+ let plans_dir = temp.join(".agents").join("plans");
4479
+ std::fs::create_dir_all(&plans_dir).unwrap();
4480
+
4481
+ let plan1_content = "# First Plan\n- [ ] Task 1";
4482
+ let plan2_content = "# Second Plan\n- [x] Done";
4483
+ std::fs::write(plans_dir.join("20261102-180000-plan-one.md"), plan1_content).unwrap();
4484
+ std::fs::write(plans_dir.join("20261102-183000-plan-two.md"), plan2_content).unwrap();
4485
+
4486
+ let mut session = InteractiveSession {
4487
+ chat_id: mint_core::CHAT_CLI_ID.to_string(),
4488
+ config: MintConfig::default(),
4489
+ current_dir: temp.clone(),
4490
+ fast_mode: false,
4491
+ plan_mode: false,
4492
+ pending_image: None,
4493
+ history: Vec::new(),
4494
+ jobs: BackgroundJobs::new(),
4495
+ };
4496
+
4497
+ // 1. /plan list with interactive selection (pick index 0, which is the newest plan2)
4498
+ let mut ui = MockCommandUi::new();
4499
+ ui.choice_answers.push(Some(0));
4500
+ let res = handle_slash_command(&mut session, &mut ui, "/plan list").await;
4501
+ assert!(matches!(res, Some(SlashResult::Handled)));
4502
+ assert_eq!(ui.plans.len(), 1);
4503
+ assert_eq!(ui.plans[0], plan2_content);
4504
+
4505
+ // 2. /plan list 1 directly views the first plan
4506
+ let mut ui = MockCommandUi::new();
4507
+ let res = handle_slash_command(&mut session, &mut ui, "/plan list 1").await;
4508
+ assert!(matches!(res, Some(SlashResult::Handled)));
4509
+ assert_eq!(ui.plans.len(), 1);
4510
+ assert_eq!(ui.plans[0], plan2_content);
4511
+
4512
+ // 3. /plan show 2 views the second plan
4513
+ let mut ui = MockCommandUi::new();
4514
+ let res = handle_slash_command(&mut session, &mut ui, "/plan show 2").await;
4515
+ assert!(matches!(res, Some(SlashResult::Handled)));
4516
+ assert_eq!(ui.plans.len(), 1);
4517
+ assert_eq!(ui.plans[0], plan1_content);
4518
+
4519
+ let _ = std::fs::remove_dir_all(temp);
4520
+ }
4521
+
4522
+ #[tokio::test]
4523
+ async fn slash_voice_stop_is_handled_cleanly() {
4524
+ let mut session = InteractiveSession {
4525
+ chat_id: mint_core::CHAT_CLI_ID.to_string(),
4526
+ config: MintConfig::default(),
4527
+ current_dir: std::env::current_dir().unwrap(),
4528
+ fast_mode: false,
4529
+ plan_mode: false,
4530
+ pending_image: None,
4531
+ history: Vec::new(),
4532
+ jobs: BackgroundJobs::new(),
4533
+ };
4534
+ let mut ui = MockCommandUi::new();
4535
+ let res = handle_slash_command(&mut session, &mut ui, "/voice stop").await;
4536
+ assert!(matches!(res, Some(SlashResult::Handled)));
4537
+ assert!(
4538
+ ui.notices.iter().any(|n| n.contains("Voice recording")),
4539
+ "expected voice notice, got: {:?}",
4540
+ ui.notices
4541
+ );
4542
+ }
4266
4543
  }