@agent-native/core-corpus 0.205.0 → 0.205.1-nightly-20261008213122

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 (33) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/changelog/2026-10-08-onboarding-journeys-use-the-current-builder-setup-label.md +6 -0
  3. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -1
  4. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +10 -0
  5. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
  6. package/corpus/templates/analytics/server/lib/journey-steps.ts +1 -1
  7. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +24 -4
  8. package/corpus/templates/clips/changelog/2026-10-08-clips-respects-skip-after-storage-connects-and-gives-accurat.md +6 -0
  9. package/corpus/templates/slides/.agents/skills/apply-slide-comments/SKILL.md +55 -0
  10. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +34 -10
  11. package/corpus/templates/slides/.agents/skills/slide-design/SKILL.md +9 -5
  12. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +40 -15
  13. package/corpus/templates/slides/AGENTS.md +3 -2
  14. package/corpus/templates/slides/agent-native.app-skill.json +5 -0
  15. package/corpus/templates/slides/app/components/editor/AlignmentGuides.tsx +2 -2
  16. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1129 -863
  17. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +2 -4
  18. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +14 -73
  19. package/corpus/templates/slides/app/components/editor/fit-text-object.ts +31 -0
  20. package/corpus/templates/slides/app/components/editor/in-place-text-session.ts +45 -0
  21. package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +56 -0
  22. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +371 -44
  23. package/corpus/templates/slides/app/components/editor/slide-pointer-target.ts +400 -0
  24. package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +61 -12
  25. package/corpus/templates/slides/app/context/DeckContext.tsx +12 -4
  26. package/corpus/templates/slides/app/global.css +95 -24
  27. package/corpus/templates/slides/changelog/2026-10-07-clicking-dragging-and-selecting-in-the-slide-editor-now-work.md +6 -0
  28. package/corpus/templates/slides/changelog/2026-10-07-text-boxes-grow-with-their-text-and-the-selection-outline-fo.md +6 -0
  29. package/corpus/templates/slides/changelog/2026-10-07-the-ai-can-now-plan-slide-height-before-writing-and-apply-co.md +6 -0
  30. package/corpus/templates/slides/interaction-oracle.md +252 -0
  31. package/corpus/templates/slides/interaction-parity.md +47 -22
  32. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  33. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 11555
35
+ - template files: 11564
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-08
4
+ ---
5
+
6
+ Onboarding journeys use the current Builder setup label
@@ -859,7 +859,7 @@
859
859
  "chartType": "table",
860
860
  "source": "first-party",
861
861
  "width": 3,
862
- "sql": "WITH auth_identity_bridge AS (\n SELECT linked_email, MIN(auth_user_id) AS auth_user_id\n FROM (\n SELECT lower(COALESCE(\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END\n )) AS linked_email,\n NULLIF(e.properties::jsonb ->> 'auth_user_id', '') AS auth_user_id\n FROM analytics_events e\n WHERE (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND event_date >= '{{timeRangeStart}}' AND event_date <= '{{timeRangeEnd}}')))\n AND ('{{appFilter}}' IN ('', 'all') OR lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) = lower('{{appFilter}}'))\n AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) IN ('analytics', 'assets', 'brain', 'calendar', 'chat', 'clips', 'content', 'design', 'dispatch', 'forms', 'mail', 'plan', 'slides')\n ) AS identities\n WHERE linked_email IS NOT NULL\n AND auth_user_id IS NOT NULL\n GROUP BY linked_email\n HAVING COUNT(DISTINCT auth_user_id) = 1\n), scoped_onboarding_events AS (\n SELECT e.*,\n COALESCE(\n NULLIF(e.properties::jsonb ->> 'auth_user_id', ''),\n auth_identity_bridge.auth_user_id,\n NULLIF(e.user_key, ''),\n NULLIF(e.user_id, ''),\n NULLIF(e.anonymous_id, '')\n ) AS funnel_user_key,\n COALESCE(\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END,\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.properties::jsonb ->> 'auth_user_id', '') LIKE '%@%.%' THEN e.properties::jsonb ->> 'auth_user_id' END\n ) AS funnel_user_email\n FROM analytics_events e\n LEFT JOIN auth_identity_bridge ON auth_identity_bridge.linked_email = lower(COALESCE(\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END\n ))\n WHERE (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND event_date >= '{{timeRangeStart}}' AND event_date <= '{{timeRangeEnd}}')))\n AND ('{{appFilter}}' IN ('', 'all') OR lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) = lower('{{appFilter}}'))\n AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) IN ('analytics', 'assets', 'brain', 'calendar', 'chat', 'clips', 'content', 'design', 'dispatch', 'forms', 'mail', 'plan', 'slides')\n), onboarding_events AS (\n SELECT * FROM scoped_onboarding_events\n WHERE ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(funnel_user_email, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(funnel_user_email, '')) LIKE '%@builder.io'))\n AND NOT (strpos(lower(trim(coalesce(funnel_user_email, ''))), '@') > 1 AND (strpos(lower(trim(coalesce(funnel_user_email, ''))), '+autoz') > 0 OR strpos(lower(trim(coalesce(funnel_user_email, ''))), '+qa-test-bot-') > 0 OR left(lower(trim(coalesce(funnel_user_email, ''))), 12) = 'qa-test-bot-' OR left(lower(trim(coalesce(funnel_user_email, ''))), 13) = 'an-e2e-probe-' OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.com') OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.net') OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.org') OR right(lower(trim(coalesce(funnel_user_email, ''))), 5) = '@test' OR right(lower(trim(coalesce(funnel_user_email, ''))), 5) = '.test' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '@invalid' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '.invalid' OR right(lower(trim(coalesce(funnel_user_email, ''))), 10) = '@localhost' OR right(lower(trim(coalesce(funnel_user_email, ''))), 10) = '.localhost' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '@example' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '.example'))\n), choice_viewers AS (\n SELECT DISTINCT funnel_user_key\n FROM onboarding_events\n WHERE event_name = 'onboarding_step_viewed'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), choices AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id,\n timestamp::timestamptz AS clicked_at,\n id\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_clicked'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), ranked_choices AS (\n SELECT choices.*,\n ROW_NUMBER() OVER (PARTITION BY funnel_user_key ORDER BY clicked_at, id) AS choice_number\n FROM choices\n JOIN choice_viewers USING (funnel_user_key)\n), first_choice_summary AS (\n SELECT method_id, COUNT(DISTINCT funnel_user_key) AS first_choice_users\n FROM ranked_choices\n WHERE choice_number = 1\n GROUP BY method_id\n), selection_summary AS (\n SELECT method_id,\n COUNT(DISTINCT funnel_user_key) AS selected_users,\n COUNT(*) AS method_clicks,\n COUNT(DISTINCT attempt_id) AS selection_attempts\n FROM choices\n GROUP BY method_id\n), starts AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_started'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), method_outcomes AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id,\n COALESCE(NULLIF(properties::jsonb ->> 'outcome', ''), 'unknown') AS outcome\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_outcome'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), attempts AS (\n SELECT starts.funnel_user_key, starts.method_id, starts.attempt_id,\n MAX(CASE WHEN method_outcomes.outcome IN ('connected', 'already_connected') THEN 1 ELSE 0 END) AS connected,\n MAX(CASE WHEN method_outcomes.outcome = 'failed' THEN 1 ELSE 0 END) AS failed,\n MAX(CASE WHEN method_outcomes.outcome = 'handoff_failed' THEN 1 ELSE 0 END) AS handoff_failed,\n MAX(CASE WHEN method_outcomes.outcome = 'settings_opened' THEN 1 ELSE 0 END) AS settings_opened,\n MAX(CASE WHEN method_outcomes.outcome IS NOT NULL THEN 1 ELSE 0 END) AS has_outcome\n FROM starts\n LEFT JOIN method_outcomes\n ON method_outcomes.funnel_user_key = starts.funnel_user_key\n AND method_outcomes.method_id = starts.method_id\n AND method_outcomes.attempt_id = starts.attempt_id\n GROUP BY starts.funnel_user_key, starts.method_id, starts.attempt_id\n), attempt_summary AS (\n SELECT method_id,\n COUNT(DISTINCT CASE WHEN connected = 1 THEN attempt_id END) AS connection_success_attempts,\n COUNT(DISTINCT CASE WHEN failed = 1 THEN attempt_id END) AS connection_failure_attempts,\n COUNT(DISTINCT CASE WHEN handoff_failed = 1 THEN attempt_id END) AS handoff_failure_attempts,\n COUNT(DISTINCT CASE WHEN connected = 1 THEN funnel_user_key END) AS connection_success_users,\n COUNT(DISTINCT CASE WHEN failed = 1 THEN funnel_user_key END) AS connection_failure_users,\n COUNT(DISTINCT CASE WHEN connected = 0 AND failed = 0 AND has_outcome = 0 THEN attempt_id END) AS connection_no_outcome_attempts,\n COUNT(DISTINCT CASE WHEN settings_opened = 1 THEN attempt_id END) AS settings_handoff_attempts\n FROM attempts\n GROUP BY method_id\n), method_list AS (\n SELECT 'builder_create_account' AS method_id, 'Create Builder.io account' AS method_label\n UNION ALL SELECT 'builder_sign_in', 'Sign in with Builder.io account'\n UNION ALL SELECT 'custom_keys', 'Configure custom keys'\n)\nSELECT method_list.method_id, method_list.method_label,\n (SELECT COUNT(DISTINCT funnel_user_key) FROM choice_viewers) AS choice_screen_viewers,\n COALESCE(first_choice_summary.first_choice_users, 0) AS first_choice_users,\n COALESCE(first_choice_summary.first_choice_users::float / NULLIF((SELECT COUNT(DISTINCT funnel_user_key) FROM choice_viewers), 0), 0) AS first_choice_rate,\n COALESCE(selection_summary.selected_users, 0) AS selected_users,\n COALESCE(selection_summary.method_clicks, 0) AS method_clicks,\n COALESCE(selection_summary.selection_attempts, 0) AS selection_attempts,\n COALESCE(attempt_summary.connection_success_attempts, 0) AS connection_success_attempts,\n COALESCE(attempt_summary.connection_failure_attempts, 0) AS connection_failure_attempts,\n COALESCE(attempt_summary.handoff_failure_attempts, 0) AS handoff_failure_attempts,\n COALESCE(attempt_summary.connection_success_users, 0) AS connection_success_users,\n COALESCE(attempt_summary.connection_failure_users, 0) AS connection_failure_users,\n COALESCE(attempt_summary.connection_no_outcome_attempts, 0) AS connection_no_outcome_attempts,\n COALESCE(attempt_summary.settings_handoff_attempts, 0) AS settings_handoff_attempts,\n attempt_summary.connection_success_attempts::float / NULLIF(attempt_summary.connection_success_attempts + attempt_summary.connection_failure_attempts, 0) AS connection_success_rate\nFROM method_list\nLEFT JOIN first_choice_summary ON first_choice_summary.method_id = method_list.method_id\nLEFT JOIN selection_summary ON selection_summary.method_id = method_list.method_id\nLEFT JOIN attempt_summary ON attempt_summary.method_id = method_list.method_id\nORDER BY method_list.method_id",
862
+ "sql": "WITH auth_identity_bridge AS (\n SELECT linked_email, MIN(auth_user_id) AS auth_user_id\n FROM (\n SELECT lower(COALESCE(\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END\n )) AS linked_email,\n NULLIF(e.properties::jsonb ->> 'auth_user_id', '') AS auth_user_id\n FROM analytics_events e\n WHERE (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND event_date >= '{{timeRangeStart}}' AND event_date <= '{{timeRangeEnd}}')))\n AND ('{{appFilter}}' IN ('', 'all') OR lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) = lower('{{appFilter}}'))\n AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) IN ('analytics', 'assets', 'brain', 'calendar', 'chat', 'clips', 'content', 'design', 'dispatch', 'forms', 'mail', 'plan', 'slides')\n ) AS identities\n WHERE linked_email IS NOT NULL\n AND auth_user_id IS NOT NULL\n GROUP BY linked_email\n HAVING COUNT(DISTINCT auth_user_id) = 1\n), scoped_onboarding_events AS (\n SELECT e.*,\n COALESCE(\n NULLIF(e.properties::jsonb ->> 'auth_user_id', ''),\n auth_identity_bridge.auth_user_id,\n NULLIF(e.user_key, ''),\n NULLIF(e.user_id, ''),\n NULLIF(e.anonymous_id, '')\n ) AS funnel_user_key,\n COALESCE(\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END,\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.properties::jsonb ->> 'auth_user_id', '') LIKE '%@%.%' THEN e.properties::jsonb ->> 'auth_user_id' END\n ) AS funnel_user_email\n FROM analytics_events e\n LEFT JOIN auth_identity_bridge ON auth_identity_bridge.linked_email = lower(COALESCE(\n CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,\n CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END\n ))\n WHERE (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND event_date >= '{{timeRangeStart}}' AND event_date <= '{{timeRangeEnd}}')))\n AND ('{{appFilter}}' IN ('', 'all') OR lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) = lower('{{appFilter}}'))\n AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) IN ('analytics', 'assets', 'brain', 'calendar', 'chat', 'clips', 'content', 'design', 'dispatch', 'forms', 'mail', 'plan', 'slides')\n), onboarding_events AS (\n SELECT * FROM scoped_onboarding_events\n WHERE ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(funnel_user_email, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(funnel_user_email, '')) LIKE '%@builder.io'))\n AND NOT (strpos(lower(trim(coalesce(funnel_user_email, ''))), '@') > 1 AND (strpos(lower(trim(coalesce(funnel_user_email, ''))), '+autoz') > 0 OR strpos(lower(trim(coalesce(funnel_user_email, ''))), '+qa-test-bot-') > 0 OR left(lower(trim(coalesce(funnel_user_email, ''))), 12) = 'qa-test-bot-' OR left(lower(trim(coalesce(funnel_user_email, ''))), 13) = 'an-e2e-probe-' OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.com') OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.net') OR (left(lower(trim(coalesce(funnel_user_email, ''))), 4) = 'e2e-' AND right(lower(trim(coalesce(funnel_user_email, ''))), 12) = '@example.org') OR right(lower(trim(coalesce(funnel_user_email, ''))), 5) = '@test' OR right(lower(trim(coalesce(funnel_user_email, ''))), 5) = '.test' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '@invalid' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '.invalid' OR right(lower(trim(coalesce(funnel_user_email, ''))), 10) = '@localhost' OR right(lower(trim(coalesce(funnel_user_email, ''))), 10) = '.localhost' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '@example' OR right(lower(trim(coalesce(funnel_user_email, ''))), 8) = '.example'))\n), choice_viewers AS (\n SELECT DISTINCT funnel_user_key\n FROM onboarding_events\n WHERE event_name = 'onboarding_step_viewed'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), choices AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id,\n timestamp::timestamptz AS clicked_at,\n id\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_clicked'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), ranked_choices AS (\n SELECT choices.*,\n ROW_NUMBER() OVER (PARTITION BY funnel_user_key ORDER BY clicked_at, id) AS choice_number\n FROM choices\n JOIN choice_viewers USING (funnel_user_key)\n), first_choice_summary AS (\n SELECT method_id, COUNT(DISTINCT funnel_user_key) AS first_choice_users\n FROM ranked_choices\n WHERE choice_number = 1\n GROUP BY method_id\n), selection_summary AS (\n SELECT method_id,\n COUNT(DISTINCT funnel_user_key) AS selected_users,\n COUNT(*) AS method_clicks,\n COUNT(DISTINCT attempt_id) AS selection_attempts\n FROM choices\n GROUP BY method_id\n), starts AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_started'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), method_outcomes AS (\n SELECT funnel_user_key,\n COALESCE(NULLIF(properties::jsonb ->> 'method_id', ''), 'unknown') AS method_id,\n NULLIF(properties::jsonb ->> 'onboarding_attempt_id', '') AS attempt_id,\n COALESCE(NULLIF(properties::jsonb ->> 'outcome', ''), 'unknown') AS outcome\n FROM onboarding_events\n WHERE event_name = 'onboarding_method_outcome'\n AND COALESCE(NULLIF(properties::jsonb ->> 'flow', ''), '') = 'first_run'\n AND COALESCE(NULLIF(properties::jsonb ->> 'step_id', ''), '') = 'choice'\n AND funnel_user_key IS NOT NULL\n), attempts AS (\n SELECT starts.funnel_user_key, starts.method_id, starts.attempt_id,\n MAX(CASE WHEN method_outcomes.outcome IN ('connected', 'already_connected') THEN 1 ELSE 0 END) AS connected,\n MAX(CASE WHEN method_outcomes.outcome = 'failed' THEN 1 ELSE 0 END) AS failed,\n MAX(CASE WHEN method_outcomes.outcome = 'handoff_failed' THEN 1 ELSE 0 END) AS handoff_failed,\n MAX(CASE WHEN method_outcomes.outcome = 'settings_opened' THEN 1 ELSE 0 END) AS settings_opened,\n MAX(CASE WHEN method_outcomes.outcome IS NOT NULL THEN 1 ELSE 0 END) AS has_outcome\n FROM starts\n LEFT JOIN method_outcomes\n ON method_outcomes.funnel_user_key = starts.funnel_user_key\n AND method_outcomes.method_id = starts.method_id\n AND method_outcomes.attempt_id = starts.attempt_id\n GROUP BY starts.funnel_user_key, starts.method_id, starts.attempt_id\n), attempt_summary AS (\n SELECT method_id,\n COUNT(DISTINCT CASE WHEN connected = 1 THEN attempt_id END) AS connection_success_attempts,\n COUNT(DISTINCT CASE WHEN failed = 1 THEN attempt_id END) AS connection_failure_attempts,\n COUNT(DISTINCT CASE WHEN handoff_failed = 1 THEN attempt_id END) AS handoff_failure_attempts,\n COUNT(DISTINCT CASE WHEN connected = 1 THEN funnel_user_key END) AS connection_success_users,\n COUNT(DISTINCT CASE WHEN failed = 1 THEN funnel_user_key END) AS connection_failure_users,\n COUNT(DISTINCT CASE WHEN connected = 0 AND failed = 0 AND has_outcome = 0 THEN attempt_id END) AS connection_no_outcome_attempts,\n COUNT(DISTINCT CASE WHEN settings_opened = 1 THEN attempt_id END) AS settings_handoff_attempts\n FROM attempts\n GROUP BY method_id\n), method_list AS (\n SELECT 'builder_create_account' AS method_id, 'Use Builder.io' AS method_label\n UNION ALL SELECT 'builder_sign_in', 'Sign in with Builder.io account'\n UNION ALL SELECT 'custom_keys', 'Configure custom keys'\n)\nSELECT method_list.method_id, method_list.method_label,\n (SELECT COUNT(DISTINCT funnel_user_key) FROM choice_viewers) AS choice_screen_viewers,\n COALESCE(first_choice_summary.first_choice_users, 0) AS first_choice_users,\n COALESCE(first_choice_summary.first_choice_users::float / NULLIF((SELECT COUNT(DISTINCT funnel_user_key) FROM choice_viewers), 0), 0) AS first_choice_rate,\n COALESCE(selection_summary.selected_users, 0) AS selected_users,\n COALESCE(selection_summary.method_clicks, 0) AS method_clicks,\n COALESCE(selection_summary.selection_attempts, 0) AS selection_attempts,\n COALESCE(attempt_summary.connection_success_attempts, 0) AS connection_success_attempts,\n COALESCE(attempt_summary.connection_failure_attempts, 0) AS connection_failure_attempts,\n COALESCE(attempt_summary.handoff_failure_attempts, 0) AS handoff_failure_attempts,\n COALESCE(attempt_summary.connection_success_users, 0) AS connection_success_users,\n COALESCE(attempt_summary.connection_failure_users, 0) AS connection_failure_users,\n COALESCE(attempt_summary.connection_no_outcome_attempts, 0) AS connection_no_outcome_attempts,\n COALESCE(attempt_summary.settings_handoff_attempts, 0) AS settings_handoff_attempts,\n attempt_summary.connection_success_attempts::float / NULLIF(attempt_summary.connection_success_attempts + attempt_summary.connection_failure_attempts, 0) AS connection_success_rate\nFROM method_list\nLEFT JOIN first_choice_summary ON first_choice_summary.method_id = method_list.method_id\nLEFT JOIN selection_summary ON selection_summary.method_id = method_list.method_id\nLEFT JOIN attempt_summary ON attempt_summary.method_id = method_list.method_id\nORDER BY method_list.method_id",
863
863
  "config": {
864
864
  "description": "Choice-screen viewers, first selected setup path, repeat selections, and linked Builder connection outcomes. Rates use choice-screen viewers or resolved Builder outcomes as their denominator; started attempts with no outcome are unknown or abandoned, not assumed failures. Handoff failures are reported separately from Builder connection failures. A successful Builder outcome confirms credentials connected, not that a new external account was created. Account-exists is counted as a failed create-account attempt. Test identities are excluded.",
865
865
  "columns": [
@@ -634,6 +634,16 @@ function repairFingerprintedPanelQueries(
634
634
 
635
635
  const CANONICAL_CUSTOM_PANEL_REPLACEMENTS: readonly ExactFirstPartyPanelReplacement[] =
636
636
  [
637
+ {
638
+ id: "onboarding-setup-choice",
639
+ legacySql: [
640
+ buildPanel("onboarding-setup-choice")!.sql.replace(
641
+ "'Use Builder.io' AS method_label",
642
+ "'Create Builder.io account' AS method_label",
643
+ ),
644
+ ],
645
+ sql: buildPanel("onboarding-setup-choice")!.sql,
646
+ },
637
647
  {
638
648
  id: "signups-over-time",
639
649
  legacySql: [
@@ -1309,7 +1309,7 @@ const ONBOARDING_SETUP_CHOICE_SQL = `${ONBOARDING_EVENTS_CTE}, choice_viewers AS
1309
1309
  FROM attempts
1310
1310
  GROUP BY method_id
1311
1311
  ), method_list AS (
1312
- SELECT 'builder_create_account' AS method_id, 'Create Builder.io account' AS method_label
1312
+ SELECT 'builder_create_account' AS method_id, 'Use Builder.io' AS method_label
1313
1313
  UNION ALL SELECT 'builder_sign_in', 'Sign in with Builder.io account'
1314
1314
  UNION ALL SELECT 'custom_keys', 'Configure custom keys'
1315
1315
  )
@@ -29,7 +29,7 @@ export interface JourneyStep {
29
29
 
30
30
  /** Display names match the `onboarding-setup-choice` metric's method list. */
31
31
  const METHOD_LABELS: Record<string, string> = {
32
- builder_create_account: "Create Builder.io account",
32
+ builder_create_account: "Use Builder.io",
33
33
  builder_sign_in: "Sign in with Builder.io account",
34
34
  custom_keys: "Configure custom keys",
35
35
  };
@@ -62,12 +62,22 @@ export function StorageSetupCard({
62
62
  const [retryingBuilderStatus, setRetryingBuilderStatus] = useState(false);
63
63
  const retryingBuilderStatusAtCountRef = useRef<number | null>(null);
64
64
  const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
65
+ const configuredTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
66
+ null,
67
+ );
65
68
  const mountedRef = useRef(true);
66
69
  const inFlightRef = useRef(false);
67
70
  const visibilityHandlerRef = useRef<(() => void) | null>(null);
68
71
  const connectRequestedRef = useRef(false);
69
72
  const connectIntentExpiresAtRef = useRef<number | null>(null);
70
73
 
74
+ const clearConfiguredTimeout = useCallback(() => {
75
+ if (configuredTimeoutRef.current !== null) {
76
+ clearTimeout(configuredTimeoutRef.current);
77
+ configuredTimeoutRef.current = null;
78
+ }
79
+ }, []);
80
+
71
81
  const stopVisibilityHandler = useCallback(() => {
72
82
  if (visibilityHandlerRef.current) {
73
83
  document.removeEventListener(
@@ -79,6 +89,7 @@ export function StorageSetupCard({
79
89
  }, []);
80
90
 
81
91
  const startFileUploadPoll = useCallback(() => {
92
+ clearConfiguredTimeout();
82
93
  if (pollRef.current) {
83
94
  clearInterval(pollRef.current);
84
95
  pollRef.current = null;
@@ -135,7 +146,11 @@ export function StorageSetupCard({
135
146
  stop();
136
147
  setConnecting(false);
137
148
  setConnected(true);
138
- setTimeout(() => void onConfigured(), 800);
149
+ clearConfiguredTimeout();
150
+ configuredTimeoutRef.current = setTimeout(() => {
151
+ configuredTimeoutRef.current = null;
152
+ if (mountedRef.current) void onConfigured();
153
+ }, 800);
139
154
  } else if (status.state === "unavailable") {
140
155
  stop();
141
156
  setConnecting(false);
@@ -162,7 +177,7 @@ export function StorageSetupCard({
162
177
  if (!document.hidden) void tick();
163
178
  };
164
179
  document.addEventListener("visibilitychange", visibilityHandlerRef.current);
165
- }, [onConfigured, stopVisibilityHandler, t]);
180
+ }, [clearConfiguredTimeout, onConfigured, stopVisibilityHandler, t]);
166
181
 
167
182
  const handleBuilderConnected = useCallback(() => {
168
183
  if (!connectRequestedRef.current) return;
@@ -232,6 +247,10 @@ export function StorageSetupCard({
232
247
  Date.now() + CANCELLED_SETUP_RECOVERY_MS;
233
248
  builderConnect.cancel();
234
249
  }, [builderConnect.cancel]);
250
+ const handleSkip = useCallback(() => {
251
+ clearConfiguredTimeout();
252
+ onSkip?.();
253
+ }, [clearConfiguredTimeout, onSkip]);
235
254
  const builderConnectErrorMessage = builderConnect.error
236
255
  ? builderConnect.errorKind === "launch"
237
256
  ? t("storageSetup.builderConnectPopupError")
@@ -250,13 +269,14 @@ export function StorageSetupCard({
250
269
  mountedRef.current = true;
251
270
  return () => {
252
271
  mountedRef.current = false;
272
+ clearConfiguredTimeout();
253
273
  if (pollRef.current) {
254
274
  clearInterval(pollRef.current);
255
275
  pollRef.current = null;
256
276
  }
257
277
  stopVisibilityHandler();
258
278
  };
259
- }, [stopVisibilityHandler]);
279
+ }, [clearConfiguredTimeout, stopVisibilityHandler]);
260
280
 
261
281
  return (
262
282
  <div className="mx-auto flex w-full max-w-md flex-col gap-5 rounded-2xl border border-border bg-card p-6 shadow-lg">
@@ -379,7 +399,7 @@ export function StorageSetupCard({
379
399
  variant="ghost"
380
400
  size="sm"
381
401
  className="self-center text-muted-foreground"
382
- onClick={onSkip}
402
+ onClick={handleSkip}
383
403
  >
384
404
  {t("agentChat.onboarding.skipForNow")}
385
405
  </Button>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-08
4
+ ---
5
+
6
+ Clips respects Skip after storage connects and gives accurate guidance when Builder does not respond
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: apply-slide-comments
3
+ description: >-
4
+ Turn open slide comments into edits and resolve them. Use when the user asks
5
+ to apply, address, resolve, or work through comments, review notes, or
6
+ feedback left on a deck or slide.
7
+ ---
8
+
9
+ # Apply Slide Comments
10
+
11
+ Comments are instructions anchored to a slide. Edits follow `slide-editing`;
12
+ this skill is the loop around them.
13
+
14
+ ## Procedure
15
+
16
+ 1. **Collect.** `list-slide-comments` with `deckId` (add `slideId` only when the
17
+ user scoped the request; page with `next_cursor` while `has_more`). Keep
18
+ threads with `resolved: false`. A thread is the root comment plus replies
19
+ (`thread_id`, `parent_id`); read the whole thread, and treat the latest
20
+ instruction as current.
21
+ 2. **Group by slide** and note each comment's anchor: `anchor.objectId` is the
22
+ `data-slide-object-id` element, `quoted_text` / `anchor.targetText` and the
23
+ text offsets locate the words, and an anchor with only `x` / `y` is a
24
+ position on the slide.
25
+ 3. **Read each affected slide** once: `view-screen` for the open slide,
26
+ `get-deck` with `slideIds` and `compact=false` for the rest. Interpret the
27
+ comment against its anchor object, not the whole slide.
28
+ 4. **Apply every edit for one slide in a single `patch-deck` `patch-slide`
29
+ operation** with that slide's `baseContentHash`. Preserve ids and
30
+ `left`/`top`/`width`, and change only what the comment asks for.
31
+ 5. **Verify.** When text length changed, call `get-layout-overflows` once after
32
+ all slides are written, and `audit-contrast` last (see `slide-editing`). Read
33
+ the persisted slide back before resolving anything.
34
+ 6. **Resolve** each applied thread: `update-slide-comment` with `id`, `deckId`,
35
+ `resolved: true` (resolving applies to the full thread). Never delete a
36
+ user's comment.
37
+ 7. **Leave open** any comment you skipped, that is ambiguous, or that asks for
38
+ something outside the deck (new data, an image only the user has). Say why
39
+ in the summary; reply with `add-slide-comment` (`threadId` + `parentId`)
40
+ only if the user asked you to.
41
+
42
+ ## Judgment
43
+
44
+ - Ambiguous but low-risk: apply the smallest reasonable reading and state the
45
+ assumption. Ambiguous and costly to undo (cuts content, changes numbers,
46
+ conflicts with another comment): skip it.
47
+ - Conflicting comments on one element: apply neither, name both.
48
+ - A comment about a fact needs the source; follow `creative-context`, do not
49
+ guess.
50
+ - Leave no TODO or "per comment" text in slide HTML.
51
+
52
+ ## Summary
53
+
54
+ Finish with one line, `N applied, M skipped`, then one line per change
55
+ (slide number, what changed) and one per skipped comment with its reason.
@@ -36,6 +36,8 @@ Context, reference deck, or source material that the app already provides.
36
36
 
37
37
  When speaker notes are requested, put presenter-only text in each slide's
38
38
  `notes` field on `create-deck` or `add-slide`; keep it out of the slide HTML.
39
+ Write notes as speech, not a summary of the slide: the opener, the transition
40
+ to the next slide, numbers to cite, a timing cue.
39
41
  Preserve existing notes when editing or importing a source deck.
40
42
 
41
43
  When the UI has already created the empty deck, keep its id and rename it before
@@ -62,7 +64,12 @@ and slide chrome. Read `slide-design` for visual craft; it works inside the
62
64
  active system and reference deck, never as a competing theme. If the
63
65
  request is open-ended and no approved direction exists, ask one targeted guided
64
66
  question or present a bounded choice before writing; do not silently pick a new
65
- brand language.
67
+ brand language. Write a direction choice for this topic: three options, each a
68
+ vibe word plus one concrete visual cue (palette, type, motif; for example "rust
69
+ technical editorial: warm rust on charcoal, mono headings, code-grid layout"),
70
+ clearly different from each other, the best fit marked recommended. Never offer
71
+ generic labels such as "minimal" or "corporate" alone. Take everything else
72
+ (density, slide count, structure) from the request and do the work.
66
73
 
67
74
  Before the first slide, lock a deck-level visual contract: background family,
68
75
  text and surface roles, accent treatment, heading/body type pairing, spacing
@@ -232,13 +239,24 @@ component language as a substitute for choosing.
232
239
 
233
240
  The canvas is fixed at its aspect-ratio dimensions. With the standard 16:9
234
241
  canvas (960x540) and `padding: 64px 80px`, the usable content area is only
235
- 800x412px. Treat that as a hard budget for the main flow: use at most two title
236
- lines, three short bullets or cards, and two or three short items per column.
237
- Split dense source material across slides instead of shrinking it into a dense
238
- stack. Keep body text at or above 16px. Never hide overflow with zoom,
239
- `transform: scale()`, clipping, or scroll overflow. A later structural repair
240
- may reduce the slide's explicit padding, and that padding must remain intact
241
- when the saved HTML is rendered.
242
+ 800x412px (usable height is canvas height minus both vertical paddings). Treat
243
+ that as a hard budget for the main flow: use at most two title lines, three
244
+ short bullets or cards, and two or three short items per column. A content slide
245
+ carries about 40 words besides its title. A bullet fits on one line (about 70
246
+ characters at 18px); only a card may wrap, to two lines. Split dense source
247
+ material across slides instead of shrinking it into a dense stack. Keep body
248
+ text at or above 16px. Never hide overflow with zoom, `transform: scale()`,
249
+ clipping, or scroll overflow. A later structural repair may reduce the slide's
250
+ explicit padding, and that padding must remain intact when the saved HTML is
251
+ rendered.
252
+
253
+ Before writing a slide, total its flow height: per text block, font-size x
254
+ line-height x wrapped lines (average glyph width is about 0.5 x font-size), plus
255
+ padding and gaps. Count every wrapped line and keep about 10% slack for the
256
+ measured check. Example: a two-line 34px/1.12 heading (76) + 18 gap + three
257
+ cards of two 18px/1.4 lines plus 24 padding (3 x 74 = 223) + two 14px gaps (28)
258
+ = 345, inside 412; a fourth card adds 88 and does not fit. When the total
259
+ exceeds the budget, split the slide; do not shrink type, padding, or gaps.
242
260
 
243
261
  Build an intentional composition beyond a text dump: use a title block,
244
262
  two-column split, metric treatment, rule, callout, visual placeholder, or
@@ -249,6 +267,9 @@ with nothing real to show is complete with type, spacing, and a rule. A
249
267
  built-in template's signature art belongs to that template; do not carry it
250
268
  into other decks.
251
269
 
270
+ Author in normal flex/grid flow; the card, wrapper, height, and `contain` rules
271
+ are in `slide-editing` (Flow Layout and the Editor).
272
+
252
273
  ## Bounded visual QA
253
274
 
254
275
  Before calling the deck complete, render every changed slide at its canonical
@@ -306,8 +327,11 @@ short rule, or callout should support the message, not fill empty space.
306
327
 
307
328
  ## Image Placeholders
308
329
 
309
- When a slide needs a visual, use this div. It renders as a styled placeholder
310
- and can later be replaced with a generated image:
330
+ When a slide needs a specific visual, use this div. It renders as a styled
331
+ placeholder and can later be replaced with a generated image. Describe the
332
+ content the image must show ("Q3 revenue by region, bar chart"), not its role
333
+ ("hero image"). If type, layout, and color can carry the slide, use no
334
+ placeholder:
311
335
 
312
336
  ```html
313
337
  <div class="fmd-img-placeholder" style="width: 100%; min-height: 220px; border-radius: var(--deck-radius);">[Description of what image should show]</div>
@@ -90,8 +90,11 @@ Type does most of the work.
90
90
  for related, far for unrelated; equal gaps everywhere read as amateur.
91
91
  - **Optical alignment:** nudge large type to look flush with small text.
92
92
  - **One focal point.** Squint; one thing should land first.
93
- - Keep generated layout in normal flex/grid flow and never clip or scale
94
- overflow; `slide-editing` owns those constraints.
93
+ - **One idea per slide; split, do not shrink.** Total the flow height and word
94
+ count before writing (Fit budget in `create-deck`); a slide that does not fit
95
+ becomes two slides, not smaller type, tighter padding, or clipped overflow.
96
+ - Flow, card, and fixed-height constraints: `slide-editing` (Flow Layout and the
97
+ Editor).
95
98
 
96
99
  ## Color
97
100
 
@@ -117,7 +120,7 @@ With a system or reference, use only its colors; these rules govern how.
117
120
  - **Screenshots as hero shots:** crop to the region the headline is about,
118
121
  scale it up, set it on a surface that fits the direction.
119
122
  - **One graphic vocabulary:** 1-2px rules, large index numbers ("01"), one
120
- repeated shape motif. Every shape needs a semantic role; no inline SVG.
123
+ repeated shape motif. Every shape needs a semantic role.
121
124
  - **Charts in the deck's type and palette:** thin strokes, direct labels, the
122
125
  key series in the accent, the rest in a tinted neutral, no gridline or
123
126
  legend clutter.
@@ -128,8 +131,9 @@ With a system or reference, use only its colors; these rules govern how.
128
131
  headline + image, split, grid of 3, quote, closing. Alternate dense and
129
132
  sparse; avoid three identical layouts in a row.
130
133
  - Fixed chrome in the exact same place on every slide: a small running head,
131
- a page number like `04 / 12`, maybe a hairline. Quiet repetition is what
132
- makes a deck feel designed.
134
+ maybe a hairline. Quiet repetition is what makes a deck feel designed. Do not
135
+ type page numbers such as `04 / 12`: inserting or reordering slides makes
136
+ them wrong everywhere, and the renderer provides no page-number token.
133
137
  - Title and section slides carry the strongest expression of the direction.
134
138
 
135
139
  ## What reads as generic
@@ -85,14 +85,10 @@ panels, fake logos, and filler bullets.
85
85
 
86
86
  ## Fit and Density
87
87
 
88
- Fit the main content to the native content area, not merely to the outer
89
- wrapper. For the default 16:9 canvas, the standard `64px 80px` padding leaves
90
- 800x412px. Keep titles to two lines, content slides to three short bullets or
91
- three compact cards, and two-column slides to two or three short items per
92
- column. If the source is denser, split it across slides. Never use zoom,
93
- `transform: scale()`, clipping, or scroll overflow to hide a fit issue; body
94
- text must remain at least 16px. Explicitly reduced slide padding is allowed when
95
- the content still needs the space.
88
+ Edits obey the Fit budget in `create-deck`, measured against the slide's own
89
+ padding and aspect ratio. When an edit adds or lengthens text, redo the height
90
+ arithmetic for that slide and split it instead of shrinking. Explicitly reduced
91
+ slide padding is allowed when the content still needs the space.
96
92
 
97
93
  After all deck edits, call `get-layout-overflows` once. If you repair a
98
94
  measured overflow, call it once more; do not check between writes. If status is
@@ -139,6 +135,12 @@ To edit a slide's content:
139
135
  For a targeted persisted read, pass that stable `slideId` to `get-deck` so
140
136
  only the target slide is returned; use `compact=false` when you need its
141
137
  full HTML.
138
+ The user navigates and reselects between turns. On any turn that says
139
+ this, here, that slide, these, or the selected one (including a follow-up
140
+ like "now make it bigger"), call `view-screen` again before acting; never
141
+ reuse the previous turn's slide ID or selection. If it shows nothing
142
+ selected or the target is unclear, ask which slide rather than guessing from
143
+ recent edits.
142
144
  2. **Retrieve before generating**: when the edit changes facts, brand language,
143
145
  or layout, follow the `creative-context` skill and query those roles
144
146
  separately. Respect opt-out, pinned packs, and the exact reuse ladder.
@@ -259,6 +261,10 @@ same operations.
259
261
  ## Click-to-reveal animations
260
262
 
261
263
  Animations are metadata over the final slide HTML, not alternate slide markup.
264
+ Reveal only where the order of ideas carries the argument (a list whose payoff
265
+ is the last item, a build to a conclusion, before/after); a title, single
266
+ quote, or diagram the audience should take in at a glance is stronger shown
267
+ whole. Do not add reveals by default.
262
268
  Read the full target slide, keep its existing visual structure, and patch the
263
269
  complete ordered `animations` list with `elementPath` values from that exact
264
270
  HTML. Elements omitted from the list remain visible immediately, so labels and
@@ -300,23 +306,42 @@ children of `.fmd-slide`. Give each one a stable `data-slide-object-id`:
300
306
  </div>
301
307
  ```
302
308
 
303
- - Preserve `data-slide-object-id` when updating, moving, resizing, or styling an
304
- existing object.
309
+ - Preserve `data-slide-object-id` and the `left`/`top`/`width` of hand-placed
310
+ objects when updating, moving, resizing, or styling them.
305
311
  - Mint a new unique object ID when duplicating an object.
306
312
  - Do not use runtime-only `data-builder-id` values in saved slide HTML.
307
- - Keep generated flex and grid content in normal flow. Do not silently
308
- absolute-position a nested layout child just to make it draggable; create a
309
- deliberate freeform object instead.
313
+ - A text box (`.fmd-text-box`) with no inline `height` auto-grows with its text
314
+ from the top edge.
310
315
  - Build editable shapes with styled HTML elements such as `div`. Do not use
311
316
  inline SVG, which the slide sanitizer removes.
312
317
 
318
+ ## Flow Layout and the Editor
319
+
320
+ Keep generated flex and grid content in normal flow. Do not silently
321
+ absolute-position a nested layout child just to make it draggable; create a
322
+ deliberate freeform object instead. The editor presents flow content as flat
323
+ objects the way Google Slides does, so write markup that maps cleanly:
324
+
325
+ - A card is one painted box (background, border, or shadow on a single element)
326
+ that owns its text. Never stack separately positioned text over a card
327
+ background.
328
+ - Text containers, including `.fmd-text-box`, have no fixed `height`
329
+ (`min-height` only for a deliberate minimum), so text grows instead of
330
+ overflowing. Never write `contain` or `contain-intrinsic-size`.
331
+ - Keep nesting shallow. Unpainted wrappers with no direct text (grid rows,
332
+ columns) are fine; the pointer skips them.
333
+ - Ids belong to freeform objects only. Never stamp `data-slide-object-id` on a
334
+ flow region: any id marks an object freeform and `export-pptx` rejects it.
335
+ Preserve existing ids when rewriting a slide.
336
+
313
337
  ## Image Placeholders
314
338
 
315
- For visual elements (diagrams, charts, photos), use placeholder divs:
339
+ For visual elements (diagrams, charts, photos), use placeholder divs whose text
340
+ names the content to show, not its role (see `create-deck` Image Placeholders):
316
341
 
317
342
  ```html
318
343
  <div class="fmd-img-placeholder" style="width: 100%; height: 300px; border-radius: 12px;">
319
- Description of the image
344
+ Q3 revenue by region, bar chart
320
345
  </div>
321
346
  ```
322
347
 
@@ -13,6 +13,7 @@ Read relevant guides before deeper work:
13
13
  - `.agents/skills/image-generation-via-a2a/SKILL.md` — for A2A image generation.
14
14
  - `.agents/skills/design-systems/SKILL.md` — for deck design systems.
15
15
  - `.agents/skills/slide-design/SKILL.md` — for visual-craft requests.
16
+ - `.agents/skills/apply-slide-comments/SKILL.md` — for applying/resolving comments.
16
17
  - `.agents/skills/creative-context/SKILL.md` — for source reuse and context.
17
18
  - `.agents/skills/analytics-data-for-decks/SKILL.md` — for delegated data requests.
18
19
 
@@ -45,8 +46,8 @@ Use local framework docs, not web research: `pnpm action docs-search --query "<t
45
46
  - UI feedback: target 100 ms, never exceed 400 ms; acknowledge before network work.
46
47
  - Use actions for deck/slide writes; keep large files in configured file storage, not SQL/settings/resources. Never hardcode secrets or private/customer data; use obvious placeholders.
47
48
  - For external integrations, inspect the workspace/provider connection catalog first; reuse its scoped resolver.
48
- - Use `view-screen` when the active deck/slide/layout is unclear. Preserve requested structure; do not restyle imports marked partial or with `imagesSkipped` without reporting the warning.
49
- - Preserve freeform objects and their ids. Keep generated flex/grid in normal flow and use styled HTML, not inline SVG; see `slide-editing`.
49
+ - Use `view-screen` when the active deck/slide/layout is unclear, and on every turn that says "this"/"here"/"selected". Preserve requested structure; do not restyle imports marked partial or with `imagesSkipped` without reporting the warning.
50
+ - Slide HTML layout, card, no-SVG, and freeform-id rules: `slide-editing`.
50
51
  - Import attachments only on request or via Import. `sourceImport` records provenance; structural edits clear it. Read preloaded new-deck attachments before deciding whether to import.
51
52
  - Provider API actions are shortcuts, not limits. For exact Drive API needs use `provider-api-catalog` / `-docs` / `-request` with the user's Google Docs OAuth. Preserve imported PPTX timing metadata.
52
53
  - For data requests, follow `analytics-data-for-decks` and delegate via Analytics/A2A; never query SQL or providers directly. Without a reference deck or design system, read `get-workspace-defaults` before generation; use `creative-context` for source order and context submission.
@@ -42,6 +42,11 @@
42
42
  "visibility": "both",
43
43
  "exportAs": "slide-editing"
44
44
  },
45
+ {
46
+ "path": ".agents/skills/apply-slide-comments",
47
+ "visibility": "both",
48
+ "exportAs": "apply-slide-comments"
49
+ },
45
50
  {
46
51
  "path": ".agents/skills/deck-management",
47
52
  "visibility": "internal"
@@ -32,8 +32,8 @@ export function AlignmentGuides({
32
32
  position: "fixed",
33
33
  pointerEvents: "none",
34
34
  zIndex: 70,
35
- backgroundColor: "hsl(var(--destructive))",
36
- boxShadow: "0 0 0 1px hsl(var(--destructive) / 0.2)",
35
+ // guard:allow-raw-color — Google Slides guides are literal #ff0000, theme-independent (interaction-oracle.md 9.2)
36
+ backgroundColor: "#ff0000",
37
37
  };
38
38
 
39
39
  return createPortal(