stream_weaver 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 (381) hide show
  1. checksums.yaml +7 -0
  2. data/.beads/.gitignore +77 -0
  3. data/.beads/README.md +81 -0
  4. data/.beads/config.yaml +54 -0
  5. data/.beads/metadata.json +7 -0
  6. data/.rspec +3 -0
  7. data/AGENTS.md +91 -0
  8. data/CHANGELOG.md +294 -0
  9. data/CLAUDE.md +67 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +565 -0
  12. data/Rakefile +13 -0
  13. data/assets/streamweaver-hero.jpg +0 -0
  14. data/bench/baselines/ledger.rb +58 -0
  15. data/bench/baselines/warroom.rb +51 -0
  16. data/bench/fixtures/ledger.rb +76 -0
  17. data/bench/fixtures/warroom.rb +60 -0
  18. data/bench/results/6daf8b8.md +102 -0
  19. data/bench/results/c8d749b.md +102 -0
  20. data/bench/results/eb06ab9.md +102 -0
  21. data/bench/run.rb +151 -0
  22. data/bench/support.rb +84 -0
  23. data/copy_this/dto_run_sample_run_7631.html +220 -0
  24. data/docs/SERVICE_MODE.md +302 -0
  25. data/docs/architecture/how_streamweaver_works.md +772 -0
  26. data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
  27. data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
  28. data/docs/canvas-ipc-session-summary.md +127 -0
  29. data/docs/canvas-panel-workflow.md +172 -0
  30. data/docs/canvas-read.md +147 -0
  31. data/docs/canvas-roadmap.md +275 -0
  32. data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
  33. data/docs/claude-code-companion-skill-spec.md +567 -0
  34. data/docs/components_reference.md +1223 -0
  35. data/docs/crud-patterns.md +265 -0
  36. data/docs/endpoints.md +122 -0
  37. data/docs/for_llms.md +1 -0
  38. data/docs/form-patterns.md +165 -0
  39. data/docs/frontend-only.md +169 -0
  40. data/docs/html-artifact-audit.md +238 -0
  41. data/docs/ideas/2025-01-01-as-a-service.md +203 -0
  42. data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
  43. data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
  44. data/docs/ideas/dhh-review-future-refactors.md +142 -0
  45. data/docs/opal-jamstack.md +47 -0
  46. data/docs/opal-npm-direction.md +184 -0
  47. data/docs/opal-spike-findings.md +194 -0
  48. data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
  49. data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
  50. data/docs/plans/2026-01-02-charts-design.md +288 -0
  51. data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
  52. data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
  53. data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
  54. data/docs/plans/canvas-claude-project-design.md +238 -0
  55. data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
  56. data/docs/plans/org-doc-preview-surfaces.md +293 -0
  57. data/docs/plans/shadcn-polish-plan.md +417 -0
  58. data/docs/porting-artifacts.md +252 -0
  59. data/docs/reference/agent-skills-comparison.md +369 -0
  60. data/docs/reference/travel-state-prd.artifact.html +780 -0
  61. data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
  62. data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
  63. data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
  64. data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
  65. data/docs/research/frontend-only-matrix.md +278 -0
  66. data/docs/research/streamweaver-way-spike-findings.md +529 -0
  67. data/docs/resource-dsl.md +342 -0
  68. data/docs/routing.md +302 -0
  69. data/docs/ruby-ui-comparison.md +319 -0
  70. data/docs/shared-dsl-fragments.md +356 -0
  71. data/docs/streamweaver-for-ai-agents.md +401 -0
  72. data/docs/streamweaver-frontend-vision.md +123 -0
  73. data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
  74. data/docs/streamweaver_canvas/save_example.rb +9 -0
  75. data/docs/templates.md +213 -0
  76. data/docs/testing.md +155 -0
  77. data/docs/theming-hooks.md +281 -0
  78. data/docs/tutorials/the-streamweaver-way.md +882 -0
  79. data/docs/university/capability-inventory.md +137 -0
  80. data/docs/university/dependency-survey.md +114 -0
  81. data/docs/university/design-spec.md +217 -0
  82. data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
  83. data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
  84. data/docs/university/mockups/course-list-dark.png +0 -0
  85. data/docs/university/mockups/course-list-light.png +0 -0
  86. data/docs/university/mockups/course_canvas_mockup.rb +741 -0
  87. data/docs/university/mockups/step-screen-dark.png +0 -0
  88. data/docs/university/mockups/step-screen-light.png +0 -0
  89. data/docs/university/roadmap.md +72 -0
  90. data/docs/university/send-to-coworker.md +77 -0
  91. data/docs/university/worker-session-mining.md +109 -0
  92. data/docs/visual/browser-rendering-spike.md +173 -0
  93. data/docs/visual/builder-visual-plan-analysis.md +378 -0
  94. data/docs/visual/sw-plan-format-exploration.md +511 -0
  95. data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
  96. data/docs/visual-skills/PROGRESS.md +60 -0
  97. data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
  98. data/docs/visual-skills/analysis/components.md +317 -0
  99. data/docs/visual-skills/analysis/overlap.md +360 -0
  100. data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
  101. data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
  102. data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
  103. data/docs/visual-skills/analysis/visual-explainer.md +964 -0
  104. data/docs/visual-skills/blog/blog-series-outline.md +39 -0
  105. data/docs/visual-skills/blog/token-efficiency.md +73 -0
  106. data/docs/visual-skills/design/architecture.md +1725 -0
  107. data/docs/visual-skills/design/codex-review.md +340 -0
  108. data/docs/visual-skills/design/dhh-review.md +209 -0
  109. data/docs/visual-skills/design/evolution.md +100 -0
  110. data/docs/visual-skills/design/gemini-review.md +55 -0
  111. data/docs/visual-skills/design/review-synthesis.md +149 -0
  112. data/docs/visual-skills/implementation/STATE.md +22 -0
  113. data/docs/visual-skills/implementation/plan.md +347 -0
  114. data/docs/visual-skills/implementation/spike-findings.md +237 -0
  115. data/docs/visual-skills/implementation/tasks.md +702 -0
  116. data/docs/visual-skills/lessons-learned/process.md +145 -0
  117. data/examples/README.md +96 -0
  118. data/examples/advanced/all_components.rb +177 -0
  119. data/examples/advanced/examples_browser.rb +394 -0
  120. data/examples/advanced/teachables_browser.rb +261 -0
  121. data/examples/advanced/theme_tweaker.rb +366 -0
  122. data/examples/advanced/tutorial.rb +1528 -0
  123. data/examples/agentic/agentic_form.rb +30 -0
  124. data/examples/agentic/agentic_form_autoclose.rb +30 -0
  125. data/examples/agentic/cultivation_tracker.rb +69 -0
  126. data/examples/basic/hello_world.rb +22 -0
  127. data/examples/basic/opal_tabs_table.rb +44 -0
  128. data/examples/basic/todo_list.rb +39 -0
  129. data/examples/button_loading_test.rb +36 -0
  130. data/examples/canvas/mermaid_canvas_demo.sh +151 -0
  131. data/examples/charts/bar_chart_demo.rb +57 -0
  132. data/examples/charts/line_chart_demo.rb +73 -0
  133. data/examples/charts/pie_area_demo.rb +87 -0
  134. data/examples/charts/stacked_bar_chart_demo.rb +69 -0
  135. data/examples/claude_code/README.md +85 -0
  136. data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
  137. data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
  138. data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
  139. data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
  140. data/examples/claude_code/codebreaker/README.md +213 -0
  141. data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
  142. data/examples/claude_code/tutorial/README.md +102 -0
  143. data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
  144. data/examples/claude_code/verification_flow/README.md +126 -0
  145. data/examples/components/annotated_code_demo.rb +54 -0
  146. data/examples/components/callout_demo.rb +33 -0
  147. data/examples/components/checkbox_group_demo.rb +46 -0
  148. data/examples/components/design_review.css +468 -0
  149. data/examples/components/design_review_demo.rb +44 -0
  150. data/examples/components/design_review_dsl.rb +300 -0
  151. data/examples/components/diff_block_demo.rb +112 -0
  152. data/examples/components/events_demo.rb +184 -0
  153. data/examples/components/form_demo.rb +62 -0
  154. data/examples/components/lesson_demo.rb +53 -0
  155. data/examples/components/markdown_demo.rb +110 -0
  156. data/examples/components/mermaid_demo.rb +166 -0
  157. data/examples/components/pareto_set.rb +64 -0
  158. data/examples/components/prd_demo.rb +21 -0
  159. data/examples/components/prd_dsl.rb +385 -0
  160. data/examples/components/quiz_demo.rb +47 -0
  161. data/examples/components/run_viewer_demo.rb +100 -0
  162. data/examples/components/score_and_collapsible_demo.rb +82 -0
  163. data/examples/components/select_stale_value_smoke_test.rb +39 -0
  164. data/examples/components/table_demo.rb +247 -0
  165. data/examples/components/timer_with_state_demo.rb +58 -0
  166. data/examples/components/todo_due_dates.rb +63 -0
  167. data/examples/components/uat_gaps_demo.rb +112 -0
  168. data/examples/dashboard/feed_simulator.rb +60 -0
  169. data/examples/dashboard/live_dashboard.rb +80 -0
  170. data/examples/dashboard_components.rb +108 -0
  171. data/examples/deferred_fragments_demo.rb +62 -0
  172. data/examples/generate_more_spike/README.md +63 -0
  173. data/examples/generate_more_spike/app.rb +441 -0
  174. data/examples/git_health.sh +401 -0
  175. data/examples/layout/layout_components_demo.rb +174 -0
  176. data/examples/layout/modal_demo.rb +215 -0
  177. data/examples/layout/navigation_demo.rb +227 -0
  178. data/examples/layout/route_tabs_demo.rb +52 -0
  179. data/examples/layout/routing_demo.rb +52 -0
  180. data/examples/layout/scroll_box_demo.rb +154 -0
  181. data/examples/lazy_fragments_demo.rb +151 -0
  182. data/examples/my_todos/README.md +29 -0
  183. data/examples/my_todos/my_todos.rb +312 -0
  184. data/examples/my_todos/store.rb +86 -0
  185. data/examples/opal/reactive_demo.rb +62 -0
  186. data/examples/opal/scenarios/s1_counter.rb +15 -0
  187. data/examples/opal/scenarios/s2_search_filter.rb +17 -0
  188. data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
  189. data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
  190. data/examples/opal/scenarios/s5_watch.rb +39 -0
  191. data/examples/opal/scenarios/s6_on_start.rb +20 -0
  192. data/examples/opal/scenarios/s7_wizard.rb +33 -0
  193. data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
  194. data/examples/opal/scenarios/s9_dashboard.rb +27 -0
  195. data/examples/operations_dashboard_demo.rb +167 -0
  196. data/examples/panel_demo.sh +447 -0
  197. data/examples/parity/assets/tyrion_slice.css +86 -0
  198. data/examples/parity/rivet_people_slice.rb +182 -0
  199. data/examples/parity/tyrion_components.css +777 -0
  200. data/examples/parity/tyrion_warroom_components.rb +414 -0
  201. data/examples/parity/tyrion_warroom_slice.rb +549 -0
  202. data/examples/puma_dev/README.md +124 -0
  203. data/examples/puma_dev/config.ru +34 -0
  204. data/examples/puma_dev/standalone_app.rb +34 -0
  205. data/examples/scaffolding/blog.rb +49 -0
  206. data/examples/scaffolding/utf_lite.rb +105 -0
  207. data/examples/styling/feedback_demo.rb +229 -0
  208. data/examples/styling/style_showcase.rb +172 -0
  209. data/examples/styling/theme_demo.rb +508 -0
  210. data/examples/timer_health_checker.rb +111 -0
  211. data/examples/timer_showcase.rb +131 -0
  212. data/examples/tutorials/render_markdown.rb +112 -0
  213. data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
  214. data/examples/tutorials/tutorial_content.rb +1082 -0
  215. data/examples/visual_skills/design_deck_demo.rb +314 -0
  216. data/examples/visual_skills/explainer_demo.rb +499 -0
  217. data/exe/streamweaver +9 -0
  218. data/gsd/ROADMAP-1.0.md +167 -0
  219. data/gsd/STATE.md +13 -0
  220. data/gsd/research/market-positioning-research.md +139 -0
  221. data/gsd/research/production-patterns-research.md +270 -0
  222. data/gsd/research/repo-audit-1.0.md +274 -0
  223. data/lib/stream_weaver/action_token.rb +58 -0
  224. data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
  225. data/lib/stream_weaver/adapter/base.rb +591 -0
  226. data/lib/stream_weaver/adapter/opal.rb +334 -0
  227. data/lib/stream_weaver/adapter/static.rb +1118 -0
  228. data/lib/stream_weaver/admin.rb +176 -0
  229. data/lib/stream_weaver/app.rb +1821 -0
  230. data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
  231. data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
  232. data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
  233. data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
  234. data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
  235. data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
  236. data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
  237. data/lib/stream_weaver/canvas/bridge.rb +239 -0
  238. data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
  239. data/lib/stream_weaver/canvas/client.rb +298 -0
  240. data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
  241. data/lib/stream_weaver/canvas/doc_store.rb +252 -0
  242. data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
  243. data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
  244. data/lib/stream_weaver/canvas/gist_store.rb +135 -0
  245. data/lib/stream_weaver/canvas/helpers.rb +109 -0
  246. data/lib/stream_weaver/canvas/history.rb +90 -0
  247. data/lib/stream_weaver/canvas/protocol.rb +86 -0
  248. data/lib/stream_weaver/canvas/reader.rb +853 -0
  249. data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
  250. data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
  251. data/lib/stream_weaver/canvas/session.rb +132 -0
  252. data/lib/stream_weaver/cli.rb +3235 -0
  253. data/lib/stream_weaver/component_assets.rb +70 -0
  254. data/lib/stream_weaver/component_registry.rb +67 -0
  255. data/lib/stream_weaver/component_renderer.rb +48 -0
  256. data/lib/stream_weaver/components/annotated_code.rb +53 -0
  257. data/lib/stream_weaver/components/api_endpoint.rb +42 -0
  258. data/lib/stream_weaver/components/callout.rb +59 -0
  259. data/lib/stream_weaver/components/chart.rb +84 -0
  260. data/lib/stream_weaver/components/code_block.rb +77 -0
  261. data/lib/stream_weaver/components/comparison.rb +39 -0
  262. data/lib/stream_weaver/components/decision.rb +38 -0
  263. data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
  264. data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
  265. data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
  266. data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
  267. data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
  268. data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
  269. data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
  270. data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
  271. data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
  272. data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
  273. data/lib/stream_weaver/components/diff_block.rb +125 -0
  274. data/lib/stream_weaver/components/doc_header.rb +57 -0
  275. data/lib/stream_weaver/components/image_block.rb +70 -0
  276. data/lib/stream_weaver/components/implementation_map.rb +33 -0
  277. data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
  278. data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
  279. data/lib/stream_weaver/components/mermaid.rb +79 -0
  280. data/lib/stream_weaver/components/pipeline.rb +63 -0
  281. data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
  282. data/lib/stream_weaver/components/slide_container.rb +141 -0
  283. data/lib/stream_weaver/components/timeline_event.rb +58 -0
  284. data/lib/stream_weaver/components/wireframe.rb +29 -0
  285. data/lib/stream_weaver/components/wireframe_block.rb +34 -0
  286. data/lib/stream_weaver/components.rb +2711 -0
  287. data/lib/stream_weaver/css.rb +230 -0
  288. data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
  289. data/lib/stream_weaver/display_dsl.rb +1004 -0
  290. data/lib/stream_weaver/export/html_exporter.rb +478 -0
  291. data/lib/stream_weaver/feed.rb +34 -0
  292. data/lib/stream_weaver/feed_builder.rb +31 -0
  293. data/lib/stream_weaver/fonts.rb +33 -0
  294. data/lib/stream_weaver/interaction_runner.rb +487 -0
  295. data/lib/stream_weaver/iterm.rb +460 -0
  296. data/lib/stream_weaver/layout_registry.rb +92 -0
  297. data/lib/stream_weaver/opal/bridge.rb +52 -0
  298. data/lib/stream_weaver/opal/builder.rb +193 -0
  299. data/lib/stream_weaver/opal/env.rb +29 -0
  300. data/lib/stream_weaver/opal/reactive_state.rb +75 -0
  301. data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
  302. data/lib/stream_weaver/opal/renderer.rb +77 -0
  303. data/lib/stream_weaver/opal/runtime.rb +250 -0
  304. data/lib/stream_weaver/opal/shell.rb +145 -0
  305. data/lib/stream_weaver/opal/string_bridge.rb +50 -0
  306. data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
  307. data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
  308. data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
  309. data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
  310. data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
  311. data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
  312. data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
  313. data/lib/stream_weaver/opal_entry.rb +136 -0
  314. data/lib/stream_weaver/org/inline.rb +95 -0
  315. data/lib/stream_weaver/org/reader.rb +561 -0
  316. data/lib/stream_weaver/org/recording_context.rb +86 -0
  317. data/lib/stream_weaver/org/source_splitter.rb +63 -0
  318. data/lib/stream_weaver/org/writer.rb +300 -0
  319. data/lib/stream_weaver/page_shell.rb +530 -0
  320. data/lib/stream_weaver/portfile.rb +79 -0
  321. data/lib/stream_weaver/pushable.rb +42 -0
  322. data/lib/stream_weaver/resource/default_views.rb +111 -0
  323. data/lib/stream_weaver/resource/field_input.rb +25 -0
  324. data/lib/stream_weaver/resource/state_keys.rb +16 -0
  325. data/lib/stream_weaver/resource/store.rb +18 -0
  326. data/lib/stream_weaver/resource.rb +104 -0
  327. data/lib/stream_weaver/server.rb +1471 -0
  328. data/lib/stream_weaver/service.rb +1240 -0
  329. data/lib/stream_weaver/service_client.rb +104 -0
  330. data/lib/stream_weaver/session_store.rb +186 -0
  331. data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
  332. data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
  333. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
  334. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
  335. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
  336. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
  337. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
  338. data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
  339. data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
  340. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
  341. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
  342. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
  343. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
  344. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
  345. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
  346. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
  347. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
  348. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
  349. data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
  350. data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
  351. data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
  352. data/lib/stream_weaver/streamer.rb +63 -0
  353. data/lib/stream_weaver/templates/choices.rb +149 -0
  354. data/lib/stream_weaver/templates/code.rb +157 -0
  355. data/lib/stream_weaver/templates/confirm.rb +110 -0
  356. data/lib/stream_weaver/templates/diff.rb +195 -0
  357. data/lib/stream_weaver/templates/info.rb +142 -0
  358. data/lib/stream_weaver/templates/table.rb +169 -0
  359. data/lib/stream_weaver/templates/wizard.rb +271 -0
  360. data/lib/stream_weaver/theme/auto_mode.rb +116 -0
  361. data/lib/stream_weaver/theme/presets.rb +555 -0
  362. data/lib/stream_weaver/theme.rb +639 -0
  363. data/lib/stream_weaver/university/canvas.rb +924 -0
  364. data/lib/stream_weaver/university/course.rb +699 -0
  365. data/lib/stream_weaver/university/demos/counter.rb +33 -0
  366. data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
  367. data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
  368. data/lib/stream_weaver/university/demos.rb +50 -0
  369. data/lib/stream_weaver/university/listener.rb +467 -0
  370. data/lib/stream_weaver/university/progress.rb +201 -0
  371. data/lib/stream_weaver/university/runner.rb +138 -0
  372. data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
  373. data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
  374. data/lib/stream_weaver/utils.rb +31 -0
  375. data/lib/stream_weaver/version.rb +5 -0
  376. data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
  377. data/lib/stream_weaver/views.rb +3940 -0
  378. data/lib/stream_weaver.rb +122 -0
  379. data/llms.txt +1269 -0
  380. data/sig/stream_weaver.rbs +4 -0
  381. metadata +598 -0
@@ -0,0 +1,244 @@
1
+ ---
2
+ name: visual-recap
3
+ description: Use after finishing a task (or mid-task for incremental updates) — renders a live canvas showing what changed, why, and the key design choices made during implementation
4
+ ---
5
+
6
+ # Visual Recap
7
+
8
+ Post-implementation canvas recap. Pushes a live visual summary of what actually changed: files touched, the most significant diffs, annotated complex code, and design choices flagged during the work — all in one persistent canvas that evolves with the task.
9
+
10
+ ## When to Use
11
+
12
+ After any task where:
13
+ - Multiple files changed
14
+ - A design decision was made
15
+ - The user wants to see what happened, not just a text summary
16
+ - You're mid-task and want to show progress without finishing
17
+
18
+ Push at task start (showing intended changes), update mid-task, finalize when done. The canvas evolves — it is not a one-shot publish.
19
+
20
+ Skip for trivial one-liner edits where a text summary is faster than a canvas.
21
+
22
+ ## Why StreamWeaver vs Static Recap
23
+
24
+ Builder.io's visual-recap and similar tools generate a static artifact at the end of a task — one snapshot, one publish, frozen. StreamWeaver's recap is **live and incremental**:
25
+
26
+ - Push at the start with the intended change map
27
+ - Update after each batch of files
28
+ - Add diff blocks as significant changes land
29
+ - Finalize with annotated code and design decisions
30
+
31
+ The user sees real progress. Mid-task pivots are visible. The canvas reflects the actual task state, not a post-hoc reconstruction.
32
+
33
+ ## !! DO NOT LAUNCH STANDALONE SERVERS PER UPDATE !!
34
+
35
+ **NEVER** run `ruby app.rb` or `streamweaver <file.rb>` for each recap update. Use `canvas-push` to update a single persistent window throughout the session.
36
+
37
+ ## Step 1 — Get the Changed Files
38
+
39
+ ```bash
40
+ # After finishing: diff since the last commit
41
+ git diff HEAD~1 --name-only
42
+
43
+ # Mid-task or across a feature branch:
44
+ git diff main..HEAD --name-only
45
+
46
+ # For uncommitted changes:
47
+ git diff --name-only
48
+ git status --short
49
+ ```
50
+
51
+ Parse the output to build a `{ path:, note: }` list. The `note:` should explain **why** the file changed, not what the file is. Write it now — don't defer to Step 3.
52
+
53
+ Example mapping:
54
+
55
+ ```
56
+ lib/stream_weaver/components/callout.rb → "Added :decision and :risk tone variants"
57
+ spec/components/callout_spec.rb → "Cover new tone variants, edge cases for missing title"
58
+ lib/stream_weaver/display_dsl.rb → "Expose callout shorthand with default variant: :info"
59
+ ```
60
+
61
+ ## Step 2 — Start the Canvas
62
+
63
+ ```bash
64
+ streamweaver panel recap
65
+ ```
66
+
67
+ This opens a persistent canvas named `recap` in an iTerm2 split pane (or browser tab elsewhere). The URL is printed to stdout — tell the user where to look.
68
+
69
+ ## Step 3 — Push the Implementation Map
70
+
71
+ ```bash
72
+ streamweaver canvas-push recap <<'RUBY'
73
+ header1 "Recap: <task name>"
74
+
75
+ implementation_map(files: [
76
+ { path: "lib/stream_weaver/components/callout.rb",
77
+ note: "Added :decision and :risk tone variants with icon and border color" },
78
+ { path: "spec/components/callout_spec.rb",
79
+ note: "Cover new tone variants and missing-title edge case" },
80
+ { path: "lib/stream_weaver/display_dsl.rb",
81
+ note: "Expose callout shorthand: default variant :info, no block required" }
82
+ ])
83
+ RUBY
84
+ ```
85
+
86
+ `note:` is the rationale — **why this file changed**, not just what it contains. One sentence.
87
+
88
+ ## Step 4 — Push Diffs for the 2-3 Most Significant Changes
89
+
90
+ Pick the files where the change is non-obvious from the file name. Push a `diff` block per file:
91
+
92
+ ```bash
93
+ streamweaver canvas-push recap <<'RUBY'
94
+ header2 "Key Changes"
95
+
96
+ diff(language: "ruby") do
97
+ before do
98
+ <<~CODE
99
+ def callout(variant: :info, title: nil, &block)
100
+ @components << Components::Callout.new(variant: variant, title: title, &block)
101
+ end
102
+ CODE
103
+ end
104
+ after do
105
+ <<~CODE
106
+ def callout(variant: :info, title: nil, **options, &block)
107
+ with_container(Components::Callout.new(variant: variant, title: title, **options), &block)
108
+ end
109
+ CODE
110
+ end
111
+ end
112
+ RUBY
113
+ ```
114
+
115
+ Limit to 2-3 diffs. If more files changed significantly, pick the ones that tell the story of the task. The `before`/`after` content should be the actual code — copy from the diff, don't paraphrase.
116
+
117
+ ## Step 5 — Annotated Code for the Most Complex Change (Optional)
118
+
119
+ If one file has a non-obvious implementation worth explaining:
120
+
121
+ ```bash
122
+ streamweaver canvas-push recap <<'RUBY'
123
+ header2 "Implementation Detail"
124
+
125
+ annotated_code(
126
+ language: "ruby",
127
+ annotations: [
128
+ { line: 3, label: "Delegates to with_container",
129
+ note: "Allows callout to hold nested DSL components — text, md, badge, etc." },
130
+ { line: 7, label: "Variant maps to CSS class",
131
+ note: "sw-callout-decision, sw-callout-risk, sw-callout-info — defined in callout.css" },
132
+ { line: 12, label: "Title is optional",
133
+ note: "Renders as a <strong> header when present; omitted entirely if nil" }
134
+ ]
135
+ ) do
136
+ <<~CODE
137
+ def callout(variant: :info, title: nil, **options, &block)
138
+ with_container(
139
+ Components::Callout.new(variant: variant, title: title, **options),
140
+ &block
141
+ )
142
+ end
143
+
144
+ class Callout
145
+ VARIANT_CLASSES = { info: "sw-callout-info", decision: "sw-callout-decision",
146
+ risk: "sw-callout-risk", tip: "sw-callout-tip" }
147
+ # ...
148
+ def render_title
149
+ strong { @title } if @title
150
+ end
151
+ end
152
+ CODE
153
+ end
154
+ RUBY
155
+ ```
156
+
157
+ Skip this step if the implementation is straightforward.
158
+
159
+ ## Step 6 — Flag Design Choices
160
+
161
+ Push a `callout(:decision)` for each meaningful design decision made during the task:
162
+
163
+ ```bash
164
+ streamweaver canvas-push recap <<'RUBY'
165
+ header2 "Design Choices"
166
+
167
+ callout(variant: :decision, title: "with_container instead of direct push") do
168
+ text "Callout now accepts nested DSL calls (text, md, badge). Using with_container lets the"
169
+ text "component collect children from the block the same way card and columns do."
170
+ text "Alternative: parse the block as a string. Rejected — loses type safety and child ordering."
171
+ end
172
+
173
+ callout(variant: :decision, title: ":info as the default variant") do
174
+ text "Matches the most common use case (informational callouts). Callers who want :risk or"
175
+ text ":decision must opt in — no accidental risk styling on plain callouts."
176
+ end
177
+ RUBY
178
+ ```
179
+
180
+ One `callout(:decision)` per meaningful choice. If you made a risky call, use `callout(:risk)` instead.
181
+
182
+ ## Incremental Updates (Mid-Task)
183
+
184
+ You do not have to wait until a task is done. Update the recap as you go:
185
+
186
+ ```bash
187
+ # After the first batch of files:
188
+ streamweaver canvas-push recap <<'RUBY'
189
+ header1 "Recap: Add Callout Tones — In Progress"
190
+
191
+ implementation_map(files: [
192
+ { path: "lib/stream_weaver/components/callout.rb", note: "[done] Tone variants added" },
193
+ { path: "spec/components/callout_spec.rb", note: "[wip]" },
194
+ { path: "lib/stream_weaver/display_dsl.rb", note: "[pending]" }
195
+ ])
196
+ RUBY
197
+ ```
198
+
199
+ Use `[done]`, `[wip]`, and `[pending]` prefixes in the `note:` field to show live status.
200
+
201
+ ## Combining Steps in One Push
202
+
203
+ For small tasks, push everything in a single canvas update:
204
+
205
+ ```bash
206
+ streamweaver canvas-push recap <<'RUBY'
207
+ header1 "Recap: Add Callout Tones"
208
+
209
+ implementation_map(files: [
210
+ { path: "lib/stream_weaver/components/callout.rb",
211
+ note: "Added :decision and :risk tone variants with icon and border color" },
212
+ { path: "spec/components/callout_spec.rb",
213
+ note: "Cover new tone variants and missing-title edge case" }
214
+ ])
215
+
216
+ diff(language: "ruby") do
217
+ before { "def callout(variant: :info, &block)\n ..." }
218
+ after { "def callout(variant: :info, **options, &block)\n with_container(...)\nend" }
219
+ end
220
+
221
+ callout(variant: :decision, title: "with_container for nested DSL") do
222
+ text "Allows callout to contain text, md, badge — matches card/columns pattern."
223
+ end
224
+ RUBY
225
+ ```
226
+
227
+ ## DSL Reference
228
+
229
+ | Method | Purpose |
230
+ |---|---|
231
+ | `implementation_map(files: [...])` | File-to-rationale map; `note:` is the why |
232
+ | `diff(language:) { before { } after { } }` | Side-by-side code diff |
233
+ | `annotated_code(language:, annotations: [...]) { }` | Code with line-pinned notes |
234
+ | `callout(variant: :decision, title: "...") { }` | Design choice flag |
235
+ | `callout(variant: :risk, title: "...") { }` | Risk or caveat flag |
236
+
237
+ ## Known Gotchas
238
+
239
+ - DSL method is `diff`, not `diff_block` — `diff_block` does not exist
240
+ - `annotated_code` line numbers are 1-indexed; they match the code block you supply, not the original file
241
+ - `callout` block must use `text` or `md` — nesting `diff` or `implementation_map` inside a callout throws
242
+ - `spacer` and `divider` don't exist — use `div(style: "height:Npx")` for spacing
243
+ - StreamWeaver auto-selects a port — capture the URL from stdout, don't hardcode 4567
244
+ - If `undefined method` errors appear, the bridge is stale: `streamweaver canvas-stop && streamweaver panel recap`
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module StreamWeaver
6
+ # Thread-safe SSE connection manager with Turbo Stream-like actions.
7
+ # Manages browser SSE connections and broadcasts targeted DOM updates.
8
+ #
9
+ # @example Internal usage (from app stream block)
10
+ # streamer.replace("#metric-rps", "<div>3500</div>")
11
+ #
12
+ # @example External usage (via POST /stream/push)
13
+ # curl -X POST localhost:4567/stream/push -d 'target=#metric-rps&html=<div>3500</div>'
14
+ class Streamer
15
+ include Pushable
16
+
17
+ ACTIONS = %i[replace append prepend remove add_class remove_class].freeze
18
+
19
+ def initialize
20
+ @connections = []
21
+ @mutex = Mutex.new
22
+ @shutting_down = false
23
+ end
24
+
25
+ def shutdown!
26
+ @shutting_down = true
27
+ end
28
+
29
+ def shutting_down?
30
+ @shutting_down
31
+ end
32
+
33
+ def add_connection(conn)
34
+ @mutex.synchronize { @connections << conn }
35
+ end
36
+
37
+ def remove_connection(conn)
38
+ @mutex.synchronize { @connections.delete(conn) }
39
+ end
40
+
41
+ def connection_count
42
+ @mutex.synchronize { @connections.size }
43
+ end
44
+
45
+ private
46
+
47
+ def push_update(action:, target:, html: nil, value: nil)
48
+ payload = { action: action, target: target }
49
+ payload[:html] = html if html
50
+ payload[:value] = value if value
51
+ data = payload.to_json
52
+ @mutex.synchronize do
53
+ dead = []
54
+ @connections.each do |conn|
55
+ conn << "data: #{data}\n\n"
56
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Puma::ConnectionError
57
+ dead << conn
58
+ end
59
+ @connections -= dead
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require_relative '../cli'
7
+
8
+ module StreamWeaver
9
+ module Templates
10
+ # Simple choice selector template
11
+ # Shows options as buttons, returns the selected value
12
+ #
13
+ # Usage:
14
+ # streamweaver template choices SESSION '{"title":"Pick a color","options":["Red","Green","Blue"]}'
15
+ # # Returns: {"choice":"Green"}
16
+ #
17
+ # # With descriptions:
18
+ # streamweaver template choices SESSION '{
19
+ # "title":"Database",
20
+ # "description":"Which database to use?",
21
+ # "options":[
22
+ # {"label":"PostgreSQL","description":"Best for complex queries"},
23
+ # {"label":"SQLite","description":"Simple, file-based"},
24
+ # {"label":"MySQL","description":"Popular, widely supported"}
25
+ # ]
26
+ # }'
27
+ #
28
+ class Choices
29
+ def initialize(session:, config:, port: nil)
30
+ @session = session
31
+ @config = config
32
+ @port = port || detect_port
33
+ @title = config['title'] || 'Choose an option'
34
+ @description = config['description']
35
+ @options = config['options'] || []
36
+ @run_id = Time.now.to_i.to_s(36)
37
+ end
38
+
39
+ def run
40
+ clear_submissions
41
+ push_choices
42
+ data = wait_for_submission
43
+
44
+ # Extract choice from button id (btn_LABEL_1 format)
45
+ button_id = data['_button'] || ''
46
+ # Button id is like "btn_postgresql_1" - extract the label part
47
+ choice_key = button_id.sub(/^btn_/, '').sub(/_\d+$/, '')
48
+
49
+ # Find matching option
50
+ selected = find_option_by_key(choice_key)
51
+
52
+ { 'choice' => selected }
53
+ end
54
+
55
+ private
56
+
57
+ def detect_port
58
+ pid_file = File.expand_path('~/.streamweaver/service.pid')
59
+ return 4576 unless File.exist?(pid_file)
60
+ data = JSON.parse(File.read(pid_file))
61
+ data['port'] || 4576
62
+ rescue
63
+ 4576
64
+ end
65
+
66
+ def find_option_by_key(key)
67
+ @options.each do |opt|
68
+ if opt.is_a?(Hash)
69
+ label = opt['label']
70
+ # Normalize: downcase, replace spaces with underscores
71
+ normalized = label.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
72
+ return label if normalized == key
73
+ else
74
+ normalized = opt.to_s.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
75
+ return opt if normalized == key
76
+ end
77
+ end
78
+ key # fallback
79
+ end
80
+
81
+ def push_choices
82
+ dsl = build_dsl
83
+ html = StreamWeaver::CLI.render_dsl_to_html(dsl, session_name: @session)
84
+
85
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/push")
86
+ request = Net::HTTP::Post.new(uri)
87
+ request.set_form_data('content' => html, 'target' => '#main', 'action' => 'replace')
88
+
89
+ Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
90
+ end
91
+
92
+ def build_dsl
93
+ lines = []
94
+ lines << "header2 \"#{@title}\""
95
+ lines << "text \"#{@description}\"" if @description
96
+ lines << "card do"
97
+
98
+ @options.each do |opt|
99
+ if opt.is_a?(Hash)
100
+ label = opt['label']
101
+ desc = opt['description']
102
+ lines << " button \"#{label}\""
103
+ lines << " text \"#{desc}\", style: :muted" if desc
104
+ lines << " text \"\""
105
+ else
106
+ lines << " button \"#{opt}\""
107
+ end
108
+ end
109
+
110
+ lines << "end"
111
+ lines.join("\n")
112
+ end
113
+
114
+ def clear_submissions
115
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
116
+ loop do
117
+ response = Net::HTTP.get_response(uri)
118
+ break unless response.is_a?(Net::HTTPSuccess)
119
+ data = JSON.parse(response.body)
120
+ break if (data['submissions'] || []).empty?
121
+ end
122
+ rescue
123
+ # ignore
124
+ end
125
+
126
+ def wait_for_submission(timeout: 300)
127
+ start_time = Time.now
128
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
129
+
130
+ loop do
131
+ raise "Timeout" if Time.now - start_time > timeout
132
+
133
+ response = Net::HTTP.get_response(uri)
134
+ if response.is_a?(Net::HTTPSuccess)
135
+ data = JSON.parse(response.body)
136
+ submissions = data['submissions'] || []
137
+ return submissions.first['data'] if submissions.any?
138
+ end
139
+
140
+ sleep 0.3
141
+ end
142
+ end
143
+
144
+ def self.run(session:, config:, port: nil)
145
+ new(session: session, config: config, port: port).run
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require_relative '../cli'
7
+
8
+ module StreamWeaver
9
+ module Templates
10
+ # Code display template
11
+ # Shows code with optional filename and actions
12
+ #
13
+ # Usage:
14
+ # streamweaver template code SESSION '{
15
+ # "title": "Generated Code",
16
+ # "filename": "app.rb",
17
+ # "language": "ruby",
18
+ # "code": "def hello\n puts \"Hello\"\nend",
19
+ # "actions": ["Copy", "Apply", "Cancel"]
20
+ # }'
21
+ # # Returns: {"action": "Apply"}
22
+ #
23
+ class Code
24
+ def initialize(session:, config:, port: nil)
25
+ @session = session
26
+ @config = config
27
+ @port = port || detect_port
28
+ @title = config['title']
29
+ @filename = config['filename']
30
+ @language = config['language'] || 'text'
31
+ @code = config['code'] || ''
32
+ @actions = config['actions'] || ['OK']
33
+ @line_numbers = config['line_numbers'] != false
34
+ end
35
+
36
+ def run
37
+ clear_submissions
38
+ push_code
39
+ data = wait_for_submission
40
+
41
+ button_id = data['_button'] || ''
42
+ action = extract_action(button_id)
43
+ { 'action' => action }
44
+ end
45
+
46
+ private
47
+
48
+ def detect_port
49
+ pid_file = File.expand_path('~/.streamweaver/service.pid')
50
+ return 4576 unless File.exist?(pid_file)
51
+ data = JSON.parse(File.read(pid_file))
52
+ data['port'] || 4576
53
+ rescue
54
+ 4576
55
+ end
56
+
57
+ def extract_action(button_id)
58
+ key = button_id.sub(/^btn_/, '').sub(/_\d+$/, '')
59
+ @actions.each do |action|
60
+ normalized = action.to_s.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
61
+ return action if normalized == key
62
+ end
63
+ key
64
+ end
65
+
66
+ def push_code
67
+ # Build HTML directly for better code formatting
68
+ html = build_html
69
+
70
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/push")
71
+ request = Net::HTTP::Post.new(uri)
72
+ request.set_form_data('content' => html, 'target' => '#main', 'action' => 'replace')
73
+
74
+ Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
75
+ end
76
+
77
+ def build_html
78
+ escaped_code = escape_html(@code)
79
+
80
+ # Add line numbers if enabled
81
+ if @line_numbers
82
+ lines = escaped_code.split("\n")
83
+ numbered_lines = lines.each_with_index.map do |line, i|
84
+ line_num = (i + 1).to_s.rjust(3)
85
+ "<span style='color: #666; user-select: none;'>#{line_num}</span> #{line}"
86
+ end
87
+ escaped_code = numbered_lines.join("\n")
88
+ end
89
+
90
+ buttons_html = @actions.map.with_index do |action, i|
91
+ variant_class = i == 0 ? 'sw-btn-primary' : 'sw-btn-secondary'
92
+ btn_id = "btn_#{action.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')}_#{i + 1}"
93
+ %(<button id="#{btn_id}" class="sw-btn #{variant_class}"
94
+ hx-post="/live/#{@session}/action/#{btn_id}"
95
+ hx-include="[x-model]"
96
+ hx-target="#main"
97
+ hx-swap="innerHTML">#{escape_html(action)}</button>)
98
+ end.join("\n ")
99
+
100
+ <<~HTML
101
+ <div id="main" x-data="{}">
102
+ #{@title ? "<h2 style='margin-bottom: 1rem;'>#{escape_html(@title)}</h2>" : ''}
103
+ <div class="sw-card" style="padding: 0; overflow: hidden;">
104
+ #{@filename ? "<div style='padding: 0.5rem 1rem; background: var(--sw-color-bg); border-bottom: 1px solid var(--sw-color-border); font-size: 0.875rem; color: var(--sw-color-text-muted);'>#{escape_html(@filename)}</div>" : ''}
105
+ <pre style="margin: 0; padding: 1rem; background: #1e1e1e; color: #d4d4d4; overflow-x: auto; font-family: 'SF Mono', Consolas, monospace; font-size: 0.875rem; line-height: 1.5;"><code>#{escaped_code}</code></pre>
106
+ </div>
107
+ <div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
108
+ #{buttons_html}
109
+ </div>
110
+ </div>
111
+ HTML
112
+ end
113
+
114
+ def escape_html(text)
115
+ text.to_s
116
+ .gsub('&', '&amp;')
117
+ .gsub('<', '&lt;')
118
+ .gsub('>', '&gt;')
119
+ .gsub('"', '&quot;')
120
+ end
121
+
122
+ def clear_submissions
123
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
124
+ loop do
125
+ response = Net::HTTP.get_response(uri)
126
+ break unless response.is_a?(Net::HTTPSuccess)
127
+ data = JSON.parse(response.body)
128
+ break if (data['submissions'] || []).empty?
129
+ end
130
+ rescue
131
+ # ignore
132
+ end
133
+
134
+ def wait_for_submission(timeout: 300)
135
+ start_time = Time.now
136
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
137
+
138
+ loop do
139
+ raise "Timeout" if Time.now - start_time > timeout
140
+
141
+ response = Net::HTTP.get_response(uri)
142
+ if response.is_a?(Net::HTTPSuccess)
143
+ data = JSON.parse(response.body)
144
+ submissions = data['submissions'] || []
145
+ return submissions.first['data'] if submissions.any?
146
+ end
147
+
148
+ sleep 0.3
149
+ end
150
+ end
151
+
152
+ def self.run(session:, config:, port: nil)
153
+ new(session: session, config: config, port: port).run
154
+ end
155
+ end
156
+ end
157
+ end