mulmoterminal 2.1.1 → 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 +93 -17
- 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/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-1RrSDjxg.js → abnfDiagram-VRR7QNED-RjiYivmv-6R9yRbUC.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-C95zkDEU.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-sTMTrNOq.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-DzS5ABJE.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-CQmbTR0O.js → blockDiagram-677ZJIJ3-BPuAJQRW-Cw9RkJxY.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-kjKqpuxO.js → c4Diagram-LMCZKHZV-C0LAqQso-CWltXMHP.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-BYaGw2yS.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-YnuI_kVF.js → chunk-32BRIVSS-BRrYpgtb-DrrsZ2u5.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-Dv-_qNms.js → chunk-52WLFC77-BJ-ss3Xr-WJMq_L8L.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-VvGb94ad.js → chunk-C7G6YPKG-BZEucKEL-BafXJkh7.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-Cv9JdwrA.js → chunk-EX3LRPZG-DLS6FBN1-Q8lsrJXs.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-CFK4Xl3Y.js → chunk-FWX5IMBZ-DHLSFw1H-Cgm5-zaI.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-l9dGXTcV.js → chunk-HOUHSVGY-Bhlt8hXJ-Cp5J5cOD.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-oBhC9ghH.js → chunk-ICXQ74PX-DwgHBX_g-CLdnp-d2.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-CKo27qok.js → chunk-MOJQB5TN-CMZRaeqt-BsIB0ZXR.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-Di7gzOY_.js → chunk-OGEWGWER-8Qy4a8b5-DlTM-RTP.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-DuMYBLYy.js → chunk-PUDLZKDR-DcrWQRYh-XujP_H1v.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-BH8ZxP7u.js → chunk-Q4XR5HBZ-ZXVGkG8Z-DhxnzVDy.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-e5Lem5jk.js → chunk-V7JOEXUC-DmGdheTX-C-xPmCQA.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-CfgcnwuB.js → chunk-VAUOI2AC-CS9QJ4yz-Cng5RPUP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-BcBE3OXA.js → chunk-VR4S4FIN-C6a91eNY-Bmb0moA3.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-BRwSb4rl.js → chunk-WYO6CB5R-BlzOfotS-DmR4SyZL.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-yy_t_DUF.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--CEHib9RV.js → cynefinDiagram-TSTJHNR4-DHA9iPo--pv1DCSfy.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-DufbQ1ld.js → dagre-VKFMJZFB--oJKqXBZ-BPcGEiSl.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-Bcp75QDM.js → diagram-FQU43EPY-CcJCB9bG-CCOrHV0U.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-C_2FVIX_.js → diagram-G47NLZAW-C_o-WGG1-Crm_IB8Y.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--B1kVCM4I.js → diagram-NH7WQ7WH-CXJCYvY--UcH8gkTE.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-Bd1BRg0M.js → diagram-OA4YK3LP-BMzeJ87A-7C9Me5sS.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-VJzlhYeh.js → diagram-WEI45ONY-D_93NKqo-BBztQ9hK.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-BXziXSdO.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-TkJNOqEw.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-DzjaMys9.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-DqlM0R3b.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-Dy5WrrGR.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-ByceYPA5.js → infoDiagram-FWYZ7A6U-7UnoB5AP-Cr34YkAb.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-BZEvgyxt.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-at4P1zMV.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-D3IMe-Kx.js → journeyDiagram-5HDEW3XC-c5xIah9o-D4ZV57k7.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-cjfYxgHr.js → kanban-definition-HUTT4EX6-Cnt6loYD-BiJ3JUjX.js} +1 -1
- package/dist/assets/{lib-BP5ftgj4.js → lib-Du_IVDkN.js} +1 -1
- package/dist/assets/{line-D7ziSjKi-C07Dlbdw.js → line-D7ziSjKi-CPM0FMWw.js} +1 -1
- package/dist/assets/{marp-ytjNgY2U.js → marp-BBre1w8X.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-BAh0ySn4.js → mermaid-parser.core-DxEa8E3F-CO4zEYzL.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-CIFgdt3N.js → mermaid.core-V0OYwIz3-CzPQxhwu.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-1SEo71tq.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-DkewOOUw.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-DdJe9KqM.js → pieDiagram-ENE6RG2P-B_fBBS-2-DXBEfgPl.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-C8kNdasc.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-DawLoSCW.js → railroadDiagram-RFXS5EU6--lNIlG64-DHNapLB9.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-BMFbGAmA.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-C-9SVxpD.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-Dwt5PZ2I.js → sankeyDiagram-HTMAVEWB-DlutaXDv-Cv-mFz76.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-yb89czG2.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-BR0sUcBe.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-kqvWM0jR.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-CiVWAsob.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-DrqIflbo.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-COepU8y_.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-Z2AgrgfV.js → wardleyDiagram-EHGQE667-BF6c4_CW-BkrH7vLX.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-ChC7g8sJ.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/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-CtE1YID5.js +0 -1
- package/dist/assets/channel-7wUqSdoX-i8mXF_QX.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-BM_zLSMQ.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-BM_zLSMQ.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-BBb6pP0M.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-C1_24lZu.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-DQrHefx7.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DSTb91Xn.js +0 -1
- package/dist/assets/index-D4LB9YoW.js +0 -618
- package/dist/assets/index-u-r21U2t.css +0 -1
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-Cdy5Xn2F.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-BFv5gKuv.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-9hvyyDzu.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-DBeFDuLF.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-FnTYQEAW.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-Bo4xQewA.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-YFFxMVbA.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DMzqIAqS.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-QnrqYDdZ.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-CE7hrF_H.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DaEUFpbs.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-Cb7ZUpob.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-BON96qJv.js +0 -1
package/README.md
CHANGED
|
@@ -451,7 +451,9 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
451
451
|
| Field | Meaning |
|
|
452
452
|
| ------------ | ------- |
|
|
453
453
|
| `cwdPresets` | Quick-pick directories offered when launching a terminal. |
|
|
454
|
-
| `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`. |
|
|
455
457
|
| `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates (via your `gh` login). |
|
|
456
458
|
| `launchers` | `{ label, command }` entries offered in a grid cell's launcher besides Claude — a plain shell, `codex`, any interactive command. |
|
|
457
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. |
|
|
@@ -464,6 +466,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
464
466
|
| `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). |
|
|
465
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). |
|
|
466
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). |
|
|
467
470
|
|
|
468
471
|
#### Header buttons
|
|
469
472
|
|
|
@@ -482,12 +485,46 @@ there's no open PR) / `pickFile: true` (OS file dialog → insert the path).
|
|
|
482
485
|
visibility. The `/mulmoterminal-config` skill writes a valid config interactively; per-dir buttons
|
|
483
486
|
merge over the global ones by `id`.
|
|
484
487
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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.
|
|
491
528
|
|
|
492
529
|
**Web Push on task finish.** Enable `pushEnabled` in Settings to have the server send a
|
|
493
530
|
push (title = the project dir, body = the last prompt) to your registered devices each
|
|
@@ -541,7 +578,10 @@ malformed file is ignored.
|
|
|
541
578
|
"theme": "nord", // terminal palette: midnight | nord | daylight | solarized
|
|
542
579
|
"colors": { "background": "#190a23", "cursor": "#ff2e63" }, // per-key palette overrides
|
|
543
580
|
"fontSize": 16, // terminal font size in px (8–32); overrides Settings
|
|
544
|
-
"
|
|
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
|
|
545
585
|
}
|
|
546
586
|
```
|
|
547
587
|
|
|
@@ -562,12 +602,20 @@ malformed file is ignored.
|
|
|
562
602
|
| `theme` | xterm palette for terminals in this directory (one of the built-in theme ids). |
|
|
563
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. |
|
|
564
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. |
|
|
565
|
-
| `
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
569
613
|
HTTP request, so an opened project can't point the player at arbitrary files.
|
|
570
|
-
|
|
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.
|
|
571
619
|
|
|
572
620
|
---
|
|
573
621
|
|
|
@@ -712,10 +760,33 @@ tree; clicking a file opens it in a **CodeMirror** editor (Markdown / JS-TS / JS
|
|
|
712
760
|
highlighting, everything else as plain text). Markdown files get a **Preview** toggle
|
|
713
761
|
that renders via the server's sandboxed `…/md` HTML. **Save** (or ⌘/Ctrl-S) writes back.
|
|
714
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
|
+
|
|
715
771
|
All reads and writes go through `GET/PUT /api/files/browse/*?cwd=&path=`, and every
|
|
716
772
|
`path` is **contained within the project root** (server-side) — `..`/absolute escapes
|
|
717
773
|
are rejected for reads and writes alike, so editing can't reach outside the directory
|
|
718
|
-
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.
|
|
719
790
|
|
|
720
791
|
---
|
|
721
792
|
|
|
@@ -862,6 +933,11 @@ Favorited collections get their own toolbar buttons.
|
|
|
862
933
|
**Option** is treated as Meta so Claude's Alt-key bindings work. If your Claude Code is
|
|
863
934
|
rebound so Enter and Shift+Enter behave backwards, flip them with
|
|
864
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.
|
|
865
941
|
|
|
866
942
|
---
|
|
867
943
|
|
|
@@ -1033,7 +1109,7 @@ same-origin-guarded.
|
|
|
1033
1109
|
| -------- | ------- |
|
|
1034
1110
|
| `GET /api/wiki` (`?slug=`) · `/api/wiki/graph` · `/api/wiki/lint` | Read-only wiki index / page / graph / lint. |
|
|
1035
1111
|
| `GET /api/collections/…` · `/api/feeds` · `GET\|PUT /api/shortcuts` | Collections browser, feeds, favorites (see `docs/collection-plugin-integration.md`). |
|
|
1036
|
-
| `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. |
|
|
1037
1113
|
| `GET /api/files/raw?path=` | Raw asset bytes (workspace-rooted). |
|
|
1038
1114
|
|
|
1039
1115
|
**GUI panel / plugins / MCP**
|
|
@@ -1050,8 +1126,8 @@ same-origin-guarded.
|
|
|
1050
1126
|
|
|
1051
1127
|
| Endpoint | Purpose |
|
|
1052
1128
|
| -------- | ------- |
|
|
1053
|
-
| `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `quickCommands`, `userMcpServers`, `providers`). |
|
|
1054
|
-
| `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. |
|
|
1055
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. |
|
|
1056
1132
|
| `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
|
|
1057
1133
|
| `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The xterm font-family stack. Shared because both sides decide from it: the server validates
|
|
2
|
+
// `.mulmoterminal.json` and `config.json`, and the client re-validates what comes off the wire
|
|
3
|
+
// and supplies the fallback when neither file names one.
|
|
4
|
+
|
|
5
|
+
// The CJK tail every monospace stack in the app ends with, Japanese before the other locales — a
|
|
6
|
+
// stack that reached `…CJK SC` first would draw kanji with mainland glyph shapes. Named explicitly
|
|
7
|
+
// rather than left to the generic `monospace` fallback: the face a browser picks for an unnamed
|
|
8
|
+
// glyph is not required to be em-square, and a terminal reserves exactly TWO cells for a fullwidth
|
|
9
|
+
// character, so a face whose fullwidth advance isn't exactly twice the Latin one tears every
|
|
10
|
+
// box-drawing frame — which is most of what an agent TUI draws.
|
|
11
|
+
//
|
|
12
|
+
// Exported because the app chrome needs the same tail: Tailwind's `font-mono` (`--font-mono` in
|
|
13
|
+
// src/tailwind.css) had no CJK either, so a Japanese header or diff line fell to the browser's
|
|
14
|
+
// pick. CSS can't import this, so that file restates it and a spec pins the two together.
|
|
15
|
+
export const TERMINAL_CJK_FAMILIES = [
|
|
16
|
+
"'Noto Sans Mono CJK JP'",
|
|
17
|
+
"'Hiragino Sans'",
|
|
18
|
+
"'BIZ UDGothic'",
|
|
19
|
+
"'MS Gothic'",
|
|
20
|
+
"IPAGothic",
|
|
21
|
+
"'Noto Sans Mono CJK KR'",
|
|
22
|
+
"'Malgun Gothic'",
|
|
23
|
+
"'Noto Sans Mono CJK SC'",
|
|
24
|
+
"'Microsoft YaHei'",
|
|
25
|
+
"'Noto Sans Mono CJK TC'",
|
|
26
|
+
"'Microsoft JhengHei'",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Latin first, so an existing install's ASCII rendering is unchanged.
|
|
30
|
+
export const TERMINAL_FONT_FAMILY_DEFAULT = ["'JetBrains Mono'", "'Fira Code'", "Menlo", "Consolas", ...TERMINAL_CJK_FAMILIES, "monospace"].join(", ");
|
|
31
|
+
|
|
32
|
+
// Long enough for a hand-written stack of a dozen names, short enough that the value stays a font
|
|
33
|
+
// list rather than a place to park arbitrary text.
|
|
34
|
+
export const TERMINAL_FONT_FAMILY_MAX_CHARS = 400;
|
|
35
|
+
|
|
36
|
+
// The portable half of the rule, for the JSON Schema shipped with the config skill. A JSON Schema
|
|
37
|
+
// `pattern` is ECMA-262 WITHOUT the `u` flag, so it cannot use the `\p{L}` classes the exact rule
|
|
38
|
+
// below needs to accept font names in every script. It denies the CSS syntax that makes a value
|
|
39
|
+
// dangerous or silently breaking, and leaves the per-entry structure to normalizeFontFamily at
|
|
40
|
+
// load time. A spec pins that whatever the exact rule accepts, this accepts too.
|
|
41
|
+
export const TERMINAL_FONT_FAMILY_SAFE_RE = /^[^;{}()<>\\/@!]+$/;
|
|
42
|
+
|
|
43
|
+
// One entry of the list: a leading letter or digit, then letters of ANY script (`游ゴシック` and
|
|
44
|
+
// `MS ゴシック` are real font names and must pass), digits, combining marks, spaces of any width,
|
|
45
|
+
// and the three separators real font names use. Everything CSS treats as syntax, plus quotes and
|
|
46
|
+
// control characters, is outside it and stays out.
|
|
47
|
+
const FONT_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}\p{M}\p{Zs}._-]*$/u;
|
|
48
|
+
|
|
49
|
+
// CSS falls back to the browser's default when no named family matched, and that default is
|
|
50
|
+
// PROPORTIONAL — an xterm rendered in it has every column misaligned, with nothing on screen to
|
|
51
|
+
// explain why. So a stack that names no generic family gets `monospace` appended.
|
|
52
|
+
const GENERIC_FAMILIES = new Set(["monospace", "ui-monospace", "system-ui", "serif", "sans-serif", "cursive", "fantasy"]);
|
|
53
|
+
|
|
54
|
+
// Quotes are legal, but only as a matching pair around a whole entry: a stray one unbalances the
|
|
55
|
+
// CSS declaration, which invalidates it entirely rather than just that entry.
|
|
56
|
+
function isFontName(entry: string): boolean {
|
|
57
|
+
const quote = entry[0] === "'" || entry[0] === '"' ? entry[0] : "";
|
|
58
|
+
if (!quote) return FONT_NAME_RE.test(entry);
|
|
59
|
+
return entry.length >= 3 && entry.endsWith(quote) && FONT_NAME_RE.test(entry.slice(1, -1));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Deliberately unquoted-only. In CSS a generic family is a KEYWORD: quoting it turns it into a
|
|
63
|
+
// lookup for a font literally named "monospace", which resolves nowhere and provides no fallback.
|
|
64
|
+
// Counting a quoted one as satisfying the rule below would skip the append in precisely the case
|
|
65
|
+
// the append exists to catch.
|
|
66
|
+
const isGenericFamily = (entry: string): boolean => GENERIC_FAMILIES.has(entry.toLowerCase());
|
|
67
|
+
|
|
68
|
+
// Reject the whole stack on a bad entry, where normalizeFontSize clamps: a size is a continuous
|
|
69
|
+
// quantity, so honouring the direction the user asked for beats ignoring it, but a stack is one
|
|
70
|
+
// intent — dropping half of it would render in a font the author never named. Returning null lets
|
|
71
|
+
// the caller fall back to the level below (dir → global → default).
|
|
72
|
+
export function normalizeFontFamily(input: unknown): string | null {
|
|
73
|
+
if (typeof input !== "string") return null;
|
|
74
|
+
const stack = input.trim();
|
|
75
|
+
if (!stack || stack.length > TERMINAL_FONT_FAMILY_MAX_CHARS) return null;
|
|
76
|
+
const names = stack.split(",").map((name) => name.trim());
|
|
77
|
+
if (!names.every(isFontName)) return null;
|
|
78
|
+
return [...names, ...(names.some(isGenericFamily) ? [] : ["monospace"])].join(", ");
|
|
79
|
+
}
|
|
@@ -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-CO4zEYzL.js";import"./chunk-WYO6CB5R-BlzOfotS-DmR4SyZL.js";import"./chunk-VAUOI2AC-CS9QJ4yz-Cng5RPUP.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-BsIB0ZXR.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-CO4zEYzL.js";export{e as createArchitectureServices};
|