@pheem49/mint 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (421) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +2 -2
  3. package/README.md +8 -2
  4. package/Release_Note.md +222 -71
  5. package/crates/mint-cli/src/agent/diff_render.rs +66 -44
  6. package/crates/mint-cli/src/agent/live_status.rs +21 -4
  7. package/crates/mint-cli/src/agent/markdown_render.rs +342 -5
  8. package/crates/mint-cli/src/agent.rs +25 -10
  9. package/crates/mint-cli/src/commands/agent.rs +4 -92
  10. package/crates/mint-cli/src/commands/browser.rs +2 -15
  11. package/crates/mint-cli/src/commands/integrations.rs +3 -3
  12. package/crates/mint-cli/src/commands/mod.rs +0 -3
  13. package/crates/mint-cli/src/commands/tailnet_web.rs +1 -0
  14. package/crates/mint-cli/src/interactive/chat_tui/dialog.rs +572 -0
  15. package/crates/mint-cli/src/interactive/chat_tui/mod.rs +1427 -0
  16. package/crates/mint-cli/src/interactive/chat_tui/render.rs +1524 -0
  17. package/crates/mint-cli/src/interactive/chat_tui/selection.rs +401 -0
  18. package/crates/mint-cli/src/interactive/chat_tui/state.rs +1833 -0
  19. package/crates/mint-cli/src/interactive/chat_tui/tests.rs +3414 -0
  20. package/crates/mint-cli/src/interactive/chat_tui/thought.rs +202 -0
  21. package/crates/mint-cli/src/interactive/chat_tui/transcript.rs +728 -0
  22. package/crates/mint-cli/src/interactive/chat_tui/watermark.rs +148 -0
  23. package/crates/mint-cli/src/interactive/command_ui.rs +53 -5
  24. package/crates/mint-cli/src/interactive/composer.rs +186 -0
  25. package/crates/mint-cli/src/interactive/format.rs +78 -3
  26. package/crates/mint-cli/src/interactive/input_box.rs +61 -1
  27. package/crates/mint-cli/src/interactive/resume_picker.rs +367 -25
  28. package/crates/mint-cli/src/interactive/slash_commands.rs +648 -371
  29. package/crates/mint-cli/src/interactive/tool_activity.rs +93 -20
  30. package/crates/mint-cli/src/interactive.rs +37 -0
  31. package/crates/mint-cli/src/main.rs +6 -3
  32. package/crates/mint-cli/src/markdown.rs +2 -37
  33. package/crates/mint-cli/src/onboard.rs +700 -282
  34. package/crates/mint-cli/src/setup.rs +1342 -624
  35. package/crates/mint-cli/src/terminal_theme.rs +159 -35
  36. package/crates/mint-cli/tests/library_intake.rs +1 -0
  37. package/crates/mint-core/src/agent/chat/anthropic.rs +318 -0
  38. package/crates/mint-core/src/agent/chat/custom.rs +149 -0
  39. package/crates/mint-core/src/agent/chat/gemini.rs +518 -0
  40. package/crates/mint-core/src/agent/chat/mod.rs +857 -0
  41. package/crates/mint-core/src/agent/chat/ollama.rs +321 -0
  42. package/crates/mint-core/src/agent/chat/openai.rs +582 -0
  43. package/crates/mint-core/src/agent/chat/streaming.rs +469 -0
  44. package/crates/mint-core/src/agent/chat/tests.rs +1479 -0
  45. package/crates/mint-core/src/agent/memory.rs +56 -2
  46. package/crates/mint-core/src/agent/tasks.rs +2 -2
  47. package/crates/mint-core/src/api_server/routes/chat.rs +24 -2
  48. package/crates/mint-core/src/api_server/routes/gemini_live.rs +14 -1
  49. package/crates/mint-core/src/api_server/routes/html_preview.rs +1 -8
  50. package/crates/mint-core/src/api_server/routes/sessions.rs +21 -0
  51. package/crates/mint-core/src/avatar_bridge.rs +0 -22
  52. package/crates/mint-core/src/browser/cdp.rs +10 -4
  53. package/crates/mint-core/src/browser/headless.rs +582 -0
  54. package/crates/mint-core/src/browser/input.rs +7 -62
  55. package/crates/mint-core/src/browser/interact.rs +0 -14
  56. package/crates/mint-core/src/browser/mod.rs +8 -29
  57. package/crates/mint-core/src/browser/navigate.rs +2 -36
  58. package/crates/mint-core/src/browser/runtime.rs +2 -4
  59. package/crates/mint-core/src/browser/session.rs +50 -466
  60. package/crates/mint-core/src/browser/workflow.rs +142 -32
  61. package/crates/mint-core/src/cron/store.rs +2 -2
  62. package/crates/mint-core/src/integrations/mcp.rs +6 -8
  63. package/crates/mint-core/src/lib.rs +0 -5
  64. package/crates/mint-core/src/media/gemini_live.rs +636 -86
  65. package/crates/mint-core/src/media/library/migration.rs +26 -0
  66. package/crates/mint-core/src/media/library/tests.rs +1 -1
  67. package/crates/mint-core/src/media/library/thumbnails.rs +8 -2
  68. package/crates/mint-core/src/media/pictures.rs +1 -1
  69. package/crates/mint-core/src/media/tool_images.rs +92 -1
  70. package/crates/mint-core/src/mods/manager.rs +101 -35
  71. package/crates/mint-core/src/orchestration/context_render.rs +441 -24
  72. package/crates/mint-core/src/orchestration/decision_parsing.rs +531 -16
  73. package/crates/mint-core/src/orchestration/full_skill_result_tests.rs +12 -0
  74. package/crates/mint-core/src/orchestration/loop_guard.rs +145 -0
  75. package/crates/mint-core/src/orchestration/mcp_outcome_tests.rs +864 -0
  76. package/crates/mint-core/src/orchestration/memory_skill.rs +88 -28
  77. package/crates/mint-core/src/orchestration/mod.rs +320 -4249
  78. package/crates/mint-core/src/orchestration/reliability.rs +67 -27
  79. package/crates/mint-core/src/orchestration/review_regressions.rs +5 -0
  80. package/crates/mint-core/src/orchestration/tests.rs +1665 -0
  81. package/crates/mint-core/src/orchestration/tools/dispatch.rs +967 -0
  82. package/crates/mint-core/src/orchestration/tools/files.rs +1 -0
  83. package/crates/mint-core/src/orchestration/tools/mod.rs +5 -1
  84. package/crates/mint-core/src/orchestration/tools/planning.rs +102 -42
  85. package/crates/mint-core/src/orchestration/tools/shell.rs +129 -58
  86. package/crates/mint-core/src/orchestration/truncation.rs +170 -13
  87. package/crates/mint-core/src/orchestration/types.rs +648 -0
  88. package/crates/mint-core/src/orchestration/verification.rs +0 -3
  89. package/crates/mint-core/src/orchestration/workspace_helpers.rs +24 -6
  90. package/crates/mint-core/src/prompts/agent.rs +52 -76
  91. package/crates/mint-core/src/prompts/tool_catalog.rs +8 -151
  92. package/crates/mint-core/src/search/files.rs +20 -0
  93. package/crates/mint-core/src/slash/mod.rs +228 -265
  94. package/crates/mint-core/src/slash/models.rs +6 -1
  95. package/crates/mint-core/src/system/bg_shell.rs +1 -0
  96. package/crates/mint-core/src/system/config.rs +176 -34
  97. package/crates/mint-mod-runtime/src/lib.rs +37 -23
  98. package/out/web/assets/ArtifactPreviewPanel-Cx6BVOOF.js +2 -0
  99. package/out/web/assets/CalculationCard-D2E74u5q.js +1 -0
  100. package/out/web/assets/CliSessionsView-ZbdtSjvK.css +1 -0
  101. package/out/web/assets/{CliSessionsView-MI8kXMAb.js → CliSessionsView-z5i6qGyb.js} +1 -1
  102. package/out/web/assets/CodeReviewPage-BK2bOScd.css +1 -0
  103. package/out/web/assets/CodeReviewPage-Dp6zHuHu.js +2 -0
  104. package/out/web/assets/{CommandPalette-BD8SlTE5.js → CommandPalette-DaKlzbKs.js} +1 -1
  105. package/out/web/assets/ImageGenCard-BMCJHP9O.js +1 -0
  106. package/out/web/assets/ImageSearchCard-Cld_SOUg.js +1 -0
  107. package/out/web/assets/{ImageStudioPanel-BP7N4rLr.js → ImageStudioPanel-B5N7TjeB.js} +1 -1
  108. package/out/web/assets/LibraryPage-CMB89rJ0.css +1 -0
  109. package/out/web/assets/LibraryPage-DbZEi4W_.js +2 -0
  110. package/out/web/assets/LinkedFoldersView-9B9DIPoZ.js +1 -0
  111. package/out/web/assets/{McpRegistryPicker-CoS83vtc.js → McpRegistryPicker-DnXPlzI7.js} +1 -1
  112. package/out/web/assets/McpServersView-CiNBNUl4.js +1 -0
  113. package/out/web/assets/MermaidCard-BBDNEXbY.js +2 -0
  114. package/out/web/assets/MintDashboard-C_UYPoNV.js +119 -0
  115. package/out/web/assets/MintDashboard-DTd8C_c8.css +1 -0
  116. package/out/web/assets/PlanCard-C2x5pajq.js +1 -0
  117. package/out/web/assets/{PluginsView-jBYmcKh4.js → PluginsView-DoTg9MVa.js} +1 -1
  118. package/out/web/assets/RewindModal-B5oowrR0.js +1 -0
  119. package/out/web/assets/{ScheduledTasksView-5uw9-V9H.js → ScheduledTasksView-BD5DVPkZ.js} +1 -1
  120. package/out/web/assets/{SelectField-DXBQtrUK.js → SelectField-Cy6CghrU.js} +1 -1
  121. package/out/web/assets/SettingsWindow-BYl_oCFh.js +4 -0
  122. package/out/web/assets/SettingsWindow-Ckcgnjt8.css +1 -0
  123. package/out/web/assets/{SkillsView-C-gp9J7m.js → SkillsView-CIeWwq5z.js} +1 -1
  124. package/out/web/assets/StockCard-DxUciKyp.js +1 -0
  125. package/out/web/assets/ToolSurfacePage-qBgs_VHq.css +1 -0
  126. package/out/web/assets/ToolSurfacePage-saVbVrkE.js +38 -0
  127. package/out/web/assets/{UiFeatureCard-6BwxTOCe.js → UiFeatureCard-CdY0hGWy.js} +1 -1
  128. package/out/web/assets/{UiGridCard-DvZcLNxb.js → UiGridCard-BBK3I5hC.js} +1 -1
  129. package/out/web/assets/UiMockupWidget-CpR9EDuc.js +1 -0
  130. package/out/web/assets/{VeoStudioPanel-DOj7_WR3.css → VeoStudioPanel-9kU9YLEN.css} +1 -1
  131. package/out/web/assets/{VeoStudioPanel-DfsOJKv0.js → VeoStudioPanel-Co6-wEWM.js} +2 -2
  132. package/out/web/assets/WeatherCard-Buzi7Vfu.js +1 -0
  133. package/out/web/assets/WorkspacePdfPreview-Bi8Zzyir.js +2 -0
  134. package/out/web/assets/{abnfDiagram-VCTEODGH-BOo3Ckng.js → abnfDiagram-VCTEODGH-CMfR95N2.js} +1 -1
  135. package/out/web/assets/{architecture-7GRP2DOG-ZOwheW3n.js → architecture-7GRP2DOG-DhAM4HyO.js} +1 -1
  136. package/out/web/assets/{architectureDiagram-5GKGNRK7-DC2FDzVd.js → architectureDiagram-5GKGNRK7-Yk3TUJ_F.js} +1 -1
  137. package/out/web/assets/{blockDiagram-I7D4REHJ-BrKFWdAH.js → blockDiagram-I7D4REHJ-X9fBgJik.js} +1 -1
  138. package/out/web/assets/{c4Diagram-7LVT6UL2-BHTHK1Yd.js → c4Diagram-7LVT6UL2-Bk9x7N-8.js} +1 -1
  139. package/out/web/assets/{chunk-4HAMMTFA-BYFkZwNj.js → chunk-4HAMMTFA-Dx5fk1_0.js} +1 -1
  140. package/out/web/assets/{chunk-75Z2AOVW-BSKfSKSA.js → chunk-75Z2AOVW-BszTQoM4.js} +1 -1
  141. package/out/web/assets/{chunk-DU6HZSFF-vIhsiW6n.js → chunk-DU6HZSFF-DK-As3rk.js} +1 -1
  142. package/out/web/assets/{chunk-F27PBJKO-BlYpW9xq.js → chunk-F27PBJKO-LmncZZIS.js} +1 -1
  143. package/out/web/assets/{chunk-GMAD6QVW-BIBz_Ug5.js → chunk-GMAD6QVW-DSQ_sDRJ.js} +1 -1
  144. package/out/web/assets/{chunk-GVQU2GXP-NPSHxeqC.js → chunk-GVQU2GXP-Cno3ulje.js} +1 -1
  145. package/out/web/assets/{chunk-IMKFNOWR-YlJK8_Fp.js → chunk-IMKFNOWR-bIzfY9Jc.js} +1 -1
  146. package/out/web/assets/{chunk-L3NEJ4N5-DMYi4-af.js → chunk-L3NEJ4N5-Dl6eOa0D.js} +1 -1
  147. package/out/web/assets/{chunk-OSK3NFVY-Begd3-jA.js → chunk-OSK3NFVY-BaUGJ-cM.js} +1 -1
  148. package/out/web/assets/{chunk-P2QGCYS3-D1I4xFHN.js → chunk-P2QGCYS3-_066ndhH.js} +1 -1
  149. package/out/web/assets/{chunk-POPQ4Y6H-CafZZzNE.js → chunk-POPQ4Y6H-YT1AKhTv.js} +1 -1
  150. package/out/web/assets/{chunk-PWAF6VOD-Dwh6_Lnt.js → chunk-PWAF6VOD-CrEgDsxh.js} +1 -1
  151. package/out/web/assets/{chunk-SHT3W25Y-B0XrCSEb.js → chunk-SHT3W25Y-_KwaRLOl.js} +1 -1
  152. package/out/web/assets/{chunk-SVP7TREG-CxLsddxC.js → chunk-SVP7TREG-BQ1qb32L.js} +1 -1
  153. package/out/web/assets/{chunk-TICWLB2K-y156QInb.js → chunk-TICWLB2K-BDyehCtg.js} +1 -1
  154. package/out/web/assets/classDiagram-ZZMXUADV-BoSpzRCJ.js +1 -0
  155. package/out/web/assets/classDiagram-v2-VYDZK3BY-BoSpzRCJ.js +1 -0
  156. package/out/web/assets/{config-DqXJXeZM.css → config-2suVFPZg.css} +1 -1
  157. package/out/web/assets/config-CkRCdkxn.js +2 -0
  158. package/out/web/assets/{cynefin-OW5HDTMX-SMVvrLf-.js → cynefin-OW5HDTMX-Dl_31aaN.js} +1 -1
  159. package/out/web/assets/{cynefinDiagram-5FMLGOSQ-rZznN24T.js → cynefinDiagram-5FMLGOSQ-Bpir8jha.js} +1 -1
  160. package/out/web/assets/{dagre-GXQ25YYZ-BOToydFu.js → dagre-GXQ25YYZ-D9xu3nsq.js} +1 -1
  161. package/out/web/assets/{diagram-S7CK7UJ4-TMVaGQEJ.js → diagram-S7CK7UJ4-DeveoEeB.js} +1 -1
  162. package/out/web/assets/{diagram-UQ7AKVKN-C2JK-GP2.js → diagram-UQ7AKVKN-p4xWRESV.js} +1 -1
  163. package/out/web/assets/{diagram-VSXAHHWV-Cw8VSi2K.js → diagram-VSXAHHWV-C7qz9NDF.js} +1 -1
  164. package/out/web/assets/{diagram-VX7I27RA-DXp4fKk9.js → diagram-VX7I27RA-DRiCa3Vd.js} +1 -1
  165. package/out/web/assets/{diagram-Z3DM3KII-CzSicYiW.js → diagram-Z3DM3KII-Bt7zduQX.js} +1 -1
  166. package/out/web/assets/{ebnfDiagram-PWID7BFC-Dqj-zqPu.js → ebnfDiagram-PWID7BFC-DV9WpwdR.js} +1 -1
  167. package/out/web/assets/{erDiagram-RLTQ6QDP-DZwFZAuD.js → erDiagram-RLTQ6QDP-BJ_nwG2m.js} +1 -1
  168. package/out/web/assets/{eventmodeling-NTZA5JFV-C0O-Lvgp.js → eventmodeling-NTZA5JFV-BTBMrfVa.js} +1 -1
  169. package/out/web/assets/flowDiagram-HODETNUW-Cdlq3cJL.js +1 -0
  170. package/out/web/assets/{ganttDiagram-EL5Y4UJY-Da4ULXTl.js → ganttDiagram-EL5Y4UJY-DQLdsXrC.js} +1 -1
  171. package/out/web/assets/{gitGraph-4MIJSDKK-B9wNFZIP.js → gitGraph-4MIJSDKK-CL1OEOQ_.js} +1 -1
  172. package/out/web/assets/{gitGraphDiagram-WWUBYQGX-RjrhB0MP.js → gitGraphDiagram-WWUBYQGX-8cSi42H7.js} +1 -1
  173. package/out/web/assets/index-BocxnrDG.js +168 -0
  174. package/out/web/assets/index-yeh_r55a.css +1 -0
  175. package/out/web/assets/{info-A6RAGUB7-B3dfCpeY.js → info-A6RAGUB7-C1zsX5sc.js} +1 -1
  176. package/out/web/assets/{infoDiagram-27XIBGKW-QVWkLVx7.js → infoDiagram-27XIBGKW-DsRYtIyI.js} +1 -1
  177. package/out/web/assets/{ishikawaDiagram-5VMMS53U-CLFNIRI9.js → ishikawaDiagram-5VMMS53U-B2crwIXj.js} +1 -1
  178. package/out/web/assets/{journeyDiagram-3NMN7TZE-fgOQadYF.js → journeyDiagram-3NMN7TZE-BTH-GyVR.js} +1 -1
  179. package/out/web/assets/{kanban-definition-UXKFOSKX-G3Q_9g9O.js → kanban-definition-UXKFOSKX-BeRfCnuA.js} +1 -1
  180. package/out/web/assets/{mermaid-parser.core-v3Jm7nco.js → mermaid-parser.core-BJwgCBBc.js} +3 -3
  181. package/out/web/assets/{mermaid.core-DTEjLUA1.js → mermaid.core-CsDDodN8.js} +4 -4
  182. package/out/web/assets/{mindmap-definition-YA3MSWOX-C_D42_fF.js → mindmap-definition-YA3MSWOX-B42efOQ1.js} +1 -1
  183. package/out/web/assets/{modelManager-BPLycQrN.js → modelManager-B3ZFMh8v.js} +2 -2
  184. package/out/web/assets/{oauthManager-BUK7ic7q.js → oauthManager-Ch5Du2fF.js} +1 -1
  185. package/out/web/assets/{packet-AYTQ26CC-B-R-SHlF.js → packet-AYTQ26CC-B6xUyE6r.js} +1 -1
  186. package/out/web/assets/{pegDiagram-XKGWAZYB-Da2q_giz.js → pegDiagram-XKGWAZYB-1LabVycL.js} +1 -1
  187. package/out/web/assets/{pie-WAS4IAKB-BC8F0N7-.js → pie-WAS4IAKB-DJwo-nf_.js} +1 -1
  188. package/out/web/assets/{pieDiagram-E7YTZNPT-Iy9REJNC.js → pieDiagram-E7YTZNPT-icmAqzy4.js} +1 -1
  189. package/out/web/assets/{quadrantDiagram-AXDQQJYC-ztsiYw7h.js → quadrantDiagram-AXDQQJYC-CZ-wEx41.js} +1 -1
  190. package/out/web/assets/{radar-RG4KPBEZ-B_1P8Z_z.js → radar-RG4KPBEZ-D7d-Xriw.js} +1 -1
  191. package/out/web/assets/{railroad-74A4TZTK-DuTqczSo.js → railroad-74A4TZTK-viuhdYbs.js} +1 -1
  192. package/out/web/assets/{railroad-abnf-HS5TGJTU-Df29WLmp.js → railroad-abnf-HS5TGJTU-DNXiCNea.js} +1 -1
  193. package/out/web/assets/{railroad-ebnf-LZEXJU2U-ePFMo-qp.js → railroad-ebnf-LZEXJU2U-nkSu1-0C.js} +1 -1
  194. package/out/web/assets/{railroad-peg-WCYAUIDC-DYXLW4_E.js → railroad-peg-WCYAUIDC-BhbMeQzz.js} +1 -1
  195. package/out/web/assets/{railroadDiagram-O6MQD6OU-LStnZf2S.js → railroadDiagram-O6MQD6OU-CLpG1jUX.js} +1 -1
  196. package/out/web/assets/{requirementDiagram-BXWQKSXE-DmzonMH-.js → requirementDiagram-BXWQKSXE-kRDrvWrc.js} +1 -1
  197. package/out/web/assets/{sankeyDiagram-P5KCCOFB-B8vjmUU1.js → sankeyDiagram-P5KCCOFB-DxAwmq4x.js} +1 -1
  198. package/out/web/assets/{sequenceDiagram-WJ2MYXX4-CTnplOAX.js → sequenceDiagram-WJ2MYXX4-aLgY-6Wz.js} +1 -1
  199. package/out/web/assets/{stateDiagram-D77RDMKH-BFrIaLdI.js → stateDiagram-D77RDMKH-D5QWfOhp.js} +1 -1
  200. package/out/web/assets/stateDiagram-v2-MP3YSRHH-D6OiZoAP.js +1 -0
  201. package/out/web/assets/{swimlanes-42K2YHIH-BXGngUNU.js → swimlanes-42K2YHIH-CRs5UyA-.js} +1 -1
  202. package/out/web/assets/swimlanesDiagram-VR7AAH4N-UgEXIsV0.js +8 -0
  203. package/out/web/assets/syntaxHighlight-C-wAGT84.js +3 -0
  204. package/out/web/assets/{timeline-definition-24CTP7MA-CegH7n43.js → timeline-definition-24CTP7MA-Denqd9qb.js} +1 -1
  205. package/out/web/assets/{treeView-Q6P3EWNA-DTpJv2Tw.js → treeView-Q6P3EWNA-0my3Z3s4.js} +1 -1
  206. package/out/web/assets/{treemap-WGGIJYW6-i8M9iJU6.js → treemap-WGGIJYW6-B_EjkLYq.js} +1 -1
  207. package/out/web/assets/{vennDiagram-4TSXK5OY-Dpzo1S8U.js → vennDiagram-4TSXK5OY-DxVg3Ewr.js} +1 -1
  208. package/out/web/assets/{wardley-WFR3VGLG-BrNsgtfw.js → wardley-WFR3VGLG-CGD-Tj4p.js} +1 -1
  209. package/out/web/assets/{wardleyDiagram-VM6X3IG4-lQ92LFE1.js → wardleyDiagram-VM6X3IG4-CmfS39Xm.js} +1 -1
  210. package/out/web/assets/{xychartDiagram-S5SC5T6Z-BaOaEoBd.js → xychartDiagram-S5SC5T6Z-BVM9bKis.js} +1 -1
  211. package/out/web/index-web.html +2 -2
  212. package/out/web/index.html +2 -2
  213. package/package.json +2 -2
  214. package/shared-modules/CODEOWNERS +3 -3
  215. package/shared-modules/SDL2/SDL2_mixer.json +27 -0
  216. package/shared-modules/appstream/README.md +13 -0
  217. package/shared-modules/appstream/appstream-compose.json +111 -0
  218. package/shared-modules/dbus-glib/dbus-glib.json +2 -2
  219. package/shared-modules/libportal/libportal.json +23 -0
  220. package/shared-modules/linux-audio/fluidsynth2-static.json +2 -2
  221. package/shared-modules/linux-audio/fluidsynth2.json +2 -2
  222. package/shared-modules/linux-audio/libinstpatch.json +2 -2
  223. package/shared-modules/linux-audio/liblo-static.json +3 -0
  224. package/shared-modules/luajit/luajit.json +2 -2
  225. package/shared-modules/squashfs-tools/squashfs-tools.json +0 -3
  226. package/slash-commands.json +7 -2
  227. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  228. package/src/renderer/shared/agentProgress.ts +51 -3
  229. package/src/renderer/shared/components/Settings/AutomationTab.tsx +2 -56
  230. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +15 -1
  231. package/src/renderer/shared/components/Settings/GeneralTab.tsx +25 -12
  232. package/src/renderer/shared/components/Settings/ThemeTab.test.tsx +45 -0
  233. package/src/renderer/shared/components/Settings/ThemeTab.tsx +106 -3
  234. package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +67 -17
  235. package/src/renderer/shared/components/activity/AgentActivityTable.tsx +376 -134
  236. package/src/renderer/shared/components/activity/AskUserActivityCard.test.tsx +122 -0
  237. package/src/renderer/shared/components/activity/AskUserActivityCard.tsx +70 -0
  238. package/src/renderer/shared/components/activity/McpToolResults.tsx +39 -2
  239. package/src/renderer/shared/components/activity/SubagentDagView.tsx +7 -7
  240. package/src/renderer/shared/components/activity/ViewedImages.tsx +41 -2
  241. package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +3 -3
  242. package/src/renderer/shared/components/app/CommandPalette.tsx +7 -7
  243. package/src/renderer/shared/components/app/DesktopTitlebar.tsx +2 -2
  244. package/src/renderer/shared/components/app/MintDashboard.tsx +27 -17
  245. package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +3 -3
  246. package/src/renderer/shared/components/cards/CalculationCard.tsx +3 -3
  247. package/src/renderer/shared/components/cards/ImageGenCard.tsx +2 -2
  248. package/src/renderer/shared/components/cards/ImageSearchCard.tsx +2 -2
  249. package/src/renderer/shared/components/cards/MermaidCard.tsx +1 -1
  250. package/src/renderer/shared/components/cards/PlanCard.test.tsx +41 -0
  251. package/src/renderer/shared/components/cards/PlanCard.tsx +32 -0
  252. package/src/renderer/shared/components/cards/ReleaseNotesCard.test.tsx +29 -0
  253. package/src/renderer/shared/components/cards/ReleaseNotesCard.tsx +137 -0
  254. package/src/renderer/shared/components/cards/StockCard.tsx +7 -7
  255. package/src/renderer/shared/components/cards/ToolResultCards.tsx +11 -3
  256. package/src/renderer/shared/components/cards/UiMockupWidget.tsx +4 -4
  257. package/src/renderer/shared/components/cards/WeatherCard.tsx +5 -5
  258. package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +11 -0
  259. package/src/renderer/shared/components/chat/ApprovalCard.tsx +21 -2
  260. package/src/renderer/shared/components/chat/ArtifactCards.test.tsx +69 -0
  261. package/src/renderer/shared/components/chat/ArtifactCards.tsx +238 -0
  262. package/src/renderer/shared/components/chat/AttachedDocumentCard.test.tsx +39 -0
  263. package/src/renderer/shared/components/chat/AttachedDocumentCard.tsx +220 -0
  264. package/src/renderer/shared/components/chat/ChatCodeBlock.test.tsx +33 -0
  265. package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +15 -1
  266. package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +40 -2
  267. package/src/renderer/shared/components/chat/ChatMessageItem.tsx +180 -34
  268. package/src/renderer/shared/components/chat/ChatModeSelector.css +227 -0
  269. package/src/renderer/shared/components/chat/ChatModeSelector.test.tsx +45 -0
  270. package/src/renderer/shared/components/chat/ChatModeSelector.tsx +198 -0
  271. package/src/renderer/shared/components/chat/ChatPanel.test.tsx +22 -0
  272. package/src/renderer/shared/components/chat/ChatPanel.tsx +227 -53
  273. package/src/renderer/shared/components/chat/DiffReviewModal.tsx +6 -6
  274. package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +142 -1
  275. package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +45 -3
  276. package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +42 -14
  277. package/src/renderer/shared/components/chat/RewindModal.tsx +10 -10
  278. package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +14 -0
  279. package/src/renderer/shared/components/chat/SourcesBlock.test.tsx +53 -0
  280. package/src/renderer/shared/components/chat/SourcesBlock.tsx +236 -90
  281. package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +45 -1
  282. package/src/renderer/shared/components/chat/TurnFileChanges.tsx +202 -169
  283. package/src/renderer/shared/components/extensions/CliSessionsView.tsx +5 -4
  284. package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +3 -3
  285. package/src/renderer/shared/components/studio/LibraryPage.tsx +48 -5
  286. package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +125 -13
  287. package/src/renderer/shared/components/workspace/CodeReviewPage.test.tsx +50 -0
  288. package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +77 -16
  289. package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +78 -22
  290. package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +13 -0
  291. package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +48 -28
  292. package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +266 -47
  293. package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +18 -6
  294. package/src/renderer/shared/components/workspace/background-terminals.css +11 -11
  295. package/src/renderer/shared/constants/config.ts +1 -5
  296. package/src/renderer/shared/constants/models.test.ts +51 -0
  297. package/src/renderer/shared/constants/models.ts +139 -32
  298. package/src/renderer/shared/conversation/lifecycle.test.ts +91 -3
  299. package/src/renderer/shared/conversation/lifecycle.ts +104 -5
  300. package/src/renderer/shared/conversation/progressiveStream.test.ts +83 -0
  301. package/src/renderer/shared/conversation/progressiveStream.ts +219 -0
  302. package/src/renderer/shared/css/agent-observability.css +133 -0
  303. package/src/renderer/shared/css/chat-ui-cards.css +13 -13
  304. package/src/renderer/shared/css/code-review.css +63 -34
  305. package/src/renderer/shared/css/code-sessions.css +47 -18
  306. package/src/renderer/shared/css/collapsed-paste.css +1 -1
  307. package/src/renderer/shared/css/error-notice.css +1 -1
  308. package/src/renderer/shared/css/file-changes.css +165 -80
  309. package/src/renderer/shared/css/library.css +105 -10
  310. package/src/renderer/shared/css/management-views.css +1 -1
  311. package/src/renderer/shared/css/mcp-results.css +5 -5
  312. package/src/renderer/shared/css/model-selector.css +18 -18
  313. package/src/renderer/shared/css/mods.css +7 -7
  314. package/src/renderer/shared/css/notification-notice.css +2 -2
  315. package/src/renderer/shared/css/queued-messages.css +5 -5
  316. package/src/renderer/shared/css/settings/general.css +18 -13
  317. package/src/renderer/shared/css/settings/profile.css +5 -5
  318. package/src/renderer/shared/css/settings/theme.css +5 -5
  319. package/src/renderer/shared/css/slash-choice.css +2 -2
  320. package/src/renderer/shared/css/slash-suggestions.css +3 -3
  321. package/src/renderer/shared/css/sources-block.css +41 -2
  322. package/src/renderer/shared/css/tools-panel.css +2 -2
  323. package/src/renderer/shared/css/ui-tailwind.css +8 -0
  324. package/src/renderer/shared/css/veo-studio.css +2 -2
  325. package/src/renderer/shared/css/viewed-images.css +7 -7
  326. package/src/renderer/shared/platform.ts +2 -1
  327. package/src/renderer/shared/theme/themeManager.test.ts +17 -0
  328. package/src/renderer/shared/theme/themeManager.ts +21 -4
  329. package/src/renderer/shared/theme/themes.ts +54 -0
  330. package/src/renderer/shared/theme/tokens.css +6 -4
  331. package/src/renderer/shared/types.ts +1 -1
  332. package/src/renderer/shared/utils/agentActivity.test.ts +468 -2
  333. package/src/renderer/shared/utils/agentActivity.ts +602 -35
  334. package/src/renderer/shared/utils/artifactBrowser.test.ts +37 -32
  335. package/src/renderer/shared/utils/artifactBrowser.ts +17 -19
  336. package/src/renderer/shared/utils/attachedDocument.test.ts +73 -0
  337. package/src/renderer/shared/utils/attachedDocument.ts +49 -0
  338. package/src/renderer/shared/utils/fileIcons.test.ts +59 -0
  339. package/src/renderer/shared/utils/fileIcons.ts +149 -16
  340. package/src/renderer/shared/utils/markdown.tsx +72 -19
  341. package/src/renderer/shared/utils/markdown.webImages.test.tsx +16 -0
  342. package/src/renderer/shared/utils/mcpResults.test.tsx +29 -0
  343. package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
  344. package/src/renderer/shared/utils/syntaxHighlight.test.tsx +11 -1
  345. package/src/renderer/shared/utils/syntaxHighlight.tsx +51 -3
  346. package/src/renderer/shared/utils/useGeminiLiveVoice.ts +324 -51
  347. package/src/renderer/shared/utils/workspaceDrag.test.ts +153 -0
  348. package/src/renderer/shared/utils/workspaceDrag.ts +142 -0
  349. package/src/renderer/shared/utils/workspaceMedia.ts +10 -2
  350. package/src/renderer/src/App.tsx +14 -8
  351. package/src/renderer/src/agentProgress.ts +1 -0
  352. package/src/renderer/src/components/ScreenPicker.css +1 -1
  353. package/src/renderer/src/components/WorkspacePanel.tsx +6 -5
  354. package/src/renderer/src/css/app-shell.css +4 -4
  355. package/src/renderer/src/css/chat.css +1261 -241
  356. package/src/renderer/src/css/desktop-tools.css +11 -11
  357. package/src/renderer/src/css/sidebar-workspace.css +2 -2
  358. package/src/renderer/src/css/tool-surfaces.css +12 -12
  359. package/src/renderer/src/index.css +38 -20
  360. package/src/renderer/src/tauri.ts +22 -2
  361. package/src/renderer/src-web/App.tsx +14 -0
  362. package/src/renderer/src-web/agentProgress.ts +1 -0
  363. package/src/renderer/src-web/css/app-shell.css +4 -4
  364. package/src/renderer/src-web/css/chat.css +1278 -190
  365. package/src/renderer/src-web/css/mobile.css +29 -13
  366. package/src/renderer/src-web/css/settings/mobile.css +2 -2
  367. package/src/renderer/src-web/css/sidebar-workspace.css +1 -1
  368. package/src/renderer/src-web/index.css +38 -20
  369. package/src/renderer/src-web/tauri.ts +22 -2
  370. package/src/renderer/tests/browserArtifacts.browser.tsx +1 -1
  371. package/src/renderer/tests/library.browser.tsx +3 -0
  372. package/src/renderer/tests/liveVoice.browser.html +1 -0
  373. package/src/renderer/tests/workspaceDrop.browser.tsx +1 -1
  374. package/src-tauri/src/app/lifecycle.rs +1 -14
  375. package/src-tauri/src/background/headless.rs +14 -8
  376. package/src-tauri/src/commands/conversation.rs +38 -0
  377. package/src-tauri/src/native/mod.rs +0 -1
  378. package/src-tauri/src/native/system.rs +4 -47
  379. package/crates/mint-cli/src/interactive/chat_tui.rs +0 -8855
  380. package/crates/mint-core/src/agent/chat.rs +0 -4394
  381. package/crates/mint-core/src/browser/lifecycle.rs +0 -291
  382. package/crates/mint-core/src/browser/logging.rs +0 -20
  383. package/crates/mint-core/src/orchestration/tools/browser.rs +0 -31
  384. package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +0 -2
  385. package/out/web/assets/CalculationCard-CRjP3E1j.js +0 -1
  386. package/out/web/assets/CliSessionsView-C3Br2v5Y.css +0 -1
  387. package/out/web/assets/CodeReviewPage-CUJceQme.css +0 -1
  388. package/out/web/assets/CodeReviewPage-VCUgPla_.js +0 -2
  389. package/out/web/assets/ImageGenCard-BCXxkVJX.js +0 -1
  390. package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +0 -1
  391. package/out/web/assets/LibraryPage-DAxUTAg7.css +0 -1
  392. package/out/web/assets/LibraryPage-WlviRdR8.js +0 -2
  393. package/out/web/assets/LinkedFoldersView-CKRgroWK.js +0 -1
  394. package/out/web/assets/McpServersView-D3wHnuHg.js +0 -1
  395. package/out/web/assets/MermaidCard-NqrHdyJW.js +0 -2
  396. package/out/web/assets/MintDashboard-B2DJsDDO.css +0 -1
  397. package/out/web/assets/MintDashboard-COw_xol7.js +0 -63
  398. package/out/web/assets/RewindModal-DempuDod.js +0 -1
  399. package/out/web/assets/SettingsWindow-2YP3yj7s.js +0 -4
  400. package/out/web/assets/SettingsWindow-Dz8LQVgD.css +0 -1
  401. package/out/web/assets/StockCard-BDMCe7mx.js +0 -1
  402. package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +0 -38
  403. package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +0 -1
  404. package/out/web/assets/UiMockupWidget-DD73QWg1.js +0 -1
  405. package/out/web/assets/WeatherCard-CPucIfq8.js +0 -1
  406. package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +0 -2
  407. package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +0 -1
  408. package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +0 -1
  409. package/out/web/assets/config-CvIO5hkn.js +0 -2
  410. package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +0 -1
  411. package/out/web/assets/index-BmpulIkg.js +0 -168
  412. package/out/web/assets/index-CkvBwhX_.css +0 -1
  413. package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +0 -1
  414. package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +0 -8
  415. package/shared-modules/libsecret/libsecret.json +0 -23
  416. package/shared-modules/libusb/libusb.json +0 -27
  417. package/shared-modules/lzo/lzo.json +0 -27
  418. package/src/renderer/src/components/BrowserShell.tsx +0 -108
  419. package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +0 -91
  420. package/src/renderer/src/components/browser-shell.css +0 -185
  421. package/src-tauri/src/native/browser.rs +0 -252
@@ -0,0 +1,857 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use serde_json::Value;
3
+ use thiserror::Error;
4
+
5
+ use crate::MintConfig;
6
+
7
+ mod anthropic;
8
+ mod custom;
9
+ mod gemini;
10
+ mod ollama;
11
+ mod openai;
12
+ mod streaming;
13
+
14
+ #[cfg(test)]
15
+ mod tests;
16
+
17
+ use anthropic::*;
18
+ use custom::*;
19
+ pub(crate) use gemini::gemini_sanitize_schema;
20
+ use gemini::*;
21
+ use ollama::*;
22
+ use openai::*;
23
+
24
+ /// Provider-neutral pieces produced while a model response is still arriving.
25
+ /// Callers never need to know which vendor-specific field carried the data.
26
+ #[derive(Debug, Clone, PartialEq, Eq)]
27
+ pub enum ChatStreamEvent {
28
+ TextDelta {
29
+ delta: String,
30
+ },
31
+ ReasoningDelta {
32
+ delta: String,
33
+ },
34
+ ToolCallDelta {
35
+ index: usize,
36
+ name: Option<String>,
37
+ arguments: Option<String>,
38
+ input: Option<Value>,
39
+ },
40
+ }
41
+
42
+ /// `available_providers()`'s configured-providers list, reordered so
43
+ /// `gemini` is tried first among fallbacks (when it's configured and isn't
44
+ /// already the primary) — the user's explicit preference, ahead of
45
+ /// `available_providers()`'s own anthropic/openai/openrouter/deepseek/
46
+ /// gemini/... ordering (which exists for display purposes, e.g. the
47
+ /// `/models` picker, and is left alone). Everything else keeps its existing
48
+ /// relative order.
49
+ fn fallback_provider_order(config: &MintConfig) -> Vec<String> {
50
+ let mut providers = config.available_providers();
51
+ if let Some(pos) = providers.iter().position(|p| p == "gemini") {
52
+ let gemini = providers.remove(pos);
53
+ providers.insert(0, gemini);
54
+ }
55
+ providers
56
+ }
57
+
58
+ /// Send a chat request, automatically falling back to other configured providers
59
+ /// if the primary one returns a recoverable error. Tries `gemini` first among
60
+ /// the fallbacks when it's configured (see `fallback_provider_order`), then
61
+ /// the rest in their usual order.
62
+ /// Returns `(response, Option<fallback_provider>)`.
63
+ pub async fn send_chat_with_fallback(
64
+ config: &MintConfig,
65
+ request: &ChatRequest,
66
+ ) -> Result<(ChatResponse, Option<String>), ChatError> {
67
+ let primary_error = match send_chat(config, request).await {
68
+ Ok(r) => return Ok((r, None)),
69
+ Err(e) if !is_recoverable(&e) => return Err(e),
70
+ Err(e) => e,
71
+ };
72
+ // No point cycling through other cloud providers if the network itself is
73
+ // unreachable — they all sit behind the same dead link, so every attempt
74
+ // would just fail the same way after its own connect timeout. Surface
75
+ // this immediately so the caller can retry-and-wait instead.
76
+ if matches!(primary_error, ChatError::NetworkUnavailable) {
77
+ return Err(primary_error);
78
+ }
79
+ let fallback_reason = fallback_reason_text(&config.ai_provider, &primary_error);
80
+ for provider in fallback_provider_order(config) {
81
+ if provider.as_str() == config.ai_provider.as_str() {
82
+ continue;
83
+ }
84
+ let alt = config_for_provider(config, &provider);
85
+ if let Ok(mut r) = send_chat(&alt, request).await {
86
+ r.fallback_provider = Some(config.ai_provider.clone());
87
+ r.fallback_reason = fallback_reason.clone();
88
+ return Ok((r, Some(provider)));
89
+ }
90
+ }
91
+ // all fallbacks failed — retry primary to surface original error
92
+ send_chat(config, request).await.map(|r| (r, None))
93
+ }
94
+
95
+ /// Short, human-readable explanation for why `send_chat_with_fallback` swapped
96
+ /// away from `provider` — `None` for an ordinary transient failure, so routine
97
+ /// hiccups don't get an extra notice while specific, actionable ones do.
98
+ fn fallback_reason_text(provider: &str, error: &ChatError) -> Option<String> {
99
+ let provider_name = if provider.starts_with("custom:") {
100
+ provider.strip_prefix("custom:").unwrap_or(provider)
101
+ } else {
102
+ provider
103
+ };
104
+ match error {
105
+ ChatError::InsufficientBalance(_) => Some(format!("{provider_name} ran out of balance")),
106
+ ChatError::Request(e) => {
107
+ if let Some(status) = e.status() {
108
+ if status == reqwest::StatusCode::PAYLOAD_TOO_LARGE {
109
+ Some(format!("{provider_name} payload too large (HTTP 413)"))
110
+ } else if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
111
+ Some(format!("{provider_name} rate limit exceeded (HTTP 429)"))
112
+ } else {
113
+ Some(format!("{provider_name} returned HTTP {status}"))
114
+ }
115
+ } else {
116
+ None
117
+ }
118
+ }
119
+ _ => None,
120
+ }
121
+ }
122
+
123
+ /// Stream a chat request with automatic provider fallback.
124
+ /// Returns `(response, Option<fallback_provider>)`.
125
+ pub async fn stream_chat_with_fallback<F>(
126
+ config: &MintConfig,
127
+ request: &ChatRequest,
128
+ mut on_chunk: F,
129
+ ) -> Result<(ChatResponse, Option<String>), ChatError>
130
+ where
131
+ F: FnMut(String),
132
+ {
133
+ let primary_error = match stream_chat(config, request, &mut on_chunk).await {
134
+ Ok(r) => return Ok((r, None)),
135
+ Err(e) if !is_recoverable(&e) => return Err(e),
136
+ Err(e) => {
137
+ eprintln!("Ollama stream chat failed, falling back: {:?}", e);
138
+ e
139
+ }
140
+ };
141
+ if matches!(primary_error, ChatError::NetworkUnavailable) {
142
+ return Err(primary_error);
143
+ }
144
+ let fallback_reason = fallback_reason_text(&config.ai_provider, &primary_error);
145
+ for provider in fallback_provider_order(config) {
146
+ if provider.as_str() == config.ai_provider.as_str() {
147
+ continue;
148
+ }
149
+ let alt = config_for_provider(config, &provider);
150
+ if let Ok(mut r) = stream_chat(&alt, request, &mut on_chunk).await {
151
+ r.fallback_provider = Some(config.ai_provider.clone());
152
+ r.fallback_reason = fallback_reason.clone();
153
+ return Ok((r, Some(provider)));
154
+ }
155
+ }
156
+ stream_chat(config, request, &mut on_chunk)
157
+ .await
158
+ .map(|r| (r, None))
159
+ }
160
+
161
+ /// Streaming equivalent of [`send_chat_with_fallback`] that preserves the
162
+ /// complete response (tool calls, usage, stop reason, and reasoning) while
163
+ /// exposing provider-neutral deltas as they arrive.
164
+ pub async fn stream_chat_events_with_fallback<F>(
165
+ config: &MintConfig,
166
+ request: &ChatRequest,
167
+ mut on_event: F,
168
+ ) -> Result<(ChatResponse, Option<String>), ChatError>
169
+ where
170
+ F: FnMut(ChatStreamEvent),
171
+ {
172
+ let primary_error = match stream_chat_events(config, request, &mut on_event).await {
173
+ Ok(r) => return Ok((r, None)),
174
+ Err(e) if !is_recoverable(&e) => return Err(e),
175
+ Err(e) => e,
176
+ };
177
+ if matches!(primary_error, ChatError::NetworkUnavailable) {
178
+ return Err(primary_error);
179
+ }
180
+ let fallback_reason = fallback_reason_text(&config.ai_provider, &primary_error);
181
+ for provider in fallback_provider_order(config) {
182
+ if provider == config.ai_provider {
183
+ continue;
184
+ }
185
+ let alt = config_for_provider(config, &provider);
186
+ if let Ok(mut response) = stream_chat_events(&alt, request, &mut on_event).await {
187
+ response.fallback_provider = Some(config.ai_provider.clone());
188
+ response.fallback_reason = fallback_reason.clone();
189
+ return Ok((response, Some(provider)));
190
+ }
191
+ }
192
+ stream_chat_events(config, request, &mut on_event)
193
+ .await
194
+ .map(|response| (response, None))
195
+ }
196
+
197
+ /// Whether an error warrants trying another provider.
198
+ fn is_recoverable(e: &ChatError) -> bool {
199
+ matches!(
200
+ e,
201
+ ChatError::MissingApiKey(_)
202
+ | ChatError::Request(_)
203
+ | ChatError::MissingResponseText
204
+ | ChatError::UnsupportedAttachments(_)
205
+ | ChatError::InsufficientBalance(_)
206
+ | ChatError::NetworkUnavailable
207
+ )
208
+ }
209
+
210
+ /// Clone the config with a different active provider.
211
+ fn config_for_provider(config: &MintConfig, provider: &str) -> MintConfig {
212
+ MintConfig {
213
+ ai_provider: provider.to_owned(),
214
+ ..config.clone()
215
+ }
216
+ }
217
+
218
+ #[derive(Debug, Clone, Deserialize, Default)]
219
+ #[serde(rename_all = "camelCase")]
220
+ pub struct ChatRequest {
221
+ pub message: String,
222
+ #[serde(default)]
223
+ pub system_instruction: String,
224
+ #[serde(default)]
225
+ pub chat_id: Option<String>,
226
+ #[serde(default)]
227
+ pub image_data_uri: Option<String>,
228
+ #[serde(default)]
229
+ pub audio_data_uri: Option<String>,
230
+ #[serde(default)]
231
+ pub video_data_uri: Option<String>,
232
+ #[serde(default)]
233
+ pub document_attachment: Option<DocumentAttachment>,
234
+ #[serde(default)]
235
+ pub workspace_path: Option<String>,
236
+ #[serde(default)]
237
+ pub agent_id: Option<String>,
238
+ #[serde(default)]
239
+ pub plan_mode: bool,
240
+ /// When set, restricts this turn's `mcp_tool`/`mcp_list_tools` calls to this one
241
+ /// configured MCP server — set by picking a plugin from the composer's `@` mention
242
+ /// dropdown, rather than leaving server selection to the model's own judgment.
243
+ #[serde(default)]
244
+ pub pinned_mcp_server: Option<String>,
245
+ /// Structured conversation history for native tool-calling. When set, provider
246
+ /// adapters build the request from this instead of the flat `message` field.
247
+ #[serde(default)]
248
+ pub messages: Option<Vec<ChatMessage>>,
249
+ /// Tool definitions to offer the model via each provider's native
250
+ /// function/tool-calling API. Ignored when `messages` is `None`.
251
+ #[serde(default)]
252
+ pub tools: Option<Vec<ToolSpec>>,
253
+ /// Optional per-request temperature override. When `None`, falls back to
254
+ /// `config.resolved_temperature()`.
255
+ #[serde(default)]
256
+ pub temperature: Option<f64>,
257
+ /// Optional per-request thinking/reasoning enabled override.
258
+ #[serde(default)]
259
+ pub thinking_enabled: Option<bool>,
260
+ /// Optional per-request thinking effort override ("low", "medium", "high", "extra_high").
261
+ #[serde(default)]
262
+ pub thinking_effort: Option<String>,
263
+ }
264
+
265
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
266
+ #[serde(rename_all = "camelCase")]
267
+ pub enum ChatRole {
268
+ System,
269
+ User,
270
+ Assistant,
271
+ Tool,
272
+ }
273
+
274
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275
+ #[serde(rename_all = "camelCase")]
276
+ pub struct ChatMessage {
277
+ pub role: ChatRole,
278
+ pub content: Vec<ContentBlock>,
279
+ }
280
+
281
+ impl ChatMessage {
282
+ pub fn text(role: ChatRole, text: impl Into<String>) -> Self {
283
+ ChatMessage {
284
+ role,
285
+ content: vec![ContentBlock::Text { text: text.into() }],
286
+ }
287
+ }
288
+ }
289
+
290
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291
+ #[serde(tag = "type", rename_all = "camelCase")]
292
+ pub enum ContentBlock {
293
+ Text {
294
+ text: String,
295
+ },
296
+ ToolUse {
297
+ id: String,
298
+ name: String,
299
+ input: Value,
300
+ /// See `ToolCall::thought_signature` — carried on `ToolUse` too so it
301
+ /// survives being replayed back into `native_messages` history on the
302
+ /// next turn. `None` for every provider but Gemini.
303
+ #[serde(default, skip_serializing_if = "Option::is_none")]
304
+ thought_signature: Option<String>,
305
+ },
306
+ ToolResult {
307
+ tool_use_id: String,
308
+ content: String,
309
+ #[serde(default)]
310
+ is_error: bool,
311
+ },
312
+ Image {
313
+ data_uri: String,
314
+ },
315
+ Audio {
316
+ data_uri: String,
317
+ },
318
+ Video {
319
+ data_uri: String,
320
+ },
321
+ }
322
+
323
+ /// A tool definition offered to the model via native function/tool-calling.
324
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
325
+ #[serde(rename_all = "camelCase")]
326
+ pub struct ToolSpec {
327
+ pub name: String,
328
+ pub description: String,
329
+ /// JSON schema (draft-7-ish, as accepted by the provider's tool-parameter format).
330
+ pub parameters: Value,
331
+ }
332
+
333
+ /// A tool invocation the model requested via native function/tool-calling.
334
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
335
+ #[serde(rename_all = "camelCase")]
336
+ pub struct ToolCall {
337
+ pub id: String,
338
+ pub name: String,
339
+ pub input: Value,
340
+ /// Gemini 3.x's "thinking" models attach an opaque `thoughtSignature` to
341
+ /// each `functionCall` part; it must be echoed back verbatim on the next
342
+ /// turn's replayed `functionCall` or the API rejects the request with a
343
+ /// 400 (`INVALID_ARGUMENT: Function call is missing a thought_signature`).
344
+ /// `None` for every other provider.
345
+ #[serde(default, skip_serializing_if = "Option::is_none")]
346
+ pub thought_signature: Option<String>,
347
+ }
348
+
349
+ #[derive(Debug, Clone, Deserialize)]
350
+ #[serde(rename_all = "camelCase")]
351
+ pub struct DocumentAttachment {
352
+ pub filename: String,
353
+ pub data_uri: String,
354
+ }
355
+
356
+ const MAX_DOCUMENT_BYTES: usize = 10 * 1024 * 1024;
357
+ const MAX_DOCUMENT_CONTEXT_CHARS: usize = 400_000;
358
+ static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
359
+
360
+ impl ChatRequest {
361
+ pub(crate) fn with_stored_document_context(
362
+ &self,
363
+ _config: &crate::MintConfig,
364
+ path: &std::path::Path,
365
+ ) -> Result<ChatRequest, String> {
366
+ let Some(document) = &self.document_attachment else {
367
+ return Ok(self.clone());
368
+ };
369
+ let extracted =
370
+ crate::knowledge::extract_managed_document_text(path).map_err(|e| e.to_string())?;
371
+ self.with_document_text(document, extracted)
372
+ }
373
+ fn with_document_text(
374
+ &self,
375
+ document: &DocumentAttachment,
376
+ extracted: String,
377
+ ) -> Result<ChatRequest, String> {
378
+ let mut context: String = extracted.chars().take(MAX_DOCUMENT_CONTEXT_CHARS).collect();
379
+ if extracted.chars().count() > MAX_DOCUMENT_CONTEXT_CHARS {
380
+ context.push_str("\n\n[Document text truncated because it is long.]");
381
+ }
382
+
383
+ let filename = document.filename.trim();
384
+ let filename = if filename.is_empty() {
385
+ "attached document"
386
+ } else {
387
+ filename
388
+ };
389
+
390
+ let mut enriched = self.clone();
391
+ enriched.document_attachment = None;
392
+ enriched.message = format!(
393
+ "{}\n\nAttached document: {filename}\nUse the extracted document text below when answering. If the user asks for a summary, summarize this document.\n\n--- Extracted document text ---\n{context}\n--- End extracted document text ---",
394
+ self.message
395
+ );
396
+ Ok(enriched)
397
+ }
398
+ pub fn with_document_context(&self, config: &crate::MintConfig) -> Result<ChatRequest, String> {
399
+ let Some(document) = &self.document_attachment else {
400
+ return Ok(self.clone());
401
+ };
402
+
403
+ let (_mime_type, encoded) = document
404
+ .data_uri
405
+ .strip_prefix("data:")
406
+ .and_then(|payload| payload.split_once(";base64,"))
407
+ .ok_or_else(|| "invalid document attachment data URI".to_string())?;
408
+
409
+ // Dispatch by the filename's extension, not the data URI's MIME type:
410
+ // browsers report an empty or generic `application/octet-stream` type
411
+ // for many of these formats (.md, .toml, .yaml, ...), so MIME type
412
+ // isn't a reliable signal here. `extract_document_text` below performs
413
+ // the real allowlist check and rejects anything it doesn't recognize.
414
+ let extension = std::path::Path::new(document.filename.trim())
415
+ .extension()
416
+ .and_then(|ext| ext.to_str())
417
+ .map(|ext| ext.to_ascii_lowercase())
418
+ .filter(|ext| !ext.is_empty())
419
+ .unwrap_or_else(|| "txt".to_string());
420
+
421
+ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
422
+ let bytes = BASE64_STANDARD
423
+ .decode(encoded)
424
+ .map_err(|error| format!("invalid document attachment encoding: {error}"))?;
425
+ if bytes.len() > MAX_DOCUMENT_BYTES {
426
+ return Err("document attachment is too large (> 10 MiB)".into());
427
+ }
428
+
429
+ let directory = crate::config::config_path()
430
+ .map_err(|error| error.to_string())?
431
+ .with_file_name("Attachments");
432
+ std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
433
+
434
+ let safe_name: String = document
435
+ .filename
436
+ .chars()
437
+ .filter(|c| c.is_ascii_alphanumeric() || matches!(*c, '.' | '-' | '_'))
438
+ .take(80)
439
+ .collect();
440
+ let safe_name = if safe_name.is_empty() {
441
+ "document".into()
442
+ } else {
443
+ safe_name
444
+ };
445
+
446
+ let path = directory.join(format!(
447
+ "mint-doc-{}-{}.{}",
448
+ COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
449
+ safe_name,
450
+ extension
451
+ ));
452
+ std::fs::write(&path, bytes).map_err(|error| error.to_string())?;
453
+
454
+ let extracted = crate::knowledge::extract_document_text(&path, config)
455
+ .map_err(|error| error.to_string());
456
+ let _ = std::fs::remove_file(&path);
457
+ let extracted = extracted?;
458
+
459
+ self.with_document_text(document, extracted)
460
+ }
461
+ }
462
+
463
+ #[derive(Debug, Clone, Serialize)]
464
+ #[serde(rename_all = "camelCase")]
465
+ pub struct ChatResponse {
466
+ pub provider: String,
467
+ pub model: String,
468
+ pub text: String,
469
+ #[serde(skip_serializing_if = "Option::is_none")]
470
+ pub fallback_provider: Option<String>,
471
+ /// Short, human-readable reason `send_chat_with_fallback` swapped providers,
472
+ /// when the primary's failure was specific enough to explain (insufficient
473
+ /// balance, no network) — `None` for an ordinary transient error, so a
474
+ /// ordinary hiccup doesn't get an extra notice while a genuinely actionable
475
+ /// one does.
476
+ #[serde(skip_serializing_if = "Option::is_none")]
477
+ pub fallback_reason: Option<String>,
478
+ /// Tool calls the model requested via native function/tool-calling, when the
479
+ /// request set `ChatRequest.tools` and the provider honored it.
480
+ #[serde(skip_serializing_if = "Option::is_none")]
481
+ pub tool_calls: Option<Vec<ToolCall>>,
482
+ /// Provider-reported reason generation stopped (e.g. `"tool_use"`, `"end_turn"`,
483
+ /// `"stop"`, `"max_tokens"`), when the provider reports one.
484
+ #[serde(skip_serializing_if = "Option::is_none")]
485
+ pub stop_reason: Option<String>,
486
+ /// Total tokens (input + output) the provider billed for this turn, when
487
+ /// reported. Used to track how close a long-running conversation is to the
488
+ /// model's context window, rather than estimating client-side. Derived from
489
+ /// `input_tokens + output_tokens` when both are present.
490
+ #[serde(skip_serializing_if = "Option::is_none")]
491
+ pub total_tokens: Option<u32>,
492
+ /// Prompt/context tokens the provider processed for this turn (the whole
493
+ /// resent history + system prompt + tool schemas), when reported — the
494
+ /// "sent up to the model" half, tracked separately from `output_tokens` so
495
+ /// the UI can show `↑ context · ↓ generated` instead of one opaque sum.
496
+ #[serde(skip_serializing_if = "Option::is_none")]
497
+ pub input_tokens: Option<u32>,
498
+ /// Completion tokens the model generated this turn, when reported — the
499
+ /// "came back down from the model" half. See `input_tokens`.
500
+ #[serde(skip_serializing_if = "Option::is_none")]
501
+ pub output_tokens: Option<u32>,
502
+ /// Model reasoning / chain-of-thought tokens, when reported by reasoning models
503
+ /// (e.g. DeepSeek Reasoner, Claude 3.7 Thinking, Gemini Thinking, or <think> tags).
504
+ #[serde(skip_serializing_if = "Option::is_none")]
505
+ pub thought: Option<String>,
506
+ }
507
+
508
+ /// Internal, provider-agnostic result of a single non-streaming provider call.
509
+ /// `send_chat` maps this onto the public `ChatResponse`.
510
+ pub(super) struct ProviderReply {
511
+ pub(super) model: String,
512
+ pub(super) text: String,
513
+ pub(super) thought: Option<String>,
514
+ pub(super) tool_calls: Option<Vec<ToolCall>>,
515
+ pub(super) stop_reason: Option<String>,
516
+ /// Prompt/context tokens processed for this call, when the provider reports
517
+ /// usage. `send_chat` derives `ChatResponse::total_tokens` from this plus
518
+ /// `output_tokens`.
519
+ pub(super) input_tokens: Option<u32>,
520
+ /// Completion tokens generated for this call, when reported.
521
+ pub(super) output_tokens: Option<u32>,
522
+ }
523
+
524
+ #[derive(Debug, Error)]
525
+ pub enum ChatError {
526
+ #[error("provider '{0}' is not implemented in the Rust backend yet")]
527
+ UnsupportedProvider(String),
528
+ #[error("missing API key for provider '{0}'")]
529
+ MissingApiKey(String),
530
+ #[error("provider request failed: {0}")]
531
+ Request(#[from] reqwest::Error),
532
+ #[error("provider response did not include assistant text")]
533
+ MissingResponseText,
534
+ #[error("provider '{0}' does not support Mint multimodal attachments yet")]
535
+ UnsupportedAttachments(String),
536
+ #[error("invalid multimodal data URI")]
537
+ InvalidAttachment,
538
+ /// Provider returned `402 Payment Required` — distinguished from the generic
539
+ /// `Request` case so callers can tell "your account ran out of balance" apart
540
+ /// from an ordinary transient failure instead of silently swapping providers
541
+ /// with no explanation.
542
+ #[error("provider '{0}' rejected the request: insufficient balance")]
543
+ InsufficientBalance(String),
544
+ /// The request never reached any server at all (DNS failure, connection
545
+ /// refused, no route) — as opposed to reaching a server that returned an
546
+ /// error. Distinguished so a caller can retry-and-wait instead of pointlessly
547
+ /// cycling through other cloud providers that sit behind the same dead
548
+ /// network link.
549
+ #[error("no network connection to reach the provider")]
550
+ NetworkUnavailable,
551
+ #[error("provider API error: {0}")]
552
+ Api(String),
553
+ }
554
+
555
+ pub async fn send_chat(
556
+ config: &MintConfig,
557
+ request: &ChatRequest,
558
+ ) -> Result<ChatResponse, ChatError> {
559
+ let client = crate::HTTP_CLIENT.clone();
560
+ let provider = config.ai_provider.as_str();
561
+ require_supported_attachments(provider, request)?;
562
+ let reply = match provider {
563
+ "gemini" => call_gemini(&client, config, request).await?,
564
+ "openai" | "local_openai" | "openrouter" | "deepseek" => {
565
+ call_openai(&client, config, request).await?
566
+ }
567
+ "ollama" => call_ollama(&client, config, request).await?,
568
+ "anthropic" => call_anthropic(&client, config, request).await?,
569
+ "huggingface" => call_huggingface(&client, config, request).await?,
570
+ p if p.starts_with("custom:") => call_custom_provider(&client, config, request).await?,
571
+ other => return Err(ChatError::UnsupportedProvider(other.into())),
572
+ };
573
+ Ok(ChatResponse {
574
+ provider: provider.into(),
575
+ model: reply.model,
576
+ text: reply.text,
577
+ thought: reply.thought,
578
+ fallback_provider: None,
579
+ fallback_reason: None,
580
+ tool_calls: reply.tool_calls,
581
+ stop_reason: reply.stop_reason,
582
+ total_tokens: match (reply.input_tokens, reply.output_tokens) {
583
+ (Some(i), Some(o)) => Some(i + o),
584
+ (i, o) => i.or(o),
585
+ },
586
+ input_tokens: reply.input_tokens,
587
+ output_tokens: reply.output_tokens,
588
+ })
589
+ }
590
+
591
+ pub async fn stream_chat<F>(
592
+ config: &MintConfig,
593
+ request: &ChatRequest,
594
+ mut on_chunk: F,
595
+ ) -> Result<ChatResponse, ChatError>
596
+ where
597
+ F: FnMut(String),
598
+ {
599
+ stream_chat_events(config, request, |event| {
600
+ if let ChatStreamEvent::TextDelta { delta } = event {
601
+ on_chunk(delta);
602
+ }
603
+ })
604
+ .await
605
+ }
606
+
607
+ pub async fn stream_chat_events<F>(
608
+ config: &MintConfig,
609
+ request: &ChatRequest,
610
+ mut on_event: F,
611
+ ) -> Result<ChatResponse, ChatError>
612
+ where
613
+ F: FnMut(ChatStreamEvent),
614
+ {
615
+ let client = crate::HTTP_CLIENT.clone();
616
+ let provider = config.ai_provider.as_str();
617
+ require_supported_attachments(provider, request)?;
618
+ let reply = match provider {
619
+ "gemini" => stream_gemini_reply(&client, config, request, &mut on_event).await?,
620
+ "openai" | "local_openai" | "openrouter" | "deepseek" => {
621
+ stream_openai_reply(&client, config, request, &mut on_event).await?
622
+ }
623
+ "ollama" => stream_ollama_reply(&client, config, request, &mut on_event).await?,
624
+ "anthropic" => stream_anthropic_reply(&client, config, request, &mut on_event).await?,
625
+ "huggingface" => stream_huggingface_reply(&client, config, request, &mut on_event).await?,
626
+ p if p.starts_with("custom:") => {
627
+ stream_custom_provider_reply(&client, config, request, &mut on_event).await?
628
+ }
629
+ other => return Err(ChatError::UnsupportedProvider(other.into())),
630
+ };
631
+ Ok(ChatResponse {
632
+ provider: provider.into(),
633
+ model: reply.model,
634
+ text: reply.text,
635
+ thought: reply.thought,
636
+ fallback_provider: None,
637
+ fallback_reason: None,
638
+ tool_calls: reply.tool_calls,
639
+ stop_reason: reply.stop_reason,
640
+ total_tokens: match (reply.input_tokens, reply.output_tokens) {
641
+ (Some(input), Some(output)) => Some(input + output),
642
+ (input, output) => input.or(output),
643
+ },
644
+ input_tokens: reply.input_tokens,
645
+ output_tokens: reply.output_tokens,
646
+ })
647
+ }
648
+
649
+ /// Extracts `<think>...</think>` or `<thought>...</thought>` chain-of-thought blocks emitted by
650
+ /// reasoning models or the prompt-based CoT fallback when embedded directly into plain text content.
651
+ /// Returns `(Option<thought>, remaining_text)`.
652
+ pub(crate) fn extract_think_tag(text: &str) -> (Option<String>, String) {
653
+ for (open_tag, close_tag) in [("<think>", "</think>"), ("<thought>", "</thought>")] {
654
+ if let Some(start) = text.find(open_tag) {
655
+ let open_len = open_tag.len();
656
+ let close_len = close_tag.len();
657
+ if let Some(end) = text[start + open_len..].find(close_tag) {
658
+ let thought = text[start + open_len..start + open_len + end]
659
+ .trim()
660
+ .to_string();
661
+ let mut remaining = text[..start].to_string();
662
+ remaining.push_str(&text[start + open_len + end + close_len..]);
663
+ let cleaned = remaining.trim().to_string();
664
+ return (
665
+ if thought.is_empty() {
666
+ None
667
+ } else {
668
+ Some(thought)
669
+ },
670
+ cleaned,
671
+ );
672
+ } else {
673
+ let thought = text[start + open_len..].trim().to_string();
674
+ let cleaned = text[..start].trim().to_string();
675
+ return (
676
+ if thought.is_empty() {
677
+ None
678
+ } else {
679
+ Some(thought)
680
+ },
681
+ cleaned,
682
+ );
683
+ }
684
+ }
685
+ }
686
+ (None, text.to_string())
687
+ }
688
+
689
+ pub(super) fn required_key(provider: &str, key: &str) -> Result<(), ChatError> {
690
+ if key.trim().is_empty() {
691
+ Err(ChatError::MissingApiKey(provider.into()))
692
+ } else {
693
+ Ok(())
694
+ }
695
+ }
696
+
697
+ pub(super) fn provider_key(configured: &str, environment_variable: &str) -> String {
698
+ if configured.trim().is_empty() {
699
+ std::env::var(environment_variable).unwrap_or_default()
700
+ } else {
701
+ configured.into()
702
+ }
703
+ }
704
+
705
+ pub(super) fn require_supported_attachments(
706
+ provider: &str,
707
+ request: &ChatRequest,
708
+ ) -> Result<(), ChatError> {
709
+ let has_image = request
710
+ .image_data_uri
711
+ .as_ref()
712
+ .is_some_and(|s| !s.trim().is_empty());
713
+ let has_audio = request
714
+ .audio_data_uri
715
+ .as_ref()
716
+ .is_some_and(|s| !s.trim().is_empty());
717
+ let has_video = request
718
+ .video_data_uri
719
+ .as_ref()
720
+ .is_some_and(|s| !s.trim().is_empty());
721
+
722
+ match provider {
723
+ "gemini" => Ok(()),
724
+ "ollama" if has_audio || has_video => {
725
+ Err(ChatError::UnsupportedAttachments(provider.into()))
726
+ }
727
+ "ollama" => Ok(()),
728
+ // The real OpenAI API supports audio input natively (`input_audio` content
729
+ // parts, see `openai_audio_part`) — video still isn't implemented for it.
730
+ "openai" if has_video => Err(ChatError::UnsupportedAttachments(provider.into())),
731
+ "openai" => Ok(()),
732
+ // These all build their payload via `openai_chat_payload`/`anthropic_chat_payload`,
733
+ // which support image attachments (see `openai_message_content`/
734
+ // `anthropic_message_content`). Unlike literal `"openai"` above, these are
735
+ // separate services/proxies that aren't confirmed to speak the same
736
+ // `input_audio` schema, so audio/video stay unsupported for them.
737
+ "local_openai" | "openrouter" | "deepseek" | "huggingface" | "anthropic"
738
+ if has_audio || has_video =>
739
+ {
740
+ Err(ChatError::UnsupportedAttachments(provider.into()))
741
+ }
742
+ "local_openai" | "openrouter" | "deepseek" | "huggingface" | "anthropic" => Ok(()),
743
+ // Custom providers route through `call_custom_provider`, which also uses
744
+ // `openai_chat_payload`, so images are supported the same way; audio/video
745
+ // are not implemented for them yet.
746
+ p if p.starts_with("custom:") && (has_audio || has_video) => {
747
+ Err(ChatError::UnsupportedAttachments(provider.into()))
748
+ }
749
+ p if p.starts_with("custom:") => Ok(()),
750
+ _ if has_image || has_audio || has_video => {
751
+ Err(ChatError::UnsupportedAttachments(provider.into()))
752
+ }
753
+ _ => Ok(()),
754
+ }
755
+ }
756
+
757
+ pub(super) fn wants_agent_json(request: &ChatRequest) -> bool {
758
+ let instruction = request.system_instruction.as_str();
759
+ instruction.contains("Return only JSON")
760
+ || (instruction.contains("Return exactly one JSON object per response")
761
+ && instruction.contains("Input formats:")
762
+ && instruction.contains("- finish:"))
763
+ }
764
+
765
+ /// Resolves the effective sampling temperature for this request, prioritizing
766
+ /// `request.temperature` if present, otherwise falling back to `config.resolved_temperature()`.
767
+ pub(crate) fn effective_temperature(config: &MintConfig, request: &ChatRequest) -> f64 {
768
+ request
769
+ .temperature
770
+ .unwrap_or_else(|| config.resolved_temperature())
771
+ }
772
+
773
+ /// Resolves (enabled, effort) for this request and model, prioritizing request overrides.
774
+ pub(crate) fn effective_thinking(
775
+ config: &MintConfig,
776
+ request: &ChatRequest,
777
+ model: &str,
778
+ ) -> (bool, String) {
779
+ if !MintConfig::is_thinking_supported_for_model(&config.ai_provider, model) {
780
+ return (false, "medium".to_string());
781
+ }
782
+ let enabled = request
783
+ .thinking_enabled
784
+ .unwrap_or_else(|| config.resolved_thinking_enabled_for_model(&config.ai_provider, model));
785
+ let effort = request
786
+ .thinking_effort
787
+ .as_deref()
788
+ .map(str::to_ascii_lowercase)
789
+ .unwrap_or_else(|| config.resolved_thinking_effort_for_model(model).to_string());
790
+ (enabled, effort)
791
+ }
792
+
793
+ pub(crate) fn thinking_prompt_directive(effort: &str) -> &'static str {
794
+ match effort {
795
+ "low" => {
796
+ "\n\n[Thinking Mode: Enabled (Low Effort)]\nBefore providing your response or calling any tool, you MUST write a brief and concise (2-4 sentences) internal reasoning process enclosed strictly within <think>...</think> tags at the very beginning of your message content (never inside tool arguments), followed by your tool call or final answer outside the tags."
797
+ }
798
+ "high" => {
799
+ "\n\n[Thinking Mode: Enabled (High Effort)]\nBefore providing your response or calling any tool, you MUST write a thorough and detailed internal reasoning process, carefully analyzing edge cases and trade-offs, enclosed strictly within <think>...</think> tags at the very beginning of your message content (never inside tool arguments), followed by your tool call or final answer outside the tags."
800
+ }
801
+ "extra_high" => {
802
+ "\n\n[Thinking Mode: Enabled (Extra High Effort)]\nBefore providing your response or calling any tool, you MUST write an exhaustive, step-by-step internal reasoning process, verifying assumptions and alternative approaches in depth, enclosed strictly within <think>...</think> tags at the very beginning of your message content (never inside tool arguments), followed by your tool call or final answer outside the tags."
803
+ }
804
+ _ => {
805
+ "\n\n[Thinking Mode: Enabled (Medium Effort)]\nBefore providing your response or calling any tool, you MUST write a clear and structured internal reasoning process enclosed strictly within <think>...</think> tags at the very beginning of your message content (never inside tool arguments), followed by your tool call or final answer outside the tags."
806
+ }
807
+ }
808
+ }
809
+
810
+ /// Returns the effective system instruction for a request, appending a `<think>...</think>`
811
+ /// Chain-of-Thought directive when Thinking is enabled on a chat model that does not use
812
+ /// a native visible reasoning API parameter.
813
+ pub(crate) fn effective_system_instruction(
814
+ config: &MintConfig,
815
+ request: &ChatRequest,
816
+ provider: &str,
817
+ model: &str,
818
+ ) -> String {
819
+ let base = request.system_instruction.as_str();
820
+ if wants_agent_json(request) {
821
+ return base.to_string();
822
+ }
823
+ let (enabled, effort) = effective_thinking(config, request, model);
824
+ if enabled && !MintConfig::has_native_thinking_api(provider, model) {
825
+ format!("{base}{}", thinking_prompt_directive(&effort))
826
+ } else {
827
+ base.to_string()
828
+ }
829
+ }
830
+
831
+ /// Concatenates all `Text` blocks in a message (tool-use/result/image blocks
832
+ /// contribute nothing).
833
+ pub(super) fn message_text(message: &ChatMessage) -> String {
834
+ message
835
+ .content
836
+ .iter()
837
+ .filter_map(|block| match block {
838
+ ContentBlock::Text { text } => Some(text.as_str()),
839
+ _ => None,
840
+ })
841
+ .collect::<Vec<_>>()
842
+ .join("")
843
+ }
844
+
845
+ /// Splits a `data:<mime>;base64,<data>` URI into `(mime_type, base64_data)`,
846
+ /// validating the mime type starts with `expected_prefix` (e.g. `"image/"`).
847
+ pub(super) fn split_data_uri(
848
+ data_uri: &str,
849
+ expected_prefix: &str,
850
+ ) -> Result<(String, String), ChatError> {
851
+ data_uri
852
+ .strip_prefix("data:")
853
+ .and_then(|payload| payload.split_once(";base64,"))
854
+ .filter(|(mime, data)| mime.starts_with(expected_prefix) && !data.is_empty())
855
+ .map(|(mime, data)| (mime.to_owned(), data.to_owned()))
856
+ .ok_or(ChatError::InvalidAttachment)
857
+ }