@agent-native/core-corpus 0.204.0-nightly-20261008011018 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/analytics/CHANGELOG.md +10 -0
- package/corpus/templates/analytics/app/i18n/en-US.ts +7 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +116 -40
- package/corpus/templates/analytics/app/pages/sessions/session-replay-screenshot.ts +107 -10
- package/corpus/templates/analytics/changelog/2026-10-07-session-replay-screenshots-can-be-copied-directly-into-desig.md +6 -0
- package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/clips/CHANGELOG.md +10 -0
- package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +13 -7
- package/corpus/templates/content/CHANGELOG.md +9 -0
- package/corpus/templates/content/actions/_database-setup-mutation.ts +3 -7
- package/corpus/templates/content/actions/_document-lifecycle.ts +250 -0
- package/corpus/templates/content/actions/delete-content-database.ts +6 -0
- package/corpus/templates/content/actions/delete-document.ts +169 -45
- package/corpus/templates/content/actions/list-content-trash.ts +2 -1
- package/corpus/templates/content/actions/resolve-content-landing.ts +14 -3
- package/corpus/templates/content/actions/restore-content-database.ts +20 -4
- package/corpus/templates/content/actions/restore-document.ts +121 -6
- package/corpus/templates/content/app/lib/content-landing.ts +67 -0
- package/corpus/templates/content/app/root.tsx +7 -1
- package/corpus/templates/content/app/routes/_app.home.tsx +65 -24
- package/corpus/templates/content/changelog/2026-10-06-connecting-notion-no-longer-leaves-its-used-authorization-co.md +6 -0
- package/corpus/templates/content/changelog/2026-10-06-external-agents-can-now-move-a-page-and-its-sub-pages-to-rec.md +6 -0
- package/corpus/templates/content/changelog/2026-10-06-opening-content-from-home-shows-your-last-page-sooner.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.agent.action-parity.md +5 -2
- package/corpus/templates/content/e2e/helpers.ts +4 -3
- package/corpus/templates/content/parity/matrix.md +2 -2
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/content/server/routes/api/notion/callback.get.ts +14 -2
- package/corpus/templates/content/shared/content-landing.ts +2 -0
- package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/design/CHANGELOG.md +7 -0
- package/corpus/templates/design/app/components/design/KeyboardShortcutsDialog.tsx +480 -0
- package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +1 -4
- package/corpus/templates/design/app/components/design/keyboard-shortcut-sections.ts +114 -0
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +1 -1
- package/corpus/templates/design/app/i18n/en-US.ts +5 -6
- package/corpus/templates/design/app/lib/design-sync-invalidation.ts +86 -0
- package/corpus/templates/design/app/pages/design-editor/editor-session.ts +2 -1
- package/corpus/templates/design/app/root.tsx +2 -0
- package/corpus/templates/design/changelog/2026-10-07-collaborator-edits-refresh-fewer-unrelated-panels.md +6 -0
- package/corpus/templates/design/changelog/2026-10-07-keyboard-shortcuts-now-open-in-a-dialog-with-search-and-a-ca.md +6 -0
- package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/dispatch/CHANGELOG.md +1 -0
- package/corpus/templates/dispatch/changelog/2026-10-06-signing-in-to-another-app-through-dispatch-no-longer-leaves-.md +6 -0
- package/corpus/templates/dispatch/server/plugins/identity-sso.ts +9 -2
- package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/slides/CHANGELOG.md +24 -0
- package/corpus/templates/slides/actions/edit-image.ts +1 -18
- package/corpus/templates/slides/actions/generate-image.ts +1 -18
- package/corpus/templates/slides/app/context/DeckContext.tsx +12 -2
- package/corpus/templates/slides/changelog/2026-10-07-slide-image-actions-preserve-argument-text-that-starts-with-.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-two-people-typing-in-different-text-boxes-on-the-same-slide-.md +6 -0
- package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +1 -0
- package/package.json +1 -1
- package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +0 -434
package/corpus/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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,6 +15,7 @@ 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;
|
|
19
20
|
const VIDEO_READY_STATE_HAVE_CURRENT_DATA = 2;
|
|
20
21
|
const REPLAY_SCREENSHOT_MARKER = "data-replay-screenshot-map";
|
|
@@ -1179,13 +1180,87 @@ function waitForReplayPaint(signal?: AbortSignal): Promise<void> {
|
|
|
1179
1180
|
});
|
|
1180
1181
|
}
|
|
1181
1182
|
|
|
1182
|
-
export
|
|
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(
|
|
1183
1259
|
stage: HTMLElement,
|
|
1184
1260
|
stageRoot: HTMLElement,
|
|
1185
1261
|
iframe: HTMLIFrameElement,
|
|
1186
|
-
filename: string,
|
|
1187
1262
|
signal?: AbortSignal,
|
|
1188
|
-
): Promise<
|
|
1263
|
+
): Promise<Blob> {
|
|
1189
1264
|
const replayWindow = iframe.contentWindow;
|
|
1190
1265
|
const replayDocument = iframe.contentDocument;
|
|
1191
1266
|
if (
|
|
@@ -1293,13 +1368,7 @@ export async function downloadReplayScreenshot(
|
|
|
1293
1368
|
assertScreenshotDimensions(canvas.width, canvas.height);
|
|
1294
1369
|
const blob = await canvasToBlob(canvas);
|
|
1295
1370
|
assertCaptureAvailable();
|
|
1296
|
-
|
|
1297
|
-
const downloadUrl = URL.createObjectURL(blob);
|
|
1298
|
-
const link = document.createElement("a");
|
|
1299
|
-
link.download = filename;
|
|
1300
|
-
link.href = downloadUrl;
|
|
1301
|
-
link.click();
|
|
1302
|
-
window.setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000);
|
|
1371
|
+
return blob;
|
|
1303
1372
|
} finally {
|
|
1304
1373
|
if (previousStageFrameMarker === null) {
|
|
1305
1374
|
iframe.removeAttribute(REPLAY_SCREENSHOT_MARKER);
|
|
@@ -1309,6 +1378,34 @@ export async function downloadReplayScreenshot(
|
|
|
1309
1378
|
}
|
|
1310
1379
|
}
|
|
1311
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
|
+
|
|
1312
1409
|
function assertScreenshotDimensions(width: number, height: number): void {
|
|
1313
1410
|
if (
|
|
1314
1411
|
!Number.isSafeInteger(width) ||
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -169,17 +169,23 @@ pnpm action suggest-document-edit --id abc123 \
|
|
|
169
169
|
|
|
170
170
|
### delete-document
|
|
171
171
|
|
|
172
|
-
Move a
|
|
173
|
-
collection membership remain intact so the subtree can be restored.
|
|
172
|
+
Move a page and all its sub-pages to Trash. IDs, bodies, hierarchy, and
|
|
173
|
+
collection membership remain intact so the subtree can be restored. Agents pass
|
|
174
|
+
the page's exact `updatedAt` from `<current-screen>` or a fresh read, plus an
|
|
175
|
+
idempotency key. A conflict means the page changed: read it again before
|
|
176
|
+
deciding to trash it. Collection pages use `delete-content-database` instead.
|
|
174
177
|
|
|
175
178
|
```bash
|
|
176
|
-
pnpm action delete-document --id abc123
|
|
179
|
+
pnpm action delete-document --id abc123 \
|
|
180
|
+
--expectedUpdatedAt '<updatedAt>' --idempotencyKey '<uuid>'
|
|
177
181
|
```
|
|
178
182
|
|
|
179
|
-
Restore the
|
|
183
|
+
Restore the receipt's `trashRootId` with its `trashedAt` (also listed by
|
|
184
|
+
`list-content-trash`), or permanently delete it only after it is in Trash:
|
|
180
185
|
|
|
181
186
|
```bash
|
|
182
|
-
pnpm action restore-document --id abc123
|
|
187
|
+
pnpm action restore-document --id abc123 \
|
|
188
|
+
--expectedTrashedAt '<trashedAt>' --idempotencyKey '<uuid>'
|
|
183
189
|
pnpm action plan-content-trash-purge --mode selection --documentIds '["abc123"]'
|
|
184
190
|
pnpm action permanently-delete-document --id abc123 --planId '<reviewed plan ID>' --scopeToken '<opaque plan token>'
|
|
185
191
|
```
|
|
@@ -335,7 +341,7 @@ failures stop the run.
|
|
|
335
341
|
| "What am I looking at?" | Answer from `<current-screen>` (call `view-screen` only if truncated) |
|
|
336
342
|
| "Create a page about X" | `create-document`, then `navigate --documentId <returned id>` and verify with `view-screen` |
|
|
337
343
|
| "Fix a typo / small edit" | ID from `<current-screen>`, `edit-document --id ... --find "old" --replace "new"` |
|
|
338
|
-
| "Delete this page" | ID from `<current-screen>`, `delete-document --id ...`
|
|
344
|
+
| "Delete this page" | ID and `updatedAt` from `<current-screen>`, `delete-document --id ... --expectedUpdatedAt ... --idempotencyKey ...` |
|
|
339
345
|
|
|
340
346
|
## Common Tasks
|
|
341
347
|
|
|
@@ -346,7 +352,7 @@ failures stop the run.
|
|
|
346
352
|
| "Find my meeting notes" | `search-documents --query "meeting notes"` |
|
|
347
353
|
| "Fix a typo / edit a line" | `view-screen` to get ID, then `edit-document --id ... --find "old" --replace "new"` |
|
|
348
354
|
| "Rewrite this document" | `view-screen` to get ID, then `update-document --id ... --content ...` |
|
|
349
|
-
| "Delete this page" | `view-screen`
|
|
355
|
+
| "Delete this page" | `view-screen` for ID and `updatedAt`, then `delete-document` with both and an idempotency key |
|
|
350
356
|
| "Add a sub-page" | `create-document --title "Sub" --parentId <parentId>` |
|
|
351
357
|
| "Show me the document tree" | `list-documents` |
|
|
352
358
|
|
|
@@ -7,6 +7,10 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
7
7
|
|
|
8
8
|
### Fixed
|
|
9
9
|
|
|
10
|
+
- ChatGPT widgets identify a stalled Content editor instead of keeping its body skeleton onscreen.
|
|
11
|
+
- ChatGPT widgets open document content without a cookie session
|
|
12
|
+
- Content documents now open in ChatGPT widgets with their full body
|
|
13
|
+
- Text two people type at the same spot now reaches the saved page for both of them, so readers and agents see it
|
|
10
14
|
- ChatGPT widget previews show saved document bodies without a Content session.
|
|
11
15
|
- Documents opened in ChatGPT show their saved content right away.
|
|
12
16
|
- ChatGPT widgets show saved document bodies in read-only mode without a Content session.
|
|
@@ -14,12 +18,17 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
14
18
|
|
|
15
19
|
## 2026-10-06
|
|
16
20
|
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- External agents can now move a page and its sub-pages to recoverable Trash, list Trash, and restore it through Content's MCP tools.
|
|
24
|
+
|
|
17
25
|
### Improved
|
|
18
26
|
|
|
19
27
|
- ChatGPT can open Content document and database editors in the existing workspace, and document links use the right label.
|
|
20
28
|
|
|
21
29
|
### Fixed
|
|
22
30
|
|
|
31
|
+
- Connecting Notion no longer leaves its used authorization code in the address bar.
|
|
23
32
|
- Documents and databases opened from ChatGPT load in the Content editor without a separate sign-in.
|
|
24
33
|
- ChatGPT Content widgets load database views correctly.
|
|
25
34
|
- Reopening a ChatGPT Content widget restores the editor for its saved document or collection.
|
|
@@ -148,8 +148,7 @@ export async function claimSetupIntent(
|
|
|
148
148
|
payload: unknown,
|
|
149
149
|
): Promise<Claim> {
|
|
150
150
|
const email = getRequestUserEmail()?.trim().toLowerCase();
|
|
151
|
-
if (!email)
|
|
152
|
-
setupError("UNAUTHORIZED", "Sign in to change a Content database.", 401);
|
|
151
|
+
if (!email) setupError("UNAUTHORIZED", "Sign in to change Content.", 401);
|
|
153
152
|
const payloadDigest = digest(payload);
|
|
154
153
|
const table = schema.contentDatabaseSetupReceipts;
|
|
155
154
|
const inserted = await tx
|
|
@@ -177,14 +176,11 @@ export async function claimSetupIntent(
|
|
|
177
176
|
)
|
|
178
177
|
.returning();
|
|
179
178
|
if (!claim)
|
|
180
|
-
setupError(
|
|
181
|
-
"RECEIPT_MISMATCH",
|
|
182
|
-
"The database operation claim could not be read.",
|
|
183
|
-
);
|
|
179
|
+
setupError("RECEIPT_MISMATCH", "The operation claim could not be read.");
|
|
184
180
|
if (claim.payloadDigest !== payloadDigest)
|
|
185
181
|
setupError(
|
|
186
182
|
"IDEMPOTENCY_KEY_REUSED",
|
|
187
|
-
"This key was already used for a different
|
|
183
|
+
"This key was already used for a different Content operation. Use the original payload to retry it.",
|
|
188
184
|
);
|
|
189
185
|
if (inserted.length === 0 && claim.resultJson === null)
|
|
190
186
|
setupError(
|