@agent-native/core 0.207.0-nightly-20261011124220 → 0.207.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 (108) hide show
  1. package/dist/agent/production-agent.js +23 -6
  2. package/dist/agent/thread-data-builder.js +7 -4
  3. package/dist/cli/eval.js +20 -4
  4. package/dist/client/analytics/index.d.ts +1 -1
  5. package/dist/client/analytics.d.ts +12 -3
  6. package/dist/client/analytics.js +51 -17
  7. package/dist/client/onboarding/use-onboarding.d.ts +1 -0
  8. package/dist/client/onboarding/use-onboarding.js +19 -6
  9. package/dist/collab/struct-routes.d.ts +1 -1
  10. package/dist/eval/agent-runner.js +18 -8
  11. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  12. package/dist/mcp/connect-route.js +9 -4
  13. package/dist/observability/routes.d.ts +7 -7
  14. package/dist/onboarding/plugin.js +15 -45
  15. package/dist/progress/routes.d.ts +1 -1
  16. package/dist/resources/store.js +25 -30
  17. package/dist/server/agent-discovery.js +47 -53
  18. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  19. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  20. package/dist/server/builtin-agents.d.ts +2 -0
  21. package/dist/server/builtin-agents.js +50 -3
  22. package/dist/server/credential-provider.d.ts +6 -9
  23. package/dist/server/credential-provider.js +33 -21
  24. package/dist/templates/builder-code-starter/.agents/skills/adding-a-feature/SKILL.md.patch +1 -1
  25. package/dist/templates/builder-code-starter/AGENTS.md.patch +3 -3
  26. package/dist/templates/builder-code-starter/app/components/layout/Layout.tsx.patch +6 -7
  27. package/dist/templates/builder-code-starter/app/hooks/use-navigation-state.ts.patch +21 -28
  28. package/dist/templates/builder-code-starter/app/root.tsx.patch +16 -5
  29. package/dist/templates/builder-code-starter/template-layer.json +4 -0
  30. package/dist/templates/chat/.agents/skills/secrets/SKILL.md +6 -2
  31. package/dist/templates/chat/AGENTS.md +3 -3
  32. package/dist/templates/chat/CHANGELOG.md +6 -0
  33. package/dist/templates/chat/app/components/chat/ChatHomeRedirect.tsx +25 -0
  34. package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +828 -15
  35. package/dist/templates/chat/app/components/home/HomePage.tsx +332 -0
  36. package/dist/templates/chat/app/components/layout/AgentInspector.tsx +2 -4
  37. package/dist/templates/chat/app/components/layout/Header.tsx +1 -1
  38. package/dist/templates/chat/app/components/layout/Layout.tsx +5 -6
  39. package/dist/templates/chat/app/components/layout/Sidebar.tsx +35 -11
  40. package/dist/templates/chat/app/components/ui/item.tsx +1 -0
  41. package/dist/templates/chat/app/components/ui/separator.tsx +1 -0
  42. package/dist/templates/chat/app/components/ui/skeleton.tsx +1 -0
  43. package/dist/templates/chat/app/hooks/use-navigation-state.ts +16 -22
  44. package/dist/templates/chat/app/i18n/ar-SA.ts +25 -0
  45. package/dist/templates/chat/app/i18n/de-DE.ts +26 -0
  46. package/dist/templates/chat/app/i18n/en-US.ts +25 -0
  47. package/dist/templates/chat/app/i18n/es-ES.ts +26 -0
  48. package/dist/templates/chat/app/i18n/fr-FR.ts +26 -0
  49. package/dist/templates/chat/app/i18n/hi-IN.ts +26 -0
  50. package/dist/templates/chat/app/i18n/ja-JP.ts +26 -0
  51. package/dist/templates/chat/app/i18n/ko-KR.ts +26 -0
  52. package/dist/templates/chat/app/i18n/pt-BR.ts +26 -0
  53. package/dist/templates/chat/app/i18n/zh-CN.ts +25 -0
  54. package/dist/templates/chat/app/i18n/zh-TW.ts +25 -0
  55. package/dist/templates/chat/app/lib/chat-paths.ts +479 -0
  56. package/dist/templates/chat/app/root.tsx +14 -3
  57. package/dist/templates/chat/app/routes/chat._index.tsx +5 -0
  58. package/dist/templates/chat/app/routes/home.tsx +3 -22
  59. package/dist/templates/chat/changelog/2026-10-10-home-handoff-recovery-capacity.md +6 -0
  60. package/dist/templates/default/.agents/skills/secrets/SKILL.md +6 -2
  61. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +6 -2
  62. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +6 -2
  63. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  64. package/docs/content/deployment-environment-variables.mdx +10 -10
  65. package/docs/content/environment-variables.mdx +38 -6
  66. package/docs/content/getting-started.mdx +7 -7
  67. package/docs/content/multi-app-workspace.mdx +7 -3
  68. package/docs/content/template-chat-first-edits.mdx +46 -33
  69. package/package.json +4 -4
  70. package/src/templates/builder-code-starter/.agents/skills/adding-a-feature/SKILL.md.patch +1 -1
  71. package/src/templates/builder-code-starter/AGENTS.md.patch +3 -3
  72. package/src/templates/builder-code-starter/app/components/layout/Layout.tsx.patch +6 -7
  73. package/src/templates/builder-code-starter/app/hooks/use-navigation-state.ts.patch +21 -28
  74. package/src/templates/builder-code-starter/app/root.tsx.patch +16 -5
  75. package/src/templates/builder-code-starter/template-layer.json +4 -0
  76. package/src/templates/chat/.agents/skills/secrets/SKILL.md +6 -2
  77. package/src/templates/chat/AGENTS.md +3 -3
  78. package/src/templates/chat/CHANGELOG.md +6 -0
  79. package/src/templates/chat/app/components/chat/ChatHomeRedirect.tsx +25 -0
  80. package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +828 -15
  81. package/src/templates/chat/app/components/home/HomePage.tsx +332 -0
  82. package/src/templates/chat/app/components/layout/AgentInspector.tsx +2 -4
  83. package/src/templates/chat/app/components/layout/Header.tsx +1 -1
  84. package/src/templates/chat/app/components/layout/Layout.tsx +5 -6
  85. package/src/templates/chat/app/components/layout/Sidebar.tsx +35 -11
  86. package/src/templates/chat/app/components/ui/item.tsx +1 -0
  87. package/src/templates/chat/app/components/ui/separator.tsx +1 -0
  88. package/src/templates/chat/app/components/ui/skeleton.tsx +1 -0
  89. package/src/templates/chat/app/hooks/use-navigation-state.ts +16 -22
  90. package/src/templates/chat/app/i18n/ar-SA.ts +25 -0
  91. package/src/templates/chat/app/i18n/de-DE.ts +26 -0
  92. package/src/templates/chat/app/i18n/en-US.ts +25 -0
  93. package/src/templates/chat/app/i18n/es-ES.ts +26 -0
  94. package/src/templates/chat/app/i18n/fr-FR.ts +26 -0
  95. package/src/templates/chat/app/i18n/hi-IN.ts +26 -0
  96. package/src/templates/chat/app/i18n/ja-JP.ts +26 -0
  97. package/src/templates/chat/app/i18n/ko-KR.ts +26 -0
  98. package/src/templates/chat/app/i18n/pt-BR.ts +26 -0
  99. package/src/templates/chat/app/i18n/zh-CN.ts +25 -0
  100. package/src/templates/chat/app/i18n/zh-TW.ts +25 -0
  101. package/src/templates/chat/app/lib/chat-paths.ts +479 -0
  102. package/src/templates/chat/app/root.tsx +14 -3
  103. package/src/templates/chat/app/routes/chat._index.tsx +5 -0
  104. package/src/templates/chat/app/routes/home.tsx +3 -22
  105. package/src/templates/chat/changelog/2026-10-10-home-handoff-recovery-capacity.md +6 -0
  106. package/src/templates/default/.agents/skills/secrets/SKILL.md +6 -2
  107. package/src/templates/headless/.agents/skills/secrets/SKILL.md +6 -2
  108. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +6 -2
@@ -1,7 +1,11 @@
1
1
  import type {
2
2
  AgentConnectionRequest,
3
3
  AgentMessage,
4
+ AgentRunOptions,
5
+ FilePart,
4
6
  } from "@agent-native/agentkit";
7
+ import { appendAgentChatContextToMessage } from "@agent-native/agentkit";
8
+ import { hasActiveAgentRuns } from "@agent-native/agentkit/client";
5
9
  import { createAgentKitIntegrityReporter } from "@agent-native/core/client/agentkit-chat/integrity";
6
10
  import { createAgentNativeAgentKitTransport } from "@agent-native/core/client/agentkit-chat/transport";
7
11
  import {
@@ -41,15 +45,21 @@ import {
41
45
  BuilderSetupCard,
42
46
  isMissingLlmProviderRunError,
43
47
  } from "@agent-native/toolkit/app/chat/chat/run-recovery";
44
- import { IconLayoutSidebarRight } from "@tabler/icons-react";
48
+ import type {
49
+ PromptComposerFile,
50
+ PromptComposerSubmitOptions,
51
+ Reference,
52
+ } from "@agent-native/toolkit/composer";
53
+ import { IconLayoutSidebarRight, IconX } from "@tabler/icons-react";
45
54
  import {
46
55
  useCallback,
47
56
  useEffect,
57
+ useMemo,
48
58
  useRef,
49
59
  useState,
50
60
  type ReactNode,
51
61
  } from "react";
52
- import { useNavigate, useParams } from "react-router";
62
+ import { useLocation, useNavigate, useParams } from "react-router";
53
63
  import { toast } from "sonner";
54
64
 
55
65
  import { Button } from "@/components/ui/button";
@@ -60,16 +70,31 @@ import {
60
70
  } from "@/components/ui/tooltip";
61
71
  import { APP_TITLE } from "@/lib/app-config";
62
72
  import { consumeChatHomeThreadId } from "@/lib/chat-home-thread";
73
+ import {
74
+ clearFailedChatHandoff,
75
+ chatThreadPath,
76
+ readFailedChatHandoff,
77
+ type ChatInitialComposerOptions,
78
+ type FailedChatHandoffClearResult,
79
+ type FailedChatHandoffEnvelope,
80
+ type FailedChatHandoffReadResult,
81
+ type FailedChatHandoffWriteResult,
82
+ initialComposerOptionsFromState,
83
+ initialMessageFromState,
84
+ writeFailedChatHandoff,
85
+ } from "@/lib/chat-paths";
63
86
  import { TAB_ID } from "@/lib/tab-id";
64
87
 
65
- function chatThreadPath(threadId: string | null) {
66
- return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/home";
67
- }
68
-
69
88
  // Module scope on purpose: CoreAgentKitRoot memoizes the client on its options, so
70
89
  // a new callback each render would rebuild the client and drop the stream.
71
90
  const reportStreamIntegrity = createAgentKitIntegrityReporter("chat");
72
91
 
92
+ type FailedInitialDraft = FailedChatHandoffEnvelope & {
93
+ attempt: number;
94
+ threadId: string;
95
+ unsaved: boolean;
96
+ };
97
+
73
98
  export default function ChatRouteContent({
74
99
  initialThreadId,
75
100
  }: {
@@ -101,6 +126,186 @@ function ChatThreadRouteContent({
101
126
  }) {
102
127
  const t = useT();
103
128
  const [workspaceOpen, setWorkspaceOpen] = useState(false);
129
+ const [handoffStorageRevision, setHandoffStorageRevision] = useState(0);
130
+ const failedHandoffStorageFailureReportedRef = useRef(new Set<string>());
131
+ const reportHandoffStorageFailure = useCallback(
132
+ (
133
+ result:
134
+ | FailedChatHandoffReadResult
135
+ | FailedChatHandoffWriteResult
136
+ | FailedChatHandoffClearResult,
137
+ message: string,
138
+ ) => {
139
+ if (result.status !== "invalid" && result.status !== "unavailable") {
140
+ return;
141
+ }
142
+ if (
143
+ failedHandoffStorageFailureReportedRef.current.has(resolvedThreadId)
144
+ ) {
145
+ return;
146
+ }
147
+ failedHandoffStorageFailureReportedRef.current.add(resolvedThreadId);
148
+ captureException(
149
+ result.status === "unavailable"
150
+ ? result.cause
151
+ : new Error(`${message}: ${result.reason}`),
152
+ { tags: { area: "chat_handoff_storage" } },
153
+ );
154
+ },
155
+ [resolvedThreadId],
156
+ );
157
+ const restoredHandoffResult = useMemo(
158
+ () => readFailedChatHandoff(resolvedThreadId),
159
+ [handoffStorageRevision, resolvedThreadId],
160
+ );
161
+ const restoredHandoff =
162
+ restoredHandoffResult.status === "found"
163
+ ? restoredHandoffResult.handoff
164
+ : null;
165
+ const [failedInitialDrafts, setFailedInitialDrafts] = useState<
166
+ Map<string, FailedInitialDraft>
167
+ >(() => {
168
+ return restoredHandoff
169
+ ? new Map([
170
+ [
171
+ resolvedThreadId,
172
+ {
173
+ text: restoredHandoff.text,
174
+ options: restoredHandoff.options,
175
+ attempt: 0,
176
+ threadId: resolvedThreadId,
177
+ unsaved: false,
178
+ },
179
+ ],
180
+ ])
181
+ : new Map();
182
+ });
183
+ const failedInitialDraftsRef = useRef(failedInitialDrafts);
184
+ const restoredForRoute = restoredHandoff;
185
+ const failedDraftForRoute =
186
+ failedInitialDrafts.get(resolvedThreadId) ??
187
+ (restoredForRoute
188
+ ? {
189
+ text: restoredForRoute.text,
190
+ options: restoredForRoute.options,
191
+ attempt: 0,
192
+ threadId: resolvedThreadId,
193
+ unsaved: false,
194
+ }
195
+ : null);
196
+ const failedInitialDraftRef = useRef(failedDraftForRoute);
197
+ failedInitialDraftRef.current = failedDraftForRoute;
198
+ const failedInitialDraftAttemptRef = useRef(0);
199
+ const setFailedInitialDraftForThread = useCallback(
200
+ (threadId: string, draft: FailedInitialDraft | null) => {
201
+ const next = new Map(failedInitialDraftsRef.current);
202
+ if (draft) next.set(threadId, draft);
203
+ else next.delete(threadId);
204
+ failedInitialDraftsRef.current = next;
205
+ setFailedInitialDrafts(next);
206
+ },
207
+ [],
208
+ );
209
+ useEffect(() => {
210
+ reportHandoffStorageFailure(
211
+ restoredHandoffResult,
212
+ "Could not restore the failed Chat handoff",
213
+ );
214
+ if (failedInitialDraftsRef.current.has(resolvedThreadId)) return;
215
+ if (!restoredHandoff) return;
216
+ setFailedInitialDraftForThread(resolvedThreadId, {
217
+ text: restoredHandoff.text,
218
+ options: restoredHandoff.options,
219
+ attempt: 0,
220
+ threadId: resolvedThreadId,
221
+ unsaved: false,
222
+ });
223
+ }, [
224
+ reportHandoffStorageFailure,
225
+ resolvedThreadId,
226
+ restoredHandoff,
227
+ restoredHandoffResult,
228
+ setFailedInitialDraftForThread,
229
+ ]);
230
+ const handleInitialMessageFailure = useCallback(
231
+ (
232
+ text: string,
233
+ options: ChatInitialComposerOptions,
234
+ persistence = writeFailedChatHandoff(resolvedThreadId, text, options),
235
+ ) => {
236
+ failedInitialDraftAttemptRef.current += 1;
237
+ reportHandoffStorageFailure(
238
+ persistence,
239
+ "Could not save the failed Chat handoff",
240
+ );
241
+ const failedDraft = {
242
+ text,
243
+ options,
244
+ attempt: failedInitialDraftAttemptRef.current,
245
+ threadId: resolvedThreadId,
246
+ unsaved: persistence.status !== "stored",
247
+ };
248
+ failedInitialDraftRef.current = failedDraft;
249
+ setFailedInitialDraftForThread(resolvedThreadId, failedDraft);
250
+ },
251
+ [
252
+ reportHandoffStorageFailure,
253
+ resolvedThreadId,
254
+ setFailedInitialDraftForThread,
255
+ ],
256
+ );
257
+ const handleInitialMessageAccepted = useCallback(() => {
258
+ reportHandoffStorageFailure(
259
+ clearFailedChatHandoff(resolvedThreadId),
260
+ "Could not clear the accepted Chat handoff",
261
+ );
262
+ if (failedInitialDraftsRef.current.has(resolvedThreadId)) {
263
+ if (failedInitialDraftRef.current?.threadId === resolvedThreadId) {
264
+ failedInitialDraftRef.current = null;
265
+ }
266
+ setFailedInitialDraftForThread(resolvedThreadId, null);
267
+ }
268
+ setHandoffStorageRevision((revision) => revision + 1);
269
+ }, [
270
+ reportHandoffStorageFailure,
271
+ resolvedThreadId,
272
+ setFailedInitialDraftForThread,
273
+ ]);
274
+ const updateFailedInitialDraft = useCallback(
275
+ (
276
+ update: Partial<FailedChatHandoffEnvelope> & {
277
+ options?: Partial<ChatInitialComposerOptions>;
278
+ },
279
+ ) => {
280
+ const current = failedInitialDraftRef.current;
281
+ if (current?.threadId !== resolvedThreadId) return;
282
+ const next = {
283
+ ...current,
284
+ ...update,
285
+ options: { ...current.options, ...update.options },
286
+ };
287
+ const persistence = writeFailedChatHandoff(
288
+ next.threadId,
289
+ next.text,
290
+ next.options,
291
+ );
292
+ const nextDraft = {
293
+ ...next,
294
+ unsaved: persistence.status !== "stored",
295
+ };
296
+ failedInitialDraftRef.current = nextDraft;
297
+ reportHandoffStorageFailure(
298
+ persistence,
299
+ "Could not save the failed Chat handoff",
300
+ );
301
+ setFailedInitialDraftForThread(resolvedThreadId, nextDraft);
302
+ },
303
+ [
304
+ reportHandoffStorageFailure,
305
+ resolvedThreadId,
306
+ setFailedInitialDraftForThread,
307
+ ],
308
+ );
104
309
 
105
310
  const [transport] = useState(() =>
106
311
  createAgentNativeAgentKitTransport({
@@ -148,10 +353,33 @@ function ChatThreadRouteContent({
148
353
  onThreadForked={(thread) => navigate(chatThreadPath(thread.id))}
149
354
  >
150
355
  <ChatLifecycleTracking threadId={resolvedThreadId} />
356
+ <ChatInitialMessage
357
+ threadId={resolvedThreadId}
358
+ hasRestoredDraft={failedDraftForRoute !== null}
359
+ onFailure={handleInitialMessageFailure}
360
+ onAccepted={handleInitialMessageAccepted}
361
+ />
151
362
  <ChatMcpConnectionResume />
152
363
  <ChatCanvas
153
364
  workspaceOpen={workspaceOpen}
154
365
  setWorkspaceOpen={setWorkspaceOpen}
366
+ initialText={
367
+ failedDraftForRoute ? failedDraftForRoute.text : undefined
368
+ }
369
+ initialTextKey={
370
+ failedDraftForRoute
371
+ ? `${resolvedThreadId}:${failedDraftForRoute.attempt}`
372
+ : undefined
373
+ }
374
+ recoveryOptions={
375
+ failedDraftForRoute ? failedDraftForRoute.options : undefined
376
+ }
377
+ recoveryDraftUnsaved={failedDraftForRoute?.unsaved ?? false}
378
+ onRecoveryOptionsChange={(options) =>
379
+ updateFailedInitialDraft({ options })
380
+ }
381
+ onRecoveryDraftChange={updateFailedInitialDraft}
382
+ onRecoverySubmitAccepted={handleInitialMessageAccepted}
155
383
  />
156
384
  </CoreAgentKitRoot>
157
385
  </CoreComposerRuntimeProvider>
@@ -193,6 +421,74 @@ type ChatRetryError = {
193
421
  runId: string;
194
422
  };
195
423
 
424
+ function RecoveryIncludedSources({
425
+ references,
426
+ attachments,
427
+ onRemoveReference,
428
+ onRemoveAttachment,
429
+ }: {
430
+ references: readonly Reference[];
431
+ attachments: readonly FilePart[];
432
+ onRemoveReference: (index: number) => void;
433
+ onRemoveAttachment: (index: number) => void;
434
+ }) {
435
+ const t = useT();
436
+ if (references.length === 0 && attachments.length === 0) return null;
437
+ return (
438
+ <div
439
+ className="flex max-w-full flex-wrap gap-1.5"
440
+ data-testid="recovery-included-sources"
441
+ >
442
+ {references.map((reference, index) => (
443
+ <span
444
+ key={`reference:${index}:${reference.path}`}
445
+ className="inline-flex min-w-0 items-center gap-1 rounded-md border border-border bg-muted/50 px-2 py-1 text-xs"
446
+ data-recovery-source="reference"
447
+ >
448
+ <span className="max-w-48 truncate">
449
+ {reference.name || reference.path}
450
+ </span>
451
+ <Button
452
+ type="button"
453
+ variant="ghost"
454
+ size="icon-xs"
455
+ className="-me-1"
456
+ aria-label={t("composer.removeReference", {
457
+ name: reference.name || reference.path,
458
+ })}
459
+ data-testid={`remove-recovery-reference-${index}`}
460
+ onClick={() => onRemoveReference(index)}
461
+ >
462
+ <IconX aria-hidden="true" className="size-3" />
463
+ </Button>
464
+ </span>
465
+ ))}
466
+ {attachments.map((attachment, index) => (
467
+ <span
468
+ key={`attachment:${index}:${attachment.name}`}
469
+ className="inline-flex min-w-0 items-center gap-1 rounded-md border border-border bg-muted/50 px-2 py-1 text-xs"
470
+ data-recovery-source="attachment"
471
+ >
472
+ <span className="max-w-48 truncate">{attachment.name}</span>
473
+ <Button
474
+ type="button"
475
+ variant="ghost"
476
+ size="icon-xs"
477
+ className="-me-1"
478
+ aria-label={t("composer.removeAttachment", {
479
+ name: attachment.name,
480
+ })}
481
+ data-testid={`remove-recovery-attachment-${index}`}
482
+ onClick={() => onRemoveAttachment(index)}
483
+ >
484
+ <IconX aria-hidden="true" className="size-3" />
485
+ </Button>
486
+ </span>
487
+ ))}
488
+ </div>
489
+ );
490
+ }
491
+
196
492
  function ChatRunFailure({
197
493
  error,
198
494
  runId,
@@ -210,6 +506,9 @@ function ChatRunFailure({
210
506
  custom?: {
211
507
  agentNativeRecoveryAction?: unknown;
212
508
  agentNativeRecoveryOfRunId?: unknown;
509
+ agentNativeQueueWhileRunning?: unknown;
510
+ agentNativeQueuedWhileRunActive?: unknown;
511
+ agentNativeInterruptActiveRun?: unknown;
213
512
  };
214
513
  }
215
514
  | undefined
@@ -220,6 +519,96 @@ function ChatRunFailure({
220
519
  return action !== "continue" && action !== "retry";
221
520
  });
222
521
  const originalRequest = userRequests[0];
522
+ const originalMetadata =
523
+ originalRequest?.metadata &&
524
+ typeof originalRequest.metadata === "object" &&
525
+ !Array.isArray(originalRequest.metadata)
526
+ ? (originalRequest.metadata as Record<string, unknown>)
527
+ : {};
528
+ const originalMode =
529
+ originalMetadata.mode === "act" || originalMetadata.mode === "plan"
530
+ ? originalMetadata.mode
531
+ : originalMetadata.requestMode === "act" ||
532
+ originalMetadata.requestMode === "plan"
533
+ ? originalMetadata.requestMode
534
+ : undefined;
535
+ const originalComposerOptions = initialComposerOptionsFromState({
536
+ initialComposerOptions: {
537
+ ...(typeof originalMetadata.engine === "string"
538
+ ? { engine: originalMetadata.engine }
539
+ : {}),
540
+ ...(typeof originalMetadata.model === "string"
541
+ ? { model: originalMetadata.model }
542
+ : {}),
543
+ ...(typeof originalMetadata.effort === "string"
544
+ ? { effort: originalMetadata.effort }
545
+ : {}),
546
+ ...(originalMode ? { mode: originalMode } : {}),
547
+ ...(Array.isArray(originalMetadata.references)
548
+ ? { references: originalMetadata.references }
549
+ : {}),
550
+ ...(Array.isArray(originalMetadata.contextItems)
551
+ ? { contextItems: originalMetadata.contextItems }
552
+ : {}),
553
+ },
554
+ });
555
+ const originalComposerSettings: ChatInitialComposerOptions =
556
+ originalComposerOptions.status === "valid"
557
+ ? originalComposerOptions.options
558
+ : { mode: "act" };
559
+ const originalCustomMetadata = originalRequest
560
+ ? recoveryMetadata(originalRequest)
561
+ : undefined;
562
+ const queueWhileRunning =
563
+ typeof originalCustomMetadata?.agentNativeQueueWhileRunning === "boolean"
564
+ ? originalCustomMetadata.agentNativeQueueWhileRunning
565
+ : originalComposerSettings.intent !== "immediate";
566
+ const queuedWhileRunActive =
567
+ typeof originalCustomMetadata?.agentNativeQueuedWhileRunActive === "boolean"
568
+ ? originalCustomMetadata.agentNativeQueuedWhileRunActive
569
+ : originalComposerSettings.intent === "queued";
570
+ const interruptActiveRun =
571
+ typeof originalCustomMetadata?.agentNativeInterruptActiveRun === "boolean"
572
+ ? originalCustomMetadata.agentNativeInterruptActiveRun
573
+ : originalComposerSettings.steer === true;
574
+ const originalReferences = originalComposerSettings.references ?? [];
575
+ const hasOriginalExecutionSettings = Boolean(
576
+ originalMode ||
577
+ originalComposerSettings.engine ||
578
+ originalComposerSettings.model ||
579
+ originalComposerSettings.effort,
580
+ );
581
+ const originalAttachments =
582
+ originalRequest?.parts?.filter((part) => part.type === "file") ?? [];
583
+ const [retryItems, setRetryItems] = useState<{
584
+ runId: string;
585
+ references: Reference[];
586
+ attachments: FilePart[];
587
+ } | null>(null);
588
+ const visibleRetryItems =
589
+ retryItems?.runId === runId
590
+ ? retryItems
591
+ : {
592
+ runId,
593
+ references: originalReferences,
594
+ attachments: originalAttachments,
595
+ };
596
+ const updateRetryItems = (update: {
597
+ references?: Reference[];
598
+ attachments?: FilePart[];
599
+ }) => {
600
+ setRetryError(null);
601
+ setRetryItems((current) => ({
602
+ ...(current?.runId === runId
603
+ ? current
604
+ : {
605
+ runId,
606
+ references: [...originalReferences],
607
+ attachments: originalAttachments,
608
+ }),
609
+ ...update,
610
+ }));
611
+ };
223
612
  const hasRetryForThisRun = thread.messages.some(
224
613
  (message) =>
225
614
  recoveryMetadata(message)?.agentNativeRecoveryAction === "retry" &&
@@ -227,8 +616,7 @@ function ChatRunFailure({
227
616
  );
228
617
  const retryFirstMessage = useCallback(async () => {
229
618
  if (retryStartedForRunsRef.current.has(runId)) return;
230
- const attachments =
231
- originalRequest?.parts.filter((part) => part.type === "file") ?? [];
619
+ const attachments = visibleRetryItems.attachments;
232
620
  if (attachments.some((part) => part.fileId && !part.url)) {
233
621
  setRetryError({ code: "attachment_id_unavailable", runId });
234
622
  return;
@@ -240,18 +628,59 @@ function ChatRunFailure({
240
628
  .filter((part) => part.type === "text")
241
629
  .map((part) => part.text)
242
630
  .join("\n") ?? "";
631
+ const retryMetadata = {
632
+ ...(originalComposerSettings.engine
633
+ ? { engine: originalComposerSettings.engine }
634
+ : {}),
635
+ ...(originalComposerSettings.model
636
+ ? { model: originalComposerSettings.model }
637
+ : {}),
638
+ ...(originalComposerSettings.effort
639
+ ? { effort: originalComposerSettings.effort }
640
+ : {}),
641
+ ...(visibleRetryItems.references.length
642
+ ? { references: visibleRetryItems.references }
643
+ : {}),
644
+ ...(originalComposerSettings.contextItems === undefined
645
+ ? {}
646
+ : { contextItems: originalComposerSettings.contextItems }),
647
+ ...(originalMode
648
+ ? { mode: originalMode, requestMode: originalMode }
649
+ : {}),
650
+ custom: {
651
+ agentNativeRecoveryAction: "retry",
652
+ agentNativeRecoveryOfRunId: runId,
653
+ agentNativeQueueWhileRunning: queueWhileRunning,
654
+ agentNativeQueuedWhileRunActive: queuedWhileRunActive,
655
+ agentNativeInterruptActiveRun: interruptActiveRun,
656
+ },
657
+ };
658
+ const retryRunOptions: AgentRunOptions = {
659
+ ...(originalComposerSettings.model
660
+ ? { model: originalComposerSettings.model }
661
+ : {}),
662
+ ...(originalMode ? { mode: originalMode } : {}),
663
+ ...(originalComposerSettings.effort &&
664
+ !["auto", "max"].includes(originalComposerSettings.effort)
665
+ ? {
666
+ reasoningEffort: originalComposerSettings.effort as NonNullable<
667
+ AgentRunOptions["reasoningEffort"]
668
+ >,
669
+ }
670
+ : {}),
671
+ metadata: retryMetadata,
672
+ };
243
673
  let send: unknown;
244
674
  try {
245
675
  send = controller.sendMessage({
246
676
  threadId,
247
677
  text: prompt || t("chat.retryPreviousRequest"),
248
678
  ...(attachments.length ? { attachments } : {}),
249
- metadata: {
250
- custom: {
251
- agentNativeRecoveryAction: "retry",
252
- agentNativeRecoveryOfRunId: runId,
253
- },
254
- },
679
+ ...(hasOriginalExecutionSettings ? { options: retryRunOptions } : {}),
680
+ metadata: retryMetadata,
681
+ queueWhileRunning,
682
+ queuedWhileRunActive,
683
+ ...(interruptActiveRun ? { interruptActiveRun: true } : {}),
255
684
  });
256
685
  } catch (error) {
257
686
  retryStartedForRunsRef.current.delete(runId);
@@ -262,7 +691,19 @@ function ChatRunFailure({
262
691
  retryStartedForRunsRef.current.delete(runId);
263
692
  captureException(error, { tags: { area: "chat_retry" } });
264
693
  });
265
- }, [controller, originalRequest, runId, t, threadId]);
694
+ }, [
695
+ controller,
696
+ originalComposerSettings,
697
+ queueWhileRunning,
698
+ queuedWhileRunActive,
699
+ interruptActiveRun,
700
+ originalMode,
701
+ originalRequest,
702
+ runId,
703
+ t,
704
+ threadId,
705
+ visibleRetryItems,
706
+ ]);
266
707
  const isFirstMessage = userRequests.length === 1 && !hasRetryForThisRun;
267
708
  if (
268
709
  isFirstMessage &&
@@ -279,6 +720,24 @@ function ChatRunFailure({
279
720
  layout="sidebar"
280
721
  onRetry={retryFirstMessage}
281
722
  />
723
+ <RecoveryIncludedSources
724
+ references={visibleRetryItems.references}
725
+ attachments={visibleRetryItems.attachments}
726
+ onRemoveReference={(index) =>
727
+ updateRetryItems({
728
+ references: visibleRetryItems.references.filter(
729
+ (_reference, candidate) => candidate !== index,
730
+ ),
731
+ })
732
+ }
733
+ onRemoveAttachment={(index) =>
734
+ updateRetryItems({
735
+ attachments: visibleRetryItems.attachments.filter(
736
+ (_attachment, candidate) => candidate !== index,
737
+ ),
738
+ })
739
+ }
740
+ />
282
741
  {retryError?.runId === runId &&
283
742
  retryError.code === "attachment_id_unavailable" ? (
284
743
  <p role="alert" className="mt-2 px-3 text-sm text-destructive">
@@ -291,6 +750,159 @@ function ChatRunFailure({
291
750
  return <AgentRunFailure error={error} runId={runId} threadId={threadId} />;
292
751
  }
293
752
 
753
+ function ChatInitialMessage({
754
+ threadId,
755
+ hasRestoredDraft,
756
+ onFailure,
757
+ onAccepted,
758
+ }: {
759
+ threadId: string;
760
+ hasRestoredDraft: boolean;
761
+ onFailure: (
762
+ text: string,
763
+ options: ChatInitialComposerOptions,
764
+ persistence?: FailedChatHandoffWriteResult,
765
+ ) => void;
766
+ onAccepted: () => void;
767
+ }) {
768
+ const location = useLocation();
769
+ const navigate = useNavigate();
770
+ const t = useT();
771
+ const control = useAgentKitControl(threadId);
772
+ const thread = useAgentThread(threadId);
773
+ const sentRef = useRef(false);
774
+ const message = initialMessageFromState(location.state);
775
+ const composerOptionsResult = initialComposerOptionsFromState(location.state);
776
+ const invalidComposerOptions = composerOptionsResult.status === "invalid";
777
+ const composerOptions =
778
+ composerOptionsResult.status === "valid"
779
+ ? composerOptionsResult.options
780
+ : undefined;
781
+ const engine = composerOptions?.engine;
782
+ const model = composerOptions?.model;
783
+ const effort = composerOptions?.effort;
784
+ const mode = composerOptions?.mode ?? "act";
785
+ const references = composerOptions?.references ?? [];
786
+ const contextItems = composerOptions?.contextItems;
787
+ const activeAtSubmit = hasActiveAgentRuns(thread);
788
+
789
+ useEffect(() => {
790
+ if (!message || sentRef.current) return;
791
+ if (hasRestoredDraft) {
792
+ sentRef.current = true;
793
+ return;
794
+ }
795
+ sentRef.current = true;
796
+ const clearRouteState = () =>
797
+ navigate(
798
+ { pathname: location.pathname, search: location.search },
799
+ { replace: true, state: null },
800
+ );
801
+ if (invalidComposerOptions) {
802
+ clearRouteState();
803
+ onFailure(message, {});
804
+ toast.error(t("chat.invalidHandoffOptions"));
805
+ captureException(new Error("Invalid options in initial Chat handoff"), {
806
+ tags: { area: "chat_initial_message" },
807
+ });
808
+ return;
809
+ }
810
+ const options = composerOptions ?? {};
811
+ const persistence = writeFailedChatHandoff(threadId, message, options);
812
+ if (persistence.status !== "stored") {
813
+ clearRouteState();
814
+ onFailure(message, options, persistence);
815
+ if (
816
+ persistence.status !== "invalid" ||
817
+ persistence.reason !== "payload-too-large"
818
+ ) {
819
+ return;
820
+ }
821
+ } else {
822
+ // Router state survives a reload, so drop it before sending or a refresh
823
+ // cannot distinguish a pending send from a failed one.
824
+ clearRouteState();
825
+ }
826
+ const context = [
827
+ composerOptions?.composerModeContext,
828
+ contextItems?.map((item) => item.context).join("\n\n"),
829
+ ]
830
+ .filter(Boolean)
831
+ .join("\n\n");
832
+ const requestText = context
833
+ ? appendAgentChatContextToMessage(message, context)
834
+ : message;
835
+ const queuedIntent =
836
+ composerOptions?.intent === "queued" ||
837
+ (composerOptions?.intent !== "immediate" && activeAtSubmit);
838
+ const metadata = {
839
+ ...(engine ? { engine } : {}),
840
+ ...(model ? { model } : {}),
841
+ ...(effort ? { effort } : {}),
842
+ ...(references.length ? { references } : {}),
843
+ ...(contextItems === undefined ? {} : { contextItems }),
844
+ mode,
845
+ requestMode: mode,
846
+ custom: {
847
+ agentNativeQueueWhileRunning: composerOptions?.intent !== "immediate",
848
+ agentNativeQueuedWhileRunActive: queuedIntent,
849
+ agentNativeInterruptActiveRun: composerOptions?.steer === true,
850
+ },
851
+ };
852
+ const runOptions: AgentRunOptions = {
853
+ ...(model ? { model } : {}),
854
+ mode,
855
+ ...(effort && !["auto", "max"].includes(effort)
856
+ ? {
857
+ reasoningEffort: effort as NonNullable<
858
+ AgentRunOptions["reasoningEffort"]
859
+ >,
860
+ }
861
+ : {}),
862
+ metadata,
863
+ };
864
+ void Promise.resolve(
865
+ control.sendMessage({
866
+ text: requestText,
867
+ attachments: [...(composerOptions?.uploadedAttachments ?? [])],
868
+ options: runOptions,
869
+ metadata,
870
+ queueWhileRunning: metadata.custom.agentNativeQueueWhileRunning,
871
+ queuedWhileRunActive: queuedIntent,
872
+ ...(metadata.custom.agentNativeInterruptActiveRun
873
+ ? { interruptActiveRun: true }
874
+ : {}),
875
+ }),
876
+ )
877
+ .then(onAccepted)
878
+ .catch((error: unknown) => {
879
+ onFailure(message, composerOptions ?? {});
880
+ captureException(error, { tags: { area: "chat_initial_message" } });
881
+ toast.error(error instanceof Error ? error.message : String(error));
882
+ });
883
+ }, [
884
+ composerOptions,
885
+ control,
886
+ contextItems,
887
+ activeAtSubmit,
888
+ hasRestoredDraft,
889
+ invalidComposerOptions,
890
+ engine,
891
+ effort,
892
+ location.pathname,
893
+ location.search,
894
+ message,
895
+ model,
896
+ navigate,
897
+ onAccepted,
898
+ onFailure,
899
+ t,
900
+ threadId,
901
+ ]);
902
+
903
+ return null;
904
+ }
905
+
294
906
  function ChatLifecycleTracking({ threadId }: { threadId: string }) {
295
907
  const thread = useAgentThread(threadId);
296
908
  const lifecycleRef = useRef({
@@ -520,17 +1132,160 @@ function ChatEmptyState() {
520
1132
  function ChatCanvas({
521
1133
  workspaceOpen,
522
1134
  setWorkspaceOpen,
1135
+ initialText,
1136
+ initialTextKey,
1137
+ recoveryOptions,
1138
+ onRecoveryOptionsChange,
1139
+ onRecoveryDraftChange,
1140
+ onRecoverySubmitAccepted,
1141
+ recoveryDraftUnsaved,
523
1142
  }: {
524
1143
  workspaceOpen: boolean;
525
1144
  setWorkspaceOpen: (value: boolean | ((current: boolean) => boolean)) => void;
1145
+ initialText?: string;
1146
+ initialTextKey?: string;
1147
+ recoveryOptions?: ChatInitialComposerOptions;
1148
+ recoveryDraftUnsaved: boolean;
1149
+ onRecoveryOptionsChange: (
1150
+ options: Partial<ChatInitialComposerOptions>,
1151
+ ) => void;
1152
+ onRecoveryDraftChange: (
1153
+ update: Partial<FailedChatHandoffEnvelope> & {
1154
+ options?: Partial<ChatInitialComposerOptions>;
1155
+ },
1156
+ ) => void;
1157
+ onRecoverySubmitAccepted: () => void;
526
1158
  }) {
527
1159
  const t = useT();
1160
+ const { threadId } = useAgentKit();
528
1161
  const thread = useAgentThread();
1162
+ const control = useAgentKitControl(threadId);
529
1163
  const stopButton = useAgentKitStopButton({
530
1164
  label: t("agentChat.composer.stopResponse"), // i18n-key-ignore shared framework catalog
531
1165
  onError: (error) => toast.error(error.message),
532
1166
  });
533
1167
  const hasConversation = thread.messages.length > 0;
1168
+ const activeAtSubmit = hasActiveAgentRuns(thread);
1169
+
1170
+ const retryInitialDraft = useCallback(
1171
+ async (
1172
+ text: string,
1173
+ files: PromptComposerFile[],
1174
+ references: Reference[],
1175
+ options: PromptComposerSubmitOptions,
1176
+ ) => {
1177
+ if (!recoveryOptions) return;
1178
+ const contextItems = options.contextItems ?? recoveryOptions.contextItems;
1179
+ const composerModeContext =
1180
+ options.composerModeContext ?? recoveryOptions.composerModeContext;
1181
+ const context = [
1182
+ composerModeContext,
1183
+ contextItems?.map((item) => item.context).join("\n\n"),
1184
+ ]
1185
+ .filter(Boolean)
1186
+ .join("\n\n");
1187
+ const requestText = context
1188
+ ? appendAgentChatContextToMessage(text, context)
1189
+ : text;
1190
+ const runMode = recoveryOptions.mode ?? "act";
1191
+ const model = options.model ?? recoveryOptions.model;
1192
+ const engine = options.engine ?? recoveryOptions.engine;
1193
+ const effort = options.effort ?? recoveryOptions.effort;
1194
+ const mergedReferences = [
1195
+ ...(recoveryOptions.references ?? []),
1196
+ ...references,
1197
+ ].filter(
1198
+ (reference, index, all) =>
1199
+ all.findIndex(
1200
+ (candidate) =>
1201
+ candidate.type === reference.type &&
1202
+ candidate.source === reference.source &&
1203
+ candidate.path === reference.path &&
1204
+ candidate.refId === reference.refId,
1205
+ ) === index,
1206
+ );
1207
+ const retryIntent = options.intent ?? recoveryOptions.intent;
1208
+ const queuedIntent =
1209
+ retryIntent === "queued" ||
1210
+ (retryIntent === undefined && activeAtSubmit);
1211
+ const retrySteer = options.steer ?? recoveryOptions.steer;
1212
+ const retryOptions: ChatInitialComposerOptions = {
1213
+ ...recoveryOptions,
1214
+ mode: runMode,
1215
+ ...(model ? { model } : {}),
1216
+ ...(engine ? { engine } : {}),
1217
+ ...(effort ? { effort } : {}),
1218
+ ...(retryIntent === undefined ? {} : { intent: retryIntent }),
1219
+ ...(retrySteer === undefined ? {} : { steer: retrySteer }),
1220
+ ...(contextItems === undefined ? {} : { contextItems }),
1221
+ ...(composerModeContext === undefined ? {} : { composerModeContext }),
1222
+ references: mergedReferences,
1223
+ uploadedAttachments: recoveryOptions.uploadedAttachments ?? [],
1224
+ };
1225
+ onRecoveryDraftChange({ text, options: retryOptions });
1226
+ const uploadedAttachments = files.length
1227
+ ? await control.uploadFiles(
1228
+ files.map((file) => ({
1229
+ name: file.name,
1230
+ mediaType: file.type || "application/octet-stream",
1231
+ size: file.size,
1232
+ body: file,
1233
+ })),
1234
+ )
1235
+ : [];
1236
+ const acceptedOptions = {
1237
+ ...retryOptions,
1238
+ uploadedAttachments: [
1239
+ ...(recoveryOptions.uploadedAttachments ?? []),
1240
+ ...uploadedAttachments,
1241
+ ],
1242
+ };
1243
+ onRecoveryDraftChange({ text, options: acceptedOptions });
1244
+ const metadata = {
1245
+ ...(engine ? { engine } : {}),
1246
+ ...(model ? { model } : {}),
1247
+ ...(effort ? { effort } : {}),
1248
+ ...(mergedReferences.length ? { references: mergedReferences } : {}),
1249
+ ...(contextItems === undefined ? {} : { contextItems }),
1250
+ mode: runMode,
1251
+ requestMode: runMode,
1252
+ };
1253
+ const runOptions: AgentRunOptions = {
1254
+ ...(model ? { model } : {}),
1255
+ mode: runMode,
1256
+ ...(effort && !["auto", "max"].includes(effort)
1257
+ ? {
1258
+ reasoningEffort: effort as NonNullable<
1259
+ AgentRunOptions["reasoningEffort"]
1260
+ >,
1261
+ }
1262
+ : {}),
1263
+ metadata,
1264
+ };
1265
+ await control.sendMessage({
1266
+ text: requestText,
1267
+ attachments: [
1268
+ ...(recoveryOptions.uploadedAttachments ?? []),
1269
+ ...uploadedAttachments,
1270
+ ],
1271
+ options: runOptions,
1272
+ metadata,
1273
+ queueWhileRunning: retryIntent !== "immediate",
1274
+ queuedWhileRunActive: queuedIntent,
1275
+ ...(retrySteer ? { interruptActiveRun: true } : {}),
1276
+ onLocalSubmit: options.onLocalSubmit,
1277
+ });
1278
+ onRecoverySubmitAccepted();
1279
+ },
1280
+ [
1281
+ activeAtSubmit,
1282
+ control,
1283
+ threadId,
1284
+ onRecoveryDraftChange,
1285
+ onRecoverySubmitAccepted,
1286
+ recoveryOptions,
1287
+ ],
1288
+ );
534
1289
 
535
1290
  useEffect(() => {
536
1291
  if (!hasConversation) setWorkspaceOpen(false);
@@ -563,6 +1318,64 @@ function ChatCanvas({
563
1318
  emptyComposerPlacement="center"
564
1319
  composerProps={{
565
1320
  requireAgentEngine: true,
1321
+ initialText,
1322
+ initialTextKey,
1323
+ ...(recoveryOptions
1324
+ ? {
1325
+ mode: recoveryOptions.mode ?? "act",
1326
+ selectedModel: recoveryOptions.model,
1327
+ selectedEngine: recoveryOptions.engine,
1328
+ selectedEffort: recoveryOptions.effort,
1329
+ contextItems: recoveryOptions.contextItems,
1330
+ onModeChange: (mode: "act" | "plan") =>
1331
+ onRecoveryOptionsChange({ mode }),
1332
+ onModelChange: (model: string, engine: string) =>
1333
+ onRecoveryOptionsChange({ model, engine }),
1334
+ onEffortChange: (
1335
+ effort: NonNullable<ChatInitialComposerOptions["effort"]>,
1336
+ ) => onRecoveryOptionsChange({ effort }),
1337
+ onRemoveContextItem: (key: string) =>
1338
+ onRecoveryOptionsChange({
1339
+ contextItems: recoveryOptions.contextItems?.filter(
1340
+ (item) => item.key !== key,
1341
+ ),
1342
+ }),
1343
+ onTextChange: (text: string) => onRecoveryDraftChange({ text }),
1344
+ toolbarSlot: (
1345
+ <>
1346
+ {recoveryDraftUnsaved ? (
1347
+ <p
1348
+ className="px-2 text-xs text-destructive"
1349
+ data-testid="chat-recovery-draft-unsaved"
1350
+ role="status"
1351
+ >
1352
+ {t("chat.recoveryDraftUnsaved")}
1353
+ </p>
1354
+ ) : null}
1355
+ <RecoveryIncludedSources
1356
+ references={recoveryOptions.references ?? []}
1357
+ attachments={recoveryOptions.uploadedAttachments ?? []}
1358
+ onRemoveReference={(index) =>
1359
+ onRecoveryOptionsChange({
1360
+ references: recoveryOptions.references?.filter(
1361
+ (_reference, candidate) => candidate !== index,
1362
+ ),
1363
+ })
1364
+ }
1365
+ onRemoveAttachment={(index) =>
1366
+ onRecoveryOptionsChange({
1367
+ uploadedAttachments:
1368
+ recoveryOptions.uploadedAttachments?.filter(
1369
+ (_attachment, candidate) => candidate !== index,
1370
+ ),
1371
+ })
1372
+ }
1373
+ />
1374
+ </>
1375
+ ),
1376
+ onSubmit: retryInitialDraft,
1377
+ }
1378
+ : {}),
566
1379
  stopButton,
567
1380
  queueWhileRunning: true,
568
1381
  autoFocus: true,