@changfenhuang/dsh-genui 0.11.2 → 0.11.4

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.
@@ -1,15 +1,14 @@
1
- import type { AnswersState, GenuiBlockProps, QuestionMeta } from './state.ts';
1
+ import type { RadioSubmissionMember } from '../submission-registry.ts';
2
+ import type { AnswersState, GenuiBlockProps } from './state.ts';
2
3
  import type { GenuiInput, GenuiRadio, GenuiSelect, GenuiSlider, GenuiSubmit, GenuiSwitch, GenuiTextarea } from '../spec.ts';
3
4
  export declare function RadioNode({ node, onAction, answers }: {
4
5
  node: GenuiRadio;
5
6
  onAction?: GenuiBlockProps['onAction'];
6
7
  answers?: AnswersState | undefined;
7
8
  }): import("react").JSX.Element;
8
- /** Resolve a question's correct label from its metadata. */
9
- export declare function correctLabelOf(m: QuestionMeta): string | undefined;
10
- /** Submit: collect grouped radio and checkbox answers. Radio-only scopes keep
11
- * LOCAL-FIRST grading; when checkbox selections participate, the click falls
12
- * back to the aggregation action so multi-select data is never discarded. */
9
+ /** 根据静态 radio member 获取正确选项的标签。 */
10
+ export declare function correctLabelOf(m: RadioSubmissionMember): string | undefined;
11
+ /** 汇总当前 block 的表单状态,并根据 resolver 的结果提交或本地判卷。 */
13
12
  export declare function SubmitNode({ node, onAction, answers }: {
14
13
  node: GenuiSubmit;
15
14
  onAction?: GenuiBlockProps['onAction'];
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { GenuiSpec } from '../spec.ts';
9
9
  import type { BlockInteractionState } from '../interaction-store.ts';
10
+ import type { SubmissionRegistry } from '../submission-registry.ts';
10
11
  export interface GenuiBlockProps {
11
12
  /** Parsed spec to render. */
12
13
  spec: GenuiSpec;
@@ -33,15 +34,6 @@ export interface GenuiBlockProps {
33
34
  /** Observe the current secret-free durable state without changing persistence ownership. */
34
35
  onStateSnapshot?: ((state: BlockInteractionState) => void) | undefined;
35
36
  }
36
- /** Per-question metadata registered by grouped radios for local grading. */
37
- export interface QuestionMeta {
38
- label: string;
39
- options: string[];
40
- /** Correct option: index (number) or label (string); absent = no local grading. */
41
- answer?: number | string | undefined;
42
- /** Shown after local grading. */
43
- explanation?: string | undefined;
44
- }
45
37
  /** Block-wide aggregation state. Grouped radios store one selected label in
46
38
  * `answers`; grouped checkboxes store a selected-label array in
47
39
  * `multiAnswers`. Keeping the registries separate preserves the existing
@@ -57,7 +49,7 @@ export interface AnswersState {
57
49
  fields: Record<string, string>;
58
50
  /** Field ids whose value must never be persisted or collected (secrets). */
59
51
  secretFields: ReadonlySet<string>;
60
- meta: Record<string, QuestionMeta>;
52
+ registry: SubmissionRegistry;
61
53
  /** True after a local grading: questions are locked until 重新作答. */
62
54
  locked: boolean;
63
55
  /** Bumped by every reset; radios use it as their remount key. */
@@ -66,7 +58,6 @@ export interface AnswersState {
66
58
  setMultiAnswer: (group: string, choice: string, checked: boolean) => void;
67
59
  setField: (id: string, value: string) => void;
68
60
  registerSecretField: (id: string) => void;
69
- registerMeta: (group: string, meta: QuestionMeta) => void;
70
61
  clear: () => void;
71
62
  setLocked: (locked: boolean) => void;
72
63
  }
@@ -372,17 +372,13 @@ export interface GenuiRadio {
372
372
  /** v2.6: per-question explanation shown after local grading. */
373
373
  explanation?: string;
374
374
  }
375
- /** Submit node: collects sibling `radio` and grouped `checkbox` answers in
376
- * this block. LOCAL-FIRST grading still applies to radio-only question scopes;
377
- * aggregation scopes can emit strings for radio groups and string arrays for
378
- * checkbox groups in the same `answers` object. */
375
+ /** 提交节点收集当前 block 的答案和字段值;纯 radio 范围可在本地判卷。 */
379
376
  export interface GenuiSubmit {
380
377
  type: 'submit';
381
378
  label: string;
382
379
  /**
383
- * Optional action name. Local-first: when the in-scope questions can be
384
- * graded entirely locally, no action is needed. Aggregation submits require
385
- * an action and emit `{type:'submit', answers, total, answered}`.
380
+ * 可选 action 名称。纯 radio 范围满足本地判卷条件时无需 action;其他提交
381
+ * 通过 action 发送 `answers`、可选的 `fields`、`total` 和 `answered`。
386
382
  */
387
383
  action?: string;
388
384
  /**
@@ -392,10 +388,11 @@ export interface GenuiSubmit {
392
388
  */
393
389
  resetAction?: string;
394
390
  /**
395
- * Optional explicit group list to wait for; when absent the submit enables
396
- * once at least one grouped answer (or filled field) exists. When present
397
- * it stays disabled until EVERY listed radio/checkbox group has a non-empty
398
- * recorded answer (the hint shows the progress).
391
+ * `groups` 引用当前 block 中的 submission member key,来源为 radio.group、
392
+ * checkbox.group 以及 input/textarea/select/slider.id。radio 需已选择,
393
+ * checkbox 需至少选择一项,普通 field 需在 trim 后非空。`groups` 控制提交
394
+ * 所需成员与完成进度;payload 仍收集当前 block 中已填写的表单状态。
395
+ * 未设置时,只要存在至少一个已完成成员即可提交。
399
396
  */
400
397
  groups?: string[];
401
398
  }
@@ -0,0 +1,51 @@
1
+ import type { GenuiSpec } from './spec.ts';
2
+ export type SubmissionMember = RadioSubmissionMember | CheckboxSubmissionMember | FieldSubmissionMember;
3
+ export interface RadioSubmissionMember {
4
+ kind: 'radio';
5
+ key: string;
6
+ label: string;
7
+ options: string[];
8
+ answer?: number | string;
9
+ explanation?: string;
10
+ }
11
+ export interface CheckboxSubmissionMember {
12
+ kind: 'checkbox';
13
+ key: string;
14
+ }
15
+ export interface FieldSubmissionMember {
16
+ kind: 'field';
17
+ key: string;
18
+ fieldType: 'input' | 'textarea' | 'select' | 'slider';
19
+ secret: boolean;
20
+ }
21
+ export interface SubmissionRegistry {
22
+ members: ReadonlyMap<string, SubmissionMember>;
23
+ }
24
+ export interface SubmitInteractionState {
25
+ answers: Record<string, string>;
26
+ multiAnswers: Record<string, string[]>;
27
+ fields: Record<string, string>;
28
+ secretFields: ReadonlySet<string>;
29
+ }
30
+ export interface ResolvedSubmitState {
31
+ scope: SubmissionMember[];
32
+ answered: number;
33
+ total: number;
34
+ localGradeEligible: boolean;
35
+ hasOutOfScopePayload: boolean;
36
+ }
37
+ /** 从 spec 建立成员表,并诊断 submission key 和 submit.groups。 */
38
+ export declare function analyzeSubmissionRegistry(spec: GenuiSpec): {
39
+ registry: SubmissionRegistry;
40
+ diagnostics: string[];
41
+ };
42
+ /** 编译供运行时读取的静态 submission 成员表。 */
43
+ export declare function compileSubmissionRegistry(spec: GenuiSpec): SubmissionRegistry;
44
+ /** 根据当前交互状态判断一个 submission member 是否完成。 */
45
+ export declare function isSubmissionMemberAnswered(member: SubmissionMember, state: SubmitInteractionState): boolean;
46
+ /** 计算 submit 进度和纯本地判卷条件。 */
47
+ export declare function resolveSubmitState({ registry, groups, state }: {
48
+ registry: SubmissionRegistry;
49
+ groups?: string[];
50
+ state: SubmitInteractionState;
51
+ }): ResolvedSubmitState;
@@ -0,0 +1,20 @@
1
+ /** 保存 repair 所需的原始 table 字段。 */
2
+ export interface TableDetailSource {
3
+ columns?: unknown;
4
+ rows?: unknown;
5
+ data?: unknown;
6
+ types?: unknown;
7
+ }
8
+ /** 保存 repair 后的 table 列和行。 */
9
+ export interface PreparedTableRows {
10
+ columns: string[];
11
+ rows: Array<Array<string | number>>;
12
+ }
13
+ /** 使用 table repair 相同的转换规则处理列和行。 */
14
+ export declare function prepareTableRows(table: TableDetailSource): PreparedTableRows | null;
15
+ /** 返回 repair 后可显示详情的 table 行。 */
16
+ export declare function tableRowsForDetails<Row = unknown>(table: TableDetailSource): Row[];
17
+ /** 按照 table renderer 的规则识别分组标题行。 */
18
+ export declare function isTableGroupHeaderRow(row: unknown, types: unknown): boolean;
19
+ /** 判断详情索引对应的行是否可展开并显示。 */
20
+ export declare function isTableDetailReachable(table: TableDetailSource, rowIndex: number): boolean;
@@ -0,0 +1,3 @@
1
+ import type { GenuiNode, GenuiSpec } from './spec.ts';
2
+ /** 遍历完整 GenUI 组件树,并提供每个组件在 spec 中的路径。 */
3
+ export declare function walkGenuiNodes(spec: GenuiSpec, visitor: (node: GenuiNode, path: string) => void): void;
@@ -8,8 +8,10 @@
8
8
  *
9
9
  * The loop is deliberately narrow, matching the contract agreed on the issue:
10
10
  * - **默认开启。** 插件配置中的 `fenceFeedback: false` 可以关闭回合转向。
11
- * - **Bounded.** At most one correction per turn AND at most one per fence
12
- * body per process, so a correction that is itself wrong cannot loop.
11
+ * - **修正上限。** 每个 turn 最多发送两条 correction;render failure 和 delivery reminder 共用上限,
12
+ * 每个 fence body 在当前回合中最多修正一次,新回合可重新修正。
13
+ * - **仅含 reasoning 的恢复。** 已验证的 GenUI 回合若只以 reasoning block 结束,使用宿主的
14
+ * `EMPTY_RESPONSE` retry policy。
13
15
  * - **Never for subagents.** A child session's fence belongs to a parent reply.
14
16
  * - **Exact fence matching.** Only an info string of exactly `dsh-ui` opens a
15
17
  * fence, so ` ```dsh-ui-dark `, indented prose, or a mention of the name is
@@ -28,6 +30,11 @@ import type { UserMessage } from '@deepseek-ai/dsh-session';
28
30
  export declare const FEEDBACK_PLUGIN_NAME = "@changfenhuang/dsh-genui";
29
31
  /** Source kind persisted by this plugin in Session format v4. */
30
32
  export declare const FEEDBACK_SOURCE_KIND: "plugin:@changfenhuang/dsh-genui";
33
+ /**
34
+ * 同一个 turn 内 fence-feedback correction 的共享上限。
35
+ * Render failure 和 nothing-delivered reminder 共用该 budget;各自的 ledger 独立阻止重复修正。
36
+ */
37
+ export declare const MAX_CORRECTIONS_PER_TURN = 2;
31
38
  /** One ```dsh-ui fence found in an assistant reply. */
32
39
  export interface ExtractedFence {
33
40
  /** Raw body between the fences (no delimiters). */
@@ -82,31 +89,63 @@ export declare function fenceCorrectionText(failures: readonly FenceFailure[]):
82
89
  export declare function createFeedbackMessage(text: string, sessionFormatVersion: number): UserMessage;
83
90
  /** What the pure planner needs to decide whether a correction may be sent. */
84
91
  export interface FenceFeedbackPlanInput {
92
+ /** Latest assistant reply text of the current turn. */
85
93
  readonly text: string;
86
94
  readonly turn: number;
87
- readonly lastCorrectedTurn: number | undefined;
88
- readonly corrected: ReadonlySet<string>;
95
+ /** Fence bodies already corrected for a render failure in the current turn. */
96
+ readonly correctedSpec: ReadonlySet<string>;
97
+ /** Turns already given the delivery reminder. */
98
+ readonly deliveryRemindedTurns?: ReadonlySet<number> | undefined;
99
+ /** A `validate_dsh_ui` call happened this turn (formal GenUI signal). */
100
+ readonly validatedThisTurn?: boolean | undefined;
101
+ /** The turn already delivered a body or a successful `render_ui` result. */
102
+ readonly deliveredThisTurn?: boolean | undefined;
89
103
  readonly aborted: boolean;
104
+ /** Corrections already steered in this turn (shared hard cap). */
105
+ readonly correctionsThisTurn?: number | undefined;
106
+ /** Turn {@link correctionsThisTurn} counts (stale counts are ignored). */
107
+ readonly correctionsTurn?: number | undefined;
90
108
  }
91
109
  /** A correction the caller must account for before steering. */
92
110
  export interface FenceFeedbackPlan {
93
111
  readonly text: string;
94
112
  readonly fingerprints: readonly string[];
95
113
  readonly turn: number;
114
+ /**
115
+ * `render` corrects a fence that failed to resolve; `delivery` reminds the
116
+ * model that the turn produced nothing formal. They share the per-turn budget
117
+ * but keep separate ledgers.
118
+ */
119
+ readonly kind: 'render' | 'delivery';
96
120
  }
97
121
  /**
98
- * Decide whether this turn boundary should steer a fence correction — the pure
99
- * core of the loop, so every bound (one per turn, one per fence, cancellation)
100
- * is testable without a host.
122
+ * Decide whether this turn boundary should steer a correction — the pure core of
123
+ * the loop, so every bound (per-turn cap, per-fence ledger, cancellation) is
124
+ * testable without a host.
101
125
  *
102
- * @param input - reply text, turn identity, and the session's accounting.
126
+ * The decision is driven by FORMAL events only: fences in the reply body, a
127
+ * `validate_dsh_ui` call, a delivered body text or a successful `render_ui`
128
+ * result. The reasoning block is
129
+ * never read here — a draft inside the thinking block is not proof that the model
130
+ * chose to deliver it, so it must not change any decision.
131
+ *
132
+ * @param input - reply text, turn identity, formal signals, and the accounting.
103
133
  * @returns the correction to send, or null when the loop must stay silent.
104
134
  */
105
135
  export declare function planFenceFeedback(input: FenceFeedbackPlanInput): FenceFeedbackPlan | null;
106
136
  /**
107
- * 根据插件配置启用围栏反馈流程。
137
+ * Correction for a validated GenUI turn that reached the boundary without any
138
+ * formal delivery.
139
+ *
140
+ * @param turn - turn that reached the boundary.
141
+ * @param attempt - correction number within the shared turn budget.
142
+ * @returns the message text to steer into the running turn.
143
+ */
144
+ export declare function missingBodyCorrectionText(turn: number, attempt?: number): string;
145
+ /**
146
+ * 注册 GenUI 回合跟踪、宿主重试判定和可选的围栏修正流程。
108
147
  *
109
- * @param ctx - the host context.
110
- * @param enabled - the plugin config flag; the loop is inert when false.
148
+ * @param ctx - 宿主 Context。
149
+ * @param enabled - 是否启用同回合围栏修正。
111
150
  */
112
151
  export declare function installFenceFeedback(ctx: Context, enabled: boolean): void;
@@ -13,7 +13,7 @@ import { Context } from '@deepseek-ai/cordis';
13
13
  * Deliberately slim: the `genui` skill carries the full component→field
14
14
  * mapping; this section keeps only the contract that must always be
15
15
  * present (fence syntax, type whitelist, and critical behavioral rules). */
16
- export declare const GENUI_SECTION_TEXT = "You can render interactive UI components INSIDE your reply \u2014 between paragraphs \u2014 by emitting a fenced block with the language tag `dsh-ui` containing a JSON spec:\n\n```dsh-ui\n{\"title\":\"<user-language text>\",\"gap\":14,\"items\":[...]}\n```\n\nAllowed `type` values; the `genui` skill, when available, carries the full content\u2192component mapping and per-component field details:\n\n- \u5E03\u5C40: text \u00B7 row \u00B7 col \u00B7 grid \u00B7 card \u00B7 divider \u00B7 spacer \u00B7 hero\uFF08\u5C01\u9762\u5757\uFF0C\u4E00\u6761\u56DE\u7B54\u6700\u591A\u4E00\u4E2A\uFF09\n- \u5C55\u793A: badge \u00B7 stat \u00B7 progress \u00B7 list \u00B7 table \u00B7 keyvalue \u00B7 timeline \u00B7 file-tree \u00B7 breadcrumb \u00B7 callout \u00B7 steps \u00B7 diff \u00B7 json \u00B7 code \u00B7 copy \u00B7 avatar \u00B7 audio \u00B7 video\n- \u56FE\u8868: chart {\"kind\":\"bars|line|donut\",\"data\":[{\"label\":\"...\",\"value\":n}],\"series\":[{\"label\":\"...\",\"data\":[...]}]?,\"horizontal\":true?,\"stacked\":true?}\uFF08series\uFF1Abars \u5206\u7EC4/\u5806\u53E0 / line \u591A\u5E8F\u5217\uFF1Bhorizontal \u6A2A\u5411\u67F1\uFF09 \u00B7 echart (preset \u540D\u6216 option \u76F4\u901A\uFF0C\u9884\u8BBE\u6E05\u5355\u89C1 skill) \u00B7 plot (\u51FD\u6570\u56FE)\n- \u4EA4\u4E92: button \u00B7 input \u00B7 textarea \u00B7 select \u00B7 checkbox \u00B7 switch \u00B7 slider \u00B7 radio \u00B7 submit \u00B7 quiz \u00B7 link \u00B7 tabs \u00B7 accordion\n- \u9AD8\u7EA7: mermaid (\u6D41\u7A0B\u56FE/\u65F6\u5E8F/\u7518\u7279/ER \u7B49\uFF0C\u5173\u952E\u5B57\u89C1 skill) \u00B7 diagram (\u67B6\u6784/\u6D41\u7A0B\u56FE\uFF0C27 \u79CD kind) \u00B7 scene3d (3D WebGL)\n\n**\u9ED8\u8BA4\u5C31\u8BE5\u51FA UI**\uFF1A\u51FA\u73B0\u4E0B\u5217\u60C5\u51B5\u81F3\u5C11\u51FA\u4E00\u4E2A\u56F4\u680F\uFF1A\n- \u22653 \u6761\u5E76\u5217\u8981\u70B9 \u2192 `list`\uFF1B\u6570\u5B57\u5BF9\u6BD4 \u2192 `table`\uFF1B\u6307\u6807/\u8FDB\u5EA6/\u72B6\u6001 \u2192 `stat`/`progress`/`badge`\n- \u6B65\u9AA4/\u65F6\u95F4\u7EBF \u2192 `steps`/`timeline`/`mermaid`\uFF1B\u67B6\u6784/\u6D41\u7A0B \u2192 `diagram` \u6216 `mermaid`\uFF1B\u98CE\u9669/\u7ED3\u8BBA \u2192 `callout`\uFF1B\u4EE3\u7801/\u6539\u52A8 \u2192 `code`/`diff`/`json`\n- \u884C\u5185\u5BCC\u6587\u672C\uFF1A\u652F\u6301\u516C\u5F0F\u3001`code`\u3001\u52A0\u7C97\u3001\u9AD8\u4EAE\u3001\u94FE\u63A5\uFF1B\u7981\u7528 Markdown table / fenced code\uFF0C\u6539\u7528 table / code / diff / json\u3002\n- \u9ED8\u8BA4\u65E0\u5361\uFF1A\u6309\u89E6\u53D1\u6761\u4EF6\u4F7F\u7528\u7EC4\u4EF6\uFF0C\u6BCF\u4E2A\u7EC4\u4EF6\u627F\u8F7D\u4E0D\u540C\u4FE1\u606F\uFF1B\u5361\u7247\u53EA\u7528\u4E8E\u5E76\u6392\u9879\u4E0E\u6570\u636E\u5BF9\u8C61\uFF0C\u5355\u6BB5\u6587\u5B57\u7528\u6807\u9898\u3001\u6B63\u6587\u4E0E\u95F4\u8DDD\u3002\n\n**\u53D1\u56DE\u7B54\u524D\u6700\u540E\u81EA\u68C0\u4E00\u6B21**\uFF1A\u8FD9\u6BB5\u5185\u5BB9\u91CC\u6709\u6CA1\u6709 \u22653 \u6761\u5E76\u5217\u8981\u70B9\u3001\u4EFB\u4F55\u5BF9\u6BD4\u3001\u4EFB\u4F55\u6570\u5B57/\u6307\u6807\u3001\u4EFB\u4F55\u6B65\u9AA4\u6216\u6D41\u7A0B\uFF1F\u6709\u5C31\u5148\u8F6C\u6210\u7EC4\u4EF6\u518D\u5F00\u53E3\u3002**\u72B6\u6001\u6C47\u62A5\u3001\u8FDB\u5EA6\u8BF4\u660E\u3001\u63D0\u4EA4\u4E0E\u6539\u52A8\u6E05\u5355\u540C\u6837\u7B97**\u3002\n- \u8D8B\u52BF/\u5360\u6BD4 \u2192 `chart`\uFF08\u22648 \u70B9\uFF09\u6216 `echart`\uFF08\u591A\u5E8F\u5217/\u8981\u4EA4\u4E92\u65F6\uFF09\uFF1B\u914D\u8272\u9ED8\u8BA4\u8DDF\u968F\u4E3B\u9898\uFF0C\u53EA\u6709\u8BED\u4E49\u9700\u8981\u65F6\u624D\u7528 `palette`/`card.accent`\uFF1Bgrid \u5B50\u8282\u70B9\u7528 `\"span\":2` \u8DE8\u5217\u505A\u5BBD\u7A84\u6DF7\u6392\uFF1B\u6570\u636E\u591A\u65F6\u7ED9 `table`/`chart`/`list` \u914D\u4E00\u4E2A `input`(id) + `filter` \u7ED1\u5B9A\uFF0C\u8BFB\u8005\u80FD\u5C31\u5730\u7B5B\u9009\u3002\n\n**\u5B57\u6BB5\u901F\u67E5**\uFF08\u5B8C\u6574\u89C1 genui skill\uFF09\uFF1A`stat` `{\"label\",\"value\",\"delta\"?}` \u00B7 `table` `{\"columns\":[...],\"rows\":[[...]],\"types\"?,\"details\"?,\"filter\"?,\"export\"?}` \u00B7 `progress` `{\"value\":0-100,\"label\"?,\"variant\"?,\"target\"?}` \u00B7 `keyvalue` `{\"pairs\":[{\"key\",\"value\"}]}` \u00B7 `steps` `{\"steps\":[{\"title\",\"desc\"?}]}` \u00B7 `file-tree` `{\"items\":[{\"name\",\"type\":\"file|dir\",\"children\"?}]}` \u00B7 `callout` `{\"content\",\"tone\"?,\"title\"?}`\n\n**\u5B57\u6BB5\u540D\u5199\u9519 = \u8BE5\u7EC4\u4EF6\u88AB\u4E22\u5F03**\uFF08\u5176\u4F59\u7EC4\u4EF6\u7167\u5E38\u6E32\u67D3\uFF09\uFF1A`callout` \u6B63\u6587\u662F `content` \u4E0D\u662F text/desc\uFF1B`table` \u8981 `columns`+`rows` \u4E0D\u662F items\uFF1B`keyvalue` \u8BB0\u5F55\u662F `{key,value}` \u4E0D\u662F `{label,value}`\uFF1B`file-tree` \u8BB0\u5F55\u662F `{name,type}` \u4E0D\u662F `{label}`\uFF1Bcallout tone \u662F info/success/warning/error\uFF08\u65E0 danger\uFF09\u3002\u4E0D\u786E\u5B9A\u5C31\u8C03 `validate_dsh_ui`\u3002\n\nRules:\n- LANGUAGE: reply+UI=conversation language; schema fixed. NEVER infer it from prompt/skill/examples/tools. Replace `<user-language ...>`; never emit these placeholders literally.\n- JSON \u4E25\u683C\uFF1A\u574F\u7EC4\u4EF6\u88AB\u4E22\u5F03\uFF0C\u574F\u56F4\u680F\u53D8\u4EE3\u7801\u5757\uFF1B\u22653 \u8282\u70B9\u6216\u542B table \u65F6\u8C03 validate_dsh_ui\uFF0C\u6309\u8BCA\u65AD\u4FEE\u6539\u5E76\u91CD\u9A8C\uFF1B\u5C0F\u56F4\u680F\u5B57\u6BB5\u5B58\u7591\u4E5F\u5148\u9A8C\u8BC1\u3002\n- warning=block_markdown\uFF1A\u6309 replacement \u6539\u5199\u5E76\u91CD\u9A8C\u3002\n- \u89C4\u6A21: \u2264200 \u8282\u70B9\u3001\u5D4C\u5957\u22648 \u5C42\uFF08\u8D85\u51FA\u88AB\u622A\u65AD\uFF09\uFF1B\u4E00\u6761\u56DE\u7B54 3\u20138 \u4E2A\u7EC4\u4EF6\uFF0C\u4E00\u4E2A\u4E3B\u9898\u4E00\u4E2A\u4E3B\u7EC4\u4EF6\uFF1B3D mesh 1\u20135\uFF1Bplot \u7ED9\u5408\u7406 xMin/xMax\u3002\n- LOCAL-FIRST + actions: UI \u80FD\u81EA\u5DF1\u505A\u7684\u72B6\u6001\u53D8\u5316\uFF08\u5224\u5377\u3001\u5224\u9898\u3001\u91CD\u7F6E\u3001\u5C55\u5F00\u3001\u9009\u4E2D\uFF09\u5C31\u5730\u5B8C\u6210\uFF0C\u96F6\u5F80\u8FD4\uFF1Baction \u53EA\u7528\u4E8E\u5FC5\u987B\u6A21\u578B\u53C2\u4E0E\u7684\u4E8B\u3002\u4EA4\u4E92\u7EC4\u4EF6\u5E26 \"action\":\"name\"\uFF0C\u4EA4\u4E92\u4EE5 [genui-action] name + \u7EC4\u4EF6\u6570\u636E\u56DE\u4F20\uFF0C\u5C4A\u65F6\u91CD\u6E32\u67D3\u66F4\u65B0 UI\uFF1B\u65E0 action \u7684\u6309\u94AE\u7981\u7528\u3002\n- Durable state: \u4EA4\u4E92\u72B6\u6001\u6309\u300C\u4F1A\u8BDD+\u5185\u5BB9\u6307\u7EB9\u300D\u6301\u4E45\u5316\u2014\u2014\u5237\u65B0/\u91CD\u653E\u6062\u590D\uFF1B\u91CD\u6E32\u67D3\u76F8\u540C\u5185\u5BB9\u4FDD\u7559\uFF0C\u65B0\u5185\u5BB9\u91CD\u7F6E\u3002\n- \u5377\u5B50\u6A21\u5F0F: \u6BCF\u9898\u4E00\u4E2A radio\uFF08group+answer+explanation\uFF09+ \u4E00\u4E2A submit\uFF08groups \u5168\u5217\uFF09\uFF0C\u672C\u5730\u5224\u5206\u3002\n- Secrets ban: \u4E0D\u7D22\u53D6\u5BC6\u7801\u3001API Key\u3001Token\u3001\u6062\u590D\u7801\uFF1B\u9700\u8981\u65F6\u62D2\u7EDD\u5E76\u89E3\u91CA\u3002\n- Tool channel: render_ui \u5DE5\u5177\u628A\u540C\u4E00 spec \u6E32\u67D3\u4E3A\u5DE5\u5177\u884C\u5361\u7247\uFF08\u4EA4\u4ED8\u7269\u578B\u754C\u9762\u7528\uFF09\uFF1B\u56F4\u680F\u7528\u4E8E\u56DE\u7B54\u5185\u8054 UI\u3002\n- Panel: \"panel\":true \u53EA\u6E32\u67D3\u8FDB\u4F1A\u8BDD\u9762\u677F dock \u5E76\u539F\u5730\u66F4\u65B0\uFF1B\"append\":true \u8FFD\u52A0\u5408\u5E76\uFF08\u540C\u6807\u7B7E tabs \u8FFD\u52A0/\u65B0\u6807\u7B7E\u52A0\u5165/\u5C3E\u90E8\u8FFD\u52A0\uFF09\uFF1B\u4E0A\u9650 200 \u8282\u70B9/200 \u6B21\u8FFD\u52A0\uFF0C\u6EE1\u4E86\u53D1 replace \u91CD\u5EFA\u3002\u9762\u677F\u7EC4\u4EF6\u6765\u7684 [genui-action] \u53EA\u56DE\u4E00\u4E2A panel:true \u56F4\u680F + \u81F3\u591A\u4E00\u884C 10 \u5B57\u5185\u786E\u8BA4\uFF0C\u4E0D\u89E3\u91CA\u3001\u4E0D\u7528\u666E\u901A\u56F4\u680F\u3002";
16
+ export declare const GENUI_SECTION_TEXT = "You can render interactive UI components INSIDE your reply \u2014 between paragraphs \u2014 by emitting a fenced block with the language tag `dsh-ui` containing a JSON spec:\n\n```dsh-ui\n{\"title\":\"<user-language text>\",\"gap\":14,\"items\":[...]}\n```\n\nAllowed `type` values; the `genui` skill, when available, carries the full content\u2192component mapping and per-component field details:\n\n- \u5E03\u5C40: text \u00B7 row \u00B7 col \u00B7 grid \u00B7 card \u00B7 divider \u00B7 spacer \u00B7 hero\uFF08\u5C01\u9762\u5757\uFF0C\u4E00\u6761\u56DE\u7B54\u6700\u591A\u4E00\u4E2A\uFF09\n- \u5C55\u793A: badge \u00B7 stat \u00B7 progress \u00B7 list \u00B7 table \u00B7 keyvalue \u00B7 timeline \u00B7 file-tree \u00B7 breadcrumb \u00B7 callout \u00B7 steps \u00B7 diff \u00B7 json \u00B7 code \u00B7 copy \u00B7 avatar \u00B7 audio \u00B7 video\n- \u56FE\u8868: chart {\"kind\":\"bars|line|donut\",\"data\":[{\"label\":\"...\",\"value\":n}],\"series\":[{\"label\":\"...\",\"data\":[...]}]?,\"horizontal\":true?,\"stacked\":true?}\uFF08series\uFF1Abars \u5206\u7EC4/\u5806\u53E0 / line \u591A\u5E8F\u5217\uFF1Bhorizontal \u6A2A\u5411\u67F1\uFF09 \u00B7 echart (preset \u540D\u6216 option \u76F4\u901A\uFF0C\u9884\u8BBE\u6E05\u5355\u89C1 skill) \u00B7 plot (\u51FD\u6570\u56FE)\n- \u4EA4\u4E92: button \u00B7 input \u00B7 textarea \u00B7 select \u00B7 checkbox \u00B7 switch \u00B7 slider \u00B7 radio \u00B7 submit \u00B7 quiz \u00B7 link \u00B7 tabs \u00B7 accordion\n- \u9AD8\u7EA7: mermaid (\u6D41\u7A0B\u56FE/\u65F6\u5E8F/\u7518\u7279/ER \u7B49\uFF0C\u5173\u952E\u5B57\u89C1 skill) \u00B7 diagram (\u67B6\u6784/\u6D41\u7A0B\u56FE\uFF0C27 \u79CD kind) \u00B7 scene3d (3D WebGL)\n\n**\u9ED8\u8BA4\u5C31\u8BE5\u51FA UI**\uFF1A\u51FA\u73B0\u4E0B\u5217\u60C5\u51B5\u81F3\u5C11\u51FA\u4E00\u4E2A\u56F4\u680F\uFF1A\n- \u22653 \u6761\u5E76\u5217\u8981\u70B9 \u2192 `list`\uFF1B\u6570\u5B57\u5BF9\u6BD4 \u2192 `table`\uFF1B\u6307\u6807/\u8FDB\u5EA6/\u72B6\u6001 \u2192 `stat`/`progress`/`badge`\n- \u6B65\u9AA4/\u65F6\u95F4\u7EBF \u2192 `steps`/`timeline`/`mermaid`\uFF1B\u67B6\u6784/\u6D41\u7A0B \u2192 `diagram` \u6216 `mermaid`\uFF1B\u98CE\u9669/\u7ED3\u8BBA \u2192 `callout`\uFF1B\u4EE3\u7801/\u6539\u52A8 \u2192 `code`/`diff`/`json`\n- \u884C\u5185\u5BCC\u6587\u672C\uFF1A\u652F\u6301\u516C\u5F0F\u3001`code`\u3001\u52A0\u7C97\u3001\u9AD8\u4EAE\u3001\u94FE\u63A5\uFF1B\u7981\u7528 Markdown table / fenced code\uFF0C\u6539\u7528 table / code / diff / json\u3002\n- \u9ED8\u8BA4\u65E0\u5361\uFF1A\u6309\u89E6\u53D1\u6761\u4EF6\u4F7F\u7528\u7EC4\u4EF6\uFF0C\u6BCF\u4E2A\u7EC4\u4EF6\u627F\u8F7D\u4E0D\u540C\u4FE1\u606F\uFF1B\u5361\u7247\u53EA\u7528\u4E8E\u5E76\u6392\u9879\u4E0E\u6570\u636E\u5BF9\u8C61\uFF0C\u5355\u6BB5\u6587\u5B57\u7528\u6807\u9898\u3001\u6B63\u6587\u4E0E\u95F4\u8DDD\u3002\n\n**\u53D1\u56DE\u7B54\u524D\u6700\u540E\u81EA\u68C0\u4E00\u6B21**\uFF1A\u8FD9\u6BB5\u5185\u5BB9\u91CC\u6709\u6CA1\u6709 \u22653 \u6761\u5E76\u5217\u8981\u70B9\u3001\u4EFB\u4F55\u5BF9\u6BD4\u3001\u4EFB\u4F55\u6570\u5B57/\u6307\u6807\u3001\u4EFB\u4F55\u6B65\u9AA4\u6216\u6D41\u7A0B\uFF1F\u6709\u5C31\u5148\u8F6C\u6210\u7EC4\u4EF6\u518D\u5F00\u53E3\u3002**\u72B6\u6001\u6C47\u62A5\u3001\u8FDB\u5EA6\u8BF4\u660E\u3001\u63D0\u4EA4\u4E0E\u6539\u52A8\u6E05\u5355\u540C\u6837\u7B97**\u3002\n- \u8D8B\u52BF/\u5360\u6BD4 \u2192 `chart`\uFF08\u22648 \u70B9\uFF09\u6216 `echart`\uFF08\u591A\u5E8F\u5217/\u8981\u4EA4\u4E92\u65F6\uFF09\uFF1B\u914D\u8272\u9ED8\u8BA4\u8DDF\u968F\u4E3B\u9898\uFF0C\u53EA\u6709\u8BED\u4E49\u9700\u8981\u65F6\u624D\u7528 `palette`/`card.accent`\uFF1Bgrid \u5B50\u8282\u70B9\u7528 `\"span\":2` \u8DE8\u5217\u505A\u5BBD\u7A84\u6DF7\u6392\uFF1B\u6570\u636E\u591A\u65F6\u7ED9 `table`/`chart`/`list` \u914D\u4E00\u4E2A `input`(id) + `filter` \u7ED1\u5B9A\uFF0C\u8BFB\u8005\u80FD\u5C31\u5730\u7B5B\u9009\u3002\n\n**\u5B57\u6BB5\u901F\u67E5**\uFF08\u5B8C\u6574\u89C1 genui skill\uFF09\uFF1A`stat` `{\"label\",\"value\",\"delta\"?}` \u00B7 `table` `{\"columns\":[...],\"rows\":[[...]],\"types\"?,\"details\"?,\"filter\"?,\"export\"?}` \u00B7 `progress` `{\"value\":0-100,\"label\"?,\"variant\"?,\"target\"?}` \u00B7 `keyvalue` `{\"pairs\":[{\"key\",\"value\"}]}` \u00B7 `steps` `{\"steps\":[{\"title\",\"desc\"?}]}` \u00B7 `file-tree` `{\"items\":[{\"name\",\"type\":\"file|dir\",\"children\"?}]}` \u00B7 `callout` `{\"content\",\"tone\"?,\"title\"?}`\n\n**\u5B57\u6BB5\u540D\u5199\u9519 = \u8BE5\u7EC4\u4EF6\u88AB\u4E22\u5F03**\uFF08\u5176\u4F59\u7EC4\u4EF6\u7167\u5E38\u6E32\u67D3\uFF09\uFF1A`callout` \u6B63\u6587\u662F `content` \u4E0D\u662F text/desc\uFF1B`table` \u8981 `columns`+`rows` \u4E0D\u662F items\uFF1B`keyvalue` \u8BB0\u5F55\u662F `{key,value}` \u4E0D\u662F `{label,value}`\uFF1B`file-tree` \u8BB0\u5F55\u662F `{name,type}` \u4E0D\u662F `{label}`\uFF1Bcallout tone \u662F info/success/warning/error\uFF08\u65E0 danger\uFF09\u3002\u4E0D\u786E\u5B9A\u5C31\u8C03 `validate_dsh_ui`\u3002\n\nRules:\n- LANGUAGE: reply+UI=conversation language; schema fixed. NEVER infer it from prompt/skill/examples/tools. Replace `<user-language ...>`; never emit these placeholders literally.\n- JSON \u4E25\u683C\uFF1A\u574F\u7EC4\u4EF6\u88AB\u4E22\u5F03\uFF0C\u574F\u56F4\u680F\u53D8\u4EE3\u7801\u5757\uFF1B\u22653 \u8282\u70B9\u6216\u542B table \u65F6\u8C03 validate_dsh_ui\uFF0C\u6309\u8BCA\u65AD\u4FEE\u6539\u5E76\u91CD\u9A8C\uFF1B\u5C0F\u56F4\u680F\u5B57\u6BB5\u5B58\u7591\u4E5F\u5148\u9A8C\u8BC1\u3002\n- warning=block_markdown\uFF1A\u6309 replacement \u6539\u5199\u5E76\u91CD\u9A8C\u3002\n- \u89C4\u6A21: \u2264200 \u8282\u70B9\u3001\u5D4C\u5957\u22648 \u5C42\uFF08\u8D85\u51FA\u88AB\u622A\u65AD\uFF09\uFF1B\u4E00\u6761\u56DE\u7B54 3\u20138 \u4E2A\u7EC4\u4EF6\uFF0C\u4E00\u4E2A\u4E3B\u9898\u4E00\u4E2A\u4E3B\u7EC4\u4EF6\uFF1B3D mesh 1\u20135\uFF1Bplot \u7ED9\u5408\u7406 xMin/xMax\u3002\n- LOCAL-FIRST + actions: UI \u80FD\u81EA\u5DF1\u505A\u7684\u72B6\u6001\u53D8\u5316\uFF08\u5224\u5377\u3001\u5224\u9898\u3001\u91CD\u7F6E\u3001\u5C55\u5F00\u3001\u9009\u4E2D\uFF09\u5C31\u5730\u5B8C\u6210\uFF0C\u96F6\u5F80\u8FD4\uFF1Baction \u53EA\u7528\u4E8E\u5FC5\u987B\u6A21\u578B\u53C2\u4E0E\u7684\u4E8B\u3002\u4EA4\u4E92\u4EE5 [genui-action] name + \u7EC4\u4EF6\u6570\u636E\u56DE\u4F20\uFF0C\u5C4A\u65F6\u91CD\u6E32\u67D3\u66F4\u65B0 UI\uFF1B\u65E0 action \u7684\u6309\u94AE\u7981\u7528\u3002\n- Durable state: \u4EA4\u4E92\u72B6\u6001\u6309\u300C\u4F1A\u8BDD+\u5185\u5BB9\u6307\u7EB9\u300D\u6301\u4E45\u5316\u2014\u2014\u5237\u65B0/\u91CD\u653E\u6062\u590D\uFF1B\u76F8\u540C\u5185\u5BB9\u4FDD\u7559\uFF0C\u65B0\u5185\u5BB9\u91CD\u7F6E\u3002\n- \u5377\u5B50\u6A21\u5F0F: \u6BCF\u9898\u4E00\u4E2A radio\uFF08group+answer+explanation\uFF09+ \u4E00\u4E2A submit\uFF08groups \u5168\u5217\uFF09\uFF0C\u672C\u5730\u5224\u5206\u3002\n- Secrets ban: \u4E0D\u7D22\u53D6\u5BC6\u7801\u3001API Key\u3001Token\u3001\u6062\u590D\u7801\uFF1B\u9700\u8981\u65F6\u62D2\u7EDD\u5E76\u89E3\u91CA\u3002\n- Tool channel: render_ui \u5DE5\u5177\u628A\u540C\u4E00 spec \u6E32\u67D3\u4E3A\u5DE5\u5177\u884C\u5361\u7247\uFF1B\u56F4\u680F\u7528\u4E8E\u56DE\u7B54\u5185\u8054 UI\u3002\n- \u56F4\u680F\u4F4D\u7F6E\uFF1A`dsh-ui` \u53EA\u5199\u5728**\u56DE\u7B54\u6B63\u6587**\uFF1B\u5199\u5728 reasoning/\u601D\u8003\u5757\u91CC\u4E0D\u6E32\u67D3\u3001\u7528\u6237\u770B\u4E0D\u5230\u2014\u2014\u601D\u8003\u91CC\u9A8C\u8BC1\u597D spec\uFF0C\u6B63\u6587\u518D\u8F93\u51FA\u540C\u4E00\u4EFD\u3002\n- Panel: \"panel\":true \u53EA\u6E32\u67D3\u8FDB\u4F1A\u8BDD\u9762\u677F dock \u5E76\u539F\u5730\u66F4\u65B0\uFF1B\"append\":true \u8FFD\u52A0\u5408\u5E76\uFF1B\u4E0A\u9650 200 \u8282\u70B9/200 \u6B21\u8FFD\u52A0\uFF0C\u6EE1\u4E86\u53D1 replace \u91CD\u5EFA\u3002\u9762\u677F\u6765\u7684 [genui-action] \u53EA\u56DE\u4E00\u4E2A panel:true \u56F4\u680F + \u81F3\u591A\u4E00\u884C 10 \u5B57\u5185\u786E\u8BA4\u3002";
17
17
  /**
18
18
  * Register the GenUI output-language section and the render_ui tool.
19
19
  * @param ctx - cordis context.
@@ -27,8 +27,8 @@ export declare const inject: string[];
27
27
  export interface GenuiPluginConfig {
28
28
  /**
29
29
  * 在最终 dsh-ui 围栏无法渲染的回合中请求模型发送一次修正版(issue #160)。
30
- * 默认开启,设置为 false 可以关闭。每回合和每个围栏正文最多请求一次,子代理不触发,
31
- * 每次请求会消耗模型步数。
30
+ * 默认开启,设置为 false 可以关闭同回合围栏修正。GenUI 回合的 reasoning-only 响应仍会交给宿主重试策略处理。
31
+ * 每回合和每个围栏正文最多请求一次,子代理不触发,每次修正请求会消耗模型步数。
32
32
  */
33
33
  fenceFeedback?: boolean;
34
34
  }
@@ -32,11 +32,10 @@ export declare function describeJsonFailure(raw: string): string | null;
32
32
  * fail near that quote with "Expected ',' or ']'...".
33
33
  * 2. Trailing commas before `}` / `]` or at end of input.
34
34
  *
35
- * The state-machine scan walks the raw body tracking string-open state:
36
- * - inside a string, a quote whose next non-space char is NOT one of `, ] } :`
37
- * (or end of input) cannot legally close the string → escape it as `\"`;
38
- * - a `,` whose next non-space char is `}` / `]` / end of input is a trailing
39
- * comma → drop it.
35
+ * A shared grammar-aware scan distinguishes object keys from values and
36
+ * checks the continuation after a potential string terminator. Ambiguous
37
+ * value quotes use bounded backtracking; trailing commas are dropped only
38
+ * outside strings.
40
39
  *
41
40
  * Returns `{ text, repairs }` on success, or null when nothing needed fixing
42
41
  * or the body still does not parse (callers fall through to tier-2 / banner).
@@ -45,6 +44,18 @@ export declare function repairFenceJson(raw: string): {
45
44
  text: string;
46
45
  repairs: number;
47
46
  } | null;
47
+ /**
48
+ * 取**第一个平衡根值**的文本(丢弃其后的杂字符);没有平衡根时返回 null。
49
+ *
50
+ * 与 {@link completeFenceJson} 里的前缀回退同源,但**只做裁剪、不做结构补全**:
51
+ * 内容识别用它来容忍「合法 JSON + 尾部泄漏文本」(真实样本:模型把自己的工具调用
52
+ * 模板泄漏在 JSON 之后,且围栏没闭合)。根值正好结束在末尾时返回 null —— 那种情况
53
+ * `JSON.parse` 本来就会成功。
54
+ *
55
+ * @param text - 候选正文。
56
+ * @returns 平衡根前缀;无可裁剪内容时 null。
57
+ */
58
+ export declare function trimToBalancedRoot(text: string): string | null;
48
59
  /**
49
60
  * Tier-2 repair — SETTLED MESSAGES ONLY (never while streaming): heals
50
61
  * structural incompleteness — missing closing quotes/brackets — by appending
@@ -54,8 +65,8 @@ export declare function repairFenceJson(raw: string): {
54
65
  * client uses the host-provided fence source; the validate tool is by
55
66
  * definition pre-emission), so a streaming half can never flash premature UI.
56
67
  *
57
- * ONE unified scan: the tier-1 fixes (quote escaping + trailing-comma drops)
58
- * are folded into the same pass, so bodies that combine BOTH defect classes
68
+ * One shared scan implementation folds the tier-1 fixes (quote escaping +
69
+ * trailing-comma drops) into structural completion, so bodies with BOTH defects
59
70
  * (a trailing comma AND a missing closer) heal in one shot — the old
60
71
  * two-phase chain lost tier-1's partial work when its whole-body parse
61
72
  * failed, and re-scanning the raw text could not compose the repairs.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@changfenhuang/dsh-genui",
3
3
  "description": "GenUI for DeepSeek Harness: interactive UI components rendered inline in assistant replies via the ```dsh-ui fence — layout, charts, plots, forms, quizzes, mermaid, 3D scenes, and an action event loop back to the model. Ships the fence-teaching host plugin, the browser renderer (client half), and the genui skill.",
4
- "version": "0.11.2",
4
+ "version": "0.11.4",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/plugin/index.d.ts",
@@ -75,24 +75,24 @@
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@deepseek-ai/cordis": "^4.0.1",
78
- "@deepseek-ai/dsh-agent": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
79
- "@deepseek-ai/dsh-api-session-controller": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
80
- "@deepseek-ai/dsh-client-store": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
81
- "@deepseek-ai/dsh-client-ui-chat": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
82
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
83
- "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
84
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
85
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
86
- "@deepseek-ai/dsh-client-ui-session": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
87
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
88
- "@deepseek-ai/dsh-client-ui-tool": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
89
- "@deepseek-ai/dsh-invariants": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
90
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
91
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
92
- "@deepseek-ai/dsh-skill": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
93
- "@deepseek-ai/dsh-system-prompt": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
94
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
95
- "@deepseek-ai/dsh-util-values": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
78
+ "@deepseek-ai/dsh-agent": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
79
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
80
+ "@deepseek-ai/dsh-client-store": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
81
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
82
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
83
+ "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
84
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
85
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
86
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
87
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
88
+ "@deepseek-ai/dsh-client-ui-tool": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
89
+ "@deepseek-ai/dsh-invariants": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
90
+ "@deepseek-ai/dsh-llm": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
91
+ "@deepseek-ai/dsh-session": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
92
+ "@deepseek-ai/dsh-skill": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
93
+ "@deepseek-ai/dsh-system-prompt": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
94
+ "@deepseek-ai/dsh-tools": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
95
+ "@deepseek-ai/dsh-util-values": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
96
96
  "react": "^18.0.0 || ^19.0.0"
97
97
  },
98
98
  "devDependencies": {
@@ -165,6 +165,7 @@
165
165
  "lightningcss": "^1.25.0",
166
166
  "mermaid": "11.16.0",
167
167
  "react-dom": "^18.3.1",
168
+ "semver": "^7.8.5",
168
169
  "three": "^0.180.0",
169
170
  "tsdown": "^0.22.2",
170
171
  "tsx": "^4.0.0",
@@ -191,7 +191,7 @@ body[data-ds-dark-theme] .panel {
191
191
 
192
192
  /* ---------- text ---------- */
193
193
 
194
- .text { color: var(--dsw-alias-label-primary); }
194
+ .text { color: var(--dsw-alias-label-primary); white-space: pre-line; }
195
195
  .text.h1 {
196
196
  font-size: var(--dsl-g-font-h1);
197
197
  font-weight: 800;
@@ -229,6 +229,7 @@ body[data-ds-dark-theme] .panel {
229
229
  cursor: pointer;
230
230
  transition: border-color 0.15s, filter 0.15s, background 0.15s, transform 0.1s;
231
231
  text-align: center;
232
+ white-space: pre-line;
232
233
  /* A button in a `col` must hug its label; only `full: true` stretches.
233
234
  (Default flex `align-items: stretch` used to make every stacked button a
234
235
  full-width slab.) */
@@ -628,6 +629,69 @@ img.mediaPlayer { aspect-ratio: 16 / 9; object-fit: cover; background: var(--dsl
628
629
  .tdDelta { font-weight: 600; }
629
630
  .tdDeltaUp { color: var(--dsw-alias-state-success-secondary, #1f9d63); }
630
631
  .tdDeltaDown { color: var(--dsw-alias-state-error-primary, #d94848); }
632
+ /* A cell carrying real line breaks opts out of the `nowrap` data voice.
633
+ Prose keeps `pre-line` (newlines paint, spaces collapse as everywhere else);
634
+ a code-shaped cell uses `pre-wrap` so leading indentation survives too.
635
+ Either way the newline characters stay in the DOM, which is what makes the
636
+ cell selectable and copyable as the multi-line snippet the model wrote. */
637
+ .table th.tdMultiline,
638
+ .table td.tdMultiline {
639
+ white-space: pre-line;
640
+ overflow-wrap: anywhere;
641
+ word-break: normal;
642
+ vertical-align: top;
643
+ }
644
+ .table th.tdCode,
645
+ .table td.tdCode {
646
+ white-space: pre-wrap;
647
+ overflow-wrap: anywhere;
648
+ word-break: normal;
649
+ vertical-align: top;
650
+ }
651
+ /* An inline code span keeps its own spacing too (a pasted snippet often lives
652
+ inside backticks), matching the block-level code surface. */
653
+ .inlineCode { white-space: pre-wrap; }
654
+ /* Rich-text owners must paint the real newlines kept by renderInline. Scope
655
+ this to prose/labels: table data keeps nowrap and code keeps pre-wrap. For
656
+ flex controls, target the label span so emphasis and plain text share one
657
+ line-formatting context (rather than becoming separate flex items). */
658
+ .calloutBody,
659
+ .liTitle,
660
+ .liDesc,
661
+ .kvValue,
662
+ .tlDesc,
663
+ .detailBody,
664
+ .accBody,
665
+ .banner,
666
+ .cardTitle,
667
+ .mediaLabel,
668
+ .heroLabel,
669
+ .heroTitle,
670
+ .heroSubtitle,
671
+ .statLabel,
672
+ .ringLabel,
673
+ .ringSub,
674
+ .calloutTitle,
675
+ .stepTitle,
676
+ .stepDesc,
677
+ .kvKey,
678
+ .tlTitle,
679
+ .tlTime,
680
+ .accTitle,
681
+ .field > span,
682
+ .fieldLabel,
683
+ .switchLabel,
684
+ .radio > span,
685
+ .checkbox > span,
686
+ .tab,
687
+ .badgeLabel,
688
+ .gradeQ,
689
+ .gradeAns,
690
+ .gradeRight,
691
+ .gradeExp,
692
+ .quizQuestion,
693
+ .quizFeedback,
694
+ .quizExplanation { white-space: pre-line; }
631
695
 
632
696
  /* sortable header button (v2.9) */
633
697
  .thSort {
@@ -13,8 +13,9 @@ import css from './GenuiBlock.module.css'
13
13
  import { loadBlockState, saveBlockState } from './interaction-store.ts'
14
14
  import { recordFence, recordInteraction } from './achievement-store.ts'
15
15
  import { renderNode } from './blocks/render-node.tsx'
16
- import type { AnswersState, GenuiBlockProps, QuestionMeta } from './blocks/state.ts'
16
+ import type { AnswersState, GenuiBlockProps } from './blocks/state.ts'
17
17
  import type { GenuiSpec } from './spec.ts'
18
+ import { compileSubmissionRegistry } from './submission-registry.ts'
18
19
 
19
20
  export const GENUI_ACTION_DEBOUNCE_MS = 300
20
21
 
@@ -96,7 +97,7 @@ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialSta
96
97
  const [answers, setAnswers] = useState<Record<string, string>>(persisted?.answers ?? {})
97
98
  const [multiAnswers, setMultiAnswers] = useState<Record<string, string[]>>(persisted?.multiAnswers ?? {})
98
99
  const [fields, setFields] = useState<Record<string, string>>(persisted?.fields ?? {})
99
- const [meta, setMeta] = useState<Record<string, QuestionMeta>>({})
100
+ const submissionRegistry = useMemo(() => compileSubmissionRegistry(spec), [spec])
100
101
  const [locked, setLocked] = useState(persisted?.locked === true)
101
102
  const [round, setRound] = useState(0)
102
103
  // Secret (password) field ids: their values never persist and never join
@@ -123,7 +124,7 @@ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialSta
123
124
  })
124
125
  }, [])
125
126
  const setField = useCallback((id: string, value: string) => {
126
- // Registry presence means "the user has touched this field" — a blank
127
+ // Field state presence means "the user has touched this field" — a blank
127
128
  // value is stored as '' instead of deleting the entry, so a user who
128
129
  // CLEARS a model-provided default does not get the default back on the
129
130
  // next mount. Blank values are still excluded from submit collection by
@@ -134,14 +135,6 @@ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialSta
134
135
  const registerSecretField = useCallback((id: string) => {
135
136
  setSecretFields(prev => (prev.has(id) ? prev : new Set(prev).add(id)))
136
137
  }, [])
137
- const registerMeta = useCallback((group: string, m: QuestionMeta) => {
138
- setMeta(prev => {
139
- const existing = prev[group]
140
- if (existing !== undefined && existing.label === m.label && existing.answer === m.answer
141
- && existing.explanation === m.explanation) return prev
142
- return { ...prev, [group]: m }
143
- })
144
- }, [])
145
138
  const clear = useCallback(() => {
146
139
  setAnswers({})
147
140
  setMultiAnswers({})
@@ -150,10 +143,10 @@ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialSta
150
143
  }, [])
151
144
  const answersState = useMemo<AnswersState>(
152
145
  () => ({
153
- answers, multiAnswers, fields, secretFields, meta, locked, round,
154
- setAnswer, setMultiAnswer, setField, registerSecretField, registerMeta, clear, setLocked,
146
+ answers, multiAnswers, fields, secretFields, registry: submissionRegistry, locked, round,
147
+ setAnswer, setMultiAnswer, setField, registerSecretField, clear, setLocked,
155
148
  }),
156
- [answers, multiAnswers, fields, secretFields, meta, locked, round, setAnswer, setMultiAnswer, setField, registerSecretField, registerMeta, clear],
149
+ [answers, multiAnswers, fields, secretFields, submissionRegistry, locked, round, setAnswer, setMultiAnswer, setField, registerSecretField, clear],
157
150
  )
158
151
  const durableState = useMemo(() => {
159
152
  const safeFields = Object.fromEntries(Object.entries(fields).filter(([id]) => !secretFields.has(id)))