pi-studio 0.9.38 → 0.9.40

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.
@@ -180,11 +180,12 @@
180
180
  : "full";
181
181
  const isEditorOnlyMode = studioMode === "editor-only";
182
182
  const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
183
- const EDITOR_ONLY_RIGHT_VIEW_ALLOWED = new Set(["editor-preview", "files", "changes", "repl"]);
183
+ const EDITOR_ONLY_RIGHT_VIEW_ALLOWED = new Set(["editor-preview", "editor-quarto-preview", "files", "changes", "repl"]);
184
184
  const RIGHT_VIEW_LABELS = {
185
185
  markdown: "Response (Raw)",
186
186
  preview: "Response (Preview)",
187
187
  "editor-preview": "Editor (Preview)",
188
+ "editor-quarto-preview": "Editor (Quarto Preview)",
188
189
  trace: "Working",
189
190
  changes: "Changes",
190
191
  files: "Files",
@@ -276,17 +277,22 @@
276
277
  ? "preview"
277
278
  : (raw === "editor-preview"
278
279
  ? "editor-preview"
279
- : (raw === "repl"
280
- ? "repl"
281
- : (raw === "files"
282
- ? "files"
283
- : (raw === "changes"
284
- ? "changes"
285
- : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown")))));
280
+ : (raw === "editor-quarto-preview"
281
+ ? "editor-quarto-preview"
282
+ : (raw === "repl"
283
+ ? "repl"
284
+ : (raw === "files"
285
+ ? "files"
286
+ : (raw === "changes"
287
+ ? "changes"
288
+ : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown"))))));
286
289
  }
287
290
 
288
291
  function normalizeRightViewValue(nextView) {
289
292
  const normalized = canonicalRightViewValue(nextView);
293
+ if (normalized === "editor-quarto-preview" && !isCurrentStudioQuartoDocument()) {
294
+ return "editor-preview";
295
+ }
290
296
  if (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(normalized)) {
291
297
  return "editor-preview";
292
298
  }
@@ -295,6 +301,7 @@
295
301
 
296
302
  function isRightViewAvailableInCurrentMode(view) {
297
303
  const normalized = canonicalRightViewValue(view);
304
+ if (normalized === "editor-quarto-preview" && !isCurrentStudioQuartoDocument()) return false;
298
305
  return !isEditorOnlyMode || EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(normalized);
299
306
  }
300
307
 
@@ -309,13 +316,16 @@
309
316
 
310
317
  function syncRightViewModeOptions() {
311
318
  if (!rightViewSelect || !rightViewSelect.options) return;
319
+ const quartoRelevant = isCurrentStudioQuartoDocument();
312
320
  Array.from(rightViewSelect.options).forEach((option) => {
313
321
  if (!option) return;
314
- option.disabled = isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value);
322
+ const isQuartoOption = option.value === "editor-quarto-preview";
323
+ if (isQuartoOption) option.hidden = !quartoRelevant;
324
+ option.disabled = (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value)) || (isQuartoOption && !quartoRelevant);
315
325
  });
316
326
  rightViewSelect.title = isEditorOnlyMode
317
- ? "Editor-only views: Editor Preview, Changes, Files, or REPL. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7 switch directly to available right-pane views."
318
- : "Right pane view mode. F7 cycles; Cmd/Ctrl+Alt+1–7 switches directly between all right-pane views. Cmd/Ctrl+Alt+P/E/W keep their mnemonic Preview/Editor Preview/Working shortcuts.";
327
+ ? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, or REPL. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7 switch directly to numbered right-pane views."
328
+ : "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–7 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W keep their mnemonic Preview/Editor Preview/Working shortcuts.";
319
329
  }
320
330
 
321
331
  function getInitialRightView(source) {
@@ -1200,11 +1210,21 @@
1200
1210
 
1201
1211
  function getStudioShortcutLabel(kind) {
1202
1212
  const mac = isMacShortcutPlatform();
1203
- if (kind === "repl-send") return mac ? "⇧⌘↵" : "Ctrl+Shift+Enter";
1213
+ if (kind === "repl-send") return mac ? "⌃⇧↵" : "Ctrl+Shift+Enter";
1204
1214
  if (kind === "run") return mac ? "⌘↵" : "Ctrl+Enter";
1205
1215
  return "";
1206
1216
  }
1207
1217
 
1218
+ function getStudioShortcutDescription(kind) {
1219
+ if (kind === "repl-send") {
1220
+ return isMacShortcutPlatform()
1221
+ ? "Ctrl+Shift+Enter (Cmd+Shift+Enter also works where available)"
1222
+ : "Ctrl+Shift+Enter";
1223
+ }
1224
+ if (kind === "run") return "Cmd/Ctrl+Enter";
1225
+ return "";
1226
+ }
1227
+
1208
1228
  function withStudioShortcutLabel(label, kind) {
1209
1229
  const shortcut = getStudioShortcutLabel(kind);
1210
1230
  return shortcut ? (label + " " + shortcut) : label;
@@ -2009,6 +2029,22 @@
2009
2029
  path: initialSourceState.path,
2010
2030
  draftId: initialSourceState.draftId,
2011
2031
  };
2032
+ let quartoPreviewContext = null;
2033
+ let quartoPreviewCheckRequestId = null;
2034
+ let quartoPreviewCheckSourcePath = "";
2035
+ let quartoPreviewActionRequestId = null;
2036
+ let quartoPreviewLogVisible = false;
2037
+ let quartoPreviewState = {
2038
+ status: "idle",
2039
+ sourcePath: "",
2040
+ url: "",
2041
+ log: "",
2042
+ error: null,
2043
+ context: null,
2044
+ startedAt: null,
2045
+ updatedAt: 0,
2046
+ actionRequestId: null,
2047
+ };
2012
2048
  let fileBackedBaselineText = null;
2013
2049
  let activePane = "left";
2014
2050
  let paneFocusTarget = "off";
@@ -3908,7 +3944,7 @@
3908
3944
  function getSelectEnabledValues(selectEl) {
3909
3945
  if (!selectEl || !selectEl.options) return [];
3910
3946
  return Array.from(selectEl.options)
3911
- .filter((option) => option && !option.disabled)
3947
+ .filter((option) => option && !option.disabled && !option.hidden)
3912
3948
  .map((option) => option.value)
3913
3949
  .filter((value) => typeof value === "string" && value);
3914
3950
  }
@@ -4743,6 +4779,22 @@
4743
4779
  return;
4744
4780
  }
4745
4781
 
4782
+ if (rightView === "editor-quarto-preview") {
4783
+ const context = getCurrentStudioQuartoContext();
4784
+ const current = getStudioQuartoProcessStatusForCurrentSource();
4785
+ const label = context && context.projectLabel ? context.projectLabel : (sourceState.label || "Quarto document");
4786
+ const statusLabel = quartoPreviewCheckRequestId
4787
+ ? "checking"
4788
+ : (context && !context.available
4789
+ ? "unavailable"
4790
+ : (current.sameSource
4791
+ ? (current.status === "running" ? "running" : (current.status === "starting" ? "starting" : (current.status === "error" ? "error" : "stopped")))
4792
+ : ((quartoPreviewState.status === "running" || quartoPreviewState.status === "starting") ? "another preview running" : "ready to start")));
4793
+ referenceBadgeEl.textContent = "Quarto: " + label + " · " + statusLabel + " · saved-file preview"
4794
+ + (isStudioQuartoEditorDirty() ? " · unsaved changes excluded" : "");
4795
+ return;
4796
+ }
4797
+
4746
4798
  if (rightView === "editor-preview") {
4747
4799
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
4748
4800
  if (hasResponse) {
@@ -8182,6 +8234,7 @@
8182
8234
  critiqueViewEl.addEventListener("click", handleReplPaneClick);
8183
8235
  critiqueViewEl.addEventListener("click", handleFilesPaneClick);
8184
8236
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
8237
+ critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
8185
8238
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
8186
8239
  critiqueViewEl.addEventListener("change", (event) => {
8187
8240
  void handleFilesPaneChange(event);
@@ -8207,7 +8260,7 @@
8207
8260
 
8208
8261
  function applyPendingResponseScrollReset() {
8209
8262
  if (!pendingResponseScrollReset || !critiqueViewEl) return false;
8210
- if (rightView === "editor-preview") return false;
8263
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return false;
8211
8264
 
8212
8265
  pendingResponseScrollReset = false;
8213
8266
  let targetEl = replaceResponsePaneWithClone();
@@ -8216,7 +8269,7 @@
8216
8269
  : (cb) => window.setTimeout(cb, 16);
8217
8270
  const resetScroll = () => {
8218
8271
  if (!targetEl || !targetEl.isConnected) return;
8219
- if (rightView === "editor-preview") return;
8272
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return;
8220
8273
  targetEl.scrollTop = 0;
8221
8274
  targetEl.scrollLeft = 0;
8222
8275
  };
@@ -10548,8 +10601,289 @@
10548
10601
  }
10549
10602
  }
10550
10603
 
10604
+ function getStudioQuartoProcessStatusForCurrentSource() {
10605
+ const sourcePath = getCurrentStudioQuartoSourcePath();
10606
+ return {
10607
+ sameSource: studioQuartoPathsMatch(quartoPreviewState.sourcePath, sourcePath)
10608
+ || Boolean(quartoPreviewState.context && studioQuartoPathsMatch(quartoPreviewState.context.requestedSourcePath, sourcePath)),
10609
+ sourcePath,
10610
+ status: quartoPreviewState.status,
10611
+ };
10612
+ }
10613
+
10614
+ function isStudioQuartoEditorDirty() {
10615
+ return Boolean(isCurrentStudioQuartoDocument() && editorDiffersFromFileBackedBaseline());
10616
+ }
10617
+
10618
+ function requestStudioQuartoPreviewCheck(force) {
10619
+ const sourcePath = getCurrentStudioQuartoSourcePath();
10620
+ if (!isCurrentStudioQuartoDocument()) return false;
10621
+ if (!ws || ws.readyState !== WebSocket.OPEN) return false;
10622
+ if (!force && getCurrentStudioQuartoContext()) return true;
10623
+ if (!force && quartoPreviewCheckRequestId && studioQuartoPathsMatch(quartoPreviewCheckSourcePath, sourcePath)) return true;
10624
+ const requestId = makeRequestId();
10625
+ quartoPreviewCheckRequestId = requestId;
10626
+ quartoPreviewCheckSourcePath = sourcePath;
10627
+ quartoPreviewContext = null;
10628
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
10629
+ const sent = sendMessage({ type: "quarto_preview_check_request", requestId, sourcePath });
10630
+ if (!sent) {
10631
+ quartoPreviewCheckRequestId = null;
10632
+ quartoPreviewCheckSourcePath = "";
10633
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
10634
+ }
10635
+ return sent;
10636
+ }
10637
+
10638
+ function requestStudioQuartoPreviewStart() {
10639
+ const sourcePath = getCurrentStudioQuartoSourcePath();
10640
+ if (!isCurrentStudioQuartoDocument()) {
10641
+ setStatus("Quarto preview requires a file-backed .qmd, .md, or .markdown document.", "warning");
10642
+ return false;
10643
+ }
10644
+ if (quartoPreviewActionRequestId) return false;
10645
+ const requestId = makeRequestId();
10646
+ quartoPreviewActionRequestId = requestId;
10647
+ const sent = sendMessage({ type: "quarto_preview_start_request", requestId, sourcePath });
10648
+ if (!sent) quartoPreviewActionRequestId = null;
10649
+ renderQuartoPreviewView();
10650
+ if (sent) setStatus("Starting Quarto preview with computational cell execution disabled…", "warning");
10651
+ return sent;
10652
+ }
10653
+
10654
+ function requestStudioQuartoPreviewStop() {
10655
+ if (quartoPreviewActionRequestId) return false;
10656
+ const requestId = makeRequestId();
10657
+ quartoPreviewActionRequestId = requestId;
10658
+ const sent = sendMessage({ type: "quarto_preview_stop_request", requestId });
10659
+ if (!sent) quartoPreviewActionRequestId = null;
10660
+ renderQuartoPreviewView();
10661
+ if (sent) setStatus("Stopping Quarto preview…", "warning");
10662
+ return sent;
10663
+ }
10664
+
10665
+ function openStudioQuartoPreviewInBrowser() {
10666
+ const rawUrl = String(quartoPreviewState.url || "").trim();
10667
+ if (!rawUrl) {
10668
+ setStatus("Quarto preview is not running yet.", "warning");
10669
+ return false;
10670
+ }
10671
+ try {
10672
+ const parsed = new URL(rawUrl);
10673
+ const hostname = parsed.hostname.toLowerCase();
10674
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("Unsupported preview protocol.");
10675
+ if (hostname !== "127.0.0.1" && hostname !== "localhost" && hostname !== "::1") throw new Error("Preview URL is not loopback-only.");
10676
+ const opened = window.open(parsed.href, "_blank");
10677
+ if (!opened) {
10678
+ setStatus("Browser blocked the Quarto preview tab. Allow pop-ups and try again.", "warning");
10679
+ return false;
10680
+ }
10681
+ try { opened.opener = null; } catch {}
10682
+ setStatus("Opening Quarto preview in a browser tab.", "success");
10683
+ return true;
10684
+ } catch (error) {
10685
+ setStatus("Could not open Quarto preview: " + ((error && error.message) ? error.message : String(error)), "error");
10686
+ return false;
10687
+ }
10688
+ }
10689
+
10690
+ function openStudioQuartoInstallationInstructions() {
10691
+ try {
10692
+ const opened = window.open("https://quarto.org/docs/get-started/", "_blank");
10693
+ if (!opened) {
10694
+ setStatus("Browser blocked the Quarto installation page.", "warning");
10695
+ return;
10696
+ }
10697
+ try { opened.opener = null; } catch {}
10698
+ } catch (error) {
10699
+ setStatus("Could not open Quarto installation instructions.", "warning");
10700
+ }
10701
+ }
10702
+
10703
+ function buildStudioQuartoLogDetails(log, open) {
10704
+ const text = String(log || "").trim();
10705
+ if (!text) return "";
10706
+ return "<details class='quarto-preview-log-details'" + (open ? " open" : "") + ">"
10707
+ + "<summary>Quarto log</summary>"
10708
+ + "<pre>" + escapeHtml(text) + "</pre>"
10709
+ + "</details>";
10710
+ }
10711
+
10712
+ function buildStudioQuartoContextSummary(context) {
10713
+ if (!context) return "Quarto document";
10714
+ const kind = context.isProject
10715
+ ? (context.projectType ? (context.projectType.charAt(0).toUpperCase() + context.projectType.slice(1) + " project") : "Quarto project")
10716
+ : "Standalone document";
10717
+ return kind + (context.version ? " · Quarto " + context.version : "");
10718
+ }
10719
+
10720
+ function updateStudioQuartoRunningShell() {
10721
+ const context = getCurrentStudioQuartoContext() || quartoPreviewState.context;
10722
+ const url = String(quartoPreviewState.url || "");
10723
+ let shell = critiqueViewEl ? critiqueViewEl.querySelector(".quarto-preview-shell[data-preview-url]") : null;
10724
+ if (!shell || shell.getAttribute("data-preview-url") !== url) {
10725
+ const label = context && context.projectLabel ? context.projectLabel : (sourceState.label || "Quarto document");
10726
+ critiqueViewEl.innerHTML = ""
10727
+ + "<div class='quarto-preview-shell' data-preview-url='" + escapeHtml(url) + "'>"
10728
+ + "<div class='quarto-preview-toolbar'>"
10729
+ + "<div class='quarto-preview-title-group'>"
10730
+ + "<span class='quarto-preview-title'>" + escapeHtml(label) + "</span>"
10731
+ + "<span class='quarto-preview-subtitle'>" + escapeHtml(buildStudioQuartoContextSummary(context)) + "</span>"
10732
+ + "</div>"
10733
+ + "<div class='quarto-preview-actions'>"
10734
+ + "<button type='button' data-quarto-action='open'>Open in browser ↗</button>"
10735
+ + "<button type='button' data-quarto-action='restart'>Restart</button>"
10736
+ + "<button type='button' data-quarto-action='toggle-log'>Show log</button>"
10737
+ + "<button type='button' data-quarto-action='stop'>Stop</button>"
10738
+ + "</div>"
10739
+ + "</div>"
10740
+ + "<div class='quarto-preview-dirty-warning' hidden>Unsaved editor changes are not included. Save the editor to update Quarto.</div>"
10741
+ + "<iframe class='quarto-preview-frame' src='" + escapeHtml(url) + "' title='Quarto preview for " + escapeHtml(label) + "' sandbox='allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads' allow='clipboard-write; fullscreen' allowfullscreen referrerpolicy='no-referrer'></iframe>"
10742
+ + "<div class='quarto-preview-live-log' hidden><pre></pre></div>"
10743
+ + "</div>";
10744
+ shell = critiqueViewEl.querySelector(".quarto-preview-shell[data-preview-url]");
10745
+ }
10746
+ const warningEl = shell ? shell.querySelector(".quarto-preview-dirty-warning") : null;
10747
+ if (warningEl) warningEl.hidden = !isStudioQuartoEditorDirty();
10748
+ const logPanelEl = shell ? shell.querySelector(".quarto-preview-live-log") : null;
10749
+ const logPreEl = logPanelEl ? logPanelEl.querySelector("pre") : null;
10750
+ if (logPanelEl) logPanelEl.hidden = !quartoPreviewLogVisible;
10751
+ if (logPreEl && logPreEl.textContent !== quartoPreviewState.log) logPreEl.textContent = quartoPreviewState.log;
10752
+ const toggleLogBtn = shell ? shell.querySelector("[data-quarto-action='toggle-log']") : null;
10753
+ if (toggleLogBtn) toggleLogBtn.textContent = quartoPreviewLogVisible ? "Hide log" : "Show log";
10754
+ if (shell) {
10755
+ shell.querySelectorAll("button[data-quarto-action]").forEach((button) => {
10756
+ if (button.getAttribute("data-quarto-action") === "open" || button.getAttribute("data-quarto-action") === "toggle-log") return;
10757
+ button.disabled = Boolean(quartoPreviewActionRequestId);
10758
+ });
10759
+ }
10760
+ }
10761
+
10762
+ function renderQuartoPreviewView() {
10763
+ if (!critiqueViewEl) return;
10764
+ finishPreviewRender(critiqueViewEl);
10765
+ if (!isCurrentStudioQuartoDocument()) {
10766
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card'><h2>Quarto preview unavailable</h2><p>Open a file-backed <code>.qmd</code>, <code>.md</code>, or <code>.markdown</code> document to use this view.</p></div>";
10767
+ return;
10768
+ }
10769
+
10770
+ const current = getStudioQuartoProcessStatusForCurrentSource();
10771
+ const context = getCurrentStudioQuartoContext();
10772
+ const checking = Boolean(quartoPreviewCheckRequestId && studioQuartoPathsMatch(quartoPreviewCheckSourcePath, current.sourcePath));
10773
+ const actionPending = Boolean(quartoPreviewActionRequestId);
10774
+ if (current.sameSource && current.status === "running" && quartoPreviewState.url) {
10775
+ updateStudioQuartoRunningShell();
10776
+ return;
10777
+ }
10778
+
10779
+ if (checking && !context) {
10780
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10781
+ + "<div class='quarto-preview-spinner' aria-hidden='true'></div>"
10782
+ + "<h2>Checking Quarto…</h2>"
10783
+ + "<p>Inspecting the saved document and its project configuration.</p>"
10784
+ + "</div>";
10785
+ return;
10786
+ }
10787
+
10788
+ if (!context) {
10789
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10790
+ + "<h2>Editor (Quarto Preview)</h2>"
10791
+ + "<p>Check the installed Quarto version and this saved document before starting a preview.</p>"
10792
+ + "<div class='quarto-preview-card-actions'><button type='button' data-quarto-action='check'>Check Quarto</button></div>"
10793
+ + "</div>";
10794
+ return;
10795
+ }
10796
+
10797
+ if (!context.available) {
10798
+ const missing = context.reason === "not-found";
10799
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10800
+ + "<h2>" + (missing ? "Quarto not found" : "Quarto preview unavailable") + "</h2>"
10801
+ + "<p>" + escapeHtml(context.error || "Studio could not inspect this Quarto document.") + "</p>"
10802
+ + (missing ? "<p class='quarto-preview-note'>Studio could not find <code>quarto</code> on its PATH.</p>" : "")
10803
+ + "<div class='quarto-preview-card-actions'>"
10804
+ + "<button type='button' data-quarto-action='check'>Check again</button>"
10805
+ + (missing ? "<button type='button' data-quarto-action='install'>Installation instructions ↗</button>" : "")
10806
+ + "</div>"
10807
+ + buildStudioQuartoLogDetails(context.inspectLog, true)
10808
+ + "</div>";
10809
+ return;
10810
+ }
10811
+
10812
+ const runningAnother = !current.sameSource && (current.status === "running" || current.status === "starting" || current.status === "stopping");
10813
+ const dirtyWarning = isStudioQuartoEditorDirty()
10814
+ ? "<div class='quarto-preview-dirty-warning'>Unsaved editor changes are not included. Save before starting or refreshing Quarto.</div>"
10815
+ : "";
10816
+ if (current.sameSource && (current.status === "starting" || current.status === "stopping")) {
10817
+ const stopping = current.status === "stopping";
10818
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10819
+ + "<div class='quarto-preview-spinner' aria-hidden='true'></div>"
10820
+ + "<h2>" + (stopping ? "Stopping Quarto preview…" : "Starting Quarto preview…") + "</h2>"
10821
+ + "<p>" + escapeHtml(buildStudioQuartoContextSummary(context)) + " · computational cells will not be executed.</p>"
10822
+ + dirtyWarning
10823
+ + (!stopping ? "<div class='quarto-preview-card-actions'><button type='button' data-quarto-action='stop'" + (actionPending ? " disabled" : "") + ">Stop</button></div>" : "")
10824
+ + buildStudioQuartoLogDetails(quartoPreviewState.log, true)
10825
+ + "</div>";
10826
+ return;
10827
+ }
10828
+
10829
+ const sameSourceError = current.sameSource && current.status === "error";
10830
+ const label = context.projectLabel || sourceState.label || "Quarto document";
10831
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10832
+ + "<h2>" + (sameSourceError ? "Quarto preview stopped with an error" : "Ready to preview with Quarto") + "</h2>"
10833
+ + "<p><strong>" + escapeHtml(label) + "</strong><br>" + escapeHtml(buildStudioQuartoContextSummary(context)) + "</p>"
10834
+ + (sameSourceError ? "<div class='quarto-preview-error'>" + escapeHtml(quartoPreviewState.error || "Quarto preview failed.") + "</div>" : "")
10835
+ + (runningAnother ? "<div class='quarto-preview-note'>Another Quarto document is currently being previewed. Starting this one will replace it.</div>" : "")
10836
+ + dirtyWarning
10837
+ + "<div class='quarto-preview-safety-note'><strong>Saved-file preview.</strong> Studio starts Quarto on <code>127.0.0.1</code> with <code>--no-execute</code>, so computational cells are not run. Quarto still processes the project's trusted configuration, extensions, filters, and render hooks, controls the page styling, and may create or update its normal rendered output files.</div>"
10838
+ + "<div class='quarto-preview-card-actions'>"
10839
+ + "<button type='button' class='quarto-preview-primary' data-quarto-action='start'" + (actionPending ? " disabled" : "") + ">" + (sameSourceError ? "Retry Quarto preview" : (runningAnother ? "Preview this document" : "Start Quarto preview")) + "</button>"
10840
+ + (runningAnother && quartoPreviewState.url ? "<button type='button' data-quarto-action='open'>Open running preview ↗</button>" : "")
10841
+ + (runningAnother ? "<button type='button' data-quarto-action='stop'" + (actionPending ? " disabled" : "") + ">Stop running preview</button>" : "")
10842
+ + "</div>"
10843
+ + buildStudioQuartoLogDetails(sameSourceError ? quartoPreviewState.log : context.inspectLog, sameSourceError)
10844
+ + "</div>";
10845
+ }
10846
+
10847
+ function syncStudioQuartoDirtyUi() {
10848
+ if (rightView !== "editor-quarto-preview") return;
10849
+ const warningEl = critiqueViewEl ? critiqueViewEl.querySelector(".quarto-preview-dirty-warning") : null;
10850
+ if (warningEl && warningEl.classList && warningEl.closest(".quarto-preview-shell")) {
10851
+ warningEl.hidden = !isStudioQuartoEditorDirty();
10852
+ }
10853
+ updateReferenceBadge();
10854
+ }
10855
+
10856
+ function handleStudioQuartoPreviewClick(event) {
10857
+ if (rightView !== "editor-quarto-preview") return;
10858
+ const target = event && event.target instanceof Element ? event.target.closest("[data-quarto-action]") : null;
10859
+ if (!target || !critiqueViewEl.contains(target)) return;
10860
+ event.preventDefault();
10861
+ const action = target.getAttribute("data-quarto-action");
10862
+ if (action === "check") {
10863
+ requestStudioQuartoPreviewCheck(true);
10864
+ } else if (action === "start" || action === "restart") {
10865
+ requestStudioQuartoPreviewStart();
10866
+ } else if (action === "stop") {
10867
+ requestStudioQuartoPreviewStop();
10868
+ } else if (action === "open") {
10869
+ openStudioQuartoPreviewInBrowser();
10870
+ } else if (action === "install") {
10871
+ openStudioQuartoInstallationInstructions();
10872
+ } else if (action === "toggle-log") {
10873
+ quartoPreviewLogVisible = !quartoPreviewLogVisible;
10874
+ updateStudioQuartoRunningShell();
10875
+ }
10876
+ }
10877
+
10551
10878
  function renderActiveResult() {
10552
- if (critiqueViewEl) critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
10879
+ if (critiqueViewEl) {
10880
+ critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
10881
+ critiqueViewEl.classList.toggle("quarto-preview-host", rightView === "editor-quarto-preview");
10882
+ }
10883
+ if (rightView === "editor-quarto-preview") {
10884
+ renderQuartoPreviewView();
10885
+ return;
10886
+ }
10553
10887
  if (rightView === "trace") {
10554
10888
  renderTraceView();
10555
10889
  return;
@@ -10648,7 +10982,7 @@
10648
10982
  : normalizeForCompare(sourceTextEl.value);
10649
10983
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
10650
10984
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
10651
- const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes";
10985
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes" || rightView === "editor-quarto-preview";
10652
10986
 
10653
10987
  if (responseWrapEl) {
10654
10988
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -10732,6 +11066,7 @@
10732
11066
  : (exportingReplJournal ? "Export the Studio REPL record as standalone HTML and open it in the default browser." : "Export the current right-pane preview as standalone HTML and open it in the default browser.");
10733
11067
  }
10734
11068
  if (exportPreviewControlsEl) {
11069
+ exportPreviewControlsEl.hidden = rightView === "editor-quarto-preview";
10735
11070
  exportPreviewControlsEl.title = canExportPreview
10736
11071
  ? (exportingReplJournal
10737
11072
  ? "Choose a format and export destination for the Studio REPL record."
@@ -10746,6 +11081,7 @@
10746
11081
  pullLatestBtn.textContent = queuedLatestResponse ? "Fetch latest response *" : "Fetch latest response";
10747
11082
 
10748
11083
  updateSyncBadge(normalizedEditor);
11084
+ syncStudioQuartoDirtyUi();
10749
11085
  }
10750
11086
 
10751
11087
  function refreshResponseUi() {
@@ -10802,6 +11138,86 @@
10802
11138
  return null;
10803
11139
  }
10804
11140
 
11141
+ function normalizeStudioQuartoClientPath(value) {
11142
+ const normalized = String(value || "").trim().replace(/\\/g, "/").replace(/\/+$/, "");
11143
+ return /^[A-Za-z]:\//.test(normalized) ? normalized.toLowerCase() : normalized;
11144
+ }
11145
+
11146
+ function getCurrentStudioQuartoSourcePath() {
11147
+ return sourceState && typeof sourceState.path === "string" ? sourceState.path.trim() : "";
11148
+ }
11149
+
11150
+ function isCurrentStudioQuartoDocument() {
11151
+ return /\.(?:qmd|md|markdown)$/i.test(getCurrentStudioQuartoSourcePath());
11152
+ }
11153
+
11154
+ function studioQuartoPathsMatch(left, right) {
11155
+ const a = normalizeStudioQuartoClientPath(left);
11156
+ const b = normalizeStudioQuartoClientPath(right);
11157
+ return Boolean(a && b && a === b);
11158
+ }
11159
+
11160
+ function normalizeStudioQuartoContext(value) {
11161
+ if (!value || typeof value !== "object") return null;
11162
+ return {
11163
+ sourcePath: typeof value.sourcePath === "string" ? value.sourcePath : "",
11164
+ requestedSourcePath: typeof value.requestedSourcePath === "string" ? value.requestedSourcePath : (typeof value.sourcePath === "string" ? value.sourcePath : ""),
11165
+ available: value.available === true,
11166
+ reason: typeof value.reason === "string" ? value.reason : "inspect-error",
11167
+ version: typeof value.version === "string" ? value.version : "",
11168
+ projectRoot: typeof value.projectRoot === "string" ? value.projectRoot : "",
11169
+ projectType: typeof value.projectType === "string" ? value.projectType : "",
11170
+ projectLabel: typeof value.projectLabel === "string" ? value.projectLabel : "Quarto document",
11171
+ outputFile: typeof value.outputFile === "string" ? value.outputFile : "",
11172
+ isProject: value.isProject === true,
11173
+ error: typeof value.error === "string" && value.error ? value.error : null,
11174
+ inspectLog: typeof value.inspectLog === "string" ? value.inspectLog : "",
11175
+ };
11176
+ }
11177
+
11178
+ function normalizeStudioQuartoPreviewState(value) {
11179
+ if (!value || typeof value !== "object") return {
11180
+ status: "idle",
11181
+ sourcePath: "",
11182
+ url: "",
11183
+ log: "",
11184
+ error: null,
11185
+ context: null,
11186
+ startedAt: null,
11187
+ updatedAt: 0,
11188
+ actionRequestId: null,
11189
+ };
11190
+ const allowedStatuses = new Set(["idle", "starting", "running", "stopping", "stopped", "error"]);
11191
+ return {
11192
+ status: allowedStatuses.has(value.status) ? value.status : "idle",
11193
+ sourcePath: typeof value.sourcePath === "string" ? value.sourcePath : "",
11194
+ url: typeof value.url === "string" ? value.url : "",
11195
+ log: typeof value.log === "string" ? value.log : "",
11196
+ error: typeof value.error === "string" && value.error ? value.error : null,
11197
+ context: normalizeStudioQuartoContext(value.context),
11198
+ startedAt: typeof value.startedAt === "number" && Number.isFinite(value.startedAt) ? value.startedAt : null,
11199
+ updatedAt: typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) ? value.updatedAt : 0,
11200
+ actionRequestId: typeof value.actionRequestId === "string" ? value.actionRequestId : null,
11201
+ };
11202
+ }
11203
+
11204
+ function getCurrentStudioQuartoContext() {
11205
+ const sourcePath = getCurrentStudioQuartoSourcePath();
11206
+ if (quartoPreviewContext && (
11207
+ studioQuartoPathsMatch(quartoPreviewContext.sourcePath, sourcePath)
11208
+ || studioQuartoPathsMatch(quartoPreviewContext.requestedSourcePath, sourcePath)
11209
+ )) {
11210
+ return quartoPreviewContext;
11211
+ }
11212
+ if (quartoPreviewState.context && (
11213
+ studioQuartoPathsMatch(quartoPreviewState.context.sourcePath, sourcePath)
11214
+ || studioQuartoPathsMatch(quartoPreviewState.context.requestedSourcePath, sourcePath)
11215
+ )) {
11216
+ return quartoPreviewState.context;
11217
+ }
11218
+ return null;
11219
+ }
11220
+
10805
11221
  function getHtmlPreviewResourceContextOptions() {
10806
11222
  const sourcePath = getEffectiveSavePath() || sourceState.path || "";
10807
11223
  const resourceDir = getCurrentResourceDirValue();
@@ -10917,6 +11333,7 @@
10917
11333
 
10918
11334
  function setSourceState(next, options) {
10919
11335
  const previousDescriptor = getCurrentStudioDocumentDescriptor();
11336
+ const previousQuartoPath = getCurrentStudioQuartoSourcePath();
10920
11337
  const nextPath = next && next.path ? next.path : null;
10921
11338
  sourceState = {
10922
11339
  source: next && next.source ? next.source : "blank",
@@ -10926,9 +11343,24 @@
10926
11343
  ? null
10927
11344
  : (next && next.draftId ? next.draftId : makeStudioDraftId()),
10928
11345
  };
11346
+ const nextQuartoPath = getCurrentStudioQuartoSourcePath();
11347
+ const quartoSourceChanged = !studioQuartoPathsMatch(previousQuartoPath, nextQuartoPath);
11348
+ if (quartoSourceChanged) {
11349
+ quartoPreviewContext = null;
11350
+ quartoPreviewCheckRequestId = null;
11351
+ quartoPreviewCheckSourcePath = "";
11352
+ quartoPreviewActionRequestId = null;
11353
+ quartoPreviewLogVisible = false;
11354
+ }
10929
11355
  if (!sourceState.path) {
10930
11356
  clearFileBackedBaseline();
10931
11357
  }
11358
+ syncRightViewModeOptions();
11359
+ const leavingUnavailableQuartoView = rightView === "editor-quarto-preview" && !isCurrentStudioQuartoDocument();
11360
+ if (leavingUnavailableQuartoView) {
11361
+ rightView = "editor-preview";
11362
+ rightViewSelect.value = rightView;
11363
+ }
10932
11364
  updateStudioDocumentUrlState(sourceState);
10933
11365
  updateSourceBadge();
10934
11366
  syncActionButtons();
@@ -10942,6 +11374,9 @@
10942
11374
  previousDescriptor: previousDescriptor,
10943
11375
  carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
10944
11376
  });
11377
+ const refreshChangedQuartoView = rightView === "editor-quarto-preview" && quartoSourceChanged && isCurrentStudioQuartoDocument();
11378
+ if (refreshChangedQuartoView) requestStudioQuartoPreviewCheck(false);
11379
+ if (leavingUnavailableQuartoView || refreshChangedQuartoView) refreshResponseUi();
10945
11380
  scheduleWorkspacePersistence();
10946
11381
  }
10947
11382
 
@@ -11759,6 +12194,9 @@
11759
12194
  if (rightView !== "editor-preview") {
11760
12195
  clearPreviewJumpHighlight(critiqueViewEl);
11761
12196
  }
12197
+ if (rightView === "editor-quarto-preview") {
12198
+ requestStudioQuartoPreviewCheck(false);
12199
+ }
11762
12200
 
11763
12201
  refreshResponseUi();
11764
12202
  syncActionButtons();
@@ -18760,6 +19198,7 @@
18760
19198
  const canQueueSteering = studioRunChainActive && !critiqueIsStop;
18761
19199
  const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
18762
19200
  const showReplSend = rightView === "repl";
19201
+ const replSendShortcut = getStudioShortcutDescription("repl-send");
18763
19202
 
18764
19203
  if (isEditorOnlyMode) {
18765
19204
  if (sendRunBtn) {
@@ -18781,8 +19220,8 @@
18781
19220
  sendReplBtn.textContent = showReplSend ? withStudioShortcutLabel(replSendMode === "literate" ? "Send selection/chunks" : "Send to REPL", "repl-send") : "Send to REPL";
18782
19221
  sendReplBtn.title = hasReplSession
18783
19222
  ? (replSendMode === "literate"
18784
- ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
18785
- : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
19223
+ ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: " + replSendShortcut + "."
19224
+ : "Raw send: selection, or full editor if no selection. Shortcut: " + replSendShortcut + ".")
18786
19225
  : "Start or select a REPL session in the right pane first.";
18787
19226
  const replActionLine = sendReplBtn.closest(".repl-action-line");
18788
19227
  if (replActionLine instanceof HTMLElement) replActionLine.hidden = !showReplSend;
@@ -18842,8 +19281,8 @@
18842
19281
  sendReplBtn.textContent = showReplSend ? withStudioShortcutLabel(replSendMode === "literate" ? "Send selection/chunks" : "Send to REPL", "repl-send") : "Send to REPL";
18843
19282
  sendReplBtn.title = hasReplSession
18844
19283
  ? (replSendMode === "literate"
18845
- ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
18846
- : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
19284
+ ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: " + replSendShortcut + "."
19285
+ : "Raw send: selection, or full editor if no selection. Shortcut: " + replSendShortcut + ".")
18847
19286
  : "Start or select a REPL session in the right pane first.";
18848
19287
  const replActionLine = sendReplBtn.closest(".repl-action-line");
18849
19288
  if (replActionLine instanceof HTMLElement) replActionLine.hidden = !showReplSend;
@@ -19038,6 +19477,80 @@
19038
19477
  return;
19039
19478
  }
19040
19479
 
19480
+ if (message.type === "quarto_preview_context") {
19481
+ const requestId = typeof message.requestId === "string" ? message.requestId : "";
19482
+ const context = normalizeStudioQuartoContext(message.context);
19483
+ const matchesCheck = Boolean(requestId && quartoPreviewCheckRequestId === requestId);
19484
+ const matchesAction = Boolean(requestId && quartoPreviewActionRequestId === requestId);
19485
+ if (!matchesCheck && !matchesAction) return;
19486
+ if (matchesCheck) {
19487
+ quartoPreviewCheckRequestId = null;
19488
+ quartoPreviewCheckSourcePath = "";
19489
+ }
19490
+ if (context) quartoPreviewContext = context;
19491
+ if (matchesAction && (!context || !context.available)) {
19492
+ quartoPreviewActionRequestId = null;
19493
+ }
19494
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
19495
+ updateReferenceBadge();
19496
+ return;
19497
+ }
19498
+
19499
+ if (message.type === "quarto_preview_state") {
19500
+ const previousQuartoStatus = quartoPreviewState.status;
19501
+ quartoPreviewState = normalizeStudioQuartoPreviewState(message.preview);
19502
+ if (quartoPreviewState.actionRequestId && quartoPreviewActionRequestId === quartoPreviewState.actionRequestId) {
19503
+ quartoPreviewActionRequestId = null;
19504
+ }
19505
+ if (quartoPreviewState.context && (
19506
+ studioQuartoPathsMatch(quartoPreviewState.context.sourcePath, getCurrentStudioQuartoSourcePath())
19507
+ || studioQuartoPathsMatch(quartoPreviewState.context.requestedSourcePath, getCurrentStudioQuartoSourcePath())
19508
+ )) {
19509
+ quartoPreviewContext = quartoPreviewState.context;
19510
+ }
19511
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
19512
+ updateReferenceBadge();
19513
+ syncActionButtons();
19514
+ if (quartoPreviewState.status !== previousQuartoStatus) {
19515
+ if (quartoPreviewState.status === "running") {
19516
+ setStatus("Quarto preview is running with computational cell execution disabled.", "success");
19517
+ } else if (quartoPreviewState.status === "error" && quartoPreviewState.error) {
19518
+ setStatus(quartoPreviewState.error, "error");
19519
+ } else if (quartoPreviewState.status === "stopped") {
19520
+ setStatus("Quarto preview stopped.");
19521
+ }
19522
+ }
19523
+ return;
19524
+ }
19525
+
19526
+ if (message.type === "quarto_preview_action_error") {
19527
+ const requestId = typeof message.requestId === "string" ? message.requestId : "";
19528
+ if (requestId && quartoPreviewCheckRequestId === requestId) {
19529
+ quartoPreviewCheckRequestId = null;
19530
+ quartoPreviewCheckSourcePath = "";
19531
+ }
19532
+ if (requestId && quartoPreviewActionRequestId === requestId) quartoPreviewActionRequestId = null;
19533
+ const messageText = typeof message.message === "string" ? message.message : "Quarto preview request failed.";
19534
+ quartoPreviewContext = {
19535
+ sourcePath: getCurrentStudioQuartoSourcePath(),
19536
+ requestedSourcePath: getCurrentStudioQuartoSourcePath(),
19537
+ available: false,
19538
+ reason: "inspect-error",
19539
+ version: "",
19540
+ projectRoot: "",
19541
+ projectType: "",
19542
+ projectLabel: sourceState.label || "Quarto document",
19543
+ outputFile: "",
19544
+ isProject: false,
19545
+ error: messageText,
19546
+ inspectLog: "",
19547
+ };
19548
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
19549
+ updateReferenceBadge();
19550
+ setStatus(messageText, "error");
19551
+ return;
19552
+ }
19553
+
19041
19554
  if (message.type === "hello_ack") {
19042
19555
  const busy = Boolean(message.busy);
19043
19556
  agentBusyFromServer = Boolean(message.agentBusy);
@@ -19050,6 +19563,15 @@
19050
19563
  }
19051
19564
  updatePiSessionModelState(message);
19052
19565
  updatePiThemeState(message);
19566
+ if (message.quartoPreview && typeof message.quartoPreview === "object") {
19567
+ quartoPreviewState = normalizeStudioQuartoPreviewState(message.quartoPreview);
19568
+ if (quartoPreviewState.context && (
19569
+ studioQuartoPathsMatch(quartoPreviewState.context.sourcePath, getCurrentStudioQuartoSourcePath())
19570
+ || studioQuartoPathsMatch(quartoPreviewState.context.requestedSourcePath, getCurrentStudioQuartoSourcePath())
19571
+ )) {
19572
+ quartoPreviewContext = quartoPreviewState.context;
19573
+ }
19574
+ }
19053
19575
  if (typeof message.terminalSessionLabel === "string") {
19054
19576
  terminalSessionLabel = message.terminalSessionLabel;
19055
19577
  }
@@ -19130,6 +19652,11 @@
19130
19652
  handleIncomingResponse(lastMarkdown, lastResponseKind, message.lastResponse.timestamp, message.lastResponse.thinking);
19131
19653
  }
19132
19654
 
19655
+ if (rightView === "editor-quarto-preview") {
19656
+ requestStudioQuartoPreviewCheck(false);
19657
+ renderQuartoPreviewView();
19658
+ }
19659
+
19133
19660
  if (pendingRequestId) {
19134
19661
  if (busy) {
19135
19662
  setStatus(getStudioBusyStatus(pendingKind), "warning");
@@ -19840,6 +20367,10 @@
19840
20367
  if (ws === socket) {
19841
20368
  ws = null;
19842
20369
  }
20370
+ quartoPreviewCheckRequestId = null;
20371
+ quartoPreviewCheckSourcePath = "";
20372
+ quartoPreviewActionRequestId = null;
20373
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
19843
20374
  setBusy(true);
19844
20375
 
19845
20376
  if (kind === "invalidated") {
@@ -21295,6 +21826,7 @@
21295
21826
  resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
21296
21827
  }
21297
21828
  setSourceState(initialSourceState);
21829
+ if (initialSourceState.path) markFileBackedBaseline(sourceTextEl.value);
21298
21830
  refreshResponseUi();
21299
21831
  updateAnnotatedReplyHeaderButton();
21300
21832
  setActivePane("left");