@agent-native/core 0.68.0 → 0.68.2

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 (152) hide show
  1. package/README.md +4 -4
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +18 -0
  4. package/corpus/core/docs/content/authentication.md +16 -2
  5. package/corpus/core/docs/content/template-analytics.md +14 -9
  6. package/corpus/core/docs/content/template-clips.md +30 -2
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/agent/engine/builder-engine.ts +22 -3
  9. package/corpus/core/src/agent/engine/types.ts +1 -0
  10. package/corpus/core/src/agent/production-agent.ts +2 -0
  11. package/corpus/core/src/agent/types.ts +1 -0
  12. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  13. package/corpus/core/src/client/AssistantChat.tsx +45 -6
  14. package/corpus/core/src/client/CommandMenu.tsx +98 -3
  15. package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
  16. package/corpus/core/src/client/active-run-state.ts +16 -0
  17. package/corpus/core/src/client/guided-questions.tsx +73 -17
  18. package/corpus/core/src/client/index.ts +2 -0
  19. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  20. package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
  21. package/corpus/core/src/provider-api/index.ts +37 -17
  22. package/corpus/core/src/server/auth.ts +6 -8
  23. package/corpus/core/src/server/better-auth-instance.ts +19 -4
  24. package/corpus/core/src/server/core-routes-plugin.ts +157 -5
  25. package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
  26. package/corpus/core/src/server/index.ts +8 -0
  27. package/corpus/core/src/server/onboarding-html.ts +2 -1
  28. package/corpus/core/src/templates/workspace-root/.env.example +4 -0
  29. package/corpus/templates/analytics/app/routes/$.tsx +9 -0
  30. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
  31. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
  32. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
  33. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
  34. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
  35. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
  36. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
  37. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
  38. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
  39. package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
  40. package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
  41. package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
  42. package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
  43. package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
  44. package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
  45. package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
  46. package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
  47. package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
  48. package/corpus/templates/calendar/shared/api.ts +4 -0
  49. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
  50. package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
  51. package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
  52. package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
  53. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
  54. package/corpus/templates/clips/app/lib/capture-install-options.ts +13 -0
  55. package/corpus/templates/clips/app/root.tsx +54 -2
  56. package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
  57. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
  58. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
  59. package/corpus/templates/clips/app/routes/download.tsx +49 -0
  60. package/corpus/templates/clips/app/routes/record.tsx +4 -4
  61. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
  62. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +26 -0
  63. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
  64. package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
  65. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
  66. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
  67. package/corpus/templates/design/actions/show-design-questions.ts +15 -1
  68. package/corpus/templates/design/app/hooks/use-agent-generating.ts +47 -8
  69. package/corpus/templates/design/app/pages/DesignEditor.tsx +6 -0
  70. package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
  71. package/corpus/templates/design/app/routes/$.tsx +9 -0
  72. package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
  73. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  74. package/corpus/templates/plan/app/global.css +2 -2
  75. package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
  76. package/corpus/templates/slides/app/routes/$.tsx +9 -0
  77. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
  78. package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
  79. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  80. package/dist/agent/engine/builder-engine.js +21 -3
  81. package/dist/agent/engine/builder-engine.js.map +1 -1
  82. package/dist/agent/engine/types.d.ts +2 -0
  83. package/dist/agent/engine/types.d.ts.map +1 -1
  84. package/dist/agent/engine/types.js.map +1 -1
  85. package/dist/agent/production-agent.d.ts.map +1 -1
  86. package/dist/agent/production-agent.js +3 -0
  87. package/dist/agent/production-agent.js.map +1 -1
  88. package/dist/agent/types.d.ts +2 -0
  89. package/dist/agent/types.d.ts.map +1 -1
  90. package/dist/agent/types.js.map +1 -1
  91. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  92. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  93. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  94. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts.map +1 -1
  96. package/dist/client/AssistantChat.js +42 -7
  97. package/dist/client/AssistantChat.js.map +1 -1
  98. package/dist/client/CommandMenu.d.ts +15 -2
  99. package/dist/client/CommandMenu.d.ts.map +1 -1
  100. package/dist/client/CommandMenu.js +46 -3
  101. package/dist/client/CommandMenu.js.map +1 -1
  102. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  103. package/dist/client/ConnectBuilderCard.js +11 -2
  104. package/dist/client/ConnectBuilderCard.js.map +1 -1
  105. package/dist/client/active-run-state.d.ts +2 -0
  106. package/dist/client/active-run-state.d.ts.map +1 -1
  107. package/dist/client/active-run-state.js +10 -0
  108. package/dist/client/active-run-state.js.map +1 -1
  109. package/dist/client/guided-questions.d.ts +2 -0
  110. package/dist/client/guided-questions.d.ts.map +1 -1
  111. package/dist/client/guided-questions.js +52 -7
  112. package/dist/client/guided-questions.js.map +1 -1
  113. package/dist/client/index.d.ts +1 -1
  114. package/dist/client/index.d.ts.map +1 -1
  115. package/dist/client/index.js.map +1 -1
  116. package/dist/client/sse-event-processor.d.ts +1 -1
  117. package/dist/client/sse-event-processor.d.ts.map +1 -1
  118. package/dist/client/sse-event-processor.js +7 -1
  119. package/dist/client/sse-event-processor.js.map +1 -1
  120. package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
  121. package/dist/oauth-tokens/google-refresh.js +29 -17
  122. package/dist/oauth-tokens/google-refresh.js.map +1 -1
  123. package/dist/provider-api/index.d.ts.map +1 -1
  124. package/dist/provider-api/index.js +28 -16
  125. package/dist/provider-api/index.js.map +1 -1
  126. package/dist/server/auth.d.ts.map +1 -1
  127. package/dist/server/auth.js +6 -6
  128. package/dist/server/auth.js.map +1 -1
  129. package/dist/server/better-auth-instance.d.ts.map +1 -1
  130. package/dist/server/better-auth-instance.js +18 -4
  131. package/dist/server/better-auth-instance.js.map +1 -1
  132. package/dist/server/core-routes-plugin.d.ts +5 -0
  133. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  134. package/dist/server/core-routes-plugin.js +107 -5
  135. package/dist/server/core-routes-plugin.js.map +1 -1
  136. package/dist/server/google-oauth-credentials.d.ts +15 -0
  137. package/dist/server/google-oauth-credentials.d.ts.map +1 -0
  138. package/dist/server/google-oauth-credentials.js +34 -0
  139. package/dist/server/google-oauth-credentials.js.map +1 -0
  140. package/dist/server/index.d.ts +1 -0
  141. package/dist/server/index.d.ts.map +1 -1
  142. package/dist/server/index.js +1 -0
  143. package/dist/server/index.js.map +1 -1
  144. package/dist/server/onboarding-html.d.ts.map +1 -1
  145. package/dist/server/onboarding-html.js +2 -1
  146. package/dist/server/onboarding-html.js.map +1 -1
  147. package/dist/templates/workspace-root/.env.example +4 -0
  148. package/docs/content/authentication.md +16 -2
  149. package/docs/content/template-analytics.md +14 -9
  150. package/docs/content/template-clips.md +30 -2
  151. package/package.json +1 -1
  152. package/src/templates/workspace-root/.env.example +4 -0
@@ -50,7 +50,12 @@ import type {
50
50
  ChatThreadSnapshot,
51
51
  } from "./use-chat-threads.js";
52
52
  import { useAgentEngineConfigured } from "./use-agent-engine-configured.js";
53
- import { getActiveRun } from "./active-run-state.js";
53
+ import {
54
+ getActiveRun,
55
+ resolveReconnectAfterSeq,
56
+ setActiveRun,
57
+ updateActiveRunSeq,
58
+ } from "./active-run-state.js";
54
59
  import {
55
60
  AgentAutoContinueSignal,
56
61
  type ContentPart,
@@ -1247,6 +1252,7 @@ const AssistantChatInner = forwardRef<
1247
1252
  // When stop is clicked during reconnect, keep content visible (don't wipe it)
1248
1253
  const [reconnectFrozen, setReconnectFrozen] = useState(false);
1249
1254
  const reconnectRunIdRef = useRef<string | null>(null);
1255
+ const [reconnectAfterSeq, setReconnectAfterSeq] = useState(0);
1250
1256
  const reconnectAbortRef = useRef<AbortController | null>(null);
1251
1257
  // Nuclear stop: user clicked stop. Clears the stop button/indicator AND
1252
1258
  // lets new submissions go through immediately — prevents the "stuck
@@ -1456,6 +1462,13 @@ const AssistantChatInner = forwardRef<
1456
1462
  if (reconnectRunIdRef.current === runId) return true;
1457
1463
 
1458
1464
  reconnectRunIdRef.current = runId;
1465
+ const afterSeq = resolveReconnectAfterSeq(threadId, runId);
1466
+ setReconnectAfterSeq(afterSeq);
1467
+ setActiveRun({
1468
+ threadId,
1469
+ runId,
1470
+ lastSeq: afterSeq > 0 ? afterSeq - 1 : -1,
1471
+ });
1459
1472
  setIsReconnecting(true);
1460
1473
  setReconnectFrozen(false);
1461
1474
  setReconnectContent([]);
@@ -1498,9 +1511,16 @@ const AssistantChatInner = forwardRef<
1498
1511
  const streamReconnect = async () => {
1499
1512
  let noProgressDuringReconnect = false;
1500
1513
  let latestContent: ContentPart[] = [];
1514
+ const threadPollInterval =
1515
+ afterSeq > 0
1516
+ ? window.setInterval(() => {
1517
+ if (reconnectRunIdRef.current !== runId) return;
1518
+ void refreshThreadFromServer();
1519
+ }, 2000)
1520
+ : undefined;
1501
1521
  try {
1502
1522
  const sseRes = await fetch(
1503
- `${apiUrl}/runs/${encodeURIComponent(runId)}/events?after=0`,
1523
+ `${apiUrl}/runs/${encodeURIComponent(runId)}/events?after=${afterSeq}`,
1504
1524
  { signal: abortCtrl.signal },
1505
1525
  );
1506
1526
  if (sseRes.ok && sseRes.body) {
@@ -1511,6 +1531,7 @@ const AssistantChatInner = forwardRef<
1511
1531
  let rafPending = false;
1512
1532
  let latestSnapshot: ContentPart[] = [];
1513
1533
  const scheduleUpdate = (snapshot: ContentPart[]) => {
1534
+ if (afterSeq > 0) return;
1514
1535
  latestSnapshot = snapshot;
1515
1536
  if (rafPending) return;
1516
1537
  rafPending = true;
@@ -1526,8 +1547,11 @@ const AssistantChatInner = forwardRef<
1526
1547
  toolCallCounter,
1527
1548
  tabId,
1528
1549
  scheduleUpdate,
1550
+ (seq) => updateActiveRunSeq(seq),
1529
1551
  );
1530
- setReconnectContent([...content]);
1552
+ if (afterSeq === 0) {
1553
+ setReconnectContent([...content]);
1554
+ }
1531
1555
  }
1532
1556
  } catch (err) {
1533
1557
  if (
@@ -1543,6 +1567,9 @@ const AssistantChatInner = forwardRef<
1543
1567
  noProgressDuringReconnect = true;
1544
1568
  }
1545
1569
  } finally {
1570
+ if (threadPollInterval !== undefined) {
1571
+ window.clearInterval(threadPollInterval);
1572
+ }
1546
1573
  clearInterval(watchdog);
1547
1574
  clearTimeout(maxReconnectTimer);
1548
1575
  }
@@ -1570,9 +1597,17 @@ const AssistantChatInner = forwardRef<
1570
1597
  } catch {
1571
1598
  // Best effort — the important part is unwinding the UI.
1572
1599
  }
1573
- settleInterruptedToolCalls(latestContent);
1574
- setReconnectContent([...latestContent]);
1575
- setReconnectFrozen(latestContent.length > 0);
1600
+ if (afterSeq > 0) {
1601
+ // Tail-resume only replays new events; never freeze that slice as a
1602
+ // complete assistant turn — the server thread is authoritative.
1603
+ await refreshThreadFromServer();
1604
+ setReconnectContent([]);
1605
+ setReconnectFrozen(false);
1606
+ } else {
1607
+ settleInterruptedToolCalls(latestContent);
1608
+ setReconnectContent([...latestContent]);
1609
+ setReconnectFrozen(latestContent.length > 0);
1610
+ }
1576
1611
  setRunErrorInfo({
1577
1612
  message:
1578
1613
  "The previous agent run stopped producing visible progress while reconnecting, so it was stopped before it could keep looping.",
@@ -1584,6 +1619,7 @@ const AssistantChatInner = forwardRef<
1584
1619
  reconnectAbortRef.current = null;
1585
1620
  setIsReconnecting(false);
1586
1621
  reconnectRunIdRef.current = null;
1622
+ setReconnectAfterSeq(0);
1587
1623
  window.dispatchEvent(
1588
1624
  new CustomEvent("agentNative.chatRunning", {
1589
1625
  detail: { isRunning: false, tabId: tabId || threadId },
@@ -1610,6 +1646,7 @@ const AssistantChatInner = forwardRef<
1610
1646
  reconnectAbortRef.current = null;
1611
1647
  setIsReconnecting(false);
1612
1648
  reconnectRunIdRef.current = null;
1649
+ setReconnectAfterSeq(0);
1613
1650
  window.dispatchEvent(
1614
1651
  new CustomEvent("agentNative.chatRunning", {
1615
1652
  detail: { isRunning: false, tabId: tabId || threadId },
@@ -2360,6 +2397,7 @@ const AssistantChatInner = forwardRef<
2360
2397
  reconnectAbortRef.current?.abort();
2361
2398
  reconnectAbortRef.current = null;
2362
2399
  reconnectRunIdRef.current = null;
2400
+ setReconnectAfterSeq(0);
2363
2401
  setIsReconnecting(false);
2364
2402
  setReconnectFrozen(reconnectContent.length > 0);
2365
2403
  }
@@ -3159,6 +3197,7 @@ const AssistantChatInner = forwardRef<
3159
3197
  />
3160
3198
  )}
3161
3199
  {(isReconnecting || reconnectFrozen) &&
3200
+ reconnectAfterSeq === 0 &&
3162
3201
  reconnectContent.length > 0 && (
3163
3202
  <ReconnectStreamMessage content={reconnectContent} />
3164
3203
  )}
@@ -24,7 +24,12 @@ import React, {
24
24
  useState,
25
25
  type ReactNode,
26
26
  } from "react";
27
- import { IconSearch, IconMessage } from "@tabler/icons-react";
27
+ import {
28
+ IconBook2,
29
+ IconExternalLink,
30
+ IconSearch,
31
+ IconMessage,
32
+ } from "@tabler/icons-react";
28
33
  import { sendToAgentChat } from "./agent-chat.js";
29
34
  import { cn } from "./utils.js";
30
35
 
@@ -96,6 +101,7 @@ interface CommandItemProps {
96
101
  children: ReactNode;
97
102
  keywords?: string[];
98
103
  className?: string;
104
+ deferSelect?: boolean;
99
105
  }
100
106
 
101
107
  function CommandItem({
@@ -103,12 +109,19 @@ function CommandItem({
103
109
  children,
104
110
  keywords: _keywords,
105
111
  className,
112
+ deferSelect = true,
106
113
  }: CommandItemProps) {
107
114
  const { onOpenChange, containerRef, setSelectedIndex } =
108
115
  useCommandMenuContext();
109
116
  const itemRef = useRef<HTMLDivElement>(null);
110
117
 
111
118
  const handleSelect = () => {
119
+ if (!deferSelect) {
120
+ onSelect();
121
+ onOpenChange(false);
122
+ return;
123
+ }
124
+
112
125
  onOpenChange(false);
113
126
  // Small delay to let dialog close animation start
114
127
  setTimeout(onSelect, 50);
@@ -159,6 +172,68 @@ function CommandSeparator({ className }: { className?: string }) {
159
172
  return <div className={cn("-mx-1 my-1 h-px bg-border", className)} />;
160
173
  }
161
174
 
175
+ export interface CommandMenuDoc {
176
+ title: string;
177
+ href: string;
178
+ description?: string;
179
+ keywords?: string[];
180
+ }
181
+
182
+ interface CommandDocsGroupProps {
183
+ docs: CommandMenuDoc[];
184
+ heading?: string;
185
+ }
186
+
187
+ function commandDocSearchText(doc: CommandMenuDoc): string {
188
+ return [doc.title, doc.description, ...(doc.keywords ?? [])]
189
+ .filter(Boolean)
190
+ .join(" ")
191
+ .toLowerCase();
192
+ }
193
+
194
+ function filterCommandDocs(docs: CommandMenuDoc[], search: string) {
195
+ const searchLower = search.trim().toLowerCase();
196
+ if (!searchLower) return docs;
197
+ return docs.filter((doc) => commandDocSearchText(doc).includes(searchLower));
198
+ }
199
+
200
+ function openDocsHref(href: string) {
201
+ if (/^https?:\/\//i.test(href)) {
202
+ window.open(href, "_blank", "noopener,noreferrer");
203
+ return;
204
+ }
205
+ window.location.assign(href);
206
+ }
207
+
208
+ function CommandDocsGroup({ docs, heading = "Docs" }: CommandDocsGroupProps) {
209
+ if (docs.length === 0) return null;
210
+
211
+ return (
212
+ <CommandGroup heading={heading}>
213
+ {docs.map((doc) => (
214
+ <CommandItem
215
+ key={doc.href}
216
+ onSelect={() => openDocsHref(doc.href)}
217
+ keywords={[doc.title, doc.description ?? "", ...(doc.keywords ?? [])]}
218
+ deferSelect={false}
219
+ className="items-start py-2"
220
+ >
221
+ <IconBook2 className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
222
+ <span className="min-w-0 flex-1">
223
+ <span className="block truncate font-medium">{doc.title}</span>
224
+ {doc.description ? (
225
+ <span className="mt-0.5 block line-clamp-2 text-xs leading-snug text-muted-foreground">
226
+ {doc.description}
227
+ </span>
228
+ ) : null}
229
+ </span>
230
+ <IconExternalLink className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
231
+ </CommandItem>
232
+ ))}
233
+ </CommandGroup>
234
+ );
235
+ }
236
+
162
237
  // ─── Main Component ─────────────────────────────────────────────────────────
163
238
 
164
239
  export interface CommandMenuProps {
@@ -305,6 +380,18 @@ export function CommandMenu({
305
380
  } as Record<string, unknown>);
306
381
  }
307
382
 
383
+ if (child.type === CommandDocsGroup) {
384
+ const docs = Array.isArray(props.docs)
385
+ ? (props.docs as CommandMenuDoc[])
386
+ : [];
387
+ const filteredDocs = filterCommandDocs(docs, search);
388
+ if (filteredDocs.length === 0) return null;
389
+ return React.cloneElement(child, {
390
+ ...props,
391
+ docs: filteredDocs,
392
+ } as Record<string, unknown>);
393
+ }
394
+
308
395
  // If it's a CommandItem, check if it matches search
309
396
  if (child.type === CommandItem) {
310
397
  if (!search) return child;
@@ -330,7 +417,9 @@ export function CommandMenu({
330
417
 
331
418
  const filteredChildren = filterChildren(children);
332
419
  const hasResults = React.Children.toArray(filteredChildren).some(
333
- (child) => React.isValidElement(child) && child.type === CommandGroup,
420
+ (child) =>
421
+ React.isValidElement(child) &&
422
+ (child.type === CommandGroup || child.type === CommandDocsGroup),
334
423
  );
335
424
 
336
425
  return (
@@ -435,6 +524,7 @@ function getTextContent(children: ReactNode): string {
435
524
  // Attach sub-components
436
525
  CommandMenu.Group = CommandGroup;
437
526
  CommandMenu.Item = CommandItem;
527
+ CommandMenu.DocsGroup = CommandDocsGroup;
438
528
  CommandMenu.Shortcut = CommandShortcut;
439
529
  CommandMenu.Separator = CommandSeparator;
440
530
 
@@ -465,4 +555,9 @@ export function useCommandMenuShortcut(onOpen: () => void) {
465
555
  }, [onOpen]);
466
556
  }
467
557
 
468
- export type { CommandGroupProps, CommandItemProps, CommandShortcutProps };
558
+ export type {
559
+ CommandDocsGroupProps,
560
+ CommandGroupProps,
561
+ CommandItemProps,
562
+ CommandShortcutProps,
563
+ };
@@ -142,7 +142,16 @@ export function ConnectBuilderCard({
142
142
  agentNativePath("/_agent-native/builder/branch-waitlist"),
143
143
  origin,
144
144
  ).href,
145
- { method: "POST" },
145
+ {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({
149
+ prompt,
150
+ orgName,
151
+ pageUrl: window.location.href,
152
+ source: "connect_builder_card",
153
+ }),
154
+ },
146
155
  );
147
156
  if (!res.ok) {
148
157
  const data = await res.json().catch(() => ({}));
@@ -160,7 +169,7 @@ export function ConnectBuilderCard({
160
169
  setWaitlistErr(e instanceof Error ? e.message : "Couldn't join waitlist");
161
170
  setJoiningWaitlist(false);
162
171
  }
163
- }, []);
172
+ }, [orgName, prompt]);
164
173
 
165
174
  // Combine connect-flow errors, send errors, and waitlist errors.
166
175
  const err = sendErr ?? waitlistErr ?? flow.error;
@@ -35,3 +35,19 @@ export function clearActiveRun(): void {
35
35
  sessionStorage.removeItem(STORAGE_KEY);
36
36
  } catch {}
37
37
  }
38
+
39
+ /** Resume reconnect SSE after the last seen event (0 = replay from the start). */
40
+ export function resolveReconnectAfterSeq(
41
+ threadId: string,
42
+ runId: string,
43
+ ): number {
44
+ const stored = getActiveRun();
45
+ if (
46
+ stored?.threadId === threadId &&
47
+ stored?.runId === runId &&
48
+ Number.isFinite(stored.lastSeq)
49
+ ) {
50
+ return stored.lastSeq + 1;
51
+ }
52
+ return 0;
53
+ }
@@ -357,6 +357,37 @@ function optionKey(option: GuidedQuestionOption): string {
357
357
  return `${option.value.toLowerCase()}::${option.label.toLowerCase()}`;
358
358
  }
359
359
 
360
+ /** Stable content hash so poll refreshes do not reset in-progress answers. */
361
+ export function guidedQuestionsFingerprint(
362
+ questions: GuidedQuestion[],
363
+ ): string {
364
+ return JSON.stringify(
365
+ questions.map((question) => ({
366
+ id: question.id,
367
+ type: question.type,
368
+ header: question.header ?? null,
369
+ question: question.question,
370
+ description: question.description ?? null,
371
+ multiSelect: question.multiSelect ?? false,
372
+ required: question.required ?? false,
373
+ allowOther: question.allowOther ?? null,
374
+ includeExplore: question.includeExplore ?? null,
375
+ includeDecide: question.includeDecide ?? null,
376
+ min: question.min ?? null,
377
+ max: question.max ?? null,
378
+ step: question.step ?? null,
379
+ placeholder: question.placeholder ?? null,
380
+ options: (question.options ?? question.choices ?? []).map((option) => ({
381
+ label: option.label,
382
+ value: option.value,
383
+ color: option.color ?? null,
384
+ description: option.description ?? null,
385
+ recommended: option.recommended ?? false,
386
+ })),
387
+ })),
388
+ );
389
+ }
390
+
360
391
  function withDefaultOptions(question: GuidedQuestion): GuidedQuestionOption[] {
361
392
  const base = question.options ?? question.choices ?? [];
362
393
  const seen = new Set(base.map(optionKey));
@@ -403,10 +434,14 @@ export function GuidedQuestionFlow({
403
434
  className,
404
435
  }: GuidedQuestionFlowProps) {
405
436
  const [answers, setAnswers] = useState<GuidedQuestionAnswers>({});
437
+ const questionsFingerprint = useMemo(
438
+ () => guidedQuestionsFingerprint(questions),
439
+ [questions],
440
+ );
406
441
 
407
442
  useEffect(() => {
408
443
  setAnswers({});
409
- }, [questions]);
444
+ }, [questionsFingerprint]);
410
445
 
411
446
  const setAnswer = useCallback((id: string, value: unknown) => {
412
447
  setAnswers((prev) => ({ ...prev, [id]: value }));
@@ -663,23 +698,26 @@ function OptionButton({
663
698
  <button
664
699
  type="button"
665
700
  onClick={onClick}
701
+ aria-pressed={selected}
666
702
  className={cn(
667
703
  "group flex min-h-[56px] cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-left transition-colors",
668
704
  selected
669
- ? "border-primary bg-primary/10 text-primary"
705
+ ? "border-primary bg-primary/10 text-foreground ring-2 ring-primary/25"
670
706
  : "border-border bg-muted/30 text-muted-foreground hover:border-muted-foreground/50 hover:bg-muted/45 hover:text-foreground",
671
707
  )}
672
708
  >
673
- {multiSelect && (
674
- <span
675
- className={cn(
676
- "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border",
677
- selected ? "border-primary bg-primary text-primary-foreground" : "",
678
- )}
679
- >
680
- {selected && <IconCheck className="h-3 w-3" />}
681
- </span>
682
- )}
709
+ <span
710
+ className={cn(
711
+ "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center border",
712
+ multiSelect ? "rounded-sm" : "rounded-full",
713
+ selected
714
+ ? "border-primary bg-primary text-primary-foreground"
715
+ : "border-border bg-background",
716
+ )}
717
+ aria-hidden
718
+ >
719
+ {selected && <IconCheck className="h-3 w-3" />}
720
+ </span>
683
721
  <span className="min-w-0 flex-1">
684
722
  <span className="flex flex-wrap items-center gap-1.5 text-sm font-medium leading-5">
685
723
  {option.label}
@@ -940,6 +978,17 @@ export function useGuidedQuestionFlow({
940
978
  [queryKey, normalizedBrowserTabId],
941
979
  );
942
980
 
981
+ const resolvedRefetchInterval =
982
+ refetchInterval === false
983
+ ? false
984
+ : (query: { state: { data?: GuidedQuestionPayload | null } }) => {
985
+ const activeQuestions = query.state.data?.questions;
986
+ if (Array.isArray(activeQuestions) && activeQuestions.length > 0) {
987
+ return false;
988
+ }
989
+ return refetchInterval;
990
+ };
991
+
943
992
  const { data } = useQuery({
944
993
  queryKey: resolvedQueryKey,
945
994
  queryFn: async () => {
@@ -951,9 +1000,7 @@ export function useGuidedQuestionFlow({
951
1000
  try {
952
1001
  const parsed = JSON.parse(text);
953
1002
  if (Array.isArray(parsed?.questions) && parsed.questions.length > 0) {
954
- return { ...parsed, _ts: Date.now() } as GuidedQuestionPayload & {
955
- _ts: number;
956
- };
1003
+ return parsed as GuidedQuestionPayload;
957
1004
  }
958
1005
  } catch {
959
1006
  return null;
@@ -967,13 +1014,22 @@ export function useGuidedQuestionFlow({
967
1014
  (await read(stateKey))
968
1015
  );
969
1016
  },
970
- refetchInterval,
1017
+ refetchInterval: resolvedRefetchInterval,
971
1018
  structuralSharing: false,
972
1019
  });
973
1020
 
974
1021
  useEffect(() => {
975
1022
  if (Array.isArray(data?.questions) && data.questions.length > 0) {
976
- setPayload(data);
1023
+ setPayload((prev) => {
1024
+ if (
1025
+ prev &&
1026
+ guidedQuestionsFingerprint(prev.questions) ===
1027
+ guidedQuestionsFingerprint(data.questions)
1028
+ ) {
1029
+ return prev;
1030
+ }
1031
+ return data;
1032
+ });
977
1033
  } else {
978
1034
  setPayload(null);
979
1035
  }
@@ -707,6 +707,8 @@ export {
707
707
  openAgentSidebar,
708
708
  submitToAgent,
709
709
  type CommandMenuProps,
710
+ type CommandMenuDoc,
711
+ type CommandDocsGroupProps,
710
712
  type CommandGroupProps,
711
713
  type CommandItemProps,
712
714
  type CommandShortcutProps,
@@ -357,6 +357,10 @@ export function processEvent(
357
357
  };
358
358
  }
359
359
 
360
+ if (ev.type === "stream_keepalive") {
361
+ return { action: "continue" };
362
+ }
363
+
360
364
  if (ev.type === "activity") {
361
365
  const tool = ev.tool?.trim() || undefined;
362
366
  const label = humanizeToolLabelText(ev.label ?? "Working", tool);
@@ -868,6 +872,7 @@ export async function readSSEStreamRaw(
868
872
  toolCallCounter: { value: number },
869
873
  tabId: string | undefined,
870
874
  onUpdate: (content: ContentPart[]) => void,
875
+ onSeq?: (seq: number) => void,
871
876
  ): Promise<void> {
872
877
  const reader = body.getReader();
873
878
  const decoder = new TextDecoder();
@@ -907,6 +912,10 @@ export async function readSSEStreamRaw(
907
912
  sawDataEvent = true;
908
913
  lastMeaningfulEventAt = Date.now();
909
914
 
915
+ if (ev.seq !== undefined && onSeq) {
916
+ onSeq(ev.seq);
917
+ }
918
+
910
919
  const { action, autoContinue } = processEvent(
911
920
  ev,
912
921
  content,
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { listOAuthAccounts, saveOAuthTokens } from "./store.js";
15
+ import { resolveGoogleProviderCredentialCandidates } from "../server/google-oauth-credentials.js";
15
16
 
16
17
  const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
17
18
 
@@ -31,29 +32,43 @@ interface RefreshResponse {
31
32
  }
32
33
 
33
34
  async function refreshOne(refreshToken: string): Promise<RefreshResponse> {
34
- const clientId = process.env.GOOGLE_CLIENT_ID;
35
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
36
- if (!clientId || !clientSecret) {
35
+ const credentialCandidates = resolveGoogleProviderCredentialCandidates();
36
+ if (!credentialCandidates.length) {
37
37
  throw new Error("GOOGLE_CLIENT_ID/SECRET not set");
38
38
  }
39
- const res = await fetch(GOOGLE_TOKEN_URL, {
40
- method: "POST",
41
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
42
- body: new URLSearchParams({
43
- refresh_token: refreshToken,
44
- client_id: clientId,
45
- client_secret: clientSecret,
46
- grant_type: "refresh_token",
47
- }),
48
- });
49
- const data = (await res.json()) as Record<string, unknown>;
50
- if (!res.ok) {
39
+
40
+ let data: Record<string, unknown> | null = null;
41
+ let lastStatusText = "refresh failed";
42
+ for (const credentials of credentialCandidates) {
43
+ const res = await fetch(GOOGLE_TOKEN_URL, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
46
+ body: new URLSearchParams({
47
+ refresh_token: refreshToken,
48
+ client_id: credentials.clientId,
49
+ client_secret: credentials.clientSecret,
50
+ grant_type: "refresh_token",
51
+ }),
52
+ });
53
+ lastStatusText = res.statusText;
54
+ data = (await res.json()) as Record<string, unknown>;
55
+ if (res.ok) return data as unknown as RefreshResponse;
56
+ if (
57
+ data.error !== "invalid_grant" &&
58
+ data.error !== "unauthorized_client" &&
59
+ data.error !== "invalid_client"
60
+ ) {
61
+ break;
62
+ }
63
+ }
64
+
65
+ if (data) {
51
66
  const err = (data.error_description ||
52
67
  data.error ||
53
- res.statusText) as string;
68
+ lastStatusText) as string;
54
69
  throw new Error(err);
55
70
  }
56
- return data as unknown as RefreshResponse;
71
+ throw new Error(lastStatusText);
57
72
  }
58
73
 
59
74
  /**
@@ -19,6 +19,7 @@ import {
19
19
  saveOAuthTokens,
20
20
  } from "../oauth-tokens/index.js";
21
21
  import { getCredentialContext } from "../server/request-context.js";
22
+ import { resolveGoogleProviderCredentialCandidates } from "../server/google-oauth-credentials.js";
22
23
  import { resolveWorkspaceConnectionCredentialForApp } from "../workspace-connections/credentials.js";
23
24
  import type { WorkspaceConnectionTemplateUse } from "../connections/catalog.js";
24
25
  import type {
@@ -3432,36 +3433,55 @@ async function refreshGoogleOAuthToken(
3432
3433
  },
3433
3434
  refreshToken: string,
3434
3435
  ): Promise<string> {
3435
- const clientId = process.env.GOOGLE_CLIENT_ID;
3436
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
3437
- if (!clientId || !clientSecret) {
3436
+ const credentialCandidates = resolveGoogleProviderCredentialCandidates();
3437
+ if (!credentialCandidates.length) {
3438
3438
  throw new Error(
3439
3439
  "GOOGLE_CLIENT_ID/SECRET not set for Google OAuth refresh.",
3440
3440
  );
3441
3441
  }
3442
- const res = await fetch("https://oauth2.googleapis.com/token", {
3443
- method: "POST",
3444
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
3445
- body: new URLSearchParams({
3446
- refresh_token: refreshToken,
3447
- client_id: clientId,
3448
- client_secret: clientSecret,
3449
- grant_type: "refresh_token",
3450
- }),
3451
- });
3452
- const data = (await res.json()) as {
3442
+
3443
+ let data: {
3453
3444
  access_token?: string;
3454
3445
  expires_in?: number;
3455
3446
  token_type?: string;
3456
3447
  scope?: string;
3457
3448
  error?: string;
3458
3449
  error_description?: string;
3459
- };
3460
- if (!res.ok || !data.access_token) {
3450
+ } | null = null;
3451
+ let lastStatusText = "refresh failed";
3452
+ for (const credentials of credentialCandidates) {
3453
+ const res = await fetch("https://oauth2.googleapis.com/token", {
3454
+ method: "POST",
3455
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3456
+ body: new URLSearchParams({
3457
+ refresh_token: refreshToken,
3458
+ client_id: credentials.clientId,
3459
+ client_secret: credentials.clientSecret,
3460
+ grant_type: "refresh_token",
3461
+ }),
3462
+ });
3463
+ lastStatusText = res.statusText;
3464
+ data = (await res.json()) as {
3465
+ access_token?: string;
3466
+ expires_in?: number;
3467
+ token_type?: string;
3468
+ scope?: string;
3469
+ error?: string;
3470
+ error_description?: string;
3471
+ };
3472
+ if (res.ok && data.access_token) break;
3461
3473
  if (data.error && PERMANENT_GOOGLE_OAUTH_REFRESH_ERRORS.has(data.error)) {
3474
+ continue;
3475
+ }
3476
+ const detail = data.error_description ?? data.error ?? lastStatusText;
3477
+ throw new Error(`Google OAuth refresh failed: ${detail}`);
3478
+ }
3479
+
3480
+ if (!data?.access_token) {
3481
+ if (data?.error && PERMANENT_GOOGLE_OAUTH_REFRESH_ERRORS.has(data.error)) {
3462
3482
  await deleteOAuthTokens(options.oauthProvider, options.accountId);
3463
3483
  }
3464
- const detail = data.error_description ?? data.error ?? res.statusText;
3484
+ const detail = data?.error_description ?? data?.error ?? lastStatusText;
3465
3485
  throw new Error(`Google OAuth refresh failed: ${detail}`);
3466
3486
  }
3467
3487
  const merged: OAuthTokens = {