@agent-native/core 0.164.12 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
- package/corpus/templates/content/actions/_database-utils.ts +2 -0
- package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
- package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
- package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
- package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
- package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +78 -1
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
- package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
- package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
- package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
- package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
- package/corpus/templates/content/app/i18n-data.ts +79 -0
- package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
- package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
- package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
- package/corpus/templates/content/app/root.tsx +2 -1
- package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
- package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
- package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
- package/corpus/templates/content/docs/product/roadmap.md +2 -2
- package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
- package/corpus/templates/content/shared/api.ts +25 -0
- package/dist/app-config/auth.d.ts +4 -0
- package/dist/app-config/auth.js +7 -0
- package/dist/app-config/schema.d.ts +3 -0
- package/dist/app-config/schema.js +2 -0
- package/dist/cli/code-agent-executor.js +6 -1
- package/dist/cli/code-agent-mcp-config.d.ts +2 -2
- package/dist/cli/code-agent-mcp-config.js +34 -9
- package/dist/client/org/OrgSwitcher.js +8 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/mcp-client/config.d.ts +11 -0
- package/dist/mcp-client/config.js +33 -26
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/auth.js +15 -1
- package/package.json +1 -1
|
@@ -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
|
-
| {
|
|
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(
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
240
|
+
return <LocalFolderLiveSync />;
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
function ThemeToggleItem() {
|