@agent-native/core 0.84.12 → 0.84.14

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 (112) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +42 -5
  5. package/corpus/core/src/agent/production-agent.ts +72 -10
  6. package/corpus/core/src/agent/run-manager.ts +84 -4
  7. package/corpus/core/src/agent/run-store.ts +55 -11
  8. package/corpus/core/src/chat-threads/store.ts +2 -0
  9. package/corpus/core/src/cli/skills.ts +10 -6
  10. package/corpus/core/src/client/AssistantChat.tsx +40 -9
  11. package/corpus/core/src/client/agent-chat-adapter.ts +44 -5
  12. package/corpus/core/src/client/blocks/library/diagram.tsx +3 -3
  13. package/corpus/core/src/client/chat/message-components.tsx +17 -0
  14. package/corpus/core/src/client/chat/run-recovery.tsx +52 -44
  15. package/corpus/core/src/client/chat/tool-call-display.tsx +7 -2
  16. package/corpus/core/src/observability/traces.ts +17 -3
  17. package/corpus/core/src/server/agent-chat-plugin.ts +12 -1
  18. package/corpus/core/src/shared/streaming-text-smoothing.ts +10 -5
  19. package/corpus/core/src/styles/blocks.css +44 -1
  20. package/corpus/templates/clips/app/components/library/recording-card.tsx +14 -4
  21. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +10 -12
  22. package/corpus/templates/clips/app/hooks/use-library.ts +7 -4
  23. package/corpus/templates/clips/app/lib/recording-status.ts +32 -0
  24. package/corpus/templates/clips/changelog/2026-07-01-chrome-extension-recordings-stream-uploads-while-recording.md +6 -0
  25. package/corpus/templates/clips/changelog/2026-07-01-dictation-keeps-listening-through-natural-pauses-and-gives-c.md +6 -0
  26. package/corpus/templates/clips/changelog/2026-07-01-redoing-a-paused-desktop-recording-no-longer-leaves-the-recording-controls-disabled.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-07-01-stalled-clip-uploads-now-update-in-the-library-and-surface-as-failed.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/src/background.ts +8 -0
  29. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +61 -16
  30. package/corpus/templates/clips/desktop/src/lib/recorder.ts +36 -18
  31. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +99 -27
  32. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -43
  33. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +3 -0
  34. package/corpus/templates/clips/desktop/src/styles.css +5 -48
  35. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +4 -5
  36. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +15 -6
  37. package/corpus/templates/design/AGENTS.md +8 -4
  38. package/corpus/templates/design/actions/create-design.ts +1 -0
  39. package/corpus/templates/design/actions/duplicate-design.ts +3 -1
  40. package/corpus/templates/design/actions/edit-design.ts +43 -13
  41. package/corpus/templates/design/actions/generate-design.ts +5 -1
  42. package/corpus/templates/design/actions/present-design-variants.ts +17 -12
  43. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  44. package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
  45. package/corpus/templates/design/changelog/2026-07-01-org-members-can-see-design-projects-created-in-their-workspace.md +6 -0
  46. package/corpus/templates/design/server/plugins/db.ts +9 -0
  47. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +44 -1
  48. package/corpus/templates/plan/changelog/2026-07-01-visual-plan-and-recap-diagram-labels-now-wrap-inside-their-b.md +6 -0
  49. package/dist/agent/durable-background.d.ts +3 -0
  50. package/dist/agent/durable-background.d.ts.map +1 -1
  51. package/dist/agent/durable-background.js +33 -3
  52. package/dist/agent/durable-background.js.map +1 -1
  53. package/dist/agent/production-agent.d.ts +1 -0
  54. package/dist/agent/production-agent.d.ts.map +1 -1
  55. package/dist/agent/production-agent.js +55 -9
  56. package/dist/agent/production-agent.js.map +1 -1
  57. package/dist/agent/run-manager.d.ts +2 -0
  58. package/dist/agent/run-manager.d.ts.map +1 -1
  59. package/dist/agent/run-manager.js +68 -5
  60. package/dist/agent/run-manager.js.map +1 -1
  61. package/dist/agent/run-store.d.ts +9 -0
  62. package/dist/agent/run-store.d.ts.map +1 -1
  63. package/dist/agent/run-store.js +47 -11
  64. package/dist/agent/run-store.js.map +1 -1
  65. package/dist/chat-threads/store.d.ts +1 -0
  66. package/dist/chat-threads/store.d.ts.map +1 -1
  67. package/dist/chat-threads/store.js +2 -0
  68. package/dist/chat-threads/store.js.map +1 -1
  69. package/dist/cli/skills.d.ts.map +1 -1
  70. package/dist/cli/skills.js +10 -6
  71. package/dist/cli/skills.js.map +1 -1
  72. package/dist/client/AssistantChat.d.ts +10 -0
  73. package/dist/client/AssistantChat.d.ts.map +1 -1
  74. package/dist/client/AssistantChat.js +26 -8
  75. package/dist/client/AssistantChat.js.map +1 -1
  76. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  77. package/dist/client/agent-chat-adapter.js +40 -5
  78. package/dist/client/agent-chat-adapter.js.map +1 -1
  79. package/dist/client/blocks/library/diagram.js +3 -3
  80. package/dist/client/blocks/library/diagram.js.map +1 -1
  81. package/dist/client/chat/message-components.d.ts +3 -1
  82. package/dist/client/chat/message-components.d.ts.map +1 -1
  83. package/dist/client/chat/message-components.js +16 -2
  84. package/dist/client/chat/message-components.js.map +1 -1
  85. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  86. package/dist/client/chat/run-recovery.js +15 -10
  87. package/dist/client/chat/run-recovery.js.map +1 -1
  88. package/dist/client/chat/tool-call-display.d.ts +1 -0
  89. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  90. package/dist/client/chat/tool-call-display.js +3 -2
  91. package/dist/client/chat/tool-call-display.js.map +1 -1
  92. package/dist/collab/awareness.d.ts +2 -2
  93. package/dist/collab/awareness.d.ts.map +1 -1
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/notifications/routes.d.ts +3 -3
  96. package/dist/observability/routes.d.ts +6 -6
  97. package/dist/observability/traces.d.ts +5 -0
  98. package/dist/observability/traces.d.ts.map +1 -1
  99. package/dist/observability/traces.js +9 -3
  100. package/dist/observability/traces.js.map +1 -1
  101. package/dist/resources/handlers.d.ts +2 -2
  102. package/dist/server/agent-chat-plugin.d.ts +2 -1
  103. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  104. package/dist/server/agent-chat-plugin.js +8 -3
  105. package/dist/server/agent-chat-plugin.js.map +1 -1
  106. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  107. package/dist/server/transcribe-voice.d.ts +1 -1
  108. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  109. package/dist/shared/streaming-text-smoothing.js +8 -5
  110. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  111. package/dist/styles/blocks.css +44 -1
  112. package/package.json +1 -1
@@ -513,7 +513,9 @@ export function RunErrorRecoveryCard({
513
513
  onDismiss: () => void;
514
514
  }) {
515
515
  const [detailsOpen, setDetailsOpen] = useState(false);
516
- const [copied, setCopied] = useState(false);
516
+ const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">(
517
+ "idle",
518
+ );
517
519
  const [forking, setForking] = useState(false);
518
520
  const [forkError, setForkError] = useState<string | null>(null);
519
521
  const builderReconnect = useBuilderConnectFlow({
@@ -538,11 +540,15 @@ export function RunErrorRecoveryCard({
538
540
  ]
539
541
  .filter(Boolean)
540
542
  .join("\n\n");
541
- void writeClipboardText(text).then((ok) => {
542
- if (!ok) return;
543
- setCopied(true);
544
- setTimeout(() => setCopied(false), 1200);
545
- });
543
+ void writeClipboardText(text)
544
+ .then((ok) => {
545
+ setCopyState(ok ? "copied" : "failed");
546
+ setTimeout(() => setCopyState("idle"), 1600);
547
+ })
548
+ .catch(() => {
549
+ setCopyState("failed");
550
+ setTimeout(() => setCopyState("idle"), 1600);
551
+ });
546
552
  }, [info]);
547
553
  const startNewChat = useCallback(() => {
548
554
  window.dispatchEvent(new CustomEvent("agent-chat:new-chat"));
@@ -705,8 +711,20 @@ export function RunErrorRecoveryCard({
705
711
  onClick={copyDetails}
706
712
  className="ml-auto inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-muted-foreground hover:bg-background/80 hover:text-foreground"
707
713
  >
708
- {copied ? <IconCheck size={13} /> : <IconCopy size={13} />}
709
- {copied ? "Copied" : copyLabel}
714
+ {copyState === "copied" ? (
715
+ <IconCheck size={13} />
716
+ ) : copyState === "failed" ? (
717
+ <IconX size={13} />
718
+ ) : (
719
+ <IconCopy size={13} />
720
+ )}
721
+ <span aria-live="polite">
722
+ {copyState === "copied"
723
+ ? "Copied"
724
+ : copyState === "failed"
725
+ ? "Copy failed"
726
+ : copyLabel}
727
+ </span>
710
728
  </button>
711
729
  </div>
712
730
  {shouldShowBuilderReconnect && builderReconnect.error && (
@@ -913,42 +931,32 @@ export function PlanModeCallout({
913
931
  onSwitchToAct: () => void;
914
932
  }) {
915
933
  return (
916
- <div className="shrink-0 px-3 pt-2">
917
- <div className="rounded-lg border border-blue-500/25 bg-blue-500/[0.06] px-3 py-2.5 shadow-sm">
918
- <div className="flex items-center gap-2.5">
919
- <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-blue-600 dark:text-blue-300">
920
- <IconClipboardList size={15} />
921
- </span>
922
- <div className="min-w-0 flex-1">
923
- <p className="text-sm font-medium text-foreground">
924
- {canImplementPlan ? "Plan ready" : "Plan mode is on"}
925
- </p>
926
- <p className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
927
- {canImplementPlan
928
- ? "Switch to Act and run the proposed plan."
929
- : "The next turn will stay read-only until you switch to Act."}
930
- </p>
931
- </div>
932
- {canImplementPlan ? (
933
- <button
934
- type="button"
935
- onClick={onImplementPlan}
936
- className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md bg-foreground px-3 text-xs font-medium text-background hover:opacity-90"
937
- >
938
- <IconPlayerPlay size={13} />
939
- Implement Plan
940
- </button>
941
- ) : (
942
- <button
943
- type="button"
944
- onClick={onSwitchToAct}
945
- className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground hover:bg-accent"
946
- >
947
- Act
948
- <IconArrowRight size={13} />
949
- </button>
950
- )}
951
- </div>
934
+ <div className="shrink-0 px-3 pt-1">
935
+ <div className="ml-auto flex w-fit max-w-full items-center gap-2 rounded-full border border-border/70 bg-background/95 px-2 py-1.5 text-xs text-muted-foreground shadow-sm">
936
+ <IconClipboardList size={13} className="shrink-0" />
937
+ <span className="min-w-0 truncate">
938
+ {canImplementPlan ? "Plan ready" : "Plan mode"}
939
+ </span>
940
+ {canImplementPlan ? (
941
+ <button
942
+ type="button"
943
+ onClick={onImplementPlan}
944
+ className="inline-flex h-6 shrink-0 items-center gap-1 rounded-full bg-foreground px-2.5 text-[11px] font-medium text-background hover:opacity-90"
945
+ >
946
+ <IconPlayerPlay size={12} />
947
+ Implement
948
+ </button>
949
+ ) : (
950
+ <button
951
+ type="button"
952
+ onClick={onSwitchToAct}
953
+ className="inline-flex h-6 shrink-0 items-center gap-1 rounded-full border border-border bg-background px-2.5 text-[11px] font-medium text-foreground hover:bg-accent"
954
+ aria-label="Switch to Act mode"
955
+ >
956
+ Act
957
+ <IconArrowRight size={12} />
958
+ </button>
959
+ )}
952
960
  </div>
953
961
  </div>
954
962
  );
@@ -692,10 +692,12 @@ export function ToolCallFallback({
692
692
  mcpApp?: AgentMcpAppPayload;
693
693
  chatUI?: ActionChatUIConfig;
694
694
  structuredMeta?: Record<string, unknown>;
695
+ activity?: boolean;
695
696
  approval?: { approvalKey: string; dismissed?: boolean };
696
697
  }) {
697
698
  const chatRunning = React.useContext(ChatRunningContext);
698
- const isRunning = result === undefined && chatRunning;
699
+ const isRunning =
700
+ result === undefined && (chatRunning || rest.activity === true);
699
701
  return (
700
702
  <ToolCallDisplay
701
703
  toolName={toolName}
@@ -757,7 +759,10 @@ export function ReconnectStreamMessage({
757
759
  mcpApp={part.mcpApp}
758
760
  chatUI={part.chatUI}
759
761
  structuredMeta={part.structuredMeta}
760
- isRunning={part.result === undefined && chatRunning}
762
+ isRunning={
763
+ part.result === undefined &&
764
+ (chatRunning || part.activity === true)
765
+ }
761
766
  approval={part.approval}
762
767
  />
763
768
  );
@@ -89,6 +89,14 @@ export async function instrumentAgentLoop(opts: {
89
89
  * reads. */
90
90
  userId: string | null;
91
91
  config: ObservabilityConfig;
92
+ classifyError?: (error: unknown) =>
93
+ | {
94
+ status?: "success" | "error";
95
+ errorMessage?: string | null;
96
+ metadata?: Record<string, unknown> | null;
97
+ }
98
+ | null
99
+ | undefined;
92
100
  }): Promise<AgentLoopUsage> {
93
101
  const { runAgentLoop, loopOpts, runId, threadId, userId, config } = opts;
94
102
  const runStart = Date.now();
@@ -260,11 +268,17 @@ export async function instrumentAgentLoop(opts: {
260
268
  let usage: AgentLoopUsage | undefined;
261
269
  let runStatus: "success" | "error" = "success";
262
270
  let errorMessage: string | null = null;
271
+ let runMetadata: Record<string, unknown> | null = null;
263
272
  try {
264
273
  usage = await runAgentLoop({ ...loopOpts, send: instrumentedSend });
265
274
  } catch (err: any) {
266
- runStatus = "error";
267
- errorMessage = err?.message ?? String(err);
275
+ const classification = opts.classifyError?.(err) ?? null;
276
+ runStatus = classification?.status ?? "error";
277
+ errorMessage =
278
+ classification?.errorMessage === undefined
279
+ ? (err?.message ?? String(err))
280
+ : classification.errorMessage;
281
+ runMetadata = classification?.metadata ?? null;
268
282
  throw err;
269
283
  } finally {
270
284
  const runEnd = Date.now();
@@ -325,7 +339,7 @@ export async function instrumentAgentLoop(opts: {
325
339
  durationMs: totalDurationMs,
326
340
  status: runStatus,
327
341
  errorMessage,
328
- metadata: null,
342
+ metadata: runMetadata,
329
343
  createdAt: runStart,
330
344
  };
331
345
  spans.push(parentSpan);
@@ -69,6 +69,7 @@ import {
69
69
  recordRunDiagnostic,
70
70
  RUN_DIAG_STAGE,
71
71
  setRunError,
72
+ setRunTerminalReason,
72
73
  updateRunStatusIfRunning,
73
74
  } from "../agent/run-store.js";
74
75
  import { buildRuntimeContextPrompt } from "../agent/runtime-context.js";
@@ -3876,6 +3877,7 @@ type AgentChatProcessRunFailureDeps = {
3876
3877
  readBackgroundRunClaim?: typeof readBackgroundRunClaim;
3877
3878
  recordRunDiagnostic?: typeof recordRunDiagnostic;
3878
3879
  setRunError?: typeof setRunError;
3880
+ setRunTerminalReason?: typeof setRunTerminalReason;
3879
3881
  updateRunStatusIfRunning?: typeof updateRunStatusIfRunning;
3880
3882
  ensureTerminalRunEvent?: typeof ensureTerminalRunEvent;
3881
3883
  };
@@ -3888,6 +3890,7 @@ export async function finalizeClaimedAgentChatProcessRunFailure(
3888
3890
  const readClaim = deps.readBackgroundRunClaim ?? readBackgroundRunClaim;
3889
3891
  const record = deps.recordRunDiagnostic ?? recordRunDiagnostic;
3890
3892
  const setError = deps.setRunError ?? setRunError;
3893
+ const setTerminalReason = deps.setRunTerminalReason ?? setRunTerminalReason;
3891
3894
  const updateStatus =
3892
3895
  deps.updateRunStatusIfRunning ?? updateRunStatusIfRunning;
3893
3896
  const ensureTerminal = deps.ensureTerminalRunEvent ?? ensureTerminalRunEvent;
@@ -3908,7 +3911,13 @@ export async function finalizeClaimedAgentChatProcessRunFailure(
3908
3911
  CLAIMED_BACKGROUND_WORKER_FAILED_ERROR_EVENT.errorCode,
3909
3912
  `${CLAIMED_BACKGROUND_WORKER_FAILED_ERROR_EVENT.details} setupError=${message}`,
3910
3913
  ).catch(() => {});
3911
- await updateStatus(runId, "errored").catch(() => {});
3914
+ const statusUpdated = await updateStatus(runId, "errored").catch(() => false);
3915
+ if (statusUpdated) {
3916
+ await setTerminalReason(
3917
+ runId,
3918
+ CLAIMED_BACKGROUND_WORKER_FAILED_ERROR_EVENT.errorCode,
3919
+ ).catch(() => {});
3920
+ }
3912
3921
  await ensureTerminal(
3913
3922
  runId,
3914
3923
  CLAIMED_BACKGROUND_WORKER_FAILED_ERROR_EVENT,
@@ -7400,6 +7409,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7400
7409
  // unreadable Netlify background-function logs — read
7401
7410
  // `/runs/active?threadId=...` and inspect `diagStage`.
7402
7411
  dispatchMode: run.dispatchMode ?? null,
7412
+ terminalReason: run.terminalReason ?? null,
7403
7413
  diagStage: run.diagStage ?? null,
7404
7414
  workerStage: workerClaim?.workerStage ?? null,
7405
7415
  // Server clock so the client computes "stuck" elapsed time
@@ -7694,6 +7704,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7694
7704
  nextTitle,
7695
7705
  nextPreview,
7696
7706
  newMessageCount,
7707
+ { ignoreConflicts: true },
7697
7708
  );
7698
7709
  // Scope updates piggyback on the PUT — the client uses this
7699
7710
  // path for both "detach" (scope: null) and "retag" flows.
@@ -1,6 +1,7 @@
1
1
  export const SMOOTH_STREAMING_COMMIT_INTERVAL_MS = 16;
2
2
  export const SMOOTH_STREAMING_LONG_TEXT_THRESHOLD_GRAPHEMES = 640;
3
3
  export const SMOOTH_STREAMING_LONG_TEXT_TAIL_GRAPHEMES = 180;
4
+ const SMOOTH_STREAMING_SPEED_MULTIPLIER = 2;
4
5
 
5
6
  type SegmenterInstance = {
6
7
  segment(input: string): Iterable<{ segment: string }>;
@@ -137,7 +138,7 @@ export function smoothStreamingRevealCount({
137
138
  return 0;
138
139
  }
139
140
 
140
- const charactersPerSecond = inputDone
141
+ const baseCharactersPerSecond = inputDone
141
142
  ? backlog > 800
142
143
  ? 900
143
144
  : 420
@@ -148,8 +149,12 @@ export function smoothStreamingRevealCount({
148
149
  : backlog > 180
149
150
  ? 190
150
151
  : 95;
152
+ const charactersPerSecond =
153
+ baseCharactersPerSecond * SMOOTH_STREAMING_SPEED_MULTIPLIER;
151
154
 
152
- const maxBurst = inputDone ? 160 : backlog > 1400 ? 120 : 72;
155
+ const maxBurst =
156
+ (inputDone ? 160 : backlog > 1400 ? 120 : 72) *
157
+ SMOOTH_STREAMING_SPEED_MULTIPLIER;
153
158
  const count = Math.floor((elapsedMs / 1000) * charactersPerSecond);
154
159
 
155
160
  return Math.min(backlog, Math.max(1, count), maxBurst);
@@ -164,15 +169,15 @@ export function smoothStreamingPunctuationDelayMs(
164
169
  }
165
170
 
166
171
  if (grapheme === "\n") {
167
- return 80;
172
+ return 80 / SMOOTH_STREAMING_SPEED_MULTIPLIER;
168
173
  }
169
174
 
170
175
  if (/[.!?)]/.test(grapheme)) {
171
- return 70;
176
+ return 70 / SMOOTH_STREAMING_SPEED_MULTIPLIER;
172
177
  }
173
178
 
174
179
  if (/[,;:]/.test(grapheme)) {
175
- return 35;
180
+ return 35 / SMOOTH_STREAMING_SPEED_MULTIPLIER;
176
181
  }
177
182
 
178
183
  return 0;
@@ -1355,8 +1355,37 @@
1355
1355
  color: var(--wf-ink) !important;
1356
1356
  }
1357
1357
 
1358
- .plan-diagram-frame :is(.diagram-node, .diagram-box, .diagram-card) {
1358
+ .plan-diagram-frame
1359
+ :is(
1360
+ .diagram-panel,
1361
+ .diagram-card,
1362
+ .diagram-node,
1363
+ .diagram-box,
1364
+ [class*="card"],
1365
+ [class*="panel"],
1366
+ [class*="box"]
1367
+ ) {
1359
1368
  max-width: 100%;
1369
+ overflow-wrap: anywhere !important;
1370
+ word-break: break-word;
1371
+ white-space: normal !important;
1372
+ }
1373
+
1374
+ .plan-diagram-frame
1375
+ :is(
1376
+ .diagram-panel,
1377
+ .diagram-card,
1378
+ .diagram-node,
1379
+ .diagram-box,
1380
+ [class*="card"],
1381
+ [class*="panel"],
1382
+ [class*="box"]
1383
+ )
1384
+ :is(h1, h2, h3, p, small, strong, span, li) {
1385
+ max-width: 100%;
1386
+ overflow-wrap: anywhere !important;
1387
+ word-break: break-word;
1388
+ white-space: normal !important;
1360
1389
  }
1361
1390
 
1362
1391
  /* Base padding so primitive text never touches the box edge, even when an author
@@ -1422,6 +1451,7 @@
1422
1451
  display: inline-flex;
1423
1452
  align-items: center;
1424
1453
  justify-content: center;
1454
+ flex-wrap: wrap;
1425
1455
  /* A pill hugs its label. `fit-content` is a definite cross size, so a flex
1426
1456
  * column with the default `align-items: stretch` no longer blows the pill out
1427
1457
  * to full width (the number chips and short status labels stay pill-shaped).
@@ -1434,6 +1464,19 @@
1434
1464
  padding: 3px 11px;
1435
1465
  color: var(--wf-ink);
1436
1466
  background: var(--wf-paper);
1467
+ overflow-wrap: anywhere !important;
1468
+ text-align: center;
1469
+ white-space: normal !important;
1470
+ word-break: break-word;
1471
+ }
1472
+
1473
+ .plan-diagram-frame
1474
+ :is(.diagram-pill, [class*="pill"], [class*="badge"], [class*="chip"])
1475
+ > * {
1476
+ min-width: 0;
1477
+ max-width: 100%;
1478
+ overflow-wrap: anywhere !important;
1479
+ white-space: normal !important;
1437
1480
  }
1438
1481
 
1439
1482
  .plan-diagram-frame :is(.diagram-accent, .diagram-pill.accent) {
@@ -28,6 +28,7 @@ import {
28
28
  } from "@/components/ui/dropdown-menu";
29
29
  import { isDefaultTitle } from "@/hooks/use-auto-title";
30
30
  import type { RecordingSummary } from "@/hooks/use-library";
31
+ import { isStaleRecordingUpload } from "@/lib/recording-status";
31
32
  import { isStorageSetupFailureReason } from "@/lib/storage-failures";
32
33
  import { cn } from "@/lib/utils";
33
34
 
@@ -100,6 +101,12 @@ export function RecordingCard({
100
101
  const waitingForStorage = isStorageSetupFailureReason(
101
102
  recording.failureReason,
102
103
  );
104
+ const staleUpload = isStaleRecordingUpload(recording);
105
+ const displayFailed = recording.status === "failed" || staleUpload;
106
+ const failureReason = staleUpload
107
+ ? (recording.failureReason ??
108
+ t("recordingPage.processingStuck", { status: recording.status }))
109
+ : (recording.failureReason ?? t("clipsFinalRaw.removeFailedClip"));
103
110
  const nativeUploadPaused =
104
111
  recording.status === "failed" &&
105
112
  /native recording|native fullscreen|screencapture|avconvert/i.test(
@@ -221,11 +228,15 @@ export function RecordingCard({
221
228
  {/* Status pill for non-ready recordings */}
222
229
  {recording.status !== "ready" && (
223
230
  <div className="absolute top-2 end-2 rounded-full bg-black/80 px-2 py-0.5 text-[10px] font-medium text-white uppercase tracking-wide">
224
- {waitingForStorage ? "storage" : recording.status}
231
+ {waitingForStorage
232
+ ? "storage"
233
+ : staleUpload
234
+ ? "failed"
235
+ : recording.status}
225
236
  </div>
226
237
  )}
227
238
 
228
- {(recording.status === "failed" || waitingForStorage) && (
239
+ {(displayFailed || waitingForStorage) && (
229
240
  <div
230
241
  className={cn(
231
242
  "absolute inset-x-2 bottom-2 rounded-md border bg-background/95 p-2 text-start shadow-sm backdrop-blur",
@@ -252,8 +263,7 @@ export function RecordingCard({
252
263
  ? t("clipsFinalRaw.connectStorageToFinish")
253
264
  : nativeUploadPaused
254
265
  ? t("clipsFinalRaw.retryFromClipsMenu")
255
- : (recording.failureReason ??
256
- t("clipsFinalRaw.removeFailedClip"))}
266
+ : failureReason}
257
267
  </div>
258
268
  </div>
259
269
  {!waitingForStorage && (
@@ -1145,6 +1145,9 @@ export class RecorderEngine {
1145
1145
  // mid-state — the UI spinner is wired to engine state and would
1146
1146
  // hang forever otherwise.
1147
1147
  const e = err instanceof Error ? err : new Error(String(err));
1148
+ if (e.name !== "AbortError") {
1149
+ this.rememberUploadFailure(e);
1150
+ }
1148
1151
  this.transition("error", { message: e.message });
1149
1152
  throw e;
1150
1153
  } finally {
@@ -1183,6 +1186,9 @@ export class RecorderEngine {
1183
1186
  return this.toFinalizeResult(result, meta);
1184
1187
  } catch (err) {
1185
1188
  const e = err instanceof Error ? err : new Error(String(err));
1189
+ if (e.name !== "AbortError") {
1190
+ this.rememberUploadFailure(e);
1191
+ }
1186
1192
  this.transition("error", { message: e.message });
1187
1193
  throw e;
1188
1194
  } finally {
@@ -1595,26 +1601,18 @@ export class RecorderEngine {
1595
1601
  const failure = err instanceof Error ? err : new Error(String(err));
1596
1602
  // User-initiated cancel — cancel() already runs the abortUrl path.
1597
1603
  if (failure.name === "AbortError") return;
1598
- await this.markUploadFailed(failure);
1604
+ this.rememberUploadFailure(failure);
1599
1605
  this.emitError(failure);
1600
1606
  }
1601
1607
  });
1602
1608
  }
1603
1609
 
1604
- private async markUploadFailed(err: Error): Promise<void> {
1610
+ private rememberUploadFailure(err: Error): void {
1605
1611
  if (!this.uploadFailure) {
1606
1612
  this.uploadFailure = err;
1607
1613
  }
1608
- if (!this.opts.abortUrl) return;
1609
- try {
1610
- await fetch(this.opts.abortUrl, {
1611
- method: "POST",
1612
- headers: { "Content-Type": "application/json" },
1613
- body: JSON.stringify({ reason: err.message }),
1614
- });
1615
- } catch {
1616
- // ignore — the stop path will surface the original upload error.
1617
- }
1614
+ // Do not call abortUrl for retryable upload failures. retryUpload() reuses
1615
+ // this recording id; cancel() owns the terminal server-side abort path.
1618
1616
  }
1619
1617
 
1620
1618
  private async uploadBlobInSlices(
@@ -1,5 +1,7 @@
1
1
  import { useActionQuery, useActionMutation } from "@agent-native/core/client";
2
2
 
3
+ import { isLiveRecordingUpload } from "@/lib/recording-status";
4
+
3
5
  export interface RecordingSummary {
4
6
  id: string;
5
7
  title: string;
@@ -72,16 +74,17 @@ export function useRecordings(args: ListRecordingsArgs = {}) {
72
74
  recordings: Array.isArray(data?.recordings) ? data.recordings : [],
73
75
  };
74
76
  },
75
- // If any recording is still on a replaceable seed title, keep a 3s
76
- // refetch cadence so the skeleton in `recording-card` upgrades to
77
- // the real title promptly even if the refresh-signal poll is missed.
77
+ // Keep a short poll while uploads/processors are active so the library
78
+ // card does not get stuck if the global refresh signal is missed.
79
+ // Also poll for replaceable seed titles so the card upgrades promptly.
78
80
  refetchInterval: (q) => {
79
81
  const recs = (q.state.data as any)?.recordings as
80
82
  | RecordingSummary[]
81
83
  | undefined;
82
84
  if (!recs || recs.length === 0) return false;
85
+ const pendingUpload = recs.some((rec) => isLiveRecordingUpload(rec));
83
86
  const pendingTitle = recs.some(isAwaitingAutoTitle);
84
- return pendingTitle ? 3000 : false;
87
+ return pendingUpload || pendingTitle ? 3000 : false;
85
88
  },
86
89
  },
87
90
  );
@@ -0,0 +1,32 @@
1
+ export const STALE_RECORDING_UPLOAD_MS = 30 * 60 * 1000;
2
+
3
+ type RecordingStatusLike = {
4
+ status?: string | null;
5
+ updatedAt?: string | null;
6
+ };
7
+
8
+ export function isActiveRecordingUploadStatus(
9
+ status: string | null | undefined,
10
+ ): boolean {
11
+ return status === "uploading" || status === "processing";
12
+ }
13
+
14
+ export function isStaleRecordingUpload(
15
+ recording: RecordingStatusLike,
16
+ nowMs = Date.now(),
17
+ ): boolean {
18
+ if (!isActiveRecordingUploadStatus(recording.status)) return false;
19
+ const updatedAtMs = Date.parse(recording.updatedAt ?? "");
20
+ if (!Number.isFinite(updatedAtMs)) return false;
21
+ return nowMs - updatedAtMs >= STALE_RECORDING_UPLOAD_MS;
22
+ }
23
+
24
+ export function isLiveRecordingUpload(
25
+ recording: RecordingStatusLike,
26
+ nowMs = Date.now(),
27
+ ): boolean {
28
+ if (!isActiveRecordingUploadStatus(recording.status)) return false;
29
+ const updatedAtMs = Date.parse(recording.updatedAt ?? "");
30
+ if (!Number.isFinite(updatedAtMs)) return true;
31
+ return nowMs - updatedAtMs < STALE_RECORDING_UPLOAD_MS;
32
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Chrome extension recordings now stream uploads while recording so saving is less likely to time out.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Dictation keeps listening through natural pauses and gives clearer start feedback.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Redoing a paused desktop recording no longer leaves the recording controls disabled.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Stalled clip uploads now update in the library and surface as failed instead of staying stuck as uploading.
@@ -32,6 +32,7 @@ const UNQUOTED_SECRET_VALUE_RE = new RegExp(
32
32
  type CaptureSurface = "browser" | "window" | "monitor" | "camera";
33
33
  type ConsoleLevel = "debug" | "log" | "info" | "warn" | "error";
34
34
  type NetworkType = "fetch" | "xhr";
35
+ type UploadMode = "streaming" | "buffered";
35
36
 
36
37
  type ExtensionSettings = {
37
38
  clipsBaseUrl: string;
@@ -134,6 +135,7 @@ type NativeRecording = {
134
135
  recordingId: string;
135
136
  clipsBaseUrl: string;
136
137
  uploadUrl: string;
138
+ uploadMode: UploadMode;
137
139
  targetTabId: number;
138
140
  targetTitle: string | null;
139
141
  targetUrl: string | null;
@@ -1049,6 +1051,7 @@ async function armRecording(args: {
1049
1051
  id?: string;
1050
1052
  uploadChunkUrl?: string;
1051
1053
  abortUrl?: string;
1054
+ uploadMode?: UploadMode;
1052
1055
  };
1053
1056
  let created: CreatedRecording;
1054
1057
  try {
@@ -1061,6 +1064,8 @@ async function armRecording(args: {
1061
1064
  hasAudio:
1062
1065
  settings.includeMicrophone || settings.captureSurface !== "camera",
1063
1066
  visibility: "public",
1067
+ mimeType: "video/webm",
1068
+ requestStreaming: true,
1064
1069
  });
1065
1070
  } catch (err) {
1066
1071
  captureExtensionError(err, {
@@ -1091,6 +1096,7 @@ async function armRecording(args: {
1091
1096
  recordingId: created.id,
1092
1097
  clipsBaseUrl: settings.clipsBaseUrl,
1093
1098
  uploadUrl,
1099
+ uploadMode: created.uploadMode ?? "buffered",
1094
1100
  targetTabId: tab.id as number,
1095
1101
  targetTitle: tab.title?.trim() || null,
1096
1102
  targetUrl: tab.url?.trim() || null,
@@ -1122,6 +1128,7 @@ async function armRecording(args: {
1122
1128
  sessionId,
1123
1129
  recordingId: created.id,
1124
1130
  uploadUrl,
1131
+ uploadMode: created.uploadMode ?? "buffered",
1125
1132
  hasCamera: cameraInvolved,
1126
1133
  startDelayMs,
1127
1134
  authToken,
@@ -1274,6 +1281,7 @@ async function handleOverlayRestart() {
1274
1281
  sessionId: recording.sessionId,
1275
1282
  recordingId: recording.recordingId,
1276
1283
  uploadUrl: recording.uploadUrl,
1284
+ uploadMode: recording.uploadMode ?? "buffered",
1277
1285
  hasCamera:
1278
1286
  recording.captureSurface === "camera" || recording.includeCamera,
1279
1287
  startDelayMs: COUNTDOWN_SECONDS * 1000,