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.
Files changed (381) hide show
  1. checksums.yaml +7 -0
  2. data/.beads/.gitignore +77 -0
  3. data/.beads/README.md +81 -0
  4. data/.beads/config.yaml +54 -0
  5. data/.beads/metadata.json +7 -0
  6. data/.rspec +3 -0
  7. data/AGENTS.md +91 -0
  8. data/CHANGELOG.md +294 -0
  9. data/CLAUDE.md +67 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +565 -0
  12. data/Rakefile +13 -0
  13. data/assets/streamweaver-hero.jpg +0 -0
  14. data/bench/baselines/ledger.rb +58 -0
  15. data/bench/baselines/warroom.rb +51 -0
  16. data/bench/fixtures/ledger.rb +76 -0
  17. data/bench/fixtures/warroom.rb +60 -0
  18. data/bench/results/6daf8b8.md +102 -0
  19. data/bench/results/c8d749b.md +102 -0
  20. data/bench/results/eb06ab9.md +102 -0
  21. data/bench/run.rb +151 -0
  22. data/bench/support.rb +84 -0
  23. data/copy_this/dto_run_sample_run_7631.html +220 -0
  24. data/docs/SERVICE_MODE.md +302 -0
  25. data/docs/architecture/how_streamweaver_works.md +772 -0
  26. data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
  27. data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
  28. data/docs/canvas-ipc-session-summary.md +127 -0
  29. data/docs/canvas-panel-workflow.md +172 -0
  30. data/docs/canvas-read.md +147 -0
  31. data/docs/canvas-roadmap.md +275 -0
  32. data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
  33. data/docs/claude-code-companion-skill-spec.md +567 -0
  34. data/docs/components_reference.md +1223 -0
  35. data/docs/crud-patterns.md +265 -0
  36. data/docs/endpoints.md +122 -0
  37. data/docs/for_llms.md +1 -0
  38. data/docs/form-patterns.md +165 -0
  39. data/docs/frontend-only.md +169 -0
  40. data/docs/html-artifact-audit.md +238 -0
  41. data/docs/ideas/2025-01-01-as-a-service.md +203 -0
  42. data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
  43. data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
  44. data/docs/ideas/dhh-review-future-refactors.md +142 -0
  45. data/docs/opal-jamstack.md +47 -0
  46. data/docs/opal-npm-direction.md +184 -0
  47. data/docs/opal-spike-findings.md +194 -0
  48. data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
  49. data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
  50. data/docs/plans/2026-01-02-charts-design.md +288 -0
  51. data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
  52. data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
  53. data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
  54. data/docs/plans/canvas-claude-project-design.md +238 -0
  55. data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
  56. data/docs/plans/org-doc-preview-surfaces.md +293 -0
  57. data/docs/plans/shadcn-polish-plan.md +417 -0
  58. data/docs/porting-artifacts.md +252 -0
  59. data/docs/reference/agent-skills-comparison.md +369 -0
  60. data/docs/reference/travel-state-prd.artifact.html +780 -0
  61. data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
  62. data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
  63. data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
  64. data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
  65. data/docs/research/frontend-only-matrix.md +278 -0
  66. data/docs/research/streamweaver-way-spike-findings.md +529 -0
  67. data/docs/resource-dsl.md +342 -0
  68. data/docs/routing.md +302 -0
  69. data/docs/ruby-ui-comparison.md +319 -0
  70. data/docs/shared-dsl-fragments.md +356 -0
  71. data/docs/streamweaver-for-ai-agents.md +401 -0
  72. data/docs/streamweaver-frontend-vision.md +123 -0
  73. data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
  74. data/docs/streamweaver_canvas/save_example.rb +9 -0
  75. data/docs/templates.md +213 -0
  76. data/docs/testing.md +155 -0
  77. data/docs/theming-hooks.md +281 -0
  78. data/docs/tutorials/the-streamweaver-way.md +882 -0
  79. data/docs/university/capability-inventory.md +137 -0
  80. data/docs/university/dependency-survey.md +114 -0
  81. data/docs/university/design-spec.md +217 -0
  82. data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
  83. data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
  84. data/docs/university/mockups/course-list-dark.png +0 -0
  85. data/docs/university/mockups/course-list-light.png +0 -0
  86. data/docs/university/mockups/course_canvas_mockup.rb +741 -0
  87. data/docs/university/mockups/step-screen-dark.png +0 -0
  88. data/docs/university/mockups/step-screen-light.png +0 -0
  89. data/docs/university/roadmap.md +72 -0
  90. data/docs/university/send-to-coworker.md +77 -0
  91. data/docs/university/worker-session-mining.md +109 -0
  92. data/docs/visual/browser-rendering-spike.md +173 -0
  93. data/docs/visual/builder-visual-plan-analysis.md +378 -0
  94. data/docs/visual/sw-plan-format-exploration.md +511 -0
  95. data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
  96. data/docs/visual-skills/PROGRESS.md +60 -0
  97. data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
  98. data/docs/visual-skills/analysis/components.md +317 -0
  99. data/docs/visual-skills/analysis/overlap.md +360 -0
  100. data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
  101. data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
  102. data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
  103. data/docs/visual-skills/analysis/visual-explainer.md +964 -0
  104. data/docs/visual-skills/blog/blog-series-outline.md +39 -0
  105. data/docs/visual-skills/blog/token-efficiency.md +73 -0
  106. data/docs/visual-skills/design/architecture.md +1725 -0
  107. data/docs/visual-skills/design/codex-review.md +340 -0
  108. data/docs/visual-skills/design/dhh-review.md +209 -0
  109. data/docs/visual-skills/design/evolution.md +100 -0
  110. data/docs/visual-skills/design/gemini-review.md +55 -0
  111. data/docs/visual-skills/design/review-synthesis.md +149 -0
  112. data/docs/visual-skills/implementation/STATE.md +22 -0
  113. data/docs/visual-skills/implementation/plan.md +347 -0
  114. data/docs/visual-skills/implementation/spike-findings.md +237 -0
  115. data/docs/visual-skills/implementation/tasks.md +702 -0
  116. data/docs/visual-skills/lessons-learned/process.md +145 -0
  117. data/examples/README.md +96 -0
  118. data/examples/advanced/all_components.rb +177 -0
  119. data/examples/advanced/examples_browser.rb +394 -0
  120. data/examples/advanced/teachables_browser.rb +261 -0
  121. data/examples/advanced/theme_tweaker.rb +366 -0
  122. data/examples/advanced/tutorial.rb +1528 -0
  123. data/examples/agentic/agentic_form.rb +30 -0
  124. data/examples/agentic/agentic_form_autoclose.rb +30 -0
  125. data/examples/agentic/cultivation_tracker.rb +69 -0
  126. data/examples/basic/hello_world.rb +22 -0
  127. data/examples/basic/opal_tabs_table.rb +44 -0
  128. data/examples/basic/todo_list.rb +39 -0
  129. data/examples/button_loading_test.rb +36 -0
  130. data/examples/canvas/mermaid_canvas_demo.sh +151 -0
  131. data/examples/charts/bar_chart_demo.rb +57 -0
  132. data/examples/charts/line_chart_demo.rb +73 -0
  133. data/examples/charts/pie_area_demo.rb +87 -0
  134. data/examples/charts/stacked_bar_chart_demo.rb +69 -0
  135. data/examples/claude_code/README.md +85 -0
  136. data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
  137. data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
  138. data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
  139. data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
  140. data/examples/claude_code/codebreaker/README.md +213 -0
  141. data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
  142. data/examples/claude_code/tutorial/README.md +102 -0
  143. data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
  144. data/examples/claude_code/verification_flow/README.md +126 -0
  145. data/examples/components/annotated_code_demo.rb +54 -0
  146. data/examples/components/callout_demo.rb +33 -0
  147. data/examples/components/checkbox_group_demo.rb +46 -0
  148. data/examples/components/design_review.css +468 -0
  149. data/examples/components/design_review_demo.rb +44 -0
  150. data/examples/components/design_review_dsl.rb +300 -0
  151. data/examples/components/diff_block_demo.rb +112 -0
  152. data/examples/components/events_demo.rb +184 -0
  153. data/examples/components/form_demo.rb +62 -0
  154. data/examples/components/lesson_demo.rb +53 -0
  155. data/examples/components/markdown_demo.rb +110 -0
  156. data/examples/components/mermaid_demo.rb +166 -0
  157. data/examples/components/pareto_set.rb +64 -0
  158. data/examples/components/prd_demo.rb +21 -0
  159. data/examples/components/prd_dsl.rb +385 -0
  160. data/examples/components/quiz_demo.rb +47 -0
  161. data/examples/components/run_viewer_demo.rb +100 -0
  162. data/examples/components/score_and_collapsible_demo.rb +82 -0
  163. data/examples/components/select_stale_value_smoke_test.rb +39 -0
  164. data/examples/components/table_demo.rb +247 -0
  165. data/examples/components/timer_with_state_demo.rb +58 -0
  166. data/examples/components/todo_due_dates.rb +63 -0
  167. data/examples/components/uat_gaps_demo.rb +112 -0
  168. data/examples/dashboard/feed_simulator.rb +60 -0
  169. data/examples/dashboard/live_dashboard.rb +80 -0
  170. data/examples/dashboard_components.rb +108 -0
  171. data/examples/deferred_fragments_demo.rb +62 -0
  172. data/examples/generate_more_spike/README.md +63 -0
  173. data/examples/generate_more_spike/app.rb +441 -0
  174. data/examples/git_health.sh +401 -0
  175. data/examples/layout/layout_components_demo.rb +174 -0
  176. data/examples/layout/modal_demo.rb +215 -0
  177. data/examples/layout/navigation_demo.rb +227 -0
  178. data/examples/layout/route_tabs_demo.rb +52 -0
  179. data/examples/layout/routing_demo.rb +52 -0
  180. data/examples/layout/scroll_box_demo.rb +154 -0
  181. data/examples/lazy_fragments_demo.rb +151 -0
  182. data/examples/my_todos/README.md +29 -0
  183. data/examples/my_todos/my_todos.rb +312 -0
  184. data/examples/my_todos/store.rb +86 -0
  185. data/examples/opal/reactive_demo.rb +62 -0
  186. data/examples/opal/scenarios/s1_counter.rb +15 -0
  187. data/examples/opal/scenarios/s2_search_filter.rb +17 -0
  188. data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
  189. data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
  190. data/examples/opal/scenarios/s5_watch.rb +39 -0
  191. data/examples/opal/scenarios/s6_on_start.rb +20 -0
  192. data/examples/opal/scenarios/s7_wizard.rb +33 -0
  193. data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
  194. data/examples/opal/scenarios/s9_dashboard.rb +27 -0
  195. data/examples/operations_dashboard_demo.rb +167 -0
  196. data/examples/panel_demo.sh +447 -0
  197. data/examples/parity/assets/tyrion_slice.css +86 -0
  198. data/examples/parity/rivet_people_slice.rb +182 -0
  199. data/examples/parity/tyrion_components.css +777 -0
  200. data/examples/parity/tyrion_warroom_components.rb +414 -0
  201. data/examples/parity/tyrion_warroom_slice.rb +549 -0
  202. data/examples/puma_dev/README.md +124 -0
  203. data/examples/puma_dev/config.ru +34 -0
  204. data/examples/puma_dev/standalone_app.rb +34 -0
  205. data/examples/scaffolding/blog.rb +49 -0
  206. data/examples/scaffolding/utf_lite.rb +105 -0
  207. data/examples/styling/feedback_demo.rb +229 -0
  208. data/examples/styling/style_showcase.rb +172 -0
  209. data/examples/styling/theme_demo.rb +508 -0
  210. data/examples/timer_health_checker.rb +111 -0
  211. data/examples/timer_showcase.rb +131 -0
  212. data/examples/tutorials/render_markdown.rb +112 -0
  213. data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
  214. data/examples/tutorials/tutorial_content.rb +1082 -0
  215. data/examples/visual_skills/design_deck_demo.rb +314 -0
  216. data/examples/visual_skills/explainer_demo.rb +499 -0
  217. data/exe/streamweaver +9 -0
  218. data/gsd/ROADMAP-1.0.md +167 -0
  219. data/gsd/STATE.md +13 -0
  220. data/gsd/research/market-positioning-research.md +139 -0
  221. data/gsd/research/production-patterns-research.md +270 -0
  222. data/gsd/research/repo-audit-1.0.md +274 -0
  223. data/lib/stream_weaver/action_token.rb +58 -0
  224. data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
  225. data/lib/stream_weaver/adapter/base.rb +591 -0
  226. data/lib/stream_weaver/adapter/opal.rb +334 -0
  227. data/lib/stream_weaver/adapter/static.rb +1118 -0
  228. data/lib/stream_weaver/admin.rb +176 -0
  229. data/lib/stream_weaver/app.rb +1821 -0
  230. data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
  231. data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
  232. data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
  233. data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
  234. data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
  235. data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
  236. data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
  237. data/lib/stream_weaver/canvas/bridge.rb +239 -0
  238. data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
  239. data/lib/stream_weaver/canvas/client.rb +298 -0
  240. data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
  241. data/lib/stream_weaver/canvas/doc_store.rb +252 -0
  242. data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
  243. data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
  244. data/lib/stream_weaver/canvas/gist_store.rb +135 -0
  245. data/lib/stream_weaver/canvas/helpers.rb +109 -0
  246. data/lib/stream_weaver/canvas/history.rb +90 -0
  247. data/lib/stream_weaver/canvas/protocol.rb +86 -0
  248. data/lib/stream_weaver/canvas/reader.rb +853 -0
  249. data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
  250. data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
  251. data/lib/stream_weaver/canvas/session.rb +132 -0
  252. data/lib/stream_weaver/cli.rb +3235 -0
  253. data/lib/stream_weaver/component_assets.rb +70 -0
  254. data/lib/stream_weaver/component_registry.rb +67 -0
  255. data/lib/stream_weaver/component_renderer.rb +48 -0
  256. data/lib/stream_weaver/components/annotated_code.rb +53 -0
  257. data/lib/stream_weaver/components/api_endpoint.rb +42 -0
  258. data/lib/stream_weaver/components/callout.rb +59 -0
  259. data/lib/stream_weaver/components/chart.rb +84 -0
  260. data/lib/stream_weaver/components/code_block.rb +77 -0
  261. data/lib/stream_weaver/components/comparison.rb +39 -0
  262. data/lib/stream_weaver/components/decision.rb +38 -0
  263. data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
  264. data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
  265. data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
  266. data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
  267. data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
  268. data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
  269. data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
  270. data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
  271. data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
  272. data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
  273. data/lib/stream_weaver/components/diff_block.rb +125 -0
  274. data/lib/stream_weaver/components/doc_header.rb +57 -0
  275. data/lib/stream_weaver/components/image_block.rb +70 -0
  276. data/lib/stream_weaver/components/implementation_map.rb +33 -0
  277. data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
  278. data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
  279. data/lib/stream_weaver/components/mermaid.rb +79 -0
  280. data/lib/stream_weaver/components/pipeline.rb +63 -0
  281. data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
  282. data/lib/stream_weaver/components/slide_container.rb +141 -0
  283. data/lib/stream_weaver/components/timeline_event.rb +58 -0
  284. data/lib/stream_weaver/components/wireframe.rb +29 -0
  285. data/lib/stream_weaver/components/wireframe_block.rb +34 -0
  286. data/lib/stream_weaver/components.rb +2711 -0
  287. data/lib/stream_weaver/css.rb +230 -0
  288. data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
  289. data/lib/stream_weaver/display_dsl.rb +1004 -0
  290. data/lib/stream_weaver/export/html_exporter.rb +478 -0
  291. data/lib/stream_weaver/feed.rb +34 -0
  292. data/lib/stream_weaver/feed_builder.rb +31 -0
  293. data/lib/stream_weaver/fonts.rb +33 -0
  294. data/lib/stream_weaver/interaction_runner.rb +487 -0
  295. data/lib/stream_weaver/iterm.rb +460 -0
  296. data/lib/stream_weaver/layout_registry.rb +92 -0
  297. data/lib/stream_weaver/opal/bridge.rb +52 -0
  298. data/lib/stream_weaver/opal/builder.rb +193 -0
  299. data/lib/stream_weaver/opal/env.rb +29 -0
  300. data/lib/stream_weaver/opal/reactive_state.rb +75 -0
  301. data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
  302. data/lib/stream_weaver/opal/renderer.rb +77 -0
  303. data/lib/stream_weaver/opal/runtime.rb +250 -0
  304. data/lib/stream_weaver/opal/shell.rb +145 -0
  305. data/lib/stream_weaver/opal/string_bridge.rb +50 -0
  306. data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
  307. data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
  308. data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
  309. data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
  310. data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
  311. data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
  312. data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
  313. data/lib/stream_weaver/opal_entry.rb +136 -0
  314. data/lib/stream_weaver/org/inline.rb +95 -0
  315. data/lib/stream_weaver/org/reader.rb +561 -0
  316. data/lib/stream_weaver/org/recording_context.rb +86 -0
  317. data/lib/stream_weaver/org/source_splitter.rb +63 -0
  318. data/lib/stream_weaver/org/writer.rb +300 -0
  319. data/lib/stream_weaver/page_shell.rb +530 -0
  320. data/lib/stream_weaver/portfile.rb +79 -0
  321. data/lib/stream_weaver/pushable.rb +42 -0
  322. data/lib/stream_weaver/resource/default_views.rb +111 -0
  323. data/lib/stream_weaver/resource/field_input.rb +25 -0
  324. data/lib/stream_weaver/resource/state_keys.rb +16 -0
  325. data/lib/stream_weaver/resource/store.rb +18 -0
  326. data/lib/stream_weaver/resource.rb +104 -0
  327. data/lib/stream_weaver/server.rb +1471 -0
  328. data/lib/stream_weaver/service.rb +1240 -0
  329. data/lib/stream_weaver/service_client.rb +104 -0
  330. data/lib/stream_weaver/session_store.rb +186 -0
  331. data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
  332. data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
  333. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
  334. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
  335. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
  336. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
  337. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
  338. data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
  339. data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
  340. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
  341. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
  342. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
  343. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
  344. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
  345. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
  346. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
  347. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
  348. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
  349. data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
  350. data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
  351. data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
  352. data/lib/stream_weaver/streamer.rb +63 -0
  353. data/lib/stream_weaver/templates/choices.rb +149 -0
  354. data/lib/stream_weaver/templates/code.rb +157 -0
  355. data/lib/stream_weaver/templates/confirm.rb +110 -0
  356. data/lib/stream_weaver/templates/diff.rb +195 -0
  357. data/lib/stream_weaver/templates/info.rb +142 -0
  358. data/lib/stream_weaver/templates/table.rb +169 -0
  359. data/lib/stream_weaver/templates/wizard.rb +271 -0
  360. data/lib/stream_weaver/theme/auto_mode.rb +116 -0
  361. data/lib/stream_weaver/theme/presets.rb +555 -0
  362. data/lib/stream_weaver/theme.rb +639 -0
  363. data/lib/stream_weaver/university/canvas.rb +924 -0
  364. data/lib/stream_weaver/university/course.rb +699 -0
  365. data/lib/stream_weaver/university/demos/counter.rb +33 -0
  366. data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
  367. data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
  368. data/lib/stream_weaver/university/demos.rb +50 -0
  369. data/lib/stream_weaver/university/listener.rb +467 -0
  370. data/lib/stream_weaver/university/progress.rb +201 -0
  371. data/lib/stream_weaver/university/runner.rb +138 -0
  372. data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
  373. data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
  374. data/lib/stream_weaver/utils.rb +31 -0
  375. data/lib/stream_weaver/version.rb +5 -0
  376. data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
  377. data/lib/stream_weaver/views.rb +3940 -0
  378. data/lib/stream_weaver.rb +122 -0
  379. data/llms.txt +1269 -0
  380. data/sig/stream_weaver.rbs +4 -0
  381. metadata +598 -0
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # StreamWeaver Examples Browser
5
+ # Browse, view, and run StreamWeaver examples with syntax highlighting.
6
+ #
7
+ # Run with: streamweaver showcase
8
+ # (Or: ruby examples/advanced/examples_browser.rb)
9
+
10
+ require_relative "../../lib/stream_weaver"
11
+
12
+ # Source identifier for tracking apps in service
13
+ SOURCE = "examples_browser"
14
+
15
+ # Track loaded apps: file_path => app_id (for edit-replace and cleanup)
16
+ LOADED_APPS = {}
17
+
18
+ # Cleanup all loaded apps on exit
19
+ at_exit do
20
+ next if LOADED_APPS.empty?
21
+ puts "\nCleaning up #{LOADED_APPS.size} loaded example(s)..."
22
+ Object.new.extend(StreamWeaver::ServiceClient).clear_source_via_service(SOURCE)
23
+ end
24
+
25
+ # Helper module for example discovery
26
+ module ExamplesBrowser
27
+ include StreamWeaver::ServiceClient
28
+ require 'fileutils'
29
+
30
+ EXAMPLES_ROOT = File.expand_path("../../", __FILE__)
31
+ PLAYGROUND_ROOT = File.expand_path("../../../examples_playground", __FILE__)
32
+
33
+ # Skip the browser itself to avoid inception
34
+ SKIP_FILES = ["examples_browser.rb"].freeze
35
+
36
+ # Directory display order and descriptions
37
+ DIRECTORIES = {
38
+ basic: "Basic",
39
+ agentic: "Agentic",
40
+ components: "Components",
41
+ layout: "Layout",
42
+ styling: "Styling",
43
+ advanced: "Advanced"
44
+ }.freeze
45
+
46
+ def ensure_playground_exists
47
+ return if File.directory?(PLAYGROUND_ROOT)
48
+ FileUtils.cp_r(EXAMPLES_ROOT, PLAYGROUND_ROOT)
49
+ # Remove the browser itself from playground
50
+ FileUtils.rm_f(File.join(PLAYGROUND_ROOT, "advanced/examples_browser.rb"))
51
+ end
52
+
53
+ def playground_path(dir_key, filename)
54
+ File.join(PLAYGROUND_ROOT, dir_key.to_s, filename)
55
+ end
56
+
57
+ def original_path(dir_key, filename)
58
+ File.join(EXAMPLES_ROOT, dir_key.to_s, filename)
59
+ end
60
+
61
+ def reset_file(dir_key, filename)
62
+ src = original_path(dir_key, filename)
63
+ dst = playground_path(dir_key, filename)
64
+ FileUtils.cp(src, dst)
65
+ end
66
+
67
+ def discover_examples
68
+ DIRECTORIES.map do |dir_key, label|
69
+ dir_path = File.join(EXAMPLES_ROOT, dir_key.to_s)
70
+ next unless File.directory?(dir_path)
71
+
72
+ files = Dir.glob(File.join(dir_path, "*.rb"))
73
+ .map { |f| File.basename(f) }
74
+ .reject { |f| SKIP_FILES.include?(f) }
75
+ .sort
76
+
77
+ { key: dir_key, label: label, files: files }
78
+ end.compact
79
+ end
80
+
81
+ def read_file(dir_key, filename)
82
+ ensure_playground_exists
83
+ path = playground_path(dir_key, filename)
84
+ return "" unless File.exist?(path)
85
+ File.read(path)
86
+ end
87
+
88
+ def file_path(dir_key, filename)
89
+ ensure_playground_exists
90
+ playground_path(dir_key, filename)
91
+ end
92
+
93
+ def check_syntax(code)
94
+ require 'open3'
95
+ temp_file = "/tmp/sw_syntax_check_#{Process.pid}.rb"
96
+ File.write(temp_file, code)
97
+
98
+ stdout, stderr, status = Open3.capture3("ruby", "-c", temp_file)
99
+ File.delete(temp_file) rescue nil
100
+
101
+ if status.success?
102
+ { ok: true, message: "Syntax OK" }
103
+ else
104
+ error = stderr.gsub(temp_file, "example.rb")
105
+ { ok: false, message: error.strip }
106
+ end
107
+ end
108
+
109
+ # Uses ServiceClient for: service_port, open_in_browser, remove_app_via_service
110
+
111
+ def run_example_via_service(file_path, name: nil)
112
+ expanded_path = File.expand_path(file_path)
113
+
114
+ # Remove old version if same file was already loaded (edit-replace)
115
+ if LOADED_APPS[expanded_path]
116
+ remove_app_via_service(LOADED_APPS[expanded_path][:app_id])
117
+ LOADED_APPS.delete(expanded_path)
118
+ end
119
+
120
+ # Load via ServiceClient
121
+ result = load_app_via_service(expanded_path, source: SOURCE, name: name)
122
+
123
+ if result[:ok]
124
+ # Track for cleanup and edit-replace
125
+ LOADED_APPS[expanded_path] = {
126
+ app_id: result[:app_id],
127
+ aliased_url: result[:aliased_url]
128
+ }
129
+ end
130
+
131
+ result
132
+ end
133
+
134
+ def save_file(dir_key, filename, content)
135
+ ensure_playground_exists
136
+ path = playground_path(dir_key, filename)
137
+ File.write(path, content)
138
+ { ok: true, message: "Saved #{filename}" }
139
+ rescue => e
140
+ { ok: false, message: "Failed to save: #{e.message}" }
141
+ end
142
+
143
+ def all_dir_keys
144
+ DIRECTORIES.keys
145
+ end
146
+ end
147
+
148
+ # CodeMirror 5 CDN URLs
149
+ CODEMIRROR_CSS = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css"
150
+ CODEMIRROR_JS = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"
151
+ CODEMIRROR_RUBY = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/ruby/ruby.min.js"
152
+
153
+ generated_app = app(
154
+ "Example Playground",
155
+ layout: :fluid,
156
+ theme: :default,
157
+ stylesheets: [CODEMIRROR_CSS],
158
+ scripts: [CODEMIRROR_JS, CODEMIRROR_RUBY],
159
+ components: [ExamplesBrowser]
160
+ ) do
161
+ # Initialize state - derive examples fresh each time (not stored in session)
162
+ examples = discover_examples
163
+ state[:expanded_dirs] ||= [:basic]
164
+ state[:selected_dir] ||= :basic
165
+ state[:selected_file] ||= examples.first&.dig(:files)&.first
166
+ state[:error_modal_open] ||= false
167
+ state[:error_message] ||= ""
168
+
169
+ # Track which file is loaded to detect file changes
170
+ # Only read from file when selection changes (not on every request!)
171
+ # This preserves edited content when clicking Save/Run/Check
172
+ loaded_key = "#{state[:selected_dir]}/#{state[:selected_file]}"
173
+
174
+ if state[:selected_dir] && state[:selected_file]
175
+ state[:current_file_path] = file_path(state[:selected_dir], state[:selected_file])
176
+
177
+ # Only read from file when selection changes OR content is empty (recovery from bad session state)
178
+ if state[:loaded_file_key] != loaded_key || state[:code_content].to_s.strip.empty?
179
+ state[:code_content] = read_file(state[:selected_dir], state[:selected_file])
180
+ state[:loaded_file_key] = loaded_key
181
+ # Clear stale status from previous session
182
+ state[:save_ok] = nil
183
+ state[:syntax_ok] = nil
184
+ state[:reset_ok] = nil
185
+ end
186
+ else
187
+ state[:current_file_path] = nil
188
+ state[:code_content] = ""
189
+ state[:loaded_file_key] = nil
190
+ end
191
+
192
+ # Main layout
193
+ div style: "display: flex; gap: 20px;" do
194
+ # Sidebar - file browser (Finder-style)
195
+ div style: "width: 220px; flex-shrink: 0;" do
196
+ # Expand/Collapse all buttons
197
+ div style: "display: flex; gap: 4px; margin-bottom: 8px;" do
198
+ small_btn = "padding: 4px 8px; font-size: 11px; background: #fff; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; color: #666;"
199
+ button "Expand All", style: small_btn do |s|
200
+ s[:expanded_dirs] = all_dir_keys
201
+ end
202
+ button "Collapse All", style: small_btn do |s|
203
+ s[:expanded_dirs] = []
204
+ end
205
+ end
206
+
207
+ div style: "background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 6px; padding: 8px;" do
208
+ examples.each do |dir|
209
+ is_expanded = state[:expanded_dirs].include?(dir[:key])
210
+ disclosure = is_expanded ? "▾" : "▸"
211
+
212
+ # Folder row (clickable to expand/collapse)
213
+ folder_style = "display: block !important; padding: 6px 8px !important; cursor: pointer !important; border-radius: 4px !important; font-weight: 500 !important; color: #333 !important; font-size: 13px !important; border: none !important; background: transparent !important; width: 100% !important; text-align: left !important;"
214
+ folder_label = "#{disclosure} 📁 #{dir[:label]} (#{dir[:files].length})"
215
+ button folder_label, style: folder_style do |s|
216
+ if s[:expanded_dirs].include?(dir[:key])
217
+ s[:expanded_dirs] = s[:expanded_dirs] - [dir[:key]]
218
+ else
219
+ s[:expanded_dirs] = s[:expanded_dirs] + [dir[:key]]
220
+ end
221
+ end
222
+
223
+ # File list (if expanded)
224
+ if is_expanded
225
+ div do
226
+ dir[:files].each do |filename|
227
+ is_selected = state[:selected_dir] == dir[:key] && state[:selected_file] == filename
228
+ bg_color = is_selected ? "#d4e5f7" : "transparent"
229
+ text_color = is_selected ? "#333" : "#555"
230
+ file_style = "display: block !important; padding: 5px 8px 5px 28px !important; cursor: pointer !important; border-radius: 4px !important; color: #{text_color} !important; background: #{bg_color} !important; font-size: 13px !important; border: none !important; width: 100% !important; text-align: left !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important;"
231
+
232
+ button filename, style: file_style do |s|
233
+ s[:selected_dir] = dir[:key]
234
+ s[:selected_file] = filename
235
+ # Don't clear last_run_file - keep showing which server is running
236
+ s[:syntax_ok] = nil
237
+ s[:save_ok] = nil
238
+ s[:reset_ok] = nil
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ # Main content
248
+ div style: "flex: 1; min-width: 0;" do
249
+ if state[:selected_file]
250
+ # Header with file path and buttons
251
+ div style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;" do
252
+ div style: "font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; font-size: 13px; color: #666;" do
253
+ text "#{state[:selected_dir]}/#{state[:selected_file]}"
254
+ end
255
+ div style: "display: flex; gap: 8px;" do
256
+ secondary_btn = "background: white; border: 1px solid #ccc; color: #333; padding: 8px 16px; border-radius: 6px; font-size: 13px; cursor: pointer;"
257
+
258
+ button "Check", style: secondary_btn do |s|
259
+ result = check_syntax(s[:code_content])
260
+ if result[:ok]
261
+ s[:syntax_ok] = true
262
+ s[:save_ok] = nil
263
+ else
264
+ s[:syntax_ok] = false
265
+ s[:save_ok] = nil
266
+ s[:error_message] = result[:message]
267
+ s[:error_modal_open] = true
268
+ end
269
+ end
270
+
271
+ button "Save", style: secondary_btn do |s|
272
+ result = check_syntax(s[:code_content])
273
+ if result[:ok]
274
+ save_result = save_file(s[:selected_dir], s[:selected_file], s[:code_content])
275
+ s[:save_ok] = save_result[:ok]
276
+ s[:syntax_ok] = nil
277
+ else
278
+ s[:error_message] = result[:message]
279
+ s[:error_modal_open] = true
280
+ end
281
+ end
282
+
283
+ button "Reset", style: secondary_btn do |s|
284
+ reset_file(s[:selected_dir], s[:selected_file])
285
+ s[:loaded_file_key] = nil # Force re-read on next render
286
+ s[:reset_ok] = true
287
+ s[:save_ok] = nil
288
+ s[:syntax_ok] = nil
289
+ end
290
+
291
+ run_btn_style = "background: #CC342D; border: none; color: white; padding: 8px 16px; border-radius: 6px; font-size: 13px; cursor: pointer;"
292
+ button "▶ Run", style: run_btn_style do |s|
293
+ result = check_syntax(s[:code_content])
294
+ if result[:ok]
295
+ # Auto-save before running so edits take effect
296
+ save_file(s[:selected_dir], s[:selected_file], s[:code_content])
297
+ # Load via service API (replaces old version if same file)
298
+ app_result = run_example_via_service(s[:current_file_path], name: "#{s[:selected_dir]}/#{s[:selected_file]}")
299
+ if app_result[:ok]
300
+ # Auto-open in browser
301
+ open_in_browser(app_result[:url])
302
+ s[:syntax_ok] = nil
303
+ s[:save_ok] = nil
304
+ else
305
+ s[:error_message] = app_result[:error]
306
+ s[:error_modal_open] = true
307
+ end
308
+ else
309
+ s[:error_message] = result[:message]
310
+ s[:error_modal_open] = true
311
+ end
312
+ end
313
+ end
314
+ end
315
+
316
+ # Code editor with syntax highlighting (editable)
317
+ code_editor :code_content, language: :ruby, readonly: false, height: "500px"
318
+
319
+ # Status messages
320
+ status_style = "margin-top: 8px; padding: 8px 12px; background: #d4edda; color: #155724; border-radius: 4px; font-size: 13px;"
321
+ if state[:syntax_ok] == true
322
+ div style: status_style do
323
+ text "✓ Syntax OK"
324
+ end
325
+ end
326
+
327
+ if state[:save_ok] == true
328
+ div style: status_style do
329
+ text "✓ Saved"
330
+ end
331
+ end
332
+
333
+ if state[:reset_ok] == true
334
+ div style: status_style do
335
+ text "✓ Reset to original"
336
+ end
337
+ end
338
+
339
+ # Show if current file is loaded in service (can reopen)
340
+ current_app_info = LOADED_APPS[state[:current_file_path]]
341
+ if current_app_info
342
+ running_style = "margin-top: 8px; padding: 8px 12px; background: #e8f5e9; border: 1px solid #81c784; border-radius: 4px; font-size: 13px;"
343
+ current_app_id = current_app_info[:app_id]
344
+ aliased_url = current_app_info[:aliased_url]
345
+ # aliased_url from ServiceClient is already a full URL
346
+ app_url = aliased_url || "http://localhost:#{service_port}/apps/#{current_app_id}"
347
+
348
+ div style: running_style do
349
+ div style: "display: flex; justify-content: space-between; align-items: center;" do
350
+ div do
351
+ text "▶ Running in service"
352
+ if aliased_url
353
+ div style: "font-size: 0.85rem; color: #666; font-family: monospace; margin-top: 2px;" do
354
+ text aliased_url
355
+ end
356
+ end
357
+ end
358
+ div style: "display: flex; gap: 8px; align-items: center;" do
359
+ reopen_style = "background: #4caf50; border: none; color: white; padding: 4px 12px; border-radius: 4px; font-size: 13px; cursor: pointer;"
360
+ button "Reopen ↗", style: reopen_style do |_s|
361
+ open_in_browser(app_url)
362
+ end
363
+ remove_link_style = "background: transparent; border: none; color: #666; cursor: pointer; font-size: 13px; padding: 2px 8px;"
364
+ button "Unload", style: remove_link_style do |_s|
365
+ remove_app_via_service(current_app_id)
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
371
+ else
372
+ div style: "color: #888; padding: 40px; text-align: center;" do
373
+ text "Select an example from the sidebar to view its code."
374
+ end
375
+ end
376
+ end
377
+ end
378
+
379
+ # Error modal
380
+ modal :error, title: "Syntax Error", size: :md do
381
+ div style: "font-family: monospace; white-space: pre-wrap; background: #fee; padding: 1rem; border-radius: 4px; color: #c00; font-size: 13px;" do
382
+ text state[:error_message]
383
+ end
384
+
385
+ modal_footer do
386
+ button "Close" do |s|
387
+ s[:error_modal_open] = false
388
+ end
389
+ end
390
+ end
391
+ end
392
+
393
+ # Always run - showcase command runs this standalone
394
+ generated_app.run!
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Teachables Browser - Visual triage for teachable moments
5
+ #
6
+ # Run: ruby teachables_browser.rb
7
+ # Opens: http://localhost:8080
8
+
9
+ require_relative '../../lib/stream_weaver'
10
+ require 'json'
11
+ require 'yaml'
12
+ require 'fileutils'
13
+
14
+ # Configuration
15
+ CULTIV_OS = File.expand_path('~/cultiv-os')
16
+ PENDING_DIR = File.join(CULTIV_OS, 'captures', 'pending')
17
+ APPROVED_DIR = File.join(CULTIV_OS, 'captures', 'approved')
18
+ REJECTED_DIR = File.join(CULTIV_OS, 'captures', 'rejected')
19
+ DEFERRED_DIR = File.join(CULTIV_OS, 'captures', 'deferred')
20
+
21
+ # Ensure directories exist
22
+ [APPROVED_DIR, REJECTED_DIR, DEFERRED_DIR].each { |d| FileUtils.mkdir_p(d) }
23
+
24
+ # Helper to load all pending teachables
25
+ def load_pending_teachables
26
+ candidates = []
27
+ Dir.glob(File.join(PENDING_DIR, '*.json')).each do |f|
28
+ data = JSON.parse(File.read(f)) rescue {}
29
+ (data['candidates'] || []).each do |c|
30
+ c['_source_file'] = f
31
+ candidates << c
32
+ end
33
+ end
34
+ candidates
35
+ end
36
+
37
+ # Helper to move teachable to a directory
38
+ def move_teachable(candidate, to_dir, status, extra = {})
39
+ source_file = candidate['_source_file']
40
+ return unless source_file && File.exist?(source_file)
41
+
42
+ # Remove from pending JSON
43
+ data = JSON.parse(File.read(source_file))
44
+ data['candidates'].reject! { |c| c['id'] == candidate['id'] }
45
+ File.write(source_file, JSON.pretty_generate(data))
46
+
47
+ # Save to target directory
48
+ candidate.delete('_source_file')
49
+ candidate['status'] = status
50
+ candidate['#{status}_at'] = Time.now.iso8601
51
+ candidate.merge!(extra)
52
+
53
+ slug = candidate['id'].gsub(/[^a-zA-Z0-9-]/, '-')[0..50]
54
+ filepath = File.join(to_dir, "#{Date.today}-#{slug}.yaml")
55
+ File.write(filepath, candidate.to_yaml)
56
+ filepath
57
+ end
58
+
59
+ # Count items in directory
60
+ def count_items(dir)
61
+ Dir.glob(File.join(dir, '*.yaml')).length
62
+ end
63
+
64
+ # Audience badge colors
65
+ def audience_style(aud)
66
+ case aud
67
+ when 'blog' then :strong
68
+ when 'team' then :maybe
69
+ else :skip
70
+ end
71
+ end
72
+
73
+ App = app "Teachables Browser" do
74
+ header "Teachables Browser"
75
+
76
+ # Load data
77
+ all_teachables = load_pending_teachables
78
+
79
+ # Stats row
80
+ columns widths: ['25%', '25%', '25%', '25%'] do
81
+ column do
82
+ card do
83
+ header4 "Pending"
84
+ text "#{all_teachables.length}"
85
+ end
86
+ end
87
+ column do
88
+ card do
89
+ header4 "Approved"
90
+ text "#{count_items(APPROVED_DIR)}"
91
+ end
92
+ end
93
+ column do
94
+ card do
95
+ header4 "Rejected"
96
+ text "#{count_items(REJECTED_DIR)}"
97
+ end
98
+ end
99
+ column do
100
+ card do
101
+ header4 "Deferred"
102
+ text "#{count_items(DEFERRED_DIR)}"
103
+ end
104
+ end
105
+ end
106
+
107
+ # Filters
108
+ header3 "Filters"
109
+
110
+ columns widths: ['33%', '33%', '33%'] do
111
+ column do
112
+ text "Audience:"
113
+ tag_buttons :audience_filter, ["all", "blog", "team", "self"]
114
+ end
115
+ column do
116
+ text "Min External Interest:"
117
+ select :min_external, ["0", "5", "6", "7", "8", "9"], default: "0"
118
+ end
119
+ column do
120
+ text "Sort by:"
121
+ select :sort_by, ["external_interest", "novelty", "reusability"], default: "external_interest"
122
+ end
123
+ end
124
+
125
+ # Apply filters
126
+ state[:audience_filter] ||= "all"
127
+ state[:min_external] ||= "0"
128
+ state[:sort_by] ||= "external_interest"
129
+
130
+ filtered = all_teachables.dup
131
+
132
+ unless state[:audience_filter] == "all"
133
+ filtered = filtered.select { |t| t['audience'] == state[:audience_filter] }
134
+ end
135
+
136
+ min_ext = state[:min_external].to_i
137
+ if min_ext > 0
138
+ filtered = filtered.select { |t| (t.dig('scores', 'external_interest') || 0) >= min_ext }
139
+ end
140
+
141
+ # Sort
142
+ sort_field = state[:sort_by]
143
+ filtered = filtered.sort_by { |t| -(t.dig('scores', sort_field) || 0) }
144
+
145
+ # Results header
146
+ header3 "#{filtered.length} Teachables"
147
+
148
+ if filtered.empty?
149
+ text "No teachables match your filters."
150
+ else
151
+ # Display each teachable
152
+ filtered.each_with_index do |t, idx|
153
+ card do
154
+ # Header row with scores and audience
155
+ columns widths: ['70%', '30%'] do
156
+ column do
157
+ header4 t['summary'][0..80] + (t['summary'].length > 80 ? '...' : '')
158
+ end
159
+ column do
160
+ status_badge audience_style(t['audience']), t['audience']
161
+ end
162
+ end
163
+
164
+ # Metadata row
165
+ text "Project: #{t['project']} | Session: #{t['session_id'][0..20]}..."
166
+
167
+ # Scores
168
+ scores = t['scores'] || {}
169
+ md "**Scores:** Novelty: #{scores['novelty']} | Struggle: #{scores['struggle_to_solution']} | Reuse: #{scores['reusability']} | External: #{scores['external_interest']}"
170
+
171
+ # Themes
172
+ themes = (t['themes'] || [])[0..5].join(', ')
173
+ text "Themes: #{themes}" unless themes.empty?
174
+
175
+ # Patterns
176
+ known = (t.dig('patterns_detected', 'known') || []).join(', ')
177
+ coined = (t.dig('patterns_detected', 'new_coined') || []).join(', ')
178
+ text "Known patterns: #{known}" unless known.empty?
179
+ text "New coined: #{coined}" unless coined.empty?
180
+
181
+ # Context (collapsible)
182
+ collapsible "Context" do
183
+ md t['context'] || "(no context)"
184
+ end
185
+
186
+ # Action buttons
187
+ columns widths: ['25%', '25%', '25%', '25%'] do
188
+ column do
189
+ button "Approve", style: :primary do |state|
190
+ filepath = move_teachable(t, APPROVED_DIR, 'approved')
191
+ state[:last_action] = "Approved: #{t['id']} -> #{filepath}"
192
+ end
193
+ end
194
+ column do
195
+ button "Reject", style: :secondary do |state|
196
+ filepath = move_teachable(t, REJECTED_DIR, 'rejected')
197
+ state[:last_action] = "Rejected: #{t['id']}"
198
+ end
199
+ end
200
+ column do
201
+ button "Defer", style: :secondary do |state|
202
+ filepath = move_teachable(t, DEFERRED_DIR, 'deferred')
203
+ state[:last_action] = "Deferred: #{t['id']}"
204
+ end
205
+ end
206
+ column do
207
+ button "Promote to Blog" do |state|
208
+ # Create blog draft
209
+ title = t['summary'].split(/[.!?]/)[0].strip
210
+ slug = title.downcase.gsub(/[^a-z0-9]+/, '-')[0..50]
211
+
212
+ content = <<~MD
213
+ ---
214
+ title: "#{title}"
215
+ date: #{Date.today}
216
+ status: draft
217
+ source_session: #{t['session_id']}
218
+ themes: #{(t['themes'] || []).join(', ')}
219
+ ---
220
+
221
+ # #{title}
222
+
223
+ ## Summary
224
+
225
+ #{t['summary']}
226
+
227
+ ## Context
228
+
229
+ #{t['context']}
230
+
231
+ ## Key Insights
232
+
233
+ TODO: Extract key insights and expand
234
+
235
+ ---
236
+ *Generated from teachable: #{t['id']}*
237
+ MD
238
+
239
+ blog_dir = File.join(CULTIV_OS, 'blog-drafts')
240
+ FileUtils.mkdir_p(blog_dir)
241
+ filepath = File.join(blog_dir, "#{Date.today}-#{slug}.md")
242
+ File.write(filepath, content)
243
+
244
+ # Also approve it
245
+ move_teachable(t, APPROVED_DIR, 'approved', { promoted_to: 'blog', blog_draft: filepath })
246
+ state[:last_action] = "Promoted to blog: #{filepath}"
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ # Show last action
255
+ if state[:last_action]
256
+ header3 "Last Action"
257
+ text state[:last_action]
258
+ end
259
+ end
260
+
261
+ App.run! if __FILE__ == $0