@pheem49/mint 1.14.0 → 1.15.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 (585) hide show
  1. package/CLAUDE.md +1 -1
  2. package/Cargo.lock +291 -11
  3. package/Cargo.toml +1 -1
  4. package/README.md +71 -9
  5. package/ROADMAP.md +30 -0
  6. package/Release_Note.md +579 -465
  7. package/crates/mint-cli/Cargo.toml +3 -1
  8. package/crates/mint-cli/src/agent/approval_prompts.rs +16 -3
  9. package/crates/mint-cli/src/agent/live_status.rs +367 -118
  10. package/crates/mint-cli/src/agent/markdown_render.rs +601 -61
  11. package/crates/mint-cli/src/agent.rs +452 -215
  12. package/crates/mint-cli/src/commands/agent.rs +16 -21
  13. package/crates/mint-cli/src/commands/eval.rs +9 -5
  14. package/crates/mint-cli/src/commands/git.rs +204 -0
  15. package/crates/mint-cli/src/commands/integrations.rs +28 -0
  16. package/crates/mint-cli/src/commands/knowledge.rs +1 -0
  17. package/crates/mint-cli/src/commands/mod.rs +37 -2
  18. package/crates/mint-cli/src/commands/tailnet_web.rs +235 -0
  19. package/crates/mint-cli/src/image.rs +26 -3
  20. package/crates/mint-cli/src/interactive/chat_tui.rs +4742 -0
  21. package/crates/mint-cli/src/interactive/command_ui.rs +477 -0
  22. package/crates/mint-cli/src/interactive/format.rs +8 -0
  23. package/crates/mint-cli/src/interactive/input_box.rs +38 -2
  24. package/crates/mint-cli/src/interactive/picker.rs +180 -37
  25. package/crates/mint-cli/src/interactive/resume_picker.rs +686 -0
  26. package/crates/mint-cli/src/interactive/slash_commands.rs +868 -450
  27. package/crates/mint-cli/src/interactive/thought_viewer.rs +286 -0
  28. package/crates/mint-cli/src/interactive.rs +400 -136
  29. package/crates/mint-cli/src/main.rs +84 -23
  30. package/crates/mint-cli/src/markdown.rs +12 -5
  31. package/crates/mint-cli/src/onboard.rs +32 -30
  32. package/crates/mint-cli/src/plugins_cli.rs +8 -5
  33. package/crates/mint-cli/src/setup.rs +10 -8
  34. package/crates/mint-cli/src/terminal_theme.rs +187 -0
  35. package/crates/mint-cli/src/updater.rs +56 -21
  36. package/crates/mint-core/src/agent/agent_loop.rs +1 -0
  37. package/crates/mint-core/src/agent/chat.rs +843 -177
  38. package/crates/mint-core/src/agent/code_tools.rs +83 -2
  39. package/crates/mint-core/src/agent/memory.rs +658 -20
  40. package/crates/mint-core/src/agent/skills.rs +1 -1
  41. package/crates/mint-core/src/api_server/routes/chat.rs +31 -5
  42. package/crates/mint-core/src/api_server/routes/linked_folders.rs +118 -3
  43. package/crates/mint-core/src/api_server/routes/misc.rs +10 -14
  44. package/crates/mint-core/src/api_server/routes/sessions.rs +44 -0
  45. package/crates/mint-core/src/api_server.rs +22 -3
  46. package/crates/mint-core/src/avatar_bridge.rs +2 -0
  47. package/crates/mint-core/src/browser/cdp.rs +74 -83
  48. package/crates/mint-core/src/browser/evidence.rs +260 -0
  49. package/crates/mint-core/src/browser/input.rs +1 -1
  50. package/crates/mint-core/src/browser/interact.rs +98 -124
  51. package/crates/mint-core/src/browser/lifecycle.rs +75 -4
  52. package/crates/mint-core/src/browser/mod.rs +16 -2
  53. package/crates/mint-core/src/browser/navigate.rs +27 -19
  54. package/crates/mint-core/src/browser/observe.js +40 -0
  55. package/crates/mint-core/src/browser/session.rs +1558 -0
  56. package/crates/mint-core/src/git/branches.rs +348 -0
  57. package/crates/mint-core/src/git/checkpoint.rs +40 -0
  58. package/crates/mint-core/src/git/diff.rs +206 -0
  59. package/crates/mint-core/src/git/mod.rs +10 -2
  60. package/crates/mint-core/src/integrations/channels.rs +1 -0
  61. package/crates/mint-core/src/lib.rs +22 -14
  62. package/crates/mint-core/src/live_sync.rs +28 -9
  63. package/crates/mint-core/src/media/auto_shorts.rs +1 -0
  64. package/crates/mint-core/src/media/gemini_live.rs +12 -1
  65. package/crates/mint-core/src/media/mic_transcribe.rs +1 -0
  66. package/crates/mint-core/src/media/subtitle.rs +1 -0
  67. package/crates/mint-core/src/orchestration/context_render.rs +1 -0
  68. package/crates/mint-core/src/orchestration/memory_skill.rs +6 -1
  69. package/crates/mint-core/src/orchestration/mod.rs +1024 -271
  70. package/crates/mint-core/src/orchestration/tools/browser.rs +16 -139
  71. package/crates/mint-core/src/orchestration/tools/misc.rs +2 -1
  72. package/crates/mint-core/src/orchestration/tools/web.rs +1 -1
  73. package/crates/mint-core/src/prompts/agent.rs +58 -29
  74. package/crates/mint-core/src/prompts/chat.rs +1 -1
  75. package/crates/mint-core/src/prompts/persona.rs +1 -1
  76. package/crates/mint-core/src/prompts/tool_catalog.rs +93 -25
  77. package/crates/mint-core/src/search/linked_folders/index.rs +732 -0
  78. package/crates/mint-core/src/search/linked_folders.rs +391 -130
  79. package/crates/mint-core/src/slash/mod.rs +470 -22
  80. package/crates/mint-core/src/slash/model_fetcher.rs +12 -3
  81. package/crates/mint-core/src/slash/models.rs +66 -10
  82. package/crates/mint-core/src/system/config.rs +230 -0
  83. package/crates/mint-core/src/system/workspace.rs +228 -0
  84. package/crates/mint-core/src/system.rs +1 -0
  85. package/crates/mint-core/tests/memory_persistence.rs +39 -0
  86. package/mint-presentation-demo.mp4 +0 -0
  87. package/out/web/assets/CliSessionsView-BxUVWEEL.js +1 -0
  88. package/out/web/assets/CliSessionsView-CBDTXuQv.css +1 -0
  89. package/out/web/assets/CodeReviewPage-DkMgfcos.css +1 -0
  90. package/out/web/assets/CodeReviewPage-jKlmLJwT.js +3 -0
  91. package/out/web/assets/CommandPalette-DnwPA5hy.js +1 -0
  92. package/out/web/assets/ImageStudioPanel-BR1XFwhR.js +1 -0
  93. package/out/web/assets/ImageStudioPanel-D0lnhQV5.css +1 -0
  94. package/out/web/assets/LinkedFoldersView-bi-CWQ-I.js +1 -0
  95. package/out/web/assets/McpRegistryPicker-D5tEFnyq.js +1 -0
  96. package/out/web/assets/McpServersView-BGG1dhFM.js +1 -0
  97. package/out/web/assets/MintDashboard-BIglDEWX.js +20 -0
  98. package/out/web/assets/MintDashboard-DSMywSgs.css +1 -0
  99. package/out/web/assets/ModelPanel-CbJ1PFoZ.js +1 -0
  100. package/out/web/assets/PicturesLibrary-CQP0fLP1.css +1 -0
  101. package/out/web/assets/PicturesLibrary-DoYN7T1p.js +1 -0
  102. package/out/web/assets/PluginsView-CU1HWXGk.js +1 -0
  103. package/out/web/assets/ScheduledTasksView-Bnl36y2N.js +1 -0
  104. package/out/web/assets/SettingsWindow-DhWLAn9T.css +1 -0
  105. package/out/web/assets/SettingsWindow-Dw8L01pw.js +2 -0
  106. package/out/web/assets/SkillsView-cnl8aY5p.js +2 -0
  107. package/out/web/assets/ToolSurfacePage-CgsQw6l5.css +1 -0
  108. package/out/web/assets/ToolSurfacePage-Cz2VJAPZ.js +37 -0
  109. package/out/web/assets/VeoStudioPanel-D4JnERWR.js +4 -0
  110. package/out/web/assets/VeoStudioPanel-DOj7_WR3.css +1 -0
  111. package/out/web/assets/WorkspacePanel-CbJ1PFoZ.js +1 -0
  112. package/out/web/assets/{abnfDiagram-VCTEODGH-DAOBA7ZG.js → abnfDiagram-VCTEODGH-ByMyfanq.js} +1 -1
  113. package/out/web/assets/{architecture-7GRP2DOG-CgSwQOPQ.js → architecture-7GRP2DOG-CLq2kEOB.js} +1 -1
  114. package/out/web/assets/{architectureDiagram-5GKGNRK7-CYp3yupP.js → architectureDiagram-5GKGNRK7-6PoVSmNG.js} +1 -1
  115. package/out/web/assets/{blockDiagram-NRAW4CY4-CyWybDWr.js → blockDiagram-NRAW4CY4-cKw9mAKi.js} +1 -1
  116. package/out/web/assets/{c4Diagram-UCG6FXSJ-BvY3U709.js → c4Diagram-UCG6FXSJ-Cqy8N5Ge.js} +1 -1
  117. package/out/web/assets/{chunk-75Z2AOVW-TLXCoZYv.js → chunk-75Z2AOVW-3qybijF5.js} +1 -1
  118. package/out/web/assets/chunk-CKBBP62Z-cIrXq6jN.js +2 -0
  119. package/out/web/assets/{chunk-CLGD4ZFX-xjsKtNS7.js → chunk-CLGD4ZFX-CndDQkww.js} +1 -1
  120. package/out/web/assets/chunk-DSNCTWBM-Cc4NxvgD.js +2 -0
  121. package/out/web/assets/chunk-DU6HZSFF-drzh6GsU.js +128 -0
  122. package/out/web/assets/{chunk-F27PBJKO-BFpgF55A.js → chunk-F27PBJKO-FiY35l4v.js} +1 -1
  123. package/out/web/assets/{chunk-G27WJ6UU-DfF7qW-C.js → chunk-G27WJ6UU-Bjt9Il4A.js} +1 -1
  124. package/out/web/assets/{chunk-GMAD6QVW-C-558kh_.js → chunk-GMAD6QVW-Bppi3Ctd.js} +1 -1
  125. package/out/web/assets/{chunk-JQ64N6SF-wJmeGKAw.js → chunk-JQ64N6SF-DGg5kcEV.js} +1 -1
  126. package/out/web/assets/{chunk-LCL6LL3I-C1wHD8Ie.js → chunk-LCL6LL3I-CCYcjd7p.js} +1 -1
  127. package/out/web/assets/{chunk-OBVCFTLP-BsRTPBqs.js → chunk-OBVCFTLP-DJeNil5M.js} +1 -1
  128. package/out/web/assets/{chunk-P2QGCYS3-HZIiM7Ni.js → chunk-P2QGCYS3-lRzkRjBB.js} +1 -1
  129. package/out/web/assets/{chunk-POPQ4Y6H-Dk9wvVb0.js → chunk-POPQ4Y6H-DouASMBW.js} +1 -1
  130. package/out/web/assets/{chunk-PWAF6VOD-BmjYLNbq.js → chunk-PWAF6VOD-DMmD1vVf.js} +1 -1
  131. package/out/web/assets/{chunk-R7TYR2AO-CaHIf-lK.js → chunk-R7TYR2AO-CYLA5UC5.js} +1 -1
  132. package/out/web/assets/{chunk-RHFEMEQ7-_T561hOU.js → chunk-RHFEMEQ7-BiXwEcUT.js} +1 -1
  133. package/out/web/assets/{chunk-SVP7TREG-CM-GGYoY.js → chunk-SVP7TREG-BV-zPslF.js} +1 -1
  134. package/out/web/assets/{chunk-TEH6E4GO-K2A8Vg2C.js → chunk-TEH6E4GO-B2RGEohZ.js} +1 -1
  135. package/out/web/assets/classDiagram-DTDB5LWJ-Dq0smRWk.js +1 -0
  136. package/out/web/assets/classDiagram-v2-JRS7N3AN-Dq0smRWk.js +1 -0
  137. package/out/web/assets/config-C-ZFtipU.css +1 -0
  138. package/out/web/assets/config-eEDngJXO.js +1 -0
  139. package/out/web/assets/core-hQ2VA9Ti.js +1 -0
  140. package/out/web/assets/{cynefin-OW5HDTMX-ClzaB6cc.js → cynefin-OW5HDTMX-Cas4csHM.js} +1 -1
  141. package/out/web/assets/{cynefinDiagram-5FMLGOSQ-D_JzZBzf.js → cynefinDiagram-5FMLGOSQ-Bbpj66Y3.js} +1 -1
  142. package/out/web/assets/{dagre-3AP2YEHR-BDuUeMHj.js → dagre-3AP2YEHR-B0-2RDEd.js} +1 -1
  143. package/out/web/assets/{diagram-S7CK7UJ4-DgtcfE8P.js → diagram-S7CK7UJ4-BdzEjUyH.js} +1 -1
  144. package/out/web/assets/{diagram-UQ7AKVKN-DTLoEYti.js → diagram-UQ7AKVKN-CJDwyPxF.js} +1 -1
  145. package/out/web/assets/{diagram-VSXAHHWV-ChGl9pXz.js → diagram-VSXAHHWV-BxaG1r_B.js} +1 -1
  146. package/out/web/assets/{diagram-VX7I27RA-Be8DWz0S.js → diagram-VX7I27RA-BMmUqm8f.js} +1 -1
  147. package/out/web/assets/{diagram-Z3DM3KII-BMXjRTOF.js → diagram-Z3DM3KII-CFtd03eE.js} +1 -1
  148. package/out/web/assets/dist-js-Bf2bhoku.js +1 -0
  149. package/out/web/assets/{ebnfDiagram-PWID7BFC-8Q9pl1CJ.js → ebnfDiagram-PWID7BFC-DwAen1hO.js} +1 -1
  150. package/out/web/assets/{erDiagram-SSCWMZ5O-DjH8Lnkn.js → erDiagram-SSCWMZ5O-BCZSHACj.js} +1 -1
  151. package/out/web/assets/event-ChBVpRy0.js +1 -0
  152. package/out/web/assets/{eventmodeling-NTZA5JFV-BKUH_DOp.js → eventmodeling-NTZA5JFV-XHStE4E_.js} +1 -1
  153. package/out/web/assets/fira-code-latin-600-normal-BmSSHshu.woff +0 -0
  154. package/out/web/assets/fira-code-latin-600-normal-CvYZpPHg.woff2 +0 -0
  155. package/out/web/assets/fira-code-symbols2-600-normal-Dvq6IQE5.woff +0 -0
  156. package/out/web/assets/flowDiagram-A5DVABFB-BLg7nEcd.js +1 -0
  157. package/out/web/assets/{ganttDiagram-EL5Y4UJY-D7fZKJNM.js → ganttDiagram-EL5Y4UJY-BAEu1Cql.js} +1 -1
  158. package/out/web/assets/{gitGraph-4MIJSDKK-BypWaaBU.js → gitGraph-4MIJSDKK-r32Vyper.js} +1 -1
  159. package/out/web/assets/{gitGraphDiagram-WWUBYQGX-DWiw_3bq.js → gitGraphDiagram-WWUBYQGX-z9o9qt6V.js} +1 -1
  160. package/out/web/assets/index-Btsf4L8M.css +1 -0
  161. package/out/web/assets/index-D8D9MeRA.js +11 -0
  162. package/out/web/assets/{info-A6RAGUB7-KkAUxT4j.js → info-A6RAGUB7-BvLaC2cw.js} +1 -1
  163. package/out/web/assets/{infoDiagram-RXCK75RN-BNb6E48y.js → infoDiagram-RXCK75RN-Be6WAXXw.js} +1 -1
  164. package/out/web/assets/{ishikawaDiagram-5VMMS53U-Caa-JoAK.js → ishikawaDiagram-5VMMS53U-DZjoHJHC.js} +1 -1
  165. package/out/web/assets/{journeyDiagram-EYS64GPL-DOrmFGz6.js → journeyDiagram-EYS64GPL-CtVkjkGm.js} +1 -1
  166. package/out/web/assets/{kanban-definition-3QL26DDD-BOWCYcK0.js → kanban-definition-3QL26DDD-CW5FMv1w.js} +1 -1
  167. package/out/web/assets/lucide-react-Ip3L6Jtn.js +1 -0
  168. package/out/web/assets/markdown-BSeeUbJH.js +38 -0
  169. package/out/web/assets/{mermaid-parser.core-BQqjCkS_.js → mermaid-parser.core-aUgCqsZ5.js} +3 -3
  170. package/out/web/assets/mermaid.core-D57sEiSL.js +11 -0
  171. package/out/web/assets/{mindmap-definition-FBJOCRG2-jTceWpr0.js → mindmap-definition-FBJOCRG2-DAmSpM3a.js} +1 -1
  172. package/out/web/assets/modelManager-tMVmJVAW.js +2 -0
  173. package/out/web/assets/oauthManager-9jxicDWJ.js +1 -0
  174. package/out/web/assets/{packet-AYTQ26CC-CJ5RXgol.js → packet-AYTQ26CC-DJ27iIoJ.js} +1 -1
  175. package/out/web/assets/{pegDiagram-XKGWAZYB-y8wMtd0y.js → pegDiagram-XKGWAZYB-Bj3ii-gS.js} +1 -1
  176. package/out/web/assets/{pie-WAS4IAKB-72fTBbl4.js → pie-WAS4IAKB-DG5mCUbc.js} +1 -1
  177. package/out/web/assets/{pieDiagram-E7YTZNPT-e0O02qWq.js → pieDiagram-E7YTZNPT-BrJLPnDA.js} +1 -1
  178. package/out/web/assets/prompt-latin-700-normal-BfbftDSq.woff +0 -0
  179. package/out/web/assets/prompt-latin-700-normal-I2gc831J.woff2 +0 -0
  180. package/out/web/assets/prompt-thai-700-normal-Cb5ilR-c.woff +0 -0
  181. package/out/web/assets/prompt-thai-700-normal-Cg4aQ0Nn.woff2 +0 -0
  182. package/out/web/assets/{quadrantDiagram-AXDQQJYC-C9phYLom.js → quadrantDiagram-AXDQQJYC-3x_TmF3j.js} +1 -1
  183. package/out/web/assets/{radar-RG4KPBEZ-BAU2hzUT.js → radar-RG4KPBEZ-CrXaTTuV.js} +1 -1
  184. package/out/web/assets/{railroad-74A4TZTK-B0bCAVHC.js → railroad-74A4TZTK-BpqMH9BE.js} +1 -1
  185. package/out/web/assets/{railroad-abnf-HS5TGJTU-WhxO7ndE.js → railroad-abnf-HS5TGJTU-DHI0uLlV.js} +1 -1
  186. package/out/web/assets/{railroad-ebnf-LZEXJU2U-BkA0m7-F.js → railroad-ebnf-LZEXJU2U-CQCb316y.js} +1 -1
  187. package/out/web/assets/{railroad-peg-WCYAUIDC-BsVpHIaj.js → railroad-peg-WCYAUIDC-Ph1giKkH.js} +1 -1
  188. package/out/web/assets/{railroadDiagram-O6MQD6OU-CyOLN65Q.js → railroadDiagram-O6MQD6OU-BRq6wIxx.js} +1 -1
  189. package/out/web/assets/{requirementDiagram-EFPCY7ZU-DQXJ7Dud.js → requirementDiagram-EFPCY7ZU-DIJj6G1g.js} +1 -1
  190. package/out/web/assets/{sankeyDiagram-P5KCCOFB-B2XLyIIc.js → sankeyDiagram-P5KCCOFB-DZajSryq.js} +1 -1
  191. package/out/web/assets/{sequenceDiagram-WJ2MYXX4-DGQY4wwm.js → sequenceDiagram-WJ2MYXX4-DxJCWyZS.js} +1 -1
  192. package/out/web/assets/{stateDiagram-HBIQ2CUA-DHZpDwNy.js → stateDiagram-HBIQ2CUA-k6voi8Vf.js} +1 -1
  193. package/out/web/assets/stateDiagram-v2-4QOOHH4V-DeEc0lGZ.js +1 -0
  194. package/out/web/assets/studio-B1mk40wD.js +1 -0
  195. package/out/web/assets/{swimlanes-XN3QIQJK-Bn_jcZz-.js → swimlanes-XN3QIQJK-igYhTjXB.js} +1 -1
  196. package/out/web/assets/swimlanesDiagram-VK2B7HYN-DpVik9v7.js +8 -0
  197. package/out/web/assets/{timeline-definition-24CTP7MA-CdTERuJZ.js → timeline-definition-24CTP7MA-Dke2p93p.js} +1 -1
  198. package/out/web/assets/{treeView-Q6P3EWNA-TKBKQG6R.js → treeView-Q6P3EWNA-hNELnXUs.js} +1 -1
  199. package/out/web/assets/{treemap-WGGIJYW6-BP7gQoiC.js → treemap-WGGIJYW6-D2B6wDsx.js} +1 -1
  200. package/out/web/assets/{vennDiagram-4TSXK5OY-BHUmxDuK.js → vennDiagram-4TSXK5OY-CDkW7rUP.js} +1 -1
  201. package/out/web/assets/{wardley-WFR3VGLG-9bct5AAN.js → wardley-WFR3VGLG-BECdXETi.js} +1 -1
  202. package/out/web/assets/{wardleyDiagram-VM6X3IG4-BfJwDDso.js → wardleyDiagram-VM6X3IG4-B5BycpQ4.js} +1 -1
  203. package/out/web/assets/window-5qYXKoW7.js +1 -0
  204. package/out/web/assets/{xychartDiagram-S5SC5T6Z-Dqd9Mvpo.js → xychartDiagram-S5SC5T6Z-CtJS3b--.js} +1 -1
  205. package/out/web/index-web.html +3 -4
  206. package/out/web/index.html +3 -4
  207. package/package.json +17 -6
  208. package/skills-lock.json +36 -0
  209. package/slash-commands.json +9 -0
  210. package/src/renderer/index-web.html +1 -0
  211. package/src/renderer/index.html +1 -0
  212. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  213. package/src/renderer/shared/agentProgress.ts +168 -3
  214. package/src/renderer/shared/components/AgentActivityDrawer.tsx +6 -6
  215. package/src/renderer/shared/components/AgentActivityTable.tsx +192 -132
  216. package/src/renderer/shared/components/ArtifactPreviewPanel.tsx +4 -4
  217. package/src/renderer/shared/components/AuthGate.tsx +4 -9
  218. package/src/renderer/shared/components/CalculationCard.tsx +41 -15
  219. package/src/renderer/shared/components/ChatMessageItem.tsx +20 -21
  220. package/src/renderer/shared/components/ChatPanel.tsx +432 -193
  221. package/src/renderer/shared/components/ChunkErrorBoundary.tsx +198 -49
  222. package/src/renderer/shared/components/CliSessionsView.tsx +495 -0
  223. package/src/renderer/shared/components/CodeReviewPage.tsx +565 -0
  224. package/src/renderer/shared/components/CommandPalette.tsx +8 -8
  225. package/src/renderer/shared/components/DashboardSidebar.tsx +862 -125
  226. package/src/renderer/shared/components/DesktopTitlebar.tsx +345 -0
  227. package/src/renderer/shared/components/DiffReviewModal.tsx +3 -3
  228. package/src/renderer/shared/components/GitBranchSelector.tsx +442 -0
  229. package/src/renderer/shared/components/ImageGenCard.tsx +31 -12
  230. package/src/renderer/shared/components/ImageSearchCard.tsx +59 -38
  231. package/src/renderer/shared/components/ImageStudioPanel.tsx +40 -41
  232. package/src/renderer/shared/components/InlineThoughtNote.tsx +27 -0
  233. package/src/renderer/shared/components/LinkedFoldersView.tsx +365 -46
  234. package/src/renderer/shared/components/McpRegistryPicker.tsx +1 -1
  235. package/src/renderer/shared/components/McpServersView.tsx +24 -22
  236. package/src/renderer/shared/components/MermaidCard.tsx +14 -8
  237. package/src/renderer/shared/components/MintDashboard.tsx +1141 -508
  238. package/src/renderer/shared/components/ModelSelectorPopover.tsx +535 -214
  239. package/src/renderer/shared/components/PlanChecklistWidget.tsx +1 -1
  240. package/src/renderer/shared/components/PluginsView.tsx +1 -1
  241. package/src/renderer/shared/components/RewindModal.tsx +6 -6
  242. package/src/renderer/shared/components/RunSummaryDashboard.tsx +3 -3
  243. package/src/renderer/shared/components/ScheduledTasksView.tsx +4 -4
  244. package/src/renderer/shared/components/ScreenCaptureDialog.css +221 -0
  245. package/src/renderer/shared/components/ScreenCaptureDialog.tsx +199 -0
  246. package/src/renderer/shared/components/Settings/AgentsTab.tsx +34 -39
  247. package/src/renderer/shared/components/Settings/AudioTab.tsx +8 -8
  248. package/src/renderer/shared/components/Settings/AutomationTab.tsx +8 -58
  249. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +368 -0
  250. package/src/renderer/shared/components/Settings/GeneralTab.tsx +335 -81
  251. package/src/renderer/shared/components/Settings/MemoryTab.tsx +3 -3
  252. package/src/renderer/shared/components/Settings/PluginsTab.tsx +20 -12
  253. package/src/renderer/shared/components/Settings/ProfileTab.tsx +29 -5
  254. package/src/renderer/shared/components/Settings/SearchableModelCombobox.tsx +411 -0
  255. package/src/renderer/shared/components/Settings/ThemeTab.tsx +401 -158
  256. package/src/renderer/shared/components/SkillsView.tsx +41 -16
  257. package/src/renderer/shared/components/SlashSuggestions.tsx +3 -2
  258. package/src/renderer/shared/components/SourcesBlock.tsx +44 -32
  259. package/src/renderer/shared/components/StockCard.tsx +58 -24
  260. package/src/renderer/shared/components/SubagentDagView.tsx +4 -4
  261. package/src/renderer/shared/components/TerminalDock.tsx +241 -0
  262. package/src/renderer/shared/components/ThinkingBlock.tsx +14 -8
  263. package/src/renderer/shared/components/ToolSurfacePage.tsx +520 -0
  264. package/src/renderer/shared/components/UiFeatureCard.tsx +267 -0
  265. package/src/renderer/shared/components/UiGridCard.tsx +188 -0
  266. package/src/renderer/shared/components/UiMockupWidget.tsx +149 -0
  267. package/src/renderer/shared/components/VeoStudioPanel.tsx +13 -29
  268. package/src/renderer/shared/components/WeatherCard.tsx +3 -3
  269. package/src/renderer/shared/components/WorkspaceContextPopover.tsx +133 -0
  270. package/src/renderer/shared/components/WorkspaceSelector.tsx +149 -0
  271. package/src/renderer/shared/constants/config.ts +28 -3
  272. package/src/renderer/shared/constants/models.ts +189 -2
  273. package/src/renderer/shared/conversation/coordinator.test.ts +27 -0
  274. package/src/renderer/shared/conversation/coordinator.ts +82 -0
  275. package/src/renderer/shared/conversation/syncView.test.ts +46 -0
  276. package/src/renderer/shared/conversation/syncView.ts +43 -0
  277. package/src/renderer/shared/conversation/useConversationCoordinator.ts +106 -0
  278. package/src/renderer/shared/css/agent-observability.css +35 -3
  279. package/src/renderer/shared/css/auth-gate.css +2 -2
  280. package/src/renderer/shared/css/chat-ui-cards.css +495 -0
  281. package/src/renderer/shared/css/code-review.css +823 -0
  282. package/src/renderer/shared/css/code-sessions.css +452 -0
  283. package/src/renderer/shared/css/file-changes.css +132 -0
  284. package/src/renderer/shared/css/image-studio.css +16 -16
  285. package/src/renderer/shared/css/management-views.css +277 -27
  286. package/src/renderer/shared/css/model-selector.css +442 -42
  287. package/src/renderer/shared/css/settings/agents.css +1 -1
  288. package/src/renderer/shared/css/settings/automation.css +13 -13
  289. package/src/renderer/shared/css/settings/general.css +526 -5
  290. package/src/renderer/shared/css/settings/plugins.css +5 -5
  291. package/src/renderer/shared/css/settings/profile.css +331 -1
  292. package/src/renderer/shared/css/settings/theme.css +651 -3
  293. package/src/renderer/shared/css/veo-studio.css +16 -15
  294. package/src/renderer/shared/fonts.ts +19 -37
  295. package/src/renderer/shared/platform.test.ts +34 -0
  296. package/src/renderer/shared/platform.ts +173 -6
  297. package/src/renderer/shared/theme/themeManager.ts +144 -0
  298. package/src/renderer/shared/theme/themes.ts +116 -0
  299. package/src/renderer/shared/theme/tokens.css +318 -0
  300. package/src/renderer/shared/types.ts +82 -1
  301. package/src/renderer/shared/utils/agentActivity.ts +126 -5
  302. package/src/renderer/shared/utils/cardJson.test.ts +35 -0
  303. package/src/renderer/shared/utils/cardJson.ts +30 -0
  304. package/src/renderer/shared/utils/fileIcons.ts +1 -1
  305. package/src/renderer/shared/utils/markdown.tsx +329 -80
  306. package/src/renderer/shared/utils/markdown.webImages.test.tsx +40 -0
  307. package/src/renderer/shared/utils/providers.ts +8 -3
  308. package/src/renderer/shared/utils/screenCapture.ts +43 -0
  309. package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
  310. package/src/renderer/shared/utils/ui.ts +4 -64
  311. package/src/renderer/src/App.tsx +7 -0
  312. package/src/renderer/src/components/BrowserShell.tsx +108 -0
  313. package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +91 -0
  314. package/src/renderer/src/components/Live2DStage.tsx +32 -9
  315. package/src/renderer/src/components/MintDashboard.tsx +4 -2
  316. package/src/renderer/src/components/ModelPanel.tsx +2 -1
  317. package/src/renderer/src/components/PicturesLibrary.tsx +3 -2
  318. package/src/renderer/src/components/ScreenPicker.css +190 -0
  319. package/src/renderer/src/components/ScreenPicker.tsx +185 -540
  320. package/src/renderer/src/components/SettingsWindow.tsx +40 -2
  321. package/src/renderer/src/components/SpotlightWindow.tsx +1 -0
  322. package/src/renderer/src/components/WidgetWindow.tsx +27 -3
  323. package/src/renderer/src/components/WorkspacePanel.tsx +61 -43
  324. package/src/renderer/src/components/browser-shell.css +185 -0
  325. package/src/renderer/src/components/browser-suggestions-popup.css +55 -0
  326. package/src/renderer/src/components/liveTranslateFrames.test.ts +23 -0
  327. package/src/renderer/src/components/liveTranslateFrames.ts +34 -0
  328. package/src/renderer/src/css/app-shell.css +394 -113
  329. package/src/renderer/src/css/chat.css +1926 -380
  330. package/src/renderer/src/css/dashboard.css +11 -0
  331. package/src/renderer/src/css/desktop-tools.css +730 -0
  332. package/src/renderer/src/css/misc.css +19 -22
  333. package/src/renderer/src/css/model.css +49 -48
  334. package/src/renderer/src/css/pictures.css +144 -136
  335. package/src/renderer/src/css/settings/base.css +357 -123
  336. package/src/renderer/src/css/sidebar-workspace.css +930 -291
  337. package/src/renderer/src/css/spotlight.css +15 -26
  338. package/src/renderer/src/css/tool-surfaces.css +728 -0
  339. package/src/renderer/src/css/widget.css +35 -7
  340. package/src/renderer/src/env.d.ts +3 -0
  341. package/src/renderer/src/hooks/useProviderModels.ts +2 -9
  342. package/src/renderer/src/index.css +53 -46
  343. package/src/renderer/src/main.tsx +11 -0
  344. package/src/renderer/src/tauri.ts +201 -30
  345. package/src/renderer/src-web/App.tsx +40 -0
  346. package/src/renderer/src-web/components/MintDashboard.tsx +4 -2
  347. package/src/renderer/src-web/components/PicturesLibrary.tsx +3 -2
  348. package/src/renderer/src-web/components/PullToRefresh.tsx +107 -0
  349. package/src/renderer/src-web/components/SettingsWindow.tsx +28 -2
  350. package/src/renderer/src-web/css/app-shell.css +174 -96
  351. package/src/renderer/src-web/css/chat.css +1519 -316
  352. package/src/renderer/src-web/css/dashboard.css +11 -0
  353. package/src/renderer/src-web/css/misc.css +17 -17
  354. package/src/renderer/src-web/css/mobile.css +678 -0
  355. package/src/renderer/src-web/css/model.css +46 -46
  356. package/src/renderer/src-web/css/pictures.css +81 -34
  357. package/src/renderer/src-web/css/pull-to-refresh.css +54 -0
  358. package/src/renderer/src-web/css/settings/base.css +316 -100
  359. package/src/renderer/src-web/css/settings/mobile.css +320 -0
  360. package/src/renderer/src-web/css/sidebar-workspace.css +545 -100
  361. package/src/renderer/src-web/css/spotlight.css +9 -26
  362. package/src/renderer/src-web/css/widget.css +0 -5
  363. package/src/renderer/src-web/env.d.ts +3 -0
  364. package/src/renderer/src-web/hooks/useProviderModels.ts +2 -9
  365. package/src/renderer/src-web/index.css +53 -96
  366. package/src/renderer/src-web/main.tsx +11 -0
  367. package/src/renderer/src-web/tauri.ts +200 -30
  368. package/vite.config.web.ts +1 -0
  369. package/out/web/assets/MintDashboard-DWSqucgb.js +0 -57
  370. package/out/web/assets/SettingsWindow-DFM2aj5B.js +0 -2
  371. package/out/web/assets/chunk-CKBBP62Z-nr1uKPj9.js +0 -2
  372. package/out/web/assets/chunk-DSNCTWBM-Dtj8Rqsd.js +0 -2
  373. package/out/web/assets/chunk-DU6HZSFF-2tv5K1wT.js +0 -128
  374. package/out/web/assets/classDiagram-DTDB5LWJ-BdSFArgw.js +0 -1
  375. package/out/web/assets/classDiagram-v2-JRS7N3AN-BdSFArgw.js +0 -1
  376. package/out/web/assets/core-DImejIUG.js +0 -1
  377. package/out/web/assets/dist-js-BxuKEVWM.js +0 -1
  378. package/out/web/assets/event-BNl8Zahw.js +0 -1
  379. package/out/web/assets/fira-code-cyrillic-400-normal-UC0NFL4U.woff2 +0 -0
  380. package/out/web/assets/fira-code-cyrillic-400-normal-dc1q3VD7.woff +0 -0
  381. package/out/web/assets/fira-code-cyrillic-500-normal-Bu7VCL72.woff +0 -0
  382. package/out/web/assets/fira-code-cyrillic-500-normal-CX35h3Mg.woff2 +0 -0
  383. package/out/web/assets/fira-code-cyrillic-ext-400-normal-Buh61xzB.woff +0 -0
  384. package/out/web/assets/fira-code-cyrillic-ext-400-normal-txZ9Fk_1.woff2 +0 -0
  385. package/out/web/assets/fira-code-cyrillic-ext-500-normal-CYRspHgm.woff2 +0 -0
  386. package/out/web/assets/fira-code-cyrillic-ext-500-normal-CwtLmSFD.woff +0 -0
  387. package/out/web/assets/fira-code-greek-400-normal-B2Gh_Y8s.woff2 +0 -0
  388. package/out/web/assets/fira-code-greek-400-normal-DobhaxvF.woff +0 -0
  389. package/out/web/assets/fira-code-greek-500-normal-CVO0IRNt.woff2 +0 -0
  390. package/out/web/assets/fira-code-greek-500-normal-DEXIWiey.woff +0 -0
  391. package/out/web/assets/fira-code-greek-ext-400-normal-DR7mBgIM.woff2 +0 -0
  392. package/out/web/assets/fira-code-greek-ext-400-normal-DSPUheWg.woff +0 -0
  393. package/out/web/assets/fira-code-greek-ext-500-normal-BO3iq16n.woff2 +0 -0
  394. package/out/web/assets/fira-code-greek-ext-500-normal-CMiNLXER.woff +0 -0
  395. package/out/web/assets/fira-code-latin-ext-400-normal-DG73JjOf.woff +0 -0
  396. package/out/web/assets/fira-code-latin-ext-400-normal-KSMg0QLl.woff2 +0 -0
  397. package/out/web/assets/fira-code-latin-ext-500-normal-CaANqXjI.woff +0 -0
  398. package/out/web/assets/fira-code-latin-ext-500-normal-Crd9e_oI.woff2 +0 -0
  399. package/out/web/assets/flowDiagram-A5DVABFB-BpnHUSU7.js +0 -1
  400. package/out/web/assets/index-CVRg270z.css +0 -1
  401. package/out/web/assets/index-DxZQr9yD.js +0 -10
  402. package/out/web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  403. package/out/web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  404. package/out/web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  405. package/out/web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  406. package/out/web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  407. package/out/web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  408. package/out/web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  409. package/out/web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  410. package/out/web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  411. package/out/web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  412. package/out/web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  413. package/out/web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  414. package/out/web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  415. package/out/web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  416. package/out/web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  417. package/out/web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  418. package/out/web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  419. package/out/web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  420. package/out/web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  421. package/out/web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  422. package/out/web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  423. package/out/web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  424. package/out/web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  425. package/out/web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  426. package/out/web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  427. package/out/web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  428. package/out/web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  429. package/out/web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  430. package/out/web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  431. package/out/web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  432. package/out/web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  433. package/out/web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  434. package/out/web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  435. package/out/web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  436. package/out/web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  437. package/out/web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  438. package/out/web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  439. package/out/web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  440. package/out/web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  441. package/out/web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  442. package/out/web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  443. package/out/web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  444. package/out/web/assets/kanit-latin-400-normal-BUlsQXH3.woff2 +0 -0
  445. package/out/web/assets/kanit-latin-400-normal-HVZWrfev.woff +0 -0
  446. package/out/web/assets/kanit-latin-500-normal-CX_4XcKi.woff2 +0 -0
  447. package/out/web/assets/kanit-latin-500-normal-qBha86O4.woff +0 -0
  448. package/out/web/assets/kanit-latin-600-normal-0lS78yAd.woff2 +0 -0
  449. package/out/web/assets/kanit-latin-600-normal-DlacpeTg.woff +0 -0
  450. package/out/web/assets/kanit-latin-ext-400-normal-B2xPf58u.woff2 +0 -0
  451. package/out/web/assets/kanit-latin-ext-400-normal-CHvNKHHD.woff +0 -0
  452. package/out/web/assets/kanit-latin-ext-500-normal-BFdWrG22.woff +0 -0
  453. package/out/web/assets/kanit-latin-ext-500-normal-EHs3suD6.woff2 +0 -0
  454. package/out/web/assets/kanit-latin-ext-600-normal-BBDzoLaj.woff +0 -0
  455. package/out/web/assets/kanit-latin-ext-600-normal-DAX3wtuq.woff2 +0 -0
  456. package/out/web/assets/kanit-thai-400-normal-D2nliXKa.woff +0 -0
  457. package/out/web/assets/kanit-thai-400-normal-D80Pglro.woff2 +0 -0
  458. package/out/web/assets/kanit-thai-500-normal-BB8gri04.woff2 +0 -0
  459. package/out/web/assets/kanit-thai-500-normal-Iy_w0a80.woff +0 -0
  460. package/out/web/assets/kanit-thai-600-normal-Bj6oiaDc.woff2 +0 -0
  461. package/out/web/assets/kanit-thai-600-normal-wbTlPO7I.woff +0 -0
  462. package/out/web/assets/kanit-vietnamese-400-normal-DRI8YWyb.woff2 +0 -0
  463. package/out/web/assets/kanit-vietnamese-400-normal-dow6Bnto.woff +0 -0
  464. package/out/web/assets/kanit-vietnamese-500-normal-BzTrpCM3.woff2 +0 -0
  465. package/out/web/assets/kanit-vietnamese-500-normal-T1ndT7MF.woff +0 -0
  466. package/out/web/assets/kanit-vietnamese-600-normal-Bi7fw3I4.woff2 +0 -0
  467. package/out/web/assets/kanit-vietnamese-600-normal-DXCRAa_8.woff +0 -0
  468. package/out/web/assets/mali-latin-400-normal-ByPcZ8Gx.woff +0 -0
  469. package/out/web/assets/mali-latin-400-normal-Cuc6Ykl8.woff2 +0 -0
  470. package/out/web/assets/mali-latin-500-normal-B01M-LP6.woff2 +0 -0
  471. package/out/web/assets/mali-latin-500-normal-Thgqw_9f.woff +0 -0
  472. package/out/web/assets/mali-latin-600-normal-B9TiUNpT.woff2 +0 -0
  473. package/out/web/assets/mali-latin-600-normal-CycpQCwU.woff +0 -0
  474. package/out/web/assets/mali-latin-ext-400-normal-DUVdJQO4.woff +0 -0
  475. package/out/web/assets/mali-latin-ext-400-normal-iCG1ALuJ.woff2 +0 -0
  476. package/out/web/assets/mali-latin-ext-500-normal-BfJE4UMZ.woff2 +0 -0
  477. package/out/web/assets/mali-latin-ext-500-normal-FoLEyOKS.woff +0 -0
  478. package/out/web/assets/mali-latin-ext-600-normal-BTvIfiCp.woff2 +0 -0
  479. package/out/web/assets/mali-latin-ext-600-normal-DAQcLVmD.woff +0 -0
  480. package/out/web/assets/mali-thai-400-normal-BO7A3bLZ.woff2 +0 -0
  481. package/out/web/assets/mali-thai-400-normal-CX9VrjTn.woff +0 -0
  482. package/out/web/assets/mali-thai-500-normal-Cph2qha9.woff +0 -0
  483. package/out/web/assets/mali-thai-500-normal-D89rMT77.woff2 +0 -0
  484. package/out/web/assets/mali-thai-600-normal-DPgXT6rZ.woff +0 -0
  485. package/out/web/assets/mali-thai-600-normal-DlTx3OsB.woff2 +0 -0
  486. package/out/web/assets/mali-vietnamese-400-normal-DUKxzXaz.woff2 +0 -0
  487. package/out/web/assets/mali-vietnamese-400-normal-gFDLvsOK.woff +0 -0
  488. package/out/web/assets/mali-vietnamese-500-normal--OY07q2c.woff +0 -0
  489. package/out/web/assets/mali-vietnamese-500-normal-KDrNRwfW.woff2 +0 -0
  490. package/out/web/assets/mali-vietnamese-600-normal-CtTMUU7T.woff +0 -0
  491. package/out/web/assets/mali-vietnamese-600-normal-DFlJr66u.woff2 +0 -0
  492. package/out/web/assets/mermaid.core-DYGGlqOf.js +0 -11
  493. package/out/web/assets/mitr-latin-400-normal-Cb18vPmZ.woff +0 -0
  494. package/out/web/assets/mitr-latin-400-normal-CwbkLQ4a.woff2 +0 -0
  495. package/out/web/assets/mitr-latin-500-normal-B4JOhzAi.woff +0 -0
  496. package/out/web/assets/mitr-latin-500-normal-BmClOow2.woff2 +0 -0
  497. package/out/web/assets/mitr-latin-ext-400-normal-B3CPG7Yo.woff2 +0 -0
  498. package/out/web/assets/mitr-latin-ext-400-normal-DTx65hiC.woff +0 -0
  499. package/out/web/assets/mitr-latin-ext-500-normal-B_Diajz1.woff2 +0 -0
  500. package/out/web/assets/mitr-latin-ext-500-normal-DYOsVaVT.woff +0 -0
  501. package/out/web/assets/mitr-thai-400-normal-CS4Do6aM.woff +0 -0
  502. package/out/web/assets/mitr-thai-400-normal-CXWaAdVj.woff2 +0 -0
  503. package/out/web/assets/mitr-thai-500-normal-CVdBrEVH.woff2 +0 -0
  504. package/out/web/assets/mitr-thai-500-normal-CW_B5D-i.woff +0 -0
  505. package/out/web/assets/mitr-vietnamese-400-normal-CPMGWyoC.woff2 +0 -0
  506. package/out/web/assets/mitr-vietnamese-400-normal-Z6XhIpdY.woff +0 -0
  507. package/out/web/assets/mitr-vietnamese-500-normal-CDrv0585.woff2 +0 -0
  508. package/out/web/assets/mitr-vietnamese-500-normal-CMQWyq-Z.woff +0 -0
  509. package/out/web/assets/noto-sans-thai-latin-400-normal-BaWBrqFP.woff2 +0 -0
  510. package/out/web/assets/noto-sans-thai-latin-400-normal-Dh_FJjOB.woff +0 -0
  511. package/out/web/assets/noto-sans-thai-latin-500-normal-BC04jsmm.woff +0 -0
  512. package/out/web/assets/noto-sans-thai-latin-500-normal-fyBKClF1.woff2 +0 -0
  513. package/out/web/assets/noto-sans-thai-latin-600-normal-CemcjnVR.woff2 +0 -0
  514. package/out/web/assets/noto-sans-thai-latin-600-normal-Dbc-dNO6.woff +0 -0
  515. package/out/web/assets/noto-sans-thai-latin-ext-400-normal-B-1H0YkH.woff +0 -0
  516. package/out/web/assets/noto-sans-thai-latin-ext-400-normal-nBP59pVw.woff2 +0 -0
  517. package/out/web/assets/noto-sans-thai-latin-ext-500-normal-B-ZWb8SZ.woff2 +0 -0
  518. package/out/web/assets/noto-sans-thai-latin-ext-500-normal-CrEHO7C8.woff +0 -0
  519. package/out/web/assets/noto-sans-thai-latin-ext-600-normal-PkMZo0xa.woff +0 -0
  520. package/out/web/assets/noto-sans-thai-latin-ext-600-normal-maHHBsez.woff2 +0 -0
  521. package/out/web/assets/noto-sans-thai-thai-400-normal-BEOmKAI1.woff2 +0 -0
  522. package/out/web/assets/noto-sans-thai-thai-400-normal-Dxb_unuU.woff +0 -0
  523. package/out/web/assets/noto-sans-thai-thai-500-normal-BOGXO2tw.woff +0 -0
  524. package/out/web/assets/noto-sans-thai-thai-500-normal-CU_X86GQ.woff2 +0 -0
  525. package/out/web/assets/noto-sans-thai-thai-600-normal-CmgB8_JK.woff +0 -0
  526. package/out/web/assets/noto-sans-thai-thai-600-normal-Cs73sVjl.woff2 +0 -0
  527. package/out/web/assets/oauthManager-DNiwM8Rn.js +0 -2
  528. package/out/web/assets/oauthManager-DuamS1Av.css +0 -1
  529. package/out/web/assets/outfit-latin-300-normal-AWLfuCom.woff +0 -0
  530. package/out/web/assets/outfit-latin-300-normal-CRYTwY6m.woff2 +0 -0
  531. package/out/web/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
  532. package/out/web/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
  533. package/out/web/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
  534. package/out/web/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
  535. package/out/web/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
  536. package/out/web/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
  537. package/out/web/assets/outfit-latin-ext-300-normal-BYS6Dpvq.woff2 +0 -0
  538. package/out/web/assets/outfit-latin-ext-300-normal-CMIMshEd.woff +0 -0
  539. package/out/web/assets/outfit-latin-ext-400-normal-5tcqmc2S.woff2 +0 -0
  540. package/out/web/assets/outfit-latin-ext-400-normal-DHm7mdGe.woff +0 -0
  541. package/out/web/assets/outfit-latin-ext-500-normal-DrCvqoFD.woff +0 -0
  542. package/out/web/assets/outfit-latin-ext-500-normal-zeox_O30.woff2 +0 -0
  543. package/out/web/assets/outfit-latin-ext-600-normal-B85nYjL1.woff2 +0 -0
  544. package/out/web/assets/outfit-latin-ext-600-normal-CWJcPgd7.woff +0 -0
  545. package/out/web/assets/preload-helper-D0uHx7FM.js +0 -1
  546. package/out/web/assets/prompt-latin-ext-400-normal-1pHbN9uy.woff +0 -0
  547. package/out/web/assets/prompt-latin-ext-400-normal-DdSafGZ9.woff2 +0 -0
  548. package/out/web/assets/prompt-latin-ext-500-normal--EZ1um7s.woff2 +0 -0
  549. package/out/web/assets/prompt-latin-ext-500-normal-D-lI1EVT.woff +0 -0
  550. package/out/web/assets/prompt-latin-ext-600-normal-C8b50x67.woff +0 -0
  551. package/out/web/assets/prompt-latin-ext-600-normal-Cg9L7iJU.woff2 +0 -0
  552. package/out/web/assets/prompt-vietnamese-400-normal-BCPzsgPT.woff2 +0 -0
  553. package/out/web/assets/prompt-vietnamese-400-normal-D4pLyeNK.woff +0 -0
  554. package/out/web/assets/prompt-vietnamese-500-normal-DmzxmPwa.woff2 +0 -0
  555. package/out/web/assets/prompt-vietnamese-500-normal-w_sS_j2F.woff +0 -0
  556. package/out/web/assets/prompt-vietnamese-600-normal-1dyohtNh.woff +0 -0
  557. package/out/web/assets/prompt-vietnamese-600-normal-7QWjJBsF.woff2 +0 -0
  558. package/out/web/assets/sarabun-latin-400-normal-DmguIS-7.woff +0 -0
  559. package/out/web/assets/sarabun-latin-400-normal-URPBxl-K.woff2 +0 -0
  560. package/out/web/assets/sarabun-latin-500-normal-BjUTcdxu.woff2 +0 -0
  561. package/out/web/assets/sarabun-latin-500-normal-CJexTAnl.woff +0 -0
  562. package/out/web/assets/sarabun-latin-600-normal-9FonlkKx.woff +0 -0
  563. package/out/web/assets/sarabun-latin-600-normal-DMD3TROr.woff2 +0 -0
  564. package/out/web/assets/sarabun-latin-ext-400-normal-BbP7GpLL.woff2 +0 -0
  565. package/out/web/assets/sarabun-latin-ext-400-normal-BsmTn0SS.woff +0 -0
  566. package/out/web/assets/sarabun-latin-ext-500-normal-Cot16Bd2.woff +0 -0
  567. package/out/web/assets/sarabun-latin-ext-500-normal-DE1LysqI.woff2 +0 -0
  568. package/out/web/assets/sarabun-latin-ext-600-normal-0tXu9x_i.woff +0 -0
  569. package/out/web/assets/sarabun-latin-ext-600-normal-BRxjKUQ6.woff2 +0 -0
  570. package/out/web/assets/sarabun-thai-400-normal-C2DaJlKK.woff2 +0 -0
  571. package/out/web/assets/sarabun-thai-400-normal-CH-UbWgg.woff +0 -0
  572. package/out/web/assets/sarabun-thai-500-normal-BVssjame.woff2 +0 -0
  573. package/out/web/assets/sarabun-thai-500-normal-C3iDLEzi.woff +0 -0
  574. package/out/web/assets/sarabun-thai-600-normal-3dIAQ-_s.woff2 +0 -0
  575. package/out/web/assets/sarabun-thai-600-normal-DWKeTe6a.woff +0 -0
  576. package/out/web/assets/sarabun-vietnamese-400-normal-DPfkSCMy.woff +0 -0
  577. package/out/web/assets/sarabun-vietnamese-400-normal-igdrzAGY.woff2 +0 -0
  578. package/out/web/assets/sarabun-vietnamese-500-normal-CpRVvGRU.woff +0 -0
  579. package/out/web/assets/sarabun-vietnamese-500-normal-Dpe7Br9m.woff2 +0 -0
  580. package/out/web/assets/sarabun-vietnamese-600-normal-BMbOgAW-.woff +0 -0
  581. package/out/web/assets/sarabun-vietnamese-600-normal-DkxrOlLB.woff2 +0 -0
  582. package/out/web/assets/stateDiagram-v2-4QOOHH4V-s6P9g2_b.js +0 -1
  583. package/out/web/assets/swimlanesDiagram-VK2B7HYN-CV9fOLrV.js +0 -8
  584. package/out/web/assets/tauri-CNrHQ2Gb.js +0 -3
  585. package/out/web/assets/window-DkIhgI7B.js +0 -1
@@ -1,3 +1,5 @@
1
+ const ANSI_BLUE: &str = crate::terminal_theme::ANSI_BLUE;
2
+
1
3
  use super::*;
2
4
 
3
5
  fn is_reachable(host: &str, port: u16) -> bool {
@@ -165,31 +167,11 @@ fn first_line_preview(path: &Path) -> String {
165
167
  }
166
168
  }
167
169
 
168
- fn print_rendered_markdown(text: &str) {
169
- let mut table_buffer: Vec<String> = Vec::new();
170
- for line in text.lines() {
171
- if crate::markdown::is_table_line(line) {
172
- table_buffer.push(line.to_string());
173
- } else {
174
- if !table_buffer.is_empty() {
175
- print!("{}", crate::markdown::render_markdown_table(&table_buffer));
176
- table_buffer.clear();
177
- }
178
- if let Some(h) = line.strip_prefix("## ") {
179
- println!("\n{BLUE}{h}{RESET}");
180
- } else if let Some(h) = line.strip_prefix("# ") {
181
- println!("\n{BLUE}{h}{RESET}");
182
- } else {
183
- println!("{line}");
184
- }
185
- }
186
- }
187
- if !table_buffer.is_empty() {
188
- print!("{}", crate::markdown::render_markdown_table(&table_buffer));
189
- }
190
- }
191
-
192
- async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Option<SlashResult> {
170
+ async fn execute_core_slash(
171
+ session: &mut InteractiveSession,
172
+ ui: &mut dyn CommandUi,
173
+ query: &str,
174
+ ) -> Option<SlashResult> {
193
175
  use mint_core::slash::{SlashEffect, SlashRequest, SlashResponse};
194
176
 
195
177
  let trimmed = query.trim();
@@ -213,28 +195,17 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
213
195
  | "/videomodels"
214
196
  | "/video-provider"
215
197
  ) {
216
- print!("{DIM}Fetching available models...{RESET}\r");
217
- let _ = std::io::Write::flush(&mut std::io::stdout());
198
+ ui.set_status(vec!["Fetching available models…".into()]);
218
199
  }
219
200
 
220
201
  let response = mint_core::slash::execute_async(&req, &mut session.config).await;
221
-
222
- if matches!(
223
- cmd,
224
- "/models"
225
- | "/image-models"
226
- | "/image-provider"
227
- | "/video-models"
228
- | "/videomodels"
229
- | "/video-provider"
230
- ) {
231
- print!("\r\x1b[2K");
232
- let _ = std::io::Write::flush(&mut std::io::stdout());
233
- }
202
+ ui.clear_status();
234
203
 
235
204
  match response {
236
205
  SlashResponse::Message { markdown } => {
237
- print_rendered_markdown(&markdown);
206
+ if !markdown.is_empty() {
207
+ ui.push_command_output(markdown);
208
+ }
238
209
  Some(SlashResult::Handled)
239
210
  }
240
211
  SlashResponse::Applied { markdown, effects } => {
@@ -246,7 +217,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
246
217
  }
247
218
  SlashEffect::ProviderChanged { display } => {
248
219
  save_needed = true;
249
- println!("Switched to: {MINT}{display}{RESET}\n");
220
+ ui.push_notice(format!("Switched to: {display}"));
250
221
  }
251
222
  SlashEffect::FastModeChanged { enabled } => {
252
223
  session.fast_mode = *enabled;
@@ -264,6 +235,10 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
264
235
  }
265
236
  SlashEffect::HistoryCleared => {
266
237
  session.history.clear();
238
+ ui.clear_transcript();
239
+ }
240
+ SlashEffect::ThinkingChanged { .. } => {
241
+ save_needed = true;
267
242
  }
268
243
  }
269
244
  }
@@ -271,7 +246,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
271
246
  let _ = mint_core::save_config(&session.config);
272
247
  }
273
248
  if !markdown.is_empty() {
274
- print_rendered_markdown(&markdown);
249
+ ui.push_command_output(markdown);
275
250
  }
276
251
  Some(SlashResult::Handled)
277
252
  }
@@ -280,20 +255,49 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
280
255
  title,
281
256
  options,
282
257
  } => {
283
- let labels: Vec<String> = options.iter().map(|o| o.label.clone()).collect();
284
- let default_choice = labels.first().cloned().unwrap_or_default();
285
- match prompt_interactive_select(&title, &labels, &default_choice) {
286
- Ok(Some(selected_label)) => {
287
- if let Some(opt) = options.iter().find(|o| o.label == selected_label) {
288
- let next_input = format!("{command} {}", opt.value);
289
- return Box::pin(handle_slash_command(session, &next_input)).await;
258
+ let choice_items: Vec<ChoiceItem> = options
259
+ .iter()
260
+ .map(|opt| ChoiceItem::new(opt.label.clone(), opt.value.clone()))
261
+ .collect();
262
+ match ui.prompt_choice(&title, "", &choice_items) {
263
+ Ok(Some(idx)) => {
264
+ let opt = &options[idx];
265
+ if opt.value == "__action:search" {
266
+ let text = ui
267
+ .prompt_text("Search models", "Search query", None)
268
+ .ok()
269
+ .flatten()
270
+ .unwrap_or_default();
271
+ let trimmed = text.trim();
272
+ if !trimmed.is_empty() {
273
+ let next_input = format!("{command} __action:search:{trimmed}");
274
+ return Box::pin(handle_slash_command(session, ui, &next_input)).await;
275
+ }
276
+ ui.push_notice("Cancelled.");
277
+ return Some(SlashResult::Handled);
290
278
  }
279
+ if opt.value == "__action:custom" {
280
+ let custom = ui
281
+ .prompt_text("Custom model", "Enter custom model ID", None)
282
+ .ok()
283
+ .flatten()
284
+ .unwrap_or_default();
285
+ let trimmed = custom.trim();
286
+ if !trimmed.is_empty() {
287
+ let next_input = format!("{command} {trimmed}");
288
+ return Box::pin(handle_slash_command(session, ui, &next_input)).await;
289
+ }
290
+ ui.push_notice("Cancelled.");
291
+ return Some(SlashResult::Handled);
292
+ }
293
+ let next_input = format!("{command} {}", opt.value);
294
+ return Box::pin(handle_slash_command(session, ui, &next_input)).await;
291
295
  }
292
296
  Ok(None) => {
293
- println!("Cancelled.\n");
297
+ ui.push_notice("Command cancelled");
294
298
  }
295
299
  Err(e) => {
296
- println!("{ERROR}Selection error:{RESET} {e}\n");
300
+ ui.report_error(&e);
297
301
  }
298
302
  }
299
303
  Some(SlashResult::Handled)
@@ -308,7 +312,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
308
312
  }
309
313
  SlashResponse::Navigate { markdown, .. } => {
310
314
  if !markdown.is_empty() {
311
- print_rendered_markdown(&markdown);
315
+ ui.push_command_output(markdown);
312
316
  }
313
317
  Some(SlashResult::Handled)
314
318
  }
@@ -320,6 +324,7 @@ async fn execute_core_slash(session: &mut InteractiveSession, query: &str) -> Op
320
324
  /// Route `/…` commands. Returns `None` if the input is not a slash command.
321
325
  pub async fn handle_slash_command(
322
326
  session: &mut InteractiveSession,
327
+ ui: &mut dyn CommandUi,
323
328
  query: &str,
324
329
  ) -> Option<SlashResult> {
325
330
  let trimmed = query.trim();
@@ -337,10 +342,15 @@ pub async fn handle_slash_command(
337
342
  let is_cli_only_or_wizard = matches!(
338
343
  cmd,
339
344
  "/plan"
345
+ | "/thought"
346
+ | "/think"
347
+ | "/resume"
348
+ | "/branch"
340
349
  | "/bg"
341
350
  | "/jobs"
342
351
  | "/shells"
343
352
  | "/palette"
353
+ | "/theme"
344
354
  | "/image"
345
355
  | "/paste"
346
356
  | "/avatar"
@@ -352,28 +362,33 @@ pub async fn handle_slash_command(
352
362
  && rest.is_empty());
353
363
 
354
364
  if !is_cli_only_or_wizard {
355
- if let Some(res) = execute_core_slash(session, trimmed).await {
365
+ if let Some(res) = execute_core_slash(session, ui, trimmed).await {
356
366
  return Some(res);
357
367
  }
358
368
  }
359
369
 
360
370
  match cmd {
361
371
  "/help" => {
362
- println!("\n{BLUE}────────────────────────────────────────────{RESET}");
363
- println!("{MINT} Mint Interactive Commands{RESET}");
364
- println!("{BLUE}────────────────────────────────────────────{RESET}");
372
+ let mut lines = Vec::new();
373
+ lines.push(format!(
374
+ "{BLUE}────────────────────────────────────────────{RESET}"
375
+ ));
376
+ lines.push(format!("{MINT} Mint Interactive Commands{RESET}"));
377
+ lines.push(format!(
378
+ "{BLUE}────────────────────────────────────────────{RESET}"
379
+ ));
365
380
  for spec in SLASH_COMMANDS.iter() {
366
381
  let label = if spec.usage.is_empty() {
367
382
  spec.token.to_string()
368
383
  } else {
369
384
  format!("{} {}", spec.token, spec.usage)
370
385
  };
371
- println!(
386
+ lines.push(format!(
372
387
  " {MINT}{:<30}{RESET} {DIM}{}{RESET}",
373
388
  label, spec.description
374
- );
389
+ ));
375
390
  }
376
- println!();
391
+ ui.push_command_output(lines.join("\n"));
377
392
  Some(SlashResult::Handled)
378
393
  }
379
394
 
@@ -384,34 +399,20 @@ pub async fn handle_slash_command(
384
399
  "/fast" => {
385
400
  let choice = if rest.is_empty() {
386
401
  let options = vec![
387
- "on (hide thinking traces)".to_string(),
388
- "off (show thinking traces)".to_string(),
402
+ ChoiceItem::new("on (hide thinking traces)", "on"),
403
+ ChoiceItem::new("off (show thinking traces)", "off"),
389
404
  ];
390
- let current = if session.fast_mode {
391
- &options[0]
392
- } else {
393
- &options[1]
394
- };
395
- match prompt_interactive_select("Select Fast Mode", &options, current) {
396
- Ok(Some(sel)) => {
397
- if sel.starts_with("on") {
398
- Some(true)
399
- } else {
400
- Some(false)
401
- }
402
- }
403
- Ok(None) => None,
404
- Err(e) => {
405
- println!("{ERROR}Error selecting fast mode:{RESET} {e}\n");
406
- None
407
- }
405
+ match ui.prompt_choice("Select Fast Mode", "", &options) {
406
+ Ok(Some(0)) => Some(true),
407
+ Ok(Some(1)) => Some(false),
408
+ _ => None,
408
409
  }
409
410
  } else {
410
411
  match rest {
411
412
  "on" => Some(true),
412
413
  "off" => Some(false),
413
414
  _ => {
414
- println!("{WARN}/fast usage: /fast [on|off]{RESET}\n");
415
+ ui.push_notice("/fast usage: /fast [on|off]");
415
416
  None
416
417
  }
417
418
  }
@@ -420,16 +421,82 @@ pub async fn handle_slash_command(
420
421
  if let Some(mode) = choice {
421
422
  session.fast_mode = mode;
422
423
  if session.fast_mode {
423
- println!("{DIM}[Fast] mode ON — thinking traces hidden{RESET}\n");
424
+ ui.push_notice("[Fast] mode ON — thinking traces hidden");
425
+ } else {
426
+ ui.push_notice("[Fast] mode OFF");
427
+ }
428
+ }
429
+ Some(SlashResult::Handled)
430
+ }
431
+
432
+ "/resume" => {
433
+ let target_session_id = if rest.is_empty() {
434
+ prompt_resume_session_picker_with_ui(ui, &session.current_dir, &session.chat_id)
435
+ .ok()
436
+ .flatten()
437
+ } else {
438
+ let trimmed = rest.trim();
439
+ let candidate =
440
+ if !trimmed.starts_with("cli::") && trimmed != mint_core::CHAT_CLI_ID {
441
+ format!("cli::{trimmed}")
442
+ } else {
443
+ trimmed.to_string()
444
+ };
445
+ if let Ok(memory) = MemoryStore::open_default()
446
+ && let Ok(sessions) = memory.list_chat_sessions()
447
+ && sessions.iter().any(|s| s.id == candidate)
448
+ {
449
+ Some(candidate)
424
450
  } else {
425
- println!("{DIM}[Fast] mode OFF{RESET}\n");
451
+ Some(trimmed.to_string())
452
+ }
453
+ };
454
+
455
+ if let Some(target_id) = target_session_id {
456
+ if target_id == session.chat_id {
457
+ ui.push_notice(format!("Already in session '{target_id}'."));
458
+ } else {
459
+ session.chat_id = target_id.clone();
460
+ mint_core::live_sync::update_live_sync_chat_id(&session.chat_id);
461
+ ui.reload_transcript(&session.chat_id, &session.current_dir);
462
+ if let Ok(memory) = MemoryStore::open_default() {
463
+ let sessions = memory.list_chat_sessions().unwrap_or_default();
464
+ let title = sessions
465
+ .iter()
466
+ .find(|s| s.id == session.chat_id)
467
+ .map(|s| s.title.as_str())
468
+ .unwrap_or("Conversation");
469
+ ui.push_notice(format!("Switched to session: {title} ({target_id})"));
470
+ if let Ok(recent) = memory.get_session_preview(&session.chat_id, 2)
471
+ && let Some(last) = recent.first()
472
+ {
473
+ let snippet = crate::interactive::truncate_utf8(&last.user_text, 80);
474
+ ui.push_notice(format!("Last turn: {snippet}"));
475
+ }
476
+ } else {
477
+ ui.push_notice(format!("Switched to session: {target_id}"));
478
+ }
426
479
  }
480
+ } else {
481
+ ui.push_notice("Resume cancelled.");
427
482
  }
428
483
  Some(SlashResult::Handled)
429
484
  }
430
485
 
431
486
  "/rewind" => {
432
- let checkpoints = mint_core::git::list_checkpoints(mint_core::CHAT_CLI_ID);
487
+ let (target_rewind_chat_id, checkpoints) = {
488
+ let local_cps = mint_core::git::list_checkpoints(&session.chat_id);
489
+ if local_cps.is_empty() && session.chat_id != mint_core::CHAT_CLI_ID {
490
+ let fallback_cps = mint_core::git::list_checkpoints(mint_core::CHAT_CLI_ID);
491
+ if !fallback_cps.is_empty() {
492
+ (mint_core::CHAT_CLI_ID.to_string(), fallback_cps)
493
+ } else {
494
+ (session.chat_id.clone(), local_cps)
495
+ }
496
+ } else {
497
+ (session.chat_id.clone(), local_cps)
498
+ }
499
+ };
433
500
  if rest.is_empty() {
434
501
  if checkpoints.is_empty() {
435
502
  println!("\n{DIM}No git checkpoints recorded for this CLI session yet.{RESET}");
@@ -472,7 +539,7 @@ pub async fn handle_slash_command(
472
539
  Ok(step) => {
473
540
  match mint_core::git::rollback_to_step(
474
541
  &session.current_dir,
475
- mint_core::CHAT_CLI_ID,
542
+ &target_rewind_chat_id,
476
543
  step,
477
544
  ) {
478
545
  Ok(msg) => {
@@ -493,67 +560,125 @@ pub async fn handle_slash_command(
493
560
  }
494
561
  }
495
562
 
563
+ "/theme" => {
564
+ let current = crate::terminal_theme::TuiTheme::from_config(&session.config.tui_theme);
565
+ match ui.prompt_theme(current.index()) {
566
+ Ok(Some(index)) => {
567
+ let selected = crate::terminal_theme::TuiTheme::from_index(index);
568
+ let previous = std::mem::replace(
569
+ &mut session.config.tui_theme,
570
+ selected.config_value().to_owned(),
571
+ );
572
+ if let Err(error) = mint_core::save_config(&session.config) {
573
+ session.config.tui_theme = previous;
574
+ ui.report_error(&error.into());
575
+ } else {
576
+ ui.set_theme(selected);
577
+ ui.push_notice(format!("Theme: {}", selected.label()));
578
+ }
579
+ }
580
+ Ok(None) => {}
581
+ Err(error) => ui.report_error(&error),
582
+ }
583
+ Some(SlashResult::Handled)
584
+ }
585
+
496
586
  "/palette" => {
497
587
  let options = vec![
498
- "Quick Actions Menu".to_string(),
499
- "Switch AI Model (/models)".to_string(),
500
- "Toggle Plan Mode (/plan)".to_string(),
501
- "Toggle Fast Mode (/fast)".to_string(),
502
- "Git Checkpoint Rollback (/rewind)".to_string(),
503
- "Manage MCP Servers (/mcp)".to_string(),
504
- "Manage Memory & Facts (/memory)".to_string(),
505
- "Change Directory (/cd)".to_string(),
506
- "Clear Conversation (/clear)".to_string(),
507
- "Help & Documentation (/help)".to_string(),
588
+ ChoiceItem::with_description(
589
+ "Quick Actions Menu",
590
+ "Show general quick actions",
591
+ "tip",
592
+ ),
593
+ ChoiceItem::with_description(
594
+ "Switch AI Model (/models)",
595
+ "Change active model or provider",
596
+ "/models",
597
+ ),
598
+ ChoiceItem::with_description(
599
+ "Toggle Plan Mode (/plan)",
600
+ "Turn plan mode on or off",
601
+ "/plan",
602
+ ),
603
+ ChoiceItem::with_description(
604
+ "Toggle Fast Mode (/fast)",
605
+ "Turn fast mode on or off",
606
+ "/fast",
607
+ ),
608
+ ChoiceItem::with_description(
609
+ "Git Checkpoint Rollback (/rewind)",
610
+ "Undo recent AI edits",
611
+ "/rewind",
612
+ ),
613
+ ChoiceItem::with_description(
614
+ "Manage MCP Servers (/mcp)",
615
+ "Add, remove, or configure tools",
616
+ "/mcp",
617
+ ),
618
+ ChoiceItem::with_description(
619
+ "Manage Memory & Facts (/memory)",
620
+ "View or clear project memory",
621
+ "/memory",
622
+ ),
623
+ ChoiceItem::with_description(
624
+ "Change Directory (/cd)",
625
+ "Change workspace directory",
626
+ "/cd",
627
+ ),
628
+ ChoiceItem::with_description(
629
+ "Clear Conversation (/clear)",
630
+ "Clear current conversation history",
631
+ "/clear",
632
+ ),
633
+ ChoiceItem::with_description(
634
+ "Help & Documentation (/help)",
635
+ "List all available slash commands",
636
+ "/help",
637
+ ),
508
638
  ];
509
- match prompt_interactive_select("Universal Command Palette", &options, &options[0]) {
510
- Ok(Some(sel)) => {
511
- let next_cmd = if sel.contains("/models") {
512
- "/models"
513
- } else if sel.contains("/plan") {
514
- "/plan"
515
- } else if sel.contains("/fast") {
516
- "/fast"
517
- } else if sel.contains("/rewind") {
518
- "/rewind"
519
- } else if sel.contains("/mcp") {
520
- "/mcp"
521
- } else if sel.contains("/memory") {
522
- "/memory"
523
- } else if sel.contains("/clear") {
524
- "/clear"
525
- } else if sel.contains("/help") {
526
- "/help"
527
- } else {
528
- println!(
529
- "{MINT}Tip:{RESET} Type {BLUE}/<command>{RESET} or press {MINT}Tab{RESET} to autocomplete commands anytime.\n"
639
+ match ui.prompt_choice(
640
+ "Universal Command Palette",
641
+ "Select command to run",
642
+ &options,
643
+ ) {
644
+ Ok(Some(idx)) => {
645
+ let val = &options[idx].value;
646
+ if val == "tip" {
647
+ ui.push_notice(
648
+ "Tip: Type /<command> or press Tab to autocomplete commands anytime.",
530
649
  );
531
- return Some(SlashResult::Handled);
532
- };
533
- Box::pin(handle_slash_command(session, next_cmd)).await
534
- }
535
- Ok(None) => Some(SlashResult::Handled),
536
- Err(e) => {
537
- println!("{ERROR}Palette error:{RESET} {e}\n");
538
- Some(SlashResult::Handled)
650
+ Some(SlashResult::Handled)
651
+ } else {
652
+ Box::pin(handle_slash_command(session, ui, val)).await
653
+ }
539
654
  }
655
+ _ => Some(SlashResult::Handled),
540
656
  }
541
657
  }
542
658
 
659
+ "/thought" | "/think" => {
660
+ if let Some(record) = get_last_thought() {
661
+ let _ = show_thought_viewer(&record.thought, &record.elapsed_str);
662
+ } else {
663
+ ui.push_notice("No recent thought process recorded for this turn.");
664
+ }
665
+ Some(SlashResult::Handled)
666
+ }
667
+
543
668
  "/plan" if rest == "list" => {
544
669
  let files = list_plan_files(&session.current_dir);
545
670
  if files.is_empty() {
546
- println!("{DIM}No saved plans yet.{RESET}\n");
671
+ ui.push_notice("No saved plans yet.");
547
672
  } else {
548
- println!("\n{BLUE}Saved plans:{RESET}");
673
+ let mut lines = vec![format!("{BLUE}Saved plans:{RESET}")];
549
674
  for (idx, path) in files.iter().enumerate() {
550
- println!(
675
+ lines.push(format!(
551
676
  " {DIM}[{}]{RESET} {BLUE}{}{RESET}",
552
677
  idx + 1,
553
678
  first_line_preview(path)
554
- );
679
+ ));
555
680
  }
556
- println!();
681
+ ui.push_command_output(lines.join("\n"));
557
682
  }
558
683
  Some(SlashResult::Handled)
559
684
  }
@@ -572,12 +697,12 @@ pub async fn handle_slash_command(
572
697
  };
573
698
  match found {
574
699
  Some(path) => match std::fs::read_to_string(path) {
575
- Ok(content) => println!("\n{content}\n"),
576
- Err(e) => println!("{ERROR}Could not read plan file:{RESET} {e}\n"),
700
+ Ok(content) => ui.push_command_output(content),
701
+ Err(e) => ui.push_notice(format!("Could not read plan file: {e}")),
577
702
  },
578
- None => println!(
579
- "{WARN}No saved plan matching \"{arg}\" — run /plan list to see saved plans.{RESET}\n"
580
- ),
703
+ None => ui.push_notice(format!(
704
+ "No saved plan matching \"{arg}\" — run /plan list to see saved plans."
705
+ )),
581
706
  }
582
707
  Some(SlashResult::Handled)
583
708
  }
@@ -585,27 +710,13 @@ pub async fn handle_slash_command(
585
710
  "/plan" => {
586
711
  let choice = if rest.is_empty() {
587
712
  let options = vec![
588
- "on (read-only until plan is approved)".to_string(),
589
- "off (edits/shell run immediately)".to_string(),
713
+ ChoiceItem::new("on (read-only until plan is approved)", "on"),
714
+ ChoiceItem::new("off (edits/shell run immediately)", "off"),
590
715
  ];
591
- let current = if session.plan_mode {
592
- &options[0]
593
- } else {
594
- &options[1]
595
- };
596
- match prompt_interactive_select("Select Plan Mode", &options, current) {
597
- Ok(Some(sel)) => {
598
- if sel.starts_with("on") {
599
- Some(true)
600
- } else {
601
- Some(false)
602
- }
603
- }
604
- Ok(None) => None,
605
- Err(e) => {
606
- println!("{ERROR}Error selecting plan mode:{RESET} {e}\n");
607
- None
608
- }
716
+ match ui.prompt_choice("Select Plan Mode", "", &options) {
717
+ Ok(Some(0)) => Some(true),
718
+ Ok(Some(1)) => Some(false),
719
+ _ => None,
609
720
  }
610
721
  } else {
611
722
  match rest {
@@ -613,9 +724,7 @@ pub async fn handle_slash_command(
613
724
  "off" => Some(false),
614
725
  task => {
615
726
  session.plan_mode = true;
616
- println!(
617
- "{DIM}[Plan] mode ON — agent will investigate read-only and present a plan before editing{RESET}\n"
618
- );
727
+ ui.push_notice("[Plan] mode ON — agent will investigate read-only and present a plan before editing");
619
728
  return Some(SlashResult::ForwardToAgent(task.to_string()));
620
729
  }
621
730
  }
@@ -624,11 +733,9 @@ pub async fn handle_slash_command(
624
733
  if let Some(mode) = choice {
625
734
  session.plan_mode = mode;
626
735
  if session.plan_mode {
627
- println!(
628
- "{DIM}[Plan] mode ON — agent will investigate read-only and present a plan via exit_plan_mode before editing files or running commands{RESET}\n"
629
- );
736
+ ui.push_notice("[Plan] mode ON — agent will investigate read-only and present a plan via exit_plan_mode before editing files or running commands");
630
737
  } else {
631
- println!("{DIM}[Plan] mode OFF{RESET}\n");
738
+ ui.push_notice("[Plan] mode OFF");
632
739
  }
633
740
  }
634
741
  Some(SlashResult::Handled)
@@ -1438,18 +1545,15 @@ pub async fn handle_slash_command(
1438
1545
 
1439
1546
  "/bg" => {
1440
1547
  if rest.is_empty() {
1441
- println!("{ERROR}Usage:{RESET} /bg <query>\n");
1548
+ ui.push_notice("Usage: /bg <query>");
1442
1549
  return Some(SlashResult::Handled);
1443
1550
  }
1444
1551
  let id = session
1445
1552
  .jobs
1446
1553
  .spawn(&session.config, &session.current_dir, rest);
1447
- println!(
1448
- "{DIM}Started background job #{id}. Keep chatting — check on it with {RESET}/jobs{DIM}, or view its result with {RESET}/jobs show {id}{DIM}.{RESET}"
1449
- );
1450
- println!(
1451
- "{DIM}Note: /bg runs non-interactively, so file writes/shell/plugin/MCP actions needing approval are declined automatically.{RESET}\n"
1452
- );
1554
+ ui.push_notice(format!(
1555
+ "Started background job #{id}. Keep chatting — check on it with /jobs, or view its result with /jobs show {id}."
1556
+ ));
1453
1557
  Some(SlashResult::Handled)
1454
1558
  }
1455
1559
 
@@ -1457,9 +1561,9 @@ pub async fn handle_slash_command(
1457
1561
  if rest.is_empty() {
1458
1562
  let list = session.jobs.list();
1459
1563
  if list.is_empty() {
1460
- println!("{DIM}No background jobs yet. Start one with /bg <query>.{RESET}\n");
1564
+ ui.push_notice("No background jobs yet. Start one with /bg <query>.");
1461
1565
  } else {
1462
- println!("\n{BLUE}Background jobs{RESET}");
1566
+ let mut lines = vec![format!("{BLUE}Background jobs{RESET}")];
1463
1567
  for job in &list {
1464
1568
  let status_str = match job.status {
1465
1569
  JobStatus::Running => format!("{MINT}running{RESET}"),
@@ -1473,13 +1577,13 @@ pub async fn handle_slash_command(
1473
1577
  } else {
1474
1578
  ""
1475
1579
  };
1476
- println!(
1580
+ lines.push(format!(
1477
1581
  " #{:<3} [{status_str}] {DIM}{:>4}s{RESET} {preview}{suffix}",
1478
1582
  job.id,
1479
1583
  job.elapsed_secs(),
1480
- );
1584
+ ));
1481
1585
  }
1482
- println!();
1586
+ ui.push_command_output(lines.join("\n"));
1483
1587
  }
1484
1588
  return Some(SlashResult::Handled);
1485
1589
  }
@@ -1492,22 +1596,30 @@ pub async fn handle_slash_command(
1492
1596
  match sub {
1493
1597
  "show" => match arg.parse::<u32>().ok().and_then(|id| session.jobs.show(id)) {
1494
1598
  Some(job) => {
1495
- println!("\n{BLUE}Job #{} — {}{RESET}", job.id, job.status);
1496
- println!("{DIM}Query:{RESET} {}", job.query);
1599
+ let mut lines = vec![
1600
+ format!("{BLUE}Job #{} — {}{RESET}", job.id, job.status),
1601
+ format!("{DIM}Query:{RESET} {}", job.query),
1602
+ ];
1497
1603
  match job.result {
1498
- Some(result) => println!("\n{}\n", result),
1499
- None => println!("{DIM}(still running — no result yet){RESET}\n"),
1604
+ Some(result) => {
1605
+ lines.push(String::new());
1606
+ lines.push(result);
1607
+ }
1608
+ None => {
1609
+ lines.push(format!("{DIM}(still running — no result yet){RESET}"))
1610
+ }
1500
1611
  }
1612
+ ui.push_command_output(lines.join("\n"));
1501
1613
  }
1502
- None => println!("{ERROR}No such job.{RESET}\n"),
1614
+ None => ui.push_notice("No such job."),
1503
1615
  },
1504
1616
  "cancel" => match arg.parse::<u32>().ok() {
1505
1617
  Some(id) if session.jobs.cancel(id) => {
1506
- println!("{DIM}Cancelling job #{id}...{RESET}\n");
1618
+ ui.push_notice(format!("Cancelling job #{id}..."));
1507
1619
  }
1508
- _ => println!("{ERROR}No such running job.{RESET}\n"),
1620
+ _ => ui.push_notice("No such running job."),
1509
1621
  },
1510
- _ => println!("{ERROR}Usage:{RESET} /jobs [show <id>|cancel <id>]\n"),
1622
+ _ => ui.push_notice("Usage: /jobs [show <id>|cancel <id>]"),
1511
1623
  }
1512
1624
  Some(SlashResult::Handled)
1513
1625
  }
@@ -1516,11 +1628,11 @@ pub async fn handle_slash_command(
1516
1628
  if rest.is_empty() {
1517
1629
  let jobs = mint_core::bg_shell::list_jobs();
1518
1630
  if jobs.is_empty() {
1519
- println!(
1520
- "{DIM}No background shell jobs. The agent starts one when it calls run_shell with background: true.{RESET}\n"
1631
+ ui.push_notice(
1632
+ "No background shell jobs. The agent starts one when it calls run_shell with background: true."
1521
1633
  );
1522
1634
  } else {
1523
- println!("\n{BLUE}Background shell jobs{RESET}");
1635
+ let mut lines = vec![format!("{BLUE}Background shell jobs{RESET}")];
1524
1636
  for job in &jobs {
1525
1637
  let status_str = match &job.status {
1526
1638
  mint_core::bg_shell::JobStatus::Running => {
@@ -1543,12 +1655,12 @@ pub async fn handle_slash_command(
1543
1655
  } else {
1544
1656
  ""
1545
1657
  };
1546
- println!(
1658
+ lines.push(format!(
1547
1659
  " {:<6} [{status_str}] {DIM}{:>4}s{RESET} {preview}{suffix}",
1548
1660
  job.id, job.elapsed_secs,
1549
- );
1661
+ ));
1550
1662
  }
1551
- println!();
1663
+ ui.push_command_output(lines.join("\n"));
1552
1664
  }
1553
1665
  return Some(SlashResult::Handled);
1554
1666
  }
@@ -1561,48 +1673,48 @@ pub async fn handle_slash_command(
1561
1673
  match sub {
1562
1674
  "show" if !arg.is_empty() => match mint_core::bg_shell::snapshot(arg) {
1563
1675
  Ok(job) => {
1564
- println!("\n{BLUE}Job {} — {}{RESET}", job.id, job.command);
1676
+ let mut lines =
1677
+ vec![format!("{BLUE}Job {} — {}{RESET}", job.id, job.command)];
1565
1678
  if let Some(pid) = job.pid {
1566
- println!("{DIM}pid: {pid} cwd: {}{RESET}", job.cwd.display());
1679
+ lines.push(format!(
1680
+ "{DIM}pid: {pid} cwd: {}{RESET}",
1681
+ job.cwd.display()
1682
+ ));
1567
1683
  }
1568
- println!("{DIM}stdout:{RESET}\n{}", job.stdout);
1569
- println!("{DIM}stderr:{RESET}\n{}\n", job.stderr);
1684
+ lines.push(format!("{DIM}stdout:{RESET}\n{}", job.stdout));
1685
+ lines.push(format!("{DIM}stderr:{RESET}\n{}", job.stderr));
1686
+ ui.push_command_output(lines.join("\n"));
1570
1687
  }
1571
- Err(_) => println!("{ERROR}No such job.{RESET}\n"),
1688
+ Err(_) => ui.push_notice("No such job."),
1572
1689
  },
1573
1690
  "kill" if !arg.is_empty() => match mint_core::bg_shell::kill_job(arg) {
1574
- Ok(msg) => println!("{DIM}{msg}{RESET}\n"),
1575
- Err(_) => println!("{ERROR}No such job.{RESET}\n"),
1691
+ Ok(msg) => ui.push_notice(msg),
1692
+ Err(_) => ui.push_notice("No such job."),
1576
1693
  },
1577
- _ => println!("{ERROR}Usage:{RESET} /shells [show <id>|kill <id>]\n"),
1694
+ _ => ui.push_notice("Usage: /shells [show <id>|kill <id>]"),
1578
1695
  }
1579
1696
  Some(SlashResult::Handled)
1580
1697
  }
1581
1698
 
1582
1699
  "/clear" | "/reset" => {
1583
- let options = vec![
1584
- "No (keep history)".to_string(),
1585
- "Yes (clear history)".to_string(),
1586
- ];
1587
- let choice = match prompt_interactive_select(
1588
- "Clear conversation history?",
1589
- &options,
1590
- &options[0],
1591
- ) {
1592
- Ok(Some(sel)) => sel == options[1],
1593
- _ => false,
1594
- };
1595
-
1700
+ let choice = ui
1701
+ .prompt_confirm("Clear conversation history?", false)
1702
+ .unwrap_or(false);
1596
1703
  if choice {
1704
+ clear_last_thought();
1705
+ session.history.clear();
1706
+ ui.clear_transcript();
1597
1707
  if let Ok(memory) = MemoryStore::open_default() {
1598
- match memory.clear_interactions() {
1599
- Ok(count) => println!("{DIM}Cleared {count} interactions.{RESET}"),
1600
- Err(error) => println!("{ERROR}Memory error:{RESET} {error}"),
1708
+ match memory.clear_interactions_for_chat(&session.chat_id) {
1709
+ Ok(count) => {
1710
+ ui.push_notice(format!("Cleared {count} interactions for session."));
1711
+ }
1712
+ Err(error) => ui.push_notice(format!("Memory error: {error}")),
1601
1713
  }
1602
1714
  }
1603
- println!("{DIM}Conversation context cleared.{RESET}\n");
1715
+ ui.push_notice("Conversation context cleared.");
1604
1716
  } else {
1605
- println!("{DIM}Cancelled.{RESET}\n");
1717
+ ui.push_notice("Cancelled.");
1606
1718
  }
1607
1719
  Some(SlashResult::Handled)
1608
1720
  }
@@ -1616,7 +1728,7 @@ pub async fn handle_slash_command(
1616
1728
  session.current_dir = new_dir.canonicalize().unwrap_or(new_dir);
1617
1729
  println!(
1618
1730
  "{DIM}Workspace: {}{RESET}\n",
1619
- format_path_with_tilde(&session.current_dir)
1731
+ format_workspace_with_branch(&session.current_dir)
1620
1732
  );
1621
1733
  } else {
1622
1734
  println!("{ERROR}Directory not found:{RESET} {rest}\n");
@@ -1625,6 +1737,8 @@ pub async fn handle_slash_command(
1625
1737
  Some(SlashResult::Handled)
1626
1738
  }
1627
1739
 
1740
+ "/branch" => Some(handle_branch_slash(session, ui, rest)),
1741
+
1628
1742
  "/veo" => {
1629
1743
  if rest.is_empty() {
1630
1744
  println!(
@@ -2272,8 +2386,35 @@ pub async fn handle_slash_command(
2272
2386
  }
2273
2387
  }
2274
2388
  }
2389
+ "refresh" => match mint_core::refresh_linked_folder(args) {
2390
+ Ok(status) => println!("Indexed {} files in {args}.\n", status.indexed_files),
2391
+ Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
2392
+ },
2393
+ "notes" => match mint_core::list_linked_folder_notes(args) {
2394
+ Ok(notes) => {
2395
+ for note in notes {
2396
+ println!("{} [{}] {}", note.created_at, note.status, note.path);
2397
+ }
2398
+ }
2399
+ Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
2400
+ },
2401
+ "save" => {
2402
+ if let Some((name, content)) = args.split_once('|') {
2403
+ match mint_core::save_linked_folder_note(name.trim(), content.trim()) {
2404
+ Ok(note) if note.status == "saved" => {
2405
+ println!("Saved note: {}", note.path)
2406
+ }
2407
+ Ok(note) => {
2408
+ println!("{ERROR}{}{RESET}", note.error.unwrap_or_default())
2409
+ }
2410
+ Err(e) => println!("{ERROR}Error:{RESET} {e}"),
2411
+ }
2412
+ } else {
2413
+ println!("{WARN}/link save <name> | <text>{RESET}");
2414
+ }
2415
+ }
2275
2416
  _ => println!(
2276
- "{WARN}/link usage: list | add <name> | <path> | <description> | remove <name>{RESET}\n"
2417
+ "{WARN}/link usage: list | add <name> | <path> | <description> | remove <name> | refresh <name> | notes <name> | save <name> | <text>{RESET}\n"
2277
2418
  ),
2278
2419
  }
2279
2420
  Some(SlashResult::Handled)
@@ -2337,7 +2478,7 @@ pub async fn handle_slash_command(
2337
2478
  .split_once(char::is_whitespace)
2338
2479
  .map(|(c, a)| (c, a.trim()))
2339
2480
  .unwrap_or((rest, ""));
2340
- handle_mcp_slash(session, subcmd, args);
2481
+ handle_mcp_slash(session, ui, subcmd, args);
2341
2482
  Some(SlashResult::Handled)
2342
2483
  }
2343
2484
 
@@ -2356,36 +2497,42 @@ pub async fn handle_slash_command(
2356
2497
  })
2357
2498
  .map(|v| v.len())
2358
2499
  .unwrap_or(0);
2359
- println!("\n{BLUE}─ Session Stats ─────────────────────────{RESET}");
2360
- println!(" Provider : {MINT}{provider}{RESET}");
2361
- println!(" Model : {model}");
2362
- println!(
2500
+ let mut out = Vec::new();
2501
+ out.push(format!(
2502
+ "\n{BLUE}─ Session Stats ─────────────────────────{RESET}"
2503
+ ));
2504
+ out.push(format!(" Provider : {MINT}{provider}{RESET}"));
2505
+ out.push(format!(" Model : {model}"));
2506
+ out.push(format!(
2363
2507
  " Workspace: {}",
2364
- format_path_with_tilde(&session.current_dir)
2365
- );
2366
- println!(
2508
+ format_workspace_with_branch(&session.current_dir)
2509
+ ));
2510
+ out.push(format!(
2367
2511
  " Fast mode: {}",
2368
2512
  if session.fast_mode { "on" } else { "off" }
2369
- );
2370
- println!(" Memory : {interactions} interactions");
2513
+ ));
2514
+ out.push(format!(" Memory : {interactions} interactions"));
2371
2515
  if let Some(ref img_data) = session.pending_image {
2372
2516
  let count = img_data.split_whitespace().count();
2373
2517
  if count > 1 {
2374
- println!(" Images : {WARN}{} images attached{RESET}", count);
2518
+ out.push(format!(
2519
+ " Images : {WARN}{} images attached{RESET}",
2520
+ count
2521
+ ));
2375
2522
  } else {
2376
- println!(" Image : {WARN}attached{RESET}");
2523
+ out.push(format!(" Image : {WARN}attached{RESET}"));
2377
2524
  }
2378
2525
  }
2379
-
2380
- println!();
2526
+ ui.push_command_output(out.join("\n"));
2381
2527
  Some(SlashResult::Handled)
2382
2528
  }
2383
2529
 
2384
2530
  "/release-notes" => {
2385
2531
  const RELEASE_NOTES: &str = include_str!("../../../../Release_Note.md");
2386
- println!("\n{BLUE}────────────────────────────────────────────{RESET}");
2387
- println!("{}", RELEASE_NOTES.trim());
2388
- println!("{BLUE}────────────────────────────────────────────{RESET}\n");
2532
+ ui.push_command_output(format!(
2533
+ "\n{BLUE}────────────────────────────────────────────{RESET}\n{}\n{BLUE}────────────────────────────────────────────{RESET}",
2534
+ RELEASE_NOTES.trim()
2535
+ ));
2389
2536
  Some(SlashResult::Handled)
2390
2537
  }
2391
2538
 
@@ -2541,7 +2688,7 @@ pub async fn handle_slash_command(
2541
2688
  } else if e.is_oauth {
2542
2689
  format!("{DIM}{:<22}{RESET}", plain_status)
2543
2690
  } else {
2544
- format!("\x1b[36m{:<22}\x1b[0m", plain_status)
2691
+ format!("{ANSI_BLUE}{:<22}\x1b[0m", plain_status)
2545
2692
  };
2546
2693
  println!(
2547
2694
  " {BLUE}❯ {:<18}{RESET}{} {DIM}{short_desc}{RESET}",
@@ -2824,6 +2971,136 @@ pub async fn handle_slash_command(
2824
2971
  }
2825
2972
  }
2826
2973
 
2974
+ fn handle_branch_slash(
2975
+ session: &mut InteractiveSession,
2976
+ ui: &mut dyn CommandUi,
2977
+ requested: &str,
2978
+ ) -> SlashResult {
2979
+ use mint_core::git::{BranchChange, BranchChangeOutcome, change_branch, read_branch_info};
2980
+
2981
+ let info = match read_branch_info(&session.current_dir) {
2982
+ Ok(info) if info.is_repository => info,
2983
+ Ok(_) => {
2984
+ ui.push_notice("Current workspace is not a Git repository.");
2985
+ return SlashResult::Handled;
2986
+ }
2987
+ Err(error) => {
2988
+ ui.push_notice(format!("Git error: {error}"));
2989
+ return SlashResult::Handled;
2990
+ }
2991
+ };
2992
+
2993
+ let (remote, branch) = if requested.is_empty() {
2994
+ let mut choices = info
2995
+ .branches
2996
+ .iter()
2997
+ .map(|branch| {
2998
+ (
2999
+ format!(
3000
+ "{branch}{}",
3001
+ if info.current_branch.as_deref() == Some(branch) {
3002
+ " (current)"
3003
+ } else {
3004
+ ""
3005
+ }
3006
+ ),
3007
+ false,
3008
+ branch.clone(),
3009
+ )
3010
+ })
3011
+ .collect::<Vec<_>>();
3012
+ choices.extend(
3013
+ info.remote_branches
3014
+ .iter()
3015
+ .map(|branch| (format!("{branch} (remote · track)"), true, branch.clone())),
3016
+ );
3017
+ if choices.is_empty() {
3018
+ ui.push_notice("No branches found in this workspace.");
3019
+ return SlashResult::Handled;
3020
+ }
3021
+ let items: Vec<ChoiceItem> = choices
3022
+ .iter()
3023
+ .map(|(label, is_remote, name)| {
3024
+ let desc = if *is_remote {
3025
+ "remote · track"
3026
+ } else {
3027
+ "local"
3028
+ };
3029
+ ChoiceItem::with_description(label.clone(), desc, name.clone())
3030
+ })
3031
+ .collect();
3032
+ match ui.prompt_choice("Switch Git Branch", "Select a branch to switch to", &items) {
3033
+ Ok(Some(idx)) => {
3034
+ let (_, is_remote, ref b) = choices[idx];
3035
+ (is_remote, b.clone())
3036
+ }
3037
+ _ => {
3038
+ ui.push_notice("Branch selection cancelled.");
3039
+ return SlashResult::Handled;
3040
+ }
3041
+ }
3042
+ } else if info.branches.iter().any(|branch| branch == requested) {
3043
+ (false, requested.to_string())
3044
+ } else if info
3045
+ .remote_branches
3046
+ .iter()
3047
+ .any(|branch| branch == requested)
3048
+ {
3049
+ (true, requested.to_string())
3050
+ } else {
3051
+ ui.push_notice(format!("Branch not found: {requested}"));
3052
+ return SlashResult::Handled;
3053
+ };
3054
+
3055
+ if !remote && info.current_branch.as_deref() == Some(branch.as_str()) {
3056
+ ui.push_notice(format!("Already on branch {branch}."));
3057
+ return SlashResult::Handled;
3058
+ }
3059
+
3060
+ let change = if remote {
3061
+ BranchChange::Track {
3062
+ remote_branch: branch.clone(),
3063
+ }
3064
+ } else {
3065
+ BranchChange::Switch {
3066
+ branch: branch.clone(),
3067
+ }
3068
+ };
3069
+ let result = match change_branch(&session.current_dir, &change, false) {
3070
+ Ok(BranchChangeOutcome::Changed { info }) => Ok(info),
3071
+ Ok(BranchChangeOutcome::ConfirmationRequired { .. }) => {
3072
+ match ui.prompt_confirm(
3073
+ "Workspace has uncommitted changes. Continue switching branches?",
3074
+ false,
3075
+ ) {
3076
+ Ok(true) => {
3077
+ change_branch(&session.current_dir, &change, true).and_then(|outcome| {
3078
+ match outcome {
3079
+ BranchChangeOutcome::Changed { info } => Ok(info),
3080
+ BranchChangeOutcome::ConfirmationRequired { .. } => {
3081
+ Err("Workspace still requires confirmation.".to_string())
3082
+ }
3083
+ }
3084
+ })
3085
+ }
3086
+ _ => {
3087
+ ui.push_notice("Branch change cancelled.");
3088
+ return SlashResult::Handled;
3089
+ }
3090
+ }
3091
+ }
3092
+ Err(error) => Err(error),
3093
+ };
3094
+ match result {
3095
+ Ok(next) => {
3096
+ let current = next.current_branch.as_deref().unwrap_or("detached HEAD");
3097
+ ui.push_notice(format!("Switched to {current}."));
3098
+ }
3099
+ Err(error) => ui.push_notice(format!("Git error: {error}")),
3100
+ }
3101
+ SlashResult::Handled
3102
+ }
3103
+
2827
3104
  // ── /mcp interactive handling ────────────────────────────────────────────────
2828
3105
 
2829
3106
  /// Pull the just-persisted config back into the session so subsequent turns
@@ -2834,9 +3111,14 @@ fn reload_session_config(session: &mut InteractiveSession) {
2834
3111
  }
2835
3112
  }
2836
3113
 
2837
- fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str) {
3114
+ fn handle_mcp_slash(
3115
+ session: &mut InteractiveSession,
3116
+ ui: &mut dyn CommandUi,
3117
+ subcmd: &str,
3118
+ args: &str,
3119
+ ) {
2838
3120
  match subcmd {
2839
- "list" | "" => mcp_interactive_picker(session),
3121
+ "list" | "" => mcp_interactive_picker(session, ui),
2840
3122
 
2841
3123
  "add" => {
2842
3124
  let mut tokens: Vec<&str> = args.split_whitespace().collect();
@@ -2857,54 +3139,52 @@ fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str)
2857
3139
  if allow_all {
2858
3140
  let _ = crate::mcp::allow(name, "*");
2859
3141
  }
2860
- println!(
2861
- "{MINT}Added MCP server '{name}'{}.{RESET}\n",
3142
+ ui.push_notice(format!(
3143
+ "Added MCP server '{name}'{}.",
2862
3144
  if allow_all {
2863
3145
  " (all tools allowed)"
2864
3146
  } else {
2865
3147
  ""
2866
3148
  }
2867
- );
3149
+ ));
2868
3150
  reload_session_config(session);
2869
3151
  }
2870
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3152
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2871
3153
  }
2872
3154
  }
2873
- _ => println!(
2874
- "{WARN}/mcp add usage:{RESET} <name> <command|url> [args...] [--allow-all]\n"
2875
- ),
3155
+ _ => ui.push_notice("/mcp add usage: <name> <command|url> [args...] [--allow-all]"),
2876
3156
  }
2877
3157
  }
2878
3158
 
2879
3159
  "remove" if !args.is_empty() => match crate::mcp::remove(args) {
2880
3160
  Ok(true) => {
2881
- println!("{MINT}Removed '{args}'.{RESET}\n");
3161
+ ui.push_notice(format!("Removed '{args}'."));
2882
3162
  reload_session_config(session);
2883
3163
  }
2884
- Ok(false) => println!("{WARN}No MCP server named '{args}'.{RESET}\n"),
2885
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3164
+ Ok(false) => ui.push_notice(format!("No MCP server named '{args}'.")),
3165
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2886
3166
  },
2887
3167
 
2888
3168
  "clear" => match crate::mcp::clear() {
2889
3169
  Ok(()) => {
2890
- println!("{MINT}Removed all MCP servers.{RESET}\n");
3170
+ ui.push_notice("Removed all MCP servers.");
2891
3171
  reload_session_config(session);
2892
3172
  }
2893
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3173
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2894
3174
  },
2895
3175
 
2896
3176
  "enable" | "disable" if !args.is_empty() => {
2897
3177
  let disable = subcmd == "disable";
2898
3178
  match crate::mcp::set_disabled(args, disable) {
2899
3179
  Ok(true) => {
2900
- println!(
2901
- "{MINT}{} '{args}'.{RESET}\n",
3180
+ ui.push_notice(format!(
3181
+ "{} '{args}'.",
2902
3182
  if disable { "Disabled" } else { "Enabled" }
2903
- );
3183
+ ));
2904
3184
  reload_session_config(session);
2905
3185
  }
2906
- Ok(false) => println!("{WARN}No MCP server named '{args}'.{RESET}\n"),
2907
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3186
+ Ok(false) => ui.push_notice(format!("No MCP server named '{args}'.")),
3187
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2908
3188
  }
2909
3189
  }
2910
3190
 
@@ -2918,9 +3198,7 @@ fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str)
2918
3198
  crate::mcp::edit(name, Some(value.to_string()), None, None, None)
2919
3199
  }
2920
3200
  "command" => {
2921
- println!(
2922
- "{WARN}/mcp edit <name> command <value> needs a value{RESET}\n"
2923
- );
3201
+ ui.push_notice("/mcp edit <name> command <value> needs a value");
2924
3202
  return;
2925
3203
  }
2926
3204
  "args" => crate::mcp::edit(
@@ -2938,20 +3216,20 @@ fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str)
2938
3216
  Some((!value.is_empty()).then(|| value.to_string())),
2939
3217
  ),
2940
3218
  _ => {
2941
- println!("{WARN}Editable fields:{RESET} command | args | icon\n");
3219
+ ui.push_notice("Editable fields: command | args | icon");
2942
3220
  return;
2943
3221
  }
2944
3222
  };
2945
3223
  match result {
2946
3224
  Ok(true) => {
2947
- println!("{MINT}Updated '{name}'.{RESET}\n");
3225
+ ui.push_notice(format!("Updated '{name}'."));
2948
3226
  reload_session_config(session);
2949
3227
  }
2950
- Ok(false) => println!("{WARN}No MCP server named '{name}'.{RESET}\n"),
2951
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3228
+ Ok(false) => ui.push_notice(format!("No MCP server named '{name}'.")),
3229
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2952
3230
  }
2953
3231
  }
2954
- _ => println!("{WARN}/mcp edit usage:{RESET} <name> command|args|icon <value>\n"),
3232
+ _ => ui.push_notice("/mcp edit usage: <name> command|args|icon <value>"),
2955
3233
  }
2956
3234
  }
2957
3235
 
@@ -2966,45 +3244,43 @@ fn handle_mcp_slash(session: &mut InteractiveSession, subcmd: &str, args: &str)
2966
3244
  };
2967
3245
  match result {
2968
3246
  Ok(true) => {
2969
- println!("{MINT}{verb} {server}/{tool}.{RESET}\n");
3247
+ ui.push_notice(format!("{verb} {server}/{tool}."));
2970
3248
  reload_session_config(session);
2971
3249
  }
2972
- Ok(false) => println!("{DIM}No change for {server}/{tool}.{RESET}\n"),
2973
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3250
+ Ok(false) => ui.push_notice(format!("No change for {server}/{tool}.")),
3251
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2974
3252
  }
2975
3253
  }
2976
- _ => println!("{WARN}/mcp {subcmd} usage:{RESET} <server> <tool>\n"),
3254
+ _ => ui.push_notice(format!("/mcp {subcmd} usage: <server> <tool>")),
2977
3255
  }
2978
3256
  }
2979
3257
 
2980
3258
  "reauth" if !args.is_empty() => {
2981
- println!(
2982
- "{DIM}Re-authenticating MCP server '{args}'... (a browser tab may open){RESET}\n"
2983
- );
3259
+ ui.push_notice(format!(
3260
+ "Re-authenticating MCP server '{args}'... (a browser tab may open)"
3261
+ ));
2984
3262
  match crate::mcp::reauth(args) {
2985
- Ok(true) => println!("{MINT}Re-authentication succeeded for '{args}'.{RESET}\n"),
2986
- Ok(false) => println!(
2987
- "{ERROR}Re-authentication failed for '{args}' (see output above).{RESET}\n"
2988
- ),
2989
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3263
+ Ok(true) => ui.push_notice(format!("Re-authentication succeeded for '{args}'.")),
3264
+ Ok(false) => ui.push_notice(format!("Re-authentication failed for '{args}'.")),
3265
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
2990
3266
  }
2991
3267
  }
2992
3268
 
2993
- _ => println!(
2994
- "{WARN}/mcp usage:{RESET} list | add <name> <cmd> [args...] | remove <name> | \
3269
+ _ => ui.push_notice(
3270
+ "/mcp usage: list | add <name> <cmd> [args...] | remove <name> | \
2995
3271
  enable|disable <name> | edit <name> command|args|icon <value> | \
2996
- allow|disallow <server> <tool> | reauth <server> | clear\n"
3272
+ allow|disallow <server> <tool> | reauth <server> | clear",
2997
3273
  ),
2998
3274
  }
2999
3275
  }
3000
3276
 
3001
- /// The arrow-key `/mcp` flow: an "add" row plus every configured server; picking
3277
+ /// The `/mcp` flow: an "add" row plus every configured server; picking
3002
3278
  /// a server opens a per-server action menu.
3003
- fn mcp_interactive_picker(session: &mut InteractiveSession) {
3279
+ fn mcp_interactive_picker(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3004
3280
  let servers = match crate::mcp::list() {
3005
3281
  Ok(servers) => servers,
3006
3282
  Err(e) => {
3007
- println!("{ERROR}MCP error:{RESET} {e}\n");
3283
+ ui.push_notice(format!("MCP error: {e}"));
3008
3284
  return;
3009
3285
  }
3010
3286
  };
@@ -3019,100 +3295,114 @@ fn mcp_interactive_picker(session: &mut InteractiveSession) {
3019
3295
  .and_then(|v| v.as_object())
3020
3296
  .cloned();
3021
3297
 
3022
- let mut choices = vec![ADD_ROW.to_string()];
3298
+ let mut choices = vec![ChoiceItem::with_description(
3299
+ ADD_ROW,
3300
+ "Connect a new MCP server (catalog, remote SSE, or local stdio)",
3301
+ ADD_ROW,
3302
+ )];
3023
3303
  let mut names = Vec::new();
3024
- let width = servers.keys().map(|k| k.len()).max().unwrap_or(6) + 2;
3025
3304
  for (name, srv) in &servers {
3026
3305
  let tools = allowed
3027
3306
  .as_ref()
3028
3307
  .and_then(|m| m.get(name))
3029
3308
  .and_then(|v| v.as_array());
3030
3309
  let label = match tools {
3031
- Some(a) if a.iter().any(|t| t.as_str() == Some("*")) => {
3032
- format!("{MINT}[tools: *]{RESET}")
3033
- }
3310
+ Some(a) if a.iter().any(|t| t.as_str() == Some("*")) => "[tools: *]".to_string(),
3034
3311
  Some(a) if !a.is_empty() => format!(
3035
- "{MINT}[tools: {}]{RESET}",
3312
+ "[tools: {}]",
3036
3313
  a.iter()
3037
3314
  .filter_map(|t| t.as_str())
3038
3315
  .collect::<Vec<_>>()
3039
3316
  .join(", ")
3040
3317
  ),
3041
- _ => format!("{DIM}[no tools allowed]{RESET}"),
3042
- };
3043
- let disabled = if srv.disabled {
3044
- format!(" {DIM}(disabled){RESET}")
3045
- } else {
3046
- String::new()
3318
+ _ => "[no tools allowed]".to_string(),
3047
3319
  };
3320
+ let disabled = if srv.disabled { " (disabled)" } else { "" };
3048
3321
  let desc = if let Some(url) = srv.remote_url() {
3049
3322
  format!("(url: {url})")
3050
3323
  } else {
3051
3324
  format!("({} {})", srv.command, srv.args.join(" "))
3052
3325
  };
3053
- choices.push(format!(
3054
- "{name:<width$}{label}{disabled} {DIM}{desc}{RESET}",
3326
+ choices.push(ChoiceItem::with_description(
3327
+ name.clone(),
3328
+ format!("{label}{disabled} {desc}"),
3329
+ name.clone(),
3055
3330
  ));
3056
3331
  names.push(name.clone());
3057
3332
  }
3058
- choices.push(CANCEL.to_string());
3059
-
3060
- let Ok(Some(pick)) = prompt_interactive_select("MCP Servers", &choices, ADD_ROW) else {
3333
+ choices.push(ChoiceItem::with_description(
3334
+ CANCEL,
3335
+ "Close this menu",
3336
+ CANCEL,
3337
+ ));
3338
+
3339
+ let Ok(Some(idx)) = ui.prompt_choice(
3340
+ "MCP Servers",
3341
+ "Select a server to manage or add a new one",
3342
+ &choices,
3343
+ ) else {
3061
3344
  return;
3062
3345
  };
3346
+ let pick = &choices[idx].label;
3063
3347
  if pick == CANCEL {
3064
3348
  return;
3065
3349
  }
3066
3350
  if pick == ADD_ROW {
3067
- mcp_add_flow(session);
3351
+ mcp_add_flow(session, ui);
3068
3352
  return;
3069
3353
  }
3070
- let Some(pos) = choices.iter().position(|c| *c == pick) else {
3071
- return;
3072
- };
3073
- let name = names[pos - 1].clone();
3354
+ let name = names[idx - 1].clone();
3074
3355
  let disabled = servers.get(&name).map(|s| s.disabled).unwrap_or(false);
3075
- mcp_server_actions(session, &name, disabled);
3356
+ mcp_server_actions(session, ui, &name, disabled);
3076
3357
  }
3077
3358
 
3078
- fn mcp_add_flow(session: &mut InteractiveSession) {
3359
+ fn mcp_add_flow(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3079
3360
  let choices = vec![
3080
- "From catalog".to_string(),
3081
- "Remote URL (SSE)".to_string(),
3082
- "Local Command (stdio)".to_string(),
3361
+ ChoiceItem::with_description(
3362
+ "From catalog",
3363
+ "Install from known MCP tools catalog",
3364
+ "catalog",
3365
+ ),
3366
+ ChoiceItem::with_description(
3367
+ "Remote URL (SSE)",
3368
+ "Connect to remote HTTP/SSE MCP endpoint",
3369
+ "remote",
3370
+ ),
3371
+ ChoiceItem::with_description(
3372
+ "Local Command (stdio)",
3373
+ "Run local command (e.g. npx, uvx)",
3374
+ "local",
3375
+ ),
3083
3376
  ];
3084
- match prompt_interactive_select("Add MCP server", &choices, &choices[0]) {
3085
- Ok(Some(c)) if c == "From catalog" => mcp_add_from_catalog(session),
3086
- Ok(Some(c)) if c == "Remote URL (SSE)" => mcp_add_remote_flow(session),
3087
- Ok(Some(c)) if c == "Local Command (stdio)" => mcp_add_custom_flow(session),
3377
+ match ui.prompt_choice("Add MCP server", "Select server type", &choices) {
3378
+ Ok(Some(0)) => mcp_add_from_catalog(session, ui),
3379
+ Ok(Some(1)) => mcp_add_remote_flow(session, ui),
3380
+ Ok(Some(2)) => mcp_add_custom_flow(session, ui),
3088
3381
  _ => {}
3089
3382
  }
3090
3383
  }
3091
3384
 
3092
- fn mcp_add_from_catalog(session: &mut InteractiveSession) {
3385
+ fn mcp_add_from_catalog(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3093
3386
  let entries = crate::mcp::registry();
3094
- let labels: Vec<String> = entries
3387
+ let choices: Vec<ChoiceItem> = entries
3095
3388
  .iter()
3096
3389
  .map(|e| {
3097
3390
  let needs = if e.required_env.is_empty() {
3098
3391
  String::new()
3099
3392
  } else {
3100
- format!(" {DIM}(needs a key){RESET}")
3393
+ " (needs a key)".to_string()
3101
3394
  };
3102
- format!("{:<20} {}{needs}", e.name, e.desc)
3395
+ ChoiceItem::with_description(&e.name, format!("{}{needs}", e.desc), &e.key)
3103
3396
  })
3104
3397
  .collect();
3105
- let Ok(Some(picked)) = prompt_interactive_select("MCP server catalog", &labels, &labels[0])
3106
- else {
3107
- return;
3108
- };
3109
- let Some(entry) = labels
3110
- .iter()
3111
- .position(|l| *l == picked)
3112
- .map(|i| &entries[i])
3113
- else {
3398
+ let Ok(Some(idx)) = ui.prompt_choice(
3399
+ "MCP server catalog",
3400
+ "Select an MCP server from catalog",
3401
+ &choices,
3402
+ ) else {
3114
3403
  return;
3115
3404
  };
3405
+ let entry = &entries[idx];
3116
3406
 
3117
3407
  // One prompt per declared argInput (path / connection string / …).
3118
3408
  let mut extra_args = Vec::new();
@@ -3122,10 +3412,14 @@ fn mcp_add_from_catalog(session: &mut InteractiveSession) {
3122
3412
  } else {
3123
3413
  format!("{} (e.g. {})", input.label, input.placeholder)
3124
3414
  };
3125
- let value = crate::onboard::prompt_input(&hint, None).unwrap_or_default();
3415
+ let value = ui
3416
+ .prompt_text(&entry.name, &hint, None)
3417
+ .ok()
3418
+ .flatten()
3419
+ .unwrap_or_default();
3126
3420
  let value = value.trim();
3127
3421
  if value.is_empty() {
3128
- println!("{WARN}Cancelled ('{}' is required).{RESET}\n", input.label);
3422
+ ui.push_notice(format!("Cancelled ('{}' is required).", input.label));
3129
3423
  return;
3130
3424
  }
3131
3425
  extra_args.push(value.to_string());
@@ -3134,65 +3428,96 @@ fn mcp_add_from_catalog(session: &mut InteractiveSession) {
3134
3428
  // One prompt per required env var.
3135
3429
  let mut env = Vec::new();
3136
3430
  for var in &entry.required_env {
3137
- if let Some(help) = &var.help {
3138
- println!("{DIM} {}: get one at {help}{RESET}", var.label);
3139
- }
3140
- let value = crate::onboard::prompt_input(&var.label, None).unwrap_or_default();
3431
+ let prompt_body = if let Some(help) = &var.help {
3432
+ format!("{} (get one at {help})", var.label)
3433
+ } else {
3434
+ var.label.clone()
3435
+ };
3436
+ let value = ui
3437
+ .prompt_text(&entry.name, &prompt_body, None)
3438
+ .ok()
3439
+ .flatten()
3440
+ .unwrap_or_default();
3141
3441
  let value = value.trim();
3142
3442
  if value.is_empty() {
3143
- println!("{WARN}Cancelled ('{}' is required).{RESET}\n", var.label);
3443
+ ui.push_notice(format!("Cancelled ('{}' is required).", var.label));
3144
3444
  return;
3145
3445
  }
3146
3446
  env.push(format!("{}={value}", var.key));
3147
3447
  }
3148
3448
 
3149
- let name = crate::onboard::prompt_input("Server name", Some(&entry.key)).unwrap_or_default();
3449
+ let name = ui
3450
+ .prompt_text(
3451
+ "Server name",
3452
+ "Choose a unique name for this MCP server",
3453
+ Some(&entry.key),
3454
+ )
3455
+ .ok()
3456
+ .flatten()
3457
+ .unwrap_or_default();
3150
3458
  let name = if name.trim().is_empty() {
3151
3459
  entry.key.clone()
3152
3460
  } else {
3153
3461
  name.trim().to_string()
3154
3462
  };
3155
3463
 
3156
- let allow_all = confirm(
3157
- "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3158
- )
3159
- .unwrap_or(false);
3464
+ let allow_all = ui
3465
+ .prompt_confirm(
3466
+ "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3467
+ false,
3468
+ )
3469
+ .unwrap_or(false);
3160
3470
 
3161
3471
  match crate::mcp::registry_add(&entry.key, Some(&name), extra_args, env, allow_all) {
3162
3472
  Ok(saved) => {
3163
- println!(
3164
- "{MINT}Added MCP server '{saved}'{}.{RESET}\n",
3473
+ ui.push_notice(format!(
3474
+ "Added MCP server '{saved}'{}.",
3165
3475
  if allow_all {
3166
3476
  " (all tools allowed)"
3167
3477
  } else {
3168
3478
  ""
3169
3479
  }
3170
- );
3480
+ ));
3171
3481
  reload_session_config(session);
3172
3482
  }
3173
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3483
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3174
3484
  }
3175
3485
  }
3176
3486
 
3177
- fn mcp_add_remote_flow(session: &mut InteractiveSession) {
3178
- let name = crate::onboard::prompt_input("Server name", None).unwrap_or_default();
3487
+ fn mcp_add_remote_flow(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3488
+ let name = ui
3489
+ .prompt_text("Remote MCP Server", "Server name", None)
3490
+ .ok()
3491
+ .flatten()
3492
+ .unwrap_or_default();
3179
3493
  let name = name.trim().to_string();
3180
3494
  if name.is_empty() {
3181
- println!("{WARN}Cancelled (no name).{RESET}\n");
3495
+ ui.push_notice("Cancelled (no name).");
3182
3496
  return;
3183
3497
  }
3184
- let url = crate::onboard::prompt_input("Server URL (e.g. https://example.com/sse)", None)
3498
+ let url = ui
3499
+ .prompt_text(
3500
+ "Remote MCP Server",
3501
+ "Server URL (e.g. https://example.com/sse)",
3502
+ None,
3503
+ )
3504
+ .ok()
3505
+ .flatten()
3185
3506
  .unwrap_or_default();
3186
3507
  let url = url.trim().to_string();
3187
3508
  if url.is_empty() {
3188
- println!("{WARN}Cancelled (no URL).{RESET}\n");
3509
+ ui.push_notice("Cancelled (no URL).");
3189
3510
  return;
3190
3511
  }
3191
- let auth = crate::onboard::prompt_input(
3192
- "Auth header or bearer token (blank for none, e.g. Bearer token)",
3193
- None,
3194
- )
3195
- .unwrap_or_default();
3512
+ let auth = ui
3513
+ .prompt_text(
3514
+ "Remote MCP Server",
3515
+ "Auth header or bearer token (blank for none, e.g. Bearer token)",
3516
+ None,
3517
+ )
3518
+ .ok()
3519
+ .flatten()
3520
+ .unwrap_or_default();
3196
3521
  let headers = if auth.trim().is_empty() {
3197
3522
  None
3198
3523
  } else {
@@ -3212,46 +3537,65 @@ fn mcp_add_remote_flow(session: &mut InteractiveSession) {
3212
3537
 
3213
3538
  match crate::mcp::add_remote(&name, &url, headers) {
3214
3539
  Ok(()) => {
3215
- let allow_all = confirm(
3216
- "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3217
- )
3218
- .unwrap_or(false);
3540
+ let allow_all = ui
3541
+ .prompt_confirm(
3542
+ "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3543
+ false,
3544
+ )
3545
+ .unwrap_or(false);
3219
3546
  if allow_all {
3220
3547
  let _ = crate::mcp::allow(&name, "*");
3221
3548
  }
3222
- println!(
3223
- "{MINT}Added Remote MCP server '{name}'{}.{RESET}\n",
3549
+ ui.push_notice(format!(
3550
+ "Added Remote MCP server '{name}'{}.",
3224
3551
  if allow_all {
3225
3552
  " (all tools allowed)"
3226
3553
  } else {
3227
3554
  ""
3228
3555
  }
3229
- );
3556
+ ));
3230
3557
  reload_session_config(session);
3231
3558
  }
3232
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3559
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3233
3560
  }
3234
3561
  }
3235
3562
 
3236
- fn mcp_add_custom_flow(session: &mut InteractiveSession) {
3237
- let name = crate::onboard::prompt_input("Server name", None).unwrap_or_default();
3563
+ fn mcp_add_custom_flow(session: &mut InteractiveSession, ui: &mut dyn CommandUi) {
3564
+ let name = ui
3565
+ .prompt_text("Custom MCP Server", "Server name", None)
3566
+ .ok()
3567
+ .flatten()
3568
+ .unwrap_or_default();
3238
3569
  let name = name.trim().to_string();
3239
3570
  if name.is_empty() {
3240
- println!("{WARN}Cancelled (no name).{RESET}\n");
3571
+ ui.push_notice("Cancelled (no name).");
3241
3572
  return;
3242
3573
  }
3243
- let command = crate::onboard::prompt_input("Command or URL (e.g. npx or https://...)", None)
3574
+ let command = ui
3575
+ .prompt_text(
3576
+ "Custom MCP Server",
3577
+ "Command or URL (e.g. npx or https://...)",
3578
+ None,
3579
+ )
3580
+ .ok()
3581
+ .flatten()
3244
3582
  .unwrap_or_default();
3245
3583
  let command = command.trim().to_string();
3246
3584
  if command.is_empty() {
3247
- println!("{WARN}Cancelled (no command).{RESET}\n");
3585
+ ui.push_notice("Cancelled (no command).");
3248
3586
  return;
3249
3587
  }
3250
3588
 
3251
3589
  if command.starts_with("http://") || command.starts_with("https://") {
3252
- let auth =
3253
- crate::onboard::prompt_input("Auth header or bearer token (blank for none)", None)
3254
- .unwrap_or_default();
3590
+ let auth = ui
3591
+ .prompt_text(
3592
+ "Remote MCP Server",
3593
+ "Auth header or bearer token (blank for none)",
3594
+ None,
3595
+ )
3596
+ .ok()
3597
+ .flatten()
3598
+ .unwrap_or_default();
3255
3599
  let headers = if auth.trim().is_empty() {
3256
3600
  None
3257
3601
  } else {
@@ -3270,35 +3614,48 @@ fn mcp_add_custom_flow(session: &mut InteractiveSession) {
3270
3614
  };
3271
3615
  match crate::mcp::add_remote(&name, &command, headers) {
3272
3616
  Ok(()) => {
3273
- let allow_all = confirm(
3274
- "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3275
- )
3276
- .unwrap_or(false);
3617
+ let allow_all = ui
3618
+ .prompt_confirm(
3619
+ "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3620
+ false,
3621
+ )
3622
+ .unwrap_or(false);
3277
3623
  if allow_all {
3278
3624
  let _ = crate::mcp::allow(&name, "*");
3279
3625
  }
3280
- println!(
3281
- "{MINT}Added Remote MCP server '{name}'{}.{RESET}\n",
3626
+ ui.push_notice(format!(
3627
+ "Added Remote MCP server '{name}'{}.",
3282
3628
  if allow_all {
3283
3629
  " (all tools allowed)"
3284
3630
  } else {
3285
3631
  ""
3286
3632
  }
3287
- );
3633
+ ));
3288
3634
  reload_session_config(session);
3289
3635
  }
3290
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3636
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3291
3637
  }
3292
3638
  return;
3293
3639
  }
3294
3640
 
3295
- let args = crate::onboard::prompt_input("Arguments (space-separated, blank for none)", None)
3641
+ let args = ui
3642
+ .prompt_text(
3643
+ "Custom MCP Server",
3644
+ "Arguments (space-separated, blank for none)",
3645
+ None,
3646
+ )
3647
+ .ok()
3648
+ .flatten()
3649
+ .unwrap_or_default();
3650
+ let env_raw = ui
3651
+ .prompt_text(
3652
+ "Custom MCP Server",
3653
+ "Env vars (KEY=VALUE, comma-separated, blank for none)",
3654
+ None,
3655
+ )
3656
+ .ok()
3657
+ .flatten()
3296
3658
  .unwrap_or_default();
3297
- let env_raw = crate::onboard::prompt_input(
3298
- "Env vars (KEY=VALUE, comma-separated, blank for none)",
3299
- None,
3300
- )
3301
- .unwrap_or_default();
3302
3659
 
3303
3660
  let args: Vec<String> = args.split_whitespace().map(str::to_string).collect();
3304
3661
  let env: Vec<String> = env_raw
@@ -3310,79 +3667,115 @@ fn mcp_add_custom_flow(session: &mut InteractiveSession) {
3310
3667
 
3311
3668
  match crate::mcp::add(&name, &command, args, env) {
3312
3669
  Ok(()) => {
3313
- let allow_all = confirm(
3314
- "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3315
- )
3316
- .unwrap_or(false);
3670
+ let allow_all = ui
3671
+ .prompt_confirm(
3672
+ "Allow the agent to call all of this server's tools now? (else approve them one by one)",
3673
+ false,
3674
+ )
3675
+ .unwrap_or(false);
3317
3676
  if allow_all {
3318
3677
  let _ = crate::mcp::allow(&name, "*");
3319
3678
  }
3320
- println!(
3321
- "{MINT}Added MCP server '{name}'{}.{RESET}\n",
3679
+ ui.push_notice(format!(
3680
+ "Added MCP server '{name}'{}.",
3322
3681
  if allow_all {
3323
3682
  " (all tools allowed)"
3324
3683
  } else {
3325
3684
  ""
3326
3685
  }
3327
- );
3686
+ ));
3328
3687
  reload_session_config(session);
3329
3688
  }
3330
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3689
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3331
3690
  }
3332
3691
  }
3333
3692
 
3334
- fn mcp_server_actions(session: &mut InteractiveSession, name: &str, disabled: bool) {
3693
+ fn mcp_server_actions(
3694
+ session: &mut InteractiveSession,
3695
+ ui: &mut dyn CommandUi,
3696
+ name: &str,
3697
+ disabled: bool,
3698
+ ) {
3335
3699
  let toggle_label = if disabled {
3336
3700
  "Enable server"
3337
3701
  } else {
3338
3702
  "Disable server"
3339
3703
  };
3340
3704
  let actions = vec![
3341
- "Check connection".to_string(),
3342
- "Allow all tools (*)".to_string(),
3343
- "Allow a specific tool".to_string(),
3344
- "Disallow a tool".to_string(),
3345
- "Re-authenticate (OAuth)".to_string(),
3346
- toggle_label.to_string(),
3347
- "Edit (command / args / icon)".to_string(),
3348
- "Remove server".to_string(),
3349
- "Back".to_string(),
3705
+ ChoiceItem::with_description("Check connection", "Test tool discovery", "check"),
3706
+ ChoiceItem::with_description(
3707
+ "Allow all tools (*)",
3708
+ "Grant unrestricted tool call access",
3709
+ "allow_all",
3710
+ ),
3711
+ ChoiceItem::with_description(
3712
+ "Allow a specific tool",
3713
+ "Whitelist an individual tool name",
3714
+ "allow_tool",
3715
+ ),
3716
+ ChoiceItem::with_description(
3717
+ "Disallow a tool",
3718
+ "Remove a tool from allowlist",
3719
+ "disallow_tool",
3720
+ ),
3721
+ ChoiceItem::with_description(
3722
+ "Re-authenticate (OAuth)",
3723
+ "Trigger browser OAuth login flow",
3724
+ "reauth",
3725
+ ),
3726
+ ChoiceItem::with_description(toggle_label, "Toggle server active status", "toggle"),
3727
+ ChoiceItem::with_description(
3728
+ "Edit (command / args / icon)",
3729
+ "Update server settings",
3730
+ "edit",
3731
+ ),
3732
+ ChoiceItem::with_description("Remove server", "Delete server from config", "remove"),
3733
+ ChoiceItem::with_description("Back", "Return to MCP servers list", "back"),
3350
3734
  ];
3351
- let Ok(Some(choice)) =
3352
- prompt_interactive_select(&format!("MCP: {name}"), &actions, &actions[0])
3353
- else {
3735
+ let Ok(Some(idx)) = ui.prompt_choice(&format!("MCP: {name}"), "Choose action", &actions) else {
3354
3736
  return;
3355
3737
  };
3356
3738
 
3357
- match choice.as_str() {
3358
- "Check connection" => match mint_core::list_server_tools(&session.config, name) {
3359
- Ok(_) => println!("{MINT}● Connected{RESET}\n"),
3360
- Err(e) => println!("{ERROR}● Not connected:{RESET} {e}\n"),
3739
+ match actions[idx].value.as_str() {
3740
+ "check" => match mint_core::list_server_tools(&session.config, name) {
3741
+ Ok(_) => ui.push_notice("● Connected"),
3742
+ Err(e) => ui.push_notice(format!("● Not connected: {e}")),
3361
3743
  },
3362
- "Allow all tools (*)" => match crate::mcp::allow(name, "*") {
3744
+ "allow_all" => match crate::mcp::allow(name, "*") {
3363
3745
  Ok(_) => {
3364
- println!("{MINT}Allowed all tools for '{name}'.{RESET}\n");
3746
+ ui.push_notice(format!("Allowed all tools for '{name}'."));
3365
3747
  reload_session_config(session);
3366
3748
  }
3367
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3749
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3368
3750
  },
3369
- "Allow a specific tool" => {
3370
- let tool = crate::onboard::prompt_input("Tool name", None).unwrap_or_default();
3751
+ "allow_tool" => {
3752
+ let tool = ui
3753
+ .prompt_text(&format!("MCP: {name}"), "Tool name", None)
3754
+ .ok()
3755
+ .flatten()
3756
+ .unwrap_or_default();
3371
3757
  let tool = tool.trim();
3372
3758
  if tool.is_empty() {
3373
3759
  return;
3374
3760
  }
3375
3761
  match crate::mcp::allow(name, tool) {
3376
3762
  Ok(true) => {
3377
- println!("{MINT}Allowed {name}/{tool}.{RESET}\n");
3763
+ ui.push_notice(format!("Allowed {name}/{tool}."));
3378
3764
  reload_session_config(session);
3379
3765
  }
3380
- Ok(false) => println!("{DIM}{name}/{tool} was already allowed.{RESET}\n"),
3381
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3766
+ Ok(false) => ui.push_notice(format!("{name}/{tool} was already allowed.")),
3767
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3382
3768
  }
3383
3769
  }
3384
- "Disallow a tool" => {
3385
- let tool = crate::onboard::prompt_input("Tool to remove (* to clear all)", None)
3770
+ "disallow_tool" => {
3771
+ let tool = ui
3772
+ .prompt_text(
3773
+ &format!("MCP: {name}"),
3774
+ "Tool to remove (* to clear all)",
3775
+ None,
3776
+ )
3777
+ .ok()
3778
+ .flatten()
3386
3779
  .unwrap_or_default();
3387
3780
  let tool = tool.trim();
3388
3781
  if tool.is_empty() {
@@ -3390,42 +3783,45 @@ fn mcp_server_actions(session: &mut InteractiveSession, name: &str, disabled: bo
3390
3783
  }
3391
3784
  match crate::mcp::disallow(name, tool) {
3392
3785
  Ok(true) => {
3393
- println!("{MINT}Removed {name}/{tool} from the allowlist.{RESET}\n");
3786
+ ui.push_notice(format!("Removed {name}/{tool} from the allowlist."));
3394
3787
  reload_session_config(session);
3395
3788
  }
3396
- Ok(false) => println!("{DIM}{name}/{tool} was not in the allowlist.{RESET}\n"),
3397
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3789
+ Ok(false) => ui.push_notice(format!("{name}/{tool} was not in the allowlist.")),
3790
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3398
3791
  }
3399
3792
  }
3400
- "Re-authenticate (OAuth)" => {
3401
- println!("{DIM}Re-authenticating '{name}'... (a browser tab may open){RESET}\n");
3793
+ "reauth" => {
3794
+ ui.push_notice(format!(
3795
+ "Re-authenticating '{name}'... (a browser tab may open)"
3796
+ ));
3402
3797
  match crate::mcp::reauth(name) {
3403
- Ok(true) => println!("{MINT}Re-authentication succeeded.{RESET}\n"),
3404
- Ok(false) => {
3405
- println!("{ERROR}Re-authentication failed (see output above).{RESET}\n")
3406
- }
3407
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3798
+ Ok(true) => ui.push_notice("Re-authentication succeeded."),
3799
+ Ok(false) => ui.push_notice("Re-authentication failed (see output above)."),
3800
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3408
3801
  }
3409
3802
  }
3410
- other if other == toggle_label => match crate::mcp::set_disabled(name, !disabled) {
3803
+ "toggle" => match crate::mcp::set_disabled(name, !disabled) {
3411
3804
  Ok(_) => {
3412
- println!(
3413
- "{MINT}{} '{name}'.{RESET}\n",
3805
+ ui.push_notice(format!(
3806
+ "{} '{name}'.",
3414
3807
  if disabled { "Enabled" } else { "Disabled" }
3415
- );
3808
+ ));
3416
3809
  reload_session_config(session);
3417
3810
  }
3418
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3811
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3419
3812
  },
3420
- "Edit (command / args / icon)" => mcp_edit_flow(session, name),
3421
- "Remove server" => {
3422
- if confirm(&format!("Remove MCP server '{name}'?")).unwrap_or(false) {
3813
+ "edit" => mcp_edit_flow(session, ui, name),
3814
+ "remove" => {
3815
+ if ui
3816
+ .prompt_confirm(&format!("Remove MCP server '{name}'?"), false)
3817
+ .unwrap_or(false)
3818
+ {
3423
3819
  match crate::mcp::remove(name) {
3424
3820
  Ok(_) => {
3425
- println!("{MINT}Removed '{name}'.{RESET}\n");
3821
+ ui.push_notice(format!("Removed '{name}'."));
3426
3822
  reload_session_config(session);
3427
3823
  }
3428
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3824
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3429
3825
  }
3430
3826
  }
3431
3827
  }
@@ -3433,25 +3829,28 @@ fn mcp_server_actions(session: &mut InteractiveSession, name: &str, disabled: bo
3433
3829
  }
3434
3830
  }
3435
3831
 
3436
- fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3832
+ fn mcp_edit_flow(session: &mut InteractiveSession, ui: &mut dyn CommandUi, name: &str) {
3437
3833
  let Some(srv) = crate::mcp::list().ok().and_then(|m| m.get(name).cloned()) else {
3438
- println!("{WARN}No MCP server named '{name}'.{RESET}\n");
3834
+ ui.push_notice(format!("No MCP server named '{name}'."));
3439
3835
  return;
3440
3836
  };
3441
3837
  let fields = vec![
3442
- "command".to_string(),
3443
- "args".to_string(),
3444
- "icon".to_string(),
3445
- "Back".to_string(),
3838
+ ChoiceItem::with_description("command", &srv.command, "command"),
3839
+ ChoiceItem::with_description("args", srv.args.join(" "), "args"),
3840
+ ChoiceItem::with_description("icon", srv.icon.as_deref().unwrap_or("(none)"), "icon"),
3841
+ ChoiceItem::with_description("Back", "Cancel edit", "back"),
3446
3842
  ];
3447
- let Ok(Some(field)) = prompt_interactive_select(&format!("Edit {name}"), &fields, &fields[0])
3843
+ let Ok(Some(idx)) = ui.prompt_choice(&format!("Edit {name}"), "Select field to edit", &fields)
3448
3844
  else {
3449
3845
  return;
3450
3846
  };
3451
- let result = match field.as_str() {
3847
+ let result = match fields[idx].value.as_str() {
3452
3848
  "command" => {
3453
- let value =
3454
- crate::onboard::prompt_input("Command", Some(&srv.command)).unwrap_or_default();
3849
+ let value = ui
3850
+ .prompt_text(&format!("Edit {name}"), "Command", Some(&srv.command))
3851
+ .ok()
3852
+ .flatten()
3853
+ .unwrap_or_default();
3455
3854
  let value = value.trim();
3456
3855
  if value.is_empty() {
3457
3856
  return;
@@ -3460,7 +3859,14 @@ fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3460
3859
  }
3461
3860
  "args" => {
3462
3861
  let current = srv.args.join(" ");
3463
- let value = crate::onboard::prompt_input("Arguments (space-separated)", Some(&current))
3862
+ let value = ui
3863
+ .prompt_text(
3864
+ &format!("Edit {name}"),
3865
+ "Arguments (space-separated)",
3866
+ Some(&current),
3867
+ )
3868
+ .ok()
3869
+ .flatten()
3464
3870
  .unwrap_or_default();
3465
3871
  crate::mcp::edit(
3466
3872
  name,
@@ -3472,7 +3878,14 @@ fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3472
3878
  }
3473
3879
  "icon" => {
3474
3880
  let current = srv.icon.clone().unwrap_or_default();
3475
- let value = crate::onboard::prompt_input("Icon (blank to clear)", Some(&current))
3881
+ let value = ui
3882
+ .prompt_text(
3883
+ &format!("Edit {name}"),
3884
+ "Icon (blank to clear)",
3885
+ Some(&current),
3886
+ )
3887
+ .ok()
3888
+ .flatten()
3476
3889
  .unwrap_or_default();
3477
3890
  let value = value.trim();
3478
3891
  crate::mcp::edit(
@@ -3487,10 +3900,10 @@ fn mcp_edit_flow(session: &mut InteractiveSession, name: &str) {
3487
3900
  };
3488
3901
  match result {
3489
3902
  Ok(_) => {
3490
- println!("{MINT}Updated '{name}'.{RESET}\n");
3903
+ ui.push_notice(format!("Updated '{name}'."));
3491
3904
  reload_session_config(session);
3492
3905
  }
3493
- Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
3906
+ Err(e) => ui.push_notice(format!("MCP error: {e}")),
3494
3907
  }
3495
3908
  }
3496
3909
 
@@ -3500,7 +3913,8 @@ mod tests {
3500
3913
 
3501
3914
  /// Commands that are dispatched but deliberately left undocumented —
3502
3915
  /// shortcuts for another command's own token, not gaps in `SLASH_COMMANDS`.
3503
- const UNDOCUMENTED_ALIASES: &[&str] = &["/quit", "/reset", "/plugin", "/searchProvider"];
3916
+ const UNDOCUMENTED_ALIASES: &[&str] =
3917
+ &["/quit", "/reset", "/plugin", "/searchProvider", "/think"];
3504
3918
 
3505
3919
  /// Regression guard for the exact bug that motivated `SLASH_COMMANDS`:
3506
3920
  /// `/edit-image`, `/gen-image`, `/shells`, and `/subagent` all worked but
@@ -3555,6 +3969,7 @@ mod tests {
3555
3969
  #[tokio::test]
3556
3970
  async fn core_slash_clear_clears_history() {
3557
3971
  let mut session = InteractiveSession {
3972
+ chat_id: mint_core::CHAT_CLI_ID.to_string(),
3558
3973
  config: MintConfig::default(),
3559
3974
  current_dir: std::env::current_dir().unwrap(),
3560
3975
  fast_mode: false,
@@ -3564,7 +3979,8 @@ mod tests {
3564
3979
  jobs: BackgroundJobs::new(),
3565
3980
  };
3566
3981
 
3567
- let res = handle_slash_command(&mut session, "/clear").await;
3982
+ let mut ui = MockCommandUi::new();
3983
+ let res = handle_slash_command(&mut session, &mut ui, "/clear").await;
3568
3984
  assert!(matches!(res, Some(SlashResult::Handled)));
3569
3985
  assert!(session.history.is_empty());
3570
3986
  }
@@ -3572,6 +3988,7 @@ mod tests {
3572
3988
  #[tokio::test]
3573
3989
  async fn core_slash_fast_mode_toggle() {
3574
3990
  let mut session = InteractiveSession {
3991
+ chat_id: mint_core::CHAT_CLI_ID.to_string(),
3575
3992
  config: MintConfig::default(),
3576
3993
  current_dir: std::env::current_dir().unwrap(),
3577
3994
  fast_mode: false,
@@ -3581,7 +3998,8 @@ mod tests {
3581
3998
  jobs: BackgroundJobs::new(),
3582
3999
  };
3583
4000
 
3584
- let res = handle_slash_command(&mut session, "/fast on").await;
4001
+ let mut ui = MockCommandUi::new();
4002
+ let res = handle_slash_command(&mut session, &mut ui, "/fast on").await;
3585
4003
  assert!(matches!(res, Some(SlashResult::Handled)));
3586
4004
  assert!(session.fast_mode);
3587
4005
  }