@agent-native/core 0.84.13 → 0.84.16

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 (96) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/package.json +2 -2
  4. package/corpus/core/src/agent/durable-background.ts +18 -5
  5. package/corpus/core/src/agent/engine/builder-engine.ts +7 -3
  6. package/corpus/core/src/agent/production-agent.ts +48 -7
  7. package/corpus/core/src/agent/run-manager.ts +39 -2
  8. package/corpus/core/src/agent/run-store.ts +55 -11
  9. package/corpus/core/src/client/AssistantChat.tsx +30 -4
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +11 -3
  11. package/corpus/core/src/client/agent-chat-adapter.ts +21 -3
  12. package/corpus/core/src/client/agent-chat.ts +18 -3
  13. package/corpus/core/src/client/blocks/library/diagram.tsx +3 -3
  14. package/corpus/core/src/client/chat/message-components.tsx +17 -0
  15. package/corpus/core/src/client/use-agent-engine-configured.ts +7 -4
  16. package/corpus/core/src/db/client.ts +10 -1
  17. package/corpus/core/src/observability/traces.ts +17 -3
  18. package/corpus/core/src/server/agent-chat-plugin.ts +80 -44
  19. package/corpus/core/src/styles/blocks.css +44 -1
  20. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +18 -24
  21. package/corpus/templates/analytics/changelog/2026-07-01-dashboard-charts-no-longer-show-future-dates-when-clients-se.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-07-01-dashboard-filters-take-less-space-and-keep-view-actions-tucked.md +6 -0
  23. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
  24. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +6 -2
  25. package/corpus/templates/analytics/server/lib/session-replay.ts +109 -50
  26. package/corpus/templates/analytics/server/plugins/db.ts +21 -0
  27. package/corpus/templates/clips/changelog/2026-07-01-chrome-extension-recordings-stream-uploads-while-recording.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  29. package/corpus/templates/clips/chrome-extension/src/background.ts +8 -0
  30. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +61 -16
  31. package/corpus/templates/design/app/pages/DesignEditor.tsx +176 -13
  32. package/corpus/templates/design/changelog/2026-07-01-exporting-a-design-as-png-or-svg-now-downloads-just-the-sele.md +6 -0
  33. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +44 -1
  34. package/corpus/templates/plan/changelog/2026-07-01-visual-plan-and-recap-diagram-labels-now-wrap-inside-their-b.md +6 -0
  35. package/dist/agent/durable-background.d.ts +1 -0
  36. package/dist/agent/durable-background.d.ts.map +1 -1
  37. package/dist/agent/durable-background.js +17 -3
  38. package/dist/agent/durable-background.js.map +1 -1
  39. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  40. package/dist/agent/engine/builder-engine.js +7 -1
  41. package/dist/agent/engine/builder-engine.js.map +1 -1
  42. package/dist/agent/production-agent.d.ts.map +1 -1
  43. package/dist/agent/production-agent.js +32 -7
  44. package/dist/agent/production-agent.js.map +1 -1
  45. package/dist/agent/run-manager.d.ts +2 -0
  46. package/dist/agent/run-manager.d.ts.map +1 -1
  47. package/dist/agent/run-manager.js +28 -3
  48. package/dist/agent/run-manager.js.map +1 -1
  49. package/dist/agent/run-store.d.ts +9 -0
  50. package/dist/agent/run-store.d.ts.map +1 -1
  51. package/dist/agent/run-store.js +47 -11
  52. package/dist/agent/run-store.js.map +1 -1
  53. package/dist/client/AssistantChat.d.ts +8 -2
  54. package/dist/client/AssistantChat.d.ts.map +1 -1
  55. package/dist/client/AssistantChat.js +18 -2
  56. package/dist/client/AssistantChat.js.map +1 -1
  57. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  58. package/dist/client/MultiTabAssistantChat.js +7 -3
  59. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  60. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  61. package/dist/client/agent-chat-adapter.js +19 -3
  62. package/dist/client/agent-chat-adapter.js.map +1 -1
  63. package/dist/client/agent-chat.d.ts +7 -0
  64. package/dist/client/agent-chat.d.ts.map +1 -1
  65. package/dist/client/agent-chat.js +5 -1
  66. package/dist/client/agent-chat.js.map +1 -1
  67. package/dist/client/blocks/library/diagram.js +3 -3
  68. package/dist/client/blocks/library/diagram.js.map +1 -1
  69. package/dist/client/chat/message-components.d.ts +3 -1
  70. package/dist/client/chat/message-components.d.ts.map +1 -1
  71. package/dist/client/chat/message-components.js +16 -2
  72. package/dist/client/chat/message-components.js.map +1 -1
  73. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  74. package/dist/client/use-agent-engine-configured.js +7 -1
  75. package/dist/client/use-agent-engine-configured.js.map +1 -1
  76. package/dist/collab/routes.d.ts +2 -2
  77. package/dist/db/client.d.ts +6 -0
  78. package/dist/db/client.d.ts.map +1 -1
  79. package/dist/db/client.js +8 -1
  80. package/dist/db/client.js.map +1 -1
  81. package/dist/notifications/routes.d.ts +2 -2
  82. package/dist/observability/routes.d.ts +3 -3
  83. package/dist/observability/traces.d.ts +5 -0
  84. package/dist/observability/traces.d.ts.map +1 -1
  85. package/dist/observability/traces.js +9 -3
  86. package/dist/observability/traces.js.map +1 -1
  87. package/dist/progress/routes.d.ts +1 -1
  88. package/dist/resources/handlers.d.ts +3 -3
  89. package/dist/server/agent-chat-plugin.d.ts +2 -1
  90. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  91. package/dist/server/agent-chat-plugin.js +64 -42
  92. package/dist/server/agent-chat-plugin.js.map +1 -1
  93. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  94. package/dist/server/transcribe-voice.d.ts +1 -1
  95. package/dist/styles/blocks.css +44 -1
  96. package/package.json +2 -2
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: 4917
31
+ - template files: 4922
@@ -1,5 +1,30 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.16
4
+
5
+ ### Patch Changes
6
+
7
+ - 564460a: Add an optional `focus` flag to `setAgentChatContextItem` (and thread it through to the composer). Callers that mirror ambient UI state into chat context — such as a design canvas element selection — can now pass `focus: false` to stage the context chip without moving keyboard focus into the composer. This stops passive context staging from blurring and tearing down an in-progress inline text editor in the Design canvas (which re-fires on every selection and on each get-design poll during an agent run). Focus stays enabled by default, so existing callers are unchanged.
8
+ - 2b27c0f: Avoid unsupported array helpers in Builder engine message caching and stabilize prep-load tests.
9
+ - a4f5303: Keep background chat continuation markers and database pool detection aligned with the actual background function dispatch path.
10
+
11
+ ## 0.84.15
12
+
13
+ ### Patch Changes
14
+
15
+ - 946ff86: Fix the "No LLM provider is connected" chat banner staying visible after a
16
+ provider is connected. The composer now clears that error once the engine
17
+ status flips to configured, and the status check no longer reads a stale
18
+ cached "missing" response after connecting.
19
+
20
+ ## 0.84.14
21
+
22
+ ### Patch Changes
23
+
24
+ - bbc0a56: Persist terminal reasons for agent runs and classify recoverable run-timeout continuations separately in traces.
25
+ - bbc0a56: Improve agent run diagnostics by recording terminal continuation reasons and adding safe run context to copied recovery debug details.
26
+ - bbc0a56: Keep visual plan and recap diagram labels wrapped inside their boxes instead of overflowing authored diagram shapes.
27
+
3
28
  ## 0.84.13
4
29
 
5
30
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.13",
3
+ "version": "0.84.16",
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": {
@@ -184,7 +184,7 @@
184
184
  "build": "tsgo && tsgo -p tsconfig.cli.json && node scripts/finalize-build.mjs && node scripts/check-dist-imports.mjs",
185
185
  "dev": "tsgo --watch",
186
186
  "typecheck": "tsgo --noEmit",
187
- "test": "vitest --run src",
187
+ "test": "vitest --run src --maxWorkers=25%",
188
188
  "prepack": "tsx ../../scripts/sync-workspace-core-skills.ts --check && npm run build && cp ../../README.md ./README.md",
189
189
  "prepublishOnly": "npm run build"
190
190
  },
@@ -97,6 +97,17 @@ function resolveWorkspaceBackgroundFunctionUrlPath(): string | null {
97
97
  return `/.netlify/functions/${candidate}-agent-background`;
98
98
  }
99
99
 
100
+ function isNetlifyHostedRuntimeForDispatch(): boolean {
101
+ if (process.env.NETLIFY_LOCAL === "true") return false;
102
+ if (process.env.NETLIFY === "false") return false;
103
+ if (process.env.NETLIFY && process.env.NETLIFY !== "false") return true;
104
+ // Netlify sets AWS Lambda runtime env on deployed Functions, but the build-time
105
+ // NETLIFY flag is not always present in the runtime isolate. Treat Lambda as
106
+ // Netlify here unless Netlify was explicitly disabled above; non-Netlify AWS
107
+ // falls back inline if the /.netlify/functions dispatch fast-fails.
108
+ return Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME);
109
+ }
110
+
100
111
  /**
101
112
  * Resolve the path the foreground POST should self-dispatch the chat background
102
113
  * worker to.
@@ -129,11 +140,7 @@ function resolveWorkspaceBackgroundFunctionUrlPath(): string | null {
129
140
  * to shadow because `/.netlify/*` is already excluded from the `server` catch-all.
130
141
  */
131
142
  export function resolveAgentChatProcessRunDispatchPath(): string {
132
- if (
133
- process.env.NETLIFY &&
134
- process.env.NETLIFY !== "false" &&
135
- process.env.NETLIFY_LOCAL !== "true"
136
- ) {
143
+ if (isNetlifyHostedRuntimeForDispatch()) {
137
144
  return (
138
145
  resolveWorkspaceBackgroundFunctionUrlPath() ??
139
146
  AGENT_BACKGROUND_FUNCTION_URL_PATH
@@ -142,6 +149,12 @@ export function resolveAgentChatProcessRunDispatchPath(): string {
142
149
  return AGENT_CHAT_PROCESS_RUN_PATH;
143
150
  }
144
151
 
152
+ export function dispatchPathTargetsNetlifyBackgroundFunction(
153
+ dispatchPath: string,
154
+ ): boolean {
155
+ return dispatchPath.startsWith("/.netlify/functions/");
156
+ }
157
+
145
158
  /**
146
159
  * Env flag for durable background runs. DEFAULT-OFF (opt-in): unset means
147
160
  * disabled; an app opts IN with an explicit truthy value (`true`/`1`/`yes`/`on`).
@@ -193,9 +193,13 @@ class BuilderEngine implements AgentEngine {
193
193
  // content block so the entire conversation prefix is cached.
194
194
  let cachedMessages = messages;
195
195
  if (cacheEnabled && messages.length > 0) {
196
- const lastUserIdx = [...messages].findLastIndex(
197
- (m: any) => m.role === "user",
198
- );
196
+ let lastUserIdx = -1;
197
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
198
+ if ((messages[i] as any).role === "user") {
199
+ lastUserIdx = i;
200
+ break;
201
+ }
202
+ }
199
203
  if (lastUserIdx >= 0) {
200
204
  cachedMessages = [...messages];
201
205
  const lastMsg = { ...cachedMessages[lastUserIdx] } as any;
@@ -54,6 +54,7 @@ import { computeProtectedSegmentIds } from "./context-xray/segments.js";
54
54
  import {
55
55
  AGENT_CHAT_BACKGROUND_RUN_FIELD,
56
56
  backgroundRuntimeDiagnosticDetail,
57
+ dispatchPathTargetsNetlifyBackgroundFunction,
57
58
  isAgentChatDurableBackgroundEnabled,
58
59
  resolveAgentChatProcessRunDispatchPath,
59
60
  shouldUseBackgroundFunctionTimeoutForWorker,
@@ -124,6 +125,7 @@ import {
124
125
  insertRun,
125
126
  updateRunHeartbeat,
126
127
  updateRunStatusIfRunning,
128
+ setRunTerminalReason,
127
129
  claimBackgroundRun,
128
130
  readBackgroundRunClaim,
129
131
  recordRunDiagnostic,
@@ -5016,7 +5018,7 @@ export function createProductionAgentHandler(
5016
5018
  let dispatched = false;
5017
5019
  const backgroundDispatchPath = resolveAgentChatProcessRunDispatchPath();
5018
5020
  const expectsNetlifyBackgroundFunction =
5019
- backgroundDispatchPath.startsWith("/.netlify/functions/");
5021
+ dispatchPathTargetsNetlifyBackgroundFunction(backgroundDispatchPath);
5020
5022
  try {
5021
5023
  await fireInternalDispatch({
5022
5024
  event,
@@ -5089,7 +5091,17 @@ export function createProductionAgentHandler(
5089
5091
  }
5090
5092
  // A background worker owns this run but we cannot subscribe — surface an
5091
5093
  // error rather than risk a double-run by falling through to inline.
5092
- await updateRunStatusIfRunning(runId, "errored").catch(() => {});
5094
+ const terminalReason =
5095
+ backgroundOutcome.action === "stream"
5096
+ ? "background_subscribe_failed"
5097
+ : "background_dispatch_failed";
5098
+ const statusUpdated = await updateRunStatusIfRunning(
5099
+ runId,
5100
+ "errored",
5101
+ ).catch(() => false);
5102
+ if (statusUpdated) {
5103
+ await setRunTerminalReason(runId, terminalReason).catch(() => {});
5104
+ }
5093
5105
  setResponseStatus(event, 500);
5094
5106
  return {
5095
5107
  error:
@@ -5302,6 +5314,12 @@ export function createProductionAgentHandler(
5302
5314
  // its seq log starts clean; same turnId folds the assistant
5303
5315
  // message across chunks.
5304
5316
  const nextRunId = generateRunId();
5317
+ const continuationDispatchPath =
5318
+ resolveAgentChatProcessRunDispatchPath();
5319
+ const continuationExpectsNetlifyBackgroundFunction =
5320
+ dispatchPathTargetsNetlifyBackgroundFunction(
5321
+ continuationDispatchPath,
5322
+ );
5305
5323
  try {
5306
5324
  await fireInternalDispatch({
5307
5325
  event,
@@ -5311,7 +5329,7 @@ export function createProductionAgentHandler(
5311
5329
  // background:true; never shadowed because /.netlify/* is
5312
5330
  // excluded from the /* catch-all) so each chunk keeps the
5313
5331
  // 15-min budget; off-Netlify the in-process framework route.
5314
- path: resolveAgentChatProcessRunDispatchPath(),
5332
+ path: continuationDispatchPath,
5315
5333
  taskId: nextRunId,
5316
5334
  body: {
5317
5335
  ...body,
@@ -5321,7 +5339,7 @@ export function createProductionAgentHandler(
5321
5339
  turnId: effectiveTurnId,
5322
5340
  continuationCount: backgroundContinuationCount + 1,
5323
5341
  backgroundFunctionRuntimeExpected:
5324
- runsInBackgroundFunction,
5342
+ continuationExpectsNetlifyBackgroundFunction,
5325
5343
  },
5326
5344
  },
5327
5345
  });
@@ -5333,9 +5351,16 @@ export function createProductionAgentHandler(
5333
5351
  "[agent-chat] background continuation dispatch failed:",
5334
5352
  chainErr instanceof Error ? chainErr.message : chainErr,
5335
5353
  );
5336
- await updateRunStatusIfRunning(runId, "errored").catch(
5337
- () => {},
5338
- );
5354
+ const statusUpdated = await updateRunStatusIfRunning(
5355
+ runId,
5356
+ "errored",
5357
+ ).catch(() => false);
5358
+ if (statusUpdated) {
5359
+ await setRunTerminalReason(
5360
+ runId,
5361
+ "background_continuation_dispatch_failed",
5362
+ ).catch(() => {});
5363
+ }
5339
5364
  }
5340
5365
  }
5341
5366
  } finally {
@@ -5766,6 +5791,22 @@ export function createProductionAgentHandler(
5766
5791
  threadId: threadId ?? null,
5767
5792
  userId: ownerEmail,
5768
5793
  config: obsConfig,
5794
+ classifyError: () => {
5795
+ if (
5796
+ agentLoopOpts.signal.aborted &&
5797
+ agentLoopOpts.signal.reason === "run_timeout"
5798
+ ) {
5799
+ return {
5800
+ status: "success",
5801
+ errorMessage: null,
5802
+ metadata: {
5803
+ terminalReason: "run_timeout",
5804
+ recoverableContinuation: true,
5805
+ },
5806
+ };
5807
+ }
5808
+ return null;
5809
+ },
5769
5810
  });
5770
5811
  }
5771
5812
  } catch (err) {
@@ -18,6 +18,7 @@ import {
18
18
  reapUnclaimedBackgroundRun,
19
19
  ensureTerminalRunEvent,
20
20
  setRunError,
21
+ setRunTerminalReason,
21
22
  STALE_RUN_ERROR_EVENT,
22
23
  } from "./run-store.js";
23
24
  import type { AgentChatEvent, RunEvent, RunStatus } from "./types.js";
@@ -293,6 +294,26 @@ function isTerminalRunEvent(event: AgentChatEvent): boolean {
293
294
  );
294
295
  }
295
296
 
297
+ function terminalReasonForRun(
298
+ finalStatus: "completed" | "errored" | "aborted",
299
+ terminalEvent: AgentChatEvent | null,
300
+ abortReason: string | undefined,
301
+ completionError: unknown,
302
+ ): string {
303
+ if (terminalEvent?.type === "auto_continue") {
304
+ return terminalEvent.reason || "auto_continue";
305
+ }
306
+ if (terminalEvent?.type === "loop_limit") return "loop_limit";
307
+ if (terminalEvent?.type === "missing_api_key") return "missing_api_key";
308
+ if (terminalEvent?.type === "error") {
309
+ return `error:${terminalEvent.errorCode || "unknown"}`;
310
+ }
311
+ if (finalStatus === "aborted") return `aborted:${abortReason ?? "user"}`;
312
+ if (completionError) return "completion_error";
313
+ if (finalStatus === "errored") return "error:unknown";
314
+ return "done";
315
+ }
316
+
296
317
  function abortInMemoryRun(run: ActiveRun, reason: string = "user") {
297
318
  run.abortReason = reason;
298
319
  run.status = "aborted";
@@ -459,7 +480,7 @@ export function startRun(
459
480
  type: "auto_continue",
460
481
  reason: "run_timeout",
461
482
  });
462
- abort.abort();
483
+ abort.abort("run_timeout");
463
484
  }, softTimeoutMs)
464
485
  : null;
465
486
  let pendingTerminalEvent: RunEvent | null = null;
@@ -634,6 +655,12 @@ export function startRun(
634
655
  : run.status === "errored" || completionError
635
656
  ? "errored"
636
657
  : "completed";
658
+ const terminalReason = terminalReasonForRun(
659
+ finalStatus,
660
+ pendingTerminalEvent?.event ?? null,
661
+ run.abortReason,
662
+ completionError,
663
+ );
637
664
 
638
665
  // 3. Emit the terminal event only after thread_data is durable. Live
639
666
  // SSE clients close on this event and usually fetch thread_data
@@ -706,7 +733,13 @@ export function startRun(
706
733
  try {
707
734
  await insertRunPromise;
708
735
  if (!terminalPersistenceError) {
709
- await updateRunStatusIfRunning(runId, finalStatus);
736
+ const statusUpdated = await updateRunStatusIfRunning(
737
+ runId,
738
+ finalStatus,
739
+ );
740
+ if (statusUpdated) {
741
+ await setRunTerminalReason(runId, terminalReason);
742
+ }
710
743
  }
711
744
  } catch {
712
745
  // Best-effort — reapIfStale will eventually clean this up via
@@ -1077,6 +1110,8 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1077
1110
  lastProgressAt: number | null;
1078
1111
  /** How the run was dispatched (NULL/foreground, background, background-processing). */
1079
1112
  dispatchMode?: string | null;
1113
+ /** Compact terminal classification, e.g. done, run_timeout, stale_run. */
1114
+ terminalReason?: string | null;
1080
1115
  /**
1081
1116
  * Last reached `_process-run` worker stage as a JSON string
1082
1117
  * `{stage,detail?,at}`. Surfaced so a silent background-worker death is
@@ -1143,6 +1178,7 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1143
1178
  heartbeatAt: sqlRun.heartbeatAt ?? sqlRun.startedAt,
1144
1179
  lastProgressAt: sqlRun.lastProgressAt,
1145
1180
  dispatchMode: sqlRun.dispatchMode,
1181
+ terminalReason: sqlRun.terminalReason,
1146
1182
  diagStage: sqlRun.diagStage,
1147
1183
  };
1148
1184
  }
@@ -1170,6 +1206,7 @@ export async function getActiveRunForThreadAsync(threadId: string): Promise<{
1170
1206
  heartbeatAt: sqlRun.heartbeatAt ?? sqlRun.startedAt,
1171
1207
  lastProgressAt: sqlRun.lastProgressAt,
1172
1208
  dispatchMode: sqlRun.dispatchMode,
1209
+ terminalReason: sqlRun.terminalReason,
1173
1210
  diagStage: sqlRun.diagStage,
1174
1211
  };
1175
1212
  }
@@ -116,6 +116,7 @@ async function ensureRunTables(): Promise<void> {
116
116
  turn_id TEXT,
117
117
  error_code TEXT,
118
118
  error_detail TEXT,
119
+ terminal_reason TEXT,
119
120
  dispatch_mode TEXT,
120
121
  diag_stage TEXT
121
122
  )
@@ -187,6 +188,7 @@ async function ensureRunTables(): Promise<void> {
187
188
  ["turn_id", "TEXT"],
188
189
  ["error_code", "TEXT"],
189
190
  ["error_detail", "TEXT"],
191
+ ["terminal_reason", "TEXT"],
190
192
  ["dispatch_mode", "TEXT"],
191
193
  ["diag_stage", "TEXT"],
192
194
  ["worker_stage", "TEXT"],
@@ -264,6 +266,7 @@ async function ensureRunTables(): Promise<void> {
264
266
  "turn_id",
265
267
  "error_code",
266
268
  "error_detail",
269
+ "terminal_reason",
267
270
  "dispatch_mode",
268
271
  "diag_stage",
269
272
  "worker_stage",
@@ -621,6 +624,28 @@ export async function setRunError(
621
624
  }
622
625
  }
623
626
 
627
+ /**
628
+ * Record why a run reached its terminal status. Unlike error_code/error_detail,
629
+ * this is set for successful checkpoint boundaries too (for example
630
+ * status='completed' + terminal_reason='run_timeout').
631
+ */
632
+ export async function setRunTerminalReason(
633
+ runId: string,
634
+ terminalReason: string | undefined,
635
+ ): Promise<void> {
636
+ if (!terminalReason) return;
637
+ try {
638
+ await ensureRunTables();
639
+ const client = getDbExec();
640
+ await client.execute({
641
+ sql: `UPDATE agent_runs SET terminal_reason = ? WHERE id = ?`,
642
+ args: [terminalReason.slice(0, 200), runId],
643
+ });
644
+ } catch {
645
+ // Diagnostics are best-effort; never let them break completion.
646
+ }
647
+ }
648
+
624
649
  /**
625
650
  * Diagnostic stage names recorded onto a background run as it moves through the
626
651
  * `_process-run` worker pipeline. Each value is the LAST stage successfully
@@ -765,7 +790,8 @@ export async function reapIfStale(
765
790
  SET status = 'errored',
766
791
  completed_at = ?,
767
792
  error_code = ?,
768
- error_detail = ?
793
+ error_detail = ?,
794
+ terminal_reason = ?
769
795
  WHERE id = ?
770
796
  AND status = 'running'
771
797
  AND ${staleClause}`,
@@ -773,6 +799,7 @@ export async function reapIfStale(
773
799
  completedAt,
774
800
  STALE_RUN_ERROR_EVENT.errorCode,
775
801
  STALE_RUN_ERROR_EVENT.details,
802
+ STALE_RUN_ERROR_EVENT.errorCode,
776
803
  runId,
777
804
  ...staleArgs,
778
805
  ],
@@ -821,7 +848,8 @@ export async function reapUnclaimedBackgroundRun(
821
848
  SET status = 'errored',
822
849
  completed_at = ?,
823
850
  error_code = ?,
824
- error_detail = ?
851
+ error_detail = ?,
852
+ terminal_reason = ?
825
853
  WHERE id = ?
826
854
  AND status = 'running'
827
855
  AND dispatch_mode = 'background'
@@ -830,6 +858,7 @@ export async function reapUnclaimedBackgroundRun(
830
858
  completedAt,
831
859
  UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
832
860
  UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.details,
861
+ UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT.errorCode,
833
862
  runId,
834
863
  cutoff,
835
864
  ],
@@ -902,8 +931,8 @@ export async function markRunAborted(
902
931
  await ensureRunTables();
903
932
  const client = getDbExec();
904
933
  await client.execute({
905
- sql: `UPDATE agent_runs SET status = 'aborted', abort_reason = ?, completed_at = ? WHERE id = ?`,
906
- args: [reason ?? "user", Date.now(), runId],
934
+ sql: `UPDATE agent_runs SET status = 'aborted', abort_reason = ?, completed_at = ?, terminal_reason = ? WHERE id = ?`,
935
+ args: [reason ?? "user", Date.now(), `aborted:${reason ?? "user"}`, runId],
907
936
  });
908
937
  await safeAppendTerminalRunEvent(runId, { type: "done" }, "mark-aborted");
909
938
  }
@@ -1005,13 +1034,14 @@ export async function getRunByThread(
1005
1034
  completedAt: number | null;
1006
1035
  lastProgressAt: number | null;
1007
1036
  dispatchMode: string | null;
1037
+ terminalReason: string | null;
1008
1038
  diagStage: string | null;
1009
1039
  } | null> {
1010
1040
  await ensureRunTables();
1011
1041
  const client = getDbExec();
1012
1042
  const sql = options?.includeTerminal
1013
- ? `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, diag_stage FROM agent_runs WHERE thread_id = ? ORDER BY started_at DESC LIMIT 1`
1014
- : `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, diag_stage FROM agent_runs WHERE thread_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1`;
1043
+ ? `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage FROM agent_runs WHERE thread_id = ? ORDER BY started_at DESC LIMIT 1`
1044
+ : `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage FROM agent_runs WHERE thread_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1`;
1015
1045
  const { rows } = await client.execute({ sql, args: [threadId] });
1016
1046
  if (rows.length === 0) return null;
1017
1047
  const r = rows[0] as {
@@ -1024,6 +1054,7 @@ export async function getRunByThread(
1024
1054
  completed_at: number | string | null;
1025
1055
  last_progress_at: number | string | null;
1026
1056
  dispatch_mode?: string | null;
1057
+ terminal_reason?: string | null;
1027
1058
  diag_stage?: string | null;
1028
1059
  };
1029
1060
  return {
@@ -1037,6 +1068,7 @@ export async function getRunByThread(
1037
1068
  lastProgressAt:
1038
1069
  r.last_progress_at == null ? null : Number(r.last_progress_at),
1039
1070
  dispatchMode: r.dispatch_mode ?? null,
1071
+ terminalReason: r.terminal_reason ?? null,
1040
1072
  diagStage: r.diag_stage ?? null,
1041
1073
  };
1042
1074
  }
@@ -1053,6 +1085,7 @@ export interface AgentRunSummary {
1053
1085
  errorCode: string | null;
1054
1086
  abortReason: string | null;
1055
1087
  dispatchMode: string | null;
1088
+ terminalReason: string | null;
1056
1089
  /** Last reached `_process-run` worker stage (JSON `{stage,detail?,at}`). */
1057
1090
  diagStage: string | null;
1058
1091
  }
@@ -1065,7 +1098,7 @@ export async function listRunsForThread(
1065
1098
  const limit = Math.min(Math.max(options.limit ?? 10, 1), 50);
1066
1099
  const client = getDbExec();
1067
1100
  const { rows } = await client.execute({
1068
- sql: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, error_code, abort_reason, dispatch_mode, diag_stage
1101
+ sql: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, error_code, abort_reason, dispatch_mode, terminal_reason, diag_stage
1069
1102
  FROM agent_runs
1070
1103
  WHERE thread_id = ?
1071
1104
  ORDER BY started_at DESC
@@ -1085,6 +1118,7 @@ export async function listRunsForThread(
1085
1118
  error_code?: string | null;
1086
1119
  abort_reason?: string | null;
1087
1120
  dispatch_mode?: string | null;
1121
+ terminal_reason?: string | null;
1088
1122
  diag_stage?: string | null;
1089
1123
  };
1090
1124
  return {
@@ -1100,6 +1134,7 @@ export async function listRunsForThread(
1100
1134
  errorCode: row.error_code ?? null,
1101
1135
  abortReason: row.abort_reason ?? null,
1102
1136
  dispatchMode: row.dispatch_mode ?? null,
1137
+ terminalReason: row.terminal_reason ?? null,
1103
1138
  diagStage: row.diag_stage ?? null,
1104
1139
  };
1105
1140
  });
@@ -1183,13 +1218,15 @@ export async function reapAllStaleRuns(): Promise<number> {
1183
1218
  SET status = 'errored',
1184
1219
  completed_at = ?,
1185
1220
  error_code = ?,
1186
- error_detail = ?
1221
+ error_detail = ?,
1222
+ terminal_reason = ?
1187
1223
  WHERE status = 'running'
1188
1224
  AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1189
1225
  args: [
1190
1226
  completedAt,
1191
1227
  STALE_RUN_ERROR_EVENT.errorCode,
1192
1228
  STALE_RUN_ERROR_EVENT.details,
1229
+ STALE_RUN_ERROR_EVENT.errorCode,
1193
1230
  completedAt,
1194
1231
  ],
1195
1232
  });
@@ -1241,12 +1278,14 @@ export async function cleanupOldRuns(
1241
1278
  SET status = 'errored',
1242
1279
  completed_at = ?,
1243
1280
  error_code = ?,
1244
- error_detail = ?
1281
+ error_detail = ?,
1282
+ terminal_reason = ?
1245
1283
  WHERE status = 'running' AND started_at < ?`,
1246
1284
  args: [
1247
1285
  completedAt,
1248
1286
  STALE_RUN_ERROR_EVENT.errorCode,
1249
1287
  STALE_RUN_ERROR_EVENT.details,
1288
+ STALE_RUN_ERROR_EVENT.errorCode,
1250
1289
  cutoff,
1251
1290
  ],
1252
1291
  });
@@ -1257,13 +1296,15 @@ export async function cleanupOldRuns(
1257
1296
  SET status = 'errored',
1258
1297
  completed_at = ?,
1259
1298
  error_code = ?,
1260
- error_detail = ?
1299
+ error_detail = ?,
1300
+ terminal_reason = ?
1261
1301
  WHERE status = 'running'
1262
1302
  AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1263
1303
  args: [
1264
1304
  completedAt,
1265
1305
  STALE_RUN_ERROR_EVENT.errorCode,
1266
1306
  STALE_RUN_ERROR_EVENT.details,
1307
+ STALE_RUN_ERROR_EVENT.errorCode,
1267
1308
  completedAt,
1268
1309
  ],
1269
1310
  });
@@ -1312,6 +1353,7 @@ export async function listErroredRuns(options?: {
1312
1353
  status: string;
1313
1354
  errorCode: string | null;
1314
1355
  errorDetail: string | null;
1356
+ terminalReason: string | null;
1315
1357
  startedAt: number;
1316
1358
  completedAt: number | null;
1317
1359
  durationMs: number | null;
@@ -1323,7 +1365,7 @@ export async function listErroredRuns(options?: {
1323
1365
  const since =
1324
1366
  options?.sinceMs && options.sinceMs > 0 ? Date.now() - options.sinceMs : 0;
1325
1367
  const { rows } = await client.execute({
1326
- sql: `SELECT id, thread_id, turn_id, status, error_code, error_detail, started_at, completed_at
1368
+ sql: `SELECT id, thread_id, turn_id, status, error_code, error_detail, terminal_reason, started_at, completed_at
1327
1369
  FROM agent_runs
1328
1370
  WHERE status IN ('errored', 'aborted')
1329
1371
  AND COALESCE(completed_at, started_at) >= ?
@@ -1339,6 +1381,7 @@ export async function listErroredRuns(options?: {
1339
1381
  status: string;
1340
1382
  error_code: string | null;
1341
1383
  error_detail: string | null;
1384
+ terminal_reason: string | null;
1342
1385
  started_at: number | string;
1343
1386
  completed_at: number | string | null;
1344
1387
  };
@@ -1352,6 +1395,7 @@ export async function listErroredRuns(options?: {
1352
1395
  status: row.status,
1353
1396
  errorCode: row.error_code ?? null,
1354
1397
  errorDetail: row.error_detail ?? null,
1398
+ terminalReason: row.terminal_reason ?? null,
1355
1399
  startedAt,
1356
1400
  completedAt,
1357
1401
  durationMs: completedAt == null ? null : completedAt - startedAt,
@@ -648,8 +648,15 @@ export interface AssistantChatHandle {
648
648
  ): void;
649
649
  /** Programmatically prefill the composer without submitting. */
650
650
  prefillMessage(text: string): void;
651
- /** Add or replace keyed context for the next composer submission. */
652
- setComposerContextItem(item: AgentChatContextItem): void;
651
+ /**
652
+ * Add or replace keyed context for the next composer submission.
653
+ * Focuses the composer by default; pass `{ focus: false }` for passive
654
+ * context mirroring (e.g. canvas selection) that must not steal focus.
655
+ */
656
+ setComposerContextItem(
657
+ item: AgentChatContextItem,
658
+ options?: { focus?: boolean },
659
+ ): void;
653
660
  /** Remove a keyed context item from the composer. */
654
661
  removeComposerContextItem(key: string): void;
655
662
  /** Clear all staged context items from the composer. */
@@ -1205,6 +1212,17 @@ const AssistantChatInner = forwardRef<
1205
1212
  );
1206
1213
  const missingApiKey = agentEngineConfigured.missing;
1207
1214
  const isComposerDisabled = missingApiKey || composerDisabled;
1215
+ // Once a provider connects, `useAgentEngineConfigured` flips to "configured"
1216
+ // off the `agent-engine:configured-changed` event. The composer banner is a
1217
+ // separate piece of state that would otherwise only clear on manual dismiss
1218
+ // or the next successful send, so drop the stale "no LLM provider" message
1219
+ // here to match the resolved connection.
1220
+ useEffect(() => {
1221
+ if (agentEngineConfigured.state !== "configured") return;
1222
+ setComposerError((current) =>
1223
+ current === LLM_MISSING_CREDENTIALS_MESSAGE ? null : current,
1224
+ );
1225
+ }, [agentEngineConfigured.state]);
1208
1226
  const missingApiKeySetupAboveComposer =
1209
1227
  missingApiKeySetupLayout === "sidebar";
1210
1228
  // Increments each time the user tries to chat while no LLM is connected.
@@ -2958,9 +2976,17 @@ const AssistantChatInner = forwardRef<
2958
2976
  tiptapRef.current?.setText(text);
2959
2977
  tiptapRef.current?.focus();
2960
2978
  },
2961
- setComposerContextItem(item: AgentChatContextItem) {
2979
+ setComposerContextItem(
2980
+ item: AgentChatContextItem,
2981
+ options?: { focus?: boolean },
2982
+ ) {
2962
2983
  stageComposerContextItem(item);
2963
- tiptapRef.current?.focus();
2984
+ // Only pull focus into the composer for explicit user gestures.
2985
+ // Passive context mirroring (e.g. a canvas selection staged with
2986
+ // openSidebar:false) must not steal focus — doing so blurs an active
2987
+ // inline text editor living in the design canvas iframe and tears it
2988
+ // down the instant it opens.
2989
+ if (options?.focus !== false) tiptapRef.current?.focus();
2964
2990
  },
2965
2991
  removeComposerContextItem(key: string) {
2966
2992
  removeComposerContextItem(key);
@@ -1118,10 +1118,14 @@ export function MultiTabAssistantChat({
1118
1118
  const postMessageSubmissionsDisabled = props.composerDisabled === true;
1119
1119
 
1120
1120
  const setContextInTab = useCallback(
1121
- (threadId: string, item: AgentChatContextItem) => {
1121
+ (
1122
+ threadId: string,
1123
+ item: AgentChatContextItem,
1124
+ options?: { focus?: boolean },
1125
+ ) => {
1122
1126
  const ref = chatRefs.current.get(threadId);
1123
1127
  if (ref) {
1124
- ref.setComposerContextItem(item);
1128
+ ref.setComposerContextItem(item, options);
1125
1129
  return;
1126
1130
  }
1127
1131
  const existing = pendingContextItems.current.get(threadId) ?? [];
@@ -1712,7 +1716,11 @@ export function MultiTabAssistantChat({
1712
1716
  if (postMessageSubmissionsDisabled) return;
1713
1717
  const currentTabId = activeThreadIdRef.current;
1714
1718
  if (!currentTabId) return;
1715
- setContextInTab(currentTabId, item);
1719
+ // Focus defaults to true; a caller opts out with `focus: false` for
1720
+ // passive context (e.g. a canvas selection) so staging never steals
1721
+ // focus from an in-progress inline editor.
1722
+ const focus = (event.data.data?.focus as boolean | undefined) !== false;
1723
+ setContextInTab(currentTabId, item, { focus });
1716
1724
  return;
1717
1725
  }
1718
1726
  if (event.data?.type === AGENT_CHAT_REMOVE_CONTEXT_MESSAGE_TYPE) {