@assistant-ui/react-generative-ui 0.0.12 → 0.0.14

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 (48) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +5 -0
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +12 -3
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/renderGenerativeUI.js +2 -1
  6. package/dist/renderGenerativeUI.js.map +1 -1
  7. package/dist/slack/constants.d.ts +11 -4
  8. package/dist/slack/constants.d.ts.map +1 -1
  9. package/dist/slack/constants.js +11 -4
  10. package/dist/slack/constants.js.map +1 -1
  11. package/dist/slack/fromSlackBlocks.js +2 -2
  12. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  13. package/dist/slack/toSlackBlocks.js +114 -19
  14. package/dist/slack/toSlackBlocks.js.map +1 -1
  15. package/dist/slack/types.d.ts +7 -1
  16. package/dist/slack/types.d.ts.map +1 -1
  17. package/dist/teams/constants.d.ts +9 -2
  18. package/dist/teams/constants.d.ts.map +1 -1
  19. package/dist/teams/constants.js +10 -3
  20. package/dist/teams/constants.js.map +1 -1
  21. package/dist/teams/toAdaptiveCard.d.ts +10 -2
  22. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  23. package/dist/teams/toAdaptiveCard.js +54 -15
  24. package/dist/teams/toAdaptiveCard.js.map +1 -1
  25. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  26. package/dist/teams/toTeamsAttachments.js +21 -13
  27. package/dist/teams/toTeamsAttachments.js.map +1 -1
  28. package/dist/teams/types.d.ts +11 -2
  29. package/dist/teams/types.d.ts.map +1 -1
  30. package/dist/vocabulary/data.js +3 -2
  31. package/dist/vocabulary/data.js.map +1 -1
  32. package/dist/vocabulary/layout.js +2 -1
  33. package/dist/vocabulary/layout.js.map +1 -1
  34. package/package.json +5 -5
  35. package/src/JSONGenerativeUI.client.tsx +13 -5
  36. package/src/JSONGenerativeUI.test.tsx +21 -2
  37. package/src/slack/constants.ts +11 -3
  38. package/src/slack/toSlackBlocks.test.ts +374 -8
  39. package/src/slack/toSlackBlocks.ts +181 -24
  40. package/src/slack/types.ts +7 -1
  41. package/src/teams/constants.ts +10 -2
  42. package/src/teams/toAdaptiveCard.test.ts +204 -10
  43. package/src/teams/toAdaptiveCard.ts +79 -15
  44. package/src/teams/toTeamsAttachments.test.ts +47 -2
  45. package/src/teams/toTeamsAttachments.ts +17 -11
  46. package/src/teams/types.ts +11 -2
  47. package/src/vocabulary/layout.test.tsx +16 -1
  48. package/src/vocabulary/layout.tsx +4 -1
@@ -16,6 +16,11 @@ declare class JSONGenerativeUI {
16
16
  private readonly parameters;
17
17
  private readonly actions;
18
18
  constructor(options: JSONGenerativeUIOptions);
19
+ /**
20
+ * The surface a `present` call paints into. It owns the vertical rhythm
21
+ * between top-level blocks, which the host's message container does not
22
+ * provide, and stays out of the way while it has nothing to show.
23
+ */
19
24
  private readonly render;
20
25
  present(options?: PresentToolOptions): PresentTool;
21
26
  promptUser(): PromptUserTool;
@@ -1 +1 @@
1
- {"version":3,"file":"JSONGenerativeUI.client.d.ts","names":[],"sources":["../src/JSONGenerativeUI.client.tsx"],"mappings":";;;;;;;;;;;;;cAkCa;mBACM;mBACA;mBACA;EAEL,YAAA,SAAS;mBAMJ;EAYjB,QAAQ,UAAU,qBAAqB;EASvC,cAAc"}
1
+ {"version":3,"file":"JSONGenerativeUI.client.d.ts","names":[],"sources":["../src/JSONGenerativeUI.client.tsx"],"mappings":";;;;;;;;;;;;;cAkCa;mBACM;mBACA;mBACA;EAEL,YAAA,SAAS;;;;;;mBAWJ;EAejB,QAAQ,UAAU,qBAAqB;EASvC,cAAc"}
@@ -1,6 +1,7 @@
1
1
  import { buildPresentParameters } from "./buildPresentParameters.js";
2
2
  import { presentToolBase, promptUserToolBase } from "./JSONGenerativeUI.shared.js";
3
3
  import { renderGenerativeUI } from "./renderGenerativeUI.js";
4
+ import { jsx } from "react/jsx-runtime";
4
5
  //#region src/JSONGenerativeUI.client.tsx
5
6
  /** Maps a tool-call part status to the generative-UI streaming status. Only a
6
7
  * `complete` call has fully-arrived args; `running` and `incomplete`
@@ -29,9 +30,17 @@ var JSONGenerativeUI = class {
29
30
  this.parameters = buildPresentParameters(options.library);
30
31
  this.actions = options.actions;
31
32
  }
32
- render = ({ args, status }) => renderGenerativeUI(args, this.library, {
33
- status: uiStatus(status),
34
- ...this.actions ? { dispatch: this.actions.dispatch } : {}
33
+ /**
34
+ * The surface a `present` call paints into. It owns the vertical rhythm
35
+ * between top-level blocks, which the host's message container does not
36
+ * provide, and stays out of the way while it has nothing to show.
37
+ */
38
+ render = ({ args, status }) => /* @__PURE__ */ jsx("div", {
39
+ "data-aui": "root",
40
+ children: renderGenerativeUI(args, this.library, {
41
+ status: uiStatus(status),
42
+ ...this.actions ? { dispatch: this.actions.dispatch } : {}
43
+ })
35
44
  });
36
45
  present(options) {
37
46
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"JSONGenerativeUI.client.js","names":[],"sources":["../src/JSONGenerativeUI.client.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { buildPresentParameters } from \"./buildPresentParameters\";\nimport {\n presentToolBase,\n promptUserToolBase,\n type JSONGenerativeUIOptions,\n type PresentParameters,\n type PresentTool,\n type PresentToolOptions,\n type PromptUserTool,\n} from \"./JSONGenerativeUI.shared\";\nimport { type ActionRegistry } from \"./actionRegistry\";\nimport { renderGenerativeUI } from \"./renderGenerativeUI\";\nimport type { GenerativeUILibrary, GenerativeUIStatus } from \"./types\";\n\n/** Maps a tool-call part status to the generative-UI streaming status. Only a\n * `complete` call has fully-arrived args; `running` and `incomplete`\n * (aborted/errored, so args may be partial) both render as `\"streaming\"` so a\n * non-streaming component is never handed partial props. */\nfunction uiStatus(status: { type: string }): GenerativeUIStatus {\n return status.type === \"complete\" ? \"done\" : \"streaming\";\n}\n\n/**\n * Client build of {@link JSONGenerativeUI}, resolved through the package's\n * `default` export condition (browser and SSR).\n *\n * `present` is a frontend tool that renders the model's `{ $type, ...props }`\n * tree against the library and resolves immediately. `prompt_user` is a\n * human-in-the-loop tool: the model pauses and the rendered UI supplies the\n * result. Both draw the tree the same way, so they share one `render`. The\n * `actions` registry (if provided) is threaded into the render context so\n * interactive components can fire `$action` through `$dispatch`.\n */\nexport class JSONGenerativeUI {\n private readonly library: GenerativeUILibrary;\n private readonly parameters: PresentParameters;\n private readonly actions: ActionRegistry | undefined;\n\n constructor(options: JSONGenerativeUIOptions) {\n this.library = options.library;\n this.parameters = buildPresentParameters(options.library);\n this.actions = options.actions;\n }\n\n private readonly render = ({\n args,\n status,\n }: {\n args: unknown;\n status: { type: string };\n }): ReactNode =>\n renderGenerativeUI(args, this.library, {\n status: uiStatus(status),\n ...(this.actions ? { dispatch: this.actions.dispatch } : {}),\n });\n\n present(options?: PresentToolOptions): PresentTool {\n return {\n ...presentToolBase(this.parameters, options),\n unstable_backendDefault: { parameters: true },\n execute: async () => ({}),\n render: this.render,\n };\n }\n\n promptUser(): PromptUserTool {\n return {\n ...promptUserToolBase(this.parameters),\n unstable_backendDefault: { parameters: true },\n render: this.render,\n };\n }\n}\n"],"mappings":";;;;;;;;AAmBA,SAAS,SAAS,QAA8C;CAC9D,OAAO,OAAO,SAAS,aAAa,SAAS;AAC/C;;;;;;;;;;;;AAaA,IAAa,mBAAb,MAA8B;CAC5B;CACA;CACA;CAEA,YAAY,SAAkC;EAC5C,KAAK,UAAU,QAAQ;EACvB,KAAK,aAAa,uBAAuB,QAAQ,OAAO;EACxD,KAAK,UAAU,QAAQ;CACzB;CAEA,UAA2B,EACzB,MACA,aAKA,mBAAmB,MAAM,KAAK,SAAS;EACrC,QAAQ,SAAS,MAAM;EACvB,GAAI,KAAK,UAAU,EAAE,UAAU,KAAK,QAAQ,SAAS,IAAI,CAAC;CAC5D,CAAC;CAEH,QAAQ,SAA2C;EACjD,OAAO;GACL,GAAG,gBAAgB,KAAK,YAAY,OAAO;GAC3C,yBAAyB,EAAE,YAAY,KAAK;GAC5C,SAAS,aAAa,CAAC;GACvB,QAAQ,KAAK;EACf;CACF;CAEA,aAA6B;EAC3B,OAAO;GACL,GAAG,mBAAmB,KAAK,UAAU;GACrC,yBAAyB,EAAE,YAAY,KAAK;GAC5C,QAAQ,KAAK;EACf;CACF;AACF"}
1
+ {"version":3,"file":"JSONGenerativeUI.client.js","names":[],"sources":["../src/JSONGenerativeUI.client.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { buildPresentParameters } from \"./buildPresentParameters\";\nimport {\n presentToolBase,\n promptUserToolBase,\n type JSONGenerativeUIOptions,\n type PresentParameters,\n type PresentTool,\n type PresentToolOptions,\n type PromptUserTool,\n} from \"./JSONGenerativeUI.shared\";\nimport { type ActionRegistry } from \"./actionRegistry\";\nimport { renderGenerativeUI } from \"./renderGenerativeUI\";\nimport type { GenerativeUILibrary, GenerativeUIStatus } from \"./types\";\n\n/** Maps a tool-call part status to the generative-UI streaming status. Only a\n * `complete` call has fully-arrived args; `running` and `incomplete`\n * (aborted/errored, so args may be partial) both render as `\"streaming\"` so a\n * non-streaming component is never handed partial props. */\nfunction uiStatus(status: { type: string }): GenerativeUIStatus {\n return status.type === \"complete\" ? \"done\" : \"streaming\";\n}\n\n/**\n * Client build of {@link JSONGenerativeUI}, resolved through the package's\n * `default` export condition (browser and SSR).\n *\n * `present` is a frontend tool that renders the model's `{ $type, ...props }`\n * tree against the library and resolves immediately. `prompt_user` is a\n * human-in-the-loop tool: the model pauses and the rendered UI supplies the\n * result. Both draw the tree the same way, so they share one `render`. The\n * `actions` registry (if provided) is threaded into the render context so\n * interactive components can fire `$action` through `$dispatch`.\n */\nexport class JSONGenerativeUI {\n private readonly library: GenerativeUILibrary;\n private readonly parameters: PresentParameters;\n private readonly actions: ActionRegistry | undefined;\n\n constructor(options: JSONGenerativeUIOptions) {\n this.library = options.library;\n this.parameters = buildPresentParameters(options.library);\n this.actions = options.actions;\n }\n\n /**\n * The surface a `present` call paints into. It owns the vertical rhythm\n * between top-level blocks, which the host's message container does not\n * provide, and stays out of the way while it has nothing to show.\n */\n private readonly render = ({\n args,\n status,\n }: {\n args: unknown;\n status: { type: string };\n }): ReactNode => (\n <div data-aui=\"root\">\n {renderGenerativeUI(args, this.library, {\n status: uiStatus(status),\n ...(this.actions ? { dispatch: this.actions.dispatch } : {}),\n })}\n </div>\n );\n\n present(options?: PresentToolOptions): PresentTool {\n return {\n ...presentToolBase(this.parameters, options),\n unstable_backendDefault: { parameters: true },\n execute: async () => ({}),\n render: this.render,\n };\n }\n\n promptUser(): PromptUserTool {\n return {\n ...promptUserToolBase(this.parameters),\n unstable_backendDefault: { parameters: true },\n render: this.render,\n };\n }\n}\n"],"mappings":";;;;;;;;;AAmBA,SAAS,SAAS,QAA8C;CAC9D,OAAO,OAAO,SAAS,aAAa,SAAS;AAC/C;;;;;;;;;;;;AAaA,IAAa,mBAAb,MAA8B;CAC5B;CACA;CACA;CAEA,YAAY,SAAkC;EAC5C,KAAK,UAAU,QAAQ;EACvB,KAAK,aAAa,uBAAuB,QAAQ,OAAO;EACxD,KAAK,UAAU,QAAQ;CACzB;;;;;;CAOA,UAA2B,EACzB,MACA,aAKA,oBAAC,OAAD;EAAK,YAAS;EACX,UAAA,mBAAmB,MAAM,KAAK,SAAS;GACtC,QAAQ,SAAS,MAAM;GACvB,GAAI,KAAK,UAAU,EAAE,UAAU,KAAK,QAAQ,SAAS,IAAI,CAAC;EAC5D,CAAC;CACE,CAAA;CAGP,QAAQ,SAA2C;EACjD,OAAO;GACL,GAAG,gBAAgB,KAAK,YAAY,OAAO;GAC3C,yBAAyB,EAAE,YAAY,KAAK;GAC5C,SAAS,aAAa,CAAC;GACvB,QAAQ,KAAK;EACf;CACF;CAEA,aAA6B;EAC3B,OAAO;GACL,GAAG,mBAAmB,KAAK,UAAU;GACrC,yBAAyB,EAAE,YAAY,KAAK;GAC5C,QAAQ,KAAK;EACf;CACF;AACF"}
@@ -17,7 +17,8 @@ const isElement = (node) => typeof node === "object" && node !== null && !Array.
17
17
  */
18
18
  function renderGenerativeUI(node, library, context = DEFAULT_CONTEXT) {
19
19
  const meta = getPartialJsonObjectMeta(node);
20
- return renderNode(normalizeUINode(node, meta?.state === "partial" ? meta.partialPath : void 0), library, context);
20
+ const partialPath = meta?.state === "partial" ? meta.partialPath : void 0;
21
+ return renderNode(normalizeUINode(node, partialPath), library, context);
21
22
  }
22
23
  function renderNode(node, library, context) {
23
24
  if (node == null || typeof node === "boolean") return null;
@@ -1 +1 @@
1
- {"version":3,"file":"renderGenerativeUI.js","names":[],"sources":["../src/renderGenerativeUI.tsx"],"sourcesContent":["import { getPartialJsonObjectMeta } from \"assistant-stream/utils\";\nimport { Fragment, type ReactNode } from \"react\";\nimport {\n normalizeUINode,\n type NormalizedUIElement,\n type NormalizedUINode,\n} from \"./ir\";\nimport type { GenerativeUILibrary, GenerativeUIRenderContext } from \"./types\";\n\nconst DEFAULT_CONTEXT: GenerativeUIRenderContext = { status: \"done\" };\n\nconst isElement = (node: NormalizedUINode): node is NormalizedUIElement =>\n typeof node === \"object\" && node !== null && !Array.isArray(node);\n\n/**\n * Renders a generative-ui tree against a {@link GenerativeUILibrary}.\n *\n * The model emits each node as a flat object `{ $type, ...props }`. We first\n * normalize that wire form into the canonical {@link NormalizedUINode} (with\n * `children` lifted to a reserved top-level key), then render: each `type` is\n * looked up in the library and its `props` are passed to the component's\n * `render(props, context)`, with `children` rendered recursively so components\n * can nest.\n */\nexport function renderGenerativeUI(\n node: unknown,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext = DEFAULT_CONTEXT,\n): ReactNode {\n // Tool args are parsed incrementally, and the parse meta records which path\n // is still mid-arrival, so normalization can hold back a node whose `$type`\n // string has not finished streaming.\n const meta = getPartialJsonObjectMeta(node as Record<symbol, unknown>);\n const partialPath = meta?.state === \"partial\" ? meta.partialPath : undefined;\n return renderNode(normalizeUINode(node, partialPath), library, context);\n}\n\nfunction renderNode(\n node: NormalizedUINode,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n if (node == null || typeof node === \"boolean\") return null;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node)) {\n // Use a model-provided stable key when present. Otherwise, keep the\n // positional fallback: pairing the index with the node's kind means that\n // when the kind at an index changes, React remounts instead of handing a\n // streaming node's hook state to a different component.\n return node.map((child, index) => (\n <Fragment key={nodeKey(child, index)}>\n {renderNode(child, library, context)}\n </Fragment>\n ));\n }\n return isElement(node) ? renderElement(node, library, context) : null;\n}\n\nfunction renderElement(\n element: NormalizedUIElement,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n const entry = library[element.type];\n if (!entry) {\n reportUnknownComponent(element.type, Object.keys(library));\n return null;\n }\n\n // Components that opt out of prop streaming wait until their props are\n // complete rather than rendering from a partial parse.\n if (!entry.streamProperties && context.status === \"streaming\") return null;\n\n // `children` is a reserved top-level key on the normalized element, not a\n // prop, so it is not in `props`. Inject the framework props last so the model\n // can never override them. `$action` is likewise reserved and stripped from\n // the prop bag during normalization, so it is re-injected here for components\n // that carry behavior (e.g. `Button`).\n const props: Record<string, unknown> = {\n ...element.props,\n $status: context.status,\n };\n if (context.dispatch !== undefined) {\n props[\"$dispatch\"] = context.dispatch;\n }\n if (element.action !== undefined) {\n props[\"$action\"] = element.action;\n }\n if (element.children !== undefined) {\n props[\"children\"] = renderNode(element.children, library, context);\n }\n\n return <GenerativeUIComponentRenderer render={entry.render} props={props} />;\n}\n\n/**\n * Mounts a single node's `render` on its own fiber so the function may use\n * hooks and hold state independently of its siblings and parent.\n */\nfunction GenerativeUIComponentRenderer({\n render,\n props,\n}: {\n render: (props: any) => ReactNode;\n props: Record<string, unknown>;\n}): ReactNode {\n return render(props);\n}\n\n/** A coarse kind tag for a child, used in its list key so a node changing kind\n * at a given index forces a remount rather than a wrong-fiber reuse. */\nfunction nodeKind(node: NormalizedUINode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return \"#text\";\n if (Array.isArray(node)) return \"#array\";\n return isElement(node) ? node.type : \"\";\n}\n\nfunction nodeKey(node: NormalizedUINode, index: number): string {\n return isElement(node) &&\n (typeof node.key === \"string\" || typeof node.key === \"number\")\n ? `model:${node.key}`\n : `${index}:${nodeKind(node)}`;\n}\n\nfunction reportUnknownComponent(type: string, available: string[]): void {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.error(\n `[@assistant-ui/react-generative-ui] Unknown component \"${type}\". ` +\n `Available components: ${available.join(\", \") || \"(none)\"}.`,\n );\n }\n}\n"],"mappings":";;;;;AASA,MAAM,kBAA6C,EAAE,QAAQ,OAAO;AAEpE,MAAM,aAAa,SACjB,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI;;;;;;;;;;;AAYlE,SAAgB,mBACd,MACA,SACA,UAAqC,iBAC1B;CAIX,MAAM,OAAO,yBAAyB,IAA+B;CAErE,OAAO,WAAW,gBAAgB,MADd,MAAM,UAAU,YAAY,KAAK,cAAc,KAAA,CAChB,GAAG,SAAS,OAAO;AACxE;AAEA,SAAS,WACP,MACA,SACA,SACW;CACX,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAKpB,OAAO,KAAK,KAAK,OAAO,UACtB,oBAAC,UAAD,EAAA,UACG,WAAW,OAAO,SAAS,OAAO,EAC3B,GAFK,QAAQ,OAAO,KAAK,CAEzB,CACX;CAEH,OAAO,UAAU,IAAI,IAAI,cAAc,MAAM,SAAS,OAAO,IAAI;AACnE;AAEA,SAAS,cACP,SACA,SACA,SACW;CACX,MAAM,QAAQ,QAAQ,QAAQ;CAC9B,IAAI,CAAC,OAAO;EACV,uBAAuB,QAAQ,MAAM,OAAO,KAAK,OAAO,CAAC;EACzD,OAAO;CACT;CAIA,IAAI,CAAC,MAAM,oBAAoB,QAAQ,WAAW,aAAa,OAAO;CAOtE,MAAM,QAAiC;EACrC,GAAG,QAAQ;EACX,SAAS,QAAQ;CACnB;CACA,IAAI,QAAQ,aAAa,KAAA,GACvB,MAAM,eAAe,QAAQ;CAE/B,IAAI,QAAQ,WAAW,KAAA,GACrB,MAAM,aAAa,QAAQ;CAE7B,IAAI,QAAQ,aAAa,KAAA,GACvB,MAAM,cAAc,WAAW,QAAQ,UAAU,SAAS,OAAO;CAGnE,OAAO,oBAAC,+BAAD;EAA+B,QAAQ,MAAM;EAAe;CAAQ,CAAA;AAC7E;;;;;AAMA,SAAS,8BAA8B,EACrC,QACA,SAIY;CACZ,OAAO,OAAO,KAAK;AACrB;;;AAIA,SAAS,SAAS,MAAgC;CAChD,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;CAChC,OAAO,UAAU,IAAI,IAAI,KAAK,OAAO;AACvC;AAEA,SAAS,QAAQ,MAAwB,OAAuB;CAC9D,OAAO,UAAU,IAAI,MAClB,OAAO,KAAK,QAAQ,YAAY,OAAO,KAAK,QAAQ,YACnD,SAAS,KAAK,QACd,GAAG,MAAM,GAAG,SAAS,IAAI;AAC/B;AAEA,SAAS,uBAAuB,MAAc,WAA2B;CACvE,IAAI,QAAQ,IAAI,gBAAgB,cAE9B,QAAQ,MACN,0DAA0D,KAAK,2BACpC,UAAU,KAAK,IAAI,KAAK,SAAS,EAC9D;AAEJ"}
1
+ {"version":3,"file":"renderGenerativeUI.js","names":[],"sources":["../src/renderGenerativeUI.tsx"],"sourcesContent":["import { getPartialJsonObjectMeta } from \"assistant-stream/utils\";\nimport { Fragment, type ReactNode } from \"react\";\nimport {\n normalizeUINode,\n type NormalizedUIElement,\n type NormalizedUINode,\n} from \"./ir\";\nimport type { GenerativeUILibrary, GenerativeUIRenderContext } from \"./types\";\n\nconst DEFAULT_CONTEXT: GenerativeUIRenderContext = { status: \"done\" };\n\nconst isElement = (node: NormalizedUINode): node is NormalizedUIElement =>\n typeof node === \"object\" && node !== null && !Array.isArray(node);\n\n/**\n * Renders a generative-ui tree against a {@link GenerativeUILibrary}.\n *\n * The model emits each node as a flat object `{ $type, ...props }`. We first\n * normalize that wire form into the canonical {@link NormalizedUINode} (with\n * `children` lifted to a reserved top-level key), then render: each `type` is\n * looked up in the library and its `props` are passed to the component's\n * `render(props, context)`, with `children` rendered recursively so components\n * can nest.\n */\nexport function renderGenerativeUI(\n node: unknown,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext = DEFAULT_CONTEXT,\n): ReactNode {\n // Tool args are parsed incrementally, and the parse meta records which path\n // is still mid-arrival, so normalization can hold back a node whose `$type`\n // string has not finished streaming.\n const meta = getPartialJsonObjectMeta(node as Record<symbol, unknown>);\n const partialPath = meta?.state === \"partial\" ? meta.partialPath : undefined;\n return renderNode(normalizeUINode(node, partialPath), library, context);\n}\n\nfunction renderNode(\n node: NormalizedUINode,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n if (node == null || typeof node === \"boolean\") return null;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node)) {\n // Use a model-provided stable key when present. Otherwise, keep the\n // positional fallback: pairing the index with the node's kind means that\n // when the kind at an index changes, React remounts instead of handing a\n // streaming node's hook state to a different component.\n return node.map((child, index) => (\n <Fragment key={nodeKey(child, index)}>\n {renderNode(child, library, context)}\n </Fragment>\n ));\n }\n return isElement(node) ? renderElement(node, library, context) : null;\n}\n\nfunction renderElement(\n element: NormalizedUIElement,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n const entry = library[element.type];\n if (!entry) {\n reportUnknownComponent(element.type, Object.keys(library));\n return null;\n }\n\n // Components that opt out of prop streaming wait until their props are\n // complete rather than rendering from a partial parse.\n if (!entry.streamProperties && context.status === \"streaming\") return null;\n\n // `children` is a reserved top-level key on the normalized element, not a\n // prop, so it is not in `props`. Inject the framework props last so the model\n // can never override them. `$action` is likewise reserved and stripped from\n // the prop bag during normalization, so it is re-injected here for components\n // that carry behavior (e.g. `Button`).\n const props: Record<string, unknown> = {\n ...element.props,\n $status: context.status,\n };\n if (context.dispatch !== undefined) {\n props[\"$dispatch\"] = context.dispatch;\n }\n if (element.action !== undefined) {\n props[\"$action\"] = element.action;\n }\n if (element.children !== undefined) {\n props[\"children\"] = renderNode(element.children, library, context);\n }\n\n return <GenerativeUIComponentRenderer render={entry.render} props={props} />;\n}\n\n/**\n * Mounts a single node's `render` on its own fiber so the function may use\n * hooks and hold state independently of its siblings and parent.\n */\nfunction GenerativeUIComponentRenderer({\n render,\n props,\n}: {\n render: (props: any) => ReactNode;\n props: Record<string, unknown>;\n}): ReactNode {\n return render(props);\n}\n\n/** A coarse kind tag for a child, used in its list key so a node changing kind\n * at a given index forces a remount rather than a wrong-fiber reuse. */\nfunction nodeKind(node: NormalizedUINode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return \"#text\";\n if (Array.isArray(node)) return \"#array\";\n return isElement(node) ? node.type : \"\";\n}\n\nfunction nodeKey(node: NormalizedUINode, index: number): string {\n return isElement(node) &&\n (typeof node.key === \"string\" || typeof node.key === \"number\")\n ? `model:${node.key}`\n : `${index}:${nodeKind(node)}`;\n}\n\nfunction reportUnknownComponent(type: string, available: string[]): void {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.error(\n `[@assistant-ui/react-generative-ui] Unknown component \"${type}\". ` +\n `Available components: ${available.join(\", \") || \"(none)\"}.`,\n );\n }\n}\n"],"mappings":";;;;;AASA,MAAM,kBAA6C,EAAE,QAAQ,OAAO;AAEpE,MAAM,aAAa,SACjB,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI;;;;;;;;;;;AAYlE,SAAgB,mBACd,MACA,SACA,UAAqC,iBAC1B;CAIX,MAAM,OAAO,yBAAyB,IAA+B;CACrE,MAAM,cAAc,MAAM,UAAU,YAAY,KAAK,cAAc,KAAA;CACnE,OAAO,WAAW,gBAAgB,MAAM,WAAW,GAAG,SAAS,OAAO;AACxE;AAEA,SAAS,WACP,MACA,SACA,SACW;CACX,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAKpB,OAAO,KAAK,KAAK,OAAO,UACtB,oBAAC,UAAD,EAAA,UACG,WAAW,OAAO,SAAS,OAAO,EAC3B,GAFK,QAAQ,OAAO,KAAK,CAEzB,CACX;CAEH,OAAO,UAAU,IAAI,IAAI,cAAc,MAAM,SAAS,OAAO,IAAI;AACnE;AAEA,SAAS,cACP,SACA,SACA,SACW;CACX,MAAM,QAAQ,QAAQ,QAAQ;CAC9B,IAAI,CAAC,OAAO;EACV,uBAAuB,QAAQ,MAAM,OAAO,KAAK,OAAO,CAAC;EACzD,OAAO;CACT;CAIA,IAAI,CAAC,MAAM,oBAAoB,QAAQ,WAAW,aAAa,OAAO;CAOtE,MAAM,QAAiC;EACrC,GAAG,QAAQ;EACX,SAAS,QAAQ;CACnB;CACA,IAAI,QAAQ,aAAa,KAAA,GACvB,MAAM,eAAe,QAAQ;CAE/B,IAAI,QAAQ,WAAW,KAAA,GACrB,MAAM,aAAa,QAAQ;CAE7B,IAAI,QAAQ,aAAa,KAAA,GACvB,MAAM,cAAc,WAAW,QAAQ,UAAU,SAAS,OAAO;CAGnE,OAAO,oBAAC,+BAAD;EAA+B,QAAQ,MAAM;EAAe;CAAQ,CAAA;AAC7E;;;;;AAMA,SAAS,8BAA8B,EACrC,QACA,SAIY;CACZ,OAAO,OAAO,KAAK;AACrB;;;AAIA,SAAS,SAAS,MAAgC;CAChD,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;CAChC,OAAO,UAAU,IAAI,IAAI,KAAK,OAAO;AACvC;AAEA,SAAS,QAAQ,MAAwB,OAAuB;CAC9D,OAAO,UAAU,IAAI,MAClB,OAAO,KAAK,QAAQ,YAAY,OAAO,KAAK,QAAQ,YACnD,SAAS,KAAK,QACd,GAAG,MAAM,GAAG,SAAS,IAAI;AAC/B;AAEA,SAAS,uBAAuB,MAAc,WAA2B;CACvE,IAAI,QAAQ,IAAI,gBAAgB,cAE9B,QAAQ,MACN,0DAA0D,KAAK,2BACpC,UAAU,KAAK,IAAI,KAAK,SAAS,EAC9D;AAEJ"}
@@ -1,7 +1,14 @@
1
1
  import { SlackAlertBlock, SlackButtonElement, SlackCardBlock, SlackCarouselBlock, SlackContextBlock, SlackDataTableBlock, SlackDataTableCell, SlackImageBlock, SlackSectionBlock, SlackTextObject } from "./types.js";
2
2
  //#region src/slack/constants.d.ts
3
- /** The maximum normalized traversal depth. */
3
+ /** The maximum traversal depth, counted in visited values, not tree levels. */
4
4
  declare const MAX_TRAVERSAL_DEPTH = 64;
5
+ /**
6
+ * The element-nesting ceiling a caller observes. `boundNode` visits an element
7
+ * and its `children` array separately, so one level of nesting spends two of
8
+ * {@link MAX_TRAVERSAL_DEPTH}; it runs before the conversion walk and is never
9
+ * the looser of the two. A root array costs one more level than a root object.
10
+ */
11
+ declare const MAX_ELEMENT_DEPTH: number;
5
12
  /**
6
13
  * The maximum number of entries kept at any single level (root, or a
7
14
  * `children` array at any depth) of the raw spec, before it ever reaches
@@ -74,14 +81,14 @@ declare const ALERT_TEXT_CAP = 200;
74
81
  * The maximum number of data rows in a data-table block; the emitted `rows`
75
82
  * array additionally carries one header row.
76
83
  */
77
- declare const DATA_TABLE_ROW_CAP = 100;
84
+ declare const DATA_TABLE_ROW_CAP = 200;
78
85
  /** The maximum number of columns in a data-table block. */
79
86
  declare const DATA_TABLE_COLUMN_CAP = 20;
80
87
  /**
81
88
  * The cumulative character limit (summed across every cell's text) shared by
82
89
  * every data-table block in one payload.
83
90
  */
84
- declare const DATA_TABLE_CHAR_BUDGET = 10000;
91
+ declare const DATA_TABLE_CHAR_BUDGET = 20000;
85
92
  /** The caption emitted on every native data-table block. */
86
93
  declare const TABLE_CAPTION = "Table";
87
94
  /** The cumulative character limit for native markdown blocks. */
@@ -107,5 +114,5 @@ declare function buildDataTableBlock(rows: readonly (readonly SlackDataTableCell
107
114
  /** Builds the final note used when a surface block budget is exceeded. */
108
115
  declare function buildBlockBudgetNote(omitted: number): SlackContextBlock;
109
116
  //#endregion
110
- export { ACTIONS_ELEMENT_CAP, ACTION_ID_CAP, ALERT_TEXT_CAP, BUTTON_VALUE_CAP, CARD_ACTIONS_CAP, CARD_BODY_CAP, CARD_SUBTEXT_CAP, CARD_TITLE_CAP, CAROUSEL_CARD_CAP, CAROUSEL_CARD_MIN, CHILDREN_CAP, CONTEXT_ELEMENT_CAP, CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, DATA_TABLE_COLUMN_CAP, DATA_TABLE_ROW_CAP, FACT_FIELD_CAP, FACT_FIELD_TEXT_CAP, HEADER_TEXT_CAP, INBOUND_BLOCK_CAP, INPUT_LABEL_CAP, INTERACTIVE_TEXT_CAP, MARKDOWN_TEXT_BUDGET, MAX_TRAVERSAL_DEPTH, MESSAGE_BLOCK_CAP, MODAL_BLOCK_CAP, NODE_BUDGET, PLACEHOLDER_TEXT_CAP, RADIO_OPTION_CAP, SECTION_TEXT_CAP, SELECT_OPTION_CAP, TABLE_CAPTION, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock };
117
+ export { ACTIONS_ELEMENT_CAP, ACTION_ID_CAP, ALERT_TEXT_CAP, BUTTON_VALUE_CAP, CARD_ACTIONS_CAP, CARD_BODY_CAP, CARD_SUBTEXT_CAP, CARD_TITLE_CAP, CAROUSEL_CARD_CAP, CAROUSEL_CARD_MIN, CHILDREN_CAP, CONTEXT_ELEMENT_CAP, CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, DATA_TABLE_COLUMN_CAP, DATA_TABLE_ROW_CAP, FACT_FIELD_CAP, FACT_FIELD_TEXT_CAP, HEADER_TEXT_CAP, INBOUND_BLOCK_CAP, INPUT_LABEL_CAP, INTERACTIVE_TEXT_CAP, MARKDOWN_TEXT_BUDGET, MAX_ELEMENT_DEPTH, MAX_TRAVERSAL_DEPTH, MESSAGE_BLOCK_CAP, MODAL_BLOCK_CAP, NODE_BUDGET, PLACEHOLDER_TEXT_CAP, RADIO_OPTION_CAP, SECTION_TEXT_CAP, SELECT_OPTION_CAP, TABLE_CAPTION, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock };
111
118
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","names":[],"sources":["../../src/slack/constants.ts"],"mappings":";;;cAca;;;;;;;;cASA;;;;;;;;cASA;;cAGA;;cAGA;;;;;;cAOA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;;;;cAMA;;cAGA;;;;;cAMA;;cAGA;;cAGA;;iBAGG,mBACd,eACA,qBACA,4DACW,mBAAmB;;iBAmBhB,gBACd,cACA,kDACC;KAKE;WACM,YAAY;WACZ,QAAQ;WACR,WAAW;WACX,OAAO;WACP,UAAU;WACV,mBAAmB;;;iBAId,eAAe,QAAQ,kBAAkB;;iBAczC,mBACd,mBAAmB,mBAClB;;iBAKa,oBACd,yBAAyB,0BACxB;;iBAKa,qBAAqB,kBAAkB"}
1
+ {"version":3,"file":"constants.d.ts","names":[],"sources":["../../src/slack/constants.ts"],"mappings":";;;cAca;;;;;;;cAQA;;;;;;;;cASA;;;;;;;;cASA;;cAGA;;cAGA;;;;;;cAOA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;;;;cAMA;;cAGA;;;;;cAMA;;cAGA;;cAGA;;iBAGG,mBACd,eACA,qBACA,4DACW,mBAAmB;;iBAmBhB,gBACd,cACA,kDACC;KAKE;WACM,YAAY;WACZ,QAAQ;WACR,WAAW;WACX,OAAO;WACP,UAAU;WACV,mBAAmB;;;iBAId,eAAe,QAAQ,kBAAkB;;iBAczC,mBACd,mBAAmB,mBAClB;;iBAKa,oBACd,yBAAyB,0BACxB;;iBAKa,qBAAqB,kBAAkB"}
@@ -1,7 +1,14 @@
1
1
  //#region src/slack/constants.ts
2
- /** The maximum normalized traversal depth. */
2
+ /** The maximum traversal depth, counted in visited values, not tree levels. */
3
3
  const MAX_TRAVERSAL_DEPTH = 64;
4
4
  /**
5
+ * The element-nesting ceiling a caller observes. `boundNode` visits an element
6
+ * and its `children` array separately, so one level of nesting spends two of
7
+ * {@link MAX_TRAVERSAL_DEPTH}; it runs before the conversion walk and is never
8
+ * the looser of the two. A root array costs one more level than a root object.
9
+ */
10
+ const MAX_ELEMENT_DEPTH = 32;
11
+ /**
5
12
  * The maximum number of entries kept at any single level (root, or a
6
13
  * `children` array at any depth) of the raw spec, before it ever reaches
7
14
  * `normalizeSpec`. Bounds a hostile array's reported `length` so the pre-pass
@@ -73,14 +80,14 @@ const ALERT_TEXT_CAP = 200;
73
80
  * The maximum number of data rows in a data-table block; the emitted `rows`
74
81
  * array additionally carries one header row.
75
82
  */
76
- const DATA_TABLE_ROW_CAP = 100;
83
+ const DATA_TABLE_ROW_CAP = 200;
77
84
  /** The maximum number of columns in a data-table block. */
78
85
  const DATA_TABLE_COLUMN_CAP = 20;
79
86
  /**
80
87
  * The cumulative character limit (summed across every cell's text) shared by
81
88
  * every data-table block in one payload.
82
89
  */
83
- const DATA_TABLE_CHAR_BUDGET = 1e4;
90
+ const DATA_TABLE_CHAR_BUDGET = 2e4;
84
91
  /** The caption emitted on every native data-table block. */
85
92
  const TABLE_CAPTION = "Table";
86
93
  /** The cumulative character limit for native markdown blocks. */
@@ -157,6 +164,6 @@ function buildBlockBudgetNote(omitted) {
157
164
  };
158
165
  }
159
166
  //#endregion
160
- export { ACTIONS_ELEMENT_CAP, ACTION_ID_CAP, ALERT_TEXT_CAP, BUTTON_VALUE_CAP, CARD_ACTIONS_CAP, CARD_BODY_CAP, CARD_SUBTEXT_CAP, CARD_TITLE_CAP, CAROUSEL_CARD_CAP, CAROUSEL_CARD_MIN, CHILDREN_CAP, CONTEXT_ELEMENT_CAP, CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, DATA_TABLE_COLUMN_CAP, DATA_TABLE_ROW_CAP, FACT_FIELD_CAP, FACT_FIELD_TEXT_CAP, HEADER_TEXT_CAP, INBOUND_BLOCK_CAP, INPUT_LABEL_CAP, INTERACTIVE_TEXT_CAP, MARKDOWN_TEXT_BUDGET, MAX_TRAVERSAL_DEPTH, MESSAGE_BLOCK_CAP, MODAL_BLOCK_CAP, NODE_BUDGET, PLACEHOLDER_TEXT_CAP, RADIO_OPTION_CAP, SECTION_TEXT_CAP, SELECT_OPTION_CAP, TABLE_CAPTION, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock };
167
+ export { ACTIONS_ELEMENT_CAP, ACTION_ID_CAP, ALERT_TEXT_CAP, BUTTON_VALUE_CAP, CARD_ACTIONS_CAP, CARD_BODY_CAP, CARD_SUBTEXT_CAP, CARD_TITLE_CAP, CAROUSEL_CARD_CAP, CAROUSEL_CARD_MIN, CHILDREN_CAP, CONTEXT_ELEMENT_CAP, CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, DATA_TABLE_COLUMN_CAP, DATA_TABLE_ROW_CAP, FACT_FIELD_CAP, FACT_FIELD_TEXT_CAP, HEADER_TEXT_CAP, INBOUND_BLOCK_CAP, INPUT_LABEL_CAP, INTERACTIVE_TEXT_CAP, MARKDOWN_TEXT_BUDGET, MAX_ELEMENT_DEPTH, MAX_TRAVERSAL_DEPTH, MESSAGE_BLOCK_CAP, MODAL_BLOCK_CAP, NODE_BUDGET, PLACEHOLDER_TEXT_CAP, RADIO_OPTION_CAP, SECTION_TEXT_CAP, SELECT_OPTION_CAP, TABLE_CAPTION, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock };
161
168
 
162
169
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","names":[],"sources":["../../src/slack/constants.ts"],"sourcesContent":["import type {\n SlackAlertBlock,\n SlackButtonElement,\n SlackCardBlock,\n SlackCarouselBlock,\n SlackContextBlock,\n SlackDataTableBlock,\n SlackDataTableCell,\n SlackImageBlock,\n SlackSectionBlock,\n SlackTextObject,\n} from \"./types\";\n\n/** The maximum normalized traversal depth. */\nexport const MAX_TRAVERSAL_DEPTH = 64;\n\n/**\n * The maximum number of entries kept at any single level (root, or a\n * `children` array at any depth) of the raw spec, before it ever reaches\n * `normalizeSpec`. Bounds a hostile array's reported `length` so the pre-pass\n * can never be made to walk further than this regardless of what the array\n * claims about itself.\n */\nexport const CHILDREN_CAP = 200;\n\n/**\n * The total number of nodes the pre-normalization bounding walk will visit\n * across one call, regardless of how many times a shared reference recurs.\n * Bounds the combinatorial work a DAG of shared or self-referential nodes\n * would otherwise force even though each individual array stays within\n * {@link CHILDREN_CAP}.\n */\nexport const NODE_BUDGET = 5000;\n\n/** The block limit for a Slack message. */\nexport const MESSAGE_BLOCK_CAP = 50;\n\n/** The block limit for a Slack modal. */\nexport const MODAL_BLOCK_CAP = 100;\n\n/**\n * The maximum number of top-level blocks accepted when parsing Slack Block\n * Kit JSON back into generative UI, guarding against a hostile or malformed\n * payload.\n */\nexport const INBOUND_BLOCK_CAP = 200;\n\n/** The character limit for header text. */\nexport const HEADER_TEXT_CAP = 150;\n\n/** The character limit for section text. */\nexport const SECTION_TEXT_CAP = 3000;\n\n/** The maximum number of fields in one section. */\nexport const FACT_FIELD_CAP = 10;\n\n/** The character limit for one section field. */\nexport const FACT_FIELD_TEXT_CAP = 2000;\n\n/** The character limit for button labels and option labels. */\nexport const INTERACTIVE_TEXT_CAP = 75;\n\n/** The character limit for an action_id. */\nexport const ACTION_ID_CAP = 255;\n\n/** The character limit for a button's serialized value payload. */\nexport const BUTTON_VALUE_CAP = 2000;\n\n/** The maximum number of elements in one actions block. */\nexport const ACTIONS_ELEMENT_CAP = 25;\n\n/** The maximum number of options in a static select. */\nexport const SELECT_OPTION_CAP = 100;\n\n/** The maximum number of options in a radio-button group. */\nexport const RADIO_OPTION_CAP = 10;\n\n/** The character limit for a select or input placeholder. */\nexport const PLACEHOLDER_TEXT_CAP = 150;\n\n/** The character limit for an input's label. */\nexport const INPUT_LABEL_CAP = 2000;\n\n/** The maximum number of elements in one context block. */\nexport const CONTEXT_ELEMENT_CAP = 10;\n\n/** The character limit for one context element's text. */\nexport const CONTEXT_TEXT_CAP = 3000;\n\n/** The character limit for card titles and subtitles. */\nexport const CARD_TITLE_CAP = 150;\n\n/** The character limit for a card's body text. */\nexport const CARD_BODY_CAP = 200;\n\n/** The character limit for a card's subtext. */\nexport const CARD_SUBTEXT_CAP = 200;\n\n/** The maximum number of buttons in a card's actions. */\nexport const CARD_ACTIONS_CAP = 3;\n\n/** The minimum number of elements in a renderable carousel. */\nexport const CAROUSEL_CARD_MIN = 1;\n\n/** The maximum number of elements in a carousel. */\nexport const CAROUSEL_CARD_CAP = 10;\n\n/** The character limit for an alert's text. */\nexport const ALERT_TEXT_CAP = 200;\n\n/**\n * The maximum number of data rows in a data-table block; the emitted `rows`\n * array additionally carries one header row.\n */\nexport const DATA_TABLE_ROW_CAP = 100;\n\n/** The maximum number of columns in a data-table block. */\nexport const DATA_TABLE_COLUMN_CAP = 20;\n\n/**\n * The cumulative character limit (summed across every cell's text) shared by\n * every data-table block in one payload.\n */\nexport const DATA_TABLE_CHAR_BUDGET = 10000;\n\n/** The caption emitted on every native data-table block. */\nexport const TABLE_CAPTION = \"Table\";\n\n/** The cumulative character limit for native markdown blocks. */\nexport const MARKDOWN_TEXT_BUDGET = 12000;\n\n/** Builds the message-surface fallback for an alert. */\nexport function buildAlertFallback(\n title: string,\n description: string,\n tone: \"info\" | \"success\" | \"warning\" | \"danger\",\n): readonly [SlackContextBlock, SlackSectionBlock] {\n const emoji = {\n info: \"ℹ️\",\n success: \"✅\",\n warning: \"⚠️\",\n danger: \"🛑\",\n }[tone];\n return [\n {\n type: \"context\",\n elements: [\n { type: \"mrkdwn\", text: title ? `${emoji} *${title}*` : emoji },\n ],\n },\n { type: \"section\", text: { type: \"mrkdwn\", text: description } },\n ];\n}\n\n/** Builds a 2026 Slack modal-only alert block, mapping `tone` to its `level`. */\nexport function buildAlertBlock(\n text: string,\n tone: \"info\" | \"success\" | \"warning\" | \"danger\",\n): SlackAlertBlock {\n const level = tone === \"danger\" ? \"error\" : tone;\n return { type: \"alert\", text: { type: \"mrkdwn\", text }, level };\n}\n\ntype CardBlockFields = {\n readonly heroImage?: SlackImageBlock;\n readonly title?: SlackTextObject;\n readonly subtitle?: SlackTextObject;\n readonly body?: SlackTextObject;\n readonly subtext?: SlackTextObject;\n readonly actions?: readonly SlackButtonElement[];\n};\n\n/** Builds a 2026 Slack card block from its already-clamped fields. */\nexport function buildCardBlock(fields: CardBlockFields): SlackCardBlock {\n const { heroImage, title, subtitle, body, subtext, actions } = fields;\n return {\n type: \"card\",\n ...(heroImage !== undefined ? { hero_image: heroImage } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(subtitle !== undefined ? { subtitle } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(subtext !== undefined ? { subtext } : {}),\n ...(actions !== undefined ? { actions } : {}),\n };\n}\n\n/** Builds a 2026 Slack carousel block. */\nexport function buildCarouselBlock(\n elements: readonly SlackCardBlock[],\n): SlackCarouselBlock {\n return { type: \"carousel\", elements };\n}\n\n/** Builds a 2026 Slack data-table block; `rows[0]` must be the header row. */\nexport function buildDataTableBlock(\n rows: readonly (readonly SlackDataTableCell[])[],\n): SlackDataTableBlock {\n return { type: \"data_table\", caption: TABLE_CAPTION, rows };\n}\n\n/** Builds the final note used when a surface block budget is exceeded. */\nexport function buildBlockBudgetNote(omitted: number): SlackContextBlock {\n return {\n type: \"context\",\n elements: [\n {\n type: \"mrkdwn\",\n text: `${omitted} ${omitted === 1 ? \"block was\" : \"blocks were\"} omitted.`,\n },\n ],\n };\n}\n"],"mappings":";;AAcA,MAAa,sBAAsB;;;;;;;;AASnC,MAAa,eAAe;;;;;;;;AAS5B,MAAa,cAAc;;AAG3B,MAAa,oBAAoB;;AAGjC,MAAa,kBAAkB;;;;;;AAO/B,MAAa,oBAAoB;;AAGjC,MAAa,kBAAkB;;AAG/B,MAAa,mBAAmB;;AAGhC,MAAa,iBAAiB;;AAG9B,MAAa,sBAAsB;;AAGnC,MAAa,uBAAuB;;AAGpC,MAAa,gBAAgB;;AAG7B,MAAa,mBAAmB;;AAGhC,MAAa,sBAAsB;;AAGnC,MAAa,oBAAoB;;AAGjC,MAAa,mBAAmB;;AAGhC,MAAa,uBAAuB;;AAGpC,MAAa,kBAAkB;;AAG/B,MAAa,sBAAsB;;AAGnC,MAAa,mBAAmB;;AAGhC,MAAa,iBAAiB;;AAG9B,MAAa,gBAAgB;;AAG7B,MAAa,mBAAmB;;AAGhC,MAAa,mBAAmB;;AAGhC,MAAa,oBAAoB;;AAGjC,MAAa,oBAAoB;;AAGjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,qBAAqB;;AAGlC,MAAa,wBAAwB;;;;;AAMrC,MAAa,yBAAyB;;AAGtC,MAAa,gBAAgB;;AAG7B,MAAa,uBAAuB;;AAGpC,SAAgB,mBACd,OACA,aACA,MACiD;CACjD,MAAM,QAAQ;EACZ,MAAM;EACN,SAAS;EACT,SAAS;EACT,QAAQ;CACV,EAAE;CACF,OAAO,CACL;EACE,MAAM;EACN,UAAU,CACR;GAAE,MAAM;GAAU,MAAM,QAAQ,GAAG,MAAM,IAAI,MAAM,KAAK;EAAM,CAChE;CACF,GACA;EAAE,MAAM;EAAW,MAAM;GAAE,MAAM;GAAU,MAAM;EAAY;CAAE,CACjE;AACF;;AAGA,SAAgB,gBACd,MACA,MACiB;CAEjB,OAAO;EAAE,MAAM;EAAS,MAAM;GAAE,MAAM;GAAU;EAAK;EAAG,OAD1C,SAAS,WAAW,UAAU;CACkB;AAChE;;AAYA,SAAgB,eAAe,QAAyC;CACtE,MAAM,EAAE,WAAW,OAAO,UAAU,MAAM,SAAS,YAAY;CAC/D,OAAO;EACL,MAAM;EACN,GAAI,cAAc,KAAA,IAAY,EAAE,YAAY,UAAU,IAAI,CAAC;EAC3D,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACvC,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC7C,GAAI,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,CAAC;EACrC,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;EAC3C,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;CAC7C;AACF;;AAGA,SAAgB,mBACd,UACoB;CACpB,OAAO;EAAE,MAAM;EAAY;CAAS;AACtC;;AAGA,SAAgB,oBACd,MACqB;CACrB,OAAO;EAAE,MAAM;EAAc,SAAS;EAAe;CAAK;AAC5D;;AAGA,SAAgB,qBAAqB,SAAoC;CACvE,OAAO;EACL,MAAM;EACN,UAAU,CACR;GACE,MAAM;GACN,MAAM,GAAG,QAAQ,GAAG,YAAY,IAAI,cAAc,cAAc;EAClE,CACF;CACF;AACF"}
1
+ {"version":3,"file":"constants.js","names":[],"sources":["../../src/slack/constants.ts"],"sourcesContent":["import type {\n SlackAlertBlock,\n SlackButtonElement,\n SlackCardBlock,\n SlackCarouselBlock,\n SlackContextBlock,\n SlackDataTableBlock,\n SlackDataTableCell,\n SlackImageBlock,\n SlackSectionBlock,\n SlackTextObject,\n} from \"./types\";\n\n/** The maximum traversal depth, counted in visited values, not tree levels. */\nexport const MAX_TRAVERSAL_DEPTH = 64;\n\n/**\n * The element-nesting ceiling a caller observes. `boundNode` visits an element\n * and its `children` array separately, so one level of nesting spends two of\n * {@link MAX_TRAVERSAL_DEPTH}; it runs before the conversion walk and is never\n * the looser of the two. A root array costs one more level than a root object.\n */\nexport const MAX_ELEMENT_DEPTH = MAX_TRAVERSAL_DEPTH / 2;\n\n/**\n * The maximum number of entries kept at any single level (root, or a\n * `children` array at any depth) of the raw spec, before it ever reaches\n * `normalizeSpec`. Bounds a hostile array's reported `length` so the pre-pass\n * can never be made to walk further than this regardless of what the array\n * claims about itself.\n */\nexport const CHILDREN_CAP = 200;\n\n/**\n * The total number of nodes the pre-normalization bounding walk will visit\n * across one call, regardless of how many times a shared reference recurs.\n * Bounds the combinatorial work a DAG of shared or self-referential nodes\n * would otherwise force even though each individual array stays within\n * {@link CHILDREN_CAP}.\n */\nexport const NODE_BUDGET = 5000;\n\n/** The block limit for a Slack message. */\nexport const MESSAGE_BLOCK_CAP = 50;\n\n/** The block limit for a Slack modal. */\nexport const MODAL_BLOCK_CAP = 100;\n\n/**\n * The maximum number of top-level blocks accepted when parsing Slack Block\n * Kit JSON back into generative UI, guarding against a hostile or malformed\n * payload.\n */\nexport const INBOUND_BLOCK_CAP = 200;\n\n/** The character limit for header text. */\nexport const HEADER_TEXT_CAP = 150;\n\n/** The character limit for section text. */\nexport const SECTION_TEXT_CAP = 3000;\n\n/** The maximum number of fields in one section. */\nexport const FACT_FIELD_CAP = 10;\n\n/** The character limit for one section field. */\nexport const FACT_FIELD_TEXT_CAP = 2000;\n\n/** The character limit for button labels and option labels. */\nexport const INTERACTIVE_TEXT_CAP = 75;\n\n/** The character limit for an action_id. */\nexport const ACTION_ID_CAP = 255;\n\n/** The character limit for a button's serialized value payload. */\nexport const BUTTON_VALUE_CAP = 2000;\n\n/** The maximum number of elements in one actions block. */\nexport const ACTIONS_ELEMENT_CAP = 25;\n\n/** The maximum number of options in a static select. */\nexport const SELECT_OPTION_CAP = 100;\n\n/** The maximum number of options in a radio-button group. */\nexport const RADIO_OPTION_CAP = 10;\n\n/** The character limit for a select or input placeholder. */\nexport const PLACEHOLDER_TEXT_CAP = 150;\n\n/** The character limit for an input's label. */\nexport const INPUT_LABEL_CAP = 2000;\n\n/** The maximum number of elements in one context block. */\nexport const CONTEXT_ELEMENT_CAP = 10;\n\n/** The character limit for one context element's text. */\nexport const CONTEXT_TEXT_CAP = 3000;\n\n/** The character limit for card titles and subtitles. */\nexport const CARD_TITLE_CAP = 150;\n\n/** The character limit for a card's body text. */\nexport const CARD_BODY_CAP = 200;\n\n/** The character limit for a card's subtext. */\nexport const CARD_SUBTEXT_CAP = 200;\n\n/** The maximum number of buttons in a card's actions. */\nexport const CARD_ACTIONS_CAP = 3;\n\n/** The minimum number of elements in a renderable carousel. */\nexport const CAROUSEL_CARD_MIN = 1;\n\n/** The maximum number of elements in a carousel. */\nexport const CAROUSEL_CARD_CAP = 10;\n\n/** The character limit for an alert's text. */\nexport const ALERT_TEXT_CAP = 200;\n\n/**\n * The maximum number of data rows in a data-table block; the emitted `rows`\n * array additionally carries one header row.\n */\nexport const DATA_TABLE_ROW_CAP = 200;\n\n/** The maximum number of columns in a data-table block. */\nexport const DATA_TABLE_COLUMN_CAP = 20;\n\n/**\n * The cumulative character limit (summed across every cell's text) shared by\n * every data-table block in one payload.\n */\nexport const DATA_TABLE_CHAR_BUDGET = 20000;\n\n/** The caption emitted on every native data-table block. */\nexport const TABLE_CAPTION = \"Table\";\n\n/** The cumulative character limit for native markdown blocks. */\nexport const MARKDOWN_TEXT_BUDGET = 12000;\n\n/** Builds the message-surface fallback for an alert. */\nexport function buildAlertFallback(\n title: string,\n description: string,\n tone: \"info\" | \"success\" | \"warning\" | \"danger\",\n): readonly [SlackContextBlock, SlackSectionBlock] {\n const emoji = {\n info: \"ℹ️\",\n success: \"✅\",\n warning: \"⚠️\",\n danger: \"🛑\",\n }[tone];\n return [\n {\n type: \"context\",\n elements: [\n { type: \"mrkdwn\", text: title ? `${emoji} *${title}*` : emoji },\n ],\n },\n { type: \"section\", text: { type: \"mrkdwn\", text: description } },\n ];\n}\n\n/** Builds a 2026 Slack modal-only alert block, mapping `tone` to its `level`. */\nexport function buildAlertBlock(\n text: string,\n tone: \"info\" | \"success\" | \"warning\" | \"danger\",\n): SlackAlertBlock {\n const level = tone === \"danger\" ? \"error\" : tone;\n return { type: \"alert\", text: { type: \"mrkdwn\", text }, level };\n}\n\ntype CardBlockFields = {\n readonly heroImage?: SlackImageBlock;\n readonly title?: SlackTextObject;\n readonly subtitle?: SlackTextObject;\n readonly body?: SlackTextObject;\n readonly subtext?: SlackTextObject;\n readonly actions?: readonly SlackButtonElement[];\n};\n\n/** Builds a 2026 Slack card block from its already-clamped fields. */\nexport function buildCardBlock(fields: CardBlockFields): SlackCardBlock {\n const { heroImage, title, subtitle, body, subtext, actions } = fields;\n return {\n type: \"card\",\n ...(heroImage !== undefined ? { hero_image: heroImage } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(subtitle !== undefined ? { subtitle } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(subtext !== undefined ? { subtext } : {}),\n ...(actions !== undefined ? { actions } : {}),\n };\n}\n\n/** Builds a 2026 Slack carousel block. */\nexport function buildCarouselBlock(\n elements: readonly SlackCardBlock[],\n): SlackCarouselBlock {\n return { type: \"carousel\", elements };\n}\n\n/** Builds a 2026 Slack data-table block; `rows[0]` must be the header row. */\nexport function buildDataTableBlock(\n rows: readonly (readonly SlackDataTableCell[])[],\n): SlackDataTableBlock {\n return { type: \"data_table\", caption: TABLE_CAPTION, rows };\n}\n\n/** Builds the final note used when a surface block budget is exceeded. */\nexport function buildBlockBudgetNote(omitted: number): SlackContextBlock {\n return {\n type: \"context\",\n elements: [\n {\n type: \"mrkdwn\",\n text: `${omitted} ${omitted === 1 ? \"block was\" : \"blocks were\"} omitted.`,\n },\n ],\n };\n}\n"],"mappings":";;AAcA,MAAa,sBAAsB;;;;;;;AAQnC,MAAa,oBAAA;;;;;;;;AASb,MAAa,eAAe;;;;;;;;AAS5B,MAAa,cAAc;;AAG3B,MAAa,oBAAoB;;AAGjC,MAAa,kBAAkB;;;;;;AAO/B,MAAa,oBAAoB;;AAGjC,MAAa,kBAAkB;;AAG/B,MAAa,mBAAmB;;AAGhC,MAAa,iBAAiB;;AAG9B,MAAa,sBAAsB;;AAGnC,MAAa,uBAAuB;;AAGpC,MAAa,gBAAgB;;AAG7B,MAAa,mBAAmB;;AAGhC,MAAa,sBAAsB;;AAGnC,MAAa,oBAAoB;;AAGjC,MAAa,mBAAmB;;AAGhC,MAAa,uBAAuB;;AAGpC,MAAa,kBAAkB;;AAG/B,MAAa,sBAAsB;;AAGnC,MAAa,mBAAmB;;AAGhC,MAAa,iBAAiB;;AAG9B,MAAa,gBAAgB;;AAG7B,MAAa,mBAAmB;;AAGhC,MAAa,mBAAmB;;AAGhC,MAAa,oBAAoB;;AAGjC,MAAa,oBAAoB;;AAGjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,qBAAqB;;AAGlC,MAAa,wBAAwB;;;;;AAMrC,MAAa,yBAAyB;;AAGtC,MAAa,gBAAgB;;AAG7B,MAAa,uBAAuB;;AAGpC,SAAgB,mBACd,OACA,aACA,MACiD;CACjD,MAAM,QAAQ;EACZ,MAAM;EACN,SAAS;EACT,SAAS;EACT,QAAQ;CACV,EAAE;CACF,OAAO,CACL;EACE,MAAM;EACN,UAAU,CACR;GAAE,MAAM;GAAU,MAAM,QAAQ,GAAG,MAAM,IAAI,MAAM,KAAK;EAAM,CAChE;CACF,GACA;EAAE,MAAM;EAAW,MAAM;GAAE,MAAM;GAAU,MAAM;EAAY;CAAE,CACjE;AACF;;AAGA,SAAgB,gBACd,MACA,MACiB;CAEjB,OAAO;EAAE,MAAM;EAAS,MAAM;GAAE,MAAM;GAAU;EAAK;EAAG,OAD1C,SAAS,WAAW,UAAU;CACkB;AAChE;;AAYA,SAAgB,eAAe,QAAyC;CACtE,MAAM,EAAE,WAAW,OAAO,UAAU,MAAM,SAAS,YAAY;CAC/D,OAAO;EACL,MAAM;EACN,GAAI,cAAc,KAAA,IAAY,EAAE,YAAY,UAAU,IAAI,CAAC;EAC3D,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACvC,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC7C,GAAI,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,CAAC;EACrC,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;EAC3C,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;CAC7C;AACF;;AAGA,SAAgB,mBACd,UACoB;CACpB,OAAO;EAAE,MAAM;EAAY;CAAS;AACtC;;AAGA,SAAgB,oBACd,MACqB;CACrB,OAAO;EAAE,MAAM;EAAc,SAAS;EAAe;CAAK;AAC5D;;AAGA,SAAgB,qBAAqB,SAAoC;CACvE,OAAO;EACL,MAAM;EACN,UAAU,CACR;GACE,MAAM;GACN,MAAM,GAAG,QAAQ,GAAG,YAAY,IAAI,cAAc,cAAc;EAClE,CACF;CACF;AACF"}
@@ -308,8 +308,8 @@ const dataTableCellValue = (cell) => {
308
308
  return text;
309
309
  };
310
310
  const dataTableFrom = (block, warnings) => {
311
- const { items: rows, truncated: rowsTruncated } = clampArray(block["rows"], 101);
312
- if (rowsTruncated) warn(warnings, "clamped", "Table", `rows were clamped to 100 entries.`);
311
+ const { items: rows, truncated: rowsTruncated } = clampArray(block["rows"], 201);
312
+ if (rowsTruncated) warn(warnings, "clamped", "Table", `rows were clamped to 200 entries.`);
313
313
  const [headerRow, ...dataRows] = rows;
314
314
  const { items: headerCells, truncated: headerTruncated } = clampArray(headerRow, 20);
315
315
  let columnsTruncated = headerTruncated;
@@ -1 +1 @@
1
- {"version":3,"file":"toSlackBlocks.d.ts","names":[],"sources":["../../src/slack/toSlackBlocks.ts"],"mappings":";;;iBAgvCgB,cACd,eACA,UAAU,uBACT"}
1
+ {"version":3,"file":"toSlackBlocks.d.ts","names":[],"sources":["../../src/slack/toSlackBlocks.ts"],"mappings":";;;iBA64CgB,cACd,eACA,UAAU,uBACT"}
@@ -8,6 +8,17 @@ const INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
8
8
  "Checkbox",
9
9
  "RadioGroup"
10
10
  ]);
11
+ /**
12
+ * Types whose control disappears when a card is reshaped to text. `Input` and
13
+ * `Form` are not in {@link INTERACTIVE_TYPES} because they emit their own
14
+ * block rather than an actions element, but a reshape loses them the same way,
15
+ * and a `Form` gets a Submit button whether or not it carries an action.
16
+ */
17
+ const CONTROL_TYPES = /* @__PURE__ */ new Set([
18
+ ...INTERACTIVE_TYPES,
19
+ "Input",
20
+ "Form"
21
+ ]);
11
22
  const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
12
23
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
24
  const isElement = (node) => isRecord(node);
@@ -62,6 +73,11 @@ const optionFrom = (value, component, context) => {
62
73
  value: value["value"]
63
74
  };
64
75
  };
76
+ const warnDroppedOptions = (kept, taken, component, context) => {
77
+ const dropped = taken - kept;
78
+ if (dropped === 0) return;
79
+ warn(context, "dropped", component, `${dropped} ${dropped === 1 ? "option was" : "options were"} dropped for want of a string label and value.`);
80
+ };
65
81
  const toActionElement = (element, context) => {
66
82
  const { props, action } = element;
67
83
  switch (element.type) {
@@ -69,7 +85,9 @@ const toActionElement = (element, context) => {
69
85
  case "Select": {
70
86
  const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
71
87
  if (rawOptions.length > 100) warn(context, "clamped", "Select", `options were clamped to 100 entries.`);
72
- const options = rawOptions.slice(0, 100).map((option) => optionFrom(option, "Select", context)).filter((option) => option !== void 0);
88
+ const takenOptions = rawOptions.slice(0, 100);
89
+ const options = takenOptions.map((option) => optionFrom(option, "Select", context)).filter((option) => option !== void 0);
90
+ warnDroppedOptions(options.length, takenOptions.length, "Select", context);
73
91
  const placeholder = clampText(asString(props["placeholder"]), 150, "Select", "placeholder", context);
74
92
  return {
75
93
  type: "static_select",
@@ -105,7 +123,9 @@ const toActionElement = (element, context) => {
105
123
  case "RadioGroup": {
106
124
  const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
107
125
  if (rawOptions.length > 10) warn(context, "clamped", "RadioGroup", `options were clamped to 10 entries.`);
108
- const options = rawOptions.slice(0, 10).map((option) => optionFrom(option, "RadioGroup", context)).filter((option) => option !== void 0);
126
+ const takenOptions = rawOptions.slice(0, 10);
127
+ const options = takenOptions.map((option) => optionFrom(option, "RadioGroup", context)).filter((option) => option !== void 0);
128
+ warnDroppedOptions(options.length, takenOptions.length, "RadioGroup", context);
109
129
  const selectedValue = typeof props["value"] === "string" ? props["value"] : asString(props["defaultValue"]);
110
130
  const initialOption = options.find((option) => option.value === selectedValue);
111
131
  return {
@@ -267,19 +287,56 @@ const convertCard = (element, context, depth) => {
267
287
  }
268
288
  return [card];
269
289
  };
290
+ /** The content kinds a reshaped carousel card cannot carry, in report order. */
291
+ const LOST_CONTENT_KINDS = [
292
+ "images",
293
+ "tables",
294
+ "charts",
295
+ "controls"
296
+ ];
297
+ const listPhrase = (items) => items.length <= 1 ? items[0] ?? "" : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
298
+ /**
299
+ * Collects the kinds of content a reshape drops, walking the same tree
300
+ * {@link collectText} does so a node nested below the top level counts too.
301
+ * These are exactly the pieces text cannot represent: an image has no text, a
302
+ * table and a chart carry theirs in array props, and a control is behavior,
303
+ * whether or not it carries an action of its own. An `$action` counts only on
304
+ * a node that renders a control from it, since a `Box` or a `Row` carrying one
305
+ * renders no control on the clean path either.
306
+ */
307
+ const scanLostContent = (node, into, depth) => {
308
+ if (depth > 64) return;
309
+ if (Array.isArray(node)) {
310
+ for (const child of node) scanLostContent(child, into, depth + 1);
311
+ return;
312
+ }
313
+ if (node === void 0 || !isElement(node)) return;
314
+ if (CONTROL_TYPES.has(node.type) || node.type === "ListViewItem" && isRecord(node.action) || node.type === "Card" && (isRecord(node.props["confirm"]) || isRecord(node.props["cancel"]))) into.add("controls");
315
+ if (node.type === "Image") into.add("images");
316
+ if (node.type === "Table") into.add("tables");
317
+ if (node.type === "Chart") into.add("charts");
318
+ scanLostContent(node.children, into, depth + 1);
319
+ };
270
320
  /**
271
321
  * Degrades a card that cannot map cleanly into a title-and-body card block.
272
- * A carousel cannot fall back to a block sequence like a standalone card
273
- * can, so its content is clamped down to fit the card shape instead.
322
+ * A carousel cannot fall back to a block sequence like a standalone card can,
323
+ * so the card is reshaped to the two text fields the block has. Text reachable
324
+ * by {@link collectText} survives at any depth; an image, a table, a chart,
325
+ * and a control do not, and are reported separately from the reshape itself.
274
326
  */
275
327
  const degradeCard = (element, context, depth) => {
276
328
  const rawTitle = asString(element.props["title"]);
277
329
  const textChunks = collectText(element.children, depth + 1);
278
330
  const titleSource = rawTitle || textChunks[0] || "";
279
331
  const bodyChunks = rawTitle ? textChunks : textChunks.slice(1);
280
- const titleText = titleSource.slice(0, 150);
281
- const bodyText = bodyChunks.join("\n").slice(0, 200);
282
- warn(context, "clamped", "Card", "A card inside a carousel was degraded to title and body.");
332
+ const titleText = clampText(titleSource, 150, "Card", "title", context);
333
+ const bodyText = clampText(bodyChunks.join("\n"), 200, "Card", "body", context);
334
+ warn(context, "fallback", "Card", "A card inside a carousel was reshaped to title and body.");
335
+ const lostKinds = /* @__PURE__ */ new Set();
336
+ if (isRecord(element.props["confirm"]) || isRecord(element.props["cancel"])) lostKinds.add("controls");
337
+ scanLostContent(element.children, lostKinds, depth + 1);
338
+ const lost = LOST_CONTENT_KINDS.filter((kind) => lostKinds.has(kind));
339
+ if (lost.length > 0) warn(context, "dropped", "Card", `A reshaped carousel card's ${listPhrase(lost)} were dropped.`);
283
340
  return buildCardBlock({
284
341
  ...titleText ? { title: {
285
342
  type: "mrkdwn",
@@ -326,13 +383,47 @@ const convertListItem = (element, context, depth) => {
326
383
  ...accessory !== void 0 ? { accessory } : {}
327
384
  };
328
385
  };
386
+ /**
387
+ * Converts a child whose output is thrown away, and reports whether anything
388
+ * was lost with it. A scratch context keeps the throwaway out of the shared
389
+ * markdown and data-table budgets that surviving blocks still need; only its
390
+ * `dropped` warnings are forwarded, because those describe the tree the caller
391
+ * wrote, while a clamp or a fallback would describe content never delivered.
392
+ */
393
+ const discardedChild = (child, context, depth) => {
394
+ const scratch = {
395
+ ...context,
396
+ warnings: []
397
+ };
398
+ const produced = convertSequence(child, scratch, depth).length > 0;
399
+ const lost = scratch.warnings.filter((warning) => warning.code === "dropped");
400
+ context.warnings.push(...lost);
401
+ return produced || lost.length > 0;
402
+ };
329
403
  const convertListView = (element, context, depth) => {
330
- return normalizedList(element.children).filter((child) => isElement(child) && child.type === "ListViewItem").flatMap((item, index) => [...index > 0 ? [{ type: "divider" }] : [], convertListItem(item, context, depth + 1)]);
404
+ const items = [];
405
+ let discarded = 0;
406
+ for (const child of normalizedList(element.children)) {
407
+ if (isElement(child) && child.type === "ListViewItem") {
408
+ items.push(child);
409
+ continue;
410
+ }
411
+ if (discardedChild(child, context, depth + 1)) discarded += 1;
412
+ }
413
+ if (discarded > 0) warn(context, "dropped", "ListView", `${discarded} non-item ${discarded === 1 ? "child was" : "children were"} dropped.`);
414
+ return items.flatMap((item, index) => [...index > 0 ? [{ type: "divider" }] : [], convertListItem(item, context, depth + 1)]);
331
415
  };
332
416
  const convertCarousel = (element, context, depth) => {
333
417
  const cardChildren = [];
334
- for (const child of normalizedList(element.children)) if (isElement(child) && child.type === "Card") cardChildren.push(child);
335
- else warn(context, "dropped", "Carousel", "A non-card child was dropped.");
418
+ let droppedCards = 0;
419
+ for (const child of normalizedList(element.children)) {
420
+ if (isElement(child) && child.type === "Card") {
421
+ cardChildren.push(child);
422
+ continue;
423
+ }
424
+ if (discardedChild(child, context, depth + 1)) droppedCards += 1;
425
+ }
426
+ if (droppedCards > 0) warn(context, "dropped", "Carousel", `${droppedCards} non-card ${droppedCards === 1 ? "child was" : "children were"} dropped.`);
336
427
  if (cardChildren.length > 10) warn(context, "clamped", "Carousel", `cards were clamped to 10 entries.`);
337
428
  const cards = cardChildren.slice(0, 10).map((card) => convertCarouselCard(card, context, depth + 1)).filter((card) => card !== void 0);
338
429
  if (cards.length < 1) {
@@ -356,12 +447,15 @@ const convertTable = (element, context) => {
356
447
  const rawColumns = Array.isArray(element.props["columns"]) ? element.props["columns"] : [];
357
448
  const rawRows = Array.isArray(element.props["rows"]) ? element.props["rows"] : [];
358
449
  if (rawColumns.length > 20) warn(context, "clamped", "Table", `columns were clamped to 20 entries.`);
359
- if (rawRows.length > 100) warn(context, "clamped", "Table", `rows were clamped to 100 entries.`);
360
- const columnHeaderRow = rawColumns.slice(0, 20).filter(isRecord).map((column) => ({
450
+ if (rawRows.length > 200) warn(context, "clamped", "Table", `rows were clamped to 200 entries.`);
451
+ const takenColumns = rawColumns.slice(0, 20);
452
+ const unlabeled = takenColumns.filter((column) => !isRecord(column) || typeof column["label"] !== "string").length;
453
+ if (unlabeled > 0) warn(context, "dropped", "Table", `${unlabeled} column ${unlabeled === 1 ? "header was" : "headers were"} left blank for want of a string label.`);
454
+ const columnHeaderRow = takenColumns.map((column) => ({
361
455
  type: "raw_text",
362
- text: asString(column["label"])
456
+ text: isRecord(column) ? asString(column["label"]) : ""
363
457
  }));
364
- const dataRows = rawRows.slice(0, 100).map((row) => (Array.isArray(row) ? row : []).slice(0, 20).map((value) => toDataTableCell(value) ?? {
458
+ const dataRows = rawRows.slice(0, 200).map((row) => (Array.isArray(row) ? row : []).slice(0, 20).map((value) => toDataTableCell(value) ?? {
365
459
  type: "raw_text",
366
460
  text: ""
367
461
  }));
@@ -372,7 +466,7 @@ const convertTable = (element, context) => {
372
466
  }))];
373
467
  const headerRow = padRow(columnHeaderRow);
374
468
  const headerCharacters = rowCharacters(headerRow);
375
- if (context.dataTableCharacters + headerCharacters > 1e4) {
469
+ if (context.dataTableCharacters + headerCharacters > 2e4) {
376
470
  warn(context, "dropped", "Table", `the table was dropped because its header row exceeds the ${DATA_TABLE_CHAR_BUDGET}-character table budget.`);
377
471
  return [];
378
472
  }
@@ -381,7 +475,7 @@ const convertTable = (element, context) => {
381
475
  let budgetClamped = false;
382
476
  for (const row of dataRows.map(padRow)) {
383
477
  const chars = rowCharacters(row);
384
- if (context.dataTableCharacters + tableCharacters + chars > 1e4) {
478
+ if (context.dataTableCharacters + tableCharacters + chars > 2e4) {
385
479
  budgetClamped = true;
386
480
  break;
387
481
  }
@@ -616,10 +710,11 @@ function toSlackBlocks(node, options) {
616
710
  dataTableCharacters: 0
617
711
  };
618
712
  try {
619
- const { root } = normalizeSpec(boundSpec(node, (reason) => {
620
- const detail = reason === "budget" ? `the tree was truncated after ${NODE_BUDGET} nodes.` : reason === "cycle" ? "a self-referencing node was dropped." : reason === "depth" ? `nodes deeper than 64 levels were dropped.` : `children were clamped to 200 entries.`;
713
+ const bounded = boundSpec(node, (reason) => {
714
+ const detail = reason === "budget" ? `the tree was truncated after ${NODE_BUDGET} nodes.` : reason === "cycle" ? "a self-referencing node was dropped." : reason === "depth" ? `nodes deeper than 32 levels were dropped.` : `children were clamped to 200 entries.`;
621
715
  warn(context, "clamped", "Root", detail);
622
- }));
716
+ });
717
+ const { root } = normalizeSpec(bounded);
623
718
  const converted = convertSequence(root, context, 0);
624
719
  if (converted.length <= context.blockCap) return {
625
720
  blocks: converted,