mulmoterminal 2.3.0 → 2.5.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 +39 -3
- package/bin/cli-args.d.ts +1 -0
- package/bin/cli-args.js +16 -0
- package/bin/mulmoterminal.js +2 -1
- package/common/dirConfigSource.ts +101 -0
- package/common/prPhase.ts +65 -0
- package/common/rateLimits.ts +37 -0
- package/common/terminalScrollSpeed.ts +31 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-B8nu2pUR.js → abnfDiagram-VRR7QNED-RjiYivmv-ZKZeabOo.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-BlV5di_4.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-ByxXyiOP.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-CzywLoPM.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-Brt9e7H_.js → blockDiagram-677ZJIJ3-BPuAJQRW-aOYmvIDN.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-D598g8JV.js → c4Diagram-LMCZKHZV-C0LAqQso-DrRq0Tz1.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-DBiTAZ-Z.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-BEOPPcME.js → chunk-32BRIVSS-BRrYpgtb-D-VyP-2M.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-BmZ-jgWV.js → chunk-52WLFC77-BJ-ss3Xr-xsQKbX6Y.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-DhvCnMVg.js → chunk-C7G6YPKG-BZEucKEL-LmuOyVM4.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-CxeaHZOV.js → chunk-EX3LRPZG-DLS6FBN1-D_ZX8lXD.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-COykUZUW.js → chunk-FWX5IMBZ-DHLSFw1H-DABXVNFD.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-DFKJuoMh.js → chunk-HOUHSVGY-Bhlt8hXJ-Clcclm_e.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-CFoH6Yjf.js → chunk-ICXQ74PX-DwgHBX_g-DVrDEih_.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-Dy6S_sS_.js → chunk-MOJQB5TN-CMZRaeqt-B6_lajzj.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-XcjK3kCc.js → chunk-OGEWGWER-8Qy4a8b5-BK8v7oHr.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-CFVQ5Aq8.js → chunk-PUDLZKDR-DcrWQRYh-BpZVxE3I.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-D2iTH8ap.js → chunk-Q4XR5HBZ-ZXVGkG8Z-BJub83F1.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-Dfius2MW.js → chunk-V7JOEXUC-DmGdheTX-BwD2Cu1l.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-Dd_VpRsu.js → chunk-VAUOI2AC-CS9QJ4yz-DYmUgyge.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-U_jwmwMS.js → chunk-VR4S4FIN-C6a91eNY-CZWE7I5r.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-04XZ1Jfd.js → chunk-WYO6CB5R-BlzOfotS-CoeWhGPT.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-DBPsh-p3.js → chunk-ZGVPDNZ5-BYwxNFTK-DHPaQ6Oa.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-DEqD9yXA.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-DEqD9yXA.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-rrcpRbiY.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--D-iTSsn0.js → cynefinDiagram-TSTJHNR4-DHA9iPo--BVz-2Ntu.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-G72EKlyP.js → dagre-VKFMJZFB--oJKqXBZ-CgKI-Did.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-DgT-eqMs.js → diagram-FQU43EPY-CcJCB9bG-DIRlUnKd.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-D_JxH6o2.js → diagram-G47NLZAW-C_o-WGG1-DNgB0vZj.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--pVhp9uBi.js → diagram-NH7WQ7WH-CXJCYvY--DtULNtOi.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-CFoCoA1K.js → diagram-OA4YK3LP-BMzeJ87A-Betvk9-o.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-COhtVv5k.js → diagram-WEI45ONY-D_93NKqo-DpjErInC.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-cHbupDvE.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-CkKEjG4D.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-Ch6F8aLL.js → erDiagram-Q63AITRT-CiABnA0s-BOytIrTd.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-lhGPdKsg.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-Ce3_LeXQ.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-DcmaS_P9.js → ganttDiagram-NO4QXBWP-DQZvdFo1-Bv6Ol2Sx.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DgqDgNXY.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-CgaMpUqZ.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-aUQOq1B7.js} +1 -1
- package/dist/assets/index-BwvrN1j0.css +1 -0
- package/dist/assets/{index-QEGG3YTz.js → index-CbGZuCmY.js} +202 -202
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-CbcoQy2P.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-f2daXl9J.js → infoDiagram-FWYZ7A6U-7UnoB5AP-D5riRt52.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-DYE7Ld9-.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-DUHr3ImH.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-B0woNVtP.js → journeyDiagram-5HDEW3XC-c5xIah9o-BmYfNX5M.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-Dm8evmYs.js → kanban-definition-HUTT4EX6-Cnt6loYD-CYSjS-Kr.js} +1 -1
- package/dist/assets/{lib-DtgnGSMW.js → lib-4D_VJiWI.js} +1 -1
- package/dist/assets/{line-D7ziSjKi-CRwDdfSt.js → line-D7ziSjKi-BSuJe7JA.js} +1 -1
- package/dist/assets/{marp-Djuncd6G.js → marp-aQPALUt4.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-BILJo_fK.js → mermaid-parser.core-DxEa8E3F-BAMiAMZz.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-CabnyPOO.js → mermaid.core-V0OYwIz3-0t7TpKVw.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-DF1aNqGu.js → mindmap-definition-LN4V7U3C-CexN3O6L-Y5ILic5l.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-Bzib0qjU.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-q-89nsVP.js → pegDiagram-2B236MQR-BiM4G0cW-BcRtbtj2.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-sgdov0Km.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-B3pcC0Pg.js → pieDiagram-ENE6RG2P-B_fBBS-2-BoLxHrrD.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-BBKSbiVD.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-CaBUCRTr.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-HeF-YUqy.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-CVEaRb6p.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-Da0o3UJC.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-d8_8RJsZ.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CJVm33Iy.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-Do0ZI2kT.js → railroadDiagram-RFXS5EU6--lNIlG64-BZODWkTp.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-B4oci7Dv.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-CTG1__VO.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-KRXYn9rF.js → sankeyDiagram-HTMAVEWB-DlutaXDv-BHkDstqV.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-zw0J8MZu.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-B47PY7W5.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-vFGH-mvy.js → stateDiagram-2N3HPSRC-jf_dXUEU-4bMMcfLt.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-BhlNWKJ_.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-DhegOTK_.js → swimlanes-5IMT3BWC-CYjtALQH-Dkkp7Xaf.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-BIUkJIiD.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-a7d-hF5Y.js → timeline-definition-FHXFAJF6-DYJ4oUm8-CKcCEyil.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-BTmguQlN.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-C69GNR2Y.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-La4xWHGC.js → vennDiagram-L72KCM5P-CdKAHoek-D4FLVcsc.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-4mdSyYTy.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-M6-Z6fe5.js → wardleyDiagram-EHGQE667-BF6c4_CW-CRhp69sj.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-BtTeKKIV.js → xychartDiagram-FW5EYKEG-CGiKngj7-CqYw8fCB.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/server/agents/claude-args.ts +10 -1
- package/server/agents/codex-rate-limits.spec.ts +74 -0
- package/server/agents/codex-rate-limits.ts +94 -0
- package/server/agents/codex-rollout.ts +79 -0
- package/server/agents/pr-clone-prompt.ts +30 -0
- package/server/agents/rate-limit-persist.ts +59 -0
- package/server/agents/rate-limit-probe.spec.ts +66 -0
- package/server/agents/rate-limit-probe.ts +114 -0
- package/server/agents/rate-limit-routes.ts +66 -0
- package/server/agents/rate-limit-store.spec.ts +91 -0
- package/server/agents/rate-limit-store.ts +73 -0
- package/server/agents/statusline.spec.ts +66 -0
- package/server/agents/statusline.ts +48 -0
- package/server/cli-init.ts +2 -2
- package/server/config/app-config.ts +48 -4
- package/server/config/config-routes.ts +10 -2
- package/server/config/config-schema.ts +1 -1
- package/server/config/dir-config.ts +83 -0
- package/server/config/env.ts +12 -0
- package/server/config/header-context.ts +9 -8
- package/server/config/workspace.ts +17 -7
- package/server/git/gitRemote.ts +14 -31
- package/server/git/pr-footer.ts +17 -4
- package/server/git/prPhase.ts +40 -12
- package/server/git/remote-ref.ts +50 -0
- package/server/git/repo-root-sync.ts +55 -0
- package/server/git/worktree-pr.ts +1 -0
- package/server/index.ts +42 -5
- package/server/infra/allowed-origin.ts +106 -25
- package/server/infra/hide-error-stacks.ts +15 -0
- package/server/infra/spa-fallback.ts +18 -0
- package/server/infra/tmux.ts +40 -0
- package/server/routes/app-routes.ts +7 -2
- package/server/routes/dir-routes.ts +19 -3
- package/server/session/spawn-claude.ts +39 -16
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-TY_v-qbq.js +0 -1
- package/dist/assets/channel-7wUqSdoX-DRUj8A2e.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-BVAZDjb0.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-BVAZDjb0.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-CFSruy5n.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-B09FMUUq.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BswknzuF.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-Bg_TZB3f.js +0 -1
- package/dist/assets/index-DWePsXYt.css +0 -1
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-BAI4sdgi.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-Bqib4t7m.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-C7f4Ij-s.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-C0_E6OiV.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-C2_X_Txm.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-wMka3Tbm.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-B7uu2NPX.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-D4Sj2mdo.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-PiMBzK3e.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-ByceWEns.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-Bf6uz3xV.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-CLzvNsC2.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-OXtZqto9.js +0 -1
package/README.md
CHANGED
|
@@ -21,6 +21,13 @@ ping to your phone when a task finishes. One `npx` command, no Electron, no conf
|
|
|
21
21
|
npx mulmoterminal # starts on http://localhost:34567 and opens your browser
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
> **Something looks wrong?** Type `/mulmoterminal-bug-report` in any MulmoTerminal session. The
|
|
25
|
+
> bundled skill hears the symptom out, checks your **real** config, schema and version to see
|
|
26
|
+
> whether the behaviour is configuration or by design, searches the existing issues — and only
|
|
27
|
+
> helps you file one if none of that explains it, with the environment collected and secrets
|
|
28
|
+
> masked. Getting you unstuck is the goal; an issue is what is left when the first three steps
|
|
29
|
+
> fail.
|
|
30
|
+
|
|
24
31
|

|
|
25
32
|
|
|
26
33
|
*The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working / done (blue), needs-you (amber — e.g. waiting on a permission), idle — with an attention chime so a stuck cell off-screen still pulls you back. Supervise many; only step in where you're called.*
|
|
@@ -93,6 +100,13 @@ than as bytes (files within the session's working directory only):
|
|
|
93
100
|
| source, config, logs, and `.txt` — 46 extensions | the app's own **Files** view (`/files?path=`), where CodeMirror highlights it, the tree is right there, and it can be edited |
|
|
94
101
|
| everything else — images, PDF, SVG, HTML, video | raw bytes in a new tab, which the browser renders better than an editor would |
|
|
95
102
|
|
|
103
|
+
**While a grid cell is enlarged, the [Files pane](#files-view-browse--edit) takes the click first** — every
|
|
104
|
+
row above except the last one, since the pane is the same editor plus a Markdown preview. The
|
|
105
|
+
file opens *beside* the terminal that printed it, and the pane opens itself if it was closed.
|
|
106
|
+
It declines, leaving the routing above untouched, when nothing is enlarged, when the path is
|
|
107
|
+
not under that cell's own directory (the pane cannot walk above its root), or for the raw-bytes
|
|
108
|
+
row, where it would only show an empty editor.
|
|
109
|
+
|
|
96
110
|
Highlighting in the Files view covers the JS/TS family, JSON and Markdown (the modes
|
|
97
111
|
`cmEditor.ts` bundles); other languages open as plain text.
|
|
98
112
|
|
|
@@ -448,7 +462,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
448
462
|
|
|
449
463
|

|
|
450
464
|
|
|
451
|
-
*Open it from the ⚙ button in the toolbar. Pick a **theme**, set the **terminal font size**, set a custom **attention sound**, list the repos the cross-repo **PRs & Issues** view should aggregate, add **launch commands** for grid cells, and register your own **MCP servers** — no need to hand-edit the config file. Note that **theme
|
|
465
|
+
*Open it from the ⚙ button in the toolbar. Pick a **theme**, set the **terminal font size** and **scroll speed**, set a custom **attention sound**, list the repos the cross-repo **PRs & Issues** view should aggregate, add **launch commands** for grid cells, and register your own **MCP servers** — no need to hand-edit the config file. Note that **theme, font size and scroll speed are stored per browser** (they're display preferences, so a phone and a desktop keep their own); the rest live in `~/.mulmoterminal/config.json` and are shared by every client.*
|
|
452
466
|
|
|
453
467
|
| Field | Meaning |
|
|
454
468
|
| ------------ | ------- |
|
|
@@ -461,16 +475,23 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
461
475
|
| `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. |
|
|
462
476
|
| `userMcpServers` | `{ id, url }` HTTP MCP servers merged into the **single-view** Claude session's `--mcp-config` (a `localhost` URL is reached over `host.docker.internal` in the Docker sandbox). Takes effect on the next session. |
|
|
463
477
|
| `buttons` | Header action buttons — see [Header buttons](#header-buttons). Omit to keep the defaults; set to replace them. |
|
|
464
|
-
| `chips` | Header info chips. Omit to keep the default set; `[]` hides all built-ins. |
|
|
478
|
+
| `chips` | Header info chips (`dir` / `git` / `work` / `diff` / `ctx` / `usage` / `status` / `tools`, or custom text). Omit to keep the default set; `[]` hides all built-ins. `work` shows which PR / issue the cell is on (`#977 → #966`) and clears itself when the PR merges — see the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#work-chip). |
|
|
465
479
|
| `pushEnabled` | `true` to send a **Web Push** to your registered devices. Off by default; only sends while the **RemoteHost** channel is connected (see below). The master switch — `pushKinds` picks which moments. |
|
|
466
480
|
| `pushKinds` | Which moments push: `"finished"` (a turn ended, ✅) and/or `"waiting"` (the agent stopped to ask — a permission prompt or a question, ❓, **once per prompt**). Omit to keep both; `[]` for none. A kind added in a later version stays off until you tick it. |
|
|
467
481
|
| `worklogEnabled` | `true` to run the built-in **dev worklog** batch (see below). Off by default (each run spawns an LLM session, so it costs tokens). |
|
|
468
482
|
| `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). |
|
|
469
483
|
| `terminalSubmit` | Which bytes Claude reads as **submit** vs **newline**: `"cr"` (default — Enter submits, Shift+Enter makes a newline) or `"esc-cr"` (for a Claude Code rebound the other way). Applies to the keyboard **and** the phone remote-view submit, for **Claude sessions only** (shell/codex keep plain Enter). See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#terminal-submit). |
|
|
470
484
|
| `copyOnSelect` | `true` puts a **mouse selection on the clipboard the moment it settles**, with no key pressed (the PuTTY / iTerm2 behaviour). **Off by default** — it changes the clipboard when you may only have meant to highlight something. No Settings UI: edit the file and reload the tab. Composes with the `copy` keymap action rather than replacing it. Over plain `http://` the browser gives a page no clipboard access, so a fallback asks xterm to copy instead; see the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#copy-on-select). |
|
|
471
|
-
| `prWorkdirFooter` | Ends
|
|
485
|
+
| `prWorkdirFooter` | Ends a PR body with `work in <clone>` — the directory name of the clone the work happened in, so a PR says which of several side-by-side checkouts produced it. Applies to **both** paths that open PRs here: **⧉ Open PR** appends it to the PR it creates, and every Claude session is told to end the bodies it writes with the same line (the name is resolved by the server, so a session inside a managed worktree still names the main checkout). **On by default**; set `false` to opt out — read per PR and per session spawn, so no restart is needed (there is no Settings control for it). Appending is idempotent: an existing PR never gets a second copy. |
|
|
472
486
|
| `fontFamily` | The **terminal font** every session renders in — a CSS font-family stack, e.g. `"'Cica', 'MS Gothic', monospace"`. No Settings UI: edit the file, then **restart** (this config is read once at startup). Unset uses the built-in stack (JetBrains Mono / Fira Code / Menlo / Consolas, then CJK faces for Japanese, Korean and Chinese). Unlike the per-browser font **size**, this is one value for the whole host — it names fonts, and which fonts exist is a property of the machine. A directory can override it. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#font-family). |
|
|
473
487
|
|
|
488
|
+
Every MulmoTerminal on the machine shares this one file, so an older build could save over a key a
|
|
489
|
+
newer one wrote. It doesn't: a **top-level key this version doesn't recognise is written back
|
|
490
|
+
untouched**, which is what makes running two versions side by side — or downgrading for a while —
|
|
491
|
+
safe. A mistyped key survives on the same rule, which is deliberate: a line you can still see is
|
|
492
|
+
easier to debug than one that silently vanished. See the
|
|
493
|
+
[Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#unknown-keys).
|
|
494
|
+
|
|
474
495
|
#### Header buttons
|
|
475
496
|
|
|
476
497
|
Each terminal header shows configurable **action buttons**. Omitting `buttons` (globally or per-dir)
|
|
@@ -620,6 +641,12 @@ as the reload signal, so colors, palette, font size and grid order update withou
|
|
|
620
641
|
anything. There is no filesystem watcher, so an edit made **outside** a session (your own
|
|
621
642
|
editor) is picked up when the terminal is next opened.
|
|
622
643
|
|
|
644
|
+
**Checking what took effect.** Settings → **Directory settings** lists your recent directories
|
|
645
|
+
and expands each one to the values in force, with a swatch per color and the path of the file
|
|
646
|
+
they came from. It also names the keys it **dropped** (a color that isn't `#rrggbb`, a size out
|
|
647
|
+
of range) and the keys it doesn't read at all (`badgeColour`, a global-only setting) — which is
|
|
648
|
+
what tells "I never set that" apart from "I set it and it didn't take".
|
|
649
|
+
|
|
623
650
|
---
|
|
624
651
|
|
|
625
652
|
## Running
|
|
@@ -771,6 +798,10 @@ shrinks the pane rather than reflowing xterm into garbage. It works in both zoom
|
|
|
771
798
|
(cockpit roster and thumbnail filmstrip), the pane re-roots as you walk the zoom between
|
|
772
799
|
terminals, and whether it's open plus how wide it is are remembered per browser.
|
|
773
800
|
|
|
801
|
+
The toggle is not the only way in: while a cell is enlarged, **clicking a file path the agent
|
|
802
|
+
printed** opens it here too, rather than in a new tab or full-screen — see
|
|
803
|
+
[Clicking a file path](#clicking-a-file-path).
|
|
804
|
+
|
|
774
805
|
All reads and writes go through `GET/PUT /api/files/browse/*?cwd=&path=`, and every
|
|
775
806
|
`path` is **contained within the project root** (server-side) — `..`/absolute escapes
|
|
776
807
|
are rejected for reads and writes alike, so editing can't reach outside the directory
|
|
@@ -941,6 +972,10 @@ Favorited collections get their own toolbar buttons.
|
|
|
941
972
|
**Option** is treated as Meta so Claude's Alt-key bindings work. If your Claude Code is
|
|
942
973
|
rebound so Enter and Shift+Enter behave backwards, flip them with
|
|
943
974
|
[`terminalSubmit`](https://receptron.github.io/mulmoterminal/guide/en/config.html#terminal-submit).
|
|
975
|
+
- **Scroll speed** — one wheel notch or trackpad swipe moves the terminal the same distance
|
|
976
|
+
whether you're reading a shell's scrollback or a full-screen app like Claude Code. If a
|
|
977
|
+
two-finger scroll on a Mac trackpad flies past what you were reading, turn **terminal scroll
|
|
978
|
+
speed** down in Settings (0.25×–3×, per browser — it's a property of the pointing device).
|
|
944
979
|
- **No accidental page zoom** — `Ctrl`+wheel and a trackpad pinch would rescale the whole
|
|
945
980
|
page and drag the layout and the terminal's fit along with it, so both are ignored.
|
|
946
981
|
Keyboard zoom (`Cmd`/`Ctrl` `+` / `-`) still works when you mean it, and a phone's finger
|
|
@@ -1137,6 +1172,7 @@ same-origin-guarded.
|
|
|
1137
1172
|
| -------- | ------- |
|
|
1138
1173
|
| `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `soundKinds`, `sounds`, `prRepos`, `launchers`, `quickCommands`, `userMcpServers`, `providers`). |
|
|
1139
1174
|
| `GET /api/sound?kind=` · `/api/dir-sound?cwd=&kind=` · `/api/sound-preset/:id` · `/api/dir-config?cwd=` | Custom / per-directory / preset attention sound + per-dir config. `kind` selects a config entry, never a path. |
|
|
1175
|
+
| `GET /api/dir-config-detail?cwd=` | The same per-dir config, **plus** the settings a running terminal doesn't need (`provider`, `model`, `skills`, `addDirs`, header button/chip **labels**), **plus** which keys the file set and how each fared (applied / dropped in validation / not a setting at all). Read-only; backs the Settings modal's **Directory settings** preview. Unlike the other `?cwd=` routes this one does **not** fall back to the default workspace — it reports on the directory it was asked about, so a path that no longer exists comes back as `exists:false`. Sound paths and button commands stay server-side. |
|
|
1140
1176
|
| `GET /api/launch-options` | The Anthropic-compatible backends this server can reach, each with its models and — when it can't — the reason. Reports the **name** of the env var a key is read from, never the key. |
|
|
1141
1177
|
| `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
|
|
1142
1178
|
| `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
|
package/bin/cli-args.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export declare const SECOND_INSTANCE_NOTE: string;
|
|
|
10
10
|
export declare const MIN_NODE_LABEL: string;
|
|
11
11
|
export declare function nodeMeetsMinimum(version: string): boolean;
|
|
12
12
|
export declare function serverNodeArgs(serverEntry: string, launchDir: string): string[];
|
|
13
|
+
export declare function serverSpawnEnv(env: Record<string, string | undefined>, port: number, cwd: string): Record<string, string | undefined>;
|
package/bin/cli-args.js
CHANGED
|
@@ -150,3 +150,19 @@ export function nodeMeetsMinimum(version) {
|
|
|
150
150
|
export function serverNodeArgs(serverEntry, launchDir) {
|
|
151
151
|
return ["--import", "tsx", `--env-file-if-exists=${join(launchDir, ".env")}`, serverEntry];
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The environment the launcher spawns the server with.
|
|
156
|
+
*
|
|
157
|
+
* Only the two values the server cannot work out for itself. In particular NOT `NODE_ENV`:
|
|
158
|
+
* the server hands its own environment to every PTY it spawns, so a `NODE_ENV=production`
|
|
159
|
+
* set here reaches every terminal in every cell — where yarn v1 reads it and installs
|
|
160
|
+
* WITHOUT devDependencies while still reporting success (#955). Express is pinned to
|
|
161
|
+
* production separately, in the server, so nothing here needs to say it.
|
|
162
|
+
*
|
|
163
|
+
* A `NODE_ENV` the user exported themselves passes through untouched: this decides what the
|
|
164
|
+
* launcher adds, not what it removes.
|
|
165
|
+
*/
|
|
166
|
+
export function serverSpawnEnv(env, port, cwd) {
|
|
167
|
+
return { ...env, PORT: String(port), CLAUDE_CWD: cwd };
|
|
168
|
+
}
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
nodeMeetsMinimum,
|
|
27
27
|
MIN_NODE_LABEL,
|
|
28
28
|
serverNodeArgs,
|
|
29
|
+
serverSpawnEnv,
|
|
29
30
|
} from "./cli-args.js";
|
|
30
31
|
|
|
31
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -272,7 +273,7 @@ function runServer(port, noOpen, cwd, onChild) {
|
|
|
272
273
|
// package directory, so serverNodeArgs makes that path absolute (#795).
|
|
273
274
|
const server = spawn(process.execPath, serverNodeArgs(SERVER_ENTRY, process.cwd()), {
|
|
274
275
|
cwd: PKG_DIR,
|
|
275
|
-
env:
|
|
276
|
+
env: serverSpawnEnv(process.env, port, cwd),
|
|
276
277
|
stdio: ["inherit", "inherit", "pipe"],
|
|
277
278
|
});
|
|
278
279
|
let stderrTail = "";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Where a directory's settings actually came from, for the settings modal's preview.
|
|
2
|
+
//
|
|
3
|
+
// The rest of the app only ever needs the RESOLVED config — the values a terminal adopts.
|
|
4
|
+
// That is exactly what makes a mistyped or rejected setting invisible: the file is on disk,
|
|
5
|
+
// the app reads it, and the value silently isn't there. This says which keys the file set,
|
|
6
|
+
// which of them survived validation, and which the app doesn't read at all.
|
|
7
|
+
|
|
8
|
+
// Every key `loadDirConfig` reads. A key outside this list is a typo as far as the app is
|
|
9
|
+
// concerned, however sensible it looks — `server/config/dir-config.spec.ts` pins the two
|
|
10
|
+
// together so a field added to the loader can't quietly go missing here.
|
|
11
|
+
export const DIR_CONFIG_KEYS = [
|
|
12
|
+
"name",
|
|
13
|
+
"badgeColor",
|
|
14
|
+
"headerColor",
|
|
15
|
+
"headerTextColor",
|
|
16
|
+
"cellColor",
|
|
17
|
+
"cellBorderColor",
|
|
18
|
+
"dotColor",
|
|
19
|
+
"buttonColor",
|
|
20
|
+
"fontSize",
|
|
21
|
+
"fontFamily",
|
|
22
|
+
"orderPriority",
|
|
23
|
+
"theme",
|
|
24
|
+
"colors",
|
|
25
|
+
"sound",
|
|
26
|
+
"sounds",
|
|
27
|
+
"buttons",
|
|
28
|
+
"chips",
|
|
29
|
+
"skills",
|
|
30
|
+
"provider",
|
|
31
|
+
"model",
|
|
32
|
+
"addDirs",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export interface DirConfigSource {
|
|
36
|
+
// Keys the file set that the app is using.
|
|
37
|
+
applied: string[];
|
|
38
|
+
// Keys the file set that the app read and then dropped — an out-of-range number, a colour
|
|
39
|
+
// that isn't #rrggbb, a sound outside the directory. The value is gone; the key is the clue.
|
|
40
|
+
ignored: string[];
|
|
41
|
+
// Keys the app doesn't read at all: a misspelling (`badgeColour`), or a setting that belongs
|
|
42
|
+
// in the global config instead of a directory's file.
|
|
43
|
+
unknown: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const EMPTY_DIR_CONFIG_SOURCE: DirConfigSource = { applied: [], ignored: [], unknown: [] };
|
|
47
|
+
|
|
48
|
+
// The settings a directory can make that the per-cell config deliberately doesn't carry — the
|
|
49
|
+
// browser has no use for them while running a terminal, but the preview has to SHOW them or a
|
|
50
|
+
// key like `provider` can only be named as applied, never read back.
|
|
51
|
+
//
|
|
52
|
+
// Labels only for buttons and chips: their `cmd` / `text` is what the button would type into
|
|
53
|
+
// the session, which is not what "did my config take effect" needs, and is the sort of thing a
|
|
54
|
+
// screenshot of the settings screen shouldn't carry. Sound paths stay out for the same reason
|
|
55
|
+
// they stay out of the per-cell config — that path is the confinement boundary.
|
|
56
|
+
export interface DirConfigExtras {
|
|
57
|
+
provider: string | null;
|
|
58
|
+
model: string | null;
|
|
59
|
+
skills: string[] | null;
|
|
60
|
+
addDirs: string[] | null;
|
|
61
|
+
buttonLabels: string[];
|
|
62
|
+
chipLabels: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const EMPTY_DIR_CONFIG_EXTRAS: DirConfigExtras = {
|
|
66
|
+
provider: null,
|
|
67
|
+
model: null,
|
|
68
|
+
skills: null,
|
|
69
|
+
addDirs: null,
|
|
70
|
+
buttonLabels: [],
|
|
71
|
+
chipLabels: [],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// "The loader kept nothing for this key" — null/undefined, but also the empty collections the
|
|
75
|
+
// loader returns when every entry in a map or list was rejected.
|
|
76
|
+
function isEmptyValue(value: unknown): boolean {
|
|
77
|
+
if (value === null || value === undefined) return true;
|
|
78
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
79
|
+
if (typeof value === "object") return Object.keys(value as Record<string, unknown>).length === 0;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The keys a resolved config actually holds a value for. Taken as a SET by describeDirConfig
|
|
84
|
+
// rather than the config object itself: the loader's type is an interface with named fields,
|
|
85
|
+
// which no amount of `Record<string, unknown>` will accept without a cast.
|
|
86
|
+
export function keysWithValue(resolved: object): Set<string> {
|
|
87
|
+
const entries: [string, unknown][] = Object.entries(resolved);
|
|
88
|
+
return new Set(entries.filter(([, value]) => !isEmptyValue(value)).map(([key]) => key));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// `raw` is the file as parsed; `kept` is which keys survived the loader (see keysWithValue).
|
|
92
|
+
export function describeDirConfig(raw: Record<string, unknown>, kept: ReadonlySet<string>): DirConfigSource {
|
|
93
|
+
const known = new Set<string>(DIR_CONFIG_KEYS);
|
|
94
|
+
const source: DirConfigSource = { applied: [], ignored: [], unknown: [] };
|
|
95
|
+
for (const key of Object.keys(raw)) {
|
|
96
|
+
if (!known.has(key)) source.unknown.push(key);
|
|
97
|
+
else if (kept.has(key)) source.applied.push(key);
|
|
98
|
+
else source.ignored.push(key);
|
|
99
|
+
}
|
|
100
|
+
return source;
|
|
101
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// What a cell is working on, as it travels between the two sides: the branch's PR, its phase in
|
|
2
|
+
// the review loop, and the issue that PR closes. Shared because BOTH sides decide from it — the
|
|
3
|
+
// server derives it from `gh`, the client decides what the header chip shows — and it used to be
|
|
4
|
+
// two declarations with a "keep them in sync" comment on one of them (src/components/rosterPhase.ts).
|
|
5
|
+
|
|
6
|
+
// Ordered roughly along the lifecycle so the client can pick a colour/label per phase.
|
|
7
|
+
// `none` = no PR for this branch yet (still local work); `ready` = open, CI green, no
|
|
8
|
+
// changes requested — i.e. waiting to merge.
|
|
9
|
+
export type PrPhase = "none" | "draft" | "ci-failing" | "changes-requested" | "ci-running" | "ready" | "merged" | "closed";
|
|
10
|
+
|
|
11
|
+
export const PR_PHASES: readonly PrPhase[] = ["none", "draft", "ci-failing", "changes-requested", "ci-running", "ready", "merged", "closed"];
|
|
12
|
+
|
|
13
|
+
export const isPrPhase = (v: unknown): v is PrPhase => typeof v === "string" && (PR_PHASES as readonly string[]).includes(v);
|
|
14
|
+
|
|
15
|
+
// The /api/pr-phase response. `phase` and `prUrl` predate the rest and the roster reads them;
|
|
16
|
+
// the numbers are what lets a cell say WHICH work it is on rather than just how far along it is.
|
|
17
|
+
export interface WorkItem {
|
|
18
|
+
phase: PrPhase;
|
|
19
|
+
pr: number | null;
|
|
20
|
+
prUrl: string | null;
|
|
21
|
+
issue: number | null;
|
|
22
|
+
issueUrl: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const EMPTY_WORK_ITEM: Readonly<WorkItem> = { phase: "none", pr: null, prUrl: null, issue: null, issueUrl: null };
|
|
26
|
+
|
|
27
|
+
// GitHub's closing keywords, as GitHub itself matches them: any of these, then optional
|
|
28
|
+
// whitespace/colon, then #N. Anything else in the body is a mention, not a link — a PR that
|
|
29
|
+
// says "related to #12" is not working on #12.
|
|
30
|
+
// One character class for the separator, not `\s*:?\s+`: two adjacent whitespace quantifiers
|
|
31
|
+
// backtrack super-linearly on a long body, and a PR description is user input.
|
|
32
|
+
//
|
|
33
|
+
// `[1-9]\d*` rather than `\d+`: there is no issue #0, and `#0123` is not issue 123 — both are
|
|
34
|
+
// typos, and a typo that renders as a link to somebody else's issue is worse than no chip.
|
|
35
|
+
const CLOSING_KEYWORD = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)[:\s]+#([1-9]\d*)/i;
|
|
36
|
+
|
|
37
|
+
// Digits from a body or a branch name are unbounded, and `Number("9".repeat(20))` is 1e20 — which
|
|
38
|
+
// would render in the chip as "#1e+20" and link nowhere. Anything past the safe-integer range is
|
|
39
|
+
// not an issue number (found by Codex review).
|
|
40
|
+
function toIssueNumber(digits: string): number | null {
|
|
41
|
+
const n = Number(digits);
|
|
42
|
+
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The issue a PR body says it closes, or null. Deliberately blind to the full-URL form
|
|
46
|
+
// (`Fixes https://github.com/o/r/issues/12`): that can name ANOTHER repository, and a number
|
|
47
|
+
// shown next to this cell's PR has to belong to the same repo to be clickable and true.
|
|
48
|
+
export function issueRefFromPrBody(body: string | null | undefined): number | null {
|
|
49
|
+
const found = typeof body === "string" ? CLOSING_KEYWORD.exec(body) : null;
|
|
50
|
+
return found ? toIssueNumber(found[1]) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A branch named after its issue the way this repo names them: `fix/966-preserve-unknown-keys`.
|
|
54
|
+
// Requires a type prefix and a hyphen after the digits, so `chore/dep-updates-20260728` is not
|
|
55
|
+
// read as issue #20260728.
|
|
56
|
+
const BRANCH_ISSUE = /^[a-z][a-z-]*\/([1-9]\d*)-/;
|
|
57
|
+
|
|
58
|
+
// A CANDIDATE, never an answer: `release/2026-07-28-hotfix` yields 2026 here, and no pattern can
|
|
59
|
+
// tell that apart from a branch for issue #2026 — the year and the number have the same shape.
|
|
60
|
+
// The caller must confirm the issue exists before showing it, or a cell claims to be working on
|
|
61
|
+
// somebody else's issue. Named for the doubt so a call site can't forget it.
|
|
62
|
+
export function issueCandidateFromBranch(branch: string | null | undefined): number | null {
|
|
63
|
+
const found = typeof branch === "string" ? BRANCH_ISSUE.exec(branch) : null;
|
|
64
|
+
return found ? toIssueNumber(found[1]) : null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The rate-limit wire shape, and the one parser for it (#387).
|
|
2
|
+
//
|
|
3
|
+
// Here rather than on either side because BOTH decide from it: the server reads it back out of its
|
|
4
|
+
// own cache file, and the browser reads it off `/api/rate-limits`. Written twice it was written
|
|
5
|
+
// twice differently — jscpd caught the copies before they had a chance to drift.
|
|
6
|
+
//
|
|
7
|
+
// Everything is optional on purpose. A window can be absent because the agent is not installed,
|
|
8
|
+
// because the plan is API-key billed, because no session has answered yet, or because upstream
|
|
9
|
+
// dropped the field (anthropics/claude-code#40094). None of those are zero, and a gauge reading
|
|
10
|
+
// 0% when the truth is 83% is the worst thing this data can do.
|
|
11
|
+
|
|
12
|
+
export interface RateLimitWindow {
|
|
13
|
+
usedPercentage: number; // 0-100, fractional
|
|
14
|
+
resetsAt_sec: number | null; // Unix epoch seconds
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RateLimits {
|
|
18
|
+
fiveHour: RateLimitWindow | null;
|
|
19
|
+
sevenDay: RateLimitWindow | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
|
|
23
|
+
const finiteNumber = (value: unknown): number | null => (typeof value === "number" && Number.isFinite(value) ? value : null);
|
|
24
|
+
|
|
25
|
+
export function parseRateLimitWindow(raw: unknown): RateLimitWindow | null {
|
|
26
|
+
if (!isRecord(raw)) return null;
|
|
27
|
+
const used = finiteNumber(raw.usedPercentage);
|
|
28
|
+
return used === null ? null : { usedPercentage: used, resetsAt_sec: finiteNumber(raw.resetsAt_sec) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Null when neither window survives, so a caller can tell "nothing to show" from "0% used". */
|
|
32
|
+
export function parseRateLimits(raw: unknown): RateLimits | null {
|
|
33
|
+
if (!isRecord(raw)) return null;
|
|
34
|
+
const fiveHour = parseRateLimitWindow(raw.fiveHour);
|
|
35
|
+
const sevenDay = parseRateLimitWindow(raw.sevenDay);
|
|
36
|
+
return fiveHour || sevenDay ? { fiveHour, sevenDay } : null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// How far one wheel/trackpad gesture scrolls the terminal, as a multiplier on the xterm
|
|
2
|
+
// default (1 = xterm's own speed). Shared because both sides decide from the same numbers:
|
|
3
|
+
// the Settings stepper bounds its own buttons, and the client re-validates what it reads back
|
|
4
|
+
// out of localStorage.
|
|
5
|
+
//
|
|
6
|
+
// It drives BOTH scrolling paths, which are different code: xterm's `scrollSensitivity` for
|
|
7
|
+
// the normal buffer's scrollback, and the wheel-report accumulator in ./mouseReports for a
|
|
8
|
+
// full-screen TUI in the alternate buffer (Claude Code). One setting, because to the person
|
|
9
|
+
// scrolling they are the same gesture.
|
|
10
|
+
export const TERMINAL_SCROLL_SPEED_DEFAULT = 1;
|
|
11
|
+
// A macOS trackpad emits a wheel event every few pixels, so the useful range sits below 1 —
|
|
12
|
+
// 0.25 is "a swipe moves a paragraph", still fast enough to cross a long transcript. Above 3
|
|
13
|
+
// a single flick outruns the renderer and lands nowhere readable, which is the complaint this
|
|
14
|
+
// setting exists to answer.
|
|
15
|
+
export const TERMINAL_SCROLL_SPEED_MIN = 0.25;
|
|
16
|
+
export const TERMINAL_SCROLL_SPEED_MAX = 3;
|
|
17
|
+
export const TERMINAL_SCROLL_SPEED_STEP = 0.25;
|
|
18
|
+
|
|
19
|
+
// Clamp rather than reject an out-of-range number, for the same reason the font size does:
|
|
20
|
+
// honouring the direction the user asked for reads as working, where snapping back to the
|
|
21
|
+
// default reads as ignored. Non-numeric input means "nothing set here" and stays null so the
|
|
22
|
+
// caller can fall back. Values are snapped to the step so the label stays readable (0.75, not
|
|
23
|
+
// 0.7499999) and so the stepper can't drift off its own grid.
|
|
24
|
+
export function normalizeScrollSpeed(input: unknown): number | null {
|
|
25
|
+
if (typeof input !== "number" || !Number.isFinite(input)) return null;
|
|
26
|
+
const clamped = Math.min(TERMINAL_SCROLL_SPEED_MAX, Math.max(TERMINAL_SCROLL_SPEED_MIN, input));
|
|
27
|
+
const snapped = Math.round(clamped / TERMINAL_SCROLL_SPEED_STEP) * TERMINAL_SCROLL_SPEED_STEP;
|
|
28
|
+
// The multiply above reintroduces binary-float noise (0.30000000000000004); two decimals is
|
|
29
|
+
// more than the step needs and leaves the value safe to compare and to render.
|
|
30
|
+
return Math.round(snapped * 100) / 100;
|
|
31
|
+
}
|
|
@@ -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-BAMiAMZz.js";import"./chunk-WYO6CB5R-BlzOfotS-CoeWhGPT.js";import"./chunk-VAUOI2AC-CS9QJ4yz-DYmUgyge.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-B6_lajzj.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-BAMiAMZz.js";export{e as createArchitectureServices};
|