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,1725 @@
|
|
|
1
|
+
# StreamWeaver Visual Skills -- Architecture Design
|
|
2
|
+
|
|
3
|
+
*Date: 2026-03-12*
|
|
4
|
+
*Author: Architecture design for porting pi-design-deck and visual-explainer into StreamWeaver*
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Design Philosophy
|
|
9
|
+
|
|
10
|
+
### Core Principles
|
|
11
|
+
|
|
12
|
+
**The DSL IS the API.** An agent (Claude Code, Pi, Codex) communicates with StreamWeaver by writing Ruby DSL calls. Not JSON. Not raw HTML. Ruby. The DSL is expressive enough that the agent's output reads like a description of what should appear, and the framework handles rendering, theming, interactivity, and streaming.
|
|
13
|
+
|
|
14
|
+
**Follow existing patterns.** StreamWeaver already has a clear architecture: `Components::Base` subclasses, `DisplayDSL` module methods, `Adapter::Base` rendering dispatch, Phlex views, Sinatra routes, and `Pushable` for SSE. New visual skills components slot into this architecture -- they do not introduce parallel systems.
|
|
15
|
+
|
|
16
|
+
**Pareto code.** 37% of the two projects is shared infrastructure. Build the shared 37% first, then the deck-specific and explainer-specific components layer on top. No premature abstraction: if a component is only used by one skill, it lives in that skill's namespace.
|
|
17
|
+
|
|
18
|
+
**Composable, not monolithic.** Each component is independently usable. You can drop a `mermaid` block into any StreamWeaver app, not just a design deck or visual explainer. The deck and explainer are orchestrators that compose shared components into specific workflows.
|
|
19
|
+
|
|
20
|
+
### Architectural Decision: Flat Namespace
|
|
21
|
+
|
|
22
|
+
Components live in `StreamWeaver::Components::*` -- the same namespace as existing components. No `VisualSkills` sub-namespace. Rationale:
|
|
23
|
+
|
|
24
|
+
1. Existing components like `Table`, `Card`, `Grid` are already in `Components::*`
|
|
25
|
+
2. New components (`Mermaid`, `CodeBlock`, `Pipeline`) are general-purpose -- they belong alongside `Table` and `Card`
|
|
26
|
+
3. The DSL methods on `DisplayDSL` are flat (`mermaid`, `code_block`, `pipeline`) -- nesting the classes adds complexity with no benefit
|
|
27
|
+
4. Deck-specific orchestrators (`DesignDeck`, `DeckSlide`) are the exception -- they get a `Components::Deck::*` sub-namespace because they form a coherent subsystem
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. Module Structure
|
|
32
|
+
|
|
33
|
+
### Directory Tree
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
lib/stream_weaver/
|
|
37
|
+
components.rb # Existing -- add new component classes here
|
|
38
|
+
display_dsl.rb # Existing -- add new DSL methods here
|
|
39
|
+
app.rb # Existing -- add deck/explainer DSL methods
|
|
40
|
+
theme.rb # Existing -- enhance with presets, auto-mode
|
|
41
|
+
pushable.rb # Existing -- no changes needed
|
|
42
|
+
streamer.rb # Existing -- no changes needed
|
|
43
|
+
feed.rb # Existing -- no changes needed
|
|
44
|
+
|
|
45
|
+
components/
|
|
46
|
+
mermaid.rb # Mermaid diagram component
|
|
47
|
+
code_block.rb # Syntax-highlighted code block
|
|
48
|
+
image_block.rb # Image with caption, base64 export support
|
|
49
|
+
slide_container.rb # Slide navigation (swap + scroll-snap modes)
|
|
50
|
+
slide.rb # Single slide within a container
|
|
51
|
+
progress_indicator.rb # Fixed-position navigation progress bar
|
|
52
|
+
keyboard_shortcuts.rb # Shortcut registration system
|
|
53
|
+
callout.rb # Bordered info/warning/tip box
|
|
54
|
+
pipeline.rb # Step flow visualization
|
|
55
|
+
comparison.rb # Side-by-side diff panels
|
|
56
|
+
chart.rb # Chart.js wrapper
|
|
57
|
+
sidebar_toc.rb # Sticky TOC with scroll spy
|
|
58
|
+
ve_card.rb # Depth-tiered card (hero/elevated/default/recessed)
|
|
59
|
+
kpi_dashboard.rb # Metrics grid wrapping stat_display
|
|
60
|
+
data_table.rb # Enhanced table with sticky headers
|
|
61
|
+
|
|
62
|
+
# NOTE: hero_section, prose, pullquote, dir_tree, legend, flow_arrow,
|
|
63
|
+
# and layout_toggle are CSS-only helpers -- no separate component files.
|
|
64
|
+
# They are implemented as thin DSL methods that emit styled divs directly.
|
|
65
|
+
|
|
66
|
+
deck/ # Design deck subsystem
|
|
67
|
+
design_deck.rb # Top-level deck orchestrator
|
|
68
|
+
deck_slide.rb # Decision slide with options grid
|
|
69
|
+
deck_option.rb # Selectable option card
|
|
70
|
+
deck_summary.rb # Auto-generated summary slide
|
|
71
|
+
generate_more_controls.rb # Generate button, count, prompt
|
|
72
|
+
skeleton_placeholder.rb # Shimmer loading animation
|
|
73
|
+
model_selector.rb # AI model picker
|
|
74
|
+
confirmation_bar.rb # Cancel confirmation
|
|
75
|
+
close_overlay.rb # Post-submit/cancel overlay
|
|
76
|
+
|
|
77
|
+
theme/
|
|
78
|
+
presets.rb # Curated font+color preset definitions
|
|
79
|
+
auto_mode.rb # OS preference detection, localStorage persistence
|
|
80
|
+
|
|
81
|
+
export/
|
|
82
|
+
html_exporter.rb # Self-contained HTML generation pipeline
|
|
83
|
+
|
|
84
|
+
assets/
|
|
85
|
+
js/
|
|
86
|
+
mermaid_zoom.js # ~200 line zoom/pan engine
|
|
87
|
+
keyboard_shortcuts.js # Centralized key handler
|
|
88
|
+
slide_navigation.js # Shared slide nav logic
|
|
89
|
+
deck_selection.js # Option selection radio behavior
|
|
90
|
+
generate_more.js # SSE listener for new options
|
|
91
|
+
css/
|
|
92
|
+
visual_skills.css # Shared visual skills styles
|
|
93
|
+
deck.css # Deck-specific styles
|
|
94
|
+
explainer.css # Explainer-specific styles
|
|
95
|
+
depth_tiers.css # Surface depth tier styles
|
|
96
|
+
slide_transitions.css # Slide animation presets
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Module Inclusion Hierarchy
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
StreamWeaver::DisplayDSL # Shared display-only DSL
|
|
103
|
+
includes: mermaid, code_block, callout, pipeline, comparison,
|
|
104
|
+
chart, ve_card, kpi_dashboard, data_table,
|
|
105
|
+
image_block, sidebar_toc, slide_container
|
|
106
|
+
# CSS-only helpers (thin DSL methods, no Component class):
|
|
107
|
+
# prose, pullquote, hero_section, dir_tree, legend,
|
|
108
|
+
# flow_arrow, layout_toggle
|
|
109
|
+
|
|
110
|
+
StreamWeaver::App
|
|
111
|
+
includes DisplayDSL # Gets all shared methods
|
|
112
|
+
adds: design_deck, theme_toggle, keyboard_shortcuts,
|
|
113
|
+
html_export, toast (already exists),
|
|
114
|
+
theme_preset, generate_more_controls, model_selector,
|
|
115
|
+
confirmation_bar, close_overlay
|
|
116
|
+
|
|
117
|
+
StreamWeaver::FeedBuilder
|
|
118
|
+
includes DisplayDSL # Feed can push any display component
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 3. Shared Components
|
|
124
|
+
|
|
125
|
+
### 3.1 Mermaid
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# DSL usage
|
|
129
|
+
mermaid "graph TD\n A-->B\n B-->C"
|
|
130
|
+
mermaid "graph LR\n A-->B", zoom: true, layout: :elk
|
|
131
|
+
mermaid code, compact: true, theme_vars: { primaryColor: "#ff0000" }
|
|
132
|
+
|
|
133
|
+
# Class
|
|
134
|
+
class Components::Mermaid < Components::Base
|
|
135
|
+
attr_reader :code, :zoom, :compact, :layout, :theme_vars
|
|
136
|
+
|
|
137
|
+
def initialize(code, zoom: false, compact: false, layout: :default, theme_vars: {}, **options)
|
|
138
|
+
def render(view, state)
|
|
139
|
+
def cdn_assets # => [:mermaid] or [:mermaid, :mermaid_elk]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Adapter method
|
|
143
|
+
class Adapter::AlpineJS
|
|
144
|
+
def render_mermaid(view, component, state)
|
|
145
|
+
# Renders: <div class="sw-mermaid-wrap" [x-data for zoom state]>
|
|
146
|
+
# <pre class="mermaid">#{code}</pre>
|
|
147
|
+
# [zoom controls if zoom: true]
|
|
148
|
+
# </div>
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Conceptual HTML output:
|
|
154
|
+
- Compact mode: `<div class="sw-mermaid sw-mermaid--compact"><pre class="mermaid">...</pre></div>`
|
|
155
|
+
- Zoom mode: `<div class="sw-mermaid sw-mermaid--zoom" x-data="mermaidZoom()">` with +/- buttons, expand button, pan/zoom container
|
|
156
|
+
|
|
157
|
+
CDN: `<script type="module">import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'</script>`
|
|
158
|
+
|
|
159
|
+
### 3.2 CodeBlock
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
# DSL usage
|
|
163
|
+
code_block "const x = 1;", lang: "javascript"
|
|
164
|
+
code_block File.read("src/app.rb"), lang: "ruby", file: "src/app.rb"
|
|
165
|
+
code_block snippet, lang: "ts", truncate: 3 # thumbnail mode
|
|
166
|
+
|
|
167
|
+
# Class
|
|
168
|
+
class Components::CodeBlock < Components::Base
|
|
169
|
+
attr_reader :code, :lang, :file, :truncate, :scroll
|
|
170
|
+
|
|
171
|
+
def initialize(code, lang: nil, file: nil, truncate: nil, scroll: true, **options)
|
|
172
|
+
def render(view, state)
|
|
173
|
+
def cdn_assets # => [:prismjs]
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Conceptual HTML: `<div class="sw-code-block"><div class="sw-code-header">src/app.rb</div><pre><code class="language-ruby">...</code></pre></div>`
|
|
178
|
+
|
|
179
|
+
### 3.3 ThemeToggle
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# DSL usage (in App context)
|
|
183
|
+
theme_toggle mode: :auto, hotkey: "mod+shift+l"
|
|
184
|
+
|
|
185
|
+
# Class
|
|
186
|
+
class Components::ThemeToggle < Components::Base
|
|
187
|
+
def initialize(mode: :dark, hotkey: nil, persist: true, **options)
|
|
188
|
+
def render(view, state)
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This enhances the existing `ThemeSwitcher` component. Adds:
|
|
193
|
+
- Auto mode via `prefers-color-scheme` media query listener
|
|
194
|
+
- Keyboard shortcut registration (delegates to `KeyboardShortcuts`)
|
|
195
|
+
- `localStorage` persistence of override
|
|
196
|
+
- `<meta name="theme-color">` management
|
|
197
|
+
- Sun/moon toggle button UI
|
|
198
|
+
|
|
199
|
+
### 3.4 KeyboardShortcuts
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# DSL usage (in App context)
|
|
203
|
+
keyboard_shortcuts do |kb|
|
|
204
|
+
kb.on "mod+s", context: :global do |state|
|
|
205
|
+
# save action
|
|
206
|
+
end
|
|
207
|
+
kb.on "ArrowRight", context: :navigation do |state|
|
|
208
|
+
# next slide
|
|
209
|
+
end
|
|
210
|
+
kb.on "1..9", context: :selection do |state, key|
|
|
211
|
+
# quick select
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Class
|
|
216
|
+
class Components::KeyboardShortcuts < Components::Base
|
|
217
|
+
def initialize(**options)
|
|
218
|
+
def on(key, context: :global, &block)
|
|
219
|
+
def render(view, state) # Emits JS registration script
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This is a non-visual component. It emits a `<script>` block that registers keyboard handlers. Context awareness is handled client-side: handlers check `document.activeElement` against suppression selectors (`.sw-mermaid-wrap`, `.sw-code-scroll`, `textarea`, `input[type=text]`).
|
|
224
|
+
|
|
225
|
+
"mod" maps to Meta on Mac, Control elsewhere. This mapping happens in JS.
|
|
226
|
+
|
|
227
|
+
### 3.5 HtmlExporter
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
# DSL usage (not a display component -- called programmatically)
|
|
231
|
+
StreamWeaver::Export::HtmlExporter.export(
|
|
232
|
+
app,
|
|
233
|
+
path: "~/.agent/diagrams/review.html",
|
|
234
|
+
inline_images: true
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Class (not a Component -- infrastructure)
|
|
238
|
+
module StreamWeaver::Export
|
|
239
|
+
class HtmlExporter
|
|
240
|
+
def initialize(app, state: {})
|
|
241
|
+
def export(path:, inline_images: false)
|
|
242
|
+
def to_html(inline_images: false) # => String
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
def collect_cdn_links # Mermaid, Chart.js, Prism.js, Google Fonts
|
|
246
|
+
def inline_css # All theme + component CSS
|
|
247
|
+
def inline_images_as_base64 # Convert image src to data URIs
|
|
248
|
+
def render_body # Phlex render to string
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 3.6 SlideContainer
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# DSL usage
|
|
257
|
+
slide_container mode: :swap, progress_bar: true do
|
|
258
|
+
slide "intro", "Introduction" do
|
|
259
|
+
# content
|
|
260
|
+
end
|
|
261
|
+
slide "arch", "Architecture" do
|
|
262
|
+
# content
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
slide_container mode: :scroll_snap, nav_dots: true, counter: true do
|
|
267
|
+
slide "title", type: :title do ... end
|
|
268
|
+
slide "content1", type: :content do ... end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Class
|
|
272
|
+
class Components::SlideContainer < Components::Base
|
|
273
|
+
attr_reader :mode, :progress_bar, :nav_dots, :counter, :keyboard_nav
|
|
274
|
+
attr_accessor :children # Array<Components::Slide>
|
|
275
|
+
|
|
276
|
+
def initialize(mode: :swap, progress_bar: true, keyboard_nav: true,
|
|
277
|
+
nav_dots: false, counter: false, **options)
|
|
278
|
+
def render(view, state)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
class Components::Slide < Components::Base
|
|
282
|
+
attr_reader :id, :title, :type
|
|
283
|
+
attr_accessor :children
|
|
284
|
+
|
|
285
|
+
def initialize(id, title = nil, type: :content, **options)
|
|
286
|
+
def render(view, state)
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Two modes share the same container class:
|
|
291
|
+
- `:swap` -- only active slide visible, Back/Next buttons, fade transition (deck use case)
|
|
292
|
+
- `:scroll_snap` -- all slides rendered, CSS `scroll-snap-type: y mandatory`, 100dvh per slide (explainer use case)
|
|
293
|
+
|
|
294
|
+
Both modes share: keyboard navigation state, progress tracking, Alpine.js `x-data` for current slide index.
|
|
295
|
+
|
|
296
|
+
### 3.7 ProgressIndicator
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
# DSL usage (usually implicit within slide_container)
|
|
300
|
+
progress_indicator current: 2, total: 5, position: :top
|
|
301
|
+
|
|
302
|
+
# Class
|
|
303
|
+
class Components::ProgressIndicator < Components::Base
|
|
304
|
+
def initialize(current: 0, total: 0, position: :top, animated: true, **options)
|
|
305
|
+
def render(view, state)
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Renders a fixed-position bar at the top of the viewport. Width = `(current / total) * 100%`. CSS transition on width for animation.
|
|
310
|
+
|
|
311
|
+
### 3.8 Toast
|
|
312
|
+
|
|
313
|
+
Already exists as `Components::ToastContainer` with `show_toast` DSL method. No new component needed. The existing implementation covers deck's needs for save confirmations and generation timeouts.
|
|
314
|
+
|
|
315
|
+
### 3.9 ImageBlock
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
# DSL usage
|
|
319
|
+
image_block "assets/diagram.png", alt: "Architecture", caption: "System overview"
|
|
320
|
+
image_block "/path/to/file.png", base64: true # for export mode
|
|
321
|
+
|
|
322
|
+
# Class
|
|
323
|
+
class Components::ImageBlock < Components::Base
|
|
324
|
+
attr_reader :src, :alt, :caption, :base64_mode
|
|
325
|
+
|
|
326
|
+
def initialize(src, alt: "", caption: nil, base64: false, **options)
|
|
327
|
+
def render(view, state)
|
|
328
|
+
def to_data_uri # Convert file to base64 data URI
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
StreamWeaver's Rack server serves static assets. For local file paths, the adapter generates a URL to an asset-serving endpoint. In export mode (`base64: true`), the image is inlined as a data URI.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 4. Design Deck Architecture
|
|
337
|
+
|
|
338
|
+
### 4.1 DesignDeck -- The Orchestrator
|
|
339
|
+
|
|
340
|
+
The design deck is **not** a subclass of `App`. It is a set of DSL methods available in `App` that compose shared components into the deck workflow. This follows the existing pattern where `tabs`, `modal`, and `form` are DSL methods on `App`, not separate app types.
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# Agent writes this Ruby script:
|
|
344
|
+
app "Architecture Direction", theme: :dark do
|
|
345
|
+
design_deck "Architecture Direction" do
|
|
346
|
+
slide "arch", "System Architecture", context: "Choose the backend pattern" do
|
|
347
|
+
option "Monolith", aside: "Simpler to deploy" do
|
|
348
|
+
mermaid "graph TD\n Client-->API\n API-->DB", compact: true
|
|
349
|
+
code_block "app.listen(3000)", lang: "ts"
|
|
350
|
+
end
|
|
351
|
+
option "Microservices", recommended: true do
|
|
352
|
+
mermaid "graph LR\n Gateway-->Auth\n Gateway-->Orders", compact: true
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
slide "db", "Database Strategy" do
|
|
357
|
+
option "PostgreSQL", aside: "ACID compliance" do
|
|
358
|
+
code_block "CREATE TABLE users (...)", lang: "sql"
|
|
359
|
+
end
|
|
360
|
+
option "MongoDB" do
|
|
361
|
+
code_block "db.users.insertOne({...})", lang: "javascript"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
# Class hierarchy
|
|
370
|
+
class Components::Deck::DesignDeck < Components::Base
|
|
371
|
+
attr_reader :title
|
|
372
|
+
attr_accessor :children # Array<Deck::DeckSlide>
|
|
373
|
+
|
|
374
|
+
def initialize(title, **options)
|
|
375
|
+
def render(view, state)
|
|
376
|
+
|
|
377
|
+
# Validates: no duplicate slide IDs, no "summary" ID, only one active deck
|
|
378
|
+
def validate!
|
|
379
|
+
end
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The `design_deck` DSL method on `App`:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
class App
|
|
386
|
+
def design_deck(title, **options, &block)
|
|
387
|
+
deck = Components::Deck::DesignDeck.new(title, **options)
|
|
388
|
+
@components << deck
|
|
389
|
+
@current_deck = deck
|
|
390
|
+
|
|
391
|
+
parent_components = @components
|
|
392
|
+
@components = []
|
|
393
|
+
instance_eval(&block)
|
|
394
|
+
deck.children = @components
|
|
395
|
+
@components = parent_components
|
|
396
|
+
|
|
397
|
+
# Auto-append summary slide
|
|
398
|
+
deck.children << Components::Deck::DeckSummary.new
|
|
399
|
+
deck.validate!
|
|
400
|
+
|
|
401
|
+
@current_deck = nil
|
|
402
|
+
deck
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def slide(id, title = nil, **options, &block)
|
|
406
|
+
raise "slide must be inside design_deck" unless @current_deck
|
|
407
|
+
slide = Components::Deck::DeckSlide.new(id, title, **options)
|
|
408
|
+
@components << slide
|
|
409
|
+
|
|
410
|
+
parent_components = @components
|
|
411
|
+
@components = []
|
|
412
|
+
instance_eval(&block)
|
|
413
|
+
slide.children = @components
|
|
414
|
+
@components = parent_components
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def option(label, **options, &block)
|
|
418
|
+
raise "option must be inside a slide" unless @current_deck
|
|
419
|
+
opt = Components::Deck::DeckOption.new(label, **options)
|
|
420
|
+
@components << opt
|
|
421
|
+
|
|
422
|
+
parent_components = @components
|
|
423
|
+
@components = []
|
|
424
|
+
instance_eval(&block)
|
|
425
|
+
opt.children = @components # preview blocks: mermaid, code_block, etc.
|
|
426
|
+
@components = parent_components
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 4.2 DeckSlide
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
class Components::Deck::DeckSlide < Components::Base
|
|
435
|
+
attr_reader :id, :title, :context_text, :columns
|
|
436
|
+
attr_accessor :children # Array<Deck::DeckOption>
|
|
437
|
+
|
|
438
|
+
def initialize(id, title, context: nil, columns: nil, **options)
|
|
439
|
+
def render(view, state)
|
|
440
|
+
def auto_columns # => 1, 2, 3, or 4 based on option count
|
|
441
|
+
end
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Renders as a grid of option cards within a slide wrapper. Auto-column detection: 1 option -> 1 col, 2 -> 2, 3 -> 3, 4+ -> 2 or auto-fit.
|
|
445
|
+
|
|
446
|
+
### 4.3 DeckOption
|
|
447
|
+
|
|
448
|
+
```ruby
|
|
449
|
+
class Components::Deck::DeckOption < Components::Base
|
|
450
|
+
attr_reader :label, :aside, :recommended
|
|
451
|
+
attr_accessor :children # Preview blocks
|
|
452
|
+
|
|
453
|
+
def initialize(label, aside: nil, recommended: false, description: nil, **options)
|
|
454
|
+
def render(view, state)
|
|
455
|
+
end
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Renders as a clickable card with:
|
|
459
|
+
- Radio indicator (unfilled circle / filled circle)
|
|
460
|
+
- Label header with optional "Recommended" badge
|
|
461
|
+
- Preview content area (children: mermaid, code_block, image_block, etc.)
|
|
462
|
+
- Aside text below preview
|
|
463
|
+
- Notes textarea
|
|
464
|
+
|
|
465
|
+
Selection state stored in `state[:deck_selections]` hash: `{ slide_id => option_label }`.
|
|
466
|
+
|
|
467
|
+
ARIA: `role="radio"`, `aria-checked`, parent grid has `role="radiogroup"`.
|
|
468
|
+
|
|
469
|
+
### 4.4 DeckSummary
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
class Components::Deck::DeckSummary < Components::Base
|
|
473
|
+
def initialize(**options)
|
|
474
|
+
def render(view, state)
|
|
475
|
+
# Reads state[:deck_selections] to build summary cards
|
|
476
|
+
# Shows: slide title, selected option label, preview thumbnail, aside, notes
|
|
477
|
+
# Submit button gated on complete selections
|
|
478
|
+
# Final notes textarea
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Auto-generated. No DSL call needed -- appended automatically by `design_deck`.
|
|
484
|
+
|
|
485
|
+
### 4.5 GenerateMoreControls
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
# Rendered within each slide's footer area
|
|
489
|
+
class Components::Deck::GenerateMoreControls < Components::Base
|
|
490
|
+
attr_reader :max_count
|
|
491
|
+
|
|
492
|
+
def initialize(max_count: 3, **options)
|
|
493
|
+
def render(view, state)
|
|
494
|
+
end
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Renders: prompt text input, count dropdown (1-3), "Generate" button, "Regenerate all" button. Uses Alpine.js for loading states.
|
|
498
|
+
|
|
499
|
+
### 4.6 Remaining Deck Components
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
# All in Components::Deck namespace
|
|
503
|
+
|
|
504
|
+
class SkeletonPlaceholder < Components::Base
|
|
505
|
+
def initialize(count: 1, **options)
|
|
506
|
+
# Shimmer animation: linear-gradient background-position 1.5s infinite
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
class ModelSelector < Components::Base
|
|
510
|
+
def initialize(models: [], default_model: nil, **options)
|
|
511
|
+
# Provider filter pills, model list, thinking level pills
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
class ConfirmationBar < Components::Base
|
|
515
|
+
def initialize(message:, confirm_label: "Cancel", cancel_label: "Keep Going",
|
|
516
|
+
auto_hide: 5, **options)
|
|
517
|
+
# Fixed top bar, slide-down, auto-hide timer
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
class CloseOverlay < Components::Base
|
|
521
|
+
def initialize(status:, message:, **options)
|
|
522
|
+
# Full-screen blur backdrop, color-coded, auto-close tab
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
class LayoutToggle < Components::Base
|
|
526
|
+
def initialize(**options)
|
|
527
|
+
# Footer buttons: 1 / 2 / 3 / 4 columns
|
|
528
|
+
end
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## 5. Visual Explainer Architecture
|
|
534
|
+
|
|
535
|
+
The visual explainer is simpler than the deck architecturally. It is a **page generator** -- the agent writes a Ruby script that uses shared components, and either:
|
|
536
|
+
1. Serves it live via StreamWeaver (for interactive viewing), or
|
|
537
|
+
2. Exports it as self-contained HTML (for sharing/archiving)
|
|
538
|
+
|
|
539
|
+
There is no special orchestrator class like `DesignDeck`. The agent simply composes components in an `app` block.
|
|
540
|
+
|
|
541
|
+
### 5.1 VeCard
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
# DSL usage
|
|
545
|
+
ve_card depth: :hero, accent: :a do
|
|
546
|
+
header2 "Executive Summary"
|
|
547
|
+
text "The changes introduce..."
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
ve_card depth: :recessed, label: "RISK" do
|
|
551
|
+
text "Cognitive complexity exceeds threshold"
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# Class
|
|
555
|
+
class Components::VeCard < Components::Base
|
|
556
|
+
attr_reader :depth, :accent, :label
|
|
557
|
+
attr_accessor :children
|
|
558
|
+
|
|
559
|
+
def initialize(depth: :default, accent: nil, label: nil, **options)
|
|
560
|
+
def render(view, state)
|
|
561
|
+
end
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Depth tiers map to CSS classes:
|
|
565
|
+
- `:hero` -- `sw-card--hero` (accent-tinted bg, elevated shadow)
|
|
566
|
+
- `:elevated` -- `sw-card--elevated` (subtle shadow)
|
|
567
|
+
- `:default` -- `sw-card--default` (flat, border)
|
|
568
|
+
- `:recessed` -- `sw-card--recessed` (inset shadow)
|
|
569
|
+
- `:glass` -- `sw-card--glass` (transparent, backdrop blur)
|
|
570
|
+
|
|
571
|
+
**Design Decision:** `VeCard` is a separate class from `Card`, not a variant. The existing `Card` has header/body/footer sub-components with specific rendering. `VeCard` is a simpler depth-styled container. Attempting to merge them creates complexity with minimal benefit.
|
|
572
|
+
|
|
573
|
+
### 5.2 KpiDashboard
|
|
574
|
+
|
|
575
|
+
```ruby
|
|
576
|
+
# DSL usage
|
|
577
|
+
kpi_dashboard metrics: [
|
|
578
|
+
{ value: "3,500", label: "Requests/sec", color: :blue, trend: :up },
|
|
579
|
+
{ value: "12ms", label: "P99 Latency", color: :green },
|
|
580
|
+
{ value: "99.97%", label: "Uptime", color: :green, trend: :flat }
|
|
581
|
+
]
|
|
582
|
+
|
|
583
|
+
# Class
|
|
584
|
+
class Components::KpiDashboard < Components::Base
|
|
585
|
+
attr_reader :metrics
|
|
586
|
+
|
|
587
|
+
def initialize(metrics:, **options)
|
|
588
|
+
def render(view, state)
|
|
589
|
+
# Renders auto-fit grid of KPI cards
|
|
590
|
+
# Each card: large value, label, optional trend arrow, fadeScale entry animation
|
|
591
|
+
end
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### 5.3 DataTable
|
|
595
|
+
|
|
596
|
+
```ruby
|
|
597
|
+
# DSL usage
|
|
598
|
+
data_table headers: ["File", "Lines", "Status"],
|
|
599
|
+
rows: [
|
|
600
|
+
["app.rb", "42", { status: :match, label: "Match" }],
|
|
601
|
+
["config.rb", "18", { status: :gap, label: "Missing" }]
|
|
602
|
+
],
|
|
603
|
+
sticky_header: true
|
|
604
|
+
|
|
605
|
+
# Class -- enhances existing Components::Table
|
|
606
|
+
class Components::DataTable < Components::Base
|
|
607
|
+
attr_reader :headers, :rows, :sticky_header, :alternating, :hover, :scrollable
|
|
608
|
+
|
|
609
|
+
def initialize(headers:, rows:, sticky_header: true, alternating: true,
|
|
610
|
+
hover: true, scrollable: true, **options)
|
|
611
|
+
def render(view, state)
|
|
612
|
+
end
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
**Design Decision:** New class rather than modifying existing `Table`. The existing `Table` accepts data in multiple formats (positional, headers+rows, file, path) with block support. `DataTable` has a focused API for the visual explainer's specific needs (status badges, sticky headers, scroll container). They can share CSS but the classes serve different use cases.
|
|
616
|
+
|
|
617
|
+
### 5.4 SidebarToc
|
|
618
|
+
|
|
619
|
+
```ruby
|
|
620
|
+
# DSL usage
|
|
621
|
+
sidebar_toc sections: [
|
|
622
|
+
{ id: "summary", label: "Executive Summary" },
|
|
623
|
+
{ id: "architecture", label: "Architecture" },
|
|
624
|
+
{ id: "risks", label: "Risks" }
|
|
625
|
+
]
|
|
626
|
+
|
|
627
|
+
# Class
|
|
628
|
+
class Components::SidebarToc < Components::Base
|
|
629
|
+
attr_reader :sections
|
|
630
|
+
|
|
631
|
+
def initialize(sections:, **options)
|
|
632
|
+
def render(view, state)
|
|
633
|
+
# Desktop: sticky 170px sidebar, IntersectionObserver scroll spy
|
|
634
|
+
# Mobile (<1000px): horizontal scrollable sticky bar
|
|
635
|
+
end
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### 5.5 Other Explainer Components
|
|
639
|
+
|
|
640
|
+
```ruby
|
|
641
|
+
# Comparison panels
|
|
642
|
+
comparison before_label: "Current", after_label: "Proposed" do
|
|
643
|
+
before { mermaid "graph TD\n A-->B" }
|
|
644
|
+
after { mermaid "graph TD\n A-->B\n B-->C" }
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
class Components::Comparison < Components::Base
|
|
648
|
+
attr_accessor :before_content, :after_content, :before_label, :after_label
|
|
649
|
+
def initialize(before_label: "Before", after_label: "After", **options)
|
|
650
|
+
def render(view, state)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Pipeline
|
|
654
|
+
pipeline steps: [
|
|
655
|
+
{ label: "Parse", description: "Read input", status: :complete },
|
|
656
|
+
{ label: "Transform", description: "Apply rules", status: :active },
|
|
657
|
+
{ label: "Emit", description: "Write output", status: :pending }
|
|
658
|
+
]
|
|
659
|
+
|
|
660
|
+
class Components::Pipeline < Components::Base
|
|
661
|
+
attr_reader :steps
|
|
662
|
+
def initialize(steps:, **options)
|
|
663
|
+
def render(view, state)
|
|
664
|
+
# Horizontal flow with arrow connectors, responsive vertical fallback
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Chart (Chart.js)
|
|
668
|
+
chart type: :bar, data: { labels: [...], datasets: [...] }, height: 300
|
|
669
|
+
|
|
670
|
+
class Components::Chart < Components::Base
|
|
671
|
+
attr_reader :type, :data, :chart_options, :height
|
|
672
|
+
def initialize(type:, data:, options: {}, height: 300, **extra)
|
|
673
|
+
def render(view, state)
|
|
674
|
+
def cdn_assets # => [:chartjs]
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# Callout
|
|
678
|
+
callout variant: :warning, title: "Breaking Change" do
|
|
679
|
+
text "This removes the deprecated API endpoint."
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
class Components::Callout < Components::Base
|
|
683
|
+
attr_reader :variant, :title
|
|
684
|
+
attr_accessor :children
|
|
685
|
+
def initialize(variant: :info, title: nil, **options)
|
|
686
|
+
def render(view, state)
|
|
687
|
+
# Colored left border: info=blue, warning=amber, success=green, tip=purple
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Prose
|
|
691
|
+
prose width: :narrow, dropcap: true do
|
|
692
|
+
md "Long-form content with **markdown** support..."
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
class Components::Prose < Components::Base
|
|
696
|
+
attr_reader :width, :dropcap
|
|
697
|
+
attr_accessor :children
|
|
698
|
+
def initialize(width: :default, dropcap: false, **options)
|
|
699
|
+
def render(view, state)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Pullquote
|
|
703
|
+
pullquote "Design is not just what it looks like. Design is how it works.",
|
|
704
|
+
attribution: "Steve Jobs"
|
|
705
|
+
|
|
706
|
+
class Components::Pullquote < Components::Base
|
|
707
|
+
attr_reader :text, :attribution, :centered
|
|
708
|
+
def initialize(text, attribution: nil, centered: false, **options)
|
|
709
|
+
def render(view, state)
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Hero Section
|
|
713
|
+
hero_section variant: :centered do
|
|
714
|
+
header1 "Diff Review: feature/auth"
|
|
715
|
+
text "main...feature/auth -- 14 files changed"
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
class Components::HeroSection < Components::Base
|
|
719
|
+
attr_reader :variant
|
|
720
|
+
attr_accessor :children
|
|
721
|
+
def initialize(variant: :centered, **options)
|
|
722
|
+
def render(view, state)
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Directory Tree
|
|
726
|
+
dir_tree <<~TREE
|
|
727
|
+
src/
|
|
728
|
+
app.rb [modified]
|
|
729
|
+
config.rb [new]
|
|
730
|
+
routes/
|
|
731
|
+
api.rb [modified]
|
|
732
|
+
TREE
|
|
733
|
+
|
|
734
|
+
class Components::DirTree < Components::Base
|
|
735
|
+
attr_reader :tree
|
|
736
|
+
def initialize(tree, **options)
|
|
737
|
+
def render(view, state)
|
|
738
|
+
# Monospace, color-coded: green=new, amber=modified, red=deleted
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# Legend
|
|
742
|
+
legend items: [
|
|
743
|
+
{ color: "#22c55e", label: "New" },
|
|
744
|
+
{ color: "#eab308", label: "Modified" },
|
|
745
|
+
{ color: "#ef4444", label: "Deleted" }
|
|
746
|
+
]
|
|
747
|
+
|
|
748
|
+
class Components::Legend < Components::Base
|
|
749
|
+
attr_reader :items
|
|
750
|
+
def initialize(items:, **options)
|
|
751
|
+
def render(view, state)
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Flow Arrow
|
|
755
|
+
flow_arrow label: "transforms into"
|
|
756
|
+
|
|
757
|
+
class Components::FlowArrow < Components::Base
|
|
758
|
+
attr_reader :label
|
|
759
|
+
def initialize(label: nil, **options)
|
|
760
|
+
def render(view, state)
|
|
761
|
+
end
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### 5.6 Theme Presets
|
|
765
|
+
|
|
766
|
+
```ruby
|
|
767
|
+
# DSL usage (at app level)
|
|
768
|
+
app "Diff Review", theme: :dark do
|
|
769
|
+
theme_preset :editorial # Sets fonts, colors, surface styles
|
|
770
|
+
# ... page content
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# Infrastructure
|
|
774
|
+
module StreamWeaver::Theme::Presets
|
|
775
|
+
PRESETS = {
|
|
776
|
+
editorial: {
|
|
777
|
+
fonts: { display: "Instrument Serif", body: "Source Sans 3", mono: "JetBrains Mono" },
|
|
778
|
+
palette: { accent: "#c2825a", accent_dim: "rgba(194,130,90,0.15)",
|
|
779
|
+
surface: "#1a1917", text: "#e8e4dc" }
|
|
780
|
+
},
|
|
781
|
+
technical: {
|
|
782
|
+
fonts: { display: "DM Sans", body: "DM Sans", mono: "Fira Code" },
|
|
783
|
+
palette: { accent: "#14b8a6", accent_dim: "rgba(20,184,166,0.15)",
|
|
784
|
+
surface: "#0f172a", text: "#e2e8f0" }
|
|
785
|
+
},
|
|
786
|
+
warm: { ... },
|
|
787
|
+
minimal: { ... },
|
|
788
|
+
terminal: { ... },
|
|
789
|
+
# Slide-specific:
|
|
790
|
+
midnight_editorial: { ... },
|
|
791
|
+
warm_signal: { ... },
|
|
792
|
+
terminal_mono: { ... },
|
|
793
|
+
swiss_clean: { ... }
|
|
794
|
+
}.freeze
|
|
795
|
+
|
|
796
|
+
def self.apply(name, app)
|
|
797
|
+
preset = PRESETS.fetch(name)
|
|
798
|
+
# Registers Google Fonts CDN links
|
|
799
|
+
# Sets CSS custom properties via theme_overrides
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## 6. Agent Communication Protocol
|
|
807
|
+
|
|
808
|
+
### 6.1 Primary Protocol: Agent Writes a Ruby Script
|
|
809
|
+
|
|
810
|
+
The agent generates a Ruby file, runs it, and StreamWeaver serves the result.
|
|
811
|
+
|
|
812
|
+
```
|
|
813
|
+
Agent StreamWeaver Browser
|
|
814
|
+
| | |
|
|
815
|
+
|-- writes deck.rb ------->| |
|
|
816
|
+
|-- runs: ruby deck.rb --->| |
|
|
817
|
+
| |-- starts Puma server ---->|
|
|
818
|
+
| |-- opens browser --------->|
|
|
819
|
+
| | |-- user views deck
|
|
820
|
+
| | |
|
|
821
|
+
| |<-- SSE /stream -----------|
|
|
822
|
+
| | |
|
|
823
|
+
| |<-- POST /action/submit ---| (user submits)
|
|
824
|
+
| |-- result -> stdout ------>|
|
|
825
|
+
|<-- reads stdout ---------| |
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
For **design deck** (bidirectional): use `run_once!` which blocks until the user submits, then returns the result as JSON to stdout.
|
|
829
|
+
|
|
830
|
+
For **visual explainer** (unidirectional): use `run!` for live serving, or `HtmlExporter.export` for static file output.
|
|
831
|
+
|
|
832
|
+
### 6.2 Agent Receives Deck Results
|
|
833
|
+
|
|
834
|
+
The deck uses StreamWeaver's existing `run_once!` pattern:
|
|
835
|
+
|
|
836
|
+
```ruby
|
|
837
|
+
# Agent generates and runs this:
|
|
838
|
+
result = app("Architecture Direction") {
|
|
839
|
+
design_deck "Architecture Direction" do
|
|
840
|
+
slide "arch", "System Architecture" do
|
|
841
|
+
option "Monolith" do ... end
|
|
842
|
+
option "Microservices" do ... end
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
}.run_once!(auto_close_window: true)
|
|
846
|
+
|
|
847
|
+
# result => { deck_selections: { "arch" => "Microservices" }, deck_notes: { ... } }
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
The `run_once!` method already exists and handles:
|
|
851
|
+
- Starting the server
|
|
852
|
+
- Opening the browser
|
|
853
|
+
- Blocking until form submission
|
|
854
|
+
- Returning state as JSON
|
|
855
|
+
- Shutting down
|
|
856
|
+
|
|
857
|
+
The deck's "Submit" button on the summary slide sets `state[:_result]`, which triggers `run_once!` to unblock and return.
|
|
858
|
+
|
|
859
|
+
### 6.3 Agent Pushes Generate-More Options (Push-to-State)
|
|
860
|
+
|
|
861
|
+
The agent pushes new options into server-side state via `POST /deck/add_option`. StreamWeaver's reactive re-render handles display -- no direct DOM manipulation via SSE.
|
|
862
|
+
|
|
863
|
+
```ruby
|
|
864
|
+
# Agent script (simplified):
|
|
865
|
+
app_instance = app("My Deck") {
|
|
866
|
+
design_deck "My Deck" do
|
|
867
|
+
slide "arch", "Architecture" do
|
|
868
|
+
option "Monolith" do ... end
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
# The agent polls for generate requests, then pushes options to state:
|
|
874
|
+
# 1. User clicks "Generate" -> browser POSTs to /deck/generate
|
|
875
|
+
# 2. Server queues the request, updates state to :generating (re-render shows skeletons)
|
|
876
|
+
# 3. Agent polls GET /deck/pending, receives the request
|
|
877
|
+
# 4. Agent generates new options with LLM
|
|
878
|
+
# 5. Agent pushes each option to state via POST /deck/add_option
|
|
879
|
+
# 6. Server updates state hash, triggers SSE re-render notification
|
|
880
|
+
# 7. Browser re-renders -- new option appears, skeleton count decreases,
|
|
881
|
+
# summary slide automatically reflects the new option
|
|
882
|
+
|
|
883
|
+
# See Section 7 for the full state machine and endpoint details.
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### 6.4 Visual Explainer Output
|
|
887
|
+
|
|
888
|
+
For the explainer, the agent either:
|
|
889
|
+
|
|
890
|
+
**Option A: Live server** (interactive viewing with scroll spy, theme toggle):
|
|
891
|
+
```ruby
|
|
892
|
+
app "Diff Review: feature/auth", theme: :dark do
|
|
893
|
+
theme_preset :editorial
|
|
894
|
+
sidebar_toc sections: [...]
|
|
895
|
+
hero_section { header1 "Diff Review" }
|
|
896
|
+
ve_card(depth: :hero) { ... }
|
|
897
|
+
# ...
|
|
898
|
+
end
|
|
899
|
+
# App.run! starts server and opens browser
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
**Option B: Static HTML export** (for sharing, archiving):
|
|
903
|
+
```ruby
|
|
904
|
+
my_app = app "Diff Review: feature/auth", theme: :dark do
|
|
905
|
+
# ... same DSL
|
|
906
|
+
end
|
|
907
|
+
StreamWeaver::Export::HtmlExporter.export(my_app, path: "~/.agent/diagrams/review.html")
|
|
908
|
+
system("open", "~/.agent/diagrams/review.html")
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### 6.5 Agent Integration (No Skill Classes in Gem)
|
|
912
|
+
|
|
913
|
+
**The DSL IS the API.** Skill entry point classes (`DesignDeckSkill`, `VisualExplainerSkill`) are intentionally excluded from the gem. The agent communicates with StreamWeaver by writing Ruby DSL scripts directly -- no wrapper classes needed.
|
|
914
|
+
|
|
915
|
+
Agent glue code (slash commands like `/design-deck`, `/diff-review`, etc.) lives outside the gem as **Claude Code custom commands** in the project's `.claude/commands/` directory. These commands handle data gathering (git diffs, file reading, codebase analysis) and then emit DSL scripts that StreamWeaver renders.
|
|
916
|
+
|
|
917
|
+
This separation keeps the gem focused on rendering and keeps agent-specific orchestration where it belongs -- in the agent's command layer. See Q9 in Section 12 for the rationale.
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
## 7. The Generate-More Loop (Push-to-State Architecture)
|
|
922
|
+
|
|
923
|
+
This is the most architecturally complex feature. The key architectural decision is **push-to-state, not push-to-DOM**: the agent pushes new options into server-side state, and StreamWeaver's reactive re-render handles display. This eliminates the "phantom option" race condition (user on Slide B when options arrive for Slide A), keeps the summary slide in sync automatically, and aligns with StreamWeaver's existing reactive model.
|
|
924
|
+
|
|
925
|
+
### 7.1 State Machine
|
|
926
|
+
|
|
927
|
+
```
|
|
928
|
+
IDLE
|
|
929
|
+
|
|
|
930
|
+
[user clicks Generate]
|
|
931
|
+
|
|
|
932
|
+
v
|
|
933
|
+
GENERATING
|
|
934
|
+
/ \
|
|
935
|
+
[agent pushes [timeout 30s]
|
|
936
|
+
option to state] |
|
|
937
|
+
| v
|
|
938
|
+
v TIMED_OUT
|
|
939
|
+
state updated |
|
|
940
|
+
re-render fires [show toast]
|
|
941
|
+
| |
|
|
942
|
+
[all received] v
|
|
943
|
+
| IDLE
|
|
944
|
+
v
|
|
945
|
+
IDLE
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
The critical difference from push-to-DOM: options are added to the state hash, which triggers a reactive re-render of the current view. The browser never receives raw HTML snippets via SSE -- it receives state-change notifications that cause the existing component tree to re-render.
|
|
949
|
+
|
|
950
|
+
### 7.2 State Storage
|
|
951
|
+
|
|
952
|
+
```ruby
|
|
953
|
+
# In app state hash:
|
|
954
|
+
state[:deck_generate] = {
|
|
955
|
+
status: :idle, # :idle | :generating | :timed_out
|
|
956
|
+
slide_id: nil, # Which slide is generating
|
|
957
|
+
requested_count: 0, # How many options requested
|
|
958
|
+
received_count: 0, # How many received so far
|
|
959
|
+
prompt: nil, # User's custom prompt
|
|
960
|
+
started_at: nil # For timeout tracking
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
# Generated options live in the slide's options array (NOT a separate SSE target):
|
|
964
|
+
state[:deck_slides]["arch"][:options] << {
|
|
965
|
+
label: "Event-Driven",
|
|
966
|
+
aside: "Decoupled via message bus",
|
|
967
|
+
children: [{ type: :mermaid, code: "graph LR\n Events-->Handler", compact: true }],
|
|
968
|
+
generated: true # Flag to distinguish from original options
|
|
969
|
+
}
|
|
970
|
+
# Adding to state triggers re-render -- DeckSlide component picks up new options
|
|
971
|
+
# automatically, and DeckSummary reflects them because it reads the same state.
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
### 7.3 Data Flow (Push-to-State)
|
|
975
|
+
|
|
976
|
+
```
|
|
977
|
+
Browser Server (Puma) Agent Script
|
|
978
|
+
| | |
|
|
979
|
+
|--[click Generate]--------->| |
|
|
980
|
+
| POST /deck/generate | |
|
|
981
|
+
| {slide_id, count, prompt} | |
|
|
982
|
+
| |--[queue request]-------->|
|
|
983
|
+
| | |
|
|
984
|
+
|<-[update generate status]--| |
|
|
985
|
+
| state[:deck_generate] | |
|
|
986
|
+
| status: :generating | |
|
|
987
|
+
| (re-render shows | |
|
|
988
|
+
| skeleton placeholders) | |
|
|
989
|
+
| | |
|
|
990
|
+
| |<---[poll for requests]---|
|
|
991
|
+
| | GET /deck/pending |
|
|
992
|
+
| | |
|
|
993
|
+
| | [agent generates
|
|
994
|
+
| | option with LLM]
|
|
995
|
+
| | |
|
|
996
|
+
| |<--[push option to state]-|
|
|
997
|
+
| | POST /deck/add_option |
|
|
998
|
+
| | {slide_id, option_data}|
|
|
999
|
+
| | |
|
|
1000
|
+
|<-[SSE: state changed]-----| |
|
|
1001
|
+
| re-render picks up new | |
|
|
1002
|
+
| option from state hash | |
|
|
1003
|
+
| skeleton count decreases | |
|
|
1004
|
+
| summary slide updates | |
|
|
1005
|
+
| | |
|
|
1006
|
+
| [repeat for each option] | |
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
### 7.4 Server-Side Endpoints (New Routes)
|
|
1010
|
+
|
|
1011
|
+
```ruby
|
|
1012
|
+
# In the SinatraApp (or as a Sinatra extension):
|
|
1013
|
+
|
|
1014
|
+
# User requests more options
|
|
1015
|
+
post '/deck/generate' do
|
|
1016
|
+
slide_id = params[:slide_id]
|
|
1017
|
+
count = params[:count].to_i
|
|
1018
|
+
prompt = params[:prompt]
|
|
1019
|
+
session_id = session[:session_id]
|
|
1020
|
+
|
|
1021
|
+
# Update generate status in state (triggers re-render with skeletons)
|
|
1022
|
+
state = session[:streamlit_state] ||= {}
|
|
1023
|
+
state[:deck_generate] = {
|
|
1024
|
+
status: :generating,
|
|
1025
|
+
slide_id: slide_id,
|
|
1026
|
+
requested_count: count,
|
|
1027
|
+
received_count: 0,
|
|
1028
|
+
prompt: prompt,
|
|
1029
|
+
started_at: Time.now.to_i
|
|
1030
|
+
}
|
|
1031
|
+
session[:streamlit_state] = state
|
|
1032
|
+
|
|
1033
|
+
# Store in session-scoped thread-safe queue for agent polling.
|
|
1034
|
+
# Queue entries include session_id so stale requests from killed
|
|
1035
|
+
# agent processes don't persist across sessions.
|
|
1036
|
+
settings.generate_requests << {
|
|
1037
|
+
session_id: session_id,
|
|
1038
|
+
slide_id: slide_id,
|
|
1039
|
+
count: count,
|
|
1040
|
+
prompt: prompt,
|
|
1041
|
+
timestamp: Time.now
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
status 202
|
|
1045
|
+
{ status: "generating", skeletons: count }.to_json
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
# Agent polls for pending generate requests
|
|
1049
|
+
get '/deck/pending' do
|
|
1050
|
+
content_type :json
|
|
1051
|
+
session_id = params[:session_id]
|
|
1052
|
+
requests = settings.generate_requests
|
|
1053
|
+
# Only return requests matching this session; discard stale ones
|
|
1054
|
+
pending = requests.select { |r| r[:session_id] == session_id }
|
|
1055
|
+
requests.reject! { |r| r[:session_id] == session_id }
|
|
1056
|
+
{ requests: pending }.to_json
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
# Agent pushes a generated option into state
|
|
1060
|
+
post '/deck/add_option' do
|
|
1061
|
+
slide_id = params[:slide_id]
|
|
1062
|
+
option_data = JSON.parse(request.body.read, symbolize_names: true)
|
|
1063
|
+
|
|
1064
|
+
state = session[:streamlit_state] ||= {}
|
|
1065
|
+
state[:deck_slides] ||= {}
|
|
1066
|
+
state[:deck_slides][slide_id] ||= { options: [] }
|
|
1067
|
+
state[:deck_slides][slide_id][:options] << option_data.merge(generated: true)
|
|
1068
|
+
|
|
1069
|
+
# Update received count; reset status if all received
|
|
1070
|
+
gen = state[:deck_generate]
|
|
1071
|
+
if gen && gen[:slide_id] == slide_id
|
|
1072
|
+
gen[:received_count] += 1
|
|
1073
|
+
gen[:status] = :idle if gen[:received_count] >= gen[:requested_count]
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
session[:streamlit_state] = state
|
|
1077
|
+
|
|
1078
|
+
# Trigger SSE re-render notification to connected browsers
|
|
1079
|
+
settings.streamer&.notify_state_change
|
|
1080
|
+
|
|
1081
|
+
status 200
|
|
1082
|
+
{ received: gen&.dig(:received_count) }.to_json
|
|
1083
|
+
end
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
**Session-scoped queue cleanup:** When a session expires or a heartbeat timeout fires (see 7.7), all queue entries with that `session_id` are purged. This prevents stale requests from killed agent processes from accumulating.
|
|
1087
|
+
|
|
1088
|
+
### 7.5 Agent-Side Polling
|
|
1089
|
+
|
|
1090
|
+
```ruby
|
|
1091
|
+
# The agent's deck script includes a generate-more handler:
|
|
1092
|
+
Thread.new do
|
|
1093
|
+
loop do
|
|
1094
|
+
response = Net::HTTP.get(URI("#{url}/deck/pending?session_id=#{session_id}"))
|
|
1095
|
+
requests = JSON.parse(response)["requests"]
|
|
1096
|
+
|
|
1097
|
+
requests.each do |req|
|
|
1098
|
+
# Agent calls LLM to generate options
|
|
1099
|
+
new_options = generate_options(req["slide_id"], req["count"], req["prompt"])
|
|
1100
|
+
|
|
1101
|
+
# Push each option to server-side state (NOT directly to DOM)
|
|
1102
|
+
new_options.each do |opt|
|
|
1103
|
+
uri = URI("#{url}/deck/add_option")
|
|
1104
|
+
Net::HTTP.post(uri, {
|
|
1105
|
+
slide_id: req["slide_id"],
|
|
1106
|
+
label: opt[:label],
|
|
1107
|
+
aside: opt[:aside],
|
|
1108
|
+
children: opt[:children] # Serialized component descriptions
|
|
1109
|
+
}.to_json, "Content-Type" => "application/json")
|
|
1110
|
+
end
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
sleep 1
|
|
1114
|
+
end
|
|
1115
|
+
end
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
### 7.6 Timeout Handling
|
|
1119
|
+
|
|
1120
|
+
Client-side: the generating state drives skeleton display. If `state[:deck_generate][:status]` remains `:generating` for 30 seconds (checked via the `started_at` timestamp), the client triggers a state update to `:timed_out`, shows a toast, and restores the Generate button. Server-side: the `started_at` timestamp enables the agent to also detect stale requests and skip them.
|
|
1121
|
+
|
|
1122
|
+
### 7.7 Cancellation and Heartbeat
|
|
1123
|
+
|
|
1124
|
+
**Tab-close / beforeunload:** The browser sends a `beforeunload` beacon to `POST /deck/disconnect`. However, `beforeunload` is unreliable. The primary mechanism is a heartbeat.
|
|
1125
|
+
|
|
1126
|
+
**Heartbeat mechanism:** The browser pings `POST /heartbeat` every 5 seconds. If the server detects no heartbeat for 10 seconds (2x interval), it assumes the browser disconnected and:
|
|
1127
|
+
1. Sets `state[:deck_generate][:status] = :idle`
|
|
1128
|
+
2. Purges pending generate requests for that session from the queue
|
|
1129
|
+
3. Marks the session as stale for cleanup
|
|
1130
|
+
|
|
1131
|
+
**Timeout:** `run_once!` accepts an optional `timeout:` parameter (default: `nil` = no timeout). When set, the server will auto-return a timeout result after the specified duration:
|
|
1132
|
+
|
|
1133
|
+
```ruby
|
|
1134
|
+
result = app("My Deck") { ... }.run_once!(timeout: 300) # 5 minute timeout
|
|
1135
|
+
# result => { _timeout: true } if timed out
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
**Agent process crash:** If the agent process dies, it stops polling `/deck/pending`. The heartbeat mechanism on the browser side continues. Pending generate requests accumulate but are cleaned up when the session expires or when a new agent connects with a fresh session.
|
|
1139
|
+
|
|
1140
|
+
---
|
|
1141
|
+
|
|
1142
|
+
## 7A. State Ownership Table
|
|
1143
|
+
|
|
1144
|
+
This table documents what state lives where and how it stays in sync across client and server.
|
|
1145
|
+
|
|
1146
|
+
| State | Location | Sync Mechanism |
|
|
1147
|
+
|-------|----------|----------------|
|
|
1148
|
+
| Deck selections | Server (file-backed) | POST on select |
|
|
1149
|
+
| Deck notes | Server (file-backed) | POST on blur/change |
|
|
1150
|
+
| Current slide index | Client (Alpine.js) | URL hash + x-data |
|
|
1151
|
+
| Generate status | Server state hash | SSE push on change |
|
|
1152
|
+
| Theme preference | Client (localStorage) | Read on load |
|
|
1153
|
+
| Keyboard shortcuts | Client (JS) | Static registration |
|
|
1154
|
+
|
|
1155
|
+
**File-backed state:** Deck selections and notes use file-backed server state (option B from Q1). Codex's argument about cookie overflow with user-generated notes was decisive -- a single long note can exceed the 4KB cookie limit. The session cookie stores a session ID; the full state is persisted to a JSON file in the session directory.
|
|
1156
|
+
|
|
1157
|
+
---
|
|
1158
|
+
|
|
1159
|
+
## 8. Data Flow Diagrams
|
|
1160
|
+
|
|
1161
|
+
### 8.1 Design Deck: Agent Creates Deck, User Selects, Agent Gets Results
|
|
1162
|
+
|
|
1163
|
+
```
|
|
1164
|
+
Agent StreamWeaver Browser User
|
|
1165
|
+
| | | |
|
|
1166
|
+
| write deck.rb | | |
|
|
1167
|
+
| ruby deck.rb | | |
|
|
1168
|
+
| (blocks on run_once!) | | |
|
|
1169
|
+
| | Puma starts | |
|
|
1170
|
+
| | open browser -------->| |
|
|
1171
|
+
| | | render slide 1 |
|
|
1172
|
+
| | |<---- views -------|
|
|
1173
|
+
| | | |
|
|
1174
|
+
| | |---- click opt --->|
|
|
1175
|
+
| |<-- Alpine x-model -----| (client-side) |
|
|
1176
|
+
| | | |
|
|
1177
|
+
| | |---- Next -------->|
|
|
1178
|
+
| | | render slide 2 |
|
|
1179
|
+
| | | |
|
|
1180
|
+
| | |---- submit ------>|
|
|
1181
|
+
| |<-- POST /action/submit | |
|
|
1182
|
+
| | | |
|
|
1183
|
+
| | state[:_result] set | |
|
|
1184
|
+
| run_once! unblocks | | |
|
|
1185
|
+
|<-- JSON result -----------| | |
|
|
1186
|
+
| | server shuts down | |
|
|
1187
|
+
| | | window closes |
|
|
1188
|
+
| reads selections | | |
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
### 8.2 Generate-More: User Requests, Agent Generates, State Updates (Push-to-State)
|
|
1192
|
+
|
|
1193
|
+
```
|
|
1194
|
+
User Browser Server Agent
|
|
1195
|
+
| | | |
|
|
1196
|
+
| click Gen | | |
|
|
1197
|
+
|-------------->| POST /deck/gen | |
|
|
1198
|
+
| | {slide, count} | |
|
|
1199
|
+
| |------------------->| |
|
|
1200
|
+
| | | queue request |
|
|
1201
|
+
| |<-- state change ---| |
|
|
1202
|
+
| sees shimmer | re-render shows | |
|
|
1203
|
+
| | skeleton cards | |
|
|
1204
|
+
| | |<-- GET /pending ---| (poll)
|
|
1205
|
+
| | |--- {requests} ---->|
|
|
1206
|
+
| | | |
|
|
1207
|
+
| | | [LLM generates]
|
|
1208
|
+
| | | |
|
|
1209
|
+
| | |<- POST /add_opt ---|
|
|
1210
|
+
| | | {slide, option} |
|
|
1211
|
+
| | | update state hash |
|
|
1212
|
+
| |<-- SSE: re-render -| |
|
|
1213
|
+
| sees option | state has new opt | |
|
|
1214
|
+
| | component re-renders |
|
|
1215
|
+
| | summary also updates |
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
### 8.3 Visual Explainer: Agent Generates Page, User Views
|
|
1219
|
+
|
|
1220
|
+
```
|
|
1221
|
+
Agent StreamWeaver Browser
|
|
1222
|
+
| | |
|
|
1223
|
+
| write page.rb | |
|
|
1224
|
+
| ruby page.rb | |
|
|
1225
|
+
| | Puma starts |
|
|
1226
|
+
| | open browser --------->|
|
|
1227
|
+
| | | render full page
|
|
1228
|
+
| | | scroll spy active
|
|
1229
|
+
| | | theme toggle works
|
|
1230
|
+
| | |
|
|
1231
|
+
| (agent done, server | |
|
|
1232
|
+
| keeps running for user) | |
|
|
1233
|
+
|
|
1234
|
+
--- OR (static export) ---
|
|
1235
|
+
|
|
1236
|
+
| write page.rb | |
|
|
1237
|
+
| ruby page.rb | |
|
|
1238
|
+
| | HtmlExporter.export |
|
|
1239
|
+
| | writes review.html |
|
|
1240
|
+
| open review.html -------->| |
|
|
1241
|
+
| | | render from file
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
---
|
|
1245
|
+
|
|
1246
|
+
## 9. CSS/JS Asset Strategy
|
|
1247
|
+
|
|
1248
|
+
### 9.1 External Libraries (CDN)
|
|
1249
|
+
|
|
1250
|
+
| Library | CDN URL | Used By |
|
|
1251
|
+
|---------|---------|---------|
|
|
1252
|
+
| Mermaid.js 11 | `cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs` | Mermaid |
|
|
1253
|
+
| Mermaid ELK | `cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs` | Mermaid (ELK layout) |
|
|
1254
|
+
| Prism.js | `cdn.jsdelivr.net/npm/prismjs@1/prism.min.js` + autoloader | CodeBlock |
|
|
1255
|
+
| Chart.js 4 | `cdn.jsdelivr.net/npm/chart.js@4` | Chart |
|
|
1256
|
+
| Google Fonts | `fonts.googleapis.com` | Theme presets |
|
|
1257
|
+
|
|
1258
|
+
**Decision: CDN, not vendored.** Rationale:
|
|
1259
|
+
1. StreamWeaver is a local dev tool, not a production web app -- CDN latency is acceptable
|
|
1260
|
+
2. Self-contained HTML export preserves CDN links (they work offline if cached)
|
|
1261
|
+
3. Vendoring would bloat the gem significantly
|
|
1262
|
+
4. The existing AlpineJS + HTMX are already loaded from CDN
|
|
1263
|
+
|
|
1264
|
+
### 9.2 Custom JS Assets
|
|
1265
|
+
|
|
1266
|
+
StreamWeaver needs to serve custom JS files for:
|
|
1267
|
+
- **Mermaid zoom engine** (~200 lines) -- zoom, pan, expand controls
|
|
1268
|
+
- **Keyboard shortcuts** -- centralized registration with context awareness
|
|
1269
|
+
- **Slide navigation** -- swap mode + scroll-snap mode logic
|
|
1270
|
+
- **Deck selection** -- radio behavior, number key selection
|
|
1271
|
+
- **Generate-more client** -- SSE listener, skeleton replacement
|
|
1272
|
+
|
|
1273
|
+
These are served from `lib/stream_weaver/assets/js/` via a new Sinatra route:
|
|
1274
|
+
|
|
1275
|
+
```ruby
|
|
1276
|
+
get '/assets/js/:filename' do
|
|
1277
|
+
file = File.join(ASSETS_DIR, "js", params[:filename])
|
|
1278
|
+
halt 404 unless File.exist?(file)
|
|
1279
|
+
content_type 'application/javascript'
|
|
1280
|
+
File.read(file)
|
|
1281
|
+
end
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
### 9.3 CSS Strategy
|
|
1285
|
+
|
|
1286
|
+
**Naming Convention:** All visual skills CSS classes MUST use the `sw-` prefix to prevent conflicts with user styles. Examples: `sw-mermaid-wrap`, `sw-code-block`, `sw-deck-option`, `sw-card--hero`. BEM-style modifiers use double-dash: `sw-component--variant`. This is already visible in the component examples throughout this document (e.g., `sw-mermaid--compact`, `sw-mermaid--zoom`, `sw-code-block`, `sw-card--elevated`).
|
|
1287
|
+
|
|
1288
|
+
StreamWeaver already has a CSS module (`StreamWeaver::CSS`) that generates the full stylesheet. New visual skills CSS is added as additional stylesheet sections:
|
|
1289
|
+
|
|
1290
|
+
```ruby
|
|
1291
|
+
module StreamWeaver::CSS
|
|
1292
|
+
def self.visual_skills_css
|
|
1293
|
+
# Returns CSS for: mermaid containers, code blocks, depth tiers,
|
|
1294
|
+
# deck layouts, slide transitions, explainer typography, etc.
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
def self.full_stylesheet
|
|
1298
|
+
# Existing call -- augmented to include visual_skills_css
|
|
1299
|
+
end
|
|
1300
|
+
end
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
For theme presets, CSS custom properties are injected via `<style>` blocks in the page head:
|
|
1304
|
+
|
|
1305
|
+
```css
|
|
1306
|
+
:root {
|
|
1307
|
+
--sw-vs-font-display: 'Instrument Serif', serif;
|
|
1308
|
+
--sw-vs-font-body: 'Source Sans 3', sans-serif;
|
|
1309
|
+
--sw-vs-font-mono: 'JetBrains Mono', monospace;
|
|
1310
|
+
--sw-vs-accent: #c2825a;
|
|
1311
|
+
--sw-vs-surface-hero: rgba(194, 130, 90, 0.08);
|
|
1312
|
+
/* ... */
|
|
1313
|
+
}
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
### 9.4 CDN Asset Declaration
|
|
1317
|
+
|
|
1318
|
+
Components declare which CDN assets they need via a `cdn_assets` method. The view collects all required assets and deduplicates:
|
|
1319
|
+
|
|
1320
|
+
```ruby
|
|
1321
|
+
class Components::Mermaid < Components::Base
|
|
1322
|
+
def cdn_assets
|
|
1323
|
+
assets = [:mermaid]
|
|
1324
|
+
assets << :mermaid_elk if @layout == :elk
|
|
1325
|
+
assets
|
|
1326
|
+
end
|
|
1327
|
+
end
|
|
1328
|
+
|
|
1329
|
+
# View (Phlex) collects:
|
|
1330
|
+
def head_scripts
|
|
1331
|
+
required = collect_cdn_assets(@components).uniq
|
|
1332
|
+
required.each { |asset| render_cdn_script(asset) }
|
|
1333
|
+
end
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
---
|
|
1337
|
+
|
|
1338
|
+
## 9A. Accessibility
|
|
1339
|
+
|
|
1340
|
+
Visual skills components must be usable with assistive technologies and keyboard-only navigation.
|
|
1341
|
+
|
|
1342
|
+
### ARIA Attributes
|
|
1343
|
+
|
|
1344
|
+
| Context | Attribute | Purpose |
|
|
1345
|
+
|---------|-----------|---------|
|
|
1346
|
+
| Generate-more region | `aria-live="polite"` | New options announced to screen readers without interrupting |
|
|
1347
|
+
| Active slide in nav | `aria-current="step"` | Identifies the current slide in navigation controls |
|
|
1348
|
+
| During generation | `aria-busy="true"` | Signals that content is loading/updating |
|
|
1349
|
+
| Option cards grid | `role="radiogroup"` | Groups option cards as a radio selection |
|
|
1350
|
+
| Individual option card | `role="radio"` + `aria-checked` | Each option is a selectable radio item |
|
|
1351
|
+
|
|
1352
|
+
### Focus Management
|
|
1353
|
+
|
|
1354
|
+
- **Slide navigation:** Focus is trapped within the active slide. When navigating to the next/previous slide, focus moves to the slide container.
|
|
1355
|
+
- **Generate-more:** When new options appear (via state re-render), focus remains on the current element. Screen readers announce new options via `aria-live`.
|
|
1356
|
+
- **Modal overlays** (CloseOverlay, ConfirmationBar): Focus is trapped within the overlay and restored to the triggering element on dismiss.
|
|
1357
|
+
- **Keyboard navigation:** Covered by the KeyboardShortcuts component (Section 3.4). All interactive elements are reachable via Tab. Number keys for quick-select are a progressive enhancement, not a replacement for Tab+Enter.
|
|
1358
|
+
|
|
1359
|
+
---
|
|
1360
|
+
|
|
1361
|
+
## 9B. Scalability Limits
|
|
1362
|
+
|
|
1363
|
+
Soft limits trigger warnings in development mode. Hard limits are enforced and raise errors.
|
|
1364
|
+
|
|
1365
|
+
| Resource | Soft Limit | Hard Limit | Rationale |
|
|
1366
|
+
|----------|-----------|------------|-----------|
|
|
1367
|
+
| Slides per deck | 10 | 20 | DOM size degrades rendering performance |
|
|
1368
|
+
| Options per slide | 6 | 12 | Grid layout breaks down; cognitive overload |
|
|
1369
|
+
| Mermaid diagrams per page | 10 | 25 | Mermaid.js rendering time grows non-linearly |
|
|
1370
|
+
| Total file size for HTML export | 2 MB | 10 MB | Browser memory; email attachment limits |
|
|
1371
|
+
| Concurrent SSE connections | 1 | 1 | Puma single-worker limitation |
|
|
1372
|
+
|
|
1373
|
+
**Enforcement:** Soft limits log a warning via `StreamWeaver.logger.warn`. Hard limits raise `StreamWeaver::LimitExceededError` with a descriptive message. The `validate!` method on `DesignDeck` checks slide and option counts. The `HtmlExporter` checks file size. SSE connection count is enforced by the Streamer.
|
|
1374
|
+
|
|
1375
|
+
---
|
|
1376
|
+
|
|
1377
|
+
## 10. DSL Examples
|
|
1378
|
+
|
|
1379
|
+
### 10.1 Design Deck -- Full Example
|
|
1380
|
+
|
|
1381
|
+
```ruby
|
|
1382
|
+
app "UI Component Library", theme: :dark do
|
|
1383
|
+
design_deck "Component Library Direction" do
|
|
1384
|
+
slide "palette", "Color Palette",
|
|
1385
|
+
context: "Choose the color direction for the design system" do
|
|
1386
|
+
option "Warm Earth Tones", recommended: true,
|
|
1387
|
+
aside: "Terracotta, sage, warm gray.\nCalm and professional." do
|
|
1388
|
+
code_block <<~CSS, lang: "css"
|
|
1389
|
+
:root {
|
|
1390
|
+
--primary: #c2825a;
|
|
1391
|
+
--surface: #f5f0eb;
|
|
1392
|
+
}
|
|
1393
|
+
CSS
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
option "Cool Ocean", aside: "Teal, slate, cool gray" do
|
|
1397
|
+
code_block <<~CSS, lang: "css"
|
|
1398
|
+
:root {
|
|
1399
|
+
--primary: #14b8a6;
|
|
1400
|
+
--surface: #f0f4f8;
|
|
1401
|
+
}
|
|
1402
|
+
CSS
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
option "Monochrome", aside: "Pure black, white, and grays" do
|
|
1406
|
+
code_block <<~CSS, lang: "css"
|
|
1407
|
+
:root {
|
|
1408
|
+
--primary: #111111;
|
|
1409
|
+
--surface: #fafafa;
|
|
1410
|
+
}
|
|
1411
|
+
CSS
|
|
1412
|
+
end
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1415
|
+
slide "layout", "Page Layout" do
|
|
1416
|
+
option "Sidebar Navigation" do
|
|
1417
|
+
mermaid <<~MERMAID, compact: true
|
|
1418
|
+
graph LR
|
|
1419
|
+
Sidebar-->Content
|
|
1420
|
+
Sidebar-->Footer
|
|
1421
|
+
MERMAID
|
|
1422
|
+
end
|
|
1423
|
+
option "Top Navigation" do
|
|
1424
|
+
mermaid <<~MERMAID, compact: true
|
|
1425
|
+
graph TD
|
|
1426
|
+
Navbar-->Content
|
|
1427
|
+
Content-->Footer
|
|
1428
|
+
MERMAID
|
|
1429
|
+
end
|
|
1430
|
+
end
|
|
1431
|
+
end
|
|
1432
|
+
end
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
### 10.2 Visual Explainer -- Diff Review Page
|
|
1436
|
+
|
|
1437
|
+
```ruby
|
|
1438
|
+
app "Diff Review: feature/auth", theme: :dark do
|
|
1439
|
+
theme_preset :editorial
|
|
1440
|
+
|
|
1441
|
+
sidebar_toc sections: [
|
|
1442
|
+
{ id: "summary", label: "Executive Summary" },
|
|
1443
|
+
{ id: "architecture", label: "Module Architecture" },
|
|
1444
|
+
{ id: "review", label: "Code Review" },
|
|
1445
|
+
{ id: "risks", label: "Risk Assessment" }
|
|
1446
|
+
]
|
|
1447
|
+
|
|
1448
|
+
hero_section variant: :centered do
|
|
1449
|
+
header1 "Diff Review"
|
|
1450
|
+
text "main...feature/auth -- 14 files changed, +482 / -91"
|
|
1451
|
+
kpi_dashboard metrics: [
|
|
1452
|
+
{ value: "14", label: "Files Changed" },
|
|
1453
|
+
{ value: "+482", label: "Lines Added", color: :green },
|
|
1454
|
+
{ value: "-91", label: "Lines Removed", color: :red }
|
|
1455
|
+
]
|
|
1456
|
+
end
|
|
1457
|
+
|
|
1458
|
+
ve_card depth: :hero, id: "summary" do
|
|
1459
|
+
header2 "Executive Summary"
|
|
1460
|
+
prose do
|
|
1461
|
+
md <<~MD
|
|
1462
|
+
This PR introduces JWT-based authentication, replacing the
|
|
1463
|
+
session-cookie approach. The **primary motivation** is enabling
|
|
1464
|
+
stateless horizontal scaling of the API tier.
|
|
1465
|
+
MD
|
|
1466
|
+
end
|
|
1467
|
+
end
|
|
1468
|
+
|
|
1469
|
+
ve_card id: "architecture" do
|
|
1470
|
+
header2 "Module Architecture"
|
|
1471
|
+
mermaid <<~MERMAID, zoom: true
|
|
1472
|
+
graph TD
|
|
1473
|
+
AuthMiddleware-->JWTService
|
|
1474
|
+
JWTService-->TokenStore
|
|
1475
|
+
AuthMiddleware-->SessionFallback
|
|
1476
|
+
MERMAID
|
|
1477
|
+
legend items: [
|
|
1478
|
+
{ color: "#22c55e", label: "New modules" },
|
|
1479
|
+
{ color: "#eab308", label: "Modified" }
|
|
1480
|
+
]
|
|
1481
|
+
end
|
|
1482
|
+
|
|
1483
|
+
ve_card id: "review" do
|
|
1484
|
+
header2 "Code Review"
|
|
1485
|
+
|
|
1486
|
+
callout variant: :success, title: "Good" do
|
|
1487
|
+
text "Clean separation of JWT logic into dedicated service class."
|
|
1488
|
+
end
|
|
1489
|
+
|
|
1490
|
+
callout variant: :error, title: "Needs Fix" do
|
|
1491
|
+
text "Token refresh endpoint missing rate limiting."
|
|
1492
|
+
code_block <<~RUBY, lang: "ruby", file: "app/controllers/tokens_controller.rb"
|
|
1493
|
+
def refresh
|
|
1494
|
+
# TODO: add rate limiting
|
|
1495
|
+
new_token = JWTService.refresh(current_token)
|
|
1496
|
+
render json: { token: new_token }
|
|
1497
|
+
end
|
|
1498
|
+
RUBY
|
|
1499
|
+
end
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
ve_card depth: :elevated, id: "risks" do
|
|
1503
|
+
header2 "Risk Assessment"
|
|
1504
|
+
data_table headers: ["Risk", "Severity", "Mitigation"],
|
|
1505
|
+
rows: [
|
|
1506
|
+
["Token leakage via logs", "High", "Add log filtering"],
|
|
1507
|
+
["Clock skew on expiry", "Medium", "Use 30s grace period"]
|
|
1508
|
+
]
|
|
1509
|
+
end
|
|
1510
|
+
end
|
|
1511
|
+
```
|
|
1512
|
+
|
|
1513
|
+
### 10.3 Visual Explainer -- Slide Deck
|
|
1514
|
+
|
|
1515
|
+
```ruby
|
|
1516
|
+
app "API Gateway Redesign", theme: :dark do
|
|
1517
|
+
theme_preset :midnight_editorial
|
|
1518
|
+
|
|
1519
|
+
slide_container mode: :scroll_snap, nav_dots: true, counter: true do
|
|
1520
|
+
slide "title", type: :title do
|
|
1521
|
+
header1 "API Gateway Redesign"
|
|
1522
|
+
text "Q2 2026 Architecture Proposal"
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
slide "problem", type: :split do
|
|
1526
|
+
comparison before_label: "Current", after_label: "Proposed" do
|
|
1527
|
+
before do
|
|
1528
|
+
mermaid "graph TD\n Client-->Monolith-->DB", compact: true
|
|
1529
|
+
end
|
|
1530
|
+
after do
|
|
1531
|
+
mermaid "graph TD\n Client-->Gateway-->Auth\n Gateway-->Orders", compact: true
|
|
1532
|
+
end
|
|
1533
|
+
end
|
|
1534
|
+
end
|
|
1535
|
+
|
|
1536
|
+
slide "metrics", type: :dashboard do
|
|
1537
|
+
kpi_dashboard metrics: [
|
|
1538
|
+
{ value: "3x", label: "Throughput Increase" },
|
|
1539
|
+
{ value: "50%", label: "Latency Reduction" },
|
|
1540
|
+
{ value: "99.99%", label: "Target Uptime" }
|
|
1541
|
+
]
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
slide "timeline", type: :content do
|
|
1545
|
+
header2 "Implementation Timeline"
|
|
1546
|
+
pipeline steps: [
|
|
1547
|
+
{ label: "Phase 1", description: "Gateway MVP", status: :complete },
|
|
1548
|
+
{ label: "Phase 2", description: "Auth migration", status: :active },
|
|
1549
|
+
{ label: "Phase 3", description: "Traffic cutover", status: :pending }
|
|
1550
|
+
]
|
|
1551
|
+
end
|
|
1552
|
+
end
|
|
1553
|
+
end
|
|
1554
|
+
```
|
|
1555
|
+
|
|
1556
|
+
---
|
|
1557
|
+
|
|
1558
|
+
## 11. Implementation Phases
|
|
1559
|
+
|
|
1560
|
+
Aligned with the analysis in `components.md`, ordered by dependency and value.
|
|
1561
|
+
|
|
1562
|
+
### Phase 1: Shared Foundation (Highest Value, Unblocks Everything)
|
|
1563
|
+
|
|
1564
|
+
| # | Component | Effort | Unblocks |
|
|
1565
|
+
|---|-----------|--------|----------|
|
|
1566
|
+
| 1 | `Mermaid` component + zoom JS | Medium | Both projects |
|
|
1567
|
+
| 2 | `CodeBlock` component + Prism.js | Small | Both projects |
|
|
1568
|
+
| 3 | Theme enhancements (presets, auto mode, CSS vars) | Medium | Both projects |
|
|
1569
|
+
| 4 | `KeyboardShortcuts` system | Small | Deck navigation |
|
|
1570
|
+
| 5 | `HtmlExporter` pipeline | Medium | Explainer output |
|
|
1571
|
+
| 6 | CDN asset declaration + serving infrastructure | Small | All JS-dependent components |
|
|
1572
|
+
| 7 | `ImageBlock` component | Small | Deck previews |
|
|
1573
|
+
|
|
1574
|
+
### Phase 2: Design Deck Core
|
|
1575
|
+
|
|
1576
|
+
| # | Component | Effort | Notes |
|
|
1577
|
+
|---|-----------|--------|-------|
|
|
1578
|
+
| 8 | `DesignDeck` + `DeckSlide` + `DeckOption` | Large | The deck shell |
|
|
1579
|
+
| 9 | `SlideContainer` (:swap mode) | Medium | Slide navigation |
|
|
1580
|
+
| 10 | `DeckSummary` auto-generated slide | Medium | Reads selection state |
|
|
1581
|
+
| 11 | Selection state + radio behavior (JS) | Medium | Client-side Alpine.js |
|
|
1582
|
+
| 12 | Deck-specific keyboard shortcuts | Small | Number keys, Enter, Escape |
|
|
1583
|
+
| 13 | `ProgressIndicator` | Small | Nav progress bar |
|
|
1584
|
+
|
|
1585
|
+
### Phase 3: Generate-More Loop
|
|
1586
|
+
|
|
1587
|
+
| # | Component | Effort | Notes |
|
|
1588
|
+
|---|-----------|--------|-------|
|
|
1589
|
+
| 14 | `GenerateMoreControls` UI | Small | Button, input, dropdown |
|
|
1590
|
+
| 15 | `SkeletonPlaceholder` | Small | Shimmer CSS animation |
|
|
1591
|
+
| 16 | Generate request queue + `/deck/generate` + `/deck/pending` | Medium | Server routes |
|
|
1592
|
+
| 17 | SSE option push integration | Medium | Feed + Streamer |
|
|
1593
|
+
| 18 | Timeout handling (client + server) | Small | Toast on timeout |
|
|
1594
|
+
| 19 | `ConfirmationBar` + `CloseOverlay` | Small | UX polish |
|
|
1595
|
+
|
|
1596
|
+
### Phase 4: Visual Explainer Core
|
|
1597
|
+
|
|
1598
|
+
| # | Component | Effort | Notes |
|
|
1599
|
+
|---|-----------|--------|-------|
|
|
1600
|
+
| 20 | `VeCard` with depth tiers | Small | CSS classes on a div |
|
|
1601
|
+
| 21 | `DataTable` enhanced table | Medium | Sticky header, badges |
|
|
1602
|
+
| 22 | `SidebarToc` with scroll spy | Medium | IntersectionObserver JS |
|
|
1603
|
+
| 23 | `KpiDashboard` | Small | Grid of stat cards |
|
|
1604
|
+
| 24 | `Callout` | Small | Colored border box |
|
|
1605
|
+
| 25 | `Comparison` panels | Small | Side-by-side layout |
|
|
1606
|
+
| 26 | `Pipeline` step flow | Small | CSS flexbox + arrows |
|
|
1607
|
+
| 27 | `Chart` (Chart.js) | Medium | CDN, dark mode, data binding |
|
|
1608
|
+
|
|
1609
|
+
### Phase 5: Polish and Remaining
|
|
1610
|
+
|
|
1611
|
+
| # | Component | Effort | Notes |
|
|
1612
|
+
|---|-----------|--------|-------|
|
|
1613
|
+
| 28 | `SlideContainer` (:scroll_snap mode) | Medium | Explainer presentations |
|
|
1614
|
+
| 29 | CSS-only helpers (prose, pullquote, hero_section, dir_tree, legend, flow_arrow, layout_toggle) | Small each | Thin DSL methods + CSS |
|
|
1615
|
+
| 30 | `ModelSelector` | Medium | Model picker UI |
|
|
1616
|
+
| 31 | Save/Load snapshot system | Medium | File I/O, localStorage |
|
|
1617
|
+
| 32 | Animation choreography | Medium | Staggered reveals, CSS counters |
|
|
1618
|
+
| 33 | Theme preset library (all presets) | Medium | Fonts, colors, curated combos |
|
|
1619
|
+
|
|
1620
|
+
---
|
|
1621
|
+
|
|
1622
|
+
## 12. Open Questions / Design Decisions Needed
|
|
1623
|
+
|
|
1624
|
+
### Q1: Deck State -- Server Session vs. Client-Side? **[RESOLVED: Option B]**
|
|
1625
|
+
|
|
1626
|
+
The existing StreamWeaver uses server-side session (cookies) for state. The design deck has a lot of state (selections, notes, dirty tracking). Cookie size limit is ~4KB.
|
|
1627
|
+
|
|
1628
|
+
**Options:**
|
|
1629
|
+
- **A) Server-side session** -- fits existing pattern, but may hit cookie size limits with many slides/notes
|
|
1630
|
+
- **B) Server-side with file-backed overflow** -- session stores an ID, full state in a file
|
|
1631
|
+
- **C) Client-side localStorage** -- the original pi-design-deck approach, but breaks the StreamWeaver pattern
|
|
1632
|
+
|
|
1633
|
+
**Decision: Option B (file-backed state).** Codex's argument about cookie overflow with user-generated notes was decisive -- a single detailed note on one slide can exceed the 4KB cookie limit. The session cookie stores only a session ID; full deck state (selections, notes, generate status) is persisted to a JSON file in a session-scoped directory. See Section 7A (State Ownership Table) for the complete state location map.
|
|
1634
|
+
|
|
1635
|
+
### Q2: AlpineJS Adapter Extension vs. New Adapter Methods?
|
|
1636
|
+
|
|
1637
|
+
New components need rendering. Two approaches:
|
|
1638
|
+
- **A) Add `render_mermaid`, `render_code_block`, etc. to `Adapter::Base` and `Adapter::AlpineJS`** -- follows existing pattern exactly
|
|
1639
|
+
- **B) Components render themselves via Phlex, only delegate to adapter for interactive bits** -- less adapter bloat, but breaks the adapter abstraction
|
|
1640
|
+
|
|
1641
|
+
**Recommendation:** (A) for interactive components (those with Alpine.js state), (B) for pure display components (Mermaid, CodeBlock, VeCard just render HTML). This is already the implicit pattern: `Text`, `Header`, `Badge` all render in their own `render(view, state)` without adapter delegation. Only form controls delegate to the adapter.
|
|
1642
|
+
|
|
1643
|
+
### Q3: Generate-More -- Polling vs. Callback?
|
|
1644
|
+
|
|
1645
|
+
How does the agent learn that the user clicked "Generate"?
|
|
1646
|
+
|
|
1647
|
+
**Options:**
|
|
1648
|
+
- **A) Agent polls `/deck/pending`** -- simple, works with any agent, but adds latency
|
|
1649
|
+
- **B) Agent provides a callback URL/block** -- lower latency, but requires the agent script to run a mini HTTP server or use a callback mechanism
|
|
1650
|
+
- **C) Agent monitors stdout** -- server prints generate requests to stdout, agent reads them
|
|
1651
|
+
|
|
1652
|
+
**Recommendation:** (A) polling. Simplicity wins. The agent script already runs in a loop (for `run_once!` it polls `result_container`). Adding a poll for generate requests is trivial. Latency of 1-2 seconds (poll interval) is acceptable for a "generate with LLM" operation that itself takes 5-15 seconds.
|
|
1653
|
+
|
|
1654
|
+
### Q4: Should the Deck Be an App Subclass?
|
|
1655
|
+
|
|
1656
|
+
**Options:**
|
|
1657
|
+
- **A) DSL methods on App** (recommended above) -- `design_deck` is like `tabs` or `modal`
|
|
1658
|
+
- **B) `DesignDeckApp < App`** -- a specialized subclass with deck-specific routes
|
|
1659
|
+
|
|
1660
|
+
**Recommendation:** (A). The deck needs a few custom routes (`/deck/generate`, `/deck/pending`), but these can be added to the SinatraApp conditionally (only when the app contains a DesignDeck component). Making it a subclass creates a parallel hierarchy that complicates the codebase.
|
|
1661
|
+
|
|
1662
|
+
### Q5: Shared Components in DisplayDSL vs. App Only?
|
|
1663
|
+
|
|
1664
|
+
Should `mermaid`, `code_block`, `chart` etc. be available in `DisplayDSL` (and thus in `FeedBuilder` for push updates) or only in `App`?
|
|
1665
|
+
|
|
1666
|
+
**Recommendation:** `DisplayDSL` for all display-only components. This means a `Feed` can push a mermaid diagram or code block into a running app, which is powerful for the generate-more loop. Only deck-specific interactive components (`design_deck`, `option`, etc.) belong on `App` alone.
|
|
1667
|
+
|
|
1668
|
+
### Q6: How to Handle Mermaid Re-rendering on SSE Push?
|
|
1669
|
+
|
|
1670
|
+
When a new option with a Mermaid diagram is pushed via SSE, the `<pre class="mermaid">` element needs to be processed by Mermaid.js. But Mermaid has already initialized.
|
|
1671
|
+
|
|
1672
|
+
**Options:**
|
|
1673
|
+
- **A) Call `mermaid.run()` after each SSE DOM update** -- re-processes all unrendered diagrams
|
|
1674
|
+
- **B) Use MutationObserver** to auto-detect new mermaid elements
|
|
1675
|
+
- **C) Include a `<script>` tag in the pushed HTML that triggers rendering
|
|
1676
|
+
|
|
1677
|
+
**Recommendation:** (A). The SSE client-side handler already knows when it receives updates. Adding `mermaid.run({ nodes: [newElement] })` after DOM insertion is straightforward.
|
|
1678
|
+
|
|
1679
|
+
### Q7: Anti-Slop Enforcement -- Build-Time or Runtime?
|
|
1680
|
+
|
|
1681
|
+
The visual explainer spec forbids certain colors, fonts, and patterns. Should this be enforced?
|
|
1682
|
+
|
|
1683
|
+
**Options:**
|
|
1684
|
+
- **A) Documentation only** -- trust the agent to follow the rules
|
|
1685
|
+
- **B) Build-time validation** -- theme presets reject forbidden values
|
|
1686
|
+
- **C) Runtime CSS override** -- a stylesheet that `!important`-overrides forbidden patterns
|
|
1687
|
+
|
|
1688
|
+
**Recommendation:** (A) for now. The anti-slop rules are guidance for the agent's prompt, not application logic. Theme presets inherently avoid slop by providing curated alternatives. If slop becomes a problem in practice, add (B) as a development-mode warning.
|
|
1689
|
+
|
|
1690
|
+
### Q8: Comparison Component -- Block Syntax
|
|
1691
|
+
|
|
1692
|
+
The `comparison` component needs two child regions. How to express this in the DSL?
|
|
1693
|
+
|
|
1694
|
+
**Options:**
|
|
1695
|
+
- **A) Named blocks:**
|
|
1696
|
+
```ruby
|
|
1697
|
+
comparison do
|
|
1698
|
+
before { mermaid "..." }
|
|
1699
|
+
after { mermaid "..." }
|
|
1700
|
+
end
|
|
1701
|
+
```
|
|
1702
|
+
- **B) Positional blocks:**
|
|
1703
|
+
```ruby
|
|
1704
|
+
comparison do
|
|
1705
|
+
panel "Before" do mermaid "..." end
|
|
1706
|
+
panel "After" do mermaid "..." end
|
|
1707
|
+
end
|
|
1708
|
+
```
|
|
1709
|
+
- **C) Hash of content:**
|
|
1710
|
+
```ruby
|
|
1711
|
+
comparison before: mermaid_html("..."), after: mermaid_html("...")
|
|
1712
|
+
```
|
|
1713
|
+
|
|
1714
|
+
**Recommendation:** (A). Named blocks (`before`/`after`) are the most readable and follow the pattern of `trigger`/`menu` in the existing `dropdown` component.
|
|
1715
|
+
|
|
1716
|
+
### Q9: Explainer Slash Commands -- Where Do They Live? **[RESOLVED: Option C]**
|
|
1717
|
+
|
|
1718
|
+
The visual explainer has 8 slash commands that each gather different data before generating a page. These are agent-side behaviors, not StreamWeaver server logic.
|
|
1719
|
+
|
|
1720
|
+
**Options:**
|
|
1721
|
+
- **A) Skills in StreamWeaver gem** -- `StreamWeaver::Skills::DiffReview`, etc.
|
|
1722
|
+
- **B) Separate gem/tool** -- skills live outside StreamWeaver, call it as a library
|
|
1723
|
+
- **C) Claude Code custom commands** -- the `/commands` directory, written in shell/Ruby
|
|
1724
|
+
|
|
1725
|
+
**Decision: Option C (Claude Code custom commands).** The DSL IS the API -- no skill wrapper classes in the gem. Agent glue code (slash commands) lives in `.claude/commands/` as shell/Ruby scripts. The data gathering (git diffs, file reading, codebase analysis) is agent-side work that doesn't belong in a rendering library. See Section 6.5 for details.
|