@agent-native/core 0.164.11 → 0.164.15

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 (106) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +84 -0
  3. package/corpus/templates/calendar/AGENTS.md +5 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +1 -1
  5. package/corpus/templates/calendar/actions/delete-events.ts +508 -0
  6. package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
  7. package/corpus/templates/calendar/actions/list-events.ts +53 -0
  8. package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
  9. package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
  10. package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
  11. package/corpus/templates/content/actions/_database-utils.ts +2 -0
  12. package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
  13. package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
  14. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
  15. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
  16. package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
  17. package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
  18. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
  19. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
  20. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +78 -1
  21. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
  22. package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
  23. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
  24. package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
  25. package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
  26. package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
  27. package/corpus/templates/content/app/i18n-data.ts +79 -0
  28. package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
  29. package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
  30. package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
  31. package/corpus/templates/content/app/root.tsx +2 -1
  32. package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
  33. package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
  34. package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
  35. package/corpus/templates/content/docs/product/roadmap.md +2 -2
  36. package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
  37. package/corpus/templates/content/shared/api.ts +25 -0
  38. package/corpus/templates/factory/AGENTS.md +2 -1
  39. package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
  40. package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
  41. package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
  43. package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
  44. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
  45. package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
  46. package/corpus/templates/factory/app/i18n/en-US.ts +43 -0
  47. package/corpus/templates/factory/app/routes/factory.tsx +115 -15
  48. package/corpus/templates/factory/server/plugins/agent-chat.ts +3 -1
  49. package/dist/app-config/auth.d.ts +4 -0
  50. package/dist/app-config/auth.js +7 -0
  51. package/dist/app-config/schema.d.ts +3 -0
  52. package/dist/app-config/schema.js +2 -0
  53. package/dist/cli/code-agent-executor.js +6 -1
  54. package/dist/cli/code-agent-mcp-config.d.ts +2 -2
  55. package/dist/cli/code-agent-mcp-config.js +34 -9
  56. package/dist/client/org/OrgSwitcher.js +8 -4
  57. package/dist/collab/awareness.d.ts +2 -2
  58. package/dist/collab/routes.d.ts +1 -1
  59. package/dist/collab/struct-routes.d.ts +1 -1
  60. package/dist/mcp-client/config.d.ts +11 -0
  61. package/dist/mcp-client/config.js +33 -26
  62. package/dist/notifications/routes.d.ts +3 -3
  63. package/dist/observability/routes.d.ts +3 -3
  64. package/dist/progress/routes.d.ts +1 -1
  65. package/dist/resources/handlers.d.ts +2 -2
  66. package/dist/secrets/routes.d.ts +9 -9
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/auth.js +15 -1
  69. package/dist/server/realtime-token.d.ts +1 -1
  70. package/docs/content/actions-access-control.mdx +136 -2
  71. package/docs/content/actions-agent-tools.mdx +101 -0
  72. package/docs/content/actions-defining.mdx +2 -2
  73. package/docs/content/audit-log.mdx +1 -1
  74. package/docs/content/authentication.mdx +1 -1
  75. package/docs/content/cloudflare.mdx +43 -0
  76. package/docs/content/creating-templates.mdx +1 -1
  77. package/docs/content/deploy-an-app.mdx +109 -0
  78. package/docs/content/deployment-environment-variables.mdx +101 -0
  79. package/docs/content/deployment.mdx +125 -792
  80. package/docs/content/docs-components.mdx +2 -2
  81. package/docs/content/environment-variables.mdx +41 -41
  82. package/docs/content/file-uploads.mdx +74 -40
  83. package/docs/content/harness-agents.mdx +2 -2
  84. package/docs/content/internationalization.mdx +61 -65
  85. package/docs/content/key-concepts.mdx +3 -3
  86. package/docs/content/multi-app-workspace.mdx +2 -2
  87. package/docs/content/netlify.mdx +47 -0
  88. package/docs/content/node-docker.mdx +138 -0
  89. package/docs/content/organizations-teams-permissions.mdx +5 -5
  90. package/docs/content/other-platforms.mdx +27 -0
  91. package/docs/content/ssr-caching.mdx +45 -0
  92. package/docs/content/template-assets-developers.mdx +2 -2
  93. package/docs/content/template-chat-developers.mdx +2 -2
  94. package/docs/content/template-content-developers.mdx +2 -2
  95. package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
  96. package/docs/content/template-content-sync.mdx +2 -2
  97. package/docs/content/toolkit-agent-ux.mdx +1 -1
  98. package/docs/content/toolkit-resources.mdx +2 -2
  99. package/docs/content/updating-ui-in-production.mdx +35 -0
  100. package/docs/content/vercel.mdx +38 -0
  101. package/docs/content/what-is-agent-native.mdx +1 -1
  102. package/docs/content/workspace-deployment.mdx +350 -0
  103. package/package.json +1 -1
  104. package/docs/content/agents.mdx +0 -9
  105. package/docs/content/database.mdx +0 -317
  106. package/docs/content/human-approval.mdx +0 -156
@@ -6,6 +6,11 @@ import {
6
6
  } from "@shared/content-source";
7
7
 
8
8
  import { getDesktopContentFiles } from "./desktop-content-files";
9
+ import type {
10
+ DesktopContentFileRevision,
11
+ DesktopContentFilesChange,
12
+ DesktopContentFilesFolder,
13
+ } from "./desktop-content-files";
9
14
 
10
15
  type PermissionState = "granted" | "denied" | "prompt";
11
16
  type LocalWritable = {
@@ -57,8 +62,18 @@ export type LocalSourceFileResult =
57
62
  path: string;
58
63
  absolutePath?: string;
59
64
  runtime: "browser" | "desktop" | "server-local";
65
+ revision?: DesktopContentFileRevision;
60
66
  }
61
- | { ok: false; error: string; unavailable?: boolean };
67
+ | {
68
+ ok: false;
69
+ error: string;
70
+ unavailable?: boolean;
71
+ conflict?: {
72
+ path: string;
73
+ expectedRevision?: string | null;
74
+ actualRevision?: DesktopContentFileRevision;
75
+ };
76
+ };
62
77
 
63
78
  export type LocalSourceDocumentReadResult =
64
79
  | {
@@ -68,9 +83,23 @@ export type LocalSourceDocumentReadResult =
68
83
  document: Document;
69
84
  updatedAt: string;
70
85
  runtime: "browser" | "desktop";
86
+ revision?: DesktopContentFileRevision;
71
87
  }
72
88
  | { ok: false; error: string; unavailable?: boolean };
73
89
 
90
+ export type LinkedLocalSourceChange = DesktopContentFilesChange & {
91
+ sourcePath: string;
92
+ };
93
+
94
+ export type LinkedLocalSourceWatchResult =
95
+ | { ok: true; unsubscribe(): void }
96
+ | { ok: false; error: string; unavailable?: boolean };
97
+
98
+ export type LinkedLocalSourceWriteOptions = {
99
+ /** Opaque revision observed when the editor loaded the physical file. */
100
+ expectedRevision?: string;
101
+ };
102
+
74
103
  const LOCAL_FILES_DB_NAME = "content-local-files";
75
104
  const LOCAL_FILES_DB_VERSION = 1;
76
105
  const LOCAL_FILES_STORE_NAME = "handles";
@@ -211,7 +240,10 @@ async function resolveBrowserSourceForPath(filePath: string) {
211
240
  };
212
241
  }
213
242
 
214
- async function resolveDesktopSourceForPath(filePath: string) {
243
+ async function resolveDesktopSourceForPath(
244
+ filePath: string,
245
+ source?: DocumentSourceInfo,
246
+ ) {
215
247
  const desktopFiles = getDesktopContentFiles();
216
248
  if (!desktopFiles) return null;
217
249
  const result = await desktopFiles.getFolder();
@@ -220,7 +252,31 @@ async function resolveDesktopSourceForPath(filePath: string) {
220
252
  result.folders && result.folders.length > 0
221
253
  ? result.folders
222
254
  : [result.folder];
255
+ const sourceFolder = source?.rootPath
256
+ ? folders.find(
257
+ (folder) =>
258
+ folder.id === source.rootPath ||
259
+ folder.sourcePrefix === source.rootPath ||
260
+ folder.name === source.rootPath,
261
+ )
262
+ : undefined;
263
+ if (sourceFolder) {
264
+ return { api: desktopFiles, folder: sourceFolder, path: filePath };
265
+ }
223
266
  const resolved = resolveSourcePathForFolders(filePath, folders);
267
+ if (!resolved && folders.length > 1) {
268
+ const matches: DesktopContentFilesFolder[] = [];
269
+ for (const folder of folders) {
270
+ if (!folder.id) continue;
271
+ const read = await desktopFiles.readFiles({ folderId: folder.id });
272
+ if (read.ok && read.sources?.[filePath] !== undefined) {
273
+ matches.push(folder);
274
+ }
275
+ }
276
+ if (matches.length === 1) {
277
+ return { api: desktopFiles, folder: matches[0]!, path: filePath };
278
+ }
279
+ }
224
280
  if (!resolved) {
225
281
  throw new Error(`Local source folder for "${filePath}" was not found.`);
226
282
  }
@@ -295,23 +351,24 @@ async function readBrowserFile(root: LocalDirectoryHandle, filePath: string) {
295
351
  };
296
352
  }
297
353
 
298
- function joinDesktopAbsolutePath(folderPath: string, filePath: string) {
299
- const folder = folderPath.replace(/[\\/]+$/, "");
300
- const separator = folder.includes("\\") ? "\\" : "/";
301
- const folderParts = folder.split(/[\\/]/).filter(Boolean);
302
- const folderName = folderParts[folderParts.length - 1];
303
- let relativePath = filePath;
304
- if (
305
- folderName === CONTENT_SOURCE_ROOT &&
306
- relativePath.startsWith(`${CONTENT_SOURCE_ROOT}/`)
307
- ) {
308
- relativePath = relativePath.slice(CONTENT_SOURCE_ROOT.length + 1);
309
- }
310
- return `${folder}${separator}${relativePath.replace(/\//g, separator)}`;
311
- }
312
-
313
- function sourceFileContent(document: Document) {
314
- return serializeContentSourceDocument({
354
+ const MANAGED_SOURCE_FRONTMATTER_KEYS = new Set([
355
+ "id",
356
+ "title",
357
+ "description",
358
+ "parentId",
359
+ "icon",
360
+ "position",
361
+ "isFavorite",
362
+ "hideFromSearch",
363
+ "visibility",
364
+ "updatedAt",
365
+ ]);
366
+
367
+ const SOURCE_FRONTMATTER_RE =
368
+ /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n\r?\n|\r?\n|$)/;
369
+
370
+ export function sourceFileContent(document: Document, existingSource?: string) {
371
+ const serialized = serializeContentSourceDocument({
315
372
  id: document.id,
316
373
  parentId: document.parentId,
317
374
  title: document.title,
@@ -323,6 +380,19 @@ function sourceFileContent(document: Document) {
323
380
  visibility: document.visibility,
324
381
  updatedAt: document.updatedAt,
325
382
  });
383
+ const existingFrontmatter = existingSource?.match(SOURCE_FRONTMATTER_RE)?.[1];
384
+ if (!existingFrontmatter) return serialized;
385
+
386
+ const preservedLines = existingFrontmatter.split(/\r?\n/).filter((line) => {
387
+ const key = line.match(/^([A-Za-z][A-Za-z0-9_-]*):/)?.[1];
388
+ return !key || !MANAGED_SOURCE_FRONTMATTER_KEYS.has(key);
389
+ });
390
+ if (preservedLines.length === 0) return serialized;
391
+
392
+ return serialized.replace(
393
+ "\n---\n\n",
394
+ `\n${preservedLines.join("\n")}\n---\n\n`,
395
+ );
326
396
  }
327
397
 
328
398
  function documentFromSourceContent(input: {
@@ -378,6 +448,7 @@ export function canWriteLinkedLocalSource(
378
448
  export async function writeDocumentToLinkedLocalSource(
379
449
  document: Document,
380
450
  source: DocumentSourceInfo | undefined = document.source,
451
+ options: LinkedLocalSourceWriteOptions = {},
381
452
  ): Promise<LocalSourceFileResult> {
382
453
  const filePath = normalizeSourcePath(source?.path);
383
454
  if (!filePath) {
@@ -395,22 +466,56 @@ export async function writeDocumentToLinkedLocalSource(
395
466
  };
396
467
  }
397
468
 
398
- const content = sourceFileContent(document);
399
- const desktopSource = await resolveDesktopSourceForPath(filePath);
469
+ const desktopSource = await resolveDesktopSourceForPath(filePath, source);
400
470
  if (desktopSource) {
471
+ const current = await desktopSource.api.readFiles({
472
+ folderId: desktopSource.folder.id,
473
+ });
474
+ if (!current.ok) {
475
+ return { ok: false, error: current.error };
476
+ }
477
+ const existingSource = current.sources?.[desktopSource.path];
478
+ if (existingSource === undefined) {
479
+ return {
480
+ ok: false,
481
+ error: `Local file "${filePath}" was not found.`,
482
+ };
483
+ }
484
+ const content = sourceFileContent(document, existingSource);
485
+ const expectedRevision =
486
+ options.expectedRevision ?? current.revisions?.[desktopSource.path];
487
+ if (!expectedRevision) {
488
+ return {
489
+ ok: false,
490
+ error: `Local file "${filePath}" has no observed revision.`,
491
+ };
492
+ }
401
493
  const result = await desktopSource.api.writeFile({
402
494
  folderId: desktopSource.folder.id,
403
495
  path: desktopSource.path,
404
496
  content,
497
+ expectedRevision,
405
498
  });
406
- if (!result.ok) return { ok: false, error: result.error };
499
+ if (!result.ok) {
500
+ return {
501
+ ok: false,
502
+ error: result.error,
503
+ unavailable: result.code === "unavailable",
504
+ conflict:
505
+ result.code === "conflict" && result.conflict
506
+ ? {
507
+ path: filePath,
508
+ expectedRevision: result.conflict.expectedRevision,
509
+ actualRevision: result.conflict.actualRevision,
510
+ }
511
+ : undefined,
512
+ };
513
+ }
407
514
  return {
408
515
  ok: true,
409
516
  path: filePath,
410
- absolutePath: result.folder.path
411
- ? joinDesktopAbsolutePath(result.folder.path, desktopSource.path)
412
- : undefined,
413
517
  runtime: "desktop",
518
+ revision: result.revisions?.[desktopSource.path],
414
519
  };
415
520
  }
416
521
 
@@ -429,10 +534,80 @@ export async function writeDocumentToLinkedLocalSource(
429
534
  error: "Write permission was not granted for the source folder.",
430
535
  };
431
536
  }
537
+ const existingSource = await readBrowserFile(
538
+ browserSource.handle,
539
+ browserSource.path,
540
+ );
541
+ const content = sourceFileContent(document, existingSource.content);
432
542
  await writeBrowserFile(browserSource.handle, browserSource.path, content);
433
543
  return { ok: true, path: filePath, runtime: "browser" };
434
544
  }
435
545
 
546
+ /**
547
+ * Subscribe only through an already-authorized Desktop grant. Browser folder
548
+ * handles intentionally have no ambient watcher capability.
549
+ */
550
+ export async function watchLinkedLocalSource(
551
+ source: DocumentSourceInfo | undefined,
552
+ onChange: (change: LinkedLocalSourceChange) => void,
553
+ ): Promise<LinkedLocalSourceWatchResult> {
554
+ const filePath = normalizeSourcePath(source?.path);
555
+ if (!filePath) {
556
+ return {
557
+ ok: false,
558
+ error: "This document is not linked to a source file.",
559
+ };
560
+ }
561
+ const desktopSource = await resolveDesktopSourceForPath(filePath, source);
562
+ if (desktopSource?.api.watchFiles) {
563
+ return desktopSource.api.watchFiles(
564
+ { folderId: desktopSource.folder.id },
565
+ (change) => {
566
+ const legacyChange = change as DesktopContentFilesChange & {
567
+ path?: string;
568
+ previousPath?: string;
569
+ };
570
+ if (
571
+ legacyChange.path !== desktopSource.path &&
572
+ legacyChange.previousPath !== desktopSource.path
573
+ ) {
574
+ return;
575
+ }
576
+ onChange({ ...change, sourcePath: filePath });
577
+ },
578
+ );
579
+ }
580
+ if (
581
+ !desktopSource?.api.subscribeChanges ||
582
+ !desktopSource.api.unsubscribeChanges ||
583
+ !desktopSource.api.onChange
584
+ ) {
585
+ return {
586
+ ok: false,
587
+ unavailable: true,
588
+ error:
589
+ "Live local file updates require a current Agent Native Desktop bridge.",
590
+ };
591
+ }
592
+ const subscribed = await desktopSource.api.subscribeChanges({
593
+ folderId: desktopSource.folder.id,
594
+ });
595
+ if (!subscribed.ok) return { ok: false, error: subscribed.error };
596
+ const removeListener = desktopSource.api.onChange((change) => {
597
+ if (change.folderId !== desktopSource.folder.id) return;
598
+ onChange({ ...change, sourcePath: filePath });
599
+ });
600
+ return {
601
+ ok: true,
602
+ unsubscribe: () => {
603
+ removeListener();
604
+ void desktopSource.api.unsubscribeChanges?.({
605
+ folderId: desktopSource.folder.id,
606
+ });
607
+ },
608
+ };
609
+ }
610
+
436
611
  export async function readDocumentFromLinkedLocalSource(
437
612
  document: Document,
438
613
  source: DocumentSourceInfo | undefined = document.source,
@@ -453,7 +628,7 @@ export async function readDocumentFromLinkedLocalSource(
453
628
  };
454
629
  }
455
630
 
456
- const desktopSource = await resolveDesktopSourceForPath(filePath);
631
+ const desktopSource = await resolveDesktopSourceForPath(filePath, source);
457
632
  if (desktopSource) {
458
633
  const result = await desktopSource.api.readFiles({
459
634
  folderId: desktopSource.folder.id,
@@ -471,7 +646,13 @@ export async function readDocumentFromLinkedLocalSource(
471
646
  content,
472
647
  updatedAt,
473
648
  });
474
- return read.ok ? { ...read, runtime: "desktop" } : read;
649
+ return read.ok
650
+ ? {
651
+ ...read,
652
+ runtime: "desktop",
653
+ revision: result.revisions?.[desktopSource.path],
654
+ }
655
+ : read;
475
656
  }
476
657
 
477
658
  const browserSource = await resolveBrowserSourceForPath(filePath);
@@ -521,9 +702,7 @@ export async function localSourceAbsolutePath(
521
702
  if (!filePath) return source?.absolutePath ?? null;
522
703
  if (source?.absolutePath) return source.absolutePath;
523
704
 
524
- const desktopSource = await resolveDesktopSourceForPath(filePath);
525
- if (!desktopSource?.folder.path) return null;
526
- return joinDesktopAbsolutePath(desktopSource.folder.path, desktopSource.path);
705
+ return null;
527
706
  }
528
707
 
529
708
  export async function revealLinkedLocalSourceFile(
@@ -537,7 +716,7 @@ export async function revealLinkedLocalSourceFile(
537
716
  };
538
717
  }
539
718
 
540
- const desktopSource = await resolveDesktopSourceForPath(filePath);
719
+ const desktopSource = await resolveDesktopSourceForPath(filePath, source);
541
720
  if (!desktopSource) {
542
721
  return {
543
722
  ok: false,
@@ -554,9 +733,6 @@ export async function revealLinkedLocalSourceFile(
554
733
  return {
555
734
  ok: true,
556
735
  path: filePath,
557
- absolutePath: result.folder.path
558
- ? joinDesktopAbsolutePath(result.folder.path, desktopSource.path)
559
- : undefined,
560
736
  runtime: "desktop",
561
737
  };
562
738
  }
@@ -0,0 +1,191 @@
1
+ import { callAction } from "@agent-native/core/client/hooks";
2
+
3
+ import type { DesktopContentFilesFolder } from "./desktop-content-files";
4
+ import { getDesktopContentFiles } from "./desktop-content-files";
5
+
6
+ const REGISTRY_KEY = "content-local-folder-live-sources-v1";
7
+ const ACTIVATION_EVENT = "content-local-folder-working-copy-activation";
8
+
9
+ let requestedWorkingCopyId: string | null = null;
10
+
11
+ interface LiveLocalFolderSource {
12
+ folderId: string;
13
+ sourceId: string;
14
+ databaseId?: string;
15
+ repositoryId?: string;
16
+ }
17
+
18
+ function readRegistry(): LiveLocalFolderSource[] {
19
+ if (typeof window === "undefined") return [];
20
+ try {
21
+ const value = JSON.parse(window.localStorage.getItem(REGISTRY_KEY) ?? "[]");
22
+ if (!Array.isArray(value)) return [];
23
+ return value.filter(
24
+ (entry): entry is LiveLocalFolderSource =>
25
+ typeof entry?.folderId === "string" &&
26
+ typeof entry?.sourceId === "string" &&
27
+ (entry.databaseId === undefined ||
28
+ typeof entry.databaseId === "string") &&
29
+ (entry.repositoryId === undefined ||
30
+ typeof entry.repositoryId === "string"),
31
+ );
32
+ } catch {
33
+ // coercion-ok: corrupt device-local cache is treated as an absent optional registry
34
+ return [];
35
+ }
36
+ }
37
+
38
+ export async function rememberLiveLocalFolderSource(
39
+ folder: DesktopContentFilesFolder,
40
+ sourceId: string,
41
+ databaseId?: string | null,
42
+ ) {
43
+ if (!folder.id || typeof window === "undefined") return;
44
+ const next = [
45
+ ...readRegistry().filter((entry) => entry.folderId !== folder.id),
46
+ {
47
+ folderId: folder.id,
48
+ sourceId,
49
+ ...(databaseId ? { databaseId } : {}),
50
+ ...(folder.repository?.localId
51
+ ? { repositoryId: folder.repository.localId }
52
+ : {}),
53
+ },
54
+ ];
55
+ window.localStorage.setItem(REGISTRY_KEY, JSON.stringify(next));
56
+ const persisted = await getDesktopContentFiles()?.associateSource?.({
57
+ folderId: folder.id,
58
+ sourceId,
59
+ ...(databaseId ? { databaseId } : {}),
60
+ });
61
+ if (persisted && !persisted.ok) throw new Error(persisted.error);
62
+ }
63
+
64
+ export function forgetLiveLocalFolderSource(folderId: string) {
65
+ if (typeof window === "undefined") return;
66
+ const next = readRegistry().filter((entry) => entry.folderId !== folderId);
67
+ window.localStorage.setItem(REGISTRY_KEY, JSON.stringify(next));
68
+ }
69
+
70
+ export function liveLocalFolderSourceId(folderId: string) {
71
+ return readRegistry().find((entry) => entry.folderId === folderId)?.sourceId;
72
+ }
73
+
74
+ export function requestLiveLocalFolderActivation(folderId: string) {
75
+ requestedWorkingCopyId = folderId;
76
+ if (typeof window !== "undefined") {
77
+ window.dispatchEvent(
78
+ new CustomEvent(ACTIVATION_EVENT, { detail: folderId }),
79
+ );
80
+ }
81
+ }
82
+
83
+ export function pendingLiveLocalFolderActivation() {
84
+ return requestedWorkingCopyId;
85
+ }
86
+
87
+ export function consumeLiveLocalFolderActivation(folderId: string) {
88
+ if (requestedWorkingCopyId === folderId) requestedWorkingCopyId = null;
89
+ }
90
+
91
+ export function subscribeLiveLocalFolderActivation(
92
+ callback: (folderId: string) => void,
93
+ ) {
94
+ if (typeof window === "undefined") return () => {};
95
+ const listener = (event: Event) => {
96
+ const folderId = (event as CustomEvent<unknown>).detail;
97
+ if (typeof folderId === "string") callback(folderId);
98
+ };
99
+ window.addEventListener(ACTIVATION_EVENT, listener);
100
+ return () => window.removeEventListener(ACTIVATION_EVENT, listener);
101
+ }
102
+
103
+ export async function connectTemporaryLocalFolder(
104
+ folder: DesktopContentFilesFolder,
105
+ ) {
106
+ if (!folder.id || folder.kind !== "temporary") return null;
107
+ const existing = readRegistry().find((entry) => entry.folderId === folder.id);
108
+ if (
109
+ existing?.repositoryId &&
110
+ existing.repositoryId === folder.repository?.localId
111
+ ) {
112
+ return existing.sourceId;
113
+ }
114
+ if (existing) forgetLiveLocalFolderSource(folder.id);
115
+ const persistent = readRegistry().find(
116
+ (entry) =>
117
+ entry.databaseId &&
118
+ entry.repositoryId &&
119
+ entry.repositoryId === folder.repository?.localId,
120
+ );
121
+ if (!persistent?.databaseId) return null;
122
+ const connection = await callAction<{
123
+ sourceId: string | null;
124
+ filesDatabaseId: string | null;
125
+ }>(
126
+ "connect-local-folder-source" as never,
127
+ {
128
+ connectionId: folder.id,
129
+ label: folder.name,
130
+ databaseId: persistent.databaseId,
131
+ truthPolicy: "source_primary",
132
+ connectionMetadata: {
133
+ liveBridgeEnabled: true,
134
+ repository: folder.repository
135
+ ? { localId: folder.repository.localId }
136
+ : undefined,
137
+ workingCopy: {
138
+ id: folder.id,
139
+ repositoryId: folder.repository?.localId,
140
+ kind: "temporary",
141
+ name: folder.name,
142
+ branch: folder.repository?.branch,
143
+ commit: folder.repository?.commit,
144
+ deviceId: "agent-native-desktop",
145
+ },
146
+ },
147
+ dryRun: false,
148
+ } as never,
149
+ );
150
+ if (!connection.sourceId) return null;
151
+ await rememberLiveLocalFolderSource(
152
+ folder,
153
+ connection.sourceId,
154
+ connection.filesDatabaseId,
155
+ );
156
+ return connection.sourceId;
157
+ }
158
+
159
+ export async function syncLiveLocalFolder(folderId: string) {
160
+ let sourceId = liveLocalFolderSourceId(folderId);
161
+ if (!sourceId) {
162
+ const folderResult = await getDesktopContentFiles()?.getFolder({
163
+ folderId,
164
+ });
165
+ if (folderResult?.ok && folderResult.folder.contentSource?.sourceId) {
166
+ sourceId = folderResult.folder.contentSource.sourceId;
167
+ await rememberLiveLocalFolderSource(
168
+ folderResult.folder,
169
+ sourceId,
170
+ folderResult.folder.contentSource.databaseId,
171
+ );
172
+ }
173
+ }
174
+ if (!sourceId) return { synced: false as const, reason: "unregistered" };
175
+
176
+ const desktop = getDesktopContentFiles();
177
+ if (!desktop) return { synced: false as const, reason: "unavailable" };
178
+ const read = await desktop.readFiles({ folderId });
179
+ if (!read.ok) throw new Error(read.error);
180
+ const result = await callAction(
181
+ "sync-local-folder-source" as never,
182
+ {
183
+ sourceId,
184
+ files: read.sources ?? {},
185
+ fileIdentities: read.identities,
186
+ observedRevisions: read.revisions,
187
+ dryRun: false,
188
+ } as never,
189
+ );
190
+ return { synced: true as const, result };
191
+ }
@@ -68,6 +68,7 @@ import { Toaster } from "@/components/ui/toaster";
68
68
  import { AppToolkitProvider } from "@/components/ui/toolkit-provider";
69
69
 
70
70
  import changelog from "../CHANGELOG.md?raw";
71
+ import { LocalFolderLiveSync } from "./components/LocalFolderLiveSync";
71
72
  import { useDbSync } from "./hooks/use-db-sync";
72
73
  import { useNavigationState } from "./hooks/use-navigation-state";
73
74
  import { i18nCatalog } from "./i18n";
@@ -236,7 +237,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
236
237
  function AppSetup() {
237
238
  useDbSync();
238
239
  useNavigationState();
239
- return null;
240
+ return <LocalFolderLiveSync />;
240
241
  }
241
242
 
242
243
  function ThemeToggleItem() {