@agent-native/core 0.154.5 → 0.155.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 (185) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/account-health/SKILL.md +76 -0
  3. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +3 -0
  4. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +2 -1
  5. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +6 -5
  6. package/corpus/templates/analytics/AGENTS.md +3 -0
  7. package/corpus/templates/analytics/actions/account-deep-dive.ts +3 -2
  8. package/corpus/templates/analytics/actions/bigquery.ts +1 -1
  9. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -1
  10. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +12 -4
  12. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +5 -2
  13. package/corpus/templates/analytics/app/pages/adhoc/explorer/property-label.ts +32 -0
  14. package/corpus/templates/analytics/changelog/2026-08-13-account-health-readouts-verify-customer-scope-and-contract-metrics.md +7 -0
  15. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +43 -17
  16. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +17 -0
  17. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  18. package/corpus/templates/calendar/AGENTS.md +2 -0
  19. package/corpus/templates/calendar/DEVELOPING.md +4 -4
  20. package/corpus/templates/calendar/actions/get-settings.ts +4 -11
  21. package/corpus/templates/calendar/actions/update-availability.ts +15 -4
  22. package/corpus/templates/calendar/actions/update-settings.ts +15 -4
  23. package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +9 -3
  24. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +21 -19
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +9 -6
  26. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +25 -3
  27. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +10 -3
  28. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +9 -9
  29. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -3
  30. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +10 -4
  31. package/corpus/templates/calendar/app/i18n-data.ts +30 -0
  32. package/corpus/templates/calendar/app/lib/availability-schedule.ts +53 -0
  33. package/corpus/templates/calendar/app/lib/event-form-utils.ts +5 -0
  34. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +90 -28
  35. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +135 -61
  36. package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -0
  37. package/corpus/templates/calendar/app/pages/CalendarView.tsx +30 -15
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +60 -1
  39. package/corpus/templates/calendar/changelog/2026-08-13-find-a-time-now-closes-from-a-clear-header-action.md +6 -0
  40. package/corpus/templates/calendar/server/handlers/bookings.ts +2 -1
  41. package/corpus/templates/calendar/server/handlers/settings.ts +12 -16
  42. package/corpus/templates/calendar/shared/api.ts +1 -0
  43. package/corpus/templates/calendar/shared/availability-schedule.ts +64 -0
  44. package/corpus/templates/calendar/shared/calendar-week.ts +21 -0
  45. package/corpus/templates/calendar/shared/settings.ts +44 -0
  46. package/corpus/templates/clips/AGENTS.md +2 -0
  47. package/corpus/templates/clips/actions/add-comment.ts +5 -2
  48. package/corpus/templates/clips/actions/finalize-recording.ts +31 -1
  49. package/corpus/templates/clips/actions/get-recording-player-data.ts +9 -0
  50. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +21 -1
  51. package/corpus/templates/clips/actions/reply-to-comment.ts +5 -2
  52. package/corpus/templates/clips/actions/update-comment.ts +9 -2
  53. package/corpus/templates/clips/app/components/library/search-bar.tsx +26 -13
  54. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +135 -3
  55. package/corpus/templates/clips/app/components/player/comments-panel.tsx +5 -99
  56. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +7 -3
  57. package/corpus/templates/clips/app/components/player/video-player.tsx +84 -24
  58. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +11 -3
  59. package/corpus/templates/clips/app/i18n/en-US.ts +12 -0
  60. package/corpus/templates/clips/app/lib/public-meeting.ts +11 -3
  61. package/corpus/templates/clips/app/lib/search-focus.ts +15 -0
  62. package/corpus/templates/clips/app/root.tsx +2 -1
  63. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +94 -26
  64. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +24 -7
  65. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +8 -4
  66. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -7
  67. package/corpus/templates/clips/app/routes/share.$shareId.tsx +8 -4
  68. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +72 -26
  69. package/corpus/templates/clips/changelog/2026-08-13-inline-markdown-comments.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-08-13-private-meeting-agent-links.md +6 -0
  71. package/corpus/templates/clips/desktop/src/app.tsx +116 -19
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +13 -29
  73. package/corpus/templates/clips/desktop/src/lib/recording-request.ts +39 -0
  74. package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +11 -0
  75. package/corpus/templates/clips/desktop/src/lib/whisper-model-picker.ts +7 -0
  76. package/corpus/templates/clips/desktop/src/styles.css +117 -0
  77. package/corpus/templates/clips/server/db/index.ts +10 -0
  78. package/corpus/templates/clips/server/db/schema.ts +1 -0
  79. package/corpus/templates/clips/server/lib/recordings.ts +3 -1
  80. package/corpus/templates/clips/server/lib/seekable-media-state.ts +90 -0
  81. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  82. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +37 -4
  83. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +16 -5
  84. package/corpus/templates/clips/shared/clips-ai-prefs.ts +3 -0
  85. package/corpus/templates/clips/shared/meeting-agent-access.ts +5 -0
  86. package/corpus/templates/content/actions/add-comment.ts +2 -1
  87. package/corpus/templates/content/actions/update-comment.ts +1 -1
  88. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +24 -15
  89. package/corpus/templates/design/actions/generate-design.ts +10 -7
  90. package/corpus/templates/design/actions/present-design-variants.ts +35 -6
  91. package/corpus/templates/design/actions/run-design-audit.ts +33 -0
  92. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-catches-a-screen-that-renders-behind-an.md +6 -0
  93. package/corpus/templates/design/shared/design-review.ts +1 -0
  94. package/corpus/templates/design/shared/html-integrity.ts +66 -0
  95. package/corpus/templates/dispatch/.env.example +5 -0
  96. package/corpus/templates/dispatch/AGENTS.md +24 -19
  97. package/corpus/templates/dispatch/app/i18n-data.ts +20 -0
  98. package/corpus/templates/dispatch/server/lib/identity-sso.ts +21 -90
  99. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +3 -12
  100. package/corpus/templates/dispatch/shared/feature-flags.ts +4 -0
  101. package/corpus/templates/factory/AGENTS.md +5 -1
  102. package/corpus/templates/factory/README.md +23 -0
  103. package/corpus/templates/factory/actions/view-screen.ts +17 -0
  104. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +239 -0
  105. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +2 -0
  106. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +130 -7
  107. package/corpus/templates/factory/app/i18n/en-US.ts +20 -0
  108. package/corpus/templates/factory/app/routes/factory.tsx +41 -0
  109. package/corpus/templates/factory/package.json +1 -0
  110. package/corpus/templates/factory/server/factory-graph/contracts.ts +2 -0
  111. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -0
  112. package/corpus/templates/factory/server/plugins/dispatch-db.ts +1 -0
  113. package/corpus/templates/factory/server/plugins/setup-dispatch.ts +3 -0
  114. package/corpus/templates/mail/app/lib/native-mcp-exclusions.ts +3 -0
  115. package/corpus/templates/mail/vite.config.ts +7 -0
  116. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +7 -2
  117. package/corpus/templates/plan/actions/resolve-plan-comment.ts +2 -2
  118. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  119. package/corpus/templates/plan/app/pages/PlansPage.tsx +29 -20
  120. package/corpus/templates/slides/actions/add-slide-comment.ts +1 -1
  121. package/corpus/templates/slides/actions/update-slide-comment.ts +1 -1
  122. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +9 -6
  123. package/dist/agent/production-agent.js +5 -1
  124. package/dist/agent/thread-data-builder.js +17 -1
  125. package/dist/cli/skills-content/help.js +6 -4
  126. package/dist/cli/skills.js +17 -2
  127. package/dist/cli/workspacify.d.ts +5 -0
  128. package/dist/cli/workspacify.js +65 -8
  129. package/dist/client/AgentPanel.d.ts +5 -0
  130. package/dist/client/AgentPanel.js +20 -4
  131. package/dist/client/AssistantChat.d.ts +21 -0
  132. package/dist/client/AssistantChat.js +66 -68
  133. package/dist/client/active-run-state.d.ts +14 -1
  134. package/dist/client/active-run-state.js +0 -0
  135. package/dist/client/agent-chat-adapter.js +107 -5
  136. package/dist/client/analytics.js +9 -2
  137. package/dist/client/chat/tool-call-display.js +4 -4
  138. package/dist/client/chat-first/apps-rail.js +9 -4
  139. package/dist/client/chat-first/primary-nav.d.ts +2 -1
  140. package/dist/client/chat-first/primary-nav.js +3 -3
  141. package/dist/client/chat-first/types.d.ts +5 -1
  142. package/dist/client/markdown/InlineMarkdown.d.ts +23 -0
  143. package/dist/client/markdown/InlineMarkdown.js +65 -0
  144. package/dist/client/markdown/index.d.ts +1 -0
  145. package/dist/client/markdown/index.js +1 -0
  146. package/dist/client/review/ReviewThreadPanel.js +2 -1
  147. package/dist/client/run-stream-ownership.d.ts +39 -0
  148. package/dist/client/run-stream-ownership.js +65 -0
  149. package/dist/collab/awareness.d.ts +2 -2
  150. package/dist/collab/struct-routes.d.ts +1 -1
  151. package/dist/feature-flags/index.d.ts +1 -1
  152. package/dist/feature-flags/index.js +1 -1
  153. package/dist/feature-flags/store.d.ts +10 -0
  154. package/dist/feature-flags/store.js +112 -1
  155. package/dist/guards/no-env-credentials.js +4 -0
  156. package/dist/index.d.ts +1 -1
  157. package/dist/index.js +1 -1
  158. package/dist/integrations/adapters/slack.js +5 -1
  159. package/dist/localization/default-messages.d.ts +1 -0
  160. package/dist/localization/default-messages.js +1 -0
  161. package/dist/observability/routes.d.ts +6 -6
  162. package/dist/progress/routes.d.ts +1 -1
  163. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  164. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  165. package/dist/resources/agents.js +40 -5
  166. package/dist/resources/handlers.d.ts +1 -1
  167. package/dist/resources/metadata.d.ts +10 -0
  168. package/dist/resources/metadata.js +7 -3
  169. package/dist/review/actions/create-review-comment.js +1 -1
  170. package/dist/review/actions/reply-review-comment.js +1 -1
  171. package/dist/review/actions/resolve-review-thread.js +1 -1
  172. package/dist/secrets/routes.d.ts +3 -3
  173. package/dist/server/action-routes.js +32 -6
  174. package/dist/server/agent-discovery.js +14 -3
  175. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  176. package/dist/server/core-routes-plugin.js +1 -1
  177. package/dist/server/identity-sso-migrations.d.ts +7 -0
  178. package/dist/server/identity-sso-migrations.js +36 -0
  179. package/dist/server/identity-sso-store.d.ts +3 -2
  180. package/dist/server/identity-sso-store.js +8 -3
  181. package/dist/server/realtime-token.d.ts +1 -1
  182. package/dist/server/release-migrations.js +4 -0
  183. package/dist/tracking/providers.js +13 -3
  184. package/dist/workspace-files/tool.js +1 -1
  185. package/package.json +3 -2
@@ -17,6 +17,11 @@ export interface WorkspacifyOptions {
17
17
  toolkitDependencyVersion?: string;
18
18
  }
19
19
  export declare function workspacifyApp(opts: WorkspacifyOptions): void;
20
+ export declare function removeCopiedFrameworkSkills(appDir: string, options?: {
21
+ workspaceRoot?: string;
22
+ allowUnverified?: boolean;
23
+ }): string[];
24
+ export declare function linkDefaultWorkspaceSkills(appDir: string, workspaceRoot: string): string[];
20
25
  /**
21
26
  * Parse a workspace core package name into its npm scope.
22
27
  * "@my-company/shared" → "my-company"
@@ -144,27 +144,83 @@ export function workspacifyApp(opts) {
144
144
  }
145
145
  }
146
146
  function linkInheritedWorkspaceSkills(opts) {
147
- const appSkillsDir = path.join(opts.appDir, ".agents", "skills");
148
147
  const workspaceSkillsDir = path.join(opts.workspaceRoot, ".agents", "skills");
149
148
  if (!fs.existsSync(workspaceSkillsDir))
150
149
  return;
151
- fs.mkdirSync(appSkillsDir, { recursive: true });
150
+ removeCopiedFrameworkSkills(opts.appDir, { allowUnverified: true });
151
+ linkDefaultWorkspaceSkills(opts.appDir, opts.workspaceRoot);
152
+ }
153
+ function skillDirContentsMatch(sourceDir, targetDir) {
154
+ const sourceStat = fs.lstatSync(sourceDir, { throwIfNoEntry: false });
155
+ const targetStat = fs.lstatSync(targetDir, { throwIfNoEntry: false });
156
+ if (!sourceStat?.isDirectory() || !targetStat?.isDirectory())
157
+ return false;
158
+ const sourceEntries = fs.readdirSync(sourceDir, { withFileTypes: true });
159
+ const targetEntries = fs.readdirSync(targetDir, { withFileTypes: true });
160
+ if (sourceEntries.length !== targetEntries.length)
161
+ return false;
162
+ return sourceEntries.every((sourceEntry) => {
163
+ const sourcePath = path.join(sourceDir, sourceEntry.name);
164
+ const targetPath = path.join(targetDir, sourceEntry.name);
165
+ const targetEntry = targetEntries.find((entry) => entry.name === sourceEntry.name);
166
+ if (!targetEntry)
167
+ return false;
168
+ if (sourceEntry.isSymbolicLink()) {
169
+ return (targetEntry.isSymbolicLink() &&
170
+ fs.readlinkSync(sourcePath) === fs.readlinkSync(targetPath));
171
+ }
172
+ if (sourceEntry.isDirectory()) {
173
+ return (targetEntry.isDirectory() &&
174
+ skillDirContentsMatch(sourcePath, targetPath));
175
+ }
176
+ return (sourceEntry.isFile() &&
177
+ targetEntry.isFile() &&
178
+ fs.readFileSync(sourcePath).equals(fs.readFileSync(targetPath)));
179
+ });
180
+ }
181
+ export function removeCopiedFrameworkSkills(appDir, options = {}) {
182
+ const appSkillsDir = path.join(appDir, ".agents", "skills");
183
+ const workspaceSkillsDir = options.workspaceRoot
184
+ ? path.join(options.workspaceRoot, ".agents", "skills")
185
+ : undefined;
186
+ const preserved = [];
152
187
  for (const skill of FRAMEWORK_TEMPLATE_SHARED_SKILLS) {
153
188
  const localPath = path.join(appSkillsDir, skill);
154
- if (fs.existsSync(localPath) ||
155
- fs.lstatSync(localPath, { throwIfNoEntry: false })) {
156
- fs.rmSync(localPath, { recursive: true, force: true });
189
+ const stat = fs.lstatSync(localPath, { throwIfNoEntry: false });
190
+ if (!stat || stat.isSymbolicLink())
191
+ continue;
192
+ const workspacePath = workspaceSkillsDir
193
+ ? path.join(workspaceSkillsDir, skill)
194
+ : undefined;
195
+ if (!options.allowUnverified &&
196
+ (!workspacePath || !skillDirContentsMatch(workspacePath, localPath))) {
197
+ preserved.push(skill);
198
+ continue;
157
199
  }
200
+ fs.rmSync(localPath, { recursive: true, force: true });
158
201
  }
202
+ return preserved;
203
+ }
204
+ export function linkDefaultWorkspaceSkills(appDir, workspaceRoot) {
205
+ const appSkillsDir = path.join(appDir, ".agents", "skills");
206
+ const workspaceSkillsDir = path.join(workspaceRoot, ".agents", "skills");
207
+ if (!fs.existsSync(workspaceSkillsDir))
208
+ return [];
209
+ fs.mkdirSync(appSkillsDir, { recursive: true });
210
+ const preserved = [];
159
211
  for (const skill of DEFAULT_WORKSPACE_SKILLS) {
160
212
  const inheritedPath = path.join(workspaceSkillsDir, skill);
161
213
  if (!fs.existsSync(inheritedPath))
162
214
  continue;
163
215
  const linkPath = path.join(appSkillsDir, skill);
164
- if (fs.existsSync(linkPath) ||
165
- fs.lstatSync(linkPath, { throwIfNoEntry: false })) {
166
- fs.rmSync(linkPath, { recursive: true, force: true });
216
+ const existing = fs.lstatSync(linkPath, { throwIfNoEntry: false });
217
+ if (existing &&
218
+ !existing.isSymbolicLink() &&
219
+ !skillDirContentsMatch(inheritedPath, linkPath)) {
220
+ preserved.push(skill);
221
+ continue;
167
222
  }
223
+ fs.rmSync(linkPath, { recursive: true, force: true });
168
224
  try {
169
225
  fs.symlinkSync(path.relative(appSkillsDir, inheritedPath), linkPath, "dir");
170
226
  }
@@ -172,6 +228,7 @@ function linkInheritedWorkspaceSkills(opts) {
172
228
  throw new Error(`Could not link inherited workspace skill ${skill} into ${appSkillsDir}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
173
229
  }
174
230
  }
231
+ return preserved;
175
232
  }
176
233
  /**
177
234
  * The version an existing workspace already pins for a framework package.
@@ -29,6 +29,11 @@ export declare function normalizeAgentPanelModeForSurface(mode: PanelMode, allow
29
29
  export declare function deferAgentPanelOverlayOpen(event: {
30
30
  preventDefault: () => void;
31
31
  }, closeMenu: () => void, openOverlay: () => void): void;
32
+ export declare function consumeAgentPanelOverlayFocusRestore(pendingOverlayRef: {
33
+ current: boolean;
34
+ }, event: {
35
+ preventDefault: () => void;
36
+ }): void;
32
37
  export declare function getAgentPanelChatTabGroups(tabs: MultiTabAssistantChatHeaderProps["tabs"], activeTabId: string): {
33
38
  activeTab: import("./MultiTabAssistantChat.js").ChatTab | undefined;
34
39
  childTabs: import("./MultiTabAssistantChat.js").ChatTab[];
@@ -157,6 +157,12 @@ export function deferAgentPanelOverlayOpen(event, closeMenu, openOverlay) {
157
157
  setTimeout(openOverlay, 0);
158
158
  }
159
159
  }
160
+ export function consumeAgentPanelOverlayFocusRestore(pendingOverlayRef, event) {
161
+ if (!pendingOverlayRef.current)
162
+ return;
163
+ pendingOverlayRef.current = false;
164
+ event.preventDefault();
165
+ }
160
166
  function useAvailableClis() {
161
167
  const [clis, setClis] = useState([]);
162
168
  useEffect(() => {
@@ -667,6 +673,11 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
667
673
  const [headerMenuOpen, setHeaderMenuOpen] = useState(false);
668
674
  const [feedbackOpen, setFeedbackOpen] = useState(false);
669
675
  const [shareFromMenuOpen, setShareFromMenuOpen] = useState(false);
676
+ const preventHeaderMenuFocusRestoreRef = useRef(false);
677
+ const closeHeaderMenuForOverlay = useCallback(() => {
678
+ preventHeaderMenuFocusRestoreRef.current = true;
679
+ setHeaderMenuOpen(false);
680
+ }, []);
670
681
  const getChatThreadShareUrl = useCallback((threadId) => {
671
682
  if (typeof window === "undefined")
672
683
  return undefined;
@@ -712,7 +723,11 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
712
723
  }
713
724
  return (_jsx(ShareButton, { resourceType: "chat_thread", resourceId: activeTab.id, allowedRoles: ["viewer", "editor", "admin"], resourceTitle: activeTab.label || "Chat", shareUrl: getChatThreadShareUrl(activeTab.id), trigger: "icon", triggerClassName: "h-7 w-7", defaultOpen: onCollapse && shareFromMenuOpen, onOpenChange: onCollapse ? setShareFromMenuOpen : undefined }));
714
725
  })(), feedbackEnabled ? (_jsx(FeedbackButton, { variant: "icon", side: "bottom", align: "end", chatSessionId: activeChatSessionId, chatStorageKey: storageKey, open: feedbackOpen, onOpenChange: setFeedbackOpen, trigger: _jsx("button", { type: "button", tabIndex: -1, "aria-hidden": "true", className: "pointer-events-none absolute end-0 top-full h-px w-px opacity-0" }) })) : null, mode === "chat" && (_jsx(IconTooltip, { content: t("agentPanel.newChat"), children: _jsx("button", { onClick: addTab, "aria-label": t("agentPanel.newChat"), className: "flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50", children: _jsx(IconPlus, { size: 14 }) }) })), !onCollapse && mode === "cli" && canUseCodeTools && (_jsx(IconTooltip, { content: t("agentPanel.newTerminal"), children: _jsx("button", { onClick: addCliTab, "aria-label": t("agentPanel.newTerminal"), className: "flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50", children: _jsx(IconPlus, { size: 14 }) }) })), _jsxs(DropdownMenu, { open: headerMenuOpen, onOpenChange: setHeaderMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { className: cn("flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50", (headerMenuOpen || mode === "settings") &&
715
- "bg-accent text-foreground"), "aria-label": t("agentPanel.panelOptions"), children: _jsx(IconDotsVertical, { size: 14 }) }) }), _jsxs(DropdownMenuContent, { align: "end", sideOffset: 6, className: "w-48", children: [onCollapse && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuItem, { onSelect: onCollapse, children: [_jsx(IconLayoutSidebarRightCollapse, { size: 14, className: "shrink-0" }), t("agentPanel.collapseSidebar"), _jsx(DropdownMenuShortcut, { children: toggleSidebarHint })] }), _jsx(DropdownMenuSeparator, {})] })), onCollapse && mode === "chat" && wideDrawerAction ? (_jsxs(DropdownMenuItem, { onSelect: wideDrawerAction, children: [_jsx(IconArrowsHorizontal, { size: 14, className: "shrink-0" }), wideDrawerLabel, _jsx(DropdownMenuShortcut, { children: widenChatHint })] })) : null, fullViewAction?.kind === "callback" && onFullViewRequest ? (_jsxs(DropdownMenuItem, { onSelect: onFullViewRequest, "aria-label": t("agentPanel.openFullView"), children: [_jsx(IconArrowsMaximize, { size: 14, className: "shrink-0" }), t("agentPanel.openFullView")] })) : fullViewAction?.kind === "link" ? (_jsx(DropdownMenuItem, { asChild: true, children: _jsxs(Link, { to: fullViewAction.href, "aria-label": t("agentPanel.openFullView"), children: [_jsx(IconArrowsMaximize, { size: 14, className: "shrink-0" }), t("agentPanel.openFullView")] }) })) : null, (onCollapse && mode === "chat" && wideDrawerAction) ||
726
+ "bg-accent text-foreground"), "aria-label": t("agentPanel.panelOptions"), children: _jsx(IconDotsVertical, { size: 14 }) }) }), _jsxs(DropdownMenuContent, { align: "end", sideOffset: 6, className: "w-48", onCloseAutoFocus: (event) => {
727
+ // A sibling overlay owns focus next; restoring it to the menu
728
+ // trigger would dismiss that overlay as an outside interaction.
729
+ consumeAgentPanelOverlayFocusRestore(preventHeaderMenuFocusRestoreRef, event);
730
+ }, children: [onCollapse && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuItem, { onSelect: onCollapse, children: [_jsx(IconLayoutSidebarRightCollapse, { size: 14, className: "shrink-0" }), t("agentPanel.collapseSidebar"), _jsx(DropdownMenuShortcut, { children: toggleSidebarHint })] }), _jsx(DropdownMenuSeparator, {})] })), onCollapse && mode === "chat" && wideDrawerAction ? (_jsxs(DropdownMenuItem, { onSelect: wideDrawerAction, children: [_jsx(IconArrowsHorizontal, { size: 14, className: "shrink-0" }), wideDrawerLabel, _jsx(DropdownMenuShortcut, { children: widenChatHint })] })) : null, fullViewAction?.kind === "callback" && onFullViewRequest ? (_jsxs(DropdownMenuItem, { onSelect: onFullViewRequest, "aria-label": t("agentPanel.openFullView"), children: [_jsx(IconArrowsMaximize, { size: 14, className: "shrink-0" }), t("agentPanel.openFullView")] })) : fullViewAction?.kind === "link" ? (_jsx(DropdownMenuItem, { asChild: true, children: _jsxs(Link, { to: fullViewAction.href, "aria-label": t("agentPanel.openFullView"), children: [_jsx(IconArrowsMaximize, { size: 14, className: "shrink-0" }), t("agentPanel.openFullView")] }) })) : null, (onCollapse && mode === "chat" && wideDrawerAction) ||
716
731
  fullViewAction ? (_jsx(DropdownMenuSeparator, {})) : null, onCollapse && mode === "chat" && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuItem, { onSelect: addTab, children: [_jsx(IconPlus, { size: 14, className: "shrink-0" }), t("agentPanel.newChat")] }), (() => {
717
732
  const activeTab = activeChatSessionId
718
733
  ? tabs.find((tab) => tab.id === activeChatSessionId)
@@ -721,18 +736,19 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
721
736
  (activeTabMessageCount <= 0 && activeTab.status === "idle")) {
722
737
  return null;
723
738
  }
724
- return (_jsxs(DropdownMenuItem, { onSelect: (event) => deferAgentPanelOverlayOpen(event, () => setHeaderMenuOpen(false), () => setShareFromMenuOpen(true)), children: [_jsx(IconShare3, { size: 14, className: "shrink-0" }), "Share"] }));
725
- })()] })), mode === "chat" && toggleHistory && (_jsxs(DropdownMenuItem, { onSelect: (event) => deferAgentPanelOverlayOpen(event, () => setHeaderMenuOpen(false), toggleHistory), children: [_jsx(IconHistory, { size: 14, className: "shrink-0" }), showHistory
739
+ return (_jsxs(DropdownMenuItem, { onSelect: (event) => deferAgentPanelOverlayOpen(event, closeHeaderMenuForOverlay, () => setShareFromMenuOpen(true)), children: [_jsx(IconShare3, { size: 14, className: "shrink-0" }), "Share"] }));
740
+ })()] })), mode === "chat" && toggleHistory && (_jsxs(DropdownMenuItem, { onSelect: (event) => deferAgentPanelOverlayOpen(event, closeHeaderMenuForOverlay, toggleHistory), children: [_jsx(IconHistory, { size: 14, className: "shrink-0" }), showHistory
726
741
  ? t("agentPanel.hideChats")
727
742
  : t("agentPanel.allChats")] })), mode === "chat" && (_jsx(RunsTrayMenuItem, { pollMs: 0, limit: 12, showRecent: true, onOpenThread: openRunThread })), mode === "chat" && _jsx(DropdownMenuSeparator, {}), mode === "cli" && availableClis.length > 0 && (_jsxs(_Fragment, { children: [availableClis.map((cli) => (_jsxs(DropdownMenuItem, { onSelect: () => selectCli(cli.command), className: cn(cli.command === selectedCli
728
743
  ? "font-medium"
729
- : "text-muted-foreground"), children: [cli.command === selectedCli ? (_jsx(IconCheck, { size: 12, className: "shrink-0" })) : (_jsx("span", { className: "w-3" })), cli.label] }, cli.command))), _jsx(DropdownMenuSeparator, {})] })), allowSettingsMode && (_jsxs(DropdownMenuItem, { onSelect: () => switchMode("settings"), className: cn(mode === "settings" ? "font-medium" : "text-muted-foreground"), children: [_jsx(IconSettings, { size: 14, className: "shrink-0" }), t("agentPanel.settings")] })), feedbackEnabled ? (_jsxs(DropdownMenuItem, { onSelect: (event) => deferAgentPanelOverlayOpen(event, () => setHeaderMenuOpen(false), () => setFeedbackOpen(true)), children: [_jsx(IconMessageDots, { size: 14, className: "shrink-0" }), t("agentPanel.feedback")] })) : null, ((mode === "chat" && activeTabId) ||
744
+ : "text-muted-foreground"), children: [cli.command === selectedCli ? (_jsx(IconCheck, { size: 12, className: "shrink-0" })) : (_jsx("span", { className: "w-3" })), cli.label] }, cli.command))), _jsx(DropdownMenuSeparator, {})] })), allowSettingsMode && (_jsxs(DropdownMenuItem, { onSelect: () => switchMode("settings"), className: cn(mode === "settings" ? "font-medium" : "text-muted-foreground"), children: [_jsx(IconSettings, { size: 14, className: "shrink-0" }), t("agentPanel.settings")] })), feedbackEnabled ? (_jsxs(DropdownMenuItem, { onSelect: (event) => deferAgentPanelOverlayOpen(event, closeHeaderMenuForOverlay, () => setFeedbackOpen(true)), children: [_jsx(IconMessageDots, { size: 14, className: "shrink-0" }), t("agentPanel.feedback")] })) : null, ((mode === "chat" && activeTabId) ||
730
745
  (mode === "cli" && canUseCodeTools && activeCliTab)) && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), mode === "chat" ? (shouldShowAgentPanelChatTabBar(tabs, activeTabId) ? (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuItem, { onSelect: () => closeTab(activeTabId), children: [_jsx(IconX, { size: 14, className: "shrink-0" }), t("agentPanel.closeTab"), _jsx(DropdownMenuShortcut, { children: closeTabHint })] }), _jsx(DropdownMenuItem, { onSelect: () => closeOtherTabs(activeTabId), children: t("agentPanel.closeOtherTabs") }), _jsxs(DropdownMenuItem, { onSelect: () => closeAllTabs(), children: [t("agentPanel.closeAllTabs"), _jsx(DropdownMenuShortcut, { children: closeAllTabsHint })] })] })) : (_jsxs(DropdownMenuItem, { onSelect: clearActiveTab, children: [_jsx(IconX, { size: 14, className: "shrink-0" }), t("agentPanel.clearChat")] }))) : (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuItem, { onSelect: () => closeCliTab(activeCliTab), children: [_jsx(IconX, { size: 14, className: "shrink-0" }), t("agentPanel.closeTab"), _jsx(DropdownMenuShortcut, { children: closeTabHint })] }), _jsx(DropdownMenuItem, { onSelect: () => closeOtherCliTabs(activeCliTab), children: t("agentPanel.closeOtherTabs") }), _jsxs(DropdownMenuItem, { onSelect: () => closeAllCliTabs(), children: [t("agentPanel.closeAllTabs"), _jsx(DropdownMenuShortcut, { children: closeAllTabsHint })] })] }))] }))] })] }), onCollapse && (_jsx(IconTooltip, { content: t("agentPanel.collapseSidebar"), children: _jsx("button", { type: "button", onClick: onCollapse, "aria-label": t("agentPanel.collapseSidebar"), className: "flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50", children: _jsx(IconX, { size: 14 }) }) }))] })), [
731
746
  activeCliTab,
732
747
  addCliTab,
733
748
  allowSettingsMode,
734
749
  availableClis,
735
750
  canUseCodeTools,
751
+ closeHeaderMenuForOverlay,
736
752
  closeAllCliTabs,
737
753
  closeAllTabsHint,
738
754
  closeCliTab,
@@ -90,6 +90,27 @@ export declare function settleInterruptedAssistantToolCallsInRepo<T extends {
90
90
  };
91
91
  export declare function waitForThreadRunToClear(apiUrl: string, threadId?: string): Promise<boolean>;
92
92
  export declare function reconnectActivityFallbackContent(toolName: string | null | undefined): ContentPart[];
93
+ /**
94
+ * Whether the reconnect overlay — a SECOND fold of a run, rendered as a sibling
95
+ * of the message list — may appear.
96
+ *
97
+ * The adapter runtime and the reconnect reader both fold the same SSE events
98
+ * into their own accumulator. Whenever both are on screen the user sees the
99
+ * turn twice: duplicate tool cards (one spinning, one static) and the final
100
+ * message streaming in two places. Content-similarity dedupe cannot reliably
101
+ * hide the second copy, because the two readers disagree on tool-call identity
102
+ * (id-less activity cards get reader-local ids) and on how a turn is split
103
+ * across assistant messages.
104
+ *
105
+ * So ownership decides visibility, not similarity: if a runtime owns the turn,
106
+ * the overlay does not render. Keep this a pure function — it is the invariant
107
+ * the duplicate-render bug kept violating, and it must stay falsifiable.
108
+ */
109
+ export declare function shouldShowReconnectOverlay(state: {
110
+ isRuntimeRunning: boolean;
111
+ isReconnecting: boolean;
112
+ reconnectFrozen: boolean;
113
+ }): boolean;
93
114
  export declare function dedupeReconnectContentAgainstMessages(content: ContentPart[], messages: readonly unknown[], options?: {
94
115
  suppressToolRepeats?: boolean;
95
116
  trimTailTextOverlap?: boolean;
@@ -35,6 +35,7 @@ import { GuidedQuestionFlow, useGuidedQuestionFlow, } from "./guided-questions.j
35
35
  import { buildSignInReturnHref } from "./require-session.js";
36
36
  import { addMcpConnectionCompleteListener, consumeMcpConnectionResume, } from "./resources/mcp-connection-resume.js";
37
37
  import { McpConnectionSuggestion } from "./resources/McpConnectionSuggestion.js";
38
+ import { claimRunStream, createRunStreamToken, ownsRunStream, releaseRunStream, } from "./run-stream-ownership.js";
38
39
  import { AgentAutoContinueSignal, readSSEStreamRaw, settleInterruptedToolCalls, } from "./sse-event-processor.js";
39
40
  import { useAgentEngineConfigured } from "./use-agent-engine-configured.js";
40
41
  import { useDevMode } from "./use-dev-mode.js";
@@ -786,6 +787,27 @@ function trimReconnectTextAlreadyRendered(content, renderedAssistantText, option
786
787
  }
787
788
  return changed ? next : content;
788
789
  }
790
+ /**
791
+ * Whether the reconnect overlay — a SECOND fold of a run, rendered as a sibling
792
+ * of the message list — may appear.
793
+ *
794
+ * The adapter runtime and the reconnect reader both fold the same SSE events
795
+ * into their own accumulator. Whenever both are on screen the user sees the
796
+ * turn twice: duplicate tool cards (one spinning, one static) and the final
797
+ * message streaming in two places. Content-similarity dedupe cannot reliably
798
+ * hide the second copy, because the two readers disagree on tool-call identity
799
+ * (id-less activity cards get reader-local ids) and on how a turn is split
800
+ * across assistant messages.
801
+ *
802
+ * So ownership decides visibility, not similarity: if a runtime owns the turn,
803
+ * the overlay does not render. Keep this a pure function — it is the invariant
804
+ * the duplicate-render bug kept violating, and it must stay falsifiable.
805
+ */
806
+ export function shouldShowReconnectOverlay(state) {
807
+ if (state.isRuntimeRunning)
808
+ return false;
809
+ return state.isReconnecting || state.reconnectFrozen;
810
+ }
789
811
  export function dedupeReconnectContentAgainstMessages(content, messages, options) {
790
812
  if (content.length === 0)
791
813
  return content;
@@ -1605,7 +1627,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1605
1627
  const [reconnectFrozen, setReconnectFrozen] = useState(false);
1606
1628
  // Adapter took over while reconnect still had visible tool cards — keep the
1607
1629
  // overlay until the adapter message catches up so we don't flash an empty gap.
1608
- const [adapterHandoffPending, setAdapterHandoffPending] = useState(false);
1609
1630
  const reconnectRunIdRef = useRef(null);
1610
1631
  const reconnectTurnIdRef = useRef(null);
1611
1632
  const reconnectTailOnlyRef = useRef(false);
@@ -1624,7 +1645,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1624
1645
  setIsReconnecting(false);
1625
1646
  setReconnectFrozen(false);
1626
1647
  setReconnectContent([]);
1627
- setAdapterHandoffPending(false);
1628
1648
  setPendingReconnectRecovery(null);
1629
1649
  resetRunningActivity();
1630
1650
  }, [resetRunningActivity]);
@@ -1702,14 +1722,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1702
1722
  isReconnecting,
1703
1723
  hasReconnectContent: reconnectContent.length > 0,
1704
1724
  });
1705
- const reconnectActivityContent = useMemo(() => isReconnecting || reconnectFrozen || adapterHandoffPending
1725
+ const reconnectActivityContent = useMemo(() => isReconnecting || reconnectFrozen
1706
1726
  ? reconnectActivityFallbackContent(runningActivityTool)
1707
- : [], [
1708
- adapterHandoffPending,
1709
- isReconnecting,
1710
- reconnectFrozen,
1711
- runningActivityTool,
1712
- ]);
1727
+ : [], [isReconnecting, reconnectFrozen, runningActivityTool]);
1713
1728
  const lastBroadcastRunningRef = useRef(isRunning);
1714
1729
  const tiptapRef = useRef(null);
1715
1730
  const focusComposerAfterConnectRef = useRef(false);
@@ -2010,6 +2025,13 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2010
2025
  if (isRuntimeRunningRef.current || isAutoResumingRef.current) {
2011
2026
  return false;
2012
2027
  }
2028
+ // The refs above lag a render and are per-component-instance, while
2029
+ // MultiTabAssistantChat mounts several instances against one run. The
2030
+ // claim is the actual mutual exclusion: module-scoped, synchronous, and
2031
+ // re-checked on every state write below.
2032
+ const ownershipToken = createRunStreamToken(`reconnect:${runId}`);
2033
+ if (!claimRunStream(threadId, runId, ownershipToken))
2034
+ return false;
2013
2035
  // SUPERSEDE THE PREVIOUS RECONNECT GENERATION. A turn that keeps failing
2014
2036
  // (e.g. repeated stale_run at "Contacting model") produces a new runId
2015
2037
  // every few seconds; each call here used to OVERWRITE the single-slot
@@ -2050,7 +2072,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2050
2072
  });
2051
2073
  setIsReconnecting(true);
2052
2074
  setReconnectFrozen(false);
2053
- setAdapterHandoffPending(false);
2054
2075
  setReconnectContent([]);
2055
2076
  window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
2056
2077
  detail: { isRunning: true, tabId: tabId || threadId },
@@ -2197,7 +2218,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2197
2218
  requestAnimationFrame(() => {
2198
2219
  rafPending = false;
2199
2220
  if (!reconnectOwnerMountedRef.current ||
2200
- reconnectRunIdRef.current !== runId) {
2221
+ reconnectRunIdRef.current !== runId ||
2222
+ !ownsRunStream(threadId, runId, ownershipToken)) {
2201
2223
  return;
2202
2224
  }
2203
2225
  setReconnectContent(latestSnapshot);
@@ -2205,9 +2227,14 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2205
2227
  };
2206
2228
  try {
2207
2229
  await readSSEStreamRaw(sseRes.body, content, toolCallCounter, tabId, scheduleUpdate, (seq, isProgress) => {
2230
+ // The adapter can preempt this reader mid-stream. Advancing
2231
+ // the cursor after that would move a run this reader no
2232
+ // longer represents.
2233
+ if (!ownsRunStream(threadId, runId, ownershipToken))
2234
+ return;
2208
2235
  markReconnectProgress();
2209
2236
  reconnectRetryCount = 0;
2210
- updateActiveRunSeq(seq, isProgress);
2237
+ updateActiveRunSeq(threadId, runId, seq, isProgress);
2211
2238
  }, { preparingActionState });
2212
2239
  if (reconnectAfterSeq === 0) {
2213
2240
  setReconnectContent([...content]);
@@ -2255,6 +2282,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2255
2282
  threadPollEngine?.stop();
2256
2283
  watchdog.stop();
2257
2284
  clearInterval(idleCheck);
2285
+ releaseRunStream(threadId, runId, ownershipToken);
2258
2286
  }
2259
2287
  // A newer reader, live adapter, stop action, or component unmount took
2260
2288
  // ownership while this async loop was unwinding. Do not let the stale
@@ -3192,11 +3220,14 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3192
3220
  prevIsRuntimeRunningRef.current = isRuntimeRunning;
3193
3221
  if (isRuntimeRunning && !wasRunning) {
3194
3222
  // SINGLE-READER OWNERSHIP: the adapter runtime just took over (a new run
3195
- // started or an adopted run resumed). Abort the reconnect reader, but keep
3196
- // its visible content until the adapter message has tool/text parts so the
3197
- // UI does not flash an empty gap between readers.
3223
+ // started or an adopted run resumed), so it is now the only owner of this
3224
+ // turn's rendering. The overlay used to be kept alive here for up to
3225
+ // 2500ms so the UI would not flash a gap — but that deliberately put two
3226
+ // independent folds of the same run on screen at once, and the only thing
3227
+ // hiding the second was content-similarity guessing that fails whenever
3228
+ // the two readers disagree (id-less activity cards, a turn split across
3229
+ // several assistant messages). One owner, one surface: drop the overlay.
3198
3230
  if (reconnectRunIdRef.current !== null) {
3199
- const keepOverlay = reconnectContent.length > 0;
3200
3231
  reconnectRunIdRef.current = null;
3201
3232
  reconnectAbortRef.current?.abort();
3202
3233
  reconnectAbortRef.current = null;
@@ -3204,56 +3235,18 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3204
3235
  setReconnectFrozen(false);
3205
3236
  reconnectCanMaterializeRef.current = false;
3206
3237
  reconnectTailOnlyRef.current = false;
3207
- if (keepOverlay) {
3208
- setAdapterHandoffPending(true);
3209
- }
3210
- else {
3211
- setReconnectContent([]);
3212
- setAdapterHandoffPending(false);
3213
- }
3238
+ setReconnectContent([]);
3214
3239
  }
3215
3240
  else if (reconnectFrozen) {
3216
3241
  setReconnectFrozen(false);
3217
3242
  setReconnectContent([]);
3218
- setAdapterHandoffPending(false);
3219
3243
  reconnectCanMaterializeRef.current = false;
3220
3244
  }
3221
3245
  if (forceStopped) {
3222
3246
  setForceStopped(false);
3223
3247
  }
3224
3248
  }
3225
- }, [
3226
- isRuntimeRunning,
3227
- reconnectFrozen,
3228
- forceStopped,
3229
- reconnectContent.length,
3230
- ]);
3231
- // Release the deferred reconnect overlay once thread messages have caught
3232
- // up enough that dedupe would hide the overlay, or after a short timeout so
3233
- // a stuck handoff cannot leave duplicate tool cards forever.
3234
- useEffect(() => {
3235
- if (!adapterHandoffPending)
3236
- return;
3237
- if (!isRuntimeRunning) {
3238
- setReconnectContent([]);
3239
- setAdapterHandoffPending(false);
3240
- return;
3241
- }
3242
- const stillNeeded = dedupeReconnectContentAgainstMessages(reconnectContent, messages, {
3243
- suppressToolRepeats: true,
3244
- trimTailTextOverlap: true,
3245
- }).length > 0;
3246
- if (!stillNeeded) {
3247
- setReconnectContent([]);
3248
- setAdapterHandoffPending(false);
3249
- return;
3250
- }
3251
- const timer = window.setTimeout(() => {
3252
- setReconnectContent([]);
3253
- setAdapterHandoffPending(false);
3254
- }, 2500);
3255
- return () => window.clearTimeout(timer);
3256
- }, [adapterHandoffPending, isRuntimeRunning, messages, reconnectContent]);
3249
+ }, [isRuntimeRunning, reconnectFrozen, forceStopped]);
3257
3250
  // Same transition guard for isReconnecting: only clear forceStopped on
3258
3251
  // the false→true edge (a new reconnect starting on page load).
3259
3252
  const prevIsReconnectingRef = useRef(isReconnecting);
@@ -3270,7 +3263,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3270
3263
  if (!reconnectCanMaterializeRef.current) {
3271
3264
  setReconnectFrozen(false);
3272
3265
  setReconnectContent([]);
3273
- setAdapterHandoffPending(false);
3274
3266
  return;
3275
3267
  }
3276
3268
  try {
@@ -3316,7 +3308,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3316
3308
  threadRuntime.import(ensureMessageMetadata(repo));
3317
3309
  setReconnectFrozen(false);
3318
3310
  setReconnectContent([]);
3319
- setAdapterHandoffPending(false);
3320
3311
  reconnectCanMaterializeRef.current = false;
3321
3312
  reconnectTurnIdRef.current = null;
3322
3313
  }
@@ -3453,7 +3444,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3453
3444
  reconnectAbortRef.current = null;
3454
3445
  reconnectRunIdRef.current = null;
3455
3446
  setIsReconnecting(false);
3456
- setAdapterHandoffPending(false);
3457
3447
  const shouldFreezeReconnectContent = !reconnectTailOnlyRef.current &&
3458
3448
  reconnectCanMaterializeRef.current &&
3459
3449
  reconnectContent.length > 0;
@@ -3844,8 +3834,19 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3844
3834
  // an ahead-of-the-thread reconnect copy visible creates the familiar
3845
3835
  // two-card stack while the adapter catches up, so prefer one row and
3846
3836
  // let the live message advance in place.
3847
- suppressToolRepeats: adapterHandoffPending || isReconnecting || reconnectFrozen,
3848
- trimTailTextOverlap: adapterHandoffPending || reconnectTailOnlyRef.current,
3837
+ suppressToolRepeats: isReconnecting || reconnectFrozen,
3838
+ trimTailTextOverlap: reconnectTailOnlyRef.current,
3839
+ });
3840
+ // The reconnect overlay is a SECOND fold of the same run, rendered as a
3841
+ // sibling of the message list. It may only appear while no adapter runtime
3842
+ // owns the turn. Deriving it from `isRuntimeRunning` at render time — rather
3843
+ // than relying on an effect to clear the overlay's own flags afterwards —
3844
+ // is what makes two streaming copies structurally impossible instead of
3845
+ // merely unlikely; the effect below runs a frame too late to prevent it.
3846
+ const showReconnectOverlay = shouldShowReconnectOverlay({
3847
+ isRuntimeRunning,
3848
+ isReconnecting,
3849
+ reconnectFrozen,
3849
3850
  });
3850
3851
  const latestMessage = messages[messages.length - 1];
3851
3852
  const reconnectStatusContent = visibleReconnectContent.length > 0
@@ -4039,7 +4040,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4039
4040
  getApprovalResolution,
4040
4041
  onApprovalResolved: recordApprovalResolution,
4041
4042
  onApprove: (approvalKey) => {
4042
- void addToQueue("Approved. Go ahead and run the requested action.", undefined, undefined, undefined, undefined, "queued", undefined, false, false, false, false, undefined, [approvalKey]);
4043
+ void addToQueue("Approved. Go ahead and run the requested action.", undefined, undefined, undefined, undefined, "queued", undefined, false, false, false, true, // hideUserMessage: this is a protocol continuation, not a new prompt
4044
+ undefined, [approvalKey]);
4043
4045
  },
4044
4046
  ...(approvalActions?.onDeny ? { onDeny: approvalActions.onDeny } : {}),
4045
4047
  ...(approvalActions?.onAlwaysAllow
@@ -4126,12 +4128,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4126
4128
  setDismissedRunErrorKey(visibleRunErrorKey);
4127
4129
  }
4128
4130
  setRunErrorInfo(null);
4129
- } }) })), (isReconnecting ||
4130
- reconnectFrozen ||
4131
- adapterHandoffPending) &&
4132
- visibleReconnectContent.length > 0 && (_jsx(MessageScrollerItem, { children: _jsx(ReconnectStreamMessage, { content: visibleReconnectContent, allowActivitySpinner: !reconnectFrozen }) })), (isReconnecting ||
4133
- reconnectFrozen ||
4134
- adapterHandoffPending) &&
4131
+ } }) })), showReconnectOverlay &&
4132
+ visibleReconnectContent.length > 0 && (_jsx(MessageScrollerItem, { children: _jsx(ReconnectStreamMessage, { content: visibleReconnectContent, allowActivitySpinner: !reconnectFrozen }) })), showReconnectOverlay &&
4135
4133
  visibleReconnectContent.length === 0 &&
4136
4134
  reconnectContent.length === 0 &&
4137
4135
  reconnectActivityContent.length > 0 && (_jsx(MessageScrollerItem, { children: _jsx(ReconnectStreamMessage, { content: reconnectActivityContent, allowActivitySpinner: !reconnectFrozen }) })), showGlobalRunningStatus && (_jsx(MessageScrollerItem, { children: _jsx(RunningActivityStatus, { label: runningStatusLabel }) })), visibleQueuedMessages.length > 0 && (_jsx(MessageScrollerItem, { children: _jsxs("div", { className: "flex items-center justify-end gap-1.5 pr-0.5 text-xs text-muted-foreground", children: [_jsx(IconClock, { className: "h-3 w-3" }), _jsxs("span", { children: [visibleQueuedMessages.length, " ", "queued"] })] }) })), visibleQueuedMessages.map((msg) => {
@@ -17,11 +17,24 @@ export interface PendingTurnState {
17
17
  }
18
18
  export declare function setActiveRun(state: ActiveRunState): void;
19
19
  export declare function getActiveRun(): ActiveRunState | null;
20
- export declare function updateActiveRunSeq(seq: number, isProgress?: boolean): void;
20
+ /**
21
+ * Advance one run's resume cursor. Identity is required: the previous
22
+ * signature took only `seq` and wrote into whichever run happened to occupy
23
+ * the shared slot, so a second concurrent run silently corrupted the first
24
+ * run's cursor. An unknown run is ignored rather than created — a cursor with
25
+ * no matching stream is indistinguishable from a real one at replay time.
26
+ */
27
+ export declare function updateActiveRunSeq(threadId: string, runId: string, seq: number, isProgress?: boolean): void;
21
28
  /** Whether this browser has received real progress for the matching run recently. */
22
29
  export declare function hasRecentActiveRunProgress(threadId: string | null | undefined, runId: string | null | undefined, thresholdMs: number, now?: number): boolean;
23
30
  export declare function updateActiveRunActivity(toolName: string | null | undefined): void;
24
31
  export declare function getActiveRunActivityTool(threadId: string, runId: string): string | null;
32
+ /**
33
+ * Drop the focused-run pointer. The run's resume cursor is intentionally kept:
34
+ * callers fire this from many unrelated teardown paths, and deleting the
35
+ * cursor here is what previously made a later reconnect replay the run from
36
+ * its first event.
37
+ */
25
38
  export declare function clearActiveRun(): void;
26
39
  export declare function clearActiveRunIfMatches(threadId: string, runId: string): void;
27
40
  export declare function setPendingTurn(state: PendingTurnState): void;
Binary file