@pheem49/mint 1.16.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 (984) 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 -72
  5. package/apps/mint-companion/src-tauri/src/compact.rs +69 -0
  6. package/apps/mint-companion/src-tauri/src/main.rs +218 -11
  7. package/apps/mint-companion/vite.config.ts +2 -1
  8. package/crates/mint-cli/src/agent/diff_render.rs +66 -44
  9. package/crates/mint-cli/src/agent/live_status.rs +64 -6
  10. package/crates/mint-cli/src/agent/markdown_render.rs +342 -5
  11. package/crates/mint-cli/src/agent.rs +202 -47
  12. package/crates/mint-cli/src/commands/agent.rs +223 -117
  13. package/crates/mint-cli/src/commands/browser.rs +114 -0
  14. package/crates/mint-cli/src/commands/config.rs +14 -1
  15. package/crates/mint-cli/src/commands/integrations.rs +3 -3
  16. package/crates/mint-cli/src/commands/knowledge.rs +16 -2
  17. package/crates/mint-cli/src/commands/mod.rs +15 -4
  18. package/crates/mint-cli/src/commands/tailnet_web.rs +3 -7
  19. package/crates/mint-cli/src/companion_navigation.rs +268 -0
  20. package/crates/mint-cli/src/interactive/chat_tui/dialog.rs +572 -0
  21. package/crates/mint-cli/src/interactive/chat_tui/mod.rs +1427 -0
  22. package/crates/mint-cli/src/interactive/chat_tui/render.rs +1524 -0
  23. package/crates/mint-cli/src/interactive/chat_tui/selection.rs +401 -0
  24. package/crates/mint-cli/src/interactive/chat_tui/state.rs +1833 -0
  25. package/crates/mint-cli/src/interactive/chat_tui/tests.rs +3414 -0
  26. package/crates/mint-cli/src/interactive/chat_tui/thought.rs +202 -0
  27. package/crates/mint-cli/src/interactive/chat_tui/transcript.rs +728 -0
  28. package/crates/mint-cli/src/interactive/chat_tui/watermark.rs +148 -0
  29. package/crates/mint-cli/src/interactive/command_ui.rs +92 -5
  30. package/crates/mint-cli/src/interactive/composer.rs +186 -0
  31. package/crates/mint-cli/src/interactive/format.rs +78 -3
  32. package/crates/mint-cli/src/interactive/input_box.rs +61 -1
  33. package/crates/mint-cli/src/interactive/resume_picker.rs +367 -25
  34. package/crates/mint-cli/src/interactive/slash_commands.rs +758 -373
  35. package/crates/mint-cli/src/interactive/tool_activity.rs +468 -56
  36. package/crates/mint-cli/src/interactive.rs +74 -5
  37. package/crates/mint-cli/src/main.rs +26 -2
  38. package/crates/mint-cli/src/markdown.rs +2 -37
  39. package/crates/mint-cli/src/onboard.rs +3897 -1321
  40. package/crates/mint-cli/src/setup.rs +1342 -624
  41. package/crates/mint-cli/src/terminal_theme.rs +159 -35
  42. package/crates/mint-cli/tests/library_intake.rs +1 -0
  43. package/crates/mint-cli/tests/mods_testing.rs +5 -1
  44. package/crates/mint-companion-protocol/src/lib.rs +113 -0
  45. package/crates/mint-core/src/agent/chat/anthropic.rs +318 -0
  46. package/crates/mint-core/src/agent/chat/custom.rs +149 -0
  47. package/crates/mint-core/src/agent/chat/gemini.rs +518 -0
  48. package/crates/mint-core/src/agent/chat/mod.rs +857 -0
  49. package/crates/mint-core/src/agent/chat/ollama.rs +321 -0
  50. package/crates/mint-core/src/agent/chat/openai.rs +582 -0
  51. package/crates/mint-core/src/agent/chat/streaming.rs +469 -0
  52. package/crates/mint-core/src/agent/chat/tests.rs +1479 -0
  53. package/crates/mint-core/src/agent/memory.rs +129 -8
  54. package/crates/mint-core/src/agent/safety.rs +56 -4
  55. package/crates/mint-core/src/agent/skill_inventory.rs +3 -1
  56. package/crates/mint-core/src/agent/skills.rs +1 -0
  57. package/crates/mint-core/src/agent/tasks.rs +2 -2
  58. package/crates/mint-core/src/api_server/routes/auth.rs +1 -4
  59. package/crates/mint-core/src/api_server/routes/chat.rs +53 -17
  60. package/crates/mint-core/src/api_server/routes/gemini_live.rs +14 -1
  61. package/crates/mint-core/src/api_server/routes/html_preview.rs +1 -8
  62. package/crates/mint-core/src/api_server/routes/misc.rs +116 -0
  63. package/crates/mint-core/src/api_server/routes/profile_oauth.rs +43 -26
  64. package/crates/mint-core/src/api_server/routes/sessions.rs +114 -76
  65. package/crates/mint-core/src/api_server/routes/skills_subagents.rs +11 -7
  66. package/crates/mint-core/src/api_server/routes/status_health.rs +18 -8
  67. package/crates/mint-core/src/api_server.rs +211 -65
  68. package/crates/mint-core/src/avatar_bridge.rs +5 -24
  69. package/crates/mint-core/src/browser/cdp.rs +92 -30
  70. package/crates/mint-core/src/browser/headless.rs +582 -0
  71. package/crates/mint-core/src/browser/input.rs +7 -62
  72. package/crates/mint-core/src/browser/interact.rs +0 -14
  73. package/crates/mint-core/src/browser/mod.rs +10 -30
  74. package/crates/mint-core/src/browser/navigate.rs +83 -44
  75. package/crates/mint-core/src/browser/runtime.rs +282 -0
  76. package/crates/mint-core/src/browser/session.rs +240 -400
  77. package/crates/mint-core/src/browser/workflow.rs +496 -0
  78. package/crates/mint-core/src/companion/navigation.rs +115 -0
  79. package/crates/mint-core/src/companion.rs +109 -1
  80. package/crates/mint-core/src/cron/store.rs +2 -2
  81. package/crates/mint-core/src/integrations/mcp.rs +6 -8
  82. package/crates/mint-core/src/integrations/mcp_result.rs +45 -0
  83. package/crates/mint-core/src/lib.rs +10 -11
  84. package/crates/mint-core/src/media/gemini_live.rs +636 -86
  85. package/crates/mint-core/src/media/image_proxy.rs +563 -0
  86. package/crates/mint-core/src/media/library/migration.rs +26 -0
  87. package/crates/mint-core/src/media/library/tests.rs +1 -1
  88. package/crates/mint-core/src/media/library/thumbnails.rs +8 -2
  89. package/crates/mint-core/src/media/pictures.rs +1 -1
  90. package/crates/mint-core/src/media/tool_images.rs +287 -0
  91. package/crates/mint-core/src/media/tts/playback.rs +609 -0
  92. package/crates/mint-core/src/media/tts.rs +1 -0
  93. package/crates/mint-core/src/media.rs +2 -0
  94. package/crates/mint-core/src/mods/manager.rs +101 -35
  95. package/crates/mint-core/src/orchestration/context_render.rs +878 -44
  96. package/crates/mint-core/src/orchestration/decision_parsing.rs +536 -39
  97. package/crates/mint-core/src/orchestration/diagnostics.rs +307 -0
  98. package/crates/mint-core/src/orchestration/full_skill_result_tests.rs +12 -0
  99. package/crates/mint-core/src/orchestration/image_regressions.rs +379 -0
  100. package/crates/mint-core/src/orchestration/loop_guard.rs +479 -0
  101. package/crates/mint-core/src/orchestration/mcp_outcome_tests.rs +864 -0
  102. package/crates/mint-core/src/orchestration/memory_skill.rs +88 -28
  103. package/crates/mint-core/src/orchestration/mod.rs +859 -4244
  104. package/crates/mint-core/src/orchestration/reliability.rs +581 -0
  105. package/crates/mint-core/src/orchestration/repeated_failures.rs +8 -0
  106. package/crates/mint-core/src/orchestration/review_regressions.rs +179 -28
  107. package/crates/mint-core/src/orchestration/tests.rs +1665 -0
  108. package/crates/mint-core/src/orchestration/tools/dispatch.rs +967 -0
  109. package/crates/mint-core/src/orchestration/tools/files.rs +32 -3
  110. package/crates/mint-core/src/orchestration/tools/mod.rs +5 -1
  111. package/crates/mint-core/src/orchestration/tools/planning.rs +102 -42
  112. package/crates/mint-core/src/orchestration/tools/plugins_mcp.rs +19 -4
  113. package/crates/mint-core/src/orchestration/tools/project.rs +1 -1
  114. package/crates/mint-core/src/orchestration/tools/shell.rs +157 -62
  115. package/crates/mint-core/src/orchestration/tools/web.rs +5 -1
  116. package/crates/mint-core/src/orchestration/trajectory_schema.rs +353 -0
  117. package/crates/mint-core/src/orchestration/truncation.rs +368 -0
  118. package/crates/mint-core/src/orchestration/types.rs +648 -0
  119. package/crates/mint-core/src/orchestration/verification.rs +36 -9
  120. package/crates/mint-core/src/orchestration/workspace_helpers.rs +76 -27
  121. package/crates/mint-core/src/prompts/agent.rs +69 -102
  122. package/crates/mint-core/src/prompts/tool_catalog.rs +104 -153
  123. package/crates/mint-core/src/search/files.rs +20 -0
  124. package/crates/mint-core/src/search/web_search.rs +44 -3
  125. package/crates/mint-core/src/slash/mod.rs +481 -55
  126. package/crates/mint-core/src/slash/models.rs +6 -1
  127. package/crates/mint-core/src/swarm/batch.rs +643 -0
  128. package/crates/mint-core/src/swarm/dossier.rs +152 -0
  129. package/crates/mint-core/src/swarm/labels.rs +100 -0
  130. package/crates/mint-core/src/swarm/mod.rs +158 -0
  131. package/crates/mint-core/src/swarm/permissions.rs +167 -0
  132. package/crates/mint-core/src/system/auth.rs +156 -7
  133. package/crates/mint-core/src/system/bg_shell.rs +78 -9
  134. package/crates/mint-core/src/system/config.rs +266 -35
  135. package/crates/mint-core/src/system/mint_web.rs +33 -12
  136. package/crates/mint-core/src/system/shell.rs +179 -10
  137. package/crates/mint-core/src/system/workspace.rs +204 -0
  138. package/crates/mint-core/tests/background_retention.rs +1 -1
  139. package/crates/mint-core/tests/memory_persistence.rs +27 -0
  140. package/crates/mint-mod-runtime/src/lib.rs +37 -23
  141. package/out/web/assets/ArtifactPreviewPanel-Cx6BVOOF.js +2 -0
  142. package/out/web/assets/ArtifactPreviewPanel-DAT_Y7i0.css +1 -0
  143. package/out/web/assets/CalculationCard-D2E74u5q.js +1 -0
  144. package/out/web/assets/CliSessionsView-ZbdtSjvK.css +1 -0
  145. package/out/web/assets/CliSessionsView-z5i6qGyb.js +1 -0
  146. package/out/web/assets/CodeReviewPage-BK2bOScd.css +1 -0
  147. package/out/web/assets/CodeReviewPage-Dp6zHuHu.js +2 -0
  148. package/out/web/assets/CommandPalette-DaKlzbKs.js +1 -0
  149. package/out/web/assets/ImageGenCard-BMCJHP9O.js +1 -0
  150. package/out/web/assets/ImageSearchCard-Cld_SOUg.js +1 -0
  151. package/out/web/assets/ImageStudioPanel-B5N7TjeB.js +1 -0
  152. package/out/web/assets/LibraryPage-CMB89rJ0.css +1 -0
  153. package/out/web/assets/LibraryPage-DbZEi4W_.js +2 -0
  154. package/out/web/assets/LinkedFoldersView-9B9DIPoZ.js +1 -0
  155. package/out/web/assets/McpRegistryPicker-DnXPlzI7.js +1 -0
  156. package/out/web/assets/McpServersView-CiNBNUl4.js +1 -0
  157. package/out/web/assets/MermaidCard-BBDNEXbY.js +2 -0
  158. package/out/web/assets/MintDashboard-C_UYPoNV.js +119 -0
  159. package/out/web/assets/MintDashboard-DTd8C_c8.css +1 -0
  160. package/out/web/assets/PlanCard-C2x5pajq.js +1 -0
  161. package/out/web/assets/PluginsView-DoTg9MVa.js +1 -0
  162. package/out/web/assets/RewindModal-B5oowrR0.js +1 -0
  163. package/out/web/assets/ScheduledTasksView-BD5DVPkZ.js +1 -0
  164. package/out/web/assets/SelectField-Cy6CghrU.js +1 -0
  165. package/out/web/assets/SelectField-lFEkkWCU.css +1 -0
  166. package/out/web/assets/SettingsWindow-BYl_oCFh.js +4 -0
  167. package/out/web/assets/SettingsWindow-Ckcgnjt8.css +1 -0
  168. package/out/web/assets/SkillsView-CIeWwq5z.js +2 -0
  169. package/out/web/assets/StockCard-DxUciKyp.js +1 -0
  170. package/out/web/assets/ToolSurfacePage-qBgs_VHq.css +1 -0
  171. package/out/web/assets/ToolSurfacePage-saVbVrkE.js +38 -0
  172. package/out/web/assets/UiFeatureCard-CdY0hGWy.js +1 -0
  173. package/out/web/assets/UiGridCard-BBK3I5hC.js +1 -0
  174. package/out/web/assets/UiMockupWidget-CpR9EDuc.js +1 -0
  175. package/out/web/assets/{VeoStudioPanel-DOj7_WR3.css → VeoStudioPanel-9kU9YLEN.css} +1 -1
  176. package/out/web/assets/VeoStudioPanel-Co6-wEWM.js +4 -0
  177. package/out/web/assets/WeatherCard-Buzi7Vfu.js +1 -0
  178. package/out/web/assets/WorkspacePanel-BfHKkulU.js +1 -0
  179. package/out/web/assets/WorkspacePdfPreview-Bi8Zzyir.js +2 -0
  180. package/out/web/assets/abnfDiagram-VCTEODGH-CMfR95N2.js +1 -0
  181. package/out/web/assets/architecture-7GRP2DOG-DhAM4HyO.js +1 -0
  182. package/out/web/assets/architectureDiagram-5GKGNRK7-Yk3TUJ_F.js +36 -0
  183. package/out/web/assets/{blockDiagram-NRAW4CY4-BrrqzHW5.js → blockDiagram-I7D4REHJ-X9fBgJik.js} +4 -4
  184. package/out/web/assets/c4Diagram-7LVT6UL2-Bk9x7N-8.js +38 -0
  185. package/out/web/assets/chunk-2Q5K7J3B-D2JLPQzF.js +1 -0
  186. package/out/web/assets/{chunk-TEH6E4GO-G4Cdlyrt.js → chunk-4HAMMTFA-Dx5fk1_0.js} +3 -3
  187. package/out/web/assets/{chunk-5VM5RSS4-BiPuE0io.js → chunk-5VM5RSS4-BZ2oBPYQ.js} +1 -1
  188. package/out/web/assets/chunk-75Z2AOVW-BszTQoM4.js +2 -0
  189. package/out/web/assets/chunk-DU6HZSFF-DK-As3rk.js +128 -0
  190. package/out/web/assets/chunk-F27PBJKO-LmncZZIS.js +1 -0
  191. package/out/web/assets/chunk-FOHPRMQF-JXEeeJ9r.js +161 -0
  192. package/out/web/assets/{chunk-GMAD6QVW-BuhEcgHq.js → chunk-GMAD6QVW-DSQ_sDRJ.js} +12 -12
  193. package/out/web/assets/chunk-GVQU2GXP-Cno3ulje.js +1 -0
  194. package/out/web/assets/chunk-IMKFNOWR-bIzfY9Jc.js +231 -0
  195. package/out/web/assets/chunk-JWPE2WC7-BeEMUFDw.js +1 -0
  196. package/out/web/assets/chunk-L3NEJ4N5-Dl6eOa0D.js +1 -0
  197. package/out/web/assets/chunk-OSK3NFVY-BaUGJ-cM.js +10 -0
  198. package/out/web/assets/chunk-P2QGCYS3-_066ndhH.js +1 -0
  199. package/out/web/assets/chunk-POPQ4Y6H-YT1AKhTv.js +1 -0
  200. package/out/web/assets/chunk-PWAF6VOD-CrEgDsxh.js +1 -0
  201. package/out/web/assets/chunk-SHT3W25Y-_KwaRLOl.js +168 -0
  202. package/out/web/assets/{chunk-SVP7TREG-Bu_yYb-V.js → chunk-SVP7TREG-BQ1qb32L.js} +1 -1
  203. package/out/web/assets/{chunk-LCL6LL3I-BhXV9zyH.js → chunk-TICWLB2K-BDyehCtg.js} +4 -4
  204. package/out/web/assets/chunk-XXDRQBXY-B9YlhOkE.js +1 -0
  205. package/out/web/assets/chunk-Y2CYZVJY-BxCWjNVG.js +1 -0
  206. package/out/web/assets/classDiagram-ZZMXUADV-BoSpzRCJ.js +1 -0
  207. package/out/web/assets/classDiagram-v2-VYDZK3BY-BoSpzRCJ.js +1 -0
  208. package/out/web/assets/config-2suVFPZg.css +1 -0
  209. package/out/web/assets/config-CkRCdkxn.js +2 -0
  210. package/out/web/assets/core-Mi3s5D4_.js +1 -0
  211. package/out/web/assets/cose-bilkent-JH36ORCC-qlna31ty.js +1 -0
  212. package/out/web/assets/cynefin-OW5HDTMX-Dl_31aaN.js +1 -0
  213. package/out/web/assets/cynefinDiagram-5FMLGOSQ-Bpir8jha.js +62 -0
  214. package/out/web/assets/cytoscape.esm-DWtAUG0r.js +321 -0
  215. package/out/web/assets/dagre-CnWJJM5f.js +1 -0
  216. package/out/web/assets/dagre-GXQ25YYZ-D9xu3nsq.js +4 -0
  217. package/out/web/assets/diagram-S7CK7UJ4-DeveoEeB.js +30 -0
  218. package/out/web/assets/diagram-UQ7AKVKN-p4xWRESV.js +41 -0
  219. package/out/web/assets/{diagram-VSXAHHWV-BJ86Ov2Y.js → diagram-VSXAHHWV-C7qz9NDF.js} +1 -1
  220. package/out/web/assets/diagram-VX7I27RA-DRiCa3Vd.js +24 -0
  221. package/out/web/assets/diagram-Z3DM3KII-Bt7zduQX.js +24 -0
  222. package/out/web/assets/dist-9ISJ7r-g.js +1 -0
  223. package/out/web/assets/dist-js-BV3mI7uw.js +1 -0
  224. package/out/web/assets/ebnfDiagram-PWID7BFC-DV9WpwdR.js +1 -0
  225. package/out/web/assets/{erDiagram-SSCWMZ5O-CzzmnZWT.js → erDiagram-RLTQ6QDP-BJ_nwG2m.js} +3 -3
  226. package/out/web/assets/event-D7D-EBy9.js +1 -0
  227. package/out/web/assets/eventmodeling-NTZA5JFV-BTBMrfVa.js +1 -0
  228. package/out/web/assets/flowDiagram-HODETNUW-Cdlq3cJL.js +1 -0
  229. package/out/web/assets/ganttDiagram-EL5Y4UJY-DQLdsXrC.js +292 -0
  230. package/out/web/assets/gitGraph-4MIJSDKK-CL1OEOQ_.js +1 -0
  231. package/out/web/assets/gitGraphDiagram-WWUBYQGX-8cSi42H7.js +106 -0
  232. package/out/web/assets/graphlib-Bh9gHply.js +1 -0
  233. package/out/web/assets/index-BocxnrDG.js +168 -0
  234. package/out/web/assets/index-yeh_r55a.css +1 -0
  235. package/out/web/assets/info-A6RAGUB7-C1zsX5sc.js +1 -0
  236. package/out/web/assets/infoDiagram-27XIBGKW-DsRYtIyI.js +2 -0
  237. package/out/web/assets/{ishikawaDiagram-5VMMS53U-s38yo28y.js → ishikawaDiagram-5VMMS53U-B2crwIXj.js} +4 -4
  238. package/out/web/assets/journeyDiagram-3NMN7TZE-BTH-GyVR.js +139 -0
  239. package/out/web/assets/jsx-runtime-DmUwWVTj.js +1 -0
  240. package/out/web/assets/{kanban-definition-3QL26DDD-DodjgySL.js → kanban-definition-UXKFOSKX-BeRfCnuA.js} +6 -6
  241. package/out/web/assets/katex-MCdQaEAD.js +257 -0
  242. package/out/web/assets/mermaid-parser.core-BJwgCBBc.js +7 -0
  243. package/out/web/assets/mermaid.core-CsDDodN8.js +44 -0
  244. package/out/web/assets/{mindmap-definition-FBJOCRG2-260WBDxZ.js → mindmap-definition-YA3MSWOX-B42efOQ1.js} +4 -4
  245. package/out/web/assets/modelManager-B3ZFMh8v.js +2 -0
  246. package/out/web/assets/oauthManager-Ch5Du2fF.js +1 -0
  247. package/out/web/assets/packet-AYTQ26CC-B6xUyE6r.js +1 -0
  248. package/out/web/assets/pdf-Bcahc2yC.js +56 -0
  249. package/out/web/assets/pdf.worker.min-Dkey6ZUl.mjs +27 -0
  250. package/out/web/assets/pegDiagram-XKGWAZYB-1LabVycL.js +1 -0
  251. package/out/web/assets/pie-WAS4IAKB-DJwo-nf_.js +1 -0
  252. package/out/web/assets/pieDiagram-E7YTZNPT-icmAqzy4.js +39 -0
  253. package/out/web/assets/{quadrantDiagram-AXDQQJYC-B8D3gU5d.js → quadrantDiagram-AXDQQJYC-CZ-wEx41.js} +4 -4
  254. package/out/web/assets/radar-RG4KPBEZ-D7d-Xriw.js +1 -0
  255. package/out/web/assets/railroad-74A4TZTK-viuhdYbs.js +1 -0
  256. package/out/web/assets/railroad-abnf-HS5TGJTU-DNXiCNea.js +1 -0
  257. package/out/web/assets/railroad-ebnf-LZEXJU2U-nkSu1-0C.js +1 -0
  258. package/out/web/assets/railroad-peg-WCYAUIDC-BhbMeQzz.js +1 -0
  259. package/out/web/assets/railroadDiagram-O6MQD6OU-CLpG1jUX.js +1 -0
  260. package/out/web/assets/{requirementDiagram-EFPCY7ZU-D2X-Ewik.js → requirementDiagram-BXWQKSXE-kRDrvWrc.js} +3 -3
  261. package/out/web/assets/rolldown-runtime-CNOWq4Kh.js +1 -0
  262. package/out/web/assets/rough.esm-CNnFptCR.js +1 -0
  263. package/out/web/assets/sankeyDiagram-P5KCCOFB-DxAwmq4x.js +40 -0
  264. package/out/web/assets/sequenceDiagram-WJ2MYXX4-aLgY-6Wz.js +162 -0
  265. package/out/web/assets/sizeCapture-INFHLROL-C8Q8DbV5.js +1 -0
  266. package/out/web/assets/src-CRDfRlHM.js +1 -0
  267. package/out/web/assets/stateDiagram-D77RDMKH-D5QWfOhp.js +1 -0
  268. package/out/web/assets/stateDiagram-v2-MP3YSRHH-D6OiZoAP.js +1 -0
  269. package/out/web/assets/studio-BBZQ_-QK.js +1 -0
  270. package/out/web/assets/swimlanes-42K2YHIH-CRs5UyA-.js +1 -0
  271. package/out/web/assets/swimlanesDiagram-VR7AAH4N-UgEXIsV0.js +8 -0
  272. package/out/web/assets/syntaxHighlight-C-wAGT84.js +3 -0
  273. package/out/web/assets/timeline-definition-24CTP7MA-Denqd9qb.js +120 -0
  274. package/out/web/assets/treeView-Q6P3EWNA-0my3Z3s4.js +1 -0
  275. package/out/web/assets/treemap-WGGIJYW6-B_EjkLYq.js +1 -0
  276. package/out/web/assets/vennDiagram-4TSXK5OY-DxVg3Ewr.js +34 -0
  277. package/out/web/assets/wardley-WFR3VGLG-CGD-Tj4p.js +1 -0
  278. package/out/web/assets/{wardleyDiagram-VM6X3IG4-DAdXELbQ.js → wardleyDiagram-VM6X3IG4-CmfS39Xm.js} +35 -35
  279. package/out/web/assets/window-CkUOP7t0.js +1 -0
  280. package/out/web/assets/workspaces-SYPrmbgC.js +1 -0
  281. package/out/web/assets/{xychartDiagram-S5SC5T6Z-IZIXf3Vo.js → xychartDiagram-S5SC5T6Z-BVM9bKis.js} +4 -4
  282. package/out/web/index-web.html +4 -4
  283. package/out/web/index.html +4 -4
  284. package/out/web/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
  285. package/out/web/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
  286. package/out/web/pdfjs/cmaps/78-H.bcmap +0 -0
  287. package/out/web/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
  288. package/out/web/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
  289. package/out/web/pdfjs/cmaps/78-V.bcmap +0 -0
  290. package/out/web/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
  291. package/out/web/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
  292. package/out/web/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
  293. package/out/web/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
  294. package/out/web/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
  295. package/out/web/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
  296. package/out/web/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
  297. package/out/web/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
  298. package/out/web/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
  299. package/out/web/pdfjs/cmaps/Add-H.bcmap +0 -0
  300. package/out/web/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
  301. package/out/web/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
  302. package/out/web/pdfjs/cmaps/Add-V.bcmap +0 -0
  303. package/out/web/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
  304. package/out/web/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
  305. package/out/web/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
  306. package/out/web/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
  307. package/out/web/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
  308. package/out/web/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
  309. package/out/web/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
  310. package/out/web/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  311. package/out/web/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
  312. package/out/web/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
  313. package/out/web/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
  314. package/out/web/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
  315. package/out/web/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
  316. package/out/web/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
  317. package/out/web/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  318. package/out/web/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
  319. package/out/web/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
  320. package/out/web/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
  321. package/out/web/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
  322. package/out/web/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
  323. package/out/web/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
  324. package/out/web/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
  325. package/out/web/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  326. package/out/web/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
  327. package/out/web/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
  328. package/out/web/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
  329. package/out/web/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  330. package/out/web/pdfjs/cmaps/B5-H.bcmap +0 -0
  331. package/out/web/pdfjs/cmaps/B5-V.bcmap +0 -0
  332. package/out/web/pdfjs/cmaps/B5pc-H.bcmap +0 -0
  333. package/out/web/pdfjs/cmaps/B5pc-V.bcmap +0 -0
  334. package/out/web/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
  335. package/out/web/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
  336. package/out/web/pdfjs/cmaps/CNS1-H.bcmap +0 -0
  337. package/out/web/pdfjs/cmaps/CNS1-V.bcmap +0 -0
  338. package/out/web/pdfjs/cmaps/CNS2-H.bcmap +0 -0
  339. package/out/web/pdfjs/cmaps/CNS2-V.bcmap +3 -0
  340. package/out/web/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
  341. package/out/web/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
  342. package/out/web/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
  343. package/out/web/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
  344. package/out/web/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
  345. package/out/web/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
  346. package/out/web/pdfjs/cmaps/EUC-H.bcmap +0 -0
  347. package/out/web/pdfjs/cmaps/EUC-V.bcmap +0 -0
  348. package/out/web/pdfjs/cmaps/Ext-H.bcmap +0 -0
  349. package/out/web/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
  350. package/out/web/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
  351. package/out/web/pdfjs/cmaps/Ext-V.bcmap +0 -0
  352. package/out/web/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
  353. package/out/web/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
  354. package/out/web/pdfjs/cmaps/GB-H.bcmap +4 -0
  355. package/out/web/pdfjs/cmaps/GB-V.bcmap +0 -0
  356. package/out/web/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
  357. package/out/web/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
  358. package/out/web/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
  359. package/out/web/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
  360. package/out/web/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
  361. package/out/web/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
  362. package/out/web/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
  363. package/out/web/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
  364. package/out/web/pdfjs/cmaps/GBT-H.bcmap +0 -0
  365. package/out/web/pdfjs/cmaps/GBT-V.bcmap +0 -0
  366. package/out/web/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
  367. package/out/web/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
  368. package/out/web/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
  369. package/out/web/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
  370. package/out/web/pdfjs/cmaps/H.bcmap +0 -0
  371. package/out/web/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
  372. package/out/web/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
  373. package/out/web/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
  374. package/out/web/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
  375. package/out/web/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
  376. package/out/web/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
  377. package/out/web/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
  378. package/out/web/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
  379. package/out/web/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
  380. package/out/web/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
  381. package/out/web/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
  382. package/out/web/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
  383. package/out/web/pdfjs/cmaps/Hankaku.bcmap +0 -0
  384. package/out/web/pdfjs/cmaps/Hiragana.bcmap +0 -0
  385. package/out/web/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
  386. package/out/web/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
  387. package/out/web/pdfjs/cmaps/KSC-H.bcmap +0 -0
  388. package/out/web/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
  389. package/out/web/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
  390. package/out/web/pdfjs/cmaps/KSC-V.bcmap +0 -0
  391. package/out/web/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
  392. package/out/web/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  393. package/out/web/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  394. package/out/web/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
  395. package/out/web/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
  396. package/out/web/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
  397. package/out/web/pdfjs/cmaps/Katakana.bcmap +0 -0
  398. package/out/web/pdfjs/cmaps/LICENSE +36 -0
  399. package/out/web/pdfjs/cmaps/NWP-H.bcmap +0 -0
  400. package/out/web/pdfjs/cmaps/NWP-V.bcmap +0 -0
  401. package/out/web/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
  402. package/out/web/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
  403. package/out/web/pdfjs/cmaps/Roman.bcmap +0 -0
  404. package/out/web/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  405. package/out/web/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  406. package/out/web/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  407. package/out/web/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  408. package/out/web/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  409. package/out/web/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  410. package/out/web/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  411. package/out/web/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  412. package/out/web/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
  413. package/out/web/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
  414. package/out/web/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
  415. package/out/web/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
  416. package/out/web/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
  417. package/out/web/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
  418. package/out/web/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
  419. package/out/web/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
  420. package/out/web/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  421. package/out/web/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  422. package/out/web/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  423. package/out/web/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  424. package/out/web/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  425. package/out/web/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  426. package/out/web/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  427. package/out/web/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  428. package/out/web/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  429. package/out/web/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  430. package/out/web/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  431. package/out/web/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  432. package/out/web/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  433. package/out/web/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  434. package/out/web/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  435. package/out/web/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  436. package/out/web/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  437. package/out/web/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  438. package/out/web/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  439. package/out/web/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  440. package/out/web/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  441. package/out/web/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  442. package/out/web/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  443. package/out/web/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
  444. package/out/web/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
  445. package/out/web/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
  446. package/out/web/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
  447. package/out/web/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
  448. package/out/web/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
  449. package/out/web/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
  450. package/out/web/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
  451. package/out/web/pdfjs/cmaps/V.bcmap +0 -0
  452. package/out/web/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
  453. package/out/web/pdfjs/standard_fonts/FoxitDingbats.pfb +0 -0
  454. package/out/web/pdfjs/standard_fonts/FoxitFixed.pfb +0 -0
  455. package/out/web/pdfjs/standard_fonts/FoxitFixedBold.pfb +0 -0
  456. package/out/web/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
  457. package/out/web/pdfjs/standard_fonts/FoxitFixedItalic.pfb +0 -0
  458. package/out/web/pdfjs/standard_fonts/FoxitSerif.pfb +0 -0
  459. package/out/web/pdfjs/standard_fonts/FoxitSerifBold.pfb +0 -0
  460. package/out/web/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
  461. package/out/web/pdfjs/standard_fonts/FoxitSerifItalic.pfb +0 -0
  462. package/out/web/pdfjs/standard_fonts/FoxitSymbol.pfb +0 -0
  463. package/out/web/pdfjs/standard_fonts/LICENSE_FOXIT +27 -0
  464. package/out/web/pdfjs/standard_fonts/LICENSE_LIBERATION +363 -0
  465. package/out/web/pdfjs/standard_fonts/LiberationSans-Bold.ttf +0 -0
  466. package/out/web/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
  467. package/out/web/pdfjs/standard_fonts/LiberationSans-Italic.ttf +0 -0
  468. package/out/web/pdfjs/standard_fonts/LiberationSans-Regular.ttf +0 -0
  469. package/out/web/pdfjs/wasm/LICENSE_JBIG2 +196 -0
  470. package/out/web/pdfjs/wasm/LICENSE_OPENJPEG +39 -0
  471. package/out/web/pdfjs/wasm/LICENSE_PDFJS_JBIG2 +13 -0
  472. package/out/web/pdfjs/wasm/LICENSE_PDFJS_OPENJPEG +22 -0
  473. package/out/web/pdfjs/wasm/LICENSE_PDFJS_QCMS +19 -0
  474. package/out/web/pdfjs/wasm/LICENSE_QCMS +21 -0
  475. package/out/web/pdfjs/wasm/jbig2.wasm +0 -0
  476. package/out/web/pdfjs/wasm/jbig2_nowasm_fallback.js +15 -0
  477. package/out/web/pdfjs/wasm/openjpeg.wasm +0 -0
  478. package/out/web/pdfjs/wasm/openjpeg_nowasm_fallback.js +17 -0
  479. package/out/web/pdfjs/wasm/qcms_bg.wasm +0 -0
  480. package/out/web/pdfjs/wasm/quickjs-eval.js +16 -0
  481. package/out/web/pdfjs/wasm/quickjs-eval.wasm +0 -0
  482. package/package.json +6 -4
  483. package/shared-modules/CODEOWNERS +3 -3
  484. package/shared-modules/SDL2/SDL2_mixer.json +27 -0
  485. package/shared-modules/appstream/README.md +13 -0
  486. package/shared-modules/appstream/appstream-compose.json +111 -0
  487. package/shared-modules/dbus-glib/dbus-glib.json +2 -2
  488. package/shared-modules/libportal/libportal.json +23 -0
  489. package/shared-modules/linux-audio/fluidsynth2-static.json +2 -2
  490. package/shared-modules/linux-audio/fluidsynth2.json +2 -2
  491. package/shared-modules/linux-audio/libinstpatch.json +2 -2
  492. package/shared-modules/linux-audio/liblo-static.json +3 -0
  493. package/shared-modules/luajit/luajit.json +2 -2
  494. package/shared-modules/squashfs-tools/squashfs-tools.json +0 -3
  495. package/slash-commands.json +14 -3
  496. package/src/bin/index.js +4 -1
  497. package/src/bin/pdfjs-assets.mjs +31 -0
  498. package/src/renderer/node_modules/.vite/vitest/CACHEDIR.TAG +3 -0
  499. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  500. package/src/renderer/shared/agentProgress.ts +62 -5
  501. package/src/renderer/shared/components/Settings/AgentsTab.tsx +70 -6
  502. package/src/renderer/shared/components/Settings/AudioTab.tsx +1 -1
  503. package/src/renderer/shared/components/Settings/AutomationTab.tsx +32 -29
  504. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +32 -12
  505. package/src/renderer/shared/components/Settings/CustomProviderLogoPicker.test.tsx +65 -0
  506. package/src/renderer/shared/components/Settings/CustomProviderLogoPicker.tsx +456 -0
  507. package/src/renderer/shared/components/Settings/GeneralTab.tsx +65 -94
  508. package/src/renderer/shared/components/Settings/PluginsTab.tsx +101 -24
  509. package/src/renderer/shared/components/Settings/ProfileTab.tsx +1 -1
  510. package/src/renderer/shared/components/Settings/SettingsWindow.tsx +796 -0
  511. package/src/renderer/shared/components/Settings/ThemeTab.test.tsx +45 -0
  512. package/src/renderer/shared/components/Settings/ThemeTab.tsx +119 -4
  513. package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +270 -0
  514. package/src/renderer/shared/components/activity/AgentActivityTable.tsx +582 -0
  515. package/src/renderer/shared/components/activity/AskUserActivityCard.test.tsx +122 -0
  516. package/src/renderer/shared/components/activity/AskUserActivityCard.tsx +70 -0
  517. package/src/renderer/shared/components/{McpToolResults.tsx → activity/McpToolResults.tsx} +69 -13
  518. package/src/renderer/shared/components/{PlanChecklistWidget.tsx → activity/PlanChecklistWidget.tsx} +2 -2
  519. package/src/renderer/shared/components/activity/RunSummaryDashboard.test.tsx +127 -0
  520. package/src/renderer/shared/components/activity/RunSummaryDashboard.tsx +222 -0
  521. package/src/renderer/shared/components/activity/SubagentDagView.test.ts +123 -0
  522. package/src/renderer/shared/components/{SubagentDagView.tsx → activity/SubagentDagView.tsx} +104 -50
  523. package/src/renderer/shared/components/activity/SubagentStatusStrip.test.tsx +82 -0
  524. package/src/renderer/shared/components/activity/SubagentStatusStrip.tsx +107 -0
  525. package/src/renderer/shared/components/activity/ViewedImages.tsx +179 -0
  526. package/src/renderer/shared/components/{AuthGate.tsx → app/AuthGate.tsx} +3 -3
  527. package/src/renderer/shared/components/{ChunkErrorBoundary.tsx → app/ChunkErrorBoundary.tsx} +13 -13
  528. package/src/renderer/shared/components/{CommandPalette.tsx → app/CommandPalette.tsx} +30 -30
  529. package/src/renderer/shared/components/{DashboardSidebar.tsx → app/DashboardSidebar.tsx} +7 -5
  530. package/src/renderer/shared/components/{DesktopTitlebar.tsx → app/DesktopTitlebar.tsx} +9 -26
  531. package/src/renderer/shared/components/{ErrorNotice.tsx → app/ErrorNotice.tsx} +1 -1
  532. package/src/renderer/shared/components/{MintDashboard.tsx → app/MintDashboard.tsx} +217 -228
  533. package/src/renderer/shared/components/{ModalErrorBoundary.tsx → app/ModalErrorBoundary.tsx} +8 -8
  534. package/src/renderer/shared/components/{CalculationCard.tsx → cards/CalculationCard.tsx} +16 -30
  535. package/src/renderer/shared/components/{ImageGenCard.tsx → cards/ImageGenCard.tsx} +11 -24
  536. package/src/renderer/shared/components/{ImageSearchCard.tsx → cards/ImageSearchCard.tsx} +11 -24
  537. package/src/renderer/shared/components/{MermaidCard.tsx → cards/MermaidCard.tsx} +38 -22
  538. package/src/renderer/shared/components/cards/PlanCard.test.tsx +41 -0
  539. package/src/renderer/shared/components/cards/PlanCard.tsx +32 -0
  540. package/src/renderer/shared/components/cards/ReleaseNotesCard.test.tsx +29 -0
  541. package/src/renderer/shared/components/cards/ReleaseNotesCard.tsx +137 -0
  542. package/src/renderer/shared/components/{StockCard.tsx → cards/StockCard.tsx} +22 -38
  543. package/src/renderer/shared/components/cards/ToolResultCards.tsx +94 -0
  544. package/src/renderer/shared/components/{UiFeatureCard.tsx → cards/UiFeatureCard.tsx} +6 -6
  545. package/src/renderer/shared/components/{UiGridCard.tsx → cards/UiGridCard.tsx} +3 -3
  546. package/src/renderer/shared/components/{UiMockupWidget.tsx → cards/UiMockupWidget.tsx} +4 -4
  547. package/src/renderer/shared/components/{WeatherCard.tsx → cards/WeatherCard.tsx} +6 -6
  548. package/src/renderer/shared/components/{ApprovalCard.test.tsx → chat/ApprovalCard.test.tsx} +12 -1
  549. package/src/renderer/shared/components/{ApprovalCard.tsx → chat/ApprovalCard.tsx} +26 -7
  550. package/src/renderer/shared/components/chat/ArtifactCards.test.tsx +69 -0
  551. package/src/renderer/shared/components/chat/ArtifactCards.tsx +238 -0
  552. package/src/renderer/shared/components/chat/AttachedDocumentCard.test.tsx +39 -0
  553. package/src/renderer/shared/components/chat/AttachedDocumentCard.tsx +220 -0
  554. package/src/renderer/shared/components/chat/ChatCodeBlock.test.tsx +33 -0
  555. package/src/renderer/shared/components/{ChatCodeBlock.tsx → chat/ChatCodeBlock.tsx} +16 -2
  556. package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +183 -0
  557. package/src/renderer/shared/components/chat/ChatMessageItem.tsx +388 -0
  558. package/src/renderer/shared/components/chat/ChatModeSelector.css +227 -0
  559. package/src/renderer/shared/components/chat/ChatModeSelector.test.tsx +45 -0
  560. package/src/renderer/shared/components/chat/ChatModeSelector.tsx +198 -0
  561. package/src/renderer/shared/components/chat/ChatPanel.test.tsx +53 -0
  562. package/src/renderer/shared/components/{ChatPanel.tsx → chat/ChatPanel.tsx} +462 -537
  563. package/src/renderer/shared/components/{CollapsiblePasteInput.tsx → chat/CollapsiblePasteInput.tsx} +2 -2
  564. package/src/renderer/shared/components/{DiffReviewModal.tsx → chat/DiffReviewModal.tsx} +44 -44
  565. package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +165 -0
  566. package/src/renderer/shared/components/{GeminiLiveOverlay.tsx → chat/GeminiLiveOverlay.tsx} +47 -5
  567. package/src/renderer/shared/components/{ModelSelectorPopover.tsx → chat/ModelSelectorPopover.tsx} +44 -16
  568. package/src/renderer/shared/components/chat/QueuedMessagesBanner.test.tsx +89 -0
  569. package/src/renderer/shared/components/chat/QueuedMessagesBanner.tsx +201 -0
  570. package/src/renderer/shared/components/{RewindModal.tsx → chat/RewindModal.tsx} +14 -14
  571. package/src/renderer/shared/components/{SlashChoicePanel.tsx → chat/SlashChoicePanel.tsx} +2 -2
  572. package/src/renderer/shared/components/{SlashSuggestions.test.tsx → chat/SlashSuggestions.test.tsx} +14 -0
  573. package/src/renderer/shared/components/{SlashSuggestions.tsx → chat/SlashSuggestions.tsx} +4 -4
  574. package/src/renderer/shared/components/chat/SourcesBlock.test.tsx +53 -0
  575. package/src/renderer/shared/components/{SourcesBlock.tsx → chat/SourcesBlock.tsx} +262 -123
  576. package/src/renderer/shared/components/{ThinkingBlock.tsx → chat/ThinkingBlock.tsx} +1 -1
  577. package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +130 -0
  578. package/src/renderer/shared/components/chat/TurnFileChanges.tsx +463 -0
  579. package/src/renderer/shared/components/{VoicePill.css → chat/VoicePill.css} +1 -1
  580. package/src/renderer/shared/components/{VoicePill.jsx → chat/VoicePill.jsx} +3 -2
  581. package/src/renderer/shared/components/{CliSessionsView.test.tsx → extensions/CliSessionsView.test.tsx} +2 -2
  582. package/src/renderer/shared/components/{CliSessionsView.tsx → extensions/CliSessionsView.tsx} +19 -16
  583. package/src/renderer/shared/components/{LinkedFoldersView.tsx → extensions/LinkedFoldersView.tsx} +3 -3
  584. package/src/renderer/shared/components/{McpRegistryPicker.tsx → extensions/McpRegistryPicker.tsx} +6 -6
  585. package/src/renderer/shared/components/{McpServersView.tsx → extensions/McpServersView.tsx} +105 -36
  586. package/src/renderer/shared/components/{McpToolAllowlist.tsx → extensions/McpToolAllowlist.tsx} +2 -2
  587. package/src/renderer/shared/components/{ModUi.test.tsx → extensions/ModUi.test.tsx} +9 -4
  588. package/src/renderer/shared/components/{ModsPanel.tsx → extensions/ModsPanel.tsx} +59 -7
  589. package/src/renderer/shared/components/{PluginsView.tsx → extensions/PluginsView.tsx} +6 -6
  590. package/src/renderer/shared/components/{ScheduledTasksView.tsx → extensions/ScheduledTasksView.tsx} +5 -5
  591. package/src/renderer/shared/components/extensions/SkillsView.test.tsx +31 -0
  592. package/src/renderer/shared/components/{SkillsView.tsx → extensions/SkillsView.tsx} +20 -4
  593. package/src/renderer/shared/components/{ImageStudioPanel.tsx → studio/ImageStudioPanel.tsx} +6 -6
  594. package/src/renderer/shared/components/{LibraryItemCard.tsx → studio/LibraryItemCard.tsx} +2 -2
  595. package/src/renderer/shared/components/{LibraryPage.tsx → studio/LibraryPage.tsx} +53 -10
  596. package/src/renderer/shared/components/{LibraryPreview.tsx → studio/LibraryPreview.tsx} +2 -2
  597. package/src/renderer/shared/components/{VeoStudioPanel.tsx → studio/VeoStudioPanel.tsx} +7 -7
  598. package/src/renderer/shared/components/{GlideSelect.css → ui/GlideSelect.css} +7 -6
  599. package/src/renderer/shared/components/ui/ProviderBrandIcon.test.tsx +67 -0
  600. package/src/renderer/shared/components/ui/ProviderBrandIcon.tsx +167 -0
  601. package/src/renderer/shared/components/ui/SelectField.tsx +506 -0
  602. package/src/renderer/shared/components/ui/error-recovery.css +10 -10
  603. package/src/renderer/shared/components/ui/error-recovery.tsx +1 -1
  604. package/src/renderer/shared/components/{ArtifactPreviewPanel.tsx → workspace/ArtifactPreviewPanel.tsx} +374 -118
  605. package/src/renderer/shared/components/workspace/BackgroundTerminals.test.tsx +63 -0
  606. package/src/renderer/shared/components/{BackgroundTerminals.tsx → workspace/BackgroundTerminals.tsx} +40 -9
  607. package/src/renderer/shared/components/workspace/CodeReviewPage.test.tsx +50 -0
  608. package/src/renderer/shared/components/{CodeReviewPage.tsx → workspace/CodeReviewPage.tsx} +124 -24
  609. package/src/renderer/shared/components/{GitBranchSelector.tsx → workspace/GitBranchSelector.tsx} +80 -24
  610. package/src/renderer/shared/components/workspace/SessionSources.tsx +113 -0
  611. package/src/renderer/shared/components/{TerminalDock.tsx → workspace/TerminalDock.tsx} +5 -2
  612. package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +81 -0
  613. package/src/renderer/shared/components/{ToolSurfacePage.tsx → workspace/ToolSurfacePage.tsx} +361 -90
  614. package/src/renderer/shared/components/{ToolsPanelToggle.tsx → workspace/ToolsPanelToggle.tsx} +1 -1
  615. package/src/renderer/shared/components/workspace/WorkspaceFileInfo.tsx +28 -0
  616. package/src/renderer/shared/components/workspace/WorkspaceMediaPreview.tsx +17 -0
  617. package/src/renderer/shared/components/{WorkspacePaneResizer.tsx → workspace/WorkspacePaneResizer.tsx} +1 -1
  618. package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +308 -0
  619. package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +101 -0
  620. package/src/renderer/shared/components/{WorkspacePreviewActions.tsx → workspace/WorkspacePreviewActions.tsx} +3 -3
  621. package/src/renderer/shared/components/{WorkspaceSelector.tsx → workspace/WorkspaceSelector.tsx} +99 -7
  622. package/src/renderer/shared/components/{background-terminals.css → workspace/background-terminals.css} +11 -11
  623. package/src/renderer/shared/components/{workspace-selector.css → workspace/workspace-selector.css} +84 -2
  624. package/src/renderer/shared/constants/config.ts +6 -3
  625. package/src/renderer/shared/constants/models.test.ts +51 -0
  626. package/src/renderer/shared/constants/models.ts +139 -32
  627. package/src/renderer/shared/conversation/coordinator.test.ts +46 -0
  628. package/src/renderer/shared/conversation/coordinator.ts +50 -2
  629. package/src/renderer/shared/conversation/lifecycle.test.ts +254 -4
  630. package/src/renderer/shared/conversation/lifecycle.ts +268 -12
  631. package/src/renderer/shared/conversation/progressiveStream.test.ts +83 -0
  632. package/src/renderer/shared/conversation/progressiveStream.ts +219 -0
  633. package/src/renderer/shared/conversation/projectNavigation.test.ts +18 -0
  634. package/src/renderer/shared/conversation/projectNavigation.ts +11 -0
  635. package/src/renderer/shared/conversation/transport.test.ts +20 -0
  636. package/src/renderer/shared/conversation/transport.ts +51 -9
  637. package/src/renderer/shared/conversation/useConversationCoordinator.ts +42 -12
  638. package/src/renderer/shared/css/agent-observability.css +205 -11
  639. package/src/renderer/shared/css/chat-ui-cards.css +48 -48
  640. package/src/renderer/shared/css/code-review.css +139 -110
  641. package/src/renderer/shared/css/code-sessions.css +56 -27
  642. package/src/renderer/shared/css/collapsed-paste.css +1 -1
  643. package/src/renderer/shared/css/error-notice.css +1 -1
  644. package/src/renderer/shared/css/file-changes.css +298 -51
  645. package/src/renderer/shared/css/library.css +105 -10
  646. package/src/renderer/shared/css/management-views.css +79 -38
  647. package/src/renderer/shared/css/mcp-results.css +48 -5
  648. package/src/renderer/shared/css/model-selector.css +18 -18
  649. package/src/renderer/shared/css/mods.css +7 -7
  650. package/src/renderer/shared/css/notification-notice.css +2 -2
  651. package/src/renderer/shared/css/queued-messages.css +221 -0
  652. package/src/renderer/shared/css/session-sources.css +12 -7
  653. package/src/renderer/shared/css/settings/agents.css +2 -2
  654. package/src/renderer/{src → shared}/css/settings/base.css +101 -107
  655. package/src/renderer/shared/css/settings/general.css +397 -100
  656. package/src/renderer/shared/css/settings/plugins.css +8 -8
  657. package/src/renderer/shared/css/settings/profile.css +5 -5
  658. package/src/renderer/shared/css/settings/theme.css +49 -37
  659. package/src/renderer/shared/css/slash-choice.css +2 -2
  660. package/src/renderer/shared/css/slash-suggestions.css +3 -3
  661. package/src/renderer/shared/css/sources-block.css +41 -2
  662. package/src/renderer/shared/css/tools-panel.css +2 -2
  663. package/src/renderer/shared/css/ui-tailwind.css +10 -2
  664. package/src/renderer/shared/css/veo-studio.css +2 -2
  665. package/src/renderer/shared/css/viewed-images.css +93 -4
  666. package/src/renderer/shared/css/workspace-file-info.css +12 -0
  667. package/src/renderer/shared/css/workspace-media-preview.css +13 -0
  668. package/src/renderer/shared/env.d.ts +9 -0
  669. package/src/renderer/shared/platform.test.ts +143 -0
  670. package/src/renderer/shared/platform.ts +12 -5
  671. package/src/renderer/shared/speech/player.ts +160 -0
  672. package/src/renderer/shared/speech/types.ts +11 -0
  673. package/src/renderer/shared/theme/themeManager.test.ts +37 -0
  674. package/src/renderer/shared/theme/themeManager.ts +24 -5
  675. package/src/renderer/shared/theme/themes.ts +71 -4
  676. package/src/renderer/shared/theme/tokens.css +452 -53
  677. package/src/renderer/shared/types.ts +57 -2
  678. package/src/renderer/shared/utils/agentActivity.test.ts +633 -8
  679. package/src/renderer/shared/utils/agentActivity.ts +776 -59
  680. package/src/renderer/shared/utils/artifactBrowser.test.ts +37 -32
  681. package/src/renderer/shared/utils/artifactBrowser.ts +18 -20
  682. package/src/renderer/shared/utils/artifacts.ts +55 -0
  683. package/src/renderer/shared/utils/attachedDocument.test.ts +73 -0
  684. package/src/renderer/shared/utils/attachedDocument.ts +49 -0
  685. package/src/renderer/shared/utils/contextCompaction.test.tsx +1 -1
  686. package/src/renderer/shared/utils/externalLinks.ts +91 -0
  687. package/src/renderer/shared/utils/fileIcons.test.ts +59 -0
  688. package/src/renderer/shared/utils/fileIcons.ts +149 -16
  689. package/src/renderer/shared/utils/lazyWithRetry.ts +13 -0
  690. package/src/renderer/shared/utils/markdown.tsx +238 -51
  691. package/src/renderer/shared/utils/markdown.webImages.test.tsx +140 -2
  692. package/src/renderer/shared/utils/mcpResults.test.tsx +53 -1
  693. package/src/renderer/shared/utils/pdfRange.ts +16 -0
  694. package/src/renderer/shared/utils/previewObjectUrls.ts +20 -0
  695. package/src/renderer/shared/utils/sessionSources.test.ts +41 -1
  696. package/src/renderer/shared/utils/sessionSources.ts +2 -0
  697. package/src/renderer/shared/utils/slashCommandProcessor.ts +7 -1
  698. package/src/renderer/shared/utils/speech.ts +18 -4
  699. package/src/renderer/shared/utils/syntaxHighlight.test.tsx +11 -1
  700. package/src/renderer/shared/utils/syntaxHighlight.tsx +51 -3
  701. package/src/renderer/shared/utils/useGeminiLiveVoice.ts +324 -51
  702. package/src/renderer/shared/utils/useNativeVoiceInput.ts +19 -5
  703. package/src/renderer/shared/utils/useSessionSources.ts +4 -0
  704. package/src/renderer/shared/utils/viewedImages.test.ts +129 -0
  705. package/src/renderer/shared/utils/viewedImages.ts +81 -10
  706. package/src/renderer/shared/utils/workspaceDrag.test.ts +153 -0
  707. package/src/renderer/shared/utils/workspaceDrag.ts +172 -0
  708. package/src/renderer/shared/utils/workspaceImage.ts +17 -0
  709. package/src/renderer/shared/utils/workspaceListPreferences.test.ts +64 -0
  710. package/src/renderer/shared/utils/workspaceListPreferences.ts +54 -0
  711. package/src/renderer/shared/utils/workspaceMedia.ts +49 -0
  712. package/src/renderer/shared/utils/workspacePreview.test.ts +147 -2
  713. package/src/renderer/shared/utils/workspacePreview.ts +34 -3
  714. package/src/renderer/shared/utils/workspaces.ts +3 -2
  715. package/src/renderer/src/App.tsx +20 -16
  716. package/src/renderer/src/agentProgress.ts +1 -0
  717. package/src/renderer/src/components/ChatPanel.tsx +1 -1
  718. package/src/renderer/src/components/DashboardSidebar.tsx +2 -2
  719. package/src/renderer/src/components/ImageStudioPanel.tsx +1 -1
  720. package/src/renderer/src/components/LiveTranslateControls.tsx +1 -1
  721. package/src/renderer/src/components/MintDashboard.tsx +1 -1
  722. package/src/renderer/src/components/ProactiveGlow.tsx +1 -1
  723. package/src/renderer/src/components/ScreenPicker.css +1 -1
  724. package/src/renderer/src/components/ScreenPicker.tsx +1 -1
  725. package/src/renderer/src/components/SettingsWindow.tsx +6 -830
  726. package/src/renderer/src/components/SpotlightWindow.tsx +4 -32
  727. package/src/renderer/src/components/VeoStudioPanel.tsx +1 -1
  728. package/src/renderer/src/components/WorkspacePanel.tsx +49 -15
  729. package/src/renderer/src/components/browser-suggestions-popup.css +8 -8
  730. package/src/renderer/src/css/app-shell.css +26 -106
  731. package/src/renderer/src/css/chat.css +1499 -292
  732. package/src/renderer/src/css/desktop-tools.css +17 -348
  733. package/src/renderer/src/css/sidebar-workspace.css +22 -2
  734. package/src/renderer/src/css/spotlight.css +2 -2
  735. package/src/renderer/src/css/tool-surfaces.css +335 -65
  736. package/src/renderer/src/css/widget.css +6 -5
  737. package/src/renderer/src/env.d.ts +13 -0
  738. package/src/renderer/src/index.css +53 -83
  739. package/src/renderer/src/tauri.ts +99 -13
  740. package/src/renderer/src/voiceInput.ts +3 -1
  741. package/src/renderer/src-web/App.tsx +20 -8
  742. package/src/renderer/src-web/agentProgress.ts +1 -0
  743. package/src/renderer/src-web/components/ChatPanel.tsx +1 -1
  744. package/src/renderer/src-web/components/DashboardSidebar.tsx +2 -2
  745. package/src/renderer/src-web/components/ImageStudioPanel.tsx +1 -1
  746. package/src/renderer/src-web/components/MintDashboard.tsx +1 -1
  747. package/src/renderer/src-web/components/SettingsWindow.tsx +6 -851
  748. package/src/renderer/src-web/components/VeoStudioPanel.tsx +1 -1
  749. package/src/renderer/src-web/css/app-shell.css +20 -104
  750. package/src/renderer/src-web/css/chat.css +1536 -261
  751. package/src/renderer/src-web/css/mobile.css +29 -13
  752. package/src/renderer/src-web/css/settings/mobile.css +2 -2
  753. package/src/renderer/src-web/css/sidebar-workspace.css +21 -1
  754. package/src/renderer/src-web/env.d.ts +14 -0
  755. package/src/renderer/src-web/index.css +53 -83
  756. package/src/renderer/src-web/tauri.ts +93 -15
  757. package/src/renderer/tests/browserArtifacts.browser.html +1 -0
  758. package/src/renderer/tests/browserArtifacts.browser.tsx +68 -0
  759. package/src/renderer/tests/chatEscape.browser.html +1 -0
  760. package/src/renderer/tests/chatEscape.browser.tsx +25 -0
  761. package/src/renderer/tests/chatPerformance.browser.html +1 -1
  762. package/src/renderer/tests/collapsedPaste.browser.html +1 -1
  763. package/src/renderer/tests/composerMenus.browser.html +3 -3
  764. package/src/renderer/tests/contextCompaction.browser.html +1 -1
  765. package/src/renderer/tests/errorRecovery.browser.html +1 -1
  766. package/src/renderer/tests/fixtures/preview.webm +0 -0
  767. package/src/renderer/tests/glideSelect.browser.html +1 -2
  768. package/src/renderer/tests/glideSelect.preview.html +1 -1
  769. package/src/renderer/tests/library.browser.tsx +4 -1
  770. package/src/renderer/tests/liveOrb.browser.html +1 -1
  771. package/src/renderer/tests/liveVoice.browser.html +1 -0
  772. package/src/renderer/tests/mediaFixtures.ts +32 -0
  773. package/src/renderer/tests/modInput.browser.html +1 -1
  774. package/src/renderer/tests/sessionSources.browser.tsx +76 -15
  775. package/src/renderer/tests/toolsPanel.browser.tsx +5 -5
  776. package/src/renderer/tests/viewedImages.browser.tsx +3 -3
  777. package/src/renderer/tests/voiceDictation.browser.html +101 -0
  778. package/src/renderer/tests/voicePill.browser.html +3 -3
  779. package/src/renderer/tests/voicePill.preview.html +1 -1
  780. package/src/renderer/tests/webSources.browser.tsx +2 -98
  781. package/src/renderer/tests/workspaceDrop.browser.html +1 -0
  782. package/src/renderer/tests/workspaceDrop.browser.tsx +183 -0
  783. package/src/renderer/tests/workspaceExpansion.browser.html +1 -0
  784. package/src/renderer/tests/workspaceExpansion.browser.tsx +66 -0
  785. package/src/renderer/tests/workspaceRemoval.browser.html +66 -0
  786. package/src/renderer/tests/workspaceUndo.browser.html +10 -2
  787. package/src-tauri/src/app/lifecycle.rs +45 -15
  788. package/src-tauri/src/background/headless.rs +14 -8
  789. package/src-tauri/src/commands/conversation.rs +137 -73
  790. package/src-tauri/src/commands/integrations.rs +7 -1
  791. package/src-tauri/src/commands/media.rs +7 -0
  792. package/src-tauri/src/commands/workspace.rs +38 -0
  793. package/src-tauri/src/native/mod.rs +0 -1
  794. package/src-tauri/src/native/system.rs +4 -47
  795. package/tsconfig.json +15 -1
  796. package/vite.config.ts +2 -1
  797. package/vite.config.web.ts +2 -0
  798. package/crates/mint-cli/src/interactive/chat_tui.rs +0 -7853
  799. package/crates/mint-core/src/agent/chat.rs +0 -4155
  800. package/crates/mint-core/src/browser/lifecycle.rs +0 -191
  801. package/crates/mint-core/src/browser/logging.rs +0 -20
  802. package/crates/mint-core/src/orchestration/tools/browser.rs +0 -31
  803. package/out/web/assets/CliSessionsView-CtzUJpK_.css +0 -1
  804. package/out/web/assets/CliSessionsView-D7hw0Mgj.js +0 -1
  805. package/out/web/assets/CodeReviewPage-C7bEOJfd.js +0 -4
  806. package/out/web/assets/CodeReviewPage-Dte9uLiL.css +0 -1
  807. package/out/web/assets/CommandPalette-0RrF-AOT.js +0 -1
  808. package/out/web/assets/ImageStudioPanel-CdxkAf_r.js +0 -1
  809. package/out/web/assets/LibraryPage--Qfisp6D.js +0 -2
  810. package/out/web/assets/LibraryPage-DAxUTAg7.css +0 -1
  811. package/out/web/assets/LinkedFoldersView-2nfZbDgN.js +0 -1
  812. package/out/web/assets/McpRegistryPicker-h9uS8_wT.js +0 -1
  813. package/out/web/assets/McpServersView-slfQJzNB.js +0 -1
  814. package/out/web/assets/MintDashboard-BSR5XpXh.js +0 -25
  815. package/out/web/assets/MintDashboard-CWUcCGhd.css +0 -1
  816. package/out/web/assets/PluginsView-l7LSm503.js +0 -1
  817. package/out/web/assets/ScheduledTasksView-CttmLbq1.js +0 -1
  818. package/out/web/assets/SettingsWindow-BCMLYvEV.js +0 -4
  819. package/out/web/assets/SettingsWindow-DcMVmU9u.css +0 -1
  820. package/out/web/assets/SkillsView-rNs1qr_U.js +0 -2
  821. package/out/web/assets/ToolSurfacePage-B9DzEPWN.js +0 -37
  822. package/out/web/assets/ToolSurfacePage-CTfgggF2.css +0 -1
  823. package/out/web/assets/VeoStudioPanel-DiJs_Cg2.js +0 -4
  824. package/out/web/assets/WorkspacePanel-CbJ1PFoZ.js +0 -1
  825. package/out/web/assets/WorkspaceSelector-B2xIp04D.js +0 -1
  826. package/out/web/assets/WorkspaceSelector-Ca8uJOsS.css +0 -1
  827. package/out/web/assets/abnfDiagram-VCTEODGH-CD0CjoGl.js +0 -1
  828. package/out/web/assets/architecture-7GRP2DOG-RKV83OeZ.js +0 -1
  829. package/out/web/assets/architectureDiagram-5GKGNRK7-nN_HAFct.js +0 -36
  830. package/out/web/assets/c4Diagram-UCG6FXSJ-DTdV8_zE.js +0 -38
  831. package/out/web/assets/chunk-2Q5K7J3B-Dmqrq8Tp.js +0 -1
  832. package/out/web/assets/chunk-75Z2AOVW-_M0z5olV.js +0 -2
  833. package/out/web/assets/chunk-CKBBP62Z-BpQEgeHF.js +0 -2
  834. package/out/web/assets/chunk-CLGD4ZFX-D54yGvGS.js +0 -1
  835. package/out/web/assets/chunk-CacB07dV.js +0 -1
  836. package/out/web/assets/chunk-DSNCTWBM-C-gvT2ru.js +0 -2
  837. package/out/web/assets/chunk-DU6HZSFF-CnFngIhW.js +0 -128
  838. package/out/web/assets/chunk-F27PBJKO-BqtWrnEO.js +0 -1
  839. package/out/web/assets/chunk-FOHPRMQF-BaVC1uA7.js +0 -161
  840. package/out/web/assets/chunk-G27WJ6UU-Ckoq5mi2.js +0 -231
  841. package/out/web/assets/chunk-JQ64N6SF-CF8T7GiZ.js +0 -1
  842. package/out/web/assets/chunk-JWPE2WC7-BwtYOHqU.js +0 -1
  843. package/out/web/assets/chunk-LNGE3PJU-AdBoOWw2.js +0 -34
  844. package/out/web/assets/chunk-OBVCFTLP-CRIkunUB.js +0 -1
  845. package/out/web/assets/chunk-P2QGCYS3-UFEU6BxZ.js +0 -1
  846. package/out/web/assets/chunk-POPQ4Y6H-CQcrNbjr.js +0 -1
  847. package/out/web/assets/chunk-PWAF6VOD-Cc2uVDsG.js +0 -1
  848. package/out/web/assets/chunk-R7TYR2AO-DiCxzdAY.js +0 -10
  849. package/out/web/assets/chunk-RHFEMEQ7-b7hnPmrR.js +0 -168
  850. package/out/web/assets/chunk-XXDRQBXY-DnJkbUaw.js +0 -1
  851. package/out/web/assets/chunk-Y2CYZVJY-C9o-s6OY.js +0 -1
  852. package/out/web/assets/classDiagram-DTDB5LWJ-RnC2mYO4.js +0 -1
  853. package/out/web/assets/classDiagram-v2-JRS7N3AN-RnC2mYO4.js +0 -1
  854. package/out/web/assets/config-CnvCChUM.js +0 -2
  855. package/out/web/assets/config-DM6oIVO3.css +0 -1
  856. package/out/web/assets/core-DsKkQ7ig.js +0 -1
  857. package/out/web/assets/cose-bilkent-JH36ORCC--UN4aw5-.js +0 -1
  858. package/out/web/assets/cynefin-OW5HDTMX-CnzQfOGp.js +0 -1
  859. package/out/web/assets/cynefinDiagram-5FMLGOSQ-DVtQnfpm.js +0 -62
  860. package/out/web/assets/cytoscape.esm-Df1UN4Y1.js +0 -321
  861. package/out/web/assets/dagre-3AP2YEHR-DG76_k5_.js +0 -4
  862. package/out/web/assets/dagre-BWadQu2F.js +0 -1
  863. package/out/web/assets/diagram-S7CK7UJ4-CaouUoze.js +0 -30
  864. package/out/web/assets/diagram-UQ7AKVKN-B4bEc26B.js +0 -41
  865. package/out/web/assets/diagram-VX7I27RA-7hH5IPYf.js +0 -24
  866. package/out/web/assets/diagram-Z3DM3KII-CCM4clo8.js +0 -24
  867. package/out/web/assets/dist-BoTOASLF.js +0 -1
  868. package/out/web/assets/dist-js-BkYNznHX.js +0 -1
  869. package/out/web/assets/ebnfDiagram-PWID7BFC-Da7WAM3k.js +0 -1
  870. package/out/web/assets/event-Cdt7Byyj.js +0 -1
  871. package/out/web/assets/eventmodeling-NTZA5JFV-xcfsA0XD.js +0 -1
  872. package/out/web/assets/flowDiagram-A5DVABFB-JdtH1673.js +0 -1
  873. package/out/web/assets/ganttDiagram-EL5Y4UJY-B3ClFGmL.js +0 -292
  874. package/out/web/assets/gitGraph-4MIJSDKK-BZ_y_cDU.js +0 -1
  875. package/out/web/assets/gitGraphDiagram-WWUBYQGX-drhRSYm3.js +0 -106
  876. package/out/web/assets/graphlib-CTR5ZhLf.js +0 -1
  877. package/out/web/assets/index-BZ9xgmMP.css +0 -1
  878. package/out/web/assets/index-Ca8cCS4h.js +0 -168
  879. package/out/web/assets/info-A6RAGUB7-B7I3K0F1.js +0 -1
  880. package/out/web/assets/infoDiagram-RXCK75RN-deX_1MPh.js +0 -2
  881. package/out/web/assets/journeyDiagram-EYS64GPL-CZn2kB8b.js +0 -139
  882. package/out/web/assets/jsx-runtime-DN4NhQUK.js +0 -1
  883. package/out/web/assets/katex-O3jvqgnU.js +0 -257
  884. package/out/web/assets/markdown-CMhYIcWB.js +0 -39
  885. package/out/web/assets/mermaid-parser.core-Whu2rlJC.js +0 -7
  886. package/out/web/assets/mermaid.core-CKyPo8VH.js +0 -11
  887. package/out/web/assets/modelManager-CUaGWMWu.js +0 -2
  888. package/out/web/assets/oauthManager-DlNhXJr6.js +0 -1
  889. package/out/web/assets/packet-AYTQ26CC-CEeIRKRG.js +0 -1
  890. package/out/web/assets/pegDiagram-XKGWAZYB-BYadI-KO.js +0 -1
  891. package/out/web/assets/pie-WAS4IAKB-D8uf4dh5.js +0 -1
  892. package/out/web/assets/pieDiagram-E7YTZNPT-CjSDy4hO.js +0 -39
  893. package/out/web/assets/radar-RG4KPBEZ-DZJxnokY.js +0 -1
  894. package/out/web/assets/railroad-74A4TZTK-DBysUiUi.js +0 -1
  895. package/out/web/assets/railroad-abnf-HS5TGJTU-CHWxoJYg.js +0 -1
  896. package/out/web/assets/railroad-ebnf-LZEXJU2U-DDv7X6bY.js +0 -1
  897. package/out/web/assets/railroad-peg-WCYAUIDC-DpfcYYNJ.js +0 -1
  898. package/out/web/assets/railroadDiagram-O6MQD6OU-CYtyv0t3.js +0 -1
  899. package/out/web/assets/rough.esm-DFbTUy8a.js +0 -1
  900. package/out/web/assets/sankeyDiagram-P5KCCOFB-Bn2x6C4Y.js +0 -40
  901. package/out/web/assets/sequenceDiagram-WJ2MYXX4-BvFLKtPd.js +0 -162
  902. package/out/web/assets/sizeCapture-X5ZJPWSS-DKeOCaN2.js +0 -1
  903. package/out/web/assets/src-DnfeXQPr.js +0 -1
  904. package/out/web/assets/stateDiagram-HBIQ2CUA-Bx0GWeaY.js +0 -1
  905. package/out/web/assets/stateDiagram-v2-4QOOHH4V--hol1yg_.js +0 -1
  906. package/out/web/assets/studio-B1mk40wD.js +0 -1
  907. package/out/web/assets/swimlanes-XN3QIQJK-CCma0T7L.js +0 -1
  908. package/out/web/assets/swimlanesDiagram-VK2B7HYN-BkohoVsU.js +0 -8
  909. package/out/web/assets/timeline-definition-24CTP7MA-iCRhhJ7C.js +0 -120
  910. package/out/web/assets/treeView-Q6P3EWNA-DZ-y08qp.js +0 -1
  911. package/out/web/assets/treemap-WGGIJYW6-De0CxzMx.js +0 -1
  912. package/out/web/assets/vennDiagram-4TSXK5OY-w_7aZ6xe.js +0 -34
  913. package/out/web/assets/wardley-WFR3VGLG-DT0y0xJ4.js +0 -1
  914. package/out/web/assets/window-BJCVtyET.js +0 -1
  915. package/shared-modules/libsecret/libsecret.json +0 -23
  916. package/shared-modules/libusb/libusb.json +0 -27
  917. package/shared-modules/lzo/lzo.json +0 -27
  918. package/src/renderer/shared/components/AgentActivityDrawer.tsx +0 -204
  919. package/src/renderer/shared/components/AgentActivityTable.tsx +0 -301
  920. package/src/renderer/shared/components/ChatMessageItem.test.tsx +0 -68
  921. package/src/renderer/shared/components/ChatMessageItem.tsx +0 -326
  922. package/src/renderer/shared/components/ChatPanel.test.tsx +0 -21
  923. package/src/renderer/shared/components/FormattedMessage.tsx +0 -11
  924. package/src/renderer/shared/components/GeminiLiveOverlay.test.tsx +0 -24
  925. package/src/renderer/shared/components/GlideSelect.d.ts +0 -31
  926. package/src/renderer/shared/components/GlideSelect.jsx +0 -266
  927. package/src/renderer/shared/components/InlineThoughtNote.tsx +0 -27
  928. package/src/renderer/shared/components/ModSessionControls.tsx +0 -8
  929. package/src/renderer/shared/components/ModUi.tsx +0 -26
  930. package/src/renderer/shared/components/ProgressActivity.tsx +0 -17
  931. package/src/renderer/shared/components/ReviewHistory.tsx +0 -42
  932. package/src/renderer/shared/components/RunSummaryDashboard.tsx +0 -143
  933. package/src/renderer/shared/components/SelectField.tsx +0 -38
  934. package/src/renderer/shared/components/SessionSources.tsx +0 -48
  935. package/src/renderer/shared/components/SessionSourcesPopover.tsx +0 -61
  936. package/src/renderer/shared/components/ToolSurfacePage.test.tsx +0 -32
  937. package/src/renderer/shared/components/ViewedImages.tsx +0 -36
  938. package/src/renderer/shared/components/WorkspaceContextPopover.tsx +0 -138
  939. package/src/renderer/shared/components/WorkspacePreviewActions.test.tsx +0 -44
  940. package/src/renderer/shared/components/ui/demo.tsx +0 -26
  941. package/src/renderer/shared/css/session-sources-popover.css +0 -9
  942. package/src/renderer/src/components/BrowserShell.tsx +0 -108
  943. package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +0 -91
  944. package/src/renderer/src/components/CalculationCard.tsx +0 -3
  945. package/src/renderer/src/components/ImageGenCard.tsx +0 -2
  946. package/src/renderer/src/components/ImageSearchCard.tsx +0 -2
  947. package/src/renderer/src/components/MermaidCard.tsx +0 -1
  948. package/src/renderer/src/components/PicturesLibrary.tsx +0 -2
  949. package/src/renderer/src/components/Settings/AgentsTab.tsx +0 -4
  950. package/src/renderer/src/components/Settings/AudioTab.tsx +0 -3
  951. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -3
  952. package/src/renderer/src/components/Settings/GeneralTab.tsx +0 -3
  953. package/src/renderer/src/components/Settings/MemoryTab.tsx +0 -3
  954. package/src/renderer/src/components/Settings/PluginsTab.tsx +0 -3
  955. package/src/renderer/src/components/Settings/ProfileTab.tsx +0 -3
  956. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +0 -3
  957. package/src/renderer/src/components/Settings/ThemeTab.tsx +0 -3
  958. package/src/renderer/src/components/StockCard.tsx +0 -3
  959. package/src/renderer/src/components/WeatherCard.tsx +0 -3
  960. package/src/renderer/src/components/browser-shell.css +0 -185
  961. package/src/renderer/src/css/pictures.css +0 -2
  962. package/src/renderer/src-web/components/PicturesLibrary.tsx +0 -2
  963. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +0 -4
  964. package/src/renderer/src-web/components/Settings/AudioTab.tsx +0 -3
  965. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -3
  966. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +0 -3
  967. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +0 -3
  968. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +0 -3
  969. package/src/renderer/src-web/components/Settings/ProfileTab.tsx +0 -3
  970. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +0 -3
  971. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +0 -3
  972. package/src/renderer/src-web/css/pictures.css +0 -2
  973. package/src/renderer/src-web/css/settings/base.css +0 -1266
  974. package/src/renderer/src-web/css/spotlight.css +0 -102
  975. package/src/renderer/src-web/css/widget.css +0 -213
  976. package/src-tauri/src/native/browser.rs +0 -252
  977. /package/src/renderer/shared/components/{ContextCompactionStatus.tsx → chat/ContextCompactionStatus.tsx} +0 -0
  978. /package/src/renderer/shared/components/{GeminiLiveOverlay.css → chat/GeminiLiveOverlay.css} +0 -0
  979. /package/src/renderer/shared/components/{VirtualChatHistory.tsx → chat/VirtualChatHistory.tsx} +0 -0
  980. /package/src/renderer/shared/components/{VoicePill.d.ts → chat/VoicePill.d.ts} +0 -0
  981. /package/src/renderer/shared/components/{VoicePill.test.tsx → chat/VoicePill.test.tsx} +0 -0
  982. /package/src/renderer/shared/components/{ScreenCaptureDialog.css → studio/ScreenCaptureDialog.css} +0 -0
  983. /package/src/renderer/shared/components/{ScreenCaptureDialog.tsx → studio/ScreenCaptureDialog.tsx} +0 -0
  984. /package/src/renderer/shared/components/{SelectField.test.tsx → ui/SelectField.test.tsx} +0 -0
@@ -0,0 +1,1479 @@
1
+ use super::anthropic::*;
2
+ use super::gemini::*;
3
+ use super::ollama::*;
4
+ use super::openai::*;
5
+ use super::streaming::*;
6
+ use super::*;
7
+ use reqwest::Client;
8
+ use serde_json::json;
9
+
10
+ #[test]
11
+ fn rejects_missing_api_key() {
12
+ let error = required_key("gemini", "").unwrap_err();
13
+ assert!(matches!(error, ChatError::MissingApiKey(provider) if provider == "gemini"));
14
+ }
15
+
16
+ #[test]
17
+ fn rejects_video_attachments_for_non_gemini_providers() {
18
+ let request = ChatRequest {
19
+ message: "describe this video".into(),
20
+ system_instruction: String::new(),
21
+ chat_id: None,
22
+ image_data_uri: None,
23
+ audio_data_uri: None,
24
+ video_data_uri: Some("data:video/mp4;base64,AAAA".into()),
25
+ document_attachment: None,
26
+ workspace_path: None,
27
+ agent_id: None,
28
+ plan_mode: false,
29
+ pinned_mcp_server: None,
30
+ messages: None,
31
+ tools: None,
32
+ temperature: None,
33
+ ..Default::default()
34
+ };
35
+
36
+ let error = require_supported_attachments("openai", &request).unwrap_err();
37
+ assert!(matches!(error, ChatError::UnsupportedAttachments(provider) if provider == "openai"));
38
+ }
39
+
40
+ #[test]
41
+ fn parses_stream_provider_formats() {
42
+ let openai = parse_stream_value(
43
+ StreamFormat::OpenAi,
44
+ r#"data: {"choices":[{"delta":{"content":"hello"}}]}"#,
45
+ )
46
+ .unwrap();
47
+ assert_eq!(openai["choices"][0]["delta"]["content"], "hello");
48
+ let ollama =
49
+ parse_stream_value(StreamFormat::Ollama, r#"{"message":{"content":"hi"}}"#).unwrap();
50
+ assert_eq!(ollama["message"]["content"], "hi");
51
+ assert!(parse_stream_value(StreamFormat::Anthropic, "data: [DONE]").is_none());
52
+ }
53
+
54
+ #[test]
55
+ fn malformed_tool_arguments_survive_provider_parsing_for_rejection() {
56
+ let malformed = r#"{"path":"~/Downloads/YOUTUBE_DL""#;
57
+ let function = json!({"name": "list_files", "arguments": malformed});
58
+ let openai = parse_openai_style_reply(
59
+ "test".into(),
60
+ &json!({
61
+ "choices": [{"message": {"tool_calls": [{"id": "call_1", "function": function}]}}]
62
+ }),
63
+ )
64
+ .unwrap();
65
+ let ollama = parse_ollama_reply(
66
+ "test".into(),
67
+ &json!({
68
+ "message": {"tool_calls": [{"function": function}]}
69
+ }),
70
+ )
71
+ .unwrap();
72
+ let mut accumulator = StreamReplyAccumulator::default();
73
+ accumulator.consume_openai(&json!({
74
+ "choices": [{"delta": {"tool_calls": [{"index": 0, "id": "call_1", "function": function}]}}]
75
+ }), &mut |_| {});
76
+ let streamed = accumulator.finish("test".into()).unwrap();
77
+ for reply in [openai, ollama, streamed] {
78
+ let calls = reply.tool_calls.unwrap();
79
+ assert_eq!(calls.len(), 1);
80
+ assert_eq!(calls[0].input, Value::String(malformed.into()));
81
+ }
82
+ assert_eq!(decode_tool_arguments(&json!("{}")), json!({}));
83
+ assert_eq!(decode_tool_arguments(&Value::Null), Value::Null);
84
+ }
85
+
86
+ #[test]
87
+ fn accumulates_openai_reasoning_tools_and_usage() {
88
+ let mut accumulator = StreamReplyAccumulator::default();
89
+ let mut events = Vec::new();
90
+ accumulator.consume_openai(
91
+ &json!({
92
+ "choices": [{"delta": {"reasoning_content": "inspect "}}]
93
+ }),
94
+ &mut |event| events.push(event),
95
+ );
96
+ accumulator.consume_openai(&json!({
97
+ "choices": [{"delta": {"reasoning_content": "files", "tool_calls": [{
98
+ "index": 0, "id": "call_1", "function": {"name": "read_file", "arguments": "{\"path\":"}
99
+ }]}}]
100
+ }), &mut |event| events.push(event));
101
+ accumulator.consume_openai(
102
+ &json!({
103
+ "choices": [{"delta": {"tool_calls": [{
104
+ "index": 0, "function": {"arguments": "\"README.md\"}"}
105
+ }]}, "finish_reason": "tool_calls"}],
106
+ "usage": {"prompt_tokens": 10, "completion_tokens": 5}
107
+ }),
108
+ &mut |event| events.push(event),
109
+ );
110
+ accumulator.flush_tag_buffer(&mut |event| events.push(event));
111
+ let reply = accumulator.finish("test".into()).unwrap();
112
+ assert_eq!(reply.thought.as_deref(), Some("inspect files"));
113
+ assert_eq!(reply.stop_reason.as_deref(), Some("tool_calls"));
114
+ assert_eq!(reply.input_tokens, Some(10));
115
+ assert_eq!(reply.tool_calls.unwrap()[0].input["path"], "README.md");
116
+ assert_eq!(
117
+ events
118
+ .iter()
119
+ .filter(|event| matches!(event, ChatStreamEvent::ReasoningDelta { .. }))
120
+ .count(),
121
+ 2
122
+ );
123
+ assert_eq!(
124
+ events
125
+ .iter()
126
+ .filter(|event| matches!(event, ChatStreamEvent::ToolCallDelta { .. }))
127
+ .count(),
128
+ 2
129
+ );
130
+ }
131
+
132
+ #[test]
133
+ fn streams_think_tags_split_across_chunks() {
134
+ let mut accumulator = StreamReplyAccumulator::default();
135
+ let mut events = Vec::new();
136
+ for chunk in ["ก่อน<thi", "nk>คิด", "อยู่</th", "ink>หลัง"] {
137
+ accumulator.push_text(chunk, &mut |event| events.push(event));
138
+ }
139
+ accumulator.flush_tag_buffer(&mut |event| events.push(event));
140
+ let reply = accumulator.finish("test".into()).unwrap();
141
+ assert_eq!(reply.text, "ก่อนหลัง");
142
+ assert_eq!(reply.thought.as_deref(), Some("คิดอยู่"));
143
+ let streamed_reasoning: String = events
144
+ .iter()
145
+ .filter_map(|event| match event {
146
+ ChatStreamEvent::ReasoningDelta { delta } => Some(delta.as_str()),
147
+ _ => None,
148
+ })
149
+ .collect();
150
+ assert_eq!(streamed_reasoning, "คิดอยู่");
151
+ }
152
+
153
+ #[test]
154
+ fn builds_gemini_multimodal_parts() {
155
+ let parts = gemini_parts(&ChatRequest {
156
+ message: "describe".into(),
157
+ system_instruction: String::new(),
158
+ chat_id: None,
159
+ image_data_uri: Some("data:image/png;base64,aGk= data:image/jpeg;base64,Ynl5".into()),
160
+ audio_data_uri: None,
161
+ video_data_uri: None,
162
+ document_attachment: None,
163
+ workspace_path: None,
164
+ agent_id: None,
165
+ plan_mode: false,
166
+ pinned_mcp_server: None,
167
+ messages: None,
168
+ tools: None,
169
+ temperature: None,
170
+ ..Default::default()
171
+ })
172
+ .unwrap();
173
+ assert_eq!(parts.len(), 3);
174
+ assert_eq!(parts[1]["inlineData"]["mimeType"], "image/png");
175
+ assert_eq!(parts[2]["inlineData"]["mimeType"], "image/jpeg");
176
+ }
177
+
178
+ fn sample_tool() -> ToolSpec {
179
+ ToolSpec {
180
+ name: "read_file".into(),
181
+ description: "Reads a file".into(),
182
+ parameters: json!({
183
+ "type": "object",
184
+ "properties": { "path": { "type": "string" } },
185
+ "required": ["path"]
186
+ }),
187
+ }
188
+ }
189
+
190
+ fn native_request(messages: Vec<ChatMessage>, tools: Option<Vec<ToolSpec>>) -> ChatRequest {
191
+ ChatRequest {
192
+ message: String::new(),
193
+ system_instruction: "You are Mint.".into(),
194
+ chat_id: None,
195
+ image_data_uri: None,
196
+ audio_data_uri: None,
197
+ video_data_uri: None,
198
+ document_attachment: None,
199
+ workspace_path: None,
200
+ agent_id: None,
201
+ plan_mode: false,
202
+ pinned_mcp_server: None,
203
+ messages: Some(messages),
204
+ tools,
205
+ temperature: None,
206
+ ..Default::default()
207
+ }
208
+ }
209
+
210
+ #[test]
211
+ fn anthropic_native_payload_includes_tools_and_tool_result_messages() {
212
+ let messages = vec![
213
+ ChatMessage::text(ChatRole::User, "read main.rs"),
214
+ ChatMessage {
215
+ role: ChatRole::Assistant,
216
+ content: vec![ContentBlock::ToolUse {
217
+ id: "call_1".into(),
218
+ name: "read_file".into(),
219
+ input: json!({ "path": "main.rs" }),
220
+ thought_signature: None,
221
+ }],
222
+ },
223
+ ChatMessage {
224
+ role: ChatRole::Tool,
225
+ content: vec![ContentBlock::ToolResult {
226
+ tool_use_id: "call_1".into(),
227
+ content: "fn main() {}".into(),
228
+ is_error: false,
229
+ }],
230
+ },
231
+ ];
232
+ let request = native_request(messages, Some(vec![sample_tool()]));
233
+ let cfg = MintConfig::default();
234
+ let payload = anthropic_chat_payload(&cfg, "claude-x", &request, false).unwrap();
235
+
236
+ assert_eq!(payload["system"][0]["text"], "You are Mint.");
237
+ assert_eq!(payload["tools"][0]["name"], "read_file");
238
+ assert_eq!(payload["tools"][0]["input_schema"]["type"], "object");
239
+ let msgs = payload["messages"].as_array().unwrap();
240
+ assert_eq!(msgs.len(), 3);
241
+ assert_eq!(msgs[1]["role"], "assistant");
242
+ assert_eq!(msgs[1]["content"][0]["type"], "tool_use");
243
+ assert_eq!(msgs[2]["role"], "user");
244
+ assert_eq!(msgs[2]["content"][0]["type"], "tool_result");
245
+ assert_eq!(msgs[2]["content"][0]["tool_use_id"], "call_1");
246
+ }
247
+
248
+ #[test]
249
+ fn anthropic_payload_marks_the_system_prompt_as_cacheable() {
250
+ let request = native_request(vec![ChatMessage::text(ChatRole::User, "hi")], None);
251
+ let cfg = MintConfig::default();
252
+ let payload = anthropic_chat_payload(&cfg, "claude-x", &request, false).unwrap();
253
+
254
+ assert_eq!(payload["system"][0]["type"], "text");
255
+ assert_eq!(payload["system"][0]["text"], "You are Mint.");
256
+ assert_eq!(payload["system"][0]["cache_control"]["type"], "ephemeral");
257
+ }
258
+
259
+ #[test]
260
+ fn review_anthropic_tool_results_preserve_failure_and_success_flags() {
261
+ for (text, is_error) in [
262
+ ("User denied MCP tool call", true),
263
+ ("MCP call was not approved; user feedback", true),
264
+ ("Error: literal diagnostic label", false),
265
+ ] {
266
+ let request = native_request(
267
+ vec![ChatMessage {
268
+ role: ChatRole::Tool,
269
+ content: vec![ContentBlock::ToolResult {
270
+ tool_use_id: "mcp-call".into(),
271
+ content: text.into(),
272
+ is_error,
273
+ }],
274
+ }],
275
+ None,
276
+ );
277
+ let payload =
278
+ anthropic_chat_payload(&MintConfig::default(), "claude-x", &request, false).unwrap();
279
+ assert_eq!(payload["messages"][0]["content"][0]["is_error"], is_error);
280
+ assert_eq!(payload["messages"][0]["content"][0]["content"], text);
281
+ }
282
+ }
283
+
284
+ #[test]
285
+ fn anthropic_payload_omits_the_cache_breakpoint_for_an_empty_system_prompt() {
286
+ let mut request = native_request(vec![ChatMessage::text(ChatRole::User, "hi")], None);
287
+ request.system_instruction = String::new();
288
+ let cfg = MintConfig::default();
289
+ let payload = anthropic_chat_payload(&cfg, "claude-x", &request, false).unwrap();
290
+
291
+ // Falls back to the plain-string shape rather than an empty cacheable
292
+ // block, matching the pre-caching behavior exactly for this case.
293
+ assert_eq!(payload["system"], "");
294
+ }
295
+
296
+ #[test]
297
+ fn anthropic_payload_marks_only_the_last_tool_as_the_cache_breakpoint() {
298
+ let second_tool = ToolSpec {
299
+ name: "write_file".into(),
300
+ description: "Writes a file".into(),
301
+ parameters: json!({ "type": "object" }),
302
+ };
303
+ let request = native_request(
304
+ vec![ChatMessage::text(ChatRole::User, "hi")],
305
+ Some(vec![sample_tool(), second_tool]),
306
+ );
307
+ let cfg = MintConfig::default();
308
+ let payload = anthropic_chat_payload(&cfg, "claude-x", &request, false).unwrap();
309
+
310
+ let tools = payload["tools"].as_array().unwrap();
311
+ assert_eq!(tools.len(), 2);
312
+ assert!(tools[0].get("cache_control").is_none());
313
+ assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
314
+ }
315
+
316
+ #[test]
317
+ fn anthropic_attaches_cache_control_to_penultimate_turn() {
318
+ let request = native_request(
319
+ vec![
320
+ ChatMessage::text(ChatRole::User, "step 1 user"),
321
+ ChatMessage::text(ChatRole::Assistant, "step 1 assistant"),
322
+ ChatMessage::text(ChatRole::User, "step 2 user"),
323
+ ],
324
+ None,
325
+ );
326
+ let cfg = MintConfig::default();
327
+ let payload = anthropic_chat_payload(&cfg, "claude-x", &request, false).unwrap();
328
+ let messages = payload["messages"].as_array().unwrap();
329
+ assert_eq!(messages.len(), 3);
330
+ assert!(messages[0]["content"][0].get("cache_control").is_none());
331
+ assert_eq!(
332
+ messages[1]["content"][0]["cache_control"]["type"],
333
+ "ephemeral"
334
+ );
335
+ assert!(messages[2]["content"][0].get("cache_control").is_none());
336
+ }
337
+
338
+ #[test]
339
+ fn parses_anthropic_reply_with_text_and_tool_use_blocks() {
340
+ let response = json!({
341
+ "stop_reason": "tool_use",
342
+ "content": [
343
+ { "type": "text", "text": "Let me check that file." },
344
+ { "type": "tool_use", "id": "toolu_1", "name": "read_file", "input": { "path": "main.rs" } }
345
+ ]
346
+ });
347
+ let reply = parse_anthropic_reply("claude-x".into(), &response).unwrap();
348
+ assert_eq!(reply.text, "Let me check that file.");
349
+ let calls = reply.tool_calls.unwrap();
350
+ assert_eq!(calls.len(), 1);
351
+ assert_eq!(calls[0].name, "read_file");
352
+ assert_eq!(calls[0].input["path"], "main.rs");
353
+ assert_eq!(reply.stop_reason.as_deref(), Some("tool_use"));
354
+ }
355
+
356
+ #[test]
357
+ fn parses_anthropic_reply_with_tool_use_only_and_no_text() {
358
+ let response = json!({
359
+ "content": [
360
+ { "type": "tool_use", "id": "toolu_1", "name": "run_shell", "input": { "command": "ls" } }
361
+ ]
362
+ });
363
+ let reply = parse_anthropic_reply("claude-x".into(), &response).unwrap();
364
+ assert_eq!(reply.text, "");
365
+ assert_eq!(reply.tool_calls.unwrap().len(), 1);
366
+ }
367
+
368
+ #[test]
369
+ fn openai_native_payload_encodes_tool_calls_and_results() {
370
+ let messages = vec![
371
+ ChatMessage::text(ChatRole::User, "list files"),
372
+ ChatMessage {
373
+ role: ChatRole::Assistant,
374
+ content: vec![ContentBlock::ToolUse {
375
+ id: "call_1".into(),
376
+ name: "list_files".into(),
377
+ input: json!({ "path": "." }),
378
+ thought_signature: None,
379
+ }],
380
+ },
381
+ ChatMessage {
382
+ role: ChatRole::Tool,
383
+ content: vec![ContentBlock::ToolResult {
384
+ tool_use_id: "call_1".into(),
385
+ content: "Cargo.toml\nsrc/".into(),
386
+ is_error: false,
387
+ }],
388
+ },
389
+ ];
390
+ let request = native_request(messages, Some(vec![sample_tool()]));
391
+ let cfg = MintConfig::default();
392
+ let payload = openai_chat_payload(&cfg, "gpt-x", &request, false).unwrap();
393
+
394
+ assert_eq!(payload["tools"][0]["type"], "function");
395
+ assert_eq!(payload["tools"][0]["function"]["name"], "read_file");
396
+ let msgs = payload["messages"].as_array().unwrap();
397
+ // system + user + assistant(tool_calls) + tool
398
+ assert_eq!(msgs.len(), 4);
399
+ assert_eq!(msgs[2]["role"], "assistant");
400
+ assert_eq!(msgs[2]["tool_calls"][0]["function"]["name"], "list_files");
401
+ assert_eq!(msgs[3]["role"], "tool");
402
+ assert_eq!(msgs[3]["tool_call_id"], "call_1");
403
+ }
404
+
405
+ #[test]
406
+ fn openai_native_payload_forwards_a_screenshot_riding_a_tool_result_as_a_user_turn() {
407
+ // OpenAI's Chat Completions `tool` role only accepts string content, so an
408
+ // `Image` block attached to a tool result (e.g. `view_image`) must
409
+ // surface as a follow-up `user` message instead of being dropped.
410
+ let messages = vec![ChatMessage {
411
+ role: ChatRole::Tool,
412
+ content: vec![
413
+ ContentBlock::ToolResult {
414
+ tool_use_id: "call_1".into(),
415
+ content: "[Screenshot captured — see attached image]".into(),
416
+ is_error: false,
417
+ },
418
+ ContentBlock::Image {
419
+ data_uri: "data:image/png;base64,QUJD".into(),
420
+ },
421
+ ],
422
+ }];
423
+ let built = openai_native_messages(&messages).unwrap();
424
+ assert_eq!(built.len(), 2);
425
+ assert_eq!(built[0]["role"], "tool");
426
+ assert_eq!(built[0]["tool_call_id"], "call_1");
427
+ assert_eq!(built[1]["role"], "user");
428
+ assert_eq!(built[1]["content"][0]["type"], "image_url");
429
+ assert_eq!(
430
+ built[1]["content"][0]["image_url"]["url"],
431
+ "data:image/png;base64,QUJD"
432
+ );
433
+ }
434
+
435
+ #[test]
436
+ fn openai_native_messages_attaches_reasoning_content_for_tool_calls() {
437
+ let messages = vec![ChatMessage {
438
+ role: ChatRole::Assistant,
439
+ content: vec![ContentBlock::ToolUse {
440
+ id: "call_abc".into(),
441
+ name: "read_file".into(),
442
+ input: json!({"path": "src/main.rs"}),
443
+ thought_signature: None,
444
+ }],
445
+ }];
446
+ let built = openai_native_messages(&messages).unwrap();
447
+ assert_eq!(built.len(), 1);
448
+ assert_eq!(built[0]["role"], "assistant");
449
+ assert_eq!(built[0]["reasoning_content"], json!(""));
450
+ assert_eq!(built[0]["tool_calls"][0]["function"]["name"], "read_file");
451
+ }
452
+
453
+ #[test]
454
+ fn openai_serializes_compacted_history_with_role_alternation() {
455
+ let messages = vec![
456
+ ChatMessage::text(ChatRole::User, "Fix the bug"),
457
+ ChatMessage::text(
458
+ ChatRole::Assistant,
459
+ "Summary of earlier steps:\n- read file.rs\n- found syntax error",
460
+ ),
461
+ ChatMessage::text(
462
+ ChatRole::User,
463
+ "Please continue the task from where you left off based on the summary above.",
464
+ ),
465
+ ChatMessage {
466
+ role: ChatRole::Assistant,
467
+ content: vec![ContentBlock::ToolUse {
468
+ id: "call_1".into(),
469
+ name: "apply_patch".into(),
470
+ input: json!({"path": "file.rs", "patch": "..."}),
471
+ thought_signature: None,
472
+ }],
473
+ },
474
+ ChatMessage {
475
+ role: ChatRole::Tool,
476
+ content: vec![ContentBlock::ToolResult {
477
+ tool_use_id: "call_1".into(),
478
+ content: "Patch applied".into(),
479
+ is_error: false,
480
+ }],
481
+ },
482
+ ];
483
+ let built = openai_native_messages(&messages).unwrap();
484
+ assert_eq!(built.len(), 5);
485
+ assert_eq!(built[0]["role"], "user");
486
+ assert_eq!(built[1]["role"], "assistant");
487
+ assert_eq!(built[2]["role"], "user");
488
+ assert_eq!(built[3]["role"], "assistant");
489
+ assert_eq!(built[4]["role"], "tool");
490
+ }
491
+
492
+ #[test]
493
+ fn anthropic_serializes_compacted_history_with_role_alternation() {
494
+ let messages = vec![
495
+ ChatMessage::text(ChatRole::User, "Fix the bug"),
496
+ ChatMessage::text(ChatRole::Assistant, "Summary of earlier steps"),
497
+ ChatMessage::text(ChatRole::User, "Please continue"),
498
+ ChatMessage {
499
+ role: ChatRole::Assistant,
500
+ content: vec![ContentBlock::ToolUse {
501
+ id: "toolu_1".into(),
502
+ name: "read_file".into(),
503
+ input: json!({"path": "file.rs"}),
504
+ thought_signature: None,
505
+ }],
506
+ },
507
+ ChatMessage {
508
+ role: ChatRole::Tool,
509
+ content: vec![ContentBlock::ToolResult {
510
+ tool_use_id: "toolu_1".into(),
511
+ content: "Code content".into(),
512
+ is_error: false,
513
+ }],
514
+ },
515
+ ];
516
+ let built = anthropic_messages(&messages).unwrap();
517
+ assert_eq!(built.len(), 5);
518
+ assert_eq!(built[0]["role"], "user");
519
+ assert_eq!(built[1]["role"], "assistant");
520
+ assert_eq!(built[2]["role"], "user");
521
+ assert_eq!(built[3]["role"], "assistant");
522
+ assert_eq!(built[4]["role"], "user");
523
+ }
524
+
525
+ #[test]
526
+ fn gemini_serializes_compacted_history_with_role_alternation() {
527
+ let messages = vec![
528
+ ChatMessage::text(ChatRole::User, "Fix the bug"),
529
+ ChatMessage::text(ChatRole::Assistant, "Summary of earlier steps"),
530
+ ChatMessage::text(ChatRole::User, "Please continue"),
531
+ ChatMessage {
532
+ role: ChatRole::Assistant,
533
+ content: vec![ContentBlock::ToolUse {
534
+ id: "call_1".into(),
535
+ name: "read_file".into(),
536
+ input: json!({"path": "file.rs"}),
537
+ thought_signature: None,
538
+ }],
539
+ },
540
+ ChatMessage {
541
+ role: ChatRole::Tool,
542
+ content: vec![ContentBlock::ToolResult {
543
+ tool_use_id: "call_1".into(),
544
+ content: "Code content".into(),
545
+ is_error: false,
546
+ }],
547
+ },
548
+ ];
549
+ let built = gemini_native_contents(&messages).unwrap();
550
+ assert!(built.len() >= 4);
551
+ }
552
+
553
+ #[test]
554
+ fn openai_allows_audio_attachments() {
555
+ let request = ChatRequest {
556
+ message: "transcribe this".into(),
557
+ system_instruction: String::new(),
558
+ chat_id: None,
559
+ image_data_uri: None,
560
+ audio_data_uri: Some("data:audio/wav;base64,QUJD".into()),
561
+ video_data_uri: None,
562
+ document_attachment: None,
563
+ workspace_path: None,
564
+ agent_id: None,
565
+ plan_mode: false,
566
+ pinned_mcp_server: None,
567
+ messages: None,
568
+ tools: None,
569
+ temperature: None,
570
+ ..Default::default()
571
+ };
572
+ assert!(require_supported_attachments("openai", &request).is_ok());
573
+ }
574
+
575
+ #[test]
576
+ fn non_openai_openai_family_providers_still_reject_audio() {
577
+ // Only the real OpenAI API is confirmed to speak the `input_audio`
578
+ // schema — proxies/other services built on the same wire format
579
+ // (local_openai, openrouter, deepseek, huggingface) and Anthropic
580
+ // (no audio modality at all) must stay rejected.
581
+ let request = ChatRequest {
582
+ message: "transcribe this".into(),
583
+ system_instruction: String::new(),
584
+ chat_id: None,
585
+ image_data_uri: None,
586
+ audio_data_uri: Some("data:audio/wav;base64,QUJD".into()),
587
+ video_data_uri: None,
588
+ document_attachment: None,
589
+ workspace_path: None,
590
+ agent_id: None,
591
+ plan_mode: false,
592
+ pinned_mcp_server: None,
593
+ messages: None,
594
+ tools: None,
595
+ temperature: None,
596
+ ..Default::default()
597
+ };
598
+ for provider in [
599
+ "local_openai",
600
+ "openrouter",
601
+ "deepseek",
602
+ "huggingface",
603
+ "anthropic",
604
+ ] {
605
+ let error = require_supported_attachments(provider, &request).unwrap_err();
606
+ assert!(
607
+ matches!(&error, ChatError::UnsupportedAttachments(p) if p == provider),
608
+ "expected {provider} to reject audio, got {error:?}"
609
+ );
610
+ }
611
+ }
612
+
613
+ #[test]
614
+ fn openai_audio_part_splits_the_data_uri_and_maps_wav_format() {
615
+ let part = openai_audio_part("data:audio/wav;base64,QUJD").unwrap();
616
+ assert_eq!(part["type"], "input_audio");
617
+ assert_eq!(part["input_audio"]["data"], "QUJD");
618
+ assert_eq!(part["input_audio"]["format"], "wav");
619
+ }
620
+
621
+ #[test]
622
+ fn openai_audio_part_defaults_unrecognized_formats_to_mp3() {
623
+ let part = openai_audio_part("data:audio/webm;base64,QUJD").unwrap();
624
+ assert_eq!(part["input_audio"]["format"], "mp3");
625
+ }
626
+
627
+ #[test]
628
+ fn openai_audio_part_rejects_a_malformed_data_uri() {
629
+ assert!(openai_audio_part("not-a-data-uri").is_err());
630
+ }
631
+
632
+ #[test]
633
+ fn openai_message_content_embeds_audio_alongside_text() {
634
+ let content =
635
+ openai_message_content("what is this?", None, Some("data:audio/mp3;base64,QUJD")).unwrap();
636
+ let parts = content.as_array().unwrap();
637
+ assert_eq!(parts[0]["type"], "text");
638
+ assert_eq!(parts[1]["type"], "input_audio");
639
+ assert_eq!(parts[1]["input_audio"]["data"], "QUJD");
640
+ }
641
+
642
+ #[test]
643
+ fn openai_user_content_encodes_an_audio_content_block() {
644
+ let message = ChatMessage {
645
+ role: ChatRole::User,
646
+ content: vec![
647
+ ContentBlock::Text {
648
+ text: "what is this?".into(),
649
+ },
650
+ ContentBlock::Audio {
651
+ data_uri: "data:audio/wav;base64,QUJD".into(),
652
+ },
653
+ ],
654
+ };
655
+ let content = openai_user_content(&message).unwrap();
656
+ let parts = content.as_array().unwrap();
657
+ assert_eq!(parts.len(), 2);
658
+ assert_eq!(parts[1]["type"], "input_audio");
659
+ assert_eq!(parts[1]["input_audio"]["format"], "wav");
660
+ }
661
+
662
+ #[test]
663
+ fn parses_openai_style_reply_with_tool_calls() {
664
+ let response = json!({
665
+ "choices": [{
666
+ "finish_reason": "tool_calls",
667
+ "message": {
668
+ "content": null,
669
+ "tool_calls": [{
670
+ "id": "call_1",
671
+ "function": { "name": "read_file", "arguments": "{\"path\":\"main.rs\"}" }
672
+ }]
673
+ }
674
+ }]
675
+ });
676
+ let reply = parse_openai_style_reply("gpt-x".into(), &response).unwrap();
677
+ assert_eq!(reply.text, "");
678
+ let calls = reply.tool_calls.unwrap();
679
+ assert_eq!(calls[0].name, "read_file");
680
+ assert_eq!(calls[0].input["path"], "main.rs");
681
+ assert_eq!(reply.stop_reason.as_deref(), Some("tool_calls"));
682
+ }
683
+
684
+ #[test]
685
+ fn gemini_sanitize_schema_strips_additional_properties_at_any_depth() {
686
+ let schema = json!({
687
+ "type": "object",
688
+ "properties": {
689
+ "emotions": {
690
+ "type": "object",
691
+ "additionalProperties": { "type": "string" }
692
+ },
693
+ "nested": {
694
+ "type": "array",
695
+ "items": {
696
+ "type": "object",
697
+ "additionalProperties": true
698
+ }
699
+ }
700
+ }
701
+ });
702
+ let sanitized = gemini_sanitize_schema(&schema);
703
+ assert!(
704
+ sanitized["properties"]["emotions"]
705
+ .get("additionalProperties")
706
+ .is_none()
707
+ );
708
+ assert!(
709
+ sanitized["properties"]["nested"]["items"]
710
+ .get("additionalProperties")
711
+ .is_none()
712
+ );
713
+ // Everything else is preserved untouched.
714
+ assert_eq!(sanitized["properties"]["emotions"]["type"], "object");
715
+ }
716
+
717
+ #[test]
718
+ fn gemini_native_payload_strips_additional_properties_from_tool_schemas() {
719
+ let tool = ToolSpec {
720
+ name: "avatar_signal".into(),
721
+ description: "set avatar state".into(),
722
+ parameters: json!({
723
+ "type": "object",
724
+ "properties": {
725
+ "emotions": {
726
+ "type": "object",
727
+ "additionalProperties": { "type": "string" }
728
+ }
729
+ }
730
+ }),
731
+ };
732
+ let request = native_request(
733
+ vec![ChatMessage::text(ChatRole::User, "hi")],
734
+ Some(vec![tool]),
735
+ );
736
+ let payload = gemini_native_payload(&request).unwrap();
737
+ assert!(
738
+ payload["tools"][0]["functionDeclarations"][0]["parameters"]["properties"]["emotions"]
739
+ .get("additionalProperties")
740
+ .is_none()
741
+ );
742
+ }
743
+
744
+ #[test]
745
+ fn gemini_native_payload_uses_function_declarations_and_resolves_tool_result_names() {
746
+ let messages = vec![
747
+ ChatMessage::text(ChatRole::User, "check weather"),
748
+ ChatMessage {
749
+ role: ChatRole::Assistant,
750
+ content: vec![ContentBlock::ToolUse {
751
+ id: "call_0".into(),
752
+ name: "weather".into(),
753
+ input: json!({ "city": "Bangkok" }),
754
+ thought_signature: None,
755
+ }],
756
+ },
757
+ ChatMessage {
758
+ role: ChatRole::Tool,
759
+ content: vec![ContentBlock::ToolResult {
760
+ tool_use_id: "call_0".into(),
761
+ content: "32C, sunny".into(),
762
+ is_error: false,
763
+ }],
764
+ },
765
+ ];
766
+ let request = native_request(messages, Some(vec![sample_tool()]));
767
+ let payload = gemini_native_payload(&request).unwrap();
768
+
769
+ assert_eq!(
770
+ payload["tools"][0]["functionDeclarations"][0]["name"],
771
+ "read_file"
772
+ );
773
+ let contents = payload["contents"].as_array().unwrap();
774
+ assert_eq!(contents[1]["role"], "model");
775
+ assert_eq!(contents[1]["parts"][0]["functionCall"]["name"], "weather");
776
+ assert_eq!(contents[2]["role"], "user");
777
+ // Resolved back to the function name "weather", not the call id "call_0".
778
+ assert_eq!(
779
+ contents[2]["parts"][0]["functionResponse"]["name"],
780
+ "weather"
781
+ );
782
+ }
783
+
784
+ #[test]
785
+ fn gemini_native_payload_replays_the_thought_signature_on_the_next_turn() {
786
+ // Gemini 3.x rejects a replayed `functionCall` that's missing the
787
+ // `thoughtSignature` the model originally attached to it (400
788
+ // INVALID_ARGUMENT) — without this, every second tool-calling turn
789
+ // failed and silently fell back to a different, weaker provider.
790
+ let messages = vec![
791
+ ChatMessage::text(ChatRole::User, "check weather"),
792
+ ChatMessage {
793
+ role: ChatRole::Assistant,
794
+ content: vec![ContentBlock::ToolUse {
795
+ id: "call_0".into(),
796
+ name: "weather".into(),
797
+ input: json!({ "city": "Bangkok" }),
798
+ thought_signature: Some("opaque-signature-abc".into()),
799
+ }],
800
+ },
801
+ ];
802
+ let request = native_request(messages, Some(vec![sample_tool()]));
803
+ let payload = gemini_native_payload(&request).unwrap();
804
+
805
+ let contents = payload["contents"].as_array().unwrap();
806
+ assert_eq!(
807
+ contents[1]["parts"][0]["thoughtSignature"],
808
+ "opaque-signature-abc"
809
+ );
810
+ // Sibling of `functionCall`, not nested inside it.
811
+ assert_eq!(contents[1]["parts"][0]["functionCall"]["name"], "weather");
812
+ }
813
+
814
+ #[test]
815
+ fn gemini_native_payload_omits_thought_signature_when_absent() {
816
+ let messages = vec![ChatMessage {
817
+ role: ChatRole::Assistant,
818
+ content: vec![ContentBlock::ToolUse {
819
+ id: "call_0".into(),
820
+ name: "weather".into(),
821
+ input: json!({ "city": "Bangkok" }),
822
+ thought_signature: None,
823
+ }],
824
+ }];
825
+ let request = native_request(messages, Some(vec![sample_tool()]));
826
+ let payload = gemini_native_payload(&request).unwrap();
827
+
828
+ let contents = payload["contents"].as_array().unwrap();
829
+ assert!(contents[0]["parts"][0].get("thoughtSignature").is_none());
830
+ }
831
+
832
+ #[test]
833
+ fn parses_gemini_reply_with_function_call_parts() {
834
+ let response = json!({
835
+ "candidates": [{
836
+ "finishReason": "STOP",
837
+ "content": {
838
+ "parts": [
839
+ { "text": "Checking the weather now." },
840
+ { "functionCall": { "name": "weather", "args": { "city": "Bangkok" } } }
841
+ ]
842
+ }
843
+ }]
844
+ });
845
+ let reply = parse_gemini_reply("gemini-x".into(), &response).unwrap();
846
+ assert_eq!(reply.text, "Checking the weather now.");
847
+ let calls = reply.tool_calls.unwrap();
848
+ assert_eq!(calls[0].name, "weather");
849
+ assert_eq!(calls[0].input["city"], "Bangkok");
850
+ }
851
+
852
+ #[test]
853
+ fn parses_gemini_reply_captures_the_thought_signature_off_the_function_call_part() {
854
+ let response = json!({
855
+ "candidates": [{
856
+ "finishReason": "STOP",
857
+ "content": {
858
+ "parts": [{
859
+ "functionCall": { "name": "weather", "args": { "city": "Bangkok" } },
860
+ "thoughtSignature": "opaque-signature-abc"
861
+ }]
862
+ }
863
+ }]
864
+ });
865
+ let reply = parse_gemini_reply("gemini-x".into(), &response).unwrap();
866
+ let calls = reply.tool_calls.unwrap();
867
+ assert_eq!(
868
+ calls[0].thought_signature.as_deref(),
869
+ Some("opaque-signature-abc")
870
+ );
871
+ }
872
+
873
+ #[test]
874
+ fn ollama_native_messages_send_tool_call_arguments_as_object_not_string() {
875
+ let messages = vec![
876
+ ChatMessage::text(ChatRole::User, "list files"),
877
+ ChatMessage {
878
+ role: ChatRole::Assistant,
879
+ content: vec![ContentBlock::ToolUse {
880
+ id: "call_1".into(),
881
+ name: "list_files".into(),
882
+ input: json!({ "path": "." }),
883
+ thought_signature: None,
884
+ }],
885
+ },
886
+ ];
887
+ let built = ollama_native_messages(&messages);
888
+ assert_eq!(built[1]["tool_calls"][0]["function"]["name"], "list_files");
889
+ // Ollama expects `arguments` as a JSON object, unlike OpenAI's stringified form.
890
+ assert!(built[1]["tool_calls"][0]["function"]["arguments"].is_object());
891
+ }
892
+
893
+ #[test]
894
+ fn ollama_native_messages_forward_a_screenshot_riding_a_tool_result_as_a_user_turn() {
895
+ // Ollama's `/api/chat` `tool` role message has no image field, so an
896
+ // `Image` block attached to a tool result (e.g. `view_image`)
897
+ // must surface as a follow-up `user` message with the `images` field.
898
+ let messages = vec![ChatMessage {
899
+ role: ChatRole::Tool,
900
+ content: vec![
901
+ ContentBlock::ToolResult {
902
+ tool_use_id: "call_1".into(),
903
+ content: "[Screenshot captured — see attached image]".into(),
904
+ is_error: false,
905
+ },
906
+ ContentBlock::Image {
907
+ data_uri: "data:image/png;base64,QUJD".into(),
908
+ },
909
+ ],
910
+ }];
911
+ let built = ollama_native_messages(&messages);
912
+ assert_eq!(built.len(), 2);
913
+ assert_eq!(built[0]["role"], "tool");
914
+ assert_eq!(built[1]["role"], "user");
915
+ assert_eq!(built[1]["images"][0], "QUJD");
916
+ }
917
+
918
+ #[test]
919
+ fn parses_ollama_reply_from_native_message_envelope() {
920
+ let response = json!({
921
+ "message": {
922
+ "content": "",
923
+ "tool_calls": [{
924
+ "function": { "name": "list_files", "arguments": { "path": "." } }
925
+ }]
926
+ }
927
+ });
928
+ let reply = parse_ollama_reply("llama3.1".into(), &response).unwrap();
929
+ assert_eq!(reply.tool_calls.unwrap()[0].name, "list_files");
930
+ }
931
+
932
+ #[test]
933
+ fn extracts_input_and_output_tokens_from_each_provider_usage_shape() {
934
+ let anthropic = parse_anthropic_reply(
935
+ "claude-x".into(),
936
+ &json!({
937
+ "content": [{ "type": "text", "text": "hi" }],
938
+ "usage": { "input_tokens": 100, "output_tokens": 20 }
939
+ }),
940
+ )
941
+ .unwrap();
942
+ assert_eq!(
943
+ (anthropic.input_tokens, anthropic.output_tokens),
944
+ (Some(100), Some(20))
945
+ );
946
+
947
+ let openai = parse_openai_style_reply(
948
+ "gpt-x".into(),
949
+ &json!({
950
+ "choices": [{ "message": { "content": "hi" } }],
951
+ "usage": { "prompt_tokens": 130, "completion_tokens": 20, "total_tokens": 150 }
952
+ }),
953
+ )
954
+ .unwrap();
955
+ assert_eq!(
956
+ (openai.input_tokens, openai.output_tokens),
957
+ (Some(130), Some(20))
958
+ );
959
+
960
+ let gemini = parse_gemini_reply(
961
+ "gemini-x".into(),
962
+ &json!({
963
+ "candidates": [{ "content": { "parts": [{ "text": "hi" }] } }],
964
+ "usageMetadata": {
965
+ "promptTokenCount": 160,
966
+ "candidatesTokenCount": 40,
967
+ "totalTokenCount": 200
968
+ }
969
+ }),
970
+ )
971
+ .unwrap();
972
+ assert_eq!(
973
+ (gemini.input_tokens, gemini.output_tokens),
974
+ (Some(160), Some(40))
975
+ );
976
+
977
+ let ollama = parse_ollama_reply(
978
+ "llama3.1".into(),
979
+ &json!({
980
+ "message": { "content": "hi" },
981
+ "prompt_eval_count": 80,
982
+ "eval_count": 40
983
+ }),
984
+ )
985
+ .unwrap();
986
+ assert_eq!(
987
+ (ollama.input_tokens, ollama.output_tokens),
988
+ (Some(80), Some(40))
989
+ );
990
+ }
991
+ #[test]
992
+ fn anthropic_cached_prompt_tokens_count_toward_context_in_json_and_streams() {
993
+ let usage = json!({"input_tokens":100,"cache_read_input_tokens":98_000,"cache_creation_input_tokens":1_000,"output_tokens":400,"cache_creation":{"ephemeral_5m_input_tokens":1_000}});
994
+ let reply = parse_anthropic_reply(
995
+ "claude-test".into(),
996
+ &json!({"content":[{"type":"text","text":"Ready"}],"usage":usage}),
997
+ )
998
+ .unwrap();
999
+ assert_eq!(reply.input_tokens, Some(99_100));
1000
+ let mut stream = StreamReplyAccumulator::default();
1001
+ stream.consume_anthropic(
1002
+ &json!({"type":"message_start","message":{"usage":usage}}),
1003
+ &mut |_| {},
1004
+ );
1005
+ stream.consume_anthropic(
1006
+ &json!({"type":"content_block_delta","delta":{"type":"text_delta","text":"Ready"}}),
1007
+ &mut |_| {},
1008
+ );
1009
+ stream.consume_anthropic(
1010
+ &json!({"type":"message_delta","usage":{"output_tokens":400}}),
1011
+ &mut |_| {},
1012
+ );
1013
+ let reply = stream.finish("claude-test".into()).unwrap();
1014
+ assert_eq!(reply.input_tokens, Some(99_100));
1015
+ assert_eq!(reply.output_tokens, Some(400));
1016
+ }
1017
+
1018
+ #[test]
1019
+ fn missing_usage_data_yields_no_token_counts() {
1020
+ let reply = parse_openai_style_reply(
1021
+ "gpt-x".into(),
1022
+ &json!({ "choices": [{ "message": { "content": "hi" } }] }),
1023
+ )
1024
+ .unwrap();
1025
+ assert_eq!((reply.input_tokens, reply.output_tokens), (None, None));
1026
+ }
1027
+
1028
+ #[tokio::test]
1029
+ async fn send_openai_style_request_classifies_a_connect_failure_as_network_unavailable() {
1030
+ let client = Client::new();
1031
+ // Port 1 is reserved and nothing listens on it, so this fails fast
1032
+ // with a connection error rather than timing out.
1033
+ let result = send_openai_style_request(
1034
+ client.post("http://127.0.0.1:1/chat/completions"),
1035
+ "deepseek",
1036
+ )
1037
+ .await;
1038
+ assert!(
1039
+ matches!(result, Err(ChatError::NetworkUnavailable)),
1040
+ "expected NetworkUnavailable, got {result:?}"
1041
+ );
1042
+ }
1043
+
1044
+ #[tokio::test]
1045
+ async fn send_openai_style_request_classifies_402_as_insufficient_balance() {
1046
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
1047
+ use tokio::net::TcpListener;
1048
+
1049
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1050
+ let addr = listener.local_addr().unwrap();
1051
+ tokio::spawn(async move {
1052
+ let (mut socket, _) = listener.accept().await.unwrap();
1053
+ let mut buf = [0u8; 1024];
1054
+ let _ = socket.read(&mut buf).await;
1055
+ let body = "{}";
1056
+ let response = format!(
1057
+ "HTTP/1.1 402 Payment Required\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1058
+ body.len()
1059
+ );
1060
+ let _ = socket.write_all(response.as_bytes()).await;
1061
+ let _ = socket.shutdown().await;
1062
+ });
1063
+
1064
+ let client = Client::new();
1065
+ let result = send_openai_style_request(
1066
+ client.post(format!("http://{addr}/chat/completions")),
1067
+ "deepseek",
1068
+ )
1069
+ .await;
1070
+ assert!(
1071
+ matches!(&result, Err(ChatError::InsufficientBalance(p)) if p == "deepseek"),
1072
+ "expected InsufficientBalance(\"deepseek\"), got {result:?}"
1073
+ );
1074
+ }
1075
+
1076
+ #[test]
1077
+ fn fallback_provider_order_moves_gemini_to_the_front() {
1078
+ let config = MintConfig {
1079
+ anthropic_api_key: "a".into(),
1080
+ openai_api_key: "b".into(),
1081
+ deepseek_api_key: "c".into(),
1082
+ api_key: "d".into(), // gemini
1083
+ hf_api_key: "e".into(),
1084
+ ..MintConfig::default()
1085
+ };
1086
+ let order = fallback_provider_order(&config);
1087
+ assert_eq!(order[0], "gemini");
1088
+ // Everyone else keeps their existing relative order from
1089
+ // `available_providers()` — only gemini moves.
1090
+ let without_gemini: Vec<&String> = order.iter().skip(1).collect();
1091
+ assert_eq!(
1092
+ without_gemini,
1093
+ vec!["anthropic", "openai", "deepseek", "huggingface", "ollama"]
1094
+ );
1095
+ }
1096
+
1097
+ #[test]
1098
+ fn fallback_provider_order_is_unchanged_without_gemini_configured() {
1099
+ let config = MintConfig {
1100
+ deepseek_api_key: "c".into(),
1101
+ hf_api_key: "e".into(),
1102
+ ..MintConfig::default()
1103
+ };
1104
+ assert_eq!(
1105
+ fallback_provider_order(&config),
1106
+ config.available_providers()
1107
+ );
1108
+ }
1109
+
1110
+ #[test]
1111
+ fn openai_payload_includes_smart_temperature_for_deepseek_and_omits_for_o1() {
1112
+ let request = native_request(vec![ChatMessage::text(ChatRole::User, "hi")], None);
1113
+
1114
+ // DeepSeek smart default is 0.6
1115
+ let ds_config = MintConfig {
1116
+ ai_provider: "deepseek".into(),
1117
+ deepseek_model: "deepseek-chat".into(),
1118
+ ..MintConfig::default()
1119
+ };
1120
+ let ds_payload = openai_chat_payload(&ds_config, "deepseek-chat", &request, false).unwrap();
1121
+ assert_eq!(ds_payload["temperature"], 0.6);
1122
+
1123
+ // Standard OpenAI model default is 0.2
1124
+ let openai_config = MintConfig {
1125
+ ai_provider: "openai".into(),
1126
+ openai_model: "gpt-4o".into(),
1127
+ ..MintConfig::default()
1128
+ };
1129
+ let openai_payload = openai_chat_payload(&openai_config, "gpt-4o", &request, false).unwrap();
1130
+ assert_eq!(openai_payload["temperature"], 0.2);
1131
+
1132
+ // Reasoning models (o1, o3) omit temperature parameter
1133
+ let o1_payload = openai_chat_payload(&openai_config, "o1-preview", &request, false).unwrap();
1134
+ assert!(o1_payload.get("temperature").is_none());
1135
+
1136
+ let o3_payload = openai_chat_payload(&openai_config, "o3-mini", &request, false).unwrap();
1137
+ assert!(o3_payload.get("temperature").is_none());
1138
+
1139
+ // Explicit request temperature override
1140
+ let mut override_req = request.clone();
1141
+ override_req.temperature = Some(0.75);
1142
+ let override_payload =
1143
+ openai_chat_payload(&ds_config, "deepseek-chat", &override_req, false).unwrap();
1144
+ assert_eq!(override_payload["temperature"], 0.75);
1145
+ }
1146
+
1147
+ #[test]
1148
+ fn anthropic_payload_includes_effective_temperature() {
1149
+ let request = native_request(vec![ChatMessage::text(ChatRole::User, "hi")], None);
1150
+ let config = MintConfig {
1151
+ ai_provider: "anthropic".into(),
1152
+ anthropic_model: "claude-sonnet-5".into(),
1153
+ thinking_enabled: false,
1154
+ ..MintConfig::default()
1155
+ };
1156
+ let payload = anthropic_chat_payload(&config, "claude-sonnet-5", &request, false).unwrap();
1157
+ assert_eq!(payload["temperature"], 0.2);
1158
+ }
1159
+
1160
+ #[test]
1161
+ fn parse_openai_style_reply_extracts_deepseek_reasoning_content() {
1162
+ let response = json!({
1163
+ "choices": [{
1164
+ "message": {
1165
+ "role": "assistant",
1166
+ "content": "",
1167
+ "reasoning_content": "Evaluating 15 + 6 * (8 - 3) - 14 / 2 step by step...",
1168
+ "tool_calls": [{
1169
+ "id": "call_calc_1",
1170
+ "type": "function",
1171
+ "function": {
1172
+ "name": "calculation",
1173
+ "arguments": "{\"expression\":\"15 + 6 * (8 - 3) - 14 / 2\"}"
1174
+ }
1175
+ }]
1176
+ },
1177
+ "finish_reason": "tool_calls"
1178
+ }]
1179
+ });
1180
+ let reply = parse_openai_style_reply("deepseek-reasoner".into(), &response).unwrap();
1181
+ assert_eq!(reply.text, "");
1182
+ assert_eq!(
1183
+ reply.thought.as_deref(),
1184
+ Some("Evaluating 15 + 6 * (8 - 3) - 14 / 2 step by step...")
1185
+ );
1186
+ let calls = reply.tool_calls.unwrap();
1187
+ assert_eq!(calls.len(), 1);
1188
+ assert_eq!(calls[0].name, "calculation");
1189
+ }
1190
+
1191
+ #[test]
1192
+ fn parse_openai_style_reply_extracts_openrouter_reasoning() {
1193
+ let response = json!({
1194
+ "choices": [{
1195
+ "message": {
1196
+ "role": "assistant",
1197
+ "content": "The answer is 38.",
1198
+ "reasoning": "First parentheses: 8 - 3 = 5..."
1199
+ },
1200
+ "finish_reason": "stop"
1201
+ }]
1202
+ });
1203
+ let reply = parse_openai_style_reply("deepseek/deepseek-r1".into(), &response).unwrap();
1204
+ assert_eq!(reply.text, "The answer is 38.");
1205
+ assert_eq!(
1206
+ reply.thought.as_deref(),
1207
+ Some("First parentheses: 8 - 3 = 5...")
1208
+ );
1209
+ }
1210
+
1211
+ #[test]
1212
+ fn parse_openai_style_reply_extracts_think_tags() {
1213
+ let response = json!({
1214
+ "choices": [{
1215
+ "message": {
1216
+ "role": "assistant",
1217
+ "content": "<think>\nLet me solve this problem.\n</think>\nThe result is 42."
1218
+ },
1219
+ "finish_reason": "stop"
1220
+ }]
1221
+ });
1222
+ let reply = parse_openai_style_reply("qwq-32b".into(), &response).unwrap();
1223
+ assert_eq!(reply.text, "The result is 42.");
1224
+ assert_eq!(reply.thought.as_deref(), Some("Let me solve this problem."));
1225
+ }
1226
+
1227
+ #[test]
1228
+ fn parse_gemini_reply_extracts_thought_parts() {
1229
+ let response = json!({
1230
+ "candidates": [{
1231
+ "content": {
1232
+ "parts": [
1233
+ { "text": "Thinking through the formula...", "thought": true },
1234
+ { "text": "The answer is 38." }
1235
+ ]
1236
+ },
1237
+ "finishReason": "STOP"
1238
+ }]
1239
+ });
1240
+ let reply = parse_gemini_reply("gemini-2.0-flash-thinking".into(), &response).unwrap();
1241
+ assert_eq!(reply.text, "The answer is 38.");
1242
+ assert_eq!(
1243
+ reply.thought.as_deref(),
1244
+ Some("Thinking through the formula...")
1245
+ );
1246
+ }
1247
+
1248
+ #[test]
1249
+ fn parse_anthropic_reply_extracts_thinking_blocks() {
1250
+ let response = json!({
1251
+ "content": [
1252
+ { "type": "thinking", "thinking": "Let's break down the arithmetic..." },
1253
+ { "type": "text", "text": "The result is 38." }
1254
+ ],
1255
+ "stop_reason": "end_turn"
1256
+ });
1257
+ let reply = parse_anthropic_reply("claude-3-7-sonnet".into(), &response).unwrap();
1258
+ assert_eq!(reply.text, "The result is 38.");
1259
+ assert_eq!(
1260
+ reply.thought.as_deref(),
1261
+ Some("Let's break down the arithmetic...")
1262
+ );
1263
+ }
1264
+
1265
+ #[test]
1266
+ fn parse_ollama_reply_extracts_thinking_field() {
1267
+ let response = json!({
1268
+ "message": {
1269
+ "role": "assistant",
1270
+ "content": "Result is 38.",
1271
+ "thinking": "Ollama internal thinking trace..."
1272
+ }
1273
+ });
1274
+ let reply = parse_ollama_reply("deepseek-r1:8b".into(), &response).unwrap();
1275
+ assert_eq!(reply.text, "Result is 38.");
1276
+ assert_eq!(
1277
+ reply.thought.as_deref(),
1278
+ Some("Ollama internal thinking trace...")
1279
+ );
1280
+ }
1281
+
1282
+ #[test]
1283
+ fn decode_tool_arguments_sanitizes_fences_tokens_and_trailing_commas() {
1284
+ // 1. Markdown fences
1285
+ let fenced =
1286
+ Value::String("```json\n{\"command\": \"curl -s https://example.com\"}\n```".into());
1287
+ let decoded = decode_tool_arguments(&fenced);
1288
+ assert_eq!(decoded["command"], "curl -s https://example.com");
1289
+
1290
+ // 2. DeepSeek DSML token
1291
+ let dsml = Value::String("{\"command\": \"ls -la\"}</||DSML||".into());
1292
+ let decoded_dsml = decode_tool_arguments(&dsml);
1293
+ assert_eq!(decoded_dsml["command"], "ls -la");
1294
+
1295
+ // 3. Trailing commas
1296
+ let trailing = Value::String("{\"command\": \"echo ok\", \"flag\": true, }".into());
1297
+ let decoded_trailing = decode_tool_arguments(&trailing);
1298
+ assert_eq!(decoded_trailing["command"], "echo ok");
1299
+ assert_eq!(decoded_trailing["flag"], true);
1300
+ }
1301
+
1302
+ #[test]
1303
+ fn gemini_thinking_config_includes_thoughts_flag_and_skips_non_native_models() {
1304
+ let mut config = MintConfig::default();
1305
+ config.ai_provider = "gemini".into();
1306
+ config.gemini_model = "gemini-2.5-flash".into();
1307
+
1308
+ let request_on = ChatRequest {
1309
+ message: "Solve 12 * 13".into(),
1310
+ thinking_enabled: Some(true),
1311
+ thinking_effort: Some("high".into()),
1312
+ ..Default::default()
1313
+ };
1314
+ let payload_on = gemini_chat_payload(&config, &request_on).unwrap();
1315
+ assert_eq!(
1316
+ payload_on["generationConfig"]["thinkingConfig"]["includeThoughts"],
1317
+ true
1318
+ );
1319
+ assert_eq!(
1320
+ payload_on["generationConfig"]["thinkingConfig"]["thinkingBudget"],
1321
+ 16384
1322
+ );
1323
+
1324
+ let request_off = ChatRequest {
1325
+ message: "Hello".into(),
1326
+ thinking_enabled: Some(false),
1327
+ ..Default::default()
1328
+ };
1329
+ let payload_off = gemini_chat_payload(&config, &request_off).unwrap();
1330
+ assert_eq!(
1331
+ payload_off["generationConfig"]["thinkingConfig"]["includeThoughts"],
1332
+ false
1333
+ );
1334
+ assert_eq!(
1335
+ payload_off["generationConfig"]["thinkingConfig"]["thinkingBudget"],
1336
+ 0
1337
+ );
1338
+
1339
+ // Non-text image generation model does not attach thinkingConfig or prompt directive
1340
+ config.gemini_model = "nano-banana-pro-preview".into();
1341
+ let payload_banana = gemini_chat_payload(&config, &request_on).unwrap();
1342
+ assert!(
1343
+ payload_banana["generationConfig"]
1344
+ .get("thinkingConfig")
1345
+ .is_none()
1346
+ );
1347
+ let banana_sys = payload_banana["systemInstruction"]["parts"][0]["text"]
1348
+ .as_str()
1349
+ .unwrap_or("");
1350
+ assert!(!banana_sys.contains("<think>"));
1351
+ }
1352
+
1353
+ #[test]
1354
+ fn deepseek_v4_and_prompt_fallback_thinking_payloads() {
1355
+ let mut config = MintConfig::default();
1356
+ config.ai_provider = "deepseek".into();
1357
+ config.deepseek_model = "deepseek-flash".into();
1358
+
1359
+ let req_on = ChatRequest {
1360
+ message: "Explain quantum entanglement".into(),
1361
+ system_instruction: "You are helpful.".into(),
1362
+ thinking_enabled: Some(true),
1363
+ thinking_effort: Some("extra_high".into()),
1364
+ ..Default::default()
1365
+ };
1366
+ let payload_on = openai_chat_payload(&config, "deepseek-flash", &req_on, false).unwrap();
1367
+ assert_eq!(payload_on["reasoning_effort"], "max");
1368
+ assert_eq!(payload_on["thinking"]["type"], "enabled");
1369
+ assert!(payload_on.get("temperature").is_none());
1370
+ // Native thinking model should not get prompt-fallback directive
1371
+ let sys_on = payload_on["messages"][0]["content"].as_str().unwrap();
1372
+ assert_eq!(sys_on, "You are helpful.");
1373
+
1374
+ let req_off = ChatRequest {
1375
+ message: "Hi".into(),
1376
+ system_instruction: "You are helpful.".into(),
1377
+ thinking_enabled: Some(false),
1378
+ ..Default::default()
1379
+ };
1380
+ let payload_off = openai_chat_payload(&config, "deepseek-flash", &req_off, false).unwrap();
1381
+ assert_eq!(payload_off["reasoning_effort"], "none");
1382
+ assert_eq!(payload_off["thinking"]["type"], "disabled");
1383
+ assert!(payload_off.get("temperature").is_some());
1384
+
1385
+ // Non-native chat model (`gpt-4o`) with thinking_enabled = true gets prompt-based CoT directive
1386
+ config.ai_provider = "openai".into();
1387
+ config.openai_model = "gpt-4o".into();
1388
+ let payload_fallback = openai_chat_payload(&config, "gpt-4o", &req_on, false).unwrap();
1389
+ let sys_fallback = payload_fallback["messages"][0]["content"].as_str().unwrap();
1390
+ assert!(sys_fallback.contains("[Thinking Mode: Enabled (Extra High Effort)]"));
1391
+ assert!(sys_fallback.contains("<think>...</think>"));
1392
+ }
1393
+
1394
+ #[test]
1395
+ fn extract_think_tag_supports_both_think_and_thought_tags() {
1396
+ let (thought1, cleaned1) = extract_think_tag("<think>\nStep 1\n</think>\nFinal answer 1");
1397
+ assert_eq!(cleaned1, "Final answer 1");
1398
+ assert_eq!(thought1.as_deref(), Some("Step 1"));
1399
+
1400
+ let (thought2, cleaned2) = extract_think_tag("<thought>\nStep 2\n</thought>\nFinal answer 2");
1401
+ assert_eq!(cleaned2, "Final answer 2");
1402
+ assert_eq!(thought2.as_deref(), Some("Step 2"));
1403
+ }
1404
+
1405
+ #[tokio::test]
1406
+ async fn collect_stream_reply_preserves_multibyte_utf8_split_across_chunks() {
1407
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
1408
+ use tokio::net::TcpListener;
1409
+
1410
+ let expected_text = "ข้อมูลนี้ค้นหาและตรวจสอบผ่าน Brave Search เรียบร้อยแล้วค่ะ";
1411
+ let sse_line = format!(
1412
+ "data: {}\n\n",
1413
+ json!({
1414
+ "candidates": [{
1415
+ "content": {
1416
+ "parts": [{"text": expected_text}]
1417
+ }
1418
+ }]
1419
+ })
1420
+ );
1421
+ let bytes = sse_line.into_bytes();
1422
+ // Locate the 3-byte UTF-8 sequence for Thai tone mark `้` (0xE0, 0xB9, 0x89)
1423
+ // and split the HTTP chunk right after the first byte (0xE0).
1424
+ let split_index = bytes
1425
+ .windows(3)
1426
+ .position(|w| w == [0xE0, 0xB9, 0x89])
1427
+ .expect("should contain Thai tone mark UTF-8 bytes")
1428
+ + 1;
1429
+ let first_chunk = bytes[..split_index].to_vec();
1430
+ let second_chunk = bytes[split_index..].to_vec();
1431
+
1432
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1433
+ let addr = listener.local_addr().unwrap();
1434
+ tokio::spawn(async move {
1435
+ let (mut socket, _) = listener.accept().await.unwrap();
1436
+ let mut buf = [0u8; 1024];
1437
+ let _ = socket.read(&mut buf).await;
1438
+ let headers = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n";
1439
+ socket.write_all(headers.as_bytes()).await.unwrap();
1440
+ socket
1441
+ .write_all(format!("{:X}\r\n", first_chunk.len()).as_bytes())
1442
+ .await
1443
+ .unwrap();
1444
+ socket.write_all(&first_chunk).await.unwrap();
1445
+ socket.write_all(b"\r\n").await.unwrap();
1446
+ socket.flush().await.unwrap();
1447
+
1448
+ socket
1449
+ .write_all(format!("{:X}\r\n", second_chunk.len()).as_bytes())
1450
+ .await
1451
+ .unwrap();
1452
+ socket.write_all(&second_chunk).await.unwrap();
1453
+ socket.write_all(b"\r\n0\r\n\r\n").await.unwrap();
1454
+ let _ = socket.shutdown().await;
1455
+ });
1456
+
1457
+ let client = Client::new();
1458
+ let response = client
1459
+ .get(format!("http://{addr}/stream"))
1460
+ .send()
1461
+ .await
1462
+ .unwrap();
1463
+ let mut streamed = String::new();
1464
+ let reply = collect_stream_reply(
1465
+ response,
1466
+ StreamFormat::Gemini,
1467
+ "gemini-3.1-flash-lite-preview".into(),
1468
+ &mut |event| {
1469
+ if let ChatStreamEvent::TextDelta { delta } = event {
1470
+ streamed.push_str(&delta);
1471
+ }
1472
+ },
1473
+ )
1474
+ .await
1475
+ .unwrap();
1476
+
1477
+ assert_eq!(reply.text, expected_text);
1478
+ assert_eq!(streamed, expected_text);
1479
+ }