pi-studio 0.9.48 → 0.9.49
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 +9 -0
- package/README.md +8 -4
- package/client/studio-client.js +337 -9
- package/client/studio-preview-resource-helpers.js +52 -0
- package/client/studio.css +28 -0
- package/index.ts +141 -72
- package/package.json +1 -1
- package/shared/studio-local-preview-path.js +46 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.49] — 2026-08-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Open local PDFs directly with `/studio <path.pdf>` or `/studio-editor-only <path.pdf>` in a read-only, right-focused companion preview, including optional `#page=N` hints and the existing PDF viewer actions without replacing an open full Studio workspace.
|
|
11
|
+
- Add opt-in PDF auto-refresh through `/studio --watch <path.pdf>`, `watch: true` preview blocks, and synchronized card/focus-viewer toggles; Studio checks only visible tabs, waits for a changed file to stabilize, and retains **Cmd/Ctrl+Alt+R** as a manual refresh shortcut.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Show active PDF auto-refresh controls as plain accent text, matching Studio's current active-tool treatment instead of using a filled toggle pill.
|
|
15
|
+
|
|
7
16
|
## [0.9.48] — 2026-08-21
|
|
8
17
|
|
|
9
18
|
### Added
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ _The video shows an earlier version of the Studio interface. The basic workflow
|
|
|
54
54
|
- Renders Markdown/LaTeX/code previews (math + Mermaid) plus lightweight CSV/TSV table previews, theme-synced with pi, with copy buttons for code blocks and blockquotes; Mermaid previews include Lucide/Logos icon nodes and accessible label contrast over custom fills
|
|
55
55
|
- Adds a contextual **Editor (Quarto Preview)** right-pane view for file-backed `.qmd`, `.md`, and `.markdown` documents. Studio checks Quarto and the document/project configuration before showing an explicit start action, launches a single loopback `quarto preview` process with `--no-execute`, embeds Quarto's authoritative saved-file output without restyling it, and provides open-in-browser, restart, stop, logs, and unsaved-editor warnings. If Quarto is missing, the view remains available with an actionable dependency message.
|
|
56
56
|
- Renders straight, unfenced interactive HTML in preview via a sandboxed browser iframe with zoom controls, while fenced `html` blocks remain source code
|
|
57
|
-
- Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks
|
|
57
|
+
- Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks and opens existing PDFs directly with `/studio <path.pdf>`, with Focus, browser-tab, system-viewer, show-in-folder, manual refresh, and opt-in stable-file auto-refresh actions
|
|
58
58
|
- Ships optional `pi-studio-dark` and `pi-studio-light` themes tuned for Studio's browser workspace
|
|
59
59
|
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML into the source file directory, Studio working directory, or Pi session directory; PDF export can open in a Studio preview tab or the default PDF viewer, and HTML export can open in the default browser or in a new Studio editor tab for inspection/commenting, while preserving authored HTML previews as HTML and rendering CSV/TSV editor previews as tables
|
|
60
60
|
- 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. Agent tools `studio_export_pdf` and `studio_export_html` expose the same export pipeline for remote/Telegram-style sessions.
|
|
@@ -65,7 +65,8 @@ _The video shows an earlier version of the Studio interface. The basic workflow
|
|
|
65
65
|
| Command | Description |
|
|
66
66
|
|---|---|
|
|
67
67
|
| `/studio` | Open in Muxy or cmux when available, otherwise the system browser, with the last assistant response (fallback: blank) |
|
|
68
|
-
| `/studio <path>` | Open
|
|
68
|
+
| `/studio <path>` | Open a text file in the editor, or a PDF in a read-only companion preview tab |
|
|
69
|
+
| `/studio --watch <pdf>` | Open a local PDF with stable-file auto-refresh enabled |
|
|
69
70
|
| `/studio --last` | Force last response |
|
|
70
71
|
| `/studio --blank` | Force blank editor |
|
|
71
72
|
| `/studio --no-browser` | Start/print the Studio URL without opening a browser, useful for forwarded or phone/browser sessions |
|
|
@@ -74,7 +75,7 @@ _The video shows an earlier version of the Studio interface. The basic workflow
|
|
|
74
75
|
| `/studio --stop` | Stop studio server |
|
|
75
76
|
| `/studio --help` | Show help |
|
|
76
77
|
| `/studio-replace [path\|--blank\|--last]` | Replace the current full Studio view with a new full Studio view |
|
|
77
|
-
| `/studio-editor-only [path\|--blank\|--last]` | Open an editor-only
|
|
78
|
+
| `/studio-editor-only [path\|--blank\|--last]` | Open an editor-only view, or a read-only PDF preview; multiple companion views may be open at once |
|
|
78
79
|
| `/studio-current <path>` | Load a file into currently open Studio tab(s) without opening a new browser window |
|
|
79
80
|
| `/studio-pdf [path] [options]` | Export a local file, or the last model response when no path is given, via the Studio PDF pipeline |
|
|
80
81
|
| `/studio-html [path]` | Export a local file, or the last model response when no path is given, to standalone HTML via the Studio preview pipeline |
|
|
@@ -118,11 +119,14 @@ path: attachments/paper.pdf
|
|
|
118
119
|
title: Optional title
|
|
119
120
|
page: 3
|
|
120
121
|
height: 760
|
|
122
|
+
watch: true
|
|
121
123
|
caption: Optional caption
|
|
122
124
|
```
|
|
123
125
|
````
|
|
124
126
|
|
|
125
|
-
`path` must point to a local `.pdf` within the current Studio resource directory. Relative paths resolve from the opened document's directory, or from Studio's working dir for non-file-backed content. `page` is an initial page hint for the browser PDF viewer,
|
|
127
|
+
`path` must point to a local `.pdf` within the current Studio resource directory. Relative paths resolve from the opened document's directory, or from Studio's working dir for non-file-backed content. `page` is an initial page hint for the browser PDF viewer, `height` controls the embedded frame height in pixels, and `watch: true` enables auto-refresh after Studio observes the changed file in a stable state. Use normal Markdown links for PDFs when embedding is not useful.
|
|
128
|
+
|
|
129
|
+
To view an existing PDF directly, run `/studio report.pdf` (or `/studio "path with spaces/report.pdf"`). The PDF opens read-only in a focused companion preview, even when the full Studio workspace is already open. A `#page=N` suffix selects the initial page. Auto-refresh is off by default; use `/studio --watch report.pdf` to start with it on, or toggle **Auto-refresh** in the PDF card or focused viewer. Studio checks only while the tab is visible and waits for two matching file-version observations before reloading, which avoids reading a PDF while LaTeX is still writing it. **Cmd/Ctrl+Alt+R** manually refreshes the focused or visible PDF. This is distinct from `/studio-pdf`, which exports Markdown, LaTeX, code, or the last response to a new PDF.
|
|
126
130
|
|
|
127
131
|
### Mermaid icons
|
|
128
132
|
|
package/client/studio-client.js
CHANGED
|
@@ -227,7 +227,10 @@
|
|
|
227
227
|
if (
|
|
228
228
|
!previewResourceHelpers
|
|
229
229
|
|| typeof previewResourceHelpers.areStudioPreviewResourceContextsEqual !== "function"
|
|
230
|
+
|| typeof previewResourceHelpers.buildStudioPdfVersionSignature !== "function"
|
|
231
|
+
|| typeof previewResourceHelpers.createStudioPdfVersionObservationState !== "function"
|
|
230
232
|
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalImages !== "function"
|
|
233
|
+
|| typeof previewResourceHelpers.observeStudioPdfVersion !== "function"
|
|
231
234
|
) {
|
|
232
235
|
throw new Error("Studio preview resource helpers failed to load.");
|
|
233
236
|
}
|
|
@@ -271,11 +274,17 @@
|
|
|
271
274
|
let studioPdfFocusOpenLinkEl = null;
|
|
272
275
|
let studioPdfFocusSystemViewerBtn = null;
|
|
273
276
|
let studioPdfFocusRevealBtn = null;
|
|
277
|
+
let studioPdfFocusAutoRefreshBtn = null;
|
|
274
278
|
let studioPdfFocusFullscreenBtn = null;
|
|
275
279
|
let studioPdfFocusCloseBtn = null;
|
|
276
280
|
let studioPdfFocusLastFocusedEl = null;
|
|
277
281
|
let studioPdfFocusMovedFrameState = null;
|
|
278
282
|
let studioPdfFocusResourceQuery = null;
|
|
283
|
+
let studioPdfFocusSourceCard = null;
|
|
284
|
+
let studioPdfFocusStandaloneAutoRefreshState = null;
|
|
285
|
+
const studioPdfCardAutoRefreshStates = new WeakMap();
|
|
286
|
+
const STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS = 1_000;
|
|
287
|
+
const STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS = 2;
|
|
279
288
|
let studioHtmlFocusOverlayEl = null;
|
|
280
289
|
let studioHtmlFocusShellEl = null;
|
|
281
290
|
let studioHtmlFocusFullscreenBtn = null;
|
|
@@ -4489,6 +4498,15 @@
|
|
|
4489
4498
|
|
|
4490
4499
|
if (handleStudioImageFocusShortcut(event)) return;
|
|
4491
4500
|
|
|
4501
|
+
const otherModalOwnsEvent = scratchpadOwnsEvent
|
|
4502
|
+
|| reviewNotesOwnsEvent
|
|
4503
|
+
|| outlineOwnsEvent
|
|
4504
|
+
|| shortcutsOwnsEvent
|
|
4505
|
+
|| htmlFocusOwnsEvent
|
|
4506
|
+
|| imageFocusOwnsEvent
|
|
4507
|
+
|| quizOwnsEvent;
|
|
4508
|
+
if (!otherModalOwnsEvent && handleStudioPdfRefreshShortcut(event)) return;
|
|
4509
|
+
|
|
4492
4510
|
if (isScratchpadOpen() && plainEscape) {
|
|
4493
4511
|
event.preventDefault();
|
|
4494
4512
|
closeScratchpad();
|
|
@@ -6851,7 +6869,7 @@
|
|
|
6851
6869
|
}
|
|
6852
6870
|
|
|
6853
6871
|
function parseStudioPdfBlockOptions(body) {
|
|
6854
|
-
const options = { path: "", title: "", caption: "", page: "", height: "" };
|
|
6872
|
+
const options = { path: "", title: "", caption: "", page: "", height: "", watch: "" };
|
|
6855
6873
|
String(body || "").split(/\r?\n/).forEach((line) => {
|
|
6856
6874
|
const raw = String(line || "").trim();
|
|
6857
6875
|
if (!raw || raw.startsWith("#")) return;
|
|
@@ -6864,6 +6882,7 @@
|
|
|
6864
6882
|
else if (key === "caption") options.caption = value;
|
|
6865
6883
|
else if (key === "page") options.page = value;
|
|
6866
6884
|
else if (key === "height") options.height = value;
|
|
6885
|
+
else if (key === "watch" || key === "auto-refresh" || key === "autorefresh") options.watch = value;
|
|
6867
6886
|
return;
|
|
6868
6887
|
}
|
|
6869
6888
|
if (!options.path) options.path = stripMatchingQuotes(raw);
|
|
@@ -6895,6 +6914,10 @@
|
|
|
6895
6914
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
6896
6915
|
}
|
|
6897
6916
|
|
|
6917
|
+
function normalizeStudioPdfAutoRefresh(value) {
|
|
6918
|
+
return /^(?:1|true|yes|on|watch)$/i.test(String(value || "").trim());
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6898
6921
|
function isStudioPdfFocusOpen() {
|
|
6899
6922
|
return Boolean(studioPdfFocusOverlayEl && studioPdfFocusOverlayEl.hidden === false);
|
|
6900
6923
|
}
|
|
@@ -6969,11 +6992,21 @@
|
|
|
6969
6992
|
refreshBtn.type = "button";
|
|
6970
6993
|
refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
|
|
6971
6994
|
refreshBtn.textContent = "Refresh";
|
|
6972
|
-
refreshBtn.title = "Reload this PDF preview from disk.";
|
|
6995
|
+
refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
|
|
6973
6996
|
refreshBtn.setAttribute("aria-label", "Refresh PDF preview from disk");
|
|
6974
6997
|
refreshBtn.addEventListener("click", () => refreshStudioPdfFocusViewer());
|
|
6975
6998
|
actions.appendChild(refreshBtn);
|
|
6976
6999
|
|
|
7000
|
+
const autoRefreshBtn = document.createElement("button");
|
|
7001
|
+
autoRefreshBtn.type = "button";
|
|
7002
|
+
autoRefreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-auto-refresh";
|
|
7003
|
+
autoRefreshBtn.textContent = "Auto-refresh";
|
|
7004
|
+
autoRefreshBtn.title = "Watch this local PDF and reload it after a changed file is stable on disk.";
|
|
7005
|
+
autoRefreshBtn.setAttribute("aria-label", "Enable PDF auto-refresh");
|
|
7006
|
+
autoRefreshBtn.setAttribute("aria-pressed", "false");
|
|
7007
|
+
autoRefreshBtn.addEventListener("click", () => toggleStudioPdfFocusAutoRefresh());
|
|
7008
|
+
actions.appendChild(autoRefreshBtn);
|
|
7009
|
+
|
|
6977
7010
|
const fullscreenBtn = document.createElement("button");
|
|
6978
7011
|
fullscreenBtn.type = "button";
|
|
6979
7012
|
fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
|
|
@@ -7030,6 +7063,7 @@
|
|
|
7030
7063
|
studioPdfFocusOpenLinkEl = openLink;
|
|
7031
7064
|
studioPdfFocusSystemViewerBtn = systemViewerBtn;
|
|
7032
7065
|
studioPdfFocusRevealBtn = revealBtn;
|
|
7066
|
+
studioPdfFocusAutoRefreshBtn = autoRefreshBtn;
|
|
7033
7067
|
studioPdfFocusFullscreenBtn = fullscreenBtn;
|
|
7034
7068
|
studioPdfFocusCloseBtn = closeBtn;
|
|
7035
7069
|
syncStudioPdfFocusFullscreenButton();
|
|
@@ -7037,11 +7071,12 @@
|
|
|
7037
7071
|
return overlay;
|
|
7038
7072
|
}
|
|
7039
7073
|
|
|
7040
|
-
function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery) {
|
|
7074
|
+
function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, sourceCard) {
|
|
7041
7075
|
const src = String(viewerUrl || "").trim();
|
|
7042
7076
|
if (!src) return;
|
|
7043
7077
|
ensureStudioPdfFocusViewer();
|
|
7044
7078
|
studioPdfFocusResourceQuery = normalizeStudioPdfResourceQuery(resourceQuery);
|
|
7079
|
+
setStudioPdfFocusAutoRefreshSource(sourceCard, studioPdfFocusResourceQuery);
|
|
7045
7080
|
syncStudioPdfFocusResourceActions();
|
|
7046
7081
|
studioPdfFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
7047
7082
|
if (studioPdfFocusTitleEl) studioPdfFocusTitleEl.textContent = String(title || "PDF preview").trim() || "PDF preview";
|
|
@@ -7071,6 +7106,7 @@
|
|
|
7071
7106
|
restoreStudioPdfFocusMovedFrame();
|
|
7072
7107
|
if (studioPdfFocusFrameEl) studioPdfFocusFrameEl.src = "about:blank";
|
|
7073
7108
|
if (document.body) document.body.classList.remove("studio-pdf-focus-open");
|
|
7109
|
+
clearStudioPdfFocusAutoRefreshSource();
|
|
7074
7110
|
studioPdfFocusResourceQuery = null;
|
|
7075
7111
|
syncStudioPdfFocusResourceActions();
|
|
7076
7112
|
syncStudioPdfFocusFullscreenButton();
|
|
@@ -7113,6 +7149,214 @@
|
|
|
7113
7149
|
return query;
|
|
7114
7150
|
}
|
|
7115
7151
|
|
|
7152
|
+
function syncStudioPdfAutoRefreshButton(button, enabled, available) {
|
|
7153
|
+
if (!button) return;
|
|
7154
|
+
const active = Boolean(enabled && available);
|
|
7155
|
+
button.disabled = !available;
|
|
7156
|
+
button.setAttribute("aria-pressed", active ? "true" : "false");
|
|
7157
|
+
button.setAttribute("aria-label", active ? "Disable PDF auto-refresh" : "Enable PDF auto-refresh");
|
|
7158
|
+
button.textContent = "Auto-refresh";
|
|
7159
|
+
button.title = active
|
|
7160
|
+
? "Auto-refresh is on. Studio checks this PDF while the tab is visible and reloads it after a changed file is stable on disk."
|
|
7161
|
+
: "Watch this local PDF and reload it after a changed file is stable on disk.";
|
|
7162
|
+
}
|
|
7163
|
+
|
|
7164
|
+
function createStudioPdfAutoRefreshState(resourceQuery, hooks) {
|
|
7165
|
+
return {
|
|
7166
|
+
resourceQuery: normalizeStudioPdfResourceQuery(resourceQuery),
|
|
7167
|
+
enabled: false,
|
|
7168
|
+
timer: null,
|
|
7169
|
+
inFlight: false,
|
|
7170
|
+
generation: 0,
|
|
7171
|
+
observation: previewResourceHelpers.createStudioPdfVersionObservationState(),
|
|
7172
|
+
isAlive: hooks && typeof hooks.isAlive === "function" ? hooks.isAlive : () => false,
|
|
7173
|
+
refresh: hooks && typeof hooks.refresh === "function" ? hooks.refresh : () => false,
|
|
7174
|
+
sync: hooks && typeof hooks.sync === "function" ? hooks.sync : () => {},
|
|
7175
|
+
};
|
|
7176
|
+
}
|
|
7177
|
+
|
|
7178
|
+
function resetStudioPdfAutoRefreshObservation(state) {
|
|
7179
|
+
if (!state) return;
|
|
7180
|
+
state.observation = previewResourceHelpers.createStudioPdfVersionObservationState();
|
|
7181
|
+
}
|
|
7182
|
+
|
|
7183
|
+
function scheduleStudioPdfAutoRefreshPoll(state, delayMs) {
|
|
7184
|
+
if (!state || !state.enabled) return;
|
|
7185
|
+
if (state.timer) window.clearTimeout(state.timer);
|
|
7186
|
+
state.timer = window.setTimeout(() => {
|
|
7187
|
+
state.timer = null;
|
|
7188
|
+
void pollStudioPdfAutoRefreshState(state);
|
|
7189
|
+
}, Math.max(0, Number(delayMs) || 0));
|
|
7190
|
+
}
|
|
7191
|
+
|
|
7192
|
+
function stopStudioPdfAutoRefreshState(state) {
|
|
7193
|
+
if (!state) return;
|
|
7194
|
+
state.enabled = false;
|
|
7195
|
+
state.inFlight = false;
|
|
7196
|
+
state.generation += 1;
|
|
7197
|
+
if (state.timer) window.clearTimeout(state.timer);
|
|
7198
|
+
state.timer = null;
|
|
7199
|
+
resetStudioPdfAutoRefreshObservation(state);
|
|
7200
|
+
state.sync();
|
|
7201
|
+
}
|
|
7202
|
+
|
|
7203
|
+
function setStudioPdfAutoRefreshEnabled(state, enabled, options) {
|
|
7204
|
+
if (!state) return false;
|
|
7205
|
+
const nextEnabled = Boolean(enabled && state.resourceQuery);
|
|
7206
|
+
if (state.enabled === nextEnabled) {
|
|
7207
|
+
state.sync();
|
|
7208
|
+
return nextEnabled;
|
|
7209
|
+
}
|
|
7210
|
+
if (state.timer) window.clearTimeout(state.timer);
|
|
7211
|
+
state.timer = null;
|
|
7212
|
+
state.enabled = nextEnabled;
|
|
7213
|
+
state.inFlight = false;
|
|
7214
|
+
state.generation += 1;
|
|
7215
|
+
resetStudioPdfAutoRefreshObservation(state);
|
|
7216
|
+
state.sync();
|
|
7217
|
+
if (nextEnabled) {
|
|
7218
|
+
if (!options || !options.silent) state.refresh();
|
|
7219
|
+
scheduleStudioPdfAutoRefreshPoll(state, 0);
|
|
7220
|
+
}
|
|
7221
|
+
if (!options || !options.silent) {
|
|
7222
|
+
setStatus(nextEnabled ? "PDF auto-refresh on." : "PDF auto-refresh off.", "success");
|
|
7223
|
+
}
|
|
7224
|
+
return nextEnabled;
|
|
7225
|
+
}
|
|
7226
|
+
|
|
7227
|
+
async function fetchStudioPdfVersionSignature(resourceQuery) {
|
|
7228
|
+
const url = buildStudioPdfResourceUrl(resourceQuery, false);
|
|
7229
|
+
if (!url) throw new Error("Could not resolve this PDF for auto-refresh.");
|
|
7230
|
+
const response = await fetchWithTimeout(url, {
|
|
7231
|
+
method: "HEAD",
|
|
7232
|
+
cache: "no-store",
|
|
7233
|
+
}, 4_000, "PDF auto-refresh check");
|
|
7234
|
+
if (!response.ok) throw new Error("PDF auto-refresh check failed with HTTP " + response.status + ".");
|
|
7235
|
+
return previewResourceHelpers.buildStudioPdfVersionSignature(response.headers);
|
|
7236
|
+
}
|
|
7237
|
+
|
|
7238
|
+
async function pollStudioPdfAutoRefreshState(state) {
|
|
7239
|
+
if (!state || !state.enabled || state.inFlight) return;
|
|
7240
|
+
if (!state.isAlive()) {
|
|
7241
|
+
stopStudioPdfAutoRefreshState(state);
|
|
7242
|
+
return;
|
|
7243
|
+
}
|
|
7244
|
+
if (document.hidden || document.visibilityState === "hidden") {
|
|
7245
|
+
scheduleStudioPdfAutoRefreshPoll(state, STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS);
|
|
7246
|
+
return;
|
|
7247
|
+
}
|
|
7248
|
+
const generation = state.generation;
|
|
7249
|
+
state.inFlight = true;
|
|
7250
|
+
try {
|
|
7251
|
+
const signature = await fetchStudioPdfVersionSignature(state.resourceQuery);
|
|
7252
|
+
if (!state.enabled || state.generation !== generation) return;
|
|
7253
|
+
if (document.hidden || document.visibilityState === "hidden") return;
|
|
7254
|
+
if (!state.isAlive()) {
|
|
7255
|
+
stopStudioPdfAutoRefreshState(state);
|
|
7256
|
+
return;
|
|
7257
|
+
}
|
|
7258
|
+
const observed = previewResourceHelpers.observeStudioPdfVersion(
|
|
7259
|
+
state.observation,
|
|
7260
|
+
signature,
|
|
7261
|
+
STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS,
|
|
7262
|
+
);
|
|
7263
|
+
state.observation = observed.state;
|
|
7264
|
+
if (observed.changed) state.refresh();
|
|
7265
|
+
} catch {
|
|
7266
|
+
// LaTeX tools may briefly replace or lock the output PDF. Retry quietly.
|
|
7267
|
+
} finally {
|
|
7268
|
+
if (state.generation !== generation) return;
|
|
7269
|
+
state.inFlight = false;
|
|
7270
|
+
if (state.enabled) scheduleStudioPdfAutoRefreshPoll(state, STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS);
|
|
7271
|
+
}
|
|
7272
|
+
}
|
|
7273
|
+
|
|
7274
|
+
function syncStudioPdfCardAutoRefreshButton(card) {
|
|
7275
|
+
if (!card) return;
|
|
7276
|
+
const state = studioPdfCardAutoRefreshStates.get(card) || null;
|
|
7277
|
+
const button = typeof card.querySelector === "function" ? card.querySelector(".studio-pdf-card-auto-refresh") : null;
|
|
7278
|
+
syncStudioPdfAutoRefreshButton(button, state && state.enabled, Boolean(state && state.resourceQuery));
|
|
7279
|
+
}
|
|
7280
|
+
|
|
7281
|
+
function ensureStudioPdfCardAutoRefreshState(card, resourceQuery) {
|
|
7282
|
+
if (!card) return null;
|
|
7283
|
+
let state = studioPdfCardAutoRefreshStates.get(card) || null;
|
|
7284
|
+
if (state) return state;
|
|
7285
|
+
state = createStudioPdfAutoRefreshState(resourceQuery, {
|
|
7286
|
+
isAlive: () => Boolean(card.isConnected || (isStudioPdfFocusOpen() && studioPdfFocusSourceCard === card)),
|
|
7287
|
+
refresh: () => {
|
|
7288
|
+
if (isStudioPdfFocusOpen() && studioPdfFocusSourceCard === card) {
|
|
7289
|
+
return refreshStudioPdfFocusViewer({ automatic: true });
|
|
7290
|
+
}
|
|
7291
|
+
return refreshStudioPdfCard(card, { automatic: true });
|
|
7292
|
+
},
|
|
7293
|
+
sync: () => {
|
|
7294
|
+
syncStudioPdfCardAutoRefreshButton(card);
|
|
7295
|
+
if (studioPdfFocusSourceCard === card) syncStudioPdfFocusAutoRefreshButton();
|
|
7296
|
+
},
|
|
7297
|
+
});
|
|
7298
|
+
studioPdfCardAutoRefreshStates.set(card, state);
|
|
7299
|
+
syncStudioPdfCardAutoRefreshButton(card);
|
|
7300
|
+
return state;
|
|
7301
|
+
}
|
|
7302
|
+
|
|
7303
|
+
function setStudioPdfCardAutoRefresh(card, enabled, options) {
|
|
7304
|
+
const state = studioPdfCardAutoRefreshStates.get(card) || null;
|
|
7305
|
+
return setStudioPdfAutoRefreshEnabled(state, enabled, options);
|
|
7306
|
+
}
|
|
7307
|
+
|
|
7308
|
+
function getStudioPdfFocusAutoRefreshState() {
|
|
7309
|
+
if (studioPdfFocusSourceCard) return studioPdfCardAutoRefreshStates.get(studioPdfFocusSourceCard) || null;
|
|
7310
|
+
return studioPdfFocusStandaloneAutoRefreshState;
|
|
7311
|
+
}
|
|
7312
|
+
|
|
7313
|
+
function syncStudioPdfFocusAutoRefreshButton() {
|
|
7314
|
+
const state = getStudioPdfFocusAutoRefreshState();
|
|
7315
|
+
syncStudioPdfAutoRefreshButton(
|
|
7316
|
+
studioPdfFocusAutoRefreshBtn,
|
|
7317
|
+
state && state.enabled,
|
|
7318
|
+
Boolean(state && state.resourceQuery),
|
|
7319
|
+
);
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
function clearStudioPdfFocusAutoRefreshSource() {
|
|
7323
|
+
if (studioPdfFocusStandaloneAutoRefreshState) stopStudioPdfAutoRefreshState(studioPdfFocusStandaloneAutoRefreshState);
|
|
7324
|
+
studioPdfFocusStandaloneAutoRefreshState = null;
|
|
7325
|
+
studioPdfFocusSourceCard = null;
|
|
7326
|
+
syncStudioPdfFocusAutoRefreshButton();
|
|
7327
|
+
}
|
|
7328
|
+
|
|
7329
|
+
function setStudioPdfFocusAutoRefreshSource(sourceCard, resourceQuery) {
|
|
7330
|
+
clearStudioPdfFocusAutoRefreshSource();
|
|
7331
|
+
const card = sourceCard instanceof Element ? sourceCard : null;
|
|
7332
|
+
if (card) {
|
|
7333
|
+
studioPdfFocusSourceCard = card;
|
|
7334
|
+
ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
|
|
7335
|
+
syncStudioPdfFocusAutoRefreshButton();
|
|
7336
|
+
return;
|
|
7337
|
+
}
|
|
7338
|
+
const query = normalizeStudioPdfResourceQuery(resourceQuery);
|
|
7339
|
+
if (!query) {
|
|
7340
|
+
syncStudioPdfFocusAutoRefreshButton();
|
|
7341
|
+
return;
|
|
7342
|
+
}
|
|
7343
|
+
studioPdfFocusStandaloneAutoRefreshState = createStudioPdfAutoRefreshState(query, {
|
|
7344
|
+
isAlive: () => Boolean(isStudioPdfFocusOpen() && !studioPdfFocusSourceCard),
|
|
7345
|
+
refresh: () => refreshStudioPdfFocusViewer({ automatic: true }),
|
|
7346
|
+
sync: () => syncStudioPdfFocusAutoRefreshButton(),
|
|
7347
|
+
});
|
|
7348
|
+
syncStudioPdfFocusAutoRefreshButton();
|
|
7349
|
+
}
|
|
7350
|
+
|
|
7351
|
+
function toggleStudioPdfFocusAutoRefresh() {
|
|
7352
|
+
const state = getStudioPdfFocusAutoRefreshState();
|
|
7353
|
+
if (!state) {
|
|
7354
|
+
setStatus("Could not resolve this PDF for auto-refresh.", "warning");
|
|
7355
|
+
return false;
|
|
7356
|
+
}
|
|
7357
|
+
return setStudioPdfAutoRefreshEnabled(state, !state.enabled);
|
|
7358
|
+
}
|
|
7359
|
+
|
|
7116
7360
|
async function runStudioPdfLocalAction(action, resourceQuery) {
|
|
7117
7361
|
const query = normalizeStudioPdfResourceQuery(resourceQuery);
|
|
7118
7362
|
if (!query) {
|
|
@@ -7174,7 +7418,7 @@
|
|
|
7174
7418
|
if (focusBtn && focusBtn.dataset) focusBtn.dataset.studioPdfViewerUrl = nextUrl;
|
|
7175
7419
|
}
|
|
7176
7420
|
|
|
7177
|
-
function refreshStudioPdfCard(card) {
|
|
7421
|
+
function refreshStudioPdfCard(card, options) {
|
|
7178
7422
|
if (!card) return false;
|
|
7179
7423
|
const frame = typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
|
|
7180
7424
|
const currentUrl = String(card.dataset && card.dataset.studioPdfViewerUrl ? card.dataset.studioPdfViewerUrl : "").trim()
|
|
@@ -7182,7 +7426,14 @@
|
|
|
7182
7426
|
const nextUrl = buildRefreshedStudioPdfViewerUrl(currentUrl);
|
|
7183
7427
|
if (!nextUrl) return false;
|
|
7184
7428
|
syncStudioPdfCardViewerUrl(card, nextUrl);
|
|
7185
|
-
|
|
7429
|
+
if (!options || !options.automatic) {
|
|
7430
|
+
resetStudioPdfAutoRefreshObservation(studioPdfCardAutoRefreshStates.get(card) || null);
|
|
7431
|
+
}
|
|
7432
|
+
if (!options || !options.silent) {
|
|
7433
|
+
setStatus(options && options.automatic
|
|
7434
|
+
? "PDF changed on disk; refreshed preview."
|
|
7435
|
+
: "Refreshed PDF preview from disk.", "success");
|
|
7436
|
+
}
|
|
7186
7437
|
return true;
|
|
7187
7438
|
}
|
|
7188
7439
|
|
|
@@ -7193,7 +7444,7 @@
|
|
|
7193
7444
|
return studioPdfFocusFrameEl;
|
|
7194
7445
|
}
|
|
7195
7446
|
|
|
7196
|
-
function refreshStudioPdfFocusViewer() {
|
|
7447
|
+
function refreshStudioPdfFocusViewer(options) {
|
|
7197
7448
|
const frame = getStudioPdfFocusActiveFrame();
|
|
7198
7449
|
const currentUrl = String(frame && frame.src ? frame.src : "").trim()
|
|
7199
7450
|
|| String(studioPdfFocusOpenLinkEl && studioPdfFocusOpenLinkEl.href ? studioPdfFocusOpenLinkEl.href : "").trim();
|
|
@@ -7204,7 +7455,58 @@
|
|
|
7204
7455
|
}
|
|
7205
7456
|
if (frame) frame.src = nextUrl;
|
|
7206
7457
|
if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = nextUrl;
|
|
7207
|
-
|
|
7458
|
+
if (studioPdfFocusSourceCard) syncStudioPdfCardViewerUrl(studioPdfFocusSourceCard, nextUrl);
|
|
7459
|
+
if (!options || !options.automatic) {
|
|
7460
|
+
resetStudioPdfAutoRefreshObservation(getStudioPdfFocusAutoRefreshState());
|
|
7461
|
+
}
|
|
7462
|
+
if (!options || !options.silent) {
|
|
7463
|
+
setStatus(options && options.automatic
|
|
7464
|
+
? "PDF changed on disk; refreshed preview."
|
|
7465
|
+
: "Refreshed PDF preview from disk.", "success");
|
|
7466
|
+
}
|
|
7467
|
+
return true;
|
|
7468
|
+
}
|
|
7469
|
+
|
|
7470
|
+
function getVisibleStudioPdfCards() {
|
|
7471
|
+
return Array.from(document.querySelectorAll(".studio-pdf-card")).filter((card) => {
|
|
7472
|
+
if (!card || !card.isConnected || card.hidden) return false;
|
|
7473
|
+
if (typeof card.getClientRects === "function" && card.getClientRects().length === 0) return false;
|
|
7474
|
+
try {
|
|
7475
|
+
const style = window.getComputedStyle(card);
|
|
7476
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
7477
|
+
} catch {
|
|
7478
|
+
return true;
|
|
7479
|
+
}
|
|
7480
|
+
});
|
|
7481
|
+
}
|
|
7482
|
+
|
|
7483
|
+
function refreshVisibleStudioPdfPreviews() {
|
|
7484
|
+
if (isStudioPdfFocusOpen()) return refreshStudioPdfFocusViewer();
|
|
7485
|
+
const cards = getVisibleStudioPdfCards();
|
|
7486
|
+
let refreshed = 0;
|
|
7487
|
+
cards.forEach((card) => {
|
|
7488
|
+
if (refreshStudioPdfCard(card, { silent: true })) refreshed += 1;
|
|
7489
|
+
});
|
|
7490
|
+
if (refreshed > 0) {
|
|
7491
|
+
setStatus(refreshed === 1
|
|
7492
|
+
? "Refreshed PDF preview from disk."
|
|
7493
|
+
: ("Refreshed " + refreshed + " PDF previews from disk."), "success");
|
|
7494
|
+
return true;
|
|
7495
|
+
}
|
|
7496
|
+
return false;
|
|
7497
|
+
}
|
|
7498
|
+
|
|
7499
|
+
function handleStudioPdfRefreshShortcut(event) {
|
|
7500
|
+
if (!event) return false;
|
|
7501
|
+
const key = typeof event.key === "string" ? event.key.toLowerCase() : "";
|
|
7502
|
+
const code = typeof event.code === "string" ? event.code : "";
|
|
7503
|
+
const matches = (key === "r" || code === "KeyR")
|
|
7504
|
+
&& (event.metaKey || event.ctrlKey)
|
|
7505
|
+
&& event.altKey
|
|
7506
|
+
&& !event.shiftKey;
|
|
7507
|
+
if (!matches) return false;
|
|
7508
|
+
if (!refreshVisibleStudioPdfPreviews()) return false;
|
|
7509
|
+
event.preventDefault();
|
|
7208
7510
|
return true;
|
|
7209
7511
|
}
|
|
7210
7512
|
|
|
@@ -7285,7 +7587,7 @@
|
|
|
7285
7587
|
})
|
|
7286
7588
|
: null;
|
|
7287
7589
|
if (!viewerUrl) return false;
|
|
7288
|
-
openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery);
|
|
7590
|
+
openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, card);
|
|
7289
7591
|
return true;
|
|
7290
7592
|
}
|
|
7291
7593
|
|
|
@@ -7698,6 +8000,7 @@
|
|
|
7698
8000
|
const caption = String(options.caption || "").trim();
|
|
7699
8001
|
const height = normalizeStudioPdfHeight(options.height);
|
|
7700
8002
|
const page = normalizeStudioPdfPage(options.page);
|
|
8003
|
+
const autoRefreshRequested = normalizeStudioPdfAutoRefresh(options.watch);
|
|
7701
8004
|
const resourceQuery = buildStudioPdfResourceQuery(options, useEditorResourceContext);
|
|
7702
8005
|
const resourceUrl = buildStudioPdfResourceUrl(options, useEditorResourceContext);
|
|
7703
8006
|
const viewerUrl = resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
@@ -7778,7 +8081,7 @@
|
|
|
7778
8081
|
refreshBtn.type = "button";
|
|
7779
8082
|
refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
|
|
7780
8083
|
refreshBtn.textContent = "Refresh";
|
|
7781
|
-
refreshBtn.title = "Reload this PDF preview from disk.";
|
|
8084
|
+
refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
|
|
7782
8085
|
refreshBtn.addEventListener("click", (event) => {
|
|
7783
8086
|
event.preventDefault();
|
|
7784
8087
|
event.stopPropagation();
|
|
@@ -7786,6 +8089,25 @@
|
|
|
7786
8089
|
});
|
|
7787
8090
|
actions.appendChild(refreshBtn);
|
|
7788
8091
|
|
|
8092
|
+
const autoRefreshBtn = document.createElement("button");
|
|
8093
|
+
autoRefreshBtn.type = "button";
|
|
8094
|
+
autoRefreshBtn.className = "studio-pdf-card-action studio-pdf-card-auto-refresh";
|
|
8095
|
+
autoRefreshBtn.textContent = "Auto-refresh";
|
|
8096
|
+
autoRefreshBtn.title = "Watch this local PDF and reload it after a changed file is stable on disk.";
|
|
8097
|
+
autoRefreshBtn.setAttribute("aria-label", "Enable PDF auto-refresh");
|
|
8098
|
+
autoRefreshBtn.setAttribute("aria-pressed", "false");
|
|
8099
|
+
autoRefreshBtn.addEventListener("click", (event) => {
|
|
8100
|
+
event.preventDefault();
|
|
8101
|
+
event.stopPropagation();
|
|
8102
|
+
const state = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
|
|
8103
|
+
if (!state) {
|
|
8104
|
+
setStatus("Could not resolve this PDF for auto-refresh.", "warning");
|
|
8105
|
+
return;
|
|
8106
|
+
}
|
|
8107
|
+
setStudioPdfCardAutoRefresh(card, !state.enabled);
|
|
8108
|
+
});
|
|
8109
|
+
actions.appendChild(autoRefreshBtn);
|
|
8110
|
+
|
|
7789
8111
|
header.appendChild(actions);
|
|
7790
8112
|
}
|
|
7791
8113
|
card.appendChild(header);
|
|
@@ -7812,6 +8134,12 @@
|
|
|
7812
8134
|
iframe.loading = "lazy";
|
|
7813
8135
|
iframe.style.height = height + "px";
|
|
7814
8136
|
card.appendChild(iframe);
|
|
8137
|
+
const autoRefreshState = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
|
|
8138
|
+
if (autoRefreshRequested) {
|
|
8139
|
+
setStudioPdfAutoRefreshEnabled(autoRefreshState, true, { silent: true });
|
|
8140
|
+
} else {
|
|
8141
|
+
syncStudioPdfCardAutoRefreshButton(card);
|
|
8142
|
+
}
|
|
7815
8143
|
return card;
|
|
7816
8144
|
}
|
|
7817
8145
|
|
|
@@ -43,10 +43,62 @@
|
|
|
43
43
|
return { attempted: images.length, resolved };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function buildStudioPdfVersionSignature(headers) {
|
|
47
|
+
if (!headers || typeof headers.get !== "function") return "";
|
|
48
|
+
const etag = String(headers.get("etag") || "").trim();
|
|
49
|
+
const modified = String(headers.get("last-modified") || "").trim();
|
|
50
|
+
const length = String(headers.get("content-length") || "").trim();
|
|
51
|
+
if (!etag && !modified && !length) return "";
|
|
52
|
+
return [etag, modified, length].join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createStudioPdfVersionObservationState() {
|
|
56
|
+
return {
|
|
57
|
+
baseline: "",
|
|
58
|
+
candidate: "",
|
|
59
|
+
candidateCount: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function observeStudioPdfVersion(previousState, signature, stableObservations) {
|
|
64
|
+
const state = previousState && typeof previousState === "object"
|
|
65
|
+
? previousState
|
|
66
|
+
: createStudioPdfVersionObservationState();
|
|
67
|
+
const nextSignature = String(signature || "").trim();
|
|
68
|
+
const required = Math.max(2, Number.parseInt(String(stableObservations || "2"), 10) || 2);
|
|
69
|
+
if (!nextSignature) return { state, changed: false };
|
|
70
|
+
if (!state.baseline) {
|
|
71
|
+
return {
|
|
72
|
+
state: { baseline: nextSignature, candidate: "", candidateCount: 0 },
|
|
73
|
+
changed: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (nextSignature === state.baseline) {
|
|
77
|
+
return {
|
|
78
|
+
state: { baseline: state.baseline, candidate: "", candidateCount: 0 },
|
|
79
|
+
changed: false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const candidateCount = nextSignature === state.candidate ? state.candidateCount + 1 : 1;
|
|
83
|
+
if (candidateCount < required) {
|
|
84
|
+
return {
|
|
85
|
+
state: { baseline: state.baseline, candidate: nextSignature, candidateCount },
|
|
86
|
+
changed: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
state: { baseline: nextSignature, candidate: "", candidateCount: 0 },
|
|
91
|
+
changed: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
46
95
|
globalThis.PiStudioPreviewResourceHelpers = Object.freeze({
|
|
47
96
|
STUDIO_PREVIEW_LOCAL_IMAGE_LIMIT,
|
|
48
97
|
areStudioPreviewResourceContextsEqual,
|
|
98
|
+
buildStudioPdfVersionSignature,
|
|
99
|
+
createStudioPdfVersionObservationState,
|
|
49
100
|
hydrateStudioPreviewLocalImages,
|
|
50
101
|
isResolvableStudioPreviewImageSource,
|
|
102
|
+
observeStudioPdfVersion,
|
|
51
103
|
});
|
|
52
104
|
})();
|
package/client/studio.css
CHANGED
|
@@ -1822,6 +1822,20 @@
|
|
|
1822
1822
|
border-color: var(--control-border);
|
|
1823
1823
|
}
|
|
1824
1824
|
|
|
1825
|
+
.rendered-markdown button.studio-pdf-card-auto-refresh[aria-pressed="true"] {
|
|
1826
|
+
background: transparent;
|
|
1827
|
+
color: var(--accent);
|
|
1828
|
+
border-color: transparent;
|
|
1829
|
+
font-weight: 600;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
.rendered-markdown button.studio-pdf-card-auto-refresh[aria-pressed="true"]:not(:disabled):hover,
|
|
1833
|
+
.rendered-markdown button.studio-pdf-card-auto-refresh[aria-pressed="true"]:focus-visible {
|
|
1834
|
+
background: var(--panel);
|
|
1835
|
+
color: var(--accent);
|
|
1836
|
+
border-color: var(--control-border);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1825
1839
|
.rendered-markdown button.studio-pdf-card-focus {
|
|
1826
1840
|
width: 29px;
|
|
1827
1841
|
min-width: 29px;
|
|
@@ -1983,6 +1997,20 @@
|
|
|
1983
1997
|
color: var(--text);
|
|
1984
1998
|
}
|
|
1985
1999
|
|
|
2000
|
+
.studio-pdf-focus-auto-refresh[aria-pressed="true"] {
|
|
2001
|
+
background: transparent;
|
|
2002
|
+
border-color: transparent;
|
|
2003
|
+
color: var(--accent);
|
|
2004
|
+
font-weight: 600;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
.studio-pdf-focus-auto-refresh[aria-pressed="true"]:not(:disabled):hover,
|
|
2008
|
+
.studio-pdf-focus-auto-refresh[aria-pressed="true"]:focus-visible {
|
|
2009
|
+
background: var(--panel);
|
|
2010
|
+
border-color: var(--control-border);
|
|
2011
|
+
color: var(--accent);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1986
2014
|
.studio-pdf-focus-btn .studio-refresh-icon {
|
|
1987
2015
|
width: 14px;
|
|
1988
2016
|
height: 14px;
|
package/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
preserveLiteralLatexCommandsInMarkdown,
|
|
29
29
|
} from "./shared/studio-markdown-latex-literals.js";
|
|
30
30
|
import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
|
|
31
|
+
import { parseStudioLocalPreviewPage, parseStudioPdfLaunchTarget } from "./shared/studio-local-preview-path.js";
|
|
31
32
|
import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
|
|
32
33
|
import { createStudioPandocHtmlResourceFlagResolver } from "./shared/studio-pandoc-resource-flag.js";
|
|
33
34
|
import { prepareStudioLatexForPandoc } from "./shared/studio-latex-pandoc-compat.js";
|
|
@@ -276,6 +277,21 @@ interface InitialStudioDocument {
|
|
|
276
277
|
resourceDir?: string;
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
interface StudioLaunchSelection {
|
|
281
|
+
document: InitialStudioDocument;
|
|
282
|
+
kind: "document" | "pdf-preview";
|
|
283
|
+
mode?: StudioUiMode;
|
|
284
|
+
transient?: boolean;
|
|
285
|
+
skipWorkspaceRestore?: boolean;
|
|
286
|
+
paneFocus?: "left" | "right";
|
|
287
|
+
resourcePath?: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface StudioUrlOptions {
|
|
291
|
+
skipWorkspaceRestore?: boolean;
|
|
292
|
+
paneFocus?: "left" | "right";
|
|
293
|
+
}
|
|
294
|
+
|
|
279
295
|
type PersistedStudioReviewNoteAnchorKind = "source" | "html-selection" | "html-element" | "html-page";
|
|
280
296
|
|
|
281
297
|
interface PersistedStudioReviewNote {
|
|
@@ -2949,35 +2965,6 @@ function decodeStudioHtmlPreviewResourcePath(resourcePath: string): string {
|
|
|
2949
2965
|
}
|
|
2950
2966
|
}
|
|
2951
2967
|
|
|
2952
|
-
function parseStudioLocalPreviewResourcePage(resourcePath: string): number | null {
|
|
2953
|
-
const raw = String(resourcePath || "");
|
|
2954
|
-
const parts: string[] = [];
|
|
2955
|
-
const queryIndex = raw.indexOf("?");
|
|
2956
|
-
if (queryIndex >= 0) {
|
|
2957
|
-
const queryEnd = raw.indexOf("#", queryIndex);
|
|
2958
|
-
parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
|
|
2959
|
-
}
|
|
2960
|
-
const hashIndex = raw.indexOf("#");
|
|
2961
|
-
if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
|
|
2962
|
-
for (const part of parts) {
|
|
2963
|
-
try {
|
|
2964
|
-
const params = new URLSearchParams(part);
|
|
2965
|
-
const rawPage = params.get("page") || params.get("p");
|
|
2966
|
-
if (rawPage) {
|
|
2967
|
-
const page = Number.parseInt(rawPage, 10);
|
|
2968
|
-
if (Number.isFinite(page) && page > 0) return page;
|
|
2969
|
-
}
|
|
2970
|
-
} catch {
|
|
2971
|
-
const match = part.match(/(?:^|[&;])page=(\d+)/i) || part.match(/^page=(\d+)$/i);
|
|
2972
|
-
if (match && match[1]) {
|
|
2973
|
-
const page = Number.parseInt(match[1], 10);
|
|
2974
|
-
if (Number.isFinite(page) && page > 0) return page;
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
return null;
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
2968
|
function getStudioLocalPreviewResourceKind(extension: string, filePathOrName?: string): StudioLocalPreviewResourceKind {
|
|
2982
2969
|
const ext = extension.toLowerCase();
|
|
2983
2970
|
const name = basename(String(filePathOrName || "")).toLowerCase();
|
|
@@ -3022,7 +3009,7 @@ function resolveStudioLocalPreviewResourcePath(
|
|
|
3022
3009
|
label: rel && rel !== "" ? rel : basename(candidateReal),
|
|
3023
3010
|
extension,
|
|
3024
3011
|
kind: getStudioLocalPreviewResourceKind(extension, candidateReal),
|
|
3025
|
-
page:
|
|
3012
|
+
page: parseStudioLocalPreviewPage(rawPath),
|
|
3026
3013
|
resourceDir: boundaryReal,
|
|
3027
3014
|
};
|
|
3028
3015
|
}
|
|
@@ -7369,16 +7356,31 @@ function respondPdfFile(req: IncomingMessage, res: ServerResponse, filePath: str
|
|
|
7369
7356
|
return;
|
|
7370
7357
|
}
|
|
7371
7358
|
|
|
7372
|
-
const
|
|
7373
|
-
|
|
7359
|
+
const stats = statSync(filePath);
|
|
7360
|
+
const etag = `W/"${[
|
|
7361
|
+
stats.size,
|
|
7362
|
+
Math.trunc(stats.mtimeMs),
|
|
7363
|
+
Math.trunc(stats.ctimeMs),
|
|
7364
|
+
stats.ino,
|
|
7365
|
+
].map((value) => Number(value).toString(16)).join("-")}"`;
|
|
7366
|
+
const commonHeaders = {
|
|
7374
7367
|
"Content-Type": "application/pdf",
|
|
7375
|
-
"Content-Length": String(pdf.length),
|
|
7376
7368
|
"Content-Disposition": `inline; filename="${basename(filePath).replace(/["\\]/g, "") || "document.pdf"}"`,
|
|
7377
7369
|
"Cache-Control": "no-store",
|
|
7378
7370
|
"X-Content-Type-Options": "nosniff",
|
|
7379
7371
|
"Cross-Origin-Resource-Policy": "same-origin",
|
|
7380
|
-
|
|
7381
|
-
|
|
7372
|
+
"ETag": etag,
|
|
7373
|
+
"Last-Modified": stats.mtime.toUTCString(),
|
|
7374
|
+
};
|
|
7375
|
+
if (method === "HEAD") {
|
|
7376
|
+
res.writeHead(200, { ...commonHeaders, "Content-Length": String(stats.size) });
|
|
7377
|
+
res.end();
|
|
7378
|
+
return;
|
|
7379
|
+
}
|
|
7380
|
+
|
|
7381
|
+
const pdf = readFileSync(filePath);
|
|
7382
|
+
res.writeHead(200, { ...commonHeaders, "Content-Length": String(pdf.length) });
|
|
7383
|
+
res.end(pdf);
|
|
7382
7384
|
}
|
|
7383
7385
|
|
|
7384
7386
|
function respondHtmlPreviewResourceJson(req: IncomingMessage, res: ServerResponse, filePath: string, mimeType: string): void {
|
|
@@ -7406,7 +7408,7 @@ function sanitizeStudioPreviewBlockLine(value: string): string {
|
|
|
7406
7408
|
return String(value || "").replace(/[\r\n]+/g, " ").trim();
|
|
7407
7409
|
}
|
|
7408
7410
|
|
|
7409
|
-
function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewResource): InitialStudioDocument {
|
|
7411
|
+
function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewResource, options?: { watchPdf?: boolean }): InitialStudioDocument {
|
|
7410
7412
|
const label = basename(resource.filePath) || resource.label || "local preview";
|
|
7411
7413
|
const resourcePath = resource.label || basename(resource.filePath) || resource.filePath;
|
|
7412
7414
|
const title = sanitizeStudioPreviewBlockLine(label);
|
|
@@ -7415,6 +7417,8 @@ function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewRes
|
|
|
7415
7417
|
text = "```studio-pdf\n"
|
|
7416
7418
|
+ `path: ${sanitizeStudioPreviewBlockLine(resourcePath)}\n`
|
|
7417
7419
|
+ `title: ${title || "PDF preview"}\n`
|
|
7420
|
+
+ (resource.page ? `page: ${resource.page}\n` : "")
|
|
7421
|
+
+ (options?.watchPdf ? "watch: true\n" : "")
|
|
7418
7422
|
+ "height: 820\n"
|
|
7419
7423
|
+ "```\n";
|
|
7420
7424
|
} else if (resource.kind === "image") {
|
|
@@ -10283,7 +10287,7 @@ function buildStudioRelativeUrl(
|
|
|
10283
10287
|
mode: StudioUiMode = "full",
|
|
10284
10288
|
doc?: InitialStudioDocument | null,
|
|
10285
10289
|
docId?: string,
|
|
10286
|
-
options?:
|
|
10290
|
+
options?: StudioUrlOptions,
|
|
10287
10291
|
): string {
|
|
10288
10292
|
const params = new URLSearchParams({ token });
|
|
10289
10293
|
if (mode !== "full") params.set("mode", mode);
|
|
@@ -10294,6 +10298,7 @@ function buildStudioRelativeUrl(
|
|
|
10294
10298
|
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
10295
10299
|
if (doc?.resourceDir) params.set("resourceDir", doc.resourceDir);
|
|
10296
10300
|
if (options?.skipWorkspaceRestore) params.set("skipWorkspaceRestore", "1");
|
|
10301
|
+
if (options?.paneFocus) params.set("paneFocus", options.paneFocus);
|
|
10297
10302
|
return `/?${params.toString()}`;
|
|
10298
10303
|
}
|
|
10299
10304
|
|
|
@@ -10303,7 +10308,7 @@ function buildStudioUrl(
|
|
|
10303
10308
|
mode: StudioUiMode = "full",
|
|
10304
10309
|
doc?: InitialStudioDocument | null,
|
|
10305
10310
|
docId?: string,
|
|
10306
|
-
options?:
|
|
10311
|
+
options?: StudioUrlOptions,
|
|
10307
10312
|
): string {
|
|
10308
10313
|
return `http://127.0.0.1:${port}${buildStudioRelativeUrl(token, mode, doc, docId, options)}`;
|
|
10309
10314
|
}
|
|
@@ -10312,16 +10317,18 @@ interface StudioLaunchFlags {
|
|
|
10312
10317
|
args: string;
|
|
10313
10318
|
openRemoteBrowser: boolean;
|
|
10314
10319
|
noBrowser: boolean;
|
|
10320
|
+
watchPdf: boolean;
|
|
10315
10321
|
port?: number;
|
|
10316
10322
|
error?: string;
|
|
10317
10323
|
}
|
|
10318
10324
|
|
|
10319
10325
|
function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
10320
10326
|
const parsed = tokenizeStudioCommandArgs(rawArgs);
|
|
10321
|
-
if (parsed.error) return { args: rawArgs, openRemoteBrowser: false, noBrowser: false, error: parsed.error };
|
|
10327
|
+
if (parsed.error) return { args: rawArgs, openRemoteBrowser: false, noBrowser: false, watchPdf: false, error: parsed.error };
|
|
10322
10328
|
const remaining: string[] = [];
|
|
10323
10329
|
let openRemoteBrowser = false;
|
|
10324
10330
|
let noBrowser = false;
|
|
10331
|
+
let watchPdf = false;
|
|
10325
10332
|
let port: number | undefined;
|
|
10326
10333
|
for (let i = 0; i < parsed.tokens.length; i += 1) {
|
|
10327
10334
|
const token = parsed.tokens[i]!;
|
|
@@ -10333,14 +10340,18 @@ function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
|
10333
10340
|
noBrowser = true;
|
|
10334
10341
|
continue;
|
|
10335
10342
|
}
|
|
10343
|
+
if (token === "--watch" || token === "--auto-refresh") {
|
|
10344
|
+
watchPdf = true;
|
|
10345
|
+
continue;
|
|
10346
|
+
}
|
|
10336
10347
|
if (token === "--port" || token.startsWith("--port=")) {
|
|
10337
10348
|
const rawPort = token.startsWith("--port=") ? token.slice("--port=".length) : parsed.tokens[++i];
|
|
10338
10349
|
if (!rawPort) {
|
|
10339
|
-
return { args: rawArgs, openRemoteBrowser, noBrowser, error: "Missing value for --port." };
|
|
10350
|
+
return { args: rawArgs, openRemoteBrowser, noBrowser, watchPdf, error: "Missing value for --port." };
|
|
10340
10351
|
}
|
|
10341
10352
|
const requestedPort = Number(rawPort);
|
|
10342
10353
|
if (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65535) {
|
|
10343
|
-
return { args: rawArgs, openRemoteBrowser, noBrowser, error: `Invalid --port value: ${rawPort}. Use an integer from 1 to 65535.` };
|
|
10354
|
+
return { args: rawArgs, openRemoteBrowser, noBrowser, watchPdf, error: `Invalid --port value: ${rawPort}. Use an integer from 1 to 65535.` };
|
|
10344
10355
|
}
|
|
10345
10356
|
port = requestedPort;
|
|
10346
10357
|
continue;
|
|
@@ -10348,9 +10359,9 @@ function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
|
10348
10359
|
remaining.push(token);
|
|
10349
10360
|
}
|
|
10350
10361
|
if (openRemoteBrowser && noBrowser) {
|
|
10351
|
-
return { args: rawArgs, openRemoteBrowser, noBrowser, port, error: "Use either --no-browser or --open-browser, not both." };
|
|
10362
|
+
return { args: rawArgs, openRemoteBrowser, noBrowser, watchPdf, port, error: "Use either --no-browser or --open-browser, not both." };
|
|
10352
10363
|
}
|
|
10353
|
-
return { args: remaining.join(" "), openRemoteBrowser, noBrowser, port };
|
|
10364
|
+
return { args: remaining.join(" "), openRemoteBrowser, noBrowser, watchPdf, port };
|
|
10354
10365
|
}
|
|
10355
10366
|
|
|
10356
10367
|
function shouldAutoOpenStudioBrowser(options?: { openRemoteBrowser?: boolean; noBrowser?: boolean }): boolean {
|
|
@@ -11082,6 +11093,7 @@ ${cssVarsBlock}
|
|
|
11082
11093
|
<div><dt>Alt/Option+=</dt><dd>Increase the active pane's text size when not editing text</dd></div>
|
|
11083
11094
|
<div><dt>Alt/Option+-</dt><dd>Decrease the active pane's text size when not editing text</dd></div>
|
|
11084
11095
|
<div><dt>Alt/Option+0</dt><dd>Reset the active pane's text size when not editing text</dd></div>
|
|
11096
|
+
<div><dt>Cmd/Ctrl+Alt+R</dt><dd>Refresh the focused or visible PDF preview from disk</dd></div>
|
|
11085
11097
|
</dl>
|
|
11086
11098
|
</section>
|
|
11087
11099
|
<section class="shortcuts-group">
|
|
@@ -15744,10 +15756,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
15744
15756
|
const resolveStudioLaunchDocument = (
|
|
15745
15757
|
trimmed: string,
|
|
15746
15758
|
ctx: ExtensionCommandContext,
|
|
15747
|
-
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string },
|
|
15748
|
-
):
|
|
15759
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; allowPdfPreview?: boolean; watchPdf?: boolean },
|
|
15760
|
+
): StudioLaunchSelection | null => {
|
|
15749
15761
|
const defaultSource = options?.defaultSource === "blank" ? "blank" : "last-response";
|
|
15750
15762
|
const commandLabel = options?.commandLabel ?? "/studio";
|
|
15763
|
+
const selectDocument = (document: InitialStudioDocument): StudioLaunchSelection => ({ document, kind: "document" });
|
|
15751
15764
|
const latestAssistant =
|
|
15752
15765
|
extractLatestAssistantFromEntries(ctx.sessionManager.getBranch())
|
|
15753
15766
|
?? extractLatestAssistantFromEntries(ctx.sessionManager.getEntries())
|
|
@@ -15756,51 +15769,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
15756
15769
|
|
|
15757
15770
|
if (!trimmed) {
|
|
15758
15771
|
if (defaultSource === "last-response" && latestAssistant) {
|
|
15759
|
-
return {
|
|
15772
|
+
return selectDocument({
|
|
15760
15773
|
text: latestAssistant,
|
|
15761
15774
|
label: "last model response",
|
|
15762
15775
|
source: "last-response",
|
|
15763
15776
|
draftId: createStudioDraftId(),
|
|
15764
15777
|
resourceDir: ctx.cwd,
|
|
15765
|
-
};
|
|
15778
|
+
});
|
|
15766
15779
|
}
|
|
15767
|
-
return {
|
|
15780
|
+
return selectDocument({
|
|
15768
15781
|
text: "",
|
|
15769
15782
|
label: "blank",
|
|
15770
15783
|
source: "blank",
|
|
15771
15784
|
draftId: createStudioDraftId(),
|
|
15772
15785
|
resourceDir: ctx.cwd,
|
|
15773
|
-
};
|
|
15786
|
+
});
|
|
15774
15787
|
}
|
|
15775
15788
|
|
|
15776
15789
|
if (trimmed === "--blank" || trimmed === "blank") {
|
|
15777
|
-
return {
|
|
15790
|
+
return selectDocument({
|
|
15778
15791
|
text: "",
|
|
15779
15792
|
label: "blank",
|
|
15780
15793
|
source: "blank",
|
|
15781
15794
|
draftId: createStudioDraftId(),
|
|
15782
15795
|
resourceDir: ctx.cwd,
|
|
15783
|
-
};
|
|
15796
|
+
});
|
|
15784
15797
|
}
|
|
15785
15798
|
|
|
15786
15799
|
if (trimmed === "--last" || trimmed === "last") {
|
|
15787
15800
|
if (!latestAssistant) {
|
|
15788
15801
|
ctx.ui.notify("No assistant response found; opening blank studio.", "warning");
|
|
15789
|
-
return {
|
|
15802
|
+
return selectDocument({
|
|
15790
15803
|
text: "",
|
|
15791
15804
|
label: "blank",
|
|
15792
15805
|
source: "blank",
|
|
15793
15806
|
draftId: createStudioDraftId(),
|
|
15794
15807
|
resourceDir: ctx.cwd,
|
|
15795
|
-
};
|
|
15808
|
+
});
|
|
15796
15809
|
}
|
|
15797
|
-
return {
|
|
15810
|
+
return selectDocument({
|
|
15798
15811
|
text: latestAssistant,
|
|
15799
15812
|
label: "last model response",
|
|
15800
15813
|
source: "last-response",
|
|
15801
15814
|
draftId: createStudioDraftId(),
|
|
15802
15815
|
resourceDir: ctx.cwd,
|
|
15803
|
-
};
|
|
15816
|
+
});
|
|
15804
15817
|
}
|
|
15805
15818
|
|
|
15806
15819
|
if (trimmed.startsWith("-")) {
|
|
@@ -15814,6 +15827,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
15814
15827
|
return null;
|
|
15815
15828
|
}
|
|
15816
15829
|
|
|
15830
|
+
const pdfTarget = options?.allowPdfPreview ? parseStudioPdfLaunchTarget(normalizePathInput(pathArg)) : null;
|
|
15831
|
+
if (pdfTarget) {
|
|
15832
|
+
const resolved = resolveStudioPath(pdfTarget.path, ctx.cwd);
|
|
15833
|
+
if (resolved.ok === false) {
|
|
15834
|
+
ctx.ui.notify(resolved.message, "error");
|
|
15835
|
+
return null;
|
|
15836
|
+
}
|
|
15837
|
+
try {
|
|
15838
|
+
const resource = resolveStudioLocalPreviewResourcePath(
|
|
15839
|
+
pdfTarget.page ? `${resolved.resolved}#page=${pdfTarget.page}` : resolved.resolved,
|
|
15840
|
+
resolved.resolved,
|
|
15841
|
+
dirname(resolved.resolved),
|
|
15842
|
+
ctx.cwd,
|
|
15843
|
+
);
|
|
15844
|
+
if (resource.kind !== "pdf") throw new Error("Only local .pdf files can open in the Studio PDF viewer.");
|
|
15845
|
+
return {
|
|
15846
|
+
document: buildStudioLocalResourcePreviewDocument(resource, { watchPdf: options?.watchPdf }),
|
|
15847
|
+
kind: "pdf-preview",
|
|
15848
|
+
mode: "editor-only",
|
|
15849
|
+
transient: true,
|
|
15850
|
+
skipWorkspaceRestore: true,
|
|
15851
|
+
paneFocus: "right",
|
|
15852
|
+
resourcePath: resource.filePath,
|
|
15853
|
+
};
|
|
15854
|
+
} catch (error) {
|
|
15855
|
+
ctx.ui.notify(`Could not open PDF preview: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
15856
|
+
return null;
|
|
15857
|
+
}
|
|
15858
|
+
}
|
|
15859
|
+
|
|
15817
15860
|
const file = readStudioFile(pathArg, ctx.cwd);
|
|
15818
15861
|
if (file.ok === false) {
|
|
15819
15862
|
ctx.ui.notify(file.message, "error");
|
|
@@ -15827,13 +15870,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
15827
15870
|
);
|
|
15828
15871
|
}
|
|
15829
15872
|
|
|
15830
|
-
return {
|
|
15873
|
+
return selectDocument({
|
|
15831
15874
|
text: file.text,
|
|
15832
15875
|
label: file.label,
|
|
15833
15876
|
source: "file",
|
|
15834
15877
|
path: file.resolvedPath,
|
|
15835
15878
|
resourceDir: ctx.cwd,
|
|
15836
|
-
};
|
|
15879
|
+
});
|
|
15837
15880
|
};
|
|
15838
15881
|
|
|
15839
15882
|
const resolveLastModelResponseForExport = (ctx: ExtensionContext): { markdown: string } | null => {
|
|
@@ -16090,7 +16133,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16090
16133
|
trimmed: string,
|
|
16091
16134
|
ctx: ExtensionCommandContext,
|
|
16092
16135
|
mode: StudioUiMode,
|
|
16093
|
-
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean },
|
|
16136
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean; allowPdfPreview?: boolean; watchPdf?: boolean },
|
|
16094
16137
|
) => {
|
|
16095
16138
|
const launchOpenFlags = parseStudioLaunchOpenFlags(trimmed);
|
|
16096
16139
|
if (launchOpenFlags.error) {
|
|
@@ -16101,7 +16144,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
16101
16144
|
if (serverState && launchOpenFlags.port && serverState.port !== launchOpenFlags.port) {
|
|
16102
16145
|
ctx.ui.notify(`Studio server is already running on port ${serverState.port}; requested port ${launchOpenFlags.port}. Use /studio --stop, then restart Studio with --port ${launchOpenFlags.port} to change it.`, "warning");
|
|
16103
16146
|
}
|
|
16104
|
-
|
|
16147
|
+
|
|
16148
|
+
const parsedLaunchPath = options?.allowPdfPreview ? parsePathArgument(launchArgs) : null;
|
|
16149
|
+
const launchesPdfPreview = parsedLaunchPath
|
|
16150
|
+
? Boolean(parseStudioPdfLaunchTarget(normalizePathInput(parsedLaunchPath)))
|
|
16151
|
+
: false;
|
|
16152
|
+
if (launchOpenFlags.watchPdf && !launchesPdfPreview) {
|
|
16153
|
+
ctx.ui.notify("--watch requires a local PDF path, for example: /studio --watch main.pdf", "error");
|
|
16154
|
+
return;
|
|
16155
|
+
}
|
|
16156
|
+
const requestedLaunchMode: StudioUiMode = launchesPdfPreview ? "editor-only" : mode;
|
|
16157
|
+
if (requestedLaunchMode === "full" && hasConnectedFullStudioView()) {
|
|
16105
16158
|
if (options?.replaceExistingFull) {
|
|
16106
16159
|
closeStudioClientsByMode("full", 4001, "Full Studio replaced");
|
|
16107
16160
|
} else {
|
|
@@ -16132,9 +16185,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
16132
16185
|
// ignore theme read errors
|
|
16133
16186
|
}
|
|
16134
16187
|
|
|
16135
|
-
const
|
|
16136
|
-
|
|
16137
|
-
|
|
16188
|
+
const selection = resolveStudioLaunchDocument(launchArgs, ctx, {
|
|
16189
|
+
...options,
|
|
16190
|
+
watchPdf: launchOpenFlags.watchPdf,
|
|
16191
|
+
});
|
|
16192
|
+
if (!selection) return;
|
|
16193
|
+
const selected = selection.document;
|
|
16194
|
+
const launchMode = selection.mode ?? requestedLaunchMode;
|
|
16195
|
+
if (!selection.transient) initialStudioDocument = selected;
|
|
16138
16196
|
|
|
16139
16197
|
let state: StudioServerState;
|
|
16140
16198
|
try {
|
|
@@ -16145,10 +16203,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
16145
16203
|
ctx.ui.notify(`Failed to start Studio server${portText}: ${message}`, "error");
|
|
16146
16204
|
return;
|
|
16147
16205
|
}
|
|
16148
|
-
const
|
|
16206
|
+
const docId = selection.transient ? storeTransientStudioDocument(selected) : undefined;
|
|
16207
|
+
const url = buildStudioUrl(state.port, state.token, launchMode, selected, docId, {
|
|
16208
|
+
skipWorkspaceRestore: selection.skipWorkspaceRestore,
|
|
16209
|
+
paneFocus: selection.paneFocus,
|
|
16210
|
+
});
|
|
16149
16211
|
const tunnelHint = buildStudioSshTunnelHint(state.port, url)
|
|
16150
16212
|
?? (launchOpenFlags.noBrowser ? buildStudioForwardingHint(state.port, url, { prefix: "Browser auto-open was skipped because --no-browser was used." }) : null);
|
|
16151
|
-
const openedLabel =
|
|
16213
|
+
const openedLabel = selection.kind === "pdf-preview"
|
|
16214
|
+
? "pi Studio PDF preview"
|
|
16215
|
+
: (launchMode === "editor-only" ? "pi Studio editor-only view" : "pi Studio");
|
|
16152
16216
|
|
|
16153
16217
|
const shouldOpenBrowser = shouldAutoOpenStudioBrowser({
|
|
16154
16218
|
openRemoteBrowser: launchOpenFlags.openRemoteBrowser,
|
|
@@ -16160,7 +16224,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
16160
16224
|
ctx.ui.notify(`${openedLabel} is ready. Browser auto-open was skipped because ${skipReason}.`, "info");
|
|
16161
16225
|
} else {
|
|
16162
16226
|
await openStudioUrlInBrowser(url);
|
|
16163
|
-
if (
|
|
16227
|
+
if (selection.kind === "pdf-preview") {
|
|
16228
|
+
const watchLabel = launchOpenFlags.watchPdf ? " (auto-refresh on)" : "";
|
|
16229
|
+
ctx.ui.notify(`Opened ${openedLabel}${watchLabel}: ${selection.resourcePath ?? selected.label}`, "info");
|
|
16230
|
+
} else if (selected.source === "file") {
|
|
16164
16231
|
ctx.ui.notify(`Opened ${openedLabel} with file loaded: ${selected.label}`, "info");
|
|
16165
16232
|
} else if (selected.source === "last-response") {
|
|
16166
16233
|
ctx.ui.notify(`Opened ${openedLabel} with last model response (${selected.text.length} chars).`, "info");
|
|
@@ -16182,7 +16249,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16182
16249
|
};
|
|
16183
16250
|
|
|
16184
16251
|
pi.registerCommand("studio", {
|
|
16185
|
-
description: "Open pi Studio browser UI (/studio, /studio <file>, /studio --blank, /studio --last, /studio --no-browser
|
|
16252
|
+
description: "Open pi Studio browser UI or a PDF preview (/studio, /studio <file>, /studio --watch <pdf>, /studio --blank, /studio --last, /studio --no-browser)",
|
|
16186
16253
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
16187
16254
|
const trimmed = args.trim();
|
|
16188
16255
|
|
|
@@ -16212,7 +16279,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
16212
16279
|
ctx.ui.notify(
|
|
16213
16280
|
"Usage: /studio [path|--blank|--last]\n"
|
|
16214
16281
|
+ " /studio Open studio with last model response (fallback: blank)\n"
|
|
16215
|
-
+ " /studio <path> Open
|
|
16282
|
+
+ " /studio <path> Open a text file in Studio, or a PDF in a read-only companion preview\n"
|
|
16283
|
+
+ " /studio --watch <pdf> Open a PDF with auto-refresh enabled\n"
|
|
16216
16284
|
+ " /studio --blank Open with blank editor\n"
|
|
16217
16285
|
+ " /studio --last Open with last model response\n"
|
|
16218
16286
|
+ " /studio --no-browser Print the Studio URL without opening a browser\n"
|
|
@@ -16220,7 +16288,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16220
16288
|
+ " /studio --open-remote Over SSH, open the remote browser anyway\n"
|
|
16221
16289
|
+ " /studio --status Show studio status\n"
|
|
16222
16290
|
+ " /studio --stop Stop studio server\n"
|
|
16223
|
-
+ " Note: only one full /studio view is allowed per Pi session.\n"
|
|
16291
|
+
+ " Note: only one full /studio view is allowed per Pi session; PDF previews open as companions.\n"
|
|
16224
16292
|
+ " /studio-replace [path] Replace the current full Studio view with a new one\n"
|
|
16225
16293
|
+ " /studio-editor-only [path] Open another Studio tab in editor-only mode\n"
|
|
16226
16294
|
+ " /studio-current <path> Load a file into currently open Studio tab(s)\n"
|
|
@@ -16231,7 +16299,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16231
16299
|
return;
|
|
16232
16300
|
}
|
|
16233
16301
|
|
|
16234
|
-
await openStudioView(trimmed, ctx, "full", { defaultSource: "last-response", commandLabel: "/studio" });
|
|
16302
|
+
await openStudioView(trimmed, ctx, "full", { defaultSource: "last-response", commandLabel: "/studio", allowPdfPreview: true });
|
|
16235
16303
|
},
|
|
16236
16304
|
});
|
|
16237
16305
|
|
|
@@ -16263,14 +16331,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
16263
16331
|
});
|
|
16264
16332
|
|
|
16265
16333
|
pi.registerCommand("studio-editor-only", {
|
|
16266
|
-
description: "Open pi Studio in editor-only mode (/studio-editor-only, /studio-editor-only <file>, /studio-editor-only --
|
|
16334
|
+
description: "Open pi Studio in editor-only mode or preview a PDF (/studio-editor-only, /studio-editor-only <file>, /studio-editor-only --watch <pdf>)",
|
|
16267
16335
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
16268
16336
|
const trimmed = args.trim();
|
|
16269
16337
|
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
16270
16338
|
ctx.ui.notify(
|
|
16271
16339
|
"Usage: /studio-editor-only [path|--blank|--last]\n"
|
|
16272
16340
|
+ " /studio-editor-only Open an editor-only Studio view (default: blank editor)\n"
|
|
16273
|
-
+ " /studio-editor-only <path> Open
|
|
16341
|
+
+ " /studio-editor-only <path> Open a text file for editing, or a PDF in a read-only preview\n"
|
|
16342
|
+
+ " /studio-editor-only --watch <pdf> Open a PDF with auto-refresh enabled\n"
|
|
16274
16343
|
+ " /studio-editor-only --blank Open with blank editor\n"
|
|
16275
16344
|
+ " /studio-editor-only --last Open with last model response loaded into the editor\n"
|
|
16276
16345
|
+ " /studio-editor-only --no-browser Print URL without opening a browser\n"
|
|
@@ -16281,7 +16350,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16281
16350
|
return;
|
|
16282
16351
|
}
|
|
16283
16352
|
|
|
16284
|
-
await openStudioView(trimmed, ctx, "editor-only", { defaultSource: "blank", commandLabel: "/studio-editor-only" });
|
|
16353
|
+
await openStudioView(trimmed, ctx, "editor-only", { defaultSource: "blank", commandLabel: "/studio-editor-only", allowPdfPreview: true });
|
|
16285
16354
|
},
|
|
16286
16355
|
});
|
|
16287
16356
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.49",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function parseStudioLocalPreviewPage(resourcePath) {
|
|
2
|
+
const raw = String(resourcePath || "");
|
|
3
|
+
const parts = [];
|
|
4
|
+
const queryIndex = raw.indexOf("?");
|
|
5
|
+
if (queryIndex >= 0) {
|
|
6
|
+
const queryEnd = raw.indexOf("#", queryIndex);
|
|
7
|
+
parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
|
|
8
|
+
}
|
|
9
|
+
const hashIndex = raw.indexOf("#");
|
|
10
|
+
if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
try {
|
|
13
|
+
const params = new URLSearchParams(part);
|
|
14
|
+
const rawPage = params.get("page") || params.get("p");
|
|
15
|
+
if (rawPage) {
|
|
16
|
+
const page = Number.parseInt(rawPage, 10);
|
|
17
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
const match = part.match(/(?:^|[&;])page=(\d+)/i) || part.match(/^page=(\d+)$/i);
|
|
21
|
+
if (match && match[1]) {
|
|
22
|
+
const page = Number.parseInt(match[1], 10);
|
|
23
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseStudioPdfLaunchTarget(pathInput) {
|
|
31
|
+
const raw = String(pathInput || "").trim();
|
|
32
|
+
if (!raw || /\0/.test(raw) || /^\/\//.test(raw)) return null;
|
|
33
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(raw) && !/^[a-z]:[\\/]/i.test(raw)) return null;
|
|
34
|
+
|
|
35
|
+
const match = raw.match(/^(.*?\.pdf)(?:(?:\?[^#]*)?(?:#.*)?)?$/i);
|
|
36
|
+
if (!match || !match[1]) return null;
|
|
37
|
+
return {
|
|
38
|
+
path: match[1],
|
|
39
|
+
page: parseStudioLocalPreviewPage(raw),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
parseStudioLocalPreviewPage,
|
|
45
|
+
parseStudioPdfLaunchTarget,
|
|
46
|
+
};
|