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,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: visual-recap
|
|
3
|
+
description: Use after finishing a task (or mid-task for incremental updates) — renders a live canvas showing what changed, why, and the key design choices made during implementation
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Visual Recap
|
|
7
|
+
|
|
8
|
+
Post-implementation canvas recap. Pushes a live visual summary of what actually changed: files touched, the most significant diffs, annotated complex code, and design choices flagged during the work — all in one persistent canvas that evolves with the task.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
After any task where:
|
|
13
|
+
- Multiple files changed
|
|
14
|
+
- A design decision was made
|
|
15
|
+
- The user wants to see what happened, not just a text summary
|
|
16
|
+
- You're mid-task and want to show progress without finishing
|
|
17
|
+
|
|
18
|
+
Push at task start (showing intended changes), update mid-task, finalize when done. The canvas evolves — it is not a one-shot publish.
|
|
19
|
+
|
|
20
|
+
Skip for trivial one-liner edits where a text summary is faster than a canvas.
|
|
21
|
+
|
|
22
|
+
## Why StreamWeaver vs Static Recap
|
|
23
|
+
|
|
24
|
+
Builder.io's visual-recap and similar tools generate a static artifact at the end of a task — one snapshot, one publish, frozen. StreamWeaver's recap is **live and incremental**:
|
|
25
|
+
|
|
26
|
+
- Push at the start with the intended change map
|
|
27
|
+
- Update after each batch of files
|
|
28
|
+
- Add diff blocks as significant changes land
|
|
29
|
+
- Finalize with annotated code and design decisions
|
|
30
|
+
|
|
31
|
+
The user sees real progress. Mid-task pivots are visible. The canvas reflects the actual task state, not a post-hoc reconstruction.
|
|
32
|
+
|
|
33
|
+
## !! DO NOT LAUNCH STANDALONE SERVERS PER UPDATE !!
|
|
34
|
+
|
|
35
|
+
**NEVER** run `ruby app.rb` or `streamweaver <file.rb>` for each recap update. Use `canvas-push` to update a single persistent window throughout the session.
|
|
36
|
+
|
|
37
|
+
## Step 1 — Get the Changed Files
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# After finishing: diff since the last commit
|
|
41
|
+
git diff HEAD~1 --name-only
|
|
42
|
+
|
|
43
|
+
# Mid-task or across a feature branch:
|
|
44
|
+
git diff main..HEAD --name-only
|
|
45
|
+
|
|
46
|
+
# For uncommitted changes:
|
|
47
|
+
git diff --name-only
|
|
48
|
+
git status --short
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Parse the output to build a `{ path:, note: }` list. The `note:` should explain **why** the file changed, not what the file is. Write it now — don't defer to Step 3.
|
|
52
|
+
|
|
53
|
+
Example mapping:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
lib/stream_weaver/components/callout.rb → "Added :decision and :risk tone variants"
|
|
57
|
+
spec/components/callout_spec.rb → "Cover new tone variants, edge cases for missing title"
|
|
58
|
+
lib/stream_weaver/display_dsl.rb → "Expose callout shorthand with default variant: :info"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Step 2 — Start the Canvas
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
streamweaver panel recap
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This opens a persistent canvas named `recap` in an iTerm2 split pane (or browser tab elsewhere). The URL is printed to stdout — tell the user where to look.
|
|
68
|
+
|
|
69
|
+
## Step 3 — Push the Implementation Map
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
streamweaver canvas-push recap <<'RUBY'
|
|
73
|
+
header1 "Recap: <task name>"
|
|
74
|
+
|
|
75
|
+
implementation_map(files: [
|
|
76
|
+
{ path: "lib/stream_weaver/components/callout.rb",
|
|
77
|
+
note: "Added :decision and :risk tone variants with icon and border color" },
|
|
78
|
+
{ path: "spec/components/callout_spec.rb",
|
|
79
|
+
note: "Cover new tone variants and missing-title edge case" },
|
|
80
|
+
{ path: "lib/stream_weaver/display_dsl.rb",
|
|
81
|
+
note: "Expose callout shorthand: default variant :info, no block required" }
|
|
82
|
+
])
|
|
83
|
+
RUBY
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`note:` is the rationale — **why this file changed**, not just what it contains. One sentence.
|
|
87
|
+
|
|
88
|
+
## Step 4 — Push Diffs for the 2-3 Most Significant Changes
|
|
89
|
+
|
|
90
|
+
Pick the files where the change is non-obvious from the file name. Push a `diff` block per file:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
streamweaver canvas-push recap <<'RUBY'
|
|
94
|
+
header2 "Key Changes"
|
|
95
|
+
|
|
96
|
+
diff(language: "ruby") do
|
|
97
|
+
before do
|
|
98
|
+
<<~CODE
|
|
99
|
+
def callout(variant: :info, title: nil, &block)
|
|
100
|
+
@components << Components::Callout.new(variant: variant, title: title, &block)
|
|
101
|
+
end
|
|
102
|
+
CODE
|
|
103
|
+
end
|
|
104
|
+
after do
|
|
105
|
+
<<~CODE
|
|
106
|
+
def callout(variant: :info, title: nil, **options, &block)
|
|
107
|
+
with_container(Components::Callout.new(variant: variant, title: title, **options), &block)
|
|
108
|
+
end
|
|
109
|
+
CODE
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
RUBY
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Limit to 2-3 diffs. If more files changed significantly, pick the ones that tell the story of the task. The `before`/`after` content should be the actual code — copy from the diff, don't paraphrase.
|
|
116
|
+
|
|
117
|
+
## Step 5 — Annotated Code for the Most Complex Change (Optional)
|
|
118
|
+
|
|
119
|
+
If one file has a non-obvious implementation worth explaining:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
streamweaver canvas-push recap <<'RUBY'
|
|
123
|
+
header2 "Implementation Detail"
|
|
124
|
+
|
|
125
|
+
annotated_code(
|
|
126
|
+
language: "ruby",
|
|
127
|
+
annotations: [
|
|
128
|
+
{ line: 3, label: "Delegates to with_container",
|
|
129
|
+
note: "Allows callout to hold nested DSL components — text, md, badge, etc." },
|
|
130
|
+
{ line: 7, label: "Variant maps to CSS class",
|
|
131
|
+
note: "sw-callout-decision, sw-callout-risk, sw-callout-info — defined in callout.css" },
|
|
132
|
+
{ line: 12, label: "Title is optional",
|
|
133
|
+
note: "Renders as a <strong> header when present; omitted entirely if nil" }
|
|
134
|
+
]
|
|
135
|
+
) do
|
|
136
|
+
<<~CODE
|
|
137
|
+
def callout(variant: :info, title: nil, **options, &block)
|
|
138
|
+
with_container(
|
|
139
|
+
Components::Callout.new(variant: variant, title: title, **options),
|
|
140
|
+
&block
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class Callout
|
|
145
|
+
VARIANT_CLASSES = { info: "sw-callout-info", decision: "sw-callout-decision",
|
|
146
|
+
risk: "sw-callout-risk", tip: "sw-callout-tip" }
|
|
147
|
+
# ...
|
|
148
|
+
def render_title
|
|
149
|
+
strong { @title } if @title
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
CODE
|
|
153
|
+
end
|
|
154
|
+
RUBY
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Skip this step if the implementation is straightforward.
|
|
158
|
+
|
|
159
|
+
## Step 6 — Flag Design Choices
|
|
160
|
+
|
|
161
|
+
Push a `callout(:decision)` for each meaningful design decision made during the task:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
streamweaver canvas-push recap <<'RUBY'
|
|
165
|
+
header2 "Design Choices"
|
|
166
|
+
|
|
167
|
+
callout(variant: :decision, title: "with_container instead of direct push") do
|
|
168
|
+
text "Callout now accepts nested DSL calls (text, md, badge). Using with_container lets the"
|
|
169
|
+
text "component collect children from the block the same way card and columns do."
|
|
170
|
+
text "Alternative: parse the block as a string. Rejected — loses type safety and child ordering."
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
callout(variant: :decision, title: ":info as the default variant") do
|
|
174
|
+
text "Matches the most common use case (informational callouts). Callers who want :risk or"
|
|
175
|
+
text ":decision must opt in — no accidental risk styling on plain callouts."
|
|
176
|
+
end
|
|
177
|
+
RUBY
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
One `callout(:decision)` per meaningful choice. If you made a risky call, use `callout(:risk)` instead.
|
|
181
|
+
|
|
182
|
+
## Incremental Updates (Mid-Task)
|
|
183
|
+
|
|
184
|
+
You do not have to wait until a task is done. Update the recap as you go:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# After the first batch of files:
|
|
188
|
+
streamweaver canvas-push recap <<'RUBY'
|
|
189
|
+
header1 "Recap: Add Callout Tones — In Progress"
|
|
190
|
+
|
|
191
|
+
implementation_map(files: [
|
|
192
|
+
{ path: "lib/stream_weaver/components/callout.rb", note: "[done] Tone variants added" },
|
|
193
|
+
{ path: "spec/components/callout_spec.rb", note: "[wip]" },
|
|
194
|
+
{ path: "lib/stream_weaver/display_dsl.rb", note: "[pending]" }
|
|
195
|
+
])
|
|
196
|
+
RUBY
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Use `[done]`, `[wip]`, and `[pending]` prefixes in the `note:` field to show live status.
|
|
200
|
+
|
|
201
|
+
## Combining Steps in One Push
|
|
202
|
+
|
|
203
|
+
For small tasks, push everything in a single canvas update:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
streamweaver canvas-push recap <<'RUBY'
|
|
207
|
+
header1 "Recap: Add Callout Tones"
|
|
208
|
+
|
|
209
|
+
implementation_map(files: [
|
|
210
|
+
{ path: "lib/stream_weaver/components/callout.rb",
|
|
211
|
+
note: "Added :decision and :risk tone variants with icon and border color" },
|
|
212
|
+
{ path: "spec/components/callout_spec.rb",
|
|
213
|
+
note: "Cover new tone variants and missing-title edge case" }
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
diff(language: "ruby") do
|
|
217
|
+
before { "def callout(variant: :info, &block)\n ..." }
|
|
218
|
+
after { "def callout(variant: :info, **options, &block)\n with_container(...)\nend" }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
callout(variant: :decision, title: "with_container for nested DSL") do
|
|
222
|
+
text "Allows callout to contain text, md, badge — matches card/columns pattern."
|
|
223
|
+
end
|
|
224
|
+
RUBY
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## DSL Reference
|
|
228
|
+
|
|
229
|
+
| Method | Purpose |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `implementation_map(files: [...])` | File-to-rationale map; `note:` is the why |
|
|
232
|
+
| `diff(language:) { before { } after { } }` | Side-by-side code diff |
|
|
233
|
+
| `annotated_code(language:, annotations: [...]) { }` | Code with line-pinned notes |
|
|
234
|
+
| `callout(variant: :decision, title: "...") { }` | Design choice flag |
|
|
235
|
+
| `callout(variant: :risk, title: "...") { }` | Risk or caveat flag |
|
|
236
|
+
|
|
237
|
+
## Known Gotchas
|
|
238
|
+
|
|
239
|
+
- DSL method is `diff`, not `diff_block` — `diff_block` does not exist
|
|
240
|
+
- `annotated_code` line numbers are 1-indexed; they match the code block you supply, not the original file
|
|
241
|
+
- `callout` block must use `text` or `md` — nesting `diff` or `implementation_map` inside a callout throws
|
|
242
|
+
- `spacer` and `divider` don't exist — use `div(style: "height:Npx")` for spacing
|
|
243
|
+
- StreamWeaver auto-selects a port — capture the URL from stdout, don't hardcode 4567
|
|
244
|
+
- If `undefined method` errors appear, the bridge is stale: `streamweaver canvas-stop && streamweaver panel recap`
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module StreamWeaver
|
|
6
|
+
# Thread-safe SSE connection manager with Turbo Stream-like actions.
|
|
7
|
+
# Manages browser SSE connections and broadcasts targeted DOM updates.
|
|
8
|
+
#
|
|
9
|
+
# @example Internal usage (from app stream block)
|
|
10
|
+
# streamer.replace("#metric-rps", "<div>3500</div>")
|
|
11
|
+
#
|
|
12
|
+
# @example External usage (via POST /stream/push)
|
|
13
|
+
# curl -X POST localhost:4567/stream/push -d 'target=#metric-rps&html=<div>3500</div>'
|
|
14
|
+
class Streamer
|
|
15
|
+
include Pushable
|
|
16
|
+
|
|
17
|
+
ACTIONS = %i[replace append prepend remove add_class remove_class].freeze
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@connections = []
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
@shutting_down = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def shutdown!
|
|
26
|
+
@shutting_down = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def shutting_down?
|
|
30
|
+
@shutting_down
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_connection(conn)
|
|
34
|
+
@mutex.synchronize { @connections << conn }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def remove_connection(conn)
|
|
38
|
+
@mutex.synchronize { @connections.delete(conn) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def connection_count
|
|
42
|
+
@mutex.synchronize { @connections.size }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def push_update(action:, target:, html: nil, value: nil)
|
|
48
|
+
payload = { action: action, target: target }
|
|
49
|
+
payload[:html] = html if html
|
|
50
|
+
payload[:value] = value if value
|
|
51
|
+
data = payload.to_json
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
dead = []
|
|
54
|
+
@connections.each do |conn|
|
|
55
|
+
conn << "data: #{data}\n\n"
|
|
56
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Puma::ConnectionError
|
|
57
|
+
dead << conn
|
|
58
|
+
end
|
|
59
|
+
@connections -= dead
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require_relative '../cli'
|
|
7
|
+
|
|
8
|
+
module StreamWeaver
|
|
9
|
+
module Templates
|
|
10
|
+
# Simple choice selector template
|
|
11
|
+
# Shows options as buttons, returns the selected value
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# streamweaver template choices SESSION '{"title":"Pick a color","options":["Red","Green","Blue"]}'
|
|
15
|
+
# # Returns: {"choice":"Green"}
|
|
16
|
+
#
|
|
17
|
+
# # With descriptions:
|
|
18
|
+
# streamweaver template choices SESSION '{
|
|
19
|
+
# "title":"Database",
|
|
20
|
+
# "description":"Which database to use?",
|
|
21
|
+
# "options":[
|
|
22
|
+
# {"label":"PostgreSQL","description":"Best for complex queries"},
|
|
23
|
+
# {"label":"SQLite","description":"Simple, file-based"},
|
|
24
|
+
# {"label":"MySQL","description":"Popular, widely supported"}
|
|
25
|
+
# ]
|
|
26
|
+
# }'
|
|
27
|
+
#
|
|
28
|
+
class Choices
|
|
29
|
+
def initialize(session:, config:, port: nil)
|
|
30
|
+
@session = session
|
|
31
|
+
@config = config
|
|
32
|
+
@port = port || detect_port
|
|
33
|
+
@title = config['title'] || 'Choose an option'
|
|
34
|
+
@description = config['description']
|
|
35
|
+
@options = config['options'] || []
|
|
36
|
+
@run_id = Time.now.to_i.to_s(36)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run
|
|
40
|
+
clear_submissions
|
|
41
|
+
push_choices
|
|
42
|
+
data = wait_for_submission
|
|
43
|
+
|
|
44
|
+
# Extract choice from button id (btn_LABEL_1 format)
|
|
45
|
+
button_id = data['_button'] || ''
|
|
46
|
+
# Button id is like "btn_postgresql_1" - extract the label part
|
|
47
|
+
choice_key = button_id.sub(/^btn_/, '').sub(/_\d+$/, '')
|
|
48
|
+
|
|
49
|
+
# Find matching option
|
|
50
|
+
selected = find_option_by_key(choice_key)
|
|
51
|
+
|
|
52
|
+
{ 'choice' => selected }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def detect_port
|
|
58
|
+
pid_file = File.expand_path('~/.streamweaver/service.pid')
|
|
59
|
+
return 4576 unless File.exist?(pid_file)
|
|
60
|
+
data = JSON.parse(File.read(pid_file))
|
|
61
|
+
data['port'] || 4576
|
|
62
|
+
rescue
|
|
63
|
+
4576
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def find_option_by_key(key)
|
|
67
|
+
@options.each do |opt|
|
|
68
|
+
if opt.is_a?(Hash)
|
|
69
|
+
label = opt['label']
|
|
70
|
+
# Normalize: downcase, replace spaces with underscores
|
|
71
|
+
normalized = label.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
|
|
72
|
+
return label if normalized == key
|
|
73
|
+
else
|
|
74
|
+
normalized = opt.to_s.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
|
|
75
|
+
return opt if normalized == key
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
key # fallback
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def push_choices
|
|
82
|
+
dsl = build_dsl
|
|
83
|
+
html = StreamWeaver::CLI.render_dsl_to_html(dsl, session_name: @session)
|
|
84
|
+
|
|
85
|
+
uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/push")
|
|
86
|
+
request = Net::HTTP::Post.new(uri)
|
|
87
|
+
request.set_form_data('content' => html, 'target' => '#main', 'action' => 'replace')
|
|
88
|
+
|
|
89
|
+
Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_dsl
|
|
93
|
+
lines = []
|
|
94
|
+
lines << "header2 \"#{@title}\""
|
|
95
|
+
lines << "text \"#{@description}\"" if @description
|
|
96
|
+
lines << "card do"
|
|
97
|
+
|
|
98
|
+
@options.each do |opt|
|
|
99
|
+
if opt.is_a?(Hash)
|
|
100
|
+
label = opt['label']
|
|
101
|
+
desc = opt['description']
|
|
102
|
+
lines << " button \"#{label}\""
|
|
103
|
+
lines << " text \"#{desc}\", style: :muted" if desc
|
|
104
|
+
lines << " text \"\""
|
|
105
|
+
else
|
|
106
|
+
lines << " button \"#{opt}\""
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
lines << "end"
|
|
111
|
+
lines.join("\n")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def clear_submissions
|
|
115
|
+
uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
|
|
116
|
+
loop do
|
|
117
|
+
response = Net::HTTP.get_response(uri)
|
|
118
|
+
break unless response.is_a?(Net::HTTPSuccess)
|
|
119
|
+
data = JSON.parse(response.body)
|
|
120
|
+
break if (data['submissions'] || []).empty?
|
|
121
|
+
end
|
|
122
|
+
rescue
|
|
123
|
+
# ignore
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def wait_for_submission(timeout: 300)
|
|
127
|
+
start_time = Time.now
|
|
128
|
+
uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
|
|
129
|
+
|
|
130
|
+
loop do
|
|
131
|
+
raise "Timeout" if Time.now - start_time > timeout
|
|
132
|
+
|
|
133
|
+
response = Net::HTTP.get_response(uri)
|
|
134
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
135
|
+
data = JSON.parse(response.body)
|
|
136
|
+
submissions = data['submissions'] || []
|
|
137
|
+
return submissions.first['data'] if submissions.any?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
sleep 0.3
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.run(session:, config:, port: nil)
|
|
145
|
+
new(session: session, config: config, port: port).run
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require_relative '../cli'
|
|
7
|
+
|
|
8
|
+
module StreamWeaver
|
|
9
|
+
module Templates
|
|
10
|
+
# Code display template
|
|
11
|
+
# Shows code with optional filename and actions
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# streamweaver template code SESSION '{
|
|
15
|
+
# "title": "Generated Code",
|
|
16
|
+
# "filename": "app.rb",
|
|
17
|
+
# "language": "ruby",
|
|
18
|
+
# "code": "def hello\n puts \"Hello\"\nend",
|
|
19
|
+
# "actions": ["Copy", "Apply", "Cancel"]
|
|
20
|
+
# }'
|
|
21
|
+
# # Returns: {"action": "Apply"}
|
|
22
|
+
#
|
|
23
|
+
class Code
|
|
24
|
+
def initialize(session:, config:, port: nil)
|
|
25
|
+
@session = session
|
|
26
|
+
@config = config
|
|
27
|
+
@port = port || detect_port
|
|
28
|
+
@title = config['title']
|
|
29
|
+
@filename = config['filename']
|
|
30
|
+
@language = config['language'] || 'text'
|
|
31
|
+
@code = config['code'] || ''
|
|
32
|
+
@actions = config['actions'] || ['OK']
|
|
33
|
+
@line_numbers = config['line_numbers'] != false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run
|
|
37
|
+
clear_submissions
|
|
38
|
+
push_code
|
|
39
|
+
data = wait_for_submission
|
|
40
|
+
|
|
41
|
+
button_id = data['_button'] || ''
|
|
42
|
+
action = extract_action(button_id)
|
|
43
|
+
{ 'action' => action }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def detect_port
|
|
49
|
+
pid_file = File.expand_path('~/.streamweaver/service.pid')
|
|
50
|
+
return 4576 unless File.exist?(pid_file)
|
|
51
|
+
data = JSON.parse(File.read(pid_file))
|
|
52
|
+
data['port'] || 4576
|
|
53
|
+
rescue
|
|
54
|
+
4576
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extract_action(button_id)
|
|
58
|
+
key = button_id.sub(/^btn_/, '').sub(/_\d+$/, '')
|
|
59
|
+
@actions.each do |action|
|
|
60
|
+
normalized = action.to_s.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
|
|
61
|
+
return action if normalized == key
|
|
62
|
+
end
|
|
63
|
+
key
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def push_code
|
|
67
|
+
# Build HTML directly for better code formatting
|
|
68
|
+
html = build_html
|
|
69
|
+
|
|
70
|
+
uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/push")
|
|
71
|
+
request = Net::HTTP::Post.new(uri)
|
|
72
|
+
request.set_form_data('content' => html, 'target' => '#main', 'action' => 'replace')
|
|
73
|
+
|
|
74
|
+
Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_html
|
|
78
|
+
escaped_code = escape_html(@code)
|
|
79
|
+
|
|
80
|
+
# Add line numbers if enabled
|
|
81
|
+
if @line_numbers
|
|
82
|
+
lines = escaped_code.split("\n")
|
|
83
|
+
numbered_lines = lines.each_with_index.map do |line, i|
|
|
84
|
+
line_num = (i + 1).to_s.rjust(3)
|
|
85
|
+
"<span style='color: #666; user-select: none;'>#{line_num}</span> #{line}"
|
|
86
|
+
end
|
|
87
|
+
escaped_code = numbered_lines.join("\n")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
buttons_html = @actions.map.with_index do |action, i|
|
|
91
|
+
variant_class = i == 0 ? 'sw-btn-primary' : 'sw-btn-secondary'
|
|
92
|
+
btn_id = "btn_#{action.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')}_#{i + 1}"
|
|
93
|
+
%(<button id="#{btn_id}" class="sw-btn #{variant_class}"
|
|
94
|
+
hx-post="/live/#{@session}/action/#{btn_id}"
|
|
95
|
+
hx-include="[x-model]"
|
|
96
|
+
hx-target="#main"
|
|
97
|
+
hx-swap="innerHTML">#{escape_html(action)}</button>)
|
|
98
|
+
end.join("\n ")
|
|
99
|
+
|
|
100
|
+
<<~HTML
|
|
101
|
+
<div id="main" x-data="{}">
|
|
102
|
+
#{@title ? "<h2 style='margin-bottom: 1rem;'>#{escape_html(@title)}</h2>" : ''}
|
|
103
|
+
<div class="sw-card" style="padding: 0; overflow: hidden;">
|
|
104
|
+
#{@filename ? "<div style='padding: 0.5rem 1rem; background: var(--sw-color-bg); border-bottom: 1px solid var(--sw-color-border); font-size: 0.875rem; color: var(--sw-color-text-muted);'>#{escape_html(@filename)}</div>" : ''}
|
|
105
|
+
<pre style="margin: 0; padding: 1rem; background: #1e1e1e; color: #d4d4d4; overflow-x: auto; font-family: 'SF Mono', Consolas, monospace; font-size: 0.875rem; line-height: 1.5;"><code>#{escaped_code}</code></pre>
|
|
106
|
+
</div>
|
|
107
|
+
<div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
|
|
108
|
+
#{buttons_html}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
HTML
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def escape_html(text)
|
|
115
|
+
text.to_s
|
|
116
|
+
.gsub('&', '&')
|
|
117
|
+
.gsub('<', '<')
|
|
118
|
+
.gsub('>', '>')
|
|
119
|
+
.gsub('"', '"')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def clear_submissions
|
|
123
|
+
uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
|
|
124
|
+
loop do
|
|
125
|
+
response = Net::HTTP.get_response(uri)
|
|
126
|
+
break unless response.is_a?(Net::HTTPSuccess)
|
|
127
|
+
data = JSON.parse(response.body)
|
|
128
|
+
break if (data['submissions'] || []).empty?
|
|
129
|
+
end
|
|
130
|
+
rescue
|
|
131
|
+
# ignore
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def wait_for_submission(timeout: 300)
|
|
135
|
+
start_time = Time.now
|
|
136
|
+
uri = URI("http://localhost:#{@port}/live/#{URI.encode_www_form_component(@session)}/submissions")
|
|
137
|
+
|
|
138
|
+
loop do
|
|
139
|
+
raise "Timeout" if Time.now - start_time > timeout
|
|
140
|
+
|
|
141
|
+
response = Net::HTTP.get_response(uri)
|
|
142
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
143
|
+
data = JSON.parse(response.body)
|
|
144
|
+
submissions = data['submissions'] || []
|
|
145
|
+
return submissions.first['data'] if submissions.any?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
sleep 0.3
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.run(session:, config:, port: nil)
|
|
153
|
+
new(session: session, config: config, port: port).run
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|