@agent-native/core 0.84.18 → 0.84.21

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 (157) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/caller-auth.ts +37 -8
  5. package/corpus/core/src/a2a/client.ts +178 -67
  6. package/corpus/core/src/agent/production-agent.ts +19 -3
  7. package/corpus/core/src/agent/run-store.ts +180 -13
  8. package/corpus/core/src/agent/types.ts +1 -1
  9. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  10. package/corpus/core/src/cli/recap.ts +43 -34
  11. package/corpus/core/src/client/AssistantChat.tsx +49 -0
  12. package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
  13. package/corpus/core/src/client/active-run-state.ts +30 -0
  14. package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
  15. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +41 -3
  16. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +133 -0
  17. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -2
  18. package/corpus/core/src/client/i18n.tsx +10 -5
  19. package/corpus/core/src/client/sse-event-processor.ts +147 -5
  20. package/corpus/core/src/extensions/html-shell.ts +17 -3
  21. package/corpus/core/src/extensions/theme.ts +39 -0
  22. package/corpus/core/src/file-upload/builder.ts +35 -14
  23. package/corpus/core/src/file-upload/types.ts +3 -0
  24. package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
  25. package/corpus/core/src/mcp/builtin-tools.ts +3 -0
  26. package/corpus/core/src/mcp/org-directory.ts +66 -20
  27. package/corpus/core/src/scripts/call-agent.ts +7 -2
  28. package/corpus/templates/analytics/actions/provider-api-request.ts +8 -1
  29. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -1
  30. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +91 -33
  31. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
  32. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
  33. package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
  34. package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
  35. package/corpus/templates/clips/actions/create-recording.ts +7 -2
  36. package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
  37. package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
  38. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
  39. package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
  40. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
  41. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
  42. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
  43. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +65 -1
  44. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
  45. package/corpus/templates/clips/app/routes/record.tsx +48 -8
  46. package/corpus/templates/clips/changelog/2026-07-01-chrome-recording-uploads-now-serialize-chunks-to-avoid-resum.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
  55. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  56. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
  57. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +94 -29
  58. package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
  59. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
  60. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -1
  61. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
  62. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
  63. package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
  64. package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
  65. package/corpus/templates/plan/app/global.css +1 -1
  66. package/dist/a2a/caller-auth.d.ts +1 -0
  67. package/dist/a2a/caller-auth.d.ts.map +1 -1
  68. package/dist/a2a/caller-auth.js +34 -8
  69. package/dist/a2a/caller-auth.js.map +1 -1
  70. package/dist/a2a/client.d.ts +4 -0
  71. package/dist/a2a/client.d.ts.map +1 -1
  72. package/dist/a2a/client.js +148 -58
  73. package/dist/a2a/client.js.map +1 -1
  74. package/dist/agent/production-agent.d.ts.map +1 -1
  75. package/dist/agent/production-agent.js +19 -4
  76. package/dist/agent/production-agent.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +19 -6
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +163 -13
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/agent/types.d.ts +1 -0
  82. package/dist/agent/types.d.ts.map +1 -1
  83. package/dist/agent/types.js.map +1 -1
  84. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  85. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  86. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  87. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  88. package/dist/cli/recap.d.ts +3 -3
  89. package/dist/cli/recap.d.ts.map +1 -1
  90. package/dist/cli/recap.js +41 -34
  91. package/dist/cli/recap.js.map +1 -1
  92. package/dist/client/AssistantChat.d.ts +2 -0
  93. package/dist/client/AssistantChat.d.ts.map +1 -1
  94. package/dist/client/AssistantChat.js +36 -2
  95. package/dist/client/AssistantChat.js.map +1 -1
  96. package/dist/client/PoweredByBadge.d.ts +9 -3
  97. package/dist/client/PoweredByBadge.d.ts.map +1 -1
  98. package/dist/client/PoweredByBadge.js +47 -15
  99. package/dist/client/PoweredByBadge.js.map +1 -1
  100. package/dist/client/active-run-state.d.ts +3 -0
  101. package/dist/client/active-run-state.d.ts.map +1 -1
  102. package/dist/client/active-run-state.js +24 -0
  103. package/dist/client/active-run-state.js.map +1 -1
  104. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  105. package/dist/client/agent-chat-adapter.js +5 -1
  106. package/dist/client/agent-chat-adapter.js.map +1 -1
  107. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  108. package/dist/client/extensions/EmbeddedExtension.js +32 -3
  109. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  110. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  111. package/dist/client/extensions/ExtensionViewer.js +59 -2
  112. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  113. package/dist/client/extensions/ExtensionsSidebarSection.js +2 -2
  114. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  115. package/dist/client/i18n.d.ts +1 -1
  116. package/dist/client/i18n.d.ts.map +1 -1
  117. package/dist/client/i18n.js +6 -2
  118. package/dist/client/i18n.js.map +1 -1
  119. package/dist/client/sse-event-processor.d.ts +2 -0
  120. package/dist/client/sse-event-processor.d.ts.map +1 -1
  121. package/dist/client/sse-event-processor.js +121 -5
  122. package/dist/client/sse-event-processor.js.map +1 -1
  123. package/dist/collab/awareness.d.ts +2 -2
  124. package/dist/collab/awareness.d.ts.map +1 -1
  125. package/dist/collab/routes.d.ts +1 -1
  126. package/dist/extensions/html-shell.d.ts.map +1 -1
  127. package/dist/extensions/html-shell.js +17 -3
  128. package/dist/extensions/html-shell.js.map +1 -1
  129. package/dist/extensions/theme.d.ts +9 -0
  130. package/dist/extensions/theme.d.ts.map +1 -1
  131. package/dist/extensions/theme.js +38 -0
  132. package/dist/extensions/theme.js.map +1 -1
  133. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  134. package/dist/file-upload/builder.d.ts.map +1 -1
  135. package/dist/file-upload/builder.js +22 -8
  136. package/dist/file-upload/builder.js.map +1 -1
  137. package/dist/file-upload/types.d.ts +5 -1
  138. package/dist/file-upload/types.d.ts.map +1 -1
  139. package/dist/file-upload/types.js.map +1 -1
  140. package/dist/integrations/a2a-continuation-processor.js +43 -18
  141. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  142. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  143. package/dist/mcp/builtin-tools.js +3 -0
  144. package/dist/mcp/builtin-tools.js.map +1 -1
  145. package/dist/mcp/org-directory.d.ts.map +1 -1
  146. package/dist/mcp/org-directory.js +58 -20
  147. package/dist/mcp/org-directory.js.map +1 -1
  148. package/dist/notifications/routes.d.ts +3 -3
  149. package/dist/observability/routes.d.ts +6 -6
  150. package/dist/progress/routes.d.ts +1 -1
  151. package/dist/resources/handlers.d.ts +3 -3
  152. package/dist/scripts/call-agent.d.ts.map +1 -1
  153. package/dist/scripts/call-agent.js +2 -2
  154. package/dist/scripts/call-agent.js.map +1 -1
  155. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  156. package/dist/server/transcribe-voice.d.ts +1 -1
  157. package/package.json +1 -1
@@ -2,6 +2,7 @@ import {
2
2
  IconArrowLeft,
3
3
  IconArrowBackUp,
4
4
  IconChevronRight,
5
+ IconCode,
5
6
  IconDotsVertical,
6
7
  IconHistory,
7
8
  IconLoader2,
@@ -20,6 +21,7 @@ import { getThemeVars } from "../../extensions/theme.js";
20
21
  import { sendToAgentChat } from "../agent-chat.js";
21
22
  import { AgentToggleButton } from "../AgentPanel.js";
22
23
  import { agentNativePath, appPath } from "../api-path.js";
24
+ import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog.js";
23
25
  import {
24
26
  Popover,
25
27
  PopoverContent,
@@ -324,6 +326,131 @@ function applyCanonicalLink(path: string): () => void {
324
326
  };
325
327
  }
326
328
 
329
+ function SourceCodeDialog({
330
+ extension,
331
+ onSaved,
332
+ }: {
333
+ extension: Extension;
334
+ onSaved?: () => void;
335
+ }) {
336
+ const queryClient = useQueryClient();
337
+ const [open, setOpen] = useState(false);
338
+ const [code, setCode] = useState(extension.content ?? "");
339
+ const [saving, setSaving] = useState(false);
340
+ const [error, setError] = useState<string | null>(null);
341
+
342
+ // Sync in when the dialog opens, or when the viewed extension changes
343
+ // while the dialog stays mounted (e.g. re-parented to a different id).
344
+ useEffect(() => {
345
+ if (open) setCode(extension.content ?? "");
346
+ }, [open, extension.id]);
347
+
348
+ const isDirty = code !== (extension.content ?? "");
349
+
350
+ // Block Escape / outside-click from closing while there are unsaved edits.
351
+ const handleOpenChange = (next: boolean) => {
352
+ if (!next && isDirty) return;
353
+ setOpen(next);
354
+ if (!next) setError(null);
355
+ };
356
+
357
+ const handleCancel = () => {
358
+ setCode(extension.content ?? "");
359
+ setOpen(false);
360
+ setError(null);
361
+ };
362
+
363
+ const handleSave = async () => {
364
+ setSaving(true);
365
+ setError(null);
366
+ try {
367
+ const res = await fetch(
368
+ agentNativePath(`/_agent-native/extensions/${extension.id}`),
369
+ {
370
+ method: "PUT",
371
+ headers: { "Content-Type": "application/json" },
372
+ body: JSON.stringify({ content: code }),
373
+ },
374
+ );
375
+ if (!res.ok) {
376
+ const body = await res.json().catch(() => ({}));
377
+ throw new Error(body?.error ?? `Save failed (${res.status})`);
378
+ }
379
+ setOpen(false);
380
+ queryClient.setQueryData<Extension>(["extension", extension.id], (old) =>
381
+ old ? { ...old, content: code } : old,
382
+ );
383
+ queryClient.invalidateQueries({
384
+ queryKey: ["extension", extension.id],
385
+ });
386
+ queryClient.invalidateQueries({ queryKey: ["extensions"] });
387
+ onSaved?.();
388
+ } catch (err: any) {
389
+ setError(err?.message ?? "Save failed");
390
+ } finally {
391
+ setSaving(false);
392
+ }
393
+ };
394
+
395
+ return (
396
+ <>
397
+ <Tooltip>
398
+ <TooltipTrigger asChild>
399
+ <button
400
+ type="button"
401
+ onClick={() => setOpen(true)}
402
+ className="inline-flex items-center justify-center rounded-md h-8 w-8 text-muted-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer"
403
+ >
404
+ <IconCode className="h-4 w-4" />
405
+ </button>
406
+ </TooltipTrigger>
407
+ <TooltipContent>View / edit source</TooltipContent>
408
+ </Tooltip>
409
+ <Dialog open={open} onOpenChange={handleOpenChange}>
410
+ <DialogContent className="flex h-[85vh] w-[90vw] max-w-[900px] flex-col gap-0 overflow-hidden p-0">
411
+ <div className="flex shrink-0 items-center border-b border-border px-5 py-3 pr-12">
412
+ <DialogTitle className="truncate text-sm font-medium">
413
+ {extension.name} — source
414
+ </DialogTitle>
415
+ </div>
416
+ <textarea
417
+ className="flex-1 resize-none bg-muted/40 px-5 py-4 font-mono text-xs leading-relaxed text-foreground focus:outline-none"
418
+ value={code}
419
+ onChange={(e) => setCode(e.target.value)}
420
+ spellCheck={false}
421
+ />
422
+ <div className="flex shrink-0 items-center justify-between border-t border-border px-5 py-3">
423
+ {error ? (
424
+ <p className="text-xs text-destructive">{error}</p>
425
+ ) : (
426
+ <span className="text-xs text-muted-foreground">
427
+ Alpine.js / HTML &middot; {code.length.toLocaleString()} chars
428
+ </span>
429
+ )}
430
+ <div className="flex items-center gap-2">
431
+ <button
432
+ type="button"
433
+ onClick={handleCancel}
434
+ className="inline-flex h-8 cursor-pointer items-center rounded-md border border-input px-3 text-xs hover:bg-accent"
435
+ >
436
+ Cancel
437
+ </button>
438
+ <button
439
+ type="button"
440
+ onClick={handleSave}
441
+ disabled={saving}
442
+ className="inline-flex h-8 cursor-pointer items-center rounded-md bg-primary px-4 text-xs font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
443
+ >
444
+ {saving ? "Saving…" : "Save"}
445
+ </button>
446
+ </div>
447
+ </div>
448
+ </DialogContent>
449
+ </Dialog>
450
+ </>
451
+ );
452
+ }
453
+
327
454
  function EditToolPopover({
328
455
  extension,
329
456
  onOpenChange,
@@ -1157,6 +1284,12 @@ export function ExtensionViewer({ extensionId }: ExtensionViewerProps) {
1157
1284
  onRestored={() => setRefreshKey((k) => k + 1)}
1158
1285
  onOpenChange={onPopoverOpenChange}
1159
1286
  />
1287
+ {extension.canEdit && (
1288
+ <SourceCodeDialog
1289
+ extension={extension}
1290
+ onSaved={() => setRefreshKey((k) => k + 1)}
1291
+ />
1292
+ )}
1160
1293
  <EditToolPopover
1161
1294
  extension={extension}
1162
1295
  onOpenChange={onPopoverOpenChange}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IconChevronDown,
3
3
  IconPlus,
4
- IconSettings,
4
+ IconFilter,
5
5
  IconStar,
6
6
  IconStarFilled,
7
7
  IconTrash,
@@ -570,7 +570,7 @@ function ExtensionSortMenu({
570
570
  className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-muted-foreground/45 opacity-0 transition-all hover:bg-accent hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring group-hover/extensions-section:opacity-100"
571
571
  aria-label={copy.sortOptions}
572
572
  >
573
- <IconSettings className="h-3.5 w-3.5" />
573
+ <IconFilter className="h-3.5 w-3.5" />
574
574
  </button>
575
575
  </DropdownMenuTrigger>
576
576
  </TooltipTrigger>
@@ -674,7 +674,7 @@ export function LanguagePicker({
674
674
  className?: string;
675
675
  includeSystem?: boolean;
676
676
  label?: string;
677
- variant?: "select" | "icon";
677
+ variant?: "select" | "icon" | "ghost-icon";
678
678
  }) {
679
679
  const { locale, preference, setPreference } = useLocale();
680
680
  const [open, setOpen] = useState(false);
@@ -716,10 +716,15 @@ export function LanguagePicker({
716
716
  title={triggerLabel}
717
717
  data-language-picker-trigger
718
718
  className={cn(
719
- "shrink-0 rounded-md border border-border bg-background text-foreground outline-none transition-colors hover:border-foreground/30 hover:bg-accent/40 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=open]:border-foreground/30 data-[state=open]:bg-accent/40",
719
+ "shrink-0 rounded-md outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
720
+ variant === "ghost-icon"
721
+ ? "flex h-9 w-9 items-center justify-center text-muted-foreground hover:bg-accent hover:text-foreground data-[state=open]:bg-accent data-[state=open]:text-foreground"
722
+ : "border border-border bg-background text-foreground hover:border-foreground/30 hover:bg-accent/40 hover:text-foreground data-[state=open]:border-foreground/30 data-[state=open]:bg-accent/40",
720
723
  variant === "icon"
721
724
  ? "flex h-8 w-8 items-center justify-center"
722
- : "flex h-9 w-full items-center justify-between gap-2 px-3 text-start text-sm",
725
+ : variant === "select"
726
+ ? "flex h-9 w-full items-center justify-between gap-2 px-3 text-start text-sm"
727
+ : null,
723
728
  )}
724
729
  >
725
730
  <span className="flex min-w-0 items-center gap-2">
@@ -740,12 +745,12 @@ export function LanguagePicker({
740
745
  </PopoverPrimitive.Trigger>
741
746
  <PopoverPrimitive.Portal>
742
747
  <PopoverPrimitive.Content
743
- align={variant === "icon" ? "end" : "start"}
748
+ align={variant === "select" ? "start" : "end"}
744
749
  sideOffset={6}
745
750
  role="menu"
746
751
  className={cn(
747
752
  "z-[9999] max-h-[min(20rem,var(--radix-popover-content-available-height))] overflow-y-auto rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none will-change-[transform,opacity] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:duration-100 data-[state=open]:duration-150 data-[state=closed]:ease-in data-[state=open]:ease-out data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
748
- variant === "icon"
753
+ variant === "icon" || variant === "ghost-icon"
749
754
  ? "min-w-56"
750
755
  : "w-[min(20rem,calc(100vw-2rem))] min-w-[var(--radix-popover-trigger-width)]",
751
756
  )}
@@ -49,6 +49,7 @@ export interface SSEEvent {
49
49
  /** Server-assigned call identifier emitted on tool_start / tool_done events. */
50
50
  id?: string;
51
51
  label?: string;
52
+ progressBytes?: number;
52
53
  input?: Record<string, string>;
53
54
  result?: string;
54
55
  isError?: boolean;
@@ -144,9 +145,58 @@ export class AgentAutoContinueSignal extends Error {
144
145
  }
145
146
 
146
147
  export const SSE_NO_PROGRESS_TIMEOUT_MS = 75_000;
148
+ export const SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS = 90_000;
147
149
 
148
150
  type ActivityTrailEntry = AgentActivityTrailEntry;
149
151
 
152
+ type PreparingActionState = {
153
+ tool?: string;
154
+ startedAt?: number;
155
+ /**
156
+ * Timestamp of the last real streaming progress for the in-preparation tool
157
+ * input. The server emits a throttled `activity` heartbeat per
158
+ * `tool-input-delta`, so while the model is actively streaming a (possibly
159
+ * very large) tool argument this keeps advancing. The stall guard measures
160
+ * silence from HERE — not from `startedAt` — so a legitimately large, still-
161
+ * streaming input is never aborted; only genuine silence (keepalive-only, no
162
+ * further deltas) can trip it.
163
+ */
164
+ lastProgressAt?: number;
165
+ };
166
+
167
+ function formatProgressBytes(bytes: number): string {
168
+ if (bytes < 1024) return `${bytes} B`;
169
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
170
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
171
+ }
172
+
173
+ function activityProgressBytes(ev: SSEEvent): number | undefined {
174
+ return typeof ev.progressBytes === "number" &&
175
+ Number.isFinite(ev.progressBytes) &&
176
+ ev.progressBytes >= 0
177
+ ? Math.floor(ev.progressBytes)
178
+ : undefined;
179
+ }
180
+
181
+ function isPreparingActionActivity(ev: SSEEvent): boolean {
182
+ if (ev.type !== "activity") return false;
183
+ const label = (ev.label ?? "").trim().toLowerCase();
184
+ return label.startsWith("preparing ") && label.includes(" action");
185
+ }
186
+
187
+ function baseActivityLabel(ev: SSEEvent, tool?: string): string {
188
+ return humanizeToolLabelText(ev.label ?? "Working", tool);
189
+ }
190
+
191
+ function visibleActivityLabel(ev: SSEEvent, tool?: string): string {
192
+ const label = baseActivityLabel(ev, tool);
193
+ const progressBytes = activityProgressBytes(ev);
194
+ if (progressBytes === undefined || !isPreparingActionActivity(ev)) {
195
+ return label;
196
+ }
197
+ return `${label} (${formatProgressBytes(progressBytes)} streamed)`;
198
+ }
199
+
150
200
  function findPendingToolCallIndex(
151
201
  content: ContentPart[],
152
202
  toolName: string,
@@ -199,6 +249,53 @@ function appendActivityTrail(
199
249
  }
200
250
  }
201
251
 
252
+ function updatePreparingActionState(
253
+ state: PreparingActionState,
254
+ ev: SSEEvent,
255
+ now: number,
256
+ ) {
257
+ if (ev.type === "activity" && isPreparingActionActivity(ev)) {
258
+ const tool = ev.tool?.trim() || undefined;
259
+ if (!tool) return;
260
+ if (state.tool !== tool || state.startedAt === undefined) {
261
+ state.tool = tool;
262
+ state.startedAt = now;
263
+ }
264
+ // Every tool-input activity is a throttled proof the model is still
265
+ // streaming this action's argument. Treat it as progress so large inputs
266
+ // that take many seconds/minutes to stream are NOT flagged as stalled.
267
+ state.lastProgressAt = now;
268
+ return;
269
+ }
270
+
271
+ if (
272
+ ev.type === "clear" ||
273
+ ev.type === "text" ||
274
+ ev.type === "tool_start" ||
275
+ ev.type === "tool_done" ||
276
+ ev.type === "done" ||
277
+ ev.type === "error" ||
278
+ ev.type === "missing_api_key"
279
+ ) {
280
+ state.tool = undefined;
281
+ state.startedAt = undefined;
282
+ state.lastProgressAt = undefined;
283
+ }
284
+ }
285
+
286
+ function hasStalledPreparingAction(state: PreparingActionState, now: number) {
287
+ // Fire only when a tool input has gone SILENT — no further streaming deltas
288
+ // for the whole window — never merely because a large input has been
289
+ // streaming for a long time. `lastProgressAt` advances on every delta
290
+ // heartbeat, so an actively-streaming large output keeps resetting this and
291
+ // survives; a genuinely stuck prep (keepalive-only, no deltas) trips it.
292
+ return (
293
+ state.tool !== undefined &&
294
+ state.lastProgressAt !== undefined &&
295
+ now - state.lastProgressAt >= SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS
296
+ );
297
+ }
298
+
202
299
  async function readChunkWithProgressTimeout(
203
300
  reader: ReadableStreamDefaultReader<Uint8Array>,
204
301
  lastMeaningfulEventAt: number,
@@ -491,7 +588,7 @@ export function processEvent(
491
588
 
492
589
  if (ev.type === "activity") {
493
590
  const tool = ev.tool?.trim() || undefined;
494
- const label = humanizeToolLabelText(ev.label ?? "Working", tool);
591
+ const label = visibleActivityLabel(ev, tool);
495
592
  if (typeof window !== "undefined") {
496
593
  window.dispatchEvent(
497
594
  new CustomEvent("agent-chat:activity", {
@@ -941,6 +1038,7 @@ export async function* readSSEStream(
941
1038
  let buf = "";
942
1039
  let lastMeaningfulEventAt = Date.now();
943
1040
  const activityTrail: ActivityTrailEntry[] = [];
1041
+ const preparingActionState: PreparingActionState = {};
944
1042
  const processEventState: ProcessEventState = {
945
1043
  completedToolsAfterLastAssistantText: new Set(),
946
1044
  };
@@ -1000,7 +1098,9 @@ export async function* readSSEStream(
1000
1098
  continue;
1001
1099
  }
1002
1100
  sawDataEvent = true;
1003
- lastMeaningfulEventAt = Date.now();
1101
+ const now = Date.now();
1102
+ lastMeaningfulEventAt = now;
1103
+ updatePreparingActionState(preparingActionState, ev, now);
1004
1104
 
1005
1105
  // Track sequence number for reconnection
1006
1106
  if (ev.seq !== undefined && onSeq) {
@@ -1012,7 +1112,7 @@ export async function* readSSEStream(
1012
1112
  } else if (ev.type === "activity") {
1013
1113
  const tool = ev.tool?.trim() || undefined;
1014
1114
  appendActivityTrail(activityTrail, {
1015
- label: humanizeToolLabelText(ev.label ?? "Working", tool),
1115
+ label: baseActivityLabel(ev, tool),
1016
1116
  ...(tool ? { tool } : {}),
1017
1117
  });
1018
1118
  } else if (ev.type === "tool_start") {
@@ -1039,6 +1139,12 @@ export async function* readSSEStream(
1039
1139
  );
1040
1140
 
1041
1141
  if (result) yield withStreamMetadata(result);
1142
+ if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1143
+ throw new AgentAutoContinueSignal({
1144
+ reason: "no_progress",
1145
+ activityTrail: [...activityTrail],
1146
+ });
1147
+ }
1042
1148
  if (action === "auto_continue") {
1043
1149
  throw new AgentAutoContinueSignal(
1044
1150
  autoContinue
@@ -1099,6 +1205,8 @@ export async function readSSEStreamRaw(
1099
1205
  const decoder = new TextDecoder();
1100
1206
  let buf = "";
1101
1207
  let lastMeaningfulEventAt = Date.now();
1208
+ const activityTrail: ActivityTrailEntry[] = [];
1209
+ const preparingActionState: PreparingActionState = {};
1102
1210
  const processEventState: ProcessEventState = {
1103
1211
  completedToolsAfterLastAssistantText: new Set(),
1104
1212
  };
@@ -1134,12 +1242,37 @@ export async function readSSEStreamRaw(
1134
1242
  continue;
1135
1243
  }
1136
1244
  sawDataEvent = true;
1137
- lastMeaningfulEventAt = Date.now();
1245
+ const now = Date.now();
1246
+ lastMeaningfulEventAt = now;
1247
+ updatePreparingActionState(preparingActionState, ev, now);
1138
1248
 
1139
1249
  if (ev.seq !== undefined && onSeq) {
1140
1250
  onSeq(ev.seq);
1141
1251
  }
1142
1252
 
1253
+ if (ev.type === "clear") {
1254
+ activityTrail.length = 0;
1255
+ } else if (ev.type === "activity") {
1256
+ const tool = ev.tool?.trim() || undefined;
1257
+ appendActivityTrail(activityTrail, {
1258
+ label: baseActivityLabel(ev, tool),
1259
+ ...(tool ? { tool } : {}),
1260
+ });
1261
+ } else if (ev.type === "tool_start") {
1262
+ const tool = ev.tool ?? "unknown";
1263
+ appendActivityTrail(activityTrail, {
1264
+ label: runningToolLabel(tool),
1265
+ tool,
1266
+ });
1267
+ } else if (ev.type === "tool_done") {
1268
+ const tool = ev.tool ?? "unknown";
1269
+ for (let i = activityTrail.length - 1; i >= 0; i--) {
1270
+ if (activityTrail[i]?.tool === tool) {
1271
+ activityTrail.splice(i, 1);
1272
+ }
1273
+ }
1274
+ }
1275
+
1143
1276
  const { action, autoContinue } = processEvent(
1144
1277
  ev,
1145
1278
  content,
@@ -1159,9 +1292,18 @@ export async function readSSEStreamRaw(
1159
1292
  if (action === "auto_continue") {
1160
1293
  onUpdate([...content]);
1161
1294
  throw new AgentAutoContinueSignal(
1162
- autoContinue ?? { reason: "stream_ended" },
1295
+ autoContinue
1296
+ ? { ...autoContinue, activityTrail: [...activityTrail] }
1297
+ : { reason: "stream_ended", activityTrail: [...activityTrail] },
1163
1298
  );
1164
1299
  }
1300
+ if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1301
+ onUpdate([...content]);
1302
+ throw new AgentAutoContinueSignal({
1303
+ reason: "no_progress",
1304
+ activityTrail: [...activityTrail],
1305
+ });
1306
+ }
1165
1307
  if (
1166
1308
  action === "done" ||
1167
1309
  action === "error" ||
@@ -226,11 +226,19 @@ export function buildExtensionHtml(
226
226
  </style>
227
227
  <style>
228
228
  *, *::before, *::after { border-color: hsl(var(--border)); }
229
+ html, body {
230
+ /* Transparent so the iframe inherits the host surface (dashboard panel,
231
+ sidebar, chat) instead of painting the browser's default white canvas.
232
+ The dark class still flips the theme vars; content paints its own
233
+ bg-background / bg-card surfaces. */
234
+ background: transparent;
235
+ }
229
236
  body {
230
237
  --agent-native-extension-padding: clamp(16px, 2vw, 24px);
231
238
  /* Legacy alias for pre-rename extension content (do not remove). */
232
239
  --agent-native-tool-padding: var(--agent-native-extension-padding);
233
240
  box-sizing: border-box;
241
+ color: hsl(var(--foreground));
234
242
  font-family: 'Inter', sans-serif;
235
243
  margin: 0;
236
244
  min-height: 100vh;
@@ -403,8 +411,14 @@ export function buildExtensionHtml(
403
411
  }
404
412
 
405
413
  if (!res.ok) {
406
- var err = res.body || { error: res.statusText };
407
- throw new Error(err.error || 'Action failed: ' + res.status);
414
+ var err = res.body || {};
415
+ var rawError = typeof err === 'string' ? err : err.error;
416
+ var message = (typeof rawError === 'string' && rawError.trim())
417
+ ? rawError
418
+ : (res.status === 404
419
+ ? "Action '" + name + "' is not available over HTTP (404). It may be agent-only (http: false); expose it with an HTTP-mounted action to call it from an extension."
420
+ : "Action '" + name + "' failed (" + (res.status || 'network error') + ")");
421
+ throw new Error(message);
408
422
  }
409
423
  return res.body;
410
424
  }
@@ -730,7 +744,7 @@ export function buildExtensionHtml(
730
744
  });
731
745
  </script>
732
746
  </head>
733
- <body${extensionId ? ` data-extension-id="${extensionIdAttr}" data-tool-id="${extensionIdAttr}"` : ""} class="bg-background text-foreground">
747
+ <body${extensionId ? ` data-extension-id="${extensionIdAttr}" data-tool-id="${extensionIdAttr}"` : ""} class="text-foreground">
734
748
  ${content}
735
749
  <div id="__extension-error-toast">
736
750
  <div style="display:flex;align-items:flex-start;gap:8px;">
@@ -1,3 +1,42 @@
1
+ /**
2
+ * Canonical set of semantic theme token names shared by the app and extension
3
+ * iframes. This is the single source of truth: `getThemeVars` bakes default
4
+ * values for these into every iframe, and the host reads the *actual* computed
5
+ * values for the same names to sync its live palette into embedded extensions
6
+ * (see EmbeddedExtension). Custom properties can't be enumerated portably from
7
+ * a computed style, so the names must be listed explicitly.
8
+ */
9
+ export const THEME_VAR_NAMES = [
10
+ "--background",
11
+ "--foreground",
12
+ "--card",
13
+ "--card-foreground",
14
+ "--popover",
15
+ "--popover-foreground",
16
+ "--primary",
17
+ "--primary-foreground",
18
+ "--secondary",
19
+ "--secondary-foreground",
20
+ "--muted",
21
+ "--muted-foreground",
22
+ "--accent",
23
+ "--accent-foreground",
24
+ "--destructive",
25
+ "--destructive-foreground",
26
+ "--border",
27
+ "--input",
28
+ "--ring",
29
+ "--radius",
30
+ "--sidebar-background",
31
+ "--sidebar-foreground",
32
+ "--sidebar-primary",
33
+ "--sidebar-primary-foreground",
34
+ "--sidebar-accent",
35
+ "--sidebar-accent-foreground",
36
+ "--sidebar-border",
37
+ "--sidebar-ring",
38
+ ] as const;
39
+
1
40
  /**
2
41
  * CSS variables baked into every extension iframe. Both light and dark are
3
42
  * always emitted — the `.dark` class on the iframe's `<html>` toggles between
@@ -27,6 +27,15 @@ function makeBody(bytes: Uint8Array, mimeType: string): BodyInit {
27
27
  : (bytes as unknown as BodyInit);
28
28
  }
29
29
 
30
+ function shouldUseSignedUrlUpload(
31
+ bytes: Uint8Array,
32
+ mimeType: string,
33
+ ): boolean {
34
+ return (
35
+ bytes.byteLength > LARGE_FILE_THRESHOLD_BYTES || /^video\//i.test(mimeType)
36
+ );
37
+ }
38
+
30
39
  function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
31
40
  const controller = new AbortController();
32
41
  const timer = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
@@ -86,6 +95,7 @@ async function uploadLargeFileViaSignedUrl(
86
95
  privateKey,
87
96
  assetId,
88
97
  input.filename,
98
+ { skipCompressionWait: input.skipCompressionWait },
89
99
  );
90
100
  console.log(`[builder-upload] done [${assetId}]: ${url}`);
91
101
  return { url, id, provider: "builder" };
@@ -139,19 +149,21 @@ async function completeBuilderUpload(
139
149
  privateKey: string,
140
150
  assetId: string,
141
151
  filename: string | undefined,
152
+ options?: { skipCompressionWait?: boolean },
142
153
  ): Promise<{ url: string; id?: string }> {
143
154
  const host = builderUploadHost();
144
- const res = await fetchWithTimeout(
145
- new URL("/api/v1/upload/complete", host).toString(),
146
- {
147
- method: "POST",
148
- headers: {
149
- Authorization: `Bearer ${privateKey}`,
150
- "Content-Type": "application/json",
151
- },
152
- body: JSON.stringify({ assetId, name: filename }),
155
+ const url = new URL("/api/v1/upload/complete", host);
156
+ if (options?.skipCompressionWait) {
157
+ url.searchParams.set("skipCompressionWait", "true");
158
+ }
159
+ const res = await fetchWithTimeout(url.toString(), {
160
+ method: "POST",
161
+ headers: {
162
+ Authorization: `Bearer ${privateKey}`,
163
+ "Content-Type": "application/json",
153
164
  },
154
- );
165
+ body: JSON.stringify({ assetId, name: filename }),
166
+ });
155
167
  await assertOk(res, "Builder.io upload complete failed");
156
168
  const json = (await res.json()) as { url?: string; id?: string };
157
169
  if (!json.url) throw new Error("Builder.io upload/complete returned no URL");
@@ -204,7 +216,8 @@ export const builderFileUploadProvider: FileUploadProvider = {
204
216
  id: "builder",
205
217
  name: "Builder.io",
206
218
  isConfigured: () => !!process.env.BUILDER_PRIVATE_KEY,
207
- upload: async ({ data, filename, mimeType }: FileUploadInput) => {
219
+ upload: async (input: FileUploadInput) => {
220
+ const { data, filename, mimeType } = input;
208
221
  const { resolveBuilderPrivateKey } =
209
222
  await import("../server/credential-provider.js");
210
223
  const privateKey = await resolveBuilderPrivateKey();
@@ -226,9 +239,9 @@ export const builderFileUploadProvider: FileUploadProvider = {
226
239
  data instanceof Uint8Array ? data : new Uint8Array(data as any);
227
240
  const mb = (bytes.byteLength / (1024 * 1024)).toFixed(1);
228
241
 
229
- if (bytes.byteLength > LARGE_FILE_THRESHOLD_BYTES) {
242
+ if (shouldUseSignedUrlUpload(bytes, bareMimeType)) {
230
243
  return uploadLargeFileViaSignedUrl(
231
- { data, filename, mimeType },
244
+ input,
232
245
  privateKey,
233
246
  bareMimeType,
234
247
  bytes,
@@ -241,6 +254,9 @@ export const builderFileUploadProvider: FileUploadProvider = {
241
254
 
242
255
  const url = new URL("/api/v1/upload", builderUploadHost());
243
256
  if (filename) url.searchParams.set("name", filename);
257
+ if (input.skipCompressionWait) {
258
+ url.searchParams.set("skipCompressionWait", "true");
259
+ }
244
260
 
245
261
  const response = await uploadSmallFile(url, {
246
262
  method: "POST",
@@ -366,7 +382,7 @@ export const builderFileUploadProvider: FileUploadProvider = {
366
382
  : new Error("GCS PUT failed after retries");
367
383
  },
368
384
 
369
- async completeSession(session, filename) {
385
+ async completeSession(session, filename, options) {
370
386
  const { resolveBuilderPrivateKey } =
371
387
  await import("../server/credential-provider.js");
372
388
  const privateKey = await resolveBuilderPrivateKey();
@@ -378,6 +394,11 @@ export const builderFileUploadProvider: FileUploadProvider = {
378
394
  privateKey,
379
395
  assetId,
380
396
  filename,
397
+ {
398
+ skipCompressionWait:
399
+ options?.skipCompressionWait ||
400
+ session.meta.skipCompressionWait === true,
401
+ },
381
402
  );
382
403
  console.log(`[builder-resumable] upload complete: ${url}`);
383
404
  return url;
@@ -16,6 +16,8 @@ export interface FileUploadInput {
16
16
  mimeType?: string;
17
17
  /** Optional owner email for per-user scoping in fallback storage. */
18
18
  ownerEmail?: string;
19
+ /** Builder.io upload hint: return after asset registration instead of waiting for server-side compression. */
20
+ skipCompressionWait?: boolean;
19
21
  }
20
22
 
21
23
  export interface FileUploadResult {
@@ -77,6 +79,7 @@ export interface FileUploadProvider {
77
79
  completeSession(
78
80
  session: ResumableUploadSession,
79
81
  filename: string,
82
+ options?: { skipCompressionWait?: boolean },
80
83
  ): Promise<string>;
81
84
  };
82
85
  }