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,542 @@
|
|
|
1
|
+
# StreamWeaver for TUI: Adapting the DSL to Charm
|
|
2
|
+
|
|
3
|
+
**Date:** January 16, 2026
|
|
4
|
+
**Status:** Exploration / Feasibility Analysis
|
|
5
|
+
|
|
6
|
+
## Executive Summary
|
|
7
|
+
|
|
8
|
+
StreamWeaver's reactive DSL could be adapted to target Charm's TUI libraries (Bubbletea, Lipgloss, Bubbles) instead of web browsers. The core insight—**your Ruby block re-executes on every interaction**—translates naturally to Bubbletea's Elm Architecture. This document explores feasibility, architectural alignment, and a potential implementation path.
|
|
9
|
+
|
|
10
|
+
## The Opportunity
|
|
11
|
+
|
|
12
|
+
With Marco Roth's Charm Ruby gems now available, Ruby developers can build "glamorous" terminal UIs. But Charm follows the Elm Architecture pattern, requiring explicit model/update/view separation:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Current Charm/Bubbletea approach
|
|
16
|
+
class Counter
|
|
17
|
+
include Bubbletea::Model
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@count = 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def update(msg)
|
|
24
|
+
case msg
|
|
25
|
+
when Bubbletea::KeyMsg
|
|
26
|
+
@count += 1 if msg.key == "+"
|
|
27
|
+
@count -= 1 if msg.key == "-"
|
|
28
|
+
end
|
|
29
|
+
[self, nil]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def view
|
|
33
|
+
"Count: #{@count}\n\nPress + or - to change, q to quit"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Bubbletea::Program.new(Counter.new).run
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
StreamWeaver's approach is more declarative—state changes and UI definition live together:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# StreamWeaver's reactive approach
|
|
44
|
+
app "Counter" do
|
|
45
|
+
state[:count] ||= 0
|
|
46
|
+
|
|
47
|
+
text "Count: #{state[:count]}"
|
|
48
|
+
|
|
49
|
+
button "+" do |s|
|
|
50
|
+
s[:count] += 1
|
|
51
|
+
end
|
|
52
|
+
end.run!
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Question: Can we bring StreamWeaver's token-efficient DSL to terminal UIs?**
|
|
56
|
+
|
|
57
|
+
## Architectural Alignment Analysis
|
|
58
|
+
|
|
59
|
+
### What Aligns Well
|
|
60
|
+
|
|
61
|
+
| StreamWeaver Concept | Charm Equivalent | Compatibility |
|
|
62
|
+
|---------------------|------------------|---------------|
|
|
63
|
+
| `state[:key]` hash | Model instance variables | Direct mapping |
|
|
64
|
+
| Block re-execution | `view()` method called after `update()` | Same pattern |
|
|
65
|
+
| Component rendering | Lipgloss styled strings | Conceptual match |
|
|
66
|
+
| Button callbacks | KeyMsg/MouseMsg handling | Needs translation layer |
|
|
67
|
+
| Layout (vstack, hstack) | Lipgloss.join_vertical/horizontal | Direct equivalent |
|
|
68
|
+
|
|
69
|
+
### Key Differences
|
|
70
|
+
|
|
71
|
+
| Aspect | StreamWeaver (Web) | Charm (TUI) |
|
|
72
|
+
|--------|-------------------|-------------|
|
|
73
|
+
| Event model | HTTP requests via HTMX | Keyboard/mouse messages |
|
|
74
|
+
| Rendering | HTML via Phlex | ANSI strings via Lipgloss |
|
|
75
|
+
| Components | Form elements (text_field, select) | Bubbles components (TextInput, List) |
|
|
76
|
+
| State persistence | Session cookies | In-memory only |
|
|
77
|
+
| Interaction | Click, type, submit | Keypress, mouse click |
|
|
78
|
+
|
|
79
|
+
## Proposed Architecture: CharmWeaver
|
|
80
|
+
|
|
81
|
+
A new rendering backend for StreamWeaver's DSL that targets Charm instead of web browsers.
|
|
82
|
+
|
|
83
|
+
### Core Abstraction
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# Same DSL, different target
|
|
87
|
+
tui "Counter" do
|
|
88
|
+
state[:count] ||= 0
|
|
89
|
+
|
|
90
|
+
text "Count: #{state[:count]}"
|
|
91
|
+
|
|
92
|
+
# TUI-specific: key bindings instead of buttons
|
|
93
|
+
on_key "+" do |s|
|
|
94
|
+
s[:count] += 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
on_key "-" do |s|
|
|
98
|
+
s[:count] -= 1
|
|
99
|
+
end
|
|
100
|
+
end.run!
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Implementation Strategy
|
|
104
|
+
|
|
105
|
+
**Phase 1: Proof of Concept**
|
|
106
|
+
- Create `CharmWeaver::App` that wraps Bubbletea::Model
|
|
107
|
+
- Implement basic components: `text`, `header`, `vstack`, `hstack`
|
|
108
|
+
- Map `state[:key]` to model instance variables
|
|
109
|
+
- Block re-execution on each `view()` call
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
module CharmWeaver
|
|
113
|
+
class App
|
|
114
|
+
include Bubbletea::Model
|
|
115
|
+
|
|
116
|
+
def initialize(title, &block)
|
|
117
|
+
@title = title
|
|
118
|
+
@block = block
|
|
119
|
+
@state = {}
|
|
120
|
+
@components = []
|
|
121
|
+
@key_handlers = {}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def init
|
|
125
|
+
[self, nil]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def update(msg)
|
|
129
|
+
case msg
|
|
130
|
+
when Bubbletea::KeyMsg
|
|
131
|
+
return [self, Bubbletea.quit] if msg.key == "q"
|
|
132
|
+
|
|
133
|
+
if handler = @key_handlers[msg.key]
|
|
134
|
+
handler.call(@state)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
[self, nil]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def view
|
|
141
|
+
@components.clear
|
|
142
|
+
@key_handlers.clear
|
|
143
|
+
|
|
144
|
+
# Re-execute block (StreamWeaver's core insight)
|
|
145
|
+
instance_eval(&@block)
|
|
146
|
+
|
|
147
|
+
render_components
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def state
|
|
153
|
+
@state
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def text(content)
|
|
157
|
+
@components << { type: :text, content: content }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def on_key(key, &handler)
|
|
161
|
+
@key_handlers[key] = handler
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def render_components
|
|
165
|
+
@components.map do |c|
|
|
166
|
+
case c[:type]
|
|
167
|
+
when :text
|
|
168
|
+
c[:content]
|
|
169
|
+
end
|
|
170
|
+
end.join("\n")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def tui(title, &block)
|
|
176
|
+
CharmWeaver::App.new(title, &block)
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Phase 2: Component Library**
|
|
181
|
+
|
|
182
|
+
Map StreamWeaver components to Charm equivalents:
|
|
183
|
+
|
|
184
|
+
| StreamWeaver | CharmWeaver | Implementation |
|
|
185
|
+
|--------------|-------------|----------------|
|
|
186
|
+
| `text` | `text` | Lipgloss styled string |
|
|
187
|
+
| `header` | `header` | Lipgloss bold/colored |
|
|
188
|
+
| `text_field :name` | `text_input :name` | Bubbles::TextInput |
|
|
189
|
+
| `text_area :bio` | `text_area :bio` | Bubbles::TextArea |
|
|
190
|
+
| `select :choice, [...]` | `list :choice, [...]` | Bubbles::List |
|
|
191
|
+
| `checkbox :agree` | `toggle :agree` | Custom toggle component |
|
|
192
|
+
| `button "Click"` | `on_key "enter"` | Key binding |
|
|
193
|
+
| `vstack` | `vstack` | Lipgloss.join_vertical |
|
|
194
|
+
| `hstack` | `hstack` | Lipgloss.join_horizontal |
|
|
195
|
+
| `card` | `box` | Lipgloss border + padding |
|
|
196
|
+
| `alert` | `styled_box` | Lipgloss with color variant |
|
|
197
|
+
|
|
198
|
+
**Phase 3: Focus Management**
|
|
199
|
+
|
|
200
|
+
TUIs need focus handling for input components:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
tui "Form" do
|
|
204
|
+
state[:focus] ||= :name
|
|
205
|
+
|
|
206
|
+
text_input :name, focused: state[:focus] == :name
|
|
207
|
+
text_input :email, focused: state[:focus] == :email
|
|
208
|
+
|
|
209
|
+
on_key "tab" do |s|
|
|
210
|
+
s[:focus] = s[:focus] == :name ? :email : :name
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
on_key "enter" do |s|
|
|
214
|
+
# Submit
|
|
215
|
+
end
|
|
216
|
+
end.run!
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Phase 4: Agentic Mode**
|
|
220
|
+
|
|
221
|
+
StreamWeaver's `run_once!` maps to TUI forms:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
result = tui "Quick Input" do
|
|
225
|
+
text "Enter your name:"
|
|
226
|
+
text_input :name
|
|
227
|
+
|
|
228
|
+
on_key "enter" do |s|
|
|
229
|
+
s[:_submit] = true
|
|
230
|
+
end
|
|
231
|
+
end.run_once!
|
|
232
|
+
|
|
233
|
+
puts result[:name]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Component Mapping Deep Dive
|
|
237
|
+
|
|
238
|
+
### Text & Display
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# StreamWeaver (web)
|
|
242
|
+
text "Hello, #{name}"
|
|
243
|
+
md "**Bold** text"
|
|
244
|
+
header "Section"
|
|
245
|
+
|
|
246
|
+
# CharmWeaver (TUI)
|
|
247
|
+
text "Hello, #{name}" # Plain text
|
|
248
|
+
text "**Bold** text", style: :markdown # Glamour rendering
|
|
249
|
+
header "Section" # Lipgloss bold + color
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Input Components
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# StreamWeaver (web)
|
|
256
|
+
text_field :name, placeholder: "Name"
|
|
257
|
+
|
|
258
|
+
# CharmWeaver (TUI) - wraps Bubbles::TextInput
|
|
259
|
+
text_input :name, placeholder: "Name"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The key difference: TUI inputs require focus management and cursor handling, which Bubbles handles internally.
|
|
263
|
+
|
|
264
|
+
### Layout
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
# StreamWeaver (web)
|
|
268
|
+
columns widths: ['30%', '70%'] do
|
|
269
|
+
column { sidebar_content }
|
|
270
|
+
column { main_content }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# CharmWeaver (TUI) - character-based widths
|
|
274
|
+
columns widths: [20, 60] do
|
|
275
|
+
column { sidebar_content }
|
|
276
|
+
column { main_content }
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Styling
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# New: Lipgloss-based styling DSL
|
|
284
|
+
tui "Styled" do
|
|
285
|
+
style :title,
|
|
286
|
+
bold: true,
|
|
287
|
+
foreground: "#FF6B6B",
|
|
288
|
+
margin_bottom: 1
|
|
289
|
+
|
|
290
|
+
text "Welcome", style: :title
|
|
291
|
+
end.run!
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Challenges & Solutions
|
|
295
|
+
|
|
296
|
+
### Challenge 1: Event Model Difference
|
|
297
|
+
|
|
298
|
+
**Web:** Events trigger HTTP requests, server re-renders entire UI
|
|
299
|
+
**TUI:** Events are keyboard/mouse messages, model updates locally
|
|
300
|
+
|
|
301
|
+
**Solution:** CharmWeaver's `update()` method triggers block re-execution, mimicking StreamWeaver's behavior. The pattern is the same—just the transport differs.
|
|
302
|
+
|
|
303
|
+
### Challenge 2: Component Complexity
|
|
304
|
+
|
|
305
|
+
Bubbles components (TextInput, List) maintain internal state (cursor position, scroll offset). StreamWeaver components are stateless—state lives in the hash.
|
|
306
|
+
|
|
307
|
+
**Solution:** CharmWeaver manages Bubbles component instances internally, syncing their values to `state[:key]` after each update cycle.
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
class CharmWeaver::App
|
|
311
|
+
def text_input(key, **opts)
|
|
312
|
+
@bubble_components[key] ||= Bubbles::TextInput.new(**opts)
|
|
313
|
+
@components << { type: :text_input, key: key, component: @bubble_components[key] }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def update(msg)
|
|
317
|
+
# Forward messages to focused component
|
|
318
|
+
if focused = @bubble_components[@focused_key]
|
|
319
|
+
focused, cmd = focused.update(msg)
|
|
320
|
+
@state[@focused_key] = focused.value
|
|
321
|
+
end
|
|
322
|
+
[self, cmd]
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Challenge 3: Mouse Support
|
|
328
|
+
|
|
329
|
+
StreamWeaver buttons are clickable. TUI "buttons" are typically key-bound.
|
|
330
|
+
|
|
331
|
+
**Solution:** Use Bubblezone for mouse region tracking:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
tui "Clickable" do
|
|
335
|
+
button "Save" do |s|
|
|
336
|
+
s[:saved] = true
|
|
337
|
+
end
|
|
338
|
+
# Renders as clickable zone + key hint: [Save] (Enter)
|
|
339
|
+
end.run!
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Challenge 4: Forms & Submission
|
|
343
|
+
|
|
344
|
+
StreamWeaver's `run_once!` returns when a form is "submitted." TUIs need explicit submission handling.
|
|
345
|
+
|
|
346
|
+
**Solution:** Define submission triggers:
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
result = tui "Form" do
|
|
350
|
+
text_input :name
|
|
351
|
+
text_input :email
|
|
352
|
+
|
|
353
|
+
# Explicit submit key
|
|
354
|
+
submit_on "ctrl+s"
|
|
355
|
+
# Or automatic when all fields filled
|
|
356
|
+
submit_when { state[:name] && state[:email] }
|
|
357
|
+
end.run_once!
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## API Design: Full Example
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
require 'charm_weaver'
|
|
364
|
+
|
|
365
|
+
tui "Task Manager",
|
|
366
|
+
theme: :dracula, # Lipgloss color scheme
|
|
367
|
+
border: :rounded # App border style
|
|
368
|
+
do
|
|
369
|
+
header "My Tasks"
|
|
370
|
+
|
|
371
|
+
# Input with auto-focus
|
|
372
|
+
text_input :new_task, placeholder: "Add task..."
|
|
373
|
+
|
|
374
|
+
on_key "enter" do |s|
|
|
375
|
+
next if s[:new_task].to_s.empty?
|
|
376
|
+
s[:tasks] ||= []
|
|
377
|
+
s[:tasks] << { name: s[:new_task], done: false }
|
|
378
|
+
s[:new_task] = ""
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
divider
|
|
382
|
+
|
|
383
|
+
# List with selection
|
|
384
|
+
state[:tasks]&.each_with_index do |task, i|
|
|
385
|
+
selected = state[:selected] == i
|
|
386
|
+
|
|
387
|
+
hstack do
|
|
388
|
+
text selected ? ">" : " "
|
|
389
|
+
text task[:done] ? "[x]" : "[ ]"
|
|
390
|
+
text task[:name], style: task[:done] ? :dim : :normal
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Key bindings
|
|
395
|
+
on_key "j" do |s|
|
|
396
|
+
s[:selected] = [(s[:selected] || 0) + 1, (s[:tasks]&.size || 1) - 1].min
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
on_key "k" do |s|
|
|
400
|
+
s[:selected] = [(s[:selected] || 0) - 1, 0].max
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
on_key "space" do |s|
|
|
404
|
+
if s[:tasks] && s[:selected]
|
|
405
|
+
s[:tasks][s[:selected]][:done] ^= true
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
on_key "d" do |s|
|
|
410
|
+
s[:tasks]&.delete_at(s[:selected]) if s[:selected]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Footer
|
|
414
|
+
divider
|
|
415
|
+
text "j/k: navigate | space: toggle | d: delete | q: quit", style: :help
|
|
416
|
+
end.run!
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Token Efficiency Comparison
|
|
420
|
+
|
|
421
|
+
The goal: same expressiveness, fewer tokens for LLM-driven development.
|
|
422
|
+
|
|
423
|
+
**Raw Bubbletea (58 lines):**
|
|
424
|
+
```ruby
|
|
425
|
+
class TaskManager
|
|
426
|
+
include Bubbletea::Model
|
|
427
|
+
|
|
428
|
+
def initialize
|
|
429
|
+
@tasks = []
|
|
430
|
+
@new_task = Bubbles::TextInput.new(placeholder: "Add task...")
|
|
431
|
+
@selected = 0
|
|
432
|
+
@focused = :input
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def init
|
|
436
|
+
[self, Bubbles::TextInput.focus]
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def update(msg)
|
|
440
|
+
case msg
|
|
441
|
+
when Bubbletea::KeyMsg
|
|
442
|
+
case msg.key
|
|
443
|
+
when "enter"
|
|
444
|
+
unless @new_task.value.empty?
|
|
445
|
+
@tasks << { name: @new_task.value, done: false }
|
|
446
|
+
@new_task.set_value("")
|
|
447
|
+
end
|
|
448
|
+
when "j"
|
|
449
|
+
@selected = [@selected + 1, @tasks.size - 1].min
|
|
450
|
+
when "k"
|
|
451
|
+
@selected = [@selected - 1, 0].max
|
|
452
|
+
when " "
|
|
453
|
+
@tasks[@selected][:done] ^= true if @tasks[@selected]
|
|
454
|
+
when "d"
|
|
455
|
+
@tasks.delete_at(@selected)
|
|
456
|
+
when "q"
|
|
457
|
+
return [self, Bubbletea.quit]
|
|
458
|
+
else
|
|
459
|
+
@new_task, cmd = @new_task.update(msg)
|
|
460
|
+
return [self, cmd]
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
[self, nil]
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def view
|
|
467
|
+
lines = ["My Tasks", "", @new_task.view, ""]
|
|
468
|
+
@tasks.each_with_index do |t, i|
|
|
469
|
+
prefix = i == @selected ? ">" : " "
|
|
470
|
+
check = t[:done] ? "[x]" : "[ ]"
|
|
471
|
+
lines << "#{prefix} #{check} #{t[:name]}"
|
|
472
|
+
end
|
|
473
|
+
lines << ""
|
|
474
|
+
lines << "j/k: navigate | space: toggle | d: delete | q: quit"
|
|
475
|
+
lines.join("\n")
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
Bubbletea::Program.new(TaskManager.new).run
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**CharmWeaver DSL (35 lines):** ~40% fewer tokens, same functionality.
|
|
483
|
+
|
|
484
|
+
## Implementation Roadmap
|
|
485
|
+
|
|
486
|
+
### Phase 1: Core (Week 1-2)
|
|
487
|
+
- [ ] `CharmWeaver::App` base class wrapping Bubbletea::Model
|
|
488
|
+
- [ ] Basic components: `text`, `header`, `divider`, `vstack`, `hstack`
|
|
489
|
+
- [ ] State hash integration
|
|
490
|
+
- [ ] `on_key` handler registration
|
|
491
|
+
- [ ] Block re-execution in `view()`
|
|
492
|
+
|
|
493
|
+
### Phase 2: Input Components (Week 3-4)
|
|
494
|
+
- [ ] `text_input` wrapping Bubbles::TextInput
|
|
495
|
+
- [ ] `text_area` wrapping Bubbles::TextArea
|
|
496
|
+
- [ ] Focus management system
|
|
497
|
+
- [ ] Tab navigation between inputs
|
|
498
|
+
|
|
499
|
+
### Phase 3: Selection Components (Week 5-6)
|
|
500
|
+
- [ ] `list` wrapping Bubbles::List
|
|
501
|
+
- [ ] `table` wrapping Bubbles::Table
|
|
502
|
+
- [ ] Selection state synchronization
|
|
503
|
+
|
|
504
|
+
### Phase 4: Styling & Polish (Week 7-8)
|
|
505
|
+
- [ ] Theme support via Lipgloss
|
|
506
|
+
- [ ] `style` DSL for custom styles
|
|
507
|
+
- [ ] `box` component with borders
|
|
508
|
+
- [ ] `alert` with color variants
|
|
509
|
+
|
|
510
|
+
### Phase 5: Advanced Features (Week 9-10)
|
|
511
|
+
- [ ] Mouse support via Bubblezone
|
|
512
|
+
- [ ] `run_once!` agentic mode
|
|
513
|
+
- [ ] Animation via Harmonica
|
|
514
|
+
- [ ] Charts via NTCharts
|
|
515
|
+
|
|
516
|
+
## Risks & Mitigations
|
|
517
|
+
|
|
518
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
519
|
+
|------|------------|--------|------------|
|
|
520
|
+
| Bubbles component state sync complexity | High | Medium | Start with simple components, add complex ones incrementally |
|
|
521
|
+
| Focus management edge cases | Medium | High | Study Huh? form library implementation |
|
|
522
|
+
| Performance with large state | Low | Medium | Optimize re-render diffing if needed |
|
|
523
|
+
| Charm gem API changes | Medium | Medium | Pin gem versions, contribute upstream |
|
|
524
|
+
|
|
525
|
+
## Success Criteria
|
|
526
|
+
|
|
527
|
+
1. **Token efficiency:** CharmWeaver DSL uses 30-50% fewer tokens than raw Bubbletea for equivalent functionality
|
|
528
|
+
2. **Familiar API:** StreamWeaver users can transfer knowledge directly
|
|
529
|
+
3. **Full component coverage:** All common TUI patterns expressible
|
|
530
|
+
4. **Production quality:** Suitable for real CLI tools, not just demos
|
|
531
|
+
|
|
532
|
+
## Conclusion
|
|
533
|
+
|
|
534
|
+
Adapting StreamWeaver's DSL to Charm is **highly feasible**. The reactive model (block re-execution on state change) maps directly to Bubbletea's Elm Architecture. The main work is:
|
|
535
|
+
|
|
536
|
+
1. Wrapping Bubbles components to sync with `state[:key]`
|
|
537
|
+
2. Implementing focus management
|
|
538
|
+
3. Translating web concepts (buttons → key bindings)
|
|
539
|
+
|
|
540
|
+
The payoff: Ruby developers get a token-efficient, declarative DSL for beautiful TUIs, consistent with StreamWeaver's web approach. Same mental model, different output target.
|
|
541
|
+
|
|
542
|
+
**Recommendation:** Proceed with Phase 1 proof of concept to validate the component wrapping approach before committing to full implementation.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Static Doc Shelf — free multi-doc export for canvas docs
|
|
2
|
+
|
|
3
|
+
**Status:** captured, not scheduled. Brainstormed 2026-08-27; no implementation.
|
|
4
|
+
**Trigger to pick this up:** when a doc corpus needs to reach a reader who
|
|
5
|
+
should not have to run StreamWeaver, use GitHub, or hold an account.
|
|
6
|
+
|
|
7
|
+
## The gap, in one line
|
|
8
|
+
|
|
9
|
+
`streamweaver export` gives you one self-contained HTML file. `canvas-read`
|
|
10
|
+
gives you a browsable *shelf* of many docs — but only as a local Sinatra app.
|
|
11
|
+
**There is no static shelf.** That is the whole missing piece.
|
|
12
|
+
|
|
13
|
+
## Why this is small
|
|
14
|
+
|
|
15
|
+
Both halves already exist and are reused verbatim:
|
|
16
|
+
|
|
17
|
+
- `StreamWeaver::Export::HtmlExporter.from_dsl_file(path, theme:, layout:)` —
|
|
18
|
+
per-doc rendering, already handles `.rb`, mermaid, images, CDN collection.
|
|
19
|
+
- `StreamWeaver::Canvas::Reader::FileList.build(args, ...)` — input resolution,
|
|
20
|
+
already globs `.rb` and `.org`, already does multi-root discovery.
|
|
21
|
+
- `StreamWeaver::Org::Reader` — `.org` → DSL, already parses `#+TITLE:`
|
|
22
|
+
(`lib/stream_weaver/org/reader.rb:62`).
|
|
23
|
+
|
|
24
|
+
No new rendering engine. No Opal component-parity dependency.
|
|
25
|
+
|
|
26
|
+
## Framing decisions (settled during the brainstorm)
|
|
27
|
+
|
|
28
|
+
| Question | Decision |
|
|
29
|
+
|---|---|
|
|
30
|
+
| What is the durable asset? | The extraction pipeline, the doc format, and the curated corpus. **Not** the delivery. |
|
|
31
|
+
| So how much delivery do we build? | As little as possible. Delivery is plumbing; over-building it means building the thing that is not the moat. |
|
|
32
|
+
| What do recipients do? | **Read only.** Published shelf. No accounts, no writes, no server. Tracking stays on the author's side. |
|
|
33
|
+
| Build strategy | Approach A (a real `shelf` command), shipped C-first (walk + export + index usable before search exists). |
|
|
34
|
+
|
|
35
|
+
### Approaches considered and rejected
|
|
36
|
+
|
|
37
|
+
- **One Opal app that is the shelf.** Single bundle, instant nav, snappy — but
|
|
38
|
+
every doc in the corpus must first clear Opal component parity (tables,
|
|
39
|
+
mermaid, images), and `stream_weaver-5n9w` shows doc output there still has
|
|
40
|
+
rough edges. Rejected: it gates a content corpus on framework work.
|
|
41
|
+
- **Bash loop + hand-written index.** `for f in docs/*.rb; do streamweaver
|
|
42
|
+
export "$f"; done` plus a small index generator. Genuinely available today
|
|
43
|
+
and worth doing if the need is urgent and one-off. Differs from the real
|
|
44
|
+
thing by exactly two features: search and shared nav.
|
|
45
|
+
|
|
46
|
+
## Design § 1 — Doc identity and the manifest
|
|
47
|
+
|
|
48
|
+
This is the part that is expensive to change later, and the reason to design it
|
|
49
|
+
now rather than improvise it under time pressure.
|
|
50
|
+
|
|
51
|
+
**Stable, content-derived slugs — never ordinal.** The reader addresses docs by
|
|
52
|
+
position (`?file=0`); `FileList` carries `files`, `history_roots`, and `labels`
|
|
53
|
+
but no slug or id. A link to "the third doc in the directory" breaks the moment
|
|
54
|
+
a doc is added. Precedence for the shelf:
|
|
55
|
+
|
|
56
|
+
1. explicit `#+SLUG:` in the org file
|
|
57
|
+
2. slugified `#+TITLE:`
|
|
58
|
+
3. filename basename
|
|
59
|
+
|
|
60
|
+
Output is `dist/<slug>.html`, stable across rebuilds and reordering.
|
|
61
|
+
|
|
62
|
+
**`dist/shelf.json` — one manifest, three consumers.** An array of
|
|
63
|
+
`{slug, title, source, headings[], tags[], links_out[], links_in[]}`:
|
|
64
|
+
|
|
65
|
+
1. feeds client-side search (no separate `search.json`)
|
|
66
|
+
2. is what an **agent** reads to find what is in the corpus without crawling HTML
|
|
67
|
+
3. is the substrate a future wiki computes related-content over
|
|
68
|
+
|
|
69
|
+
The builder walks every file anyway, so this is close to free.
|
|
70
|
+
|
|
71
|
+
**Tags from org.** Teach `Org::Reader` to read `#+FILETAGS:` into the manifest —
|
|
72
|
+
a few lines, and it means docs can be categorized in the authoring format
|
|
73
|
+
starting immediately, so a later wiki inherits real data instead of an empty
|
|
74
|
+
corpus.
|
|
75
|
+
|
|
76
|
+
### Wiki-readiness (deliberate seam)
|
|
77
|
+
|
|
78
|
+
An org-based Karpathy-style wiki over the doc corpus is anticipated but **out of
|
|
79
|
+
scope here** — no related-content rendering, no category pages, no graph view.
|
|
80
|
+
What this design *does* commit to, so the wiki is cheap when it arrives:
|
|
81
|
+
|
|
82
|
+
- `[[slug]]` resolves to `<a href="slug.html">` when the slug is in the
|
|
83
|
+
manifest; stays literal text when it is not. ~10 lines.
|
|
84
|
+
- Forward links **and backlinks** are recorded in `shelf.json`.
|
|
85
|
+
|
|
86
|
+
The payoff is timing: links can be written into org docs from day one, so the
|
|
87
|
+
corpus arrives at the wiki already connected rather than needing a linking pass
|
|
88
|
+
over the whole shelf. Related: a desire to extend UKF to support org mode.
|
|
89
|
+
|
|
90
|
+
## Design § 2 — The build pipeline
|
|
91
|
+
|
|
92
|
+
**Command:** `streamweaver shelf <paths...> [-o dist/] [--title "..."] [--offline]`
|
|
93
|
+
|
|
94
|
+
Same argument shape as `canvas-read`, using **the same `FileList.build`** — so
|
|
95
|
+
whatever `canvas-read` shows, `shelf` builds, and the two cannot drift into
|
|
96
|
+
disagreeing about what is in the corpus.
|
|
97
|
+
|
|
98
|
+
**Three passes, because links force it:**
|
|
99
|
+
|
|
100
|
+
1. **Metadata** — walk every file, extract `{slug, title, tags, source}`. No
|
|
101
|
+
rendering. Exists because `[[slug]]` cannot resolve until all slugs are known.
|
|
102
|
+
2. **Render** — per doc, `HtmlExporter.from_dsl_file(path, theme: :default,
|
|
103
|
+
layout: :fluid)` (identical defaults to `streamweaver export`, so a shelf
|
|
104
|
+
page and a single-file export of the same doc are byte-comparable), then
|
|
105
|
+
resolve `[[slug]]` and record edges.
|
|
106
|
+
3. **Write** — `dist/<slug>.html`, `dist/index.html`, `dist/shelf.json` with
|
|
107
|
+
backlinks now that all edges are known.
|
|
108
|
+
|
|
109
|
+
**Units:**
|
|
110
|
+
|
|
111
|
+
| Unit | Job | Knows nothing about |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `Shelf::Manifest` | slug/title/tag extraction, doc list | HTML |
|
|
114
|
+
| `Shelf::LinkResolver` | `[[slug]]` → anchor, emits edges | files, rendering |
|
|
115
|
+
| `Shelf::IndexPage` | builds the index | the filesystem |
|
|
116
|
+
| `Shelf::Builder` | orchestrates passes, writes output | how any of the above work |
|
|
117
|
+
|
|
118
|
+
**The index page is itself a StreamWeaver DSL doc**, rendered through the same
|
|
119
|
+
`HtmlExporter`. No second templating system, no second styling path — the index
|
|
120
|
+
inherits the theme and is authored in the DSL.
|
|
121
|
+
|
|
122
|
+
**Where a shelf differs from N single files** (decided, not open):
|
|
123
|
+
|
|
124
|
+
- **Images copy to `dist/assets/` with paths rewritten, not base64.**
|
|
125
|
+
`--inline-images` suits one emailed file; across a 50-doc shelf it duplicates
|
|
126
|
+
every shared image into every referencing page. The flag stays for the
|
|
127
|
+
single-file path.
|
|
128
|
+
- **Mermaid is written once to `dist/assets/` and shared.** `--offline`
|
|
129
|
+
currently inlines the whole library per document — fine once, absurd fifty
|
|
130
|
+
times. One copy, every page points at it, still works with no network.
|
|
131
|
+
|
|
132
|
+
## Slices
|
|
133
|
+
|
|
134
|
+
1. **Walk + per-doc export + generated index.** Usable immediately; no search.
|
|
135
|
+
2. **`shelf.json` + client-side search + shared nav strip.**
|
|
136
|
+
3. **`[[slug]]` resolution + backlinks.** (Can fold into 1 if links are wanted
|
|
137
|
+
from the very first corpus.)
|
|
138
|
+
|
|
139
|
+
## Open questions deferred to build time
|
|
140
|
+
|
|
141
|
+
- **Hosting/privacy.** GitHub Pages is free only for public repos; a corpus
|
|
142
|
+
intended for a named set of students is not obviously public. Alternatives
|
|
143
|
+
worth pricing then: Netlify or Cloudflare Pages (both serve from a private
|
|
144
|
+
repo on free tiers).
|
|
145
|
+
- Whether readers want search at all, categorization by category/position, or
|
|
146
|
+
links back to source-video timestamps. Unknown until a real reader uses it —
|
|
147
|
+
do not guess, ship slice 1 and watch.
|
|
148
|
+
|
|
149
|
+
## Related
|
|
150
|
+
|
|
151
|
+
- `docs/opal-jamstack.md` — the static Opal path and its Phase 1 scope limits
|
|
152
|
+
- `stream_weaver-5n9w` — opal-build standalone HTML `sidebar_toc` scroll-spy gap
|
|
153
|
+
- `stream_weaver-mdc` — canvas docs epic (discovery, export, cleanup, fidelity)
|