@agent-native/core 0.12.39 → 0.13.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 (50) hide show
  1. package/dist/agent/run-manager.d.ts +5 -1
  2. package/dist/agent/run-manager.d.ts.map +1 -1
  3. package/dist/agent/run-manager.js +48 -2
  4. package/dist/agent/run-manager.js.map +1 -1
  5. package/dist/agent/run-store.d.ts +8 -0
  6. package/dist/agent/run-store.d.ts.map +1 -1
  7. package/dist/agent/run-store.js +36 -5
  8. package/dist/agent/run-store.js.map +1 -1
  9. package/dist/client/AssistantChat.d.ts.map +1 -1
  10. package/dist/client/AssistantChat.js +42 -3
  11. package/dist/client/AssistantChat.js.map +1 -1
  12. package/dist/client/ConnectBuilderCard.js +1 -1
  13. package/dist/client/ConnectBuilderCard.js.map +1 -1
  14. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  15. package/dist/client/MultiTabAssistantChat.js +27 -16
  16. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  17. package/dist/client/RunStuckBanner.d.ts +35 -0
  18. package/dist/client/RunStuckBanner.d.ts.map +1 -0
  19. package/dist/client/RunStuckBanner.js +66 -0
  20. package/dist/client/RunStuckBanner.js.map +1 -0
  21. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  22. package/dist/client/agent-chat-adapter.js +19 -0
  23. package/dist/client/agent-chat-adapter.js.map +1 -1
  24. package/dist/client/components/CodeRequiredDialog.js +1 -1
  25. package/dist/client/components/CodeRequiredDialog.js.map +1 -1
  26. package/dist/client/composer/ComposerPlusMenu.d.ts +3 -3
  27. package/dist/client/composer/ComposerPlusMenu.js +3 -3
  28. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  29. package/dist/client/composer/TiptapComposer.js +1 -1
  30. package/dist/client/composer/TiptapComposer.js.map +1 -1
  31. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  32. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  33. package/dist/client/index.d.ts +2 -0
  34. package/dist/client/index.d.ts.map +1 -1
  35. package/dist/client/index.js +2 -0
  36. package/dist/client/index.js.map +1 -1
  37. package/dist/client/resources/ResourcesPanel.js +2 -2
  38. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  39. package/dist/client/use-chat-threads.d.ts +7 -0
  40. package/dist/client/use-chat-threads.d.ts.map +1 -1
  41. package/dist/client/use-chat-threads.js +91 -43
  42. package/dist/client/use-chat-threads.js.map +1 -1
  43. package/dist/client/use-run-stuck-detection.d.ts +47 -0
  44. package/dist/client/use-run-stuck-detection.d.ts.map +1 -0
  45. package/dist/client/use-run-stuck-detection.js +102 -0
  46. package/dist/client/use-run-stuck-detection.js.map +1 -0
  47. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  48. package/dist/server/agent-chat-plugin.js +68 -3
  49. package/dist/server/agent-chat-plugin.js.map +1 -1
  50. package/package.json +1 -1
@@ -1,21 +1,47 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
2
  import { agentNativePath } from "./api-path.js";
3
3
  const ACTIVE_THREAD_KEY = "agent-chat-active-thread";
4
+ const THREAD_DATA_CACHE_PREFIX = "agent-chat-thread-cache:";
5
+ /**
6
+ * Key for the per-thread message cache in localStorage. AssistantChat reads
7
+ * this synchronously on mount so existing chats can hydrate from cache and
8
+ * paint immediately, then refreshes from the server in the background.
9
+ */
10
+ export function getThreadCacheKey(threadId) {
11
+ return `${THREAD_DATA_CACHE_PREFIX}${threadId}`;
12
+ }
4
13
  export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-chat"), storageKey) {
5
14
  const activeThreadKey = storageKey
6
15
  ? `${ACTIVE_THREAD_KEY}:${storageKey}`
7
16
  : ACTIVE_THREAD_KEY;
8
17
  const [threads, setThreads] = useState([]);
18
+ // IDs we generated client-side this session — consumers use this to know
19
+ // whether to skip the per-thread restore skeleton. Tracked by ref instead
20
+ // of state because the consumer reads it inside the render path and we
21
+ // never need to re-render when the set changes.
22
+ const newlyCreatedRef = useRef(new Set());
9
23
  const [activeThreadId, setActiveThreadId] = useState(() => {
24
+ if (typeof window === "undefined")
25
+ return null;
10
26
  try {
11
- return localStorage.getItem(activeThreadKey);
27
+ const saved = localStorage.getItem(activeThreadKey);
28
+ if (saved)
29
+ return saved;
12
30
  }
13
- catch {
14
- return null;
31
+ catch { }
32
+ // No saved thread — generate one synchronously so the chat shell + composer
33
+ // can paint on first render instead of after a network round-trip.
34
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
35
+ const id = crypto.randomUUID();
36
+ newlyCreatedRef.current.add(id);
37
+ return id;
15
38
  }
39
+ return null;
16
40
  });
17
41
  const [isLoading, setIsLoading] = useState(true);
18
42
  const fetchedRef = useRef(false);
43
+ const activeThreadIdRef = useRef(activeThreadId);
44
+ activeThreadIdRef.current = activeThreadId;
19
45
  // Persist active thread ID
20
46
  useEffect(() => {
21
47
  try {
@@ -34,46 +60,24 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
34
60
  if (!res.ok)
35
61
  return;
36
62
  const data = await res.json();
37
- setThreads(data.threads ?? []);
63
+ setThreads((prev) => {
64
+ const loaded = (data.threads ?? []);
65
+ // Preserve any optimistic threads we've created this session that
66
+ // haven't shown up in the server list yet (POST still in-flight).
67
+ const loadedIds = new Set(loaded.map((t) => t.id));
68
+ const optimisticOnly = prev.filter((t) => newlyCreatedRef.current.has(t.id) && !loadedIds.has(t.id));
69
+ return [...optimisticOnly, ...loaded];
70
+ });
38
71
  return data.threads;
39
72
  }
40
73
  catch {
41
74
  return undefined;
42
75
  }
43
76
  }, [apiUrl]);
44
- // Initial load
45
- useEffect(() => {
46
- if (fetchedRef.current)
47
- return;
48
- fetchedRef.current = true;
49
- (async () => {
50
- setIsLoading(true);
51
- const loadedThreads = await fetchThreads();
52
- if (loadedThreads && loadedThreads.length > 0) {
53
- // If the saved active thread still exists, keep it. Otherwise use the most recent.
54
- const savedId = activeThreadId;
55
- if (!savedId || !loadedThreads.find((t) => t.id === savedId)) {
56
- setActiveThreadId(loadedThreads[0].id);
57
- }
58
- }
59
- else {
60
- // No threads — create the first one
61
- try {
62
- const res = await fetch(`${apiUrl}/threads`, { method: "POST" });
63
- if (res.ok) {
64
- const thread = await res.json();
65
- setThreads([thread]);
66
- setActiveThreadId(thread.id);
67
- }
68
- }
69
- catch { }
70
- }
71
- setIsLoading(false);
72
- })();
73
- }, [fetchThreads, apiUrl, activeThreadId]);
74
- const createThread = useCallback((preferredId) => {
75
- // Generate ID client-side for instant UI response
76
- const id = preferredId || crypto.randomUUID();
77
+ // Persist a client-generated thread to the server in the background.
78
+ // Optimistically adds it to the local thread list so callers can render
79
+ // immediately; rolls back on failure.
80
+ const persistNewThread = useCallback((id) => {
77
81
  const now = Date.now();
78
82
  const optimistic = {
79
83
  id,
@@ -83,9 +87,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
83
87
  createdAt: now,
84
88
  updatedAt: now,
85
89
  };
86
- setThreads((prev) => [optimistic, ...prev]);
87
- setActiveThreadId(id);
88
- // Persist to server in the background
90
+ setThreads((prev) => prev.some((t) => t.id === id) ? prev : [optimistic, ...prev]);
89
91
  fetch(`${apiUrl}/threads`, {
90
92
  method: "POST",
91
93
  headers: { "Content-Type": "application/json" },
@@ -103,13 +105,47 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
103
105
  setThreads((prev) => prev.map((thread) => (thread.id === id ? created : thread)));
104
106
  })
105
107
  .catch(() => {
106
- // If server fails, remove the optimistic thread instead of leaving a
107
- // phantom active tab that disappears on the next refresh.
108
108
  setThreads((prev) => prev.filter((t) => t.id !== id));
109
+ newlyCreatedRef.current.delete(id);
109
110
  setActiveThreadId((current) => (current === id ? null : current));
110
111
  });
111
- return Promise.resolve(id);
112
112
  }, [apiUrl]);
113
+ // Initial load. Runs in the background — does NOT gate the consumer's
114
+ // first paint. The composer renders against the optimistic active thread
115
+ // we set up in useState above; this fetch just populates the history list
116
+ // and reconciles a stale saved active id.
117
+ useEffect(() => {
118
+ if (fetchedRef.current)
119
+ return;
120
+ fetchedRef.current = true;
121
+ // Persist any thread we optimistically created during the initial render.
122
+ for (const id of newlyCreatedRef.current) {
123
+ persistNewThread(id);
124
+ }
125
+ (async () => {
126
+ const loadedThreads = await fetchThreads();
127
+ if (loadedThreads && loadedThreads.length > 0) {
128
+ const savedId = activeThreadIdRef.current;
129
+ // If the saved active thread isn't on the server (and isn't one we
130
+ // just created client-side), fall back to the most recent.
131
+ if (savedId &&
132
+ !newlyCreatedRef.current.has(savedId) &&
133
+ !loadedThreads.find((t) => t.id === savedId)) {
134
+ setActiveThreadId(loadedThreads[0].id);
135
+ }
136
+ }
137
+ setIsLoading(false);
138
+ })();
139
+ }, [fetchThreads, persistNewThread]);
140
+ const createThread = useCallback((preferredId) => {
141
+ // Generate ID client-side for instant UI response
142
+ const id = preferredId || crypto.randomUUID();
143
+ newlyCreatedRef.current.add(id);
144
+ setActiveThreadId(id);
145
+ persistNewThread(id);
146
+ return Promise.resolve(id);
147
+ }, [persistNewThread]);
148
+ const isNewThread = useCallback((id) => newlyCreatedRef.current.has(id), []);
113
149
  const switchThread = useCallback((id) => {
114
150
  setActiveThreadId(id);
115
151
  }, []);
@@ -120,6 +156,10 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
120
156
  });
121
157
  }
122
158
  catch { }
159
+ try {
160
+ localStorage.removeItem(getThreadCacheKey(id));
161
+ }
162
+ catch { }
123
163
  setThreads((prev) => {
124
164
  const next = prev.filter((t) => t.id !== id);
125
165
  if (id === activeThreadId) {
@@ -136,6 +176,13 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
136
176
  });
137
177
  }, [apiUrl, activeThreadId, createThread]);
138
178
  const saveThreadData = useCallback(async (id, data) => {
179
+ // Cache locally so the next mount of this thread can hydrate
180
+ // synchronously and skip the per-message restore skeleton. Quota errors
181
+ // (5–10MB cap) are swallowed — the thread just falls back to fetching.
182
+ try {
183
+ localStorage.setItem(getThreadCacheKey(id), data.threadData);
184
+ }
185
+ catch { }
139
186
  try {
140
187
  await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {
141
188
  method: "PUT",
@@ -238,6 +285,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
238
285
  generateTitle,
239
286
  searchThreads,
240
287
  refreshThreads,
288
+ isNewThread,
241
289
  };
242
290
  }
243
291
  //# sourceMappingURL=use-chat-threads.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat-threads.js","sourceRoot":"","sources":["../../src/client/use-chat-threads.ts"],"names":[],"mappings":"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","sourcesContent":["import { useState, useEffect, useCallback, useRef } from \"react\";\nimport { agentNativePath } from \"./api-path.js\";\n\nexport interface ChatThreadSummary {\n id: string;\n title: string;\n preview: string;\n messageCount: number;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface ChatThreadData {\n id: string;\n ownerEmail: string;\n title: string;\n preview: string;\n threadData: string;\n messageCount: number;\n createdAt: number;\n updatedAt: number;\n}\n\nconst ACTIVE_THREAD_KEY = \"agent-chat-active-thread\";\n\nexport function useChatThreads(\n apiUrl = agentNativePath(\"/_agent-native/agent-chat\"),\n storageKey?: string,\n) {\n const activeThreadKey = storageKey\n ? `${ACTIVE_THREAD_KEY}:${storageKey}`\n : ACTIVE_THREAD_KEY;\n const [threads, setThreads] = useState<ChatThreadSummary[]>([]);\n const [activeThreadId, setActiveThreadId] = useState<string | null>(() => {\n try {\n return localStorage.getItem(activeThreadKey);\n } catch {\n return null;\n }\n });\n const [isLoading, setIsLoading] = useState(true);\n const fetchedRef = useRef(false);\n\n // Persist active thread ID\n useEffect(() => {\n try {\n if (activeThreadId) {\n localStorage.setItem(activeThreadKey, activeThreadId);\n } else {\n localStorage.removeItem(activeThreadKey);\n }\n } catch {}\n }, [activeThreadId, activeThreadKey]);\n\n const fetchThreads = useCallback(async () => {\n try {\n const res = await fetch(`${apiUrl}/threads`);\n if (!res.ok) return;\n const data = await res.json();\n setThreads(data.threads ?? []);\n return data.threads as ChatThreadSummary[];\n } catch {\n return undefined;\n }\n }, [apiUrl]);\n\n // Initial load\n useEffect(() => {\n if (fetchedRef.current) return;\n fetchedRef.current = true;\n\n (async () => {\n setIsLoading(true);\n const loadedThreads = await fetchThreads();\n\n if (loadedThreads && loadedThreads.length > 0) {\n // If the saved active thread still exists, keep it. Otherwise use the most recent.\n const savedId = activeThreadId;\n if (!savedId || !loadedThreads.find((t) => t.id === savedId)) {\n setActiveThreadId(loadedThreads[0].id);\n }\n } else {\n // No threads — create the first one\n try {\n const res = await fetch(`${apiUrl}/threads`, { method: \"POST\" });\n if (res.ok) {\n const thread = await res.json();\n setThreads([thread]);\n setActiveThreadId(thread.id);\n }\n } catch {}\n }\n setIsLoading(false);\n })();\n }, [fetchThreads, apiUrl, activeThreadId]);\n\n const createThread = useCallback(\n (preferredId?: string): Promise<string | null> => {\n // Generate ID client-side for instant UI response\n const id = preferredId || crypto.randomUUID();\n const now = Date.now();\n const optimistic: ChatThreadSummary = {\n id,\n title: \"\",\n preview: \"\",\n messageCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n setThreads((prev) => [optimistic, ...prev]);\n setActiveThreadId(id);\n\n // Persist to server in the background\n fetch(`${apiUrl}/threads`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id }),\n })\n .then(async (res) => {\n if (!res.ok) {\n throw new Error(`Thread create failed with ${res.status}`);\n }\n const created = (await res\n .json()\n .catch(() => null)) as ChatThreadSummary | null;\n if (!created) return;\n setThreads((prev) =>\n prev.map((thread) => (thread.id === id ? created : thread)),\n );\n })\n .catch(() => {\n // If server fails, remove the optimistic thread instead of leaving a\n // phantom active tab that disappears on the next refresh.\n setThreads((prev) => prev.filter((t) => t.id !== id));\n setActiveThreadId((current) => (current === id ? null : current));\n });\n\n return Promise.resolve(id);\n },\n [apiUrl],\n );\n\n const switchThread = useCallback((id: string) => {\n setActiveThreadId(id);\n }, []);\n\n const removeThread = useCallback(\n async (id: string) => {\n try {\n await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n });\n } catch {}\n setThreads((prev) => {\n const next = prev.filter((t) => t.id !== id);\n if (id === activeThreadId) {\n // Switch to the next available thread, or create new if empty\n if (next.length > 0) {\n setActiveThreadId(next[0].id);\n } else {\n // Create a new thread\n createThread();\n }\n }\n return next;\n });\n },\n [apiUrl, activeThreadId, createThread],\n );\n\n const saveThreadData = useCallback(\n async (\n id: string,\n data: {\n threadData: string;\n title: string;\n preview: string;\n messageCount?: number;\n },\n ) => {\n try {\n await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n });\n // Update local thread list metadata\n setThreads((prev) =>\n prev.map((t) =>\n t.id === id\n ? {\n ...t,\n title: data.title,\n preview: data.preview,\n ...(data.messageCount != null && {\n messageCount: data.messageCount,\n }),\n updatedAt: Date.now(),\n }\n : t,\n ),\n );\n } catch {}\n },\n [apiUrl],\n );\n\n const generateTitle = useCallback(\n async (threadId: string, message: string): Promise<string | null> => {\n try {\n const res = await fetch(`${apiUrl}/generate-title`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ message }),\n });\n if (!res.ok) return null;\n const data = await res.json();\n const title = data.title;\n if (!title) return null;\n // Update the title in local state\n setThreads((prev) =>\n prev.map((t) => (t.id === threadId ? { ...t, title } : t)),\n );\n return title;\n } catch {\n return null;\n }\n },\n [apiUrl],\n );\n\n const forkThread = useCallback(\n async (sourceId: string): Promise<string | null> => {\n const id = crypto.randomUUID();\n try {\n const res = await fetch(\n `${apiUrl}/threads/${encodeURIComponent(sourceId)}/fork`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id }),\n },\n );\n if (!res.ok) {\n // Surface failures so a click on the Fork button isn't a silent\n // no-op when the source thread can't be found or auth has lapsed.\n console.error(\n `[chat] fork failed for ${sourceId}: ${res.status} ${res.statusText}`,\n );\n return null;\n }\n const thread = await res.json();\n setThreads((prev) => [\n {\n id: thread.id,\n title: thread.title,\n preview: thread.preview,\n messageCount: thread.messageCount,\n createdAt: thread.createdAt,\n updatedAt: thread.updatedAt,\n },\n ...prev,\n ]);\n return thread.id;\n } catch (err) {\n console.error(`[chat] fork threw for ${sourceId}:`, err);\n return null;\n }\n },\n [apiUrl],\n );\n\n const searchThreads = useCallback(\n async (query: string): Promise<ChatThreadSummary[]> => {\n try {\n const res = await fetch(\n `${apiUrl}/threads?q=${encodeURIComponent(query)}`,\n );\n if (!res.ok) return [];\n const data = await res.json();\n return data.threads ?? [];\n } catch {\n return [];\n }\n },\n [apiUrl],\n );\n\n const refreshThreads = useCallback(() => {\n fetchThreads();\n }, [fetchThreads]);\n\n return {\n threads,\n activeThreadId,\n isLoading,\n createThread,\n switchThread,\n deleteThread: removeThread,\n forkThread,\n saveThreadData,\n generateTitle,\n searchThreads,\n refreshThreads,\n };\n}\n"]}
1
+ {"version":3,"file":"use-chat-threads.js","sourceRoot":"","sources":["../../src/client/use-chat-threads.ts"],"names":[],"mappings":"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","sourcesContent":["import { useState, useEffect, useCallback, useRef } from \"react\";\nimport { agentNativePath } from \"./api-path.js\";\n\nexport interface ChatThreadSummary {\n id: string;\n title: string;\n preview: string;\n messageCount: number;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface ChatThreadData {\n id: string;\n ownerEmail: string;\n title: string;\n preview: string;\n threadData: string;\n messageCount: number;\n createdAt: number;\n updatedAt: number;\n}\n\nconst ACTIVE_THREAD_KEY = \"agent-chat-active-thread\";\nconst THREAD_DATA_CACHE_PREFIX = \"agent-chat-thread-cache:\";\n\n/**\n * Key for the per-thread message cache in localStorage. AssistantChat reads\n * this synchronously on mount so existing chats can hydrate from cache and\n * paint immediately, then refreshes from the server in the background.\n */\nexport function getThreadCacheKey(threadId: string): string {\n return `${THREAD_DATA_CACHE_PREFIX}${threadId}`;\n}\n\nexport function useChatThreads(\n apiUrl = agentNativePath(\"/_agent-native/agent-chat\"),\n storageKey?: string,\n) {\n const activeThreadKey = storageKey\n ? `${ACTIVE_THREAD_KEY}:${storageKey}`\n : ACTIVE_THREAD_KEY;\n const [threads, setThreads] = useState<ChatThreadSummary[]>([]);\n\n // IDs we generated client-side this session — consumers use this to know\n // whether to skip the per-thread restore skeleton. Tracked by ref instead\n // of state because the consumer reads it inside the render path and we\n // never need to re-render when the set changes.\n const newlyCreatedRef = useRef<Set<string>>(new Set());\n\n const [activeThreadId, setActiveThreadId] = useState<string | null>(() => {\n if (typeof window === \"undefined\") return null;\n try {\n const saved = localStorage.getItem(activeThreadKey);\n if (saved) return saved;\n } catch {}\n // No saved thread — generate one synchronously so the chat shell + composer\n // can paint on first render instead of after a network round-trip.\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n const id = crypto.randomUUID();\n newlyCreatedRef.current.add(id);\n return id;\n }\n return null;\n });\n const [isLoading, setIsLoading] = useState(true);\n const fetchedRef = useRef(false);\n const activeThreadIdRef = useRef(activeThreadId);\n activeThreadIdRef.current = activeThreadId;\n\n // Persist active thread ID\n useEffect(() => {\n try {\n if (activeThreadId) {\n localStorage.setItem(activeThreadKey, activeThreadId);\n } else {\n localStorage.removeItem(activeThreadKey);\n }\n } catch {}\n }, [activeThreadId, activeThreadKey]);\n\n const fetchThreads = useCallback(async () => {\n try {\n const res = await fetch(`${apiUrl}/threads`);\n if (!res.ok) return;\n const data = await res.json();\n setThreads((prev) => {\n const loaded = (data.threads ?? []) as ChatThreadSummary[];\n // Preserve any optimistic threads we've created this session that\n // haven't shown up in the server list yet (POST still in-flight).\n const loadedIds = new Set(loaded.map((t) => t.id));\n const optimisticOnly = prev.filter(\n (t) => newlyCreatedRef.current.has(t.id) && !loadedIds.has(t.id),\n );\n return [...optimisticOnly, ...loaded];\n });\n return data.threads as ChatThreadSummary[];\n } catch {\n return undefined;\n }\n }, [apiUrl]);\n\n // Persist a client-generated thread to the server in the background.\n // Optimistically adds it to the local thread list so callers can render\n // immediately; rolls back on failure.\n const persistNewThread = useCallback(\n (id: string) => {\n const now = Date.now();\n const optimistic: ChatThreadSummary = {\n id,\n title: \"\",\n preview: \"\",\n messageCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n setThreads((prev) =>\n prev.some((t) => t.id === id) ? prev : [optimistic, ...prev],\n );\n fetch(`${apiUrl}/threads`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id }),\n })\n .then(async (res) => {\n if (!res.ok) {\n throw new Error(`Thread create failed with ${res.status}`);\n }\n const created = (await res\n .json()\n .catch(() => null)) as ChatThreadSummary | null;\n if (!created) return;\n setThreads((prev) =>\n prev.map((thread) => (thread.id === id ? created : thread)),\n );\n })\n .catch(() => {\n setThreads((prev) => prev.filter((t) => t.id !== id));\n newlyCreatedRef.current.delete(id);\n setActiveThreadId((current) => (current === id ? null : current));\n });\n },\n [apiUrl],\n );\n\n // Initial load. Runs in the background — does NOT gate the consumer's\n // first paint. The composer renders against the optimistic active thread\n // we set up in useState above; this fetch just populates the history list\n // and reconciles a stale saved active id.\n useEffect(() => {\n if (fetchedRef.current) return;\n fetchedRef.current = true;\n\n // Persist any thread we optimistically created during the initial render.\n for (const id of newlyCreatedRef.current) {\n persistNewThread(id);\n }\n\n (async () => {\n const loadedThreads = await fetchThreads();\n if (loadedThreads && loadedThreads.length > 0) {\n const savedId = activeThreadIdRef.current;\n // If the saved active thread isn't on the server (and isn't one we\n // just created client-side), fall back to the most recent.\n if (\n savedId &&\n !newlyCreatedRef.current.has(savedId) &&\n !loadedThreads.find((t) => t.id === savedId)\n ) {\n setActiveThreadId(loadedThreads[0].id);\n }\n }\n setIsLoading(false);\n })();\n }, [fetchThreads, persistNewThread]);\n\n const createThread = useCallback(\n (preferredId?: string): Promise<string | null> => {\n // Generate ID client-side for instant UI response\n const id = preferredId || crypto.randomUUID();\n newlyCreatedRef.current.add(id);\n setActiveThreadId(id);\n persistNewThread(id);\n return Promise.resolve(id);\n },\n [persistNewThread],\n );\n\n const isNewThread = useCallback(\n (id: string) => newlyCreatedRef.current.has(id),\n [],\n );\n\n const switchThread = useCallback((id: string) => {\n setActiveThreadId(id);\n }, []);\n\n const removeThread = useCallback(\n async (id: string) => {\n try {\n await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n });\n } catch {}\n try {\n localStorage.removeItem(getThreadCacheKey(id));\n } catch {}\n setThreads((prev) => {\n const next = prev.filter((t) => t.id !== id);\n if (id === activeThreadId) {\n // Switch to the next available thread, or create new if empty\n if (next.length > 0) {\n setActiveThreadId(next[0].id);\n } else {\n // Create a new thread\n createThread();\n }\n }\n return next;\n });\n },\n [apiUrl, activeThreadId, createThread],\n );\n\n const saveThreadData = useCallback(\n async (\n id: string,\n data: {\n threadData: string;\n title: string;\n preview: string;\n messageCount?: number;\n },\n ) => {\n // Cache locally so the next mount of this thread can hydrate\n // synchronously and skip the per-message restore skeleton. Quota errors\n // (5–10MB cap) are swallowed — the thread just falls back to fetching.\n try {\n localStorage.setItem(getThreadCacheKey(id), data.threadData);\n } catch {}\n try {\n await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {\n method: \"PUT\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n });\n // Update local thread list metadata\n setThreads((prev) =>\n prev.map((t) =>\n t.id === id\n ? {\n ...t,\n title: data.title,\n preview: data.preview,\n ...(data.messageCount != null && {\n messageCount: data.messageCount,\n }),\n updatedAt: Date.now(),\n }\n : t,\n ),\n );\n } catch {}\n },\n [apiUrl],\n );\n\n const generateTitle = useCallback(\n async (threadId: string, message: string): Promise<string | null> => {\n try {\n const res = await fetch(`${apiUrl}/generate-title`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ message }),\n });\n if (!res.ok) return null;\n const data = await res.json();\n const title = data.title;\n if (!title) return null;\n // Update the title in local state\n setThreads((prev) =>\n prev.map((t) => (t.id === threadId ? { ...t, title } : t)),\n );\n return title;\n } catch {\n return null;\n }\n },\n [apiUrl],\n );\n\n const forkThread = useCallback(\n async (sourceId: string): Promise<string | null> => {\n const id = crypto.randomUUID();\n try {\n const res = await fetch(\n `${apiUrl}/threads/${encodeURIComponent(sourceId)}/fork`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id }),\n },\n );\n if (!res.ok) {\n // Surface failures so a click on the Fork button isn't a silent\n // no-op when the source thread can't be found or auth has lapsed.\n console.error(\n `[chat] fork failed for ${sourceId}: ${res.status} ${res.statusText}`,\n );\n return null;\n }\n const thread = await res.json();\n setThreads((prev) => [\n {\n id: thread.id,\n title: thread.title,\n preview: thread.preview,\n messageCount: thread.messageCount,\n createdAt: thread.createdAt,\n updatedAt: thread.updatedAt,\n },\n ...prev,\n ]);\n return thread.id;\n } catch (err) {\n console.error(`[chat] fork threw for ${sourceId}:`, err);\n return null;\n }\n },\n [apiUrl],\n );\n\n const searchThreads = useCallback(\n async (query: string): Promise<ChatThreadSummary[]> => {\n try {\n const res = await fetch(\n `${apiUrl}/threads?q=${encodeURIComponent(query)}`,\n );\n if (!res.ok) return [];\n const data = await res.json();\n return data.threads ?? [];\n } catch {\n return [];\n }\n },\n [apiUrl],\n );\n\n const refreshThreads = useCallback(() => {\n fetchThreads();\n }, [fetchThreads]);\n\n return {\n threads,\n activeThreadId,\n isLoading,\n createThread,\n switchThread,\n deleteThread: removeThread,\n forkThread,\n saveThreadData,\n generateTitle,\n searchThreads,\n refreshThreads,\n isNewThread,\n };\n}\n"]}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Per-thread chat run health, derived from the durable `last_progress_at`
3
+ * timestamp on the server. Drives the user-visible "this chat looks stuck"
4
+ * affordance — distinct from the silent reconnect logic in
5
+ * `agent-chat-adapter.ts`, which keeps trying in the background. When
6
+ * automatic recovery isn't making progress (for whatever reason), this
7
+ * hook surfaces a Retry / Cancel button to the user instead of leaving
8
+ * them staring at a frozen spinner.
9
+ */
10
+ export interface RunStuckState {
11
+ /** True when an active run hasn't emitted an event for `stuckThresholdMs`. */
12
+ isStuck: boolean;
13
+ /** ID of the active run, or null when nothing is in flight. */
14
+ runId: string | null;
15
+ /** Server-side run status ("running" / "completed" / "errored" / etc.). */
16
+ status: string | null;
17
+ /** Server timestamp (ms) of the last emitted event, or null if none yet. */
18
+ lastProgressAt: number | null;
19
+ /** Milliseconds since `lastProgressAt`, or null. */
20
+ stuckSinceMs: number | null;
21
+ /** Server timestamp (ms) of the last process-alive heartbeat. */
22
+ heartbeatAt: number | null;
23
+ }
24
+ export interface UseRunStuckDetectionOptions {
25
+ /** The thread to monitor. Pass null/undefined to disable polling. */
26
+ threadId: string | null | undefined;
27
+ /**
28
+ * Threshold above which an in-flight run is considered stuck. The default
29
+ * sits comfortably above the adapter's 75s no-progress reconnect — by then
30
+ * automatic recovery has already had its chance.
31
+ */
32
+ stuckThresholdMs?: number;
33
+ /** Poll interval. Default 5_000ms. */
34
+ pollIntervalMs?: number;
35
+ /** API base path. Default `/_agent-native/agent-chat`. */
36
+ apiUrl?: string;
37
+ }
38
+ export declare function useRunStuckDetection({ threadId, stuckThresholdMs, pollIntervalMs, apiUrl, }: UseRunStuckDetectionOptions): RunStuckState;
39
+ /**
40
+ * POST `/runs/:id/abort` so the server flips the run to "aborted" and the
41
+ * adapter's reconnect loop exits cleanly. Returns the run id that was
42
+ * aborted (or null on failure) so callers can correlate observability
43
+ * events. Best-effort — failures are swallowed, since the user's intent
44
+ * is already captured locally.
45
+ */
46
+ export declare function useAbortRun(apiUrl?: string): (runId: string, reason?: string) => Promise<string | null>;
47
+ //# sourceMappingURL=use-run-stuck-detection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-run-stuck-detection.d.ts","sourceRoot":"","sources":["../../src/client/use-run-stuck-detection.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,OAAO,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,oDAAoD;IACpD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iEAAiE;IACjE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,qEAAqE;IACrE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sCAAsC;IACtC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuBD,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,gBAA6C,EAC7C,cAAyC,EACzC,MAAM,GACP,EAAE,2BAA2B,GAAG,aAAa,CAoE7C;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,WAEzB,MAAM,WAAU,MAAM,KAAY,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoBzE"}
@@ -0,0 +1,102 @@
1
+ import { useEffect, useState, useCallback } from "react";
2
+ import { agentNativePath } from "./api-path.js";
3
+ const DEFAULT_STUCK_THRESHOLD_MS = 90_000;
4
+ const DEFAULT_POLL_INTERVAL_MS = 5_000;
5
+ const IDLE_BACKOFF_INTERVAL_MS = 15_000;
6
+ const EMPTY_STATE = {
7
+ isStuck: false,
8
+ runId: null,
9
+ status: null,
10
+ lastProgressAt: null,
11
+ stuckSinceMs: null,
12
+ heartbeatAt: null,
13
+ };
14
+ export function useRunStuckDetection({ threadId, stuckThresholdMs = DEFAULT_STUCK_THRESHOLD_MS, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, apiUrl, }) {
15
+ const [state, setState] = useState(EMPTY_STATE);
16
+ useEffect(() => {
17
+ // Reset on every thread change so the previous thread's stuck banner
18
+ // doesn't bleed onto the new one before the first poll completes.
19
+ setState(EMPTY_STATE);
20
+ if (!threadId)
21
+ return;
22
+ const base = apiUrl ?? agentNativePath("/_agent-native/agent-chat");
23
+ let cancelled = false;
24
+ let timer = null;
25
+ const poll = async () => {
26
+ if (cancelled)
27
+ return;
28
+ let nextDelay = pollIntervalMs;
29
+ try {
30
+ const res = await fetch(`${base}/runs/active?threadId=${encodeURIComponent(threadId)}`, { credentials: "same-origin" });
31
+ if (cancelled)
32
+ return;
33
+ if (res.ok) {
34
+ const data = (await res.json());
35
+ const lastProgressAt = data.lastProgressAt ?? null;
36
+ const stuckSinceMs = lastProgressAt != null ? Date.now() - lastProgressAt : null;
37
+ const isStuck = Boolean(data.active &&
38
+ data.status === "running" &&
39
+ stuckSinceMs != null &&
40
+ stuckSinceMs > stuckThresholdMs);
41
+ setState({
42
+ isStuck,
43
+ runId: data.runId ?? null,
44
+ status: data.status ?? null,
45
+ lastProgressAt,
46
+ stuckSinceMs,
47
+ heartbeatAt: data.heartbeatAt ?? null,
48
+ });
49
+ // Back off polling when nothing is in flight — there's no point
50
+ // hammering the endpoint while the chat is idle. We still poll
51
+ // occasionally so a fresh run started in another tab is picked up.
52
+ if (!data.active || data.status !== "running") {
53
+ nextDelay = IDLE_BACKOFF_INTERVAL_MS;
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ // Network blip — leave previous state. Next tick will retry.
59
+ }
60
+ if (!cancelled) {
61
+ timer = setTimeout(poll, nextDelay);
62
+ }
63
+ };
64
+ // Stagger the first poll so a freshly-started run isn't immediately
65
+ // classified as stuck before the server has had a chance to record
66
+ // any progress events.
67
+ timer = setTimeout(poll, 2_000);
68
+ return () => {
69
+ cancelled = true;
70
+ if (timer)
71
+ clearTimeout(timer);
72
+ };
73
+ }, [threadId, stuckThresholdMs, pollIntervalMs, apiUrl]);
74
+ return state;
75
+ }
76
+ /**
77
+ * POST `/runs/:id/abort` so the server flips the run to "aborted" and the
78
+ * adapter's reconnect loop exits cleanly. Returns the run id that was
79
+ * aborted (or null on failure) so callers can correlate observability
80
+ * events. Best-effort — failures are swallowed, since the user's intent
81
+ * is already captured locally.
82
+ */
83
+ export function useAbortRun(apiUrl) {
84
+ return useCallback(async (runId, reason = "user") => {
85
+ const base = apiUrl ?? agentNativePath("/_agent-native/agent-chat");
86
+ try {
87
+ const res = await fetch(`${base}/runs/${encodeURIComponent(runId)}/abort`, {
88
+ method: "POST",
89
+ headers: { "Content-Type": "application/json" },
90
+ credentials: "same-origin",
91
+ body: JSON.stringify({ reason }),
92
+ });
93
+ if (!res.ok)
94
+ return null;
95
+ return runId;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }, [apiUrl]);
101
+ }
102
+ //# sourceMappingURL=use-run-stuck-detection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-run-stuck-detection.js","sourceRoot":"","sources":["../../src/client/use-run-stuck-detection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAyChD,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAC1C,MAAM,wBAAwB,GAAG,KAAK,CAAC;AACvC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAUxC,MAAM,WAAW,GAAkB;IACjC,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,IAAI;IAClB,WAAW,EAAE,IAAI;CAClB,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAAC,EACnC,QAAQ,EACR,gBAAgB,GAAG,0BAA0B,EAC7C,cAAc,GAAG,wBAAwB,EACzC,MAAM,GACsB;IAC5B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,WAAW,CAAC,CAAC;IAE/D,SAAS,CAAC,GAAG,EAAE;QACb,qEAAqE;QACrE,kEAAkE;QAClE,QAAQ,CAAC,WAAW,CAAC,CAAC;QACtB,IAAI,CAAC,QAAQ;YAAE,OAAO;QAEtB,MAAM,IAAI,GAAG,MAAM,IAAI,eAAe,CAAC,2BAA2B,CAAC,CAAC;QACpE,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,KAAK,GAAyC,IAAI,CAAC;QAEvD,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;YACtB,IAAI,SAAS;gBAAE,OAAO;YACtB,IAAI,SAAS,GAAG,cAAc,CAAC;YAC/B,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,KAAK,CACrB,GAAG,IAAI,yBAAyB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,EAC9D,EAAE,WAAW,EAAE,aAAa,EAAE,CAC/B,CAAC;gBACF,IAAI,SAAS;oBAAE,OAAO;gBACtB,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;oBACX,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAsB,CAAC;oBACrD,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC;oBACnD,MAAM,YAAY,GAChB,cAAc,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC;oBAC9D,MAAM,OAAO,GAAG,OAAO,CACrB,IAAI,CAAC,MAAM;wBACX,IAAI,CAAC,MAAM,KAAK,SAAS;wBACzB,YAAY,IAAI,IAAI;wBACpB,YAAY,GAAG,gBAAgB,CAChC,CAAC;oBACF,QAAQ,CAAC;wBACP,OAAO;wBACP,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI;wBACzB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI;wBAC3B,cAAc;wBACd,YAAY;wBACZ,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;qBACtC,CAAC,CAAC;oBACH,gEAAgE;oBAChE,+DAA+D;oBAC/D,mEAAmE;oBACnE,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;wBAC9C,SAAS,GAAG,wBAAwB,CAAC;oBACvC,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,6DAA6D;YAC/D,CAAC;YACD,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YACtC,CAAC;QACH,CAAC,CAAC;QAEF,oEAAoE;QACpE,mEAAmE;QACnE,uBAAuB;QACvB,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAEhC,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;YACjB,IAAI,KAAK;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC;IAEzD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,MAAe;IACzC,OAAO,WAAW,CAChB,KAAK,EAAE,KAAa,EAAE,SAAiB,MAAM,EAA0B,EAAE;QACvE,MAAM,IAAI,GAAG,MAAM,IAAI,eAAe,CAAC,2BAA2B,CAAC,CAAC;QACpE,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CACrB,GAAG,IAAI,SAAS,kBAAkB,CAAC,KAAK,CAAC,QAAQ,EACjD;gBACE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,WAAW,EAAE,aAAa;gBAC1B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;aACjC,CACF,CAAC;YACF,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC;YACzB,OAAO,KAAK,CAAC;QACf,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;AACJ,CAAC","sourcesContent":["import { useEffect, useState, useCallback } from \"react\";\nimport { agentNativePath } from \"./api-path.js\";\n\n/**\n * Per-thread chat run health, derived from the durable `last_progress_at`\n * timestamp on the server. Drives the user-visible \"this chat looks stuck\"\n * affordance — distinct from the silent reconnect logic in\n * `agent-chat-adapter.ts`, which keeps trying in the background. When\n * automatic recovery isn't making progress (for whatever reason), this\n * hook surfaces a Retry / Cancel button to the user instead of leaving\n * them staring at a frozen spinner.\n */\nexport interface RunStuckState {\n /** True when an active run hasn't emitted an event for `stuckThresholdMs`. */\n isStuck: boolean;\n /** ID of the active run, or null when nothing is in flight. */\n runId: string | null;\n /** Server-side run status (\"running\" / \"completed\" / \"errored\" / etc.). */\n status: string | null;\n /** Server timestamp (ms) of the last emitted event, or null if none yet. */\n lastProgressAt: number | null;\n /** Milliseconds since `lastProgressAt`, or null. */\n stuckSinceMs: number | null;\n /** Server timestamp (ms) of the last process-alive heartbeat. */\n heartbeatAt: number | null;\n}\n\nexport interface UseRunStuckDetectionOptions {\n /** The thread to monitor. Pass null/undefined to disable polling. */\n threadId: string | null | undefined;\n /**\n * Threshold above which an in-flight run is considered stuck. The default\n * sits comfortably above the adapter's 75s no-progress reconnect — by then\n * automatic recovery has already had its chance.\n */\n stuckThresholdMs?: number;\n /** Poll interval. Default 5_000ms. */\n pollIntervalMs?: number;\n /** API base path. Default `/_agent-native/agent-chat`. */\n apiUrl?: string;\n}\n\nconst DEFAULT_STUCK_THRESHOLD_MS = 90_000;\nconst DEFAULT_POLL_INTERVAL_MS = 5_000;\nconst IDLE_BACKOFF_INTERVAL_MS = 15_000;\n\ninterface ActiveRunResponse {\n active: boolean;\n runId?: string;\n status?: string;\n heartbeatAt: number | null;\n lastProgressAt?: number | null;\n}\n\nconst EMPTY_STATE: RunStuckState = {\n isStuck: false,\n runId: null,\n status: null,\n lastProgressAt: null,\n stuckSinceMs: null,\n heartbeatAt: null,\n};\n\nexport function useRunStuckDetection({\n threadId,\n stuckThresholdMs = DEFAULT_STUCK_THRESHOLD_MS,\n pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,\n apiUrl,\n}: UseRunStuckDetectionOptions): RunStuckState {\n const [state, setState] = useState<RunStuckState>(EMPTY_STATE);\n\n useEffect(() => {\n // Reset on every thread change so the previous thread's stuck banner\n // doesn't bleed onto the new one before the first poll completes.\n setState(EMPTY_STATE);\n if (!threadId) return;\n\n const base = apiUrl ?? agentNativePath(\"/_agent-native/agent-chat\");\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const poll = async () => {\n if (cancelled) return;\n let nextDelay = pollIntervalMs;\n try {\n const res = await fetch(\n `${base}/runs/active?threadId=${encodeURIComponent(threadId)}`,\n { credentials: \"same-origin\" },\n );\n if (cancelled) return;\n if (res.ok) {\n const data = (await res.json()) as ActiveRunResponse;\n const lastProgressAt = data.lastProgressAt ?? null;\n const stuckSinceMs =\n lastProgressAt != null ? Date.now() - lastProgressAt : null;\n const isStuck = Boolean(\n data.active &&\n data.status === \"running\" &&\n stuckSinceMs != null &&\n stuckSinceMs > stuckThresholdMs,\n );\n setState({\n isStuck,\n runId: data.runId ?? null,\n status: data.status ?? null,\n lastProgressAt,\n stuckSinceMs,\n heartbeatAt: data.heartbeatAt ?? null,\n });\n // Back off polling when nothing is in flight — there's no point\n // hammering the endpoint while the chat is idle. We still poll\n // occasionally so a fresh run started in another tab is picked up.\n if (!data.active || data.status !== \"running\") {\n nextDelay = IDLE_BACKOFF_INTERVAL_MS;\n }\n }\n } catch {\n // Network blip — leave previous state. Next tick will retry.\n }\n if (!cancelled) {\n timer = setTimeout(poll, nextDelay);\n }\n };\n\n // Stagger the first poll so a freshly-started run isn't immediately\n // classified as stuck before the server has had a chance to record\n // any progress events.\n timer = setTimeout(poll, 2_000);\n\n return () => {\n cancelled = true;\n if (timer) clearTimeout(timer);\n };\n }, [threadId, stuckThresholdMs, pollIntervalMs, apiUrl]);\n\n return state;\n}\n\n/**\n * POST `/runs/:id/abort` so the server flips the run to \"aborted\" and the\n * adapter's reconnect loop exits cleanly. Returns the run id that was\n * aborted (or null on failure) so callers can correlate observability\n * events. Best-effort — failures are swallowed, since the user's intent\n * is already captured locally.\n */\nexport function useAbortRun(apiUrl?: string) {\n return useCallback(\n async (runId: string, reason: string = \"user\"): Promise<string | null> => {\n const base = apiUrl ?? agentNativePath(\"/_agent-native/agent-chat\");\n try {\n const res = await fetch(\n `${base}/runs/${encodeURIComponent(runId)}/abort`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n credentials: \"same-origin\",\n body: JSON.stringify({ reason }),\n },\n );\n if (!res.ok) return null;\n return runId;\n } catch {\n return null;\n }\n },\n [apiUrl],\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"agent-chat-plugin.d.ts","sourceRoot":"","sources":["../../src/server/agent-chat-plugin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-chat-plugin.d.ts","sourceRoot":"","sources":["../../src/server/agent-chat-plugin.ts"],"names":[],"mappings":"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"}
@@ -2991,13 +2991,65 @@ export function createAgentChatPlugin(options) {
2991
2991
  : undefined;
2992
2992
  return buildRuntimeContextPrompt({ timezone });
2993
2993
  };
2994
+ // Chat-in-browser-on-localdev is the one surface where the agent must
2995
+ // not edit code: source-file edits trigger Vite HMR / page reloads and
2996
+ // kill the chat session mid-run. The client sends an
2997
+ // `x-agent-native-surface` header (desktop | frame | browser); we fall
2998
+ // back to UA + Host inspection when the header is missing (older clients,
2999
+ // server-to-server callers, etc.). Returning true forces the prod
3000
+ // handler (no shell / no fs) AND injects a redirect-prompt block telling
3001
+ // the agent to point users at Desktop / Claude Code / Codex / Builder.io.
3002
+ const isChatInBrowserOnLocalDev = (event) => {
3003
+ const surface = (getHeader(event, "x-agent-native-surface") || "").toLowerCase();
3004
+ const ua = getHeader(event, "user-agent") || "";
3005
+ const isDesktop = surface === "desktop" || /AgentNativeDesktop/i.test(ua);
3006
+ if (isDesktop)
3007
+ return false;
3008
+ if (surface === "frame")
3009
+ return false;
3010
+ const host = (getHeader(event, "host") || "").toLowerCase();
3011
+ const hostname = host.split(":")[0] ?? "";
3012
+ const isLocal = hostname === "localhost" ||
3013
+ hostname === "127.0.0.1" ||
3014
+ hostname === "::1" ||
3015
+ hostname === "[::1]";
3016
+ if (!isLocal)
3017
+ return false;
3018
+ // No header from an older client + non-desktop UA: be conservative and
3019
+ // only trip on plain browser UAs. Treat unknown clients as safe (frame
3020
+ // / desktop / scripting) so we don't break their tool access.
3021
+ if (!surface) {
3022
+ return /Mozilla\/|Chrome\/|Safari\/|Firefox\/|Edg\//i.test(ua);
3023
+ }
3024
+ return surface === "browser";
3025
+ };
3026
+ const CHAT_IN_BROWSER_LOCAL_DEV_PROMPT = `
3027
+
3028
+ <chat-in-browser-on-localdev>
3029
+ This chat is running in a plain browser tab on localhost. Source-code edits would trigger Vite HMR or a full page reload, which kills the chat session mid-run, so source-code work cannot happen on this surface.
3030
+
3031
+ When the user asks for ANY of the following — add a feature, edit a component, fix a bug in the app itself, change styles, add a route, scaffold a new app, run shell commands that modify code, or anything else that requires touching source files:
3032
+
3033
+ 1. Do NOT call \`connect-builder\`, \`scaffold-workspace-app\`, \`start-workspace-app-creation\`, or any other tool that creates or edits source.
3034
+ 2. Do NOT write code, list files, propose patches, or describe what you would change.
3035
+ 3. Reply with one short message saying chat-in-browser on localhost can't edit code (page reloads kill the session) and offer these alternatives, in this order:
3036
+ - **Agent Native Desktop** — https://www.agent-native.com/download (recommended; same chat, no reload risk)
3037
+ - **Claude Code** — \`claude\` in the project directory
3038
+ - **Codex** — \`codex\` in the project directory
3039
+ - **Builder.io** — open the project in Builder for cloud-based code changes
3040
+
3041
+ Non-code requests are still fine on this surface — read data, navigate the UI, summarize, search, create/update extensions (sandboxed Alpine.js mini-apps stored in SQL), and call template actions. The restriction is specifically about editing the app's own source files.
3042
+ </chat-in-browser-on-localdev>`;
2994
3043
  const prodHandler = createProductionAgentHandler({
2995
3044
  actions: leanPrompt ? leanActions : prodActions,
2996
3045
  systemPrompt: async (event) => {
2997
3046
  const { owner, extra } = await prepareRun(event);
2998
3047
  const runtimeContext = runtimeContextForEvent(event);
3048
+ const browserLocalDev = isChatInBrowserOnLocalDev(event)
3049
+ ? CHAT_IN_BROWSER_LOCAL_DEV_PROMPT
3050
+ : "";
2999
3051
  if (leanPrompt) {
3000
- return setSystemPromptOnContext(leanBasePrompt + runtimeContext + extra);
3052
+ return setSystemPromptOnContext(leanBasePrompt + runtimeContext + browserLocalDev + extra);
3001
3053
  }
3002
3054
  const resources = await loadResourcesForPrompt(owner, lazyContext);
3003
3055
  // In lazy context mode, skip embedding the full schema — the agent
@@ -3005,7 +3057,12 @@ export function createAgentChatPlugin(options) {
3005
3057
  const schemaBlock = lazyContext
3006
3058
  ? ""
3007
3059
  : await buildSchemaBlock(owner, false);
3008
- return setSystemPromptOnContext(basePrompt + runtimeContext + resources + schemaBlock + extra);
3060
+ return setSystemPromptOnContext(basePrompt +
3061
+ runtimeContext +
3062
+ resources +
3063
+ schemaBlock +
3064
+ browserLocalDev +
3065
+ extra);
3009
3066
  },
3010
3067
  model: options?.model,
3011
3068
  apiKey: options?.apiKey,
@@ -3732,6 +3789,7 @@ export function createAgentChatPlugin(options) {
3732
3789
  threadId,
3733
3790
  status: "idle",
3734
3791
  heartbeatAt: null,
3792
+ lastProgressAt: null,
3735
3793
  };
3736
3794
  }
3737
3795
  return {
@@ -3740,6 +3798,7 @@ export function createAgentChatPlugin(options) {
3740
3798
  threadId: run.threadId,
3741
3799
  status: run.status,
3742
3800
  heartbeatAt: run.heartbeatAt,
3801
+ lastProgressAt: run.lastProgressAt,
3743
3802
  };
3744
3803
  }
3745
3804
  setResponseStatus(event, 405);
@@ -4022,9 +4081,15 @@ export function createAgentChatPlugin(options) {
4022
4081
  orgId: resolvedOrgId,
4023
4082
  timezone,
4024
4083
  }, () => {
4084
+ // Chat-in-browser on localhost can't host code edits — Vite HMR
4085
+ // and full reloads would kill the chat mid-run. Force the prod
4086
+ // handler (no shell / no fs); the prompt block injected by
4087
+ // `prodHandler.systemPrompt` then steers the agent to suggest
4088
+ // Desktop / Claude Code / Codex / Builder.io instead.
4089
+ const browserLocalDev = isChatInBrowserOnLocalDev(event);
4025
4090
  const handler = ownerContext.anonymous && anonymousHandler
4026
4091
  ? anonymousHandler
4027
- : currentDevMode && devHandler
4092
+ : !browserLocalDev && currentDevMode && devHandler
4028
4093
  ? devHandler
4029
4094
  : prodHandler;
4030
4095
  return handler(event);