@pheem49/mint 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (421) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +2 -2
  3. package/README.md +8 -2
  4. package/Release_Note.md +222 -71
  5. package/crates/mint-cli/src/agent/diff_render.rs +66 -44
  6. package/crates/mint-cli/src/agent/live_status.rs +21 -4
  7. package/crates/mint-cli/src/agent/markdown_render.rs +342 -5
  8. package/crates/mint-cli/src/agent.rs +25 -10
  9. package/crates/mint-cli/src/commands/agent.rs +4 -92
  10. package/crates/mint-cli/src/commands/browser.rs +2 -15
  11. package/crates/mint-cli/src/commands/integrations.rs +3 -3
  12. package/crates/mint-cli/src/commands/mod.rs +0 -3
  13. package/crates/mint-cli/src/commands/tailnet_web.rs +1 -0
  14. package/crates/mint-cli/src/interactive/chat_tui/dialog.rs +572 -0
  15. package/crates/mint-cli/src/interactive/chat_tui/mod.rs +1427 -0
  16. package/crates/mint-cli/src/interactive/chat_tui/render.rs +1524 -0
  17. package/crates/mint-cli/src/interactive/chat_tui/selection.rs +401 -0
  18. package/crates/mint-cli/src/interactive/chat_tui/state.rs +1833 -0
  19. package/crates/mint-cli/src/interactive/chat_tui/tests.rs +3414 -0
  20. package/crates/mint-cli/src/interactive/chat_tui/thought.rs +202 -0
  21. package/crates/mint-cli/src/interactive/chat_tui/transcript.rs +728 -0
  22. package/crates/mint-cli/src/interactive/chat_tui/watermark.rs +148 -0
  23. package/crates/mint-cli/src/interactive/command_ui.rs +53 -5
  24. package/crates/mint-cli/src/interactive/composer.rs +186 -0
  25. package/crates/mint-cli/src/interactive/format.rs +78 -3
  26. package/crates/mint-cli/src/interactive/input_box.rs +61 -1
  27. package/crates/mint-cli/src/interactive/resume_picker.rs +367 -25
  28. package/crates/mint-cli/src/interactive/slash_commands.rs +648 -371
  29. package/crates/mint-cli/src/interactive/tool_activity.rs +93 -20
  30. package/crates/mint-cli/src/interactive.rs +37 -0
  31. package/crates/mint-cli/src/main.rs +6 -3
  32. package/crates/mint-cli/src/markdown.rs +2 -37
  33. package/crates/mint-cli/src/onboard.rs +700 -282
  34. package/crates/mint-cli/src/setup.rs +1342 -624
  35. package/crates/mint-cli/src/terminal_theme.rs +159 -35
  36. package/crates/mint-cli/tests/library_intake.rs +1 -0
  37. package/crates/mint-core/src/agent/chat/anthropic.rs +318 -0
  38. package/crates/mint-core/src/agent/chat/custom.rs +149 -0
  39. package/crates/mint-core/src/agent/chat/gemini.rs +518 -0
  40. package/crates/mint-core/src/agent/chat/mod.rs +857 -0
  41. package/crates/mint-core/src/agent/chat/ollama.rs +321 -0
  42. package/crates/mint-core/src/agent/chat/openai.rs +582 -0
  43. package/crates/mint-core/src/agent/chat/streaming.rs +469 -0
  44. package/crates/mint-core/src/agent/chat/tests.rs +1479 -0
  45. package/crates/mint-core/src/agent/memory.rs +56 -2
  46. package/crates/mint-core/src/agent/tasks.rs +2 -2
  47. package/crates/mint-core/src/api_server/routes/chat.rs +24 -2
  48. package/crates/mint-core/src/api_server/routes/gemini_live.rs +14 -1
  49. package/crates/mint-core/src/api_server/routes/html_preview.rs +1 -8
  50. package/crates/mint-core/src/api_server/routes/sessions.rs +21 -0
  51. package/crates/mint-core/src/avatar_bridge.rs +0 -22
  52. package/crates/mint-core/src/browser/cdp.rs +10 -4
  53. package/crates/mint-core/src/browser/headless.rs +582 -0
  54. package/crates/mint-core/src/browser/input.rs +7 -62
  55. package/crates/mint-core/src/browser/interact.rs +0 -14
  56. package/crates/mint-core/src/browser/mod.rs +8 -29
  57. package/crates/mint-core/src/browser/navigate.rs +2 -36
  58. package/crates/mint-core/src/browser/runtime.rs +2 -4
  59. package/crates/mint-core/src/browser/session.rs +50 -466
  60. package/crates/mint-core/src/browser/workflow.rs +142 -32
  61. package/crates/mint-core/src/cron/store.rs +2 -2
  62. package/crates/mint-core/src/integrations/mcp.rs +6 -8
  63. package/crates/mint-core/src/lib.rs +0 -5
  64. package/crates/mint-core/src/media/gemini_live.rs +636 -86
  65. package/crates/mint-core/src/media/library/migration.rs +26 -0
  66. package/crates/mint-core/src/media/library/tests.rs +1 -1
  67. package/crates/mint-core/src/media/library/thumbnails.rs +8 -2
  68. package/crates/mint-core/src/media/pictures.rs +1 -1
  69. package/crates/mint-core/src/media/tool_images.rs +92 -1
  70. package/crates/mint-core/src/mods/manager.rs +101 -35
  71. package/crates/mint-core/src/orchestration/context_render.rs +441 -24
  72. package/crates/mint-core/src/orchestration/decision_parsing.rs +531 -16
  73. package/crates/mint-core/src/orchestration/full_skill_result_tests.rs +12 -0
  74. package/crates/mint-core/src/orchestration/loop_guard.rs +145 -0
  75. package/crates/mint-core/src/orchestration/mcp_outcome_tests.rs +864 -0
  76. package/crates/mint-core/src/orchestration/memory_skill.rs +88 -28
  77. package/crates/mint-core/src/orchestration/mod.rs +320 -4249
  78. package/crates/mint-core/src/orchestration/reliability.rs +67 -27
  79. package/crates/mint-core/src/orchestration/review_regressions.rs +5 -0
  80. package/crates/mint-core/src/orchestration/tests.rs +1665 -0
  81. package/crates/mint-core/src/orchestration/tools/dispatch.rs +967 -0
  82. package/crates/mint-core/src/orchestration/tools/files.rs +1 -0
  83. package/crates/mint-core/src/orchestration/tools/mod.rs +5 -1
  84. package/crates/mint-core/src/orchestration/tools/planning.rs +102 -42
  85. package/crates/mint-core/src/orchestration/tools/shell.rs +129 -58
  86. package/crates/mint-core/src/orchestration/truncation.rs +170 -13
  87. package/crates/mint-core/src/orchestration/types.rs +648 -0
  88. package/crates/mint-core/src/orchestration/verification.rs +0 -3
  89. package/crates/mint-core/src/orchestration/workspace_helpers.rs +24 -6
  90. package/crates/mint-core/src/prompts/agent.rs +52 -76
  91. package/crates/mint-core/src/prompts/tool_catalog.rs +8 -151
  92. package/crates/mint-core/src/search/files.rs +20 -0
  93. package/crates/mint-core/src/slash/mod.rs +228 -265
  94. package/crates/mint-core/src/slash/models.rs +6 -1
  95. package/crates/mint-core/src/system/bg_shell.rs +1 -0
  96. package/crates/mint-core/src/system/config.rs +176 -34
  97. package/crates/mint-mod-runtime/src/lib.rs +37 -23
  98. package/out/web/assets/ArtifactPreviewPanel-Cx6BVOOF.js +2 -0
  99. package/out/web/assets/CalculationCard-D2E74u5q.js +1 -0
  100. package/out/web/assets/CliSessionsView-ZbdtSjvK.css +1 -0
  101. package/out/web/assets/{CliSessionsView-MI8kXMAb.js → CliSessionsView-z5i6qGyb.js} +1 -1
  102. package/out/web/assets/CodeReviewPage-BK2bOScd.css +1 -0
  103. package/out/web/assets/CodeReviewPage-Dp6zHuHu.js +2 -0
  104. package/out/web/assets/{CommandPalette-BD8SlTE5.js → CommandPalette-DaKlzbKs.js} +1 -1
  105. package/out/web/assets/ImageGenCard-BMCJHP9O.js +1 -0
  106. package/out/web/assets/ImageSearchCard-Cld_SOUg.js +1 -0
  107. package/out/web/assets/{ImageStudioPanel-BP7N4rLr.js → ImageStudioPanel-B5N7TjeB.js} +1 -1
  108. package/out/web/assets/LibraryPage-CMB89rJ0.css +1 -0
  109. package/out/web/assets/LibraryPage-DbZEi4W_.js +2 -0
  110. package/out/web/assets/LinkedFoldersView-9B9DIPoZ.js +1 -0
  111. package/out/web/assets/{McpRegistryPicker-CoS83vtc.js → McpRegistryPicker-DnXPlzI7.js} +1 -1
  112. package/out/web/assets/McpServersView-CiNBNUl4.js +1 -0
  113. package/out/web/assets/MermaidCard-BBDNEXbY.js +2 -0
  114. package/out/web/assets/MintDashboard-C_UYPoNV.js +119 -0
  115. package/out/web/assets/MintDashboard-DTd8C_c8.css +1 -0
  116. package/out/web/assets/PlanCard-C2x5pajq.js +1 -0
  117. package/out/web/assets/{PluginsView-jBYmcKh4.js → PluginsView-DoTg9MVa.js} +1 -1
  118. package/out/web/assets/RewindModal-B5oowrR0.js +1 -0
  119. package/out/web/assets/{ScheduledTasksView-5uw9-V9H.js → ScheduledTasksView-BD5DVPkZ.js} +1 -1
  120. package/out/web/assets/{SelectField-DXBQtrUK.js → SelectField-Cy6CghrU.js} +1 -1
  121. package/out/web/assets/SettingsWindow-BYl_oCFh.js +4 -0
  122. package/out/web/assets/SettingsWindow-Ckcgnjt8.css +1 -0
  123. package/out/web/assets/{SkillsView-C-gp9J7m.js → SkillsView-CIeWwq5z.js} +1 -1
  124. package/out/web/assets/StockCard-DxUciKyp.js +1 -0
  125. package/out/web/assets/ToolSurfacePage-qBgs_VHq.css +1 -0
  126. package/out/web/assets/ToolSurfacePage-saVbVrkE.js +38 -0
  127. package/out/web/assets/{UiFeatureCard-6BwxTOCe.js → UiFeatureCard-CdY0hGWy.js} +1 -1
  128. package/out/web/assets/{UiGridCard-DvZcLNxb.js → UiGridCard-BBK3I5hC.js} +1 -1
  129. package/out/web/assets/UiMockupWidget-CpR9EDuc.js +1 -0
  130. package/out/web/assets/{VeoStudioPanel-DOj7_WR3.css → VeoStudioPanel-9kU9YLEN.css} +1 -1
  131. package/out/web/assets/{VeoStudioPanel-DfsOJKv0.js → VeoStudioPanel-Co6-wEWM.js} +2 -2
  132. package/out/web/assets/WeatherCard-Buzi7Vfu.js +1 -0
  133. package/out/web/assets/WorkspacePdfPreview-Bi8Zzyir.js +2 -0
  134. package/out/web/assets/{abnfDiagram-VCTEODGH-BOo3Ckng.js → abnfDiagram-VCTEODGH-CMfR95N2.js} +1 -1
  135. package/out/web/assets/{architecture-7GRP2DOG-ZOwheW3n.js → architecture-7GRP2DOG-DhAM4HyO.js} +1 -1
  136. package/out/web/assets/{architectureDiagram-5GKGNRK7-DC2FDzVd.js → architectureDiagram-5GKGNRK7-Yk3TUJ_F.js} +1 -1
  137. package/out/web/assets/{blockDiagram-I7D4REHJ-BrKFWdAH.js → blockDiagram-I7D4REHJ-X9fBgJik.js} +1 -1
  138. package/out/web/assets/{c4Diagram-7LVT6UL2-BHTHK1Yd.js → c4Diagram-7LVT6UL2-Bk9x7N-8.js} +1 -1
  139. package/out/web/assets/{chunk-4HAMMTFA-BYFkZwNj.js → chunk-4HAMMTFA-Dx5fk1_0.js} +1 -1
  140. package/out/web/assets/{chunk-75Z2AOVW-BSKfSKSA.js → chunk-75Z2AOVW-BszTQoM4.js} +1 -1
  141. package/out/web/assets/{chunk-DU6HZSFF-vIhsiW6n.js → chunk-DU6HZSFF-DK-As3rk.js} +1 -1
  142. package/out/web/assets/{chunk-F27PBJKO-BlYpW9xq.js → chunk-F27PBJKO-LmncZZIS.js} +1 -1
  143. package/out/web/assets/{chunk-GMAD6QVW-BIBz_Ug5.js → chunk-GMAD6QVW-DSQ_sDRJ.js} +1 -1
  144. package/out/web/assets/{chunk-GVQU2GXP-NPSHxeqC.js → chunk-GVQU2GXP-Cno3ulje.js} +1 -1
  145. package/out/web/assets/{chunk-IMKFNOWR-YlJK8_Fp.js → chunk-IMKFNOWR-bIzfY9Jc.js} +1 -1
  146. package/out/web/assets/{chunk-L3NEJ4N5-DMYi4-af.js → chunk-L3NEJ4N5-Dl6eOa0D.js} +1 -1
  147. package/out/web/assets/{chunk-OSK3NFVY-Begd3-jA.js → chunk-OSK3NFVY-BaUGJ-cM.js} +1 -1
  148. package/out/web/assets/{chunk-P2QGCYS3-D1I4xFHN.js → chunk-P2QGCYS3-_066ndhH.js} +1 -1
  149. package/out/web/assets/{chunk-POPQ4Y6H-CafZZzNE.js → chunk-POPQ4Y6H-YT1AKhTv.js} +1 -1
  150. package/out/web/assets/{chunk-PWAF6VOD-Dwh6_Lnt.js → chunk-PWAF6VOD-CrEgDsxh.js} +1 -1
  151. package/out/web/assets/{chunk-SHT3W25Y-B0XrCSEb.js → chunk-SHT3W25Y-_KwaRLOl.js} +1 -1
  152. package/out/web/assets/{chunk-SVP7TREG-CxLsddxC.js → chunk-SVP7TREG-BQ1qb32L.js} +1 -1
  153. package/out/web/assets/{chunk-TICWLB2K-y156QInb.js → chunk-TICWLB2K-BDyehCtg.js} +1 -1
  154. package/out/web/assets/classDiagram-ZZMXUADV-BoSpzRCJ.js +1 -0
  155. package/out/web/assets/classDiagram-v2-VYDZK3BY-BoSpzRCJ.js +1 -0
  156. package/out/web/assets/{config-DqXJXeZM.css → config-2suVFPZg.css} +1 -1
  157. package/out/web/assets/config-CkRCdkxn.js +2 -0
  158. package/out/web/assets/{cynefin-OW5HDTMX-SMVvrLf-.js → cynefin-OW5HDTMX-Dl_31aaN.js} +1 -1
  159. package/out/web/assets/{cynefinDiagram-5FMLGOSQ-rZznN24T.js → cynefinDiagram-5FMLGOSQ-Bpir8jha.js} +1 -1
  160. package/out/web/assets/{dagre-GXQ25YYZ-BOToydFu.js → dagre-GXQ25YYZ-D9xu3nsq.js} +1 -1
  161. package/out/web/assets/{diagram-S7CK7UJ4-TMVaGQEJ.js → diagram-S7CK7UJ4-DeveoEeB.js} +1 -1
  162. package/out/web/assets/{diagram-UQ7AKVKN-C2JK-GP2.js → diagram-UQ7AKVKN-p4xWRESV.js} +1 -1
  163. package/out/web/assets/{diagram-VSXAHHWV-Cw8VSi2K.js → diagram-VSXAHHWV-C7qz9NDF.js} +1 -1
  164. package/out/web/assets/{diagram-VX7I27RA-DXp4fKk9.js → diagram-VX7I27RA-DRiCa3Vd.js} +1 -1
  165. package/out/web/assets/{diagram-Z3DM3KII-CzSicYiW.js → diagram-Z3DM3KII-Bt7zduQX.js} +1 -1
  166. package/out/web/assets/{ebnfDiagram-PWID7BFC-Dqj-zqPu.js → ebnfDiagram-PWID7BFC-DV9WpwdR.js} +1 -1
  167. package/out/web/assets/{erDiagram-RLTQ6QDP-DZwFZAuD.js → erDiagram-RLTQ6QDP-BJ_nwG2m.js} +1 -1
  168. package/out/web/assets/{eventmodeling-NTZA5JFV-C0O-Lvgp.js → eventmodeling-NTZA5JFV-BTBMrfVa.js} +1 -1
  169. package/out/web/assets/flowDiagram-HODETNUW-Cdlq3cJL.js +1 -0
  170. package/out/web/assets/{ganttDiagram-EL5Y4UJY-Da4ULXTl.js → ganttDiagram-EL5Y4UJY-DQLdsXrC.js} +1 -1
  171. package/out/web/assets/{gitGraph-4MIJSDKK-B9wNFZIP.js → gitGraph-4MIJSDKK-CL1OEOQ_.js} +1 -1
  172. package/out/web/assets/{gitGraphDiagram-WWUBYQGX-RjrhB0MP.js → gitGraphDiagram-WWUBYQGX-8cSi42H7.js} +1 -1
  173. package/out/web/assets/index-BocxnrDG.js +168 -0
  174. package/out/web/assets/index-yeh_r55a.css +1 -0
  175. package/out/web/assets/{info-A6RAGUB7-B3dfCpeY.js → info-A6RAGUB7-C1zsX5sc.js} +1 -1
  176. package/out/web/assets/{infoDiagram-27XIBGKW-QVWkLVx7.js → infoDiagram-27XIBGKW-DsRYtIyI.js} +1 -1
  177. package/out/web/assets/{ishikawaDiagram-5VMMS53U-CLFNIRI9.js → ishikawaDiagram-5VMMS53U-B2crwIXj.js} +1 -1
  178. package/out/web/assets/{journeyDiagram-3NMN7TZE-fgOQadYF.js → journeyDiagram-3NMN7TZE-BTH-GyVR.js} +1 -1
  179. package/out/web/assets/{kanban-definition-UXKFOSKX-G3Q_9g9O.js → kanban-definition-UXKFOSKX-BeRfCnuA.js} +1 -1
  180. package/out/web/assets/{mermaid-parser.core-v3Jm7nco.js → mermaid-parser.core-BJwgCBBc.js} +3 -3
  181. package/out/web/assets/{mermaid.core-DTEjLUA1.js → mermaid.core-CsDDodN8.js} +4 -4
  182. package/out/web/assets/{mindmap-definition-YA3MSWOX-C_D42_fF.js → mindmap-definition-YA3MSWOX-B42efOQ1.js} +1 -1
  183. package/out/web/assets/{modelManager-BPLycQrN.js → modelManager-B3ZFMh8v.js} +2 -2
  184. package/out/web/assets/{oauthManager-BUK7ic7q.js → oauthManager-Ch5Du2fF.js} +1 -1
  185. package/out/web/assets/{packet-AYTQ26CC-B-R-SHlF.js → packet-AYTQ26CC-B6xUyE6r.js} +1 -1
  186. package/out/web/assets/{pegDiagram-XKGWAZYB-Da2q_giz.js → pegDiagram-XKGWAZYB-1LabVycL.js} +1 -1
  187. package/out/web/assets/{pie-WAS4IAKB-BC8F0N7-.js → pie-WAS4IAKB-DJwo-nf_.js} +1 -1
  188. package/out/web/assets/{pieDiagram-E7YTZNPT-Iy9REJNC.js → pieDiagram-E7YTZNPT-icmAqzy4.js} +1 -1
  189. package/out/web/assets/{quadrantDiagram-AXDQQJYC-ztsiYw7h.js → quadrantDiagram-AXDQQJYC-CZ-wEx41.js} +1 -1
  190. package/out/web/assets/{radar-RG4KPBEZ-B_1P8Z_z.js → radar-RG4KPBEZ-D7d-Xriw.js} +1 -1
  191. package/out/web/assets/{railroad-74A4TZTK-DuTqczSo.js → railroad-74A4TZTK-viuhdYbs.js} +1 -1
  192. package/out/web/assets/{railroad-abnf-HS5TGJTU-Df29WLmp.js → railroad-abnf-HS5TGJTU-DNXiCNea.js} +1 -1
  193. package/out/web/assets/{railroad-ebnf-LZEXJU2U-ePFMo-qp.js → railroad-ebnf-LZEXJU2U-nkSu1-0C.js} +1 -1
  194. package/out/web/assets/{railroad-peg-WCYAUIDC-DYXLW4_E.js → railroad-peg-WCYAUIDC-BhbMeQzz.js} +1 -1
  195. package/out/web/assets/{railroadDiagram-O6MQD6OU-LStnZf2S.js → railroadDiagram-O6MQD6OU-CLpG1jUX.js} +1 -1
  196. package/out/web/assets/{requirementDiagram-BXWQKSXE-DmzonMH-.js → requirementDiagram-BXWQKSXE-kRDrvWrc.js} +1 -1
  197. package/out/web/assets/{sankeyDiagram-P5KCCOFB-B8vjmUU1.js → sankeyDiagram-P5KCCOFB-DxAwmq4x.js} +1 -1
  198. package/out/web/assets/{sequenceDiagram-WJ2MYXX4-CTnplOAX.js → sequenceDiagram-WJ2MYXX4-aLgY-6Wz.js} +1 -1
  199. package/out/web/assets/{stateDiagram-D77RDMKH-BFrIaLdI.js → stateDiagram-D77RDMKH-D5QWfOhp.js} +1 -1
  200. package/out/web/assets/stateDiagram-v2-MP3YSRHH-D6OiZoAP.js +1 -0
  201. package/out/web/assets/{swimlanes-42K2YHIH-BXGngUNU.js → swimlanes-42K2YHIH-CRs5UyA-.js} +1 -1
  202. package/out/web/assets/swimlanesDiagram-VR7AAH4N-UgEXIsV0.js +8 -0
  203. package/out/web/assets/syntaxHighlight-C-wAGT84.js +3 -0
  204. package/out/web/assets/{timeline-definition-24CTP7MA-CegH7n43.js → timeline-definition-24CTP7MA-Denqd9qb.js} +1 -1
  205. package/out/web/assets/{treeView-Q6P3EWNA-DTpJv2Tw.js → treeView-Q6P3EWNA-0my3Z3s4.js} +1 -1
  206. package/out/web/assets/{treemap-WGGIJYW6-i8M9iJU6.js → treemap-WGGIJYW6-B_EjkLYq.js} +1 -1
  207. package/out/web/assets/{vennDiagram-4TSXK5OY-Dpzo1S8U.js → vennDiagram-4TSXK5OY-DxVg3Ewr.js} +1 -1
  208. package/out/web/assets/{wardley-WFR3VGLG-BrNsgtfw.js → wardley-WFR3VGLG-CGD-Tj4p.js} +1 -1
  209. package/out/web/assets/{wardleyDiagram-VM6X3IG4-lQ92LFE1.js → wardleyDiagram-VM6X3IG4-CmfS39Xm.js} +1 -1
  210. package/out/web/assets/{xychartDiagram-S5SC5T6Z-BaOaEoBd.js → xychartDiagram-S5SC5T6Z-BVM9bKis.js} +1 -1
  211. package/out/web/index-web.html +2 -2
  212. package/out/web/index.html +2 -2
  213. package/package.json +2 -2
  214. package/shared-modules/CODEOWNERS +3 -3
  215. package/shared-modules/SDL2/SDL2_mixer.json +27 -0
  216. package/shared-modules/appstream/README.md +13 -0
  217. package/shared-modules/appstream/appstream-compose.json +111 -0
  218. package/shared-modules/dbus-glib/dbus-glib.json +2 -2
  219. package/shared-modules/libportal/libportal.json +23 -0
  220. package/shared-modules/linux-audio/fluidsynth2-static.json +2 -2
  221. package/shared-modules/linux-audio/fluidsynth2.json +2 -2
  222. package/shared-modules/linux-audio/libinstpatch.json +2 -2
  223. package/shared-modules/linux-audio/liblo-static.json +3 -0
  224. package/shared-modules/luajit/luajit.json +2 -2
  225. package/shared-modules/squashfs-tools/squashfs-tools.json +0 -3
  226. package/slash-commands.json +7 -2
  227. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  228. package/src/renderer/shared/agentProgress.ts +51 -3
  229. package/src/renderer/shared/components/Settings/AutomationTab.tsx +2 -56
  230. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +15 -1
  231. package/src/renderer/shared/components/Settings/GeneralTab.tsx +25 -12
  232. package/src/renderer/shared/components/Settings/ThemeTab.test.tsx +45 -0
  233. package/src/renderer/shared/components/Settings/ThemeTab.tsx +106 -3
  234. package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +67 -17
  235. package/src/renderer/shared/components/activity/AgentActivityTable.tsx +376 -134
  236. package/src/renderer/shared/components/activity/AskUserActivityCard.test.tsx +122 -0
  237. package/src/renderer/shared/components/activity/AskUserActivityCard.tsx +70 -0
  238. package/src/renderer/shared/components/activity/McpToolResults.tsx +39 -2
  239. package/src/renderer/shared/components/activity/SubagentDagView.tsx +7 -7
  240. package/src/renderer/shared/components/activity/ViewedImages.tsx +41 -2
  241. package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +3 -3
  242. package/src/renderer/shared/components/app/CommandPalette.tsx +7 -7
  243. package/src/renderer/shared/components/app/DesktopTitlebar.tsx +2 -2
  244. package/src/renderer/shared/components/app/MintDashboard.tsx +27 -17
  245. package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +3 -3
  246. package/src/renderer/shared/components/cards/CalculationCard.tsx +3 -3
  247. package/src/renderer/shared/components/cards/ImageGenCard.tsx +2 -2
  248. package/src/renderer/shared/components/cards/ImageSearchCard.tsx +2 -2
  249. package/src/renderer/shared/components/cards/MermaidCard.tsx +1 -1
  250. package/src/renderer/shared/components/cards/PlanCard.test.tsx +41 -0
  251. package/src/renderer/shared/components/cards/PlanCard.tsx +32 -0
  252. package/src/renderer/shared/components/cards/ReleaseNotesCard.test.tsx +29 -0
  253. package/src/renderer/shared/components/cards/ReleaseNotesCard.tsx +137 -0
  254. package/src/renderer/shared/components/cards/StockCard.tsx +7 -7
  255. package/src/renderer/shared/components/cards/ToolResultCards.tsx +11 -3
  256. package/src/renderer/shared/components/cards/UiMockupWidget.tsx +4 -4
  257. package/src/renderer/shared/components/cards/WeatherCard.tsx +5 -5
  258. package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +11 -0
  259. package/src/renderer/shared/components/chat/ApprovalCard.tsx +21 -2
  260. package/src/renderer/shared/components/chat/ArtifactCards.test.tsx +69 -0
  261. package/src/renderer/shared/components/chat/ArtifactCards.tsx +238 -0
  262. package/src/renderer/shared/components/chat/AttachedDocumentCard.test.tsx +39 -0
  263. package/src/renderer/shared/components/chat/AttachedDocumentCard.tsx +220 -0
  264. package/src/renderer/shared/components/chat/ChatCodeBlock.test.tsx +33 -0
  265. package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +15 -1
  266. package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +40 -2
  267. package/src/renderer/shared/components/chat/ChatMessageItem.tsx +180 -34
  268. package/src/renderer/shared/components/chat/ChatModeSelector.css +227 -0
  269. package/src/renderer/shared/components/chat/ChatModeSelector.test.tsx +45 -0
  270. package/src/renderer/shared/components/chat/ChatModeSelector.tsx +198 -0
  271. package/src/renderer/shared/components/chat/ChatPanel.test.tsx +22 -0
  272. package/src/renderer/shared/components/chat/ChatPanel.tsx +227 -53
  273. package/src/renderer/shared/components/chat/DiffReviewModal.tsx +6 -6
  274. package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +142 -1
  275. package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +45 -3
  276. package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +42 -14
  277. package/src/renderer/shared/components/chat/RewindModal.tsx +10 -10
  278. package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +14 -0
  279. package/src/renderer/shared/components/chat/SourcesBlock.test.tsx +53 -0
  280. package/src/renderer/shared/components/chat/SourcesBlock.tsx +236 -90
  281. package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +45 -1
  282. package/src/renderer/shared/components/chat/TurnFileChanges.tsx +202 -169
  283. package/src/renderer/shared/components/extensions/CliSessionsView.tsx +5 -4
  284. package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +3 -3
  285. package/src/renderer/shared/components/studio/LibraryPage.tsx +48 -5
  286. package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +125 -13
  287. package/src/renderer/shared/components/workspace/CodeReviewPage.test.tsx +50 -0
  288. package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +77 -16
  289. package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +78 -22
  290. package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +13 -0
  291. package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +48 -28
  292. package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +266 -47
  293. package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +18 -6
  294. package/src/renderer/shared/components/workspace/background-terminals.css +11 -11
  295. package/src/renderer/shared/constants/config.ts +1 -5
  296. package/src/renderer/shared/constants/models.test.ts +51 -0
  297. package/src/renderer/shared/constants/models.ts +139 -32
  298. package/src/renderer/shared/conversation/lifecycle.test.ts +91 -3
  299. package/src/renderer/shared/conversation/lifecycle.ts +104 -5
  300. package/src/renderer/shared/conversation/progressiveStream.test.ts +83 -0
  301. package/src/renderer/shared/conversation/progressiveStream.ts +219 -0
  302. package/src/renderer/shared/css/agent-observability.css +133 -0
  303. package/src/renderer/shared/css/chat-ui-cards.css +13 -13
  304. package/src/renderer/shared/css/code-review.css +63 -34
  305. package/src/renderer/shared/css/code-sessions.css +47 -18
  306. package/src/renderer/shared/css/collapsed-paste.css +1 -1
  307. package/src/renderer/shared/css/error-notice.css +1 -1
  308. package/src/renderer/shared/css/file-changes.css +165 -80
  309. package/src/renderer/shared/css/library.css +105 -10
  310. package/src/renderer/shared/css/management-views.css +1 -1
  311. package/src/renderer/shared/css/mcp-results.css +5 -5
  312. package/src/renderer/shared/css/model-selector.css +18 -18
  313. package/src/renderer/shared/css/mods.css +7 -7
  314. package/src/renderer/shared/css/notification-notice.css +2 -2
  315. package/src/renderer/shared/css/queued-messages.css +5 -5
  316. package/src/renderer/shared/css/settings/general.css +18 -13
  317. package/src/renderer/shared/css/settings/profile.css +5 -5
  318. package/src/renderer/shared/css/settings/theme.css +5 -5
  319. package/src/renderer/shared/css/slash-choice.css +2 -2
  320. package/src/renderer/shared/css/slash-suggestions.css +3 -3
  321. package/src/renderer/shared/css/sources-block.css +41 -2
  322. package/src/renderer/shared/css/tools-panel.css +2 -2
  323. package/src/renderer/shared/css/ui-tailwind.css +8 -0
  324. package/src/renderer/shared/css/veo-studio.css +2 -2
  325. package/src/renderer/shared/css/viewed-images.css +7 -7
  326. package/src/renderer/shared/platform.ts +2 -1
  327. package/src/renderer/shared/theme/themeManager.test.ts +17 -0
  328. package/src/renderer/shared/theme/themeManager.ts +21 -4
  329. package/src/renderer/shared/theme/themes.ts +54 -0
  330. package/src/renderer/shared/theme/tokens.css +6 -4
  331. package/src/renderer/shared/types.ts +1 -1
  332. package/src/renderer/shared/utils/agentActivity.test.ts +468 -2
  333. package/src/renderer/shared/utils/agentActivity.ts +602 -35
  334. package/src/renderer/shared/utils/artifactBrowser.test.ts +37 -32
  335. package/src/renderer/shared/utils/artifactBrowser.ts +17 -19
  336. package/src/renderer/shared/utils/attachedDocument.test.ts +73 -0
  337. package/src/renderer/shared/utils/attachedDocument.ts +49 -0
  338. package/src/renderer/shared/utils/fileIcons.test.ts +59 -0
  339. package/src/renderer/shared/utils/fileIcons.ts +149 -16
  340. package/src/renderer/shared/utils/markdown.tsx +72 -19
  341. package/src/renderer/shared/utils/markdown.webImages.test.tsx +16 -0
  342. package/src/renderer/shared/utils/mcpResults.test.tsx +29 -0
  343. package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
  344. package/src/renderer/shared/utils/syntaxHighlight.test.tsx +11 -1
  345. package/src/renderer/shared/utils/syntaxHighlight.tsx +51 -3
  346. package/src/renderer/shared/utils/useGeminiLiveVoice.ts +324 -51
  347. package/src/renderer/shared/utils/workspaceDrag.test.ts +153 -0
  348. package/src/renderer/shared/utils/workspaceDrag.ts +142 -0
  349. package/src/renderer/shared/utils/workspaceMedia.ts +10 -2
  350. package/src/renderer/src/App.tsx +14 -8
  351. package/src/renderer/src/agentProgress.ts +1 -0
  352. package/src/renderer/src/components/ScreenPicker.css +1 -1
  353. package/src/renderer/src/components/WorkspacePanel.tsx +6 -5
  354. package/src/renderer/src/css/app-shell.css +4 -4
  355. package/src/renderer/src/css/chat.css +1261 -241
  356. package/src/renderer/src/css/desktop-tools.css +11 -11
  357. package/src/renderer/src/css/sidebar-workspace.css +2 -2
  358. package/src/renderer/src/css/tool-surfaces.css +12 -12
  359. package/src/renderer/src/index.css +38 -20
  360. package/src/renderer/src/tauri.ts +22 -2
  361. package/src/renderer/src-web/App.tsx +14 -0
  362. package/src/renderer/src-web/agentProgress.ts +1 -0
  363. package/src/renderer/src-web/css/app-shell.css +4 -4
  364. package/src/renderer/src-web/css/chat.css +1278 -190
  365. package/src/renderer/src-web/css/mobile.css +29 -13
  366. package/src/renderer/src-web/css/settings/mobile.css +2 -2
  367. package/src/renderer/src-web/css/sidebar-workspace.css +1 -1
  368. package/src/renderer/src-web/index.css +38 -20
  369. package/src/renderer/src-web/tauri.ts +22 -2
  370. package/src/renderer/tests/browserArtifacts.browser.tsx +1 -1
  371. package/src/renderer/tests/library.browser.tsx +3 -0
  372. package/src/renderer/tests/liveVoice.browser.html +1 -0
  373. package/src/renderer/tests/workspaceDrop.browser.tsx +1 -1
  374. package/src-tauri/src/app/lifecycle.rs +1 -14
  375. package/src-tauri/src/background/headless.rs +14 -8
  376. package/src-tauri/src/commands/conversation.rs +38 -0
  377. package/src-tauri/src/native/mod.rs +0 -1
  378. package/src-tauri/src/native/system.rs +4 -47
  379. package/crates/mint-cli/src/interactive/chat_tui.rs +0 -8855
  380. package/crates/mint-core/src/agent/chat.rs +0 -4394
  381. package/crates/mint-core/src/browser/lifecycle.rs +0 -291
  382. package/crates/mint-core/src/browser/logging.rs +0 -20
  383. package/crates/mint-core/src/orchestration/tools/browser.rs +0 -31
  384. package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +0 -2
  385. package/out/web/assets/CalculationCard-CRjP3E1j.js +0 -1
  386. package/out/web/assets/CliSessionsView-C3Br2v5Y.css +0 -1
  387. package/out/web/assets/CodeReviewPage-CUJceQme.css +0 -1
  388. package/out/web/assets/CodeReviewPage-VCUgPla_.js +0 -2
  389. package/out/web/assets/ImageGenCard-BCXxkVJX.js +0 -1
  390. package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +0 -1
  391. package/out/web/assets/LibraryPage-DAxUTAg7.css +0 -1
  392. package/out/web/assets/LibraryPage-WlviRdR8.js +0 -2
  393. package/out/web/assets/LinkedFoldersView-CKRgroWK.js +0 -1
  394. package/out/web/assets/McpServersView-D3wHnuHg.js +0 -1
  395. package/out/web/assets/MermaidCard-NqrHdyJW.js +0 -2
  396. package/out/web/assets/MintDashboard-B2DJsDDO.css +0 -1
  397. package/out/web/assets/MintDashboard-COw_xol7.js +0 -63
  398. package/out/web/assets/RewindModal-DempuDod.js +0 -1
  399. package/out/web/assets/SettingsWindow-2YP3yj7s.js +0 -4
  400. package/out/web/assets/SettingsWindow-Dz8LQVgD.css +0 -1
  401. package/out/web/assets/StockCard-BDMCe7mx.js +0 -1
  402. package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +0 -38
  403. package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +0 -1
  404. package/out/web/assets/UiMockupWidget-DD73QWg1.js +0 -1
  405. package/out/web/assets/WeatherCard-CPucIfq8.js +0 -1
  406. package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +0 -2
  407. package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +0 -1
  408. package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +0 -1
  409. package/out/web/assets/config-CvIO5hkn.js +0 -2
  410. package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +0 -1
  411. package/out/web/assets/index-BmpulIkg.js +0 -168
  412. package/out/web/assets/index-CkvBwhX_.css +0 -1
  413. package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +0 -1
  414. package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +0 -8
  415. package/shared-modules/libsecret/libsecret.json +0 -23
  416. package/shared-modules/libusb/libusb.json +0 -27
  417. package/shared-modules/lzo/lzo.json +0 -27
  418. package/src/renderer/src/components/BrowserShell.tsx +0 -108
  419. package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +0 -91
  420. package/src/renderer/src/components/browser-shell.css +0 -185
  421. package/src-tauri/src/native/browser.rs +0 -252
@@ -46,6 +46,8 @@ pub enum GeminiLiveEvent {
46
46
  /// The current turn (user's utterance or the model's reply) has finished —
47
47
  /// the next `Transcript` starts a new line instead of continuing this one.
48
48
  TurnComplete,
49
+ /// The user barged in while the model was speaking; queued playback must stop immediately.
50
+ Interrupted,
49
51
  /// Progress/result of a tool call the model triggered mid-conversation.
50
52
  ToolStatus {
51
53
  action: String,
@@ -53,12 +55,118 @@ pub enum GeminiLiveEvent {
53
55
  result: Option<String>,
54
56
  error: Option<String>,
55
57
  },
58
+ /// Tool approval request for Web / remote sessions where approvals travel over the Live stream.
59
+ ApprovalRequested {
60
+ token: String,
61
+ approval: AgentApproval,
62
+ },
56
63
  Error {
57
64
  message: String,
58
65
  },
59
66
  Closed,
60
67
  }
61
68
 
69
+ #[derive(Debug, Default, Clone)]
70
+ pub(crate) struct LiveTurnAccumulator {
71
+ pub(crate) user_segments: Vec<String>,
72
+ pub(crate) assistant_segments: Vec<String>,
73
+ pub(crate) tool_activity: Vec<crate::orchestration::AgentProgress>,
74
+ }
75
+
76
+ impl LiveTurnAccumulator {
77
+ pub(crate) fn is_empty(&self) -> bool {
78
+ self.user_segments.iter().all(|s| s.trim().is_empty())
79
+ && self.assistant_segments.iter().all(|s| s.trim().is_empty())
80
+ && self.tool_activity.is_empty()
81
+ }
82
+
83
+ pub(crate) fn should_flush_before_user_transcript(&self) -> bool {
84
+ !self.assistant_segments.iter().all(|s| s.trim().is_empty())
85
+ || !self.tool_activity.is_empty()
86
+ }
87
+
88
+ pub(crate) fn push_user_transcript(&mut self, text: &str) {
89
+ if !text.is_empty() {
90
+ self.user_segments.push(text.to_string());
91
+ }
92
+ }
93
+
94
+ pub(crate) fn push_assistant_transcript(&mut self, text: &str) {
95
+ if !text.is_empty() {
96
+ self.assistant_segments.push(text.to_string());
97
+ }
98
+ }
99
+
100
+ pub(crate) fn record_tool_call(
101
+ &mut self,
102
+ call_id: Option<String>,
103
+ action: &str,
104
+ input: Value,
105
+ result: &str,
106
+ elapsed_ms: Option<u64>,
107
+ is_error: bool,
108
+ ) {
109
+ self.tool_activity
110
+ .push(crate::orchestration::AgentProgress::ToolStart {
111
+ call_id: call_id.clone(),
112
+ action: action.to_string(),
113
+ input: input.clone(),
114
+ subagent: None,
115
+ });
116
+ self.tool_activity
117
+ .push(crate::orchestration::AgentProgress::ToolEnd {
118
+ elapsed_ms,
119
+ call_id,
120
+ status: Some(if is_error {
121
+ crate::integrations::mcp_result::ToolStatus::Failed
122
+ } else {
123
+ crate::integrations::mcp_result::ToolStatus::Success
124
+ }),
125
+ action: action.to_string(),
126
+ input,
127
+ result: result.to_string(),
128
+ subagent: None,
129
+ });
130
+ }
131
+
132
+ pub(crate) fn flush_to_memory(
133
+ &mut self,
134
+ memory: &crate::MemoryStore,
135
+ chat_id: &str,
136
+ workspace_root: &Path,
137
+ model: &str,
138
+ ) -> Option<i64> {
139
+ if self.is_empty() {
140
+ self.user_segments.clear();
141
+ self.assistant_segments.clear();
142
+ return None;
143
+ }
144
+ let scoped_id =
145
+ crate::memory::scoped_chat_id(chat_id, Some(workspace_root.to_string_lossy().as_ref()));
146
+ let mut user_text = self.user_segments.concat().trim().to_string();
147
+ if user_text.is_empty() {
148
+ user_text = "[Voice]".to_string();
149
+ }
150
+ let mut ai_text = self.assistant_segments.concat().trim().to_string();
151
+ if ai_text.is_empty() && !self.tool_activity.is_empty() {
152
+ ai_text = "✓ Completed voice tool action.".to_string();
153
+ }
154
+ let activity = std::mem::take(&mut self.tool_activity);
155
+ self.user_segments.clear();
156
+ self.assistant_segments.clear();
157
+
158
+ let interaction_id = memory
159
+ .add_interaction_for_chat(&scoped_id, &user_text, &ai_text, "gemini", model)
160
+ .ok()?;
161
+ if !activity.is_empty()
162
+ && let Ok(activity_json) = serde_json::to_string(&activity)
163
+ {
164
+ let _ = memory.set_interaction_agent_activity_json(interaction_id, &activity_json);
165
+ }
166
+ Some(interaction_id)
167
+ }
168
+ }
169
+
62
170
  /// Handle for pushing mic audio into a running session.
63
171
  pub struct GeminiLiveHandle {
64
172
  audio_tx: mpsc::UnboundedSender<Vec<u8>>,
@@ -98,6 +206,11 @@ where
98
206
 
99
207
  let on_event = Arc::new(on_event);
100
208
  let session_events = Arc::clone(&on_event);
209
+ let turn_accumulator = Arc::new(std::sync::Mutex::new(LiveTurnAccumulator::default()));
210
+ let session_turn = Arc::clone(&turn_accumulator);
211
+ let close_workspace = workspace_root.clone();
212
+ let close_chat_id = chat_id.clone();
213
+ let close_model = resolve_live_model(&config);
101
214
  spawn_cancellable_live_session(
102
215
  chat_id.clone(),
103
216
  audio_tx,
@@ -110,13 +223,23 @@ where
110
223
  &mut approve_cb,
111
224
  &*session_events,
112
225
  &mut audio_rx,
226
+ &session_turn,
113
227
  )
114
228
  .await
115
229
  {
116
230
  session_events(GeminiLiveEvent::Error { message });
117
231
  }
118
232
  },
119
- move || on_event(GeminiLiveEvent::Closed),
233
+ move || {
234
+ if let Ok(mut turn) = turn_accumulator.lock()
235
+ && !turn.is_empty()
236
+ && let Ok(memory) = crate::MemoryStore::open_default()
237
+ {
238
+ let _ =
239
+ turn.flush_to_memory(&memory, &close_chat_id, &close_workspace, &close_model);
240
+ }
241
+ on_event(GeminiLiveEvent::Closed);
242
+ },
120
243
  )
121
244
  }
122
245
 
@@ -152,15 +275,13 @@ where
152
275
  }
153
276
  }
154
277
 
155
- /// Keep browser targeting, evidence, and recovery state for the entire voice
156
- /// task, including successive tool-call batches and spoken turns. Dropping the
157
- /// task releases its tab leases; the next voice session starts independently.
278
+ /// Spawns a live voice task session.
158
279
  pub(crate) fn spawn_live_session<F>(future: F) -> tokio::task::JoinHandle<F::Output>
159
280
  where
160
281
  F: std::future::Future + Send + 'static,
161
282
  F::Output: Send + 'static,
162
283
  {
163
- tokio::spawn(crate::browser::run_session(future))
284
+ tokio::spawn(future)
164
285
  }
165
286
 
166
287
  fn resolve_api_key(config: &MintConfig) -> String {
@@ -191,29 +312,175 @@ fn resolve_live_voice(config: &MintConfig) -> String {
191
312
  .to_string()
192
313
  }
193
314
 
194
- fn agent_action_tool_declaration() -> Value {
315
+ fn agent_action_fallback_declaration() -> Value {
195
316
  json!({
196
- "functionDeclarations": [{
197
- "name": "agent_action",
198
- "description": "Execute one of Mint's built-in actions (video editing, subtitle generation, file/browser/shell operations, and more). Use this instead of describing the action in words.",
199
- "parameters": {
200
- "type": "OBJECT",
201
- "properties": {
202
- "action": {
203
- "type": "STRING",
204
- "description": "The action name, e.g. video_trim, video_extract_audio, speech_transcribe, list_files, read_file, run_shell."
205
- },
206
- "input": {
207
- "type": "OBJECT",
208
- "description": "Arguments for the action, e.g. {\"input\": \"...\", \"output\": \"...\", \"start\": 0, \"end\": 10}."
209
- }
317
+ "name": "agent_action",
318
+ "description": "Fallback action dispatcher: execute one of Mint's built-in actions by specifying its action name and input object.",
319
+ "parameters": {
320
+ "type": "OBJECT",
321
+ "properties": {
322
+ "action": {
323
+ "type": "STRING",
324
+ "description": "The action name, e.g. video_trim, video_extract_audio, speech_transcribe, list_files, read_file, run_shell."
210
325
  },
211
- "required": ["action"]
212
- }
213
- }]
326
+ "input": {
327
+ "type": "OBJECT",
328
+ "description": "Arguments for the action, e.g. {\"input\": \"...\", \"output\": \"...\", \"start\": 0, \"end\": 10}."
329
+ }
330
+ },
331
+ "required": ["action"]
332
+ }
214
333
  })
215
334
  }
216
335
 
336
+ fn live_tool_declarations(config: &MintConfig, workspace_root: &Path) -> Value {
337
+ let catalog = crate::prompts::tool_catalog::tool_catalog(config, false, workspace_root, false);
338
+ let mut declarations: Vec<Value> = catalog
339
+ .into_iter()
340
+ .filter(|tool| {
341
+ !matches!(
342
+ tool.name.as_str(),
343
+ "finish" | "enter_plan_mode" | "exit_plan_mode"
344
+ )
345
+ })
346
+ .map(|tool| {
347
+ json!({
348
+ "name": tool.name,
349
+ "description": tool.description,
350
+ "parameters": crate::chat::gemini_sanitize_schema(&tool.parameters),
351
+ })
352
+ })
353
+ .collect();
354
+ declarations.push(agent_action_fallback_declaration());
355
+ json!({ "functionDeclarations": declarations })
356
+ }
357
+
358
+ fn resolve_live_tool_call(call: &Value) -> (String, Value) {
359
+ let name = call["name"].as_str().unwrap_or_default();
360
+ if name == "agent_action" {
361
+ let action = call["args"]["action"]
362
+ .as_str()
363
+ .unwrap_or_default()
364
+ .to_string();
365
+ let input = call["args"]
366
+ .get("input")
367
+ .cloned()
368
+ .unwrap_or_else(|| json!({}));
369
+ (action, input)
370
+ } else {
371
+ let input = call.get("args").cloned().unwrap_or_else(|| json!({}));
372
+ (name.to_string(), input)
373
+ }
374
+ }
375
+
376
+ fn dispatch_server_content_events<Event>(value: &Value, on_event: &Event)
377
+ where
378
+ Event: Fn(GeminiLiveEvent),
379
+ {
380
+ if let Some(parts) = value["serverContent"]["modelTurn"]["parts"].as_array() {
381
+ for part in parts {
382
+ if let Some(data) = part["inlineData"]["data"].as_str() {
383
+ on_event(GeminiLiveEvent::AudioChunk {
384
+ data: data.to_string(),
385
+ });
386
+ }
387
+ }
388
+ }
389
+ if let Some(text) = value["serverContent"]["outputTranscription"]["text"].as_str()
390
+ && !text.is_empty()
391
+ {
392
+ on_event(GeminiLiveEvent::Transcript {
393
+ role: "assistant".into(),
394
+ text: text.into(),
395
+ });
396
+ }
397
+ if let Some(text) = value["serverContent"]["inputTranscription"]["text"].as_str()
398
+ && !text.is_empty()
399
+ {
400
+ on_event(GeminiLiveEvent::Transcript {
401
+ role: "user".into(),
402
+ text: text.into(),
403
+ });
404
+ }
405
+ if value["serverContent"]["interrupted"].as_bool() == Some(true) {
406
+ on_event(GeminiLiveEvent::Interrupted);
407
+ }
408
+ if value["serverContent"]["turnComplete"].as_bool() == Some(true) {
409
+ on_event(GeminiLiveEvent::TurnComplete);
410
+ }
411
+ }
412
+
413
+ const MAX_LIVE_BASE_PROMPT_CHARS: usize = 12_000;
414
+ const MAX_LIVE_MEMORY_CONTEXT_CHARS: usize = 6_000;
415
+ const MAX_LIVE_SKILLS_CONTEXT_CHARS: usize = 4_000;
416
+ const LIVE_VOICE_MODE_OVERRIDE: &str = "LIVE VOICE MODE OVERRIDE:\n- You are in a real-time spoken voice conversation (Gemini Live). Speak naturally and concisely to the user.\n- When the user asks you to perform an action, call the matching tool function directly (e.g. `list_files`, `read_file`, `git_status`, `web_search`, `video_trim`, `run_shell`, etc.) rather than describing the action in words.\n- Do NOT call a `finish` tool — once your tool calls complete (or if no tool is needed), simply speak your final response out loud to the user.";
417
+
418
+ pub(crate) fn build_live_system_instruction(
419
+ config: &MintConfig,
420
+ workspace_root: &Path,
421
+ chat_id: &str,
422
+ memory_store: Option<&crate::MemoryStore>,
423
+ ) -> String {
424
+ let user_name = memory_store.and_then(|memory| memory.get_profile("name").ok().flatten());
425
+ let mut base_prompt =
426
+ crate::prompts::agent::build_system_prompt(config, false, true, user_name.as_deref(), None);
427
+ if base_prompt.chars().count() > MAX_LIVE_BASE_PROMPT_CHARS {
428
+ base_prompt = base_prompt
429
+ .chars()
430
+ .take(MAX_LIVE_BASE_PROMPT_CHARS)
431
+ .collect();
432
+ }
433
+
434
+ let mut sections = vec![base_prompt.trim().to_string()];
435
+
436
+ if let Some(memory) = memory_store {
437
+ let mut memory_buf = String::new();
438
+ crate::orchestration::append_memory_context(
439
+ &mut memory_buf,
440
+ memory,
441
+ chat_id,
442
+ Some(workspace_root.to_string_lossy().as_ref()),
443
+ None,
444
+ config.semantic_fact_recall,
445
+ true,
446
+ );
447
+ let trimmed_mem = memory_buf.trim();
448
+ if !trimmed_mem.is_empty() {
449
+ let mem_chars: Vec<char> = trimmed_mem.chars().collect();
450
+ let budgeted_mem = if mem_chars.len() > MAX_LIVE_MEMORY_CONTEXT_CHARS {
451
+ // Keep profile/facts at the head and the newest conversation turns at the tail.
452
+ let head_len = 1_500;
453
+ let tail_len = MAX_LIVE_MEMORY_CONTEXT_CHARS.saturating_sub(head_len + 40);
454
+ let head: String = mem_chars[..head_len].iter().collect();
455
+ let tail: String = mem_chars[mem_chars.len() - tail_len..].iter().collect();
456
+ format!("{head}\n...[older turns omitted]...\n{tail}")
457
+ } else {
458
+ trimmed_mem.to_string()
459
+ };
460
+ sections.push(budgeted_mem);
461
+ }
462
+ }
463
+
464
+ if let Ok(skills) = crate::skills::learned_skills_context(Some(workspace_root), Some(chat_id)) {
465
+ let trimmed_skills = skills.trim();
466
+ if !trimmed_skills.is_empty() {
467
+ let skill_chars: Vec<char> = trimmed_skills.chars().collect();
468
+ let budgeted_skills = if skill_chars.len() > MAX_LIVE_SKILLS_CONTEXT_CHARS {
469
+ let prefix: String = skill_chars[..MAX_LIVE_SKILLS_CONTEXT_CHARS]
470
+ .iter()
471
+ .collect();
472
+ format!("Learned skills:\n{prefix}\n...[truncated]")
473
+ } else {
474
+ format!("Learned skills:\n{trimmed_skills}")
475
+ };
476
+ sections.push(budgeted_skills);
477
+ }
478
+ }
479
+
480
+ sections.push(LIVE_VOICE_MODE_OVERRIDE.to_string());
481
+ sections.join("\n\n")
482
+ }
483
+
217
484
  async fn run_session<Approve, Event>(
218
485
  config: &MintConfig,
219
486
  workspace_root: &Path,
@@ -221,6 +488,7 @@ async fn run_session<Approve, Event>(
221
488
  approve_cb: &mut Approve,
222
489
  on_event: &Event,
223
490
  audio_rx: &mut mpsc::UnboundedReceiver<Vec<u8>>,
491
+ turn_accumulator: &Arc<std::sync::Mutex<LiveTurnAccumulator>>,
224
492
  ) -> Result<(), String>
225
493
  where
226
494
  Approve: FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send,
@@ -254,46 +522,9 @@ where
254
522
  eprintln!("[gemini-live] connected, sending setup...");
255
523
  let (mut ws_write, mut ws_read) = ws_stream.split();
256
524
 
257
- let user_name = crate::MemoryStore::open_default()
258
- .ok()
259
- .and_then(|memory| memory.get_profile("name").ok().flatten());
260
-
261
- let mut system_instruction = format!(
262
- "{}\n\nWhen the user asks you to perform an action, call the `agent_action` function with `action` set to the action name and `input` set to a JSON object of its arguments, rather than describing the action in words.",
263
- crate::prompts::agent::build_system_prompt(config, false, true, user_name.as_deref(), None)
264
- );
265
- // Same cross-session context (saved profile/preferences + this chat's recent
266
- // history) the typed-chat agent loop gets, so a Live call isn't a blank slate
267
- // that's forgotten everything the user already told Mint.
268
- crate::orchestration::append_memory_context(
269
- &mut system_instruction,
270
- chat_id,
271
- Some(workspace_root.to_string_lossy().as_ref()),
272
- None,
273
- config.semantic_fact_recall,
274
- );
275
- if let Ok(skills) = crate::skills::learned_skills_context(Some(workspace_root), Some(chat_id))
276
- && !skills.trim().is_empty()
277
- {
278
- system_instruction = format!(
279
- "{}\n\nLearned skills:\n{}",
280
- system_instruction.trim(),
281
- skills.trim()
282
- );
283
- }
284
- // Recent conversation history and skills have no size cap of their own (skills
285
- // alone can run up to 16KB — see MAX_CONTEXT_BYTES in skills.rs) — a long chat
286
- // history would otherwise bloat every Live session's setup message, adding to
287
- // the delay before the model starts responding. The core instructions and tool
288
- // declaration are built first, so truncating off the end only drops overflow
289
- // context, never the instructions themselves.
290
- const MAX_LIVE_SYSTEM_INSTRUCTION_CHARS: usize = 12_000;
291
- if system_instruction.chars().count() > MAX_LIVE_SYSTEM_INSTRUCTION_CHARS {
292
- system_instruction = system_instruction
293
- .chars()
294
- .take(MAX_LIVE_SYSTEM_INSTRUCTION_CHARS)
295
- .collect();
296
- }
525
+ let memory_store = crate::MemoryStore::open_default().ok();
526
+ let system_instruction =
527
+ build_live_system_instruction(config, workspace_root, chat_id, memory_store.as_ref());
297
528
 
298
529
  // Confirmed against a live server rejection: `responseModalities` must be nested under
299
530
  // `generationConfig` — the server explicitly rejects it at the top level of `setup`
@@ -310,7 +541,7 @@ where
310
541
  }
311
542
  },
312
543
  "systemInstruction": { "parts": [{ "text": system_instruction }] },
313
- "tools": [agent_action_tool_declaration()],
544
+ "tools": [live_tool_declarations(config, workspace_root)],
314
545
  "outputAudioTranscription": {},
315
546
  "inputAudioTranscription": {},
316
547
  // Default end-of-speech detection is quick to decide the user is done
@@ -424,35 +655,46 @@ where
424
655
  return Err(message);
425
656
  }
426
657
 
427
- if let Some(parts) = value["serverContent"]["modelTurn"]["parts"].as_array() {
428
- for part in parts {
429
- if let Some(data) = part["inlineData"]["data"].as_str() {
430
- on_event(GeminiLiveEvent::AudioChunk { data: data.to_string() });
658
+ dispatch_server_content_events(&value, &|ev| {
659
+ match &ev {
660
+ GeminiLiveEvent::Transcript { role, text } => {
661
+ if let Ok(mut turn) = turn_accumulator.lock() {
662
+ if role == "user" {
663
+ if turn.should_flush_before_user_transcript()
664
+ && let Some(ref memory) = memory_store
665
+ {
666
+ let _ = turn.flush_to_memory(
667
+ memory,
668
+ chat_id,
669
+ workspace_root,
670
+ &model,
671
+ );
672
+ }
673
+ turn.push_user_transcript(text);
674
+ } else if role == "assistant" {
675
+ turn.push_assistant_transcript(text);
676
+ }
677
+ }
678
+ on_event(ev);
431
679
  }
680
+ GeminiLiveEvent::Interrupted | GeminiLiveEvent::TurnComplete => {
681
+ if let Some(ref memory) = memory_store
682
+ && let Ok(mut turn) = turn_accumulator.lock()
683
+ {
684
+ let _ = turn.flush_to_memory(memory, chat_id, workspace_root, &model);
685
+ }
686
+ on_event(ev);
687
+ }
688
+ _ => on_event(ev),
432
689
  }
433
- }
434
- if let Some(text) = value["serverContent"]["outputTranscription"]["text"].as_str() {
435
- if !text.is_empty() {
436
- on_event(GeminiLiveEvent::Transcript { role: "assistant".into(), text: text.into() });
437
- }
438
- }
439
- if let Some(text) = value["serverContent"]["inputTranscription"]["text"].as_str() {
440
- if !text.is_empty() {
441
- on_event(GeminiLiveEvent::Transcript { role: "user".into(), text: text.into() });
442
- }
443
- }
444
- if value["serverContent"]["turnComplete"].as_bool() == Some(true)
445
- || value["serverContent"]["interrupted"].as_bool() == Some(true)
446
- {
447
- on_event(GeminiLiveEvent::TurnComplete);
448
- }
690
+ });
449
691
 
450
692
  if let Some(calls) = value["toolCall"]["functionCalls"].as_array() {
451
693
  for call in calls {
452
694
  let id = call["id"].as_str().unwrap_or_default().to_string();
453
695
  let name = call["name"].as_str().unwrap_or_default().to_string();
454
- let action = call["args"]["action"].as_str().unwrap_or_default().to_string();
455
- let input = call["args"]["input"].clone();
696
+ let (action, input) = resolve_live_tool_call(call);
697
+ let call_started = std::time::Instant::now();
456
698
 
457
699
  let outcome = execute_tool_from_json(
458
700
  workspace_root,
@@ -463,6 +705,7 @@ where
463
705
  approve_cb,
464
706
  )
465
707
  .await;
708
+ let elapsed_ms = Some(call_started.elapsed().as_millis() as u64);
466
709
 
467
710
  let (result_str, error_str, response_payload) = match &outcome {
468
711
  Ok(result) => (Some(result.clone()), None, json!({ "result": result })),
@@ -471,6 +714,20 @@ where
471
714
  (None, Some(message.clone()), json!({ "error": message }))
472
715
  }
473
716
  };
717
+ if let Ok(mut turn) = turn_accumulator.lock() {
718
+ let summary = result_str
719
+ .as_deref()
720
+ .or(error_str.as_deref())
721
+ .unwrap_or_default();
722
+ turn.record_tool_call(
723
+ if id.is_empty() { None } else { Some(id.clone()) },
724
+ &action,
725
+ input.clone(),
726
+ summary,
727
+ elapsed_ms,
728
+ error_str.is_some(),
729
+ );
730
+ }
474
731
  on_event(GeminiLiveEvent::ToolStatus {
475
732
  action: action.clone(),
476
733
  input,
@@ -578,4 +835,297 @@ mod approval_lifecycle_tests {
578
835
  "voice stop waited for transport: {result:?}"
579
836
  );
580
837
  }
838
+
839
+ #[test]
840
+ fn live_tool_declarations_include_real_catalog_tools_without_unsupported_schema_fields() {
841
+ let config = MintConfig::default();
842
+ let workspace = std::path::Path::new(".");
843
+ let decl = live_tool_declarations(&config, workspace);
844
+ let funcs = decl["functionDeclarations"]
845
+ .as_array()
846
+ .expect("functionDeclarations array");
847
+ let names: Vec<&str> = funcs.iter().filter_map(|f| f["name"].as_str()).collect();
848
+
849
+ assert!(
850
+ names.contains(&"list_files"),
851
+ "missing list_files: {names:?}"
852
+ );
853
+ assert!(names.contains(&"read_file"), "missing read_file: {names:?}");
854
+ assert!(
855
+ names.contains(&"git_status"),
856
+ "missing git_status: {names:?}"
857
+ );
858
+ assert!(
859
+ names.contains(&"web_search"),
860
+ "missing web_search: {names:?}"
861
+ );
862
+ assert!(
863
+ names.contains(&"video_trim"),
864
+ "missing video_trim: {names:?}"
865
+ );
866
+ assert!(
867
+ names.contains(&"agent_action"),
868
+ "missing fallback agent_action: {names:?}"
869
+ );
870
+ assert!(
871
+ !names.contains(&"finish"),
872
+ "live voice mode must not expose finish tool"
873
+ );
874
+ assert!(
875
+ !names.contains(&"enter_plan_mode"),
876
+ "live voice mode must not expose enter_plan_mode"
877
+ );
878
+ assert!(
879
+ !names.contains(&"exit_plan_mode"),
880
+ "live voice mode must not expose exit_plan_mode"
881
+ );
882
+
883
+ let serialized = serde_json::to_string(&decl).unwrap();
884
+ assert!(
885
+ !serialized.contains("\"additionalProperties\""),
886
+ "Gemini Live tool schemas must strip additionalProperties"
887
+ );
888
+ }
889
+
890
+ #[test]
891
+ fn resolve_live_tool_call_handles_both_direct_tools_and_agent_action_wrapper() {
892
+ let direct_call = json!({
893
+ "id": "call-1",
894
+ "name": "read_file",
895
+ "args": { "path": "README.md", "startLine": 1 }
896
+ });
897
+ let (action, input) = resolve_live_tool_call(&direct_call);
898
+ assert_eq!(action, "read_file");
899
+ assert_eq!(input, json!({ "path": "README.md", "startLine": 1 }));
900
+
901
+ let wrapped_call = json!({
902
+ "id": "call-2",
903
+ "name": "agent_action",
904
+ "args": {
905
+ "action": "video_trim",
906
+ "input": { "input": "a.mp4", "output": "b.mp4", "start": 0, "end": 5 }
907
+ }
908
+ });
909
+ let (action2, input2) = resolve_live_tool_call(&wrapped_call);
910
+ assert_eq!(action2, "video_trim");
911
+ assert_eq!(
912
+ input2,
913
+ json!({ "input": "a.mp4", "output": "b.mp4", "start": 0, "end": 5 })
914
+ );
915
+ }
916
+
917
+ #[test]
918
+ fn server_content_interrupted_emits_interrupted_event_distinct_from_turn_complete() {
919
+ let events = std::sync::Mutex::new(Vec::new());
920
+ let interrupted_msg = json!({
921
+ "serverContent": {
922
+ "interrupted": true
923
+ }
924
+ });
925
+ dispatch_server_content_events(&interrupted_msg, &|ev| events.lock().unwrap().push(ev));
926
+ {
927
+ let recorded = events.lock().unwrap();
928
+ assert_eq!(recorded.len(), 1);
929
+ assert!(
930
+ matches!(recorded[0], GeminiLiveEvent::Interrupted),
931
+ "expected Interrupted event, got: {:?}",
932
+ recorded[0]
933
+ );
934
+ }
935
+
936
+ events.lock().unwrap().clear();
937
+ let turn_complete_msg = json!({
938
+ "serverContent": {
939
+ "turnComplete": true
940
+ }
941
+ });
942
+ dispatch_server_content_events(&turn_complete_msg, &|ev| events.lock().unwrap().push(ev));
943
+ {
944
+ let recorded = events.lock().unwrap();
945
+ assert_eq!(recorded.len(), 1);
946
+ assert!(
947
+ matches!(recorded[0], GeminiLiveEvent::TurnComplete),
948
+ "expected TurnComplete event, got: {:?}",
949
+ recorded[0]
950
+ );
951
+ }
952
+ }
953
+
954
+ #[test]
955
+ fn live_turn_accumulator_flushes_transcripts_and_tool_activity_to_memory_store() {
956
+ let db_path = std::env::temp_dir().join(format!(
957
+ "mint-live-turn-test-{}.sqlite3",
958
+ uuid::Uuid::new_v4()
959
+ ));
960
+ let memory = crate::MemoryStore::open(&db_path);
961
+ let workspace = std::path::Path::new("/tmp/mint-live-test-ws");
962
+ let chat_id = "chat-live-1";
963
+ let model = "gemini-2.5-flash-native-audio-preview-12-2025";
964
+
965
+ let mut turn = LiveTurnAccumulator::default();
966
+ assert!(turn.is_empty());
967
+ assert_eq!(
968
+ turn.flush_to_memory(&memory, chat_id, workspace, model),
969
+ None
970
+ );
971
+
972
+ turn.push_user_transcript("ช่วยดูไฟล์ ");
973
+ turn.push_user_transcript("README ให้หน่อย");
974
+ turn.record_tool_call(
975
+ Some("call-1".to_string()),
976
+ "read_file",
977
+ json!({ "path": "README.md" }),
978
+ "file contents here",
979
+ Some(12),
980
+ false,
981
+ );
982
+ turn.push_assistant_transcript("อ่านไฟล์ README ");
983
+ turn.push_assistant_transcript("เรียบร้อยแล้วครับ");
984
+
985
+ let interaction_id = turn
986
+ .flush_to_memory(&memory, chat_id, workspace, model)
987
+ .expect("should persist non-empty turn");
988
+ assert!(interaction_id > 0);
989
+ assert!(turn.is_empty(), "accumulator must reset after flush");
990
+
991
+ let scoped_id =
992
+ crate::memory::scoped_chat_id(chat_id, Some(workspace.to_string_lossy().as_ref()));
993
+ let snapshot = memory
994
+ .conversation_snapshot(&scoped_id, None, 10)
995
+ .expect("snapshot");
996
+ assert_eq!(snapshot.interactions.len(), 1);
997
+ let saved = &snapshot.interactions[0];
998
+ assert_eq!(saved.user_text, "ช่วยดูไฟล์ README ให้หน่อย");
999
+ assert_eq!(saved.ai_text, "อ่านไฟล์ README เรียบร้อยแล้วครับ");
1000
+ assert_eq!(saved.provider, "gemini");
1001
+ assert_eq!(saved.model, model);
1002
+ let activity = saved
1003
+ .agent_activity
1004
+ .as_ref()
1005
+ .and_then(|v| v.as_array())
1006
+ .expect("agent_activity should be saved as array");
1007
+ assert_eq!(activity.len(), 2, "expected ToolStart and ToolEnd");
1008
+ let _ = std::fs::remove_file(&db_path);
1009
+ }
1010
+
1011
+ #[test]
1012
+ fn live_turn_accumulator_flushes_previous_turn_when_new_user_speech_arrives_after_assistant_reply()
1013
+ {
1014
+ let db_path = std::env::temp_dir().join(format!(
1015
+ "mint-live-turn-barge-test-{}.sqlite3",
1016
+ uuid::Uuid::new_v4()
1017
+ ));
1018
+ let memory = crate::MemoryStore::open(&db_path);
1019
+ let workspace = std::path::Path::new("/tmp/mint-live-barge-ws");
1020
+ let chat_id = "chat-live-barge";
1021
+ let model = "gemini-2.5-flash-native-audio-preview-12-2025";
1022
+
1023
+ let mut turn = LiveTurnAccumulator::default();
1024
+ turn.push_user_transcript("สวัสดีครับ");
1025
+ turn.push_assistant_transcript("สวัสดีครับ มีอะไรให้ช่วยไหมครับ");
1026
+
1027
+ // User starts speaking Turn 2 before `interrupted`/`turnComplete` arrives:
1028
+ assert!(
1029
+ turn.should_flush_before_user_transcript(),
1030
+ "once assistant has replied/acted, a new user utterance after assistant output should flush Turn 1 first"
1031
+ );
1032
+ let first_id = turn
1033
+ .flush_to_memory(&memory, chat_id, workspace, model)
1034
+ .expect("should flush Turn 1");
1035
+ assert!(first_id > 0);
1036
+
1037
+ turn.push_user_transcript("เดี๋ยวก่อนนะ ขอเปลี่ยนคำถาม");
1038
+ assert!(
1039
+ !turn.should_flush_before_user_transcript(),
1040
+ "consecutive user fragments in Turn 2 must accumulate together"
1041
+ );
1042
+ turn.push_assistant_transcript("ได้ครับ ว่ามาได้เลย");
1043
+ let second_id = turn
1044
+ .flush_to_memory(&memory, chat_id, workspace, model)
1045
+ .expect("should flush Turn 2");
1046
+ assert!(second_id > first_id);
1047
+
1048
+ let scoped_id =
1049
+ crate::memory::scoped_chat_id(chat_id, Some(workspace.to_string_lossy().as_ref()));
1050
+ let snapshot = memory
1051
+ .conversation_snapshot(&scoped_id, None, 10)
1052
+ .expect("snapshot");
1053
+ assert_eq!(snapshot.interactions.len(), 2);
1054
+ assert_eq!(snapshot.interactions[0].user_text, "สวัสดีครับ");
1055
+ assert_eq!(snapshot.interactions[0].ai_text, "สวัสดีครับ มีอะไรให้ช่วยไหมครับ");
1056
+ assert_eq!(snapshot.interactions[1].user_text, "เดี๋ยวก่อนนะ ขอเปลี่ยนคำถาม");
1057
+ assert_eq!(snapshot.interactions[1].ai_text, "ได้ครับ ว่ามาได้เลย");
1058
+ let _ = std::fs::remove_file(&db_path);
1059
+ }
1060
+
1061
+ #[test]
1062
+ fn approval_requested_event_serializes_with_camel_case_type() {
1063
+ let ev = GeminiLiveEvent::ApprovalRequested {
1064
+ token: "tok-123".to_string(),
1065
+ approval: AgentApproval::RunShell {
1066
+ command: "cargo test".to_string(),
1067
+ mode: "workspace-write".to_string(),
1068
+ background: false,
1069
+ },
1070
+ };
1071
+ let v = serde_json::to_value(&ev).expect("serialize ApprovalRequested");
1072
+ assert_eq!(v["type"], "approvalRequested");
1073
+ assert_eq!(v["token"], "tok-123");
1074
+ assert_eq!(v["approval"]["RunShell"]["command"], "cargo test");
1075
+ }
1076
+
1077
+ #[test]
1078
+ fn build_live_system_instruction_preserves_memory_skills_and_voice_override_under_budget() {
1079
+ let db_path = std::env::temp_dir().join(format!(
1080
+ "mint-live-sysprompt-test-{}.sqlite3",
1081
+ uuid::Uuid::new_v4()
1082
+ ));
1083
+ let memory = crate::MemoryStore::open(&db_path);
1084
+ let workspace = std::path::Path::new("/tmp/mint-live-budget-ws");
1085
+ let chat_id = "chat-budget-1";
1086
+ let scoped =
1087
+ crate::memory::scoped_chat_id(chat_id, Some(workspace.to_string_lossy().as_ref()));
1088
+
1089
+ memory.set_profile("name", "Pheem").unwrap();
1090
+ for idx in 0..8 {
1091
+ let filler = format!("Turn {idx}: {}", "รายละเอียดระบบเดิม ".repeat(60));
1092
+ memory
1093
+ .add_interaction_for_chat(&scoped, &filler, &filler, "gemini", "gemini-2.5-flash")
1094
+ .unwrap();
1095
+ }
1096
+ memory
1097
+ .add_interaction_for_chat(
1098
+ &scoped,
1099
+ "โปรเจกต์นี้ใช้พอร์ต 4173 นะ",
1100
+ "รับทราบครับ พอร์ต 4173",
1101
+ "gemini",
1102
+ "gemini-2.5-flash",
1103
+ )
1104
+ .unwrap();
1105
+
1106
+ let config = MintConfig::default();
1107
+ let instruction = build_live_system_instruction(&config, workspace, chat_id, Some(&memory));
1108
+
1109
+ assert!(
1110
+ instruction.contains("4173"),
1111
+ "memory context must not be truncated out by a long base prompt"
1112
+ );
1113
+ assert!(
1114
+ instruction.contains("LIVE VOICE MODE OVERRIDE:"),
1115
+ "voice mode override must always be present"
1116
+ );
1117
+ assert!(
1118
+ instruction
1119
+ .trim_end()
1120
+ .ends_with("simply speak your final response out loud to the user."),
1121
+ "voice mode override must be pinned at the very end of system_instruction"
1122
+ );
1123
+ assert!(
1124
+ instruction.chars().count() <= 24_000,
1125
+ "instruction should stay within total 24k char budget, got {}",
1126
+ instruction.chars().count()
1127
+ );
1128
+
1129
+ let _ = std::fs::remove_file(&db_path);
1130
+ }
581
1131
  }