@agent-native/core-corpus 0.203.2-nightly-20261007213109 → 0.204.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 (127) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +1 -0
  3. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  4. package/corpus/templates/analytics/app/i18n/en-US.ts +7 -0
  5. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +116 -40
  6. package/corpus/templates/analytics/app/pages/sessions/session-replay-screenshot.ts +214 -22
  7. package/corpus/templates/analytics/changelog/2026-10-07-session-replay-screenshot-export-captures-readable-video-fra.md +6 -0
  8. package/corpus/templates/analytics/changelog/2026-10-07-session-replay-screenshots-can-be-copied-directly-into-desig.md +6 -0
  9. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +1 -0
  10. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +1 -0
  11. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +1 -0
  12. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
  13. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +1 -0
  14. package/corpus/templates/clips/CHANGELOG.md +10 -0
  15. package/corpus/templates/clips/app/i18n/en-US.ts +1 -1
  16. package/corpus/templates/clips/changelog/2026-10-07-builder-io-setup-choices-now-say-use-builder-io.md +6 -0
  17. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +1 -0
  18. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +13 -7
  19. package/corpus/templates/content/CHANGELOG.md +9 -0
  20. package/corpus/templates/content/actions/_database-setup-mutation.ts +3 -7
  21. package/corpus/templates/content/actions/_document-lifecycle.ts +250 -0
  22. package/corpus/templates/content/actions/delete-content-database.ts +6 -0
  23. package/corpus/templates/content/actions/delete-document.ts +169 -45
  24. package/corpus/templates/content/actions/list-content-trash.ts +2 -1
  25. package/corpus/templates/content/actions/resolve-content-landing.ts +14 -3
  26. package/corpus/templates/content/actions/restore-content-database.ts +20 -4
  27. package/corpus/templates/content/actions/restore-document.ts +121 -6
  28. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +74 -5
  29. package/corpus/templates/content/app/lib/content-landing.ts +67 -0
  30. package/corpus/templates/content/app/root.tsx +7 -1
  31. package/corpus/templates/content/app/routes/_app.home.tsx +65 -24
  32. package/corpus/templates/content/changelog/2026-10-06-connecting-notion-no-longer-leaves-its-used-authorization-co.md +6 -0
  33. package/corpus/templates/content/changelog/2026-10-06-external-agents-can-now-move-a-page-and-its-sub-pages-to-rec.md +6 -0
  34. package/corpus/templates/content/changelog/2026-10-06-opening-content-from-home-shows-your-last-page-sooner.md +6 -0
  35. package/corpus/templates/content/changelog/2026-10-07-text-two-people-type-at-the-same-spot-now-reaches-the-saved-.md +6 -0
  36. package/corpus/templates/content/docs/product/capabilities/content.agent.action-parity.md +5 -2
  37. package/corpus/templates/content/e2e/helpers.ts +4 -3
  38. package/corpus/templates/content/parity/matrix.md +2 -2
  39. package/corpus/templates/content/parity/matrix.ts +2 -0
  40. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -1
  41. package/corpus/templates/content/server/routes/api/notion/callback.get.ts +14 -2
  42. package/corpus/templates/content/shared/content-landing.ts +2 -0
  43. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +1 -0
  44. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +1 -0
  45. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +24 -6
  46. package/corpus/templates/design/CHANGELOG.md +7 -0
  47. package/corpus/templates/design/app/components/design/KeyboardShortcutsDialog.tsx +480 -0
  48. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +26 -3
  49. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +1 -4
  50. package/corpus/templates/design/app/components/design/keyboard-shortcut-sections.ts +114 -0
  51. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +1 -1
  52. package/corpus/templates/design/app/i18n/en-US.ts +5 -6
  53. package/corpus/templates/design/app/lib/design-sync-invalidation.ts +86 -0
  54. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +40 -11
  55. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +2 -1
  56. package/corpus/templates/design/app/pages/design-editor/position-coordinate-context.ts +21 -8
  57. package/corpus/templates/design/app/root.tsx +2 -0
  58. package/corpus/templates/design/changelog/2026-10-07-canvas-layers-now-stay-positioned-and-selected-correctly-whe.md +6 -0
  59. package/corpus/templates/design/changelog/2026-10-07-collaborator-edits-refresh-fewer-unrelated-panels.md +6 -0
  60. package/corpus/templates/design/changelog/2026-10-07-fixed-position-readouts-inside-transformed-containers-and-ke.md +6 -0
  61. package/corpus/templates/design/changelog/2026-10-07-keyboard-shortcuts-now-open-in-a-dialog-with-search-and-a-ca.md +6 -0
  62. package/corpus/templates/design/e2e/helpers.ts +5 -3
  63. package/corpus/templates/design/vite.config.ts +50 -0
  64. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +1 -0
  65. package/corpus/templates/dispatch/CHANGELOG.md +1 -0
  66. package/corpus/templates/dispatch/changelog/2026-10-06-signing-in-to-another-app-through-dispatch-no-longer-leaves-.md +6 -0
  67. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +9 -2
  68. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +1 -0
  69. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
  70. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
  71. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +1 -0
  72. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +1 -0
  73. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +1 -0
  74. package/corpus/templates/plan/actions/seed-plan-collab.ts +4 -3
  75. package/corpus/templates/plan/app/components/plan/PlanContentRenderer.tsx +8 -14
  76. package/corpus/templates/plan/app/components/plan/PlanMarkdownEditor.tsx +56 -0
  77. package/corpus/templates/plan/shared/plan-autosave-retry.ts +20 -0
  78. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +1 -0
  79. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +24 -0
  80. package/corpus/templates/slides/AGENTS.md +1 -1
  81. package/corpus/templates/slides/CHANGELOG.md +24 -0
  82. package/corpus/templates/slides/actions/edit-image.ts +1 -18
  83. package/corpus/templates/slides/actions/export-html.ts +29 -16
  84. package/corpus/templates/slides/actions/generate-image.ts +1 -18
  85. package/corpus/templates/slides/actions/view-screen.ts +8 -0
  86. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +18 -2
  87. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +20 -2
  88. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +56 -0
  89. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +69 -14
  90. package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +4 -0
  91. package/corpus/templates/slides/app/components/editor/bullet-editing.ts +1 -1
  92. package/corpus/templates/slides/app/components/editor/in-place-text-session.ts +226 -11
  93. package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +32 -21
  94. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +10 -1
  95. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +2 -0
  96. package/corpus/templates/slides/app/components/visual-editor/SaveStatusIndicator.tsx +20 -7
  97. package/corpus/templates/slides/app/context/DeckContext.tsx +96 -4
  98. package/corpus/templates/slides/app/hooks/use-deck-access-reload.ts +5 -1
  99. package/corpus/templates/slides/app/i18n/en-US.ts +13 -0
  100. package/corpus/templates/slides/app/lib/deck-access-lost.ts +4 -0
  101. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +229 -62
  102. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +30 -0
  103. package/corpus/templates/slides/app/lib/slide-source-map.ts +13 -1
  104. package/corpus/templates/slides/app/lib/slide-video.ts +163 -0
  105. package/corpus/templates/slides/app/lib/video-upload.ts +40 -0
  106. package/corpus/templates/slides/app/pages/DeckEditor.tsx +82 -0
  107. package/corpus/templates/slides/changelog/2026-10-06-deleting-between-slide-paragraphs-now-preserves-inline-forma.md +6 -0
  108. package/corpus/templates/slides/changelog/2026-10-07-a-deck-whose-access-is-revoked-while-it-is-open-now-says-acc.md +6 -0
  109. package/corpus/templates/slides/changelog/2026-10-07-forward-delete-keeps-the-caret-on-the-correct-line-when-join.md +6 -0
  110. package/corpus/templates/slides/changelog/2026-10-07-long-slide-text-editing-sessions-can-be-undone-without-losin.md +6 -0
  111. package/corpus/templates/slides/changelog/2026-10-07-markdown-divider-shortcuts-and-autocorrect-replacements-rema.md +6 -0
  112. package/corpus/templates/slides/changelog/2026-10-07-pasted-images-stay-inside-their-crop-frame-while-uploads-fin.md +6 -0
  113. package/corpus/templates/slides/changelog/2026-10-07-right-click-slide-images-to-open-the-custom-menu-with-arrang.md +6 -0
  114. package/corpus/templates/slides/changelog/2026-10-07-slide-image-actions-preserve-argument-text-that-starts-with-.md +6 -0
  115. package/corpus/templates/slides/changelog/2026-10-07-svgs-exported-with-a-standard-svg-1-1-declaration-now-upload.md +6 -0
  116. package/corpus/templates/slides/changelog/2026-10-07-two-people-typing-in-different-text-boxes-on-the-same-slide-.md +6 -0
  117. package/corpus/templates/slides/changelog/2026-10-07-underline-styling-stays-consistent-when-formatting-nested-sl.md +6 -0
  118. package/corpus/templates/slides/changelog/2026-10-07-videos-can-be-added-to-slides-and-played-on-click-or-automat.md +6 -0
  119. package/corpus/templates/slides/scripts/edit-fidelity/README.md +8 -0
  120. package/corpus/templates/slides/scripts/edit-fidelity/authoring-fuzz.ts +556 -54
  121. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +281 -14
  122. package/corpus/templates/slides/server/handlers/assets.ts +1356 -8
  123. package/corpus/templates/slides/server/handlers/uploads.ts +13 -7
  124. package/corpus/templates/slides/server/routes/api/assets/upload-video.post.ts +1 -0
  125. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +1 -0
  126. package/package.json +1 -1
  127. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +0 -434
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: 11353
35
+ - template files: 11386
@@ -16,6 +16,7 @@ Rules:
16
16
  - To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
17
17
  - To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
18
18
  - If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
19
+ - MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
19
20
 
20
21
  Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
21
22
 
@@ -3,6 +3,16 @@
3
3
  All notable user-facing changes to Agent-Native Analytics are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
+ ## 2026-10-07
7
+
8
+ ### Added
9
+
10
+ - Session replay screenshots can be copied directly into Design.
11
+
12
+ ### Fixed
13
+
14
+ - Session replay screenshot export captures readable video frames and safe poster images.
15
+
6
16
  ## 2026-10-06
7
17
 
8
18
  ### Added
@@ -1581,6 +1581,13 @@ export default {
1581
1581
  savingScreenshot: "Saving screenshot…",
1582
1582
  screenshotDownloaded: "Screenshot downloaded",
1583
1583
  screenshotSaveFailed: "Couldn't save screenshot",
1584
+ copyScreenshot: "Copy to Design",
1585
+ copyingScreenshot: "Copying screenshot…",
1586
+ screenshotCopiedForDesign: "Screenshot copied. Paste it into Design.",
1587
+ screenshotCopyFailed:
1588
+ "Couldn't copy the screenshot to the clipboard. Try Save screenshot, then upload the PNG in Design.",
1589
+ screenshotCopyUnsupportedAssets:
1590
+ "Screenshot not copied: this moment contains media or images that can't be captured securely. Try another moment in the replay.",
1584
1591
  screenshotUnsupportedAssets:
1585
1592
  "Screenshot not saved: some embedded media or images can't be captured securely.",
1586
1593
  timeline: "Event timeline",
@@ -96,9 +96,11 @@ import {
96
96
  import { extractReplayDiagnostics } from "./session-replay-devtools";
97
97
  import type { ReplayDevToolsDiagnostics } from "./session-replay-devtools";
98
98
  import {
99
+ captureReplayScreenshot,
99
100
  completeReplayScreenshotCapture,
100
- downloadReplayScreenshot,
101
+ downloadReplayScreenshotBlob,
101
102
  ReplayScreenshotAssetError,
103
+ writeReplayScreenshotToClipboard,
102
104
  } from "./session-replay-screenshot";
103
105
  import {
104
106
  type SessionIssueMatch,
@@ -582,6 +584,9 @@ function ReplayPlayer({
582
584
  const [currentTime, setCurrentTime] = useState(0);
583
585
  const [totalTime, setTotalTime] = useState(0);
584
586
  const [speed, setSpeed] = useState(DEFAULT_SPEED);
587
+ const [screenshotAction, setScreenshotAction] = useState<
588
+ "copy" | "save" | null
589
+ >(null);
585
590
  const [skipInactive, setSkipInactive] = useState(true);
586
591
  const [devToolsOpen, setDevToolsOpen] = useState(false);
587
592
  const [devToolsHeight, setDevToolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);
@@ -1032,18 +1037,23 @@ function ReplayPlayer({
1032
1037
  }
1033
1038
  }
1034
1039
 
1035
- async function saveScreenshot() {
1040
+ function runScreenshotAction(
1041
+ action: "copy" | "save",
1042
+ operation: (
1043
+ screenshot: Promise<Blob>,
1044
+ filename: string,
1045
+ onClipboardWriteFailure: () => void,
1046
+ ) => Promise<void>,
1047
+ onSuccess: () => void,
1048
+ onFailure: (error: unknown) => void,
1049
+ ) {
1036
1050
  const replayer = replayerRef.current;
1037
1051
  const iframe = replayer?.iframe as HTMLIFrameElement | undefined;
1038
1052
  const stage = stageAreaRef.current;
1039
1053
  const stageRoot = stageRootRef.current;
1040
- if (
1041
- !replayer ||
1042
- !iframe ||
1043
- !stage ||
1044
- !stageRoot ||
1045
- savingScreenshotRef.current
1046
- ) {
1054
+ if (savingScreenshotRef.current) return;
1055
+ if (!replayer || !iframe || !stage || !stageRoot) {
1056
+ onFailure(new Error("Replay screenshot is unavailable"));
1047
1057
  return;
1048
1058
  }
1049
1059
 
@@ -1052,56 +1062,108 @@ function ReplayPlayer({
1052
1062
  replayer.getCurrentTime?.() ?? currentTimeRef.current,
1053
1063
  );
1054
1064
  const capture = new AbortController();
1065
+ let clipboardWriteFailed = false;
1066
+ const onClipboardWriteFailure = () => {
1067
+ if (screenshotCaptureRef.current !== capture) return;
1068
+ clipboardWriteFailed = true;
1069
+ capture.abort();
1070
+ };
1055
1071
  screenshotCaptureRef.current = capture;
1056
1072
  savingScreenshotRef.current = true;
1057
1073
  setSavingScreenshot(true);
1074
+ setScreenshotAction(action);
1058
1075
 
1076
+ const filename = `session-replay-${Math.floor(captureAt / 1000)
1077
+ .toString()
1078
+ .padStart(4, "0")}.png`;
1079
+ let actionPromise: Promise<void>;
1059
1080
  try {
1060
1081
  replayer.pause(captureAt);
1061
1082
  setPlaying(false);
1062
1083
  updateTime(captureAt);
1063
- await downloadReplayScreenshot(
1084
+ const screenshot = captureReplayScreenshot(
1064
1085
  stage,
1065
1086
  stageRoot,
1066
1087
  iframe,
1067
- `session-replay-${Math.floor(captureAt / 1000)
1068
- .toString()
1069
- .padStart(4, "0")}.png`,
1070
1088
  capture.signal,
1071
1089
  );
1072
- toast.success(t("sessions.screenshotDownloaded"));
1090
+ // Clipboard writes need this click's activation, so start the operation before awaiting capture.
1091
+ actionPromise = operation(screenshot, filename, onClipboardWriteFailure);
1073
1092
  } catch (error) {
1074
- if (!capture.signal.aborted) {
1093
+ actionPromise = Promise.reject(error);
1094
+ }
1095
+
1096
+ void actionPromise
1097
+ .then(onSuccess, (error: unknown) => {
1098
+ if (
1099
+ !capture.signal.aborted ||
1100
+ (clipboardWriteFailed && screenshotCaptureRef.current === capture)
1101
+ ) {
1102
+ capture.abort();
1103
+ onFailure(error);
1104
+ }
1105
+ })
1106
+ .finally(() => {
1107
+ const captureStillCurrent = completeReplayScreenshotCapture(
1108
+ screenshotCaptureRef,
1109
+ capture,
1110
+ () => {
1111
+ savingScreenshotRef.current = false;
1112
+ setSavingScreenshot(false);
1113
+ },
1114
+ );
1115
+ if (captureStillCurrent) setScreenshotAction(null);
1116
+ if (
1117
+ captureStillCurrent &&
1118
+ wasPlaying &&
1119
+ replayerRef.current === replayer
1120
+ ) {
1121
+ try {
1122
+ replayer.play(captureAt);
1123
+ setPlaying(true);
1124
+ } catch {
1125
+ setPlaying(false);
1126
+ }
1127
+ }
1128
+ });
1129
+ }
1130
+
1131
+ function saveScreenshot() {
1132
+ runScreenshotAction(
1133
+ "save",
1134
+ (screenshot, filename) =>
1135
+ screenshot.then((blob) => downloadReplayScreenshotBlob(blob, filename)),
1136
+ () => toast.success(t("sessions.screenshotDownloaded")),
1137
+ (error) =>
1075
1138
  toast.error(
1076
1139
  t(
1077
1140
  error instanceof ReplayScreenshotAssetError
1078
1141
  ? "sessions.screenshotUnsupportedAssets"
1079
1142
  : "sessions.screenshotSaveFailed",
1080
1143
  ),
1081
- );
1082
- }
1083
- } finally {
1084
- const captureStillCurrent = completeReplayScreenshotCapture(
1085
- screenshotCaptureRef,
1086
- capture,
1087
- () => {
1088
- savingScreenshotRef.current = false;
1089
- setSavingScreenshot(false);
1090
- },
1091
- );
1092
- if (
1093
- captureStillCurrent &&
1094
- wasPlaying &&
1095
- replayerRef.current === replayer
1096
- ) {
1097
- try {
1098
- replayer.play(captureAt);
1099
- setPlaying(true);
1100
- } catch {
1101
- setPlaying(false);
1102
- }
1103
- }
1104
- }
1144
+ ),
1145
+ );
1146
+ }
1147
+
1148
+ function copyScreenshotToDesign() {
1149
+ runScreenshotAction(
1150
+ "copy",
1151
+ (screenshot, _filename, onClipboardWriteFailure) =>
1152
+ writeReplayScreenshotToClipboard(
1153
+ screenshot,
1154
+ undefined,
1155
+ onClipboardWriteFailure,
1156
+ ),
1157
+ () => toast.success(t("sessions.screenshotCopiedForDesign")),
1158
+ (error) =>
1159
+ toast.error(
1160
+ t(
1161
+ error instanceof ReplayScreenshotAssetError
1162
+ ? "sessions.screenshotCopyUnsupportedAssets"
1163
+ : "sessions.screenshotCopyFailed",
1164
+ ),
1165
+ ),
1166
+ );
1105
1167
  }
1106
1168
 
1107
1169
  const disabled = status !== "ready" || savingScreenshot;
@@ -1224,11 +1286,25 @@ function ReplayPlayer({
1224
1286
  >
1225
1287
  <IconDownload className="me-1.5 h-4 w-4" />
1226
1288
  {t(
1227
- savingScreenshot
1289
+ savingScreenshot && screenshotAction === "save"
1228
1290
  ? "sessions.savingScreenshot"
1229
1291
  : "sessions.saveScreenshot",
1230
1292
  )}
1231
1293
  </Button>
1294
+ <Button
1295
+ type="button"
1296
+ variant="outline"
1297
+ size="sm"
1298
+ disabled={disabled}
1299
+ onClick={copyScreenshotToDesign}
1300
+ >
1301
+ <IconCopy className="me-1.5 h-4 w-4" />
1302
+ {t(
1303
+ savingScreenshot && screenshotAction === "copy"
1304
+ ? "sessions.copyingScreenshot"
1305
+ : "sessions.copyScreenshot",
1306
+ )}
1307
+ </Button>
1232
1308
 
1233
1309
  <span className="w-12 text-center font-mono text-xs text-muted-foreground">
1234
1310
  {formatClock(currentTime)}
@@ -15,7 +15,9 @@ const MAX_INLINE_ASSET_BYTES = 32_000_000;
15
15
  const MAX_IMAGE_RESPONSE_BYTES = 12_000_000;
16
16
  const MAX_SCREENSHOT_DIMENSION = 8_192;
17
17
  const MAX_SCREENSHOT_PIXELS = 16_000_000;
18
+ const MAX_CLIPBOARD_PNG_BYTES = 32_000_000;
18
19
  const MAX_REPLAY_IFRAME_DEPTH = 8;
20
+ const VIDEO_READY_STATE_HAVE_CURRENT_DATA = 2;
19
21
  const REPLAY_SCREENSHOT_MARKER = "data-replay-screenshot-map";
20
22
 
21
23
  type ReplayImageResource = { document: Document; url: string };
@@ -567,8 +569,12 @@ function imageResourcesInDocuments(
567
569
  (element): element is HTMLVideoElement => element.tagName === "VIDEO",
568
570
  )) {
569
571
  if (!isElementRendered(video, current)) continue;
570
- if (
571
- video.poster ||
572
+ if (video.controls) throw new ReplayScreenshotAssetError();
573
+ if (hasCurrentVideoFrame(video)) {
574
+ assertVideoFrameReadable(video);
575
+ } else if (video.poster) {
576
+ addUrl(video.poster, current.baseURI, current);
577
+ } else if (
572
578
  video.currentSrc ||
573
579
  video.hasAttribute("src") ||
574
580
  video.srcObject ||
@@ -631,6 +637,68 @@ function imageResourcesInDocuments(
631
637
  );
632
638
  }
633
639
 
640
+ function hasCurrentVideoFrame(video: HTMLVideoElement): boolean {
641
+ return (
642
+ video.readyState >= VIDEO_READY_STATE_HAVE_CURRENT_DATA &&
643
+ video.videoWidth > 0 &&
644
+ video.videoHeight > 0
645
+ );
646
+ }
647
+
648
+ function assertVideoFrameReadable(video: HTMLVideoElement): void {
649
+ const probe = video.ownerDocument.createElement("canvas");
650
+ probe.width = 1;
651
+ probe.height = 1;
652
+ const context = probe.getContext("2d");
653
+ if (!context) throw new ReplayScreenshotAssetError();
654
+ try {
655
+ context.drawImage(video, 0, 0, 1, 1);
656
+ context.getImageData(0, 0, 1, 1);
657
+ } catch {
658
+ throw new ReplayScreenshotAssetError();
659
+ }
660
+ }
661
+
662
+ function copyComputedStyles(
663
+ source: Element,
664
+ target: HTMLElement,
665
+ view: Window,
666
+ ): void {
667
+ const styles = view.getComputedStyle(source);
668
+ for (let index = 0; index < styles.length; index += 1) {
669
+ const property = styles.item(index);
670
+ target.style.setProperty(
671
+ property,
672
+ styles.getPropertyValue(property),
673
+ styles.getPropertyPriority(property),
674
+ );
675
+ }
676
+ }
677
+
678
+ function html2CanvasVideoClone(
679
+ video: HTMLVideoElement,
680
+ clonesByMarker: Map<string, Element>,
681
+ ): Element | undefined {
682
+ const parent = video.parentElement;
683
+ const parentMarker = parent?.getAttribute(REPLAY_SCREENSHOT_MARKER);
684
+ const clonedParent = parentMarker ? clonesByMarker.get(parentMarker) : null;
685
+ if (!parent || !clonedParent) return undefined;
686
+
687
+ const originalChildren = Array.from(parent.children).filter(
688
+ (element) => element.tagName !== "SCRIPT",
689
+ );
690
+ const videoIndex = originalChildren.indexOf(video);
691
+ if (videoIndex === -1) return undefined;
692
+
693
+ const clonedChildren = Array.from(clonedParent.children).filter(
694
+ (element) =>
695
+ element.tagName !== "SCRIPT" &&
696
+ element.localName !== "html2canvaspseudoelement",
697
+ );
698
+ const clone = clonedChildren[videoIndex];
699
+ return clone?.tagName === "CANVAS" ? clone : undefined;
700
+ }
701
+
634
702
  function imageUrlsInDocuments(documents: Document[]): string[] {
635
703
  return [
636
704
  ...new Set(imageResourcesInDocuments(documents).map(({ url }) => url)),
@@ -859,6 +927,17 @@ export function inlineReplayAssets(
859
927
  }
860
928
  }
861
929
 
930
+ for (const originalElement of originalElements) {
931
+ if (originalElement.tagName !== "VIDEO") continue;
932
+ const marker = originalElement.getAttribute(REPLAY_SCREENSHOT_MARKER);
933
+ if (!marker || clonesByMarker.has(marker)) continue;
934
+ const clone = html2CanvasVideoClone(
935
+ originalElement as HTMLVideoElement,
936
+ clonesByMarker,
937
+ );
938
+ if (clone) clonesByMarker.set(marker, clone);
939
+ }
940
+
862
941
  for (const [documentIndex, original] of originalDocuments.entries()) {
863
942
  const rootMarker = original.documentElement.getAttribute(
864
943
  REPLAY_SCREENSHOT_MARKER,
@@ -878,9 +957,13 @@ export function inlineReplayAssets(
878
957
  if (originalElement.tagName === "SCRIPT") continue;
879
958
  const marker = originalElement.getAttribute(REPLAY_SCREENSHOT_MARKER);
880
959
  let clonedElement = marker ? clonesByMarker.get(marker) : undefined;
960
+ const isHtml2CanvasVideoClone =
961
+ originalElement.tagName === "VIDEO" &&
962
+ clonedElement?.tagName === "CANVAS";
881
963
  if (
882
964
  !clonedElement ||
883
- (originalElement.tagName !== clonedElement.tagName &&
965
+ (!isHtml2CanvasVideoClone &&
966
+ originalElement.tagName !== clonedElement.tagName &&
884
967
  !originalElement.localName.includes("-"))
885
968
  ) {
886
969
  throw new ReplayScreenshotAssetError();
@@ -894,21 +977,34 @@ export function inlineReplayAssets(
894
977
  const image = cloned.createElement("img");
895
978
  image.alt = "";
896
979
  image.src = screenshot;
897
- const frameStyles = view.getComputedStyle(originalElement);
898
- for (let index = 0; index < frameStyles.length; index += 1) {
899
- const property = frameStyles.item(index);
900
- image.style.setProperty(
901
- property,
902
- frameStyles.getPropertyValue(property),
903
- frameStyles.getPropertyPriority(property),
904
- );
905
- }
980
+ copyComputedStyles(originalElement, image, view);
906
981
  image.style.setProperty("object-fit", "fill", "important");
907
982
  clonedElement.replaceWith(image);
908
983
  clonedElement = image;
909
984
  if (marker) clonesByMarker.set(marker, image);
910
985
  }
911
986
  }
987
+ if (originalElement.tagName === "VIDEO") {
988
+ const video = originalElement as HTMLVideoElement;
989
+ if (!hasCurrentVideoFrame(video) && video.poster) {
990
+ const posterUrl = new URL(video.poster, original.baseURI).href;
991
+ const poster = documentAssets.get(posterUrl);
992
+ if (!poster) throw new ReplayScreenshotAssetError();
993
+ const image = cloned.createElement("img");
994
+ image.alt = "";
995
+ image.src = poster;
996
+ const videoStyles = view.getComputedStyle(video);
997
+ copyComputedStyles(video, image, view);
998
+ image.style.setProperty(
999
+ "object-fit",
1000
+ videoStyles.objectFit || "contain",
1001
+ "important",
1002
+ );
1003
+ clonedElement.replaceWith(image);
1004
+ clonedElement = image;
1005
+ if (marker) clonesByMarker.set(marker, image);
1006
+ }
1007
+ }
912
1008
  if (originalElement instanceof view.HTMLImageElement) {
913
1009
  const source = originalElement.currentSrc || originalElement.src;
914
1010
  const dataUrl = documentAssets.get(source);
@@ -1084,13 +1180,87 @@ function waitForReplayPaint(signal?: AbortSignal): Promise<void> {
1084
1180
  });
1085
1181
  }
1086
1182
 
1087
- export async function downloadReplayScreenshot(
1183
+ export class ReplayScreenshotClipboardError extends Error {
1184
+ constructor() {
1185
+ super("Replay screenshot could not be copied to the clipboard");
1186
+ this.name = "ReplayScreenshotClipboardError";
1187
+ }
1188
+ }
1189
+
1190
+ export function writeReplayScreenshotToClipboard(
1191
+ screenshot: Blob | Promise<Blob>,
1192
+ clipboard: Pick<Clipboard, "write"> | undefined = globalThis.navigator
1193
+ ?.clipboard,
1194
+ onClipboardWriteFailure?: () => void,
1195
+ ): Promise<void> {
1196
+ const ClipboardItemConstructor = globalThis.ClipboardItem;
1197
+ const boundedPng = Promise.resolve(screenshot).then((blob) => {
1198
+ if (
1199
+ blob.type !== "image/png" ||
1200
+ blob.size === 0 ||
1201
+ blob.size > MAX_CLIPBOARD_PNG_BYTES
1202
+ ) {
1203
+ throw new ReplayScreenshotClipboardError();
1204
+ }
1205
+ return blob;
1206
+ });
1207
+ const pngResult = boundedPng.then(
1208
+ () => ({ kind: "png-ready" as const }),
1209
+ (error: unknown) => ({ error, kind: "png-failed" as const }),
1210
+ );
1211
+
1212
+ let clipboardWrite: Promise<void>;
1213
+ try {
1214
+ if (!clipboard?.write || !ClipboardItemConstructor) {
1215
+ throw new ReplayScreenshotClipboardError();
1216
+ }
1217
+
1218
+ // Start the clipboard write with the click; the promised PNG can render afterward.
1219
+ const item = new ClipboardItemConstructor({ "image/png": boundedPng });
1220
+ clipboardWrite = Promise.resolve(clipboard.write([item]));
1221
+ } catch (error) {
1222
+ clipboardWrite = Promise.reject(error);
1223
+ }
1224
+
1225
+ const writeResult = clipboardWrite.then(
1226
+ () => ({ kind: "write-succeeded" as const }),
1227
+ () => ({ kind: "write-failed" as const }),
1228
+ );
1229
+
1230
+ return Promise.race([pngResult, writeResult]).then(async (firstResult) => {
1231
+ if (firstResult.kind === "write-failed") {
1232
+ const error = new ReplayScreenshotClipboardError();
1233
+ onClipboardWriteFailure?.();
1234
+ throw error;
1235
+ }
1236
+
1237
+ const finalPngResult =
1238
+ firstResult.kind === "png-ready" || firstResult.kind === "png-failed"
1239
+ ? firstResult
1240
+ : await pngResult;
1241
+ if (finalPngResult.kind === "png-failed") {
1242
+ if (finalPngResult.error instanceof ReplayScreenshotAssetError) {
1243
+ throw finalPngResult.error;
1244
+ }
1245
+ throw new ReplayScreenshotClipboardError();
1246
+ }
1247
+
1248
+ const finalWriteResult =
1249
+ firstResult.kind === "write-succeeded" ? firstResult : await writeResult;
1250
+ if (finalWriteResult.kind === "write-failed") {
1251
+ const error = new ReplayScreenshotClipboardError();
1252
+ onClipboardWriteFailure?.();
1253
+ throw error;
1254
+ }
1255
+ });
1256
+ }
1257
+
1258
+ export async function captureReplayScreenshot(
1088
1259
  stage: HTMLElement,
1089
1260
  stageRoot: HTMLElement,
1090
1261
  iframe: HTMLIFrameElement,
1091
- filename: string,
1092
1262
  signal?: AbortSignal,
1093
- ): Promise<void> {
1263
+ ): Promise<Blob> {
1094
1264
  const replayWindow = iframe.contentWindow;
1095
1265
  const replayDocument = iframe.contentDocument;
1096
1266
  if (
@@ -1198,13 +1368,7 @@ export async function downloadReplayScreenshot(
1198
1368
  assertScreenshotDimensions(canvas.width, canvas.height);
1199
1369
  const blob = await canvasToBlob(canvas);
1200
1370
  assertCaptureAvailable();
1201
-
1202
- const downloadUrl = URL.createObjectURL(blob);
1203
- const link = document.createElement("a");
1204
- link.download = filename;
1205
- link.href = downloadUrl;
1206
- link.click();
1207
- window.setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000);
1371
+ return blob;
1208
1372
  } finally {
1209
1373
  if (previousStageFrameMarker === null) {
1210
1374
  iframe.removeAttribute(REPLAY_SCREENSHOT_MARKER);
@@ -1214,6 +1378,34 @@ export async function downloadReplayScreenshot(
1214
1378
  }
1215
1379
  }
1216
1380
 
1381
+ export function downloadReplayScreenshotBlob(
1382
+ blob: Blob,
1383
+ filename: string,
1384
+ ): void {
1385
+ const downloadUrl = URL.createObjectURL(blob);
1386
+ const link = document.createElement("a");
1387
+ link.download = filename;
1388
+ link.href = downloadUrl;
1389
+ link.click();
1390
+ window.setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000);
1391
+ }
1392
+
1393
+ export async function downloadReplayScreenshot(
1394
+ stage: HTMLElement,
1395
+ stageRoot: HTMLElement,
1396
+ iframe: HTMLIFrameElement,
1397
+ filename: string,
1398
+ signal?: AbortSignal,
1399
+ ): Promise<void> {
1400
+ const screenshot = await captureReplayScreenshot(
1401
+ stage,
1402
+ stageRoot,
1403
+ iframe,
1404
+ signal,
1405
+ );
1406
+ downloadReplayScreenshotBlob(screenshot, filename);
1407
+ }
1408
+
1217
1409
  function assertScreenshotDimensions(width: number, height: number): void {
1218
1410
  if (
1219
1411
  !Number.isSafeInteger(width) ||
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-07
4
+ ---
5
+
6
+ Session replay screenshot export captures readable video frames and safe poster images.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-10-07
4
+ ---
5
+
6
+ Session replay screenshots can be copied directly into Design.
@@ -16,6 +16,7 @@ Rules:
16
16
  - To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
17
17
  - To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
18
18
  - If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
19
+ - MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
19
20
 
20
21
  Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
21
22
 
@@ -16,6 +16,7 @@ Rules:
16
16
  - To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
17
17
  - To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
18
18
  - If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
19
+ - MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
19
20
 
20
21
  Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
21
22
 
@@ -16,6 +16,7 @@ Rules:
16
16
  - To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
17
17
  - To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
18
18
  - If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
19
+ - MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
19
20
 
20
21
  Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
21
22
 
@@ -16,6 +16,7 @@ Rules:
16
16
  - To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
17
17
  - To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
18
18
  - If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
19
+ - MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
19
20
 
20
21
  Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
21
22
 
@@ -16,6 +16,7 @@ Rules:
16
16
  - To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
17
17
  - To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
18
18
  - If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
19
+ - MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
19
20
 
20
21
  Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
21
22
 
@@ -3,6 +3,16 @@
3
3
  All notable user-facing changes to Clips are documented here. Open it any time
4
4
  from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
+ ## 2026-10-07
7
+
8
+ ### Improved
9
+
10
+ - Builder.io setup choices now say “Use Builder.io”.
11
+
12
+ ### Fixed
13
+
14
+ - Clips sizes Settings and recording views correctly while a recording is active.
15
+
6
16
  ## 2026-10-06
7
17
 
8
18
  ### Fixed