@agent-native/core 0.14.1 → 0.14.3

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 (38) hide show
  1. package/dist/agent/run-manager.d.ts.map +1 -1
  2. package/dist/agent/run-manager.js +11 -5
  3. package/dist/agent/run-manager.js.map +1 -1
  4. package/dist/agent/run-store.d.ts +18 -0
  5. package/dist/agent/run-store.d.ts.map +1 -1
  6. package/dist/agent/run-store.js +66 -8
  7. package/dist/agent/run-store.js.map +1 -1
  8. package/dist/client/AgentPanel.d.ts.map +1 -1
  9. package/dist/client/AgentPanel.js +10 -0
  10. package/dist/client/AgentPanel.js.map +1 -1
  11. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  12. package/dist/client/MultiTabAssistantChat.js +19 -10
  13. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  14. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  15. package/dist/client/composer/PromptComposer.js +28 -3
  16. package/dist/client/composer/PromptComposer.js.map +1 -1
  17. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  18. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  19. package/dist/client/use-chat-threads.d.ts.map +1 -1
  20. package/dist/client/use-chat-threads.js +114 -72
  21. package/dist/client/use-chat-threads.js.map +1 -1
  22. package/dist/server/auth.d.ts.map +1 -1
  23. package/dist/server/auth.js +12 -1
  24. package/dist/server/auth.js.map +1 -1
  25. package/dist/server/google-auth-plugin.d.ts.map +1 -1
  26. package/dist/server/google-auth-plugin.js +40 -1
  27. package/dist/server/google-auth-plugin.js.map +1 -1
  28. package/dist/server/google-oauth.d.ts.map +1 -1
  29. package/dist/server/google-oauth.js +8 -7
  30. package/dist/server/google-oauth.js.map +1 -1
  31. package/dist/server/oauth-return-url.d.ts +6 -0
  32. package/dist/server/oauth-return-url.d.ts.map +1 -0
  33. package/dist/server/oauth-return-url.js +78 -0
  34. package/dist/server/oauth-return-url.js.map +1 -0
  35. package/dist/server/onboarding-html.d.ts.map +1 -1
  36. package/dist/server/onboarding-html.js +40 -1
  37. package/dist/server/onboarding-html.js.map +1 -1
  38. package/package.json +1 -1
@@ -7,15 +7,18 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
7
7
  : ACTIVE_THREAD_KEY;
8
8
  const [threads, setThreads] = useState([]);
9
9
  // IDs we generated client-side this session — consumers use this to know
10
- // whether to skip the per-thread restore skeleton. Tracked by ref instead
11
- // of state because the consumer reads it inside the render path and we
12
- // never need to re-render when the set changes.
10
+ // whether to skip the per-thread restore skeleton, and we use it to
11
+ // protect the optimistic-only thread from being yanked out of local
12
+ // state when the server's threads list (which never sees it) loads.
13
13
  const newlyCreatedRef = useRef(new Set());
14
14
  // Restore the saved active thread synchronously on mount so the chat shell
15
15
  // can paint immediately. We do NOT synthesize a fresh UUID here when no
16
- // saved id exists — that races with the agent run's server-side thread
17
- // create and was producing ghost threads that disappeared from history.
18
- // First-time users see the brief loading state while threads load.
16
+ // saved id exists — that flow was creating empty `chat_threads` rows on
17
+ // every page load via the optimistic POST, even if the user never chatted.
18
+ // (Steve's account had 127 threads; 112 had message_count=0 and zero
19
+ // agent_runs — pure ghosts.) When localStorage is empty, the initial
20
+ // useEffect picks the most-recent server thread, or synthesizes a brand
21
+ // new id only when there are no server threads at all.
19
22
  const [activeThreadId, setActiveThreadId] = useState(() => {
20
23
  if (typeof window === "undefined")
21
24
  return null;
@@ -50,11 +53,35 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
50
53
  const data = await res.json();
51
54
  setThreads((prev) => {
52
55
  const loaded = (data.threads ?? []);
53
- // Preserve any optimistic threads we've created this session that
54
- // haven't shown up in the server list yet (POST still in-flight).
55
56
  const loadedIds = new Set(loaded.map((t) => t.id));
57
+ // Preserve any optimistic threads we've created this session that
58
+ // haven't shown up in the server list yet — the server only learns
59
+ // about a thread when the user actually sends a message and the
60
+ // agent run's `persistSubmittedUserMessage` writes the row.
56
61
  const optimisticOnly = prev.filter((t) => newlyCreatedRef.current.has(t.id) && !loadedIds.has(t.id));
57
- return [...optimisticOnly, ...loaded];
62
+ // Reconcile each server thread against our local copy. If the local
63
+ // copy has a newer updatedAt or higher messageCount, keep those
64
+ // fields — the server probably hasn't observed the user's latest
65
+ // send yet, and naively replacing makes the recent-chats list
66
+ // visibly jump back to older timestamps right after a send.
67
+ const merged = loaded.map((server) => {
68
+ const local = prev.find((t) => t.id === server.id);
69
+ if (!local)
70
+ return server;
71
+ const next = { ...server };
72
+ if (local.updatedAt > server.updatedAt) {
73
+ next.updatedAt = local.updatedAt;
74
+ }
75
+ if (local.messageCount > server.messageCount) {
76
+ next.messageCount = local.messageCount;
77
+ if (local.preview)
78
+ next.preview = local.preview;
79
+ if (local.title)
80
+ next.title = local.title;
81
+ }
82
+ return next;
83
+ });
84
+ return [...optimisticOnly, ...merged];
58
85
  });
59
86
  return data.threads;
60
87
  }
@@ -62,13 +89,16 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
62
89
  return undefined;
63
90
  }
64
91
  }, [apiUrl]);
65
- // Persist a client-generated thread to the server in the background.
66
- // Optimistically adds it to the local thread list so callers can render
67
- // immediately. POST /threads is idempotent server-side (returns the
68
- // existing thread on UNIQUE conflict instead of 500), so client retries
69
- // and races with the agent run's `persistSubmittedUserMessage` no longer
70
- // wipe a freshly-created thread out of local state.
71
- const persistNewThread = useCallback((id) => {
92
+ // Add a client-generated thread to the local list optimistically.
93
+ //
94
+ // Critically, this does NOT `POST /threads` to the server that path was
95
+ // creating an empty row in `chat_threads` (message_count=0, no
96
+ // agent_runs) on every page mount and every "+" click. The server
97
+ // already creates the row idempotently the moment the user actually
98
+ // sends their first message (`persistSubmittedUserMessage`
99
+ // `createThread`), so the client doesn't need to pre-create it. This
100
+ // makes the threads table reflect real conversations only.
101
+ const addOptimisticThread = useCallback((id) => {
72
102
  const now = Date.now();
73
103
  const optimistic = {
74
104
  id,
@@ -79,30 +109,21 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
79
109
  updatedAt: now,
80
110
  };
81
111
  setThreads((prev) => prev.some((t) => t.id === id) ? prev : [optimistic, ...prev]);
82
- fetch(`${apiUrl}/threads`, {
83
- method: "POST",
84
- headers: { "Content-Type": "application/json" },
85
- body: JSON.stringify({ id }),
86
- })
87
- .then(async (res) => {
88
- if (!res.ok)
89
- return;
90
- const created = (await res
91
- .json()
92
- .catch(() => null));
93
- if (!created)
94
- return;
95
- setThreads((prev) => prev.map((thread) => (thread.id === id ? created : thread)));
96
- })
97
- .catch(() => {
98
- // Network blip — keep the optimistic row and let the next
99
- // fetchThreads reconcile. Don't yank the thread out of local
100
- // state: the agent's onRunPrepared will (idempotently) create
101
- // it server-side anyway and we don't want to drop the user's
102
- // active conversation just because POST /threads was flaky.
103
- });
104
- }, [apiUrl]);
105
- // Initial load.
112
+ }, []);
113
+ // Initial load: load threads from server, then reconcile against the
114
+ // saved active thread.
115
+ //
116
+ // - savedId in loadedThreads → keep it (user's last conversation).
117
+ // - savedId in newlyCreatedRef (we just created it this session) keep
118
+ // it; the server hasn't seen it yet because there's no POST anymore,
119
+ // the row gets written when the user sends a message.
120
+ // - savedId is set but neither on the server nor newly created here
121
+ // it's a stale id from a previous session whose row no longer exists
122
+ // (was a ghost cleaned up, or the user emptied their account, etc.).
123
+ // Drop them on the most-recent real thread instead of leaving them
124
+ // staring at a 404'd composer.
125
+ // - No savedId, no server threads synthesize a fresh local id (no
126
+ // POST; server creates the row on first message).
106
127
  useEffect(() => {
107
128
  if (fetchedRef.current)
108
129
  return;
@@ -110,40 +131,41 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
110
131
  (async () => {
111
132
  setIsLoading(true);
112
133
  const loadedThreads = await fetchThreads();
134
+ const savedId = activeThreadIdRef.current;
113
135
  if (loadedThreads && loadedThreads.length > 0) {
114
- const savedId = activeThreadIdRef.current;
115
- // If the saved active thread isn't on the server, fall back to the
116
- // most recent so the user lands on real history instead of a ghost.
117
- if (!savedId || !loadedThreads.find((t) => t.id === savedId)) {
136
+ if (savedId &&
137
+ !newlyCreatedRef.current.has(savedId) &&
138
+ !loadedThreads.find((t) => t.id === savedId)) {
139
+ setActiveThreadId(loadedThreads[0].id);
140
+ }
141
+ else if (!savedId) {
118
142
  setActiveThreadId(loadedThreads[0].id);
119
143
  }
120
144
  }
121
- else if (!activeThreadIdRef.current) {
122
- // No threads on server and no saved active id create a fresh one
123
- // so the composer has something to send into. This matches the
124
- // pre-instant-paint behavior.
125
- try {
126
- const res = await fetch(`${apiUrl}/threads`, { method: "POST" });
127
- if (res.ok) {
128
- const thread = (await res.json());
129
- newlyCreatedRef.current.add(thread.id);
130
- setThreads([thread]);
131
- setActiveThreadId(thread.id);
132
- }
145
+ else if (!savedId) {
146
+ // Brand new user synthesize a local id so the composer has a
147
+ // target. No POST: the server creates the row on first send.
148
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
149
+ const id = crypto.randomUUID();
150
+ newlyCreatedRef.current.add(id);
151
+ addOptimisticThread(id);
152
+ setActiveThreadId(id);
133
153
  }
134
- catch { }
135
154
  }
136
155
  setIsLoading(false);
137
156
  })();
138
- }, [fetchThreads, apiUrl]);
157
+ }, [fetchThreads, addOptimisticThread]);
139
158
  const createThread = useCallback((preferredId) => {
140
- // Generate ID client-side for instant UI response
159
+ // Generate ID client-side for instant UI response. No POST — the
160
+ // server creates the row when the user actually sends a message,
161
+ // which prevents accumulation of empty thread rows when the user
162
+ // clicks "+" but never chats.
141
163
  const id = preferredId || crypto.randomUUID();
142
164
  newlyCreatedRef.current.add(id);
165
+ addOptimisticThread(id);
143
166
  setActiveThreadId(id);
144
- persistNewThread(id);
145
167
  return Promise.resolve(id);
146
- }, [persistNewThread]);
168
+ }, [addOptimisticThread]);
147
169
  const isNewThread = useCallback((id) => newlyCreatedRef.current.has(id), []);
148
170
  const switchThread = useCallback((id) => {
149
171
  setActiveThreadId(id);
@@ -177,18 +199,38 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
177
199
  headers: { "Content-Type": "application/json" },
178
200
  body: JSON.stringify(data),
179
201
  });
180
- // Update local thread list metadata
181
- setThreads((prev) => prev.map((t) => t.id === id
182
- ? {
183
- ...t,
184
- title: data.title,
185
- preview: data.preview,
186
- ...(data.messageCount != null && {
187
- messageCount: data.messageCount,
188
- }),
189
- updatedAt: Date.now(),
202
+ // Update local thread list metadata. If the thread isn't in our
203
+ // local list yet (an optimistic-only thread that the server just
204
+ // created via persistSubmittedUserMessage), add it so HistoryPopover
205
+ // can show it once it has messages.
206
+ setThreads((prev) => {
207
+ const exists = prev.some((t) => t.id === id);
208
+ if (exists) {
209
+ return prev.map((t) => t.id === id
210
+ ? {
211
+ ...t,
212
+ title: data.title,
213
+ preview: data.preview,
214
+ ...(data.messageCount != null && {
215
+ messageCount: data.messageCount,
216
+ }),
217
+ updatedAt: Date.now(),
218
+ }
219
+ : t);
190
220
  }
191
- : t));
221
+ const now = Date.now();
222
+ return [
223
+ {
224
+ id,
225
+ title: data.title,
226
+ preview: data.preview,
227
+ messageCount: data.messageCount ?? 0,
228
+ createdAt: now,
229
+ updatedAt: now,
230
+ },
231
+ ...prev,
232
+ ];
233
+ });
192
234
  }
193
235
  catch { }
194
236
  }, [apiUrl]);
@@ -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\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 // Restore the saved active thread synchronously on mount so the chat shell\n // can paint immediately. We do NOT synthesize a fresh UUID here when no\n // saved id exists — that races with the agent run's server-side thread\n // create and was producing ghost threads that disappeared from history.\n // First-time users see the brief loading state while threads load.\n const [activeThreadId, setActiveThreadId] = useState<string | null>(() => {\n if (typeof window === \"undefined\") return 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 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. POST /threads is idempotent server-side (returns the\n // existing thread on UNIQUE conflict instead of 500), so client retries\n // and races with the agent run's `persistSubmittedUserMessage` no longer\n // wipe a freshly-created thread out of local state.\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) return;\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 // Network blip — keep the optimistic row and let the next\n // fetchThreads reconcile. Don't yank the thread out of local\n // state: the agent's onRunPrepared will (idempotently) create\n // it server-side anyway and we don't want to drop the user's\n // active conversation just because POST /threads was flaky.\n });\n },\n [apiUrl],\n );\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 const savedId = activeThreadIdRef.current;\n // If the saved active thread isn't on the server, fall back to the\n // most recent so the user lands on real history instead of a ghost.\n if (!savedId || !loadedThreads.find((t) => t.id === savedId)) {\n setActiveThreadId(loadedThreads[0].id);\n }\n } else if (!activeThreadIdRef.current) {\n // No threads on server and no saved active id — create a fresh one\n // so the composer has something to send into. This matches the\n // pre-instant-paint behavior.\n try {\n const res = await fetch(`${apiUrl}/threads`, { method: \"POST\" });\n if (res.ok) {\n const thread = (await res.json()) as ChatThreadSummary;\n newlyCreatedRef.current.add(thread.id);\n setThreads([thread]);\n setActiveThreadId(thread.id);\n }\n } catch {}\n }\n setIsLoading(false);\n })();\n }, [fetchThreads, apiUrl]);\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 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 isNewThread,\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\";\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, and we use it to\n // protect the optimistic-only thread from being yanked out of local\n // state when the server's threads list (which never sees it) loads.\n const newlyCreatedRef = useRef<Set<string>>(new Set());\n\n // Restore the saved active thread synchronously on mount so the chat shell\n // can paint immediately. We do NOT synthesize a fresh UUID here when no\n // saved id exists — that flow was creating empty `chat_threads` rows on\n // every page load via the optimistic POST, even if the user never chatted.\n // (Steve's account had 127 threads; 112 had message_count=0 and zero\n // agent_runs — pure ghosts.) When localStorage is empty, the initial\n // useEffect picks the most-recent server thread, or synthesizes a brand\n // new id only when there are no server threads at all.\n const [activeThreadId, setActiveThreadId] = useState<string | null>(() => {\n if (typeof window === \"undefined\") return 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 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 const loadedIds = new Set(loaded.map((t) => t.id));\n // Preserve any optimistic threads we've created this session that\n // haven't shown up in the server list yet — the server only learns\n // about a thread when the user actually sends a message and the\n // agent run's `persistSubmittedUserMessage` writes the row.\n const optimisticOnly = prev.filter(\n (t) => newlyCreatedRef.current.has(t.id) && !loadedIds.has(t.id),\n );\n // Reconcile each server thread against our local copy. If the local\n // copy has a newer updatedAt or higher messageCount, keep those\n // fields — the server probably hasn't observed the user's latest\n // send yet, and naively replacing makes the recent-chats list\n // visibly jump back to older timestamps right after a send.\n const merged = loaded.map((server) => {\n const local = prev.find((t) => t.id === server.id);\n if (!local) return server;\n const next = { ...server };\n if (local.updatedAt > server.updatedAt) {\n next.updatedAt = local.updatedAt;\n }\n if (local.messageCount > server.messageCount) {\n next.messageCount = local.messageCount;\n if (local.preview) next.preview = local.preview;\n if (local.title) next.title = local.title;\n }\n return next;\n });\n return [...optimisticOnly, ...merged];\n });\n return data.threads as ChatThreadSummary[];\n } catch {\n return undefined;\n }\n }, [apiUrl]);\n\n // Add a client-generated thread to the local list optimistically.\n //\n // Critically, this does NOT `POST /threads` to the server — that path was\n // creating an empty row in `chat_threads` (message_count=0, no\n // agent_runs) on every page mount and every \"+\" click. The server\n // already creates the row idempotently the moment the user actually\n // sends their first message (`persistSubmittedUserMessage` →\n // `createThread`), so the client doesn't need to pre-create it. This\n // makes the threads table reflect real conversations only.\n const addOptimisticThread = useCallback((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 }, []);\n\n // Initial load: load threads from server, then reconcile against the\n // saved active thread.\n //\n // - savedId in loadedThreads → keep it (user's last conversation).\n // - savedId in newlyCreatedRef (we just created it this session) → keep\n // it; the server hasn't seen it yet because there's no POST anymore,\n // the row gets written when the user sends a message.\n // - savedId is set but neither on the server nor newly created here →\n // it's a stale id from a previous session whose row no longer exists\n // (was a ghost cleaned up, or the user emptied their account, etc.).\n // Drop them on the most-recent real thread instead of leaving them\n // staring at a 404'd composer.\n // - No savedId, no server threads → synthesize a fresh local id (no\n // POST; server creates the row on first message).\n useEffect(() => {\n if (fetchedRef.current) return;\n fetchedRef.current = true;\n\n (async () => {\n setIsLoading(true);\n const loadedThreads = await fetchThreads();\n const savedId = activeThreadIdRef.current;\n\n if (loadedThreads && loadedThreads.length > 0) {\n if (\n savedId &&\n !newlyCreatedRef.current.has(savedId) &&\n !loadedThreads.find((t) => t.id === savedId)\n ) {\n setActiveThreadId(loadedThreads[0].id);\n } else if (!savedId) {\n setActiveThreadId(loadedThreads[0].id);\n }\n } else if (!savedId) {\n // Brand new user — synthesize a local id so the composer has a\n // target. No POST: the server creates the row on first send.\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n const id = crypto.randomUUID();\n newlyCreatedRef.current.add(id);\n addOptimisticThread(id);\n setActiveThreadId(id);\n }\n }\n setIsLoading(false);\n })();\n }, [fetchThreads, addOptimisticThread]);\n\n const createThread = useCallback(\n (preferredId?: string): Promise<string | null> => {\n // Generate ID client-side for instant UI response. No POST — the\n // server creates the row when the user actually sends a message,\n // which prevents accumulation of empty thread rows when the user\n // clicks \"+\" but never chats.\n const id = preferredId || crypto.randomUUID();\n newlyCreatedRef.current.add(id);\n addOptimisticThread(id);\n setActiveThreadId(id);\n return Promise.resolve(id);\n },\n [addOptimisticThread],\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 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. If the thread isn't in our\n // local list yet (an optimistic-only thread that the server just\n // created via persistSubmittedUserMessage), add it so HistoryPopover\n // can show it once it has messages.\n setThreads((prev) => {\n const exists = prev.some((t) => t.id === id);\n if (exists) {\n return 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 const now = Date.now();\n return [\n {\n id,\n title: data.title,\n preview: data.preview,\n messageCount: data.messageCount ?? 0,\n createdAt: now,\n updatedAt: now,\n },\n ...prev,\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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"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"}
@@ -41,6 +41,7 @@ import { getOnboardingHtml, getResetPasswordHtml } from "./onboarding-html.js";
41
41
  import { readBody } from "../server/h3-helpers.js";
42
42
  import { readDesktopSso, writeDesktopSso, clearDesktopSso, } from "./desktop-sso.js";
43
43
  import { isElectron as isElectronRequest, getAppBasePath, getAppUrl, encodeOAuthState, decodeOAuthState, createOAuthSession, oauthCallbackResponse, oauthErrorPage, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, } from "./google-oauth.js";
44
+ import { safeOAuthReturnUrl } from "./oauth-return-url.js";
44
45
  import { captureAuthError } from "./sentry.js";
45
46
  import { extractOAuthStateAppId } from "../shared/oauth-state.js";
46
47
  import { isValidWorkspaceAppIdFormat } from "../shared/workspace-app-id.js";
@@ -140,6 +141,12 @@ function oauthDebugUrlPath(value) {
140
141
  return undefined;
141
142
  }
142
143
  }
144
+ function isBuilderOAuthRequest(event) {
145
+ const userAgent = getHeader(event, "user-agent") || "";
146
+ const referer = getHeader(event, "referer") || "";
147
+ return (/Electron/i.test(userAgent) ||
148
+ /builder\.(io|my)|builderio\.(xyz|dev)/i.test(referer));
149
+ }
143
150
  function logGoogleOAuthDebug(event, phase, details = {}) {
144
151
  const { flowId, ...rest } = details;
145
152
  const reqUrl = event.node?.req?.url ?? event.path ?? "";
@@ -1405,7 +1412,11 @@ async function mountBetterAuthRoutes(app, options) {
1405
1412
  // skip embedding "/" (the default fallback) so the state stays
1406
1413
  // small for the common case.
1407
1414
  const returnQuery = q.return;
1408
- const validated = typeof returnQuery === "string" ? safeReturnPath(returnQuery) : "/";
1415
+ const validated = typeof returnQuery === "string"
1416
+ ? safeOAuthReturnUrl(returnQuery, {
1417
+ allowDefaultLoopback: isBuilderOAuthRequest(event),
1418
+ })
1419
+ : "/";
1409
1420
  const returnUrl = validated !== "/" ? validated : undefined;
1410
1421
  const state = encodeOAuthState({
1411
1422
  redirectUri,