@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
@@ -0,0 +1,479 @@
1
+ import type { FilePart } from "@agent-native/agentkit";
2
+ import {
3
+ COMPOSER_CONTEXT_MAX_BYTES,
4
+ COMPOSER_CONTEXT_MAX_ITEMS,
5
+ type PromptComposerSubmitOptions,
6
+ type Reference,
7
+ } from "@agent-native/toolkit/composer";
8
+
9
+ export const HOME_PATH = "/home";
10
+ export const NEW_CHAT_PATH = "/chat";
11
+
12
+ const FAILED_CHAT_HANDOFF_PREFIX = "agent-native.chat.failed-handoff:";
13
+ const MAX_FAILED_HANDOFF_BYTES = COMPOSER_CONTEXT_MAX_BYTES * 2 + 32 * 1024;
14
+ const MAX_HANDOFF_FIELD_LENGTH = 8 * 1024;
15
+ const MAX_HANDOFF_ITEMS = 20;
16
+ const INVALID_JSON_VALUE = Symbol("invalid-json-value");
17
+ const HANDOFF_COMPOSER_OPTION_KEYS = new Set([
18
+ "mode",
19
+ "engine",
20
+ "model",
21
+ "effort",
22
+ "intent",
23
+ "steer",
24
+ "composerModeContext",
25
+ "references",
26
+ "uploadedAttachments",
27
+ "contextItems",
28
+ ]);
29
+
30
+ export function chatThreadPath(threadId: string | null | undefined): string {
31
+ return threadId
32
+ ? `${NEW_CHAT_PATH}/${encodeURIComponent(threadId)}`
33
+ : NEW_CHAT_PATH;
34
+ }
35
+
36
+ export function isChatPathname(pathname: string): boolean {
37
+ return pathname === NEW_CHAT_PATH || pathname.startsWith(`${NEW_CHAT_PATH}/`);
38
+ }
39
+
40
+ export function threadIdFromPath(pathname: string): string | null {
41
+ const match = pathname.match(/^\/chat\/([^/]+)/);
42
+ if (!match) return null;
43
+ try {
44
+ const value = decodeURIComponent(match[1]).trim();
45
+ return value || null;
46
+ // coercion-ok: a malformed percent-encoding is not a thread id, so the path has no thread.
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ export type ChatInitialComposerOptions = Omit<
53
+ PromptComposerSubmitOptions,
54
+ "onLocalSubmit" | "attachments"
55
+ > & {
56
+ mode?: "act" | "plan";
57
+ references?: readonly Reference[];
58
+ uploadedAttachments?: readonly FilePart[];
59
+ };
60
+
61
+ /** Router state that carries a prompt typed elsewhere into the chat it opens. */
62
+ export interface ChatRouteState {
63
+ initialMessage: string;
64
+ initialComposerOptions?: ChatInitialComposerOptions;
65
+ }
66
+
67
+ export interface FailedChatHandoffEnvelope {
68
+ text: string;
69
+ options: ChatInitialComposerOptions;
70
+ }
71
+
72
+ export type FailedChatHandoffReadResult =
73
+ | { status: "found"; handoff: FailedChatHandoffEnvelope }
74
+ | { status: "absent" }
75
+ | {
76
+ status: "invalid";
77
+ reason:
78
+ | "invalid-thread-id"
79
+ | "too-large"
80
+ | "invalid-json"
81
+ | "invalid-envelope";
82
+ cause?: unknown;
83
+ }
84
+ | { status: "unavailable"; cause: unknown };
85
+
86
+ export type FailedChatHandoffWriteResult =
87
+ | { status: "stored" }
88
+ | {
89
+ status: "invalid";
90
+ reason: "invalid-thread-id" | "invalid-options" | "payload-too-large";
91
+ }
92
+ | { status: "unavailable"; cause: unknown };
93
+
94
+ export type FailedChatHandoffClearResult =
95
+ | { status: "cleared" }
96
+ | { status: "absent" }
97
+ | { status: "unavailable"; cause: unknown };
98
+
99
+ function isRecord(value: unknown): value is Record<string, unknown> {
100
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
101
+ }
102
+
103
+ function normalizeJsonValue(
104
+ value: unknown,
105
+ depth = 0,
106
+ ): unknown | typeof INVALID_JSON_VALUE {
107
+ if (value === null || typeof value === "boolean") return value;
108
+ if (typeof value === "string") {
109
+ return value.length <= MAX_HANDOFF_FIELD_LENGTH
110
+ ? value
111
+ : INVALID_JSON_VALUE;
112
+ }
113
+ if (typeof value === "number") {
114
+ return Number.isFinite(value) ? value : INVALID_JSON_VALUE;
115
+ }
116
+ if (depth >= 5) return INVALID_JSON_VALUE;
117
+ if (Array.isArray(value)) {
118
+ if (value.length > MAX_HANDOFF_ITEMS) return INVALID_JSON_VALUE;
119
+ const normalized = value.map((item) => normalizeJsonValue(item, depth + 1));
120
+ return normalized.some((item) => item === INVALID_JSON_VALUE)
121
+ ? INVALID_JSON_VALUE
122
+ : normalized;
123
+ }
124
+ if (!isRecord(value)) return INVALID_JSON_VALUE;
125
+ const prototype = Object.getPrototypeOf(value);
126
+ if (prototype !== Object.prototype && prototype !== null) {
127
+ return INVALID_JSON_VALUE;
128
+ }
129
+ const entries = Object.entries(value);
130
+ if (entries.length > MAX_HANDOFF_ITEMS) return INVALID_JSON_VALUE;
131
+ const normalized: Record<string, unknown> = {};
132
+ for (const [key, entry] of entries) {
133
+ if (key.length > 128) return INVALID_JSON_VALUE;
134
+ const normalizedEntry = normalizeJsonValue(entry, depth + 1);
135
+ if (normalizedEntry === INVALID_JSON_VALUE) return INVALID_JSON_VALUE;
136
+ normalized[key] = normalizedEntry;
137
+ }
138
+ return normalized;
139
+ }
140
+
141
+ function copyOptionalString(
142
+ source: Record<string, unknown>,
143
+ key: string,
144
+ target: Record<string, unknown>,
145
+ maxLength = MAX_HANDOFF_FIELD_LENGTH,
146
+ ): boolean {
147
+ if (!(key in source)) return true;
148
+ const value = source[key];
149
+ if (value === undefined || value === null) return true;
150
+ if (typeof value !== "string" || value.length > maxLength) return false;
151
+ target[key] = value;
152
+ return true;
153
+ }
154
+
155
+ function normalizeReference(value: unknown): Reference | null {
156
+ if (!isRecord(value)) return null;
157
+ const types = ["file", "skill", "mention", "agent", "custom-agent"];
158
+ if (!types.includes(String(value.type))) return null;
159
+ const result: Record<string, unknown> = { type: value.type };
160
+ for (const key of ["path", "name", "source"] as const) {
161
+ if (typeof value[key] !== "string" || value[key].length > 4096) {
162
+ return null;
163
+ }
164
+ result[key] = value[key];
165
+ }
166
+ for (const key of ["refType", "refId", "slotKey", "slotLabel"]) {
167
+ if (!copyOptionalString(value, key, result, 4096)) return null;
168
+ }
169
+ if (
170
+ "metadata" in value &&
171
+ value.metadata !== undefined &&
172
+ value.metadata !== null
173
+ ) {
174
+ const metadata = normalizeJsonValue(value.metadata);
175
+ if (!isRecord(metadata)) return null;
176
+ result.metadata = metadata;
177
+ }
178
+ return result as unknown as Reference;
179
+ }
180
+
181
+ function normalizeFilePart(value: unknown): FilePart | null {
182
+ if (!isRecord(value) || value.type !== "file") return null;
183
+ if (typeof value.name !== "string" || value.name.length > 1024) return null;
184
+ const result: Record<string, unknown> = { type: "file", name: value.name };
185
+ if (!copyOptionalString(value, "mediaType", result, 512)) return null;
186
+ if (!copyOptionalString(value, "fileId", result, 1024)) return null;
187
+ if (!copyOptionalString(value, "url", result, 4096)) return null;
188
+ if (typeof result.url === "string" && /^\s*data:/i.test(result.url)) {
189
+ return null;
190
+ }
191
+ if ("omitted" in value) {
192
+ if (value.omitted !== "inline-bytes" && value.omitted !== "unsafe-url") {
193
+ return null;
194
+ }
195
+ result.omitted = value.omitted;
196
+ }
197
+ return result as unknown as FilePart;
198
+ }
199
+
200
+ function normalizeContextItems(
201
+ value: unknown,
202
+ ): NonNullable<ChatInitialComposerOptions["contextItems"]> | null {
203
+ if (!Array.isArray(value) || value.length > COMPOSER_CONTEXT_MAX_ITEMS) {
204
+ return null;
205
+ }
206
+ const result: NonNullable<
207
+ ChatInitialComposerOptions["contextItems"]
208
+ >[number][] = [];
209
+ for (const item of value) {
210
+ if (!isRecord(item)) return null;
211
+ const normalized: Record<string, unknown> = {};
212
+ for (const key of ["key", "title", "context"] as const) {
213
+ if (typeof item[key] !== "string") return null;
214
+ normalized[key] = item[key];
215
+ }
216
+ for (const key of ["contextNamespace", "targetThreadId"]) {
217
+ if (!copyOptionalString(item, key, normalized, 4096)) return null;
218
+ }
219
+ result.push(
220
+ normalized as NonNullable<
221
+ ChatInitialComposerOptions["contextItems"]
222
+ >[number],
223
+ );
224
+ }
225
+ if (
226
+ new TextEncoder().encode(JSON.stringify(result)).byteLength >
227
+ COMPOSER_CONTEXT_MAX_BYTES
228
+ ) {
229
+ return null;
230
+ }
231
+ return result;
232
+ }
233
+
234
+ function normalizeComposerOptions(
235
+ value: unknown,
236
+ ): ChatInitialComposerOptions | null {
237
+ if (!isRecord(value)) return null;
238
+ if (
239
+ Object.keys(value).some((key) => !HANDOFF_COMPOSER_OPTION_KEYS.has(key))
240
+ ) {
241
+ return null;
242
+ }
243
+ if ("mode" in value && value.mode !== "act" && value.mode !== "plan") {
244
+ return null;
245
+ }
246
+ const result: Record<string, unknown> = {
247
+ mode: value.mode === "plan" ? "plan" : "act",
248
+ };
249
+ for (const key of ["engine", "model"] as const) {
250
+ if (!copyOptionalString(value, key, result, 512)) return null;
251
+ }
252
+ if ("effort" in value) {
253
+ if (
254
+ ![
255
+ "auto",
256
+ "none",
257
+ "minimal",
258
+ "low",
259
+ "medium",
260
+ "high",
261
+ "xhigh",
262
+ "max",
263
+ ].includes(String(value.effort))
264
+ ) {
265
+ return null;
266
+ }
267
+ result.effort = value.effort;
268
+ }
269
+ if ("intent" in value) {
270
+ if (value.intent !== "immediate" && value.intent !== "queued") return null;
271
+ result.intent = value.intent;
272
+ }
273
+ if ("steer" in value) {
274
+ if (typeof value.steer !== "boolean") return null;
275
+ result.steer = value.steer;
276
+ }
277
+ if ("composerModeContext" in value) {
278
+ if (!copyOptionalString(value, "composerModeContext", result, 16 * 1024)) {
279
+ return null;
280
+ }
281
+ }
282
+ if ("references" in value) {
283
+ if (
284
+ !Array.isArray(value.references) ||
285
+ value.references.length > MAX_HANDOFF_ITEMS
286
+ ) {
287
+ return null;
288
+ }
289
+ const references = value.references.map(normalizeReference);
290
+ if (references.some((reference) => !reference)) return null;
291
+ result.references = references;
292
+ }
293
+ if ("uploadedAttachments" in value) {
294
+ if (
295
+ !Array.isArray(value.uploadedAttachments) ||
296
+ value.uploadedAttachments.length > MAX_HANDOFF_ITEMS
297
+ ) {
298
+ return null;
299
+ }
300
+ const attachments = value.uploadedAttachments.map(normalizeFilePart);
301
+ if (attachments.some((attachment) => !attachment)) return null;
302
+ result.uploadedAttachments = attachments;
303
+ }
304
+ if ("contextItems" in value) {
305
+ const contextItems = normalizeContextItems(value.contextItems);
306
+ if (!contextItems) return null;
307
+ result.contextItems = contextItems;
308
+ }
309
+ return result as ChatInitialComposerOptions;
310
+ }
311
+
312
+ function failedChatHandoffKey(threadId: string): string {
313
+ return `${FAILED_CHAT_HANDOFF_PREFIX}${encodeURIComponent(threadId)}`;
314
+ }
315
+
316
+ function clearFailedChatHandoffMarker(threadId: string): string {
317
+ return JSON.stringify({ version: 1, threadId, status: "cleared" });
318
+ }
319
+
320
+ function replaceFailedChatHandoffWithMarker(
321
+ threadId: string,
322
+ ): { status: "stored" } | { status: "unavailable"; cause: unknown } {
323
+ try {
324
+ window.sessionStorage.setItem(
325
+ failedChatHandoffKey(threadId),
326
+ clearFailedChatHandoffMarker(threadId),
327
+ );
328
+ return { status: "stored" };
329
+ } catch (cause) {
330
+ return { status: "unavailable", cause };
331
+ }
332
+ }
333
+
334
+ function invalidateExistingFailedChatHandoff(threadId: string): void {
335
+ if (typeof window === "undefined") return;
336
+ clearFailedChatHandoff(threadId);
337
+ }
338
+
339
+ /** Persist a bounded, JSON-safe Home-to-Chat submit until retry is accepted. */
340
+ export function writeFailedChatHandoff(
341
+ threadId: string,
342
+ text: string,
343
+ options: ChatInitialComposerOptions,
344
+ ): FailedChatHandoffWriteResult {
345
+ if (typeof window === "undefined") {
346
+ return {
347
+ status: "unavailable",
348
+ cause: new Error("Chat handoff storage is only available in a browser"),
349
+ };
350
+ }
351
+ if (!threadId.trim() || threadId.length > 512) {
352
+ return { status: "invalid", reason: "invalid-thread-id" };
353
+ }
354
+ const normalizedOptions = normalizeComposerOptions(options);
355
+ if (!normalizedOptions) {
356
+ invalidateExistingFailedChatHandoff(threadId);
357
+ return { status: "invalid", reason: "invalid-options" };
358
+ }
359
+ const serialized = JSON.stringify({
360
+ version: 1,
361
+ threadId,
362
+ text,
363
+ options: normalizedOptions,
364
+ });
365
+ if (
366
+ new TextEncoder().encode(serialized).byteLength > MAX_FAILED_HANDOFF_BYTES
367
+ ) {
368
+ invalidateExistingFailedChatHandoff(threadId);
369
+ return { status: "invalid", reason: "payload-too-large" };
370
+ }
371
+ try {
372
+ window.sessionStorage.setItem(failedChatHandoffKey(threadId), serialized);
373
+ return { status: "stored" };
374
+ } catch (cause) {
375
+ invalidateExistingFailedChatHandoff(threadId);
376
+ return { status: "unavailable", cause };
377
+ }
378
+ }
379
+
380
+ export function readFailedChatHandoff(
381
+ threadId: string,
382
+ ): FailedChatHandoffReadResult {
383
+ if (typeof window === "undefined") return { status: "absent" };
384
+ if (!threadId.trim()) {
385
+ return { status: "invalid", reason: "invalid-thread-id" };
386
+ }
387
+ try {
388
+ const serialized = window.sessionStorage.getItem(
389
+ failedChatHandoffKey(threadId),
390
+ );
391
+ if (serialized === null) return { status: "absent" };
392
+ if (
393
+ new TextEncoder().encode(serialized).byteLength > MAX_FAILED_HANDOFF_BYTES
394
+ ) {
395
+ return { status: "invalid", reason: "too-large" };
396
+ }
397
+ let envelope: unknown;
398
+ try {
399
+ envelope = JSON.parse(serialized);
400
+ } catch (cause) {
401
+ return { status: "invalid", reason: "invalid-json", cause };
402
+ }
403
+ if (!isRecord(envelope) || envelope.version !== 1) {
404
+ return { status: "invalid", reason: "invalid-envelope" };
405
+ }
406
+ if (envelope.status === "cleared" && envelope.threadId === threadId) {
407
+ return { status: "absent" };
408
+ }
409
+ if (envelope.threadId !== threadId || typeof envelope.text !== "string") {
410
+ return { status: "invalid", reason: "invalid-envelope" };
411
+ }
412
+ const options = normalizeComposerOptions(envelope.options);
413
+ if (!options) return { status: "invalid", reason: "invalid-envelope" };
414
+ return { status: "found", handoff: { text: envelope.text, options } };
415
+ } catch (cause) {
416
+ return { status: "unavailable", cause };
417
+ }
418
+ }
419
+
420
+ export function clearFailedChatHandoff(
421
+ threadId: string,
422
+ ): FailedChatHandoffClearResult {
423
+ if (typeof window === "undefined") {
424
+ return {
425
+ status: "unavailable",
426
+ cause: new Error("Chat handoff storage is only available in a browser"),
427
+ };
428
+ }
429
+ if (!threadId.trim()) return { status: "absent" };
430
+ const key = failedChatHandoffKey(threadId);
431
+ try {
432
+ if (window.sessionStorage.getItem(key) === null) {
433
+ return { status: "absent" };
434
+ }
435
+ } catch (cause) {
436
+ const marker = replaceFailedChatHandoffWithMarker(threadId);
437
+ if (marker.status === "stored") {
438
+ return { status: "cleared" };
439
+ }
440
+ try {
441
+ window.sessionStorage.removeItem(key);
442
+ return { status: "cleared" };
443
+ } catch (removeCause) {
444
+ return { status: "unavailable", cause: removeCause ?? cause };
445
+ }
446
+ }
447
+ try {
448
+ window.sessionStorage.removeItem(key);
449
+ return { status: "cleared" };
450
+ } catch (cause) {
451
+ const marker = replaceFailedChatHandoffWithMarker(threadId);
452
+ if (marker.status === "stored") {
453
+ return { status: "cleared" };
454
+ }
455
+ return { status: "unavailable", cause };
456
+ }
457
+ }
458
+
459
+ export function initialMessageFromState(state: unknown): string | null {
460
+ if (!state || typeof state !== "object") return null;
461
+ const message = (state as Partial<ChatRouteState>).initialMessage;
462
+ return typeof message === "string" && message.trim() ? message : null;
463
+ }
464
+
465
+ export function initialComposerOptionsFromState(
466
+ state: unknown,
467
+ ): InitialComposerOptionsReadResult {
468
+ if (!isRecord(state) || !("initialComposerOptions" in state)) {
469
+ return { status: "absent" };
470
+ }
471
+ if (state.initialComposerOptions === undefined) return { status: "absent" };
472
+ const options = normalizeComposerOptions(state.initialComposerOptions);
473
+ return options ? { status: "valid", options } : { status: "invalid" };
474
+ }
475
+
476
+ export type InitialComposerOptionsReadResult =
477
+ | { status: "absent" }
478
+ | { status: "valid"; options: ChatInitialComposerOptions }
479
+ | { status: "invalid" };
@@ -26,6 +26,12 @@ import { Layout as AppLayout } from "@/components/layout/Layout";
26
26
  import { AppToolkitProvider } from "@/components/ui/toolkit-provider";
27
27
  import { useNavigationState } from "@/hooks/use-navigation-state";
28
28
  import { APP_TITLE } from "@/lib/app-config";
29
+ import {
30
+ HOME_PATH,
31
+ isChatPathname,
32
+ NEW_CHAT_PATH,
33
+ threadIdFromPath,
34
+ } from "@/lib/chat-paths";
29
35
  import { TAB_ID } from "@/lib/tab-id";
30
36
 
31
37
  import { i18nCatalog } from "./i18n";
@@ -115,7 +121,7 @@ function AppContent() {
115
121
  const navigate = useNavigate();
116
122
  const t = useT();
117
123
  const location = useLocation();
118
- const isChatThread = location.pathname.startsWith("/chat/");
124
+ const isChatThread = threadIdFromPath(location.pathname) !== null;
119
125
  useCommandMenuShortcut(useCallback(() => setCmdkOpen(true), []));
120
126
  return (
121
127
  <>
@@ -134,8 +140,13 @@ function AppContent() {
134
140
  {t("chat.newChat")}
135
141
  </CommandMenu.Item>
136
142
  ) : null}
137
- {!isChatThread && location.pathname !== "/home" ? (
138
- <CommandMenu.Item onSelect={() => navigate("/home")}>
143
+ {location.pathname !== HOME_PATH ? (
144
+ <CommandMenu.Item onSelect={() => navigate(HOME_PATH)}>
145
+ {t("navigation.home")}
146
+ </CommandMenu.Item>
147
+ ) : null}
148
+ {!isChatPathname(location.pathname) ? (
149
+ <CommandMenu.Item onSelect={() => navigate(NEW_CHAT_PATH)}>
139
150
  {t("navigation.chat")}
140
151
  </CommandMenu.Item>
141
152
  ) : null}
@@ -0,0 +1,5 @@
1
+ import ChatHomeRedirect from "@/components/chat/ChatHomeRedirect";
2
+
3
+ export { meta } from "./home";
4
+
5
+ export default ChatHomeRedirect;
@@ -1,9 +1,5 @@
1
- import { appPath } from "@agent-native/core/client/api-path";
2
- import { markAgentChatHomeHandoff } from "@agent-native/toolkit/app/chat/agentkit-chat/rail";
3
- import { useEffect, useRef, useState } from "react";
4
-
1
+ import HomePage from "@/components/home/HomePage";
5
2
  import { APP_TITLE } from "@/lib/app-config";
6
- import { getChatHomeThreadId } from "@/lib/chat-home-thread";
7
3
 
8
4
  const SEO_TITLE = `${APP_TITLE} - Open Source AI app starter with actions`;
9
5
  const SEO_DESCRIPTION =
@@ -24,21 +20,6 @@ export function meta() {
24
20
  ];
25
21
  }
26
22
 
27
- export default function ChatRoute() {
28
- const [threadId] = useState(getChatHomeThreadId);
29
- const handoffStartedRef = useRef(false);
30
-
31
- useEffect(() => {
32
- if (handoffStartedRef.current) return;
33
- handoffStartedRef.current = true;
34
- markAgentChatHomeHandoff("chat");
35
- try {
36
- window.location.replace(appPath(`/chat/${encodeURIComponent(threadId)}`));
37
- } catch (error) {
38
- handoffStartedRef.current = false;
39
- throw error;
40
- }
41
- }, [threadId]);
42
-
43
- return null;
23
+ export default function HomeRoute() {
24
+ return <HomePage />;
44
25
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-10
4
+ ---
5
+
6
+ Large prompts with full context remain available to retry when sending fails.
@@ -25,8 +25,12 @@ never copy a value into app code or client-visible state.
25
25
  3. If the app truly needs its own API key, register it with the shared secrets
26
26
  surface. Do not build a parallel key form or storage table.
27
27
  4. Use deployment configuration only for a secret owned by the deployed app,
28
- such as a webhook verification secret. A deployment key must not stand in
29
- for a user's or customer's provider credential.
28
+ such as a webhook verification secret. LLM inference keys are an app-owned
29
+ exception: self-hosted apps may use provider keys from deployment env as a
30
+ shared fallback, while scoped user or organization keys take precedence.
31
+ Hosted-workspace deployments backed by a shared database must not share
32
+ those env keys across apps or tenants. This exception does not authorize
33
+ using env credentials to access user-owned provider data.
30
34
 
31
35
  ## Register an app-local API key
32
36
 
@@ -25,8 +25,12 @@ never copy a value into app code or client-visible state.
25
25
  3. If the app truly needs its own API key, register it with the shared secrets
26
26
  surface. Do not build a parallel key form or storage table.
27
27
  4. Use deployment configuration only for a secret owned by the deployed app,
28
- such as a webhook verification secret. A deployment key must not stand in
29
- for a user's or customer's provider credential.
28
+ such as a webhook verification secret. LLM inference keys are an app-owned
29
+ exception: self-hosted apps may use provider keys from deployment env as a
30
+ shared fallback, while scoped user or organization keys take precedence.
31
+ Hosted-workspace deployments backed by a shared database must not share
32
+ those env keys across apps or tenants. This exception does not authorize
33
+ using env credentials to access user-owned provider data.
30
34
 
31
35
  ## Register an app-local API key
32
36
 
@@ -25,8 +25,12 @@ never copy a value into app code or client-visible state.
25
25
  3. If the app truly needs its own API key, register it with the shared secrets
26
26
  surface. Do not build a parallel key form or storage table.
27
27
  4. Use deployment configuration only for a secret owned by the deployed app,
28
- such as a webhook verification secret. A deployment key must not stand in
29
- for a user's or customer's provider credential.
28
+ such as a webhook verification secret. LLM inference keys are an app-owned
29
+ exception: self-hosted apps may use provider keys from deployment env as a
30
+ shared fallback, while scoped user or organization keys take precedence.
31
+ Hosted-workspace deployments backed by a shared database must not share
32
+ those env keys across apps or tenants. This exception does not authorize
33
+ using env credentials to access user-owned provider data.
30
34
 
31
35
  ## Register an app-local API key
32
36
 
@@ -29,10 +29,10 @@ declare const _default: import("../../action.js").ActionDefinition<{
29
29
  executionEngine?: undefined;
30
30
  executionCwd?: undefined;
31
31
  } | {
32
+ scope?: undefined;
32
33
  enabled?: undefined;
33
34
  nextRun?: undefined;
34
35
  created?: undefined;
35
- scope?: undefined;
36
36
  triggerType?: undefined;
37
37
  event?: undefined;
38
38
  deleted: boolean;
@@ -45,9 +45,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
45
45
  executionCwd?: undefined;
46
46
  webhookPath?: undefined;
47
47
  } | {
48
+ scope?: undefined;
48
49
  deleted?: undefined;
49
50
  created?: undefined;
50
- scope?: undefined;
51
51
  triggerType?: undefined;
52
52
  event?: undefined;
53
53
  name: string;
@@ -58,16 +58,16 @@ platform when it is unset. Workspace and A2A settings are conditional on those
58
58
  features. Missing values either fail-closed or fall back to weaker behavior
59
59
  with a loud warning.
60
60
 
61
- | Variable | Description |
62
- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
- | `BETTER_AUTH_SECRET` | 32+ char random string. Signs session cookies and is the fallback for `OAUTH_STATE_SECRET` and app-local encrypted values. It is also a legacy read candidate for existing shared vault ciphertext. Hard-required: the framework throws on startup if missing in production. |
64
- | `BETTER_AUTH_URL` | Optional public origin override for this app (e.g. `https://mail.example.com`). The framework infers it from request, known-template, and hosting-platform context when unset. |
65
- | `ANTHROPIC_API_KEY` | API key for the embedded production agent. **In multi-tenant deploys**, the framework refuses to fall back to this when the user has no per-user key. Bring-your-own-key is required. Single-tenant self-hosted installs use it as a global key. |
66
- | `OAUTH_STATE_SECRET` | Dedicated HMAC key for OAuth state envelopes (Google, Atlassian, Zoom). Falls back to `BETTER_AUTH_SECRET` when unset, but a dedicated value is recommended so rotating one doesn't invalidate the other. Generate via `openssl rand -hex 32`. |
67
- | `A2A_SECRET` | Shared HMAC for inter-app A2A JSON-RPC and signed background handoffs. Required for hosted workspace deploys and A2A features. Without it, those endpoints fail closed in production. When no dedicated workspace vault key is set, shared vault ciphertext uses a purpose-derived key from this value, so rotating it also rotates the vault key. |
68
- | `WORKSPACE_SECRETS_ENCRYPTION_KEY` | Preferred stable AES-256-GCM key for the shared encrypted-at-rest secrets vault. Set the same value in every app that reads the vault. It is separate from app-local OAuth/credential encryption, so A2A trust can rotate without stranding vault rows. Existing A2A-, Better Auth-, and app-key ciphertext is migrated on successful reads. |
69
- | `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` | Optional previous workspace vault key used only during a rotation window. Deploy it alongside the new `WORKSPACE_SECRETS_ENCRYPTION_KEY`, let successful reads re-encrypt rows, then remove it after migration. |
70
- | `SECRETS_ENCRYPTION_KEY` | Legacy combined key for app-local encryption and the shared vault. It remains supported, but new multi-app deployments should use `WORKSPACE_SECRETS_ENCRYPTION_KEY` for the shared vault so changing workspace encryption does not change app-local OAuth ciphertext. |
61
+ | Variable | Description |
62
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
63
+ | `BETTER_AUTH_SECRET` | 32+ char random string. Signs session cookies and is the fallback for `OAUTH_STATE_SECRET` and app-local encrypted values. It is also a legacy read candidate for existing shared vault ciphertext. Hard-required: the framework throws on startup if missing in production. |
64
+ | `BETTER_AUTH_URL` | Optional public origin override for this app (e.g. `https://mail.example.com`). The framework infers it from request, known-template, and hosting-platform context when unset. |
65
+ | `ANTHROPIC_API_KEY` | API key for the embedded agent. Self-hosted apps use deployment LLM provider keys as an app-wide fallback, including when using a remote database. Any signed-in user without a scoped key can use the deployment account across organizations, and usage is billed to that account. Production Hosted workspace runtimes block deployment provider-key fallbacks. |
66
+ | `OAUTH_STATE_SECRET` | Dedicated HMAC key for OAuth state envelopes (Google, Atlassian, Zoom). Falls back to `BETTER_AUTH_SECRET` when unset, but a dedicated value is recommended so rotating one doesn't invalidate the other. Generate via `openssl rand -hex 32`. |
67
+ | `A2A_SECRET` | Shared HMAC for inter-app A2A JSON-RPC and signed background handoffs. Required for hosted workspace deploys and A2A features. Without it, those endpoints fail closed in production. When no dedicated workspace vault key is set, shared vault ciphertext uses a purpose-derived key from this value, so rotating it also rotates the vault key. |
68
+ | `WORKSPACE_SECRETS_ENCRYPTION_KEY` | Preferred stable AES-256-GCM key for the shared encrypted-at-rest secrets vault. Set the same value in every app that reads the vault. It is separate from app-local OAuth/credential encryption, so A2A trust can rotate without stranding vault rows. Existing A2A-, Better Auth-, and app-key ciphertext is migrated on successful reads. |
69
+ | `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` | Optional previous workspace vault key used only during a rotation window. Deploy it alongside the new `WORKSPACE_SECRETS_ENCRYPTION_KEY`, let successful reads re-encrypt rows, then remove it after migration. |
70
+ | `SECRETS_ENCRYPTION_KEY` | Legacy combined key for app-local encryption and the shared vault. It remains supported, but new multi-app deployments should use `WORKSPACE_SECRETS_ENCRYPTION_KEY` for the shared vault so changing workspace encryption does not change app-local OAuth ciphertext. |
71
71
 
72
72
  ## Auth & Identity {#env-auth}
73
73