pi-studio 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.2] — 2026-05-16
8
+
9
+ ### Added
10
+ - Added a global **⊙ Zen** / **Exit Zen** toggle that hides secondary Studio chrome while preserving the current pane layout and panel focus state.
11
+ - Added a Focus action for explicit `studio-pdf` preview cards, opening the embedded PDF in a larger Studio overlay with optional browser fullscreen.
12
+
13
+ ### Fixed
14
+ - HTML export no longer lets Pandoc's standalone template/CSS leak into the exported document for local-resource previews, fixing narrow/mobile-like exports for larger documents with embedded assets.
15
+ - HTML/PDF export subprocess handling now uses bounded output capture and explicit timeout paths without truncating successful embedded-asset HTML renders.
16
+ - PDF Focus now works from response previews before the same card has been rendered in editor preview, and its controls use theme-consistent focus/fullscreen icons.
17
+
7
18
  ## [0.9.1] — 2026-05-15
8
19
 
9
20
  ### Added
package/README.md CHANGED
@@ -20,6 +20,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
20
20
 
21
21
  - Opens a two-pane browser workspace: **Editor** (left) + **Response/Working/Editor Preview** (right)
22
22
  - Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces; the editor toolbar can open a detached copy of the current editor text as a companion view
23
+ - Includes a global **⊙ Zen** mode for hiding secondary Studio chrome without changing the current left/right pane layout
23
24
  - Runs editor text directly, or asks for structured critique (auto/writing/code focus)
24
25
  - Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters, image previews for image-producing tool outputs, plus **Load visible into editor** and **Copy visible** actions; when cycling response history, Working follows saved working details for the selected response when available
25
26
  - Includes an optional tmux-backed **REPL** view for Shell, Python, IPython, Julia, R, GHCi, and Clojure sessions, with Raw/Literate send modes, `Cmd/Ctrl+Shift+Enter` **Send to REPL**, session start/new/stop/interrupt controls, a compact refresh-persistent **REPL Studio** record of user and Pi-sent code, a secondary raw tmux mirror, agent-facing `studio_repl_status` / `studio_repl_send` tools, and Markdown/PDF/HTML export
@@ -37,7 +38,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
37
38
  - saves `.annotated.md`
38
39
  - Renders Markdown/LaTeX/code previews (math + Mermaid), theme-synced with pi, with copy buttons for code blocks and blockquotes
39
40
  - Renders straight, unfenced interactive HTML in preview via a sandboxed browser iframe with zoom controls, while fenced `html` blocks remain source code
40
- - Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks
41
+ - Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks, with a Focus action for temporarily enlarging the embedded viewer
41
42
  - Ships optional `pi-studio-dark` and `pi-studio-light` themes tuned for Studio's browser workspace
42
43
  - Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML, preserving authored HTML previews as HTML
43
44
  - Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf` or `/studio-html <path>` to `<name>.studio.html`; without a path, those commands export the last model response to a timestamped file
@@ -110,6 +111,7 @@ caption: Optional caption
110
111
  - Full preview/PDF quality depends on `pandoc` (and `xelatex` for PDF):
111
112
  - `brew install pandoc`
112
113
  - install TeX Live/MacTeX for PDF export
114
+ - Export subprocess timeouts default to bounded values and can be tuned with `PI_STUDIO_PANDOC_TIMEOUT_MS`, `PI_STUDIO_LATEX_TIMEOUT_MS`, `PI_STUDIO_MERMAID_TIMEOUT_MS`, and `PI_STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES` for unusually large embedded-asset HTML exports.
113
115
  - Mermaid diagrams in exported PDFs may also require Mermaid CLI (`mmdc` / `@mermaid-js/mermaid-cli`) when you want diagram blocks rendered as diagrams rather than left as code.
114
116
 
115
117
  ## License
@@ -104,6 +104,7 @@
104
104
  const sendEditorBtn = document.getElementById("sendEditorBtn");
105
105
  const openCompanionBtn = document.getElementById("openCompanionBtn");
106
106
  const getEditorBtn = document.getElementById("getEditorBtn");
107
+ const zenModeBtn = document.getElementById("zenModeBtn");
107
108
  const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
108
109
  const sendRunBtn = document.getElementById("sendRunBtn");
109
110
  const queueSteerBtn = document.getElementById("queueSteerBtn");
@@ -180,6 +181,16 @@
180
181
  let statusLevel = "";
181
182
  let reconnectTimer = null;
182
183
  let reconnectAttempt = 0;
184
+ let studioPdfFocusOverlayEl = null;
185
+ let studioPdfFocusDialogEl = null;
186
+ let studioPdfFocusFrameSlotEl = null;
187
+ let studioPdfFocusFrameEl = null;
188
+ let studioPdfFocusTitleEl = null;
189
+ let studioPdfFocusOpenLinkEl = null;
190
+ let studioPdfFocusFullscreenBtn = null;
191
+ let studioPdfFocusCloseBtn = null;
192
+ let studioPdfFocusLastFocusedEl = null;
193
+ let studioPdfFocusMovedFrameState = null;
183
194
  let pendingRequestId = null;
184
195
  let pendingKind = null;
185
196
  let stickyStudioKind = null;
@@ -221,6 +232,8 @@
221
232
  const REPL_TRANSCRIPT_MAX_CHARS = 200_000;
222
233
  const REPL_JOURNAL_OUTPUT_MAX_CHARS = 80_000;
223
234
  const REPL_JOURNAL_MAX_ENTRIES = 80;
235
+ const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
236
+ const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
224
237
  const EDITOR_TAB_TEXT = " ";
225
238
  let replTmuxAvailable = null;
226
239
  let replSessions = [];
@@ -1667,6 +1680,7 @@
1667
1680
  let lineNumbersRenderRaf = null;
1668
1681
  let annotationsEnabled = true;
1669
1682
  const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
1683
+ const STUDIO_ZEN_MODE_STORAGE_KEY = "piStudio.zenMode";
1670
1684
  const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
1671
1685
  const EDITOR_FONT_SIZE_OPTIONS = [10, 11, 12, 13, 14, 15, 16, 18];
1672
1686
  const RESPONSE_FONT_SIZE_OPTIONS = [11, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 18, 20];
@@ -1675,9 +1689,13 @@
1675
1689
  let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
1676
1690
  let responseFontSize = DEFAULT_RESPONSE_FONT_SIZE;
1677
1691
  let studioUiRefreshUi = null;
1692
+ let studioZenModeEnabled = readStudioZenModeEnabled();
1678
1693
  if (studioUiRefreshEnabled && document.body) {
1679
1694
  document.body.classList.add("studio-ui-refresh");
1680
1695
  }
1696
+ if (studioZenModeEnabled && document.body) {
1697
+ document.body.classList.add("studio-zen-mode");
1698
+ }
1681
1699
  let scratchpadText = "";
1682
1700
  let scratchpadReturnFocusEl = null;
1683
1701
  let scratchpadPersistTimer = null;
@@ -1719,6 +1737,46 @@
1719
1737
  return true;
1720
1738
  }
1721
1739
 
1740
+ function readStudioZenModeEnabled() {
1741
+ const normalize = (value) => String(value == null ? "" : value).trim().toLowerCase();
1742
+ const isTruthy = (value) => ["1", "true", "yes", "on", "zen"].indexOf(normalize(value)) !== -1;
1743
+ const isFalsey = (value) => ["0", "false", "no", "off"].indexOf(normalize(value)) !== -1;
1744
+ const queryValue = initialQueryParams.has("zen") ? initialQueryParams.get("zen") : null;
1745
+ if (queryValue !== null) {
1746
+ const normalizedQuery = normalize(queryValue);
1747
+ const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
1748
+ try {
1749
+ window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, enabled ? "1" : "0");
1750
+ } catch {}
1751
+ return enabled;
1752
+ }
1753
+ try {
1754
+ const stored = window.localStorage ? window.localStorage.getItem(STUDIO_ZEN_MODE_STORAGE_KEY) : null;
1755
+ if (stored === null) return false;
1756
+ return isTruthy(stored) || (!isFalsey(stored) && normalize(stored) !== "");
1757
+ } catch {
1758
+ return false;
1759
+ }
1760
+ }
1761
+
1762
+ function syncStudioZenModeUi() {
1763
+ if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
1764
+ if (!zenModeBtn) return;
1765
+ zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "⊙ Zen";
1766
+ zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls." : "Hide secondary Studio controls.";
1767
+ zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
1768
+ }
1769
+
1770
+ function setStudioZenMode(enabled) {
1771
+ studioZenModeEnabled = Boolean(enabled);
1772
+ try {
1773
+ window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, studioZenModeEnabled ? "1" : "0");
1774
+ } catch {}
1775
+ closeStudioUiRefreshMenus();
1776
+ closeExportPreviewMenu();
1777
+ syncStudioZenModeUi();
1778
+ }
1779
+
1722
1780
  function makeStudioUiRefreshElement(tagName, className, text) {
1723
1781
  const element = document.createElement(tagName);
1724
1782
  if (className) element.className = className;
@@ -1735,9 +1793,16 @@
1735
1793
  svg.setAttribute("viewBox", "0 0 24 24");
1736
1794
  svg.setAttribute("aria-hidden", "true");
1737
1795
  svg.classList.add("studio-refresh-icon");
1738
- const paths = kind === "focus-exit"
1739
- ? ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"]
1740
- : ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
1796
+ let paths;
1797
+ if (kind === "focus-exit") {
1798
+ paths = ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"];
1799
+ } else if (kind === "fullscreen") {
1800
+ paths = ["M8 4H4v4", "M16 4h4v4", "M20 16v4h-4", "M4 16v4h4"];
1801
+ } else if (kind === "fullscreen-exit") {
1802
+ paths = ["M9 5v4H5", "M15 5v4h4", "M19 15h-4v4", "M5 15h4v4"];
1803
+ } else {
1804
+ paths = ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
1805
+ }
1741
1806
  for (const d of paths) {
1742
1807
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1743
1808
  path.setAttribute("d", d);
@@ -2098,6 +2163,7 @@
2098
2163
 
2099
2164
  setupStudioUiRefreshToggleButton();
2100
2165
  setupStudioUiRefreshPrototype();
2166
+ syncStudioZenModeUi();
2101
2167
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
2102
2168
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
2103
2169
  throw new Error("Studio annotation helpers failed to load.");
@@ -2979,6 +3045,18 @@
2979
3045
  && typeof outlineDialogEl.contains === "function"
2980
3046
  && outlineDialogEl.contains(event.target)
2981
3047
  );
3048
+ const pdfFocusOwnsEvent = Boolean(
3049
+ studioPdfFocusDialogEl
3050
+ && event.target
3051
+ && typeof studioPdfFocusDialogEl.contains === "function"
3052
+ && studioPdfFocusDialogEl.contains(event.target)
3053
+ );
3054
+
3055
+ if (isStudioPdfFocusOpen() && plainEscape) {
3056
+ event.preventDefault();
3057
+ closeStudioPdfFocusViewer();
3058
+ return;
3059
+ }
2982
3060
 
2983
3061
  if (isScratchpadOpen() && plainEscape) {
2984
3062
  event.preventDefault();
@@ -2998,7 +3076,7 @@
2998
3076
  return;
2999
3077
  }
3000
3078
 
3001
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent) {
3079
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent) {
3002
3080
  return;
3003
3081
  }
3004
3082
 
@@ -3847,50 +3925,316 @@
3847
3925
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
3848
3926
  }
3849
3927
 
3850
- function buildStudioPdfResourceUrl(options) {
3928
+ function isStudioPdfFocusOpen() {
3929
+ return Boolean(studioPdfFocusOverlayEl && studioPdfFocusOverlayEl.hidden === false);
3930
+ }
3931
+
3932
+ function ensureStudioPdfFocusViewer() {
3933
+ if (studioPdfFocusOverlayEl) return studioPdfFocusOverlayEl;
3934
+
3935
+ const overlay = document.createElement("div");
3936
+ overlay.className = "studio-pdf-focus-overlay";
3937
+ overlay.hidden = true;
3938
+ overlay.setAttribute("role", "dialog");
3939
+ overlay.setAttribute("aria-modal", "true");
3940
+ overlay.setAttribute("aria-labelledby", "studioPdfFocusTitle");
3941
+
3942
+ const dialog = document.createElement("div");
3943
+ dialog.className = "studio-pdf-focus-dialog";
3944
+
3945
+ const header = document.createElement("div");
3946
+ header.className = "studio-pdf-focus-header";
3947
+
3948
+ const titleGroup = document.createElement("div");
3949
+ titleGroup.className = "studio-pdf-focus-title-group";
3950
+
3951
+ const closeBtn = document.createElement("button");
3952
+ closeBtn.type = "button";
3953
+ closeBtn.className = "studio-pdf-focus-btn studio-pdf-focus-close";
3954
+ closeBtn.title = "Exit PDF focus view.";
3955
+ closeBtn.setAttribute("aria-label", "Exit PDF focus view");
3956
+ closeBtn.appendChild(makeStudioUiRefreshIcon("focus-exit"));
3957
+ closeBtn.addEventListener("click", () => closeStudioPdfFocusViewer());
3958
+ titleGroup.appendChild(closeBtn);
3959
+
3960
+ const titleEl = document.createElement("div");
3961
+ titleEl.id = "studioPdfFocusTitle";
3962
+ titleEl.className = "studio-pdf-focus-title";
3963
+ titleEl.textContent = "PDF preview";
3964
+ titleGroup.appendChild(titleEl);
3965
+ header.appendChild(titleGroup);
3966
+
3967
+ const actions = document.createElement("div");
3968
+ actions.className = "studio-pdf-focus-actions";
3969
+
3970
+ const openLink = document.createElement("a");
3971
+ openLink.className = "studio-pdf-focus-link";
3972
+ openLink.target = "_blank";
3973
+ openLink.rel = "noopener noreferrer";
3974
+ openLink.textContent = "Open PDF";
3975
+ actions.appendChild(openLink);
3976
+
3977
+ const fullscreenBtn = document.createElement("button");
3978
+ fullscreenBtn.type = "button";
3979
+ fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
3980
+ fullscreenBtn.addEventListener("click", async () => {
3981
+ const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
3982
+ if (isFullscreen) {
3983
+ try {
3984
+ if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
3985
+ } catch (error) {
3986
+ setStatus("Could not exit PDF fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
3987
+ } finally {
3988
+ syncStudioPdfFocusFullscreenButton();
3989
+ }
3990
+ return;
3991
+ }
3992
+ if (!studioPdfFocusDialogEl || typeof studioPdfFocusDialogEl.requestFullscreen !== "function") {
3993
+ setStatus("Browser fullscreen is not available for this PDF viewer.", "warning");
3994
+ return;
3995
+ }
3996
+ try {
3997
+ await studioPdfFocusDialogEl.requestFullscreen();
3998
+ } catch (error) {
3999
+ setStatus("Could not enter PDF fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
4000
+ } finally {
4001
+ syncStudioPdfFocusFullscreenButton();
4002
+ }
4003
+ });
4004
+ actions.appendChild(fullscreenBtn);
4005
+
4006
+ header.appendChild(actions);
4007
+ dialog.appendChild(header);
4008
+
4009
+ const frameSlot = document.createElement("div");
4010
+ frameSlot.className = "studio-pdf-focus-frame-slot";
4011
+ const frame = document.createElement("iframe");
4012
+ frame.className = "studio-pdf-focus-frame";
4013
+ frame.title = "PDF focus viewer";
4014
+ frame.loading = "eager";
4015
+ frameSlot.appendChild(frame);
4016
+ dialog.appendChild(frameSlot);
4017
+
4018
+ overlay.appendChild(dialog);
4019
+ overlay.addEventListener("click", (event) => {
4020
+ if (event.target === overlay) closeStudioPdfFocusViewer();
4021
+ });
4022
+ document.addEventListener("fullscreenchange", syncStudioPdfFocusFullscreenButton);
4023
+
4024
+ document.body.appendChild(overlay);
4025
+ studioPdfFocusOverlayEl = overlay;
4026
+ studioPdfFocusDialogEl = dialog;
4027
+ studioPdfFocusFrameSlotEl = frameSlot;
4028
+ studioPdfFocusFrameEl = frame;
4029
+ studioPdfFocusTitleEl = titleEl;
4030
+ studioPdfFocusOpenLinkEl = openLink;
4031
+ studioPdfFocusFullscreenBtn = fullscreenBtn;
4032
+ studioPdfFocusCloseBtn = closeBtn;
4033
+ syncStudioPdfFocusFullscreenButton();
4034
+ return overlay;
4035
+ }
4036
+
4037
+ function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame) {
4038
+ const src = String(viewerUrl || "").trim();
4039
+ if (!src) return;
4040
+ ensureStudioPdfFocusViewer();
4041
+ studioPdfFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
4042
+ if (studioPdfFocusTitleEl) studioPdfFocusTitleEl.textContent = String(title || "PDF preview").trim() || "PDF preview";
4043
+ if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = src;
4044
+ setStudioPdfFocusFrameSource(src, title, sourceFrame);
4045
+ if (document.body) document.body.classList.add("studio-pdf-focus-open");
4046
+ if (studioPdfFocusOverlayEl) studioPdfFocusOverlayEl.hidden = false;
4047
+ syncStudioPdfFocusFullscreenButton();
4048
+ closeStudioUiRefreshMenus();
4049
+ closeExportPreviewMenu();
4050
+ window.setTimeout(() => {
4051
+ if (studioPdfFocusCloseBtn && typeof studioPdfFocusCloseBtn.focus === "function") {
4052
+ studioPdfFocusCloseBtn.focus();
4053
+ }
4054
+ }, 0);
4055
+ }
4056
+
4057
+ function closeStudioPdfFocusViewer() {
4058
+ if (!isStudioPdfFocusOpen()) return false;
4059
+ if (document.fullscreenElement && studioPdfFocusDialogEl && studioPdfFocusDialogEl.contains(document.fullscreenElement)) {
4060
+ try {
4061
+ const exitResult = document.exitFullscreen && document.exitFullscreen();
4062
+ if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
4063
+ } catch {}
4064
+ }
4065
+ if (studioPdfFocusOverlayEl) studioPdfFocusOverlayEl.hidden = true;
4066
+ restoreStudioPdfFocusMovedFrame();
4067
+ if (studioPdfFocusFrameEl) studioPdfFocusFrameEl.src = "about:blank";
4068
+ if (document.body) document.body.classList.remove("studio-pdf-focus-open");
4069
+ syncStudioPdfFocusFullscreenButton();
4070
+ const focusTarget = studioPdfFocusLastFocusedEl;
4071
+ studioPdfFocusLastFocusedEl = null;
4072
+ if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
4073
+ window.setTimeout(() => focusTarget.focus(), 0);
4074
+ }
4075
+ return true;
4076
+ }
4077
+
4078
+ function buildStudioPdfResourceUrl(options, useEditorResourceContext) {
3851
4079
  const token = getToken();
3852
4080
  if (!token) return "";
3853
4081
  const pdfPath = String(options && options.path ? options.path : "").trim();
3854
4082
  if (!pdfPath) return "";
3855
4083
  const effectivePath = getEffectiveSavePath();
3856
- const sourcePath = effectivePath || sourceState.path || "";
4084
+ const sourcePath = useEditorResourceContext ? (effectivePath || sourceState.path || "") : "";
4085
+ const resourceDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
3857
4086
  const params = new URLSearchParams({ token, path: pdfPath });
3858
4087
  if (sourcePath) {
3859
4088
  params.set("sourcePath", sourcePath);
3860
- } else if (resourceDirInput && resourceDirInput.value.trim()) {
3861
- params.set("resourceDir", resourceDirInput.value.trim());
4089
+ } else if (resourceDir) {
4090
+ params.set("resourceDir", resourceDir);
3862
4091
  }
3863
4092
  return "/pdf-resource?" + params.toString();
3864
4093
  }
3865
4094
 
3866
- function createStudioPdfCard(block) {
4095
+ function syncStudioPdfFocusFullscreenButton() {
4096
+ if (!studioPdfFocusFullscreenBtn) return;
4097
+ const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
4098
+ studioPdfFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
4099
+ const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
4100
+ studioPdfFocusFullscreenBtn.title = isFullscreen
4101
+ ? "Exit browser fullscreen and keep the PDF focus viewer open."
4102
+ : "Ask the browser to make this PDF viewer fullscreen.";
4103
+ studioPdfFocusFullscreenBtn.setAttribute("aria-label", label);
4104
+ studioPdfFocusFullscreenBtn.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
4105
+ }
4106
+
4107
+ function restoreStudioPdfFocusMovedFrame() {
4108
+ const state = studioPdfFocusMovedFrameState;
4109
+ studioPdfFocusMovedFrameState = null;
4110
+ if (!state || !state.frame) return;
4111
+ const frame = state.frame;
4112
+ frame.className = state.className;
4113
+ frame.style.cssText = state.styleCssText;
4114
+ if (state.title !== null) frame.setAttribute("title", state.title);
4115
+ else frame.removeAttribute("title");
4116
+ if (state.placeholder && state.placeholder.parentNode) {
4117
+ state.placeholder.parentNode.insertBefore(frame, state.placeholder);
4118
+ state.placeholder.remove();
4119
+ } else if (state.parent && state.parent.isConnected) {
4120
+ state.parent.insertBefore(frame, state.nextSibling && state.nextSibling.parentNode === state.parent ? state.nextSibling : null);
4121
+ }
4122
+ }
4123
+
4124
+ function setStudioPdfFocusFrameSource(src, title, sourceFrame) {
4125
+ if (!studioPdfFocusFrameSlotEl || !studioPdfFocusFrameEl) return;
4126
+ restoreStudioPdfFocusMovedFrame();
4127
+ const sourceIframe = sourceFrame instanceof HTMLIFrameElement ? sourceFrame : null;
4128
+ if (sourceIframe && sourceIframe.isConnected) {
4129
+ const placeholder = document.createElement("span");
4130
+ placeholder.hidden = true;
4131
+ const parent = sourceIframe.parentNode;
4132
+ parent && parent.insertBefore(placeholder, sourceIframe);
4133
+ studioPdfFocusMovedFrameState = {
4134
+ frame: sourceIframe,
4135
+ parent,
4136
+ nextSibling: placeholder.nextSibling,
4137
+ placeholder,
4138
+ className: sourceIframe.className,
4139
+ styleCssText: sourceIframe.style.cssText,
4140
+ title: sourceIframe.getAttribute("title"),
4141
+ };
4142
+ if (studioPdfFocusFrameEl.parentNode) studioPdfFocusFrameEl.parentNode.removeChild(studioPdfFocusFrameEl);
4143
+ sourceIframe.classList.add("studio-pdf-focus-frame");
4144
+ sourceIframe.style.height = "auto";
4145
+ sourceIframe.style.flex = "1 1 auto";
4146
+ sourceIframe.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
4147
+ studioPdfFocusFrameSlotEl.appendChild(sourceIframe);
4148
+ return;
4149
+ }
4150
+ if (!studioPdfFocusFrameEl.parentNode) studioPdfFocusFrameSlotEl.appendChild(studioPdfFocusFrameEl);
4151
+ studioPdfFocusFrameEl.src = src;
4152
+ studioPdfFocusFrameEl.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
4153
+ }
4154
+
4155
+ function openStudioPdfFocusFromButton(buttonEl) {
4156
+ if (!buttonEl) return false;
4157
+ const card = buttonEl.closest && buttonEl.closest(".studio-pdf-card");
4158
+ const viewerUrl = String(buttonEl.dataset && buttonEl.dataset.studioPdfViewerUrl ? buttonEl.dataset.studioPdfViewerUrl : "").trim()
4159
+ || String(card && card.dataset ? (card.dataset.studioPdfViewerUrl || "") : "").trim();
4160
+ const title = String(buttonEl.dataset && buttonEl.dataset.studioPdfTitle ? buttonEl.dataset.studioPdfTitle : "").trim()
4161
+ || String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
4162
+ || "PDF preview";
4163
+ const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
4164
+ if (!viewerUrl) return false;
4165
+ openStudioPdfFocusViewer(viewerUrl, title, sourceFrame);
4166
+ return true;
4167
+ }
4168
+
4169
+ function handleStudioPdfFocusButtonClick(event) {
4170
+ const target = event && event.target;
4171
+ const buttonEl = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
4172
+ if (!buttonEl) return;
4173
+ event.preventDefault();
4174
+ event.stopPropagation();
4175
+ if (typeof event.stopImmediatePropagation === "function") {
4176
+ event.stopImmediatePropagation();
4177
+ }
4178
+ if (!openStudioPdfFocusFromButton(buttonEl)) {
4179
+ setStatus("Could not open PDF focus view for this card.", "warning");
4180
+ }
4181
+ }
4182
+
4183
+ function createStudioPdfCard(block, useEditorResourceContext) {
3867
4184
  const options = block && block.options ? block.options : {};
3868
4185
  const path = String(options.path || "").trim();
3869
4186
  const title = String(options.title || path || "Embedded PDF").trim();
3870
4187
  const caption = String(options.caption || "").trim();
3871
4188
  const height = normalizeStudioPdfHeight(options.height);
3872
4189
  const page = normalizeStudioPdfPage(options.page);
3873
- const resourceUrl = buildStudioPdfResourceUrl(options);
4190
+ const resourceUrl = buildStudioPdfResourceUrl(options, useEditorResourceContext);
3874
4191
  const viewerUrl = resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
3875
4192
 
3876
4193
  const card = document.createElement("figure");
3877
4194
  card.className = "studio-pdf-card";
4195
+ if (card.dataset) {
4196
+ card.dataset.studioPdfViewerUrl = viewerUrl || "";
4197
+ card.dataset.studioPdfTitle = title;
4198
+ }
3878
4199
 
3879
4200
  const header = document.createElement("figcaption");
3880
4201
  header.className = "studio-pdf-card-header";
4202
+
4203
+ const titleGroup = document.createElement("div");
4204
+ titleGroup.className = "studio-pdf-card-title-group";
4205
+ if (resourceUrl) {
4206
+ const focusBtn = document.createElement("button");
4207
+ focusBtn.type = "button";
4208
+ focusBtn.className = "studio-pdf-card-action studio-pdf-card-focus";
4209
+ focusBtn.title = "Open this PDF in a larger Studio overlay.";
4210
+ focusBtn.setAttribute("aria-label", "Focus PDF");
4211
+ if (focusBtn.dataset) {
4212
+ focusBtn.dataset.studioPdfViewerUrl = viewerUrl;
4213
+ focusBtn.dataset.studioPdfTitle = title;
4214
+ }
4215
+ focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
4216
+ focusBtn.addEventListener("click", handleStudioPdfFocusButtonClick);
4217
+ titleGroup.appendChild(focusBtn);
4218
+ }
3881
4219
  const label = document.createElement("div");
3882
4220
  label.className = "studio-pdf-card-title";
3883
4221
  label.textContent = title;
3884
- header.appendChild(label);
4222
+ titleGroup.appendChild(label);
4223
+ header.appendChild(titleGroup);
3885
4224
 
3886
4225
  if (resourceUrl) {
4226
+ const actions = document.createElement("div");
4227
+ actions.className = "studio-pdf-card-actions";
4228
+
3887
4229
  const openLink = document.createElement("a");
3888
- openLink.className = "studio-pdf-card-link";
4230
+ openLink.className = "studio-pdf-card-link studio-pdf-card-action";
3889
4231
  openLink.href = viewerUrl;
3890
4232
  openLink.target = "_blank";
3891
4233
  openLink.rel = "noopener noreferrer";
3892
4234
  openLink.textContent = "Open PDF";
3893
- header.appendChild(openLink);
4235
+ actions.appendChild(openLink);
4236
+
4237
+ header.appendChild(actions);
3894
4238
  }
3895
4239
  card.appendChild(header);
3896
4240
 
@@ -3919,7 +4263,7 @@
3919
4263
  return card;
3920
4264
  }
3921
4265
 
3922
- function renderStudioPdfBlocksInElement(targetEl, blocks) {
4266
+ function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
3923
4267
  if (!targetEl || !Array.isArray(blocks) || blocks.length === 0) return;
3924
4268
  const candidates = Array.from(targetEl.querySelectorAll("p, pre, div"));
3925
4269
  blocks.forEach((block) => {
@@ -3927,7 +4271,7 @@
3927
4271
  if (!placeholder) return;
3928
4272
  const match = candidates.find((el) => String(el.textContent || "").trim() === placeholder);
3929
4273
  if (match && match.parentNode) {
3930
- match.replaceWith(createStudioPdfCard(block));
4274
+ match.replaceWith(createStudioPdfCard(block, useEditorResourceContext));
3931
4275
  }
3932
4276
  });
3933
4277
  }
@@ -4952,6 +5296,22 @@
4952
5296
  return "";
4953
5297
  }
4954
5298
 
5299
+ async function fetchWithTimeout(url, options, timeoutMs, timeoutLabel) {
5300
+ if (typeof AbortController === "undefined") return fetch(url, options);
5301
+ const controller = new AbortController();
5302
+ const timer = window.setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || PDF_EXPORT_FETCH_TIMEOUT_MS));
5303
+ try {
5304
+ return await fetch(url, { ...(options || {}), signal: controller.signal });
5305
+ } catch (error) {
5306
+ if (error && error.name === "AbortError") {
5307
+ throw new Error((timeoutLabel || "Request") + " timed out. Try a smaller export or check the PDF toolchain.");
5308
+ }
5309
+ throw error;
5310
+ } finally {
5311
+ window.clearTimeout(timer);
5312
+ }
5313
+ }
5314
+
4955
5315
  async function exportRightPanePdf() {
4956
5316
  if (uiBusy || previewExportInProgress) {
4957
5317
  setStatus("Studio is busy.", "warning");
@@ -5011,7 +5371,7 @@
5011
5371
  setStatus("Exporting PDF…", "warning");
5012
5372
 
5013
5373
  try {
5014
- const response = await fetch("/export-pdf?token=" + encodeURIComponent(token), {
5374
+ const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
5015
5375
  method: "POST",
5016
5376
  headers: {
5017
5377
  "Content-Type": "application/json",
@@ -5024,7 +5384,7 @@
5024
5384
  editorPdfLanguage: editorPdfLanguage,
5025
5385
  filenameHint: filenameHint,
5026
5386
  }),
5027
- });
5387
+ }, PDF_EXPORT_FETCH_TIMEOUT_MS, "PDF export");
5028
5388
 
5029
5389
  const contentType = String(response.headers.get("content-type") || "").toLowerCase();
5030
5390
  if (!response.ok) {
@@ -5178,7 +5538,7 @@
5178
5538
  setStatus("Exporting HTML…", "warning");
5179
5539
 
5180
5540
  try {
5181
- const response = await fetch("/export-html?token=" + encodeURIComponent(token), {
5541
+ const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
5182
5542
  method: "POST",
5183
5543
  headers: {
5184
5544
  "Content-Type": "application/json",
@@ -5192,7 +5552,7 @@
5192
5552
  filenameHint: filenameHint,
5193
5553
  title: titleHint,
5194
5554
  }),
5195
- });
5555
+ }, HTML_EXPORT_FETCH_TIMEOUT_MS, "HTML export");
5196
5556
 
5197
5557
  const contentType = String(response.headers.get("content-type") || "").toLowerCase();
5198
5558
  if (!response.ok) {
@@ -5521,7 +5881,7 @@
5521
5881
  clearPreviewJumpHighlight(targetEl);
5522
5882
  finishPreviewRender(targetEl);
5523
5883
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
5524
- renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks);
5884
+ renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
5525
5885
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
5526
5886
  await renderAnnotationMathInElement(targetEl);
5527
5887
  decoratePdfEmbeds(targetEl);
@@ -13876,6 +14236,12 @@
13876
14236
  });
13877
14237
  }
13878
14238
 
14239
+ if (zenModeBtn) {
14240
+ zenModeBtn.addEventListener("click", () => {
14241
+ setStudioZenMode(!studioZenModeEnabled);
14242
+ });
14243
+ }
14244
+
13879
14245
  sendRunBtn.addEventListener("click", () => {
13880
14246
  if (getAbortablePendingKind() === "direct") {
13881
14247
  requestCancelForPendingRequest("direct");
@@ -14057,6 +14423,13 @@
14057
14423
  });
14058
14424
  }
14059
14425
 
14426
+ document.addEventListener("click", (event) => {
14427
+ const target = event.target;
14428
+ const focusBtn = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
14429
+ if (!focusBtn) return;
14430
+ handleStudioPdfFocusButtonClick(event);
14431
+ }, true);
14432
+
14060
14433
  document.addEventListener("click", (event) => {
14061
14434
  const target = event.target;
14062
14435
  const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;