@agimon-ai/po-author 0.0.0-alpha.41 → 0.0.0-alpha.43
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/dist/extensions/native.mjs +3109 -0
- package/dist/extensions/native.mjs.map +1 -0
- package/dist/extensions/web.mjs +52 -17
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +27 -11
|
@@ -0,0 +1,3109 @@
|
|
|
1
|
+
import { apiResponse, createApiClient, decodeBase64, defineChannelFile, defineFileLinks, defineFill, defineGlobalStore, defineNativePlugin, defineSessionStore, defineSlotFile, defineToolRenderer, defineWebLifecycle, encodeBase64, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
|
|
2
|
+
import { Store } from "@tanstack/store";
|
|
3
|
+
import { Image } from "react-native-unistyles/components/native/Image";
|
|
4
|
+
import { AnnotationSurface, Badge, Button, EmptyState, Markdown, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, SectionLabel, SegmentedControl, Spinner, Textarea, TrashIcon, collapseLines, fontOf, resultTextLines, toolTone } from "@agimon-ai/po-native-components";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { AppState } from "react-native";
|
|
7
|
+
import { View } from "react-native-unistyles/components/native/View";
|
|
8
|
+
import { Text } from "react-native-unistyles/components/native/Text";
|
|
9
|
+
import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
|
|
10
|
+
import { Pressable } from "react-native-unistyles/components/native/Pressable";
|
|
11
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
12
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
//#region src/types/webAuthor.ts
|
|
14
|
+
const authorChannelType = "author_webmcp";
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/types/author.ts
|
|
17
|
+
const AUTHOR_MODE_ID = "author";
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorRuntime.ts
|
|
20
|
+
const AUTHOR_RUNTIME_BINDING_IDS = {
|
|
21
|
+
text: "author.viewport.text",
|
|
22
|
+
media: "author.viewport.media"
|
|
23
|
+
};
|
|
24
|
+
const runtimeIds = new Store(0);
|
|
25
|
+
function runtimePrefix() {
|
|
26
|
+
runtimeIds.setState((id) => id + 1);
|
|
27
|
+
return `a${runtimeIds.state.toString(36)}_`;
|
|
28
|
+
}
|
|
29
|
+
function assertProfile(profile) {
|
|
30
|
+
if (profile.id.trim() === "") throw new Error("Author profile id must not be empty");
|
|
31
|
+
if (profile.tools.length > 15) throw new Error("Author profiles may expose at most 15 tools");
|
|
32
|
+
const names = /* @__PURE__ */ new Set();
|
|
33
|
+
for (const tool of profile.tools) {
|
|
34
|
+
if (!/^[a-z][a-z0-9_]{0,29}$/.test(tool.name)) throw new Error(`Invalid Author tool name: ${tool.name}`);
|
|
35
|
+
if (names.has(tool.name)) throw new Error(`Duplicate Author tool name: ${tool.name}`);
|
|
36
|
+
names.add(tool.name);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function descriptorMatches(actual, expected) {
|
|
40
|
+
return actual.name === expected.name && actual.description === expected.description && JSON.stringify(actual.inputSchema) === JSON.stringify(expected.inputSchema);
|
|
41
|
+
}
|
|
42
|
+
function staleError() {
|
|
43
|
+
return /* @__PURE__ */ new Error("Author viewport catalog is stale");
|
|
44
|
+
}
|
|
45
|
+
function invocationSignal(catalogSignal, requestSignal) {
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
const abortCatalog = () => controller.abort(catalogSignal.reason);
|
|
48
|
+
const abortRequest = () => controller.abort(requestSignal.reason);
|
|
49
|
+
if (catalogSignal.aborted) abortCatalog();
|
|
50
|
+
else if (requestSignal.aborted) abortRequest();
|
|
51
|
+
else {
|
|
52
|
+
catalogSignal.addEventListener("abort", abortCatalog, { once: true });
|
|
53
|
+
requestSignal.addEventListener("abort", abortRequest, { once: true });
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
release: () => {
|
|
58
|
+
catalogSignal.removeEventListener("abort", abortCatalog);
|
|
59
|
+
requestSignal.removeEventListener("abort", abortRequest);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Owns page-lifetime WebMCP registrations. Profiles are replaced as one abortable catalog. */
|
|
64
|
+
var AuthorRuntime = class {
|
|
65
|
+
#runtime;
|
|
66
|
+
#registrationPrefix = runtimePrefix();
|
|
67
|
+
#context;
|
|
68
|
+
#active;
|
|
69
|
+
#pending;
|
|
70
|
+
#generation = 0;
|
|
71
|
+
#disposed = false;
|
|
72
|
+
constructor(runtime) {
|
|
73
|
+
this.#runtime = runtime;
|
|
74
|
+
}
|
|
75
|
+
async replaceProfiles(profiles) {
|
|
76
|
+
if (this.#disposed) throw new Error("Author runtime is disposed");
|
|
77
|
+
for (const profile of profiles) assertProfile(profile);
|
|
78
|
+
const profileMap = new Map(profiles.map((profile) => [profile.id, profile]));
|
|
79
|
+
const tools = profiles.flatMap((profile) => profile.tools);
|
|
80
|
+
if (new Set(tools.map((tool) => tool.name)).size !== tools.length) throw new Error("Author tool names must be unique across trusted profiles");
|
|
81
|
+
const context = this.#context ?? (await this.#runtime.acquireModelContext?.())?.modelContext;
|
|
82
|
+
if (context === void 0) throw new Error("Model Context is unavailable");
|
|
83
|
+
this.#context = context;
|
|
84
|
+
const registrations = new Map(tools.map((tool, index) => [tool.name, `${this.#registrationPrefix}${index.toString(36)}`]));
|
|
85
|
+
const generation = ++this.#generation;
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const candidate = {
|
|
88
|
+
generation,
|
|
89
|
+
controller,
|
|
90
|
+
profiles: profileMap,
|
|
91
|
+
registrations
|
|
92
|
+
};
|
|
93
|
+
const inactive = staleError();
|
|
94
|
+
this.#pending?.controller.abort(inactive);
|
|
95
|
+
this.#active?.controller.abort(inactive);
|
|
96
|
+
this.#pending = candidate;
|
|
97
|
+
this.#active = void 0;
|
|
98
|
+
const assertCurrent = () => {
|
|
99
|
+
if (controller.signal.aborted || this.#disposed || this.#active?.generation !== generation) throw staleError();
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
await Promise.all(tools.map(async (tool) => context.registerTool({
|
|
103
|
+
name: registrations.get(tool.name),
|
|
104
|
+
description: tool.description,
|
|
105
|
+
inputSchema: tool.inputSchema,
|
|
106
|
+
execute: async (input, options) => {
|
|
107
|
+
assertCurrent();
|
|
108
|
+
const parsed = JSON.parse(input);
|
|
109
|
+
assertCurrent();
|
|
110
|
+
const invocation = invocationSignal(controller.signal, options.signal);
|
|
111
|
+
try {
|
|
112
|
+
const result = await tool.execute(parsed, invocation.signal);
|
|
113
|
+
assertCurrent();
|
|
114
|
+
return result;
|
|
115
|
+
} finally {
|
|
116
|
+
invocation.release();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, { signal: controller.signal })));
|
|
120
|
+
const registered = await context.getTools();
|
|
121
|
+
if (!tools.every((tool) => registered.some((descriptor) => descriptorMatches(descriptor, {
|
|
122
|
+
...tool,
|
|
123
|
+
name: registrations.get(tool.name)
|
|
124
|
+
})))) throw new Error("Model Context did not confirm the Author catalog");
|
|
125
|
+
if (this.#disposed || generation !== this.#generation || this.#pending !== candidate) throw staleError();
|
|
126
|
+
this.#pending = void 0;
|
|
127
|
+
this.#active = candidate;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
controller.abort(error);
|
|
130
|
+
if (this.#pending === candidate) this.#pending = void 0;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
return () => {
|
|
134
|
+
if (this.#active?.generation !== generation) return;
|
|
135
|
+
controller.abort(staleError());
|
|
136
|
+
this.#active = void 0;
|
|
137
|
+
this.#generation += 1;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async execute(name, input, signal) {
|
|
141
|
+
const context = this.#context;
|
|
142
|
+
if (context === void 0 || this.#active === void 0) throw new Error("Author catalog is not registered");
|
|
143
|
+
const registration = this.#active.registrations.get(name);
|
|
144
|
+
if (registration === void 0) throw new Error(`Unknown Author capability: ${name}`);
|
|
145
|
+
return context.executeTool(registration, JSON.stringify(input), { signal });
|
|
146
|
+
}
|
|
147
|
+
dispose() {
|
|
148
|
+
if (this.#disposed) return;
|
|
149
|
+
this.#disposed = true;
|
|
150
|
+
this.#generation += 1;
|
|
151
|
+
this.#pending?.controller.abort(/* @__PURE__ */ new Error("Author runtime disposed"));
|
|
152
|
+
this.#active?.controller.abort(/* @__PURE__ */ new Error("Author runtime disposed"));
|
|
153
|
+
this.#pending = void 0;
|
|
154
|
+
this.#active = void 0;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorBrowserBridge.ts
|
|
159
|
+
const canvasKey = (sessionId, alias) => `${sessionId}\n${alias}`;
|
|
160
|
+
/** How long a refused viewport waits before asking again, so it takes over once the holder lets go. */
|
|
161
|
+
const REJECTED_RETRY_MS = 1e4;
|
|
162
|
+
/** Why the server refused a canvas here, usually another device holding the document for the agent. */
|
|
163
|
+
const authorCanvasRejections = new Store({});
|
|
164
|
+
const setRejection = (key, reason) => {
|
|
165
|
+
if (authorCanvasRejections.state[key] === reason) return;
|
|
166
|
+
authorCanvasRejections.setState((state) => {
|
|
167
|
+
const next = { ...state };
|
|
168
|
+
if (reason === void 0) delete next[key];
|
|
169
|
+
else next[key] = reason;
|
|
170
|
+
return next;
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const VISIBLE_ONLY = /* @__PURE__ */ new Set(["author_describe_grid", "author_resolve_grid_cell"]);
|
|
174
|
+
const documentProfiles = (profiles) => profiles.map((profile) => ({
|
|
175
|
+
...profile,
|
|
176
|
+
tools: profile.tools.filter((tool) => !VISIBLE_ONLY.has(tool.name))
|
|
177
|
+
}));
|
|
178
|
+
var AuthorBrowserBridge = class {
|
|
179
|
+
#host;
|
|
180
|
+
#canvases = /* @__PURE__ */ new Map();
|
|
181
|
+
#parked = /* @__PURE__ */ new Map();
|
|
182
|
+
#opening = /* @__PURE__ */ new Map();
|
|
183
|
+
#modeWatches = /* @__PURE__ */ new Map();
|
|
184
|
+
#generation = 0;
|
|
185
|
+
#disposed = false;
|
|
186
|
+
#releaseConnected;
|
|
187
|
+
constructor(host) {
|
|
188
|
+
this.#host = host;
|
|
189
|
+
this.#releaseConnected = host.onHubConnected(() => {
|
|
190
|
+
for (const canvas of this.#canvases.values()) {
|
|
191
|
+
canvas.ownerToken = void 0;
|
|
192
|
+
canvas.catalogToken = void 0;
|
|
193
|
+
this.#register(canvas);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async open(sessionId, alias, profiles) {
|
|
198
|
+
if (this.#disposed) throw new Error("Author browser bridge is disposed");
|
|
199
|
+
this.#watch(sessionId);
|
|
200
|
+
const key = canvasKey(sessionId, alias);
|
|
201
|
+
if (this.#canvases.has(key)) return;
|
|
202
|
+
const parkedCanvas = this.#parked.get(key);
|
|
203
|
+
if (parkedCanvas !== void 0) {
|
|
204
|
+
if (this.#agentEnabled(sessionId)) await this.#openLive(parkedCanvas);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const pending = this.#opening.get(key);
|
|
208
|
+
if (pending !== void 0) return await pending.promise;
|
|
209
|
+
const parked = {
|
|
210
|
+
sessionId,
|
|
211
|
+
alias,
|
|
212
|
+
profiles: documentProfiles(profiles)
|
|
213
|
+
};
|
|
214
|
+
if (!this.#agentEnabled(sessionId)) {
|
|
215
|
+
this.#parked.set(key, parked);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
await this.#openLive(parked);
|
|
219
|
+
}
|
|
220
|
+
async #openLive(parked) {
|
|
221
|
+
const { sessionId, alias } = parked;
|
|
222
|
+
const profiles = parked.focus?.profiles ?? parked.profiles;
|
|
223
|
+
const key = canvasKey(sessionId, alias);
|
|
224
|
+
if (this.#disposed || this.#canvases.has(key)) return;
|
|
225
|
+
const pending = this.#opening.get(key);
|
|
226
|
+
if (pending !== void 0) return await pending.promise;
|
|
227
|
+
if (!this.#agentEnabled(sessionId)) {
|
|
228
|
+
this.#parked.set(key, parked);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.#parked.delete(key);
|
|
232
|
+
const runtime = new AuthorRuntime(this.#host);
|
|
233
|
+
const generation = ++this.#generation;
|
|
234
|
+
let opening;
|
|
235
|
+
opening = (async () => {
|
|
236
|
+
let release;
|
|
237
|
+
try {
|
|
238
|
+
release = await runtime.replaceProfiles(profiles);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (this.#disposed || !this.#agentEnabled(sessionId) || this.#opening.get(key)?.promise !== opening) return;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
if (this.#disposed || !this.#agentEnabled(sessionId) || this.#canvases.has(key) || this.#opening.get(key)?.promise !== opening) {
|
|
244
|
+
release();
|
|
245
|
+
runtime.dispose();
|
|
246
|
+
if (!this.#disposed && !this.#agentEnabled(sessionId)) this.#parked.set(key, parked);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const canvas = {
|
|
250
|
+
sessionId,
|
|
251
|
+
alias,
|
|
252
|
+
generation,
|
|
253
|
+
profiles,
|
|
254
|
+
intent: parked,
|
|
255
|
+
runtime,
|
|
256
|
+
release,
|
|
257
|
+
pending: /* @__PURE__ */ new Map(),
|
|
258
|
+
replacementId: 0
|
|
259
|
+
};
|
|
260
|
+
this.#canvases.set(key, canvas);
|
|
261
|
+
const currentProfiles = parked.focus?.profiles ?? parked.profiles;
|
|
262
|
+
if (currentProfiles !== profiles) await this.#replace(canvas, currentProfiles);
|
|
263
|
+
else this.#register(canvas);
|
|
264
|
+
})();
|
|
265
|
+
this.#opening.set(key, {
|
|
266
|
+
promise: opening,
|
|
267
|
+
runtime,
|
|
268
|
+
parked
|
|
269
|
+
});
|
|
270
|
+
try {
|
|
271
|
+
await opening;
|
|
272
|
+
} finally {
|
|
273
|
+
if (this.#opening.get(key)?.promise === opening) this.#opening.delete(key);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async focus(sessionId, alias, profiles) {
|
|
277
|
+
await this.open(sessionId, alias, profiles);
|
|
278
|
+
const key = canvasKey(sessionId, alias);
|
|
279
|
+
const canvas = this.#canvases.get(key);
|
|
280
|
+
const intent = canvas?.intent ?? this.#parked.get(key) ?? this.#opening.get(key)?.parked;
|
|
281
|
+
if (intent === void 0) return () => void 0;
|
|
282
|
+
const focus = { profiles };
|
|
283
|
+
intent.focus = focus;
|
|
284
|
+
if (canvas !== void 0) await this.#replace(canvas, profiles);
|
|
285
|
+
return () => {
|
|
286
|
+
if (intent.focus !== focus) return;
|
|
287
|
+
intent.focus = void 0;
|
|
288
|
+
const current = this.#canvases.get(key);
|
|
289
|
+
if (current?.intent === intent) this.#replace(current, intent.profiles);
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
drop(sessionId, alias) {
|
|
293
|
+
for (const [key, entry] of this.#opening) {
|
|
294
|
+
if (entry.parked.sessionId !== sessionId || alias !== void 0 && entry.parked.alias !== alias) continue;
|
|
295
|
+
this.#opening.delete(key);
|
|
296
|
+
entry.runtime.dispose();
|
|
297
|
+
}
|
|
298
|
+
for (const [key, parked] of this.#parked) {
|
|
299
|
+
if (parked.sessionId !== sessionId || alias !== void 0 && parked.alias !== alias) continue;
|
|
300
|
+
this.#parked.delete(key);
|
|
301
|
+
}
|
|
302
|
+
for (const canvas of this.#canvases.values()) {
|
|
303
|
+
if (canvas.sessionId !== sessionId || alias !== void 0 && canvas.alias !== alias) continue;
|
|
304
|
+
this.#dropLive(canvas);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
apply(sessionId, message) {
|
|
308
|
+
if (!this.#agentEnabled(sessionId) || message.alias === void 0) return;
|
|
309
|
+
const canvas = this.#canvases.get(canvasKey(sessionId, message.alias));
|
|
310
|
+
if (canvas === void 0) return;
|
|
311
|
+
if (message.kind === "rejected") {
|
|
312
|
+
setRejection(canvasKey(sessionId, message.alias), message.reason);
|
|
313
|
+
clearTimeout(canvas.leaseTimer);
|
|
314
|
+
canvas.leaseTimer = setTimeout(() => this.#register(canvas), REJECTED_RETRY_MS);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (message.generation !== canvas.generation || message.ownerToken !== canvas.ownerToken && canvas.ownerToken !== void 0) return;
|
|
318
|
+
if (message.kind === "accepted") {
|
|
319
|
+
setRejection(canvasKey(sessionId, message.alias), void 0);
|
|
320
|
+
canvas.ownerToken = message.ownerToken;
|
|
321
|
+
clearTimeout(canvas.leaseTimer);
|
|
322
|
+
canvas.leaseTimer = setTimeout(() => this.#register(canvas), Math.max(1e3, Math.floor(message.leaseMs / 2)));
|
|
323
|
+
if (message.catalogToken === void 0) this.#sendCatalog(canvas);
|
|
324
|
+
else canvas.catalogToken = message.catalogToken;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (message.catalogToken !== canvas.catalogToken) return;
|
|
328
|
+
if (message.kind === "cancel") {
|
|
329
|
+
canvas.pending.get(message.requestId)?.abort(/* @__PURE__ */ new Error("Author tool request cancelled"));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (message.kind !== "request" || canvas.pending.has(message.requestId)) return;
|
|
333
|
+
const controller = new AbortController();
|
|
334
|
+
canvas.pending.set(message.requestId, controller);
|
|
335
|
+
canvas.runtime.execute(message.name, message.arguments, controller.signal).then((result) => {
|
|
336
|
+
if (!this.#current(canvas, message.requestId, controller)) return;
|
|
337
|
+
if (controller.signal.aborted) this.#sendCancelled(canvas, message.requestId);
|
|
338
|
+
else this.#send(canvas, {
|
|
339
|
+
kind: "result",
|
|
340
|
+
alias: canvas.alias,
|
|
341
|
+
generation: canvas.generation,
|
|
342
|
+
ownerToken: canvas.ownerToken,
|
|
343
|
+
catalogToken: canvas.catalogToken,
|
|
344
|
+
requestId: message.requestId,
|
|
345
|
+
result
|
|
346
|
+
});
|
|
347
|
+
}).catch((error) => {
|
|
348
|
+
if (!this.#current(canvas, message.requestId, controller)) return;
|
|
349
|
+
if (controller.signal.aborted) this.#sendCancelled(canvas, message.requestId);
|
|
350
|
+
else {
|
|
351
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
352
|
+
const code = /^([A-Z][A-Z_]+):/u.exec(messageText)?.[1] ?? "AUTHOR_TOOL_ERROR";
|
|
353
|
+
this.#send(canvas, {
|
|
354
|
+
kind: "result",
|
|
355
|
+
alias: canvas.alias,
|
|
356
|
+
generation: canvas.generation,
|
|
357
|
+
ownerToken: canvas.ownerToken,
|
|
358
|
+
catalogToken: canvas.catalogToken,
|
|
359
|
+
requestId: message.requestId,
|
|
360
|
+
result: { error: {
|
|
361
|
+
code,
|
|
362
|
+
message: messageText
|
|
363
|
+
} }
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}).finally(() => {
|
|
367
|
+
if (canvas.pending.get(message.requestId) === controller) canvas.pending.delete(message.requestId);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
activeView(sessionId) {
|
|
371
|
+
const active = [...this.#canvases.values()].filter((canvas) => canvas.sessionId === sessionId && canvas.catalogToken);
|
|
372
|
+
return active.length > 0 ? {
|
|
373
|
+
activation: "active",
|
|
374
|
+
capabilityCount: active.reduce((count, canvas) => count + canvas.profiles.reduce((total, profile) => total + profile.tools.length, 0), 0)
|
|
375
|
+
} : {
|
|
376
|
+
activation: "inactive",
|
|
377
|
+
capabilityCount: 0
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
dispose() {
|
|
381
|
+
if (this.#disposed) return;
|
|
382
|
+
this.#disposed = true;
|
|
383
|
+
this.#releaseConnected();
|
|
384
|
+
for (const release of this.#modeWatches.values()) release();
|
|
385
|
+
this.#modeWatches.clear();
|
|
386
|
+
for (const canvas of this.#canvases.values()) this.#dropLive(canvas);
|
|
387
|
+
this.#parked.clear();
|
|
388
|
+
for (const entry of this.#opening.values()) entry.runtime.dispose();
|
|
389
|
+
this.#opening.clear();
|
|
390
|
+
}
|
|
391
|
+
#agentEnabled(sessionId) {
|
|
392
|
+
return this.#host.minorModes?.active(sessionId).includes(AUTHOR_MODE_ID) === true;
|
|
393
|
+
}
|
|
394
|
+
#watch(sessionId) {
|
|
395
|
+
if (this.#modeWatches.has(sessionId) || this.#host.minorModes === void 0) return;
|
|
396
|
+
const release = this.#host.minorModes.subscribe(sessionId, () => this.#modeChanged(sessionId));
|
|
397
|
+
this.#modeWatches.set(sessionId, release);
|
|
398
|
+
}
|
|
399
|
+
#modeChanged(sessionId) {
|
|
400
|
+
if (this.#disposed) return;
|
|
401
|
+
if (this.#agentEnabled(sessionId)) {
|
|
402
|
+
for (const parked of this.#parked.values()) {
|
|
403
|
+
if (parked.sessionId !== sessionId) continue;
|
|
404
|
+
this.#openLive(parked).catch((error) => console.warn(`Author canvas '${parked.alias}' could not enable browser tools: ${error instanceof Error ? error.message : String(error)}`));
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
for (const [key, entry] of this.#opening) {
|
|
409
|
+
if (entry.parked.sessionId !== sessionId) continue;
|
|
410
|
+
this.#opening.delete(key);
|
|
411
|
+
this.#parked.set(key, entry.parked);
|
|
412
|
+
entry.runtime.dispose();
|
|
413
|
+
}
|
|
414
|
+
for (const canvas of this.#canvases.values()) {
|
|
415
|
+
if (canvas.sessionId !== sessionId) continue;
|
|
416
|
+
this.#parked.set(canvasKey(canvas.sessionId, canvas.alias), canvas.intent);
|
|
417
|
+
this.#dropLive(canvas);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
#dropLive(canvas) {
|
|
421
|
+
this.#canvases.delete(canvasKey(canvas.sessionId, canvas.alias));
|
|
422
|
+
setRejection(canvasKey(canvas.sessionId, canvas.alias), void 0);
|
|
423
|
+
this.#release(canvas);
|
|
424
|
+
canvas.runtime.dispose();
|
|
425
|
+
}
|
|
426
|
+
async #replace(canvas, profiles) {
|
|
427
|
+
const key = canvasKey(canvas.sessionId, canvas.alias);
|
|
428
|
+
if (this.#disposed || !this.#agentEnabled(canvas.sessionId) || this.#canvases.get(key) !== canvas) return;
|
|
429
|
+
this.#release(canvas);
|
|
430
|
+
const generation = canvas.generation;
|
|
431
|
+
const replacementId = ++canvas.replacementId;
|
|
432
|
+
let release;
|
|
433
|
+
try {
|
|
434
|
+
release = await canvas.runtime.replaceProfiles(profiles);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (canvas.replacementId !== replacementId || this.#canvases.get(key) !== canvas) return;
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
if (this.#disposed || this.#canvases.get(key) !== canvas || canvas.generation !== generation || canvas.replacementId !== replacementId) {
|
|
440
|
+
release();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
canvas.release = release;
|
|
444
|
+
canvas.profiles = profiles;
|
|
445
|
+
this.#register(canvas);
|
|
446
|
+
}
|
|
447
|
+
#register(canvas) {
|
|
448
|
+
if (this.#agentEnabled(canvas.sessionId) && this.#canvases.get(canvasKey(canvas.sessionId, canvas.alias)) === canvas) this.#send(canvas, {
|
|
449
|
+
kind: "register",
|
|
450
|
+
alias: canvas.alias,
|
|
451
|
+
generation: canvas.generation
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
#sendCatalog(canvas) {
|
|
455
|
+
this.#send(canvas, {
|
|
456
|
+
kind: "catalog",
|
|
457
|
+
alias: canvas.alias,
|
|
458
|
+
generation: canvas.generation,
|
|
459
|
+
ownerToken: canvas.ownerToken,
|
|
460
|
+
tools: canvas.profiles.flatMap((profile) => profile.tools.map((tool) => ({
|
|
461
|
+
name: tool.name,
|
|
462
|
+
label: tool.label ?? tool.name,
|
|
463
|
+
description: tool.description,
|
|
464
|
+
inputSchema: tool.inputSchema
|
|
465
|
+
})))
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
#sendCancelled(canvas, requestId) {
|
|
469
|
+
this.#send(canvas, {
|
|
470
|
+
kind: "cancelled",
|
|
471
|
+
alias: canvas.alias,
|
|
472
|
+
generation: canvas.generation,
|
|
473
|
+
ownerToken: canvas.ownerToken,
|
|
474
|
+
catalogToken: canvas.catalogToken,
|
|
475
|
+
requestId
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
#send(canvas, payload) {
|
|
479
|
+
const workspaceId = this.#host.mount?.scope === "session" ? this.#host.mount.workspaceId : void 0;
|
|
480
|
+
const mount = workspaceId ? {
|
|
481
|
+
scope: "workspace",
|
|
482
|
+
workspaceId
|
|
483
|
+
} : { scope: "global" };
|
|
484
|
+
this.#host.invokeServerMethod({
|
|
485
|
+
mount,
|
|
486
|
+
service: "author.bridge",
|
|
487
|
+
method: "send",
|
|
488
|
+
input: {
|
|
489
|
+
sessionId: canvas.sessionId,
|
|
490
|
+
message: payload
|
|
491
|
+
}
|
|
492
|
+
}).catch((error) => console.warn(`Author canvas '${canvas.alias}' could not send '${payload.kind}': ${error instanceof Error ? error.message : String(error)}`));
|
|
493
|
+
}
|
|
494
|
+
#current(canvas, requestId, controller) {
|
|
495
|
+
return this.#canvases.get(canvasKey(canvas.sessionId, canvas.alias)) === canvas && canvas.pending.get(requestId) === controller;
|
|
496
|
+
}
|
|
497
|
+
#release(canvas) {
|
|
498
|
+
clearTimeout(canvas.leaseTimer);
|
|
499
|
+
canvas.leaseTimer = void 0;
|
|
500
|
+
this.#send(canvas, {
|
|
501
|
+
kind: "release",
|
|
502
|
+
alias: canvas.alias,
|
|
503
|
+
generation: canvas.generation
|
|
504
|
+
});
|
|
505
|
+
canvas.release();
|
|
506
|
+
for (const controller of canvas.pending.values()) controller.abort(/* @__PURE__ */ new Error("Author viewport changed"));
|
|
507
|
+
canvas.pending.clear();
|
|
508
|
+
canvas.ownerToken = void 0;
|
|
509
|
+
canvas.catalogToken = void 0;
|
|
510
|
+
canvas.generation = ++this.#generation;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const authorBridgeRuntimes = new Store(/* @__PURE__ */ new Map());
|
|
514
|
+
const bridgeFor = (sessionId) => authorBridgeRuntimes.state.get(sessionId) ?? authorBridgeRuntimes.state.get("global");
|
|
515
|
+
function startAuthorBrowserBridge(runtime) {
|
|
516
|
+
const key = runtime.mount?.scope === "session" ? runtime.mount.sessionId : "global";
|
|
517
|
+
authorBridgeRuntimes.state.get(key)?.dispose();
|
|
518
|
+
const next = new AuthorBrowserBridge(runtime);
|
|
519
|
+
authorBridgeRuntimes.setState((state) => new Map(state).set(key, next));
|
|
520
|
+
return () => {
|
|
521
|
+
if (authorBridgeRuntimes.state.get(key) !== next) return;
|
|
522
|
+
next.dispose();
|
|
523
|
+
authorBridgeRuntimes.setState((state) => {
|
|
524
|
+
const remaining = new Map(state);
|
|
525
|
+
remaining.delete(key);
|
|
526
|
+
return remaining;
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
async function openAuthorCanvas(sessionId, alias, profiles) {
|
|
531
|
+
await bridgeFor(sessionId)?.open(sessionId, alias, profiles);
|
|
532
|
+
}
|
|
533
|
+
function applyAuthorHubMessage(sessionId, message) {
|
|
534
|
+
bridgeFor(sessionId)?.apply(sessionId, message);
|
|
535
|
+
}
|
|
536
|
+
function dropAuthorViewportSession(sessionId, alias) {
|
|
537
|
+
bridgeFor(sessionId)?.drop(sessionId, alias);
|
|
538
|
+
}
|
|
539
|
+
/** Why the agent cannot use this canvas here, such as another device holding it; undefined when it can. */
|
|
540
|
+
function authorCanvasRejection(sessionId, alias) {
|
|
541
|
+
return authorCanvasRejections.state[canvasKey(sessionId, alias)];
|
|
542
|
+
}
|
|
543
|
+
function subscribeAuthorCanvasRejections(listener) {
|
|
544
|
+
const subscription = authorCanvasRejections.subscribe(listener);
|
|
545
|
+
return () => subscription.unsubscribe();
|
|
546
|
+
}
|
|
547
|
+
function authorBridgeView(sessionId) {
|
|
548
|
+
return bridgeFor(sessionId)?.activeView(sessionId) ?? {
|
|
549
|
+
activation: "inactive",
|
|
550
|
+
capabilityCount: 0
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/_lib/authorHubMessage.ts
|
|
555
|
+
function isRecord(value) {
|
|
556
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
557
|
+
}
|
|
558
|
+
/** Gates one Author hub frame for the web and native channels alike: a bridge message, a view, or nothing. */
|
|
559
|
+
function parseAuthorHubMessage(input) {
|
|
560
|
+
if (!isRecord(input)) return null;
|
|
561
|
+
if ((input.activation === "active" || input.activation === "inactive") && Number.isSafeInteger(input.capabilityCount) && input.capabilityCount >= 0) return input;
|
|
562
|
+
if (typeof input.kind !== "string") return null;
|
|
563
|
+
if (input.kind === "rejected") return typeof input.reason === "string" ? input : null;
|
|
564
|
+
if (!Number.isSafeInteger(input.generation) || typeof input.ownerToken !== "string") return null;
|
|
565
|
+
if (input.kind === "accepted") return typeof input.leaseMs === "number" && (input.catalogToken === void 0 || typeof input.catalogToken === "string") ? input : null;
|
|
566
|
+
if ((input.kind === "request" || input.kind === "cancel") && typeof input.catalogToken === "string" && typeof input.requestId === "string") {
|
|
567
|
+
if (input.kind === "cancel") return input;
|
|
568
|
+
return typeof input.name === "string" && isRecord(input.arguments) ? input : null;
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/_lib/authorStore.native.ts
|
|
574
|
+
/**
|
|
575
|
+
* The native copy of authorStore.ts. The phone keeps no grid or workspace
|
|
576
|
+
* annotations, so a dropped session only releases its canvases.
|
|
577
|
+
*/
|
|
578
|
+
const author = defineSessionStore({
|
|
579
|
+
activation: "inactive",
|
|
580
|
+
capabilityCount: 0
|
|
581
|
+
});
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/author-webmcp.native.ts
|
|
584
|
+
var author_webmcp_native_default = defineChannelFile({
|
|
585
|
+
channel: authorChannelType,
|
|
586
|
+
parse: parseAuthorHubMessage,
|
|
587
|
+
apply(sessionId, message) {
|
|
588
|
+
if ("activation" in message) {
|
|
589
|
+
author.update(sessionId, () => message);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
applyAuthorHubMessage(sessionId, message);
|
|
593
|
+
author.update(sessionId, () => authorBridgeView(sessionId));
|
|
594
|
+
},
|
|
595
|
+
drop(sessionId) {
|
|
596
|
+
dropAuthorViewportSession(sessionId);
|
|
597
|
+
author.drop(sessionId);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/constants/author.ts
|
|
602
|
+
const AUTHOR_FILE_TAB_PREFIX = "author-file-";
|
|
603
|
+
/** Names a file in the session's artifact folder rather than the working tree. Artifacts open read-only. */
|
|
604
|
+
const AUTHOR_ARTIFACT_PREFIX = "artifact:";
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/types/authorPreview.ts
|
|
607
|
+
function parseAuthorOpenSourceAction(input) {
|
|
608
|
+
if (typeof input !== "object" || input === null) return null;
|
|
609
|
+
const candidate = input;
|
|
610
|
+
return candidate.version === 1 && typeof candidate.createTab === "function" ? candidate : null;
|
|
611
|
+
}
|
|
612
|
+
function parseAuthorPreviewAction(input) {
|
|
613
|
+
if (typeof input !== "object" || input === null) return null;
|
|
614
|
+
const candidate = input;
|
|
615
|
+
if (candidate.version !== 1 || typeof candidate.label !== "string" || candidate.label.trim() === "") return null;
|
|
616
|
+
if (candidate.detail !== void 0 && typeof candidate.detail !== "string") return null;
|
|
617
|
+
if (typeof candidate.createTab !== "function") return null;
|
|
618
|
+
if (candidate.supportsSource !== void 0 && typeof candidate.supportsSource !== "function") return null;
|
|
619
|
+
if (candidate.embeddedPanel !== void 0 && typeof candidate.embeddedPanel !== "function") return null;
|
|
620
|
+
return candidate;
|
|
621
|
+
}
|
|
622
|
+
const authorPreviewActionSlot = {
|
|
623
|
+
slot: "author.preview-action",
|
|
624
|
+
parse: parseAuthorPreviewAction
|
|
625
|
+
};
|
|
626
|
+
/** Keep freehand evidence small and source-relative. Reject malformed geometry rather than degrading it into a crop. */
|
|
627
|
+
function validAuthorStroke(value) {
|
|
628
|
+
return Array.isArray(value) && value.length >= 2 && value.length <= 128 && value.some((point) => point.x !== value[0]?.x || point.y !== value[0]?.y) && value.every((point) => typeof point === "object" && point !== null && Number.isFinite(point.x) && Number.isFinite(point.y) && point.x >= 0 && point.x <= 1 && point.y >= 0 && point.y <= 1);
|
|
629
|
+
}
|
|
630
|
+
function authorStrokeBounds(points) {
|
|
631
|
+
if (!validAuthorStroke(points)) return null;
|
|
632
|
+
const left = Math.min(...points.map((point) => point.x));
|
|
633
|
+
const top = Math.min(...points.map((point) => point.y));
|
|
634
|
+
const right = Math.max(...points.map((point) => point.x));
|
|
635
|
+
const bottom = Math.max(...points.map((point) => point.y));
|
|
636
|
+
const x = Math.max(0, Math.min(left, .999));
|
|
637
|
+
const y = Math.max(0, Math.min(top, .999));
|
|
638
|
+
return {
|
|
639
|
+
x,
|
|
640
|
+
y,
|
|
641
|
+
width: Math.min(1 - x, Math.max(right - x, .001)),
|
|
642
|
+
height: Math.min(1 - y, Math.max(bottom - y, .001))
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/** Clamp a drag to the source element, including drags made in reverse. */
|
|
646
|
+
function normalizedAuthorRectangle(bounds, drag) {
|
|
647
|
+
const width = bounds.right - bounds.left;
|
|
648
|
+
const height = bounds.bottom - bounds.top;
|
|
649
|
+
if (![...Object.values(bounds), ...Object.values(drag)].every(Number.isFinite) || width <= 0 || height <= 0) return null;
|
|
650
|
+
const left = Math.max(bounds.left, Math.min(drag.left, drag.right));
|
|
651
|
+
const right = Math.min(bounds.right, Math.max(drag.left, drag.right));
|
|
652
|
+
const top = Math.max(bounds.top, Math.min(drag.top, drag.bottom));
|
|
653
|
+
const bottom = Math.min(bounds.bottom, Math.max(drag.top, drag.bottom));
|
|
654
|
+
if (right <= left || bottom <= top) return null;
|
|
655
|
+
return {
|
|
656
|
+
x: (left - bounds.left) / width,
|
|
657
|
+
y: (top - bounds.top) / height,
|
|
658
|
+
width: (right - left) / width,
|
|
659
|
+
height: (bottom - top) / height
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const AUTHOR_QUOTE_MAX_BYTES = 4096;
|
|
663
|
+
function bytes$1(value) {
|
|
664
|
+
let length = 0;
|
|
665
|
+
for (const character of value) {
|
|
666
|
+
const code = character.codePointAt(0);
|
|
667
|
+
length += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
|
|
668
|
+
}
|
|
669
|
+
return length;
|
|
670
|
+
}
|
|
671
|
+
/** Capture DTOs are JSON; detach nested provenance and anchors without host clone APIs. */
|
|
672
|
+
function cloneJson(value) {
|
|
673
|
+
return JSON.parse(JSON.stringify(value));
|
|
674
|
+
}
|
|
675
|
+
function utf8Prefix(value, limit) {
|
|
676
|
+
if (bytes$1(value) <= limit) return value;
|
|
677
|
+
let low = 0;
|
|
678
|
+
let high = value.length;
|
|
679
|
+
while (low < high) {
|
|
680
|
+
const middle = Math.ceil((low + high) / 2);
|
|
681
|
+
if (bytes$1(value.slice(0, middle)) <= limit) low = middle;
|
|
682
|
+
else high = middle - 1;
|
|
683
|
+
}
|
|
684
|
+
return value.slice(0, low);
|
|
685
|
+
}
|
|
686
|
+
function createAuthorCapturePacket(captureId, capturedAt, document, regions, alias) {
|
|
687
|
+
if (regions.length === 0 || regions.length > 16) throw new Error("Capture requires between 1 and 16 regions.");
|
|
688
|
+
if (document.kind === "story-preview" && document.storyPreview === void 0) throw new Error("Story preview capture requires source provenance.");
|
|
689
|
+
const packet = {
|
|
690
|
+
version: 1,
|
|
691
|
+
captureId,
|
|
692
|
+
capturedAt,
|
|
693
|
+
document: {
|
|
694
|
+
path: document.path,
|
|
695
|
+
...alias === void 0 ? {} : { alias },
|
|
696
|
+
kind: document.kind,
|
|
697
|
+
revision: document.version,
|
|
698
|
+
...document.sourceSha256 === void 0 ? {} : { sourceSha256: document.sourceSha256 },
|
|
699
|
+
...document.storyPreview === void 0 ? {} : { storyPreview: cloneJson(document.storyPreview) },
|
|
700
|
+
...document.imageArtifact === void 0 ? {} : { imageArtifact: { ...document.imageArtifact } },
|
|
701
|
+
...document.markdownMedia === void 0 ? {} : { markdownMedia: { ...document.markdownMedia } }
|
|
702
|
+
},
|
|
703
|
+
regions: regions.map((region, index) => {
|
|
704
|
+
if (region.documentPath !== document.path || region.revision !== document.version || region.sourceSha256 !== document.sourceSha256) throw new Error(`Region ${String(index + 1)} is stale for the focused document.`);
|
|
705
|
+
if (region.stroke !== void 0 && (!validAuthorStroke(region.stroke) || !("rect" in region.anchor) || region.mode !== "region")) throw new Error(`Region ${String(index + 1)} has invalid feedback stroke geometry.`);
|
|
706
|
+
if (region.comment.trim() === "") throw new Error(`Region ${String(index + 1)} requires a comment.`);
|
|
707
|
+
if (bytes$1(region.comment) > 2048) throw new Error(`Region ${String(index + 1)} comment exceeds 2 KiB.`);
|
|
708
|
+
return {
|
|
709
|
+
id: region.id,
|
|
710
|
+
ordinal: index + 1,
|
|
711
|
+
comment: region.comment,
|
|
712
|
+
version: region.version ?? 1,
|
|
713
|
+
mode: region.mode ?? ("point" in region.anchor ? "point" : "region"),
|
|
714
|
+
...region.quote === void 0 ? {} : { quote: utf8Prefix(region.quote, AUTHOR_QUOTE_MAX_BYTES) },
|
|
715
|
+
anchor: cloneJson(region.anchor),
|
|
716
|
+
...region.stroke === void 0 ? {} : { stroke: region.stroke.map((point) => ({ ...point })) },
|
|
717
|
+
viewport: { ...region.viewport },
|
|
718
|
+
...region.voiceGrid === void 0 ? {} : { voiceGrid: { ...region.voiceGrid } }
|
|
719
|
+
};
|
|
720
|
+
})
|
|
721
|
+
};
|
|
722
|
+
if (bytes$1(JSON.stringify(packet)) > 65536) throw new Error("Author capture packet exceeds 64 KiB.");
|
|
723
|
+
return packet;
|
|
724
|
+
}
|
|
725
|
+
function percent$1(value) {
|
|
726
|
+
return `${String(Math.round(value * 1e3) / 10)}%`;
|
|
727
|
+
}
|
|
728
|
+
function rectLocation(rect) {
|
|
729
|
+
return `x ${percent$1(rect.x)}, y ${percent$1(rect.y)}, w ${percent$1(rect.width)}, h ${percent$1(rect.height)}`;
|
|
730
|
+
}
|
|
731
|
+
function anchorLocation(anchor) {
|
|
732
|
+
switch (anchor.kind) {
|
|
733
|
+
case "text-range": return anchor.startLine === anchor.endLine ? `line ${String(anchor.startLine)}` : `lines ${String(anchor.startLine)}-${String(anchor.endLine)}`;
|
|
734
|
+
case "cell": return `${anchor.sheet === void 0 ? "" : `${anchor.sheet}!`}${anchor.location}`;
|
|
735
|
+
case "slide-element": return `slide ${String(anchor.slide)}: ${anchor.location}`;
|
|
736
|
+
case "image-rect": return `image ${rectLocation(anchor.rect)}`;
|
|
737
|
+
case "pdf-page-rect": return `page ${String(anchor.page)}, ${rectLocation(anchor.rect)}`;
|
|
738
|
+
case "video-time-rect": return `time ${anchor.timeSeconds.toFixed(3)}s${anchor.frame === void 0 ? "" : `, frame ${String(anchor.frame)}`}, ${rectLocation(anchor.rect)}`;
|
|
739
|
+
case "story-preview-rect": return `story preview ${rectLocation(anchor.rect)}`;
|
|
740
|
+
case "image-point": return `image point x ${percent$1(anchor.point.x)}, y ${percent$1(anchor.point.y)}`;
|
|
741
|
+
case "pdf-page-point": return `page ${String(anchor.page)}, point x ${percent$1(anchor.point.x)}, y ${percent$1(anchor.point.y)}`;
|
|
742
|
+
case "video-time-point": return `time ${anchor.timeSeconds.toFixed(3)}s, point x ${percent$1(anchor.point.x)}, y ${percent$1(anchor.point.y)}`;
|
|
743
|
+
case "story-preview-point": return `story preview point x ${percent$1(anchor.point.x)}, y ${percent$1(anchor.point.y)}`;
|
|
744
|
+
case "html-rect": return `rendered page ${anchor.element === void 0 ? "" : `${anchor.element.selector}, `}${rectLocation(anchor.rect)}`;
|
|
745
|
+
case "html-point": return `rendered page ${anchor.element === void 0 ? "" : `${anchor.element.selector}, `}point x ${percent$1(anchor.point.x)}, y ${percent$1(anchor.point.y)}`;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function storyPreviewProvenance(packet) {
|
|
749
|
+
const preview = packet.document.storyPreview;
|
|
750
|
+
if (preview === void 0) return void 0;
|
|
751
|
+
const sources = preview.sources.map((source) => `- ${source.path} (sha256 ${source.sha256})`).join("\n");
|
|
752
|
+
return [
|
|
753
|
+
"Story preview source (edit these sources, never the generated preview artifact):",
|
|
754
|
+
`Project: ${preview.projectPath}`,
|
|
755
|
+
`Story: ${preview.storyPath}#${preview.storyExport}`,
|
|
756
|
+
`Build revision: ${preview.buildRevision}`,
|
|
757
|
+
`Viewport: ${String(preview.viewport.width)}x${String(preview.viewport.height)}`,
|
|
758
|
+
"Sources:",
|
|
759
|
+
sources,
|
|
760
|
+
"After editing the underlying story or component source, rebuild the preview before treating it as visually verified."
|
|
761
|
+
].join("\n");
|
|
762
|
+
}
|
|
763
|
+
function filePath(path) {
|
|
764
|
+
return path.startsWith("artifact:") ? `${path} (relative to the session artifact folder)` : path;
|
|
765
|
+
}
|
|
766
|
+
function markdownMediaProvenance(packet) {
|
|
767
|
+
const media = packet.document.markdownMedia;
|
|
768
|
+
if (media === void 0) return void 0;
|
|
769
|
+
const lines = [media.kind === "mermaid" ? "Reviewed a mermaid diagram rendered from Markdown. Change the diagram by editing its source." : "Reviewed an image shown in rendered Markdown. Change it by editing or regenerating its source file."];
|
|
770
|
+
if (media.sourcePath !== void 0) lines.push(`Source image: ${filePath(media.sourcePath)}`);
|
|
771
|
+
if (media.documentPath !== void 0) lines.push(`Embedded in document: ${filePath(media.documentPath)}`);
|
|
772
|
+
if (media.documentPath === void 0 && media.sourcePath === void 0) lines.push("Embedded in a conversation message, not a file.");
|
|
773
|
+
if (media.mermaidSource !== void 0) lines.push("Mermaid source:", "```mermaid", media.mermaidSource, "```");
|
|
774
|
+
return lines.join("\n");
|
|
775
|
+
}
|
|
776
|
+
function authorCaptureContext(packet) {
|
|
777
|
+
const metadata = JSON.stringify(packet);
|
|
778
|
+
if (bytes$1(metadata) > 65536) throw new Error("Author capture packet exceeds 64 KiB.");
|
|
779
|
+
const annotations = packet.regions.map((region) => `(${String(region.ordinal)}) ${region.comment} [${anchorLocation(region.anchor)}]\nregionId: ${JSON.stringify(region.id)}${region.quote ? `\nQuoted text: ${region.quote}` : ""}`).join("\n\n");
|
|
780
|
+
const artifact = packet.document.imageArtifact;
|
|
781
|
+
const provenance = artifact === void 0 ? markdownMediaProvenance(packet) ?? storyPreviewProvenance(packet) ?? (packet.document.kind === "html" ? "Annotations mark the rendered page. Locations are relative to the visible frame; the selector and quoted text name the marked element. Change the page by editing this HTML source." : void 0) : [
|
|
782
|
+
"Generated image feedback: preserve the original image; generate a new refinement only when requested.",
|
|
783
|
+
`Original artifact: ${artifact.id} (session ${artifact.sessionId})`,
|
|
784
|
+
`Original canonical path: ${artifact.originalPath}`,
|
|
785
|
+
`Original SHA-256: ${artifact.sourceSha256}`,
|
|
786
|
+
`For imagegen refinement use referenced_image_paths: ${JSON.stringify([artifact.originalPath])}`
|
|
787
|
+
].join("\n");
|
|
788
|
+
const content = provenance === void 0 ? annotations : `${provenance}\n\nAnnotations:\n${annotations}`;
|
|
789
|
+
const fences = JSON.stringify({
|
|
790
|
+
expectedRevision: packet.document.revision,
|
|
791
|
+
expectedSourceSha256: packet.document.sourceSha256
|
|
792
|
+
});
|
|
793
|
+
return {
|
|
794
|
+
kind: "author-capture",
|
|
795
|
+
source: "author",
|
|
796
|
+
id: packet.captureId,
|
|
797
|
+
label: `${packet.regions.length} annotation${packet.regions.length === 1 ? "" : "s"} · ${packet.document.path}`,
|
|
798
|
+
content: `Author document: ${packet.document.path}${packet.document.alias === void 0 ? "" : ` (canvas ${packet.document.alias})`}\nMutation fences: ${fences}\n${content}`,
|
|
799
|
+
metadata
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorFiles.native.ts
|
|
804
|
+
const MAX_FILE_BYTES = 5242880;
|
|
805
|
+
const IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
806
|
+
"image/png",
|
|
807
|
+
"image/jpeg",
|
|
808
|
+
"image/gif",
|
|
809
|
+
"image/webp",
|
|
810
|
+
"image/bmp"
|
|
811
|
+
]);
|
|
812
|
+
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
813
|
+
"md",
|
|
814
|
+
"mmd",
|
|
815
|
+
"mermaid",
|
|
816
|
+
"txt",
|
|
817
|
+
"json",
|
|
818
|
+
"yaml",
|
|
819
|
+
"yml",
|
|
820
|
+
"xml",
|
|
821
|
+
"svg",
|
|
822
|
+
"css",
|
|
823
|
+
"html",
|
|
824
|
+
"htm",
|
|
825
|
+
"js",
|
|
826
|
+
"jsx",
|
|
827
|
+
"ts",
|
|
828
|
+
"tsx"
|
|
829
|
+
]);
|
|
830
|
+
function structuredFormat(path) {
|
|
831
|
+
const extension = extensionOf(path);
|
|
832
|
+
return extension === "csv" || extension === "xlsx" || extension === "pptx" ? extension : void 0;
|
|
833
|
+
}
|
|
834
|
+
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
835
|
+
"png",
|
|
836
|
+
"jpg",
|
|
837
|
+
"jpeg",
|
|
838
|
+
"gif",
|
|
839
|
+
"webp",
|
|
840
|
+
"bmp"
|
|
841
|
+
]);
|
|
842
|
+
const VIDEO_MIMES = {
|
|
843
|
+
mp4: "video/mp4",
|
|
844
|
+
m4v: "video/mp4",
|
|
845
|
+
mov: "video/quicktime",
|
|
846
|
+
webm: "video/webm"
|
|
847
|
+
};
|
|
848
|
+
const IMAGE_SAVE_MAX_BYTES = 33554432;
|
|
849
|
+
const PDF_MAX_BYTES = 33554432;
|
|
850
|
+
const VIDEO_MAX_BYTES = 134217728;
|
|
851
|
+
const extensionOf = (path) => path.toLowerCase().split("/").at(-1)?.split(".").slice(1).at(-1) ?? "";
|
|
852
|
+
/** Whether the phone's canvas can mark this path up; anything else stays with its own viewer. */
|
|
853
|
+
function authorNativeCanOpen(path) {
|
|
854
|
+
const extension = extensionOf(path);
|
|
855
|
+
return extension === "pdf" || VIDEO_MIMES[extension] !== void 0 || structuredFormat(path) !== void 0 || IMAGE_EXTENSIONS.has(extension) || TEXT_EXTENSIONS.has(extension);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* The raw reads Author's canvas already uses, restated for the native transport:
|
|
859
|
+
* po-artifact's guarded artifact read and the host's session file, plus the two
|
|
860
|
+
* canvas routes from apiRoutes.ts, whose definer the phone does not load.
|
|
861
|
+
*/
|
|
862
|
+
const artifactFiles = createApiClient({ content: {
|
|
863
|
+
method: "GET",
|
|
864
|
+
path: "/content",
|
|
865
|
+
query: ["path"],
|
|
866
|
+
raw: true
|
|
867
|
+
} }, {
|
|
868
|
+
scopes: ["session"],
|
|
869
|
+
basePath: "artifacts",
|
|
870
|
+
transport: nativeApiTransport,
|
|
871
|
+
sessionAddress: sessionApiAddress
|
|
872
|
+
});
|
|
873
|
+
const sessionFiles = createApiClient({
|
|
874
|
+
file: {
|
|
875
|
+
method: "GET",
|
|
876
|
+
path: "/file",
|
|
877
|
+
query: ["path"],
|
|
878
|
+
host: true,
|
|
879
|
+
raw: true
|
|
880
|
+
},
|
|
881
|
+
fileWrite: {
|
|
882
|
+
method: "PUT",
|
|
883
|
+
path: "/file",
|
|
884
|
+
query: ["path"],
|
|
885
|
+
host: true,
|
|
886
|
+
raw: true
|
|
887
|
+
},
|
|
888
|
+
documentsOpen: {
|
|
889
|
+
method: "POST",
|
|
890
|
+
path: "/documents/open",
|
|
891
|
+
query: ["session"],
|
|
892
|
+
response: apiResponse()
|
|
893
|
+
},
|
|
894
|
+
documentsPreflight: {
|
|
895
|
+
method: "POST",
|
|
896
|
+
path: "/documents/preflight",
|
|
897
|
+
query: ["session"],
|
|
898
|
+
response: apiResponse()
|
|
899
|
+
},
|
|
900
|
+
documentsSerialize: {
|
|
901
|
+
method: "POST",
|
|
902
|
+
path: "/documents/serialize",
|
|
903
|
+
query: ["session"],
|
|
904
|
+
response: apiResponse()
|
|
905
|
+
}
|
|
906
|
+
}, {
|
|
907
|
+
scopes: ["session"],
|
|
908
|
+
basePath: "author",
|
|
909
|
+
transport: nativeApiTransport,
|
|
910
|
+
sessionAddress: sessionApiAddress
|
|
911
|
+
});
|
|
912
|
+
/** Opens the document's canvas on the server, the one the agent's Author tools address, and returns its alias. */
|
|
913
|
+
async function openAuthorNativeCanvas(sessionId, path, signal) {
|
|
914
|
+
const result = await sessionFiles.session(sessionId).documentsOpen({
|
|
915
|
+
body: { path },
|
|
916
|
+
query: { session: sessionId },
|
|
917
|
+
signal
|
|
918
|
+
});
|
|
919
|
+
if (!result.ok) throw new Error(result.error || `Author canvas could not open (${String(result.status)})`);
|
|
920
|
+
if (!("alias" in result.data) || typeof result.data.alias !== "string") throw new Error("The Author canvas did not return an alias.");
|
|
921
|
+
return {
|
|
922
|
+
alias: result.data.alias,
|
|
923
|
+
path: result.data.path
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/** Writes only against the digest we read. A stale document is refused, never silently overwritten. */
|
|
927
|
+
async function saveAuthorNativeText(sessionId, path, text, sourceSha256, signal) {
|
|
928
|
+
if (path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
|
|
929
|
+
if (!/^[a-f0-9]{64}$/.test(sourceSha256)) throw new Error("This Author view has no valid source digest.");
|
|
930
|
+
if (new Blob([text]).size > MAX_FILE_BYTES) throw new Error("This file is too large to save here.");
|
|
931
|
+
return await saveAuthorNativeBytes(sessionId, path, text, sourceSha256, signal);
|
|
932
|
+
}
|
|
933
|
+
/** Saves a crop without changing the original file's image encoding. */
|
|
934
|
+
async function saveAuthorNativeImage(sessionId, path, image, sourceSha256, signal) {
|
|
935
|
+
if (path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
|
|
936
|
+
if (!validDigest(sourceSha256)) throw new Error("This Author view has no valid source digest.");
|
|
937
|
+
const extension = extensionOf(path);
|
|
938
|
+
const mimeType = extension === "png" ? "image/png" : extension === "jpg" || extension === "jpeg" ? "image/jpeg" : void 0;
|
|
939
|
+
if (mimeType === void 0 || image.mimeType !== mimeType) throw new Error("The cropped image encoding does not match its path.");
|
|
940
|
+
const bytes = image.bytes;
|
|
941
|
+
if (bytes.byteLength === 0 || bytes.byteLength > IMAGE_SAVE_MAX_BYTES) throw new Error("This image is empty or too large to save here.");
|
|
942
|
+
if (!(mimeType === "image/png" ? [
|
|
943
|
+
137,
|
|
944
|
+
80,
|
|
945
|
+
78,
|
|
946
|
+
71,
|
|
947
|
+
13,
|
|
948
|
+
10,
|
|
949
|
+
26,
|
|
950
|
+
10
|
|
951
|
+
] : [
|
|
952
|
+
255,
|
|
953
|
+
216,
|
|
954
|
+
255
|
|
955
|
+
]).every((byte, index) => bytes[index] === byte)) throw new Error("The cropped image bytes do not match their encoding.");
|
|
956
|
+
const digest = await saveAuthorNativeBytes(sessionId, path, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), sourceSha256, signal);
|
|
957
|
+
if (digest === sourceSha256) throw new Error("Author save returned a stale digest.");
|
|
958
|
+
return digest;
|
|
959
|
+
}
|
|
960
|
+
async function saveAuthorNativeBytes(sessionId, path, body, sourceSha256, signal) {
|
|
961
|
+
const result = await sessionFiles.session(sessionId).fileWrite({
|
|
962
|
+
query: { path },
|
|
963
|
+
headers: {
|
|
964
|
+
"Content-Type": "application/octet-stream",
|
|
965
|
+
"X-Expected-SHA256": sourceSha256
|
|
966
|
+
},
|
|
967
|
+
body,
|
|
968
|
+
signal
|
|
969
|
+
});
|
|
970
|
+
if (!result.ok) {
|
|
971
|
+
if (result.status === 409) throw new Error("This file changed elsewhere. Keep your edits and reopen it before saving.");
|
|
972
|
+
throw new Error(result.error || `Author save failed (${String(result.status)})`);
|
|
973
|
+
}
|
|
974
|
+
const digest = result.response.headers.get("X-File-SHA256");
|
|
975
|
+
if (digest === null || !/^[a-f0-9]{64}$/.test(digest)) throw new Error("Author save returned an invalid digest.");
|
|
976
|
+
return digest;
|
|
977
|
+
}
|
|
978
|
+
/** Reads one document for marking up on the phone: an image as base64, anything textual as a string. */
|
|
979
|
+
async function loadAuthorFile(sessionId, path, signal) {
|
|
980
|
+
const format = structuredFormat(path);
|
|
981
|
+
if (format !== void 0) return await loadStructuredFile(sessionId, path, format, signal);
|
|
982
|
+
const artifact = path.startsWith(AUTHOR_ARTIFACT_PREFIX);
|
|
983
|
+
const query = { path: artifact ? path.slice(9) : path };
|
|
984
|
+
const result = artifact ? await artifactFiles.session(sessionId).content({
|
|
985
|
+
query,
|
|
986
|
+
signal
|
|
987
|
+
}) : await sessionFiles.session(sessionId).file({
|
|
988
|
+
query,
|
|
989
|
+
signal
|
|
990
|
+
});
|
|
991
|
+
if (!result.ok) throw new Error(result.error || "This file could not be read.");
|
|
992
|
+
const response = result.response;
|
|
993
|
+
if (!response) throw new Error("This file could not be read.");
|
|
994
|
+
const extension = extensionOf(query.path);
|
|
995
|
+
const mediaMime = extension === "pdf" ? "application/pdf" : VIDEO_MIMES[extension];
|
|
996
|
+
const limit = extension === "pdf" ? PDF_MAX_BYTES : mediaMime ? VIDEO_MAX_BYTES : MAX_FILE_BYTES;
|
|
997
|
+
if (Number(response.headers.get("content-length") ?? 0) > limit) throw new Error("This file is too large to open here.");
|
|
998
|
+
const mimeType = (response.headers.get("content-type")?.split(";")[0]?.trim() ?? "").toLowerCase();
|
|
999
|
+
const sourceSha256 = response.headers.get("X-File-SHA256") ?? void 0;
|
|
1000
|
+
if (mediaMime !== void 0) {
|
|
1001
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1002
|
+
if (signal.aborted) throw new Error("Opening was cancelled.");
|
|
1003
|
+
if (bytes.byteLength > limit) throw new Error("This file is too large to open here.");
|
|
1004
|
+
return {
|
|
1005
|
+
kind: extension === "pdf" ? "pdf" : "video",
|
|
1006
|
+
bytes,
|
|
1007
|
+
mimeType: mediaMime,
|
|
1008
|
+
...sourceSha256 === void 0 ? {} : { sourceSha256 }
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
if (IMAGE_TYPES.has(mimeType)) {
|
|
1012
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1013
|
+
if (signal.aborted) throw new Error("Opening was cancelled.");
|
|
1014
|
+
if (bytes.byteLength > MAX_FILE_BYTES) throw new Error("This file is too large to open here.");
|
|
1015
|
+
return {
|
|
1016
|
+
kind: "image",
|
|
1017
|
+
data: encodeBase64(bytes),
|
|
1018
|
+
mimeType,
|
|
1019
|
+
...sourceSha256 === void 0 ? {} : { sourceSha256 }
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
if (mimeType.startsWith("text/") || mimeType === "application/json" || TEXT_EXTENSIONS.has(extension)) {
|
|
1023
|
+
const text = await response.text();
|
|
1024
|
+
if (signal.aborted) throw new Error("Opening was cancelled.");
|
|
1025
|
+
if (new Blob([text]).size > MAX_FILE_BYTES) throw new Error("This file is too large to open here.");
|
|
1026
|
+
const sourceSha256 = response.headers.get("X-File-SHA256") ?? void 0;
|
|
1027
|
+
return {
|
|
1028
|
+
kind: "text",
|
|
1029
|
+
text,
|
|
1030
|
+
...sourceSha256 === void 0 ? {} : { sourceSha256 }
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
throw new Error("This kind of file cannot be marked up on this device yet.");
|
|
1034
|
+
}
|
|
1035
|
+
const validDigest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
1036
|
+
async function loadStructuredFile(sessionId, path, format, signal) {
|
|
1037
|
+
const result = await sessionFiles.session(sessionId).documentsOpen({
|
|
1038
|
+
body: {
|
|
1039
|
+
path,
|
|
1040
|
+
format
|
|
1041
|
+
},
|
|
1042
|
+
query: { session: sessionId },
|
|
1043
|
+
signal
|
|
1044
|
+
});
|
|
1045
|
+
if (!result.ok) throw new Error(result.error || "This document could not be parsed.");
|
|
1046
|
+
const parsed = result.data;
|
|
1047
|
+
if (!("fragments" in parsed) || !Array.isArray(parsed.fragments) || parsed.manifest?.format !== format || !validDigest(parsed.manifest.sourceDigest)) throw new Error("Author returned an invalid structured document or source digest.");
|
|
1048
|
+
if (!Number.isSafeInteger(parsed.manifest.byteLength) || parsed.manifest.byteLength < 0 || parsed.manifest.byteLength > MAX_FILE_BYTES || new Blob([JSON.stringify(parsed.fragments)]).size > MAX_FILE_BYTES) throw new Error("This file is too large to open here.");
|
|
1049
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1050
|
+
for (const fragment of parsed.fragments) {
|
|
1051
|
+
if (typeof fragment.id !== "string" || typeof fragment.text !== "string" || ids.has(fragment.id)) throw new Error("Author returned invalid structured fragments.");
|
|
1052
|
+
ids.add(fragment.id);
|
|
1053
|
+
}
|
|
1054
|
+
let csvDialect;
|
|
1055
|
+
if (format === "csv") {
|
|
1056
|
+
const { delimiter, quote, recordDelimiter } = parsed.manifest.metadata ?? {};
|
|
1057
|
+
if (delimiter !== "," && delimiter !== ";" && delimiter !== " " && delimiter !== "|" || quote !== "\"" && quote !== "'" || recordDelimiter !== "\n" && recordDelimiter !== "\r\n") throw new Error("Author returned an invalid CSV dialect.");
|
|
1058
|
+
csvDialect = {
|
|
1059
|
+
delimiter,
|
|
1060
|
+
quote,
|
|
1061
|
+
recordDelimiter
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
if (signal.aborted) throw new Error("Opening was cancelled.");
|
|
1065
|
+
return {
|
|
1066
|
+
kind: "structured",
|
|
1067
|
+
format,
|
|
1068
|
+
fragments: parsed.fragments,
|
|
1069
|
+
originalFragments: parsed.fragments.map((fragment) => ({ ...fragment })),
|
|
1070
|
+
sourceSha256: parsed.manifest.sourceDigest,
|
|
1071
|
+
...csvDialect === void 0 ? {} : { csvDialect }
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
/** The server owns parsing and preservation. Native sends only replacements of known editable fragments. */
|
|
1075
|
+
async function saveAuthorNativeStructured(sessionId, path, file, signal) {
|
|
1076
|
+
if (path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
|
|
1077
|
+
if (!validDigest(file.sourceSha256)) throw new Error("This Author view has no valid source digest.");
|
|
1078
|
+
if (structuredFormat(path) !== file.format) throw new Error("The structured document format does not match its path.");
|
|
1079
|
+
const originals = new Map(file.originalFragments.map((fragment) => [fragment.id, fragment]));
|
|
1080
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1081
|
+
const operations = [];
|
|
1082
|
+
for (const fragment of file.fragments) {
|
|
1083
|
+
const original = originals.get(fragment.id);
|
|
1084
|
+
if (original === void 0 || seen.has(fragment.id)) throw new Error("Unknown or duplicate structured fragment.");
|
|
1085
|
+
seen.add(fragment.id);
|
|
1086
|
+
if (original.text === fragment.text) continue;
|
|
1087
|
+
if (original.readOnly || fragment.readOnly) throw new Error("This structured fragment is read-only.");
|
|
1088
|
+
operations.push({
|
|
1089
|
+
fragmentId: fragment.id,
|
|
1090
|
+
replacement: fragment.text
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
if (seen.size !== originals.size) throw new Error("Structured fragments cannot be removed.");
|
|
1094
|
+
if (operations.length > 1e5 || new Blob([JSON.stringify(operations)]).size > MAX_FILE_BYTES) throw new Error("This file is too large to save here.");
|
|
1095
|
+
const body = {
|
|
1096
|
+
path,
|
|
1097
|
+
format: file.format,
|
|
1098
|
+
operations,
|
|
1099
|
+
...file.csvDialect === void 0 ? {} : { csvDialect: file.csvDialect }
|
|
1100
|
+
};
|
|
1101
|
+
const client = sessionFiles.session(sessionId);
|
|
1102
|
+
const preflight = await client.documentsPreflight({
|
|
1103
|
+
body,
|
|
1104
|
+
query: { session: sessionId },
|
|
1105
|
+
signal
|
|
1106
|
+
});
|
|
1107
|
+
if (!preflight.ok) throw new Error(preflight.error || "Author preflight failed.");
|
|
1108
|
+
if (!validDigest(preflight.data.sourceDigest) || preflight.data.sourceDigest !== file.sourceSha256) throw new Error("Document source changed before saving. Reopen the document.");
|
|
1109
|
+
if (!preflight.data.accepted) throw new Error(preflight.data.issues.map((issue) => issue.message).join("\n") || "Author preflight rejected the edit.");
|
|
1110
|
+
if (!validDigest(preflight.data.digest)) throw new Error("Author preflight returned an invalid digest.");
|
|
1111
|
+
const serialized = await client.documentsSerialize({
|
|
1112
|
+
body: {
|
|
1113
|
+
...body,
|
|
1114
|
+
preflightDigest: preflight.data.digest
|
|
1115
|
+
},
|
|
1116
|
+
query: { session: sessionId },
|
|
1117
|
+
signal
|
|
1118
|
+
});
|
|
1119
|
+
if (!serialized.ok) throw new Error(serialized.error || "Author serialization failed.");
|
|
1120
|
+
if (serialized.data.encoding !== "base64" || typeof serialized.data.bytes !== "string") throw new Error("Author serialize returned an unsupported encoding.");
|
|
1121
|
+
if (serialized.data.bytes.length > Math.ceil(MAX_FILE_BYTES / 3) * 4) throw new Error("This file is too large to save here.");
|
|
1122
|
+
const bytes = decodeBase64(serialized.data.bytes);
|
|
1123
|
+
if (bytes.byteLength > MAX_FILE_BYTES) throw new Error("This file is too large to save here.");
|
|
1124
|
+
return await saveAuthorNativeBytes(sessionId, path, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), file.sourceSha256, signal);
|
|
1125
|
+
}
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorMarkdownText.ts
|
|
1128
|
+
const HEADING = /^#{1,6} /;
|
|
1129
|
+
const LIST_ITEM = /^(?:[-*+]|\d+[.)]) /;
|
|
1130
|
+
/**
|
|
1131
|
+
* One formatting edit for the selected source range. Bold, Heading and List toggle off when already applied;
|
|
1132
|
+
* Heading and List apply to every touched line and replace each other.
|
|
1133
|
+
*/
|
|
1134
|
+
function authorMarkdownFormat(content, from, to, format) {
|
|
1135
|
+
if (format === "bold") {
|
|
1136
|
+
const selected = content.slice(from, to);
|
|
1137
|
+
if (selected.length >= 4 && selected.startsWith("**") && selected.endsWith("**")) return {
|
|
1138
|
+
from,
|
|
1139
|
+
to,
|
|
1140
|
+
insert: selected.slice(2, -2),
|
|
1141
|
+
select: {
|
|
1142
|
+
from,
|
|
1143
|
+
to: to - 4
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
if (from >= 2 && content.slice(from - 2, from) === "**" && content.slice(to, to + 2) === "**") return {
|
|
1147
|
+
from: from - 2,
|
|
1148
|
+
to: to + 2,
|
|
1149
|
+
insert: selected,
|
|
1150
|
+
select: {
|
|
1151
|
+
from: from - 2,
|
|
1152
|
+
to: to - 2
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
const text = selected || "bold text";
|
|
1156
|
+
return {
|
|
1157
|
+
from,
|
|
1158
|
+
to,
|
|
1159
|
+
insert: `**${text}**`,
|
|
1160
|
+
select: {
|
|
1161
|
+
from: from + 2,
|
|
1162
|
+
to: from + 2 + text.length
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
if (format === "link") {
|
|
1167
|
+
const text = content.slice(from, to) || "link text";
|
|
1168
|
+
const url = from + text.length + 3;
|
|
1169
|
+
return {
|
|
1170
|
+
from,
|
|
1171
|
+
to,
|
|
1172
|
+
insert: `[${text}](https://)`,
|
|
1173
|
+
select: {
|
|
1174
|
+
from: url,
|
|
1175
|
+
to: url + 8
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const start = content.lastIndexOf("\n", from - 1) + 1;
|
|
1180
|
+
const last = to > from && content[to - 1] === "\n" ? to - 1 : to;
|
|
1181
|
+
const lineEnd = content.indexOf("\n", Math.max(last, start));
|
|
1182
|
+
const end = lineEnd === -1 ? content.length : lineEnd;
|
|
1183
|
+
const own = format === "heading" ? HEADING : LIST_ITEM;
|
|
1184
|
+
const prefix = format === "heading" ? "## " : "- ";
|
|
1185
|
+
const lines = content.slice(start, end).split("\n");
|
|
1186
|
+
const written = lines.filter((line) => line.trim() !== "");
|
|
1187
|
+
const remove = written.length > 0 && written.every((line) => own.test(line));
|
|
1188
|
+
const insert = lines.map((line) => line.trim() === "" ? line : remove ? line.replace(own, "") : prefix + line.replace(HEADING, "").replace(LIST_ITEM, "")).join("\n");
|
|
1189
|
+
return {
|
|
1190
|
+
from: start,
|
|
1191
|
+
to: end,
|
|
1192
|
+
insert,
|
|
1193
|
+
select: {
|
|
1194
|
+
from: start,
|
|
1195
|
+
to: start + insert.length
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
/** One-based source line of an offset. */
|
|
1200
|
+
function authorSourceLine(source, offset) {
|
|
1201
|
+
return source.slice(0, offset).split("\n").length;
|
|
1202
|
+
}
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorNotes.ts
|
|
1205
|
+
const NOTE_MAX_CHARS = 1e3;
|
|
1206
|
+
/** Validate against canonical loaded source, using the same one-based line convention as web Author. */
|
|
1207
|
+
function nativeAuthorTextRange(source, range) {
|
|
1208
|
+
const { startOffset, endOffset } = range;
|
|
1209
|
+
if (!Number.isSafeInteger(startOffset) || !Number.isSafeInteger(endOffset) || startOffset < 0 || endOffset <= startOffset || endOffset > source.length) return void 0;
|
|
1210
|
+
return {
|
|
1211
|
+
kind: "text-range",
|
|
1212
|
+
startOffset,
|
|
1213
|
+
endOffset,
|
|
1214
|
+
startLine: authorSourceLine(source, startOffset),
|
|
1215
|
+
endLine: authorSourceLine(source, endOffset)
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/** One screenshot evidences only the displayed page or captured frame. Sub-millisecond clock noise is not another frame. */
|
|
1219
|
+
function nativeAuthorVisibleNotes(notes, kind, page, timeSeconds) {
|
|
1220
|
+
return notes.filter((note) => kind === "pdf" ? note.page === page && page !== void 0 : kind === "video" ? timeSeconds !== void 0 && note.timeSeconds !== void 0 && Math.abs(note.timeSeconds - timeSeconds) < .001 : true);
|
|
1221
|
+
}
|
|
1222
|
+
const percent = (value) => `${Math.round(Math.min(1, Math.max(0, Number.isFinite(value) ? value : 0)) * 100)}%`;
|
|
1223
|
+
/** A repository-relative or `artifact:` path, or undefined when it could escape the session's folders. */
|
|
1224
|
+
function authorNotePath(path) {
|
|
1225
|
+
const local = (path.startsWith("artifact:") ? path.slice(9) : path).replaceAll("\\", "/");
|
|
1226
|
+
const parts = local.split("/");
|
|
1227
|
+
if (local.trim() === "" || local.startsWith("/") || /^[A-Za-z]:/u.test(local) || /[\u0000-\u001f]/u.test(local) || parts.includes("..")) return;
|
|
1228
|
+
return path;
|
|
1229
|
+
}
|
|
1230
|
+
/** The model-facing text for the notes a person placed on one document. */
|
|
1231
|
+
function authorNotesContent(path, notes) {
|
|
1232
|
+
const heading = `Author document: ${path}`;
|
|
1233
|
+
if (notes.length === 0) return heading;
|
|
1234
|
+
return [
|
|
1235
|
+
heading,
|
|
1236
|
+
"Notes from the person: source selections use UTF-16 source offsets; point notes use fractions of the document shown on the phone.",
|
|
1237
|
+
...notes.map((note, index) => {
|
|
1238
|
+
const anchors = [
|
|
1239
|
+
...note.page !== void 0 ? [`page ${String(note.page)}`] : [],
|
|
1240
|
+
...note.timeSeconds !== void 0 ? [`time ${String(note.timeSeconds)}s`] : [],
|
|
1241
|
+
...note.textRange === void 0 ? [`x ${percent(note.x)}, y ${percent(note.y)}`] : [`source offsets ${String(note.textRange.startOffset)}-${String(note.textRange.endOffset)}`]
|
|
1242
|
+
];
|
|
1243
|
+
const stroke = note.points?.slice(0, 128).filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
|
|
1244
|
+
return `(${String(index + 1)}) ${note.text.trim().slice(0, NOTE_MAX_CHARS)} [${anchors.join("; ")}]${stroke?.length ? ` stroke: ${stroke.map((point) => `${percent(point.x)},${percent(point.y)}`).join(" -> ")}` : ""}`;
|
|
1245
|
+
})
|
|
1246
|
+
].join("\n");
|
|
1247
|
+
}
|
|
1248
|
+
//#endregion
|
|
1249
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorNativeCapture.ts
|
|
1250
|
+
/** A native mark is feedback on what was displayed, never source-edit authority. */
|
|
1251
|
+
function nativeAuthorCapture(captureId, capturedAt, document, notes, viewport, alias) {
|
|
1252
|
+
const regions = notes.map((note) => {
|
|
1253
|
+
if (![note.x, note.y].every((value) => Number.isFinite(value) && value >= 0 && value <= 1)) throw new Error("A note has invalid native coordinates.");
|
|
1254
|
+
if (note.sourceSha256 !== void 0 && note.sourceSha256 !== document.sourceSha256) throw new Error("These notes refer to an earlier file version. Keep them and reopen the original before sending.");
|
|
1255
|
+
const point = {
|
|
1256
|
+
x: note.x,
|
|
1257
|
+
y: note.y
|
|
1258
|
+
};
|
|
1259
|
+
const rect = note.points === void 0 ? void 0 : authorStrokeBounds(note.points);
|
|
1260
|
+
if (rect === null) throw new Error("A note has invalid freehand geometry.");
|
|
1261
|
+
let anchor;
|
|
1262
|
+
let comment = note.text;
|
|
1263
|
+
let quote;
|
|
1264
|
+
if (note.textRange !== void 0) {
|
|
1265
|
+
if (document.kind !== "text" || document.sourceText === void 0 || note.sourceSha256 === void 0 || !/^[a-f0-9]{64}$/u.test(note.sourceSha256)) throw new Error("A source selection requires loaded text and its source digest.");
|
|
1266
|
+
const range = nativeAuthorTextRange(document.sourceText, note.textRange);
|
|
1267
|
+
if (range === void 0) throw new Error("A source selection is outside the loaded text.");
|
|
1268
|
+
anchor = range;
|
|
1269
|
+
quote = document.sourceText.slice(range.startOffset, range.endOffset);
|
|
1270
|
+
} else if (document.kind === "pdf") {
|
|
1271
|
+
if (!Number.isSafeInteger(note.page) || note.page < 1) throw new Error("A PDF note has no page.");
|
|
1272
|
+
comment = `[Native PDF page viewport coordinates, not intrinsic PDF coordinates] ${comment}`;
|
|
1273
|
+
anchor = rect ? {
|
|
1274
|
+
kind: "pdf-page-rect",
|
|
1275
|
+
page: note.page,
|
|
1276
|
+
rect
|
|
1277
|
+
} : {
|
|
1278
|
+
kind: "pdf-page-point",
|
|
1279
|
+
page: note.page,
|
|
1280
|
+
point
|
|
1281
|
+
};
|
|
1282
|
+
} else if (document.kind === "video") {
|
|
1283
|
+
if (!Number.isFinite(note.timeSeconds) || note.timeSeconds < 0) throw new Error("A video note has no frame time.");
|
|
1284
|
+
anchor = rect ? {
|
|
1285
|
+
kind: "video-time-rect",
|
|
1286
|
+
timeSeconds: note.timeSeconds,
|
|
1287
|
+
rect
|
|
1288
|
+
} : {
|
|
1289
|
+
kind: "video-time-point",
|
|
1290
|
+
timeSeconds: note.timeSeconds,
|
|
1291
|
+
point
|
|
1292
|
+
};
|
|
1293
|
+
} else if (document.kind === "html") {
|
|
1294
|
+
comment = `[Native semantic HTML viewport, not browser layout] ${comment}`;
|
|
1295
|
+
anchor = rect ? {
|
|
1296
|
+
kind: "html-rect",
|
|
1297
|
+
rect
|
|
1298
|
+
} : {
|
|
1299
|
+
kind: "html-point",
|
|
1300
|
+
point
|
|
1301
|
+
};
|
|
1302
|
+
} else if (note.fragmentId !== void 0 && !rect) {
|
|
1303
|
+
const fragment = document.fragments?.find((entry) => entry.id === note.fragmentId);
|
|
1304
|
+
if (!fragment) throw new Error("This note refers to a fragment that is no longer displayed.");
|
|
1305
|
+
if (document.kind === "pptx") {
|
|
1306
|
+
const slide = /slide(\d+)\.xml$/u.exec(fragment.location)?.[1];
|
|
1307
|
+
if (slide === void 0) throw new Error("This slide note has no source slide.");
|
|
1308
|
+
anchor = {
|
|
1309
|
+
kind: "slide-element",
|
|
1310
|
+
fragmentId: fragment.id,
|
|
1311
|
+
location: fragment.location,
|
|
1312
|
+
slide: Number(slide)
|
|
1313
|
+
};
|
|
1314
|
+
} else anchor = {
|
|
1315
|
+
kind: "cell",
|
|
1316
|
+
fragmentId: fragment.id,
|
|
1317
|
+
location: fragment.location
|
|
1318
|
+
};
|
|
1319
|
+
} else {
|
|
1320
|
+
const naturalWidth = document.naturalWidth ?? viewport.width;
|
|
1321
|
+
const naturalHeight = document.naturalHeight ?? viewport.height;
|
|
1322
|
+
if (document.kind !== "image") comment = `[Native viewport feedback, not a source selection] ${comment}`;
|
|
1323
|
+
anchor = rect ? {
|
|
1324
|
+
kind: "image-rect",
|
|
1325
|
+
rect,
|
|
1326
|
+
naturalWidth,
|
|
1327
|
+
naturalHeight
|
|
1328
|
+
} : {
|
|
1329
|
+
kind: "image-point",
|
|
1330
|
+
point,
|
|
1331
|
+
naturalWidth,
|
|
1332
|
+
naturalHeight
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
id: note.id,
|
|
1337
|
+
documentPath: document.path,
|
|
1338
|
+
revision: document.version,
|
|
1339
|
+
version: 1,
|
|
1340
|
+
sourceSha256: document.sourceSha256,
|
|
1341
|
+
mode: rect || note.textRange ? "region" : "point",
|
|
1342
|
+
comment,
|
|
1343
|
+
quote,
|
|
1344
|
+
anchor,
|
|
1345
|
+
stroke: note.points,
|
|
1346
|
+
viewport: {
|
|
1347
|
+
...viewport,
|
|
1348
|
+
page: note.page,
|
|
1349
|
+
timeSeconds: note.timeSeconds
|
|
1350
|
+
},
|
|
1351
|
+
createdAt: capturedAt
|
|
1352
|
+
};
|
|
1353
|
+
});
|
|
1354
|
+
return {
|
|
1355
|
+
packet: createAuthorCapturePacket(captureId, capturedAt, document, regions, alias),
|
|
1356
|
+
regions
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
//#endregion
|
|
1360
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorNativeDrafts.ts
|
|
1361
|
+
function object(value) {
|
|
1362
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid stored Author draft.");
|
|
1363
|
+
return value;
|
|
1364
|
+
}
|
|
1365
|
+
function text(value, limit) {
|
|
1366
|
+
if (typeof value !== "string" || value.length > limit) throw new Error("Invalid stored Author draft text.");
|
|
1367
|
+
return value;
|
|
1368
|
+
}
|
|
1369
|
+
function fraction(value) {
|
|
1370
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) throw new Error("Invalid stored Author mark.");
|
|
1371
|
+
return value;
|
|
1372
|
+
}
|
|
1373
|
+
function digest(value) {
|
|
1374
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) throw new Error("Invalid stored Author source digest.");
|
|
1375
|
+
return value;
|
|
1376
|
+
}
|
|
1377
|
+
function note(value) {
|
|
1378
|
+
const record = object(value);
|
|
1379
|
+
const result = {
|
|
1380
|
+
x: fraction(record.x),
|
|
1381
|
+
y: fraction(record.y),
|
|
1382
|
+
text: text(record.text, 2048)
|
|
1383
|
+
};
|
|
1384
|
+
if (record.points !== void 0) {
|
|
1385
|
+
if (!Array.isArray(record.points) || record.points.length < 2 || record.points.length > 128) throw new Error("Invalid stored Author stroke.");
|
|
1386
|
+
result.points = record.points.map((value) => {
|
|
1387
|
+
const point = object(value);
|
|
1388
|
+
return {
|
|
1389
|
+
x: fraction(point.x),
|
|
1390
|
+
y: fraction(point.y)
|
|
1391
|
+
};
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
if (record.page !== void 0) {
|
|
1395
|
+
if (typeof record.page !== "number" || !Number.isSafeInteger(record.page) || record.page < 1) throw new Error("Invalid stored Author page.");
|
|
1396
|
+
result.page = record.page;
|
|
1397
|
+
}
|
|
1398
|
+
if (record.timeSeconds !== void 0) {
|
|
1399
|
+
if (typeof record.timeSeconds !== "number" || !Number.isFinite(record.timeSeconds) || record.timeSeconds < 0) throw new Error("Invalid stored Author frame.");
|
|
1400
|
+
result.timeSeconds = record.timeSeconds;
|
|
1401
|
+
}
|
|
1402
|
+
if (record.fragmentId !== void 0) result.fragmentId = text(record.fragmentId, 4096);
|
|
1403
|
+
if (record.sourceSha256 !== void 0) result.sourceSha256 = digest(record.sourceSha256);
|
|
1404
|
+
if (record.textRange !== void 0) {
|
|
1405
|
+
const { startOffset, endOffset } = object(record.textRange);
|
|
1406
|
+
if (typeof startOffset !== "number" || typeof endOffset !== "number" || !Number.isSafeInteger(startOffset) || !Number.isSafeInteger(endOffset) || startOffset < 0 || endOffset <= startOffset || endOffset > 5242880 || result.sourceSha256 === void 0) throw new Error("Invalid stored Author source selection.");
|
|
1407
|
+
result.textRange = {
|
|
1408
|
+
startOffset,
|
|
1409
|
+
endOffset
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
return result;
|
|
1413
|
+
}
|
|
1414
|
+
/** Validate and detach restored local data before it can become marks or a file edit. */
|
|
1415
|
+
function parseNativeAuthorDraft(value) {
|
|
1416
|
+
const record = object(value);
|
|
1417
|
+
if (record.version !== 1 || !Array.isArray(record.notes) || record.notes.length > 100) throw new Error("Unsupported stored Author draft.");
|
|
1418
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1419
|
+
const result = {
|
|
1420
|
+
version: 1,
|
|
1421
|
+
notes: record.notes.map((value) => {
|
|
1422
|
+
const id = text(object(value).id, 512);
|
|
1423
|
+
if (!id || ids.has(id)) throw new Error("Duplicate or empty stored Author note.");
|
|
1424
|
+
ids.add(id);
|
|
1425
|
+
return {
|
|
1426
|
+
id,
|
|
1427
|
+
...note(value)
|
|
1428
|
+
};
|
|
1429
|
+
})
|
|
1430
|
+
};
|
|
1431
|
+
if (record.draft !== void 0) result.draft = note(record.draft);
|
|
1432
|
+
if (record.edit !== void 0) {
|
|
1433
|
+
const edit = object(record.edit);
|
|
1434
|
+
const sourceSha256 = digest(edit.sourceSha256);
|
|
1435
|
+
if (edit.kind === "text") result.edit = {
|
|
1436
|
+
kind: "text",
|
|
1437
|
+
sourceSha256,
|
|
1438
|
+
text: text(edit.text, 65536)
|
|
1439
|
+
};
|
|
1440
|
+
else if (edit.kind === "structured" && Array.isArray(edit.fragments) && edit.fragments.length <= 1e5) {
|
|
1441
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1442
|
+
result.edit = {
|
|
1443
|
+
kind: "structured",
|
|
1444
|
+
sourceSha256,
|
|
1445
|
+
fragments: edit.fragments.map((value) => {
|
|
1446
|
+
const fragment = object(value);
|
|
1447
|
+
const id = text(fragment.id, 4096);
|
|
1448
|
+
if (!id || seen.has(id)) throw new Error("Duplicate or empty stored Author fragment.");
|
|
1449
|
+
seen.add(id);
|
|
1450
|
+
return {
|
|
1451
|
+
id,
|
|
1452
|
+
text: text(fragment.text, 65536)
|
|
1453
|
+
};
|
|
1454
|
+
})
|
|
1455
|
+
};
|
|
1456
|
+
} else throw new Error("Unsupported stored Author edit.");
|
|
1457
|
+
}
|
|
1458
|
+
return result;
|
|
1459
|
+
}
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorNativeRequests.native.ts
|
|
1462
|
+
const RECORD_LIMIT = 100;
|
|
1463
|
+
const BYTE_LIMIT = 524288;
|
|
1464
|
+
const ACTIVE_RESERVE_BYTES = 2048;
|
|
1465
|
+
const ERROR_CHARACTER_LIMIT = 256;
|
|
1466
|
+
const terminal = (record) => [
|
|
1467
|
+
"COMPLETE",
|
|
1468
|
+
"FAILED",
|
|
1469
|
+
"CANCELLED",
|
|
1470
|
+
"UNTRACKED"
|
|
1471
|
+
].includes(record.status);
|
|
1472
|
+
/** Subscribe to store, then select(state, sessionId) for only that session's history. */
|
|
1473
|
+
const nativeAuthorRequests = defineSessionStore([]);
|
|
1474
|
+
const mounts = defineGlobalStore(/* @__PURE__ */ new Map());
|
|
1475
|
+
function requireSession(sessionId) {
|
|
1476
|
+
if (!mounts.store.state.has(sessionId)) throw new Error("Native Author requests require a mounted session.");
|
|
1477
|
+
}
|
|
1478
|
+
function bytes(value) {
|
|
1479
|
+
let size = 0;
|
|
1480
|
+
for (const character of JSON.stringify(value)) {
|
|
1481
|
+
const code = character.codePointAt(0);
|
|
1482
|
+
size += code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
|
|
1483
|
+
}
|
|
1484
|
+
return size;
|
|
1485
|
+
}
|
|
1486
|
+
function bound(records) {
|
|
1487
|
+
const next = [...records];
|
|
1488
|
+
while (next.length > RECORD_LIMIT || bytes(next) + next.filter((record) => !terminal(record)).length * ACTIVE_RESERVE_BYTES > BYTE_LIMIT) {
|
|
1489
|
+
const index = next.findIndex(terminal);
|
|
1490
|
+
if (index < 0) throw new Error("Author request history is full while active requests are still running.");
|
|
1491
|
+
next.splice(index, 1);
|
|
1492
|
+
}
|
|
1493
|
+
return next;
|
|
1494
|
+
}
|
|
1495
|
+
function comparableJson(value) {
|
|
1496
|
+
return JSON.stringify(value, (_key, item) => item !== null && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([left], [right]) => left.localeCompare(right))) : item);
|
|
1497
|
+
}
|
|
1498
|
+
/** Record before submitCapture. Throws without changing history on validation or backpressure. */
|
|
1499
|
+
function recordNativeAuthorRequest(sessionId, packet, regions) {
|
|
1500
|
+
requireSession(sessionId);
|
|
1501
|
+
if (packet.version !== 1 || packet.captureId.trim() === "" || !Number.isFinite(packet.capturedAt) || packet.document.path.trim() === "" || !Number.isSafeInteger(packet.document.revision) || packet.document.revision < 0) throw new Error("Invalid Author capture identity.");
|
|
1502
|
+
if (packet.document.imageArtifact !== void 0 && packet.document.imageArtifact.sessionId !== sessionId) throw new Error("Author capture belongs to another session.");
|
|
1503
|
+
const validated = createAuthorCapturePacket(packet.captureId, packet.capturedAt, {
|
|
1504
|
+
...packet.document,
|
|
1505
|
+
version: packet.document.revision
|
|
1506
|
+
}, regions, packet.document.alias);
|
|
1507
|
+
if (comparableJson(validated) !== comparableJson(packet) || regions.some((region) => region.id.trim() === "") || new Set(regions.map((region) => region.id)).size !== regions.length) throw new Error("Author capture does not match its regions.");
|
|
1508
|
+
const detachedRegions = validated.regions.map((region) => ({
|
|
1509
|
+
...region,
|
|
1510
|
+
documentPath: validated.document.path,
|
|
1511
|
+
revision: validated.document.revision,
|
|
1512
|
+
sourceSha256: validated.document.sourceSha256,
|
|
1513
|
+
createdAt: validated.capturedAt
|
|
1514
|
+
}));
|
|
1515
|
+
const record = JSON.parse(JSON.stringify({
|
|
1516
|
+
id: packet.captureId,
|
|
1517
|
+
captureId: packet.captureId,
|
|
1518
|
+
documentPath: packet.document.path,
|
|
1519
|
+
requestText: authorCaptureContext(validated).content,
|
|
1520
|
+
regions: detachedRegions,
|
|
1521
|
+
pendingRegions: detachedRegions,
|
|
1522
|
+
status: "REQUESTED",
|
|
1523
|
+
createdAt: packet.capturedAt,
|
|
1524
|
+
updatedAt: packet.capturedAt,
|
|
1525
|
+
revision: packet.document.revision,
|
|
1526
|
+
sourceSha256: packet.document.sourceSha256
|
|
1527
|
+
}));
|
|
1528
|
+
nativeAuthorRequests.update(sessionId, (records) => {
|
|
1529
|
+
if (records.some((item) => item.captureId === record.captureId)) throw new Error("Author capture already recorded.");
|
|
1530
|
+
return bound([...records, record]);
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
function fold(sessionId, event) {
|
|
1534
|
+
if (event.sessionId !== sessionId || event.status === "queued") return;
|
|
1535
|
+
nativeAuthorRequests.update(sessionId, (records) => {
|
|
1536
|
+
const index = records.findIndex((record) => record.captureId === event.captureId);
|
|
1537
|
+
const record = records[index];
|
|
1538
|
+
if (record === void 0 || terminal(record)) return records;
|
|
1539
|
+
const next = {
|
|
1540
|
+
...record,
|
|
1541
|
+
status: event.status === "working" ? "CHANGING" : event.status === "completed" ? "COMPLETE" : event.status === "untracked" ? "UNTRACKED" : "FAILED",
|
|
1542
|
+
currentOperation: event.status === "working" ? "Agent is working on this request" : void 0,
|
|
1543
|
+
pendingRegions: event.status === "completed" ? [] : record.pendingRegions,
|
|
1544
|
+
error: event.status === "error" ? (event.error || "Request execution failed.").slice(0, ERROR_CHARACTER_LIMIT) : void 0,
|
|
1545
|
+
updatedAt: Date.now()
|
|
1546
|
+
};
|
|
1547
|
+
return records.map((item, position) => position === index ? next : item);
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
function failNativeAuthorRequest(sessionId, captureId, error) {
|
|
1551
|
+
requireSession(sessionId);
|
|
1552
|
+
fold(sessionId, {
|
|
1553
|
+
sessionId,
|
|
1554
|
+
captureId,
|
|
1555
|
+
status: "error",
|
|
1556
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
function startNativeAuthorRequests(runtime) {
|
|
1560
|
+
if (runtime.mount?.scope !== "session") throw new Error("Native Author requests require a session mount.");
|
|
1561
|
+
const sessionId = runtime.mount.sessionId;
|
|
1562
|
+
const token = {};
|
|
1563
|
+
mounts.update((current) => new Map(current).set(sessionId, token));
|
|
1564
|
+
let active = true;
|
|
1565
|
+
const stop = runtime.onCaptureStatus?.((event) => {
|
|
1566
|
+
if (active && mounts.store.state.get(sessionId) === token) fold(sessionId, event);
|
|
1567
|
+
});
|
|
1568
|
+
return () => {
|
|
1569
|
+
active = false;
|
|
1570
|
+
stop?.();
|
|
1571
|
+
mounts.update((current) => {
|
|
1572
|
+
if (current.get(sessionId) !== token) return current;
|
|
1573
|
+
const next = new Map(current);
|
|
1574
|
+
next.delete(sessionId);
|
|
1575
|
+
return next;
|
|
1576
|
+
});
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
//#endregion
|
|
1580
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorDocumentView.native.tsx
|
|
1581
|
+
const DRAFT_ID = "draft";
|
|
1582
|
+
/** One open document on the phone: the content to mark up, its notes, and one way to hand them to the agent. */
|
|
1583
|
+
function AuthorDocumentView(props) {
|
|
1584
|
+
const { body, notes, draft, status } = props;
|
|
1585
|
+
const [tool, setTool] = useState("pin");
|
|
1586
|
+
const [readingMode, setReadingMode] = useState("read");
|
|
1587
|
+
const [selection, setSelection] = useState({
|
|
1588
|
+
start: 0,
|
|
1589
|
+
end: 0
|
|
1590
|
+
});
|
|
1591
|
+
const [sourceSelection, setSourceSelection] = useState();
|
|
1592
|
+
const sourceKey = `${props.name}\n${props.sourceSha256 ?? ""}`;
|
|
1593
|
+
const selectedRange = body.kind === "text" && sourceSelection?.key === sourceKey ? nativeAuthorTextRange(body.text, sourceSelection) : void 0;
|
|
1594
|
+
const canInspectSource = body.kind === "text" && props.nativeContent === void 0;
|
|
1595
|
+
const markdown = body.kind === "text" && /\.(?:md|markdown)$/iu.test(props.name);
|
|
1596
|
+
const rangeLabel = (note) => {
|
|
1597
|
+
if (note.textRange === void 0) return "";
|
|
1598
|
+
const range = body.kind === "text" && note.sourceSha256 === props.sourceSha256 ? nativeAuthorTextRange(body.text, note.textRange) : void 0;
|
|
1599
|
+
return range === void 0 ? "Source selection: " : range.startLine === range.endLine ? `Line ${String(range.startLine)}: ` : `Lines ${String(range.startLine)} to ${String(range.endLine)}: `;
|
|
1600
|
+
};
|
|
1601
|
+
const formatMarkdown = (format) => {
|
|
1602
|
+
if (props.edit?.text === void 0 || props.edit.saving || !props.onEditChange) return;
|
|
1603
|
+
const text = props.edit.text;
|
|
1604
|
+
const edit = authorMarkdownFormat(text, Math.min(selection.start, text.length), Math.min(selection.end, text.length), format);
|
|
1605
|
+
props.onEditChange(text.slice(0, edit.from) + edit.insert + text.slice(edit.to));
|
|
1606
|
+
setSelection({
|
|
1607
|
+
start: edit.select.from,
|
|
1608
|
+
end: edit.select.to
|
|
1609
|
+
});
|
|
1610
|
+
};
|
|
1611
|
+
const marks = nativeAuthorVisibleNotes([...notes.map((note, index) => ({
|
|
1612
|
+
id: note.id,
|
|
1613
|
+
x: note.x,
|
|
1614
|
+
y: note.y,
|
|
1615
|
+
label: String(index + 1),
|
|
1616
|
+
description: note.text,
|
|
1617
|
+
points: note.points,
|
|
1618
|
+
page: note.page,
|
|
1619
|
+
timeSeconds: note.timeSeconds,
|
|
1620
|
+
sourceSha256: note.sourceSha256,
|
|
1621
|
+
textRange: note.textRange
|
|
1622
|
+
})), ...draft ? [{
|
|
1623
|
+
id: DRAFT_ID,
|
|
1624
|
+
x: draft.x,
|
|
1625
|
+
y: draft.y,
|
|
1626
|
+
label: String(notes.length + 1),
|
|
1627
|
+
points: draft.points,
|
|
1628
|
+
page: draft.page,
|
|
1629
|
+
timeSeconds: draft.timeSeconds,
|
|
1630
|
+
sourceSha256: draft.sourceSha256,
|
|
1631
|
+
textRange: draft.textRange
|
|
1632
|
+
}] : []].filter((mark) => mark.textRange === void 0 && (mark.sourceSha256 === void 0 || mark.sourceSha256 === props.sourceSha256)), body.kind, props.page, props.timeSeconds);
|
|
1633
|
+
const sending = status.state === "sending";
|
|
1634
|
+
const inspectingSource = canInspectSource && readingMode === "source";
|
|
1635
|
+
const annotation = /* @__PURE__ */ jsx(AnnotationSurface, {
|
|
1636
|
+
accessibilityLabel: props.name,
|
|
1637
|
+
zoomable: body.kind === "image" && !sending,
|
|
1638
|
+
onViewportChange: (viewport) => props.onZoomChange?.(viewport.zoom),
|
|
1639
|
+
marks: inspectingSource ? [] : marks,
|
|
1640
|
+
selectedId: inspectingSource ? void 0 : draft ? DRAFT_ID : props.selectedId,
|
|
1641
|
+
onPlace: inspectingSource || draft || sending || tool !== "pin" ? void 0 : props.onPlace,
|
|
1642
|
+
onDraw: !inspectingSource && !draft && !sending && props.onDraw && tool !== "pin" ? (points) => {
|
|
1643
|
+
if (tool === "draw") {
|
|
1644
|
+
props.onDraw?.(points);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const first = points[0];
|
|
1648
|
+
const last = points.at(-1);
|
|
1649
|
+
if (!first || !last) return;
|
|
1650
|
+
const rect = normalizedAuthorRectangle({
|
|
1651
|
+
left: 0,
|
|
1652
|
+
top: 0,
|
|
1653
|
+
right: 1,
|
|
1654
|
+
bottom: 1
|
|
1655
|
+
}, {
|
|
1656
|
+
left: first.x,
|
|
1657
|
+
top: first.y,
|
|
1658
|
+
right: last.x,
|
|
1659
|
+
bottom: last.y
|
|
1660
|
+
});
|
|
1661
|
+
if (rect) props.onDraw?.([
|
|
1662
|
+
{
|
|
1663
|
+
x: rect.x,
|
|
1664
|
+
y: rect.y
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
x: rect.x + rect.width,
|
|
1668
|
+
y: rect.y
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
x: rect.x + rect.width,
|
|
1672
|
+
y: rect.y + rect.height
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
x: rect.x,
|
|
1676
|
+
y: rect.y + rect.height
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
x: rect.x,
|
|
1680
|
+
y: rect.y
|
|
1681
|
+
}
|
|
1682
|
+
]);
|
|
1683
|
+
} : void 0,
|
|
1684
|
+
onSelectMark: inspectingSource ? void 0 : (id) => id !== DRAFT_ID && props.onSelect?.(id),
|
|
1685
|
+
children: body.kind === "image" ? /* @__PURE__ */ jsx(Image, {
|
|
1686
|
+
source: { uri: body.uri },
|
|
1687
|
+
style: [s.image, { aspectRatio: body.aspectRatio }],
|
|
1688
|
+
resizeMode: "contain"
|
|
1689
|
+
}) : body.kind === "structured" ? /* @__PURE__ */ jsxs(View, {
|
|
1690
|
+
style: s.notes,
|
|
1691
|
+
children: [body.format === "pptx" ? /* @__PURE__ */ jsx(Text, {
|
|
1692
|
+
style: s.meta,
|
|
1693
|
+
children: "Slide text, not a layout preview. Images, notes and embedded objects stay in the file."
|
|
1694
|
+
}) : null, body.fragments.map((fragment, index) => /* @__PURE__ */ jsxs(Pressable, {
|
|
1695
|
+
accessibilityRole: "button",
|
|
1696
|
+
accessibilityLabel: `Comment on ${fragment.location}`,
|
|
1697
|
+
disabled: !!draft || sending || !props.onPlace,
|
|
1698
|
+
style: [s.notes, s.fragment],
|
|
1699
|
+
onPress: (event) => {
|
|
1700
|
+
event.stopPropagation();
|
|
1701
|
+
props.onPlace?.({
|
|
1702
|
+
x: .5,
|
|
1703
|
+
y: (index + .5) / body.fragments.length,
|
|
1704
|
+
fragmentId: fragment.id
|
|
1705
|
+
});
|
|
1706
|
+
},
|
|
1707
|
+
children: [/* @__PURE__ */ jsx(SectionLabel, { children: fragment.location }), /* @__PURE__ */ jsx(Text, {
|
|
1708
|
+
style: s.text,
|
|
1709
|
+
children: fragment.text
|
|
1710
|
+
})]
|
|
1711
|
+
}, fragment.id))]
|
|
1712
|
+
}) : props.nativeContent ?? (body.kind === "text" ? inspectingSource ? /* @__PURE__ */ jsx(Textarea, {
|
|
1713
|
+
accessibilityLabel: `Select source text in ${props.name}`,
|
|
1714
|
+
style: s.text,
|
|
1715
|
+
rows: 16,
|
|
1716
|
+
value: body.text,
|
|
1717
|
+
readOnly: true,
|
|
1718
|
+
showSoftInputOnFocus: false,
|
|
1719
|
+
scrollEnabled: true,
|
|
1720
|
+
onSelectionChange: (event) => {
|
|
1721
|
+
const { start, end } = event.nativeEvent.selection;
|
|
1722
|
+
setSourceSelection({
|
|
1723
|
+
key: sourceKey,
|
|
1724
|
+
startOffset: start,
|
|
1725
|
+
endOffset: end
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
}) : markdown ? /* @__PURE__ */ jsx(Markdown, { text: body.text }) : /* @__PURE__ */ jsx(Text, {
|
|
1729
|
+
style: s.text,
|
|
1730
|
+
children: body.text
|
|
1731
|
+
}) : null)
|
|
1732
|
+
});
|
|
1733
|
+
return /* @__PURE__ */ jsxs(View, {
|
|
1734
|
+
style: s.screen,
|
|
1735
|
+
children: [
|
|
1736
|
+
/* @__PURE__ */ jsxs(View, {
|
|
1737
|
+
style: s.header,
|
|
1738
|
+
children: [
|
|
1739
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1740
|
+
style: s.title,
|
|
1741
|
+
numberOfLines: 1,
|
|
1742
|
+
children: props.name
|
|
1743
|
+
}),
|
|
1744
|
+
props.ownedElsewhere ? /* @__PURE__ */ jsxs(View, {
|
|
1745
|
+
style: s.ask,
|
|
1746
|
+
children: [/* @__PURE__ */ jsx(Badge, {
|
|
1747
|
+
tone: "magenta",
|
|
1748
|
+
children: "Open on another device"
|
|
1749
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
1750
|
+
style: s.meta,
|
|
1751
|
+
children: "You can still add notes. The agent keeps using the other device until it closes there."
|
|
1752
|
+
})]
|
|
1753
|
+
}) : null,
|
|
1754
|
+
props.onEdit && !props.edit ? /* @__PURE__ */ jsx(Button, {
|
|
1755
|
+
variant: "ghost",
|
|
1756
|
+
disabled: sending || draft !== void 0,
|
|
1757
|
+
onPress: props.onEdit,
|
|
1758
|
+
children: body.kind === "structured" ? "Edit document" : "Edit text"
|
|
1759
|
+
}) : null,
|
|
1760
|
+
props.onDraw && !props.edit && !inspectingSource ? /* @__PURE__ */ jsx(SegmentedControl, {
|
|
1761
|
+
accessibilityLabel: "Mark tool",
|
|
1762
|
+
options: [
|
|
1763
|
+
{
|
|
1764
|
+
value: "pin",
|
|
1765
|
+
label: "Pin"
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
value: "rectangle",
|
|
1769
|
+
label: "Rectangle"
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
value: "draw",
|
|
1773
|
+
label: "Draw"
|
|
1774
|
+
}
|
|
1775
|
+
],
|
|
1776
|
+
value: tool,
|
|
1777
|
+
disabled: sending || draft !== void 0,
|
|
1778
|
+
onValueChange: (value) => {
|
|
1779
|
+
if (value === "pin" || value === "rectangle" || value === "draw") setTool(value);
|
|
1780
|
+
}
|
|
1781
|
+
}) : null,
|
|
1782
|
+
props.onCrop && !props.edit ? /* @__PURE__ */ jsx(Button, {
|
|
1783
|
+
variant: "ghost",
|
|
1784
|
+
disabled: sending || !!draft,
|
|
1785
|
+
loading: props.cropping,
|
|
1786
|
+
onPress: props.onCrop,
|
|
1787
|
+
children: "Crop to selected mark"
|
|
1788
|
+
}) : null,
|
|
1789
|
+
props.onResumeVideo ? /* @__PURE__ */ jsx(Button, {
|
|
1790
|
+
variant: "ghost",
|
|
1791
|
+
disabled: sending || !!draft,
|
|
1792
|
+
onPress: props.onResumeVideo,
|
|
1793
|
+
children: "Back to video"
|
|
1794
|
+
}) : null,
|
|
1795
|
+
canInspectSource && !props.edit ? /* @__PURE__ */ jsx(SegmentedControl, {
|
|
1796
|
+
accessibilityLabel: "Document view",
|
|
1797
|
+
options: [{
|
|
1798
|
+
value: "read",
|
|
1799
|
+
label: "Read"
|
|
1800
|
+
}, {
|
|
1801
|
+
value: "source",
|
|
1802
|
+
label: "Source"
|
|
1803
|
+
}],
|
|
1804
|
+
value: readingMode,
|
|
1805
|
+
disabled: sending || !!draft,
|
|
1806
|
+
onValueChange: (value) => {
|
|
1807
|
+
if (value === "read" || value === "source") setReadingMode(value);
|
|
1808
|
+
}
|
|
1809
|
+
}) : null,
|
|
1810
|
+
inspectingSource && props.onSelectText && !props.edit ? /* @__PURE__ */ jsx(Button, {
|
|
1811
|
+
variant: "ghost",
|
|
1812
|
+
disabled: sending || !!draft || selectedRange === void 0,
|
|
1813
|
+
onPress: () => {
|
|
1814
|
+
if (selectedRange !== void 0) props.onSelectText?.({
|
|
1815
|
+
startOffset: selectedRange.startOffset,
|
|
1816
|
+
endOffset: selectedRange.endOffset
|
|
1817
|
+
});
|
|
1818
|
+
},
|
|
1819
|
+
children: "Note on selected text"
|
|
1820
|
+
}) : null,
|
|
1821
|
+
props.storageMessage ? /* @__PURE__ */ jsx(Text, {
|
|
1822
|
+
style: s.meta,
|
|
1823
|
+
accessibilityLiveRegion: "polite",
|
|
1824
|
+
children: props.storageMessage
|
|
1825
|
+
}) : null
|
|
1826
|
+
]
|
|
1827
|
+
}),
|
|
1828
|
+
/* @__PURE__ */ jsxs(ScrollView, {
|
|
1829
|
+
style: s.scroll,
|
|
1830
|
+
contentContainerStyle: s.content,
|
|
1831
|
+
keyboardShouldPersistTaps: "handled",
|
|
1832
|
+
children: [
|
|
1833
|
+
props.edit?.image ? /* @__PURE__ */ jsx(Image, {
|
|
1834
|
+
accessibilityLabel: "Crop preview",
|
|
1835
|
+
source: { uri: props.edit.image.uri },
|
|
1836
|
+
style: [s.image, { aspectRatio: props.edit.image.aspectRatio }],
|
|
1837
|
+
resizeMode: "contain"
|
|
1838
|
+
}) : props.edit?.fragments !== void 0 ? /* @__PURE__ */ jsx(View, {
|
|
1839
|
+
style: s.notes,
|
|
1840
|
+
children: props.edit.fragments.map((fragment) => /* @__PURE__ */ jsxs(View, {
|
|
1841
|
+
style: s.notes,
|
|
1842
|
+
children: [/* @__PURE__ */ jsxs(SectionLabel, { children: [fragment.location, fragment.readOnly ? " (read-only)" : ""] }), /* @__PURE__ */ jsx(Textarea, {
|
|
1843
|
+
accessibilityLabel: `Edit ${fragment.location}`,
|
|
1844
|
+
value: fragment.text,
|
|
1845
|
+
disabled: props.edit?.saving || fragment.readOnly,
|
|
1846
|
+
onChangeText: (text) => props.onEditFragmentChange?.(fragment.id, text)
|
|
1847
|
+
})]
|
|
1848
|
+
}, fragment.id))
|
|
1849
|
+
}) : props.edit ? /* @__PURE__ */ jsxs(View, {
|
|
1850
|
+
style: s.notes,
|
|
1851
|
+
children: [markdown ? /* @__PURE__ */ jsx(View, {
|
|
1852
|
+
style: s.toolbar,
|
|
1853
|
+
children: [
|
|
1854
|
+
"bold",
|
|
1855
|
+
"heading",
|
|
1856
|
+
"list",
|
|
1857
|
+
"link"
|
|
1858
|
+
].map((format) => /* @__PURE__ */ jsx(Button, {
|
|
1859
|
+
variant: "ghost",
|
|
1860
|
+
disabled: props.edit?.saving || !props.onEditChange,
|
|
1861
|
+
accessibilityLabel: `Format ${format}`,
|
|
1862
|
+
onPress: () => formatMarkdown(format),
|
|
1863
|
+
children: format[0].toUpperCase() + format.slice(1)
|
|
1864
|
+
}, format))
|
|
1865
|
+
}) : null, /* @__PURE__ */ jsx(Textarea, {
|
|
1866
|
+
accessibilityLabel: `Edit ${props.name}`,
|
|
1867
|
+
rows: 16,
|
|
1868
|
+
value: props.edit.text,
|
|
1869
|
+
onChangeText: props.onEditChange,
|
|
1870
|
+
selection: markdown ? selection : void 0,
|
|
1871
|
+
onSelectionChange: markdown ? (event) => setSelection(event.nativeEvent.selection) : void 0,
|
|
1872
|
+
disabled: props.edit.saving,
|
|
1873
|
+
autoFocus: true
|
|
1874
|
+
})]
|
|
1875
|
+
}) : props.captureContent ? props.captureContent(annotation) : annotation,
|
|
1876
|
+
(notes.length === 0 || inspectingSource) && !draft && !props.edit ? /* @__PURE__ */ jsx(Text, {
|
|
1877
|
+
style: s.meta,
|
|
1878
|
+
children: inspectingSource ? "Select source text to add a note. Switch to Read to send notes." : `${body.kind === "image" ? "Tap the image" : "Tap the document"} where something should change, then say what.`
|
|
1879
|
+
}) : null,
|
|
1880
|
+
notes.length > 0 ? /* @__PURE__ */ jsxs(View, {
|
|
1881
|
+
style: s.notes,
|
|
1882
|
+
children: [/* @__PURE__ */ jsx(SectionLabel, { children: "Notes" }), notes.map((note, index) => /* @__PURE__ */ jsxs(Pressable, {
|
|
1883
|
+
accessibilityRole: "button",
|
|
1884
|
+
accessibilityState: { selected: note.id === props.selectedId },
|
|
1885
|
+
onPress: () => props.onSelect?.(note.id),
|
|
1886
|
+
style: [s.note, note.id === props.selectedId && s.noteSelected],
|
|
1887
|
+
children: [
|
|
1888
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1889
|
+
style: s.number,
|
|
1890
|
+
children: index + 1
|
|
1891
|
+
}),
|
|
1892
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
1893
|
+
style: s.noteText,
|
|
1894
|
+
children: [
|
|
1895
|
+
note.sourceSha256 !== void 0 && note.sourceSha256 !== props.sourceSha256 ? "Earlier version: " : "",
|
|
1896
|
+
note.page !== void 0 ? `Page ${String(note.page)}: ` : note.timeSeconds !== void 0 ? `${String(Math.round(note.timeSeconds * 10) / 10)}s: ` : "",
|
|
1897
|
+
rangeLabel(note),
|
|
1898
|
+
note.text
|
|
1899
|
+
]
|
|
1900
|
+
}),
|
|
1901
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1902
|
+
variant: "ghost",
|
|
1903
|
+
size: "icon",
|
|
1904
|
+
accessibilityLabel: `Remove note ${index + 1}`,
|
|
1905
|
+
disabled: sending,
|
|
1906
|
+
onPress: () => props.onDelete?.(note.id),
|
|
1907
|
+
children: /* @__PURE__ */ jsx(TrashIcon, {})
|
|
1908
|
+
})
|
|
1909
|
+
]
|
|
1910
|
+
}, note.id))]
|
|
1911
|
+
}) : null,
|
|
1912
|
+
props.requests?.length ? /* @__PURE__ */ jsxs(View, {
|
|
1913
|
+
style: s.notes,
|
|
1914
|
+
children: [/* @__PURE__ */ jsx(SectionLabel, { children: "Requests" }), props.requests.map((request, index) => /* @__PURE__ */ jsxs(View, {
|
|
1915
|
+
style: s.notes,
|
|
1916
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
1917
|
+
style: request.status === "FAILED" ? s.failed : s.meta,
|
|
1918
|
+
children: `Request ${String(index + 1)}: ${request.status.toLowerCase()}`
|
|
1919
|
+
}), request.error ? /* @__PURE__ */ jsx(Text, {
|
|
1920
|
+
style: s.failed,
|
|
1921
|
+
children: request.error
|
|
1922
|
+
}) : null]
|
|
1923
|
+
}, request.id))]
|
|
1924
|
+
}) : null
|
|
1925
|
+
]
|
|
1926
|
+
}),
|
|
1927
|
+
props.edit || draft || notes.length > 0 || status.state !== "idle" ? /* @__PURE__ */ jsx(View, {
|
|
1928
|
+
style: s.footer,
|
|
1929
|
+
children: props.edit ? /* @__PURE__ */ jsxs(Fragment, { children: [props.edit.error ? /* @__PURE__ */ jsxs(Text, {
|
|
1930
|
+
style: s.failed,
|
|
1931
|
+
accessibilityLiveRegion: "polite",
|
|
1932
|
+
children: [props.edit.error, " Your edits are still here."]
|
|
1933
|
+
}) : null, /* @__PURE__ */ jsxs(View, {
|
|
1934
|
+
style: s.row,
|
|
1935
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
1936
|
+
variant: "ghost",
|
|
1937
|
+
style: s.grow,
|
|
1938
|
+
disabled: props.edit.saving,
|
|
1939
|
+
onPress: props.onEditCancel,
|
|
1940
|
+
children: "Cancel"
|
|
1941
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1942
|
+
variant: "primary",
|
|
1943
|
+
style: s.grow,
|
|
1944
|
+
loading: props.edit.saving,
|
|
1945
|
+
loadingLabel: "Saving document",
|
|
1946
|
+
onPress: props.onEditSave,
|
|
1947
|
+
children: "Save"
|
|
1948
|
+
})]
|
|
1949
|
+
})] }) : draft ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Textarea, {
|
|
1950
|
+
autoFocus: true,
|
|
1951
|
+
accessibilityLabel: `Note ${notes.length + 1}`,
|
|
1952
|
+
placeholder: "What should change here?",
|
|
1953
|
+
maxLength: 2048,
|
|
1954
|
+
value: draft.text,
|
|
1955
|
+
onChangeText: props.onDraftChange
|
|
1956
|
+
}), /* @__PURE__ */ jsxs(View, {
|
|
1957
|
+
style: s.row,
|
|
1958
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
1959
|
+
variant: "ghost",
|
|
1960
|
+
style: s.grow,
|
|
1961
|
+
onPress: props.onDraftCancel,
|
|
1962
|
+
children: "Cancel"
|
|
1963
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1964
|
+
variant: "primary",
|
|
1965
|
+
style: s.grow,
|
|
1966
|
+
disabled: !draft.text.trim(),
|
|
1967
|
+
onPress: props.onDraftSave,
|
|
1968
|
+
children: "Add note"
|
|
1969
|
+
})]
|
|
1970
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1971
|
+
status.state === "failed" ? /* @__PURE__ */ jsxs(Text, {
|
|
1972
|
+
style: s.failed,
|
|
1973
|
+
accessibilityLiveRegion: "polite",
|
|
1974
|
+
children: [
|
|
1975
|
+
"Couldn't send: ",
|
|
1976
|
+
status.message,
|
|
1977
|
+
" Your notes are still here."
|
|
1978
|
+
]
|
|
1979
|
+
}) : null,
|
|
1980
|
+
status.state === "sent" ? /* @__PURE__ */ jsx(Text, {
|
|
1981
|
+
style: s.meta,
|
|
1982
|
+
accessibilityLiveRegion: "polite",
|
|
1983
|
+
children: "Sent to the agent. You can keep adding notes."
|
|
1984
|
+
}) : null,
|
|
1985
|
+
notes.length > 0 ? /* @__PURE__ */ jsx(Button, {
|
|
1986
|
+
variant: "primary",
|
|
1987
|
+
size: "lg",
|
|
1988
|
+
loading: sending,
|
|
1989
|
+
disabled: inspectingSource || props.sendCount === 0,
|
|
1990
|
+
loadingLabel: "Sending notes",
|
|
1991
|
+
onPress: props.onSend,
|
|
1992
|
+
children: status.state === "failed" ? "Try again" : props.sendCount === 0 ? "Open a marked page or frame to send" : `Send ${props.sendCount ?? notes.length} note${(props.sendCount ?? notes.length) === 1 ? "" : "s"} to the agent`
|
|
1993
|
+
}) : null
|
|
1994
|
+
] })
|
|
1995
|
+
}) : null
|
|
1996
|
+
]
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
const s = StyleSheet.create((theme) => ({
|
|
2000
|
+
screen: {
|
|
2001
|
+
flex: 1,
|
|
2002
|
+
backgroundColor: theme.colors.bg,
|
|
2003
|
+
uni__dependencies: [0]
|
|
2004
|
+
},
|
|
2005
|
+
header: {
|
|
2006
|
+
gap: theme.spacing(2),
|
|
2007
|
+
padding: theme.spacing(4),
|
|
2008
|
+
paddingBottom: theme.spacing(2),
|
|
2009
|
+
uni__dependencies: [0]
|
|
2010
|
+
},
|
|
2011
|
+
title: {
|
|
2012
|
+
color: theme.colors.hi,
|
|
2013
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2014
|
+
fontSize: theme.fontSize.lg,
|
|
2015
|
+
lineHeight: theme.lineHeight.lg,
|
|
2016
|
+
fontWeight: theme.fontWeight.semibold,
|
|
2017
|
+
uni__dependencies: [0]
|
|
2018
|
+
},
|
|
2019
|
+
ask: {
|
|
2020
|
+
alignItems: "flex-start",
|
|
2021
|
+
gap: theme.spacing(2),
|
|
2022
|
+
padding: theme.spacing(3),
|
|
2023
|
+
borderWidth: 1,
|
|
2024
|
+
borderColor: theme.colors["edge-magenta"],
|
|
2025
|
+
backgroundColor: theme.colors["tint-magenta"],
|
|
2026
|
+
borderRadius: theme.borderRadius.md,
|
|
2027
|
+
uni__dependencies: [0]
|
|
2028
|
+
},
|
|
2029
|
+
scroll: { flex: 1 },
|
|
2030
|
+
content: {
|
|
2031
|
+
gap: theme.spacing(4),
|
|
2032
|
+
padding: theme.spacing(4),
|
|
2033
|
+
uni__dependencies: [0]
|
|
2034
|
+
},
|
|
2035
|
+
image: {
|
|
2036
|
+
width: "100%",
|
|
2037
|
+
borderRadius: theme.borderRadius.md,
|
|
2038
|
+
backgroundColor: theme.colors.deep,
|
|
2039
|
+
uni__dependencies: [0]
|
|
2040
|
+
},
|
|
2041
|
+
text: {
|
|
2042
|
+
padding: theme.spacing(3),
|
|
2043
|
+
borderRadius: theme.borderRadius.md,
|
|
2044
|
+
backgroundColor: theme.colors.deep,
|
|
2045
|
+
color: theme.colors.hi,
|
|
2046
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
2047
|
+
fontSize: theme.fontSize.sm,
|
|
2048
|
+
lineHeight: theme.lineHeightRelaxed.sm,
|
|
2049
|
+
uni__dependencies: [0]
|
|
2050
|
+
},
|
|
2051
|
+
meta: {
|
|
2052
|
+
color: theme.colors.dim,
|
|
2053
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2054
|
+
fontSize: theme.fontSize.sm,
|
|
2055
|
+
lineHeight: theme.lineHeight.sm,
|
|
2056
|
+
uni__dependencies: [0]
|
|
2057
|
+
},
|
|
2058
|
+
notes: {
|
|
2059
|
+
gap: theme.spacing(2),
|
|
2060
|
+
uni__dependencies: [0]
|
|
2061
|
+
},
|
|
2062
|
+
fragment: { minHeight: 44 },
|
|
2063
|
+
note: {
|
|
2064
|
+
minHeight: 44,
|
|
2065
|
+
flexDirection: "row",
|
|
2066
|
+
alignItems: "center",
|
|
2067
|
+
gap: theme.spacing(3),
|
|
2068
|
+
paddingLeft: theme.spacing(3),
|
|
2069
|
+
borderWidth: 1,
|
|
2070
|
+
borderColor: theme.colors.border,
|
|
2071
|
+
borderRadius: theme.borderRadius.md,
|
|
2072
|
+
uni__dependencies: [0]
|
|
2073
|
+
},
|
|
2074
|
+
noteSelected: {
|
|
2075
|
+
borderColor: theme.colors["edge-magenta"],
|
|
2076
|
+
backgroundColor: theme.colors["tint-magenta"],
|
|
2077
|
+
uni__dependencies: [0]
|
|
2078
|
+
},
|
|
2079
|
+
number: {
|
|
2080
|
+
color: theme.colors.magenta,
|
|
2081
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2082
|
+
fontSize: theme.fontSize.sm,
|
|
2083
|
+
fontWeight: theme.fontWeight.semibold,
|
|
2084
|
+
uni__dependencies: [0]
|
|
2085
|
+
},
|
|
2086
|
+
noteText: {
|
|
2087
|
+
flex: 1,
|
|
2088
|
+
paddingVertical: theme.spacing(2),
|
|
2089
|
+
color: theme.colors.hi,
|
|
2090
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2091
|
+
fontSize: theme.fontSize.base,
|
|
2092
|
+
lineHeight: theme.lineHeight.base,
|
|
2093
|
+
uni__dependencies: [0]
|
|
2094
|
+
},
|
|
2095
|
+
footer: {
|
|
2096
|
+
gap: theme.spacing(3),
|
|
2097
|
+
padding: theme.spacing(4),
|
|
2098
|
+
borderTopWidth: 1,
|
|
2099
|
+
borderTopColor: theme.colors.border,
|
|
2100
|
+
backgroundColor: theme.colors.rail,
|
|
2101
|
+
uni__dependencies: [0]
|
|
2102
|
+
},
|
|
2103
|
+
failed: {
|
|
2104
|
+
color: theme.colors.red,
|
|
2105
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2106
|
+
fontSize: theme.fontSize.sm,
|
|
2107
|
+
lineHeight: theme.lineHeight.sm,
|
|
2108
|
+
uni__dependencies: [0]
|
|
2109
|
+
},
|
|
2110
|
+
toolbar: {
|
|
2111
|
+
flexDirection: "row",
|
|
2112
|
+
flexWrap: "wrap",
|
|
2113
|
+
gap: theme.spacing(1),
|
|
2114
|
+
uni__dependencies: [0]
|
|
2115
|
+
},
|
|
2116
|
+
row: {
|
|
2117
|
+
flexDirection: "row",
|
|
2118
|
+
gap: theme.spacing(2),
|
|
2119
|
+
uni__dependencies: [0]
|
|
2120
|
+
},
|
|
2121
|
+
grow: { flex: 1 }
|
|
2122
|
+
}));
|
|
2123
|
+
//#endregion
|
|
2124
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorDocumentPanel.native.tsx
|
|
2125
|
+
const notesStore = defineGlobalStore({});
|
|
2126
|
+
const noteKey = (sessionId, path) => `${sessionId}\n${path}`;
|
|
2127
|
+
const EMPTY = [];
|
|
2128
|
+
const subscribeNotes = (listener) => {
|
|
2129
|
+
const subscription = notesStore.store.subscribe(listener);
|
|
2130
|
+
return () => subscription.unsubscribe();
|
|
2131
|
+
};
|
|
2132
|
+
const subscribeRequests = (listener) => {
|
|
2133
|
+
const subscription = nativeAuthorRequests.store.subscribe(listener);
|
|
2134
|
+
return () => subscription.unsubscribe();
|
|
2135
|
+
};
|
|
2136
|
+
function setNotes(key, update) {
|
|
2137
|
+
notesStore.update((state) => ({
|
|
2138
|
+
...state,
|
|
2139
|
+
[key]: update(state[key] ?? EMPTY)
|
|
2140
|
+
}));
|
|
2141
|
+
}
|
|
2142
|
+
const formsStore = defineGlobalStore({});
|
|
2143
|
+
const subscribeForms = (listener) => {
|
|
2144
|
+
const subscription = formsStore.store.subscribe(listener);
|
|
2145
|
+
return () => subscription.unsubscribe();
|
|
2146
|
+
};
|
|
2147
|
+
const hydratedKeys = /* @__PURE__ */ new Set();
|
|
2148
|
+
function setForm(key, draft) {
|
|
2149
|
+
formsStore.update((state) => ({
|
|
2150
|
+
...state,
|
|
2151
|
+
[key]: draft
|
|
2152
|
+
}));
|
|
2153
|
+
}
|
|
2154
|
+
/** The server-confirmed canvas alias for each open document, the only name the agent knows it by. */
|
|
2155
|
+
const aliasStore = defineGlobalStore({});
|
|
2156
|
+
const subscribeAliases = (listener) => {
|
|
2157
|
+
const subscription = aliasStore.store.subscribe(listener);
|
|
2158
|
+
return () => subscription.unsubscribe();
|
|
2159
|
+
};
|
|
2160
|
+
const pendingCanvases = /* @__PURE__ */ new Map();
|
|
2161
|
+
/** The loaded text per document, so the agent's comments can be pinned beside the line they quote. */
|
|
2162
|
+
const documentTexts = /* @__PURE__ */ new Map();
|
|
2163
|
+
function forgetAlias(key) {
|
|
2164
|
+
aliasStore.update((state) => {
|
|
2165
|
+
const next = { ...state };
|
|
2166
|
+
delete next[key];
|
|
2167
|
+
return next;
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
const COMMENT_SCHEMA = {
|
|
2171
|
+
type: "object",
|
|
2172
|
+
additionalProperties: false,
|
|
2173
|
+
required: ["body"],
|
|
2174
|
+
properties: {
|
|
2175
|
+
body: {
|
|
2176
|
+
type: "string",
|
|
2177
|
+
minLength: 1
|
|
2178
|
+
},
|
|
2179
|
+
quote: { type: "string" }
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
/** What the agent may do on the phone's canvas: text takes comments as notes, images are read-only as on web. */
|
|
2183
|
+
function nativeProfiles(key, kind) {
|
|
2184
|
+
if (kind !== "text" && kind !== "structured") return [{
|
|
2185
|
+
id: AUTHOR_RUNTIME_BINDING_IDS.media,
|
|
2186
|
+
tools: []
|
|
2187
|
+
}];
|
|
2188
|
+
return [{
|
|
2189
|
+
id: AUTHOR_RUNTIME_BINDING_IDS.text,
|
|
2190
|
+
tools: [{
|
|
2191
|
+
name: "author_add_comment",
|
|
2192
|
+
label: "Add comment",
|
|
2193
|
+
description: "Add a review note to the open Author document, beside the quoted text when given.",
|
|
2194
|
+
inputSchema: COMMENT_SCHEMA,
|
|
2195
|
+
execute: async (input) => {
|
|
2196
|
+
const value = typeof input === "object" && input !== null ? input : {};
|
|
2197
|
+
if (typeof value.body !== "string" || value.body.trim() === "" || value.body.length > 2048) throw new Error("body must be 1 to 2048 characters");
|
|
2198
|
+
if (value.quote !== void 0 && (typeof value.quote !== "string" || value.quote.length > 4096)) throw new Error("quote must be bounded text");
|
|
2199
|
+
if (Object.keys(value).some((name) => name !== "body" && name !== "quote")) throw new Error("Unknown comment argument.");
|
|
2200
|
+
const body = value.body;
|
|
2201
|
+
const document = documentTexts.get(key);
|
|
2202
|
+
const text = document?.text ?? "";
|
|
2203
|
+
const at = typeof value.quote === "string" && value.quote !== "" ? text.indexOf(value.quote) : -1;
|
|
2204
|
+
const lines = text.split("\n").length;
|
|
2205
|
+
const line = at < 0 ? 0 : text.slice(0, at).split("\n").length - 1;
|
|
2206
|
+
const y = Math.min(1, (line + .5) / Math.max(1, lines));
|
|
2207
|
+
setNotes(key, (notes) => [...notes, {
|
|
2208
|
+
id: `agent-${String(Date.now())}-${String(notes.length)}`,
|
|
2209
|
+
x: .04,
|
|
2210
|
+
y,
|
|
2211
|
+
text: body,
|
|
2212
|
+
sourceSha256: document?.sourceSha256
|
|
2213
|
+
}]);
|
|
2214
|
+
return { added: true };
|
|
2215
|
+
}
|
|
2216
|
+
}]
|
|
2217
|
+
}];
|
|
2218
|
+
}
|
|
2219
|
+
/** Hands the document to the agent once per tab; a second call while open or opening does nothing. */
|
|
2220
|
+
function openNativeCanvas(sessionId, path, kind) {
|
|
2221
|
+
const key = noteKey(sessionId, path);
|
|
2222
|
+
if (pendingCanvases.has(key) || aliasStore.store.state[key] !== void 0) return;
|
|
2223
|
+
const controller = new AbortController();
|
|
2224
|
+
pendingCanvases.set(key, controller);
|
|
2225
|
+
openAuthorNativeCanvas(sessionId, path, controller.signal).then(async ({ alias }) => {
|
|
2226
|
+
if (controller.signal.aborted) return;
|
|
2227
|
+
aliasStore.update((state) => ({
|
|
2228
|
+
...state,
|
|
2229
|
+
[key]: alias
|
|
2230
|
+
}));
|
|
2231
|
+
await openAuthorCanvas(sessionId, alias, nativeProfiles(key, kind));
|
|
2232
|
+
}).catch((error) => {
|
|
2233
|
+
if (!controller.signal.aborted) console.warn(`Author could not hand '${path}' to the agent: ${String(error)}`);
|
|
2234
|
+
}).finally(() => {
|
|
2235
|
+
if (pendingCanvases.get(key) === controller) pendingCanvases.delete(key);
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
function closeNativeCanvas(sessionId, path) {
|
|
2239
|
+
const key = noteKey(sessionId, path);
|
|
2240
|
+
pendingCanvases.get(key)?.abort();
|
|
2241
|
+
pendingCanvases.delete(key);
|
|
2242
|
+
documentTexts.delete(key);
|
|
2243
|
+
const alias = aliasStore.store.state[key];
|
|
2244
|
+
if (alias === void 0) return;
|
|
2245
|
+
forgetAlias(key);
|
|
2246
|
+
dropAuthorViewportSession(sessionId, alias);
|
|
2247
|
+
}
|
|
2248
|
+
const fileName = (path) => (path.startsWith("artifact:") ? path.slice(9) : path).split("/").at(-1) ?? path;
|
|
2249
|
+
const editsStore = defineGlobalStore({});
|
|
2250
|
+
const subscribeEdits = (listener) => {
|
|
2251
|
+
const subscription = editsStore.store.subscribe(listener);
|
|
2252
|
+
return () => subscription.unsubscribe();
|
|
2253
|
+
};
|
|
2254
|
+
function setEdit(key, edit) {
|
|
2255
|
+
editsStore.update((state) => ({
|
|
2256
|
+
...state,
|
|
2257
|
+
[key]: edit
|
|
2258
|
+
}));
|
|
2259
|
+
}
|
|
2260
|
+
async function persistNativeDraft(key, path, host) {
|
|
2261
|
+
if (!hydratedKeys.has(key) || typeof host?.saveDraft !== "function") return;
|
|
2262
|
+
const edit = editsStore.store.state[key];
|
|
2263
|
+
const savedEdit = edit?.kind === "text" ? {
|
|
2264
|
+
kind: "text",
|
|
2265
|
+
text: edit.text,
|
|
2266
|
+
sourceSha256: edit.sourceSha256
|
|
2267
|
+
} : edit?.kind === "structured" ? {
|
|
2268
|
+
kind: "structured",
|
|
2269
|
+
sourceSha256: edit.file.sourceSha256,
|
|
2270
|
+
fragments: edit.file.fragments.map(({ id, text }) => ({
|
|
2271
|
+
id,
|
|
2272
|
+
text
|
|
2273
|
+
}))
|
|
2274
|
+
} : void 0;
|
|
2275
|
+
const value = parseNativeAuthorDraft({
|
|
2276
|
+
version: 1,
|
|
2277
|
+
notes: notesStore.store.state[key] ?? EMPTY,
|
|
2278
|
+
draft: formsStore.store.state[key],
|
|
2279
|
+
edit: savedEdit
|
|
2280
|
+
});
|
|
2281
|
+
await host.saveDraft(path, value.notes.length || value.draft || value.edit ? value : null);
|
|
2282
|
+
}
|
|
2283
|
+
/** The phone's Author canvas: loads one document, keeps its notes, and hands them to the agent. */
|
|
2284
|
+
function AuthorDocumentPanel({ path, ...props }) {
|
|
2285
|
+
const { sessionId } = props;
|
|
2286
|
+
const key = noteKey(sessionId ?? "", path);
|
|
2287
|
+
const notes = useSyncExternalStore(subscribeNotes, () => notesStore.store.state[key] ?? EMPTY);
|
|
2288
|
+
const alias = useSyncExternalStore(subscribeAliases, () => aliasStore.store.state[key]);
|
|
2289
|
+
const ownedElsewhere = useSyncExternalStore(subscribeAuthorCanvasRejections, () => alias !== void 0 && sessionId !== null && authorCanvasRejection(sessionId, alias) !== void 0);
|
|
2290
|
+
const [loaded, setLoaded] = useState();
|
|
2291
|
+
const draft = useSyncExternalStore(subscribeForms, () => formsStore.store.state[key]);
|
|
2292
|
+
const setDraft = (value) => setForm(key, value);
|
|
2293
|
+
const [storageState, setStorageState] = useState();
|
|
2294
|
+
const [selectedId, setSelectedId] = useState();
|
|
2295
|
+
const [status, setStatus] = useState({ state: "idle" });
|
|
2296
|
+
const requests = useSyncExternalStore(subscribeRequests, () => nativeAuthorRequests.select(nativeAuthorRequests.store.state, sessionId));
|
|
2297
|
+
const sending = useRef(false);
|
|
2298
|
+
const [mediaPage, setMediaPage] = useState(1);
|
|
2299
|
+
const [seekSeconds, setSeekSeconds] = useState();
|
|
2300
|
+
const [cropping, setCropping] = useState(false);
|
|
2301
|
+
const [videoFrame, setVideoFrame] = useState();
|
|
2302
|
+
const mediaTime = useRef(0);
|
|
2303
|
+
const imageZoom = useRef(1);
|
|
2304
|
+
const edit = useSyncExternalStore(subscribeEdits, () => editsStore.store.state[key]);
|
|
2305
|
+
const captureRef = useRef(void 0);
|
|
2306
|
+
const onCaptureReady = useCallback((capture) => {
|
|
2307
|
+
if (capture !== void 0) captureRef.current = {
|
|
2308
|
+
key,
|
|
2309
|
+
capture
|
|
2310
|
+
};
|
|
2311
|
+
else if (captureRef.current?.key === key) captureRef.current = void 0;
|
|
2312
|
+
}, [key]);
|
|
2313
|
+
useEffect(() => {
|
|
2314
|
+
if (sessionId === null) return;
|
|
2315
|
+
const controller = new AbortController();
|
|
2316
|
+
const host = props.nativeAuthor;
|
|
2317
|
+
const fail = (error) => {
|
|
2318
|
+
if (!controller.signal.aborted) setLoaded({
|
|
2319
|
+
key,
|
|
2320
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2321
|
+
});
|
|
2322
|
+
};
|
|
2323
|
+
(async () => {
|
|
2324
|
+
let restoredEdit;
|
|
2325
|
+
let restoreReady = hydratedKeys.has(key);
|
|
2326
|
+
if (!hydratedKeys.has(key)) try {
|
|
2327
|
+
if (typeof host?.loadDraft !== "function") throw new Error("Update the native app to keep drafts across restarts.");
|
|
2328
|
+
const value = await host.loadDraft(path);
|
|
2329
|
+
if (controller.signal.aborted) return;
|
|
2330
|
+
if (value !== null) {
|
|
2331
|
+
const restored = parseNativeAuthorDraft(value);
|
|
2332
|
+
setNotes(key, (notes) => [...restored.notes.filter((note) => !notes.some((current) => current.id === note.id)), ...notes]);
|
|
2333
|
+
if (formsStore.store.state[key] === void 0) setForm(key, restored.draft);
|
|
2334
|
+
restoredEdit = restored.edit;
|
|
2335
|
+
}
|
|
2336
|
+
restoreReady = true;
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
if (controller.signal.aborted) return;
|
|
2339
|
+
setStorageState({
|
|
2340
|
+
key,
|
|
2341
|
+
message: `Draft storage unavailable: ${error instanceof Error ? error.message : String(error)} Keep this tab open.`
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
const file = await loadAuthorFile(sessionId, path, controller.signal);
|
|
2345
|
+
if (controller.signal.aborted) return;
|
|
2346
|
+
if (restoredEdit && editsStore.store.state[key] === void 0) {
|
|
2347
|
+
if (restoredEdit.kind === "text" && file.kind === "text") setEdit(key, {
|
|
2348
|
+
...restoredEdit,
|
|
2349
|
+
saving: false
|
|
2350
|
+
});
|
|
2351
|
+
else if (restoredEdit.kind === "structured" && file.kind === "structured" && restoredEdit.sourceSha256 === file.sourceSha256) {
|
|
2352
|
+
const replacements = new Map(restoredEdit.fragments.map((fragment) => [fragment.id, fragment.text]));
|
|
2353
|
+
if (replacements.size === file.fragments.length && file.fragments.every((fragment) => replacements.has(fragment.id) && (!fragment.readOnly || replacements.get(fragment.id) === fragment.text))) setEdit(key, {
|
|
2354
|
+
kind: "structured",
|
|
2355
|
+
file: {
|
|
2356
|
+
...file,
|
|
2357
|
+
fragments: file.fragments.map((fragment) => ({
|
|
2358
|
+
...fragment,
|
|
2359
|
+
text: replacements.get(fragment.id)
|
|
2360
|
+
}))
|
|
2361
|
+
},
|
|
2362
|
+
saving: false
|
|
2363
|
+
});
|
|
2364
|
+
else restoredEdit = void 0;
|
|
2365
|
+
} else restoredEdit = void 0;
|
|
2366
|
+
if (restoredEdit === void 0) {
|
|
2367
|
+
restoreReady = false;
|
|
2368
|
+
setStorageState({
|
|
2369
|
+
key,
|
|
2370
|
+
message: "The stored edit no longer matches this document. Its stored copy was kept; new notes stay in memory. Keep this tab open."
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
if (restoreReady) {
|
|
2375
|
+
hydratedKeys.add(key);
|
|
2376
|
+
setStorageState({ key });
|
|
2377
|
+
}
|
|
2378
|
+
openNativeCanvas(sessionId, path, file.kind);
|
|
2379
|
+
if (file.kind === "text") {
|
|
2380
|
+
documentTexts.set(key, {
|
|
2381
|
+
text: file.text,
|
|
2382
|
+
sourceSha256: file.sourceSha256
|
|
2383
|
+
});
|
|
2384
|
+
setLoaded({
|
|
2385
|
+
key,
|
|
2386
|
+
file,
|
|
2387
|
+
body: {
|
|
2388
|
+
kind: "text",
|
|
2389
|
+
text: file.text
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
} else if (file.kind === "structured") {
|
|
2393
|
+
documentTexts.set(key, {
|
|
2394
|
+
text: file.fragments.map((fragment) => fragment.text).join("\n"),
|
|
2395
|
+
sourceSha256: file.sourceSha256
|
|
2396
|
+
});
|
|
2397
|
+
setLoaded({
|
|
2398
|
+
key,
|
|
2399
|
+
file,
|
|
2400
|
+
body: {
|
|
2401
|
+
kind: "structured",
|
|
2402
|
+
format: file.format,
|
|
2403
|
+
fragments: file.fragments
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2406
|
+
} else if (file.kind === "pdf" || file.kind === "video") {
|
|
2407
|
+
setLoaded({
|
|
2408
|
+
key,
|
|
2409
|
+
file,
|
|
2410
|
+
body: { kind: file.kind }
|
|
2411
|
+
});
|
|
2412
|
+
setMediaPage(1);
|
|
2413
|
+
setSeekSeconds(void 0);
|
|
2414
|
+
setVideoFrame(void 0);
|
|
2415
|
+
mediaTime.current = 0;
|
|
2416
|
+
} else {
|
|
2417
|
+
const uri = `data:${file.mimeType};base64,${file.data}`;
|
|
2418
|
+
Image.getSize(uri, (width, height) => {
|
|
2419
|
+
if (!controller.signal.aborted) setLoaded({
|
|
2420
|
+
key,
|
|
2421
|
+
file,
|
|
2422
|
+
body: {
|
|
2423
|
+
kind: "image",
|
|
2424
|
+
uri,
|
|
2425
|
+
aspectRatio: height > 0 ? width / height : 1,
|
|
2426
|
+
naturalWidth: width,
|
|
2427
|
+
naturalHeight: height
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2430
|
+
}, fail);
|
|
2431
|
+
}
|
|
2432
|
+
})().catch(fail);
|
|
2433
|
+
return () => controller.abort();
|
|
2434
|
+
}, [
|
|
2435
|
+
sessionId,
|
|
2436
|
+
path,
|
|
2437
|
+
key
|
|
2438
|
+
]);
|
|
2439
|
+
useEffect(() => {
|
|
2440
|
+
const timer = setTimeout(() => {
|
|
2441
|
+
persistNativeDraft(key, path, props.nativeAuthor).then(() => {
|
|
2442
|
+
if (hydratedKeys.has(key)) setStorageState({ key });
|
|
2443
|
+
}).catch((error) => setStorageState({
|
|
2444
|
+
key,
|
|
2445
|
+
message: `Couldn't store this draft: ${error instanceof Error ? error.message : String(error)} Keep this tab open.`
|
|
2446
|
+
}));
|
|
2447
|
+
}, 300);
|
|
2448
|
+
return () => clearTimeout(timer);
|
|
2449
|
+
}, [
|
|
2450
|
+
key,
|
|
2451
|
+
path,
|
|
2452
|
+
notes,
|
|
2453
|
+
draft,
|
|
2454
|
+
edit
|
|
2455
|
+
]);
|
|
2456
|
+
useEffect(() => {
|
|
2457
|
+
let mounted = true;
|
|
2458
|
+
const host = props.nativeAuthor;
|
|
2459
|
+
const flush = () => {
|
|
2460
|
+
persistNativeDraft(key, path, host).catch((error) => {
|
|
2461
|
+
console.warn(`Author draft storage failed for '${path}': ${String(error)}`);
|
|
2462
|
+
if (mounted) setStorageState({
|
|
2463
|
+
key,
|
|
2464
|
+
message: `Couldn't store this draft: ${String(error)} Keep this tab open.`
|
|
2465
|
+
});
|
|
2466
|
+
});
|
|
2467
|
+
};
|
|
2468
|
+
const subscription = AppState.addEventListener("change", (state) => {
|
|
2469
|
+
if (state !== "active") flush();
|
|
2470
|
+
});
|
|
2471
|
+
return () => {
|
|
2472
|
+
mounted = false;
|
|
2473
|
+
subscription.remove();
|
|
2474
|
+
flush();
|
|
2475
|
+
};
|
|
2476
|
+
}, [key, path]);
|
|
2477
|
+
if (sessionId === null) return /* @__PURE__ */ jsx(EmptyState, { title: "This session is unavailable." });
|
|
2478
|
+
const current = loaded?.key === key ? loaded : void 0;
|
|
2479
|
+
if (current === void 0) return /* @__PURE__ */ jsx(Spinner, { label: "Opening" });
|
|
2480
|
+
if ("error" in current) return /* @__PURE__ */ jsx(EmptyState, {
|
|
2481
|
+
title: "Couldn't open this file",
|
|
2482
|
+
description: current.error
|
|
2483
|
+
});
|
|
2484
|
+
const visibleNotes = nativeAuthorVisibleNotes(notes, current.file.kind, mediaPage, videoFrame?.timeSeconds);
|
|
2485
|
+
const send = async () => {
|
|
2486
|
+
if (sending.current) return;
|
|
2487
|
+
sending.current = true;
|
|
2488
|
+
const sent = visibleNotes.slice(0, 16);
|
|
2489
|
+
const capturedAt = Date.now();
|
|
2490
|
+
const captureId = `native-author-${String(capturedAt)}-${sent[0]?.id ?? "notes"}`;
|
|
2491
|
+
let recorded = false;
|
|
2492
|
+
try {
|
|
2493
|
+
if (!sent.length) throw new Error("Open a marked page or capture its video frame before sending notes.");
|
|
2494
|
+
if (!props.submitCapture) throw new Error("This host cannot confirm feedback delivery. Your notes have not been sent.");
|
|
2495
|
+
const provider = captureRef.current;
|
|
2496
|
+
if (provider?.key !== key) throw new Error("The document is not ready to capture. Try again when it has finished opening.");
|
|
2497
|
+
const capture = await provider.capture();
|
|
2498
|
+
setStatus({ state: "sending" });
|
|
2499
|
+
if (capture.bytes.byteLength > 2097152 || Math.max(capture.width, capture.height) > 1600) throw new Error("This capture exceeds the Author evidence budget.");
|
|
2500
|
+
const { file, body } = current;
|
|
2501
|
+
const { packet, regions } = nativeAuthorCapture(captureId, capturedAt, {
|
|
2502
|
+
path,
|
|
2503
|
+
version: 1,
|
|
2504
|
+
sourceSha256: file.sourceSha256,
|
|
2505
|
+
kind: file.kind === "structured" ? file.format : file.kind === "text" && /\.html?$/iu.test(path) ? "html" : file.kind,
|
|
2506
|
+
...file.kind === "structured" ? { fragments: file.fragments } : {},
|
|
2507
|
+
...file.kind === "text" ? { sourceText: file.text } : {},
|
|
2508
|
+
...body.kind === "image" ? {
|
|
2509
|
+
naturalWidth: body.naturalWidth,
|
|
2510
|
+
naturalHeight: body.naturalHeight
|
|
2511
|
+
} : {}
|
|
2512
|
+
}, sent, {
|
|
2513
|
+
width: capture.width,
|
|
2514
|
+
height: capture.height,
|
|
2515
|
+
...file.kind === "image" ? { zoom: imageZoom.current } : {}
|
|
2516
|
+
}, alias);
|
|
2517
|
+
recordNativeAuthorRequest(sessionId, packet, regions);
|
|
2518
|
+
recorded = true;
|
|
2519
|
+
await props.submitCapture({
|
|
2520
|
+
data: encodeBase64(capture.bytes),
|
|
2521
|
+
mimeType: capture.mimeType,
|
|
2522
|
+
context: authorCaptureContext(packet)
|
|
2523
|
+
});
|
|
2524
|
+
setNotes(key, (notes) => notes.filter((note) => !sent.includes(note)));
|
|
2525
|
+
setSelectedId(void 0);
|
|
2526
|
+
setStatus({ state: "sent" });
|
|
2527
|
+
await persistNativeDraft(key, path, props.nativeAuthor).catch((error) => setStorageState({
|
|
2528
|
+
key,
|
|
2529
|
+
message: `Sent, but couldn't update the stored draft: ${String(error)} It may reappear after restart.`
|
|
2530
|
+
}));
|
|
2531
|
+
} catch (error) {
|
|
2532
|
+
if (recorded) failNativeAuthorRequest(sessionId, captureId, error);
|
|
2533
|
+
setStatus({
|
|
2534
|
+
state: "failed",
|
|
2535
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2536
|
+
});
|
|
2537
|
+
} finally {
|
|
2538
|
+
sending.current = false;
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
const save = async () => {
|
|
2542
|
+
const pending = editsStore.store.state[key];
|
|
2543
|
+
if (pending === void 0 || pending.saving) return;
|
|
2544
|
+
setEdit(key, {
|
|
2545
|
+
...pending,
|
|
2546
|
+
saving: true
|
|
2547
|
+
});
|
|
2548
|
+
try {
|
|
2549
|
+
const signal = new AbortController().signal;
|
|
2550
|
+
if (pending.kind === "structured") {
|
|
2551
|
+
const digest = await saveAuthorNativeStructured(sessionId, path, pending.file, signal);
|
|
2552
|
+
const file = {
|
|
2553
|
+
...pending.file,
|
|
2554
|
+
sourceSha256: digest,
|
|
2555
|
+
originalFragments: pending.file.fragments.map((fragment) => ({ ...fragment }))
|
|
2556
|
+
};
|
|
2557
|
+
documentTexts.set(key, {
|
|
2558
|
+
text: file.fragments.map((fragment) => fragment.text).join("\n"),
|
|
2559
|
+
sourceSha256: file.sourceSha256
|
|
2560
|
+
});
|
|
2561
|
+
setLoaded({
|
|
2562
|
+
key,
|
|
2563
|
+
file,
|
|
2564
|
+
body: {
|
|
2565
|
+
kind: "structured",
|
|
2566
|
+
format: file.format,
|
|
2567
|
+
fragments: file.fragments
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
} else if (pending.kind === "image") {
|
|
2571
|
+
const digest = await saveAuthorNativeImage(sessionId, path, pending.image, pending.sourceSha256, signal);
|
|
2572
|
+
const { width, height, mimeType, bytes } = pending.image;
|
|
2573
|
+
setLoaded({
|
|
2574
|
+
key,
|
|
2575
|
+
file: {
|
|
2576
|
+
kind: "image",
|
|
2577
|
+
data: encodeBase64(bytes),
|
|
2578
|
+
mimeType,
|
|
2579
|
+
sourceSha256: digest
|
|
2580
|
+
},
|
|
2581
|
+
body: {
|
|
2582
|
+
kind: "image",
|
|
2583
|
+
uri: pending.uri,
|
|
2584
|
+
aspectRatio: width / height,
|
|
2585
|
+
naturalWidth: width,
|
|
2586
|
+
naturalHeight: height
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
} else {
|
|
2590
|
+
const digest = await saveAuthorNativeText(sessionId, path, pending.text, pending.sourceSha256, signal);
|
|
2591
|
+
documentTexts.set(key, {
|
|
2592
|
+
text: pending.text,
|
|
2593
|
+
sourceSha256: digest
|
|
2594
|
+
});
|
|
2595
|
+
setLoaded({
|
|
2596
|
+
key,
|
|
2597
|
+
file: {
|
|
2598
|
+
kind: "text",
|
|
2599
|
+
text: pending.text,
|
|
2600
|
+
sourceSha256: digest
|
|
2601
|
+
},
|
|
2602
|
+
body: {
|
|
2603
|
+
kind: "text",
|
|
2604
|
+
text: pending.text
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
setEdit(key, void 0);
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
setEdit(key, {
|
|
2611
|
+
...pending,
|
|
2612
|
+
saving: false,
|
|
2613
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
const native = props.nativeAuthor;
|
|
2618
|
+
const selectedNote = notes.find((note) => note.id === selectedId);
|
|
2619
|
+
const cropBounds = selectedNote?.points === void 0 ? null : authorStrokeBounds(selectedNote.points);
|
|
2620
|
+
const canCrop = native !== void 0 && current.file.kind === "image" && current.file.sourceSha256 !== void 0 && selectedNote?.sourceSha256 === current.file.sourceSha256 && /\.(?:png|jpe?g)$/iu.test(path) && !path.startsWith("artifact:") && current.body.kind === "image" && current.body.naturalWidth !== void 0 && current.body.naturalHeight !== void 0 && cropBounds !== null;
|
|
2621
|
+
const crop = async () => {
|
|
2622
|
+
if (!canCrop || cropping || current.file.kind !== "image" || current.body.kind !== "image" || !native || !cropBounds) return;
|
|
2623
|
+
const width = current.body.naturalWidth;
|
|
2624
|
+
const height = current.body.naturalHeight;
|
|
2625
|
+
const originX = Math.floor(cropBounds.x * width);
|
|
2626
|
+
const originY = Math.floor(cropBounds.y * height);
|
|
2627
|
+
setCropping(true);
|
|
2628
|
+
try {
|
|
2629
|
+
const image = await native.cropImage({
|
|
2630
|
+
source: {
|
|
2631
|
+
sessionId,
|
|
2632
|
+
bytes: decodeBase64(current.file.data),
|
|
2633
|
+
mimeType: current.file.mimeType
|
|
2634
|
+
},
|
|
2635
|
+
rect: {
|
|
2636
|
+
originX,
|
|
2637
|
+
originY,
|
|
2638
|
+
width: Math.max(1, Math.min(width, Math.ceil((cropBounds.x + cropBounds.width) * width)) - originX),
|
|
2639
|
+
height: Math.max(1, Math.min(height, Math.ceil((cropBounds.y + cropBounds.height) * height)) - originY)
|
|
2640
|
+
},
|
|
2641
|
+
format: /\.png$/iu.test(path) ? "png" : "jpeg",
|
|
2642
|
+
signal: new AbortController().signal
|
|
2643
|
+
});
|
|
2644
|
+
setEdit(key, {
|
|
2645
|
+
kind: "image",
|
|
2646
|
+
image,
|
|
2647
|
+
uri: `data:${image.mimeType};base64,${encodeBase64(image.bytes)}`,
|
|
2648
|
+
sourceSha256: current.file.sourceSha256,
|
|
2649
|
+
saving: false
|
|
2650
|
+
});
|
|
2651
|
+
} catch (error) {
|
|
2652
|
+
setStatus({
|
|
2653
|
+
state: "failed",
|
|
2654
|
+
message: `Crop failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2655
|
+
});
|
|
2656
|
+
} finally {
|
|
2657
|
+
setCropping(false);
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
const nativeContent = current.file.kind === "pdf" ? native?.renderPdf({
|
|
2661
|
+
key,
|
|
2662
|
+
source: {
|
|
2663
|
+
sessionId,
|
|
2664
|
+
bytes: current.file.bytes,
|
|
2665
|
+
mimeType: current.file.mimeType
|
|
2666
|
+
},
|
|
2667
|
+
page: mediaPage,
|
|
2668
|
+
onPageChange: ({ page }) => setMediaPage(page)
|
|
2669
|
+
}) ?? /* @__PURE__ */ jsx(EmptyState, { title: "Update the native app to view this PDF." }) : current.file.kind === "video" ? videoFrame !== void 0 ? /* @__PURE__ */ jsx(Image, {
|
|
2670
|
+
accessibilityLabel: `Video frame at ${String(videoFrame.timeSeconds)} seconds`,
|
|
2671
|
+
source: { uri: `data:${videoFrame.mimeType};base64,${encodeBase64(videoFrame.bytes)}` },
|
|
2672
|
+
style: {
|
|
2673
|
+
width: "100%",
|
|
2674
|
+
aspectRatio: videoFrame.width / videoFrame.height
|
|
2675
|
+
},
|
|
2676
|
+
resizeMode: "contain"
|
|
2677
|
+
}) : native?.renderVideo({
|
|
2678
|
+
key,
|
|
2679
|
+
source: {
|
|
2680
|
+
sessionId,
|
|
2681
|
+
bytes: current.file.bytes,
|
|
2682
|
+
mimeType: current.file.mimeType
|
|
2683
|
+
},
|
|
2684
|
+
seekSeconds,
|
|
2685
|
+
onTimeChange: (seconds) => {
|
|
2686
|
+
mediaTime.current = seconds;
|
|
2687
|
+
},
|
|
2688
|
+
onFrame: (frame) => {
|
|
2689
|
+
mediaTime.current = frame.timeSeconds;
|
|
2690
|
+
setVideoFrame(frame);
|
|
2691
|
+
}
|
|
2692
|
+
}) ?? /* @__PURE__ */ jsx(EmptyState, { title: "Update the native app to play this video." }) : current.file.kind === "text" && /\.html?$/iu.test(path) ? native?.renderHtml({
|
|
2693
|
+
key,
|
|
2694
|
+
html: current.file.text
|
|
2695
|
+
}) : void 0;
|
|
2696
|
+
const noteLocation = () => current.file.kind === "pdf" ? { page: mediaPage } : current.file.kind === "video" ? { timeSeconds: mediaTime.current } : {};
|
|
2697
|
+
return /* @__PURE__ */ jsx(AuthorDocumentView, {
|
|
2698
|
+
name: fileName(path),
|
|
2699
|
+
body: current.body,
|
|
2700
|
+
sourceSha256: current.file.sourceSha256,
|
|
2701
|
+
nativeContent,
|
|
2702
|
+
captureContent: (children) => native?.renderCapture({
|
|
2703
|
+
key,
|
|
2704
|
+
children,
|
|
2705
|
+
onReady: onCaptureReady
|
|
2706
|
+
}) ?? children,
|
|
2707
|
+
onZoomChange: (zoom) => {
|
|
2708
|
+
imageZoom.current = zoom;
|
|
2709
|
+
},
|
|
2710
|
+
onResumeVideo: videoFrame === void 0 ? void 0 : () => {
|
|
2711
|
+
setSeekSeconds(videoFrame.timeSeconds);
|
|
2712
|
+
setVideoFrame(void 0);
|
|
2713
|
+
},
|
|
2714
|
+
storageMessage: storageState?.key === key ? storageState.message ?? (edit?.kind === "image" ? "Crop previews are temporary until saved." : void 0) : void 0,
|
|
2715
|
+
page: current.file.kind === "pdf" ? mediaPage : void 0,
|
|
2716
|
+
timeSeconds: current.file.kind === "video" ? mediaTime.current : void 0,
|
|
2717
|
+
notes,
|
|
2718
|
+
requests: requests.filter((request) => request.documentPath === path),
|
|
2719
|
+
draft,
|
|
2720
|
+
status,
|
|
2721
|
+
edit: edit?.kind === "structured" ? {
|
|
2722
|
+
fragments: edit.file.fragments,
|
|
2723
|
+
saving: edit.saving,
|
|
2724
|
+
error: edit.error
|
|
2725
|
+
} : edit?.kind === "image" ? {
|
|
2726
|
+
image: {
|
|
2727
|
+
uri: edit.uri,
|
|
2728
|
+
aspectRatio: edit.image.width / edit.image.height
|
|
2729
|
+
},
|
|
2730
|
+
saving: edit.saving,
|
|
2731
|
+
error: edit.error
|
|
2732
|
+
} : edit,
|
|
2733
|
+
onCrop: canCrop ? () => void crop() : void 0,
|
|
2734
|
+
cropping,
|
|
2735
|
+
onEdit: (current.file.kind === "text" || current.file.kind === "structured") && current.file.sourceSha256 !== void 0 && !path.startsWith("artifact:") ? () => {
|
|
2736
|
+
if (current.file.kind === "text" && current.file.sourceSha256 !== void 0) setEdit(key, {
|
|
2737
|
+
kind: "text",
|
|
2738
|
+
text: current.file.text,
|
|
2739
|
+
sourceSha256: current.file.sourceSha256,
|
|
2740
|
+
saving: false
|
|
2741
|
+
});
|
|
2742
|
+
else if (current.file.kind === "structured") setEdit(key, {
|
|
2743
|
+
kind: "structured",
|
|
2744
|
+
file: current.file,
|
|
2745
|
+
saving: false
|
|
2746
|
+
});
|
|
2747
|
+
} : void 0,
|
|
2748
|
+
onEditChange: (text) => {
|
|
2749
|
+
if (edit?.kind === "text" && !edit.saving) setEdit(key, {
|
|
2750
|
+
...edit,
|
|
2751
|
+
text
|
|
2752
|
+
});
|
|
2753
|
+
},
|
|
2754
|
+
onEditFragmentChange: (id, text) => {
|
|
2755
|
+
if (edit?.kind !== "structured" || edit.saving) return;
|
|
2756
|
+
const fragments = edit.file.fragments.map((fragment) => fragment.id === id && !fragment.readOnly ? {
|
|
2757
|
+
...fragment,
|
|
2758
|
+
text
|
|
2759
|
+
} : fragment);
|
|
2760
|
+
setEdit(key, {
|
|
2761
|
+
...edit,
|
|
2762
|
+
file: {
|
|
2763
|
+
...edit.file,
|
|
2764
|
+
fragments
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
},
|
|
2768
|
+
onEditCancel: () => {
|
|
2769
|
+
if (!edit?.saving) setEdit(key, void 0);
|
|
2770
|
+
},
|
|
2771
|
+
onEditSave: () => void save(),
|
|
2772
|
+
ownedElsewhere,
|
|
2773
|
+
...selectedId === void 0 ? {} : { selectedId },
|
|
2774
|
+
onPlace: current.file.kind === "video" && videoFrame === void 0 ? void 0 : (point) => {
|
|
2775
|
+
setSelectedId(void 0);
|
|
2776
|
+
setDraft({
|
|
2777
|
+
...point,
|
|
2778
|
+
...noteLocation(),
|
|
2779
|
+
sourceSha256: current.file.sourceSha256,
|
|
2780
|
+
text: draft?.text ?? ""
|
|
2781
|
+
});
|
|
2782
|
+
},
|
|
2783
|
+
onSelectText: current.file.kind === "text" && current.file.sourceSha256 !== void 0 && /^[a-f0-9]{64}$/u.test(current.file.sourceSha256) && !/\.html?$/iu.test(path) ? (textRange) => {
|
|
2784
|
+
if (current.file.kind !== "text" || nativeAuthorTextRange(current.file.text, textRange) === void 0) return;
|
|
2785
|
+
setSelectedId(void 0);
|
|
2786
|
+
setDraft({
|
|
2787
|
+
x: .5,
|
|
2788
|
+
y: .5,
|
|
2789
|
+
textRange: { ...textRange },
|
|
2790
|
+
sourceSha256: current.file.sourceSha256,
|
|
2791
|
+
text: ""
|
|
2792
|
+
});
|
|
2793
|
+
} : void 0,
|
|
2794
|
+
onDraw: current.file.kind === "video" && videoFrame === void 0 ? void 0 : (points) => {
|
|
2795
|
+
const point = points.at(-1);
|
|
2796
|
+
if (point === void 0) return;
|
|
2797
|
+
setSelectedId(void 0);
|
|
2798
|
+
setDraft({
|
|
2799
|
+
...point,
|
|
2800
|
+
...noteLocation(),
|
|
2801
|
+
sourceSha256: current.file.sourceSha256,
|
|
2802
|
+
text: "",
|
|
2803
|
+
points
|
|
2804
|
+
});
|
|
2805
|
+
},
|
|
2806
|
+
onSelect: (id) => {
|
|
2807
|
+
const note = notes.find((note) => note.id === id);
|
|
2808
|
+
if (note?.page !== void 0) setMediaPage(note.page);
|
|
2809
|
+
if (note?.timeSeconds !== void 0) {
|
|
2810
|
+
setVideoFrame(void 0);
|
|
2811
|
+
setSeekSeconds(note.timeSeconds);
|
|
2812
|
+
}
|
|
2813
|
+
setSelectedId((value) => value === id ? void 0 : id);
|
|
2814
|
+
},
|
|
2815
|
+
onDelete: (id) => {
|
|
2816
|
+
setNotes(key, (notes) => notes.filter((note) => note.id !== id));
|
|
2817
|
+
if (selectedId === id) setSelectedId(void 0);
|
|
2818
|
+
},
|
|
2819
|
+
onDraftChange: (text) => {
|
|
2820
|
+
if (draft !== void 0) setDraft({
|
|
2821
|
+
...draft,
|
|
2822
|
+
text
|
|
2823
|
+
});
|
|
2824
|
+
},
|
|
2825
|
+
onDraftCancel: () => setDraft(void 0),
|
|
2826
|
+
onDraftSave: () => {
|
|
2827
|
+
if (draft === void 0 || draft.text.trim() === "") return;
|
|
2828
|
+
setNotes(key, (notes) => [...notes, {
|
|
2829
|
+
id: `note-${String(Date.now())}-${String(notes.length)}`,
|
|
2830
|
+
...draft
|
|
2831
|
+
}]);
|
|
2832
|
+
setDraft(void 0);
|
|
2833
|
+
if (status.state !== "sending") setStatus({ state: "idle" });
|
|
2834
|
+
},
|
|
2835
|
+
sendCount: Math.min(16, visibleNotes.length),
|
|
2836
|
+
onSend: () => void send()
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
function tabId(sessionId, path) {
|
|
2840
|
+
let hash = 2166136261;
|
|
2841
|
+
for (const char of `${sessionId}\n${path}`) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
|
|
2842
|
+
return `${AUTHOR_FILE_TAB_PREFIX}${(hash >>> 0).toString(36)}`;
|
|
2843
|
+
}
|
|
2844
|
+
/** The `author.open-source` fill on the phone: the same contract web reads, opening the native canvas. */
|
|
2845
|
+
const authorNativeOpenSourceAction = {
|
|
2846
|
+
version: 1,
|
|
2847
|
+
createTab({ sessionId, path, preview }) {
|
|
2848
|
+
const valid = authorNotePath(path);
|
|
2849
|
+
if (sessionId.trim() === "" || valid === void 0) throw new Error("Author source opening requires a session and a repository-relative path.");
|
|
2850
|
+
if (!authorNativeCanOpen(valid)) throw new Error("This kind of file cannot be marked up on this device yet.");
|
|
2851
|
+
return {
|
|
2852
|
+
id: tabId(sessionId, valid),
|
|
2853
|
+
label: fileName(valid),
|
|
2854
|
+
retainComposer: true,
|
|
2855
|
+
panel: (props) => {
|
|
2856
|
+
if (/\.stories\.(?:jsx?|tsx?)$/iu.test(valid)) {
|
|
2857
|
+
const source = {
|
|
2858
|
+
path: valid,
|
|
2859
|
+
kind: "story-preview",
|
|
2860
|
+
hasUnsavedChanges: editsStore.store.state[noteKey(sessionId, valid)] !== void 0,
|
|
2861
|
+
preview: {
|
|
2862
|
+
appPath: preview?.appPath ?? "",
|
|
2863
|
+
storyExport: preview?.storyExport ?? "Playground",
|
|
2864
|
+
snapshot: true
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
const Preview = props.slotData(authorPreviewActionSlot).find((fill) => fill.data.supportsSource?.(source))?.data.embeddedPanel;
|
|
2868
|
+
if (Preview !== void 0) return /* @__PURE__ */ jsx(Preview, {
|
|
2869
|
+
...props,
|
|
2870
|
+
sessionId,
|
|
2871
|
+
source
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
return /* @__PURE__ */ jsx(AuthorDocumentPanel, {
|
|
2875
|
+
...props,
|
|
2876
|
+
sessionId,
|
|
2877
|
+
path: valid
|
|
2878
|
+
});
|
|
2879
|
+
},
|
|
2880
|
+
onClose(target) {
|
|
2881
|
+
closeNativeCanvas(target, valid);
|
|
2882
|
+
},
|
|
2883
|
+
composerContext(target) {
|
|
2884
|
+
const key = noteKey(target, valid);
|
|
2885
|
+
const notes = notesStore.store.state[key] ?? EMPTY;
|
|
2886
|
+
if (notes.length === 0) return void 0;
|
|
2887
|
+
const alias = aliasStore.store.state[key];
|
|
2888
|
+
return {
|
|
2889
|
+
kind: "author-document",
|
|
2890
|
+
source: "author",
|
|
2891
|
+
id: valid,
|
|
2892
|
+
label: alias === void 0 ? fileName(valid) : `${alias} · ${fileName(valid)}`,
|
|
2893
|
+
content: authorNotesContent(valid, notes)
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2899
|
+
const nativeFileTab = (sessionId, path) => {
|
|
2900
|
+
if (sessionId === null) return void 0;
|
|
2901
|
+
const normalized = authorNotePath(path.replace(/:\d+(?::\d+)?$/, ""));
|
|
2902
|
+
return normalized !== void 0 && authorNativeCanOpen(normalized) ? authorNativeOpenSourceAction.createTab({
|
|
2903
|
+
sessionId,
|
|
2904
|
+
path: normalized
|
|
2905
|
+
}) : void 0;
|
|
2906
|
+
};
|
|
2907
|
+
//#endregion
|
|
2908
|
+
//#region src/extensions/workspaces/sessions/(frontend)/file-links/author-document.native.ts
|
|
2909
|
+
var author_document_native_default = defineFileLinks({
|
|
2910
|
+
requiredMinorMode: "author",
|
|
2911
|
+
subscribe: subscribeAliases,
|
|
2912
|
+
fingerprint(sessionId) {
|
|
2913
|
+
return Object.keys(aliasStore.store.state).filter((key) => key.startsWith(`${sessionId ?? ""}\n`)).sort().join("\n");
|
|
2914
|
+
},
|
|
2915
|
+
resolve(sessionId, path) {
|
|
2916
|
+
const normalized = authorNotePath(path.replace(/:\d+(?::\d+)?$/, ""));
|
|
2917
|
+
return sessionId !== null && normalized !== void 0 && aliasStore.store.state[noteKey(sessionId, normalized)] !== void 0 ? nativeFileTab(sessionId, normalized) : void 0;
|
|
2918
|
+
},
|
|
2919
|
+
openPath: nativeFileTab
|
|
2920
|
+
});
|
|
2921
|
+
//#endregion
|
|
2922
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/Author.author.open-source.native.ts
|
|
2923
|
+
var Author_author_open_source_native_default = defineFill({
|
|
2924
|
+
slot: "author.open-source",
|
|
2925
|
+
id: "author",
|
|
2926
|
+
data: authorNativeOpenSourceAction
|
|
2927
|
+
});
|
|
2928
|
+
//#endregion
|
|
2929
|
+
//#region src/extensions/workspaces/sessions/(frontend)/lifecycle/start.native.ts
|
|
2930
|
+
var start_native_default = defineWebLifecycle((runtime) => {
|
|
2931
|
+
const stopRequests = startNativeAuthorRequests(runtime);
|
|
2932
|
+
const stopBridge = startAuthorBrowserBridge(runtime);
|
|
2933
|
+
return () => {
|
|
2934
|
+
try {
|
|
2935
|
+
stopBridge();
|
|
2936
|
+
} finally {
|
|
2937
|
+
stopRequests();
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
});
|
|
2941
|
+
//#endregion
|
|
2942
|
+
//#region src/extensions/workspaces/sessions/(frontend)/slot/open-source.native.ts
|
|
2943
|
+
var open_source_native_default = defineSlotFile({
|
|
2944
|
+
slot: "author.open-source",
|
|
2945
|
+
parse: parseAuthorOpenSourceAction
|
|
2946
|
+
});
|
|
2947
|
+
//#endregion
|
|
2948
|
+
//#region src/extensions/workspaces/sessions/(frontend)/slot/preview-action.native.ts
|
|
2949
|
+
var preview_action_native_default = defineSlotFile({
|
|
2950
|
+
slot: "author.preview-action",
|
|
2951
|
+
parse: parseAuthorPreviewAction
|
|
2952
|
+
});
|
|
2953
|
+
//#endregion
|
|
2954
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/authorToolFile.native.ts
|
|
2955
|
+
/** Read only server result details, never the unvalidated requested path. */
|
|
2956
|
+
function nativeAuthorToolFile(result) {
|
|
2957
|
+
const details = result?.details;
|
|
2958
|
+
if (typeof details !== "object" || details === null || !("path" in details) || typeof details.path !== "string") return void 0;
|
|
2959
|
+
const path = authorNotePath(details.path);
|
|
2960
|
+
if (path === void 0) return void 0;
|
|
2961
|
+
const alias = "alias" in details && typeof details.alias === "string" && details.alias.trim() !== "" ? details.alias : void 0;
|
|
2962
|
+
return {
|
|
2963
|
+
path,
|
|
2964
|
+
...alias === void 0 ? {} : { alias }
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
/** Unsupported results remain a readable tool card instead of breaking live frame delivery. */
|
|
2968
|
+
function nativeAuthorToolTab(sessionId, result) {
|
|
2969
|
+
const file = nativeAuthorToolFile(result);
|
|
2970
|
+
if (sessionId === null || sessionId.trim() === "" || file === void 0) return void 0;
|
|
2971
|
+
try {
|
|
2972
|
+
const tab = authorNativeOpenSourceAction.createTab({
|
|
2973
|
+
sessionId,
|
|
2974
|
+
path: file.path
|
|
2975
|
+
});
|
|
2976
|
+
return file.alias === void 0 ? tab : {
|
|
2977
|
+
...tab,
|
|
2978
|
+
label: `${file.alias} · ${tab.label}`
|
|
2979
|
+
};
|
|
2980
|
+
} catch {
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
//#endregion
|
|
2985
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/_components/AuthorToolMessage.native.tsx
|
|
2986
|
+
const COLLAPSED_LINES = 8;
|
|
2987
|
+
/** Native Author tool outcomes use the same shared timeline shell as the host's tools. */
|
|
2988
|
+
function AuthorToolMessage(props) {
|
|
2989
|
+
const { toolName, args, result, output, running, isError, sessionId } = props;
|
|
2990
|
+
const opening = toolName === "open_authoring_file";
|
|
2991
|
+
const file = opening ? nativeAuthorToolFile(result) : void 0;
|
|
2992
|
+
const tab = opening && !running && !isError ? nativeAuthorToolTab(sessionId, result) : void 0;
|
|
2993
|
+
const unavailable = opening && !running && !isError && result !== null && tab === void 0;
|
|
2994
|
+
const failed = isError || unavailable;
|
|
2995
|
+
const summary = opening ? file?.alias === void 0 ? typeof args.path === "string" ? args.path : "document" : `${file.alias} · ${file.path}` : toolName === "use_author_tools" && typeof args.name === "string" ? args.name : "";
|
|
2996
|
+
const lines = resultTextLines(result, output);
|
|
2997
|
+
const collapsed = collapseLines(lines, COLLAPSED_LINES, false);
|
|
2998
|
+
return /* @__PURE__ */ jsx(MessageItem, {
|
|
2999
|
+
tone: toolTone({
|
|
3000
|
+
running,
|
|
3001
|
+
isError: failed
|
|
3002
|
+
}),
|
|
3003
|
+
expandable: collapsed.hidden > 0,
|
|
3004
|
+
children: ({ expanded }) => {
|
|
3005
|
+
const view = expanded ? collapseLines(lines, COLLAPSED_LINES, true) : collapsed;
|
|
3006
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MessageItemHeader, {
|
|
3007
|
+
title: toolName,
|
|
3008
|
+
children: summary
|
|
3009
|
+
}), view.shown.length > 0 || running || failed || opening ? /* @__PURE__ */ jsxs(MessageItemBody, {
|
|
3010
|
+
testID: `tool-result-${toolName}`,
|
|
3011
|
+
children: [
|
|
3012
|
+
view.shown.map((line, index) => /* @__PURE__ */ jsx(Text, {
|
|
3013
|
+
selectable: true,
|
|
3014
|
+
style: [styles.line, isError && styles.error],
|
|
3015
|
+
children: line
|
|
3016
|
+
}, index)),
|
|
3017
|
+
view.hidden > 0 ? /* @__PURE__ */ jsxs(MessageItemStatus, {
|
|
3018
|
+
expands: true,
|
|
3019
|
+
children: [view.hidden, " more line(s)"]
|
|
3020
|
+
}) : null,
|
|
3021
|
+
opening || running || isError ? /* @__PURE__ */ jsx(MessageItemStatus, {
|
|
3022
|
+
tone: failed ? "error" : running ? "running" : void 0,
|
|
3023
|
+
children: isError ? "failed" : running ? opening ? "validating" : "running" : unavailable ? "unavailable" : tab === void 0 ? "waiting" : "ready"
|
|
3024
|
+
}) : null,
|
|
3025
|
+
tab !== void 0 ? /* @__PURE__ */ jsx(Button, {
|
|
3026
|
+
variant: "outline",
|
|
3027
|
+
onPress: () => {
|
|
3028
|
+
props.openTransientTab(tab);
|
|
3029
|
+
props.openTab(tab.id);
|
|
3030
|
+
},
|
|
3031
|
+
children: "Open in Author"
|
|
3032
|
+
}) : null
|
|
3033
|
+
]
|
|
3034
|
+
}) : null] });
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
const styles = StyleSheet.create((theme) => ({
|
|
3039
|
+
line: {
|
|
3040
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
3041
|
+
fontSize: theme.fontSize.base,
|
|
3042
|
+
lineHeight: theme.lineHeight.base,
|
|
3043
|
+
color: theme.colors.dim,
|
|
3044
|
+
uni__dependencies: [0]
|
|
3045
|
+
},
|
|
3046
|
+
error: {
|
|
3047
|
+
color: theme.colors.red,
|
|
3048
|
+
uni__dependencies: [0]
|
|
3049
|
+
}
|
|
3050
|
+
}));
|
|
3051
|
+
//#endregion
|
|
3052
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/describe-author-tools.native.tsx
|
|
3053
|
+
var describe_author_tools_native_default = defineToolRenderer({
|
|
3054
|
+
tools: ["describe_author_tools"],
|
|
3055
|
+
message: AuthorToolMessage
|
|
3056
|
+
});
|
|
3057
|
+
//#endregion
|
|
3058
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/open-authoring-file.native.tsx
|
|
3059
|
+
var open_authoring_file_native_default = defineToolRenderer({
|
|
3060
|
+
tools: ["open_authoring_file"],
|
|
3061
|
+
message: AuthorToolMessage,
|
|
3062
|
+
completionTab: ({ sessionId, result }) => nativeAuthorToolTab(sessionId, result)
|
|
3063
|
+
});
|
|
3064
|
+
//#endregion
|
|
3065
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/use-author-tools.native.tsx
|
|
3066
|
+
var use_author_tools_native_default = defineToolRenderer({
|
|
3067
|
+
tools: ["use_author_tools"],
|
|
3068
|
+
message: AuthorToolMessage
|
|
3069
|
+
});
|
|
3070
|
+
//#endregion
|
|
3071
|
+
//#region generated/native.ts
|
|
3072
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
3073
|
+
...identity,
|
|
3074
|
+
...value
|
|
3075
|
+
};
|
|
3076
|
+
const nativePlugin = defineNativePlugin({
|
|
3077
|
+
id: "author",
|
|
3078
|
+
session: {
|
|
3079
|
+
get channels() {
|
|
3080
|
+
return [via({ channel: "author_webmcp" }, author_webmcp_native_default)];
|
|
3081
|
+
},
|
|
3082
|
+
get fileLinks() {
|
|
3083
|
+
return author_document_native_default;
|
|
3084
|
+
},
|
|
3085
|
+
get fills() {
|
|
3086
|
+
return [via({
|
|
3087
|
+
slot: "author.open-source",
|
|
3088
|
+
id: "author"
|
|
3089
|
+
}, Author_author_open_source_native_default)];
|
|
3090
|
+
},
|
|
3091
|
+
get slots() {
|
|
3092
|
+
return [via({ slot: "author.open-source" }, open_source_native_default), via({ slot: "author.preview-action" }, preview_action_native_default)];
|
|
3093
|
+
},
|
|
3094
|
+
get start() {
|
|
3095
|
+
return start_native_default;
|
|
3096
|
+
},
|
|
3097
|
+
get toolRenderers() {
|
|
3098
|
+
return [
|
|
3099
|
+
via({ tools: ["describe_author_tools"] }, describe_author_tools_native_default),
|
|
3100
|
+
via({ tools: ["open_authoring_file"] }, open_authoring_file_native_default),
|
|
3101
|
+
via({ tools: ["use_author_tools"] }, use_author_tools_native_default)
|
|
3102
|
+
];
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
});
|
|
3106
|
+
//#endregion
|
|
3107
|
+
export { nativePlugin };
|
|
3108
|
+
|
|
3109
|
+
//# sourceMappingURL=native.mjs.map
|