mulmoterminal 4.1.1 → 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 +27 -16
- 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 +3 -3
- package/common/readString.ts +28 -0
- package/common/repoEntry.ts +3 -2
- package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-CuUAI5MR.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-rRDT09Vx.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-DXXAwU3I.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-By0PkzIO.js → blockDiagram-677ZJIJ3-BPuAJQRW-CKecg_L3.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-GgOhwf63.js → c4Diagram-LMCZKHZV-C0LAqQso-5vO8udpQ.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-CYxVMjfv.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-C_AxGcxt.js → chunk-32BRIVSS-BRrYpgtb-G2dVX9R4.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-DNopp04b.js → chunk-52WLFC77-BJ-ss3Xr-5jRfBwS-.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL---6fNwPJ.js → chunk-C7G6YPKG-BZEucKEL-dH1xp00-.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-D5lN82UI.js → chunk-EX3LRPZG-DLS6FBN1-Bm_0Dq7o.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-DKxptXUj.js → chunk-FWX5IMBZ-DHLSFw1H-DMe5J-nM.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-CAh9s_YU.js → chunk-HOUHSVGY-Bhlt8hXJ-ByrE_Y0q.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-jwQc10EC.js → chunk-ICXQ74PX-DwgHBX_g-C7WGmPMk.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-DcPt_trW.js → chunk-MOJQB5TN-CMZRaeqt-BwgEBlFh.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-BuRF4rGR.js → chunk-OGEWGWER-8Qy4a8b5-DJR11LQX.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-BYgad2gt.js → chunk-PUDLZKDR-DcrWQRYh-CRXRg8rj.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-DhnrB6RT.js → chunk-Q4XR5HBZ-ZXVGkG8Z-CuyNeF8W.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-Ur1KzthC.js → chunk-V7JOEXUC-DmGdheTX-BXL1l6dM.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-BZe0Q9XX.js → chunk-VAUOI2AC-CS9QJ4yz-o20b35Qf.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-2iUlb5mQ.js → chunk-VR4S4FIN-C6a91eNY-DcR5TXVW.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-CeZxJxL0.js → chunk-WYO6CB5R-BlzOfotS-CbdIi1Rx.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-Ct0mEJmO.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--BISNJm8X.js → cynefinDiagram-TSTJHNR4-DHA9iPo--C-vvAO00.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-B459NNxB.js → dagre-VKFMJZFB--oJKqXBZ-j_nnCAk3.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-DVyaiIsO.js → diagram-FQU43EPY-CcJCB9bG-Bg8kALAQ.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-BhuGPhW4.js → diagram-G47NLZAW-C_o-WGG1-D8IYmNOo.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--DzbRZHC1.js → diagram-NH7WQ7WH-CXJCYvY--ntIh1pVU.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-D8t4UJW_.js → diagram-OA4YK3LP-BMzeJ87A-1rD8ncO1.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-Bbl2-CHc.js → diagram-WEI45ONY-D_93NKqo-DWStdu4t.js} +1 -1
- package/dist/assets/{dist-ClO6UEBJ.js → dist-C1t_ZgKo.js} +1 -1
- package/dist/assets/{dist-D8AEcEHx.js → dist-CpuWpSIw.js} +1 -1
- package/dist/assets/{dist-Daghl_nN.js → dist-QEbKPDTX.js} +1 -1
- package/dist/assets/{dist-epQ42VP2.js → dist-w2lrwfZt.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-Bbg4X-2o.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-uJbtiAJU.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-CKq_tipK.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-jmx958Wo.js → ganttDiagram-NO4QXBWP-DQZvdFo1-BM70MIdN.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DlP31iSf.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-DuZaMN46.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-C8ikfN1o.js → infoDiagram-FWYZ7A6U-7UnoB5AP-Llrz1ikt.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-ChrD-r9s.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-DkyrG0F4.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-uY7L1btR.js → journeyDiagram-5HDEW3XC-c5xIah9o-C6nobV_J.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-CNv5ziZo.js → kanban-definition-HUTT4EX6-Cnt6loYD-r8kRECnn.js} +1 -1
- package/dist/assets/{lib-dCbEkBG7.js → lib-CGMPNazU.js} +1 -1
- package/dist/assets/{line-D7ziSjKi-BEYdEqj_.js → line-D7ziSjKi-BGdO3L7C.js} +1 -1
- package/dist/assets/{marp-CU_bIZEZ.js → marp-CYclI9Sp.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-akMN_vOP.js → mermaid-parser.core-DxEa8E3F-C7Z349Pq.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-BMPzsgXF.js → mermaid.core-V0OYwIz3-BAg6sjQI.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-Bed_YDhf.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-CBrM86uu.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-D-tRTGSr.js → pieDiagram-ENE6RG2P-B_fBBS-2-DognIWKZ.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-CH-H4qGn.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-jEmJgaFj.js → railroadDiagram-RFXS5EU6--lNIlG64-5B9Ve7w1.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-DurumWyC.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-iRCTL6WF.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-Doed1fks.js → sankeyDiagram-HTMAVEWB-DlutaXDv-BWx6oil_.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-CmkZuV9D.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-Dyjw8haJ.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-BFXZmQ_i.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-e_82UQvE.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-CO0rLezO.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-BO9jvKIz.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-eXxyipXb.js → wardleyDiagram-EHGQE667-BF6c4_CW-ND9jAABP.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-CMZLvO1e.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 +1 -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 +1 -0
- package/server/agents/rate-limit-store.spec.ts +1 -0
- package/server/agents/statusline.spec.ts +1 -0
- package/server/backends/collections.ts +5 -3
- package/server/backends/remoteHost/attachment-path.ts +1 -1
- 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 +3 -2
- package/server/backends/remoteHost/handlers/issueWork.ts +6 -4
- package/server/backends/remoteHost/handlers/mutateRemoteView.ts +3 -2
- package/server/backends/remoteHost/issueWork.spec.ts +2 -1
- 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/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/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.ts +64 -12
- package/server/git/issue-work.ts +5 -3
- package/server/git/issues.ts +2 -2
- package/server/git/prPhase.ts +6 -6
- package/server/git/prs.ts +5 -4
- package/server/git/remote-ref.ts +1 -1
- package/server/git/work-comment.ts +7 -7
- package/server/git/worktree-routes.ts +6 -7
- package/server/git/worktrees.ts +26 -1
- package/server/index.ts +3 -2
- package/server/infra/accounting-tool.ts +2 -2
- package/server/infra/pluginRuntime.ts +1 -1
- package/server/infra/plugins-registry.ts +79 -25
- 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 +7 -2
- package/server/infra/tmux.ts +2 -2
- 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/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/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/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 +12 -7
- 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-B89nYL-T.js +0 -1
- package/dist/assets/channel-7wUqSdoX-DLHsiYx_.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-vnBEnnpL.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-vnBEnnpL.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-U6pbCywv.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-Dz8P7VVP.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-Bqi3MvHN.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-C_bvrqXL.js +0 -1
- package/dist/assets/index-CXYjAh9r.css +0 -1
- package/dist/assets/index-D9sJGCR9.js +0 -616
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-aO_hAsb7.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-jjlc375I.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-BGKp68D7.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-ChTyqNRH.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-BRepafjd.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-B2E_hGfU.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-C2k_5LtZ.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DSwMdPCz.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CZroC0CQ.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-C2aatMMX.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DhW5sK_F.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-oZY-xCg-.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-BCWqjXDb.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
|
|
|
@@ -214,7 +214,7 @@ Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
|
|
|
214
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 |
|
|
215
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) |
|
|
216
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 **
|
|
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`) |
|
|
218
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) |
|
|
219
219
|
| Optional | `codex` | [Codex sessions](#agents-claude--codex) in a cell, alongside Claude | `npm i -g @openai/codex` |
|
|
220
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` |
|
|
@@ -543,6 +543,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
543
543
|
| `soundKinds` | Which moments beep — see [Notification sounds](#notification-sounds). Defaults to `["finished","waiting"]`; the other kinds are opt-in. |
|
|
544
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`. |
|
|
545
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. |
|
|
546
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. |
|
|
547
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. |
|
|
548
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. |
|
|
@@ -740,22 +741,21 @@ yarn dev # backend (:34567) + Vite UI (:6856), concurrently — o
|
|
|
740
741
|
yarn dev:server # backend only (node --import tsx --env-file-if-exists=.env server/index.ts)
|
|
741
742
|
yarn dev:client # Vite dev server only
|
|
742
743
|
|
|
743
|
-
yarn
|
|
744
|
-
yarn
|
|
745
|
-
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/
|
|
746
746
|
yarn server # run backend; serves dist/ + the APIs on :34567
|
|
747
747
|
yarn test # vitest run
|
|
748
748
|
```
|
|
749
749
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
checking
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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).
|
|
759
759
|
|
|
760
760
|
In dev, open the Vite URL; its proxy forwards `/ws`, `/ws/pubsub`, and `/api` to
|
|
761
761
|
`:34567`. In production, run `yarn build` then `yarn server` and open
|
|
@@ -920,6 +920,17 @@ separate working tree that shares the repo's `.git`, so several agents can work
|
|
|
920
920
|
repo without colliding. Worktrees live under `~/.mulmoterminal/worktrees/` (override with
|
|
921
921
|
`MULMOTERMINAL_HOME`), and existing ones are listed below the field.
|
|
922
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
|
+
|
|
923
934
|
**One worktree, one session.** A worktree is tied to a branch, so it is never started
|
|
924
935
|
twice: a listed row **resumes** that worktree's session when it has one, and **starts** one
|
|
925
936
|
only when it has none. A row whose session is open in another terminal reads `in use` and
|
|
@@ -968,7 +979,7 @@ Closing a worktree cell asks whether to **keep** the worktree or **discard & rem
|
|
|
968
979
|
|
|
969
980
|
**PRs & Issues (cross-repo).** The toolbar's **Pull requests** button opens a full-screen
|
|
970
981
|
view that aggregates open PRs **and** issues across the repos listed in Settings →
|
|
971
|
-
**Pull request repos** (`prRepos`, `owner/repo` entries, or `gitlab.com/group/project`) via your server-side `gh` / `glab` 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.
|
|
972
983
|
PRs show a CI-rollup / review-decision / draft badge; each repo lists its latest open
|
|
973
984
|
issues. Rows are real links, per-repo errors don't sink the view, and the two lists load
|
|
974
985
|
independently. Backed by `GET /api/prs` and `GET /api/issues`.
|
|
@@ -1309,7 +1320,7 @@ same-origin-guarded.
|
|
|
1309
1320
|
| `POST /api/git-remote` | The dir's GitHub repo URL (for the header GitHub menu). |
|
|
1310
1321
|
| `GET /api/worktrees?cwd=` · `GET /api/worktrees/diff?cwd=` | List managed worktrees / diff one vs its base. |
|
|
1311
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). |
|
|
1312
|
-
| `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` — `gh` for github.com entries, `glab` for
|
|
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`. |
|
|
1313
1324
|
| `GET /api/repo-dirs` | Which saved directories clone which GitHub repo, ordered, with the recorded choice per repo. |
|
|
1314
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. |
|
|
1315
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. |
|
|
@@ -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
|
@@ -83,7 +83,7 @@ function toIssueNumber(digits: string): number | null {
|
|
|
83
83
|
// shown next to this cell's PR has to belong to the same repo to be clickable and true.
|
|
84
84
|
export function issueRefFromPrBody(body: string | null | undefined): number | null {
|
|
85
85
|
const found = typeof body === "string" ? CLOSING_KEYWORD.exec(body) : null;
|
|
86
|
-
return found ? toIssueNumber(found[1])
|
|
86
|
+
return found?.[1] === undefined ? null : toIssueNumber(found[1]);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Whether the body ALREADY states what merging it closes, in either form GitHub honours. Broader
|
|
@@ -105,7 +105,7 @@ const BRANCH_ISSUE = /^[a-z][a-z-]*\/([1-9]\d*)-/;
|
|
|
105
105
|
// somebody else's issue. Named for the doubt so a call site can't forget it.
|
|
106
106
|
export function issueCandidateFromBranch(branch: string | null | undefined): number | null {
|
|
107
107
|
const found = typeof branch === "string" ? BRANCH_ISSUE.exec(branch) : null;
|
|
108
|
-
return found ? toIssueNumber(found[1])
|
|
108
|
+
return found?.[1] === undefined ? null : toIssueNumber(found[1]);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// The prefix this app gives a branch it creates FOR an issue (#1171). Shared because the two
|
|
@@ -121,5 +121,5 @@ const ANCHORED_ISSUE = new RegExp(`^${ISSUE_BRANCH_PREFIX}([1-9]\\d*)-`);
|
|
|
121
121
|
// else's issue the moment the PR merges, which no amount of confirming afterwards undoes.
|
|
122
122
|
export function issueFromAnchoredBranch(branch: string | null | undefined): number | null {
|
|
123
123
|
const found = typeof branch === "string" ? ANCHORED_ISSUE.exec(branch) : null;
|
|
124
|
-
return found ? toIssueNumber(found[1])
|
|
124
|
+
return found?.[1] === undefined ? null : toIssueNumber(found[1]);
|
|
125
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
|
|
@@ -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};
|