@changfenhuang/dsh-genui 0.9.1

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 (106) hide show
  1. package/CHANGELOG.md +367 -0
  2. package/LICENSE +21 -0
  3. package/README.md +261 -0
  4. package/README.zh-CN.md +261 -0
  5. package/SKILL.md +115 -0
  6. package/cordis.patch.yml +9 -0
  7. package/demo-prompts.md +118 -0
  8. package/lib/assets/echarts.js +64 -0
  9. package/lib/assets/mermaid.js +3591 -0
  10. package/lib/assets/three.js +3844 -0
  11. package/lib/client.js +5 -0
  12. package/lib/index.js +1895 -0
  13. package/lib/invariant.js +19 -0
  14. package/lib/types/client/EChartNode.d.ts +4 -0
  15. package/lib/types/client/ErrorBoundary.d.ts +30 -0
  16. package/lib/types/client/GenuiBlock.d.ts +7 -0
  17. package/lib/types/client/PlotBlock.d.ts +44 -0
  18. package/lib/types/client/action-context.d.ts +7 -0
  19. package/lib/types/client/asset-echarts.d.ts +15 -0
  20. package/lib/types/client/asset-loader.d.ts +31 -0
  21. package/lib/types/client/asset-mermaid.d.ts +1 -0
  22. package/lib/types/client/asset-three.d.ts +1 -0
  23. package/lib/types/client/blocks/advanced.d.ts +104 -0
  24. package/lib/types/client/blocks/basic.d.ts +27 -0
  25. package/lib/types/client/blocks/charts.d.ts +38 -0
  26. package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
  27. package/lib/types/client/blocks/diagram/index.d.ts +5 -0
  28. package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
  29. package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
  30. package/lib/types/client/blocks/forms.d.ts +83 -0
  31. package/lib/types/client/blocks/render-node.d.ts +10 -0
  32. package/lib/types/client/blocks/state.d.ts +58 -0
  33. package/lib/types/client/dom-fence.d.ts +14 -0
  34. package/lib/types/client/echarts-lazy.d.ts +17 -0
  35. package/lib/types/client/fence-render.d.ts +58 -0
  36. package/lib/types/client/gallery.d.ts +9 -0
  37. package/lib/types/client/guard.d.ts +126 -0
  38. package/lib/types/client/index.d.ts +36 -0
  39. package/lib/types/client/interaction-store.d.ts +43 -0
  40. package/lib/types/client/mermaid-core.d.ts +8 -0
  41. package/lib/types/client/mermaid-lazy.d.ts +9 -0
  42. package/lib/types/client/mermaid-safe.d.ts +52 -0
  43. package/lib/types/client/panel-command.d.ts +29 -0
  44. package/lib/types/client/panel-store.d.ts +94 -0
  45. package/lib/types/client/panel.d.ts +15 -0
  46. package/lib/types/client/parse-partial.d.ts +37 -0
  47. package/lib/types/client/safe-math.d.ts +43 -0
  48. package/lib/types/client/scene3d-core.d.ts +21 -0
  49. package/lib/types/client/scene3d-lazy.d.ts +8 -0
  50. package/lib/types/client/spec.d.ts +610 -0
  51. package/lib/types/client/toolview.d.ts +7 -0
  52. package/lib/types/index.d.ts +1 -0
  53. package/lib/types/plugin/index.d.ts +25 -0
  54. package/lib/types/plugin/invariant.d.ts +15 -0
  55. package/lib/types/plugin/tool.d.ts +27 -0
  56. package/lib/types/shared/fence-repair.d.ts +67 -0
  57. package/package.json +121 -0
  58. package/src/client/EChartNode.tsx +242 -0
  59. package/src/client/ErrorBoundary.tsx +66 -0
  60. package/src/client/GenuiBlock.module.css +1263 -0
  61. package/src/client/GenuiBlock.tsx +166 -0
  62. package/src/client/PlotBlock.module.css +155 -0
  63. package/src/client/PlotBlock.tsx +431 -0
  64. package/src/client/action-context.ts +32 -0
  65. package/src/client/asset-echarts.ts +25 -0
  66. package/src/client/asset-loader.ts +79 -0
  67. package/src/client/asset-mermaid.ts +13 -0
  68. package/src/client/asset-three.ts +12 -0
  69. package/src/client/blocks/advanced.tsx +435 -0
  70. package/src/client/blocks/basic.tsx +116 -0
  71. package/src/client/blocks/charts.tsx +319 -0
  72. package/src/client/blocks/diagram/geometry.ts +151 -0
  73. package/src/client/blocks/diagram/index.tsx +284 -0
  74. package/src/client/blocks/diagram/layout.ts +199 -0
  75. package/src/client/blocks/diagram/theme.ts +108 -0
  76. package/src/client/blocks/forms.tsx +523 -0
  77. package/src/client/blocks/render-node.tsx +260 -0
  78. package/src/client/blocks/state.ts +61 -0
  79. package/src/client/dom-fence.tsx +641 -0
  80. package/src/client/echarts-lazy.ts +35 -0
  81. package/src/client/fence-render.tsx +230 -0
  82. package/src/client/gallery.ts +128 -0
  83. package/src/client/guard.ts +1450 -0
  84. package/src/client/index.tsx +195 -0
  85. package/src/client/interaction-store.ts +122 -0
  86. package/src/client/mermaid-core.ts +121 -0
  87. package/src/client/mermaid-lazy.ts +31 -0
  88. package/src/client/mermaid-safe.ts +122 -0
  89. package/src/client/panel-command.ts +119 -0
  90. package/src/client/panel-store.ts +471 -0
  91. package/src/client/panel.tsx +185 -0
  92. package/src/client/parse-partial.ts +162 -0
  93. package/src/client/safe-math.ts +270 -0
  94. package/src/client/scene3d-core.ts +192 -0
  95. package/src/client/scene3d-lazy.ts +26 -0
  96. package/src/client/spec.ts +767 -0
  97. package/src/client/toolview.tsx +66 -0
  98. package/src/css-modules.d.ts +5 -0
  99. package/src/index.ts +12 -0
  100. package/src/plugin/index.ts +173 -0
  101. package/src/plugin/invariant.ts +30 -0
  102. package/src/plugin/node-shims.d.ts +31 -0
  103. package/src/plugin/tool.ts +330 -0
  104. package/src/shared/fence-repair.ts +244 -0
  105. package/tsconfig.json +35 -0
  106. package/tsdown.config.ts +193 -0
package/lib/index.js ADDED
@@ -0,0 +1,1895 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/client/spec.ts
5
+ /**
6
+ * Wrap a bare component object into a col root. Returns null when `value` is
7
+ * not component-shaped (no usable `type`). `panel`/`append` live on the root
8
+ * spec, so they are hoisted onto the wrapper.
9
+ */
10
+ function wrapSingleComponentRoot(value) {
11
+ if (typeof value !== "object" || value === null) return null;
12
+ const v = value;
13
+ if (typeof v.type !== "string" || v.type === "") return null;
14
+ const root = {
15
+ type: "col",
16
+ items: [value]
17
+ };
18
+ if (v.panel === true) root.panel = true;
19
+ if (v.append === true) root.append = true;
20
+ return root;
21
+ }
22
+ //#endregion
23
+ //#region src/client/guard.ts
24
+ /** Hard resource limits enforced by repair (and mirrored at render time). */
25
+ const GENUI_LIMITS = {
26
+ /** Maximum nesting depth of the component tree. */
27
+ maxDepth: 8,
28
+ /** Maximum total nodes across the whole spec. */
29
+ maxNodes: 200,
30
+ /** Maximum length of any plain string field. */
31
+ maxString: 2e3,
32
+ /** Maximum length of a `code` body. */
33
+ maxCode: 12e3,
34
+ /** Maximum length of a mermaid source. */
35
+ maxMermaid: 8e3,
36
+ /** Maximum `grid` columns. */
37
+ maxGridCols: 12,
38
+ /** Maximum `tabs` count. */
39
+ maxTabs: 12,
40
+ /** Maximum `accordion` items. */
41
+ maxAccordionItems: 24,
42
+ /** Maximum `list` items. */
43
+ maxListItems: 50,
44
+ /** Maximum `select`/`radio` options. */
45
+ maxOptions: 50,
46
+ /** Maximum `table` rows / columns. */
47
+ maxTableRows: 50,
48
+ maxTableCols: 12,
49
+ /** Maximum `chart` data points per series. */
50
+ maxChartPoints: 60,
51
+ /** Maximum `plot` series and per-series parameters. */
52
+ maxPlotSeries: 8,
53
+ maxPlotParams: 6,
54
+ /** Maximum `scene3d` meshes. */
55
+ maxMeshes: 5,
56
+ /** Maximum `quiz` options. */
57
+ maxQuizOptions: 8,
58
+ /** Maximum `steps` / `timeline` / `breadcrumb` / `keyvalue` entries. */
59
+ maxSteps: 24,
60
+ maxTimelineItems: 24,
61
+ maxBreadcrumbItems: 12,
62
+ maxKeyValuePairs: 24,
63
+ /** Maximum `file-tree` nesting. */
64
+ maxTreeDepth: 6,
65
+ /** Maximum `diagram` nodes / edges / zones / focal accents (editorial
66
+ * complexity budget, mirroring diagram-design's §7 limits). */
67
+ maxDiagramNodes: 9,
68
+ maxDiagramEdges: 12,
69
+ maxDiagramZones: 3,
70
+ maxDiagramFocal: 2,
71
+ maxDiagramLabel: 14,
72
+ /** Maximum depth of an `echart` option object (prevents pathological nested
73
+ * ECharts configs from stalling the guard walk). */
74
+ maxEChartOptionDepth: 10,
75
+ /** Maximum length of any single array inside an `echart` option (prevents
76
+ * a model from stalling rendering with `series.data` of hundreds of
77
+ * thousands of points). */
78
+ maxEChartArrayLen: 500,
79
+ /** Maximum total entries (object keys + array elements) traversed while
80
+ * sanitizing an `echart` option. Bounds the walk so a pathologically
81
+ * large option object cannot stall the guard. */
82
+ maxEChartOptionNodes: 2e3
83
+ };
84
+ /** Is `v` one of `values`? (enum guard) */
85
+ function inEnum(v, values) {
86
+ return typeof v === "string" && values.includes(v);
87
+ }
88
+ /** String field: truncate a string to `cap`, or undefined when not a string. */
89
+ function str(v, cap) {
90
+ return typeof v === "string" ? v.slice(0, cap) : void 0;
91
+ }
92
+ /**
93
+ * Color field: the value lands in an inline `style` (background/stroke) or
94
+ * THREE.Color. Arbitrary CSS values are an exfiltration channel — a model
95
+ * (or a hostile spec) could emit `url(https://attacker/track?...)` and the
96
+ * browser would fetch it. Only formats that name a color pass: hex, rgb/hsl
97
+ * functions, and host design tokens (`var(--dsw-*)`). Anything else degrades
98
+ * to the component's default palette.
99
+ */
100
+ const SAFE_COLOR_RE = /^(?:#[\da-fA-F]{3,8}|rgba?\([^)]{0,64}\)|hsla?\([^)]{0,64}\)|var\(--dsw-[\w-]+(?:,\s*#[0-9a-fA-F]{3,8})?\))$/;
101
+ function color(v) {
102
+ if (typeof v !== "string") return void 0;
103
+ const s = v.trim();
104
+ return s.length <= 64 && SAFE_COLOR_RE.test(s) ? s : void 0;
105
+ }
106
+ /**
107
+ * Link target field: only http(s) and mailto survive. `javascript:`/`data:`
108
+ * and every other scheme degrade to a plain-text node — the model's link is
109
+ * display, not an execution channel.
110
+ */
111
+ function safeHref(v) {
112
+ if (typeof v !== "string") return void 0;
113
+ const s = v.trim();
114
+ if (s.length > 2048) return void 0;
115
+ return /^https?:\/\//i.test(s) || /^mailto:[^@\s]+@[^@\s]+$/i.test(s) ? s : void 0;
116
+ }
117
+ /** Media loads bytes, so accept only browser-reachable http(s) or same-origin
118
+ * relative paths. Active/local schemes and protocol-relative URLs are
119
+ * rejected. The renderer always keeps playback user-controlled. */
120
+ function safeMediaSrc(v) {
121
+ if (typeof v !== "string") return void 0;
122
+ const s = v.trim();
123
+ if (s === "" || s.length > 2048) return void 0;
124
+ if (/^https?:\/\//i.test(s)) return s;
125
+ if (/^[a-z][a-z0-9+.-]*:/i.test(s) || /^[/\\]{2}/.test(s)) return void 0;
126
+ return s;
127
+ }
128
+ /** Finite-number field: clamp into [min, max], or undefined when not finite. */
129
+ function num(v, min, max) {
130
+ return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : void 0;
131
+ }
132
+ /** Integer field: clamp into [min, max], or undefined when not a finite integer. */
133
+ function int(v, min, max) {
134
+ return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, Math.trunc(v))) : void 0;
135
+ }
136
+ /** Optional enum field: the value when it matches, otherwise undefined. */
137
+ function enu(v, values) {
138
+ return inEnum(v, values) ? v : void 0;
139
+ }
140
+ /** Plain object (not array, not null). */
141
+ function obj(v) {
142
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
143
+ }
144
+ /**
145
+ * Optional-field spread helper. `exactOptionalPropertyTypes` forbids
146
+ * `{ gap: number | undefined }`; computing the value into a const first and
147
+ * spreading `opt('gap', g)` keeps every optional field either absent or a
148
+ * plain value.
149
+ */
150
+ function opt(key, value) {
151
+ return value === void 0 ? {} : { [key]: value };
152
+ }
153
+ const TEXT_SIZES = [
154
+ "h1",
155
+ "h2",
156
+ "h3",
157
+ "body",
158
+ "muted",
159
+ "caption"
160
+ ];
161
+ const BUTTON_TONES = [
162
+ "primary",
163
+ "danger",
164
+ "success",
165
+ "ghost"
166
+ ];
167
+ const BADGE_TONES = [
168
+ "success",
169
+ "warn",
170
+ "danger",
171
+ "accent"
172
+ ];
173
+ const INPUT_TYPES = [
174
+ "text",
175
+ "email",
176
+ "password"
177
+ ];
178
+ const CALLOUT_TONES = [
179
+ "info",
180
+ "success",
181
+ "warning",
182
+ "error"
183
+ ];
184
+ const CHART_KINDS = [
185
+ "bars",
186
+ "line",
187
+ "donut"
188
+ ];
189
+ const PLOT_KINDS = [
190
+ "line",
191
+ "area",
192
+ "scatter"
193
+ ];
194
+ const MEDIA_ASPECT_RATIOS = [
195
+ "16:9",
196
+ "4:3",
197
+ "1:1",
198
+ "9:16"
199
+ ];
200
+ const MESH_SHAPES = [
201
+ "box",
202
+ "sphere",
203
+ "cone",
204
+ "cylinder",
205
+ "torus"
206
+ ];
207
+ const FILE_TYPES = ["file", "dir"];
208
+ const DIAGRAM_KINDS = [
209
+ "architecture",
210
+ "it-state",
211
+ "flowchart",
212
+ "sequence",
213
+ "state",
214
+ "er",
215
+ "timeline",
216
+ "swimlane",
217
+ "quadrant",
218
+ "radar",
219
+ "loop",
220
+ "nested",
221
+ "tree",
222
+ "org-chart",
223
+ "layers",
224
+ "venn",
225
+ "pyramid",
226
+ "bar",
227
+ "line",
228
+ "gantt",
229
+ "scatter",
230
+ "high-level",
231
+ "process",
232
+ "medallion",
233
+ "data-flow",
234
+ "dp-integration",
235
+ "dp-security-matrix"
236
+ ];
237
+ const DIAGRAM_NODE_TYPES = [
238
+ "focal",
239
+ "backend",
240
+ "store",
241
+ "external",
242
+ "input",
243
+ "optional",
244
+ "security"
245
+ ];
246
+ const DIAGRAM_VARIANTS = [
247
+ "light",
248
+ "dark",
249
+ "editorial"
250
+ ];
251
+ const DIAGRAM_EDGE_KINDS = [
252
+ "solid",
253
+ "dashed",
254
+ "accent",
255
+ "link"
256
+ ];
257
+ const DIAGRAM_ROUTES = [
258
+ "auto",
259
+ "orthogonal",
260
+ "straight"
261
+ ];
262
+ const ECHART_PRESETS = [
263
+ "bar",
264
+ "line",
265
+ "area",
266
+ "pie",
267
+ "scatter"
268
+ ];
269
+ /** Walk `list` with the shared node budget; drops invalid entries. */
270
+ function repairItems(list, ctx, depth) {
271
+ if (!Array.isArray(list)) return [];
272
+ const out = [];
273
+ for (const item of list) {
274
+ if (ctx.remaining <= 0) break;
275
+ ctx.remaining -= 1;
276
+ const node = repairNode(item, ctx, depth);
277
+ if (node !== null) out.push(node);
278
+ }
279
+ return out;
280
+ }
281
+ function repairNode(value, ctx, depth) {
282
+ if (depth > GENUI_LIMITS.maxDepth) return null;
283
+ const v = obj(value);
284
+ if (v === void 0) return null;
285
+ const type = v.type;
286
+ if (typeof type !== "string") return null;
287
+ switch (type) {
288
+ case "text": {
289
+ const content = str(v.content, GENUI_LIMITS.maxString) ?? str(v.text, GENUI_LIMITS.maxString);
290
+ if (content === void 0) return null;
291
+ return {
292
+ type: "text",
293
+ content,
294
+ ...opt("size", enu(v.size, TEXT_SIZES)),
295
+ ...opt("center", v.center === true ? true : void 0)
296
+ };
297
+ }
298
+ case "row": return {
299
+ type: "row",
300
+ items: repairItems(v.items, ctx, depth + 1),
301
+ ...opt("wrap", v.wrap === true ? true : void 0),
302
+ ...opt("spacer", v.spacer === true ? true : void 0)
303
+ };
304
+ case "col": return {
305
+ type: "col",
306
+ items: repairItems(v.items, ctx, depth + 1),
307
+ ...opt("gap", num(v.gap, 0, 96))
308
+ };
309
+ case "grid": return {
310
+ type: "grid",
311
+ cols: int(v.cols, 1, GENUI_LIMITS.maxGridCols) ?? 1,
312
+ items: repairItems(v.items, ctx, depth + 1)
313
+ };
314
+ case "card": return {
315
+ type: "card",
316
+ items: repairItems(v.items, ctx, depth + 1),
317
+ ...opt("title", str(v.title, GENUI_LIMITS.maxString))
318
+ };
319
+ case "button": {
320
+ const label = str(v.label, GENUI_LIMITS.maxString);
321
+ if (label === void 0) return null;
322
+ return {
323
+ type: "button",
324
+ label,
325
+ ...opt("tone", enu(v.tone, BUTTON_TONES)),
326
+ ...opt("full", v.full === true ? true : void 0),
327
+ ...opt("small", v.small === true ? true : void 0),
328
+ ...opt("icon", str(v.icon, 64)),
329
+ ...opt("action", str(v.action, 200))
330
+ };
331
+ }
332
+ case "input": return {
333
+ type: "input",
334
+ ...opt("label", str(v.label, GENUI_LIMITS.maxString)),
335
+ ...opt("placeholder", str(v.placeholder, GENUI_LIMITS.maxString)),
336
+ ...opt("value", str(v.value, GENUI_LIMITS.maxString)),
337
+ ...opt("inputType", enu(v.inputType, INPUT_TYPES)),
338
+ ...opt("action", str(v.action, 200)),
339
+ ...opt("id", str(v.id, 200))
340
+ };
341
+ case "select": {
342
+ const options = repairStrings(v.options, GENUI_LIMITS.maxOptions, GENUI_LIMITS.maxString);
343
+ if (options === void 0) return null;
344
+ return {
345
+ type: "select",
346
+ options,
347
+ ...opt("label", str(v.label, GENUI_LIMITS.maxString)),
348
+ ...opt("action", str(v.action, 200)),
349
+ ...opt("selected", int(v.selected, 0, options.length - 1)),
350
+ ...opt("id", str(v.id, 200))
351
+ };
352
+ }
353
+ case "checkbox": {
354
+ const label = str(v.label, GENUI_LIMITS.maxString);
355
+ if (label === void 0) return null;
356
+ return {
357
+ type: "checkbox",
358
+ label,
359
+ ...opt("checked", v.checked === true ? true : void 0),
360
+ ...opt("action", str(v.action, 200))
361
+ };
362
+ }
363
+ case "link": {
364
+ const label = str(v.label, GENUI_LIMITS.maxString);
365
+ if (label === void 0) return null;
366
+ return {
367
+ type: "link",
368
+ label,
369
+ ...opt("href", safeHref(v.href))
370
+ };
371
+ }
372
+ case "audio": {
373
+ const src = safeMediaSrc(v.src);
374
+ if (src === void 0) return null;
375
+ return {
376
+ type: "audio",
377
+ src,
378
+ ...opt("alt", str(v.alt, GENUI_LIMITS.maxString)),
379
+ ...opt("loop", v.loop === true ? true : void 0)
380
+ };
381
+ }
382
+ case "video": {
383
+ const src = safeMediaSrc(v.src);
384
+ if (src === void 0) return null;
385
+ return {
386
+ type: "video",
387
+ src,
388
+ ...opt("alt", str(v.alt, GENUI_LIMITS.maxString)),
389
+ ...opt("poster", safeMediaSrc(v.poster)),
390
+ ...opt("loop", v.loop === true ? true : void 0),
391
+ ...opt("muted", v.muted === true ? true : void 0),
392
+ ...opt("aspectRatio", enu(v.aspectRatio, MEDIA_ASPECT_RATIOS))
393
+ };
394
+ }
395
+ case "badge": {
396
+ const label = str(v.label, GENUI_LIMITS.maxString) ?? str(v.text, GENUI_LIMITS.maxString) ?? str(v.value, GENUI_LIMITS.maxString);
397
+ if (label === void 0) return null;
398
+ return {
399
+ type: "badge",
400
+ label,
401
+ ...opt("tone", enu(v.tone, BADGE_TONES)),
402
+ ...opt("icon", str(v.icon, 64))
403
+ };
404
+ }
405
+ case "stat": {
406
+ const label = str(v.label, GENUI_LIMITS.maxString);
407
+ const value = str(v.value, 128);
408
+ if (label === void 0 || value === void 0) return null;
409
+ return {
410
+ type: "stat",
411
+ label,
412
+ value,
413
+ ...opt("delta", str(v.delta, 64))
414
+ };
415
+ }
416
+ case "progress": {
417
+ const value = num(v.value, 0, 100);
418
+ if (value === void 0) return null;
419
+ return {
420
+ type: "progress",
421
+ value,
422
+ ...opt("label", str(v.label, GENUI_LIMITS.maxString)),
423
+ ...opt("valueLabel", str(v.valueLabel, 64))
424
+ };
425
+ }
426
+ case "divider": return { type: "divider" };
427
+ case "spacer": return { type: "spacer" };
428
+ case "avatar": {
429
+ const name = str(v.name, 64);
430
+ if (name === void 0) return null;
431
+ return {
432
+ type: "avatar",
433
+ name,
434
+ ...opt("color", color(v.color))
435
+ };
436
+ }
437
+ case "list": {
438
+ const items = repairListItems(v.items, GENUI_LIMITS.maxListItems, ctx, depth + 1);
439
+ if (items === void 0) return null;
440
+ return {
441
+ type: "list",
442
+ items
443
+ };
444
+ }
445
+ case "table": {
446
+ let rawCols = v.columns;
447
+ let rawRows = v.rows !== void 0 ? v.rows : v.data;
448
+ if (Array.isArray(rawCols) && rawCols.length > 0 && typeof rawCols[0] === "object" && rawCols[0] !== null) rawCols = rawCols.map((c) => columnHeaderText(c));
449
+ if (Array.isArray(rawRows) && rawRows.length > 0 && typeof rawRows[0] === "object" && rawRows[0] !== null && !Array.isArray(rawRows[0])) {
450
+ const keys = Array.isArray(v.columns) && v.columns.length > 0 && typeof v.columns[0] === "object" && v.columns[0] !== null ? v.columns.map((c) => columnKeyOf(c)).filter((k) => k !== void 0) : Object.keys(rawRows[0]);
451
+ rawRows = rawRows.map((row) => keys.map((k) => cellText(row[k])));
452
+ }
453
+ const columns = repairStrings(rawCols, GENUI_LIMITS.maxTableCols, 128);
454
+ const rows = repairRows(rawRows, GENUI_LIMITS.maxTableRows, GENUI_LIMITS.maxTableCols);
455
+ if (columns === void 0 || rows === void 0) return null;
456
+ return {
457
+ type: "table",
458
+ columns,
459
+ rows
460
+ };
461
+ }
462
+ case "chart": {
463
+ const data = repairChartData(v.data, GENUI_LIMITS.maxChartPoints);
464
+ const series = Array.isArray(v.series) ? repairSeries(v.series, GENUI_LIMITS.maxPlotSeries, GENUI_LIMITS.maxChartPoints) : void 0;
465
+ if (data === void 0 && series === void 0) return null;
466
+ return {
467
+ type: "chart",
468
+ data: data ?? [],
469
+ ...opt("kind", enu(v.kind, CHART_KINDS)),
470
+ ...opt("series", series)
471
+ };
472
+ }
473
+ case "tabs": {
474
+ const tabs = repairTabs(v.tabs, ctx, depth);
475
+ if (tabs === void 0) return null;
476
+ return {
477
+ type: "tabs",
478
+ tabs
479
+ };
480
+ }
481
+ case "plot": {
482
+ const series = repairPlotSeries(v.series, GENUI_LIMITS.maxPlotSeries);
483
+ if (series === void 0) return null;
484
+ return {
485
+ type: "plot",
486
+ series,
487
+ ...opt("xMin", num(v.xMin, -1e6, 1e6)),
488
+ ...opt("xMax", num(v.xMax, -1e6, 1e6)),
489
+ ...opt("yMin", num(v.yMin, -1e9, 1e9)),
490
+ ...opt("yMax", num(v.yMax, -1e9, 1e9)),
491
+ ...opt("title", str(v.title, GENUI_LIMITS.maxString))
492
+ };
493
+ }
494
+ case "callout": {
495
+ const content = str(v.content, GENUI_LIMITS.maxString);
496
+ if (content === void 0) return null;
497
+ return {
498
+ type: "callout",
499
+ content,
500
+ ...opt("tone", enu(v.tone, CALLOUT_TONES)),
501
+ ...opt("title", str(v.title, GENUI_LIMITS.maxString))
502
+ };
503
+ }
504
+ case "steps": {
505
+ const steps = repairSteps(v.steps);
506
+ if (steps === void 0) return null;
507
+ return {
508
+ type: "steps",
509
+ steps,
510
+ ...opt("current", int(v.current, 0, steps.length))
511
+ };
512
+ }
513
+ case "keyvalue": {
514
+ const pairs = repairPairs(v.pairs, GENUI_LIMITS.maxKeyValuePairs);
515
+ if (pairs === void 0) return null;
516
+ return {
517
+ type: "keyvalue",
518
+ pairs
519
+ };
520
+ }
521
+ case "diff": {
522
+ const diffs = repairDiffs(v.diffs);
523
+ if (diffs === void 0) return null;
524
+ return {
525
+ type: "diff",
526
+ diffs
527
+ };
528
+ }
529
+ case "json":
530
+ if (!("value" in v)) return null;
531
+ return {
532
+ type: "json",
533
+ value: v.value
534
+ };
535
+ case "code": {
536
+ const code = str(v.code, GENUI_LIMITS.maxCode);
537
+ if (code === void 0) return null;
538
+ return {
539
+ type: "code",
540
+ code,
541
+ ...opt("lang", str(v.lang, 64))
542
+ };
543
+ }
544
+ case "radio": {
545
+ const options = repairStrings(v.options, GENUI_LIMITS.maxOptions, GENUI_LIMITS.maxString);
546
+ if (options === void 0) return null;
547
+ return {
548
+ type: "radio",
549
+ options,
550
+ ...opt("label", str(v.label, GENUI_LIMITS.maxString)),
551
+ ...opt("selected", int(v.selected, 0, options.length - 1)),
552
+ ...opt("action", str(v.action, 200)),
553
+ ...opt("group", str(v.group, 200)),
554
+ ...opt("answer", typeof v.answer === "number" && Number.isFinite(v.answer) && v.answer >= 0 && v.answer < options.length ? Math.trunc(v.answer) : typeof v.answer === "string" ? v.answer.slice(0, 512) : void 0),
555
+ ...opt("explanation", str(v.explanation, GENUI_LIMITS.maxString))
556
+ };
557
+ }
558
+ case "submit": {
559
+ const label = str(v.label, GENUI_LIMITS.maxString);
560
+ const action = str(v.action, 200);
561
+ if (label === void 0) return null;
562
+ return {
563
+ type: "submit",
564
+ label,
565
+ ...opt("action", action),
566
+ ...opt("resetAction", str(v.resetAction, 200)),
567
+ ...opt("groups", repairStrings(v.groups, GENUI_LIMITS.maxOptions, 200))
568
+ };
569
+ }
570
+ case "switch": {
571
+ const label = str(v.label, GENUI_LIMITS.maxString);
572
+ if (label === void 0) return null;
573
+ return {
574
+ type: "switch",
575
+ label,
576
+ ...opt("checked", v.checked === true ? true : void 0),
577
+ ...opt("action", str(v.action, 200))
578
+ };
579
+ }
580
+ case "slider": {
581
+ const min = num(v.min, -1e9, 1e9) ?? 0;
582
+ const max = num(v.max, -1e9, 1e9) ?? 100;
583
+ const lo = Math.min(min, max);
584
+ const hi = Math.max(min, max);
585
+ const step = num(v.step, 1e-9, Math.max(hi - lo, 1e-9));
586
+ const value = num(v.value, lo, hi) ?? lo;
587
+ return {
588
+ type: "slider",
589
+ min: lo,
590
+ max: hi,
591
+ ...opt("step", step),
592
+ value,
593
+ ...opt("label", str(v.label, GENUI_LIMITS.maxString)),
594
+ ...opt("action", str(v.action, 200)),
595
+ ...opt("id", str(v.id, 200))
596
+ };
597
+ }
598
+ case "textarea": return {
599
+ type: "textarea",
600
+ ...opt("label", str(v.label, GENUI_LIMITS.maxString)),
601
+ ...opt("placeholder", str(v.placeholder, GENUI_LIMITS.maxString)),
602
+ ...opt("rows", int(v.rows, 1, 30)),
603
+ ...opt("value", str(v.value, GENUI_LIMITS.maxString)),
604
+ ...opt("action", str(v.action, 200)),
605
+ ...opt("id", str(v.id, 200))
606
+ };
607
+ case "accordion": {
608
+ const items = repairAccordion(v.items, ctx, depth);
609
+ if (items === void 0) return null;
610
+ return {
611
+ type: "accordion",
612
+ items
613
+ };
614
+ }
615
+ case "copy": {
616
+ const text = str(v.text, GENUI_LIMITS.maxCode);
617
+ if (text === void 0) return null;
618
+ return {
619
+ type: "copy",
620
+ text,
621
+ ...opt("label", str(v.label, 128))
622
+ };
623
+ }
624
+ case "mermaid": {
625
+ const code = str(v.code, GENUI_LIMITS.maxMermaid);
626
+ if (code === void 0) return null;
627
+ return {
628
+ type: "mermaid",
629
+ code
630
+ };
631
+ }
632
+ case "scene3d": {
633
+ const meshes = repairMeshes(v.meshes);
634
+ if (meshes === void 0) return null;
635
+ return {
636
+ type: "scene3d",
637
+ meshes,
638
+ ...opt("title", str(v.title, GENUI_LIMITS.maxString)),
639
+ ...opt("ambient", num(v.ambient, 0, 2)),
640
+ ...opt("background", color(v.background))
641
+ };
642
+ }
643
+ case "diagram": return repairDiagram(v);
644
+ case "timeline": {
645
+ const items = repairTimeline(v.items, GENUI_LIMITS.maxTimelineItems);
646
+ if (items === void 0) return null;
647
+ return {
648
+ type: "timeline",
649
+ items
650
+ };
651
+ }
652
+ case "file-tree": {
653
+ const items = repairTree(v.items, GENUI_LIMITS.maxListItems);
654
+ if (items === void 0) return null;
655
+ return {
656
+ type: "file-tree",
657
+ items
658
+ };
659
+ }
660
+ case "breadcrumb": {
661
+ const items = repairStrings(v.items, GENUI_LIMITS.maxBreadcrumbItems, GENUI_LIMITS.maxString);
662
+ if (items === void 0) return null;
663
+ return {
664
+ type: "breadcrumb",
665
+ items
666
+ };
667
+ }
668
+ case "quiz": {
669
+ const question = str(v.question, GENUI_LIMITS.maxString);
670
+ const options = repairQuizOptions(v.options);
671
+ if (question === void 0 || options === void 0) return null;
672
+ return {
673
+ type: "quiz",
674
+ question,
675
+ options,
676
+ ...opt("explanation", str(v.explanation, GENUI_LIMITS.maxString)),
677
+ ...opt("id", str(v.id, 200)),
678
+ ...opt("action", str(v.action, 200))
679
+ };
680
+ }
681
+ case "echart": {
682
+ const data = v.data !== void 0 ? repairChartData(v.data, GENUI_LIMITS.maxChartPoints) : void 0;
683
+ const series = v.series !== void 0 && Array.isArray(v.series) ? repairSeries(v.series, GENUI_LIMITS.maxPlotSeries, GENUI_LIMITS.maxChartPoints) : void 0;
684
+ const sanitized = v.option !== void 0 ? sanitizeEChartOption(v.option, 0, { count: GENUI_LIMITS.maxEChartOptionNodes }) : void 0;
685
+ const option = sanitized === void 0 || typeof sanitized !== "object" || sanitized === null || Array.isArray(sanitized) ? void 0 : sanitized;
686
+ if (option === void 0 && data === void 0 && series === void 0) return null;
687
+ return {
688
+ type: "echart",
689
+ ...opt("title", str(v.title, GENUI_LIMITS.maxString)),
690
+ ...opt("height", int(v.height, 100, 800)),
691
+ ...opt("preset", enu(v.preset, ECHART_PRESETS)),
692
+ ...opt("data", data),
693
+ ...opt("series", series),
694
+ ...opt("option", option)
695
+ };
696
+ }
697
+ default: return value;
698
+ }
699
+ }
700
+ function repairStrings(v, cap, strCap) {
701
+ if (!Array.isArray(v)) return void 0;
702
+ const out = [];
703
+ for (const item of v) {
704
+ if (out.length >= cap) break;
705
+ if (typeof item === "string") out.push(item.slice(0, strCap));
706
+ else if (item !== null && typeof item === "object") {
707
+ const o = item;
708
+ const s = typeof o.label === "string" ? o.label : typeof o.value === "string" ? o.value : typeof o.title === "string" ? o.title : JSON.stringify(item);
709
+ out.push(s.slice(0, strCap));
710
+ }
711
+ }
712
+ return out;
713
+ }
714
+ function repairListItems(v, cap, ctx, depth) {
715
+ if (!Array.isArray(v)) return void 0;
716
+ const out = [];
717
+ for (const item of v) {
718
+ if (out.length >= cap) break;
719
+ if (typeof item === "string") {
720
+ out.push(item.slice(0, GENUI_LIMITS.maxString));
721
+ continue;
722
+ }
723
+ const o = obj(item);
724
+ const title = o === void 0 ? void 0 : str(o.title, GENUI_LIMITS.maxString);
725
+ if (title !== void 0) {
726
+ out.push({
727
+ title,
728
+ ...opt("desc", o === void 0 ? void 0 : str(o.desc, GENUI_LIMITS.maxString))
729
+ });
730
+ continue;
731
+ }
732
+ if (o !== void 0 && typeof o.type === "string") {
733
+ if (ctx.remaining <= 0) break;
734
+ ctx.remaining -= 1;
735
+ const node = repairNode(o, ctx, depth);
736
+ if (node !== null) out.push(node);
737
+ }
738
+ }
739
+ return out;
740
+ }
741
+ function repairRows(v, rowCap, colCap) {
742
+ if (!Array.isArray(v)) return void 0;
743
+ const out = [];
744
+ for (const row of v) {
745
+ if (out.length >= rowCap) break;
746
+ if (!Array.isArray(row)) continue;
747
+ const cells = [];
748
+ for (const cell of row) {
749
+ if (cells.length >= colCap) break;
750
+ if (typeof cell === "string") cells.push(cell.slice(0, 256));
751
+ else if (typeof cell === "number" && Number.isFinite(cell)) cells.push(cell);
752
+ }
753
+ if (cells.length > 0) out.push(cells);
754
+ }
755
+ return out;
756
+ }
757
+ function repairChartData(v, cap) {
758
+ if (!Array.isArray(v)) return void 0;
759
+ const out = [];
760
+ for (const datum of v) {
761
+ if (out.length >= cap) break;
762
+ const o = obj(datum);
763
+ const label = o === void 0 ? void 0 : str(o.label, 128);
764
+ const value = o === void 0 ? void 0 : num(o.value, -0xe8d4a51000, 0xe8d4a51000);
765
+ if (label === void 0 || value === void 0) continue;
766
+ out.push({
767
+ label,
768
+ value,
769
+ ...opt("color", o === void 0 ? void 0 : color(o.color))
770
+ });
771
+ }
772
+ return out;
773
+ }
774
+ function repairSeries(v, cap, pointCap) {
775
+ if (!Array.isArray(v)) return void 0;
776
+ const out = [];
777
+ for (const s of v) {
778
+ if (out.length >= cap) break;
779
+ const o = obj(s);
780
+ const label = o === void 0 ? void 0 : str(o.label, 128);
781
+ const data = o === void 0 ? void 0 : repairChartData(o.data, pointCap);
782
+ if (label === void 0 || data === void 0) continue;
783
+ out.push({
784
+ label,
785
+ data,
786
+ ...opt("color", o === void 0 ? void 0 : color(o.color))
787
+ });
788
+ }
789
+ return out;
790
+ }
791
+ function repairTabs(v, ctx, depth) {
792
+ if (!Array.isArray(v)) return void 0;
793
+ const out = [];
794
+ for (const tab of v) {
795
+ if (out.length >= GENUI_LIMITS.maxTabs) break;
796
+ const o = obj(tab);
797
+ const label = o === void 0 ? void 0 : str(o.label, 128);
798
+ if (label === void 0 || o === void 0) continue;
799
+ const rawItems = o.items !== void 0 ? o.items : o.content !== void 0 ? Array.isArray(o.content) ? o.content : [o.content] : void 0;
800
+ out.push({
801
+ label,
802
+ items: repairItems(rawItems, ctx, depth + 1)
803
+ });
804
+ }
805
+ return out;
806
+ }
807
+ /** Header text for an object-shaped table column ({title,key} antd style). */
808
+ function columnHeaderText(c) {
809
+ const o = obj(c);
810
+ if (o === void 0) return String(c);
811
+ for (const k of [
812
+ "title",
813
+ "label",
814
+ "key",
815
+ "dataIndex"
816
+ ]) {
817
+ const s = o[k];
818
+ if (typeof s === "string" && s !== "") return s;
819
+ }
820
+ return JSON.stringify(c);
821
+ }
822
+ /** Row key for an object-shaped column, mirroring columnHeaderText's order. */
823
+ function columnKeyOf(c) {
824
+ const o = obj(c);
825
+ if (o === void 0) return void 0;
826
+ for (const k of [
827
+ "key",
828
+ "dataIndex",
829
+ "title",
830
+ "label"
831
+ ]) {
832
+ const s = o[k];
833
+ if (typeof s === "string" && s !== "") return s;
834
+ }
835
+ }
836
+ /** Cell text for object-array rows: strings/finite numbers pass through,
837
+ * everything else stringifies so the column alignment is preserved
838
+ * (repairRows would drop null/undefined cells and shift the row). */
839
+ function cellText(v) {
840
+ if (typeof v === "string") return v;
841
+ if (typeof v === "number" && Number.isFinite(v)) return v;
842
+ if (v === null || v === void 0) return "";
843
+ return JSON.stringify(v);
844
+ }
845
+ function repairPlotSeries(v, cap) {
846
+ if (!Array.isArray(v)) return void 0;
847
+ const out = [];
848
+ for (const s of v) {
849
+ if (out.length >= cap) break;
850
+ const o = obj(s);
851
+ const expr = o === void 0 ? void 0 : str(o.expr, 512);
852
+ if (expr === void 0 || o === void 0) continue;
853
+ const params = [];
854
+ if (Array.isArray(o.params)) for (const p of o.params) {
855
+ if (params.length >= GENUI_LIMITS.maxPlotParams) break;
856
+ const po = obj(p);
857
+ const name = po === void 0 ? void 0 : str(po.name, 64);
858
+ const value = po === void 0 ? void 0 : num(po.value, -1e9, 1e9);
859
+ if (name === void 0 || value === void 0) continue;
860
+ params.push({
861
+ name,
862
+ value,
863
+ ...opt("min", po === void 0 ? void 0 : num(po.min, -1e9, 1e9)),
864
+ ...opt("max", po === void 0 ? void 0 : num(po.max, -1e9, 1e9)),
865
+ ...opt("step", po === void 0 ? void 0 : num(po.step, 1e-9, 1e9)),
866
+ ...opt("animateTo", po === void 0 ? void 0 : num(po.animateTo, -1e9, 1e9)),
867
+ ...opt("durationMs", po === void 0 ? void 0 : num(po.durationMs, 1, 12e4)),
868
+ ...opt("loop", po === void 0 ? void 0 : po.loop === true ? true : void 0)
869
+ });
870
+ }
871
+ out.push({
872
+ expr,
873
+ ...opt("label", str(o.label, 128)),
874
+ ...opt("color", color(o.color)),
875
+ ...opt("kind", enu(o.kind, PLOT_KINDS)),
876
+ ...opt("params", params.length > 0 ? params : void 0)
877
+ });
878
+ }
879
+ return out;
880
+ }
881
+ function repairSteps(v) {
882
+ if (!Array.isArray(v)) return void 0;
883
+ const out = [];
884
+ for (const s of v) {
885
+ if (out.length >= GENUI_LIMITS.maxSteps) break;
886
+ const o = obj(s);
887
+ const title = o === void 0 ? void 0 : str(o.title, 256);
888
+ if (title === void 0) continue;
889
+ out.push({
890
+ title,
891
+ ...opt("desc", o === void 0 ? void 0 : str(o.desc, GENUI_LIMITS.maxString))
892
+ });
893
+ }
894
+ return out;
895
+ }
896
+ function repairPairs(v, cap) {
897
+ if (!Array.isArray(v)) return void 0;
898
+ const out = [];
899
+ for (const p of v) {
900
+ if (out.length >= cap) break;
901
+ const o = obj(p);
902
+ const key = o === void 0 ? void 0 : str(o.key, 256);
903
+ const value = o === void 0 ? void 0 : str(o.value, GENUI_LIMITS.maxString);
904
+ if (key === void 0 || value === void 0) continue;
905
+ out.push({
906
+ key,
907
+ value
908
+ });
909
+ }
910
+ return out;
911
+ }
912
+ function repairDiffs(v) {
913
+ if (!Array.isArray(v)) return void 0;
914
+ const out = [];
915
+ for (const d of v) {
916
+ if (out.length >= 24) break;
917
+ const o = obj(d);
918
+ const path = o === void 0 ? void 0 : str(o.path, 1024);
919
+ const newText = o === void 0 ? void 0 : str(o.newText, 2e4);
920
+ if (path === void 0 || newText === void 0) continue;
921
+ const old = o === void 0 ? void 0 : o.oldText;
922
+ out.push({
923
+ path,
924
+ newText,
925
+ oldText: old === null || typeof old !== "string" ? null : old.slice(0, 2e4)
926
+ });
927
+ }
928
+ return out;
929
+ }
930
+ function repairAccordion(v, ctx, depth) {
931
+ if (!Array.isArray(v)) return void 0;
932
+ const out = [];
933
+ for (const item of v) {
934
+ if (out.length >= GENUI_LIMITS.maxAccordionItems) break;
935
+ const o = obj(item);
936
+ const title = o === void 0 ? void 0 : str(o.title, 256);
937
+ if (title === void 0 || o === void 0) continue;
938
+ out.push({
939
+ title,
940
+ items: repairItems(o.items, ctx, depth + 1)
941
+ });
942
+ }
943
+ return out;
944
+ }
945
+ function repairMeshes(v) {
946
+ if (!Array.isArray(v)) return void 0;
947
+ const out = [];
948
+ for (const m of v) {
949
+ if (out.length >= GENUI_LIMITS.maxMeshes) break;
950
+ const o = obj(m);
951
+ const shape = o === void 0 ? void 0 : enu(o.shape, MESH_SHAPES);
952
+ if (shape === void 0) continue;
953
+ const scale = o === void 0 ? void 0 : num(o.scale, -1e6, 1e6) ?? tuple3(o.scale);
954
+ const size = o === void 0 ? void 0 : num(o.size, -1e6, 1e6) ?? tuple3(o.size);
955
+ out.push({
956
+ shape,
957
+ ...opt("color", o === void 0 ? void 0 : color(o.color)),
958
+ ...opt("position", o === void 0 ? void 0 : tuple3(o.position)),
959
+ ...opt("rotation", o === void 0 ? void 0 : tuple3(o.rotation)),
960
+ ...opt("scale", scale),
961
+ ...opt("size", size)
962
+ });
963
+ }
964
+ return out;
965
+ }
966
+ function tuple3(v) {
967
+ if (!Array.isArray(v) || v.length !== 3) return void 0;
968
+ const [a, b, c] = v;
969
+ if (typeof a !== "number" || !Number.isFinite(a) || typeof b !== "number" || !Number.isFinite(b) || typeof c !== "number" || !Number.isFinite(c)) return void 0;
970
+ return [
971
+ Math.min(1e6, Math.max(-1e6, a)),
972
+ Math.min(1e6, Math.max(-1e6, b)),
973
+ Math.min(1e6, Math.max(-1e6, c))
974
+ ];
975
+ }
976
+ /** Clamp a coordinate/size to the 4px editorial grid. */
977
+ function grid4(v, min, max) {
978
+ return Math.min(max, Math.max(min, Math.round(v / 4) * 4));
979
+ }
980
+ function repairDiagramNodes(v) {
981
+ if (!Array.isArray(v)) return void 0;
982
+ const out = [];
983
+ const seen = /* @__PURE__ */ new Set();
984
+ for (const raw of v) {
985
+ if (out.length >= GENUI_LIMITS.maxDiagramNodes) break;
986
+ const o = obj(raw);
987
+ if (o === void 0) continue;
988
+ const id = str(o.id, 128);
989
+ const label = str(o.label, GENUI_LIMITS.maxString);
990
+ if (id === void 0 || label === void 0) continue;
991
+ if (seen.has(id)) continue;
992
+ seen.add(id);
993
+ const nodeType = enu(o.type, DIAGRAM_NODE_TYPES);
994
+ const x = o.x === void 0 ? void 0 : grid4(num(o.x, -1e6, 1e6) ?? 0, 0, 1e6);
995
+ const y = o.y === void 0 ? void 0 : grid4(num(o.y, -1e6, 1e6) ?? 0, 0, 1e6);
996
+ const w = o.w === void 0 ? void 0 : grid4(num(o.w, -1e6, 1e6) ?? 96, 40, 2e3);
997
+ const h = o.h === void 0 ? void 0 : grid4(num(o.h, -1e6, 1e6) ?? 48, 24, 1200);
998
+ out.push({
999
+ id,
1000
+ label,
1001
+ ...opt("sub", str(o.sub, 256)),
1002
+ ...opt("type", nodeType),
1003
+ ...opt("x", x),
1004
+ ...opt("y", y),
1005
+ ...opt("w", w),
1006
+ ...opt("h", h),
1007
+ ...opt("tag", str(o.tag, 32))
1008
+ });
1009
+ }
1010
+ return out;
1011
+ }
1012
+ function repairDiagramEdges(v) {
1013
+ if (v === void 0) return [];
1014
+ if (!Array.isArray(v)) return void 0;
1015
+ const out = [];
1016
+ for (const raw of v) {
1017
+ if (out.length >= GENUI_LIMITS.maxDiagramEdges) break;
1018
+ const o = obj(raw);
1019
+ if (o === void 0) continue;
1020
+ const from = str(o.from, 128);
1021
+ const to = str(o.to, 128);
1022
+ if (from === void 0 || to === void 0) continue;
1023
+ out.push({
1024
+ from,
1025
+ to,
1026
+ ...opt("label", str(o.label, GENUI_LIMITS.maxDiagramLabel)),
1027
+ ...opt("kind", enu(o.kind, DIAGRAM_EDGE_KINDS)),
1028
+ ...opt("route", enu(o.route, DIAGRAM_ROUTES))
1029
+ });
1030
+ }
1031
+ return out;
1032
+ }
1033
+ function repairDiagramTheme(v) {
1034
+ const o = obj(v);
1035
+ if (o === void 0) return void 0;
1036
+ const out = {};
1037
+ for (const key of [
1038
+ "paper",
1039
+ "paper-2",
1040
+ "ink",
1041
+ "muted",
1042
+ "soft",
1043
+ "rule",
1044
+ "accent",
1045
+ "accent-tint",
1046
+ "link"
1047
+ ]) {
1048
+ const c = color(o[key]);
1049
+ if (c !== void 0) out[key] = c;
1050
+ }
1051
+ return Object.keys(out).length === 0 ? void 0 : out;
1052
+ }
1053
+ function repairDiagramZones(v) {
1054
+ if (v === void 0) return [];
1055
+ if (!Array.isArray(v)) return void 0;
1056
+ const out = [];
1057
+ for (const raw of v) {
1058
+ if (out.length >= GENUI_LIMITS.maxDiagramZones) break;
1059
+ const o = obj(raw);
1060
+ if (o === void 0) continue;
1061
+ const label = str(o.label, 64);
1062
+ if (label === void 0) continue;
1063
+ out.push({
1064
+ label,
1065
+ ...opt("x", o.x === void 0 ? void 0 : grid4(num(o.x, -1e6, 1e6) ?? 0, 0, 1e6)),
1066
+ ...opt("y", o.y === void 0 ? void 0 : grid4(num(o.y, -1e6, 1e6) ?? 0, 0, 1e6)),
1067
+ ...opt("w", o.w === void 0 ? void 0 : grid4(num(o.w, -1e6, 1e6) ?? 100, 40, 2e3)),
1068
+ ...opt("h", o.h === void 0 ? void 0 : grid4(num(o.h, -1e6, 1e6) ?? 100, 40, 1200))
1069
+ });
1070
+ }
1071
+ return out;
1072
+ }
1073
+ function repairDiagram(v) {
1074
+ const o = obj(v);
1075
+ if (o === void 0) return null;
1076
+ const kind = enu(o.kind, DIAGRAM_KINDS);
1077
+ if (kind === void 0) return null;
1078
+ const nodes = repairDiagramNodes(o.nodes);
1079
+ if (nodes === void 0) return null;
1080
+ const edges = repairDiagramEdges(o.edges);
1081
+ if (edges === void 0) return null;
1082
+ const zones = repairDiagramZones(o.zones);
1083
+ if (zones === void 0) return null;
1084
+ return {
1085
+ type: "diagram",
1086
+ kind,
1087
+ nodes,
1088
+ edges,
1089
+ zones,
1090
+ ...opt("variant", enu(o.variant, DIAGRAM_VARIANTS)),
1091
+ ...opt("title", str(o.title, 256)),
1092
+ ...opt("theme", repairDiagramTheme(o.theme))
1093
+ };
1094
+ }
1095
+ function repairTimeline(v, cap) {
1096
+ if (!Array.isArray(v)) return void 0;
1097
+ const out = [];
1098
+ for (const item of v) {
1099
+ if (out.length >= cap) break;
1100
+ const o = obj(item);
1101
+ const title = o === void 0 ? void 0 : str(o.title, 256);
1102
+ if (title === void 0) continue;
1103
+ out.push({
1104
+ title,
1105
+ ...opt("desc", o === void 0 ? void 0 : str(o.desc, GENUI_LIMITS.maxString)),
1106
+ ...opt("time", o === void 0 ? void 0 : str(o.time, 128))
1107
+ });
1108
+ }
1109
+ return out;
1110
+ }
1111
+ function repairTree(v, cap) {
1112
+ return walkTree(v, cap, GENUI_LIMITS.maxTreeDepth);
1113
+ }
1114
+ function walkTree(v, cap, depthLeft) {
1115
+ if (!Array.isArray(v)) return void 0;
1116
+ const out = [];
1117
+ for (const item of v) {
1118
+ if (out.length >= cap) break;
1119
+ const o = obj(item);
1120
+ const name = o === void 0 ? void 0 : str(o.name, 256);
1121
+ if (name === void 0) continue;
1122
+ const children = o !== void 0 && depthLeft > 0 && Array.isArray(o.children) ? walkTree(o.children, cap, depthLeft - 1) : void 0;
1123
+ out.push({
1124
+ name,
1125
+ ...opt("type", o === void 0 ? void 0 : enu(o.type, FILE_TYPES)),
1126
+ ...opt("children", children)
1127
+ });
1128
+ }
1129
+ return out;
1130
+ }
1131
+ function repairQuizOptions(v) {
1132
+ if (!Array.isArray(v)) return void 0;
1133
+ const out = [];
1134
+ for (const optItem of v) {
1135
+ if (out.length >= GENUI_LIMITS.maxQuizOptions) break;
1136
+ const o = obj(optItem);
1137
+ const label = o === void 0 ? void 0 : str(o.label, 512);
1138
+ if (label === void 0) continue;
1139
+ out.push({
1140
+ label,
1141
+ ...opt("correct", o === void 0 ? void 0 : o.correct === true ? true : void 0),
1142
+ ...opt("feedback", o === void 0 ? void 0 : str(o.feedback, GENUI_LIMITS.maxString))
1143
+ });
1144
+ }
1145
+ return out;
1146
+ }
1147
+ /**
1148
+ * Patterns that indicate HTML/script injection in a string field. ECharts
1149
+ * default `tooltip.renderMode: 'html'` writes tooltip content via
1150
+ * `innerHTML`; even with renderMode forced to 'richText' (see below),
1151
+ * filtering these patterns is defense-in-depth — a model (or a
1152
+ * prompt-injected model) should never emit `<script>`, `onerror=`, or
1153
+ * `javascript:` inside a chart option string.
1154
+ */
1155
+ const ECHART_HTML_DANGER_RE = /<(?:script|img|svg|iframe|video|audio|object|embed|source)\b|on[a-z]+\s*=|javascript:/i;
1156
+ /**
1157
+ * Sanitize an ECharts option object: depth-bounded, budget-bounded
1158
+ * pass-through that strips dangerous values (functions, `url()` in styles,
1159
+ * HTML/script injection patterns in strings) but preserves the object shape
1160
+ * ECharts needs. Scalars are KEPT: ECharts options are full of them,
1161
+ * including inside `data` arrays (`data: [120, 150, 180]`,
1162
+ * `xAxis.data: ['1月', '2月']`). Previously a scalar hit the plain-object
1163
+ * gate below and returned undefined, so every primitive-valued array was
1164
+ * filtered to empty and dropped — a chart with a full `option` rendered
1165
+ * with empty series (blank canvas). This is a safety walk, not an ECharts
1166
+ * semantic validator.
1167
+ *
1168
+ * Security: `tooltip.renderMode` is forced to `'richText'` on every tooltip
1169
+ * object. ECharts' default `'html'` mode writes tooltip content via
1170
+ * `innerHTML`, which is an XSS vector when the option originates from model
1171
+ * output — a prompt-injected model could emit
1172
+ * `{"tooltip":{"formatter":"<img src=x onerror=...>"}}` and execute
1173
+ * arbitrary script. `richText` renders as text, never touching innerHTML.
1174
+ */
1175
+ function sanitizeEChartOption(v, depth, budget) {
1176
+ if (budget.count <= 0) return void 0;
1177
+ budget.count -= 1;
1178
+ if (depth > GENUI_LIMITS.maxEChartOptionDepth) return void 0;
1179
+ if (typeof v === "string") {
1180
+ const s = v.slice(0, GENUI_LIMITS.maxString);
1181
+ if (s.toLowerCase().includes("url(") || ECHART_HTML_DANGER_RE.test(s)) return void 0;
1182
+ return s;
1183
+ }
1184
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1185
+ if (typeof v === "boolean") return v;
1186
+ if (v === null) return null;
1187
+ if (Array.isArray(v)) {
1188
+ const cap = Math.min(v.length, GENUI_LIMITS.maxEChartArrayLen);
1189
+ const arr = [];
1190
+ for (let i = 0; i < cap; i++) {
1191
+ const s = sanitizeEChartOption(v[i], depth + 1, budget);
1192
+ if (s !== void 0) arr.push(s);
1193
+ }
1194
+ return arr.length > 0 ? arr : void 0;
1195
+ }
1196
+ const o = obj(v);
1197
+ if (o === void 0) return void 0;
1198
+ const out = {};
1199
+ for (const [key, val] of Object.entries(o)) {
1200
+ const s = sanitizeEChartOption(val, depth + 1, budget);
1201
+ if (s === void 0) continue;
1202
+ if (key === "tooltip" && typeof s === "object" && s !== null && !Array.isArray(s)) s.renderMode = "richText";
1203
+ out[key] = s;
1204
+ }
1205
+ return Object.keys(out).length > 0 ? out : void 0;
1206
+ }
1207
+ /**
1208
+ * Deterministically repair a raw spec value into a renderable GenuiSpec.
1209
+ * Returns null only when the root is not an object with an `items` array
1210
+ * (a bare component root is wrapped into a col first — the documented fence
1211
+ * vocabulary allows single-component bodies); every other defect is healed by
1212
+ * dropping/clamping/truncating. Idempotent: repairing a repaired spec is a
1213
+ * no-op.
1214
+ */
1215
+ function repairGenuiSpec(value) {
1216
+ const v = obj(value);
1217
+ if (v === void 0) return null;
1218
+ if (!Array.isArray(v.items)) {
1219
+ const wrapped = wrapSingleComponentRoot(value);
1220
+ if (wrapped === null) return null;
1221
+ return repairGenuiSpec(wrapped);
1222
+ }
1223
+ const ctx = { remaining: GENUI_LIMITS.maxNodes };
1224
+ return {
1225
+ ...opt("title", str(v.title, GENUI_LIMITS.maxString)),
1226
+ ...opt("gap", num(v.gap, 0, 96)),
1227
+ ...opt("panel", v.panel === true ? true : void 0),
1228
+ ...opt("append", v.append === true ? true : void 0),
1229
+ items: repairItems(v.items, ctx, 0)
1230
+ };
1231
+ }
1232
+ /**
1233
+ * Count the nodes of a spec tree (every item, descending into tabs /
1234
+ * accordion / file-tree / list containers — the same descent
1235
+ * `validateGenuiSpec` walks). Shared by the panel fold (node-budget gate)
1236
+ * and validation, so the panel never runs a second, divergent traversal.
1237
+ * `cap` bounds the walk for hostile inputs; the panel passes
1238
+ * `PANEL_LIMITS.maxNodes + 1` to detect overflow without counting the whole
1239
+ * tree.
1240
+ */
1241
+ function countGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
1242
+ let count = 0;
1243
+ const walk = (list) => {
1244
+ if (!Array.isArray(list)) return;
1245
+ for (const item of list) {
1246
+ if (count >= cap) return;
1247
+ count += 1;
1248
+ const v = obj(item);
1249
+ if (v === void 0) continue;
1250
+ if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) {
1251
+ if (count >= cap) return;
1252
+ const to = obj(t);
1253
+ if (to !== void 0) walk(to.items);
1254
+ }
1255
+ else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) {
1256
+ if (count >= cap) return;
1257
+ const io = obj(it);
1258
+ if (io !== void 0) walk(io.items);
1259
+ }
1260
+ else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
1261
+ else if (v.type === "file-tree" && Array.isArray(v.items)) walk(v.items);
1262
+ else if (v.type === "list" && Array.isArray(v.items)) for (const li of v.items) {
1263
+ if (count >= cap) return;
1264
+ const lo = obj(li);
1265
+ if (lo !== void 0 && typeof lo.type === "string") walk([lo]);
1266
+ }
1267
+ }
1268
+ };
1269
+ const root = obj(value);
1270
+ walk(root === void 0 ? [] : root.items);
1271
+ return count;
1272
+ }
1273
+ /** Every white-listed node `type`. Keep in sync with the repairNode switch —
1274
+ * validate_dsh_ui uses it to tell declared GenUI nodes apart from unrelated
1275
+ * `"type"` strings (e.g. file-tree's `{type:'file'}` children). */
1276
+ const GENUI_NODE_TYPES = /* @__PURE__ */ new Set([
1277
+ "accordion",
1278
+ "audio",
1279
+ "avatar",
1280
+ "badge",
1281
+ "breadcrumb",
1282
+ "button",
1283
+ "callout",
1284
+ "card",
1285
+ "chart",
1286
+ "checkbox",
1287
+ "code",
1288
+ "col",
1289
+ "copy",
1290
+ "diff",
1291
+ "divider",
1292
+ "file-tree",
1293
+ "grid",
1294
+ "input",
1295
+ "json",
1296
+ "keyvalue",
1297
+ "link",
1298
+ "list",
1299
+ "mermaid",
1300
+ "plot",
1301
+ "progress",
1302
+ "quiz",
1303
+ "radio",
1304
+ "row",
1305
+ "scene3d",
1306
+ "select",
1307
+ "slider",
1308
+ "spacer",
1309
+ "stat",
1310
+ "steps",
1311
+ "submit",
1312
+ "switch",
1313
+ "table",
1314
+ "tabs",
1315
+ "text",
1316
+ "textarea",
1317
+ "timeline",
1318
+ "video",
1319
+ "echart",
1320
+ "diagram"
1321
+ ]);
1322
+ /**
1323
+ * Count DECLARED nodes in a raw spec tree: objects whose `type` is a
1324
+ * white-listed string, descending the same containers `countGenuiNodes`
1325
+ * walks. `validate_dsh_ui` compares this with the repaired count to surface
1326
+ * children the repair silently dropped (blank-render class of bugs, issue
1327
+ * #42) instead of reporting a green check on a half-empty tree.
1328
+ */
1329
+ function countDeclaredGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
1330
+ let count = 0;
1331
+ const declared = (candidate) => {
1332
+ const o = obj(candidate);
1333
+ return o !== void 0 && typeof o.type === "string" && GENUI_NODE_TYPES.has(o.type);
1334
+ };
1335
+ const walk = (list) => {
1336
+ if (!Array.isArray(list)) return;
1337
+ for (const item of list) {
1338
+ if (count >= cap) return;
1339
+ if (!declared(item)) continue;
1340
+ count += 1;
1341
+ const v = obj(item);
1342
+ if (v === void 0) continue;
1343
+ if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) walkItemsOf(t);
1344
+ else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) walkItemsOf(it);
1345
+ else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
1346
+ else if (v.type === "list" && Array.isArray(v.items)) {
1347
+ for (const li of v.items) if (declared(li)) walk([li]);
1348
+ }
1349
+ }
1350
+ };
1351
+ const walkItemsOf = (holder) => {
1352
+ const o = obj(holder);
1353
+ if (o === void 0) return;
1354
+ const items = o.items !== void 0 ? o.items : o.content;
1355
+ if (Array.isArray(items)) walk(items);
1356
+ else if (declared(items)) walk([items]);
1357
+ };
1358
+ const root = obj(value);
1359
+ if (root === void 0) return count;
1360
+ if (!Array.isArray(root.items) && declared(value)) walk([value]);
1361
+ else walk(root.items);
1362
+ return count;
1363
+ }
1364
+ //#endregion
1365
+ //#region src/shared/fence-repair.ts
1366
+ /**
1367
+ * Tier-2 repair — SETTLED MESSAGES ONLY (never while streaming): heals
1368
+ * structural incompleteness — missing closing quotes/brackets — by appending
1369
+ * the missing terminators, and heals stray closers — a `]` mistyped as `}` or
1370
+ * a duplicated terminator — by skipping closers that do not match the open
1371
+ * stack (they cannot be legal JSON). Callers gate it on settled messages (the
1372
+ * client uses the host-provided fence source; the validate tool is by
1373
+ * definition pre-emission), so a streaming half can never flash premature UI.
1374
+ *
1375
+ * ONE unified scan: the tier-1 fixes (quote escaping + trailing-comma drops)
1376
+ * are folded into the same pass, so bodies that combine BOTH defect classes
1377
+ * (a trailing comma AND a missing closer) heal in one shot — the old
1378
+ * two-phase chain lost tier-1's partial work when its whole-body parse
1379
+ * failed, and re-scanning the raw text could not compose the repairs.
1380
+ * Adopted only when the completed body parses as whole JSON.
1381
+ */
1382
+ function completeFenceJson(raw) {
1383
+ try {
1384
+ JSON.parse(raw);
1385
+ return null;
1386
+ } catch {}
1387
+ let out = "";
1388
+ const stack = [];
1389
+ let inString = false;
1390
+ let escaped = false;
1391
+ let repairs = 0;
1392
+ for (let i = 0; i < raw.length; i++) {
1393
+ const ch = raw[i];
1394
+ if (escaped) {
1395
+ out += ch;
1396
+ escaped = false;
1397
+ continue;
1398
+ }
1399
+ if (inString) {
1400
+ if (ch === "\\") {
1401
+ out += ch;
1402
+ escaped = true;
1403
+ continue;
1404
+ }
1405
+ if (ch !== "\"") {
1406
+ out += ch;
1407
+ continue;
1408
+ }
1409
+ let j = i + 1;
1410
+ while (j < raw.length && (raw[j] === " " || raw[j] === " " || raw[j] === "\n" || raw[j] === "\r")) j++;
1411
+ const next = j < raw.length ? raw[j] : "";
1412
+ if (next === "," || next === "]" || next === "}" || next === ":" || next === "") {
1413
+ inString = false;
1414
+ out += ch;
1415
+ } else {
1416
+ out += "\\\"";
1417
+ repairs++;
1418
+ }
1419
+ continue;
1420
+ }
1421
+ if (ch === "\"") {
1422
+ inString = true;
1423
+ out += ch;
1424
+ continue;
1425
+ }
1426
+ if (ch === "{") {
1427
+ stack.push("}");
1428
+ out += ch;
1429
+ continue;
1430
+ }
1431
+ if (ch === "[") {
1432
+ stack.push("]");
1433
+ out += ch;
1434
+ continue;
1435
+ }
1436
+ if (ch === "}" || ch === "]") {
1437
+ if (stack[stack.length - 1] === ch) {
1438
+ stack.pop();
1439
+ out += ch;
1440
+ } else repairs++;
1441
+ continue;
1442
+ }
1443
+ if (ch === ",") {
1444
+ let j = i + 1;
1445
+ while (j < raw.length && (raw[j] === " " || raw[j] === " " || raw[j] === "\n" || raw[j] === "\r")) j++;
1446
+ const next = j < raw.length ? raw[j] : "";
1447
+ if (next === "}" || next === "]" || next === "") {
1448
+ repairs++;
1449
+ continue;
1450
+ }
1451
+ }
1452
+ out += ch;
1453
+ }
1454
+ if (inString) {
1455
+ out += "\"";
1456
+ repairs++;
1457
+ }
1458
+ while (stack.length > 0) {
1459
+ out += stack.pop();
1460
+ repairs++;
1461
+ }
1462
+ if (repairs === 0) return null;
1463
+ try {
1464
+ JSON.parse(out);
1465
+ return {
1466
+ text: out,
1467
+ repairs
1468
+ };
1469
+ } catch {
1470
+ return null;
1471
+ }
1472
+ }
1473
+ //#endregion
1474
+ //#region src/plugin/tool.ts
1475
+ /**
1476
+ * Arguments schema: an open `spec` slot. The schema must NOT reject anything
1477
+ * the guard could repair — the model's component trees are imperfect by
1478
+ * nature, and the guard heals them; argument validation would only strand
1479
+ * them. `additionalProperties: false` keeps the call shape honest.
1480
+ *
1481
+ * `spec` IS typed `object` on purpose: the guard can only repair plain
1482
+ * records (a serialized JSON string, array, or scalar root is unusable), so
1483
+ * argument validation rejecting non-objects loses nothing repairable — and
1484
+ * it stops the model from double-encoding the tree as a string (observed
1485
+ * twice in the wild), failing fast with a clear schema error instead.
1486
+ */
1487
+ const RENDER_UI_PARAMETERS = {
1488
+ type: "object",
1489
+ properties: { spec: {
1490
+ type: "object",
1491
+ description: "GenUI component tree (white-listed vocabulary, see the dsh-ui fence section in the system prompt). Deep-validated and repaired by the renderer. Pass the spec as a JSON OBJECT — never as a serialized JSON string (a string fails argument validation).",
1492
+ properties: {
1493
+ title: {
1494
+ type: "string",
1495
+ description: "Short title shown as the card banner."
1496
+ },
1497
+ gap: {
1498
+ type: "number",
1499
+ description: "Vertical gap between root items in px."
1500
+ },
1501
+ panel: {
1502
+ type: "boolean",
1503
+ description: "Panel-only: renders into the session panel dock instead of the message flow."
1504
+ },
1505
+ items: {
1506
+ type: "array",
1507
+ description: "Root component list (white-listed vocabulary).",
1508
+ items: { type: "object" }
1509
+ }
1510
+ }
1511
+ } },
1512
+ required: ["spec"],
1513
+ additionalProperties: false
1514
+ };
1515
+ /** The tool's canonical value is a short model-facing summary string. */
1516
+ const RENDER_UI_OUTPUT_SCHEMA = {
1517
+ type: "string",
1518
+ description: "One-line human-readable render summary for the model."
1519
+ };
1520
+ /**
1521
+ * Read the `spec` argument defensively (presenters run on replayed args).
1522
+ *
1523
+ * The harness tool-call bridge has been observed to deliver arguments in
1524
+ * shapes other than the authored `{ spec: <object> }`:
1525
+ * - `{ spec: "<JSON string>" }` — spec serialized to text;
1526
+ * - `{ arguments: "<JSON string>" }` / `{ arguments: <object> }` — a
1527
+ * double-encoded wrapper from the SDK tool-call bridge (seen live in the
1528
+ * web GUI: small specs arrived wrapped this way, large specs arrived with
1529
+ * their JSON corrupted mid-stream);
1530
+ * - a bare JSON string (double-encoded root).
1531
+ * Each shape is unwrapped here so the guard can repair the actual tree.
1532
+ * Corrupted JSON cannot be recovered (bytes were lost in transit): it yields
1533
+ * `undefined` plus a diagnostic log line for the transport-layer bug.
1534
+ */
1535
+ function specOf(args) {
1536
+ if (typeof args === "string") return parseSpecJson(args, "bare-string");
1537
+ if (typeof args !== "object" || args === null) return void 0;
1538
+ const record = args;
1539
+ if ("spec" in record) {
1540
+ const s = record.spec;
1541
+ if (typeof s === "string") return parseSpecJson(s, "spec-string");
1542
+ return unwrapSpec(s, "spec");
1543
+ }
1544
+ if ("arguments" in record) {
1545
+ const a = record.arguments;
1546
+ if (typeof a === "string") return parseSpecJson(a, "arguments-string");
1547
+ if (typeof a === "object" && a !== null) return unwrapSpec(a, "arguments");
1548
+ }
1549
+ }
1550
+ /**
1551
+ * Peel nested `{ spec: ... }` wrapper layers. Observed bridge shapes nest the
1552
+ * authored `spec` object one or more levels deep (e.g. the serialized text
1553
+ * inside `{ arguments: "..." }` is itself `{ spec: { title, gap, items } }`),
1554
+ * so unwrapping stops only at a value that carries no `spec` key.
1555
+ */
1556
+ function unwrapSpec(value, shape) {
1557
+ if (typeof value === "object" && value !== null) {
1558
+ const record = value;
1559
+ if ("spec" in record) {
1560
+ const s = record.spec;
1561
+ if (typeof s === "string") return parseSpecJson(s, `${shape}/spec-string`);
1562
+ return unwrapSpec(s, `${shape}/spec`);
1563
+ }
1564
+ }
1565
+ return value;
1566
+ }
1567
+ /** Try to decode a serialized spec; log a diagnostic when it is broken. */
1568
+ function parseSpecJson(raw, shape) {
1569
+ try {
1570
+ return unwrapSpec(JSON.parse(raw), shape);
1571
+ } catch (error) {
1572
+ const detail = error instanceof Error ? error.message : String(error);
1573
+ const pos = /position (\d+)/.exec(detail)?.[1] ?? "?";
1574
+ console.error(`[genui-tool] spec wrapped as ${shape} but its JSON is broken (${raw.length} bytes, error at ${pos}); cannot recover — bytes lost in transit`);
1575
+ return;
1576
+ }
1577
+ }
1578
+ /** Total node count of a repaired spec — the shared guard traversal
1579
+ * (`countGenuiNodes`) so the tool reports the SAME number the panel fold and
1580
+ * validation use. A local walker used to under-count specs whose content
1581
+ * lives inside tabs/accordion/file-tree (their children are not `.items`). */
1582
+ function countNodes(spec) {
1583
+ return countGenuiNodes(spec, GENUI_LIMITS.maxNodes);
1584
+ }
1585
+ /** Tool-call title shared by the pending and completed presentations. */
1586
+ function cardTitle(args) {
1587
+ const spec = repairGenuiSpec(specOf(args));
1588
+ return spec === null ? void 0 : `渲染 UI:${spec.title ?? "未命名"}`;
1589
+ }
1590
+ /**
1591
+ * Build the render_ui tool definition. Registered by the plugin node half;
1592
+ * `ctx.tools.register` consumes it exactly like a `defineTool` result.
1593
+ */
1594
+ function createRenderUiTool() {
1595
+ return {
1596
+ name: "render_ui",
1597
+ description: "Render an interactive UI card in the conversation tool row by passing a GenUI spec (a white-listed component tree; the same vocabulary as the ```dsh-ui fence, see the system prompt). Use it when the user asks for a structured panel, dashboard, or form that belongs in the tool row rather than inline in the reply. The card is interactive client-side (tabs, buttons, inputs, switches); components carrying an \"action\" field send [genui-action] back to you when the user interacts, and you should re-render the updated UI.",
1598
+ parameters: RENDER_UI_PARAMETERS,
1599
+ output: {
1600
+ schema: RENDER_UI_OUTPUT_SCHEMA,
1601
+ render(_args, value) {
1602
+ return [{
1603
+ type: "text",
1604
+ text: String(value)
1605
+ }];
1606
+ },
1607
+ presentationMeta(args) {
1608
+ return repairGenuiSpec(specOf(args));
1609
+ }
1610
+ },
1611
+ async execute(args) {
1612
+ const spec = repairGenuiSpec(specOf(args));
1613
+ if (spec === null) return "render_ui:spec 无效 —— 根对象需要 \"items\" 数组(组件树白名单见系统提示词),请修正后重试。";
1614
+ return `已渲染 UI「${spec.title ?? "未命名"}」(${countNodes(spec)} 个组件)。用户现在可以看到这张卡片;组件带 action 时,用户交互会以 [genui-action] 消息发回给你,届时请重新渲染更新后的界面。`;
1615
+ },
1616
+ presentCall(args) {
1617
+ const title = cardTitle(args);
1618
+ return title === void 0 ? void 0 : {
1619
+ card: "generic",
1620
+ title,
1621
+ kind: "other"
1622
+ };
1623
+ },
1624
+ presentResult(args) {
1625
+ const title = cardTitle(args);
1626
+ return title === void 0 ? void 0 : {
1627
+ card: "generic",
1628
+ title
1629
+ };
1630
+ }
1631
+ };
1632
+ }
1633
+ /**
1634
+ * The `validate_dsh_ui` tool: a model-facing pre-flight check for the
1635
+ * ```dsh-ui fence channel. The model calls it with the JSON text it is about
1636
+ * to put inside a fence; it reports whether the body parses as a valid GenUI
1637
+ * spec, and when it does not, WHERE it breaks and WHAT is likely wrong
1638
+ * (bracket counts, common typo classes) so the model can fix and re-validate
1639
+ * before emitting — turning "render a red banner after the fact" into
1640
+ * "verify before you send". Purely local: no LLM, no network, no DOM.
1641
+ */
1642
+ const VALIDATE_DESCRIPTION = "Validate the JSON body of a ```dsh-ui fence BEFORE emitting it — use for non-trivial specs (≥3 nodes or containing a table); skip for trivial ones (≤2 nodes). Pass the exact JSON text you are about to put inside the fence as the \"spec\" argument (a string). Returns ✅ when it parses as a valid GenUI spec, or ❌ with the exact position, bracket counts, and likely causes when it does not — fix the JSON, re-validate, and only then emit the fence. When the JSON is broken but repairable (unescaped quotes, trailing commas, missing closers), the ❌ reply INCLUDES the auto-repaired JSON — copy it verbatim into the fence instead of rewriting by hand.";
1643
+ const VALIDATE_PARAMETERS = {
1644
+ type: "object",
1645
+ properties: { spec: {
1646
+ oneOf: [{
1647
+ type: "string",
1648
+ description: "The exact JSON text of the fence body."
1649
+ }, {
1650
+ type: "object",
1651
+ description: "The spec object (serialized before validation)."
1652
+ }],
1653
+ description: "The dsh-ui fence body to validate: pass the JSON as a string for an exact check, or as the spec object."
1654
+ } },
1655
+ required: ["spec"],
1656
+ additionalProperties: false
1657
+ };
1658
+ /** Read the fence-body text from the call args (string preferred, object serialized). */
1659
+ function fenceTextOf(args) {
1660
+ if (typeof args === "string") return args;
1661
+ if (typeof args !== "object" || args === null) return null;
1662
+ const record = args;
1663
+ const s = "spec" in record ? record.spec : "arguments" in record ? record.arguments : void 0;
1664
+ if (typeof s === "string") return s;
1665
+ if (typeof s === "object" && s !== null) return JSON.stringify(s);
1666
+ return null;
1667
+ }
1668
+ /** Count structural brackets outside string literals. */
1669
+ function bracketCounts(raw) {
1670
+ const counts = {
1671
+ "{": 0,
1672
+ "}": 0,
1673
+ "[": 0,
1674
+ "]": 0
1675
+ };
1676
+ let inString = false;
1677
+ let escaped = false;
1678
+ for (let i = 0; i < raw.length; i++) {
1679
+ const ch = raw[i];
1680
+ if (escaped) {
1681
+ escaped = false;
1682
+ continue;
1683
+ }
1684
+ if (inString) {
1685
+ if (ch === "\\") escaped = true;
1686
+ else if (ch === "\"") inString = false;
1687
+ continue;
1688
+ }
1689
+ if (ch === "\"") {
1690
+ inString = true;
1691
+ continue;
1692
+ }
1693
+ if (ch === "{") counts["{"] += 1;
1694
+ else if (ch === "}") counts["}"] += 1;
1695
+ else if (ch === "[") counts["["] += 1;
1696
+ else if (ch === "]") counts["]"] += 1;
1697
+ }
1698
+ return counts;
1699
+ }
1700
+ /** Short structural hint from bracket counts (empty when balanced). */
1701
+ function bracketDiagnostic(raw) {
1702
+ const c = bracketCounts(raw);
1703
+ const diffs = [];
1704
+ if (c["{"] !== c["}"]) {
1705
+ const d = c["{"] - c["}"];
1706
+ diffs.push(`{ ×${c["{"]} / } ×${c["}"]} → ${d > 0 ? `缺 ${d} 个 }` : `多 ${-d} 个 }`}`);
1707
+ }
1708
+ if (c["["] !== c["]"]) {
1709
+ const d = c["["] - c["]"];
1710
+ diffs.push(`[ ×${c["["]} / ] ×${c["]"]} → ${d > 0 ? `缺 ${d} 个 ]` : `多 ${-d} 个 ]`}`);
1711
+ }
1712
+ return diffs.length === 0 ? "" : ` 括号计数:${diffs.join(";")}(长表格最易在收尾处错位,如把 ]]}]} 写成 ]}]}]})\n`;
1713
+ }
1714
+ const COMMON_CAUSES = "常见原因:① 收尾括号错位/缺失({ 与 }、[ 与 ] 数量不相等)② 字符串值内用了半角引号 \"(中文引语请用 “” 或 「」)③ 尾随逗号 ④ 字符串未闭合";
1715
+ /** Build the validate_dsh_ui tool definition (registered alongside render_ui). */
1716
+ function createValidateDshUiTool() {
1717
+ return {
1718
+ name: "validate_dsh_ui",
1719
+ description: VALIDATE_DESCRIPTION,
1720
+ parameters: VALIDATE_PARAMETERS,
1721
+ output: {
1722
+ schema: {
1723
+ type: "string",
1724
+ description: "Validation verdict for the model."
1725
+ },
1726
+ render(_args, value) {
1727
+ return [{
1728
+ type: "text",
1729
+ text: String(value)
1730
+ }];
1731
+ }
1732
+ },
1733
+ async execute(args) {
1734
+ const raw = fenceTextOf(args);
1735
+ if (raw === null || raw.trim() === "") return "❌ validate_dsh_ui:缺少 spec 参数 —— 把围栏 JSON 文本作为 spec 传入。";
1736
+ let parsed;
1737
+ try {
1738
+ parsed = JSON.parse(raw);
1739
+ } catch (error) {
1740
+ const detail = error instanceof Error ? error.message : String(error);
1741
+ const repaired = completeFenceJson(raw);
1742
+ if (repaired !== null && repairGenuiSpec(JSON.parse(repaired.text)) !== null) return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``;
1743
+ return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 自动修复未能恢复(结构损坏),请按错误信息修正后重新调用本工具验证,通过后再发出围栏。\n${COMMON_CAUSES}`;
1744
+ }
1745
+ const spec = repairGenuiSpec(parsed);
1746
+ if (spec === null) return "❌ 不是合法 GenUI spec:根对象需要 \"items\" 数组,且每个节点 type 必须在白名单内(见系统提示词)。请修正后重新验证。";
1747
+ const validCount = countNodes(spec);
1748
+ const declaredCount = countDeclaredGenuiNodes(parsed, GENUI_LIMITS.maxNodes + 1);
1749
+ if (declaredCount > validCount) return `❌ 验证未通过:检测到声明了 ${declaredCount} 个组件,但仅成功解析出 ${validCount} 个(有 ${declaredCount - validCount} 个组件因字段格式异常被丢弃)。常见原因:table 的 columns/rows 不是二维字符串数组、tabs 的 items/content 缺失、嵌套组件字段类型不符。请修正后重新验证。`;
1750
+ return `✅ dsh-ui spec 合法(${validCount} 个组件),可以发出围栏。`;
1751
+ },
1752
+ presentCall() {
1753
+ return {
1754
+ card: "generic",
1755
+ title: "验证 dsh-ui 围栏",
1756
+ kind: "other"
1757
+ };
1758
+ },
1759
+ presentResult() {
1760
+ return {
1761
+ card: "generic",
1762
+ title: "验证 dsh-ui 围栏"
1763
+ };
1764
+ }
1765
+ };
1766
+ }
1767
+ //#endregion
1768
+ //#region src/plugin/index.ts
1769
+ /** Convention: tool guidance uses 100–199; bash's section is 104. */
1770
+ const GENUI_SECTION_ORDER = 105;
1771
+ /**
1772
+ * The mermaid/three engines ship as standalone IIFE bundles under
1773
+ * `lib/assets/` and are fetched by the client ONLY when a spec needs them.
1774
+ * This route serves them from the plugin's own package directory through the
1775
+ * host webserver service — the longest-prefix rule lets it win over the
1776
+ * generic `/plugins` bundle route, and no host source change is needed. The
1777
+ * service is optional at this plugin's start time (same ordering reality as
1778
+ * the tools registry), so registration probes immediately AND on the
1779
+ * `internal/service` event, exactly like the tools registration below.
1780
+ */
1781
+ /** Route prefix under /plugins; anything under it is this plugin's asset. */
1782
+ const ASSET_ROUTE_PATH = "/plugins/@changfenhuang/dsh-genui/assets";
1783
+ /** Safe flat file names only: no slashes, no traversal, js assets only. */
1784
+ const ASSET_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\.js$/;
1785
+ /** The handler itself (registered via the optional webServer probe). */
1786
+ async function serveGenuiAsset(req, res) {
1787
+ if (req.method !== "GET" && req.method !== "HEAD") {
1788
+ res.writeHead(405);
1789
+ res.end();
1790
+ return;
1791
+ }
1792
+ let pathname;
1793
+ try {
1794
+ pathname = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
1795
+ } catch {
1796
+ res.writeHead(400);
1797
+ res.end();
1798
+ return;
1799
+ }
1800
+ const rel = pathname.startsWith(`${ASSET_ROUTE_PATH}/`) ? pathname.slice(40) : null;
1801
+ if (rel === null) {
1802
+ res.writeHead(404);
1803
+ res.end();
1804
+ return;
1805
+ }
1806
+ const file = rel.slice(1);
1807
+ if (!ASSET_FILE_RE.test(file)) {
1808
+ res.writeHead(404);
1809
+ res.end();
1810
+ return;
1811
+ }
1812
+ try {
1813
+ const dir = fileURLToPath(new URL("./assets/", import.meta.url));
1814
+ const body = await readFile(join(dir, file));
1815
+ res.writeHead(200, {
1816
+ "content-type": "text/javascript; charset=utf-8",
1817
+ "cache-control": "no-cache"
1818
+ });
1819
+ res.end(body);
1820
+ } catch {
1821
+ res.writeHead(404);
1822
+ res.end();
1823
+ }
1824
+ }
1825
+ /** The fence language description injected into every assembled system prompt.
1826
+ * Deliberately slim: the `genui` skill carries the full component→field
1827
+ * mapping; this section keeps only the contract that must always be
1828
+ * present (fence syntax, type whitelist, and critical behavioral rules). */
1829
+ const GENUI_SECTION_TEXT = `You can render interactive UI components INSIDE your reply — between paragraphs — by emitting a fenced block with the language tag \`dsh-ui\` containing a JSON spec:
1830
+
1831
+ \`\`\`dsh-ui
1832
+ {"title":"可选标题","gap":14,"items":[...]}
1833
+ \`\`\`
1834
+
1835
+ The spec is a white-listed component tree rendered inline where the fence sits. Only these \`type\` values; the \`genui\` skill, when available, carries the full content→component mapping and per-component field details:
1836
+
1837
+ - 布局: text · row · col · grid · card · divider · spacer
1838
+ - 展示: badge · stat · progress · list · table · keyvalue · avatar · audio · video · timeline · file-tree · breadcrumb · callout · steps · diff · json · code · copy
1839
+ - 图表: chart (bars|line|donut) · echart (preset|option) · plot (函数图)
1840
+ - 交互: button · input · textarea · select · checkbox · switch · slider · radio · submit · quiz · link · tabs · accordion
1841
+ - 高级: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) · diagram (编辑级架构/流程图,27 种 kind) · scene3d (3D WebGL)
1842
+
1843
+ Rules:
1844
+ - 触发: 结构化表达优于纯文本时主动用(要点、强调、对比、流程、步骤、状态、数据、演示),纯问答与一句话不套 UI;一个主题一个主组件,每次 3–8 个组件,同一数据不重复出现。
1845
+ - JSON 严格: 坏围栏降级为代码块;≥3 节点或含 table 的围栏发出前调用 validate_dsh_ui,❌ 修好再发(若附「已自动修复」JSON 照抄即可)。
1846
+ - 规模: ≤200 节点、嵌套≤8 层(超出被截断);3D mesh 1–5;plot 给合理 xMin/xMax。
1847
+ - LOCAL-FIRST + actions: UI 能自己做的状态变化(判卷、判题、重置、展开、选中)就地完成,零往返;action 只用于必须模型参与的事。交互组件带 "action":"name",交互以 [genui-action] name + 组件数据回传,届时重渲染更新 UI;无 action 的按钮禁用。
1848
+ - Durable state: 交互状态按「会话+内容指纹」持久化——刷新/重放恢复;重渲染相同内容保留,新内容重置。
1849
+ - 卷子模式: 每题一个 radio(group+answer+explanation)+ 一个 submit(groups 全列),本地判分。
1850
+ - Secrets ban: 不索取密码、API Key、Token、恢复码;需要时拒绝并解释。
1851
+ - Tool channel: render_ui 工具把同一 spec 渲染为工具行卡片(交付物型界面用);围栏用于回答内联 UI。
1852
+ - Panel: "panel":true 只渲染进会话面板 dock 并原地更新;"append":true 追加合并(同标签 tabs 追加/新标签加入/尾部追加);上限 200 节点/200 次追加,满了发 replace 重建。面板组件来的 [genui-action] 只回一个 panel:true 围栏 + 至多一行 10 字内确认,不解释、不用普通围栏。`;
1853
+ /**
1854
+ * Register the GenUI output-language section and the render_ui tool.
1855
+ * @param ctx - cordis context.
1856
+ */
1857
+ const inject = ["systemPrompt"];
1858
+ function apply(ctx) {
1859
+ ctx.systemPrompt.section({
1860
+ name: "genui:fence",
1861
+ order: 105,
1862
+ text: GENUI_SECTION_TEXT
1863
+ });
1864
+ let registered = false;
1865
+ const tryRegister = (value) => {
1866
+ if (registered) return;
1867
+ const tools = value ?? ctx.reflect.get("tools", false);
1868
+ if (tools === void 0) return;
1869
+ tools.register(createRenderUiTool());
1870
+ tools.register(createValidateDshUiTool());
1871
+ registered = true;
1872
+ };
1873
+ tryRegister(void 0);
1874
+ ctx.on("internal/service", (name, value) => {
1875
+ if (name === "tools") tryRegister(value);
1876
+ });
1877
+ let assetsRegistered = false;
1878
+ const tryRegisterAssets = (value) => {
1879
+ if (assetsRegistered) return;
1880
+ const webServer = value ?? ctx.reflect.get("webServer", false);
1881
+ if (webServer === void 0) return;
1882
+ webServer.register({
1883
+ kind: "prefix",
1884
+ path: ASSET_ROUTE_PATH,
1885
+ handler: serveGenuiAsset
1886
+ });
1887
+ assetsRegistered = true;
1888
+ };
1889
+ tryRegisterAssets(void 0);
1890
+ ctx.on("internal/service", (name, value) => {
1891
+ if (name === "webServer") tryRegisterAssets(value);
1892
+ });
1893
+ }
1894
+ //#endregion
1895
+ export { GENUI_SECTION_ORDER, GENUI_SECTION_TEXT, apply, inject };