@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