@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -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/client/extensions/EmbeddedExtension.tsx +89 -1
- 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/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +65 -1
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/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/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- 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/client/extensions/EmbeddedExtension.tsx +89 -1
- package/src/db/client.ts +44 -1
|
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
-
error?: undefined;
|
|
45
44
|
summary: import("./types.js").TraceSummary;
|
|
46
45
|
spans: import("./types.js").TraceSpan[];
|
|
47
46
|
id?: undefined;
|
|
47
|
+
error?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
|
-
error?: undefined;
|
|
51
50
|
summary?: undefined;
|
|
52
51
|
spans?: undefined;
|
|
53
52
|
id: string;
|
|
53
|
+
error?: undefined;
|
|
54
54
|
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
@@ -59,10 +59,10 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
59
59
|
error: any;
|
|
60
60
|
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
summary?: undefined;
|
|
64
63
|
spans?: undefined;
|
|
65
64
|
id?: undefined;
|
|
66
65
|
ok: boolean;
|
|
66
|
+
error?: undefined;
|
|
67
67
|
}>>;
|
|
68
68
|
//# sourceMappingURL=routes.d.ts.map
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
37
37
|
status?: undefined;
|
|
38
38
|
ok?: undefined;
|
|
39
39
|
} | {
|
|
40
|
-
error?: undefined;
|
|
41
40
|
ok: boolean;
|
|
42
41
|
status: string;
|
|
42
|
+
error?: undefined;
|
|
43
43
|
} | {
|
|
44
44
|
error: string;
|
|
45
45
|
removed?: undefined;
|
|
46
46
|
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
|
-
error?: undefined;
|
|
49
48
|
ok: boolean;
|
|
50
49
|
removed: boolean;
|
|
50
|
+
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — re-run the validator against the
|
|
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
|
|
|
58
58
|
note?: undefined;
|
|
59
59
|
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
|
-
error?: undefined;
|
|
62
61
|
ok: boolean;
|
|
63
62
|
note?: undefined;
|
|
64
|
-
} | {
|
|
65
63
|
error?: undefined;
|
|
64
|
+
} | {
|
|
66
65
|
ok: boolean;
|
|
67
66
|
note: string;
|
|
67
|
+
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,12 +95,12 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
-
error?: undefined;
|
|
99
98
|
ok: boolean;
|
|
100
99
|
key: string;
|
|
101
|
-
} | {
|
|
102
100
|
error?: undefined;
|
|
101
|
+
} | {
|
|
103
102
|
ok: boolean;
|
|
104
103
|
removed: boolean;
|
|
104
|
+
error?: undefined;
|
|
105
105
|
}>>;
|
|
106
106
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
+
error?: undefined;
|
|
30
31
|
ok: boolean;
|
|
31
32
|
key: string;
|
|
32
33
|
baseUrlKey?: string;
|
|
33
34
|
scope: AgentEngineApiKeyScope;
|
|
34
|
-
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
37
37
|
//# sourceMappingURL=agent-engine-api-key-route.d.ts.map
|
package/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(
|
|
@@ -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;
|
package/src/db/client.ts
CHANGED
|
@@ -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
|
}
|