mulmoterminal 2.1.0 → 2.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 +116 -25
- package/bin/mulmoterminal.js +26 -7
- package/common/collectionPush.ts +26 -0
- package/common/dirChrome.ts +10 -0
- package/common/fileWriteChannel.ts +9 -0
- package/common/keymap.ts +22 -1
- package/common/notifyKinds.ts +26 -0
- package/common/notifySounds.ts +44 -0
- package/common/orderPriority.ts +20 -0
- package/common/pushKinds.ts +6 -2
- package/common/terminalClipboard.ts +52 -0
- package/common/terminalFontFamily.ts +79 -0
- package/common/voiceInputStatus.ts +21 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-BMVn5P4x.js → abnfDiagram-VRR7QNED-RjiYivmv-6R9yRbUC.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-C95zkDEU.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-BaVwq5YO.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-DzS5ABJE.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-O8HvKllF.js → blockDiagram-677ZJIJ3-BPuAJQRW-Cw9RkJxY.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-qV_LVJW3.js → c4Diagram-LMCZKHZV-C0LAqQso-CWltXMHP.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-BYaGw2yS.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-B9twQFRU.js → chunk-32BRIVSS-BRrYpgtb-DrrsZ2u5.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-OC_lubzj.js → chunk-52WLFC77-BJ-ss3Xr-WJMq_L8L.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-mCVH59ut.js → chunk-C7G6YPKG-BZEucKEL-BafXJkh7.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-CL8lofeg.js → chunk-EX3LRPZG-DLS6FBN1-Q8lsrJXs.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-BcVi6QV6.js → chunk-FWX5IMBZ-DHLSFw1H-Cgm5-zaI.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-D3M0sdaj.js → chunk-HOUHSVGY-Bhlt8hXJ-Cp5J5cOD.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-9pz8KjdB.js → chunk-ICXQ74PX-DwgHBX_g-CLdnp-d2.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-DsiY753w.js → chunk-MOJQB5TN-CMZRaeqt-BsIB0ZXR.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-DzD8vBv9.js → chunk-OGEWGWER-8Qy4a8b5-DlTM-RTP.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-B3EqHnC7.js → chunk-PUDLZKDR-DcrWQRYh-XujP_H1v.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-DAzhqKqQ.js → chunk-Q4XR5HBZ-ZXVGkG8Z-DhxnzVDy.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-sWPxsoge.js → chunk-V7JOEXUC-DmGdheTX-C-xPmCQA.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-C6T0-Q0n.js → chunk-VAUOI2AC-CS9QJ4yz-Cng5RPUP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-CzCW244T.js → chunk-VR4S4FIN-C6a91eNY-Bmb0moA3.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-BRhqiyoJ.js → chunk-WYO6CB5R-BlzOfotS-DmR4SyZL.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-2uN_ofIP.js → chunk-ZGVPDNZ5-BYwxNFTK-CD8nNM8_.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-EDJcEZLp.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-EDJcEZLp.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-Co-phwWx.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--rvLMXkNB.js → cynefinDiagram-TSTJHNR4-DHA9iPo--pv1DCSfy.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-CGanW7aY.js → dagre-VKFMJZFB--oJKqXBZ-BPcGEiSl.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-BcrxMiAD.js → diagram-FQU43EPY-CcJCB9bG-CCOrHV0U.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-4PTH1NOO.js → diagram-G47NLZAW-C_o-WGG1-Crm_IB8Y.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--DLVaK2vZ.js → diagram-NH7WQ7WH-CXJCYvY--UcH8gkTE.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-C0axta9a.js → diagram-OA4YK3LP-BMzeJ87A-7C9Me5sS.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-3zfR6xo1.js → diagram-WEI45ONY-D_93NKqo-BBztQ9hK.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-Bfd3ExQu.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-TkJNOqEw.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-BGq_5dj0.js → erDiagram-Q63AITRT-CiABnA0s-DS2blJuc.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-D-dhMOz8.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-KiTeh-IZ.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-z8Owlyg6.js → ganttDiagram-NO4QXBWP-DQZvdFo1-ZB_lhurA.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-Cd1rBWSG.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-BA-6Yy_K.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-Vm6I27hg.js} +1 -1
- package/dist/assets/index-BP05wTLW.js +618 -0
- package/dist/assets/index-CGwcisib.css +1 -0
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-Dic4-0_Y.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-BW36KQ8e.js → infoDiagram-FWYZ7A6U-7UnoB5AP-Cr34YkAb.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-DrQDG1os.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-at4P1zMV.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-CtJ0D_mQ.js → journeyDiagram-5HDEW3XC-c5xIah9o-D4ZV57k7.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-bm-RqJor.js → kanban-definition-HUTT4EX6-Cnt6loYD-BiJ3JUjX.js} +1 -1
- package/dist/assets/{lib-BmwIEKvb.js → lib-Du_IVDkN.js} +1 -1
- package/dist/assets/{line-D7ziSjKi-DrYnqDhF.js → line-D7ziSjKi-CPM0FMWw.js} +1 -1
- package/dist/assets/{marp-gEV1mQM4.js → marp-BBre1w8X.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-iymJ5XTw.js → mermaid-parser.core-DxEa8E3F-CO4zEYzL.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-CEFC0tc3.js → mermaid.core-V0OYwIz3-CzPQxhwu.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-maxHe_oT.js → mindmap-definition-LN4V7U3C-CexN3O6L-Nez_xicT.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-BBywyu3N.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-CrLCH9dK.js → pegDiagram-2B236MQR-BiM4G0cW-0AEyhHwQ.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-CoicOL1H.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-DVbko3rP.js → pieDiagram-ENE6RG2P-B_fBBS-2-DXBEfgPl.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-DUwLLJXe.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-dZ16XJ8-.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-BeFXvO0F.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-BZY5Z4oN.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-Dubi7PGc.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-wnb7FBOh.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-P7vJjL8M.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-t4pt5DRk.js → railroadDiagram-RFXS5EU6--lNIlG64-DHNapLB9.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-DJVGeSnt.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-C-9SVxpD.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-2m-PGq5V.js → sankeyDiagram-HTMAVEWB-DlutaXDv-Cv-mFz76.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-DSwO6_CW.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-BR0sUcBe.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-DuBsukdi.js → stateDiagram-2N3HPSRC-jf_dXUEU-DjqY9a1u.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-DjKerI1O.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-BwT2NtP8.js → swimlanes-5IMT3BWC-CYjtALQH-DV8Ha8XJ.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-Dm5TS_6E.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-B7uglxWU.js → timeline-definition-FHXFAJF6-DYJ4oUm8-D2oq5bFn.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-H3pflPYY.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-CIoNkikG.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-Bi97b0Q1.js → vennDiagram-L72KCM5P-CdKAHoek-I-wP-h9k.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-JQdCXyum.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-DuE3v_zR.js → wardleyDiagram-EHGQE667-BF6c4_CW-BkrH7vLX.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-B0CJGTzS.js → xychartDiagram-FW5EYKEG-CGiKngj7-DMYCP46V.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -8
- package/server/agents/claude-args.ts +7 -0
- package/server/backends/calendarPush.ts +81 -0
- package/server/backends/calendarPushResult.ts +44 -0
- package/server/backends/remoteHost/googleCalendar.spec.ts +7 -1
- package/server/backends/whisper.ts +3 -5
- package/server/config/app-config.ts +56 -0
- package/server/config/config-body.ts +12 -1
- package/server/config/config-routes.ts +35 -4
- package/server/config/config-schema.ts +66 -0
- package/server/config/dir-config.ts +75 -19
- package/server/config/sound-presets.ts +96 -0
- package/server/files/atomic-write.ts +1 -1
- package/server/files/backup-store.ts +101 -0
- package/server/files/files-browse.ts +85 -5
- package/server/files/tool-writes.ts +20 -0
- package/server/infra/sandbox.ts +7 -0
- package/server/routes/app-routes.ts +11 -2
- package/server/routes/dir-routes.ts +16 -5
- package/server/routes/hook-routes.ts +19 -4
- package/server/session/activity-hook.ts +23 -4
- package/server/session/pty-spawn.ts +2 -2
- package/server/session/spawn-claude.ts +2 -1
- package/server/skills/mulmoterminal-config/SKILL.md +37 -3
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-JfeahpZ_.js +0 -1
- package/dist/assets/channel-7wUqSdoX-C4JGl6VC.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-DtJISQUb.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-DtJISQUb.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-DB6705af.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-Bl19zj9_.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-DdIGCbvn.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-BnZW4EXW.js +0 -1
- package/dist/assets/index-DbKckN7Z.js +0 -618
- package/dist/assets/index-u-r21U2t.css +0 -1
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-CrJfKOA1.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-mf_ng5Fx.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-CSySnxGv.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-DfA0aimG.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-T35PQ5gg.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-DavwfL2m.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-Yk8Oj2bi.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DOPPkkTw.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-BuaQTnnn.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-CVMbYKXX.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DSRzdn6E.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-pjqGjUmo.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-_qoOv9rX.js +0 -1
package/README.md
CHANGED
|
@@ -111,11 +111,23 @@ dotfiles are server-only. The 45 extensions both sides agree on live in
|
|
|
111
111
|
|
|
112
112
|
## Install & run
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
|
|
115
|
+
|
|
116
|
+
| | Tool | What it gives you | Install |
|
|
117
|
+
| --- | --- | --- | --- |
|
|
118
|
+
| **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 |
|
|
119
|
+
| **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) |
|
|
120
|
+
| **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` |
|
|
121
|
+
| 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) |
|
|
122
|
+
| Optional | `codex` | [Codex sessions](#agents-claude--codex) in a cell, alongside Claude | `npm i -g @openai/codex` |
|
|
123
|
+
| Optional | `docker` | the experimental [Docker sandbox](#docker-sandbox-experimental-single-view) | [docs.docker.com](https://docs.docker.com/get-started/get-docker/) |
|
|
124
|
+
| 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` |
|
|
125
|
+
| Optional | `ollama` | [`claude-ollama`](https://receptron.github.io/mulmoterminal/guide/en/claude-ollama.html) — Claude Code against a fully local model | [ollama.com/download](https://ollama.com/download) |
|
|
126
|
+
|
|
127
|
+
The server starts without any of the non-required rows; you just lose that row's feature,
|
|
128
|
+
and the header/panel for it says so. `git` and `gh` are marked required because losing them
|
|
129
|
+
costs whole views rather than one button. `npx mulmoterminal init` (below) reports which of
|
|
130
|
+
these it can find.
|
|
119
131
|
|
|
120
132
|
```bash
|
|
121
133
|
npx mulmoterminal # start on http://localhost:34567 and open the browser
|
|
@@ -124,8 +136,8 @@ npm install -g mulmoterminal
|
|
|
124
136
|
mulmoterminal
|
|
125
137
|
```
|
|
126
138
|
|
|
127
|
-
**First-run setup (optional).** `npx mulmoterminal init` checks your environment (Node ≥ 22.9
|
|
128
|
-
|
|
139
|
+
**First-run setup (optional).** `npx mulmoterminal init` checks your environment (Node ≥ 22.9
|
|
140
|
+
and every CLI in the table above), seeds the launcher's **directory
|
|
129
141
|
presets** from the projects in your Claude Code history, and writes `~/.mulmoterminal/config.json`.
|
|
130
142
|
It's **idempotent** — re-run it any time to refresh the presets; it overwrites the managed parts
|
|
131
143
|
and keeps your other settings. When `claude` is installed it can hand off to the
|
|
@@ -439,7 +451,9 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
439
451
|
| Field | Meaning |
|
|
440
452
|
| ------------ | ------- |
|
|
441
453
|
| `cwdPresets` | Quick-pick directories offered when launching a terminal. |
|
|
442
|
-
| `soundFile` | Absolute path to a custom **attention sound
|
|
454
|
+
| `soundFile` | Absolute path to a custom **attention sound**, the fallback for every kind. Empty/unset uses the built-in synthesized chime. |
|
|
455
|
+
| `soundKinds` | Which moments beep — see [Notification sounds](#notification-sounds). Defaults to `["finished","waiting"]`; the other kinds are opt-in. |
|
|
456
|
+
| `sounds` | Per-kind sound: `{ "waiting": "preset:coin" }`. A `preset:<id>` reference or an absolute path; a kind with no entry falls back to `soundFile`. |
|
|
443
457
|
| `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates (via your `gh` login). |
|
|
444
458
|
| `launchers` | `{ label, command }` entries offered in a grid cell's launcher besides Claude — a plain shell, `codex`, any interactive command. |
|
|
445
459
|
| `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. |
|
|
@@ -452,6 +466,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
452
466
|
| `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). |
|
|
453
467
|
| `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). |
|
|
454
468
|
| `prWorkdirFooter` | Ends the body of a PR **⧉ Open PR** creates 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. **On by default**; set `false` to opt out — read from the file per PR, so no restart is needed (there is no Settings control for it). Only applied to PRs this app creates (pressing the button again on an existing PR never re-appends). |
|
|
469
|
+
| `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). |
|
|
455
470
|
|
|
456
471
|
#### Header buttons
|
|
457
472
|
|
|
@@ -470,12 +485,46 @@ there's no open PR) / `pickFile: true` (OS file dialog → insert the path).
|
|
|
470
485
|
visibility. The `/mulmoterminal-config` skill writes a valid config interactively; per-dir buttons
|
|
471
486
|
merge over the global ones by `id`.
|
|
472
487
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
488
|
+
### Notification sounds
|
|
489
|
+
|
|
490
|
+
Six moments can beep, each with its own sound and its own on/off switch. Running many
|
|
491
|
+
agents at once is what turns notifications into noise, so **only the first two are on by
|
|
492
|
+
default** — the rest are opt-in from Settings.
|
|
493
|
+
|
|
494
|
+
| Kind | When | Default |
|
|
495
|
+
| --- | --- | --- |
|
|
496
|
+
| `finished` | the turn ended and the output is unread | **on** |
|
|
497
|
+
| `waiting` | it stopped to ask — a permission prompt or a question | **on** |
|
|
498
|
+
| `command-done` | a **Run cell's** command exited 0 | off |
|
|
499
|
+
| `command-failed` | a **Run cell's** command exited non-zero, or never started | off |
|
|
500
|
+
| `session-exited` | a session's terminal ended — **including when you close the cell yourself** | off |
|
|
501
|
+
| `pr-ci-failed` | a directory's PR went red. Only seen **while the roster is on screen**, since that is what polls the phase | off |
|
|
502
|
+
|
|
503
|
+
A **Run cell** is the one-shot cell a `script.json` entry or a `run:"shell"` header button
|
|
504
|
+
opens — not a shell launcher cell. A launcher runs an interactive shell that stays alive, so
|
|
505
|
+
nothing marks where one command inside it ended; only the one-shot cell reports an exit code.
|
|
506
|
+
|
|
507
|
+
`finished` and `waiting` reach the phone too (`pushKinds`); the other four are seen only in
|
|
508
|
+
the browser — a Run PTY never enters the session registry, and a PR phase is something the
|
|
509
|
+
page polls — so Web Push cannot raise them.
|
|
510
|
+
|
|
511
|
+
**What each one plays.** The default chime is generated with the Web Audio API — **no audio
|
|
512
|
+
file is bundled**, so the npm package stays light and has no media-licensing concerns. Beyond
|
|
513
|
+
it there are two options:
|
|
514
|
+
|
|
515
|
+
- **Presets** — seven sounds hosted in the [ownplate](https://github.com/Nakajima-Foundation/ownplate)
|
|
516
|
+
repo (MIT), referenced as `preset:<id>`: `chime` `coin` `cheep` `door` `gong` `magic` `meow`.
|
|
517
|
+
The first play downloads one into `~/.mulmoterminal/sounds/`; every later play reads that
|
|
518
|
+
file, so a preset keeps working offline. A failed download is not remembered as one — you get
|
|
519
|
+
the chime that time and the next play retries. That holds on both sides: the server caches no
|
|
520
|
+
failure, and it answers **503** (not 404) for a preset it could not fetch, because the browser
|
|
521
|
+
remembers a 404 for the life of the page and only retries a 5xx.
|
|
522
|
+
- **Your own file** — an absolute path, per kind in `sounds` or as the all-kind `soundFile`.
|
|
523
|
+
|
|
524
|
+
Resolution per kind, nearest first: the session directory's `sounds[kind]`, its `sound`, your
|
|
525
|
+
`sounds[kind]`, your `soundFile`, then the chime. The server streams whichever applies at
|
|
526
|
+
`GET /api/sound?kind=` / `GET /api/dir-sound?cwd=&kind=`, and the client falls back to the
|
|
527
|
+
chime if it's missing or not audio.
|
|
479
528
|
|
|
480
529
|
**Web Push on task finish.** Enable `pushEnabled` in Settings to have the server send a
|
|
481
530
|
push (title = the project dir, body = the last prompt) to your registered devices each
|
|
@@ -529,7 +578,10 @@ malformed file is ignored.
|
|
|
529
578
|
"theme": "nord", // terminal palette: midnight | nord | daylight | solarized
|
|
530
579
|
"colors": { "background": "#190a23", "cursor": "#ff2e63" }, // per-key palette overrides
|
|
531
580
|
"fontSize": 16, // terminal font size in px (8–32); overrides Settings
|
|
532
|
-
"
|
|
581
|
+
"fontFamily": "'Cica', monospace", // terminal font stack; overrides the global config
|
|
582
|
+
"orderPriority": 10, // rank in the grid's "priority" ordering (lowest first)
|
|
583
|
+
"sound": "./.mulmoterminal/alert.mp3", // attention sound, RELATIVE to this directory
|
|
584
|
+
"sounds": { "command-failed": "preset:gong" } // per-notification-kind override
|
|
533
585
|
}
|
|
534
586
|
```
|
|
535
587
|
|
|
@@ -550,12 +602,20 @@ malformed file is ignored.
|
|
|
550
602
|
| `theme` | xterm palette for terminals in this directory (one of the built-in theme ids). |
|
|
551
603
|
| `colors` | Per-key xterm palette overrides applied on top of `theme` (or the app theme when `theme` is unset). Keys are xterm `ITheme` names (`background`, `foreground`, `cursor`, `selectionBackground`, the 16 ANSI colors, …); values are hex (`#rgb` / `#rrggbb` / `#rrggbbaa`). Unknown keys / bad values are dropped. |
|
|
552
604
|
| `fontSize` | Terminal font size in px for this directory (8–32), overriding the Settings value. A size outside the range is clamped; a non-number is ignored. Changing it re-fits the terminal, so the PTY learns the new width — unlike browser zoom, which leaves the two disagreeing. |
|
|
553
|
-
| `
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
605
|
+
| `orderPriority` | This directory's rank in the grid's **priority** ordering — the third mode on the toolbar's ordering button, next to auto (attention-first) and manual (the move buttons). Any integer, **lowest first**; negatives are allowed. Directories that set nothing sort last, keeping their existing order, so adding the key to one project doesn't shuffle the rest. Only the priority mode reads it. |
|
|
606
|
+
| `fontFamily` | CSS font-family stack for this directory's terminals, overriding the global `fontFamily`. Use the names as your OS lists them (`"'Cica', 'MS Gothic', monospace"`). An unusable stack is ignored whole rather than half-applied; `monospace` is appended if you name no generic family. Prefer fonts whose fullwidth glyphs are exactly twice the Latin width, or box-drawing frames tear. |
|
|
607
|
+
| `sound` | Attention sound for this directory's sessions, a path **relative to the directory** (served at `GET /api/dir-sound`). The fallback for every kind. |
|
|
608
|
+
| `sounds` | Per-kind override of `sound`: `{ "command-failed": "preset:gong" }`. Each value is a `preset:<id>` or a directory-relative path, under the same confinement. |
|
|
609
|
+
| `addDirs` | Extra directories this project's Claude sessions may read and edit — the terminal-side equivalent of opening several folders in one VS Code workspace, via Claude Code's `--add-dir`. Relative entries resolve against **this file's directory** (`"../shared-lib"`), a path that doesn't exist is dropped, max 16. In the Docker sandbox each one is bind-mounted too, so the grant is real inside the container — which widens the sandbox on purpose. Claude only: codex has no equivalent flag and ignores the key. |
|
|
610
|
+
|
|
611
|
+
**Security.** `sound` and every `sounds` entry are directory-relative paths only — absolute
|
|
612
|
+
paths and any `../` that escapes the directory are rejected, and the path is never taken from the
|
|
557
613
|
HTTP request, so an opened project can't point the player at arbitrary files.
|
|
558
|
-
|
|
614
|
+
**When changes take effect.** A write made *through Claude's tools* — which includes the
|
|
615
|
+
`mulmoterminal-config` skill — applies **live**: the tool hook that reports the write doubles
|
|
616
|
+
as the reload signal, so colors, palette, font size and grid order update without reopening
|
|
617
|
+
anything. There is no filesystem watcher, so an edit made **outside** a session (your own
|
|
618
|
+
editor) is picked up when the terminal is next opened.
|
|
559
619
|
|
|
560
620
|
---
|
|
561
621
|
|
|
@@ -700,10 +760,33 @@ tree; clicking a file opens it in a **CodeMirror** editor (Markdown / JS-TS / JS
|
|
|
700
760
|
highlighting, everything else as plain text). Markdown files get a **Preview** toggle
|
|
701
761
|
that renders via the server's sandboxed `…/md` HTML. **Save** (or ⌘/Ctrl-S) writes back.
|
|
702
762
|
|
|
763
|
+
**Beside an enlarged terminal, not only full-screen.** Expand a grid cell (**⤢**) and its
|
|
764
|
+
header gains a **folder** toggle that splits the enlarged area in two: terminal on the left,
|
|
765
|
+
the same explorer + editor on the right, rooted at that cell's directory. Drag the divider
|
|
766
|
+
(or focus it and use ←/→, Home, End) to resize — the terminal keeps a floor, so a squeeze
|
|
767
|
+
shrinks the pane rather than reflowing xterm into garbage. It works in both zoomed layouts
|
|
768
|
+
(cockpit roster and thumbnail filmstrip), the pane re-roots as you walk the zoom between
|
|
769
|
+
terminals, and whether it's open plus how wide it is are remembered per browser.
|
|
770
|
+
|
|
703
771
|
All reads and writes go through `GET/PUT /api/files/browse/*?cwd=&path=`, and every
|
|
704
772
|
`path` is **contained within the project root** (server-side) — `..`/absolute escapes
|
|
705
773
|
are rejected for reads and writes alike, so editing can't reach outside the directory
|
|
706
|
-
the terminal is pointed at.
|
|
774
|
+
the terminal is pointed at. A save sends the version the file had when it was opened, so
|
|
775
|
+
it is **refused (409) rather than silently overwriting** an agent that edited the same
|
|
776
|
+
file meanwhile; the editor then offers to reload or to overwrite deliberately.
|
|
777
|
+
|
|
778
|
+
You usually hear about it before that. An open file that changes on disk is picked up from
|
|
779
|
+
Claude's own write hook (immediately) and from a 30-second version check (which catches Codex,
|
|
780
|
+
git, builds and other editors too). A **clean** buffer just takes the new content — the pane
|
|
781
|
+
reads as a live view — while a **dirty** one raises the same banner rather than choosing for you.
|
|
782
|
+
|
|
783
|
+
**Leaving an open file saves it** — switching files, moving the enlargement to another
|
|
784
|
+
terminal, closing the pane, navigating away. No dialog interrupts you mid-flow, because
|
|
785
|
+
opening a file, and replacing one, keep a copy under `~/.mulmoterminal/backups/` — **three
|
|
786
|
+
generations per file**, outside the project so they never reach `git status` or the agent's
|
|
787
|
+
view of its own repo. A parting save that loses the version race banks your version there
|
|
788
|
+
instead of overwriting the other writer. Re-opening unchanged content doesn't rotate one in, and a backup that
|
|
789
|
+
can't be written never blocks the read or the save it was taken for.
|
|
707
790
|
|
|
708
791
|
---
|
|
709
792
|
|
|
@@ -838,7 +921,10 @@ Favorited collections get their own toolbar buttons.
|
|
|
838
921
|
- **Notifications** (🔔) — a toolbar bell with an unread badge and a dropdown of active
|
|
839
922
|
notifications; click a row to jump to its session.
|
|
840
923
|
- **Voice input** — dictate a prompt via on-device Whisper (`POST /api/transcribe`, macOS
|
|
841
|
-
only; the model downloads on first use).
|
|
924
|
+
only; the model downloads on first use). Settings picks **the language you dictate in**
|
|
925
|
+
(per browser): your browser's, whisper's own per-clip detection, or a fixed one. Worth
|
|
926
|
+
setting — speech in a language the mic is not expecting comes back *translated* into the
|
|
927
|
+
one it is, so an English browser silently turned Japanese dictation into English.
|
|
842
928
|
- **Remote host** — link MulmoTerminal to the companion phone client (Google sign-in) to
|
|
843
929
|
watch and start sessions from your phone.
|
|
844
930
|
- **Themes** — four terminal palettes (midnight / nord / daylight / solarized), your pick
|
|
@@ -847,6 +933,11 @@ Favorited collections get their own toolbar buttons.
|
|
|
847
933
|
**Option** is treated as Meta so Claude's Alt-key bindings work. If your Claude Code is
|
|
848
934
|
rebound so Enter and Shift+Enter behave backwards, flip them with
|
|
849
935
|
[`terminalSubmit`](https://receptron.github.io/mulmoterminal/guide/en/config.html#terminal-submit).
|
|
936
|
+
- **No accidental page zoom** — `Ctrl`+wheel and a trackpad pinch would rescale the whole
|
|
937
|
+
page and drag the layout and the terminal's fit along with it, so both are ignored.
|
|
938
|
+
Keyboard zoom (`Cmd`/`Ctrl` `+` / `-`) still works when you mean it, and a phone's finger
|
|
939
|
+
pinch is untouched. To make terminal text bigger for real, use the font size in Settings
|
|
940
|
+
(or a directory's `fontSize`) — that re-fits the PTY instead of leaving it disagreeing.
|
|
850
941
|
|
|
851
942
|
---
|
|
852
943
|
|
|
@@ -1018,7 +1109,7 @@ same-origin-guarded.
|
|
|
1018
1109
|
| -------- | ------- |
|
|
1019
1110
|
| `GET /api/wiki` (`?slug=`) · `/api/wiki/graph` · `/api/wiki/lint` | Read-only wiki index / page / graph / lint. |
|
|
1020
1111
|
| `GET /api/collections/…` · `/api/feeds` · `GET\|PUT /api/shortcuts` | Collections browser, feeds, favorites (see `docs/collection-plugin-integration.md`). |
|
|
1021
|
-
| `GET /api/files/browse/{list,text,md}` · `PUT /api/files/browse/write` | File tree / read / Markdown-render / write (contained within the project root). |
|
|
1112
|
+
| `GET /api/files/browse/{list,text,version,md}` · `PUT /api/files/browse/{write,backup}` | File tree / read / Markdown-render / write (contained within the project root). `text` answers `{ text, version }`; `write` takes `{ text, baseVersion }` (`null` = expecting to create it) and answers **409** with the version now on disk if the file changed since — so a save can't silently overwrite the agent that edits the same files. `version` answers that token alone, for the editor's periodic check. `backup` banks a buffer the editor is about to discard. |
|
|
1022
1113
|
| `GET /api/files/raw?path=` | Raw asset bytes (workspace-rooted). |
|
|
1023
1114
|
|
|
1024
1115
|
**GUI panel / plugins / MCP**
|
|
@@ -1035,8 +1126,8 @@ same-origin-guarded.
|
|
|
1035
1126
|
|
|
1036
1127
|
| Endpoint | Purpose |
|
|
1037
1128
|
| -------- | ------- |
|
|
1038
|
-
| `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `quickCommands`, `userMcpServers`, `providers`). |
|
|
1039
|
-
| `GET /api/sound
|
|
1129
|
+
| `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `soundKinds`, `sounds`, `prRepos`, `launchers`, `quickCommands`, `userMcpServers`, `providers`). |
|
|
1130
|
+
| `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. |
|
|
1040
1131
|
| `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. |
|
|
1041
1132
|
| `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
|
|
1042
1133
|
| `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -76,6 +76,31 @@ function claudeInstalled() {
|
|
|
76
76
|
return hasCommand("claude");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// PATH tools the app shells out to; mirrors the requirements table in README.md. `required`
|
|
80
|
+
// ones back the core grid — without them a developer loses whole views rather than one
|
|
81
|
+
// feature — so a miss is an ✗, not an ○.
|
|
82
|
+
const PATH_TOOLS = [
|
|
83
|
+
{ cmd: "git", versionArg: "--version", required: true, why: "worktrees, per-cell branch/diff, PR footer", hint: "brew install git · apt install git" },
|
|
84
|
+
{ cmd: "gh", versionArg: "--version", required: true, why: "PRs & Issues view + one-click PRs", hint: "https://cli.github.com (then: gh auth login)" },
|
|
85
|
+
{ cmd: "tmux", versionArg: "-V", required: false, why: "sessions survive a restart", hint: "brew install tmux · apt install tmux" },
|
|
86
|
+
{ cmd: "codex", versionArg: "--version", required: false, why: "run OpenAI Codex as an agent", hint: "npm install -g @openai/codex" },
|
|
87
|
+
{ cmd: "docker", versionArg: "--version", required: false, why: "the experimental Docker sandbox", hint: "https://docs.docker.com/get-started/get-docker/" },
|
|
88
|
+
{
|
|
89
|
+
cmd: "ffmpeg",
|
|
90
|
+
versionArg: "-version",
|
|
91
|
+
required: false,
|
|
92
|
+
why: "video rendering in the mulmo-script panel",
|
|
93
|
+
hint: "brew install ffmpeg · apt install ffmpeg",
|
|
94
|
+
},
|
|
95
|
+
{ cmd: "ollama", versionArg: "--version", required: false, why: "a fully local model via claude-ollama", hint: "https://ollama.com/download" },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
function toolCheckLine({ cmd, versionArg, required, why, hint }) {
|
|
99
|
+
if (hasCommand(cmd, versionArg)) return ` ✓ ${cmd} — ${why}`;
|
|
100
|
+
const head = required ? ` ✗ ${cmd} — not found, needed for ${why}` : ` ○ ${cmd} — optional (${why})`;
|
|
101
|
+
return `${head}\n → ${hint}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
function promptYesNo(question) {
|
|
80
105
|
return new Promise((res) => {
|
|
81
106
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -103,13 +128,7 @@ async function runInit(initArgs) {
|
|
|
103
128
|
console.log(" → npm install -g @anthropic-ai/claude-code (then run `claude` and log in)");
|
|
104
129
|
}
|
|
105
130
|
|
|
106
|
-
|
|
107
|
-
["tmux", "-V", "sessions survive a restart", "brew install tmux · apt install tmux"],
|
|
108
|
-
["gh", "--version", "PRs & Issues view + one-click PRs", "https://cli.github.com (then: gh auth login)"],
|
|
109
|
-
["codex", "--version", "run OpenAI Codex as an agent", "npm install -g @openai/codex"],
|
|
110
|
-
]) {
|
|
111
|
-
console.log(hasCommand(cmd, versionArg) ? ` ✓ ${cmd} — ${why}` : ` ○ ${cmd} — optional (${why})\n → ${hint}`);
|
|
112
|
-
}
|
|
131
|
+
PATH_TOOLS.forEach((tool) => console.log(toolCheckLine(tool)));
|
|
113
132
|
|
|
114
133
|
// Config half: derive working-dir presets from Claude history + write config.json.
|
|
115
134
|
console.log("");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The `POST /api/collections/:slug/calendar-push` response. The server builds it from the
|
|
2
|
+
// engine's outcome; the collection view reads it to say what the click did — both sides
|
|
3
|
+
// decide from it, so it lives here.
|
|
4
|
+
//
|
|
5
|
+
// The shape (and the route's path) mirrors MulmoClaude's `CollectionPushBody`
|
|
6
|
+
// (server/api/routes/collectionCalendarPush.ts) so the two hosts over the shared workspace
|
|
7
|
+
// answer the same plugin identically. Re-stated rather than imported: the plugin ships its
|
|
8
|
+
// `CollectionPushResult` from `@mulmoclaude/collection-plugin/vue`, and the server has no
|
|
9
|
+
// business pulling a Vue package in to describe its own response.
|
|
10
|
+
|
|
11
|
+
export interface CollectionPushResult {
|
|
12
|
+
/** Always true — "the push ran", not "records moved". The plugin's own type widens this
|
|
13
|
+
* to `boolean` but never reads it, and MulmoClaude pins it to `true`; a refusal is told
|
|
14
|
+
* through `errors`, so the two hosts stay identical on a field neither of them uses. */
|
|
15
|
+
pushed: true;
|
|
16
|
+
created: number;
|
|
17
|
+
updated: number;
|
|
18
|
+
/** Edited on both sides; skipped so neither version is destroyed. */
|
|
19
|
+
conflicts: number;
|
|
20
|
+
/** Deleted locally. Reported only — a push never deletes in Google. */
|
|
21
|
+
localDeletes: number;
|
|
22
|
+
/** Records that could not be pushed as they stand, each with its reason. */
|
|
23
|
+
skipped: string[];
|
|
24
|
+
/** Why the push as a whole did not do what was asked. */
|
|
25
|
+
errors: string[];
|
|
26
|
+
}
|
package/common/dirChrome.ts
CHANGED
|
@@ -21,6 +21,14 @@ export interface DirChrome {
|
|
|
21
21
|
// Unlike the colors above, this changes the cell metrics — every path that applies it has
|
|
22
22
|
// to re-fit and push the new cols/rows to the PTY, or the grid drifts from the canvas.
|
|
23
23
|
fontSize: number | null;
|
|
24
|
+
// The CSS font-family stack for terminals opened here, or null to use the global setting.
|
|
25
|
+
// Changes the cell metrics for the same reason `fontSize` does — a different face has a
|
|
26
|
+
// different advance width — so it re-fits on the same path.
|
|
27
|
+
fontFamily: string | null;
|
|
28
|
+
// Where this directory's cells sit in the grid's "priority" sort order — ascending, and
|
|
29
|
+
// null (unset) sorts last so adding it to one directory doesn't displace every other cell.
|
|
30
|
+
// Only that one sort mode reads it; "auto" and "manual" ignore it entirely.
|
|
31
|
+
orderPriority: number | null;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
// "Nothing configured" — the base every DirConfig/PublicDirConfig empty spreads, so adding
|
|
@@ -37,4 +45,6 @@ export const EMPTY_DIR_CHROME: Readonly<DirChrome> = {
|
|
|
37
45
|
dotColor: null,
|
|
38
46
|
buttonColor: null,
|
|
39
47
|
fontSize: null,
|
|
48
|
+
fontFamily: null,
|
|
49
|
+
orderPriority: null,
|
|
40
50
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The pub/sub channel carrying "an agent just wrote this file". Both sides decide from it —
|
|
2
|
+
// the server publishes, the editor listens to know its open file moved under it — so the name
|
|
3
|
+
// and the payload shape live here rather than as a string literal on each side.
|
|
4
|
+
export const FILE_WRITE_CHANNEL = "file-write";
|
|
5
|
+
|
|
6
|
+
/** Absolute path, as the server resolved it. */
|
|
7
|
+
export interface FileWriteEvent {
|
|
8
|
+
file: string;
|
|
9
|
+
}
|
package/common/keymap.ts
CHANGED
|
@@ -10,12 +10,33 @@
|
|
|
10
10
|
// "keymap": { "zoom-next": "PageDown", "zoom-prev": "Shift+PageUp" }
|
|
11
11
|
|
|
12
12
|
// Actions a key can be bound to. Adding one here is all it takes for the config to accept it.
|
|
13
|
-
export const KEYMAP_ACTIONS = [
|
|
13
|
+
export const KEYMAP_ACTIONS = [
|
|
14
|
+
"zoom-toggle",
|
|
15
|
+
"zoom-next",
|
|
16
|
+
"zoom-prev",
|
|
17
|
+
"next-attention",
|
|
18
|
+
"terminal-new",
|
|
19
|
+
"terminal-new-adjacent",
|
|
20
|
+
"terminal-close",
|
|
21
|
+
"copy",
|
|
22
|
+
"paste",
|
|
23
|
+
] as const;
|
|
14
24
|
export type KeymapAction = (typeof KEYMAP_ACTIONS)[number];
|
|
15
25
|
|
|
16
26
|
export const isKeymapAction = (value: unknown): value is KeymapAction => typeof value === "string" && (KEYMAP_ACTIONS as readonly string[]).includes(value);
|
|
17
27
|
|
|
18
28
|
// action -> binding string. Absent action = unbound = that shortcut does nothing.
|
|
29
|
+
// Actions the GRID's key handler must never claim, because they are decided inside the terminal
|
|
30
|
+
// instead (see terminalClipboard.ts). One `keymap` block stays the user's single place to bind a
|
|
31
|
+
// key; only the dispatch differs, and it has to:
|
|
32
|
+
//
|
|
33
|
+
// - The grid handler ends every match with preventDefault(). For `paste` that is fatal — the
|
|
34
|
+
// browser's own paste is what actually inserts the text, and cancelling the keydown cancels
|
|
35
|
+
// it. xterm implements paste as a `paste` DOM listener, not a key binding.
|
|
36
|
+
// - `copy` must fall through to the terminal when there is NO selection, so Ctrl+C still sends
|
|
37
|
+
// ^C. A handler that has already swallowed the key cannot change its mind.
|
|
38
|
+
export const TERMINAL_SCOPED_ACTIONS: readonly KeymapAction[] = ["copy", "paste"];
|
|
39
|
+
|
|
19
40
|
export type Keymap = Partial<Record<KeymapAction, string>>;
|
|
20
41
|
|
|
21
42
|
// A parsed binding. `key` is matched against `KeyboardEvent.key` exactly as the browser
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The moments MulmoTerminal can notify you about. The attention sound reads this whole
|
|
2
|
+
// list; Web Push reads the PUSH_KINDS subset in pushKinds.ts, because a phone can only be
|
|
3
|
+
// told about what the SERVER observes — the four kinds added below are seen in the browser.
|
|
4
|
+
|
|
5
|
+
// Every kind that exists.
|
|
6
|
+
// finished — the turn ended, output is waiting to be reviewed.
|
|
7
|
+
// waiting — the agent is blocked on input (a permission prompt or a question).
|
|
8
|
+
// command-done — a Run cell's command exited 0.
|
|
9
|
+
// command-failed — a Run cell's command exited non-zero.
|
|
10
|
+
// session-exited — a session's PTY ended. Closing a cell yourself goes through the same
|
|
11
|
+
// path, so this one fires on a deliberate close too.
|
|
12
|
+
// pr-ci-failed — a directory's PR phase became ci-failing. The phase poll runs only
|
|
13
|
+
// while the roster is on screen, so a failure that lands while you are
|
|
14
|
+
// in another view is not seen.
|
|
15
|
+
export const NOTIFY_KINDS = ["finished", "waiting", "command-done", "command-failed", "session-exited", "pr-ci-failed"] as const;
|
|
16
|
+
|
|
17
|
+
export type NotifyKind = (typeof NOTIFY_KINDS)[number];
|
|
18
|
+
|
|
19
|
+
// What a config with no `soundKinds` gets. Deliberately a SEPARATE list from NOTIFY_KINDS,
|
|
20
|
+
// not a copy of it: a kind added later must NOT start beeping at people who never asked for
|
|
21
|
+
// it — the same rule DEFAULT_PUSH_KINDS documents, and the reason the four kinds above are
|
|
22
|
+
// absent here. Add a kind to NOTIFY_KINDS so it can be switched on, and leave it out of
|
|
23
|
+
// here so it stays opt-in.
|
|
24
|
+
export const DEFAULT_SOUND_KINDS: NotifyKind[] = ["finished", "waiting"];
|
|
25
|
+
|
|
26
|
+
export const isNotifyKind = (value: unknown): value is NotifyKind => NOTIFY_KINDS.some((kind) => kind === value);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The preset attention sounds, and how a configured sound names one.
|
|
2
|
+
//
|
|
3
|
+
// The audio lives in the ownplate repo (MIT, same org) rather than in this package, so an
|
|
4
|
+
// install doesn't carry ~380 KB of audio nobody may play. The server fetches a preset once
|
|
5
|
+
// into ~/.mulmoterminal/sounds/ and serves it from there afterwards, so it keeps working
|
|
6
|
+
// offline — see server/config/sound-presets.ts.
|
|
7
|
+
|
|
8
|
+
// Pinned to the commit that last touched these files (2022-03-30) rather than to a branch:
|
|
9
|
+
// a branch ref would let the bytes behind a cached preset change, and a user who picked
|
|
10
|
+
// "coin" would silently get something else on a machine that hadn't cached it yet.
|
|
11
|
+
export const SOUND_PRESET_COMMIT = "fb36eb8748b7f3d181d7fc0e366e01971a56ad2f";
|
|
12
|
+
export const SOUND_PRESET_BASE_URL = `https://raw.githubusercontent.com/Nakajima-Foundation/ownplate/${SOUND_PRESET_COMMIT}/public/`;
|
|
13
|
+
|
|
14
|
+
export interface SoundPreset {
|
|
15
|
+
id: string;
|
|
16
|
+
file: string;
|
|
17
|
+
label: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SOUND_PRESETS: readonly SoundPreset[] = [
|
|
21
|
+
{ id: "chime", file: "sound_default.mp3", label: "Chime" },
|
|
22
|
+
{ id: "coin", file: "sound_coin.mp3", label: "Coin" },
|
|
23
|
+
{ id: "cheep", file: "sound_cheep_cheep.mp3", label: "Cheep" },
|
|
24
|
+
{ id: "door", file: "sound_door_chime.mp3", label: "Door chime" },
|
|
25
|
+
{ id: "gong", file: "sound_gong.mp3", label: "Gong" },
|
|
26
|
+
{ id: "magic", file: "sound_magic.mp3", label: "Magic" },
|
|
27
|
+
{ id: "meow", file: "sound_meow.mp3", label: "Meow" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// A sound value is either a preset reference or a path to the user's own file. The prefix is
|
|
31
|
+
// what tells them apart, and it can't collide with a path: an absolute path starts with "/"
|
|
32
|
+
// (or a drive letter), and a relative one is rejected before it gets here.
|
|
33
|
+
const PRESET_PREFIX = "preset:";
|
|
34
|
+
|
|
35
|
+
export const presetRef = (id: string): string => `${PRESET_PREFIX}${id}`;
|
|
36
|
+
|
|
37
|
+
export const soundPresetById = (id: string): SoundPreset | null => SOUND_PRESETS.find((preset) => preset.id === id) ?? null;
|
|
38
|
+
|
|
39
|
+
/** The preset id in a `preset:<id>` value, or null when it names a file path or an unknown preset. */
|
|
40
|
+
export function parsePresetRef(value: string): string | null {
|
|
41
|
+
if (!value.startsWith(PRESET_PREFIX)) return null;
|
|
42
|
+
const id = value.slice(PRESET_PREFIX.length);
|
|
43
|
+
return soundPresetById(id) ? id : null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A directory's rank in the grid's "priority" sort order. Shared so the two boundaries that
|
|
2
|
+
// validate it — the server reading .mulmoterminal.json, and the client reading /api/dir-config —
|
|
3
|
+
// cannot disagree about what counts as a rank. They did briefly: one accepted any finite number
|
|
4
|
+
// while the other required an integer, so a fractional value would have sorted on one side and
|
|
5
|
+
// read as unset on the other.
|
|
6
|
+
//
|
|
7
|
+
// Integers only: a rank is an ordering, so 1.5 buys nothing and invites float-comparison
|
|
8
|
+
// surprises. Not range-limited, unlike a font size — every integer is a usable rank, and
|
|
9
|
+
// negatives are how a project sorts ahead of everything at 0.
|
|
10
|
+
//
|
|
11
|
+
// SAFE integers, because the strict half of the pair is `z.number().int()` and zod@4 reads that
|
|
12
|
+
// as safe-only. `Number.isInteger` alone accepts 2^53+1 and 1e300, which this would have taken
|
|
13
|
+
// while writableDirConfigSchema rejected them — the exact disagreement the paragraph above says
|
|
14
|
+
// this module exists to prevent, one boundary over. Past 2^53 an integer is not distinct from its
|
|
15
|
+
// neighbours anyway, so it cannot express a rank.
|
|
16
|
+
//
|
|
17
|
+
// null means "unset", which the sort reads as "after everything that declares a rank".
|
|
18
|
+
export function normalizeOrderPriority(input: unknown): number | null {
|
|
19
|
+
return typeof input === "number" && Number.isSafeInteger(input) ? input : null;
|
|
20
|
+
}
|
package/common/pushKinds.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// The kinds of Web Push a session can raise. The server decides which one a hook warrants and
|
|
2
2
|
// the settings UI offers them as checkboxes, so the list is a value both sides read.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import type { NotifyKind } from "./notifyKinds.js";
|
|
5
|
+
|
|
6
|
+
// Every kind that exists — a SUBSET of NOTIFY_KINDS, since a push can only report what the
|
|
7
|
+
// server itself observes. The rest of the notify kinds are browser-side signals (a Run cell's
|
|
8
|
+
// exit, a PR phase poll) that never reach the process holding the Firebase auth.
|
|
5
9
|
// finished — the turn ended, output is waiting to be reviewed.
|
|
6
10
|
// waiting — the agent is blocked on input (a permission prompt or a question), so answering
|
|
7
11
|
// from the phone unblocks real work. Fires once per prompt, which on a long task
|
|
8
12
|
// that asks repeatedly is what makes pushes feel frequent (#850).
|
|
9
|
-
export const PUSH_KINDS = ["finished", "waiting"] as const;
|
|
13
|
+
export const PUSH_KINDS = ["finished", "waiting"] as const satisfies readonly NotifyKind[];
|
|
10
14
|
|
|
11
15
|
export type PushKind = (typeof PUSH_KINDS)[number];
|
|
12
16
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Whether a keystroke should hand the terminal's clipboard work to the BROWSER, decided without
|
|
2
|
+
// touching the DOM so the rules are unit-testable on their own (same shape as `enterKeyOverride`
|
|
3
|
+
// in terminalSubmit.ts).
|
|
4
|
+
//
|
|
5
|
+
// The thing to understand before changing any of this: xterm already implements copy and paste.
|
|
6
|
+
// It listens for the `copy` and `paste` DOM events on its own element and textarea, writes the
|
|
7
|
+
// selection out, and brackets pasted text. Nothing here reads or writes a clipboard — which is
|
|
8
|
+
// also why no clipboard PERMISSION is involved, unlike `navigator.clipboard.readText()`.
|
|
9
|
+
//
|
|
10
|
+
// What is missing is only that the browser never fires those events: xterm's key handling turns
|
|
11
|
+
// Ctrl+C into ^C and cancels the keydown, so the platform's copy shortcut never happens. The one
|
|
12
|
+
// decision this module makes is when to stand back and let it.
|
|
13
|
+
import { actionForKey, type Keymap, type KeymapAction } from "./keymap.js";
|
|
14
|
+
|
|
15
|
+
// The structural shape of a keydown these rules need; a real KeyboardEvent satisfies it and so
|
|
16
|
+
// does a plain test object.
|
|
17
|
+
export interface ClipboardKeyEvent {
|
|
18
|
+
type: string;
|
|
19
|
+
key: string;
|
|
20
|
+
shiftKey: boolean;
|
|
21
|
+
altKey: boolean;
|
|
22
|
+
ctrlKey: boolean;
|
|
23
|
+
metaKey: boolean;
|
|
24
|
+
isComposing?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ClipboardAction = Extract<KeymapAction, "copy" | "paste">;
|
|
28
|
+
|
|
29
|
+
// `hasSelection` is the terminal's own answer, passed in rather than read here so this stays
|
|
30
|
+
// DOM-free.
|
|
31
|
+
//
|
|
32
|
+
// Returning an action means ONE thing to the caller: return false from xterm's custom key
|
|
33
|
+
// handler. xterm then skips its own translation and — critically — does NOT preventDefault, so
|
|
34
|
+
// the browser performs the copy or paste it was always going to, and xterm's own listeners see
|
|
35
|
+
// it. Verified in @xterm/xterm 6.0.0:
|
|
36
|
+
//
|
|
37
|
+
// if (this._customKeyEventHandler && false === this._customKeyEventHandler(e)) return false;
|
|
38
|
+
//
|
|
39
|
+
// null means "not ours" — the key goes to the terminal exactly as before.
|
|
40
|
+
export function clipboardActionFor(keymap: Keymap, e: ClipboardKeyEvent, hasSelection: boolean): ClipboardAction | null {
|
|
41
|
+
if (e.type !== "keydown") return null;
|
|
42
|
+
// An IME candidate list drives itself with ordinary keys; that keystroke belongs to the
|
|
43
|
+
// composition, never to us.
|
|
44
|
+
if (e.isComposing) return null;
|
|
45
|
+
const action = actionForKey(keymap, e);
|
|
46
|
+
if (action !== "copy" && action !== "paste") return null;
|
|
47
|
+
// Copy only when there is something to copy. This is what keeps Ctrl+C usable as INTERRUPT:
|
|
48
|
+
// with no selection the key is not ours, so the terminal sends ^C exactly as it always did.
|
|
49
|
+
// Deciding up front — rather than copying and undoing it on failure — is why nothing here has
|
|
50
|
+
// to be reversed.
|
|
51
|
+
return action === "copy" && !hasSelection ? null : action;
|
|
52
|
+
}
|