@agent-native/core 0.77.10 → 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.
Files changed (60) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/cli/create.ts +65 -15
  5. package/corpus/core/src/cli/sync-builder-starter-manifest.ts +261 -65
  6. package/corpus/core/src/collab/awareness.ts +12 -9
  7. package/corpus/core/src/collab/client.ts +4 -2
  8. package/corpus/templates/clips/actions/finalize-recording.ts +1 -1
  9. package/corpus/templates/clips/chrome-extension/package.json +1 -1
  10. package/corpus/templates/clips/desktop/src/app.tsx +35 -19
  11. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +38 -1
  12. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +343 -14
  14. package/corpus/templates/content/actions/list-trashed-content-databases.ts +36 -2
  15. package/corpus/templates/content/actions/move-document.ts +79 -33
  16. package/corpus/templates/content/actions/restore-content-database.ts +40 -3
  17. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +84 -2
  18. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +177 -49
  19. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +393 -219
  20. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +632 -467
  21. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +114 -14
  22. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +56 -15
  23. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +6 -5
  24. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +12 -4
  25. package/corpus/templates/content/app/global.css +9 -0
  26. package/corpus/templates/content/app/hooks/use-create-page.ts +36 -7
  27. package/corpus/templates/content/app/i18n-data.ts +140 -10
  28. package/corpus/templates/content/changelog/2026-06-25-comment-cards-now-track-their-highlighted-text-while-scrolli.md +6 -0
  29. package/corpus/templates/content/changelog/2026-06-25-comments-now-scroll-with-the-document-and-stay-below-the-pag.md +6 -0
  30. package/corpus/templates/content/changelog/2026-06-25-creating-a-database-from-the-slash-menu-no-longer-leaves-sta.md +6 -0
  31. package/corpus/templates/content/changelog/2026-06-25-sidebar-hover-controls-no-longer-leave-a-lingering-fade-when.md +6 -0
  32. package/corpus/templates/content/changelog/2026-06-25-sidebar-page-actions-are-now-easier-to-see-when-hovering-ina.md +6 -0
  33. package/corpus/templates/content/changelog/2026-06-25-text-selections-in-the-editor-no-longer-show-white-gaps-when.md +6 -0
  34. package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-has-a-one-click-page-link-copy-button.md +6 -0
  35. package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-shows-page-breadcrumbs-and-the-latest.md +6 -0
  36. package/corpus/templates/content/changelog/2026-06-25-the-page-command-now-leaves-a-notion-style-page-reference-an.md +6 -0
  37. package/corpus/templates/content/shared/nfm.ts +16 -0
  38. package/dist/cli/create.d.ts.map +1 -1
  39. package/dist/cli/create.js +53 -15
  40. package/dist/cli/create.js.map +1 -1
  41. package/dist/cli/sync-builder-starter-manifest.d.ts +36 -2
  42. package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -1
  43. package/dist/cli/sync-builder-starter-manifest.js +180 -37
  44. package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
  45. package/dist/collab/awareness.d.ts +1 -1
  46. package/dist/collab/awareness.d.ts.map +1 -1
  47. package/dist/collab/awareness.js +12 -8
  48. package/dist/collab/awareness.js.map +1 -1
  49. package/dist/collab/client.d.ts.map +1 -1
  50. package/dist/collab/client.js +4 -3
  51. package/dist/collab/client.js.map +1 -1
  52. package/dist/collab/routes.d.ts +1 -1
  53. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  54. package/dist/notifications/routes.d.ts +2 -2
  55. package/dist/observability/routes.d.ts +5 -5
  56. package/dist/progress/routes.d.ts +1 -1
  57. package/dist/resources/handlers.d.ts +3 -3
  58. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  59. package/dist/server/transcribe-voice.d.ts +1 -1
  60. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 1933
31
- - template files: 4293
31
+ - template files: 4302
@@ -1,5 +1,17 @@
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
+
9
+ ## 0.77.11
10
+
11
+ ### Patch Changes
12
+
13
+ - 7560a7c: Clear collaborative awareness state immediately when an editor leaves a document.
14
+
3
15
  ## 0.77.10
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.77.10",
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": {
@@ -211,8 +211,8 @@
211
211
  "@react-router/dev": "^8.0.1",
212
212
  "@react-router/fs-routes": "^8.0.1",
213
213
  "@resvg/resvg-js": "^2.6.2",
214
- "@sentry/browser": "^10.61.0",
215
- "@sentry/node": "^10.61.0",
214
+ "@sentry/browser": "10.60.0",
215
+ "@sentry/node": "10.60.0",
216
216
  "@standard-schema/spec": "^1.1.0",
217
217
  "@tanstack/react-table": "^8.21.3",
218
218
  "@tiptap/core": "3.27.1",
@@ -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
 
@@ -102,7 +102,7 @@ function pruneIfEmpty(docId: string, map: Map<number, AwarenessEntry>): void {
102
102
  *
103
103
  * Client sends its awareness state and receives other clients' states.
104
104
  *
105
- * Body: { clientId: number, state: string (base64) }
105
+ * Body: { clientId: number, state: string | null (JSON-encoded awareness state, or null to clear) }
106
106
  * Response: { states: Array<{ clientId: number, state: string }> }
107
107
  */
108
108
  export const postAwareness = defineEventHandler(async (event: H3Event) => {
@@ -115,28 +115,31 @@ export const postAwareness = defineEventHandler(async (event: H3Event) => {
115
115
  const body = await readBody(event);
116
116
  const { clientId, state } = body as {
117
117
  clientId?: number;
118
- state?: string;
118
+ state?: string | null;
119
119
  };
120
120
 
121
- if (clientId == null || !state) {
121
+ if (clientId == null || state === undefined) {
122
122
  // `!clientId` would wrongly reject clientId === 0, which is a valid
123
- // (if rare) Yjs client id. Only reject missing/null ids here.
123
+ // (if rare) Yjs client id. A null state is valid: it clears this client.
124
124
  setResponseStatus(event, 400);
125
125
  return { error: "clientId and state required" };
126
126
  }
127
127
 
128
128
  const map = getDocAwareness(docId);
129
129
 
130
- // Store this client's state
131
- map.set(clientId, { clientId, state, lastSeen: Date.now() });
130
+ if (state === null) {
131
+ map.delete(clientId);
132
+ } else {
133
+ // Store this client's state
134
+ map.set(clientId, { clientId, state, lastSeen: Date.now() });
135
+ }
132
136
 
133
137
  // Clean expired entries, then prune the outer-map entry if it becomes empty.
134
138
  // Without pruning, a deployment with many transient docIds (e.g. one per
135
139
  // session) would grow _awarenessMap without bound.
136
140
  cleanExpired(map);
137
- // map has at least the sender's entry so size >= 1 here; pruneIfEmpty is a
138
- // no-op in the normal path but guards against edge cases (e.g. clientId 0
139
- // that was immediately evicted by a concurrent cleanExpired run).
141
+ // Null-state clears and expiry can empty the map; prune the outer entry so
142
+ // transient document ids do not accumulate.
140
143
  pruneIfEmpty(docId, map);
141
144
 
142
145
  // Build the full list of current states (all clients including sender).
@@ -276,11 +276,13 @@ function scheduleAwarenessPush(
276
276
  const timer = setTimeout(() => {
277
277
  _awarenessThrottleTimers.delete(key);
278
278
  const state = getState();
279
- if (!state) return;
280
279
  fetch(`${baseUrl}/${docId}/awareness`, {
281
280
  method: "POST",
282
281
  headers: { "Content-Type": "application/json" },
283
- body: JSON.stringify({ clientId, state: JSON.stringify(state) }),
282
+ body: JSON.stringify({
283
+ clientId,
284
+ state: state ? JSON.stringify(state) : null,
285
+ }),
284
286
  }).catch(() => {}); // best-effort; poll cycle is the baseline fallback
285
287
  }, 150);
286
288
 
@@ -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, {
@@ -11,7 +11,7 @@
11
11
  "package": "pnpm build && tsx scripts/package.ts"
12
12
  },
13
13
  "dependencies": {
14
- "@sentry/browser": "^10.61.0"
14
+ "@sentry/browser": "10.60.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/chrome": "^0.2.0",