@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.
- package/Cargo.lock +6 -6
- package/Cargo.toml +2 -2
- package/README.md +8 -2
- package/Release_Note.md +222 -71
- package/crates/mint-cli/src/agent/diff_render.rs +66 -44
- package/crates/mint-cli/src/agent/live_status.rs +21 -4
- package/crates/mint-cli/src/agent/markdown_render.rs +342 -5
- package/crates/mint-cli/src/agent.rs +25 -10
- package/crates/mint-cli/src/commands/agent.rs +4 -92
- package/crates/mint-cli/src/commands/browser.rs +2 -15
- package/crates/mint-cli/src/commands/integrations.rs +3 -3
- package/crates/mint-cli/src/commands/mod.rs +0 -3
- package/crates/mint-cli/src/commands/tailnet_web.rs +1 -0
- package/crates/mint-cli/src/interactive/chat_tui/dialog.rs +572 -0
- package/crates/mint-cli/src/interactive/chat_tui/mod.rs +1427 -0
- package/crates/mint-cli/src/interactive/chat_tui/render.rs +1524 -0
- package/crates/mint-cli/src/interactive/chat_tui/selection.rs +401 -0
- package/crates/mint-cli/src/interactive/chat_tui/state.rs +1833 -0
- package/crates/mint-cli/src/interactive/chat_tui/tests.rs +3414 -0
- package/crates/mint-cli/src/interactive/chat_tui/thought.rs +202 -0
- package/crates/mint-cli/src/interactive/chat_tui/transcript.rs +728 -0
- package/crates/mint-cli/src/interactive/chat_tui/watermark.rs +148 -0
- package/crates/mint-cli/src/interactive/command_ui.rs +53 -5
- package/crates/mint-cli/src/interactive/composer.rs +186 -0
- package/crates/mint-cli/src/interactive/format.rs +78 -3
- package/crates/mint-cli/src/interactive/input_box.rs +61 -1
- package/crates/mint-cli/src/interactive/resume_picker.rs +367 -25
- package/crates/mint-cli/src/interactive/slash_commands.rs +648 -371
- package/crates/mint-cli/src/interactive/tool_activity.rs +93 -20
- package/crates/mint-cli/src/interactive.rs +37 -0
- package/crates/mint-cli/src/main.rs +6 -3
- package/crates/mint-cli/src/markdown.rs +2 -37
- package/crates/mint-cli/src/onboard.rs +700 -282
- package/crates/mint-cli/src/setup.rs +1342 -624
- package/crates/mint-cli/src/terminal_theme.rs +159 -35
- package/crates/mint-cli/tests/library_intake.rs +1 -0
- package/crates/mint-core/src/agent/chat/anthropic.rs +318 -0
- package/crates/mint-core/src/agent/chat/custom.rs +149 -0
- package/crates/mint-core/src/agent/chat/gemini.rs +518 -0
- package/crates/mint-core/src/agent/chat/mod.rs +857 -0
- package/crates/mint-core/src/agent/chat/ollama.rs +321 -0
- package/crates/mint-core/src/agent/chat/openai.rs +582 -0
- package/crates/mint-core/src/agent/chat/streaming.rs +469 -0
- package/crates/mint-core/src/agent/chat/tests.rs +1479 -0
- package/crates/mint-core/src/agent/memory.rs +56 -2
- package/crates/mint-core/src/agent/tasks.rs +2 -2
- package/crates/mint-core/src/api_server/routes/chat.rs +24 -2
- package/crates/mint-core/src/api_server/routes/gemini_live.rs +14 -1
- package/crates/mint-core/src/api_server/routes/html_preview.rs +1 -8
- package/crates/mint-core/src/api_server/routes/sessions.rs +21 -0
- package/crates/mint-core/src/avatar_bridge.rs +0 -22
- package/crates/mint-core/src/browser/cdp.rs +10 -4
- package/crates/mint-core/src/browser/headless.rs +582 -0
- package/crates/mint-core/src/browser/input.rs +7 -62
- package/crates/mint-core/src/browser/interact.rs +0 -14
- package/crates/mint-core/src/browser/mod.rs +8 -29
- package/crates/mint-core/src/browser/navigate.rs +2 -36
- package/crates/mint-core/src/browser/runtime.rs +2 -4
- package/crates/mint-core/src/browser/session.rs +50 -466
- package/crates/mint-core/src/browser/workflow.rs +142 -32
- package/crates/mint-core/src/cron/store.rs +2 -2
- package/crates/mint-core/src/integrations/mcp.rs +6 -8
- package/crates/mint-core/src/lib.rs +0 -5
- package/crates/mint-core/src/media/gemini_live.rs +636 -86
- package/crates/mint-core/src/media/library/migration.rs +26 -0
- package/crates/mint-core/src/media/library/tests.rs +1 -1
- package/crates/mint-core/src/media/library/thumbnails.rs +8 -2
- package/crates/mint-core/src/media/pictures.rs +1 -1
- package/crates/mint-core/src/media/tool_images.rs +92 -1
- package/crates/mint-core/src/mods/manager.rs +101 -35
- package/crates/mint-core/src/orchestration/context_render.rs +441 -24
- package/crates/mint-core/src/orchestration/decision_parsing.rs +531 -16
- package/crates/mint-core/src/orchestration/full_skill_result_tests.rs +12 -0
- package/crates/mint-core/src/orchestration/loop_guard.rs +145 -0
- package/crates/mint-core/src/orchestration/mcp_outcome_tests.rs +864 -0
- package/crates/mint-core/src/orchestration/memory_skill.rs +88 -28
- package/crates/mint-core/src/orchestration/mod.rs +320 -4249
- package/crates/mint-core/src/orchestration/reliability.rs +67 -27
- package/crates/mint-core/src/orchestration/review_regressions.rs +5 -0
- package/crates/mint-core/src/orchestration/tests.rs +1665 -0
- package/crates/mint-core/src/orchestration/tools/dispatch.rs +967 -0
- package/crates/mint-core/src/orchestration/tools/files.rs +1 -0
- package/crates/mint-core/src/orchestration/tools/mod.rs +5 -1
- package/crates/mint-core/src/orchestration/tools/planning.rs +102 -42
- package/crates/mint-core/src/orchestration/tools/shell.rs +129 -58
- package/crates/mint-core/src/orchestration/truncation.rs +170 -13
- package/crates/mint-core/src/orchestration/types.rs +648 -0
- package/crates/mint-core/src/orchestration/verification.rs +0 -3
- package/crates/mint-core/src/orchestration/workspace_helpers.rs +24 -6
- package/crates/mint-core/src/prompts/agent.rs +52 -76
- package/crates/mint-core/src/prompts/tool_catalog.rs +8 -151
- package/crates/mint-core/src/search/files.rs +20 -0
- package/crates/mint-core/src/slash/mod.rs +228 -265
- package/crates/mint-core/src/slash/models.rs +6 -1
- package/crates/mint-core/src/system/bg_shell.rs +1 -0
- package/crates/mint-core/src/system/config.rs +176 -34
- package/crates/mint-mod-runtime/src/lib.rs +37 -23
- package/out/web/assets/ArtifactPreviewPanel-Cx6BVOOF.js +2 -0
- package/out/web/assets/CalculationCard-D2E74u5q.js +1 -0
- package/out/web/assets/CliSessionsView-ZbdtSjvK.css +1 -0
- package/out/web/assets/{CliSessionsView-MI8kXMAb.js → CliSessionsView-z5i6qGyb.js} +1 -1
- package/out/web/assets/CodeReviewPage-BK2bOScd.css +1 -0
- package/out/web/assets/CodeReviewPage-Dp6zHuHu.js +2 -0
- package/out/web/assets/{CommandPalette-BD8SlTE5.js → CommandPalette-DaKlzbKs.js} +1 -1
- package/out/web/assets/ImageGenCard-BMCJHP9O.js +1 -0
- package/out/web/assets/ImageSearchCard-Cld_SOUg.js +1 -0
- package/out/web/assets/{ImageStudioPanel-BP7N4rLr.js → ImageStudioPanel-B5N7TjeB.js} +1 -1
- package/out/web/assets/LibraryPage-CMB89rJ0.css +1 -0
- package/out/web/assets/LibraryPage-DbZEi4W_.js +2 -0
- package/out/web/assets/LinkedFoldersView-9B9DIPoZ.js +1 -0
- package/out/web/assets/{McpRegistryPicker-CoS83vtc.js → McpRegistryPicker-DnXPlzI7.js} +1 -1
- package/out/web/assets/McpServersView-CiNBNUl4.js +1 -0
- package/out/web/assets/MermaidCard-BBDNEXbY.js +2 -0
- package/out/web/assets/MintDashboard-C_UYPoNV.js +119 -0
- package/out/web/assets/MintDashboard-DTd8C_c8.css +1 -0
- package/out/web/assets/PlanCard-C2x5pajq.js +1 -0
- package/out/web/assets/{PluginsView-jBYmcKh4.js → PluginsView-DoTg9MVa.js} +1 -1
- package/out/web/assets/RewindModal-B5oowrR0.js +1 -0
- package/out/web/assets/{ScheduledTasksView-5uw9-V9H.js → ScheduledTasksView-BD5DVPkZ.js} +1 -1
- package/out/web/assets/{SelectField-DXBQtrUK.js → SelectField-Cy6CghrU.js} +1 -1
- package/out/web/assets/SettingsWindow-BYl_oCFh.js +4 -0
- package/out/web/assets/SettingsWindow-Ckcgnjt8.css +1 -0
- package/out/web/assets/{SkillsView-C-gp9J7m.js → SkillsView-CIeWwq5z.js} +1 -1
- package/out/web/assets/StockCard-DxUciKyp.js +1 -0
- package/out/web/assets/ToolSurfacePage-qBgs_VHq.css +1 -0
- package/out/web/assets/ToolSurfacePage-saVbVrkE.js +38 -0
- package/out/web/assets/{UiFeatureCard-6BwxTOCe.js → UiFeatureCard-CdY0hGWy.js} +1 -1
- package/out/web/assets/{UiGridCard-DvZcLNxb.js → UiGridCard-BBK3I5hC.js} +1 -1
- package/out/web/assets/UiMockupWidget-CpR9EDuc.js +1 -0
- package/out/web/assets/{VeoStudioPanel-DOj7_WR3.css → VeoStudioPanel-9kU9YLEN.css} +1 -1
- package/out/web/assets/{VeoStudioPanel-DfsOJKv0.js → VeoStudioPanel-Co6-wEWM.js} +2 -2
- package/out/web/assets/WeatherCard-Buzi7Vfu.js +1 -0
- package/out/web/assets/WorkspacePdfPreview-Bi8Zzyir.js +2 -0
- package/out/web/assets/{abnfDiagram-VCTEODGH-BOo3Ckng.js → abnfDiagram-VCTEODGH-CMfR95N2.js} +1 -1
- package/out/web/assets/{architecture-7GRP2DOG-ZOwheW3n.js → architecture-7GRP2DOG-DhAM4HyO.js} +1 -1
- package/out/web/assets/{architectureDiagram-5GKGNRK7-DC2FDzVd.js → architectureDiagram-5GKGNRK7-Yk3TUJ_F.js} +1 -1
- package/out/web/assets/{blockDiagram-I7D4REHJ-BrKFWdAH.js → blockDiagram-I7D4REHJ-X9fBgJik.js} +1 -1
- package/out/web/assets/{c4Diagram-7LVT6UL2-BHTHK1Yd.js → c4Diagram-7LVT6UL2-Bk9x7N-8.js} +1 -1
- package/out/web/assets/{chunk-4HAMMTFA-BYFkZwNj.js → chunk-4HAMMTFA-Dx5fk1_0.js} +1 -1
- package/out/web/assets/{chunk-75Z2AOVW-BSKfSKSA.js → chunk-75Z2AOVW-BszTQoM4.js} +1 -1
- package/out/web/assets/{chunk-DU6HZSFF-vIhsiW6n.js → chunk-DU6HZSFF-DK-As3rk.js} +1 -1
- package/out/web/assets/{chunk-F27PBJKO-BlYpW9xq.js → chunk-F27PBJKO-LmncZZIS.js} +1 -1
- package/out/web/assets/{chunk-GMAD6QVW-BIBz_Ug5.js → chunk-GMAD6QVW-DSQ_sDRJ.js} +1 -1
- package/out/web/assets/{chunk-GVQU2GXP-NPSHxeqC.js → chunk-GVQU2GXP-Cno3ulje.js} +1 -1
- package/out/web/assets/{chunk-IMKFNOWR-YlJK8_Fp.js → chunk-IMKFNOWR-bIzfY9Jc.js} +1 -1
- package/out/web/assets/{chunk-L3NEJ4N5-DMYi4-af.js → chunk-L3NEJ4N5-Dl6eOa0D.js} +1 -1
- package/out/web/assets/{chunk-OSK3NFVY-Begd3-jA.js → chunk-OSK3NFVY-BaUGJ-cM.js} +1 -1
- package/out/web/assets/{chunk-P2QGCYS3-D1I4xFHN.js → chunk-P2QGCYS3-_066ndhH.js} +1 -1
- package/out/web/assets/{chunk-POPQ4Y6H-CafZZzNE.js → chunk-POPQ4Y6H-YT1AKhTv.js} +1 -1
- package/out/web/assets/{chunk-PWAF6VOD-Dwh6_Lnt.js → chunk-PWAF6VOD-CrEgDsxh.js} +1 -1
- package/out/web/assets/{chunk-SHT3W25Y-B0XrCSEb.js → chunk-SHT3W25Y-_KwaRLOl.js} +1 -1
- package/out/web/assets/{chunk-SVP7TREG-CxLsddxC.js → chunk-SVP7TREG-BQ1qb32L.js} +1 -1
- package/out/web/assets/{chunk-TICWLB2K-y156QInb.js → chunk-TICWLB2K-BDyehCtg.js} +1 -1
- package/out/web/assets/classDiagram-ZZMXUADV-BoSpzRCJ.js +1 -0
- package/out/web/assets/classDiagram-v2-VYDZK3BY-BoSpzRCJ.js +1 -0
- package/out/web/assets/{config-DqXJXeZM.css → config-2suVFPZg.css} +1 -1
- package/out/web/assets/config-CkRCdkxn.js +2 -0
- package/out/web/assets/{cynefin-OW5HDTMX-SMVvrLf-.js → cynefin-OW5HDTMX-Dl_31aaN.js} +1 -1
- package/out/web/assets/{cynefinDiagram-5FMLGOSQ-rZznN24T.js → cynefinDiagram-5FMLGOSQ-Bpir8jha.js} +1 -1
- package/out/web/assets/{dagre-GXQ25YYZ-BOToydFu.js → dagre-GXQ25YYZ-D9xu3nsq.js} +1 -1
- package/out/web/assets/{diagram-S7CK7UJ4-TMVaGQEJ.js → diagram-S7CK7UJ4-DeveoEeB.js} +1 -1
- package/out/web/assets/{diagram-UQ7AKVKN-C2JK-GP2.js → diagram-UQ7AKVKN-p4xWRESV.js} +1 -1
- package/out/web/assets/{diagram-VSXAHHWV-Cw8VSi2K.js → diagram-VSXAHHWV-C7qz9NDF.js} +1 -1
- package/out/web/assets/{diagram-VX7I27RA-DXp4fKk9.js → diagram-VX7I27RA-DRiCa3Vd.js} +1 -1
- package/out/web/assets/{diagram-Z3DM3KII-CzSicYiW.js → diagram-Z3DM3KII-Bt7zduQX.js} +1 -1
- package/out/web/assets/{ebnfDiagram-PWID7BFC-Dqj-zqPu.js → ebnfDiagram-PWID7BFC-DV9WpwdR.js} +1 -1
- package/out/web/assets/{erDiagram-RLTQ6QDP-DZwFZAuD.js → erDiagram-RLTQ6QDP-BJ_nwG2m.js} +1 -1
- package/out/web/assets/{eventmodeling-NTZA5JFV-C0O-Lvgp.js → eventmodeling-NTZA5JFV-BTBMrfVa.js} +1 -1
- package/out/web/assets/flowDiagram-HODETNUW-Cdlq3cJL.js +1 -0
- package/out/web/assets/{ganttDiagram-EL5Y4UJY-Da4ULXTl.js → ganttDiagram-EL5Y4UJY-DQLdsXrC.js} +1 -1
- package/out/web/assets/{gitGraph-4MIJSDKK-B9wNFZIP.js → gitGraph-4MIJSDKK-CL1OEOQ_.js} +1 -1
- package/out/web/assets/{gitGraphDiagram-WWUBYQGX-RjrhB0MP.js → gitGraphDiagram-WWUBYQGX-8cSi42H7.js} +1 -1
- package/out/web/assets/index-BocxnrDG.js +168 -0
- package/out/web/assets/index-yeh_r55a.css +1 -0
- package/out/web/assets/{info-A6RAGUB7-B3dfCpeY.js → info-A6RAGUB7-C1zsX5sc.js} +1 -1
- package/out/web/assets/{infoDiagram-27XIBGKW-QVWkLVx7.js → infoDiagram-27XIBGKW-DsRYtIyI.js} +1 -1
- package/out/web/assets/{ishikawaDiagram-5VMMS53U-CLFNIRI9.js → ishikawaDiagram-5VMMS53U-B2crwIXj.js} +1 -1
- package/out/web/assets/{journeyDiagram-3NMN7TZE-fgOQadYF.js → journeyDiagram-3NMN7TZE-BTH-GyVR.js} +1 -1
- package/out/web/assets/{kanban-definition-UXKFOSKX-G3Q_9g9O.js → kanban-definition-UXKFOSKX-BeRfCnuA.js} +1 -1
- package/out/web/assets/{mermaid-parser.core-v3Jm7nco.js → mermaid-parser.core-BJwgCBBc.js} +3 -3
- package/out/web/assets/{mermaid.core-DTEjLUA1.js → mermaid.core-CsDDodN8.js} +4 -4
- package/out/web/assets/{mindmap-definition-YA3MSWOX-C_D42_fF.js → mindmap-definition-YA3MSWOX-B42efOQ1.js} +1 -1
- package/out/web/assets/{modelManager-BPLycQrN.js → modelManager-B3ZFMh8v.js} +2 -2
- package/out/web/assets/{oauthManager-BUK7ic7q.js → oauthManager-Ch5Du2fF.js} +1 -1
- package/out/web/assets/{packet-AYTQ26CC-B-R-SHlF.js → packet-AYTQ26CC-B6xUyE6r.js} +1 -1
- package/out/web/assets/{pegDiagram-XKGWAZYB-Da2q_giz.js → pegDiagram-XKGWAZYB-1LabVycL.js} +1 -1
- package/out/web/assets/{pie-WAS4IAKB-BC8F0N7-.js → pie-WAS4IAKB-DJwo-nf_.js} +1 -1
- package/out/web/assets/{pieDiagram-E7YTZNPT-Iy9REJNC.js → pieDiagram-E7YTZNPT-icmAqzy4.js} +1 -1
- package/out/web/assets/{quadrantDiagram-AXDQQJYC-ztsiYw7h.js → quadrantDiagram-AXDQQJYC-CZ-wEx41.js} +1 -1
- package/out/web/assets/{radar-RG4KPBEZ-B_1P8Z_z.js → radar-RG4KPBEZ-D7d-Xriw.js} +1 -1
- package/out/web/assets/{railroad-74A4TZTK-DuTqczSo.js → railroad-74A4TZTK-viuhdYbs.js} +1 -1
- package/out/web/assets/{railroad-abnf-HS5TGJTU-Df29WLmp.js → railroad-abnf-HS5TGJTU-DNXiCNea.js} +1 -1
- package/out/web/assets/{railroad-ebnf-LZEXJU2U-ePFMo-qp.js → railroad-ebnf-LZEXJU2U-nkSu1-0C.js} +1 -1
- package/out/web/assets/{railroad-peg-WCYAUIDC-DYXLW4_E.js → railroad-peg-WCYAUIDC-BhbMeQzz.js} +1 -1
- package/out/web/assets/{railroadDiagram-O6MQD6OU-LStnZf2S.js → railroadDiagram-O6MQD6OU-CLpG1jUX.js} +1 -1
- package/out/web/assets/{requirementDiagram-BXWQKSXE-DmzonMH-.js → requirementDiagram-BXWQKSXE-kRDrvWrc.js} +1 -1
- package/out/web/assets/{sankeyDiagram-P5KCCOFB-B8vjmUU1.js → sankeyDiagram-P5KCCOFB-DxAwmq4x.js} +1 -1
- package/out/web/assets/{sequenceDiagram-WJ2MYXX4-CTnplOAX.js → sequenceDiagram-WJ2MYXX4-aLgY-6Wz.js} +1 -1
- package/out/web/assets/{stateDiagram-D77RDMKH-BFrIaLdI.js → stateDiagram-D77RDMKH-D5QWfOhp.js} +1 -1
- package/out/web/assets/stateDiagram-v2-MP3YSRHH-D6OiZoAP.js +1 -0
- package/out/web/assets/{swimlanes-42K2YHIH-BXGngUNU.js → swimlanes-42K2YHIH-CRs5UyA-.js} +1 -1
- package/out/web/assets/swimlanesDiagram-VR7AAH4N-UgEXIsV0.js +8 -0
- package/out/web/assets/syntaxHighlight-C-wAGT84.js +3 -0
- package/out/web/assets/{timeline-definition-24CTP7MA-CegH7n43.js → timeline-definition-24CTP7MA-Denqd9qb.js} +1 -1
- package/out/web/assets/{treeView-Q6P3EWNA-DTpJv2Tw.js → treeView-Q6P3EWNA-0my3Z3s4.js} +1 -1
- package/out/web/assets/{treemap-WGGIJYW6-i8M9iJU6.js → treemap-WGGIJYW6-B_EjkLYq.js} +1 -1
- package/out/web/assets/{vennDiagram-4TSXK5OY-Dpzo1S8U.js → vennDiagram-4TSXK5OY-DxVg3Ewr.js} +1 -1
- package/out/web/assets/{wardley-WFR3VGLG-BrNsgtfw.js → wardley-WFR3VGLG-CGD-Tj4p.js} +1 -1
- package/out/web/assets/{wardleyDiagram-VM6X3IG4-lQ92LFE1.js → wardleyDiagram-VM6X3IG4-CmfS39Xm.js} +1 -1
- package/out/web/assets/{xychartDiagram-S5SC5T6Z-BaOaEoBd.js → xychartDiagram-S5SC5T6Z-BVM9bKis.js} +1 -1
- package/out/web/index-web.html +2 -2
- package/out/web/index.html +2 -2
- package/package.json +2 -2
- package/shared-modules/CODEOWNERS +3 -3
- package/shared-modules/SDL2/SDL2_mixer.json +27 -0
- package/shared-modules/appstream/README.md +13 -0
- package/shared-modules/appstream/appstream-compose.json +111 -0
- package/shared-modules/dbus-glib/dbus-glib.json +2 -2
- package/shared-modules/libportal/libportal.json +23 -0
- package/shared-modules/linux-audio/fluidsynth2-static.json +2 -2
- package/shared-modules/linux-audio/fluidsynth2.json +2 -2
- package/shared-modules/linux-audio/libinstpatch.json +2 -2
- package/shared-modules/linux-audio/liblo-static.json +3 -0
- package/shared-modules/luajit/luajit.json +2 -2
- package/shared-modules/squashfs-tools/squashfs-tools.json +0 -3
- package/slash-commands.json +7 -2
- package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/renderer/shared/agentProgress.ts +51 -3
- package/src/renderer/shared/components/Settings/AutomationTab.tsx +2 -56
- package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +15 -1
- package/src/renderer/shared/components/Settings/GeneralTab.tsx +25 -12
- package/src/renderer/shared/components/Settings/ThemeTab.test.tsx +45 -0
- package/src/renderer/shared/components/Settings/ThemeTab.tsx +106 -3
- package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +67 -17
- package/src/renderer/shared/components/activity/AgentActivityTable.tsx +376 -134
- package/src/renderer/shared/components/activity/AskUserActivityCard.test.tsx +122 -0
- package/src/renderer/shared/components/activity/AskUserActivityCard.tsx +70 -0
- package/src/renderer/shared/components/activity/McpToolResults.tsx +39 -2
- package/src/renderer/shared/components/activity/SubagentDagView.tsx +7 -7
- package/src/renderer/shared/components/activity/ViewedImages.tsx +41 -2
- package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +3 -3
- package/src/renderer/shared/components/app/CommandPalette.tsx +7 -7
- package/src/renderer/shared/components/app/DesktopTitlebar.tsx +2 -2
- package/src/renderer/shared/components/app/MintDashboard.tsx +27 -17
- package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +3 -3
- package/src/renderer/shared/components/cards/CalculationCard.tsx +3 -3
- package/src/renderer/shared/components/cards/ImageGenCard.tsx +2 -2
- package/src/renderer/shared/components/cards/ImageSearchCard.tsx +2 -2
- package/src/renderer/shared/components/cards/MermaidCard.tsx +1 -1
- package/src/renderer/shared/components/cards/PlanCard.test.tsx +41 -0
- package/src/renderer/shared/components/cards/PlanCard.tsx +32 -0
- package/src/renderer/shared/components/cards/ReleaseNotesCard.test.tsx +29 -0
- package/src/renderer/shared/components/cards/ReleaseNotesCard.tsx +137 -0
- package/src/renderer/shared/components/cards/StockCard.tsx +7 -7
- package/src/renderer/shared/components/cards/ToolResultCards.tsx +11 -3
- package/src/renderer/shared/components/cards/UiMockupWidget.tsx +4 -4
- package/src/renderer/shared/components/cards/WeatherCard.tsx +5 -5
- package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +11 -0
- package/src/renderer/shared/components/chat/ApprovalCard.tsx +21 -2
- package/src/renderer/shared/components/chat/ArtifactCards.test.tsx +69 -0
- package/src/renderer/shared/components/chat/ArtifactCards.tsx +238 -0
- package/src/renderer/shared/components/chat/AttachedDocumentCard.test.tsx +39 -0
- package/src/renderer/shared/components/chat/AttachedDocumentCard.tsx +220 -0
- package/src/renderer/shared/components/chat/ChatCodeBlock.test.tsx +33 -0
- package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +15 -1
- package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +40 -2
- package/src/renderer/shared/components/chat/ChatMessageItem.tsx +180 -34
- package/src/renderer/shared/components/chat/ChatModeSelector.css +227 -0
- package/src/renderer/shared/components/chat/ChatModeSelector.test.tsx +45 -0
- package/src/renderer/shared/components/chat/ChatModeSelector.tsx +198 -0
- package/src/renderer/shared/components/chat/ChatPanel.test.tsx +22 -0
- package/src/renderer/shared/components/chat/ChatPanel.tsx +227 -53
- package/src/renderer/shared/components/chat/DiffReviewModal.tsx +6 -6
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +142 -1
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +45 -3
- package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +42 -14
- package/src/renderer/shared/components/chat/RewindModal.tsx +10 -10
- package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +14 -0
- package/src/renderer/shared/components/chat/SourcesBlock.test.tsx +53 -0
- package/src/renderer/shared/components/chat/SourcesBlock.tsx +236 -90
- package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +45 -1
- package/src/renderer/shared/components/chat/TurnFileChanges.tsx +202 -169
- package/src/renderer/shared/components/extensions/CliSessionsView.tsx +5 -4
- package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +3 -3
- package/src/renderer/shared/components/studio/LibraryPage.tsx +48 -5
- package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +125 -13
- package/src/renderer/shared/components/workspace/CodeReviewPage.test.tsx +50 -0
- package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +77 -16
- package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +78 -22
- package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +13 -0
- package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +48 -28
- package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +266 -47
- package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +18 -6
- package/src/renderer/shared/components/workspace/background-terminals.css +11 -11
- package/src/renderer/shared/constants/config.ts +1 -5
- package/src/renderer/shared/constants/models.test.ts +51 -0
- package/src/renderer/shared/constants/models.ts +139 -32
- package/src/renderer/shared/conversation/lifecycle.test.ts +91 -3
- package/src/renderer/shared/conversation/lifecycle.ts +104 -5
- package/src/renderer/shared/conversation/progressiveStream.test.ts +83 -0
- package/src/renderer/shared/conversation/progressiveStream.ts +219 -0
- package/src/renderer/shared/css/agent-observability.css +133 -0
- package/src/renderer/shared/css/chat-ui-cards.css +13 -13
- package/src/renderer/shared/css/code-review.css +63 -34
- package/src/renderer/shared/css/code-sessions.css +47 -18
- package/src/renderer/shared/css/collapsed-paste.css +1 -1
- package/src/renderer/shared/css/error-notice.css +1 -1
- package/src/renderer/shared/css/file-changes.css +165 -80
- package/src/renderer/shared/css/library.css +105 -10
- package/src/renderer/shared/css/management-views.css +1 -1
- package/src/renderer/shared/css/mcp-results.css +5 -5
- package/src/renderer/shared/css/model-selector.css +18 -18
- package/src/renderer/shared/css/mods.css +7 -7
- package/src/renderer/shared/css/notification-notice.css +2 -2
- package/src/renderer/shared/css/queued-messages.css +5 -5
- package/src/renderer/shared/css/settings/general.css +18 -13
- package/src/renderer/shared/css/settings/profile.css +5 -5
- package/src/renderer/shared/css/settings/theme.css +5 -5
- package/src/renderer/shared/css/slash-choice.css +2 -2
- package/src/renderer/shared/css/slash-suggestions.css +3 -3
- package/src/renderer/shared/css/sources-block.css +41 -2
- package/src/renderer/shared/css/tools-panel.css +2 -2
- package/src/renderer/shared/css/ui-tailwind.css +8 -0
- package/src/renderer/shared/css/veo-studio.css +2 -2
- package/src/renderer/shared/css/viewed-images.css +7 -7
- package/src/renderer/shared/platform.ts +2 -1
- package/src/renderer/shared/theme/themeManager.test.ts +17 -0
- package/src/renderer/shared/theme/themeManager.ts +21 -4
- package/src/renderer/shared/theme/themes.ts +54 -0
- package/src/renderer/shared/theme/tokens.css +6 -4
- package/src/renderer/shared/types.ts +1 -1
- package/src/renderer/shared/utils/agentActivity.test.ts +468 -2
- package/src/renderer/shared/utils/agentActivity.ts +602 -35
- package/src/renderer/shared/utils/artifactBrowser.test.ts +37 -32
- package/src/renderer/shared/utils/artifactBrowser.ts +17 -19
- package/src/renderer/shared/utils/attachedDocument.test.ts +73 -0
- package/src/renderer/shared/utils/attachedDocument.ts +49 -0
- package/src/renderer/shared/utils/fileIcons.test.ts +59 -0
- package/src/renderer/shared/utils/fileIcons.ts +149 -16
- package/src/renderer/shared/utils/markdown.tsx +72 -19
- package/src/renderer/shared/utils/markdown.webImages.test.tsx +16 -0
- package/src/renderer/shared/utils/mcpResults.test.tsx +29 -0
- package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
- package/src/renderer/shared/utils/syntaxHighlight.test.tsx +11 -1
- package/src/renderer/shared/utils/syntaxHighlight.tsx +51 -3
- package/src/renderer/shared/utils/useGeminiLiveVoice.ts +324 -51
- package/src/renderer/shared/utils/workspaceDrag.test.ts +153 -0
- package/src/renderer/shared/utils/workspaceDrag.ts +142 -0
- package/src/renderer/shared/utils/workspaceMedia.ts +10 -2
- package/src/renderer/src/App.tsx +14 -8
- package/src/renderer/src/agentProgress.ts +1 -0
- package/src/renderer/src/components/ScreenPicker.css +1 -1
- package/src/renderer/src/components/WorkspacePanel.tsx +6 -5
- package/src/renderer/src/css/app-shell.css +4 -4
- package/src/renderer/src/css/chat.css +1261 -241
- package/src/renderer/src/css/desktop-tools.css +11 -11
- package/src/renderer/src/css/sidebar-workspace.css +2 -2
- package/src/renderer/src/css/tool-surfaces.css +12 -12
- package/src/renderer/src/index.css +38 -20
- package/src/renderer/src/tauri.ts +22 -2
- package/src/renderer/src-web/App.tsx +14 -0
- package/src/renderer/src-web/agentProgress.ts +1 -0
- package/src/renderer/src-web/css/app-shell.css +4 -4
- package/src/renderer/src-web/css/chat.css +1278 -190
- package/src/renderer/src-web/css/mobile.css +29 -13
- package/src/renderer/src-web/css/settings/mobile.css +2 -2
- package/src/renderer/src-web/css/sidebar-workspace.css +1 -1
- package/src/renderer/src-web/index.css +38 -20
- package/src/renderer/src-web/tauri.ts +22 -2
- package/src/renderer/tests/browserArtifacts.browser.tsx +1 -1
- package/src/renderer/tests/library.browser.tsx +3 -0
- package/src/renderer/tests/liveVoice.browser.html +1 -0
- package/src/renderer/tests/workspaceDrop.browser.tsx +1 -1
- package/src-tauri/src/app/lifecycle.rs +1 -14
- package/src-tauri/src/background/headless.rs +14 -8
- package/src-tauri/src/commands/conversation.rs +38 -0
- package/src-tauri/src/native/mod.rs +0 -1
- package/src-tauri/src/native/system.rs +4 -47
- package/crates/mint-cli/src/interactive/chat_tui.rs +0 -8855
- package/crates/mint-core/src/agent/chat.rs +0 -4394
- package/crates/mint-core/src/browser/lifecycle.rs +0 -291
- package/crates/mint-core/src/browser/logging.rs +0 -20
- package/crates/mint-core/src/orchestration/tools/browser.rs +0 -31
- package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +0 -2
- package/out/web/assets/CalculationCard-CRjP3E1j.js +0 -1
- package/out/web/assets/CliSessionsView-C3Br2v5Y.css +0 -1
- package/out/web/assets/CodeReviewPage-CUJceQme.css +0 -1
- package/out/web/assets/CodeReviewPage-VCUgPla_.js +0 -2
- package/out/web/assets/ImageGenCard-BCXxkVJX.js +0 -1
- package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +0 -1
- package/out/web/assets/LibraryPage-DAxUTAg7.css +0 -1
- package/out/web/assets/LibraryPage-WlviRdR8.js +0 -2
- package/out/web/assets/LinkedFoldersView-CKRgroWK.js +0 -1
- package/out/web/assets/McpServersView-D3wHnuHg.js +0 -1
- package/out/web/assets/MermaidCard-NqrHdyJW.js +0 -2
- package/out/web/assets/MintDashboard-B2DJsDDO.css +0 -1
- package/out/web/assets/MintDashboard-COw_xol7.js +0 -63
- package/out/web/assets/RewindModal-DempuDod.js +0 -1
- package/out/web/assets/SettingsWindow-2YP3yj7s.js +0 -4
- package/out/web/assets/SettingsWindow-Dz8LQVgD.css +0 -1
- package/out/web/assets/StockCard-BDMCe7mx.js +0 -1
- package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +0 -38
- package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +0 -1
- package/out/web/assets/UiMockupWidget-DD73QWg1.js +0 -1
- package/out/web/assets/WeatherCard-CPucIfq8.js +0 -1
- package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +0 -2
- package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +0 -1
- package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +0 -1
- package/out/web/assets/config-CvIO5hkn.js +0 -2
- package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +0 -1
- package/out/web/assets/index-BmpulIkg.js +0 -168
- package/out/web/assets/index-CkvBwhX_.css +0 -1
- package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +0 -1
- package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +0 -8
- package/shared-modules/libsecret/libsecret.json +0 -23
- package/shared-modules/libusb/libusb.json +0 -27
- package/shared-modules/lzo/lzo.json +0 -27
- package/src/renderer/src/components/BrowserShell.tsx +0 -108
- package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +0 -91
- package/src/renderer/src/components/browser-shell.css +0 -185
- 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 ||
|
|
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
|
-
///
|
|
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(
|
|
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
|
|
315
|
+
fn agent_action_fallback_declaration() -> Value {
|
|
195
316
|
json!({
|
|
196
|
-
"
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"
|
|
200
|
-
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
|
|
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
|
-
"
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
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": [
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
455
|
-
let
|
|
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
|
}
|