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,460 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module StreamWeaver
|
|
7
|
+
# iTerm2 integration for panel workflow via the optional iterm2_ruby gem
|
|
8
|
+
# (`gem install iterm2_ruby` — https://rubygems.org/gems/iterm2_ruby).
|
|
9
|
+
# Falls back to system browser when iTerm2 API is unavailable.
|
|
10
|
+
class ITerm
|
|
11
|
+
# iTerm2's built-in profile that renders a web view instead of a shell.
|
|
12
|
+
BROWSER_PROFILE = "Web Browser"
|
|
13
|
+
|
|
14
|
+
# Default window frames (points) for the two windows this class ever
|
|
15
|
+
# creates from scratch -- the University controller (narrow, tall) and a
|
|
16
|
+
# worker tab that couldn't inherit the caller's window (wide, short).
|
|
17
|
+
# Each is overridable via env as "x,y,width,height".
|
|
18
|
+
CONTROLLER_FRAME_DEFAULTS = { x: 40, y: 40, width: 760, height: 1200 }.freeze
|
|
19
|
+
WORKER_FRAME_DEFAULTS = { x: 80, y: 80, width: 1600, height: 1000 }.freeze
|
|
20
|
+
|
|
21
|
+
# Bracketed paste (DEC 2004). A TUI that has enabled the mode reads
|
|
22
|
+
# everything between these two sequences as one pasted block and takes
|
|
23
|
+
# the block whole, rather than interpreting each byte as it arrives --
|
|
24
|
+
# which is what lets the Return that follows read as a keypress.
|
|
25
|
+
PASTE_START = "\e[200~"
|
|
26
|
+
PASTE_END = "\e[201~"
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
def available?
|
|
30
|
+
return @available if defined?(@available)
|
|
31
|
+
@available = check_availability
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# iTerm2's real value always has the shape "w0t0p0:<UUID>" -- the part
|
|
35
|
+
# after the colon is the session id every RPC in this file actually
|
|
36
|
+
# wants. A value with no colon at all (missing entirely, or garbled by
|
|
37
|
+
# something outside iTerm2) is never that shape, so it returns nil
|
|
38
|
+
# rather than passing the raw, meaningless string through as if it
|
|
39
|
+
# were a real session id (code review, round-9 UAT: `focus-me` calls
|
|
40
|
+
# this blind on every course prompt and must never mistake garbage
|
|
41
|
+
# for a target).
|
|
42
|
+
def current_session_guid
|
|
43
|
+
raw = ENV["ITERM_SESSION_ID"]
|
|
44
|
+
return nil unless raw&.include?(":")
|
|
45
|
+
|
|
46
|
+
guid = raw.split(":", 2).last
|
|
47
|
+
guid.empty? ? nil : guid
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# True only when the user is inside iTerm2 on macOS but the optional gem
|
|
51
|
+
# isn't installed — the one case where a "gem install iterm2_ruby" hint
|
|
52
|
+
# is actionable rather than noise.
|
|
53
|
+
def gem_missing?
|
|
54
|
+
!available? &&
|
|
55
|
+
RbConfig::CONFIG["host_os"].match?(/darwin/) &&
|
|
56
|
+
!ENV["ITERM_SESSION_ID"].to_s.empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Split a browser pane with the URL into `target_session` (any session
|
|
60
|
+
# id, e.g. a just-created worker tab's session -- not necessarily the
|
|
61
|
+
# calling pane). Defaults to the calling session (current_session_guid)
|
|
62
|
+
# when target_session is nil, matching the original single-pane
|
|
63
|
+
# `panel` behavior. Returns Hash with :type (:browser, :external, or
|
|
64
|
+
# nil) and :pane_id.
|
|
65
|
+
def split_vertical_with_url(url, open_browser: true, horizontal: false, target_session: nil)
|
|
66
|
+
return { type: nil, pane_id: nil } unless available?
|
|
67
|
+
|
|
68
|
+
pane_id = split_browser_pane(url, horizontal: horizontal, target_session: target_session)
|
|
69
|
+
|
|
70
|
+
if pane_id
|
|
71
|
+
{ type: :browser, pane_id: pane_id }
|
|
72
|
+
elsif open_browser && !ENV['SW_NO_OPEN']
|
|
73
|
+
system("open", url)
|
|
74
|
+
{ type: :external, pane_id: nil }
|
|
75
|
+
else
|
|
76
|
+
{ type: nil, pane_id: nil }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Opens `url` in an iTerm2 browser pane that owns a WHOLE WINDOW, and
|
|
81
|
+
# returns that browser session's id (nil if it could not be opened) --
|
|
82
|
+
# same shape as open_worker_tab.
|
|
83
|
+
#
|
|
84
|
+
# StreamWeaver University's canvas is a controller, not a sidecar: it
|
|
85
|
+
# gets its own window so the worker tab stays free for the agent and
|
|
86
|
+
# the demo canvas panes the course steps have it create. iTerm2's API
|
|
87
|
+
# has no "new browser window" call, so this composes three primitives
|
|
88
|
+
# that already work elsewhere in this file -- create_tab with no
|
|
89
|
+
# window_id makes a new window, split_pane with the Web Browser
|
|
90
|
+
# profile makes the browser, and the shell that came with the window
|
|
91
|
+
# is then closed so only the canvas is left.
|
|
92
|
+
#
|
|
93
|
+
# A brand new iTerm2 window opens at the profile's default size --
|
|
94
|
+
# unlike open_worker_tab below, this one has no caller's window to
|
|
95
|
+
# reuse (the controller is its own window by design), so it sizes
|
|
96
|
+
# itself explicitly via set_window_frame instead: a narrow controller
|
|
97
|
+
# shape (CONTROLLER_FRAME_DEFAULTS, overridable via SW_CONTROLLER_FRAME).
|
|
98
|
+
# Degrades silently on an older iterm2_ruby that lacks the method.
|
|
99
|
+
def open_browser_window(url)
|
|
100
|
+
return nil unless available?
|
|
101
|
+
|
|
102
|
+
with_timeout(10, default: nil) do
|
|
103
|
+
connect do |c|
|
|
104
|
+
created = c.create_tab
|
|
105
|
+
shell = created && created[:session_id]
|
|
106
|
+
next nil unless shell
|
|
107
|
+
|
|
108
|
+
apply_window_frame(c, created[:window_id], "SW_CONTROLLER_FRAME", CONTROLLER_FRAME_DEFAULTS)
|
|
109
|
+
|
|
110
|
+
pane = browser_pane_in(c, shell, url)
|
|
111
|
+
|
|
112
|
+
# Either way the shell goes: with a browser it would be clutter
|
|
113
|
+
# beside the canvas, and without one it is a window the user
|
|
114
|
+
# never asked for and cannot account for. Closing the only
|
|
115
|
+
# session takes the window with it. Scoped so a close that
|
|
116
|
+
# fails can never cost us a working pane's id -- losing that
|
|
117
|
+
# would leave a canvas window nothing can close later.
|
|
118
|
+
close_quietly(c, shell)
|
|
119
|
+
pane
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
rescue StandardError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def close_pane(pane_id)
|
|
127
|
+
return false unless available? && pane_id
|
|
128
|
+
connect { |c| c.close_session(pane_id, force: true) }
|
|
129
|
+
rescue ITerm2::Error
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Brings an already-open pane to the front. Public wrapper around the
|
|
134
|
+
# private, connection-scoped activate_session_quietly used by the
|
|
135
|
+
# raise-on-run path (send_to_session, above) -- this is the same
|
|
136
|
+
# primitive for a caller that isn't already inside a `connect` block
|
|
137
|
+
# (`streamweaver canvas-raise`, round-7 UAT: a worker-initiated canvas
|
|
138
|
+
# push, unlike a Run submit, has nothing that raises it, and `panel`
|
|
139
|
+
# can't be called twice without splitting a second pane). Fire-and-
|
|
140
|
+
# forget, same as the private version: false only means "could not
|
|
141
|
+
# even try", not "did not land".
|
|
142
|
+
def activate_session(session_id)
|
|
143
|
+
return false unless available? && session_id
|
|
144
|
+
|
|
145
|
+
connect { |c| activate_session_quietly(c, session_id) }
|
|
146
|
+
true
|
|
147
|
+
rescue StandardError
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def split_vertical_with_command(command)
|
|
152
|
+
return false unless available?
|
|
153
|
+
|
|
154
|
+
guid = current_session_guid or return false
|
|
155
|
+
|
|
156
|
+
connect do |c|
|
|
157
|
+
new_id = c.split_pane(guid, vertical: true)
|
|
158
|
+
c.send_text(new_id, "#{command}\n")
|
|
159
|
+
end
|
|
160
|
+
true
|
|
161
|
+
rescue ITerm2::Error
|
|
162
|
+
false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def navigate_browser(session_id, url)
|
|
166
|
+
return false unless available? && session_id
|
|
167
|
+
connect { |c| c.set_profile_property(session_id, "Initial URL", url) }
|
|
168
|
+
rescue ITerm2::Error
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Attempts an actual connection to iTerm2's Python API server (osascript
|
|
173
|
+
# auth handshake + websocket upgrade) -- distinct from `available?`,
|
|
174
|
+
# which only confirms the gem itself is installed/loadable. The one
|
|
175
|
+
# case this tells apart: gem installed, but iTerm2's own
|
|
176
|
+
# Preferences > General > Magic > Enable Python API toggle is off.
|
|
177
|
+
def python_api_reachable?
|
|
178
|
+
return false unless available?
|
|
179
|
+
with_timeout(5, default: false) { connect { true } }
|
|
180
|
+
rescue StandardError
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Create a new iTerm2 tab, cd into `dir` (defaults to the caller's cwd),
|
|
185
|
+
# then run `command` in it (e.g. the `claude` or `codex` CLI). A new
|
|
186
|
+
# iTerm2 tab starts in the user's home directory, not the caller's --
|
|
187
|
+
# without the cd, the agent launches at ~ instead of the project it was
|
|
188
|
+
# invoked from. Returns the new tab's primary session_id, or nil if
|
|
189
|
+
# unavailable or the RPC failed.
|
|
190
|
+
def open_worker_tab(command, dir: Dir.pwd)
|
|
191
|
+
return nil unless available?
|
|
192
|
+
with_timeout(8, default: nil) do
|
|
193
|
+
connect do |c|
|
|
194
|
+
# In the caller's own window when we can find it -- the window
|
|
195
|
+
# the user is already sitting in is by definition the size they
|
|
196
|
+
# chose, so that path leaves the frame untouched apart from
|
|
197
|
+
# growing it up to the worker minimum (and the calling PANE is
|
|
198
|
+
# untouched either way, which is the actual product promise).
|
|
199
|
+
# Only the fallback path -- a genuinely new window -- gets an
|
|
200
|
+
# explicit wide frame via set_window_frame, since a brand new
|
|
201
|
+
# iTerm2 window otherwise opens at the profile's default size
|
|
202
|
+
# (UAT found that unusably small twice over).
|
|
203
|
+
window_id = calling_window_id(c)
|
|
204
|
+
result = window_id ? c.create_tab(window_id: window_id) : c.create_tab
|
|
205
|
+
session_id = result && result[:session_id]
|
|
206
|
+
if session_id
|
|
207
|
+
window_id ? ensure_min_worker_frame(c, window_id) : apply_window_frame(c, result[:window_id], "SW_WORKER_FRAME", WORKER_FRAME_DEFAULTS)
|
|
208
|
+
c.send_text(session_id, "cd #{Shellwords.escape(dir)} && #{command}\n")
|
|
209
|
+
end
|
|
210
|
+
session_id
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
rescue StandardError
|
|
214
|
+
nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# --- Driver adapter -------------------------------------------------
|
|
218
|
+
# The surface adapter above puts a canvas beside a terminal; these two
|
|
219
|
+
# put text *into* one specific terminal. StreamWeaver University's
|
|
220
|
+
# runner uses them to send a step's prompt to the worker session
|
|
221
|
+
# `get-started` recorded, and only that one -- see
|
|
222
|
+
# lib/stream_weaver/university/runner.rb.
|
|
223
|
+
|
|
224
|
+
# Types `text` into exactly `session_id`, then presses Return unless
|
|
225
|
+
# `submit: false`. Never falls back to the calling session: a
|
|
226
|
+
# mistargeted send drops a prompt into whatever pane the user happens
|
|
227
|
+
# to be looking at, which is the failure this whole path exists to
|
|
228
|
+
# prevent.
|
|
229
|
+
#
|
|
230
|
+
# Return is a carriage return, not a line feed -- that is what a
|
|
231
|
+
# terminal actually sends when a human presses the key, and a raw-mode
|
|
232
|
+
# TUI (the `claude` / `codex` CLIs) does not read an LF as submit.
|
|
233
|
+
#
|
|
234
|
+
# It also goes in a write of its OWN. UAT 2026-08-31: with the CR
|
|
235
|
+
# appended to the same write, the prompt appeared in the claude pane
|
|
236
|
+
# and just sat there -- the TUI read the whole write as pasted text,
|
|
237
|
+
# so the CR became a character in the composer rather than a keypress.
|
|
238
|
+
# A real Return is a separate event, so we send a separate one.
|
|
239
|
+
#
|
|
240
|
+
# UAT 2026-09-03 (round 3): a separate CR write still did not submit.
|
|
241
|
+
# The text write is therefore wrapped in bracketed paste markers, so
|
|
242
|
+
# the TUI knows exactly where the pasted block ends -- without them it
|
|
243
|
+
# keeps treating the following bytes as more of the same paste, and
|
|
244
|
+
# the CR is swallowed as paste content rather than read as Return.
|
|
245
|
+
# Only the text is bracketed; the CR must arrive OUTSIDE the block or
|
|
246
|
+
# the whole exercise is moot.
|
|
247
|
+
#
|
|
248
|
+
# Owning the keystroke here is what lets callers hand over prompt text
|
|
249
|
+
# and nothing terminal-shaped; a later herdr/cmux driver makes the
|
|
250
|
+
# same promise its own way.
|
|
251
|
+
def send_to_session(session_id, text, submit: true)
|
|
252
|
+
return false unless available? && session_id
|
|
253
|
+
|
|
254
|
+
with_timeout(8, default: false) do
|
|
255
|
+
connect do |c|
|
|
256
|
+
next false unless c.send_text(session_id, bracketed_paste(text))
|
|
257
|
+
next true unless submit
|
|
258
|
+
next false unless c.send_text(session_id, "\r")
|
|
259
|
+
|
|
260
|
+
# A successful submit is exactly the moment the worker's tab and
|
|
261
|
+
# window should come to front -- Forrest's UAT wants Run to feel
|
|
262
|
+
# like handing control to the agent, not typing into a pane he
|
|
263
|
+
# has to go find. Only a full send (text landed AND Return
|
|
264
|
+
# landed) gets here; a refused, degraded, or half-sent attempt
|
|
265
|
+
# returns false above and never raises anything. Fire-and-forget:
|
|
266
|
+
# no poll to confirm it landed (see activate_session_quietly).
|
|
267
|
+
activate_session_quietly(c, session_id)
|
|
268
|
+
true
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
rescue StandardError
|
|
272
|
+
false
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# True only when `session_id` is still in iTerm2's live topology --
|
|
276
|
+
# the check that distinguishes "the worker tab is there" from "the
|
|
277
|
+
# user closed it an hour ago". False on any doubt (gem missing, API
|
|
278
|
+
# unreachable, RPC error), so callers degrade rather than guess.
|
|
279
|
+
def session_alive?(session_id)
|
|
280
|
+
return false unless available? && session_id
|
|
281
|
+
|
|
282
|
+
with_timeout(5, default: false) do
|
|
283
|
+
connect { |c| c.topology.any? { |s| s[:session_id] == session_id } }
|
|
284
|
+
end
|
|
285
|
+
rescue StandardError
|
|
286
|
+
false
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
APP_NAME = "StreamWeaver"
|
|
292
|
+
|
|
293
|
+
# `text` as one bracketed-paste block. A terminal that has not enabled
|
|
294
|
+
# the mode drops the markers on the floor, so this is safe to apply
|
|
295
|
+
# unconditionally rather than sniffing what is running in the pane.
|
|
296
|
+
def bracketed_paste(text) = "#{PASTE_START}#{text}#{PASTE_END}"
|
|
297
|
+
|
|
298
|
+
# The browser pane for `url`, or nil. The real client raises on a
|
|
299
|
+
# failed split (and returns nil only for an OK response with no
|
|
300
|
+
# session), so both spellings of failure are caught here rather than
|
|
301
|
+
# unwinding past the shell cleanup above.
|
|
302
|
+
def browser_pane_in(client, shell, url)
|
|
303
|
+
client.split_pane(
|
|
304
|
+
shell,
|
|
305
|
+
vertical: true,
|
|
306
|
+
profile_name: BROWSER_PROFILE,
|
|
307
|
+
profile_customizations: { "Initial URL" => url }
|
|
308
|
+
)
|
|
309
|
+
rescue StandardError
|
|
310
|
+
nil
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def close_quietly(client, session_id)
|
|
314
|
+
client.close_session(session_id, force: true)
|
|
315
|
+
rescue StandardError
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Sets window_id's frame to defaults, overridden by an "x,y,width,height"
|
|
320
|
+
# value in ENV[env_var] when present and parseable. Silently a no-op on
|
|
321
|
+
# an iterm2_ruby too old to have set_window_frame (one stderr hint,
|
|
322
|
+
# printed once) -- callers don't need to know which gem version is
|
|
323
|
+
# installed.
|
|
324
|
+
def apply_window_frame(client, window_id, env_var, defaults)
|
|
325
|
+
return unless window_id
|
|
326
|
+
|
|
327
|
+
unless client.respond_to?(:set_window_frame)
|
|
328
|
+
warn_frame_unsupported_once
|
|
329
|
+
return
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
frame = parse_frame_env(ENV[env_var], defaults)
|
|
333
|
+
client.set_window_frame(window_id, **frame)
|
|
334
|
+
rescue StandardError
|
|
335
|
+
nil
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def parse_frame_env(raw, defaults)
|
|
339
|
+
return defaults if raw.to_s.empty?
|
|
340
|
+
|
|
341
|
+
parts = raw.split(",").map(&:strip)
|
|
342
|
+
return defaults unless parts.size == 4
|
|
343
|
+
|
|
344
|
+
x, y, width, height = parts.map { |p| Integer(p, exception: false) }
|
|
345
|
+
return defaults if [x, y, width, height].any?(&:nil?)
|
|
346
|
+
|
|
347
|
+
{ x: x, y: y, width: width, height: height }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def warn_frame_unsupported_once
|
|
351
|
+
return if @frame_hint_shown
|
|
352
|
+
@frame_hint_shown = true
|
|
353
|
+
warn "StreamWeaver: window sizing needs iterm2_ruby >= 0.3.0 (gem install iterm2_ruby)"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Grows window_id up to the worker minimum (WORKER_FRAME_DEFAULTS,
|
|
357
|
+
# overridable via SW_WORKER_FRAME) when the caller's own window --
|
|
358
|
+
# reused rather than created by open_worker_tab -- is smaller than
|
|
359
|
+
# that on either axis. Keeps the window's current x,y: this is a
|
|
360
|
+
# floor, not a reposition. Never shrinks a window already at or above
|
|
361
|
+
# the minimum, and never touches set_window_frame at all in that
|
|
362
|
+
# case. Degrades silently on an older iterm2_ruby that lacks
|
|
363
|
+
# get_window_frame.
|
|
364
|
+
def ensure_min_worker_frame(client, window_id)
|
|
365
|
+
return unless client.respond_to?(:get_window_frame)
|
|
366
|
+
|
|
367
|
+
current = client.get_window_frame(window_id)
|
|
368
|
+
return unless current && current[:width] && current[:height]
|
|
369
|
+
|
|
370
|
+
minimum = parse_frame_env(ENV["SW_WORKER_FRAME"], WORKER_FRAME_DEFAULTS)
|
|
371
|
+
return if current[:width] >= minimum[:width] && current[:height] >= minimum[:height]
|
|
372
|
+
|
|
373
|
+
client.set_window_frame(
|
|
374
|
+
window_id,
|
|
375
|
+
x: current[:x],
|
|
376
|
+
y: current[:y],
|
|
377
|
+
width: [current[:width], minimum[:width]].max,
|
|
378
|
+
height: [current[:height], minimum[:height]].max
|
|
379
|
+
)
|
|
380
|
+
rescue StandardError
|
|
381
|
+
nil
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Best-effort, fire-and-forget: brings the worker's tab and window to
|
|
385
|
+
# the front after a step prompt actually goes out, so Run feels like
|
|
386
|
+
# handing control to the agent rather than something the user has to
|
|
387
|
+
# go find. Never polls to confirm the raise actually landed (the gem
|
|
388
|
+
# author flagged iterm2_ruby's own client.focus as unreliable with
|
|
389
|
+
# multiple windows open) -- fire it and move on. The respond_to?
|
|
390
|
+
# check is cheap insurance, not a version gate: activate_session has
|
|
391
|
+
# shipped since iterm2_ruby 0.1.0, so this only degrades on a client
|
|
392
|
+
# missing it altogether. One memoized stderr hint either way; never
|
|
393
|
+
# raises past this, since a raise failing is not a reason to report
|
|
394
|
+
# the send itself as failed.
|
|
395
|
+
def activate_session_quietly(client, session_id)
|
|
396
|
+
unless client.respond_to?(:activate_session)
|
|
397
|
+
warn_activate_unsupported_once
|
|
398
|
+
return
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
client.activate_session(session_id)
|
|
402
|
+
rescue StandardError
|
|
403
|
+
nil
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def warn_activate_unsupported_once
|
|
407
|
+
return if @activate_hint_shown
|
|
408
|
+
@activate_hint_shown = true
|
|
409
|
+
warn "StreamWeaver: raise-on-run needs an iterm2_ruby client with activate_session (gem install iterm2_ruby)"
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# The window holding the session this process is running in, or nil if
|
|
413
|
+
# it can't be determined (not in iTerm2, or the lookup failed). Callers
|
|
414
|
+
# treat nil as "open a new window".
|
|
415
|
+
def calling_window_id(client)
|
|
416
|
+
guid = current_session_guid or return nil
|
|
417
|
+
client.topology.find { |s| s[:session_id] == guid }&.dig(:window_id)
|
|
418
|
+
rescue StandardError
|
|
419
|
+
nil
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def check_availability
|
|
423
|
+
return false unless RbConfig::CONFIG["host_os"].match?(/darwin/)
|
|
424
|
+
# Fast check: ITERM_SESSION_ID is set when running inside iTerm2.
|
|
425
|
+
# Avoids the slow ITerm2.connect call (which spawns a Python subprocess
|
|
426
|
+
# and can hang for seconds when the Python API is slow to initialize).
|
|
427
|
+
return false unless ENV['ITERM_SESSION_ID']
|
|
428
|
+
|
|
429
|
+
require "iterm2"
|
|
430
|
+
true
|
|
431
|
+
rescue LoadError
|
|
432
|
+
false
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def connect(&) = ITerm2.connect(app_name: APP_NAME, &)
|
|
436
|
+
|
|
437
|
+
def split_browser_pane(url, horizontal: false, target_session: nil)
|
|
438
|
+
guid = target_session || current_session_guid or return nil
|
|
439
|
+
with_timeout(8, default: nil) do
|
|
440
|
+
connect do |c|
|
|
441
|
+
c.split_pane(
|
|
442
|
+
guid,
|
|
443
|
+
vertical: !horizontal,
|
|
444
|
+
profile_name: BROWSER_PROFILE,
|
|
445
|
+
profile_customizations: { "Initial URL" => url }
|
|
446
|
+
)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
rescue ITerm2::Error
|
|
450
|
+
nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def with_timeout(seconds, default: nil)
|
|
454
|
+
Timeout.timeout(seconds) { yield }
|
|
455
|
+
rescue Timeout::Error
|
|
456
|
+
default
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
# Registry for named page layouts.
|
|
5
|
+
#
|
|
6
|
+
# A layout controls the outer chrome (topbar, sidebar, main area) of a StreamWeaver app.
|
|
7
|
+
# The built-in `:default` layout is the standard single-column centered body.
|
|
8
|
+
# Custom layouts can opt out of the default chrome with `exclusive: true`.
|
|
9
|
+
#
|
|
10
|
+
# @example Registering a custom layout
|
|
11
|
+
# StreamWeaver.register_layout(:two_rail, exclusive: true, body_classes: %w[my-shell]) do
|
|
12
|
+
# div(class: "my-header") { render_slot(:header) }
|
|
13
|
+
# div(class: "my-content") do
|
|
14
|
+
# div(class: "my-sidebar") { render_slot(:sidebar_left) }
|
|
15
|
+
# main_content_region
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Using it in an app
|
|
20
|
+
# app "X", layout: :two_rail do
|
|
21
|
+
# layout_slot(:header) { text "Header content" }
|
|
22
|
+
# layout_slot(:sidebar_left) { text "Sidebar content" }
|
|
23
|
+
# text "Main content"
|
|
24
|
+
# end
|
|
25
|
+
module LayoutRegistry
|
|
26
|
+
CHROMELESS_ENTRY = {
|
|
27
|
+
exclusive: true,
|
|
28
|
+
# sw-chromeless carries a minimal, token-driven padding baseline (see
|
|
29
|
+
# views.rb) so bare content isn't glued to the viewport edge -- still
|
|
30
|
+
# overridable to zero by an app's own CSS (FAC-9u2).
|
|
31
|
+
body_classes: ["sw-chromeless"].freeze,
|
|
32
|
+
css_path: nil,
|
|
33
|
+
render_block: proc { main_content_region }
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
@registry = {}
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Register a named layout.
|
|
40
|
+
#
|
|
41
|
+
# @param name [Symbol, String] Layout identifier used as `layout:` in app(...)
|
|
42
|
+
# @param exclusive [Boolean] When true, suppresses default h1/body-padding/#app-container chrome
|
|
43
|
+
# @param body_classes [Array<String>] Extra classes added to <body> (in addition to sw-theme-*)
|
|
44
|
+
# @param css_path [String, nil] Absolute path to a CSS file for this layout
|
|
45
|
+
# @yield Block that is instance_exec'd on AppView — use Phlex DSL + render_slot/main_content_region
|
|
46
|
+
def register(name, exclusive: false, body_classes: [], css_path: nil, &render_block)
|
|
47
|
+
entry = {
|
|
48
|
+
exclusive: exclusive,
|
|
49
|
+
body_classes: Array(body_classes),
|
|
50
|
+
css_path: css_path,
|
|
51
|
+
render_block: render_block
|
|
52
|
+
}
|
|
53
|
+
ComponentAssets.register_file(css_path) if css_path
|
|
54
|
+
@registry[name.to_sym] = entry
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def [](name)
|
|
58
|
+
@registry[name.to_sym]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def registered?(name)
|
|
62
|
+
@registry.key?(name.to_sym)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def all
|
|
66
|
+
@registry.dup
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def chromeless
|
|
70
|
+
CHROMELESS_ENTRY
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reset!
|
|
74
|
+
@registry.clear
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class << self
|
|
80
|
+
# Register a named page layout.
|
|
81
|
+
#
|
|
82
|
+
# @param name [Symbol, String] Layout identifier
|
|
83
|
+
# @param exclusive [Boolean] Opt out of default header/padding/#app-container chrome
|
|
84
|
+
# @param body_classes [Array<String>] Extra <body> classes
|
|
85
|
+
# @param css_path [String, nil] Layout-specific CSS file path
|
|
86
|
+
# @yield Phlex DSL block (instance_exec'd on AppView); call render_slot/main_content_region
|
|
87
|
+
def register_layout(name, exclusive: false, body_classes: [], css_path: nil, &block)
|
|
88
|
+
LayoutRegistry.register(name, exclusive: exclusive, body_classes: body_classes,
|
|
89
|
+
css_path: css_path, &block)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "env"
|
|
4
|
+
|
|
5
|
+
module StreamWeaver
|
|
6
|
+
module Opal
|
|
7
|
+
# Wires an OpalRuntime to window.SWRuntime in the browser.
|
|
8
|
+
# Registers delegated event listeners so no inline JS is needed in HTML.
|
|
9
|
+
#
|
|
10
|
+
# Every line below needs a DOM -- `window`, `document`, and event
|
|
11
|
+
# delegation -- so installing is a no-op where there is none. That is what
|
|
12
|
+
# lets `app()` stay a single unconditional call site while the same bundle
|
|
13
|
+
# also loads in Node, where the string path (StringBridge) takes over.
|
|
14
|
+
class OpalBridge
|
|
15
|
+
def initialize(runtime)
|
|
16
|
+
@runtime = runtime
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def install
|
|
20
|
+
# :nocov:
|
|
21
|
+
return unless Env.dom?
|
|
22
|
+
runtime = @runtime
|
|
23
|
+
%x{
|
|
24
|
+
window.SWRuntime = {
|
|
25
|
+
start: function() {
|
|
26
|
+
#{runtime.render_and_patch}
|
|
27
|
+
document.addEventListener('click', function(e) {
|
|
28
|
+
var el = e.target.closest('[data-sw-invoke]');
|
|
29
|
+
if (el) #{runtime.invoke_and_patch(`el.dataset.swInvoke`)};
|
|
30
|
+
});
|
|
31
|
+
document.addEventListener('click', function(e) {
|
|
32
|
+
var el = e.target.closest('[data-sw-action]');
|
|
33
|
+
if (el && el.dataset.swAction === 'toggle-theme') {
|
|
34
|
+
if (typeof swToggleTheme === 'function') swToggleTheme();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
document.addEventListener('input', function(e) {
|
|
38
|
+
var key = e.target.dataset && e.target.dataset.swUpdate;
|
|
39
|
+
if (key) #{runtime.update_and_patch(`key`, `e.target.value`)};
|
|
40
|
+
});
|
|
41
|
+
document.addEventListener('change', function(e) {
|
|
42
|
+
var key = e.target.dataset && e.target.dataset.swToggle;
|
|
43
|
+
if (key) #{runtime.update_and_patch(`key`, `e.target.checked`)};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
# :nocov:
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|