@agent-native/core 0.0.0-beta-20260820172558 → 0.0.0-beta-20260820181038

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 (28) hide show
  1. package/corpus/templates/analytics/CHANGELOG.md +6 -13
  2. package/corpus/templates/analytics/changelog/2026-08-20-staged-data-limit-recovery.md +1 -0
  3. package/corpus/templates/clips/CHANGELOG.md +11 -10
  4. package/corpus/templates/clips/app/i18n/en-US.ts +6 -2
  5. package/corpus/templates/clips/app/routes/download.tsx +111 -17
  6. package/corpus/templates/clips/changelog/2026-08-20-cross-tab-recording-controls.md +1 -0
  7. package/corpus/templates/clips/changelog/2026-08-20-overlay-follow-review-fixes.md +1 -0
  8. package/corpus/templates/clips/changelog/2026-08-20-provider-key-recovery.md +1 -0
  9. package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +109 -46
  10. package/dist/client/blocks/library/diagram.js +1 -1
  11. package/dist/collab/awareness.d.ts +2 -2
  12. package/dist/deploy/workspace-deploy.js +7 -0
  13. package/dist/mcp-client/oauth-routes.d.ts +1 -0
  14. package/dist/mcp-client/oauth-routes.js +15 -2
  15. package/dist/mcp-client/remote-store.js +83 -43
  16. package/dist/notifications/routes.d.ts +3 -3
  17. package/dist/observability/routes.d.ts +3 -3
  18. package/dist/progress/routes.d.ts +1 -1
  19. package/dist/secrets/routes.d.ts +6 -6
  20. package/dist/server/google-oauth-credentials.js +14 -1
  21. package/dist/settings/index.d.ts +1 -1
  22. package/dist/settings/index.js +1 -1
  23. package/dist/settings/store.d.ts +7 -1
  24. package/dist/settings/store.js +25 -3
  25. package/dist/settings/user-settings.d.ts +2 -0
  26. package/dist/settings/user-settings.js +65 -2
  27. package/docs/content/deployment.mdx +89 -14
  28. package/package.json +1 -1
@@ -5,6 +5,12 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
6
  Older updates live in [the changelog folder](./changelog/) and are included in the in-app "What's new" view.
7
7
 
8
+ ## 2026-08-20
9
+
10
+ ### Improved
11
+
12
+ - Analytics now explains how to recover when staged data reaches its size limit.
13
+
8
14
  ## 2026-08-18
9
15
 
10
16
  ### Fixed
@@ -36,16 +42,3 @@ Older updates live in [the changelog folder](./changelog/) and are included in t
36
42
 
37
43
  - Feature flags can be managed across apps whose workspace records use different local IDs
38
44
  - Kept feature flag details readable by stacking rollout controls at narrow widths.
39
-
40
- ## 2026-08-11
41
-
42
- ### Improved
43
-
44
- - Full-page chat composers stay at a focused 750px width.
45
-
46
- ### Fixed
47
-
48
- - Analytics custom blocks keep loading when they use the legacy BigQuery query action name.
49
- - Analytics dashboards keep the selected tab when reopened
50
- - Analytics routes company-knowledge and Slack-context questions to Brain
51
- - Chrome no longer offers to install Analytics as a desktop app.
@@ -2,4 +2,5 @@
2
2
  type: improved
3
3
  date: 2026-08-20
4
4
  ---
5
+
5
6
  Analytics now explains how to recover when staged data reaches its size limit.
@@ -5,6 +5,17 @@ from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
6
  Older updates live in [the changelog folder](./changelog/) and are included in the in-app "What's new" view.
7
7
 
8
+ ## 2026-08-20
9
+
10
+ ### Improved
11
+
12
+ - Recording controls now follow you across browser tabs and page navigations.
13
+
14
+ ### Fixed
15
+
16
+ - Clips overlays now follow tab switches during the recording countdown and recover cleanly on tabs opened before Clips.
17
+ - Clips now opens provider setup instead of retrying a rejected key in a loop.
18
+
8
19
  ## 2026-08-19
9
20
 
10
21
  ### Added
@@ -50,13 +61,3 @@ Older updates live in [the changelog folder](./changelog/) and are included in t
50
61
 
51
62
  - Long desktop recordings now complete or abort resumable uploads cleanly, preserve local recovery copies, and show the actual upload error when saving fails.
52
63
  - Clips now uses consistent text-only share controls with compact copy actions and an expandable Share with agents section across recordings and meetings.
53
-
54
- ## 2026-08-13
55
-
56
- ### Added
57
-
58
- - Private meeting notes can be shared with an external agent through a temporary link.
59
-
60
- ### Improved
61
-
62
- - Comments now render inline Markdown for emphasis, inline code, links, and line breaks while keeping headings out of compact comment surfaces.
@@ -615,12 +615,16 @@ const messages = {
615
615
  alsoFor: "Also available for {{platform}}",
616
616
  backToLibrary: "Back to library",
617
617
  clipsDesktop: "Clips Desktop",
618
+ stable: "Stable",
619
+ nightly: "Nightly",
620
+ switchToNightly: "Switch to Nightly builds",
621
+ switchToStable: "Switch to stable builds",
622
+ retry: "Try again",
618
623
  heroDescription:
619
624
  "A menu-bar recorder for screen, camera, and screen + camera. One-click start, draggable camera bubble, instant-share link when you stop.",
620
625
  versionReleased: "Version {{version}} — released {{date}}",
621
626
  version: "Version {{version}}",
622
- manifestError:
623
- "Could not load release manifest — pick an installer from the releases page.",
627
+ manifestError: "Could not load release manifest. Try again.",
624
628
  loadingRelease: "Loading latest release…",
625
629
  chromeTitle: "Chrome extension for browser logs",
626
630
  chromeDescription:
@@ -29,6 +29,7 @@ export function meta() {
29
29
  }
30
30
 
31
31
  type PlatformId = "mac" | "windows" | "linux";
32
+ type ReleaseChannel = "production" | "nightly";
32
33
 
33
34
  interface PlatformVariant {
34
35
  id: PlatformId;
@@ -48,9 +49,6 @@ interface PlatformVariant {
48
49
 
49
50
  const LATEST_JSON_URL = `${appBasePath()}/api/clips-latest.json`;
50
51
 
51
- const RELEASE_PAGE_URL =
52
- "https://github.com/BuilderIO/agent-native/releases?q=clips-v";
53
-
54
52
  const VARIANTS: PlatformVariant[] = [
55
53
  {
56
54
  id: "mac",
@@ -114,12 +112,18 @@ function primaryDownloadButton(
114
112
  manifest: Manifest | null,
115
113
  manifestError: boolean,
116
114
  downloadLabel: string,
115
+ retryLabel: string,
116
+ onRetry: () => void,
117
117
  ) {
118
118
  const asset = pickAsset(manifest, variant);
119
119
  const Icon = variant.icon;
120
120
  if (asset) {
121
121
  return (
122
- <Button asChild size="lg" className="h-12 gap-2 px-6 text-base">
122
+ <Button
123
+ asChild
124
+ size="lg"
125
+ className="h-12 min-w-[252px] gap-2 px-6 text-base"
126
+ >
123
127
  <a href={asset.url} download onClick={markDesktopAppDownloaded}>
124
128
  <Icon className="h-5 w-5" />
125
129
  {downloadLabel}
@@ -130,17 +134,28 @@ function primaryDownloadButton(
130
134
  if (manifest === null && !manifestError) {
131
135
  return <Skeleton className="h-12 w-[252px] rounded-md" />;
132
136
  }
137
+ if (manifestError) {
138
+ return (
139
+ <Button
140
+ size="lg"
141
+ variant="outline"
142
+ className="h-12 min-w-[252px] gap-2 px-6 text-base"
143
+ onClick={onRetry}
144
+ >
145
+ <Icon className="h-5 w-5" />
146
+ {retryLabel}
147
+ </Button>
148
+ );
149
+ }
133
150
  return (
134
151
  <Button
135
- asChild
136
152
  size="lg"
137
153
  variant="outline"
138
- className="h-12 gap-2 px-6 text-base"
154
+ className="h-12 min-w-[252px] gap-2 px-6 text-base"
155
+ disabled
139
156
  >
140
- <a href={RELEASE_PAGE_URL} rel="noreferrer">
141
- <Icon className="h-5 w-5" />
142
- {downloadLabel}
143
- </a>
157
+ <Icon className="h-5 w-5" />
158
+ {downloadLabel}
144
159
  </Button>
145
160
  );
146
161
  }
@@ -150,6 +165,8 @@ function secondaryDownloadButton(
150
165
  manifest: Manifest | null,
151
166
  manifestError: boolean,
152
167
  downloadLabel: string,
168
+ retryLabel: string,
169
+ onRetry: () => void,
153
170
  ) {
154
171
  const asset = pickAsset(manifest, variant);
155
172
  const Icon = variant.icon;
@@ -168,12 +185,18 @@ function secondaryDownloadButton(
168
185
  if (manifest === null && !manifestError) {
169
186
  return <Skeleton className="h-7 w-[208px] rounded-md" />;
170
187
  }
171
- return (
172
- <Button asChild variant="ghost" className={className}>
173
- <a href={RELEASE_PAGE_URL} rel="noreferrer">
188
+ if (manifestError) {
189
+ return (
190
+ <Button variant="ghost" className={className} onClick={onRetry}>
174
191
  <Icon className="h-4 w-4" />
175
- {downloadLabel}
176
- </a>
192
+ {retryLabel}
193
+ </Button>
194
+ );
195
+ }
196
+ return (
197
+ <Button variant="ghost" className={className} disabled>
198
+ <Icon className="h-4 w-4" />
199
+ {downloadLabel}
177
200
  </Button>
178
201
  );
179
202
  }
@@ -181,9 +204,11 @@ function secondaryDownloadButton(
181
204
  export default function DownloadPage() {
182
205
  const chromeExtensionEnabled = useClipsChromeExtensionEnabled();
183
206
  const t = useT();
207
+ const [channel, setChannel] = useState<ReleaseChannel>("production");
184
208
  const [manifest, setManifest] = useState<Manifest | null>(null);
185
209
  const [manifestError, setManifestError] = useState(false);
186
210
  const [detected, setDetected] = useState<PlatformId | null>(null);
211
+ const [manifestRequest, setManifestRequest] = useState(0);
187
212
 
188
213
  useEffect(() => {
189
214
  setDetected(detectPlatform());
@@ -191,7 +216,13 @@ export default function DownloadPage() {
191
216
 
192
217
  useEffect(() => {
193
218
  let cancelled = false;
194
- fetch(LATEST_JSON_URL)
219
+ setManifest(null);
220
+ setManifestError(false);
221
+ const manifestUrl =
222
+ channel === "nightly"
223
+ ? `${LATEST_JSON_URL}?channel=nightly`
224
+ : LATEST_JSON_URL;
225
+ fetch(manifestUrl)
195
226
  .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))))
196
227
  .then((json) => {
197
228
  if (!cancelled) setManifest(json as Manifest);
@@ -202,7 +233,17 @@ export default function DownloadPage() {
202
233
  return () => {
203
234
  cancelled = true;
204
235
  };
205
- }, []);
236
+ }, [channel, manifestRequest]);
237
+
238
+ const retryManifest = () => {
239
+ setManifestRequest((request) => request + 1);
240
+ };
241
+
242
+ const handleChannelChange = (nextChannel: ReleaseChannel) => {
243
+ setManifest(null);
244
+ setManifestError(false);
245
+ setChannel(nextChannel);
246
+ };
206
247
 
207
248
  const primary = VARIANTS.find((v) => v.id === detected) ?? VARIANTS[0];
208
249
  const secondary = VARIANTS.filter((v) => v.id !== primary.id);
@@ -242,6 +283,14 @@ export default function DownloadPage() {
242
283
  <div className="flex flex-col items-center text-center">
243
284
  <h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">
244
285
  {t("downloadRoute.clipsDesktop")}
286
+ {channel === "nightly" && (
287
+ <>
288
+ {" "}
289
+ <span className="text-highlight">
290
+ {t("downloadRoute.nightly")}
291
+ </span>
292
+ </>
293
+ )}
245
294
  </h1>
246
295
  <p className="mt-4 max-w-xl text-base text-muted-foreground">
247
296
  {t("downloadRoute.heroDescription")}
@@ -253,6 +302,8 @@ export default function DownloadPage() {
253
302
  manifest,
254
303
  manifestError,
255
304
  t("downloadRoute.downloadFor", { platform: primary.label }),
305
+ t("downloadRoute.retry"),
306
+ retryManifest,
256
307
  )}
257
308
  {secondary.map((variant) => (
258
309
  <div key={variant.id}>
@@ -261,6 +312,8 @@ export default function DownloadPage() {
261
312
  manifest,
262
313
  manifestError,
263
314
  t("downloadRoute.alsoFor", { platform: variant.label }),
315
+ t("downloadRoute.retry"),
316
+ retryManifest,
264
317
  )}
265
318
  </div>
266
319
  ))}
@@ -282,6 +335,47 @@ export default function DownloadPage() {
282
335
  <>{t("downloadRoute.loadingRelease")}</>
283
336
  )}
284
337
  </div>
338
+ <div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
339
+ <span>{t("downloadRoute.stable")}</span>
340
+ <button
341
+ type="button"
342
+ role="switch"
343
+ aria-checked={channel === "nightly"}
344
+ aria-label={t(
345
+ channel === "nightly"
346
+ ? "downloadRoute.switchToStable"
347
+ : "downloadRoute.switchToNightly",
348
+ )}
349
+ onClick={() =>
350
+ handleChannelChange(
351
+ channel === "nightly" ? "production" : "nightly",
352
+ )
353
+ }
354
+ className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 ${
355
+ channel === "nightly"
356
+ ? "bg-foreground"
357
+ : "bg-muted-foreground/20"
358
+ }`}
359
+ >
360
+ <span
361
+ aria-hidden="true"
362
+ className={`block size-3.5 rounded-full bg-primary-foreground shadow-sm transition-transform ${
363
+ channel === "nightly"
364
+ ? "translate-x-[18px]"
365
+ : "translate-x-[2px]"
366
+ }`}
367
+ />
368
+ </button>
369
+ <span
370
+ className={
371
+ channel === "nightly"
372
+ ? "font-medium text-foreground"
373
+ : "text-muted-foreground"
374
+ }
375
+ >
376
+ {t("downloadRoute.nightly")}
377
+ </span>
378
+ </div>
285
379
  </div>
286
380
 
287
381
  {chromeExtensionEnabled && (
@@ -2,4 +2,5 @@
2
2
  type: improved
3
3
  date: 2026-08-20
4
4
  ---
5
+
5
6
  Recording controls now follow you across browser tabs and page navigations.
@@ -2,4 +2,5 @@
2
2
  type: fixed
3
3
  date: 2026-08-20
4
4
  ---
5
+
5
6
  Clips overlays now follow tab switches during the recording countdown and recover cleanly on tabs opened before Clips.
@@ -2,4 +2,5 @@
2
2
  type: fixed
3
3
  date: 2026-08-20
4
4
  ---
5
+
5
6
  Clips now opens provider setup instead of retrying a rejected key in a loop.
@@ -1,4 +1,9 @@
1
- import { defineEventHandler, setResponseHeaders, createError } from "h3";
1
+ import {
2
+ createError,
3
+ defineEventHandler,
4
+ getQuery,
5
+ setResponseHeaders,
6
+ } from "h3";
2
7
 
3
8
  /**
4
9
  * Same-origin endpoint that tells the download page which user-facing
@@ -12,14 +17,13 @@ import { defineEventHandler, setResponseHeaders, createError } from "h3";
12
17
  * /download want the raw installers (.dmg / .msi / .AppImage).
13
18
  *
14
19
  * This route therefore hits GitHub's REST API, paginates through
15
- * releases until it finds the most recent published `clips-v*` release,
16
- * and returns its asset list plus metadata.
20
+ * releases until it finds the most recent published release for the requested
21
+ * channel, and returns its asset list plus metadata.
17
22
  *
18
- * The `clips-latest` pointer release is still the release-channel hint:
19
- * when it has a signed updater manifest, we resolve that manifest's
20
- * version back to the matching `clips-v*` release before scanning. That
21
- * keeps the manual download page and the in-app updater pointed at the
22
- * same build by default.
23
+ * The channel's pointer release is still the release-channel hint: when it
24
+ * has a signed updater manifest, we resolve that manifest's version back to
25
+ * the matching versioned release before scanning. That keeps the manual
26
+ * download page and the in-app updater pointed at the same build by default.
23
27
  *
24
28
  * ## Rate-limit hardening
25
29
  *
@@ -38,14 +42,30 @@ import { defineEventHandler, setResponseHeaders, createError } from "h3";
38
42
 
39
43
  const RELEASES_URL_BASE =
40
44
  "https://api.github.com/repos/BuilderIO/agent-native/releases";
41
- const UPDATER_MANIFEST_URL =
42
- "https://github.com/BuilderIO/agent-native/releases/download/clips-latest/clips-latest.json";
43
45
  const PER_PAGE = 100;
44
- // Up to 10 pages = 1000 releases. If clips-v* hasn't shown up by then,
45
- // something else is wrong and the 404 is correct.
46
+ // Up to 10 pages = 1000 releases. If the requested channel has not appeared
47
+ // by then, something else is wrong and the 404 is correct.
46
48
  const MAX_PAGES = 10;
47
49
  const CACHE_TTL_MS = 5 * 60_000;
48
50
 
51
+ export type ClipsReleaseChannel = "production" | "nightly";
52
+
53
+ const RELEASE_CHANNEL_CONFIG: Record<
54
+ ClipsReleaseChannel,
55
+ { releasePrefix: string; updaterManifestUrl: string }
56
+ > = {
57
+ production: {
58
+ releasePrefix: "clips-v",
59
+ updaterManifestUrl:
60
+ "https://github.com/BuilderIO/agent-native/releases/download/clips-latest/clips-latest.json",
61
+ },
62
+ nightly: {
63
+ releasePrefix: "clips-nightly-v",
64
+ updaterManifestUrl:
65
+ "https://github.com/BuilderIO/agent-native/releases/download/clips-nightly-latest/clips-nightly-latest.json",
66
+ },
67
+ };
68
+
49
69
  interface GhAsset {
50
70
  name: string;
51
71
  browser_download_url: string;
@@ -121,7 +141,9 @@ export function classifyClipsAsset(
121
141
  }
122
142
 
123
143
  function parseClipsVersion(tagName: string): number[] | null {
124
- const match = /^clips-v(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(tagName);
144
+ const match = /^clips(?:-nightly)?-v(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(
145
+ tagName,
146
+ );
125
147
  if (!match) return null;
126
148
  return match.slice(1, 4).map((part) => Number(part));
127
149
  }
@@ -139,6 +161,22 @@ export function compareClipsReleaseTags(a: string, b: string): number {
139
161
  return 0;
140
162
  }
141
163
 
164
+ export function isClipsReleaseForChannel(
165
+ release: GhRelease,
166
+ channel: ClipsReleaseChannel,
167
+ ): boolean {
168
+ if (release.draft || !hasInstallerAssets(release)) return false;
169
+ if (channel === "production") {
170
+ return (
171
+ !release.prerelease && /^clips-v\d+\.\d+\.\d+$/.test(release.tag_name)
172
+ );
173
+ }
174
+ return (
175
+ release.prerelease &&
176
+ /^clips-nightly-v\d+\.\d+\.\d+(?:[-+].*)?$/.test(release.tag_name)
177
+ );
178
+ }
179
+
142
180
  function isBetterRelease(candidate: GhRelease, current: GhRelease | null) {
143
181
  if (!current) return true;
144
182
  const versionOrder = compareClipsReleaseTags(
@@ -158,8 +196,11 @@ function hasInstallerAssets(release: GhRelease) {
158
196
  );
159
197
  }
160
198
 
161
- let cache: { data: DownloadManifest; ts: number } | null = null;
162
- let inFlight: Promise<DownloadManifest> | null = null;
199
+ const cache = new Map<
200
+ ClipsReleaseChannel,
201
+ { data: DownloadManifest; ts: number }
202
+ >();
203
+ const inFlight = new Map<ClipsReleaseChannel, Promise<DownloadManifest>>();
163
204
 
164
205
  class UpstreamError extends Error {
165
206
  statusCode: number;
@@ -215,8 +256,10 @@ function isUpdaterManifestLike(value: unknown): value is UpdaterManifest {
215
256
  return typeof obj.version === "string" && obj.version.length > 0;
216
257
  }
217
258
 
218
- async function fetchUpdaterManifest(): Promise<UpdaterManifest> {
219
- const res = await fetch(UPDATER_MANIFEST_URL, {
259
+ async function fetchUpdaterManifest(
260
+ channel: ClipsReleaseChannel,
261
+ ): Promise<UpdaterManifest> {
262
+ const res = await fetch(RELEASE_CHANNEL_CONFIG[channel].updaterManifestUrl, {
220
263
  headers: {
221
264
  accept: "application/json",
222
265
  "user-agent": "clips-download-page",
@@ -236,35 +279,38 @@ async function fetchUpdaterManifest(): Promise<UpdaterManifest> {
236
279
  return json;
237
280
  }
238
281
 
239
- async function findUpdaterPinnedRelease(): Promise<GhRelease | null> {
282
+ async function findUpdaterPinnedRelease(
283
+ channel: ClipsReleaseChannel,
284
+ ): Promise<GhRelease | null> {
240
285
  try {
241
- const manifest = await fetchUpdaterManifest();
242
- const version = manifest.version.replace(/^clips-v/, "");
243
- const release = await fetchReleaseByTag(`clips-v${version}`);
244
- if (!release) return null;
245
- if (release.draft || release.prerelease) return null;
246
- if (!release.tag_name.startsWith("clips-v")) return null;
247
- if (!hasInstallerAssets(release)) return null;
248
- return release;
286
+ const manifest = await fetchUpdaterManifest(channel);
287
+ const releasePrefix = RELEASE_CHANNEL_CONFIG[channel].releasePrefix;
288
+ const version = manifest.version
289
+ .replace(/^clips(?:-nightly)?-v/, "")
290
+ .replace(/^v/, "");
291
+ const release = await fetchReleaseByTag(`${releasePrefix}${version}`);
292
+ return release && isClipsReleaseForChannel(release, channel)
293
+ ? release
294
+ : null;
249
295
  } catch {
250
296
  return null;
251
297
  }
252
298
  }
253
299
 
254
- async function findLatestClipsRelease(): Promise<GhRelease | null> {
255
- // Start with the updater's stable pointer so fresh manual installs and
300
+ async function findLatestClipsRelease(
301
+ channel: ClipsReleaseChannel,
302
+ ): Promise<GhRelease | null> {
303
+ // Start with the updater's channel pointer so fresh manual installs and
256
304
  // auto-updates agree about the channel's current version. Then scan the
257
305
  // versioned releases as a fallback/guard and prefer the highest semver
258
306
  // tag; a republished older tag must not beat a newer build just because
259
307
  // it has a later `published_at`.
260
- let best: GhRelease | null = await findUpdaterPinnedRelease();
308
+ let best: GhRelease | null = await findUpdaterPinnedRelease(channel);
261
309
  for (let page = 1; page <= MAX_PAGES; page++) {
262
310
  const batch = await fetchPage(page);
263
311
  if (batch.length === 0) break;
264
312
  for (const r of batch) {
265
- if (r.draft || r.prerelease) continue;
266
- if (!r.tag_name.startsWith("clips-v")) continue;
267
- if (!hasInstallerAssets(r)) continue;
313
+ if (!isClipsReleaseForChannel(r, channel)) continue;
268
314
  if (isBetterRelease(r, best)) {
269
315
  best = r;
270
316
  }
@@ -274,16 +320,21 @@ async function findLatestClipsRelease(): Promise<GhRelease | null> {
274
320
  return best;
275
321
  }
276
322
 
277
- async function buildManifest(): Promise<DownloadManifest> {
278
- const latest = await findLatestClipsRelease();
323
+ async function buildManifest(
324
+ channel: ClipsReleaseChannel,
325
+ ): Promise<DownloadManifest> {
326
+ const latest = await findLatestClipsRelease(channel);
279
327
  if (!latest) {
280
328
  throw createError({
281
329
  statusCode: 404,
282
- statusMessage: "No published clips-v* release found",
330
+ statusMessage:
331
+ channel === "nightly"
332
+ ? "No published clips-nightly-v* release found"
333
+ : "No published clips-v* release found",
283
334
  });
284
335
  }
285
336
  return {
286
- version: latest.tag_name.replace(/^clips-v/, ""),
337
+ version: latest.tag_name.replace(/^clips(?:-nightly)?-v/, ""),
287
338
  tag: latest.tag_name,
288
339
  pub_date: latest.published_at,
289
340
  notes: latest.body,
@@ -296,31 +347,43 @@ async function buildManifest(): Promise<DownloadManifest> {
296
347
  };
297
348
  }
298
349
 
299
- async function getManifest(): Promise<DownloadManifest> {
350
+ async function getManifest(
351
+ channel: ClipsReleaseChannel = "production",
352
+ ): Promise<DownloadManifest> {
300
353
  const now = Date.now();
301
- if (cache && now - cache.ts < CACHE_TTL_MS) return cache.data;
302
- if (inFlight) return inFlight;
303
- inFlight = (async () => {
354
+ const cached = cache.get(channel);
355
+ if (cached && now - cached.ts < CACHE_TTL_MS) return cached.data;
356
+ const pending = inFlight.get(channel);
357
+ if (pending) return pending;
358
+ const request = (async () => {
304
359
  try {
305
- const data = await buildManifest();
306
- cache = { data, ts: Date.now() };
360
+ const data = await buildManifest(channel);
361
+ cache.set(channel, { data, ts: Date.now() });
307
362
  return data;
308
363
  } catch (err) {
309
364
  // Stale-while-error: if we have an older payload, serve it. Only
310
365
  // bubble the error if the cache is empty.
311
- if (cache) return cache.data;
366
+ if (cached) return cached.data;
312
367
  throw err;
313
368
  } finally {
314
- inFlight = null;
369
+ inFlight.delete(channel);
315
370
  }
316
371
  })();
317
- return inFlight;
372
+ inFlight.set(channel, request);
373
+ return request;
374
+ }
375
+
376
+ export function normalizeClipsReleaseChannel(
377
+ value: unknown,
378
+ ): ClipsReleaseChannel {
379
+ return value === "nightly" ? "nightly" : "production";
318
380
  }
319
381
 
320
382
  export default defineEventHandler(async (event) => {
383
+ const channel = normalizeClipsReleaseChannel(getQuery(event).channel);
321
384
  let manifest: DownloadManifest;
322
385
  try {
323
- manifest = await getManifest();
386
+ manifest = await getManifest(channel);
324
387
  } catch (err) {
325
388
  const e = err as {
326
389
  statusCode?: number;
@@ -63,7 +63,7 @@ function HtmlDiagram({ data, ctx, compact, }) {
63
63
  ? scopeDesignCss(safeCss, `[data-plan-diagram-scope="${scopeId}"]`)
64
64
  : "";
65
65
  }, [data.css, scopeId]);
66
- return (_jsxs("div", { ref: ref, className: cn("plan-diagram-shell", compact && "plan-diagram-compact"), children: [_jsxs("div", { className: "plan-diagram-frame", dir: ctx.textDirection, "data-theme": isDark ? "dark" : "light", "data-style": style, "data-frame": showFrame ? "show" : "hide", "data-text-direction": ctx.textDirection, "data-plan-diagram-scope": scopeId, children: [scopedCss && _jsx("style", { children: scopedCss }), _jsx("div", { className: "plan-diagram-frame-content", dangerouslySetInnerHTML: { __html: safeHtml } })] }), _jsx(RoughOverlay, { scopeRef: ref, enabled: !designMode && style === "sketchy", drawFrame: showFrame, selector: DIAGRAM_ROUGH_SELECTOR }), data.caption && !compact && (_jsx("p", { className: "mt-3 text-sm leading-6 text-muted-foreground", children: data.caption }))] }));
66
+ return (_jsxs("div", { ref: ref, className: cn("plan-diagram-shell", compact && "plan-diagram-compact"), children: [_jsxs("div", { className: "plan-diagram-frame", dir: ctx.textDirection, "data-theme": isDark ? "dark" : "light", "data-style": style, "data-frame": showFrame ? "show" : "hide", "data-text-direction": ctx.textDirection, "data-plan-diagram-scope": scopeId, children: [scopedCss && _jsx("style", { children: scopedCss }), _jsx("div", { className: "plan-diagram-frame-content", "data-theme": isDark ? "dark" : "light", dangerouslySetInnerHTML: { __html: safeHtml } })] }), _jsx(RoughOverlay, { scopeRef: ref, enabled: !designMode && style === "sketchy", drawFrame: showFrame, selector: DIAGRAM_ROUGH_SELECTOR }), data.caption && !compact && (_jsx("p", { className: "mt-3 text-sm leading-6 text-muted-foreground", children: data.caption }))] }));
67
67
  }
68
68
  function resolveVisualFrame(frame, ctx) {
69
69
  const resolved = frame && frame !== "auto" ? frame : (ctx.visualFrame ?? "show");
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
@@ -27,6 +27,7 @@ import { assertEmittedBackgroundFunctionOnDisk, isRecurringJobsDeployEnabled, }
27
27
  import { cloneServerBundleForFunction } from "./function-bundle.js";
28
28
  import { collectImmutableAssetPaths, IMMUTABLE_ASSET_CACHE_HEADERS, } from "./immutable-assets.js";
29
29
  const NETLIFY_WORKSPACE_STATIC_DIR = "_workspace_static";
30
+ const DEFAULT_HOSTED_FEEDBACK_URL = "https://forms.agent-native.com/f/agent-native-feedback/_16ewV";
30
31
  const NETLIFY_PUBLIC_ASSET_EXTENSIONS = new Set([
31
32
  "avif",
32
33
  "css",
@@ -152,6 +153,12 @@ function buildOneApp(workspaceRoot, app, preset, execFile, workspaceApps) {
152
153
  AGENT_NATIVE_WORKSPACE_APP_ID: app,
153
154
  VITE_AGENT_NATIVE_WORKSPACE: "1",
154
155
  VITE_AGENT_NATIVE_WORKSPACE_APP_ID: app,
156
+ ...(preset === "netlify"
157
+ ? {
158
+ VITE_AGENT_NATIVE_FEEDBACK_URL: process.env.VITE_AGENT_NATIVE_FEEDBACK_URL ??
159
+ DEFAULT_HOSTED_FEEDBACK_URL,
160
+ }
161
+ : {}),
155
162
  APP_BASE_PATH: `/${app}`,
156
163
  VITE_APP_BASE_PATH: `/${app}`,
157
164
  AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: workspaceAppAudience,
@@ -25,6 +25,7 @@ export interface McpOAuthRoutesOptions {
25
25
  export declare function redirectWithStagedCookies(event: H3Event, location: string): Response;
26
26
  export declare function mountMcpOAuthRoutes(nitroApp: any, options: McpOAuthRoutesOptions): void;
27
27
  export declare function resolveMcpOAuthScope(serverUrl: URL, requestedScope: unknown): RemoteMcpScope | null;
28
+ export declare function stripMcpOAuthAppBasePath(path: string, basePath: string): string;
28
29
  export declare function resolveManagedMcpOAuthClient(serverUrl: URL): Promise<StoredOAuthClientInformation | undefined>;
29
30
  export declare function setMcpOAuthFlowCookie(event: H3Event, flow: McpOAuthFlow, secure: boolean): void;
30
31
  export declare function readMcpOAuthFlowCookie(event: H3Event): McpOAuthFlow | null;