mulmoterminal 6.2.0 → 6.4.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.
Files changed (101) hide show
  1. package/common/keymap.ts +113 -19
  2. package/common/mdPreviewMessage.ts +21 -2
  3. package/common/previewTheme.ts +39 -0
  4. package/common/themeVars.ts +7 -2
  5. package/dist/assets/{abnfDiagram-O67JEVCF-C1zjpyt6.js → abnfDiagram-O67JEVCF-0EIO6Rib.js} +1 -1
  6. package/dist/assets/architecture-7GRP2DOG-BrJq_YrR.js +1 -0
  7. package/dist/assets/{architectureDiagram-NJMV4G6O-Dh4NuwwE.js → architectureDiagram-NJMV4G6O-Dv4vmoGJ.js} +1 -1
  8. package/dist/assets/{blockDiagram-BEXU5L5S-BmtSvQPC.js → blockDiagram-BEXU5L5S-DtAUYedX.js} +1 -1
  9. package/dist/assets/{c4Diagram-YGBWAQC7-CJxoSiL6.js → c4Diagram-YGBWAQC7-BUjpgg36.js} +1 -1
  10. package/dist/assets/channel-1p-80xtZ.js +1 -0
  11. package/dist/assets/{chunk-3FUC2YCW-DMMruEcd.js → chunk-3FUC2YCW-BRaUCmDn.js} +1 -1
  12. package/dist/assets/{chunk-5DYCD2WN-Bl71EWDi.js → chunk-5DYCD2WN-BcCVJxNU.js} +1 -1
  13. package/dist/assets/{chunk-742MDFTN-C429-MoI.js → chunk-742MDFTN-r7J0AnEC.js} +1 -1
  14. package/dist/assets/{chunk-7INBJB4K-BBvpxDmr.js → chunk-7INBJB4K-BgjDPpSK.js} +1 -1
  15. package/dist/assets/{chunk-7M6MHVWA-021YZyhO.js → chunk-7M6MHVWA-BkohiMAa.js} +1 -1
  16. package/dist/assets/{chunk-7PRAP22T-BJG76bZX.js → chunk-7PRAP22T-BgtGs2QT.js} +1 -1
  17. package/dist/assets/{chunk-GTNCS2PH-I3-3mz_9.js → chunk-GTNCS2PH-CabGHxDI.js} +1 -1
  18. package/dist/assets/{chunk-GWA4HPMP-B47G1yLN.js → chunk-GWA4HPMP-ByBWBjP8.js} +1 -1
  19. package/dist/assets/{chunk-MBY4JIJT-Csr7e6LI.js → chunk-MBY4JIJT-CoqxPGVJ.js} +1 -1
  20. package/dist/assets/{chunk-NETBCI7D-CE5rKdCJ.js → chunk-NETBCI7D-C3TzGKdM.js} +1 -1
  21. package/dist/assets/{chunk-O7XYJQB3-DD5uf7v4.js → chunk-O7XYJQB3-C_ibJVHM.js} +1 -1
  22. package/dist/assets/{chunk-UA2S7LBM-do2dnctY.js → chunk-UA2S7LBM-cdcwqQWa.js} +1 -1
  23. package/dist/assets/{chunk-WEXAMYUT-DPC0vchG.js → chunk-WEXAMYUT-DpgQkB4Y.js} +1 -1
  24. package/dist/assets/{chunk-Z7XXMR3K-C660bAsb.js → chunk-Z7XXMR3K-43mpKAmk.js} +1 -1
  25. package/dist/assets/{chunk-ZIGJFQKS-CYuzZekO.js → chunk-ZIGJFQKS-BqjohiQA.js} +1 -1
  26. package/dist/assets/{classDiagram-v2-NBCMYWYE-C4L6KcyR.js → classDiagram-v2-NBCMYWYE--RyRI2eN.js} +1 -1
  27. package/dist/assets/{cynefin-OW5HDTMX-D6NfyzYL.js → cynefin-OW5HDTMX-Dr5PAqCO.js} +1 -1
  28. package/dist/assets/{cynefinDiagram-VND7K2PF-Vd-8pNNS.js → cynefinDiagram-VND7K2PF-CsJyNlep.js} +1 -1
  29. package/dist/assets/{dagre-6A5THRUB-D-7Yw7-l.js → dagre-6A5THRUB-C5ja50BF.js} +1 -1
  30. package/dist/assets/{diagram-22UHCM2B-CArtfv7Z.js → diagram-22UHCM2B-C4cl0V63.js} +1 -1
  31. package/dist/assets/{diagram-3UASUU5V-CP7L7_9J.js → diagram-3UASUU5V-DV_eMiwc.js} +1 -1
  32. package/dist/assets/{diagram-ATOU4E4O-4CGO7_Nv.js → diagram-ATOU4E4O-jzKf3iFX.js} +1 -1
  33. package/dist/assets/{diagram-CDSNMT55-DO9ZsCbO.js → diagram-CDSNMT55-BwFqusNo.js} +1 -1
  34. package/dist/assets/{diagram-MLGK6HIB-Rt7PU-rl.js → diagram-MLGK6HIB-CoMwibZN.js} +1 -1
  35. package/dist/assets/{diagram-MPIPVDR6-BZN51op4.js → diagram-MPIPVDR6-CR1GehW4.js} +1 -1
  36. package/dist/assets/{dist-S-qmLesP.js → dist-4y2pKI__.js} +1 -1
  37. package/dist/assets/{dist-Cqt7UR3S.js → dist-BtevH0MI.js} +1 -1
  38. package/dist/assets/{dist-CGRj7B3E.js → dist-Cl-PfToj.js} +1 -1
  39. package/dist/assets/{dist-BS6ENUZ5.js → dist-DzuN4wOr.js} +1 -1
  40. package/dist/assets/{ebnfDiagram-ZINNZB2B-4TBVkCad.js → ebnfDiagram-ZINNZB2B-DofcoTlD.js} +1 -1
  41. package/dist/assets/{elk-276RUBZZ-DvmgrOqN.js → elk-276RUBZZ-BPEoPGYi.js} +1 -1
  42. package/dist/assets/{erDiagram-OPXOYQCR-BViK6jL0.js → erDiagram-OPXOYQCR-BW9UZdWF.js} +1 -1
  43. package/dist/assets/eventmodeling-NTZA5JFV-B13TyR96.js +1 -0
  44. package/dist/assets/flowDiagram-KWPJA3E3-CU0tsDCp.js +1 -0
  45. package/dist/assets/{ganttDiagram-FUAMR5RP-DOHZe1GL.js → ganttDiagram-FUAMR5RP-QI2xjALT.js} +1 -1
  46. package/dist/assets/{gitGraph-4MIJSDKK-Bjw76Jgx.js → gitGraph-4MIJSDKK-DwWl22rs.js} +1 -1
  47. package/dist/assets/{gitGraphDiagram-X574FWY7-BsfWNDkM.js → gitGraphDiagram-X574FWY7-OdBtq_J3.js} +1 -1
  48. package/dist/assets/index-BMqji566.css +1 -0
  49. package/dist/assets/{index-D4TSv5Vq.js → index-DZ3P7G29.js} +341 -341
  50. package/dist/assets/{info-A6RAGUB7-CSDWzzYX.js → info-A6RAGUB7-D4dgNXV5.js} +1 -1
  51. package/dist/assets/{infoDiagram-VRGFBTTK-BkeBDik0.js → infoDiagram-VRGFBTTK-BLafPiGw.js} +1 -1
  52. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CaZjQCuc.js → ishikawaDiagram-OU5B5YK6-hNUEv0D2.js} +1 -1
  53. package/dist/assets/{journeyDiagram-ZHPQQLJL-DBYQQ2A-.js → journeyDiagram-ZHPQQLJL-COYfA96w.js} +1 -1
  54. package/dist/assets/{kanban-definition-PNTS6WVX-BGs8gNiX.js → kanban-definition-PNTS6WVX-C5jDklyw.js} +1 -1
  55. package/dist/assets/{line-DUl7ImOy.js → line-D8KBsMtt.js} +1 -1
  56. package/dist/assets/{mermaid-parser.core-BCgSuYuM.js → mermaid-parser.core-Dlf5XX-m.js} +3 -3
  57. package/dist/assets/{mermaid.core-CXneRxVh.js → mermaid.core-77q0N4Kj.js} +4 -4
  58. package/dist/assets/{mindmap-definition-NLK3R4M7-De20bPa8.js → mindmap-definition-NLK3R4M7-Bh3ST-uc.js} +1 -1
  59. package/dist/assets/{packet-AYTQ26CC-mJz8Bz7N.js → packet-AYTQ26CC-dzdfTl_4.js} +1 -1
  60. package/dist/assets/{pegDiagram-GJSIUBJH-CjYv3ydG.js → pegDiagram-GJSIUBJH-HUFfoKUw.js} +1 -1
  61. package/dist/assets/{pie-WAS4IAKB-BoYT2-Lx.js → pie-WAS4IAKB-0S5KvqDy.js} +1 -1
  62. package/dist/assets/{pieDiagram-5QR66LMP-BSCkw202.js → pieDiagram-5QR66LMP-BAUKrsuB.js} +1 -1
  63. package/dist/assets/{quadrantDiagram-O4NWA36T-cN5UVYyK.js → quadrantDiagram-O4NWA36T-WKXQPbHy.js} +1 -1
  64. package/dist/assets/{radar-RG4KPBEZ-BGvSx7Xa.js → radar-RG4KPBEZ-CmvfbUxs.js} +1 -1
  65. package/dist/assets/{railroad-74A4TZTK-DXEwjzqA.js → railroad-74A4TZTK-B1eU1nZI.js} +1 -1
  66. package/dist/assets/railroad-abnf-HS5TGJTU-Buwlc453.js +1 -0
  67. package/dist/assets/railroad-ebnf-LZEXJU2U-CTzd-w0L.js +1 -0
  68. package/dist/assets/railroad-peg-WCYAUIDC-BzSzMdhx.js +1 -0
  69. package/dist/assets/{railroadDiagram-XR7U4H2S-DcrFAMsB.js → railroadDiagram-XR7U4H2S-DI3y2Box.js} +1 -1
  70. package/dist/assets/{requirementDiagram-PLB6GJNP-BsS2J2kW.js → requirementDiagram-PLB6GJNP-BHVnyaGP.js} +1 -1
  71. package/dist/assets/{sankeyDiagram-IPEJSGJF-D2pxMjGY.js → sankeyDiagram-IPEJSGJF-DPhG8F4e.js} +1 -1
  72. package/dist/assets/{sequenceDiagram-PO4LG4MO-B9vVuW4z.js → sequenceDiagram-PO4LG4MO-BNnXeMyc.js} +1 -1
  73. package/dist/assets/{stateDiagram-v2-GCMORJYK-BjzELVSp.js → stateDiagram-v2-GCMORJYK-Dg80rzkM.js} +1 -1
  74. package/dist/assets/{swimlanes-2SLR337P-WOSIWdzc.js → swimlanes-2SLR337P-BjIVtOvB.js} +1 -1
  75. package/dist/assets/swimlanesDiagram-TC7HE7FX-_i8YJGRx.js +8 -0
  76. package/dist/assets/{timeline-definition-EJHVYXUP-jnMwXmH2.js → timeline-definition-EJHVYXUP-CWVsm4DO.js} +1 -1
  77. package/dist/assets/{treeView-Q6P3EWNA-9cyRJzxa.js → treeView-Q6P3EWNA-VIzH5Ryp.js} +1 -1
  78. package/dist/assets/{treemap-WGGIJYW6-HiTaQH4L.js → treemap-WGGIJYW6-DIurGTrO.js} +1 -1
  79. package/dist/assets/{usecaseDiagram-POWQR4AR-Slkru8mH.js → usecaseDiagram-POWQR4AR-CGjjyVXQ.js} +1 -1
  80. package/dist/assets/{vennDiagram-UO4OBE2U-BEaufcCS.js → vennDiagram-UO4OBE2U-D25_R90R.js} +1 -1
  81. package/dist/assets/{wardley-WFR3VGLG-DuQ2s-AL.js → wardley-WFR3VGLG-AZXL1jj9.js} +1 -1
  82. package/dist/assets/{wardleyDiagram-VNRHLVJA-BdpX2VuS.js → wardleyDiagram-VNRHLVJA-D3g7xxxA.js} +1 -1
  83. package/dist/assets/{xychartDiagram-PMCCYNJV-DyeDWDle.js → xychartDiagram-PMCCYNJV-C5FxR_Fd.js} +1 -1
  84. package/dist/index.html +2 -2
  85. package/package.json +7 -2
  86. package/server/config/keymap-check.ts +1 -1
  87. package/server/files/files-browse.ts +45 -10
  88. package/server/files/mdImageSrc.ts +53 -0
  89. package/server/files/mdPreviewEmbed.ts +11 -0
  90. package/server/files/pathContainment.ts +6 -4
  91. package/server/files/renderedDoc.ts +16 -1
  92. package/server/skills/mulmoterminal-keys/SKILL.md +28 -4
  93. package/dist/assets/architecture-7GRP2DOG-Cy27i--D.js +0 -1
  94. package/dist/assets/channel-CSCWwH8Q.js +0 -1
  95. package/dist/assets/eventmodeling-NTZA5JFV-Bm_UwsCa.js +0 -1
  96. package/dist/assets/flowDiagram-KWPJA3E3-DRt5T1of.js +0 -1
  97. package/dist/assets/index-C34PsKMj.css +0 -1
  98. package/dist/assets/railroad-abnf-HS5TGJTU-nQCMBk1C.js +0 -1
  99. package/dist/assets/railroad-ebnf-LZEXJU2U-C2aWtRTa.js +0 -1
  100. package/dist/assets/railroad-peg-WCYAUIDC-apE5DNQM.js +0 -1
  101. package/dist/assets/swimlanesDiagram-TC7HE7FX-B-NIBD7m.js +0 -8
package/common/keymap.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // The server sanitizes and persists it; the browser matches keydowns against it. One
8
8
  // definition here so the accepted syntax can't drift between the two.
9
9
  //
10
- // "keymap": { "zoom-next": "PageDown", "zoom-prev": "Shift+PageUp" }
10
+ // "keymap": { "zoom-next": "PageDown", "zoom-prev": "Shift+PageUp", "files-find": "Cmd+K p" }
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
13
  import { isRecord } from "./isRecord.js";
@@ -26,6 +26,7 @@ export const KEYMAP_ACTIONS = [
26
26
  "terminal-restart",
27
27
  "files-find",
28
28
  "files-search",
29
+ "command-palette",
29
30
  "copy",
30
31
  "paste",
31
32
  ] as const;
@@ -128,6 +129,9 @@ export function parseKeyBinding(input: string): KeyBinding | null {
128
129
  if (parts.length === 0 || parts.some((part) => part === "")) return null;
129
130
  const key = parts[parts.length - 1];
130
131
  if (key === undefined) return null; // unreachable: parts.length was checked above
132
+ // No browser reports a key with whitespace in it, so `"Cmd+K p"` read as ONE keystroke named
133
+ // "K p" was accepted and never fired. A sequence is spelled with a space; see parseKeySequence.
134
+ if (/\s/.test(key)) return null;
131
135
  const binding: KeyBinding = { key, shift: false, alt: false, ctrl: false, meta: false };
132
136
  for (const part of parts.slice(0, -1)) {
133
137
  const flag = MODIFIERS[part.toLowerCase()];
@@ -138,6 +142,47 @@ export function parseKeyBinding(input: string): KeyBinding | null {
138
142
  return MODIFIERS[key.toLowerCase()] ? null : binding;
139
143
  }
140
144
 
145
+ // The most keystrokes one binding can be. Two is a prefix and a key after it, as in tmux or Emacs's
146
+ // `C-x b` — enough to put many actions behind one key the browser lets through (#2265).
147
+ export const MAX_SEQUENCE_STROKES = 2;
148
+
149
+ // Parse `"Cmd+K p"` into its keystrokes, or null when it is malformed. Strokes are separated by
150
+ // whitespace, which no single keystroke can contain. A single keystroke is a sequence of one.
151
+ export function parseKeySequence(input: string): KeyBinding[] | null {
152
+ const strokes = input.trim().split(/\s+/);
153
+ if (strokes.length > MAX_SEQUENCE_STROKES) return null;
154
+ const parsed = strokes.map(parseKeyBinding);
155
+ return parsed.every((stroke): stroke is KeyBinding => stroke !== null) ? parsed : null;
156
+ }
157
+
158
+ // Esc with no modifier always ends a sequence's wait, so it can never be a sequence's second key.
159
+ export const isBareEscape = (e: KeymapKeyEvent): boolean => e.key === "Escape" && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey;
160
+
161
+ // Actions that can be bound to a sequence. Not `copy` / `paste`: they are decided inside the
162
+ // terminal, one keystroke at a time (TERMINAL_SCOPED_ACTIONS), and nothing there can wait.
163
+ export const takesSequence = (action: KeymapAction): boolean => !TERMINAL_SCOPED_ACTIONS.includes(action);
164
+
165
+ /** One action bound to two keystrokes. */
166
+ export interface SequenceBinding {
167
+ action: KeymapAction;
168
+ first: KeyBinding;
169
+ second: KeyBinding;
170
+ /** How the user wrote each keystroke, for the hint shown while waiting for the second. */
171
+ firstLabel: string;
172
+ secondLabel: string;
173
+ }
174
+
175
+ // Every two-keystroke binding in a keymap, in action order.
176
+ export function sequenceBindings(keymap: Keymap): SequenceBinding[] {
177
+ return KEYMAP_ACTIONS.flatMap((action): SequenceBinding[] => {
178
+ const raw = keymap[action];
179
+ const strokes = raw === undefined || !takesSequence(action) ? null : parseKeySequence(raw);
180
+ const [first, second] = strokes ?? [];
181
+ const [firstLabel, secondLabel] = raw?.trim().split(/\s+/) ?? [];
182
+ return first && second && firstLabel && secondLabel ? [{ action, first, second, firstLabel, secondLabel }] : [];
183
+ });
184
+ }
185
+
141
186
  // The structural shape of a keydown a binding is matched against. A real KeyboardEvent
142
187
  // satisfies it, and so does a plain test object — no DOM dependency.
143
188
  export interface KeymapKeyEvent {
@@ -159,6 +204,7 @@ export function actionForKey(keymap: Keymap, e: KeymapKeyEvent): KeymapAction |
159
204
  for (const action of KEYMAP_ACTIONS) {
160
205
  const raw = keymap[action];
161
206
  if (raw === undefined) continue;
207
+ // A sequence is resolved by prefixStep (src/composables/prefixKeys.ts), never here.
162
208
  const binding = parseKeyBinding(raw);
163
209
  if (binding && matchesBinding(binding, e)) return action;
164
210
  }
@@ -215,26 +261,62 @@ export function validateKeymap(input: unknown): KeymapProblem[] {
215
261
  // deliberately, so Ctrl+C stays an interrupt — and the same-key send fires then. Naming `copy`
216
262
  // as the winner there tells the user the opposite of what they will see half the time (#1901).
217
263
  const bound = new Map<string, Claim[]>();
218
- const claim = (parsed: KeyBinding, entry: Claim): void => {
219
- const key = canonicalBinding(parsed); // as PARSED: "Shift+PageUp" and "shift+pageup" are one keystroke
264
+ const claim = (strokes: KeyBinding[], entry: Claim): void => {
265
+ const key = canonicalSequence(strokes); // as PARSED: "Shift+PageUp" and "shift+pageup" are one keystroke
220
266
  bound.set(key, [...(bound.get(key) ?? []), entry]);
221
267
  };
222
- const problems = entries.flatMap(([action, binding]): KeymapProblem[] => {
223
- if (action === "send") return sendProblems(binding, claim);
224
- if (!isKeymapAction(action)) {
225
- return [{ action, binding, reason: `unknown action (known: ${KEYMAP_ACTIONS.join(", ")}, send)`, fatal: false }];
226
- }
227
- if (typeof binding !== "string") return [{ action, binding, reason: "binding must be a string", fatal: true }];
228
- const parsed = parseKeyBinding(binding);
229
- if (parsed === null) {
230
- return [{ action, binding, reason: 'unparseable key binding — expected e.g. "PageDown" or "Shift+PageUp"', fatal: true }];
231
- }
232
- claim(parsed, { label: action, binding, rank: KEYMAP_ACTIONS.indexOf(action), kind: "action" });
233
- return unshiftedUnderCmdWarnings(action, binding, parsed);
268
+ const problems = entries.flatMap(([action, binding]): KeymapProblem[] =>
269
+ action === "send" ? sendProblems(binding, claim) : actionProblems(action, binding, claim),
270
+ );
271
+ return [...problems, ...duplicateWarnings(bound), ...prefixWarnings(bound)];
272
+ }
273
+
274
+ // Everything wrong with one action's binding, claiming its keystrokes when it is well-formed.
275
+ function actionProblems(action: string, binding: unknown, claim: (strokes: KeyBinding[], entry: Claim) => void): KeymapProblem[] {
276
+ if (!isKeymapAction(action)) {
277
+ return [{ action, binding, reason: `unknown action (known: ${KEYMAP_ACTIONS.join(", ")}, send)`, fatal: false }];
278
+ }
279
+ if (typeof binding !== "string") return [{ action, binding, reason: "binding must be a string", fatal: true }];
280
+ const strokes = parseKeySequence(binding);
281
+ if (strokes === null) {
282
+ return [{ action, binding, reason: 'unparseable key binding — expected e.g. "PageDown", "Shift+PageUp" or two keys "Cmd+K p"', fatal: true }];
283
+ }
284
+ if (strokes.length > 1 && !takesSequence(action)) {
285
+ return [{ action, binding, reason: "takes a single keystroke — it is decided inside the terminal, which cannot wait for a second key", fatal: true }];
286
+ }
287
+ claim(strokes, { label: action, binding, rank: KEYMAP_ACTIONS.indexOf(action), kind: "action" });
288
+ return [...strokes.flatMap((stroke) => unshiftedUnderCmdWarnings(action, binding, stroke)), ...escapeSecondWarnings(action, binding, strokes)];
289
+ }
290
+
291
+ const escapeSecondWarnings = (action: string, binding: string, [, second]: KeyBinding[]): KeymapProblem[] =>
292
+ second && isBareEscape({ key: second.key, shiftKey: second.shift, altKey: second.alt, ctrlKey: second.ctrl, metaKey: second.meta })
293
+ ? [{ action, binding, reason: "never fires — a bare Escape always cancels a sequence; add a modifier or pick another second key", fatal: false }]
294
+ : [];
295
+
296
+ // A sequence whose FIRST key is also a keystroke of its own. The single keystroke is claimed before
297
+ // any sequence can start (see GridView's handler), so the sequence never gets its first key.
298
+ function prefixWarnings(bound: Map<string, Claim[]>): KeymapProblem[] {
299
+ return [...bound.entries()].flatMap(([key, claims]) => {
300
+ const [first, second] = key.split(SEQUENCE_SEPARATOR);
301
+ const singles = second === undefined || first === undefined ? [] : (bound.get(first) ?? []);
302
+ if (singles.length === 0) return [];
303
+ const reason = prefixCollision(singles);
304
+ return claims.map((claim) => ({ action: claim.label, binding: claim.binding, reason, fatal: false }));
234
305
  });
235
- return [...problems, ...duplicateWarnings(bound)];
236
306
  }
237
307
 
308
+ // Deliberately NOT a prediction of the states the sequence still starts in. Which single binding
309
+ // acts depends on the zoom state, on a selection for `copy`, and on which of several claims dispatch
310
+ // reaches — twice a narrower sentence was wrong for a combination it did not list (codex on #2283).
311
+ // Every claim is named, in dispatch order, and the advice is the one that is always right.
312
+ const prefixCollision = (singles: Claim[]): string => {
313
+ const names = [...singles]
314
+ .sort((x, y) => x.rank - y.rank)
315
+ .map((claim) => `\`${claim.label}\``)
316
+ .join(", ");
317
+ return `its first key is also bound on its own, to ${names} — those take the key whenever they act, so this sequence may never start; give it a first key nothing else uses`;
318
+ };
319
+
238
320
  // A binding that says one keystroke and waits for another. While Cmd is held, a macOS browser puts
239
321
  // the UNSHIFTED character in `KeyboardEvent.key` — Cmd+Shift+P arrives as `"p"` — so a binding
240
322
  // written `"Cmd+Shift+P"` waits for a `"P"` that never comes, and nothing downstream can see the
@@ -273,7 +355,7 @@ interface Claim {
273
355
  // Fatal throughout, for the reason the module header gives: a send binding is invisible until
274
356
  // the key is pressed, so a dropped one is indistinguishable from a shortcut that "doesn't work".
275
357
  // Empty `bytes` is fatal too — it would take the key away from the terminal and put nothing back.
276
- function sendProblems(input: unknown, claim: (parsed: KeyBinding, entry: Claim) => void): KeymapProblem[] {
358
+ function sendProblems(input: unknown, claim: (strokes: KeyBinding[], entry: Claim) => void): KeymapProblem[] {
277
359
  if (!Array.isArray(input)) {
278
360
  return [{ action: "send", binding: input, reason: "`send` must be an array of { key, bytes }", fatal: true }];
279
361
  }
@@ -288,7 +370,7 @@ function sendProblems(input: unknown, claim: (parsed: KeyBinding, entry: Claim)
288
370
  return [{ action: label, binding: entry.key, reason: "`bytes` is empty — the key would be taken from the terminal and nothing sent", fatal: true }];
289
371
  }
290
372
  // Ranked after every action, matching who actually wins (see the `bound` comment above).
291
- claim(parsed, { label, binding: entry.key, rank: KEYMAP_ACTIONS.length + i, kind: "send" });
373
+ claim([parsed], { label, binding: entry.key, rank: KEYMAP_ACTIONS.length + i, kind: "send" });
292
374
  return unshiftedUnderCmdWarnings(label, entry.key, parsed);
293
375
  });
294
376
  }
@@ -358,13 +440,25 @@ const collisionReason = (winner: Claim, runnerUp: Claim | null, loser: Claim): s
358
440
  // A binding's identity as a keystroke, for spotting two actions that claim the same one.
359
441
  const canonicalBinding = (b: KeyBinding): string => `${b.shift ? "S" : ""}${b.alt ? "A" : ""}${b.ctrl ? "C" : ""}${b.meta ? "M" : ""}|${b.key}`;
360
442
 
443
+ // The same for a sequence. A keystroke's identity cannot contain the separator, because no key
444
+ // does (parseKeyBinding refuses whitespace), so a sequence's first key splits back out exactly.
445
+ const SEQUENCE_SEPARATOR = " ";
446
+ const canonicalSequence = (strokes: KeyBinding[]): string => strokes.map(canonicalBinding).join(SEQUENCE_SEPARATOR);
447
+
448
+ // A binding the dispatch can act on: it parses, and it is a single keystroke unless the action can
449
+ // wait for a second one.
450
+ const isUsableBinding = (action: KeymapAction, binding: string): boolean => {
451
+ const strokes = parseKeySequence(binding);
452
+ return strokes !== null && (strokes.length === 1 || takesSequence(action));
453
+ };
454
+
361
455
  // Keep only known actions bound to a parseable, non-empty string. Unknown keys and
362
456
  // malformed bindings are dropped rather than rejecting the whole map, matching how the
363
457
  // rest of the config treats one bad entry.
364
458
  export function sanitizeKeymap(input: unknown): Keymap {
365
459
  if (!isRecord(input)) return {};
366
460
  const entries = Object.entries(input).filter(
367
- (entry): entry is [KeymapAction, string] => isKeymapAction(entry[0]) && typeof entry[1] === "string" && parseKeyBinding(entry[1]) !== null,
461
+ (entry): entry is [KeymapAction, string] => isKeymapAction(entry[0]) && typeof entry[1] === "string" && isUsableBinding(entry[0], entry[1]),
368
462
  );
369
463
  const send = sanitizeSendBindings(input.send);
370
464
  return { ...Object.fromEntries(entries), ...(send.length ? { send } : {}) };
@@ -24,8 +24,23 @@ export const MD_PREVIEW_EMBED_PARAM = "embed";
24
24
  export const MD_PREVIEW_EMBED_ON = "1";
25
25
 
26
26
  /** What the preview document says. `ready` is a fresh document announcing it can be scrolled;
27
- * `scroll` is where the reader now is, in CSS pixels from the top of that document. */
28
- export type MdPreviewFrameMessage = { kind: "ready" } | { kind: "scroll"; scrollY: number };
27
+ * `scroll` is where the reader now is, in CSS pixels from the top of that document; `navigate`
28
+ * is a click on an external link, which the HOST opens (#2259) — the sandbox has no
29
+ * `allow-popups`, and following it inside the frame is what showed "refused to connect". */
30
+ export type MdPreviewFrameMessage = { kind: "ready" } | { kind: "scroll"; scrollY: number } | { kind: "navigate"; href: string };
31
+
32
+ /** `value` as an absolute http(s) URL, or null. The only kind of link the host opens on the
33
+ * document's behalf: the document is a file this app never sanitised, so a `javascript:` or
34
+ * `file:` href it posts must not reach `window.open`. */
35
+ export const externalHref = (value: unknown): string | null => {
36
+ if (typeof value !== "string") return null;
37
+ try {
38
+ const url = new URL(value);
39
+ return url.protocol === "http:" || url.protocol === "https:" ? url.href : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ };
29
44
 
30
45
  /** What the host says back: put the reader here. Sent in answer to `ready`, so the host never has
31
46
  * to guess when the document became scrollable. */
@@ -40,6 +55,10 @@ export interface MdPreviewHostMessage {
40
55
  export const mdPreviewFrameMessage = (data: unknown): MdPreviewFrameMessage | null => {
41
56
  if (!isRecord(data) || data.source !== MD_PREVIEW_FROM_FRAME) return null;
42
57
  if (data.kind === "ready") return { kind: "ready" };
58
+ if (data.kind === "navigate") {
59
+ const href = externalHref(data.href);
60
+ return href === null ? null : { kind: "navigate", href };
61
+ }
43
62
  if (data.kind !== "scroll") return null;
44
63
  const scrollY = finiteNumber(data.scrollY);
45
64
  // A negative offset is not a place in a document; it would scroll the restore to the top and
@@ -0,0 +1,39 @@
1
+ // The app's theme, carried to the Markdown preview (#2263). The preview is a sandboxed document the
2
+ // app cannot style from outside, so the host hands it the few colours it paints with on the URL and
3
+ // the server writes them into the document's own <style>.
4
+ //
5
+ // In `common/` because both ends decide from it: the pane builds the query, the server reads it.
6
+ // Every value lands in CSS, so a colour is accepted only in the one shape the app's theme variables
7
+ // use — `#` and hex digits — and any other value drops the WHOLE theme, leaving the document on the
8
+ // reader's system colours as before. A partial theme could put dark text on a dark background.
9
+ import { isPaletteColor } from "./themeColors.js";
10
+ import type { ThemeVarKey, ThemeVars } from "./themeVars.js";
11
+
12
+ /** Query parameter → the app theme variable it carries. */
13
+ export const PREVIEW_THEME_PARAMS = {
14
+ bg: "--bg-base",
15
+ fg: "--text",
16
+ muted: "--text-secondary",
17
+ subtle: "--bg-subtle",
18
+ border: "--border",
19
+ link: "--accent",
20
+ } as const satisfies Record<string, ThemeVarKey>;
21
+
22
+ export type PreviewThemeParam = keyof typeof PREVIEW_THEME_PARAMS;
23
+ export type PreviewTheme = Record<PreviewThemeParam, string>;
24
+
25
+ /** Build a theme from named values, or null unless every one is a hex colour. */
26
+ function themeOf(valueFor: (param: PreviewThemeParam) => unknown): PreviewTheme | null {
27
+ const colour = (param: PreviewThemeParam): string | null => {
28
+ const value = valueFor(param);
29
+ return isPaletteColor(value) ? value : null;
30
+ };
31
+ const [bg, fg, muted, subtle, border, link] = [colour("bg"), colour("fg"), colour("muted"), colour("subtle"), colour("border"), colour("link")];
32
+ return bg && fg && muted && subtle && border && link ? { bg, fg, muted, subtle, border, link } : null;
33
+ }
34
+
35
+ /** The preview's colours from the app's theme variables. */
36
+ export const previewThemeFromVars = (vars: ThemeVars): PreviewTheme | null => themeOf((param) => vars[PREVIEW_THEME_PARAMS[param]]);
37
+
38
+ /** The preview's colours from a request's query, or null when any is missing or not a hex colour. */
39
+ export const previewThemeFromQuery = (query: Record<string, unknown>): PreviewTheme | null => themeOf((param) => query[param]);
@@ -129,11 +129,16 @@ export function relativeLuminance(color: string): number | null {
129
129
  // thinks about it still gets legible pills.
130
130
  const LIGHT_LUMINANCE_THRESHOLD = 0.4;
131
131
 
132
- export function isLightTheme(vars: ThemeVars): boolean {
133
- const luminance = relativeLuminance(vars["--bg-base"]);
132
+ /** Whether a background colour reads as light — the same line `isLightTheme` draws for the chrome. */
133
+ export function isLightColor(color: string): boolean {
134
+ const luminance = relativeLuminance(color);
134
135
  return luminance !== null && luminance > LIGHT_LUMINANCE_THRESHOLD;
135
136
  }
136
137
 
138
+ export function isLightTheme(vars: ThemeVars): boolean {
139
+ return isLightColor(vars["--bg-base"]);
140
+ }
141
+
137
142
  /** The xterm palette a theme's variables imply. xterm draws on a canvas and cannot read CSS
138
143
  * variables, so the same colours have to reach it as values — derived here rather than asked
139
144
  * for twice, since two copies of one colour is two chances to change only one. The 16 ANSI
@@ -1 +1 @@
1
- import{m as e,t}from"./mermaid-parser.core-BCgSuYuM.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-CllyRBe0.js";import"./chunk-O7XYJQB3-DD5uf7v4.js";import{n as i,r as a,t as o}from"./chunk-GTNCS2PH-I3-3mz_9.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-CXneRxVh.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(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=n(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=n(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=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
1
+ import{m as e,t}from"./mermaid-parser.core-Dlf5XX-m.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-CllyRBe0.js";import"./chunk-O7XYJQB3-C_ibJVHM.js";import{n as i,r as a,t as o}from"./chunk-GTNCS2PH-CabGHxDI.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-77q0N4Kj.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(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=n(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=n(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=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
@@ -0,0 +1 @@
1
+ import"./chunk-FOHPRMQF-DzxwRta7.js";import{M as e}from"./mermaid-parser.core-Dlf5XX-m.js";export{e as createArchitectureServices};