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,646 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'json'
5
+ require 'socket'
6
+ require 'fileutils'
7
+ require_relative 'protocol'
8
+ require_relative 'scroll_top_hint'
9
+ require_relative 'session'
10
+ require_relative 'bridge'
11
+ require_relative 'doc_store'
12
+ require_relative 'gist_store'
13
+ require_relative 'gist_publisher'
14
+ require_relative 'gist_save_handler'
15
+ require_relative 'save_doc_widget'
16
+ require_relative '../org/writer'
17
+
18
+ # Load StreamWeaver core for adapter and views
19
+ require_relative '../adapter/base'
20
+ require_relative '../adapter/alpinejs'
21
+ require_relative '../views'
22
+ require_relative '../page_shell'
23
+
24
+ module StreamWeaver
25
+ module Canvas
26
+ # Sinatra-based server that bridges Claude Code (Unix socket) and browsers (WebSocket).
27
+ #
28
+ # Architecture:
29
+ # Claude Code ◄──Unix Socket──► BridgeServer ◄──WebSocket──► Browser
30
+ #
31
+ # The server:
32
+ # 1. Listens on a Unix socket for messages from Claude Code
33
+ # 2. Serves HTML pages with the StreamWeaver UI
34
+ # 3. Maintains WebSocket connections with browsers
35
+ # 4. Routes messages between Claude and browsers
36
+ #
37
+ class BridgeServer < Sinatra::Base
38
+ include GistSaveHandler
39
+
40
+ SOCKET_PATH = File.expand_path('~/.streamweaver/canvas.sock')
41
+ PID_FILE_PATH = File.expand_path('~/.streamweaver/canvas.pid')
42
+ DEFAULT_PORT = 4700
43
+
44
+ class << self
45
+ attr_accessor :bridge, :unix_server, :claude_connections, :port
46
+
47
+ # Must resolve identically to Canvas::Client's -- a redirected
48
+ # client talking to a server that bound the default socket is a
49
+ # silent no-op. See the comment there for why the override exists.
50
+ def socket_path
51
+ ENV['STREAMWEAVER_CANVAS_SOCKET'] || SOCKET_PATH
52
+ end
53
+
54
+ def pid_file_path
55
+ ENV['STREAMWEAVER_CANVAS_PID'] || PID_FILE_PATH
56
+ end
57
+
58
+ def default_port
59
+ DEFAULT_PORT
60
+ end
61
+
62
+ def setup!
63
+ @port ||= DEFAULT_PORT
64
+ @bridge ||= Bridge.new(port: @port)
65
+ @claude_connections ||= []
66
+ end
67
+
68
+ # Find an available port starting from DEFAULT_PORT
69
+ def find_available_port(start_port = DEFAULT_PORT)
70
+ port = start_port
71
+ loop do
72
+ begin
73
+ server = TCPServer.new('127.0.0.1', port)
74
+ server.close
75
+ return port
76
+ rescue Errno::EADDRINUSE
77
+ port += 1
78
+ raise "No available ports found for canvas bridge" if port > start_port + 100
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ # Initialize bridge on first request
85
+ before do
86
+ self.class.setup!
87
+ end
88
+
89
+ configure do
90
+ set :port, DEFAULT_PORT
91
+ set :bind, '127.0.0.1'
92
+ set :show_exceptions, false
93
+ set :logging, false
94
+ end
95
+
96
+ # Health check
97
+ get '/health' do
98
+ content_type :json
99
+ {
100
+ status: 'ok',
101
+ sessions: self.class.bridge.sessions.keys,
102
+ port: self.class.port || DEFAULT_PORT
103
+ }.to_json
104
+ end
105
+
106
+ post '/shutdown' do
107
+ content_type :json
108
+ Thread.new { sleep 0.1; exit(0) }
109
+ { status: 'shutting_down' }.to_json
110
+ end
111
+
112
+ # List all sessions
113
+ get '/sessions' do
114
+ content_type :json
115
+ self.class.bridge.list_sessions.to_json
116
+ end
117
+
118
+ # Canvas session page
119
+ get '/canvas/:name' do
120
+ session_name = params[:name]
121
+
122
+ # Create or get session
123
+ session = self.class.bridge.create_session(session_name)
124
+
125
+ # Render the canvas page
126
+ content_type :html
127
+ render_canvas_page(session_name, session)
128
+ end
129
+
130
+ # Poll endpoint for browsers without WebSocket
131
+ # Returns HTML content and version for incremental updates
132
+ get '/canvas/:name/poll' do
133
+ session_name = params[:name]
134
+ session = self.class.bridge.get_session(session_name)
135
+
136
+ halt 404, { error: 'Session not found' }.to_json unless session
137
+
138
+ content_type :json
139
+ {
140
+ state: session.state,
141
+ html: session.html,
142
+ version: session.html_version,
143
+ toasts: session.pop_toasts
144
+ }.to_json
145
+ end
146
+
147
+ # Save the session's last-good DSL as a persistent canvas doc (Tier 2).
148
+ # Body: {"name": "<doc-name>", "format": "rb"|"org"}; ".rb"/".org" is
149
+ # added/normalized by DocStore. format defaults to "rb".
150
+ post '/canvas/:name/save-doc' do
151
+ content_type :json
152
+ session_name = params[:name]
153
+ session = self.class.bridge.get_session(session_name)
154
+
155
+ halt 404, { ok: false, error: "Session not found: #{session_name}" }.to_json unless session
156
+ halt 422, { ok: false, error: "No DSL stored for session #{session_name}" }.to_json if session.dsl.nil?
157
+
158
+ body = JSON.parse(request.body.read, symbolize_names: true) rescue {}
159
+ doc_name = body[:name]
160
+ format = body[:format] || 'rb'
161
+ halt 422, { ok: false, error: "unrecognized format: #{format.inspect}" }.to_json unless %w[rb org].include?(format)
162
+
163
+ # Gist is a third destination, not a third format -- checked against
164
+ # the raw scope value before it collapses to :repo/:global below (that
165
+ # ternary treats anything other than 'global' as :repo, which would
166
+ # silently misroute a gist save into a file write).
167
+ if body[:scope] == 'gist'
168
+ return handle_gist_save(session.dsl, doc_name, theme: session.theme, layout: session.layout)
169
+ end
170
+
171
+ # The Save-as-doc toggle (stream_weaver-j3b3): 'global' always means
172
+ # DEFAULT_ROOT; anything else (including a missing/malformed value)
173
+ # means "this repo", resolved from the session's own source_dir --
174
+ # never the server's cwd.
175
+ scope = body[:scope] == 'global' ? :global : :repo
176
+
177
+ if format == 'org'
178
+ return rescue_save_errors do
179
+ writer = StreamWeaver::Org::Writer.new(session.dsl)
180
+ org_text = writer.call
181
+ coverage = writer.coverage
182
+ # Strip any .rb/.org the user already typed before appending .org --
183
+ # otherwise a name like "mydoc.org" round-trips to "mydoc.org.org"
184
+ # (DocStore.normalize_name only strips ONE trailing extension, so
185
+ # blindly appending here is not idempotent against an already-typed one).
186
+ # Only strip when doc_name is actually a String -- a non-String
187
+ # (e.g. accidentally posted as a number) is passed through as-is
188
+ # so DocStore.save's own type check rejects it with the same
189
+ # ArgumentError the .rb path below already gets for free, instead
190
+ # of silently coercing it via #to_s into a technically-valid name.
191
+ org_name = doc_name.is_a?(String) ? "#{doc_name.sub(/\.(rb|org)\z/, '')}.org" : doc_name
192
+ path = StreamWeaver::Canvas::DocStore.save(org_name, org_text, scope: scope, source_dir: session.source_dir)
193
+ { ok: true, path: path, coverage: coverage }.to_json
194
+ end
195
+ end
196
+
197
+ # Carry the live session's theme/layout into the saved text -- canvas-read
198
+ # re-renders the file with no session to inherit from (stream_weaver-csf).
199
+ dsl = StreamWeaver::Canvas::DocStore.dsl_with_metadata(
200
+ session.dsl, theme: session.theme, layout: session.layout
201
+ )
202
+
203
+ rescue_save_errors do
204
+ path = StreamWeaver::Canvas::DocStore.save(doc_name, dsl, scope: scope, source_dir: session.source_dir)
205
+ { ok: true, path: path }.to_json
206
+ end
207
+ end
208
+
209
+ # Receive events from browser (fallback for WebSocket)
210
+ post '/canvas/:name/event' do
211
+ session_name = params[:name]
212
+ session = self.class.bridge.get_session(session_name)
213
+
214
+ halt 404, { error: 'Session not found' }.to_json unless session
215
+
216
+ # Parse the event
217
+ body = request.body.read
218
+ event = JSON.parse(body, symbolize_names: true) rescue {}
219
+
220
+ # Update session state
221
+ if event[:state]
222
+ session.update_state(event[:state])
223
+ end
224
+
225
+ # Forward to Claude via Unix socket
226
+ forward_to_claude(session_name, event)
227
+
228
+ content_type :json
229
+ { success: true }.to_json
230
+ end
231
+
232
+ private
233
+
234
+ def render_canvas_page(session_name, session)
235
+ # Create adapter in websocket mode
236
+ adapter = StreamWeaver::Adapter::AlpineJS.new(
237
+ url_prefix: "/canvas/#{session_name}",
238
+ mode: :websocket
239
+ )
240
+
241
+ # If session already has HTML, show it; otherwise show waiting message
242
+ initial_content = if session.html
243
+ session.html
244
+ else
245
+ <<~WAITING
246
+ <div class="sw-canvas-waiting">
247
+ <div class="sw-canvas-logo">
248
+ <svg viewBox="0 0 24 24" width="48" height="48">
249
+ <path fill="currentColor" d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
250
+ </svg>
251
+ </div>
252
+ <h1>StreamWeaver Canvas</h1>
253
+ <div class="sw-canvas-spinner"></div>
254
+ <p class="sw-canvas-status">Waiting for Claude Code...</p>
255
+ <div class="sw-canvas-info">
256
+ <p class="sw-canvas-session">Session: <code>#{session_name}</code></p>
257
+ <p class="sw-canvas-ready">Ready to receive content</p>
258
+ </div>
259
+ <div class="sw-canvas-tip">
260
+ <p>Push content with:</p>
261
+ <code>streamweaver canvas-push #{session_name} &lt;&lt;'RUBY'</code>
262
+ </div>
263
+ </div>
264
+ WAITING
265
+ end
266
+
267
+ <<~HTML
268
+ <!DOCTYPE html>
269
+ <html>
270
+ <head>
271
+ <meta charset="utf-8">
272
+ <meta name="viewport" content="width=device-width, initial-scale=1">
273
+ <title>StreamWeaver Canvas: #{session_name}</title>
274
+ #{StreamWeaver::PageShell.framework_css_html}
275
+ #{inline_stylesheets_html(session)}
276
+ <script>#{StreamWeaver::Theme::AutoMode.inline_script}</script>
277
+ #{adapter.cdn_scripts.join("\n")}
278
+ <!-- Chart.js for charts -->
279
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
280
+ <!-- Chart lazy-init engine — always present (like the mermaid engine below) so a
281
+ `chart type:` push is initialized even though the poll swap that lands it
282
+ replaces #app-container via innerHTML, which never executes embedded <script>
283
+ tags. Without this, only a chart present in the page's very first HTML render
284
+ -- never a later canvas-push -- would ever call `new Chart(...)`. -->
285
+ <style>#{StreamWeaver::Adapter::AlpineJS::CHART_CSS}</style>
286
+ <script>#{StreamWeaver::Adapter::AlpineJS::CHART_JS_INIT}</script>
287
+ <!-- Highlight.js for syntax highlighting -->
288
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css">
289
+ <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
290
+ <!-- Mermaid zoom/pan engine — always present so canvas-push with mermaid works on first push -->
291
+ <script>#{File.read(File.join(__dir__, '..', 'assets', 'js', 'sw-mermaid-zoom.js'))}</script>
292
+ <!-- Kick off CDN fetch immediately so mermaid is ready before first push arrives -->
293
+ <script>if (window.swMermaidPreload) window.swMermaidPreload();</script>
294
+ </head>
295
+ <body class="sw-theme-#{session.theme} sw-layout-#{session.layout}">
296
+ <div id="app-container" #{container_attrs(session.state, adapter)}>
297
+ #{initial_content}
298
+ </div>
299
+ #{save_doc_widget(session_name, session)}
300
+ <script>
301
+ #{polling_script(session_name, session.html_version)}
302
+ </script>
303
+ </body>
304
+ </html>
305
+ HTML
306
+ end
307
+
308
+ # CSS carried by the pushed DSL's `use_stylesheet` calls (stream_weaver-9uk)
309
+ # -- canvas has no route to serve a referenced asset file across
310
+ # processes, so the bridge inlines the raw content it was handed
311
+ # instead. `session.stylesheets` is already deduped/replaced per push
312
+ # (Session#set_stylesheets), so no further digest bookkeeping is
313
+ # needed here to avoid stacking duplicate tags across re-pushes.
314
+ def inline_stylesheets_html(session)
315
+ StreamWeaver::PageShell.user_css_html(inline_stylesheets: session.stylesheets)
316
+ end
317
+
318
+ # Floating "Save as doc" button + Alpine.js modal that POSTs the
319
+ # session's last-good DSL to /canvas/:name/save-doc, promoting it from
320
+ # ephemeral history to docs/streamweaver_canvas/<name>.rb. Chrome shared
321
+ # with the reader's own Save-as-doc widget via SaveDocWidget
322
+ # (stream_weaver-e13); only the endpoint, default-name source, and copy
323
+ # below are specific to the live canvas page.
324
+ def save_doc_widget(session_name, session)
325
+ StreamWeaver::Canvas::SaveDocWidget.render(
326
+ endpoint: "/canvas/#{session_name}/save-doc",
327
+ button_title: 'Save this canvas as a persistent doc',
328
+ dialog_title: 'Save canvas as doc',
329
+ source_dir: session.source_dir,
330
+ hint_html: <<~HINT.strip,
331
+ <span x-show="format !== 'org'">Writes to <code>docs/streamweaver_canvas/&lt;name&gt;.rb</code> (or <code>~/.streamweaver/canvas/</code> outside a git repo).</span>
332
+ <span x-show="format === 'org'">Writes a human-readable <code>&lt;name&gt;.org</code> sibling file alongside the canonical <code>.rb</code>.</span>
333
+ HINT
334
+ name_init: "''",
335
+ reset_name_js: 'this.name = this.defaultName();',
336
+ # defaultName() recomputes a fresh timestamp on every openDialog()
337
+ # call (a long-lived canvas session can be saved many times), unlike
338
+ # the reader's fixed snapshot-derived name -- see SaveDocWidget's
339
+ # name_init/reset_name_js doc comment.
340
+ extra_alpine_data: <<~JS,
341
+ defaultName() {
342
+ const d = new Date();
343
+ const pad = n => String(n).padStart(2, '0');
344
+ const ymd = d.getFullYear() + pad(d.getMonth()+1) + pad(d.getDate());
345
+ const hm = pad(d.getHours()) + pad(d.getMinutes());
346
+ return '#{session_name}-' + ymd + '-' + hm;
347
+ },
348
+ JS
349
+ gist: gist_widget_data(session_name)
350
+ )
351
+ end
352
+
353
+ # Builds SaveDocWidget's `gist:` kwarg (share-to-gist epic) -- see that
354
+ # module's doc comment for the exact shape expected.
355
+ #
356
+ # known/prefill_name need both the doc NAME a gist was recorded under
357
+ # and its entry; GistStore.latest_for_prefix only returns the entry
358
+ # (see disc-155), so this re-derives the [name, entry] pair locally
359
+ # via GistStore.all with the same prefix/max_by logic instead.
360
+ def gist_widget_data(session_name)
361
+ available = StreamWeaver::Canvas::GistPublisher.gh_available?
362
+ name, entry = StreamWeaver::Canvas::GistStore.all
363
+ .select { |n, _| n.start_with?(session_name) }
364
+ .max_by { |_, e| e['updated_at'] }
365
+
366
+ {
367
+ available: available,
368
+ unavailable_reason: available ? nil : 'gh CLI not found on PATH',
369
+ known: entry ? { name => { url: entry['url'], revisions: entry['revisions'] } } : {},
370
+ prefill_name: name
371
+ }
372
+ end
373
+
374
+ def polling_script(session_name, current_version)
375
+ <<~JS
376
+ (function() {
377
+ let currentVersion = #{current_version};
378
+ let sessionClosed = false;
379
+ const pollUrl = '/canvas/#{session_name}/poll';
380
+ const container = document.getElementById('app-container');
381
+ const closedHtml = '<div style="text-align:center;padding:60px;color:#374151;"><div style="font-size:3rem;margin-bottom:16px;">✓</div><h2 style="margin:0 0 12px">Session Complete</h2><p style="color:#666;margin:0">This StreamWeaver canvas session has closed.</p></div>';
382
+
383
+ // Shared state for coordinating poll updates with showFeedback.
384
+ // When showFeedback shows a spinner, it records the version at that
385
+ // time. The poll will only replace the spinner with content from a
386
+ // NEWER version, preventing stale feedback from clobbering new content.
387
+ window._swContentVersion = currentVersion;
388
+ window._swFeedbackActive = false;
389
+
390
+ // --- Optional debug overlay: add ?debug to the URL to enable ---
391
+ if (location.search.includes('debug')) {
392
+ const _dbg = document.createElement('div');
393
+ _dbg.id = 'sw-debug';
394
+ _dbg.style.cssText = 'position:fixed;top:0;left:0;right:0;background:rgba(0,0,0,0.85);color:#0f0;font:10px/1.3 monospace;padding:2px 6px;z-index:99999;max-height:60px;overflow-y:auto;pointer-events:none;';
395
+ document.body.appendChild(_dbg);
396
+ const _dl = [];
397
+ window._dbgLog = function(msg) {
398
+ const t = new Date().toLocaleTimeString('en-US',{hour12:false});
399
+ _dl.push(t + ' ' + msg);
400
+ if (_dl.length > 30) _dl.shift();
401
+ _dbg.innerHTML = _dl.join('<br>');
402
+ _dbg.scrollTop = 99999;
403
+ };
404
+ window._dbgLog('init v=' + currentVersion);
405
+ } else {
406
+ window._dbgLog = function(){};
407
+ }
408
+
409
+ function showToast(toast) {
410
+ // Remove any existing toast
411
+ const existing = document.querySelector('.sw-toast');
412
+ if (existing) existing.remove();
413
+
414
+ const el = document.createElement('div');
415
+ el.className = 'sw-toast sw-toast-' + (toast.variant || 'warning');
416
+ el.innerHTML = '<span class="sw-toast-message">' + escapeHtml(toast.message) + '</span>' +
417
+ '<button class="sw-toast-close" onclick="this.parentElement.remove()">&times;</button>';
418
+ document.body.appendChild(el);
419
+
420
+ // Auto-dismiss if duration > 0
421
+ if (toast.duration > 0) {
422
+ setTimeout(() => el.remove(), toast.duration);
423
+ }
424
+ }
425
+
426
+ function escapeHtml(text) {
427
+ const div = document.createElement('div');
428
+ div.textContent = text;
429
+ return div.innerHTML;
430
+ }
431
+
432
+ async function poll() {
433
+ try {
434
+ const resp = await fetch(pollUrl);
435
+ if (!resp.ok) {
436
+ if (resp.status === 404 && !sessionClosed) {
437
+ sessionClosed = true;
438
+ container.innerHTML = closedHtml;
439
+ }
440
+ return;
441
+ }
442
+
443
+ const data = await resp.json();
444
+
445
+ // Show any pending toasts
446
+ if (data.toasts && data.toasts.length > 0) {
447
+ data.toasts.forEach(toast => showToast(toast));
448
+ }
449
+
450
+ // Update if version changed and there's HTML
451
+ if (data.version > currentVersion && data.html) {
452
+ window._dbgLog('POLL v' + currentVersion + '->' + data.version + ' feedback=' + window._swFeedbackActive);
453
+ currentVersion = data.version;
454
+ window._swContentVersion = data.version;
455
+ window._swFeedbackActive = false;
456
+
457
+ // Auto-scroll to follow doc growth (round-7 UAT): a growing
458
+ // canvas (step 4's doc) lands new sections below the fold
459
+ // with nothing to tell the viewer to look down. Standard
460
+ // chat-app rule -- only follow if the viewer was already
461
+ // at/near the bottom BEFORE this swap; never yank someone
462
+ // who scrolled up to reread something.
463
+ const wasNearBottom = (window.innerHeight + window.scrollY) >=
464
+ (document.documentElement.scrollHeight - 150);
465
+
466
+ container.innerHTML = data.html;
467
+
468
+ // Scroll-to-top hint (round-9 UAT): a push that reshapes
469
+ // the whole page -- e.g. the completion recap that
470
+ // appears the moment the last step is marked done -- is
471
+ // not a growing doc continuing in place, so it must not
472
+ // inherit the wasNearBottom follow-the-viewer logic below.
473
+ // A one-shot marker element in the SWAPPED-IN html (see
474
+ // Listener::SCROLL_TOP_HINT_DSL) means "land at the top of
475
+ // this page"; it needs no cleanup because the very next
476
+ // poll swap replaces the whole DOM regardless.
477
+ const scrollToTop = !!container.querySelector('##{SCROLL_TOP_HINT_ID}');
478
+
479
+ // Remove toast when new content arrives (unless persistent)
480
+ const existingToast = document.querySelector('.sw-toast');
481
+ if (existingToast) existingToast.remove();
482
+
483
+ // Re-initialize Alpine.js on the new content
484
+ if (window.Alpine) {
485
+ Alpine.initTree(container);
486
+ }
487
+
488
+ // Initialize mermaid diagrams (swMermaidInit is idempotent via data-sw-mermaid-done guard)
489
+ if (window.swMermaidInit) window.swMermaidInit();
490
+
491
+ // Apply syntax highlighting and initialize charts after DOM update
492
+ setTimeout(() => {
493
+ // Apply syntax highlighting to code blocks
494
+ if (window.hljs) {
495
+ container.querySelectorAll('pre code:not(.hljs)').forEach((block) => {
496
+ hljs.highlightElement(block);
497
+ });
498
+ }
499
+
500
+ // Initialize `chart type:` charts (data-sw-chart-* canvases --
501
+ // see sw-chart__canvas in adapter/alpinejs.rb). The shorthand
502
+ // chart family (bar_chart, line_chart, ...) uses Alpine x-init
503
+ // and is already covered by Alpine.initTree(container) above.
504
+ if (window.swChartInit) window.swChartInit();
505
+
506
+ if (scrollToTop) {
507
+ window.scrollTo({ top: 0, behavior: 'auto' });
508
+ } else if (wasNearBottom) {
509
+ window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' });
510
+ }
511
+ }, 10);
512
+ }
513
+ } catch (e) {
514
+ console.error('Poll error:', e);
515
+ }
516
+ }
517
+
518
+ // Poll every 500ms
519
+ setInterval(poll, 500);
520
+
521
+ // Also poll immediately
522
+ poll();
523
+ })();
524
+ JS
525
+ end
526
+
527
+ def container_attrs(state, adapter)
528
+ attrs = adapter.container_attributes(state)
529
+ attrs.map { |k, v| "#{k}='#{v.gsub("'", "\\\\'")}'" }.join(' ')
530
+ end
531
+
532
+ def forward_to_claude(session_name, event)
533
+ message = Protocol::Messages.event(
534
+ session_name,
535
+ event[:type] || 'event',
536
+ event
537
+ )
538
+
539
+ connections = self.class.claude_connections || []
540
+
541
+ # Send to all connected Claude clients
542
+ connections.each do |conn|
543
+ conn.write(Protocol.encode(message))
544
+ rescue => e
545
+ warn "canvas: dropped event for #{session_name}: #{e.message}"
546
+ end
547
+ end
548
+
549
+ # Start the Unix socket server in a background thread
550
+ def self.start_unix_socket_server
551
+ # Ensure directory exists
552
+ FileUtils.mkdir_p(File.dirname(socket_path))
553
+
554
+ # Remove stale socket
555
+ File.delete(socket_path) if File.exist?(socket_path)
556
+
557
+ @unix_server = UNIXServer.new(socket_path)
558
+
559
+ Thread.new do
560
+ loop do
561
+ begin
562
+ client = @unix_server.accept
563
+ @claude_connections << client
564
+ handle_claude_connection(client)
565
+ rescue => e
566
+ break if @unix_server.nil? || @unix_server.closed?
567
+ end
568
+ end
569
+ end
570
+ end
571
+
572
+ def self.handle_claude_connection(client)
573
+ Thread.new do
574
+ buffer = ''
575
+
576
+ loop do
577
+ begin
578
+ data = client.read_nonblock(4096)
579
+ buffer += data
580
+
581
+ # Parse complete messages
582
+ messages, buffer = Protocol.parse_buffer(buffer)
583
+
584
+ messages.each do |msg|
585
+ response = @bridge.handle_claude_message(msg)
586
+
587
+ if response
588
+ client.write(Protocol.encode(response))
589
+ end
590
+
591
+ # If push message, broadcast to browser websockets
592
+ if msg[:type] == 'push'
593
+ broadcast_to_browsers(msg[:name])
594
+ end
595
+ end
596
+ rescue IO::WaitReadable
597
+ IO.select([client], nil, nil, 0.1)
598
+ rescue EOFError, Errno::ECONNRESET
599
+ break
600
+ end
601
+ end
602
+
603
+ @claude_connections.delete(client)
604
+ client.close rescue nil
605
+ end
606
+ end
607
+
608
+ def self.broadcast_to_browsers(session_name)
609
+ session = @bridge.get_session(session_name)
610
+ return unless session
611
+
612
+ # Session broadcasts to its websockets
613
+ # For HTTP polling mode, browsers will get updates on next poll
614
+ end
615
+
616
+ # Write PID file
617
+ def self.write_pid_file
618
+ FileUtils.mkdir_p(File.dirname(pid_file_path))
619
+ File.write(pid_file_path, "pid=#{Process.pid}\nport=#{@port || DEFAULT_PORT}\n")
620
+ end
621
+
622
+ # Cleanup on shutdown
623
+ def self.cleanup
624
+ File.delete(socket_path) if File.exist?(socket_path)
625
+ File.delete(pid_file_path) if File.exist?(pid_file_path)
626
+ @unix_server&.close
627
+ end
628
+
629
+ # Start the full server (Unix socket + HTTP)
630
+ def self.run!
631
+ # Find available port before setup
632
+ @port = find_available_port
633
+ set :port, @port
634
+
635
+ setup!
636
+ write_pid_file
637
+ start_unix_socket_server
638
+
639
+ at_exit { cleanup }
640
+
641
+ # Start Sinatra
642
+ super
643
+ end
644
+ end
645
+ end
646
+ end