@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.
- 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 +214 -22
- package/corpus/templates/analytics/changelog/2026-10-07-session-replay-screenshot-export-captures-readable-video-fra.md +6 -0
- 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/clips/app/i18n/en-US.ts +1 -1
- package/corpus/templates/clips/changelog/2026-10-07-builder-io-setup-choices-now-say-use-builder-io.md +6 -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/components/editor/DocumentEditor.tsx +74 -5
- 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/changelog/2026-10-07-text-two-people-type-at-the-same-spot-now-reaches-the-saved-.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/.generated/bridge/editor-chrome.generated.ts +24 -6
- 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/bridge/editor-chrome.bridge.ts +26 -3
- 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-helpers.ts +40 -11
- package/corpus/templates/design/app/pages/design-editor/editor-session.ts +2 -1
- package/corpus/templates/design/app/pages/design-editor/position-coordinate-context.ts +21 -8
- package/corpus/templates/design/app/root.tsx +2 -0
- package/corpus/templates/design/changelog/2026-10-07-canvas-layers-now-stay-positioned-and-selected-correctly-whe.md +6 -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-fixed-position-readouts-inside-transformed-containers-and-ke.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/design/e2e/helpers.ts +5 -3
- package/corpus/templates/design/vite.config.ts +50 -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/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -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/plan/actions/seed-plan-collab.ts +4 -3
- package/corpus/templates/plan/app/components/plan/PlanContentRenderer.tsx +8 -14
- package/corpus/templates/plan/app/components/plan/PlanMarkdownEditor.tsx +56 -0
- package/corpus/templates/plan/shared/plan-autosave-retry.ts +20 -0
- package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +1 -0
- package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +24 -0
- package/corpus/templates/slides/AGENTS.md +1 -1
- package/corpus/templates/slides/CHANGELOG.md +24 -0
- package/corpus/templates/slides/actions/edit-image.ts +1 -18
- package/corpus/templates/slides/actions/export-html.ts +29 -16
- package/corpus/templates/slides/actions/generate-image.ts +1 -18
- package/corpus/templates/slides/actions/view-screen.ts +8 -0
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +18 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +20 -2
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +56 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +69 -14
- package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +4 -0
- package/corpus/templates/slides/app/components/editor/bullet-editing.ts +1 -1
- package/corpus/templates/slides/app/components/editor/in-place-text-session.ts +226 -11
- package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +32 -21
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +10 -1
- package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +2 -0
- package/corpus/templates/slides/app/components/visual-editor/SaveStatusIndicator.tsx +20 -7
- package/corpus/templates/slides/app/context/DeckContext.tsx +96 -4
- package/corpus/templates/slides/app/hooks/use-deck-access-reload.ts +5 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +13 -0
- package/corpus/templates/slides/app/lib/deck-access-lost.ts +4 -0
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +229 -62
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +30 -0
- package/corpus/templates/slides/app/lib/slide-source-map.ts +13 -1
- package/corpus/templates/slides/app/lib/slide-video.ts +163 -0
- package/corpus/templates/slides/app/lib/video-upload.ts +40 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +82 -0
- package/corpus/templates/slides/changelog/2026-10-06-deleting-between-slide-paragraphs-now-preserves-inline-forma.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-a-deck-whose-access-is-revoked-while-it-is-open-now-says-acc.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-forward-delete-keeps-the-caret-on-the-correct-line-when-join.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-long-slide-text-editing-sessions-can-be-undone-without-losin.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-markdown-divider-shortcuts-and-autocorrect-replacements-rema.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-pasted-images-stay-inside-their-crop-frame-while-uploads-fin.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-right-click-slide-images-to-open-the-custom-menu-with-arrang.md +6 -0
- 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-svgs-exported-with-a-standard-svg-1-1-declaration-now-upload.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/slides/changelog/2026-10-07-underline-styling-stays-consistent-when-formatting-nested-sl.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-07-videos-can-be-added-to-slides-and-played-on-click-or-automat.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/README.md +8 -0
- package/corpus/templates/slides/scripts/edit-fidelity/authoring-fuzz.ts +556 -54
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +281 -14
- package/corpus/templates/slides/server/handlers/assets.ts +1356 -8
- package/corpus/templates/slides/server/handlers/uploads.ts +13 -7
- package/corpus/templates/slides/server/routes/api/assets/upload-video.post.ts +1 -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,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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
|
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<
|
|
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) ||
|
|
@@ -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
|