@agent-native/core 0.175.3 → 0.175.4-nightly-20260826190205
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/clips/server/plugins/agent-chat.ts +3 -0
- package/corpus/templates/content/actions/_linked-local-document-edit.ts +167 -0
- package/corpus/templates/content/actions/edit-document.ts +181 -61
- package/corpus/templates/content/actions/import-content-source.ts +33 -1
- package/corpus/templates/content/actions/update-document.ts +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +68 -0
- package/corpus/templates/content/app/components/editor/LinkedLocalDocumentAgentBridge.tsx +245 -0
- package/corpus/templates/content/app/lib/local-content-source-files.ts +96 -9
- package/corpus/templates/content/shared/api.ts +2 -0
- package/corpus/templates/content/shared/content-source.ts +14 -0
- package/corpus/templates/content/shared/document-text-edits.ts +39 -0
- package/corpus/templates/content/shared/nfm.ts +347 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
- package/dist/agent/engine/ai-sdk-engine.js +14 -4
- package/dist/agent/engine/anthropic-engine.js +6 -4
- package/dist/agent/engine/builder-engine.js +11 -4
- package/dist/agent/engine/tool-name.d.ts +9 -0
- package/dist/agent/engine/tool-name.js +56 -0
- package/dist/agent/engine/translate-ai-sdk.d.ts +5 -3
- package/dist/agent/engine/translate-ai-sdk.js +16 -13
- package/dist/agent/engine/translate-anthropic.d.ts +8 -6
- package/dist/agent/engine/translate-anthropic.js +21 -22
- package/dist/agent/production-agent.js +23 -5
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/fetch-tool.js +73 -5
- package/dist/extensions/proxy-security.d.ts +5 -0
- package/dist/extensions/proxy-security.js +21 -13
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/server/agent-chat/context-tools.js +5 -0
- package/dist/server/credential-provider.js +47 -10
- package/dist/server/realtime-token.d.ts +1 -1
- package/docs/content/webmcp.mdx +5 -5
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -49,6 +49,9 @@ export default createAgentChatPlugin({
|
|
|
49
49
|
`<clips-transcript-guidance>
|
|
50
50
|
The transcript in view-screen and get-recording-player-data are bounded previews when called by the agent. When previewTruncated is true, the text is expected to end mid-sentence and is never evidence that transcription stopped early. Use the bounded payload for a concise summary and do not request or reconstruct the omitted transcript by searching other recordings. For a failed or pending transcript, use request-transcript with force=true. For an explicit fresh retry of an existing ready transcript, also pass regenerate=true. Agent-triggered retries are queued for the durable worker and return pending while processing.
|
|
51
51
|
</clips-transcript-guidance>
|
|
52
|
+
<clips-agent-routing>
|
|
53
|
+
For summaries, action-item lists, follow-up lists, and todo lists based on the recording in view, use Clips' recording/transcript actions and answer directly in this chat. Do not call Plan or another app for a generic written list. Only delegate to Plan when the user explicitly asks for a visual or structured Agent-Native Plan, a Plan artifact, or the Plans app.
|
|
54
|
+
</clips-agent-routing>
|
|
52
55
|
<clips-recording-discovery>
|
|
53
56
|
Clips public recordings are unlisted-by-link, not a searchable public catalog. Treat a recording as known only when it is owned by the current user, already appears in the current screen, or the current user has already viewed it. Do not use list-recordings or search-recordings to discover someone else's clips, recover from a failed direct lookup, answer a date/time question, or search by a title that was not supplied by the user. A question such as when this clip was created refers to the clip already in context; answer it from current-screen or get-recording-player-data. If a direct recording lookup fails, report the failure and stop instead of broadening the search. Only inspect another clip when the user provides its share URL or recording id in the request.
|
|
54
57
|
</clips-recording-discovery>`,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callBrowserSession,
|
|
3
|
+
listBrowserSessions,
|
|
4
|
+
} from "@agent-native/core/server";
|
|
5
|
+
|
|
6
|
+
import type { DocumentTextEdit } from "../shared/document-text-edits.js";
|
|
7
|
+
|
|
8
|
+
export const linkedLocalDocumentEditActionName = (documentId: string) =>
|
|
9
|
+
`content-edit-linked-document:${documentId}`;
|
|
10
|
+
|
|
11
|
+
type LinkedLocalEditReceipt =
|
|
12
|
+
| {
|
|
13
|
+
status: "persisted";
|
|
14
|
+
content: string;
|
|
15
|
+
title: string;
|
|
16
|
+
path: string;
|
|
17
|
+
runtime: "browser" | "desktop";
|
|
18
|
+
revision?: string;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
status: "source-persisted/history-pending";
|
|
22
|
+
content: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
metadata: {
|
|
26
|
+
parentId: string | null;
|
|
27
|
+
icon: string | null;
|
|
28
|
+
position: number;
|
|
29
|
+
isFavorite: boolean;
|
|
30
|
+
hideFromSearch: boolean;
|
|
31
|
+
visibility: "private" | "org" | "public";
|
|
32
|
+
};
|
|
33
|
+
path: string;
|
|
34
|
+
runtime: "browser" | "desktop";
|
|
35
|
+
revision?: string;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
status: "source-persisted/readback-pending";
|
|
39
|
+
content: string;
|
|
40
|
+
title: string;
|
|
41
|
+
path: string;
|
|
42
|
+
runtime: "browser" | "desktop";
|
|
43
|
+
revision?: string;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
status: "conflict" | "unavailable" | "failed";
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function isReceipt(value: unknown): value is LinkedLocalEditReceipt {
|
|
51
|
+
if (!value || typeof value !== "object") return false;
|
|
52
|
+
const receipt = value as Record<string, unknown>;
|
|
53
|
+
if (
|
|
54
|
+
receipt.status === "persisted" ||
|
|
55
|
+
receipt.status === "source-persisted/history-pending" ||
|
|
56
|
+
receipt.status === "source-persisted/readback-pending"
|
|
57
|
+
) {
|
|
58
|
+
const hasPersistedFields =
|
|
59
|
+
typeof receipt.content === "string" &&
|
|
60
|
+
typeof receipt.title === "string" &&
|
|
61
|
+
receipt.title.length > 0 &&
|
|
62
|
+
typeof receipt.path === "string" &&
|
|
63
|
+
receipt.path.length > 0 &&
|
|
64
|
+
(receipt.runtime === "browser" || receipt.runtime === "desktop") &&
|
|
65
|
+
(receipt.revision === undefined ||
|
|
66
|
+
(typeof receipt.revision === "string" && receipt.revision.length > 0));
|
|
67
|
+
if (!hasPersistedFields) return false;
|
|
68
|
+
if (receipt.status !== "source-persisted/history-pending") return true;
|
|
69
|
+
const metadata = receipt.metadata as Record<string, unknown> | undefined;
|
|
70
|
+
return (
|
|
71
|
+
typeof receipt.description === "string" &&
|
|
72
|
+
!!metadata &&
|
|
73
|
+
(metadata.parentId === null || typeof metadata.parentId === "string") &&
|
|
74
|
+
(metadata.icon === null || typeof metadata.icon === "string") &&
|
|
75
|
+
typeof metadata.position === "number" &&
|
|
76
|
+
typeof metadata.isFavorite === "boolean" &&
|
|
77
|
+
typeof metadata.hideFromSearch === "boolean" &&
|
|
78
|
+
(metadata.visibility === "private" ||
|
|
79
|
+
metadata.visibility === "org" ||
|
|
80
|
+
metadata.visibility === "public")
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return (
|
|
84
|
+
(receipt.status === "conflict" ||
|
|
85
|
+
receipt.status === "unavailable" ||
|
|
86
|
+
receipt.status === "failed") &&
|
|
87
|
+
typeof receipt.error === "string" &&
|
|
88
|
+
receipt.error.length > 0
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function editLinkedLocalDocumentThroughBrowser(args: {
|
|
93
|
+
ownerEmail: string;
|
|
94
|
+
documentId: string;
|
|
95
|
+
expectedContent: string;
|
|
96
|
+
expectedTitle: string;
|
|
97
|
+
expectedDescription: string;
|
|
98
|
+
expectedMetadata: string;
|
|
99
|
+
expectedResultContent: string;
|
|
100
|
+
edits: DocumentTextEdit[];
|
|
101
|
+
}): Promise<LinkedLocalEditReceipt> {
|
|
102
|
+
const name = linkedLocalDocumentEditActionName(args.documentId);
|
|
103
|
+
const sessions = await listBrowserSessions(args.ownerEmail, { limit: 100 });
|
|
104
|
+
const matches = sessions.filter((session) =>
|
|
105
|
+
session.actions.some((action) => action.name === name),
|
|
106
|
+
);
|
|
107
|
+
if (matches.length === 0) {
|
|
108
|
+
return {
|
|
109
|
+
status: "unavailable",
|
|
110
|
+
error:
|
|
111
|
+
"Open this linked document in a browser with its local source folder connected, then retry the edit.",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (matches.length > 1) {
|
|
115
|
+
return {
|
|
116
|
+
status: "conflict",
|
|
117
|
+
error:
|
|
118
|
+
"This linked document is open in more than one writable browser session. Close the extra tab and retry.",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await callBrowserSession(
|
|
124
|
+
args.ownerEmail,
|
|
125
|
+
matches[0]!.sessionId,
|
|
126
|
+
{
|
|
127
|
+
type: "run-action",
|
|
128
|
+
name,
|
|
129
|
+
args: {
|
|
130
|
+
documentId: args.documentId,
|
|
131
|
+
expectedContent: args.expectedContent,
|
|
132
|
+
expectedTitle: args.expectedTitle,
|
|
133
|
+
expectedDescription: args.expectedDescription,
|
|
134
|
+
expectedMetadata: args.expectedMetadata,
|
|
135
|
+
edits: args.edits,
|
|
136
|
+
},
|
|
137
|
+
timeoutMs: 30_000,
|
|
138
|
+
},
|
|
139
|
+
{ timeoutMs: 30_000 },
|
|
140
|
+
);
|
|
141
|
+
if (!isReceipt(result)) {
|
|
142
|
+
return {
|
|
143
|
+
status: "failed",
|
|
144
|
+
error: "The local source returned an invalid receipt.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
(result.status === "persisted" ||
|
|
149
|
+
result.status === "source-persisted/readback-pending") &&
|
|
150
|
+
result.content !== args.expectedResultContent
|
|
151
|
+
) {
|
|
152
|
+
return {
|
|
153
|
+
status: "conflict",
|
|
154
|
+
error: "The local source receipt did not match the requested edit.",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
status: "failed",
|
|
161
|
+
error:
|
|
162
|
+
error instanceof Error
|
|
163
|
+
? error.message
|
|
164
|
+
: "The local source write failed.",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
hasCollabState,
|
|
6
6
|
searchAndReplace,
|
|
7
7
|
} from "@agent-native/core/collab";
|
|
8
|
+
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
8
9
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
9
10
|
import {
|
|
10
11
|
getGenerationCreativeContext,
|
|
@@ -13,14 +14,17 @@ import {
|
|
|
13
14
|
validateGenerationCreativeContext,
|
|
14
15
|
} from "@agent-native/creative-context/server";
|
|
15
16
|
import type { CreativeContextReuseLabel } from "@agent-native/creative-context/types";
|
|
16
|
-
import { eq } from "drizzle-orm";
|
|
17
|
+
import { and, eq } from "drizzle-orm";
|
|
17
18
|
import { z } from "zod";
|
|
18
19
|
|
|
19
20
|
import { getDb, schema } from "../server/db/index.js";
|
|
21
|
+
import { applyDocumentTextEdits } from "../shared/document-text-edits.js";
|
|
22
|
+
import { inspectNfmFidelity } from "../shared/nfm.js";
|
|
20
23
|
import {
|
|
21
24
|
lockPrimaryBlocksFields,
|
|
22
25
|
persistBlocksFieldIdentity,
|
|
23
26
|
} from "./_blocks-field-identity.js";
|
|
27
|
+
import { editLinkedLocalDocumentThroughBrowser } from "./_linked-local-document-edit.js";
|
|
24
28
|
|
|
25
29
|
interface TextEdit {
|
|
26
30
|
find: string;
|
|
@@ -110,46 +114,47 @@ export default defineAction({
|
|
|
110
114
|
|
|
111
115
|
// ─── Apply edits to the document markdown ───────────────────────────────
|
|
112
116
|
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// and
|
|
117
|
-
//
|
|
118
|
-
// (lists, headings, tables) renders correctly and merges with any
|
|
119
|
-
// concurrent human edits via the Yjs CRDT. See the `real-time-collab` skill.
|
|
117
|
+
// Native documents edit canonical `documents.content`. A linked local file
|
|
118
|
+
// instead commits through its exact live source bridge before SQL mirrors
|
|
119
|
+
// the accepted bytes. The SQL change is delivered to open editors through
|
|
120
|
+
// normal change-sync and parsed through the real editor pipeline so new
|
|
121
|
+
// block structure renders correctly and merges through Yjs.
|
|
120
122
|
//
|
|
121
123
|
// (The old approach POSTed a Yjs search-replace to a localhost collab origin,
|
|
122
124
|
// which silently no-oped on serverless — different process, no localhost —
|
|
123
125
|
// and could only patch text inside existing nodes, never create structure.)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const appliedEdits: TextEdit[] = [];
|
|
128
|
-
|
|
129
|
-
for (const edit of edits) {
|
|
130
|
-
const idx = content.indexOf(edit.find);
|
|
131
|
-
if (idx === -1) {
|
|
132
|
-
results.push(
|
|
133
|
-
`NOT FOUND: "${edit.find.slice(0, 60)}${edit.find.length > 60 ? "..." : ""}"`,
|
|
134
|
-
);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
content =
|
|
138
|
-
content.slice(0, idx) +
|
|
139
|
-
edit.replace +
|
|
140
|
-
content.slice(idx + edit.find.length);
|
|
141
|
-
changeCount++;
|
|
142
|
-
appliedEdits.push(edit);
|
|
143
|
-
const action = edit.replace === "" ? "deleted" : "replaced";
|
|
144
|
-
results.push(
|
|
145
|
-
`${action}: "${edit.find.slice(0, 40)}${edit.find.length > 40 ? "..." : ""}"`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
126
|
+
const applied = applyDocumentTextEdits(existing.content ?? "", edits);
|
|
127
|
+
let { content } = applied;
|
|
128
|
+
const { results, appliedEdits, changeCount } = applied;
|
|
148
129
|
|
|
149
130
|
if (changeCount === 0) {
|
|
150
131
|
return { applied: 0, total: edits.length, results };
|
|
151
132
|
}
|
|
152
133
|
|
|
134
|
+
let linkedLocalPersistence:
|
|
135
|
+
| {
|
|
136
|
+
status:
|
|
137
|
+
| "persisted"
|
|
138
|
+
| "source-persisted/readback-pending"
|
|
139
|
+
| "source-persisted/history-pending";
|
|
140
|
+
path: string;
|
|
141
|
+
runtime: "browser" | "desktop";
|
|
142
|
+
}
|
|
143
|
+
| undefined;
|
|
144
|
+
let linkedLocalHistoryReconciliation = false;
|
|
145
|
+
let linkedLocalReconciliationDocument:
|
|
146
|
+
| {
|
|
147
|
+
title: string;
|
|
148
|
+
description: string;
|
|
149
|
+
parentId: string | null;
|
|
150
|
+
icon: string | null;
|
|
151
|
+
position: number;
|
|
152
|
+
isFavorite: boolean;
|
|
153
|
+
hideFromSearch: boolean;
|
|
154
|
+
visibility: "private" | "org" | "public";
|
|
155
|
+
}
|
|
156
|
+
| undefined;
|
|
157
|
+
|
|
153
158
|
const previousGeneration =
|
|
154
159
|
args.contextModeOverride === "off"
|
|
155
160
|
? null
|
|
@@ -236,39 +241,146 @@ export default defineAction({
|
|
|
236
241
|
};
|
|
237
242
|
}
|
|
238
243
|
|
|
244
|
+
const isLinkedLocalSource =
|
|
245
|
+
existing.sourceMode === "local-files" &&
|
|
246
|
+
existing.sourceKind !== "folder" &&
|
|
247
|
+
Boolean(existing.sourcePath) &&
|
|
248
|
+
!id.startsWith("local-file:") &&
|
|
249
|
+
!id.startsWith("local-folder:");
|
|
250
|
+
if (isLinkedLocalSource) {
|
|
251
|
+
const ownerEmail = getRequestUserEmail();
|
|
252
|
+
if (!ownerEmail) {
|
|
253
|
+
return {
|
|
254
|
+
applied: 0,
|
|
255
|
+
total: edits.length,
|
|
256
|
+
results,
|
|
257
|
+
persistence: "unavailable",
|
|
258
|
+
error:
|
|
259
|
+
"No authenticated user is available for the local source write.",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const receipt = await editLinkedLocalDocumentThroughBrowser({
|
|
263
|
+
ownerEmail,
|
|
264
|
+
documentId: id,
|
|
265
|
+
expectedContent: existing.content ?? "",
|
|
266
|
+
expectedTitle: existing.title,
|
|
267
|
+
expectedDescription: existing.description ?? "",
|
|
268
|
+
expectedMetadata: JSON.stringify({
|
|
269
|
+
parentId: existing.parentId,
|
|
270
|
+
icon: existing.icon,
|
|
271
|
+
position: existing.position,
|
|
272
|
+
isFavorite: Boolean(existing.isFavorite),
|
|
273
|
+
hideFromSearch: Boolean(existing.hideFromSearch),
|
|
274
|
+
visibility: existing.visibility,
|
|
275
|
+
}),
|
|
276
|
+
expectedResultContent: applied.content,
|
|
277
|
+
edits,
|
|
278
|
+
});
|
|
279
|
+
if (receipt.status === "source-persisted/history-pending") {
|
|
280
|
+
linkedLocalHistoryReconciliation = true;
|
|
281
|
+
linkedLocalReconciliationDocument = {
|
|
282
|
+
title: receipt.title,
|
|
283
|
+
description: receipt.description,
|
|
284
|
+
...receipt.metadata,
|
|
285
|
+
visibility: existing.visibility,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
receipt.status !== "persisted" &&
|
|
290
|
+
receipt.status !== "source-persisted/history-pending" &&
|
|
291
|
+
receipt.status !== "source-persisted/readback-pending"
|
|
292
|
+
) {
|
|
293
|
+
return {
|
|
294
|
+
applied: 0,
|
|
295
|
+
total: edits.length,
|
|
296
|
+
results,
|
|
297
|
+
persistence: receipt.status,
|
|
298
|
+
error: receipt.error,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
content = receipt.content;
|
|
302
|
+
linkedLocalPersistence = {
|
|
303
|
+
status: receipt.status,
|
|
304
|
+
path: receipt.path,
|
|
305
|
+
runtime: receipt.runtime,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
239
309
|
// Persist. The fresh updatedAt is the signal the open editor uses to tell an
|
|
240
310
|
// intentional external edit apart from a stale autosave echo.
|
|
241
311
|
const db = getDb();
|
|
242
312
|
const now = new Date().toISOString();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
313
|
+
try {
|
|
314
|
+
await db.transaction(async (tx: any) => {
|
|
315
|
+
const primaryBlocksFields = await lockPrimaryBlocksFields(tx, id);
|
|
316
|
+
const mirrored = await tx
|
|
317
|
+
.update(schema.documents)
|
|
318
|
+
.set({
|
|
319
|
+
content,
|
|
320
|
+
updatedAt: now,
|
|
321
|
+
...(linkedLocalReconciliationDocument ?? {}),
|
|
322
|
+
})
|
|
323
|
+
.where(
|
|
324
|
+
and(
|
|
325
|
+
eq(schema.documents.id, id),
|
|
326
|
+
eq(schema.documents.updatedAt, existing.updatedAt),
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
.returning({ id: schema.documents.id });
|
|
330
|
+
if (mirrored.length !== 1) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
"The document changed before Content history could be updated.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
for (const field of primaryBlocksFields) {
|
|
336
|
+
await persistBlocksFieldIdentity({
|
|
337
|
+
db: tx as unknown as ReturnType<typeof getDb>,
|
|
338
|
+
ownerEmail: field.ownerEmail,
|
|
339
|
+
documentId: id,
|
|
340
|
+
propertyId: field.propertyId,
|
|
341
|
+
previousMarkdown: existing.content ?? "",
|
|
342
|
+
markdown: content,
|
|
343
|
+
now,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (creativeContext) {
|
|
347
|
+
await recordGenerationCreativeContext(
|
|
348
|
+
{
|
|
349
|
+
appId: "content",
|
|
350
|
+
artifactType: "document",
|
|
351
|
+
artifactId: id,
|
|
352
|
+
...creativeContext,
|
|
353
|
+
},
|
|
354
|
+
{ db: tx },
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (!linkedLocalPersistence) throw error;
|
|
360
|
+
return {
|
|
361
|
+
applied: changeCount,
|
|
362
|
+
total: edits.length,
|
|
363
|
+
results,
|
|
364
|
+
persistence: "source-persisted/history-pending",
|
|
365
|
+
path: linkedLocalPersistence.path,
|
|
366
|
+
error:
|
|
367
|
+
error instanceof Error
|
|
368
|
+
? error.message
|
|
369
|
+
: "The local file changed, but Content history was not updated.",
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (linkedLocalHistoryReconciliation) {
|
|
374
|
+
return {
|
|
375
|
+
applied: 0,
|
|
376
|
+
total: edits.length,
|
|
377
|
+
results,
|
|
378
|
+
persistence: "source-persisted/history-reconciled",
|
|
379
|
+
path: linkedLocalPersistence?.path,
|
|
380
|
+
error:
|
|
381
|
+
"The source changed during verification. Content history was reconciled to the physical file, but the requested agent edit was not confirmed.",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
272
384
|
|
|
273
385
|
// Make the agent edit VISIBLE as a live collaborator. Content's collab doc
|
|
274
386
|
// binds the TipTap Y.XmlFragment("default"), so a live editing session is
|
|
@@ -310,6 +422,14 @@ export default defineAction({
|
|
|
310
422
|
applied: changeCount,
|
|
311
423
|
total: edits.length,
|
|
312
424
|
results,
|
|
425
|
+
...(linkedLocalPersistence
|
|
426
|
+
? {
|
|
427
|
+
persistence: linkedLocalPersistence.status,
|
|
428
|
+
path: linkedLocalPersistence.path,
|
|
429
|
+
runtime: linkedLocalPersistence.runtime,
|
|
430
|
+
}
|
|
431
|
+
: {}),
|
|
432
|
+
contentFidelity: inspectNfmFidelity(content),
|
|
313
433
|
...(creativeContext
|
|
314
434
|
? {
|
|
315
435
|
contextMode: creativeContext.contextMode,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
parseContentSourceFile,
|
|
16
16
|
type ParsedContentSourceFile,
|
|
17
17
|
} from "../shared/content-source.js";
|
|
18
|
+
import { inspectNfmFidelity } from "../shared/nfm.js";
|
|
18
19
|
import {
|
|
19
20
|
favoriteDocumentIds,
|
|
20
21
|
setFavoriteMembership,
|
|
@@ -63,6 +64,10 @@ function canEditRole(role: string) {
|
|
|
63
64
|
return ROLE_RANK[role as keyof typeof ROLE_RANK] >= ROLE_RANK.editor;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
function canAdminRole(role: string) {
|
|
68
|
+
return ROLE_RANK[role as keyof typeof ROLE_RANK] >= ROLE_RANK.admin;
|
|
69
|
+
}
|
|
70
|
+
|
|
66
71
|
function normalizedFileEntries(files: Record<string, string>) {
|
|
67
72
|
return Object.entries(files)
|
|
68
73
|
.filter(([filePath]) => isContentSourcePath(filePath))
|
|
@@ -214,6 +219,9 @@ export default defineAction({
|
|
|
214
219
|
const parsed: ParsedContentSourceFile[] = entries.map(
|
|
215
220
|
([filePath, source]) => parseContentSourceFile(filePath, source),
|
|
216
221
|
);
|
|
222
|
+
const fidelityByPath = new Map(
|
|
223
|
+
parsed.map((file) => [file.path, inspectNfmFidelity(file.content)]),
|
|
224
|
+
);
|
|
217
225
|
const importPaths = [...new Set(parsed.map((file) => file.path))];
|
|
218
226
|
const existingLocalDocs =
|
|
219
227
|
importPaths.length > 0
|
|
@@ -292,6 +300,14 @@ export default defineAction({
|
|
|
292
300
|
});
|
|
293
301
|
continue;
|
|
294
302
|
}
|
|
303
|
+
const fidelity = fidelityByPath.get(file.path);
|
|
304
|
+
if (fidelity?.status === "failed") {
|
|
305
|
+
errors.push({
|
|
306
|
+
path: file.path,
|
|
307
|
+
reason: `Document conversion failed: ${fidelity.error ?? "unreadable content"}.`,
|
|
308
|
+
});
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
295
311
|
|
|
296
312
|
const sourceMatchedDocument = file.id
|
|
297
313
|
? null
|
|
@@ -337,6 +353,19 @@ export default defineAction({
|
|
|
337
353
|
const discoverabilityChanged =
|
|
338
354
|
file.hideFromSearch !== undefined &&
|
|
339
355
|
boolToInt(file.hideFromSearch) !== (existing.hideFromSearch ?? 0);
|
|
356
|
+
const visibilityChanged =
|
|
357
|
+
file.visibility !== undefined &&
|
|
358
|
+
file.visibility !== existing.visibility;
|
|
359
|
+
if (
|
|
360
|
+
visibilityChanged &&
|
|
361
|
+
(!existingRole || !canAdminRole(existingRole))
|
|
362
|
+
) {
|
|
363
|
+
skipped.push({
|
|
364
|
+
path: file.path,
|
|
365
|
+
reason: `Requires admin access to change visibility on document "${id}".`,
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
340
369
|
const sourceUpdates = localSourceFields(file.path, now);
|
|
341
370
|
const sourceChanged =
|
|
342
371
|
existing.sourceMode !== sourceUpdates.sourceMode ||
|
|
@@ -351,6 +380,7 @@ export default defineAction({
|
|
|
351
380
|
iconChanged ||
|
|
352
381
|
favoriteChanged ||
|
|
353
382
|
discoverabilityChanged ||
|
|
383
|
+
visibilityChanged ||
|
|
354
384
|
sourceChanged ||
|
|
355
385
|
spaceChanged;
|
|
356
386
|
|
|
@@ -379,6 +409,7 @@ export default defineAction({
|
|
|
379
409
|
if (discoverabilityChanged) {
|
|
380
410
|
updates.hideFromSearch = boolToInt(file.hideFromSearch);
|
|
381
411
|
}
|
|
412
|
+
if (visibilityChanged) updates.visibility = file.visibility;
|
|
382
413
|
Object.assign(updates, sourceUpdates);
|
|
383
414
|
|
|
384
415
|
await db
|
|
@@ -416,7 +447,7 @@ export default defineAction({
|
|
|
416
447
|
isFavorite: boolToInt(file.isFavorite),
|
|
417
448
|
hideFromSearch: boolToInt(file.hideFromSearch),
|
|
418
449
|
...localSourceFields(file.path, now),
|
|
419
|
-
visibility: "private",
|
|
450
|
+
visibility: file.visibility ?? "private",
|
|
420
451
|
createdAt: now,
|
|
421
452
|
updatedAt: now,
|
|
422
453
|
});
|
|
@@ -544,6 +575,7 @@ export default defineAction({
|
|
|
544
575
|
skipped,
|
|
545
576
|
errors,
|
|
546
577
|
idByPath: Object.fromEntries(idByPath),
|
|
578
|
+
fidelity: Object.fromEntries(fidelityByPath),
|
|
547
579
|
};
|
|
548
580
|
},
|
|
549
581
|
});
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
parseDocumentHideFromSearch,
|
|
20
20
|
} from "../server/lib/documents.js";
|
|
21
21
|
import type { DocumentUpdateResponse } from "../shared/api.js";
|
|
22
|
+
import { inspectNfmFidelity } from "../shared/nfm.js";
|
|
22
23
|
import {
|
|
23
24
|
lockPrimaryBlocksFields,
|
|
24
25
|
persistBlocksFieldIdentity,
|
|
@@ -778,6 +779,7 @@ export default defineAction({
|
|
|
778
779
|
canManage: canManageRole(access.role),
|
|
779
780
|
createdAt: doc.createdAt,
|
|
780
781
|
updatedAt: doc.updatedAt,
|
|
782
|
+
contentFidelity: inspectNfmFidelity(doc.content),
|
|
781
783
|
source: serializeDocumentSource(doc),
|
|
782
784
|
softDeletedDatabaseIds,
|
|
783
785
|
...(creativeContext
|
|
@@ -105,6 +105,7 @@ import { DocumentEditorSkeleton } from "./DocumentEditorSkeleton";
|
|
|
105
105
|
import { DocumentInfoPanel } from "./DocumentInfoPanel";
|
|
106
106
|
import { DocumentToolbar, type ToolbarBreadcrumbItem } from "./DocumentToolbar";
|
|
107
107
|
import { EmojiPicker } from "./EmojiPicker";
|
|
108
|
+
import { LinkedLocalDocumentAgentBridge } from "./LinkedLocalDocumentAgentBridge";
|
|
108
109
|
import {
|
|
109
110
|
classifyLocalSourceRead,
|
|
110
111
|
localSourceRevisionForQueuedEdit,
|
|
@@ -749,6 +750,13 @@ function DocumentEditorBody({
|
|
|
749
750
|
localTitleRef.current = localTitle;
|
|
750
751
|
const localContentRef = useRef(localContent);
|
|
751
752
|
localContentRef.current = localContent;
|
|
753
|
+
const getLinkedLocalEditorSnapshot = useCallback(
|
|
754
|
+
() => ({
|
|
755
|
+
title: localTitleRef.current,
|
|
756
|
+
content: localContentRef.current,
|
|
757
|
+
}),
|
|
758
|
+
[],
|
|
759
|
+
);
|
|
752
760
|
const localSourceWriteErrorShownRef = useRef(false);
|
|
753
761
|
const documentUpdatedAtRef = useRef<string | null>(
|
|
754
762
|
document.updatedAt ?? null,
|
|
@@ -1267,6 +1275,59 @@ function DocumentEditorBody({
|
|
|
1267
1275
|
};
|
|
1268
1276
|
}, [document.id, document.source, isLinkedLocalSourceDocument]);
|
|
1269
1277
|
|
|
1278
|
+
const handleLinkedLocalAgentPersistence = useCallback(
|
|
1279
|
+
(persisted: Document, revision?: DesktopContentFileRevision) => {
|
|
1280
|
+
localSourceRevisionRef.current = revision;
|
|
1281
|
+
localTitleRef.current = persisted.title;
|
|
1282
|
+
localContentRef.current = persisted.content;
|
|
1283
|
+
setLocalTitle(persisted.title);
|
|
1284
|
+
setLocalContent(persisted.content);
|
|
1285
|
+
setLocalContentUpdatedAt(persisted.updatedAt ?? new Date().toISOString());
|
|
1286
|
+
lastSavedTitleRef.current = {
|
|
1287
|
+
title: persisted.title,
|
|
1288
|
+
updatedAt: lastSavedTitleRef.current.updatedAt,
|
|
1289
|
+
};
|
|
1290
|
+
lastSavedContentRef.current = {
|
|
1291
|
+
content: persisted.content,
|
|
1292
|
+
updatedAt: lastSavedContentRef.current.updatedAt,
|
|
1293
|
+
};
|
|
1294
|
+
setLocalFileSyncRevision((revision) => revision + 1);
|
|
1295
|
+
setLocalSourceConflict(null);
|
|
1296
|
+
const sqlUpdatedAt = documentUpdatedAtRef.current;
|
|
1297
|
+
queryClient.setQueriesData(documentQueryFilter(documentId), (old) =>
|
|
1298
|
+
mergeDocumentIntoDocumentCache(old, {
|
|
1299
|
+
...persisted,
|
|
1300
|
+
updatedAt: sqlUpdatedAt ?? persisted.updatedAt,
|
|
1301
|
+
}),
|
|
1302
|
+
);
|
|
1303
|
+
},
|
|
1304
|
+
[documentId, queryClient],
|
|
1305
|
+
);
|
|
1306
|
+
|
|
1307
|
+
useEffect(() => {
|
|
1308
|
+
if (
|
|
1309
|
+
!isLinkedLocalSourceDocument ||
|
|
1310
|
+
document.title !== localTitleRef.current ||
|
|
1311
|
+
document.content !== localContentRef.current ||
|
|
1312
|
+
!document.updatedAt
|
|
1313
|
+
) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
lastSavedTitleRef.current = {
|
|
1317
|
+
title: document.title,
|
|
1318
|
+
updatedAt: document.updatedAt,
|
|
1319
|
+
};
|
|
1320
|
+
lastSavedContentRef.current = {
|
|
1321
|
+
content: document.content,
|
|
1322
|
+
updatedAt: document.updatedAt,
|
|
1323
|
+
};
|
|
1324
|
+
}, [
|
|
1325
|
+
document.content,
|
|
1326
|
+
document.title,
|
|
1327
|
+
document.updatedAt,
|
|
1328
|
+
isLinkedLocalSourceDocument,
|
|
1329
|
+
]);
|
|
1330
|
+
|
|
1270
1331
|
const saveDocumentImmediately = useCallback(
|
|
1271
1332
|
async (
|
|
1272
1333
|
title: string,
|
|
@@ -2024,6 +2085,13 @@ function DocumentEditorBody({
|
|
|
2024
2085
|
registry={contentBlockRegistry}
|
|
2025
2086
|
ctx={blockRenderContext}
|
|
2026
2087
|
>
|
|
2088
|
+
{isLinkedLocalSourceDocument && editorCanEdit ? (
|
|
2089
|
+
<LinkedLocalDocumentAgentBridge
|
|
2090
|
+
document={document}
|
|
2091
|
+
getEditorSnapshot={getLinkedLocalEditorSnapshot}
|
|
2092
|
+
onPersisted={handleLinkedLocalAgentPersistence}
|
|
2093
|
+
/>
|
|
2094
|
+
) : null}
|
|
2027
2095
|
<div
|
|
2028
2096
|
className="relative flex min-h-0 min-w-0 flex-1"
|
|
2029
2097
|
data-document-print-root
|