mulmoterminal 4.11.0 → 4.12.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 (140) hide show
  1. package/bin/cli-args.d.ts +2 -0
  2. package/bin/cli-args.js +82 -6
  3. package/bin/mulmoterminal.js +58 -22
  4. package/common/keymap.ts +1 -0
  5. package/common/modelPresets.ts +31 -31
  6. package/dist/assets/{AllPackages-CF2GkZ5g-TMZZvtbW.js → AllPackages-CF2GkZ5g-CM3rGYLT.js} +1 -1
  7. package/dist/assets/{BaseConfiguration-CAwZOAki-GjB8ynTB.js → BaseConfiguration-CAwZOAki-DTVgM-PG.js} +1 -1
  8. package/dist/assets/{Element-BMI7Nj5w-DDy3RxhW.js → Element-BMI7Nj5w-B9RqTJwZ.js} +1 -1
  9. package/dist/assets/{Entities-C8p_0jCn-TW67o_f5.js → Entities-C8p_0jCn-B96BdpL6.js} +1 -1
  10. package/dist/assets/{FunctionList-YWk54TCy-BWTkoS8A.js → FunctionList-YWk54TCy-Dftsdlhu.js} +1 -1
  11. package/dist/assets/{InputJax-C5dj-ntL-D3nfgJug.js → InputJax-C5dj-ntL-CSXYU6NO.js} +1 -1
  12. package/dist/assets/{MathItem-YJvQL-XJ-CRfwqo-l.js → MathItem-YJvQL-XJ-Chiavq2E.js} +1 -1
  13. package/dist/assets/{MmlFactory-DuXYIgx8-CPv_nF61.js → MmlFactory-DuXYIgx8-DxABlOoL.js} +1 -1
  14. package/dist/assets/{Options-oARQeeuT-B-G7KuuU.js → Options-oARQeeuT-C2dJZp_J.js} +1 -1
  15. package/dist/assets/{OutputJax-DfUZNNCu-YqxqNBIm.js → OutputJax-DfUZNNCu-CFu_qe01.js} +1 -1
  16. package/dist/assets/{PrioritizedList-6lun1Xqd-C9mZ74pe.js → PrioritizedList-6lun1Xqd-B1IOIxus.js} +1 -1
  17. package/dist/assets/{Styles-BPXeoqzi-BlJASvFz.js → Styles-BPXeoqzi-CENhg2Rx.js} +1 -1
  18. package/dist/assets/{TeXAtom-LHT3KFWH-dhvXc6fF.js → TeXAtom-LHT3KFWH-DzkJr1zJ.js} +1 -1
  19. package/dist/assets/{abnfDiagram-VCTEODGH-DoYNjdBB-CTSR-65e.js → abnfDiagram-VCTEODGH-DoYNjdBB-BTUEucbt.js} +1 -1
  20. package/dist/assets/{arc-CqByPKkF-BWYcebkO.js → arc-CqByPKkF-D8g1A1VD.js} +1 -1
  21. package/dist/assets/architecture-7GRP2DOG-CRS69XmP-Ujv7TsXX.js +1 -0
  22. package/dist/assets/{architectureDiagram-5GKGNRK7-BhcT6qS5-ot6sa_to.js → architectureDiagram-5GKGNRK7-BhcT6qS5-CYwxV1T4.js} +1 -1
  23. package/dist/assets/{assistive-mml-DXJXho4o-67vts26y.js → assistive-mml-DXJXho4o-DHaLYeaf.js} +1 -1
  24. package/dist/assets/{blockDiagram-I7D4REHJ-1Cng0Q4K-DXMuvcCc.js → blockDiagram-I7D4REHJ-1Cng0Q4K-jkjSYlHD.js} +1 -1
  25. package/dist/assets/{c4Diagram-7LVT6UL2-CAb9c8nc-odesI5Pl.js → c4Diagram-7LVT6UL2-CAb9c8nc-CVBP7bme.js} +1 -1
  26. package/dist/assets/channel-BjESmYnj-BFm2Wbs0.js +1 -0
  27. package/dist/assets/{chunk-2E4U76K2-CMyNbRyJ-B-LU-Now.js → chunk-2E4U76K2-CMyNbRyJ-CYlEPSYt.js} +1 -1
  28. package/dist/assets/{chunk-4HAMMTFA-ChwfIAnF-BiK_fwBU.js → chunk-4HAMMTFA-ChwfIAnF-DLsNAJrX.js} +1 -1
  29. package/dist/assets/{chunk-75Z2AOVW-CbPA0Zt_-D1SUnijv.js → chunk-75Z2AOVW-CbPA0Zt_-PBrNY7F4.js} +1 -1
  30. package/dist/assets/{chunk-CLGD4ZFX-CVw8XHcp-B9QKRmDx.js → chunk-CLGD4ZFX-CVw8XHcp-XVRICqz6.js} +1 -1
  31. package/dist/assets/{chunk-DU6HZSFF-DuOx2LFX-C43wsWw1.js → chunk-DU6HZSFF-DuOx2LFX-B0VtwR-4.js} +1 -1
  32. package/dist/assets/{chunk-F27PBJKO-CgBMw9yN-DNwf2ctW.js → chunk-F27PBJKO-CgBMw9yN-AuWmarZj.js} +1 -1
  33. package/dist/assets/{chunk-GMAD6QVW-JTqlimUX-BUdn0gzb.js → chunk-GMAD6QVW-JTqlimUX-DjcFuvKJ.js} +1 -1
  34. package/dist/assets/{chunk-GVQU2GXP-D6ccB4DO-B92RdJRw.js → chunk-GVQU2GXP-D6ccB4DO-z0JsvnGC.js} +1 -1
  35. package/dist/assets/{chunk-IMKFNOWR-ij1DSzMb-DOUGEsyB.js → chunk-IMKFNOWR-ij1DSzMb-Cp2kQ6eT.js} +1 -1
  36. package/dist/assets/{chunk-L3NEJ4N5-CUoAQVG2-fG9UphPN.js → chunk-L3NEJ4N5-CUoAQVG2-B_R7YU1U.js} +1 -1
  37. package/dist/assets/{chunk-OSK3NFVY-_VNbXQSu-CThk-KLN.js → chunk-OSK3NFVY-_VNbXQSu-D2nH_Zoh.js} +1 -1
  38. package/dist/assets/{chunk-P2QGCYS3-B8lzOn3Y-jyt35vFj.js → chunk-P2QGCYS3-B8lzOn3Y-CdxuD9Vb.js} +1 -1
  39. package/dist/assets/{chunk-POPQ4Y6H-Bh_uOTmp-Dky8MeiN.js → chunk-POPQ4Y6H-Bh_uOTmp-DV6DnZ5t.js} +1 -1
  40. package/dist/assets/{chunk-PWAF6VOD-DzjnfYSw-BlUVhmUo.js → chunk-PWAF6VOD-DzjnfYSw-CbNtsCaS.js} +1 -1
  41. package/dist/assets/{chunk-SHT3W25Y-B8dQLS5w-BcPtuz8O.js → chunk-SHT3W25Y-B8dQLS5w-CLbCDkIa.js} +1 -1
  42. package/dist/assets/{chunk-SVP7TREG-B6A-T8Th-CwjRILYx.js → chunk-SVP7TREG-B6A-T8Th-C73GGvfo.js} +1 -1
  43. package/dist/assets/{chunk-TICWLB2K-C1kwD29l--WxpGM-A.js → chunk-TICWLB2K-C1kwD29l-kqi5-13I.js} +1 -1
  44. package/dist/assets/{chunk-TLUHSLCS-DYzGlCmi-CzsyGew0.js → chunk-TLUHSLCS-DYzGlCmi-Btk2TYDp.js} +2 -2
  45. package/dist/assets/{chunk-XXDRQBXY-vg0kfC73-BmBF_aLi.js → chunk-XXDRQBXY-vg0kfC73-C9Og4olA.js} +1 -1
  46. package/dist/assets/classDiagram-ZZMXUADV-C7gP-pUH-BDpOwtPC.js +1 -0
  47. package/dist/assets/classDiagram-v2-VYDZK3BY-CUwV5k1F-BDpOwtPC.js +1 -0
  48. package/dist/assets/{cose-bilkent-JH36ORCC-NuHCtE3P-DukuXr0B.js → cose-bilkent-JH36ORCC-NuHCtE3P-sgsTiDYm.js} +1 -1
  49. package/dist/assets/cynefin-OW5HDTMX-C4iKivN4-C8N8egm0.js +1 -0
  50. package/dist/assets/{cynefinDiagram-5FMLGOSQ-CA7sro34-B1_Vwft3.js → cynefinDiagram-5FMLGOSQ-CA7sro34-tAE4dFM9.js} +1 -1
  51. package/dist/assets/{dagre-GXQ25YYZ-DOztjUR_-B2ZYuKJq.js → dagre-GXQ25YYZ-DOztjUR_-CfL9WIVN.js} +1 -1
  52. package/dist/assets/{diagram-S7CK7UJ4-D_nsLfRT-C1iWmOQl.js → diagram-S7CK7UJ4-D_nsLfRT-DEQewt62.js} +1 -1
  53. package/dist/assets/{diagram-UQ7AKVKN-DNN-YX9a-D9BFLsdH.js → diagram-UQ7AKVKN-DNN-YX9a-Yh8uxMUF.js} +1 -1
  54. package/dist/assets/{diagram-VSXAHHWV--HENzo-l-Bilfd4yf.js → diagram-VSXAHHWV--HENzo-l-0GmA-ovB.js} +1 -1
  55. package/dist/assets/{diagram-VX7I27RA-m2m9dROj-Q5Y5GIFz.js → diagram-VX7I27RA-m2m9dROj-Cm-nFEG1.js} +1 -1
  56. package/dist/assets/{diagram-Z3DM3KII-D4qpv4-B-77eizdrr.js → diagram-Z3DM3KII-D4qpv4-B-BVX8YbHU.js} +1 -1
  57. package/dist/assets/{dist-DGvve3y_.js → dist-C8NBNY_7.js} +1 -1
  58. package/dist/assets/{dist-DLWwOuwV.js → dist-CfoBFpi6.js} +1 -1
  59. package/dist/assets/{dist-b5QRFCaM.js → dist-D24DAoP7.js} +1 -1
  60. package/dist/assets/{dist-Be3Ws2yA.js → dist-D7zq5Vj0.js} +1 -1
  61. package/dist/assets/{dist-DeiyD9Ki-tTrbAW3O.js → dist-DeiyD9Ki-9JRmM4Z8.js} +1 -1
  62. package/dist/assets/{ebnfDiagram-PWID7BFC-CAbv3sZg-DuR3ooJN.js → ebnfDiagram-PWID7BFC-CAbv3sZg-D8QuBFYM.js} +1 -1
  63. package/dist/assets/{erDiagram-RLTQ6QDP-a10Ryh0--D8L6d95w.js → erDiagram-RLTQ6QDP-a10Ryh0--B1HSzwsq.js} +1 -1
  64. package/dist/assets/eventmodeling-NTZA5JFV-DhqtNS8R-G2GKHcUd.js +1 -0
  65. package/dist/assets/flowDiagram-HODETNUW-CAW_8mwY-DUShNtXL.js +1 -0
  66. package/dist/assets/{ganttDiagram-EL5Y4UJY-Cb7leeOC-BYaZlBFC.js → ganttDiagram-EL5Y4UJY-Cb7leeOC-keCW-kOD.js} +1 -1
  67. package/dist/assets/gitGraph-4MIJSDKK-S_Zs6Nxs-AZ1aHKyD.js +1 -0
  68. package/dist/assets/{gitGraphDiagram-WWUBYQGX-BCSbUpXG-DeD4rKlZ.js → gitGraphDiagram-WWUBYQGX-BCSbUpXG-DnzApyVv.js} +1 -1
  69. package/dist/assets/{html-DdQiKDqM-BgbS8tLG.js → html-DdQiKDqM-B-K3Ykhn.js} +1 -1
  70. package/dist/assets/{index-DTF7hxpt.js → index-CeO5tt2R.js} +151 -151
  71. package/dist/assets/index-T6iAhWWD.css +1 -0
  72. package/dist/assets/info-A6RAGUB7-4sGJh1is-BsYxYGyO.js +1 -0
  73. package/dist/assets/{infoDiagram-27XIBGKW-BJRaZItN-vTwEGfqm.js → infoDiagram-27XIBGKW-BJRaZItN-Vim8p0oz.js} +1 -1
  74. package/dist/assets/{ishikawaDiagram-5VMMS53U-DHlFe__S-BDYdHY2W.js → ishikawaDiagram-5VMMS53U-DHlFe__S-O3s46PZl.js} +1 -1
  75. package/dist/assets/{journeyDiagram-3NMN7TZE-BOIo3JM--BKbx2G9c.js → journeyDiagram-3NMN7TZE-BOIo3JM--BUdLRefU.js} +1 -1
  76. package/dist/assets/{kanban-definition-UXKFOSKX-yrQdG683-NGZtsrcK.js → kanban-definition-UXKFOSKX-yrQdG683-tuB3PItC.js} +1 -1
  77. package/dist/assets/{lengths-CvmPfBid-BXoQHcuz.js → lengths-CvmPfBid-QuWmlAY4.js} +1 -1
  78. package/dist/assets/{line-DUb1TjJS-DnfCySOi.js → line-DUb1TjJS-CIXFDmfK.js} +1 -1
  79. package/dist/assets/{linear-xsuGWu5q-D8AeuxUI.js → linear-xsuGWu5q-B0UqjTq4.js} +1 -1
  80. package/dist/assets/{liteAdaptor-C0FQiOEx-CVfFqXXv.js → liteAdaptor-C0FQiOEx-Dd7ejALY.js} +1 -1
  81. package/dist/assets/{marp-CEgDv2lS.js → marp-eTHdUTCa.js} +1 -1
  82. package/dist/assets/{mathjax-DVEo4UYj-20UgsMMl.js → mathjax-DVEo4UYj-DR6bsbF5.js} +1 -1
  83. package/dist/assets/{mermaid-parser.core-fkjeiYO7-QyTXgNIG.js → mermaid-parser.core-fkjeiYO7-UP10Y-9g.js} +2 -2
  84. package/dist/assets/{mermaid.core-CZDx2Cyn-D-r9MD5t.js → mermaid.core-CZDx2Cyn-DW_Cfwmb.js} +3 -3
  85. package/dist/assets/{mindmap-definition-YA3MSWOX-BpKAVyqs-ChxVu37V.js → mindmap-definition-YA3MSWOX-BpKAVyqs-BT2I6xrj.js} +1 -1
  86. package/dist/assets/{mo-CrlNnGXX-BXF4Cr9t.js → mo-CrlNnGXX-BbSsbziG.js} +1 -1
  87. package/dist/assets/packet-AYTQ26CC-MQQyx8fI-CgY-ZvU6.js +1 -0
  88. package/dist/assets/{pegDiagram-XKGWAZYB-C7eG_Ohl-W4Os6F4I.js → pegDiagram-XKGWAZYB-C7eG_Ohl-CwOHpMIE.js} +1 -1
  89. package/dist/assets/pie-WAS4IAKB-CyaL5i_d-Bfo-rUMV.js +1 -0
  90. package/dist/assets/{pieDiagram-E7YTZNPT-5f9wSfDR-BotXdxTh.js → pieDiagram-E7YTZNPT-5f9wSfDR-BfiHzTle.js} +1 -1
  91. package/dist/assets/{quadrantDiagram-AXDQQJYC-mWeaOj5S-cfsl-P0E.js → quadrantDiagram-AXDQQJYC-mWeaOj5S-DmKj1SlO.js} +1 -1
  92. package/dist/assets/radar-RG4KPBEZ-DR-SsHDJ-ciBy6b1Q.js +1 -0
  93. package/dist/assets/railroad-74A4TZTK-C2gqsOGX-CHNvkyIP.js +1 -0
  94. package/dist/assets/railroad-abnf-HS5TGJTU-Dpzw6P5u-BRKL6bPY.js +1 -0
  95. package/dist/assets/railroad-ebnf-LZEXJU2U-J_BuIrPo-CgDH6pLR.js +1 -0
  96. package/dist/assets/railroad-peg-WCYAUIDC-DzMStOR--CiY5naOb.js +1 -0
  97. package/dist/assets/{railroadDiagram-O6MQD6OU-DJskhE7u-DjP6Khna.js → railroadDiagram-O6MQD6OU-DJskhE7u-wtloi7bZ.js} +1 -1
  98. package/dist/assets/{requirementDiagram-BXWQKSXE-B5E0bU4h-DLRfaRNQ.js → requirementDiagram-BXWQKSXE-B5E0bU4h-FfOKAaTo.js} +1 -1
  99. package/dist/assets/{sankeyDiagram-P5KCCOFB-DS3AOTai-DcRRSBZW.js → sankeyDiagram-P5KCCOFB-DS3AOTai-Ct9wlCuh.js} +1 -1
  100. package/dist/assets/{sequenceDiagram-WJ2MYXX4-Bn2L9osv-DiKTI-JP.js → sequenceDiagram-WJ2MYXX4-Bn2L9osv-CYfRdJtF.js} +1 -1
  101. package/dist/assets/{src-CLC0ddSd-BAgR7bTG.js → src-CLC0ddSd-BIuq0-nf.js} +1 -1
  102. package/dist/assets/{stateDiagram-D77RDMKH-DbtoW7HL-CQtZxWRh.js → stateDiagram-D77RDMKH-DbtoW7HL-CDtw4W1S.js} +1 -1
  103. package/dist/assets/stateDiagram-v2-MP3YSRHH-CX6RalKY-hZ4fjyMZ.js +1 -0
  104. package/dist/assets/{svg-CaHU_sxA-C22w-_1l.js → svg-CaHU_sxA-CszaUDKG.js} +1 -1
  105. package/dist/assets/{swimlanes-42K2YHIH-90h8w_3B-CORPhAYt.js → swimlanes-42K2YHIH-90h8w_3B-CQvXtcT-.js} +1 -1
  106. package/dist/assets/swimlanesDiagram-VR7AAH4N-49XH_TMr-DzHdUIfM.js +8 -0
  107. package/dist/assets/{tex-B1cONMvb-CiAo3GBL.js → tex-B1cONMvb-ChHo1cBg.js} +1 -1
  108. package/dist/assets/{timeline-definition-24CTP7MA-Bx3KcICO-B1qH6cuQ.js → timeline-definition-24CTP7MA-Bx3KcICO-U9ql-I57.js} +1 -1
  109. package/dist/assets/treeView-Q6P3EWNA-gFs854Sn-Ce_qY2Wq.js +1 -0
  110. package/dist/assets/treemap-WGGIJYW6-DGh_szUA-CDNn6UCI.js +1 -0
  111. package/dist/assets/{vennDiagram-4TSXK5OY-DSexNe_H-SEUt9G7Z.js → vennDiagram-4TSXK5OY-DSexNe_H-BzdM6vG-.js} +1 -1
  112. package/dist/assets/wardley-WFR3VGLG-BVLYL3gB-CQexd8Yy.js +1 -0
  113. package/dist/assets/{wardleyDiagram-VM6X3IG4-DesYYyZW-B2VPVh08.js → wardleyDiagram-VM6X3IG4-DesYYyZW-CFU64ptS.js} +1 -1
  114. package/dist/assets/{xychartDiagram-S5SC5T6Z-dIt6pHWV-DSXBgmnQ.js → xychartDiagram-S5SC5T6Z-dIt6pHWV-Dp9Unxs1.js} +1 -1
  115. package/dist/index.html +2 -2
  116. package/package.json +1 -1
  117. package/server/backends/mulmoscript.ts +51 -9
  118. package/server/skills/mulmoterminal-keys/SKILL.md +12 -2
  119. package/dist/assets/architecture-7GRP2DOG-CRS69XmP-DCR_CQex.js +0 -1
  120. package/dist/assets/channel-BjESmYnj-BFLfK2V8.js +0 -1
  121. package/dist/assets/classDiagram-ZZMXUADV-C7gP-pUH-CH08mQjV.js +0 -1
  122. package/dist/assets/classDiagram-v2-VYDZK3BY-CUwV5k1F-CH08mQjV.js +0 -1
  123. package/dist/assets/cynefin-OW5HDTMX-C4iKivN4-BZcOh7ip.js +0 -1
  124. package/dist/assets/eventmodeling-NTZA5JFV-DhqtNS8R-BYT21Mst.js +0 -1
  125. package/dist/assets/flowDiagram-HODETNUW-CAW_8mwY-xa7OIyau.js +0 -1
  126. package/dist/assets/gitGraph-4MIJSDKK-S_Zs6Nxs-DZTFZUNY.js +0 -1
  127. package/dist/assets/index-O8auC4ls.css +0 -1
  128. package/dist/assets/info-A6RAGUB7-4sGJh1is-Dn_wrRwd.js +0 -1
  129. package/dist/assets/packet-AYTQ26CC-MQQyx8fI-DcIAZ4rr.js +0 -1
  130. package/dist/assets/pie-WAS4IAKB-CyaL5i_d-C7pXqqeF.js +0 -1
  131. package/dist/assets/radar-RG4KPBEZ-DR-SsHDJ-Bo1enSO-.js +0 -1
  132. package/dist/assets/railroad-74A4TZTK-C2gqsOGX-C9uY65xk.js +0 -1
  133. package/dist/assets/railroad-abnf-HS5TGJTU-Dpzw6P5u-vTPRJQDL.js +0 -1
  134. package/dist/assets/railroad-ebnf-LZEXJU2U-J_BuIrPo-CEO_EQLT.js +0 -1
  135. package/dist/assets/railroad-peg-WCYAUIDC-DzMStOR--DC7VemfO.js +0 -1
  136. package/dist/assets/stateDiagram-v2-MP3YSRHH-CX6RalKY-C8DhecgJ.js +0 -1
  137. package/dist/assets/swimlanesDiagram-VR7AAH4N-49XH_TMr-C2_Ucv4j.js +0 -8
  138. package/dist/assets/treeView-Q6P3EWNA-gFs854Sn-DmraGKxe.js +0 -1
  139. package/dist/assets/treemap-WGGIJYW6-DGh_szUA-BgIqgXYw.js +0 -1
  140. package/dist/assets/wardley-WFR3VGLG-BVLYL3gB-C8z5LWTl.js +0 -1
package/bin/cli-args.d.ts CHANGED
@@ -6,6 +6,8 @@ export declare function probeFailureIsPortInUse(err: { code?: string } | null |
6
6
  export declare function companionHostsFor(boundAddress: string): string[];
7
7
  export declare function launcherReachHost(bindHost: string): string | null;
8
8
  export declare function launcherUrl(reachHost: string, port: number): string;
9
+ export declare function browserUrl(port: number): string;
10
+ export declare function launchTarget(reachHost: string, port: number, localhostIsUnambiguous: boolean): { url: string; note: string | null };
9
11
  export declare function chooseCwd(args: string[], env: Record<string, string | undefined>): CwdChoice;
10
12
  export declare function portInUseMessage(port: number, explicit: boolean): string;
11
13
  export declare function portInUseAction(explicit: boolean, isTTY: boolean | undefined): "ask" | "stop";
package/bin/cli-args.js CHANGED
@@ -156,13 +156,13 @@ export function launcherReachHost(bindHost) {
156
156
  return isIP(bindHost) ? bindHost : null;
157
157
  }
158
158
 
159
- /** What to PRINT for a concrete address — never `localhost`.
159
+ /** What to name a concrete address with — never `localhost`, because this one exists to say
160
+ * WHICH ADDRESS WAS CHECKED. `launcherReachHost` picks `::1` precisely so an IPv4 listener
161
+ * cannot answer for us, and writing `localhost` would hand that choice straight back: measured,
162
+ * `localhost` resolves to BOTH `::1` and `127.0.0.1`.
160
163
  *
161
- * It did say `localhost` for the loopback cases, because that is friendlier. CodeRabbit caught
162
- * what that throws away: `launcherReachHost` picks `::1` precisely so an IPv4 listener cannot
163
- * answer for us, and printing `localhost` hands the choice straight back — measured, `localhost`
164
- * resolves to BOTH `::1` and `127.0.0.1`, so the browser may open the very process the poll was
165
- * written to avoid. A URL the user clicks has to name the address that was checked.
164
+ * This is no longer what the BROWSER is sent to — see `browserUrl`, and #1889 for why those are
165
+ * two different questions.
166
166
  *
167
167
  * Built through `URL` so the platform does the escaping: an IPv6 literal has to be bracketed or
168
168
  * `http://::1:34567` is not a URL at all. The brackets go on BEFORE the assignment because the
@@ -174,6 +174,82 @@ export function launcherUrl(reachHost, port) {
174
174
  return url.origin;
175
175
  }
176
176
 
177
+ /**
178
+ * The URL the BROWSER is opened at — always `localhost`, and that is a COMPATIBILITY constraint,
179
+ * not a preference about which spelling reads better.
180
+ *
181
+ * The grid layout (`grid_v2`), the theme, the font size and eleven other things live in
182
+ * `localStorage`, which is partitioned by ORIGIN. So this string is not merely an address the
183
+ * user is sent to; it is the key their state is filed under. Changing it silently empties the
184
+ * app: 4.11.0 moved it to `http://127.0.0.1:<port>` as part of #1876's review and every upgrading
185
+ * user's grid came up blank, with their sessions alive in tmux and nothing on screen to say the
186
+ * layout was one hostname away (#1889).
187
+ *
188
+ * And there is no way back for state left on the old origin. Measured on Chrome 152: a hidden
189
+ * iframe on the old origin reads an EMPTY store (third-party storage partitioning), and
190
+ * `document.requestStorageAccess()` resolving does not change that — it governs cookies. Only a
191
+ * TOP-LEVEL visit to the old origin can see it. A URL that has to be stable is therefore cheaper
192
+ * to keep stable than to migrate.
193
+ *
194
+ * Reachability is not the reason it is safe; #1834 is. `server/infra/loopback-listener.ts` makes
195
+ * this server serve `127.0.0.1` in EVERY configuration — as the primary bind, or as a second
196
+ * listener when the operator widens it — because eight local callers write that address as a
197
+ * literal. So `localhost` arrives here whatever `MULMOTERMINAL_HOST` says, falling back to v4
198
+ * when v6 refuses.
199
+ *
200
+ * What this deliberately does NOT do is decide anything about the port PROBE or the readiness
201
+ * POLL. Those still follow the address the child reports binding, which is what #1876 actually
202
+ * fixed — the second-instance guard, and a ready banner that was reporting a stranger's 200. The
203
+ * residual risk restored here is only the one 4.10.1 and every release before it carried: another
204
+ * APP holding this port on `::1` alone could answer a browser resolving `localhost` that way. A
205
+ * second MulmoTerminal cannot, because `companionHostsFor` requires `127.0.0.1` free for every
206
+ * bind before the child is ever spawned.
207
+ */
208
+ export function browserUrl(port) {
209
+ return new URL(`http://localhost:${port}`).origin;
210
+ }
211
+
212
+ /**
213
+ * Where to send the browser, and what to say about it — as one decision, because the two answers
214
+ * have to agree.
215
+ *
216
+ * `localhostIsUnambiguous` is a MEASUREMENT, not a guess: the launcher tried to bind `[::1]:port`
217
+ * before spawning, so `false` means something else already answers there. That is the hole Codex
218
+ * flagged on this change and it is real for the DEFAULT bind, not only a widened one —
219
+ * `companionHostsFor` requires `127.0.0.1` free for every launch but never asks about `::1`, and
220
+ * `localhost` resolves to BOTH (measured). Sending the browser to `localhost` while a stranger
221
+ * holds the v6 loopback is exactly the case #1876's review was worried about.
222
+ *
223
+ * So the rule is: use the name the user's state is filed under WHEN NOTHING ELSE CAN ANSWER TO
224
+ * IT, and otherwise say plainly that we stepped aside — including where their layout went, since
225
+ * a silent switch here is #1889 all over again for that one user.
226
+ *
227
+ * The probe cannot close the window, only narrow it: `::1` is free when asked, and unless the
228
+ * server binds it (a `::1` or `::` bind), a stranger could still take it afterwards. That is the
229
+ * same probe/bind race `isPortFree` already documents and accepts.
230
+ *
231
+ * The loopback spellings below are compared against the address the kernel REPORTED, whose output
232
+ * vocabulary is finite — not against anything a user typed.
233
+ */
234
+ export function launchTarget(reachHost, port, localhostIsUnambiguous) {
235
+ if (!localhostIsUnambiguous) {
236
+ const checked = launcherUrl(reachHost, port);
237
+ return {
238
+ url: checked,
239
+ note: `Something else is already listening on [::1]:${port}, so the browser is being sent to ${checked} — the address that was checked. If your layout and settings look empty, they are filed under http://localhost:${port}.`,
240
+ };
241
+ }
242
+ const bound = reachHost === V4_LOOPBACK_CLIENTS_DIAL || reachHost === "::1";
243
+ return {
244
+ url: browserUrl(port),
245
+ // `browserUrl` sends everyone to `localhost`, which is right for the machine the launcher runs
246
+ // on and useless to the operator who set `MULMOTERMINAL_HOST` so ANOTHER machine could open
247
+ // the app. 4.11.0 told them their address by putting it in the banner; saying it here means
248
+ // restoring the origin costs them nothing.
249
+ note: bound ? null : `Bound to ${reachHost} — reachable from another machine at ${launcherUrl(reachHost, port)}`,
250
+ };
251
+ }
252
+
177
253
  /**
178
254
  * Which directory to run in, before it is made absolute.
179
255
  * Precedence: `--cwd` (relative allowed) > CLAUDE_CWD > the directory the launcher was run
@@ -19,9 +19,9 @@ import { waitUntilReady } from "./wait-ready.js";
19
19
  import {
20
20
  bindHostFor,
21
21
  chooseCwd,
22
+ launchTarget,
22
23
  companionHostsFor,
23
24
  launcherReachHost,
24
- launcherUrl,
25
25
  probeFailureIsPortInUse,
26
26
  parsePortArg,
27
27
  portInUseAction,
@@ -295,6 +295,24 @@ async function isPortFree(port) {
295
295
  return primary;
296
296
  }
297
297
 
298
+ // Can `localhost` reach anything OTHER than the server this launcher is about to start? (#1889)
299
+ //
300
+ // The browser is sent to `localhost` so a user's saved layout stays where it has always been, and
301
+ // `localhost` resolves to BOTH `::1` and `127.0.0.1` (measured). `isPortFree` already proves
302
+ // `127.0.0.1` is free for every launch — `companionHostsFor` requires it — but nothing asks about
303
+ // `::1`, so a stranger holding the v6 loopback could answer a browser that resolves that way.
304
+ //
305
+ // Asked by BINDING rather than by classifying the host string, which is this file's rule
306
+ // throughout: free means nobody is there, and that is true whether the server is about to take
307
+ // `::1` itself (a `::1` or `::` bind) or leave it empty. Both are answers the browser can only
308
+ // reach us with. A machine with no IPv6 fails EADDRNOTAVAIL, which probeFailureIsPortInUse
309
+ // correctly reports as "not in use" — and there `localhost` is v4-only, so it is unambiguous.
310
+ //
311
+ // Must run BEFORE the server is spawned, or the server's own `::1` listener answers the probe.
312
+ async function localhostReachesOnlyUs(port) {
313
+ return (await canBind(port, "::1")).free;
314
+ }
315
+
298
316
  function printReadyBanner(url, stopCommand) {
299
317
  const bar = "\x1b[32m" + "─".repeat(48) + "\x1b[0m";
300
318
  console.log(`\n${bar}`);
@@ -367,7 +385,7 @@ async function confirmNoRunningInstance() {
367
385
  log(SECOND_INSTANCE_NOTE);
368
386
  }
369
387
 
370
- async function choosePort(requested, explicit) {
388
+ async function pickPort(requested, explicit) {
371
389
  const asked = await isPortFree(requested);
372
390
  if (asked.free) return { port: requested, address: asked.address };
373
391
  // No SILENT fallback: starting a second server on another port without saying so is how
@@ -386,12 +404,39 @@ async function choosePort(requested, explicit) {
386
404
  return fallback;
387
405
  }
388
406
 
407
+ // The port, the address a probe of it landed on, and whether `localhost` can reach anything but us
408
+ // on it — settled together because all three have to be known BEFORE the spawn, which is the only
409
+ // moment `::1` can be asked about honestly (see localhostReachesOnlyUs).
410
+ async function choosePort(requested, explicit) {
411
+ const chosen = await pickPort(requested, explicit);
412
+ return { ...chosen, localhostIsUnambiguous: await localhostReachesOnlyUs(chosen.port) };
413
+ }
414
+
415
+ // What the launcher does the moment the server answers: say where it is, and open it there.
416
+ //
417
+ // `url` is where the BROWSER goes and `note` is what launchTarget wants said about that choice —
418
+ // two different facts since #1889, and the note is null whenever the URL already covers it.
419
+ function announceReady(url, note, noOpen) {
420
+ printReadyBanner(url, STOP_COMMAND);
421
+ // Either the address a widened bind serves other machines on, or why the browser was NOT sent
422
+ // to `localhost`. Null whenever the URL above already said everything.
423
+ if (note) log(note);
424
+ if (noOpen) return;
425
+ try {
426
+ // The command is a hardcoded literal; url is built by browserUrl from a numeric port, so it
427
+ // is http://localhost:<n>.
428
+ execSync(`${pickOpenCommand()} ${url}`, { stdio: "pipe" });
429
+ } catch {
430
+ log(`Open your browser: ${url}`);
431
+ }
432
+ }
433
+
389
434
  // Spawn the server on `port` and report the child via `onChild` (so signal
390
435
  // handlers target the live process). Resolves only when the server exits because
391
436
  // the port was taken at bind time before it became ready — the caller then
392
437
  // reports that and stops. In every other case (clean shutdown, fatal error,
393
438
  // or the server simply running) the process exits with the server's code.
394
- function runServer(port, probedAddress, noOpen, cwd, onChild) {
439
+ function runServer(port, probedAddress, localhostIsUnambiguous, noOpen, cwd, onChild) {
395
440
  return new Promise((resolveExit) => {
396
441
  log(`Starting MulmoTerminal on port ${port}...`);
397
442
  // stderr is piped (and passed through) so a fatal boot error can be inspected once the
@@ -427,26 +472,17 @@ function runServer(port, probedAddress, noOpen, cwd, onChild) {
427
472
  let readyStarted = false;
428
473
  let cancelReady = () => {};
429
474
  // Takes a CONCRETE address — callers resolve first, and a caller that cannot does not call.
475
+ //
476
+ // Two different questions, and #1889 is what happens when one answer is given to both. The
477
+ // POLL asks "did the server we started come up", so it uses the address the child reported
478
+ // binding — that is #1876's fix and it stays. The URL asks "where does this user's browser
479
+ // keep its state", and the only answer that does not empty the app is the one it has always
480
+ // been given: see browserUrl.
430
481
  const beginReady = (reachHost) => {
431
482
  if (readyStarted) return;
432
483
  readyStarted = true;
433
- const url = launcherUrl(reachHost, port);
434
- cancelReady = waitUntilReady(
435
- port,
436
- () => {
437
- printReadyBanner(url, STOP_COMMAND);
438
- if (noOpen) return;
439
- try {
440
- // The command is a hardcoded literal; url is built by launcherUrl from the address the
441
- // child reported and a numeric port, so it is http://<addr>:<n> or http://[<v6>]:<n>.
442
-
443
- execSync(`${pickOpenCommand()} ${url}`, { stdio: "pipe" });
444
- } catch {
445
- log(`Open your browser: ${url}`);
446
- }
447
- },
448
- { host: reachHost },
449
- );
484
+ const { url, note } = launchTarget(reachHost, port, localhostIsUnambiguous);
485
+ cancelReady = waitUntilReady(port, () => announceReady(url, note, noOpen), { host: reachHost });
450
486
  };
451
487
  server.on("message", (msg) => {
452
488
  if (!isRecordLike(msg) || msg.type !== "listening" || typeof msg.address !== "string") return;
@@ -613,12 +649,12 @@ async function main() {
613
649
  // The address comes back with the port because the PROBE learned it from the kernel, which is
614
650
  // the one answer that needs no interpreting (#1876). It is the fallback the readiness poll uses
615
651
  // when the child reports nothing.
616
- const { port, address: probedAddress } = await choosePort(requestedPort, portExplicit);
652
+ const { port, address: probedAddress, localhostIsUnambiguous } = await choosePort(requestedPort, portExplicit);
617
653
  // Named only now, because the port is half the name — and named at all so that the user who
618
654
  // loses this terminal has something to search for (#1820). The server child names itself the
619
655
  // same, so `pkill mulmoterminal` reaches whichever half is found first.
620
656
  setProcessTitle(port);
621
- await runServer(port, probedAddress, noOpen, cwd, (c) => {
657
+ await runServer(port, probedAddress, localhostIsUnambiguous, noOpen, cwd, (c) => {
622
658
  child = c;
623
659
  });
624
660
  error(portInUseMessage(port, portExplicit));
package/common/keymap.ts CHANGED
@@ -18,6 +18,7 @@ export const KEYMAP_ACTIONS = [
18
18
  "zoom-prev",
19
19
  "next-attention",
20
20
  "terminal-new",
21
+ "terminal-new-here",
21
22
  "terminal-new-adjacent",
22
23
  "terminal-close",
23
24
  "copy",
@@ -78,7 +78,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
78
78
  id: "nvidia/nemotron-3-super-120b-a12b",
79
79
  label: "Nemotron 3 Super 120B",
80
80
  contextLength: 1_000_000,
81
- pricePerMTok: { input: 0.08, output: 0.45 },
81
+ pricePerMTok: { input: 0.085, output: 0.4 },
82
82
  trials: measured(3, 3, 18),
83
83
  },
84
84
  {
@@ -86,7 +86,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
86
86
  id: "qwen/qwen3-235b-a22b-2507",
87
87
  label: "Qwen3 235B A22B",
88
88
  contextLength: 262_144,
89
- pricePerMTok: { input: 0.09, output: 0.55 },
89
+ pricePerMTok: { input: 0.0875, output: 0.35 },
90
90
  trials: measured(3, 3, 16),
91
91
  },
92
92
  {
@@ -94,7 +94,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
94
94
  id: "minimax/minimax-m2.7",
95
95
  label: "MiniMax M2.7",
96
96
  contextLength: 204_800,
97
- pricePerMTok: { input: 0.25, output: 1.0 },
97
+ pricePerMTok: { input: 0.3, output: 1.2 },
98
98
  trials: measured(3, 3, 16),
99
99
  },
100
100
  {
@@ -102,7 +102,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
102
102
  id: "deepseek/deepseek-v3.2",
103
103
  label: "DeepSeek V3.2",
104
104
  contextLength: 163_840,
105
- pricePerMTok: { input: 0.269, output: 0.4 },
105
+ pricePerMTok: { input: 0.26, output: 0.38 },
106
106
  trials: measured(3, 3, 42),
107
107
  },
108
108
  {
@@ -118,7 +118,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
118
118
  id: "deepseek/deepseek-v4-pro",
119
119
  label: "DeepSeek V4 Pro",
120
120
  contextLength: 1_048_576,
121
- pricePerMTok: { input: 0.435, output: 0.87 },
121
+ pricePerMTok: { input: 0.87, output: 1.74 },
122
122
  trials: measured(3, 3, 20),
123
123
  },
124
124
  {
@@ -126,7 +126,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
126
126
  id: "deepseek/deepseek-v4-flash",
127
127
  label: "DeepSeek V4 Flash",
128
128
  contextLength: 1_048_576,
129
- pricePerMTok: { input: 0.094, output: 0.188 },
129
+ pricePerMTok: { input: 0.088606, output: 0.177212 },
130
130
  trials: measured(3, 4, 26),
131
131
  },
132
132
  {
@@ -158,7 +158,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
158
158
  id: "moonshotai/kimi-k2.6",
159
159
  label: "Kimi K2.6",
160
160
  contextLength: 262_144,
161
- pricePerMTok: { input: 0.684, output: 3.42 },
161
+ pricePerMTok: { input: 0.95, output: 4.0 },
162
162
  trials: measured(3, 3, 46),
163
163
  },
164
164
  {
@@ -166,7 +166,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
166
166
  id: "z-ai/glm-5.2",
167
167
  label: "GLM 5.2",
168
168
  contextLength: 1_048_576,
169
- pricePerMTok: { input: 0.819, output: 2.574 },
169
+ pricePerMTok: { input: 1.19, output: 3.74 },
170
170
  trials: measured(3, 3, 21),
171
171
  },
172
172
  {
@@ -174,7 +174,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
174
174
  id: "moonshotai/kimi-k2.7-code",
175
175
  label: "Kimi K2.7 Code",
176
176
  contextLength: 262_144,
177
- pricePerMTok: { input: 0.82, output: 3.75 },
177
+ pricePerMTok: { input: 0.66, output: 3.4 },
178
178
  trials: measured(3, 3, 14),
179
179
  },
180
180
  {
@@ -190,19 +190,35 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
190
190
  id: "tencent/hy3",
191
191
  label: "Tencent Hy3",
192
192
  contextLength: 262_144,
193
- pricePerMTok: { input: 0.14, output: 0.58 },
193
+ pricePerMTok: { input: 0.132, output: 0.528 },
194
194
  trials: measured(3, 3, 17),
195
195
  },
196
196
  {
197
197
  provider: "openrouter",
198
198
  id: "nvidia/nemotron-3-ultra-550b-a55b",
199
199
  label: "Nemotron 3 Ultra 550B",
200
- contextLength: 524_288, // 512 KiB = 512 * 1024; was 512_288 (a typo)
201
- pricePerMTok: { input: 0.6, output: 3.6 },
200
+ contextLength: 262_144,
201
+ pricePerMTok: { input: 0.5, output: 2.2 },
202
202
  trials: measured(3, 3, 13),
203
203
  },
204
204
 
205
205
  // ── Frontier vendors: cheapest first ─────────────────────────────────────────
206
+ {
207
+ provider: "openrouter",
208
+ id: "openai/gpt-5.6-luna",
209
+ label: "GPT-5.6 Luna",
210
+ contextLength: 1_050_000,
211
+ pricePerMTok: { input: 0.2, output: 1.2 },
212
+ trials: measured(3, 3, 27),
213
+ },
214
+ {
215
+ provider: "openrouter",
216
+ id: "openai/gpt-5.6-luna-pro",
217
+ label: "GPT-5.6 Luna Pro",
218
+ contextLength: 1_050_000,
219
+ pricePerMTok: { input: 0.2, output: 1.2 },
220
+ trials: measured(3, 3, 69),
221
+ },
206
222
  {
207
223
  provider: "openrouter",
208
224
  id: "google/gemini-3.5-flash-lite",
@@ -219,28 +235,12 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
219
235
  pricePerMTok: { input: 0.3, output: 2.5 },
220
236
  trials: measured(2, 3, 20),
221
237
  },
222
- {
223
- provider: "openrouter",
224
- id: "openai/gpt-5.6-luna",
225
- label: "GPT-5.6 Luna",
226
- contextLength: 1_050_000,
227
- pricePerMTok: { input: 1.0, output: 6.0 },
228
- trials: measured(3, 3, 27),
229
- },
230
- {
231
- provider: "openrouter",
232
- id: "openai/gpt-5.6-luna-pro",
233
- label: "GPT-5.6 Luna Pro",
234
- contextLength: 1_050_000,
235
- pricePerMTok: { input: 1.0, output: 6.0 },
236
- trials: measured(3, 3, 69),
237
- },
238
238
  {
239
239
  provider: "openrouter",
240
240
  id: "google/gemini-3.6-flash",
241
241
  label: "Gemini 3.6 Flash",
242
242
  contextLength: 1_048_576,
243
- pricePerMTok: { input: 1.5, output: 7.5 },
243
+ pricePerMTok: { input: 0.75, output: 3.75 },
244
244
  trials: measured(3, 3, 16),
245
245
  },
246
246
  {
@@ -256,7 +256,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
256
256
  id: "openai/gpt-5.6-terra-pro",
257
257
  label: "GPT-5.6 Terra Pro",
258
258
  contextLength: 1_050_000,
259
- pricePerMTok: { input: 2.5, output: 15.0 },
259
+ pricePerMTok: { input: 2.0, output: 12.0 },
260
260
  trials: measured(3, 3, 38),
261
261
  },
262
262
 
@@ -303,7 +303,7 @@ export const MODEL_PRESETS: readonly ModelPreset[] = [
303
303
  id: "mistralai/devstral-2512",
304
304
  label: "Devstral 2512",
305
305
  contextLength: 262_144,
306
- pricePerMTok: { input: 0.4, output: 2.0 },
306
+ pricePerMTok: { input: 0.44, output: 2.2 },
307
307
  trials: unreachable(ACCOUNT_PRIVACY),
308
308
  },
309
309
  ];