@beeuni/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +19 -10
  2. package/dist/beeuni.esm.min.js +37 -35
  3. package/dist/beeuni.global.js +37 -35
  4. package/dist/browser/cache/idb-sync-cache.js +15 -5
  5. package/dist/browser/session-sync/server-transcript-mirror.d.ts +7 -1
  6. package/dist/browser/session-sync/server-transcript-mirror.js +30 -24
  7. package/dist/browser/session-sync/session-sync-registry.d.ts +3 -1
  8. package/dist/browser/session-sync/session-sync-registry.js +48 -3
  9. package/dist/browser/stores/sync-store/keep-unchanged.d.ts +4 -0
  10. package/dist/browser/stores/sync-store/keep-unchanged.js +34 -0
  11. package/dist/browser/stores/sync-store.d.ts +1 -0
  12. package/dist/browser/stores/sync-store.js +72 -16
  13. package/dist/core/attachments/prompt-attachments.d.ts +4 -0
  14. package/dist/core/attachments/prompt-attachments.js +34 -0
  15. package/dist/core/capture/rules.d.ts +17 -0
  16. package/dist/core/capture/rules.js +27 -0
  17. package/dist/core/client/beeuni.d.ts +92 -5
  18. package/dist/core/client/beeuni.js +2 -0
  19. package/dist/core/client/capture.d.ts +105 -0
  20. package/dist/core/client/capture.js +48 -0
  21. package/dist/core/client/facade-identity.d.ts +1 -0
  22. package/dist/core/client/facade-identity.js +1 -0
  23. package/dist/core/client/project-access-resources.d.ts +3 -1
  24. package/dist/core/client/project-access-resources.js +1 -1
  25. package/dist/core/client/project-operations-admin.d.ts +4 -4
  26. package/dist/core/client/project-operations-review.d.ts +2 -3
  27. package/dist/core/client/project-operations-review.js +1 -0
  28. package/dist/core/client/project-operations-workspace.d.ts +4 -4
  29. package/dist/core/client/project-operations-workspace.js +1 -1
  30. package/dist/core/client/project-operations.d.ts +5 -4
  31. package/dist/core/client/session-context.d.ts +2 -0
  32. package/dist/core/client/session-handle.d.ts +13 -0
  33. package/dist/core/client/session-lifecycle-resources.d.ts +11 -0
  34. package/dist/core/client/session-lifecycle-resources.js +4 -0
  35. package/dist/core/client/session-lifecycle-security.d.ts +2 -0
  36. package/dist/core/client/session-lifecycle-security.js +1 -0
  37. package/dist/core/client/session-readiness.d.ts +2 -0
  38. package/dist/core/client/session-readiness.js +20 -1
  39. package/dist/core/http/api-client.d.ts +1 -0
  40. package/dist/core/http/api-client.js +1 -0
  41. package/dist/core/http/config.d.ts +2 -0
  42. package/dist/core/http/response-error.js +26 -11
  43. package/dist/core/http/transport.js +6 -1
  44. package/dist/core/models/composer-model.d.ts +14 -0
  45. package/dist/core/models/composer-model.js +21 -1
  46. package/dist/core/rest/platform-client/capture.d.ts +363 -0
  47. package/dist/core/rest/platform-client/capture.js +116 -0
  48. package/dist/core/rest/platform-client/index.d.ts +1 -0
  49. package/dist/core/rest/platform-client/index.js +1 -0
  50. package/dist/core/rest/platform-client/lifecycle.js +18 -12
  51. package/dist/core/rest/platform-client/shared.d.ts +5 -0
  52. package/dist/core/rest/platform-client/shared.js +18 -0
  53. package/dist/core/rest/projects-client/account-secret-resources.d.ts +2 -1
  54. package/dist/core/rest/projects-client/accounts.d.ts +34 -0
  55. package/dist/core/rest/projects-client/accounts.js +10 -0
  56. package/dist/core/rest/projects-client/admin-providers.d.ts +0 -61
  57. package/dist/core/rest/projects-client/admin-providers.js +0 -6
  58. package/dist/core/rest/projects-client/agent-config.d.ts +3 -0
  59. package/dist/core/rest/projects-client/apps-fleet.d.ts +5 -1
  60. package/dist/core/rest/projects-client/apps-fleet.js +2 -2
  61. package/dist/core/rest/projects-client/apps.d.ts +3 -0
  62. package/dist/core/rest/projects-client/change-requests.d.ts +2 -0
  63. package/dist/core/rest/projects-client/change-requests.js +2 -0
  64. package/dist/core/rest/projects-client/connectors.d.ts +7 -0
  65. package/dist/core/rest/projects-client/connectors.js +3 -0
  66. package/dist/core/rest/projects-client/files.d.ts +3 -0
  67. package/dist/core/rest/projects-client/files.js +4 -0
  68. package/dist/core/rest/projects-client/gateway.d.ts +5 -0
  69. package/dist/core/rest/projects-client/iam-actions.d.ts +76 -0
  70. package/dist/core/rest/projects-client/iam-actions.js +101 -0
  71. package/dist/core/rest/projects-client/iam.d.ts +1 -0
  72. package/dist/core/rest/projects-client/iam.js +1 -0
  73. package/dist/core/rest/projects-client/notifications.d.ts +1 -1
  74. package/dist/core/rest/projects-client/notifications.js +4 -0
  75. package/dist/core/rest/projects-client/projects.d.ts +3 -2
  76. package/dist/core/rest/projects-client/projects.js +2 -3
  77. package/dist/core/rest/projects-client/review-items.d.ts +2 -0
  78. package/dist/core/rest/projects-client/review-items.js +4 -0
  79. package/dist/core/rest/projects-client/review.d.ts +17 -3
  80. package/dist/core/rest/projects-client/review.js +11 -0
  81. package/dist/core/rest/projects-client/secrets.d.ts +3 -0
  82. package/dist/core/rest/projects-client/session-attachments.js +14 -1
  83. package/dist/core/rest/projects-client/sessions.d.ts +29 -2
  84. package/dist/core/rest/projects-client/sessions.js +34 -4
  85. package/dist/core/runtime/runtime-rest-client.js +1 -1
  86. package/dist/core/runtime/runtime-types.d.ts +1 -0
  87. package/dist/core/session/open-bundle.d.ts +4 -4
  88. package/dist/core/session/open-bundle.js +29 -46
  89. package/dist/core/session/turn-end-watch.d.ts +47 -0
  90. package/dist/core/session/turn-end-watch.js +141 -0
  91. package/dist/core/session-log/index.d.ts +4 -0
  92. package/dist/core/session-log/index.js +3 -0
  93. package/dist/core/session-log/tool-call.d.ts +7 -2
  94. package/dist/core/session-log/tool-call.js +25 -6
  95. package/dist/core/session-log/turns/blocks.d.ts +60 -0
  96. package/dist/core/session-log/turns/blocks.js +124 -0
  97. package/dist/core/session-log/turns/history.d.ts +32 -0
  98. package/dist/core/session-log/turns/history.js +99 -0
  99. package/dist/core/session-log/turns/index.d.ts +4 -0
  100. package/dist/core/session-log/turns/index.js +4 -0
  101. package/dist/core/session-log/turns/metrics.d.ts +21 -0
  102. package/dist/core/session-log/turns/metrics.js +29 -0
  103. package/dist/core/session-log/turns/turn.d.ts +55 -0
  104. package/dist/core/session-log/turns/turn.js +279 -0
  105. package/dist/core/session-log/turns/v1-parts.d.ts +8 -0
  106. package/dist/core/session-log/turns/v1-parts.js +43 -0
  107. package/dist/core/session-log/v1-view.d.ts +5 -2
  108. package/dist/core/session-log/v1-view.js +86 -22
  109. package/dist/core/session-log/v1.js +10 -3
  110. package/dist/core/session-sync/saved-transcript.d.ts +15 -1
  111. package/dist/core/session-sync/saved-transcript.js +55 -0
  112. package/dist/core/session-sync/session-sync-controller.js +3 -0
  113. package/dist/core/stream/event-stream.js +3 -3
  114. package/dist/core/turns/classify.js +4 -2
  115. package/dist/core/turns/edit-blocks.d.ts +1 -0
  116. package/dist/core/turns/edit-blocks.js +15 -0
  117. package/dist/core/turns/segments/burst-summary.d.ts +1 -0
  118. package/dist/core/turns/segments/burst-summary.js +24 -0
  119. package/dist/core/turns/segments/group-steps.d.ts +4 -3
  120. package/dist/core/turns/segments/group-steps.js +9 -6
  121. package/dist/core/turns/segments/narration.d.ts +5 -4
  122. package/dist/core/turns/segments/narration.js +12 -3
  123. package/dist/core/turns/segments/session-activity-groups.d.ts +2 -1
  124. package/dist/core/turns/segments/session-activity-groups.js +3 -1
  125. package/dist/core/turns/segments/step-label.d.ts +2 -1
  126. package/dist/core/turns/segments/step-label.js +3 -1
  127. package/dist/core/turns/segments/working-turn.d.ts +4 -2
  128. package/dist/core/turns/segments/working-turn.js +9 -8
  129. package/dist/core/turns/tool-kind.d.ts +1 -1
  130. package/dist/core/turns/tool-kind.js +3 -2
  131. package/dist/core/turns/tool-registry.js +1 -0
  132. package/dist/core/turns/tools/tool-meta.d.ts +1 -1
  133. package/dist/core/turns/tools/tool-meta.js +3 -2
  134. package/dist/core/turns/view-model.js +1 -13
  135. package/dist/genui/catalog.d.ts +28 -0
  136. package/dist/genui/catalog.js +396 -0
  137. package/dist/genui/expansion.d.ts +3 -0
  138. package/dist/genui/expansion.js +295 -0
  139. package/dist/genui/fence-entry.d.ts +2 -0
  140. package/dist/genui/fence-entry.js +1 -0
  141. package/dist/genui/fence.d.ts +21 -0
  142. package/dist/genui/fence.js +138 -0
  143. package/dist/genui/index.d.ts +6 -0
  144. package/dist/genui/index.js +6 -0
  145. package/dist/genui/markdown.d.ts +12 -0
  146. package/dist/genui/markdown.js +104 -0
  147. package/dist/genui/parse.d.ts +6 -0
  148. package/dist/genui/parse.js +75 -0
  149. package/dist/genui/prompt.d.ts +2 -0
  150. package/dist/genui/prompt.js +206 -0
  151. package/dist/genui/react/default-components.d.ts +2 -0
  152. package/dist/genui/react/default-components.js +72 -0
  153. package/dist/genui/react/default-components.test.d.ts +1 -0
  154. package/dist/genui/react/default-components.test.js +94 -0
  155. package/dist/genui/react/genui-block-disabled.test.d.ts +1 -0
  156. package/dist/genui/react/genui-block-disabled.test.js +50 -0
  157. package/dist/genui/react/genui-block.d.ts +28 -0
  158. package/dist/genui/react/genui-block.js +91 -0
  159. package/dist/genui/react/genui-block.test.d.ts +1 -0
  160. package/dist/genui/react/genui-block.test.js +205 -0
  161. package/dist/genui/react/index.d.ts +2 -0
  162. package/dist/genui/react/index.js +2 -0
  163. package/dist/genui/share.d.ts +5 -0
  164. package/dist/genui/share.js +30 -0
  165. package/dist/genui/test-fixtures.d.ts +2 -0
  166. package/dist/genui/test-fixtures.js +16 -0
  167. package/dist/genui/types.d.ts +29 -0
  168. package/dist/genui/types.js +1 -0
  169. package/dist/genui/urls.d.ts +1 -0
  170. package/dist/genui/urls.js +18 -0
  171. package/dist/genui/validate.d.ts +9 -0
  172. package/dist/genui/validate.js +116 -0
  173. package/dist/index.d.ts +2 -2
  174. package/dist/index.js +2 -2
  175. package/dist/internal/connector-catalog.d.ts +1 -0
  176. package/dist/platform/ui.d.ts +8 -2
  177. package/dist/platform/ui.js +19 -4
  178. package/dist/react/chat/use-chat-turns.d.ts +1 -1
  179. package/dist/react/human-presence.d.ts +9 -0
  180. package/dist/react/human-presence.js +12 -0
  181. package/dist/react/index.d.ts +4 -0
  182. package/dist/react/index.js +4 -0
  183. package/dist/react/opencode.d.ts +1 -1
  184. package/dist/react/opencode.js +1 -1
  185. package/dist/react/query-contracts.d.ts +6 -0
  186. package/dist/react/query-contracts.js +6 -0
  187. package/dist/react/query-keys.d.ts +17 -1
  188. package/dist/react/query-keys.js +24 -1
  189. package/dist/react/reset-identity-state.js +2 -0
  190. package/dist/react/session-cache-write.d.ts +1 -0
  191. package/dist/react/session-cache-write.js +20 -0
  192. package/dist/react/session-transcript-subscription.js +1 -1
  193. package/dist/react/sign-in-with-beeuni.d.ts +5 -5
  194. package/dist/react/turn-end-handoff.d.ts +13 -0
  195. package/dist/react/turn-end-handoff.js +101 -0
  196. package/dist/react/use-admin-providers.d.ts +0 -3
  197. package/dist/react/use-admin-providers.js +1 -11
  198. package/dist/react/use-capture.d.ts +47 -0
  199. package/dist/react/use-capture.js +173 -0
  200. package/dist/react/use-feature-flag.js +1 -0
  201. package/dist/react/use-opencode-events/handle-interaction-event.js +8 -3
  202. package/dist/react/use-opencode-events/handle-session-event.js +13 -8
  203. package/dist/react/use-opencode-events/handler-context.d.ts +1 -0
  204. package/dist/react/use-opencode-events/helpers.d.ts +1 -0
  205. package/dist/react/use-opencode-events/helpers.js +11 -1
  206. package/dist/react/use-opencode-local.d.ts +5 -1
  207. package/dist/react/use-opencode-local.js +15 -2
  208. package/dist/react/use-opencode-sessions/commands.d.ts +11 -1
  209. package/dist/react/use-opencode-sessions/commands.js +21 -1
  210. package/dist/react/use-opencode-sessions/permissions.js +28 -1
  211. package/dist/react/use-project-sessions.d.ts +1 -1
  212. package/dist/react/use-project-sessions.js +6 -5
  213. package/dist/react/use-prompt-attachments.js +1 -0
  214. package/dist/react/use-review-summary.d.ts +13 -0
  215. package/dist/react/use-review-summary.js +32 -0
  216. package/dist/react/use-runtime-reconnect.js +2 -2
  217. package/dist/react/use-session-list-state.d.ts +6 -0
  218. package/dist/react/use-session-list-state.js +33 -0
  219. package/dist/react/use-session-log.d.ts +3 -2
  220. package/dist/react/use-session-log.js +26 -17
  221. package/dist/react/use-session-messages.d.ts +2 -1
  222. package/dist/react/use-session-messages.js +15 -14
  223. package/dist/react/use-session-stream.js +12 -1
  224. package/dist/react/use-session-sync.d.ts +2 -2
  225. package/dist/react/use-session-sync.js +10 -8
  226. package/dist/react/use-session-transcript-history.js +2 -12
  227. package/dist/react/use-session-working.d.ts +2 -0
  228. package/dist/react/use-session-working.js +10 -0
  229. package/dist/react/use-session.d.ts +6 -0
  230. package/dist/react/use-session.js +87 -7
  231. package/package.json +49 -3
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useState } from 'react';
3
+ import { genuiA11yText } from '../index.js';
4
+ const v = (name, fallback) => `var(--genui-${name}, ${fallback})`;
5
+ const box = {
6
+ border: `1px solid ${v('border', 'rgba(0,0,0,.12)')}`,
7
+ borderRadius: v('radius', '10px'),
8
+ background: v('surface', 'transparent'),
9
+ padding: 12,
10
+ };
11
+ const muted = { color: v('muted', 'rgba(0,0,0,.6)'), fontSize: 13 };
12
+ const TONE = { good: '#15803d', warn: '#b45309', bad: '#b91c1c', neutral: 'inherit' };
13
+ const kids = (value) => (Array.isArray(value) ? value : []);
14
+ function DataTable({ head, rows, caption }) {
15
+ return (_jsxs("table", { style: { borderCollapse: 'collapse', width: '100%', fontSize: 14 }, children: [caption ? _jsx("caption", { style: { ...muted, textAlign: 'left', captionSide: 'bottom', paddingTop: 6 }, children: caption }) : null, _jsx("thead", { children: _jsx("tr", { children: head.map((cell, i) => (_jsx("th", { scope: "col", style: { textAlign: 'left', padding: '6px 8px', borderBottom: `1px solid ${v('border', 'rgba(0,0,0,.12)')}` }, children: cell }, i))) }) }), _jsx("tbody", { children: rows.map((row, r) => (_jsx("tr", { children: head.map((_, c) => (_jsx("td", { style: { padding: '6px 8px' }, children: String(row[c] ?? '') }, c))) }, r))) })] }));
16
+ }
17
+ function seriesRows(labels, series) {
18
+ return labels.map((label, i) => [label, ...series.map((s) => s.props.values?.[i] ?? '')]);
19
+ }
20
+ const Stack = ({ props, renderChild }) => (_jsx("div", { style: { display: 'flex', flexDirection: props.direction === 'row' ? 'row' : 'column', flexWrap: 'wrap', gap: 12 }, children: kids(props.children).map(renderChild) }));
21
+ const Card = ({ props, renderChild }) => (_jsxs("article", { style: box, children: [props.image ? _jsx("img", { src: props.image, alt: "", referrerPolicy: "no-referrer", loading: "lazy", style: { width: '100%', borderRadius: 6, marginBottom: 8 } }) : null, _jsx("h4", { style: { margin: 0 }, children: props.href ? _jsx("a", { href: props.href, target: "_blank", rel: "noopener noreferrer", children: props.title }) : props.title }), props.subtitle ? _jsx("p", { style: muted, children: props.subtitle }) : null, props.body ? _jsx("p", { style: { margin: '6px 0 0' }, children: props.body }) : null, kids(props.badges).length > 0 ? _jsx("div", { style: { display: 'flex', gap: 6, marginTop: 8 }, children: kids(props.badges).map(renderChild) }) : null] }));
22
+ const Stat = ({ props }) => (_jsxs("div", { style: { ...box, minWidth: 120 }, children: [_jsx("div", { style: muted, children: props.label }), _jsxs("div", { style: { fontSize: 22, fontWeight: 600 }, children: [props.value, props.unit ? _jsxs("span", { style: muted, children: [" ", props.unit] }) : null] }), props.delta ? _jsx("div", { style: { color: TONE[props.trend === 'down' ? 'bad' : props.trend === 'up' ? 'good' : 'neutral'] }, children: props.delta }) : null] }));
23
+ const StatRow = ({ props, renderChild }) => (_jsx("div", { style: { display: 'flex', gap: 12, flexWrap: 'wrap' }, children: kids(props.stats).map(renderChild) }));
24
+ const Table = ({ props }) => _jsx(DataTable, { head: props.columns, rows: props.rows, caption: props.caption });
25
+ const Compare = ({ props }) => {
26
+ const items = kids(props.items);
27
+ const specs = props.specs ?? [];
28
+ return (_jsxs("div", { style: box, children: [specs.length > 0 ? (_jsx(DataTable, { head: ['', ...items.map((item) => String(item.props.name))], rows: specs.map((spec, i) => [spec, ...items.map((item) => item.props.values[i] ?? '—')]) })) : null, _jsx("div", { style: { display: 'grid', gridTemplateColumns: `repeat(${items.length}, 1fr)`, gap: 12, marginTop: 8 }, children: items.map((item) => (_jsxs("div", { children: [_jsxs("strong", { children: [String(item.props.name), props.winner === item.props.name ? ' ✓' : ''] }), _jsxs("ul", { style: { margin: '6px 0', paddingLeft: 18 }, children: [(item.props.pros ?? []).map((pro, i) => _jsxs("li", { children: ["+ ", pro] }, `p${i}`)), (item.props.cons ?? []).map((con, i) => _jsxs("li", { children: ["\u2212 ", con] }, `c${i}`))] })] }, item.id))) })] }));
29
+ };
30
+ const RankedList = ({ props }) => (_jsx("ol", { style: { margin: 0, paddingLeft: 20, display: 'grid', gap: 8 }, children: kids(props.items).map((item) => (_jsxs("li", { children: [_jsx("strong", { children: item.props.href ? _jsx("a", { href: String(item.props.href), target: "_blank", rel: "noopener noreferrer", children: String(item.props.title) }) : String(item.props.title) }), item.props.meta ? _jsxs("span", { style: muted, children: [" \u00B7 ", String(item.props.meta)] }) : null, _jsx("div", { children: String(item.props.reason) })] }, item.id))) }));
31
+ const Steps = ({ props }) => (_jsx("ol", { style: { margin: 0, paddingLeft: 20, display: 'grid', gap: 8 }, children: kids(props.steps).map((step) => (_jsxs("li", { children: [step.props.image && !step.partial ? (_jsx("img", { src: String(step.props.image), alt: "", width: 64, height: 64, loading: "lazy", referrerPolicy: "no-referrer", style: { objectFit: 'cover', borderRadius: 6 } })) : null, _jsx("strong", { children: String(step.props.title) }), step.props.meta ? _jsxs("span", { style: muted, children: [" \u00B7 ", String(step.props.meta)] }) : null, step.props.text ? _jsx("div", { style: { whiteSpace: 'pre-line' }, children: String(step.props.text) }) : null] }, step.id))) }));
32
+ const Checklist = ({ props }) => (_jsxs("div", { style: box, children: [_jsx("strong", { children: props.title }), _jsx("ul", { style: { listStyle: 'none', margin: 0, padding: 0 }, children: (props.items ?? []).map((item, i) => (_jsx("li", { children: _jsxs("label", { children: [_jsx("input", { type: "checkbox" }), " ", item] }) }, i))) })] }));
33
+ const SeriesChart = ({ node, props }) => {
34
+ const labels = props.categories ?? props.x ?? [];
35
+ const series = kids(props.series);
36
+ return (_jsx("figure", { style: { ...box, margin: 0 }, "aria-label": genuiA11yText(node) ?? undefined, children: _jsx(DataTable, { head: ['', ...series.map((s) => String(s.props.name))], rows: seriesRows(labels, series), caption: `Source: ${props.source}` }) }));
37
+ };
38
+ const PieChart = ({ node, props }) => (_jsx("figure", { style: { ...box, margin: 0 }, "aria-label": genuiA11yText(node) ?? undefined, children: _jsx(DataTable, { head: ['', props.unit ?? ''], rows: kids(props.slices).map((s) => [s.props.label, s.props.value]), caption: `Source: ${props.source}` }) }));
39
+ const Map = ({ node, props }) => (_jsxs("figure", { style: { ...box, margin: 0 }, "aria-label": genuiA11yText(node) ?? undefined, children: [_jsx("ul", { style: { margin: 0, paddingLeft: 18 }, children: kids(props.markers).map((m) => (_jsxs("li", { children: [_jsx("a", { href: `https://www.openstreetmap.org/?mlat=${m.props.lat}&mlon=${m.props.lng}#map=15/${m.props.lat}/${m.props.lng}`, target: "_blank", rel: "noopener noreferrer", children: String(m.props.label) }), m.props.description ? ` — ${String(m.props.description)}` : ''] }, m.id))) }), _jsxs("figcaption", { style: muted, children: ["Source: ", props.source] })] }));
40
+ const Tabs = ({ props, renderChild }) => {
41
+ const tabs = kids(props.tabs);
42
+ const [active, setActive] = useState(0);
43
+ const base = useId();
44
+ const current = tabs[Math.min(active, tabs.length - 1)];
45
+ return (_jsxs("div", { children: [_jsx("div", { role: "tablist", style: { display: 'flex', gap: 4, borderBottom: `1px solid ${v('border', 'rgba(0,0,0,.12)')}` }, children: tabs.map((tab, i) => (_jsx("button", { role: "tab", id: `${base}-t${i}`, "aria-selected": i === active, "aria-controls": `${base}-p${i}`, onClick: () => setActive(i), style: { padding: '6px 10px', border: 0, background: 'none', borderBottom: i === active ? `2px solid ${v('accent', 'currentColor')}` : '2px solid transparent', cursor: 'pointer' }, children: String(tab.props.label) }, tab.id))) }), current ? (_jsx("div", { role: "tabpanel", id: `${base}-p${active}`, "aria-labelledby": `${base}-t${active}`, style: { paddingTop: 12, display: 'grid', gap: 12 }, children: kids(current.props.children).map(renderChild) })) : null] }));
46
+ };
47
+ const Accordion = ({ props, renderChild }) => (_jsx("div", { style: { display: 'grid', gap: 6 }, children: kids(props.items).map((item) => (_jsxs("details", { style: box, children: [_jsx("summary", { style: { cursor: 'pointer', fontWeight: 600 }, children: String(item.props.title) }), _jsx("div", { style: { paddingTop: 8, display: 'grid', gap: 12 }, children: kids(item.props.children).map(renderChild) })] }, item.id))) }));
48
+ const Badge = ({ props }) => (_jsx("span", { style: { border: `1px solid ${v('border', 'rgba(0,0,0,.12)')}`, borderRadius: 999, padding: '1px 8px', fontSize: 12, color: TONE[props.tone ?? 'neutral'] }, children: props.label }));
49
+ const Callout = ({ props }) => (_jsxs("aside", { role: "note", style: { ...box, borderLeft: `3px solid ${TONE[props.tone === 'warn' ? 'warn' : props.tone === 'success' ? 'good' : 'neutral']}` }, children: [props.title ? _jsxs("strong", { children: [props.title, " "] }) : null, props.body] }));
50
+ const Image = ({ props }) => (_jsxs("figure", { style: { margin: 0, display: props.text ? 'flex' : undefined, gap: 12, alignItems: 'flex-start' }, children: [_jsx("img", { src: props.src, alt: props.alt, referrerPolicy: "no-referrer", loading: "lazy", style: props.text ? { width: 140, flexShrink: 0, borderRadius: 6 } : { maxWidth: '100%', borderRadius: 6 } }), props.text ? _jsx("figcaption", { children: props.text }) : props.caption ? _jsx("figcaption", { style: muted, children: props.caption }) : null] }));
51
+ const Link = ({ props }) => (_jsx("a", { href: props.href, target: "_blank", rel: "noopener noreferrer", children: props.label }));
52
+ export const defaultGenuiComponents = {
53
+ Stack,
54
+ Card,
55
+ Stat,
56
+ StatRow,
57
+ Table,
58
+ Compare,
59
+ RankedList,
60
+ BarChart: SeriesChart,
61
+ LineChart: SeriesChart,
62
+ PieChart,
63
+ Map,
64
+ Steps,
65
+ Checklist,
66
+ Tabs,
67
+ Accordion,
68
+ Badge,
69
+ Callout,
70
+ Image,
71
+ Link,
72
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, test } from 'bun:test';
3
+ import { renderToStaticMarkup } from 'react-dom/server';
4
+ import { defaultGenuiComponents } from './default-components.js';
5
+ import { GenuiBlock } from './genui-block.js';
6
+ const md = (markdown) => _jsx("pre", { children: markdown });
7
+ const render = (code) => renderToStaticMarkup(_jsx(GenuiBlock, { code: code, streaming: false, components: defaultGenuiComponents, renderMarkdown: md }));
8
+ const ALL = `root = Stack([top, bottom])
9
+ top = Stack([stats, card, table, cmp, list, bar, line])
10
+ bottom = Stack([pie, map, tabs, acc, note, img, link])
11
+ stats = StatRow([s1, s2])
12
+ s1 = Stat("Revenue", "12k", "+4%", "up", "USD")
13
+ s2 = Stat("Users", "900")
14
+ card = Card("Option A", "Close to the venue", "4.7 stars", null, "https://example.com/a", [tag])
15
+ tag = Badge("Top pick", "good")
16
+ table = Table(["Name", "Value"], [["a", 1], ["b", 2]], "Sample")
17
+ cmp = Compare([x, y], ["Price", "Rating"], "X")
18
+ x = CompareItem("X", ["$10", "4.5"], ["Cheap"], ["Small"])
19
+ y = CompareItem("Y", ["$20", "4.8"])
20
+ list = RankedList([r1])
21
+ r1 = RankedItem("First", "Best overall", "4.7 stars")
22
+ bar = BarChart(["Q1", "Q2"], [rev], "billing export", "USD")
23
+ rev = Series("Revenue", [1, 2])
24
+ line = LineChart(["Jan", "Feb"], [rev], "billing export")
25
+ pie = PieChart([p1, p2], "survey tool")
26
+ p1 = Slice("Yes", 3)
27
+ p2 = Slice("No", 1)
28
+ map = Map([m1], "places tool")
29
+ m1 = Marker(48.85, 2.35, "Center", "Main square")
30
+ tabs = Tabs([t1, t2])
31
+ t1 = Tab("One", [b1])
32
+ t2 = Tab("Two", [b2])
33
+ b1 = Badge("first")
34
+ b2 = Badge("second")
35
+ acc = Accordion([a1])
36
+ a1 = AccordionItem("Details", [b1])
37
+ note = Callout("warn", "Check the dates", "Note")
38
+ img = Image("https://example.com/i.png", "A picture", "Caption")
39
+ link = Link("Book", "https://example.com/book")`;
40
+ describe('defaultGenuiComponents', () => {
41
+ test('renders every v1 component from one block', () => {
42
+ const html = render(ALL);
43
+ for (const expected of [
44
+ 'Revenue', '12k', 'Option A', 'Top pick', '<table', 'Sample', 'Cheap', 'Best overall',
45
+ 'Source: billing export', 'Source: survey tool', 'openstreetmap.org', 'role="tablist"', '<details',
46
+ 'Check the dates', 'alt="A picture"', 'href="https://example.com/book"',
47
+ ]) {
48
+ expect(html).toContain(expected);
49
+ }
50
+ expect(html).not.toContain('<pre>');
51
+ });
52
+ test('every block component has a default', () => {
53
+ const names = ['Stack', 'Card', 'Stat', 'StatRow', 'Table', 'Compare', 'RankedList', 'BarChart', 'LineChart', 'PieChart', 'Map', 'Steps', 'Checklist', 'Tabs', 'Accordion', 'Badge', 'Callout', 'Image', 'Link'];
54
+ expect(Object.keys(defaultGenuiComponents).sort()).toEqual([...names].sort());
55
+ });
56
+ test('Steps render each title, meta, text, and photo; a Checklist renders a checkbox per item', () => {
57
+ const steps = render('root = Steps([a, b])\na = Step("Home → Central Station", "Take a taxi.", "https://example.com/s.jpg", "about 20 min")\nb = Step("Find your gate")');
58
+ for (const expected of ['<ol', 'Home → Central Station', 'about 20 min', 'Take a taxi.', 'Find your gate', 'src="https://example.com/s.jpg"', 'referrerPolicy="no-referrer"']) {
59
+ expect(steps).toContain(expected);
60
+ }
61
+ expect(steps).not.toContain('<pre>');
62
+ const list = render('root = Checklist("Pack", ["Valid passport", "Bank card"])');
63
+ expect(list).toContain('Pack');
64
+ expect(list).toContain('Valid passport');
65
+ expect(list.match(/type="checkbox"/g)).toHaveLength(2);
66
+ expect(list).not.toContain('<pre>');
67
+ });
68
+ test('a Step still streaming loads no photo: its URL may be half-written', () => {
69
+ const code = 'root = Steps([a, b])\na = Step("Done", "Go.", "https://example.com/a.jpg")\nb = Step("Next", "Go.", "https://example.com/photos/g';
70
+ const html = renderToStaticMarkup(_jsx(GenuiBlock, { code: code, streaming: true, components: defaultGenuiComponents, renderMarkdown: md }));
71
+ expect(html).toContain('Next');
72
+ expect(html).toContain('src="https://example.com/a.jpg"');
73
+ expect(html).not.toContain('https://example.com/photos/g');
74
+ });
75
+ test('external links open safely', () => {
76
+ expect(render('root = Stack([l])\nl = Link("x", "https://example.com")')).toContain('rel="noopener noreferrer"');
77
+ });
78
+ test('charts carry an accessible label', () => {
79
+ expect(render('root = Stack([c])\nc = BarChart(["a"], [s], "src")\ns = Series("S", [1])')).toContain('aria-label="Bar chart: S. Source: src"');
80
+ });
81
+ test('images do not leak the referrer and load lazily', () => {
82
+ const html = render('root = Stack([i])\ni = Image("https://example.com/i.png", "A picture")');
83
+ expect(html).toContain('referrerPolicy="no-referrer"');
84
+ expect(html).toContain('loading="lazy"');
85
+ const card = render('root = Stack([c])\nc = Card("T", null, null, "https://example.com/c.png")');
86
+ expect(card).toContain('referrerPolicy="no-referrer"');
87
+ expect(card).toContain('loading="lazy"');
88
+ });
89
+ test('a javascript: link never reaches the markup and siblings still render', () => {
90
+ const html = render('root = Stack([l, ok])\nl = Link("x", "javascript:alert(1)")\nok = Badge("kept")');
91
+ expect(html).not.toContain('javascript:');
92
+ expect(html).toContain('kept');
93
+ });
94
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { expect, mock, test } from 'bun:test';
3
+ import * as langCore from '@openuidev/lang-core';
4
+ import { act, create } from 'react-test-renderer';
5
+ const real = { ...langCore };
6
+ let parsersCreated = 0;
7
+ mock.module('@openuidev/lang-core', () => ({
8
+ ...real,
9
+ createStreamingParser: (...args) => {
10
+ parsersCreated += 1;
11
+ return real.createStreamingParser(...args);
12
+ },
13
+ }));
14
+ const { GenuiBlock } = await import('./genui-block.js');
15
+ const { GENUI_CUT_OFF_NOTE } = await import('../markdown.js');
16
+ Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', { value: true, configurable: true });
17
+ const CODE = 'root = Stack([a, b])\na = Badge("kept")\nb = Callout("info", "a longer sentence that streams in")';
18
+ const renderMarkdown = (markdown) => _jsx("pre", { "data-type": "markdown", children: markdown });
19
+ const shown = (renderer) => renderer.root.findAllByProps({ 'data-type': 'markdown' }).map((node) => node.children.join(''));
20
+ function mount(element) {
21
+ let renderer;
22
+ act(() => {
23
+ renderer = create(element);
24
+ });
25
+ return renderer;
26
+ }
27
+ test('Rich answers off: streamed ticks never show the cut-off note; a settled cut-off block does', () => {
28
+ const props = { version: 1, enabled: false, components: {}, renderMarkdown };
29
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, 30), streaming: true }));
30
+ const ticks = [];
31
+ for (let end = 31; end <= CODE.length; end += 3) {
32
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, end), streaming: true })));
33
+ ticks.push(shown(renderer));
34
+ }
35
+ expect(ticks.flat().some((markdown) => markdown.includes(GENUI_CUT_OFF_NOTE))).toBe(false);
36
+ expect(ticks.some((markdown) => markdown.join('') === '[kept]')).toBe(true);
37
+ const cutOff = CODE.slice(0, CODE.indexOf('streams'));
38
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: cutOff, streaming: false })));
39
+ expect(shown(renderer)).toEqual([`[kept]\n\n*${GENUI_CUT_OFF_NOTE}*`]);
40
+ });
41
+ test('Rich answers off: a streamed block uses one incremental parser, not one parse per tick', () => {
42
+ const props = { version: 1, enabled: false, components: {}, renderMarkdown };
43
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, 30), streaming: true }));
44
+ parsersCreated = 0;
45
+ for (let end = 31; end <= CODE.length; end += 3) {
46
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, end), streaming: true })));
47
+ }
48
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: false })));
49
+ expect(parsersCreated).toBe(0);
50
+ });
@@ -0,0 +1,28 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ import { type GenuiNode, type GenuiParseResult } from '../index.js';
3
+ export interface GenuiComponentProps {
4
+ node: GenuiNode;
5
+ props: Record<string, any>;
6
+ renderChild: (node: GenuiNode) => ReactNode;
7
+ streaming: boolean;
8
+ }
9
+ export type GenuiComponentMap = Readonly<Partial<Record<string, ComponentType<GenuiComponentProps>>>>;
10
+ export type GenuiOutcome = 'rendered' | 'fallback' | 'parse_error' | 'render_error' | 'unsupported';
11
+ export interface GenuiBlockEvent {
12
+ outcome: GenuiOutcome;
13
+ components: string[];
14
+ msToFirstPaint: number | null;
15
+ issueCount: number;
16
+ }
17
+ export interface GenuiBlockProps {
18
+ code: string;
19
+ version?: number;
20
+ streaming: boolean;
21
+ components: GenuiComponentMap;
22
+ renderMarkdown: (markdown: string) => ReactNode;
23
+ renderPending?: (node: GenuiNode) => ReactNode;
24
+ enabled?: boolean;
25
+ onSettled?: (event: GenuiBlockEvent) => void;
26
+ }
27
+ export declare function useGenuiParse(code: string, version: number, streaming: boolean): GenuiParseResult;
28
+ export declare function GenuiBlock({ code, version, streaming, components, renderMarkdown, renderPending, enabled, onSettled, }: GenuiBlockProps): import("react").JSX.Element | null;
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Component, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, } from 'react';
3
+ import { genuiResultToMarkdown } from '../markdown.js';
4
+ import { createGenuiParser, genuiNodeToMarkdown, GENUI_CUT_OFF_NOTE, GENUI_SCHEMA_VERSION, GENUI_UNSUPPORTED_NOTE, } from '../index.js';
5
+ const RenderContext = createContext(null);
6
+ const renderNothing = () => null;
7
+ const GenuiNodeView = memo(function NodeView({ node }) {
8
+ const context = useContext(RenderContext);
9
+ const renderChild = useCallback((child) => _jsx(GenuiNodeView, { node: child }, child.id), []);
10
+ if (node.partial)
11
+ return _jsx(_Fragment, { children: context.renderPending(node) });
12
+ const HostComponent = context.components[node.type];
13
+ if (!HostComponent)
14
+ return _jsx(_Fragment, { children: context.renderMarkdown(genuiNodeToMarkdown(node)) });
15
+ return _jsx(HostComponent, { node: node, props: node.props, renderChild: renderChild, streaming: context.streaming });
16
+ });
17
+ class BlockBoundary extends Component {
18
+ state = { failed: false };
19
+ static getDerivedStateFromError() {
20
+ return { failed: true };
21
+ }
22
+ componentDidCatch() {
23
+ this.props.onError();
24
+ }
25
+ componentDidUpdate(previous) {
26
+ if (previous.streaming && !this.props.streaming && this.state.failed)
27
+ this.setState({ failed: false });
28
+ }
29
+ render() {
30
+ return this.state.failed ? this.props.fallback : this.props.children;
31
+ }
32
+ }
33
+ function collectTypes(node, into) {
34
+ if (!node)
35
+ return into;
36
+ into.add(node.type);
37
+ for (const value of Object.values(node.props)) {
38
+ if (Array.isArray(value))
39
+ for (const child of value)
40
+ if (child && typeof child === 'object' && 'type' in child)
41
+ collectTypes(child, into);
42
+ }
43
+ return into;
44
+ }
45
+ const BlockFallback = memo(function BlockFallback({ result, renderMarkdown, }) {
46
+ const markdown = useMemo(() => genuiResultToMarkdown(result), [result]);
47
+ return _jsx(_Fragment, { children: markdown ? renderMarkdown(markdown) : null });
48
+ });
49
+ export function useGenuiParse(code, version, streaming) {
50
+ const parserRef = useRef(null);
51
+ if (!parserRef.current || parserRef.current.version !== version) {
52
+ parserRef.current = { version, parser: createGenuiParser(version) };
53
+ }
54
+ return parserRef.current.parser.update(code, streaming);
55
+ }
56
+ export function GenuiBlock({ code, version = GENUI_SCHEMA_VERSION, streaming, components, renderMarkdown, renderPending = renderNothing, enabled = true, onSettled, }) {
57
+ const result = useGenuiParse(code, version, streaming);
58
+ const mountedAt = useRef(performance.now());
59
+ const firstPaint = useRef(null);
60
+ const renderError = useRef(false);
61
+ if (enabled && result.root && firstPaint.current === null)
62
+ firstPaint.current = performance.now() - mountedAt.current;
63
+ const context = useMemo(() => ({ components, renderMarkdown, renderPending, streaming }), [components, renderMarkdown, renderPending, streaming]);
64
+ const unsupported = version !== GENUI_SCHEMA_VERSION;
65
+ useEffect(() => {
66
+ if (streaming || !onSettled)
67
+ return;
68
+ const outcome = unsupported
69
+ ? 'unsupported'
70
+ : !enabled
71
+ ? 'fallback'
72
+ : renderError.current
73
+ ? 'render_error'
74
+ : result.root
75
+ ? 'rendered'
76
+ : 'parse_error';
77
+ onSettled({
78
+ outcome,
79
+ components: enabled ? [...collectTypes(result.root, new Set())].sort() : [],
80
+ msToFirstPaint: firstPaint.current === null ? null : Math.round(firstPaint.current),
81
+ issueCount: enabled ? result.issues.length : 0,
82
+ });
83
+ }, [streaming]);
84
+ if (unsupported)
85
+ return _jsx(_Fragment, { children: renderMarkdown(`*${GENUI_UNSUPPORTED_NOTE}*`) });
86
+ if (!enabled)
87
+ return _jsx(BlockFallback, { result: result, renderMarkdown: renderMarkdown });
88
+ if (!result.root)
89
+ return streaming ? null : _jsx(BlockFallback, { result: result, renderMarkdown: renderMarkdown });
90
+ return (_jsxs(BlockBoundary, { streaming: streaming, fallback: _jsx(BlockFallback, { result: result, renderMarkdown: renderMarkdown }), onError: () => (renderError.current = !streaming), children: [_jsx(RenderContext.Provider, { value: context, children: _jsx(GenuiNodeView, { node: result.root }) }), !streaming && result.issues.some((issue) => issue.code === 'cut-off') ? renderMarkdown(`*${GENUI_CUT_OFF_NOTE}*`) : null] }));
91
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,205 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, test } from 'bun:test';
3
+ import { useEffect } from 'react';
4
+ import { act, create } from 'react-test-renderer';
5
+ import { GenuiBlock } from './genui-block.js';
6
+ Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', { value: true, configurable: true });
7
+ const renders = new Map();
8
+ const count = (id) => renders.set(id, (renders.get(id) ?? 0) + 1);
9
+ const Stack = ({ node, props, renderChild }) => {
10
+ count(node.id);
11
+ return _jsx("div", { "data-type": "Stack", children: (props.children ?? []).map(renderChild) });
12
+ };
13
+ const Stat = ({ node, props }) => {
14
+ count(node.id);
15
+ return _jsx("span", { "data-type": "Stat", children: `${props.label}=${props.value}` });
16
+ };
17
+ const Boom = () => {
18
+ throw new Error('render failure');
19
+ };
20
+ const COMPONENTS = { Stack, Stat };
21
+ const renderNothing = () => null;
22
+ const renderMarkdown = (markdown) => _jsx("pre", { "data-type": "markdown", children: markdown });
23
+ const CODE = 'root = Stack([a, b, c])\na = Stat("A", "1")\nb = Stat("B", "2")\nc = Stat("C", "3")';
24
+ function mount(element) {
25
+ let renderer;
26
+ act(() => {
27
+ renderer = create(element);
28
+ });
29
+ return renderer;
30
+ }
31
+ describe('GenuiBlock', () => {
32
+ test('a finished statement renders once while later statements stream', () => {
33
+ renders.clear();
34
+ const props = { version: 1, components: COMPONENTS, renderMarkdown };
35
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, 44), streaming: true }));
36
+ const midString = CODE.indexOf('"2"') + 1;
37
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, midString), streaming: true })));
38
+ const cut = CODE.indexOf('\nc =');
39
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, cut), streaming: true })));
40
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: true })));
41
+ expect(renders.get('a')).toBe(1);
42
+ expect(renderer.root.findAll((n) => n.props['data-type'] === 'Stat').map((n) => n.children.join(''))).toEqual([
43
+ 'A=1',
44
+ 'B=2',
45
+ 'C=3',
46
+ ]);
47
+ });
48
+ test('disabled renders the markdown fallback', () => {
49
+ const renderer = mount(_jsx(GenuiBlock, { code: CODE, streaming: false, enabled: false, components: COMPONENTS, renderMarkdown: renderMarkdown }));
50
+ expect(renderer.root.findByProps({ 'data-type': 'markdown' }).children.join('')).toBe('**A:** 1\n\n**B:** 2\n\n**C:** 3');
51
+ });
52
+ test('a component missing from the host map renders that node as markdown', () => {
53
+ const renderer = mount(_jsx(GenuiBlock, { code: CODE, streaming: false, components: { Stack }, renderMarkdown: renderMarkdown }));
54
+ expect(renderer.root.findAllByProps({ 'data-type': 'markdown' }).map((n) => n.children.join(''))).toEqual([
55
+ '**A:** 1',
56
+ '**B:** 2',
57
+ '**C:** 3',
58
+ ]);
59
+ });
60
+ test('a render error falls back to markdown and reports render_error', () => {
61
+ const events = [];
62
+ const original = console.error;
63
+ console.error = () => { };
64
+ const renderer = mount(_jsx(GenuiBlock, { code: CODE, streaming: false, components: { Stack, Stat: Boom }, renderMarkdown: renderMarkdown, onSettled: (event) => events.push(event) }));
65
+ console.error = original;
66
+ expect(renderer.root.findByProps({ 'data-type': 'markdown' })).toBeTruthy();
67
+ expect(events.map((e) => e.outcome)).toEqual(['render_error']);
68
+ });
69
+ test('broken source renders a one-line note, never raw source, and reports parse_error', () => {
70
+ const events = [];
71
+ const renderer = mount(_jsx(GenuiBlock, { code: "not openui", streaming: false, components: COMPONENTS, renderMarkdown: renderMarkdown, onSettled: (e) => events.push(e) }));
72
+ expect(renderer.root.findByProps({ 'data-type': 'markdown' }).children.join('')).toBe('*This content could not be displayed.*');
73
+ expect(events[0]?.outcome).toBe('parse_error');
74
+ });
75
+ test('a block cut off mid-statement renders its valid part and the cut-off note', () => {
76
+ const renderer = mount(_jsx(GenuiBlock, { code: 'root = Stack([a, b])\na = Stat("A", "1")\nb = Stat("B", "', streaming: false, components: COMPONENTS, renderMarkdown: renderMarkdown }));
77
+ expect(renderer.root.findAll((n) => n.props['data-type'] === 'Stat').map((n) => n.children.join(''))).toEqual(['A=1']);
78
+ expect(renderer.root.findByProps({ 'data-type': 'markdown' }).children.join('')).toBe('*Response was cut off.*');
79
+ });
80
+ test('a newer version shows the unsupported note', () => {
81
+ const renderer = mount(_jsx(GenuiBlock, { code: CODE, version: 2, streaming: false, components: COMPONENTS, renderMarkdown: renderMarkdown }));
82
+ expect(renderer.root.findByProps({ 'data-type': 'markdown' }).children.join('')).toContain('newer version');
83
+ });
84
+ test('settled event names components, never content', () => {
85
+ const events = [];
86
+ const props = { version: 1, components: COMPONENTS, renderMarkdown, onSettled: (e) => events.push(e) };
87
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: CODE, streaming: true }));
88
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: false })));
89
+ expect(events).toHaveLength(1);
90
+ expect(events[0]).toMatchObject({ outcome: 'rendered', components: ['Stack', 'Stat'], issueCount: 0 });
91
+ expect(JSON.stringify(events[0])).not.toContain('A=1');
92
+ });
93
+ test('a stable renderMarkdown keeps finished nodes from re-rendering', () => {
94
+ renders.clear();
95
+ const props = { version: 1, components: COMPONENTS, renderMarkdown, renderPending: renderNothing };
96
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, 44), streaming: true }));
97
+ const midString = CODE.indexOf('"2"') + 1;
98
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, midString), streaming: true })));
99
+ const cut = CODE.indexOf('\nc =');
100
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, cut), streaming: true })));
101
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: true })));
102
+ expect(renders.get('a')).toBe(1);
103
+ });
104
+ test('swapping renderMarkdown re-renders the fallback', () => {
105
+ const rendererA = (m) => _jsx("pre", { "data-r": "A", children: m });
106
+ const rendererB = (m) => _jsx("pre", { "data-r": "B", children: m });
107
+ const props = { version: 1, enabled: false, components: COMPONENTS, code: CODE, streaming: false };
108
+ const renderer = mount(_jsx(GenuiBlock, { ...props, renderMarkdown: rendererA }));
109
+ expect(renderer.root.findAllByProps({ 'data-r': 'A' })).toHaveLength(1);
110
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, renderMarkdown: rendererB })));
111
+ expect(renderer.root.findAllByProps({ 'data-r': 'B' })).toHaveLength(1);
112
+ expect(renderer.root.findAllByProps({ 'data-r': 'A' })).toHaveLength(0);
113
+ });
114
+ test('swapping renderMarkdown reaches nodes rendered as markdown', () => {
115
+ const rendererA = (m) => _jsx("pre", { "data-r": "A", children: m });
116
+ const rendererB = (m) => _jsx("pre", { "data-r": "B", children: m });
117
+ const props = { version: 1, components: { Stack }, code: CODE, streaming: false };
118
+ const renderer = mount(_jsx(GenuiBlock, { ...props, renderMarkdown: rendererA }));
119
+ expect(renderer.root.findAllByProps({ 'data-r': 'A' }).length).toBeGreaterThan(0);
120
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, renderMarkdown: rendererB })));
121
+ expect(renderer.root.findAllByProps({ 'data-r': 'B' }).length).toBeGreaterThan(0);
122
+ expect(renderer.root.findAllByProps({ 'data-r': 'A' })).toHaveLength(0);
123
+ });
124
+ test('the fallback markdown is not computed while a valid block streams', () => {
125
+ let markdownCalls = 0;
126
+ const countingMarkdown = (markdown) => {
127
+ markdownCalls += 1;
128
+ return _jsx("pre", { "data-type": "markdown", children: markdown });
129
+ };
130
+ const props = { version: 1, components: COMPONENTS, renderMarkdown: countingMarkdown };
131
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, 44), streaming: true }));
132
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE.slice(0, CODE.indexOf('\nc =')), streaming: true })));
133
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: true })));
134
+ expect(markdownCalls).toBe(0);
135
+ });
136
+ test('a throw while streaming gets a fresh try once the stream settles', () => {
137
+ const FlakyStat = ({ node, props, streaming }) => {
138
+ if (streaming)
139
+ throw new Error('only while streaming');
140
+ count(node.id);
141
+ return _jsx("span", { "data-type": "Stat", children: `${props.label}=${props.value}` });
142
+ };
143
+ const components = { Stack, Stat: FlakyStat };
144
+ const original = console.error;
145
+ console.error = () => { };
146
+ const props = { version: 1, components, renderMarkdown };
147
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: CODE, streaming: true }));
148
+ expect(renderer.root.findAllByProps({ 'data-type': 'Stat' })).toHaveLength(0);
149
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: false })));
150
+ console.error = original;
151
+ expect(renderer.root.findAll((n) => n.props['data-type'] === 'Stat').map((n) => n.children.join(''))).toEqual([
152
+ 'A=1',
153
+ 'B=2',
154
+ 'C=3',
155
+ ]);
156
+ });
157
+ test('settling does not remount host components', () => {
158
+ let mounts = 0;
159
+ const MountCounter = ({ props }) => {
160
+ useEffect(() => {
161
+ mounts += 1;
162
+ }, []);
163
+ return _jsx("span", { "data-type": "Stat", children: `${props.label}=${props.value}` });
164
+ };
165
+ const props = { version: 1, components: { Stack, Stat: MountCounter }, renderMarkdown };
166
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: CODE, streaming: true }));
167
+ expect(mounts).toBe(3);
168
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: false })));
169
+ expect(mounts).toBe(3);
170
+ });
171
+ test('a disabled block reports no components and no first paint', () => {
172
+ const events = [];
173
+ mount(_jsx(GenuiBlock, { code: CODE, streaming: false, enabled: false, components: COMPONENTS, renderMarkdown: renderMarkdown, onSettled: (e) => events.push(e) }));
174
+ expect(events).toHaveLength(1);
175
+ expect(events[0]).toMatchObject({ outcome: 'fallback', components: [], msToFirstPaint: null });
176
+ });
177
+ test('a disabled block reports no issues', () => {
178
+ const events = [];
179
+ mount(_jsx(GenuiBlock, { code: CODE, streaming: false, enabled: false, components: COMPONENTS, renderMarkdown: renderMarkdown, onSettled: (e) => events.push(e) }));
180
+ expect(events[0]?.issueCount).toBe(0);
181
+ });
182
+ test('a disabled block converts to markdown once per distinct code, not once per render', () => {
183
+ let markdownCalls = 0;
184
+ const counting = (markdown) => {
185
+ markdownCalls += 1;
186
+ return _jsx("pre", { "data-type": "markdown", children: markdown });
187
+ };
188
+ const props = { version: 1, enabled: false, components: COMPONENTS, renderMarkdown: counting };
189
+ const ticks = [CODE.slice(0, 44), CODE.slice(0, CODE.indexOf('\nc =')), CODE];
190
+ const renderer = mount(_jsx(GenuiBlock, { ...props, code: ticks[0], streaming: true }));
191
+ for (const code of ticks.slice(1))
192
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: code, streaming: true })));
193
+ expect(markdownCalls).toBe(3);
194
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: true })));
195
+ act(() => renderer.update(_jsx(GenuiBlock, { ...props, code: CODE, streaming: true })));
196
+ expect(markdownCalls).toBe(3);
197
+ });
198
+ test('20,000 nested Stack([ never throws, enabled or disabled', () => {
199
+ const code = `root = ${'Stack(['.repeat(20_000)}Badge("x")${'])'.repeat(20_000)}`;
200
+ for (const enabled of [true, false]) {
201
+ const renderer = mount(_jsx(GenuiBlock, { code: code, streaming: false, enabled: enabled, components: COMPONENTS, renderMarkdown: renderMarkdown }));
202
+ expect(renderer.root.findByProps({ 'data-type': 'markdown' }).children.join('')).toBe('*This content could not be displayed.*');
203
+ }
204
+ });
205
+ });
@@ -0,0 +1,2 @@
1
+ export { defaultGenuiComponents } from './default-components.js';
2
+ export { GenuiBlock, useGenuiParse, type GenuiBlockEvent, type GenuiBlockProps, type GenuiComponentMap, type GenuiComponentProps, type GenuiOutcome, } from './genui-block.js';
@@ -0,0 +1,2 @@
1
+ export { defaultGenuiComponents } from './default-components.js';
2
+ export { GenuiBlock, useGenuiParse, } from './genui-block.js';
@@ -0,0 +1,5 @@
1
+ import type { GenuiNode } from './types.js';
2
+ export declare function shareNodes(root: GenuiNode | null, previous: ReadonlyMap<string, GenuiNode>): {
3
+ root: GenuiNode | null;
4
+ byId: Map<string, GenuiNode>;
5
+ };
@@ -0,0 +1,30 @@
1
+ const isNode = (value) => typeof value === 'object' && value !== null && 'id' in value && 'type' in value && 'props' in value;
2
+ function valueEqual(a, b) {
3
+ if (a === b)
4
+ return true;
5
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
6
+ return false;
7
+ return a.every((item, index) => valueEqual(item, b[index]));
8
+ }
9
+ function propsEqual(a, b) {
10
+ const keys = Object.keys(a);
11
+ if (keys.length !== Object.keys(b).length)
12
+ return false;
13
+ return keys.every((key) => valueEqual(a[key], b[key]));
14
+ }
15
+ export function shareNodes(root, previous) {
16
+ const byId = new Map();
17
+ function share(node) {
18
+ const props = {};
19
+ for (const [key, value] of Object.entries(node.props)) {
20
+ props[key] = Array.isArray(value) && value.length > 0 && value.every(isNode) ? value.map(share) : value;
21
+ }
22
+ const before = previous.get(node.id);
23
+ const kept = before && before.type === node.type && before.partial === node.partial && propsEqual(before.props, props)
24
+ ? before
25
+ : { ...node, props };
26
+ byId.set(node.id, kept);
27
+ return kept;
28
+ }
29
+ return { root: root ? share(root) : null, byId };
30
+ }
@@ -0,0 +1,2 @@
1
+ export declare const HOTEL = "root = Stack([stats, list, tip])\nstats = StatRow([a, b])\na = Stat(\"Options checked\", \"24\")\nb = Stat(\"Under budget\", \"9\", \"+2\", \"up\")\nlist = RankedList([h1, h2])\nh1 = RankedItem(\"Option A\", \"Closest to the venue\", \"4.7 stars\", null, \"https://example.com/a\")\nh2 = RankedItem(\"Option B\", \"Quietest rooms\")\ntip = Callout(\"info\", \"Book by Friday\", \"Tip\")";
2
+ export declare const BROKEN_TABS: string;
@@ -0,0 +1,16 @@
1
+ export const HOTEL = `root = Stack([stats, list, tip])
2
+ stats = StatRow([a, b])
3
+ a = Stat("Options checked", "24")
4
+ b = Stat("Under budget", "9", "+2", "up")
5
+ list = RankedList([h1, h2])
6
+ h1 = RankedItem("Option A", "Closest to the venue", "4.7 stars", null, "https://example.com/a")
7
+ h2 = RankedItem("Option B", "Quietest rooms")
8
+ tip = Callout("info", "Book by Friday", "Tip")`;
9
+ export const BROKEN_TABS = `root = Stack([tabs])
10
+ tabs = Tabs([t1, t2])
11
+ t1 = Tab("${'L'.repeat(31)}", [note, a])
12
+ t2 = Tab("${'M'.repeat(31)}", [b, c])
13
+ note = Callout("info", "${'x'.repeat(401)}")
14
+ a = Table(["k"], [["1"]])
15
+ b = Table(["k"], [["2"]])
16
+ c = Table(["k"], [["3"]])`;