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,1725 @@
1
+ # StreamWeaver Visual Skills -- Architecture Design
2
+
3
+ *Date: 2026-03-12*
4
+ *Author: Architecture design for porting pi-design-deck and visual-explainer into StreamWeaver*
5
+
6
+ ---
7
+
8
+ ## 1. Design Philosophy
9
+
10
+ ### Core Principles
11
+
12
+ **The DSL IS the API.** An agent (Claude Code, Pi, Codex) communicates with StreamWeaver by writing Ruby DSL calls. Not JSON. Not raw HTML. Ruby. The DSL is expressive enough that the agent's output reads like a description of what should appear, and the framework handles rendering, theming, interactivity, and streaming.
13
+
14
+ **Follow existing patterns.** StreamWeaver already has a clear architecture: `Components::Base` subclasses, `DisplayDSL` module methods, `Adapter::Base` rendering dispatch, Phlex views, Sinatra routes, and `Pushable` for SSE. New visual skills components slot into this architecture -- they do not introduce parallel systems.
15
+
16
+ **Pareto code.** 37% of the two projects is shared infrastructure. Build the shared 37% first, then the deck-specific and explainer-specific components layer on top. No premature abstraction: if a component is only used by one skill, it lives in that skill's namespace.
17
+
18
+ **Composable, not monolithic.** Each component is independently usable. You can drop a `mermaid` block into any StreamWeaver app, not just a design deck or visual explainer. The deck and explainer are orchestrators that compose shared components into specific workflows.
19
+
20
+ ### Architectural Decision: Flat Namespace
21
+
22
+ Components live in `StreamWeaver::Components::*` -- the same namespace as existing components. No `VisualSkills` sub-namespace. Rationale:
23
+
24
+ 1. Existing components like `Table`, `Card`, `Grid` are already in `Components::*`
25
+ 2. New components (`Mermaid`, `CodeBlock`, `Pipeline`) are general-purpose -- they belong alongside `Table` and `Card`
26
+ 3. The DSL methods on `DisplayDSL` are flat (`mermaid`, `code_block`, `pipeline`) -- nesting the classes adds complexity with no benefit
27
+ 4. Deck-specific orchestrators (`DesignDeck`, `DeckSlide`) are the exception -- they get a `Components::Deck::*` sub-namespace because they form a coherent subsystem
28
+
29
+ ---
30
+
31
+ ## 2. Module Structure
32
+
33
+ ### Directory Tree
34
+
35
+ ```
36
+ lib/stream_weaver/
37
+ components.rb # Existing -- add new component classes here
38
+ display_dsl.rb # Existing -- add new DSL methods here
39
+ app.rb # Existing -- add deck/explainer DSL methods
40
+ theme.rb # Existing -- enhance with presets, auto-mode
41
+ pushable.rb # Existing -- no changes needed
42
+ streamer.rb # Existing -- no changes needed
43
+ feed.rb # Existing -- no changes needed
44
+
45
+ components/
46
+ mermaid.rb # Mermaid diagram component
47
+ code_block.rb # Syntax-highlighted code block
48
+ image_block.rb # Image with caption, base64 export support
49
+ slide_container.rb # Slide navigation (swap + scroll-snap modes)
50
+ slide.rb # Single slide within a container
51
+ progress_indicator.rb # Fixed-position navigation progress bar
52
+ keyboard_shortcuts.rb # Shortcut registration system
53
+ callout.rb # Bordered info/warning/tip box
54
+ pipeline.rb # Step flow visualization
55
+ comparison.rb # Side-by-side diff panels
56
+ chart.rb # Chart.js wrapper
57
+ sidebar_toc.rb # Sticky TOC with scroll spy
58
+ ve_card.rb # Depth-tiered card (hero/elevated/default/recessed)
59
+ kpi_dashboard.rb # Metrics grid wrapping stat_display
60
+ data_table.rb # Enhanced table with sticky headers
61
+
62
+ # NOTE: hero_section, prose, pullquote, dir_tree, legend, flow_arrow,
63
+ # and layout_toggle are CSS-only helpers -- no separate component files.
64
+ # They are implemented as thin DSL methods that emit styled divs directly.
65
+
66
+ deck/ # Design deck subsystem
67
+ design_deck.rb # Top-level deck orchestrator
68
+ deck_slide.rb # Decision slide with options grid
69
+ deck_option.rb # Selectable option card
70
+ deck_summary.rb # Auto-generated summary slide
71
+ generate_more_controls.rb # Generate button, count, prompt
72
+ skeleton_placeholder.rb # Shimmer loading animation
73
+ model_selector.rb # AI model picker
74
+ confirmation_bar.rb # Cancel confirmation
75
+ close_overlay.rb # Post-submit/cancel overlay
76
+
77
+ theme/
78
+ presets.rb # Curated font+color preset definitions
79
+ auto_mode.rb # OS preference detection, localStorage persistence
80
+
81
+ export/
82
+ html_exporter.rb # Self-contained HTML generation pipeline
83
+
84
+ assets/
85
+ js/
86
+ mermaid_zoom.js # ~200 line zoom/pan engine
87
+ keyboard_shortcuts.js # Centralized key handler
88
+ slide_navigation.js # Shared slide nav logic
89
+ deck_selection.js # Option selection radio behavior
90
+ generate_more.js # SSE listener for new options
91
+ css/
92
+ visual_skills.css # Shared visual skills styles
93
+ deck.css # Deck-specific styles
94
+ explainer.css # Explainer-specific styles
95
+ depth_tiers.css # Surface depth tier styles
96
+ slide_transitions.css # Slide animation presets
97
+ ```
98
+
99
+ ### Module Inclusion Hierarchy
100
+
101
+ ```
102
+ StreamWeaver::DisplayDSL # Shared display-only DSL
103
+ includes: mermaid, code_block, callout, pipeline, comparison,
104
+ chart, ve_card, kpi_dashboard, data_table,
105
+ image_block, sidebar_toc, slide_container
106
+ # CSS-only helpers (thin DSL methods, no Component class):
107
+ # prose, pullquote, hero_section, dir_tree, legend,
108
+ # flow_arrow, layout_toggle
109
+
110
+ StreamWeaver::App
111
+ includes DisplayDSL # Gets all shared methods
112
+ adds: design_deck, theme_toggle, keyboard_shortcuts,
113
+ html_export, toast (already exists),
114
+ theme_preset, generate_more_controls, model_selector,
115
+ confirmation_bar, close_overlay
116
+
117
+ StreamWeaver::FeedBuilder
118
+ includes DisplayDSL # Feed can push any display component
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 3. Shared Components
124
+
125
+ ### 3.1 Mermaid
126
+
127
+ ```ruby
128
+ # DSL usage
129
+ mermaid "graph TD\n A-->B\n B-->C"
130
+ mermaid "graph LR\n A-->B", zoom: true, layout: :elk
131
+ mermaid code, compact: true, theme_vars: { primaryColor: "#ff0000" }
132
+
133
+ # Class
134
+ class Components::Mermaid < Components::Base
135
+ attr_reader :code, :zoom, :compact, :layout, :theme_vars
136
+
137
+ def initialize(code, zoom: false, compact: false, layout: :default, theme_vars: {}, **options)
138
+ def render(view, state)
139
+ def cdn_assets # => [:mermaid] or [:mermaid, :mermaid_elk]
140
+ end
141
+
142
+ # Adapter method
143
+ class Adapter::AlpineJS
144
+ def render_mermaid(view, component, state)
145
+ # Renders: <div class="sw-mermaid-wrap" [x-data for zoom state]>
146
+ # <pre class="mermaid">#{code}</pre>
147
+ # [zoom controls if zoom: true]
148
+ # </div>
149
+ end
150
+ end
151
+ ```
152
+
153
+ Conceptual HTML output:
154
+ - Compact mode: `<div class="sw-mermaid sw-mermaid--compact"><pre class="mermaid">...</pre></div>`
155
+ - Zoom mode: `<div class="sw-mermaid sw-mermaid--zoom" x-data="mermaidZoom()">` with +/- buttons, expand button, pan/zoom container
156
+
157
+ CDN: `<script type="module">import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'</script>`
158
+
159
+ ### 3.2 CodeBlock
160
+
161
+ ```ruby
162
+ # DSL usage
163
+ code_block "const x = 1;", lang: "javascript"
164
+ code_block File.read("src/app.rb"), lang: "ruby", file: "src/app.rb"
165
+ code_block snippet, lang: "ts", truncate: 3 # thumbnail mode
166
+
167
+ # Class
168
+ class Components::CodeBlock < Components::Base
169
+ attr_reader :code, :lang, :file, :truncate, :scroll
170
+
171
+ def initialize(code, lang: nil, file: nil, truncate: nil, scroll: true, **options)
172
+ def render(view, state)
173
+ def cdn_assets # => [:prismjs]
174
+ end
175
+ ```
176
+
177
+ Conceptual HTML: `<div class="sw-code-block"><div class="sw-code-header">src/app.rb</div><pre><code class="language-ruby">...</code></pre></div>`
178
+
179
+ ### 3.3 ThemeToggle
180
+
181
+ ```ruby
182
+ # DSL usage (in App context)
183
+ theme_toggle mode: :auto, hotkey: "mod+shift+l"
184
+
185
+ # Class
186
+ class Components::ThemeToggle < Components::Base
187
+ def initialize(mode: :dark, hotkey: nil, persist: true, **options)
188
+ def render(view, state)
189
+ end
190
+ ```
191
+
192
+ This enhances the existing `ThemeSwitcher` component. Adds:
193
+ - Auto mode via `prefers-color-scheme` media query listener
194
+ - Keyboard shortcut registration (delegates to `KeyboardShortcuts`)
195
+ - `localStorage` persistence of override
196
+ - `<meta name="theme-color">` management
197
+ - Sun/moon toggle button UI
198
+
199
+ ### 3.4 KeyboardShortcuts
200
+
201
+ ```ruby
202
+ # DSL usage (in App context)
203
+ keyboard_shortcuts do |kb|
204
+ kb.on "mod+s", context: :global do |state|
205
+ # save action
206
+ end
207
+ kb.on "ArrowRight", context: :navigation do |state|
208
+ # next slide
209
+ end
210
+ kb.on "1..9", context: :selection do |state, key|
211
+ # quick select
212
+ end
213
+ end
214
+
215
+ # Class
216
+ class Components::KeyboardShortcuts < Components::Base
217
+ def initialize(**options)
218
+ def on(key, context: :global, &block)
219
+ def render(view, state) # Emits JS registration script
220
+ end
221
+ ```
222
+
223
+ This is a non-visual component. It emits a `<script>` block that registers keyboard handlers. Context awareness is handled client-side: handlers check `document.activeElement` against suppression selectors (`.sw-mermaid-wrap`, `.sw-code-scroll`, `textarea`, `input[type=text]`).
224
+
225
+ "mod" maps to Meta on Mac, Control elsewhere. This mapping happens in JS.
226
+
227
+ ### 3.5 HtmlExporter
228
+
229
+ ```ruby
230
+ # DSL usage (not a display component -- called programmatically)
231
+ StreamWeaver::Export::HtmlExporter.export(
232
+ app,
233
+ path: "~/.agent/diagrams/review.html",
234
+ inline_images: true
235
+ )
236
+
237
+ # Class (not a Component -- infrastructure)
238
+ module StreamWeaver::Export
239
+ class HtmlExporter
240
+ def initialize(app, state: {})
241
+ def export(path:, inline_images: false)
242
+ def to_html(inline_images: false) # => String
243
+
244
+ private
245
+ def collect_cdn_links # Mermaid, Chart.js, Prism.js, Google Fonts
246
+ def inline_css # All theme + component CSS
247
+ def inline_images_as_base64 # Convert image src to data URIs
248
+ def render_body # Phlex render to string
249
+ end
250
+ end
251
+ ```
252
+
253
+ ### 3.6 SlideContainer
254
+
255
+ ```ruby
256
+ # DSL usage
257
+ slide_container mode: :swap, progress_bar: true do
258
+ slide "intro", "Introduction" do
259
+ # content
260
+ end
261
+ slide "arch", "Architecture" do
262
+ # content
263
+ end
264
+ end
265
+
266
+ slide_container mode: :scroll_snap, nav_dots: true, counter: true do
267
+ slide "title", type: :title do ... end
268
+ slide "content1", type: :content do ... end
269
+ end
270
+
271
+ # Class
272
+ class Components::SlideContainer < Components::Base
273
+ attr_reader :mode, :progress_bar, :nav_dots, :counter, :keyboard_nav
274
+ attr_accessor :children # Array<Components::Slide>
275
+
276
+ def initialize(mode: :swap, progress_bar: true, keyboard_nav: true,
277
+ nav_dots: false, counter: false, **options)
278
+ def render(view, state)
279
+ end
280
+
281
+ class Components::Slide < Components::Base
282
+ attr_reader :id, :title, :type
283
+ attr_accessor :children
284
+
285
+ def initialize(id, title = nil, type: :content, **options)
286
+ def render(view, state)
287
+ end
288
+ ```
289
+
290
+ Two modes share the same container class:
291
+ - `:swap` -- only active slide visible, Back/Next buttons, fade transition (deck use case)
292
+ - `:scroll_snap` -- all slides rendered, CSS `scroll-snap-type: y mandatory`, 100dvh per slide (explainer use case)
293
+
294
+ Both modes share: keyboard navigation state, progress tracking, Alpine.js `x-data` for current slide index.
295
+
296
+ ### 3.7 ProgressIndicator
297
+
298
+ ```ruby
299
+ # DSL usage (usually implicit within slide_container)
300
+ progress_indicator current: 2, total: 5, position: :top
301
+
302
+ # Class
303
+ class Components::ProgressIndicator < Components::Base
304
+ def initialize(current: 0, total: 0, position: :top, animated: true, **options)
305
+ def render(view, state)
306
+ end
307
+ ```
308
+
309
+ Renders a fixed-position bar at the top of the viewport. Width = `(current / total) * 100%`. CSS transition on width for animation.
310
+
311
+ ### 3.8 Toast
312
+
313
+ Already exists as `Components::ToastContainer` with `show_toast` DSL method. No new component needed. The existing implementation covers deck's needs for save confirmations and generation timeouts.
314
+
315
+ ### 3.9 ImageBlock
316
+
317
+ ```ruby
318
+ # DSL usage
319
+ image_block "assets/diagram.png", alt: "Architecture", caption: "System overview"
320
+ image_block "/path/to/file.png", base64: true # for export mode
321
+
322
+ # Class
323
+ class Components::ImageBlock < Components::Base
324
+ attr_reader :src, :alt, :caption, :base64_mode
325
+
326
+ def initialize(src, alt: "", caption: nil, base64: false, **options)
327
+ def render(view, state)
328
+ def to_data_uri # Convert file to base64 data URI
329
+ end
330
+ ```
331
+
332
+ StreamWeaver's Rack server serves static assets. For local file paths, the adapter generates a URL to an asset-serving endpoint. In export mode (`base64: true`), the image is inlined as a data URI.
333
+
334
+ ---
335
+
336
+ ## 4. Design Deck Architecture
337
+
338
+ ### 4.1 DesignDeck -- The Orchestrator
339
+
340
+ The design deck is **not** a subclass of `App`. It is a set of DSL methods available in `App` that compose shared components into the deck workflow. This follows the existing pattern where `tabs`, `modal`, and `form` are DSL methods on `App`, not separate app types.
341
+
342
+ ```ruby
343
+ # Agent writes this Ruby script:
344
+ app "Architecture Direction", theme: :dark do
345
+ design_deck "Architecture Direction" do
346
+ slide "arch", "System Architecture", context: "Choose the backend pattern" do
347
+ option "Monolith", aside: "Simpler to deploy" do
348
+ mermaid "graph TD\n Client-->API\n API-->DB", compact: true
349
+ code_block "app.listen(3000)", lang: "ts"
350
+ end
351
+ option "Microservices", recommended: true do
352
+ mermaid "graph LR\n Gateway-->Auth\n Gateway-->Orders", compact: true
353
+ end
354
+ end
355
+
356
+ slide "db", "Database Strategy" do
357
+ option "PostgreSQL", aside: "ACID compliance" do
358
+ code_block "CREATE TABLE users (...)", lang: "sql"
359
+ end
360
+ option "MongoDB" do
361
+ code_block "db.users.insertOne({...})", lang: "javascript"
362
+ end
363
+ end
364
+ end
365
+ end
366
+ ```
367
+
368
+ ```ruby
369
+ # Class hierarchy
370
+ class Components::Deck::DesignDeck < Components::Base
371
+ attr_reader :title
372
+ attr_accessor :children # Array<Deck::DeckSlide>
373
+
374
+ def initialize(title, **options)
375
+ def render(view, state)
376
+
377
+ # Validates: no duplicate slide IDs, no "summary" ID, only one active deck
378
+ def validate!
379
+ end
380
+ ```
381
+
382
+ The `design_deck` DSL method on `App`:
383
+
384
+ ```ruby
385
+ class App
386
+ def design_deck(title, **options, &block)
387
+ deck = Components::Deck::DesignDeck.new(title, **options)
388
+ @components << deck
389
+ @current_deck = deck
390
+
391
+ parent_components = @components
392
+ @components = []
393
+ instance_eval(&block)
394
+ deck.children = @components
395
+ @components = parent_components
396
+
397
+ # Auto-append summary slide
398
+ deck.children << Components::Deck::DeckSummary.new
399
+ deck.validate!
400
+
401
+ @current_deck = nil
402
+ deck
403
+ end
404
+
405
+ def slide(id, title = nil, **options, &block)
406
+ raise "slide must be inside design_deck" unless @current_deck
407
+ slide = Components::Deck::DeckSlide.new(id, title, **options)
408
+ @components << slide
409
+
410
+ parent_components = @components
411
+ @components = []
412
+ instance_eval(&block)
413
+ slide.children = @components
414
+ @components = parent_components
415
+ end
416
+
417
+ def option(label, **options, &block)
418
+ raise "option must be inside a slide" unless @current_deck
419
+ opt = Components::Deck::DeckOption.new(label, **options)
420
+ @components << opt
421
+
422
+ parent_components = @components
423
+ @components = []
424
+ instance_eval(&block)
425
+ opt.children = @components # preview blocks: mermaid, code_block, etc.
426
+ @components = parent_components
427
+ end
428
+ end
429
+ ```
430
+
431
+ ### 4.2 DeckSlide
432
+
433
+ ```ruby
434
+ class Components::Deck::DeckSlide < Components::Base
435
+ attr_reader :id, :title, :context_text, :columns
436
+ attr_accessor :children # Array<Deck::DeckOption>
437
+
438
+ def initialize(id, title, context: nil, columns: nil, **options)
439
+ def render(view, state)
440
+ def auto_columns # => 1, 2, 3, or 4 based on option count
441
+ end
442
+ ```
443
+
444
+ Renders as a grid of option cards within a slide wrapper. Auto-column detection: 1 option -> 1 col, 2 -> 2, 3 -> 3, 4+ -> 2 or auto-fit.
445
+
446
+ ### 4.3 DeckOption
447
+
448
+ ```ruby
449
+ class Components::Deck::DeckOption < Components::Base
450
+ attr_reader :label, :aside, :recommended
451
+ attr_accessor :children # Preview blocks
452
+
453
+ def initialize(label, aside: nil, recommended: false, description: nil, **options)
454
+ def render(view, state)
455
+ end
456
+ ```
457
+
458
+ Renders as a clickable card with:
459
+ - Radio indicator (unfilled circle / filled circle)
460
+ - Label header with optional "Recommended" badge
461
+ - Preview content area (children: mermaid, code_block, image_block, etc.)
462
+ - Aside text below preview
463
+ - Notes textarea
464
+
465
+ Selection state stored in `state[:deck_selections]` hash: `{ slide_id => option_label }`.
466
+
467
+ ARIA: `role="radio"`, `aria-checked`, parent grid has `role="radiogroup"`.
468
+
469
+ ### 4.4 DeckSummary
470
+
471
+ ```ruby
472
+ class Components::Deck::DeckSummary < Components::Base
473
+ def initialize(**options)
474
+ def render(view, state)
475
+ # Reads state[:deck_selections] to build summary cards
476
+ # Shows: slide title, selected option label, preview thumbnail, aside, notes
477
+ # Submit button gated on complete selections
478
+ # Final notes textarea
479
+ end
480
+ end
481
+ ```
482
+
483
+ Auto-generated. No DSL call needed -- appended automatically by `design_deck`.
484
+
485
+ ### 4.5 GenerateMoreControls
486
+
487
+ ```ruby
488
+ # Rendered within each slide's footer area
489
+ class Components::Deck::GenerateMoreControls < Components::Base
490
+ attr_reader :max_count
491
+
492
+ def initialize(max_count: 3, **options)
493
+ def render(view, state)
494
+ end
495
+ ```
496
+
497
+ Renders: prompt text input, count dropdown (1-3), "Generate" button, "Regenerate all" button. Uses Alpine.js for loading states.
498
+
499
+ ### 4.6 Remaining Deck Components
500
+
501
+ ```ruby
502
+ # All in Components::Deck namespace
503
+
504
+ class SkeletonPlaceholder < Components::Base
505
+ def initialize(count: 1, **options)
506
+ # Shimmer animation: linear-gradient background-position 1.5s infinite
507
+ end
508
+
509
+ class ModelSelector < Components::Base
510
+ def initialize(models: [], default_model: nil, **options)
511
+ # Provider filter pills, model list, thinking level pills
512
+ end
513
+
514
+ class ConfirmationBar < Components::Base
515
+ def initialize(message:, confirm_label: "Cancel", cancel_label: "Keep Going",
516
+ auto_hide: 5, **options)
517
+ # Fixed top bar, slide-down, auto-hide timer
518
+ end
519
+
520
+ class CloseOverlay < Components::Base
521
+ def initialize(status:, message:, **options)
522
+ # Full-screen blur backdrop, color-coded, auto-close tab
523
+ end
524
+
525
+ class LayoutToggle < Components::Base
526
+ def initialize(**options)
527
+ # Footer buttons: 1 / 2 / 3 / 4 columns
528
+ end
529
+ ```
530
+
531
+ ---
532
+
533
+ ## 5. Visual Explainer Architecture
534
+
535
+ The visual explainer is simpler than the deck architecturally. It is a **page generator** -- the agent writes a Ruby script that uses shared components, and either:
536
+ 1. Serves it live via StreamWeaver (for interactive viewing), or
537
+ 2. Exports it as self-contained HTML (for sharing/archiving)
538
+
539
+ There is no special orchestrator class like `DesignDeck`. The agent simply composes components in an `app` block.
540
+
541
+ ### 5.1 VeCard
542
+
543
+ ```ruby
544
+ # DSL usage
545
+ ve_card depth: :hero, accent: :a do
546
+ header2 "Executive Summary"
547
+ text "The changes introduce..."
548
+ end
549
+
550
+ ve_card depth: :recessed, label: "RISK" do
551
+ text "Cognitive complexity exceeds threshold"
552
+ end
553
+
554
+ # Class
555
+ class Components::VeCard < Components::Base
556
+ attr_reader :depth, :accent, :label
557
+ attr_accessor :children
558
+
559
+ def initialize(depth: :default, accent: nil, label: nil, **options)
560
+ def render(view, state)
561
+ end
562
+ ```
563
+
564
+ Depth tiers map to CSS classes:
565
+ - `:hero` -- `sw-card--hero` (accent-tinted bg, elevated shadow)
566
+ - `:elevated` -- `sw-card--elevated` (subtle shadow)
567
+ - `:default` -- `sw-card--default` (flat, border)
568
+ - `:recessed` -- `sw-card--recessed` (inset shadow)
569
+ - `:glass` -- `sw-card--glass` (transparent, backdrop blur)
570
+
571
+ **Design Decision:** `VeCard` is a separate class from `Card`, not a variant. The existing `Card` has header/body/footer sub-components with specific rendering. `VeCard` is a simpler depth-styled container. Attempting to merge them creates complexity with minimal benefit.
572
+
573
+ ### 5.2 KpiDashboard
574
+
575
+ ```ruby
576
+ # DSL usage
577
+ kpi_dashboard metrics: [
578
+ { value: "3,500", label: "Requests/sec", color: :blue, trend: :up },
579
+ { value: "12ms", label: "P99 Latency", color: :green },
580
+ { value: "99.97%", label: "Uptime", color: :green, trend: :flat }
581
+ ]
582
+
583
+ # Class
584
+ class Components::KpiDashboard < Components::Base
585
+ attr_reader :metrics
586
+
587
+ def initialize(metrics:, **options)
588
+ def render(view, state)
589
+ # Renders auto-fit grid of KPI cards
590
+ # Each card: large value, label, optional trend arrow, fadeScale entry animation
591
+ end
592
+ ```
593
+
594
+ ### 5.3 DataTable
595
+
596
+ ```ruby
597
+ # DSL usage
598
+ data_table headers: ["File", "Lines", "Status"],
599
+ rows: [
600
+ ["app.rb", "42", { status: :match, label: "Match" }],
601
+ ["config.rb", "18", { status: :gap, label: "Missing" }]
602
+ ],
603
+ sticky_header: true
604
+
605
+ # Class -- enhances existing Components::Table
606
+ class Components::DataTable < Components::Base
607
+ attr_reader :headers, :rows, :sticky_header, :alternating, :hover, :scrollable
608
+
609
+ def initialize(headers:, rows:, sticky_header: true, alternating: true,
610
+ hover: true, scrollable: true, **options)
611
+ def render(view, state)
612
+ end
613
+ ```
614
+
615
+ **Design Decision:** New class rather than modifying existing `Table`. The existing `Table` accepts data in multiple formats (positional, headers+rows, file, path) with block support. `DataTable` has a focused API for the visual explainer's specific needs (status badges, sticky headers, scroll container). They can share CSS but the classes serve different use cases.
616
+
617
+ ### 5.4 SidebarToc
618
+
619
+ ```ruby
620
+ # DSL usage
621
+ sidebar_toc sections: [
622
+ { id: "summary", label: "Executive Summary" },
623
+ { id: "architecture", label: "Architecture" },
624
+ { id: "risks", label: "Risks" }
625
+ ]
626
+
627
+ # Class
628
+ class Components::SidebarToc < Components::Base
629
+ attr_reader :sections
630
+
631
+ def initialize(sections:, **options)
632
+ def render(view, state)
633
+ # Desktop: sticky 170px sidebar, IntersectionObserver scroll spy
634
+ # Mobile (<1000px): horizontal scrollable sticky bar
635
+ end
636
+ ```
637
+
638
+ ### 5.5 Other Explainer Components
639
+
640
+ ```ruby
641
+ # Comparison panels
642
+ comparison before_label: "Current", after_label: "Proposed" do
643
+ before { mermaid "graph TD\n A-->B" }
644
+ after { mermaid "graph TD\n A-->B\n B-->C" }
645
+ end
646
+
647
+ class Components::Comparison < Components::Base
648
+ attr_accessor :before_content, :after_content, :before_label, :after_label
649
+ def initialize(before_label: "Before", after_label: "After", **options)
650
+ def render(view, state)
651
+ end
652
+
653
+ # Pipeline
654
+ pipeline steps: [
655
+ { label: "Parse", description: "Read input", status: :complete },
656
+ { label: "Transform", description: "Apply rules", status: :active },
657
+ { label: "Emit", description: "Write output", status: :pending }
658
+ ]
659
+
660
+ class Components::Pipeline < Components::Base
661
+ attr_reader :steps
662
+ def initialize(steps:, **options)
663
+ def render(view, state)
664
+ # Horizontal flow with arrow connectors, responsive vertical fallback
665
+ end
666
+
667
+ # Chart (Chart.js)
668
+ chart type: :bar, data: { labels: [...], datasets: [...] }, height: 300
669
+
670
+ class Components::Chart < Components::Base
671
+ attr_reader :type, :data, :chart_options, :height
672
+ def initialize(type:, data:, options: {}, height: 300, **extra)
673
+ def render(view, state)
674
+ def cdn_assets # => [:chartjs]
675
+ end
676
+
677
+ # Callout
678
+ callout variant: :warning, title: "Breaking Change" do
679
+ text "This removes the deprecated API endpoint."
680
+ end
681
+
682
+ class Components::Callout < Components::Base
683
+ attr_reader :variant, :title
684
+ attr_accessor :children
685
+ def initialize(variant: :info, title: nil, **options)
686
+ def render(view, state)
687
+ # Colored left border: info=blue, warning=amber, success=green, tip=purple
688
+ end
689
+
690
+ # Prose
691
+ prose width: :narrow, dropcap: true do
692
+ md "Long-form content with **markdown** support..."
693
+ end
694
+
695
+ class Components::Prose < Components::Base
696
+ attr_reader :width, :dropcap
697
+ attr_accessor :children
698
+ def initialize(width: :default, dropcap: false, **options)
699
+ def render(view, state)
700
+ end
701
+
702
+ # Pullquote
703
+ pullquote "Design is not just what it looks like. Design is how it works.",
704
+ attribution: "Steve Jobs"
705
+
706
+ class Components::Pullquote < Components::Base
707
+ attr_reader :text, :attribution, :centered
708
+ def initialize(text, attribution: nil, centered: false, **options)
709
+ def render(view, state)
710
+ end
711
+
712
+ # Hero Section
713
+ hero_section variant: :centered do
714
+ header1 "Diff Review: feature/auth"
715
+ text "main...feature/auth -- 14 files changed"
716
+ end
717
+
718
+ class Components::HeroSection < Components::Base
719
+ attr_reader :variant
720
+ attr_accessor :children
721
+ def initialize(variant: :centered, **options)
722
+ def render(view, state)
723
+ end
724
+
725
+ # Directory Tree
726
+ dir_tree <<~TREE
727
+ src/
728
+ app.rb [modified]
729
+ config.rb [new]
730
+ routes/
731
+ api.rb [modified]
732
+ TREE
733
+
734
+ class Components::DirTree < Components::Base
735
+ attr_reader :tree
736
+ def initialize(tree, **options)
737
+ def render(view, state)
738
+ # Monospace, color-coded: green=new, amber=modified, red=deleted
739
+ end
740
+
741
+ # Legend
742
+ legend items: [
743
+ { color: "#22c55e", label: "New" },
744
+ { color: "#eab308", label: "Modified" },
745
+ { color: "#ef4444", label: "Deleted" }
746
+ ]
747
+
748
+ class Components::Legend < Components::Base
749
+ attr_reader :items
750
+ def initialize(items:, **options)
751
+ def render(view, state)
752
+ end
753
+
754
+ # Flow Arrow
755
+ flow_arrow label: "transforms into"
756
+
757
+ class Components::FlowArrow < Components::Base
758
+ attr_reader :label
759
+ def initialize(label: nil, **options)
760
+ def render(view, state)
761
+ end
762
+ ```
763
+
764
+ ### 5.6 Theme Presets
765
+
766
+ ```ruby
767
+ # DSL usage (at app level)
768
+ app "Diff Review", theme: :dark do
769
+ theme_preset :editorial # Sets fonts, colors, surface styles
770
+ # ... page content
771
+ end
772
+
773
+ # Infrastructure
774
+ module StreamWeaver::Theme::Presets
775
+ PRESETS = {
776
+ editorial: {
777
+ fonts: { display: "Instrument Serif", body: "Source Sans 3", mono: "JetBrains Mono" },
778
+ palette: { accent: "#c2825a", accent_dim: "rgba(194,130,90,0.15)",
779
+ surface: "#1a1917", text: "#e8e4dc" }
780
+ },
781
+ technical: {
782
+ fonts: { display: "DM Sans", body: "DM Sans", mono: "Fira Code" },
783
+ palette: { accent: "#14b8a6", accent_dim: "rgba(20,184,166,0.15)",
784
+ surface: "#0f172a", text: "#e2e8f0" }
785
+ },
786
+ warm: { ... },
787
+ minimal: { ... },
788
+ terminal: { ... },
789
+ # Slide-specific:
790
+ midnight_editorial: { ... },
791
+ warm_signal: { ... },
792
+ terminal_mono: { ... },
793
+ swiss_clean: { ... }
794
+ }.freeze
795
+
796
+ def self.apply(name, app)
797
+ preset = PRESETS.fetch(name)
798
+ # Registers Google Fonts CDN links
799
+ # Sets CSS custom properties via theme_overrides
800
+ end
801
+ end
802
+ ```
803
+
804
+ ---
805
+
806
+ ## 6. Agent Communication Protocol
807
+
808
+ ### 6.1 Primary Protocol: Agent Writes a Ruby Script
809
+
810
+ The agent generates a Ruby file, runs it, and StreamWeaver serves the result.
811
+
812
+ ```
813
+ Agent StreamWeaver Browser
814
+ | | |
815
+ |-- writes deck.rb ------->| |
816
+ |-- runs: ruby deck.rb --->| |
817
+ | |-- starts Puma server ---->|
818
+ | |-- opens browser --------->|
819
+ | | |-- user views deck
820
+ | | |
821
+ | |<-- SSE /stream -----------|
822
+ | | |
823
+ | |<-- POST /action/submit ---| (user submits)
824
+ | |-- result -> stdout ------>|
825
+ |<-- reads stdout ---------| |
826
+ ```
827
+
828
+ For **design deck** (bidirectional): use `run_once!` which blocks until the user submits, then returns the result as JSON to stdout.
829
+
830
+ For **visual explainer** (unidirectional): use `run!` for live serving, or `HtmlExporter.export` for static file output.
831
+
832
+ ### 6.2 Agent Receives Deck Results
833
+
834
+ The deck uses StreamWeaver's existing `run_once!` pattern:
835
+
836
+ ```ruby
837
+ # Agent generates and runs this:
838
+ result = app("Architecture Direction") {
839
+ design_deck "Architecture Direction" do
840
+ slide "arch", "System Architecture" do
841
+ option "Monolith" do ... end
842
+ option "Microservices" do ... end
843
+ end
844
+ end
845
+ }.run_once!(auto_close_window: true)
846
+
847
+ # result => { deck_selections: { "arch" => "Microservices" }, deck_notes: { ... } }
848
+ ```
849
+
850
+ The `run_once!` method already exists and handles:
851
+ - Starting the server
852
+ - Opening the browser
853
+ - Blocking until form submission
854
+ - Returning state as JSON
855
+ - Shutting down
856
+
857
+ The deck's "Submit" button on the summary slide sets `state[:_result]`, which triggers `run_once!` to unblock and return.
858
+
859
+ ### 6.3 Agent Pushes Generate-More Options (Push-to-State)
860
+
861
+ The agent pushes new options into server-side state via `POST /deck/add_option`. StreamWeaver's reactive re-render handles display -- no direct DOM manipulation via SSE.
862
+
863
+ ```ruby
864
+ # Agent script (simplified):
865
+ app_instance = app("My Deck") {
866
+ design_deck "My Deck" do
867
+ slide "arch", "Architecture" do
868
+ option "Monolith" do ... end
869
+ end
870
+ end
871
+ }
872
+
873
+ # The agent polls for generate requests, then pushes options to state:
874
+ # 1. User clicks "Generate" -> browser POSTs to /deck/generate
875
+ # 2. Server queues the request, updates state to :generating (re-render shows skeletons)
876
+ # 3. Agent polls GET /deck/pending, receives the request
877
+ # 4. Agent generates new options with LLM
878
+ # 5. Agent pushes each option to state via POST /deck/add_option
879
+ # 6. Server updates state hash, triggers SSE re-render notification
880
+ # 7. Browser re-renders -- new option appears, skeleton count decreases,
881
+ # summary slide automatically reflects the new option
882
+
883
+ # See Section 7 for the full state machine and endpoint details.
884
+ ```
885
+
886
+ ### 6.4 Visual Explainer Output
887
+
888
+ For the explainer, the agent either:
889
+
890
+ **Option A: Live server** (interactive viewing with scroll spy, theme toggle):
891
+ ```ruby
892
+ app "Diff Review: feature/auth", theme: :dark do
893
+ theme_preset :editorial
894
+ sidebar_toc sections: [...]
895
+ hero_section { header1 "Diff Review" }
896
+ ve_card(depth: :hero) { ... }
897
+ # ...
898
+ end
899
+ # App.run! starts server and opens browser
900
+ ```
901
+
902
+ **Option B: Static HTML export** (for sharing, archiving):
903
+ ```ruby
904
+ my_app = app "Diff Review: feature/auth", theme: :dark do
905
+ # ... same DSL
906
+ end
907
+ StreamWeaver::Export::HtmlExporter.export(my_app, path: "~/.agent/diagrams/review.html")
908
+ system("open", "~/.agent/diagrams/review.html")
909
+ ```
910
+
911
+ ### 6.5 Agent Integration (No Skill Classes in Gem)
912
+
913
+ **The DSL IS the API.** Skill entry point classes (`DesignDeckSkill`, `VisualExplainerSkill`) are intentionally excluded from the gem. The agent communicates with StreamWeaver by writing Ruby DSL scripts directly -- no wrapper classes needed.
914
+
915
+ Agent glue code (slash commands like `/design-deck`, `/diff-review`, etc.) lives outside the gem as **Claude Code custom commands** in the project's `.claude/commands/` directory. These commands handle data gathering (git diffs, file reading, codebase analysis) and then emit DSL scripts that StreamWeaver renders.
916
+
917
+ This separation keeps the gem focused on rendering and keeps agent-specific orchestration where it belongs -- in the agent's command layer. See Q9 in Section 12 for the rationale.
918
+
919
+ ---
920
+
921
+ ## 7. The Generate-More Loop (Push-to-State Architecture)
922
+
923
+ This is the most architecturally complex feature. The key architectural decision is **push-to-state, not push-to-DOM**: the agent pushes new options into server-side state, and StreamWeaver's reactive re-render handles display. This eliminates the "phantom option" race condition (user on Slide B when options arrive for Slide A), keeps the summary slide in sync automatically, and aligns with StreamWeaver's existing reactive model.
924
+
925
+ ### 7.1 State Machine
926
+
927
+ ```
928
+ IDLE
929
+ |
930
+ [user clicks Generate]
931
+ |
932
+ v
933
+ GENERATING
934
+ / \
935
+ [agent pushes [timeout 30s]
936
+ option to state] |
937
+ | v
938
+ v TIMED_OUT
939
+ state updated |
940
+ re-render fires [show toast]
941
+ | |
942
+ [all received] v
943
+ | IDLE
944
+ v
945
+ IDLE
946
+ ```
947
+
948
+ The critical difference from push-to-DOM: options are added to the state hash, which triggers a reactive re-render of the current view. The browser never receives raw HTML snippets via SSE -- it receives state-change notifications that cause the existing component tree to re-render.
949
+
950
+ ### 7.2 State Storage
951
+
952
+ ```ruby
953
+ # In app state hash:
954
+ state[:deck_generate] = {
955
+ status: :idle, # :idle | :generating | :timed_out
956
+ slide_id: nil, # Which slide is generating
957
+ requested_count: 0, # How many options requested
958
+ received_count: 0, # How many received so far
959
+ prompt: nil, # User's custom prompt
960
+ started_at: nil # For timeout tracking
961
+ }
962
+
963
+ # Generated options live in the slide's options array (NOT a separate SSE target):
964
+ state[:deck_slides]["arch"][:options] << {
965
+ label: "Event-Driven",
966
+ aside: "Decoupled via message bus",
967
+ children: [{ type: :mermaid, code: "graph LR\n Events-->Handler", compact: true }],
968
+ generated: true # Flag to distinguish from original options
969
+ }
970
+ # Adding to state triggers re-render -- DeckSlide component picks up new options
971
+ # automatically, and DeckSummary reflects them because it reads the same state.
972
+ ```
973
+
974
+ ### 7.3 Data Flow (Push-to-State)
975
+
976
+ ```
977
+ Browser Server (Puma) Agent Script
978
+ | | |
979
+ |--[click Generate]--------->| |
980
+ | POST /deck/generate | |
981
+ | {slide_id, count, prompt} | |
982
+ | |--[queue request]-------->|
983
+ | | |
984
+ |<-[update generate status]--| |
985
+ | state[:deck_generate] | |
986
+ | status: :generating | |
987
+ | (re-render shows | |
988
+ | skeleton placeholders) | |
989
+ | | |
990
+ | |<---[poll for requests]---|
991
+ | | GET /deck/pending |
992
+ | | |
993
+ | | [agent generates
994
+ | | option with LLM]
995
+ | | |
996
+ | |<--[push option to state]-|
997
+ | | POST /deck/add_option |
998
+ | | {slide_id, option_data}|
999
+ | | |
1000
+ |<-[SSE: state changed]-----| |
1001
+ | re-render picks up new | |
1002
+ | option from state hash | |
1003
+ | skeleton count decreases | |
1004
+ | summary slide updates | |
1005
+ | | |
1006
+ | [repeat for each option] | |
1007
+ ```
1008
+
1009
+ ### 7.4 Server-Side Endpoints (New Routes)
1010
+
1011
+ ```ruby
1012
+ # In the SinatraApp (or as a Sinatra extension):
1013
+
1014
+ # User requests more options
1015
+ post '/deck/generate' do
1016
+ slide_id = params[:slide_id]
1017
+ count = params[:count].to_i
1018
+ prompt = params[:prompt]
1019
+ session_id = session[:session_id]
1020
+
1021
+ # Update generate status in state (triggers re-render with skeletons)
1022
+ state = session[:streamlit_state] ||= {}
1023
+ state[:deck_generate] = {
1024
+ status: :generating,
1025
+ slide_id: slide_id,
1026
+ requested_count: count,
1027
+ received_count: 0,
1028
+ prompt: prompt,
1029
+ started_at: Time.now.to_i
1030
+ }
1031
+ session[:streamlit_state] = state
1032
+
1033
+ # Store in session-scoped thread-safe queue for agent polling.
1034
+ # Queue entries include session_id so stale requests from killed
1035
+ # agent processes don't persist across sessions.
1036
+ settings.generate_requests << {
1037
+ session_id: session_id,
1038
+ slide_id: slide_id,
1039
+ count: count,
1040
+ prompt: prompt,
1041
+ timestamp: Time.now
1042
+ }
1043
+
1044
+ status 202
1045
+ { status: "generating", skeletons: count }.to_json
1046
+ end
1047
+
1048
+ # Agent polls for pending generate requests
1049
+ get '/deck/pending' do
1050
+ content_type :json
1051
+ session_id = params[:session_id]
1052
+ requests = settings.generate_requests
1053
+ # Only return requests matching this session; discard stale ones
1054
+ pending = requests.select { |r| r[:session_id] == session_id }
1055
+ requests.reject! { |r| r[:session_id] == session_id }
1056
+ { requests: pending }.to_json
1057
+ end
1058
+
1059
+ # Agent pushes a generated option into state
1060
+ post '/deck/add_option' do
1061
+ slide_id = params[:slide_id]
1062
+ option_data = JSON.parse(request.body.read, symbolize_names: true)
1063
+
1064
+ state = session[:streamlit_state] ||= {}
1065
+ state[:deck_slides] ||= {}
1066
+ state[:deck_slides][slide_id] ||= { options: [] }
1067
+ state[:deck_slides][slide_id][:options] << option_data.merge(generated: true)
1068
+
1069
+ # Update received count; reset status if all received
1070
+ gen = state[:deck_generate]
1071
+ if gen && gen[:slide_id] == slide_id
1072
+ gen[:received_count] += 1
1073
+ gen[:status] = :idle if gen[:received_count] >= gen[:requested_count]
1074
+ end
1075
+
1076
+ session[:streamlit_state] = state
1077
+
1078
+ # Trigger SSE re-render notification to connected browsers
1079
+ settings.streamer&.notify_state_change
1080
+
1081
+ status 200
1082
+ { received: gen&.dig(:received_count) }.to_json
1083
+ end
1084
+ ```
1085
+
1086
+ **Session-scoped queue cleanup:** When a session expires or a heartbeat timeout fires (see 7.7), all queue entries with that `session_id` are purged. This prevents stale requests from killed agent processes from accumulating.
1087
+
1088
+ ### 7.5 Agent-Side Polling
1089
+
1090
+ ```ruby
1091
+ # The agent's deck script includes a generate-more handler:
1092
+ Thread.new do
1093
+ loop do
1094
+ response = Net::HTTP.get(URI("#{url}/deck/pending?session_id=#{session_id}"))
1095
+ requests = JSON.parse(response)["requests"]
1096
+
1097
+ requests.each do |req|
1098
+ # Agent calls LLM to generate options
1099
+ new_options = generate_options(req["slide_id"], req["count"], req["prompt"])
1100
+
1101
+ # Push each option to server-side state (NOT directly to DOM)
1102
+ new_options.each do |opt|
1103
+ uri = URI("#{url}/deck/add_option")
1104
+ Net::HTTP.post(uri, {
1105
+ slide_id: req["slide_id"],
1106
+ label: opt[:label],
1107
+ aside: opt[:aside],
1108
+ children: opt[:children] # Serialized component descriptions
1109
+ }.to_json, "Content-Type" => "application/json")
1110
+ end
1111
+ end
1112
+
1113
+ sleep 1
1114
+ end
1115
+ end
1116
+ ```
1117
+
1118
+ ### 7.6 Timeout Handling
1119
+
1120
+ Client-side: the generating state drives skeleton display. If `state[:deck_generate][:status]` remains `:generating` for 30 seconds (checked via the `started_at` timestamp), the client triggers a state update to `:timed_out`, shows a toast, and restores the Generate button. Server-side: the `started_at` timestamp enables the agent to also detect stale requests and skip them.
1121
+
1122
+ ### 7.7 Cancellation and Heartbeat
1123
+
1124
+ **Tab-close / beforeunload:** The browser sends a `beforeunload` beacon to `POST /deck/disconnect`. However, `beforeunload` is unreliable. The primary mechanism is a heartbeat.
1125
+
1126
+ **Heartbeat mechanism:** The browser pings `POST /heartbeat` every 5 seconds. If the server detects no heartbeat for 10 seconds (2x interval), it assumes the browser disconnected and:
1127
+ 1. Sets `state[:deck_generate][:status] = :idle`
1128
+ 2. Purges pending generate requests for that session from the queue
1129
+ 3. Marks the session as stale for cleanup
1130
+
1131
+ **Timeout:** `run_once!` accepts an optional `timeout:` parameter (default: `nil` = no timeout). When set, the server will auto-return a timeout result after the specified duration:
1132
+
1133
+ ```ruby
1134
+ result = app("My Deck") { ... }.run_once!(timeout: 300) # 5 minute timeout
1135
+ # result => { _timeout: true } if timed out
1136
+ ```
1137
+
1138
+ **Agent process crash:** If the agent process dies, it stops polling `/deck/pending`. The heartbeat mechanism on the browser side continues. Pending generate requests accumulate but are cleaned up when the session expires or when a new agent connects with a fresh session.
1139
+
1140
+ ---
1141
+
1142
+ ## 7A. State Ownership Table
1143
+
1144
+ This table documents what state lives where and how it stays in sync across client and server.
1145
+
1146
+ | State | Location | Sync Mechanism |
1147
+ |-------|----------|----------------|
1148
+ | Deck selections | Server (file-backed) | POST on select |
1149
+ | Deck notes | Server (file-backed) | POST on blur/change |
1150
+ | Current slide index | Client (Alpine.js) | URL hash + x-data |
1151
+ | Generate status | Server state hash | SSE push on change |
1152
+ | Theme preference | Client (localStorage) | Read on load |
1153
+ | Keyboard shortcuts | Client (JS) | Static registration |
1154
+
1155
+ **File-backed state:** Deck selections and notes use file-backed server state (option B from Q1). Codex's argument about cookie overflow with user-generated notes was decisive -- a single long note can exceed the 4KB cookie limit. The session cookie stores a session ID; the full state is persisted to a JSON file in the session directory.
1156
+
1157
+ ---
1158
+
1159
+ ## 8. Data Flow Diagrams
1160
+
1161
+ ### 8.1 Design Deck: Agent Creates Deck, User Selects, Agent Gets Results
1162
+
1163
+ ```
1164
+ Agent StreamWeaver Browser User
1165
+ | | | |
1166
+ | write deck.rb | | |
1167
+ | ruby deck.rb | | |
1168
+ | (blocks on run_once!) | | |
1169
+ | | Puma starts | |
1170
+ | | open browser -------->| |
1171
+ | | | render slide 1 |
1172
+ | | |<---- views -------|
1173
+ | | | |
1174
+ | | |---- click opt --->|
1175
+ | |<-- Alpine x-model -----| (client-side) |
1176
+ | | | |
1177
+ | | |---- Next -------->|
1178
+ | | | render slide 2 |
1179
+ | | | |
1180
+ | | |---- submit ------>|
1181
+ | |<-- POST /action/submit | |
1182
+ | | | |
1183
+ | | state[:_result] set | |
1184
+ | run_once! unblocks | | |
1185
+ |<-- JSON result -----------| | |
1186
+ | | server shuts down | |
1187
+ | | | window closes |
1188
+ | reads selections | | |
1189
+ ```
1190
+
1191
+ ### 8.2 Generate-More: User Requests, Agent Generates, State Updates (Push-to-State)
1192
+
1193
+ ```
1194
+ User Browser Server Agent
1195
+ | | | |
1196
+ | click Gen | | |
1197
+ |-------------->| POST /deck/gen | |
1198
+ | | {slide, count} | |
1199
+ | |------------------->| |
1200
+ | | | queue request |
1201
+ | |<-- state change ---| |
1202
+ | sees shimmer | re-render shows | |
1203
+ | | skeleton cards | |
1204
+ | | |<-- GET /pending ---| (poll)
1205
+ | | |--- {requests} ---->|
1206
+ | | | |
1207
+ | | | [LLM generates]
1208
+ | | | |
1209
+ | | |<- POST /add_opt ---|
1210
+ | | | {slide, option} |
1211
+ | | | update state hash |
1212
+ | |<-- SSE: re-render -| |
1213
+ | sees option | state has new opt | |
1214
+ | | component re-renders |
1215
+ | | summary also updates |
1216
+ ```
1217
+
1218
+ ### 8.3 Visual Explainer: Agent Generates Page, User Views
1219
+
1220
+ ```
1221
+ Agent StreamWeaver Browser
1222
+ | | |
1223
+ | write page.rb | |
1224
+ | ruby page.rb | |
1225
+ | | Puma starts |
1226
+ | | open browser --------->|
1227
+ | | | render full page
1228
+ | | | scroll spy active
1229
+ | | | theme toggle works
1230
+ | | |
1231
+ | (agent done, server | |
1232
+ | keeps running for user) | |
1233
+
1234
+ --- OR (static export) ---
1235
+
1236
+ | write page.rb | |
1237
+ | ruby page.rb | |
1238
+ | | HtmlExporter.export |
1239
+ | | writes review.html |
1240
+ | open review.html -------->| |
1241
+ | | | render from file
1242
+ ```
1243
+
1244
+ ---
1245
+
1246
+ ## 9. CSS/JS Asset Strategy
1247
+
1248
+ ### 9.1 External Libraries (CDN)
1249
+
1250
+ | Library | CDN URL | Used By |
1251
+ |---------|---------|---------|
1252
+ | Mermaid.js 11 | `cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs` | Mermaid |
1253
+ | Mermaid ELK | `cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs` | Mermaid (ELK layout) |
1254
+ | Prism.js | `cdn.jsdelivr.net/npm/prismjs@1/prism.min.js` + autoloader | CodeBlock |
1255
+ | Chart.js 4 | `cdn.jsdelivr.net/npm/chart.js@4` | Chart |
1256
+ | Google Fonts | `fonts.googleapis.com` | Theme presets |
1257
+
1258
+ **Decision: CDN, not vendored.** Rationale:
1259
+ 1. StreamWeaver is a local dev tool, not a production web app -- CDN latency is acceptable
1260
+ 2. Self-contained HTML export preserves CDN links (they work offline if cached)
1261
+ 3. Vendoring would bloat the gem significantly
1262
+ 4. The existing AlpineJS + HTMX are already loaded from CDN
1263
+
1264
+ ### 9.2 Custom JS Assets
1265
+
1266
+ StreamWeaver needs to serve custom JS files for:
1267
+ - **Mermaid zoom engine** (~200 lines) -- zoom, pan, expand controls
1268
+ - **Keyboard shortcuts** -- centralized registration with context awareness
1269
+ - **Slide navigation** -- swap mode + scroll-snap mode logic
1270
+ - **Deck selection** -- radio behavior, number key selection
1271
+ - **Generate-more client** -- SSE listener, skeleton replacement
1272
+
1273
+ These are served from `lib/stream_weaver/assets/js/` via a new Sinatra route:
1274
+
1275
+ ```ruby
1276
+ get '/assets/js/:filename' do
1277
+ file = File.join(ASSETS_DIR, "js", params[:filename])
1278
+ halt 404 unless File.exist?(file)
1279
+ content_type 'application/javascript'
1280
+ File.read(file)
1281
+ end
1282
+ ```
1283
+
1284
+ ### 9.3 CSS Strategy
1285
+
1286
+ **Naming Convention:** All visual skills CSS classes MUST use the `sw-` prefix to prevent conflicts with user styles. Examples: `sw-mermaid-wrap`, `sw-code-block`, `sw-deck-option`, `sw-card--hero`. BEM-style modifiers use double-dash: `sw-component--variant`. This is already visible in the component examples throughout this document (e.g., `sw-mermaid--compact`, `sw-mermaid--zoom`, `sw-code-block`, `sw-card--elevated`).
1287
+
1288
+ StreamWeaver already has a CSS module (`StreamWeaver::CSS`) that generates the full stylesheet. New visual skills CSS is added as additional stylesheet sections:
1289
+
1290
+ ```ruby
1291
+ module StreamWeaver::CSS
1292
+ def self.visual_skills_css
1293
+ # Returns CSS for: mermaid containers, code blocks, depth tiers,
1294
+ # deck layouts, slide transitions, explainer typography, etc.
1295
+ end
1296
+
1297
+ def self.full_stylesheet
1298
+ # Existing call -- augmented to include visual_skills_css
1299
+ end
1300
+ end
1301
+ ```
1302
+
1303
+ For theme presets, CSS custom properties are injected via `<style>` blocks in the page head:
1304
+
1305
+ ```css
1306
+ :root {
1307
+ --sw-vs-font-display: 'Instrument Serif', serif;
1308
+ --sw-vs-font-body: 'Source Sans 3', sans-serif;
1309
+ --sw-vs-font-mono: 'JetBrains Mono', monospace;
1310
+ --sw-vs-accent: #c2825a;
1311
+ --sw-vs-surface-hero: rgba(194, 130, 90, 0.08);
1312
+ /* ... */
1313
+ }
1314
+ ```
1315
+
1316
+ ### 9.4 CDN Asset Declaration
1317
+
1318
+ Components declare which CDN assets they need via a `cdn_assets` method. The view collects all required assets and deduplicates:
1319
+
1320
+ ```ruby
1321
+ class Components::Mermaid < Components::Base
1322
+ def cdn_assets
1323
+ assets = [:mermaid]
1324
+ assets << :mermaid_elk if @layout == :elk
1325
+ assets
1326
+ end
1327
+ end
1328
+
1329
+ # View (Phlex) collects:
1330
+ def head_scripts
1331
+ required = collect_cdn_assets(@components).uniq
1332
+ required.each { |asset| render_cdn_script(asset) }
1333
+ end
1334
+ ```
1335
+
1336
+ ---
1337
+
1338
+ ## 9A. Accessibility
1339
+
1340
+ Visual skills components must be usable with assistive technologies and keyboard-only navigation.
1341
+
1342
+ ### ARIA Attributes
1343
+
1344
+ | Context | Attribute | Purpose |
1345
+ |---------|-----------|---------|
1346
+ | Generate-more region | `aria-live="polite"` | New options announced to screen readers without interrupting |
1347
+ | Active slide in nav | `aria-current="step"` | Identifies the current slide in navigation controls |
1348
+ | During generation | `aria-busy="true"` | Signals that content is loading/updating |
1349
+ | Option cards grid | `role="radiogroup"` | Groups option cards as a radio selection |
1350
+ | Individual option card | `role="radio"` + `aria-checked` | Each option is a selectable radio item |
1351
+
1352
+ ### Focus Management
1353
+
1354
+ - **Slide navigation:** Focus is trapped within the active slide. When navigating to the next/previous slide, focus moves to the slide container.
1355
+ - **Generate-more:** When new options appear (via state re-render), focus remains on the current element. Screen readers announce new options via `aria-live`.
1356
+ - **Modal overlays** (CloseOverlay, ConfirmationBar): Focus is trapped within the overlay and restored to the triggering element on dismiss.
1357
+ - **Keyboard navigation:** Covered by the KeyboardShortcuts component (Section 3.4). All interactive elements are reachable via Tab. Number keys for quick-select are a progressive enhancement, not a replacement for Tab+Enter.
1358
+
1359
+ ---
1360
+
1361
+ ## 9B. Scalability Limits
1362
+
1363
+ Soft limits trigger warnings in development mode. Hard limits are enforced and raise errors.
1364
+
1365
+ | Resource | Soft Limit | Hard Limit | Rationale |
1366
+ |----------|-----------|------------|-----------|
1367
+ | Slides per deck | 10 | 20 | DOM size degrades rendering performance |
1368
+ | Options per slide | 6 | 12 | Grid layout breaks down; cognitive overload |
1369
+ | Mermaid diagrams per page | 10 | 25 | Mermaid.js rendering time grows non-linearly |
1370
+ | Total file size for HTML export | 2 MB | 10 MB | Browser memory; email attachment limits |
1371
+ | Concurrent SSE connections | 1 | 1 | Puma single-worker limitation |
1372
+
1373
+ **Enforcement:** Soft limits log a warning via `StreamWeaver.logger.warn`. Hard limits raise `StreamWeaver::LimitExceededError` with a descriptive message. The `validate!` method on `DesignDeck` checks slide and option counts. The `HtmlExporter` checks file size. SSE connection count is enforced by the Streamer.
1374
+
1375
+ ---
1376
+
1377
+ ## 10. DSL Examples
1378
+
1379
+ ### 10.1 Design Deck -- Full Example
1380
+
1381
+ ```ruby
1382
+ app "UI Component Library", theme: :dark do
1383
+ design_deck "Component Library Direction" do
1384
+ slide "palette", "Color Palette",
1385
+ context: "Choose the color direction for the design system" do
1386
+ option "Warm Earth Tones", recommended: true,
1387
+ aside: "Terracotta, sage, warm gray.\nCalm and professional." do
1388
+ code_block <<~CSS, lang: "css"
1389
+ :root {
1390
+ --primary: #c2825a;
1391
+ --surface: #f5f0eb;
1392
+ }
1393
+ CSS
1394
+ end
1395
+
1396
+ option "Cool Ocean", aside: "Teal, slate, cool gray" do
1397
+ code_block <<~CSS, lang: "css"
1398
+ :root {
1399
+ --primary: #14b8a6;
1400
+ --surface: #f0f4f8;
1401
+ }
1402
+ CSS
1403
+ end
1404
+
1405
+ option "Monochrome", aside: "Pure black, white, and grays" do
1406
+ code_block <<~CSS, lang: "css"
1407
+ :root {
1408
+ --primary: #111111;
1409
+ --surface: #fafafa;
1410
+ }
1411
+ CSS
1412
+ end
1413
+ end
1414
+
1415
+ slide "layout", "Page Layout" do
1416
+ option "Sidebar Navigation" do
1417
+ mermaid <<~MERMAID, compact: true
1418
+ graph LR
1419
+ Sidebar-->Content
1420
+ Sidebar-->Footer
1421
+ MERMAID
1422
+ end
1423
+ option "Top Navigation" do
1424
+ mermaid <<~MERMAID, compact: true
1425
+ graph TD
1426
+ Navbar-->Content
1427
+ Content-->Footer
1428
+ MERMAID
1429
+ end
1430
+ end
1431
+ end
1432
+ end
1433
+ ```
1434
+
1435
+ ### 10.2 Visual Explainer -- Diff Review Page
1436
+
1437
+ ```ruby
1438
+ app "Diff Review: feature/auth", theme: :dark do
1439
+ theme_preset :editorial
1440
+
1441
+ sidebar_toc sections: [
1442
+ { id: "summary", label: "Executive Summary" },
1443
+ { id: "architecture", label: "Module Architecture" },
1444
+ { id: "review", label: "Code Review" },
1445
+ { id: "risks", label: "Risk Assessment" }
1446
+ ]
1447
+
1448
+ hero_section variant: :centered do
1449
+ header1 "Diff Review"
1450
+ text "main...feature/auth -- 14 files changed, +482 / -91"
1451
+ kpi_dashboard metrics: [
1452
+ { value: "14", label: "Files Changed" },
1453
+ { value: "+482", label: "Lines Added", color: :green },
1454
+ { value: "-91", label: "Lines Removed", color: :red }
1455
+ ]
1456
+ end
1457
+
1458
+ ve_card depth: :hero, id: "summary" do
1459
+ header2 "Executive Summary"
1460
+ prose do
1461
+ md <<~MD
1462
+ This PR introduces JWT-based authentication, replacing the
1463
+ session-cookie approach. The **primary motivation** is enabling
1464
+ stateless horizontal scaling of the API tier.
1465
+ MD
1466
+ end
1467
+ end
1468
+
1469
+ ve_card id: "architecture" do
1470
+ header2 "Module Architecture"
1471
+ mermaid <<~MERMAID, zoom: true
1472
+ graph TD
1473
+ AuthMiddleware-->JWTService
1474
+ JWTService-->TokenStore
1475
+ AuthMiddleware-->SessionFallback
1476
+ MERMAID
1477
+ legend items: [
1478
+ { color: "#22c55e", label: "New modules" },
1479
+ { color: "#eab308", label: "Modified" }
1480
+ ]
1481
+ end
1482
+
1483
+ ve_card id: "review" do
1484
+ header2 "Code Review"
1485
+
1486
+ callout variant: :success, title: "Good" do
1487
+ text "Clean separation of JWT logic into dedicated service class."
1488
+ end
1489
+
1490
+ callout variant: :error, title: "Needs Fix" do
1491
+ text "Token refresh endpoint missing rate limiting."
1492
+ code_block <<~RUBY, lang: "ruby", file: "app/controllers/tokens_controller.rb"
1493
+ def refresh
1494
+ # TODO: add rate limiting
1495
+ new_token = JWTService.refresh(current_token)
1496
+ render json: { token: new_token }
1497
+ end
1498
+ RUBY
1499
+ end
1500
+ end
1501
+
1502
+ ve_card depth: :elevated, id: "risks" do
1503
+ header2 "Risk Assessment"
1504
+ data_table headers: ["Risk", "Severity", "Mitigation"],
1505
+ rows: [
1506
+ ["Token leakage via logs", "High", "Add log filtering"],
1507
+ ["Clock skew on expiry", "Medium", "Use 30s grace period"]
1508
+ ]
1509
+ end
1510
+ end
1511
+ ```
1512
+
1513
+ ### 10.3 Visual Explainer -- Slide Deck
1514
+
1515
+ ```ruby
1516
+ app "API Gateway Redesign", theme: :dark do
1517
+ theme_preset :midnight_editorial
1518
+
1519
+ slide_container mode: :scroll_snap, nav_dots: true, counter: true do
1520
+ slide "title", type: :title do
1521
+ header1 "API Gateway Redesign"
1522
+ text "Q2 2026 Architecture Proposal"
1523
+ end
1524
+
1525
+ slide "problem", type: :split do
1526
+ comparison before_label: "Current", after_label: "Proposed" do
1527
+ before do
1528
+ mermaid "graph TD\n Client-->Monolith-->DB", compact: true
1529
+ end
1530
+ after do
1531
+ mermaid "graph TD\n Client-->Gateway-->Auth\n Gateway-->Orders", compact: true
1532
+ end
1533
+ end
1534
+ end
1535
+
1536
+ slide "metrics", type: :dashboard do
1537
+ kpi_dashboard metrics: [
1538
+ { value: "3x", label: "Throughput Increase" },
1539
+ { value: "50%", label: "Latency Reduction" },
1540
+ { value: "99.99%", label: "Target Uptime" }
1541
+ ]
1542
+ end
1543
+
1544
+ slide "timeline", type: :content do
1545
+ header2 "Implementation Timeline"
1546
+ pipeline steps: [
1547
+ { label: "Phase 1", description: "Gateway MVP", status: :complete },
1548
+ { label: "Phase 2", description: "Auth migration", status: :active },
1549
+ { label: "Phase 3", description: "Traffic cutover", status: :pending }
1550
+ ]
1551
+ end
1552
+ end
1553
+ end
1554
+ ```
1555
+
1556
+ ---
1557
+
1558
+ ## 11. Implementation Phases
1559
+
1560
+ Aligned with the analysis in `components.md`, ordered by dependency and value.
1561
+
1562
+ ### Phase 1: Shared Foundation (Highest Value, Unblocks Everything)
1563
+
1564
+ | # | Component | Effort | Unblocks |
1565
+ |---|-----------|--------|----------|
1566
+ | 1 | `Mermaid` component + zoom JS | Medium | Both projects |
1567
+ | 2 | `CodeBlock` component + Prism.js | Small | Both projects |
1568
+ | 3 | Theme enhancements (presets, auto mode, CSS vars) | Medium | Both projects |
1569
+ | 4 | `KeyboardShortcuts` system | Small | Deck navigation |
1570
+ | 5 | `HtmlExporter` pipeline | Medium | Explainer output |
1571
+ | 6 | CDN asset declaration + serving infrastructure | Small | All JS-dependent components |
1572
+ | 7 | `ImageBlock` component | Small | Deck previews |
1573
+
1574
+ ### Phase 2: Design Deck Core
1575
+
1576
+ | # | Component | Effort | Notes |
1577
+ |---|-----------|--------|-------|
1578
+ | 8 | `DesignDeck` + `DeckSlide` + `DeckOption` | Large | The deck shell |
1579
+ | 9 | `SlideContainer` (:swap mode) | Medium | Slide navigation |
1580
+ | 10 | `DeckSummary` auto-generated slide | Medium | Reads selection state |
1581
+ | 11 | Selection state + radio behavior (JS) | Medium | Client-side Alpine.js |
1582
+ | 12 | Deck-specific keyboard shortcuts | Small | Number keys, Enter, Escape |
1583
+ | 13 | `ProgressIndicator` | Small | Nav progress bar |
1584
+
1585
+ ### Phase 3: Generate-More Loop
1586
+
1587
+ | # | Component | Effort | Notes |
1588
+ |---|-----------|--------|-------|
1589
+ | 14 | `GenerateMoreControls` UI | Small | Button, input, dropdown |
1590
+ | 15 | `SkeletonPlaceholder` | Small | Shimmer CSS animation |
1591
+ | 16 | Generate request queue + `/deck/generate` + `/deck/pending` | Medium | Server routes |
1592
+ | 17 | SSE option push integration | Medium | Feed + Streamer |
1593
+ | 18 | Timeout handling (client + server) | Small | Toast on timeout |
1594
+ | 19 | `ConfirmationBar` + `CloseOverlay` | Small | UX polish |
1595
+
1596
+ ### Phase 4: Visual Explainer Core
1597
+
1598
+ | # | Component | Effort | Notes |
1599
+ |---|-----------|--------|-------|
1600
+ | 20 | `VeCard` with depth tiers | Small | CSS classes on a div |
1601
+ | 21 | `DataTable` enhanced table | Medium | Sticky header, badges |
1602
+ | 22 | `SidebarToc` with scroll spy | Medium | IntersectionObserver JS |
1603
+ | 23 | `KpiDashboard` | Small | Grid of stat cards |
1604
+ | 24 | `Callout` | Small | Colored border box |
1605
+ | 25 | `Comparison` panels | Small | Side-by-side layout |
1606
+ | 26 | `Pipeline` step flow | Small | CSS flexbox + arrows |
1607
+ | 27 | `Chart` (Chart.js) | Medium | CDN, dark mode, data binding |
1608
+
1609
+ ### Phase 5: Polish and Remaining
1610
+
1611
+ | # | Component | Effort | Notes |
1612
+ |---|-----------|--------|-------|
1613
+ | 28 | `SlideContainer` (:scroll_snap mode) | Medium | Explainer presentations |
1614
+ | 29 | CSS-only helpers (prose, pullquote, hero_section, dir_tree, legend, flow_arrow, layout_toggle) | Small each | Thin DSL methods + CSS |
1615
+ | 30 | `ModelSelector` | Medium | Model picker UI |
1616
+ | 31 | Save/Load snapshot system | Medium | File I/O, localStorage |
1617
+ | 32 | Animation choreography | Medium | Staggered reveals, CSS counters |
1618
+ | 33 | Theme preset library (all presets) | Medium | Fonts, colors, curated combos |
1619
+
1620
+ ---
1621
+
1622
+ ## 12. Open Questions / Design Decisions Needed
1623
+
1624
+ ### Q1: Deck State -- Server Session vs. Client-Side? **[RESOLVED: Option B]**
1625
+
1626
+ The existing StreamWeaver uses server-side session (cookies) for state. The design deck has a lot of state (selections, notes, dirty tracking). Cookie size limit is ~4KB.
1627
+
1628
+ **Options:**
1629
+ - **A) Server-side session** -- fits existing pattern, but may hit cookie size limits with many slides/notes
1630
+ - **B) Server-side with file-backed overflow** -- session stores an ID, full state in a file
1631
+ - **C) Client-side localStorage** -- the original pi-design-deck approach, but breaks the StreamWeaver pattern
1632
+
1633
+ **Decision: Option B (file-backed state).** Codex's argument about cookie overflow with user-generated notes was decisive -- a single detailed note on one slide can exceed the 4KB cookie limit. The session cookie stores only a session ID; full deck state (selections, notes, generate status) is persisted to a JSON file in a session-scoped directory. See Section 7A (State Ownership Table) for the complete state location map.
1634
+
1635
+ ### Q2: AlpineJS Adapter Extension vs. New Adapter Methods?
1636
+
1637
+ New components need rendering. Two approaches:
1638
+ - **A) Add `render_mermaid`, `render_code_block`, etc. to `Adapter::Base` and `Adapter::AlpineJS`** -- follows existing pattern exactly
1639
+ - **B) Components render themselves via Phlex, only delegate to adapter for interactive bits** -- less adapter bloat, but breaks the adapter abstraction
1640
+
1641
+ **Recommendation:** (A) for interactive components (those with Alpine.js state), (B) for pure display components (Mermaid, CodeBlock, VeCard just render HTML). This is already the implicit pattern: `Text`, `Header`, `Badge` all render in their own `render(view, state)` without adapter delegation. Only form controls delegate to the adapter.
1642
+
1643
+ ### Q3: Generate-More -- Polling vs. Callback?
1644
+
1645
+ How does the agent learn that the user clicked "Generate"?
1646
+
1647
+ **Options:**
1648
+ - **A) Agent polls `/deck/pending`** -- simple, works with any agent, but adds latency
1649
+ - **B) Agent provides a callback URL/block** -- lower latency, but requires the agent script to run a mini HTTP server or use a callback mechanism
1650
+ - **C) Agent monitors stdout** -- server prints generate requests to stdout, agent reads them
1651
+
1652
+ **Recommendation:** (A) polling. Simplicity wins. The agent script already runs in a loop (for `run_once!` it polls `result_container`). Adding a poll for generate requests is trivial. Latency of 1-2 seconds (poll interval) is acceptable for a "generate with LLM" operation that itself takes 5-15 seconds.
1653
+
1654
+ ### Q4: Should the Deck Be an App Subclass?
1655
+
1656
+ **Options:**
1657
+ - **A) DSL methods on App** (recommended above) -- `design_deck` is like `tabs` or `modal`
1658
+ - **B) `DesignDeckApp < App`** -- a specialized subclass with deck-specific routes
1659
+
1660
+ **Recommendation:** (A). The deck needs a few custom routes (`/deck/generate`, `/deck/pending`), but these can be added to the SinatraApp conditionally (only when the app contains a DesignDeck component). Making it a subclass creates a parallel hierarchy that complicates the codebase.
1661
+
1662
+ ### Q5: Shared Components in DisplayDSL vs. App Only?
1663
+
1664
+ Should `mermaid`, `code_block`, `chart` etc. be available in `DisplayDSL` (and thus in `FeedBuilder` for push updates) or only in `App`?
1665
+
1666
+ **Recommendation:** `DisplayDSL` for all display-only components. This means a `Feed` can push a mermaid diagram or code block into a running app, which is powerful for the generate-more loop. Only deck-specific interactive components (`design_deck`, `option`, etc.) belong on `App` alone.
1667
+
1668
+ ### Q6: How to Handle Mermaid Re-rendering on SSE Push?
1669
+
1670
+ When a new option with a Mermaid diagram is pushed via SSE, the `<pre class="mermaid">` element needs to be processed by Mermaid.js. But Mermaid has already initialized.
1671
+
1672
+ **Options:**
1673
+ - **A) Call `mermaid.run()` after each SSE DOM update** -- re-processes all unrendered diagrams
1674
+ - **B) Use MutationObserver** to auto-detect new mermaid elements
1675
+ - **C) Include a `<script>` tag in the pushed HTML that triggers rendering
1676
+
1677
+ **Recommendation:** (A). The SSE client-side handler already knows when it receives updates. Adding `mermaid.run({ nodes: [newElement] })` after DOM insertion is straightforward.
1678
+
1679
+ ### Q7: Anti-Slop Enforcement -- Build-Time or Runtime?
1680
+
1681
+ The visual explainer spec forbids certain colors, fonts, and patterns. Should this be enforced?
1682
+
1683
+ **Options:**
1684
+ - **A) Documentation only** -- trust the agent to follow the rules
1685
+ - **B) Build-time validation** -- theme presets reject forbidden values
1686
+ - **C) Runtime CSS override** -- a stylesheet that `!important`-overrides forbidden patterns
1687
+
1688
+ **Recommendation:** (A) for now. The anti-slop rules are guidance for the agent's prompt, not application logic. Theme presets inherently avoid slop by providing curated alternatives. If slop becomes a problem in practice, add (B) as a development-mode warning.
1689
+
1690
+ ### Q8: Comparison Component -- Block Syntax
1691
+
1692
+ The `comparison` component needs two child regions. How to express this in the DSL?
1693
+
1694
+ **Options:**
1695
+ - **A) Named blocks:**
1696
+ ```ruby
1697
+ comparison do
1698
+ before { mermaid "..." }
1699
+ after { mermaid "..." }
1700
+ end
1701
+ ```
1702
+ - **B) Positional blocks:**
1703
+ ```ruby
1704
+ comparison do
1705
+ panel "Before" do mermaid "..." end
1706
+ panel "After" do mermaid "..." end
1707
+ end
1708
+ ```
1709
+ - **C) Hash of content:**
1710
+ ```ruby
1711
+ comparison before: mermaid_html("..."), after: mermaid_html("...")
1712
+ ```
1713
+
1714
+ **Recommendation:** (A). Named blocks (`before`/`after`) are the most readable and follow the pattern of `trigger`/`menu` in the existing `dropdown` component.
1715
+
1716
+ ### Q9: Explainer Slash Commands -- Where Do They Live? **[RESOLVED: Option C]**
1717
+
1718
+ The visual explainer has 8 slash commands that each gather different data before generating a page. These are agent-side behaviors, not StreamWeaver server logic.
1719
+
1720
+ **Options:**
1721
+ - **A) Skills in StreamWeaver gem** -- `StreamWeaver::Skills::DiffReview`, etc.
1722
+ - **B) Separate gem/tool** -- skills live outside StreamWeaver, call it as a library
1723
+ - **C) Claude Code custom commands** -- the `/commands` directory, written in shell/Ruby
1724
+
1725
+ **Decision: Option C (Claude Code custom commands).** The DSL IS the API -- no skill wrapper classes in the gem. Agent glue code (slash commands) lives in `.claude/commands/` as shell/Ruby scripts. The data gathering (git diffs, file reading, codebase analysis) is agent-side work that doesn't belong in a rendering library. See Section 6.5 for details.