mulmoterminal 2.5.1 → 2.5.3
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 +4 -1
- package/common/decisionLog.ts +49 -0
- package/common/keymap.ts +96 -18
- package/common/prPhase.ts +11 -1
- package/common/themeVars.ts +134 -0
- package/common/toolGroups.ts +80 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-BHMJcX-D.js → abnfDiagram-VRR7QNED-RjiYivmv-CkfTvNiT.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-F7APysNp.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-izYrzJUx.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-DU5fnnYi.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-BD7LRmIw.js → blockDiagram-677ZJIJ3-BPuAJQRW-DJtyRfct.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-daMFkQYF.js → c4Diagram-LMCZKHZV-C0LAqQso-CaAB4hIh.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-o7hjueM_.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-Lp6IvDwQ.js → chunk-32BRIVSS-BRrYpgtb-C2pc6s6p.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-CIA8l6cL.js → chunk-52WLFC77-BJ-ss3Xr-BiDQ5LS-.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-DBfTQdYh.js → chunk-C7G6YPKG-BZEucKEL-B6NLudrF.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-CmqVBBJi.js → chunk-EX3LRPZG-DLS6FBN1-Dydj9mRL.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-Br6MRw8l.js → chunk-FWX5IMBZ-DHLSFw1H-BtzzUMmo.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-BDsryiai.js → chunk-HOUHSVGY-Bhlt8hXJ-460OsWXy.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-Dtq6hkL5.js → chunk-ICXQ74PX-DwgHBX_g-CSp9RB18.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-xCxis4AD.js → chunk-MOJQB5TN-CMZRaeqt-CC-mdXDP.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-CEKkrNHl.js → chunk-OGEWGWER-8Qy4a8b5-nrJtDZ0I.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-CNZIIdNp.js → chunk-PUDLZKDR-DcrWQRYh-D7s7UkTC.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-BfnUuIf2.js → chunk-Q4XR5HBZ-ZXVGkG8Z-Vjuup2uc.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-Xhz9vrZQ.js → chunk-V7JOEXUC-DmGdheTX-V1zSMg_1.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-CDhJXpTw.js → chunk-VAUOI2AC-CS9QJ4yz-C1PM97Ei.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-C6K2Be2R.js → chunk-VR4S4FIN-C6a91eNY-zf8Ln3gQ.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-C6J_J8TJ.js → chunk-WYO6CB5R-BlzOfotS-CgOwOaF2.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-DM76gWAP.js → chunk-ZGVPDNZ5-BYwxNFTK-C17Ongqc.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-dAb2Nqej.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-dAb2Nqej.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-CcNtA1jr.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--CygDupfu.js → cynefinDiagram-TSTJHNR4-DHA9iPo--rvKNlp57.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-DKaBHoMD.js → dagre-VKFMJZFB--oJKqXBZ-DAPicxb0.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-BCdZM1XW.js → diagram-FQU43EPY-CcJCB9bG-Cwk0uscZ.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-E50OCsch.js → diagram-G47NLZAW-C_o-WGG1-CLLr3WWX.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--CmG9YdUu.js → diagram-NH7WQ7WH-CXJCYvY--YbeWvGrQ.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-CxPVNELp.js → diagram-OA4YK3LP-BMzeJ87A-CCzLGMoX.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-lP3U0GxX.js → diagram-WEI45ONY-D_93NKqo-C_vctp-i.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-DEA-ZvYv.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-C9RE0jib.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-uFB2JxeG.js → erDiagram-Q63AITRT-CiABnA0s-DNe_dsG7.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-CWY7xW6B.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BQbNqK8I.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-CnuwHsCh.js → ganttDiagram-NO4QXBWP-DQZvdFo1-D0fBB7vz.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DndYDaiz.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-DMwS2Tlh.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-_9JEvhUj.js} +1 -1
- package/dist/assets/{index-CIUVcLpS.js → index-BLn_JWl1.js} +202 -202
- package/dist/assets/index-BZb3UNXf.css +1 -0
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-Ch8HtmdN.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-D1UXK3im.js → infoDiagram-FWYZ7A6U-7UnoB5AP-sf0LZ0TR.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-eU53AoG2.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-Dh3uJwRU.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-BQNy69_H.js → journeyDiagram-5HDEW3XC-c5xIah9o-BaIw2Adh.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-C3Ya6-uo.js → kanban-definition-HUTT4EX6-Cnt6loYD-BLIQlGsE.js} +1 -1
- package/dist/assets/{lib-XZe86Xbo.js → lib-BkHCzFsd.js} +1 -1
- package/dist/assets/{line-D7ziSjKi-DhimY3vn.js → line-D7ziSjKi-BnFPtJcd.js} +1 -1
- package/dist/assets/{marp-DE0BYdnE.js → marp-JKciXjDr.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-C43vNJPp.js → mermaid-parser.core-DxEa8E3F-CscC8Qqr.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-v8Oxyjlo.js → mermaid.core-V0OYwIz3-g362i9RA.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-D3QMGDaH.js → mindmap-definition-LN4V7U3C-CexN3O6L-CEmgqxM2.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-kVLgbrf9.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-Bt6FgrWE.js → pegDiagram-2B236MQR-BiM4G0cW-xR2ciIPB.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-D1l75ZW4.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-CEmdDz9t.js → pieDiagram-ENE6RG2P-B_fBBS-2-Do2rjuXg.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-4kYUj0VK.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-CYjBsLx8.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CgFu134o.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-_koCxt7R.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-y8_SdxMu.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-CREWWR2J.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DkxX3rOi.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-BS12t8Yr.js → railroadDiagram-RFXS5EU6--lNIlG64-DrRDvGFv.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-B1_SLwFo.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-AohyoXS1.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-CFF-xYsd.js → sankeyDiagram-HTMAVEWB-DlutaXDv-BeuhDKAh.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-DJXV6qUI.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-BMS6Xghs.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-BD6GsGtH.js → stateDiagram-2N3HPSRC-jf_dXUEU-1ccT8EBF.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-COjV9W1k.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-CHqwZOyR.js → swimlanes-5IMT3BWC-CYjtALQH-B7FjNmUf.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-CEARqU8w.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-Dsee2TjT.js → timeline-definition-FHXFAJF6-DYJ4oUm8-Cv5APELs.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-CrGq3OoF.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-C7DIaiQ8.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-d0nAypXX.js → vennDiagram-L72KCM5P-CdKAHoek-DDVhQ5s2.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-WP0WCRL-.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-BStK630L.js → wardleyDiagram-EHGQE667-BF6c4_CW-BGrTblgg.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7--qXz8KqF.js → xychartDiagram-FW5EYKEG-CGiKngj7-D9-hceiL.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/agents/claude-args.ts +13 -3
- package/server/agents/probe-session.spec.ts +27 -0
- package/server/agents/probe-session.ts +26 -0
- package/server/agents/rate-limit-probe.ts +14 -4
- package/server/agents/rate-limit-routes.ts +13 -3
- package/server/agents/rate-limit-store.spec.ts +136 -7
- package/server/agents/rate-limit-store.ts +116 -5
- package/server/agents/session-summary-prompt.ts +10 -0
- package/server/backends/remoteHost/terminalScreen.ts +23 -0
- package/server/backends/whisper.ts +5 -6
- package/server/config/app-config.ts +44 -0
- package/server/config/config-body.ts +11 -1
- package/server/config/config-routes.ts +13 -0
- package/server/config/config-schema.ts +35 -1
- package/server/config/cwd-presets.ts +28 -5
- package/server/config/dir-config.ts +11 -1
- package/server/config/workspace.ts +16 -1
- package/server/git/prPhase.ts +33 -9
- package/server/index.ts +107 -10
- package/server/infra/gui-mcp-registration.ts +61 -0
- package/server/infra/has-binary.ts +27 -0
- package/server/infra/install-bundled-skills.ts +1 -1
- package/server/infra/path-within.ts +14 -0
- package/server/infra/plugins-registry.ts +22 -2
- package/server/infra/spawnCapture.ts +48 -3
- package/server/infra/tmux.ts +10 -2
- package/server/mcp/broker.ts +14 -6
- package/server/mcp/tool-gate.ts +24 -3
- package/server/routes/app-routes.ts +13 -2
- package/server/routes/decision-routes.ts +47 -0
- package/server/routes/gui-mcp-routes.ts +42 -0
- package/server/routes/mcp-routes.ts +47 -6
- package/server/routes/session-routes.ts +2 -1
- package/server/routes/tool-routes.ts +55 -6
- package/server/routes/ws-routes.ts +16 -3
- package/server/session/decision-digest-file.ts +57 -0
- package/server/session/decision-digest.ts +166 -0
- package/server/session/decision-scan.ts +101 -0
- package/server/session/decisions.ts +278 -0
- package/server/session/dev-terminal-cwds.ts +68 -0
- package/server/session/mcp-config.ts +13 -0
- package/server/session/pty-spawn.ts +18 -6
- package/server/session/registry.ts +110 -1
- package/server/session/session-list.ts +4 -3
- package/server/session/session-tool-groups.ts +79 -0
- package/server/session/spawn-claude.ts +15 -6
- package/server/session/spawn-deps.ts +3 -0
- package/server/skills/mulmoterminal-config/SKILL.md +35 -0
- package/server/skills/mulmoterminal-decisions/SKILL.md +77 -0
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-BQjHKl14.js +0 -1
- package/dist/assets/channel-7wUqSdoX-i1Oxds2I.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-w-oQvzN_.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-w-oQvzN_.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-BbR1aAzz.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-DXHpv7ri.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BAXt5ZcK.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-C5VWobaj.js +0 -1
- package/dist/assets/index-BL1ivfaK.css +0 -1
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-dN3WlTdS.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-0aWqmW17.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-BuudBowH.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CoI_TbnK.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-D-Aq_3zs.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-CYcDcmr_.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-D-PAMLpZ.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CAkKdvRU.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CGUWQotU.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-YHo-FlZ7.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-XRfUQQF3.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DV-AlqRD.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-Dvuabdt5.js +0 -1
package/README.md
CHANGED
|
@@ -488,6 +488,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
488
488
|
| `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). |
|
|
489
489
|
| `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). |
|
|
490
490
|
| `copyOnSelect` | `true` puts a **mouse selection on the clipboard the moment it settles**, with no key pressed (the PuTTY / iTerm2 behaviour). **Off by default** — it changes the clipboard when you may only have meant to highlight something. No Settings UI: edit the file and reload the tab. Composes with the `copy` keymap action rather than replacing it. Over plain `http://` the browser gives a page no clipboard access, so a fallback asks xterm to copy instead; see the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#copy-on-select). |
|
|
491
|
+
| `decisionDigest` | Keep a **Markdown digest of the decisions this project's sessions asked for**, refreshed at startup and every few hours, so an agent can read what has already been decided before asking something similar. Written to `~/.mulmoterminal/decisions/<project>.md` (never into your repository) and served to agents by the bundled `mulmoterminal-decisions` skill. **Off by default** — it is a vision-stage idea, and it writes a file that would otherwise not exist. The digest holds dated facts, never inferred rules. |
|
|
491
492
|
| `issueWorkComments` | Let a cell **comment on the issue it is working on**: once when it starts, and again when its PR merges (closing the issue if GitHub has not already). The comment names the working **directory** it happened in — the folder name only, never the path — so a reader can tell which clone. **Off by default**; it writes to GitHub, often on somebody else's issue. Needs `gh` logged in. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#issue-work-comments). |
|
|
492
493
|
| `prWorkdirFooter` | Ends a PR body with `work in <clone>` — the directory name of the clone the work happened in, so a PR says which of several side-by-side checkouts produced it. Applies to **both** paths that open PRs here: **⧉ Open PR** appends it to the PR it creates, and every Claude session is told to end the bodies it writes with the same line (the name is resolved by the server, so a session inside a managed worktree still names the main checkout). **On by default**; set `false` to opt out — read per PR and per session spawn, so no restart is needed (there is no Settings control for it). Appending is idempotent: an existing PR never gets a second copy. |
|
|
493
494
|
| `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). |
|
|
@@ -1144,6 +1145,8 @@ same-origin-guarded.
|
|
|
1144
1145
|
| `GET /api/cost?cwd=&session=` | Estimated $ cost — session / today / month. |
|
|
1145
1146
|
| `GET /api/transcript/timeline?session=&cwd=` | Per-session activity timeline (tools run). |
|
|
1146
1147
|
| `GET /api/transcript/last-turn?session=&cwd=&agent=` | A session's last completed exchange (`prompt`, `reply`) plus the `text` to paste into another terminal. `agent=codex` reads the codex rollout instead of the Claude transcript. |
|
|
1148
|
+
| `GET /api/decisions?cwd=&limit=` | The decisions a human was asked to make in this project, newest first — each question with the options it offered, their descriptions, and the answer. `answerKind` says whether the answer was one of the options, text the user wrote instead (the question was wrong), or never given. Read out of Claude's own transcripts; writes nothing. `scanned` reports how many transcripts were read (the scan is capped) and `unreadable` how many could not be, so a partial answer is visible rather than implied. A `cwd` that is not an existing directory answers an empty response rather than falling back to the default workspace. |
|
|
1149
|
+
| `GET /api/decisions/digest?cwd=` | The same decisions as Markdown, for an agent to read (`{ enabled, markdown }`). `enabled: false` means the `decisionDigest` setting is off — a different answer from an empty digest, so a reader can tell "switched off" from "nothing decided here". |
|
|
1147
1150
|
|
|
1148
1151
|
**Git & worktrees**
|
|
1149
1152
|
|
|
@@ -1468,7 +1471,7 @@ server/
|
|
|
1468
1471
|
mcp/ per-session MCP broker
|
|
1469
1472
|
backends/ wiki, collections, feeds, accounting, notifier,
|
|
1470
1473
|
translation, whisper, remote-host, html, files
|
|
1471
|
-
skills/ bundled skills (mulmoterminal-config, mulmoterminal-bug-report)
|
|
1474
|
+
skills/ bundled skills (mulmoterminal-config, mulmoterminal-bug-report, mulmoterminal-decisions)
|
|
1472
1475
|
fix-pty-perms.js postinstall: fixes node-pty binary permissions
|
|
1473
1476
|
src/
|
|
1474
1477
|
App.vue Layout; owns the active session + single/grid view
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// A decision a human was asked to make, as it was actually asked and answered.
|
|
2
|
+
//
|
|
3
|
+
// The record is not something this app writes: Claude Code already stores every
|
|
4
|
+
// `AskUserQuestion` in its transcript, options and chosen answer included. These types are the
|
|
5
|
+
// shape we read it back in, shared because both the API and the UI decide from them (#997).
|
|
6
|
+
|
|
7
|
+
/** One option the user was offered, with the reasoning shown alongside it — this is where the
|
|
8
|
+
* consequence of the branch NOT taken is recorded. */
|
|
9
|
+
export interface DecisionOption {
|
|
10
|
+
label: string;
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Where the answer came from. `free-text` means the user declined every option and wrote their
|
|
15
|
+
* own answer, which is evidence about the QUESTION (wrong options, or a premise the asker and
|
|
16
|
+
* the user did not share) rather than about the choice. `unanswered` is a question that was
|
|
17
|
+
* asked and never resolved — the session was interrupted or abandoned. */
|
|
18
|
+
export type DecisionAnswerKind = "option" | "free-text" | "unanswered";
|
|
19
|
+
|
|
20
|
+
export interface DecisionQuestion {
|
|
21
|
+
question: string;
|
|
22
|
+
/** The short chip the UI showed above the question ("進め方", "Auth method"). */
|
|
23
|
+
header: string;
|
|
24
|
+
multiSelect: boolean;
|
|
25
|
+
options: DecisionOption[];
|
|
26
|
+
/** Verbatim, exactly as it came back. Null when nothing came back. */
|
|
27
|
+
answer: string | null;
|
|
28
|
+
answerKind: DecisionAnswerKind;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One `AskUserQuestion` call: the questions it asked together, and what each got back. */
|
|
32
|
+
export interface DecisionRecord {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
cwd: string | null;
|
|
35
|
+
/** ISO timestamp of the turn that asked. */
|
|
36
|
+
ts: string;
|
|
37
|
+
toolUseId: string;
|
|
38
|
+
questions: DecisionQuestion[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DecisionsResponse {
|
|
42
|
+
decisions: DecisionRecord[];
|
|
43
|
+
/** How many transcripts were read successfully. Bounded, so a caller can tell a quiet project
|
|
44
|
+
* from a truncated scan. */
|
|
45
|
+
scanned: number;
|
|
46
|
+
/** How many could not be read at all. Reported rather than folded into `scanned`, because a
|
|
47
|
+
* read that failed is missing decisions — not a session that had none. */
|
|
48
|
+
unreadable: number;
|
|
49
|
+
}
|
package/common/keymap.ts
CHANGED
|
@@ -37,7 +37,21 @@ export const isKeymapAction = (value: unknown): value is KeymapAction => typeof
|
|
|
37
37
|
// ^C. A handler that has already swallowed the key cannot change its mind.
|
|
38
38
|
export const TERMINAL_SCOPED_ACTIONS: readonly KeymapAction[] = ["copy", "paste"];
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// A key that puts BYTES into the focused terminal instead of running an app action (#1005) —
|
|
41
|
+
// Cmd+Right as Ctrl+E for end-of-line, say, or Alt+B for word-back.
|
|
42
|
+
//
|
|
43
|
+
// A LIST, where every action above is a single field, because the two are shaped differently:
|
|
44
|
+
// an action is one behaviour that a key is pointed at, while every send binding carries its own
|
|
45
|
+
// payload. `{ "send": "\u0005" }` could only ever name one key.
|
|
46
|
+
//
|
|
47
|
+
// `bytes` goes to the PTY verbatim. Control characters are written the way JSON writes them
|
|
48
|
+
// (`"\u0005"` is Ctrl+E); nothing here interprets or re-escapes them.
|
|
49
|
+
export interface SendBinding {
|
|
50
|
+
key: string;
|
|
51
|
+
bytes: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type Keymap = Partial<Record<KeymapAction, string>> & { send?: SendBinding[] };
|
|
41
55
|
|
|
42
56
|
// A parsed binding. `key` is matched against `KeyboardEvent.key` exactly as the browser
|
|
43
57
|
// reports it, so it is case-sensitive for printable characters ("a" and "A" differ, the
|
|
@@ -108,6 +122,26 @@ export function actionForKey(keymap: Keymap, e: KeymapKeyEvent): KeymapAction |
|
|
|
108
122
|
return null;
|
|
109
123
|
}
|
|
110
124
|
|
|
125
|
+
const isSendBinding = (value: unknown): value is SendBinding =>
|
|
126
|
+
typeof value === "object" && value !== null && typeof (value as SendBinding).key === "string" && typeof (value as SendBinding).bytes === "string";
|
|
127
|
+
|
|
128
|
+
// The bytes this keydown should put into the terminal, or null when it is not a send binding.
|
|
129
|
+
//
|
|
130
|
+
// Only reachable for keys the GRID did not claim: its handler listens on `window` in the capture
|
|
131
|
+
// phase and calls stopPropagation(), so an action and a send binding on one keystroke is not a
|
|
132
|
+
// race — the action always wins and the send silently never fires. validateKeymap warns about it
|
|
133
|
+
// for that reason.
|
|
134
|
+
//
|
|
135
|
+
// First match wins, so a keystroke listed twice uses the earlier entry.
|
|
136
|
+
export function sendBytesFor(keymap: Keymap, e: KeymapKeyEvent & { type: string; isComposing?: boolean }): string | null {
|
|
137
|
+
if (e.type !== "keydown" || e.isComposing) return null;
|
|
138
|
+
for (const entry of keymap.send ?? []) {
|
|
139
|
+
const binding = parseKeyBinding(entry.key);
|
|
140
|
+
if (binding && matchesBinding(binding, e)) return entry.bytes;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
111
145
|
// What is wrong with one `keymap` entry. `fatal` separates a typo the user clearly meant to
|
|
112
146
|
// work (a binding we cannot parse — the shortcut would silently never fire) from an action
|
|
113
147
|
// name we simply do not know, which is what a config written for a NEWER version looks like
|
|
@@ -127,38 +161,74 @@ export function validateKeymap(input: unknown): KeymapProblem[] {
|
|
|
127
161
|
return [{ action: "keymap", binding: input, reason: "`keymap` must be an object of action -> key binding", fatal: true }];
|
|
128
162
|
}
|
|
129
163
|
const entries = Object.entries(input as Record<string, unknown>);
|
|
130
|
-
|
|
164
|
+
// A claim on one keystroke. `rank` is DISPATCH order, so the winner can be named: every action
|
|
165
|
+
// outranks every send binding, because the grid's handler runs in the capture phase and stops
|
|
166
|
+
// the event before the terminal — see sendBytesFor.
|
|
167
|
+
const bound = new Map<string, Claim[]>();
|
|
168
|
+
const claim = (parsed: KeyBinding, entry: Claim): void => {
|
|
169
|
+
const key = canonicalBinding(parsed); // as PARSED: "Shift+PageUp" and "shift+pageup" are one keystroke
|
|
170
|
+
bound.set(key, [...(bound.get(key) ?? []), entry]);
|
|
171
|
+
};
|
|
131
172
|
const problems = entries.flatMap(([action, binding]): KeymapProblem[] => {
|
|
173
|
+
if (action === "send") return sendProblems(binding, claim);
|
|
132
174
|
if (!isKeymapAction(action)) {
|
|
133
|
-
return [{ action, binding, reason: `unknown action (known: ${KEYMAP_ACTIONS.join(", ")})`, fatal: false }];
|
|
175
|
+
return [{ action, binding, reason: `unknown action (known: ${KEYMAP_ACTIONS.join(", ")}, send)`, fatal: false }];
|
|
134
176
|
}
|
|
135
177
|
if (typeof binding !== "string") return [{ action, binding, reason: "binding must be a string", fatal: true }];
|
|
136
178
|
const parsed = parseKeyBinding(binding);
|
|
137
179
|
if (parsed === null) {
|
|
138
180
|
return [{ action, binding, reason: 'unparseable key binding — expected e.g. "PageDown" or "Shift+PageUp"', fatal: true }];
|
|
139
181
|
}
|
|
140
|
-
|
|
141
|
-
const key = canonicalBinding(parsed);
|
|
142
|
-
bound.set(key, [...(bound.get(key) ?? []), { action, binding }]);
|
|
182
|
+
claim(parsed, { label: action, binding, rank: KEYMAP_ACTIONS.indexOf(action) });
|
|
143
183
|
return [];
|
|
144
184
|
});
|
|
145
185
|
return [...problems, ...duplicateWarnings(bound)];
|
|
146
186
|
}
|
|
147
187
|
|
|
148
|
-
|
|
188
|
+
interface Claim {
|
|
189
|
+
label: string;
|
|
190
|
+
binding: string;
|
|
191
|
+
rank: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Everything wrong with the `send` list, claiming the keystrokes that are well-formed.
|
|
195
|
+
//
|
|
196
|
+
// Fatal throughout, for the reason the module header gives: a send binding is invisible until
|
|
197
|
+
// the key is pressed, so a dropped one is indistinguishable from a shortcut that "doesn't work".
|
|
198
|
+
// Empty `bytes` is fatal too — it would take the key away from the terminal and put nothing back.
|
|
199
|
+
function sendProblems(input: unknown, claim: (parsed: KeyBinding, entry: Claim) => void): KeymapProblem[] {
|
|
200
|
+
if (!Array.isArray(input)) {
|
|
201
|
+
return [{ action: "send", binding: input, reason: "`send` must be an array of { key, bytes }", fatal: true }];
|
|
202
|
+
}
|
|
203
|
+
return input.flatMap((entry, i): KeymapProblem[] => {
|
|
204
|
+
const label = `send[${i}]`;
|
|
205
|
+
if (!isSendBinding(entry)) return [{ action: label, binding: entry, reason: "expected { key: string, bytes: string }", fatal: true }];
|
|
206
|
+
const parsed = parseKeyBinding(entry.key);
|
|
207
|
+
if (parsed === null) {
|
|
208
|
+
return [{ action: label, binding: entry.key, reason: 'unparseable key binding — expected e.g. "Cmd+ArrowRight"', fatal: true }];
|
|
209
|
+
}
|
|
210
|
+
if (entry.bytes === "") {
|
|
211
|
+
return [{ action: label, binding: entry.key, reason: "`bytes` is empty — the key would be taken from the terminal and nothing sent", fatal: true }];
|
|
212
|
+
}
|
|
213
|
+
// Ranked after every action, matching who actually wins (see the `bound` comment above).
|
|
214
|
+
claim(parsed, { label, binding: entry.key, rank: KEYMAP_ACTIONS.length + i });
|
|
215
|
+
return [];
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Two claims on one keystroke: only one can fire, so the others silently never work.
|
|
149
220
|
//
|
|
150
|
-
// The winner is
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
function duplicateWarnings(bound: Map<string,
|
|
221
|
+
// The winner is the one DISPATCH picks, not the one written first in the config file. Reporting
|
|
222
|
+
// the config-order winner would name the wrong entry whenever the two orders disagree, which is
|
|
223
|
+
// worse than not naming one.
|
|
224
|
+
function duplicateWarnings(bound: Map<string, Claim[]>): KeymapProblem[] {
|
|
154
225
|
return [...bound.values()].flatMap((claims) => {
|
|
155
226
|
if (claims.length < 2) return [];
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
action,
|
|
227
|
+
const [winner, ...losers] = [...claims].sort((a, b) => a.rank - b.rank);
|
|
228
|
+
return losers.map(({ label, binding }) => ({
|
|
229
|
+
action: label,
|
|
160
230
|
binding,
|
|
161
|
-
reason: `same keystroke as \`${winner.
|
|
231
|
+
reason: `same keystroke as \`${winner.label}\` — only \`${winner.label}\` will fire`,
|
|
162
232
|
fatal: false,
|
|
163
233
|
}));
|
|
164
234
|
});
|
|
@@ -172,8 +242,16 @@ const canonicalBinding = (b: KeyBinding): string => `${b.shift ? "S" : ""}${b.al
|
|
|
172
242
|
// rest of the config treats one bad entry.
|
|
173
243
|
export function sanitizeKeymap(input: unknown): Keymap {
|
|
174
244
|
if (typeof input !== "object" || input === null || Array.isArray(input)) return {};
|
|
175
|
-
const
|
|
245
|
+
const raw = input as Record<string, unknown>;
|
|
246
|
+
const entries = Object.entries(raw).filter(
|
|
176
247
|
(entry): entry is [KeymapAction, string] => isKeymapAction(entry[0]) && typeof entry[1] === "string" && parseKeyBinding(entry[1]) !== null,
|
|
177
248
|
);
|
|
178
|
-
|
|
249
|
+
const send = sanitizeSendBindings(raw.send);
|
|
250
|
+
return { ...Object.fromEntries(entries), ...(send.length ? { send } : {}) };
|
|
179
251
|
}
|
|
252
|
+
|
|
253
|
+
// An entry survives only if it names a parseable key AND carries bytes to send. An empty `send`
|
|
254
|
+
// is dropped entirely rather than kept as `[]`, so an absent and an emptied list look the same
|
|
255
|
+
// to everything downstream.
|
|
256
|
+
const sanitizeSendBindings = (input: unknown): SendBinding[] =>
|
|
257
|
+
Array.isArray(input) ? input.filter(isSendBinding).filter((entry) => entry.bytes !== "" && parseKeyBinding(entry.key) !== null) : [];
|
package/common/prPhase.ts
CHANGED
|
@@ -20,9 +20,19 @@ export interface WorkItem {
|
|
|
20
20
|
prUrl: string | null;
|
|
21
21
|
issue: number | null;
|
|
22
22
|
issueUrl: string | null;
|
|
23
|
+
// Titles, for the surfaces that have room for words rather than a number — the phone's session
|
|
24
|
+
// list above all, where "#987" alone says nothing about which request is running (#1014).
|
|
25
|
+
prTitle: string | null;
|
|
26
|
+
issueTitle: string | null;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
export const EMPTY_WORK_ITEM: Readonly<WorkItem> = { phase: "none", pr: null, prUrl: null, issue: null, issueUrl: null };
|
|
29
|
+
export const EMPTY_WORK_ITEM: Readonly<WorkItem> = { phase: "none", pr: null, prUrl: null, issue: null, issueUrl: null, prTitle: null, issueTitle: null };
|
|
30
|
+
|
|
31
|
+
// One line for a surface with room for one: what the work is FOR beats what was done about it, so
|
|
32
|
+
// the issue's title wins when there is one. Null when neither side has a title to show.
|
|
33
|
+
export function workItemHeadline(item: WorkItem): string | null {
|
|
34
|
+
return item.issueTitle ?? item.prTitle ?? null;
|
|
35
|
+
}
|
|
26
36
|
|
|
27
37
|
// GitHub's closing keywords, as GitHub itself matches them: any of these, then optional
|
|
28
38
|
// whitespace/colon, then #N. Anything else in the body is a mention, not a link — a PR that
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// A custom theme's colours, and the rules that turn a user's `themes` entry into something the
|
|
2
|
+
// app can paint with. Shared across the build boundary for the same reason THEME_COLOR_KEYS is
|
|
3
|
+
// (see themeColors.ts): the server VALIDATES a config against these keys and the client APPLIES
|
|
4
|
+
// it, so a key known to one side and not the other would be accepted and then silently ignored.
|
|
5
|
+
|
|
6
|
+
import { THEME_IDS, type ThemeId } from "./themeIds.js";
|
|
7
|
+
|
|
8
|
+
// The CSS custom properties one theme sets, mirroring the `:root[data-theme=...]` blocks in
|
|
9
|
+
// src/style.css. A theme with no `extends` has to supply every one of them — a half-painted
|
|
10
|
+
// theme is worse than none, because the missing half falls back to whatever the previous theme
|
|
11
|
+
// left on the element.
|
|
12
|
+
export const THEME_VAR_KEYS = [
|
|
13
|
+
"--bg-base",
|
|
14
|
+
"--bg-deep",
|
|
15
|
+
"--bg-panel",
|
|
16
|
+
"--bg-subtle",
|
|
17
|
+
"--bg-elevated",
|
|
18
|
+
"--bg-input",
|
|
19
|
+
"--bg-hover",
|
|
20
|
+
"--bg-selected",
|
|
21
|
+
"--bg-selected-hover",
|
|
22
|
+
"--border",
|
|
23
|
+
"--accent",
|
|
24
|
+
"--accent-bg",
|
|
25
|
+
"--accent-bg-hover",
|
|
26
|
+
"--on-accent",
|
|
27
|
+
"--text",
|
|
28
|
+
"--text-secondary",
|
|
29
|
+
"--text-muted",
|
|
30
|
+
"--text-dim",
|
|
31
|
+
"--term-fg",
|
|
32
|
+
"--term-selection",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export type ThemeVarKey = (typeof THEME_VAR_KEYS)[number];
|
|
36
|
+
export type ThemeVars = Record<ThemeVarKey, string>;
|
|
37
|
+
|
|
38
|
+
/** An id a user may give a custom theme: lowercase, digits and dashes, so it is safe both as a
|
|
39
|
+
* `data-theme` attribute value and as something to type into a `.mulmoterminal.json`. */
|
|
40
|
+
export const CUSTOM_THEME_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
41
|
+
|
|
42
|
+
/** Whether a value could name a theme — built-in OR user-defined.
|
|
43
|
+
*
|
|
44
|
+
* The result is `string`, deliberately NOT `ThemeId`: that is the CLOSED set of four built-ins,
|
|
45
|
+
* and a guard claiming it while custom ids flow through would let downstream code treat a
|
|
46
|
+
* four-way switch as exhaustive (Codex review on #996). Membership is not checked here — the
|
|
47
|
+
* server drops an id that resolves to nothing, and termThemeFor() resolves what survives. */
|
|
48
|
+
export function isThemeIdLike(value: unknown): value is string {
|
|
49
|
+
return typeof value === "string" && CUSTOM_THEME_ID_RE.test(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isBuiltinThemeId(id: string): id is ThemeId {
|
|
53
|
+
return (THEME_IDS as readonly string[]).includes(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether an id may be used for a CUSTOM theme. Built-in ids are refused rather than merged
|
|
57
|
+
* into: someone reading the guide's description of Midnight has to get Midnight. */
|
|
58
|
+
export function isUsableCustomThemeId(id: unknown): id is string {
|
|
59
|
+
return typeof id === "string" && CUSTOM_THEME_ID_RE.test(id) && !isBuiltinThemeId(id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CustomThemeInput {
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
/** A built-in theme to start from. Omitted means `colors` must be complete. */
|
|
66
|
+
extends?: ThemeId;
|
|
67
|
+
colors: Partial<ThemeVars>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The full variable set for a theme: the base it extends, with its own colours on top.
|
|
71
|
+
* `builtins` supplies the base sets so this stays pure — the client reads them from the
|
|
72
|
+
* stylesheet's source of truth, the specs from a fixture. */
|
|
73
|
+
export function resolveThemeVars(theme: CustomThemeInput, builtins: Record<ThemeId, ThemeVars>): ThemeVars | null {
|
|
74
|
+
const base = theme.extends ? builtins[theme.extends] : null;
|
|
75
|
+
const merged = { ...(base ?? {}), ...theme.colors } as Partial<ThemeVars>;
|
|
76
|
+
const missing = THEME_VAR_KEYS.filter((key) => !merged[key]);
|
|
77
|
+
// No base and an incomplete list is the one case we cannot paint: report it rather than
|
|
78
|
+
// leaving the gaps to whatever the previously applied theme put on the element.
|
|
79
|
+
return missing.length ? null : (merged as ThemeVars);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function channel(hex: string, at: number): number {
|
|
83
|
+
const value = parseInt(hex.slice(at, at + 2), 16) / 255;
|
|
84
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa` as `#rrggbb`, or null. The config schema accepts
|
|
88
|
+
* every one of those forms, so anything reading a colour has to expand the short ones — a
|
|
89
|
+
* luminance check that understood only the six-digit form would read `#fff` as unmeasurable and
|
|
90
|
+
* call a white theme dark (Codex review on #996).
|
|
91
|
+
*
|
|
92
|
+
* Alpha is dropped rather than composited: what a translucent `--bg-base` ends up looking like
|
|
93
|
+
* depends on what is behind it, which nothing here can know. The opaque colour is the closest
|
|
94
|
+
* honest answer, and `--bg-base` is the one variable where translucency makes least sense. */
|
|
95
|
+
export function normalizeHexColor(color: string): string | null {
|
|
96
|
+
const hex = color.trim().toLowerCase();
|
|
97
|
+
if (!/^#[0-9a-f]{3,8}$/.test(hex)) return null;
|
|
98
|
+
const digits = hex.slice(1);
|
|
99
|
+
if (digits.length === 3 || digits.length === 4) {
|
|
100
|
+
return `#${[...digits.slice(0, 3)].map((d) => d + d).join("")}`;
|
|
101
|
+
}
|
|
102
|
+
if (digits.length === 6 || digits.length === 8) return `#${digits.slice(0, 6)}`;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Relative luminance (WCAG) of any hex colour the config accepts. Null for anything else. */
|
|
107
|
+
export function relativeLuminance(color: string): number | null {
|
|
108
|
+
const hex = normalizeHexColor(color);
|
|
109
|
+
if (!hex) return null;
|
|
110
|
+
return 0.2126 * channel(hex, 1) + 0.7152 * channel(hex, 3) + 0.0722 * channel(hex, 5);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Above this, the chrome is treated as a light theme. The status pills (ok / warn / err) come in
|
|
114
|
+
// a dark-background set and a light-background set, and picking the wrong one leaves them
|
|
115
|
+
// unreadable — which is why this is derived rather than declared: a theme author who never
|
|
116
|
+
// thinks about it still gets legible pills.
|
|
117
|
+
const LIGHT_LUMINANCE_THRESHOLD = 0.4;
|
|
118
|
+
|
|
119
|
+
export function isLightTheme(vars: ThemeVars): boolean {
|
|
120
|
+
const luminance = relativeLuminance(vars["--bg-base"]);
|
|
121
|
+
return luminance !== null && luminance > LIGHT_LUMINANCE_THRESHOLD;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The xterm palette a theme's variables imply. xterm draws on a canvas and cannot read CSS
|
|
125
|
+
* variables, so the same colours have to reach it as values — derived here rather than asked
|
|
126
|
+
* for twice, since two copies of one colour is two chances to change only one. The 16 ANSI
|
|
127
|
+
* colours are NOT derived: they come from the base theme, which is what `extends` is for. */
|
|
128
|
+
export function termThemeFromVars(vars: ThemeVars): { background: string; foreground: string; selectionBackground: string } {
|
|
129
|
+
return {
|
|
130
|
+
background: vars["--bg-base"],
|
|
131
|
+
foreground: vars["--term-fg"],
|
|
132
|
+
selectionBackground: vars["--term-selection"],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The GUI MCP is served at one URL per GROUP of tools, so a user can turn a subset on for a
|
|
2
|
+
// directory with Claude Code's own per-folder MCP config (`.mcp.json` / `claude mcp add -s
|
|
3
|
+
// local`) instead of a MulmoTerminal setting. `.mcp.json` only switches whole SERVERS on and
|
|
4
|
+
// off — splitting the URL is what turns that into tool granularity.
|
|
5
|
+
//
|
|
6
|
+
// These names appear in the URL path AND in the server id users write in their own config
|
|
7
|
+
// (`mulmoterminal-<group>`), so they are effectively public API: renaming one breaks every
|
|
8
|
+
// config already written against it.
|
|
9
|
+
|
|
10
|
+
// The groups, ordered by how much damage a call can do.
|
|
11
|
+
// render — draws into the Canvas panel and stops there. No side effect outside it.
|
|
12
|
+
// data — reads/writes the workspace's structured data (collections, accounting).
|
|
13
|
+
// media — generation that is slow, costly, and lands files on disk.
|
|
14
|
+
// external — reaches a third-party account or API.
|
|
15
|
+
export const TOOL_GROUPS = ["render", "data", "media", "external"] as const;
|
|
16
|
+
|
|
17
|
+
export type ToolGroup = (typeof TOOL_GROUPS)[number];
|
|
18
|
+
|
|
19
|
+
export const isToolGroup = (value: unknown): value is ToolGroup => TOOL_GROUPS.some((group) => group === value);
|
|
20
|
+
|
|
21
|
+
// The group the Canvas pane is made of. The launcher switch registers it, the panel's
|
|
22
|
+
// availability is decided by it, and the server routes on it — named here rather than written
|
|
23
|
+
// as `"render"` at each of those sites, so a rename cannot leave one of them silently pointing
|
|
24
|
+
// at a group that no longer exists.
|
|
25
|
+
export const CANVAS_TOOL_GROUP: ToolGroup = "render";
|
|
26
|
+
|
|
27
|
+
// Which group each GUI tool belongs to.
|
|
28
|
+
//
|
|
29
|
+
// A tool that is absent belongs to NO group and is therefore reachable only through the
|
|
30
|
+
// all-tools URL (`/api/mcp/:sessionId`, the single view). That is the deliberate failure
|
|
31
|
+
// mode: forgetting to classify a newly added plugin withholds it from the group URLs rather
|
|
32
|
+
// than leaking it into one — the map can go stale, but only ever closed.
|
|
33
|
+
//
|
|
34
|
+
// `spawnBackgroundChat` is absent ON PURPOSE, not by omission: it starts another session,
|
|
35
|
+
// which is neither drawing, data, media, nor an external call, and a grid cell has no
|
|
36
|
+
// business doing it silently.
|
|
37
|
+
// A Map, not a plain object — the same reason the plugin dispatch map is one: object index
|
|
38
|
+
// access reads through the prototype chain, so `constructor` / `__proto__` / `toString` would
|
|
39
|
+
// resolve to an Object.prototype member and report a truthy "group". Map.get only ever returns
|
|
40
|
+
// own entries.
|
|
41
|
+
const GROUP_BY_TOOL = new Map<string, ToolGroup>([
|
|
42
|
+
["presentDocument", "render"],
|
|
43
|
+
["presentForm", "render"],
|
|
44
|
+
["presentChart", "render"],
|
|
45
|
+
["presentHtml", "render"],
|
|
46
|
+
|
|
47
|
+
// presentCollection RENDERS, but it renders collection data and only makes sense next to
|
|
48
|
+
// manageCollection — a cell offered the view without the store gets a tool it cannot fill.
|
|
49
|
+
["presentCollection", "data"],
|
|
50
|
+
["manageCollection", "data"],
|
|
51
|
+
["manageAccounting", "data"],
|
|
52
|
+
|
|
53
|
+
["generateImage", "media"],
|
|
54
|
+
["presentMulmoScript", "media"],
|
|
55
|
+
|
|
56
|
+
["google", "external"],
|
|
57
|
+
["readXPost", "external"],
|
|
58
|
+
["searchX", "external"],
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export const groupOfTool = (toolName: string): ToolGroup | null => GROUP_BY_TOOL.get(toolName) ?? null;
|
|
62
|
+
|
|
63
|
+
// The MCP server id a group is expected to be registered under. `--allowedTools` matches on
|
|
64
|
+
// `mcp__<server id>__<tool>`, and the id comes from the USER's config key — so this is a
|
|
65
|
+
// convention the enable-it-for-this-folder affordance has to write, and a user who registers
|
|
66
|
+
// the same URL under another name simply gets permission prompts (nothing breaks).
|
|
67
|
+
export const toolGroupServerId = (group: ToolGroup): string => `mulmoterminal-${group}`;
|
|
68
|
+
|
|
69
|
+
// The tools MulmoTerminal pre-approves via `--allowedTools`, so they run without a permission
|
|
70
|
+
// prompt. A list of TOOLS, not of groups: a group says which tools a directory can reach, and
|
|
71
|
+
// that is not the same question as which may run unattended.
|
|
72
|
+
//
|
|
73
|
+
// `presentDocument` is the case that forces them apart, and it is deliberately ABSENT. Its
|
|
74
|
+
// execute runs `fillImages` before saving, which resolves every image placeholder in the
|
|
75
|
+
// markdown through the image backend — a PAID generation call. Auto-allowing it would let a
|
|
76
|
+
// model spend money silently under a switch the UI presents as "let the agent draw", so it
|
|
77
|
+
// keeps Claude Code's prompt (answer it once per project and the prompt stops).
|
|
78
|
+
//
|
|
79
|
+
// The three below save an artifact and draw it, and call nothing external.
|
|
80
|
+
export const AUTO_ALLOWED_TOOLS: readonly string[] = ["presentForm", "presentChart", "presentHtml"];
|
|
@@ -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-CscC8Qqr.js";import"./chunk-WYO6CB5R-BlzOfotS-CgOwOaF2.js";import"./chunk-VAUOI2AC-CS9QJ4yz-C1PM97Ei.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-CC-mdXDP.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-CscC8Qqr.js";export{e as createArchitectureServices};
|