@agent-native/core 0.114.12 → 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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/index.ts +3 -0
- package/corpus/core/src/application-state/script-helpers.ts +12 -1
- package/corpus/core/src/application-state/store.ts +216 -15
- package/corpus/core/src/db/client.ts +44 -1
- package/corpus/templates/clips/changelog/2026-07-21-live-dictation-text-now-appears-above-the-desktop-recording-.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +14 -3
- package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +25 -0
- package/corpus/templates/clips/desktop/src/styles.css +20 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +3 -4
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +11 -4
- package/corpus/templates/design/AGENTS.md +7 -3
- package/corpus/templates/design/actions/begin-node-rewrite-request.ts +78 -0
- package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +17 -7
- package/corpus/templates/design/actions/propose-node-rewrite.ts +26 -18
- package/corpus/templates/design/actions/resolve-node-rewrite.ts +67 -19
- package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +1 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +2 -6
- package/corpus/templates/design/shared/node-rewrite.ts +32 -0
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +6 -3
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -18
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +9 -5
- package/corpus/templates/slides/app/context/DeckContext.tsx +18 -0
- package/corpus/templates/slides/changelog/2026-07-21-decks-list-now-refreshes-when-you-switch-organizations.md +6 -0
- package/corpus/templates/tasks/app/components/custom-fields/FieldCreateBar.tsx +13 -19
- package/corpus/templates/tasks/app/components/custom-fields/FieldListRow.tsx +27 -22
- package/corpus/templates/tasks/app/components/custom-fields/FieldsList.tsx +26 -22
- package/corpus/templates/tasks/app/components/custom-fields/FieldsListSkeleton.tsx +4 -1
- package/corpus/templates/tasks/app/components/custom-fields/FieldsPage.tsx +4 -2
- package/corpus/templates/tasks/app/components/custom-fields/editor/FieldEditorSidebar.tsx +12 -3
- package/corpus/templates/tasks/app/components/custom-fields/editor/FieldTitleSection.tsx +6 -3
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/CurrencyConfigControl.tsx +5 -2
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/NumberConfigControl.tsx +5 -2
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/PercentConfigControl.tsx +4 -1
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/SelectConfigControl.tsx +17 -7
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/select-colors.ts +37 -9
- package/corpus/templates/tasks/app/components/custom-fields/field-types.ts +23 -0
- package/corpus/templates/tasks/app/components/inbox/InboxList.tsx +24 -16
- package/corpus/templates/tasks/app/components/inbox/InboxListPage.tsx +5 -3
- package/corpus/templates/tasks/app/components/inbox/InboxListRow.tsx +7 -5
- package/corpus/templates/tasks/app/components/layout/Header.tsx +10 -10
- package/corpus/templates/tasks/app/components/layout/Layout.tsx +11 -7
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +17 -10
- package/corpus/templates/tasks/app/components/shared/AddListItemInput.tsx +14 -8
- package/corpus/templates/tasks/app/components/shared/AlertDialog.tsx +6 -2
- package/corpus/templates/tasks/app/components/shared/BulkDeleteDialog.tsx +13 -6
- package/corpus/templates/tasks/app/components/shared/ChipSelect.tsx +12 -5
- package/corpus/templates/tasks/app/components/shared/DeleteItemDialog.tsx +6 -6
- package/corpus/templates/tasks/app/components/shared/ListViewHeader.tsx +3 -1
- package/corpus/templates/tasks/app/components/shared/SidePanel.tsx +5 -2
- package/corpus/templates/tasks/app/components/shared/list/ListRowDragHandle.tsx +3 -1
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionBar.tsx +46 -20
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionHeaderToggle.tsx +3 -1
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionToolbar.tsx +10 -6
- package/corpus/templates/tasks/app/components/tasks/TaskList.tsx +24 -13
- package/corpus/templates/tasks/app/components/tasks/TaskListHeaderRow.tsx +4 -1
- package/corpus/templates/tasks/app/components/tasks/TaskListPage.tsx +5 -3
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +15 -6
- package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +11 -6
- package/corpus/templates/tasks/app/components/tasks/fields/TaskTitleSection.tsx +8 -3
- package/corpus/templates/tasks/app/components/tasks/fields/controls/RichTextValueControl.tsx +12 -6
- package/corpus/templates/tasks/app/components/tasks/fields/controls/SingleSelectValueControl.tsx +6 -1
- package/corpus/templates/tasks/app/i18n/en-US.ts +209 -0
- package/corpus/templates/tasks/app/i18n/index.ts +34 -0
- package/corpus/templates/tasks/app/root.tsx +34 -2
- package/corpus/templates/tasks/app/routes/extensions.$id.tsx +2 -1
- package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -1
- package/corpus/templates/tasks/app/routes/fields.tsx +2 -1
- package/corpus/templates/tasks/app/routes/inbox.tsx +2 -1
- package/corpus/templates/tasks/app/routes/tasks.tsx +2 -1
- package/corpus/templates/tasks/app/routes/team.tsx +6 -3
- package/corpus/templates/tasks/changelog/2026-07-20-tasks-is-now-available-in-10-additional-languages.md +6 -0
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/script-helpers.d.ts +3 -1
- package/dist/application-state/script-helpers.d.ts.map +1 -1
- package/dist/application-state/script-helpers.js +7 -1
- package/dist/application-state/script-helpers.js.map +1 -1
- package/dist/application-state/store.d.ts +7 -1
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +143 -17
- package/dist/application-state/store.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +39 -1
- package/dist/db/client.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/application-state/index.ts +3 -0
- package/src/application-state/script-helpers.ts +12 -1
- package/src/application-state/store.ts +216 -15
- package/src/db/client.ts +44 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.114.12
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.114.
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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(
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -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.
|
|
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
|
|
@@ -4905,13 +4905,33 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
4905
4905
|
position: fixed;
|
|
4906
4906
|
inset: var(--overlay-shadow-gutter);
|
|
4907
4907
|
display: flex;
|
|
4908
|
+
flex-direction: column;
|
|
4908
4909
|
align-items: center;
|
|
4909
4910
|
justify-content: flex-end;
|
|
4911
|
+
gap: 6px;
|
|
4910
4912
|
padding-bottom: 8px;
|
|
4911
4913
|
background: transparent;
|
|
4912
4914
|
pointer-events: none;
|
|
4913
4915
|
}
|
|
4914
4916
|
|
|
4917
|
+
.flow-bar-transcript-preview {
|
|
4918
|
+
max-width: 560px;
|
|
4919
|
+
max-height: 90px;
|
|
4920
|
+
padding: 5px 10px;
|
|
4921
|
+
overflow: hidden;
|
|
4922
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
4923
|
+
border-radius: 10px;
|
|
4924
|
+
background: rgba(18, 18, 20, 0.92);
|
|
4925
|
+
color: rgba(255, 255, 255, 0.9);
|
|
4926
|
+
font-size: 12px;
|
|
4927
|
+
font-weight: 500;
|
|
4928
|
+
line-height: 18px;
|
|
4929
|
+
text-align: center;
|
|
4930
|
+
word-break: break-word;
|
|
4931
|
+
backdrop-filter: blur(20px);
|
|
4932
|
+
-webkit-backdrop-filter: blur(20px);
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4915
4935
|
.flow-bar {
|
|
4916
4936
|
display: flex;
|
|
4917
4937
|
align-items: center;
|
|
@@ -1506,10 +1506,9 @@ pub async fn show_flow_bar(app: AppHandle) -> Result<(), String> {
|
|
|
1506
1506
|
|
|
1507
1507
|
let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
|
|
1508
1508
|
let scale = overlay_scale_factor(&app);
|
|
1509
|
-
//
|
|
1510
|
-
|
|
1511
|
-
let
|
|
1512
|
-
let content_h: u32 = (56.0 * scale).round() as u32;
|
|
1509
|
+
// Wide + tall enough for a 5-line transcript preview above the pill.
|
|
1510
|
+
let content_w: u32 = (640.0 * scale).round() as u32;
|
|
1511
|
+
let content_h: u32 = (160.0 * scale).round() as u32;
|
|
1513
1512
|
let bottom_margin: i32 = (14.0 * scale).round() as i32;
|
|
1514
1513
|
let gutter = overlay_shadow_gutter_physical(&app);
|
|
1515
1514
|
let w: u32 = content_w + gutter * 2;
|
|
@@ -2465,8 +2465,15 @@ impl LiveAudioMixer {
|
|
|
2465
2465
|
}
|
|
2466
2466
|
|
|
2467
2467
|
/// Emit mixed sample buffers covering `out_pos..safe_end` in
|
|
2468
|
-
/// `MIX_CHUNK_FRAMES` chunks:
|
|
2469
|
-
/// LPCM `CMSampleBuffer`s with contiguous PTS.
|
|
2468
|
+
/// `MIX_CHUNK_FRAMES` chunks: average system + mic per frame, clamp, wrap
|
|
2469
|
+
/// as LPCM `CMSampleBuffer`s with contiguous PTS.
|
|
2470
|
+
///
|
|
2471
|
+
/// Each source is weighted at 0.5 before summing so that two full-scale
|
|
2472
|
+
/// signals (which occurs when a USB audio interface with software monitoring
|
|
2473
|
+
/// routes the mic back through system audio) can never exceed ±1.0 and
|
|
2474
|
+
/// hard-clip. The standard SCK pipeline applies the same 0.5×L + 0.5×R
|
|
2475
|
+
/// pan-downmix for the same reason; loudnorm restores the target loudness
|
|
2476
|
+
/// in post-processing.
|
|
2470
2477
|
fn drain_ready(
|
|
2471
2478
|
&mut self,
|
|
2472
2479
|
flush: bool,
|
|
@@ -2488,8 +2495,8 @@ impl LiveAudioMixer {
|
|
|
2488
2495
|
let frame = a + f as i64;
|
|
2489
2496
|
let (sl, sr) = self.system.sample_at(frame);
|
|
2490
2497
|
let (ml, mr) = self.mic.sample_at(frame);
|
|
2491
|
-
interleaved[f * 2] = (sl + ml).clamp(-1.0, 1.0);
|
|
2492
|
-
interleaved[f * 2 + 1] = (sr + mr).clamp(-1.0, 1.0);
|
|
2498
|
+
interleaved[f * 2] = (sl * 0.5 + ml * 0.5).clamp(-1.0, 1.0);
|
|
2499
|
+
interleaved[f * 2 + 1] = (sr * 0.5 + mr * 0.5).clamp(-1.0, 1.0);
|
|
2493
2500
|
}
|
|
2494
2501
|
emitted.push(self.build_sample_buffer(&interleaved, a)?);
|
|
2495
2502
|
a = b;
|
|
@@ -288,11 +288,15 @@ ladder.
|
|
|
288
288
|
`view-screen`; not rendered as canvas overlays).
|
|
289
289
|
- `design-reprompt-pending:<designId>:<fileId>` is the client-captured source
|
|
290
290
|
selection, instruction, base hash, and authoritative current request id for
|
|
291
|
-
a scoped regenerate request.
|
|
291
|
+
a scoped regenerate request. The frontend starts requests through the
|
|
292
|
+
compare-and-set `begin-node-rewrite-request` action and must not overwrite a
|
|
293
|
+
`resolving` acceptance reservation.
|
|
292
294
|
- `design-reprompt-proposal:<designId>:<fileId>:<repromptId>` is one
|
|
293
295
|
request-specific preview-only subtree proposal. Candidate payloads have a
|
|
294
|
-
256 KiB aggregate serialized limit.
|
|
295
|
-
compare-and-set
|
|
296
|
+
256 KiB aggregate serialized limit. Proposal publication, resolution, and
|
|
297
|
+
cancellation use atomic multi-key compare-and-set transitions. Acceptance
|
|
298
|
+
reserves its matching pending request before writing design content, so a
|
|
299
|
+
newer request and an older acceptance cannot both win.
|
|
296
300
|
`view-screen` lists only proposals paired to the current pending request as
|
|
297
301
|
`pendingCandidateReviews`.
|
|
298
302
|
- `show-design-questions` opens focused pre-generation questions in the main
|