@agent-native/core 0.98.0 → 0.98.1

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 (243) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +24 -0
  3. package/corpus/core/package.json +2 -2
  4. package/corpus/core/src/action-change-marker.ts +8 -1
  5. package/corpus/core/src/changelog/parse.ts +8 -3
  6. package/corpus/core/src/client/AgentPanel.tsx +2 -3
  7. package/corpus/core/src/client/AssistantChat.tsx +83 -39
  8. package/corpus/core/src/client/analytics.ts +29 -0
  9. package/corpus/core/src/client/blocks/library/wireframe-kit.tsx +16 -2
  10. package/corpus/core/src/client/chat-model-groups.ts +28 -0
  11. package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +442 -85
  12. package/corpus/core/src/client/composer/TiptapComposer.tsx +46 -29
  13. package/corpus/core/src/client/composer/VoiceButton.tsx +2 -1
  14. package/corpus/core/src/client/composer/index.ts +5 -0
  15. package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +1206 -67
  16. package/corpus/core/src/client/index.ts +5 -0
  17. package/corpus/core/src/client/progress/RunsTray.tsx +13 -1
  18. package/corpus/core/src/client/settings/SecretsSection.tsx +387 -266
  19. package/corpus/core/src/client/settings/SettingsPanel.tsx +117 -38
  20. package/corpus/core/src/client/use-action.ts +12 -0
  21. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +8 -1
  22. package/corpus/core/src/client/use-agent-engine-configured.ts +17 -3
  23. package/corpus/core/src/client/use-db-sync.ts +84 -34
  24. package/corpus/core/src/client/use-pausing-interval.ts +14 -4
  25. package/corpus/core/src/integrations/scope-store.ts +1 -1
  26. package/corpus/core/src/integrations/usage-budget-store.ts +1 -1
  27. package/corpus/core/src/localization/default-messages.ts +44 -1
  28. package/corpus/core/src/mcp-client/workspace-servers.ts +4 -4
  29. package/corpus/core/src/secrets/crypto.ts +28 -8
  30. package/corpus/core/src/server/action-change.ts +5 -1
  31. package/corpus/core/src/server/action-routes.ts +8 -0
  32. package/corpus/core/src/server/poll.ts +1 -0
  33. package/corpus/core/src/server/realtime-voice.ts +299 -13
  34. package/corpus/core/src/styles/agent-native.css +2 -2
  35. package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  36. package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  37. package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
  39. package/corpus/core/src/vite/client.ts +9 -1
  40. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +2 -2
  41. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +10 -5
  42. package/corpus/templates/analytics/AGENTS.md +9 -13
  43. package/corpus/templates/analytics/actions/navigate.ts +59 -5
  44. package/corpus/templates/analytics/actions/view-screen.ts +3 -6
  45. package/corpus/templates/analytics/app/components/ProviderCorpusJobNotifier.tsx +20 -3
  46. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +2 -1
  47. package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -1
  48. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -6
  49. package/corpus/templates/analytics/app/global.css +19 -20
  50. package/corpus/templates/analytics/app/lib/provider-corpus-job-sync.ts +26 -0
  51. package/corpus/templates/analytics/app/pages/Settings.tsx +35 -341
  52. package/corpus/templates/analytics/app/pages/monitoring/ErrorsPanel.tsx +38 -7
  53. package/corpus/templates/analytics/app/pages/monitoring/MonitoringPage.tsx +1 -1
  54. package/corpus/templates/analytics/app/pages/monitoring/errors/i18n.ts +3 -0
  55. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +333 -272
  56. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +72 -17
  57. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +297 -108
  58. package/corpus/templates/analytics/app/root.tsx +28 -8
  59. package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +2 -1
  60. package/corpus/templates/analytics/changelog/2026-06-25-referrer-dashboard-panels-show-external-domains.md +2 -1
  61. package/corpus/templates/analytics/changelog/2026-07-06-mobile-chat-header-is-more-compact-and-the-menu-button-opens-navigation.md +1 -0
  62. package/corpus/templates/analytics/changelog/2026-07-08-dashboard-history-and-chat-context.md +1 -0
  63. package/corpus/templates/analytics/changelog/2026-07-08-settings-are-cleaner-searchable-and-alerts-have-their-own-tab.md +1 -0
  64. package/corpus/templates/analytics/changelog/2026-07-08-sidebar-sections-stay-collapsed-until-opened.md +1 -0
  65. package/corpus/templates/analytics/changelog/2026-07-11-analytics-no-longer-repeatedly-restarts-background-requests-.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-07-11-analytics-only-asks-for-replay-storage-when-you-enable-sessi.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-07-11-charts-now-show-their-loading-skeleton-while-refreshing.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-07-11-realtime-voice-now-shows-a-clear-connection-indicator-comple.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-07-11-replay-event-counts-stay-hidden-until-recordings-finish-load.md +6 -0
  70. package/corpus/templates/analytics/changelog/2026-07-11-replay-storage-key-mismatches-show-actionable-setup-guidance.md +6 -0
  71. package/corpus/templates/analytics/changelog/2026-07-11-replay-timelines-group-continuous-scrolling-and-dev-tools-co.md +6 -0
  72. package/corpus/templates/analytics/changelog/2026-07-11-session-filters-now-stay-compact-in-a-settings-menu-at-mediu.md +6 -0
  73. package/corpus/templates/analytics/changelog/2026-07-11-session-replay-console-errors-now-link-to-matching-issues-or.md +6 -0
  74. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-load-large-recordings-faster-and-surface-fai.md +6 -0
  75. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-preserve-recorded-styles-and-viewport-layout.md +6 -0
  76. package/corpus/templates/analytics/changelog/2026-07-11-session-replays-stay-centered-instead-of-clipping-wide-recor.md +6 -0
  77. package/corpus/templates/analytics/package.json +1 -1
  78. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -303
  79. package/corpus/templates/analytics/server/handlers/session-replay.ts +57 -0
  80. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
  81. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +16 -3
  82. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +12 -0
  83. package/corpus/templates/analytics/server/lib/session-replay.ts +208 -2
  84. package/corpus/templates/analytics/server/plugins/onboarding.ts +7 -79
  85. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks.get.ts +1 -0
  86. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +2 -2
  87. package/corpus/templates/assets/app/global.css +4 -4
  88. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +0 -1
  89. package/corpus/templates/assets/app/routes/library.tsx +0 -1
  90. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +2 -2
  91. package/corpus/templates/brain/app/global.css +5 -5
  92. package/corpus/templates/brain/app/root.tsx +0 -3
  93. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +2 -2
  94. package/corpus/templates/calendar/app/root.tsx +1 -0
  95. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +2 -2
  96. package/corpus/templates/clips/app/hooks/use-auto-title.ts +74 -8
  97. package/corpus/templates/clips/app/hooks/use-library.ts +12 -25
  98. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +20 -22
  99. package/corpus/templates/clips/changelog/2026-07-11-generated-titles-no-longer-cause-continuous-background-refreshes.md +6 -0
  100. package/corpus/templates/clips/changelog/2026-07-11-s3-storage-setup-stays-collapsed-until-you-choose-it-as-the-.md +6 -0
  101. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +2 -2
  102. package/corpus/templates/content/app/hooks/use-comments.ts +0 -1
  103. package/corpus/templates/content/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
  104. package/corpus/templates/design/app/components/layout/Header.tsx +1 -1
  105. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +2 -2
  106. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -2
  107. package/corpus/templates/macros/app/root.tsx +0 -3
  108. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +2 -2
  109. package/corpus/templates/mail/app/hooks/use-compose-state.ts +1 -0
  110. package/corpus/templates/mail/app/root.tsx +1 -12
  111. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +2 -2
  112. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +5 -2
  113. package/corpus/templates/plan/app/hooks/use-plans.ts +6 -13
  114. package/corpus/templates/plan/app/pages/PlansPage.tsx +5 -27
  115. package/corpus/templates/plan/changelog/2026-07-11-plan-refreshes-only-when-content-changes.md +6 -0
  116. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +4 -1
  117. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +1 -1
  118. package/corpus/templates/slides/app/components/layout/Header.tsx +1 -1
  119. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -0
  120. package/corpus/templates/slides/app/hooks/use-slide-comments.ts +7 -31
  121. package/corpus/templates/slides/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
  122. package/dist/action-change-marker.d.ts +1 -0
  123. package/dist/action-change-marker.d.ts.map +1 -1
  124. package/dist/action-change-marker.js +7 -1
  125. package/dist/action-change-marker.js.map +1 -1
  126. package/dist/changelog/parse.d.ts +4 -2
  127. package/dist/changelog/parse.d.ts.map +1 -1
  128. package/dist/changelog/parse.js +5 -3
  129. package/dist/changelog/parse.js.map +1 -1
  130. package/dist/client/AgentPanel.d.ts.map +1 -1
  131. package/dist/client/AgentPanel.js +2 -3
  132. package/dist/client/AgentPanel.js.map +1 -1
  133. package/dist/client/AssistantChat.d.ts +7 -0
  134. package/dist/client/AssistantChat.d.ts.map +1 -1
  135. package/dist/client/AssistantChat.js +62 -40
  136. package/dist/client/AssistantChat.js.map +1 -1
  137. package/dist/client/analytics.d.ts.map +1 -1
  138. package/dist/client/analytics.js +26 -0
  139. package/dist/client/analytics.js.map +1 -1
  140. package/dist/client/blocks/library/wireframe-kit.d.ts +2 -0
  141. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -1
  142. package/dist/client/blocks/library/wireframe-kit.js +9 -2
  143. package/dist/client/blocks/library/wireframe-kit.js.map +1 -1
  144. package/dist/client/chat-model-groups.d.ts.map +1 -1
  145. package/dist/client/chat-model-groups.js +24 -0
  146. package/dist/client/chat-model-groups.js.map +1 -1
  147. package/dist/client/composer/RealtimeVoiceMode.d.ts +40 -4
  148. package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
  149. package/dist/client/composer/RealtimeVoiceMode.js +172 -42
  150. package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
  151. package/dist/client/composer/TiptapComposer.d.ts +4 -0
  152. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  153. package/dist/client/composer/TiptapComposer.js +47 -29
  154. package/dist/client/composer/TiptapComposer.js.map +1 -1
  155. package/dist/client/composer/VoiceButton.d.ts.map +1 -1
  156. package/dist/client/composer/VoiceButton.js +2 -2
  157. package/dist/client/composer/VoiceButton.js.map +1 -1
  158. package/dist/client/composer/index.d.ts +2 -2
  159. package/dist/client/composer/index.d.ts.map +1 -1
  160. package/dist/client/composer/index.js +1 -1
  161. package/dist/client/composer/index.js.map +1 -1
  162. package/dist/client/composer/useRealtimeVoiceMode.d.ts +100 -3
  163. package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  164. package/dist/client/composer/useRealtimeVoiceMode.js +952 -51
  165. package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
  166. package/dist/client/index.d.ts +1 -1
  167. package/dist/client/index.d.ts.map +1 -1
  168. package/dist/client/index.js +1 -1
  169. package/dist/client/index.js.map +1 -1
  170. package/dist/client/progress/RunsTray.d.ts.map +1 -1
  171. package/dist/client/progress/RunsTray.js +10 -1
  172. package/dist/client/progress/RunsTray.js.map +1 -1
  173. package/dist/client/settings/SecretsSection.d.ts +2 -3
  174. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  175. package/dist/client/settings/SecretsSection.js +46 -22
  176. package/dist/client/settings/SecretsSection.js.map +1 -1
  177. package/dist/client/settings/SettingsPanel.d.ts +9 -0
  178. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  179. package/dist/client/settings/SettingsPanel.js +46 -12
  180. package/dist/client/settings/SettingsPanel.js.map +1 -1
  181. package/dist/client/use-action.d.ts +2 -0
  182. package/dist/client/use-action.d.ts.map +1 -1
  183. package/dist/client/use-action.js +10 -0
  184. package/dist/client/use-action.js.map +1 -1
  185. package/dist/client/use-agent-chat-home-handoff.d.ts +2 -0
  186. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  187. package/dist/client/use-agent-chat-home-handoff.js +7 -1
  188. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  189. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  190. package/dist/client/use-agent-engine-configured.js +17 -3
  191. package/dist/client/use-agent-engine-configured.js.map +1 -1
  192. package/dist/client/use-db-sync.d.ts +8 -2
  193. package/dist/client/use-db-sync.d.ts.map +1 -1
  194. package/dist/client/use-db-sync.js +57 -32
  195. package/dist/client/use-db-sync.js.map +1 -1
  196. package/dist/client/use-pausing-interval.d.ts.map +1 -1
  197. package/dist/client/use-pausing-interval.js +16 -4
  198. package/dist/client/use-pausing-interval.js.map +1 -1
  199. package/dist/integrations/scope-store.js +1 -1
  200. package/dist/integrations/scope-store.js.map +1 -1
  201. package/dist/integrations/usage-budget-store.js +1 -1
  202. package/dist/integrations/usage-budget-store.js.map +1 -1
  203. package/dist/localization/default-messages.d.ts +41 -1
  204. package/dist/localization/default-messages.d.ts.map +1 -1
  205. package/dist/localization/default-messages.js +41 -1
  206. package/dist/localization/default-messages.js.map +1 -1
  207. package/dist/mcp-client/workspace-servers.js +4 -4
  208. package/dist/mcp-client/workspace-servers.js.map +1 -1
  209. package/dist/observability/routes.d.ts +5 -5
  210. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  211. package/dist/secrets/crypto.d.ts +7 -6
  212. package/dist/secrets/crypto.d.ts.map +1 -1
  213. package/dist/secrets/crypto.js +30 -11
  214. package/dist/secrets/crypto.js.map +1 -1
  215. package/dist/secrets/routes.d.ts +9 -9
  216. package/dist/server/action-change.d.ts +1 -0
  217. package/dist/server/action-change.d.ts.map +1 -1
  218. package/dist/server/action-change.js +4 -1
  219. package/dist/server/action-change.js.map +1 -1
  220. package/dist/server/action-routes.d.ts.map +1 -1
  221. package/dist/server/action-routes.js +5 -0
  222. package/dist/server/action-routes.js.map +1 -1
  223. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  224. package/dist/server/poll.d.ts.map +1 -1
  225. package/dist/server/poll.js +1 -0
  226. package/dist/server/poll.js.map +1 -1
  227. package/dist/server/realtime-voice.d.ts +7 -0
  228. package/dist/server/realtime-voice.d.ts.map +1 -1
  229. package/dist/server/realtime-voice.js +222 -12
  230. package/dist/server/realtime-voice.js.map +1 -1
  231. package/dist/styles/agent-native.css +2 -2
  232. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  233. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  234. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  235. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
  236. package/dist/vite/client.d.ts.map +1 -1
  237. package/dist/vite/client.js +6 -1
  238. package/dist/vite/client.js.map +1 -1
  239. package/package.json +2 -2
  240. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
  241. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
  242. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
  243. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
@@ -16,6 +16,7 @@ import {
16
16
  callAction,
17
17
  sendToAgentChat,
18
18
  type AgentChatMessage,
19
+ useChangeVersions,
19
20
  } from "@agent-native/core/client";
20
21
  import { fullVideoAiModelSelection } from "@shared/clips-ai-prefs";
21
22
  import { useEffect, useRef } from "react";
@@ -23,7 +24,6 @@ import { useEffect, useRef } from "react";
23
24
  import { useRecordings, type RecordingSummary } from "./use-library";
24
25
 
25
26
  const DEFAULT_TITLE = "Untitled recording";
26
- const POLL_INTERVAL_MS = 3000;
27
27
  const TWO_MINUTES_MS = 2 * 60 * 1000;
28
28
 
29
29
  /** True when `title` is blank or equal to the server-seeded default. */
@@ -99,8 +99,9 @@ async function clearRequest(recordingId: string): Promise<void> {
99
99
  }
100
100
 
101
101
  /**
102
- * Mount this once in the app shell. It polls the recording list and fires
103
- * `sendToAgentChat` for every pending request queued by a clips action.
102
+ * Mount this once in the app shell. It watches the exact application-state
103
+ * keys used for queued Clips AI work and fires `sendToAgentChat` for every
104
+ * pending request queued by a clips action.
104
105
  * Idempotent — a given (recordingId, kind, requestedAt) is only dispatched
105
106
  * once per tab session.
106
107
  */
@@ -119,13 +120,26 @@ export function useAutoTitleBridge(): void {
119
120
  `${r.id}:${r.titleSource ?? ""}:${r.title}:${r.updatedAt}:${r.transcriptStatus ?? ""}:${r.transcriptHasText ? "1" : "0"}`,
120
121
  )
121
122
  .join("|");
123
+ const aiRequestVersion = useChangeVersions(
124
+ readyRecordings.map(
125
+ (recording) => `app-state:clips-ai-request-${recording.id}`,
126
+ ),
127
+ );
122
128
 
123
129
  useEffect(() => {
124
130
  if (readyRecordings.length === 0) return;
125
131
  let cancelled = false;
132
+ let fallbackTimer: ReturnType<typeof setTimeout> | null = null;
126
133
 
127
134
  async function tick() {
128
- if (cancelled || inflight.current) return;
135
+ if (cancelled) return;
136
+ if (inflight.current) {
137
+ // A new request-state version can arrive while the previous list read
138
+ // is in flight. Recheck after it settles so that event is not the last
139
+ // chance to dispatch the queued work.
140
+ fallbackTimer = setTimeout(() => void tick(), 50);
141
+ return;
142
+ }
129
143
  inflight.current = true;
130
144
  try {
131
145
  const requestsById = await listRequests();
@@ -192,14 +206,66 @@ export function useAutoTitleBridge(): void {
192
206
  }
193
207
  }
194
208
 
195
- tick();
196
- const handle = setInterval(tick, POLL_INTERVAL_MS);
209
+ function scheduleNextFallback() {
210
+ if (cancelled) return;
211
+ const delay = nextAutoTitleFallbackDelay(
212
+ readyRecordings,
213
+ dispatched.current,
214
+ );
215
+ if (delay === null) return;
216
+ // Keep the timeout non-zero so a failed request cannot spin a tight loop.
217
+ fallbackTimer = setTimeout(
218
+ () => {
219
+ fallbackTimer = null;
220
+ void tick().finally(scheduleNextFallback);
221
+ },
222
+ Math.max(delay, 50),
223
+ );
224
+ }
225
+
226
+ // One initial read catches requests queued before this component mounted.
227
+ // Later reads are driven by the exact clips-ai-request application-state
228
+ // counters above. The only timer left is a one-shot wake-up when an old
229
+ // transcript-backed recording becomes eligible for the legacy fallback.
230
+ void tick().finally(scheduleNextFallback);
197
231
  return () => {
198
232
  cancelled = true;
199
- clearInterval(handle);
233
+ if (fallbackTimer) clearTimeout(fallbackTimer);
200
234
  };
235
+ // readyRecordingsKey is the stable snapshot consumed by tick; depending on
236
+ // the array itself would restart the effect on every query result object.
201
237
  // eslint-disable-next-line react-hooks/exhaustive-deps
202
- }, [readyRecordingsKey]);
238
+ }, [aiRequestVersion, readyRecordingsKey]);
239
+ }
240
+
241
+ export function nextAutoTitleFallbackDelay(
242
+ recordings: readonly RecordingSummary[],
243
+ dispatched: ReadonlySet<string>,
244
+ now = Date.now(),
245
+ ): number | null {
246
+ let nextDelay: number | null = null;
247
+
248
+ for (const recording of recordings) {
249
+ if (recording.status !== "ready") continue;
250
+ if (!isAutoTitleReplaceable(recording.title, recording.titleSource)) {
251
+ continue;
252
+ }
253
+ if (
254
+ recording.transcriptStatus !== "ready" ||
255
+ recording.transcriptHasText !== true ||
256
+ dispatched.has(`${recording.id}:fallback`)
257
+ ) {
258
+ continue;
259
+ }
260
+
261
+ const createdAt = new Date(recording.createdAt).getTime();
262
+ const delay = Number.isFinite(createdAt)
263
+ ? Math.max(0, TWO_MINUTES_MS - (now - createdAt))
264
+ : 0;
265
+ nextDelay = nextDelay === null ? delay : Math.min(nextDelay, delay);
266
+ }
267
+
268
+ return nextDelay;
203
269
  }
204
270
 
205
271
  function buildRequestContext(rec: RecordingSummary, request: AiRequest) {
@@ -44,24 +44,13 @@ export interface ListRecordingsArgs {
44
44
  offset?: number;
45
45
  }
46
46
 
47
- function isAwaitingAutoTitle(recording: RecordingSummary): boolean {
48
- const title = (recording.title ?? "").trim();
49
- const titleIsReplaceable =
50
- title === "" ||
51
- title === "Untitled recording" ||
52
- recording.titleSource === "default" ||
53
- recording.titleSource === "context";
54
- if (!titleIsReplaceable) return false;
55
-
56
- if (recording.transcriptStatus === "failed") return false;
57
- if (recording.transcriptStatus === "ready") {
58
- return recording.transcriptHasText === true;
59
- }
60
-
61
- return (
62
- recording.transcriptStatus === "pending" ||
63
- recording.transcriptStatus === "streaming"
64
- );
47
+ export function recordingsRefetchInterval(
48
+ recordings: readonly RecordingSummary[] | undefined,
49
+ ): number | false {
50
+ if (!recordings || recordings.length === 0) return false;
51
+ return recordings.some((recording) => isLiveRecordingUpload(recording))
52
+ ? 3000
53
+ : false;
65
54
  }
66
55
 
67
56
  export function useRecordings(args: ListRecordingsArgs = {}) {
@@ -74,17 +63,15 @@ export function useRecordings(args: ListRecordingsArgs = {}) {
74
63
  recordings: Array.isArray(data?.recordings) ? data.recordings : [],
75
64
  };
76
65
  },
77
- // Keep a short poll while uploads/processors are active so the library
78
- // card does not get stuck if the global refresh signal is missed.
79
- // Also poll for replaceable seed titles so the card upgrades promptly.
66
+ // Keep a short poll only while uploads/processors are active so the
67
+ // library card does not get stuck if the global refresh signal is
68
+ // missed. Generated titles arrive through the shared DB sync transport;
69
+ // polling completed recordings forever is both redundant and expensive.
80
70
  refetchInterval: (q) => {
81
71
  const recs = (q.state.data as any)?.recordings as
82
72
  | RecordingSummary[]
83
73
  | undefined;
84
- if (!recs || recs.length === 0) return false;
85
- const pendingUpload = recs.some((rec) => isLiveRecordingUpload(rec));
86
- const pendingTitle = recs.some(isAwaitingAutoTitle);
87
- return pendingUpload || pendingTitle ? 3000 : false;
74
+ return recordingsRefetchInterval(recs);
88
75
  },
89
76
  },
90
77
  );
@@ -734,7 +734,7 @@ export default function SettingsIndexRoute() {
734
734
  builderStatus.loading ||
735
735
  !builderConnect.hasFetchedStatus;
736
736
  const s3Configured = activeProviderId === "s3";
737
- const s3Collapsed = builderConnected && !s3Expanded;
737
+ const s3Collapsed = !s3Expanded;
738
738
  const configuredApiKeyCount = AI_PROVIDER_FIELDS.filter(
739
739
  (field) => apiKeyStatus[field.key],
740
740
  ).length;
@@ -908,7 +908,7 @@ export default function SettingsIndexRoute() {
908
908
  </div>
909
909
 
910
910
  <Collapsible
911
- open={builderConnected ? !s3Collapsed : true}
911
+ open={!s3Collapsed}
912
912
  onOpenChange={(open) => setS3Expanded(open)}
913
913
  >
914
914
  <div className="rounded-md border border-border">
@@ -939,26 +939,24 @@ export default function SettingsIndexRoute() {
939
939
  : t("settings.s3OwnBucketDescription")}
940
940
  </p>
941
941
  </div>
942
- {builderConnected ? (
943
- <CollapsibleTrigger asChild>
944
- <Button
945
- type="button"
946
- variant="ghost"
947
- size="sm"
948
- className="shrink-0"
949
- >
950
- {s3Collapsed
951
- ? t("settings.configureS3")
952
- : t("settings.hideS3")}
953
- <IconChevronDown
954
- className={cn(
955
- "h-4 w-4 transition-transform",
956
- !s3Collapsed && "rotate-180",
957
- )}
958
- />
959
- </Button>
960
- </CollapsibleTrigger>
961
- ) : null}
942
+ <CollapsibleTrigger asChild>
943
+ <Button
944
+ type="button"
945
+ variant="ghost"
946
+ size="sm"
947
+ className="shrink-0"
948
+ >
949
+ {s3Collapsed
950
+ ? t("settings.configureS3")
951
+ : t("settings.hideS3")}
952
+ <IconChevronDown
953
+ className={cn(
954
+ "h-4 w-4 transition-transform",
955
+ !s3Collapsed && "rotate-180",
956
+ )}
957
+ />
958
+ </Button>
959
+ </CollapsibleTrigger>
962
960
  </div>
963
961
 
964
962
  <CollapsibleContent>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-11
4
+ ---
5
+
6
+ Generated titles no longer cause continuous background refreshes after a recording is ready.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-11
4
+ ---
5
+
6
+ S3 storage setup stays collapsed until you choose it as the secondary storage option.
@@ -30,7 +30,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
30
30
  useDbSync({ queryClient });
31
31
  ```
32
32
 
33
- For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries for ordinary domain events — that caused a request storm in production. The exception is `source: "action"`: a successful mutating action is the framework-wide "agent changed app data" signal, so `useDbSync` also refreshes active React Query observers as a compatibility safety net for custom apps that have not yet moved every read to `useActionQuery` or source-versioned query keys.
33
+ For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries — that caused request storms in production. A successful mutating action refreshes active `useActionQuery` observers under the `["action"]` prefix. Browser actions carry a tab id so their originating tab ignores the sync echo while other tabs refresh. Legacy apps can opt into broader compatibility with `actionInvalidatePredicate`, but first-party apps should use action-backed or source-versioned query keys. Idle fallback polling runs once per minute; active agent work temporarily uses the faster cadence.
34
34
 
35
35
  3. **Templates fold per-source counters into their query keys.** This is the pattern that makes "agent writes show up without a manual refresh" reliable:
36
36
 
@@ -117,7 +117,7 @@ useQuery({
117
117
  | UI not updating after agent writes | Is `useDbSync` called with the correct `queryClient`? Does the affected query have an active observer? |
118
118
  | Poll endpoint not responding | Is `/_agent-native/poll` accessible? Is the server running? |
119
119
  | SSE not connecting | Is `/_agent-native/events` accessible and authenticated? Polling should still keep the UI fresh as fallback. |
120
- | High CPU / event storms | The agent is writing rapidly. Add `staleTime` to queries to debounce refetches. |
120
+ | High CPU / event storms | Use targeted source keys, settle bursty list counters, and avoid broad action invalidation. |
121
121
 
122
122
  ## Jitter Prevention
123
123
 
@@ -65,7 +65,6 @@ export function useComments(documentId: string | null) {
65
65
  }
66
66
  return Array.from(threadMap.values());
67
67
  },
68
- refetchInterval: 5000,
69
68
  },
70
69
  );
71
70
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-11
4
+ ---
5
+
6
+ Comments now update from live collaboration events without continuous background requests
@@ -88,7 +88,7 @@ export function Header() {
88
88
  </div>
89
89
  <div className="flex items-center gap-2 shrink-0">
90
90
  {actions}
91
- <RunsTray onOpenThread={openRunThread} />
91
+ <RunsTray pollMs={0} onOpenThread={openRunThread} />
92
92
  <AgentToggleButton />
93
93
  </div>
94
94
  </header>
@@ -30,7 +30,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
30
30
  useDbSync({ queryClient });
31
31
  ```
32
32
 
33
- For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries for ordinary domain events — that caused a request storm in production. The exception is `source: "action"`: a successful mutating action is the framework-wide "agent changed app data" signal, so `useDbSync` also refreshes active React Query observers as a compatibility safety net for custom apps that have not yet moved every read to `useActionQuery` or source-versioned query keys.
33
+ For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries — that caused request storms in production. A successful mutating action refreshes active `useActionQuery` observers under the `["action"]` prefix. Browser actions carry a tab id so their originating tab ignores the sync echo while other tabs refresh. Legacy apps can opt into broader compatibility with `actionInvalidatePredicate`, but first-party apps should use action-backed or source-versioned query keys. Idle fallback polling runs once per minute; active agent work temporarily uses the faster cadence.
34
34
 
35
35
  3. **Templates fold per-source counters into their query keys.** This is the pattern that makes "agent writes show up without a manual refresh" reliable:
36
36
 
@@ -117,7 +117,7 @@ useQuery({
117
117
  | UI not updating after agent writes | Is `useDbSync` called with the correct `queryClient`? Does the affected query have an active observer? |
118
118
  | Poll endpoint not responding | Is `/_agent-native/poll` accessible? Is the server running? |
119
119
  | SSE not connecting | Is `/_agent-native/events` accessible and authenticated? Polling should still keep the UI fresh as fallback. |
120
- | High CPU / event storms | The agent is writing rapidly. Add `staleTime` to queries to debounce refetches. |
120
+ | High CPU / event storms | Use targeted source keys, settle bursty list counters, and avoid broad action invalidation. |
121
121
 
122
122
  ## Jitter Prevention
123
123
 
@@ -80,7 +80,7 @@ export function AppLayout({ children }: { children: React.ReactNode }) {
80
80
  }).catch(() => {});
81
81
  }, [location.pathname, isAnalytics, isSettings]);
82
82
 
83
- // Poll for navigate commands from the agent
83
+ // useDbSync invalidates this key when the agent writes a navigate command.
84
84
  const { data: navCommand } = useQuery({
85
85
  queryKey: ["navigate-command"],
86
86
  queryFn: async () => {
@@ -94,7 +94,6 @@ export function AppLayout({ children }: { children: React.ReactNode }) {
94
94
  return null;
95
95
  }
96
96
  },
97
- refetchInterval: 2000,
98
97
  });
99
98
 
100
99
  useEffect(() => {
@@ -169,9 +169,6 @@ export default function Root() {
169
169
  createAgentNativeQueryClient({
170
170
  defaultOptions: {
171
171
  queries: {
172
- // Macros UI refetches on focus to pick up meals logged in other
173
- // browser tabs or mobile, which don't always arrive via DB sync.
174
- refetchOnWindowFocus: true,
175
172
  // Flat retry: macros data errors are usually transient network
176
173
  // issues, not auth failures, so a flat count is sufficient.
177
174
  retry: 1,
@@ -30,7 +30,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
30
30
  useDbSync({ queryClient });
31
31
  ```
32
32
 
33
- For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries for ordinary domain events — that caused a request storm in production. The exception is `source: "action"`: a successful mutating action is the framework-wide "agent changed app data" signal, so `useDbSync` also refreshes active React Query observers as a compatibility safety net for custom apps that have not yet moved every read to `useActionQuery` or source-versioned query keys.
33
+ For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries — that caused request storms in production. A successful mutating action refreshes active `useActionQuery` observers under the `["action"]` prefix. Browser actions carry a tab id so their originating tab ignores the sync echo while other tabs refresh. Legacy apps can opt into broader compatibility with `actionInvalidatePredicate`, but first-party apps should use action-backed or source-versioned query keys. Idle fallback polling runs once per minute; active agent work temporarily uses the faster cadence.
34
34
 
35
35
  3. **Templates fold per-source counters into their query keys.** This is the pattern that makes "agent writes show up without a manual refresh" reliable:
36
36
 
@@ -117,7 +117,7 @@ useQuery({
117
117
  | UI not updating after agent writes | Is `useDbSync` called with the correct `queryClient`? Does the affected query have an active observer? |
118
118
  | Poll endpoint not responding | Is `/_agent-native/poll` accessible? Is the server running? |
119
119
  | SSE not connecting | Is `/_agent-native/events` accessible and authenticated? Polling should still keep the UI fresh as fallback. |
120
- | High CPU / event storms | The agent is writing rapidly. Add `staleTime` to queries to debounce refetches. |
120
+ | High CPU / event storms | Use targeted source keys, settle bursty list counters, and avoid broad action invalidation. |
121
121
 
122
122
  ## Jitter Prevention
123
123
 
@@ -139,6 +139,7 @@ export function useComposeState() {
139
139
  return merged;
140
140
  },
141
141
  staleTime: 1_000,
142
+ // request-storm-allow: one focus refresh reconciles bounded compose drafts across tabs.
142
143
  refetchOnWindowFocus: true,
143
144
  });
144
145
 
@@ -419,18 +419,7 @@ function DbSyncSetup() {
419
419
  const MAIL_TOASTER = <Toaster richColors position="bottom-left" />;
420
420
 
421
421
  export default function Root() {
422
- const [queryClient] = useState(() =>
423
- createAgentNativeQueryClient({
424
- defaultOptions: {
425
- queries: {
426
- // Mail's VisibilityRefresh handles the focus-based refresh with a
427
- // 60 s throttle, so we also want React Query's focus refetch to
428
- // fire for other query keys (labels, settings, etc.).
429
- refetchOnWindowFocus: true,
430
- },
431
- },
432
- }),
433
- );
422
+ const [queryClient] = useState(() => createAgentNativeQueryClient());
434
423
  return (
435
424
  <AppToolkitProvider>
436
425
  <AppProviders
@@ -30,7 +30,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
30
30
  useDbSync({ queryClient });
31
31
  ```
32
32
 
33
- For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries for ordinary domain events — that caused a request storm in production. The exception is `source: "action"`: a successful mutating action is the framework-wide "agent changed app data" signal, so `useDbSync` also refreshes active React Query observers as a compatibility safety net for custom apps that have not yet moved every read to `useActionQuery` or source-versioned query keys.
33
+ For each non-own event, `useDbSync` bumps a per-source counter (e.g. `dashboards`, `analyses`, `settings`, `action`) and invalidates a small fixed list of framework-internal prefixes (`["action"]`, `["app-state"]`, `["__set_url__"]`, etc.). It does **not** blanket-invalidate templates' own data queries — that caused request storms in production. A successful mutating action refreshes active `useActionQuery` observers under the `["action"]` prefix. Browser actions carry a tab id so their originating tab ignores the sync echo while other tabs refresh. Legacy apps can opt into broader compatibility with `actionInvalidatePredicate`, but first-party apps should use action-backed or source-versioned query keys. Idle fallback polling runs once per minute; active agent work temporarily uses the faster cadence.
34
34
 
35
35
  3. **Templates fold per-source counters into their query keys.** This is the pattern that makes "agent writes show up without a manual refresh" reliable:
36
36
 
@@ -117,7 +117,7 @@ useQuery({
117
117
  | UI not updating after agent writes | Is `useDbSync` called with the correct `queryClient`? Does the affected query have an active observer? |
118
118
  | Poll endpoint not responding | Is `/_agent-native/poll` accessible? Is the server running? |
119
119
  | SSE not connecting | Is `/_agent-native/events` accessible and authenticated? Polling should still keep the UI fresh as fallback. |
120
- | High CPU / event storms | The agent is writing rapidly. Add `staleTime` to queries to debounce refetches. |
120
+ | High CPU / event storms | Use targeted source keys, settle bursty list counters, and avoid broad action invalidation. |
121
121
 
122
122
  ## Jitter Prevention
123
123
 
@@ -55,7 +55,10 @@ export function useNavigationState() {
55
55
  }).catch(() => {});
56
56
  }, [location.pathname, location.search]);
57
57
 
58
- // Listen for navigate commands from agent
58
+ // Listen for one-shot navigate commands from the agent. useDbSync
59
+ // invalidates this exact key when the shared SSE/poll transport receives an
60
+ // app-state:navigate event, so this stays idle between real commands instead
61
+ // of charging the host for a request every two seconds.
59
62
  const { data: navCommand } = useQuery({
60
63
  queryKey: ["navigate-command"],
61
64
  queryFn: async () => {
@@ -70,7 +73,7 @@ export function useNavigationState() {
70
73
  }
71
74
  return null;
72
75
  },
73
- refetchInterval: 2_000,
76
+ retry: false,
74
77
  structuralSharing: false,
75
78
  });
76
79
 
@@ -17,7 +17,6 @@ import type {
17
17
  PlanVersionListResponse,
18
18
  } from "@shared/types";
19
19
  import { useQueryClient, type UseQueryOptions } from "@tanstack/react-query";
20
- import type { RefObject } from "react";
21
20
  import { toast } from "sonner";
22
21
 
23
22
  import type { PlanMdxFolder } from "@/lib/desktop-plan-files";
@@ -337,23 +336,17 @@ export function localPlanBundleQueryKey(slug: string, path?: string | null) {
337
336
  ] as const;
338
337
  }
339
338
 
340
- export function usePlan(
341
- id?: string,
342
- pausePollRef?: RefObject<boolean> | { current: boolean },
343
- ) {
339
+ export function usePlan(id?: string) {
344
340
  return useActionQuery<PlanBundle & { html?: string }>(
345
341
  "get-visual-plan",
346
342
  planBundleQueryParams(id ?? ""),
347
343
  {
348
344
  enabled: !!id,
349
- // Pause the 3-second poll while a comment mutation is in-flight so
350
- // an optimistic comment inserted into the cache isn't evicted before
351
- // the server write commits (Issue 4a).
352
- refetchInterval: (query: { state: { status: string } }) => {
353
- if (query.state.status === "error") return false;
354
- if (pausePollRef?.current) return false;
355
- return 3_000;
356
- },
345
+ // Mutating actions and collaboration writes already arrive through the
346
+ // shared useDbSync SSE/poll transport, which invalidates active action
347
+ // reads. Keep the last bundle visible during that targeted refresh and
348
+ // avoid repeatedly downloading the full plan while nothing changed.
349
+ placeholderData: (previous) => previous,
357
350
  },
358
351
  );
359
352
  }
@@ -2018,7 +2018,6 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
2018
2018
  // Ref that signals the 3-second poll to pause while a comment mutation is
2019
2019
  // in-flight. Prevents poll-driven cache replacement from evicting optimistic
2020
2020
  // comments before the server write commits (Issue 4a).
2021
- const commentMutationPendingRef = useRef(false);
2022
2021
  const { session, isLoading: sessionLoading } = useSession();
2023
2022
  const localPlanMode = Boolean(localPlanSlug);
2024
2023
  const routeSearchParams = useMemo(
@@ -2217,10 +2216,7 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
2217
2216
  const immersiveReader = Boolean(
2218
2217
  selectedId && (planFullscreen || prototypeOnly),
2219
2218
  );
2220
- const planQuery = usePlan(
2221
- localPlanMode ? undefined : selectedId,
2222
- commentMutationPendingRef,
2223
- );
2219
+ const planQuery = usePlan(localPlanMode ? undefined : selectedId);
2224
2220
  const bundle = localPlanMode ? localPlanData : planQuery.data;
2225
2221
  const localPlanBundle =
2226
2222
  localPlanMode && bundle && "localOnly" in bundle
@@ -4550,8 +4546,7 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4550
4546
  };
4551
4547
  clearPendingDocumentRestore();
4552
4548
  pendingDocumentRestoreRef.current = documentStateRef.current;
4553
- commentMutationPendingRef.current = true;
4554
- // Await the cancel so an in-flight 3s poll can't resolve *after* our
4549
+ // Await the cancel so an in-flight sync refresh can't resolve *after* our
4555
4550
  // optimistic write and revert it (the "comment lagged / didn't stick"
4556
4551
  // symptom). cancelQueries reverts outstanding fetches before we patch.
4557
4552
  await queryClient.cancelQueries({ queryKey: selectedPlanQueryKey });
@@ -4588,9 +4583,6 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4588
4583
  setInlineCommentPosition(
4589
4584
  getPositionFromAnchor(anchor) ?? capturedPosition,
4590
4585
  );
4591
- })
4592
- .finally(() => {
4593
- commentMutationPendingRef.current = false;
4594
4586
  });
4595
4587
  };
4596
4588
 
@@ -4606,7 +4598,6 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4606
4598
  annotation.anchor.resolutionTarget,
4607
4599
  ),
4608
4600
  };
4609
- commentMutationPendingRef.current = true;
4610
4601
  void writeComments(
4611
4602
  [
4612
4603
  {
@@ -4629,9 +4620,6 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4629
4620
  })
4630
4621
  .catch(() => {
4631
4622
  // The mutation hook surfaces the failure toast; just clear pending.
4632
- })
4633
- .finally(() => {
4634
- commentMutationPendingRef.current = false;
4635
4623
  });
4636
4624
  };
4637
4625
 
@@ -4669,8 +4657,7 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4669
4657
  createdAt: now,
4670
4658
  updatedAt: now,
4671
4659
  };
4672
- commentMutationPendingRef.current = true;
4673
- // Await the cancel so an in-flight 3s poll can't resolve *after* our
4660
+ // Await the cancel so an in-flight sync refresh can't resolve *after* our
4674
4661
  // optimistic write and revert it (the "comment lagged / didn't stick"
4675
4662
  // symptom). cancelQueries reverts outstanding fetches before we patch.
4676
4663
  await queryClient.cancelQueries({ queryKey: selectedPlanQueryKey });
@@ -4709,8 +4696,6 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4709
4696
  current ? removePlanCommentFromBundle(current, replyId) : current,
4710
4697
  );
4711
4698
  throw new Error("Could not send reply. Try again.");
4712
- } finally {
4713
- commentMutationPendingRef.current = false;
4714
4699
  }
4715
4700
  };
4716
4701
 
@@ -4729,8 +4714,7 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4729
4714
  // popover update without waiting for the server round-trip (Issue 3).
4730
4715
  const prevBundle =
4731
4716
  queryClient.getQueryData<PlanBundleWithHtml>(selectedPlanQueryKey);
4732
- commentMutationPendingRef.current = true;
4733
- // Await the cancel so an in-flight 3s poll can't resolve *after* our
4717
+ // Await the cancel so an in-flight sync refresh can't resolve *after* our
4734
4718
  // optimistic write and revert it (the "comment lagged / didn't stick"
4735
4719
  // symptom). cancelQueries reverts outstanding fetches before we patch.
4736
4720
  await queryClient.cancelQueries({ queryKey: selectedPlanQueryKey });
@@ -4777,9 +4761,6 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4777
4761
  if (prevBundle !== undefined) {
4778
4762
  queryClient.setQueryData(selectedPlanQueryKey, prevBundle);
4779
4763
  }
4780
- })
4781
- .finally(() => {
4782
- commentMutationPendingRef.current = false;
4783
4764
  });
4784
4765
  };
4785
4766
 
@@ -4802,8 +4783,7 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4802
4783
  const prevBundle =
4803
4784
  queryClient.getQueryData<PlanBundleWithHtml>(selectedPlanQueryKey);
4804
4785
  const commentId = request.commentId;
4805
- commentMutationPendingRef.current = true;
4806
- // Await the cancel so an in-flight 3s poll can't resolve *after* our
4786
+ // Await the cancel so an in-flight sync refresh can't resolve *after* our
4807
4787
  // optimistic write and revert it (the "comment lagged / didn't stick"
4808
4788
  // symptom). cancelQueries reverts outstanding fetches before we patch.
4809
4789
  await queryClient.cancelQueries({ queryKey: selectedPlanQueryKey });
@@ -4826,8 +4806,6 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
4826
4806
  queryClient.setQueryData(selectedPlanQueryKey, prevBundle);
4827
4807
  }
4828
4808
  setDeleteCommentRequest(request);
4829
- } finally {
4830
- commentMutationPendingRef.current = false;
4831
4809
  }
4832
4810
  };
4833
4811
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-11
4
+ ---
5
+
6
+ Plan pages stay current while making far fewer background requests when nothing has changed.
@@ -336,7 +336,10 @@ function ThreadCard({
336
336
  <TooltipTrigger asChild>
337
337
  <button
338
338
  onClick={() =>
339
- resolveComment.mutate({ id: rootComment.id })
339
+ resolveComment.mutate({
340
+ id: rootComment.id,
341
+ resolved: true,
342
+ })
340
343
  }
341
344
  className="p-0.5 rounded text-muted-foreground hover:text-green-400"
342
345
  >
@@ -1023,7 +1023,7 @@ graph TD
1023
1023
  </DropdownMenuContent>
1024
1024
  </DropdownMenu>
1025
1025
 
1026
- <RunsTray />
1026
+ <RunsTray pollMs={0} />
1027
1027
  <AgentToggleButton />
1028
1028
  </div>
1029
1029
  );