@agent-native/core 0.188.0 → 0.189.0-nightly-20260925200527
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/templates/content/actions/list-document-versions.ts +102 -2
- package/corpus/templates/content/actions/restore-document-version.ts +50 -2
- package/corpus/templates/content/app/components/layout/Layout.tsx +6 -1
- package/corpus/templates/content/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
- package/corpus/templates/content/server/lib/document-history.ts +59 -8
- package/corpus/templates/content/server/lib/document-version-context.ts +5 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +164 -0
- package/corpus/templates/design/actions/list-design-versions.ts +3 -1
- package/corpus/templates/design/app/components/layout/Layout.tsx +17 -2
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +10 -3
- package/corpus/templates/design/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
- package/corpus/templates/design/server/lib/design-versions.ts +136 -20
- package/corpus/templates/design/server/plugins/agent-chat.ts +18 -2
- package/corpus/templates/slides/actions/list-deck-versions.ts +62 -10
- package/corpus/templates/slides/app/components/layout/Layout.tsx +10 -2
- package/corpus/templates/slides/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
- package/corpus/templates/slides/server/lib/deck-versions.ts +36 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +26 -2
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/AssistantChat.js +112 -35
- package/dist/client/chat/assistant-chat-history-version.d.ts +1 -0
- package/dist/client/chat/message-components.d.ts +8 -1
- package/dist/client/chat/message-components.js +52 -11
- package/dist/client/resources/mcp-integration-catalog.js +1 -1
- package/dist/client/resources/mcp-integration-logos.js +29 -30
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/core-messages/ar-SA.js +2 -0
- package/dist/localization/core-messages/de-DE.js +2 -0
- package/dist/localization/core-messages/en-US.d.ts +2 -0
- package/dist/localization/core-messages/en-US.js +2 -0
- package/dist/localization/core-messages/es-ES.js +2 -0
- package/dist/localization/core-messages/fr-FR.js +2 -0
- package/dist/localization/core-messages/hi-IN.js +2 -0
- package/dist/localization/core-messages/ja-JP.js +2 -0
- package/dist/localization/core-messages/ko-KR.js +2 -0
- package/dist/localization/core-messages/pt-BR.js +2 -0
- package/dist/localization/core-messages/zh-CN.js +2 -0
- package/dist/localization/core-messages/zh-TW.js +2 -0
- package/dist/localization/core-messages.d.ts +2 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +4 -4
- package/dist/org/handlers.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat/plugin-options.d.ts +2 -0
- package/dist/server/agent-chat-plugin.d.ts +1 -0
- package/dist/server/agent-chat-plugin.js +26 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/onboarding-html.js +28 -5
- package/docs/content/deployment.mdx +12 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core/action";
|
|
2
2
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
3
|
+
import { and, asc, desc, eq, like } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import { documentChatStartVersionId } from "../server/lib/document-history.js";
|
|
7
8
|
import { parseDocumentVersionChatContext } from "../server/lib/document-version-context.js";
|
|
8
9
|
|
|
9
10
|
export default defineAction({
|
|
@@ -16,6 +17,7 @@ export default defineAction({
|
|
|
16
17
|
.default(true)
|
|
17
18
|
.describe("Include full version content in the response"),
|
|
18
19
|
limit: z.coerce.number().int().min(1).max(100).default(100),
|
|
20
|
+
threadId: z.string().min(1).optional(),
|
|
19
21
|
}),
|
|
20
22
|
http: { method: "GET" },
|
|
21
23
|
readOnly: true,
|
|
@@ -48,9 +50,107 @@ export default defineAction({
|
|
|
48
50
|
.where(where)
|
|
49
51
|
.orderBy(desc(schema.documentVersions.createdAt))
|
|
50
52
|
.limit(args.limit);
|
|
53
|
+
const beginningVersions = args.threadId
|
|
54
|
+
? args.includeContent
|
|
55
|
+
? await db
|
|
56
|
+
.select()
|
|
57
|
+
.from(schema.documentVersions)
|
|
58
|
+
.where(
|
|
59
|
+
and(
|
|
60
|
+
where,
|
|
61
|
+
eq(
|
|
62
|
+
schema.documentVersions.id,
|
|
63
|
+
documentChatStartVersionId(
|
|
64
|
+
ownerEmail,
|
|
65
|
+
args.documentId,
|
|
66
|
+
args.threadId,
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
.orderBy(asc(schema.documentVersions.createdAt))
|
|
72
|
+
.limit(1)
|
|
73
|
+
: await db
|
|
74
|
+
.select({
|
|
75
|
+
id: schema.documentVersions.id,
|
|
76
|
+
documentId: schema.documentVersions.documentId,
|
|
77
|
+
title: schema.documentVersions.title,
|
|
78
|
+
createdAt: schema.documentVersions.createdAt,
|
|
79
|
+
chatContext: schema.documentVersions.chatContext,
|
|
80
|
+
})
|
|
81
|
+
.from(schema.documentVersions)
|
|
82
|
+
.where(
|
|
83
|
+
and(
|
|
84
|
+
where,
|
|
85
|
+
eq(
|
|
86
|
+
schema.documentVersions.id,
|
|
87
|
+
documentChatStartVersionId(
|
|
88
|
+
ownerEmail,
|
|
89
|
+
args.documentId,
|
|
90
|
+
args.threadId,
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
.orderBy(asc(schema.documentVersions.createdAt))
|
|
96
|
+
.limit(1)
|
|
97
|
+
: args.includeContent
|
|
98
|
+
? await db
|
|
99
|
+
.select()
|
|
100
|
+
.from(schema.documentVersions)
|
|
101
|
+
.where(
|
|
102
|
+
and(
|
|
103
|
+
where,
|
|
104
|
+
like(schema.documentVersions.chatContext, '%"phase":"start"%'),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
.orderBy(desc(schema.documentVersions.createdAt))
|
|
108
|
+
.limit(args.limit)
|
|
109
|
+
: await db
|
|
110
|
+
.select({
|
|
111
|
+
id: schema.documentVersions.id,
|
|
112
|
+
documentId: schema.documentVersions.documentId,
|
|
113
|
+
title: schema.documentVersions.title,
|
|
114
|
+
createdAt: schema.documentVersions.createdAt,
|
|
115
|
+
chatContext: schema.documentVersions.chatContext,
|
|
116
|
+
})
|
|
117
|
+
.from(schema.documentVersions)
|
|
118
|
+
.where(
|
|
119
|
+
and(
|
|
120
|
+
where,
|
|
121
|
+
like(schema.documentVersions.chatContext, '%"phase":"start"%'),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
.orderBy(desc(schema.documentVersions.createdAt))
|
|
125
|
+
.limit(args.limit);
|
|
126
|
+
const versionsById = new Map(
|
|
127
|
+
[...beginningVersions, ...versions].map((version) => [
|
|
128
|
+
version.id,
|
|
129
|
+
version,
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const mergedVersions = [...versionsById.values()];
|
|
134
|
+
const sortedVersions = mergedVersions.sort(
|
|
135
|
+
(left, right) =>
|
|
136
|
+
new Date(right.createdAt).getTime() -
|
|
137
|
+
new Date(left.createdAt).getTime(),
|
|
138
|
+
);
|
|
139
|
+
const limitedVersions = sortedVersions.slice(0, args.limit);
|
|
140
|
+
const activeStart = args.threadId
|
|
141
|
+
? sortedVersions.find((version) => beginningVersions.includes(version))
|
|
142
|
+
: undefined;
|
|
143
|
+
if (activeStart && !limitedVersions.includes(activeStart)) {
|
|
144
|
+
limitedVersions[limitedVersions.length - 1] = activeStart;
|
|
145
|
+
limitedVersions.sort(
|
|
146
|
+
(left, right) =>
|
|
147
|
+
new Date(right.createdAt).getTime() -
|
|
148
|
+
new Date(left.createdAt).getTime(),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
51
151
|
|
|
52
152
|
return {
|
|
53
|
-
versions:
|
|
153
|
+
versions: limitedVersions.map((version) => {
|
|
54
154
|
let chatContext;
|
|
55
155
|
try {
|
|
56
156
|
chatContext = parseDocumentVersionChatContext(version.chatContext);
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
documentContentHash,
|
|
24
24
|
documentRevisionToken,
|
|
25
25
|
} from "./_document-edit-mutation.js";
|
|
26
|
+
import { flushOpenDocumentEditorToSql } from "./_document-flush.js";
|
|
26
27
|
|
|
27
28
|
function isLinkedLocalSource(
|
|
28
29
|
documentId: string,
|
|
@@ -78,6 +79,53 @@ export default defineAction({
|
|
|
78
79
|
if (isLinkedLocalSource(documentId, source))
|
|
79
80
|
linkedLocalRestoreUnsupported();
|
|
80
81
|
const db = getDb();
|
|
82
|
+
const [beforeFlush] = await db
|
|
83
|
+
.select({ updatedAt: schema.documents.updatedAt })
|
|
84
|
+
.from(schema.documents)
|
|
85
|
+
.where(
|
|
86
|
+
and(
|
|
87
|
+
eq(schema.documents.id, documentId),
|
|
88
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
.limit(1);
|
|
92
|
+
if (!beforeFlush) {
|
|
93
|
+
throw new ActionContractError("Document not found.", {
|
|
94
|
+
errorCode: "DOCUMENT_NOT_FOUND",
|
|
95
|
+
statusCode: 404,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (beforeFlush.updatedAt !== args.expectedUpdatedAt) {
|
|
99
|
+
throw new ActionContractError(
|
|
100
|
+
"The document changed after this restore was prepared.",
|
|
101
|
+
{
|
|
102
|
+
errorCode: "DOCUMENT_RESTORE_CONFLICT",
|
|
103
|
+
statusCode: 409,
|
|
104
|
+
details: {
|
|
105
|
+
expectedUpdatedAt: args.expectedUpdatedAt,
|
|
106
|
+
currentUpdatedAt: beforeFlush.updatedAt,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
await flushOpenDocumentEditorToSql({ documentId, ownerEmail });
|
|
112
|
+
const [afterFlush] = await db
|
|
113
|
+
.select({ updatedAt: schema.documents.updatedAt })
|
|
114
|
+
.from(schema.documents)
|
|
115
|
+
.where(
|
|
116
|
+
and(
|
|
117
|
+
eq(schema.documents.id, documentId),
|
|
118
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
.limit(1);
|
|
122
|
+
if (!afterFlush) {
|
|
123
|
+
throw new ActionContractError("Document not found.", {
|
|
124
|
+
errorCode: "DOCUMENT_NOT_FOUND",
|
|
125
|
+
statusCode: 404,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const expectedUpdatedAt = afterFlush.updatedAt;
|
|
81
129
|
let softDeletedDatabaseIds: string[] = [];
|
|
82
130
|
const updated = await db.transaction(async (rawTx) => {
|
|
83
131
|
const tx = rawTx as any;
|
|
@@ -110,14 +158,14 @@ export default defineAction({
|
|
|
110
158
|
if (isLinkedLocalSource(documentId, current)) {
|
|
111
159
|
linkedLocalRestoreUnsupported();
|
|
112
160
|
}
|
|
113
|
-
if (current.updatedAt !==
|
|
161
|
+
if (current.updatedAt !== expectedUpdatedAt) {
|
|
114
162
|
throw new ActionContractError(
|
|
115
163
|
"The document changed after this restore was prepared.",
|
|
116
164
|
{
|
|
117
165
|
errorCode: "DOCUMENT_RESTORE_CONFLICT",
|
|
118
166
|
statusCode: 409,
|
|
119
167
|
details: {
|
|
120
|
-
expectedUpdatedAt
|
|
168
|
+
expectedUpdatedAt,
|
|
121
169
|
currentUpdatedAt: current.updatedAt,
|
|
122
170
|
},
|
|
123
171
|
},
|
|
@@ -126,7 +126,12 @@ export function Layout({ children }: LayoutProps) {
|
|
|
126
126
|
return {
|
|
127
127
|
list: {
|
|
128
128
|
action: "list-document-versions",
|
|
129
|
-
args:
|
|
129
|
+
args: (threadId) => ({
|
|
130
|
+
documentId,
|
|
131
|
+
includeContent: false,
|
|
132
|
+
limit: 100,
|
|
133
|
+
...(threadId ? { threadId } : {}),
|
|
134
|
+
}),
|
|
130
135
|
getVersions: (result: unknown) => {
|
|
131
136
|
const versions =
|
|
132
137
|
result && typeof result === "object"
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { getDb, schema } from "../db/index.js";
|
|
9
9
|
import {
|
|
10
10
|
documentVersionChatContextFromAction,
|
|
11
|
+
type DocumentVersionChatContext,
|
|
11
12
|
serializeDocumentVersionChatContext,
|
|
12
13
|
} from "./document-version-context.js";
|
|
13
14
|
|
|
@@ -26,6 +27,8 @@ export interface DocumentHistoryCause {
|
|
|
26
27
|
actorEmail?: string | null;
|
|
27
28
|
actorKind?: Exclude<DocumentHistoryActorKind, "unknown">;
|
|
28
29
|
origin?: string;
|
|
30
|
+
chatContext?: DocumentVersionChatContext;
|
|
31
|
+
skipBeforeCheckpoint?: boolean;
|
|
29
32
|
operation: string;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -42,6 +45,14 @@ function newGroupId(prefix: string) {
|
|
|
42
45
|
return `${prefix}:${crypto.randomUUID()}`;
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
export function documentChatStartVersionId(
|
|
49
|
+
ownerEmail: string,
|
|
50
|
+
documentId: string,
|
|
51
|
+
chatStartKey: string,
|
|
52
|
+
) {
|
|
53
|
+
return `agent-chat-start:${encodeURIComponent(ownerEmail)}:${encodeURIComponent(documentId)}:${encodeURIComponent(chatStartKey)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
export function resolveDocumentHistoryCause(
|
|
46
57
|
cause: DocumentHistoryCause,
|
|
47
58
|
): ResolvedDocumentHistoryCause {
|
|
@@ -152,9 +163,10 @@ export async function recordDocumentHistoryTransition(args: {
|
|
|
152
163
|
|
|
153
164
|
let beforeCheckpointId: string | undefined;
|
|
154
165
|
if (
|
|
155
|
-
!
|
|
156
|
-
latest
|
|
157
|
-
|
|
166
|
+
!args.cause.skipBeforeCheckpoint &&
|
|
167
|
+
(!latest ||
|
|
168
|
+
latest.title !== args.before.title ||
|
|
169
|
+
latest.content !== args.before.content)
|
|
158
170
|
) {
|
|
159
171
|
beforeCheckpointId = crypto.randomUUID();
|
|
160
172
|
await args.db.insert(schema.documentVersions).values({
|
|
@@ -164,7 +176,8 @@ export async function recordDocumentHistoryTransition(args: {
|
|
|
164
176
|
title: args.before.title,
|
|
165
177
|
content: args.before.content,
|
|
166
178
|
chatContext: serializeDocumentVersionChatContext(
|
|
167
|
-
|
|
179
|
+
args.cause.chatContext ??
|
|
180
|
+
documentVersionChatContextFromAction(args.cause.ctx),
|
|
168
181
|
),
|
|
169
182
|
...cause,
|
|
170
183
|
checkpointKind: "before",
|
|
@@ -176,20 +189,58 @@ export async function recordDocumentHistoryTransition(args: {
|
|
|
176
189
|
const afterCreatedAt = beforeCheckpointId
|
|
177
190
|
? new Date(new Date(checkpointCreatedAt).getTime() + 1).toISOString()
|
|
178
191
|
: checkpointCreatedAt;
|
|
179
|
-
const
|
|
180
|
-
|
|
192
|
+
const isChatStart =
|
|
193
|
+
cause.groupKind === "agent_run" && cause.operation === "chat start";
|
|
194
|
+
const chatStartKey = args.cause.chatContext?.threadId ?? cause.groupId;
|
|
195
|
+
const afterCheckpointId = isChatStart
|
|
196
|
+
? documentChatStartVersionId(args.ownerEmail, args.documentId, chatStartKey)
|
|
197
|
+
: crypto.randomUUID();
|
|
198
|
+
const afterValues = {
|
|
181
199
|
id: afterCheckpointId,
|
|
182
200
|
ownerEmail: args.ownerEmail,
|
|
183
201
|
documentId: args.documentId,
|
|
184
202
|
title: args.after.title,
|
|
185
203
|
content: args.after.content,
|
|
186
204
|
chatContext: serializeDocumentVersionChatContext(
|
|
187
|
-
|
|
205
|
+
args.cause.chatContext ??
|
|
206
|
+
documentVersionChatContextFromAction(args.cause.ctx),
|
|
188
207
|
),
|
|
189
208
|
...cause,
|
|
190
209
|
checkpointKind: "after",
|
|
191
210
|
createdAt: afterCreatedAt,
|
|
192
211
|
updatedAt: afterCreatedAt,
|
|
193
|
-
}
|
|
212
|
+
};
|
|
213
|
+
if (isChatStart) {
|
|
214
|
+
const [inserted] = await args.db
|
|
215
|
+
.insert(schema.documentVersions)
|
|
216
|
+
.values(afterValues)
|
|
217
|
+
.onConflictDoNothing()
|
|
218
|
+
.returning({ id: schema.documentVersions.id });
|
|
219
|
+
if (!inserted) {
|
|
220
|
+
const [existing] = await args.db
|
|
221
|
+
.select({ id: schema.documentVersions.id })
|
|
222
|
+
.from(schema.documentVersions)
|
|
223
|
+
.where(
|
|
224
|
+
and(
|
|
225
|
+
eq(schema.documentVersions.ownerEmail, args.ownerEmail),
|
|
226
|
+
eq(schema.documentVersions.documentId, args.documentId),
|
|
227
|
+
eq(schema.documentVersions.id, afterCheckpointId),
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
.limit(1);
|
|
231
|
+
if (!existing) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
"Chat-start history checkpoint conflict had no matching checkpoint.",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
groupId: cause.groupId,
|
|
238
|
+
beforeCheckpointId,
|
|
239
|
+
afterCheckpointId: existing.id,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
await args.db.insert(schema.documentVersions).values(afterValues);
|
|
244
|
+
}
|
|
194
245
|
return { groupId: cause.groupId, beforeCheckpointId, afterCheckpointId };
|
|
195
246
|
}
|
|
@@ -4,12 +4,14 @@ export interface DocumentVersionChatContext {
|
|
|
4
4
|
threadId?: string;
|
|
5
5
|
runId?: string;
|
|
6
6
|
turnId?: string;
|
|
7
|
+
phase?: "start" | "end";
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
function contextFromFields(value: {
|
|
10
11
|
threadId?: unknown;
|
|
11
12
|
runId?: unknown;
|
|
12
13
|
turnId?: unknown;
|
|
14
|
+
phase?: unknown;
|
|
13
15
|
}): DocumentVersionChatContext | undefined {
|
|
14
16
|
const context: DocumentVersionChatContext = {};
|
|
15
17
|
for (const key of ["threadId", "runId", "turnId"] as const) {
|
|
@@ -17,6 +19,9 @@ function contextFromFields(value: {
|
|
|
17
19
|
context[key] = value[key];
|
|
18
20
|
}
|
|
19
21
|
}
|
|
22
|
+
if (value.phase === "start" || value.phase === "end") {
|
|
23
|
+
context.phase = value.phase;
|
|
24
|
+
}
|
|
20
25
|
return Object.keys(context).length > 0 ? context : undefined;
|
|
21
26
|
}
|
|
22
27
|
|
|
@@ -3,9 +3,17 @@ import {
|
|
|
3
3
|
createAgentChatPlugin,
|
|
4
4
|
loadActionsFromStaticRegistry,
|
|
5
5
|
} from "@agent-native/core/server";
|
|
6
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
7
|
+
import { and, eq } from "drizzle-orm";
|
|
6
8
|
|
|
7
9
|
import actionsRegistry from "../../.generated/actions-registry.js";
|
|
10
|
+
import { flushOpenDocumentEditorToSql } from "../../actions/_document-flush.js";
|
|
11
|
+
import { getDb, schema } from "../db/index.js";
|
|
8
12
|
import { resolveCommentAiActionSurface } from "../lib/comment-ai.js";
|
|
13
|
+
import {
|
|
14
|
+
documentChatStartVersionId,
|
|
15
|
+
recordDocumentHistoryTransition,
|
|
16
|
+
} from "../lib/document-history.js";
|
|
9
17
|
import {
|
|
10
18
|
publicDocumentExtraContext,
|
|
11
19
|
resolvePublicViewerOwner,
|
|
@@ -21,8 +29,164 @@ const INJECTED_INITIAL_TOOL_NAMES = [
|
|
|
21
29
|
"query-staged-dataset",
|
|
22
30
|
];
|
|
23
31
|
|
|
32
|
+
const DOCUMENT_EDIT_TOOLS = new Set([
|
|
33
|
+
"edit-document",
|
|
34
|
+
"restore-document-version",
|
|
35
|
+
"update-document",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function eventRecord(entry: unknown): Record<string, unknown> | undefined {
|
|
39
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
40
|
+
const event = (entry as { event?: unknown }).event;
|
|
41
|
+
return event && typeof event === "object"
|
|
42
|
+
? (event as Record<string, unknown>)
|
|
43
|
+
: undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function inputForCompletedTool(
|
|
47
|
+
events: readonly unknown[],
|
|
48
|
+
index: number,
|
|
49
|
+
completed: Record<string, unknown>,
|
|
50
|
+
): Record<string, unknown> | undefined {
|
|
51
|
+
if (completed.input && typeof completed.input === "object") {
|
|
52
|
+
return completed.input as Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
const id = typeof completed.id === "string" ? completed.id : undefined;
|
|
55
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
56
|
+
const candidate = eventRecord(events[cursor]);
|
|
57
|
+
if (
|
|
58
|
+
candidate?.type !== "tool_start" ||
|
|
59
|
+
candidate.tool !== completed.tool ||
|
|
60
|
+
(id && candidate.id !== id)
|
|
61
|
+
) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return candidate.input && typeof candidate.input === "object"
|
|
65
|
+
? (candidate.input as Record<string, unknown>)
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasDocumentEdit(
|
|
72
|
+
run: { events: readonly unknown[] },
|
|
73
|
+
documentId: string,
|
|
74
|
+
): boolean {
|
|
75
|
+
return run.events.some((entry, index) => {
|
|
76
|
+
const record = eventRecord(entry);
|
|
77
|
+
if (
|
|
78
|
+
record?.type !== "tool_done" ||
|
|
79
|
+
record.completedSideEffect !== true ||
|
|
80
|
+
record.isError === true ||
|
|
81
|
+
typeof record.tool !== "string" ||
|
|
82
|
+
!DOCUMENT_EDIT_TOOLS.has(record.tool)
|
|
83
|
+
) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const input = inputForCompletedTool(run.events, index, record);
|
|
87
|
+
return (input?.documentId ?? input?.id) === documentId;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function autosaveDocumentAtChatBoundary(
|
|
92
|
+
scope: { type: string; id: string },
|
|
93
|
+
run: { events?: readonly unknown[]; threadId?: string; runId?: string },
|
|
94
|
+
phase: "start" | "end",
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const hasEdit = run.events
|
|
97
|
+
? hasDocumentEdit({ events: run.events }, scope.id)
|
|
98
|
+
: false;
|
|
99
|
+
if (
|
|
100
|
+
scope.type !== "document" ||
|
|
101
|
+
!run.threadId ||
|
|
102
|
+
!run.runId ||
|
|
103
|
+
(phase === "end" && !hasEdit)
|
|
104
|
+
) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let access = await assertAccess("document", scope.id, "editor");
|
|
109
|
+
let document = access.resource as {
|
|
110
|
+
ownerEmail: string;
|
|
111
|
+
title: string;
|
|
112
|
+
content: string;
|
|
113
|
+
};
|
|
114
|
+
if (phase === "start") {
|
|
115
|
+
await flushOpenDocumentEditorToSql({
|
|
116
|
+
documentId: scope.id,
|
|
117
|
+
ownerEmail: document.ownerEmail,
|
|
118
|
+
});
|
|
119
|
+
access = await assertAccess("document", scope.id, "editor");
|
|
120
|
+
document = access.resource as typeof document;
|
|
121
|
+
}
|
|
122
|
+
const db = getDb();
|
|
123
|
+
const chatContext = { threadId: run.threadId, runId: run.runId, phase };
|
|
124
|
+
|
|
125
|
+
if (phase === "start") {
|
|
126
|
+
const existing = await db
|
|
127
|
+
.select({ id: schema.documentVersions.id })
|
|
128
|
+
.from(schema.documentVersions)
|
|
129
|
+
.where(
|
|
130
|
+
and(
|
|
131
|
+
eq(schema.documentVersions.documentId, scope.id),
|
|
132
|
+
eq(schema.documentVersions.ownerEmail, document.ownerEmail),
|
|
133
|
+
eq(
|
|
134
|
+
schema.documentVersions.id,
|
|
135
|
+
documentChatStartVersionId(
|
|
136
|
+
document.ownerEmail,
|
|
137
|
+
scope.id,
|
|
138
|
+
run.threadId,
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
.limit(1);
|
|
144
|
+
if (existing.length) return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const state = { title: document.title, content: document.content };
|
|
148
|
+
await recordDocumentHistoryTransition({
|
|
149
|
+
db,
|
|
150
|
+
ownerEmail: document.ownerEmail,
|
|
151
|
+
documentId: scope.id,
|
|
152
|
+
before: state,
|
|
153
|
+
after: state,
|
|
154
|
+
cause: {
|
|
155
|
+
groupId: `agent:${document.ownerEmail}:${run.runId}`,
|
|
156
|
+
groupKind: "agent_run",
|
|
157
|
+
actorEmail: document.ownerEmail,
|
|
158
|
+
actorKind: "agent",
|
|
159
|
+
origin: "agent-chat",
|
|
160
|
+
operation: phase === "start" ? "chat start" : "chat autosave",
|
|
161
|
+
chatContext,
|
|
162
|
+
...(phase === "start" ? { skipBeforeCheckpoint: true } : {}),
|
|
163
|
+
},
|
|
164
|
+
now: new Date().toISOString(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function autosaveDocumentBeforeAgentTurn(
|
|
169
|
+
scope: { type: string; id: string },
|
|
170
|
+
run: { threadId?: string; runId?: string },
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
await autosaveDocumentAtChatBoundary(scope, run, "start");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function autosaveDocumentAfterAgentTurn(
|
|
176
|
+
scope: { type: string; id: string },
|
|
177
|
+
run: {
|
|
178
|
+
events: readonly unknown[];
|
|
179
|
+
threadId?: string;
|
|
180
|
+
runId?: string;
|
|
181
|
+
},
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
await autosaveDocumentAtChatBoundary(scope, run, "end");
|
|
184
|
+
}
|
|
185
|
+
|
|
24
186
|
export default createAgentChatPlugin({
|
|
25
187
|
appId: "content",
|
|
188
|
+
onAgentTurnStart: autosaveDocumentBeforeAgentTurn,
|
|
189
|
+
onAgentTurnComplete: autosaveDocumentAfterAgentTurn,
|
|
26
190
|
nativeActionsInDev: true,
|
|
27
191
|
resolveActionSurface: resolveCommentAiActionSurface,
|
|
28
192
|
durableBackgroundRuns: true,
|
|
@@ -9,6 +9,7 @@ export default defineAction({
|
|
|
9
9
|
"metadata only, not the stored file contents.",
|
|
10
10
|
schema: z.object({
|
|
11
11
|
designId: z.string().describe("Design project ID"),
|
|
12
|
+
threadId: z.string().min(1).optional(),
|
|
12
13
|
limit: z.coerce
|
|
13
14
|
.number()
|
|
14
15
|
.int()
|
|
@@ -19,5 +20,6 @@ export default defineAction({
|
|
|
19
20
|
}),
|
|
20
21
|
http: { method: "GET" },
|
|
21
22
|
readOnly: true,
|
|
22
|
-
run: async ({ designId, limit }) =>
|
|
23
|
+
run: async ({ designId, limit, threadId }) =>
|
|
24
|
+
listDesignVersions(designId, limit, threadId),
|
|
23
25
|
});
|
|
@@ -129,15 +129,29 @@ export function Layout({ children }: LayoutProps) {
|
|
|
129
129
|
if (!designId) return null;
|
|
130
130
|
return { type: "design" as const, id: designId };
|
|
131
131
|
}, [location.pathname]);
|
|
132
|
+
const flushDesignEditorSaves = useCallback(async () => {
|
|
133
|
+
const flushes: Promise<void>[] = [];
|
|
134
|
+
window.dispatchEvent(
|
|
135
|
+
new CustomEvent("agent-native:design-flush-pending-saves", {
|
|
136
|
+
detail: flushes,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
await Promise.all(flushes);
|
|
140
|
+
}, []);
|
|
132
141
|
const designChatHistory = useMemo<
|
|
133
142
|
AssistantChatHistoryConfig | undefined
|
|
134
143
|
>(() => {
|
|
135
144
|
if (!designScope) return undefined;
|
|
136
145
|
const designId = designScope.id;
|
|
137
146
|
return {
|
|
147
|
+
beforeStart: flushDesignEditorSaves,
|
|
138
148
|
list: {
|
|
139
149
|
action: "list-design-versions",
|
|
140
|
-
args:
|
|
150
|
+
args: (threadId) => ({
|
|
151
|
+
designId,
|
|
152
|
+
limit: 100,
|
|
153
|
+
...(threadId ? { threadId } : {}),
|
|
154
|
+
}),
|
|
141
155
|
getVersions: (result: unknown) => {
|
|
142
156
|
const versions =
|
|
143
157
|
result && typeof result === "object"
|
|
@@ -154,9 +168,10 @@ export function Layout({ children }: LayoutProps) {
|
|
|
154
168
|
designId,
|
|
155
169
|
versionId: version.id,
|
|
156
170
|
}),
|
|
171
|
+
beforeRestore: flushDesignEditorSaves,
|
|
157
172
|
},
|
|
158
173
|
};
|
|
159
|
-
}, [designScope]);
|
|
174
|
+
}, [designScope, flushDesignEditorSaves]);
|
|
160
175
|
const chatHomeHandoffActive = useAgentChatHomeHandoff({
|
|
161
176
|
storageKey: DESIGN_CHAT_STORAGE_KEY,
|
|
162
177
|
activePath: location.pathname,
|
|
@@ -741,9 +741,9 @@ export function flushFileContentSavesOnBackground(
|
|
|
741
741
|
pendingByFileId: FileContentSaveRequestsById,
|
|
742
742
|
latestUnacknowledgedByFileId: FileContentSaveRequestsById,
|
|
743
743
|
timerIds: readonly number[],
|
|
744
|
-
save: (pending: FileContentSaveRequest) =>
|
|
744
|
+
save: (pending: FileContentSaveRequest) => unknown | Promise<unknown>,
|
|
745
745
|
clearTimer: (timerId: number) => void,
|
|
746
|
-
): void {
|
|
746
|
+
): Promise<void> {
|
|
747
747
|
const newestByFileId = new Map<string, FileContentSaveRequest>();
|
|
748
748
|
for (const pending of Object.values(latestUnacknowledgedByFileId)) {
|
|
749
749
|
newestByFileId.set(pending.id, pending);
|
|
@@ -754,8 +754,15 @@ export function flushFileContentSavesOnBackground(
|
|
|
754
754
|
newestByFileId.set(pending.id, pending);
|
|
755
755
|
}
|
|
756
756
|
}
|
|
757
|
-
|
|
757
|
+
const saves = [...newestByFileId.values()].map((pending) => {
|
|
758
|
+
try {
|
|
759
|
+
return Promise.resolve(save(pending));
|
|
760
|
+
} catch (error) {
|
|
761
|
+
return Promise.reject(error);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
758
764
|
for (const timerId of timerIds) clearTimer(timerId);
|
|
765
|
+
return Promise.all(saves).then(() => undefined);
|
|
759
766
|
}
|
|
760
767
|
|
|
761
768
|
/**
|