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.
- package/README.md +36 -19
- package/bin/mulmoterminal.js +9 -0
- package/common/byCodeUnit.ts +11 -0
- package/common/codeBlocks.ts +9 -5
- package/common/gitlabHosts.ts +60 -0
- package/common/isRecord.ts +6 -0
- package/common/isUnknownArray.ts +8 -0
- package/common/issueStartPlan.ts +18 -11
- package/common/keymap.ts +2 -0
- package/common/median.ts +7 -2
- package/common/prPhase.ts +21 -4
- package/common/readString.ts +28 -0
- package/common/repoEntry.ts +3 -2
- package/common/themeVars.ts +4 -2
- package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-BCXxUk19.js → abnfDiagram-VRR7QNED-RjiYivmv-Ko4XgYrQ.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-C_A3J6UT.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-Bs3iPPHw.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-DXXAwU3I.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-CxIHRfjJ.js → blockDiagram-677ZJIJ3-BPuAJQRW-CKecg_L3.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-D1Zp3ZLf.js → c4Diagram-LMCZKHZV-C0LAqQso-5vO8udpQ.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-CYxVMjfv.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-iqbVrSxA.js → chunk-32BRIVSS-BRrYpgtb-G2dVX9R4.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-DsGxy9sK.js → chunk-52WLFC77-BJ-ss3Xr-5jRfBwS-.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-FHV3qMNI.js → chunk-C7G6YPKG-BZEucKEL-dH1xp00-.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-D3JiHJ-E.js → chunk-EX3LRPZG-DLS6FBN1-Bm_0Dq7o.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-BLB-Jc4x.js → chunk-FWX5IMBZ-DHLSFw1H-DMe5J-nM.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-B9Sp5Rm4.js → chunk-HOUHSVGY-Bhlt8hXJ-ByrE_Y0q.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-Couvoogt.js → chunk-ICXQ74PX-DwgHBX_g-C7WGmPMk.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-pTxGJtb7.js → chunk-MOJQB5TN-CMZRaeqt-BwgEBlFh.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-Bjam2Bd1.js → chunk-OGEWGWER-8Qy4a8b5-DJR11LQX.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-z_O9UUQX.js → chunk-PUDLZKDR-DcrWQRYh-CRXRg8rj.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-yi9fnIVw.js → chunk-Q4XR5HBZ-ZXVGkG8Z-CuyNeF8W.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-DTtWrQx1.js → chunk-V7JOEXUC-DmGdheTX-BXL1l6dM.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-DW0Tgecw.js → chunk-VAUOI2AC-CS9QJ4yz-o20b35Qf.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-BSK94OLt.js → chunk-VR4S4FIN-C6a91eNY-DcR5TXVW.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-D5Vl4QYq.js → chunk-WYO6CB5R-BlzOfotS-CbdIi1Rx.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-BnFKS269.js → chunk-ZGVPDNZ5-BYwxNFTK-DPJ63Ug-.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-D9PYh-Ds.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-D9PYh-Ds.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-BOGZ0d_V.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--CaO8WZ9J.js → cynefinDiagram-TSTJHNR4-DHA9iPo--C-vvAO00.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-i_xOpR1t.js → dagre-VKFMJZFB--oJKqXBZ-j_nnCAk3.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-Dawy0bUD.js → diagram-FQU43EPY-CcJCB9bG-Bg8kALAQ.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-DQMAjcpJ.js → diagram-G47NLZAW-C_o-WGG1-D8IYmNOo.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--BdH6UDVY.js → diagram-NH7WQ7WH-CXJCYvY--ntIh1pVU.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-DdlsuPDl.js → diagram-OA4YK3LP-BMzeJ87A-1rD8ncO1.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-BYxqHeiT.js → diagram-WEI45ONY-D_93NKqo-DWStdu4t.js} +1 -1
- package/dist/assets/{dist-SjIG9fJN.js → dist-C1t_ZgKo.js} +1 -1
- package/dist/assets/{dist-Cl-hw6ph.js → dist-CpuWpSIw.js} +1 -1
- package/dist/assets/{dist-CCytdxgu.js → dist-QEbKPDTX.js} +1 -1
- package/dist/assets/{dist-D9fZbRmf.js → dist-w2lrwfZt.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-CqbaCkfN.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-uJbtiAJU.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-CFRhcxJL.js → erDiagram-Q63AITRT-CiABnA0s-C2GoR6NA.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-iI1MrDgd.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-CrOlHoXQ.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-D_cPrHuB.js → ganttDiagram-NO4QXBWP-DQZvdFo1-BM70MIdN.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DlP31iSf.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-CmpMFHzx.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-B_Pgy8IF.js} +1 -1
- package/dist/assets/index-C1urCATU.css +1 -0
- package/dist/assets/index-DIMX-Lgi.js +614 -0
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-3WlBQbJQ.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-1qH_6MSO.js → infoDiagram-FWYZ7A6U-7UnoB5AP-Llrz1ikt.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-C4Y9h3Kc.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-DkyrG0F4.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-2CHdMO52.js → journeyDiagram-5HDEW3XC-c5xIah9o-C6nobV_J.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-C3tvejl6.js → kanban-definition-HUTT4EX6-Cnt6loYD-r8kRECnn.js} +1 -1
- package/dist/assets/{lib-l0WFljNN.js → lib-CGMPNazU.js} +1 -1
- package/dist/assets/{line-D7ziSjKi-BgJrHM2o.js → line-D7ziSjKi-BGdO3L7C.js} +1 -1
- package/dist/assets/{marp-DXxN_4Os.js → marp-CYclI9Sp.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-XDB5rg1G.js → mermaid-parser.core-DxEa8E3F-C7Z349Pq.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-BE18ndG4.js → mermaid.core-V0OYwIz3-BAg6sjQI.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-B_Z8XGvs.js → mindmap-definition-LN4V7U3C-CexN3O6L-B4euhJCV.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-CQk0f108.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-CoS_Je3G.js → pegDiagram-2B236MQR-BiM4G0cW-Brrbufeb.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-CpVaGdmz.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-q0d7psft.js → pieDiagram-ENE6RG2P-B_fBBS-2-DognIWKZ.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-DX-o6YRc.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-BPBwmAdC.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CKmLEdNO.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-oIAJF7wn.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-B4whEhXF.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-DPRdZSTX.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CbVKtvJy.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-wRRd9wDB.js → railroadDiagram-RFXS5EU6--lNIlG64-5B9Ve7w1.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-C29F0qnM.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-iRCTL6WF.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-BPq3vrC9.js → sankeyDiagram-HTMAVEWB-DlutaXDv-BWx6oil_.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-Cq_jo_ss.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-Dyjw8haJ.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-DKYpGC0n.js → stateDiagram-2N3HPSRC-jf_dXUEU-DNMm4FaT.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-DW90BptC.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-jmF9axmm.js → swimlanes-5IMT3BWC-CYjtALQH-0aqXIsbo.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-WTqIMZw2.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-B8bL7wcx.js → timeline-definition-FHXFAJF6-DYJ4oUm8-BEW3_bqU.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-IkkpQPxV.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DgR55667.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-D9FWFG6j.js → vennDiagram-L72KCM5P-CdKAHoek-ftlvelkp.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-C-7aQQUL.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-BffT3P-h.js → wardleyDiagram-EHGQE667-BF6c4_CW-ND9jAABP.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-vPRabLJJ.js → xychartDiagram-FW5EYKEG-CGiKngj7-BbHq6IVI.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +15 -16
- package/server/agents/antigravity-session.ts +1 -1
- package/server/agents/antigravity-sessions.ts +4 -2
- package/server/agents/codex-activity.ts +1 -1
- package/server/agents/codex-rate-limits.spec.ts +1 -0
- package/server/agents/codex-rate-limits.ts +1 -1
- package/server/agents/codex-rollout.spec.ts +1 -0
- package/server/agents/codex-session.ts +4 -2
- package/server/agents/codex-sessions.ts +3 -2
- package/server/agents/probe-session.spec.ts +1 -0
- package/server/agents/probe-stall.spec.ts +104 -0
- package/server/agents/probe-stall.ts +70 -0
- package/server/agents/probe-transcript.spec.ts +11 -1
- package/server/agents/rate-limit-persist.spec.ts +1 -0
- package/server/agents/rate-limit-probe.spec.ts +91 -50
- package/server/agents/rate-limit-probe.ts +55 -33
- package/server/agents/rate-limit-routes.ts +13 -4
- package/server/agents/rate-limit-store.spec.ts +59 -4
- package/server/agents/rate-limit-store.ts +42 -5
- package/server/agents/statusline.spec.ts +1 -0
- package/server/backends/collections.ts +5 -3
- package/server/backends/html.ts +9 -2
- package/server/backends/mulmoscript.ts +10 -1
- package/server/backends/remoteHost/attachment-path.ts +1 -1
- package/server/backends/remoteHost/collectionPage.ts +8 -9
- package/server/backends/remoteHost/handlers/getCollection.ts +2 -1
- package/server/backends/remoteHost/handlers/getFeed.ts +2 -1
- package/server/backends/remoteHost/handlers/getRemoteView.ts +3 -2
- package/server/backends/remoteHost/handlers/getRemoteViewItems.ts +8 -8
- package/server/backends/remoteHost/handlers/issueWork.ts +6 -4
- package/server/backends/remoteHost/handlers/mutateRemoteView.ts +6 -4
- package/server/backends/remoteHost/handlers/terminalSession.ts +3 -2
- package/server/backends/remoteHost/issueWork.spec.ts +2 -1
- package/server/backends/remoteHost/jsonPayload.ts +55 -0
- package/server/backends/remoteHost/routes.spec.ts +1 -0
- package/server/backends/remoteHost/session.spec.ts +1 -0
- package/server/backends/remoteHost/skills.ts +1 -1
- package/server/backends/remoteHost/terminalInput.ts +1 -1
- package/server/backends/remoteHost/terminalScreen.ts +46 -2
- package/server/backends/remoteView.ts +2 -1
- package/server/backends/scheduler.ts +1 -0
- package/server/backends/translation.ts +7 -3
- package/server/backends/wiki.ts +3 -2
- package/server/config/app-config.ts +9 -0
- package/server/config/config-body.ts +1 -0
- package/server/config/config-routes.ts +16 -1
- package/server/config/dir-config.ts +1 -1
- package/server/config/header-title.ts +2 -2
- package/server/config/hue-rotate.ts +78 -0
- package/server/config/keymap-check.ts +2 -1
- package/server/config/workspace.ts +2 -1
- package/server/config/worktree-dir-config.ts +74 -0
- package/server/files/backup-store.ts +5 -3
- package/server/files/files-browse.ts +5 -3
- package/server/files/pathContainment.ts +1 -1
- package/server/files/renderedDoc.ts +1 -0
- package/server/files/scripts.ts +1 -0
- package/server/git/forge-host.ts +18 -3
- package/server/git/forge-support.ts +8 -7
- package/server/git/git-parse.ts +2 -2
- package/server/git/gitRemote.ts +1 -1
- package/server/git/glab-items.ts +104 -0
- package/server/git/glab.ts +114 -3
- package/server/git/issue-work.ts +5 -3
- package/server/git/issues.ts +2 -2
- package/server/git/prPhase.ts +61 -5
- package/server/git/prs.ts +5 -4
- package/server/git/remote-ref.ts +1 -1
- package/server/git/work-comment.ts +61 -18
- package/server/git/worktree-pr.ts +99 -19
- package/server/git/worktree-routes.ts +6 -7
- package/server/git/worktrees.ts +26 -1
- package/server/index.ts +22 -10
- package/server/infra/accounting-tool.ts +2 -2
- package/server/infra/collection-tool.ts +5 -1
- package/server/infra/pluginRuntime.ts +1 -1
- package/server/infra/plugins-registry.ts +83 -28
- package/server/infra/process-guards.ts +5 -2
- package/server/infra/pty-env.ts +1 -0
- package/server/infra/pubsub.ts +2 -2
- package/server/infra/server-tool-load.ts +13 -3
- package/server/infra/spawnCapture.ts +7 -3
- package/server/infra/tmux.ts +12 -8
- package/server/routes/gui-mcp-routes.ts +4 -2
- package/server/routes/hook-routes.ts +34 -24
- package/server/routes/issue-work-routes.ts +2 -1
- package/server/routes/mcp-routes.ts +2 -2
- package/server/routes/requestBody.ts +14 -0
- package/server/routes/session-routes.ts +14 -2
- package/server/routes/ws-routes.ts +57 -34
- package/server/session/cleared-transcripts.ts +1 -1
- package/server/session/command-summary.ts +1 -1
- package/server/session/decision-scan.ts +4 -1
- package/server/session/decisions.ts +1 -1
- package/server/session/draft-injection.ts +7 -30
- package/server/session/early-frames.ts +12 -8
- package/server/session/headlessScreen.ts +12 -6
- package/server/session/last-turn.ts +6 -3
- package/server/session/launcher-gui-mcp.ts +1 -1
- package/server/session/pty-connection.ts +17 -12
- package/server/session/pty-scan.ts +37 -2
- package/server/session/registry.ts +1 -1
- package/server/session/scheduled-sessions.ts +1 -1
- package/server/session/screen-rows.ts +4 -2
- package/server/session/session-reads.ts +3 -2
- package/server/session/shell-command.ts +1 -1
- package/server/session/tool-hook.ts +6 -3
- package/server/session/tool-store.ts +28 -9
- package/server/session/transcript.ts +16 -8
- package/server/session/translation-worker.ts +8 -4
- package/server/skills/mulmoterminal-bug-report/faq.md +16 -0
- package/server/skills/mulmoterminal-config/SKILL.md +23 -1
- package/server/skills/mulmoterminal-dirs/SKILL.md +17 -0
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-vpkNSsXO.js +0 -1
- package/dist/assets/channel-7wUqSdoX-BmaXSumw.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-jw8dvfs2.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-jw8dvfs2.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-Bb2l8CKT.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-CV0AXWMH.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BzXDVc32.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DePJCG9u.js +0 -1
- package/dist/assets/index-CNOunTbV.js +0 -616
- package/dist/assets/index-Cdf9kaYt.css +0 -1
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-CmgoBoly.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-Ckhq3qGx.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-COGnIXXn.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-DYVN7hVv.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-Ccr316bA.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-1lHKiwjs.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-CDsiITOB.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-BVRC2yH2.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CC7z1AQz.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-i5S7soVG.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DnDVKvX3.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-Dkb6cQ10.js +0 -1
- 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
|

|
|
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
|
|
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
|
-

|
|
92
92
|
|
|
93
|
-
*To focus on one agent, **zoom its cell**: it takes the window, and
|
|
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
|
|
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
|
|
738
|
-
yarn
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
checking
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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`
|
|
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
|
|
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)
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -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
|
+
}
|
package/common/codeBlocks.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
73
|
-
body.push(
|
|
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]
|
|
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
|
|
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`;
|
package/common/isRecord.ts
CHANGED
|
@@ -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);
|
package/common/issueStartPlan.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
17
|
-
* "github.com only" survived GitLab becoming startable and told users the opposite of
|
|
18
|
-
* (Codex review). Exported so the phone's refusal says the same thing. */
|
|
19
|
-
export const startableHosts = (): string => [...
|
|
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
|
-
|
|
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 && !
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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> = {
|
|
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])
|
|
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])
|
|
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])
|
|
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
|
+
};
|
package/common/repoEntry.ts
CHANGED
|
@@ -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
|
|
34
|
-
|
|
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
|
package/common/themeVars.ts
CHANGED
|
@@ -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
|
|
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-
|
|
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};
|