pi-studio 0.9.51 → 0.9.53

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.
@@ -242,6 +242,7 @@
242
242
  || typeof previewResourceHelpers.buildStudioPdfVersionSignature !== "function"
243
243
  || typeof previewResourceHelpers.createStudioPdfVersionObservationState !== "function"
244
244
  || typeof previewResourceHelpers.hydrateStudioPreviewLocalImages !== "function"
245
+ || typeof previewResourceHelpers.hydrateStudioPreviewLocalPdfEmbeds !== "function"
245
246
  || typeof previewResourceHelpers.observeStudioPdfVersion !== "function"
246
247
  ) {
247
248
  throw new Error("Studio preview resource helpers failed to load.");
@@ -296,6 +297,7 @@
296
297
  let studioPdfFocusOpenLinkEl = null;
297
298
  let studioPdfFocusSystemViewerBtn = null;
298
299
  let studioPdfFocusRevealBtn = null;
300
+ let studioPdfFocusCopyPathBtn = null;
299
301
  let studioPdfFocusAutoRefreshBtn = null;
300
302
  let studioPdfFocusFullscreenBtn = null;
301
303
  let studioPdfFocusCloseBtn = null;
@@ -305,6 +307,7 @@
305
307
  let studioPdfFocusSourceCard = null;
306
308
  let studioPdfFocusStandaloneAutoRefreshState = null;
307
309
  const studioPdfCardAutoRefreshStates = new WeakMap();
310
+ const studioPdfActionFeedbackStates = new WeakMap();
308
311
  const STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS = 1_000;
309
312
  const STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS = 2;
310
313
  let studioHtmlFocusOverlayEl = null;
@@ -400,8 +403,8 @@
400
403
  option.disabled = (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value)) || (isQuartoOption && !quartoRelevant);
401
404
  });
402
405
  rightViewSelect.title = isEditorOnlyMode
403
- ? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+Q opens Side questions."
404
- : "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W keep their mnemonic Preview/Editor Preview/Working shortcuts.";
406
+ ? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+F/Q open Files/Side questions."
407
+ : "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.";
405
408
  }
406
409
 
407
410
  function getInitialRightView(source) {
@@ -476,6 +479,7 @@
476
479
  let sideQuestionAvailablePiTools = [];
477
480
  let sideQuestionPreviewRenderNonce = 0;
478
481
  let sideQuestionContextRefreshHandle = null;
482
+ let sideQuestionContextGrantPending = false;
479
483
  let sideQuestionMarkdownExportRequest = null;
480
484
  const sideQuestionMarkdownRenderCache = new Map();
481
485
  let sideQuestionUi = {
@@ -2291,6 +2295,9 @@
2291
2295
  relativeDir: "",
2292
2296
  parentDir: null,
2293
2297
  entries: [],
2298
+ exactFiles: [],
2299
+ locations: [],
2300
+ grantRequiredPath: "",
2294
2301
  omitted: 0,
2295
2302
  omittedIgnored: 0,
2296
2303
  sort: fileBrowserSortMode,
@@ -3892,8 +3899,12 @@
3892
3899
  secondaryBtn.className = "studio-decision-secondary";
3893
3900
  secondaryBtn.hidden = true;
3894
3901
  secondaryBtn.addEventListener("click", () => {
3895
- const handler = studioDecisionState && studioDecisionState.onSecondary;
3896
- if (typeof handler !== "function") return;
3902
+ const state = studioDecisionState;
3903
+ const handler = state && state.onSecondary;
3904
+ if (typeof handler !== "function") {
3905
+ if (state && state.hasSecondaryValue) finishStudioDecision(state.secondaryValue);
3906
+ return;
3907
+ }
3897
3908
  try {
3898
3909
  const result = handler();
3899
3910
  if (result && typeof result.catch === "function") {
@@ -3988,6 +3999,8 @@
3988
3999
  resolve,
3989
4000
  returnFocusEl,
3990
4001
  onSecondary: typeof settings.onSecondary === "function" ? settings.onSecondary : null,
4002
+ hasSecondaryValue: Object.prototype.hasOwnProperty.call(settings, "secondaryValue"),
4003
+ secondaryValue: settings.secondaryValue,
3991
4004
  };
3992
4005
  studioDecisionState = decisionState;
3993
4006
  const schedule = typeof window.requestAnimationFrame === "function"
@@ -4819,6 +4832,16 @@
4819
4832
  return;
4820
4833
  }
4821
4834
 
4835
+ const isFilesShortcut = (key.toLowerCase() === "f" || code === "KeyF")
4836
+ && (event.metaKey || event.ctrlKey)
4837
+ && event.altKey
4838
+ && !event.shiftKey;
4839
+ if (isFilesShortcut) {
4840
+ event.preventDefault();
4841
+ switchRightPaneToView("files");
4842
+ return;
4843
+ }
4844
+
4822
4845
  const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
4823
4846
  && (event.metaKey || event.ctrlKey)
4824
4847
  && event.altKey
@@ -6039,7 +6062,8 @@
6039
6062
  + " const images = Array.prototype.slice.call(document.querySelectorAll('img[src]'));\n"
6040
6063
  + " images.forEach((image) => {\n"
6041
6064
  + " if (!image || !image.getAttribute) return;\n"
6042
- + " if (image.getAttribute('data-pi-studio-html-resource-resolved') === 'true') return;\n"
6065
+ + " const resourceState = image.getAttribute('data-pi-studio-html-resource-resolved') || '';\n"
6066
+ + " if (resourceState === 'true' || resourceState === 'blocked' || resourceState === 'failed') return;\n"
6043
6067
  + " const raw = String(image.getAttribute('src') || '').trim();\n"
6044
6068
  + " if (!shouldResolveHtmlPreviewResourceUrl(raw)) return;\n"
6045
6069
  + " let resourceId = image.getAttribute('data-pi-studio-html-resource-id') || '';\n"
@@ -6071,6 +6095,7 @@
6071
6095
  + " image.setAttribute('data-pi-studio-html-resource-resolved', 'true');\n"
6072
6096
  + " } else if (typeof result.error === 'string' && result.error) {\n"
6073
6097
  + " image.setAttribute('title', result.error);\n"
6098
+ + " image.setAttribute('data-pi-studio-html-resource-resolved', result.blocked === true ? 'blocked' : 'failed');\n"
6074
6099
  + " }\n"
6075
6100
  + " htmlResourcePlaceholders.delete(resourceId);\n"
6076
6101
  + " });\n"
@@ -6521,18 +6546,28 @@
6521
6546
  const message = payload && typeof payload.error === "string"
6522
6547
  ? payload.error
6523
6548
  : (timeoutLabel || "Local preview resource load") + " failed with HTTP " + response.status + ".";
6524
- throw new Error(message);
6549
+ const requestError = new Error(message);
6550
+ requestError.studioPayload = payload && typeof payload === "object" ? payload : null;
6551
+ requestError.studioStatus = response.status;
6552
+ throw requestError;
6525
6553
  }
6526
6554
  return payload.dataUrl;
6527
6555
  }
6528
6556
 
6529
6557
  function postHtmlArtifactResourceResults(record, results) {
6530
6558
  if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
6559
+ const publicResults = Array.isArray(results) ? results.map((result) => ({
6560
+ resourceId: result && result.resourceId ? result.resourceId : "",
6561
+ ok: Boolean(result && result.ok === true),
6562
+ dataUrl: result && typeof result.dataUrl === "string" ? result.dataUrl : undefined,
6563
+ error: result && typeof result.error === "string" ? result.error : undefined,
6564
+ blocked: Boolean(result && result.blocked === true),
6565
+ })) : [];
6531
6566
  try {
6532
6567
  record.iframe.contentWindow.postMessage({
6533
6568
  type: "pi-studio-html-artifact-resources-resolved",
6534
6569
  id: record.id || "",
6535
- results: Array.isArray(results) ? results : [],
6570
+ results: publicResults,
6536
6571
  }, "*");
6537
6572
  } catch {
6538
6573
  // Ignore iframe postMessage failures.
@@ -6550,14 +6585,58 @@
6550
6585
  );
6551
6586
  return { resourceId, ok: true, dataUrl };
6552
6587
  } catch (error) {
6553
- return { resourceId, ok: false, error: error && error.message ? error.message : String(error || "HTML preview resource load failed.") };
6588
+ const grantRequest = getStudioResourceGrantRequest(error);
6589
+ return {
6590
+ resourceId,
6591
+ ok: false,
6592
+ error: error && error.message ? error.message : String(error || "HTML preview resource load failed."),
6593
+ blocked: Boolean(grantRequest),
6594
+ grantRequest,
6595
+ sourceUrl: item && item.url ? String(item.url) : "",
6596
+ };
6554
6597
  }
6555
6598
  }
6556
6599
 
6600
+ function renderHtmlArtifactBlockedResources(record) {
6601
+ if (!record || !record.shell) return;
6602
+ const existing = record.shell.querySelector(".studio-html-artifact-blocked-media");
6603
+ if (existing) existing.remove();
6604
+ const blocked = record.blockedResources instanceof Map ? Array.from(record.blockedResources.values()) : [];
6605
+ if (!blocked.length) return;
6606
+ const container = document.createElement("div");
6607
+ container.className = "studio-html-artifact-blocked-media";
6608
+ blocked.forEach((entry) => {
6609
+ const notice = createStudioBlockedMediaNoticeForRequest(entry.grantRequest, "image", entry.sourceUrl);
6610
+ if (notice) container.appendChild(notice);
6611
+ });
6612
+ if (!container.childNodes.length) return;
6613
+ const frame = record.iframe && record.iframe.parentNode === record.shell ? record.iframe : null;
6614
+ record.shell.insertBefore(container, frame);
6615
+ }
6616
+
6617
+ function syncHtmlArtifactBlockedResources(record, results) {
6618
+ if (!record) return;
6619
+ if (!(record.blockedResources instanceof Map)) record.blockedResources = new Map();
6620
+ (Array.isArray(results) ? results : []).forEach((result) => {
6621
+ const resourceId = result && result.resourceId ? String(result.resourceId) : "";
6622
+ if (!resourceId) return;
6623
+ if (result.blocked === true && result.grantRequest) {
6624
+ record.blockedResources.set(resourceId, {
6625
+ grantRequest: result.grantRequest,
6626
+ sourceUrl: result.sourceUrl || "local image",
6627
+ });
6628
+ } else {
6629
+ record.blockedResources.delete(resourceId);
6630
+ }
6631
+ });
6632
+ renderHtmlArtifactBlockedResources(record);
6633
+ }
6634
+
6557
6635
  async function resolveHtmlArtifactResources(record, items) {
6558
6636
  if (!record || !Array.isArray(items) || items.length === 0) return;
6559
6637
  if (record.detail) record.detail.textContent = "HTML preview · loading local images";
6560
6638
  const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
6639
+ syncHtmlArtifactBlockedResources(record, results);
6561
6640
  postHtmlArtifactResourceResults(record, results);
6562
6641
  setHtmlArtifactDetailText(record, "HTML preview");
6563
6642
  }
@@ -6618,12 +6697,14 @@
6618
6697
  const action = typeof data.action === "string" ? data.action : "open";
6619
6698
  if (action === "contextmenu") {
6620
6699
  const point = getHtmlArtifactLocalLinkClientPoint(record, data);
6621
- showPreviewLinkMenu(null, point, context);
6700
+ void showPreviewLinkMenu(null, point, context);
6622
6701
  return;
6623
6702
  }
6624
6703
  const kind = getPreviewLocalLinkKind(context.href);
6625
6704
  if (kind === "pdf") {
6626
- openPreviewPdfLink(context.href, context.title, context);
6705
+ void openPreviewPdfLink(context.href, context.title, context).catch((error) => {
6706
+ setStatus((error && error.message) ? error.message : String(error || "Could not open linked PDF."), "warning");
6707
+ });
6627
6708
  return;
6628
6709
  }
6629
6710
  if (kind === "image") {
@@ -6633,9 +6714,8 @@
6633
6714
  return;
6634
6715
  }
6635
6716
  if (kind === "text" || kind === "office") {
6636
- void openPreviewDocumentInNewEditor(context.href, context).catch((error) => {
6637
- setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
6638
- });
6717
+ const point = getHtmlArtifactLocalLinkClientPoint(record, data);
6718
+ void showPreviewLinkMenu(null, point, context);
6639
6719
  return;
6640
6720
  }
6641
6721
  setStatus("Right-click this local HTML preview link for file actions.", "warning");
@@ -6715,10 +6795,22 @@
6715
6795
  button.setAttribute("aria-pressed", focused ? "true" : "false");
6716
6796
  }
6717
6797
 
6798
+ function isStudioFullscreenAvailable(element) {
6799
+ return Boolean(
6800
+ element
6801
+ && typeof element.requestFullscreen === "function"
6802
+ && typeof document.exitFullscreen === "function"
6803
+ );
6804
+ }
6805
+
6718
6806
  function syncStudioHtmlFocusFullscreenButton() {
6719
6807
  const button = studioHtmlFocusFullscreenBtn;
6720
6808
  if (!button) return;
6721
6809
  const shell = studioHtmlFocusShellEl;
6810
+ const available = isStudioFullscreenAvailable(shell);
6811
+ button.hidden = !available;
6812
+ button.setAttribute("aria-hidden", available ? "false" : "true");
6813
+ if (!available) return;
6722
6814
  const isFullscreen = Boolean(shell && document.fullscreenElement && document.fullscreenElement === shell);
6723
6815
  button.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
6724
6816
  const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
@@ -7027,6 +7119,7 @@
7027
7119
  mathRenderItemCount: 0,
7028
7120
  resourceResolveBatchCount: 0,
7029
7121
  resourceResolveItemCount: 0,
7122
+ blockedResources: new Map(),
7030
7123
  });
7031
7124
 
7032
7125
  targetEl.appendChild(shell);
@@ -7191,9 +7284,9 @@
7191
7284
  systemViewerBtn.type = "button";
7192
7285
  systemViewerBtn.className = "studio-pdf-focus-btn studio-pdf-focus-system-viewer";
7193
7286
  systemViewerBtn.textContent = "System viewer";
7194
- systemViewerBtn.title = "Open the local PDF in the operating system's default PDF viewer.";
7287
+ systemViewerBtn.title = "Open the local PDF in the default viewer on the computer running Pi.";
7195
7288
  systemViewerBtn.addEventListener("click", () => {
7196
- void runStudioPdfLocalAction("system-viewer", studioPdfFocusResourceQuery);
7289
+ void runStudioPdfLocalAction("system-viewer", studioPdfFocusResourceQuery, systemViewerBtn);
7197
7290
  });
7198
7291
  actions.appendChild(systemViewerBtn);
7199
7292
 
@@ -7201,12 +7294,22 @@
7201
7294
  revealBtn.type = "button";
7202
7295
  revealBtn.className = "studio-pdf-focus-btn studio-pdf-focus-reveal";
7203
7296
  revealBtn.textContent = "Show in folder";
7204
- revealBtn.title = "Reveal the local PDF in Finder or the system file manager.";
7297
+ revealBtn.title = "Reveal the local PDF in the file manager on the computer running Pi.";
7205
7298
  revealBtn.addEventListener("click", () => {
7206
- void runStudioPdfLocalAction("reveal", studioPdfFocusResourceQuery);
7299
+ void runStudioPdfLocalAction("reveal", studioPdfFocusResourceQuery, revealBtn);
7207
7300
  });
7208
7301
  actions.appendChild(revealBtn);
7209
7302
 
7303
+ const copyPathBtn = document.createElement("button");
7304
+ copyPathBtn.type = "button";
7305
+ copyPathBtn.className = "studio-pdf-focus-btn studio-pdf-focus-copy-path";
7306
+ copyPathBtn.textContent = "Copy path";
7307
+ copyPathBtn.title = "Copy the PDF path on the computer running Pi.";
7308
+ copyPathBtn.addEventListener("click", () => {
7309
+ void copyStudioPdfResourcePath(studioPdfFocusResourceQuery, copyPathBtn);
7310
+ });
7311
+ actions.appendChild(copyPathBtn);
7312
+
7210
7313
  const refreshBtn = document.createElement("button");
7211
7314
  refreshBtn.type = "button";
7212
7315
  refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
@@ -7282,6 +7385,7 @@
7282
7385
  studioPdfFocusOpenLinkEl = openLink;
7283
7386
  studioPdfFocusSystemViewerBtn = systemViewerBtn;
7284
7387
  studioPdfFocusRevealBtn = revealBtn;
7388
+ studioPdfFocusCopyPathBtn = copyPathBtn;
7285
7389
  studioPdfFocusAutoRefreshBtn = autoRefreshBtn;
7286
7390
  studioPdfFocusFullscreenBtn = fullscreenBtn;
7287
7391
  studioPdfFocusCloseBtn = closeBtn;
@@ -7576,9 +7680,33 @@
7576
7680
  return setStudioPdfAutoRefreshEnabled(state, !state.enabled);
7577
7681
  }
7578
7682
 
7579
- async function runStudioPdfLocalAction(action, resourceQuery) {
7683
+ function flashStudioPdfActionFeedback(buttonEl, label, level) {
7684
+ if (!(buttonEl instanceof HTMLButtonElement)) return;
7685
+ const existing = studioPdfActionFeedbackStates.get(buttonEl) || null;
7686
+ if (existing && existing.timer) window.clearTimeout(existing.timer);
7687
+ const state = {
7688
+ baselineText: existing ? existing.baselineText : buttonEl.textContent,
7689
+ baselineAriaLabel: existing ? existing.baselineAriaLabel : buttonEl.getAttribute("aria-label"),
7690
+ timer: null,
7691
+ };
7692
+ studioPdfActionFeedbackStates.set(buttonEl, state);
7693
+ buttonEl.textContent = String(label || "Done");
7694
+ buttonEl.dataset.studioActionFeedback = level === "warning" ? "warning" : "success";
7695
+ buttonEl.setAttribute("aria-label", String(label || "Done"));
7696
+ state.timer = window.setTimeout(() => {
7697
+ if (studioPdfActionFeedbackStates.get(buttonEl) !== state) return;
7698
+ buttonEl.textContent = state.baselineText;
7699
+ if (state.baselineAriaLabel === null) buttonEl.removeAttribute("aria-label");
7700
+ else buttonEl.setAttribute("aria-label", state.baselineAriaLabel);
7701
+ delete buttonEl.dataset.studioActionFeedback;
7702
+ studioPdfActionFeedbackStates.delete(buttonEl);
7703
+ }, 2_200);
7704
+ }
7705
+
7706
+ async function runStudioPdfLocalAction(action, resourceQuery, actionButton) {
7580
7707
  const query = normalizeStudioPdfResourceQuery(resourceQuery);
7581
7708
  if (!query) {
7709
+ flashStudioPdfActionFeedback(actionButton, "Unavailable", "warning");
7582
7710
  setStatus("Could not resolve this PDF's local path.", "warning");
7583
7711
  return false;
7584
7712
  }
@@ -7589,11 +7717,13 @@
7589
7717
  method: "POST",
7590
7718
  body: JSON.stringify(query),
7591
7719
  });
7720
+ flashStudioPdfActionFeedback(actionButton, openInSystemViewer ? "Opened ✓" : "Shown ✓", "success");
7592
7721
  setStatus(payload && payload.message
7593
7722
  ? payload.message
7594
7723
  : (openInSystemViewer ? "Opened PDF in the system viewer." : "Revealed PDF in the file manager."), "success");
7595
7724
  return true;
7596
7725
  } catch (error) {
7726
+ flashStudioPdfActionFeedback(actionButton, "Failed", "warning");
7597
7727
  setStatus((error && error.message)
7598
7728
  ? error.message
7599
7729
  : (openInSystemViewer ? "Could not open PDF in the system viewer." : "Could not reveal PDF in the file manager."), "warning");
@@ -7605,6 +7735,31 @@
7605
7735
  const available = Boolean(normalizeStudioPdfResourceQuery(studioPdfFocusResourceQuery));
7606
7736
  if (studioPdfFocusSystemViewerBtn) studioPdfFocusSystemViewerBtn.disabled = !available;
7607
7737
  if (studioPdfFocusRevealBtn) studioPdfFocusRevealBtn.disabled = !available;
7738
+ if (studioPdfFocusCopyPathBtn) studioPdfFocusCopyPathBtn.disabled = !available;
7739
+ }
7740
+
7741
+ async function copyStudioPdfResourcePath(resourceQuery, actionButton) {
7742
+ const query = normalizeStudioPdfResourceQuery(resourceQuery);
7743
+ if (!query) {
7744
+ flashStudioPdfActionFeedback(actionButton, "Unavailable", "warning");
7745
+ setStatus("Could not resolve this PDF's local path.", "warning");
7746
+ return false;
7747
+ }
7748
+ try {
7749
+ const payload = await fetchStudioJson("/local-preview-link", {
7750
+ query: { ...query, action: "resolve" },
7751
+ });
7752
+ const path = payload && typeof payload.path === "string" ? payload.path : "";
7753
+ if (!path) throw new Error("Studio did not return a PDF path.");
7754
+ if (!(await writeTextToClipboard(path))) throw new Error("Clipboard write failed.");
7755
+ flashStudioPdfActionFeedback(actionButton, "Copied ✓", "success");
7756
+ setStatus("Copied PDF path from the computer running Pi.", "success");
7757
+ return true;
7758
+ } catch (error) {
7759
+ flashStudioPdfActionFeedback(actionButton, "Failed", "warning");
7760
+ setStatus((error && error.message) ? error.message : "Could not copy this PDF's local path.", "warning");
7761
+ return false;
7762
+ }
7608
7763
  }
7609
7764
 
7610
7765
  function buildRefreshedStudioPdfViewerUrl(value) {
@@ -7731,6 +7886,10 @@
7731
7886
 
7732
7887
  function syncStudioPdfFocusFullscreenButton() {
7733
7888
  if (!studioPdfFocusFullscreenBtn) return;
7889
+ const available = isStudioFullscreenAvailable(studioPdfFocusDialogEl);
7890
+ studioPdfFocusFullscreenBtn.hidden = !available;
7891
+ studioPdfFocusFullscreenBtn.setAttribute("aria-hidden", available ? "false" : "true");
7892
+ if (!available) return;
7734
7893
  const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
7735
7894
  studioPdfFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
7736
7895
  const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
@@ -7789,6 +7948,16 @@
7789
7948
  studioPdfFocusFrameEl.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
7790
7949
  }
7791
7950
 
7951
+ function getStudioPdfCardResourceQuery(card) {
7952
+ return card && card.dataset
7953
+ ? normalizeStudioPdfResourceQuery({
7954
+ path: card.dataset.studioPdfPath || "",
7955
+ sourcePath: card.dataset.studioPdfSourcePath || "",
7956
+ resourceDir: card.dataset.studioPdfResourceDir || "",
7957
+ })
7958
+ : null;
7959
+ }
7960
+
7792
7961
  function openStudioPdfFocusFromButton(buttonEl) {
7793
7962
  if (!buttonEl) return false;
7794
7963
  const card = buttonEl.closest && buttonEl.closest(".studio-pdf-card");
@@ -7798,13 +7967,7 @@
7798
7967
  || String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
7799
7968
  || "PDF preview";
7800
7969
  const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
7801
- const resourceQuery = card && card.dataset
7802
- ? normalizeStudioPdfResourceQuery({
7803
- path: card.dataset.studioPdfPath || "",
7804
- sourcePath: card.dataset.studioPdfSourcePath || "",
7805
- resourceDir: card.dataset.studioPdfResourceDir || "",
7806
- })
7807
- : null;
7970
+ const resourceQuery = getStudioPdfCardResourceQuery(card);
7808
7971
  if (!viewerUrl) return false;
7809
7972
  openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, card);
7810
7973
  return true;
@@ -7824,6 +7987,80 @@
7824
7987
  }
7825
7988
  }
7826
7989
 
7990
+ function consumeStudioPreviewMediaEvent(event) {
7991
+ event.preventDefault();
7992
+ event.stopPropagation();
7993
+ if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
7994
+ }
7995
+
7996
+ function handleStudioPreviewMediaKeydown(event) {
7997
+ if (!event || (event.key !== "Enter" && event.key !== " ")) return;
7998
+ const target = event.target;
7999
+ const imageEl = target instanceof Element ? target.closest("img.studio-image-focus-target") : null;
8000
+ if (!imageEl) return;
8001
+ consumeStudioPreviewMediaEvent(event);
8002
+ if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
8003
+ }
8004
+
8005
+ function handleStudioPreviewMediaActivation(event) {
8006
+ const target = event && event.target;
8007
+ if (!(target instanceof Element)) return;
8008
+
8009
+ const blockedMediaButton = target.closest("button.studio-blocked-media-allow");
8010
+ if (blockedMediaButton) {
8011
+ consumeStudioPreviewMediaEvent(event);
8012
+ void handleStudioBlockedMediaAllowButton(blockedMediaButton);
8013
+ return;
8014
+ }
8015
+
8016
+ const imageEl = target.closest("img.studio-image-focus-target");
8017
+ if (imageEl) {
8018
+ consumeStudioPreviewMediaEvent(event);
8019
+ if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
8020
+ return;
8021
+ }
8022
+
8023
+ const pdfFigureEl = target.closest(".studio-pdf-preview-focus-target");
8024
+ if (pdfFigureEl && !target.closest("button, a")) {
8025
+ consumeStudioPreviewMediaEvent(event);
8026
+ if (!openPreviewPdfFigureInFocus(pdfFigureEl)) setStatus("Could not enlarge this PDF figure preview.", "warning");
8027
+ return;
8028
+ }
8029
+
8030
+ const actionBtn = target.closest(
8031
+ ".studio-pdf-card-system-viewer, .studio-pdf-card-reveal, .studio-pdf-card-copy-path, .studio-pdf-card-refresh, .studio-pdf-card-auto-refresh"
8032
+ );
8033
+ if (!actionBtn) return;
8034
+ const card = actionBtn.closest(".studio-pdf-card");
8035
+ if (!card) return;
8036
+ consumeStudioPreviewMediaEvent(event);
8037
+ const resourceQuery = getStudioPdfCardResourceQuery(card);
8038
+ if (actionBtn.classList.contains("studio-pdf-card-system-viewer")) {
8039
+ void runStudioPdfLocalAction("system-viewer", resourceQuery, actionBtn);
8040
+ return;
8041
+ }
8042
+ if (actionBtn.classList.contains("studio-pdf-card-reveal")) {
8043
+ void runStudioPdfLocalAction("reveal", resourceQuery, actionBtn);
8044
+ return;
8045
+ }
8046
+ if (actionBtn.classList.contains("studio-pdf-card-copy-path")) {
8047
+ void copyStudioPdfResourcePath(resourceQuery, actionBtn);
8048
+ return;
8049
+ }
8050
+ if (actionBtn.classList.contains("studio-pdf-card-refresh")) {
8051
+ const refreshed = refreshStudioPdfCard(card);
8052
+ flashStudioPdfActionFeedback(actionBtn, refreshed ? "Refreshed ✓" : "Failed", refreshed ? "success" : "warning");
8053
+ if (!refreshed) setStatus("Could not refresh this PDF preview.", "warning");
8054
+ return;
8055
+ }
8056
+ const state = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
8057
+ if (!state) {
8058
+ setStatus("Could not resolve this PDF for auto-refresh.", "warning");
8059
+ return;
8060
+ }
8061
+ setStudioPdfCardAutoRefresh(card, !state.enabled);
8062
+ }
8063
+
7827
8064
  function isStudioImageFocusOpen() {
7828
8065
  return Boolean(studioImageFocusOverlayEl && studioImageFocusOverlayEl.hidden === false);
7829
8066
  }
@@ -7976,6 +8213,10 @@
7976
8213
 
7977
8214
  function syncStudioImageFocusFullscreenButton() {
7978
8215
  if (!studioImageFocusFullscreenBtn) return;
8216
+ const available = isStudioFullscreenAvailable(studioImageFocusDialogEl);
8217
+ studioImageFocusFullscreenBtn.hidden = !available;
8218
+ studioImageFocusFullscreenBtn.setAttribute("aria-hidden", available ? "false" : "true");
8219
+ if (!available) return;
7979
8220
  const isFullscreen = Boolean(document.fullscreenElement && studioImageFocusDialogEl && document.fullscreenElement === studioImageFocusDialogEl);
7980
8221
  studioImageFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
7981
8222
  const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
@@ -8212,7 +8453,86 @@
8212
8453
  });
8213
8454
  }
8214
8455
 
8215
- function createStudioPdfCard(block, useEditorResourceContext) {
8456
+ function openPreviewPdfFigureInFocus(previewEl) {
8457
+ const canvas = previewEl && typeof previewEl.querySelector === "function"
8458
+ ? previewEl.querySelector("canvas")
8459
+ : null;
8460
+ if (!(canvas instanceof HTMLCanvasElement)) return false;
8461
+ try {
8462
+ const dataUrl = canvas.toDataURL("image/png");
8463
+ const title = String(previewEl.getAttribute("title") || "PDF figure preview").trim() || "PDF figure preview";
8464
+ return openStudioImageFocusViewer(dataUrl, title);
8465
+ } catch {
8466
+ return false;
8467
+ }
8468
+ }
8469
+
8470
+ function decoratePreviewPdfFigures(targetEl) {
8471
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
8472
+ const previews = Array.from(targetEl.querySelectorAll(".studio-pdf-preview"));
8473
+ previews.forEach((previewEl) => {
8474
+ if (!(previewEl instanceof HTMLElement)) return;
8475
+ if (previewEl.dataset && previewEl.dataset.studioPdfFigureFocusDecorated === "1") return;
8476
+ if (!previewEl.querySelector("canvas")) return;
8477
+ previewEl.classList.add("studio-pdf-preview-focus-target");
8478
+ previewEl.title = "PDF figure preview (page 1). Click to enlarge.";
8479
+ if (previewEl.dataset) previewEl.dataset.studioPdfFigureFocusDecorated = "1";
8480
+
8481
+ previewEl.addEventListener("click", (event) => {
8482
+ if (event.target instanceof Element && event.target.closest("button, a")) return;
8483
+ event.preventDefault();
8484
+ if (!openPreviewPdfFigureInFocus(previewEl)) setStatus("Could not enlarge this PDF figure preview.", "warning");
8485
+ });
8486
+ });
8487
+ }
8488
+
8489
+ function createStudioPdfStatusCard(block, content) {
8490
+ const options = block && block.options ? block.options : {};
8491
+ const path = String(options.path || "").trim();
8492
+ const title = String(options.title || path || "Embedded PDF").trim();
8493
+ const caption = String(options.caption || "").trim();
8494
+ const card = document.createElement("figure");
8495
+ card.className = "studio-pdf-card studio-pdf-card-status";
8496
+ const header = document.createElement("figcaption");
8497
+ header.className = "studio-pdf-card-header";
8498
+ const titleEl = document.createElement("div");
8499
+ titleEl.className = "studio-pdf-card-title";
8500
+ titleEl.textContent = title;
8501
+ header.appendChild(titleEl);
8502
+ card.appendChild(header);
8503
+ if (caption) {
8504
+ const captionEl = document.createElement("div");
8505
+ captionEl.className = "studio-pdf-card-caption";
8506
+ captionEl.textContent = caption;
8507
+ card.appendChild(captionEl);
8508
+ }
8509
+ if (content instanceof Element) {
8510
+ card.appendChild(content);
8511
+ } else {
8512
+ const errorEl = document.createElement("div");
8513
+ errorEl.className = "studio-pdf-card-error";
8514
+ errorEl.textContent = String(content || "PDF resource unavailable.");
8515
+ card.appendChild(errorEl);
8516
+ }
8517
+ return card;
8518
+ }
8519
+
8520
+ async function createStudioPdfCard(block, useEditorResourceContext) {
8521
+ const options = block && block.options ? block.options : {};
8522
+ const path = String(options.path || "").trim();
8523
+ if (!path) return createAuthorizedStudioPdfCard(block, useEditorResourceContext);
8524
+ const resourceQuery = buildStudioPdfResourceQuery(options, useEditorResourceContext);
8525
+ try {
8526
+ await fetchPreviewLocalLink("resolve", path, resourceQuery, { skipGrantPrompt: true });
8527
+ return createAuthorizedStudioPdfCard(block, useEditorResourceContext);
8528
+ } catch (error) {
8529
+ const notice = createStudioBlockedMediaNotice(error, "pdf", path);
8530
+ if (notice) return createStudioPdfStatusCard(block, notice);
8531
+ return createStudioPdfStatusCard(block, error && error.message ? error.message : "PDF resource unavailable.");
8532
+ }
8533
+ }
8534
+
8535
+ function createAuthorizedStudioPdfCard(block, useEditorResourceContext) {
8216
8536
  const options = block && block.options ? block.options : {};
8217
8537
  const path = String(options.path || "").trim();
8218
8538
  const title = String(options.title || path || "Embedded PDF").trim();
@@ -8244,12 +8564,13 @@
8244
8564
  focusBtn.type = "button";
8245
8565
  focusBtn.className = "studio-pdf-card-action studio-pdf-card-focus";
8246
8566
  focusBtn.title = "Open this PDF in a larger Studio overlay.";
8247
- focusBtn.setAttribute("aria-label", "Focus PDF");
8567
+ focusBtn.setAttribute("aria-label", "Enlarge PDF");
8248
8568
  if (focusBtn.dataset) {
8249
8569
  focusBtn.dataset.studioPdfViewerUrl = viewerUrl;
8250
8570
  focusBtn.dataset.studioPdfTitle = title;
8251
8571
  }
8252
8572
  focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
8573
+ focusBtn.appendChild(document.createTextNode("Enlarge"));
8253
8574
  focusBtn.addEventListener("click", handleStudioPdfFocusButtonClick);
8254
8575
  titleGroup.appendChild(focusBtn);
8255
8576
  }
@@ -8276,11 +8597,11 @@
8276
8597
  systemViewerBtn.type = "button";
8277
8598
  systemViewerBtn.className = "studio-pdf-card-action studio-pdf-card-system-viewer";
8278
8599
  systemViewerBtn.textContent = "System viewer";
8279
- systemViewerBtn.title = "Open the local PDF in the operating system's default PDF viewer.";
8600
+ systemViewerBtn.title = "Open the local PDF in the default viewer on the computer running Pi.";
8280
8601
  systemViewerBtn.addEventListener("click", (event) => {
8281
8602
  event.preventDefault();
8282
8603
  event.stopPropagation();
8283
- void runStudioPdfLocalAction("system-viewer", resourceQuery);
8604
+ void runStudioPdfLocalAction("system-viewer", resourceQuery, systemViewerBtn);
8284
8605
  });
8285
8606
  actions.appendChild(systemViewerBtn);
8286
8607
 
@@ -8288,14 +8609,26 @@
8288
8609
  revealBtn.type = "button";
8289
8610
  revealBtn.className = "studio-pdf-card-action studio-pdf-card-reveal";
8290
8611
  revealBtn.textContent = "Show in folder";
8291
- revealBtn.title = "Reveal the local PDF in Finder or the system file manager.";
8612
+ revealBtn.title = "Reveal the local PDF in the file manager on the computer running Pi.";
8292
8613
  revealBtn.addEventListener("click", (event) => {
8293
8614
  event.preventDefault();
8294
8615
  event.stopPropagation();
8295
- void runStudioPdfLocalAction("reveal", resourceQuery);
8616
+ void runStudioPdfLocalAction("reveal", resourceQuery, revealBtn);
8296
8617
  });
8297
8618
  actions.appendChild(revealBtn);
8298
8619
 
8620
+ const copyPathBtn = document.createElement("button");
8621
+ copyPathBtn.type = "button";
8622
+ copyPathBtn.className = "studio-pdf-card-action studio-pdf-card-copy-path";
8623
+ copyPathBtn.textContent = "Copy path";
8624
+ copyPathBtn.title = "Copy the PDF path on the computer running Pi.";
8625
+ copyPathBtn.addEventListener("click", (event) => {
8626
+ event.preventDefault();
8627
+ event.stopPropagation();
8628
+ void copyStudioPdfResourcePath(resourceQuery, copyPathBtn);
8629
+ });
8630
+ actions.appendChild(copyPathBtn);
8631
+
8299
8632
  const refreshBtn = document.createElement("button");
8300
8633
  refreshBtn.type = "button";
8301
8634
  refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
@@ -8362,17 +8695,17 @@
8362
8695
  return card;
8363
8696
  }
8364
8697
 
8365
- function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
8698
+ async function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
8366
8699
  if (!targetEl || !Array.isArray(blocks) || blocks.length === 0) return;
8367
8700
  const candidates = Array.from(targetEl.querySelectorAll("p, pre, div"));
8368
- blocks.forEach((block) => {
8701
+ for (const block of blocks) {
8369
8702
  const placeholder = block && block.placeholder ? block.placeholder : "";
8370
- if (!placeholder) return;
8703
+ if (!placeholder) continue;
8371
8704
  const match = candidates.find((el) => String(el.textContent || "").trim() === placeholder);
8372
- if (match && match.parentNode) {
8373
- match.replaceWith(createStudioPdfCard(block, useEditorResourceContext));
8374
- }
8375
- });
8705
+ if (!match || !match.parentNode) continue;
8706
+ const card = await createStudioPdfCard(block, useEditorResourceContext);
8707
+ if (match.isConnected && match.parentNode) match.replaceWith(card);
8708
+ }
8376
8709
  }
8377
8710
 
8378
8711
  function sanitizeRenderedHtml(html, markdown, options) {
@@ -8944,6 +9277,91 @@
8944
9277
  targetEl.appendChild(el);
8945
9278
  }
8946
9279
 
9280
+ function createStudioBlockedMediaNotice(error, mediaKind, sourceLabel) {
9281
+ return createStudioBlockedMediaNoticeForRequest(getStudioResourceGrantRequest(error), mediaKind, sourceLabel);
9282
+ }
9283
+
9284
+ function createStudioBlockedMediaNoticeForRequest(grantRequest, mediaKind, sourceLabel) {
9285
+ if (!grantRequest) return null;
9286
+ const kind = mediaKind === "pdf" ? "PDF" : "image";
9287
+ const notice = document.createElement("span");
9288
+ notice.className = "studio-blocked-media studio-blocked-media-" + (mediaKind === "pdf" ? "pdf" : "image");
9289
+ notice.setAttribute("role", "note");
9290
+
9291
+ const text = document.createElement("span");
9292
+ text.className = "studio-blocked-media-text";
9293
+ const title = document.createElement("strong");
9294
+ title.textContent = "Local " + kind + " blocked";
9295
+ const detail = document.createElement("span");
9296
+ detail.textContent = "This media is outside the locations allowed for the current Studio session.";
9297
+ const path = document.createElement("code");
9298
+ path.textContent = grantRequest.path || String(sourceLabel || "local media");
9299
+ path.title = "Path on the computer running Pi";
9300
+ text.appendChild(title);
9301
+ text.appendChild(detail);
9302
+ text.appendChild(path);
9303
+ notice.appendChild(text);
9304
+
9305
+ const allowButton = document.createElement("button");
9306
+ allowButton.type = "button";
9307
+ allowButton.className = "studio-blocked-media-allow";
9308
+ allowButton.textContent = "Allow local " + kind + "…";
9309
+ allowButton.title = "Choose whether to allow only this file or its containing folder for the current Studio session.";
9310
+ allowButton.dataset.studioBlockedMediaPath = grantRequest.path;
9311
+ allowButton.dataset.studioBlockedMediaDirectory = grantRequest.directoryPath;
9312
+ allowButton.dataset.studioBlockedMediaLabel = grantRequest.label;
9313
+ allowButton.dataset.studioBlockedMediaKind = mediaKind === "pdf" ? "pdf" : "image";
9314
+ notice.appendChild(allowButton);
9315
+ return notice;
9316
+ }
9317
+
9318
+ function replaceStudioBlockedMediaElement(element, source, error, mediaKind) {
9319
+ if (!element || !element.parentNode) return false;
9320
+ const notice = createStudioBlockedMediaNotice(error, mediaKind, source);
9321
+ if (!notice) return false;
9322
+ element.replaceWith(notice);
9323
+ return true;
9324
+ }
9325
+
9326
+ function refreshStudioPassiveMediaAfterGrant() {
9327
+ renderSourcePreview({ previewDelayMs: 0 });
9328
+ if (rightView === "preview" || rightView === "editor-preview" || rightView === "side-questions") {
9329
+ renderActiveResult();
9330
+ }
9331
+ if (isQuizOpen()) renderQuizOverlay({ preserveScroll: true });
9332
+ }
9333
+
9334
+ async function handleStudioBlockedMediaAllowButton(button) {
9335
+ if (!(button instanceof HTMLButtonElement) || button.dataset.studioBlockedMediaBusy === "1") return false;
9336
+ const request = {
9337
+ path: String(button.dataset.studioBlockedMediaPath || "").trim(),
9338
+ directoryPath: String(button.dataset.studioBlockedMediaDirectory || "").trim(),
9339
+ label: String(button.dataset.studioBlockedMediaLabel || "local media").trim() || "local media",
9340
+ };
9341
+ if (!request.path || !request.directoryPath) {
9342
+ setStatus("Could not resolve this blocked media path.", "warning");
9343
+ return false;
9344
+ }
9345
+ button.dataset.studioBlockedMediaBusy = "1";
9346
+ button.disabled = true;
9347
+ const baselineText = button.textContent;
9348
+ button.textContent = "Choosing…";
9349
+ try {
9350
+ if (!(await requestStudioResourceGrant(request))) return false;
9351
+ refreshStudioPassiveMediaAfterGrant();
9352
+ return true;
9353
+ } catch (error) {
9354
+ setStatus("Could not allow local media: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
9355
+ return false;
9356
+ } finally {
9357
+ if (button.isConnected) {
9358
+ delete button.dataset.studioBlockedMediaBusy;
9359
+ button.disabled = false;
9360
+ button.textContent = baselineText;
9361
+ }
9362
+ }
9363
+ }
9364
+
8947
9365
  function hasMeaningfulPreviewContent(targetEl) {
8948
9366
  if (!targetEl || typeof targetEl.querySelector !== "function") return false;
8949
9367
  if (targetEl.querySelector(".preview-loading")) return false;
@@ -9238,6 +9656,32 @@
9238
9656
  });
9239
9657
  }
9240
9658
 
9659
+ function copyResponsePaneCanvasState(sourceEl, replacementEl) {
9660
+ if (!sourceEl || !replacementEl || typeof sourceEl.querySelectorAll !== "function" || typeof replacementEl.querySelectorAll !== "function") {
9661
+ return false;
9662
+ }
9663
+ const sourceCanvases = Array.from(sourceEl.querySelectorAll("canvas"));
9664
+ const replacementCanvases = Array.from(replacementEl.querySelectorAll("canvas"));
9665
+ if (sourceCanvases.length !== replacementCanvases.length) return false;
9666
+
9667
+ for (let index = 0; index < sourceCanvases.length; index += 1) {
9668
+ const sourceCanvas = sourceCanvases[index];
9669
+ const replacementCanvas = replacementCanvases[index];
9670
+ if (!(sourceCanvas instanceof HTMLCanvasElement) || !(replacementCanvas instanceof HTMLCanvasElement)) return false;
9671
+ if (sourceCanvas.width < 1 || sourceCanvas.height < 1) continue;
9672
+ try {
9673
+ replacementCanvas.width = sourceCanvas.width;
9674
+ replacementCanvas.height = sourceCanvas.height;
9675
+ const context = replacementCanvas.getContext("2d");
9676
+ if (!context) return false;
9677
+ context.drawImage(sourceCanvas, 0, 0);
9678
+ } catch {
9679
+ return false;
9680
+ }
9681
+ }
9682
+ return true;
9683
+ }
9684
+
9241
9685
  function replaceResponsePaneWithClone() {
9242
9686
  const currentEl = critiqueViewEl;
9243
9687
  if (!currentEl || !currentEl.parentNode || typeof currentEl.cloneNode !== "function") {
@@ -9245,7 +9689,7 @@
9245
9689
  }
9246
9690
 
9247
9691
  const replacement = currentEl.cloneNode(true);
9248
- if (!replacement || replacement.nodeType !== 1) {
9692
+ if (!replacement || replacement.nodeType !== 1 || !copyResponsePaneCanvasState(currentEl, replacement)) {
9249
9693
  return currentEl;
9250
9694
  }
9251
9695
 
@@ -10019,6 +10463,10 @@
10019
10463
 
10020
10464
  function exportRightPaneFormat(format) {
10021
10465
  closeExportPreviewMenu();
10466
+ if (String(format || "").startsWith("side-markdown-") && rightView !== "side-questions") {
10467
+ setStatus("Side-thread Markdown export is available only in Side questions.", "warning");
10468
+ return false;
10469
+ }
10022
10470
  if (format === "side-markdown-save") {
10023
10471
  return saveSideQuestionTranscriptMarkdown();
10024
10472
  }
@@ -10220,6 +10668,33 @@
10220
10668
  });
10221
10669
  }
10222
10670
 
10671
+ async function hydrateStudioPreviewLocalMedia(targetEl, resourceContext) {
10672
+ const fetchResource = (resourceUrl, timeoutLabel) => fetchLocalPreviewResourceDataUrl(
10673
+ resourceContext,
10674
+ resourceUrl,
10675
+ RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS,
10676
+ timeoutLabel,
10677
+ );
10678
+ await previewResourceHelpers.hydrateStudioPreviewLocalImages(
10679
+ targetEl,
10680
+ (resourceUrl) => fetchResource(resourceUrl, "Preview image load"),
10681
+ {
10682
+ onError: (imageEl, resourceUrl, error) => {
10683
+ replaceStudioBlockedMediaElement(imageEl, resourceUrl, error, "image");
10684
+ },
10685
+ },
10686
+ );
10687
+ await previewResourceHelpers.hydrateStudioPreviewLocalPdfEmbeds(
10688
+ targetEl,
10689
+ (resourceUrl) => fetchResource(resourceUrl, "Preview PDF load"),
10690
+ {
10691
+ onError: (embedEl, resourceUrl, error) => {
10692
+ replaceStudioBlockedMediaElement(embedEl, resourceUrl, error, "pdf");
10693
+ },
10694
+ },
10695
+ );
10696
+ }
10697
+
10223
10698
  async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
10224
10699
  const previewPrepared = annotationsEnabled
10225
10700
  ? prepareMarkdownForPandocPreview(markdown)
@@ -10246,19 +10721,13 @@
10246
10721
  clearPreviewJumpHighlight(targetEl);
10247
10722
  finishPreviewRender(targetEl);
10248
10723
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
10249
- await previewResourceHelpers.hydrateStudioPreviewLocalImages(targetEl, (resourceUrl) => (
10250
- fetchLocalPreviewResourceDataUrl(
10251
- previewResourceContext,
10252
- resourceUrl,
10253
- RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS,
10254
- "Preview image load",
10255
- )
10256
- ));
10257
- renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
10724
+ await hydrateStudioPreviewLocalMedia(targetEl, previewResourceContext);
10725
+ await renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
10258
10726
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
10259
10727
  await renderAnnotationMathInElement(targetEl);
10260
10728
  decoratePdfEmbeds(targetEl);
10261
10729
  await renderPdfPreviewsInElement(targetEl);
10730
+ decoratePreviewPdfFigures(targetEl);
10262
10731
  const annotationMode = (pane === "source" || pane === "response")
10263
10732
  ? (annotationsEnabled ? "highlight" : "hide")
10264
10733
  : "none";
@@ -11123,71 +11592,92 @@
11123
11592
  return "<select class='files-sort-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
11124
11593
  }
11125
11594
 
11595
+ function buildFileBrowserEntryRowHtml(entry) {
11596
+ const type = entry && entry.type === "directory" ? "directory" : "file";
11597
+ const kind = (entry && entry.kind) || (type === "directory" ? "directory" : "other");
11598
+ const path = String((entry && entry.path) || "");
11599
+ const name = String((entry && entry.name) || basenameForStudioPath(path));
11600
+ const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
11601
+ const metaParts = [getFileBrowserKindLabel(entry)];
11602
+ if (type === "file") metaParts.push(formatFileBrowserSize(entry && entry.size));
11603
+ const time = formatFileBrowserTime(entry && entry.mtimeMs);
11604
+ if (time) metaParts.push(time);
11605
+ const newTabAction = kind === "text" || kind === "office"
11606
+ ? "open-new"
11607
+ : ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
11608
+ const newTabLabel = kind === "text"
11609
+ ? "Open file-backed tab"
11610
+ : (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
11611
+ const newTabTitle = kind === "text"
11612
+ ? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
11613
+ : (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
11614
+ const newTabButton = newTabAction
11615
+ ? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
11616
+ : "";
11617
+ const openTitle = type === "directory"
11618
+ ? "Open folder"
11619
+ : (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
11620
+ return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
11621
+ + "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
11622
+ + "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
11623
+ + "<span class='files-name'>" + escapeHtml(name) + "</span>"
11624
+ + "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
11625
+ + "</button>"
11626
+ + "<span class='files-actions'>"
11627
+ + newTabButton
11628
+ + "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(path) + "'>Copy path</button>"
11629
+ + (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(path) + "'>Reveal</button>" : "")
11630
+ + "</span>"
11631
+ + "</div>";
11632
+ }
11633
+
11634
+ function buildFileBrowserLocationSelectHtml(state) {
11635
+ const locations = Array.isArray(state && state.locations) ? state.locations : [];
11636
+ if (!locations.length) return "<span class='files-no-locations'>No allowed folders</span>";
11637
+ const rootDir = String((state && state.rootDir) || "");
11638
+ const options = locations.map((location) => {
11639
+ const path = String((location && location.path) || "");
11640
+ const label = String((location && location.label) || basenameForStudioPath(path) || path);
11641
+ return "<option value='" + escapeHtml(path) + "'" + (path === rootDir ? " selected" : "") + ">" + escapeHtml(label + " — " + path) + "</option>";
11642
+ }).join("");
11643
+ return "<select class='files-location-select' data-files-location aria-label='Allowed Files location' title='Choose a folder allowed for this Studio session on the computer running Pi.'>" + options + "</select>";
11644
+ }
11645
+
11126
11646
  function buildFileBrowserPanelHtml() {
11127
11647
  const state = fileBrowserState || {};
11128
11648
  const entries = Array.isArray(state.entries) ? state.entries : [];
11649
+ const exactFiles = Array.isArray(state.exactFiles) ? state.exactFiles : [];
11129
11650
  const currentDir = state.currentDir || "";
11130
11651
  const rootDir = state.rootDir || "";
11131
11652
  const relativeDir = state.relativeDir || ".";
11132
11653
  const parentDisabled = state.parentDir ? "" : " disabled";
11133
11654
  const rows = entries.length
11134
- ? entries.map((entry) => {
11135
- const type = entry.type === "directory" ? "directory" : "file";
11136
- const kind = entry.kind || (type === "directory" ? "directory" : "other");
11137
- const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
11138
- const metaParts = [];
11139
- metaParts.push(getFileBrowserKindLabel(entry));
11140
- if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
11141
- const time = formatFileBrowserTime(entry.mtimeMs);
11142
- if (time) metaParts.push(time);
11143
- const newTabAction = kind === "text" || kind === "office"
11144
- ? "open-new"
11145
- : ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
11146
- const newTabLabel = kind === "text"
11147
- ? "Open file-backed tab"
11148
- : (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
11149
- const newTabTitle = kind === "text"
11150
- ? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
11151
- : (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
11152
- const textActions = newTabAction
11153
- ? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
11154
- : "";
11155
- const openTitle = type === "directory"
11156
- ? "Open folder"
11157
- : (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
11158
- return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
11159
- + "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(entry.path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
11160
- + "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
11161
- + "<span class='files-name'>" + escapeHtml(entry.name) + "</span>"
11162
- + "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
11163
- + "</button>"
11164
- + "<span class='files-actions'>"
11165
- + textActions
11166
- + "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(entry.path) + "'>Copy path</button>"
11167
- + (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(entry.path) + "'>Reveal</button>" : "")
11168
- + "</span>"
11169
- + "</div>";
11170
- }).join("")
11655
+ ? entries.map(buildFileBrowserEntryRowHtml).join("")
11171
11656
  : "<div class='files-empty'>" + (state.loading ? "Loading files…" : "This folder is empty.") + "</div>";
11657
+ const exactRows = exactFiles.map(buildFileBrowserEntryRowHtml).join("");
11172
11658
  const notices = [];
11173
11659
  if (state.error) notices.push("<div class='files-notice files-notice-error'>" + escapeHtml(state.error) + "</div>");
11660
+ if (state.grantRequiredPath) notices.push("<div class='files-notice'>Allow that folder to browse it in Files. Exact-file grants do not expose their parent folders.</div>");
11174
11661
  if (state.omitted) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omitted)) + " item" + (state.omitted === 1 ? "" : "s") + " omitted.</div>");
11175
11662
  if (state.omittedIgnored) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omittedIgnored)) + " heavy/cache folder" + (state.omittedIgnored === 1 ? "" : "s") + " hidden.</div>");
11176
11663
  return "<div class='files-panel'>"
11177
11664
  + "<div class='files-toolbar'>"
11178
11665
  + "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
11179
11666
  + "<div class='files-toolbar-actions'>"
11667
+ + buildFileBrowserLocationSelectHtml(state)
11180
11668
  + buildFileBrowserSortSelectHtml()
11669
+ + "<button type='button' data-files-action='allow-folder'>Allow folder…</button>"
11181
11670
  + "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
11182
11671
  + "<button type='button' data-files-action='refresh'>Refresh</button>"
11183
11672
  + (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
11184
11673
  + (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
11185
- + (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager.'>Open root</button>" : "")
11674
+ + (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager on the computer running Pi.'>Open root</button>" : "")
11186
11675
  + (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
11187
11676
  + "</div>"
11188
11677
  + "</div>"
11189
- + "<div class='files-subtitle'>Root: <span title='" + escapeHtml(rootDir) + "'>" + escapeHtml(rootDir || "current Studio directory") + "</span></div>"
11678
+ + "<div class='files-subtitle'>Allowed root on the computer running Pi: <span title='" + escapeHtml(rootDir) + "'>" + escapeHtml(rootDir || "none selected") + "</span></div>"
11190
11679
  + notices.join("")
11680
+ + (exactRows ? "<section class='files-exact-section'><div class='files-section-title'>Allowed exact files</div><div class='files-list' role='list'>" + exactRows + "</div></section>" : "")
11191
11681
  + "<div class='files-list' role='list'>" + rows + "</div>"
11192
11682
  + "</div>";
11193
11683
  }
@@ -11202,6 +11692,9 @@
11202
11692
  relativeDir: "",
11203
11693
  parentDir: null,
11204
11694
  entries: [],
11695
+ exactFiles: [],
11696
+ locations: [],
11697
+ grantRequiredPath: "",
11205
11698
  omitted: 0,
11206
11699
  omittedIgnored: 0,
11207
11700
  sort: fileBrowserSortMode,
@@ -11223,12 +11716,17 @@
11223
11716
  async function loadFileBrowserDirectory(dir, options) {
11224
11717
  const context = getHtmlPreviewResourceContextOptions();
11225
11718
  const contextKey = getFileBrowserContextKey();
11719
+ const config = options && typeof options === "object" ? options : {};
11720
+ const requestedRoot = Object.prototype.hasOwnProperty.call(config, "root")
11721
+ ? normalizeStudioResourceDirValue(config.root || "")
11722
+ : normalizeStudioResourceDirValue(fileBrowserState.rootDir || "");
11226
11723
  const nonce = ++fileBrowserLoadNonce;
11227
11724
  fileBrowserState = {
11228
11725
  ...fileBrowserState,
11229
11726
  contextKey,
11230
11727
  loading: true,
11231
11728
  error: "",
11729
+ grantRequiredPath: "",
11232
11730
  };
11233
11731
  if (rightView === "files") {
11234
11732
  finishPreviewRender(critiqueViewEl);
@@ -11237,6 +11735,7 @@
11237
11735
  try {
11238
11736
  const query = {};
11239
11737
  if (dir) query.dir = String(dir);
11738
+ if (requestedRoot) query.root = requestedRoot;
11240
11739
  if (context.sourcePath) query.sourcePath = context.sourcePath;
11241
11740
  if (context.resourceDir) query.resourceDir = context.resourceDir;
11242
11741
  query.sort = normalizeFileBrowserSortMode(fileBrowserSortMode);
@@ -11249,6 +11748,9 @@
11249
11748
  relativeDir: typeof payload.relativeDir === "string" ? payload.relativeDir : ".",
11250
11749
  parentDir: typeof payload.parentDir === "string" ? payload.parentDir : null,
11251
11750
  entries: Array.isArray(payload.entries) ? payload.entries : [],
11751
+ exactFiles: Array.isArray(payload.exactFiles) ? payload.exactFiles : [],
11752
+ locations: Array.isArray(payload.locations) ? payload.locations : [],
11753
+ grantRequiredPath: "",
11252
11754
  omitted: Number(payload.omitted) || 0,
11253
11755
  omittedIgnored: Number(payload.omittedIgnored) || 0,
11254
11756
  sort: normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode),
@@ -11262,11 +11764,18 @@
11262
11764
  critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
11263
11765
  scheduleResponsePaneRepaintNudge();
11264
11766
  }
11265
- if (options && options.user) setStatus("Loaded file list.", "success");
11767
+ if (config.user) setStatus("Loaded file list.", "success");
11266
11768
  } catch (error) {
11267
11769
  if (nonce !== fileBrowserLoadNonce) return;
11770
+ const errorPayload = error && error.studioPayload && typeof error.studioPayload === "object" ? error.studioPayload : null;
11771
+ const grantRequiredPath = errorPayload && errorPayload.code === "studio-resource-directory-grant-required" && typeof errorPayload.directoryPath === "string"
11772
+ ? normalizeStudioResourceDirValue(errorPayload.directoryPath)
11773
+ : "";
11268
11774
  fileBrowserState = {
11269
11775
  ...fileBrowserState,
11776
+ exactFiles: errorPayload && Array.isArray(errorPayload.exactFiles) ? errorPayload.exactFiles : fileBrowserState.exactFiles,
11777
+ locations: errorPayload && Array.isArray(errorPayload.locations) ? errorPayload.locations : fileBrowserState.locations,
11778
+ grantRequiredPath,
11270
11779
  loading: false,
11271
11780
  error: (error && error.message) ? error.message : String(error || "Could not load files."),
11272
11781
  loaded: true,
@@ -11314,7 +11823,7 @@
11314
11823
  return;
11315
11824
  }
11316
11825
  if (kind === "pdf") {
11317
- openPreviewPdfLink(path, path, context);
11826
+ await openPreviewPdfLink(path, path, context);
11318
11827
  return;
11319
11828
  }
11320
11829
  if (kind === "image") {
@@ -11324,6 +11833,34 @@
11324
11833
  setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
11325
11834
  }
11326
11835
 
11836
+ async function allowFileBrowserFolder() {
11837
+ const context = getHtmlPreviewResourceContextOptions();
11838
+ const suggested = normalizeStudioResourceDirValue(
11839
+ fileBrowserState.grantRequiredPath
11840
+ || fileBrowserState.currentDir
11841
+ || fileBrowserState.rootDir
11842
+ || context.resourceDir
11843
+ || (context.sourcePath ? dirnameForDisplayPath(context.sourcePath) : "")
11844
+ || "./",
11845
+ );
11846
+ const path = await requestStudioTextInput(
11847
+ "Allow a folder on the computer running Pi for this Studio session:",
11848
+ suggested,
11849
+ { title: "Allow folder", confirmLabel: "Allow folder", inputLabel: "Folder path" },
11850
+ );
11851
+ if (!path) return;
11852
+ const payload = await fetchStudioJson("/resource-grants", {
11853
+ method: "POST",
11854
+ body: JSON.stringify({ grantKind: "directory", path }),
11855
+ });
11856
+ const grantedPath = payload && payload.grant && payload.grant.kind === "directory" && typeof payload.grant.path === "string"
11857
+ ? payload.grant.path
11858
+ : "";
11859
+ if (!grantedPath) throw new Error("Studio did not return the allowed folder.");
11860
+ await loadFileBrowserDirectory("", { root: grantedPath });
11861
+ setStatus(typeof payload.message === "string" ? payload.message : "Allowed folder for this Studio session.", "success");
11862
+ }
11863
+
11327
11864
  function setFileBrowserCurrentDirectoryAsWorkingDir(path) {
11328
11865
  const nextDir = normalizeStudioResourceDirValue(path || fileBrowserState.currentDir || "");
11329
11866
  if (!nextDir) {
@@ -11345,6 +11882,7 @@
11345
11882
  }
11346
11883
  const context = getHtmlPreviewResourceContextOptions();
11347
11884
  const body = { dir: targetDir };
11885
+ if (fileBrowserState.rootDir) body.root = fileBrowserState.rootDir;
11348
11886
  if (context.sourcePath) body.sourcePath = context.sourcePath;
11349
11887
  if (context.resourceDir) body.resourceDir = context.resourceDir;
11350
11888
  const payload = await fetchStudioJson("/file-browser-open", {
@@ -11357,6 +11895,14 @@
11357
11895
  async function handleFilesPaneChange(event) {
11358
11896
  if (rightView !== "files") return;
11359
11897
  const target = event.target;
11898
+ const locationSelect = target instanceof Element ? target.closest("[data-files-location]") : null;
11899
+ if (locationSelect && "value" in locationSelect) {
11900
+ const nextRoot = normalizeStudioResourceDirValue(locationSelect.value || "");
11901
+ if (nextRoot && nextRoot !== fileBrowserState.rootDir) {
11902
+ await loadFileBrowserDirectory("", { root: nextRoot, user: true });
11903
+ }
11904
+ return;
11905
+ }
11360
11906
  const sortSelect = target instanceof Element ? target.closest("[data-files-sort]") : null;
11361
11907
  if (!sortSelect || !("value" in sortSelect)) return;
11362
11908
  const nextSort = writeFileBrowserSortMode(sortSelect.value);
@@ -11375,6 +11921,10 @@
11375
11921
  const path = actionEl.getAttribute("data-files-path") || "";
11376
11922
  const kind = actionEl.getAttribute("data-files-kind") || getPreviewLocalLinkKind(path);
11377
11923
  try {
11924
+ if (action === "allow-folder") {
11925
+ await allowFileBrowserFolder();
11926
+ return;
11927
+ }
11378
11928
  if (action === "parent") {
11379
11929
  if (fileBrowserState.parentDir) await loadFileBrowserDirectory(fileBrowserState.parentDir, { user: true });
11380
11930
  return;
@@ -11392,14 +11942,6 @@
11392
11942
  return;
11393
11943
  }
11394
11944
  if (action === "open-new") {
11395
- if (kind === "text" && isLikelyAbsoluteStudioPath(path)) {
11396
- openFileBackedStudioEditorTab(path, {
11397
- label: basenameForStudioPath(path),
11398
- resourceDir: fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(path),
11399
- });
11400
- setStatus("Opening file-backed document in a new editor.");
11401
- return;
11402
- }
11403
11945
  await openPreviewDocumentInNewEditor(path, getFileBrowserLocalLinkContext());
11404
11946
  return;
11405
11947
  }
@@ -12113,6 +12655,33 @@
12113
12655
  };
12114
12656
  }
12115
12657
 
12658
+ async function ensureSideQuestionContextRootAuthorized(context) {
12659
+ if (!context || context.gatherScope === "none") return { contextRoot: "" };
12660
+ const body = JSON.stringify({
12661
+ gatherScope: context.gatherScope,
12662
+ sourcePath: context.sourcePath,
12663
+ resourceDir: context.resourceDir,
12664
+ contextPath: context.contextPath,
12665
+ });
12666
+ const check = () => fetchStudioJson("/side-question-context-root", { method: "POST", body });
12667
+ try {
12668
+ return await check();
12669
+ } catch (error) {
12670
+ const request = getStudioDirectoryGrantRequest(error);
12671
+ if (!request) throw error;
12672
+ const allowed = await requestStudioDirectoryGrant(request, {
12673
+ message: "Side questions can map, search, and read supported files beneath this folder through read-only context tools. Allow that access for this Studio session?",
12674
+ cancelStatus: "Side-question folder access cancelled.",
12675
+ });
12676
+ if (!allowed) {
12677
+ const cancelled = new Error("Side-question folder access cancelled.");
12678
+ cancelled.studioCancelled = true;
12679
+ throw cancelled;
12680
+ }
12681
+ return check();
12682
+ }
12683
+ }
12684
+
12116
12685
  async function renderSideQuestionMarkdownToHtml(markdown) {
12117
12686
  const source = String(markdown || "");
12118
12687
  if (sideQuestionMarkdownRenderCache.has(source)) return sideQuestionMarkdownRenderCache.get(source);
@@ -12137,10 +12706,15 @@
12137
12706
  const html = await renderSideQuestionMarkdownToHtml(markdown);
12138
12707
  if (nonce !== sideQuestionPreviewRenderNonce || rightView !== "side-questions" || !critiqueViewEl.contains(target)) return;
12139
12708
  target.innerHTML = html;
12709
+ await hydrateStudioPreviewLocalMedia(target, getHtmlPreviewResourceContextOptions());
12140
12710
  await renderAnnotationMathInElement(target);
12711
+ decoratePdfEmbeds(target);
12712
+ await renderPdfPreviewsInElement(target);
12713
+ decoratePreviewPdfFigures(target);
12141
12714
  await renderMermaidInElement(target);
12142
12715
  await renderMathFallbackInElement(target);
12143
12716
  decorateCopyablePreviewBlocks(target);
12717
+ decoratePreviewImages(target);
12144
12718
  } catch (error) {
12145
12719
  console.error("Side-question markdown preview failed:", error);
12146
12720
  target.classList.add("side-question-markdown-failed");
@@ -12199,6 +12773,7 @@
12199
12773
  ], sideQuestionUi.thinking) + "</select></label>"
12200
12774
  + "</div>"
12201
12775
  + "<details class='side-question-context-rule'><summary id='sideQuestionContextRule'>Automatic: selection → heading block at cursor → nearby text</summary><p>A heading block starts at the nearest Markdown/LaTeX heading above the cursor and ends before the next heading of the same or higher level. With no heading, Studio uses the surrounding text block or a nearby excerpt.</p></details>"
12776
+ + (scope === "none" ? "" : "<p class='side-question-context-access-note'>Related-file access is limited to folders allowed for this Studio session. Studio asks before starting if this folder is not already allowed.</p>")
12202
12777
  + (scope === "custom" ? "<label class='side-question-path-label'>Folder path<input data-side-question-field='customPath' type='text' value='" + escapeHtml(sideQuestionUi.customPath) + "' placeholder='Folder on the computer running Pi'></label>" : "")
12203
12778
  + "<div class='side-question-checks'>"
12204
12779
  + "<label><input data-side-question-field='includeConversation' type='checkbox'" + (sideQuestionUi.includeConversation ? " checked" : "") + "> Include the current main conversation snapshot</label>"
@@ -12209,7 +12784,7 @@
12209
12784
  + "<dl class='side-question-context-summary'><div><dt>Starting text</dt><dd>" + escapeHtml(summary.attachmentText) + "</dd></div><div><dt>Related files</dt><dd>" + escapeHtml(summary.relatedFilesText) + "</dd></div>" + (summary.gitContextText ? "<div><dt>Git context</dt><dd>" + escapeHtml(summary.gitContextText) + "</dd></div>" : "") + "</dl>"
12210
12785
  + "<label class='side-question-composer-label'>Question<textarea data-side-question-field='draft' rows='4' title='Enter adds a new line. Cmd/Ctrl+Enter asks the side question.' placeholder='Ask about the starting text or anything you want checked…'>" + escapeHtml(sideQuestionUi.draft) + "</textarea></label>"
12211
12786
  + (sideQuestionState && sideQuestionState.error ? "<div class='side-question-error'>" + escapeHtml(sideQuestionState.error) + "</div>" : "")
12212
- + "<div class='side-question-actions'><button type='button' class='side-question-primary' data-side-question-action='ask' aria-keyshortcuts='Meta+Enter Control+Enter' title='Ask side question (Cmd/Ctrl+Enter)'" + (!isSideQuestionConnectionReady() || (sideQuestionState && sideQuestionState.status === "running") || !sideQuestionUi.draft.trim() || (scope === "custom" && !sideQuestionUi.customPath.trim()) ? " disabled" : "") + ">" + (sideQuestionState && sideQuestionState.status === "running" ? "Preparing side thread…" : "Ask side question") + "</button></div>"
12787
+ + "<div class='side-question-actions'><button type='button' class='side-question-primary' data-side-question-action='ask' aria-keyshortcuts='Meta+Enter Control+Enter' title='Ask side question (Cmd/Ctrl+Enter)'" + (sideQuestionContextGrantPending || !isSideQuestionConnectionReady() || (sideQuestionState && sideQuestionState.status === "running") || !sideQuestionUi.draft.trim() || (scope === "custom" && !sideQuestionUi.customPath.trim()) ? " disabled" : "") + ">" + (sideQuestionContextGrantPending ? "Checking related-file access…" : (sideQuestionState && sideQuestionState.status === "running" ? "Preparing side thread…" : "Ask side question")) + "</button></div>"
12213
12788
  + "</div>";
12214
12789
  }
12215
12790
 
@@ -12280,7 +12855,8 @@
12280
12855
  }
12281
12856
  }
12282
12857
 
12283
- function submitSideQuestion() {
12858
+ async function submitSideQuestion() {
12859
+ if (sideQuestionContextGrantPending) return;
12284
12860
  const question = String(sideQuestionUi.draft || "").trim();
12285
12861
  if (!question) {
12286
12862
  setStatus("Enter a side question first.", "warning");
@@ -12299,11 +12875,31 @@
12299
12875
  if (sideQuestionState && sideQuestionState.threadId) {
12300
12876
  message.threadId = sideQuestionState.threadId;
12301
12877
  } else {
12302
- message.context = buildSideQuestionContextPayload();
12303
- if (message.context.gatherScope === "custom" && !String(message.context.contextPath || "").trim()) {
12878
+ const context = buildSideQuestionContextPayload();
12879
+ if (context.gatherScope === "custom" && !String(context.contextPath || "").trim()) {
12304
12880
  setStatus("Choose a custom context path first.", "warning");
12305
12881
  return;
12306
12882
  }
12883
+ if (context.gatherScope !== "none") {
12884
+ sideQuestionContextGrantPending = true;
12885
+ if (rightView === "side-questions") renderSideQuestionView();
12886
+ setStatus("Checking side-question folder access…", "warning");
12887
+ try {
12888
+ await ensureSideQuestionContextRootAuthorized(context);
12889
+ } catch (error) {
12890
+ if (!(error && error.studioCancelled)) {
12891
+ const messageText = error && error.message ? error.message : String(error || "Could not authorize side-question context.");
12892
+ if (!sideQuestionState) sideQuestionState = normalizeSideQuestionState(null);
12893
+ sideQuestionState.error = messageText;
12894
+ setStatus("Could not use related files: " + messageText, "warning");
12895
+ }
12896
+ return;
12897
+ } finally {
12898
+ sideQuestionContextGrantPending = false;
12899
+ if (rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) renderSideQuestionView();
12900
+ }
12901
+ }
12902
+ message.context = context;
12307
12903
  }
12308
12904
  if (!sendMessage(message)) return;
12309
12905
  sideQuestionUi.draft = "";
@@ -12419,7 +13015,7 @@
12419
13015
  event.preventDefault();
12420
13016
  const action = target.getAttribute("data-side-question-action");
12421
13017
  if (action === "ask") {
12422
- submitSideQuestion();
13018
+ await submitSideQuestion();
12423
13019
  } else if (action === "stop") {
12424
13020
  if (sideQuestionState && sideQuestionState.threadId && sideQuestionState.requestId) {
12425
13021
  sendMessage({ type: "side_question_cancel_request", threadId: sideQuestionState.threadId, requestId: sideQuestionState.requestId });
@@ -12472,7 +13068,7 @@
12472
13068
  if (field === "draft") sideQuestionUi.draft = target.value;
12473
13069
  if (field === "customPath") sideQuestionUi.customPath = target.value;
12474
13070
  const askButton = critiqueViewEl.querySelector("[data-side-question-action='ask']");
12475
- if (askButton) askButton.disabled = !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
13071
+ if (askButton) askButton.disabled = sideQuestionContextGrantPending || !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
12476
13072
  }
12477
13073
 
12478
13074
  function handleSideQuestionKeydown(event) {
@@ -12486,7 +13082,7 @@
12486
13082
  if (!submitShortcut) return;
12487
13083
  event.preventDefault();
12488
13084
  event.stopPropagation();
12489
- if (!sideQuestionState || sideQuestionState.status !== "running") submitSideQuestion();
13085
+ if (!sideQuestionState || sideQuestionState.status !== "running") void submitSideQuestion();
12490
13086
  }
12491
13087
 
12492
13088
  async function handleSideQuestionChange(event) {
@@ -14305,6 +14901,11 @@
14305
14901
  return launch.fail(message || "Studio could not prepare this tab. Return to the originating Studio page for details.");
14306
14902
  }
14307
14903
 
14904
+ function cancelPendingStudioTab(launch, message) {
14905
+ if (!launch || typeof launch.cancel !== "function") return false;
14906
+ return launch.cancel(message || "Studio tab launch was cancelled.");
14907
+ }
14908
+
14308
14909
  function abandonAllStudioTabLaunches(message) {
14309
14910
  const launches = Array.from(activeStudioTabLaunches);
14310
14911
  activeStudioTabLaunches.clear();
@@ -14383,7 +14984,10 @@
14383
14984
  const message = payload && typeof payload.error === "string"
14384
14985
  ? payload.error
14385
14986
  : (response.status + " " + response.statusText).trim();
14386
- throw new Error(message || (method + " " + pathname + " failed."));
14987
+ const requestError = new Error(message || (method + " " + pathname + " failed."));
14988
+ requestError.studioPayload = payload && typeof payload === "object" ? payload : null;
14989
+ requestError.studioStatus = response.status;
14990
+ throw requestError;
14387
14991
  }
14388
14992
  return payload;
14389
14993
  }
@@ -14416,6 +15020,7 @@
14416
15020
  ]);
14417
15021
  let previewLinkMenuEl = null;
14418
15022
  let activePreviewLinkContext = null;
15023
+ let previewLinkMenuRequestId = 0;
14419
15024
 
14420
15025
  function stripPreviewLocalLinkUrlSuffix(href) {
14421
15026
  const raw = String(href || "").trim();
@@ -14482,7 +15087,7 @@
14482
15087
  if (!raw || raw.charAt(0) === "#") return false;
14483
15088
  if (/^\/\//.test(raw)) return false;
14484
15089
  if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
14485
- if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
15090
+ if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|resource-grants|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
14486
15091
  return true;
14487
15092
  }
14488
15093
 
@@ -14552,19 +15157,35 @@
14552
15157
  menu.style.top = y + "px";
14553
15158
  }
14554
15159
 
14555
- function showPreviewLinkMenu(anchor, event, contextOverride) {
15160
+ async function showPreviewLinkMenu(anchor, event, contextOverride) {
14556
15161
  const href = String(anchor && anchor.getAttribute ? anchor.getAttribute("href") || "" : (contextOverride && contextOverride.href ? contextOverride.href : "")).trim();
14557
15162
  if (!isStudioLocalPreviewHref(href)) return false;
14558
15163
  const kind = getPreviewLocalLinkKind(href);
14559
- const menu = ensurePreviewLinkMenu();
14560
- menu.innerHTML = "";
14561
15164
  const linkContext = getEffectivePreviewLinkContext(contextOverride);
14562
- activePreviewLinkContext = {
15165
+ const nextContext = {
14563
15166
  href,
14564
15167
  title: String((contextOverride && contextOverride.title) || (anchor && anchor.textContent) || href || "local link").trim() || href,
14565
15168
  sourcePath: linkContext.sourcePath,
14566
15169
  resourceDir: linkContext.resourceDir,
14567
15170
  };
15171
+ const menuPoint = {
15172
+ clientX: event && event.clientX,
15173
+ clientY: event && event.clientY,
15174
+ };
15175
+ closePreviewLinkMenu();
15176
+ const menuRequestId = ++previewLinkMenuRequestId;
15177
+ try {
15178
+ await fetchPreviewLocalLink("resolve", href, nextContext);
15179
+ } catch (error) {
15180
+ if (!(error && error.studioCancelled)) {
15181
+ setStatus((error && error.message) ? error.message : String(error || "Could not inspect this local resource."), "warning");
15182
+ }
15183
+ return false;
15184
+ }
15185
+ if (menuRequestId !== previewLinkMenuRequestId) return false;
15186
+ const menu = ensurePreviewLinkMenu();
15187
+ menu.innerHTML = "";
15188
+ activePreviewLinkContext = nextContext;
14568
15189
  if (kind === "pdf") {
14569
15190
  appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
14570
15191
  appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
@@ -14581,7 +15202,7 @@
14581
15202
  }
14582
15203
  appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
14583
15204
  appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
14584
- positionPreviewLinkMenu(menu, event && event.clientX, event && event.clientY);
15205
+ positionPreviewLinkMenu(menu, menuPoint.clientX, menuPoint.clientY);
14585
15206
  const firstButton = menu.querySelector("button");
14586
15207
  if (firstButton && typeof firstButton.focus === "function") {
14587
15208
  window.setTimeout(() => firstButton.focus({ preventScroll: true }), 0);
@@ -14589,10 +15210,102 @@
14589
15210
  return true;
14590
15211
  }
14591
15212
 
14592
- async function fetchPreviewLocalLink(action, href, contextOverride) {
14593
- return fetchStudioJson("/local-preview-link", {
15213
+ function getStudioResourceGrantRequest(error) {
15214
+ const payload = error && error.studioPayload && typeof error.studioPayload === "object"
15215
+ ? error.studioPayload
15216
+ : null;
15217
+ if (!payload || payload.code !== "studio-resource-grant-required") return null;
15218
+ const path = typeof payload.path === "string" ? payload.path.trim() : "";
15219
+ const directoryPath = typeof payload.directoryPath === "string" ? payload.directoryPath.trim() : "";
15220
+ if (!path || !directoryPath) return null;
15221
+ return {
15222
+ path,
15223
+ directoryPath,
15224
+ label: typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : basenameForStudioPath(path),
15225
+ };
15226
+ }
15227
+
15228
+ function getStudioDirectoryGrantRequest(error) {
15229
+ const payload = error && error.studioPayload && typeof error.studioPayload === "object"
15230
+ ? error.studioPayload
15231
+ : null;
15232
+ if (!payload || payload.code !== "studio-resource-directory-grant-required") return null;
15233
+ const directoryPath = typeof payload.directoryPath === "string" ? payload.directoryPath.trim() : "";
15234
+ if (!directoryPath) return null;
15235
+ return {
15236
+ directoryPath,
15237
+ label: typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : basenameForStudioPath(directoryPath),
15238
+ };
15239
+ }
15240
+
15241
+ async function requestStudioDirectoryGrant(request, options) {
15242
+ if (!request || !request.directoryPath) return false;
15243
+ const config = options && typeof options === "object" ? options : {};
15244
+ const choice = await openStudioDecision({
15245
+ mode: "confirm",
15246
+ title: "Allow " + (request.label || "this folder") + "?",
15247
+ message: (config.message || "Allow read-only access beneath this folder for the current Studio session?")
15248
+ + "\n\nFolder on the computer running Pi:\n" + request.directoryPath,
15249
+ cancelLabel: "Cancel",
15250
+ confirmLabel: "Allow this folder for this Studio session",
15251
+ });
15252
+ if (choice !== true) {
15253
+ setStatus(config.cancelStatus || "Folder access cancelled.", "warning");
15254
+ return false;
15255
+ }
15256
+ const payload = await fetchStudioJson("/resource-grants", {
15257
+ method: "POST",
15258
+ body: JSON.stringify({ grantKind: "directory", path: request.directoryPath }),
15259
+ });
15260
+ fileBrowserState = { ...fileBrowserState, contextKey: "", loaded: false };
15261
+ setStatus(typeof payload.message === "string" ? payload.message : "Allowed this folder for the current Studio session.", "success");
15262
+ return true;
15263
+ }
15264
+
15265
+ async function requestStudioResourceGrant(request) {
15266
+ if (!request) return false;
15267
+ const choice = await openStudioDecision({
15268
+ mode: "confirm",
15269
+ title: "Allow " + request.label + "?",
15270
+ message: "This local resource is outside the locations currently available to Studio.\n\n"
15271
+ + "File on the computer running Pi:\n" + request.path + "\n\n"
15272
+ + "Allow only this file, or allow its containing folder for this Studio session?",
15273
+ cancelLabel: "Cancel",
15274
+ secondaryLabel: "Allow this folder for this Studio session",
15275
+ secondaryValue: "directory",
15276
+ confirmLabel: "Allow this file",
15277
+ });
15278
+ const grantKind = choice === true ? "file" : choice === "directory" ? "directory" : "";
15279
+ if (!grantKind) {
15280
+ setStatus("Local resource access cancelled.", "warning");
15281
+ return false;
15282
+ }
15283
+ const grantPath = grantKind === "directory" ? request.directoryPath : request.path;
15284
+ const payload = await fetchStudioJson("/resource-grants", {
15285
+ method: "POST",
15286
+ body: JSON.stringify({ grantKind, path: grantPath }),
15287
+ });
15288
+ fileBrowserState = { ...fileBrowserState, contextKey: "", loaded: false };
15289
+ setStatus(typeof payload.message === "string" ? payload.message : "Allowed local resource for this Studio session.", "success");
15290
+ return true;
15291
+ }
15292
+
15293
+ async function fetchPreviewLocalLink(action, href, contextOverride, options) {
15294
+ const request = () => fetchStudioJson("/local-preview-link", {
14594
15295
  query: { ...getPreviewLinkResourceQuery(href, contextOverride), action },
14595
15296
  });
15297
+ try {
15298
+ return await request();
15299
+ } catch (error) {
15300
+ const grantRequest = getStudioResourceGrantRequest(error);
15301
+ if (!grantRequest || (options && options.skipGrantPrompt === true)) throw error;
15302
+ if (!(await requestStudioResourceGrant(grantRequest))) {
15303
+ const cancelled = new Error("Local resource access cancelled.");
15304
+ cancelled.studioCancelled = true;
15305
+ throw cancelled;
15306
+ }
15307
+ return request();
15308
+ }
14596
15309
  }
14597
15310
 
14598
15311
  function getPreviewPdfViewerUrl(href, contextOverride) {
@@ -14603,16 +15316,16 @@
14603
15316
  return resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
14604
15317
  }
14605
15318
 
14606
- function openPreviewPdfLink(href, title, contextOverride) {
15319
+ async function openPreviewPdfLink(href, title, contextOverride) {
15320
+ await fetchPreviewLocalLink("resolve", href, contextOverride);
14607
15321
  const viewerUrl = getPreviewPdfViewerUrl(href, contextOverride);
14608
15322
  if (!viewerUrl) {
14609
15323
  setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
14610
15324
  return false;
14611
15325
  }
14612
- const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
14613
15326
  const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
14614
15327
  const resourceQuery = buildStudioPdfResourceQuery({
14615
- path: cleanPath,
15328
+ path: stripPreviewLocalLinkUrlSuffix(href),
14616
15329
  sourcePath: context.sourcePath || "",
14617
15330
  resourceDir: context.resourceDir || "",
14618
15331
  }, true);
@@ -14621,6 +15334,7 @@
14621
15334
  }
14622
15335
 
14623
15336
  async function openPreviewImageLink(href, title, contextOverride) {
15337
+ await fetchPreviewLocalLink("resolve", href, contextOverride);
14624
15338
  const payload = await fetchStudioJson("/html-preview-resource", {
14625
15339
  query: getPreviewLinkResourceQuery(href, contextOverride),
14626
15340
  });
@@ -14722,7 +15436,11 @@
14722
15436
  navigatePendingStudioTab(launch, relativeUrl);
14723
15437
  setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.");
14724
15438
  } catch (error) {
14725
- failPendingStudioTab(launch, "Studio could not prepare this document tab. Return to the originating Studio page for details.");
15439
+ if (error && error.studioCancelled) {
15440
+ cancelPendingStudioTab(launch, "Local resource access was cancelled.");
15441
+ } else {
15442
+ failPendingStudioTab(launch, "Studio could not prepare this document tab. Return to the originating Studio page for details.");
15443
+ }
14726
15444
  throw error;
14727
15445
  }
14728
15446
  }
@@ -14737,7 +15455,11 @@
14737
15455
  navigatePendingStudioTab(launch, relativeUrl);
14738
15456
  setStatus("Opening preview in a new Studio tab.");
14739
15457
  } catch (error) {
14740
- failPendingStudioTab(launch, "Studio could not prepare this preview tab. Return to the originating Studio page for details.");
15458
+ if (error && error.studioCancelled) {
15459
+ cancelPendingStudioTab(launch, "Local resource access was cancelled.");
15460
+ } else {
15461
+ failPendingStudioTab(launch, "Studio could not prepare this preview tab. Return to the originating Studio page for details.");
15462
+ }
14741
15463
  throw error;
14742
15464
  }
14743
15465
  }
@@ -14752,6 +15474,7 @@
14752
15474
  }
14753
15475
 
14754
15476
  async function revealPreviewLocalLink(href, contextOverride) {
15477
+ await fetchPreviewLocalLink("resolve", href, contextOverride);
14755
15478
  const query = getPreviewLinkResourceQuery(href, contextOverride);
14756
15479
  const payload = await fetchStudioJson("/reveal-local-resource", {
14757
15480
  method: "POST",
@@ -14765,10 +15488,11 @@
14765
15488
  if (!href) return;
14766
15489
  try {
14767
15490
  if (action === "open-pdf") {
14768
- openPreviewPdfLink(href, context.title || href, context);
15491
+ await openPreviewPdfLink(href, context.title || href, context);
14769
15492
  return;
14770
15493
  }
14771
15494
  if (action === "open-system") {
15495
+ await fetchPreviewLocalLink("resolve", href, context);
14772
15496
  await runStudioPdfLocalAction("system-viewer", getPreviewLinkResourceQuery(href, context));
14773
15497
  return;
14774
15498
  }
@@ -14810,7 +15534,9 @@
14810
15534
  closePreviewLinkMenu();
14811
15535
  const title = String(anchor.textContent || href).trim() || href;
14812
15536
  if (kind === "pdf") {
14813
- openPreviewPdfLink(href, title);
15537
+ void openPreviewPdfLink(href, title).catch((error) => {
15538
+ setStatus((error && error.message) ? error.message : String(error || "Could not open linked PDF."), "warning");
15539
+ });
14814
15540
  return;
14815
15541
  }
14816
15542
  if (kind === "image") {
@@ -14820,9 +15546,7 @@
14820
15546
  return;
14821
15547
  }
14822
15548
  if (kind === "text" || kind === "office") {
14823
- void openPreviewDocumentInNewEditor(href).catch((error) => {
14824
- setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
14825
- });
15549
+ void showPreviewLinkMenu(anchor, event);
14826
15550
  return;
14827
15551
  }
14828
15552
  setStatus("Right-click this local link for file actions.", "warning");
@@ -14833,7 +15557,7 @@
14833
15557
  if (!anchor) return;
14834
15558
  event.preventDefault();
14835
15559
  event.stopPropagation();
14836
- showPreviewLinkMenu(anchor, event);
15560
+ void showPreviewLinkMenu(anchor, event);
14837
15561
  }
14838
15562
 
14839
15563
  function makeRequestId() {
@@ -15012,12 +15736,15 @@
15012
15736
  const html = await renderQuizMarkdownToHtml(markdown);
15013
15737
  if (nonce !== quizPreviewRenderNonce || !quizDialogEl || !quizDialogEl.contains(target)) return;
15014
15738
  target.innerHTML = html;
15739
+ await hydrateStudioPreviewLocalMedia(target, getHtmlPreviewResourceContextOptions());
15015
15740
  await renderAnnotationMathInElement(target);
15016
15741
  decoratePdfEmbeds(target);
15017
15742
  await renderPdfPreviewsInElement(target);
15743
+ decoratePreviewPdfFigures(target);
15018
15744
  await renderMermaidInElement(target);
15019
15745
  await renderMathFallbackInElement(target);
15020
15746
  decorateCopyablePreviewBlocks(target);
15747
+ decoratePreviewImages(target);
15021
15748
  if (preserveScroll) restoreQuizScrollTopSoon(scrollTop);
15022
15749
  } catch (error) {
15023
15750
  console.error("Quiz markdown preview render failed:", error);
@@ -23552,6 +24279,9 @@
23552
24279
  });
23553
24280
  }
23554
24281
 
24282
+ document.addEventListener("click", handleStudioPreviewMediaActivation, true);
24283
+ document.addEventListener("keydown", handleStudioPreviewMediaKeydown, true);
24284
+
23555
24285
  document.addEventListener("click", (event) => {
23556
24286
  const target = event.target;
23557
24287
  const focusBtn = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;