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,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} <<'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/<name>.rb</code> (or <code>~/.streamweaver/canvas/</code> outside a git repo).</span>
|
|
332
|
+
<span x-show="format === 'org'">Writes a human-readable <code><name>.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()">×</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
|