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,108 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Dashboard Components
5
+ # Showcases all dashboard-style components with dark theme
6
+ # Run with: ./exe/streamweaver examples/dashboard_components.rb
7
+
8
+ app "Dashboard Components Demo", theme: :dark, layout: :wide do
9
+ # Header with pulse indicator
10
+ app_header "Dashboard Components", variant: :primary do
11
+ pulse_indicator color: :green, label: "System Active"
12
+ end
13
+
14
+ header2 "Status Dots"
15
+ text "Colored indicator dots with optional glow effect:"
16
+ hstack spacing: :lg, align: :center do
17
+ vstack spacing: :xs, align: :center do
18
+ status_dot status: :red
19
+ text "Red"
20
+ end
21
+ vstack spacing: :xs, align: :center do
22
+ status_dot status: :yellow
23
+ text "Yellow"
24
+ end
25
+ vstack spacing: :xs, align: :center do
26
+ status_dot status: :green
27
+ text "Green"
28
+ end
29
+ vstack spacing: :xs, align: :center do
30
+ status_dot status: :green, pulse: true
31
+ text "Pulsing"
32
+ end
33
+ vstack spacing: :xs, align: :center do
34
+ status_dot status: :gray
35
+ text "Gray"
36
+ end
37
+ end
38
+
39
+ header2 "Badges"
40
+ text "Count/label badges in various variants:"
41
+ hstack spacing: :md do
42
+ badge "5", variant: :default
43
+ badge "3", variant: :danger
44
+ badge "12", variant: :warning
45
+ badge "OK", variant: :success
46
+ badge "new", variant: :info
47
+ end
48
+
49
+ header2 "Stat Displays"
50
+ text "Large metric numbers with labels:"
51
+ hstack spacing: :xl do
52
+ stat_display value: 24, label: "TOTAL", color: :default
53
+ stat_display value: 7, label: "PENDING", color: :blue
54
+ stat_display value: 12, label: "IN PROGRESS", color: :purple
55
+ stat_display value: 5, label: "BLOCKED", color: :red
56
+ end
57
+
58
+ header2 "Type Tags"
59
+ text "Activity type badges:"
60
+ hstack spacing: :md do
61
+ type_tag :research
62
+ type_tag :task
63
+ type_tag :escalation
64
+ type_tag :communication
65
+ type_tag :warning
66
+ type_tag :info
67
+ end
68
+
69
+ header2 "Priority Items"
70
+ text "Items with priority-colored left borders (hover to see slide effect):"
71
+ vstack spacing: :md do
72
+ priority_item priority: :critical, title: "Database migration blocked",
73
+ description: "Production schema changes require downtime window approval.",
74
+ meta_left: "ops", meta_right: "Schedule window"
75
+
76
+ priority_item priority: :urgent, title: "API rate limits exceeded",
77
+ description: "Third-party integration hitting 429 errors. Need backoff strategy.",
78
+ meta_left: "backend", meta_right: "Implement retry"
79
+
80
+ priority_item priority: :high, title: "Security audit findings",
81
+ description: "3 medium-severity items need remediation before release.",
82
+ meta_left: "security", meta_right: "Review findings"
83
+
84
+ priority_item priority: :normal, title: "Documentation update",
85
+ description: "API docs need refresh for v2 endpoints.",
86
+ meta_left: "docs", meta_right: "Update guides"
87
+ end
88
+
89
+ header2 "Activity Items"
90
+ text "Activity feed with time, title, summary, and type (hover to see highlight):"
91
+ vstack spacing: :none do
92
+ activity_item time: "15:00", title: "Performance analysis complete",
93
+ summary: "Identified 3 slow queries, recommended indexes",
94
+ type: :research
95
+
96
+ activity_item time: "15:05", title: "Deployment approval needed",
97
+ summary: "Staging verified, awaiting prod sign-off",
98
+ type: :escalation
99
+
100
+ activity_item time: "14:00", title: "Feature branch merged",
101
+ summary: "User preferences module integrated",
102
+ type: :task
103
+
104
+ activity_item time: "13:30", title: "Standup notes shared",
105
+ summary: "Team aligned on sprint priorities",
106
+ type: :communication
107
+ end
108
+ end
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Manual-verification harness for deferred fragments (`fragment ..., defer: true`)
5
+ # -- the Turbo `turbo_frame_tag ..., src:` equivalent. Every deferred block below
6
+ # sleeps, so the point is visible: the shell paints immediately and each region
7
+ # fills in on its own afterwards, with zero JavaScript in this file.
8
+ #
9
+ # SW_NO_OPEN=1 STREAMWEAVER_PORT=4599 ruby examples/deferred_fragments_demo.rb
10
+ #
11
+ # SW_DEFER_DELAY overrides the per-fragment sleep (default 1.5s, matching the
12
+ # learnhotwire course's `sleep 1.5` hover-card demo).
13
+
14
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
15
+ require "stream_weaver"
16
+
17
+ DELAY = (ENV["SW_DEFER_DELAY"] || "1.5").to_f
18
+
19
+ def slow_work(label)
20
+ sleep DELAY
21
+ "#{label} finished at #{Time.now.strftime('%H:%M:%S.%L')}"
22
+ end
23
+
24
+ app "Deferred Fragments" do
25
+ header1 "Deferred fragments"
26
+ md "This line and everything above it render **immediately**. Each panel below " \
27
+ "sleeps #{DELAY}s inside its `defer: true` block, so the block cannot run " \
28
+ "during the page render -- the shell would take #{(DELAY * 3).round(1)}s if it did."
29
+ text "Shell rendered at #{Time.now.strftime('%H:%M:%S.%L')}"
30
+
31
+ header3 "1. Default placeholder (spinner)"
32
+ fragment :default_placeholder, defer: true do
33
+ text slow_work("Panel 1")
34
+ end
35
+
36
+ header3 "2. String placeholder"
37
+ fragment :string_placeholder, defer: true, placeholder: "Crunching numbers…" do
38
+ text slow_work("Panel 2")
39
+ end
40
+
41
+ header3 "3. Custom DSL placeholder, reading state from the field above it"
42
+ text_field :who, placeholder: "type a name, then reload", label: "Who"
43
+ fragment :reads_state, defer: true, placeholder: -> { alert(variant: :info) { text "Waiting for the server…" } } do
44
+ text slow_work("Panel 3 for #{state[:who].to_s.empty? ? '(nobody)' : state[:who]}")
45
+ end
46
+
47
+ header3 "4. Deferred nested inside deferred (loads as a chain)"
48
+ fragment :outer_panel, defer: true, placeholder: "Outer loading…" do
49
+ text slow_work("Outer")
50
+ fragment :inner_panel, defer: true, placeholder: "Inner loading…" do
51
+ text slow_work("Inner")
52
+ end
53
+ end
54
+
55
+ header3 "5. Full-container swap"
56
+ md "This button swaps the whole app container. The panels above should return " \
57
+ "to their placeholders and fetch again rather than sticking on a placeholder."
58
+ button "Re-render the page" do |s|
59
+ s[:renders] = s[:renders].to_i + 1
60
+ end
61
+ text "Full re-renders so far: #{state[:renders].to_i}"
62
+ end.run!
@@ -0,0 +1,63 @@
1
+ # Generate-More Spike (T1)
2
+
3
+ Standalone spike demonstrating the generate-more loop using **push-to-state** architecture.
4
+
5
+ ## How to Run
6
+
7
+ ```bash
8
+ cd examples/generate_more_spike
9
+ ruby app.rb
10
+ ```
11
+
12
+ The app opens at `http://127.0.0.1:4567` (or next available port).
13
+
14
+ ## What It Demonstrates
15
+
16
+ 1. **Generate More button** -- Enter an optional prompt and select how many options to generate (1-5).
17
+ 2. **Skeleton placeholders** -- While generating, skeleton shimmer cards appear for pending options.
18
+ 3. **Simulated agent** -- A background thread simulates an agent (polls for requests, sleeps 1-3s per option, pushes results to state).
19
+ 4. **Push-to-state** -- New options are written to server-side shared state. SSE sends the re-rendered HTML to the browser. The browser never receives raw data -- it gets a fully rendered content div.
20
+ 5. **Timeout** -- If generation takes longer than 15s, a timeout warning appears.
21
+ 6. **Cancel** -- A cancel button aborts in-progress generation.
22
+ 7. **Session isolation** -- Each browser session gets its own state keyed by session ID.
23
+
24
+ ## Architecture Validated
25
+
26
+ ### Push-to-State Pattern
27
+
28
+ The agent pushes options into a shared server-side state store, NOT directly into the DOM via SSE. StreamWeaver's existing `Streamer.replace()` sends the complete re-rendered HTML for the content div. This:
29
+
30
+ - Eliminates the phantom option race condition (user on different slide when options arrive)
31
+ - Keeps everything in sync automatically (no stale partial state)
32
+ - Aligns with StreamWeaver's reactive model (re-render from state)
33
+ - Is simpler than SSE-to-DOM with targeted element updates
34
+
35
+ ### Key Finding: Session Cookie Limitation
36
+
37
+ The agent thread has NO access to Sinatra's cookie-based session. Push-to-state requires a server-side state store (in-memory hash, file-backed JSON, or similar) keyed by session ID. T10 must implement `DeckState` as a file-backed store, not rely on `session[:streamlit_state]`.
38
+
39
+ ## State Machine
40
+
41
+ ```
42
+ IDLE
43
+ |
44
+ [user clicks Generate]
45
+ |
46
+ v
47
+ GENERATING
48
+ / | \
49
+ [all received] | [user cancels]
50
+ | [timeout 15s] |
51
+ v | v
52
+ IDLE TIMED_OUT CANCELLED
53
+ | |
54
+ [retry/dismiss] [cleanup]
55
+ | |
56
+ v v
57
+ IDLE IDLE
58
+ ```
59
+
60
+ ## Files
61
+
62
+ - `app.rb` -- The spike app (standalone, does not modify the gem)
63
+ - `README.md` -- This file
@@ -0,0 +1,441 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # ============================================================================
5
+ # Generate-More Spike (T1)
6
+ # ============================================================================
7
+ #
8
+ # Standalone spike demonstrating the generate-more loop using push-to-state.
9
+ # This is throwaway code -- T10 will do the real implementation.
10
+ #
11
+ # Key architectural decision being validated:
12
+ # Push-to-state, NOT push-to-DOM.
13
+ # The "agent" pushes new options into server-side state, and StreamWeaver's
14
+ # reactive re-render handles display. No direct DOM manipulation via SSE.
15
+ #
16
+ # Run with:
17
+ # cd examples/generate_more_spike && ruby app.rb
18
+ #
19
+ # What it demonstrates:
20
+ # 1. "Generate More" button with prompt input
21
+ # 2. Skeleton placeholders appear while generating
22
+ # 3. Simulated agent (Thread.new) detects request, sleeps, pushes state
23
+ # 4. Page re-renders reactively with new options replacing skeletons
24
+ # 5. Timeout after 15s shows error message
25
+ # 6. Cancel button aborts pending generation
26
+ # 7. Session-scoped request queue
27
+ # ============================================================================
28
+
29
+ require_relative '../../lib/stream_weaver'
30
+ require 'json'
31
+ require 'securerandom'
32
+
33
+ # ============================================================================
34
+ # Generate-More State Machine (Discovered through implementation)
35
+ # ============================================================================
36
+ # States:
37
+ # :idle - No generation in progress. Generate button enabled.
38
+ # :generating - Request queued, skeletons visible, cancel button shown.
39
+ # :timed_out - 15s elapsed without all options arriving.
40
+ # :cancelled - User cancelled, agent acknowledged. Transient -> idle.
41
+ #
42
+ # Transitions:
43
+ # idle -> generating (user clicks Generate)
44
+ # generating -> idle (all options received)
45
+ # generating -> timed_out (15s timeout, checked in timer)
46
+ # generating -> cancelled (user clicks Cancel)
47
+ # cancelled -> idle (immediate, after cleanup)
48
+ # timed_out -> idle (user clicks Generate again)
49
+ # ============================================================================
50
+
51
+ # ============================================================================
52
+ # Shared state store (simulates file-backed DeckState for the spike)
53
+ # ============================================================================
54
+ # In T10 this becomes DeckState with file-backed JSON per session.
55
+ # Key finding: session cookies can't hold this state (agent thread has no
56
+ # access to Sinatra sessions). A shared server-side store keyed by session
57
+ # ID is required.
58
+
59
+ module SpikeState
60
+ @mutex = Mutex.new
61
+ @sessions = {}
62
+ @pending_requests = []
63
+ @cancelled_sessions = {}
64
+
65
+ class << self
66
+ attr_reader :mutex
67
+
68
+ def get(session_id)
69
+ @mutex.synchronize do
70
+ @sessions[session_id] ||= default_state
71
+ @sessions[session_id].dup
72
+ end
73
+ end
74
+
75
+ def update(session_id)
76
+ @mutex.synchronize do
77
+ @sessions[session_id] ||= default_state
78
+ yield @sessions[session_id]
79
+ end
80
+ end
81
+
82
+ def queue_request(request)
83
+ @mutex.synchronize { @pending_requests << request }
84
+ end
85
+
86
+ def take_requests
87
+ @mutex.synchronize do
88
+ taken = @pending_requests.dup
89
+ @pending_requests.clear
90
+ taken
91
+ end
92
+ end
93
+
94
+ def cancel(session_id)
95
+ @mutex.synchronize { @cancelled_sessions[session_id] = true }
96
+ end
97
+
98
+ def cancelled?(session_id)
99
+ @mutex.synchronize { @cancelled_sessions.delete(session_id) || false }
100
+ end
101
+
102
+ private
103
+
104
+ def default_state
105
+ {
106
+ options: [
107
+ { id: "opt_1", label: "Monolith", description: "Single deployable unit, simpler operations", generated: false },
108
+ { id: "opt_2", label: "Microservices", description: "Independent services, complex but scalable", generated: false },
109
+ ],
110
+ generate_status: :idle,
111
+ requested_count: 0,
112
+ received_count: 0,
113
+ prompt: nil,
114
+ started_at: nil,
115
+ }
116
+ end
117
+ end
118
+ end
119
+
120
+ # ============================================================================
121
+ # Simulated agent options
122
+ # ============================================================================
123
+
124
+ SIMULATED_OPTIONS = [
125
+ { label: "Event-Driven", description: "Loosely coupled via message bus, great for async workflows" },
126
+ { label: "Serverless", description: "Functions-as-a-service, pay per invocation, auto-scaling" },
127
+ { label: "Modular Monolith", description: "Best of both: monolith deployment, modular internals" },
128
+ { label: "Hexagonal", description: "Ports and adapters pattern, testable and flexible" },
129
+ { label: "CQRS", description: "Separate read/write models for optimized query performance" },
130
+ { label: "Service Mesh", description: "Sidecar proxies handle networking, observability built-in" },
131
+ ]
132
+
133
+ # ============================================================================
134
+ # Simulated Agent Thread
135
+ # ============================================================================
136
+ # In the real system, an external agent polls GET /deck/pending.
137
+ # Here we simulate with a thread watching the shared state.
138
+
139
+ $streamer_ref = nil
140
+ $sim_option_index = 0
141
+
142
+ def start_agent_simulator
143
+ Thread.new do
144
+ loop do
145
+ requests = SpikeState.take_requests
146
+
147
+ requests.each do |request|
148
+ session_id = request[:session_id]
149
+ count = request[:count]
150
+ prompt = request[:prompt]
151
+
152
+ $stderr.puts "[Agent] Processing generate request: count=#{count}, prompt=#{prompt.inspect}"
153
+
154
+ count.times do |i|
155
+ # Check cancellation
156
+ if SpikeState.cancelled?(session_id)
157
+ $stderr.puts "[Agent] Generation cancelled for session #{session_id}"
158
+ SpikeState.update(session_id) { |s| s[:generate_status] = :idle }
159
+ push_rerender(session_id)
160
+ break
161
+ end
162
+
163
+ # Simulate LLM generation delay (1-3 seconds per option)
164
+ sleep(1.0 + rand * 2.0)
165
+
166
+ # Check cancellation again after sleep
167
+ if SpikeState.cancelled?(session_id)
168
+ $stderr.puts "[Agent] Generation cancelled (post-sleep) for session #{session_id}"
169
+ SpikeState.update(session_id) { |s| s[:generate_status] = :idle }
170
+ push_rerender(session_id)
171
+ break
172
+ end
173
+
174
+ # Check if already timed out
175
+ state = SpikeState.get(session_id)
176
+ if state[:generate_status] != :generating
177
+ $stderr.puts "[Agent] State is #{state[:generate_status]}, stopping generation"
178
+ break
179
+ end
180
+
181
+ # Pick a simulated option
182
+ opt = SIMULATED_OPTIONS[$sim_option_index % SIMULATED_OPTIONS.length]
183
+ $sim_option_index += 1
184
+
185
+ desc = prompt && !prompt.strip.empty? ?
186
+ "#{opt[:description]} (prompted: #{prompt})" :
187
+ opt[:description]
188
+
189
+ opt_id = "gen_#{SecureRandom.hex(4)}"
190
+
191
+ # PUSH TO STATE -- the key architectural decision
192
+ SpikeState.update(session_id) do |s|
193
+ s[:options] << {
194
+ id: opt_id,
195
+ label: opt[:label],
196
+ description: desc,
197
+ generated: true,
198
+ }
199
+ s[:received_count] += 1
200
+ $stderr.puts "[Agent] Pushed option #{i + 1}/#{count}: #{opt[:label]} (#{s[:received_count]}/#{s[:requested_count]})"
201
+
202
+ # If all received, transition to idle
203
+ if s[:received_count] >= s[:requested_count]
204
+ s[:generate_status] = :idle
205
+ $stderr.puts "[Agent] All options received -> idle"
206
+ end
207
+ end
208
+
209
+ # Notify browser to re-render via SSE
210
+ push_rerender(session_id)
211
+ end
212
+ end
213
+
214
+ sleep 0.5 unless requests.any?
215
+ end
216
+ end
217
+ end
218
+
219
+ def push_rerender(session_id)
220
+ return unless $streamer_ref
221
+ html = build_options_html(session_id)
222
+ $streamer_ref.replace("#spike-content", html)
223
+ end
224
+
225
+ # ============================================================================
226
+ # HTML builder for option cards + skeletons + status
227
+ # ============================================================================
228
+ # This is what StreamWeaver's reactive re-render would produce.
229
+ # The component tree reads state and renders the current view.
230
+
231
+ def build_options_html(session_id)
232
+ state = SpikeState.get(session_id)
233
+ status = state[:generate_status]
234
+ options = state[:options]
235
+ requested = state[:requested_count]
236
+ received = state[:received_count]
237
+ remaining = status == :generating ? [requested - received, 0].max : 0
238
+
239
+ html = +""
240
+
241
+ # Status banner
242
+ case status
243
+ when :generating
244
+ elapsed = state[:started_at] ? (Time.now.to_f - state[:started_at]).round(1) : 0
245
+ html << %(<div style="background:#1e3a5f;border:1px solid #3b82f6;border-radius:8px;padding:12px 16px;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between">)
246
+ html << %(<div style="display:flex;align-items:center;gap:8px">)
247
+ html << %(<div style="width:12px;height:12px;border-radius:50%;background:#3b82f6;animation:pulse 1s ease-in-out infinite"></div>)
248
+ html << %(<span style="color:#93c5fd">Generating #{requested} option(s)... #{received}/#{requested} received (#{elapsed}s)</span>)
249
+ html << %(</div>)
250
+ html << %(<form method="post" action="/spike/cancel" style="margin:0">)
251
+ html << %(<input type="hidden" name="session_id" value="#{session_id}">)
252
+ html << %(<button type="submit" style="background:#dc2626;color:white;border:none;border-radius:6px;padding:6px 16px;cursor:pointer;font-size:0.875rem">Cancel</button>)
253
+ html << %(</form></div>)
254
+ when :timed_out
255
+ html << %(<div style="background:#451a03;border:1px solid #f59e0b;border-radius:8px;padding:12px 16px;margin-bottom:16px">)
256
+ html << %(<span style="color:#fbbf24">Generation timed out after 15s. #{received}/#{requested} options received. Click Generate to try again.</span>)
257
+ html << %(</div>)
258
+ end
259
+
260
+ # Options grid
261
+ html << %(<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px">)
262
+
263
+ options.each do |opt|
264
+ border = opt[:generated] ? "#22c55e" : "#374151"
265
+ badge = opt[:generated] ?
266
+ %(<span style="background:#166534;color:#86efac;font-size:0.7rem;padding:2px 8px;border-radius:9999px;margin-left:8px">generated</span>) : ""
267
+
268
+ html << %(<div style="background:#1f2937;border:1px solid #{border};border-radius:12px;padding:16px">)
269
+ html << %(<h3 style="margin:0 0 8px;color:#f3f4f6;font-size:1rem">#{opt[:label]}#{badge}</h3>)
270
+ html << %(<p style="margin:0;color:#9ca3af;font-size:0.875rem">#{opt[:description]}</p>)
271
+ html << %(</div>)
272
+ end
273
+
274
+ # Skeleton placeholders
275
+ remaining.times do
276
+ html << %(<div style="background:#1f2937;border:1px solid #374151;border-radius:12px;padding:16px">)
277
+ html << %(<div style="background:#374151;height:20px;width:60%;border-radius:4px;margin-bottom:12px;animation:shimmer 1.5s ease-in-out infinite alternate"></div>)
278
+ html << %(<div style="background:#374151;height:14px;width:90%;border-radius:4px;margin-bottom:8px;animation:shimmer 1.5s ease-in-out infinite alternate 0.2s"></div>)
279
+ html << %(<div style="background:#374151;height:14px;width:75%;border-radius:4px;animation:shimmer 1.5s ease-in-out infinite alternate 0.4s"></div>)
280
+ html << %(</div>)
281
+ end
282
+
283
+ html << %(</div>)
284
+
285
+ # CSS for animations (included each push since SSE replaces content)
286
+ html << %(<style>)
287
+ html << %(@keyframes shimmer{from{opacity:0.5}to{opacity:1}})
288
+ html << %(@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}})
289
+ html << %(</style>)
290
+
291
+ html
292
+ end
293
+
294
+ # ============================================================================
295
+ # StreamWeaver App
296
+ # ============================================================================
297
+
298
+ SpikeApp = app "Generate-More Spike", theme: :dark, stylesheets: [
299
+ "data:text/css,@keyframes shimmer{from{opacity:0.5}to{opacity:1}}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}"
300
+ ] do
301
+ # Assign a session ID on first visit
302
+ state[:spike_session] ||= SecureRandom.hex(8)
303
+ session_id = state[:spike_session]
304
+
305
+ # Initialize session state if needed
306
+ SpikeState.get(session_id)
307
+ spike_state = SpikeState.get(session_id)
308
+
309
+ header1 "Generate-More Spike"
310
+ text "Demonstrates the push-to-state architecture for the generate-more loop."
311
+ text "Options below are pre-seeded. Click 'Generate More' to simulate an agent generating additional options."
312
+
313
+ div style: "border-top:1px solid #374151;margin:16px 0"
314
+
315
+ # Generate controls
316
+ card do
317
+ header3 "Generate More Options"
318
+
319
+ text_field :gen_prompt, placeholder: "e.g., Focus on event-driven patterns", label: "Prompt (optional)"
320
+
321
+ select :gen_count, ["1", "2", "3", "4", "5"], default: "2", label: "Number of options"
322
+
323
+ is_generating = spike_state[:generate_status] == :generating
324
+
325
+ unless is_generating
326
+ button "Generate More", variant: :primary do |s|
327
+ sid = s[:spike_session]
328
+ count = (s[:gen_count] || "2").to_i
329
+ prompt = s[:gen_prompt]
330
+
331
+ # Transition to :generating
332
+ SpikeState.update(sid) do |ss|
333
+ ss[:generate_status] = :generating
334
+ ss[:requested_count] = count
335
+ ss[:received_count] = 0
336
+ ss[:prompt] = prompt
337
+ ss[:started_at] = Time.now.to_f
338
+ end
339
+
340
+ # Queue request for the agent simulator
341
+ SpikeState.queue_request(
342
+ session_id: sid,
343
+ count: count,
344
+ prompt: prompt,
345
+ timestamp: Time.now,
346
+ )
347
+
348
+ $stderr.puts "[Server] Generate request queued: session=#{sid} count=#{count} prompt=#{prompt.inspect}"
349
+ end
350
+ end
351
+ end
352
+
353
+ # Content area -- this div is replaced by SSE pushes
354
+ div id: "spike-content" do
355
+ # Initial render: show current options from spike state
356
+ spike_state[:options].each do |opt|
357
+ card do
358
+ badge_label = opt[:generated] ? " [generated]" : ""
359
+ header3 "#{opt[:label]}#{badge_label}"
360
+ text opt[:description]
361
+ end
362
+ end
363
+
364
+ # Show skeletons if currently generating
365
+ if spike_state[:generate_status] == :generating
366
+ remaining = [spike_state[:requested_count] - spike_state[:received_count], 0].max
367
+ remaining.times do
368
+ div style: "background:#1f2937;border:1px solid #374151;border-radius:12px;padding:16px;margin-top:8px" do
369
+ div style: "background:#374151;height:20px;width:60%;border-radius:4px;margin-bottom:12px;animation:shimmer 1.5s ease-in-out infinite alternate"
370
+ div style: "background:#374151;height:14px;width:90%;border-radius:4px;margin-bottom:8px;animation:shimmer 1.5s ease-in-out infinite alternate 0.2s"
371
+ div style: "background:#374151;height:14px;width:75%;border-radius:4px;animation:shimmer 1.5s ease-in-out infinite alternate 0.4s"
372
+ end
373
+ end
374
+ end
375
+
376
+ if spike_state[:generate_status] == :timed_out
377
+ alert variant: :warning, title: "Timeout" do
378
+ text "Generation timed out after 15s. #{spike_state[:received_count]}/#{spike_state[:requested_count]} options received."
379
+ end
380
+ end
381
+ end
382
+
383
+ # State debug panel
384
+ div style: "margin-top:24px;padding:16px;background:#111827;border:1px solid #1f2937;border-radius:8px" do
385
+ header4 "State Debug"
386
+ text "Session: #{session_id}"
387
+ text "Status: #{spike_state[:generate_status]}"
388
+ text "Options: #{spike_state[:options].length} (#{spike_state[:options].count { |o| o[:generated] }} generated)"
389
+ text "Requested: #{spike_state[:requested_count]}, Received: #{spike_state[:received_count]}"
390
+ if spike_state[:started_at] && spike_state[:generate_status] == :generating
391
+ text "Elapsed: #{(Time.now.to_f - spike_state[:started_at]).round(1)}s"
392
+ end
393
+ end
394
+
395
+ # SSE stream: agent pushes re-rendered content here
396
+ stream do |streamer|
397
+ $streamer_ref = streamer
398
+ # Timeout checker loop
399
+ loop do
400
+ sleep 1
401
+ SpikeState.mutex.synchronize do
402
+ SpikeState.instance_variable_get(:@sessions).each do |sid, s|
403
+ next unless s[:generate_status] == :generating && s[:started_at]
404
+ elapsed = Time.now.to_f - s[:started_at]
405
+ if elapsed > 15
406
+ s[:generate_status] = :timed_out
407
+ $stderr.puts "[Server] Session #{sid} timed out (#{elapsed.round(1)}s)"
408
+ Thread.new { push_rerender(sid) }
409
+ end
410
+ end
411
+ end
412
+ end
413
+ end
414
+ end
415
+
416
+ # ============================================================================
417
+ # Custom routes (cancel endpoint)
418
+ # ============================================================================
419
+
420
+ SpikeApp.post '/spike/cancel' do
421
+ state = session[:streamlit_state] ||= {}
422
+ session_id = state[:spike_session]
423
+
424
+ if session_id
425
+ SpikeState.cancel(session_id)
426
+ SpikeState.update(session_id) { |s| s[:generate_status] = :idle }
427
+ $stderr.puts "[Server] Cancel for session #{session_id}"
428
+ Thread.new { push_rerender(session_id) }
429
+ end
430
+
431
+ redirect '/'
432
+ end
433
+
434
+ # ============================================================================
435
+ # Start
436
+ # ============================================================================
437
+
438
+ agent_thread = start_agent_simulator
439
+ $stderr.puts "[Spike] Agent simulator started"
440
+
441
+ SpikeApp.run!