@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +84 -0
- package/corpus/templates/calendar/AGENTS.md +5 -0
- package/corpus/templates/calendar/actions/delete-event.ts +1 -1
- package/corpus/templates/calendar/actions/delete-events.ts +508 -0
- package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
- package/corpus/templates/calendar/actions/list-events.ts +53 -0
- package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
- package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
- 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/corpus/templates/factory/AGENTS.md +2 -1
- package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
- package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
- package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
- package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
- package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
- package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
- package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +43 -0
- package/corpus/templates/factory/app/routes/factory.tsx +115 -15
- package/corpus/templates/factory/server/plugins/agent-chat.ts +3 -1
- 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/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp-client/config.d.ts +11 -0
- package/dist/mcp-client/config.js +33 -26
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +15 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/docs/content/actions-access-control.mdx +136 -2
- package/docs/content/actions-agent-tools.mdx +101 -0
- package/docs/content/actions-defining.mdx +2 -2
- package/docs/content/audit-log.mdx +1 -1
- package/docs/content/authentication.mdx +1 -1
- package/docs/content/cloudflare.mdx +43 -0
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/deploy-an-app.mdx +109 -0
- package/docs/content/deployment-environment-variables.mdx +101 -0
- package/docs/content/deployment.mdx +125 -792
- package/docs/content/docs-components.mdx +2 -2
- package/docs/content/environment-variables.mdx +41 -41
- package/docs/content/file-uploads.mdx +74 -40
- package/docs/content/harness-agents.mdx +2 -2
- package/docs/content/internationalization.mdx +61 -65
- package/docs/content/key-concepts.mdx +3 -3
- package/docs/content/multi-app-workspace.mdx +2 -2
- package/docs/content/netlify.mdx +47 -0
- package/docs/content/node-docker.mdx +138 -0
- package/docs/content/organizations-teams-permissions.mdx +5 -5
- package/docs/content/other-platforms.mdx +27 -0
- package/docs/content/ssr-caching.mdx +45 -0
- package/docs/content/template-assets-developers.mdx +2 -2
- package/docs/content/template-chat-developers.mdx +2 -2
- package/docs/content/template-content-developers.mdx +2 -2
- package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
- package/docs/content/template-content-sync.mdx +2 -2
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +2 -2
- package/docs/content/updating-ui-in-production.mdx +35 -0
- package/docs/content/vercel.mdx +38 -0
- package/docs/content/what-is-agent-native.mdx +1 -1
- package/docs/content/workspace-deployment.mdx +350 -0
- package/package.json +1 -1
- package/docs/content/agents.mdx +0 -9
- package/docs/content/database.mdx +0 -317
- package/docs/content/human-approval.mdx +0 -156
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BlockRegistryProvider } from "@agent-native/core/blocks";
|
|
2
2
|
import { generateTabId } from "@agent-native/core/client/agent-chat";
|
|
3
3
|
import { agentNativePath } from "@agent-native/core/client/api-path";
|
|
4
|
+
import { writeClipboardText } from "@agent-native/core/client/clipboard";
|
|
4
5
|
import {
|
|
5
6
|
useCollaborativeDoc,
|
|
6
7
|
emailToColor,
|
|
@@ -79,8 +80,11 @@ import {
|
|
|
79
80
|
usePushDocumentToNotion,
|
|
80
81
|
} from "@/hooks/use-notion";
|
|
81
82
|
import { rememberContentLandingDocument } from "@/lib/content-landing";
|
|
83
|
+
import type { DesktopContentFileRevision } from "@/lib/desktop-content-files";
|
|
82
84
|
import {
|
|
83
85
|
canWriteLinkedLocalSource,
|
|
86
|
+
readDocumentFromLinkedLocalSource,
|
|
87
|
+
watchLinkedLocalSource,
|
|
84
88
|
writeDocumentToLinkedLocalSource,
|
|
85
89
|
} from "@/lib/local-content-source-files";
|
|
86
90
|
import { isDatabaseChoicePending } from "@/lib/optimistic-document";
|
|
@@ -100,13 +104,23 @@ import { DocumentEditorSkeleton } from "./DocumentEditorSkeleton";
|
|
|
100
104
|
import { DocumentInfoPanel } from "./DocumentInfoPanel";
|
|
101
105
|
import { DocumentToolbar, type ToolbarBreadcrumbItem } from "./DocumentToolbar";
|
|
102
106
|
import { EmojiPicker } from "./EmojiPicker";
|
|
107
|
+
import {
|
|
108
|
+
classifyLocalSourceRead,
|
|
109
|
+
localSourceRevisionForQueuedEdit,
|
|
110
|
+
localSourceRevisionForSave,
|
|
111
|
+
type PendingLocalSourceWrite,
|
|
112
|
+
} from "./local-source-write-state";
|
|
103
113
|
import { NotionConflictBanner } from "./NotionConflictBanner";
|
|
104
114
|
import {
|
|
105
115
|
normalizeTitleText,
|
|
106
116
|
stripMarkdownHeadingPrefixFromTitlePaste,
|
|
107
117
|
} from "./title-text";
|
|
108
118
|
import { VisualEditor } from "./VisualEditor";
|
|
109
|
-
import type {
|
|
119
|
+
import type {
|
|
120
|
+
NotionPageLink,
|
|
121
|
+
VisualEditorHistoryController,
|
|
122
|
+
VisualEditorHistoryState,
|
|
123
|
+
} from "./VisualEditor";
|
|
110
124
|
|
|
111
125
|
const TAB_ID = generateTabId();
|
|
112
126
|
|
|
@@ -303,6 +317,25 @@ export function shouldAwaitAuthoritativeDocument({
|
|
|
303
317
|
return isFetching && !isFetchedAfterMount;
|
|
304
318
|
}
|
|
305
319
|
|
|
320
|
+
export function visualEditorInstanceKey(args: {
|
|
321
|
+
documentId: string;
|
|
322
|
+
documentUpdatedAt: string | null;
|
|
323
|
+
isLocalFileDocument: boolean;
|
|
324
|
+
canEdit: boolean;
|
|
325
|
+
collabEditorEnabled: boolean;
|
|
326
|
+
hasYDoc: boolean;
|
|
327
|
+
localFileSyncRevision?: number;
|
|
328
|
+
}) {
|
|
329
|
+
const mode = args.isLocalFileDocument
|
|
330
|
+
? `local-file:${args.localFileSyncRevision ?? 0}`
|
|
331
|
+
: args.collabEditorEnabled && args.hasYDoc
|
|
332
|
+
? "live-ready"
|
|
333
|
+
: args.canEdit
|
|
334
|
+
? "live-pending"
|
|
335
|
+
: `snapshot:${args.documentUpdatedAt}`;
|
|
336
|
+
return `${args.documentId}:${mode}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
306
339
|
interface DocumentEditorBodyProps {
|
|
307
340
|
documentId: string;
|
|
308
341
|
document: Document;
|
|
@@ -319,11 +352,13 @@ type PendingDocumentSave = {
|
|
|
319
352
|
options?: DocumentSaveOptions,
|
|
320
353
|
) => unknown | Promise<unknown>;
|
|
321
354
|
canEditWhenQueued: boolean;
|
|
355
|
+
expectedLocalSourceRevision?: string | null;
|
|
322
356
|
timeout: ReturnType<typeof setTimeout>;
|
|
323
357
|
};
|
|
324
358
|
|
|
325
359
|
type DocumentSaveOptions = {
|
|
326
360
|
allowQueuedSave?: boolean;
|
|
361
|
+
expectedLocalSourceRevision?: string | null;
|
|
327
362
|
};
|
|
328
363
|
|
|
329
364
|
type DocumentSaveResult = {
|
|
@@ -605,6 +640,42 @@ function DocumentEditorBody({
|
|
|
605
640
|
const [localContentUpdatedAt, setLocalContentUpdatedAt] = useState<
|
|
606
641
|
string | null
|
|
607
642
|
>(document.updatedAt ?? null);
|
|
643
|
+
const localSourceRevisionRef = useRef<DesktopContentFileRevision | undefined>(
|
|
644
|
+
undefined,
|
|
645
|
+
);
|
|
646
|
+
const pendingLocalSourceWriteRef = useRef<PendingLocalSourceWrite | null>(
|
|
647
|
+
null,
|
|
648
|
+
);
|
|
649
|
+
const [localSourceConflict, setLocalSourceConflict] = useState<{
|
|
650
|
+
diskDocument: Document;
|
|
651
|
+
diskRevision?: DesktopContentFileRevision;
|
|
652
|
+
unsavedText: string;
|
|
653
|
+
} | null>(null);
|
|
654
|
+
const [localSourceMissing, setLocalSourceMissing] = useState(false);
|
|
655
|
+
const [localSourceAccess, setLocalSourceAccess] = useState<
|
|
656
|
+
"checking" | "available" | "unavailable"
|
|
657
|
+
>("checking");
|
|
658
|
+
const [localFileSyncRevision, setLocalFileSyncRevision] = useState(0);
|
|
659
|
+
const editorHistoryControllerRef =
|
|
660
|
+
useRef<VisualEditorHistoryController | null>(null);
|
|
661
|
+
const [editorHistoryState, setEditorHistoryState] =
|
|
662
|
+
useState<VisualEditorHistoryState>({ canUndo: false, canRedo: false });
|
|
663
|
+
const handleHistoryStateChange = useCallback(
|
|
664
|
+
(next: VisualEditorHistoryState) => {
|
|
665
|
+
setEditorHistoryState((current) =>
|
|
666
|
+
current.canUndo === next.canUndo && current.canRedo === next.canRedo
|
|
667
|
+
? current
|
|
668
|
+
: next,
|
|
669
|
+
);
|
|
670
|
+
},
|
|
671
|
+
[],
|
|
672
|
+
);
|
|
673
|
+
const handleHistoryControllerChange = useCallback(
|
|
674
|
+
(controller: VisualEditorHistoryController | null) => {
|
|
675
|
+
editorHistoryControllerRef.current = controller;
|
|
676
|
+
},
|
|
677
|
+
[],
|
|
678
|
+
);
|
|
608
679
|
const handleDeleteDocument = useCallback(async () => {
|
|
609
680
|
try {
|
|
610
681
|
if (document.database) {
|
|
@@ -777,7 +848,11 @@ function DocumentEditorBody({
|
|
|
777
848
|
});
|
|
778
849
|
const bodyHydrationPending = documentBodyHydrationIsPending(document);
|
|
779
850
|
const editorCanEdit =
|
|
780
|
-
canEdit &&
|
|
851
|
+
canEdit &&
|
|
852
|
+
!bodyHydrationPending &&
|
|
853
|
+
!localSourceMissing &&
|
|
854
|
+
(!isLocalFileDocument || localSourceAccess === "available") &&
|
|
855
|
+
(isLocalFileDocument || collabSynced);
|
|
781
856
|
// Bind an editor's stable Y.Doc on its first mount, even while the initial
|
|
782
857
|
// state is loading. Editability and the reconcile hook share the exact
|
|
783
858
|
// `collabSynced` boundary; "not loading" can precede persisted Y.Doc
|
|
@@ -810,6 +885,10 @@ function DocumentEditorBody({
|
|
|
810
885
|
saveTimeoutRef.current = null;
|
|
811
886
|
pendingDocumentSaveRef.current = null;
|
|
812
887
|
}
|
|
888
|
+
pendingLocalSourceWriteRef.current = null;
|
|
889
|
+
setLocalSourceMissing(false);
|
|
890
|
+
setLocalSourceAccess("checking");
|
|
891
|
+
setLocalFileSyncRevision(0);
|
|
813
892
|
}
|
|
814
893
|
if (!isInitializedRef.current) {
|
|
815
894
|
setLocalTitle(document.title);
|
|
@@ -951,11 +1030,69 @@ function DocumentEditorBody({
|
|
|
951
1030
|
};
|
|
952
1031
|
|
|
953
1032
|
if (isLinkedLocalSource) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1033
|
+
let expectedLocalSourceRevision = localSourceRevisionForSave(
|
|
1034
|
+
options.expectedLocalSourceRevision,
|
|
1035
|
+
localSourceRevisionRef.current,
|
|
957
1036
|
);
|
|
1037
|
+
if (!expectedLocalSourceRevision) {
|
|
1038
|
+
const baseline = await readDocumentFromLinkedLocalSource(
|
|
1039
|
+
document,
|
|
1040
|
+
localSource,
|
|
1041
|
+
);
|
|
1042
|
+
if (!baseline.ok) throw new Error(baseline.error);
|
|
1043
|
+
if (
|
|
1044
|
+
baseline.revision &&
|
|
1045
|
+
(baseline.document.content !==
|
|
1046
|
+
lastSavedContentRef.current.content ||
|
|
1047
|
+
baseline.document.title !== lastSavedTitleRef.current.title)
|
|
1048
|
+
) {
|
|
1049
|
+
setLocalSourceConflict({
|
|
1050
|
+
diskDocument: baseline.document,
|
|
1051
|
+
diskRevision: baseline.revision,
|
|
1052
|
+
unsavedText: localContentRef.current,
|
|
1053
|
+
});
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
"The file changed on disk before this edit could be saved.",
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
expectedLocalSourceRevision = baseline.revision;
|
|
1059
|
+
localSourceRevisionRef.current = baseline.revision;
|
|
1060
|
+
}
|
|
1061
|
+
const pendingLocalWrite = {
|
|
1062
|
+
title: fileFirstDocument.title,
|
|
1063
|
+
content: fileFirstDocument.content,
|
|
1064
|
+
};
|
|
1065
|
+
pendingLocalSourceWriteRef.current = pendingLocalWrite;
|
|
1066
|
+
let result;
|
|
1067
|
+
try {
|
|
1068
|
+
result = await writeDocumentToLinkedLocalSource(
|
|
1069
|
+
fileFirstDocument,
|
|
1070
|
+
localSource,
|
|
1071
|
+
{ expectedRevision: expectedLocalSourceRevision },
|
|
1072
|
+
);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
if (pendingLocalSourceWriteRef.current === pendingLocalWrite) {
|
|
1075
|
+
pendingLocalSourceWriteRef.current = null;
|
|
1076
|
+
}
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
958
1079
|
if (!result.ok) {
|
|
1080
|
+
if (pendingLocalSourceWriteRef.current === pendingLocalWrite) {
|
|
1081
|
+
pendingLocalSourceWriteRef.current = null;
|
|
1082
|
+
}
|
|
1083
|
+
if (result.conflict) {
|
|
1084
|
+
const latest = await readDocumentFromLinkedLocalSource(
|
|
1085
|
+
fileFirstDocument,
|
|
1086
|
+
localSource,
|
|
1087
|
+
);
|
|
1088
|
+
if (latest.ok) {
|
|
1089
|
+
setLocalSourceConflict({
|
|
1090
|
+
diskDocument: latest.document,
|
|
1091
|
+
diskRevision: latest.revision,
|
|
1092
|
+
unsavedText: localContentRef.current,
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
959
1096
|
if (!localSourceWriteErrorShownRef.current) {
|
|
960
1097
|
toast.error(t("editor.couldNotSaveLocalFile"), {
|
|
961
1098
|
description: result.error,
|
|
@@ -964,6 +1101,19 @@ function DocumentEditorBody({
|
|
|
964
1101
|
}
|
|
965
1102
|
throw new Error(result.error);
|
|
966
1103
|
}
|
|
1104
|
+
localSourceRevisionRef.current = result.revision;
|
|
1105
|
+
lastSavedTitleRef.current = {
|
|
1106
|
+
...lastSavedTitleRef.current,
|
|
1107
|
+
title: fileFirstDocument.title,
|
|
1108
|
+
};
|
|
1109
|
+
lastSavedContentRef.current = {
|
|
1110
|
+
...lastSavedContentRef.current,
|
|
1111
|
+
content: fileFirstDocument.content,
|
|
1112
|
+
};
|
|
1113
|
+
if (pendingLocalSourceWriteRef.current === pendingLocalWrite) {
|
|
1114
|
+
pendingLocalSourceWriteRef.current = null;
|
|
1115
|
+
}
|
|
1116
|
+
setLocalSourceConflict(null);
|
|
967
1117
|
localSourceWriteErrorShownRef.current = false;
|
|
968
1118
|
setLocalContentUpdatedAt(nextSavedAt);
|
|
969
1119
|
}
|
|
@@ -1019,6 +1169,91 @@ function DocumentEditorBody({
|
|
|
1019
1169
|
const persistDocumentUpdatesRef = useRef(persistDocumentUpdates);
|
|
1020
1170
|
persistDocumentUpdatesRef.current = persistDocumentUpdates;
|
|
1021
1171
|
|
|
1172
|
+
useEffect(() => {
|
|
1173
|
+
if (!isLinkedLocalSourceDocument) return;
|
|
1174
|
+
let active = true;
|
|
1175
|
+
|
|
1176
|
+
const adoptDisk = async () => {
|
|
1177
|
+
const result = await readDocumentFromLinkedLocalSource(document);
|
|
1178
|
+
if (!active) return;
|
|
1179
|
+
if (!result.ok) {
|
|
1180
|
+
setLocalSourceAccess("unavailable");
|
|
1181
|
+
if (result.error.includes("was not found")) {
|
|
1182
|
+
if (saveTimeoutRef.current) {
|
|
1183
|
+
clearTimeout(saveTimeoutRef.current);
|
|
1184
|
+
saveTimeoutRef.current = null;
|
|
1185
|
+
pendingDocumentSaveRef.current = null;
|
|
1186
|
+
}
|
|
1187
|
+
pendingLocalSourceWriteRef.current = null;
|
|
1188
|
+
setLocalSourceMissing(true);
|
|
1189
|
+
}
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
setLocalSourceAccess("available");
|
|
1193
|
+
setLocalSourceMissing(false);
|
|
1194
|
+
const diskContent = result.document.content;
|
|
1195
|
+
const disposition = classifyLocalSourceRead({
|
|
1196
|
+
diskTitle: result.document.title,
|
|
1197
|
+
diskContent,
|
|
1198
|
+
localContent: localContentRef.current,
|
|
1199
|
+
lastSavedTitle: lastSavedTitleRef.current.title,
|
|
1200
|
+
lastSavedContent: lastSavedContentRef.current.content,
|
|
1201
|
+
pendingWrite: pendingLocalSourceWriteRef.current,
|
|
1202
|
+
hasPendingSave: pendingDocumentSaveRef.current !== null,
|
|
1203
|
+
});
|
|
1204
|
+
if (disposition === "pending-self-write") {
|
|
1205
|
+
localSourceRevisionRef.current = result.revision;
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (disposition === "conflict") {
|
|
1209
|
+
setLocalSourceConflict({
|
|
1210
|
+
diskDocument: result.document,
|
|
1211
|
+
diskRevision: result.revision,
|
|
1212
|
+
unsavedText: localContentRef.current,
|
|
1213
|
+
});
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
localSourceRevisionRef.current = result.revision;
|
|
1217
|
+
if (disposition === "unchanged") return;
|
|
1218
|
+
if (saveTimeoutRef.current) {
|
|
1219
|
+
clearTimeout(saveTimeoutRef.current);
|
|
1220
|
+
saveTimeoutRef.current = null;
|
|
1221
|
+
pendingDocumentSaveRef.current = null;
|
|
1222
|
+
}
|
|
1223
|
+
localTitleRef.current = result.document.title;
|
|
1224
|
+
localContentRef.current = diskContent;
|
|
1225
|
+
setLocalTitle(result.document.title);
|
|
1226
|
+
setLocalContent(diskContent);
|
|
1227
|
+
setLocalContentUpdatedAt(result.updatedAt);
|
|
1228
|
+
lastSavedTitleRef.current = {
|
|
1229
|
+
title: result.document.title,
|
|
1230
|
+
updatedAt: result.updatedAt,
|
|
1231
|
+
};
|
|
1232
|
+
lastSavedContentRef.current = {
|
|
1233
|
+
content: diskContent,
|
|
1234
|
+
updatedAt: result.updatedAt,
|
|
1235
|
+
};
|
|
1236
|
+
setLocalFileSyncRevision((revision) => revision + 1);
|
|
1237
|
+
setLocalSourceConflict(null);
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
void adoptDisk();
|
|
1241
|
+
let stop: (() => void) | undefined;
|
|
1242
|
+
void watchLinkedLocalSource(document.source, () => void adoptDisk()).then(
|
|
1243
|
+
(result) => {
|
|
1244
|
+
if (!active) {
|
|
1245
|
+
if (result.ok) result.unsubscribe();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (result.ok) stop = result.unsubscribe;
|
|
1249
|
+
},
|
|
1250
|
+
);
|
|
1251
|
+
return () => {
|
|
1252
|
+
active = false;
|
|
1253
|
+
stop?.();
|
|
1254
|
+
};
|
|
1255
|
+
}, [document.id, document.source, isLinkedLocalSourceDocument]);
|
|
1256
|
+
|
|
1022
1257
|
const saveDocumentImmediately = useCallback(
|
|
1023
1258
|
async (
|
|
1024
1259
|
title: string,
|
|
@@ -1132,6 +1367,7 @@ function DocumentEditorBody({
|
|
|
1132
1367
|
void Promise.resolve(
|
|
1133
1368
|
pending.save(pending.title, pending.content, {
|
|
1134
1369
|
allowQueuedSave: true,
|
|
1370
|
+
expectedLocalSourceRevision: pending.expectedLocalSourceRevision,
|
|
1135
1371
|
}),
|
|
1136
1372
|
).catch(handleBackgroundSaveError);
|
|
1137
1373
|
},
|
|
@@ -1140,12 +1376,19 @@ function DocumentEditorBody({
|
|
|
1140
1376
|
const debouncedSave = useCallback(
|
|
1141
1377
|
(title: string, content: string) => {
|
|
1142
1378
|
if (!canEditRef.current) return;
|
|
1379
|
+
const expectedLocalSourceRevision = isLinkedLocalSourceDocument
|
|
1380
|
+
? localSourceRevisionForQueuedEdit(
|
|
1381
|
+
pendingDocumentSaveRef.current?.expectedLocalSourceRevision,
|
|
1382
|
+
localSourceRevisionRef.current,
|
|
1383
|
+
)
|
|
1384
|
+
: undefined;
|
|
1143
1385
|
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
|
1144
1386
|
const pending: PendingDocumentSave = {
|
|
1145
1387
|
title,
|
|
1146
1388
|
content,
|
|
1147
1389
|
save: queueDocumentSave,
|
|
1148
1390
|
canEditWhenQueued: canEditRef.current,
|
|
1391
|
+
expectedLocalSourceRevision,
|
|
1149
1392
|
timeout: setTimeout(() => {
|
|
1150
1393
|
if (pendingDocumentSaveRef.current === pending) {
|
|
1151
1394
|
pendingDocumentSaveRef.current = null;
|
|
@@ -1157,7 +1400,7 @@ function DocumentEditorBody({
|
|
|
1157
1400
|
pendingDocumentSaveRef.current = pending;
|
|
1158
1401
|
saveTimeoutRef.current = pending.timeout;
|
|
1159
1402
|
},
|
|
1160
|
-
[flushPendingDocumentSave, queueDocumentSave],
|
|
1403
|
+
[flushPendingDocumentSave, isLinkedLocalSourceDocument, queueDocumentSave],
|
|
1161
1404
|
);
|
|
1162
1405
|
|
|
1163
1406
|
useEffect(() => {
|
|
@@ -1437,6 +1680,7 @@ function DocumentEditorBody({
|
|
|
1437
1680
|
const handleContentChange = useCallback(
|
|
1438
1681
|
(newContent: string) => {
|
|
1439
1682
|
if (!editorCanEdit) return;
|
|
1683
|
+
localContentRef.current = newContent;
|
|
1440
1684
|
setLocalContent(newContent);
|
|
1441
1685
|
debouncedSave(localTitleRef.current, newContent);
|
|
1442
1686
|
},
|
|
@@ -1459,6 +1703,32 @@ function DocumentEditorBody({
|
|
|
1459
1703
|
[editorCanEdit, queueDocumentSave],
|
|
1460
1704
|
);
|
|
1461
1705
|
|
|
1706
|
+
const useDiskVersion = useCallback(() => {
|
|
1707
|
+
if (!localSourceConflict) return;
|
|
1708
|
+
const next = localSourceConflict.diskDocument;
|
|
1709
|
+
if (saveTimeoutRef.current) {
|
|
1710
|
+
clearTimeout(saveTimeoutRef.current);
|
|
1711
|
+
saveTimeoutRef.current = null;
|
|
1712
|
+
pendingDocumentSaveRef.current = null;
|
|
1713
|
+
}
|
|
1714
|
+
localSourceRevisionRef.current = localSourceConflict.diskRevision;
|
|
1715
|
+
localTitleRef.current = next.title;
|
|
1716
|
+
localContentRef.current = next.content;
|
|
1717
|
+
setLocalTitle(next.title);
|
|
1718
|
+
setLocalContent(next.content);
|
|
1719
|
+
setLocalContentUpdatedAt(next.updatedAt ?? new Date().toISOString());
|
|
1720
|
+
lastSavedTitleRef.current = {
|
|
1721
|
+
title: next.title,
|
|
1722
|
+
updatedAt: next.updatedAt ?? null,
|
|
1723
|
+
};
|
|
1724
|
+
lastSavedContentRef.current = {
|
|
1725
|
+
content: next.content,
|
|
1726
|
+
updatedAt: next.updatedAt ?? null,
|
|
1727
|
+
};
|
|
1728
|
+
setLocalFileSyncRevision((revision) => revision + 1);
|
|
1729
|
+
setLocalSourceConflict(null);
|
|
1730
|
+
}, [localSourceConflict]);
|
|
1731
|
+
|
|
1462
1732
|
// Comments state — pending comment from text selection
|
|
1463
1733
|
const [pendingComment, setPendingComment] = useState<{
|
|
1464
1734
|
quotedText: string;
|
|
@@ -1767,7 +2037,7 @@ function DocumentEditorBody({
|
|
|
1767
2037
|
agentPresent={agentPresent}
|
|
1768
2038
|
agentActive={agentActive}
|
|
1769
2039
|
currentUserEmail={session?.email}
|
|
1770
|
-
canEdit={
|
|
2040
|
+
canEdit={editorCanEdit}
|
|
1771
2041
|
hideFromSearch={document.hideFromSearch}
|
|
1772
2042
|
source={document.source}
|
|
1773
2043
|
canDelete={canDelete}
|
|
@@ -1779,12 +2049,76 @@ function DocumentEditorBody({
|
|
|
1779
2049
|
onUtilityPanelChange={handleUtilityPanelChange}
|
|
1780
2050
|
showCommentsControl={canComment && !isLocalFileDocument}
|
|
1781
2051
|
onOpenBreadcrumbItem={handleOpenToolbarBreadcrumb}
|
|
2052
|
+
canUndo={editorHistoryState.canUndo}
|
|
2053
|
+
canRedo={editorHistoryState.canRedo}
|
|
2054
|
+
onUndo={() => editorHistoryControllerRef.current?.undo()}
|
|
2055
|
+
onRedo={() => editorHistoryControllerRef.current?.redo()}
|
|
1782
2056
|
/>
|
|
1783
2057
|
|
|
1784
2058
|
{!isLocalFileDocument ? (
|
|
1785
2059
|
<NotionConflictBanner documentId={documentId} canEdit={canEdit} />
|
|
1786
2060
|
) : null}
|
|
1787
2061
|
|
|
2062
|
+
{localSourceConflict ? (
|
|
2063
|
+
<div
|
|
2064
|
+
className="flex flex-wrap items-center gap-2 border-b bg-muted/40 px-4 py-2 text-sm"
|
|
2065
|
+
role="alert"
|
|
2066
|
+
data-local-source-conflict
|
|
2067
|
+
>
|
|
2068
|
+
<span className="me-auto">
|
|
2069
|
+
{t("editor.localFileChangedWithUnsavedEdits")}
|
|
2070
|
+
</span>
|
|
2071
|
+
<Button
|
|
2072
|
+
type="button"
|
|
2073
|
+
size="sm"
|
|
2074
|
+
variant="ghost"
|
|
2075
|
+
onClick={() => {
|
|
2076
|
+
void writeClipboardText(localSourceConflict.unsavedText).then(
|
|
2077
|
+
(copied) => {
|
|
2078
|
+
if (copied) {
|
|
2079
|
+
toast.success(t("editor.unsavedTextCopied"));
|
|
2080
|
+
} else {
|
|
2081
|
+
toast.error(
|
|
2082
|
+
t("editor.toolbar.clipboardAccessUnavailable"),
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
);
|
|
2087
|
+
}}
|
|
2088
|
+
>
|
|
2089
|
+
{t("editor.copyUnsavedText")}
|
|
2090
|
+
</Button>
|
|
2091
|
+
<Button type="button" size="sm" onClick={useDiskVersion}>
|
|
2092
|
+
{t("editor.useDiskVersion")}
|
|
2093
|
+
</Button>
|
|
2094
|
+
</div>
|
|
2095
|
+
) : null}
|
|
2096
|
+
|
|
2097
|
+
{localSourceMissing ? (
|
|
2098
|
+
<div
|
|
2099
|
+
className="border-b bg-muted/40 px-4 py-2 text-sm"
|
|
2100
|
+
role="alert"
|
|
2101
|
+
data-local-source-missing
|
|
2102
|
+
>
|
|
2103
|
+
{t("empty.documentNotFound")}
|
|
2104
|
+
</div>
|
|
2105
|
+
) : null}
|
|
2106
|
+
|
|
2107
|
+
{isLocalFileDocument && localSourceAccess === "unavailable" ? (
|
|
2108
|
+
<div
|
|
2109
|
+
className="border-b bg-muted/20 px-4 py-1.5 text-xs text-muted-foreground"
|
|
2110
|
+
role="status"
|
|
2111
|
+
data-local-source-read-only
|
|
2112
|
+
>
|
|
2113
|
+
{t("editor.localFileReadOnlySnapshot", {
|
|
2114
|
+
device: "Agent Native Desktop",
|
|
2115
|
+
date: new Date(
|
|
2116
|
+
document.source?.updatedAt ?? document.updatedAt,
|
|
2117
|
+
).toLocaleString(),
|
|
2118
|
+
})}
|
|
2119
|
+
</div>
|
|
2120
|
+
) : null}
|
|
2121
|
+
|
|
1788
2122
|
<div
|
|
1789
2123
|
ref={scrollContainerRef}
|
|
1790
2124
|
className="flex-1 min-h-0 min-w-0 overflow-auto flex flex-col"
|
|
@@ -1976,7 +2310,15 @@ function DocumentEditorBody({
|
|
|
1976
2310
|
// fields.
|
|
1977
2311
|
const primaryEditor = (
|
|
1978
2312
|
<VisualEditor
|
|
1979
|
-
key={
|
|
2313
|
+
key={visualEditorInstanceKey({
|
|
2314
|
+
documentId,
|
|
2315
|
+
documentUpdatedAt: document.updatedAt,
|
|
2316
|
+
isLocalFileDocument,
|
|
2317
|
+
canEdit,
|
|
2318
|
+
collabEditorEnabled,
|
|
2319
|
+
hasYDoc: Boolean(ydoc),
|
|
2320
|
+
localFileSyncRevision,
|
|
2321
|
+
})}
|
|
1980
2322
|
documentId={documentId}
|
|
1981
2323
|
content={
|
|
1982
2324
|
isLocalFileDocument
|
|
@@ -2014,6 +2356,10 @@ function DocumentEditorBody({
|
|
|
2014
2356
|
notionPageLinks={notionPageLinks}
|
|
2015
2357
|
onOpenNotionPageLink={handleOpenNotionPageLink}
|
|
2016
2358
|
notionPageId={document.notionPageId}
|
|
2359
|
+
onHistoryControllerChange={
|
|
2360
|
+
handleHistoryControllerChange
|
|
2361
|
+
}
|
|
2362
|
+
onHistoryStateChange={handleHistoryStateChange}
|
|
2017
2363
|
/>
|
|
2018
2364
|
);
|
|
2019
2365
|
|
|
@@ -11,6 +11,8 @@ import type { DocumentSourceInfo } from "@shared/api";
|
|
|
11
11
|
import {
|
|
12
12
|
IconArrowBarDown,
|
|
13
13
|
IconArrowBarUp,
|
|
14
|
+
IconArrowBackUp,
|
|
15
|
+
IconArrowForwardUp,
|
|
14
16
|
IconAlertTriangle,
|
|
15
17
|
IconCheck,
|
|
16
18
|
IconCopy,
|
|
@@ -472,6 +474,10 @@ interface DocumentToolbarProps {
|
|
|
472
474
|
onUtilityPanelChange: (panel: "info" | "comments" | null) => void;
|
|
473
475
|
showCommentsControl?: boolean;
|
|
474
476
|
onOpenBreadcrumbItem?: (id: string) => void;
|
|
477
|
+
canUndo?: boolean;
|
|
478
|
+
canRedo?: boolean;
|
|
479
|
+
onUndo?: () => void;
|
|
480
|
+
onRedo?: () => void;
|
|
475
481
|
}
|
|
476
482
|
|
|
477
483
|
export function DocumentToolbar({
|
|
@@ -494,6 +500,10 @@ export function DocumentToolbar({
|
|
|
494
500
|
onUtilityPanelChange,
|
|
495
501
|
showCommentsControl = true,
|
|
496
502
|
onOpenBreadcrumbItem,
|
|
503
|
+
canUndo = false,
|
|
504
|
+
canRedo = false,
|
|
505
|
+
onUndo,
|
|
506
|
+
onRedo,
|
|
497
507
|
}: DocumentToolbarProps) {
|
|
498
508
|
const t = useT();
|
|
499
509
|
const navigate = useNavigate();
|
|
@@ -971,6 +981,17 @@ export function DocumentToolbar({
|
|
|
971
981
|
</TooltipContent>
|
|
972
982
|
</Tooltip>
|
|
973
983
|
<DropdownMenuContent align="end" className="w-60">
|
|
984
|
+
<DropdownMenuGroup>
|
|
985
|
+
<DropdownMenuItem disabled={!canUndo} onSelect={onUndo}>
|
|
986
|
+
<IconArrowBackUp className="me-2 h-4 w-4" />
|
|
987
|
+
{t("editor.toolbar.undo")}
|
|
988
|
+
</DropdownMenuItem>
|
|
989
|
+
<DropdownMenuItem disabled={!canRedo} onSelect={onRedo}>
|
|
990
|
+
<IconArrowForwardUp className="me-2 h-4 w-4" />
|
|
991
|
+
{t("editor.toolbar.redo")}
|
|
992
|
+
</DropdownMenuItem>
|
|
993
|
+
</DropdownMenuGroup>
|
|
994
|
+
<DropdownMenuSeparator />
|
|
974
995
|
<DropdownMenuGroup>
|
|
975
996
|
<DropdownMenuItem onSelect={() => void handleCopyPageLink()}>
|
|
976
997
|
<IconLink className="me-2 h-4 w-4" />
|
|
@@ -1018,7 +1039,7 @@ export function DocumentToolbar({
|
|
|
1018
1039
|
<span className="truncate">{source?.path}</span>
|
|
1019
1040
|
</DropdownMenuItem>
|
|
1020
1041
|
<DropdownMenuItem
|
|
1021
|
-
disabled={revealLocalSource.isPending}
|
|
1042
|
+
disabled={!canEdit || revealLocalSource.isPending}
|
|
1022
1043
|
onSelect={() => void handleRevealLocalPath()}
|
|
1023
1044
|
>
|
|
1024
1045
|
<IconFolderOpen className="me-2 h-4 w-4" />
|
|
@@ -1029,6 +1050,7 @@ export function DocumentToolbar({
|
|
|
1029
1050
|
{t("editor.toolbar.copyRelativePath")}
|
|
1030
1051
|
</DropdownMenuItem>
|
|
1031
1052
|
<DropdownMenuItem
|
|
1053
|
+
disabled={!canEdit}
|
|
1032
1054
|
onSelect={() => void handleCopyLocalAbsolutePath()}
|
|
1033
1055
|
>
|
|
1034
1056
|
<IconCopy className="me-2 h-4 w-4" />
|