pi-studio 0.9.37 → 0.9.39

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) {
@@ -2009,6 +2019,22 @@
2009
2019
  path: initialSourceState.path,
2010
2020
  draftId: initialSourceState.draftId,
2011
2021
  };
2022
+ let quartoPreviewContext = null;
2023
+ let quartoPreviewCheckRequestId = null;
2024
+ let quartoPreviewCheckSourcePath = "";
2025
+ let quartoPreviewActionRequestId = null;
2026
+ let quartoPreviewLogVisible = false;
2027
+ let quartoPreviewState = {
2028
+ status: "idle",
2029
+ sourcePath: "",
2030
+ url: "",
2031
+ log: "",
2032
+ error: null,
2033
+ context: null,
2034
+ startedAt: null,
2035
+ updatedAt: 0,
2036
+ actionRequestId: null,
2037
+ };
2012
2038
  let fileBackedBaselineText = null;
2013
2039
  let activePane = "left";
2014
2040
  let paneFocusTarget = "off";
@@ -3908,7 +3934,7 @@
3908
3934
  function getSelectEnabledValues(selectEl) {
3909
3935
  if (!selectEl || !selectEl.options) return [];
3910
3936
  return Array.from(selectEl.options)
3911
- .filter((option) => option && !option.disabled)
3937
+ .filter((option) => option && !option.disabled && !option.hidden)
3912
3938
  .map((option) => option.value)
3913
3939
  .filter((value) => typeof value === "string" && value);
3914
3940
  }
@@ -4743,6 +4769,22 @@
4743
4769
  return;
4744
4770
  }
4745
4771
 
4772
+ if (rightView === "editor-quarto-preview") {
4773
+ const context = getCurrentStudioQuartoContext();
4774
+ const current = getStudioQuartoProcessStatusForCurrentSource();
4775
+ const label = context && context.projectLabel ? context.projectLabel : (sourceState.label || "Quarto document");
4776
+ const statusLabel = quartoPreviewCheckRequestId
4777
+ ? "checking"
4778
+ : (context && !context.available
4779
+ ? "unavailable"
4780
+ : (current.sameSource
4781
+ ? (current.status === "running" ? "running" : (current.status === "starting" ? "starting" : (current.status === "error" ? "error" : "stopped")))
4782
+ : ((quartoPreviewState.status === "running" || quartoPreviewState.status === "starting") ? "another preview running" : "ready to start")));
4783
+ referenceBadgeEl.textContent = "Quarto: " + label + " · " + statusLabel + " · saved-file preview"
4784
+ + (isStudioQuartoEditorDirty() ? " · unsaved changes excluded" : "");
4785
+ return;
4786
+ }
4787
+
4746
4788
  if (rightView === "editor-preview") {
4747
4789
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
4748
4790
  if (hasResponse) {
@@ -8182,6 +8224,7 @@
8182
8224
  critiqueViewEl.addEventListener("click", handleReplPaneClick);
8183
8225
  critiqueViewEl.addEventListener("click", handleFilesPaneClick);
8184
8226
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
8227
+ critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
8185
8228
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
8186
8229
  critiqueViewEl.addEventListener("change", (event) => {
8187
8230
  void handleFilesPaneChange(event);
@@ -8207,7 +8250,7 @@
8207
8250
 
8208
8251
  function applyPendingResponseScrollReset() {
8209
8252
  if (!pendingResponseScrollReset || !critiqueViewEl) return false;
8210
- if (rightView === "editor-preview") return false;
8253
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return false;
8211
8254
 
8212
8255
  pendingResponseScrollReset = false;
8213
8256
  let targetEl = replaceResponsePaneWithClone();
@@ -8216,7 +8259,7 @@
8216
8259
  : (cb) => window.setTimeout(cb, 16);
8217
8260
  const resetScroll = () => {
8218
8261
  if (!targetEl || !targetEl.isConnected) return;
8219
- if (rightView === "editor-preview") return;
8262
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return;
8220
8263
  targetEl.scrollTop = 0;
8221
8264
  targetEl.scrollLeft = 0;
8222
8265
  };
@@ -10548,8 +10591,289 @@
10548
10591
  }
10549
10592
  }
10550
10593
 
10594
+ function getStudioQuartoProcessStatusForCurrentSource() {
10595
+ const sourcePath = getCurrentStudioQuartoSourcePath();
10596
+ return {
10597
+ sameSource: studioQuartoPathsMatch(quartoPreviewState.sourcePath, sourcePath)
10598
+ || Boolean(quartoPreviewState.context && studioQuartoPathsMatch(quartoPreviewState.context.requestedSourcePath, sourcePath)),
10599
+ sourcePath,
10600
+ status: quartoPreviewState.status,
10601
+ };
10602
+ }
10603
+
10604
+ function isStudioQuartoEditorDirty() {
10605
+ return Boolean(isCurrentStudioQuartoDocument() && editorDiffersFromFileBackedBaseline());
10606
+ }
10607
+
10608
+ function requestStudioQuartoPreviewCheck(force) {
10609
+ const sourcePath = getCurrentStudioQuartoSourcePath();
10610
+ if (!isCurrentStudioQuartoDocument()) return false;
10611
+ if (!ws || ws.readyState !== WebSocket.OPEN) return false;
10612
+ if (!force && getCurrentStudioQuartoContext()) return true;
10613
+ if (!force && quartoPreviewCheckRequestId && studioQuartoPathsMatch(quartoPreviewCheckSourcePath, sourcePath)) return true;
10614
+ const requestId = makeRequestId();
10615
+ quartoPreviewCheckRequestId = requestId;
10616
+ quartoPreviewCheckSourcePath = sourcePath;
10617
+ quartoPreviewContext = null;
10618
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
10619
+ const sent = sendMessage({ type: "quarto_preview_check_request", requestId, sourcePath });
10620
+ if (!sent) {
10621
+ quartoPreviewCheckRequestId = null;
10622
+ quartoPreviewCheckSourcePath = "";
10623
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
10624
+ }
10625
+ return sent;
10626
+ }
10627
+
10628
+ function requestStudioQuartoPreviewStart() {
10629
+ const sourcePath = getCurrentStudioQuartoSourcePath();
10630
+ if (!isCurrentStudioQuartoDocument()) {
10631
+ setStatus("Quarto preview requires a file-backed .qmd, .md, or .markdown document.", "warning");
10632
+ return false;
10633
+ }
10634
+ if (quartoPreviewActionRequestId) return false;
10635
+ const requestId = makeRequestId();
10636
+ quartoPreviewActionRequestId = requestId;
10637
+ const sent = sendMessage({ type: "quarto_preview_start_request", requestId, sourcePath });
10638
+ if (!sent) quartoPreviewActionRequestId = null;
10639
+ renderQuartoPreviewView();
10640
+ if (sent) setStatus("Starting Quarto preview with computational cell execution disabled…", "warning");
10641
+ return sent;
10642
+ }
10643
+
10644
+ function requestStudioQuartoPreviewStop() {
10645
+ if (quartoPreviewActionRequestId) return false;
10646
+ const requestId = makeRequestId();
10647
+ quartoPreviewActionRequestId = requestId;
10648
+ const sent = sendMessage({ type: "quarto_preview_stop_request", requestId });
10649
+ if (!sent) quartoPreviewActionRequestId = null;
10650
+ renderQuartoPreviewView();
10651
+ if (sent) setStatus("Stopping Quarto preview…", "warning");
10652
+ return sent;
10653
+ }
10654
+
10655
+ function openStudioQuartoPreviewInBrowser() {
10656
+ const rawUrl = String(quartoPreviewState.url || "").trim();
10657
+ if (!rawUrl) {
10658
+ setStatus("Quarto preview is not running yet.", "warning");
10659
+ return false;
10660
+ }
10661
+ try {
10662
+ const parsed = new URL(rawUrl);
10663
+ const hostname = parsed.hostname.toLowerCase();
10664
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("Unsupported preview protocol.");
10665
+ if (hostname !== "127.0.0.1" && hostname !== "localhost" && hostname !== "::1") throw new Error("Preview URL is not loopback-only.");
10666
+ const opened = window.open(parsed.href, "_blank");
10667
+ if (!opened) {
10668
+ setStatus("Browser blocked the Quarto preview tab. Allow pop-ups and try again.", "warning");
10669
+ return false;
10670
+ }
10671
+ try { opened.opener = null; } catch {}
10672
+ setStatus("Opening Quarto preview in a browser tab.", "success");
10673
+ return true;
10674
+ } catch (error) {
10675
+ setStatus("Could not open Quarto preview: " + ((error && error.message) ? error.message : String(error)), "error");
10676
+ return false;
10677
+ }
10678
+ }
10679
+
10680
+ function openStudioQuartoInstallationInstructions() {
10681
+ try {
10682
+ const opened = window.open("https://quarto.org/docs/get-started/", "_blank");
10683
+ if (!opened) {
10684
+ setStatus("Browser blocked the Quarto installation page.", "warning");
10685
+ return;
10686
+ }
10687
+ try { opened.opener = null; } catch {}
10688
+ } catch (error) {
10689
+ setStatus("Could not open Quarto installation instructions.", "warning");
10690
+ }
10691
+ }
10692
+
10693
+ function buildStudioQuartoLogDetails(log, open) {
10694
+ const text = String(log || "").trim();
10695
+ if (!text) return "";
10696
+ return "<details class='quarto-preview-log-details'" + (open ? " open" : "") + ">"
10697
+ + "<summary>Quarto log</summary>"
10698
+ + "<pre>" + escapeHtml(text) + "</pre>"
10699
+ + "</details>";
10700
+ }
10701
+
10702
+ function buildStudioQuartoContextSummary(context) {
10703
+ if (!context) return "Quarto document";
10704
+ const kind = context.isProject
10705
+ ? (context.projectType ? (context.projectType.charAt(0).toUpperCase() + context.projectType.slice(1) + " project") : "Quarto project")
10706
+ : "Standalone document";
10707
+ return kind + (context.version ? " · Quarto " + context.version : "");
10708
+ }
10709
+
10710
+ function updateStudioQuartoRunningShell() {
10711
+ const context = getCurrentStudioQuartoContext() || quartoPreviewState.context;
10712
+ const url = String(quartoPreviewState.url || "");
10713
+ let shell = critiqueViewEl ? critiqueViewEl.querySelector(".quarto-preview-shell[data-preview-url]") : null;
10714
+ if (!shell || shell.getAttribute("data-preview-url") !== url) {
10715
+ const label = context && context.projectLabel ? context.projectLabel : (sourceState.label || "Quarto document");
10716
+ critiqueViewEl.innerHTML = ""
10717
+ + "<div class='quarto-preview-shell' data-preview-url='" + escapeHtml(url) + "'>"
10718
+ + "<div class='quarto-preview-toolbar'>"
10719
+ + "<div class='quarto-preview-title-group'>"
10720
+ + "<span class='quarto-preview-title'>" + escapeHtml(label) + "</span>"
10721
+ + "<span class='quarto-preview-subtitle'>" + escapeHtml(buildStudioQuartoContextSummary(context)) + "</span>"
10722
+ + "</div>"
10723
+ + "<div class='quarto-preview-actions'>"
10724
+ + "<button type='button' data-quarto-action='open'>Open in browser ↗</button>"
10725
+ + "<button type='button' data-quarto-action='restart'>Restart</button>"
10726
+ + "<button type='button' data-quarto-action='toggle-log'>Show log</button>"
10727
+ + "<button type='button' data-quarto-action='stop'>Stop</button>"
10728
+ + "</div>"
10729
+ + "</div>"
10730
+ + "<div class='quarto-preview-dirty-warning' hidden>Unsaved editor changes are not included. Save the editor to update Quarto.</div>"
10731
+ + "<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>"
10732
+ + "<div class='quarto-preview-live-log' hidden><pre></pre></div>"
10733
+ + "</div>";
10734
+ shell = critiqueViewEl.querySelector(".quarto-preview-shell[data-preview-url]");
10735
+ }
10736
+ const warningEl = shell ? shell.querySelector(".quarto-preview-dirty-warning") : null;
10737
+ if (warningEl) warningEl.hidden = !isStudioQuartoEditorDirty();
10738
+ const logPanelEl = shell ? shell.querySelector(".quarto-preview-live-log") : null;
10739
+ const logPreEl = logPanelEl ? logPanelEl.querySelector("pre") : null;
10740
+ if (logPanelEl) logPanelEl.hidden = !quartoPreviewLogVisible;
10741
+ if (logPreEl && logPreEl.textContent !== quartoPreviewState.log) logPreEl.textContent = quartoPreviewState.log;
10742
+ const toggleLogBtn = shell ? shell.querySelector("[data-quarto-action='toggle-log']") : null;
10743
+ if (toggleLogBtn) toggleLogBtn.textContent = quartoPreviewLogVisible ? "Hide log" : "Show log";
10744
+ if (shell) {
10745
+ shell.querySelectorAll("button[data-quarto-action]").forEach((button) => {
10746
+ if (button.getAttribute("data-quarto-action") === "open" || button.getAttribute("data-quarto-action") === "toggle-log") return;
10747
+ button.disabled = Boolean(quartoPreviewActionRequestId);
10748
+ });
10749
+ }
10750
+ }
10751
+
10752
+ function renderQuartoPreviewView() {
10753
+ if (!critiqueViewEl) return;
10754
+ finishPreviewRender(critiqueViewEl);
10755
+ if (!isCurrentStudioQuartoDocument()) {
10756
+ 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>";
10757
+ return;
10758
+ }
10759
+
10760
+ const current = getStudioQuartoProcessStatusForCurrentSource();
10761
+ const context = getCurrentStudioQuartoContext();
10762
+ const checking = Boolean(quartoPreviewCheckRequestId && studioQuartoPathsMatch(quartoPreviewCheckSourcePath, current.sourcePath));
10763
+ const actionPending = Boolean(quartoPreviewActionRequestId);
10764
+ if (current.sameSource && current.status === "running" && quartoPreviewState.url) {
10765
+ updateStudioQuartoRunningShell();
10766
+ return;
10767
+ }
10768
+
10769
+ if (checking && !context) {
10770
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10771
+ + "<div class='quarto-preview-spinner' aria-hidden='true'></div>"
10772
+ + "<h2>Checking Quarto…</h2>"
10773
+ + "<p>Inspecting the saved document and its project configuration.</p>"
10774
+ + "</div>";
10775
+ return;
10776
+ }
10777
+
10778
+ if (!context) {
10779
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10780
+ + "<h2>Editor (Quarto Preview)</h2>"
10781
+ + "<p>Check the installed Quarto version and this saved document before starting a preview.</p>"
10782
+ + "<div class='quarto-preview-card-actions'><button type='button' data-quarto-action='check'>Check Quarto</button></div>"
10783
+ + "</div>";
10784
+ return;
10785
+ }
10786
+
10787
+ if (!context.available) {
10788
+ const missing = context.reason === "not-found";
10789
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10790
+ + "<h2>" + (missing ? "Quarto not found" : "Quarto preview unavailable") + "</h2>"
10791
+ + "<p>" + escapeHtml(context.error || "Studio could not inspect this Quarto document.") + "</p>"
10792
+ + (missing ? "<p class='quarto-preview-note'>Studio could not find <code>quarto</code> on its PATH.</p>" : "")
10793
+ + "<div class='quarto-preview-card-actions'>"
10794
+ + "<button type='button' data-quarto-action='check'>Check again</button>"
10795
+ + (missing ? "<button type='button' data-quarto-action='install'>Installation instructions ↗</button>" : "")
10796
+ + "</div>"
10797
+ + buildStudioQuartoLogDetails(context.inspectLog, true)
10798
+ + "</div>";
10799
+ return;
10800
+ }
10801
+
10802
+ const runningAnother = !current.sameSource && (current.status === "running" || current.status === "starting" || current.status === "stopping");
10803
+ const dirtyWarning = isStudioQuartoEditorDirty()
10804
+ ? "<div class='quarto-preview-dirty-warning'>Unsaved editor changes are not included. Save before starting or refreshing Quarto.</div>"
10805
+ : "";
10806
+ if (current.sameSource && (current.status === "starting" || current.status === "stopping")) {
10807
+ const stopping = current.status === "stopping";
10808
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10809
+ + "<div class='quarto-preview-spinner' aria-hidden='true'></div>"
10810
+ + "<h2>" + (stopping ? "Stopping Quarto preview…" : "Starting Quarto preview…") + "</h2>"
10811
+ + "<p>" + escapeHtml(buildStudioQuartoContextSummary(context)) + " · computational cells will not be executed.</p>"
10812
+ + dirtyWarning
10813
+ + (!stopping ? "<div class='quarto-preview-card-actions'><button type='button' data-quarto-action='stop'" + (actionPending ? " disabled" : "") + ">Stop</button></div>" : "")
10814
+ + buildStudioQuartoLogDetails(quartoPreviewState.log, true)
10815
+ + "</div>";
10816
+ return;
10817
+ }
10818
+
10819
+ const sameSourceError = current.sameSource && current.status === "error";
10820
+ const label = context.projectLabel || sourceState.label || "Quarto document";
10821
+ critiqueViewEl.innerHTML = "<div class='quarto-preview-card quarto-preview-card-centered'>"
10822
+ + "<h2>" + (sameSourceError ? "Quarto preview stopped with an error" : "Ready to preview with Quarto") + "</h2>"
10823
+ + "<p><strong>" + escapeHtml(label) + "</strong><br>" + escapeHtml(buildStudioQuartoContextSummary(context)) + "</p>"
10824
+ + (sameSourceError ? "<div class='quarto-preview-error'>" + escapeHtml(quartoPreviewState.error || "Quarto preview failed.") + "</div>" : "")
10825
+ + (runningAnother ? "<div class='quarto-preview-note'>Another Quarto document is currently being previewed. Starting this one will replace it.</div>" : "")
10826
+ + dirtyWarning
10827
+ + "<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>"
10828
+ + "<div class='quarto-preview-card-actions'>"
10829
+ + "<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>"
10830
+ + (runningAnother && quartoPreviewState.url ? "<button type='button' data-quarto-action='open'>Open running preview ↗</button>" : "")
10831
+ + (runningAnother ? "<button type='button' data-quarto-action='stop'" + (actionPending ? " disabled" : "") + ">Stop running preview</button>" : "")
10832
+ + "</div>"
10833
+ + buildStudioQuartoLogDetails(sameSourceError ? quartoPreviewState.log : context.inspectLog, sameSourceError)
10834
+ + "</div>";
10835
+ }
10836
+
10837
+ function syncStudioQuartoDirtyUi() {
10838
+ if (rightView !== "editor-quarto-preview") return;
10839
+ const warningEl = critiqueViewEl ? critiqueViewEl.querySelector(".quarto-preview-dirty-warning") : null;
10840
+ if (warningEl && warningEl.classList && warningEl.closest(".quarto-preview-shell")) {
10841
+ warningEl.hidden = !isStudioQuartoEditorDirty();
10842
+ }
10843
+ updateReferenceBadge();
10844
+ }
10845
+
10846
+ function handleStudioQuartoPreviewClick(event) {
10847
+ if (rightView !== "editor-quarto-preview") return;
10848
+ const target = event && event.target instanceof Element ? event.target.closest("[data-quarto-action]") : null;
10849
+ if (!target || !critiqueViewEl.contains(target)) return;
10850
+ event.preventDefault();
10851
+ const action = target.getAttribute("data-quarto-action");
10852
+ if (action === "check") {
10853
+ requestStudioQuartoPreviewCheck(true);
10854
+ } else if (action === "start" || action === "restart") {
10855
+ requestStudioQuartoPreviewStart();
10856
+ } else if (action === "stop") {
10857
+ requestStudioQuartoPreviewStop();
10858
+ } else if (action === "open") {
10859
+ openStudioQuartoPreviewInBrowser();
10860
+ } else if (action === "install") {
10861
+ openStudioQuartoInstallationInstructions();
10862
+ } else if (action === "toggle-log") {
10863
+ quartoPreviewLogVisible = !quartoPreviewLogVisible;
10864
+ updateStudioQuartoRunningShell();
10865
+ }
10866
+ }
10867
+
10551
10868
  function renderActiveResult() {
10552
- if (critiqueViewEl) critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
10869
+ if (critiqueViewEl) {
10870
+ critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
10871
+ critiqueViewEl.classList.toggle("quarto-preview-host", rightView === "editor-quarto-preview");
10872
+ }
10873
+ if (rightView === "editor-quarto-preview") {
10874
+ renderQuartoPreviewView();
10875
+ return;
10876
+ }
10553
10877
  if (rightView === "trace") {
10554
10878
  renderTraceView();
10555
10879
  return;
@@ -10648,7 +10972,7 @@
10648
10972
  : normalizeForCompare(sourceTextEl.value);
10649
10973
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
10650
10974
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
10651
- const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes";
10975
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes" || rightView === "editor-quarto-preview";
10652
10976
 
10653
10977
  if (responseWrapEl) {
10654
10978
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -10732,6 +11056,7 @@
10732
11056
  : (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
11057
  }
10734
11058
  if (exportPreviewControlsEl) {
11059
+ exportPreviewControlsEl.hidden = rightView === "editor-quarto-preview";
10735
11060
  exportPreviewControlsEl.title = canExportPreview
10736
11061
  ? (exportingReplJournal
10737
11062
  ? "Choose a format and export destination for the Studio REPL record."
@@ -10746,6 +11071,7 @@
10746
11071
  pullLatestBtn.textContent = queuedLatestResponse ? "Fetch latest response *" : "Fetch latest response";
10747
11072
 
10748
11073
  updateSyncBadge(normalizedEditor);
11074
+ syncStudioQuartoDirtyUi();
10749
11075
  }
10750
11076
 
10751
11077
  function refreshResponseUi() {
@@ -10802,6 +11128,86 @@
10802
11128
  return null;
10803
11129
  }
10804
11130
 
11131
+ function normalizeStudioQuartoClientPath(value) {
11132
+ const normalized = String(value || "").trim().replace(/\\/g, "/").replace(/\/+$/, "");
11133
+ return /^[A-Za-z]:\//.test(normalized) ? normalized.toLowerCase() : normalized;
11134
+ }
11135
+
11136
+ function getCurrentStudioQuartoSourcePath() {
11137
+ return sourceState && typeof sourceState.path === "string" ? sourceState.path.trim() : "";
11138
+ }
11139
+
11140
+ function isCurrentStudioQuartoDocument() {
11141
+ return /\.(?:qmd|md|markdown)$/i.test(getCurrentStudioQuartoSourcePath());
11142
+ }
11143
+
11144
+ function studioQuartoPathsMatch(left, right) {
11145
+ const a = normalizeStudioQuartoClientPath(left);
11146
+ const b = normalizeStudioQuartoClientPath(right);
11147
+ return Boolean(a && b && a === b);
11148
+ }
11149
+
11150
+ function normalizeStudioQuartoContext(value) {
11151
+ if (!value || typeof value !== "object") return null;
11152
+ return {
11153
+ sourcePath: typeof value.sourcePath === "string" ? value.sourcePath : "",
11154
+ requestedSourcePath: typeof value.requestedSourcePath === "string" ? value.requestedSourcePath : (typeof value.sourcePath === "string" ? value.sourcePath : ""),
11155
+ available: value.available === true,
11156
+ reason: typeof value.reason === "string" ? value.reason : "inspect-error",
11157
+ version: typeof value.version === "string" ? value.version : "",
11158
+ projectRoot: typeof value.projectRoot === "string" ? value.projectRoot : "",
11159
+ projectType: typeof value.projectType === "string" ? value.projectType : "",
11160
+ projectLabel: typeof value.projectLabel === "string" ? value.projectLabel : "Quarto document",
11161
+ outputFile: typeof value.outputFile === "string" ? value.outputFile : "",
11162
+ isProject: value.isProject === true,
11163
+ error: typeof value.error === "string" && value.error ? value.error : null,
11164
+ inspectLog: typeof value.inspectLog === "string" ? value.inspectLog : "",
11165
+ };
11166
+ }
11167
+
11168
+ function normalizeStudioQuartoPreviewState(value) {
11169
+ if (!value || typeof value !== "object") return {
11170
+ status: "idle",
11171
+ sourcePath: "",
11172
+ url: "",
11173
+ log: "",
11174
+ error: null,
11175
+ context: null,
11176
+ startedAt: null,
11177
+ updatedAt: 0,
11178
+ actionRequestId: null,
11179
+ };
11180
+ const allowedStatuses = new Set(["idle", "starting", "running", "stopping", "stopped", "error"]);
11181
+ return {
11182
+ status: allowedStatuses.has(value.status) ? value.status : "idle",
11183
+ sourcePath: typeof value.sourcePath === "string" ? value.sourcePath : "",
11184
+ url: typeof value.url === "string" ? value.url : "",
11185
+ log: typeof value.log === "string" ? value.log : "",
11186
+ error: typeof value.error === "string" && value.error ? value.error : null,
11187
+ context: normalizeStudioQuartoContext(value.context),
11188
+ startedAt: typeof value.startedAt === "number" && Number.isFinite(value.startedAt) ? value.startedAt : null,
11189
+ updatedAt: typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) ? value.updatedAt : 0,
11190
+ actionRequestId: typeof value.actionRequestId === "string" ? value.actionRequestId : null,
11191
+ };
11192
+ }
11193
+
11194
+ function getCurrentStudioQuartoContext() {
11195
+ const sourcePath = getCurrentStudioQuartoSourcePath();
11196
+ if (quartoPreviewContext && (
11197
+ studioQuartoPathsMatch(quartoPreviewContext.sourcePath, sourcePath)
11198
+ || studioQuartoPathsMatch(quartoPreviewContext.requestedSourcePath, sourcePath)
11199
+ )) {
11200
+ return quartoPreviewContext;
11201
+ }
11202
+ if (quartoPreviewState.context && (
11203
+ studioQuartoPathsMatch(quartoPreviewState.context.sourcePath, sourcePath)
11204
+ || studioQuartoPathsMatch(quartoPreviewState.context.requestedSourcePath, sourcePath)
11205
+ )) {
11206
+ return quartoPreviewState.context;
11207
+ }
11208
+ return null;
11209
+ }
11210
+
10805
11211
  function getHtmlPreviewResourceContextOptions() {
10806
11212
  const sourcePath = getEffectiveSavePath() || sourceState.path || "";
10807
11213
  const resourceDir = getCurrentResourceDirValue();
@@ -10917,6 +11323,7 @@
10917
11323
 
10918
11324
  function setSourceState(next, options) {
10919
11325
  const previousDescriptor = getCurrentStudioDocumentDescriptor();
11326
+ const previousQuartoPath = getCurrentStudioQuartoSourcePath();
10920
11327
  const nextPath = next && next.path ? next.path : null;
10921
11328
  sourceState = {
10922
11329
  source: next && next.source ? next.source : "blank",
@@ -10926,9 +11333,24 @@
10926
11333
  ? null
10927
11334
  : (next && next.draftId ? next.draftId : makeStudioDraftId()),
10928
11335
  };
11336
+ const nextQuartoPath = getCurrentStudioQuartoSourcePath();
11337
+ const quartoSourceChanged = !studioQuartoPathsMatch(previousQuartoPath, nextQuartoPath);
11338
+ if (quartoSourceChanged) {
11339
+ quartoPreviewContext = null;
11340
+ quartoPreviewCheckRequestId = null;
11341
+ quartoPreviewCheckSourcePath = "";
11342
+ quartoPreviewActionRequestId = null;
11343
+ quartoPreviewLogVisible = false;
11344
+ }
10929
11345
  if (!sourceState.path) {
10930
11346
  clearFileBackedBaseline();
10931
11347
  }
11348
+ syncRightViewModeOptions();
11349
+ const leavingUnavailableQuartoView = rightView === "editor-quarto-preview" && !isCurrentStudioQuartoDocument();
11350
+ if (leavingUnavailableQuartoView) {
11351
+ rightView = "editor-preview";
11352
+ rightViewSelect.value = rightView;
11353
+ }
10932
11354
  updateStudioDocumentUrlState(sourceState);
10933
11355
  updateSourceBadge();
10934
11356
  syncActionButtons();
@@ -10942,6 +11364,9 @@
10942
11364
  previousDescriptor: previousDescriptor,
10943
11365
  carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
10944
11366
  });
11367
+ const refreshChangedQuartoView = rightView === "editor-quarto-preview" && quartoSourceChanged && isCurrentStudioQuartoDocument();
11368
+ if (refreshChangedQuartoView) requestStudioQuartoPreviewCheck(false);
11369
+ if (leavingUnavailableQuartoView || refreshChangedQuartoView) refreshResponseUi();
10945
11370
  scheduleWorkspacePersistence();
10946
11371
  }
10947
11372
 
@@ -11759,6 +12184,9 @@
11759
12184
  if (rightView !== "editor-preview") {
11760
12185
  clearPreviewJumpHighlight(critiqueViewEl);
11761
12186
  }
12187
+ if (rightView === "editor-quarto-preview") {
12188
+ requestStudioQuartoPreviewCheck(false);
12189
+ }
11762
12190
 
11763
12191
  refreshResponseUi();
11764
12192
  syncActionButtons();
@@ -19038,6 +19466,80 @@
19038
19466
  return;
19039
19467
  }
19040
19468
 
19469
+ if (message.type === "quarto_preview_context") {
19470
+ const requestId = typeof message.requestId === "string" ? message.requestId : "";
19471
+ const context = normalizeStudioQuartoContext(message.context);
19472
+ const matchesCheck = Boolean(requestId && quartoPreviewCheckRequestId === requestId);
19473
+ const matchesAction = Boolean(requestId && quartoPreviewActionRequestId === requestId);
19474
+ if (!matchesCheck && !matchesAction) return;
19475
+ if (matchesCheck) {
19476
+ quartoPreviewCheckRequestId = null;
19477
+ quartoPreviewCheckSourcePath = "";
19478
+ }
19479
+ if (context) quartoPreviewContext = context;
19480
+ if (matchesAction && (!context || !context.available)) {
19481
+ quartoPreviewActionRequestId = null;
19482
+ }
19483
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
19484
+ updateReferenceBadge();
19485
+ return;
19486
+ }
19487
+
19488
+ if (message.type === "quarto_preview_state") {
19489
+ const previousQuartoStatus = quartoPreviewState.status;
19490
+ quartoPreviewState = normalizeStudioQuartoPreviewState(message.preview);
19491
+ if (quartoPreviewState.actionRequestId && quartoPreviewActionRequestId === quartoPreviewState.actionRequestId) {
19492
+ quartoPreviewActionRequestId = null;
19493
+ }
19494
+ if (quartoPreviewState.context && (
19495
+ studioQuartoPathsMatch(quartoPreviewState.context.sourcePath, getCurrentStudioQuartoSourcePath())
19496
+ || studioQuartoPathsMatch(quartoPreviewState.context.requestedSourcePath, getCurrentStudioQuartoSourcePath())
19497
+ )) {
19498
+ quartoPreviewContext = quartoPreviewState.context;
19499
+ }
19500
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
19501
+ updateReferenceBadge();
19502
+ syncActionButtons();
19503
+ if (quartoPreviewState.status !== previousQuartoStatus) {
19504
+ if (quartoPreviewState.status === "running") {
19505
+ setStatus("Quarto preview is running with computational cell execution disabled.", "success");
19506
+ } else if (quartoPreviewState.status === "error" && quartoPreviewState.error) {
19507
+ setStatus(quartoPreviewState.error, "error");
19508
+ } else if (quartoPreviewState.status === "stopped") {
19509
+ setStatus("Quarto preview stopped.");
19510
+ }
19511
+ }
19512
+ return;
19513
+ }
19514
+
19515
+ if (message.type === "quarto_preview_action_error") {
19516
+ const requestId = typeof message.requestId === "string" ? message.requestId : "";
19517
+ if (requestId && quartoPreviewCheckRequestId === requestId) {
19518
+ quartoPreviewCheckRequestId = null;
19519
+ quartoPreviewCheckSourcePath = "";
19520
+ }
19521
+ if (requestId && quartoPreviewActionRequestId === requestId) quartoPreviewActionRequestId = null;
19522
+ const messageText = typeof message.message === "string" ? message.message : "Quarto preview request failed.";
19523
+ quartoPreviewContext = {
19524
+ sourcePath: getCurrentStudioQuartoSourcePath(),
19525
+ requestedSourcePath: getCurrentStudioQuartoSourcePath(),
19526
+ available: false,
19527
+ reason: "inspect-error",
19528
+ version: "",
19529
+ projectRoot: "",
19530
+ projectType: "",
19531
+ projectLabel: sourceState.label || "Quarto document",
19532
+ outputFile: "",
19533
+ isProject: false,
19534
+ error: messageText,
19535
+ inspectLog: "",
19536
+ };
19537
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
19538
+ updateReferenceBadge();
19539
+ setStatus(messageText, "error");
19540
+ return;
19541
+ }
19542
+
19041
19543
  if (message.type === "hello_ack") {
19042
19544
  const busy = Boolean(message.busy);
19043
19545
  agentBusyFromServer = Boolean(message.agentBusy);
@@ -19050,6 +19552,15 @@
19050
19552
  }
19051
19553
  updatePiSessionModelState(message);
19052
19554
  updatePiThemeState(message);
19555
+ if (message.quartoPreview && typeof message.quartoPreview === "object") {
19556
+ quartoPreviewState = normalizeStudioQuartoPreviewState(message.quartoPreview);
19557
+ if (quartoPreviewState.context && (
19558
+ studioQuartoPathsMatch(quartoPreviewState.context.sourcePath, getCurrentStudioQuartoSourcePath())
19559
+ || studioQuartoPathsMatch(quartoPreviewState.context.requestedSourcePath, getCurrentStudioQuartoSourcePath())
19560
+ )) {
19561
+ quartoPreviewContext = quartoPreviewState.context;
19562
+ }
19563
+ }
19053
19564
  if (typeof message.terminalSessionLabel === "string") {
19054
19565
  terminalSessionLabel = message.terminalSessionLabel;
19055
19566
  }
@@ -19130,6 +19641,11 @@
19130
19641
  handleIncomingResponse(lastMarkdown, lastResponseKind, message.lastResponse.timestamp, message.lastResponse.thinking);
19131
19642
  }
19132
19643
 
19644
+ if (rightView === "editor-quarto-preview") {
19645
+ requestStudioQuartoPreviewCheck(false);
19646
+ renderQuartoPreviewView();
19647
+ }
19648
+
19133
19649
  if (pendingRequestId) {
19134
19650
  if (busy) {
19135
19651
  setStatus(getStudioBusyStatus(pendingKind), "warning");
@@ -19840,6 +20356,10 @@
19840
20356
  if (ws === socket) {
19841
20357
  ws = null;
19842
20358
  }
20359
+ quartoPreviewCheckRequestId = null;
20360
+ quartoPreviewCheckSourcePath = "";
20361
+ quartoPreviewActionRequestId = null;
20362
+ if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
19843
20363
  setBusy(true);
19844
20364
 
19845
20365
  if (kind === "invalidated") {
@@ -21295,6 +21815,7 @@
21295
21815
  resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
21296
21816
  }
21297
21817
  setSourceState(initialSourceState);
21818
+ if (initialSourceState.path) markFileBackedBaseline(sourceTextEl.value);
21298
21819
  refreshResponseUi();
21299
21820
  updateAnnotatedReplyHeaderButton();
21300
21821
  setActivePane("left");