@agent-native/core 0.90.8 → 0.90.10

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 (114) hide show
  1. package/README.md +7 -28
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +16 -0
  4. package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
  5. package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  6. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +3 -3
  7. package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  8. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +3 -3
  9. package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  10. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +3 -3
  11. package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  12. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +3 -3
  13. package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  14. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +3 -3
  15. package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  16. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +3 -3
  17. package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  18. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +3 -3
  19. package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  20. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +3 -3
  21. package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  22. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +3 -3
  23. package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  24. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +3 -3
  25. package/corpus/core/docs/content/tracking.mdx +3 -3
  26. package/corpus/core/package.json +1 -1
  27. package/corpus/core/src/agent/production-agent.ts +15 -4
  28. package/corpus/core/src/cli/deploy-build.ts +50 -0
  29. package/corpus/core/src/cli/index.ts +9 -4
  30. package/corpus/core/src/client/analytics.ts +17 -3
  31. package/corpus/core/src/client/blocks/library/wireframe-kit.tsx +3 -1
  32. package/corpus/core/src/client/blocks/library/wireframe.tsx +6 -1
  33. package/corpus/core/src/deploy/build.ts +64 -27
  34. package/corpus/core/src/file-upload/builder.ts +10 -23
  35. package/corpus/core/src/file-upload/types.ts +3 -3
  36. package/corpus/core/src/styles/blocks.css +7 -0
  37. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +9 -4
  38. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +21 -7
  39. package/corpus/templates/analytics/app/entry.client.tsx +6 -1
  40. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -1
  41. package/corpus/templates/analytics/changelog/2026-07-07-analytics-now-recovers-from-stale-route-chunks-after-deploys.md +6 -0
  42. package/corpus/templates/analytics/changelog/2026-07-07-metric-dashboard-cards-now-show-warehouse-backed-numeric-val.md +6 -0
  43. package/corpus/templates/clips/actions/create-recording.ts +1 -1
  44. package/corpus/templates/clips/actions/finalize-recording.ts +2 -2
  45. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -1
  46. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +1 -1
  47. package/corpus/templates/clips/changelog/2026-07-07-clip-video-uploads-now-compress-in-the-background-while-keeping.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-07-desktop-settings-and-fn-dictation-are-more-reliable.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-07-desktop-settings-now-includes-a-manual-update-check-with-res.md +6 -0
  50. package/corpus/templates/clips/desktop/src/app.tsx +95 -8
  51. package/corpus/templates/clips/desktop/src/lib/updater.ts +60 -43
  52. package/corpus/templates/clips/desktop/src/styles.css +5 -0
  53. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +93 -23
  54. package/corpus/templates/dispatch/changelog/2026-07-07-overview-is-now-just-ask-dispatch-and-a-list-of-apps.md +6 -0
  55. package/dist/agent/production-agent.d.ts.map +1 -1
  56. package/dist/agent/production-agent.js +16 -3
  57. package/dist/agent/production-agent.js.map +1 -1
  58. package/dist/cli/deploy-build.d.ts +12 -0
  59. package/dist/cli/deploy-build.d.ts.map +1 -0
  60. package/dist/cli/deploy-build.js +30 -0
  61. package/dist/cli/deploy-build.js.map +1 -0
  62. package/dist/cli/index.js +8 -4
  63. package/dist/cli/index.js.map +1 -1
  64. package/dist/client/analytics.d.ts.map +1 -1
  65. package/dist/client/analytics.js +14 -3
  66. package/dist/client/analytics.js.map +1 -1
  67. package/dist/client/blocks/library/wireframe-kit.d.ts +1 -0
  68. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -1
  69. package/dist/client/blocks/library/wireframe-kit.js +2 -1
  70. package/dist/client/blocks/library/wireframe-kit.js.map +1 -1
  71. package/dist/client/blocks/library/wireframe.d.ts.map +1 -1
  72. package/dist/client/blocks/library/wireframe.js +6 -1
  73. package/dist/client/blocks/library/wireframe.js.map +1 -1
  74. package/dist/collab/awareness.d.ts +2 -2
  75. package/dist/collab/awareness.d.ts.map +1 -1
  76. package/dist/collab/struct-routes.d.ts +1 -1
  77. package/dist/deploy/build.d.ts +6 -0
  78. package/dist/deploy/build.d.ts.map +1 -1
  79. package/dist/deploy/build.js +54 -25
  80. package/dist/deploy/build.js.map +1 -1
  81. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  82. package/dist/file-upload/builder.d.ts.map +1 -1
  83. package/dist/file-upload/builder.js +9 -19
  84. package/dist/file-upload/builder.js.map +1 -1
  85. package/dist/file-upload/types.d.ts +3 -3
  86. package/dist/file-upload/types.d.ts.map +1 -1
  87. package/dist/file-upload/types.js.map +1 -1
  88. package/dist/notifications/routes.d.ts +1 -1
  89. package/dist/resources/handlers.d.ts +3 -3
  90. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  91. package/dist/styles/blocks.css +7 -0
  92. package/docs/content/agent-native-toolkit.mdx +1 -1
  93. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  94. package/docs/content/locales/ar-SA/tracking.mdx +3 -3
  95. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  96. package/docs/content/locales/de-DE/tracking.mdx +3 -3
  97. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  98. package/docs/content/locales/es-ES/tracking.mdx +3 -3
  99. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  100. package/docs/content/locales/fr-FR/tracking.mdx +3 -3
  101. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  102. package/docs/content/locales/hi-IN/tracking.mdx +3 -3
  103. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  104. package/docs/content/locales/ja-JP/tracking.mdx +3 -3
  105. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  106. package/docs/content/locales/ko-KR/tracking.mdx +3 -3
  107. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  108. package/docs/content/locales/pt-BR/tracking.mdx +3 -3
  109. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  110. package/docs/content/locales/zh-CN/tracking.mdx +3 -3
  111. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  112. package/docs/content/locales/zh-TW/tracking.mdx +3 -3
  113. package/docs/content/tracking.mdx +3 -3
  114. package/package.json +1 -1
@@ -16,8 +16,8 @@ export interface FileUploadInput {
16
16
  mimeType?: string;
17
17
  /** Optional owner email for per-user scoping in fallback storage. */
18
18
  ownerEmail?: string;
19
- /** Builder.io upload hint: return after asset registration instead of waiting for server-side compression. */
20
- skipCompressionWait?: boolean;
19
+ /** Builder.io upload hint: return one stable URL while compression finishes asynchronously. */
20
+ stableUrl?: boolean;
21
21
  /** Builder.io upload hint: set false for internal artifacts that should not appear in the asset library. */
22
22
  recordAsset?: boolean;
23
23
  }
@@ -81,7 +81,7 @@ export interface FileUploadProvider {
81
81
  completeSession(
82
82
  session: ResumableUploadSession,
83
83
  filename: string,
84
- options?: { skipCompressionWait?: boolean; recordAsset?: boolean },
84
+ options?: { stableUrl?: boolean; recordAsset?: boolean },
85
85
  ): Promise<string>;
86
86
  };
87
87
  }
@@ -1007,6 +1007,13 @@
1007
1007
  min-height: 100%;
1008
1008
  }
1009
1009
 
1010
+ .plan-html-frame[data-frame="hide"]:not([data-render-mode="design"])
1011
+ > .plan-html-frame-content
1012
+ > :first-child {
1013
+ margin: 0 !important;
1014
+ padding: 0 !important;
1015
+ }
1016
+
1010
1017
  .plan-html-frame[data-prototype-live="true"] {
1011
1018
  overflow: auto;
1012
1019
  }
@@ -106,12 +106,17 @@ agent answers about browser recordings in the Analytics template.
106
106
 
107
107
  ## Capture Defaults
108
108
 
109
- - Replay is off unless enabled by config/env.
110
- - Consumers can enable replay directly with
109
+ - Replay is on by default for signed-in hosted users when
110
+ `VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY` or `configureTracking({ key })` is
111
+ present. The default sample rate is 100% of eligible sessions.
112
+ - Replay remains off when no first-party analytics key is configured, and it is
113
+ not auto-enabled on localhost/local dev. Consumers can still enable replay
114
+ directly with
111
115
  `configureTracking({ key, endpoint, sessionReplay: { enabled: true } })`.
116
+ - Apps can opt out with `configureTracking({ sessionReplay: false })`.
112
117
  - Agent Native templates already call `configureTracking()` in their roots;
113
- hosted template deployments can usually enable replay with Vite/Netlify env
114
- vars on the recorded site.
118
+ hosted template deployments only need the normal Agent Native Analytics
119
+ Vite/Netlify env vars on the recorded site.
115
120
  - Inputs are masked by default. Page text is visible unless marked with
116
121
  `.an-mask` or `data-an-mask`.
117
122
  - Use `.an-block`, `.an-ignore`, `data-an-block`, or `data-an-ignore` for
@@ -155,6 +155,24 @@ export function formatMetricValue(
155
155
  : String(raw ?? "-");
156
156
  }
157
157
 
158
+ function isNumericLikeValue(value: unknown): boolean {
159
+ if (typeof value === "number") return Number.isFinite(value);
160
+ return (
161
+ typeof value === "string" &&
162
+ value.trim() !== "" &&
163
+ Number.isFinite(Number(value))
164
+ );
165
+ }
166
+
167
+ export function detectMetricValueColumn(
168
+ row: Record<string, unknown>,
169
+ configuredKey?: string,
170
+ ): string {
171
+ const cols = Object.keys(row);
172
+ if (configuredKey && cols.includes(configuredKey)) return configuredKey;
173
+ return cols.find((key) => isNumericLikeValue(row[key])) || cols[0] || "";
174
+ }
175
+
158
176
  function parsePrometheusSeriesLabel(label: string): {
159
177
  metric: string;
160
178
  labels: Record<string, string>;
@@ -804,7 +822,7 @@ function detectKeys(
804
822
  if (yKeys.length === 0) {
805
823
  for (const c of cols) {
806
824
  if (c === xKey) continue;
807
- if (typeof sample[c] === "number") yKeys.push(c);
825
+ if (isNumericLikeValue(sample[c])) yKeys.push(c);
808
826
  }
809
827
  }
810
828
  if (yKeys.length === 0 && cols.length > 1) {
@@ -1120,14 +1138,10 @@ function MetricRenderer({
1120
1138
  panel: SqlPanel;
1121
1139
  }) {
1122
1140
  const row = rows[0];
1123
- const cols = Object.keys(row);
1124
- const valueCol =
1125
- panel.config?.yKey ||
1126
- cols.find((c) => typeof row[c] === "number") ||
1127
- cols[0];
1141
+ const valueCol = detectMetricValueColumn(row, panel.config?.yKey);
1128
1142
 
1129
1143
  let raw: unknown;
1130
- if (rows.length > 1 && typeof row[valueCol] === "number") {
1144
+ if (rows.length > 1 && isNumericLikeValue(row[valueCol])) {
1131
1145
  raw = rows.reduce((sum, r) => sum + (Number(r[valueCol]) || 0), 0);
1132
1146
  } else {
1133
1147
  raw = row[valueCol];
@@ -1,7 +1,12 @@
1
- import { appBasePath } from "@agent-native/core/client/api-path";
1
+ import {
2
+ appBasePath,
3
+ installRouteChunkRecovery,
4
+ } from "@agent-native/core/client";
2
5
  import { hydrateRoot } from "react-dom/client";
3
6
  import { HydratedRouter } from "react-router/dom";
4
7
 
8
+ installRouteChunkRecovery();
9
+
5
10
  const basePath = appBasePath();
6
11
  const pathname = window.location.pathname;
7
12
  const routerBasePath =
@@ -546,7 +546,7 @@ configureTracking({
546
546
  sessionReplay: {
547
547
  enabled: true,
548
548
  requireSignedInUser: true,
549
- sampleRate: 0.1,
549
+ sampleRate: 1,
550
550
  },
551
551
  getDefaultProps: (_event, props) => ({
552
552
  ...props,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-07
4
+ ---
5
+
6
+ Analytics now recovers from stale route chunks after deploys instead of leaving dashboards stuck on failed module loads.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-07
4
+ ---
5
+
6
+ Metric dashboard cards now show warehouse-backed numeric values correctly when query results return numbers as strings.
@@ -113,7 +113,7 @@ export default defineAction({
113
113
  sessionId: session.sessionId,
114
114
  meta: {
115
115
  ...session.meta,
116
- skipCompressionWait: true,
116
+ stableUrl: true,
117
117
  recordAsset: false,
118
118
  },
119
119
  bytesUploaded: 0,
@@ -596,7 +596,7 @@ export default defineAction({
596
596
  typeof resumableSession.meta.filename === "string"
597
597
  ? resumableSession.meta.filename
598
598
  : "",
599
- { skipCompressionWait: true, recordAsset: false },
599
+ { stableUrl: true, recordAsset: false },
600
600
  );
601
601
  debugLog("[finalize] resumable upload completed", { id, videoUrl });
602
602
  try {
@@ -966,7 +966,7 @@ export default defineAction({
966
966
  filename: `${id}.${videoFormat}`,
967
967
  mimeType,
968
968
  ownerEmail,
969
- skipCompressionWait: true,
969
+ stableUrl: true,
970
970
  recordAsset: false,
971
971
  });
972
972
  } catch (err) {
@@ -296,7 +296,7 @@ export default defineAction({
296
296
  filename: `${id}.mp4`,
297
297
  mimeType: media.mimeType,
298
298
  ownerEmail,
299
- skipCompressionWait: true,
299
+ stableUrl: true,
300
300
  recordAsset: false,
301
301
  });
302
302
 
@@ -218,7 +218,7 @@ export async function ensureRecordingSeekable(params: {
218
218
  filename: `${recordingId}.${videoFormat}`,
219
219
  mimeType,
220
220
  ownerEmail,
221
- skipCompressionWait: true,
221
+ stableUrl: true,
222
222
  recordAsset: false,
223
223
  });
224
224
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-07
4
+ ---
5
+
6
+ Clip video uploads now compress in the background while keeping a single stable playback URL.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-07
4
+ ---
5
+
6
+ Desktop settings stay out of the server URL field on open, and Fn dictation recovers when macOS disables its Input Monitoring tap.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-07
4
+ ---
5
+
6
+ Desktop settings now include a manual update check with restart status.
@@ -81,7 +81,14 @@ import {
81
81
  saveBool,
82
82
  saveString,
83
83
  } from "./lib/storage";
84
- import { installAndRestart, isUpdatePendingRestart } from "./lib/updater";
84
+ import {
85
+ canCheckForUpdates,
86
+ installAndRestart,
87
+ isUpdatePendingRestart,
88
+ retryUpdateCheck,
89
+ useUpdateStatus,
90
+ type UpdateStatus,
91
+ } from "./lib/updater";
85
92
  import { normalizeServerUrl } from "./lib/url";
86
93
  import {
87
94
  installDesktopVoiceDictation,
@@ -3521,6 +3528,25 @@ function formatStorageBytes(bytes: number): string {
3521
3528
  return `${Math.max(1, Math.round(mb))} MB`;
3522
3529
  }
3523
3530
 
3531
+ function desktopUpdateStatusText(status: UpdateStatus): string {
3532
+ switch (status.state) {
3533
+ case "idle":
3534
+ return "Clips checks automatically after launch and every 4 hours.";
3535
+ case "checking":
3536
+ return "Checking for updates...";
3537
+ case "not-available":
3538
+ return "Clips is up to date.";
3539
+ case "available":
3540
+ return `Update ${status.version} found. Downloading now...`;
3541
+ case "downloading":
3542
+ return `Downloading update ${status.version}... ${status.percent}%`;
3543
+ case "downloaded":
3544
+ return `Update ${status.version} is ready. Restart to install it.`;
3545
+ case "error":
3546
+ return `Update check failed: ${status.message}`;
3547
+ }
3548
+ }
3549
+
3524
3550
  function Setup({
3525
3551
  initial,
3526
3552
  serverUrl,
@@ -3568,8 +3594,8 @@ function Setup({
3568
3594
  }) {
3569
3595
  const [url, setUrl] = useState(initial ?? DEFAULT_URL);
3570
3596
  const [readinessOpen, setReadinessOpen] = useState(false);
3571
- const inputRef = useRef<HTMLInputElement | null>(null);
3572
3597
  const featureConfig = useFeatureConfig();
3598
+ const updateStatus = useUpdateStatus();
3573
3599
  const voiceEnabled = featureConfig?.voiceEnabled !== false;
3574
3600
  const meetingsEnabled = featureConfig?.meetingsEnabled !== false;
3575
3601
  const launchAtLoginEnabled = featureConfig?.launchAtLoginEnabled !== false;
@@ -3868,10 +3894,6 @@ function Setup({
3868
3894
  );
3869
3895
  }
3870
3896
 
3871
- useEffect(() => {
3872
- inputRef.current?.focus();
3873
- }, []);
3874
-
3875
3897
  useEffect(() => {
3876
3898
  let cancelled = false;
3877
3899
  const refresh = () => {
@@ -4134,6 +4156,32 @@ function Setup({
4134
4156
  if (providerStatus[byokProvider]) return null;
4135
4157
  return `${keyForByokProvider(byokProvider)} is not set — cleanup will fail until configured.`;
4136
4158
  })();
4159
+ const updateChecksSupported = canCheckForUpdates();
4160
+ const updateBusy =
4161
+ updateStatus.state === "checking" ||
4162
+ updateStatus.state === "available" ||
4163
+ updateStatus.state === "downloading";
4164
+ const updateReady = updateStatus.state === "downloaded";
4165
+ const updateStatusClass =
4166
+ updateStatus.state === "error"
4167
+ ? "setup-warning"
4168
+ : updateStatus.state === "downloaded" ||
4169
+ updateStatus.state === "not-available"
4170
+ ? "setup-success"
4171
+ : "setup-hint";
4172
+ const updateCheckLabel = !updateChecksSupported
4173
+ ? "Release builds only"
4174
+ : updateStatus.state === "checking"
4175
+ ? "Checking..."
4176
+ : updateStatus.state === "downloading"
4177
+ ? `Downloading ${updateStatus.percent}%`
4178
+ : "Check now";
4179
+
4180
+ function checkForDesktopUpdate() {
4181
+ retryUpdateCheck().catch((err) => {
4182
+ console.error("[clips-updater] manual check failed:", err);
4183
+ });
4184
+ }
4137
4185
 
4138
4186
  return (
4139
4187
  <form className="setup" onSubmit={handleSubmit}>
@@ -4161,13 +4209,12 @@ function Setup({
4161
4209
  />
4162
4210
  <input
4163
4211
  id="clips-url"
4164
- ref={inputRef}
4165
4212
  type="url"
4166
4213
  value={url}
4167
4214
  onChange={(e) => setUrl(e.target.value)}
4168
4215
  placeholder="http://localhost:8080"
4169
4216
  />
4170
- <button className="primary" type="submit">
4217
+ <button className="secondary setup-connect-button" type="submit">
4171
4218
  Connect
4172
4219
  </button>
4173
4220
  </div>
@@ -4214,6 +4261,46 @@ function Setup({
4214
4261
  </div>
4215
4262
  </div>
4216
4263
 
4264
+ <div className="setup-section">
4265
+ <SettingLabel
4266
+ label="Desktop updates"
4267
+ hint="Check the signed Clips desktop release channel and download any available update."
4268
+ />
4269
+ <div className="setup-button-row">
4270
+ <button
4271
+ type="button"
4272
+ className="secondary"
4273
+ onClick={checkForDesktopUpdate}
4274
+ disabled={!updateChecksSupported || updateBusy || updateReady}
4275
+ >
4276
+ <IconRefresh
4277
+ size={15}
4278
+ stroke={1.9}
4279
+ className={updateBusy ? "update-spinner" : undefined}
4280
+ />
4281
+ {updateCheckLabel}
4282
+ </button>
4283
+ {updateReady ? (
4284
+ <button
4285
+ type="button"
4286
+ className="secondary"
4287
+ onClick={() => {
4288
+ installAndRestart().catch((err) => {
4289
+ console.error("[clips-updater] relaunch failed:", err);
4290
+ });
4291
+ }}
4292
+ >
4293
+ Restart to install
4294
+ </button>
4295
+ ) : null}
4296
+ </div>
4297
+ <p className={updateStatusClass}>
4298
+ {updateChecksSupported
4299
+ ? desktopUpdateStatusText(updateStatus)
4300
+ : "Update checks are available in signed release builds."}
4301
+ </p>
4302
+ </div>
4303
+
4217
4304
  <div className="setup-section-heading">Permissions</div>
4218
4305
 
4219
4306
  <div className="setup-section">
@@ -26,6 +26,13 @@ let cachedStatus: UpdateStatus = { state: "idle" };
26
26
  let pendingUpdate: Update | null = null;
27
27
  const listeners = new Set<StatusListener>();
28
28
  let started = false;
29
+ let checkInFlight: Promise<void> | null = null;
30
+
31
+ function canRunUpdateChecks() {
32
+ // Dev and local release builds are for testing the current checkout. Do not
33
+ // replace them with the published auto-update channel.
34
+ return !import.meta.env.DEV && !__CLIPS_DESKTOP_LOCAL_BUILD__;
35
+ }
29
36
 
30
37
  function setStatus(next: UpdateStatus) {
31
38
  cachedStatus = next;
@@ -33,55 +40,60 @@ function setStatus(next: UpdateStatus) {
33
40
  }
34
41
 
35
42
  async function runCheck() {
36
- try {
37
- setStatus({ state: "checking" });
38
- const update = await check();
39
- if (!update) {
40
- setStatus({ state: "not-available" });
41
- return;
42
- }
43
- pendingUpdate = update;
44
- const version = update.version;
45
- const notes = update.body ?? undefined;
46
- setStatus({ state: "available", version, notes });
47
-
48
- // Download ONLY — do not install here. On macOS `install` swaps the .app
49
- // bundle on disk while this process keeps running, which invalidates the
50
- // running process's Screen Recording (TCC) grant and breaks capture until
51
- // relaunch. We defer the swap to `installAndRestart()` so a downloaded-but-
52
- // not-yet-installed update leaves the running app fully functional; the
53
- // user records normally until they choose to restart.
54
- let total = 0;
55
- let downloaded = 0;
56
- await update.download((event) => {
57
- if (event.event === "Started") {
58
- total = event.data.contentLength ?? 0;
59
- downloaded = 0;
60
- setStatus({ state: "downloading", version, notes, percent: 0 });
61
- } else if (event.event === "Progress") {
62
- downloaded += event.data.chunkLength;
63
- const percent =
64
- total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : 0;
65
- setStatus({ state: "downloading", version, notes, percent });
66
- } else if (event.event === "Finished") {
67
- setStatus({ state: "downloaded", version, notes });
43
+ if (checkInFlight) return checkInFlight;
44
+
45
+ checkInFlight = (async () => {
46
+ try {
47
+ setStatus({ state: "checking" });
48
+ const update = await check();
49
+ if (!update) {
50
+ setStatus({ state: "not-available" });
51
+ return;
68
52
  }
69
- });
70
- } catch (err) {
71
- const message = err instanceof Error ? err.message : String(err);
72
- setStatus({ state: "error", message });
73
- }
53
+ pendingUpdate = update;
54
+ const version = update.version;
55
+ const notes = update.body ?? undefined;
56
+ setStatus({ state: "available", version, notes });
57
+
58
+ // Download ONLY — do not install here. On macOS `install` swaps the .app
59
+ // bundle on disk while this process keeps running, which invalidates the
60
+ // running process's Screen Recording (TCC) grant and breaks capture until
61
+ // relaunch. We defer the swap to `installAndRestart()` so a downloaded-but-
62
+ // not-yet-installed update leaves the running app fully functional; the
63
+ // user records normally until they choose to restart.
64
+ let total = 0;
65
+ let downloaded = 0;
66
+ await update.download((event) => {
67
+ if (event.event === "Started") {
68
+ total = event.data.contentLength ?? 0;
69
+ downloaded = 0;
70
+ setStatus({ state: "downloading", version, notes, percent: 0 });
71
+ } else if (event.event === "Progress") {
72
+ downloaded += event.data.chunkLength;
73
+ const percent =
74
+ total > 0
75
+ ? Math.min(100, Math.round((downloaded / total) * 100))
76
+ : 0;
77
+ setStatus({ state: "downloading", version, notes, percent });
78
+ } else if (event.event === "Finished") {
79
+ setStatus({ state: "downloaded", version, notes });
80
+ }
81
+ });
82
+ } catch (err) {
83
+ const message = err instanceof Error ? err.message : String(err);
84
+ setStatus({ state: "error", message });
85
+ } finally {
86
+ checkInFlight = null;
87
+ }
88
+ })();
89
+
90
+ return checkInFlight;
74
91
  }
75
92
 
76
93
  function startUpdateLoop() {
77
94
  if (started) return;
78
95
  started = true;
79
- // Skip update checks in dev — there's no release endpoint to check.
80
- if (import.meta.env.DEV) return;
81
- // Local release builds are for testing the current checkout. Do not replace
82
- // them with the published auto-update channel just because package.json has
83
- // a lower development version.
84
- if (__CLIPS_DESKTOP_LOCAL_BUILD__) return;
96
+ if (!canRunUpdateChecks()) return;
85
97
  // Check 3s after launch (let the popover finish first paint), then every
86
98
  // 4 hours. Matches the cadence used by the Electron app.
87
99
  setTimeout(runCheck, 3000);
@@ -125,11 +137,16 @@ export function isUpdatePendingRestart(): boolean {
125
137
  return cachedStatus.state === "downloaded";
126
138
  }
127
139
 
140
+ export function canCheckForUpdates(): boolean {
141
+ return canRunUpdateChecks();
142
+ }
143
+
128
144
  /**
129
145
  * Manual retry entry point. Used by the UpdateBanner's error state to let
130
146
  * users re-attempt after a signature-verification / download / network
131
147
  * failure. Runs a full check + download pass, same as the periodic loop.
132
148
  */
133
149
  export function retryUpdateCheck(): Promise<void> {
150
+ if (!canRunUpdateChecks()) return Promise.resolve();
134
151
  return runCheck();
135
152
  }
@@ -835,6 +835,11 @@ body[data-clips-route="recording-pill"] #root {
835
835
  cursor: not-allowed;
836
836
  }
837
837
 
838
+ .setup-connect-button {
839
+ height: 38px;
840
+ margin-top: 2px;
841
+ }
842
+
838
843
  .primary.start {
839
844
  margin-top: 2px;
840
845
  }