mulmoterminal 4.1.0 → 4.2.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 (232) hide show
  1. package/README.md +36 -19
  2. package/bin/mulmoterminal.js +9 -0
  3. package/common/byCodeUnit.ts +11 -0
  4. package/common/codeBlocks.ts +9 -5
  5. package/common/gitlabHosts.ts +60 -0
  6. package/common/isRecord.ts +6 -0
  7. package/common/isUnknownArray.ts +8 -0
  8. package/common/issueStartPlan.ts +18 -11
  9. package/common/keymap.ts +2 -0
  10. package/common/median.ts +7 -2
  11. package/common/prPhase.ts +21 -4
  12. package/common/readString.ts +28 -0
  13. package/common/repoEntry.ts +3 -2
  14. package/common/themeVars.ts +4 -2
  15. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-BCXxUk19.js → abnfDiagram-VRR7QNED-RjiYivmv-Ko4XgYrQ.js} +1 -1
  16. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-C_A3J6UT.js +1 -0
  17. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-Bs3iPPHw.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-DXXAwU3I.js} +1 -1
  18. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-CxIHRfjJ.js → blockDiagram-677ZJIJ3-BPuAJQRW-CKecg_L3.js} +1 -1
  19. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-D1Zp3ZLf.js → c4Diagram-LMCZKHZV-C0LAqQso-5vO8udpQ.js} +1 -1
  20. package/dist/assets/channel-7wUqSdoX-CYxVMjfv.js +1 -0
  21. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-iqbVrSxA.js → chunk-32BRIVSS-BRrYpgtb-G2dVX9R4.js} +1 -1
  22. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-DsGxy9sK.js → chunk-52WLFC77-BJ-ss3Xr-5jRfBwS-.js} +1 -1
  23. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-FHV3qMNI.js → chunk-C7G6YPKG-BZEucKEL-dH1xp00-.js} +1 -1
  24. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-D3JiHJ-E.js → chunk-EX3LRPZG-DLS6FBN1-Bm_0Dq7o.js} +1 -1
  25. package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-BLB-Jc4x.js → chunk-FWX5IMBZ-DHLSFw1H-DMe5J-nM.js} +2 -2
  26. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-B9Sp5Rm4.js → chunk-HOUHSVGY-Bhlt8hXJ-ByrE_Y0q.js} +1 -1
  27. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-Couvoogt.js → chunk-ICXQ74PX-DwgHBX_g-C7WGmPMk.js} +1 -1
  28. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-pTxGJtb7.js → chunk-MOJQB5TN-CMZRaeqt-BwgEBlFh.js} +1 -1
  29. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-Bjam2Bd1.js → chunk-OGEWGWER-8Qy4a8b5-DJR11LQX.js} +1 -1
  30. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-z_O9UUQX.js → chunk-PUDLZKDR-DcrWQRYh-CRXRg8rj.js} +1 -1
  31. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-yi9fnIVw.js → chunk-Q4XR5HBZ-ZXVGkG8Z-CuyNeF8W.js} +1 -1
  32. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-DTtWrQx1.js → chunk-V7JOEXUC-DmGdheTX-BXL1l6dM.js} +1 -1
  33. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-DW0Tgecw.js → chunk-VAUOI2AC-CS9QJ4yz-o20b35Qf.js} +1 -1
  34. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-BSK94OLt.js → chunk-VR4S4FIN-C6a91eNY-DcR5TXVW.js} +1 -1
  35. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-D5Vl4QYq.js → chunk-WYO6CB5R-BlzOfotS-CbdIi1Rx.js} +1 -1
  36. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-BnFKS269.js → chunk-ZGVPDNZ5-BYwxNFTK-DPJ63Ug-.js} +1 -1
  37. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-D9PYh-Ds.js +1 -0
  38. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-D9PYh-Ds.js +1 -0
  39. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-BOGZ0d_V.js +1 -0
  40. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--CaO8WZ9J.js → cynefinDiagram-TSTJHNR4-DHA9iPo--C-vvAO00.js} +1 -1
  41. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-i_xOpR1t.js → dagre-VKFMJZFB--oJKqXBZ-j_nnCAk3.js} +1 -1
  42. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-Dawy0bUD.js → diagram-FQU43EPY-CcJCB9bG-Bg8kALAQ.js} +1 -1
  43. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-DQMAjcpJ.js → diagram-G47NLZAW-C_o-WGG1-D8IYmNOo.js} +1 -1
  44. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--BdH6UDVY.js → diagram-NH7WQ7WH-CXJCYvY--ntIh1pVU.js} +1 -1
  45. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-DdlsuPDl.js → diagram-OA4YK3LP-BMzeJ87A-1rD8ncO1.js} +1 -1
  46. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-BYxqHeiT.js → diagram-WEI45ONY-D_93NKqo-DWStdu4t.js} +1 -1
  47. package/dist/assets/{dist-SjIG9fJN.js → dist-C1t_ZgKo.js} +1 -1
  48. package/dist/assets/{dist-Cl-hw6ph.js → dist-CpuWpSIw.js} +1 -1
  49. package/dist/assets/{dist-CCytdxgu.js → dist-QEbKPDTX.js} +1 -1
  50. package/dist/assets/{dist-D9fZbRmf.js → dist-w2lrwfZt.js} +1 -1
  51. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-CqbaCkfN.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-uJbtiAJU.js} +1 -1
  52. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-CFRhcxJL.js → erDiagram-Q63AITRT-CiABnA0s-C2GoR6NA.js} +1 -1
  53. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-iI1MrDgd.js +1 -0
  54. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-CrOlHoXQ.js +1 -0
  55. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-D_cPrHuB.js → ganttDiagram-NO4QXBWP-DQZvdFo1-BM70MIdN.js} +1 -1
  56. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DlP31iSf.js +1 -0
  57. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-CmpMFHzx.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-B_Pgy8IF.js} +1 -1
  58. package/dist/assets/index-C1urCATU.css +1 -0
  59. package/dist/assets/index-DIMX-Lgi.js +614 -0
  60. package/dist/assets/info-DKCQHKI2-Dplx5kMp-3WlBQbJQ.js +1 -0
  61. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-1qH_6MSO.js → infoDiagram-FWYZ7A6U-7UnoB5AP-Llrz1ikt.js} +1 -1
  62. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-C4Y9h3Kc.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-DkyrG0F4.js} +1 -1
  63. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-2CHdMO52.js → journeyDiagram-5HDEW3XC-c5xIah9o-C6nobV_J.js} +1 -1
  64. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-C3tvejl6.js → kanban-definition-HUTT4EX6-Cnt6loYD-r8kRECnn.js} +1 -1
  65. package/dist/assets/{lib-l0WFljNN.js → lib-CGMPNazU.js} +1 -1
  66. package/dist/assets/{line-D7ziSjKi-BgJrHM2o.js → line-D7ziSjKi-BGdO3L7C.js} +1 -1
  67. package/dist/assets/{marp-DXxN_4Os.js → marp-CYclI9Sp.js} +1 -1
  68. package/dist/assets/{mermaid-parser.core-DxEa8E3F-XDB5rg1G.js → mermaid-parser.core-DxEa8E3F-C7Z349Pq.js} +2 -2
  69. package/dist/assets/{mermaid.core-V0OYwIz3-BE18ndG4.js → mermaid.core-V0OYwIz3-BAg6sjQI.js} +3 -3
  70. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-B_Z8XGvs.js → mindmap-definition-LN4V7U3C-CexN3O6L-B4euhJCV.js} +1 -1
  71. package/dist/assets/packet-7NZHBO7P-CQI3flND-CQk0f108.js +1 -0
  72. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-CoS_Je3G.js → pegDiagram-2B236MQR-BiM4G0cW-Brrbufeb.js} +1 -1
  73. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-CpVaGdmz.js +1 -0
  74. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-q0d7psft.js → pieDiagram-ENE6RG2P-B_fBBS-2-DognIWKZ.js} +1 -1
  75. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-DX-o6YRc.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-BPBwmAdC.js} +1 -1
  76. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CKmLEdNO.js +1 -0
  77. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-oIAJF7wn.js +1 -0
  78. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-B4whEhXF.js +1 -0
  79. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-DPRdZSTX.js +1 -0
  80. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CbVKtvJy.js +1 -0
  81. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-wRRd9wDB.js → railroadDiagram-RFXS5EU6--lNIlG64-5B9Ve7w1.js} +1 -1
  82. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-C29F0qnM.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-iRCTL6WF.js} +1 -1
  83. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-BPq3vrC9.js → sankeyDiagram-HTMAVEWB-DlutaXDv-BWx6oil_.js} +1 -1
  84. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-Cq_jo_ss.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-Dyjw8haJ.js} +1 -1
  85. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-DKYpGC0n.js → stateDiagram-2N3HPSRC-jf_dXUEU-DNMm4FaT.js} +1 -1
  86. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-DW90BptC.js +1 -0
  87. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-jmF9axmm.js → swimlanes-5IMT3BWC-CYjtALQH-0aqXIsbo.js} +1 -1
  88. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-WTqIMZw2.js +8 -0
  89. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-B8bL7wcx.js → timeline-definition-FHXFAJF6-DYJ4oUm8-BEW3_bqU.js} +1 -1
  90. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-IkkpQPxV.js +1 -0
  91. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DgR55667.js +1 -0
  92. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-D9FWFG6j.js → vennDiagram-L72KCM5P-CdKAHoek-ftlvelkp.js} +1 -1
  93. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-C-7aQQUL.js +1 -0
  94. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-BffT3P-h.js → wardleyDiagram-EHGQE667-BF6c4_CW-ND9jAABP.js} +1 -1
  95. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-vPRabLJJ.js → xychartDiagram-FW5EYKEG-CGiKngj7-BbHq6IVI.js} +1 -1
  96. package/dist/index.html +2 -2
  97. package/package.json +15 -16
  98. package/server/agents/antigravity-session.ts +1 -1
  99. package/server/agents/antigravity-sessions.ts +4 -2
  100. package/server/agents/codex-activity.ts +1 -1
  101. package/server/agents/codex-rate-limits.spec.ts +1 -0
  102. package/server/agents/codex-rate-limits.ts +1 -1
  103. package/server/agents/codex-rollout.spec.ts +1 -0
  104. package/server/agents/codex-session.ts +4 -2
  105. package/server/agents/codex-sessions.ts +3 -2
  106. package/server/agents/probe-session.spec.ts +1 -0
  107. package/server/agents/probe-stall.spec.ts +104 -0
  108. package/server/agents/probe-stall.ts +70 -0
  109. package/server/agents/probe-transcript.spec.ts +11 -1
  110. package/server/agents/rate-limit-persist.spec.ts +1 -0
  111. package/server/agents/rate-limit-probe.spec.ts +91 -50
  112. package/server/agents/rate-limit-probe.ts +55 -33
  113. package/server/agents/rate-limit-routes.ts +13 -4
  114. package/server/agents/rate-limit-store.spec.ts +59 -4
  115. package/server/agents/rate-limit-store.ts +42 -5
  116. package/server/agents/statusline.spec.ts +1 -0
  117. package/server/backends/collections.ts +5 -3
  118. package/server/backends/html.ts +9 -2
  119. package/server/backends/mulmoscript.ts +10 -1
  120. package/server/backends/remoteHost/attachment-path.ts +1 -1
  121. package/server/backends/remoteHost/collectionPage.ts +8 -9
  122. package/server/backends/remoteHost/handlers/getCollection.ts +2 -1
  123. package/server/backends/remoteHost/handlers/getFeed.ts +2 -1
  124. package/server/backends/remoteHost/handlers/getRemoteView.ts +3 -2
  125. package/server/backends/remoteHost/handlers/getRemoteViewItems.ts +8 -8
  126. package/server/backends/remoteHost/handlers/issueWork.ts +6 -4
  127. package/server/backends/remoteHost/handlers/mutateRemoteView.ts +6 -4
  128. package/server/backends/remoteHost/handlers/terminalSession.ts +3 -2
  129. package/server/backends/remoteHost/issueWork.spec.ts +2 -1
  130. package/server/backends/remoteHost/jsonPayload.ts +55 -0
  131. package/server/backends/remoteHost/routes.spec.ts +1 -0
  132. package/server/backends/remoteHost/session.spec.ts +1 -0
  133. package/server/backends/remoteHost/skills.ts +1 -1
  134. package/server/backends/remoteHost/terminalInput.ts +1 -1
  135. package/server/backends/remoteHost/terminalScreen.ts +46 -2
  136. package/server/backends/remoteView.ts +2 -1
  137. package/server/backends/scheduler.ts +1 -0
  138. package/server/backends/translation.ts +7 -3
  139. package/server/backends/wiki.ts +3 -2
  140. package/server/config/app-config.ts +9 -0
  141. package/server/config/config-body.ts +1 -0
  142. package/server/config/config-routes.ts +16 -1
  143. package/server/config/dir-config.ts +1 -1
  144. package/server/config/header-title.ts +2 -2
  145. package/server/config/hue-rotate.ts +78 -0
  146. package/server/config/keymap-check.ts +2 -1
  147. package/server/config/workspace.ts +2 -1
  148. package/server/config/worktree-dir-config.ts +74 -0
  149. package/server/files/backup-store.ts +5 -3
  150. package/server/files/files-browse.ts +5 -3
  151. package/server/files/pathContainment.ts +1 -1
  152. package/server/files/renderedDoc.ts +1 -0
  153. package/server/files/scripts.ts +1 -0
  154. package/server/git/forge-host.ts +18 -3
  155. package/server/git/forge-support.ts +8 -7
  156. package/server/git/git-parse.ts +2 -2
  157. package/server/git/gitRemote.ts +1 -1
  158. package/server/git/glab-items.ts +104 -0
  159. package/server/git/glab.ts +114 -3
  160. package/server/git/issue-work.ts +5 -3
  161. package/server/git/issues.ts +2 -2
  162. package/server/git/prPhase.ts +61 -5
  163. package/server/git/prs.ts +5 -4
  164. package/server/git/remote-ref.ts +1 -1
  165. package/server/git/work-comment.ts +61 -18
  166. package/server/git/worktree-pr.ts +99 -19
  167. package/server/git/worktree-routes.ts +6 -7
  168. package/server/git/worktrees.ts +26 -1
  169. package/server/index.ts +22 -10
  170. package/server/infra/accounting-tool.ts +2 -2
  171. package/server/infra/collection-tool.ts +5 -1
  172. package/server/infra/pluginRuntime.ts +1 -1
  173. package/server/infra/plugins-registry.ts +83 -28
  174. package/server/infra/process-guards.ts +5 -2
  175. package/server/infra/pty-env.ts +1 -0
  176. package/server/infra/pubsub.ts +2 -2
  177. package/server/infra/server-tool-load.ts +13 -3
  178. package/server/infra/spawnCapture.ts +7 -3
  179. package/server/infra/tmux.ts +12 -8
  180. package/server/routes/gui-mcp-routes.ts +4 -2
  181. package/server/routes/hook-routes.ts +34 -24
  182. package/server/routes/issue-work-routes.ts +2 -1
  183. package/server/routes/mcp-routes.ts +2 -2
  184. package/server/routes/requestBody.ts +14 -0
  185. package/server/routes/session-routes.ts +14 -2
  186. package/server/routes/ws-routes.ts +57 -34
  187. package/server/session/cleared-transcripts.ts +1 -1
  188. package/server/session/command-summary.ts +1 -1
  189. package/server/session/decision-scan.ts +4 -1
  190. package/server/session/decisions.ts +1 -1
  191. package/server/session/draft-injection.ts +7 -30
  192. package/server/session/early-frames.ts +12 -8
  193. package/server/session/headlessScreen.ts +12 -6
  194. package/server/session/last-turn.ts +6 -3
  195. package/server/session/launcher-gui-mcp.ts +1 -1
  196. package/server/session/pty-connection.ts +17 -12
  197. package/server/session/pty-scan.ts +37 -2
  198. package/server/session/registry.ts +1 -1
  199. package/server/session/scheduled-sessions.ts +1 -1
  200. package/server/session/screen-rows.ts +4 -2
  201. package/server/session/session-reads.ts +3 -2
  202. package/server/session/shell-command.ts +1 -1
  203. package/server/session/tool-hook.ts +6 -3
  204. package/server/session/tool-store.ts +28 -9
  205. package/server/session/transcript.ts +16 -8
  206. package/server/session/translation-worker.ts +8 -4
  207. package/server/skills/mulmoterminal-bug-report/faq.md +16 -0
  208. package/server/skills/mulmoterminal-config/SKILL.md +23 -1
  209. package/server/skills/mulmoterminal-dirs/SKILL.md +17 -0
  210. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-vpkNSsXO.js +0 -1
  211. package/dist/assets/channel-7wUqSdoX-BmaXSumw.js +0 -1
  212. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-jw8dvfs2.js +0 -1
  213. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-jw8dvfs2.js +0 -1
  214. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-Bb2l8CKT.js +0 -1
  215. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-CV0AXWMH.js +0 -1
  216. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BzXDVc32.js +0 -1
  217. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DePJCG9u.js +0 -1
  218. package/dist/assets/index-CNOunTbV.js +0 -616
  219. package/dist/assets/index-Cdf9kaYt.css +0 -1
  220. package/dist/assets/info-DKCQHKI2-Dplx5kMp-CmgoBoly.js +0 -1
  221. package/dist/assets/packet-7NZHBO7P-CQI3flND-Ckhq3qGx.js +0 -1
  222. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-COGnIXXn.js +0 -1
  223. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-DYVN7hVv.js +0 -1
  224. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-Ccr316bA.js +0 -1
  225. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-1lHKiwjs.js +0 -1
  226. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-CDsiITOB.js +0 -1
  227. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-BVRC2yH2.js +0 -1
  228. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CC7z1AQz.js +0 -1
  229. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-i5S7soVG.js +0 -8
  230. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DnDVKvX3.js +0 -1
  231. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-Dkb6cQ10.js +0 -1
  232. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-Za38PV_G.js +0 -1
package/README.md CHANGED
@@ -39,7 +39,7 @@ npx mulmoterminal@latest # starts on http://localhost:34567 and opens you
39
39
 
40
40
  ![MulmoTerminal's grid view — four live Claude sessions running side by side, each in its own color-coded project](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/grid-2x2-live.png)
41
41
 
42
- *The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working / done (blue), needs-you (amber — e.g. waiting on a permission), idle — with an attention chime so a stuck cell off-screen still pulls you back. Supervise many; only step in where you're called.*
42
+ *The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working (blue), done (green), needs-you (amber — e.g. waiting on a permission), idle — with an attention chime so a stuck cell off-screen still pulls you back. Supervise many; only step in where you're called.*
43
43
 
44
44
  ## Why you'll want it
45
45
 
@@ -88,9 +88,9 @@ question — an amber dot; nothing proceeds until you answer) and which are **fi
88
88
  you haven't seen** (a green dot) — driven by Claude/Codex activity hooks the server injects per
89
89
  spawn. The horizontal tab bar carries the same two dots.
90
90
 
91
- ![One agent enlarged, with the cockpit roster beside it](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/cockpit-roster.png)
91
+ ![One agent zoomed, with the GUI panel beside it](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/zoom-canvas.png)
92
92
 
93
- *To focus on one agent, **zoom its cell**: it takes the window, and a pane opens beside it — the **cockpit roster** above, or the **GUI panel** ("Canvas"), where that agent's tool calls render as documents, forms, charts, images, and HTML rather than printed text. **The app opens on the grid** (`/`, settling on `/terminals`), which is the only view; 3.x had a separate single view at `/chat` and 4.0.0 removed it, so that URL now lands on the grid like any other.*
93
+ *To focus on one agent, **zoom its cell**: it takes the window, and the **GUI panel** ("Canvas") opens beside it, where that agent's tool calls render as documents, forms, charts, images, and HTML rather than printed text. **The app opens on the grid** (`/`, settling on `/terminals`), which is the only view; 3.x had a separate single view at `/chat` and 4.0.0 removed it, so that URL now lands on the grid like any other.*
94
94
 
95
95
  **Inserting a file path** — like a native terminal, you can put a file's absolute path into
96
96
  the prompt: **drag a file** onto the terminal, or click the **file button** in the terminal
@@ -204,11 +204,17 @@ more.
204
204
 
205
205
  Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
206
206
 
207
+ > **Never installed any of this before?** The guide walks it end to end, macOS and Windows,
208
+ > assuming no command-line experience:
209
+ > [Getting started](https://receptron.github.io/mulmoterminal/guide/en/getting-started.html) ·
210
+ > [はじめに — 起動するまで](https://receptron.github.io/mulmoterminal/guide/ja/getting-started.html)
211
+
207
212
  | | Tool | What it gives you | Install |
208
213
  | --- | --- | --- | --- |
209
214
  | **Required** | [`claude`](https://claude.com/claude-code) | every Claude session — this app is a cockpit for it | `npm i -g @anthropic-ai/claude-code`, then run `claude` once to log in |
210
215
  | **Required** | `git` | [worktree isolation](#git-worktrees--pull-requests), each cell's branch / unsaved-dot / diff readout, the PR footer | `brew install git` · `sudo apt install git` · `sudo dnf install git` · Windows: [git-scm.com](https://git-scm.com/download/win) |
211
216
  | **Required** | `gh` | the cross-repo **PRs & Issues** view and one-click PR creation — it uses your `gh` login, so no token is stored | [cli.github.com](https://cli.github.com), then `gh auth login` |
217
+ | Optional | `glab` | the same for **GitLab** projects (#981) — gitlab.com, and a self-hosted instance you declare in `gitlabHosts` (#1332). Same arrangement: the CLI holds the credentials, this app stores no token | `brew install glab`, then `glab auth login` (self-hosted: `glab auth login --hostname gitlab.example.com`) |
212
218
  | Recommended | `tmux` | [session persistence](#session-persistence-tmux) — terminals survive a server restart | `brew install tmux` · `sudo apt install tmux` · `sudo dnf install tmux` · no native Windows build (falls back to plain PTYs) |
213
219
  | Optional | `codex` | [Codex sessions](#agents-claude--codex) in a cell, alongside Claude | `npm i -g @openai/codex` |
214
220
  | Optional | `ffmpeg` | video rendering from the [mulmo-script panel](#wiki-collections--the-gui-panel) (its plugin ships enabled) | `brew install ffmpeg` · `sudo apt install ffmpeg` · `sudo dnf install ffmpeg` |
@@ -536,7 +542,8 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
536
542
  | `soundFile` | Absolute path to a custom **attention sound**, the fallback for every kind. Empty/unset uses the built-in synthesized chime. |
537
543
  | `soundKinds` | Which moments beep — see [Notification sounds](#notification-sounds). Defaults to `["finished","waiting"]`; the other kinds are opt-in. |
538
544
  | `sounds` | Per-kind sound: `{ "waiting": "preset:coin" }`. A `preset:<id>` reference or an absolute path; a kind with no entry falls back to `soundFile`. |
539
- | `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates (via your `gh` login). An entry may name its host (`gitlab.com/group/project`); only github.com is read today, and other hosts show a row saying so. |
545
+ | `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates, using whichever CLI the host needs — your own `gh` or `glab` login, so no token is stored here. An entry may name its host `gitlab.com/group/project` is read with `glab`, and work can be started on it, commented on and turned into a merge request. A host that is neither shows a row saying so. |
546
+ | `gitlabHosts` | Hosts that run a **self-hosted GitLab**, e.g. `["gitlab.example.com"]`. Nothing in a URL says which forge a host runs, so declaring it is what lets `prRepos` entries on that host be read with `glab` — everything gitlab.com can do, it can do. Needs `glab auth login --hostname <host>`. config.json only (no Settings control), so a hand edit takes effect on the next server start. |
540
547
  | `repoDirs` | `{ "owner/repo": "/abs/path" }` — which local clone work on a repo starts in, when you keep several side by side. Only the *choice* is stored; which clones exist is re-derived from `cwdPresets` on every read, and an entry that no longer names a clone of that repo is ignored. |
541
548
  | `launchers` | `{ label, command }` entries offered in a grid cell's launcher besides the agents — any interactive command. A plain shell needs no entry: the launch form's **Shell** toggle opens `$SHELL` unconfigured. |
542
549
  | `quickCommands` | `{ label, text, agents? }` phrases the **phone** offers as chips on a session's terminal view. Tapping one puts `text` in the input box; it is not sent until you press send. `agents` (`"claude"` / `"codex"` / `"shell"`) scopes a chip to session kinds — omit it to offer the chip everywhere. Empty by default. |
@@ -734,22 +741,21 @@ yarn dev # backend (:34567) + Vite UI (:6856), concurrently — o
734
741
  yarn dev:server # backend only (node --import tsx --env-file-if-exists=.env server/index.ts)
735
742
  yarn dev:client # Vite dev server only
736
743
 
737
- yarn build # type-check (vue-tsc) + vite build -> dist/
738
- yarn typecheck:server # type-check the server (tsconfig.server.json)
739
- yarn typecheck:test # type-check the specs (tsconfig.test*.json)
744
+ yarn typecheck # type-check everything (vue-tsc -b)
745
+ yarn build # type-check + vite build -> dist/
740
746
  yarn server # run backend; serves dist/ + the APIs on :34567
741
747
  yarn test # vitest run
742
748
  ```
743
749
 
744
- The backend is TypeScript run directly via `tsx` (no build step); `server/` is
745
- type-checked separately through `tsconfig.server.json` (`strict`), kept out of
746
- the main `build` so the two type-check independently.
747
-
748
- Specs sit outside both of those projects, and vitest strips types rather than
749
- checking them so `yarn typecheck:test` is what keeps them honest. It mirrors
750
- the same split: `tsconfig.test.json` (client specs, DOM + `.vue`) and
751
- `tsconfig.test-server.json` (server specs, node). CI runs it alongside the
752
- other two.
750
+ `yarn typecheck` covers the whole repo. The root `tsconfig.json` is a solution
751
+ file that references all five projects, so one `vue-tsc -b` builds them:
752
+ `tsconfig.app.json` (client), `tsconfig.node.json` (vite config),
753
+ `tsconfig.server.json` (backend, run directly via `tsx` with no build step),
754
+ plus `tsconfig.test.json` and `tsconfig.test-server.json` for the specs which
755
+ need checking of their own because vitest strips types rather than checking
756
+ them. They exist as separate projects because each has its own compiler options
757
+ (the client ones DOM + `.vue`, the server ones node, the specs with
758
+ `noUncheckedIndexedAccess` off).
753
759
 
754
760
  In dev, open the Vite URL; its proxy forwards `/ws`, `/ws/pubsub`, and `/api` to
755
761
  `:34567`. In production, run `yarn build` then `yarn server` and open
@@ -914,6 +920,17 @@ separate working tree that shares the repo's `.git`, so several agents can work
914
920
  repo without colliding. Worktrees live under `~/.mulmoterminal/worktrees/` (override with
915
921
  `MULMOTERMINAL_HOME`), and existing ones are listed below the field.
916
922
 
923
+ **A worktree inherits the project's settings.** `.mulmoterminal.json` is normally gitignored,
924
+ so a fresh worktree used to have none — no colours, no name, no model, no grid rank. It is now
925
+ given its own copy derived from the project's: `name` / `theme` / `colors` / `fontSize` /
926
+ `fontFamily` / `provider` / `model` as written, the seven chrome colours **rotated 12 degrees
927
+ further around the hue wheel per worktree** (so a project's trees read as a gradient; a grey like
928
+ `#ffffff` has no hue to move and stays put), and `orderPriority` at the project's rank **+ 1**, so
929
+ the worktree sorts directly after it. `sound` / `sounds` / `addDirs` are not carried — they name
930
+ paths inside the project directory. Written only where git would **ignore** it: an untracked file
931
+ in a worktree's `git status` would make it count as dirty, and a dirty worktree is one
932
+ MulmoTerminal refuses to remove. An existing config in the worktree is never overwritten.
933
+
917
934
  **One worktree, one session.** A worktree is tied to a branch, so it is never started
918
935
  twice: a listed row **resumes** that worktree's session when it has one, and **starts** one
919
936
  only when it has none. A row whose session is open in another terminal reads `in use` and
@@ -962,7 +979,7 @@ Closing a worktree cell asks whether to **keep** the worktree or **discard & rem
962
979
 
963
980
  **PRs & Issues (cross-repo).** The toolbar's **Pull requests** button opens a full-screen
964
981
  view that aggregates open PRs **and** issues across the repos listed in Settings →
965
- **Pull request repos** (`prRepos`, `owner/repo` entries) via your server-side `gh` login.
982
+ **Pull request repos** (`prRepos`, `owner/repo` entries, or `gitlab.com/group/project` — plus any host declared in `gitlabHosts`) via your server-side `gh` / `glab` login.
966
983
  PRs show a CI-rollup / review-decision / draft badge; each repo lists its latest open
967
984
  issues. Rows are real links, per-repo errors don't sink the view, and the two lists load
968
985
  independently. Backed by `GET /api/prs` and `GET /api/issues`.
@@ -1303,7 +1320,7 @@ same-origin-guarded.
1303
1320
  | `POST /api/git-remote` | The dir's GitHub repo URL (for the header GitHub menu). |
1304
1321
  | `GET /api/worktrees?cwd=` · `GET /api/worktrees/diff?cwd=` | List managed worktrees / diff one vs its base. |
1305
1322
  | `POST /api/worktrees/create` · `/remove` · `/push` · `/pr` | Create on `agent/<slug>` — or, with `issue: <N>`, on `issue/<N>-<slug>` forked from a freshly fetched `origin/<base>`; remove (managed root only), push, open a PR (`gh`, else compare URL). |
1306
- | `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` (via `gh`). |
1323
+ | `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` `gh` for github.com entries, `glab` for gitlab.com and any host declared in `gitlabHosts`. |
1307
1324
  | `GET /api/repo-dirs` | Which saved directories clone which GitHub repo, ordered, with the recorded choice per repo. |
1308
1325
  | `POST /api/issues/start` | Cut an issue's worktree in one of that repo's known clones and spawn a session there, seeded with the issue as a draft. |
1309
1326
  | `GET /api/github/star` · `POST /api/github/star` | Whether you have starred MulmoTerminal, and star it (via `gh`). `starred: null` means `gh` could not answer, and hides the button. |
@@ -1652,7 +1669,7 @@ server/
1652
1669
  session/ per-session transcript/activity/cost: transcript.ts,
1653
1670
  session-resolve.ts, activity-*.ts, cost.ts,
1654
1671
  command-summary.ts, terminal-replay.ts, file-cache.ts
1655
- git/ git & GitHub (via gh) + worktrees: git-status.ts, gitRemote.ts,
1672
+ git/ git, GitHub (gh) and GitLab (glab) + worktrees: git-status.ts, gitRemote.ts,
1656
1673
  gh.ts, prs.ts, issues.ts, pr-for-branch.ts, worktrees.ts, worktree-*.ts
1657
1674
  files/ files-browse.ts (contained tree read/write), pick-file.ts,
1658
1675
  open-dir.ts, scripts.ts (Run-menu script.json loader)
@@ -85,6 +85,15 @@ function claudeInstalled() {
85
85
  const PATH_TOOLS = [
86
86
  { cmd: "git", versionArg: "--version", required: true, why: "worktrees, per-cell branch/diff, PR footer", hint: "brew install git · apt install git" },
87
87
  { cmd: "gh", versionArg: "--version", required: true, why: "PRs & Issues view + one-click PRs", hint: "https://cli.github.com (then: gh auth login)" },
88
+ // Optional rather than a second required tool: github.com is what the app is built around, and a
89
+ // user with no GitLab project should not be told they are missing something (#981).
90
+ {
91
+ cmd: "glab",
92
+ versionArg: "--version",
93
+ required: false,
94
+ why: "the PRs & Issues view for gitlab.com projects",
95
+ hint: "brew install glab (then: glab auth login)",
96
+ },
88
97
  { cmd: "tmux", versionArg: "-V", required: false, why: "sessions survive a restart", hint: "brew install tmux · apt install tmux" },
89
98
  { cmd: "codex", versionArg: "--version", required: false, why: "run OpenAI Codex as an agent", hint: "npm install -g @openai/codex" },
90
99
  {
@@ -0,0 +1,11 @@
1
+ // Sort filenames and directory names deterministically.
2
+ //
3
+ // `.sort()` with no comparator is flagged (sonarjs/no-alphabetical-sort) because the default is
4
+ // implementation-defined for non-ASCII. The rule suggests `localeCompare`, which would be WRONG
5
+ // here: these are filenames — zero-padded date directories and ISO-stamped rollout files — and
6
+ // ordering them by the user's locale makes the answer depend on who is running the app. Code-unit
7
+ // order is what the callers actually mean, and it is the same everywhere.
8
+ export function byCodeUnit(a: string, b: string): number {
9
+ if (a < b) return -1;
10
+ return a > b ? 1 : 0;
11
+ }
@@ -59,8 +59,12 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
59
59
  const lines = markdown.split("\n");
60
60
  const blocks: FencedBlock[] = [];
61
61
  let i = 0;
62
+ // Each line is read into a const before use: with noUncheckedIndexedAccess, `lines[i]` is
63
+ // `string | undefined` even inside `i < lines.length`, and the loop condition is what actually
64
+ // rules the undefined out.
62
65
  while (i < lines.length) {
63
- const open = fenceOf(lines[i]);
66
+ const line = lines[i];
67
+ const open = line === undefined ? null : fenceOf(line);
64
68
  if (!open) {
65
69
  i++;
66
70
  continue;
@@ -69,12 +73,12 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
69
73
  const info = open.info.trim();
70
74
  const body: string[] = [];
71
75
  i++;
72
- while (i < lines.length && !closes(lines[i], fence)) {
73
- body.push(lines[i]);
76
+ for (let next = lines[i]; next !== undefined && !closes(next, fence); next = lines[i]) {
77
+ body.push(next);
74
78
  i++;
75
79
  }
76
80
  i++; // step over the closing fence (or past the end, for an unclosed block)
77
- blocks.push({ lang: info ? (info.split(/\s+/)[0].toLowerCase() ?? null) : null, body: body.join("\n") });
81
+ blocks.push({ lang: info ? (info.split(/\s+/)[0]?.toLowerCase() ?? null) : null, body: body.join("\n") });
78
82
  }
79
83
  return blocks;
80
84
  }
@@ -84,5 +88,5 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
84
88
  * exactly like a copy that silently failed. */
85
89
  export function lastFencedBlock(markdown: string): FencedBlock | null {
86
90
  const withBody = fencedBlocks(markdown).filter((b) => b.body.trim() !== "");
87
- return withBody.length ? withBody[withBody.length - 1] : null;
91
+ return withBody[withBody.length - 1] ?? null;
88
92
  }
@@ -0,0 +1,60 @@
1
+ // Which hosts are a GitLab this app can talk to (#1332). `gitlab.com` is known from its name; a
2
+ // self-hosted instance at `gitlab.hogefuga.com` is indistinguishable from any other host, so the
3
+ // user DECLARES it in `gitlabHosts` — the config mechanism #981 said this would need.
4
+ //
5
+ // In `common/` because both sides decide from it: the server picks a CLI from it, and the browser
6
+ // says why a control is off. Two copies would drift, and disagreeing is the failure the forge
7
+ // abstraction exists to undo.
8
+ import { GITHUB_HOST, GITLAB_HOST } from "./repoEntry.js";
9
+
10
+ // A hostname, lower case, with at least one dot. The dot is not cosmetic: `parseRepoEntry` reads a
11
+ // leading segment as a host ONLY when it contains one, so a dotless declaration could never match
12
+ // an entry and would sit in the config doing nothing.
13
+ const HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
14
+
15
+ const SCHEME_RE = /^https?:\/\//;
16
+
17
+ const GITLAB_HOSTS_MAX = 20;
18
+
19
+ /** One declared host, or null when the string does not name one.
20
+ *
21
+ * A pasted `https://gitlab.example.com/` is accepted and reduced to its hostname: this value is
22
+ * hand-typed into config.json with no UI to validate it, and the browser's address bar is where
23
+ * the user is copying from. Anything else with a slash is rejected rather than truncated — a
24
+ * project path here is a different mistake, and silently keeping its host would hide it.
25
+ */
26
+ export function normalizeGitlabHost(input: unknown): string | null {
27
+ if (typeof input !== "string") return null;
28
+ const host = input.trim().toLowerCase().replace(SCHEME_RE, "").replace(/\/$/, "");
29
+ return HOSTNAME_RE.test(host) ? host : null;
30
+ }
31
+
32
+ /** The declared hosts, de-duplicated and capped. Anything unusable is dropped rather than failing
33
+ * the whole config — one typo must not cost the user the hosts they spelled correctly. */
34
+ export function sanitizeGitlabHosts(input: unknown): string[] {
35
+ if (!Array.isArray(input)) return [];
36
+ const hosts = new Set<string>();
37
+ for (const value of input) {
38
+ const host = normalizeGitlabHost(value);
39
+ if (host) hosts.add(host);
40
+ if (hosts.size >= GITLAB_HOSTS_MAX) break;
41
+ }
42
+ return [...hosts];
43
+ }
44
+
45
+ /** Whether `host` is a GitLab: gitlab.com, or one the user declared.
46
+ *
47
+ * `github.com` is refused whatever the config says. A declaration is a hand-typed line with no UI
48
+ * behind it, and the one that would do real damage — every GitHub repo suddenly addressed with
49
+ * `glab` — is also the easiest to write by accident.
50
+ */
51
+ export const isGitlabHost = (host: string, declared: readonly string[]): boolean => host === GITLAB_HOST || (host !== GITHUB_HOST && declared.includes(host));
52
+
53
+ /** Why a host is not handled, and what to do about it.
54
+ *
55
+ * ONE sentence, shared by the PRs/Issues row and the start-work refusal: #1332 was filed by a user
56
+ * who read "is not supported yet" and could not tell whether their setup was wrong, whether the
57
+ * feature was coming, or what to try — so the reason has to carry the fix.
58
+ */
59
+ export const unknownForgeReason = (host: string): string =>
60
+ `${host} is not supported yet — MulmoTerminal reads github.com and gitlab.com; if ${host} is a self-hosted GitLab, add it to "gitlabHosts" in ~/.mulmoterminal/config.json and restart`;
@@ -11,3 +11,9 @@ import { isObject } from "graphai";
11
11
  // graphai's own `isPlainObject` is exactly this (it also rejects Map/Date/class instances), but
12
12
  // it is not exported from the package root, so the array check stays here.
13
13
  export const isRecord = (value: unknown): value is Record<string, unknown> => isObject(value) && !Array.isArray(value);
14
+
15
+ // "Absent, or of this type" — what an OPTIONAL field on a wire shape has to satisfy. A guard that
16
+ // skips these asserts a type it has not seen: absent is meaningful (an older server never said the
17
+ // field), but a present one of the wrong type is a lie the rest of the code then reads.
18
+ export const optionalString = (value: unknown): boolean => value === undefined || typeof value === "string";
19
+ export const optionalBoolean = (value: unknown): boolean => value === undefined || typeof value === "boolean";
@@ -0,0 +1,8 @@
1
+ // The array counterpart to isRecord, and it exists for a reason that is easy to miss:
2
+ // `Array.isArray(value)` on an `unknown` narrows it to **`any[]`**, not `unknown[]`. So the
3
+ // obvious `Array.isArray(x) ? x : []` hands back an array whose elements are outside the type
4
+ // checker entirely, and every read off them type-checks against anything.
5
+ //
6
+ // Declaring the guard's result as `unknown[]` keeps the elements unverified-but-visible, which is
7
+ // what the callers want: they check each element before using it.
8
+ export const isUnknownArray = (value: unknown): value is unknown[] => Array.isArray(value);
@@ -8,15 +8,17 @@
8
8
  // docs/remote-host-protocol.md), so it refuses `choose` where the desktop opens it.
9
9
  import type { RepoDirs } from "./repoDirs";
10
10
  import { GITHUB_HOST, GITLAB_HOST, parseRepoEntry } from "./repoEntry";
11
+ import { isGitlabHost, unknownForgeReason } from "./gitlabHosts";
11
12
 
12
13
  // Where work can be STARTED — the issue is read and a worktree cut. A host that is merely listable
13
- // still refuses, and says which host it is rather than blaming a missing clone (#981).
14
- const STARTABLE_HOSTS: ReadonlySet<string> = new Set([GITHUB_HOST, GITLAB_HOST]);
14
+ // still refuses, and says which host it is rather than blaming a missing clone (#981). A host the
15
+ // user declared as self-hosted GitLab starts work like gitlab.com does (#1332).
16
+ const isStartableHost = (host: string, gitlabHosts: readonly string[]): boolean => host === GITHUB_HOST || isGitlabHost(host, gitlabHosts);
15
17
 
16
- /** The hosts named in a refusal, read off the set rather than written out beside it — a hardcoded
17
- * "github.com only" survived GitLab becoming startable and told users the opposite of the truth
18
- * (Codex review). Exported so the phone's refusal says the same thing. */
19
- export const startableHosts = (): string => [...STARTABLE_HOSTS].join(" and ");
18
+ /** The hosts named in a refusal, built from the same rule rather than written out beside it — a
19
+ * hardcoded "github.com only" survived GitLab becoming startable and told users the opposite of
20
+ * the truth (Codex review). Exported so the phone's refusal says the same thing. */
21
+ export const startableHosts = (gitlabHosts: readonly string[]): string => [...new Set([GITHUB_HOST, GITLAB_HOST, ...gitlabHosts])].join(" and ");
20
22
 
21
23
  export type IssueStartPlan =
22
24
  /** No clone of this repo on this machine, so there is nowhere for the work to happen. */
@@ -35,24 +37,29 @@ export type BlockedIssueStartPlan = Exclude<IssueStartPlan, { kind: "ready" }>;
35
37
 
36
38
  // `repo` is REQUIRED, not optional, even though only one branch reads it: a caller that omitted it
37
39
  // would get `no-clone` for a GitLab repo, which is the exact wrong answer this parameter was added
38
- // to fix. Making the compiler ask for it is what stops that coming back.
39
- export function issueStartPlan(entry: RepoDirs | undefined, repo: string): IssueStartPlan {
40
+ // to fix. Making the compiler ask for it is what stops that coming back. `gitlabHosts` is required
41
+ // for the same reason — defaulted to empty, a caller that forgot it would refuse the user's own
42
+ // self-hosted GitLab and blame the host (#1332).
43
+ export function issueStartPlan(entry: RepoDirs | undefined, repo: string, gitlabHosts: readonly string[]): IssueStartPlan {
40
44
  // Checked before the clone list, because a GitLab repo has no entry there and would otherwise
41
45
  // read as "no clone" — a reason that is not true and points at the wrong fix.
42
46
  const parsed = parseRepoEntry(repo);
43
- if (parsed && !STARTABLE_HOSTS.has(parsed.host)) return { kind: "unsupported-forge", host: parsed.host };
47
+ if (parsed && !isStartableHost(parsed.host, gitlabHosts)) return { kind: "unsupported-forge", host: parsed.host };
44
48
  const dirs = entry?.dirs ?? [];
45
49
  if (dirs.length === 0) return { kind: "no-clone" };
46
50
  // A recording wins even when the repo has one clone: it is still the same answer, and treating
47
51
  // the two cases the same keeps "recorded" from being a state the UI has to distinguish.
48
52
  const recorded = entry?.primary;
49
53
  if (recorded) return { kind: "ready", dir: recorded };
50
- if (dirs.length === 1) return { kind: "ready", dir: dirs[0].path };
54
+ const only = dirs.length === 1 ? dirs[0] : undefined;
55
+ if (only) return { kind: "ready", dir: only.path };
51
56
  return { kind: "choose", dirs };
52
57
  }
53
58
 
54
59
  /** Why the control is disabled, for the row's title text. Null when it is not disabled. */
55
60
  export function issueStartBlockedReason(plan: IssueStartPlan, repo: string): string | null {
56
- if (plan.kind === "unsupported-forge") return `Starting work supports ${startableHosts()} ${plan.host} issues are listed here, not started from`;
61
+ // The same sentence the row's own error carries: the host is not one this app knows, and the fix
62
+ // is a config line rather than anything about this issue or this clone (#1332).
63
+ if (plan.kind === "unsupported-forge") return unknownForgeReason(plan.host);
57
64
  return plan.kind === "no-clone" ? `No local clone of ${repo} — add one to your directory presets to start work here` : null;
58
65
  }
package/common/keymap.ts CHANGED
@@ -87,6 +87,7 @@ export function parseKeyBinding(input: string): KeyBinding | null {
87
87
  const parts = input.split("+").map((part) => part.trim());
88
88
  if (parts.length === 0 || parts.some((part) => part === "")) return null;
89
89
  const key = parts[parts.length - 1];
90
+ if (key === undefined) return null; // unreachable: parts.length was checked above
90
91
  const binding: KeyBinding = { key, shift: false, alt: false, ctrl: false, meta: false };
91
92
  for (const part of parts.slice(0, -1)) {
92
93
  const flag = MODIFIERS[part.toLowerCase()];
@@ -226,6 +227,7 @@ function duplicateWarnings(bound: Map<string, Claim[]>): KeymapProblem[] {
226
227
  return [...bound.values()].flatMap((claims) => {
227
228
  if (claims.length < 2) return [];
228
229
  const [winner, ...losers] = [...claims].sort((a, b) => a.rank - b.rank);
230
+ if (!winner) return []; // unreachable: claims.length >= 2 was checked above
229
231
  return losers.map(({ label, binding }) => ({
230
232
  action: label,
231
233
  binding,
package/common/median.ts CHANGED
@@ -8,6 +8,11 @@ export const median = (values: number[]): number | null => {
8
8
  if (values.length === 0) return null;
9
9
  const sorted = [...values].sort((a, b) => a - b);
10
10
  const mid = Math.floor(sorted.length / 2);
11
- if (sorted.length % 2 === 1) return sorted[mid];
12
- return (sorted[mid - 1] + sorted[mid]) / 2;
11
+ // Read once each: `sorted[mid]` is `number | undefined` under noUncheckedIndexedAccess, and the
12
+ // length checks above are what actually rule that out.
13
+ const middle = sorted[mid];
14
+ if (middle === undefined) return null;
15
+ if (sorted.length % 2 === 1) return middle;
16
+ const below = sorted[mid - 1];
17
+ return below === undefined ? middle : (below + middle) / 2;
13
18
  };
package/common/prPhase.ts CHANGED
@@ -24,9 +24,26 @@ export interface WorkItem {
24
24
  // list above all, where "#987" alone says nothing about which request is running (#1014).
25
25
  prTitle: string | null;
26
26
  issueTitle: string | null;
27
+ // WHY a request that is open and not obviously failing still cannot merge. GitLab collapses into
28
+ // one `detailed_merge_status` what GitHub splits across `isDraft`, `reviewDecision` and the check
29
+ // rollup, and three of its values — approvals outstanding, unresolved discussions, blocked by
30
+ // another request — have no home in `PrPhase`. Forcing them into `ready` would call something
31
+ // unmergeable ready, and into `changes-requested` would invent a review nobody left.
32
+ //
33
+ // Always null on GitHub, whose phases say everything there is to say (#981).
34
+ blockedReason: string | null;
27
35
  }
28
36
 
29
- export const EMPTY_WORK_ITEM: Readonly<WorkItem> = { phase: "none", pr: null, prUrl: null, issue: null, issueUrl: null, prTitle: null, issueTitle: null };
37
+ export const EMPTY_WORK_ITEM: Readonly<WorkItem> = {
38
+ phase: "none",
39
+ pr: null,
40
+ prUrl: null,
41
+ issue: null,
42
+ issueUrl: null,
43
+ prTitle: null,
44
+ issueTitle: null,
45
+ blockedReason: null,
46
+ };
30
47
 
31
48
  // One line for a surface with room for one: what the work is FOR beats what was done about it, so
32
49
  // the issue's title wins when there is one. Null when neither side has a title to show.
@@ -66,7 +83,7 @@ function toIssueNumber(digits: string): number | null {
66
83
  // shown next to this cell's PR has to belong to the same repo to be clickable and true.
67
84
  export function issueRefFromPrBody(body: string | null | undefined): number | null {
68
85
  const found = typeof body === "string" ? CLOSING_KEYWORD.exec(body) : null;
69
- return found ? toIssueNumber(found[1]) : null;
86
+ return found?.[1] === undefined ? null : toIssueNumber(found[1]);
70
87
  }
71
88
 
72
89
  // Whether the body ALREADY states what merging it closes, in either form GitHub honours. Broader
@@ -88,7 +105,7 @@ const BRANCH_ISSUE = /^[a-z][a-z-]*\/([1-9]\d*)-/;
88
105
  // somebody else's issue. Named for the doubt so a call site can't forget it.
89
106
  export function issueCandidateFromBranch(branch: string | null | undefined): number | null {
90
107
  const found = typeof branch === "string" ? BRANCH_ISSUE.exec(branch) : null;
91
- return found ? toIssueNumber(found[1]) : null;
108
+ return found?.[1] === undefined ? null : toIssueNumber(found[1]);
92
109
  }
93
110
 
94
111
  // The prefix this app gives a branch it creates FOR an issue (#1171). Shared because the two
@@ -104,5 +121,5 @@ const ANCHORED_ISSUE = new RegExp(`^${ISSUE_BRANCH_PREFIX}([1-9]\\d*)-`);
104
121
  // else's issue the moment the PR merges, which no amount of confirming afterwards undoes.
105
122
  export function issueFromAnchoredBranch(branch: string | null | undefined): number | null {
106
123
  const found = typeof branch === "string" ? ANCHORED_ISSUE.exec(branch) : null;
107
- return found ? toIssueNumber(found[1]) : null;
124
+ return found?.[1] === undefined ? null : toIssueNumber(found[1]);
108
125
  }
@@ -0,0 +1,28 @@
1
+ // Reading a string out of untrusted JSON — a phone's command params, a transcript line, a `gh`
2
+ // response — without `String()`.
3
+ //
4
+ // `String(value)` answers for ANY input, and that is the problem: an object becomes the literal
5
+ // text "[object Object]" and an array becomes "1,2". Nothing throws, so the bad value travels —
6
+ // as a collection slug that matches nothing, or as a session title on screen. The rule that
7
+ // catches this is @typescript-eslint/no-base-to-string.
8
+ //
9
+ // Reading returns "" (or null) instead, which the callers already handle: they were written
10
+ // against `String(x ?? "")`, so an absent field was always the empty string.
11
+
12
+ /** The value when it is a string, else `""`. For a field the caller then validates or defaults. */
13
+ export const readString = (value: unknown): string => (typeof value === "string" ? value : "");
14
+
15
+ /**
16
+ * A value rendered for a HUMAN — an error message naming what was rejected.
17
+ *
18
+ * Here the input is bad by definition, so there is something to say about any of it; `String()` is
19
+ * wrong only because "[object Object]" tells the reader nothing. JSON keeps the shape visible.
20
+ */
21
+ export const describeValue = (value: unknown): string => {
22
+ if (typeof value === "string") return value;
23
+ try {
24
+ return JSON.stringify(value) ?? String(value);
25
+ } catch {
26
+ return "[unserializable]"; // a circular structure, which JSON.stringify throws on
27
+ }
28
+ };
@@ -30,8 +30,9 @@ export interface RepoEntry {
30
30
  export function parseRepoEntry(entry: string): RepoEntry | null {
31
31
  const segments = entry.trim().split("/");
32
32
  if (segments.some((segment) => segment === "")) return null;
33
- const declared = segments.length > 1 && HOST_SEGMENT.test(segments[0]);
34
- return declared ? { host: segments[0].toLowerCase(), path: segments.slice(1), declared } : { host: GITHUB_HOST, path: segments, declared };
33
+ const first = segments[0];
34
+ const host = segments.length > 1 && first !== undefined && HOST_SEGMENT.test(first) ? first : null;
35
+ return host === null ? { host: GITHUB_HOST, path: segments, declared: false } : { host: host.toLowerCase(), path: segments.slice(1), declared: true };
35
36
  }
36
37
 
37
38
  /** The identity a repository is known by ON ITS OWN HOST — `owner/repo` for GitHub, the group path
@@ -69,7 +69,9 @@ export interface CustomThemeInput {
69
69
 
70
70
  /** The full variable set for a theme: the base it extends, with its own colours on top.
71
71
  * `builtins` supplies the base sets so this stays pure — the client reads them from the
72
- * stylesheet's source of truth, the specs from a fixture. */
72
+ * stylesheet's source of truth, the specs from a fixture. PARTIAL because a base can genuinely be
73
+ * missing (a stylesheet read that found no block for that id), and the `?? {}` below has always
74
+ * handled that — the map only ever needed one key, never all of them. */
73
75
  // Every variable present, which is what separates a set we can paint with from one we cannot.
74
76
  // A guard rather than a length check on the missing keys: both ask the same question, but only
75
77
  // this one hands the answer to the compiler.
@@ -77,7 +79,7 @@ function isCompleteThemeVars(vars: Partial<ThemeVars>): vars is ThemeVars {
77
79
  return THEME_VAR_KEYS.every((key) => !!vars[key]);
78
80
  }
79
81
 
80
- export function resolveThemeVars(theme: CustomThemeInput, builtins: Record<ThemeId, ThemeVars>): ThemeVars | null {
82
+ export function resolveThemeVars(theme: CustomThemeInput, builtins: Partial<Record<ThemeId, ThemeVars>>): ThemeVars | null {
81
83
  const base = theme.extends ? builtins[theme.extends] : null;
82
84
  const merged: Partial<ThemeVars> = { ...(base ?? {}), ...theme.colors };
83
85
  // No base and an incomplete list is the one case we cannot paint: report it rather than
@@ -1 +1 @@
1
- import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DxEa8E3F-XDB5rg1G.js";import"./chunk-WYO6CB5R-BlzOfotS-D5Vl4QYq.js";import"./chunk-VAUOI2AC-CS9QJ4yz-DW0Tgecw.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-pTxGJtb7.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
1
+ import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DxEa8E3F-C7Z349Pq.js";import"./chunk-WYO6CB5R-BlzOfotS-CbdIi1Rx.js";import"./chunk-VAUOI2AC-CS9QJ4yz-o20b35Qf.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-BwgEBlFh.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
@@ -0,0 +1 @@
1
+ import{f as e}from"./mermaid-parser.core-DxEa8E3F-C7Z349Pq.js";export{e as createArchitectureServices};