pi-studio 0.9.40 → 0.9.42

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.
@@ -208,7 +208,12 @@
208
208
  Numpad7: "repl",
209
209
  };
210
210
 
211
+ const navigationHelpers = globalThis.PiStudioNavigationHelpers;
212
+ if (!navigationHelpers || typeof navigationHelpers.readPaneFocusTarget !== "function") {
213
+ throw new Error("Studio navigation helpers failed to load.");
214
+ }
211
215
  const initialQueryParams = new URLSearchParams(window.location.search || "");
216
+ const initialPaneFocusTarget = navigationHelpers.readPaneFocusTarget(window.location);
212
217
  const skipInitialWorkspaceRestore = initialQueryParams.get("skipWorkspaceRestore") === "1";
213
218
  const explicitDocumentIdentityFromUrl = initialQueryParams.has("docId")
214
219
  || initialQueryParams.has("docSource")
@@ -264,7 +269,8 @@
264
269
  let pendingRequestId = null;
265
270
  let pendingKind = null;
266
271
  let stickyStudioKind = null;
267
- const pendingCompanionWindows = new Map();
272
+ const pendingCompanionLaunches = new Map();
273
+ const activeStudioTabLaunches = new Set();
268
274
  let sourceOriginSummaryEl = null;
269
275
  let sourceResetOriginBtn = null;
270
276
  let sourceOpenCurrentFileTabBtn = null;
@@ -2046,8 +2052,8 @@
2046
2052
  actionRequestId: null,
2047
2053
  };
2048
2054
  let fileBackedBaselineText = null;
2049
- let activePane = "left";
2050
- let paneFocusTarget = "off";
2055
+ let activePane = initialPaneFocusTarget === "right" ? "right" : "left";
2056
+ let paneFocusTarget = initialPaneFocusTarget;
2051
2057
  let paneSplitPercent = 50;
2052
2058
  const PANE_SPLIT_STORAGE_KEY = "piStudio.paneSplitPercent";
2053
2059
  const PANE_SPLIT_MIN_PERCENT = 20;
@@ -2575,17 +2581,12 @@
2575
2581
  return;
2576
2582
  }
2577
2583
  closeStudioUiRefreshMenus();
2578
- const targetUrl = buildAuthedStudioUrl("/", {
2579
- mode: "editor-only",
2580
- docSource: "file",
2581
- docLabel: sourceState && sourceState.label ? sourceState.label : basenameForStudioPath(path),
2582
- docPath: path,
2583
- resourceDir: getCurrentResourceDirValue() || dirnameForDisplayPath(path),
2584
- skipWorkspaceRestore: "1",
2585
- });
2586
2584
  try {
2587
- window.open(targetUrl, "_blank", "noopener");
2588
- setStatus("Opening current file in a new editor tab.", "success");
2585
+ openFileBackedStudioEditorTab(path, {
2586
+ label: sourceState && sourceState.label ? sourceState.label : basenameForStudioPath(path),
2587
+ resourceDir: getCurrentResourceDirValue() || dirnameForDisplayPath(path),
2588
+ });
2589
+ setStatus("Opening current file in a new editor tab.");
2589
2590
  } catch (error) {
2590
2591
  setStatus((error && error.message) ? error.message : String(error || "Could not open file tab."), "warning");
2591
2592
  }
@@ -3864,6 +3865,14 @@
3864
3865
  updatePaneFocusButtons();
3865
3866
  }
3866
3867
 
3868
+ function persistPaneFocusUrlState() {
3869
+ try {
3870
+ navigationHelpers.replacePaneFocusUrlState(window, paneFocusTarget);
3871
+ } catch {
3872
+ // Ignore URL-state update failures; focus mode still works for this page lifetime.
3873
+ }
3874
+ }
3875
+
3867
3876
  function snapshotStudioScrollablePositions() {
3868
3877
  return [sourceTextEl, sourcePreviewEl, critiqueViewEl]
3869
3878
  .filter((el) => el && typeof el.scrollTop === "number" && typeof el.scrollLeft === "number")
@@ -3918,6 +3927,7 @@
3918
3927
  if (paneFocusTarget !== "off" && paneFocusTarget !== activePane) {
3919
3928
  paneFocusTarget = activePane;
3920
3929
  applyPaneFocusClasses();
3930
+ persistPaneFocusUrlState();
3921
3931
  }
3922
3932
  }
3923
3933
 
@@ -4058,6 +4068,7 @@
4058
4068
  setActivePane(pane);
4059
4069
  paneFocusTarget = pane;
4060
4070
  applyPaneFocusClasses();
4071
+ persistPaneFocusUrlState();
4061
4072
  setStatus("Focus mode: " + paneLabel(pane) + " pane. Toggle with F10 or Cmd/Ctrl+Esc.");
4062
4073
  }
4063
4074
 
@@ -4065,6 +4076,7 @@
4065
4076
  if (paneFocusTarget === activePane) {
4066
4077
  paneFocusTarget = "off";
4067
4078
  applyPaneFocusClasses();
4079
+ persistPaneFocusUrlState();
4068
4080
  setStatus("Focus mode off.");
4069
4081
  return;
4070
4082
  }
@@ -4076,6 +4088,7 @@
4076
4088
  if (paneFocusTarget === "off") return false;
4077
4089
  paneFocusTarget = "off";
4078
4090
  applyPaneFocusClasses();
4091
+ persistPaneFocusUrlState();
4079
4092
  setStatus("Focus mode off.");
4080
4093
  return true;
4081
4094
  }
@@ -5718,7 +5731,7 @@
5718
5731
  return source.replace(/<head\b[^>]*>/i, (match) => match + "\n" + headMarkup + "\n");
5719
5732
  }
5720
5733
  if (/<body\b[^>]*>/i.test(source)) {
5721
- return source.replace(/<body\b/i, "<head>\n" + headMarkup + "\n</head>\n<body");
5734
+ return source.replace(/<body\b/i, () => "<head>\n" + headMarkup + "\n</head>\n<body");
5722
5735
  }
5723
5736
  if (/<html\b[^>]*>/i.test(source)) {
5724
5737
  return source.replace(/<html\b[^>]*>/i, (match) => match + "\n<head>\n" + headMarkup + "\n</head>\n");
@@ -6055,8 +6068,7 @@
6055
6068
  return;
6056
6069
  }
6057
6070
  if (kind === "text" || kind === "office") {
6058
- const pendingWindow = window.open("", "_blank");
6059
- void openPreviewDocumentInNewEditor(context.href, pendingWindow, context).catch((error) => {
6071
+ void openPreviewDocumentInNewEditor(context.href, context).catch((error) => {
6060
6072
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
6061
6073
  });
6062
6074
  return;
@@ -8519,19 +8531,14 @@
8519
8531
  async function exportRightPanePdf(options) {
8520
8532
  const exportOptions = options && typeof options === "object" ? options : {};
8521
8533
  const openTarget = exportOptions.openTarget === "studio" ? "studio" : "default";
8522
- let studioPopup = null;
8523
- if (openTarget === "studio") {
8524
- studioPopup = openExportStudioPlaceholderWindow("PDF");
8525
- }
8534
+ let studioLaunch = null;
8526
8535
  if (uiBusy || previewExportInProgress) {
8527
- closeExportStudioWindow(studioPopup);
8528
8536
  setStatus("Studio is busy.", "warning");
8529
8537
  return;
8530
8538
  }
8531
8539
 
8532
8540
  const token = getToken();
8533
8541
  if (!token) {
8534
- closeExportStudioWindow(studioPopup);
8535
8542
  setStatus("Missing Studio token in URL. Re-run /studio.", "error");
8536
8543
  return;
8537
8544
  }
@@ -8539,20 +8546,17 @@
8539
8546
  const exportingReplJournal = rightView === "repl";
8540
8547
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
8541
8548
  if (!rightPaneShowsPreview && !exportingReplJournal) {
8542
- closeExportStudioWindow(studioPopup);
8543
8549
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
8544
8550
  return;
8545
8551
  }
8546
8552
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
8547
8553
  if (exportingReplJournal && !replJournalExportEntries.length) {
8548
- closeExportStudioWindow(studioPopup);
8549
8554
  setStatus("No Studio REPL record entries to export for this session yet.", "warning");
8550
8555
  return;
8551
8556
  }
8552
8557
 
8553
8558
  const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
8554
8559
  if (htmlArtifactSource) {
8555
- closeExportStudioWindow(studioPopup);
8556
8560
  setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
8557
8561
  return;
8558
8562
  }
@@ -8563,7 +8567,6 @@
8563
8567
  ? prepareEditorTextForPdfExport(sourceTextEl.value)
8564
8568
  : prepareEditorTextForPreview(latestResponseMarkdown));
8565
8569
  if (!markdown || !markdown.trim()) {
8566
- closeExportStudioWindow(studioPopup);
8567
8570
  setStatus("Nothing to export yet.", "warning");
8568
8571
  return;
8569
8572
  }
@@ -8584,6 +8587,14 @@
8584
8587
  filenameHint = stem + ".studio.pdf";
8585
8588
  }
8586
8589
 
8590
+ if (openTarget === "studio") {
8591
+ try {
8592
+ studioLaunch = openPendingStudioTab("export");
8593
+ } catch (error) {
8594
+ setStatus((error && error.message) ? error.message : "Could not prepare a Studio export tab.", "error");
8595
+ return;
8596
+ }
8597
+ }
8587
8598
  previewExportInProgress = true;
8588
8599
  updateResultActionButtons();
8589
8600
  setStatus(openTarget === "studio" ? "Exporting PDF for Studio…" : "Exporting PDF…", "warning");
@@ -8641,30 +8652,30 @@
8641
8652
  }
8642
8653
 
8643
8654
  if (openTarget === "studio") {
8644
- const targetUrl = typeof payload.relativeUrl === "string" && payload.relativeUrl
8645
- ? new URL(payload.relativeUrl, window.location.href).href
8646
- : (typeof payload.url === "string" ? payload.url : "");
8647
- const openedStudio = navigateExportStudioWindow(studioPopup, targetUrl);
8648
- if (!openedStudio) {
8649
- closeExportStudioWindow(studioPopup);
8655
+ const targetUrl = typeof payload.relativeUrl === "string" ? payload.relativeUrl : "";
8656
+ const openedStudio = Boolean(targetUrl);
8657
+ if (openedStudio) {
8658
+ navigatePendingStudioTab(studioLaunch, targetUrl);
8659
+ } else {
8660
+ failPendingStudioTab(studioLaunch, "Studio did not return a preview-tab URL for this PDF export.");
8650
8661
  const viewerUrl = getStudioPdfViewerUrlForExportPayload(payload);
8651
8662
  if (viewerUrl) openStudioPdfFocusViewer(viewerUrl, downloadName);
8652
8663
  }
8653
8664
  if (writeError) {
8654
8665
  setStatus(openedStudio
8655
- ? "Opened exported PDF in a Studio preview tab, but could not write project file: " + writeError
8656
- : "Exported PDF, but could not open a Studio preview tab and could not write project file: " + writeError,
8666
+ ? "Opening exported PDF in a Studio preview tab, but could not write project file: " + writeError
8667
+ : "Exported PDF, but Studio did not return a preview-tab URL and could not write the project file: " + writeError,
8657
8668
  "warning");
8658
8669
  } else if (exportWarning) {
8659
8670
  setStatus(openedStudio
8660
- ? "Opened exported PDF in a Studio preview tab with warning: " + exportWarning
8661
- : "Exported PDF, but could not open a Studio preview tab. Warning: " + exportWarning,
8671
+ ? "Opening exported PDF in a Studio preview tab with warning: " + exportWarning
8672
+ : "Exported PDF, but Studio did not return a preview-tab URL. Warning: " + exportWarning,
8662
8673
  "warning");
8663
8674
  } else {
8664
8675
  setStatus(openedStudio
8665
- ? "Opened exported PDF in a Studio preview tab: " + (exportPath || downloadName)
8666
- : "Exported PDF, but could not open a Studio preview tab" + (targetUrl ? ": " + targetUrl : "."),
8667
- openedStudio ? "success" : "warning");
8676
+ ? "Opening exported PDF in a Studio preview tab: " + (exportPath || downloadName)
8677
+ : "Exported PDF, but Studio did not return a preview-tab URL.",
8678
+ openedStudio ? undefined : "warning");
8668
8679
  }
8669
8680
  return;
8670
8681
  }
@@ -8704,6 +8715,9 @@
8704
8715
  return;
8705
8716
  }
8706
8717
 
8718
+ if (openTarget === "studio") {
8719
+ failPendingStudioTab(studioLaunch, "Studio returned a download instead of a PDF preview tab.");
8720
+ }
8707
8721
  const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
8708
8722
  const blob = await response.blob();
8709
8723
  const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
@@ -8730,7 +8744,7 @@
8730
8744
  setStatus("Exported PDF: " + downloadName, "success");
8731
8745
  }
8732
8746
  } catch (error) {
8733
- closeExportStudioWindow(studioPopup);
8747
+ failPendingStudioTab(studioLaunch, "Studio could not prepare this PDF export tab. Return to the originating Studio page for details.");
8734
8748
  const detail = error && error.message ? error.message : String(error || "unknown error");
8735
8749
  setStatus("PDF export failed: " + detail, "error");
8736
8750
  } finally {
@@ -8739,58 +8753,17 @@
8739
8753
  }
8740
8754
  }
8741
8755
 
8742
- function openExportStudioPlaceholderWindow(formatLabel) {
8743
- const label = String(formatLabel || "preview").trim() || "preview";
8744
- let popup = null;
8745
- try {
8746
- popup = window.open("", "_blank");
8747
- if (popup && popup.document && popup.document.body) {
8748
- popup.document.title = "Opening " + label + " in Studio…";
8749
- popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Exporting " + escapeHtml(label) + " and opening it in Studio…</p>";
8750
- }
8751
- } catch {
8752
- popup = null;
8753
- }
8754
- return popup;
8755
- }
8756
-
8757
- function navigateExportStudioWindow(popup, targetUrl) {
8758
- if (!targetUrl) return false;
8759
- if (popup && !popup.closed) {
8760
- try {
8761
- popup.opener = null;
8762
- popup.location.href = targetUrl;
8763
- return true;
8764
- } catch {}
8765
- }
8766
- try {
8767
- return Boolean(window.open(targetUrl, "_blank", "noopener"));
8768
- } catch {
8769
- return false;
8770
- }
8771
- }
8772
-
8773
- function closeExportStudioWindow(popup) {
8774
- if (!popup || popup.closed) return;
8775
- try { popup.close(); } catch {}
8776
- }
8777
-
8778
8756
  async function exportRightPaneHtml(options) {
8779
8757
  const exportOptions = options && typeof options === "object" ? options : {};
8780
8758
  const openTarget = exportOptions.openTarget === "studio" ? "studio" : "browser";
8781
- let studioPopup = null;
8782
- if (openTarget === "studio") {
8783
- studioPopup = openExportStudioPlaceholderWindow("HTML");
8784
- }
8759
+ let studioLaunch = null;
8785
8760
  if (uiBusy || previewExportInProgress) {
8786
- closeExportStudioWindow(studioPopup);
8787
8761
  setStatus("Studio is busy.", "warning");
8788
8762
  return;
8789
8763
  }
8790
8764
 
8791
8765
  const token = getToken();
8792
8766
  if (!token) {
8793
- closeExportStudioWindow(studioPopup);
8794
8767
  setStatus("Missing Studio token in URL. Re-run /studio.", "error");
8795
8768
  return;
8796
8769
  }
@@ -8798,13 +8771,11 @@
8798
8771
  const exportingReplJournal = rightView === "repl";
8799
8772
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
8800
8773
  if (!rightPaneShowsPreview && !exportingReplJournal) {
8801
- closeExportStudioWindow(studioPopup);
8802
8774
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
8803
8775
  return;
8804
8776
  }
8805
8777
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
8806
8778
  if (exportingReplJournal && !replJournalExportEntries.length) {
8807
- closeExportStudioWindow(studioPopup);
8808
8779
  setStatus("No Studio REPL record entries to export for this session yet.", "warning");
8809
8780
  return;
8810
8781
  }
@@ -8814,7 +8785,6 @@
8814
8785
  ? prepareEditorTextForHtmlExport(sourceTextEl.value)
8815
8786
  : prepareEditorTextForPreview(latestResponseMarkdown)));
8816
8787
  if (!markdown || !markdown.trim()) {
8817
- closeExportStudioWindow(studioPopup);
8818
8788
  setStatus("Nothing to export yet.", "warning");
8819
8789
  return;
8820
8790
  }
@@ -8837,6 +8807,14 @@
8837
8807
  titleHint = stem + " preview";
8838
8808
  }
8839
8809
 
8810
+ if (openTarget === "studio") {
8811
+ try {
8812
+ studioLaunch = openPendingStudioTab("export");
8813
+ } catch (error) {
8814
+ setStatus((error && error.message) ? error.message : "Could not prepare a Studio export tab.", "error");
8815
+ return;
8816
+ }
8817
+ }
8840
8818
  previewExportInProgress = true;
8841
8819
  updateResultActionButtons();
8842
8820
  setStatus(openTarget === "studio" ? "Exporting HTML for Studio…" : "Exporting HTML…", "warning");
@@ -8895,26 +8873,28 @@
8895
8873
  }
8896
8874
 
8897
8875
  if (openTarget === "studio") {
8898
- const targetUrl = typeof payload.relativeUrl === "string" && payload.relativeUrl
8899
- ? new URL(payload.relativeUrl, window.location.href).href
8900
- : (typeof payload.url === "string" ? payload.url : "");
8901
- const openedStudio = navigateExportStudioWindow(studioPopup, targetUrl);
8902
- if (!openedStudio) closeExportStudioWindow(studioPopup);
8876
+ const targetUrl = typeof payload.relativeUrl === "string" ? payload.relativeUrl : "";
8877
+ const openedStudio = Boolean(targetUrl);
8878
+ if (openedStudio) {
8879
+ navigatePendingStudioTab(studioLaunch, targetUrl);
8880
+ } else {
8881
+ failPendingStudioTab(studioLaunch, "Studio did not return an editor URL for this HTML export.");
8882
+ }
8903
8883
  if (writeError) {
8904
8884
  setStatus(openedStudio
8905
- ? "Opened exported HTML in Studio as an unsaved copy; could not write project file: " + writeError
8906
- : "Exported HTML for Studio, but the popup was blocked and the project file could not be written: " + writeError,
8885
+ ? "Opening exported HTML in Studio as an unsaved copy; could not write project file: " + writeError
8886
+ : "Exported HTML for Studio, but Studio did not return an editor URL and the project file could not be written: " + writeError,
8907
8887
  "warning");
8908
8888
  } else if (exportWarning) {
8909
8889
  setStatus(openedStudio
8910
- ? "Opened exported HTML in Studio with warning: " + exportWarning
8911
- : "Exported HTML for Studio, but the popup was blocked. Warning: " + exportWarning,
8890
+ ? "Opening exported HTML in Studio with warning: " + exportWarning
8891
+ : "Exported HTML for Studio, but Studio did not return an editor URL. Warning: " + exportWarning,
8912
8892
  "warning");
8913
8893
  } else {
8914
8894
  setStatus(openedStudio
8915
- ? "Opened exported HTML in Studio: " + (exportPath || downloadName)
8916
- : (targetUrl ? "Exported HTML for Studio: " + targetUrl : "Exported HTML, but Studio did not receive an editor URL."),
8917
- openedStudio ? "success" : "warning");
8895
+ ? "Opening exported HTML in Studio: " + (exportPath || downloadName)
8896
+ : "Exported HTML, but Studio did not receive an editor URL.",
8897
+ openedStudio ? undefined : "warning");
8918
8898
  }
8919
8899
  return;
8920
8900
  }
@@ -8954,7 +8934,7 @@
8954
8934
  return;
8955
8935
  }
8956
8936
 
8957
- closeExportStudioWindow(studioPopup);
8937
+ failPendingStudioTab(studioLaunch, "Studio returned a download instead of an HTML editor tab.");
8958
8938
  const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
8959
8939
  const blob = await response.blob();
8960
8940
  const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
@@ -8981,7 +8961,7 @@
8981
8961
  setStatus("Exported HTML: " + downloadName, "success");
8982
8962
  }
8983
8963
  } catch (error) {
8984
- closeExportStudioWindow(studioPopup);
8964
+ failPendingStudioTab(studioLaunch, "Studio could not prepare this HTML export tab. Return to the originating Studio page for details.");
8985
8965
  const detail = error && error.message ? error.message : String(error || "unknown error");
8986
8966
  setStatus("HTML export failed: " + detail, "error");
8987
8967
  } finally {
@@ -10359,11 +10339,19 @@
10359
10339
  return;
10360
10340
  }
10361
10341
  if (action === "open-new") {
10362
- await openPreviewDocumentInNewEditor(path, null, getFileBrowserLocalLinkContext());
10342
+ if (kind === "text" && isLikelyAbsoluteStudioPath(path)) {
10343
+ openFileBackedStudioEditorTab(path, {
10344
+ label: basenameForStudioPath(path),
10345
+ resourceDir: fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(path),
10346
+ });
10347
+ setStatus("Opening file-backed document in a new editor.");
10348
+ return;
10349
+ }
10350
+ await openPreviewDocumentInNewEditor(path, getFileBrowserLocalLinkContext());
10363
10351
  return;
10364
10352
  }
10365
10353
  if (action === "open-preview-new") {
10366
- await openPreviewResourceInNewEditor(path, null, getFileBrowserLocalLinkContext());
10354
+ await openPreviewResourceInNewEditor(path, getFileBrowserLocalLinkContext());
10367
10355
  return;
10368
10356
  }
10369
10357
  if (action === "copy-path" || action === "copy-root" || action === "copy-current") {
@@ -10673,13 +10661,8 @@
10673
10661
  const hostname = parsed.hostname.toLowerCase();
10674
10662
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("Unsupported preview protocol.");
10675
10663
  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");
10664
+ openStudioTabDirect(parsed.href);
10665
+ setStatus("Opening Quarto preview in a browser tab.");
10683
10666
  return true;
10684
10667
  } catch (error) {
10685
10668
  setStatus("Could not open Quarto preview: " + ((error && error.message) ? error.message : String(error)), "error");
@@ -10689,12 +10672,8 @@
10689
10672
 
10690
10673
  function openStudioQuartoInstallationInstructions() {
10691
10674
  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 {}
10675
+ openStudioTabDirect("https://quarto.org/docs/get-started/");
10676
+ setStatus("Opening Quarto installation instructions.");
10698
10677
  } catch (error) {
10699
10678
  setStatus("Could not open Quarto installation instructions.", "warning");
10700
10679
  }
@@ -12447,6 +12426,99 @@
12447
12426
  return pathname + "?" + params.toString();
12448
12427
  }
12449
12428
 
12429
+ function openStudioTabDirect(targetUrl) {
12430
+ navigationHelpers.openStudioTabDirect(window, targetUrl);
12431
+ debugTrace("studio_tab_launch", { event: "direct-open-requested" });
12432
+ }
12433
+
12434
+ function removeActiveStudioTabLaunch(launch) {
12435
+ if (launch) activeStudioTabLaunches.delete(launch);
12436
+ }
12437
+
12438
+ function openPendingStudioTab(kind) {
12439
+ const token = getToken();
12440
+ if (!token) throw new Error("Missing Studio token in URL.");
12441
+ const launch = navigationHelpers.createPendingStudioLaunch({
12442
+ window,
12443
+ token,
12444
+ kind,
12445
+ onEvent(event, detail) {
12446
+ debugTrace("studio_tab_launch", {
12447
+ event,
12448
+ launchId: detail && detail.launchId ? detail.launchId : null,
12449
+ kind: detail && detail.kind ? detail.kind : kind,
12450
+ state: detail && detail.state ? detail.state : null,
12451
+ terminalType: detail && detail.terminalType ? detail.terminalType : null,
12452
+ ok: detail && typeof detail.ok === "boolean" ? detail.ok : undefined,
12453
+ });
12454
+ },
12455
+ onReadyTimeout() {
12456
+ setStatus("No new tab has acknowledged yet. The operation is still running; Studio will not open a fallback tab.", "warning");
12457
+ },
12458
+ onDeliveryTimeout(controller) {
12459
+ removeActiveStudioTabLaunch(controller);
12460
+ setStatus("Studio could not confirm delivery to the new tab. No replacement tab was opened.", "warning");
12461
+ },
12462
+ onAccepted(result) {
12463
+ removeActiveStudioTabLaunch(result && result.controller);
12464
+ if (result && result.ok === false) {
12465
+ setStatus("The pending tab rejected the Studio navigation target.", "error");
12466
+ }
12467
+ },
12468
+ onOpenError() {
12469
+ setStatus("The browser rejected the new-tab request. Studio will not open a fallback tab.", "warning");
12470
+ },
12471
+ });
12472
+ if (!launch) {
12473
+ throw new Error("This browser could not prepare a Studio tab.");
12474
+ }
12475
+ activeStudioTabLaunches.add(launch);
12476
+ return launch;
12477
+ }
12478
+
12479
+ function navigatePendingStudioTab(launch, relativeUrl) {
12480
+ if (!launch || typeof launch.navigate !== "function") {
12481
+ throw new Error("Studio did not prepare a pending tab for this operation.");
12482
+ }
12483
+ const token = getToken();
12484
+ const normalized = navigationHelpers.normalizeStudioRelativeTarget(relativeUrl, window.location, token);
12485
+ return launch.navigate(normalized);
12486
+ }
12487
+
12488
+ function failPendingStudioTab(launch, message) {
12489
+ if (!launch) return false;
12490
+ return launch.fail(message || "Studio could not prepare this tab. Return to the originating Studio page for details.");
12491
+ }
12492
+
12493
+ function abandonAllStudioTabLaunches(message) {
12494
+ const launches = Array.from(activeStudioTabLaunches);
12495
+ activeStudioTabLaunches.clear();
12496
+ launches.forEach((launch) => {
12497
+ try { launch.abandon(message || "The originating Studio page was closed."); } catch {}
12498
+ });
12499
+ }
12500
+
12501
+ function buildFileBackedStudioEditorUrl(path, options) {
12502
+ const cleanPath = stripPreviewLocalLinkUrlSuffix(path || "").trim();
12503
+ if (!isLikelyAbsoluteStudioPath(cleanPath)) {
12504
+ throw new Error("Opening a file-backed Studio tab requires an absolute local path.");
12505
+ }
12506
+ const config = options && typeof options === "object" ? options : {};
12507
+ return buildAuthedStudioUrl("/", {
12508
+ mode: "editor-only",
12509
+ docSource: "file",
12510
+ docLabel: config.label || basenameForStudioPath(cleanPath),
12511
+ docPath: cleanPath,
12512
+ resourceDir: normalizeStudioResourceDirValue(config.resourceDir || "") || dirnameForDisplayPath(cleanPath),
12513
+ skipWorkspaceRestore: "1",
12514
+ });
12515
+ }
12516
+
12517
+ function openFileBackedStudioEditorTab(path, options) {
12518
+ const targetUrl = buildFileBackedStudioEditorUrl(path, options);
12519
+ openStudioTabDirect(targetUrl);
12520
+ }
12521
+
12450
12522
  function updateStudioDocumentUrlState(state) {
12451
12523
  try {
12452
12524
  const currentUrl = new URL(window.location.href);
@@ -12723,10 +12795,7 @@
12723
12795
  return true;
12724
12796
  }
12725
12797
 
12726
- async function openPreviewImageLink(href, title, contextOverride, pendingWindow) {
12727
- if (pendingWindow && !pendingWindow.closed) {
12728
- try { pendingWindow.close(); } catch {}
12729
- }
12798
+ async function openPreviewImageLink(href, title, contextOverride) {
12730
12799
  const payload = await fetchStudioJson("/html-preview-resource", {
12731
12800
  query: getPreviewLinkResourceQuery(href, contextOverride),
12732
12801
  });
@@ -12812,73 +12881,33 @@
12812
12881
  : (path ? ("Opened file-backed document in editor: " + label) : ("Opened linked file copy in editor: " + label)), "success");
12813
12882
  }
12814
12883
 
12815
- async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
12816
- if (!confirmPreviewOfficeConversion(href, "new")) {
12817
- if (pendingWindow && !pendingWindow.closed) {
12818
- try { pendingWindow.close(); } catch {}
12819
- }
12820
- return;
12821
- }
12822
- const popup = pendingWindow || window.open("", "_blank");
12823
- const openingLabel = getPreviewLocalLinkKind(href) === "office" ? "Opening converted document…" : "Opening file tab…";
12824
- try {
12825
- if (popup && popup.document && popup.document.body) {
12826
- popup.document.title = openingLabel;
12827
- popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">" + escapeHtml(openingLabel) + "</p>";
12828
- }
12829
- } catch {}
12884
+ async function openPreviewDocumentInNewEditor(href, contextOverride) {
12885
+ if (!confirmPreviewOfficeConversion(href, "new")) return;
12886
+ let launch = null;
12830
12887
  try {
12888
+ launch = openPendingStudioTab("document");
12831
12889
  const payload = await fetchPreviewLocalLink("editor-url", href, contextOverride);
12832
- const targetUrl = payload && typeof payload.relativeUrl === "string"
12833
- ? new URL(payload.relativeUrl, window.location.href).href
12834
- : (payload && typeof payload.url === "string" ? payload.url : "");
12835
- if (!targetUrl) throw new Error("Studio did not return an editor URL.");
12836
- if (popup && !popup.closed) {
12837
- try {
12838
- popup.opener = null;
12839
- popup.location.href = targetUrl;
12840
- setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.", "success");
12841
- return;
12842
- } catch {}
12843
- }
12844
- window.open(targetUrl, "_blank", "noopener");
12845
- setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.", "success");
12890
+ const relativeUrl = payload && typeof payload.relativeUrl === "string" ? payload.relativeUrl : "";
12891
+ if (!relativeUrl) throw new Error("Studio did not return an editor URL.");
12892
+ navigatePendingStudioTab(launch, relativeUrl);
12893
+ setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.");
12846
12894
  } catch (error) {
12847
- if (popup && !popup.closed) {
12848
- try { popup.close(); } catch {}
12849
- }
12895
+ failPendingStudioTab(launch, "Studio could not prepare this document tab. Return to the originating Studio page for details.");
12850
12896
  throw error;
12851
12897
  }
12852
12898
  }
12853
12899
 
12854
- async function openPreviewResourceInNewEditor(href, pendingWindow, contextOverride) {
12855
- const popup = pendingWindow || window.open("", "_blank");
12856
- try {
12857
- if (popup && popup.document && popup.document.body) {
12858
- popup.document.title = "Opening preview…";
12859
- popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening preview…</p>";
12860
- }
12861
- } catch {}
12900
+ async function openPreviewResourceInNewEditor(href, contextOverride) {
12901
+ let launch = null;
12862
12902
  try {
12903
+ launch = openPendingStudioTab("preview");
12863
12904
  const payload = await fetchPreviewLocalLink("preview-url", href, contextOverride);
12864
- const targetUrl = payload && typeof payload.relativeUrl === "string"
12865
- ? new URL(payload.relativeUrl, window.location.href).href
12866
- : (payload && typeof payload.url === "string" ? payload.url : "");
12867
- if (!targetUrl) throw new Error("Studio did not return a preview URL.");
12868
- if (popup && !popup.closed) {
12869
- try {
12870
- popup.opener = null;
12871
- popup.location.href = targetUrl;
12872
- setStatus("Opening preview in a new Studio tab.", "success");
12873
- return;
12874
- } catch {}
12875
- }
12876
- window.open(targetUrl, "_blank", "noopener");
12877
- setStatus("Opening preview in a new Studio tab.", "success");
12905
+ const relativeUrl = payload && typeof payload.relativeUrl === "string" ? payload.relativeUrl : "";
12906
+ if (!relativeUrl) throw new Error("Studio did not return a preview URL.");
12907
+ navigatePendingStudioTab(launch, relativeUrl);
12908
+ setStatus("Opening preview in a new Studio tab.");
12878
12909
  } catch (error) {
12879
- if (popup && !popup.closed) {
12880
- try { popup.close(); } catch {}
12881
- }
12910
+ failPendingStudioTab(launch, "Studio could not prepare this preview tab. Return to the originating Studio page for details.");
12882
12911
  throw error;
12883
12912
  }
12884
12913
  }
@@ -12910,11 +12939,11 @@
12910
12939
  return;
12911
12940
  }
12912
12941
  if (action === "open-new") {
12913
- await openPreviewDocumentInNewEditor(href, null, context);
12942
+ await openPreviewDocumentInNewEditor(href, context);
12914
12943
  return;
12915
12944
  }
12916
12945
  if (action === "open-preview-new") {
12917
- await openPreviewResourceInNewEditor(href, null, context);
12946
+ await openPreviewResourceInNewEditor(href, context);
12918
12947
  return;
12919
12948
  }
12920
12949
  if (action === "open-here") {
@@ -12957,8 +12986,7 @@
12957
12986
  return;
12958
12987
  }
12959
12988
  if (kind === "text" || kind === "office") {
12960
- const pendingWindow = kind === "office" ? null : window.open("", "_blank");
12961
- void openPreviewDocumentInNewEditor(href, pendingWindow).catch((error) => {
12989
+ void openPreviewDocumentInNewEditor(href).catch((error) => {
12962
12990
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
12963
12991
  });
12964
12992
  return;
@@ -20120,24 +20148,31 @@
20120
20148
 
20121
20149
  if (message.type === "editor_only_ready") {
20122
20150
  const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
20123
- if (responseRequestId && pendingRequestId === responseRequestId) {
20124
- pendingRequestId = null;
20125
- pendingKind = null;
20126
- clearArmedTitleAttention(responseRequestId);
20127
- stickyStudioKind = null;
20151
+ if (!responseRequestId || !pendingCompanionLaunches.has(responseRequestId)) return;
20152
+ if (pendingRequestId !== responseRequestId || pendingKind !== "open_editor_only") {
20153
+ failPendingCompanionLaunch(responseRequestId, "This companion editor request was superseded.");
20154
+ return;
20128
20155
  }
20156
+ pendingRequestId = null;
20157
+ pendingKind = null;
20158
+ clearArmedTitleAttention(responseRequestId);
20159
+ stickyStudioKind = null;
20129
20160
  setBusy(false);
20130
20161
  setWsState("Ready");
20131
- const targetUrl = resolveCompanionEditorTargetUrl(message);
20132
- const opened = navigatePendingCompanionWindow(responseRequestId, targetUrl);
20162
+ const relativeUrl = resolveCompanionEditorRelativeUrl(message);
20163
+ let queued = false;
20164
+ try {
20165
+ queued = navigatePendingCompanionLaunch(responseRequestId, relativeUrl);
20166
+ } catch (error) {
20167
+ setStatus((error && error.message) ? error.message : "Studio rejected the companion editor URL.", "error");
20168
+ return;
20169
+ }
20133
20170
  const readyMessage = typeof message.message === "string" && message.message.trim()
20134
20171
  ? message.message.trim()
20135
- : "Opened editor tab with a detached copy of the current editor text.";
20172
+ : "Opening an editor tab with a detached copy of the current editor text.";
20136
20173
  setStatus(
20137
- opened
20138
- ? readyMessage
20139
- : (targetUrl ? "Editor tab ready: " + targetUrl : "Editor tab is ready, but Studio did not receive a URL."),
20140
- opened ? "success" : "warning",
20174
+ queued ? readyMessage : "Editor tab is ready, but Studio did not receive a valid relative URL.",
20175
+ queued ? undefined : "warning",
20141
20176
  );
20142
20177
  return;
20143
20178
  }
@@ -20214,7 +20249,7 @@
20214
20249
 
20215
20250
  if (message.type === "busy") {
20216
20251
  if (typeof message.requestId === "string") {
20217
- closePendingCompanionWindow(message.requestId);
20252
+ failPendingCompanionLaunch(message.requestId, "Studio could not start the companion editor because another request was busy.");
20218
20253
  }
20219
20254
  if (message.requestId && pendingRequestId === message.requestId) {
20220
20255
  if (pendingKind === "compact") {
@@ -20235,7 +20270,7 @@
20235
20270
 
20236
20271
  if (message.type === "error") {
20237
20272
  if (typeof message.requestId === "string") {
20238
- closePendingCompanionWindow(message.requestId);
20273
+ failPendingCompanionLaunch(message.requestId, "Studio could not prepare the companion editor. Return to the originating Studio page for details.");
20239
20274
  }
20240
20275
  if (message.requestId && pendingRequestId === message.requestId) {
20241
20276
  if (pendingKind === "compact") {
@@ -20370,6 +20405,7 @@
20370
20405
  quartoPreviewCheckRequestId = null;
20371
20406
  quartoPreviewCheckSourcePath = "";
20372
20407
  quartoPreviewActionRequestId = null;
20408
+ failAllPendingCompanionLaunches("The originating Studio connection was lost before the companion editor was ready.");
20373
20409
  if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
20374
20410
  setBusy(true);
20375
20411
 
@@ -20460,73 +20496,70 @@
20460
20496
  return requestId;
20461
20497
  }
20462
20498
 
20463
- function openPendingCompanionWindow(requestId) {
20499
+ function openPendingCompanionLaunch(requestId) {
20464
20500
  if (!requestId) return null;
20465
- let companionWindow = null;
20466
- try {
20467
- companionWindow = window.open("", "_blank");
20468
- if (companionWindow && companionWindow.document && companionWindow.document.body) {
20469
- companionWindow.document.title = "Opening editor tab…";
20470
- companionWindow.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening editor tab…</p>";
20471
- }
20472
- } catch {
20473
- companionWindow = null;
20474
- }
20475
- if (companionWindow) {
20476
- pendingCompanionWindows.set(requestId, companionWindow);
20477
- }
20478
- return companionWindow;
20501
+ const launch = openPendingStudioTab("document");
20502
+ pendingCompanionLaunches.set(requestId, launch);
20503
+ return launch;
20479
20504
  }
20480
20505
 
20481
- function takePendingCompanionWindow(requestId) {
20482
- if (!requestId || !pendingCompanionWindows.has(requestId)) return null;
20483
- const companionWindow = pendingCompanionWindows.get(requestId);
20484
- pendingCompanionWindows.delete(requestId);
20485
- return companionWindow || null;
20506
+ function takePendingCompanionLaunch(requestId) {
20507
+ if (!requestId || !pendingCompanionLaunches.has(requestId)) return { found: false, launch: null };
20508
+ const launch = pendingCompanionLaunches.get(requestId) || null;
20509
+ pendingCompanionLaunches.delete(requestId);
20510
+ return { found: true, launch };
20486
20511
  }
20487
20512
 
20488
- function closePendingCompanionWindow(requestId) {
20489
- const companionWindow = takePendingCompanionWindow(requestId);
20490
- if (!companionWindow || companionWindow.closed) return;
20491
- try {
20492
- companionWindow.close();
20493
- } catch {}
20513
+ function failPendingCompanionLaunch(requestId, message) {
20514
+ const pending = takePendingCompanionLaunch(requestId);
20515
+ if (!pending.found) return false;
20516
+ failPendingStudioTab(
20517
+ pending.launch,
20518
+ message || "Studio could not prepare this companion editor. Return to the originating Studio page for details.",
20519
+ );
20520
+ return true;
20494
20521
  }
20495
20522
 
20496
- function resolveCompanionEditorTargetUrl(message) {
20497
- const relativeUrl = message && typeof message.relativeUrl === "string" ? message.relativeUrl : "";
20498
- if (relativeUrl) {
20499
- try {
20500
- return new URL(relativeUrl, window.location.href).href;
20501
- } catch {}
20502
- }
20503
- return message && typeof message.url === "string" ? message.url : "";
20523
+ function failAllPendingCompanionLaunches(message) {
20524
+ const requestIds = Array.from(pendingCompanionLaunches.keys());
20525
+ requestIds.forEach((requestId) => failPendingCompanionLaunch(requestId, message));
20504
20526
  }
20505
20527
 
20506
- function navigatePendingCompanionWindow(requestId, targetUrl) {
20507
- if (!targetUrl) {
20508
- closePendingCompanionWindow(requestId);
20528
+ function resolveCompanionEditorRelativeUrl(message) {
20529
+ return message && typeof message.relativeUrl === "string" ? message.relativeUrl : "";
20530
+ }
20531
+
20532
+ function navigatePendingCompanionLaunch(requestId, relativeUrl) {
20533
+ const pending = takePendingCompanionLaunch(requestId);
20534
+ if (!pending.found || !relativeUrl) {
20535
+ if (pending.found) {
20536
+ failPendingStudioTab(pending.launch, "Studio did not return a companion editor target.");
20537
+ }
20509
20538
  return false;
20510
20539
  }
20511
- const companionWindow = takePendingCompanionWindow(requestId);
20512
- if (companionWindow && !companionWindow.closed) {
20513
- try {
20514
- companionWindow.opener = null;
20515
- companionWindow.location.href = targetUrl;
20516
- return true;
20517
- } catch {}
20518
- }
20519
20540
  try {
20520
- return Boolean(window.open(targetUrl, "_blank", "noopener"));
20521
- } catch {
20522
- return false;
20541
+ navigatePendingStudioTab(pending.launch, relativeUrl);
20542
+ return true;
20543
+ } catch (error) {
20544
+ failPendingStudioTab(pending.launch, "Studio rejected the companion editor target.");
20545
+ throw error;
20523
20546
  }
20524
20547
  }
20525
20548
 
20526
20549
  function requestOpenEditorOnlyDocument(content, options) {
20527
20550
  const requestId = beginUiAction("open_editor_only");
20528
20551
  if (!requestId) return false;
20529
- openPendingCompanionWindow(requestId);
20552
+ try {
20553
+ openPendingCompanionLaunch(requestId);
20554
+ } catch (error) {
20555
+ pendingRequestId = null;
20556
+ pendingKind = null;
20557
+ stickyStudioKind = null;
20558
+ setBusy(false);
20559
+ setWsState("Ready");
20560
+ setStatus((error && error.message) ? error.message : "Could not prepare a companion editor tab.", "error");
20561
+ return false;
20562
+ }
20530
20563
  const config = options && typeof options === "object" ? options : {};
20531
20564
  const sent = sendMessage({
20532
20565
  type: "open_editor_only_request",
@@ -20537,9 +20570,10 @@
20537
20570
  resourceDir: config.resourceDir || undefined,
20538
20571
  });
20539
20572
  if (!sent) {
20540
- closePendingCompanionWindow(requestId);
20573
+ failPendingCompanionLaunch(requestId, "Studio could not send the companion editor request.");
20541
20574
  pendingRequestId = null;
20542
20575
  pendingKind = null;
20576
+ stickyStudioKind = null;
20543
20577
  setBusy(false);
20544
20578
  }
20545
20579
  return sent;
@@ -20671,6 +20705,10 @@
20671
20705
 
20672
20706
  updatePaneFocusButtons();
20673
20707
  window.addEventListener("keydown", handlePaneShortcut);
20708
+ window.addEventListener("pagehide", () => {
20709
+ pendingCompanionLaunches.clear();
20710
+ abandonAllStudioTabLaunches("The originating Studio page was closed before this tab finished opening.");
20711
+ });
20674
20712
  window.addEventListener("beforeunload", () => {
20675
20713
  stopFooterSpinner();
20676
20714
  flushWorkspacePersistence();
@@ -21829,7 +21867,9 @@
21829
21867
  if (initialSourceState.path) markFileBackedBaseline(sourceTextEl.value);
21830
21868
  refreshResponseUi();
21831
21869
  updateAnnotatedReplyHeaderButton();
21832
- setActivePane("left");
21870
+ setActivePane(initialPaneFocusTarget === "off" ? "left" : initialPaneFocusTarget);
21871
+ applyPaneFocusClasses();
21872
+ persistPaneFocusUrlState();
21833
21873
 
21834
21874
  const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
21835
21875
  const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value !== "off");