@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.
Files changed (73) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/CHANGELOG.md +6 -0
  3. package/corpus/templates/content/actions/list-document-versions.ts +102 -2
  4. package/corpus/templates/content/actions/restore-document-version.ts +50 -2
  5. package/corpus/templates/content/app/components/layout/Layout.tsx +6 -1
  6. package/corpus/templates/content/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
  7. package/corpus/templates/content/server/lib/document-history.ts +59 -8
  8. package/corpus/templates/content/server/lib/document-version-context.ts +5 -0
  9. package/corpus/templates/content/server/plugins/agent-chat.ts +164 -0
  10. package/corpus/templates/content/vite.config.ts +2 -38
  11. package/corpus/templates/design/CHANGELOG.md +4 -0
  12. package/corpus/templates/design/actions/list-design-versions.ts +3 -1
  13. package/corpus/templates/design/app/components/layout/Layout.tsx +17 -2
  14. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +10 -3
  15. package/corpus/templates/design/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
  16. package/corpus/templates/design/server/lib/design-versions.ts +136 -20
  17. package/corpus/templates/design/server/plugins/agent-chat.ts +18 -2
  18. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +36 -11
  19. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +82 -0
  20. package/corpus/templates/mail/app/pages/InboxPage.tsx +18 -19
  21. package/corpus/templates/mail/changelog/2026-09-25-keep-inbox-tab-counts-consistent-when-switching-tabs.md +6 -0
  22. package/corpus/templates/slides/CHANGELOG.md +4 -0
  23. package/corpus/templates/slides/actions/list-deck-versions.ts +62 -10
  24. package/corpus/templates/slides/app/components/layout/Layout.tsx +10 -2
  25. package/corpus/templates/slides/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
  26. package/corpus/templates/slides/server/lib/deck-versions.ts +36 -1
  27. package/corpus/templates/slides/server/plugins/agent-chat.ts +26 -2
  28. package/dist/automation/index.d.ts +1 -1
  29. package/dist/cli/index.js +1 -2
  30. package/dist/client/AssistantChat.js +112 -35
  31. package/dist/client/chat/assistant-chat-history-version.d.ts +1 -0
  32. package/dist/client/chat/message-components.d.ts +8 -1
  33. package/dist/client/chat/message-components.js +52 -11
  34. package/dist/client/resources/mcp-integration-catalog.js +1 -1
  35. package/dist/client/resources/mcp-integration-logos.js +29 -30
  36. package/dist/collab/awareness.d.ts +2 -2
  37. package/dist/deploy/build.d.ts +1 -1
  38. package/dist/deploy/build.js +9 -348
  39. package/dist/deploy/workspace-deploy.d.ts +2 -2
  40. package/dist/deploy/workspace-deploy.js +9 -130
  41. package/dist/localization/core-messages/ar-SA.js +2 -0
  42. package/dist/localization/core-messages/de-DE.js +2 -0
  43. package/dist/localization/core-messages/en-US.d.ts +2 -0
  44. package/dist/localization/core-messages/en-US.js +2 -0
  45. package/dist/localization/core-messages/es-ES.js +2 -0
  46. package/dist/localization/core-messages/fr-FR.js +2 -0
  47. package/dist/localization/core-messages/hi-IN.js +2 -0
  48. package/dist/localization/core-messages/ja-JP.js +2 -0
  49. package/dist/localization/core-messages/ko-KR.js +2 -0
  50. package/dist/localization/core-messages/pt-BR.js +2 -0
  51. package/dist/localization/core-messages/zh-CN.js +2 -0
  52. package/dist/localization/core-messages/zh-TW.js +2 -0
  53. package/dist/localization/core-messages.d.ts +2 -0
  54. package/dist/observability/routes.d.ts +3 -3
  55. package/dist/progress/routes.d.ts +1 -1
  56. package/dist/secrets/routes.d.ts +9 -9
  57. package/dist/server/agent-chat/plugin-options.d.ts +2 -0
  58. package/dist/server/agent-chat-plugin.d.ts +1 -0
  59. package/dist/server/agent-chat-plugin.js +26 -0
  60. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  61. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  62. package/dist/server/onboarding-html.js +28 -5
  63. package/dist/vite/client.d.ts +1 -1
  64. package/docs/content/a2a-protocol.mdx +2 -2
  65. package/docs/content/cloudflare.mdx +2 -10
  66. package/docs/content/deploy-an-app.mdx +0 -1
  67. package/docs/content/deployment.mdx +14 -9
  68. package/docs/content/environment-variables.mdx +1 -1
  69. package/docs/content/multi-app-workspace.mdx +1 -1
  70. package/docs/content/netlify.mdx +1 -1
  71. package/docs/content/real-time-collaboration.mdx +6 -6
  72. package/docs/content/workspace-deployment.mdx +4 -15
  73. package/package.json +4 -4
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 9959
34
+ - template files: 9963
@@ -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: versions.map((version) => {
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 !== args.expectedUpdatedAt) {
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: args.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: { documentId, includeContent: false, limit: 100 },
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"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Chat messages can be reverted to their autosaved version, including a shortcut to restore the start of a chat.
@@ -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
- !latest ||
156
- latest.title !== args.before.title ||
157
- latest.content !== args.before.content
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
- documentVersionChatContextFromAction(args.cause.ctx),
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 afterCheckpointId = crypto.randomUUID();
180
- await args.db.insert(schema.documentVersions).values({
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
- documentVersionChatContextFromAction(args.cause.ctx),
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 the
438
- // CF Pages Functions bundle (25 MiB limit).
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 }) => listDesignVersions(designId, limit),
23
+ run: async ({ designId, limit, threadId }) =>
24
+ listDesignVersions(designId, limit, threadId),
23
25
  });