@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
@@ -1,6 +1,6 @@
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";
@@ -50,13 +50,18 @@ export default defineAction({
50
50
  schema: z.object({
51
51
  deckId: z.string().describe("Deck ID"),
52
52
  limit: z.coerce.number().int().min(1).max(100).default(50),
53
+ threadId: z.string().min(1).optional(),
53
54
  }),
54
55
  http: { method: "GET" },
55
- run: async ({ deckId, limit }) => {
56
+ run: async ({ deckId, limit, threadId }) => {
56
57
  const access = await assertAccess("deck", deckId, "viewer");
57
58
  const ownerEmail = access.resource.ownerEmail as string;
58
59
  const db = getDb();
59
60
 
61
+ const where = and(
62
+ eq(schema.deckVersions.deckId, deckId),
63
+ eq(schema.deckVersions.ownerEmail, ownerEmail),
64
+ );
60
65
  const versions = await db
61
66
  .select({
62
67
  id: schema.deckVersions.id,
@@ -68,19 +73,66 @@ export default defineAction({
68
73
  createdAt: schema.deckVersions.createdAt,
69
74
  })
70
75
  .from(schema.deckVersions)
71
- .where(
72
- and(
73
- eq(schema.deckVersions.deckId, deckId),
74
- eq(schema.deckVersions.ownerEmail, ownerEmail),
75
- ),
76
- )
76
+ .where(where)
77
77
  .orderBy(desc(schema.deckVersions.createdAt))
78
78
  .limit(limit);
79
+ const beginningVersions = await db
80
+ .select({
81
+ id: schema.deckVersions.id,
82
+ deckId: schema.deckVersions.deckId,
83
+ title: schema.deckVersions.title,
84
+ data: schema.deckVersions.data,
85
+ changeLabel: schema.deckVersions.changeLabel,
86
+ chatContext: schema.deckVersions.chatContext,
87
+ createdAt: schema.deckVersions.createdAt,
88
+ })
89
+ .from(schema.deckVersions)
90
+ .where(
91
+ threadId
92
+ ? and(
93
+ where,
94
+ eq(schema.deckVersions.changeGroup, `start:thread:${threadId}`),
95
+ )
96
+ : and(
97
+ where,
98
+ like(schema.deckVersions.chatContext, '%"phase":"start"%'),
99
+ ),
100
+ )
101
+ .orderBy(
102
+ threadId
103
+ ? asc(schema.deckVersions.createdAt)
104
+ : desc(schema.deckVersions.createdAt),
105
+ )
106
+ .limit(threadId ? 1 : limit);
107
+ const versionsById = new Map(
108
+ [...beginningVersions, ...versions].map((version) => [
109
+ version.id,
110
+ version,
111
+ ]),
112
+ );
113
+ const allVersions = [...versionsById.values()];
114
+ allVersions.sort(
115
+ (left, right) =>
116
+ new Date(right.createdAt).getTime() -
117
+ new Date(left.createdAt).getTime(),
118
+ );
119
+ const limitedVersions = allVersions.slice(0, limit);
120
+ const activeStart = threadId
121
+ ? allVersions.find((version) => beginningVersions.includes(version))
122
+ : undefined;
123
+ if (activeStart && !limitedVersions.includes(activeStart)) {
124
+ limitedVersions[limitedVersions.length - 1] = activeStart;
125
+ limitedVersions.sort(
126
+ (left, right) =>
127
+ new Date(right.createdAt).getTime() -
128
+ new Date(left.createdAt).getTime(),
129
+ );
130
+ }
79
131
 
80
132
  return {
81
133
  deckId,
82
- count: versions.length,
83
- versions: versions.map((version) => {
134
+ count: limitedVersions.length,
135
+ versions: limitedVersions.map((version) => {
84
136
  let chatContext;
85
137
  try {
86
138
  chatContext = parseDeckVersionChatContext(version.chatContext);
@@ -21,6 +21,7 @@ import { IconMenu2 } from "@tabler/icons-react";
21
21
  import { useEffect, useMemo, useRef, useState } from "react";
22
22
  import { useLocation, useNavigate } from "react-router";
23
23
 
24
+ import { useDecks } from "@/context/DeckContext";
24
25
  import { useSidebarCollapsed } from "@/hooks/use-sidebar-collapsed";
25
26
  import {
26
27
  buildSlidesAgentContext,
@@ -67,6 +68,7 @@ export function Layout({ children }: LayoutProps) {
67
68
  const location = useLocation();
68
69
  const navigate = useNavigate();
69
70
  const t = useT();
71
+ const { flushDeckSave } = useDecks();
70
72
  const creativeContextEnabled = useCreativeContextLab();
71
73
  const isChatRoute =
72
74
  location.pathname === "/chat" || location.pathname.startsWith("/chat/");
@@ -155,9 +157,14 @@ export function Layout({ children }: LayoutProps) {
155
157
  if (!deckScope) return undefined;
156
158
  const deckId = deckScope.id;
157
159
  return {
160
+ beforeStart: () => flushDeckSave(deckId),
158
161
  list: {
159
162
  action: "list-deck-versions",
160
- args: { deckId, limit: 100 },
163
+ args: (threadId) => ({
164
+ deckId,
165
+ limit: 100,
166
+ ...(threadId ? { threadId } : {}),
167
+ }),
161
168
  getVersions: (result: unknown) => {
162
169
  const versions =
163
170
  result && typeof result === "object"
@@ -174,9 +181,10 @@ export function Layout({ children }: LayoutProps) {
174
181
  deckId,
175
182
  versionId: version.id,
176
183
  }),
184
+ beforeRestore: () => flushDeckSave(deckId),
177
185
  },
178
186
  };
179
- }, [deckScope]);
187
+ }, [deckScope, flushDeckSave]);
180
188
 
181
189
  useAgentChatHomeHandoffLinks({
182
190
  storageKey: "slides",
@@ -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.
@@ -13,12 +13,14 @@ export interface DeckVersionChatContext {
13
13
  threadId?: string;
14
14
  runId?: string;
15
15
  turnId?: string;
16
+ phase?: "start" | "end";
16
17
  }
17
18
 
18
19
  function contextFromFields(value: {
19
20
  threadId?: unknown;
20
21
  runId?: unknown;
21
22
  turnId?: unknown;
23
+ phase?: unknown;
22
24
  }): DeckVersionChatContext | undefined {
23
25
  const context: DeckVersionChatContext = {};
24
26
  for (const key of ["threadId", "runId", "turnId"] as const) {
@@ -26,6 +28,9 @@ function contextFromFields(value: {
26
28
  context[key] = value[key];
27
29
  }
28
30
  }
31
+ if (value.phase === "start" || value.phase === "end") {
32
+ context.phase = value.phase;
33
+ }
29
34
  return Object.keys(context).length > 0 ? context : undefined;
30
35
  }
31
36
 
@@ -139,6 +144,7 @@ export async function createDeckVersionSnapshot(
139
144
  .limit(1);
140
145
 
141
146
  if (
147
+ options.chatContext?.phase !== "start" &&
142
148
  latestVersion &&
143
149
  latestVersion.title === source.title &&
144
150
  deckContentSignature(latestVersion.data) ===
@@ -150,8 +156,12 @@ export async function createDeckVersionSnapshot(
150
156
  const requestedChatContext = serializeDeckVersionChatContext(
151
157
  options.chatContext,
152
158
  );
153
- const changeGroup =
159
+ const turnGroup =
154
160
  options.chatContext?.turnId ?? options.chatContext?.runId ?? undefined;
161
+ const changeGroup =
162
+ options.chatContext?.phase === "start" && turnGroup
163
+ ? `start:thread:${options.chatContext.threadId ?? turnGroup}`
164
+ : turnGroup;
155
165
  if (
156
166
  requestedChatContext &&
157
167
  requestedChatContext === normalizedChatContext(latestVersion?.chatContext)
@@ -196,3 +206,28 @@ export async function createDeckVersionSnapshot(
196
206
 
197
207
  return { created: true, id };
198
208
  }
209
+
210
+ export async function createDeckChatBeginningSnapshot(
211
+ source: DeckSnapshotSource,
212
+ run: { threadId: string; runId: string },
213
+ ): Promise<{ created: boolean; id?: string; reason?: string }> {
214
+ const rows = await getDb()
215
+ .select({ id: schema.deckVersions.id })
216
+ .from(schema.deckVersions)
217
+ .where(
218
+ and(
219
+ eq(schema.deckVersions.deckId, source.id),
220
+ eq(schema.deckVersions.ownerEmail, source.ownerEmail),
221
+ eq(schema.deckVersions.changeGroup, `start:thread:${run.threadId}`),
222
+ ),
223
+ )
224
+ .limit(1);
225
+ if (rows.length) {
226
+ return { created: false, reason: "beginning-exists" };
227
+ }
228
+ return createDeckVersionSnapshot(source, {
229
+ force: true,
230
+ label: "Before chat",
231
+ chatContext: { ...run, phase: "start" },
232
+ });
233
+ }
@@ -7,7 +7,10 @@ import { assertAccess } from "@agent-native/core/sharing";
7
7
  import actionsRegistry from "../../.generated/actions-registry.js";
8
8
  import { resolveSlidesRequestAuthContext } from "../handlers/request-auth-context.js";
9
9
  import { prepareSlidesChatAttachments } from "../lib/chat-attachments.js";
10
- import { deckVersionChatContextFromRun } from "../lib/deck-versions.js";
10
+ import {
11
+ createDeckChatBeginningSnapshot,
12
+ deckVersionChatContextFromRun,
13
+ } from "../lib/deck-versions.js";
11
14
  import "../register-secrets.js";
12
15
 
13
16
  const SLIDES_BACKGROUND_RUN_SOFT_TIMEOUT_MS = 13 * 60_000;
@@ -138,6 +141,7 @@ async function autosaveDeckAfterAgentTurn(
138
141
  },
139
142
  ): Promise<void> {
140
143
  if (scope.type !== "deck" || !hasDeckEdit(run, scope.id)) return;
144
+ if (!run.threadId || !run.runId) return;
141
145
 
142
146
  const access = await assertAccess("deck", scope.id, "editor");
143
147
  const deck = access.resource as {
@@ -147,15 +151,35 @@ async function autosaveDeckAfterAgentTurn(
147
151
  ownerEmail: string;
148
152
  };
149
153
  const { createDeckVersionSnapshot } = await import("../lib/deck-versions.js");
154
+ const chatContext = deckVersionChatContextFromRun(run);
150
155
  await createDeckVersionSnapshot(deck, {
151
156
  force: true,
152
157
  label: "Chat autosave",
153
- chatContext: deckVersionChatContextFromRun(run),
158
+ chatContext: chatContext ? { ...chatContext, phase: "end" } : undefined,
159
+ });
160
+ }
161
+
162
+ async function autosaveDeckBeforeAgentTurn(
163
+ scope: { type: string; id: string },
164
+ run: { threadId?: string; runId?: string },
165
+ ): Promise<void> {
166
+ if (scope.type !== "deck" || !run.threadId || !run.runId) return;
167
+ const access = await assertAccess("deck", scope.id, "editor");
168
+ const deck = access.resource as {
169
+ id: string;
170
+ title: string;
171
+ data: string;
172
+ ownerEmail: string;
173
+ };
174
+ await createDeckChatBeginningSnapshot(deck, {
175
+ threadId: run.threadId,
176
+ runId: run.runId,
154
177
  });
155
178
  }
156
179
 
157
180
  export default createAgentChatPlugin({
158
181
  appId: "slides",
182
+ onAgentTurnStart: autosaveDeckBeforeAgentTurn,
159
183
  onAgentTurnComplete: autosaveDeckAfterAgentTurn,
160
184
  actions: loadActionsFromStaticRegistry(actionsRegistry),
161
185
  initialToolNames: INITIAL_TOOL_NAMES,
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
186
186
  duplicate?: undefined;
187
187
  eventId?: undefined;
188
188
  } | {
189
- error?: undefined;
190
189
  accepted: boolean;
191
190
  duplicate: boolean;
192
191
  eventId: string;
192
+ error?: undefined;
193
193
  }>;
package/dist/cli/index.js CHANGED
@@ -1233,8 +1233,7 @@ Options:
1233
1233
  --standalone Scaffold a single standalone app (no workspace)
1234
1234
  --emit [dir] With migrate, emit an own-agent dossier
1235
1235
  --describe <text> With migrate, describe URL/prose-only sources
1236
- --preset <name> Workspace deploy preset:
1237
- cloudflare_pages (default), netlify, or vercel
1236
+ --preset <name> Workspace deploy preset: netlify (default) or vercel
1238
1237
  --build-only Build workspace deploy artifacts without publishing
1239
1238
  --eager With workspace dev, start every app immediately
1240
1239
  --prewarm With workspace dev, warm non-default apps in the background
@@ -23,7 +23,7 @@ import { AgentActivityTrace } from "./chat/agent-activity-trace.js";
23
23
  import { DownscalingImageAttachmentAdapter, BinaryDocumentAttachmentAdapter, MAX_PDF_BYTES, MAX_ESTIMATED_BODY_BYTES, AGGRESSIVE_MAX_IMAGE_DIMENSION, AGGRESSIVE_JPEG_QUALITY, transcodeImageToDataURL, createAgentImageAttachments, serializeQueuedAttachments, getSubmittedPromptBodyStrings, measureJsonStringBytes, getAttachmentBodyStrings, } from "./chat/attachment-adapters.js";
24
24
  import { readAssistantChatComposerDraft, writeAssistantChatComposerDraft, } from "./chat/composer-draft.js";
25
25
  import { AgentTextStreamingProvider, ExternalTextStreamingContext, } from "./chat/markdown-renderer.js";
26
- import { AssistantChatHistoryContext, assistantMessageHasCompletedSideEffect, findMatchingAssistantChatHistoryVersion, isAssistantChatHistoryVersion, CheckpointContext, MessageActionsContext, assistantMessageRunId, assistantMessageTurnId, UserMessage, AssistantMessage, ExternalUserStoppedRunContext, SelectionAttachedPill, displayableUserMessageText, isHiddenUserMessage, ServerRunActiveContext, UserStoppedRunContext, } from "./chat/message-components.js";
26
+ import { AssistantChatHistoryContext, AssistantChatHistoryBeginningRevertButton, assistantMessageHasCompletedSideEffect, findAssistantChatHistoryBeginningVersion, findMatchingAssistantChatHistoryVersion, isAssistantChatHistoryVersion, CheckpointContext, MessageActionsContext, assistantMessageRunId, assistantMessageTurnId, UserMessage, AssistantMessage, ExternalUserStoppedRunContext, SelectionAttachedPill, displayableUserMessageText, isHiddenUserMessage, ServerRunActiveContext, UserStoppedRunContext, } from "./chat/message-components.js";
27
27
  import { repoHasAssistantMessage, getRepoMessages, getRepoMessage, shouldImportServerThreadData, dedupeRepoMessagesById, dropEmptyAssistantMessages, withLastAssistantRunDuration, } from "./chat/repo-helpers.js";
28
28
  import { BuilderSetupCard, LoopLimitContinueCard, RunErrorRecoveryCard, PlanModeCallout, getLoopLimitMetadata, getRunErrorMetadata, getRequestModeMetadata, isBuilderReconnectRunError, runErrorKey, } from "./chat/run-recovery.js";
29
29
  import { createAgentChatRuntimeAdapter, } from "./chat/runtime.js";
@@ -1321,6 +1321,7 @@ export function shouldShowAssistantChatSuggestions(visibility, hasAssistantMessa
1321
1321
  return visibility === "always" || hasAssistantMessage;
1322
1322
  }
1323
1323
  export async function restoreAssistantChatHistoryVersion(options) {
1324
+ await options.history.restore.beforeRestore?.();
1324
1325
  const args = await options.history.restore.args(options.version);
1325
1326
  const restored = await options.restore(args);
1326
1327
  let applicationFailed = false;
@@ -1976,7 +1977,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1976
1977
  isRunningRef.current = isRunning;
1977
1978
  const submissionInFlightRef = useRef(0);
1978
1979
  const submissionTailRef = useRef(Promise.resolve());
1979
- const chatHistoryListQuery = useActionQuery((chatHistory?.list.action ?? "list-resource-versions"), chatHistory?.list.args, { enabled: chatHistory !== undefined });
1980
+ const chatHistoryListQuery = useActionQuery((chatHistory?.list.action ?? "list-resource-versions"), (typeof chatHistory?.list.args === "function"
1981
+ ? chatHistory.list.args(threadId)
1982
+ : chatHistory?.list.args), { enabled: chatHistory !== undefined });
1980
1983
  const chatHistoryVersions = useMemo(() => {
1981
1984
  if (!chatHistory || chatHistoryListQuery.data == null)
1982
1985
  return [];
@@ -1990,24 +1993,59 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1990
1993
  const refetchChatHistory = chatHistoryListQuery.refetch;
1991
1994
  const restoreHistory = chatHistoryRestoreMutation.mutateAsync;
1992
1995
  const createHistoryVersion = chatHistoryCreateMutation.mutateAsync;
1996
+ const [isChatHistoryRestoring, setIsChatHistoryRestoring] = useState(false);
1997
+ const chatHistoryRestoreInFlightRef = useRef(false);
1998
+ const chatHistoryRestoreWaitRef = useRef(null);
1999
+ const waitForChatHistoryRestore = useCallback(async () => {
2000
+ let restoreWait = chatHistoryRestoreWaitRef.current;
2001
+ while (restoreWait) {
2002
+ await restoreWait;
2003
+ restoreWait = chatHistoryRestoreWaitRef.current;
2004
+ }
2005
+ }, []);
1993
2006
  const restoreChatHistoryVersion = useCallback(async (version) => {
1994
2007
  if (!chatHistory)
1995
2008
  return;
1996
- await restoreAssistantChatHistoryVersion({
1997
- history: chatHistory,
1998
- version,
1999
- restore: restoreHistory,
2000
- refetch: refetchChatHistory,
2001
- onRefetchError: (error) => captureError(error, {
2002
- tags: {
2003
- source: "agent-chat-client",
2004
- phase: "chat-history-refetch-after-restore",
2005
- },
2006
- }),
2009
+ if (chatHistoryRestoreInFlightRef.current) {
2010
+ throw new Error("A chat history restore is already in progress.");
2011
+ }
2012
+ if (submissionInFlightRef.current > 0) {
2013
+ throw new Error("A chat submission is already in progress.");
2014
+ }
2015
+ let finishRestore;
2016
+ const restoreWait = new Promise((resolve) => {
2017
+ finishRestore = resolve;
2007
2018
  });
2019
+ chatHistoryRestoreWaitRef.current = restoreWait;
2020
+ chatHistoryRestoreInFlightRef.current = true;
2021
+ setIsChatHistoryRestoring(true);
2022
+ try {
2023
+ await restoreAssistantChatHistoryVersion({
2024
+ history: chatHistory,
2025
+ version,
2026
+ restore: restoreHistory,
2027
+ refetch: refetchChatHistory,
2028
+ onRefetchError: (error) => captureError(error, {
2029
+ tags: {
2030
+ source: "agent-chat-client",
2031
+ phase: "chat-history-refetch-after-restore",
2032
+ },
2033
+ }),
2034
+ });
2035
+ }
2036
+ finally {
2037
+ chatHistoryRestoreInFlightRef.current = false;
2038
+ setIsChatHistoryRestoring(false);
2039
+ if (chatHistoryRestoreWaitRef.current === restoreWait) {
2040
+ chatHistoryRestoreWaitRef.current = null;
2041
+ }
2042
+ finishRestore();
2043
+ }
2008
2044
  }, [chatHistory, refetchChatHistory, restoreHistory]);
2009
2045
  const chatHistoryContext = useMemo(() => chatHistory
2010
2046
  ? {
2047
+ beginningVersion: findAssistantChatHistoryBeginningVersion(chatHistoryVersions, threadId, chatHistory.isEditable),
2048
+ isRestoring: isChatHistoryRestoring,
2011
2049
  findVersion: (message) => findMatchingAssistantChatHistoryVersion(chatHistoryVersions, message, {
2012
2050
  isEditable: chatHistory.isEditable,
2013
2051
  scope: chatHistory.scope ?? contextScope ?? undefined,
@@ -2015,7 +2053,13 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2015
2053
  }),
2016
2054
  restoreVersion: restoreChatHistoryVersion,
2017
2055
  }
2018
- : null, [chatHistory, chatHistoryVersions, restoreChatHistoryVersion]);
2056
+ : null, [
2057
+ chatHistory,
2058
+ chatHistoryVersions,
2059
+ isChatHistoryRestoring,
2060
+ restoreChatHistoryVersion,
2061
+ threadId,
2062
+ ]);
2019
2063
  const chatHistoryRunObservedRef = useRef(false);
2020
2064
  const chatHistoryCreateKeyRef = useRef(null);
2021
2065
  useEffect(() => {
@@ -2035,6 +2079,12 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2035
2079
  .find((message) => message.role === "assistant");
2036
2080
  if (!latestAssistantMessage ||
2037
2081
  !assistantMessageHasCompletedSideEffect(latestAssistantMessage)) {
2082
+ void refetchChatHistory().catch((error) => captureError(error, {
2083
+ tags: {
2084
+ source: "agent-chat-client",
2085
+ phase: "chat-history-refetch-after-run",
2086
+ },
2087
+ }));
2038
2088
  return;
2039
2089
  }
2040
2090
  const historyMessage = {
@@ -3736,6 +3786,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3736
3786
  // Keep prompts visible while setup is missing. Dequeuing them would remove
3737
3787
  // the only user-visible copy and immediately re-enter the provider failure.
3738
3788
  if (isRestoring ||
3789
+ isChatHistoryRestoring ||
3739
3790
  engineSetupRequired ||
3740
3791
  isRunning ||
3741
3792
  queuedMessages.length === 0) {
@@ -3762,7 +3813,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3762
3813
  // complete. Starting the queued turn during that window can reconnect
3763
3814
  // to the old run and replay the old answer under the new prompt.
3764
3815
  const runCleared = await waitForThreadRunToClear(apiUrl, threadId);
3765
- if (cancelled)
3816
+ if (cancelled || chatHistoryRestoreInFlightRef.current)
3766
3817
  return;
3767
3818
  if (!runCleared) {
3768
3819
  // The server still owns this turn (including a deferred durable
@@ -3869,6 +3920,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3869
3920
  apiUrl,
3870
3921
  appendThreadMessage,
3871
3922
  applyLocalQueuedMessages,
3923
+ isChatHistoryRestoring,
3872
3924
  isRestoring,
3873
3925
  isRunning,
3874
3926
  engineSetupRequired,
@@ -4201,7 +4253,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4201
4253
  }, [onStop, stopActiveRun]);
4202
4254
  // The composer stop button uses the handler above; queued send-now keeps the
4203
4255
  // active run alive and only promotes the selected message for later dequeue.
4204
- const sendQueuedMessageNow = useCallback((id) => {
4256
+ const sendQueuedMessageNow = useCallback(async (id) => {
4257
+ await waitForChatHistoryRestore();
4205
4258
  const message = queuedMessagesRef.current.find((candidate) => candidate.id === id);
4206
4259
  if (!message || message.promoted)
4207
4260
  return;
@@ -4235,9 +4288,18 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4235
4288
  return;
4236
4289
  }
4237
4290
  applyLocalQueuedMessages((prev) => promoteQueuedMessage(prev, id));
4238
- }, [appendThreadMessage, applyLocalQueuedMessages, threadId, threadRuntime]);
4291
+ }, [
4292
+ appendThreadMessage,
4293
+ applyLocalQueuedMessages,
4294
+ threadId,
4295
+ threadRuntime,
4296
+ waitForChatHistoryRestore,
4297
+ ]);
4239
4298
  const visibleQueuedMessages = useMemo(() => queuedMessages.filter((message) => !message.hideUserMessage && !message.promoted), [queuedMessages]);
4240
4299
  const addToQueue = useCallback(async (text, images, references, attachments, requestMode, intent = "queued", recoveryAction, includeComposerContext = false, trackInRunsTray = false, preserveReconnectAutoRecoveryBudget = false, hideUserMessage = false, submitMessageId, approvedToolCalls, continuationTurnId, usageLabel, actionScope) => {
4300
+ if (isAgentChatSubmitCancelled(submitMessageId))
4301
+ return false;
4302
+ await waitForChatHistoryRestore();
4241
4303
  if (isAgentChatSubmitCancelled(submitMessageId))
4242
4304
  return false;
4243
4305
  const wasSubmissionInFlight = submissionInFlightRef.current > 0;
@@ -4448,6 +4510,16 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4448
4510
  ]);
4449
4511
  }
4450
4512
  else {
4513
+ try {
4514
+ await chatHistory?.beforeStart?.();
4515
+ }
4516
+ catch (error) {
4517
+ setComposerError(String(error));
4518
+ reportAgentChatSubmitResult(submitMessageId, false, "editor-save-failed");
4519
+ return false;
4520
+ }
4521
+ if (isAgentChatSubmitCancelled(submitMessageId))
4522
+ return false;
4451
4523
  markOptimisticRunning();
4452
4524
  try {
4453
4525
  appendThreadMessage({
@@ -4506,6 +4578,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4506
4578
  t,
4507
4579
  threadId,
4508
4580
  updateComposerContextItems,
4581
+ waitForChatHistoryRestore,
4582
+ chatHistory,
4509
4583
  ]);
4510
4584
  const mcpResumeTimerRef = useRef(null);
4511
4585
  const scheduleMcpConnectionResume = useCallback((request) => {
@@ -4974,19 +5048,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4974
5048
  if (engineSetupRequired)
4975
5049
  return;
4976
5050
  void addToQueue(suggestion);
4977
- }, className: "agent-empty-suggestion w-full rounded-xl border border-border/70 bg-card/60 px-3 py-2.5 text-left text-[13px] text-muted-foreground shadow-sm transition-[border-color,background-color,color,transform] hover:-translate-y-px hover:border-border hover:bg-card hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: suggestion }, suggestion))) })) : null, showInlineEmptyThreadFooterSlot ? (_jsx("div", { className: "agent-thread-footer-slot agent-thread-footer-slot--empty", children: resolvedThreadFooterSlot })) : null, emptyStateFooter ? (_jsx("div", { className: "agent-empty-state-footer", children: emptyStateFooter })) : null] })) : (_jsxs(MessageScrollerContent, { className: "agent-thread-content gap-4 px-4 py-4", children: [resolvedThreadContentSlot ? (_jsx(MessageScrollerItem, { children: _jsx("div", { className: "agent-thread-content-slot", children: resolvedThreadContentSlot }) })) : null, threadRestoreErrorSurface ? (_jsx(MessageScrollerItem, { children: threadRestoreErrorSurface })) : null, _jsx(AssistantMessageListErrorBoundary, { resetKey: messageListResetKey, children: _jsx(UserStoppedRunContext.Provider, { value: wasUserStoppedRun, children: _jsx(ExternalUserStoppedRunContext.Provider, { value: externalUserStopped, children: _jsx(ExternalTextStreamingContext.Provider, { value: externalStreaming, children: _jsx(AssistantChatHistoryContext.Provider, { value: chatHistoryContext, children: _jsx(ThreadPrimitive.Messages
4978
- // Deliberately NOT keyed on part structure. Doing that
4979
- // remounted the whole transcript every time a tool call
4980
- // started or a placeholder id was rewritten — a flash and a
4981
- // lost scroll position in the middle of an answer. The
4982
- // error boundary above is the mechanism for assistant-ui's
4983
- // stale tap-resource errors: it catches them, clears, and
4984
- // retries, and its retry signature includes the reset key so
4985
- // a genuinely new structure always gets a fresh budget.
4986
- // `assistant-ui-part-churn.spec.tsx` drives append, mutate,
4987
- // rename and splice through both the import and streaming
4988
- // paths and records that no such error occurs.
4989
- , {
5051
+ }, className: "agent-empty-suggestion w-full rounded-xl border border-border/70 bg-card/60 px-3 py-2.5 text-left text-[13px] text-muted-foreground shadow-sm transition-[border-color,background-color,color,transform] hover:-translate-y-px hover:border-border hover:bg-card hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: suggestion }, suggestion))) })) : null, showInlineEmptyThreadFooterSlot ? (_jsx("div", { className: "agent-thread-footer-slot agent-thread-footer-slot--empty", children: resolvedThreadFooterSlot })) : null, emptyStateFooter ? (_jsx("div", { className: "agent-empty-state-footer", children: emptyStateFooter })) : null] })) : (_jsxs(MessageScrollerContent, { className: "agent-thread-content gap-4 px-4 py-4", children: [resolvedThreadContentSlot ? (_jsx(MessageScrollerItem, { children: _jsx("div", { className: "agent-thread-content-slot", children: resolvedThreadContentSlot }) })) : null, threadRestoreErrorSurface ? (_jsx(MessageScrollerItem, { children: threadRestoreErrorSurface })) : null, _jsx(AssistantMessageListErrorBoundary, { resetKey: messageListResetKey, children: _jsx(UserStoppedRunContext.Provider, { value: wasUserStoppedRun, children: _jsx(ExternalUserStoppedRunContext.Provider, { value: externalUserStopped, children: _jsx(ExternalTextStreamingContext.Provider, { value: externalStreaming, children: _jsxs(AssistantChatHistoryContext.Provider, { value: chatHistoryContext, children: [chatHistoryContext?.beginningVersion ? (_jsx(MessageScrollerItem, { children: _jsx(AssistantChatHistoryBeginningRevertButton, {}) })) : null, _jsx(ThreadPrimitive.Messages
4990
5052
  // Deliberately NOT keyed on part structure. Doing that
4991
5053
  // remounted the whole transcript every time a tool call
4992
5054
  // started or a placeholder id was rewritten — a flash and a
@@ -4998,10 +5060,22 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4998
5060
  // `assistant-ui-part-churn.spec.tsx` drives append, mutate,
4999
5061
  // rename and splice through both the import and streaming
5000
5062
  // paths and records that no such error occurs.
5001
- components: {
5002
- UserMessage: AssistantChatUserMessageItem,
5003
- AssistantMessage: AssistantChatAssistantMessageItem,
5004
- } }) }) }) }) }) }), visibleLoopLimit && !showRunningInUI && (_jsx(MessageScrollerItem, { children: _jsx(LoopLimitContinueCard, { info: visibleLoopLimit, onContinue: () => {
5063
+ , {
5064
+ // Deliberately NOT keyed on part structure. Doing that
5065
+ // remounted the whole transcript every time a tool call
5066
+ // started or a placeholder id was rewritten — a flash and a
5067
+ // lost scroll position in the middle of an answer. The
5068
+ // error boundary above is the mechanism for assistant-ui's
5069
+ // stale tap-resource errors: it catches them, clears, and
5070
+ // retries, and its retry signature includes the reset key so
5071
+ // a genuinely new structure always gets a fresh budget.
5072
+ // `assistant-ui-part-churn.spec.tsx` drives append, mutate,
5073
+ // rename and splice through both the import and streaming
5074
+ // paths and records that no such error occurs.
5075
+ components: {
5076
+ UserMessage: AssistantChatUserMessageItem,
5077
+ AssistantMessage: AssistantChatAssistantMessageItem,
5078
+ } })] }) }) }) }) }), visibleLoopLimit && !showRunningInUI && (_jsx(MessageScrollerItem, { children: _jsx(LoopLimitContinueCard, { info: visibleLoopLimit, onContinue: () => {
5005
5079
  setShowContinue(false);
5006
5080
  setLoopLimitInfo(null);
5007
5081
  const continuation = latestProtocolContinuationContext(messagesRef.current);
@@ -5089,7 +5163,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
5089
5163
  ? bounceMissingKeySetup
5090
5164
  : undefined, children: _jsxs(_Fragment, { children: [_jsx(ComposerAttachmentPreviewStrip, {}), _jsx(TiptapComposer, { focusRef: tiptapRef, maxDocumentAttachmentBytes: MAX_PDF_BYTES, initialText: initialComposerText ?? undefined, initialTextKey: composerDraftScope, onTextChange: isActiveComposer
5091
5165
  ? handleComposerTextChange
5092
- : undefined, disabled: isComposerDisabled || showMissingKeySetup, placeholder: showMissingKeySetup
5166
+ : undefined, disabled: isComposerDisabled ||
5167
+ showMissingKeySetup ||
5168
+ isChatHistoryRestoring, placeholder: showMissingKeySetup
5093
5169
  ? t("agentChat.setup.connectPlaceholder")
5094
5170
  : engineSetupRequired
5095
5171
  ? t("agentChat.setup.connectPlaceholder")
@@ -5119,6 +5195,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
5119
5195
  }
5120
5196
  }, willQueue: engineSetupRequired ||
5121
5197
  isRunning ||
5198
+ isChatHistoryRestoring ||
5122
5199
  submissionInFlightRef.current > 0, onSlashCommand: onSlashCommand, execMode: execMode, onExecModeChange: onExecModeChange, planModeDisabled: planModeDisabled, planModeDisabledReason: planModeDisabledReason, selectedModel: selectedModel ?? defaultModel, selectedEffort: selectedEffort, availableModels: availableModels, availableAgents: availableAgents, selectedAgent: selectedAgent, hostedHarness: hostedHarness, modelListLoading: modelListLoading, onModelChange: shouldShowAssistantChatModelSelector(showModelSelector)
5123
5200
  ? onModelChange
5124
5201
  : undefined, onEffortChange: onEffortChange, onAgentChange: onAgentChange, imageModelMenu: imageModelMenu, onConnectProvider: onConnectProvider, onConnectLocalRuntime: onConnectLocalRuntime, toolbarSlot: composerToolbarSlot, contextItems: visibleComposerContextItems, onRemoveContextItem: removeComposerContextItem, plusMenuMode: plusMenuMode, layoutVariant: composerLayoutVariant, providerConnectStatusEnabled: providerStatusChecksEnabled, voiceEnabled: true, draftScope: composerDraftScope, interceptBuildRequestsForBuilder: true, onAttachmentError: setComposerError, extraActionButton: composerExtraActionButton, stopButton: showRunningInUI ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: handleComposerStop, "aria-label": t("agentChat.composer.stopResponse"), "data-agent-composer-slot": "stop-button", className: "shrink-0 flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: _jsx(IconPlayerStopFilled, { className: "h-3 w-3" }) }) }), _jsx(TooltipContent, { children: t("agentChat.composer.stopResponse") })] })) : undefined })] }) }) })] })] }) }) }) }) }) }) }) }) }) }) }));
@@ -7,6 +7,7 @@ export interface AssistantChatHistoryVersion {
7
7
  threadId?: string;
8
8
  runId?: string;
9
9
  turnId?: string;
10
+ phase?: "start" | "end";
10
11
  };
11
12
  }
12
13
  export declare function coerceAssistantChatHistoryDate(value: unknown): Date | null;
@@ -57,14 +57,17 @@ export interface AssistantChatHistoryMessage {
57
57
  hasCompletedSideEffect: boolean;
58
58
  }
59
59
  export interface AssistantChatHistoryConfig<TListResult = unknown, TVersion extends AssistantChatHistoryVersion = AssistantChatHistoryVersion, TRestoreResult = unknown> {
60
+ /** Flush host editor writes before an agent turn starts. */
61
+ beforeStart?: () => void | Promise<void>;
60
62
  list: {
61
63
  action: string;
62
- args?: Record<string, unknown>;
64
+ args?: Record<string, unknown> | ((threadId?: string) => Record<string, unknown>);
63
65
  getVersions: (result: TListResult) => readonly TVersion[];
64
66
  };
65
67
  restore: {
66
68
  action: string;
67
69
  args: (version: TVersion) => Record<string, unknown> | Promise<Record<string, unknown>>;
70
+ beforeRestore?: () => void | Promise<void>;
68
71
  onRestored?: (result: TRestoreResult, version: TVersion) => void | Promise<void>;
69
72
  };
70
73
  createVersion?: {
@@ -76,6 +79,8 @@ export interface AssistantChatHistoryConfig<TListResult = unknown, TVersion exte
76
79
  matchVersion?: (version: TVersion, message: AssistantChatHistoryMessage) => boolean;
77
80
  }
78
81
  export interface AssistantChatHistoryContextValue {
82
+ beginningVersion: AssistantChatHistoryVersion | null;
83
+ isRestoring: boolean;
79
84
  findVersion: (message: AssistantChatHistoryMessage) => AssistantChatHistoryVersion | null;
80
85
  restoreVersion: (version: AssistantChatHistoryVersion) => Promise<void>;
81
86
  }
@@ -93,6 +98,7 @@ export declare const MessageActionsContext: React.Context<{
93
98
  export declare function isLocalDevelopmentHost(hostname: string): boolean;
94
99
  export declare function assistantMessageHasCompletedSideEffect(message: unknown): boolean;
95
100
  export declare function findMatchingAssistantChatHistoryVersion<TVersion extends AssistantChatHistoryVersion>(versions: readonly TVersion[], message: AssistantChatHistoryMessage, options?: Pick<AssistantChatHistoryConfig<unknown, TVersion>, "isEditable" | "matchVersion" | "scope">): TVersion | null;
101
+ export declare function findAssistantChatHistoryBeginningVersion<TVersion extends AssistantChatHistoryVersion>(versions: readonly TVersion[], threadId?: string, isEditable?: (version: TVersion) => boolean): TVersion | null;
96
102
  /**
97
103
  * Restore rewrites the working tree, so only offer it when the server actually
98
104
  * has a checkpoint for this turn. Auto-checkpointing skips turns that started
@@ -129,6 +135,7 @@ export declare function MessageActionsMenu({ showRevert, onRevert, threadId, }?:
129
135
  onRevert?: () => void;
130
136
  threadId?: string;
131
137
  }): React.JSX.Element;
138
+ export declare function AssistantChatHistoryBeginningRevertButton(): React.JSX.Element | null;
132
139
  export declare function UserMessage(): React.JSX.Element | null;
133
140
  export declare function messageTextFromContent(content: unknown): string;
134
141
  export declare function isMissingFinalResponseWarningText(text: string): boolean;