@agent-native/core 0.84.24 → 0.84.27

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 (82) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +18 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +12 -8
  5. package/corpus/core/src/agent/run-manager.ts +58 -6
  6. package/corpus/core/src/agent/types.ts +9 -2
  7. package/corpus/core/src/client/AssistantChat.tsx +51 -3
  8. package/corpus/core/src/client/FeedbackButton.tsx +2 -2
  9. package/corpus/core/src/client/components/PresenceBar.tsx +7 -2
  10. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +23 -9
  11. package/corpus/core/src/client/sharing/ShareButton.tsx +22 -3
  12. package/corpus/core/src/client/sharing/ShareDialog.tsx +13 -1
  13. package/corpus/core/src/client/sse-event-processor.ts +83 -26
  14. package/corpus/core/src/client/tool-display.ts +1 -1
  15. package/corpus/core/src/sharing/actions/set-resource-visibility.ts +3 -3
  16. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +4 -2
  17. package/corpus/templates/clips/app/components/player/share-dialog.tsx +9 -0
  18. package/corpus/templates/clips/app/i18n/ar-SA.ts +3 -0
  19. package/corpus/templates/clips/app/i18n/de-DE.ts +3 -0
  20. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  21. package/corpus/templates/clips/app/i18n/es-ES.ts +3 -0
  22. package/corpus/templates/clips/app/i18n/fr-FR.ts +3 -0
  23. package/corpus/templates/clips/app/i18n/hi-IN.ts +3 -0
  24. package/corpus/templates/clips/app/i18n/ja-JP.ts +3 -0
  25. package/corpus/templates/clips/app/i18n/ko-KR.ts +3 -0
  26. package/corpus/templates/clips/app/i18n/pt-BR.ts +3 -0
  27. package/corpus/templates/clips/app/i18n/zh-CN.ts +3 -0
  28. package/corpus/templates/clips/app/i18n/zh-TW.ts +3 -0
  29. package/corpus/templates/clips/changelog/2026-07-01-shared-clips-can-now-copy-a-ready-to-send-prompt-that-tells-.md +6 -0
  30. package/corpus/templates/design/actions/insert-asset.ts +14 -2
  31. package/corpus/templates/design/actions/insert-design-native-asset.ts +16 -4
  32. package/corpus/templates/design/actions/insert-figma-library-asset.ts +14 -2
  33. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +14 -0
  34. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +54 -10
  35. package/corpus/templates/design/app/components/design/EditPanel.tsx +23 -16
  36. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -2
  37. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +9 -5
  38. package/corpus/templates/design/app/components/layout/Header.tsx +44 -1
  39. package/corpus/templates/design/app/pages/DesignEditor.tsx +267 -105
  40. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +15 -1
  41. package/corpus/templates/design/app/pages/Index.tsx +20 -9
  42. package/corpus/templates/design/app/routes/settings.tsx +1 -1
  43. package/corpus/templates/design/changelog/2026-07-01-design-editor-polish-makes-zoom-assets-sharing-and-agent-handoffs-clearer.md +6 -0
  44. package/corpus/templates/design/changelog/2026-07-01-design-qa-fixes-make-exports-permissions-and-tools-clearer.md +6 -0
  45. package/dist/agent/production-agent.d.ts.map +1 -1
  46. package/dist/agent/production-agent.js +12 -9
  47. package/dist/agent/production-agent.js.map +1 -1
  48. package/dist/agent/run-manager.d.ts.map +1 -1
  49. package/dist/agent/run-manager.js +56 -4
  50. package/dist/agent/run-manager.js.map +1 -1
  51. package/dist/agent/types.d.ts +3 -0
  52. package/dist/agent/types.d.ts.map +1 -1
  53. package/dist/agent/types.js.map +1 -1
  54. package/dist/client/AssistantChat.d.ts.map +1 -1
  55. package/dist/client/AssistantChat.js +52 -3
  56. package/dist/client/AssistantChat.js.map +1 -1
  57. package/dist/client/FeedbackButton.js +2 -2
  58. package/dist/client/FeedbackButton.js.map +1 -1
  59. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  60. package/dist/client/components/PresenceBar.js +6 -2
  61. package/dist/client/components/PresenceBar.js.map +1 -1
  62. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  63. package/dist/client/composer/ComposerPlusMenu.js +15 -7
  64. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  65. package/dist/client/sharing/ShareButton.js +15 -2
  66. package/dist/client/sharing/ShareButton.js.map +1 -1
  67. package/dist/client/sharing/ShareDialog.js +16 -1
  68. package/dist/client/sharing/ShareDialog.js.map +1 -1
  69. package/dist/client/sse-event-processor.d.ts.map +1 -1
  70. package/dist/client/sse-event-processor.js +64 -21
  71. package/dist/client/sse-event-processor.js.map +1 -1
  72. package/dist/client/tool-display.js +1 -1
  73. package/dist/client/tool-display.js.map +1 -1
  74. package/dist/collab/awareness.d.ts +2 -2
  75. package/dist/collab/awareness.d.ts.map +1 -1
  76. package/dist/notifications/routes.d.ts +3 -3
  77. package/dist/observability/routes.d.ts +5 -5
  78. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  79. package/dist/server/transcribe-voice.d.ts +1 -1
  80. package/dist/sharing/actions/set-resource-visibility.js +3 -3
  81. package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
  82. package/package.json +1 -1
@@ -150,9 +150,10 @@ export const SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS = 90_000;
150
150
 
151
151
  type ActivityTrailEntry = AgentActivityTrailEntry;
152
152
 
153
- type PreparingActionState = {
154
- tool?: string;
153
+ type PreparingActionEntry = {
154
+ tool: string;
155
155
  startedAt?: number;
156
+ lastProgressBytes?: number;
156
157
  /**
157
158
  * Timestamp of the last real streaming progress for the in-preparation tool
158
159
  * input. The server emits a throttled `activity` heartbeat per
@@ -165,6 +166,10 @@ type PreparingActionState = {
165
166
  lastProgressAt?: number;
166
167
  };
167
168
 
169
+ type PreparingActionState = {
170
+ entries?: Map<string, PreparingActionEntry>;
171
+ };
172
+
168
173
  function formatProgressBytes(bytes: number): string {
169
174
  if (bytes < 1024) return `${bytes} B`;
170
175
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -185,8 +190,15 @@ function isPreparingActionActivity(ev: SSEEvent): boolean {
185
190
  return label.startsWith("preparing ") && label.includes(" action");
186
191
  }
187
192
 
188
- function isProgressEvent(ev: SSEEvent): boolean {
189
- return ev.type !== "stream_keepalive";
193
+ function isMeaningfulProgressEvent(
194
+ ev: SSEEvent,
195
+ actionPreparationProgress?: boolean,
196
+ ): boolean {
197
+ if (ev.type === "stream_keepalive") return false;
198
+ if (ev.type === "activity" && isPreparingActionActivity(ev)) {
199
+ return actionPreparationProgress === true;
200
+ }
201
+ return true;
190
202
  }
191
203
 
192
204
  function baseActivityLabel(ev: SSEEvent, tool?: string): string {
@@ -258,19 +270,42 @@ function updatePreparingActionState(
258
270
  state: PreparingActionState,
259
271
  ev: SSEEvent,
260
272
  now: number,
261
- ) {
273
+ ): boolean | undefined {
262
274
  if (ev.type === "activity" && isPreparingActionActivity(ev)) {
263
275
  const tool = ev.tool?.trim() || undefined;
264
- if (!tool) return;
265
- if (state.tool !== tool || state.startedAt === undefined) {
266
- state.tool = tool;
267
- state.startedAt = now;
276
+ if (!tool) return false;
277
+ const key = ev.id?.trim() || tool;
278
+ const entries = state.entries ?? new Map<string, PreparingActionEntry>();
279
+ state.entries = entries;
280
+ let entry = entries.get(key);
281
+ if (!entry) {
282
+ entry = {
283
+ tool,
284
+ startedAt: now,
285
+ lastProgressAt: undefined,
286
+ lastProgressBytes: undefined,
287
+ };
288
+ entries.set(key, entry);
268
289
  }
269
- // Every tool-input activity is a throttled proof the model is still
270
- // streaming this action's argument. Treat it as progress so large inputs
271
- // that take many seconds/minutes to stream are NOT flagged as stalled.
272
- state.lastProgressAt = now;
273
- return;
290
+ const progressBytes = activityProgressBytes(ev);
291
+ const previousBytes = entry.lastProgressBytes ?? 0;
292
+ if (progressBytes !== undefined) {
293
+ entry.lastProgressBytes = Math.max(previousBytes, progressBytes);
294
+ }
295
+ if (!ev.id?.trim()) {
296
+ if (progressBytes !== undefined && progressBytes > 0) {
297
+ entry.lastProgressAt = now;
298
+ return true;
299
+ }
300
+ return false;
301
+ }
302
+ if (progressBytes !== undefined && progressBytes > previousBytes) {
303
+ // A byte increase is proof the model is still streaming this action's
304
+ // argument. Repeated zero-byte prep activity is only a heartbeat.
305
+ entry.lastProgressAt = now;
306
+ return true;
307
+ }
308
+ return false;
274
309
  }
275
310
 
276
311
  if (
@@ -282,10 +317,19 @@ function updatePreparingActionState(
282
317
  ev.type === "error" ||
283
318
  ev.type === "missing_api_key"
284
319
  ) {
285
- state.tool = undefined;
286
- state.startedAt = undefined;
287
- state.lastProgressAt = undefined;
320
+ if (ev.type === "tool_start" || ev.type === "tool_done") {
321
+ const tool = ev.tool?.trim();
322
+ const id = ev.id?.trim();
323
+ for (const [key, entry] of state.entries ?? []) {
324
+ if ((id && key === id) || (!id && entry.tool === tool)) {
325
+ state.entries?.delete(key);
326
+ }
327
+ }
328
+ } else {
329
+ state.entries?.clear();
330
+ }
288
331
  }
332
+ return undefined;
289
333
  }
290
334
 
291
335
  function hasStalledPreparingAction(state: PreparingActionState, now: number) {
@@ -294,11 +338,16 @@ function hasStalledPreparingAction(state: PreparingActionState, now: number) {
294
338
  // streaming for a long time. `lastProgressAt` advances on every delta
295
339
  // heartbeat, so an actively-streaming large output keeps resetting this and
296
340
  // survives; a genuinely stuck prep (keepalive-only, no deltas) trips it.
297
- return (
298
- state.tool !== undefined &&
299
- state.lastProgressAt !== undefined &&
300
- now - state.lastProgressAt >= SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS
301
- );
341
+ for (const entry of state.entries?.values() ?? []) {
342
+ if (
343
+ entry.startedAt !== undefined &&
344
+ now - (entry.lastProgressAt ?? entry.startedAt) >=
345
+ SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS
346
+ ) {
347
+ return true;
348
+ }
349
+ }
350
+ return false;
302
351
  }
303
352
 
304
353
  async function readChunkWithProgressTimeout(
@@ -1193,11 +1242,15 @@ export async function* readSSEStream(
1193
1242
  continue;
1194
1243
  }
1195
1244
  const now = Date.now();
1196
- if (isProgressEvent(ev)) {
1245
+ const actionPreparationProgress = updatePreparingActionState(
1246
+ preparingActionState,
1247
+ ev,
1248
+ now,
1249
+ );
1250
+ if (isMeaningfulProgressEvent(ev, actionPreparationProgress)) {
1197
1251
  sawProgressEvent = true;
1198
1252
  lastMeaningfulEventAt = now;
1199
1253
  }
1200
- updatePreparingActionState(preparingActionState, ev, now);
1201
1254
 
1202
1255
  // Track sequence number for reconnection
1203
1256
  if (ev.seq !== undefined && onSeq) {
@@ -1355,11 +1408,15 @@ export async function readSSEStreamRaw(
1355
1408
  continue;
1356
1409
  }
1357
1410
  const now = Date.now();
1358
- if (isProgressEvent(ev)) {
1411
+ const actionPreparationProgress = updatePreparingActionState(
1412
+ preparingActionState,
1413
+ ev,
1414
+ now,
1415
+ );
1416
+ if (isMeaningfulProgressEvent(ev, actionPreparationProgress)) {
1359
1417
  sawProgressEvent = true;
1360
1418
  lastMeaningfulEventAt = now;
1361
1419
  }
1362
- updatePreparingActionState(preparingActionState, ev, now);
1363
1420
 
1364
1421
  if (ev.seq !== undefined && onSeq) {
1365
1422
  onSeq(ev.seq);
@@ -1,5 +1,5 @@
1
1
  const TOOL_DISPLAY_NAMES: Record<string, string> = {
2
- "delete-file": "delete screen",
2
+ "delete-file": "remove screen",
3
3
  "get-design-snapshot": "get screen snapshot",
4
4
  "edit-design": "edit screen",
5
5
  };
@@ -16,9 +16,9 @@ import {
16
16
 
17
17
  export default defineAction({
18
18
  description:
19
- "Change the coarse visibility of a shareable resource: 'private' keeps it owner-only, 'org' shares it with all members of the owner's organization, 'public' makes it accessible to anyone with the link. Owner or admin role required.",
20
- // (audit H5) Visibility changes are admin-tier and can flip a private
21
- // resource org-wide or public. Refuse from the tools iframe bridge.
19
+ "Change the coarse visibility of a shareable resource: 'private' keeps it owner-only, 'org' shares it with all members of the owner's organization, 'public' makes it accessible to anyone with the link. Visibility changes require owner or admin role.",
20
+ // (audit H5) Visibility changes can flip a private resource org-wide or
21
+ // public. Refuse from the tools iframe bridge.
22
22
  toolCallable: false,
23
23
  mcpApp: {
24
24
  compactCatalog: true,
@@ -184,8 +184,10 @@ These endpoints follow the same access model as `/api/public-recording`:
184
184
  raw provider URLs.
185
185
 
186
186
  The share popover's "Share with agents" field should copy the agent context URL,
187
- not raw transcript text. The context response points agents at the transcript and
188
- frame APIs so they can fetch only the visual context they need.
187
+ not raw transcript text. Its "Copy agent prompt" field may wrap that URL with
188
+ instructions to fetch transcripts, frames, and browser diagnostics, but it should
189
+ still point agents at the context response so they can fetch only the visual
190
+ context they need.
189
191
 
190
192
  ## View counting
191
193
 
@@ -324,6 +324,9 @@ function LinkTab({
324
324
  ? tokenizedAgentContextUrl
325
325
  : publicAgentContextUrl;
326
326
  const agentShareDisabled = isPending || !isPublic || !agentContextUrl;
327
+ const agentPrompt = agentContextUrl
328
+ ? t("shareDialog.agentPrompt", { agentContextUrl })
329
+ : "";
327
330
 
328
331
  return (
329
332
  <div className="space-y-4">
@@ -351,6 +354,12 @@ function LinkTab({
351
354
  disabled={agentShareDisabled}
352
355
  />
353
356
 
357
+ <CopyField
358
+ label={t("shareDialog.copyAgentPrompt")}
359
+ value={agentPrompt}
360
+ disabled={agentShareDisabled}
361
+ />
362
+
354
363
  {isPublic && hasPassword ? (
355
364
  <p className="text-xs text-muted-foreground">
356
365
  {t("shareDialog.agentTokenDescription")}
@@ -415,6 +415,9 @@ const messages = {
415
415
  embed: "تضمين",
416
416
  shareLink: "رابط المشاركة",
417
417
  shareWithAgents: "شارك مع الوكلاء",
418
+ copyAgentPrompt: "نسخ مطالبة الوكيل",
419
+ agentPrompt:
420
+ "اجلب عنوان URL لسياق وكيل Clips هذا: {{agentContextUrl}}. استخدم transcript.segments للسياق المنطوق، واجلب recommendedFrames أو عناوين URL الخاصة بواجهة API للإطارات لرؤية الشاشة، وتحقق من browserDiagnostics إن وجدت لسجلات وحدة التحكم المنقحة وبيانات طلبات fetch/XHR الوصفية.",
418
421
  agentTokenDescription:
419
422
  "يستخدم هذا الوكيل URL رمزًا مميزًا قصير العمر، بحيث يمكن للعملاء قراءة المقطع دون الكشف عن كلمة المرور.",
420
423
  gifPreview: "معاينة GIF",
@@ -432,6 +432,9 @@ const messages = {
432
432
  embed: "Einbetten",
433
433
  shareLink: "Teillink",
434
434
  shareWithAgents: "Mit Agenten teilen",
435
+ copyAgentPrompt: "Agent-Prompt kopieren",
436
+ agentPrompt:
437
+ "Rufe diese Clips-Agent-Kontext-URL ab: {{agentContextUrl}}. Verwende transcript.segments fuer den gesprochenen Kontext, rufe recommendedFrames oder die Frame-API-URLs ab, um den Bildschirm zu sehen, und pruefe browserDiagnostics, falls vorhanden, fuer redigierte Konsolenprotokolle und fetch/XHR-Anfragemetadaten.",
435
438
  agentTokenDescription:
436
439
  "Dieser Agent URL verwendet ein kurzlebiges Token, sodass Agenten den Clip lesen können, ohne das Passwort preiszugeben.",
437
440
  gifPreview: "GIF-Vorschau",
@@ -415,6 +415,9 @@ const messages = {
415
415
  embed: "Embed",
416
416
  shareLink: "Share link",
417
417
  shareWithAgents: "Share with agents",
418
+ copyAgentPrompt: "Copy agent prompt",
419
+ agentPrompt:
420
+ "Fetch this Clips agent context URL: {{agentContextUrl}}. Use transcript.segments for spoken context, fetch recommendedFrames or the frame API URLs to see the screen, and check browserDiagnostics if present for redacted console logs and fetch/XHR request metadata.",
418
421
  agentTokenDescription:
419
422
  "This agent URL uses a short-lived token, so agents can read the clip without exposing the password.",
420
423
  gifPreview: "GIF preview",
@@ -427,6 +427,9 @@ const messages = {
427
427
  embed: "Insertar",
428
428
  shareLink: "Enlace para compartir",
429
429
  shareWithAgents: "Compartir con agentes",
430
+ copyAgentPrompt: "Copiar indicación para agente",
431
+ agentPrompt:
432
+ "Obtén esta URL de contexto para agentes de Clips: {{agentContextUrl}}. Usa transcript.segments para el contexto hablado, obtén recommendedFrames o las URLs de la API de fotogramas para ver la pantalla y revisa browserDiagnostics si está presente para ver registros de consola redactados y metadatos de solicitudes fetch/XHR.",
430
433
  agentTokenDescription:
431
434
  "Este agente URL utiliza un token de corta duración, por lo que los agentes pueden leer el clip sin exponer la contraseña.",
432
435
  gifPreview: "vista previa de GIF",
@@ -427,6 +427,9 @@ const messages = {
427
427
  embed: "Intégrer",
428
428
  shareLink: "Lien de partage",
429
429
  shareWithAgents: "Partager avec les agents",
430
+ copyAgentPrompt: "Copier le prompt pour agent",
431
+ agentPrompt:
432
+ "Récupère cette URL de contexte Clips pour agent : {{agentContextUrl}}. Utilise transcript.segments pour le contexte parlé, récupère recommendedFrames ou les URLs de l'API d'images pour voir l'écran, et consulte browserDiagnostics s'il est présent pour les journaux de console expurgés et les métadonnées de requêtes fetch/XHR.",
430
433
  agentTokenDescription:
431
434
  "Cet agent URL utilise un jeton de courte durée, afin que les agents puissent lire le clip sans exposer le mot de passe.",
432
435
  gifPreview: "aperçu de GIF",
@@ -412,6 +412,9 @@ const messages = {
412
412
  embed: "एम्बेड",
413
413
  shareLink: "शेयर लिंक",
414
414
  shareWithAgents: "एजेंटों के साथ साझा करें",
415
+ copyAgentPrompt: "एजेंट प्रॉम्प्ट कॉपी करें",
416
+ agentPrompt:
417
+ "यह Clips एजेंट संदर्भ URL प्राप्त करें: {{agentContextUrl}}। बोले गए संदर्भ के लिए transcript.segments का उपयोग करें, स्क्रीन देखने के लिए recommendedFrames या फ्रेम API URL प्राप्त करें, और यदि browserDiagnostics मौजूद हो तो संशोधित कंसोल लॉग और fetch/XHR अनुरोध मेटाडेटा जांचें।",
415
418
  agentTokenDescription:
416
419
  "यह एजेंट URL एक अल्पकालिक टोकन का उपयोग करता है, ताकि एजेंट पासवर्ड उजागर किए बिना क्लिप पढ़ सकें।",
417
420
  gifPreview: "GIF पूर्वावलोकन",
@@ -424,6 +424,9 @@ const messages = {
424
424
  embed: "埋め込み",
425
425
  shareLink: "共有リンク",
426
426
  shareWithAgents: "エージェントと共有する",
427
+ copyAgentPrompt: "エージェント用プロンプトをコピー",
428
+ agentPrompt:
429
+ "この Clips エージェントコンテキスト URL を取得してください: {{agentContextUrl}}。音声の文脈には transcript.segments を使い、画面を見るために recommendedFrames またはフレーム API URL を取得し、browserDiagnostics がある場合は、編集済みのコンソールログと fetch/XHR リクエストのメタデータを確認してください。",
427
430
  agentTokenDescription:
428
431
  "このエージェント URL は有効期間の短いトークンを使用するため、エージェントはパスワードを公開せずにクリップを読み取ることができます。",
429
432
  gifPreview: "GIF プレビュー",
@@ -417,6 +417,9 @@ const messages = {
417
417
  embed: "임베드",
418
418
  shareLink: "공유 링크",
419
419
  shareWithAgents: "상담원과 공유",
420
+ copyAgentPrompt: "에이전트 프롬프트 복사",
421
+ agentPrompt:
422
+ "이 Clips 에이전트 컨텍스트 URL을 가져오세요: {{agentContextUrl}}. 말한 내용의 맥락은 transcript.segments를 사용하고, 화면을 보기 위해 recommendedFrames 또는 프레임 API URL을 가져오며, browserDiagnostics가 있으면 수정된 콘솔 로그와 fetch/XHR 요청 메타데이터를 확인하세요.",
420
423
  agentTokenDescription:
421
424
  "이 에이전트 URL는 수명이 짧은 토큰을 사용하므로 에이전트는 비밀번호를 노출하지 않고도 클립을 읽을 수 있습니다.",
422
425
  gifPreview: "GIF 미리보기",
@@ -425,6 +425,9 @@ const messages = {
425
425
  embed: "Incorporar",
426
426
  shareLink: "Link de compartilhamento",
427
427
  shareWithAgents: "Compartilhe com agentes",
428
+ copyAgentPrompt: "Copiar prompt para agente",
429
+ agentPrompt:
430
+ "Busque esta URL de contexto para agentes do Clips: {{agentContextUrl}}. Use transcript.segments para o contexto falado, busque recommendedFrames ou as URLs da API de quadros para ver a tela e confira browserDiagnostics, se presente, para logs de console redigidos e metadados de solicitações fetch/XHR.",
428
431
  agentTokenDescription:
429
432
  "Este agente URL usa um token de curta duração, para que os agentes possam ler o clipe sem expor a senha.",
430
433
  gifPreview: "visualização de GIF",
@@ -399,6 +399,9 @@ const messages = {
399
399
  embed: "嵌入",
400
400
  shareLink: "分享链接",
401
401
  shareWithAgents: "与代理商分享",
402
+ copyAgentPrompt: "复制代理提示",
403
+ agentPrompt:
404
+ "获取这个 Clips 代理上下文 URL:{{agentContextUrl}}。使用 transcript.segments 读取语音上下文,获取 recommendedFrames 或帧 API URL 来查看屏幕,并在 browserDiagnostics 存在时检查经过脱敏的控制台日志和 fetch/XHR 请求元数据。",
402
405
  agentTokenDescription:
403
406
  "该代理 URL 使用短期令牌,因此代理可以在不暴露密码的情况下读取剪辑。",
404
407
  gifPreview: "GIF 预览",
@@ -399,6 +399,9 @@ const messages = {
399
399
  embed: "嵌入",
400
400
  shareLink: "分享連結",
401
401
  shareWithAgents: "與 Agent 分享",
402
+ copyAgentPrompt: "複製 Agent 提示",
403
+ agentPrompt:
404
+ "取得這個 Clips Agent 脈絡 URL:{{agentContextUrl}}。使用 transcript.segments 讀取語音脈絡,取得 recommendedFrames 或影格 API URL 來查看螢幕,並在 browserDiagnostics 存在時檢查已遮蔽的主控台記錄和 fetch/XHR 請求中繼資料。",
402
405
  agentTokenDescription:
403
406
  "此 Agent URL 使用短期權杖,因此 Agent 可以在不暴露密碼的情況下讀取剪輯。",
404
407
  gifPreview: "GIF 預覽",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Shared clips can now copy a ready-to-send prompt that tells agents how to read transcripts, frames, and diagnostics.
@@ -57,6 +57,14 @@ function escapeHtml(value: string): string {
57
57
  .replace(/"/g, "&quot;");
58
58
  }
59
59
 
60
+ function createInsertedNodeId(prefix: string): string {
61
+ const random =
62
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
63
+ ? crypto.randomUUID().replace(/-/g, "").slice(0, 12)
64
+ : Math.random().toString(36).slice(2, 14);
65
+ return `inserted-${prefix}-${random}`;
66
+ }
67
+
60
68
  function insertBeforeClosingTag(
61
69
  html: string,
62
70
  closingTag: "main" | "body",
@@ -70,6 +78,7 @@ function insertBeforeClosingTag(
70
78
  function appendAssetMarkup(
71
79
  html: string,
72
80
  args: z.infer<typeof schemaInput>,
81
+ nodeId: string,
73
82
  ): string {
74
83
  const label = args.title?.trim() || args.altText?.trim() || "Generated asset";
75
84
  const escapedUrl = escapeHtml(args.assetUrl);
@@ -82,7 +91,7 @@ function appendAssetMarkup(
82
91
  ? `<video src="${escapedUrl}" controls class="w-full rounded-xl object-cover"></video>`
83
92
  : `<img src="${escapedUrl}" alt="${escapeHtml(args.altText?.trim() || label)}" class="w-full rounded-xl object-cover" />`;
84
93
  const snippet = `
85
- <section class="mx-auto my-8 max-w-5xl px-4" data-agent-native-asset${assetIdAttr}>
94
+ <section class="mx-auto my-8 max-w-5xl px-4" data-agent-native-asset data-agent-native-node-id="${escapeHtml(nodeId)}" data-agent-native-layer-name="${escapedLabel}"${assetIdAttr}>
86
95
  <figure class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
87
96
  ${media}
88
97
  <figcaption class="px-4 py-3 text-sm text-slate-600">${escapedLabel}</figcaption>
@@ -191,7 +200,8 @@ export default defineAction({
191
200
  // Collab read is best-effort; fall back to stored content.
192
201
  }
193
202
 
194
- const content = appendAssetMarkup(base, args);
203
+ const insertedNodeId = createInsertedNodeId("asset");
204
+ const content = appendAssetMarkup(base, args, insertedNodeId);
195
205
  const now = new Date().toISOString();
196
206
  await db
197
207
  .update(schema.designFiles)
@@ -213,6 +223,8 @@ export default defineAction({
213
223
  fileId: file.id,
214
224
  filename: file.filename,
215
225
  inserted: true,
226
+ insertedNodeId,
227
+ insertedSelector: `[data-agent-native-node-id="${insertedNodeId}"]`,
216
228
  assetId: args.assetId ?? null,
217
229
  assetUrl: args.assetUrl,
218
230
  };
@@ -50,9 +50,17 @@ function insertBeforeClosingTag(
50
50
  return html.replace(pattern, `${snippet}\n</${closingTag}>`);
51
51
  }
52
52
 
53
- function nativeSnippet(kind: DesignNativeAssetKind): string {
53
+ function createInsertedNodeId(prefix: string): string {
54
+ const random =
55
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
56
+ ? crypto.randomUUID().replace(/-/g, "").slice(0, 12)
57
+ : Math.random().toString(36).slice(2, 14);
58
+ return `inserted-${prefix}-${random}`;
59
+ }
60
+
61
+ function nativeSnippet(kind: DesignNativeAssetKind, nodeId: string): string {
54
62
  const attrs = (componentName: string) =>
55
- `data-agent-native-native-asset data-agent-native-component="${componentName}" data-agent-native-layer-name="${componentName}"`;
63
+ `data-agent-native-native-asset data-agent-native-node-id="${nodeId}" data-agent-native-component="${componentName}" data-agent-native-layer-name="${componentName}"`;
56
64
  switch (kind) {
57
65
  case "section-frame":
58
66
  return `
@@ -137,8 +145,9 @@ function nativeSnippet(kind: DesignNativeAssetKind): string {
137
145
  function appendNativeAssetMarkup(
138
146
  html: string,
139
147
  kind: DesignNativeAssetKind,
148
+ nodeId: string,
140
149
  ): string {
141
- const snippet = nativeSnippet(kind);
150
+ const snippet = nativeSnippet(kind, nodeId);
142
151
  return (
143
152
  insertBeforeClosingTag(html, "main", snippet) ??
144
153
  insertBeforeClosingTag(html, "body", snippet) ??
@@ -239,7 +248,8 @@ export default defineAction({
239
248
  // Collab read is best-effort; fall back to stored content.
240
249
  }
241
250
 
242
- const content = appendNativeAssetMarkup(base, args.kind);
251
+ const insertedNodeId = createInsertedNodeId("native");
252
+ const content = appendNativeAssetMarkup(base, args.kind, insertedNodeId);
243
253
  const now = new Date().toISOString();
244
254
  await db
245
255
  .update(schema.designFiles)
@@ -261,6 +271,8 @@ export default defineAction({
261
271
  fileId: file.id,
262
272
  filename: file.filename,
263
273
  inserted: true,
274
+ insertedNodeId,
275
+ insertedSelector: `[data-agent-native-node-id="${insertedNodeId}"]`,
264
276
  source: "design-native",
265
277
  kind: args.kind,
266
278
  };
@@ -66,6 +66,14 @@ function escapeHtml(value: string): string {
66
66
  .replace(/"/g, "&quot;");
67
67
  }
68
68
 
69
+ function createInsertedNodeId(prefix: string): string {
70
+ const random =
71
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
72
+ ? crypto.randomUUID().replace(/-/g, "").slice(0, 12)
73
+ : Math.random().toString(36).slice(2, 14);
74
+ return `inserted-${prefix}-${random}`;
75
+ }
76
+
69
77
  function insertBeforeClosingTag(
70
78
  html: string,
71
79
  closingTag: "main" | "body",
@@ -83,6 +91,7 @@ function optionalDataAttribute(name: string, value: string | undefined) {
83
91
  function appendFigmaAssetMarkup(
84
92
  html: string,
85
93
  args: z.infer<typeof schemaInput>,
94
+ nodeId: string,
86
95
  ): string {
87
96
  const label = args.name?.trim() || "Figma library asset";
88
97
  const description = args.description?.trim();
@@ -90,7 +99,7 @@ function appendFigmaAssetMarkup(
90
99
  ? `<a href="${escapeHtml(args.sourceUrl)}" target="_blank" rel="noreferrer" class="text-slate-500 underline decoration-slate-300 underline-offset-2 hover:text-slate-800">Open in Figma</a>`
91
100
  : "";
92
101
  const snippet = `
93
- <section class="mx-auto my-8 max-w-5xl px-4" data-agent-native-asset-source="figma" data-agent-native-figma-asset data-figma-file-key="${escapeHtml(args.fileKey)}"${optionalDataAttribute("data-figma-node-id", args.nodeId)}${optionalDataAttribute("data-figma-component-key", args.componentKey)} data-figma-asset-kind="${escapeHtml(args.kind)}">
102
+ <section class="mx-auto my-8 max-w-5xl px-4" data-agent-native-asset-source="figma" data-agent-native-figma-asset data-agent-native-node-id="${escapeHtml(nodeId)}" data-agent-native-layer-name="${escapeHtml(label)}" data-figma-file-key="${escapeHtml(args.fileKey)}"${optionalDataAttribute("data-figma-node-id", args.nodeId)}${optionalDataAttribute("data-figma-component-key", args.componentKey)} data-figma-asset-kind="${escapeHtml(args.kind)}">
94
103
  <figure class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
95
104
  <img src="${escapeHtml(args.renderUrl)}" alt="${escapeHtml(label)}" class="w-full rounded-t-2xl object-contain" />
96
105
  <figcaption class="flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-3 text-sm text-slate-600">
@@ -201,7 +210,8 @@ export default defineAction({
201
210
  // Collab read is best-effort; fall back to stored content.
202
211
  }
203
212
 
204
- const content = appendFigmaAssetMarkup(base, args);
213
+ const insertedNodeId = createInsertedNodeId("figma");
214
+ const content = appendFigmaAssetMarkup(base, args, insertedNodeId);
205
215
  const now = new Date().toISOString();
206
216
  await db
207
217
  .update(schema.designFiles)
@@ -223,6 +233,8 @@ export default defineAction({
223
233
  fileId: file.id,
224
234
  filename: file.filename,
225
235
  inserted: true,
236
+ insertedNodeId,
237
+ insertedSelector: `[data-agent-native-node-id="${insertedNodeId}"]`,
226
238
  source: "figma",
227
239
  fileKey: args.fileKey,
228
240
  nodeId: args.nodeId ?? null,
@@ -1775,6 +1775,11 @@ export function DesignCanvas({
1775
1775
  // explicit viewport width (e.g. 390 / 768 / 1280 side-by-side breakpoints).
1776
1776
  const resolvedWidth =
1777
1777
  previewWidthPx != null ? `${previewWidthPx}px` : iframeWidth;
1778
+ const focusScrollSurface = useCallback(() => {
1779
+ const surface = scrollContainerRef.current;
1780
+ if (!surface || document.activeElement === surface) return;
1781
+ surface.focus({ preventScroll: true });
1782
+ }, []);
1778
1783
 
1779
1784
  // Wrap the iframe in a positioned container so DrawOverlay /
1780
1785
  // CanvasCommentPins can absolutely-position themselves on top of the
@@ -1901,6 +1906,9 @@ export function DesignCanvas({
1901
1906
  return (
1902
1907
  <div
1903
1908
  ref={scrollContainerRef}
1909
+ tabIndex={-1}
1910
+ onPointerEnter={focusScrollSurface}
1911
+ onMouseEnter={focusScrollSurface}
1904
1912
  className="relative h-full w-full overflow-hidden"
1905
1913
  >
1906
1914
  {iframeElement}
@@ -1915,6 +1923,9 @@ export function DesignCanvas({
1915
1923
  return (
1916
1924
  <div
1917
1925
  ref={scrollContainerRef}
1926
+ tabIndex={-1}
1927
+ onPointerEnter={focusScrollSurface}
1928
+ onMouseEnter={focusScrollSurface}
1918
1929
  className="relative h-full w-full overflow-hidden"
1919
1930
  style={{
1920
1931
  width: embeddedFrame.displayWidth,
@@ -1945,6 +1956,9 @@ export function DesignCanvas({
1945
1956
  return (
1946
1957
  <div
1947
1958
  ref={scrollContainerRef}
1959
+ tabIndex={-1}
1960
+ onPointerEnter={focusScrollSurface}
1961
+ onMouseEnter={focusScrollSurface}
1948
1962
  className="relative flex-1 h-full overflow-auto"
1949
1963
  >
1950
1964
  {/* Canvas area. "none" mode fills the canvas (responsive preview);