@agent-native/core 0.114.11 → 0.114.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 (109) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/application-state/index.ts +3 -0
  5. package/corpus/core/src/application-state/script-helpers.ts +12 -1
  6. package/corpus/core/src/application-state/store.ts +216 -15
  7. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +89 -1
  8. package/corpus/core/src/db/client.ts +44 -1
  9. package/corpus/templates/clips/changelog/2026-07-21-live-dictation-text-now-appears-above-the-desktop-recording-.md +6 -0
  10. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +14 -3
  11. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +25 -0
  12. package/corpus/templates/clips/desktop/src/styles.css +20 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +3 -4
  14. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +11 -4
  15. package/corpus/templates/design/AGENTS.md +7 -3
  16. package/corpus/templates/design/actions/begin-node-rewrite-request.ts +78 -0
  17. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +17 -7
  18. package/corpus/templates/design/actions/propose-node-rewrite.ts +26 -18
  19. package/corpus/templates/design/actions/resolve-node-rewrite.ts +67 -19
  20. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +1 -4
  21. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +2 -6
  22. package/corpus/templates/design/shared/node-rewrite.ts +32 -0
  23. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +6 -3
  24. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -18
  25. package/corpus/templates/forms/app/pages/FormsListPage.tsx +9 -5
  26. package/corpus/templates/slides/app/context/DeckContext.tsx +18 -0
  27. package/corpus/templates/slides/changelog/2026-07-21-decks-list-now-refreshes-when-you-switch-organizations.md +6 -0
  28. package/corpus/templates/tasks/app/components/custom-fields/FieldCreateBar.tsx +13 -19
  29. package/corpus/templates/tasks/app/components/custom-fields/FieldListRow.tsx +27 -22
  30. package/corpus/templates/tasks/app/components/custom-fields/FieldsList.tsx +26 -22
  31. package/corpus/templates/tasks/app/components/custom-fields/FieldsListSkeleton.tsx +4 -1
  32. package/corpus/templates/tasks/app/components/custom-fields/FieldsPage.tsx +4 -2
  33. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldEditorSidebar.tsx +12 -3
  34. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldTitleSection.tsx +6 -3
  35. package/corpus/templates/tasks/app/components/custom-fields/editor/config/CurrencyConfigControl.tsx +5 -2
  36. package/corpus/templates/tasks/app/components/custom-fields/editor/config/NumberConfigControl.tsx +5 -2
  37. package/corpus/templates/tasks/app/components/custom-fields/editor/config/PercentConfigControl.tsx +4 -1
  38. package/corpus/templates/tasks/app/components/custom-fields/editor/config/SelectConfigControl.tsx +17 -7
  39. package/corpus/templates/tasks/app/components/custom-fields/editor/config/select-colors.ts +37 -9
  40. package/corpus/templates/tasks/app/components/custom-fields/field-types.ts +23 -0
  41. package/corpus/templates/tasks/app/components/inbox/InboxList.tsx +24 -16
  42. package/corpus/templates/tasks/app/components/inbox/InboxListPage.tsx +5 -3
  43. package/corpus/templates/tasks/app/components/inbox/InboxListRow.tsx +7 -5
  44. package/corpus/templates/tasks/app/components/layout/Header.tsx +10 -10
  45. package/corpus/templates/tasks/app/components/layout/Layout.tsx +11 -7
  46. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +17 -10
  47. package/corpus/templates/tasks/app/components/shared/AddListItemInput.tsx +14 -8
  48. package/corpus/templates/tasks/app/components/shared/AlertDialog.tsx +6 -2
  49. package/corpus/templates/tasks/app/components/shared/BulkDeleteDialog.tsx +13 -6
  50. package/corpus/templates/tasks/app/components/shared/ChipSelect.tsx +12 -5
  51. package/corpus/templates/tasks/app/components/shared/DeleteItemDialog.tsx +6 -6
  52. package/corpus/templates/tasks/app/components/shared/ListViewHeader.tsx +3 -1
  53. package/corpus/templates/tasks/app/components/shared/SidePanel.tsx +5 -2
  54. package/corpus/templates/tasks/app/components/shared/list/ListRowDragHandle.tsx +3 -1
  55. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionBar.tsx +46 -20
  56. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionHeaderToggle.tsx +3 -1
  57. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionToolbar.tsx +10 -6
  58. package/corpus/templates/tasks/app/components/tasks/TaskList.tsx +24 -13
  59. package/corpus/templates/tasks/app/components/tasks/TaskListHeaderRow.tsx +4 -1
  60. package/corpus/templates/tasks/app/components/tasks/TaskListPage.tsx +5 -3
  61. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +15 -6
  62. package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +11 -6
  63. package/corpus/templates/tasks/app/components/tasks/fields/TaskTitleSection.tsx +8 -3
  64. package/corpus/templates/tasks/app/components/tasks/fields/controls/RichTextValueControl.tsx +12 -6
  65. package/corpus/templates/tasks/app/components/tasks/fields/controls/SingleSelectValueControl.tsx +6 -1
  66. package/corpus/templates/tasks/app/i18n/en-US.ts +209 -0
  67. package/corpus/templates/tasks/app/i18n/index.ts +34 -0
  68. package/corpus/templates/tasks/app/root.tsx +34 -2
  69. package/corpus/templates/tasks/app/routes/extensions.$id.tsx +2 -1
  70. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -1
  71. package/corpus/templates/tasks/app/routes/fields.tsx +2 -1
  72. package/corpus/templates/tasks/app/routes/inbox.tsx +2 -1
  73. package/corpus/templates/tasks/app/routes/tasks.tsx +2 -1
  74. package/corpus/templates/tasks/app/routes/team.tsx +6 -3
  75. package/corpus/templates/tasks/changelog/2026-07-20-tasks-is-now-available-in-10-additional-languages.md +6 -0
  76. package/dist/application-state/index.d.ts +2 -2
  77. package/dist/application-state/index.d.ts.map +1 -1
  78. package/dist/application-state/index.js +2 -2
  79. package/dist/application-state/index.js.map +1 -1
  80. package/dist/application-state/script-helpers.d.ts +3 -1
  81. package/dist/application-state/script-helpers.d.ts.map +1 -1
  82. package/dist/application-state/script-helpers.js +7 -1
  83. package/dist/application-state/script-helpers.js.map +1 -1
  84. package/dist/application-state/store.d.ts +7 -1
  85. package/dist/application-state/store.d.ts.map +1 -1
  86. package/dist/application-state/store.js +143 -17
  87. package/dist/application-state/store.js.map +1 -1
  88. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  89. package/dist/client/extensions/EmbeddedExtension.js +65 -1
  90. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  91. package/dist/collab/awareness.d.ts +2 -2
  92. package/dist/collab/awareness.d.ts.map +1 -1
  93. package/dist/collab/routes.d.ts +1 -1
  94. package/dist/db/client.d.ts +7 -0
  95. package/dist/db/client.d.ts.map +1 -1
  96. package/dist/db/client.js +39 -1
  97. package/dist/db/client.js.map +1 -1
  98. package/dist/notifications/routes.d.ts +1 -1
  99. package/dist/observability/routes.d.ts +3 -3
  100. package/dist/progress/routes.d.ts +1 -1
  101. package/dist/secrets/routes.d.ts +6 -6
  102. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  103. package/dist/server/transcribe-voice.d.ts +1 -1
  104. package/package.json +1 -1
  105. package/src/application-state/index.ts +3 -0
  106. package/src/application-state/script-helpers.ts +12 -1
  107. package/src/application-state/store.ts +216 -15
  108. package/src/client/extensions/EmbeddedExtension.tsx +89 -1
  109. package/src/db/client.ts +44 -1
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1519
32
32
  - toolkit files: 137
33
- - template files: 6178
33
+ - template files: 6185
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.114.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 7112c17: Add portable transactional multi-key application-state compare-and-set operations, including atomic D1 batch support and missing-key creation for race-safe UI workflows.
8
+
9
+ ## 0.114.12
10
+
11
+ ### Patch Changes
12
+
13
+ - 5c18906: Fix "Fix" button on runtime errors not opening agent chat for extensions embedded via `EmbeddedExtension` (e.g. dashboard panels).
14
+
3
15
  ## 0.114.11
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.114.11",
3
+ "version": "0.114.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": {
@@ -5,8 +5,10 @@ export {
5
5
  appStatePut,
6
6
  appStateDelete,
7
7
  appStateCompareAndSet,
8
+ appStateCompareAndSetMany,
8
9
  appStateList,
9
10
  appStateDeleteByPrefix,
11
+ type AppStateCompareAndSetOperation,
10
12
  } from "./store.js";
11
13
 
12
14
  // Emitter (for SSE wiring)
@@ -35,6 +37,7 @@ export {
35
37
  writeAppState,
36
38
  deleteAppState,
37
39
  compareAndSetAppState,
40
+ compareAndSetManyAppState,
38
41
  listAppState,
39
42
  deleteAppStateByPrefix,
40
43
  readAppStateForCurrentTab,
@@ -18,8 +18,10 @@ import {
18
18
  appStatePut,
19
19
  appStateDelete,
20
20
  appStateCompareAndSet,
21
+ appStateCompareAndSetMany,
21
22
  appStateList,
22
23
  appStateDeleteByPrefix,
24
+ type AppStateCompareAndSetOperation,
23
25
  } from "./store.js";
24
26
 
25
27
  /**
@@ -74,7 +76,7 @@ export async function deleteAppState(key: string): Promise<boolean> {
74
76
 
75
77
  export async function compareAndSetAppState(
76
78
  key: string,
77
- expectedValue: Record<string, unknown>,
79
+ expectedValue: Record<string, unknown> | null,
78
80
  nextValue: Record<string, unknown> | null,
79
81
  ): Promise<boolean> {
80
82
  const sessionId = await resolveSessionId();
@@ -83,6 +85,15 @@ export async function compareAndSetAppState(
83
85
  });
84
86
  }
85
87
 
88
+ export async function compareAndSetManyAppState(
89
+ operations: readonly AppStateCompareAndSetOperation[],
90
+ ): Promise<boolean> {
91
+ const sessionId = await resolveSessionId();
92
+ return appStateCompareAndSetMany(sessionId, operations, {
93
+ requestSource: "agent",
94
+ });
95
+ }
96
+
86
97
  export async function listAppState(
87
98
  prefix: string,
88
99
  ): Promise<Array<{ key: string; value: Record<string, unknown> }>> {
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  getDbExec,
3
+ getDialect,
3
4
  isLocalDatabase,
4
5
  isConnectionError,
5
6
  isPostgres,
6
7
  intType,
8
+ type DbExec,
7
9
  } from "../db/client.js";
8
10
  import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
9
11
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
@@ -194,39 +196,238 @@ export async function appStateDelete(
194
196
  export async function appStateCompareAndSet(
195
197
  sessionId: string,
196
198
  key: string,
197
- expectedValue: Record<string, unknown>,
199
+ expectedValue: Record<string, unknown> | null,
198
200
  nextValue: Record<string, unknown> | null,
199
201
  options?: StoreWriteOptions,
200
202
  ): Promise<boolean> {
201
203
  await ensureTable();
202
204
  const client = getDbExec();
205
+ const changed = await executeAppStateCompareAndSet(
206
+ client,
207
+ sessionId,
208
+ key,
209
+ expectedValue,
210
+ nextValue,
211
+ );
212
+ if (changed) {
213
+ if (nextValue === null) {
214
+ emitAppStateDelete(key, options?.requestSource, sessionId);
215
+ } else {
216
+ emitAppStateChange(key, options?.requestSource, sessionId);
217
+ }
218
+ }
219
+ return changed;
220
+ }
221
+
222
+ export interface AppStateCompareAndSetOperation {
223
+ key: string;
224
+ expectedValue: Record<string, unknown> | null;
225
+ nextValue: Record<string, unknown> | null;
226
+ }
227
+
228
+ const APP_STATE_CAS_MISMATCH = Symbol("app-state-cas-mismatch");
229
+
230
+ async function executeAppStateCompareAndSet(
231
+ client: DbExec,
232
+ sessionId: string,
233
+ key: string,
234
+ expectedValue: Record<string, unknown> | null,
235
+ nextValue: Record<string, unknown> | null,
236
+ ): Promise<boolean> {
237
+ const statement = buildAppStateCompareAndSetStatement(
238
+ sessionId,
239
+ key,
240
+ expectedValue,
241
+ nextValue,
242
+ );
243
+ const result = await client.execute(statement);
244
+ return result.rowsAffected > 0;
245
+ }
246
+
247
+ function buildAppStateCompareAndSetStatement(
248
+ sessionId: string,
249
+ key: string,
250
+ expectedValue: Record<string, unknown> | null,
251
+ nextValue: Record<string, unknown> | null,
252
+ ): { sql: string; args: unknown[] } {
253
+ if (expectedValue === null) {
254
+ if (nextValue === null) {
255
+ throw new Error(
256
+ "Application state CAS cannot replace absence with absence.",
257
+ );
258
+ }
259
+ const next = serializeAppStateValue(key, nextValue);
260
+ return {
261
+ sql: isPostgres()
262
+ ? `INSERT INTO application_state (session_id, key, value, updated_at) SELECT ?, ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM application_state WHERE session_id = ? AND key = ?) ON CONFLICT (session_id, key) DO NOTHING`
263
+ : `INSERT OR IGNORE INTO application_state (session_id, key, value, updated_at) SELECT ?, ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM application_state WHERE session_id = ? AND key = ?)`,
264
+ args: [sessionId, key, next, Date.now(), sessionId, key],
265
+ };
266
+ }
267
+
203
268
  const expected = JSON.stringify(expectedValue);
204
269
  if (nextValue === null) {
205
- const result = await client.execute({
270
+ return {
206
271
  sql: `DELETE FROM application_state WHERE session_id = ? AND key = ? AND value = ?`,
207
272
  args: [sessionId, key, expected],
208
- });
209
- const changed = result.rowsAffected > 0;
210
- if (changed) emitAppStateDelete(key, options?.requestSource, sessionId);
211
- return changed;
273
+ };
212
274
  }
213
275
 
214
- const next = JSON.stringify(nextValue);
276
+ const next = serializeAppStateValue(key, nextValue);
277
+ return {
278
+ sql: `UPDATE application_state SET value = ?, updated_at = ? WHERE session_id = ? AND key = ? AND value = ?`,
279
+ args: [next, Date.now(), sessionId, key, expected],
280
+ };
281
+ }
282
+
283
+ function buildD1CompareAndSetGuard(
284
+ sessionId: string,
285
+ guardKey: string,
286
+ operation: AppStateCompareAndSetOperation,
287
+ ): { sql: string; args: unknown[] } {
288
+ const expected = operation.expectedValue;
289
+ const condition =
290
+ expected === null
291
+ ? "NOT EXISTS (SELECT 1 FROM application_state WHERE session_id = ? AND key = ?)"
292
+ : "EXISTS (SELECT 1 FROM application_state WHERE session_id = ? AND key = ? AND value = ?)";
293
+ return {
294
+ sql: `INSERT INTO application_state (session_id, key, value, updated_at) SELECT ?, ?, '{}', ? WHERE NOT (${condition})`,
295
+ args:
296
+ expected === null
297
+ ? [sessionId, guardKey, Date.now(), sessionId, operation.key]
298
+ : [
299
+ sessionId,
300
+ guardKey,
301
+ Date.now(),
302
+ sessionId,
303
+ operation.key,
304
+ JSON.stringify(expected),
305
+ ],
306
+ };
307
+ }
308
+
309
+ function isD1CompareAndSetMismatch(error: unknown): boolean {
310
+ const message = error instanceof Error ? error.message : String(error);
311
+ return /unique constraint failed:\s*application_state\.session_id,\s*application_state\.key/i.test(
312
+ message,
313
+ );
314
+ }
315
+
316
+ function serializeAppStateValue(
317
+ key: string,
318
+ value: Record<string, unknown>,
319
+ ): string {
320
+ const serialized = JSON.stringify(value);
215
321
  if (
216
322
  !isLocalDatabase() &&
217
- Buffer.byteLength(next, "utf8") > MAX_HOSTED_APP_STATE_VALUE_BYTES
323
+ Buffer.byteLength(serialized, "utf8") > MAX_HOSTED_APP_STATE_VALUE_BYTES
218
324
  ) {
219
325
  throw new Error(
220
326
  `application_state value "${key}" is too large for hosted SQL storage. Store large files, base64, or blobs in file storage and write only a URL or handle.`,
221
327
  );
222
328
  }
223
- const result = await client.execute({
224
- sql: `UPDATE application_state SET value = ?, updated_at = ? WHERE session_id = ? AND key = ? AND value = ?`,
225
- args: [next, Date.now(), sessionId, key, expected],
226
- });
227
- const changed = result.rowsAffected > 0;
228
- if (changed) emitAppStateChange(key, options?.requestSource, sessionId);
229
- return changed;
329
+ return serialized;
330
+ }
331
+
332
+ export async function appStateCompareAndSetMany(
333
+ sessionId: string,
334
+ operations: readonly AppStateCompareAndSetOperation[],
335
+ options?: StoreWriteOptions,
336
+ ): Promise<boolean> {
337
+ if (operations.length === 0) return true;
338
+ const keys = new Set(operations.map(({ key }) => key));
339
+ if (keys.size !== operations.length) {
340
+ throw new Error("Application state multi-key CAS requires unique keys.");
341
+ }
342
+ for (const { key, nextValue } of operations) {
343
+ if (nextValue) serializeAppStateValue(key, nextValue);
344
+ }
345
+ const orderedOperations = [...operations].sort((a, b) =>
346
+ a.key.localeCompare(b.key),
347
+ );
348
+
349
+ await ensureTable();
350
+ const client = getDbExec();
351
+ if (getDialect() === "d1") {
352
+ if (!client.atomicBatch) {
353
+ throw new Error(
354
+ "D1 application-state CAS requires atomic batch support.",
355
+ );
356
+ }
357
+ const guardKey = `__agent_native_cas_guard__:${Date.now()}:${Math.random().toString(36).slice(2)}`;
358
+ const guardInsert = {
359
+ sql: `INSERT INTO application_state (session_id, key, value, updated_at) VALUES (?, ?, '{}', ?)`,
360
+ args: [sessionId, guardKey, Date.now()],
361
+ };
362
+ const guards = orderedOperations.map((operation) =>
363
+ buildD1CompareAndSetGuard(sessionId, guardKey, operation),
364
+ );
365
+ const mutations = orderedOperations.map((operation) =>
366
+ buildAppStateCompareAndSetStatement(
367
+ sessionId,
368
+ operation.key,
369
+ operation.expectedValue,
370
+ operation.nextValue,
371
+ ),
372
+ );
373
+ let results;
374
+ try {
375
+ results = await client.atomicBatch([
376
+ guardInsert,
377
+ ...guards,
378
+ ...mutations,
379
+ {
380
+ sql: `DELETE FROM application_state WHERE session_id = ? AND key = ?`,
381
+ args: [sessionId, guardKey],
382
+ },
383
+ ]);
384
+ } catch (error) {
385
+ if (isD1CompareAndSetMismatch(error)) return false;
386
+ throw error;
387
+ }
388
+ const mutationResults = results.slice(
389
+ 1 + guards.length,
390
+ 1 + guards.length + mutations.length,
391
+ );
392
+ if (
393
+ mutationResults.length !== mutations.length ||
394
+ mutationResults.some((result) => result.rowsAffected !== 1)
395
+ ) {
396
+ throw new Error(
397
+ "D1 application-state CAS completed without applying every mutation.",
398
+ );
399
+ }
400
+ } else {
401
+ if (!client.transaction) {
402
+ throw new Error("Application state multi-key CAS requires transactions.");
403
+ }
404
+ try {
405
+ await client.transaction(async (tx) => {
406
+ for (const operation of orderedOperations) {
407
+ const changed = await executeAppStateCompareAndSet(
408
+ tx,
409
+ sessionId,
410
+ operation.key,
411
+ operation.expectedValue,
412
+ operation.nextValue,
413
+ );
414
+ if (!changed) throw APP_STATE_CAS_MISMATCH;
415
+ }
416
+ });
417
+ } catch (error) {
418
+ if (error === APP_STATE_CAS_MISMATCH) return false;
419
+ throw error;
420
+ }
421
+ }
422
+
423
+ for (const { key, nextValue } of operations) {
424
+ if (nextValue === null) {
425
+ emitAppStateDelete(key, options?.requestSource, sessionId);
426
+ } else {
427
+ emitAppStateChange(key, options?.requestSource, sessionId);
428
+ }
429
+ }
430
+ return true;
230
431
  }
231
432
 
232
433
  export async function appStateList(
@@ -177,6 +177,7 @@ export function EmbeddedExtension({
177
177
  // originates inside the same sandboxed realm as user code) and must be
178
178
  // ignored so a viewer cannot self-escalate to owner.
179
179
  const bindingLatchedRef = useRef(false);
180
+ const extensionRef = useRef(null as null | Extension);
180
181
 
181
182
  useEffect(() => {
182
183
  setIsDark(document.documentElement.classList.contains("dark"));
@@ -216,6 +217,7 @@ export function EmbeddedExtension({
216
217
  retry: shouldRetryExtensionLoad,
217
218
  retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 4000),
218
219
  });
220
+ extensionRef.current = extension ?? null;
219
221
 
220
222
  // Notify the host once when the extension can't be loaded for this viewer so
221
223
  // it can show a fallback instead of a blank panel.
@@ -338,6 +340,92 @@ export function EmbeddedExtension({
338
340
  return;
339
341
  }
340
342
 
343
+ if (message.type === "agent-native-extension-error-fix") {
344
+ const name = extensionRef.current?.name ?? extensionId;
345
+ const errors: string[] = Array.isArray((message as any).errors)
346
+ ? (message as any).errors
347
+ : [];
348
+ const errorDetails: Array<{ message: string; stack: string }> =
349
+ Array.isArray((message as any).errorDetails)
350
+ ? (message as any).errorDetails
351
+ : [];
352
+ const consoleLogs: Array<{ level: string; message: string }> =
353
+ Array.isArray((message as any).consoleLogs)
354
+ ? (message as any).consoleLogs
355
+ : [];
356
+ const networkLogs: Array<{
357
+ path: string;
358
+ method: string;
359
+ ok?: boolean;
360
+ status?: number;
361
+ error?: string;
362
+ }> = Array.isArray((message as any).networkLogs)
363
+ ? (message as any).networkLogs
364
+ : [];
365
+
366
+ const detailedTrace = errorDetails
367
+ .filter((e) => e && typeof e === "object")
368
+ .map((e) => {
369
+ const msg =
370
+ typeof e.message === "string" ? e.message : String(e.message);
371
+ return typeof e.stack === "string" ? `${msg}\n${e.stack}` : msg;
372
+ })
373
+ .join("\n\n");
374
+
375
+ // Force a fresh read from the server, same as ExtensionViewer's fix
376
+ // flow — the query cache may hold the agent's previous (broken) turn.
377
+ let freshContent: string | undefined;
378
+ try {
379
+ const res = await fetch(
380
+ agentNativePath(`/_agent-native/extensions/${extensionId}`),
381
+ { cache: "no-store" },
382
+ );
383
+ if (res.ok) {
384
+ const fresh = (await res.json()) as Extension;
385
+ freshContent =
386
+ typeof fresh?.content === "string" ? fresh.content : undefined;
387
+ }
388
+ } catch {
389
+ // Fall through with no snapshot — agent can still re-read via get-extension.
390
+ }
391
+
392
+ const contextParts = [
393
+ `The user is viewing a dashboard panel embedding extension "${name}" (id: ${extensionId}, slot: ${slotId}) and there are runtime errors that need fixing.`,
394
+ `\nFull error details:\n${detailedTrace}`,
395
+ ];
396
+
397
+ if (consoleLogs.length > 0) {
398
+ const consoleStr = consoleLogs
399
+ .map((l) => `[${l.level}] ${l.message}`)
400
+ .join("\n");
401
+ contextParts.push(`\nRecent console output:\n${consoleStr}`);
402
+ }
403
+
404
+ if (networkLogs.length > 0) {
405
+ const netStr = networkLogs
406
+ .map(
407
+ (l) =>
408
+ `${l.method} ${l.path} → ${l.ok ? l.status : "FAILED: " + (l.error || l.status)}`,
409
+ )
410
+ .join("\n");
411
+ contextParts.push(`\nRecent network requests:\n${netStr}`);
412
+ }
413
+
414
+ if (freshContent) {
415
+ contextParts.push(
416
+ `\nCurrent extension content (just re-read from the database — this is the authoritative source, not anything you may have written in a previous turn):\n\`\`\`html\n${freshContent}\n\`\`\``,
417
+ );
418
+ }
419
+
420
+ sendToAgentChat({
421
+ message: `Fix runtime errors in extension "${name}" (id: ${extensionId}), embedded as a dashboard panel. The content snapshot below was just re-read from the database — treat it as authoritative and ignore any prior version you may have generated in this chat. If in doubt, call get-extension first.\n\nErrors:\n${errors.join("\n")}`,
422
+ context: contextParts.join("\n"),
423
+ submit: true,
424
+ openSidebar: true,
425
+ });
426
+ return;
427
+ }
428
+
341
429
  if (message.type !== "agent-native-extension-request") return;
342
430
 
343
431
  const requestId = String(message.requestId ?? "");
@@ -410,7 +498,7 @@ export function EmbeddedExtension({
410
498
 
411
499
  window.addEventListener("message", handleMessage);
412
500
  return () => window.removeEventListener("message", handleMessage);
413
- }, [extensionId]);
501
+ }, [extensionId, slotId]);
414
502
 
415
503
  if (!extension) {
416
504
  if (!isLoading && !isFetching) return null;
@@ -29,6 +29,9 @@ export interface DbExec {
29
29
  sql: string | { sql: string; args?: unknown[] },
30
30
  ): Promise<{ rows: any[]; rowsAffected: number }>;
31
31
  transaction?<T>(fn: (tx: DbExec) => Promise<T>): Promise<T>;
32
+ atomicBatch?(
33
+ statements: readonly (string | { sql: string; args?: unknown[] })[],
34
+ ): Promise<Array<{ rows: any[]; rowsAffected: number }>>;
32
35
  /**
33
36
  * Release the underlying connection/pool held by this exec.
34
37
  * Only non-singleton execs created via `createDbExec()` (e.g. the migration
@@ -1100,7 +1103,17 @@ async function createDbExecInternal(
1100
1103
  };
1101
1104
  return {
1102
1105
  execute,
1103
- transaction: explicitTransaction(execute),
1106
+ async atomicBatch(statements) {
1107
+ const prepared = statements.map((statement) => {
1108
+ if (typeof statement === "string") return d1.prepare(statement);
1109
+ return d1.prepare(statement.sql).bind(...(statement.args ?? []));
1110
+ });
1111
+ const results = await d1.batch(prepared);
1112
+ return results.map((result: any) => ({
1113
+ rows: result.results || [],
1114
+ rowsAffected: result.meta?.changes ?? 0,
1115
+ }));
1116
+ },
1104
1117
  };
1105
1118
  }
1106
1119
 
@@ -1558,6 +1571,10 @@ export function getDbExec(): DbExec {
1558
1571
  // After init, swap to a sanitizing wrapper around the real client
1559
1572
  const wrapper: DbExec = {
1560
1573
  execute: (s) => _exec!.execute(sanitize(s)),
1574
+ atomicBatch: _exec!.atomicBatch
1575
+ ? (statements) =>
1576
+ _exec!.atomicBatch!(statements.map((s) => sanitize(s)))
1577
+ : undefined,
1561
1578
  transaction: _exec!.transaction
1562
1579
  ? (fn) =>
1563
1580
  _exec!.transaction!((tx) =>
@@ -1582,6 +1599,10 @@ export function getDbExec(): DbExec {
1582
1599
  }
1583
1600
  const wrapper: DbExec = {
1584
1601
  execute: (s) => _exec!.execute(sanitize(s)),
1602
+ atomicBatch: _exec!.atomicBatch
1603
+ ? (statements) =>
1604
+ _exec!.atomicBatch!(statements.map((s) => sanitize(s)))
1605
+ : undefined,
1585
1606
  transaction: _exec!.transaction
1586
1607
  ? (innerFn) =>
1587
1608
  _exec!.transaction!((tx) =>
@@ -1601,8 +1622,30 @@ export function getDbExec(): DbExec {
1601
1622
  }),
1602
1623
  );
1603
1624
  }
1625
+ if (_exec!.atomicBatch) {
1626
+ throw new Error(
1627
+ "This database supports atomic batches, not interactive transactions.",
1628
+ );
1629
+ }
1604
1630
  return explicitTransaction(wrapper.execute)(fn);
1605
1631
  },
1632
+ async atomicBatch(statements) {
1633
+ if (!_initPromise) _initPromise = initClient();
1634
+ try {
1635
+ await _initPromise;
1636
+ } catch (err) {
1637
+ _initPromise = undefined;
1638
+ _exec = undefined;
1639
+ throw err;
1640
+ }
1641
+ if (!_exec!.atomicBatch) {
1642
+ throw new Error("This database does not support atomic batches.");
1643
+ }
1644
+ const batch = (items: typeof statements) =>
1645
+ _exec!.atomicBatch!(items.map((item) => sanitize(item)));
1646
+ Object.assign(proxy, { atomicBatch: batch });
1647
+ return batch(statements);
1648
+ },
1606
1649
  };
1607
1650
  return proxy;
1608
1651
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-21
4
+ ---
5
+
6
+ Live dictation text now appears above the desktop recording pill.
@@ -1445,6 +1445,9 @@ export function installDesktopVoiceDictation(
1445
1445
  // the tail because Web Speech only marks a segment as `isFinal`
1446
1446
  // after a confidence-threshold pass.
1447
1447
  next.browserTranscript = (finalSoFar + interim).trim();
1448
+ emit("voice:dictation-preview", {
1449
+ text: next.browserTranscript,
1450
+ }).catch(() => {});
1448
1451
  };
1449
1452
  recognition.onerror = (ev) => {
1450
1453
  if (ev.error !== "no-speech" && ev.error !== "aborted") {
@@ -2038,15 +2041,16 @@ export function installDesktopVoiceDictation(
2038
2041
 
2039
2042
  // Native (SFSpeechRecognizer) event subscriptions. These are always
2040
2043
  // installed — the events only fire when the Rust side has an active
2041
- // session, so subscribing on non-native sessions is harmless. The
2042
- // flow-bar listens to `voice:partial-transcript` independently so we
2043
- // don't re-emit it here.
2044
+ // session, so subscribing on non-native sessions is harmless.
2044
2045
  onPartialTranscript(({ text }) => {
2045
2046
  const current = session;
2046
2047
  if (!current || (current.kind !== "native" && current.kind !== "whisper"))
2047
2048
  return;
2048
2049
  if (current.cancelled || current.stopping) return;
2049
2050
  setInterimTranscript(current, text);
2051
+ emit("voice:dictation-preview", {
2052
+ text: current.browserTranscript,
2053
+ }).catch(() => {});
2050
2054
  })
2051
2055
  .then((u) => unlistens.push(u))
2052
2056
  .catch(() => {});
@@ -2065,6 +2069,13 @@ export function installDesktopVoiceDictation(
2065
2069
  if (!current) return;
2066
2070
  if (current.cancelled) return;
2067
2071
  appendFinalTranscript(current, text);
2072
+ const supersededLingeringSession =
2073
+ current === lingeringSession && session !== null && session !== current;
2074
+ if (!supersededLingeringSession) {
2075
+ emit("voice:dictation-preview", {
2076
+ text: current.browserTranscript,
2077
+ }).catch(() => {});
2078
+ }
2068
2079
  if (current === lingeringSession) {
2069
2080
  current.onNativeFinalize?.();
2070
2081
  }
@@ -16,6 +16,7 @@ type FlowState = "idle" | "recording" | "processing" | "complete" | "error";
16
16
  * Events:
17
17
  * - `voice:state-change` { state: "idle"|"recording"|"processing"|"complete"|"error" }
18
18
  * - `voice:audio-level` { level: number } (0-1) for waveform visualization
19
+ * - `voice:dictation-preview` { text: string }
19
20
  */
20
21
  export function FlowBar() {
21
22
  // Default to "recording" not "idle" — there's a race between the Rust
@@ -23,6 +24,8 @@ export function FlowBar() {
23
24
  // "idle" caused the bar to flash an "EN" language pill that never went
24
25
  // away if the start event was missed.
25
26
  const [state, setState] = useState<FlowState>("recording");
27
+ const [transcript, setTranscript] = useState("");
28
+ const transcriptRef = useRef<HTMLDivElement | null>(null);
26
29
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
27
30
  const levelRef = useRef(0);
28
31
  const rafRef = useRef<number | null>(null);
@@ -48,6 +51,7 @@ export function FlowBar() {
48
51
  trackListen(
49
52
  listen<{ state: FlowState }>("voice:state-change", (ev) => {
50
53
  setState(ev.payload.state);
54
+ if (ev.payload.state === "recording") setTranscript("");
51
55
  }),
52
56
  );
53
57
 
@@ -57,6 +61,12 @@ export function FlowBar() {
57
61
  }),
58
62
  );
59
63
 
64
+ trackListen(
65
+ listen<{ text: string }>("voice:dictation-preview", (ev) => {
66
+ setTranscript(ev.payload.text.trim());
67
+ }),
68
+ );
69
+
60
70
  return () => {
61
71
  stopped = true;
62
72
  unlistens.forEach((u) => {
@@ -70,6 +80,11 @@ export function FlowBar() {
70
80
  };
71
81
  }, []);
72
82
 
83
+ useEffect(() => {
84
+ const preview = transcriptRef.current;
85
+ if (preview) preview.scrollTop = preview.scrollHeight;
86
+ }, [transcript]);
87
+
73
88
  // Waveform canvas rendering loop — only runs during the "recording" state.
74
89
  useEffect(() => {
75
90
  if (state !== "recording") {
@@ -151,6 +166,16 @@ export function FlowBar() {
151
166
 
152
167
  return (
153
168
  <div className="flow-bar-root">
169
+ {transcript ? (
170
+ <div
171
+ ref={transcriptRef}
172
+ className="flow-bar-transcript-preview"
173
+ aria-live="polite"
174
+ >
175
+ {transcript}
176
+ </div>
177
+ ) : null}
178
+
154
179
  {/* Pill is ALWAYS mounted — when state goes idle we fade the
155
180
  opacity to 0 (see CSS) instead of removing it from the DOM.
156
181
  Inner content keeps its last frame rendered during the fade