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.
- checksums.yaml +7 -0
- data/.beads/.gitignore +77 -0
- data/.beads/README.md +81 -0
- data/.beads/config.yaml +54 -0
- data/.beads/metadata.json +7 -0
- data/.rspec +3 -0
- data/AGENTS.md +91 -0
- data/CHANGELOG.md +294 -0
- data/CLAUDE.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +565 -0
- data/Rakefile +13 -0
- data/assets/streamweaver-hero.jpg +0 -0
- data/bench/baselines/ledger.rb +58 -0
- data/bench/baselines/warroom.rb +51 -0
- data/bench/fixtures/ledger.rb +76 -0
- data/bench/fixtures/warroom.rb +60 -0
- data/bench/results/6daf8b8.md +102 -0
- data/bench/results/c8d749b.md +102 -0
- data/bench/results/eb06ab9.md +102 -0
- data/bench/run.rb +151 -0
- data/bench/support.rb +84 -0
- data/copy_this/dto_run_sample_run_7631.html +220 -0
- data/docs/SERVICE_MODE.md +302 -0
- data/docs/architecture/how_streamweaver_works.md +772 -0
- data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
- data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
- data/docs/canvas-ipc-session-summary.md +127 -0
- data/docs/canvas-panel-workflow.md +172 -0
- data/docs/canvas-read.md +147 -0
- data/docs/canvas-roadmap.md +275 -0
- data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
- data/docs/claude-code-companion-skill-spec.md +567 -0
- data/docs/components_reference.md +1223 -0
- data/docs/crud-patterns.md +265 -0
- data/docs/endpoints.md +122 -0
- data/docs/for_llms.md +1 -0
- data/docs/form-patterns.md +165 -0
- data/docs/frontend-only.md +169 -0
- data/docs/html-artifact-audit.md +238 -0
- data/docs/ideas/2025-01-01-as-a-service.md +203 -0
- data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
- data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
- data/docs/ideas/dhh-review-future-refactors.md +142 -0
- data/docs/opal-jamstack.md +47 -0
- data/docs/opal-npm-direction.md +184 -0
- data/docs/opal-spike-findings.md +194 -0
- data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
- data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
- data/docs/plans/2026-01-02-charts-design.md +288 -0
- data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
- data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
- data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
- data/docs/plans/canvas-claude-project-design.md +238 -0
- data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
- data/docs/plans/org-doc-preview-surfaces.md +293 -0
- data/docs/plans/shadcn-polish-plan.md +417 -0
- data/docs/porting-artifacts.md +252 -0
- data/docs/reference/agent-skills-comparison.md +369 -0
- data/docs/reference/travel-state-prd.artifact.html +780 -0
- data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
- data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
- data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
- data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
- data/docs/research/frontend-only-matrix.md +278 -0
- data/docs/research/streamweaver-way-spike-findings.md +529 -0
- data/docs/resource-dsl.md +342 -0
- data/docs/routing.md +302 -0
- data/docs/ruby-ui-comparison.md +319 -0
- data/docs/shared-dsl-fragments.md +356 -0
- data/docs/streamweaver-for-ai-agents.md +401 -0
- data/docs/streamweaver-frontend-vision.md +123 -0
- data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
- data/docs/streamweaver_canvas/save_example.rb +9 -0
- data/docs/templates.md +213 -0
- data/docs/testing.md +155 -0
- data/docs/theming-hooks.md +281 -0
- data/docs/tutorials/the-streamweaver-way.md +882 -0
- data/docs/university/capability-inventory.md +137 -0
- data/docs/university/dependency-survey.md +114 -0
- data/docs/university/design-spec.md +217 -0
- data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
- data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
- data/docs/university/mockups/course-list-dark.png +0 -0
- data/docs/university/mockups/course-list-light.png +0 -0
- data/docs/university/mockups/course_canvas_mockup.rb +741 -0
- data/docs/university/mockups/step-screen-dark.png +0 -0
- data/docs/university/mockups/step-screen-light.png +0 -0
- data/docs/university/roadmap.md +72 -0
- data/docs/university/send-to-coworker.md +77 -0
- data/docs/university/worker-session-mining.md +109 -0
- data/docs/visual/browser-rendering-spike.md +173 -0
- data/docs/visual/builder-visual-plan-analysis.md +378 -0
- data/docs/visual/sw-plan-format-exploration.md +511 -0
- data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
- data/docs/visual-skills/PROGRESS.md +60 -0
- data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
- data/docs/visual-skills/analysis/components.md +317 -0
- data/docs/visual-skills/analysis/overlap.md +360 -0
- data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
- data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
- data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
- data/docs/visual-skills/analysis/visual-explainer.md +964 -0
- data/docs/visual-skills/blog/blog-series-outline.md +39 -0
- data/docs/visual-skills/blog/token-efficiency.md +73 -0
- data/docs/visual-skills/design/architecture.md +1725 -0
- data/docs/visual-skills/design/codex-review.md +340 -0
- data/docs/visual-skills/design/dhh-review.md +209 -0
- data/docs/visual-skills/design/evolution.md +100 -0
- data/docs/visual-skills/design/gemini-review.md +55 -0
- data/docs/visual-skills/design/review-synthesis.md +149 -0
- data/docs/visual-skills/implementation/STATE.md +22 -0
- data/docs/visual-skills/implementation/plan.md +347 -0
- data/docs/visual-skills/implementation/spike-findings.md +237 -0
- data/docs/visual-skills/implementation/tasks.md +702 -0
- data/docs/visual-skills/lessons-learned/process.md +145 -0
- data/examples/README.md +96 -0
- data/examples/advanced/all_components.rb +177 -0
- data/examples/advanced/examples_browser.rb +394 -0
- data/examples/advanced/teachables_browser.rb +261 -0
- data/examples/advanced/theme_tweaker.rb +366 -0
- data/examples/advanced/tutorial.rb +1528 -0
- data/examples/agentic/agentic_form.rb +30 -0
- data/examples/agentic/agentic_form_autoclose.rb +30 -0
- data/examples/agentic/cultivation_tracker.rb +69 -0
- data/examples/basic/hello_world.rb +22 -0
- data/examples/basic/opal_tabs_table.rb +44 -0
- data/examples/basic/todo_list.rb +39 -0
- data/examples/button_loading_test.rb +36 -0
- data/examples/canvas/mermaid_canvas_demo.sh +151 -0
- data/examples/charts/bar_chart_demo.rb +57 -0
- data/examples/charts/line_chart_demo.rb +73 -0
- data/examples/charts/pie_area_demo.rb +87 -0
- data/examples/charts/stacked_bar_chart_demo.rb +69 -0
- data/examples/claude_code/README.md +85 -0
- data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
- data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
- data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
- data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
- data/examples/claude_code/codebreaker/README.md +213 -0
- data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
- data/examples/claude_code/tutorial/README.md +102 -0
- data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
- data/examples/claude_code/verification_flow/README.md +126 -0
- data/examples/components/annotated_code_demo.rb +54 -0
- data/examples/components/callout_demo.rb +33 -0
- data/examples/components/checkbox_group_demo.rb +46 -0
- data/examples/components/design_review.css +468 -0
- data/examples/components/design_review_demo.rb +44 -0
- data/examples/components/design_review_dsl.rb +300 -0
- data/examples/components/diff_block_demo.rb +112 -0
- data/examples/components/events_demo.rb +184 -0
- data/examples/components/form_demo.rb +62 -0
- data/examples/components/lesson_demo.rb +53 -0
- data/examples/components/markdown_demo.rb +110 -0
- data/examples/components/mermaid_demo.rb +166 -0
- data/examples/components/pareto_set.rb +64 -0
- data/examples/components/prd_demo.rb +21 -0
- data/examples/components/prd_dsl.rb +385 -0
- data/examples/components/quiz_demo.rb +47 -0
- data/examples/components/run_viewer_demo.rb +100 -0
- data/examples/components/score_and_collapsible_demo.rb +82 -0
- data/examples/components/select_stale_value_smoke_test.rb +39 -0
- data/examples/components/table_demo.rb +247 -0
- data/examples/components/timer_with_state_demo.rb +58 -0
- data/examples/components/todo_due_dates.rb +63 -0
- data/examples/components/uat_gaps_demo.rb +112 -0
- data/examples/dashboard/feed_simulator.rb +60 -0
- data/examples/dashboard/live_dashboard.rb +80 -0
- data/examples/dashboard_components.rb +108 -0
- data/examples/deferred_fragments_demo.rb +62 -0
- data/examples/generate_more_spike/README.md +63 -0
- data/examples/generate_more_spike/app.rb +441 -0
- data/examples/git_health.sh +401 -0
- data/examples/layout/layout_components_demo.rb +174 -0
- data/examples/layout/modal_demo.rb +215 -0
- data/examples/layout/navigation_demo.rb +227 -0
- data/examples/layout/route_tabs_demo.rb +52 -0
- data/examples/layout/routing_demo.rb +52 -0
- data/examples/layout/scroll_box_demo.rb +154 -0
- data/examples/lazy_fragments_demo.rb +151 -0
- data/examples/my_todos/README.md +29 -0
- data/examples/my_todos/my_todos.rb +312 -0
- data/examples/my_todos/store.rb +86 -0
- data/examples/opal/reactive_demo.rb +62 -0
- data/examples/opal/scenarios/s1_counter.rb +15 -0
- data/examples/opal/scenarios/s2_search_filter.rb +17 -0
- data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
- data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
- data/examples/opal/scenarios/s5_watch.rb +39 -0
- data/examples/opal/scenarios/s6_on_start.rb +20 -0
- data/examples/opal/scenarios/s7_wizard.rb +33 -0
- data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
- data/examples/opal/scenarios/s9_dashboard.rb +27 -0
- data/examples/operations_dashboard_demo.rb +167 -0
- data/examples/panel_demo.sh +447 -0
- data/examples/parity/assets/tyrion_slice.css +86 -0
- data/examples/parity/rivet_people_slice.rb +182 -0
- data/examples/parity/tyrion_components.css +777 -0
- data/examples/parity/tyrion_warroom_components.rb +414 -0
- data/examples/parity/tyrion_warroom_slice.rb +549 -0
- data/examples/puma_dev/README.md +124 -0
- data/examples/puma_dev/config.ru +34 -0
- data/examples/puma_dev/standalone_app.rb +34 -0
- data/examples/scaffolding/blog.rb +49 -0
- data/examples/scaffolding/utf_lite.rb +105 -0
- data/examples/styling/feedback_demo.rb +229 -0
- data/examples/styling/style_showcase.rb +172 -0
- data/examples/styling/theme_demo.rb +508 -0
- data/examples/timer_health_checker.rb +111 -0
- data/examples/timer_showcase.rb +131 -0
- data/examples/tutorials/render_markdown.rb +112 -0
- data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
- data/examples/tutorials/tutorial_content.rb +1082 -0
- data/examples/visual_skills/design_deck_demo.rb +314 -0
- data/examples/visual_skills/explainer_demo.rb +499 -0
- data/exe/streamweaver +9 -0
- data/gsd/ROADMAP-1.0.md +167 -0
- data/gsd/STATE.md +13 -0
- data/gsd/research/market-positioning-research.md +139 -0
- data/gsd/research/production-patterns-research.md +270 -0
- data/gsd/research/repo-audit-1.0.md +274 -0
- data/lib/stream_weaver/action_token.rb +58 -0
- data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
- data/lib/stream_weaver/adapter/base.rb +591 -0
- data/lib/stream_weaver/adapter/opal.rb +334 -0
- data/lib/stream_weaver/adapter/static.rb +1118 -0
- data/lib/stream_weaver/admin.rb +176 -0
- data/lib/stream_weaver/app.rb +1821 -0
- data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
- data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
- data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
- data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
- data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
- data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
- data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
- data/lib/stream_weaver/canvas/bridge.rb +239 -0
- data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
- data/lib/stream_weaver/canvas/client.rb +298 -0
- data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
- data/lib/stream_weaver/canvas/doc_store.rb +252 -0
- data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
- data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
- data/lib/stream_weaver/canvas/gist_store.rb +135 -0
- data/lib/stream_weaver/canvas/helpers.rb +109 -0
- data/lib/stream_weaver/canvas/history.rb +90 -0
- data/lib/stream_weaver/canvas/protocol.rb +86 -0
- data/lib/stream_weaver/canvas/reader.rb +853 -0
- data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
- data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
- data/lib/stream_weaver/canvas/session.rb +132 -0
- data/lib/stream_weaver/cli.rb +3235 -0
- data/lib/stream_weaver/component_assets.rb +70 -0
- data/lib/stream_weaver/component_registry.rb +67 -0
- data/lib/stream_weaver/component_renderer.rb +48 -0
- data/lib/stream_weaver/components/annotated_code.rb +53 -0
- data/lib/stream_weaver/components/api_endpoint.rb +42 -0
- data/lib/stream_weaver/components/callout.rb +59 -0
- data/lib/stream_weaver/components/chart.rb +84 -0
- data/lib/stream_weaver/components/code_block.rb +77 -0
- data/lib/stream_weaver/components/comparison.rb +39 -0
- data/lib/stream_weaver/components/decision.rb +38 -0
- data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
- data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
- data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
- data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
- data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
- data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
- data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
- data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
- data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
- data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
- data/lib/stream_weaver/components/diff_block.rb +125 -0
- data/lib/stream_weaver/components/doc_header.rb +57 -0
- data/lib/stream_weaver/components/image_block.rb +70 -0
- data/lib/stream_weaver/components/implementation_map.rb +33 -0
- data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
- data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
- data/lib/stream_weaver/components/mermaid.rb +79 -0
- data/lib/stream_weaver/components/pipeline.rb +63 -0
- data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
- data/lib/stream_weaver/components/slide_container.rb +141 -0
- data/lib/stream_weaver/components/timeline_event.rb +58 -0
- data/lib/stream_weaver/components/wireframe.rb +29 -0
- data/lib/stream_weaver/components/wireframe_block.rb +34 -0
- data/lib/stream_weaver/components.rb +2711 -0
- data/lib/stream_weaver/css.rb +230 -0
- data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
- data/lib/stream_weaver/display_dsl.rb +1004 -0
- data/lib/stream_weaver/export/html_exporter.rb +478 -0
- data/lib/stream_weaver/feed.rb +34 -0
- data/lib/stream_weaver/feed_builder.rb +31 -0
- data/lib/stream_weaver/fonts.rb +33 -0
- data/lib/stream_weaver/interaction_runner.rb +487 -0
- data/lib/stream_weaver/iterm.rb +460 -0
- data/lib/stream_weaver/layout_registry.rb +92 -0
- data/lib/stream_weaver/opal/bridge.rb +52 -0
- data/lib/stream_weaver/opal/builder.rb +193 -0
- data/lib/stream_weaver/opal/env.rb +29 -0
- data/lib/stream_weaver/opal/reactive_state.rb +75 -0
- data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
- data/lib/stream_weaver/opal/renderer.rb +77 -0
- data/lib/stream_weaver/opal/runtime.rb +250 -0
- data/lib/stream_weaver/opal/shell.rb +145 -0
- data/lib/stream_weaver/opal/string_bridge.rb +50 -0
- data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
- data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
- data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
- data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
- data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
- data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
- data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
- data/lib/stream_weaver/opal_entry.rb +136 -0
- data/lib/stream_weaver/org/inline.rb +95 -0
- data/lib/stream_weaver/org/reader.rb +561 -0
- data/lib/stream_weaver/org/recording_context.rb +86 -0
- data/lib/stream_weaver/org/source_splitter.rb +63 -0
- data/lib/stream_weaver/org/writer.rb +300 -0
- data/lib/stream_weaver/page_shell.rb +530 -0
- data/lib/stream_weaver/portfile.rb +79 -0
- data/lib/stream_weaver/pushable.rb +42 -0
- data/lib/stream_weaver/resource/default_views.rb +111 -0
- data/lib/stream_weaver/resource/field_input.rb +25 -0
- data/lib/stream_weaver/resource/state_keys.rb +16 -0
- data/lib/stream_weaver/resource/store.rb +18 -0
- data/lib/stream_weaver/resource.rb +104 -0
- data/lib/stream_weaver/server.rb +1471 -0
- data/lib/stream_weaver/service.rb +1240 -0
- data/lib/stream_weaver/service_client.rb +104 -0
- data/lib/stream_weaver/session_store.rb +186 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
- data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
- data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
- data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
- data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
- data/lib/stream_weaver/streamer.rb +63 -0
- data/lib/stream_weaver/templates/choices.rb +149 -0
- data/lib/stream_weaver/templates/code.rb +157 -0
- data/lib/stream_weaver/templates/confirm.rb +110 -0
- data/lib/stream_weaver/templates/diff.rb +195 -0
- data/lib/stream_weaver/templates/info.rb +142 -0
- data/lib/stream_weaver/templates/table.rb +169 -0
- data/lib/stream_weaver/templates/wizard.rb +271 -0
- data/lib/stream_weaver/theme/auto_mode.rb +116 -0
- data/lib/stream_weaver/theme/presets.rb +555 -0
- data/lib/stream_weaver/theme.rb +639 -0
- data/lib/stream_weaver/university/canvas.rb +924 -0
- data/lib/stream_weaver/university/course.rb +699 -0
- data/lib/stream_weaver/university/demos/counter.rb +33 -0
- data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
- data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
- data/lib/stream_weaver/university/demos.rb +50 -0
- data/lib/stream_weaver/university/listener.rb +467 -0
- data/lib/stream_weaver/university/progress.rb +201 -0
- data/lib/stream_weaver/university/runner.rb +138 -0
- data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
- data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
- data/lib/stream_weaver/utils.rb +31 -0
- data/lib/stream_weaver/version.rb +5 -0
- data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
- data/lib/stream_weaver/views.rb +3940 -0
- data/lib/stream_weaver.rb +122 -0
- data/llms.txt +1269 -0
- data/sig/stream_weaver.rbs +4 -0
- metadata +598 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
# Weaving Web UIs with Ruby: A StreamWeaver Introduction
|
|
2
|
+
|
|
3
|
+
*Build reactive web interfaces with pure Ruby—no JavaScript required.*
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
<!-- Hero image: Terminal showing StreamWeaver app code on left, browser with running app on right -->
|
|
7
|
+
|
|
8
|
+
## A Personal Confession
|
|
9
|
+
|
|
10
|
+
I've spent more hours than I care to admit building internal tools. Dashboards for monitoring. Forms for data entry. Quick interfaces to wrap API calls. Every time, the same friction: set up a JavaScript build pipeline, configure a frontend framework, wire up API endpoints, manage state synchronization between client and server.
|
|
11
|
+
|
|
12
|
+
For a "simple" tool.
|
|
13
|
+
|
|
14
|
+
Python developers had Streamlit. They could write a script, sprinkle in some UI components, and have a working web app in minutes. Meanwhile, Ruby developers—blessed with one of the most expressive languages ever created—were still configuring Webpack.
|
|
15
|
+
|
|
16
|
+
StreamWeaver changes that. It's Streamlit's ease of use, brought to Ruby with a DSL that feels like it belongs in the language.
|
|
17
|
+
|
|
18
|
+
## What is StreamWeaver?
|
|
19
|
+
|
|
20
|
+
StreamWeaver is a Ruby DSL for building reactive web UIs with minimal code. Write your interface in pure Ruby. Run the script. A browser opens with your working application.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require 'stream_weaver'
|
|
24
|
+
|
|
25
|
+
app "Hello World" do
|
|
26
|
+
text_field :name, placeholder: "What's your name?"
|
|
27
|
+
|
|
28
|
+
if state[:name].to_s.strip != ""
|
|
29
|
+
text "Hello, #{state[:name]}!"
|
|
30
|
+
end
|
|
31
|
+
end.run!
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
<!-- Screenshot: Browser showing text input and greeting -->
|
|
36
|
+
|
|
37
|
+
That's it. No HTML templates. No JavaScript. No build step. No webpack. No npm. Just Ruby.
|
|
38
|
+
|
|
39
|
+
## The Key Insight
|
|
40
|
+
|
|
41
|
+
Here's the magic that makes StreamWeaver tick:
|
|
42
|
+
|
|
43
|
+
**Your Ruby block re-executes on every user interaction.**
|
|
44
|
+
|
|
45
|
+
When a user types in a text field, clicks a button, or changes a selection, your entire block runs again with the updated state. This creates reactive UI without you writing any event handling code.
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
app "Counter" do
|
|
49
|
+
state[:count] ||= 0
|
|
50
|
+
|
|
51
|
+
text "Count: #{state[:count]}"
|
|
52
|
+
|
|
53
|
+
button "+" do |s|
|
|
54
|
+
s[:count] += 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
button "-" do |s|
|
|
58
|
+
s[:count] -= 1
|
|
59
|
+
end
|
|
60
|
+
end.run!
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
<!-- Screenshot: Counter UI with + and - buttons -->
|
|
65
|
+
|
|
66
|
+
Click the "+" button:
|
|
67
|
+
1. The callback runs: `s[:count] += 1`
|
|
68
|
+
2. Your entire block re-executes
|
|
69
|
+
3. `text "Count: #{state[:count]}"` now shows the new value
|
|
70
|
+
4. The browser updates automatically
|
|
71
|
+
|
|
72
|
+
No manual DOM manipulation. No state synchronization. No WebSocket configuration. StreamWeaver handles all of it.
|
|
73
|
+
|
|
74
|
+
## Why Ruby Needs This
|
|
75
|
+
|
|
76
|
+
Ruby's philosophy has always been developer happiness. We optimize for expressiveness, readability, and joy. Rails brought this philosophy to web applications. Hotwire brought it to interactive features.
|
|
77
|
+
|
|
78
|
+
But there's a gap. When you need a quick internal tool, a prototype, or a simple UI for a script—Rails is overkill. You don't need models, migrations, and a full MVC stack for a form that calls an API.
|
|
79
|
+
|
|
80
|
+
StreamWeaver fills that gap:
|
|
81
|
+
|
|
82
|
+
| Need | Traditional Approach | StreamWeaver |
|
|
83
|
+
|------|---------------------|--------------|
|
|
84
|
+
| Quick form UI | Rails scaffold + views + JS | 10 lines of Ruby |
|
|
85
|
+
| Data dashboard | React app + API | Single Ruby file |
|
|
86
|
+
| Script with UI | CLI flags or Tk | Declarative DSL |
|
|
87
|
+
| Prototype | Full stack setup | Immediate iteration |
|
|
88
|
+
|
|
89
|
+
### The GenAI Advantage
|
|
90
|
+
|
|
91
|
+
We're in an era where AI assistants write significant portions of our code. Token efficiency matters. The more concise your DSL, the more the AI can accomplish in a single context window.
|
|
92
|
+
|
|
93
|
+
StreamWeaver's declarative approach is token-efficient by design. Instead of describing separate model, view, controller, and JavaScript layers, you describe the UI once. The AI (and you) can iterate faster.
|
|
94
|
+
|
|
95
|
+
## The Component Library
|
|
96
|
+
|
|
97
|
+
StreamWeaver provides all the building blocks you need for real applications.
|
|
98
|
+
|
|
99
|
+
### Input Components
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
app "Form Demo" do
|
|
103
|
+
text_field :name, placeholder: "Your name"
|
|
104
|
+
text_area :bio, rows: 4, placeholder: "Tell us about yourself"
|
|
105
|
+
checkbox :newsletter, "Subscribe to newsletter"
|
|
106
|
+
select :role, ["Developer", "Designer", "Manager"]
|
|
107
|
+
radio_group :priority, ["Low", "Medium", "High"]
|
|
108
|
+
end.run!
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+

|
|
112
|
+
<!-- Screenshot: Form with all input types displayed -->
|
|
113
|
+
|
|
114
|
+
Each component automatically binds to `state[:key]`. When the user types in `text_field :name`, the value is available as `state[:name]`.
|
|
115
|
+
|
|
116
|
+
### Display Components
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
app "Display Demo" do
|
|
120
|
+
header1 "Page Title"
|
|
121
|
+
header "Section Header"
|
|
122
|
+
header3 "Subsection"
|
|
123
|
+
|
|
124
|
+
text "Plain text with interpolation: #{Time.now}"
|
|
125
|
+
md "**Markdown** support with *formatting* and `code`"
|
|
126
|
+
end.run!
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+

|
|
130
|
+
<!-- Screenshot: Various header sizes and text styles -->
|
|
131
|
+
|
|
132
|
+
### Buttons & Actions
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
app "Actions" do
|
|
136
|
+
state[:message] ||= "Click a button"
|
|
137
|
+
|
|
138
|
+
text state[:message]
|
|
139
|
+
|
|
140
|
+
button "Primary Action" do |s|
|
|
141
|
+
s[:message] = "Primary clicked!"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
button "Secondary", style: :secondary do |s|
|
|
145
|
+
s[:message] = "Secondary clicked!"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
button "Danger", style: :danger do |s|
|
|
149
|
+
s[:message] = "Danger clicked!"
|
|
150
|
+
end
|
|
151
|
+
end.run!
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+

|
|
155
|
+
<!-- Screenshot: Three button styles in a row -->
|
|
156
|
+
|
|
157
|
+
### Layout Components
|
|
158
|
+
|
|
159
|
+
StreamWeaver provides flexible layout primitives:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
app "Layout Demo" do
|
|
163
|
+
columns widths: ['30%', '70%'] do
|
|
164
|
+
column do
|
|
165
|
+
card do
|
|
166
|
+
header3 "Sidebar"
|
|
167
|
+
text "Navigation goes here"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
column do
|
|
172
|
+
vstack spacing: :md do
|
|
173
|
+
card do
|
|
174
|
+
header3 "Main Content"
|
|
175
|
+
text "Your primary content area"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
hstack spacing: :sm do
|
|
179
|
+
button "Save"
|
|
180
|
+
button "Cancel", style: :secondary
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end.run!
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+

|
|
189
|
+
<!-- Screenshot: Two-column layout with sidebar and main content -->
|
|
190
|
+
|
|
191
|
+
**Layout primitives:**
|
|
192
|
+
- `columns` - Multi-column layouts with custom widths
|
|
193
|
+
- `vstack` - Vertical stacking with spacing
|
|
194
|
+
- `hstack` - Horizontal stacking with spacing
|
|
195
|
+
- `card` - Bordered container with padding
|
|
196
|
+
|
|
197
|
+
### Modals & Dialogs
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
app "Modal Demo" do
|
|
201
|
+
button "Open Settings" do |s|
|
|
202
|
+
s[:settings_open] = true
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
modal :settings, title: "Settings", size: :lg do
|
|
206
|
+
text_field :api_key, placeholder: "Enter API key"
|
|
207
|
+
checkbox :dark_mode, "Enable dark mode"
|
|
208
|
+
|
|
209
|
+
modal_footer do
|
|
210
|
+
button "Save" do |s|
|
|
211
|
+
s[:settings_open] = false
|
|
212
|
+
# Save logic here
|
|
213
|
+
end
|
|
214
|
+
button "Cancel", style: :secondary do |s|
|
|
215
|
+
s[:settings_open] = false
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end.run!
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+

|
|
223
|
+
<!-- Screenshot: Open modal dialog over main content -->
|
|
224
|
+
|
|
225
|
+
### Feedback Components
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
app "Feedback Demo" do
|
|
229
|
+
state[:status] ||= nil
|
|
230
|
+
|
|
231
|
+
button "Submit" do |s|
|
|
232
|
+
# Simulate operation
|
|
233
|
+
s[:status] = rand > 0.5 ? :success : :error
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
case state[:status]
|
|
237
|
+
when :success
|
|
238
|
+
alert(variant: :success) { text "Operation completed successfully!" }
|
|
239
|
+
when :error
|
|
240
|
+
alert(variant: :error) { text "Something went wrong. Please try again." }
|
|
241
|
+
end
|
|
242
|
+
end.run!
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+

|
|
246
|
+
<!-- Screenshot: Success and error alerts -->
|
|
247
|
+
|
|
248
|
+
**Alert variants:** `:info`, `:success`, `:warning`, `:error`
|
|
249
|
+
|
|
250
|
+
## Common Patterns
|
|
251
|
+
|
|
252
|
+
### Conditional Display
|
|
253
|
+
|
|
254
|
+
Show different UI based on state—a pattern that feels natural in Ruby:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
app "Login Flow" do
|
|
258
|
+
if state[:authenticated]
|
|
259
|
+
text "Welcome back, #{state[:username]}!"
|
|
260
|
+
|
|
261
|
+
button "Logout" do |s|
|
|
262
|
+
s[:authenticated] = false
|
|
263
|
+
s[:username] = nil
|
|
264
|
+
end
|
|
265
|
+
else
|
|
266
|
+
text_field :username, placeholder: "Username"
|
|
267
|
+
text_field :password, placeholder: "Password"
|
|
268
|
+
|
|
269
|
+
button "Login" do |s|
|
|
270
|
+
# Validate credentials
|
|
271
|
+
s[:authenticated] = true
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end.run!
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+

|
|
278
|
+
<!-- Screenshot: Side-by-side of logged out and logged in states -->
|
|
279
|
+
|
|
280
|
+
### Dynamic Lists
|
|
281
|
+
|
|
282
|
+
Build, modify, and display lists with standard Ruby iteration:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
app "Todo List" do
|
|
286
|
+
state[:todos] ||= []
|
|
287
|
+
|
|
288
|
+
text_field :new_todo, placeholder: "What needs doing?"
|
|
289
|
+
|
|
290
|
+
button "Add" do |s|
|
|
291
|
+
if s[:new_todo].to_s.strip != ""
|
|
292
|
+
s[:todos] << { text: s[:new_todo], done: false }
|
|
293
|
+
s[:new_todo] = ""
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
state[:todos].each_with_index do |todo, i|
|
|
298
|
+
hstack do
|
|
299
|
+
checkbox :"done_#{i}", "" do |s, checked|
|
|
300
|
+
s[:todos][i][:done] = checked
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
text todo[:text],
|
|
304
|
+
style: todo[:done] ? "text-decoration: line-through; opacity: 0.6" : ""
|
|
305
|
+
|
|
306
|
+
button "X", style: :danger, size: :sm do |s|
|
|
307
|
+
s[:todos].delete_at(i)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
if state[:todos].any?
|
|
313
|
+
text "#{state[:todos].count { |t| t[:done] }} of #{state[:todos].size} completed"
|
|
314
|
+
end
|
|
315
|
+
end.run!
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+

|
|
319
|
+
<!-- Screenshot: Todo list with items in various states -->
|
|
320
|
+
|
|
321
|
+
### Multi-Step Wizards
|
|
322
|
+
|
|
323
|
+
Guide users through complex flows:
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
app "Setup Wizard" do
|
|
327
|
+
state[:step] ||= 1
|
|
328
|
+
|
|
329
|
+
case state[:step]
|
|
330
|
+
when 1
|
|
331
|
+
header "Step 1: Account"
|
|
332
|
+
text_field :email, placeholder: "Email"
|
|
333
|
+
text_field :name, placeholder: "Full name"
|
|
334
|
+
|
|
335
|
+
button "Next" do |s|
|
|
336
|
+
s[:step] = 2
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
when 2
|
|
340
|
+
header "Step 2: Preferences"
|
|
341
|
+
select :theme, ["Light", "Dark", "System"]
|
|
342
|
+
checkbox :notifications, "Enable notifications"
|
|
343
|
+
|
|
344
|
+
hstack do
|
|
345
|
+
button "Back", style: :secondary do |s|
|
|
346
|
+
s[:step] = 1
|
|
347
|
+
end
|
|
348
|
+
button "Next" do |s|
|
|
349
|
+
s[:step] = 3
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
when 3
|
|
354
|
+
header "Step 3: Confirm"
|
|
355
|
+
text "Email: #{state[:email]}"
|
|
356
|
+
text "Name: #{state[:name]}"
|
|
357
|
+
text "Theme: #{state[:theme]}"
|
|
358
|
+
text "Notifications: #{state[:notifications] ? 'Yes' : 'No'}"
|
|
359
|
+
|
|
360
|
+
hstack do
|
|
361
|
+
button "Back", style: :secondary do |s|
|
|
362
|
+
s[:step] = 2
|
|
363
|
+
end
|
|
364
|
+
button "Complete Setup" do |s|
|
|
365
|
+
s[:complete] = true
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
if state[:complete]
|
|
371
|
+
alert(variant: :success) { text "Setup complete! Welcome aboard." }
|
|
372
|
+
end
|
|
373
|
+
end.run!
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+

|
|
377
|
+
<!-- Screenshot: Multi-step wizard showing step 2 -->
|
|
378
|
+
|
|
379
|
+
## Agentic Mode: One-Shot UIs
|
|
380
|
+
|
|
381
|
+
StreamWeaver isn't just for persistent applications. It excels at one-shot UIs where you need user input mid-script:
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
# Script that needs user input
|
|
385
|
+
result = app "Quick Survey" do
|
|
386
|
+
header "Before we continue..."
|
|
387
|
+
|
|
388
|
+
text_field :project_name, placeholder: "Project name"
|
|
389
|
+
select :priority, ["Low", "Medium", "High", "Critical"]
|
|
390
|
+
text_area :notes, placeholder: "Any additional notes?"
|
|
391
|
+
end.run_once!(auto_close_window: true)
|
|
392
|
+
|
|
393
|
+
# Script continues with the data
|
|
394
|
+
puts "Creating project: #{result['project_name']}"
|
|
395
|
+
puts "Priority: #{result['priority']}"
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+

|
|
399
|
+
<!-- Screenshot: One-shot form in browser -->
|
|
400
|
+
|
|
401
|
+
The browser opens, the user fills the form, submits, and the window closes. Your script receives a hash of the form values and continues execution.
|
|
402
|
+
|
|
403
|
+
This is particularly powerful for AI agents that need human input mid-task—configuration, confirmation, or parameter selection.
|
|
404
|
+
|
|
405
|
+
## App Configuration
|
|
406
|
+
|
|
407
|
+
Customize your app's appearance:
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
app "Dashboard",
|
|
411
|
+
layout: :wide, # :default, :wide, :full, :fluid
|
|
412
|
+
theme: :dashboard # :default, :dashboard, :document
|
|
413
|
+
do
|
|
414
|
+
# Wide layout for data-heavy interfaces
|
|
415
|
+
# ...
|
|
416
|
+
end.run!
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+

|
|
420
|
+
<!-- Screenshot: Side-by-side of default and wide layouts -->
|
|
421
|
+
|
|
422
|
+
## Under the Hood
|
|
423
|
+
|
|
424
|
+
StreamWeaver's architecture is intentionally simple:
|
|
425
|
+
|
|
426
|
+
- **Backend:** Sinatra server handles requests
|
|
427
|
+
- **Rendering:** Phlex generates HTML
|
|
428
|
+
- **Reactivity:** Alpine.js manages client-side state
|
|
429
|
+
- **Updates:** HTMX swaps content on state changes
|
|
430
|
+
- **State:** Server-side, persisted in session cookies
|
|
431
|
+
|
|
432
|
+
When a user interacts with the UI:
|
|
433
|
+
1. HTMX sends a request with the new state
|
|
434
|
+
2. Sinatra receives and stores the state
|
|
435
|
+
3. Your Ruby block re-executes with updated state
|
|
436
|
+
4. Phlex renders new HTML
|
|
437
|
+
5. HTMX swaps the content in the browser
|
|
438
|
+
|
|
439
|
+
No WebSocket complexity. No client-side state management. Just HTTP and HTML—the technologies that have powered the web for 30 years.
|
|
440
|
+
|
|
441
|
+
## Installation
|
|
442
|
+
|
|
443
|
+
Add to your Gemfile:
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
gem 'stream_weaver'
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Or install directly:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
gem install stream_weaver
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Requirements:** Ruby 3.1+
|
|
456
|
+
|
|
457
|
+
## Source Code
|
|
458
|
+
|
|
459
|
+
StreamWeaver is open source and available on GitHub:
|
|
460
|
+
|
|
461
|
+
| Resource | Link |
|
|
462
|
+
|----------|------|
|
|
463
|
+
| GitHub | [github.com/fkchang/stream_weaver](https://github.com/fkchang/stream_weaver) |
|
|
464
|
+
| Documentation | `docs/` directory |
|
|
465
|
+
| Examples | `examples/` directory |
|
|
466
|
+
|
|
467
|
+
## What's Next
|
|
468
|
+
|
|
469
|
+
StreamWeaver is actively developed. On the roadmap:
|
|
470
|
+
|
|
471
|
+
- **Charts integration** - Data visualization components
|
|
472
|
+
- **Table component** - Sortable, filterable data tables
|
|
473
|
+
- **File upload** - Drag-and-drop file handling
|
|
474
|
+
- **Service mode** - Run multiple apps from a single server
|
|
475
|
+
- **Theming system** - Custom CSS and color schemes
|
|
476
|
+
|
|
477
|
+
## Let's Build Together
|
|
478
|
+
|
|
479
|
+
I built StreamWeaver because I wanted the joy of Ruby for every interface I create—not just the ones worth setting up a full stack for.
|
|
480
|
+
|
|
481
|
+
If you share that vision, I'd love your input:
|
|
482
|
+
|
|
483
|
+
- **Try it** - Build something, however small
|
|
484
|
+
- **Break it** - File issues when things don't work
|
|
485
|
+
- **Extend it** - PRs welcome for new components
|
|
486
|
+
- **Share it** - Show what you've built
|
|
487
|
+
|
|
488
|
+
The best DSLs emerge from real use. Every dashboard you build, every tool you create, every experiment you run helps shape what StreamWeaver becomes.
|
|
489
|
+
|
|
490
|
+
## A Renaissance in Ruby Tooling
|
|
491
|
+
|
|
492
|
+
StreamWeaver joins a wave of projects making Ruby development more delightful:
|
|
493
|
+
|
|
494
|
+
- **Hotwire** - Interactive features without JavaScript
|
|
495
|
+
- **Phlex** - Type-safe HTML generation
|
|
496
|
+
- **Charm Ruby** - Beautiful terminal UIs
|
|
497
|
+
- **Prism** - Modern Ruby parser
|
|
498
|
+
|
|
499
|
+
Ruby isn't just surviving—it's thriving. We're building the tools we've always wanted, with the expressiveness we've always loved.
|
|
500
|
+
|
|
501
|
+
StreamWeaver is my contribution to that renaissance. I hope it saves you the hours I've spent on internal tools, and lets you focus on what matters: the problems you're solving, not the frameworks you're configuring.
|
|
502
|
+
|
|
503
|
+
Now go build something beautiful.
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
*StreamWeaver: Reactive Ruby UIs. No JavaScript required.*
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
require 'stream_weaver'
|
|
511
|
+
|
|
512
|
+
app "Your App" do
|
|
513
|
+
# Your beautiful UI here
|
|
514
|
+
end.run!
|
|
515
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# BUG: two canvas bridges racing for one hardcoded socket path - the later one silently wins, the earlier one goes unreachable
|
|
2
|
+
|
|
3
|
+
**Status:** open, worth a dedicated look
|
|
4
|
+
**Severity:** medium - not exploitable, but it's a silent-misroute waiting to happen: a push intended for one
|
|
5
|
+
bridge lands on a completely different one with no error
|
|
6
|
+
**Filed:** 2026-08-31, from an agent task in billing_engine that needed to push an edited doc to a specific
|
|
7
|
+
one of two live bridges (ports 4700 and 4701) and confirmed the wrong one received it
|
|
8
|
+
**Area:** canvas bridge (`bridge_server.rb#start_unix_socket_server`, `#write_pid_file`) /
|
|
9
|
+
`canvas/client.rb` (hardcoded `SOCKET_PATH` / `PID_FILE_PATH`) / `cli.rb#canvas_push`
|
|
10
|
+
|
|
11
|
+
## Symptom
|
|
12
|
+
|
|
13
|
+
`StreamWeaver::Canvas::Client::SOCKET_PATH` and `PID_FILE_PATH` are process-wide constants
|
|
14
|
+
(`~/.streamweaver/canvas.sock`, `~/.streamweaver/canvas.pid`), overridable only via
|
|
15
|
+
`STREAMWEAVER_CANVAS_SOCKET` / `STREAMWEAVER_CANVAS_PID` env vars. When two `BridgeServer.run!` processes
|
|
16
|
+
start on the same machine without those env vars set (e.g. two independent `streamweaver panel` sessions on
|
|
17
|
+
different ports), both call `start_unix_socket_server`, which unconditionally does
|
|
18
|
+
`File.delete(socket_path) if File.exist?(socket_path)` then binds a fresh `UNIXServer` at that same path -
|
|
19
|
+
and both call `write_pid_file`, which unconditionally overwrites the pid file. Whichever process starts
|
|
20
|
+
**later** wins: it deletes the earlier process's live socket out from under it and overwrites the pid file
|
|
21
|
+
to point at itself. There is no check for "is this socket already owned by a live process" - it's a bare
|
|
22
|
+
delete-and-rebind race, not a refusal.
|
|
23
|
+
|
|
24
|
+
`canvas-push` (`cli.rb#canvas_push` -> `Canvas::Client.send_message`) always connects via the default
|
|
25
|
+
socket path, with no `--socket` / `--port` selector. So once the race resolves, **every** `canvas-push`
|
|
26
|
+
call - regardless of which bridge's URL the user is actually looking at - goes to whichever process won,
|
|
27
|
+
silently. The loser keeps serving HTTP on its own port, but its Unix-socket listener is orphaned: its
|
|
28
|
+
sessions become permanently unreachable for pushes until it's restarted, with no error surfaced anywhere.
|
|
29
|
+
|
|
30
|
+
## Evidence
|
|
31
|
+
|
|
32
|
+
Two `puma` processes observed live: pid 58524 on port 4700 (started 00:31:09), pid 58674 on port 4701
|
|
33
|
+
(started 00:31:15, six seconds later). `~/.streamweaver/canvas.pid` read `pid=58674\nport=4701` - the
|
|
34
|
+
later process. `lsof -p <pid>` on both showed 58674 holding the listening end of
|
|
35
|
+
`~/.streamweaver/canvas.sock` (multiple accepted connections), while 58524 held a single stale fd to the
|
|
36
|
+
same path with nothing actually listening on its behalf.
|
|
37
|
+
|
|
38
|
+
Confirmed behaviorally, not just via lsof: `curl http://localhost:4700/health` and
|
|
39
|
+
`curl http://localhost:4701/health` returned two **different** session lists (4700: 3 sessions; 4701: 15
|
|
40
|
+
sessions - each bridge's own independent in-memory session store, as expected for two separate processes).
|
|
41
|
+
A `canvas-push pm-discount-eng-brief` (default socket, no override) landed its new content on port 4701's
|
|
42
|
+
session, confirmed via `/canvas/:name` page-content diff before/after the push and via `/canvas/:name/poll`
|
|
43
|
+
returning empty `html` for port 4700's same-named session throughout - it had been structurally empty since
|
|
44
|
+
6 seconds after boot, because it never got a chance to receive a single push.
|
|
45
|
+
|
|
46
|
+
## Why it matters
|
|
47
|
+
|
|
48
|
+
1. **Silent misroute, not a failure.** `canvas-push` exits 0 and prints `Pushed to <name>` regardless of
|
|
49
|
+
which bridge actually received it. A user (or an agent) watching a specific port's browser tab has no
|
|
50
|
+
signal that their push went somewhere else.
|
|
51
|
+
2. **Data can land on the wrong session store.** In the observed case, a bridge deliberately kept separate
|
|
52
|
+
for parked reference copies (many unrelated sessions) received a push meant for a different, narrower
|
|
53
|
+
working bridge - purely because of six seconds of startup ordering, nothing about intent.
|
|
54
|
+
3. **Gets worse with GEA-style concurrency.** Multiple simultaneous `streamweaver panel` sessions (Forrest's
|
|
55
|
+
normal 20+-concurrent-session operating mode) make this race routine, not an edge case.
|
|
56
|
+
|
|
57
|
+
## Suggested fix
|
|
58
|
+
|
|
59
|
+
- Derive the socket path (and pid file path) from the bridge's own port by default, e.g.
|
|
60
|
+
`~/.streamweaver/canvas-<port>.sock` / `canvas-<port>.pid`, instead of one shared hardcoded path across
|
|
61
|
+
every bridge process on the machine.
|
|
62
|
+
- Add a `--socket PATH` / `--port PORT` selector to `canvas-push` (and any other CLI command that talks to
|
|
63
|
+
a bridge) so a caller can address a specific bridge unambiguously instead of always hitting "whatever the
|
|
64
|
+
default socket currently resolves to."
|
|
65
|
+
- Make `start_unix_socket_server` refuse to start (or at least warn loudly) when the target socket path is
|
|
66
|
+
already owned by a live process, rather than deleting and rebinding over it. A stale socket (owning
|
|
67
|
+
process dead) should still be reclaimed - the current stale-cleanup behavior is fine - but a **live**
|
|
68
|
+
owner should not be silently evicted.
|
|
69
|
+
|
|
70
|
+
## Acceptance criteria for a fix
|
|
71
|
+
|
|
72
|
+
- Two bridge processes started on different ports, with no env override, do not collide: both remain
|
|
73
|
+
independently reachable for `canvas-push` for the lifetime of both processes.
|
|
74
|
+
- `canvas-push` against a specific bridge (via its new selector, or by construction once sockets are
|
|
75
|
+
per-port) always lands on the session store of the bridge the caller intended, demonstrable with the same
|
|
76
|
+
two-bridge shape observed here.
|
|
77
|
+
- Starting a second bridge that WOULD collide under the old scheme either binds its own distinct socket
|
|
78
|
+
automatically, or fails fast with a clear message - never a silent takeover.
|
|
79
|
+
|
|
80
|
+
## Provenance
|
|
81
|
+
|
|
82
|
+
Observed live during a billing_engine doc-editing task (`pm-discount-eng-brief` canvas session), 2026-08-31.
|
|
83
|
+
Diagnosed via `/health` and `/canvas/:name/poll` diffs across ports 4700 and 4701, pids 58524 and 58674.
|