@agent-native/core 0.84.12 → 0.84.13

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 (83) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +11 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +30 -0
  5. package/corpus/core/src/agent/production-agent.ts +34 -6
  6. package/corpus/core/src/agent/run-manager.ts +45 -2
  7. package/corpus/core/src/chat-threads/store.ts +2 -0
  8. package/corpus/core/src/cli/skills.ts +10 -6
  9. package/corpus/core/src/client/AssistantChat.tsx +40 -9
  10. package/corpus/core/src/client/agent-chat-adapter.ts +23 -2
  11. package/corpus/core/src/client/chat/run-recovery.tsx +52 -44
  12. package/corpus/core/src/client/chat/tool-call-display.tsx +7 -2
  13. package/corpus/core/src/server/agent-chat-plugin.ts +1 -0
  14. package/corpus/core/src/shared/streaming-text-smoothing.ts +10 -5
  15. package/corpus/templates/clips/app/components/library/recording-card.tsx +14 -4
  16. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +10 -12
  17. package/corpus/templates/clips/app/hooks/use-library.ts +7 -4
  18. package/corpus/templates/clips/app/lib/recording-status.ts +32 -0
  19. package/corpus/templates/clips/changelog/2026-07-01-dictation-keeps-listening-through-natural-pauses-and-gives-c.md +6 -0
  20. package/corpus/templates/clips/changelog/2026-07-01-redoing-a-paused-desktop-recording-no-longer-leaves-the-recording-controls-disabled.md +6 -0
  21. package/corpus/templates/clips/changelog/2026-07-01-stalled-clip-uploads-now-update-in-the-library-and-surface-as-failed.md +6 -0
  22. package/corpus/templates/clips/desktop/src/lib/recorder.ts +36 -18
  23. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +99 -27
  24. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -43
  25. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +3 -0
  26. package/corpus/templates/clips/desktop/src/styles.css +5 -48
  27. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +4 -5
  28. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +15 -6
  29. package/corpus/templates/design/AGENTS.md +8 -4
  30. package/corpus/templates/design/actions/create-design.ts +1 -0
  31. package/corpus/templates/design/actions/duplicate-design.ts +3 -1
  32. package/corpus/templates/design/actions/edit-design.ts +43 -13
  33. package/corpus/templates/design/actions/generate-design.ts +5 -1
  34. package/corpus/templates/design/actions/present-design-variants.ts +17 -12
  35. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  36. package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
  37. package/corpus/templates/design/changelog/2026-07-01-org-members-can-see-design-projects-created-in-their-workspace.md +6 -0
  38. package/corpus/templates/design/server/plugins/db.ts +9 -0
  39. package/dist/agent/durable-background.d.ts +3 -0
  40. package/dist/agent/durable-background.d.ts.map +1 -1
  41. package/dist/agent/durable-background.js +19 -0
  42. package/dist/agent/durable-background.js.map +1 -1
  43. package/dist/agent/production-agent.d.ts +1 -0
  44. package/dist/agent/production-agent.d.ts.map +1 -1
  45. package/dist/agent/production-agent.js +29 -6
  46. package/dist/agent/production-agent.js.map +1 -1
  47. package/dist/agent/run-manager.d.ts.map +1 -1
  48. package/dist/agent/run-manager.js +40 -2
  49. package/dist/agent/run-manager.js.map +1 -1
  50. package/dist/chat-threads/store.d.ts +1 -0
  51. package/dist/chat-threads/store.d.ts.map +1 -1
  52. package/dist/chat-threads/store.js +2 -0
  53. package/dist/chat-threads/store.js.map +1 -1
  54. package/dist/cli/skills.d.ts.map +1 -1
  55. package/dist/cli/skills.js +10 -6
  56. package/dist/cli/skills.js.map +1 -1
  57. package/dist/client/AssistantChat.d.ts +10 -0
  58. package/dist/client/AssistantChat.d.ts.map +1 -1
  59. package/dist/client/AssistantChat.js +26 -8
  60. package/dist/client/AssistantChat.js.map +1 -1
  61. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  62. package/dist/client/agent-chat-adapter.js +21 -2
  63. package/dist/client/agent-chat-adapter.js.map +1 -1
  64. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  65. package/dist/client/chat/run-recovery.js +15 -10
  66. package/dist/client/chat/run-recovery.js.map +1 -1
  67. package/dist/client/chat/tool-call-display.d.ts +1 -0
  68. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  69. package/dist/client/chat/tool-call-display.js +3 -2
  70. package/dist/client/chat/tool-call-display.js.map +1 -1
  71. package/dist/collab/routes.d.ts +2 -2
  72. package/dist/notifications/routes.d.ts +2 -2
  73. package/dist/observability/routes.d.ts +3 -3
  74. package/dist/progress/routes.d.ts +1 -1
  75. package/dist/resources/handlers.d.ts +1 -1
  76. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  77. package/dist/server/agent-chat-plugin.js +1 -1
  78. package/dist/server/agent-chat-plugin.js.map +1 -1
  79. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  80. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  81. package/dist/shared/streaming-text-smoothing.js +8 -5
  82. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  83. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4912
31
+ - template files: 4917
@@ -1,5 +1,16 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.13
4
+
5
+ ### Patch Changes
6
+
7
+ - dd5b0a4: Honor the long Netlify background-function timeout for durable agent-chat workers and record runtime diagnostics when a background run starts.
8
+ - dd5b0a4: Make agent chat recovery surface completed-tool timeouts more reliably and keep Design variant follow-ups bounded to the selected screen.
9
+ - dd5b0a4: Double the agent chat smooth-streaming reveal speed.
10
+ - dd5b0a4: Avoid noisy Sentry reports from expected chat auth states and best-effort client thread-save conflicts.
11
+ - dd5b0a4: Keep activity-only tool cards and the latest assistant message visibly running during agent chat continuations, steer interrupted Design generation retries toward smaller existing-file edits, and capture run persistence failures with run IDs so missing request IDs are diagnosable.
12
+ - dd5b0a4: Compress the Plan mode composer callout into a quieter inline status pill.
13
+
3
14
  ## 0.84.12
4
15
 
5
16
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.12",
3
+ "version": "0.84.13",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -235,6 +235,36 @@ export function isInBackgroundFunctionRuntime(): boolean {
235
235
  return false;
236
236
  }
237
237
 
238
+ export function backgroundRunMarkerExpectsBackgroundRuntime(
239
+ marker: unknown,
240
+ ): boolean {
241
+ return (
242
+ typeof marker === "object" &&
243
+ marker !== null &&
244
+ (marker as { backgroundFunctionRuntimeExpected?: unknown })
245
+ .backgroundFunctionRuntimeExpected === true
246
+ );
247
+ }
248
+
249
+ export function shouldUseBackgroundFunctionTimeoutForWorker(
250
+ marker: unknown,
251
+ ): boolean {
252
+ return (
253
+ isInBackgroundFunctionRuntime() ||
254
+ backgroundRunMarkerExpectsBackgroundRuntime(marker)
255
+ );
256
+ }
257
+
258
+ export function backgroundRuntimeDiagnosticDetail(marker: unknown): string {
259
+ return [
260
+ `markerExpected=${backgroundRunMarkerExpectsBackgroundRuntime(marker)}`,
261
+ `runtimeDetected=${isInBackgroundFunctionRuntime()}`,
262
+ `globalMarker=${(globalThis as Record<string, unknown>).__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true}`,
263
+ `lambdaNameEndsBackground=${typeof process.env.AWS_LAMBDA_FUNCTION_NAME === "string" && process.env.AWS_LAMBDA_FUNCTION_NAME.toLowerCase().endsWith("-background")}`,
264
+ `forceEnv=${typeof process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME === "string" && process.env.AGENT_CHAT_FORCE_BACKGROUND_RUNTIME.trim().length > 0}`,
265
+ ].join(" ");
266
+ }
267
+
238
268
  function isFlagEnabled(): boolean {
239
269
  // Read the literal key (not `process.env[CONST]`) so guard:no-env-credentials
240
270
  // can statically verify it against the allowlisted `AGENT_*` prefix. Keep this
@@ -53,9 +53,10 @@ import {
53
53
  import { computeProtectedSegmentIds } from "./context-xray/segments.js";
54
54
  import {
55
55
  AGENT_CHAT_BACKGROUND_RUN_FIELD,
56
+ backgroundRuntimeDiagnosticDetail,
56
57
  isAgentChatDurableBackgroundEnabled,
57
- isInBackgroundFunctionRuntime,
58
58
  resolveAgentChatProcessRunDispatchPath,
59
+ shouldUseBackgroundFunctionTimeoutForWorker,
59
60
  } from "./durable-background.js";
60
61
  import {
61
62
  LLM_MISSING_CREDENTIALS_ERROR_CODE,
@@ -4032,6 +4033,7 @@ export async function claimBackgroundWorkerRunEarly(opts: {
4032
4033
  requestTurnId?: string | null;
4033
4034
  continuationCount: number;
4034
4035
  runsInBackgroundFunction: boolean;
4036
+ backgroundRuntimeDetail?: string;
4035
4037
  deps?: {
4036
4038
  recordRunDiagnostic?: typeof recordRunDiagnostic;
4037
4039
  insertRun?: typeof insertRun;
@@ -4057,7 +4059,13 @@ export async function claimBackgroundWorkerRunEarly(opts: {
4057
4059
  await record(
4058
4060
  opts.runId,
4059
4061
  RUN_DIAG_STAGE.workerEntered,
4060
- `runsInBackgroundFunction=${opts.runsInBackgroundFunction} continuationCount=${opts.continuationCount}`,
4062
+ [
4063
+ `runsInBackgroundFunction=${opts.runsInBackgroundFunction}`,
4064
+ `continuationCount=${opts.continuationCount}`,
4065
+ opts.backgroundRuntimeDetail,
4066
+ ]
4067
+ .filter(Boolean)
4068
+ .join(" "),
4061
4069
  ).catch(() => {});
4062
4070
 
4063
4071
  if (opts.continuationCount > 0) {
@@ -4218,7 +4226,11 @@ export function createProductionAgentHandler(
4218
4226
  // function. Only a true value unlocks the ~13-min soft-timeout budget; a
4219
4227
  // worker on the 60s function keeps the 40s clamp and checkpoints cleanly.
4220
4228
  const runsInBackgroundFunction =
4221
- isBackgroundWorker && isInBackgroundFunctionRuntime();
4229
+ isBackgroundWorker &&
4230
+ shouldUseBackgroundFunctionTimeoutForWorker(backgroundRunMarker);
4231
+ const backgroundRuntimeDetail = isBackgroundWorker
4232
+ ? backgroundRuntimeDiagnosticDetail(backgroundRunMarker)
4233
+ : "";
4222
4234
  // How many server-driven background continuations have already chained into
4223
4235
  // this logical turn (0 on the first chunk). Used to bound the chain.
4224
4236
  const backgroundContinuationCount =
@@ -4239,6 +4251,7 @@ export function createProductionAgentHandler(
4239
4251
  requestTurnId,
4240
4252
  continuationCount: backgroundContinuationCount,
4241
4253
  runsInBackgroundFunction,
4254
+ backgroundRuntimeDetail,
4242
4255
  });
4243
4256
  if (!earlyClaim.claimed) {
4244
4257
  return { ok: true, skipped: earlyClaim.skipped };
@@ -5001,6 +5014,9 @@ export function createProductionAgentHandler(
5001
5014
  }
5002
5015
 
5003
5016
  let dispatched = false;
5017
+ const backgroundDispatchPath = resolveAgentChatProcessRunDispatchPath();
5018
+ const expectsNetlifyBackgroundFunction =
5019
+ backgroundDispatchPath.startsWith("/.netlify/functions/");
5004
5020
  try {
5005
5021
  await fireInternalDispatch({
5006
5022
  event,
@@ -5014,7 +5030,7 @@ export function createProductionAgentHandler(
5014
5030
  // inline. `fireInternalDispatch` strips the app base path for
5015
5031
  // /.netlify/* targets so the request reaches the host-root function url;
5016
5032
  // the Authorization Bearer HMAC is preserved either way.
5017
- path: resolveAgentChatProcessRunDispatchPath(),
5033
+ path: backgroundDispatchPath,
5018
5034
  taskId: runId,
5019
5035
  body: {
5020
5036
  ...body,
@@ -5022,6 +5038,8 @@ export function createProductionAgentHandler(
5022
5038
  [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
5023
5039
  runId,
5024
5040
  turnId: effectiveTurnId,
5041
+ backgroundFunctionRuntimeExpected:
5042
+ expectsNetlifyBackgroundFunction,
5025
5043
  },
5026
5044
  },
5027
5045
  });
@@ -5302,6 +5320,8 @@ export function createProductionAgentHandler(
5302
5320
  runId: nextRunId,
5303
5321
  turnId: effectiveTurnId,
5304
5322
  continuationCount: backgroundContinuationCount + 1,
5323
+ backgroundFunctionRuntimeExpected:
5324
+ runsInBackgroundFunction,
5305
5325
  },
5306
5326
  },
5307
5327
  });
@@ -5335,7 +5355,13 @@ export function createProductionAgentHandler(
5335
5355
  await recordRunDiagnostic(
5336
5356
  runId,
5337
5357
  RUN_DIAG_STAGE.workerEntered,
5338
- `runsInBackgroundFunction=${runsInBackgroundFunction} continuationCount=${backgroundContinuationCount}`,
5358
+ [
5359
+ `runsInBackgroundFunction=${runsInBackgroundFunction}`,
5360
+ `continuationCount=${backgroundContinuationCount}`,
5361
+ backgroundRuntimeDetail,
5362
+ ]
5363
+ .filter(Boolean)
5364
+ .join(" "),
5339
5365
  ).catch(() => {});
5340
5366
  if (isChainedBackgroundContinuation) {
5341
5367
  await insertRun(runId, effectiveThreadId, effectiveTurnId, {
@@ -5368,7 +5394,9 @@ export function createProductionAgentHandler(
5368
5394
  const setupDetail =
5369
5395
  Object.entries(setupMarks)
5370
5396
  .map(([k, v]) => `${k}=${v}`)
5371
- .join(" ") + ` total=${Date.now() - setupT0}`;
5397
+ .join(" ") +
5398
+ ` total=${Date.now() - setupT0}` +
5399
+ (backgroundRuntimeDetail ? ` ${backgroundRuntimeDetail}` : "");
5372
5400
 
5373
5401
  startRun(
5374
5402
  runId,
@@ -349,11 +349,45 @@ export function startRun(
349
349
  activeRuns.set(runId, run);
350
350
  threadToRun.set(threadId, runId);
351
351
 
352
+ const captureRunPersistenceError = (
353
+ error: unknown,
354
+ phase: "insert-run" | "insert-event",
355
+ extra: Record<string, unknown> = {},
356
+ ) => {
357
+ captureError(error, {
358
+ route: "/_agent-native/agent-chat",
359
+ tags: {
360
+ source: "agent-run-manager",
361
+ phase,
362
+ runStatus: run.status,
363
+ },
364
+ extra: {
365
+ runId,
366
+ threadId,
367
+ eventCount: run.events.length,
368
+ startedAt: run.startedAt,
369
+ ...extra,
370
+ },
371
+ contexts: {
372
+ agentRun: {
373
+ runId,
374
+ threadId,
375
+ status: run.status,
376
+ phase,
377
+ eventCount: run.events.length,
378
+ startedAt: run.startedAt,
379
+ },
380
+ },
381
+ });
382
+ };
383
+
352
384
  // Persist run to SQL without blocking the response. Keep the promise so
353
385
  // final status cannot race ahead of a slow initial INSERT and then get
354
386
  // overwritten by a late row stuck at status='running'.
355
387
  const insertRunPromise = insertRun(runId, threadId, options?.turnId).catch(
356
- () => {},
388
+ (error) => {
389
+ captureRunPersistenceError(error, "insert-run");
390
+ },
357
391
  );
358
392
 
359
393
  // Per-run event persistence chain: events are chained so SQL inserts commit
@@ -368,6 +402,7 @@ export function startRun(
368
402
  // chunk. The stuck-detector threshold is on the order of tens of seconds,
369
403
  // so 1s resolution is plenty.
370
404
  let lastProgressBumpAt = 0;
405
+ let eventPersistenceErrorCaptured = false;
371
406
  const bumpProgressIfDue = () => {
372
407
  const now = Date.now();
373
408
  if (now - lastProgressBumpAt < 1000) return;
@@ -494,7 +529,15 @@ export function startRun(
494
529
  const thisInsert = persistenceChain.then(() =>
495
530
  insertRunEvent(runId, runEvent.seq, JSON.stringify(runEvent.event)),
496
531
  );
497
- persistenceChain = thisInsert.catch(() => {});
532
+ persistenceChain = thisInsert.catch((error) => {
533
+ if (!eventPersistenceErrorCaptured) {
534
+ eventPersistenceErrorCaptured = true;
535
+ captureRunPersistenceError(error, "insert-event", {
536
+ seq: runEvent.seq,
537
+ eventType: runEvent.event.type,
538
+ });
539
+ }
540
+ });
498
541
  const persistence = thisInsert;
499
542
  if (!options?.surfacePersistenceError) {
500
543
  persistence.catch(() => {});
@@ -712,6 +712,7 @@ export interface UpdateThreadDataOptions {
712
712
  preserveExistingQueuedMessages?: boolean;
713
713
  preserveExistingTopLevelKeys?: boolean;
714
714
  maxAttempts?: number;
715
+ ignoreConflicts?: boolean;
715
716
  }
716
717
 
717
718
  function parseThreadData(value: string): any {
@@ -792,6 +793,7 @@ export async function updateThreadData(
792
793
  }
793
794
 
794
795
  if (lastConflict) {
796
+ if (options.ignoreConflicts) return;
795
797
  throw new Error(
796
798
  `Failed to update chat thread ${id} after concurrent write conflicts.`,
797
799
  );
@@ -329,15 +329,18 @@ iteration, or a human-in-the-loop choice among design directions.
329
329
  overview board and renders an inline chat choice with one button per screen
330
330
  name. After the user picks, delete the unchosen variant screens and continue
331
331
  from the kept screen by first calling \`get-design-snapshot\` with that
332
- screen's \`fileId\`, then making a bounded single-file pass.
332
+ screen's \`fileId\`, then calling \`edit-design\` on that same \`fileId\` in a
333
+ bounded single-file pass. Use \`mode: "replace-file"\` when expanding the
334
+ representative placeholder into the full chosen direction. Do not call
335
+ \`generate-design\` after a variant pick.
333
336
  - If the chat choice buttons are not available in the host, ask the user to
334
337
  tell you the screen name they prefer. The variants are already real screens
335
338
  on the board, so do not ask them to paste HTML or copy a generated handoff
336
339
  summary.
337
340
  - For direct refinements to an already chosen direction, call
338
341
  \`get-design-snapshot\`, edit from the current tuned HTML, and use
339
- \`edit-design\` for surgical changes. Use \`generate-design\` for new files
340
- or larger structural rewrites.
342
+ \`edit-design\` for surgical changes or \`mode: "replace-file"\` for a bounded
343
+ selected-file replacement. Use \`generate-design\` for new files only.
341
344
  - Use \`export-coding-handoff\` when the user wants to implement the chosen
342
345
  design in a codebase.
343
346
 
@@ -353,9 +356,10 @@ iteration, or a human-in-the-loop choice among design directions.
353
356
  5. After \`present-design-variants\`, wait for the user's pick before
354
357
  generating the next version. Keep the chosen screen, delete the other
355
358
  variant screens, call \`get-design-snapshot\` with \`fileId\` for the kept
356
- screen, then continue from that single file in a bounded pass. Prefer
357
- \`edit-design\`; if \`generate-design\` is needed, update only the kept file
358
- compactly and stop after the first successful save.
359
+ screen, then call \`edit-design\` on that same \`fileId\` in a bounded pass.
360
+ Use \`mode: "replace-file"\` when expanding the representative placeholder
361
+ into the full chosen direction. Do not call \`generate-design\` after a
362
+ variant pick. Stop after the first successful \`edit-design\` save.
359
363
 
360
364
  ## Design Quality Bar
361
365
 
@@ -601,6 +601,31 @@ export function resolveAssistantChatSubmitIntent({
601
601
  return requestedIntent ?? "immediate";
602
602
  }
603
603
 
604
+ export function resolveAssistantChatRunningState({
605
+ forceStopped,
606
+ isRuntimeRunning,
607
+ isReconnecting,
608
+ optimisticRunning,
609
+ isAutoResuming,
610
+ }: {
611
+ forceStopped: boolean;
612
+ isRuntimeRunning: boolean;
613
+ isReconnecting: boolean;
614
+ optimisticRunning: boolean;
615
+ isAutoResuming: boolean;
616
+ }): { isRunning: boolean; showRunningInUI: boolean } {
617
+ const isRunning =
618
+ !forceStopped && (isRuntimeRunning || isReconnecting || optimisticRunning);
619
+ return {
620
+ isRunning,
621
+ // During auto-continuation, assistant-ui can briefly mark the message done
622
+ // between chunks even though the adapter is about to POST the next run.
623
+ // Keep the visible chat state running so the latest assistant message shows
624
+ // Thinking/Resuming and does not expose footer actions prematurely.
625
+ showRunningInUI: !forceStopped && (isRunning || isAutoResuming),
626
+ };
627
+ }
628
+
604
629
  type QueuedMessage = {
605
630
  id: string;
606
631
  text: string;
@@ -1378,14 +1403,17 @@ const AssistantChatInner = forwardRef<
1378
1403
  // queueing forever" state where isReconnecting or isRuntimeRunning gets
1379
1404
  // wedged (e.g. after a tab refresh + stop during reconnect).
1380
1405
  const [forceStopped, setForceStopped] = useState(false);
1381
- // Real running state drives submission/queue gating. Treat reconnecting
1382
- // to an active run the same as running, UNLESS the user has explicitly
1383
- // clicked stop (forceStopped).
1384
- const isRunning =
1385
- !forceStopped && (isRuntimeRunning || isReconnecting || optimisticRunning);
1386
- const textStreaming = isRunning || externalStreaming;
1387
- // UI-only running state — drives the stop button and thinking indicator.
1388
- const showRunningInUI = isRunning;
1406
+ // Real running state drives submission/queue gating; UI running also covers
1407
+ // short auto-continuation gaps so the latest assistant message does not flash
1408
+ // into a done state while the agent is still working.
1409
+ const { isRunning, showRunningInUI } = resolveAssistantChatRunningState({
1410
+ forceStopped,
1411
+ isRuntimeRunning,
1412
+ isReconnecting,
1413
+ optimisticRunning,
1414
+ isAutoResuming,
1415
+ });
1416
+ const textStreaming = showRunningInUI || externalStreaming;
1389
1417
  // A revealed activity label wins; otherwise stay a steady "Thinking" by
1390
1418
  // default. We only surface "Reconnecting" while we are actively replaying
1391
1419
  // recovered content (reconnectContent populated). A bare reconnect with no
@@ -2265,6 +2293,8 @@ const AssistantChatInner = forwardRef<
2265
2293
 
2266
2294
  useEffect(() => {
2267
2295
  if (!authError) return;
2296
+ const shouldCaptureStuckAuthCard =
2297
+ authSessionAvailable || authError.sessionExpired;
2268
2298
  // Auto-recovery (`checkAuthSession`) runs immediately + at 250ms. If the
2269
2299
  // card is still showing 3 seconds later, recovery failed and the user
2270
2300
  // is about to hit "Refresh chat" — that's the "Reload UI required"
@@ -2273,6 +2303,7 @@ const AssistantChatInner = forwardRef<
2273
2303
  void (async () => {
2274
2304
  const hasSession = await checkAuthSession();
2275
2305
  if (hasSession) return;
2306
+ if (!shouldCaptureStuckAuthCard) return;
2276
2307
  captureError(new Error("agent-chat:auth_error_card_stuck"), {
2277
2308
  tags: {
2278
2309
  context: "agent-native-chat",
@@ -3211,7 +3242,7 @@ const AssistantChatInner = forwardRef<
3211
3242
  <CheckpointContext.Provider value={checkpointCtx}>
3212
3243
  <MessageActionsContext.Provider value={messageActionsCtx}>
3213
3244
  <ApprovalContext.Provider value={approvalCtx}>
3214
- <ChatRunningContext.Provider value={isRunning}>
3245
+ <ChatRunningContext.Provider value={showRunningInUI}>
3215
3246
  <TextStreamingContext.Provider value={textStreaming}>
3216
3247
  <div
3217
3248
  data-agent-empty-state={
@@ -807,6 +807,21 @@ function lastActivityTool(
807
807
  return undefined;
808
808
  }
809
809
 
810
+ function formatActivityTrail(
811
+ trail: readonly AgentActivityTrailEntry[],
812
+ ): string | undefined {
813
+ const items = trail
814
+ .slice(-8)
815
+ .map((entry) => {
816
+ const label = entry.label.replace(/\s+/g, " ").trim();
817
+ const tool = entry.tool?.replace(/\s+/g, " ").trim();
818
+ if (label && tool && label !== tool) return `${label} (${tool})`;
819
+ return label || tool || "";
820
+ })
821
+ .filter(Boolean);
822
+ return items.length > 0 ? items.join(" > ") : undefined;
823
+ }
824
+
810
825
  function lastUnresolvedToolActivity(
811
826
  content: ContentPart[],
812
827
  ): string | undefined {
@@ -928,9 +943,9 @@ function incrementalActionGuidance(tool: string): string | undefined {
928
943
  return "create a compact working v1 with `create-extension`, then use focused `update-extension` edits for refinements";
929
944
  case "generate-design":
930
945
  case "update-design":
931
- return "persist a minimal first version (fewer files) with `generate-design`, then refine individual files with `edit-design` search/replace instead of resending everything";
946
+ return 'if an existing design file or snapshot is already in history, especially after a Design variant pick, stop retrying `generate-design`: call `get-design-snapshot` for the selected file, then call `edit-design` once on that same `fileId` (`mode: "replace-file"` for a compact full-file replacement, or search/replace for smaller edits). Use `generate-design` only for a brand-new compact first file when no target file exists yet';
932
947
  case "present-design-variants":
933
- return "call `present-design-variants` with concise labels, descriptions, accent colors, and feature bullets; omit large `content` HTML when needed so the action can render compact representative screens, then refine the chosen direction with `generate-design` or `edit-design`";
948
+ return 'call `present-design-variants` with concise labels, descriptions, accent colors, and feature bullets; omit large `content` HTML when needed so the action can render compact representative screens. After the user picks a direction, delete unchosen screens, snapshot the selected `fileId`, and refine that same file with `edit-design` (`mode: "replace-file"` for placeholder expansion). Do not call `generate-design` after the variant pick';
934
949
  case "create-visual-plan":
935
950
  case "create-ui-plan":
936
951
  case "create-plan-design":
@@ -1439,6 +1454,7 @@ export function createAgentChatAdapter(
1439
1454
  let visibleContinuationPrefix: ContentPart[] = [];
1440
1455
  let lastAutoContinueReason: string | null = null;
1441
1456
  let lastRecoverableRunError: AgentAutoContinueErrorInfo | null = null;
1457
+ let lastActivityTrail: AgentActivityTrailEntry[] = [];
1442
1458
  const attemptedRunIds: string[] = [];
1443
1459
  let authRecoveryAttempted = false;
1444
1460
  let continuationToolCallCounter = 0;
@@ -1468,6 +1484,9 @@ export function createAgentChatAdapter(
1468
1484
  lastPreparingToolName
1469
1485
  ? `last_preparing_tool: ${lastPreparingToolName}`
1470
1486
  : "",
1487
+ formatActivityTrail(lastActivityTrail)
1488
+ ? `activity_trail: ${formatActivityTrail(lastActivityTrail)}`
1489
+ : "",
1471
1490
  `total_transient_continuations: ${totalTransientContinuationAttempts}`,
1472
1491
  attemptedRunIds.length > 0
1473
1492
  ? `attempted_runs: ${attemptedRunIds.join(", ")}`
@@ -1559,6 +1578,7 @@ export function createAgentChatAdapter(
1559
1578
  lastSeq,
1560
1579
  contentParts: content.length,
1561
1580
  attemptedRunIds: [...attemptedRunIds],
1581
+ activityTrail: [...lastActivityTrail],
1562
1582
  startupRecoveryAttempts,
1563
1583
  staleRunContinuationAttempts,
1564
1584
  stalledTransientContinuationAttempts,
@@ -1867,6 +1887,7 @@ export function createAgentChatAdapter(
1867
1887
  completedToolName?: string;
1868
1888
  } => {
1869
1889
  lastAutoContinueReason = signal.reason;
1890
+ lastActivityTrail = [...signal.activityTrail];
1870
1891
  if (signal.errorInfo) {
1871
1892
  lastRecoverableRunError = signal.errorInfo;
1872
1893
  }
@@ -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
  );
@@ -7694,6 +7694,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7694
7694
  nextTitle,
7695
7695
  nextPreview,
7696
7696
  newMessageCount,
7697
+ { ignoreConflicts: true },
7697
7698
  );
7698
7699
  // Scope updates piggyback on the PUT — the client uses this
7699
7700
  // path for both "detach" (scope: null) and "retag" flows.