paseo-plugin-genui 0.0.0-stage → 0.1.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 (375) hide show
  1. package/LICENSE +202 -0
  2. package/OVERVIEW.md +21 -0
  3. package/README.md +77 -2
  4. package/assets/fonts/Excalifont-OFL.txt +109 -0
  5. package/assets/fonts/Excalifont-Regular-latin.woff2 +0 -0
  6. package/assets/icon.png +0 -0
  7. package/assets/icon.svg +1 -0
  8. package/assets/screenshot-chat.png +0 -0
  9. package/assets/screenshot-gallery.png +0 -0
  10. package/assets/screenshot-playground.png +0 -0
  11. package/client/components/content.tsx +468 -0
  12. package/client/components/data.tsx +967 -0
  13. package/client/components/dev.tsx +998 -0
  14. package/client/components/diagram.tsx +513 -0
  15. package/client/components/inputs.tsx +994 -0
  16. package/client/components/layout.tsx +239 -0
  17. package/client/components/media.tsx +428 -0
  18. package/client/components/project.tsx +641 -0
  19. package/client/components/widgets.tsx +836 -0
  20. package/client/gallery.tsx +449 -0
  21. package/client/live-ui.tsx +525 -0
  22. package/client/loaders.ts +342 -0
  23. package/client/markdown.tsx +172 -0
  24. package/client/node-renderer.tsx +149 -0
  25. package/client/render.tsx +186 -0
  26. package/client/runtime.ts +103 -0
  27. package/client/settings.tsx +57 -0
  28. package/client/web.ts +186 -0
  29. package/gallery/Acceptance-baseline.ui +1 -0
  30. package/gallery/Acceptance-checklist.ui +14 -0
  31. package/gallery/Acceptance-evidence.ui +9 -0
  32. package/gallery/Acceptance.ui +1 -0
  33. package/gallery/Badge.ui +3 -0
  34. package/gallery/Benchmark-context.ui +8 -0
  35. package/gallery/Benchmark-hyperfine.ui +1 -0
  36. package/gallery/Benchmark-runs-list.ui +9 -0
  37. package/gallery/Benchmark-runs.ui +1 -0
  38. package/gallery/Benchmark-vitest.ui +1 -0
  39. package/gallery/Benchmark.ui +9 -0
  40. package/gallery/Board-bb-tasks.ui +1 -0
  41. package/gallery/Board-gh-issues.ui +1 -0
  42. package/gallery/Board-gh-prs.ui +1 -0
  43. package/gallery/Board.ui +12 -0
  44. package/gallery/Button-destructive.ui +4 -0
  45. package/gallery/Button.ui +5 -0
  46. package/gallery/Calendar.ui +15 -0
  47. package/gallery/CalendarHeatmap-short.ui +11 -0
  48. package/gallery/CalendarHeatmap.ui +4 -0
  49. package/gallery/Callout.ui +4 -0
  50. package/gallery/Card.ui +4 -0
  51. package/gallery/Carousel.ui +8 -0
  52. package/gallery/Changes-paths.ui +1 -0
  53. package/gallery/Changes-uncommitted.ui +1 -0
  54. package/gallery/Changes.ui +1 -0
  55. package/gallery/Chart-area.ui +3 -0
  56. package/gallery/Chart-funnel.ui +3 -0
  57. package/gallery/Chart-heatmap.ui +11 -0
  58. package/gallery/Chart-pie.ui +3 -0
  59. package/gallery/Chart-radar.ui +3 -0
  60. package/gallery/Chart-scatter.ui +4 -0
  61. package/gallery/Chart-stacked.ui +5 -0
  62. package/gallery/Chart.ui +3 -0
  63. package/gallery/Checklist.ui +6 -0
  64. package/gallery/Choice.ui +8 -0
  65. package/gallery/Code.ui +3 -0
  66. package/gallery/Commits-limit.ui +1 -0
  67. package/gallery/Commits.ui +1 -0
  68. package/gallery/Compare-conditions.ui +4 -0
  69. package/gallery/Compare-three.ui +7 -0
  70. package/gallery/Compare.ui +4 -0
  71. package/gallery/Coverage-go.ui +1 -0
  72. package/gallery/Coverage-lcov.ui +1 -0
  73. package/gallery/Coverage-python.ui +1 -0
  74. package/gallery/Coverage.ui +1 -0
  75. package/gallery/DateInput.ui +5 -0
  76. package/gallery/Decision-percent.ui +10 -0
  77. package/gallery/Decision.ui +12 -0
  78. package/gallery/Dependencies-audit.ui +1 -0
  79. package/gallery/Dependencies-both.ui +1 -0
  80. package/gallery/Dependencies-list.ui +7 -0
  81. package/gallery/Dependencies-pip.ui +1 -0
  82. package/gallery/Dependencies-pnpm.ui +1 -0
  83. package/gallery/Dependencies.ui +1 -0
  84. package/gallery/Details.ui +4 -0
  85. package/gallery/Diagram-flow.ui +16 -0
  86. package/gallery/Diagram-sequence.ui +15 -0
  87. package/gallery/Diagram.ui +16 -0
  88. package/gallery/Diff.ui +3 -0
  89. package/gallery/Divider.ui +5 -0
  90. package/gallery/EditableTable.ui +9 -0
  91. package/gallery/Entity-list.ui +6 -0
  92. package/gallery/Entity.ui +8 -0
  93. package/gallery/Environments.ui +1 -0
  94. package/gallery/Estimate-points.ui +6 -0
  95. package/gallery/Estimate.ui +8 -0
  96. package/gallery/File.ui +4 -0
  97. package/gallery/Findings-biome.ui +1 -0
  98. package/gallery/Findings-eslint.ui +1 -0
  99. package/gallery/Findings-oxlint.ui +1 -0
  100. package/gallery/Findings-review.ui +1 -0
  101. package/gallery/Findings-ruff.ui +1 -0
  102. package/gallery/Findings-sarif.ui +1 -0
  103. package/gallery/Findings-ship.ui +11 -0
  104. package/gallery/Findings-tsc.ui +1 -0
  105. package/gallery/Findings.ui +7 -0
  106. package/gallery/Flashcards.ui +3 -0
  107. package/gallery/FollowUps.ui +4 -0
  108. package/gallery/Form.ui +8 -0
  109. package/gallery/Gallery-links.ui +8 -0
  110. package/gallery/Gallery-strip.ui +14 -0
  111. package/gallery/Gallery.ui +10 -0
  112. package/gallery/Gauge.ui +7 -0
  113. package/gallery/Grid.ui +8 -0
  114. package/gallery/Hotspots-photo.ui +8 -0
  115. package/gallery/Hotspots.ui +11 -0
  116. package/gallery/Icon.ui +5 -0
  117. package/gallery/Image.ui +4 -0
  118. package/gallery/ImageSearch-aspects.ui +6 -0
  119. package/gallery/ImageSearch.ui +4 -0
  120. package/gallery/Input.ui +5 -0
  121. package/gallery/Json-api.ui +1 -0
  122. package/gallery/Json.ui +8 -0
  123. package/gallery/KeyValue.ui +3 -0
  124. package/gallery/List-dots.ui +8 -0
  125. package/gallery/List.ui +8 -0
  126. package/gallery/ListEditor.ui +5 -0
  127. package/gallery/Logs-jsonl.ui +1 -0
  128. package/gallery/Logs-lines.ui +11 -0
  129. package/gallery/Logs-python.ui +1 -0
  130. package/gallery/Logs.ui +1 -0
  131. package/gallery/Map-city.ui +8 -0
  132. package/gallery/Map.ui +8 -0
  133. package/gallery/Metric.ui +3 -0
  134. package/gallery/OptionCards-multi.ui +10 -0
  135. package/gallery/OptionCards.ui +10 -0
  136. package/gallery/Pipeline-checks.ui +1 -0
  137. package/gallery/Pipeline-passed.ui +1 -0
  138. package/gallery/Pipeline-running.ui +1 -0
  139. package/gallery/Pipeline-stages.ui +13 -0
  140. package/gallery/Pipeline.ui +1 -0
  141. package/gallery/Plan.ui +9 -0
  142. package/gallery/Progress.ui +4 -0
  143. package/gallery/Provenance-deploy.ui +6 -0
  144. package/gallery/Provenance-no-receipt.ui +1 -0
  145. package/gallery/Provenance.ui +1 -0
  146. package/gallery/PullRequest.ui +1 -0
  147. package/gallery/Quiz.ui +6 -0
  148. package/gallery/Radio.ui +5 -0
  149. package/gallery/Rating.ui +5 -0
  150. package/gallery/Roadmap-sprint.ui +8 -0
  151. package/gallery/Roadmap-year.ui +9 -0
  152. package/gallery/Roadmap.ui +12 -0
  153. package/gallery/Row.ui +3 -0
  154. package/gallery/SegmentedBar-status.ui +8 -0
  155. package/gallery/SegmentedBar.ui +8 -0
  156. package/gallery/Select.ui +5 -0
  157. package/gallery/Slider.ui +7 -0
  158. package/gallery/Snippet.ui +3 -0
  159. package/gallery/Sources-chips.ui +4 -0
  160. package/gallery/Sources.ui +8 -0
  161. package/gallery/Sparkline.ui +6 -0
  162. package/gallery/Stack.ui +7 -0
  163. package/gallery/StackTrace-aggregate.ui +1 -0
  164. package/gallery/StackTrace-browser.ui +1 -0
  165. package/gallery/StackTrace-go.ui +1 -0
  166. package/gallery/StackTrace-java.ui +1 -0
  167. package/gallery/StackTrace-python.ui +1 -0
  168. package/gallery/StackTrace-rust.ui +1 -0
  169. package/gallery/StackTrace.ui +1 -0
  170. package/gallery/Status.ui +9 -0
  171. package/gallery/Stepper.ui +5 -0
  172. package/gallery/Steps.ui +8 -0
  173. package/gallery/Swatches-long-names.ui +4 -0
  174. package/gallery/Swatches-picker.ui +5 -0
  175. package/gallery/Swatches.ui +10 -0
  176. package/gallery/Table.ui +7 -0
  177. package/gallery/Tabs.ui +8 -0
  178. package/gallery/Tests-junit.ui +1 -0
  179. package/gallery/Tests-vitest-junit.ui +1 -0
  180. package/gallery/Tests-vitest.ui +1 -0
  181. package/gallery/Tests.ui +9 -0
  182. package/gallery/Text.ui +4 -0
  183. package/gallery/TextArea.ui +5 -0
  184. package/gallery/Threads-ids.ui +1 -0
  185. package/gallery/Threads.ui +1 -0
  186. package/gallery/Timeline.ui +8 -0
  187. package/gallery/Timer.ui +4 -0
  188. package/gallery/Toggle.ui +5 -0
  189. package/gallery/Transcript-text.ui +1 -0
  190. package/gallery/Transcript-untimed.ui +1 -0
  191. package/gallery/Transcript-whisper.ui +1 -0
  192. package/gallery/Transcript.ui +1 -0
  193. package/gallery/Tree.ui +3 -0
  194. package/gallery/Treemap-du.ui +1 -0
  195. package/gallery/Treemap-esbuild.ui +1 -0
  196. package/gallery/Treemap-tree.ui +7 -0
  197. package/gallery/Treemap.ui +18 -0
  198. package/gallery/Walkthrough.ui +9 -0
  199. package/gallery/Waterfall-har.ui +1 -0
  200. package/gallery/Waterfall-trace.ui +1 -0
  201. package/gallery/Waterfall.ui +11 -0
  202. package/gallery/data/acceptance/coil/viewport-lifecycle.json +25 -0
  203. package/gallery/data/acceptance/coil-gate.json +153 -0
  204. package/gallery/data/acceptance/orbit-baseline.json +163 -0
  205. package/gallery/data/acceptance/shots/gate-cell-opened.png +0 -0
  206. package/gallery/data/acceptance/shots/gate-light.png +0 -0
  207. package/gallery/data/acceptance/shots/gate-phone-dark.png +0 -0
  208. package/gallery/data/activity.json +1 -0
  209. package/gallery/data/bb-tasks-orb.json +420 -0
  210. package/gallery/data/biome.json +229 -0
  211. package/gallery/data/compare/status-after.png +0 -0
  212. package/gallery/data/compare/status-before.png +0 -0
  213. package/gallery/data/compare/status-dark-2x.png +0 -0
  214. package/gallery/data/compare/status-rerender.png +0 -0
  215. package/gallery/data/coverage-py.json +1 -0
  216. package/gallery/data/coverage-summary.json +1 -0
  217. package/gallery/data/coverage.out +23 -0
  218. package/gallery/data/deploys.json +1 -0
  219. package/gallery/data/device-runs.json +285 -0
  220. package/gallery/data/du-node-modules.txt +359 -0
  221. package/gallery/data/esbuild-meta.json +292 -0
  222. package/gallery/data/eslint.json +266 -0
  223. package/gallery/data/eslint.sarif +343 -0
  224. package/gallery/data/findings-review.json +76 -0
  225. package/gallery/data/gh-issues-nextjs.json +293 -0
  226. package/gallery/data/gh-pr-checks.json +272 -0
  227. package/gallery/data/gh-prs-nextjs.json +419 -0
  228. package/gallery/data/gh-run-failed.json +1 -0
  229. package/gallery/data/gh-run-passed.json +1 -0
  230. package/gallery/data/gh-run-running.json +1 -0
  231. package/gallery/data/github-release.json +57 -0
  232. package/gallery/data/hyperfine-startup.json +187 -0
  233. package/gallery/data/lcov.info +234 -0
  234. package/gallery/data/logs/media-sync.log +127 -0
  235. package/gallery/data/logs/thumbnailer.jsonl +17 -0
  236. package/gallery/data/logs/vitest-run.log +89 -0
  237. package/gallery/data/npm-audit.json +672 -0
  238. package/gallery/data/npm-outdated.json +179 -0
  239. package/gallery/data/oxlint.json +156 -0
  240. package/gallery/data/pip-outdated.json +68 -0
  241. package/gallery/data/pnpm-outdated.json +58 -0
  242. package/gallery/data/provenance/devicectl-install.json +35 -0
  243. package/gallery/data/provenance/install-timeout.log +4 -0
  244. package/gallery/data/provenance/iphone-install.json +72 -0
  245. package/gallery/data/provenance/iphone-no-receipt.json +57 -0
  246. package/gallery/data/provenance/live-home.jpg +0 -0
  247. package/gallery/data/provenance/live-probe.txt +10 -0
  248. package/gallery/data/provenance/xcodebuild-2811.log +19 -0
  249. package/gallery/data/provenance/xcodebuild-4712.log +19 -0
  250. package/gallery/data/pytest-junit.xml +64 -0
  251. package/gallery/data/python-org-trace.json +35 -0
  252. package/gallery/data/python-org.har +31 -0
  253. package/gallery/data/retry.ts +13 -0
  254. package/gallery/data/ruff.json +261 -0
  255. package/gallery/data/src/durations.py +63 -0
  256. package/gallery/data/src/json-logic.ts +156 -0
  257. package/gallery/data/src/mapMath.ts +135 -0
  258. package/gallery/data/src/semver/bump.go +73 -0
  259. package/gallery/data/traces/browser-error.json +5 -0
  260. package/gallery/data/traces/go/main.go +27 -0
  261. package/gallery/data/traces/go-panic.txt +16 -0
  262. package/gallery/data/traces/java-caused-by.txt +15 -0
  263. package/gallery/data/traces/node-aggregate.txt +30 -0
  264. package/gallery/data/traces/node-cause.txt +21 -0
  265. package/gallery/data/traces/py/deploy.json +5 -0
  266. package/gallery/data/traces/py/deploy.py +17 -0
  267. package/gallery/data/traces/py/settings.py +19 -0
  268. package/gallery/data/traces/python-chain.txt +30 -0
  269. package/gallery/data/traces/rust/main.rs +11 -0
  270. package/gallery/data/traces/rust-panic.txt +16 -0
  271. package/gallery/data/traces/shop/cart.mjs +19 -0
  272. package/gallery/data/traces/shop/pricing.mjs +12 -0
  273. package/gallery/data/traces/shop/status.mjs +9 -0
  274. package/gallery/data/traces/web/app.js +11 -0
  275. package/gallery/data/traces/web/index.html +1 -0
  276. package/gallery/data/traces/web/search.js +10 -0
  277. package/gallery/data/transcript/call-noisy.wav +0 -0
  278. package/gallery/data/transcript/call.json +362 -0
  279. package/gallery/data/transcript/call.wav +0 -0
  280. package/gallery/data/transcript/mlx-whisper-tiny.en-noisy.json +1 -0
  281. package/gallery/data/transcript/whisper-cpp-base.en-noisy.json +658 -0
  282. package/gallery/data/transcript/whisper-cpp-base.en.json +658 -0
  283. package/gallery/data/transcript/whisper-cpp-base.en.srt +16 -0
  284. package/gallery/data/transcript/whisper-cpp-tiny.en-noisy.json +619 -0
  285. package/gallery/data/tsc.txt +4 -0
  286. package/gallery/data/vitest-bench.json +119 -0
  287. package/gallery/data/vitest-junit.xml +133 -0
  288. package/gallery/data/vitest-report.json +612 -0
  289. package/gallery/images/bell.svg +10 -0
  290. package/gallery/images/bike.svg +47 -0
  291. package/gallery/images/footer-planet.webp +0 -0
  292. package/gallery/images/front-light.svg +13 -0
  293. package/gallery/images/golden-hour.webp +0 -0
  294. package/gallery/images/option-flowers.webp +0 -0
  295. package/gallery/images/option-hilltop.webp +0 -0
  296. package/gallery/images/option-meadow.webp +0 -0
  297. package/gallery/images/option-sky.webp +0 -0
  298. package/gallery/images/u-lock.svg +10 -0
  299. package/index.client.tsx +164 -0
  300. package/index.server.ts +216 -0
  301. package/package.json +50 -4
  302. package/paseo-plugin.json +14 -0
  303. package/server/data-source.ts +407 -0
  304. package/server/project.ts +981 -0
  305. package/server/web/address.ts +57 -0
  306. package/server/web/cache.ts +85 -0
  307. package/server/web/commons.ts +126 -0
  308. package/server/web/geocode.ts +40 -0
  309. package/server/web/image.ts +35 -0
  310. package/server/web/lookups.ts +248 -0
  311. package/server/web/net.ts +210 -0
  312. package/shared/core/acceptance.ts +744 -0
  313. package/shared/core/ast.ts +76 -0
  314. package/shared/core/benchmark.ts +743 -0
  315. package/shared/core/catalog.ts +733 -0
  316. package/shared/core/compare.ts +225 -0
  317. package/shared/core/coverage.ts +456 -0
  318. package/shared/core/data.ts +334 -0
  319. package/shared/core/decisions.ts +527 -0
  320. package/shared/core/dependencies.ts +604 -0
  321. package/shared/core/diagnostics.ts +24 -0
  322. package/shared/core/environments.ts +212 -0
  323. package/shared/core/evaluate.ts +737 -0
  324. package/shared/core/fields.ts +66 -0
  325. package/shared/core/findings.ts +1172 -0
  326. package/shared/core/form.ts +69 -0
  327. package/shared/core/index.ts +44 -0
  328. package/shared/core/junit.ts +252 -0
  329. package/shared/core/lexer.ts +275 -0
  330. package/shared/core/logs.ts +496 -0
  331. package/shared/core/markdown.ts +2374 -0
  332. package/shared/core/parser.ts +355 -0
  333. package/shared/core/perf-units.ts +228 -0
  334. package/shared/core/pipeline.ts +679 -0
  335. package/shared/core/project.ts +472 -0
  336. package/shared/core/provenance.ts +725 -0
  337. package/shared/core/stack-trace.ts +718 -0
  338. package/shared/core/streaming.ts +136 -0
  339. package/shared/core/test-results.ts +562 -0
  340. package/shared/core/thread-activity.ts +203 -0
  341. package/shared/core/tracking.ts +887 -0
  342. package/shared/core/transcript.ts +1364 -0
  343. package/shared/core/treemap.ts +446 -0
  344. package/shared/core/types.ts +70 -0
  345. package/shared/core/validate.ts +574 -0
  346. package/shared/core/waterfall.ts +536 -0
  347. package/shared/diagram/flowchart.ts +205 -0
  348. package/shared/diagram/layout.ts +440 -0
  349. package/shared/diagram/markdown.ts +135 -0
  350. package/shared/diagram/sequence.ts +122 -0
  351. package/shared/diagram/zoom.ts +114 -0
  352. package/shared/fallback.ts +120 -0
  353. package/shared/genui.test.ts +122 -0
  354. package/shared/instructions.ts +144 -0
  355. package/shared/logic/calendarMath.ts +130 -0
  356. package/shared/logic/chartMath.ts +289 -0
  357. package/shared/logic/choose-logic.ts +196 -0
  358. package/shared/logic/compare-logic.ts +270 -0
  359. package/shared/logic/content-model.ts +227 -0
  360. package/shared/logic/environments-logic.ts +53 -0
  361. package/shared/logic/explain-logic.ts +315 -0
  362. package/shared/logic/fieldLogic.ts +138 -0
  363. package/shared/logic/gallery-search.ts +71 -0
  364. package/shared/logic/icons.ts +322 -0
  365. package/shared/logic/json-logic.ts +153 -0
  366. package/shared/logic/mapMath.ts +133 -0
  367. package/shared/logic/project-logic.ts +39 -0
  368. package/shared/logic/references-model.ts +201 -0
  369. package/shared/logic/sketch.ts +165 -0
  370. package/shared/logic/supersession.ts +77 -0
  371. package/shared/logic/values.ts +133 -0
  372. package/shared/logic/widget-logic.ts +163 -0
  373. package/shared/rpc.ts +252 -0
  374. package/shared/segment.ts +81 -0
  375. package/shared/settings.ts +12 -0
@@ -0,0 +1,201 @@
1
+ // Pure readers behind the reference renderers (references.tsx): Entity,
2
+ // Sources and Hotspots. They turn the loose shapes models write into exactly
3
+ // what the components draw, and report what they cannot read instead of
4
+ // throwing. A list that is still streaming ends in a half-written item; the
5
+ // readers skip that last item quietly rather than flash an error for it.
6
+ import type { Value } from "../core/types";
7
+ import { isNode, isObject, text } from "./values";
8
+ import type { KeyValuePair } from "./content-model";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // URLs and images
12
+
13
+ /** An absolute URL with one of `schemes` (lowercase, without `:`), or null. */
14
+ export function parseUrl(raw: string, schemes: readonly string[]): URL | null {
15
+ const trimmed = raw.trim();
16
+ if (trimmed === "") return null;
17
+ let url: URL;
18
+ try {
19
+ url = new URL(trimmed);
20
+ } catch {
21
+ return null;
22
+ }
23
+ return schemes.includes(url.protocol.slice(0, -1)) && url.hostname !== "" ? url : null;
24
+ }
25
+
26
+ /** The host a reader recognizes: `www.` dropped, port kept. */
27
+ export function domainOf(url: URL): string {
28
+ return url.host.replace(/^www\./i, "");
29
+ }
30
+
31
+ /** The readable part of a URL after its host (`/docs/intro` → `docs/intro`), or null for a bare host. */
32
+ export function pathOf(url: URL): string | null {
33
+ let path: string;
34
+ try {
35
+ path = decodeURIComponent(url.pathname);
36
+ } catch {
37
+ path = url.pathname;
38
+ }
39
+ path = path.replace(/^\/+|\/+$/g, "");
40
+ return path === "" ? null : path;
41
+ }
42
+
43
+ /** What an image `src` names: an https URL or a thread file, both loaded through the runtime; or a refusal. */
44
+ export type ImageRef =
45
+ | { readonly kind: "none" }
46
+ | { readonly kind: "load"; readonly src: string }
47
+ | { readonly kind: "invalid"; readonly error: string };
48
+
49
+ export function imageRef(src: string): ImageRef {
50
+ const trimmed = src.trim();
51
+ if (trimmed === "") return { kind: "none" };
52
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed)?.[1]?.toLowerCase();
53
+ // `C:\…` is a Windows path, not a URL scheme.
54
+ if (scheme === undefined || /^[a-z]:[\\/]/i.test(trimmed)) return { kind: "load", src: trimmed };
55
+ if (scheme !== "https") {
56
+ return { kind: "invalid", error: `Images must be https URLs or file paths; ${scheme}: links can't be shown` };
57
+ }
58
+ const url = parseUrl(trimmed, ["https"]);
59
+ return url === null ? { kind: "invalid", error: `Invalid image URL: ${trimmed}` } : { kind: "load", src: url.href };
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Entity
64
+
65
+ export const ENTITY_KINDS = ["product", "place", "person", "org", "link", "other"] as const;
66
+ export type EntityKind = (typeof ENTITY_KINDS)[number];
67
+
68
+ export function entityKind(value: unknown): EntityKind {
69
+ return typeof value === "string" && (ENTITY_KINDS as readonly string[]).includes(value)
70
+ ? (value as EntityKind)
71
+ : "other";
72
+ }
73
+
74
+ /** The facts worth showing on the card itself: the first `max` with a plain, non-empty value. */
75
+ export function inlineFacts(
76
+ pairs: readonly KeyValuePair[],
77
+ max: number,
78
+ ): { readonly label: string; readonly value: string }[] {
79
+ const out: { label: string; value: string }[] = [];
80
+ for (const [label, value] of pairs) {
81
+ if (out.length >= max) break;
82
+ const shown = typeof value === "string" ? value.trim() : typeof value === "number" || typeof value === "boolean" ? String(value) : "";
83
+ if (shown !== "") out.push({ label: label.trim(), value: shown });
84
+ }
85
+ return out;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Sources
90
+
91
+ export type SourceLink =
92
+ | {
93
+ readonly ok: true;
94
+ /** 1-based position in the list as written, so `[2]` in prose names the second link. */
95
+ readonly number: number;
96
+ readonly href: string;
97
+ /** The given title, else the domain. */
98
+ readonly title: string;
99
+ readonly domain: string;
100
+ /** The path, shown when there is no title to say what the page is. */
101
+ readonly path: string | null;
102
+ readonly note: string | null;
103
+ }
104
+ | { readonly ok: false; readonly number: number; readonly error: string };
105
+
106
+ /** `https://…` strings or `{url, title?, note?}` objects; anything else keeps its number and reports why. */
107
+ export function sourceLinks(items: readonly Value[]): SourceLink[] {
108
+ const out: SourceLink[] = [];
109
+ items.forEach((item, index) => {
110
+ const number = index + 1;
111
+ const last = index === items.length - 1;
112
+ const fields: Readonly<Record<string, unknown>> | null =
113
+ typeof item === "string" ? { url: item } : isObject(item) && !isNode(item) ? item : null;
114
+ if (fields === null) {
115
+ out.push({ ok: false, number, error: "Each source must be an https URL or {url, title?, note?}" });
116
+ return;
117
+ }
118
+ // A streaming `{title: "…"` has not reached its url yet.
119
+ if (fields.url === undefined && last) return;
120
+ const raw = text(fields.url).trim();
121
+ const url = parseUrl(raw, ["https"]);
122
+ if (url === null) {
123
+ out.push({
124
+ ok: false,
125
+ number,
126
+ error: raw === "" ? "This source has no url" : `Not an https link: ${raw}`,
127
+ });
128
+ return;
129
+ }
130
+ const title = text(fields.title).trim();
131
+ const note = text(fields.note).trim();
132
+ out.push({
133
+ ok: true,
134
+ number,
135
+ href: url.href,
136
+ title: title === "" ? domainOf(url) : title,
137
+ domain: domainOf(url),
138
+ path: title === "" ? pathOf(url) : null,
139
+ note: note === "" ? null : note,
140
+ });
141
+ });
142
+ return out;
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Hotspots
147
+
148
+ export interface HotspotPoint {
149
+ /** 1-based, shared by the pin and its row in the list. */
150
+ readonly number: number;
151
+ /** Percent across the image, 0–100. */
152
+ readonly x: number;
153
+ /** Percent down the image, 0–100. */
154
+ readonly y: number;
155
+ readonly title: string;
156
+ readonly text: string | null;
157
+ }
158
+
159
+ /** A percentage: a number, or a string such as `"40"` or `"40%"`; clamped to 0–100. */
160
+ export function percent(value: unknown): number | null {
161
+ const number =
162
+ typeof value === "number"
163
+ ? value
164
+ : typeof value === "string" && /^\s*-?\d+(\.\d+)?\s*%?\s*$/.test(value)
165
+ ? Number.parseFloat(value)
166
+ : Number.NaN;
167
+ return Number.isFinite(number) ? Math.min(100, Math.max(0, number)) : null;
168
+ }
169
+
170
+ /**
171
+ * `{x, y, title, text?}` points, numbered in order. A point without a
172
+ * usable x/y is reported (its number goes to the next good point); the last
173
+ * point is skipped quietly while it lacks x, y or title, because a
174
+ * streaming list ends in one that is still being written.
175
+ */
176
+ export function hotspotPoints(items: readonly Value[]): {
177
+ readonly points: HotspotPoint[];
178
+ readonly problems: string[];
179
+ } {
180
+ const points: HotspotPoint[] = [];
181
+ const problems: string[] = [];
182
+ items.forEach((item: unknown, index) => {
183
+ const last = index === items.length - 1;
184
+ if (!isObject(item) || isNode(item)) {
185
+ problems.push(`Point ${index + 1} must be {x, y, title, text?}`);
186
+ return;
187
+ }
188
+ const x = percent(item.x);
189
+ const y = percent(item.y);
190
+ const title = text(item.title).trim();
191
+ if (last && (x === null || y === null || title === "")) return;
192
+ if (x === null || y === null) {
193
+ problems.push(`Point ${index + 1}${title === "" ? "" : ` (${title})`} needs x and y in % (0–100)`);
194
+ return;
195
+ }
196
+ const body = text(item.text).trim();
197
+ const number = points.length + 1;
198
+ points.push({ number, x, y, title: title === "" ? `Point ${number}` : title, text: body === "" ? null : body });
199
+ });
200
+ return { points, problems };
201
+ }
@@ -0,0 +1,165 @@
1
+ // Turns a model's Mermaid source into an Excalidraw-looking diagram: Mermaid's
2
+ // hand-drawn look (rough strokes, hachure fills) in Excalifont, in Excalidraw's
3
+ // palette. Excalidraw's dark theme is its light canvas through
4
+ // `invert(93%) hue-rotate(180deg)`; the dark palette here is that filter
5
+ // worked out per colour, so the SVG is drawn dark natively and BB's zoom view,
6
+ // which copies the SVG elsewhere, shows it right too. Pure: no DOM.
7
+
8
+ /** The font family the diagram text uses; Excalifont is loaded by sketchFont.ts. */
9
+ export const SKETCH_FONT_FAMILY = "Excalifont, 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', cursive";
10
+
11
+ /** Excalidraw's stroke and fill colours (its default palette, light theme). */
12
+ const LIGHT = {
13
+ ink: "#1e1e1e",
14
+ canvas: "#ffffff",
15
+ blue: "#a5d8ff",
16
+ green: "#b2f2bb",
17
+ yellow: "#ffec99",
18
+ red: "#ffc9c9",
19
+ violet: "#d0bfff",
20
+ gray: "#f1f3f5",
21
+ /** Clickable nodes (Diagram details): outline, hover hachure, selected hachure. */
22
+ link: "#1971c2",
23
+ hover: "#74c0fc",
24
+ selected: "#ffd43b",
25
+ } as const;
26
+
27
+ export type SketchPalette = { readonly [K in keyof typeof LIGHT]: string };
28
+
29
+ const channel = (value: number) => Math.round(Math.min(255, Math.max(0, value)));
30
+
31
+ /** A colour through Excalidraw's dark-theme filter: invert(93%), then hue-rotate(180deg). */
32
+ export function darkened(hex: string): string {
33
+ const [r, g, b] = [1, 3, 5].map((at) => 255 * 0.93 + parseInt(hex.slice(at, at + 2), 16) * (1 - 2 * 0.93));
34
+ // The CSS hue-rotate matrix at 180°.
35
+ const rotated = [-0.574 * r + 1.43 * g + 0.144 * b, 0.426 * r + 0.43 * g + 0.144 * b, 0.426 * r + 1.43 * g - 0.856 * b];
36
+ return `#${rotated.map((value) => channel(value).toString(16).padStart(2, "0")).join("")}`;
37
+ }
38
+
39
+ const DARK = Object.fromEntries(Object.entries(LIGHT).map(([name, hex]) => [name, darkened(hex)])) as SketchPalette;
40
+
41
+ export function sketchPalette(dark: boolean): SketchPalette {
42
+ return dark ? DARK : LIGHT;
43
+ }
44
+
45
+ /** One seed, so every render of the same diagram draws the same wobble. */
46
+ const SEED = 7;
47
+
48
+ /** `backdrop` is the colour the diagram is drawn on; edge labels are cut out of the lines with it. */
49
+ function frontmatter(colors: SketchPalette, backdrop: string): string {
50
+ const variables: Record<string, string> = {
51
+ fontFamily: SKETCH_FONT_FAMILY,
52
+ fontSize: "17px",
53
+ background: backdrop,
54
+ primaryColor: colors.blue,
55
+ mainBkg: colors.blue,
56
+ secondaryColor: colors.green,
57
+ tertiaryColor: colors.yellow,
58
+ primaryBorderColor: colors.ink,
59
+ nodeBorder: colors.ink,
60
+ primaryTextColor: colors.ink,
61
+ textColor: colors.ink,
62
+ lineColor: colors.ink,
63
+ edgeLabelBackground: backdrop,
64
+ clusterBkg: colors.gray,
65
+ clusterBorder: colors.ink,
66
+ titleColor: colors.ink,
67
+ actorBkg: colors.blue,
68
+ actorBorder: colors.ink,
69
+ actorTextColor: colors.ink,
70
+ actorLineColor: colors.ink,
71
+ signalColor: colors.ink,
72
+ signalTextColor: colors.ink,
73
+ labelBoxBkgColor: colors.yellow,
74
+ labelBoxBorderColor: colors.ink,
75
+ labelTextColor: colors.ink,
76
+ loopTextColor: colors.ink,
77
+ noteBkgColor: colors.yellow,
78
+ noteBorderColor: colors.ink,
79
+ noteTextColor: colors.ink,
80
+ activationBkgColor: colors.gray,
81
+ activationBorderColor: colors.ink,
82
+ };
83
+ return [
84
+ "---",
85
+ "config:",
86
+ " look: handDrawn",
87
+ ` handDrawnSeed: ${SEED}`,
88
+ " theme: base",
89
+ " darkMode: false",
90
+ ` fontFamily: "${SKETCH_FONT_FAMILY}"`,
91
+ " themeVariables:",
92
+ ...Object.entries(variables).map(([name, value]) => ` ${name}: "${value}"`),
93
+ "---",
94
+ ].join("\n");
95
+ }
96
+
97
+ /** Flowchart node shapes that get their own colour, as Excalidraw users colour-code. */
98
+ const SHAPE_CLASSES = [
99
+ // id{text} or id{{text}}: a decision.
100
+ { name: "sketchDecision", color: "yellow", openers: ["{", "{{"] },
101
+ // id[(text)]: a data store.
102
+ { name: "sketchStore", color: "green", openers: ["[("] },
103
+ // id((text)) or id(((text))): a start or end point.
104
+ { name: "sketchTerminal", color: "red", openers: ["((", "((("] },
105
+ // id>text]: a flag, often an input or event.
106
+ { name: "sketchEvent", color: "violet", openers: [">"] },
107
+ ] as const;
108
+
109
+ /** Lines that define no nodes. */
110
+ const NOT_NODES = /^\s*(%%|subgraph\b|end\b|direction\b|click\b|class\b|classDef\b|style\b|linkStyle\b|flowchart\b|graph\b)/;
111
+ /** Arrows and `&` separate the nodes on a line: -->, ---, -.->, ==>, --o, <-->, ~~~ … */
112
+ const SEPARATORS = /\s*(?:<?(?:-{2,}|-\.+-|={2,}|~{3,})[->ox]?|&)\s*/;
113
+ /** A segment that starts a node: its id and the shape opener after it, if any. */
114
+ const NODE_START = /^([A-Za-z_][\w-]*)\s*(\{\{|\{|\[\(|\(\(\(|\(\(|>)?/;
115
+
116
+ function isFlowchart(source: string): boolean {
117
+ return /^\s*(flowchart|graph)\b/.test(source);
118
+ }
119
+
120
+ /** The model set its own look: front matter or an init directive. */
121
+ function hasOwnStyling(source: string): boolean {
122
+ return /^\s*---/.test(source) || /%%\{\s*init/.test(source);
123
+ }
124
+
125
+ function hasOwnClasses(source: string): boolean {
126
+ return /\bclassDef\b|^\s*style\s|:::/m.test(source);
127
+ }
128
+
129
+ /** Class lines colouring decisions, data stores, terminals and events in a flowchart. */
130
+ export function shapeClassLines(source: string, colors: SketchPalette = LIGHT): string[] {
131
+ if (!isFlowchart(source) || hasOwnClasses(source)) return [];
132
+ const members = new Map<string, Set<string>>(SHAPE_CLASSES.map((shape) => [shape.name, new Set()]));
133
+ for (const line of source.split("\n")) {
134
+ if (NOT_NODES.test(line)) continue;
135
+ // Edge text (|…|) and quoted labels can hold braces that are not shapes.
136
+ const bare = line.replace(/\|[^|]*\|/g, " ").replace(/"[^"]*"/g, '""');
137
+ for (const segment of bare.split(SEPARATORS)) {
138
+ const [, id, opener] = NODE_START.exec(segment.trim()) ?? [];
139
+ if (id === undefined || opener === undefined) continue;
140
+ const shape = SHAPE_CLASSES.find((candidate) => (candidate.openers as readonly string[]).includes(opener));
141
+ if (shape !== undefined) members.get(shape.name)?.add(id);
142
+ }
143
+ }
144
+ const lines: string[] = [];
145
+ for (const shape of SHAPE_CLASSES) {
146
+ const ids = [...(members.get(shape.name) ?? [])];
147
+ if (ids.length === 0) continue;
148
+ lines.push(` classDef ${shape.name} fill:${colors[shape.color]}`, ` class ${ids.join(",")} ${shape.name}`);
149
+ }
150
+ return lines;
151
+ }
152
+
153
+ /**
154
+ * The source BB's Mermaid renderer gets. A diagram that brings its own front
155
+ * matter or init directive keeps it untouched; one with its own class or style
156
+ * lines keeps its colours but still gets the hand-drawn look. The diagram has
157
+ * no canvas of its own: it is drawn on `backdrop`, Excalidraw's canvas colour
158
+ * if none is given.
159
+ */
160
+ export function sketchMermaid(source: string, dark = false, backdrop?: string): string {
161
+ const trimmed = source.trim();
162
+ if (hasOwnStyling(trimmed)) return trimmed;
163
+ const colors = sketchPalette(dark);
164
+ return [frontmatter(colors, backdrop ?? colors.canvas), trimmed, ...shapeClassLines(trimmed, colors)].join("\n");
165
+ }
@@ -0,0 +1,77 @@
1
+ // Pure rules behind useSuperseded: whether a reply has been answered, and
2
+ // which thread changes can change that. No React, no DOM, no SDK.
3
+
4
+ /** One message of BB's conversation outline (`sdk.threads.conversationOutline`), in thread order. */
5
+ export interface OutlineItem {
6
+ readonly id: string;
7
+ readonly role: "assistant" | "user";
8
+ }
9
+
10
+ /** A user row's event sequence, from its `<thread>:user-seed:<seq>` id; null for any other id. */
11
+ function userSeq(id: string): number | null {
12
+ const match = /:user-seed:(\d+)$/.exec(id);
13
+ return match === null ? null : Number(match[1]);
14
+ }
15
+
16
+ /**
17
+ * Whether a user message follows the message `anchorId` (the assistant reply
18
+ * holding a block). Null when the anchor is not in the outline, so the
19
+ * caller can keep what it knew: a nested agent's message, or a reply the
20
+ * outline no longer lists.
21
+ *
22
+ * A user row whose sequence is below one already seen is out of place, not a
23
+ * reply: BB's outline can list a rejected steer, sent long before, at its end.
24
+ */
25
+ export function hasLaterUserMessage(items: readonly OutlineItem[] | null, anchorId: string): boolean | null {
26
+ if (items === null) return null;
27
+ const anchor = items.findIndex((item) => item.id === anchorId);
28
+ if (anchor === -1) return null;
29
+ let highest = -1;
30
+ for (const [index, item] of items.entries()) {
31
+ if (item.role !== "user") continue;
32
+ const seq = userSeq(item.id);
33
+ if (seq !== null && seq < highest) continue;
34
+ if (index > anchor) return true;
35
+ if (seq !== null) highest = seq;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /** The part of BB's `thread:changed` realtime event this module reads. */
41
+ export interface ThreadChange {
42
+ readonly changes: readonly string[];
43
+ readonly metadata?: { readonly eventTypes?: readonly string[] };
44
+ }
45
+
46
+ /**
47
+ * Thread events that add, accept, reject or drop a user message. Every user
48
+ * message (typed, sent by a plugin or the CLI, dispatched from the queue)
49
+ * enters as `client/turn/requested`; the rest cover steering, manager
50
+ * messages and context resets.
51
+ */
52
+ const CONVERSATION_EVENTS: Readonly<Record<string, true>> = {
53
+ "client/thread/start": true,
54
+ "client/turn/requested": true,
55
+ "client/turn/rejected": true,
56
+ "client/turn/start": true,
57
+ "turn/input/accepted": true,
58
+ "turn/started": true,
59
+ "system/manager/user_message": true,
60
+ "thread/context/cleared": true,
61
+ "thread/compacted": true,
62
+ };
63
+
64
+ /**
65
+ * Whether a change can move a user message in or out of the outline, so it
66
+ * must be read again. Streaming deltas and tool progress never can, which
67
+ * keeps a running reply from refetching on every token.
68
+ */
69
+ export function affectsConversation(change: ThreadChange): boolean {
70
+ return change.changes.some((kind) => {
71
+ if (kind === "history-rewritten" || kind === "history-compacted") return true;
72
+ if (kind !== "events-appended") return false;
73
+ const types = change.metadata?.eventTypes;
74
+ // An append that doesn't say what it appended may be anything.
75
+ return types === undefined || types.some((type) => Object.hasOwn(CONVERSATION_EVENTS, type));
76
+ });
77
+ }
@@ -0,0 +1,133 @@
1
+ // Defensive readers for evaluated props. The core normalizes params by kind,
2
+ // but plain objects in a spec can mimic node/action shapes, so every reader
3
+ // checks the shape it relies on instead of trusting the declared kind.
4
+ import type { Action, Binding, Scalar, UiNode, Value } from "../core/types";
5
+
6
+ export type Props = Readonly<Record<string, Value>>;
7
+
8
+ /** The one object guard of the renderer; callers still check each field. */
9
+ export function isObject(value: unknown): value is Readonly<Record<string, unknown>> {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ export function asRecord(value: unknown): Readonly<Record<string, Value>> | null {
14
+ if (!isObject(value)) return null;
15
+ if ("kind" in value && (value.kind === "node" || value.kind === "action" || value.kind === "binding")) {
16
+ return null;
17
+ }
18
+ return value as Readonly<Record<string, Value>>;
19
+ }
20
+
21
+ export function isNode(value: unknown): value is UiNode {
22
+ return (
23
+ isObject(value) &&
24
+ value.kind === "node" &&
25
+ typeof value.type === "string" &&
26
+ typeof value.key === "string" &&
27
+ isObject(value.props)
28
+ );
29
+ }
30
+
31
+ export function isBinding(value: unknown): value is Binding {
32
+ return (
33
+ isObject(value) &&
34
+ value.kind === "binding" &&
35
+ typeof value.name === "string" &&
36
+ "value" in value
37
+ );
38
+ }
39
+
40
+ export function isAction(value: unknown): value is Action {
41
+ if (!isObject(value) || value.kind !== "action") return false;
42
+ switch (value.type) {
43
+ case "send":
44
+ case "copy":
45
+ return typeof value.text === "string";
46
+ case "openUrl":
47
+ return typeof value.url === "string";
48
+ case "set":
49
+ return typeof value.name === "string" && "value" in value;
50
+ default:
51
+ return false;
52
+ }
53
+ }
54
+
55
+ export function isScalar(value: unknown): value is Scalar {
56
+ return (
57
+ value === null ||
58
+ typeof value === "string" ||
59
+ typeof value === "number" ||
60
+ typeof value === "boolean"
61
+ );
62
+ }
63
+
64
+ /** A `string` param: scalars become strings, anything else is empty. */
65
+ export function text(value: unknown): string {
66
+ if (typeof value === "string") return value;
67
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
68
+ return "";
69
+ }
70
+
71
+ /** An optional `string` param: absent or null stays null. */
72
+ export function optionalText(value: unknown): string | null {
73
+ return value === undefined || value === null ? null : text(value);
74
+ }
75
+
76
+ export function finiteNumber(value: unknown): number | null {
77
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
78
+ }
79
+
80
+ export function list(value: Value | undefined): readonly Value[] {
81
+ return Array.isArray(value) ? value : [];
82
+ }
83
+
84
+ export function nodes(value: Value | undefined): UiNode[] {
85
+ return list(value).filter(isNode);
86
+ }
87
+
88
+ export type Tone = "neutral" | "good" | "warn" | "bad" | "info";
89
+
90
+ /** BB's timeline accent (a muted blue in the stock themes), else the primary ink. */
91
+ export const ACCENT = "var(--color-timeline-accent,var(--primary))";
92
+
93
+ /** Text color per tone; neutral inherits. */
94
+ export const TONE_TEXT: Record<Tone, string> = {
95
+ neutral: "",
96
+ good: "text-success",
97
+ warn: "text-warning-text",
98
+ bad: "text-destructive",
99
+ info: "text-[var(--color-timeline-accent,var(--primary))]",
100
+ };
101
+
102
+ /** Status-dot fill per tone. */
103
+ export const TONE_DOT: Record<Tone, string> = {
104
+ neutral: "bg-muted-foreground/45",
105
+ good: "bg-success",
106
+ warn: "bg-warning",
107
+ bad: "bg-destructive",
108
+ info: "bg-[var(--color-timeline-accent,var(--primary))]",
109
+ };
110
+
111
+ /** Pill surface per tone: a faint wash of the tone, no border. */
112
+ export const TONE_PILL: Record<Tone, string> = {
113
+ neutral: "bg-surface-recessed text-muted-foreground",
114
+ good: "bg-success/12 text-success",
115
+ warn: "bg-warning/14 text-warning-text",
116
+ bad: "bg-destructive/10 text-destructive",
117
+ info: "bg-[color-mix(in_oklch,var(--color-timeline-accent,var(--primary))_12%,transparent)] text-[var(--color-timeline-accent,var(--primary))]",
118
+ };
119
+
120
+ export function tone(value: Value | undefined): Tone {
121
+ return typeof value === "string" && Object.hasOwn(TONE_TEXT, value)
122
+ ? (value as Tone)
123
+ : "neutral";
124
+ }
125
+
126
+ const NUMBER_FORMAT = new Intl.NumberFormat("en-US", {
127
+ maximumFractionDigits: 2,
128
+ });
129
+
130
+ /** Locale-independent grouping, up to 2 decimals (matches `fmt`). */
131
+ export function formatNumber(value: number): string {
132
+ return NUMBER_FORMAT.format(value);
133
+ }