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,487 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
# Raised when a named action's token no longer matches the app's current
|
|
5
|
+
# action definitions (code reload changed the registered action set) or
|
|
6
|
+
# the session's action generation (server restart / session reset).
|
|
7
|
+
# server.rb's dev-mode fallback overlay (dev-loud-failure-overlay) reads
|
|
8
|
+
# #action/#fragment/#cause off the raised instance to name the stale
|
|
9
|
+
# target and likely cause -- kwargs are optional so existing
|
|
10
|
+
# `raise StaleActionDefinition` / `rescue StaleActionDefinition` call
|
|
11
|
+
# sites (and specs) that don't care about the detail keep working.
|
|
12
|
+
class StaleActionDefinition < StandardError
|
|
13
|
+
attr_reader :action, :fragment, :cause
|
|
14
|
+
|
|
15
|
+
def initialize(msg = nil, action: nil, fragment: nil, cause: nil)
|
|
16
|
+
@action = action
|
|
17
|
+
@fragment = fragment
|
|
18
|
+
@cause = cause
|
|
19
|
+
super(msg || "stale action token#{" for action #{action.inspect}" if action}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Owns the state-to-response pipeline for every interactive request.
|
|
24
|
+
#
|
|
25
|
+
# Updates and events dispatch against descriptors/components retained from the
|
|
26
|
+
# tree sent to the browser, so they need only the post-interaction rebuild.
|
|
27
|
+
# Legacy block buttons and forms still need a discovery rebuild because their
|
|
28
|
+
# executable closures are created while the DSL is evaluated. FAC-P1.3 named
|
|
29
|
+
# actions will remove that discovery rebuild for buttons.
|
|
30
|
+
class InteractionRunner
|
|
31
|
+
# The framework-wide routing params, plus the ones only interaction dispatch
|
|
32
|
+
# puts on the wire.
|
|
33
|
+
ROUTE_PARAMS = (App::ROUTE_OWNED_PARAMS + %w[key form_name _sw_fragment]).freeze
|
|
34
|
+
|
|
35
|
+
def initialize(app:, state:, params:, interaction:, target: nil, adapter:, agentic: false,
|
|
36
|
+
persist:, prepare_state: nil, result_container: nil, auto_close: false,
|
|
37
|
+
action_manifest: nil, generation: "0", persist_manifest: nil,
|
|
38
|
+
response_headers: nil, state_version: 0, persist_state_version: nil)
|
|
39
|
+
@app = app
|
|
40
|
+
@state = state
|
|
41
|
+
@params = params
|
|
42
|
+
@interaction = interaction.to_sym
|
|
43
|
+
@target = target
|
|
44
|
+
@adapter = adapter
|
|
45
|
+
@agentic = agentic
|
|
46
|
+
@persist = persist
|
|
47
|
+
@prepare_state = prepare_state
|
|
48
|
+
@result_container = result_container
|
|
49
|
+
@auto_close = auto_close
|
|
50
|
+
@action_manifest = action_manifest || Set.new
|
|
51
|
+
@generation = generation.to_s
|
|
52
|
+
@persist_manifest = persist_manifest
|
|
53
|
+
@response_headers = response_headers
|
|
54
|
+
@state_version = state_version.to_i
|
|
55
|
+
@persist_state_version = persist_state_version
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def call
|
|
59
|
+
app.with_render_lock do
|
|
60
|
+
# For named actions (discovery_required? false below), this is still
|
|
61
|
+
# the retained tree from the last rebuild -- i.e. exactly what's live
|
|
62
|
+
# in the browser right now, before this interaction's mutation. Row
|
|
63
|
+
# narrowing (#row_narrowing) diffs a table found in here against the
|
|
64
|
+
# same table after the final rebuild to prove a mutation is row-local.
|
|
65
|
+
components_before = app.components
|
|
66
|
+
before_state = deep_copy(state)
|
|
67
|
+
prepare_state&.call(state)
|
|
68
|
+
prepare_form_state if interaction == :form
|
|
69
|
+
rebuild if discovery_required?
|
|
70
|
+
|
|
71
|
+
unless interaction == :form
|
|
72
|
+
sync_params_to_state
|
|
73
|
+
handle_unchecked_checkboxes
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
dispatch
|
|
77
|
+
persist.call(state)
|
|
78
|
+
|
|
79
|
+
completion_response || begin
|
|
80
|
+
rebuild
|
|
81
|
+
persist_manifest&.call(app.render_state.action_tokens)
|
|
82
|
+
scoped_response(before_state, components_before) || Views::AppContentView.new(app, state, adapter, agentic).call
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
attr_reader :app, :state, :params, :interaction, :target, :adapter, :agentic,
|
|
90
|
+
:persist, :prepare_state, :result_container, :auto_close,
|
|
91
|
+
:action_manifest, :generation, :persist_manifest
|
|
92
|
+
|
|
93
|
+
def response_headers = @response_headers
|
|
94
|
+
def state_version = @state_version
|
|
95
|
+
def persist_state_version = @persist_state_version
|
|
96
|
+
|
|
97
|
+
def deep_copy(value)
|
|
98
|
+
Marshal.load(Marshal.dump(value))
|
|
99
|
+
rescue TypeError
|
|
100
|
+
value.transform_values { |item| item.dup rescue item }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Every rebuild in this request names the fragment the request is scoped to,
|
|
104
|
+
# so a `defer: true` fragment the client is fetching executes its block on
|
|
105
|
+
# this pass instead of rendering its placeholder again (see App#fragment).
|
|
106
|
+
# The name comes off the signed scope token, so it is the server's own
|
|
107
|
+
# claim, not the browser's.
|
|
108
|
+
def rebuild
|
|
109
|
+
app.rebuild_with_state(state, generation: generation, state_version: state_version,
|
|
110
|
+
deferred_target: declared_scope&.fetch(:fragment))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def scoped_response(before_state, components_before)
|
|
114
|
+
scope = declared_scope
|
|
115
|
+
if named_action? && !@named_action_authorized
|
|
116
|
+
response_headers&.call("HX-Retarget" => "#app-container")
|
|
117
|
+
return Views::AppContentView.new(app, state, adapter, agentic).call
|
|
118
|
+
end
|
|
119
|
+
if fragment_param_present? && !scope
|
|
120
|
+
response_headers&.call("HX-Retarget" => "#app-container")
|
|
121
|
+
return Views::AppContentView.new(app, state, adapter, agentic).call
|
|
122
|
+
end
|
|
123
|
+
return unless scope
|
|
124
|
+
|
|
125
|
+
if app.route_key && before_state[app.route_key] != state[app.route_key]
|
|
126
|
+
response_headers&.call("HX-Retarget" => "#app-container")
|
|
127
|
+
return Views::AppContentView.new(app, state, adapter, agentic).call
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
fragment = find_fragment(app.components, scope[:fragment])
|
|
131
|
+
unless fragment
|
|
132
|
+
response_headers&.call("HX-Retarget" => "#app-container")
|
|
133
|
+
return Views::AppContentView.new(app, state, adapter, agentic).call
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
extras = scope[:updates].filter_map { |name| find_fragment_by_name(app.components, name) }
|
|
137
|
+
if extras.length != scope[:updates].length
|
|
138
|
+
response_headers&.call("HX-Retarget" => "#app-container")
|
|
139
|
+
return Views::AppContentView.new(app, state, adapter, agentic).call
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# `primary: :fragment_name` (stream_weaver-78a): a button can name a
|
|
143
|
+
# sibling fragment as the response's actual primary swap target
|
|
144
|
+
# instead of its own enclosing fragment -- e.g. a board's "View"
|
|
145
|
+
# button refreshing a detail pane without resending the whole board.
|
|
146
|
+
# Resolved against the post-rebuild tree exactly like `updates:`; a
|
|
147
|
+
# name that no longer matches any fragment (conditionally removed by
|
|
148
|
+
# this very interaction) falls back to a full-container response
|
|
149
|
+
# rather than silently doing nothing.
|
|
150
|
+
primary_fragment = nil
|
|
151
|
+
if scope[:primary]
|
|
152
|
+
primary_fragment = find_fragment_by_name(app.components, scope[:primary])
|
|
153
|
+
unless primary_fragment
|
|
154
|
+
response_headers&.call("HX-Retarget" => "#app-container")
|
|
155
|
+
return Views::AppContentView.new(app, state, adapter, agentic).call
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
primary_content = primary_fragment || fragment
|
|
159
|
+
|
|
160
|
+
# Flash OOB under scoped responses (stream_weaver-m3t). A fragment
|
|
161
|
+
# named `:flash` is StreamWeaver's flash-placement convention
|
|
162
|
+
# (`fragment(:flash) { flash_messages }`); any scoped response that
|
|
163
|
+
# just set a flash message auto-delivers that fragment as an extra OOB
|
|
164
|
+
# swap so authors don't have to remember `updates: [:flash]` on every
|
|
165
|
+
# single action for flash to ever be visible.
|
|
166
|
+
unless (state[:_flash] || {}).empty?
|
|
167
|
+
flash_fragment = find_fragment_by_name(app.components, "flash")
|
|
168
|
+
extras += [flash_fragment] if flash_fragment && !extras.include?(flash_fragment)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A deferred fragment's auto-fetch carries no state patch. Every deferred
|
|
172
|
+
# fragment on a page fires its `load` trigger in the same tick, so all
|
|
173
|
+
# their requests leave with the same `sw_state_version` cookie and would
|
|
174
|
+
# each claim version N+1 -- the client applies the first and reloads on
|
|
175
|
+
# the rest (see the `patch.version !== currentVersion + 1` guard in the
|
|
176
|
+
# adapter's swap hook), which reloads into the same race. A deferred fetch
|
|
177
|
+
# dispatches nothing (see #dispatch's `:update` branch) and returns values
|
|
178
|
+
# the client already holds, so it has no patch to skip.
|
|
179
|
+
unless deferred_fetch?
|
|
180
|
+
version = state_version + 1
|
|
181
|
+
persist_state_version&.call(version)
|
|
182
|
+
patch = state_patch(before_state, state, version)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Row-granular table swaps (stream_weaver-95k). A named action's row
|
|
186
|
+
# mutation can be narrowed whether the table it touched is the primary
|
|
187
|
+
# target fragment *or* one of the declared `updates:` extras -- the
|
|
188
|
+
# common shape is actually the latter (an "Add" button living in a
|
|
189
|
+
# toolbar fragment, with `updates: :the_table_fragment`), so both are
|
|
190
|
+
# analyzed independently and each narrows or falls back on its own.
|
|
191
|
+
# Skipped when `primary:` redirects the response to an unrelated
|
|
192
|
+
# sibling fragment -- narrowing is a size optimization only (never a
|
|
193
|
+
# state-semantics change), so falling back to full fragment content is
|
|
194
|
+
# always correct.
|
|
195
|
+
if named_action? && components_before && !primary_fragment
|
|
196
|
+
row_swap = row_swap_for(components_before, primary_content)
|
|
197
|
+
extra_swaps = extras.map { |extra| row_swap_for(components_before, extra) }
|
|
198
|
+
end
|
|
199
|
+
extra_swaps ||= []
|
|
200
|
+
|
|
201
|
+
if row_swap
|
|
202
|
+
response_headers&.call(primary_swap_headers(row_swap))
|
|
203
|
+
return Views::RowSwapView.new(app, state, adapter, updates: extras, extra_swaps: extra_swaps,
|
|
204
|
+
state_patch: patch, primary: primary_swap_proc(row_swap)).call
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
response_headers&.call("HX-Retarget" => "##{primary_fragment.id}") if primary_fragment
|
|
208
|
+
|
|
209
|
+
Views::FragmentContentView.new(app, state, adapter, primary_content, updates: extras, extra_swaps: extra_swaps,
|
|
210
|
+
state_patch: patch).call
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Row-local proof for one fragment: does it contain exactly one table
|
|
214
|
+
# with row identity, and does that table's row-id list before/after this
|
|
215
|
+
# interaction's rebuild prove the mutation touched only one row? Same
|
|
216
|
+
# ordered row keys (edit), pre-set minus exactly the actioned key
|
|
217
|
+
# (delete), or pre-set plus one trailing key (create) -- anything else
|
|
218
|
+
# (a sort, a filter, more than one table, unresolvable row keys) returns
|
|
219
|
+
# nil, and the caller falls back to that fragment's full content, which
|
|
220
|
+
# is always correct (point 4 of the design: narrowing is a size
|
|
221
|
+
# optimization only, never a state-semantics change).
|
|
222
|
+
#
|
|
223
|
+
# @param components_before [Array<Components::Base>] The retained tree from before this
|
|
224
|
+
# interaction's mutation (see #call)
|
|
225
|
+
# @param new_fragment [Components::Fragment] The post-mutation fragment (target or extra)
|
|
226
|
+
# @return [Hash, nil] `{ kind:, table:, idx:, row_dom_id: }` (idx absent for :delete) or nil
|
|
227
|
+
def row_swap_for(components_before, new_fragment)
|
|
228
|
+
old_fragment = find_fragment(components_before, new_fragment.id)
|
|
229
|
+
return nil unless old_fragment
|
|
230
|
+
|
|
231
|
+
old_table = single_row_table(old_fragment.children)
|
|
232
|
+
new_table = single_row_table(new_fragment.children)
|
|
233
|
+
return nil unless old_table && new_table && old_table.dom_id == new_table.dom_id
|
|
234
|
+
|
|
235
|
+
old_ids = old_table.table_options[:row_ids]
|
|
236
|
+
new_ids = new_table.table_options[:row_ids]
|
|
237
|
+
return nil if old_ids.empty? || new_ids.empty? || old_ids.any?(&:nil?) || new_ids.any?(&:nil?)
|
|
238
|
+
|
|
239
|
+
action_key = decoded_action && decoded_action[:k]
|
|
240
|
+
return nil if action_key.nil?
|
|
241
|
+
|
|
242
|
+
if new_ids == old_ids
|
|
243
|
+
idx = old_ids.index(action_key)
|
|
244
|
+
return nil unless idx
|
|
245
|
+
{ kind: :edit, table: new_table, idx: idx, row_dom_id: "#{new_table.dom_id}-row-#{old_ids[idx]}" }
|
|
246
|
+
elsif old_ids.length == new_ids.length + 1 && (old_ids - [action_key]) == new_ids
|
|
247
|
+
{ kind: :delete, table: old_table, row_dom_id: "#{old_table.dom_id}-row-#{action_key}" }
|
|
248
|
+
elsif new_ids.length == old_ids.length + 1 && new_ids[0...-1] == old_ids
|
|
249
|
+
{ kind: :create, table: new_table, idx: new_ids.length - 1 }
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# The table a fragment's row-mutation could be narrowed against, or nil
|
|
254
|
+
# if the fragment doesn't contain exactly one table with row identity --
|
|
255
|
+
# more than one is ambiguous (which table did the action touch?), so it
|
|
256
|
+
# falls back like everything else unproven (FAC-P2.1's `dom_id` is only
|
|
257
|
+
# assigned to column-DSL tables with row identity in the first place).
|
|
258
|
+
def single_row_table(children)
|
|
259
|
+
tables = collect_tables(children)
|
|
260
|
+
tables.length == 1 ? tables.first : nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def collect_tables(children)
|
|
264
|
+
children.each_with_object([]) do |component, found|
|
|
265
|
+
found << component if component.is_a?(Components::Table) && component.dom_id
|
|
266
|
+
found.concat(collect_tables(component.children)) if component.respond_to?(:children) && component.children
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def primary_swap_headers(swap)
|
|
271
|
+
case swap[:kind]
|
|
272
|
+
when :edit then { "HX-Retarget" => "##{swap[:row_dom_id]}", "HX-Reswap" => "outerHTML" }
|
|
273
|
+
when :delete then { "HX-Retarget" => "##{swap[:row_dom_id]}", "HX-Reswap" => "delete" }
|
|
274
|
+
when :create then { "HX-Retarget" => "##{swap[:table].dom_id} tbody", "HX-Reswap" => "beforeend" }
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def primary_swap_proc(swap)
|
|
279
|
+
return nil if swap[:kind] == :delete
|
|
280
|
+
|
|
281
|
+
table = swap[:table]
|
|
282
|
+
idx = swap[:idx]
|
|
283
|
+
->(view) { adapter.render_table_row(view, table.resolved_rows[idx], idx, table.table_options, state, table.dom_id) }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def declared_scope
|
|
287
|
+
payload = named_action? ? decoded_action : decoded_fragment_param
|
|
288
|
+
return unless payload && payload[:f]
|
|
289
|
+
{ fragment: payload[:f].to_s, updates: Array(payload[:u]).map(&:to_s), primary: payload[:p]&.to_s }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Memoized: every rebuild now consults the scope too (see #rebuild), and
|
|
293
|
+
# decoding re-verifies the token's signature each time.
|
|
294
|
+
def decoded_fragment_param
|
|
295
|
+
return @decoded_fragment_param if defined?(@decoded_fragment_param)
|
|
296
|
+
|
|
297
|
+
token = params["_sw_fragment"] || params[:_sw_fragment]
|
|
298
|
+
@decoded_fragment_param = begin
|
|
299
|
+
ActionToken.decode(token) if token
|
|
300
|
+
rescue ActionToken::Invalid
|
|
301
|
+
nil
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Signed by the adapter when it renders a `defer: true` fragment's own
|
|
306
|
+
# auto-fetch (see Adapter::AlpineJS#htmx_attrs), never by a browser.
|
|
307
|
+
def deferred_fetch?
|
|
308
|
+
!named_action? && !decoded_fragment_param.nil? && !decoded_fragment_param[:d].nil?
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def fragment_param_present?
|
|
312
|
+
params.key?("_sw_fragment") || params.key?(:_sw_fragment)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def state_patch(before_state, after_state, version)
|
|
316
|
+
set = after_state.each_with_object({}) do |(key, value), changed|
|
|
317
|
+
changed[key.to_s] = value unless before_state.key?(key) && before_state[key] == value
|
|
318
|
+
end
|
|
319
|
+
deleted = before_state.keys.reject { |key| after_state.key?(key) }.map(&:to_s)
|
|
320
|
+
{ set: set, delete: deleted, version: version }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def find_fragment(components, id)
|
|
324
|
+
find_recursive(components) { |component| component.is_a?(Components::Fragment) && component.id == id }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def find_fragment_by_name(components, name)
|
|
328
|
+
find_recursive(components) { |component| component.is_a?(Components::Fragment) && component.name.to_s == name.to_s }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def discovery_required?
|
|
332
|
+
(interaction == :action && !named_action?) || interaction == :form
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def sync_params_to_state
|
|
336
|
+
params.each do |key, value|
|
|
337
|
+
next if ROUTE_PARAMS.include?(key.to_s)
|
|
338
|
+
|
|
339
|
+
if value.is_a?(Hash)
|
|
340
|
+
# A scope-nested "live" field (FAC-P3.1 handoff): Rack already
|
|
341
|
+
# parsed the Rails-style `scope[field]=value` param into a nested
|
|
342
|
+
# hash. Merge the one changed field into the scope's sub-hash --
|
|
343
|
+
# never replace it wholesale, since a single live field's keyup
|
|
344
|
+
# only posts that one key, not its siblings.
|
|
345
|
+
scope_name = key.to_sym
|
|
346
|
+
state[scope_name] ||= {}
|
|
347
|
+
value.each { |k, v| state[scope_name][k.to_sym] = coerce_param_value(v, state[scope_name][k.to_sym]) }
|
|
348
|
+
else
|
|
349
|
+
state[key.to_sym] = coerce_param_value(value, state[key.to_sym])
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def coerce_param_value(value, current_value)
|
|
355
|
+
case
|
|
356
|
+
when value.is_a?(Array) then value
|
|
357
|
+
when value == "on" || value == "true" then true
|
|
358
|
+
when value == "false" then false
|
|
359
|
+
when current_value.is_a?(Array) then Array(value)
|
|
360
|
+
else value
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def handle_unchecked_checkboxes
|
|
365
|
+
app.render_state.checkbox_keys.each do |scope_name, key|
|
|
366
|
+
if scope_name
|
|
367
|
+
nested = params[scope_name.to_s]
|
|
368
|
+
next if nested.is_a?(Hash) && (nested.key?(key.to_s) || nested.key?(key))
|
|
369
|
+
state[scope_name] ||= {}
|
|
370
|
+
state[scope_name][key] = false
|
|
371
|
+
else
|
|
372
|
+
state[key] = false unless params.key?(key.to_s) || params.key?(key)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def prepare_form_state
|
|
378
|
+
values = (params[target.to_s] || params[target.to_sym] || {}).each_with_object({}) do |(key, value), result|
|
|
379
|
+
result[key.to_sym] = coerce_param_value(value, nil)
|
|
380
|
+
end
|
|
381
|
+
state[target.to_sym] = values
|
|
382
|
+
@form_values = values
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def dispatch
|
|
386
|
+
case interaction
|
|
387
|
+
when :update
|
|
388
|
+
nil
|
|
389
|
+
when :action
|
|
390
|
+
if named_action?
|
|
391
|
+
payload = decoded_action
|
|
392
|
+
return unless payload
|
|
393
|
+
return unless action_manifest.include?(ActionToken.fingerprint(target))
|
|
394
|
+
if payload[:d] != app.action_definition_digest || payload[:g].to_s != generation
|
|
395
|
+
cause = payload[:d] != app.action_definition_digest ? :definition_changed : :generation_changed
|
|
396
|
+
raise StaleActionDefinition.new(action: payload[:a], fragment: payload[:f], cause: cause)
|
|
397
|
+
end
|
|
398
|
+
@named_action_authorized = true
|
|
399
|
+
app.bind_dispatch_state(state)
|
|
400
|
+
app.actions[payload[:a].to_sym]&.call(state, payload[:k])
|
|
401
|
+
else
|
|
402
|
+
find_button(app.components, target.to_s)&.execute(state)
|
|
403
|
+
end
|
|
404
|
+
when :event
|
|
405
|
+
component = find_component(app.components, target.to_sym)
|
|
406
|
+
return unless component
|
|
407
|
+
|
|
408
|
+
value = state[target.to_sym]
|
|
409
|
+
component.execute_on_change(state, value) if component.respond_to?(:execute_on_change)
|
|
410
|
+
component.execute_on_blur(state, value) if component.respond_to?(:execute_on_blur)
|
|
411
|
+
when :form
|
|
412
|
+
find_form(app.components, target.to_sym)&.execute_submit(state, @form_values)
|
|
413
|
+
else
|
|
414
|
+
raise ArgumentError, "unknown interaction: #{interaction.inspect}"
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def named_action?
|
|
419
|
+
target.to_s.include?(".")
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def decoded_action
|
|
423
|
+
@decoded_action ||= ActionToken.decode(target)
|
|
424
|
+
rescue ActionToken::Invalid
|
|
425
|
+
nil
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def completion_response
|
|
429
|
+
return unless agentic && state[:_result] && result_container
|
|
430
|
+
|
|
431
|
+
filtered = {}
|
|
432
|
+
collect_input_keys(app.components).each { |key| filtered[key] = state[key] if state.key?(key) }
|
|
433
|
+
filtered[:_result] = state[:_result]
|
|
434
|
+
result_container[:result] = filtered
|
|
435
|
+
result_container[:ready] = true
|
|
436
|
+
return unless auto_close
|
|
437
|
+
|
|
438
|
+
<<~HTML
|
|
439
|
+
<html><body>
|
|
440
|
+
<h1>Done!</h1>
|
|
441
|
+
<script>setTimeout(function(){ window.close(); }, 500);</script>
|
|
442
|
+
</body></html>
|
|
443
|
+
HTML
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def find_button(components, id)
|
|
447
|
+
find_recursive(components) do |component|
|
|
448
|
+
(component.is_a?(Components::Button) || component.is_a?(Components::MenuItem)) && component.id == id
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def find_form(components, name)
|
|
453
|
+
find_recursive(components) do |component|
|
|
454
|
+
component.is_a?(Components::Form) && component.name == name
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def find_component(components, key)
|
|
459
|
+
find_recursive(components) do |component|
|
|
460
|
+
component.respond_to?(:key) && component.key == key
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def find_recursive(components, &match)
|
|
465
|
+
components.each do |component|
|
|
466
|
+
return component if match.call(component)
|
|
467
|
+
|
|
468
|
+
if component.respond_to?(:children) && component.children
|
|
469
|
+
found = find_recursive(component.children, &match)
|
|
470
|
+
return found if found
|
|
471
|
+
end
|
|
472
|
+
if component.is_a?(Components::Modal) && component.footer_component&.children
|
|
473
|
+
found = find_recursive(component.footer_component.children, &match)
|
|
474
|
+
return found if found
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
nil
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def collect_input_keys(components)
|
|
481
|
+
components.each_with_object([]) do |component, keys|
|
|
482
|
+
keys << component.key if component.respond_to?(:key) && component.key
|
|
483
|
+
keys.concat(collect_input_keys(component.children)) if component.respond_to?(:children) && component.children
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|