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,108 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example: Dashboard Components
|
|
5
|
+
# Showcases all dashboard-style components with dark theme
|
|
6
|
+
# Run with: ./exe/streamweaver examples/dashboard_components.rb
|
|
7
|
+
|
|
8
|
+
app "Dashboard Components Demo", theme: :dark, layout: :wide do
|
|
9
|
+
# Header with pulse indicator
|
|
10
|
+
app_header "Dashboard Components", variant: :primary do
|
|
11
|
+
pulse_indicator color: :green, label: "System Active"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
header2 "Status Dots"
|
|
15
|
+
text "Colored indicator dots with optional glow effect:"
|
|
16
|
+
hstack spacing: :lg, align: :center do
|
|
17
|
+
vstack spacing: :xs, align: :center do
|
|
18
|
+
status_dot status: :red
|
|
19
|
+
text "Red"
|
|
20
|
+
end
|
|
21
|
+
vstack spacing: :xs, align: :center do
|
|
22
|
+
status_dot status: :yellow
|
|
23
|
+
text "Yellow"
|
|
24
|
+
end
|
|
25
|
+
vstack spacing: :xs, align: :center do
|
|
26
|
+
status_dot status: :green
|
|
27
|
+
text "Green"
|
|
28
|
+
end
|
|
29
|
+
vstack spacing: :xs, align: :center do
|
|
30
|
+
status_dot status: :green, pulse: true
|
|
31
|
+
text "Pulsing"
|
|
32
|
+
end
|
|
33
|
+
vstack spacing: :xs, align: :center do
|
|
34
|
+
status_dot status: :gray
|
|
35
|
+
text "Gray"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
header2 "Badges"
|
|
40
|
+
text "Count/label badges in various variants:"
|
|
41
|
+
hstack spacing: :md do
|
|
42
|
+
badge "5", variant: :default
|
|
43
|
+
badge "3", variant: :danger
|
|
44
|
+
badge "12", variant: :warning
|
|
45
|
+
badge "OK", variant: :success
|
|
46
|
+
badge "new", variant: :info
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
header2 "Stat Displays"
|
|
50
|
+
text "Large metric numbers with labels:"
|
|
51
|
+
hstack spacing: :xl do
|
|
52
|
+
stat_display value: 24, label: "TOTAL", color: :default
|
|
53
|
+
stat_display value: 7, label: "PENDING", color: :blue
|
|
54
|
+
stat_display value: 12, label: "IN PROGRESS", color: :purple
|
|
55
|
+
stat_display value: 5, label: "BLOCKED", color: :red
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
header2 "Type Tags"
|
|
59
|
+
text "Activity type badges:"
|
|
60
|
+
hstack spacing: :md do
|
|
61
|
+
type_tag :research
|
|
62
|
+
type_tag :task
|
|
63
|
+
type_tag :escalation
|
|
64
|
+
type_tag :communication
|
|
65
|
+
type_tag :warning
|
|
66
|
+
type_tag :info
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
header2 "Priority Items"
|
|
70
|
+
text "Items with priority-colored left borders (hover to see slide effect):"
|
|
71
|
+
vstack spacing: :md do
|
|
72
|
+
priority_item priority: :critical, title: "Database migration blocked",
|
|
73
|
+
description: "Production schema changes require downtime window approval.",
|
|
74
|
+
meta_left: "ops", meta_right: "Schedule window"
|
|
75
|
+
|
|
76
|
+
priority_item priority: :urgent, title: "API rate limits exceeded",
|
|
77
|
+
description: "Third-party integration hitting 429 errors. Need backoff strategy.",
|
|
78
|
+
meta_left: "backend", meta_right: "Implement retry"
|
|
79
|
+
|
|
80
|
+
priority_item priority: :high, title: "Security audit findings",
|
|
81
|
+
description: "3 medium-severity items need remediation before release.",
|
|
82
|
+
meta_left: "security", meta_right: "Review findings"
|
|
83
|
+
|
|
84
|
+
priority_item priority: :normal, title: "Documentation update",
|
|
85
|
+
description: "API docs need refresh for v2 endpoints.",
|
|
86
|
+
meta_left: "docs", meta_right: "Update guides"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
header2 "Activity Items"
|
|
90
|
+
text "Activity feed with time, title, summary, and type (hover to see highlight):"
|
|
91
|
+
vstack spacing: :none do
|
|
92
|
+
activity_item time: "15:00", title: "Performance analysis complete",
|
|
93
|
+
summary: "Identified 3 slow queries, recommended indexes",
|
|
94
|
+
type: :research
|
|
95
|
+
|
|
96
|
+
activity_item time: "15:05", title: "Deployment approval needed",
|
|
97
|
+
summary: "Staging verified, awaiting prod sign-off",
|
|
98
|
+
type: :escalation
|
|
99
|
+
|
|
100
|
+
activity_item time: "14:00", title: "Feature branch merged",
|
|
101
|
+
summary: "User preferences module integrated",
|
|
102
|
+
type: :task
|
|
103
|
+
|
|
104
|
+
activity_item time: "13:30", title: "Standup notes shared",
|
|
105
|
+
summary: "Team aligned on sprint priorities",
|
|
106
|
+
type: :communication
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Manual-verification harness for deferred fragments (`fragment ..., defer: true`)
|
|
5
|
+
# -- the Turbo `turbo_frame_tag ..., src:` equivalent. Every deferred block below
|
|
6
|
+
# sleeps, so the point is visible: the shell paints immediately and each region
|
|
7
|
+
# fills in on its own afterwards, with zero JavaScript in this file.
|
|
8
|
+
#
|
|
9
|
+
# SW_NO_OPEN=1 STREAMWEAVER_PORT=4599 ruby examples/deferred_fragments_demo.rb
|
|
10
|
+
#
|
|
11
|
+
# SW_DEFER_DELAY overrides the per-fragment sleep (default 1.5s, matching the
|
|
12
|
+
# learnhotwire course's `sleep 1.5` hover-card demo).
|
|
13
|
+
|
|
14
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
15
|
+
require "stream_weaver"
|
|
16
|
+
|
|
17
|
+
DELAY = (ENV["SW_DEFER_DELAY"] || "1.5").to_f
|
|
18
|
+
|
|
19
|
+
def slow_work(label)
|
|
20
|
+
sleep DELAY
|
|
21
|
+
"#{label} finished at #{Time.now.strftime('%H:%M:%S.%L')}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
app "Deferred Fragments" do
|
|
25
|
+
header1 "Deferred fragments"
|
|
26
|
+
md "This line and everything above it render **immediately**. Each panel below " \
|
|
27
|
+
"sleeps #{DELAY}s inside its `defer: true` block, so the block cannot run " \
|
|
28
|
+
"during the page render -- the shell would take #{(DELAY * 3).round(1)}s if it did."
|
|
29
|
+
text "Shell rendered at #{Time.now.strftime('%H:%M:%S.%L')}"
|
|
30
|
+
|
|
31
|
+
header3 "1. Default placeholder (spinner)"
|
|
32
|
+
fragment :default_placeholder, defer: true do
|
|
33
|
+
text slow_work("Panel 1")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
header3 "2. String placeholder"
|
|
37
|
+
fragment :string_placeholder, defer: true, placeholder: "Crunching numbers…" do
|
|
38
|
+
text slow_work("Panel 2")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
header3 "3. Custom DSL placeholder, reading state from the field above it"
|
|
42
|
+
text_field :who, placeholder: "type a name, then reload", label: "Who"
|
|
43
|
+
fragment :reads_state, defer: true, placeholder: -> { alert(variant: :info) { text "Waiting for the server…" } } do
|
|
44
|
+
text slow_work("Panel 3 for #{state[:who].to_s.empty? ? '(nobody)' : state[:who]}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
header3 "4. Deferred nested inside deferred (loads as a chain)"
|
|
48
|
+
fragment :outer_panel, defer: true, placeholder: "Outer loading…" do
|
|
49
|
+
text slow_work("Outer")
|
|
50
|
+
fragment :inner_panel, defer: true, placeholder: "Inner loading…" do
|
|
51
|
+
text slow_work("Inner")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
header3 "5. Full-container swap"
|
|
56
|
+
md "This button swaps the whole app container. The panels above should return " \
|
|
57
|
+
"to their placeholders and fetch again rather than sticking on a placeholder."
|
|
58
|
+
button "Re-render the page" do |s|
|
|
59
|
+
s[:renders] = s[:renders].to_i + 1
|
|
60
|
+
end
|
|
61
|
+
text "Full re-renders so far: #{state[:renders].to_i}"
|
|
62
|
+
end.run!
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Generate-More Spike (T1)
|
|
2
|
+
|
|
3
|
+
Standalone spike demonstrating the generate-more loop using **push-to-state** architecture.
|
|
4
|
+
|
|
5
|
+
## How to Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd examples/generate_more_spike
|
|
9
|
+
ruby app.rb
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The app opens at `http://127.0.0.1:4567` (or next available port).
|
|
13
|
+
|
|
14
|
+
## What It Demonstrates
|
|
15
|
+
|
|
16
|
+
1. **Generate More button** -- Enter an optional prompt and select how many options to generate (1-5).
|
|
17
|
+
2. **Skeleton placeholders** -- While generating, skeleton shimmer cards appear for pending options.
|
|
18
|
+
3. **Simulated agent** -- A background thread simulates an agent (polls for requests, sleeps 1-3s per option, pushes results to state).
|
|
19
|
+
4. **Push-to-state** -- New options are written to server-side shared state. SSE sends the re-rendered HTML to the browser. The browser never receives raw data -- it gets a fully rendered content div.
|
|
20
|
+
5. **Timeout** -- If generation takes longer than 15s, a timeout warning appears.
|
|
21
|
+
6. **Cancel** -- A cancel button aborts in-progress generation.
|
|
22
|
+
7. **Session isolation** -- Each browser session gets its own state keyed by session ID.
|
|
23
|
+
|
|
24
|
+
## Architecture Validated
|
|
25
|
+
|
|
26
|
+
### Push-to-State Pattern
|
|
27
|
+
|
|
28
|
+
The agent pushes options into a shared server-side state store, NOT directly into the DOM via SSE. StreamWeaver's existing `Streamer.replace()` sends the complete re-rendered HTML for the content div. This:
|
|
29
|
+
|
|
30
|
+
- Eliminates the phantom option race condition (user on different slide when options arrive)
|
|
31
|
+
- Keeps everything in sync automatically (no stale partial state)
|
|
32
|
+
- Aligns with StreamWeaver's reactive model (re-render from state)
|
|
33
|
+
- Is simpler than SSE-to-DOM with targeted element updates
|
|
34
|
+
|
|
35
|
+
### Key Finding: Session Cookie Limitation
|
|
36
|
+
|
|
37
|
+
The agent thread has NO access to Sinatra's cookie-based session. Push-to-state requires a server-side state store (in-memory hash, file-backed JSON, or similar) keyed by session ID. T10 must implement `DeckState` as a file-backed store, not rely on `session[:streamlit_state]`.
|
|
38
|
+
|
|
39
|
+
## State Machine
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
IDLE
|
|
43
|
+
|
|
|
44
|
+
[user clicks Generate]
|
|
45
|
+
|
|
|
46
|
+
v
|
|
47
|
+
GENERATING
|
|
48
|
+
/ | \
|
|
49
|
+
[all received] | [user cancels]
|
|
50
|
+
| [timeout 15s] |
|
|
51
|
+
v | v
|
|
52
|
+
IDLE TIMED_OUT CANCELLED
|
|
53
|
+
| |
|
|
54
|
+
[retry/dismiss] [cleanup]
|
|
55
|
+
| |
|
|
56
|
+
v v
|
|
57
|
+
IDLE IDLE
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Files
|
|
61
|
+
|
|
62
|
+
- `app.rb` -- The spike app (standalone, does not modify the gem)
|
|
63
|
+
- `README.md` -- This file
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ============================================================================
|
|
5
|
+
# Generate-More Spike (T1)
|
|
6
|
+
# ============================================================================
|
|
7
|
+
#
|
|
8
|
+
# Standalone spike demonstrating the generate-more loop using push-to-state.
|
|
9
|
+
# This is throwaway code -- T10 will do the real implementation.
|
|
10
|
+
#
|
|
11
|
+
# Key architectural decision being validated:
|
|
12
|
+
# Push-to-state, NOT push-to-DOM.
|
|
13
|
+
# The "agent" pushes new options into server-side state, and StreamWeaver's
|
|
14
|
+
# reactive re-render handles display. No direct DOM manipulation via SSE.
|
|
15
|
+
#
|
|
16
|
+
# Run with:
|
|
17
|
+
# cd examples/generate_more_spike && ruby app.rb
|
|
18
|
+
#
|
|
19
|
+
# What it demonstrates:
|
|
20
|
+
# 1. "Generate More" button with prompt input
|
|
21
|
+
# 2. Skeleton placeholders appear while generating
|
|
22
|
+
# 3. Simulated agent (Thread.new) detects request, sleeps, pushes state
|
|
23
|
+
# 4. Page re-renders reactively with new options replacing skeletons
|
|
24
|
+
# 5. Timeout after 15s shows error message
|
|
25
|
+
# 6. Cancel button aborts pending generation
|
|
26
|
+
# 7. Session-scoped request queue
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
require_relative '../../lib/stream_weaver'
|
|
30
|
+
require 'json'
|
|
31
|
+
require 'securerandom'
|
|
32
|
+
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# Generate-More State Machine (Discovered through implementation)
|
|
35
|
+
# ============================================================================
|
|
36
|
+
# States:
|
|
37
|
+
# :idle - No generation in progress. Generate button enabled.
|
|
38
|
+
# :generating - Request queued, skeletons visible, cancel button shown.
|
|
39
|
+
# :timed_out - 15s elapsed without all options arriving.
|
|
40
|
+
# :cancelled - User cancelled, agent acknowledged. Transient -> idle.
|
|
41
|
+
#
|
|
42
|
+
# Transitions:
|
|
43
|
+
# idle -> generating (user clicks Generate)
|
|
44
|
+
# generating -> idle (all options received)
|
|
45
|
+
# generating -> timed_out (15s timeout, checked in timer)
|
|
46
|
+
# generating -> cancelled (user clicks Cancel)
|
|
47
|
+
# cancelled -> idle (immediate, after cleanup)
|
|
48
|
+
# timed_out -> idle (user clicks Generate again)
|
|
49
|
+
# ============================================================================
|
|
50
|
+
|
|
51
|
+
# ============================================================================
|
|
52
|
+
# Shared state store (simulates file-backed DeckState for the spike)
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# In T10 this becomes DeckState with file-backed JSON per session.
|
|
55
|
+
# Key finding: session cookies can't hold this state (agent thread has no
|
|
56
|
+
# access to Sinatra sessions). A shared server-side store keyed by session
|
|
57
|
+
# ID is required.
|
|
58
|
+
|
|
59
|
+
module SpikeState
|
|
60
|
+
@mutex = Mutex.new
|
|
61
|
+
@sessions = {}
|
|
62
|
+
@pending_requests = []
|
|
63
|
+
@cancelled_sessions = {}
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
attr_reader :mutex
|
|
67
|
+
|
|
68
|
+
def get(session_id)
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
@sessions[session_id] ||= default_state
|
|
71
|
+
@sessions[session_id].dup
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def update(session_id)
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
@sessions[session_id] ||= default_state
|
|
78
|
+
yield @sessions[session_id]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def queue_request(request)
|
|
83
|
+
@mutex.synchronize { @pending_requests << request }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def take_requests
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
taken = @pending_requests.dup
|
|
89
|
+
@pending_requests.clear
|
|
90
|
+
taken
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def cancel(session_id)
|
|
95
|
+
@mutex.synchronize { @cancelled_sessions[session_id] = true }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def cancelled?(session_id)
|
|
99
|
+
@mutex.synchronize { @cancelled_sessions.delete(session_id) || false }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def default_state
|
|
105
|
+
{
|
|
106
|
+
options: [
|
|
107
|
+
{ id: "opt_1", label: "Monolith", description: "Single deployable unit, simpler operations", generated: false },
|
|
108
|
+
{ id: "opt_2", label: "Microservices", description: "Independent services, complex but scalable", generated: false },
|
|
109
|
+
],
|
|
110
|
+
generate_status: :idle,
|
|
111
|
+
requested_count: 0,
|
|
112
|
+
received_count: 0,
|
|
113
|
+
prompt: nil,
|
|
114
|
+
started_at: nil,
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# ============================================================================
|
|
121
|
+
# Simulated agent options
|
|
122
|
+
# ============================================================================
|
|
123
|
+
|
|
124
|
+
SIMULATED_OPTIONS = [
|
|
125
|
+
{ label: "Event-Driven", description: "Loosely coupled via message bus, great for async workflows" },
|
|
126
|
+
{ label: "Serverless", description: "Functions-as-a-service, pay per invocation, auto-scaling" },
|
|
127
|
+
{ label: "Modular Monolith", description: "Best of both: monolith deployment, modular internals" },
|
|
128
|
+
{ label: "Hexagonal", description: "Ports and adapters pattern, testable and flexible" },
|
|
129
|
+
{ label: "CQRS", description: "Separate read/write models for optimized query performance" },
|
|
130
|
+
{ label: "Service Mesh", description: "Sidecar proxies handle networking, observability built-in" },
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
# ============================================================================
|
|
134
|
+
# Simulated Agent Thread
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# In the real system, an external agent polls GET /deck/pending.
|
|
137
|
+
# Here we simulate with a thread watching the shared state.
|
|
138
|
+
|
|
139
|
+
$streamer_ref = nil
|
|
140
|
+
$sim_option_index = 0
|
|
141
|
+
|
|
142
|
+
def start_agent_simulator
|
|
143
|
+
Thread.new do
|
|
144
|
+
loop do
|
|
145
|
+
requests = SpikeState.take_requests
|
|
146
|
+
|
|
147
|
+
requests.each do |request|
|
|
148
|
+
session_id = request[:session_id]
|
|
149
|
+
count = request[:count]
|
|
150
|
+
prompt = request[:prompt]
|
|
151
|
+
|
|
152
|
+
$stderr.puts "[Agent] Processing generate request: count=#{count}, prompt=#{prompt.inspect}"
|
|
153
|
+
|
|
154
|
+
count.times do |i|
|
|
155
|
+
# Check cancellation
|
|
156
|
+
if SpikeState.cancelled?(session_id)
|
|
157
|
+
$stderr.puts "[Agent] Generation cancelled for session #{session_id}"
|
|
158
|
+
SpikeState.update(session_id) { |s| s[:generate_status] = :idle }
|
|
159
|
+
push_rerender(session_id)
|
|
160
|
+
break
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Simulate LLM generation delay (1-3 seconds per option)
|
|
164
|
+
sleep(1.0 + rand * 2.0)
|
|
165
|
+
|
|
166
|
+
# Check cancellation again after sleep
|
|
167
|
+
if SpikeState.cancelled?(session_id)
|
|
168
|
+
$stderr.puts "[Agent] Generation cancelled (post-sleep) for session #{session_id}"
|
|
169
|
+
SpikeState.update(session_id) { |s| s[:generate_status] = :idle }
|
|
170
|
+
push_rerender(session_id)
|
|
171
|
+
break
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Check if already timed out
|
|
175
|
+
state = SpikeState.get(session_id)
|
|
176
|
+
if state[:generate_status] != :generating
|
|
177
|
+
$stderr.puts "[Agent] State is #{state[:generate_status]}, stopping generation"
|
|
178
|
+
break
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Pick a simulated option
|
|
182
|
+
opt = SIMULATED_OPTIONS[$sim_option_index % SIMULATED_OPTIONS.length]
|
|
183
|
+
$sim_option_index += 1
|
|
184
|
+
|
|
185
|
+
desc = prompt && !prompt.strip.empty? ?
|
|
186
|
+
"#{opt[:description]} (prompted: #{prompt})" :
|
|
187
|
+
opt[:description]
|
|
188
|
+
|
|
189
|
+
opt_id = "gen_#{SecureRandom.hex(4)}"
|
|
190
|
+
|
|
191
|
+
# PUSH TO STATE -- the key architectural decision
|
|
192
|
+
SpikeState.update(session_id) do |s|
|
|
193
|
+
s[:options] << {
|
|
194
|
+
id: opt_id,
|
|
195
|
+
label: opt[:label],
|
|
196
|
+
description: desc,
|
|
197
|
+
generated: true,
|
|
198
|
+
}
|
|
199
|
+
s[:received_count] += 1
|
|
200
|
+
$stderr.puts "[Agent] Pushed option #{i + 1}/#{count}: #{opt[:label]} (#{s[:received_count]}/#{s[:requested_count]})"
|
|
201
|
+
|
|
202
|
+
# If all received, transition to idle
|
|
203
|
+
if s[:received_count] >= s[:requested_count]
|
|
204
|
+
s[:generate_status] = :idle
|
|
205
|
+
$stderr.puts "[Agent] All options received -> idle"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Notify browser to re-render via SSE
|
|
210
|
+
push_rerender(session_id)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
sleep 0.5 unless requests.any?
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def push_rerender(session_id)
|
|
220
|
+
return unless $streamer_ref
|
|
221
|
+
html = build_options_html(session_id)
|
|
222
|
+
$streamer_ref.replace("#spike-content", html)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# ============================================================================
|
|
226
|
+
# HTML builder for option cards + skeletons + status
|
|
227
|
+
# ============================================================================
|
|
228
|
+
# This is what StreamWeaver's reactive re-render would produce.
|
|
229
|
+
# The component tree reads state and renders the current view.
|
|
230
|
+
|
|
231
|
+
def build_options_html(session_id)
|
|
232
|
+
state = SpikeState.get(session_id)
|
|
233
|
+
status = state[:generate_status]
|
|
234
|
+
options = state[:options]
|
|
235
|
+
requested = state[:requested_count]
|
|
236
|
+
received = state[:received_count]
|
|
237
|
+
remaining = status == :generating ? [requested - received, 0].max : 0
|
|
238
|
+
|
|
239
|
+
html = +""
|
|
240
|
+
|
|
241
|
+
# Status banner
|
|
242
|
+
case status
|
|
243
|
+
when :generating
|
|
244
|
+
elapsed = state[:started_at] ? (Time.now.to_f - state[:started_at]).round(1) : 0
|
|
245
|
+
html << %(<div style="background:#1e3a5f;border:1px solid #3b82f6;border-radius:8px;padding:12px 16px;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between">)
|
|
246
|
+
html << %(<div style="display:flex;align-items:center;gap:8px">)
|
|
247
|
+
html << %(<div style="width:12px;height:12px;border-radius:50%;background:#3b82f6;animation:pulse 1s ease-in-out infinite"></div>)
|
|
248
|
+
html << %(<span style="color:#93c5fd">Generating #{requested} option(s)... #{received}/#{requested} received (#{elapsed}s)</span>)
|
|
249
|
+
html << %(</div>)
|
|
250
|
+
html << %(<form method="post" action="/spike/cancel" style="margin:0">)
|
|
251
|
+
html << %(<input type="hidden" name="session_id" value="#{session_id}">)
|
|
252
|
+
html << %(<button type="submit" style="background:#dc2626;color:white;border:none;border-radius:6px;padding:6px 16px;cursor:pointer;font-size:0.875rem">Cancel</button>)
|
|
253
|
+
html << %(</form></div>)
|
|
254
|
+
when :timed_out
|
|
255
|
+
html << %(<div style="background:#451a03;border:1px solid #f59e0b;border-radius:8px;padding:12px 16px;margin-bottom:16px">)
|
|
256
|
+
html << %(<span style="color:#fbbf24">Generation timed out after 15s. #{received}/#{requested} options received. Click Generate to try again.</span>)
|
|
257
|
+
html << %(</div>)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Options grid
|
|
261
|
+
html << %(<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px">)
|
|
262
|
+
|
|
263
|
+
options.each do |opt|
|
|
264
|
+
border = opt[:generated] ? "#22c55e" : "#374151"
|
|
265
|
+
badge = opt[:generated] ?
|
|
266
|
+
%(<span style="background:#166534;color:#86efac;font-size:0.7rem;padding:2px 8px;border-radius:9999px;margin-left:8px">generated</span>) : ""
|
|
267
|
+
|
|
268
|
+
html << %(<div style="background:#1f2937;border:1px solid #{border};border-radius:12px;padding:16px">)
|
|
269
|
+
html << %(<h3 style="margin:0 0 8px;color:#f3f4f6;font-size:1rem">#{opt[:label]}#{badge}</h3>)
|
|
270
|
+
html << %(<p style="margin:0;color:#9ca3af;font-size:0.875rem">#{opt[:description]}</p>)
|
|
271
|
+
html << %(</div>)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Skeleton placeholders
|
|
275
|
+
remaining.times do
|
|
276
|
+
html << %(<div style="background:#1f2937;border:1px solid #374151;border-radius:12px;padding:16px">)
|
|
277
|
+
html << %(<div style="background:#374151;height:20px;width:60%;border-radius:4px;margin-bottom:12px;animation:shimmer 1.5s ease-in-out infinite alternate"></div>)
|
|
278
|
+
html << %(<div style="background:#374151;height:14px;width:90%;border-radius:4px;margin-bottom:8px;animation:shimmer 1.5s ease-in-out infinite alternate 0.2s"></div>)
|
|
279
|
+
html << %(<div style="background:#374151;height:14px;width:75%;border-radius:4px;animation:shimmer 1.5s ease-in-out infinite alternate 0.4s"></div>)
|
|
280
|
+
html << %(</div>)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
html << %(</div>)
|
|
284
|
+
|
|
285
|
+
# CSS for animations (included each push since SSE replaces content)
|
|
286
|
+
html << %(<style>)
|
|
287
|
+
html << %(@keyframes shimmer{from{opacity:0.5}to{opacity:1}})
|
|
288
|
+
html << %(@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}})
|
|
289
|
+
html << %(</style>)
|
|
290
|
+
|
|
291
|
+
html
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# ============================================================================
|
|
295
|
+
# StreamWeaver App
|
|
296
|
+
# ============================================================================
|
|
297
|
+
|
|
298
|
+
SpikeApp = app "Generate-More Spike", theme: :dark, stylesheets: [
|
|
299
|
+
"data:text/css,@keyframes shimmer{from{opacity:0.5}to{opacity:1}}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}"
|
|
300
|
+
] do
|
|
301
|
+
# Assign a session ID on first visit
|
|
302
|
+
state[:spike_session] ||= SecureRandom.hex(8)
|
|
303
|
+
session_id = state[:spike_session]
|
|
304
|
+
|
|
305
|
+
# Initialize session state if needed
|
|
306
|
+
SpikeState.get(session_id)
|
|
307
|
+
spike_state = SpikeState.get(session_id)
|
|
308
|
+
|
|
309
|
+
header1 "Generate-More Spike"
|
|
310
|
+
text "Demonstrates the push-to-state architecture for the generate-more loop."
|
|
311
|
+
text "Options below are pre-seeded. Click 'Generate More' to simulate an agent generating additional options."
|
|
312
|
+
|
|
313
|
+
div style: "border-top:1px solid #374151;margin:16px 0"
|
|
314
|
+
|
|
315
|
+
# Generate controls
|
|
316
|
+
card do
|
|
317
|
+
header3 "Generate More Options"
|
|
318
|
+
|
|
319
|
+
text_field :gen_prompt, placeholder: "e.g., Focus on event-driven patterns", label: "Prompt (optional)"
|
|
320
|
+
|
|
321
|
+
select :gen_count, ["1", "2", "3", "4", "5"], default: "2", label: "Number of options"
|
|
322
|
+
|
|
323
|
+
is_generating = spike_state[:generate_status] == :generating
|
|
324
|
+
|
|
325
|
+
unless is_generating
|
|
326
|
+
button "Generate More", variant: :primary do |s|
|
|
327
|
+
sid = s[:spike_session]
|
|
328
|
+
count = (s[:gen_count] || "2").to_i
|
|
329
|
+
prompt = s[:gen_prompt]
|
|
330
|
+
|
|
331
|
+
# Transition to :generating
|
|
332
|
+
SpikeState.update(sid) do |ss|
|
|
333
|
+
ss[:generate_status] = :generating
|
|
334
|
+
ss[:requested_count] = count
|
|
335
|
+
ss[:received_count] = 0
|
|
336
|
+
ss[:prompt] = prompt
|
|
337
|
+
ss[:started_at] = Time.now.to_f
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Queue request for the agent simulator
|
|
341
|
+
SpikeState.queue_request(
|
|
342
|
+
session_id: sid,
|
|
343
|
+
count: count,
|
|
344
|
+
prompt: prompt,
|
|
345
|
+
timestamp: Time.now,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
$stderr.puts "[Server] Generate request queued: session=#{sid} count=#{count} prompt=#{prompt.inspect}"
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Content area -- this div is replaced by SSE pushes
|
|
354
|
+
div id: "spike-content" do
|
|
355
|
+
# Initial render: show current options from spike state
|
|
356
|
+
spike_state[:options].each do |opt|
|
|
357
|
+
card do
|
|
358
|
+
badge_label = opt[:generated] ? " [generated]" : ""
|
|
359
|
+
header3 "#{opt[:label]}#{badge_label}"
|
|
360
|
+
text opt[:description]
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Show skeletons if currently generating
|
|
365
|
+
if spike_state[:generate_status] == :generating
|
|
366
|
+
remaining = [spike_state[:requested_count] - spike_state[:received_count], 0].max
|
|
367
|
+
remaining.times do
|
|
368
|
+
div style: "background:#1f2937;border:1px solid #374151;border-radius:12px;padding:16px;margin-top:8px" do
|
|
369
|
+
div style: "background:#374151;height:20px;width:60%;border-radius:4px;margin-bottom:12px;animation:shimmer 1.5s ease-in-out infinite alternate"
|
|
370
|
+
div style: "background:#374151;height:14px;width:90%;border-radius:4px;margin-bottom:8px;animation:shimmer 1.5s ease-in-out infinite alternate 0.2s"
|
|
371
|
+
div style: "background:#374151;height:14px;width:75%;border-radius:4px;animation:shimmer 1.5s ease-in-out infinite alternate 0.4s"
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
if spike_state[:generate_status] == :timed_out
|
|
377
|
+
alert variant: :warning, title: "Timeout" do
|
|
378
|
+
text "Generation timed out after 15s. #{spike_state[:received_count]}/#{spike_state[:requested_count]} options received."
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# State debug panel
|
|
384
|
+
div style: "margin-top:24px;padding:16px;background:#111827;border:1px solid #1f2937;border-radius:8px" do
|
|
385
|
+
header4 "State Debug"
|
|
386
|
+
text "Session: #{session_id}"
|
|
387
|
+
text "Status: #{spike_state[:generate_status]}"
|
|
388
|
+
text "Options: #{spike_state[:options].length} (#{spike_state[:options].count { |o| o[:generated] }} generated)"
|
|
389
|
+
text "Requested: #{spike_state[:requested_count]}, Received: #{spike_state[:received_count]}"
|
|
390
|
+
if spike_state[:started_at] && spike_state[:generate_status] == :generating
|
|
391
|
+
text "Elapsed: #{(Time.now.to_f - spike_state[:started_at]).round(1)}s"
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# SSE stream: agent pushes re-rendered content here
|
|
396
|
+
stream do |streamer|
|
|
397
|
+
$streamer_ref = streamer
|
|
398
|
+
# Timeout checker loop
|
|
399
|
+
loop do
|
|
400
|
+
sleep 1
|
|
401
|
+
SpikeState.mutex.synchronize do
|
|
402
|
+
SpikeState.instance_variable_get(:@sessions).each do |sid, s|
|
|
403
|
+
next unless s[:generate_status] == :generating && s[:started_at]
|
|
404
|
+
elapsed = Time.now.to_f - s[:started_at]
|
|
405
|
+
if elapsed > 15
|
|
406
|
+
s[:generate_status] = :timed_out
|
|
407
|
+
$stderr.puts "[Server] Session #{sid} timed out (#{elapsed.round(1)}s)"
|
|
408
|
+
Thread.new { push_rerender(sid) }
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# ============================================================================
|
|
417
|
+
# Custom routes (cancel endpoint)
|
|
418
|
+
# ============================================================================
|
|
419
|
+
|
|
420
|
+
SpikeApp.post '/spike/cancel' do
|
|
421
|
+
state = session[:streamlit_state] ||= {}
|
|
422
|
+
session_id = state[:spike_session]
|
|
423
|
+
|
|
424
|
+
if session_id
|
|
425
|
+
SpikeState.cancel(session_id)
|
|
426
|
+
SpikeState.update(session_id) { |s| s[:generate_status] = :idle }
|
|
427
|
+
$stderr.puts "[Server] Cancel for session #{session_id}"
|
|
428
|
+
Thread.new { push_rerender(session_id) }
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
redirect '/'
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# ============================================================================
|
|
435
|
+
# Start
|
|
436
|
+
# ============================================================================
|
|
437
|
+
|
|
438
|
+
agent_thread = start_agent_simulator
|
|
439
|
+
$stderr.puts "[Spike] Agent simulator started"
|
|
440
|
+
|
|
441
|
+
SpikeApp.run!
|