@agent-native/core 0.188.0 → 0.189.0
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/CHANGELOG.md +6 -0
- 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/content/vite.config.ts +2 -38
- package/corpus/templates/design/CHANGELOG.md +4 -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/mail/app/components/layout/AppLayout.tsx +36 -11
- package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +82 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +18 -19
- package/corpus/templates/mail/changelog/2026-09-25-keep-inbox-tab-counts-consistent-when-switching-tabs.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +4 -0
- 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/cli/index.js +1 -2
- 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/collab/awareness.d.ts +2 -2
- package/dist/deploy/build.d.ts +1 -1
- package/dist/deploy/build.js +9 -348
- package/dist/deploy/workspace-deploy.d.ts +2 -2
- package/dist/deploy/workspace-deploy.js +9 -130
- 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/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- 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 +2 -2
- package/dist/server/onboarding-html.js +28 -5
- package/dist/vite/client.d.ts +1 -1
- package/docs/content/a2a-protocol.mdx +2 -2
- package/docs/content/cloudflare.mdx +2 -10
- package/docs/content/deploy-an-app.mdx +0 -1
- package/docs/content/deployment.mdx +14 -9
- package/docs/content/environment-variables.mdx +1 -1
- package/docs/content/multi-app-workspace.mdx +1 -1
- package/docs/content/netlify.mdx +1 -1
- package/docs/content/real-time-collaboration.mdx +6 -6
- package/docs/content/workspace-deployment.mdx +4 -15
- package/package.json +4 -4
package/corpus/README.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Content are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new").
|
|
5
5
|
|
|
6
|
+
## 2026-09-25
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Chat messages can be reverted to their autosaved version, including a shortcut to restore the start of a chat.
|
|
11
|
+
|
|
6
12
|
## 2026-09-23
|
|
7
13
|
|
|
8
14
|
### Added
|
|
@@ -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,
|
|
@@ -389,41 +389,6 @@ const dynamicLocalComponentDirs = (() => {
|
|
|
389
389
|
}
|
|
390
390
|
})();
|
|
391
391
|
|
|
392
|
-
const cloudflareSsrStubs =
|
|
393
|
-
process.env.NITRO_PRESET === "cloudflare_pages"
|
|
394
|
-
? [
|
|
395
|
-
"@assistant-ui/react",
|
|
396
|
-
"@tiptap/core",
|
|
397
|
-
"@tiptap/extension-blockquote",
|
|
398
|
-
"@tiptap/extension-code-block-lowlight",
|
|
399
|
-
"@tiptap/extension-collaboration",
|
|
400
|
-
"@tiptap/extension-collaboration-caret",
|
|
401
|
-
"@tiptap/extension-image",
|
|
402
|
-
"@tiptap/extension-link",
|
|
403
|
-
"@tiptap/extension-placeholder",
|
|
404
|
-
"@tiptap/extension-table",
|
|
405
|
-
"@tiptap/extension-table-cell",
|
|
406
|
-
"@tiptap/extension-table-header",
|
|
407
|
-
"@tiptap/extension-table-row",
|
|
408
|
-
"@tiptap/extension-task-item",
|
|
409
|
-
"@tiptap/extension-task-list",
|
|
410
|
-
"@tiptap/pm",
|
|
411
|
-
"@tiptap/react",
|
|
412
|
-
"@tiptap/starter-kit",
|
|
413
|
-
"@xterm/addon-fit",
|
|
414
|
-
"@xterm/addon-web-links",
|
|
415
|
-
"@xterm/xterm",
|
|
416
|
-
"lowlight",
|
|
417
|
-
"prettier",
|
|
418
|
-
"react-markdown",
|
|
419
|
-
"remark-gfm",
|
|
420
|
-
"remark-mdx",
|
|
421
|
-
"tiptap-markdown",
|
|
422
|
-
"yjs",
|
|
423
|
-
"y-protocols",
|
|
424
|
-
]
|
|
425
|
-
: [];
|
|
426
|
-
|
|
427
392
|
export default defineConfig({
|
|
428
393
|
plugins: [
|
|
429
394
|
contentLocalComponentsPlugin(),
|
|
@@ -434,9 +399,8 @@ export default defineConfig({
|
|
|
434
399
|
...(localWorkspaceRoot ? [localWorkspaceRoot] : []),
|
|
435
400
|
...dynamicLocalComponentDirs,
|
|
436
401
|
],
|
|
437
|
-
// shiki only runs in AssistantChat's useEffect — keep it out of
|
|
438
|
-
|
|
439
|
-
ssrStubs: ["shiki", ...cloudflareSsrStubs],
|
|
402
|
+
// shiki only runs in AssistantChat's useEffect — keep it out of SSR bundles.
|
|
403
|
+
ssrStubs: ["shiki"],
|
|
440
404
|
}),
|
|
441
405
|
],
|
|
442
406
|
optimizeDeps: {
|
|
@@ -5,6 +5,10 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
5
5
|
|
|
6
6
|
## 2026-09-25
|
|
7
7
|
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Chat messages can be reverted to their autosaved version, including a shortcut to restore the start of a chat.
|
|
11
|
+
|
|
8
12
|
### Fixed
|
|
9
13
|
|
|
10
14
|
- Cross-screen moves preserve Flex and Grid sizing through undo and redo.
|
|
@@ -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
|
});
|