@agent-native/core 0.77.11 → 0.77.12

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.
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.77.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 67dba9b: Sync builder-agent-native-starter toolchain files (React Router config, Vite config, server plugins, etc.) alongside the manifest so dependency bumps from templates/chat do not leave the starter in a broken state. Standalone UI scaffolds re-declare tsconfig `paths` and `baseUrl` for `@/*` resolution; headless scaffolds omit `baseUrl` for TS 6 tsgo compatibility. Netlify post-process now rewrites unindented template build commands.
8
+
3
9
  ## 0.77.11
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.77.11",
3
+ "version": "0.77.12",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -27,19 +27,7 @@ const STANDALONE_EXACT_DEPENDENCY_OVERRIDES: Record<string, string> = {
27
27
  "@react-router/fs-routes": "8.0.1",
28
28
  "react-router": "8.0.1",
29
29
  };
30
- const SENTRY_MINIMUM_RELEASE_AGE_EXCLUDES = [
31
- '"@sentry/browser"',
32
- '"@sentry/browser-utils"',
33
- '"@sentry/conventions"',
34
- '"@sentry/core"',
35
- '"@sentry/feedback"',
36
- '"@sentry/node"',
37
- '"@sentry/node-core"',
38
- '"@sentry/opentelemetry"',
39
- '"@sentry/replay"',
40
- '"@sentry/replay-canvas"',
41
- '"@sentry/server-utils"',
42
- ];
30
+ const SENTRY_MINIMUM_RELEASE_AGE_EXCLUDES = ['"@sentry/*"'];
43
31
  const FIRST_PARTY_TARBALL_SYMLINK_EXCLUDES = [
44
32
  "*/CLAUDE.md",
45
33
  "*/.claude/skills",
@@ -978,6 +966,7 @@ function postProcessStandalone(
978
966
  const appTitle = appTitleForScaffold(name);
979
967
  replacePlaceholders(targetDir, name, appTitle);
980
968
  rewriteTrackingAppId(targetDir, name, templateName);
969
+ rewriteAgentChatAppId(targetDir, name, templateName);
981
970
  fixPackageJsonName(targetDir, name, templateName);
982
971
  fixWebManifestName(targetDir, name, templateName);
983
972
  rewriteNetlifyToml(targetDir, name, "standalone");
@@ -1078,9 +1067,40 @@ function postProcessStandalone(
1078
1067
  }
1079
1068
  } catch {}
1080
1069
 
1070
+ fixStandaloneTsconfig(targetDir, templateName);
1071
+
1081
1072
  setupAgentSymlinks(targetDir);
1082
1073
  }
1083
1074
 
1075
+ function fixStandaloneTsconfig(targetDir: string, templateName?: string): void {
1076
+ const tsconfigPath = path.join(targetDir, "tsconfig.json");
1077
+ if (!fs.existsSync(tsconfigPath)) return;
1078
+ try {
1079
+ const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")) as {
1080
+ compilerOptions?: Record<string, unknown>;
1081
+ };
1082
+ tsconfig.compilerOptions ??= {};
1083
+ const hasUiApp =
1084
+ templateName !== "headless" && fs.existsSync(path.join(targetDir, "app"));
1085
+ const paths = {
1086
+ ...((tsconfig.compilerOptions.paths as Record<string, string[]>) ?? {}),
1087
+ };
1088
+ paths["*"] ??= ["./*"];
1089
+ if (hasUiApp) {
1090
+ paths["@/*"] ??= ["./app/*"];
1091
+ paths["@shared/*"] ??= ["./shared/*"];
1092
+ // Child baseUrl anchors @/* for apps extending legacy core tsconfig bases
1093
+ // that still ship baseUrl. Headless scaffolds omit it so raw tsgo --noEmit
1094
+ // keeps working under TS 6.
1095
+ tsconfig.compilerOptions.baseUrl = ".";
1096
+ } else {
1097
+ delete tsconfig.compilerOptions.baseUrl;
1098
+ }
1099
+ tsconfig.compilerOptions.paths = paths;
1100
+ fs.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
1101
+ } catch {}
1102
+ }
1103
+
1084
1104
  /* ─────────────────────────────────────────────────────────────────────────
1085
1105
  * Prompting helpers
1086
1106
  * ───────────────────────────────────────────────────────────────────────── */
@@ -1734,7 +1754,7 @@ function rewriteNetlifyToml(
1734
1754
 
1735
1755
  try {
1736
1756
  let content = fs.readFileSync(netlifyPath, "utf-8");
1737
- const originalCommand = content.match(/^ command = "([^"]*)"$/m)?.[1];
1757
+ const originalCommand = content.match(/^\s*command = "([^"]*)"$/m)?.[1];
1738
1758
  const usesUnpooledDatabase =
1739
1759
  originalCommand?.includes("NETLIFY_DATABASE_URL_UNPOOLED") ?? false;
1740
1760
  const buildCommand =
@@ -1754,7 +1774,7 @@ function rewriteNetlifyToml(
1754
1774
  : ".netlify/functions-internal";
1755
1775
 
1756
1776
  content = content
1757
- .replace(/^ command = ".*"$/m, ` command = "${command}"`)
1777
+ .replace(/^(\s*)command = ".*"$/m, `$1command = "${command}"`)
1758
1778
  .replace(
1759
1779
  /publish = "templates\/[^"]+\/dist"/g,
1760
1780
  `publish = "${publishPath}"`,
@@ -1776,6 +1796,36 @@ function rewriteNetlifyToml(
1776
1796
  } catch {}
1777
1797
  }
1778
1798
 
1799
+ function rewriteAgentChatAppId(
1800
+ appDir: string,
1801
+ appName: string,
1802
+ templateName?: string,
1803
+ ): void {
1804
+ const pluginPath = path.join(appDir, "server", "plugins", "agent-chat.ts");
1805
+ if (!fs.existsSync(pluginPath)) return;
1806
+
1807
+ try {
1808
+ const content = fs.readFileSync(pluginPath, "utf-8");
1809
+ const sourceAppIds = ["chat", "starter"];
1810
+ if (templateName && templateName !== appName) {
1811
+ sourceAppIds.push(templateName);
1812
+ }
1813
+ const pattern = new RegExp(
1814
+ `(appId:\\s*)(["'])(${sourceAppIds.map(escapeRegExp).join("|")})\\2`,
1815
+ );
1816
+ if (!pattern.test(content)) return;
1817
+
1818
+ const next = content.replace(
1819
+ pattern,
1820
+ (_match, prefix: string, quote: string) =>
1821
+ `${prefix}${quote}${appName}${quote}`,
1822
+ );
1823
+ if (next !== content) {
1824
+ fs.writeFileSync(pluginPath, next);
1825
+ }
1826
+ } catch {}
1827
+ }
1828
+
1779
1829
  function rewriteTrackingAppId(
1780
1830
  appDir: string,
1781
1831
  appName: string,
@@ -8,8 +8,45 @@ import { _postProcessStandalone } from "./create.js";
8
8
  export const STARTER_APP_NAME = "builder-agent-native-starter";
9
9
  export const CHAT_TEMPLATE = "chat";
10
10
 
11
+ /** Toolchain files that must track templates/chat or typecheck/build drift. */
12
+ export const STARTER_TOOLCHAIN_SYNC_PATHS = [
13
+ "react-router.config.ts",
14
+ "vite.config.ts",
15
+ "tsconfig.json",
16
+ "ssr-entry.ts",
17
+ "app/vite-env.d.ts",
18
+ "app/routes.ts",
19
+ "server/routes/[...page].get.ts",
20
+ // server/plugins/* are intentionally excluded: starter keeps its own
21
+ // systemPrompt and auth marketing copy; post-process only rewrites appId/title.
22
+ "server/middleware/auth.ts",
23
+ "components.json",
24
+ ".oxfmtrc.json",
25
+ "netlify.toml",
26
+ ] as const;
27
+
28
+ export type StarterToolchainSyncPath =
29
+ (typeof STARTER_TOOLCHAIN_SYNC_PATHS)[number];
30
+
11
31
  type PackageJson = Record<string, unknown>;
12
32
 
33
+ export type StandaloneChatSnapshot = {
34
+ cleanup: () => void;
35
+ dir: string;
36
+ packageJson: PackageJson;
37
+ pnpmWorkspaceYaml: string | null;
38
+ toolchainFiles: Map<StarterToolchainSyncPath, string>;
39
+ };
40
+
41
+ export function listStarterSyncPaths(): string[] {
42
+ return [
43
+ "package.json",
44
+ "pnpm-lock.yaml",
45
+ "pnpm-workspace.yaml",
46
+ ...STARTER_TOOLCHAIN_SYNC_PATHS,
47
+ ];
48
+ }
49
+
13
50
  export function findAgentNativeRoot(startDir = process.cwd()): string {
14
51
  let dir = path.resolve(startDir);
15
52
  for (let i = 0; i < 12; i++) {
@@ -31,32 +68,62 @@ export function findAgentNativeRoot(startDir = process.cwd()): string {
31
68
  );
32
69
  }
33
70
 
34
- export function generateStandaloneChatManifest(repoRoot?: string): {
35
- packageJson: PackageJson;
36
- pnpmWorkspaceYaml: string | null;
37
- } {
71
+ export function collectStarterToolchainFiles(
72
+ canonicalDir: string,
73
+ ): Map<StarterToolchainSyncPath, string> {
74
+ const files = new Map<StarterToolchainSyncPath, string>();
75
+ for (const relativePath of STARTER_TOOLCHAIN_SYNC_PATHS) {
76
+ const absolutePath = path.join(canonicalDir, relativePath);
77
+ if (!fs.existsSync(absolutePath)) continue;
78
+ files.set(relativePath, fs.readFileSync(absolutePath, "utf-8"));
79
+ }
80
+ return files;
81
+ }
82
+
83
+ export function createStandaloneChatSnapshot(
84
+ repoRoot?: string,
85
+ ): StandaloneChatSnapshot {
38
86
  const root = repoRoot ?? findAgentNativeRoot();
39
87
  const chatTemplateDir = path.join(root, "templates", CHAT_TEMPLATE);
40
88
  const tempDir = fs.mkdtempSync(
41
89
  path.join(os.tmpdir(), "an-builder-starter-sync-"),
42
90
  );
43
91
 
44
- try {
45
- fs.cpSync(chatTemplateDir, tempDir, { recursive: true });
46
- _postProcessStandalone(STARTER_APP_NAME, tempDir, CHAT_TEMPLATE);
92
+ fs.cpSync(chatTemplateDir, tempDir, { recursive: true });
93
+ _postProcessStandalone(STARTER_APP_NAME, tempDir, CHAT_TEMPLATE);
47
94
 
48
- const packageJson = JSON.parse(
49
- fs.readFileSync(path.join(tempDir, "package.json"), "utf-8"),
50
- ) as PackageJson;
95
+ const packageJson = JSON.parse(
96
+ fs.readFileSync(path.join(tempDir, "package.json"), "utf-8"),
97
+ ) as PackageJson;
51
98
 
52
- const workspacePath = path.join(tempDir, "pnpm-workspace.yaml");
53
- const pnpmWorkspaceYaml = fs.existsSync(workspacePath)
54
- ? fs.readFileSync(workspacePath, "utf-8")
55
- : null;
99
+ const workspacePath = path.join(tempDir, "pnpm-workspace.yaml");
100
+ const pnpmWorkspaceYaml = fs.existsSync(workspacePath)
101
+ ? fs.readFileSync(workspacePath, "utf-8")
102
+ : null;
56
103
 
57
- return { packageJson, pnpmWorkspaceYaml };
104
+ return {
105
+ cleanup: () => {
106
+ fs.rmSync(tempDir, { recursive: true, force: true });
107
+ },
108
+ dir: tempDir,
109
+ packageJson,
110
+ pnpmWorkspaceYaml,
111
+ toolchainFiles: collectStarterToolchainFiles(tempDir),
112
+ };
113
+ }
114
+
115
+ export function generateStandaloneChatManifest(repoRoot?: string): {
116
+ packageJson: PackageJson;
117
+ pnpmWorkspaceYaml: string | null;
118
+ } {
119
+ const snapshot = createStandaloneChatSnapshot(repoRoot);
120
+ try {
121
+ return {
122
+ packageJson: snapshot.packageJson,
123
+ pnpmWorkspaceYaml: snapshot.pnpmWorkspaceYaml,
124
+ };
58
125
  } finally {
59
- fs.rmSync(tempDir, { recursive: true, force: true });
126
+ snapshot.cleanup();
60
127
  }
61
128
  }
62
129
 
@@ -144,73 +211,185 @@ function stableJson(value: unknown): string {
144
211
  return `${JSON.stringify(value, null, 2)}\n`;
145
212
  }
146
213
 
214
+ export type StarterToolchainSyncChange = {
215
+ relativePath: StarterToolchainSyncPath;
216
+ changed: boolean;
217
+ };
218
+
219
+ export function diffStarterToolchainFiles(
220
+ starterDir: string,
221
+ canonicalFiles: Map<StarterToolchainSyncPath, string>,
222
+ ): StarterToolchainSyncChange[] {
223
+ return STARTER_TOOLCHAIN_SYNC_PATHS.map((relativePath) => {
224
+ const canonicalContent = canonicalFiles.get(relativePath) ?? null;
225
+ const targetPath = path.join(starterDir, relativePath);
226
+ const existingContent = fs.existsSync(targetPath)
227
+ ? fs.readFileSync(targetPath, "utf-8")
228
+ : null;
229
+ return {
230
+ relativePath,
231
+ changed: existingContent !== canonicalContent,
232
+ };
233
+ });
234
+ }
235
+
236
+ export function applyStarterToolchainSync(
237
+ starterDir: string,
238
+ canonicalFiles: Map<StarterToolchainSyncPath, string>,
239
+ ): StarterToolchainSyncChange[] {
240
+ const changes: StarterToolchainSyncChange[] = [];
241
+ for (const relativePath of STARTER_TOOLCHAIN_SYNC_PATHS) {
242
+ const canonicalContent = canonicalFiles.get(relativePath) ?? null;
243
+ const targetPath = path.join(starterDir, relativePath);
244
+ const existingContent = fs.existsSync(targetPath)
245
+ ? fs.readFileSync(targetPath, "utf-8")
246
+ : null;
247
+ const changed = existingContent !== canonicalContent;
248
+ if (changed) {
249
+ if (canonicalContent === null) {
250
+ if (fs.existsSync(targetPath)) {
251
+ fs.unlinkSync(targetPath);
252
+ }
253
+ } else {
254
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
255
+ fs.writeFileSync(targetPath, canonicalContent);
256
+ }
257
+ }
258
+ changes.push({ relativePath, changed });
259
+ }
260
+ return changes;
261
+ }
262
+
147
263
  export type SyncStarterManifestResult = {
148
264
  changed: boolean;
265
+ packageChanged: boolean;
266
+ workspaceChanged: boolean;
149
267
  packageJson: PackageJson;
150
268
  pnpmWorkspaceYaml: string | null;
269
+ toolchainChanges: StarterToolchainSyncChange[];
270
+ changedToolchainPaths: StarterToolchainSyncPath[];
151
271
  };
152
272
 
153
- export function syncStarterManifestFiles(options: {
273
+ export function resolveStarterPaths(options: {
274
+ starterDir?: string;
275
+ starterPackageJsonPath?: string;
276
+ starterPnpmWorkspacePath?: string;
277
+ }): {
278
+ starterDir: string;
154
279
  starterPackageJsonPath: string;
280
+ starterPnpmWorkspacePath: string;
281
+ } {
282
+ if (options.starterDir) {
283
+ return {
284
+ starterDir: path.resolve(options.starterDir),
285
+ starterPackageJsonPath: path.join(
286
+ path.resolve(options.starterDir),
287
+ "package.json",
288
+ ),
289
+ starterPnpmWorkspacePath: path.join(
290
+ path.resolve(options.starterDir),
291
+ "pnpm-workspace.yaml",
292
+ ),
293
+ };
294
+ }
295
+ if (!options.starterPackageJsonPath) {
296
+ throw new Error("Provide --starter-dir or --starter-package-json.");
297
+ }
298
+ const starterPackageJsonPath = path.resolve(options.starterPackageJsonPath);
299
+ return {
300
+ starterDir: path.dirname(starterPackageJsonPath),
301
+ starterPackageJsonPath,
302
+ starterPnpmWorkspacePath:
303
+ options.starterPnpmWorkspacePath ??
304
+ path.join(path.dirname(starterPackageJsonPath), "pnpm-workspace.yaml"),
305
+ };
306
+ }
307
+
308
+ export function syncStarterManifestFiles(options: {
309
+ starterDir?: string;
310
+ starterPackageJsonPath?: string;
155
311
  starterPnpmWorkspacePath?: string;
156
312
  repoRoot?: string;
157
313
  write?: boolean;
158
314
  }): SyncStarterManifestResult {
159
- const { packageJson: canonical, pnpmWorkspaceYaml } =
160
- generateStandaloneChatManifest(options.repoRoot);
315
+ const { starterDir, starterPackageJsonPath, starterPnpmWorkspacePath } =
316
+ resolveStarterPaths(options);
317
+ const snapshot = createStandaloneChatSnapshot(options.repoRoot);
161
318
 
162
- const starterPackageJson = JSON.parse(
163
- fs.readFileSync(options.starterPackageJsonPath, "utf-8"),
164
- ) as PackageJson;
165
- const mergedPackageJson = mergeStarterManifest(starterPackageJson, canonical);
319
+ try {
320
+ const starterPackageJson = JSON.parse(
321
+ fs.readFileSync(starterPackageJsonPath, "utf-8"),
322
+ ) as PackageJson;
323
+ const mergedPackageJson = mergeStarterManifest(
324
+ starterPackageJson,
325
+ snapshot.packageJson,
326
+ );
166
327
 
167
- let workspaceChanged = false;
168
- const existingWorkspace =
169
- options.starterPnpmWorkspacePath &&
170
- fs.existsSync(options.starterPnpmWorkspacePath)
171
- ? fs.readFileSync(options.starterPnpmWorkspacePath, "utf-8")
328
+ const existingWorkspace = fs.existsSync(starterPnpmWorkspacePath)
329
+ ? fs.readFileSync(starterPnpmWorkspacePath, "utf-8")
172
330
  : null;
173
331
 
174
- workspaceChanged = workspaceFileSyncChanged(
175
- existingWorkspace,
176
- pnpmWorkspaceYaml,
177
- );
178
-
179
- const packageChanged =
180
- stableJson(starterPackageJson) !== stableJson(mergedPackageJson);
181
- const changed = packageChanged || workspaceChanged;
182
-
183
- if (options.write && changed) {
184
- if (packageChanged) {
185
- fs.writeFileSync(
186
- options.starterPackageJsonPath,
187
- stableJson(mergedPackageJson),
188
- );
189
- }
190
- if (workspaceChanged && options.starterPnpmWorkspacePath) {
191
- applyWorkspaceFileSync(
192
- options.starterPnpmWorkspacePath,
193
- pnpmWorkspaceYaml,
194
- );
332
+ const workspaceChanged = workspaceFileSyncChanged(
333
+ existingWorkspace,
334
+ snapshot.pnpmWorkspaceYaml,
335
+ );
336
+ const packageChanged =
337
+ stableJson(starterPackageJson) !== stableJson(mergedPackageJson);
338
+ const toolchainChanges = diffStarterToolchainFiles(
339
+ starterDir,
340
+ snapshot.toolchainFiles,
341
+ );
342
+ const changedToolchainPaths = toolchainChanges
343
+ .filter((change) => change.changed)
344
+ .map((change) => change.relativePath);
345
+ const changed =
346
+ packageChanged || workspaceChanged || changedToolchainPaths.length > 0;
347
+
348
+ if (options.write && changed) {
349
+ if (packageChanged) {
350
+ fs.writeFileSync(starterPackageJsonPath, stableJson(mergedPackageJson));
351
+ }
352
+ if (workspaceChanged) {
353
+ applyWorkspaceFileSync(
354
+ starterPnpmWorkspacePath,
355
+ snapshot.pnpmWorkspaceYaml,
356
+ );
357
+ }
358
+ if (changedToolchainPaths.length > 0) {
359
+ applyStarterToolchainSync(starterDir, snapshot.toolchainFiles);
360
+ }
195
361
  }
196
- }
197
362
 
198
- return {
199
- changed,
200
- packageJson: mergedPackageJson,
201
- pnpmWorkspaceYaml,
202
- };
363
+ return {
364
+ changed,
365
+ packageChanged,
366
+ workspaceChanged,
367
+ packageJson: mergedPackageJson,
368
+ pnpmWorkspaceYaml: snapshot.pnpmWorkspaceYaml,
369
+ toolchainChanges,
370
+ changedToolchainPaths,
371
+ };
372
+ } finally {
373
+ snapshot.cleanup();
374
+ }
203
375
  }
204
376
 
205
377
  export function parseSyncStarterManifestArgs(argv: string[]): {
206
- command: "merge" | "generate";
378
+ command: "merge" | "generate" | "paths";
379
+ starterDir?: string;
207
380
  starterPackageJsonPath?: string;
208
381
  starterPnpmWorkspacePath?: string;
209
382
  write: boolean;
210
383
  repoRoot?: string;
211
384
  } {
212
385
  const [commandRaw, ...rest] = argv;
213
- const command = commandRaw === "generate" ? "generate" : "merge";
386
+ const command =
387
+ commandRaw === "generate"
388
+ ? "generate"
389
+ : commandRaw === "paths"
390
+ ? "paths"
391
+ : "merge";
392
+ let starterDir: string | undefined;
214
393
  let starterPackageJsonPath: string | undefined;
215
394
  let starterPnpmWorkspacePath: string | undefined;
216
395
  let write = false;
@@ -222,6 +401,10 @@ export function parseSyncStarterManifestArgs(argv: string[]): {
222
401
  write = true;
223
402
  continue;
224
403
  }
404
+ if (arg === "--starter-dir") {
405
+ starterDir = rest[++i];
406
+ continue;
407
+ }
225
408
  if (arg === "--starter-package-json") {
226
409
  starterPackageJsonPath = rest[++i];
227
410
  continue;
@@ -237,14 +420,15 @@ export function parseSyncStarterManifestArgs(argv: string[]): {
237
420
  throw new Error(`Unknown argument: ${arg}`);
238
421
  }
239
422
 
240
- if (command === "merge" && !starterPackageJsonPath) {
423
+ if (command === "merge" && !starterDir && !starterPackageJsonPath) {
241
424
  throw new Error(
242
- "merge requires --starter-package-json <path> [--starter-pnpm-workspace <path>] [--write] [--repo-root <path>]",
425
+ "merge requires --starter-dir <path> or --starter-package-json <path> [--starter-pnpm-workspace <path>] [--write] [--repo-root <path>]",
243
426
  );
244
427
  }
245
428
 
246
429
  return {
247
430
  command,
431
+ starterDir,
248
432
  starterPackageJsonPath,
249
433
  starterPnpmWorkspacePath,
250
434
  write,
@@ -267,25 +451,37 @@ export function runSyncStarterManifestCli(argv: string[]): number {
267
451
  return 0;
268
452
  }
269
453
 
454
+ if (args.command === "paths") {
455
+ for (const syncPath of listStarterSyncPaths()) {
456
+ process.stdout.write(`${syncPath}\n`);
457
+ }
458
+ return 0;
459
+ }
460
+
270
461
  const result = syncStarterManifestFiles({
271
- starterPackageJsonPath: args.starterPackageJsonPath!,
462
+ starterDir: args.starterDir,
463
+ starterPackageJsonPath: args.starterPackageJsonPath,
272
464
  starterPnpmWorkspacePath: args.starterPnpmWorkspacePath,
273
465
  repoRoot: args.repoRoot,
274
466
  write: args.write,
275
467
  });
276
468
 
277
469
  if (result.changed) {
470
+ const updatedPaths = [
471
+ ...(result.packageChanged ? ["package.json"] : []),
472
+ ...(result.workspaceChanged ? ["pnpm-workspace.yaml"] : []),
473
+ ...result.changedToolchainPaths,
474
+ ];
475
+ const summary = updatedPaths.length ? ` (${updatedPaths.join(", ")})` : "";
278
476
  console.log(
279
477
  args.write
280
- ? "Updated builder-agent-native-starter manifest from templates/chat."
281
- : "builder-agent-native-starter manifest is out of date with templates/chat.",
478
+ ? `Updated builder-agent-native-starter from templates/chat${summary}.`
479
+ : `builder-agent-native-starter is out of date with templates/chat${summary}.`,
282
480
  );
283
481
  return args.write ? 0 : 1;
284
482
  }
285
483
 
286
- console.log(
287
- "builder-agent-native-starter manifest already matches templates/chat.",
288
- );
484
+ console.log("builder-agent-native-starter already matches templates/chat.");
289
485
  return 0;
290
486
  }
291
487
 
@@ -349,7 +349,7 @@ export default defineAction({
349
349
 
350
350
  if (!hasPlayableMp4Metadata(uploadData)) {
351
351
  const err = new Error(
352
- "Recorded MP4 is missing playback metadata. Please retry the recording.",
352
+ "Recorded MP4 is corrupted or incomplete and cannot be recovered. Please record again.",
353
353
  );
354
354
  try {
355
355
  captureRouteError(err, {
@@ -103,6 +103,7 @@ interface PendingNativeUpload {
103
103
  lastAttemptAt?: string | null;
104
104
  lastError?: string | null;
105
105
  retryCount: number;
106
+ corrupt?: boolean;
106
107
  }
107
108
 
108
109
  type PendingDesktopUpload = PendingNativeUpload | PendingBrowserRecordingUpload;
@@ -2474,25 +2475,40 @@ function PendingUploadBanner({
2474
2475
  <IconDownload size={14} stroke={2} />
2475
2476
  </button>
2476
2477
  ) : null}
2477
- <button
2478
- type="button"
2479
- className="pending-upload-retry"
2480
- disabled={actionsDisabled}
2481
- onClick={() => onRetry(latest)}
2482
- >
2483
- <IconRefresh size={14} stroke={2} />
2484
- {retrying ? "Retrying" : "Retry"}
2485
- </button>
2486
- <button
2487
- type="button"
2488
- className="pending-upload-discard"
2489
- disabled={actionsDisabled}
2490
- onClick={() => onDiscard(latest)}
2491
- aria-label="Discard saved local clip"
2492
- title="Discard saved local clip"
2493
- >
2494
- <IconTrash size={14} stroke={2} />
2495
- </button>
2478
+ {latest.kind === "native" && latest.corrupt ? (
2479
+ <button
2480
+ type="button"
2481
+ className="pending-upload-discard"
2482
+ disabled={actionsDisabled}
2483
+ onClick={() => onDiscard(latest)}
2484
+ aria-label="Discard corrupted clip"
2485
+ title="This clip is corrupted and cannot be recovered. Discard it and record again."
2486
+ >
2487
+ <IconTrash size={14} stroke={2} />
2488
+ </button>
2489
+ ) : (
2490
+ <>
2491
+ <button
2492
+ type="button"
2493
+ className="pending-upload-retry"
2494
+ disabled={actionsDisabled}
2495
+ onClick={() => onRetry(latest)}
2496
+ >
2497
+ <IconRefresh size={14} stroke={2} />
2498
+ {retrying ? "Retrying" : "Retry"}
2499
+ </button>
2500
+ <button
2501
+ type="button"
2502
+ className="pending-upload-discard"
2503
+ disabled={actionsDisabled}
2504
+ onClick={() => onDiscard(latest)}
2505
+ aria-label="Discard saved local clip"
2506
+ title="Discard saved local clip"
2507
+ >
2508
+ <IconTrash size={14} stroke={2} />
2509
+ </button>
2510
+ </>
2511
+ )}
2496
2512
  </div>
2497
2513
  </div>
2498
2514
  );