@agimon-ai/po-author 0.0.0-alpha.34 → 0.0.0-alpha.35

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.
@@ -1,9 +1,9 @@
1
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
2
  import { Store } from "@tanstack/store";
3
3
  import { useStore } from "@tanstack/react-store";
4
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
- import { Button, CodeEditor, Dot, FileIcon, Markdown, MediaPreview, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, MessageLines, SparklesIcon, StatusBadge, TrashIcon, collapseLines, toolTone } from "@agimon-ai/po-web-components";
6
+ import { Button, CodeEditor, Dot, FileIcon, Markdown, MediaPreview, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, MessageLines, SparklesIcon, StatusBadge, TrashIcon, collapseLines, drawableImageUrl, markdownMediaReview, markdownMediaReviewSlot, svgImageUrl, toolTone } from "@agimon-ai/po-web-components";
7
7
  import { sealedTransport } from "@agimon-ai/po-web-security/browser";
8
8
  //#region src/types/webAuthor.ts
9
9
  const authorChannelType = "author_webmcp";
@@ -1219,6 +1219,8 @@ var author_webmcp_web_default = defineChannelFile({
1219
1219
  //#endregion
1220
1220
  //#region src/constants/author.ts
1221
1221
  const AUTHOR_FILE_TAB_PREFIX = "author-file-";
1222
+ /** Names a file in the session's artifact folder rather than the working tree. Artifacts open read-only. */
1223
+ const AUTHOR_ARTIFACT_PREFIX = "artifact:";
1222
1224
  //#endregion
1223
1225
  //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorGridOverlay.tsx
1224
1226
  function autonomousVoiceGridVisible(statuses) {
@@ -1396,7 +1398,8 @@ function createAuthorCapturePacket(captureId, capturedAt, document, regions, ali
1396
1398
  revision: document.version,
1397
1399
  ...document.sourceSha256 === void 0 ? {} : { sourceSha256: document.sourceSha256 },
1398
1400
  ...document.storyPreview === void 0 ? {} : { storyPreview: structuredClone(document.storyPreview) },
1399
- ...document.imageArtifact === void 0 ? {} : { imageArtifact: { ...document.imageArtifact } }
1401
+ ...document.imageArtifact === void 0 ? {} : { imageArtifact: { ...document.imageArtifact } },
1402
+ ...document.markdownMedia === void 0 ? {} : { markdownMedia: { ...document.markdownMedia } }
1400
1403
  },
1401
1404
  regions: regions.map((region, index) => {
1402
1405
  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.`);
@@ -1462,12 +1465,25 @@ function storyPreviewProvenance(packet) {
1462
1465
  "After editing the underlying story or component source, rebuild the preview before treating it as visually verified."
1463
1466
  ].join("\n");
1464
1467
  }
1468
+ function filePath(path) {
1469
+ return path.startsWith("artifact:") ? `${path} (relative to the session artifact folder)` : path;
1470
+ }
1471
+ function markdownMediaProvenance(packet) {
1472
+ const media = packet.document.markdownMedia;
1473
+ if (media === void 0) return void 0;
1474
+ const lines = [media.kind === "mermaid" ? "Reviewed a mermaid diagram rendered from Markdown. Change the diagram by editing its source." : "Reviewed an image shown in rendered Markdown. Change it by editing or regenerating its source file."];
1475
+ if (media.sourcePath !== void 0) lines.push(`Source image: ${filePath(media.sourcePath)}`);
1476
+ if (media.documentPath !== void 0) lines.push(`Embedded in document: ${filePath(media.documentPath)}`);
1477
+ if (media.documentPath === void 0 && media.sourcePath === void 0) lines.push("Embedded in a conversation message, not a file.");
1478
+ if (media.mermaidSource !== void 0) lines.push("Mermaid source:", "```mermaid", media.mermaidSource, "```");
1479
+ return lines.join("\n");
1480
+ }
1465
1481
  function authorCaptureContext(packet) {
1466
1482
  const metadata = JSON.stringify(packet);
1467
1483
  if (bytes(metadata) > 65536) throw new Error("Author capture packet exceeds 64 KiB.");
1468
1484
  const annotations = packet.regions.map((region) => `(${String(region.ordinal)}) ${region.comment} [${anchorLocation(region.anchor)}]\nregionId: ${JSON.stringify(region.id)}${region.quote ? `\nQuoted text: ${region.quote}` : ""}`).join("\n\n");
1469
1485
  const artifact = packet.document.imageArtifact;
1470
- const provenance = artifact === void 0 ? storyPreviewProvenance(packet) : [
1486
+ const provenance = artifact === void 0 ? markdownMediaProvenance(packet) ?? storyPreviewProvenance(packet) : [
1471
1487
  "Generated image feedback: preserve the original image; generate a new refinement only when requested.",
1472
1488
  `Original artifact: ${artifact.id} (session ${artifact.sessionId})`,
1473
1489
  `Original canonical path: ${artifact.originalPath}`,
@@ -1493,7 +1509,7 @@ function canvasBlob(canvas, type, quality) {
1493
1509
  canvas.toBlob((blob) => blob === null ? reject(/* @__PURE__ */ new Error("Unable to capture viewport")) : resolve(blob), type, quality);
1494
1510
  });
1495
1511
  }
1496
- async function base64(blob) {
1512
+ async function base64$1(blob) {
1497
1513
  const bytes = new Uint8Array(await blob.arrayBuffer());
1498
1514
  let binary = "";
1499
1515
  for (let offset = 0; offset < bytes.length; offset += 32768) binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
@@ -1518,7 +1534,7 @@ async function boundedCanvasCapture(canvas) {
1518
1534
  }
1519
1535
  if (blob.size > 2097152) throw new Error("Viewport capture exceeds 2 MiB");
1520
1536
  return {
1521
- data: await base64(blob),
1537
+ data: await base64$1(blob),
1522
1538
  mimeType
1523
1539
  };
1524
1540
  }
@@ -2047,222 +2063,6 @@ function AuthorRequestLog({ requests }) {
2047
2063
  });
2048
2064
  }
2049
2065
  //#endregion
2050
- //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorToolPalette.tsx
2051
- /** One tool tile. An active annotation tool is the person's mark (rose); an active view tool is Po's jade. */
2052
- function authorToolClassName(active, annotates) {
2053
- return `h-auto min-h-11 min-w-11 flex-col gap-1 rounded-md px-1 py-2 text-base [@media(pointer:fine)]:min-h-8 ${!active ? "border-po-border-soft bg-po-panel text-po-dim" : annotates ? "border-po-edge-magenta bg-po-tint-magenta font-semibold text-po-magenta hover:text-po-magenta" : "border-po-edge-green bg-po-tint-green font-semibold text-po-green hover:text-po-green"}`;
2054
- }
2055
- function AuthorToolPalette({ sessionId, path, kind, activeTool }) {
2056
- const [error, setError] = useState();
2057
- const format = (style) => {
2058
- const selected = authorDocumentAnnotations(sessionId, path).candidate;
2059
- const document = selected && authorDocument(sessionId, selected.documentPath);
2060
- if (!selected || !document || selected.anchor.kind !== "text-range" || selected.revision !== document.version || selected.sourceSha256 !== document.sourceSha256) {
2061
- setError("Select text in the document first.");
2062
- return;
2063
- }
2064
- const { startOffset, endOffset } = selected.anchor;
2065
- const content = document.content ?? "";
2066
- const text = content.slice(startOffset, endOffset);
2067
- const replacement = style === "Bold" ? `**${text}**` : style === "Link" ? `[${text}](https://)` : text.split("\n").map((line) => `${style === "Heading" ? "## " : "- "}${line}`).join("\n");
2068
- reviseAuthorDocument(sessionId, document.path, content.slice(0, startOffset) + replacement + content.slice(endOffset));
2069
- setAuthorRegionCandidate(sessionId, path, void 0);
2070
- setError(void 0);
2071
- };
2072
- const tools = [
2073
- ...[
2074
- "image",
2075
- "pdf",
2076
- "video",
2077
- "story-preview"
2078
- ].includes(kind) ? [{
2079
- label: "Pan",
2080
- glyph: "✋",
2081
- action: () => setAuthorToolMode(sessionId, path, activeTool === "pan" ? "select" : "pan"),
2082
- active: activeTool === "pan",
2083
- annotates: false
2084
- }, {
2085
- label: "Draw",
2086
- glyph: "✎",
2087
- action: () => setAuthorToolMode(sessionId, path, activeTool === "draw" ? "select" : "draw"),
2088
- active: activeTool === "draw",
2089
- annotates: true
2090
- }] : [],
2091
- {
2092
- label: "Region",
2093
- glyph: "⌗",
2094
- action: () => setAuthorToolMode(sessionId, path, activeTool === "mark" ? "select" : "mark"),
2095
- active: activeTool === "mark",
2096
- annotates: true
2097
- },
2098
- ...kind === "markdown" ? [
2099
- {
2100
- label: "Bold",
2101
- glyph: "B",
2102
- action: () => format("Bold")
2103
- },
2104
- {
2105
- label: "Heading",
2106
- glyph: "H",
2107
- action: () => format("Heading")
2108
- },
2109
- {
2110
- label: "Link",
2111
- glyph: "↗",
2112
- action: () => format("Link")
2113
- },
2114
- {
2115
- label: "List",
2116
- glyph: "≡",
2117
- action: () => format("List")
2118
- }
2119
- ] : [],
2120
- {
2121
- label: "Comment",
2122
- glyph: "●",
2123
- action: () => setAuthorToolMode(sessionId, path, activeTool === "comment" ? "select" : "comment"),
2124
- active: activeTool === "comment",
2125
- annotates: true
2126
- }
2127
- ];
2128
- return /* @__PURE__ */ jsxs("section", {
2129
- "data-testid": "author-tool-palette",
2130
- className: "space-y-2",
2131
- children: [
2132
- /* @__PURE__ */ jsx("h3", {
2133
- className: "sr-only",
2134
- children: "Tools"
2135
- }),
2136
- /* @__PURE__ */ jsx("div", {
2137
- className: "grid grid-cols-4 gap-1.5",
2138
- children: tools.map((tool) => /* @__PURE__ */ jsxs(Button, {
2139
- size: "md",
2140
- variant: "outline",
2141
- "aria-label": tool.label === "Region" ? "mark region" : tool.label,
2142
- "aria-pressed": tool.active,
2143
- onClick: tool.action,
2144
- className: authorToolClassName(tool.active === true, tool.annotates === true),
2145
- children: [/* @__PURE__ */ jsx("span", {
2146
- "aria-hidden": "true",
2147
- className: "text-lg leading-none",
2148
- children: tool.glyph
2149
- }), /* @__PURE__ */ jsx("span", {
2150
- className: "text-sm",
2151
- children: tool.label
2152
- })]
2153
- }, tool.label))
2154
- }),
2155
- /* @__PURE__ */ jsx("p", {
2156
- className: "text-base leading-normal text-po-dim sm:text-sm",
2157
- 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."
2158
- }),
2159
- error ? /* @__PURE__ */ jsx("output", {
2160
- className: "block text-base text-po-red",
2161
- children: error
2162
- }) : null
2163
- ]
2164
- });
2165
- }
2166
- //#endregion
2167
- //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorPanel.tsx
2168
- function AuthorPanel({ sessionId, submitCapture, statuses, renderSlot, renderSessionActivity }) {
2169
- const documents = useStore(authorWorkspace.store, (state) => {
2170
- if (sessionId === null) return [];
2171
- const prefix = `${sessionId}\n`;
2172
- return Object.entries(state.documents).filter(([key]) => key.startsWith(prefix)).map(([, document]) => document);
2173
- });
2174
- const workspace = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
2175
- const grid = useStore(authorGrid.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
2176
- const focused = documents.find((document) => document.path === workspace?.focusedDocument?.path);
2177
- const focusedPath = workspace?.focusedDocument?.path;
2178
- const requests = (workspace?.requests ?? []).filter((request) => focusedPath === void 0 || request.documentPath === focusedPath);
2179
- const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, focusedPath ?? "", state));
2180
- const activity = sessionId !== null && focusedPath !== void 0 && requests.some((request) => request.status === "REQUESTED" || request.status === "CHANGING") ? renderSessionActivity?.() ?? null : null;
2181
- return /* @__PURE__ */ jsxs("section", {
2182
- "data-testid": "author-panel",
2183
- className: "flex min-h-0 flex-1 flex-col overflow-hidden",
2184
- children: [/* @__PURE__ */ jsx("div", {
2185
- className: "min-h-0 flex-1 overflow-y-auto p-3",
2186
- children: /* @__PURE__ */ jsxs("div", {
2187
- className: "flex flex-col gap-3",
2188
- children: [
2189
- /* @__PURE__ */ jsx("div", {
2190
- "data-testid": "author-preview-providers",
2191
- className: "empty:hidden",
2192
- children: renderSlot("author.preview-provider")
2193
- }),
2194
- sessionId !== null && workspace !== void 0 && focused !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
2195
- /* @__PURE__ */ jsx(AuthorToolPalette, {
2196
- sessionId,
2197
- path: focused.path,
2198
- kind: focused.kind,
2199
- activeTool: workspace.toolsByDocument[normalizeAuthorPath(focused.path)] ?? "select"
2200
- }),
2201
- /* @__PURE__ */ jsx("h2", {
2202
- className: "border-t border-po-border-soft pt-3 text-lg font-semibold text-po-hi",
2203
- children: "Annotations"
2204
- }),
2205
- /* @__PURE__ */ jsx("p", {
2206
- className: "text-base leading-normal text-po-dim sm:text-sm",
2207
- children: "Drag a region or place a point, add feedback, then submit here. Requests queue while the agent is working."
2208
- }),
2209
- focused.kind === "video" ? /* @__PURE__ */ jsx("p", {
2210
- className: "text-base text-po-dim sm:text-sm",
2211
- children: "Annotations reference video frames. They do not save changes to the source video."
2212
- }) : null,
2213
- autonomousVoiceGridVisible(statuses) && grid !== void 0 ? /* @__PURE__ */ jsxs("div", {
2214
- "data-testid": "author-grid-snapshot",
2215
- className: "rounded border border-po-red/40 bg-po-red/5 p-2 text-base text-po-dim",
2216
- children: [/* @__PURE__ */ jsx("strong", {
2217
- className: "text-po-red",
2218
- children: "VOICE GRID A1–H8"
2219
- }), /* @__PURE__ */ jsxs("p", {
2220
- className: "mt-1 truncate",
2221
- children: ["token ", grid.geometryToken]
2222
- })]
2223
- }) : null,
2224
- /* @__PURE__ */ jsx("div", {
2225
- className: "max-sm:hidden",
2226
- children: /* @__PURE__ */ jsx(AuthorFeedbackControls, {
2227
- sessionId,
2228
- document: focused,
2229
- drafts,
2230
- submitCapture
2231
- }, focused.path)
2232
- })
2233
- ] }) : null,
2234
- /* @__PURE__ */ jsx("div", {
2235
- className: focused === void 0 ? void 0 : "border-t border-po-border-soft pt-3",
2236
- children: /* @__PURE__ */ jsx(AuthorRequestLog, { requests })
2237
- })
2238
- ]
2239
- })
2240
- }), activity === null ? null : /* @__PURE__ */ jsx("footer", {
2241
- "data-testid": "author-session-activity",
2242
- className: "shrink-0 border-t border-po-border bg-po-rail p-3",
2243
- children: activity
2244
- })]
2245
- });
2246
- }
2247
- //#endregion
2248
- //#region src/extensions/workspaces/sessions/(frontend)/dock/authoring.web.tsx
2249
- var authoring_web_default = defineDockFace({
2250
- id: "authoring",
2251
- label: "Author",
2252
- order: 30,
2253
- autoSelect: ({ tabId }) => tabId?.startsWith(AUTHOR_FILE_TAB_PREFIX) === true,
2254
- panel: AuthorPanel,
2255
- visibility: {
2256
- subscribe(listener) {
2257
- const subscription = authorWorkspace.store.subscribe(listener);
2258
- return () => subscription.unsubscribe();
2259
- },
2260
- isVisible(sessionId) {
2261
- return sessionId !== null && authorWorkspace.store.state.sessions[sessionId]?.focusedDocument !== void 0;
2262
- }
2263
- }
2264
- });
2265
- //#endregion
2266
2066
  //#region src/types/apiRoutes.ts
2267
2067
  /**
2268
2068
  * This package's routes, as data.
@@ -2373,27 +2173,6 @@ const api = createApiClient(apiRoutes_default, {
2373
2173
  sessionAddress: sessionApiAddress
2374
2174
  });
2375
2175
  //#endregion
2376
- //#region src/types/authorPreview.ts
2377
- function parseAuthorOpenSourceAction(input) {
2378
- if (typeof input !== "object" || input === null) return null;
2379
- const candidate = input;
2380
- return candidate.version === 1 && typeof candidate.createTab === "function" ? candidate : null;
2381
- }
2382
- function parseAuthorPreviewAction(input) {
2383
- if (typeof input !== "object" || input === null) return null;
2384
- const candidate = input;
2385
- if (candidate.version !== 1 || typeof candidate.label !== "string" || candidate.label.trim() === "") return null;
2386
- if (candidate.detail !== void 0 && typeof candidate.detail !== "string") return null;
2387
- if (typeof candidate.createTab !== "function") return null;
2388
- if (candidate.supportsSource !== void 0 && typeof candidate.supportsSource !== "function") return null;
2389
- if (candidate.embeddedPanel !== void 0 && typeof candidate.embeddedPanel !== "function") return null;
2390
- return candidate;
2391
- }
2392
- const authorPreviewActionSlot = {
2393
- slot: "author.preview-action",
2394
- parse: parseAuthorPreviewAction
2395
- };
2396
- //#endregion
2397
2176
  //#region src/types/authorApi.ts
2398
2177
  /** Query parameter naming the session a document request belongs to. */
2399
2178
  const SESSION_QUERY_PARAM = "session";
@@ -2422,6 +2201,10 @@ function structuredFormat(path) {
2422
2201
  if (lower.endsWith(".pptx")) return "pptx";
2423
2202
  if (lower.endsWith(".xlsx")) return "xlsx";
2424
2203
  }
2204
+ /** A Mermaid source file: edited as text, previewed as the diagram it draws. */
2205
+ function isMermaidPath(path) {
2206
+ return /\.(?:mmd|mermaid)$/iu.test(path);
2207
+ }
2425
2208
  function authorKindForPath(path) {
2426
2209
  const lower = path.toLowerCase();
2427
2210
  if (/\.stories\.(?:js|jsx|ts|tsx)$/u.test(lower)) return "story-preview";
@@ -2429,7 +2212,7 @@ function authorKindForPath(path) {
2429
2212
  if (format === "markdown-slides") return "slides";
2430
2213
  if (format !== void 0) return format;
2431
2214
  const extension = lower.split(".").at(-1) ?? "";
2432
- if (extension === "md") return "markdown";
2215
+ if (extension === "md" || isMermaidPath(lower)) return "markdown";
2433
2216
  if (IMAGE_EXTENSIONS.has(extension)) return "image";
2434
2217
  if (VIDEO_EXTENSIONS.has(extension)) return "video";
2435
2218
  if (extension === "pdf") return "pdf";
@@ -2460,6 +2243,27 @@ function authorSessionFileUrl(sessionId, path) {
2460
2243
  if (path.startsWith("artifact:")) return artifactApi.session(sessionId).content.url({ query: { path: path.slice(9) } });
2461
2244
  return api.session(sessionId).file.url({ query: { path } });
2462
2245
  }
2246
+ /**
2247
+ * A Markdown image path read against the document's own folder, as GitHub reads
2248
+ * it, keeping the `artifact:` scheme. Artifact paths are normalised here because
2249
+ * the artifact route refuses `..`; it still owns containment.
2250
+ */
2251
+ function authorMarkdownImagePath(documentPath, imagePath) {
2252
+ const artifact = documentPath.startsWith(AUTHOR_ARTIFACT_PREFIX);
2253
+ const local = artifact ? documentPath.slice(9) : documentPath;
2254
+ if (imagePath.startsWith("/")) {
2255
+ if (artifact) throw new Error("Artifact images must use a relative path.");
2256
+ return imagePath;
2257
+ }
2258
+ const segments = local.split("/").slice(0, -1);
2259
+ for (const segment of imagePath.split("/")) {
2260
+ if (segment === "" || segment === ".") continue;
2261
+ if (segment !== "..") segments.push(segment);
2262
+ else if (segments.length === 0 || segments.at(-1) === "") throw new Error("The image is outside this folder.");
2263
+ else segments.pop();
2264
+ }
2265
+ return `${artifact ? AUTHOR_ARTIFACT_PREFIX : ""}${segments.join("/")}`;
2266
+ }
2463
2267
  /** The file name a person sees: the last path segment, without the `artifact:` scheme. */
2464
2268
  function authorFileName(path) {
2465
2269
  const local = path.startsWith("artifact:") ? path.slice(9) : path;
@@ -2575,55 +2379,292 @@ async function croppedImageBytes(documentInput, signal) {
2575
2379
  bitmap.close();
2576
2380
  }
2577
2381
  }
2578
- async function bytesToSave(sessionId, document, signal) {
2579
- if (document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview") return document.content ?? "";
2580
- if (document.kind === "image" && document.crop !== void 0) return await croppedImageBytes(document, signal);
2581
- if (document.structuredFormat === void 0) throw new Error("This Author view cannot be saved.");
2582
- const operations = operationsFor(document);
2583
- const request = {
2584
- path: document.path,
2585
- format: document.structuredFormat,
2586
- operations,
2587
- ...document.csvDialect === void 0 ? {} : { csvDialect: document.csvDialect }
2588
- };
2589
- const preflight = await documentRequest(api.session(sessionId).documentsPreflight, sessionId, request, signal);
2590
- if (preflight.sourceDigest !== void 0 && preflight.sourceDigest !== document.sourceSha256) throw new Error("Document source changed before saving. Reopen the document.");
2591
- if (!preflight.accepted) throw new Error(preflight.issues.map((issue) => issue.message).join("\n") || "Author preflight rejected the edit.");
2592
- const serialized = await documentRequest(api.session(sessionId).documentsSerialize, sessionId, {
2593
- ...request,
2594
- preflightDigest: preflight.digest
2595
- }, signal);
2596
- if (serialized.encoding !== "base64") throw new Error("Author serialize returned an unsupported encoding.");
2597
- const bytes = decodeBase64(serialized.bytes);
2598
- return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2382
+ async function bytesToSave(sessionId, document, signal) {
2383
+ if (document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview") return document.content ?? "";
2384
+ if (document.kind === "image" && document.crop !== void 0) return await croppedImageBytes(document, signal);
2385
+ if (document.structuredFormat === void 0) throw new Error("This Author view cannot be saved.");
2386
+ const operations = operationsFor(document);
2387
+ const request = {
2388
+ path: document.path,
2389
+ format: document.structuredFormat,
2390
+ operations,
2391
+ ...document.csvDialect === void 0 ? {} : { csvDialect: document.csvDialect }
2392
+ };
2393
+ const preflight = await documentRequest(api.session(sessionId).documentsPreflight, sessionId, request, signal);
2394
+ if (preflight.sourceDigest !== void 0 && preflight.sourceDigest !== document.sourceSha256) throw new Error("Document source changed before saving. Reopen the document.");
2395
+ if (!preflight.accepted) throw new Error(preflight.issues.map((issue) => issue.message).join("\n") || "Author preflight rejected the edit.");
2396
+ const serialized = await documentRequest(api.session(sessionId).documentsSerialize, sessionId, {
2397
+ ...request,
2398
+ preflightDigest: preflight.digest
2399
+ }, signal);
2400
+ if (serialized.encoding !== "base64") throw new Error("Author serialize returned an unsupported encoding.");
2401
+ const bytes = decodeBase64(serialized.bytes);
2402
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2403
+ }
2404
+ async function saveAuthorDocument(sessionId, document, signal) {
2405
+ if (document.path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
2406
+ if (document.sourceSha256 === void 0) throw new Error("This Author view has no source digest.");
2407
+ const response = await sealedTransport.fetch(authorSessionFileUrl(sessionId, document.path), {
2408
+ method: "PUT",
2409
+ headers: {
2410
+ "Content-Type": "application/octet-stream",
2411
+ "X-Expected-SHA256": document.sourceSha256
2412
+ },
2413
+ body: await bytesToSave(sessionId, document, signal),
2414
+ signal
2415
+ });
2416
+ if (!response.ok) throw new Error(`Author save failed (${response.status})`);
2417
+ const sha256 = response.headers.get("X-File-SHA256");
2418
+ if (sha256 === null || !/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Author save returned an invalid digest.");
2419
+ return sha256;
2420
+ }
2421
+ function csvDialectFromMetadata(metadata) {
2422
+ if (metadata === void 0) return void 0;
2423
+ const { delimiter, quote, recordDelimiter } = metadata;
2424
+ if (delimiter !== "," && delimiter !== ";" && delimiter !== " " && delimiter !== "|" || quote !== "\"" && quote !== "'" || recordDelimiter !== "\n" && recordDelimiter !== "\r\n") throw new Error("Author document API returned an invalid CSV dialect.");
2425
+ return {
2426
+ delimiter,
2427
+ quote,
2428
+ recordDelimiter
2429
+ };
2430
+ }
2431
+ //#endregion
2432
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorToolPalette.tsx
2433
+ /** One tool tile. An active annotation tool is the person's mark (rose); an active view tool is Po's jade. */
2434
+ function authorToolClassName(active, annotates) {
2435
+ return `h-auto min-h-11 min-w-11 flex-col gap-1 rounded-md px-1 py-2 text-base [@media(pointer:fine)]:min-h-8 ${!active ? "border-po-border-soft bg-po-panel text-po-dim" : annotates ? "border-po-edge-magenta bg-po-tint-magenta font-semibold text-po-magenta hover:text-po-magenta" : "border-po-edge-green bg-po-tint-green font-semibold text-po-green hover:text-po-green"}`;
2436
+ }
2437
+ function AuthorToolPalette({ sessionId, path, kind, activeTool }) {
2438
+ const [error, setError] = useState();
2439
+ const format = (style) => {
2440
+ const selected = authorDocumentAnnotations(sessionId, path).candidate;
2441
+ const document = selected && authorDocument(sessionId, selected.documentPath);
2442
+ if (!selected || !document || selected.anchor.kind !== "text-range" || selected.revision !== document.version || selected.sourceSha256 !== document.sourceSha256) {
2443
+ setError("Select text in the document first.");
2444
+ return;
2445
+ }
2446
+ const { startOffset, endOffset } = selected.anchor;
2447
+ const content = document.content ?? "";
2448
+ const text = content.slice(startOffset, endOffset);
2449
+ const replacement = style === "Bold" ? `**${text}**` : style === "Link" ? `[${text}](https://)` : text.split("\n").map((line) => `${style === "Heading" ? "## " : "- "}${line}`).join("\n");
2450
+ reviseAuthorDocument(sessionId, document.path, content.slice(0, startOffset) + replacement + content.slice(endOffset));
2451
+ setAuthorRegionCandidate(sessionId, path, void 0);
2452
+ setError(void 0);
2453
+ };
2454
+ const tools = [
2455
+ ...[
2456
+ "image",
2457
+ "pdf",
2458
+ "video",
2459
+ "story-preview"
2460
+ ].includes(kind) ? [{
2461
+ label: "Pan",
2462
+ glyph: "✋",
2463
+ action: () => setAuthorToolMode(sessionId, path, activeTool === "pan" ? "select" : "pan"),
2464
+ active: activeTool === "pan",
2465
+ annotates: false
2466
+ }, {
2467
+ label: "Draw",
2468
+ glyph: "✎",
2469
+ action: () => setAuthorToolMode(sessionId, path, activeTool === "draw" ? "select" : "draw"),
2470
+ active: activeTool === "draw",
2471
+ annotates: true
2472
+ }] : [],
2473
+ {
2474
+ label: "Region",
2475
+ glyph: "⌗",
2476
+ action: () => setAuthorToolMode(sessionId, path, activeTool === "mark" ? "select" : "mark"),
2477
+ active: activeTool === "mark",
2478
+ annotates: true
2479
+ },
2480
+ ...kind === "markdown" && !isMermaidPath(path) ? [
2481
+ {
2482
+ label: "Bold",
2483
+ glyph: "B",
2484
+ action: () => format("Bold")
2485
+ },
2486
+ {
2487
+ label: "Heading",
2488
+ glyph: "H",
2489
+ action: () => format("Heading")
2490
+ },
2491
+ {
2492
+ label: "Link",
2493
+ glyph: "↗",
2494
+ action: () => format("Link")
2495
+ },
2496
+ {
2497
+ label: "List",
2498
+ glyph: "≡",
2499
+ action: () => format("List")
2500
+ }
2501
+ ] : [],
2502
+ {
2503
+ label: "Comment",
2504
+ glyph: "●",
2505
+ action: () => setAuthorToolMode(sessionId, path, activeTool === "comment" ? "select" : "comment"),
2506
+ active: activeTool === "comment",
2507
+ annotates: true
2508
+ }
2509
+ ];
2510
+ return /* @__PURE__ */ jsxs("section", {
2511
+ "data-testid": "author-tool-palette",
2512
+ className: "space-y-2",
2513
+ children: [
2514
+ /* @__PURE__ */ jsx("h3", {
2515
+ className: "sr-only",
2516
+ children: "Tools"
2517
+ }),
2518
+ /* @__PURE__ */ jsx("div", {
2519
+ className: "grid grid-cols-4 gap-1.5",
2520
+ children: tools.map((tool) => /* @__PURE__ */ jsxs(Button, {
2521
+ size: "md",
2522
+ variant: "outline",
2523
+ "aria-label": tool.label === "Region" ? "mark region" : tool.label,
2524
+ "aria-pressed": tool.active,
2525
+ onClick: tool.action,
2526
+ className: authorToolClassName(tool.active === true, tool.annotates === true),
2527
+ children: [/* @__PURE__ */ jsx("span", {
2528
+ "aria-hidden": "true",
2529
+ className: "text-lg leading-none",
2530
+ children: tool.glyph
2531
+ }), /* @__PURE__ */ jsx("span", {
2532
+ className: "text-sm",
2533
+ children: tool.label
2534
+ })]
2535
+ }, tool.label))
2536
+ }),
2537
+ /* @__PURE__ */ jsx("p", {
2538
+ className: "text-base leading-normal text-po-dim sm:text-sm",
2539
+ 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."
2540
+ }),
2541
+ error ? /* @__PURE__ */ jsx("output", {
2542
+ className: "block text-base text-po-red",
2543
+ children: error
2544
+ }) : null
2545
+ ]
2546
+ });
2547
+ }
2548
+ //#endregion
2549
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorPanel.tsx
2550
+ function AuthorPanel({ sessionId, submitCapture, statuses, renderSlot, renderSessionActivity }) {
2551
+ const documents = useStore(authorWorkspace.store, (state) => {
2552
+ if (sessionId === null) return [];
2553
+ const prefix = `${sessionId}\n`;
2554
+ return Object.entries(state.documents).filter(([key]) => key.startsWith(prefix)).map(([, document]) => document);
2555
+ });
2556
+ const workspace = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
2557
+ const grid = useStore(authorGrid.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
2558
+ const focused = documents.find((document) => document.path === workspace?.focusedDocument?.path);
2559
+ const focusedPath = workspace?.focusedDocument?.path;
2560
+ const requests = (workspace?.requests ?? []).filter((request) => focusedPath === void 0 || request.documentPath === focusedPath);
2561
+ const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, focusedPath ?? "", state));
2562
+ const activity = sessionId !== null && focusedPath !== void 0 && requests.some((request) => request.status === "REQUESTED" || request.status === "CHANGING") ? renderSessionActivity?.() ?? null : null;
2563
+ return /* @__PURE__ */ jsxs("section", {
2564
+ "data-testid": "author-panel",
2565
+ className: "flex min-h-0 flex-1 flex-col overflow-hidden",
2566
+ children: [/* @__PURE__ */ jsx("div", {
2567
+ className: "min-h-0 flex-1 overflow-y-auto p-3",
2568
+ children: /* @__PURE__ */ jsxs("div", {
2569
+ className: "flex flex-col gap-3",
2570
+ children: [
2571
+ /* @__PURE__ */ jsx("div", {
2572
+ "data-testid": "author-preview-providers",
2573
+ className: "empty:hidden",
2574
+ children: renderSlot("author.preview-provider")
2575
+ }),
2576
+ sessionId !== null && workspace !== void 0 && focused !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
2577
+ /* @__PURE__ */ jsx(AuthorToolPalette, {
2578
+ sessionId,
2579
+ path: focused.path,
2580
+ kind: focused.kind,
2581
+ activeTool: workspace.toolsByDocument[normalizeAuthorPath(focused.path)] ?? "select"
2582
+ }),
2583
+ /* @__PURE__ */ jsx("h2", {
2584
+ className: "border-t border-po-border-soft pt-3 text-lg font-semibold text-po-hi",
2585
+ children: "Annotations"
2586
+ }),
2587
+ /* @__PURE__ */ jsx("p", {
2588
+ className: "text-base leading-normal text-po-dim sm:text-sm",
2589
+ children: "Drag a region or place a point, add feedback, then submit here. Requests queue while the agent is working."
2590
+ }),
2591
+ focused.kind === "video" ? /* @__PURE__ */ jsx("p", {
2592
+ className: "text-base text-po-dim sm:text-sm",
2593
+ children: "Annotations reference video frames. They do not save changes to the source video."
2594
+ }) : null,
2595
+ autonomousVoiceGridVisible(statuses) && grid !== void 0 ? /* @__PURE__ */ jsxs("div", {
2596
+ "data-testid": "author-grid-snapshot",
2597
+ className: "rounded border border-po-red/40 bg-po-red/5 p-2 text-base text-po-dim",
2598
+ children: [/* @__PURE__ */ jsx("strong", {
2599
+ className: "text-po-red",
2600
+ children: "VOICE GRID A1–H8"
2601
+ }), /* @__PURE__ */ jsxs("p", {
2602
+ className: "mt-1 truncate",
2603
+ children: ["token ", grid.geometryToken]
2604
+ })]
2605
+ }) : null,
2606
+ /* @__PURE__ */ jsx("div", {
2607
+ className: "max-sm:hidden",
2608
+ children: /* @__PURE__ */ jsx(AuthorFeedbackControls, {
2609
+ sessionId,
2610
+ document: focused,
2611
+ drafts,
2612
+ submitCapture
2613
+ }, focused.path)
2614
+ })
2615
+ ] }) : null,
2616
+ /* @__PURE__ */ jsx("div", {
2617
+ className: focused === void 0 ? void 0 : "border-t border-po-border-soft pt-3",
2618
+ children: /* @__PURE__ */ jsx(AuthorRequestLog, { requests })
2619
+ })
2620
+ ]
2621
+ })
2622
+ }), activity === null ? null : /* @__PURE__ */ jsx("footer", {
2623
+ "data-testid": "author-session-activity",
2624
+ className: "shrink-0 border-t border-po-border bg-po-rail p-3",
2625
+ children: activity
2626
+ })]
2627
+ });
2599
2628
  }
2600
- async function saveAuthorDocument(sessionId, document, signal) {
2601
- if (document.path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
2602
- if (document.sourceSha256 === void 0) throw new Error("This Author view has no source digest.");
2603
- const response = await sealedTransport.fetch(authorSessionFileUrl(sessionId, document.path), {
2604
- method: "PUT",
2605
- headers: {
2606
- "Content-Type": "application/octet-stream",
2607
- "X-Expected-SHA256": document.sourceSha256
2629
+ //#endregion
2630
+ //#region src/extensions/workspaces/sessions/(frontend)/dock/authoring.web.tsx
2631
+ var authoring_web_default = defineDockFace({
2632
+ id: "authoring",
2633
+ label: "Author",
2634
+ order: 30,
2635
+ autoSelect: ({ tabId }) => tabId?.startsWith(AUTHOR_FILE_TAB_PREFIX) === true,
2636
+ panel: AuthorPanel,
2637
+ visibility: {
2638
+ subscribe(listener) {
2639
+ const subscription = authorWorkspace.store.subscribe(listener);
2640
+ return () => subscription.unsubscribe();
2608
2641
  },
2609
- body: await bytesToSave(sessionId, document, signal),
2610
- signal
2611
- });
2612
- if (!response.ok) throw new Error(`Author save failed (${response.status})`);
2613
- const sha256 = response.headers.get("X-File-SHA256");
2614
- if (sha256 === null || !/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Author save returned an invalid digest.");
2615
- return sha256;
2642
+ isVisible(sessionId) {
2643
+ return sessionId !== null && authorWorkspace.store.state.sessions[sessionId]?.focusedDocument !== void 0;
2644
+ }
2645
+ }
2646
+ });
2647
+ //#endregion
2648
+ //#region src/types/authorPreview.ts
2649
+ function parseAuthorOpenSourceAction(input) {
2650
+ if (typeof input !== "object" || input === null) return null;
2651
+ const candidate = input;
2652
+ return candidate.version === 1 && typeof candidate.createTab === "function" ? candidate : null;
2616
2653
  }
2617
- function csvDialectFromMetadata(metadata) {
2618
- if (metadata === void 0) return void 0;
2619
- const { delimiter, quote, recordDelimiter } = metadata;
2620
- if (delimiter !== "," && delimiter !== ";" && delimiter !== " " && delimiter !== "|" || quote !== "\"" && quote !== "'" || recordDelimiter !== "\n" && recordDelimiter !== "\r\n") throw new Error("Author document API returned an invalid CSV dialect.");
2621
- return {
2622
- delimiter,
2623
- quote,
2624
- recordDelimiter
2625
- };
2654
+ function parseAuthorPreviewAction(input) {
2655
+ if (typeof input !== "object" || input === null) return null;
2656
+ const candidate = input;
2657
+ if (candidate.version !== 1 || typeof candidate.label !== "string" || candidate.label.trim() === "") return null;
2658
+ if (candidate.detail !== void 0 && typeof candidate.detail !== "string") return null;
2659
+ if (typeof candidate.createTab !== "function") return null;
2660
+ if (candidate.supportsSource !== void 0 && typeof candidate.supportsSource !== "function") return null;
2661
+ if (candidate.embeddedPanel !== void 0 && typeof candidate.embeddedPanel !== "function") return null;
2662
+ return candidate;
2626
2663
  }
2664
+ const authorPreviewActionSlot = {
2665
+ slot: "author.preview-action",
2666
+ parse: parseAuthorPreviewAction
2667
+ };
2627
2668
  //#endregion
2628
2669
  //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorGridTools.ts
2629
2670
  const stringProperty = {
@@ -3036,6 +3077,9 @@ function authorProfilesForDocument(sessionId, path, kind) {
3036
3077
  * declares as `file` with `host: true`; this package serves no bytes of its
3037
3078
  * own. The transport stays raw because what comes back is a blob, and a call
3038
3079
  * through the generated client reads the body as JSON to report a refusal.
3080
+ *
3081
+ * An SVG comes back as a sized data URL (see `svgImageUrl`), so it has an
3082
+ * intrinsic size and a region capture can still draw it to a canvas.
3039
3083
  */
3040
3084
  async function loadAuthorMedia(source, signal, imageData) {
3041
3085
  let blob;
@@ -3047,6 +3091,14 @@ async function loadAuthorMedia(source, signal, imageData) {
3047
3091
  });
3048
3092
  if (!response.ok) throw new Error(`Author media could not be loaded (${response.status}).`);
3049
3093
  blob = await response.blob();
3094
+ if (blob.type.startsWith("image/svg+xml")) {
3095
+ const url = svgImageUrl(await blob.text());
3096
+ signal.throwIfAborted();
3097
+ return {
3098
+ url,
3099
+ dispose() {}
3100
+ };
3101
+ }
3050
3102
  }
3051
3103
  signal.throwIfAborted();
3052
3104
  const url = URL.createObjectURL(blob);
@@ -3954,7 +4006,7 @@ function AuthorStructuredView({ sessionId, document, displayedRegions }) {
3954
4006
  }
3955
4007
  //#endregion
3956
4008
  //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorTextView.tsx
3957
- function AuthorTextView({ sessionId, document, preview, displayedRegions }) {
4009
+ function AuthorTextView({ sessionId, document, preview, displayedRegions, renderMediaReview }) {
3958
4010
  const host = useRef(null);
3959
4011
  const editor = useRef(null);
3960
4012
  useEffect(() => registerAuthorGridResolver(sessionId, (cell, geometry) => {
@@ -3986,6 +4038,13 @@ function AuthorTextView({ sessionId, document, preview, displayedRegions }) {
3986
4038
  }] : []));
3987
4039
  }, [displayedRegions]);
3988
4040
  const lines = (document.content ?? "").split("\n").length;
4041
+ const loadImage = useCallback(async (imagePath) => {
4042
+ const path = authorMarkdownImagePath(document.path, imagePath);
4043
+ return {
4044
+ ...await loadAuthorMedia(authorSessionFileUrl(sessionId, path), new AbortController().signal),
4045
+ path
4046
+ };
4047
+ }, [sessionId, document.path]);
3989
4048
  return /* @__PURE__ */ jsxs("div", {
3990
4049
  ref: host,
3991
4050
  className: "flex min-h-0 flex-1 flex-col",
@@ -3994,7 +4053,12 @@ function AuthorTextView({ sessionId, document, preview, displayedRegions }) {
3994
4053
  className: "min-h-0 flex-1 overflow-auto bg-po-deep/50 p-6",
3995
4054
  children: /* @__PURE__ */ jsx("article", {
3996
4055
  className: "mx-auto min-h-full max-w-3xl rounded-md bg-po-panel px-6 py-8 sm:px-10 text-lg leading-relaxed text-po-text shadow-sm ring-1 ring-po-border-soft",
3997
- children: /* @__PURE__ */ jsx(Markdown, { text: document.content ?? "" })
4056
+ children: /* @__PURE__ */ jsx(Markdown, {
4057
+ text: isMermaidPath(document.path) ? `\`\`\`\`mermaid\n${document.content ?? ""}\n\`\`\`\`` : document.content ?? "",
4058
+ loadImage,
4059
+ documentPath: document.path,
4060
+ renderMediaReview
4061
+ })
3998
4062
  })
3999
4063
  }) : /* @__PURE__ */ jsx(CodeEditor, {
4000
4064
  value: document.content ?? "",
@@ -4160,7 +4224,8 @@ function AuthorDocumentPanel(props) {
4160
4224
  sessionId,
4161
4225
  document,
4162
4226
  preview: document.kind === "markdown" && markdownPreview && activeTool === "select" && !gridVisible,
4163
- displayedRegions
4227
+ displayedRegions,
4228
+ renderMediaReview: markdownMediaReview(props)
4164
4229
  });
4165
4230
  const sourcePanel = document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview" ? textPanel : document.structuredFormat !== void 0 ? /* @__PURE__ */ jsx(AuthorStructuredView, {
4166
4231
  sessionId,
@@ -4617,6 +4682,147 @@ var ImageFeedback_author_image_feedback_web_default = defineFill({
4617
4682
  }
4618
4683
  });
4619
4684
  //#endregion
4685
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorMarkdownMedia.ts
4686
+ const MAX_DIMENSION = 4096;
4687
+ const MERMAID_SOURCE_MAX_CHARACTERS = 8192;
4688
+ function base64(bytes) {
4689
+ let binary = "";
4690
+ for (let index = 0; index < bytes.length; index += 32768) binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
4691
+ return btoa(binary);
4692
+ }
4693
+ /**
4694
+ * A picture opened from rendered Markdown, as an in-memory Author image.
4695
+ *
4696
+ * Every picture becomes PNG bytes held in the document, so annotation and
4697
+ * capture work the same for a raster image, an SVG and a mermaid diagram, and
4698
+ * nothing is written back. The path is synthetic and keyed by content, so the
4699
+ * same picture reopens with its unsent drafts.
4700
+ */
4701
+ async function authorMarkdownMediaDocument(media, signal) {
4702
+ const image = new Image();
4703
+ image.src = await drawableImageUrl(media.url, signal);
4704
+ await image.decode();
4705
+ signal.throwIfAborted();
4706
+ if (image.naturalWidth === 0 || image.naturalHeight === 0) throw new Error("This picture has no size to annotate.");
4707
+ const scale = Math.min(1, MAX_DIMENSION / Math.max(image.naturalWidth, image.naturalHeight));
4708
+ const canvas = document.createElement("canvas");
4709
+ canvas.width = Math.max(1, Math.round(image.naturalWidth * scale));
4710
+ canvas.height = Math.max(1, Math.round(image.naturalHeight * scale));
4711
+ const context = canvas.getContext("2d");
4712
+ if (context === null) throw new Error("Canvas is unavailable.");
4713
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
4714
+ let png;
4715
+ try {
4716
+ png = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
4717
+ } catch {
4718
+ throw new Error("This picture comes from another site and cannot be annotated.");
4719
+ }
4720
+ if (png === null) throw new Error("This picture could not be prepared for annotation.");
4721
+ const bytes = new Uint8Array(await png.arrayBuffer());
4722
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
4723
+ signal.throwIfAborted();
4724
+ const sha256 = Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
4725
+ return {
4726
+ path: `markdown-media/${sha256}.png`,
4727
+ kind: "image",
4728
+ title: media.title,
4729
+ sourceSha256: sha256,
4730
+ imageData: base64(bytes),
4731
+ mediaUrl: `markdown-media:${sha256}`,
4732
+ markdownMedia: {
4733
+ kind: media.kind,
4734
+ ...media.sourcePath === void 0 ? {} : { sourcePath: media.sourcePath },
4735
+ ...media.documentPath === void 0 ? {} : { documentPath: media.documentPath },
4736
+ ...media.mermaidSource === void 0 ? {} : { mermaidSource: media.mermaidSource.slice(0, MERMAID_SOURCE_MAX_CHARACTERS) }
4737
+ }
4738
+ };
4739
+ }
4740
+ //#endregion
4741
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorMediaReviewPanel.tsx
4742
+ /**
4743
+ * Pan, annotate and send feedback on a picture opened from rendered Markdown.
4744
+ *
4745
+ * It fills the full-screen viewer, which covers the Author rail, so the tools
4746
+ * and the submit button live beside the canvas here rather than in the rail.
4747
+ */
4748
+ function AuthorMediaReviewPanel({ media, sessionId, submitCapture }) {
4749
+ const [document, setDocument] = useState();
4750
+ const [error, setError] = useState();
4751
+ useEffect(() => {
4752
+ setDocument(void 0);
4753
+ setError(void 0);
4754
+ if (sessionId === null) return;
4755
+ const controller = new AbortController();
4756
+ let generation;
4757
+ authorMarkdownMediaDocument(media, controller.signal).then((input) => {
4758
+ if (controller.signal.aborted) return;
4759
+ setDocument(putAuthorDocument(sessionId, input));
4760
+ generation = focusAuthorDocument(sessionId, input.path);
4761
+ }).catch((reason) => {
4762
+ if (!controller.signal.aborted) setError(reason instanceof Error ? reason.message : String(reason));
4763
+ });
4764
+ return () => {
4765
+ controller.abort();
4766
+ if (generation !== void 0) releaseAuthorDocumentFocus(sessionId, generation);
4767
+ };
4768
+ }, [media, sessionId]);
4769
+ const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, document?.path ?? "", state));
4770
+ const tool = useStore(authorWorkspace.store, (state) => sessionId === null ? "select" : state.sessions[sessionId]?.toolsByDocument[document?.path ?? ""] ?? "select");
4771
+ if (sessionId === null) return /* @__PURE__ */ jsx("output", {
4772
+ className: "p-4 text-po-dim",
4773
+ children: "Open a session to review this picture."
4774
+ });
4775
+ if (error) return /* @__PURE__ */ jsx("output", {
4776
+ className: "p-4 text-po-red",
4777
+ children: error
4778
+ });
4779
+ if (!document) return /* @__PURE__ */ jsx("output", {
4780
+ className: "p-4 text-po-dim",
4781
+ children: "Preparing the picture for review…"
4782
+ });
4783
+ const displayedRegions = drafts.annotations.map((region, index) => ({
4784
+ ordinal: index + 1,
4785
+ region
4786
+ }));
4787
+ return /* @__PURE__ */ jsxs("section", {
4788
+ "data-testid": "author-media-review",
4789
+ className: "flex min-h-0 flex-1 flex-col sm:flex-row",
4790
+ children: [/* @__PURE__ */ jsx("div", {
4791
+ className: "flex min-h-0 flex-1 flex-col",
4792
+ children: /* @__PURE__ */ jsx(AuthorMediaView, {
4793
+ sessionId,
4794
+ document,
4795
+ activeTool: tool,
4796
+ displayedRegions,
4797
+ pendingCandidate: drafts.candidate !== void 0
4798
+ })
4799
+ }), /* @__PURE__ */ jsxs("aside", {
4800
+ className: "flex max-h-[45dvh] shrink-0 flex-col gap-4 overflow-y-auto border-t border-po-border bg-po-bg p-4 sm:max-h-none sm:w-80 sm:border-t-0 sm:border-l",
4801
+ children: [/* @__PURE__ */ jsx(AuthorToolPalette, {
4802
+ sessionId,
4803
+ path: document.path,
4804
+ kind: "image",
4805
+ activeTool: tool
4806
+ }), /* @__PURE__ */ jsx(AuthorFeedbackControls, {
4807
+ sessionId,
4808
+ document,
4809
+ drafts,
4810
+ submitCapture
4811
+ })]
4812
+ })]
4813
+ });
4814
+ }
4815
+ //#endregion
4816
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/MediaReview.author.media-review.web.ts
4817
+ var MediaReview_author_media_review_web_default = defineFill({
4818
+ slot: "author.media-review",
4819
+ id: "media-review",
4820
+ data: {
4821
+ version: 1,
4822
+ panel: AuthorMediaReviewPanel
4823
+ }
4824
+ });
4825
+ //#endregion
4620
4826
  //#region src/extensions/workspaces/sessions/(frontend)/leader/author-toggle.web.ts
4621
4827
  var author_toggle_web_default = defineLeaderBinding({
4622
4828
  id: "author.toggle",
@@ -4890,6 +5096,13 @@ var image_feedback_web_default = defineSlotFile({
4890
5096
  parse: parseAuthorImageFeedbackContribution
4891
5097
  });
4892
5098
  //#endregion
5099
+ //#region src/extensions/workspaces/sessions/(frontend)/slot/media-review.web.ts
5100
+ /** Any Markdown host reads this through `markdownMediaReview` to annotate an opened picture. */
5101
+ var media_review_web_default = defineSlotFile({
5102
+ slot: "author.media-review",
5103
+ parse: markdownMediaReviewSlot.parse
5104
+ });
5105
+ //#endregion
4893
5106
  //#region src/extensions/workspaces/sessions/(frontend)/slot/open-source.web.ts
4894
5107
  var open_source_web_default = defineSlotFile({
4895
5108
  slot: "author.open-source",
@@ -5132,13 +5345,20 @@ const webPlugin = defineWebPlugin({
5132
5345
  return author_document_web_default;
5133
5346
  },
5134
5347
  get fills() {
5135
- return [via({
5136
- slot: "author.open-source",
5137
- id: "author"
5138
- }, Author_author_open_source_web_default), via({
5139
- slot: "author.image-feedback",
5140
- id: "image-feedback"
5141
- }, ImageFeedback_author_image_feedback_web_default)];
5348
+ return [
5349
+ via({
5350
+ slot: "author.open-source",
5351
+ id: "author"
5352
+ }, Author_author_open_source_web_default),
5353
+ via({
5354
+ slot: "author.image-feedback",
5355
+ id: "image-feedback"
5356
+ }, ImageFeedback_author_image_feedback_web_default),
5357
+ via({
5358
+ slot: "author.media-review",
5359
+ id: "media-review"
5360
+ }, MediaReview_author_media_review_web_default)
5361
+ ];
5142
5362
  },
5143
5363
  get leaderBindings() {
5144
5364
  return [via({ id: "author.author-toggle" }, author_toggle_web_default)];
@@ -5149,6 +5369,7 @@ const webPlugin = defineWebPlugin({
5149
5369
  get slots() {
5150
5370
  return [
5151
5371
  via({ slot: "author.image-feedback" }, image_feedback_web_default),
5372
+ via({ slot: "author.media-review" }, media_review_web_default),
5152
5373
  via({ slot: "author.open-source" }, open_source_web_default),
5153
5374
  via({ slot: "author.preview-action" }, preview_action_web_default),
5154
5375
  via({ slot: "author.preview-provider" }, preview_provider_web_default)