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,110 @@
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 confirmation dialog
11
+ # Returns {"confirmed": true/false}
12
+ #
13
+ # Usage:
14
+ # streamweaver template confirm SESSION '{"title":"Delete files?","message":"This will remove 3 files permanently."}'
15
+ # # Returns: {"confirmed":true} or {"confirmed":false}
16
+ #
17
+ # # Custom button labels:
18
+ # streamweaver template confirm SESSION '{"title":"Deploy?","yes":"Deploy Now","no":"Cancel"}'
19
+ #
20
+ class Confirm
21
+ def initialize(session:, config:, port: nil)
22
+ @session = session
23
+ @config = config
24
+ @port = port || detect_port
25
+ @title = config['title'] || 'Confirm'
26
+ @message = config['message']
27
+ @yes_label = config['yes'] || 'Yes'
28
+ @no_label = config['no'] || 'No'
29
+ end
30
+
31
+ def run
32
+ clear_submissions
33
+ push_dialog
34
+ data = wait_for_submission
35
+
36
+ button_id = data['_button'] || ''
37
+ confirmed = button_id.include?('yes') || button_id.include?(@yes_label.downcase.gsub(/\s+/, '_'))
38
+
39
+ { 'confirmed' => confirmed }
40
+ end
41
+
42
+ private
43
+
44
+ def detect_port
45
+ pid_file = File.expand_path('~/.streamweaver/service.pid')
46
+ return 4576 unless File.exist?(pid_file)
47
+ data = JSON.parse(File.read(pid_file))
48
+ data['port'] || 4576
49
+ rescue
50
+ 4576
51
+ end
52
+
53
+ def push_dialog
54
+ dsl = build_dsl
55
+ html = StreamWeaver::CLI.render_dsl_to_html(dsl, session_name: @session)
56
+
57
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/push")
58
+ request = Net::HTTP::Post.new(uri)
59
+ request.set_form_data('content' => html, 'target' => '#main', 'action' => 'replace')
60
+
61
+ Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
62
+ end
63
+
64
+ def build_dsl
65
+ lines = []
66
+ lines << "card title: \"#{@title}\" do"
67
+ lines << " text \"#{@message}\"" if @message
68
+ lines << " text \"\""
69
+ lines << " button \"#{@yes_label}\", variant: :primary"
70
+ lines << " button \"#{@no_label}\", variant: :secondary"
71
+ lines << "end"
72
+ lines.join("\n")
73
+ end
74
+
75
+ def clear_submissions
76
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
77
+ loop do
78
+ response = Net::HTTP.get_response(uri)
79
+ break unless response.is_a?(Net::HTTPSuccess)
80
+ data = JSON.parse(response.body)
81
+ break if (data['submissions'] || []).empty?
82
+ end
83
+ rescue
84
+ # ignore
85
+ end
86
+
87
+ def wait_for_submission(timeout: 300)
88
+ start_time = Time.now
89
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
90
+
91
+ loop do
92
+ raise "Timeout" if Time.now - start_time > timeout
93
+
94
+ response = Net::HTTP.get_response(uri)
95
+ if response.is_a?(Net::HTTPSuccess)
96
+ data = JSON.parse(response.body)
97
+ submissions = data['submissions'] || []
98
+ return submissions.first['data'] if submissions.any?
99
+ end
100
+
101
+ sleep 0.3
102
+ end
103
+ end
104
+
105
+ def self.run(session:, config:, port: nil)
106
+ new(session: session, config: config, port: port).run
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,195 @@
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
+ # Diff display template
11
+ # Shows code changes with add/remove highlighting
12
+ #
13
+ # Usage:
14
+ # streamweaver template diff SESSION '{
15
+ # "title": "Proposed Changes",
16
+ # "filename": "app.rb",
17
+ # "changes": [
18
+ # {"type": "context", "line": "def hello"},
19
+ # {"type": "remove", "line": " puts \"old\""},
20
+ # {"type": "add", "line": " puts \"new\""},
21
+ # {"type": "context", "line": "end"}
22
+ # ],
23
+ # "actions": ["Apply", "Reject", "Edit"]
24
+ # }'
25
+ # # Returns: {"action": "Apply"}
26
+ #
27
+ # # Or simplified format:
28
+ # streamweaver template diff SESSION '{
29
+ # "title": "Changes",
30
+ # "filename": "app.rb",
31
+ # "diff": "- old line\n+ new line\n context"
32
+ # }'
33
+ #
34
+ class Diff
35
+ def initialize(session:, config:, port: nil)
36
+ @session = session
37
+ @config = config
38
+ @port = port || detect_port
39
+ @title = config['title']
40
+ @filename = config['filename']
41
+ @changes = config['changes']
42
+ @diff_text = config['diff']
43
+ @actions = config['actions'] || ['Apply', 'Reject']
44
+ end
45
+
46
+ def run
47
+ clear_submissions
48
+ push_diff
49
+ data = wait_for_submission
50
+
51
+ button_id = data['_button'] || ''
52
+ action = extract_action(button_id)
53
+ { 'action' => action }
54
+ end
55
+
56
+ private
57
+
58
+ def detect_port
59
+ pid_file = File.expand_path('~/.streamweaver/service.pid')
60
+ return 4576 unless File.exist?(pid_file)
61
+ data = JSON.parse(File.read(pid_file))
62
+ data['port'] || 4576
63
+ rescue
64
+ 4576
65
+ end
66
+
67
+ def extract_action(button_id)
68
+ key = button_id.sub(/^btn_/, '').sub(/_\d+$/, '')
69
+ @actions.each do |action|
70
+ normalized = action.to_s.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
71
+ return action if normalized == key
72
+ end
73
+ key
74
+ end
75
+
76
+ def push_diff
77
+ html = build_html
78
+
79
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/push")
80
+ request = Net::HTTP::Post.new(uri)
81
+ request.set_form_data('content' => html, 'target' => '#main', 'action' => 'replace')
82
+
83
+ Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
84
+ end
85
+
86
+ def build_html
87
+ diff_lines = build_diff_lines
88
+ diff_html = diff_lines.map { |l| format_diff_line(l) }.join("\n")
89
+
90
+ buttons_html = @actions.map.with_index do |action, i|
91
+ variant_class = case action.downcase
92
+ when 'apply', 'accept', 'yes' then 'sw-btn-primary'
93
+ when 'reject', 'cancel', 'no' then 'sw-btn-secondary'
94
+ else i == 0 ? 'sw-btn-primary' : 'sw-btn-secondary'
95
+ end
96
+ btn_id = "btn_#{action.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')}_#{i + 1}"
97
+ %(<button id="#{btn_id}" class="sw-btn #{variant_class}"
98
+ hx-post="/live/#{@session}/action/#{btn_id}"
99
+ hx-include="[x-model]"
100
+ hx-target="#main"
101
+ hx-swap="innerHTML">#{escape_html(action)}</button>)
102
+ end.join("\n ")
103
+
104
+ <<~HTML
105
+ <div id="main" x-data="{}">
106
+ #{@title ? "<h2 style='margin-bottom: 1rem;'>#{escape_html(@title)}</h2>" : ''}
107
+ <div class="sw-card" style="padding: 0; overflow: hidden;">
108
+ #{@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>" : ''}
109
+ <pre style="margin: 0; padding: 0; font-family: 'SF Mono', Consolas, monospace; font-size: 0.875rem; line-height: 1.6; overflow-x: auto;">#{diff_html}</pre>
110
+ </div>
111
+ <div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
112
+ #{buttons_html}
113
+ </div>
114
+ </div>
115
+ HTML
116
+ end
117
+
118
+ def build_diff_lines
119
+ if @changes
120
+ # Structured format
121
+ @changes.map do |c|
122
+ { type: c['type'] || 'context', line: c['line'] || '' }
123
+ end
124
+ elsif @diff_text
125
+ # Parse diff text format
126
+ @diff_text.split("\n").map do |line|
127
+ case line[0]
128
+ when '+' then { type: 'add', line: line[1..] || '' }
129
+ when '-' then { type: 'remove', line: line[1..] || '' }
130
+ else { type: 'context', line: line.sub(/^\s/, '') }
131
+ end
132
+ end
133
+ else
134
+ []
135
+ end
136
+ end
137
+
138
+ def format_diff_line(diff_line)
139
+ type = diff_line[:type]
140
+ line = escape_html(diff_line[:line])
141
+
142
+ case type
143
+ when 'add'
144
+ %(<div style="background: #1a3d1a; color: #4ade80; padding: 0 1rem;"><span style="color: #22c55e; user-select: none;">+ </span>#{line}</div>)
145
+ when 'remove'
146
+ %(<div style="background: #3d1a1a; color: #f87171; padding: 0 1rem;"><span style="color: #ef4444; user-select: none;">- </span>#{line}</div>)
147
+ else
148
+ %(<div style="background: #1e1e1e; color: #d4d4d4; padding: 0 1rem;"><span style="color: #666; user-select: none;"> </span>#{line}</div>)
149
+ end
150
+ end
151
+
152
+ def escape_html(text)
153
+ text.to_s
154
+ .gsub('&', '&amp;')
155
+ .gsub('<', '&lt;')
156
+ .gsub('>', '&gt;')
157
+ .gsub('"', '&quot;')
158
+ end
159
+
160
+ def clear_submissions
161
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
162
+ loop do
163
+ response = Net::HTTP.get_response(uri)
164
+ break unless response.is_a?(Net::HTTPSuccess)
165
+ data = JSON.parse(response.body)
166
+ break if (data['submissions'] || []).empty?
167
+ end
168
+ rescue
169
+ # ignore
170
+ end
171
+
172
+ def wait_for_submission(timeout: 300)
173
+ start_time = Time.now
174
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
175
+
176
+ loop do
177
+ raise "Timeout" if Time.now - start_time > timeout
178
+
179
+ response = Net::HTTP.get_response(uri)
180
+ if response.is_a?(Net::HTTPSuccess)
181
+ data = JSON.parse(response.body)
182
+ submissions = data['submissions'] || []
183
+ return submissions.first['data'] if submissions.any?
184
+ end
185
+
186
+ sleep 0.3
187
+ end
188
+ end
189
+
190
+ def self.run(session:, config:, port: nil)
191
+ new(session: session, config: config, port: port).run
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,142 @@
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
+ # Information display with optional actions
11
+ # Shows a message and waits for user to acknowledge or choose an action
12
+ #
13
+ # Usage:
14
+ # # Simple acknowledgment:
15
+ # streamweaver template info SESSION '{"title":"Done!","message":"Task completed successfully."}'
16
+ # # Returns: {"action":"ok"}
17
+ #
18
+ # # With custom actions:
19
+ # streamweaver template info SESSION '{
20
+ # "title":"Build Complete",
21
+ # "message":"3 files compiled, 0 errors",
22
+ # "details":["main.js (12kb)","styles.css (3kb)","index.html (1kb)"],
23
+ # "actions":["Deploy","View Logs","Close"]
24
+ # }'
25
+ # # Returns: {"action":"Deploy"} or {"action":"View Logs"} etc.
26
+ #
27
+ class Info
28
+ def initialize(session:, config:, port: nil)
29
+ @session = session
30
+ @config = config
31
+ @port = port || detect_port
32
+ @title = config['title'] || 'Information'
33
+ @message = config['message']
34
+ @details = config['details'] || []
35
+ @actions = config['actions'] || ['OK']
36
+ @variant = config['variant'] # success, warning, error, info
37
+ end
38
+
39
+ def run
40
+ clear_submissions
41
+ push_info
42
+ data = wait_for_submission
43
+
44
+ button_id = data['_button'] || ''
45
+ action = extract_action(button_id)
46
+
47
+ { 'action' => action }
48
+ end
49
+
50
+ private
51
+
52
+ def detect_port
53
+ pid_file = File.expand_path('~/.streamweaver/service.pid')
54
+ return 4576 unless File.exist?(pid_file)
55
+ data = JSON.parse(File.read(pid_file))
56
+ data['port'] || 4576
57
+ rescue
58
+ 4576
59
+ end
60
+
61
+ def extract_action(button_id)
62
+ # Button id is like "btn_deploy_1" - extract the label
63
+ key = button_id.sub(/^btn_/, '').sub(/_\d+$/, '')
64
+
65
+ @actions.each do |action|
66
+ normalized = action.to_s.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
67
+ return action if normalized == key
68
+ end
69
+
70
+ key # fallback
71
+ end
72
+
73
+ def push_info
74
+ dsl = build_dsl
75
+ html = StreamWeaver::CLI.render_dsl_to_html(dsl, session_name: @session)
76
+
77
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/push")
78
+ request = Net::HTTP::Post.new(uri)
79
+ request.set_form_data('content' => html, 'target' => '#main', 'action' => 'replace')
80
+
81
+ Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
82
+ end
83
+
84
+ def build_dsl
85
+ lines = []
86
+ lines << "card title: \"#{@title}\" do"
87
+ lines << " text \"#{@message}\"" if @message
88
+
89
+ if @details.any?
90
+ lines << " text \"\""
91
+ @details.each do |detail|
92
+ lines << " text \"#{detail}\""
93
+ end
94
+ end
95
+
96
+ lines << " text \"\""
97
+
98
+ @actions.each_with_index do |action, i|
99
+ variant = i == 0 ? ', variant: :primary' : ''
100
+ lines << " button \"#{action}\"#{variant}"
101
+ end
102
+
103
+ lines << "end"
104
+ lines.join("\n")
105
+ end
106
+
107
+ def clear_submissions
108
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
109
+ loop do
110
+ response = Net::HTTP.get_response(uri)
111
+ break unless response.is_a?(Net::HTTPSuccess)
112
+ data = JSON.parse(response.body)
113
+ break if (data['submissions'] || []).empty?
114
+ end
115
+ rescue
116
+ # ignore
117
+ end
118
+
119
+ def wait_for_submission(timeout: 300)
120
+ start_time = Time.now
121
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
122
+
123
+ loop do
124
+ raise "Timeout" if Time.now - start_time > timeout
125
+
126
+ response = Net::HTTP.get_response(uri)
127
+ if response.is_a?(Net::HTTPSuccess)
128
+ data = JSON.parse(response.body)
129
+ submissions = data['submissions'] || []
130
+ return submissions.first['data'] if submissions.any?
131
+ end
132
+
133
+ sleep 0.3
134
+ end
135
+ end
136
+
137
+ def self.run(session:, config:, port: nil)
138
+ new(session: session, config: config, port: port).run
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,169 @@
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
+ # Table display template
11
+ # Shows data in rows/columns with optional row selection
12
+ #
13
+ # Usage:
14
+ # # Simple table (no interaction, just display):
15
+ # streamweaver template table SESSION '{
16
+ # "title": "Users",
17
+ # "headers": ["Name", "Email", "Role"],
18
+ # "rows": [
19
+ # ["Alice", "alice@example.com", "Admin"],
20
+ # ["Bob", "bob@example.com", "User"]
21
+ # ]
22
+ # }'
23
+ # # Returns: {"action": "ok"}
24
+ #
25
+ # # With row selection:
26
+ # streamweaver template table SESSION '{
27
+ # "title": "Select a file",
28
+ # "headers": ["File", "Size", "Modified"],
29
+ # "rows": [
30
+ # ["app.rb", "12kb", "2 hours ago"],
31
+ # ["test.rb", "8kb", "yesterday"]
32
+ # ],
33
+ # "selectable": true
34
+ # }'
35
+ # # Returns: {"selected_row": 0, "selected_data": ["app.rb", "12kb", "2 hours ago"]}
36
+ #
37
+ class Table
38
+ def initialize(session:, config:, port: nil)
39
+ @session = session
40
+ @config = config
41
+ @port = port || detect_port
42
+ @title = config['title']
43
+ @headers = config['headers'] || []
44
+ @rows = config['rows'] || []
45
+ @selectable = config['selectable'] || false
46
+ @actions = config['actions'] || (@selectable ? [] : ['OK'])
47
+ end
48
+
49
+ def run
50
+ clear_submissions
51
+ push_table
52
+ data = wait_for_submission
53
+
54
+ button_id = data['_button'] || ''
55
+
56
+ if @selectable && button_id.start_with?('btn_row_')
57
+ # Extract row index from button id like "btn_row_0_1"
58
+ row_idx = button_id.sub('btn_row_', '').sub(/_\d+$/, '').to_i
59
+ {
60
+ 'selected_row' => row_idx,
61
+ 'selected_data' => @rows[row_idx]
62
+ }
63
+ else
64
+ # Regular action button
65
+ action = extract_action(button_id)
66
+ { 'action' => action }
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def detect_port
73
+ pid_file = File.expand_path('~/.streamweaver/service.pid')
74
+ return 4576 unless File.exist?(pid_file)
75
+ data = JSON.parse(File.read(pid_file))
76
+ data['port'] || 4576
77
+ rescue
78
+ 4576
79
+ end
80
+
81
+ def extract_action(button_id)
82
+ key = button_id.sub(/^btn_/, '').sub(/_\d+$/, '')
83
+ @actions.each do |action|
84
+ normalized = action.to_s.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
85
+ return action if normalized == key
86
+ end
87
+ key
88
+ end
89
+
90
+ def push_table
91
+ dsl = build_dsl
92
+ html = StreamWeaver::CLI.render_dsl_to_html(dsl, session_name: @session)
93
+
94
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/push")
95
+ request = Net::HTTP::Post.new(uri)
96
+ request.set_form_data('content' => html, 'target' => '#main', 'action' => 'replace')
97
+
98
+ Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
99
+ end
100
+
101
+ def build_dsl
102
+ lines = []
103
+ lines << "header2 \"#{@title}\"" if @title
104
+
105
+ # Convert rows to Ruby array literal string
106
+ headers_str = @headers.map { |h| "\"#{h}\"" }.join(", ")
107
+ rows_str = @rows.map { |row| "[#{row.map { |c| "\"#{c}\"" }.join(", ")}]" }.join(", ")
108
+
109
+ if @selectable
110
+ # For selectable tables, render rows as buttons
111
+ lines << "card do"
112
+ @rows.each_with_index do |row, idx|
113
+ row_text = row.join(" | ")
114
+ lines << " button \"#{row_text}\", id: \"row_#{idx}\""
115
+ end
116
+ lines << "end"
117
+ else
118
+ # Use native table component
119
+ lines << "table headers: [#{headers_str}], rows: [#{rows_str}], striped: true, hoverable: true"
120
+
121
+ # Add action buttons if any
122
+ if @actions.any?
123
+ lines << ""
124
+ @actions.each_with_index do |action, i|
125
+ variant = i == 0 ? ", variant: :primary" : ""
126
+ lines << "button \"#{action}\"#{variant}"
127
+ end
128
+ end
129
+ end
130
+
131
+ lines.join("\n")
132
+ end
133
+
134
+ def clear_submissions
135
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
136
+ loop do
137
+ response = Net::HTTP.get_response(uri)
138
+ break unless response.is_a?(Net::HTTPSuccess)
139
+ data = JSON.parse(response.body)
140
+ break if (data['submissions'] || []).empty?
141
+ end
142
+ rescue
143
+ # ignore
144
+ end
145
+
146
+ def wait_for_submission(timeout: 300)
147
+ start_time = Time.now
148
+ uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
149
+
150
+ loop do
151
+ raise "Timeout" if Time.now - start_time > timeout
152
+
153
+ response = Net::HTTP.get_response(uri)
154
+ if response.is_a?(Net::HTTPSuccess)
155
+ data = JSON.parse(response.body)
156
+ submissions = data['submissions'] || []
157
+ return submissions.first['data'] if submissions.any?
158
+ end
159
+
160
+ sleep 0.3
161
+ end
162
+ end
163
+
164
+ def self.run(session:, config:, port: nil)
165
+ new(session: session, config: config, port: port).run
166
+ end
167
+ end
168
+ end
169
+ end