@agent-native/core 0.163.3 → 0.164.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 (202) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
  3. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
  4. package/corpus/templates/analytics/netlify.toml +2 -0
  5. package/corpus/templates/assets/netlify.toml +2 -0
  6. package/corpus/templates/brain/netlify.toml +2 -0
  7. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
  8. package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
  9. package/corpus/templates/calendar/netlify.toml +2 -0
  10. package/corpus/templates/chat/netlify.toml +2 -0
  11. package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
  12. package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
  13. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
  14. package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
  15. package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
  16. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
  17. package/corpus/templates/clips/app/routes/_index.tsx +10 -2
  18. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
  19. package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
  20. package/corpus/templates/clips/netlify.toml +2 -0
  21. package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
  22. package/corpus/templates/content/netlify.toml +2 -0
  23. package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
  24. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
  25. package/corpus/templates/design/AGENTS.md +3 -1
  26. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
  27. package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
  28. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
  29. package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
  30. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
  31. package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
  32. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
  33. package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
  34. package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
  35. package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
  36. package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
  37. package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
  38. package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
  39. package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
  40. package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
  41. package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
  42. package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
  43. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
  44. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
  45. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
  46. package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
  47. package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
  48. package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
  49. package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
  50. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
  51. package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
  52. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
  53. package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
  54. package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
  55. package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
  56. package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
  57. package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
  58. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
  59. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
  60. package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
  61. package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
  62. package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
  63. package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
  64. package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
  65. package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
  66. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
  67. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
  68. package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
  69. package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
  70. package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
  71. package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
  72. package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
  73. package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
  74. package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
  75. package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
  76. package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
  77. package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
  78. package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
  79. package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
  80. package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
  81. package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
  82. package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
  83. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
  84. package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
  85. package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
  86. package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
  87. package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
  88. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
  89. package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
  90. package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
  91. package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
  92. package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
  93. package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
  94. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
  95. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
  96. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
  97. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
  98. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
  99. package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
  100. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
  101. package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
  102. package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
  103. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
  104. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
  105. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
  106. package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
  107. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
  108. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
  109. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
  110. package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
  111. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
  112. package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
  113. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
  114. package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
  115. package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
  116. package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
  117. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
  118. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
  119. package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
  120. package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
  121. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
  122. package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
  123. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
  124. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
  125. package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
  126. package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
  127. package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
  128. package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
  129. package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
  130. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
  131. package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
  132. package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
  133. package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
  134. package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
  135. package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
  136. package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
  137. package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
  138. package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
  139. package/corpus/templates/design/netlify.toml +2 -0
  140. package/corpus/templates/design/shared/board-file.ts +2 -1
  141. package/corpus/templates/dispatch/netlify.toml +2 -0
  142. package/corpus/templates/forms/netlify.toml +2 -0
  143. package/corpus/templates/mail/netlify.toml +2 -0
  144. package/corpus/templates/plan/netlify.toml +2 -0
  145. package/corpus/templates/slides/netlify.toml +2 -0
  146. package/dist/agent/engine/builder-engine.d.ts +6 -4
  147. package/dist/agent/engine/builder-engine.js +91 -26
  148. package/dist/agent/engine/registry.js +16 -0
  149. package/dist/cli/code-agent-agent-tools.d.ts +2 -0
  150. package/dist/cli/code-agent-agent-tools.js +188 -0
  151. package/dist/cli/code-agent-collaboration.d.ts +34 -0
  152. package/dist/cli/code-agent-collaboration.js +104 -0
  153. package/dist/cli/code-agent-executor.js +6 -0
  154. package/dist/cli/code-agent-schedules.d.ts +54 -0
  155. package/dist/cli/code-agent-schedules.js +198 -0
  156. package/dist/client/AgentPanel.js +1 -1
  157. package/dist/client/ConnectBuilderCard.js +5 -2
  158. package/dist/client/EnvironmentBadge.js +6 -5
  159. package/dist/client/chat/run-recovery.js +3 -2
  160. package/dist/client/chat/tool-call-display.js +2 -3
  161. package/dist/client/components/CodeRequiredDialog.js +4 -2
  162. package/dist/client/i18n.js +5 -4
  163. package/dist/client/settings/SettingsPanel.js +1 -1
  164. package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
  165. package/dist/client/settings/useBuilderStatus.d.ts +6 -1
  166. package/dist/client/settings/useBuilderStatus.js +22 -33
  167. package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
  168. package/dist/code-agents/index.d.ts +2 -0
  169. package/dist/code-agents/index.js +2 -0
  170. package/dist/collab/routes.d.ts +1 -1
  171. package/dist/localization/shared.d.ts +2 -0
  172. package/dist/localization/shared.js +5 -0
  173. package/dist/mcp-client/oauth-client.d.ts +1 -0
  174. package/dist/mcp-client/oauth-client.js +50 -1
  175. package/dist/observability/routes.d.ts +1 -1
  176. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  177. package/dist/resources/handlers.d.ts +1 -1
  178. package/dist/server/auth-marketing-locales.d.ts +7 -0
  179. package/dist/server/auth-marketing-locales.js +1322 -0
  180. package/dist/server/auth-marketing.d.ts +1 -0
  181. package/dist/server/auth-marketing.js +35 -6
  182. package/dist/server/auth.js +4 -17
  183. package/dist/server/builder-browser.d.ts +22 -0
  184. package/dist/server/builder-browser.js +77 -1
  185. package/dist/server/builder-oauth.d.ts +50 -0
  186. package/dist/server/builder-oauth.js +263 -0
  187. package/dist/server/core-routes-plugin.d.ts +11 -0
  188. package/dist/server/core-routes-plugin.js +291 -415
  189. package/dist/server/credential-provider.d.ts +1 -1
  190. package/dist/server/credential-provider.js +1 -1
  191. package/dist/server/csrf.js +2 -2
  192. package/dist/server/onboarding-html.js +52 -120
  193. package/dist/server/realtime-token.d.ts +1 -1
  194. package/dist/server/transcribe-voice.d.ts +1 -1
  195. package/dist/settings/store.d.ts +9 -0
  196. package/dist/settings/store.js +19 -0
  197. package/dist/templates/chat/netlify.toml +2 -0
  198. package/dist/vite/client.js +35 -37
  199. package/docs/content/deployment.mdx +2 -0
  200. package/docs/content/multi-app-workspace.mdx +1 -1
  201. package/package.json +2 -2
  202. package/src/templates/chat/netlify.toml +2 -0
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8328
34
+ - template files: 8442
@@ -132,6 +132,7 @@ export function useDashboardChatContext(
132
132
  : null;
133
133
  const hasSelectedPanel = selectedPanelId !== null;
134
134
  const publishedSelectionRef = useRef<SelectedDashboardObject | null>(null);
135
+ const suppressDashboardContextRef = useRef(false);
135
136
 
136
137
  const selectPanelForChat = useCallback(
137
138
  (
@@ -140,6 +141,7 @@ export function useDashboardChatContext(
140
141
  ) => {
141
142
  if (!id) return;
142
143
  if (options.openSidebar !== true && !isAgentSidebarOpen) return;
144
+ suppressDashboardContextRef.current = false;
143
145
  const displayDashboardTitle = title?.trim() || id;
144
146
  const displayPanelTitle = panel.panelTitle.trim() || panel.panelId;
145
147
  const contextLines = [
@@ -211,13 +213,38 @@ export function useDashboardChatContext(
211
213
  );
212
214
  }, []);
213
215
 
216
+ useEffect(() => {
217
+ const handleNewChat = () => {
218
+ removeAgentChatContextItem({
219
+ key: DASHBOARD_CONTEXT_KEY,
220
+ openSidebar: false,
221
+ });
222
+ removeAgentChatContextItem({
223
+ key: DASHBOARD_PANEL_CONTEXT_KEY,
224
+ openSidebar: false,
225
+ });
226
+ const publishedSelection = publishedSelectionRef.current;
227
+ publishedSelectionRef.current = null;
228
+ suppressDashboardContextRef.current = true;
229
+ if (publishedSelection) {
230
+ void clearSelectedDashboardObjectIfOwned(publishedSelection);
231
+ }
232
+ };
233
+
234
+ window.addEventListener("agent-chat:new-chat", handleNewChat);
235
+ return () =>
236
+ window.removeEventListener("agent-chat:new-chat", handleNewChat);
237
+ }, []);
238
+
214
239
  // Dashboard metadata lands in pieces — title first, then panel count, then
215
240
  // the access role. Publishing each piece costs a round-trip and a sync event
216
241
  // that invalidates every mounted query, so only the settled value is sent.
217
242
  useEffect(() => {
218
243
  if (!id) return;
244
+ suppressDashboardContextRef.current = false;
219
245
  const displayTitle = title?.trim() || id;
220
246
  const timer = setTimeout(() => {
247
+ if (suppressDashboardContextRef.current) return;
221
248
  setAgentChatContextItem({
222
249
  key: DASHBOARD_CONTEXT_KEY,
223
250
  title: `Dashboard: ${displayTitle}`,
@@ -237,6 +264,7 @@ export function useDashboardChatContext(
237
264
 
238
265
  useEffect(() => {
239
266
  if (!id || hasSelectedPanel) return;
267
+ if (suppressDashboardContextRef.current) return;
240
268
  const displayTitle = title?.trim() || id;
241
269
  const selection: SelectedDashboardObject = {
242
270
  type: "dashboard",
@@ -248,6 +276,7 @@ export function useDashboardChatContext(
248
276
  [SELECTED_OBJECT_SOURCE_FIELD]: TAB_ID,
249
277
  };
250
278
  const timer = setTimeout(() => {
279
+ if (suppressDashboardContextRef.current) return;
251
280
  setClientAppState(SELECTED_OBJECT_STATE_KEY, selection, {
252
281
  keepalive: true,
253
282
  requestSource: TAB_ID,
@@ -526,8 +526,7 @@ export function ReplayStorageHint({
526
526
  const storageStatus = useReplayStorageStatus();
527
527
  const builderStatus = useBuilderStatus();
528
528
  const builderConnect = useBuilderConnectFlow({
529
- popupUrl:
530
- builderStatus.status?.cliAuthUrl ?? builderStatus.status?.connectUrl,
529
+ popupUrl: builderStatus.status?.connectUrl,
531
530
  trackingSource: "analytics_sessions_storage_hint",
532
531
  trackingFlow: "replay_storage",
533
532
  onConnected: async () => {
@@ -20,6 +20,8 @@ AGENT_CHAT_DURABLE_BACKGROUND = "true"
20
20
 
21
21
  [context.production.environment]
22
22
  AGENT_NATIVE_HOSTED_HARNESS = "true"
23
+ AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
24
+ AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
23
25
  # Analytics owns its schema at release time (`migrate:production` above), so
24
26
  # production request functions skip migrations. Preview functions still run
25
27
  # request-path migrations because previews do not run the production release
@@ -13,6 +13,8 @@ AGENT_CHAT_DURABLE_BACKGROUND = "true"
13
13
 
14
14
  [context.production.environment]
15
15
  AGENT_NATIVE_HOSTED_HARNESS = "true"
16
+ AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
17
+ AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
16
18
 
17
19
  # Emit the Netlify background function used by Assets' durable agent-chat opt-in.
18
20
  # The normal server timeout still protects non-agent image/video HTTP handlers.
@@ -12,6 +12,8 @@ NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
13
  [context.production.environment]
14
14
  AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+ AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
16
+ AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
15
17
 
16
18
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
17
19
  # The framework does not impose a default agent-run timeout; if a template sets
@@ -981,7 +981,8 @@ export const DayView = memo(function DayView({
981
981
  {/* Positioned events overlay */}
982
982
  <div
983
983
  data-calendar-create-surface="true"
984
- className="absolute inset-0 ml-[40px] mr-2 sm:ml-[56px] sm:mr-4"
984
+ className="absolute left-0 right-0 top-0 ml-[40px] mr-2 sm:ml-[56px] sm:mr-4"
985
+ style={{ height: `${hours.length * HOUR_HEIGHT}px` }}
985
986
  onPointerDown={(e) => {
986
987
  // Only start a create-drag from empty space, not on an event or its resize handles
987
988
  if ((e.target as HTMLElement).closest("button")) return;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-19
4
+ ---
5
+
6
+ Calendar pages stay fast after periods of inactivity.
@@ -12,6 +12,8 @@ NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
13
  [context.production.environment]
14
14
  AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+ AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
16
+ AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
15
17
 
16
18
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
17
19
  # The framework does not impose a default agent-run timeout; if a template sets
@@ -12,3 +12,5 @@ NPM_CONFIG_PRODUCTION = "false"
12
12
 
13
13
  [context.production.environment]
14
14
  AGENT_NATIVE_HOSTED_HARNESS = "true"
15
+ AGENT_NATIVE_ENABLE_KEEP_WARM = "1"
16
+ AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND = "1"
@@ -34,7 +34,7 @@ import {
34
34
  import { PLAYBACK_SPEED_OPTIONS } from "@/lib/playback-speed";
35
35
  import { cn } from "@/lib/utils";
36
36
 
37
- import { ReactionsTray } from "./reactions-tray";
37
+ import { ReactionsTray, type ReactionHandler } from "./reactions-tray";
38
38
  import { Scrubber, msToClock } from "./scrubber";
39
39
 
40
40
  export const SPEED_OPTIONS = PLAYBACK_SPEED_OPTIONS;
@@ -73,7 +73,7 @@ export interface PlayerControlsProps {
73
73
  */
74
74
  showReactionsAndComment?: boolean;
75
75
  enableReactions?: boolean;
76
- onReact?: (emoji: string) => void;
76
+ onReact?: ReactionHandler;
77
77
  enableComments?: boolean;
78
78
  onAddComment?: () => void;
79
79
  }
@@ -306,7 +306,7 @@ export function PlayerControls(props: PlayerControlsProps) {
306
306
  >
307
307
  {enableReactions && onReact ? (
308
308
  <div className="pointer-events-auto">
309
- <ReactionsTray onReact={onReact} />
309
+ <ReactionsTray reactions={reactions} onReact={onReact} />
310
310
  </div>
311
311
  ) : null}
312
312
 
@@ -8,3 +8,15 @@ export const REACTION_EMOJIS = [
8
8
  "🤯",
9
9
  "👀",
10
10
  ] as const;
11
+
12
+ export const REACTION_NAMES: Record<(typeof REACTION_EMOJIS)[number], string> =
13
+ {
14
+ "👍": "thumbs up",
15
+ "❤️": "heart",
16
+ "🔥": "fire",
17
+ "👏": "clap",
18
+ "🎉": "celebrate",
19
+ "😂": "laugh",
20
+ "🤯": "mind blown",
21
+ "👀": "eyes",
22
+ };
@@ -1,4 +1,5 @@
1
- import { useState } from "react";
1
+ import { IconCheck } from "@tabler/icons-react";
2
+ import { useMemo, useState } from "react";
2
3
 
3
4
  import {
4
5
  Tooltip,
@@ -7,10 +8,20 @@ import {
7
8
  } from "@/components/ui/tooltip";
8
9
  import { cn } from "@/lib/utils";
9
10
 
10
- import { REACTION_EMOJIS } from "./reaction-emojis";
11
+ import { REACTION_EMOJIS, REACTION_NAMES } from "./reaction-emojis";
12
+
13
+ export type ReactionHandlerResult = boolean | void | Promise<boolean | void>;
14
+ export type ReactionHandler = (emoji: string) => ReactionHandlerResult;
15
+
16
+ export interface ReactionSummary {
17
+ id: string;
18
+ emoji: string;
19
+ videoTimestampMs: number;
20
+ }
11
21
 
12
22
  export interface ReactionsTrayProps {
13
- onReact: (emoji: string) => void;
23
+ onReact: ReactionHandler;
24
+ reactions?: Pick<ReactionSummary, "emoji">[];
14
25
  disabled?: boolean;
15
26
  }
16
27
 
@@ -22,18 +33,62 @@ interface Float {
22
33
 
23
34
  let idc = 0;
24
35
 
25
- export function ReactionsTray({ onReact, disabled }: ReactionsTrayProps) {
36
+ export function ReactionsTray({
37
+ onReact,
38
+ reactions,
39
+ disabled,
40
+ }: ReactionsTrayProps) {
26
41
  const [floats, setFloats] = useState<Float[]>([]);
42
+ const [savingEmoji, setSavingEmoji] = useState<string | null>(null);
43
+ const [savedEmoji, setSavedEmoji] = useState<string | null>(null);
44
+
45
+ const reactionCounts = useMemo(() => {
46
+ const counts: Partial<Record<(typeof REACTION_EMOJIS)[number], number>> =
47
+ {};
48
+ for (const reaction of reactions ?? []) {
49
+ if (REACTION_EMOJIS.includes(reaction.emoji as never)) {
50
+ const emoji = reaction.emoji as (typeof REACTION_EMOJIS)[number];
51
+ counts[emoji] = (counts[emoji] ?? 0) + 1;
52
+ }
53
+ }
54
+ return counts;
55
+ }, [reactions]);
27
56
 
28
57
  function fire(emoji: string) {
29
- if (disabled) return;
30
- onReact(emoji);
58
+ if (disabled || savingEmoji === emoji) return;
31
59
  const id = ++idc;
32
60
  const left = 10 + Math.random() * 80; // random horizontal variance within tray
33
61
  setFloats((f) => [...f, { id, emoji, left }]);
34
62
  setTimeout(() => {
35
63
  setFloats((f) => f.filter((x) => x.id !== id));
36
64
  }, 2500);
65
+
66
+ let result: ReactionHandlerResult;
67
+ try {
68
+ result = onReact(emoji);
69
+ } catch {
70
+ setSavedEmoji((current) => (current === emoji ? null : current));
71
+ return;
72
+ }
73
+
74
+ if (result && typeof result === "object" && "then" in result) {
75
+ setSavingEmoji(emoji);
76
+ void Promise.resolve(result)
77
+ .then((saved) => {
78
+ setSavedEmoji((current) =>
79
+ saved === false ? (current === emoji ? null : current) : emoji,
80
+ );
81
+ })
82
+ .catch(() => {
83
+ setSavedEmoji((current) => (current === emoji ? null : current));
84
+ })
85
+ .finally(() => {
86
+ setSavingEmoji((current) => (current === emoji ? null : current));
87
+ });
88
+ return;
89
+ }
90
+
91
+ if (result !== false) setSavedEmoji(emoji);
37
92
  }
38
93
 
39
94
  return (
@@ -42,17 +97,43 @@ export function ReactionsTray({ onReact, disabled }: ReactionsTrayProps) {
42
97
  <Tooltip key={emoji}>
43
98
  <TooltipTrigger asChild>
44
99
  <button
100
+ type="button"
45
101
  onClick={() => fire(emoji)}
46
- disabled={disabled}
102
+ disabled={disabled || savingEmoji === emoji}
103
+ aria-label={
104
+ reactionCounts[emoji]
105
+ ? `${REACTION_NAMES[emoji]} ${reactionCounts[emoji]}`
106
+ : `React with ${emoji}`
107
+ }
108
+ aria-pressed={savedEmoji === emoji}
109
+ data-reaction-emoji={emoji}
110
+ data-reaction-count={reactionCounts[emoji] ?? 0}
111
+ data-reaction-saved={savedEmoji === emoji}
47
112
  className={cn(
48
- "flex h-7 w-7 items-center justify-center rounded-full text-base sm:h-9 sm:w-9 sm:text-xl",
113
+ "relative flex h-8 w-8 items-center justify-center rounded-full text-base transition-[background-color,transform,box-shadow] hover:scale-110 hover:bg-accent sm:h-9 sm:w-9 sm:text-xl",
114
+ savedEmoji === emoji &&
115
+ "bg-accent shadow-[0_0_0_2px_hsl(var(--primary)/0.35)]",
116
+ savingEmoji === emoji && "animate-pulse",
49
117
  disabled && "opacity-50 cursor-not-allowed",
50
118
  )}
51
119
  >
52
120
  {emoji}
121
+ {savedEmoji === emoji ? (
122
+ <span className="absolute -right-0.5 -top-0.5 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm">
123
+ <IconCheck className="h-2.5 w-2.5" stroke={3} />
124
+ </span>
125
+ ) : reactionCounts[emoji] ? (
126
+ <span className="absolute -right-0.5 -top-0.5 min-w-3.5 rounded-full bg-muted px-0.5 text-[9px] font-semibold leading-3 text-muted-foreground shadow-sm">
127
+ {reactionCounts[emoji]}
128
+ </span>
129
+ ) : null}
53
130
  </button>
54
131
  </TooltipTrigger>
55
- <TooltipContent>{`React with ${emoji}`}</TooltipContent>
132
+ <TooltipContent>
133
+ {reactionCounts[emoji]
134
+ ? `${REACTION_NAMES[emoji]} ${reactionCounts[emoji]}`
135
+ : `React with ${emoji}`}
136
+ </TooltipContent>
56
137
  </Tooltip>
57
138
  ))}
58
139
 
@@ -28,6 +28,7 @@ export function Scrubber(props: ScrubberProps) {
28
28
  const [tooltip, setTooltip] = useState<
29
29
  | { kind: "comment"; content: string; ms: number }
30
30
  | { kind: "chapter"; title: string; ms: number }
31
+ | { kind: "reaction"; content: string; ms: number }
31
32
  | null
32
33
  >(null);
33
34
 
@@ -109,6 +110,17 @@ export function Scrubber(props: ScrubberProps) {
109
110
  return map;
110
111
  }, [comments]);
111
112
 
113
+ const reactionsByMs = useMemo(() => {
114
+ const map = new Map<number, { id: string; emoji: string }[]>();
115
+ recentReactions.forEach((reaction) => {
116
+ const key = Math.round(reaction.videoTimestampMs / 500) * 500;
117
+ const list = map.get(key) ?? [];
118
+ list.push({ id: reaction.id, emoji: reaction.emoji });
119
+ map.set(key, list);
120
+ });
121
+ return map;
122
+ }, [recentReactions]);
123
+
112
124
  return (
113
125
  <div
114
126
  className="relative h-10 flex items-center touch-none cursor-pointer"
@@ -142,7 +154,9 @@ export function Scrubber(props: ScrubberProps) {
142
154
  className="absolute -top-10 -translate-x-1/2 max-w-[240px] rounded bg-primary px-2 py-1 text-[11px] text-primary-foreground"
143
155
  style={{ left: (tooltip.ms / Math.max(1, durationMs)) * 100 + "%" }}
144
156
  >
145
- {tooltip.kind === "comment" ? tooltip.content : tooltip.title}
157
+ {tooltip.kind === "comment" || tooltip.kind === "reaction"
158
+ ? tooltip.content
159
+ : tooltip.title}
146
160
  </div>
147
161
  ) : null}
148
162
 
@@ -208,17 +222,36 @@ export function Scrubber(props: ScrubberProps) {
208
222
  </button>
209
223
  ))}
210
224
 
211
- {/* Reaction dots */}
212
- {recentReactions.map((r) => (
213
- <div
214
- key={r.id}
215
- className="absolute -bottom-4 -translate-x-1/2 text-[11px] pointer-events-none"
216
- style={{
217
- left: (r.videoTimestampMs / Math.max(1, durationMs)) * 100 + "%",
225
+ {/* Reaction markers */}
226
+ {Array.from(reactionsByMs.entries()).map(([ms, list]) => (
227
+ <button
228
+ key={`reaction-${ms}`}
229
+ type="button"
230
+ data-player-reaction-marker
231
+ onPointerDown={(event) => event.stopPropagation()}
232
+ onMouseEnter={() =>
233
+ setTooltip({
234
+ kind: "reaction",
235
+ content: `${list.map((reaction) => reaction.emoji).join(" ")} · ${list.length} reaction${list.length === 1 ? "" : "s"}`,
236
+ ms,
237
+ })
238
+ }
239
+ onMouseLeave={() => setTooltip(null)}
240
+ onClick={(event) => {
241
+ event.stopPropagation();
242
+ onSeek(ms);
218
243
  }}
244
+ className="absolute -bottom-5 flex h-5 min-w-5 -translate-x-1/2 items-center justify-center rounded-full bg-background/95 px-1 text-sm shadow-md ring-1 ring-primary/40 transition-transform hover:scale-125"
245
+ style={{ left: (ms / Math.max(1, durationMs)) * 100 + "%" }}
246
+ aria-label={`${list.length} reaction${list.length === 1 ? "" : "s"} at ${msToClock(ms)}`}
219
247
  >
220
- {r.emoji}
221
- </div>
248
+ {list[0].emoji}
249
+ {list.length > 1 ? (
250
+ <span className="absolute -right-1.5 -top-1.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-primary px-0.5 text-[8px] font-bold leading-none text-primary-foreground ring-1 ring-background">
251
+ {list.length}
252
+ </span>
253
+ ) : null}
254
+ </button>
222
255
  ))}
223
256
 
224
257
  {/* Thumb */}
@@ -57,6 +57,11 @@ import {
57
57
  type PlaybackComment,
58
58
  } from "./playback-comment-overlay";
59
59
  import { PlayerControls, SPEED_OPTIONS } from "./player-controls";
60
+ import type {
61
+ ReactionHandler,
62
+ ReactionHandlerResult,
63
+ ReactionSummary,
64
+ } from "./reactions-tray";
60
65
 
61
66
  function resolveLocalUrl(url: string | null | undefined): string | undefined {
62
67
  if (!url) return undefined;
@@ -251,7 +256,7 @@ export interface VideoPlayerProps {
251
256
  * visible only while `isFullscreen` is true.
252
257
  */
253
258
  enableReactions?: boolean;
254
- onReact?: (emoji: string) => void;
259
+ onReact?: ReactionHandler;
255
260
  enableComments?: boolean;
256
261
  /**
257
262
  * Opens the existing comment composer/panel. While fullscreen, the caller
@@ -374,6 +379,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
374
379
  const [currentMs, setCurrentMs] = useState(startMs ?? 0);
375
380
  currentMsRef.current = currentMs;
376
381
  isPlayingRef.current = isPlaying;
382
+ const [optimisticReactions, setOptimisticReactions] = useState<
383
+ ReactionSummary[]
384
+ >([]);
385
+ const optimisticReactionIdRef = useRef(0);
377
386
  const [loomStartMs, setLoomStartMs] = useState<number | null>(null);
378
387
  const [volume, setVolume] = useState(1);
379
388
  // Autoplaying players (e.g. the Slack unfurl embed, `?autoplay=1`) must
@@ -492,20 +501,101 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
492
501
  ? []
493
502
  : [{ startMs: editedMs, title: chapter.title }];
494
503
  }),
495
- reactions: (reactions ?? []).flatMap((reaction) => {
496
- const editedMs = mapMarker(reaction.videoTimestampMs);
497
- return editedMs === null
498
- ? []
499
- : [
500
- {
501
- id: reaction.id,
502
- emoji: reaction.emoji,
503
- videoTimestampMs: editedMs,
504
- },
505
- ];
506
- }),
504
+ reactions: [
505
+ ...(reactions ?? []).flatMap((reaction) => {
506
+ const editedMs = mapMarker(reaction.videoTimestampMs);
507
+ return editedMs === null
508
+ ? []
509
+ : [
510
+ {
511
+ id: reaction.id,
512
+ emoji: reaction.emoji,
513
+ videoTimestampMs: editedMs,
514
+ },
515
+ ];
516
+ }),
517
+ ...optimisticReactions.flatMap((reaction) => {
518
+ const editedMs = mapMarker(reaction.videoTimestampMs);
519
+ return editedMs === null
520
+ ? []
521
+ : [
522
+ {
523
+ id: reaction.id,
524
+ emoji: reaction.emoji,
525
+ videoTimestampMs: editedMs,
526
+ },
527
+ ];
528
+ }),
529
+ ],
507
530
  };
508
- }, [chapters, comments, currentMs, edits, reactions, resolvedDurationMs]);
531
+ }, [
532
+ chapters,
533
+ comments,
534
+ currentMs,
535
+ edits,
536
+ optimisticReactions,
537
+ reactions,
538
+ resolvedDurationMs,
539
+ ]);
540
+
541
+ useEffect(() => {
542
+ if (!optimisticReactions.length || !reactions?.length) return;
543
+ setOptimisticReactions((current) => {
544
+ const next = current.filter(
545
+ (optimistic) =>
546
+ !reactions.some(
547
+ (persisted) =>
548
+ persisted.emoji === optimistic.emoji &&
549
+ Math.abs(
550
+ persisted.videoTimestampMs - optimistic.videoTimestampMs,
551
+ ) < 1000,
552
+ ),
553
+ );
554
+ return next.length === current.length ? current : next;
555
+ });
556
+ }, [optimisticReactions.length, reactions]);
557
+
558
+ const handleReact = useCallback<ReactionHandler>(
559
+ (emoji) => {
560
+ const optimistic = {
561
+ id: `optimistic-reaction-${++optimisticReactionIdRef.current}`,
562
+ emoji,
563
+ videoTimestampMs: currentMsRef.current,
564
+ };
565
+ setOptimisticReactions((current) => [...current, optimistic]);
566
+
567
+ const removeOptimistic = () => {
568
+ setOptimisticReactions((current) =>
569
+ current.filter((reaction) => reaction.id !== optimistic.id),
570
+ );
571
+ };
572
+
573
+ let result: ReactionHandlerResult | undefined;
574
+ try {
575
+ result = onReact?.(emoji);
576
+ } catch {
577
+ removeOptimistic();
578
+ return false;
579
+ }
580
+
581
+ if (result && typeof result === "object" && "then" in result) {
582
+ return Promise.resolve(result).then(
583
+ (saved) => {
584
+ if (saved === false) removeOptimistic();
585
+ return saved !== false;
586
+ },
587
+ () => {
588
+ removeOptimistic();
589
+ return false;
590
+ },
591
+ );
592
+ }
593
+
594
+ if (result === false) removeOptimistic();
595
+ return result !== false;
596
+ },
597
+ [onReact],
598
+ );
509
599
  const activeVideoSourceIdentity = useMemo(
510
600
  () => videoSourceIdentity(activeVideoSrc),
511
601
  [activeVideoSrc],
@@ -2016,7 +2106,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
2016
2106
  menuPortalContainer={fullscreenMenuContainer}
2017
2107
  showReactionsAndComment={isFullscreen}
2018
2108
  enableReactions={enableReactions}
2019
- onReact={onReact}
2109
+ onReact={handleReact}
2020
2110
  enableComments={enableComments}
2021
2111
  onAddComment={onAddComment}
2022
2112
  />
@@ -106,8 +106,7 @@ export default function SettingsIndexRoute() {
106
106
  const builderStatus = useBuilderStatus();
107
107
  const connectRequestedRef = useRef(false);
108
108
  const builderConnect = useBuilderConnectFlow({
109
- popupUrl:
110
- builderStatus.status?.cliAuthUrl ?? builderStatus.status?.connectUrl,
109
+ popupUrl: builderStatus.status?.connectUrl,
111
110
  trackingSource: "clips_settings",
112
111
  trackingFlow: "clips_setup",
113
112
  onConnected: async () => {
@@ -4,6 +4,7 @@ import { redirect, type LoaderFunctionArgs } from "react-router";
4
4
  const SEO_TITLE = "Clips - Open Source screen recorder";
5
5
  const SEO_DESCRIPTION =
6
6
  "Open Source screen recorder and meeting-notes app with AI transcripts, summaries, search, dictation, and agent-readable share links.";
7
+ const SSR_REDIRECT_CONTENT_TYPE = "text/html; charset=utf-8";
7
8
 
8
9
  export function meta() {
9
10
  return [
@@ -38,11 +39,18 @@ function buildTarget(url: URL): string {
38
39
  }
39
40
 
40
41
  export function loader({ url }: LoaderFunctionArgs) {
41
- throw redirect(buildTarget(url));
42
+ const response = redirect(buildTarget(url));
43
+ // The deploy adapter uses the HTML content type to apply Clips' public SSR
44
+ // cache policy. Without it, the root redirect reaches origin on every visit
45
+ // even though the /library shell is already cached at the edge.
46
+ response.headers.set("content-type", SSR_REDIRECT_CONTENT_TYPE);
47
+ throw response;
42
48
  }
43
49
 
44
50
  export function clientLoader({ url }: LoaderFunctionArgs) {
45
- throw redirect(buildTarget(url));
51
+ const response = redirect(buildTarget(url));
52
+ response.headers.set("content-type", SSR_REDIRECT_CONTENT_TYPE);
53
+ throw response;
46
54
  }
47
55
 
48
56
  export function HydrateFallback() {
@@ -1628,7 +1628,7 @@ export default function RecordingPage() {
1628
1628
  onReact={(emoji) => {
1629
1629
  tracking.reportReaction(emoji);
1630
1630
  const liveMs = resolvePlaybackMs();
1631
- fetch(
1631
+ return fetch(
1632
1632
  agentNativePath(
1633
1633
  "/_agent-native/actions/react-to-recording",
1634
1634
  ),
@@ -1647,9 +1647,11 @@ export default function RecordingPage() {
1647
1647
  throw new Error(`react failed: ${res.status}`);
1648
1648
  return playerDataQ.refetch();
1649
1649
  })
1650
- .catch((err) =>
1651
- console.warn("[clips] react failed", err),
1652
- );
1650
+ .then(() => true)
1651
+ .catch((err) => {
1652
+ console.warn("[clips] react failed", err);
1653
+ return false;
1654
+ });
1653
1655
  }}
1654
1656
  className="h-full w-full rounded-none sm:rounded-xl"
1655
1657
  />
@@ -1736,11 +1738,12 @@ export default function RecordingPage() {
1736
1738
  ) : null}
1737
1739
  {recording.enableReactions ? (
1738
1740
  <ReactionsTray
1741
+ reactions={reactions}
1739
1742
  disabled={!recording.enableReactions || !canComment}
1740
1743
  onReact={(emoji) => {
1741
1744
  tracking.reportReaction(emoji);
1742
1745
  const liveMs = resolvePlaybackMs();
1743
- fetch(
1746
+ return fetch(
1744
1747
  agentNativePath(
1745
1748
  "/_agent-native/actions/react-to-recording",
1746
1749
  ),
@@ -1759,9 +1762,11 @@ export default function RecordingPage() {
1759
1762
  throw new Error(`react failed: ${res.status}`);
1760
1763
  return playerDataQ.refetch();
1761
1764
  })
1762
- .catch((err) =>
1763
- console.warn("[clips] react failed", err),
1764
- );
1765
+ .then(() => true)
1766
+ .catch((err) => {
1767
+ console.warn("[clips] react failed", err);
1768
+ return false;
1769
+ });
1765
1770
  }}
1766
1771
  />
1767
1772
  ) : null}