@agent-native/core 0.80.10 → 0.81.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 (267) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +419 -19
  6. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  7. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  8. package/corpus/core/src/client/chat/index.ts +1 -0
  9. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  10. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  11. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  12. package/corpus/core/src/client/index.ts +1 -0
  13. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  15. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  17. package/corpus/core/src/db/client.ts +29 -2
  18. package/corpus/core/src/provider-api/index.ts +29 -0
  19. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  20. package/corpus/core/src/styles/agent-native.css +54 -0
  21. package/corpus/core/src/usage/store.ts +2 -2
  22. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  23. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  24. package/corpus/templates/analytics/app/global.css +8 -0
  25. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  26. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  27. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  29. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  30. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  31. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  36. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  38. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  39. package/corpus/templates/assets/AGENTS.md +4 -0
  40. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  42. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  43. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  44. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  46. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  47. package/corpus/templates/assets/app/global.css +15 -0
  48. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  49. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  50. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  51. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  52. package/corpus/templates/assets/app/routes/library.tsx +233 -52
  53. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  54. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  56. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  57. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  58. package/corpus/templates/assets/shared/api.ts +4 -0
  59. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  60. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  61. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  64. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  66. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  67. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  68. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  69. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  70. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  71. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  72. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  76. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  77. package/corpus/templates/design/AGENTS.md +27 -0
  78. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  79. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  80. package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
  81. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  82. package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
  83. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  84. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  85. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  86. package/corpus/templates/design/actions/delete-design.ts +20 -0
  87. package/corpus/templates/design/actions/export-zip.ts +23 -8
  88. package/corpus/templates/design/actions/generate-design.ts +53 -1
  89. package/corpus/templates/design/actions/get-component-details.ts +21 -26
  90. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  91. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  92. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  93. package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
  94. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  95. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  96. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  97. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  98. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  99. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  100. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  101. package/corpus/templates/design/actions/navigate.ts +18 -3
  102. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  103. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  104. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  105. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  106. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  107. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  108. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  109. package/corpus/templates/design/actions/run-design-audit.ts +74 -10
  110. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
  118. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
  120. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
  121. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
  122. package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
  123. package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
  124. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  125. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  126. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  127. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  128. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  129. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  131. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  132. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  133. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  134. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  136. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  137. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  138. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  139. package/corpus/templates/design/app/global.css +7 -0
  140. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  141. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  142. package/corpus/templates/design/app/i18n-data.ts +369 -0
  143. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
  145. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  146. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  171. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  172. package/corpus/templates/design/e2e/helpers.ts +168 -65
  173. package/corpus/templates/design/package.json +3 -1
  174. package/corpus/templates/design/server/db/schema.ts +23 -0
  175. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  176. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  177. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  178. package/corpus/templates/design/server/plugins/db.ts +39 -0
  179. package/corpus/templates/design/server/register-secrets.ts +38 -0
  180. package/corpus/templates/design/shared/board-file.ts +228 -0
  181. package/corpus/templates/design/shared/board-objects.ts +288 -0
  182. package/corpus/templates/design/shared/code-layer.ts +52 -0
  183. package/corpus/templates/design/shared/component-model.ts +35 -0
  184. package/corpus/templates/design/shared/motion-compiler.ts +137 -15
  185. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  186. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  187. package/corpus/templates/design/tsconfig.json +2 -1
  188. package/corpus/templates/slides/AGENTS.md +9 -0
  189. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  190. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  191. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  192. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  193. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  194. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  195. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  196. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  197. package/corpus/templates/slides/package.json +1 -0
  198. package/corpus/templates/slides/vite.config.ts +1 -0
  199. package/dist/agent/production-agent.d.ts.map +1 -1
  200. package/dist/agent/production-agent.js +94 -10
  201. package/dist/agent/production-agent.js.map +1 -1
  202. package/dist/cli/design-connect.d.ts +25 -0
  203. package/dist/cli/design-connect.d.ts.map +1 -1
  204. package/dist/cli/design-connect.js +357 -18
  205. package/dist/cli/design-connect.js.map +1 -1
  206. package/dist/client/AssistantChat.d.ts +14 -0
  207. package/dist/client/AssistantChat.d.ts.map +1 -1
  208. package/dist/client/AssistantChat.js +115 -22
  209. package/dist/client/AssistantChat.js.map +1 -1
  210. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  211. package/dist/client/ConnectBuilderCard.js +1 -0
  212. package/dist/client/ConnectBuilderCard.js.map +1 -1
  213. package/dist/client/chat/index.d.ts +1 -1
  214. package/dist/client/chat/index.d.ts.map +1 -1
  215. package/dist/client/chat/index.js +1 -1
  216. package/dist/client/chat/index.js.map +1 -1
  217. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  218. package/dist/client/chat/run-recovery.js +5 -5
  219. package/dist/client/chat/run-recovery.js.map +1 -1
  220. package/dist/client/chat-view-transition.d.ts +6 -0
  221. package/dist/client/chat-view-transition.d.ts.map +1 -1
  222. package/dist/client/chat-view-transition.js +23 -2
  223. package/dist/client/chat-view-transition.js.map +1 -1
  224. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  225. package/dist/client/composer/PromptComposer.js +3 -2
  226. package/dist/client/composer/PromptComposer.js.map +1 -1
  227. package/dist/client/index.d.ts +1 -1
  228. package/dist/client/index.d.ts.map +1 -1
  229. package/dist/client/index.js +1 -1
  230. package/dist/client/index.js.map +1 -1
  231. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  232. package/dist/client/org/OrgSwitcher.js +11 -4
  233. package/dist/client/org/OrgSwitcher.js.map +1 -1
  234. package/dist/client/sharing/ShareButton.js +3 -3
  235. package/dist/client/sharing/ShareButton.js.map +1 -1
  236. package/dist/client/sse-event-processor.d.ts.map +1 -1
  237. package/dist/client/sse-event-processor.js +10 -0
  238. package/dist/client/sse-event-processor.js.map +1 -1
  239. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  240. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  241. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  242. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  243. package/dist/collab/awareness.d.ts +2 -2
  244. package/dist/collab/awareness.d.ts.map +1 -1
  245. package/dist/collab/routes.d.ts +2 -2
  246. package/dist/db/client.d.ts +1 -0
  247. package/dist/db/client.d.ts.map +1 -1
  248. package/dist/db/client.js +28 -2
  249. package/dist/db/client.js.map +1 -1
  250. package/dist/notifications/routes.d.ts +3 -3
  251. package/dist/observability/routes.d.ts +3 -3
  252. package/dist/progress/routes.d.ts +1 -1
  253. package/dist/provider-api/index.d.ts +5 -4
  254. package/dist/provider-api/index.d.ts.map +1 -1
  255. package/dist/provider-api/index.js +28 -0
  256. package/dist/provider-api/index.js.map +1 -1
  257. package/dist/resources/handlers.d.ts +3 -3
  258. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  259. package/dist/server/core-routes-plugin.d.ts +25 -0
  260. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  261. package/dist/server/core-routes-plugin.js +43 -18
  262. package/dist/server/core-routes-plugin.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +54 -0
  265. package/dist/usage/store.js +2 -2
  266. package/dist/usage/store.js.map +1 -1
  267. package/package.json +1 -1
@@ -230,6 +230,12 @@ const PENDING_SELECTION_KEY = "pending-selection-context";
230
230
  const ACTIVE_RUN_CLEAR_TIMEOUT_MS = 5_000;
231
231
  const ACTIVE_RUN_STUCK_THRESHOLD_MS = 90_000;
232
232
  const ACTIVE_RUN_POLL_INTERVAL_MS = 150;
233
+ // How long a single activity (model call, tool prep, long tool) must stay
234
+ // in-flight before its label is surfaced in the running indicator. Below this
235
+ // the indicator stays a steady "Thinking" so normal fast turns don't flicker
236
+ // through transient labels ("Contacting model", "Preparing X action"); past it
237
+ // the live label appears so a genuinely slow step reads as working, not hung.
238
+ const ACTIVITY_LABEL_REVEAL_DELAY_MS = 6_000;
233
239
  const SUBMIT_ENGINE_STATUS_TIMEOUT_MS = 1000;
234
240
 
235
241
  type ActiveRunLookup = {
@@ -254,6 +260,24 @@ function activeRunLooksStale(runInfo: ActiveRunLookup): boolean {
254
260
  );
255
261
  }
256
262
 
263
+ /**
264
+ * Decide whether a reconnect should give up with "no progress". The signal is
265
+ * *time since the last streamed event*, NOT total reconnect duration: a healthy
266
+ * long-running tool (e.g. image generation, which emits `activity` heartbeats
267
+ * every few seconds) makes continuous progress and must never be aborted just
268
+ * for running longer than the threshold. Only genuine silence for the full
269
+ * stuck threshold counts as stuck. Pure + exported so the decision is unit
270
+ * testable without driving the whole reconnect lifecycle.
271
+ */
272
+ export function reconnectProgressTimedOut(args: {
273
+ lastProgressAt: number;
274
+ now: number;
275
+ thresholdMs?: number;
276
+ }): boolean {
277
+ const threshold = args.thresholdMs ?? ACTIVE_RUN_STUCK_THRESHOLD_MS;
278
+ return args.now - args.lastProgressAt >= threshold;
279
+ }
280
+
257
281
  function isAssistantUiDuplicateMessageIdError(error: unknown): boolean {
258
282
  const message = error instanceof Error ? error.message : String(error ?? "");
259
283
  return (
@@ -1323,6 +1347,24 @@ const AssistantChatInner = forwardRef<
1323
1347
  // True during the 250ms continuation window and startup of the next chunk
1324
1348
  // (adapter's auto-continue delay before POSTing the next chunk).
1325
1349
  const [isAutoResuming, setIsAutoResuming] = useState(false);
1350
+ const [runningActivityLabel, setRunningActivityLabel] = useState<
1351
+ string | null
1352
+ >(null);
1353
+ // Delayed-reveal state for the activity label (see ACTIVITY_LABEL_REVEAL_DELAY_MS).
1354
+ // `latest` holds the most recent activity label; `surfaced` flips true once the
1355
+ // reveal timer fires; `timer` is the pending one-shot reveal.
1356
+ const activityLabelTimerRef = useRef<number | null>(null);
1357
+ const latestActivityLabelRef = useRef<string | null>(null);
1358
+ const activityLabelSurfacedRef = useRef(false);
1359
+ const resetRunningActivity = useCallback(() => {
1360
+ if (activityLabelTimerRef.current !== null) {
1361
+ window.clearTimeout(activityLabelTimerRef.current);
1362
+ activityLabelTimerRef.current = null;
1363
+ }
1364
+ latestActivityLabelRef.current = null;
1365
+ activityLabelSurfacedRef.current = false;
1366
+ setRunningActivityLabel(null);
1367
+ }, []);
1326
1368
  const [reconnectContent, setReconnectContent] = useState<ContentPart[]>([]);
1327
1369
  // When stop is clicked during reconnect, keep content visible (don't wipe it)
1328
1370
  const [reconnectFrozen, setReconnectFrozen] = useState(false);
@@ -1341,6 +1383,19 @@ const AssistantChatInner = forwardRef<
1341
1383
  const textStreaming = isRunning || externalStreaming;
1342
1384
  // UI-only running state — drives the stop button and thinking indicator.
1343
1385
  const showRunningInUI = isRunning;
1386
+ // A revealed activity label wins; otherwise stay a steady "Thinking" by
1387
+ // default. We only surface "Reconnecting" while we are actively replaying
1388
+ // recovered content (reconnectContent populated). A bare reconnect with no
1389
+ // replayed content — e.g. a tail-resume after the serverless wall, where
1390
+ // `scheduleUpdate` intentionally leaves reconnectContent empty — is normal
1391
+ // ongoing work, so it must read as "Thinking", never a perpetual "Working".
1392
+ const runningStatusLabel = runningActivityLabel
1393
+ ? runningActivityLabel
1394
+ : isAutoResuming
1395
+ ? "Resuming"
1396
+ : isReconnecting && reconnectContent.length > 0
1397
+ ? "Reconnecting"
1398
+ : "Thinking";
1344
1399
  const lastBroadcastRunningRef = useRef(isRunning);
1345
1400
  const tiptapRef = useRef<TiptapComposerHandle>(null);
1346
1401
  // Stable ref to the "stop active run" action so addToQueue can abort
@@ -1608,11 +1663,32 @@ const AssistantChatInner = forwardRef<
1608
1663
  }, 1000);
1609
1664
 
1610
1665
  let reconnectTimedOut = false;
1611
- const maxReconnectTimer = setTimeout(() => {
1666
+ // Idle deadline, NOT a total-duration cap. A long-but-healthy run (image
1667
+ // generation emits `activity` heartbeats every few seconds) keeps
1668
+ // advancing `lastReconnectProgressAt`, so it is never aborted for simply
1669
+ // taking longer than the threshold. Only true silence for the full stuck
1670
+ // threshold trips it — the same semantics as `activeRunLooksStale` and the
1671
+ // SSE-level no-progress timeout. `markReconnectProgress` resets it on every
1672
+ // streamed event (see the `onSeq` callback below, which fires for every
1673
+ // event including activity, for both fresh and tail-resume reconnects).
1674
+ let lastReconnectProgressAt = Date.now();
1675
+ const markReconnectProgress = () => {
1676
+ lastReconnectProgressAt = Date.now();
1677
+ };
1678
+ const idleCheck = setInterval(() => {
1679
+ if (
1680
+ !reconnectProgressTimedOut({
1681
+ lastProgressAt: lastReconnectProgressAt,
1682
+ now: Date.now(),
1683
+ })
1684
+ ) {
1685
+ return;
1686
+ }
1612
1687
  reconnectTimedOut = true;
1613
1688
  abortCtrl.abort();
1614
1689
  clearInterval(watchdog);
1615
- }, 20_000);
1690
+ clearInterval(idleCheck);
1691
+ }, 1000);
1616
1692
 
1617
1693
  const streamReconnect = async () => {
1618
1694
  let noProgressDuringReconnect = false;
@@ -1653,7 +1729,10 @@ const AssistantChatInner = forwardRef<
1653
1729
  toolCallCounter,
1654
1730
  tabId,
1655
1731
  scheduleUpdate,
1656
- (seq) => updateActiveRunSeq(seq),
1732
+ (seq) => {
1733
+ markReconnectProgress();
1734
+ updateActiveRunSeq(seq);
1735
+ },
1657
1736
  );
1658
1737
  if (afterSeq === 0) {
1659
1738
  setReconnectContent([...content]);
@@ -1677,7 +1756,7 @@ const AssistantChatInner = forwardRef<
1677
1756
  window.clearInterval(threadPollInterval);
1678
1757
  }
1679
1758
  clearInterval(watchdog);
1680
- clearTimeout(maxReconnectTimer);
1759
+ clearInterval(idleCheck);
1681
1760
  }
1682
1761
 
1683
1762
  if (noProgressDuringReconnect && reconnectRunIdRef.current === runId) {
@@ -2235,11 +2314,9 @@ const AssistantChatInner = forwardRef<
2235
2314
  return () => window.removeEventListener("agent-chat:run-error", handler);
2236
2315
  }, [tabId]);
2237
2316
 
2238
- // Real activity means the next chunk has started leave the auto-resume
2239
- // ("Resuming") state so the indicator settles back to "Thinking". The
2240
- // activity label itself is intentionally not surfaced: the running
2241
- // indicator stays a steady "Thinking" rather than flipping through
2242
- // transient step labels.
2317
+ // Real activity means the next chunk has started. Surface longer-lived
2318
+ // activity such as "Still generating image" so active-run reconnects do not
2319
+ // look like failures while the server is still making progress.
2243
2320
  useEffect(() => {
2244
2321
  const handler = (e: Event) => {
2245
2322
  const detail = (e as CustomEvent).detail as {
@@ -2248,13 +2325,43 @@ const AssistantChatInner = forwardRef<
2248
2325
  tabId?: string;
2249
2326
  };
2250
2327
  if (tabId && detail?.tabId && detail.tabId !== tabId) return;
2251
- if (typeof detail?.label === "string" && detail.label.trim()) {
2252
- setIsAutoResuming(false);
2328
+ const label =
2329
+ typeof detail?.label === "string" ? detail.label.trim() : "";
2330
+ if (!label) return;
2331
+ setIsAutoResuming(false);
2332
+ latestActivityLabelRef.current = label;
2333
+ // Already past the delay → keep the visible label current.
2334
+ if (activityLabelSurfacedRef.current) {
2335
+ setRunningActivityLabel(label);
2336
+ return;
2337
+ }
2338
+ // Not yet surfaced → arm a single reveal timer on the FIRST activity of
2339
+ // this in-flight period. A burst of quick steps that all finish (clear)
2340
+ // before the timer fires never surfaces anything, so normal fast turns
2341
+ // stay a steady "Thinking". A step still in-flight at the deadline reveals
2342
+ // the latest label so a slow operation reads as working, not hung.
2343
+ if (activityLabelTimerRef.current === null) {
2344
+ activityLabelTimerRef.current = window.setTimeout(() => {
2345
+ activityLabelTimerRef.current = null;
2346
+ activityLabelSurfacedRef.current = true;
2347
+ if (latestActivityLabelRef.current) {
2348
+ setRunningActivityLabel(latestActivityLabelRef.current);
2349
+ }
2350
+ }, ACTIVITY_LABEL_REVEAL_DELAY_MS);
2253
2351
  }
2254
2352
  };
2353
+ const clear = (e: Event) => {
2354
+ const detail = (e as CustomEvent).detail as { tabId?: string };
2355
+ if (tabId && detail?.tabId && detail.tabId !== tabId) return;
2356
+ resetRunningActivity();
2357
+ };
2255
2358
  window.addEventListener("agent-chat:activity", handler);
2256
- return () => window.removeEventListener("agent-chat:activity", handler);
2257
- }, [tabId]);
2359
+ window.addEventListener("agent-chat:activity-clear", clear);
2360
+ return () => {
2361
+ window.removeEventListener("agent-chat:activity", handler);
2362
+ window.removeEventListener("agent-chat:activity-clear", clear);
2363
+ };
2364
+ }, [resetRunningActivity, tabId]);
2258
2365
 
2259
2366
  // Show "Resuming…" during the adapter's auto-continuation window (the
2260
2367
  // ~250ms gap between the end of one serverless chunk and the POST for the
@@ -2274,8 +2381,9 @@ const AssistantChatInner = forwardRef<
2274
2381
  useEffect(() => {
2275
2382
  if (!isRunning) {
2276
2383
  setIsAutoResuming(false);
2384
+ resetRunningActivity();
2277
2385
  }
2278
- }, [isRunning]);
2386
+ }, [isRunning, resetRunningActivity]);
2279
2387
 
2280
2388
  // Auto-dequeue: when the agent is idle, send the next queued message. This
2281
2389
  // intentionally does not depend on observing the running -> idle transition:
@@ -3336,18 +3444,7 @@ const AssistantChatInner = forwardRef<
3336
3444
  <ReconnectStreamMessage content={reconnectContent} />
3337
3445
  )}
3338
3446
  {showRunningInUI && (
3339
- <RunningActivityStatus
3340
- label={
3341
- isReconnecting
3342
- ? "Reconnecting"
3343
- : isAutoResuming
3344
- ? "Resuming"
3345
- : // Keep a steady "Thinking" while the model works —
3346
- // never flip through transient activity labels
3347
- // (e.g. "Contacting model", "Preparing X action").
3348
- "Thinking"
3349
- }
3350
- />
3447
+ <RunningActivityStatus label={runningStatusLabel} />
3351
3448
  )}
3352
3449
  {queuedMessages.map((msg) => {
3353
3450
  const displayText = msg.text
@@ -154,6 +154,7 @@ export function ConnectBuilderCard({
154
154
  orgName,
155
155
  pageUrl: window.location.href,
156
156
  source: "connect_builder_card",
157
+ useCase: "builder_agent_background_coding",
157
158
  }),
158
159
  },
159
160
  );
@@ -16,6 +16,7 @@ export {
16
16
  AGENT_CHAT_VIEW_TRANSITION_NAME,
17
17
  consumeAgentChatHomeHandoff,
18
18
  getAgentChatViewTransitionStyle,
19
+ isAgentChatHomeHandoffActive,
19
20
  markAgentChatHomeHandoff,
20
21
  navigateWithAgentChatViewTransition,
21
22
  startAgentChatViewTransition,
@@ -195,7 +195,7 @@ export function BuilderConnectCta({
195
195
  if (variant === "compact") {
196
196
  if (configured) {
197
197
  return (
198
- <span className="inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-md border border-border bg-background px-2.5 text-[11px] font-medium text-foreground">
198
+ <span className="agent-builder-setup-card__builder-button inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-md border border-border bg-background px-2.5 text-[11px] font-medium text-foreground">
199
199
  <IconCheck size={11} className="text-emerald-500" />
200
200
  {orgName ? `Connected to ${orgName}` : "Connected"}
201
201
  </span>
@@ -203,12 +203,12 @@ export function BuilderConnectCta({
203
203
  }
204
204
 
205
205
  return (
206
- <div className="flex min-w-0 flex-col items-start gap-1 sm:items-end">
206
+ <div className="agent-builder-setup-card__builder-cta flex min-w-0 flex-col items-start gap-1 sm:items-end">
207
207
  <button
208
208
  type="button"
209
209
  onClick={() => start()}
210
210
  disabled={connecting}
211
- className="inline-flex h-8 shrink-0 items-center gap-1 whitespace-nowrap rounded-md bg-foreground px-3 text-[11px] font-medium text-background hover:opacity-90 disabled:cursor-wait disabled:opacity-60"
211
+ className="agent-builder-setup-card__builder-button inline-flex h-8 shrink-0 items-center gap-1 whitespace-nowrap rounded-md bg-foreground px-3 text-[11px] font-medium text-background hover:opacity-90 disabled:cursor-wait disabled:opacity-60"
212
212
  aria-busy={connecting}
213
213
  >
214
214
  {connecting ? (
@@ -433,6 +433,8 @@ export function BuilderSetupCard({
433
433
  <div
434
434
  ref={cardRef}
435
435
  className={cn(
436
+ "agent-builder-setup-card",
437
+ sidebarLayout && "agent-builder-setup-card--sidebar",
436
438
  fullWidth
437
439
  ? "w-full pb-2"
438
440
  : sidebarLayout
@@ -441,8 +443,8 @@ export function BuilderSetupCard({
441
443
  )}
442
444
  >
443
445
  <div className="rounded-lg border border-border/80 bg-background/80 p-3 shadow-sm backdrop-blur">
444
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
445
- <div className="min-w-0">
446
+ <div className="agent-builder-setup-card__content flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
447
+ <div className="agent-builder-setup-card__copy min-w-0">
446
448
  <h3 className="text-[13px] font-medium text-foreground">
447
449
  Connect AI
448
450
  </h3>
@@ -452,7 +454,7 @@ export function BuilderSetupCard({
452
454
  </div>
453
455
  <div
454
456
  className={cn(
455
- "flex shrink-0",
457
+ "agent-builder-setup-card__actions flex shrink-0",
456
458
  sidebarLayout
457
459
  ? "flex-col items-start gap-0 sm:items-center"
458
460
  : "flex-nowrap items-center gap-2",
@@ -463,7 +465,7 @@ export function BuilderSetupCard({
463
465
  type="button"
464
466
  onClick={() => setKeyOpen((open) => !open)}
465
467
  className={cn(
466
- "inline-flex shrink-0 items-center whitespace-nowrap rounded-md text-[11px] font-medium",
468
+ "agent-builder-setup-card__key-button inline-flex shrink-0 items-center whitespace-nowrap rounded-md text-[11px] font-medium",
467
469
  sidebarLayout
468
470
  ? "h-7 border-0 bg-transparent px-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
469
471
  : "h-8 border border-border bg-background px-3 text-foreground hover:bg-accent",
@@ -132,6 +132,31 @@ export function markAgentChatHomeHandoff(storageKey?: string | null): void {
132
132
  } catch {}
133
133
  }
134
134
 
135
+ /**
136
+ * Check whether a full-page-chat handoff marker is still recent without
137
+ * consuming it. Use this when a chat route should only restore or animate a
138
+ * conversation if the user actually chatted a moment ago.
139
+ */
140
+ export function isAgentChatHomeHandoffActive(
141
+ storageKey?: string | null,
142
+ options: AgentChatHomeHandoffOptions = {},
143
+ ): boolean {
144
+ if (typeof window === "undefined") return false;
145
+
146
+ let startedAt = 0;
147
+ try {
148
+ const raw = window.sessionStorage.getItem(
149
+ agentChatHomeHandoffKey(storageKey),
150
+ );
151
+ startedAt = raw ? Number.parseInt(raw, 10) : 0;
152
+ } catch {
153
+ startedAt = 0;
154
+ }
155
+
156
+ const ttlMs = options.ttlMs ?? AGENT_CHAT_HOME_HANDOFF_TTL_MS;
157
+ return Number.isFinite(startedAt) && Date.now() - startedAt <= ttlMs;
158
+ }
159
+
135
160
  /**
136
161
  * Consume a recent full-page-chat handoff marker. Returns true only once per
137
162
  * marker, so layouts can keep `openOnChatRunning` scoped to the route that
@@ -153,9 +178,15 @@ export function consumeAgentChatHomeHandoff(
153
178
  startedAt = 0;
154
179
  }
155
180
 
181
+ return isAgentChatHomeHandoffActiveFromTimestamp(startedAt, options);
182
+ }
183
+
184
+ function isAgentChatHomeHandoffActiveFromTimestamp(
185
+ startedAt: number,
186
+ options: AgentChatHomeHandoffOptions,
187
+ ): boolean {
156
188
  const ttlMs = options.ttlMs ?? AGENT_CHAT_HOME_HANDOFF_TTL_MS;
157
- const active = Number.isFinite(startedAt) && Date.now() - startedAt <= ttlMs;
158
- return active;
189
+ return Number.isFinite(startedAt) && Date.now() - startedAt <= ttlMs;
159
190
  }
160
191
 
161
192
  /**
@@ -30,7 +30,7 @@ import {
30
30
 
31
31
  import type { ReasoningEffort } from "../../shared/reasoning-effort.js";
32
32
  import { AssistantUiStaleIndexErrorBoundary } from "../assistant-ui-recovery.js";
33
- import { BuilderSetupCard } from "../chat/run-recovery.js";
33
+ import { BuilderConnectCta, BuilderSetupCard } from "../chat/run-recovery.js";
34
34
  import { TooltipProvider } from "../components/ui/tooltip.js";
35
35
  import {
36
36
  fetchAgentEngineConfiguredState,
@@ -578,10 +578,11 @@ function PromptComposerInner({
578
578
  },
579
579
  [composerEffort, composerEngine, composerModel, onSubmit],
580
580
  );
581
+ const useInlineMissingKeySetup = layoutVariant === "compact";
581
582
 
582
583
  return (
583
584
  <>
584
- {missingApiKey ? (
585
+ {missingApiKey && !useInlineMissingKeySetup ? (
585
586
  <BuilderSetupCard
586
587
  onConnected={handleBuilderConnected}
587
588
  bouncePulse={missingKeyBouncePulse}
@@ -589,6 +590,24 @@ function PromptComposerInner({
589
590
  layout="sidebar"
590
591
  />
591
592
  ) : null}
593
+ {missingApiKey && useInlineMissingKeySetup ? (
594
+ <div className="mb-2 rounded-md border border-border/80 bg-background/80 p-2.5 text-start shadow-sm">
595
+ <div className="flex flex-col gap-2">
596
+ <div className="min-w-0">
597
+ <p className="text-[12px] font-medium text-foreground">
598
+ Connect AI
599
+ </p>
600
+ <p className="mt-0.5 text-[11px] leading-4 text-muted-foreground">
601
+ Connect Builder.io before sending.
602
+ </p>
603
+ </div>
604
+ <BuilderConnectCta
605
+ variant="compact"
606
+ onConnected={handleBuilderConnected}
607
+ />
608
+ </div>
609
+ </div>
610
+ ) : null}
592
611
  <AgentComposerFrame
593
612
  className={cn(
594
613
  "text-start",
@@ -529,6 +529,7 @@ export {
529
529
  AGENT_CHAT_VIEW_TRANSITION_NAME,
530
530
  consumeAgentChatHomeHandoff,
531
531
  getAgentChatViewTransitionStyle,
532
+ isAgentChatHomeHandoffActive,
532
533
  markAgentChatHomeHandoff,
533
534
  navigateWithAgentChatViewTransition,
534
535
  startAgentChatViewTransition,
@@ -97,6 +97,9 @@ const SECTION_LABEL_CLASS =
97
97
  const APP_SUBMENU_CONTENT_CLASS =
98
98
  "z-50 w-72 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2";
99
99
 
100
+ const SWITCHER_BUTTON_CLASS =
101
+ "flex w-full items-center gap-2 rounded-md border border-border/70 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-60 cursor-pointer";
102
+
100
103
  const DEFAULT_ORGANIZATION_SETTINGS_PATH = "/settings#team";
101
104
 
102
105
  const APP_ICON_MAP: Record<string, typeof IconApps> = {
@@ -274,6 +277,25 @@ function AppsSubmenu({
274
277
  );
275
278
  }
276
279
 
280
+ function ReservedOrgSwitcherSpace({ className }: { className?: string }) {
281
+ return <div aria-hidden="true" className={`h-8 ${className ?? ""}`} />;
282
+ }
283
+
284
+ function OrgSwitcherLoadingPlaceholder({ className }: { className?: string }) {
285
+ return (
286
+ <button
287
+ type="button"
288
+ disabled
289
+ aria-label="Loading organization"
290
+ className={`${SWITCHER_BUTTON_CLASS} animate-pulse ${className ?? ""}`}
291
+ >
292
+ <IconBuilding className="h-3.5 w-3.5 shrink-0 opacity-60" />
293
+ <span className="h-3 min-w-0 flex-1 rounded-sm bg-muted-foreground/20" />
294
+ <IconSelector className="h-3 w-3 shrink-0 opacity-30" />
295
+ </button>
296
+ );
297
+ }
298
+
277
299
  /**
278
300
  * Compact org switcher button. Shows the active org (or "Personal" when the
279
301
  * user has none); opens a popover with the user's other orgs, pending
@@ -326,7 +348,7 @@ export function OrgSwitcher({
326
348
 
327
349
  if (!org) {
328
350
  return reserveSpace && isLoading ? (
329
- <div aria-hidden="true" className={`h-8 ${className ?? ""}`} />
351
+ <OrgSwitcherLoadingPlaceholder className={className} />
330
352
  ) : null;
331
353
  }
332
354
 
@@ -338,7 +360,7 @@ export function OrgSwitcher({
338
360
  orgCount > 0 || pendingInvitations.length > 0 || domainMatches.length > 0;
339
361
  if (!hasAny && !org.email) {
340
362
  return reserveSpace ? (
341
- <div aria-hidden="true" className={`h-8 ${className ?? ""}`} />
363
+ <ReservedOrgSwitcherSpace className={className} />
342
364
  ) : null;
343
365
  }
344
366
  if (
@@ -348,7 +370,7 @@ export function OrgSwitcher({
348
370
  domainMatches.length === 0
349
371
  ) {
350
372
  return reserveSpace ? (
351
- <div aria-hidden="true" className={`h-8 ${className ?? ""}`} />
373
+ <ReservedOrgSwitcherSpace className={className} />
352
374
  ) : null;
353
375
  }
354
376
 
@@ -368,7 +390,7 @@ export function OrgSwitcher({
368
390
  <PopoverPrimitive.Trigger asChild>
369
391
  <button
370
392
  type="button"
371
- className={`flex w-full items-center gap-2 rounded-md border border-border/70 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring cursor-pointer ${className ?? ""}`}
393
+ className={`${SWITCHER_BUTTON_CLASS} ${className ?? ""}`}
372
394
  >
373
395
  <ButtonIcon className="h-3.5 w-3.5 shrink-0" />
374
396
  <span className="truncate flex-1 text-start">{buttonLabel}</span>
@@ -153,13 +153,13 @@ interface SharesResponse {
153
153
  policy?: SharesPolicy;
154
154
  }
155
155
 
156
- // Mirror shadcn's <Button size="sm" variant="outline"> class string so the
157
- // trigger sits flush next to other sm outline buttons in the template.
156
+ // Match shadcn's <Button size="sm" variant="outline"> sizing so the trigger
157
+ // sits flush next to other controls while staying transparent at rest.
158
158
  const BUTTON_BASE =
159
159
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
160
160
  const BUTTON_OUTLINE_SM = cn(
161
161
  BUTTON_BASE,
162
- "h-9 px-3 border border-[hsl(var(--sidebar-border,var(--input)))] bg-[hsl(var(--sidebar-background,var(--background)))] text-foreground hover:bg-[hsl(var(--sidebar-accent,var(--accent)))] hover:text-[hsl(var(--sidebar-accent-foreground,var(--accent-foreground)))]",
162
+ "h-9 px-3 border border-[hsl(var(--sidebar-border,var(--input)))] bg-transparent text-foreground hover:bg-[hsl(var(--sidebar-accent,var(--accent)))] hover:text-[hsl(var(--sidebar-accent-foreground,var(--accent-foreground)))]",
163
163
  );
164
164
  const BUTTON_PRIMARY_SM = cn(
165
165
  BUTTON_BASE,
@@ -352,6 +352,11 @@ export function processEvent(
352
352
  }
353
353
 
354
354
  if (ev.type === "text") {
355
+ // Visible output means the run is plainly not hanging — drop any running
356
+ // activity label so a transient "Contacting model" / "Still generating
357
+ // image" doesn't linger beside streamed text. Idempotent (clears once, then
358
+ // no-ops) so per-token text deltas stay cheap.
359
+ if (ev.text) dispatchActivityClear(tabId);
355
360
  const lastPart = content[content.length - 1];
356
361
  if (lastPart && lastPart.type === "text") {
357
362
  lastPart.text += ev.text ?? "";
@@ -492,6 +497,10 @@ export function processEvent(
492
497
  }),
493
498
  );
494
499
  }
500
+ // Clear any sticky running-activity label (e.g. "Still generating image"):
501
+ // the tool that was emitting activity heartbeats has finished, so the label
502
+ // must not linger while the model streams its follow-up text or reasoning.
503
+ dispatchActivityClear(tabId);
495
504
  // Use id-based lookup when available so parallel same-name tool calls
496
505
  // get their results correctly assigned; fall back to name-matching.
497
506
  const doneIdx = findPendingToolCallIndex(content, doneTool, ev.id);
@@ -4,6 +4,7 @@ import { useLocation, useNavigate } from "react-router";
4
4
  import { appBasePath } from "./api-path.js";
5
5
  import {
6
6
  consumeAgentChatHomeHandoff,
7
+ isAgentChatHomeHandoffActive,
7
8
  markAgentChatHomeHandoff,
8
9
  navigateWithAgentChatViewTransition,
9
10
  } from "./chat-view-transition.js";
@@ -13,6 +14,8 @@ export interface UseAgentChatHomeHandoffOptions {
13
14
  storageKey?: string | null;
14
15
  /** The current destination path that receives the sidebar handoff. */
15
16
  activePath: string;
17
+ /** How long the handoff marker remains valid. Defaults to 6 hours. */
18
+ ttlMs?: number;
16
19
  /** Disable consumption without changing hook call order. */
17
20
  enabled?: boolean;
18
21
  }
@@ -30,6 +33,10 @@ export interface UseAgentChatHomeHandoffLinksOptions {
30
33
  isChatPath?: (pathname: string) => boolean;
31
34
  /** Disable link interception without changing hook call order. */
32
35
  enabled?: boolean;
36
+ /** How long the handoff marker remains valid. Defaults to 6 hours. */
37
+ ttlMs?: number;
38
+ /** Only intercept links if a recent handoff marker already exists. */
39
+ requireActiveHandoff?: boolean;
33
40
  }
34
41
 
35
42
  function stripBasePath(path: string): string {
@@ -96,6 +103,10 @@ function shouldHandleAnchorClick(
96
103
  return true;
97
104
  }
98
105
 
106
+ function handoffTtlOptions(ttlMs: number | undefined) {
107
+ return ttlMs === undefined ? undefined : { ttlMs };
108
+ }
109
+
99
110
  /**
100
111
  * Returns true for the route that has just received a full-page-chat handoff.
101
112
  * Pass the result to `AgentSidebar openOnChatRunning`.
@@ -103,6 +114,7 @@ function shouldHandleAnchorClick(
103
114
  export function useAgentChatHomeHandoff({
104
115
  storageKey,
105
116
  activePath,
117
+ ttlMs,
106
118
  enabled = true,
107
119
  }: UseAgentChatHomeHandoffOptions): boolean {
108
120
  const [handoffPath, setHandoffPath] = useState<string | null>(null);
@@ -112,10 +124,10 @@ export function useAgentChatHomeHandoff({
112
124
  setHandoffPath(null);
113
125
  return;
114
126
  }
115
- if (consumeAgentChatHomeHandoff(storageKey)) {
127
+ if (consumeAgentChatHomeHandoff(storageKey, handoffTtlOptions(ttlMs))) {
116
128
  setHandoffPath(activePath);
117
129
  }
118
- }, [activePath, enabled, storageKey]);
130
+ }, [activePath, enabled, storageKey, ttlMs]);
119
131
 
120
132
  return enabled && handoffPath === activePath;
121
133
  }
@@ -129,6 +141,8 @@ export function useAgentChatHomeHandoffLinks({
129
141
  chatPath = "/",
130
142
  isChatPath,
131
143
  enabled = true,
144
+ ttlMs,
145
+ requireActiveHandoff = false,
132
146
  }: UseAgentChatHomeHandoffLinksOptions): void {
133
147
  const location = useLocation();
134
148
  const navigate = useNavigate();
@@ -161,6 +175,13 @@ export function useAgentChatHomeHandoffLinks({
161
175
  return;
162
176
  }
163
177
 
178
+ if (
179
+ requireActiveHandoff &&
180
+ !isAgentChatHomeHandoffActive(storageKey, handoffTtlOptions(ttlMs))
181
+ ) {
182
+ return;
183
+ }
184
+
164
185
  event.preventDefault();
165
186
  markAgentChatHomeHandoff(storageKey);
166
187
  navigateWithAgentChatViewTransition(navigate, path);
@@ -168,5 +189,12 @@ export function useAgentChatHomeHandoffLinks({
168
189
 
169
190
  document.addEventListener("click", handleClick, true);
170
191
  return () => document.removeEventListener("click", handleClick, true);
171
- }, [enabled, location.pathname, navigate, storageKey]);
192
+ }, [
193
+ enabled,
194
+ location.pathname,
195
+ navigate,
196
+ requireActiveHandoff,
197
+ storageKey,
198
+ ttlMs,
199
+ ]);
172
200
  }