mulmoterminal 4.8.2 → 4.8.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 +1 -0
- package/common/askQuestion.ts +217 -0
- package/common/gitlabHosts.ts +13 -2
- package/common/terminalReplies.ts +66 -0
- package/dist/assets/{abnfDiagram-N423BO3Z-D0pzqjdS-VtGooEqr.js → abnfDiagram-N423BO3Z-D0pzqjdS-BUEzXU7-.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-Cqhmai3f-DKxow7MX.js +1 -0
- package/dist/assets/{architectureDiagram-T3A2C74G-CX-Yoe5G-DVpZyjQ4.js → architectureDiagram-T3A2C74G-CX-Yoe5G-WO8ZZ5hV.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-B1eQZQ68-BtnxwtTR.js → blockDiagram-VBNYF7ZC-B1eQZQ68-k3zBEumU.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-SBrN9MrU-DBtqH7SN.js → c4Diagram-5PPSVZJV-SBrN9MrU-DJyJmi0Z.js} +1 -1
- package/dist/assets/channel-DKMa7Sn5-DXfk-pmR.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-DdkAx9o--u86pOcWg.js → chunk-2GRJ4B5K-DdkAx9o--CtXZO-EX.js} +1 -1
- package/dist/assets/{chunk-3NCLNEKW-CBztX243-BLYNocBE.js → chunk-3NCLNEKW-CBztX243-ChFYkHfP.js} +1 -1
- package/dist/assets/{chunk-4I5QYGJK-CmBJaHzs-BZpLhI2X.js → chunk-4I5QYGJK-CmBJaHzs-Bm62Y3D2.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-iaipMW_h-BJf96KC8.js → chunk-5RXB4S5H-iaipMW_h-DEVTDwqc.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-Cx8nz5_t-UvMPWJ3Z.js → chunk-6Q2QTUOP-Cx8nz5_t-BOfQsmdP.js} +1 -1
- package/dist/assets/{chunk-7Z6QIM7H-CZs6l8Md-D0Qz7WxJ.js → chunk-7Z6QIM7H-CZs6l8Md-Do5_5MRy.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-9OEKHEe4-DTBAv1YG.js → chunk-GF5L2VYU-9OEKHEe4-AmDI7rEM.js} +1 -1
- package/dist/assets/{chunk-I66GZJ75-DSPCVCny-BkEOB2v-.js → chunk-I66GZJ75-DSPCVCny-j7cLCRvd.js} +1 -1
- package/dist/assets/{chunk-J7OUQ5F2-CQoryswr-DXTdrzCF.js → chunk-J7OUQ5F2-CQoryswr-Cg0dLUo9.js} +2 -2
- package/dist/assets/{chunk-JQJVKLGR-6F2ui09L-DLAwdUpZ.js → chunk-JQJVKLGR-6F2ui09L-zzRmhTVI.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-DXXCSVOS-B_jSUmqA.js → chunk-KBJHAD2P-DXXCSVOS-BTbG9EBk.js} +1 -1
- package/dist/assets/{chunk-NSK5VX7P-DQgMj0Kn-Deqd9K3Z.js → chunk-NSK5VX7P-DQgMj0Kn-C7zBoCQL.js} +1 -1
- package/dist/assets/{chunk-QR6OTTB3-BQl8P7pD-DaWEByPl.js → chunk-QR6OTTB3-BQl8P7pD-MEPt0Y91.js} +1 -1
- package/dist/assets/{chunk-UBXNYLIW-BbdYEvTI-D7JTDy1_.js → chunk-UBXNYLIW-BbdYEvTI-VqdYieFu.js} +1 -1
- package/dist/assets/{chunk-W5SLKNZC-DSJLk1SI-C_F7i-8n.js → chunk-W5SLKNZC-DSJLk1SI-VPw5Pjzg.js} +1 -1
- package/dist/assets/{chunk-WRU74C26-DOE_TBL8-Dy-_nO2j.js → chunk-WRU74C26-DOE_TBL8-DHbOuCok.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-C1ePmEG1-B3i_KKpa.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-C_O3VNBf-B3i_KKpa.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DvSvuXnG-CqfiaAyR.js +1 -0
- package/dist/assets/{cynefinDiagram-MW4NZA55-DuBeESb6-BlFNNVb9.js → cynefinDiagram-MW4NZA55-DuBeESb6-D6_mAjNA.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-W5cB3U70-C6rRTcdS.js → dagre-VZM6K2ZE-W5cB3U70-D7FFnwoM.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-Fk1aWqfX-q5fpJacu.js → diagram-7IWD3JNH-Fk1aWqfX-Bh12UhXU.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-q-8xygfU-CcYA2g8i.js → diagram-B4RE2ZJO-q-8xygfU-C9wxc-GM.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-1SKCcOis-D_F_6O0X.js → diagram-LBJQPF4R-1SKCcOis-CqRHacMO.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-CtW4StAQ-C3aJWxEe.js → diagram-Q27KOJAE-CtW4StAQ-CYiDyxcF.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-r5fK-tbF-CL_8gWJg.js → diagram-UB23O5K3-r5fK-tbF-Bi_FwLPS.js} +1 -1
- package/dist/assets/{dist-CRLhMwW0.js → dist-CO4BIKZL.js} +1 -1
- package/dist/assets/{dist-uA1zE0Xj.js → dist-CqzxGXJS.js} +1 -1
- package/dist/assets/{dist-OriYIc6j.js → dist-cFsAUX4o.js} +1 -1
- package/dist/assets/{dist-BjFiDKpv.js → dist-eE3p97ig.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-BZoIqTyx-CPTvTKcL.js → ebnfDiagram-BXEA7PRR-BZoIqTyx-Cd_1jE2t.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-CM8pI2JI-CfggsaCA.js → erDiagram-JOGREHBK-CM8pI2JI-Buk10pBd.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-jdctDg0T-Bn8nOYEy.js +1 -0
- package/dist/assets/flowDiagram-UKHOOZJN-CR3dcoRl-BxBIpUfi.js +1 -0
- package/dist/assets/{ganttDiagram-PKOTCBZU-DYTkxDIh-4aHR8C-W.js → ganttDiagram-PKOTCBZU-DYTkxDIh-CqU6YC49.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-C4cKwl5j-Dbu1Qyjw.js +1 -0
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-DafidN4D-ifP2KGRx.js → gitGraphDiagram-DS77QQ5N-DafidN4D-w68Vbb_a.js} +1 -1
- package/dist/assets/index-BjTAj1ZH.css +1 -0
- package/dist/assets/{index-BuHQA0Pk.js → index-Cuz-LCIn.js} +204 -204
- package/dist/assets/info-DKCQHKI2-DPUCBYWS-SU-TWbo5.js +1 -0
- package/dist/assets/{infoDiagram-6WML65LV-DxSxsg1o-D_78B71T.js → infoDiagram-6WML65LV-DxSxsg1o-BsTGEEUd.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-kAIMMFB5-oplZ_1Pa.js → ishikawaDiagram-WSZJBQD7-kAIMMFB5-o1K9iQLz.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-ClybI8XI-BZ5uwKfU.js → journeyDiagram-NVQOT4AX-ClybI8XI-ffXeCp87.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-DDYHyW1q-_oRnFPcu.js → kanban-definition-27J2QSJJ-DDYHyW1q-DrGRavO5.js} +1 -1
- package/dist/assets/{lib-Ct1_7VHV.js → lib-B-Uaiffr.js} +1 -1
- package/dist/assets/{line-CtRAsVKI-CI3c9zx9.js → line-CtRAsVKI-BNctooIe.js} +1 -1
- package/dist/assets/{marp-Bo-5Z5k9.js → marp-BOJtZch0.js} +1 -1
- package/dist/assets/{mermaid-parser.core-C1baW7NG-DtjLsxX1.js → mermaid-parser.core-C1baW7NG-Bbu-Zwi4.js} +2 -2
- package/dist/assets/{mermaid.core-CGR3z-ay-DHUjgdHw.js → mermaid.core-CGR3z-ay-CFg0QiLR.js} +3 -3
- package/dist/assets/{mindmap-definition-FAOFIHXS-DU-wbYKK-DXKx1mdO.js → mindmap-definition-FAOFIHXS-DU-wbYKK-CgvK273C.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-CC7His0F-DAOgr6cQ.js +1 -0
- package/dist/assets/{pegDiagram-VL7TDLO6-CZuV1UUk-X_nosig9.js → pegDiagram-VL7TDLO6-CZuV1UUk-B6YJmD0c.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-CCrcq2sL-xi5VG5VX.js +1 -0
- package/dist/assets/{pieDiagram-7S7Q4E2Y-B6cd1CWd-BmhQL9Df.js → pieDiagram-7S7Q4E2Y-B6cd1CWd-rfzlH8Ma.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-BvGHVjRA-Cq90xL2F.js → quadrantDiagram-CIZ2JOQS-BvGHVjRA-NzM-AzBP.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-C5TUrpHf-i8K7RZ0u.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-CGuuFEd8-DB7F_dkQ.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-DuoCSFZY-CDOZa3yV.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-ig-DG9e_-B6N2Fb5S.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-BkBgpyvu-BgOVUqp7.js +1 -0
- package/dist/assets/{railroadDiagram-AXF67PYL-DDtBla5s-BcO3_KAp.js → railroadDiagram-AXF67PYL-DDtBla5s-CfdyAahV.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-DEh2MJlW-B7Td4CEk.js → requirementDiagram-LRYGKXZP-DEh2MJlW-uD_TKJdP.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-CHkaBx7U-D9uxGUNN.js → sankeyDiagram-W5VNT64P-CHkaBx7U-CUzwA-x1.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-D8tHDhWP-fET8ZLmj.js → sequenceDiagram-SI44F4Z6-D8tHDhWP-8SAFRxEZ.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-KNjKsfsT-DXNKv6KU.js → stateDiagram-OKZ733FA-KNjKsfsT-xMpzFiGU.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-B88w7Ous-CPHeWl4W.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-D_yJNxBK-CNDVT87D.js → swimlanes-SLNWSIFB-D_yJNxBK-CnzP6SsO.js} +1 -1
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DNcZzqTp-CYrdlCH9.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-BcEcbSq1-p5BO33mX.js → timeline-definition-Z64GVDOM-BcEcbSq1-C8coL46S.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-CqfT0I1z-DoUWczkJ.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CiJkiGWo-DYceyM_q.js +1 -0
- package/dist/assets/{vennDiagram-T6HMQDX7-CxS3fcVg-DJyPdK87.js → vennDiagram-T6HMQDX7-CxS3fcVg-E_L5qNHt.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-D1tuVHAV-D0RuWWEW.js +1 -0
- package/dist/assets/{wardleyDiagram-T6FBY63Y-D_XCiR14-BRfsmWsu.js → wardleyDiagram-T6FBY63Y-D_XCiR14-C1PqnnKt.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-BU4yBDeE-BGJGKF5o.js → xychartDiagram-ELKLHX3M-BU4yBDeE-Dm2FQAqw.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +5 -3
- package/server/backends/remoteHost/getFeed.spec.ts +3 -0
- package/server/backends/remoteHost/handlers/deps.ts +6 -0
- package/server/backends/remoteHost/handlers/terminalSession.ts +45 -1
- package/server/backends/remoteHost/handlers.spec.ts +74 -0
- package/server/backends/remoteHost/index.ts +2 -0
- package/server/backends/remoteHost/routes.spec.ts +55 -28
- package/server/backends/remoteView.ts +18 -6
- package/server/backends/sharedApp/appViews.ts +494 -0
- package/server/backends/sharedApp/context.ts +3 -11
- package/server/backends/sharedApp/declare.ts +2 -1
- package/server/backends/sharedApp/deploy.ts +81 -39
- package/server/backends/sharedApp/exclusivity.ts +1 -1
- package/server/backends/sharedApp/publicForm.ts +1 -1
- package/server/backends/sharedApp/publicView.ts +39 -23
- package/server/backends/sharedApp/publish.ts +66 -25
- package/server/backends/sharedApp/scopedFields.ts +1 -1
- package/server/backends/sharedApp/slug.ts +1 -1
- package/server/backends/sharedApp/staged.ts +1 -1
- package/server/backends/sharedApp/unpublish.ts +17 -1
- package/server/config/app-config.ts +16 -0
- package/server/config/config-routes.ts +8 -0
- package/server/index.ts +12 -16
- package/server/infra/shared-app-tool.ts +29 -0
- package/server/routes/app-routes.ts +35 -20
- package/server/routes/hook-routes.ts +35 -2
- package/server/routes/tool-routes.ts +34 -0
- package/server/session/answerQuestion.ts +136 -0
- package/server/session/answerQuestionOnHost.ts +63 -0
- package/server/session/lifecycle.ts +6 -0
- package/server/session/pty-connection.ts +11 -0
- package/server/session/pty-exit.ts +4 -0
- package/server/session/write-to-session.ts +84 -0
- package/server/skills/mulmoterminal-keys/SKILL.md +25 -0
- package/server/skills/mulmoterminal-shared-app/SKILL.md +160 -13
- package/server/skills/mulmoterminal-shared-app/templates/meeting-room.md +320 -0
- package/server/skills/mulmoterminal-shared-app/templates/salon.md +133 -14
- package/dist/assets/architecture-TIHT7OUA-Cqhmai3f-B4pyJ5Jo.js +0 -1
- package/dist/assets/channel-DKMa7Sn5-CIdApohK.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-C1ePmEG1-DObwVRUJ.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-C_O3VNBf-DObwVRUJ.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-DvSvuXnG-zjkRXhew.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-jdctDg0T-CsyWyhm5.js +0 -1
- package/dist/assets/flowDiagram-UKHOOZJN-CR3dcoRl-BFzPF9y4.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-C4cKwl5j-BuptX4em.js +0 -1
- package/dist/assets/index-AtHGuBVM.css +0 -1
- package/dist/assets/info-DKCQHKI2-DPUCBYWS-BfasXQqt.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CC7His0F-Bp4Rns7X.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CCrcq2sL-DbHN1-XM.js +0 -1
- package/dist/assets/radar-I7S5WNFK-C5TUrpHf-C1W4ONqs.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-CGuuFEd8-BkiWoTlC.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-DuoCSFZY-CdI6fYhe.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-ig-DG9e_-CHRw2D_X.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6-BkBgpyvu-CmelJuJj.js +0 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-B88w7Ous-CLsjS58j.js +0 -1
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DNcZzqTp-CXOGWaqF.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-CqfT0I1z-C1hn71OG.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CiJkiGWo-BCLsJDCI.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-D1tuVHAV-Bu2YT3gj.js +0 -1
package/README.md
CHANGED
|
@@ -595,6 +595,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
|
|
|
595
595
|
| `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). A stepper in the same Settings section covers the range. |
|
|
596
596
|
| `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). Settings → **Terminal keys** offers both, worded as behaviour. |
|
|
597
597
|
| `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. There is a checkbox in Settings → **Terminal keys**, applied at once; a hand edit of the file needs a **server restart, then a tab reload** (the server reads this file once at startup, and the browser reads the value from it on load). 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). |
|
|
598
|
+
| `questionPaneEnabled` | `true` offers a Claude session's **`AskUserQuestion` choices as buttons** in a pane beside the enlarged terminal. **The terminal's own dialog stays** and the pane drives it — a click presses the arrow keys and Enter in the real dialog, so either end can answer and the first one wins. Claude sessions only (the choices arrive on Claude Code's hooks). A **single** question can also be answered in your own words — a text box under the buttons writes into the dialog's own `Type something` row; several questions at once, or a multi-select one, get buttons only, and `Chat about this` stays in the terminal. **Off by default** — it lets a pane type into your terminal. Checkbox in Settings → **Terminal keys**, applied at once (the server re-reads the file per question). See the [Feature reference](https://receptron.github.io/mulmoterminal/guide/en/features.html#question-pane). |
|
|
598
599
|
| `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. Settings → **Sessions and background tasks** has the switch. |
|
|
599
600
|
| `issueWorkComments` | Let a cell **comment on the issue it is working on**: **one comment**, posted when the work starts and then **edited** as the PR opens and merges (closing the issue if the forge has not already), each milestone stamped in UTC. The comment names the working **directory** it happened in — the folder name only, never the path — so a reader can tell which clone, and two terminals do not start the same issue twice. It says it came from MulmoTerminal. CI is deliberately not reported: it is on the PR already, and it flaps. **Off by default**; it writes to the forge, often on somebody else's issue. Needs the matching CLI logged in — `gh` for GitHub, `glab` for gitlab.com and any host declared in `gitlabHosts`. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#issue-work-comments). Editable in Settings → **GitHub and GitLab**. |
|
|
600
601
|
| `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, from Settings → **GitHub and GitLab** or the file — read per PR and per session spawn, so no restart is needed, and a second MulmoTerminal beside this one sees the change too. Appending is idempotent: an existing PR never gets a second copy. |
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { isRecord } from "./isRecord.js";
|
|
2
|
+
|
|
3
|
+
// The pub/sub channel carrying "this session is asking the user a question". Both sides decide
|
|
4
|
+
// from it — the server publishes what the PreToolUse hook reported, the pane listens to know a
|
|
5
|
+
// dialog is up and what it offers — so the name and the payload shape live here.
|
|
6
|
+
export const ASK_QUESTION_CHANNEL = "ask-question";
|
|
7
|
+
|
|
8
|
+
/** The tool name whose input this module reads. Claude Code's built-in question dialog. */
|
|
9
|
+
export const ASK_QUESTION_TOOL = "AskUserQuestion";
|
|
10
|
+
|
|
11
|
+
export interface AskOption {
|
|
12
|
+
label: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AskQuestion {
|
|
17
|
+
question: string;
|
|
18
|
+
header: string;
|
|
19
|
+
options: AskOption[];
|
|
20
|
+
multiSelect: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AskQuestionEvent {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
toolUseId: string;
|
|
26
|
+
questions: AskQuestion[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The same dialog, now answered — in the terminal, in the pane, or cancelled with Esc. The pane
|
|
30
|
+
* drops its buttons on this: keys aimed at a dialog that has closed would reach the prompt
|
|
31
|
+
* underneath, where Down walks the input history and Enter would re-submit what it found. */
|
|
32
|
+
export interface AskQuestionDone {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
toolUseId: string;
|
|
35
|
+
done: true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parseOption = (value: unknown): AskOption | null => {
|
|
39
|
+
if (!isRecord(value) || typeof value.label !== "string" || !value.label) return null;
|
|
40
|
+
return typeof value.description === "string" ? { label: value.label, description: value.description } : { label: value.label };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const isOption = (value: AskOption | null): value is AskOption => value !== null;
|
|
44
|
+
|
|
45
|
+
const parseQuestion = (value: unknown): AskQuestion | null => {
|
|
46
|
+
if (!isRecord(value) || typeof value.question !== "string" || !Array.isArray(value.options)) return null;
|
|
47
|
+
const options = value.options.map(parseOption).filter(isOption);
|
|
48
|
+
// Length mismatch means one option failed to parse: answering by index against a list we
|
|
49
|
+
// could not read whole would pick the wrong row, so the whole question is rejected instead.
|
|
50
|
+
if (options.length === 0 || options.length !== value.options.length) return null;
|
|
51
|
+
return { question: value.question, header: typeof value.header === "string" ? value.header : "", options, multiSelect: value.multiSelect === true };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const isQuestion = (value: AskQuestion | null): value is AskQuestion => value !== null;
|
|
55
|
+
|
|
56
|
+
/** Read a PreToolUse `tool_input` for AskUserQuestion. Null when it is not one, or not whole. */
|
|
57
|
+
export const parseAskQuestions = (toolInput: unknown): AskQuestion[] | null => {
|
|
58
|
+
if (!isRecord(toolInput) || !Array.isArray(toolInput.questions)) return null;
|
|
59
|
+
const questions = toolInput.questions.map(parseQuestion).filter(isQuestion);
|
|
60
|
+
if (questions.length === 0 || questions.length !== toolInput.questions.length) return null;
|
|
61
|
+
return questions;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const identifiesADialog = (data: unknown): data is Record<string, unknown> & { sessionId: string; toolUseId: string } =>
|
|
65
|
+
isRecord(data) && typeof data.sessionId === "string" && typeof data.toolUseId === "string";
|
|
66
|
+
|
|
67
|
+
export const isAskQuestionEvent = (data: unknown): data is AskQuestionEvent =>
|
|
68
|
+
identifiesADialog(data) && data.done !== true && parseAskQuestions(data) !== null;
|
|
69
|
+
|
|
70
|
+
export const isAskQuestionDone = (data: unknown): data is AskQuestionDone => identifiesADialog(data) && data.done === true;
|
|
71
|
+
|
|
72
|
+
// The close is published even while the pane is switched OFF, and only the offer is gated.
|
|
73
|
+
// Turning the switch off mid-dialog would otherwise strand a pane that is already showing
|
|
74
|
+
// buttons: the dialog closes, nothing says so, and the next click sends Down/Enter into the
|
|
75
|
+
// prompt underneath — the exact stale-input case the close exists to prevent. A close for a
|
|
76
|
+
// question that was never offered is a no-op in the pane, so this costs nothing when the switch
|
|
77
|
+
// was off all along, and it carries no question text either way.
|
|
78
|
+
export const shouldPublishQuestion = (event: AskQuestionEvent | AskQuestionDone, paneEnabled: boolean): boolean => isAskQuestionDone(event) || paneEnabled;
|
|
79
|
+
|
|
80
|
+
/** As much of a recorded tool call as picking the open dialog out of a history needs. */
|
|
81
|
+
export interface RecordedCall {
|
|
82
|
+
toolUseId?: string | undefined;
|
|
83
|
+
toolName?: string | undefined;
|
|
84
|
+
toolInput?: unknown;
|
|
85
|
+
status?: string | undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The dialog a session is blocked on right now, rebuilt from its tool-call history. The pane's
|
|
89
|
+
// channel is event-only and replays nothing on reconnect, so a browser that reloads between the
|
|
90
|
+
// question and its answer would otherwise never learn about a session that is waiting — and with
|
|
91
|
+
// no button to open the pane, that question becomes unanswerable from the browser entirely.
|
|
92
|
+
//
|
|
93
|
+
// The LAST call, not the last one that happens to still say `running`. A session blocked on a
|
|
94
|
+
// question runs nothing else until it is answered, so a later call is proof the question is over —
|
|
95
|
+
// however it ended. Scanning back instead would keep offering a dialog whose close never arrived
|
|
96
|
+
// (an interrupted turn, a `/clear`, a hook that did not land) long after the agent moved on, and
|
|
97
|
+
// the buttons would type into whatever the screen became.
|
|
98
|
+
export const openQuestionOf = (calls: readonly RecordedCall[], sessionId: string): AskQuestionEvent | null => {
|
|
99
|
+
const last = calls[calls.length - 1];
|
|
100
|
+
if (last?.toolName !== ASK_QUESTION_TOOL || last.status !== "running" || !last.toolUseId) return null;
|
|
101
|
+
const questions = parseAskQuestions(last.toolInput);
|
|
102
|
+
return questions ? { sessionId, toolUseId: last.toolUseId, questions } : null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** Why an answer did not reach the dialog. Serialized by the host, rendered by every client, so
|
|
106
|
+
* the values live here rather than once per layer — a drift between them is a client showing the
|
|
107
|
+
* wrong explanation, or none. */
|
|
108
|
+
export type AnswerFailure =
|
|
109
|
+
/** The dialog was answered (in the terminal, or by another client) before this arrived. */
|
|
110
|
+
| "closed"
|
|
111
|
+
/** picks do not fit the questions — wrong count, out of range, or not ascending. */
|
|
112
|
+
| "bad-picks"
|
|
113
|
+
/** No PTY in this process to type into: the session outlived a server restart. */
|
|
114
|
+
| "unwritable"
|
|
115
|
+
/** Some keys reached the dialog before the rest were abandoned. Its cursor has moved and its
|
|
116
|
+
* boxes may be ticked, so where it stands is no longer knowable from here — a fresh sequence
|
|
117
|
+
* would be computed from a state the dialog left behind and could commit a different answer. */
|
|
118
|
+
| "partial";
|
|
119
|
+
|
|
120
|
+
export type AnswerResult = { ok: true } | { ok: false; reason: AnswerFailure };
|
|
121
|
+
|
|
122
|
+
/** The body of `POST /api/question/:sessionId/answer`. The session comes from the route path, not
|
|
123
|
+
* from here — a client names the dialog it is answering, never the session it belongs to. */
|
|
124
|
+
export interface AnswerRequestBody {
|
|
125
|
+
toolUseId: string;
|
|
126
|
+
/** Chosen option indexes, one entry per question. */
|
|
127
|
+
picks?: number[][];
|
|
128
|
+
/** Or the user's own words, for the dialog's text row. */
|
|
129
|
+
text?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Read a request body's SHAPE. Whether the picks or the words fit the dialog is decided against
|
|
133
|
+
* the questions the host itself recorded — see server/session/answerQuestion.ts. */
|
|
134
|
+
export const readAnswerRequest = (body: unknown): AnswerRequestBody | null => {
|
|
135
|
+
if (!isRecord(body) || typeof body.toolUseId !== "string") return null;
|
|
136
|
+
const text = typeof body.text === "string" ? { text: body.text } : {};
|
|
137
|
+
return { toolUseId: body.toolUseId, ...(body.picks === undefined ? {} : { picks: readPickRows(body.picks) }), ...text };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Rows of numbers, or nothing. A row that is not that is dropped to `[]` rather than coerced: the
|
|
141
|
+
// host then refuses it as bad-picks, and a coerced pick is a keystroke aimed at a row nobody chose.
|
|
142
|
+
const readPickRows = (picks: unknown): number[][] =>
|
|
143
|
+
Array.isArray(picks) ? picks.map((row) => (Array.isArray(row) ? row.filter((idx): idx is number => typeof idx === "number") : [])) : [];
|
|
144
|
+
|
|
145
|
+
export const isAnswerFailure = (value: unknown): value is AnswerFailure =>
|
|
146
|
+
value === "closed" || value === "bad-picks" || value === "unwritable" || value === "partial";
|
|
147
|
+
|
|
148
|
+
const KEY_DOWN = "\x1b[B";
|
|
149
|
+
const KEY_ENTER = "\r";
|
|
150
|
+
|
|
151
|
+
// The dialog lists MORE rows than `options`: a "Type something" row follows them, and a
|
|
152
|
+
// multi-select question puts its Submit row after that. So Submit sits at options.length + 1.
|
|
153
|
+
// Measured against claude 2.1.231, not inferred from the tool schema — the extra rows exist
|
|
154
|
+
// only on screen, and reading the schema alone would aim every multi-select Enter one row short.
|
|
155
|
+
const submitRow = (options: readonly AskOption[]): number => options.length + 1;
|
|
156
|
+
|
|
157
|
+
const moveDown = (from: number, to: number): string[] => Array.from({ length: Math.max(0, to - from) }, () => KEY_DOWN);
|
|
158
|
+
|
|
159
|
+
// Enter TOGGLES a multi-select row and leaves the cursor where it is, so each pick is
|
|
160
|
+
// "walk down to it, toggle", and the walk starts from the previous pick rather than the top.
|
|
161
|
+
const toggleEach = (picks: readonly number[]): { keys: string[]; cursor: number } =>
|
|
162
|
+
picks.reduce<{ keys: string[]; cursor: number }>(({ keys, cursor }, idx) => ({ keys: [...keys, ...moveDown(cursor, idx), KEY_ENTER], cursor: idx }), {
|
|
163
|
+
keys: [],
|
|
164
|
+
cursor: 0,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const keysForOne = (question: AskQuestion, picks: readonly number[]): string[] => {
|
|
168
|
+
if (!question.multiSelect) return [...moveDown(0, picks[0] ?? 0), KEY_ENTER];
|
|
169
|
+
const { keys, cursor } = toggleEach(picks);
|
|
170
|
+
return [...keys, ...moveDown(cursor, submitRow(question.options)), KEY_ENTER];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Ascending and distinct, because the walk above only ever moves DOWN: an out-of-order pick
|
|
174
|
+
// would silently toggle the wrong row rather than fail.
|
|
175
|
+
const picksValid = (question: AskQuestion, picks: readonly number[]): boolean => {
|
|
176
|
+
const inRange = picks.every((idx) => Number.isInteger(idx) && idx >= 0 && idx < question.options.length);
|
|
177
|
+
const ascending = picks.every((idx, i) => i === 0 || idx > (picks[i - 1] ?? -1));
|
|
178
|
+
return inRange && ascending && (question.multiSelect || picks.length === 1);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// The review screen ("Ready to submit your answers?", with Submit preselected) appears for every
|
|
182
|
+
// shape EXCEPT a lone single-select question, which commits on the option's own Enter. All four
|
|
183
|
+
// shapes were measured; guessing here either leaves the dialog open or drops a stray Enter into
|
|
184
|
+
// the prompt once it has closed.
|
|
185
|
+
const needsReview = (questions: readonly AskQuestion[]): boolean => questions.length > 1 || questions.some((question) => question.multiSelect);
|
|
186
|
+
|
|
187
|
+
// Answering in the user's own words, through the dialog's own `Type something` row.
|
|
188
|
+
//
|
|
189
|
+
// That row IS a text field, and the way in is to highlight it and type: the row then reads back what
|
|
190
|
+
// was typed, and Enter commits it as the ANSWER — `answers` carries the words, not a decline.
|
|
191
|
+
// Measured against claude 2.1.231. (Enter on the row while it is still empty declines instead,
|
|
192
|
+
// which is what a first reading of it mistook for its whole behaviour.)
|
|
193
|
+
//
|
|
194
|
+
// It sits directly after that question's options.
|
|
195
|
+
export const keysToAnswerInWords = (questions: readonly AskQuestion[], text: string): string[] | null => {
|
|
196
|
+
const only = questions.length === 1 ? questions[0] : undefined;
|
|
197
|
+
// The one shape this was measured on: a lone SINGLE-select question, where the text row's Enter
|
|
198
|
+
// is the whole answer. The others each end somewhere this sequence does not reach — a wizard
|
|
199
|
+
// moves on to its next question, and a multi-select dialog stops at its review screen — and the
|
|
200
|
+
// host would claim the dialog while the terminal still waits for something the pane cannot send.
|
|
201
|
+
if (!only || only.multiSelect || !text) return null;
|
|
202
|
+
return [...moveDown(0, only.options.length), text, KEY_ENTER];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The keystrokes that answer a live AskUserQuestion dialog. `picks[i]` holds the chosen option
|
|
207
|
+
* indexes of `questions[i]` — exactly one for a single-select question, any ascending set for a
|
|
208
|
+
* multi-select one. Null when the picks do not fit the questions, so a caller cannot half-drive
|
|
209
|
+
* a dialog it has misread.
|
|
210
|
+
*/
|
|
211
|
+
export const keysForAnswers = (questions: readonly AskQuestion[], picks: readonly (readonly number[])[]): string[] | null => {
|
|
212
|
+
if (questions.length === 0 || picks.length !== questions.length) return null;
|
|
213
|
+
const paired = questions.map((question, i) => ({ question, picks: picks[i] ?? [] }));
|
|
214
|
+
if (!paired.every(({ question, picks: chosen }) => picksValid(question, chosen))) return null;
|
|
215
|
+
const keys = paired.flatMap(({ question, picks: chosen }) => keysForOne(question, chosen));
|
|
216
|
+
return needsReview(questions) ? [...keys, KEY_ENTER] : keys;
|
|
217
|
+
};
|
package/common/gitlabHosts.ts
CHANGED
|
@@ -10,7 +10,18 @@ import { GITHUB_HOST, GITLAB_HOST } from "./repoEntry.js";
|
|
|
10
10
|
// A hostname, lower case, with at least one dot. The dot is not cosmetic: `parseRepoEntry` reads a
|
|
11
11
|
// leading segment as a host ONLY when it contains one, so a dotless declaration could never match
|
|
12
12
|
// an entry and would sit in the config doing nothing.
|
|
13
|
-
|
|
13
|
+
// Tested per label rather than as one hostname pattern: the single-regex form nested `[a-z0-9-]*`
|
|
14
|
+
// inside the repeated dotted group, which is the shape that goes exponential on input that almost
|
|
15
|
+
// matches. Measured, this one did not — but the per-label form removes the class instead of
|
|
16
|
+
// depending on that, and reads as the rule it enforces.
|
|
17
|
+
const LABEL_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
18
|
+
|
|
19
|
+
const isLabel = (label: string): boolean => LABEL_RE.test(label) && !label.endsWith("-");
|
|
20
|
+
|
|
21
|
+
const isHostname = (host: string): boolean => {
|
|
22
|
+
const labels = host.split(".");
|
|
23
|
+
return labels.length > 1 && labels.every(isLabel);
|
|
24
|
+
};
|
|
14
25
|
|
|
15
26
|
const SCHEME_RE = /^https?:\/\//;
|
|
16
27
|
|
|
@@ -26,7 +37,7 @@ const GITLAB_HOSTS_MAX = 20;
|
|
|
26
37
|
export function normalizeGitlabHost(input: unknown): string | null {
|
|
27
38
|
if (typeof input !== "string") return null;
|
|
28
39
|
const host = input.trim().toLowerCase().replace(SCHEME_RE, "").replace(/\/$/, "");
|
|
29
|
-
return
|
|
40
|
+
return isHostname(host) ? host : null;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
/** The declared hosts, de-duplicated and capped. Anything unusable is dropped rather than failing
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// What a terminal emulator sends back on the INPUT channel without anyone typing.
|
|
2
|
+
//
|
|
3
|
+
// xterm.js answers the application's own queries there: device attributes, the foreground and
|
|
4
|
+
// background colours, the cursor position. It also says when the terminal gained or lost focus.
|
|
5
|
+
// All of it arrives exactly as a keystroke does, and none of it is the user answering anything.
|
|
6
|
+
//
|
|
7
|
+
// That matters to anything asking "has the user typed since this question appeared?"
|
|
8
|
+
// (server/session/write-to-session.ts). Counting these replies meant an answer from the question
|
|
9
|
+
// pane was refused after any attach, resize or theme read — measured, not guessed: a single button
|
|
10
|
+
// press was preceded by `ESC[?1;2c`, `ESC[>0;276;0c` and two OSC colour replies (#1693).
|
|
11
|
+
//
|
|
12
|
+
// MOUSE reports are deliberately absent. They look the same on the wire, but a click can select an
|
|
13
|
+
// option in the dialog — that is the user answering, and an answer already being typed must yield
|
|
14
|
+
// to it exactly as it yields to a keystroke.
|
|
15
|
+
//
|
|
16
|
+
// The forms are spelled out rather than generalised — no "any OSC", no "any CSI". This list decides
|
|
17
|
+
// what CANNOT interrupt an answer, so a pattern wider than the replies actually queried would hand
|
|
18
|
+
// that exemption to input nobody has looked at.
|
|
19
|
+
const ESC = "\u001b";
|
|
20
|
+
const BEL = "\u0007";
|
|
21
|
+
const ST = `(?:${BEL}|${ESC}\\\\)`;
|
|
22
|
+
|
|
23
|
+
const REPLIES = [
|
|
24
|
+
`${ESC}\\[[IO]`, // focus in / out
|
|
25
|
+
`${ESC}\\[\\?[\\d;]*c`, // primary device attributes
|
|
26
|
+
`${ESC}\\[>[\\d;]*c`, // secondary / tertiary device attributes
|
|
27
|
+
`${ESC}\\[\\d+;\\d+R`, // cursor position report
|
|
28
|
+
`${ESC}\\]1[0-9];rgb:[\\da-fA-F/]*${ST}`, // colour queries — foreground, background, cursor
|
|
29
|
+
].join("|");
|
|
30
|
+
|
|
31
|
+
const ANY_REPLY = new RegExp(REPLIES, "g");
|
|
32
|
+
|
|
33
|
+
// A chunk can END mid-sequence: the socket splits where it likes, so `ESC[?1` and `;2c` can arrive
|
|
34
|
+
// separately. Neither half matches, and both would read as "the user typed" — the very false alarm
|
|
35
|
+
// this module exists to stop. A tail that could still GROW into a reply is held instead of judged.
|
|
36
|
+
//
|
|
37
|
+
// It stops before the terminating letter, which is what keeps a real key out of the holding pen:
|
|
38
|
+
// `ESC[B` (Down) carries its `B` already and cannot be waiting for anything.
|
|
39
|
+
//
|
|
40
|
+
// A LONE Escape is never held. It is how the user cancels the dialog, and holding it would let a
|
|
41
|
+
// paced answer carry on typing into a question they had just dismissed — while the reply it might
|
|
42
|
+
// theoretically have grown into is written whole by the emulator, in one frame, every time.
|
|
43
|
+
const STILL_GROWING = new RegExp(`^${ESC}(?:\\[[?>]?[\\d;]*|\\][\\d;]*(?:rgb:[\\da-fA-F/]*)?)$`);
|
|
44
|
+
|
|
45
|
+
/** The longest tail worth holding. Bounded so a stream of junk cannot accumulate. */
|
|
46
|
+
const MAX_PENDING = 64;
|
|
47
|
+
|
|
48
|
+
export interface InputScan {
|
|
49
|
+
/** Did anything in this chunk come from the user? */
|
|
50
|
+
fromUser: boolean;
|
|
51
|
+
/** The unfinished tail, to be carried into the next chunk. */
|
|
52
|
+
pending: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Classify one chunk of terminal input, carrying `pending` from the chunk before it.
|
|
57
|
+
*
|
|
58
|
+
* `fromUser` is false only when everything is a reply the emulator produced by itself. A trailing
|
|
59
|
+
* sequence that is still growing is neither — it waits in `pending` until it finishes.
|
|
60
|
+
*/
|
|
61
|
+
export const scanForUserInput = (pending: string, data: string): InputScan => {
|
|
62
|
+
const chunk = `${pending}${data}`;
|
|
63
|
+
const rest = chunk.replace(ANY_REPLY, "");
|
|
64
|
+
if (rest.length > 0 && rest.length <= MAX_PENDING && STILL_GROWING.test(rest)) return { fromUser: false, pending: rest };
|
|
65
|
+
return { fromUser: rest.length > 0, pending: "" };
|
|
66
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-CvsWqFln-Tc7ZSrRz.js";import{_ as n,g as r}from"./mermaid-parser.core-C1baW7NG-
|
|
1
|
+
import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-CvsWqFln-Tc7ZSrRz.js";import{_ as n,g as r}from"./mermaid-parser.core-C1baW7NG-Bbu-Zwi4.js";import"./chunk-I66GZJ75-DSPCVCny-j7cLCRvd.js";import"./chunk-3NCLNEKW-CBztX243-ChFYkHfP.js";import{n as i,r as a,t as o}from"./chunk-6Q2QTUOP-Cx8nz5_t-BOfQsmdP.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-C1baW7NG-Bbu-Zwi4.js";export{e as createArchitectureServices};
|