@agimon-ai/po-author 0.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/dist/apiContracts.cjs +348 -0
  4. package/dist/apiContracts.cjs.map +1 -0
  5. package/dist/apiContracts.d.mts +53 -0
  6. package/dist/apiContracts.d.mts.map +1 -0
  7. package/dist/apiContracts.mjs +343 -0
  8. package/dist/apiContracts.mjs.map +1 -0
  9. package/dist/authorFacade--Lc599Ft.mjs +84 -0
  10. package/dist/authorFacade--Lc599Ft.mjs.map +1 -0
  11. package/dist/authorFacade-89Eb2-sW.d.mts +132 -0
  12. package/dist/authorFacade-89Eb2-sW.d.mts.map +1 -0
  13. package/dist/authorFacade-BYvlm8Ff.cjs +119 -0
  14. package/dist/authorFacade-BYvlm8Ff.cjs.map +1 -0
  15. package/dist/authorFacade.cjs +27 -0
  16. package/dist/authorFacade.d.mts +3 -0
  17. package/dist/authorFacade.mjs +3 -0
  18. package/dist/extensions/server.cjs +845 -0
  19. package/dist/extensions/server.cjs.map +1 -0
  20. package/dist/extensions/server.d.mts +5 -0
  21. package/dist/extensions/server.d.mts.map +1 -0
  22. package/dist/extensions/server.mjs +838 -0
  23. package/dist/extensions/server.mjs.map +1 -0
  24. package/dist/extensions/web.mjs +4758 -0
  25. package/dist/extensions/web.mjs.map +1 -0
  26. package/dist/index.cjs +101 -0
  27. package/dist/index.cjs.map +1 -0
  28. package/dist/index.d.mts +435 -0
  29. package/dist/index.d.mts.map +1 -0
  30. package/dist/index.mjs +70 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/dist/structuredDocuments-B914GLxx.mjs +1064 -0
  33. package/dist/structuredDocuments-B914GLxx.mjs.map +1 -0
  34. package/dist/structuredDocuments-CNac1JmJ.cjs +1316 -0
  35. package/dist/structuredDocuments-CNac1JmJ.cjs.map +1 -0
  36. package/llms.txt +11 -0
  37. package/package.json +140 -0
  38. package/src/prompts/po-use-author/SKILL.md +20 -0
  39. package/src/types/author.ts +61 -0
  40. package/src/types/authorPreview.ts +98 -0
  41. package/src/types/structuredDocuments.ts +63 -0
  42. package/src/types/webAuthor.ts +94 -0
@@ -0,0 +1,4758 @@
1
+ import { apiResponse, createApiClient, defineApiRoutes, defineChannelFile, defineDockFace, defineFileLinks, defineFill, defineGlobalStore, defineLeaderBinding, defineMinorModeFile, defineSessionStore, defineSlotFile, defineToolRenderer, defineWebLifecycle, defineWebPlugin, sessionApiAddress, toolResultTextLines } from "@agimon-ai/po-core/web";
2
+ import { Store } from "@tanstack/store";
3
+ import { useStore } from "@tanstack/react-store";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { Button, CodeEditor, Markdown, MediaPreview, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, MessageLines, collapseLines, toolTone } from "@agimon-ai/po-web-components";
7
+ import { sealedTransport } from "@agimon-ai/po-web-security/browser";
8
+ //#region src/types/webAuthor.ts
9
+ const authorChannelType = "author_webmcp";
10
+ //#endregion
11
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorRuntime.ts
12
+ const AUTHOR_RUNTIME_BINDING_IDS = {
13
+ text: "author.viewport.text",
14
+ media: "author.viewport.media"
15
+ };
16
+ const runtimeIds = new Store(0);
17
+ function runtimePrefix() {
18
+ runtimeIds.setState((id) => id + 1);
19
+ return `a${runtimeIds.state.toString(36)}_`;
20
+ }
21
+ function assertProfile(profile) {
22
+ if (profile.id.trim() === "") throw new Error("Author profile id must not be empty");
23
+ if (profile.tools.length > 15) throw new Error("Author profiles may expose at most 15 tools");
24
+ const names = /* @__PURE__ */ new Set();
25
+ for (const tool of profile.tools) {
26
+ if (!/^[a-z][a-z0-9_]{0,29}$/.test(tool.name)) throw new Error(`Invalid Author tool name: ${tool.name}`);
27
+ if (names.has(tool.name)) throw new Error(`Duplicate Author tool name: ${tool.name}`);
28
+ names.add(tool.name);
29
+ }
30
+ }
31
+ function descriptorMatches(actual, expected) {
32
+ return actual.name === expected.name && actual.description === expected.description && JSON.stringify(actual.inputSchema) === JSON.stringify(expected.inputSchema);
33
+ }
34
+ function staleError() {
35
+ return /* @__PURE__ */ new Error("Author viewport catalog is stale");
36
+ }
37
+ function invocationSignal(catalogSignal, requestSignal) {
38
+ const controller = new AbortController();
39
+ const abortCatalog = () => controller.abort(catalogSignal.reason);
40
+ const abortRequest = () => controller.abort(requestSignal.reason);
41
+ if (catalogSignal.aborted) abortCatalog();
42
+ else if (requestSignal.aborted) abortRequest();
43
+ else {
44
+ catalogSignal.addEventListener("abort", abortCatalog, { once: true });
45
+ requestSignal.addEventListener("abort", abortRequest, { once: true });
46
+ }
47
+ return {
48
+ signal: controller.signal,
49
+ release: () => {
50
+ catalogSignal.removeEventListener("abort", abortCatalog);
51
+ requestSignal.removeEventListener("abort", abortRequest);
52
+ }
53
+ };
54
+ }
55
+ /** Owns page-lifetime WebMCP registrations. Profiles are replaced as one abortable catalog. */
56
+ var AuthorRuntime = class {
57
+ #runtime;
58
+ #registrationPrefix = runtimePrefix();
59
+ #context;
60
+ #active;
61
+ #pending;
62
+ #generation = 0;
63
+ #disposed = false;
64
+ constructor(runtime) {
65
+ this.#runtime = runtime;
66
+ }
67
+ async replaceProfiles(profiles) {
68
+ if (this.#disposed) throw new Error("Author runtime is disposed");
69
+ for (const profile of profiles) assertProfile(profile);
70
+ const profileMap = new Map(profiles.map((profile) => [profile.id, profile]));
71
+ const tools = profiles.flatMap((profile) => profile.tools);
72
+ if (new Set(tools.map((tool) => tool.name)).size !== tools.length) throw new Error("Author tool names must be unique across trusted profiles");
73
+ const context = this.#context ?? (await this.#runtime.acquireModelContext?.())?.modelContext;
74
+ if (context === void 0) throw new Error("Model Context is unavailable");
75
+ this.#context = context;
76
+ const registrations = new Map(tools.map((tool, index) => [tool.name, `${this.#registrationPrefix}${index.toString(36)}`]));
77
+ const generation = ++this.#generation;
78
+ const controller = new AbortController();
79
+ const candidate = {
80
+ generation,
81
+ controller,
82
+ profiles: profileMap,
83
+ registrations
84
+ };
85
+ const inactive = staleError();
86
+ this.#pending?.controller.abort(inactive);
87
+ this.#active?.controller.abort(inactive);
88
+ this.#pending = candidate;
89
+ this.#active = void 0;
90
+ const assertCurrent = () => {
91
+ if (controller.signal.aborted || this.#disposed || this.#active?.generation !== generation) throw staleError();
92
+ };
93
+ try {
94
+ await Promise.all(tools.map(async (tool) => context.registerTool({
95
+ name: registrations.get(tool.name),
96
+ description: tool.description,
97
+ inputSchema: tool.inputSchema,
98
+ execute: async (input, options) => {
99
+ assertCurrent();
100
+ const parsed = JSON.parse(input);
101
+ assertCurrent();
102
+ const invocation = invocationSignal(controller.signal, options.signal);
103
+ try {
104
+ const result = await tool.execute(parsed, invocation.signal);
105
+ assertCurrent();
106
+ return result;
107
+ } finally {
108
+ invocation.release();
109
+ }
110
+ }
111
+ }, { signal: controller.signal })));
112
+ const registered = await context.getTools();
113
+ if (!tools.every((tool) => registered.some((descriptor) => descriptorMatches(descriptor, {
114
+ ...tool,
115
+ name: registrations.get(tool.name)
116
+ })))) throw new Error("Model Context did not confirm the Author catalog");
117
+ if (this.#disposed || generation !== this.#generation || this.#pending !== candidate) throw staleError();
118
+ this.#pending = void 0;
119
+ this.#active = candidate;
120
+ } catch (error) {
121
+ controller.abort(error);
122
+ if (this.#pending === candidate) this.#pending = void 0;
123
+ throw error;
124
+ }
125
+ return () => {
126
+ if (this.#active?.generation !== generation) return;
127
+ controller.abort(staleError());
128
+ this.#active = void 0;
129
+ this.#generation += 1;
130
+ };
131
+ }
132
+ async execute(name, input, signal) {
133
+ const context = this.#context;
134
+ if (context === void 0 || this.#active === void 0) throw new Error("Author catalog is not registered");
135
+ const registration = this.#active.registrations.get(name);
136
+ if (registration === void 0) throw new Error(`Unknown Author capability: ${name}`);
137
+ return context.executeTool(registration, JSON.stringify(input), { signal });
138
+ }
139
+ dispose() {
140
+ if (this.#disposed) return;
141
+ this.#disposed = true;
142
+ this.#generation += 1;
143
+ this.#pending?.controller.abort(/* @__PURE__ */ new Error("Author runtime disposed"));
144
+ this.#active?.controller.abort(/* @__PURE__ */ new Error("Author runtime disposed"));
145
+ this.#pending = void 0;
146
+ this.#active = void 0;
147
+ }
148
+ };
149
+ //#endregion
150
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorBrowserBridge.ts
151
+ const canvasKey = (sessionId, alias) => `${sessionId}\n${alias}`;
152
+ const VISIBLE_ONLY = /* @__PURE__ */ new Set(["author_describe_grid", "author_resolve_grid_cell"]);
153
+ const documentProfiles = (profiles) => profiles.map((profile) => ({
154
+ ...profile,
155
+ tools: profile.tools.filter((tool) => !VISIBLE_ONLY.has(tool.name))
156
+ }));
157
+ var AuthorBrowserBridge = class {
158
+ #host;
159
+ #canvases = /* @__PURE__ */ new Map();
160
+ #opening = /* @__PURE__ */ new Map();
161
+ #generation = 0;
162
+ #disposed = false;
163
+ #releaseConnected;
164
+ constructor(host) {
165
+ this.#host = host;
166
+ this.#releaseConnected = host.onHubConnected(() => {
167
+ for (const canvas of this.#canvases.values()) {
168
+ canvas.ownerToken = void 0;
169
+ canvas.catalogToken = void 0;
170
+ this.#register(canvas);
171
+ }
172
+ });
173
+ }
174
+ async open(sessionId, alias, profiles) {
175
+ if (this.#disposed) throw new Error("Author browser bridge is disposed");
176
+ const key = canvasKey(sessionId, alias);
177
+ if (this.#canvases.has(key)) return;
178
+ const pending = this.#opening.get(key);
179
+ if (pending !== void 0) return await pending.promise;
180
+ const runtime = new AuthorRuntime(this.#host);
181
+ const generation = ++this.#generation;
182
+ const base = documentProfiles(profiles);
183
+ let opening;
184
+ opening = (async () => {
185
+ const release = await runtime.replaceProfiles(base);
186
+ if (this.#disposed || this.#canvases.has(key) || this.#opening.get(key)?.promise !== opening) {
187
+ release();
188
+ runtime.dispose();
189
+ return;
190
+ }
191
+ const canvas = {
192
+ sessionId,
193
+ alias,
194
+ generation,
195
+ profiles: base,
196
+ documentProfiles: base,
197
+ runtime,
198
+ release,
199
+ pending: /* @__PURE__ */ new Map(),
200
+ visible: false,
201
+ focusId: 0,
202
+ replacementId: 0
203
+ };
204
+ this.#canvases.set(key, canvas);
205
+ this.#register(canvas);
206
+ })();
207
+ this.#opening.set(key, {
208
+ promise: opening,
209
+ runtime
210
+ });
211
+ try {
212
+ await opening;
213
+ } finally {
214
+ if (this.#opening.get(key)?.promise === opening) this.#opening.delete(key);
215
+ }
216
+ }
217
+ async focus(sessionId, alias, profiles) {
218
+ await this.open(sessionId, alias, profiles);
219
+ const canvas = this.#canvases.get(canvasKey(sessionId, alias));
220
+ if (canvas === void 0) return () => void 0;
221
+ canvas.visible = true;
222
+ const focusId = ++canvas.focusId;
223
+ await this.#replace(canvas, profiles);
224
+ return () => {
225
+ if (this.#canvases.get(canvasKey(sessionId, alias)) !== canvas || canvas.focusId !== focusId) return;
226
+ canvas.visible = false;
227
+ canvas.focusId += 1;
228
+ this.#replace(canvas, canvas.documentProfiles);
229
+ };
230
+ }
231
+ drop(sessionId, alias) {
232
+ for (const [key, entry] of this.#opening) if (key.startsWith(`${sessionId}\n`) && (alias === void 0 || key === canvasKey(sessionId, alias))) {
233
+ this.#opening.delete(key);
234
+ entry.runtime.dispose();
235
+ }
236
+ for (const canvas of this.#canvases.values()) {
237
+ if (canvas.sessionId !== sessionId || alias !== void 0 && canvas.alias !== alias) continue;
238
+ this.#canvases.delete(canvasKey(sessionId, canvas.alias));
239
+ this.#release(canvas);
240
+ canvas.runtime.dispose();
241
+ }
242
+ }
243
+ apply(sessionId, message) {
244
+ if (message.alias === void 0) return;
245
+ const canvas = this.#canvases.get(canvasKey(sessionId, message.alias));
246
+ if (canvas === void 0 || message.kind === "rejected") return;
247
+ if (message.generation !== canvas.generation || message.ownerToken !== canvas.ownerToken && canvas.ownerToken !== void 0) return;
248
+ if (message.kind === "accepted") {
249
+ canvas.ownerToken = message.ownerToken;
250
+ clearTimeout(canvas.leaseTimer);
251
+ canvas.leaseTimer = setTimeout(() => this.#register(canvas), Math.max(1e3, Math.floor(message.leaseMs / 2)));
252
+ if (message.catalogToken === void 0) this.#sendCatalog(canvas);
253
+ else canvas.catalogToken = message.catalogToken;
254
+ return;
255
+ }
256
+ if (message.catalogToken !== canvas.catalogToken) return;
257
+ if (message.kind === "cancel") {
258
+ canvas.pending.get(message.requestId)?.abort(/* @__PURE__ */ new Error("Author tool request cancelled"));
259
+ return;
260
+ }
261
+ if (message.kind !== "request" || canvas.pending.has(message.requestId)) return;
262
+ const controller = new AbortController();
263
+ canvas.pending.set(message.requestId, controller);
264
+ canvas.runtime.execute(message.name, message.arguments, controller.signal).then((result) => {
265
+ if (!this.#current(canvas, message.requestId, controller)) return;
266
+ if (controller.signal.aborted) this.#sendCancelled(canvas, message.requestId);
267
+ else this.#send(canvas, {
268
+ kind: "result",
269
+ alias: canvas.alias,
270
+ generation: canvas.generation,
271
+ ownerToken: canvas.ownerToken,
272
+ catalogToken: canvas.catalogToken,
273
+ requestId: message.requestId,
274
+ result
275
+ });
276
+ }).catch((error) => {
277
+ if (!this.#current(canvas, message.requestId, controller)) return;
278
+ if (controller.signal.aborted) this.#sendCancelled(canvas, message.requestId);
279
+ else {
280
+ const messageText = error instanceof Error ? error.message : String(error);
281
+ const code = /^([A-Z][A-Z_]+):/u.exec(messageText)?.[1] ?? "AUTHOR_TOOL_ERROR";
282
+ this.#send(canvas, {
283
+ kind: "result",
284
+ alias: canvas.alias,
285
+ generation: canvas.generation,
286
+ ownerToken: canvas.ownerToken,
287
+ catalogToken: canvas.catalogToken,
288
+ requestId: message.requestId,
289
+ result: { error: {
290
+ code,
291
+ message: messageText
292
+ } }
293
+ });
294
+ }
295
+ }).finally(() => {
296
+ if (canvas.pending.get(message.requestId) === controller) canvas.pending.delete(message.requestId);
297
+ });
298
+ }
299
+ activeView(sessionId) {
300
+ const active = [...this.#canvases.values()].filter((canvas) => canvas.sessionId === sessionId && canvas.catalogToken);
301
+ return active.length > 0 ? {
302
+ activation: "active",
303
+ capabilityCount: active.reduce((count, canvas) => count + canvas.profiles.reduce((total, profile) => total + profile.tools.length, 0), 0)
304
+ } : {
305
+ activation: "inactive",
306
+ capabilityCount: 0
307
+ };
308
+ }
309
+ dispose() {
310
+ if (this.#disposed) return;
311
+ this.#disposed = true;
312
+ this.#releaseConnected();
313
+ for (const canvas of this.#canvases.values()) {
314
+ this.#release(canvas);
315
+ canvas.runtime.dispose();
316
+ }
317
+ this.#canvases.clear();
318
+ for (const entry of this.#opening.values()) entry.runtime.dispose();
319
+ this.#opening.clear();
320
+ }
321
+ async #replace(canvas, profiles) {
322
+ const key = canvasKey(canvas.sessionId, canvas.alias);
323
+ if (this.#disposed || this.#canvases.get(key) !== canvas) return;
324
+ this.#release(canvas);
325
+ const generation = canvas.generation;
326
+ const replacementId = ++canvas.replacementId;
327
+ let release;
328
+ try {
329
+ release = await canvas.runtime.replaceProfiles(profiles);
330
+ } catch (error) {
331
+ if (canvas.replacementId !== replacementId || this.#canvases.get(key) !== canvas) return;
332
+ throw error;
333
+ }
334
+ if (this.#disposed || this.#canvases.get(key) !== canvas || canvas.generation !== generation || canvas.replacementId !== replacementId) {
335
+ release();
336
+ return;
337
+ }
338
+ canvas.release = release;
339
+ canvas.profiles = profiles;
340
+ this.#register(canvas);
341
+ }
342
+ #register(canvas) {
343
+ if (this.#canvases.get(canvasKey(canvas.sessionId, canvas.alias)) === canvas) this.#send(canvas, {
344
+ kind: "register",
345
+ alias: canvas.alias,
346
+ generation: canvas.generation
347
+ });
348
+ }
349
+ #sendCatalog(canvas) {
350
+ this.#send(canvas, {
351
+ kind: "catalog",
352
+ alias: canvas.alias,
353
+ generation: canvas.generation,
354
+ ownerToken: canvas.ownerToken,
355
+ tools: canvas.profiles.flatMap((profile) => profile.tools.map((tool) => ({
356
+ name: tool.name,
357
+ label: tool.label ?? tool.name,
358
+ description: tool.description,
359
+ inputSchema: tool.inputSchema
360
+ })))
361
+ });
362
+ }
363
+ #sendCancelled(canvas, requestId) {
364
+ this.#send(canvas, {
365
+ kind: "cancelled",
366
+ alias: canvas.alias,
367
+ generation: canvas.generation,
368
+ ownerToken: canvas.ownerToken,
369
+ catalogToken: canvas.catalogToken,
370
+ requestId
371
+ });
372
+ }
373
+ #send(canvas, payload) {
374
+ const workspaceId = this.#host.mount?.scope === "session" ? this.#host.mount.workspaceId : void 0;
375
+ const mount = workspaceId ? {
376
+ scope: "workspace",
377
+ workspaceId
378
+ } : { scope: "global" };
379
+ this.#host.invokeServerMethod({
380
+ mount,
381
+ service: "author.bridge",
382
+ method: "send",
383
+ input: {
384
+ sessionId: canvas.sessionId,
385
+ message: payload
386
+ }
387
+ }).catch(() => void 0);
388
+ }
389
+ #current(canvas, requestId, controller) {
390
+ return this.#canvases.get(canvasKey(canvas.sessionId, canvas.alias)) === canvas && canvas.pending.get(requestId) === controller;
391
+ }
392
+ #release(canvas) {
393
+ clearTimeout(canvas.leaseTimer);
394
+ canvas.leaseTimer = void 0;
395
+ this.#send(canvas, {
396
+ kind: "release",
397
+ alias: canvas.alias,
398
+ generation: canvas.generation
399
+ });
400
+ canvas.release();
401
+ for (const controller of canvas.pending.values()) controller.abort(/* @__PURE__ */ new Error("Author viewport changed"));
402
+ canvas.pending.clear();
403
+ canvas.ownerToken = void 0;
404
+ canvas.catalogToken = void 0;
405
+ canvas.generation = ++this.#generation;
406
+ }
407
+ };
408
+ const authorBridgeRuntimes = new Store(/* @__PURE__ */ new Map());
409
+ const bridgeFor = (sessionId) => authorBridgeRuntimes.state.get(sessionId) ?? authorBridgeRuntimes.state.get("global");
410
+ function startAuthorBrowserBridge(runtime) {
411
+ const key = runtime.mount?.scope === "session" ? runtime.mount.sessionId : "global";
412
+ authorBridgeRuntimes.state.get(key)?.dispose();
413
+ const next = new AuthorBrowserBridge(runtime);
414
+ authorBridgeRuntimes.setState((state) => new Map(state).set(key, next));
415
+ return () => {
416
+ if (authorBridgeRuntimes.state.get(key) !== next) return;
417
+ next.dispose();
418
+ authorBridgeRuntimes.setState((state) => {
419
+ const remaining = new Map(state);
420
+ remaining.delete(key);
421
+ return remaining;
422
+ });
423
+ };
424
+ }
425
+ async function openAuthorCanvas(sessionId, alias, profiles) {
426
+ await bridgeFor(sessionId)?.open(sessionId, alias, profiles);
427
+ }
428
+ async function focusAuthorViewport(sessionId, profiles, alias = "default") {
429
+ return await bridgeFor(sessionId)?.focus(sessionId, alias, profiles) ?? (() => void 0);
430
+ }
431
+ function applyAuthorHubMessage(sessionId, message) {
432
+ bridgeFor(sessionId)?.apply(sessionId, message);
433
+ }
434
+ function dropAuthorViewportSession(sessionId, alias) {
435
+ bridgeFor(sessionId)?.drop(sessionId, alias);
436
+ }
437
+ function authorBridgeView(sessionId) {
438
+ return bridgeFor(sessionId)?.activeView(sessionId) ?? {
439
+ activation: "inactive",
440
+ capabilityCount: 0
441
+ };
442
+ }
443
+ //#endregion
444
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorGrid.ts
445
+ const AUTHOR_GRID_COLUMNS = "ABCDEFGH";
446
+ const nativeResolvers = /* @__PURE__ */ new Map();
447
+ const authorGrid = defineGlobalStore({ sessions: {} });
448
+ function parseAuthorGridCell(cell) {
449
+ const normalized = cell.trim().toUpperCase();
450
+ if (!/^[A-H][1-8]$/u.test(normalized)) throw new Error(`Invalid Author grid cell '${cell}'. Expected A1 through H8.`);
451
+ return {
452
+ cell: normalized,
453
+ column: AUTHOR_GRID_COLUMNS.indexOf(normalized[0]),
454
+ row: Number(normalized[1]) - 1
455
+ };
456
+ }
457
+ function updateAuthorGridGeometry(sessionId, input) {
458
+ const previous = authorGrid.store.state.sessions[sessionId];
459
+ const geometry = {
460
+ ...input,
461
+ geometryToken: crypto.randomUUID(),
462
+ snapshotId: previous?.documentPath === input.documentPath && previous.revision === input.revision && previous.sourceSha256 === input.sourceSha256 ? previous.snapshotId : crypto.randomUUID(),
463
+ updatedAt: Date.now()
464
+ };
465
+ authorGrid.update((state) => ({
466
+ ...state,
467
+ sessions: {
468
+ ...state.sessions,
469
+ [sessionId]: geometry
470
+ }
471
+ }));
472
+ return geometry;
473
+ }
474
+ function authorGridGeometry(sessionId) {
475
+ return authorGrid.store.state.sessions[sessionId];
476
+ }
477
+ function clearAuthorGridGeometry(sessionId) {
478
+ authorGrid.update((state) => {
479
+ if (state.sessions[sessionId] === void 0) return state;
480
+ const sessions = { ...state.sessions };
481
+ delete sessions[sessionId];
482
+ return {
483
+ ...state,
484
+ sessions
485
+ };
486
+ });
487
+ }
488
+ function registerAuthorGridResolver(sessionId, resolver) {
489
+ nativeResolvers.set(sessionId, resolver);
490
+ return () => {
491
+ if (nativeResolvers.get(sessionId) === resolver) nativeResolvers.delete(sessionId);
492
+ };
493
+ }
494
+ function resolveAuthorGridNativeAnchor(sessionId, cell) {
495
+ const geometry = authorGridGeometry(sessionId);
496
+ const resolved = geometry === void 0 ? void 0 : nativeResolvers.get(sessionId)?.(cell, geometry);
497
+ if (resolved === void 0) throw new Error("AMBIGUOUS_GRID: The grid cell does not resolve to one visible native document target.");
498
+ return resolved;
499
+ }
500
+ function resolveAuthorGridCell(sessionId, cell, expectedGeometryToken) {
501
+ const geometry = authorGridGeometry(sessionId);
502
+ if (geometry === void 0 || geometry.geometryToken !== expectedGeometryToken) throw new Error("STALE_GRID: The Author grid geometry changed. Describe the grid again.");
503
+ const parsed = parseAuthorGridCell(cell);
504
+ const width = 1 / 8;
505
+ const height = 1 / 8;
506
+ const rect = {
507
+ x: parsed.column * width,
508
+ y: parsed.row * height,
509
+ width,
510
+ height
511
+ };
512
+ return {
513
+ ...parsed,
514
+ rect,
515
+ center: {
516
+ x: rect.x + width / 2,
517
+ y: rect.y + height / 2
518
+ },
519
+ evidence: {
520
+ cell: parsed.cell,
521
+ geometryToken: geometry.geometryToken,
522
+ snapshotId: geometry.snapshotId
523
+ }
524
+ };
525
+ }
526
+ const TERMINAL_REQUEST_STATUSES = /* @__PURE__ */ new Set([
527
+ "COMPLETE",
528
+ "FAILED",
529
+ "CANCELLED"
530
+ ]);
531
+ const EMPTY_SESSION = {
532
+ generation: 0,
533
+ annotationsByDocument: {},
534
+ toolsByDocument: {},
535
+ requests: []
536
+ };
537
+ function normalizeAuthorPath(path) {
538
+ const parts = [];
539
+ for (const part of path.trim().replaceAll("\\", "/").split("/")) {
540
+ if (part === "" || part === ".") continue;
541
+ if (part === "..") parts.pop();
542
+ else parts.push(part);
543
+ }
544
+ return parts.join("/");
545
+ }
546
+ function authorDocumentKey(sessionId, path) {
547
+ return `${sessionId}\n${normalizeAuthorPath(path)}`;
548
+ }
549
+ const authorWorkspace = defineGlobalStore({
550
+ documents: {},
551
+ sessions: {}
552
+ });
553
+ function authorDocument(sessionId, path) {
554
+ if (sessionId === null) return void 0;
555
+ return authorWorkspace.store.state.documents[authorDocumentKey(sessionId, path)];
556
+ }
557
+ function authorSessionWorkspace(sessionId) {
558
+ if (sessionId === null) return EMPTY_SESSION;
559
+ return authorWorkspace.store.state.sessions[sessionId] ?? EMPTY_SESSION;
560
+ }
561
+ const EMPTY_COLLECTION = Object.freeze({
562
+ revision: 0,
563
+ stale: false,
564
+ candidate: void 0,
565
+ candidateText: "",
566
+ annotations: Object.freeze([]),
567
+ updatedAt: 0
568
+ });
569
+ function authorDocumentAnnotations(sessionId, path, state = authorWorkspace.store.state) {
570
+ if (sessionId === null) return EMPTY_COLLECTION;
571
+ const document = state.documents[authorDocumentKey(sessionId, path)];
572
+ const collection = state.sessions[sessionId]?.annotationsByDocument[normalizeAuthorPath(path)];
573
+ return document !== void 0 && collection !== void 0 && !collection.stale && collection.revision === document.version && collection.sourceSha256 === document.sourceSha256 ? collection : EMPTY_COLLECTION;
574
+ }
575
+ function hydrateAuthorAnnotationCollections(records) {
576
+ authorWorkspace.update((state) => {
577
+ let sessions = state.sessions;
578
+ for (const record of records) {
579
+ const path = normalizeAuthorPath(record.documentPath);
580
+ if (path === "") continue;
581
+ const session = sessions[record.sessionId] ?? EMPTY_SESSION;
582
+ const previous = session.annotationsByDocument[path];
583
+ if (previous !== void 0 && previous.updatedAt >= record.collection.updatedAt) continue;
584
+ const document = state.documents[authorDocumentKey(record.sessionId, path)];
585
+ const stale = record.collection.stale || document !== void 0 && (document.version !== record.collection.revision || document.sourceSha256 !== record.collection.sourceSha256);
586
+ const collection = restoreCollectionEvidence({
587
+ ...record.collection,
588
+ stale
589
+ });
590
+ sessions = {
591
+ ...sessions,
592
+ [record.sessionId]: {
593
+ ...session,
594
+ annotationsByDocument: {
595
+ ...session.annotationsByDocument,
596
+ [path]: collection
597
+ }
598
+ }
599
+ };
600
+ }
601
+ return sessions === state.sessions ? state : {
602
+ ...state,
603
+ sessions
604
+ };
605
+ });
606
+ }
607
+ function putAuthorDocument(sessionId, input) {
608
+ const path = normalizeAuthorPath(input.path);
609
+ if (path === "") throw new Error("Author document path must not be empty");
610
+ const key = authorDocumentKey(sessionId, path);
611
+ const staleThumbnails = [];
612
+ let result;
613
+ authorWorkspace.update((state) => {
614
+ const previous = state.documents[key];
615
+ const sourceChanged = previous?.sourceSha256 !== void 0 && input.sourceSha256 !== void 0 && previous.sourceSha256 !== input.sourceSha256;
616
+ const baselineVersion = sourceChanged ? previous.version + 1 : previous?.version ?? 0;
617
+ result = {
618
+ ...input,
619
+ path,
620
+ annotations: sourceChanged ? [] : previous?.annotations ?? [],
621
+ revisions: sourceChanged ? [] : previous?.revisions ?? [],
622
+ saveRequest: previous?.saveRequest ?? 0,
623
+ version: baselineVersion,
624
+ savedVersion: sourceChanged ? baselineVersion : previous?.savedVersion ?? 0,
625
+ ...sourceChanged || previous?.crop === void 0 ? {} : { crop: previous.crop }
626
+ };
627
+ const session = state.sessions[sessionId];
628
+ let sessions = state.sessions;
629
+ if (sourceChanged && session !== void 0) {
630
+ const collection = session.annotationsByDocument[path];
631
+ if (collection?.candidate?.thumbnailUrl !== void 0) staleThumbnails.push(collection.candidate.thumbnailUrl);
632
+ staleThumbnails.push(...(collection?.annotations ?? []).flatMap((annotation) => annotation.thumbnailUrl === void 0 ? [] : [annotation.thumbnailUrl]));
633
+ const now = Date.now();
634
+ const staleCollection = staleAuthorCollection(collection, now);
635
+ sessions = {
636
+ ...sessions,
637
+ [sessionId]: {
638
+ ...session,
639
+ annotationsByDocument: {
640
+ ...session.annotationsByDocument,
641
+ ...staleCollection && { [path]: staleCollection }
642
+ },
643
+ requests: session.requests.map((request) => input.kind !== "story-preview" && request.documentPath === path && [
644
+ "REQUESTED",
645
+ "CHANGING",
646
+ "CHANGED"
647
+ ].includes(request.status) ? {
648
+ ...request,
649
+ status: "FAILED",
650
+ currentOperation: void 0,
651
+ error: "The document source changed before the request completed.",
652
+ updatedAt: now
653
+ } : request)
654
+ }
655
+ };
656
+ }
657
+ return {
658
+ documents: {
659
+ ...state.documents,
660
+ [key]: result
661
+ },
662
+ sessions
663
+ };
664
+ });
665
+ staleThumbnails.forEach(revokeThumbnail);
666
+ return result;
667
+ }
668
+ /** Claims focus and returns a generation token that makes cleanup race-safe. */
669
+ function focusAuthorDocument(sessionId, path) {
670
+ let generation = 0;
671
+ updateSession(sessionId, (session) => {
672
+ generation = session.generation + 1;
673
+ return {
674
+ ...session,
675
+ generation,
676
+ focusedDocument: {
677
+ path: normalizeAuthorPath(path),
678
+ generation,
679
+ focusedAt: Date.now()
680
+ }
681
+ };
682
+ });
683
+ return generation;
684
+ }
685
+ function releaseAuthorDocumentFocus(sessionId, generation) {
686
+ updateSession(sessionId, (session) => session.focusedDocument?.generation === generation ? {
687
+ ...session,
688
+ focusedDocument: void 0
689
+ } : session);
690
+ }
691
+ function seekAuthorVideo(sessionId, path, timeSeconds) {
692
+ const normalized = normalizeAuthorPath(path);
693
+ const focused = authorSessionWorkspace(sessionId).focusedDocument;
694
+ if (focused?.path !== normalized || authorDocumentAnnotations(sessionId, normalized).candidate || !Number.isFinite(timeSeconds) || timeSeconds < 0 || authorDocument(sessionId, normalized)?.kind !== "video") return false;
695
+ setAuthorToolMode(sessionId, normalized, "select");
696
+ updateSession(sessionId, (session) => ({
697
+ ...session,
698
+ videoSeekRequest: {
699
+ path: normalized,
700
+ generation: focused.generation,
701
+ timeSeconds,
702
+ sequence: (session.videoSeekRequest?.sequence ?? 0) + 1
703
+ }
704
+ }));
705
+ return true;
706
+ }
707
+ function setAuthorPreviewSelection(sessionId, path, preview) {
708
+ const normalized = normalizeAuthorPath(path);
709
+ updateSession(sessionId, (session) => {
710
+ const selections = { ...session.previewSelections };
711
+ if (preview === void 0) delete selections[normalized];
712
+ else selections[normalized] = { ...preview };
713
+ return {
714
+ ...session,
715
+ previewSelections: selections
716
+ };
717
+ });
718
+ }
719
+ function setAuthorToolMode(sessionId, path, tool) {
720
+ const normalized = normalizeAuthorPath(path);
721
+ updateSession(sessionId, (session) => ({
722
+ ...session,
723
+ toolsByDocument: {
724
+ ...session.toolsByDocument,
725
+ [normalized]: tool
726
+ }
727
+ }));
728
+ }
729
+ function validateCandidate(sessionId, path, candidate) {
730
+ const document = authorDocument(sessionId, path);
731
+ if (document === void 0 || normalizeAuthorPath(candidate.documentPath) !== normalizeAuthorPath(path)) throw new Error("The Author selection does not belong to the document.");
732
+ if (document.version !== candidate.revision || document.sourceSha256 !== candidate.sourceSha256) throw new Error("The Author selection is stale for the document.");
733
+ }
734
+ function setAuthorRegionCandidate(sessionId, path, candidate) {
735
+ if (candidate !== void 0) validateCandidate(sessionId, path, candidate);
736
+ const thumbnail = authorDocumentAnnotations(sessionId, path).candidate?.thumbnailUrl;
737
+ updateCollection(sessionId, path, (collection) => ({
738
+ ...collection,
739
+ candidate: candidate && copyCandidate(candidate)
740
+ }));
741
+ if (thumbnail !== void 0 && thumbnail !== candidate?.thumbnailUrl) revokeThumbnail(thumbnail);
742
+ }
743
+ function setAuthorCandidateText(sessionId, path, candidateText) {
744
+ if (new TextEncoder().encode(candidateText).byteLength > 2048) throw new Error("Author candidate text must not exceed 2 KiB.");
745
+ updateCollection(sessionId, path, (collection) => ({
746
+ ...collection,
747
+ candidateText
748
+ }));
749
+ }
750
+ function commitAuthorRegion(sessionId, path, comment) {
751
+ const candidate = authorDocumentAnnotations(sessionId, path).candidate;
752
+ if (candidate === void 0) throw new Error("Select a document annotation before adding a comment.");
753
+ const id = crypto.randomUUID();
754
+ addAuthorRegion(sessionId, {
755
+ ...candidate,
756
+ id,
757
+ comment,
758
+ version: 1
759
+ });
760
+ updateCollection(sessionId, path, (collection) => ({
761
+ ...collection,
762
+ candidate: void 0,
763
+ candidateText: ""
764
+ }));
765
+ return id;
766
+ }
767
+ function addAuthorRegion(sessionId, region) {
768
+ if (region.comment.trim() === "") throw new Error("Every Author annotation requires a comment.");
769
+ validateCandidate(sessionId, region.documentPath, region);
770
+ updateCollection(sessionId, region.documentPath, (collection) => {
771
+ if (collection.annotations.length >= 16) throw new Error(`Author requests support at most 16 annotations.`);
772
+ if (collection.annotations.some((candidate) => candidate.id === region.id)) throw new Error(`Author annotation '${region.id}' already exists.`);
773
+ return {
774
+ ...collection,
775
+ annotations: [...collection.annotations, copyRegion({
776
+ ...region,
777
+ version: region.version ?? 1
778
+ })]
779
+ };
780
+ });
781
+ }
782
+ function removeAuthorRegion(sessionId, path, regionId, expectedVersion) {
783
+ let thumbnail;
784
+ updateCollection(sessionId, path, (collection) => {
785
+ const annotation = collection.annotations.find((candidate) => candidate.id === regionId);
786
+ if (annotation === void 0 || expectedVersion !== void 0 && (annotation.version ?? 1) !== expectedVersion) return collection;
787
+ thumbnail = annotation.thumbnailUrl;
788
+ return {
789
+ ...collection,
790
+ annotations: collection.annotations.filter((candidate) => candidate.id !== regionId)
791
+ };
792
+ });
793
+ if (thumbnail !== void 0) revokeThumbnail(thumbnail);
794
+ }
795
+ function putAuthorRequest(sessionId, record) {
796
+ if (record.requestText.trim() === "") throw new Error("An Author request requires verbatim request text.");
797
+ if (record.regions.length === 0 || record.regions.some((region) => region.comment.trim() === "")) throw new Error("An Author request requires at least one commented region.");
798
+ updateSession(sessionId, (session) => {
799
+ if (session.requests.some((request) => request.id === record.id)) throw new Error(`Author request '${record.id}' already exists.`);
800
+ return {
801
+ ...session,
802
+ requests: boundHistory([...session.requests, copyRequest(record)])
803
+ };
804
+ });
805
+ }
806
+ function updateAuthorRequest(sessionId, requestId, update) {
807
+ updateSession(sessionId, (session) => ({
808
+ ...session,
809
+ requests: boundHistory(session.requests.map((request) => request.id === requestId ? copyRequest(update(request)) : request))
810
+ }));
811
+ }
812
+ function reviseAuthorDocument(sessionId, path, content) {
813
+ updateDocument(sessionId, path, (document) => {
814
+ if (document.content === content) return document;
815
+ const version = document.version + 1;
816
+ return {
817
+ ...document,
818
+ content,
819
+ version,
820
+ revisions: [...document.revisions, {
821
+ revision: version,
822
+ content
823
+ }]
824
+ };
825
+ });
826
+ }
827
+ function reviseAuthorFragment(sessionId, path, fragmentId, text) {
828
+ updateDocument(sessionId, path, (document) => {
829
+ const fragment = document.fragments?.find((candidate) => candidate.id === fragmentId);
830
+ if (fragment === void 0 || fragment.text === text) return document;
831
+ const version = document.version + 1;
832
+ return {
833
+ ...document,
834
+ fragments: document.fragments?.map((fragment) => fragment.id === fragmentId ? {
835
+ ...fragment,
836
+ text
837
+ } : fragment),
838
+ version,
839
+ revisions: [...document.revisions, {
840
+ revision: version,
841
+ content: text
842
+ }]
843
+ };
844
+ });
845
+ }
846
+ function addAuthorAnnotation(sessionId, path, annotation) {
847
+ updateDocument(sessionId, path, (document) => ({
848
+ ...document,
849
+ annotations: [...document.annotations, annotation]
850
+ }));
851
+ }
852
+ function setAuthorCrop(sessionId, path, crop) {
853
+ updateDocument(sessionId, path, (document) => {
854
+ const version = document.version + 1;
855
+ return {
856
+ ...document,
857
+ ...crop === void 0 ? { crop: void 0 } : { crop },
858
+ version,
859
+ revisions: [...document.revisions, {
860
+ revision: version,
861
+ content: JSON.stringify(crop ?? null)
862
+ }]
863
+ };
864
+ });
865
+ }
866
+ function requestAuthorSave(sessionId, path) {
867
+ updateDocument(sessionId, path, (document) => ({
868
+ ...document,
869
+ saveRequest: document.saveRequest + 1,
870
+ savingVersion: document.version
871
+ }));
872
+ }
873
+ function failAuthorSave(sessionId, path, savingVersion) {
874
+ updateDocument(sessionId, path, (document) => document.savingVersion === savingVersion ? {
875
+ ...document,
876
+ savingVersion: void 0
877
+ } : document);
878
+ }
879
+ function completeAuthorSave(sessionId, path, sourceSha256, savedVersion, savedFragments) {
880
+ updateDocument(sessionId, path, (document) => {
881
+ if (savedVersion < document.savedVersion) return document;
882
+ return {
883
+ ...document,
884
+ sourceSha256,
885
+ savedVersion,
886
+ savingVersion: document.savingVersion === savedVersion ? void 0 : document.savingVersion,
887
+ originalFragments: savedFragments?.map((fragment) => ({ ...fragment })),
888
+ ...document.version === savedVersion ? { crop: void 0 } : {},
889
+ revisions: document.revisions.filter((revision) => revision.revision > savedVersion)
890
+ };
891
+ });
892
+ const now = Date.now();
893
+ updateSession(sessionId, (session) => ({
894
+ ...session,
895
+ requests: boundHistory(session.requests.map((request) => request.documentPath === normalizeAuthorPath(path) && request.status === "CHANGED" && request.revision <= savedVersion ? {
896
+ ...request,
897
+ status: "COMPLETE",
898
+ currentOperation: void 0,
899
+ updatedAt: now
900
+ } : request))
901
+ }));
902
+ }
903
+ function dropAuthorSession(sessionId) {
904
+ const prefix = `${sessionId}\n`;
905
+ const session = authorWorkspace.store.state.sessions[sessionId];
906
+ for (const collection of Object.values(session?.annotationsByDocument ?? {})) {
907
+ if (collection.candidate?.thumbnailUrl !== void 0) revokeThumbnail(collection.candidate.thumbnailUrl);
908
+ collection.annotations.forEach((annotation) => {
909
+ if (annotation.thumbnailUrl !== void 0) revokeThumbnail(annotation.thumbnailUrl);
910
+ });
911
+ }
912
+ authorWorkspace.update((state) => {
913
+ const sessions = { ...state.sessions };
914
+ delete sessions[sessionId];
915
+ return {
916
+ documents: Object.fromEntries(Object.entries(state.documents).filter(([key]) => !key.startsWith(prefix))),
917
+ sessions
918
+ };
919
+ });
920
+ }
921
+ function setAuthorStoryPreview(sessionId, path, storyPreview) {
922
+ updateDocument(sessionId, path, (document) => ({
923
+ ...document,
924
+ storyPreview: structuredClone(storyPreview)
925
+ }));
926
+ }
927
+ function updateDocument(sessionId, path, update) {
928
+ const key = authorDocumentKey(sessionId, path);
929
+ const staleThumbnails = [];
930
+ authorWorkspace.update((state) => {
931
+ const document = state.documents[key];
932
+ if (document === void 0) return state;
933
+ const next = update(document);
934
+ const session = state.sessions[sessionId];
935
+ const identityChanged = document.version !== next.version || document.sourceSha256 !== next.sourceSha256;
936
+ const collection = identityChanged ? session?.annotationsByDocument[document.path] : void 0;
937
+ if (collection !== void 0) {
938
+ if (collection.candidate?.thumbnailUrl !== void 0) staleThumbnails.push(collection.candidate.thumbnailUrl);
939
+ staleThumbnails.push(...collection.annotations.flatMap((annotation) => annotation.thumbnailUrl === void 0 ? [] : [annotation.thumbnailUrl]));
940
+ }
941
+ const staleCollection = identityChanged ? staleAuthorCollection(collection, Date.now()) : void 0;
942
+ const sessions = identityChanged && session !== void 0 ? {
943
+ ...state.sessions,
944
+ [sessionId]: {
945
+ ...session,
946
+ annotationsByDocument: {
947
+ ...session.annotationsByDocument,
948
+ ...staleCollection && { [document.path]: staleCollection }
949
+ }
950
+ }
951
+ } : state.sessions;
952
+ return {
953
+ documents: {
954
+ ...state.documents,
955
+ [key]: next
956
+ },
957
+ sessions
958
+ };
959
+ });
960
+ staleThumbnails.forEach(revokeThumbnail);
961
+ }
962
+ function updateSession(sessionId, update) {
963
+ authorWorkspace.update((state) => {
964
+ const session = state.sessions[sessionId] ?? EMPTY_SESSION;
965
+ const next = update(session);
966
+ return next === session ? state : {
967
+ ...state,
968
+ sessions: {
969
+ ...state.sessions,
970
+ [sessionId]: next
971
+ }
972
+ };
973
+ });
974
+ }
975
+ function copyCandidate(candidate) {
976
+ const anchor = "rect" in candidate.anchor ? {
977
+ ...candidate.anchor,
978
+ rect: { ...candidate.anchor.rect }
979
+ } : "point" in candidate.anchor ? {
980
+ ...candidate.anchor,
981
+ point: { ...candidate.anchor.point }
982
+ } : { ...candidate.anchor };
983
+ return {
984
+ ...candidate,
985
+ anchor,
986
+ viewport: { ...candidate.viewport },
987
+ stroke: candidate.stroke?.map((point) => ({ ...point })),
988
+ voiceGrid: candidate.voiceGrid && { ...candidate.voiceGrid }
989
+ };
990
+ }
991
+ function copyRegion(region) {
992
+ return copyCandidate(region);
993
+ }
994
+ function staleAuthorCollection(collection, updatedAt) {
995
+ if (collection === void 0) return void 0;
996
+ return {
997
+ ...collection,
998
+ stale: true,
999
+ candidate: collection.candidate && {
1000
+ ...copyCandidate(collection.candidate),
1001
+ thumbnailUrl: void 0
1002
+ },
1003
+ annotations: collection.annotations.map((annotation) => ({
1004
+ ...copyRegion(annotation),
1005
+ thumbnailUrl: void 0
1006
+ })),
1007
+ updatedAt
1008
+ };
1009
+ }
1010
+ function updateCollection(sessionId, path, update) {
1011
+ const normalized = normalizeAuthorPath(path);
1012
+ const document = authorDocument(sessionId, normalized);
1013
+ if (document === void 0) return;
1014
+ updateSession(sessionId, (session) => {
1015
+ const current = authorDocumentAnnotations(sessionId, normalized);
1016
+ const base = current === EMPTY_COLLECTION ? {
1017
+ ...EMPTY_COLLECTION,
1018
+ revision: document.version,
1019
+ sourceSha256: document.sourceSha256
1020
+ } : current;
1021
+ const next = update(base);
1022
+ if (next === base) return session;
1023
+ return {
1024
+ ...session,
1025
+ annotationsByDocument: {
1026
+ ...session.annotationsByDocument,
1027
+ [normalized]: {
1028
+ ...next,
1029
+ stale: false,
1030
+ updatedAt: Date.now()
1031
+ }
1032
+ }
1033
+ };
1034
+ });
1035
+ }
1036
+ function boundedHistoryText(value) {
1037
+ if (value === void 0) return void 0;
1038
+ const encoder = new TextEncoder();
1039
+ if (encoder.encode(value).byteLength <= 16384) return value;
1040
+ let end = value.length;
1041
+ while (end > 0 && encoder.encode(value.slice(0, end)).byteLength > 16384) end -= 256;
1042
+ return value.slice(0, end);
1043
+ }
1044
+ function copyRequest(record) {
1045
+ return {
1046
+ ...record,
1047
+ before: boundedHistoryText(record.before),
1048
+ after: boundedHistoryText(record.after),
1049
+ regions: record.regions.map(copyRegion),
1050
+ pendingRegions: record.pendingRegions?.map(copyRegion)
1051
+ };
1052
+ }
1053
+ function boundHistory(records) {
1054
+ const next = [...records];
1055
+ const bytes = () => new TextEncoder().encode(JSON.stringify(next)).byteLength;
1056
+ while (next.length > 100 || bytes() > 524288) {
1057
+ const terminal = next.findIndex((record) => TERMINAL_REQUEST_STATUSES.has(record.status));
1058
+ if (terminal < 0) throw new Error("Author request history is full while active requests are still running.");
1059
+ next.splice(terminal, 1);
1060
+ }
1061
+ return next;
1062
+ }
1063
+ function restoreCollectionEvidence(collection) {
1064
+ const restore = (item) => {
1065
+ const copied = copyCandidate(item);
1066
+ const existingVersion = Reflect.get(copied, "version");
1067
+ const versioned = "id" in copied ? {
1068
+ ...copied,
1069
+ version: typeof existingVersion === "number" ? existingVersion : 1
1070
+ } : copied;
1071
+ if (versioned.thumbnailUrl !== void 0 || versioned.evidence === void 0) return versioned;
1072
+ try {
1073
+ return {
1074
+ ...versioned,
1075
+ thumbnailUrl: URL.createObjectURL(versioned.evidence)
1076
+ };
1077
+ } catch {
1078
+ return versioned;
1079
+ }
1080
+ };
1081
+ return {
1082
+ ...collection,
1083
+ candidate: collection.candidate && restore(collection.candidate),
1084
+ annotations: collection.annotations.map(restore)
1085
+ };
1086
+ }
1087
+ function revokeThumbnail(url) {
1088
+ if (!url.startsWith("blob:")) return;
1089
+ try {
1090
+ URL.revokeObjectURL(url);
1091
+ } catch {}
1092
+ }
1093
+ //#endregion
1094
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/_lib/authorStore.ts
1095
+ const author = defineSessionStore({
1096
+ activation: "inactive",
1097
+ capabilityCount: 0
1098
+ });
1099
+ function isRecord(value) {
1100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1101
+ }
1102
+ function parseAuthorHubMessage(input) {
1103
+ if (!isRecord(input)) return null;
1104
+ if ((input.activation === "active" || input.activation === "inactive") && Number.isSafeInteger(input.capabilityCount) && input.capabilityCount >= 0) return input;
1105
+ if (typeof input.kind !== "string") return null;
1106
+ if (input.kind === "rejected") return typeof input.reason === "string" ? input : null;
1107
+ if (!Number.isSafeInteger(input.generation) || typeof input.ownerToken !== "string") return null;
1108
+ if (input.kind === "accepted") return typeof input.leaseMs === "number" && (input.catalogToken === void 0 || typeof input.catalogToken === "string") ? input : null;
1109
+ if ((input.kind === "request" || input.kind === "cancel") && typeof input.catalogToken === "string" && typeof input.requestId === "string") {
1110
+ if (input.kind === "cancel") return input;
1111
+ return typeof input.name === "string" && isRecord(input.arguments) ? input : null;
1112
+ }
1113
+ return null;
1114
+ }
1115
+ //#endregion
1116
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/author-webmcp.web.ts
1117
+ var author_webmcp_web_default = defineChannelFile({
1118
+ channel: authorChannelType,
1119
+ parse: parseAuthorHubMessage,
1120
+ apply(sessionId, message) {
1121
+ if ("activation" in message) {
1122
+ author.update(sessionId, () => message);
1123
+ return;
1124
+ }
1125
+ applyAuthorHubMessage(sessionId, message);
1126
+ author.update(sessionId, () => authorBridgeView(sessionId));
1127
+ },
1128
+ drop(sessionId) {
1129
+ clearAuthorGridGeometry(sessionId);
1130
+ dropAuthorViewportSession(sessionId);
1131
+ dropAuthorSession(sessionId);
1132
+ author.drop(sessionId);
1133
+ }
1134
+ });
1135
+ //#endregion
1136
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorGridOverlay.tsx
1137
+ function autonomousVoiceGridVisible(statuses) {
1138
+ return statuses["doom-voice"]?.trimStart().startsWith("voice auto:") === true;
1139
+ }
1140
+ function AuthorGridOverlay({ sessionId, document, visible }) {
1141
+ const host = useRef(null);
1142
+ useEffect(() => {
1143
+ const element = host.current?.parentElement;
1144
+ if (!visible || element === null || element === void 0) {
1145
+ clearAuthorGridGeometry(sessionId);
1146
+ return;
1147
+ }
1148
+ const publish = () => {
1149
+ const bounds = element.getBoundingClientRect();
1150
+ updateAuthorGridGeometry(sessionId, {
1151
+ documentPath: document.path,
1152
+ revision: document.version,
1153
+ sourceSha256: document.sourceSha256,
1154
+ viewport: {
1155
+ width: bounds.width,
1156
+ height: bounds.height,
1157
+ originX: bounds.left,
1158
+ originY: bounds.top,
1159
+ scrollX: element.scrollLeft,
1160
+ scrollY: element.scrollTop,
1161
+ zoom: window.devicePixelRatio
1162
+ }
1163
+ });
1164
+ };
1165
+ publish();
1166
+ const observer = new ResizeObserver(publish);
1167
+ observer.observe(element);
1168
+ element.addEventListener("scroll", publish, {
1169
+ passive: true,
1170
+ capture: true
1171
+ });
1172
+ window.addEventListener("focus", publish);
1173
+ return () => {
1174
+ observer.disconnect();
1175
+ element.removeEventListener("scroll", publish, true);
1176
+ window.removeEventListener("focus", publish);
1177
+ clearAuthorGridGeometry(sessionId);
1178
+ };
1179
+ }, [
1180
+ document.path,
1181
+ document.sourceSha256,
1182
+ document.version,
1183
+ sessionId,
1184
+ visible
1185
+ ]);
1186
+ if (!visible) return null;
1187
+ return /* @__PURE__ */ jsx("div", {
1188
+ ref: host,
1189
+ "data-testid": "author-grid",
1190
+ "aria-hidden": "true",
1191
+ className: "pointer-events-none absolute inset-0 z-20 grid grid-cols-8 grid-rows-8",
1192
+ children: Array.from({ length: 64 }, (_, index) => {
1193
+ const column = index % 8;
1194
+ const row = Math.floor(index / 8);
1195
+ return /* @__PURE__ */ jsx("div", {
1196
+ className: "relative border border-doom-red/25",
1197
+ children: /* @__PURE__ */ jsxs("span", {
1198
+ className: "absolute left-1 top-0.5 rounded bg-doom-deep/80 px-1 text-2xs font-semibold text-doom-red",
1199
+ children: [AUTHOR_GRID_COLUMNS[column], row + 1]
1200
+ })
1201
+ }, index);
1202
+ })
1203
+ });
1204
+ }
1205
+ //#endregion
1206
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorCanvasState.ts
1207
+ const canvasAliases = new Store({});
1208
+ const canvasPaths = new Store({});
1209
+ const canvasFailures = new Store({});
1210
+ const authorCanvasAlias = (sessionId, path) => canvasAliases.state[authorDocumentKey(sessionId, path)];
1211
+ /** Keep freehand evidence small and source-relative. Reject malformed geometry rather than degrading it into a crop. */
1212
+ function validAuthorStroke(value) {
1213
+ 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);
1214
+ }
1215
+ function authorStrokeBounds(points) {
1216
+ if (!validAuthorStroke(points)) return null;
1217
+ const left = Math.min(...points.map((point) => point.x));
1218
+ const top = Math.min(...points.map((point) => point.y));
1219
+ const right = Math.max(...points.map((point) => point.x));
1220
+ const bottom = Math.max(...points.map((point) => point.y));
1221
+ const x = Math.max(0, Math.min(left, .999));
1222
+ const y = Math.max(0, Math.min(top, .999));
1223
+ return {
1224
+ x,
1225
+ y,
1226
+ width: Math.min(1 - x, Math.max(right - x, .001)),
1227
+ height: Math.min(1 - y, Math.max(bottom - y, .001))
1228
+ };
1229
+ }
1230
+ function appendAuthorStrokePoint(points, point) {
1231
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return points;
1232
+ const next = {
1233
+ x: Math.max(0, Math.min(1, point.x)),
1234
+ y: Math.max(0, Math.min(1, point.y))
1235
+ };
1236
+ if (points.length >= 128) return points;
1237
+ const last = points.at(-1);
1238
+ return last?.x === next.x && last.y === next.y ? points : [...points, next];
1239
+ }
1240
+ /** Clamp a drag to the source element, including drags made in reverse. */
1241
+ function normalizedAuthorRectangle(bounds, drag) {
1242
+ const width = bounds.right - bounds.left;
1243
+ const height = bounds.bottom - bounds.top;
1244
+ if (![...Object.values(bounds), ...Object.values(drag)].every(Number.isFinite) || width <= 0 || height <= 0) return null;
1245
+ const left = Math.max(bounds.left, Math.min(drag.left, drag.right));
1246
+ const right = Math.min(bounds.right, Math.max(drag.left, drag.right));
1247
+ const top = Math.max(bounds.top, Math.min(drag.top, drag.bottom));
1248
+ const bottom = Math.min(bounds.bottom, Math.max(drag.top, drag.bottom));
1249
+ if (right <= left || bottom <= top) return null;
1250
+ return {
1251
+ x: (left - bounds.left) / width,
1252
+ y: (top - bounds.top) / height,
1253
+ width: (right - left) / width,
1254
+ height: (bottom - top) / height
1255
+ };
1256
+ }
1257
+ //#endregion
1258
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorCapture.ts
1259
+ const AUTHOR_CAPTURE_MAX_DIMENSION = 1600;
1260
+ const AUTHOR_PACKET_MAX_BYTES = 65536;
1261
+ const AUTHOR_QUOTE_MAX_BYTES = 4096;
1262
+ const AUTHOR_REGION_COLORS = [
1263
+ "#ff5f57",
1264
+ "#00c2ff",
1265
+ "#ffd166",
1266
+ "#b388ff",
1267
+ "#4ade80",
1268
+ "#ff8c42",
1269
+ "#f472b6",
1270
+ "#2dd4bf",
1271
+ "#a3e635",
1272
+ "#60a5fa",
1273
+ "#e879f9",
1274
+ "#facc15",
1275
+ "#34d399",
1276
+ "#fb7185",
1277
+ "#818cf8",
1278
+ "#22d3ee"
1279
+ ];
1280
+ function annotationForeground(color) {
1281
+ const red = Number.parseInt(color.slice(1, 3), 16);
1282
+ const green = Number.parseInt(color.slice(3, 5), 16);
1283
+ const blue = Number.parseInt(color.slice(5, 7), 16);
1284
+ return red * 299 + green * 587 + blue * 114 >= 15e4 ? "#101216" : "#ffffff";
1285
+ }
1286
+ const bytes = (value) => new TextEncoder().encode(value).byteLength;
1287
+ function utf8Prefix(value, limit) {
1288
+ if (bytes(value) <= limit) return value;
1289
+ let low = 0;
1290
+ let high = value.length;
1291
+ while (low < high) {
1292
+ const middle = Math.ceil((low + high) / 2);
1293
+ if (bytes(value.slice(0, middle)) <= limit) low = middle;
1294
+ else high = middle - 1;
1295
+ }
1296
+ return value.slice(0, low);
1297
+ }
1298
+ function createAuthorCapturePacket(captureId, capturedAt, document, regions, alias) {
1299
+ if (regions.length === 0 || regions.length > 16) throw new Error("Capture requires between 1 and 16 regions.");
1300
+ if (document.kind === "story-preview" && document.storyPreview === void 0) throw new Error("Story preview capture requires source provenance.");
1301
+ const packet = {
1302
+ version: 1,
1303
+ captureId,
1304
+ capturedAt,
1305
+ document: {
1306
+ path: document.path,
1307
+ ...alias === void 0 ? {} : { alias },
1308
+ kind: document.kind,
1309
+ revision: document.version,
1310
+ ...document.sourceSha256 === void 0 ? {} : { sourceSha256: document.sourceSha256 },
1311
+ ...document.storyPreview === void 0 ? {} : { storyPreview: structuredClone(document.storyPreview) }
1312
+ },
1313
+ regions: regions.map((region, index) => {
1314
+ 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.`);
1315
+ 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.`);
1316
+ if (region.comment.trim() === "") throw new Error(`Region ${String(index + 1)} requires a comment.`);
1317
+ if (bytes(region.comment) > 2048) throw new Error(`Region ${String(index + 1)} comment exceeds 2 KiB.`);
1318
+ return {
1319
+ id: region.id,
1320
+ ordinal: index + 1,
1321
+ comment: region.comment,
1322
+ version: region.version ?? 1,
1323
+ mode: region.mode ?? ("point" in region.anchor ? "point" : "region"),
1324
+ ...region.quote === void 0 ? {} : { quote: utf8Prefix(region.quote, AUTHOR_QUOTE_MAX_BYTES) },
1325
+ anchor: structuredClone(region.anchor),
1326
+ ...region.stroke === void 0 ? {} : { stroke: region.stroke.map((point) => ({ ...point })) },
1327
+ viewport: { ...region.viewport },
1328
+ ...region.voiceGrid === void 0 ? {} : { voiceGrid: { ...region.voiceGrid } }
1329
+ };
1330
+ })
1331
+ };
1332
+ if (bytes(JSON.stringify(packet)) > 65536) throw new Error("Author capture packet exceeds 64 KiB.");
1333
+ return packet;
1334
+ }
1335
+ function percent(value) {
1336
+ return `${String(Math.round(value * 1e3) / 10)}%`;
1337
+ }
1338
+ function rectLocation(rect) {
1339
+ return `x ${percent(rect.x)}, y ${percent(rect.y)}, w ${percent(rect.width)}, h ${percent(rect.height)}`;
1340
+ }
1341
+ /** Context stays below the host limit even when paths and anchor locations are unusually long. */
1342
+ function authorDocumentContextContent(path, alias, annotations) {
1343
+ const heading = `Author document: ${path}${alias === void 0 ? "" : ` (canvas ${alias})`}`;
1344
+ const drafts = annotations.map((region, index) => `(${index + 1}) ${region.comment.slice(0, 1e3)} [${anchorLocation(region.anchor)}]`);
1345
+ return utf8Prefix([heading, ...drafts.length === 0 ? [] : ["Unsent annotations (not yet submitted):", ...drafts]].join("\n"), AUTHOR_PACKET_MAX_BYTES);
1346
+ }
1347
+ function anchorLocation(anchor) {
1348
+ switch (anchor.kind) {
1349
+ case "text-range": return anchor.startLine === anchor.endLine ? `line ${String(anchor.startLine)}` : `lines ${String(anchor.startLine)}-${String(anchor.endLine)}`;
1350
+ case "cell": return `${anchor.sheet === void 0 ? "" : `${anchor.sheet}!`}${anchor.location}`;
1351
+ case "slide-element": return `slide ${String(anchor.slide)}: ${anchor.location}`;
1352
+ case "image-rect": return `image ${rectLocation(anchor.rect)}`;
1353
+ case "pdf-page-rect": return `page ${String(anchor.page)}, ${rectLocation(anchor.rect)}`;
1354
+ case "video-time-rect": return `time ${anchor.timeSeconds.toFixed(3)}s${anchor.frame === void 0 ? "" : `, frame ${String(anchor.frame)}`}, ${rectLocation(anchor.rect)}`;
1355
+ case "story-preview-rect": return `story preview ${rectLocation(anchor.rect)}`;
1356
+ case "image-point": return `image point x ${percent(anchor.point.x)}, y ${percent(anchor.point.y)}`;
1357
+ case "pdf-page-point": return `page ${String(anchor.page)}, point x ${percent(anchor.point.x)}, y ${percent(anchor.point.y)}`;
1358
+ case "video-time-point": return `time ${anchor.timeSeconds.toFixed(3)}s, point x ${percent(anchor.point.x)}, y ${percent(anchor.point.y)}`;
1359
+ case "story-preview-point": return `story preview point x ${percent(anchor.point.x)}, y ${percent(anchor.point.y)}`;
1360
+ }
1361
+ }
1362
+ function storyPreviewProvenance(packet) {
1363
+ const preview = packet.document.storyPreview;
1364
+ if (preview === void 0) return void 0;
1365
+ const sources = preview.sources.map((source) => `- ${source.path} (sha256 ${source.sha256})`).join("\n");
1366
+ return [
1367
+ "Story preview source (edit these sources, never the generated preview artifact):",
1368
+ `Project: ${preview.projectPath}`,
1369
+ `Story: ${preview.storyPath}#${preview.storyExport}`,
1370
+ `Build revision: ${preview.buildRevision}`,
1371
+ `Viewport: ${String(preview.viewport.width)}x${String(preview.viewport.height)}`,
1372
+ "Sources:",
1373
+ sources,
1374
+ "After editing the underlying story or component source, rebuild the preview before treating it as visually verified."
1375
+ ].join("\n");
1376
+ }
1377
+ function authorCaptureContext(packet) {
1378
+ const metadata = JSON.stringify(packet);
1379
+ if (bytes(metadata) > 65536) throw new Error("Author capture packet exceeds 64 KiB.");
1380
+ const annotations = packet.regions.map((region) => `(${String(region.ordinal)}) ${region.comment} [${anchorLocation(region.anchor)}]${region.quote ? `\nQuoted text: ${region.quote}` : ""}`).join("\n\n");
1381
+ const provenance = storyPreviewProvenance(packet);
1382
+ const content = provenance === void 0 ? annotations : `${provenance}\n\nAnnotations:\n${annotations}`;
1383
+ return {
1384
+ kind: "author-capture",
1385
+ source: "author",
1386
+ id: packet.captureId,
1387
+ label: `${packet.regions.length} annotation${packet.regions.length === 1 ? "" : "s"} · ${packet.document.path}`,
1388
+ content: `Author document: ${packet.document.path}${packet.document.alias === void 0 ? "" : ` (canvas ${packet.document.alias})`}\n${content}`,
1389
+ metadata
1390
+ };
1391
+ }
1392
+ function canvasBlob(canvas, type, quality) {
1393
+ return new Promise((resolve, reject) => {
1394
+ canvas.toBlob((blob) => blob === null ? reject(/* @__PURE__ */ new Error("Unable to capture viewport")) : resolve(blob), type, quality);
1395
+ });
1396
+ }
1397
+ async function base64(blob) {
1398
+ const bytes = new Uint8Array(await blob.arrayBuffer());
1399
+ let binary = "";
1400
+ for (let offset = 0; offset < bytes.length; offset += 32768) binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
1401
+ return btoa(binary);
1402
+ }
1403
+ function throwIfAborted(signal) {
1404
+ if (signal?.aborted === true) throw signal.reason;
1405
+ }
1406
+ async function boundedCanvasCapture(canvas) {
1407
+ let mimeType = "image/png";
1408
+ let blob = await canvasBlob(canvas, mimeType);
1409
+ for (const quality of [
1410
+ .9,
1411
+ .75,
1412
+ .6,
1413
+ .45,
1414
+ .3
1415
+ ]) {
1416
+ if (blob.size <= 2097152) break;
1417
+ mimeType = "image/jpeg";
1418
+ blob = await canvasBlob(canvas, mimeType, quality);
1419
+ }
1420
+ if (blob.size > 2097152) throw new Error("Viewport capture exceeds 2 MiB");
1421
+ return {
1422
+ data: await base64(blob),
1423
+ mimeType
1424
+ };
1425
+ }
1426
+ function loadImage(url) {
1427
+ return new Promise((resolve, reject) => {
1428
+ const image = new Image();
1429
+ image.onload = () => resolve(image);
1430
+ image.onerror = () => reject(/* @__PURE__ */ new Error("Unable to load a region capture."));
1431
+ image.src = url;
1432
+ });
1433
+ }
1434
+ function multiRegionCaptureProvider(regions) {
1435
+ return { async capture(signal) {
1436
+ throwIfAborted(signal);
1437
+ if (regions.length === 0 || regions.length > 16) throw new Error("Capture requires between 1 and 16 regions.");
1438
+ const tiles = [];
1439
+ for (const region of regions) {
1440
+ const image = region.thumbnailUrl === void 0 ? void 0 : await loadImage(region.thumbnailUrl);
1441
+ throwIfAborted(signal);
1442
+ const width = image?.naturalWidth ?? region.viewport.width;
1443
+ const height = image?.naturalHeight ?? region.viewport.height;
1444
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) throw new Error("Image is not ready to capture");
1445
+ tiles.push({
1446
+ region,
1447
+ image,
1448
+ width,
1449
+ height
1450
+ });
1451
+ }
1452
+ const width = Math.min(AUTHOR_CAPTURE_MAX_DIMENSION, Math.max(...tiles.map((tile) => tile.width)));
1453
+ const naturalHeight = tiles.reduce((height, tile) => height + tile.height * width / tile.width, 0);
1454
+ const scale = Math.min(1, AUTHOR_CAPTURE_MAX_DIMENSION / Math.max(width, naturalHeight));
1455
+ const canvas = document.createElement("canvas");
1456
+ canvas.width = Math.max(1, Math.round(width * scale));
1457
+ canvas.height = Math.max(1, Math.round(naturalHeight * scale));
1458
+ const context = canvas.getContext("2d");
1459
+ if (context === null) throw new Error("Canvas is unavailable");
1460
+ context.scale(scale, scale);
1461
+ context.fillStyle = "#101216";
1462
+ context.fillRect(0, 0, width, naturalHeight);
1463
+ let y = 0;
1464
+ for (const [index, { region, image, width: sourceWidth, height: sourceHeight }] of tiles.entries()) {
1465
+ const annotationColor = AUTHOR_REGION_COLORS[index % AUTHOR_REGION_COLORS.length];
1466
+ throwIfAborted(signal);
1467
+ const tileHeight = sourceHeight * width / sourceWidth;
1468
+ const drawWidth = width;
1469
+ const drawHeight = tileHeight;
1470
+ const drawX = 0;
1471
+ const drawY = y;
1472
+ let labelX = drawX;
1473
+ let labelY = drawY;
1474
+ if (image !== void 0) {
1475
+ context.drawImage(image, drawX, drawY, drawWidth, drawHeight);
1476
+ if (region.stroke !== void 0) {
1477
+ if (!validAuthorStroke(region.stroke)) throw new Error("Invalid feedback stroke geometry.");
1478
+ context.beginPath();
1479
+ region.stroke.forEach((point, index) => {
1480
+ const x = drawX + point.x * drawWidth;
1481
+ const y = drawY + point.y * drawHeight;
1482
+ if (index === 0) context.moveTo(x, y);
1483
+ else context.lineTo(x, y);
1484
+ });
1485
+ context.strokeStyle = annotationColor;
1486
+ context.lineWidth = Math.max(3, Math.min(drawWidth, drawHeight) / 150);
1487
+ context.lineCap = "round";
1488
+ context.lineJoin = "round";
1489
+ context.stroke();
1490
+ labelX = drawX + region.stroke[0].x * drawWidth;
1491
+ labelY = drawY + region.stroke[0].y * drawHeight;
1492
+ } else if ("rect" in region.anchor) {
1493
+ const rect = region.anchor.rect;
1494
+ const rx = drawX + rect.x * drawWidth;
1495
+ const ry = drawY + rect.y * drawHeight;
1496
+ const rw = rect.width * drawWidth;
1497
+ const rh = rect.height * drawHeight;
1498
+ context.fillStyle = "rgba(8,10,13,0.62)";
1499
+ context.fillRect(drawX, drawY, drawWidth, Math.max(0, ry - drawY));
1500
+ context.fillRect(drawX, ry + rh, drawWidth, Math.max(0, drawY + drawHeight - ry - rh));
1501
+ context.fillRect(drawX, ry, Math.max(0, rx - drawX), rh);
1502
+ context.fillRect(rx + rw, ry, Math.max(0, drawX + drawWidth - rx - rw), rh);
1503
+ context.strokeStyle = annotationColor;
1504
+ context.lineWidth = 3;
1505
+ context.strokeRect(rx, ry, rw, rh);
1506
+ labelX = rx;
1507
+ labelY = ry;
1508
+ } else if ("point" in region.anchor) {
1509
+ const px = drawX + region.anchor.point.x * drawWidth;
1510
+ const py = drawY + region.anchor.point.y * drawHeight;
1511
+ const radius = Math.max(6, Math.min(14, Math.min(drawWidth, drawHeight) / 12));
1512
+ context.beginPath();
1513
+ context.arc(px, py, radius, 0, Math.PI * 2);
1514
+ context.fillStyle = annotationColor;
1515
+ context.fill();
1516
+ context.strokeStyle = annotationForeground(annotationColor);
1517
+ context.lineWidth = 2;
1518
+ context.stroke();
1519
+ labelX = px + radius;
1520
+ labelY = py - radius;
1521
+ }
1522
+ }
1523
+ const ordinal = String(index + 1);
1524
+ let badgeScale = Math.min(1 / scale, drawHeight / 36);
1525
+ context.font = `bold ${String(24 * badgeScale)}px monospace`;
1526
+ const measuredWidth = context.measureText(ordinal).width + 12 * badgeScale;
1527
+ if (measuredWidth > drawWidth) badgeScale *= drawWidth / measuredWidth;
1528
+ context.font = `bold ${String(24 * badgeScale)}px monospace`;
1529
+ context.textBaseline = "top";
1530
+ const padding = 6 * badgeScale;
1531
+ const labelWidth = Math.min(drawWidth, context.measureText(ordinal).width + padding * 2);
1532
+ const labelHeight = 36 * badgeScale;
1533
+ labelX = Math.max(drawX, Math.min(labelX, drawX + drawWidth - labelWidth));
1534
+ labelY = Math.max(drawY, Math.min(labelY, drawY + drawHeight - labelHeight));
1535
+ context.fillStyle = annotationColor;
1536
+ context.fillRect(labelX, labelY, labelWidth, labelHeight);
1537
+ context.fillStyle = annotationForeground(annotationColor);
1538
+ context.fillText(ordinal, labelX + padding, labelY + padding);
1539
+ y += tileHeight;
1540
+ }
1541
+ throwIfAborted(signal);
1542
+ return boundedCanvasCapture(canvas);
1543
+ } };
1544
+ }
1545
+ //#endregion
1546
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorRegionDrafts.tsx
1547
+ function AuthorRegionDrafts({ sessionId, path, drafts }) {
1548
+ const [error, setError] = useState();
1549
+ const add = () => {
1550
+ try {
1551
+ commitAuthorRegion(sessionId, path, drafts.candidateText);
1552
+ setError(void 0);
1553
+ } catch (reason) {
1554
+ setError(reason instanceof Error ? reason.message : String(reason));
1555
+ }
1556
+ };
1557
+ if (!drafts.candidate && drafts.annotations.length === 0) return null;
1558
+ const candidateKind = drafts.candidate?.mode === "point" ? "point" : "region";
1559
+ return /* @__PURE__ */ jsxs("section", {
1560
+ className: "space-y-2",
1561
+ "data-testid": "author-region-drafts",
1562
+ children: [
1563
+ drafts.candidate ? /* @__PURE__ */ jsxs(Fragment, { children: [
1564
+ /* @__PURE__ */ jsxs("h3", {
1565
+ className: "text-base font-semibold text-doom-text",
1566
+ children: ["Current ", candidateKind]
1567
+ }),
1568
+ /* @__PURE__ */ jsxs("p", {
1569
+ className: "text-base text-doom-dim sm:text-sm",
1570
+ children: [
1571
+ "Add feedback to keep this ",
1572
+ candidateKind,
1573
+ " as an unsent annotation."
1574
+ ]
1575
+ }),
1576
+ drafts.candidate.anchor.kind === "video-time-rect" || drafts.candidate.anchor.kind === "video-time-point" ? /* @__PURE__ */ jsxs("p", {
1577
+ className: "text-base text-doom-dim sm:text-sm",
1578
+ children: [
1579
+ "Frame at ",
1580
+ drafts.candidate.anchor.timeSeconds.toFixed(3),
1581
+ "s"
1582
+ ]
1583
+ }) : null,
1584
+ /* @__PURE__ */ jsx("textarea", {
1585
+ "aria-label": "Annotation comment",
1586
+ value: drafts.candidateText,
1587
+ onChange: (event) => setAuthorCandidateText(sessionId, path, event.target.value),
1588
+ placeholder: "What should change here?",
1589
+ rows: 3,
1590
+ className: "min-h-11 w-full resize-y rounded border border-doom-border bg-doom-deep p-2 text-lg leading-normal text-doom-text focus:border-doom-red focus:outline-none sm:text-sm"
1591
+ }),
1592
+ /* @__PURE__ */ jsx(Button, {
1593
+ className: "min-h-11 min-w-11 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1594
+ variant: "outline",
1595
+ onClick: add,
1596
+ disabled: !drafts.candidate || !drafts.candidateText.trim() || drafts.annotations.length >= 16,
1597
+ children: "add annotation"
1598
+ }),
1599
+ /* @__PURE__ */ jsx(Button, {
1600
+ variant: "ghost",
1601
+ className: "min-h-11 min-w-11 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1602
+ onClick: () => {
1603
+ setAuthorRegionCandidate(sessionId, path, void 0);
1604
+ setAuthorCandidateText(sessionId, path, "");
1605
+ setError(void 0);
1606
+ },
1607
+ children: "Discard annotation"
1608
+ })
1609
+ ] }) : null,
1610
+ error ? /* @__PURE__ */ jsx("output", {
1611
+ className: "block text-base text-doom-red",
1612
+ children: error
1613
+ }) : null,
1614
+ drafts.annotations.length > 0 ? /* @__PURE__ */ jsxs("h3", {
1615
+ className: "text-base font-semibold text-doom-text",
1616
+ children: [
1617
+ "Unsent annotations (",
1618
+ drafts.annotations.length,
1619
+ ")"
1620
+ ]
1621
+ }) : null,
1622
+ /* @__PURE__ */ jsx("ol", {
1623
+ className: "space-y-2",
1624
+ children: drafts.annotations.map((region, index) => /* @__PURE__ */ jsxs("li", {
1625
+ className: "rounded border border-doom-border bg-doom-panel p-2 text-base leading-normal text-doom-text sm:text-sm",
1626
+ children: [
1627
+ /* @__PURE__ */ jsxs("span", { children: [
1628
+ "(",
1629
+ index + 1,
1630
+ ") ",
1631
+ region.mode === "point" ? "Point" : "Region",
1632
+ ": ",
1633
+ region.comment
1634
+ ] }),
1635
+ region.anchor.kind === "video-time-rect" || region.anchor.kind === "video-time-point" ? /* @__PURE__ */ jsxs(Button, {
1636
+ variant: "outline",
1637
+ className: "min-h-11 min-w-11 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1638
+ disabled: drafts.candidate !== void 0,
1639
+ onClick: () => {
1640
+ if (region.anchor.kind === "video-time-rect" || region.anchor.kind === "video-time-point") seekAuthorVideo(sessionId, path, region.anchor.timeSeconds);
1641
+ },
1642
+ children: [
1643
+ "Go to ",
1644
+ region.anchor.timeSeconds.toFixed(3),
1645
+ "s"
1646
+ ]
1647
+ }) : null,
1648
+ /* @__PURE__ */ jsx(Button, {
1649
+ className: "min-h-11 min-w-11 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1650
+ variant: "ghost",
1651
+ "aria-label": `Remove annotation ${index + 1}`,
1652
+ onClick: () => removeAuthorRegion(sessionId, path, region.id),
1653
+ children: "remove"
1654
+ })
1655
+ ]
1656
+ }, region.id))
1657
+ })
1658
+ ]
1659
+ });
1660
+ }
1661
+ //#endregion
1662
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorFeedbackControls.tsx
1663
+ function AuthorFeedbackControls({ sessionId, document, drafts, submitCapture }) {
1664
+ const [status, setStatus] = useState();
1665
+ const [capturing, setCapturing] = useState(false);
1666
+ return /* @__PURE__ */ jsxs("div", {
1667
+ className: "space-y-2",
1668
+ children: [
1669
+ /* @__PURE__ */ jsx(AuthorRegionDrafts, {
1670
+ sessionId,
1671
+ path: document.path,
1672
+ drafts
1673
+ }),
1674
+ drafts.annotations.length > 0 ? /* @__PURE__ */ jsx(Button, {
1675
+ className: "min-h-11 min-w-11 w-full text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1676
+ variant: "outline",
1677
+ "data-testid": "author-attach-capture",
1678
+ disabled: capturing || drafts.candidate !== void 0 || submitCapture === void 0,
1679
+ onClick: async () => {
1680
+ if (capturing || drafts.candidate || submitCapture === void 0) return;
1681
+ setCapturing(true);
1682
+ setStatus("Submitting annotations…");
1683
+ try {
1684
+ const packet = createAuthorCapturePacket(crypto.randomUUID(), Date.now(), document, drafts.annotations, authorCanvasAlias(sessionId, document.path));
1685
+ await submitCapture({
1686
+ ...await multiRegionCaptureProvider(drafts.annotations).capture(),
1687
+ context: authorCaptureContext(packet)
1688
+ });
1689
+ setStatus("Request submitted. You can keep annotating here.");
1690
+ } catch (reason) {
1691
+ setStatus(reason instanceof Error ? reason.message : String(reason));
1692
+ } finally {
1693
+ setCapturing(false);
1694
+ }
1695
+ },
1696
+ children: capturing ? "Submitting…" : `Submit ${drafts.annotations.length} annotation${drafts.annotations.length === 1 ? "" : "s"}`
1697
+ }) : null,
1698
+ submitCapture === void 0 ? /* @__PURE__ */ jsx("output", {
1699
+ className: "text-sm text-doom-red",
1700
+ children: "Submission unavailable. Reload to update the app."
1701
+ }) : null,
1702
+ status ? /* @__PURE__ */ jsx("output", {
1703
+ className: "block text-sm text-doom-dim",
1704
+ children: status
1705
+ }) : null
1706
+ ]
1707
+ });
1708
+ }
1709
+ //#endregion
1710
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorRequestLog.tsx
1711
+ const STATUS_LABELS = {
1712
+ REQUESTED: "Request queued",
1713
+ CHANGING: "Agent working",
1714
+ CHANGED: "Change ready",
1715
+ COMPLETE: "Request completed",
1716
+ FAILED: "Request failed",
1717
+ CANCELLED: "Request cancelled"
1718
+ };
1719
+ const STATUS_STYLES = {
1720
+ REQUESTED: "border-doom-yellow/35 bg-doom-yellow/5 text-doom-yellow",
1721
+ CHANGING: "border-doom-magenta/45 bg-doom-magenta/10 text-doom-yellow",
1722
+ CHANGED: "border-doom-cyan/40 bg-doom-cyan/5 text-doom-cyan",
1723
+ COMPLETE: "border-doom-green/40 bg-doom-green/5 text-doom-green",
1724
+ FAILED: "border-doom-red/45 bg-doom-red/10 text-doom-red",
1725
+ CANCELLED: "border-doom-border bg-doom-panel text-doom-dim"
1726
+ };
1727
+ function requestInstruction(requestText) {
1728
+ const referencedContext = requestText.search(/(?:^|\s+)Referenced context "/u);
1729
+ return requestText.slice(0, referencedContext < 0 ? void 0 : referencedContext).replace(/^Please address the attached feedback\.\s*Request ID: \S+\s*/u, "").trim();
1730
+ }
1731
+ function documentName(path) {
1732
+ return path.replaceAll("\\", "/").split("/").at(-1) || path;
1733
+ }
1734
+ function anchorLabel(anchor) {
1735
+ switch (anchor.kind) {
1736
+ case "text-range": return anchor.startLine === anchor.endLine ? `Line ${anchor.startLine}` : `Lines ${anchor.startLine}–${anchor.endLine}`;
1737
+ case "cell": return [anchor.sheet, anchor.location].filter(Boolean).join(" · ");
1738
+ case "slide-element": return `Slide ${anchor.slide} · ${anchor.location}`;
1739
+ case "image-rect": return "Image region";
1740
+ case "pdf-page-rect": return `Page ${anchor.page}`;
1741
+ case "video-time-rect": return `Video ${Math.floor(anchor.timeSeconds / 60)}:${Math.floor(anchor.timeSeconds % 60).toString().padStart(2, "0")}`;
1742
+ case "story-preview-rect": return `${anchor.preview.storyPath}#${anchor.preview.storyExport}`;
1743
+ case "image-point": return "Image point";
1744
+ case "pdf-page-point": return `Page ${anchor.page} point`;
1745
+ case "video-time-point": return `Video ${Math.floor(anchor.timeSeconds / 60)}:${Math.floor(anchor.timeSeconds % 60).toString().padStart(2, "0")} point`;
1746
+ case "story-preview-point": return `${anchor.preview.storyPath}#${anchor.preview.storyExport} point`;
1747
+ }
1748
+ }
1749
+ function progressLabel(request) {
1750
+ const total = request.regions.length;
1751
+ const pending = request.pendingRegions?.length;
1752
+ if (request.status === "CHANGING" && request.currentOperation === "Agent is working on this request") return "Processing request";
1753
+ if (request.status === "CHANGING" && pending !== void 0) return `${total - pending} of ${total} annotations applied`;
1754
+ if (request.status === "CHANGED") return "Changes applied · awaiting save";
1755
+ if (request.status === "COMPLETE") return "Agent finished processing this request";
1756
+ if (request.status === "FAILED") return "Stopped with an error";
1757
+ if (request.status === "CANCELLED") return "Stopped before completion";
1758
+ return `${total} annotation${total === 1 ? "" : "s"} queued`;
1759
+ }
1760
+ function RequestRecord({ request, prominent = false }) {
1761
+ const instruction = requestInstruction(request.requestText);
1762
+ return /* @__PURE__ */ jsxs("article", {
1763
+ "data-testid": prominent ? "author-active-request" : void 0,
1764
+ className: "space-y-3",
1765
+ children: [
1766
+ /* @__PURE__ */ jsxs("header", {
1767
+ className: "border-b border-doom-border-soft pb-3",
1768
+ children: [/* @__PURE__ */ jsx("p", {
1769
+ className: "text-lg font-semibold text-doom-hi",
1770
+ children: "Document context"
1771
+ }), /* @__PURE__ */ jsxs("p", {
1772
+ className: "mt-2 flex items-center gap-2 truncate text-base font-semibold text-doom-hi",
1773
+ children: [/* @__PURE__ */ jsx("span", {
1774
+ "aria-hidden": true,
1775
+ className: "text-doom-magenta",
1776
+ children: "↗"
1777
+ }), documentName(request.documentPath)]
1778
+ })]
1779
+ }),
1780
+ /* @__PURE__ */ jsx("section", {
1781
+ "aria-label": "Request locations",
1782
+ className: "rounded border border-doom-border bg-doom-panel p-3",
1783
+ children: /* @__PURE__ */ jsx("ol", {
1784
+ className: "space-y-3",
1785
+ children: request.regions.map((region, index) => /* @__PURE__ */ jsxs("li", {
1786
+ className: "space-y-2",
1787
+ children: [
1788
+ /* @__PURE__ */ jsxs("p", {
1789
+ className: "text-sm font-medium text-doom-yellow",
1790
+ children: [
1791
+ "Where · ",
1792
+ request.regions.length > 1 ? `${index + 1} · ` : "",
1793
+ documentName(request.documentPath),
1794
+ " / ",
1795
+ anchorLabel(region.anchor),
1796
+ " · rev ",
1797
+ request.revision
1798
+ ]
1799
+ }),
1800
+ region.quote ? /* @__PURE__ */ jsxs("blockquote", {
1801
+ className: "line-clamp-3 text-base italic leading-relaxed text-doom-text",
1802
+ children: [
1803
+ "“",
1804
+ region.quote,
1805
+ "”"
1806
+ ]
1807
+ }) : null,
1808
+ /* @__PURE__ */ jsx("p", {
1809
+ className: "text-base leading-relaxed text-doom-dim",
1810
+ children: region.comment
1811
+ })
1812
+ ]
1813
+ }, region.id))
1814
+ })
1815
+ }),
1816
+ /* @__PURE__ */ jsxs("section", {
1817
+ "data-testid": prominent ? "author-operation-card" : void 0,
1818
+ className: `rounded border p-3 ${STATUS_STYLES[request.status]}`,
1819
+ children: [
1820
+ /* @__PURE__ */ jsxs("p", {
1821
+ className: "text-sm font-semibold",
1822
+ children: [
1823
+ "●",
1824
+ " ",
1825
+ request.status === "CHANGING" ? "Working" : request.status === "COMPLETE" ? "Completed" : request.status === "FAILED" ? "Error" : STATUS_LABELS[request.status]
1826
+ ]
1827
+ }),
1828
+ /* @__PURE__ */ jsx("p", {
1829
+ className: "mt-2 text-base leading-relaxed text-doom-hi",
1830
+ children: request.currentOperation || STATUS_LABELS[request.status]
1831
+ }),
1832
+ request.error ? /* @__PURE__ */ jsx("p", {
1833
+ className: "mt-2 text-sm leading-relaxed text-doom-red",
1834
+ children: request.error
1835
+ }) : null,
1836
+ request.before !== void 0 || request.after !== void 0 ? /* @__PURE__ */ jsxs("details", {
1837
+ className: "mt-2 border-t border-current/20 pt-2 text-base text-doom-dim",
1838
+ children: [
1839
+ /* @__PURE__ */ jsx("summary", {
1840
+ className: "cursor-pointer select-none",
1841
+ children: "change preview"
1842
+ }),
1843
+ request.before !== void 0 ? /* @__PURE__ */ jsxs("pre", {
1844
+ className: "mt-2 max-h-28 overflow-auto whitespace-pre-wrap break-words font-mono",
1845
+ children: ["before: ", request.before]
1846
+ }) : null,
1847
+ request.after !== void 0 ? /* @__PURE__ */ jsxs("pre", {
1848
+ className: "mt-2 max-h-28 overflow-auto whitespace-pre-wrap break-words font-mono",
1849
+ children: ["after: ", request.after]
1850
+ }) : null
1851
+ ]
1852
+ }) : null
1853
+ ]
1854
+ }),
1855
+ /* @__PURE__ */ jsxs("section", {
1856
+ className: "space-y-2",
1857
+ children: [/* @__PURE__ */ jsx("p", {
1858
+ className: "text-lg font-semibold text-doom-hi",
1859
+ children: "Request · how to change"
1860
+ }), /* @__PURE__ */ jsx("div", {
1861
+ className: "rounded border border-doom-border bg-doom-deep p-3",
1862
+ children: /* @__PURE__ */ jsx("p", {
1863
+ className: "whitespace-pre-wrap text-base leading-relaxed text-doom-text",
1864
+ children: instruction || "No additional instruction."
1865
+ })
1866
+ })]
1867
+ }),
1868
+ /* @__PURE__ */ jsx("footer", {
1869
+ className: "text-sm text-doom-dim",
1870
+ children: progressLabel(request)
1871
+ })
1872
+ ]
1873
+ });
1874
+ }
1875
+ function AuthorRequestLog({ requests }) {
1876
+ const latest = requests.at(-1);
1877
+ const earlier = requests.slice(0, -1).reverse();
1878
+ return /* @__PURE__ */ jsxs("section", {
1879
+ "data-testid": "author-request-history",
1880
+ className: "space-y-3",
1881
+ children: [
1882
+ /* @__PURE__ */ jsx("h3", {
1883
+ className: "text-lg font-semibold text-doom-hi",
1884
+ children: "Authoring"
1885
+ }),
1886
+ latest === void 0 ? /* @__PURE__ */ jsx("p", {
1887
+ className: "text-sm leading-relaxed text-doom-dim",
1888
+ children: "No requests or changes yet."
1889
+ }) : /* @__PURE__ */ jsx(RequestRecord, {
1890
+ request: latest,
1891
+ prominent: true
1892
+ }),
1893
+ earlier.length > 0 ? /* @__PURE__ */ jsxs("details", {
1894
+ className: "border-t border-doom-border-soft pt-3",
1895
+ "data-testid": "author-earlier-requests",
1896
+ children: [/* @__PURE__ */ jsxs("summary", {
1897
+ className: "cursor-pointer select-none text-base font-medium text-doom-dim",
1898
+ children: [
1899
+ "Earlier requests (",
1900
+ earlier.length,
1901
+ ")"
1902
+ ]
1903
+ }), /* @__PURE__ */ jsx("ol", {
1904
+ className: "mt-3 space-y-5",
1905
+ children: earlier.map((request) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(RequestRecord, { request }) }, request.id))
1906
+ })]
1907
+ }) : null
1908
+ ]
1909
+ });
1910
+ }
1911
+ //#endregion
1912
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorToolPalette.tsx
1913
+ function AuthorToolPalette({ sessionId, path, kind, activeTool }) {
1914
+ const [error, setError] = useState();
1915
+ const format = (style) => {
1916
+ const selected = authorDocumentAnnotations(sessionId, path).candidate;
1917
+ const document = selected && authorDocument(sessionId, selected.documentPath);
1918
+ if (!selected || !document || selected.anchor.kind !== "text-range" || selected.revision !== document.version || selected.sourceSha256 !== document.sourceSha256) {
1919
+ setError("Select text in the document first.");
1920
+ return;
1921
+ }
1922
+ const { startOffset, endOffset } = selected.anchor;
1923
+ const content = document.content ?? "";
1924
+ const text = content.slice(startOffset, endOffset);
1925
+ const replacement = style === "Bold" ? `**${text}**` : style === "Link" ? `[${text}](https://)` : text.split("\n").map((line) => `${style === "Heading" ? "## " : "- "}${line}`).join("\n");
1926
+ reviseAuthorDocument(sessionId, document.path, content.slice(0, startOffset) + replacement + content.slice(endOffset));
1927
+ setAuthorRegionCandidate(sessionId, path, void 0);
1928
+ setError(void 0);
1929
+ };
1930
+ const tools = [
1931
+ ...[
1932
+ "image",
1933
+ "pdf",
1934
+ "video",
1935
+ "story-preview"
1936
+ ].includes(kind) ? [{
1937
+ label: "Pan",
1938
+ glyph: "✋",
1939
+ action: () => setAuthorToolMode(sessionId, path, activeTool === "pan" ? "select" : "pan"),
1940
+ active: activeTool === "pan"
1941
+ }, {
1942
+ label: "Draw",
1943
+ glyph: "✎",
1944
+ action: () => setAuthorToolMode(sessionId, path, activeTool === "draw" ? "select" : "draw"),
1945
+ active: activeTool === "draw"
1946
+ }] : [],
1947
+ {
1948
+ label: "Region",
1949
+ glyph: "⌗",
1950
+ action: () => setAuthorToolMode(sessionId, path, activeTool === "mark" ? "select" : "mark"),
1951
+ active: activeTool === "mark"
1952
+ },
1953
+ ...kind === "markdown" ? [
1954
+ {
1955
+ label: "Bold",
1956
+ glyph: "B",
1957
+ action: () => format("Bold")
1958
+ },
1959
+ {
1960
+ label: "Heading",
1961
+ glyph: "H",
1962
+ action: () => format("Heading")
1963
+ },
1964
+ {
1965
+ label: "Link",
1966
+ glyph: "↗",
1967
+ action: () => format("Link")
1968
+ },
1969
+ {
1970
+ label: "List",
1971
+ glyph: "≡",
1972
+ action: () => format("List")
1973
+ }
1974
+ ] : [],
1975
+ {
1976
+ label: "Comment",
1977
+ glyph: "●",
1978
+ action: () => setAuthorToolMode(sessionId, path, activeTool === "comment" ? "select" : "comment"),
1979
+ active: activeTool === "comment"
1980
+ }
1981
+ ];
1982
+ return /* @__PURE__ */ jsxs("section", {
1983
+ "data-testid": "author-tool-palette",
1984
+ className: "space-y-2 border-b border-doom-border-soft pb-3",
1985
+ children: [
1986
+ /* @__PURE__ */ jsxs("div", {
1987
+ className: "flex justify-between text-sm text-doom-dim",
1988
+ children: [/* @__PURE__ */ jsx("h3", {
1989
+ className: "text-lg font-semibold text-doom-hi",
1990
+ children: "Tools"
1991
+ }), /* @__PURE__ */ jsx("span", { children: "focused tab" })]
1992
+ }),
1993
+ /* @__PURE__ */ jsx("div", {
1994
+ className: "grid grid-cols-3 gap-1.5",
1995
+ children: tools.map((tool) => /* @__PURE__ */ jsxs(Button, {
1996
+ size: "md",
1997
+ variant: "outline",
1998
+ "aria-label": tool.label === "Region" ? "mark region" : tool.label,
1999
+ "aria-pressed": tool.active,
2000
+ onClick: tool.action,
2001
+ className: `min-h-11 min-w-11 gap-1.5 rounded border text-base [@media(pointer:fine)]:min-h-8 ${tool.active ? "border-doom-red bg-doom-red/10 text-doom-hi hover:bg-doom-red/15" : "border-doom-border bg-doom-panel text-doom-dim"}`,
2002
+ children: [/* @__PURE__ */ jsx("span", {
2003
+ "aria-hidden": "true",
2004
+ children: tool.glyph
2005
+ }), tool.label]
2006
+ }, tool.label))
2007
+ }),
2008
+ /* @__PURE__ */ jsx("p", {
2009
+ className: "text-base leading-normal text-doom-dim",
2010
+ children: activeTool === "draw" ? "Draw a location mark, then enter feedback text. Marks never edit the source." : activeTool === "pan" ? "Drag to pan; pinch to zoom. Use Fit to reset the view." : activeTool === "mark" ? "Drag over the document to mark a region." : activeTool === "comment" ? "Click the document to place a comment pin." : "Choose Region to drag an area, or Comment to place a point."
2011
+ }),
2012
+ error ? /* @__PURE__ */ jsx("output", {
2013
+ className: "block text-base text-doom-red",
2014
+ children: error
2015
+ }) : null
2016
+ ]
2017
+ });
2018
+ }
2019
+ //#endregion
2020
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorPanel.tsx
2021
+ function AuthorPanel({ sessionId, activeMinorModes, submitCapture, statuses, renderSlot, renderSessionActivity }) {
2022
+ const documents = useStore(authorWorkspace.store, (state) => {
2023
+ if (sessionId === null) return [];
2024
+ const prefix = `${sessionId}\n`;
2025
+ return Object.entries(state.documents).filter(([key]) => key.startsWith(prefix)).map(([, document]) => document);
2026
+ });
2027
+ const workspace = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
2028
+ const grid = useStore(authorGrid.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
2029
+ const focused = documents.find((document) => document.path === workspace?.focusedDocument?.path);
2030
+ const focusedPath = workspace?.focusedDocument?.path;
2031
+ const requests = (workspace?.requests ?? []).filter((request) => focusedPath === void 0 || request.documentPath === focusedPath);
2032
+ const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, focusedPath ?? "", state));
2033
+ const activity = sessionId !== null && focusedPath !== void 0 && requests.some((request) => request.status === "REQUESTED" || request.status === "CHANGING") ? renderSessionActivity?.() ?? null : null;
2034
+ if (!activeMinorModes?.includes("author")) return null;
2035
+ return /* @__PURE__ */ jsxs("section", {
2036
+ "data-testid": "author-panel",
2037
+ className: "flex min-h-0 flex-1 flex-col overflow-hidden",
2038
+ children: [/* @__PURE__ */ jsx("div", {
2039
+ className: "min-h-0 flex-1 overflow-y-auto p-3",
2040
+ children: /* @__PURE__ */ jsxs("div", {
2041
+ className: "flex flex-col gap-2",
2042
+ children: [
2043
+ /* @__PURE__ */ jsx("div", {
2044
+ "data-testid": "author-preview-providers",
2045
+ children: renderSlot("author.preview-provider")
2046
+ }),
2047
+ sessionId !== null && workspace !== void 0 && focused !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
2048
+ /* @__PURE__ */ jsx("h2", {
2049
+ className: "text-base font-semibold text-doom-text",
2050
+ children: "Annotations"
2051
+ }),
2052
+ /* @__PURE__ */ jsx("p", {
2053
+ className: "text-base leading-normal text-doom-dim sm:text-sm",
2054
+ children: "Drag a region or place a point, add feedback, then submit here. Requests queue while the agent is working."
2055
+ }),
2056
+ focused.kind === "video" ? /* @__PURE__ */ jsx("p", {
2057
+ className: "text-base text-doom-dim sm:text-sm",
2058
+ children: "Annotations reference video frames. They do not save changes to the source video."
2059
+ }) : null,
2060
+ /* @__PURE__ */ jsx(AuthorToolPalette, {
2061
+ sessionId,
2062
+ path: focused.path,
2063
+ kind: focused.kind,
2064
+ activeTool: workspace.toolsByDocument[normalizeAuthorPath(focused.path)] ?? "select"
2065
+ }),
2066
+ autonomousVoiceGridVisible(statuses) && grid !== void 0 ? /* @__PURE__ */ jsxs("div", {
2067
+ "data-testid": "author-grid-snapshot",
2068
+ className: "rounded border border-doom-red/40 bg-doom-red/5 p-2 text-base text-doom-dim",
2069
+ children: [/* @__PURE__ */ jsx("strong", {
2070
+ className: "text-doom-red",
2071
+ children: "VOICE GRID A1–H8"
2072
+ }), /* @__PURE__ */ jsxs("p", {
2073
+ className: "mt-1 truncate",
2074
+ children: ["token ", grid.geometryToken]
2075
+ })]
2076
+ }) : null,
2077
+ /* @__PURE__ */ jsx("div", {
2078
+ className: "hidden sm:block",
2079
+ children: /* @__PURE__ */ jsx(AuthorFeedbackControls, {
2080
+ sessionId,
2081
+ document: focused,
2082
+ drafts,
2083
+ submitCapture
2084
+ }, focused.path)
2085
+ })
2086
+ ] }) : null,
2087
+ /* @__PURE__ */ jsx(AuthorRequestLog, { requests })
2088
+ ]
2089
+ })
2090
+ }), activity === null ? null : /* @__PURE__ */ jsx("footer", {
2091
+ "data-testid": "author-session-activity",
2092
+ className: "shrink-0 border-t border-doom-border bg-doom-rail p-3",
2093
+ children: activity
2094
+ })]
2095
+ });
2096
+ }
2097
+ //#endregion
2098
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/authoring.web.tsx
2099
+ var authoring_web_default = defineDockFace({
2100
+ id: "authoring",
2101
+ label: "authoring",
2102
+ order: 30,
2103
+ autoSelect: true,
2104
+ panel: AuthorPanel,
2105
+ requiredMinorMode: "author",
2106
+ visibility: {
2107
+ subscribe() {
2108
+ return () => void 0;
2109
+ },
2110
+ isVisible(sessionId) {
2111
+ return sessionId !== null;
2112
+ }
2113
+ }
2114
+ });
2115
+ //#endregion
2116
+ //#region src/types/apiRoutes.ts
2117
+ /**
2118
+ * This package's routes, as data.
2119
+ *
2120
+ * No scope and no base path. The build reads both off
2121
+ * `src/extensions/workspaces/sessions/(backend)/api/author/`, so the mount is
2122
+ * stated once, by the folder that creates it. The browser used to restate it:
2123
+ * `authorStateUrl` built one URL from `API_BASE_PATH` and the document helper
2124
+ * two files away spelled `author` out again as a literal, so the two could
2125
+ * disagree and nothing would say so.
2126
+ *
2127
+ * The order below is the order the three Hono apps register in: `/state` in
2128
+ * `authorApi`, the eight bridge routes in `authorBridgeApi`, the three document
2129
+ * routes in `authorDocumentApi`. A reviewer can read the two side by side.
2130
+ *
2131
+ * Response types are named only where a caller reads the body. The bridge
2132
+ * routes are called by the hub's author channel over `requestSessionApi`, which
2133
+ * has its own message vocabulary and never sees this client.
2134
+ */
2135
+ var apiRoutes_default = defineApiRoutes({
2136
+ state: {
2137
+ method: "GET",
2138
+ path: "/state",
2139
+ response: apiResponse()
2140
+ },
2141
+ bridgeRegister: {
2142
+ method: "POST",
2143
+ path: "/bridge/register"
2144
+ },
2145
+ bridgeCatalog: {
2146
+ method: "POST",
2147
+ path: "/bridge/catalog"
2148
+ },
2149
+ /** Long-polls for the next capability request, so it settles when one arrives. */
2150
+ bridgeNext: {
2151
+ method: "POST",
2152
+ path: "/bridge/next"
2153
+ },
2154
+ bridgeResult: {
2155
+ method: "POST",
2156
+ path: "/bridge/result"
2157
+ },
2158
+ bridgeCancelled: {
2159
+ method: "POST",
2160
+ path: "/bridge/cancelled"
2161
+ },
2162
+ bridgeDisconnect: {
2163
+ method: "POST",
2164
+ path: "/bridge/disconnect"
2165
+ },
2166
+ bridgeClose: {
2167
+ method: "POST",
2168
+ path: "/bridge/close"
2169
+ },
2170
+ bridgeDescribe: {
2171
+ method: "GET",
2172
+ path: "/bridge/describe",
2173
+ query: ["alias"]
2174
+ },
2175
+ bridgeInvoke: {
2176
+ method: "POST",
2177
+ path: "/bridge/invoke"
2178
+ },
2179
+ /**
2180
+ * Parses one structured document.
2181
+ *
2182
+ * Answers a byte-length summary instead when the request names no format,
2183
+ * which is the agent-side tool path; the page always names one.
2184
+ */
2185
+ documentsOpen: {
2186
+ method: "POST",
2187
+ path: "/documents/open",
2188
+ query: ["session"],
2189
+ response: apiResponse()
2190
+ },
2191
+ documentsPreflight: {
2192
+ method: "POST",
2193
+ path: "/documents/preflight",
2194
+ query: ["session"],
2195
+ response: apiResponse()
2196
+ },
2197
+ documentsSerialize: {
2198
+ method: "POST",
2199
+ path: "/documents/serialize",
2200
+ query: ["session"],
2201
+ response: apiResponse()
2202
+ },
2203
+ /**
2204
+ * The host's own bytes route, not this package's.
2205
+ *
2206
+ * `host: true` hangs it off the session root rather than under `/plugins`,
2207
+ * which is how the cockpit already serves it. Author reads a document's
2208
+ * bytes and its digest through it, and writes the edited bytes back to the
2209
+ * same path, so both stay raw transfers rather than calls: the body is the
2210
+ * file and the digest travels in a header.
2211
+ */
2212
+ file: {
2213
+ method: "GET",
2214
+ path: "/file",
2215
+ query: ["path"],
2216
+ host: true
2217
+ }
2218
+ });
2219
+ const api = createApiClient(apiRoutes_default, {
2220
+ scopes: ["session"],
2221
+ basePath: "author",
2222
+ transport: (input, init) => sealedTransport.fetch(input, init),
2223
+ sessionAddress: sessionApiAddress
2224
+ });
2225
+ //#endregion
2226
+ //#region src/types/authorPreview.ts
2227
+ function parseAuthorOpenSourceAction(input) {
2228
+ if (typeof input !== "object" || input === null) return null;
2229
+ const candidate = input;
2230
+ return candidate.version === 1 && typeof candidate.createTab === "function" ? candidate : null;
2231
+ }
2232
+ function parseAuthorPreviewAction(input) {
2233
+ if (typeof input !== "object" || input === null) return null;
2234
+ const candidate = input;
2235
+ if (candidate.version !== 1 || typeof candidate.label !== "string" || candidate.label.trim() === "") return null;
2236
+ if (candidate.detail !== void 0 && typeof candidate.detail !== "string") return null;
2237
+ if (typeof candidate.createTab !== "function") return null;
2238
+ if (candidate.supportsSource !== void 0 && typeof candidate.supportsSource !== "function") return null;
2239
+ if (candidate.embeddedPanel !== void 0 && typeof candidate.embeddedPanel !== "function") return null;
2240
+ return candidate;
2241
+ }
2242
+ const authorPreviewActionSlot = {
2243
+ slot: "author.preview-action",
2244
+ parse: parseAuthorPreviewAction
2245
+ };
2246
+ //#endregion
2247
+ //#region src/types/authorApi.ts
2248
+ /** Query parameter naming the session a document request belongs to. */
2249
+ const SESSION_QUERY_PARAM = "session";
2250
+ //#endregion
2251
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorFiles.ts
2252
+ const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
2253
+ "avif",
2254
+ "bmp",
2255
+ "gif",
2256
+ "jpeg",
2257
+ "jpg",
2258
+ "png",
2259
+ "svg",
2260
+ "webp"
2261
+ ]);
2262
+ const VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
2263
+ "m4v",
2264
+ "mov",
2265
+ "mp4",
2266
+ "webm"
2267
+ ]);
2268
+ function structuredFormat(path) {
2269
+ const lower = path.toLowerCase();
2270
+ if (lower.endsWith(".slides.md")) return "markdown-slides";
2271
+ if (lower.endsWith(".csv")) return "csv";
2272
+ if (lower.endsWith(".pptx")) return "pptx";
2273
+ if (lower.endsWith(".xlsx")) return "xlsx";
2274
+ }
2275
+ function authorKindForPath(path) {
2276
+ const lower = path.toLowerCase();
2277
+ if (/\.stories\.(?:js|jsx|ts|tsx)$/u.test(lower)) return "story-preview";
2278
+ const format = structuredFormat(path);
2279
+ if (format === "markdown-slides") return "slides";
2280
+ if (format !== void 0) return format;
2281
+ const extension = lower.split(".").at(-1) ?? "";
2282
+ if (extension === "md") return "markdown";
2283
+ if (IMAGE_EXTENSIONS.has(extension)) return "image";
2284
+ if (VIDEO_EXTENSIONS.has(extension)) return "video";
2285
+ if (extension === "pdf") return "pdf";
2286
+ if ([
2287
+ "txt",
2288
+ "json",
2289
+ "yaml",
2290
+ "yml",
2291
+ "xml",
2292
+ "html",
2293
+ "css",
2294
+ "js",
2295
+ "ts",
2296
+ "tsx",
2297
+ "jsx"
2298
+ ].includes(extension)) return "text";
2299
+ return "opaque";
2300
+ }
2301
+ /**
2302
+ * Where a document's bytes are read from and written back to.
2303
+ *
2304
+ * This is the host's route, not this package's, which is why the route table
2305
+ * marks it `host: true` and it lands beside the session rather than under
2306
+ * `/plugins`. It stays a raw transfer rather than a call: the body is the file
2307
+ * itself, and the digest travels in a header on both legs.
2308
+ */
2309
+ function authorSessionFileUrl(sessionId, path) {
2310
+ return api.session(sessionId).file.url({ query: { path } });
2311
+ }
2312
+ /** One document route, with the refusal the reader is shown: the route's own words, or its status. */
2313
+ async function documentRequest(method, sessionId, body, signal) {
2314
+ const result = await method({
2315
+ body,
2316
+ query: { [SESSION_QUERY_PARAM]: sessionId },
2317
+ ...signal === void 0 ? {} : { signal }
2318
+ });
2319
+ if (!result.ok) throw new Error(result.error === "" ? `Author document API failed (${result.status})` : result.error);
2320
+ return result.data;
2321
+ }
2322
+ async function loadAuthorDocument(sessionId, path, signal) {
2323
+ const kind = authorKindForPath(path);
2324
+ const url = authorSessionFileUrl(sessionId, path);
2325
+ const response = await sealedTransport.fetch(url, { signal });
2326
+ if (!response.ok) throw new Error(`Author could not open ${path} (${response.status})`);
2327
+ const sourceSha256 = response.headers.get("X-File-SHA256") ?? void 0;
2328
+ const format = structuredFormat(path);
2329
+ if (format !== void 0) {
2330
+ const parsed = await documentRequest(api.session(sessionId).documentsOpen, sessionId, {
2331
+ path,
2332
+ format
2333
+ }, signal);
2334
+ if (!("fragments" in parsed) || !Array.isArray(parsed.fragments)) throw new Error("Author returned an invalid structured document.");
2335
+ const parsedDigest = parsed.manifest?.sourceDigest;
2336
+ if (sourceSha256 !== void 0 && parsedDigest !== void 0 && sourceSha256 !== parsedDigest) throw new Error("Document source changed while opening. Reopen the document.");
2337
+ const csvDialect = format === "csv" ? csvDialectFromMetadata(parsed.manifest?.metadata) : void 0;
2338
+ return {
2339
+ path,
2340
+ kind,
2341
+ title: path.split("/").at(-1) ?? path,
2342
+ structuredFormat: format,
2343
+ fragments: parsed.fragments,
2344
+ ...csvDialect === void 0 ? {} : { csvDialect },
2345
+ originalFragments: parsed.fragments.map((fragment) => ({ ...fragment })),
2346
+ ...sourceSha256 === void 0 ? {} : { sourceSha256 }
2347
+ };
2348
+ }
2349
+ const mediaUrl = kind === "image" || kind === "video" || kind === "pdf" || kind === "opaque" ? url : void 0;
2350
+ const content = kind === "text" || kind === "markdown" || kind === "story-preview" ? await response.text() : void 0;
2351
+ return {
2352
+ path,
2353
+ kind,
2354
+ title: path.split("/").at(-1) ?? path,
2355
+ ...content === void 0 ? {} : { content },
2356
+ ...mediaUrl === void 0 ? {} : { mediaUrl },
2357
+ ...sourceSha256 === void 0 ? {} : { sourceSha256 }
2358
+ };
2359
+ }
2360
+ function operationsFor(document) {
2361
+ const originals = new Map((document.originalFragments ?? []).map((fragment) => [fragment.id, fragment.text]));
2362
+ return (document.fragments ?? []).filter((fragment) => !fragment.readOnly && originals.get(fragment.id) !== fragment.text).map((fragment) => ({
2363
+ fragmentId: fragment.id,
2364
+ replacement: fragment.text
2365
+ }));
2366
+ }
2367
+ function decodeBase64(value) {
2368
+ if (!/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/.test(value)) throw new Error("Invalid serialized document bytes.");
2369
+ const binary = atob(value);
2370
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
2371
+ }
2372
+ function imageMimeType(path) {
2373
+ const extension = path.split(".").at(-1)?.toLowerCase();
2374
+ if (extension === "png") return "image/png";
2375
+ if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
2376
+ if (extension === "webp") return "image/webp";
2377
+ }
2378
+ async function croppedImageBytes(documentInput, signal) {
2379
+ const { crop, mediaUrl } = documentInput;
2380
+ if (crop === void 0 || mediaUrl === void 0) throw new Error("This Author image has no crop source.");
2381
+ const mimeType = imageMimeType(documentInput.path);
2382
+ if (mimeType === void 0) throw new Error("This image format cannot preserve its encoding after crop.");
2383
+ const response = await sealedTransport.fetch(mediaUrl, {
2384
+ signal,
2385
+ cache: "no-store"
2386
+ });
2387
+ if (!response.ok) throw new Error(`Author could not read the image before saving (${response.status})`);
2388
+ const bitmap = await createImageBitmap(await response.blob());
2389
+ try {
2390
+ const sourceX = Math.round(crop.x * bitmap.width);
2391
+ const sourceY = Math.round(crop.y * bitmap.height);
2392
+ const sourceWidth = Math.max(1, Math.round(crop.width * bitmap.width));
2393
+ const sourceHeight = Math.max(1, Math.round(crop.height * bitmap.height));
2394
+ const canvas = window.document.createElement("canvas");
2395
+ canvas.width = sourceWidth;
2396
+ canvas.height = sourceHeight;
2397
+ const context = canvas.getContext("2d");
2398
+ if (context === null) throw new Error("Canvas is unavailable for image crop.");
2399
+ context.drawImage(bitmap, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, sourceWidth, sourceHeight);
2400
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, mimeType));
2401
+ if (blob === null || blob.type !== mimeType) throw new Error("The browser cannot encode this cropped image format.");
2402
+ return blob;
2403
+ } finally {
2404
+ bitmap.close();
2405
+ }
2406
+ }
2407
+ async function bytesToSave(sessionId, document, signal) {
2408
+ if (document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview") return document.content ?? "";
2409
+ if (document.kind === "image" && document.crop !== void 0) return await croppedImageBytes(document, signal);
2410
+ if (document.structuredFormat === void 0) throw new Error("This Author view cannot be saved.");
2411
+ const operations = operationsFor(document);
2412
+ const request = {
2413
+ path: document.path,
2414
+ format: document.structuredFormat,
2415
+ operations,
2416
+ ...document.csvDialect === void 0 ? {} : { csvDialect: document.csvDialect }
2417
+ };
2418
+ const preflight = await documentRequest(api.session(sessionId).documentsPreflight, sessionId, request, signal);
2419
+ if (preflight.sourceDigest !== void 0 && preflight.sourceDigest !== document.sourceSha256) throw new Error("Document source changed before saving. Reopen the document.");
2420
+ if (!preflight.accepted) throw new Error(preflight.issues.map((issue) => issue.message).join("\n") || "Author preflight rejected the edit.");
2421
+ const serialized = await documentRequest(api.session(sessionId).documentsSerialize, sessionId, {
2422
+ ...request,
2423
+ preflightDigest: preflight.digest
2424
+ }, signal);
2425
+ if (serialized.encoding !== "base64") throw new Error("Author serialize returned an unsupported encoding.");
2426
+ const bytes = decodeBase64(serialized.bytes);
2427
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2428
+ }
2429
+ async function saveAuthorDocument(sessionId, document, signal) {
2430
+ if (document.sourceSha256 === void 0) throw new Error("This Author view has no source digest.");
2431
+ const response = await sealedTransport.fetch(authorSessionFileUrl(sessionId, document.path), {
2432
+ method: "PUT",
2433
+ headers: {
2434
+ "Content-Type": "application/octet-stream",
2435
+ "X-Expected-SHA256": document.sourceSha256
2436
+ },
2437
+ body: await bytesToSave(sessionId, document, signal),
2438
+ signal
2439
+ });
2440
+ if (!response.ok) throw new Error(`Author save failed (${response.status})`);
2441
+ const sha256 = response.headers.get("X-File-SHA256");
2442
+ if (sha256 === null || !/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Author save returned an invalid digest.");
2443
+ return sha256;
2444
+ }
2445
+ function csvDialectFromMetadata(metadata) {
2446
+ if (metadata === void 0) return void 0;
2447
+ const { delimiter, quote, recordDelimiter } = metadata;
2448
+ if (delimiter !== "," && delimiter !== ";" && delimiter !== " " && delimiter !== "|" || quote !== "\"" && quote !== "'" || recordDelimiter !== "\n" && recordDelimiter !== "\r\n") throw new Error("Author document API returned an invalid CSV dialect.");
2449
+ return {
2450
+ delimiter,
2451
+ quote,
2452
+ recordDelimiter
2453
+ };
2454
+ }
2455
+ //#endregion
2456
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorGridTools.ts
2457
+ const stringProperty = {
2458
+ type: "string",
2459
+ minLength: 1
2460
+ };
2461
+ function inputRecord(input) {
2462
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("Expected an object.");
2463
+ return input;
2464
+ }
2465
+ function requiredString(value, name) {
2466
+ if (typeof value[name] !== "string" || value[name] === "") throw new Error(`${name} must be a non-empty string.`);
2467
+ return value[name];
2468
+ }
2469
+ function resolvedAnchor(sessionId, path, kind, cell) {
2470
+ if (authorDocument(sessionId, path) === void 0) throw new Error("STALE_DOCUMENT: The focused Author document is no longer open.");
2471
+ if (kind === "pdf" || kind === "video" || kind === "opaque") throw new Error("UNSUPPORTED_GRID: PDF and video coordinates are capture-only and require a visible native selection.");
2472
+ return resolveAuthorGridNativeAnchor(sessionId, cell);
2473
+ }
2474
+ function checkedRegion(sessionId, path, value) {
2475
+ const document = authorDocument(sessionId, path);
2476
+ if (document === void 0) throw new Error("STALE_DOCUMENT: The Author document is no longer open.");
2477
+ if (document.savingVersion !== void 0) throw new Error("SAVE_IN_PROGRESS: Wait for the current Author save to finish.");
2478
+ const expectedRevision = value.expectedRevision;
2479
+ const expectedSourceSha256 = requiredString(value, "expectedSourceSha256");
2480
+ if (expectedRevision !== document.version || expectedSourceSha256 !== document.sourceSha256) throw new Error("STALE_DOCUMENT: The Author revision or source digest changed.");
2481
+ const regionId = requiredString(value, "regionId");
2482
+ const workspace = authorSessionWorkspace(sessionId);
2483
+ const region = authorDocumentAnnotations(sessionId, path)?.annotations.find((candidate) => candidate.id === regionId) ?? workspace.requests.filter((request) => request.documentPath === path && (request.status === "REQUESTED" || request.status === "CHANGING")).flatMap((request) => request.pendingRegions ?? request.regions).find((candidate) => candidate.id === regionId);
2484
+ if (region === void 0 || region.documentPath !== path) throw new Error("STALE_REGION: The resolved Author region is unavailable.");
2485
+ if (region.stroke !== void 0) throw new Error("UNSUPPORTED_REGION: Freehand feedback cannot authorize a document mutation.");
2486
+ if (region.revision !== document.version || region.sourceSha256 !== document.sourceSha256) throw new Error("STALE_REGION: The resolved Author region no longer matches the document.");
2487
+ return {
2488
+ document,
2489
+ region
2490
+ };
2491
+ }
2492
+ function gridTools(sessionId, path, kind) {
2493
+ return [{
2494
+ name: "author_describe_grid",
2495
+ label: "Describe grid",
2496
+ description: "Describe the current A1 through H8 grid and return its geometry token.",
2497
+ inputSchema: {
2498
+ type: "object",
2499
+ additionalProperties: false,
2500
+ properties: {}
2501
+ },
2502
+ execute: async () => {
2503
+ const geometry = authorGridGeometry(sessionId);
2504
+ const document = authorDocument(sessionId, path);
2505
+ if (geometry === void 0 || document === void 0 || geometry.documentPath !== path || geometry.revision !== document.version) throw new Error("STALE_GRID: The visible Author grid is unavailable.");
2506
+ return {
2507
+ columns: "A-H",
2508
+ rows: "1-8",
2509
+ ...geometry
2510
+ };
2511
+ }
2512
+ }, {
2513
+ name: "author_resolve_grid_cell",
2514
+ label: "Resolve grid cell",
2515
+ description: "Resolve one current grid cell to a native document anchor before mutation.",
2516
+ inputSchema: {
2517
+ type: "object",
2518
+ additionalProperties: false,
2519
+ required: [
2520
+ "cell",
2521
+ "geometryToken",
2522
+ "instruction"
2523
+ ],
2524
+ properties: {
2525
+ cell: stringProperty,
2526
+ geometryToken: stringProperty,
2527
+ instruction: stringProperty
2528
+ }
2529
+ },
2530
+ execute: async (input) => {
2531
+ const value = inputRecord(input);
2532
+ const instruction = requiredString(value, "instruction");
2533
+ const cell = resolveAuthorGridCell(sessionId, requiredString(value, "cell"), requiredString(value, "geometryToken"));
2534
+ const geometry = authorGridGeometry(sessionId);
2535
+ const resolved = resolvedAnchor(sessionId, path, kind, cell);
2536
+ const regionId = crypto.randomUUID();
2537
+ const createdAt = Date.now();
2538
+ const region = {
2539
+ id: regionId,
2540
+ documentPath: path,
2541
+ revision: geometry.revision,
2542
+ sourceSha256: geometry.sourceSha256,
2543
+ comment: instruction,
2544
+ ...resolved,
2545
+ viewport: { ...geometry.viewport },
2546
+ voiceGrid: cell.evidence,
2547
+ createdAt
2548
+ };
2549
+ addAuthorRegion(sessionId, region);
2550
+ putAuthorRequest(sessionId, {
2551
+ id: `voice:${regionId}`,
2552
+ documentPath: path,
2553
+ requestText: instruction,
2554
+ regions: [region],
2555
+ pendingRegions: [region],
2556
+ status: "REQUESTED",
2557
+ createdAt,
2558
+ updatedAt: createdAt,
2559
+ revision: geometry.revision,
2560
+ sourceSha256: geometry.sourceSha256
2561
+ });
2562
+ return {
2563
+ regionId,
2564
+ anchor: resolved.anchor,
2565
+ quote: resolved.quote,
2566
+ evidence: cell.evidence
2567
+ };
2568
+ }
2569
+ }];
2570
+ }
2571
+ function assertRebasable(pending, changed) {
2572
+ const remaining = pending.filter((region) => region.id !== changed.id);
2573
+ if (changed.anchor.kind === "image-rect" && remaining.length > 0) throw new Error("UNSUPPORTED_REGION: A multi-region image request cannot apply more than one crop.");
2574
+ if (changed.anchor.kind !== "text-range") return;
2575
+ for (const region of remaining) {
2576
+ if (region.anchor.kind !== "text-range") continue;
2577
+ if (!(region.anchor.endOffset <= changed.anchor.startOffset || region.anchor.startOffset >= changed.anchor.endOffset)) throw new Error("AMBIGUOUS_REGION: Submitted text regions overlap and cannot be safely rebased.");
2578
+ }
2579
+ }
2580
+ function lineAtOffset(content, offset) {
2581
+ return content.slice(0, Math.max(0, offset)).split("\n").length;
2582
+ }
2583
+ function rebasePendingRegions(pending, changed, result, nextContent) {
2584
+ return pending.filter((region) => region.id !== changed.id).map((region) => {
2585
+ if (changed.anchor.kind !== "text-range" || region.anchor.kind !== "text-range" || nextContent === void 0) return {
2586
+ ...region,
2587
+ revision: result.revision
2588
+ };
2589
+ const delta = (result.after?.length ?? 0) - (result.before?.length ?? 0);
2590
+ const shift = region.anchor.startOffset >= changed.anchor.endOffset ? delta : 0;
2591
+ const startOffset = region.anchor.startOffset + shift;
2592
+ const endOffset = region.anchor.endOffset + shift;
2593
+ return {
2594
+ ...region,
2595
+ revision: result.revision,
2596
+ anchor: {
2597
+ ...region.anchor,
2598
+ startOffset,
2599
+ endOffset,
2600
+ startLine: lineAtOffset(nextContent, startOffset),
2601
+ endLine: lineAtOffset(nextContent, endOffset)
2602
+ }
2603
+ };
2604
+ });
2605
+ }
2606
+ function mutationTool(sessionId, path, kind) {
2607
+ if (kind === "pdf" || kind === "video" || kind === "opaque") return void 0;
2608
+ const needsReplacement = kind !== "image";
2609
+ return {
2610
+ name: "author_apply_region",
2611
+ label: "Apply region change",
2612
+ description: "Apply a fenced mutation through a previously resolved native Author region.",
2613
+ inputSchema: {
2614
+ type: "object",
2615
+ additionalProperties: false,
2616
+ required: [
2617
+ "regionId",
2618
+ "expectedRevision",
2619
+ "expectedSourceSha256",
2620
+ ...needsReplacement ? ["replacement"] : []
2621
+ ],
2622
+ properties: {
2623
+ regionId: stringProperty,
2624
+ expectedRevision: {
2625
+ type: "integer",
2626
+ minimum: 0
2627
+ },
2628
+ expectedSourceSha256: stringProperty,
2629
+ ...needsReplacement ? { replacement: { type: "string" } } : {}
2630
+ }
2631
+ },
2632
+ execute: async (input, signal) => {
2633
+ const value = inputRecord(input);
2634
+ const regionId = requiredString(value, "regionId");
2635
+ const request = authorSessionWorkspace(sessionId).requests.find((candidate) => candidate.documentPath === path && candidate.status === "REQUESTED" && (candidate.pendingRegions ?? candidate.regions).some((item) => item.id === regionId));
2636
+ if (request !== void 0) updateAuthorRequest(sessionId, request.id, (current) => ({
2637
+ ...current,
2638
+ status: "CHANGING",
2639
+ currentOperation: "validating native anchor",
2640
+ updatedAt: Date.now()
2641
+ }));
2642
+ try {
2643
+ if (signal.aborted) throw signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Author mutation cancelled.");
2644
+ const { document, region } = checkedRegion(sessionId, path, value);
2645
+ const pending = request?.pendingRegions ?? request?.regions ?? [region];
2646
+ assertRebasable(pending, region);
2647
+ if (request !== void 0) updateAuthorRequest(sessionId, request.id, (current) => ({
2648
+ ...current,
2649
+ currentOperation: `mutating ${region.anchor.kind}`,
2650
+ updatedAt: Date.now()
2651
+ }));
2652
+ let result;
2653
+ let nextContent;
2654
+ if (region.anchor.kind === "text-range") {
2655
+ const replacement = typeof value.replacement === "string" ? value.replacement : "";
2656
+ const { startOffset, endOffset } = region.anchor;
2657
+ const content = document.content ?? "";
2658
+ const before = content.slice(startOffset, endOffset);
2659
+ nextContent = content.slice(0, startOffset) + replacement + content.slice(endOffset);
2660
+ reviseAuthorDocument(sessionId, path, nextContent);
2661
+ result = {
2662
+ changed: true,
2663
+ before,
2664
+ after: replacement,
2665
+ revision: document.version + 1
2666
+ };
2667
+ } else if (region.anchor.kind === "cell" || region.anchor.kind === "slide-element") {
2668
+ const fragmentId = region.anchor.fragmentId;
2669
+ const fragment = document.fragments?.find((candidate) => candidate.id === fragmentId);
2670
+ if (fragment === void 0 || fragment.readOnly === true) throw new Error("UNSUPPORTED_REGION: The native fragment is not editable.");
2671
+ const replacement = typeof value.replacement === "string" ? value.replacement : "";
2672
+ reviseAuthorFragment(sessionId, path, fragment.id, replacement);
2673
+ result = {
2674
+ changed: true,
2675
+ before: fragment.text,
2676
+ after: replacement,
2677
+ revision: document.version + 1
2678
+ };
2679
+ } else if (region.anchor.kind === "image-rect") {
2680
+ setAuthorCrop(sessionId, path, region.anchor.rect);
2681
+ result = {
2682
+ changed: true,
2683
+ crop: region.anchor.rect,
2684
+ revision: document.version + 1
2685
+ };
2686
+ } else throw new Error("UNSUPPORTED_REGION: PDF and video regions are capture-only.");
2687
+ if (request !== void 0) {
2688
+ const pendingRegions = rebasePendingRegions(pending, region, result, nextContent);
2689
+ updateAuthorRequest(sessionId, request.id, (current) => ({
2690
+ ...current,
2691
+ status: pendingRegions.length === 0 ? "CHANGED" : "REQUESTED",
2692
+ currentOperation: void 0,
2693
+ before: [current.before, result.before].filter((item) => item !== void 0).join("\n") || void 0,
2694
+ after: [current.after, result.after ?? (result.crop === void 0 ? void 0 : JSON.stringify(result.crop))].filter((item) => item !== void 0).join("\n") || void 0,
2695
+ pendingRegions,
2696
+ revision: result.revision,
2697
+ updatedAt: Date.now()
2698
+ }));
2699
+ }
2700
+ return result;
2701
+ } catch (error) {
2702
+ if (request !== void 0) updateAuthorRequest(sessionId, request.id, (current) => ({
2703
+ ...current,
2704
+ status: signal.aborted ? "CANCELLED" : "FAILED",
2705
+ currentOperation: void 0,
2706
+ error: signal.aborted ? "Author mutation cancelled." : error instanceof Error ? error.message : String(error),
2707
+ updatedAt: Date.now()
2708
+ }));
2709
+ throw error;
2710
+ }
2711
+ }
2712
+ };
2713
+ }
2714
+ function authorGridTools(sessionId, path, kind) {
2715
+ const mutation = mutationTool(sessionId, path, kind);
2716
+ return [...gridTools(sessionId, path, kind), ...mutation === void 0 ? [] : [mutation]];
2717
+ }
2718
+ //#endregion
2719
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorProfiles.ts
2720
+ function record(input) {
2721
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("Expected an object");
2722
+ return input;
2723
+ }
2724
+ function text(value, field) {
2725
+ if (typeof value !== "string") throw new Error(`${field} must be a string`);
2726
+ return value;
2727
+ }
2728
+ function target(input) {
2729
+ const value = record(input);
2730
+ return {
2731
+ sessionId: text(value.sessionId, "sessionId"),
2732
+ path: text(value.path, "path"),
2733
+ value
2734
+ };
2735
+ }
2736
+ const TARGET_PROPERTIES = {
2737
+ sessionId: {
2738
+ type: "string",
2739
+ minLength: 1
2740
+ },
2741
+ path: {
2742
+ type: "string",
2743
+ minLength: 1
2744
+ }
2745
+ };
2746
+ const AUTHOR_TEXT_PROFILE = {
2747
+ id: AUTHOR_RUNTIME_BINDING_IDS.text,
2748
+ tools: [{
2749
+ name: "author_add_comment",
2750
+ label: "Add comment",
2751
+ description: "Add a local review comment to one open Author document.",
2752
+ inputSchema: {
2753
+ type: "object",
2754
+ additionalProperties: false,
2755
+ required: [
2756
+ "sessionId",
2757
+ "path",
2758
+ "body"
2759
+ ],
2760
+ properties: {
2761
+ ...TARGET_PROPERTIES,
2762
+ body: { type: "string" },
2763
+ quote: { type: "string" }
2764
+ }
2765
+ },
2766
+ execute: async (input) => {
2767
+ const { sessionId, path, value } = target(input);
2768
+ const body = text(value.body, "body");
2769
+ addAuthorAnnotation(sessionId, path, {
2770
+ id: `${Date.now()}:${body.length}`,
2771
+ kind: "comment",
2772
+ body,
2773
+ ...typeof value.quote === "string" ? { quote: value.quote } : {}
2774
+ });
2775
+ return { added: true };
2776
+ }
2777
+ }, {
2778
+ name: "author_add_highlight",
2779
+ label: "Add highlight",
2780
+ description: "Highlight a line range in one open Author text document.",
2781
+ inputSchema: {
2782
+ type: "object",
2783
+ additionalProperties: false,
2784
+ required: [
2785
+ "sessionId",
2786
+ "path",
2787
+ "startLine",
2788
+ "endLine"
2789
+ ],
2790
+ properties: {
2791
+ ...TARGET_PROPERTIES,
2792
+ startLine: {
2793
+ type: "integer",
2794
+ minimum: 1
2795
+ },
2796
+ endLine: {
2797
+ type: "integer",
2798
+ minimum: 1
2799
+ }
2800
+ }
2801
+ },
2802
+ execute: async (input) => {
2803
+ const { sessionId, path, value } = target(input);
2804
+ if (!Number.isSafeInteger(value.startLine) || !Number.isSafeInteger(value.endLine)) throw new Error("Highlight lines must be integers");
2805
+ const startLine = value.startLine;
2806
+ const endLine = value.endLine;
2807
+ if (startLine < 1 || endLine < startLine) throw new Error("Invalid highlight range");
2808
+ addAuthorAnnotation(sessionId, path, {
2809
+ id: `${Date.now()}:${startLine}:${endLine}`,
2810
+ kind: "highlight",
2811
+ body: "",
2812
+ startLine,
2813
+ endLine
2814
+ });
2815
+ return { added: true };
2816
+ }
2817
+ }]
2818
+ };
2819
+ const AUTHOR_MEDIA_PROFILE = {
2820
+ id: AUTHOR_RUNTIME_BINDING_IDS.media,
2821
+ tools: []
2822
+ };
2823
+ function bindProfile(profile, sessionId, path) {
2824
+ return {
2825
+ id: profile.id,
2826
+ tools: profile.tools.map((tool) => {
2827
+ const schema = tool.inputSchema;
2828
+ const { sessionId: _sessionId, path: _path, ...boundProperties } = record(schema.properties);
2829
+ const required = Array.isArray(schema.required) ? schema.required.filter((entry) => entry !== "sessionId" && entry !== "path") : [];
2830
+ return {
2831
+ ...tool,
2832
+ inputSchema: {
2833
+ ...schema,
2834
+ properties: boundProperties,
2835
+ required
2836
+ },
2837
+ execute: async (input, signal) => await tool.execute({
2838
+ ...record(input),
2839
+ sessionId,
2840
+ path
2841
+ }, signal)
2842
+ };
2843
+ })
2844
+ };
2845
+ }
2846
+ function authorProfilesForDocument(sessionId, path, kind) {
2847
+ const profiles = kind === "image" ? [bindProfile(AUTHOR_MEDIA_PROFILE, sessionId, path)] : kind === "text" || kind === "markdown" ? [bindProfile(AUTHOR_TEXT_PROFILE, sessionId, path)] : [];
2848
+ const tools = authorGridTools(sessionId, path, kind);
2849
+ if (tools.length === 0) return profiles;
2850
+ return profiles.length === 0 ? [{
2851
+ id: AUTHOR_RUNTIME_BINDING_IDS.media,
2852
+ tools
2853
+ }] : [{
2854
+ ...profiles[0],
2855
+ tools: [...profiles[0].tools, ...tools]
2856
+ }];
2857
+ }
2858
+ //#endregion
2859
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorMedia.ts
2860
+ /**
2861
+ * Native media elements cannot perform the remote session's encrypted HTTP exchange.
2862
+ *
2863
+ * `source` addresses the host's own session file route, which the route table
2864
+ * declares as `file` with `host: true`; this package serves no bytes of its
2865
+ * own. The transport stays raw because what comes back is a blob, and a call
2866
+ * through the generated client reads the body as JSON to report a refusal.
2867
+ */
2868
+ async function loadAuthorMedia(source, signal) {
2869
+ const response = await sealedTransport.fetch(source, {
2870
+ signal,
2871
+ cache: "no-store"
2872
+ });
2873
+ if (!response.ok) throw new Error(`Author media could not be loaded (${response.status}).`);
2874
+ const blob = await response.blob();
2875
+ signal.throwIfAborted();
2876
+ const url = URL.createObjectURL(blob);
2877
+ let disposed = false;
2878
+ return {
2879
+ url,
2880
+ dispose() {
2881
+ if (disposed) return;
2882
+ disposed = true;
2883
+ URL.revokeObjectURL(url);
2884
+ }
2885
+ };
2886
+ }
2887
+ //#endregion
2888
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorVideoControls.tsx
2889
+ function videoTimeLabel(seconds) {
2890
+ const milliseconds = Math.max(0, Math.round((Number.isFinite(seconds) ? seconds : 0) * 1e3));
2891
+ return `${String(Math.floor(milliseconds / 6e4)).padStart(2, "0")}:${String(Math.floor(milliseconds / 1e3) % 60).padStart(2, "0")}.${String(milliseconds % 1e3).padStart(3, "0")}`;
2892
+ }
2893
+ function AuthorVideoControls({ playback, ready, locked, marking, onSeek, onToggle, onAnnotate }) {
2894
+ const [seconds, setSeconds] = useState("");
2895
+ const duration = playback.duration;
2896
+ const invalid = seconds.trim() === "" || !Number.isFinite(Number(seconds)) || Number(seconds) < 0 || Number(seconds) > duration;
2897
+ const buttonClass = "min-h-11 px-3 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm";
2898
+ return /* @__PURE__ */ jsxs("section", {
2899
+ "aria-label": "Video annotation controls",
2900
+ className: "mt-2 space-y-2 rounded border border-doom-border bg-doom-panel p-2.5 text-base text-doom-text sm:text-sm",
2901
+ children: [
2902
+ /* @__PURE__ */ jsxs("div", {
2903
+ className: "flex flex-wrap items-center justify-between gap-3",
2904
+ children: [/* @__PURE__ */ jsxs("output", {
2905
+ "aria-label": "Playback position",
2906
+ className: "font-mono text-base tabular-nums",
2907
+ children: [
2908
+ videoTimeLabel(playback.currentTime),
2909
+ " / ",
2910
+ videoTimeLabel(duration)
2911
+ ]
2912
+ }), /* @__PURE__ */ jsx("span", {
2913
+ className: "text-doom-dim",
2914
+ children: locked ? "Annotation draft: add or discard it before seeking." : marking ? "Frame paused. Drag a region on the video." : "Seek to a moment, then choose Annotate this frame."
2915
+ })]
2916
+ }),
2917
+ /* @__PURE__ */ jsx("input", {
2918
+ "aria-label": "Video timeline",
2919
+ type: "range",
2920
+ min: 0,
2921
+ max: duration || 0,
2922
+ step: .001,
2923
+ value: Math.min(playback.currentTime, duration),
2924
+ disabled: locked || duration <= 0,
2925
+ onChange: (event) => onSeek(Number(event.target.value)),
2926
+ className: "h-11 w-full cursor-pointer accent-doom-blue [@media(pointer:fine)]:h-6"
2927
+ }),
2928
+ /* @__PURE__ */ jsxs("div", {
2929
+ className: "flex flex-wrap items-center gap-2",
2930
+ children: [
2931
+ /* @__PURE__ */ jsx(Button, {
2932
+ className: buttonClass,
2933
+ disabled: locked || duration <= 0,
2934
+ onClick: onToggle,
2935
+ children: playback.playing ? "Pause" : "Play"
2936
+ }),
2937
+ /* @__PURE__ */ jsx(Button, {
2938
+ className: buttonClass,
2939
+ disabled: locked || duration <= 0,
2940
+ onClick: () => onSeek(Math.max(0, playback.currentTime - .1)),
2941
+ "aria-label": "Seek back 0.1 seconds",
2942
+ children: "−0.1s"
2943
+ }),
2944
+ /* @__PURE__ */ jsx(Button, {
2945
+ className: buttonClass,
2946
+ disabled: locked || duration <= 0,
2947
+ onClick: () => onSeek(Math.min(duration, playback.currentTime + .1)),
2948
+ "aria-label": "Seek forward 0.1 seconds",
2949
+ children: "+0.1s"
2950
+ }),
2951
+ /* @__PURE__ */ jsx(Button, {
2952
+ variant: "primary",
2953
+ className: buttonClass,
2954
+ disabled: locked || !ready,
2955
+ onClick: onAnnotate,
2956
+ children: "Annotate this frame"
2957
+ }),
2958
+ /* @__PURE__ */ jsxs("form", {
2959
+ className: "flex flex-wrap items-center gap-2 sm:ml-auto",
2960
+ onSubmit: (event) => {
2961
+ event.preventDefault();
2962
+ if (!invalid && !locked) onSeek(Number(seconds));
2963
+ },
2964
+ children: [
2965
+ /* @__PURE__ */ jsx("label", {
2966
+ htmlFor: "author-video-seconds",
2967
+ className: "text-doom-dim",
2968
+ children: "Go to seconds"
2969
+ }),
2970
+ /* @__PURE__ */ jsx("input", {
2971
+ id: "author-video-seconds",
2972
+ type: "number",
2973
+ min: 0,
2974
+ max: duration,
2975
+ step: .001,
2976
+ value: seconds,
2977
+ onChange: (event) => setSeconds(event.target.value),
2978
+ disabled: locked || duration <= 0,
2979
+ placeholder: "1.250",
2980
+ className: "h-11 w-24 rounded border border-doom-border bg-doom-deep px-2 text-lg [@media(pointer:fine)]:h-8 sm:text-sm"
2981
+ }),
2982
+ /* @__PURE__ */ jsx(Button, {
2983
+ type: "submit",
2984
+ className: buttonClass,
2985
+ disabled: locked || duration <= 0 || invalid,
2986
+ children: "Go"
2987
+ })
2988
+ ]
2989
+ })
2990
+ ]
2991
+ })
2992
+ ]
2993
+ });
2994
+ }
2995
+ //#endregion
2996
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorMediaView.tsx
2997
+ /** Marks source media. Native playback and page controls remain usable in select mode. */
2998
+ function AuthorMediaView({ sessionId, document: source, activeTool, displayedRegions, pendingCandidate = false, seekRequest }) {
2999
+ const host = useRef(null);
3000
+ const image = useRef(null);
3001
+ const video = useRef(null);
3002
+ const pdf = useRef(null);
3003
+ const start = useRef(void 0);
3004
+ const pointers = useRef(/* @__PURE__ */ new Map());
3005
+ const pan = useRef(void 0);
3006
+ const pinch = useRef(void 0);
3007
+ const [zoom, setZoom] = useState(1);
3008
+ const [stroke, setStroke] = useState();
3009
+ const [pdfPlane, setPdfPlane] = useState();
3010
+ const [playback, setPlayback] = useState({
3011
+ playing: false,
3012
+ currentTime: 0,
3013
+ duration: 0
3014
+ });
3015
+ const [frameReady, setFrameReady] = useState(false);
3016
+ const [selection, setSelection] = useState();
3017
+ const [error, setError] = useState();
3018
+ const [media, setMedia] = useState();
3019
+ useEffect(() => {
3020
+ start.current = void 0;
3021
+ pointers.current.clear();
3022
+ pinch.current = void 0;
3023
+ pan.current = void 0;
3024
+ setStroke(void 0);
3025
+ setSelection(void 0);
3026
+ setZoom(1);
3027
+ }, [
3028
+ source.path,
3029
+ source.version,
3030
+ source.sourceSha256,
3031
+ source.mediaUrl
3032
+ ]);
3033
+ useEffect(() => {
3034
+ start.current = void 0;
3035
+ setStroke(void 0);
3036
+ pointers.current.clear();
3037
+ pinch.current = void 0;
3038
+ pan.current = void 0;
3039
+ }, [activeTool]);
3040
+ const mediaUrl = media?.source === source.mediaUrl ? media?.url : void 0;
3041
+ useEffect(() => {
3042
+ if (source.kind !== "pdf" || mediaUrl === void 0) return;
3043
+ const container = host.current;
3044
+ if (container === null) return;
3045
+ const update = () => {
3046
+ const canvas = container.querySelector("canvas");
3047
+ const wrapper = container.querySelector("[data-testid=\"author-media\"]");
3048
+ if (!canvas || !wrapper) return;
3049
+ const bounds = canvas.getBoundingClientRect();
3050
+ const outer = wrapper.getBoundingClientRect();
3051
+ const next = {
3052
+ left: (bounds.left - outer.left) / zoom,
3053
+ top: (bounds.top - outer.top) / zoom,
3054
+ width: bounds.width / zoom,
3055
+ height: bounds.height / zoom
3056
+ };
3057
+ setPdfPlane((current) => current && Object.keys(next).every((key) => Math.abs(current[key] - next[key]) < .01) ? current : next);
3058
+ };
3059
+ const resize = new ResizeObserver(update);
3060
+ const mutation = new MutationObserver(() => {
3061
+ const canvas = container.querySelector("canvas");
3062
+ if (canvas) resize.observe(canvas);
3063
+ update();
3064
+ });
3065
+ mutation.observe(container, {
3066
+ childList: true,
3067
+ subtree: true
3068
+ });
3069
+ const canvas = container.querySelector("canvas");
3070
+ if (canvas) resize.observe(canvas);
3071
+ update();
3072
+ return () => {
3073
+ resize.disconnect();
3074
+ mutation.disconnect();
3075
+ };
3076
+ }, [
3077
+ source.kind,
3078
+ mediaUrl,
3079
+ zoom
3080
+ ]);
3081
+ useEffect(() => {
3082
+ if (source.mediaUrl === void 0) return;
3083
+ const input = source.mediaUrl;
3084
+ const controller = new AbortController();
3085
+ let dispose;
3086
+ setError(void 0);
3087
+ loadAuthorMedia(input, controller.signal).then((asset) => {
3088
+ if (controller.signal.aborted) {
3089
+ asset.dispose();
3090
+ return;
3091
+ }
3092
+ dispose = () => asset.dispose();
3093
+ setMedia({
3094
+ source: input,
3095
+ url: asset.url
3096
+ });
3097
+ }).catch((cause) => {
3098
+ if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
3099
+ });
3100
+ return () => {
3101
+ controller.abort();
3102
+ dispose?.();
3103
+ };
3104
+ }, [source.mediaUrl]);
3105
+ const seek = (seconds) => {
3106
+ if (authorDocumentAnnotations(sessionId, source.path).candidate || !Number.isFinite(seconds)) return;
3107
+ const player = video.current;
3108
+ if (!player) return;
3109
+ player.pause();
3110
+ setAuthorToolMode(sessionId, source.path, "select");
3111
+ setSelection(void 0);
3112
+ if (player.getState().currentTime === seconds) return;
3113
+ setFrameReady(false);
3114
+ player.seek(seconds);
3115
+ };
3116
+ useEffect(() => {
3117
+ if (seekRequest?.path === source.path && seekRequest.generation === authorSessionWorkspace(sessionId).focusedDocument?.generation) seek(seekRequest.timeSeconds);
3118
+ }, [seekRequest]);
3119
+ useEffect(() => {
3120
+ if (!pendingCandidate && start.current === void 0) setSelection(void 0);
3121
+ }, [pendingCandidate]);
3122
+ useEffect(() => registerAuthorGridResolver(sessionId, (cell, geometry) => {
3123
+ const target = image.current;
3124
+ if (source.kind !== "image" || target === null || target.naturalWidth === 0 || target.naturalHeight === 0) return void 0;
3125
+ const { originX = 0, originY = 0, width, height } = geometry.viewport;
3126
+ const cellBounds = {
3127
+ left: originX + cell.rect.x * width,
3128
+ top: originY + cell.rect.y * height,
3129
+ right: originX + (cell.rect.x + cell.rect.width) * width,
3130
+ bottom: originY + (cell.rect.y + cell.rect.height) * height
3131
+ };
3132
+ const bounds = target.getBoundingClientRect();
3133
+ const left = Math.max(bounds.left, cellBounds.left);
3134
+ const top = Math.max(bounds.top, cellBounds.top);
3135
+ const right = Math.min(bounds.right, cellBounds.right);
3136
+ const bottom = Math.min(bounds.bottom, cellBounds.bottom);
3137
+ if (right <= left || bottom <= top || bounds.width <= 0 || bounds.height <= 0) return void 0;
3138
+ return { anchor: {
3139
+ kind: "image-rect",
3140
+ rect: {
3141
+ x: (left - bounds.left) / bounds.width,
3142
+ y: (top - bounds.top) / bounds.height,
3143
+ width: (right - left) / bounds.width,
3144
+ height: (bottom - top) / bounds.height
3145
+ },
3146
+ naturalWidth: target.naturalWidth,
3147
+ naturalHeight: target.naturalHeight
3148
+ } };
3149
+ }), [
3150
+ sessionId,
3151
+ source.kind,
3152
+ source.path,
3153
+ source.version
3154
+ ]);
3155
+ const mark = async (event) => {
3156
+ const drag = start.current;
3157
+ if (!drag || drag.pointerId !== event.pointerId || pinch.current !== void 0) return;
3158
+ start.current = void 0;
3159
+ const bounds = drag.element.getBoundingClientRect();
3160
+ if (bounds.width <= 0 || bounds.height <= 0) return;
3161
+ const point = {
3162
+ x: Math.min(1, Math.max(0, (event.clientX - bounds.left) / bounds.width)),
3163
+ y: Math.min(1, Math.max(0, (event.clientY - bounds.top) / bounds.height))
3164
+ };
3165
+ const points = drag.mode === "draw" ? appendAuthorStrokePoint(drag.points, point) : void 0;
3166
+ const rect = drag.mode === "draw" ? authorStrokeBounds(points ?? []) : drag.mode === "mark" ? normalizedAuthorRectangle(bounds, {
3167
+ left: drag.x,
3168
+ top: drag.y,
3169
+ right: event.clientX,
3170
+ bottom: event.clientY
3171
+ }) : void 0;
3172
+ if (drag.mode !== "comment" && !rect) return;
3173
+ setStroke(void 0);
3174
+ setSelection(drag.mode === "draw" ? void 0 : drag.mode === "mark" ? {
3175
+ path: source.path,
3176
+ rect
3177
+ } : {
3178
+ path: source.path,
3179
+ point
3180
+ });
3181
+ const focused = authorSessionWorkspace(sessionId).focusedDocument;
3182
+ if (!focused || focused.path !== source.path) return;
3183
+ let anchor;
3184
+ let blob = null;
3185
+ let thumbnailUrl;
3186
+ try {
3187
+ if (drag.element instanceof HTMLImageElement) {
3188
+ const image = drag.element;
3189
+ if (!image.naturalWidth || !image.naturalHeight) throw new Error("Image is not ready.");
3190
+ anchor = drag.mode !== "comment" ? {
3191
+ kind: "image-rect",
3192
+ rect,
3193
+ naturalWidth: image.naturalWidth,
3194
+ naturalHeight: image.naturalHeight
3195
+ } : {
3196
+ kind: "image-point",
3197
+ point,
3198
+ naturalWidth: image.naturalWidth,
3199
+ naturalHeight: image.naturalHeight
3200
+ };
3201
+ const canvas = document.createElement("canvas");
3202
+ const scale = Math.min(1, 1600 / Math.max(image.naturalWidth, image.naturalHeight));
3203
+ canvas.width = Math.max(1, Math.round(image.naturalWidth * scale));
3204
+ canvas.height = Math.max(1, Math.round(image.naturalHeight * scale));
3205
+ const context = canvas.getContext("2d");
3206
+ if (!context) throw new Error("Canvas is unavailable.");
3207
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
3208
+ blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
3209
+ } else if (source.kind === "pdf") {
3210
+ const page = pdf.current?.getState();
3211
+ if (!page?.sourceWidth) throw new Error("PDF page is not ready.");
3212
+ anchor = drag.mode !== "comment" ? {
3213
+ kind: "pdf-page-rect",
3214
+ page: page.page,
3215
+ rect
3216
+ } : {
3217
+ kind: "pdf-page-point",
3218
+ page: page.page,
3219
+ point
3220
+ };
3221
+ blob = await pdf.current.capturePage();
3222
+ if (pdf.current?.getState().page !== page.page) return;
3223
+ } else {
3224
+ const frame = await video.current?.captureFrame();
3225
+ if (!frame) throw new Error("Video frame is not ready.");
3226
+ anchor = drag.mode !== "comment" ? {
3227
+ kind: "video-time-rect",
3228
+ rect,
3229
+ timeSeconds: frame.timeSeconds,
3230
+ intrinsicWidth: frame.width,
3231
+ intrinsicHeight: frame.height
3232
+ } : {
3233
+ kind: "video-time-point",
3234
+ point,
3235
+ timeSeconds: frame.timeSeconds,
3236
+ intrinsicWidth: frame.width,
3237
+ intrinsicHeight: frame.height
3238
+ };
3239
+ blob = frame.blob;
3240
+ if (Math.abs((video.current?.getState().currentTime ?? -1) - frame.timeSeconds) > .1 || video.current?.isFrameReady() === false) return;
3241
+ }
3242
+ if (!blob) throw new Error("Unable to capture selected media.");
3243
+ if (authorSessionWorkspace(sessionId).focusedDocument?.generation !== focused.generation) return;
3244
+ thumbnailUrl = URL.createObjectURL(blob);
3245
+ setAuthorRegionCandidate(sessionId, source.path, {
3246
+ documentPath: source.path,
3247
+ revision: source.version,
3248
+ sourceSha256: source.sourceSha256,
3249
+ mode: drag.mode === "comment" ? "point" : "region",
3250
+ anchor,
3251
+ ...points === void 0 ? {} : { stroke: points },
3252
+ viewport: {
3253
+ width: bounds.width,
3254
+ height: bounds.height
3255
+ },
3256
+ thumbnailUrl,
3257
+ evidence: blob,
3258
+ createdAt: Date.now()
3259
+ });
3260
+ setError(void 0);
3261
+ } catch (reason) {
3262
+ if (thumbnailUrl) URL.revokeObjectURL(thumbnailUrl);
3263
+ setError(reason instanceof Error ? reason.message : String(reason));
3264
+ }
3265
+ };
3266
+ const strokeOverlay = (points, ordinal) => points === void 0 ? null : /* @__PURE__ */ jsx("svg", {
3267
+ "data-testid": ordinal === void 0 ? "author-media-stroke-selection" : "author-media-stroke",
3268
+ "data-author-stroke": ordinal,
3269
+ "aria-hidden": "true",
3270
+ className: "pointer-events-none absolute inset-0 h-full w-full overflow-visible",
3271
+ viewBox: "0 0 1000 1000",
3272
+ preserveAspectRatio: "none",
3273
+ children: /* @__PURE__ */ jsx("polyline", {
3274
+ points: points.map((point) => `${String(point.x * 1e3)},${String(point.y * 1e3)}`).join(" "),
3275
+ fill: "none",
3276
+ stroke: "currentColor",
3277
+ strokeWidth: "5",
3278
+ strokeLinecap: "round",
3279
+ strokeLinejoin: "round",
3280
+ className: ordinal === void 0 ? "text-doom-red" : "text-doom-yellow"
3281
+ })
3282
+ });
3283
+ const selected = selection?.path === source.path ? selection.rect : void 0;
3284
+ const selectedPoint = selection?.path === source.path ? selection.point : void 0;
3285
+ const selectionOverlay = selected === void 0 ? null : /* @__PURE__ */ jsx("div", {
3286
+ "data-testid": "author-media-selection",
3287
+ className: "pointer-events-none absolute border-2 border-doom-red bg-doom-red/10",
3288
+ style: {
3289
+ left: `${selected.x * 100}%`,
3290
+ top: `${selected.y * 100}%`,
3291
+ width: `${selected.width * 100}%`,
3292
+ height: `${selected.height * 100}%`
3293
+ }
3294
+ });
3295
+ const pointPin = (ordinal, point, key) => /* @__PURE__ */ jsx("span", {
3296
+ "data-author-point": ordinal,
3297
+ "data-testid": ordinal === void 0 ? "author-media-point-selection" : void 0,
3298
+ className: `pointer-events-none absolute z-10 flex h-5 min-w-5 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border-2 px-1 text-2xs font-semibold ${ordinal === void 0 ? "border-white bg-doom-red text-white" : "border-doom-deep bg-doom-yellow text-doom-deep"}`,
3299
+ style: {
3300
+ left: `${String(point.x * 100)}%`,
3301
+ top: `${String(point.y * 100)}%`
3302
+ },
3303
+ children: ordinal ?? "•"
3304
+ }, key);
3305
+ const pointSelectionOverlay = selectedPoint === void 0 ? null : pointPin(void 0, selectedPoint, "selection");
3306
+ return /* @__PURE__ */ jsxs("div", {
3307
+ ref: host,
3308
+ className: "min-h-0 flex-1 overflow-auto p-4",
3309
+ style: { cursor: activeTool === "draw" || activeTool === "mark" ? "crosshair" : void 0 },
3310
+ onPointerMove: (event) => {
3311
+ if (pointers.current.has(event.pointerId)) pointers.current.set(event.pointerId, {
3312
+ x: event.clientX,
3313
+ y: event.clientY
3314
+ });
3315
+ if (pinch.current !== void 0 && pointers.current.size >= 2) {
3316
+ const [a, b] = [...pointers.current.values()];
3317
+ const distance = Math.hypot(a.x - b.x, a.y - b.y);
3318
+ if (pinch.current.distance > 0) setZoom(Math.min(8, Math.max(1, pinch.current.zoom * distance / pinch.current.distance)));
3319
+ return;
3320
+ }
3321
+ if (pan.current !== void 0 && activeTool === "pan") {
3322
+ const element = host.current;
3323
+ if (element) {
3324
+ element.scrollLeft = pan.current.left + pan.current.x - event.clientX;
3325
+ element.scrollTop = pan.current.top + pan.current.y - event.clientY;
3326
+ }
3327
+ return;
3328
+ }
3329
+ const drag = start.current;
3330
+ if (drag?.pointerId !== event.pointerId) return;
3331
+ if (drag.mode === "draw") {
3332
+ const bounds = drag.element.getBoundingClientRect();
3333
+ if (bounds.width <= 0 || bounds.height <= 0) return;
3334
+ drag.points = appendAuthorStrokePoint(drag.points, {
3335
+ x: (event.clientX - bounds.left) / bounds.width,
3336
+ y: (event.clientY - bounds.top) / bounds.height
3337
+ });
3338
+ setStroke(drag.points);
3339
+ return;
3340
+ }
3341
+ if (drag.mode !== "mark") return;
3342
+ const rect = normalizedAuthorRectangle(drag.element.getBoundingClientRect(), {
3343
+ left: drag.x,
3344
+ top: drag.y,
3345
+ right: event.clientX,
3346
+ bottom: event.clientY
3347
+ });
3348
+ setSelection(rect === null ? void 0 : {
3349
+ path: source.path,
3350
+ rect
3351
+ });
3352
+ },
3353
+ onPointerDownCapture: (event) => {
3354
+ if (event.button !== 0) return;
3355
+ const element = event.target;
3356
+ if (!(element instanceof HTMLImageElement || element instanceof HTMLVideoElement || element instanceof HTMLCanvasElement)) return;
3357
+ pointers.current.set(event.pointerId, {
3358
+ x: event.clientX,
3359
+ y: event.clientY
3360
+ });
3361
+ if (pointers.current.size === 2) {
3362
+ start.current = void 0;
3363
+ setStroke(void 0);
3364
+ setSelection(void 0);
3365
+ pan.current = void 0;
3366
+ const [a, b] = [...pointers.current.values()];
3367
+ pinch.current = {
3368
+ distance: Math.hypot(a.x - b.x, a.y - b.y),
3369
+ zoom
3370
+ };
3371
+ event.currentTarget.setPointerCapture(event.pointerId);
3372
+ return;
3373
+ }
3374
+ if (activeTool === "pan") {
3375
+ const container = host.current;
3376
+ if (container) pan.current = {
3377
+ x: event.clientX,
3378
+ y: event.clientY,
3379
+ left: container.scrollLeft,
3380
+ top: container.scrollTop
3381
+ };
3382
+ event.currentTarget.setPointerCapture(event.pointerId);
3383
+ return;
3384
+ }
3385
+ if (activeTool !== "mark" && activeTool !== "comment" && activeTool !== "draw" || pendingCandidate) return;
3386
+ event.preventDefault();
3387
+ setSelection(void 0);
3388
+ if (element instanceof HTMLVideoElement) {
3389
+ if (video.current?.isFrameReady?.() === false) {
3390
+ setError("Wait for the selected frame to finish loading.");
3391
+ return;
3392
+ }
3393
+ video.current?.pause();
3394
+ }
3395
+ const bounds = element.getBoundingClientRect();
3396
+ start.current = {
3397
+ x: event.clientX,
3398
+ y: event.clientY,
3399
+ pointerId: event.pointerId,
3400
+ mode: activeTool,
3401
+ element,
3402
+ points: activeTool === "draw" ? [{
3403
+ x: (event.clientX - bounds.left) / bounds.width,
3404
+ y: (event.clientY - bounds.top) / bounds.height
3405
+ }] : []
3406
+ };
3407
+ event.currentTarget.setPointerCapture(event.pointerId);
3408
+ },
3409
+ onPointerUp: (event) => {
3410
+ if (pinch.current === void 0 && pan.current === void 0) mark(event);
3411
+ pointers.current.delete(event.pointerId);
3412
+ pan.current = void 0;
3413
+ if (pointers.current.size < 2) pinch.current = void 0;
3414
+ },
3415
+ onPointerCancel: (event) => {
3416
+ pointers.current.delete(event.pointerId);
3417
+ pinch.current = void 0;
3418
+ pan.current = void 0;
3419
+ start.current = void 0;
3420
+ setStroke(void 0);
3421
+ setSelection(void 0);
3422
+ },
3423
+ onLostPointerCapture: () => {
3424
+ start.current = void 0;
3425
+ pan.current = void 0;
3426
+ setStroke(void 0);
3427
+ },
3428
+ children: [
3429
+ /* @__PURE__ */ jsxs("div", {
3430
+ className: "sticky top-0 z-20 mb-2 flex items-center gap-2 bg-doom-bg py-1",
3431
+ role: "group",
3432
+ "aria-label": "Canvas zoom",
3433
+ children: [
3434
+ /* @__PURE__ */ jsx(Button, {
3435
+ size: "md",
3436
+ variant: "outline",
3437
+ "aria-label": "Zoom out",
3438
+ className: "min-h-11 min-w-11",
3439
+ onClick: () => setZoom((current) => Math.max(1, current / 1.5)),
3440
+ children: "−"
3441
+ }),
3442
+ /* @__PURE__ */ jsxs("output", {
3443
+ className: "text-sm text-doom-dim",
3444
+ children: [Math.round(zoom * 100), "%"]
3445
+ }),
3446
+ /* @__PURE__ */ jsx(Button, {
3447
+ size: "md",
3448
+ variant: "outline",
3449
+ "aria-label": "Zoom in",
3450
+ className: "min-h-11 min-w-11",
3451
+ onClick: () => setZoom((current) => Math.min(8, current * 1.5)),
3452
+ children: "+"
3453
+ }),
3454
+ /* @__PURE__ */ jsx(Button, {
3455
+ size: "md",
3456
+ variant: "outline",
3457
+ "aria-label": "Fit canvas",
3458
+ className: "min-h-11",
3459
+ onClick: () => {
3460
+ setZoom(1);
3461
+ if (host.current) {
3462
+ host.current.scrollLeft = 0;
3463
+ host.current.scrollTop = 0;
3464
+ }
3465
+ },
3466
+ children: "Fit"
3467
+ })
3468
+ ]
3469
+ }),
3470
+ mediaUrl === void 0 ? /* @__PURE__ */ jsx("p", {
3471
+ className: "text-doom-dim",
3472
+ children: error === void 0 ? "Loading media..." : "Media unavailable."
3473
+ }) : source.kind === "image" ? /* @__PURE__ */ jsxs("div", {
3474
+ className: "relative inline-block max-w-full",
3475
+ style: {
3476
+ zoom,
3477
+ touchAction: activeTool === "select" ? "auto" : "none"
3478
+ },
3479
+ children: [
3480
+ /* @__PURE__ */ jsx("img", {
3481
+ ref: image,
3482
+ src: mediaUrl,
3483
+ alt: source.path,
3484
+ draggable: false,
3485
+ className: "block max-w-full"
3486
+ }),
3487
+ selectionOverlay,
3488
+ pointSelectionOverlay,
3489
+ strokeOverlay(stroke),
3490
+ displayedRegions.flatMap(({ ordinal, region }) => {
3491
+ if (region.stroke !== void 0) return [/* @__PURE__ */ jsxs("div", {
3492
+ className: "pointer-events-none absolute inset-0",
3493
+ children: [strokeOverlay(region.stroke, ordinal), pointPin(ordinal, region.stroke[0], `label:${region.id}`)]
3494
+ }, region.id)];
3495
+ if (region.anchor.kind === "image-point") return [pointPin(ordinal, region.anchor.point, region.id)];
3496
+ if (region.anchor.kind !== "image-rect") return [];
3497
+ const { rect } = region.anchor;
3498
+ return [/* @__PURE__ */ jsx("div", {
3499
+ "data-author-region": ordinal,
3500
+ className: "pointer-events-none absolute border border-doom-yellow bg-doom-yellow/10",
3501
+ style: {
3502
+ left: `${String(rect.x * 100)}%`,
3503
+ top: `${String(rect.y * 100)}%`,
3504
+ width: `${String(rect.width * 100)}%`,
3505
+ height: `${String(rect.height * 100)}%`
3506
+ },
3507
+ children: /* @__PURE__ */ jsx("span", {
3508
+ className: "absolute -left-2 -top-2 flex h-4 min-w-4 items-center justify-center rounded-full bg-doom-yellow px-1 text-2xs font-semibold text-doom-deep",
3509
+ children: ordinal
3510
+ })
3511
+ }, region.id)];
3512
+ })
3513
+ ]
3514
+ }) : /* @__PURE__ */ jsxs("div", {
3515
+ className: "relative w-fit max-w-full",
3516
+ style: {
3517
+ zoom,
3518
+ touchAction: activeTool === "select" ? "auto" : "none"
3519
+ },
3520
+ children: [/* @__PURE__ */ jsx(MediaPreview, {
3521
+ src: mediaUrl,
3522
+ path: source.path,
3523
+ kind: source.kind === "pdf" ? "pdf" : source.kind === "video" ? "video" : "download",
3524
+ controllerRef: video,
3525
+ pdfControllerRef: pdf,
3526
+ "data-testid": "author-media",
3527
+ videoControls: false,
3528
+ onPlaybackStateChange: (state) => {
3529
+ setPlayback(state);
3530
+ setFrameReady(video.current?.isFrameReady() ?? false);
3531
+ if (state.playing) setSelection(void 0);
3532
+ }
3533
+ }), /* @__PURE__ */ jsxs("div", {
3534
+ className: "pointer-events-none absolute",
3535
+ style: source.kind === "pdf" ? pdfPlane === void 0 ? { display: "none" } : {
3536
+ left: pdfPlane.left,
3537
+ top: pdfPlane.top,
3538
+ width: pdfPlane.width,
3539
+ height: pdfPlane.height
3540
+ } : { inset: 0 },
3541
+ children: [
3542
+ selectionOverlay,
3543
+ pointSelectionOverlay,
3544
+ strokeOverlay(stroke),
3545
+ displayedRegions.map(({ ordinal, region }) => {
3546
+ if (region.stroke !== void 0) {
3547
+ const anchor = region.anchor;
3548
+ if (source.kind === "pdf" && (anchor.kind !== "pdf-page-rect" || anchor.page !== pdf.current?.getState().page)) return null;
3549
+ if (source.kind === "video" && (anchor.kind !== "video-time-rect" || playback.playing || Math.abs(anchor.timeSeconds - playback.currentTime) > .1)) return null;
3550
+ return /* @__PURE__ */ jsxs("div", {
3551
+ className: "pointer-events-none absolute inset-0",
3552
+ children: [strokeOverlay(region.stroke, ordinal), pointPin(ordinal, region.stroke[0], `label:${region.id}`)]
3553
+ }, region.id);
3554
+ }
3555
+ if (source.kind === "pdf") {
3556
+ if (region.anchor.kind !== "pdf-page-point" && region.anchor.kind !== "pdf-page-rect") return null;
3557
+ if (region.anchor.page !== pdf.current?.getState().page) return null;
3558
+ if (region.anchor.kind === "pdf-page-point") return pointPin(ordinal, region.anchor.point, region.id);
3559
+ const { rect } = region.anchor;
3560
+ return /* @__PURE__ */ jsx("div", {
3561
+ "data-author-region": ordinal,
3562
+ className: "pointer-events-none absolute border border-doom-yellow bg-doom-yellow/10",
3563
+ style: {
3564
+ left: `${String(rect.x * 100)}%`,
3565
+ top: `${String(rect.y * 100)}%`,
3566
+ width: `${String(rect.width * 100)}%`,
3567
+ height: `${String(rect.height * 100)}%`
3568
+ },
3569
+ children: /* @__PURE__ */ jsx("span", {
3570
+ className: "bg-doom-yellow text-doom-deep",
3571
+ children: ordinal
3572
+ })
3573
+ }, region.id);
3574
+ }
3575
+ if (source.kind !== "video" || playback.playing) return null;
3576
+ if (region.anchor.kind === "video-time-point" && Math.abs(region.anchor.timeSeconds - playback.currentTime) <= .1) return pointPin(ordinal, region.anchor.point, region.id);
3577
+ if (region.anchor.kind !== "video-time-rect" || Math.abs(region.anchor.timeSeconds - playback.currentTime) > .1) return null;
3578
+ const { rect } = region.anchor;
3579
+ return /* @__PURE__ */ jsx("div", {
3580
+ "data-author-region": ordinal,
3581
+ className: "pointer-events-none absolute border border-doom-yellow bg-doom-yellow/10",
3582
+ style: {
3583
+ left: `${rect.x * 100}%`,
3584
+ top: `${rect.y * 100}%`,
3585
+ width: `${rect.width * 100}%`,
3586
+ height: `${rect.height * 100}%`
3587
+ },
3588
+ children: /* @__PURE__ */ jsx("span", {
3589
+ className: "bg-doom-yellow text-doom-deep",
3590
+ children: ordinal
3591
+ })
3592
+ }, region.id);
3593
+ })
3594
+ ]
3595
+ })]
3596
+ }),
3597
+ source.kind === "video" ? /* @__PURE__ */ jsx(AuthorVideoControls, {
3598
+ playback,
3599
+ ready: frameReady,
3600
+ locked: pendingCandidate,
3601
+ marking: activeTool === "mark",
3602
+ onSeek: seek,
3603
+ onToggle: () => {
3604
+ setAuthorToolMode(sessionId, source.path, "select");
3605
+ setSelection(void 0);
3606
+ if (playback.playing) video.current?.pause();
3607
+ else video.current?.play().catch((reason) => setError(String(reason)));
3608
+ },
3609
+ onAnnotate: () => {
3610
+ video.current?.pause();
3611
+ setAuthorToolMode(sessionId, source.path, "mark");
3612
+ }
3613
+ }) : null,
3614
+ error ? /* @__PURE__ */ jsx("output", {
3615
+ className: "text-base text-doom-red",
3616
+ children: error
3617
+ }) : null
3618
+ ]
3619
+ });
3620
+ }
3621
+ //#endregion
3622
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorStructuredView.tsx
3623
+ function AuthorStructuredView({ sessionId, document, displayedRegions }) {
3624
+ useEffect(() => registerAuthorGridResolver(sessionId, (cell, geometry) => {
3625
+ const { originX = 0, originY = 0, width, height } = geometry.viewport;
3626
+ const fragmentId = window.document.elementFromPoint(originX + cell.center.x * width, originY + cell.center.y * height)?.closest("[data-author-fragment]")?.dataset.authorFragment;
3627
+ const fragment = document.fragments?.find((candidate) => candidate.id === fragmentId);
3628
+ if (fragment === void 0 || fragment.readOnly === true) return void 0;
3629
+ if (document.kind === "csv" || document.kind === "xlsx") return {
3630
+ anchor: {
3631
+ kind: "cell",
3632
+ fragmentId: fragment.id,
3633
+ location: fragment.location
3634
+ },
3635
+ quote: fragment.text
3636
+ };
3637
+ const slide = Number(fragment.location.match(/(?:slide\s*|slides\/slide)(\d+)/u)?.[1]);
3638
+ if (!Number.isSafeInteger(slide)) return void 0;
3639
+ return {
3640
+ anchor: {
3641
+ kind: "slide-element",
3642
+ fragmentId: fragment.id,
3643
+ location: fragment.location,
3644
+ slide
3645
+ },
3646
+ quote: fragment.text
3647
+ };
3648
+ }), [
3649
+ document.fragments,
3650
+ document.kind,
3651
+ document.version,
3652
+ sessionId
3653
+ ]);
3654
+ const displayedByFragment = new Map(displayedRegions.flatMap(({ ordinal, region }) => region.anchor.kind === "cell" || region.anchor.kind === "slide-element" ? [[region.anchor.fragmentId, ordinal]] : []));
3655
+ const cells = document.kind === "csv" || document.kind === "xlsx";
3656
+ const groups = /* @__PURE__ */ new Map();
3657
+ for (const fragment of document.fragments ?? []) {
3658
+ const key = cells ? document.kind === "csv" ? fragment.location.split(",")[0] : fragment.location.replace(/![A-Z]+(\d+)$/, "!row $1") : fragment.location;
3659
+ const group = groups.get(key) ?? [];
3660
+ group.push(fragment);
3661
+ groups.set(key, group);
3662
+ }
3663
+ return /* @__PURE__ */ jsxs("div", {
3664
+ "data-testid": "author-structured",
3665
+ className: "min-h-0 flex-1 space-y-3 overflow-auto p-4",
3666
+ children: [!cells ? /* @__PURE__ */ jsx("p", {
3667
+ className: "text-sm text-doom-dim",
3668
+ children: "Slide text view, not a layout-faithful rendering."
3669
+ }) : null, [...groups].map(([location, fragments]) => /* @__PURE__ */ jsxs("section", {
3670
+ className: "space-y-1",
3671
+ children: [/* @__PURE__ */ jsx("h3", {
3672
+ className: "text-sm text-doom-dim",
3673
+ children: location
3674
+ }), /* @__PURE__ */ jsx("div", {
3675
+ className: cells ? "flex gap-1" : "space-y-2",
3676
+ children: fragments.map((fragment) => /* @__PURE__ */ jsxs("div", {
3677
+ "data-author-fragment": fragment.id,
3678
+ "data-author-region": displayedByFragment.get(fragment.id),
3679
+ className: `${cells ? "w-40 shrink-0" : ""} relative rounded ${displayedByFragment.has(fragment.id) ? "ring-1 ring-doom-yellow" : ""}`,
3680
+ children: [
3681
+ displayedByFragment.has(fragment.id) ? /* @__PURE__ */ jsx("span", {
3682
+ className: "absolute -left-1 -top-1 z-10 flex h-4 min-w-4 items-center justify-center rounded-full bg-doom-yellow px-1 text-2xs font-semibold text-doom-deep",
3683
+ children: displayedByFragment.get(fragment.id)
3684
+ }) : null,
3685
+ /* @__PURE__ */ jsxs(Button, {
3686
+ size: "xs",
3687
+ variant: "ghost",
3688
+ onClick: (event) => {
3689
+ const bounds = event.currentTarget.getBoundingClientRect();
3690
+ const slide = Number(fragment.location.match(/(?:slide\s*|slides\/slide)(\d+)/)?.[1]);
3691
+ if (!cells && !Number.isSafeInteger(slide)) return;
3692
+ setAuthorRegionCandidate(sessionId, document.path, {
3693
+ documentPath: document.path,
3694
+ revision: document.version,
3695
+ sourceSha256: document.sourceSha256,
3696
+ quote: fragment.text,
3697
+ anchor: cells ? {
3698
+ kind: "cell",
3699
+ fragmentId: fragment.id,
3700
+ location: fragment.location
3701
+ } : {
3702
+ kind: "slide-element",
3703
+ fragmentId: fragment.id,
3704
+ location: fragment.location,
3705
+ slide
3706
+ },
3707
+ viewport: {
3708
+ width: bounds.width,
3709
+ height: bounds.height
3710
+ },
3711
+ createdAt: Date.now()
3712
+ });
3713
+ },
3714
+ children: ["mark ", fragment.location]
3715
+ }),
3716
+ /* @__PURE__ */ jsx("textarea", {
3717
+ "aria-label": fragment.location,
3718
+ value: fragment.text,
3719
+ readOnly: fragment.readOnly === true,
3720
+ className: "w-full rounded border border-doom-border bg-doom-deep p-2 text-base text-doom-text",
3721
+ onChange: (event) => reviseAuthorFragment(sessionId, document.path, fragment.id, event.target.value)
3722
+ })
3723
+ ]
3724
+ }, fragment.id))
3725
+ })]
3726
+ }, location))]
3727
+ });
3728
+ }
3729
+ //#endregion
3730
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorTextView.tsx
3731
+ function AuthorTextView({ sessionId, document, preview, displayedRegions }) {
3732
+ const host = useRef(null);
3733
+ const editor = useRef(null);
3734
+ useEffect(() => registerAuthorGridResolver(sessionId, (cell, geometry) => {
3735
+ if (preview || editor.current === null) return void 0;
3736
+ const { originX = 0, originY = 0, width, height } = geometry.viewport;
3737
+ const range = editor.current.resolveViewportRegion({
3738
+ left: originX + cell.rect.x * width,
3739
+ top: originY + cell.rect.y * height,
3740
+ right: originX + (cell.rect.x + cell.rect.width) * width,
3741
+ bottom: originY + (cell.rect.y + cell.rect.height) * height
3742
+ });
3743
+ if (range === null || range.from === range.to) return void 0;
3744
+ return {
3745
+ anchor: {
3746
+ kind: "text-range",
3747
+ startOffset: range.from,
3748
+ endOffset: range.to,
3749
+ startLine: range.startLine,
3750
+ endLine: range.endLine
3751
+ },
3752
+ quote: range.text
3753
+ };
3754
+ }), [preview, sessionId]);
3755
+ useEffect(() => {
3756
+ editor.current?.setMarkedRanges(displayedRegions.flatMap(({ ordinal, region }) => region.anchor.kind === "text-range" ? [{
3757
+ from: region.anchor.startOffset,
3758
+ to: region.anchor.endOffset,
3759
+ label: String(ordinal)
3760
+ }] : []));
3761
+ }, [displayedRegions]);
3762
+ return /* @__PURE__ */ jsx("div", {
3763
+ ref: host,
3764
+ className: "flex min-h-0 flex-1 flex-col",
3765
+ children: preview ? /* @__PURE__ */ jsx("div", {
3766
+ "data-testid": "author-markdown",
3767
+ className: "min-h-0 flex-1 overflow-auto p-4 text-base text-doom-text",
3768
+ children: /* @__PURE__ */ jsx(Markdown, { text: document.content ?? "" })
3769
+ }) : /* @__PURE__ */ jsx(CodeEditor, {
3770
+ value: document.content ?? "",
3771
+ path: document.path,
3772
+ "data-testid": "author-editor",
3773
+ controllerRef: editor,
3774
+ className: "min-h-0 flex-1",
3775
+ onChange: (content) => reviseAuthorDocument(sessionId, document.path, content),
3776
+ onSelect: (range) => {
3777
+ const current = authorDocument(sessionId, document.path);
3778
+ const bounds = host.current?.getBoundingClientRect();
3779
+ if (!current || !bounds || range.from === range.to) return;
3780
+ setAuthorRegionCandidate(sessionId, document.path, {
3781
+ documentPath: current.path,
3782
+ revision: current.version,
3783
+ sourceSha256: current.sourceSha256,
3784
+ quote: range.text,
3785
+ anchor: {
3786
+ kind: "text-range",
3787
+ startOffset: range.from,
3788
+ endOffset: range.to,
3789
+ startLine: range.startLine,
3790
+ endLine: range.endLine
3791
+ },
3792
+ viewport: {
3793
+ width: bounds.width,
3794
+ height: bounds.height
3795
+ },
3796
+ createdAt: Date.now()
3797
+ });
3798
+ }
3799
+ })
3800
+ });
3801
+ }
3802
+ //#endregion
3803
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorDocumentPanel.tsx
3804
+ const pendingCanvases = /* @__PURE__ */ new Map();
3805
+ const canvasOwners = /* @__PURE__ */ new Map();
3806
+ function tabId(path) {
3807
+ let hash = 2166136261;
3808
+ for (const char of normalizeAuthorPath(path)) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
3809
+ return `author-file-${(hash >>> 0).toString(36)}`;
3810
+ }
3811
+ /** Unsent annotations only. Sent ones live in the request log, not on the canvas. */
3812
+ function displayedAuthorRegions(workspace) {
3813
+ return (workspace?.annotations ?? []).map((region, index) => ({
3814
+ ordinal: index + 1,
3815
+ region
3816
+ }));
3817
+ }
3818
+ function AuthorDocumentPanel(props) {
3819
+ if (!props.activeMinorModes?.includes("author")) {
3820
+ const fallback = props.fileTabFor(props.path);
3821
+ if (fallback !== void 0 && fallback.id !== tabId(props.path)) return /* @__PURE__ */ jsx(fallback.panel, { ...props });
3822
+ return /* @__PURE__ */ jsx("p", {
3823
+ "data-testid": "author-mode-inactive",
3824
+ className: "p-3 text-sm text-doom-dim",
3825
+ children: "No file viewer is available for this path."
3826
+ });
3827
+ }
3828
+ return /* @__PURE__ */ jsx(ActiveAuthorDocumentPanel, { ...props });
3829
+ }
3830
+ function ActiveAuthorDocumentPanel(props) {
3831
+ const { sessionId, statuses } = props;
3832
+ const path = useStore(canvasPaths, (state) => sessionId === null ? props.path : state[authorDocumentKey(sessionId, props.path)] ?? props.path);
3833
+ const alias = useStore(canvasAliases, (state) => props.alias ?? (sessionId === null ? void 0 : state[authorDocumentKey(sessionId, props.path)]));
3834
+ const loadFailure = useStore(canvasFailures, (state) => sessionId === null ? void 0 : state[authorDocumentKey(sessionId, props.path)]);
3835
+ const document = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.documents[authorDocumentKey(sessionId, path)]);
3836
+ const workspace = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
3837
+ const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, path, state));
3838
+ const activeTool = workspace?.toolsByDocument[normalizeAuthorPath(path)] ?? "select";
3839
+ const [markdownPreview, setMarkdownPreview] = useState(true);
3840
+ const [status, setStatus] = useState();
3841
+ const previewSelection = workspace?.previewSelections?.[normalizeAuthorPath(props.path)] ?? workspace?.previewSelections?.[normalizeAuthorPath(path)];
3842
+ const [previewOpen, setPreviewOpen] = useState(previewSelection !== void 0);
3843
+ useEffect(() => {
3844
+ if (previewSelection !== void 0) setPreviewOpen(true);
3845
+ }, [previewSelection]);
3846
+ const kind = document?.kind;
3847
+ const previewSource = document === void 0 ? void 0 : {
3848
+ path: document.path,
3849
+ kind: document.kind,
3850
+ hasUnsavedChanges: document.version !== document.savedVersion,
3851
+ revision: document.savedVersion,
3852
+ sourceSha256: document.sourceSha256,
3853
+ ...previewSelection === void 0 ? {} : { preview: previewSelection }
3854
+ };
3855
+ const PreviewPanel = (previewSource === void 0 ? void 0 : (props.slotData?.(authorPreviewActionSlot) ?? []).find(({ data }) => data.embeddedPanel !== void 0 && (data.supportsSource === void 0 || data.supportsSource(previewSource))))?.data.embeddedPanel;
3856
+ useEffect(() => {
3857
+ if (sessionId === null) return;
3858
+ const generation = focusAuthorDocument(sessionId, path);
3859
+ return () => releaseAuthorDocumentFocus(sessionId, generation);
3860
+ }, [path, sessionId]);
3861
+ useEffect(() => {
3862
+ if (kind === void 0 || sessionId === null || alias === void 0) return;
3863
+ const profiles = authorProfilesForDocument(sessionId, path, kind);
3864
+ let release;
3865
+ let cancelled = false;
3866
+ focusAuthorViewport(sessionId, profiles, alias).then((nextRelease) => {
3867
+ if (cancelled) nextRelease();
3868
+ else release = nextRelease;
3869
+ });
3870
+ return () => {
3871
+ cancelled = true;
3872
+ release?.();
3873
+ };
3874
+ }, [
3875
+ alias,
3876
+ kind,
3877
+ path,
3878
+ sessionId
3879
+ ]);
3880
+ if (document === void 0 || sessionId === null) return /* @__PURE__ */ jsx("p", {
3881
+ className: "p-4 text-sm text-doom-dim",
3882
+ children: loadFailure ?? status ?? "Loading document..."
3883
+ });
3884
+ const save = async () => {
3885
+ const savedVersion = document.version;
3886
+ const savedFragments = document.fragments;
3887
+ requestAuthorSave(sessionId, path);
3888
+ setStatus("saving");
3889
+ try {
3890
+ const sha256 = await saveAuthorDocument(sessionId, document);
3891
+ completeAuthorSave(sessionId, path, sha256, savedVersion, savedFragments);
3892
+ setStatus("saved");
3893
+ } catch (error) {
3894
+ failAuthorSave(sessionId, path, savedVersion);
3895
+ setStatus(error instanceof Error ? error.message : String(error));
3896
+ }
3897
+ };
3898
+ const gridVisible = autonomousVoiceGridVisible(statuses);
3899
+ const displayedRegions = displayedAuthorRegions(drafts);
3900
+ const displayedPreviewAnnotations = displayedRegions.flatMap(({ ordinal, region }) => {
3901
+ if (region.anchor.kind === "story-preview-rect") return [{
3902
+ ordinal,
3903
+ mode: "region",
3904
+ point: region.anchor.rect,
3905
+ rect: region.anchor.rect,
3906
+ stroke: region.stroke
3907
+ }];
3908
+ if (region.anchor.kind === "story-preview-point") return [{
3909
+ ordinal,
3910
+ mode: "point",
3911
+ point: region.anchor.point
3912
+ }];
3913
+ return [];
3914
+ });
3915
+ const textPanel = /* @__PURE__ */ jsx(AuthorTextView, {
3916
+ sessionId,
3917
+ document,
3918
+ preview: document.kind === "markdown" && markdownPreview && activeTool === "select" && !gridVisible,
3919
+ displayedRegions
3920
+ });
3921
+ const sourcePanel = document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview" ? textPanel : document.structuredFormat !== void 0 ? /* @__PURE__ */ jsx(AuthorStructuredView, {
3922
+ sessionId,
3923
+ document,
3924
+ displayedRegions
3925
+ }) : /* @__PURE__ */ jsx(AuthorMediaView, {
3926
+ sessionId,
3927
+ document,
3928
+ activeTool,
3929
+ displayedRegions,
3930
+ pendingCandidate: drafts.candidate !== void 0,
3931
+ seekRequest: workspace?.videoSeekRequest
3932
+ }, document.path);
3933
+ return /* @__PURE__ */ jsxs("section", {
3934
+ "data-testid": "author-document",
3935
+ className: "flex min-h-0 flex-1 flex-col",
3936
+ children: [
3937
+ /* @__PURE__ */ jsxs("header", {
3938
+ className: "flex items-center gap-2 border-b border-doom-border px-4 py-2",
3939
+ children: [
3940
+ /* @__PURE__ */ jsxs("strong", {
3941
+ className: "min-w-0 flex-1 truncate text-base text-doom-hi",
3942
+ children: [alias === void 0 ? "" : `${alias} · `, document.title ?? document.path]
3943
+ }),
3944
+ PreviewPanel !== void 0 && previewSource !== void 0 ? /* @__PURE__ */ jsx(Button, {
3945
+ size: "xs",
3946
+ variant: previewOpen ? "primary" : "outline",
3947
+ "data-testid": "author-story-preview-toggle",
3948
+ "aria-pressed": previewOpen,
3949
+ "aria-label": previewOpen ? "show story source" : "show story preview",
3950
+ onClick: () => setPreviewOpen(!previewOpen),
3951
+ children: previewOpen ? "Source" : "Preview"
3952
+ }) : null,
3953
+ document.kind === "markdown" ? /* @__PURE__ */ jsx(Button, {
3954
+ size: "xs",
3955
+ variant: "ghost",
3956
+ "data-testid": "author-markdown-toggle",
3957
+ onClick: () => setMarkdownPreview(!markdownPreview),
3958
+ children: markdownPreview ? "edit" : "preview"
3959
+ }) : null,
3960
+ document.kind === "video" ? /* @__PURE__ */ jsx("span", {
3961
+ className: "text-base text-doom-dim",
3962
+ children: "Video feedback"
3963
+ }) : /* @__PURE__ */ jsx(Button, {
3964
+ className: "min-h-11 px-4 text-base",
3965
+ variant: "outline",
3966
+ "data-testid": "author-save",
3967
+ disabled: document.revisions.length === 0 || document.savingVersion !== void 0,
3968
+ onClick: () => void save(),
3969
+ children: previewOpen && previewSource?.hasUnsavedChanges ? "Save & refresh" : "Save"
3970
+ })
3971
+ ]
3972
+ }),
3973
+ status === void 0 ? null : /* @__PURE__ */ jsx("output", {
3974
+ className: "px-4 py-1 text-sm text-doom-dim",
3975
+ children: status
3976
+ }),
3977
+ [
3978
+ "image",
3979
+ "pdf",
3980
+ "video",
3981
+ "story-preview"
3982
+ ].includes(document.kind) ? /* @__PURE__ */ jsx("nav", {
3983
+ "aria-label": "Author canvas tools",
3984
+ className: "flex shrink-0 gap-1 overflow-x-auto border-b border-doom-border px-2 py-1 sm:hidden",
3985
+ children: [
3986
+ "select",
3987
+ "pan",
3988
+ "mark",
3989
+ "comment",
3990
+ "draw"
3991
+ ].map((mode) => /* @__PURE__ */ jsx(Button, {
3992
+ size: "xs",
3993
+ variant: activeTool === mode ? "primary" : "outline",
3994
+ className: "min-h-11 min-w-11 shrink-0",
3995
+ "aria-label": `${mode} tool`,
3996
+ "aria-pressed": activeTool === mode,
3997
+ onClick: () => setAuthorToolMode(sessionId, document.path, mode),
3998
+ children: mode
3999
+ }, mode))
4000
+ }) : null,
4001
+ /* @__PURE__ */ jsxs("div", {
4002
+ className: "relative flex min-h-0 flex-1 overflow-hidden",
4003
+ children: [previewOpen && PreviewPanel !== void 0 && previewSource !== void 0 ? /* @__PURE__ */ jsx(PreviewPanel, {
4004
+ ...props,
4005
+ source: previewSource,
4006
+ activeTool: activeTool === "crop" ? "select" : activeTool,
4007
+ displayedAnnotations: displayedPreviewAnnotations,
4008
+ pendingCandidate: drafts.candidate !== void 0,
4009
+ onAnnotationCandidate: (candidate) => {
4010
+ setAuthorStoryPreview(sessionId, document.path, candidate.preview);
4011
+ setAuthorRegionCandidate(sessionId, document.path, {
4012
+ documentPath: document.path,
4013
+ revision: document.version,
4014
+ sourceSha256: document.sourceSha256,
4015
+ mode: candidate.mode,
4016
+ stroke: candidate.stroke,
4017
+ anchor: candidate.mode === "region" && candidate.rect !== void 0 ? {
4018
+ kind: "story-preview-rect",
4019
+ rect: candidate.rect,
4020
+ preview: candidate.preview
4021
+ } : {
4022
+ kind: "story-preview-point",
4023
+ point: candidate.point,
4024
+ preview: candidate.preview
4025
+ },
4026
+ viewport: candidate.preview.viewport,
4027
+ evidence: candidate.evidence,
4028
+ thumbnailUrl: candidate.thumbnailUrl,
4029
+ createdAt: Date.now()
4030
+ });
4031
+ }
4032
+ }) : sourcePanel, previewOpen && PreviewPanel !== void 0 && previewSource !== void 0 ? null : /* @__PURE__ */ jsx(AuthorGridOverlay, {
4033
+ sessionId,
4034
+ document,
4035
+ visible: gridVisible
4036
+ })]
4037
+ }),
4038
+ [
4039
+ "image",
4040
+ "pdf",
4041
+ "video",
4042
+ "story-preview"
4043
+ ].includes(document.kind) ? /* @__PURE__ */ jsx("div", {
4044
+ className: "max-h-56 shrink-0 overflow-y-auto border-t border-doom-border p-2 sm:hidden",
4045
+ children: /* @__PURE__ */ jsx(AuthorFeedbackControls, {
4046
+ sessionId,
4047
+ document,
4048
+ drafts,
4049
+ submitCapture: props.submitCapture
4050
+ })
4051
+ }) : null
4052
+ ]
4053
+ });
4054
+ }
4055
+ const authorOpenSourceAction = {
4056
+ version: 1,
4057
+ createTab({ sessionId, path, preview }) {
4058
+ const normalized = normalizeAuthorPath(path);
4059
+ if (sessionId.trim() === "" || normalized === "" || path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.replaceAll("\\", "/").split("/").includes("..")) throw new Error("Author source opening requires a session and a repository-relative path.");
4060
+ if (preview !== void 0 && (typeof preview.appPath !== "string" || preview.appPath.trim() === "" || typeof preview.storyExport !== "string" || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(preview.storyExport) || preview.snapshot !== void 0 && typeof preview.snapshot !== "boolean")) throw new Error("Author preview requires a project and an exact story export.");
4061
+ const canonical = canvasPaths.state[authorDocumentKey(sessionId, normalized)] ?? normalized;
4062
+ const prefix = `${sessionId}\n`;
4063
+ const tabPath = (Object.entries(canvasPaths.state).find(([key, path]) => key.startsWith(prefix) && path === canonical)?.[0])?.slice(prefix.length) ?? normalized;
4064
+ setAuthorPreviewSelection(sessionId, tabPath, preview);
4065
+ if (canonical !== tabPath) setAuthorPreviewSelection(sessionId, canonical, preview);
4066
+ return authorFileTab(tabPath, canvasAliases.state[authorDocumentKey(sessionId, tabPath)]);
4067
+ }
4068
+ };
4069
+ function authorFileTab(path, preferredAlias) {
4070
+ const normalized = normalizeAuthorPath(path);
4071
+ return {
4072
+ id: tabId(normalized),
4073
+ label: preferredAlias === void 0 ? normalized.split("/").at(-1) ?? normalized : `${preferredAlias} · ${normalized.split("/").at(-1) ?? normalized}`,
4074
+ retainComposer: true,
4075
+ composerContext(sessionId) {
4076
+ const key = authorDocumentKey(sessionId, normalized);
4077
+ const documentPath = canvasPaths.state[key] ?? normalized;
4078
+ const alias = canvasAliases.state[key];
4079
+ const name = documentPath.split("/").at(-1) ?? documentPath;
4080
+ return {
4081
+ kind: "author-document",
4082
+ source: "author",
4083
+ id: documentPath,
4084
+ label: alias === void 0 ? name : `${alias} · ${name}`,
4085
+ content: authorDocumentContextContent(documentPath, alias, authorDocumentAnnotations(sessionId, documentPath).annotations)
4086
+ };
4087
+ },
4088
+ onOpen(sessionId) {
4089
+ const key = authorDocumentKey(sessionId, normalized);
4090
+ const controller = new AbortController();
4091
+ pendingCanvases.set(key, controller);
4092
+ canvasFailures.setState((state) => {
4093
+ const next = { ...state };
4094
+ delete next[key];
4095
+ return next;
4096
+ });
4097
+ (async () => {
4098
+ const result = await api.session(sessionId).documentsOpen({
4099
+ body: {
4100
+ path: normalized,
4101
+ ...preferredAlias === void 0 ? {} : { alias: preferredAlias }
4102
+ },
4103
+ query: { session: sessionId },
4104
+ signal: controller.signal
4105
+ });
4106
+ if (!result.ok) throw new Error(result.error);
4107
+ if (!("alias" in result.data) || typeof result.data.alias !== "string") throw new Error("The Author canvas did not return an alias. Reopen the document.");
4108
+ const opened = result.data;
4109
+ const canonicalAlias = opened.alias;
4110
+ if (controller.signal.aborted || pendingCanvases.get(key) !== controller) {
4111
+ if ((canvasOwners.get(`${sessionId}\n${canonicalAlias}`)?.size ?? 0) === 0) api.session(sessionId).bridgeClose({ body: { alias: canonicalAlias } }).catch(() => void 0);
4112
+ return;
4113
+ }
4114
+ canvasAliases.setState((state) => ({
4115
+ ...state,
4116
+ [key]: canonicalAlias,
4117
+ [authorDocumentKey(sessionId, opened.path)]: canonicalAlias
4118
+ }));
4119
+ canvasPaths.setState((state) => ({
4120
+ ...state,
4121
+ [key]: opened.path
4122
+ }));
4123
+ const ownerKey = `${sessionId}\n${canonicalAlias}`;
4124
+ const owners = canvasOwners.get(ownerKey) ?? /* @__PURE__ */ new Set();
4125
+ owners.add(key);
4126
+ canvasOwners.set(ownerKey, owners);
4127
+ const documentKey = authorDocumentKey(sessionId, opened.path);
4128
+ if (authorWorkspace.store.state.documents[documentKey] === void 0) {
4129
+ const loaded = await loadAuthorDocument(sessionId, opened.path, controller.signal);
4130
+ if (controller.signal.aborted || pendingCanvases.get(key) !== controller) return;
4131
+ putAuthorDocument(sessionId, loaded);
4132
+ }
4133
+ const document = authorWorkspace.store.state.documents[documentKey];
4134
+ if (document !== void 0 && !controller.signal.aborted) await openAuthorCanvas(sessionId, canonicalAlias, authorProfilesForDocument(sessionId, opened.path, document.kind));
4135
+ })().catch((error) => {
4136
+ if (!controller.signal.aborted) canvasFailures.setState((state) => ({
4137
+ ...state,
4138
+ [key]: error instanceof Error ? error.message : String(error)
4139
+ }));
4140
+ }).finally(() => {
4141
+ if (pendingCanvases.get(key) === controller) pendingCanvases.delete(key);
4142
+ });
4143
+ },
4144
+ onClose(sessionId) {
4145
+ const key = authorDocumentKey(sessionId, normalized);
4146
+ const canonical = canvasPaths.state[key] ?? normalized;
4147
+ setAuthorToolMode(sessionId, canonical, "select");
4148
+ setAuthorPreviewSelection(sessionId, normalized, void 0);
4149
+ if (canonical !== normalized) setAuthorPreviewSelection(sessionId, canonical, void 0);
4150
+ pendingCanvases.get(key)?.abort();
4151
+ pendingCanvases.delete(key);
4152
+ const alias = canvasAliases.state[key] ?? preferredAlias;
4153
+ if (alias === void 0) {
4154
+ api.session(sessionId).documentsOpen({
4155
+ body: { path: normalized },
4156
+ query: { session: sessionId }
4157
+ }).then((result) => {
4158
+ if (!result.ok || !("alias" in result.data) || typeof result.data.alias !== "string") return;
4159
+ const recoveredAlias = result.data.alias;
4160
+ if ((canvasOwners.get(`${sessionId}\n${recoveredAlias}`)?.size ?? 0) === 0) api.session(sessionId).bridgeClose({ body: { alias: recoveredAlias } }).catch(() => void 0);
4161
+ }).catch(() => void 0);
4162
+ return;
4163
+ }
4164
+ const owners = canvasOwners.get(`${sessionId}\n${alias}`);
4165
+ owners?.delete(key);
4166
+ if (owners !== void 0 && owners.size === 0) canvasOwners.delete(`${sessionId}\n${alias}`);
4167
+ const lastOwner = owners === void 0 || owners.size === 0;
4168
+ if (lastOwner) dropAuthorViewportSession(sessionId, alias);
4169
+ canvasAliases.setState((state) => {
4170
+ const next = { ...state };
4171
+ delete next[key];
4172
+ return next;
4173
+ });
4174
+ canvasPaths.setState((state) => {
4175
+ const next = { ...state };
4176
+ delete next[key];
4177
+ return next;
4178
+ });
4179
+ canvasFailures.setState((state) => {
4180
+ const next = { ...state };
4181
+ delete next[key];
4182
+ return next;
4183
+ });
4184
+ if (lastOwner) api.session(sessionId).bridgeClose({ body: { alias } }).catch(() => void 0);
4185
+ },
4186
+ panel: (props) => /* @__PURE__ */ jsx(AuthorDocumentPanel, {
4187
+ ...props,
4188
+ path: normalized
4189
+ })
4190
+ };
4191
+ }
4192
+ //#endregion
4193
+ //#region src/extensions/workspaces/sessions/(frontend)/file-links/author-document.web.ts
4194
+ var author_document_web_default = defineFileLinks({
4195
+ requiredMinorMode: "author",
4196
+ subscribe(listener) {
4197
+ const subscription = authorWorkspace.store.subscribe(listener);
4198
+ return () => subscription.unsubscribe();
4199
+ },
4200
+ fingerprint(sessionId) {
4201
+ if (sessionId === null) return "";
4202
+ const prefix = `${sessionId}\n`;
4203
+ return Object.keys(authorWorkspace.store.state.documents).filter((key) => key.startsWith(prefix)).sort().join("\n");
4204
+ },
4205
+ resolve(sessionId, path) {
4206
+ if (sessionId === null) return void 0;
4207
+ const normalized = normalizeAuthorPath(path.replace(/:\d+(?::\d+)?$/, ""));
4208
+ return authorWorkspace.store.state.documents[authorDocumentKey(sessionId, normalized)] === void 0 ? void 0 : authorFileTab(normalized, canvasAliases.state[authorDocumentKey(sessionId, normalized)]);
4209
+ },
4210
+ openPath(sessionId, path) {
4211
+ if (sessionId === null) return void 0;
4212
+ const normalized = normalizeAuthorPath(path);
4213
+ return normalized === "" ? void 0 : authorFileTab(normalized, canvasAliases.state[authorDocumentKey(sessionId, normalized)]);
4214
+ }
4215
+ });
4216
+ //#endregion
4217
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/Author.author.open-source.web.ts
4218
+ var Author_author_open_source_web_default = defineFill({
4219
+ slot: "author.open-source",
4220
+ id: "author",
4221
+ data: authorOpenSourceAction
4222
+ });
4223
+ //#endregion
4224
+ //#region src/extensions/workspaces/sessions/(frontend)/leader/author-toggle.web.ts
4225
+ var author_toggle_web_default = defineLeaderBinding({
4226
+ id: "author.toggle",
4227
+ path: [{
4228
+ key: "o",
4229
+ label: "other",
4230
+ detail: "optional modes and tools"
4231
+ }, {
4232
+ key: "a",
4233
+ label: "author",
4234
+ detail: "enable or disable focused document authoring"
4235
+ }],
4236
+ command: "minor author"
4237
+ });
4238
+ //#endregion
4239
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorAnnotationPersistence.ts
4240
+ const AUTHOR_ANNOTATION_DATABASE = "po-author-annotations";
4241
+ const STORE = "documents";
4242
+ const RECORD_VERSION = 1;
4243
+ function object(value) {
4244
+ return typeof value === "object" && value !== null;
4245
+ }
4246
+ function finite(value) {
4247
+ return typeof value === "number" && Number.isFinite(value);
4248
+ }
4249
+ function point(value) {
4250
+ return object(value) && finite(value.x) && finite(value.y) && value.x >= 0 && value.x <= 1 && value.y >= 0 && value.y <= 1;
4251
+ }
4252
+ function rect(value) {
4253
+ return point(value) && object(value) && finite(value.width) && finite(value.height) && value.width > 0 && value.height > 0 && finite(value.x) && finite(value.y) && value.x + value.width <= 1 && value.y + value.height <= 1;
4254
+ }
4255
+ function validAnchor(value) {
4256
+ if (!object(value) || typeof value.kind !== "string") return false;
4257
+ switch (value.kind) {
4258
+ case "text-range": return finite(value.startOffset) && finite(value.endOffset) && finite(value.startLine) && finite(value.endLine);
4259
+ case "cell": return typeof value.fragmentId === "string" && typeof value.location === "string";
4260
+ case "slide-element": return typeof value.fragmentId === "string" && finite(value.slide) && typeof value.location === "string";
4261
+ case "image-rect": return rect(value.rect) && finite(value.naturalWidth) && finite(value.naturalHeight);
4262
+ case "pdf-page-rect": return finite(value.page) && rect(value.rect);
4263
+ case "video-time-rect": return finite(value.timeSeconds) && rect(value.rect);
4264
+ case "story-preview-rect": return rect(value.rect) && object(value.preview);
4265
+ case "image-point": return point(value.point) && finite(value.naturalWidth) && finite(value.naturalHeight);
4266
+ case "pdf-page-point": return finite(value.page) && point(value.point);
4267
+ case "video-time-point": return finite(value.timeSeconds) && point(value.point);
4268
+ case "story-preview-point": return point(value.point) && object(value.preview);
4269
+ default: return false;
4270
+ }
4271
+ }
4272
+ function validEvidence(value) {
4273
+ return value === void 0 || typeof Blob !== "undefined" && value instanceof Blob && value.size <= 8388608;
4274
+ }
4275
+ function validCandidate(value) {
4276
+ if (!object(value)) return false;
4277
+ return typeof value.documentPath === "string" && normalizeAuthorPath(value.documentPath) !== "" && finite(value.revision) && (value.sourceSha256 === void 0 || typeof value.sourceSha256 === "string") && (value.mode === void 0 || value.mode === "region" || value.mode === "point") && validAnchor(value.anchor) && (value.stroke === void 0 || validAuthorStroke(value.stroke) && "rect" in value.anchor && value.mode === "region") && object(value.viewport) && finite(value.viewport.width) && finite(value.viewport.height) && finite(value.createdAt) && validEvidence(value.evidence);
4278
+ }
4279
+ function validAnnotation(value) {
4280
+ return validCandidate(value) && object(value) && typeof value.id === "string" && value.id !== "" && typeof value.comment === "string" && value.comment.trim() !== "" && (value.version === void 0 || Number.isInteger(value.version) && value.version >= 1);
4281
+ }
4282
+ function utf8Length(value) {
4283
+ return new TextEncoder().encode(value).byteLength;
4284
+ }
4285
+ function validateAuthorAnnotationRecord(input) {
4286
+ if (!object(input) || input.version !== RECORD_VERSION || typeof input.key !== "string") return void 0;
4287
+ if (typeof input.sessionId !== "string" || input.sessionId === "" || typeof input.documentPath !== "string") return void 0;
4288
+ const path = normalizeAuthorPath(input.documentPath);
4289
+ if (path === "" || input.key !== `${input.sessionId}\n${path}` || !object(input.collection)) return void 0;
4290
+ const collection = input.collection;
4291
+ if (!finite(collection.revision) || collection.sourceSha256 !== void 0 && typeof collection.sourceSha256 !== "string" || typeof collection.stale !== "boolean" || typeof collection.candidateText !== "string" || utf8Length(collection.candidateText) > 2048 || !Array.isArray(collection.annotations) || collection.annotations.length > 16 || !collection.annotations.every(validAnnotation) || collection.candidate !== void 0 && !validCandidate(collection.candidate) || !finite(collection.updatedAt)) return void 0;
4292
+ return input;
4293
+ }
4294
+ function durableCandidate(candidate) {
4295
+ const { thumbnailUrl: _thumbnailUrl, ...durable } = candidate;
4296
+ return {
4297
+ ...durable,
4298
+ ...candidate.stroke === void 0 ? {} : { stroke: candidate.stroke.map((point) => ({ ...point })) }
4299
+ };
4300
+ }
4301
+ function persistenceRecords() {
4302
+ const records = [];
4303
+ for (const [sessionId, session] of Object.entries(authorWorkspace.store.state.sessions)) for (const [documentPath, collection] of Object.entries(session.annotationsByDocument)) {
4304
+ if (collection.annotations.length === 0 && collection.candidate === void 0 && collection.candidateText === "") continue;
4305
+ records.push({
4306
+ key: `${sessionId}\n${documentPath}`,
4307
+ version: RECORD_VERSION,
4308
+ sessionId,
4309
+ documentPath,
4310
+ collection: {
4311
+ ...collection,
4312
+ candidate: collection.candidate && durableCandidate(collection.candidate),
4313
+ annotations: collection.annotations.map(durableCandidate)
4314
+ }
4315
+ });
4316
+ }
4317
+ return records.sort((a, b) => b.collection.updatedAt - a.collection.updatedAt).slice(0, 100);
4318
+ }
4319
+ var IndexedDbAuthorAnnotationRepository = class {
4320
+ #database;
4321
+ #writes = Promise.resolve();
4322
+ constructor(factory) {
4323
+ this.#database = new Promise((resolve, reject) => {
4324
+ const request = factory.open(AUTHOR_ANNOTATION_DATABASE, 1);
4325
+ request.onupgradeneeded = () => {
4326
+ if (!request.result.objectStoreNames.contains(STORE)) request.result.createObjectStore(STORE, { keyPath: "key" });
4327
+ };
4328
+ request.onsuccess = () => resolve(request.result);
4329
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("Unable to open Author annotation storage."));
4330
+ });
4331
+ }
4332
+ async load() {
4333
+ const database = await this.#database;
4334
+ return await new Promise((resolve, reject) => {
4335
+ const request = database.transaction(STORE, "readonly").objectStore(STORE).getAll();
4336
+ request.onsuccess = () => resolve(request.result);
4337
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("Unable to read Author annotations."));
4338
+ });
4339
+ }
4340
+ replace(records) {
4341
+ const write = async () => {
4342
+ const database = await this.#database;
4343
+ await new Promise((resolve, reject) => {
4344
+ const transaction = database.transaction(STORE, "readwrite");
4345
+ const store = transaction.objectStore(STORE);
4346
+ store.clear();
4347
+ records.forEach((record) => store.put(record));
4348
+ transaction.oncomplete = () => resolve();
4349
+ transaction.onerror = () => reject(transaction.error ?? /* @__PURE__ */ new Error("Unable to write Author annotations."));
4350
+ transaction.onabort = () => reject(transaction.error ?? /* @__PURE__ */ new Error("Author annotation write was aborted."));
4351
+ });
4352
+ };
4353
+ this.#writes = this.#writes.catch(() => void 0).then(write);
4354
+ return this.#writes;
4355
+ }
4356
+ async close() {
4357
+ try {
4358
+ await this.#writes;
4359
+ } finally {
4360
+ (await this.#database).close();
4361
+ }
4362
+ }
4363
+ };
4364
+ function startAuthorAnnotationPersistence(repository) {
4365
+ const factory = typeof indexedDB === "undefined" ? void 0 : indexedDB;
4366
+ if (repository === void 0 && factory === void 0) return async () => void 0;
4367
+ const storage = repository ?? new IndexedDbAuthorAnnotationRepository(factory);
4368
+ let stopped = false;
4369
+ let hydrated = false;
4370
+ let dirty = false;
4371
+ const subscription = authorWorkspace.store.subscribe(() => {
4372
+ if (!hydrated) dirty = true;
4373
+ else storage.replace(persistenceRecords()).catch(() => void 0);
4374
+ });
4375
+ storage.load().then((values) => {
4376
+ if (stopped) return;
4377
+ const records = values.map(validateAuthorAnnotationRecord).filter((record) => record !== void 0).sort((a, b) => b.collection.updatedAt - a.collection.updatedAt).slice(0, 100);
4378
+ hydrateAuthorAnnotationCollections(records);
4379
+ hydrated = true;
4380
+ if (dirty || records.length > 0) storage.replace(persistenceRecords()).catch(() => void 0);
4381
+ }).catch(() => {
4382
+ if (stopped) return;
4383
+ hydrated = true;
4384
+ if (dirty) storage.replace(persistenceRecords()).catch(() => void 0);
4385
+ });
4386
+ return async () => {
4387
+ stopped = true;
4388
+ subscription.unsubscribe();
4389
+ await storage.close();
4390
+ };
4391
+ }
4392
+ //#endregion
4393
+ //#region src/extensions/workspaces/sessions/(frontend)/lifecycle/_lib/authorRequestLifecycle.ts
4394
+ function capturePacket(content) {
4395
+ try {
4396
+ const value = JSON.parse(content);
4397
+ if (value.version !== 1 || typeof value.captureId !== "string" || typeof value.capturedAt !== "number" || typeof value.document?.path !== "string" || typeof value.document.revision !== "number" || !Array.isArray(value.regions) || value.regions.length === 0 || value.regions.length > 16 || value.regions.some((region) => region.stroke !== void 0 && (!validAuthorStroke(region.stroke) || region.mode !== "region" || !("rect" in region.anchor)))) return;
4398
+ return value;
4399
+ } catch {
4400
+ return;
4401
+ }
4402
+ }
4403
+ function recordAuthorComposerSubmission(submission) {
4404
+ for (const item of submission.contextItems) {
4405
+ if (item.source !== "author" || item.kind !== "author-capture") continue;
4406
+ const packet = capturePacket(item.metadata ?? item.content);
4407
+ if (packet === void 0 || packet.captureId !== item.id) continue;
4408
+ if (authorSessionWorkspace(submission.sessionId).requests.some((request) => request.captureId === packet.captureId)) continue;
4409
+ const regions = packet.regions.map((region) => ({
4410
+ id: region.id,
4411
+ documentPath: packet.document.path,
4412
+ revision: packet.document.revision,
4413
+ sourceSha256: packet.document.sourceSha256,
4414
+ comment: region.comment,
4415
+ version: region.version ?? 1,
4416
+ mode: region.mode,
4417
+ quote: region.quote,
4418
+ anchor: structuredClone(region.anchor),
4419
+ ...region.stroke === void 0 ? {} : { stroke: region.stroke.map((point) => ({ ...point })) },
4420
+ viewport: { ...region.viewport },
4421
+ voiceGrid: region.voiceGrid && { ...region.voiceGrid },
4422
+ createdAt: packet.capturedAt
4423
+ }));
4424
+ putAuthorRequest(submission.sessionId, {
4425
+ id: crypto.randomUUID(),
4426
+ captureId: packet.captureId,
4427
+ documentPath: packet.document.path,
4428
+ requestText: submission.message,
4429
+ regions,
4430
+ pendingRegions: regions,
4431
+ status: "REQUESTED",
4432
+ createdAt: submission.submittedAt,
4433
+ updatedAt: submission.submittedAt,
4434
+ revision: packet.document.revision,
4435
+ sourceSha256: packet.document.sourceSha256
4436
+ });
4437
+ for (const region of packet.regions) removeAuthorRegion(submission.sessionId, packet.document.path, region.id, region.version ?? 1);
4438
+ }
4439
+ }
4440
+ /** Only the host's correlated execution events may settle a submitted capture. */
4441
+ function recordAuthorCaptureStatus(event) {
4442
+ const request = authorSessionWorkspace(event.sessionId).requests.find((record) => record.captureId === event.captureId);
4443
+ if (request === void 0 || [
4444
+ "COMPLETE",
4445
+ "FAILED",
4446
+ "CANCELLED"
4447
+ ].includes(request.status)) return;
4448
+ if (event.status === "queued") return;
4449
+ updateAuthorRequest(event.sessionId, request.id, (record) => ({
4450
+ ...record,
4451
+ status: event.status === "working" ? "CHANGING" : event.status === "completed" ? "COMPLETE" : "FAILED",
4452
+ currentOperation: event.status === "working" ? "Agent is working on this request" : void 0,
4453
+ pendingRegions: event.status === "completed" ? [] : record.pendingRegions,
4454
+ error: event.status === "error" ? event.error || "Request execution failed." : void 0,
4455
+ updatedAt: Date.now()
4456
+ }));
4457
+ }
4458
+ //#endregion
4459
+ //#region src/extensions/workspaces/sessions/(frontend)/lifecycle/_lib/authorBrowserLifecycle.ts
4460
+ function startAuthorBrowserLifecycle(runtime) {
4461
+ const stopBridge = startAuthorBrowserBridge(runtime);
4462
+ const stopPersistence = startAuthorAnnotationPersistence();
4463
+ const stopSubmissions = runtime.onComposerSubmitted?.(recordAuthorComposerSubmission) ?? (() => void 0);
4464
+ const stopCaptureStatus = runtime.onCaptureStatus?.(recordAuthorCaptureStatus) ?? (() => void 0);
4465
+ return async () => {
4466
+ stopSubmissions();
4467
+ stopCaptureStatus();
4468
+ stopBridge();
4469
+ await stopPersistence().catch(() => void 0);
4470
+ };
4471
+ }
4472
+ //#endregion
4473
+ //#region src/extensions/workspaces/sessions/(frontend)/lifecycle/start.web.ts
4474
+ var start_web_default = defineWebLifecycle(startAuthorBrowserLifecycle);
4475
+ //#endregion
4476
+ //#region src/extensions/workspaces/sessions/(frontend)/mode/author/mode.web.ts
4477
+ var mode_web_default = defineMinorModeFile({
4478
+ name: "author",
4479
+ keys: "o a",
4480
+ order: 45
4481
+ });
4482
+ //#endregion
4483
+ //#region src/extensions/workspaces/sessions/(frontend)/slot/open-source.web.ts
4484
+ var open_source_web_default = defineSlotFile({
4485
+ slot: "author.open-source",
4486
+ parse: parseAuthorOpenSourceAction
4487
+ });
4488
+ //#endregion
4489
+ //#region src/extensions/workspaces/sessions/(frontend)/slot/preview-action.web.ts
4490
+ /** Typed data slot for preview launchers that need Author's focused source. */
4491
+ var preview_action_web_default = defineSlotFile({
4492
+ slot: "author.preview-action",
4493
+ parse: parseAuthorPreviewAction
4494
+ });
4495
+ //#endregion
4496
+ //#region src/extensions/workspaces/sessions/(frontend)/slot/preview-provider.web.ts
4497
+ /**
4498
+ * Optional preview providers render launch controls here. The provider owns its
4499
+ * transient tab and preview lifecycle; Author owns the surrounding mode and
4500
+ * composer submission surface.
4501
+ */
4502
+ var preview_provider_web_default = defineSlotFile({});
4503
+ //#endregion
4504
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/describeAuthorToolsToolRender.ts
4505
+ function describeAuthorToolsCallSummary(_args) {
4506
+ return "";
4507
+ }
4508
+ /**
4509
+ * The body as toned lines: what the result's details say, or its text. Pure,
4510
+ * so a unit test can pin it without a DOM; the item decides how many to show.
4511
+ */
4512
+ function describeAuthorToolsResultLines(result) {
4513
+ if (result === null) return [];
4514
+ return toolResultTextLines(result.content).map((text) => ({
4515
+ text,
4516
+ tone: "dim"
4517
+ }));
4518
+ }
4519
+ //#endregion
4520
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/DescribeAuthorToolsToolCard.tsx
4521
+ /**
4522
+ * The describe_author_tools tool's timeline item: Describe the current Author viewport capabilities. The shell owns
4523
+ * the frame, the outcome tone, the status badge, and the expand toggle; this
4524
+ * card supplies the header summary and the body lines, and says whether it
4525
+ * hides any so the toggle appears only when it reveals something.
4526
+ */
4527
+ function DescribeAuthorToolsToolCard({ toolName, args, result, running, isError }) {
4528
+ const lines = describeAuthorToolsResultLines(result);
4529
+ const collapsed = collapseLines(lines, 8, false);
4530
+ return /* @__PURE__ */ jsx(MessageItem, {
4531
+ tone: toolTone({
4532
+ running,
4533
+ isError
4534
+ }),
4535
+ expandable: collapsed.hidden > 0,
4536
+ children: ({ expanded }) => {
4537
+ const view = expanded ? collapseLines(lines, 8, true) : collapsed;
4538
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MessageItemHeader, {
4539
+ title: toolName,
4540
+ children: /* @__PURE__ */ jsx("span", {
4541
+ "data-testid": `tool-call-${toolName}`,
4542
+ className: "min-w-0 flex-1 truncate text-doom-text",
4543
+ children: describeAuthorToolsCallSummary(args)
4544
+ })
4545
+ }), view.shown.length > 0 || running || isError ? /* @__PURE__ */ jsxs(MessageItemBody, {
4546
+ "data-testid": `tool-result-${toolName}`,
4547
+ className: "flex flex-col gap-1",
4548
+ children: [
4549
+ view.shown.length > 0 ? /* @__PURE__ */ jsx(MessageLines, { lines: view.shown }) : null,
4550
+ view.hidden > 0 ? /* @__PURE__ */ jsxs(MessageItemStatus, {
4551
+ expands: true,
4552
+ children: [view.hidden, " more line(s)"]
4553
+ }) : null,
4554
+ running ? /* @__PURE__ */ jsx(MessageItemStatus, {
4555
+ tone: "running",
4556
+ children: "running"
4557
+ }) : null,
4558
+ isError ? /* @__PURE__ */ jsx(MessageItemStatus, {
4559
+ tone: "error",
4560
+ children: "failed"
4561
+ }) : null
4562
+ ]
4563
+ }) : null] });
4564
+ }
4565
+ });
4566
+ }
4567
+ //#endregion
4568
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/describe-author-tools.web.tsx
4569
+ var describe_author_tools_web_default = defineToolRenderer({
4570
+ tools: ["describe_author_tools"],
4571
+ message: DescribeAuthorToolsToolCard
4572
+ });
4573
+ //#endregion
4574
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/OpenAuthoringFileToolCard.tsx
4575
+ function openAuthoringFileTab(path, openTransientTab, openTab, alias) {
4576
+ const tab = authorFileTab(path, alias);
4577
+ openTransientTab(tab);
4578
+ openTab(tab.id);
4579
+ }
4580
+ function resultPath(result) {
4581
+ if (typeof result?.details !== "object" || result.details === null || !("path" in result.details)) return void 0;
4582
+ return typeof result.details.path === "string" ? result.details.path : void 0;
4583
+ }
4584
+ function OpenAuthoringFileToolCard({ args, result, running, isError, output, openTransientTab, openTab }) {
4585
+ const path = resultPath(result);
4586
+ const unavailable = !running && !isError && result !== null && path === void 0;
4587
+ const errorLines = isError ? (output ?? "").trim().split(/\r?\n/u).filter(Boolean).slice(0, 4).map((text) => ({
4588
+ text,
4589
+ tone: "error"
4590
+ })) : [];
4591
+ const requestedPath = typeof args.path === "string" ? args.path : "document";
4592
+ const alias = typeof result?.details === "object" && result.details !== null && "alias" in result.details && typeof result.details.alias === "string" ? result.details.alias : void 0;
4593
+ return /* @__PURE__ */ jsx(MessageItem, {
4594
+ tone: toolTone({
4595
+ running,
4596
+ isError: isError || unavailable
4597
+ }),
4598
+ children: () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MessageItemHeader, {
4599
+ title: "open_authoring_file",
4600
+ children: /* @__PURE__ */ jsx("span", {
4601
+ "data-testid": "tool-call-open_authoring_file",
4602
+ className: "min-w-0 flex-1 truncate text-doom-text",
4603
+ children: alias === void 0 ? requestedPath : `${alias} · ${requestedPath}`
4604
+ })
4605
+ }), /* @__PURE__ */ jsxs(MessageItemBody, {
4606
+ "data-testid": "tool-result-open_authoring_file",
4607
+ className: "flex flex-col gap-1",
4608
+ children: [
4609
+ errorLines.length > 0 ? /* @__PURE__ */ jsx(MessageLines, { lines: errorLines }) : null,
4610
+ /* @__PURE__ */ jsx(MessageItemStatus, {
4611
+ tone: isError || unavailable ? "error" : running ? "running" : void 0,
4612
+ children: isError ? "failed" : running ? "validating" : unavailable ? "unavailable" : path === void 0 ? "waiting" : "ready"
4613
+ }),
4614
+ !running && !isError && path !== void 0 ? /* @__PURE__ */ jsx(Button, {
4615
+ size: "xs",
4616
+ variant: "outline",
4617
+ "data-testid": "author-open-file",
4618
+ onClick: () => openAuthoringFileTab(path, openTransientTab, openTab, alias),
4619
+ children: "open in Author"
4620
+ }) : null
4621
+ ]
4622
+ })] })
4623
+ });
4624
+ }
4625
+ //#endregion
4626
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/open-authoring-file.web.tsx
4627
+ var open_authoring_file_web_default = defineToolRenderer({
4628
+ tools: ["open_authoring_file"],
4629
+ message: OpenAuthoringFileToolCard,
4630
+ completionTab: ({ result }) => {
4631
+ const path = resultPath(result);
4632
+ const details = result?.details;
4633
+ return path === void 0 ? void 0 : authorFileTab(path, typeof details?.alias === "string" ? details.alias : void 0);
4634
+ }
4635
+ });
4636
+ //#endregion
4637
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/useAuthorToolsToolRender.ts
4638
+ function useAuthorToolsCallSummary(args) {
4639
+ return typeof args.name === "string" ? args.name : "";
4640
+ }
4641
+ /**
4642
+ * The body as toned lines: what the result's details say, or its text. Pure,
4643
+ * so a unit test can pin it without a DOM; the item decides how many to show.
4644
+ */
4645
+ function useAuthorToolsResultLines(result) {
4646
+ if (result === null) return [];
4647
+ return toolResultTextLines(result.content).map((text) => ({
4648
+ text,
4649
+ tone: "dim"
4650
+ }));
4651
+ }
4652
+ //#endregion
4653
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/UseAuthorToolsToolCard.tsx
4654
+ /**
4655
+ * The use_author_tools tool's timeline item: Use one capability of the current Author viewport. The shell owns
4656
+ * the frame, the outcome tone, the status badge, and the expand toggle; this
4657
+ * card supplies the header summary and the body lines, and says whether it
4658
+ * hides any so the toggle appears only when it reveals something.
4659
+ */
4660
+ function UseAuthorToolsToolCard({ toolName, args, result, running, isError }) {
4661
+ const lines = useAuthorToolsResultLines(result);
4662
+ const collapsed = collapseLines(lines, 8, false);
4663
+ return /* @__PURE__ */ jsx(MessageItem, {
4664
+ tone: toolTone({
4665
+ running,
4666
+ isError
4667
+ }),
4668
+ expandable: collapsed.hidden > 0,
4669
+ children: ({ expanded }) => {
4670
+ const view = expanded ? collapseLines(lines, 8, true) : collapsed;
4671
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MessageItemHeader, {
4672
+ title: toolName,
4673
+ children: /* @__PURE__ */ jsx("span", {
4674
+ "data-testid": `tool-call-${toolName}`,
4675
+ className: "min-w-0 flex-1 truncate text-doom-text",
4676
+ children: useAuthorToolsCallSummary(args)
4677
+ })
4678
+ }), view.shown.length > 0 || running || isError ? /* @__PURE__ */ jsxs(MessageItemBody, {
4679
+ "data-testid": `tool-result-${toolName}`,
4680
+ className: "flex flex-col gap-1",
4681
+ children: [
4682
+ view.shown.length > 0 ? /* @__PURE__ */ jsx(MessageLines, { lines: view.shown }) : null,
4683
+ view.hidden > 0 ? /* @__PURE__ */ jsxs(MessageItemStatus, {
4684
+ expands: true,
4685
+ children: [view.hidden, " more line(s)"]
4686
+ }) : null,
4687
+ running ? /* @__PURE__ */ jsx(MessageItemStatus, {
4688
+ tone: "running",
4689
+ children: "running"
4690
+ }) : null,
4691
+ isError ? /* @__PURE__ */ jsx(MessageItemStatus, {
4692
+ tone: "error",
4693
+ children: "failed"
4694
+ }) : null
4695
+ ]
4696
+ }) : null] });
4697
+ }
4698
+ });
4699
+ }
4700
+ //#endregion
4701
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/use-author-tools.web.tsx
4702
+ var use_author_tools_web_default = defineToolRenderer({
4703
+ tools: ["use_author_tools"],
4704
+ message: UseAuthorToolsToolCard
4705
+ });
4706
+ //#endregion
4707
+ //#region generated/web.ts
4708
+ const via = (identity, value) => value === void 0 ? void 0 : {
4709
+ ...identity,
4710
+ ...value
4711
+ };
4712
+ const webPlugin = defineWebPlugin({
4713
+ id: "author",
4714
+ session: {
4715
+ get channels() {
4716
+ return [via({ channel: "author_webmcp" }, author_webmcp_web_default)];
4717
+ },
4718
+ get dockFaces() {
4719
+ return [via({ id: "authoring" }, authoring_web_default)];
4720
+ },
4721
+ get fileLinks() {
4722
+ return author_document_web_default;
4723
+ },
4724
+ get fills() {
4725
+ return [via({
4726
+ slot: "author.open-source",
4727
+ id: "author"
4728
+ }, Author_author_open_source_web_default)];
4729
+ },
4730
+ get leaderBindings() {
4731
+ return [via({ id: "author.author-toggle" }, author_toggle_web_default)];
4732
+ },
4733
+ get minorModes() {
4734
+ return [via({ name: "author" }, mode_web_default)];
4735
+ },
4736
+ get slots() {
4737
+ return [
4738
+ via({ slot: "author.open-source" }, open_source_web_default),
4739
+ via({ slot: "author.preview-action" }, preview_action_web_default),
4740
+ via({ slot: "author.preview-provider" }, preview_provider_web_default)
4741
+ ];
4742
+ },
4743
+ get start() {
4744
+ return start_web_default;
4745
+ },
4746
+ get toolRenderers() {
4747
+ return [
4748
+ via({ tools: ["describe_author_tools"] }, describe_author_tools_web_default),
4749
+ via({ tools: ["open_authoring_file"] }, open_authoring_file_web_default),
4750
+ via({ tools: ["use_author_tools"] }, use_author_tools_web_default)
4751
+ ];
4752
+ }
4753
+ }
4754
+ });
4755
+ //#endregion
4756
+ export { webPlugin };
4757
+
4758
+ //# sourceMappingURL=web.mjs.map