pi-studio 0.9.50 → 0.9.52

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.
@@ -83,6 +83,7 @@
83
83
  const critiqueBtn = document.getElementById("critiqueBtn");
84
84
  const showMeBtn = document.getElementById("showMeBtn");
85
85
  const showMeResponseBtn = document.getElementById("showMeResponseBtn");
86
+ const askAsideBtn = document.getElementById("askAsideBtn");
86
87
  const quizBtn = document.getElementById("quizBtn");
87
88
  const lensSelect = document.getElementById("lensSelect");
88
89
  const importFileBtn = document.getElementById("importFileBtn");
@@ -99,6 +100,10 @@
99
100
  const copyResponseBtn = document.getElementById("copyResponseBtn");
100
101
  const exportPreviewControlsEl = document.getElementById("exportPreviewControls");
101
102
  const exportPreviewMenuEl = document.getElementById("exportPreviewMenu");
103
+ const exportSideThreadMarkdownSaveBtn = document.getElementById("exportSideThreadMarkdownSaveBtn");
104
+ const exportSideThreadMarkdownCopyBtn = document.getElementById("exportSideThreadMarkdownCopyBtn");
105
+ const exportSideThreadMarkdownEditorBtn = document.getElementById("exportSideThreadMarkdownEditorBtn");
106
+ const exportSideThreadRenderSeparatorEl = document.getElementById("exportSideThreadRenderSeparator");
102
107
  const exportPreviewPdfStudioBtn = document.getElementById("exportPreviewPdfStudioBtn");
103
108
  const exportPreviewPdfBtn = document.getElementById("exportPreviewPdfBtn");
104
109
  const exportPreviewHtmlStudioBtn = document.getElementById("exportPreviewHtmlStudioBtn");
@@ -188,7 +193,7 @@
188
193
  : "full";
189
194
  const isEditorOnlyMode = studioMode === "editor-only";
190
195
  const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
191
- const EDITOR_ONLY_RIGHT_VIEW_ALLOWED = new Set(["editor-preview", "editor-quarto-preview", "files", "changes", "repl"]);
196
+ const EDITOR_ONLY_RIGHT_VIEW_ALLOWED = new Set(["editor-preview", "editor-quarto-preview", "files", "changes", "repl", "side-questions"]);
192
197
  const RIGHT_VIEW_LABELS = {
193
198
  markdown: "Response (Raw)",
194
199
  preview: "Response (Preview)",
@@ -198,6 +203,7 @@
198
203
  changes: "Changes",
199
204
  files: "Files",
200
205
  repl: "REPL",
206
+ "side-questions": "Side questions",
201
207
  };
202
208
  const RIGHT_VIEW_NUMERIC_SHORTCUTS = {
203
209
  Digit1: "markdown",
@@ -207,6 +213,7 @@
207
213
  Digit5: "changes",
208
214
  Digit6: "files",
209
215
  Digit7: "repl",
216
+ Digit8: "side-questions",
210
217
  Numpad1: "markdown",
211
218
  Numpad2: "preview",
212
219
  Numpad3: "editor-preview",
@@ -214,6 +221,7 @@
214
221
  Numpad5: "changes",
215
222
  Numpad6: "files",
216
223
  Numpad7: "repl",
224
+ Numpad8: "side-questions",
217
225
  };
218
226
 
219
227
  const navigationHelpers = globalThis.PiStudioNavigationHelpers;
@@ -242,6 +250,16 @@
242
250
  if (!showMeHelpers || typeof showMeHelpers.chooseStudioShowMeFocus !== "function") {
243
251
  throw new Error("Studio Show me helpers failed to load.");
244
252
  }
253
+ const sideQuestionHelpers = globalThis.PiStudioSideQuestionHelpers;
254
+ if (
255
+ !sideQuestionHelpers
256
+ || typeof sideQuestionHelpers.buildStudioSideQuestionTranscriptMarkdown !== "function"
257
+ || typeof sideQuestionHelpers.chooseStudioSideQuestionFocus !== "function"
258
+ || typeof sideQuestionHelpers.findStudioSideQuestionSection !== "function"
259
+ || typeof sideQuestionHelpers.formatStudioSideQuestionTranscriptFilename !== "function"
260
+ ) {
261
+ throw new Error("Studio side-question helpers failed to load.");
262
+ }
245
263
  const studioTabStateId = navigationHelpers.ensureStudioTabStateId(window);
246
264
  const initialQueryParams = new URLSearchParams(window.location.search || "");
247
265
  const initialPaneFocusTarget = navigationHelpers.readPaneFocusTarget(window.location);
@@ -278,6 +296,7 @@
278
296
  let studioPdfFocusOpenLinkEl = null;
279
297
  let studioPdfFocusSystemViewerBtn = null;
280
298
  let studioPdfFocusRevealBtn = null;
299
+ let studioPdfFocusCopyPathBtn = null;
281
300
  let studioPdfFocusAutoRefreshBtn = null;
282
301
  let studioPdfFocusFullscreenBtn = null;
283
302
  let studioPdfFocusCloseBtn = null;
@@ -287,6 +306,7 @@
287
306
  let studioPdfFocusSourceCard = null;
288
307
  let studioPdfFocusStandaloneAutoRefreshState = null;
289
308
  const studioPdfCardAutoRefreshStates = new WeakMap();
309
+ const studioPdfActionFeedbackStates = new WeakMap();
290
310
  const STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS = 1_000;
291
311
  const STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS = 2;
292
312
  let studioHtmlFocusOverlayEl = null;
@@ -335,13 +355,15 @@
335
355
  ? "editor-preview"
336
356
  : (raw === "editor-quarto-preview"
337
357
  ? "editor-quarto-preview"
338
- : (raw === "repl"
339
- ? "repl"
340
- : (raw === "files"
358
+ : (raw === "side-questions"
359
+ ? "side-questions"
360
+ : (raw === "repl"
361
+ ? "repl"
362
+ : (raw === "files"
341
363
  ? "files"
342
364
  : (raw === "changes"
343
365
  ? "changes"
344
- : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown"))))));
366
+ : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown")))))));
345
367
  }
346
368
 
347
369
  function normalizeRightViewValue(nextView) {
@@ -380,8 +402,8 @@
380
402
  option.disabled = (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value)) || (isQuartoOption && !quartoRelevant);
381
403
  });
382
404
  rightViewSelect.title = isEditorOnlyMode
383
- ? "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."
384
- : "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.";
405
+ ? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+Q opens Side questions."
406
+ : "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W keep their mnemonic Preview/Editor Preview/Working shortcuts.";
385
407
  }
386
408
 
387
409
  function getInitialRightView(source) {
@@ -441,12 +463,49 @@
441
463
  const RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS = 8_000;
442
464
  const EDITOR_TAB_TEXT = " ";
443
465
  const QUIZ_DEFAULT_COUNT = 5;
466
+ const SIDE_QUESTION_THINKING_STORAGE_KEY = "piStudio.sideQuestionThinking";
467
+ const SIDE_QUESTION_GATHER_STORAGE_KEY = "piStudio.sideQuestionGatherScope";
468
+ const SIDE_QUESTION_TOOLS_STORAGE_KEY = "piStudio.sideQuestionTools";
469
+ const SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS = 400_000;
444
470
  const COMPLETION_CONTEXT_STORAGE_KEY = "piStudio.completionContextMode";
445
471
  const COMPLETION_MODEL_STORAGE_KEY = "piStudio.completionModel";
446
472
  const COMPLETION_CONTEXT_MAX_CHARS = 12000;
447
473
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
448
474
  const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
449
475
  const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
476
+ let sideQuestionState = null;
477
+ let sideQuestionWebSearchAvailable = false;
478
+ let sideQuestionAvailablePiTools = [];
479
+ let sideQuestionPreviewRenderNonce = 0;
480
+ let sideQuestionContextRefreshHandle = null;
481
+ let sideQuestionMarkdownExportRequest = null;
482
+ const sideQuestionMarkdownRenderCache = new Map();
483
+ let sideQuestionUi = {
484
+ focusMode: "auto",
485
+ gatherScope: (() => {
486
+ try {
487
+ const value = window.localStorage && window.localStorage.getItem(SIDE_QUESTION_GATHER_STORAGE_KEY);
488
+ return value === "none" || value === "folder" || value === "repo" || value === "custom" ? value : "";
489
+ } catch { return ""; }
490
+ })(),
491
+ customPath: "",
492
+ includeConversation: false,
493
+ gitContext: false,
494
+ webSearch: false,
495
+ toolIds: (() => {
496
+ try {
497
+ const parsed = JSON.parse((window.localStorage && window.localStorage.getItem(SIDE_QUESTION_TOOLS_STORAGE_KEY)) || "[]");
498
+ return Array.isArray(parsed) ? [...new Set(parsed.filter((id) => typeof id === "string" && /^[a-f0-9]{24}$/i.test(id)).map((id) => id.toLowerCase()))].slice(0, 12) : [];
499
+ } catch { return []; }
500
+ })(),
501
+ thinking: (() => {
502
+ try {
503
+ const value = window.localStorage && window.localStorage.getItem(SIDE_QUESTION_THINKING_STORAGE_KEY);
504
+ return ["off", "minimal", "low", "medium", "high"].includes(value) ? value : "low";
505
+ } catch { return "low"; }
506
+ })(),
507
+ draft: "",
508
+ };
450
509
  let quizOverlayEl = null;
451
510
  let quizDialogEl = null;
452
511
  let quizPreviewRenderNonce = 0;
@@ -2717,7 +2776,7 @@
2717
2776
  if (!isEditorOnlyMode && critiqueBtn && lensSelect) {
2718
2777
  const reviewButton = makeStudioUiRefreshElement("button", "studio-refresh-tool-tab studio-refresh-review-btn", "Review");
2719
2778
  reviewMenu = makeStudioUiRefreshMenu(reviewButton, "review", "studio-refresh-review-anchor");
2720
- appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, showMeBtn, showMeResponseBtn, quizBtn]);
2779
+ appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, showMeBtn, showMeResponseBtn, askAsideBtn, quizBtn]);
2721
2780
  appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
2722
2781
  }
2723
2782
 
@@ -2841,6 +2900,7 @@
2841
2900
  if (reviewNotesBtn) headerToolsEl.appendChild(reviewNotesBtn);
2842
2901
  if (outlineBtn) headerToolsEl.appendChild(outlineBtn);
2843
2902
  if (scratchpadBtn) headerToolsEl.appendChild(scratchpadBtn);
2903
+ if (isEditorOnlyMode && askAsideBtn) headerToolsEl.appendChild(askAsideBtn);
2844
2904
  if (reviewMenu) headerToolsEl.appendChild(reviewMenu.anchor);
2845
2905
  headerTopEl.appendChild(headerToolsEl);
2846
2906
 
@@ -3041,9 +3101,9 @@
3041
3101
 
3042
3102
  function getIdleStatus() {
3043
3103
  if (isEditorOnlyMode) {
3044
- return "Editor-only mode: edit, browse files, annotate, preview, save, suggest, refresh file-backed text, or send to a REPL.";
3104
+ return "Editor-only mode: edit, browse files, annotate, preview, save, suggest, ask a side question, refresh file-backed text, or send to a REPL.";
3045
3105
  }
3046
- return "Edit, load, or annotate text, then run, save, review, or ask Studio to show the current structure.";
3106
+ return "Edit, load, or annotate text, then run, save, review, explain, or ask a side question.";
3047
3107
  }
3048
3108
 
3049
3109
  function normalizeTerminalPhase(phase) {
@@ -4761,6 +4821,16 @@
4761
4821
  return;
4762
4822
  }
4763
4823
 
4824
+ const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
4825
+ && (event.metaKey || event.ctrlKey)
4826
+ && event.altKey
4827
+ && !event.shiftKey;
4828
+ if (isSideQuestionsShortcut) {
4829
+ event.preventDefault();
4830
+ switchRightPaneToView("side-questions");
4831
+ return;
4832
+ }
4833
+
4764
4834
  const isContentFocusShortcut = key === "F8" && !event.metaKey && !event.ctrlKey && !event.altKey;
4765
4835
  if (isContentFocusShortcut) {
4766
4836
  event.preventDefault();
@@ -5019,6 +5089,19 @@
5019
5089
  }
5020
5090
  }
5021
5091
 
5092
+ function syncAskAsideButton() {
5093
+ if (!askAsideBtn) return;
5094
+ const running = Boolean(sideQuestionState && sideQuestionState.status === "running");
5095
+ askAsideBtn.disabled = wsState === "Disconnected";
5096
+ askAsideBtn.textContent = isEditorOnlyMode
5097
+ ? "Side questions"
5098
+ : (running ? "Side question running…" : (sideQuestionState && sideQuestionState.threadId ? "Open side questions" : "Side question"));
5099
+ askAsideBtn.classList.toggle("request-active", running);
5100
+ askAsideBtn.title = running
5101
+ ? "Open the independently running side question."
5102
+ : "Open side questions without adding them to the main Pi conversation.";
5103
+ }
5104
+
5022
5105
  function syncTraceForSelectedHistoryItem() {
5023
5106
  const item = getSelectedHistoryItem();
5024
5107
  const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
@@ -5222,6 +5305,18 @@
5222
5305
  return;
5223
5306
  }
5224
5307
 
5308
+ if (rightView === "side-questions") {
5309
+ if (sideQuestionState && sideQuestionState.threadId) {
5310
+ const count = sideQuestionState.messages.filter((entry) => entry.role === "assistant" && entry.status === "complete").length;
5311
+ referenceBadgeEl.textContent = "Side thread: " + (sideQuestionState.status === "running" ? "answering" : "ready")
5312
+ + " · " + count + " answer" + (count === 1 ? "" : "s")
5313
+ + " · outside main context";
5314
+ } else {
5315
+ referenceBadgeEl.textContent = "Side questions: no active thread · outside main context";
5316
+ }
5317
+ return;
5318
+ }
5319
+
5225
5320
  if (rightView === "trace") {
5226
5321
  const state = traceState || createEmptyTraceState();
5227
5322
  const context = traceDisplayContext || {};
@@ -6622,10 +6717,22 @@
6622
6717
  button.setAttribute("aria-pressed", focused ? "true" : "false");
6623
6718
  }
6624
6719
 
6720
+ function isStudioFullscreenAvailable(element) {
6721
+ return Boolean(
6722
+ element
6723
+ && typeof element.requestFullscreen === "function"
6724
+ && typeof document.exitFullscreen === "function"
6725
+ );
6726
+ }
6727
+
6625
6728
  function syncStudioHtmlFocusFullscreenButton() {
6626
6729
  const button = studioHtmlFocusFullscreenBtn;
6627
6730
  if (!button) return;
6628
6731
  const shell = studioHtmlFocusShellEl;
6732
+ const available = isStudioFullscreenAvailable(shell);
6733
+ button.hidden = !available;
6734
+ button.setAttribute("aria-hidden", available ? "false" : "true");
6735
+ if (!available) return;
6629
6736
  const isFullscreen = Boolean(shell && document.fullscreenElement && document.fullscreenElement === shell);
6630
6737
  button.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
6631
6738
  const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
@@ -7098,9 +7205,9 @@
7098
7205
  systemViewerBtn.type = "button";
7099
7206
  systemViewerBtn.className = "studio-pdf-focus-btn studio-pdf-focus-system-viewer";
7100
7207
  systemViewerBtn.textContent = "System viewer";
7101
- systemViewerBtn.title = "Open the local PDF in the operating system's default PDF viewer.";
7208
+ systemViewerBtn.title = "Open the local PDF in the default viewer on the computer running Pi.";
7102
7209
  systemViewerBtn.addEventListener("click", () => {
7103
- void runStudioPdfLocalAction("system-viewer", studioPdfFocusResourceQuery);
7210
+ void runStudioPdfLocalAction("system-viewer", studioPdfFocusResourceQuery, systemViewerBtn);
7104
7211
  });
7105
7212
  actions.appendChild(systemViewerBtn);
7106
7213
 
@@ -7108,12 +7215,22 @@
7108
7215
  revealBtn.type = "button";
7109
7216
  revealBtn.className = "studio-pdf-focus-btn studio-pdf-focus-reveal";
7110
7217
  revealBtn.textContent = "Show in folder";
7111
- revealBtn.title = "Reveal the local PDF in Finder or the system file manager.";
7218
+ revealBtn.title = "Reveal the local PDF in the file manager on the computer running Pi.";
7112
7219
  revealBtn.addEventListener("click", () => {
7113
- void runStudioPdfLocalAction("reveal", studioPdfFocusResourceQuery);
7220
+ void runStudioPdfLocalAction("reveal", studioPdfFocusResourceQuery, revealBtn);
7114
7221
  });
7115
7222
  actions.appendChild(revealBtn);
7116
7223
 
7224
+ const copyPathBtn = document.createElement("button");
7225
+ copyPathBtn.type = "button";
7226
+ copyPathBtn.className = "studio-pdf-focus-btn studio-pdf-focus-copy-path";
7227
+ copyPathBtn.textContent = "Copy path";
7228
+ copyPathBtn.title = "Copy the PDF path on the computer running Pi.";
7229
+ copyPathBtn.addEventListener("click", () => {
7230
+ void copyStudioPdfResourcePath(studioPdfFocusResourceQuery, copyPathBtn);
7231
+ });
7232
+ actions.appendChild(copyPathBtn);
7233
+
7117
7234
  const refreshBtn = document.createElement("button");
7118
7235
  refreshBtn.type = "button";
7119
7236
  refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
@@ -7189,6 +7306,7 @@
7189
7306
  studioPdfFocusOpenLinkEl = openLink;
7190
7307
  studioPdfFocusSystemViewerBtn = systemViewerBtn;
7191
7308
  studioPdfFocusRevealBtn = revealBtn;
7309
+ studioPdfFocusCopyPathBtn = copyPathBtn;
7192
7310
  studioPdfFocusAutoRefreshBtn = autoRefreshBtn;
7193
7311
  studioPdfFocusFullscreenBtn = fullscreenBtn;
7194
7312
  studioPdfFocusCloseBtn = closeBtn;
@@ -7483,9 +7601,33 @@
7483
7601
  return setStudioPdfAutoRefreshEnabled(state, !state.enabled);
7484
7602
  }
7485
7603
 
7486
- async function runStudioPdfLocalAction(action, resourceQuery) {
7604
+ function flashStudioPdfActionFeedback(buttonEl, label, level) {
7605
+ if (!(buttonEl instanceof HTMLButtonElement)) return;
7606
+ const existing = studioPdfActionFeedbackStates.get(buttonEl) || null;
7607
+ if (existing && existing.timer) window.clearTimeout(existing.timer);
7608
+ const state = {
7609
+ baselineText: existing ? existing.baselineText : buttonEl.textContent,
7610
+ baselineAriaLabel: existing ? existing.baselineAriaLabel : buttonEl.getAttribute("aria-label"),
7611
+ timer: null,
7612
+ };
7613
+ studioPdfActionFeedbackStates.set(buttonEl, state);
7614
+ buttonEl.textContent = String(label || "Done");
7615
+ buttonEl.dataset.studioActionFeedback = level === "warning" ? "warning" : "success";
7616
+ buttonEl.setAttribute("aria-label", String(label || "Done"));
7617
+ state.timer = window.setTimeout(() => {
7618
+ if (studioPdfActionFeedbackStates.get(buttonEl) !== state) return;
7619
+ buttonEl.textContent = state.baselineText;
7620
+ if (state.baselineAriaLabel === null) buttonEl.removeAttribute("aria-label");
7621
+ else buttonEl.setAttribute("aria-label", state.baselineAriaLabel);
7622
+ delete buttonEl.dataset.studioActionFeedback;
7623
+ studioPdfActionFeedbackStates.delete(buttonEl);
7624
+ }, 2_200);
7625
+ }
7626
+
7627
+ async function runStudioPdfLocalAction(action, resourceQuery, actionButton) {
7487
7628
  const query = normalizeStudioPdfResourceQuery(resourceQuery);
7488
7629
  if (!query) {
7630
+ flashStudioPdfActionFeedback(actionButton, "Unavailable", "warning");
7489
7631
  setStatus("Could not resolve this PDF's local path.", "warning");
7490
7632
  return false;
7491
7633
  }
@@ -7496,11 +7638,13 @@
7496
7638
  method: "POST",
7497
7639
  body: JSON.stringify(query),
7498
7640
  });
7641
+ flashStudioPdfActionFeedback(actionButton, openInSystemViewer ? "Opened ✓" : "Shown ✓", "success");
7499
7642
  setStatus(payload && payload.message
7500
7643
  ? payload.message
7501
7644
  : (openInSystemViewer ? "Opened PDF in the system viewer." : "Revealed PDF in the file manager."), "success");
7502
7645
  return true;
7503
7646
  } catch (error) {
7647
+ flashStudioPdfActionFeedback(actionButton, "Failed", "warning");
7504
7648
  setStatus((error && error.message)
7505
7649
  ? error.message
7506
7650
  : (openInSystemViewer ? "Could not open PDF in the system viewer." : "Could not reveal PDF in the file manager."), "warning");
@@ -7512,6 +7656,31 @@
7512
7656
  const available = Boolean(normalizeStudioPdfResourceQuery(studioPdfFocusResourceQuery));
7513
7657
  if (studioPdfFocusSystemViewerBtn) studioPdfFocusSystemViewerBtn.disabled = !available;
7514
7658
  if (studioPdfFocusRevealBtn) studioPdfFocusRevealBtn.disabled = !available;
7659
+ if (studioPdfFocusCopyPathBtn) studioPdfFocusCopyPathBtn.disabled = !available;
7660
+ }
7661
+
7662
+ async function copyStudioPdfResourcePath(resourceQuery, actionButton) {
7663
+ const query = normalizeStudioPdfResourceQuery(resourceQuery);
7664
+ if (!query) {
7665
+ flashStudioPdfActionFeedback(actionButton, "Unavailable", "warning");
7666
+ setStatus("Could not resolve this PDF's local path.", "warning");
7667
+ return false;
7668
+ }
7669
+ try {
7670
+ const payload = await fetchStudioJson("/local-preview-link", {
7671
+ query: { ...query, action: "resolve" },
7672
+ });
7673
+ const path = payload && typeof payload.path === "string" ? payload.path : "";
7674
+ if (!path) throw new Error("Studio did not return a PDF path.");
7675
+ if (!(await writeTextToClipboard(path))) throw new Error("Clipboard write failed.");
7676
+ flashStudioPdfActionFeedback(actionButton, "Copied ✓", "success");
7677
+ setStatus("Copied PDF path from the computer running Pi.", "success");
7678
+ return true;
7679
+ } catch (error) {
7680
+ flashStudioPdfActionFeedback(actionButton, "Failed", "warning");
7681
+ setStatus((error && error.message) ? error.message : "Could not copy this PDF's local path.", "warning");
7682
+ return false;
7683
+ }
7515
7684
  }
7516
7685
 
7517
7686
  function buildRefreshedStudioPdfViewerUrl(value) {
@@ -7638,6 +7807,10 @@
7638
7807
 
7639
7808
  function syncStudioPdfFocusFullscreenButton() {
7640
7809
  if (!studioPdfFocusFullscreenBtn) return;
7810
+ const available = isStudioFullscreenAvailable(studioPdfFocusDialogEl);
7811
+ studioPdfFocusFullscreenBtn.hidden = !available;
7812
+ studioPdfFocusFullscreenBtn.setAttribute("aria-hidden", available ? "false" : "true");
7813
+ if (!available) return;
7641
7814
  const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
7642
7815
  studioPdfFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
7643
7816
  const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
@@ -7696,6 +7869,16 @@
7696
7869
  studioPdfFocusFrameEl.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
7697
7870
  }
7698
7871
 
7872
+ function getStudioPdfCardResourceQuery(card) {
7873
+ return card && card.dataset
7874
+ ? normalizeStudioPdfResourceQuery({
7875
+ path: card.dataset.studioPdfPath || "",
7876
+ sourcePath: card.dataset.studioPdfSourcePath || "",
7877
+ resourceDir: card.dataset.studioPdfResourceDir || "",
7878
+ })
7879
+ : null;
7880
+ }
7881
+
7699
7882
  function openStudioPdfFocusFromButton(buttonEl) {
7700
7883
  if (!buttonEl) return false;
7701
7884
  const card = buttonEl.closest && buttonEl.closest(".studio-pdf-card");
@@ -7705,13 +7888,7 @@
7705
7888
  || String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
7706
7889
  || "PDF preview";
7707
7890
  const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
7708
- const resourceQuery = card && card.dataset
7709
- ? normalizeStudioPdfResourceQuery({
7710
- path: card.dataset.studioPdfPath || "",
7711
- sourcePath: card.dataset.studioPdfSourcePath || "",
7712
- resourceDir: card.dataset.studioPdfResourceDir || "",
7713
- })
7714
- : null;
7891
+ const resourceQuery = getStudioPdfCardResourceQuery(card);
7715
7892
  if (!viewerUrl) return false;
7716
7893
  openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, card);
7717
7894
  return true;
@@ -7731,6 +7908,73 @@
7731
7908
  }
7732
7909
  }
7733
7910
 
7911
+ function consumeStudioPreviewMediaEvent(event) {
7912
+ event.preventDefault();
7913
+ event.stopPropagation();
7914
+ if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
7915
+ }
7916
+
7917
+ function handleStudioPreviewMediaKeydown(event) {
7918
+ if (!event || (event.key !== "Enter" && event.key !== " ")) return;
7919
+ const target = event.target;
7920
+ const imageEl = target instanceof Element ? target.closest("img.studio-image-focus-target") : null;
7921
+ if (!imageEl) return;
7922
+ consumeStudioPreviewMediaEvent(event);
7923
+ if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
7924
+ }
7925
+
7926
+ function handleStudioPreviewMediaActivation(event) {
7927
+ const target = event && event.target;
7928
+ if (!(target instanceof Element)) return;
7929
+
7930
+ const imageEl = target.closest("img.studio-image-focus-target");
7931
+ if (imageEl) {
7932
+ consumeStudioPreviewMediaEvent(event);
7933
+ if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
7934
+ return;
7935
+ }
7936
+
7937
+ const pdfFigureEl = target.closest(".studio-pdf-preview-focus-target");
7938
+ if (pdfFigureEl && !target.closest("button, a")) {
7939
+ consumeStudioPreviewMediaEvent(event);
7940
+ if (!openPreviewPdfFigureInFocus(pdfFigureEl)) setStatus("Could not enlarge this PDF figure preview.", "warning");
7941
+ return;
7942
+ }
7943
+
7944
+ const actionBtn = target.closest(
7945
+ ".studio-pdf-card-system-viewer, .studio-pdf-card-reveal, .studio-pdf-card-copy-path, .studio-pdf-card-refresh, .studio-pdf-card-auto-refresh"
7946
+ );
7947
+ if (!actionBtn) return;
7948
+ const card = actionBtn.closest(".studio-pdf-card");
7949
+ if (!card) return;
7950
+ consumeStudioPreviewMediaEvent(event);
7951
+ const resourceQuery = getStudioPdfCardResourceQuery(card);
7952
+ if (actionBtn.classList.contains("studio-pdf-card-system-viewer")) {
7953
+ void runStudioPdfLocalAction("system-viewer", resourceQuery, actionBtn);
7954
+ return;
7955
+ }
7956
+ if (actionBtn.classList.contains("studio-pdf-card-reveal")) {
7957
+ void runStudioPdfLocalAction("reveal", resourceQuery, actionBtn);
7958
+ return;
7959
+ }
7960
+ if (actionBtn.classList.contains("studio-pdf-card-copy-path")) {
7961
+ void copyStudioPdfResourcePath(resourceQuery, actionBtn);
7962
+ return;
7963
+ }
7964
+ if (actionBtn.classList.contains("studio-pdf-card-refresh")) {
7965
+ const refreshed = refreshStudioPdfCard(card);
7966
+ flashStudioPdfActionFeedback(actionBtn, refreshed ? "Refreshed ✓" : "Failed", refreshed ? "success" : "warning");
7967
+ if (!refreshed) setStatus("Could not refresh this PDF preview.", "warning");
7968
+ return;
7969
+ }
7970
+ const state = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
7971
+ if (!state) {
7972
+ setStatus("Could not resolve this PDF for auto-refresh.", "warning");
7973
+ return;
7974
+ }
7975
+ setStudioPdfCardAutoRefresh(card, !state.enabled);
7976
+ }
7977
+
7734
7978
  function isStudioImageFocusOpen() {
7735
7979
  return Boolean(studioImageFocusOverlayEl && studioImageFocusOverlayEl.hidden === false);
7736
7980
  }
@@ -7883,6 +8127,10 @@
7883
8127
 
7884
8128
  function syncStudioImageFocusFullscreenButton() {
7885
8129
  if (!studioImageFocusFullscreenBtn) return;
8130
+ const available = isStudioFullscreenAvailable(studioImageFocusDialogEl);
8131
+ studioImageFocusFullscreenBtn.hidden = !available;
8132
+ studioImageFocusFullscreenBtn.setAttribute("aria-hidden", available ? "false" : "true");
8133
+ if (!available) return;
7886
8134
  const isFullscreen = Boolean(document.fullscreenElement && studioImageFocusDialogEl && document.fullscreenElement === studioImageFocusDialogEl);
7887
8135
  studioImageFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
7888
8136
  const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
@@ -8119,6 +8367,39 @@
8119
8367
  });
8120
8368
  }
8121
8369
 
8370
+ function openPreviewPdfFigureInFocus(previewEl) {
8371
+ const canvas = previewEl && typeof previewEl.querySelector === "function"
8372
+ ? previewEl.querySelector("canvas")
8373
+ : null;
8374
+ if (!(canvas instanceof HTMLCanvasElement)) return false;
8375
+ try {
8376
+ const dataUrl = canvas.toDataURL("image/png");
8377
+ const title = String(previewEl.getAttribute("title") || "PDF figure preview").trim() || "PDF figure preview";
8378
+ return openStudioImageFocusViewer(dataUrl, title);
8379
+ } catch {
8380
+ return false;
8381
+ }
8382
+ }
8383
+
8384
+ function decoratePreviewPdfFigures(targetEl) {
8385
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
8386
+ const previews = Array.from(targetEl.querySelectorAll(".studio-pdf-preview"));
8387
+ previews.forEach((previewEl) => {
8388
+ if (!(previewEl instanceof HTMLElement)) return;
8389
+ if (previewEl.dataset && previewEl.dataset.studioPdfFigureFocusDecorated === "1") return;
8390
+ if (!previewEl.querySelector("canvas")) return;
8391
+ previewEl.classList.add("studio-pdf-preview-focus-target");
8392
+ previewEl.title = "PDF figure preview (page 1). Click to enlarge.";
8393
+ if (previewEl.dataset) previewEl.dataset.studioPdfFigureFocusDecorated = "1";
8394
+
8395
+ previewEl.addEventListener("click", (event) => {
8396
+ if (event.target instanceof Element && event.target.closest("button, a")) return;
8397
+ event.preventDefault();
8398
+ if (!openPreviewPdfFigureInFocus(previewEl)) setStatus("Could not enlarge this PDF figure preview.", "warning");
8399
+ });
8400
+ });
8401
+ }
8402
+
8122
8403
  function createStudioPdfCard(block, useEditorResourceContext) {
8123
8404
  const options = block && block.options ? block.options : {};
8124
8405
  const path = String(options.path || "").trim();
@@ -8151,12 +8432,13 @@
8151
8432
  focusBtn.type = "button";
8152
8433
  focusBtn.className = "studio-pdf-card-action studio-pdf-card-focus";
8153
8434
  focusBtn.title = "Open this PDF in a larger Studio overlay.";
8154
- focusBtn.setAttribute("aria-label", "Focus PDF");
8435
+ focusBtn.setAttribute("aria-label", "Enlarge PDF");
8155
8436
  if (focusBtn.dataset) {
8156
8437
  focusBtn.dataset.studioPdfViewerUrl = viewerUrl;
8157
8438
  focusBtn.dataset.studioPdfTitle = title;
8158
8439
  }
8159
8440
  focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
8441
+ focusBtn.appendChild(document.createTextNode("Enlarge"));
8160
8442
  focusBtn.addEventListener("click", handleStudioPdfFocusButtonClick);
8161
8443
  titleGroup.appendChild(focusBtn);
8162
8444
  }
@@ -8183,11 +8465,11 @@
8183
8465
  systemViewerBtn.type = "button";
8184
8466
  systemViewerBtn.className = "studio-pdf-card-action studio-pdf-card-system-viewer";
8185
8467
  systemViewerBtn.textContent = "System viewer";
8186
- systemViewerBtn.title = "Open the local PDF in the operating system's default PDF viewer.";
8468
+ systemViewerBtn.title = "Open the local PDF in the default viewer on the computer running Pi.";
8187
8469
  systemViewerBtn.addEventListener("click", (event) => {
8188
8470
  event.preventDefault();
8189
8471
  event.stopPropagation();
8190
- void runStudioPdfLocalAction("system-viewer", resourceQuery);
8472
+ void runStudioPdfLocalAction("system-viewer", resourceQuery, systemViewerBtn);
8191
8473
  });
8192
8474
  actions.appendChild(systemViewerBtn);
8193
8475
 
@@ -8195,14 +8477,26 @@
8195
8477
  revealBtn.type = "button";
8196
8478
  revealBtn.className = "studio-pdf-card-action studio-pdf-card-reveal";
8197
8479
  revealBtn.textContent = "Show in folder";
8198
- revealBtn.title = "Reveal the local PDF in Finder or the system file manager.";
8480
+ revealBtn.title = "Reveal the local PDF in the file manager on the computer running Pi.";
8199
8481
  revealBtn.addEventListener("click", (event) => {
8200
8482
  event.preventDefault();
8201
8483
  event.stopPropagation();
8202
- void runStudioPdfLocalAction("reveal", resourceQuery);
8484
+ void runStudioPdfLocalAction("reveal", resourceQuery, revealBtn);
8203
8485
  });
8204
8486
  actions.appendChild(revealBtn);
8205
8487
 
8488
+ const copyPathBtn = document.createElement("button");
8489
+ copyPathBtn.type = "button";
8490
+ copyPathBtn.className = "studio-pdf-card-action studio-pdf-card-copy-path";
8491
+ copyPathBtn.textContent = "Copy path";
8492
+ copyPathBtn.title = "Copy the PDF path on the computer running Pi.";
8493
+ copyPathBtn.addEventListener("click", (event) => {
8494
+ event.preventDefault();
8495
+ event.stopPropagation();
8496
+ void copyStudioPdfResourcePath(resourceQuery, copyPathBtn);
8497
+ });
8498
+ actions.appendChild(copyPathBtn);
8499
+
8206
8500
  const refreshBtn = document.createElement("button");
8207
8501
  refreshBtn.type = "button";
8208
8502
  refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
@@ -9135,12 +9429,42 @@
9135
9429
  critiqueViewEl.addEventListener("click", handleFilesPaneClick);
9136
9430
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
9137
9431
  critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
9432
+ critiqueViewEl.addEventListener("click", (event) => { void handleSideQuestionClick(event); });
9433
+ critiqueViewEl.addEventListener("input", handleSideQuestionInput);
9434
+ critiqueViewEl.addEventListener("keydown", handleSideQuestionKeydown);
9435
+ critiqueViewEl.addEventListener("change", handleSideQuestionChange);
9138
9436
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
9139
9437
  critiqueViewEl.addEventListener("change", (event) => {
9140
9438
  void handleFilesPaneChange(event);
9141
9439
  });
9142
9440
  }
9143
9441
 
9442
+ function copyResponsePaneCanvasState(sourceEl, replacementEl) {
9443
+ if (!sourceEl || !replacementEl || typeof sourceEl.querySelectorAll !== "function" || typeof replacementEl.querySelectorAll !== "function") {
9444
+ return false;
9445
+ }
9446
+ const sourceCanvases = Array.from(sourceEl.querySelectorAll("canvas"));
9447
+ const replacementCanvases = Array.from(replacementEl.querySelectorAll("canvas"));
9448
+ if (sourceCanvases.length !== replacementCanvases.length) return false;
9449
+
9450
+ for (let index = 0; index < sourceCanvases.length; index += 1) {
9451
+ const sourceCanvas = sourceCanvases[index];
9452
+ const replacementCanvas = replacementCanvases[index];
9453
+ if (!(sourceCanvas instanceof HTMLCanvasElement) || !(replacementCanvas instanceof HTMLCanvasElement)) return false;
9454
+ if (sourceCanvas.width < 1 || sourceCanvas.height < 1) continue;
9455
+ try {
9456
+ replacementCanvas.width = sourceCanvas.width;
9457
+ replacementCanvas.height = sourceCanvas.height;
9458
+ const context = replacementCanvas.getContext("2d");
9459
+ if (!context) return false;
9460
+ context.drawImage(sourceCanvas, 0, 0);
9461
+ } catch {
9462
+ return false;
9463
+ }
9464
+ }
9465
+ return true;
9466
+ }
9467
+
9144
9468
  function replaceResponsePaneWithClone() {
9145
9469
  const currentEl = critiqueViewEl;
9146
9470
  if (!currentEl || !currentEl.parentNode || typeof currentEl.cloneNode !== "function") {
@@ -9148,7 +9472,7 @@
9148
9472
  }
9149
9473
 
9150
9474
  const replacement = currentEl.cloneNode(true);
9151
- if (!replacement || replacement.nodeType !== 1) {
9475
+ if (!replacement || replacement.nodeType !== 1 || !copyResponsePaneCanvasState(currentEl, replacement)) {
9152
9476
  return currentEl;
9153
9477
  }
9154
9478
 
@@ -9160,7 +9484,7 @@
9160
9484
 
9161
9485
  function applyPendingResponseScrollReset() {
9162
9486
  if (!pendingResponseScrollReset || !critiqueViewEl) return false;
9163
- if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return false;
9487
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview" || rightView === "side-questions") return false;
9164
9488
 
9165
9489
  pendingResponseScrollReset = false;
9166
9490
  let targetEl = replaceResponsePaneWithClone();
@@ -9169,7 +9493,7 @@
9169
9493
  : (cb) => window.setTimeout(cb, 16);
9170
9494
  const resetScroll = () => {
9171
9495
  if (!targetEl || !targetEl.isConnected) return;
9172
- if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return;
9496
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview" || rightView === "side-questions") return;
9173
9497
  targetEl.scrollTop = 0;
9174
9498
  targetEl.scrollLeft = 0;
9175
9499
  };
@@ -9427,8 +9751,10 @@
9427
9751
  async function exportRightPanePdf(options) {
9428
9752
  const exportOptions = options && typeof options === "object" ? options : {};
9429
9753
  const openTarget = exportOptions.openTarget === "studio" ? "studio" : "default";
9754
+ const exportingSideThread = rightView === "side-questions";
9755
+ const sideThreadExportedAt = exportingSideThread ? new Date() : null;
9430
9756
  let studioLaunch = null;
9431
- if (uiBusy || previewExportInProgress) {
9757
+ if ((!exportingSideThread && uiBusy) || previewExportInProgress || sideQuestionMarkdownExportRequest) {
9432
9758
  setStatus("Studio is busy.", "warning");
9433
9759
  return;
9434
9760
  }
@@ -9441,8 +9767,8 @@
9441
9767
 
9442
9768
  const exportingReplJournal = rightView === "repl";
9443
9769
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
9444
- if (!rightPaneShowsPreview && !exportingReplJournal) {
9445
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
9770
+ if (!rightPaneShowsPreview && !exportingReplJournal && !exportingSideThread) {
9771
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export PDF.", "warning");
9446
9772
  return;
9447
9773
  }
9448
9774
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
@@ -9451,7 +9777,7 @@
9451
9777
  return;
9452
9778
  }
9453
9779
 
9454
- const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
9780
+ const htmlArtifactSource = exportingReplJournal || exportingSideThread ? "" : getRightPaneHtmlArtifactSource();
9455
9781
  if (htmlArtifactSource) {
9456
9782
  setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
9457
9783
  return;
@@ -9459,24 +9785,34 @@
9459
9785
 
9460
9786
  const markdown = exportingReplJournal
9461
9787
  ? buildReplJournalMarkdown(replJournalExportEntries)
9462
- : (rightView === "editor-preview"
9463
- ? prepareEditorTextForPdfExport(sourceTextEl.value)
9464
- : prepareEditorTextForPreview(latestResponseMarkdown));
9788
+ : (exportingSideThread
9789
+ ? buildCurrentSideQuestionTranscriptMarkdown(sideThreadExportedAt)
9790
+ : (rightView === "editor-preview"
9791
+ ? prepareEditorTextForPdfExport(sourceTextEl.value)
9792
+ : prepareEditorTextForPreview(latestResponseMarkdown)));
9465
9793
  if (!markdown || !markdown.trim()) {
9466
9794
  setStatus("Nothing to export yet.", "warning");
9467
9795
  return;
9468
9796
  }
9797
+ if (exportingSideThread && markdown.length > SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS) {
9798
+ setStatus("This side-thread transcript is too large for PDF export. Save or copy the Markdown instead.", "warning");
9799
+ return;
9800
+ }
9469
9801
 
9470
9802
  const effectivePath = getEffectiveSavePath();
9471
- const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
9803
+ const sourcePath = exportingReplJournal || exportingSideThread ? "" : (effectivePath || sourceState.path || "");
9472
9804
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
9473
9805
  const isEditorPreview = rightView === "editor-preview";
9474
9806
  const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
9475
9807
  const editorPdfLanguage = isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "";
9476
- const isLatex = isEditorPreview
9477
- ? editorPdfLanguage === "latex"
9478
- : /\\documentclass\b|\\begin\{document\}/.test(markdown);
9479
- let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.pdf"));
9808
+ const isLatex = exportingSideThread
9809
+ ? false
9810
+ : (isEditorPreview
9811
+ ? editorPdfLanguage === "latex"
9812
+ : /\\documentclass\b|\\begin\{document\}/.test(markdown));
9813
+ let filenameHint = exportingSideThread
9814
+ ? getSideQuestionTranscriptFilename(sideThreadExportedAt).replace(/\.md$/i, ".pdf")
9815
+ : (exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.pdf")));
9480
9816
  if (sourcePath) {
9481
9817
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
9482
9818
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -9493,7 +9829,9 @@
9493
9829
  }
9494
9830
  previewExportInProgress = true;
9495
9831
  updateResultActionButtons();
9496
- setStatus(openTarget === "studio" ? "Exporting PDF for Studio…" : "Exporting PDF…", "warning");
9832
+ setStatus(exportingSideThread
9833
+ ? (openTarget === "studio" ? "Exporting side thread as PDF for Studio…" : "Exporting side thread as PDF…")
9834
+ : (openTarget === "studio" ? "Exporting PDF for Studio…" : "Exporting PDF…"), "warning");
9497
9835
 
9498
9836
  try {
9499
9837
  const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
@@ -9658,8 +9996,10 @@
9658
9996
  async function exportRightPaneHtml(options) {
9659
9997
  const exportOptions = options && typeof options === "object" ? options : {};
9660
9998
  const openTarget = exportOptions.openTarget === "studio" ? "studio" : "browser";
9999
+ const exportingSideThread = rightView === "side-questions";
10000
+ const sideThreadExportedAt = exportingSideThread ? new Date() : null;
9661
10001
  let studioLaunch = null;
9662
- if (uiBusy || previewExportInProgress) {
10002
+ if ((!exportingSideThread && uiBusy) || previewExportInProgress || sideQuestionMarkdownExportRequest) {
9663
10003
  setStatus("Studio is busy.", "warning");
9664
10004
  return;
9665
10005
  }
@@ -9672,8 +10012,8 @@
9672
10012
 
9673
10013
  const exportingReplJournal = rightView === "repl";
9674
10014
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
9675
- if (!rightPaneShowsPreview && !exportingReplJournal) {
9676
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
10015
+ if (!rightPaneShowsPreview && !exportingReplJournal && !exportingSideThread) {
10016
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export HTML.", "warning");
9677
10017
  return;
9678
10018
  }
9679
10019
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
@@ -9682,26 +10022,36 @@
9682
10022
  return;
9683
10023
  }
9684
10024
 
9685
- const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
9686
- const markdown = exportingReplJournal ? buildReplJournalMarkdown(replJournalExportEntries) : (htmlArtifactSource || (rightView === "editor-preview"
9687
- ? prepareEditorTextForHtmlExport(sourceTextEl.value)
9688
- : prepareEditorTextForPreview(latestResponseMarkdown)));
10025
+ const htmlArtifactSource = exportingReplJournal || exportingSideThread ? "" : getRightPaneHtmlArtifactSource();
10026
+ const markdown = exportingReplJournal
10027
+ ? buildReplJournalMarkdown(replJournalExportEntries)
10028
+ : (exportingSideThread
10029
+ ? buildCurrentSideQuestionTranscriptMarkdown(sideThreadExportedAt)
10030
+ : (htmlArtifactSource || (rightView === "editor-preview"
10031
+ ? prepareEditorTextForHtmlExport(sourceTextEl.value)
10032
+ : prepareEditorTextForPreview(latestResponseMarkdown))));
9689
10033
  if (!markdown || !markdown.trim()) {
9690
10034
  setStatus("Nothing to export yet.", "warning");
9691
10035
  return;
9692
10036
  }
10037
+ if (exportingSideThread && markdown.length > SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS) {
10038
+ setStatus("This side-thread transcript is too large for HTML export. Save or copy the Markdown instead.", "warning");
10039
+ return;
10040
+ }
9693
10041
 
9694
10042
  const effectivePath = getEffectiveSavePath();
9695
- const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
10043
+ const sourcePath = exportingReplJournal || exportingSideThread ? "" : (effectivePath || sourceState.path || "");
9696
10044
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
9697
10045
  const isEditorPreview = rightView === "editor-preview";
9698
10046
  const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
9699
10047
  const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "");
9700
- const isLatex = htmlArtifactSource ? false : (isEditorPreview
10048
+ const isLatex = exportingSideThread ? false : (htmlArtifactSource ? false : (isEditorPreview
9701
10049
  ? editorHtmlLanguage === "latex"
9702
- : /\\documentclass\b|\\begin\{document\}/.test(markdown));
9703
- let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html"));
9704
- let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
10050
+ : /\\documentclass\b|\\begin\{document\}/.test(markdown)));
10051
+ let filenameHint = exportingSideThread
10052
+ ? getSideQuestionTranscriptFilename(sideThreadExportedAt).replace(/\.md$/i, ".html")
10053
+ : (exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html")));
10054
+ let titleHint = exportingSideThread ? "Pi Studio side questions" : (exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview"));
9705
10055
  if (sourcePath) {
9706
10056
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
9707
10057
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -9719,7 +10069,9 @@
9719
10069
  }
9720
10070
  previewExportInProgress = true;
9721
10071
  updateResultActionButtons();
9722
- setStatus(openTarget === "studio" ? "Exporting HTML for Studio…" : "Exporting HTML…", "warning");
10072
+ setStatus(exportingSideThread
10073
+ ? (openTarget === "studio" ? "Exporting side thread as HTML for Studio…" : "Exporting side thread as HTML…")
10074
+ : (openTarget === "studio" ? "Exporting HTML for Studio…" : "Exporting HTML…"), "warning");
9723
10075
 
9724
10076
  try {
9725
10077
  const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
@@ -9894,6 +10246,19 @@
9894
10246
 
9895
10247
  function exportRightPaneFormat(format) {
9896
10248
  closeExportPreviewMenu();
10249
+ if (String(format || "").startsWith("side-markdown-") && rightView !== "side-questions") {
10250
+ setStatus("Side-thread Markdown export is available only in Side questions.", "warning");
10251
+ return false;
10252
+ }
10253
+ if (format === "side-markdown-save") {
10254
+ return saveSideQuestionTranscriptMarkdown();
10255
+ }
10256
+ if (format === "side-markdown-copy") {
10257
+ return copySideQuestionTranscriptMarkdown();
10258
+ }
10259
+ if (format === "side-markdown-editor") {
10260
+ return openSideQuestionTranscriptInEditor();
10261
+ }
9897
10262
  if (format === "html-studio") {
9898
10263
  return exportRightPaneHtml({ openTarget: "studio" });
9899
10264
  }
@@ -10125,6 +10490,7 @@
10125
10490
  await renderAnnotationMathInElement(targetEl);
10126
10491
  decoratePdfEmbeds(targetEl);
10127
10492
  await renderPdfPreviewsInElement(targetEl);
10493
+ decoratePreviewPdfFigures(targetEl);
10128
10494
  const annotationMode = (pane === "source" || pane === "response")
10129
10495
  ? (annotationsEnabled ? "highlight" : "hide")
10130
10496
  : "none";
@@ -11773,10 +12139,651 @@
11773
12139
  }
11774
12140
  }
11775
12141
 
12142
+ function isSideQuestionConnectionReady() {
12143
+ return Boolean(ws && ws.readyState === WebSocket.OPEN);
12144
+ }
12145
+
12146
+ function getSideQuestionSelectedResponseText() {
12147
+ const selected = getSelectedHistoryItem();
12148
+ return selected && typeof selected.markdown === "string" ? selected.markdown : latestResponseMarkdown;
12149
+ }
12150
+
12151
+ function getSideQuestionGatherScope() {
12152
+ if (sideQuestionUi.gatherScope) return sideQuestionUi.gatherScope;
12153
+ return sideQuestionHelpers.getDefaultStudioSideQuestionGatherScope({
12154
+ sourcePath: getEffectiveSavePath() || sourceState.path || "",
12155
+ resourceDir: getCurrentResourceDirValue(),
12156
+ });
12157
+ }
12158
+
12159
+ function getSideQuestionFocus() {
12160
+ return sideQuestionHelpers.chooseStudioSideQuestionFocus({
12161
+ mode: sideQuestionUi.focusMode,
12162
+ editorText: sourceTextEl.value || "",
12163
+ responseText: getSideQuestionSelectedResponseText(),
12164
+ selectionStart: sourceTextEl.selectionStart,
12165
+ selectionEnd: sourceTextEl.selectionEnd,
12166
+ language: editorLanguage,
12167
+ });
12168
+ }
12169
+
12170
+ function persistSideQuestionToolSelection() {
12171
+ try {
12172
+ if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_TOOLS_STORAGE_KEY, JSON.stringify(sideQuestionUi.toolIds.slice(0, 12)));
12173
+ } catch {}
12174
+ }
12175
+
12176
+ function applySideQuestionToolCatalog(value) {
12177
+ const seen = new Set();
12178
+ sideQuestionAvailablePiTools = (Array.isArray(value) ? value : []).flatMap((entry) => {
12179
+ if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.name !== "string") return [];
12180
+ const id = entry.id.trim().toLowerCase();
12181
+ const name = entry.name.trim();
12182
+ if (!/^[a-f0-9]{24}$/.test(id) || !name || name.length > 200 || seen.has(id)) return [];
12183
+ seen.add(id);
12184
+ return [{
12185
+ id,
12186
+ name,
12187
+ description: typeof entry.description === "string" ? entry.description.trim().slice(0, 1000) : "",
12188
+ source: typeof entry.source === "string" ? entry.source.trim().slice(0, 500) : "Extension tool",
12189
+ gateway: entry.gateway === true,
12190
+ }];
12191
+ }).slice(0, 200);
12192
+ const available = new Set(sideQuestionAvailablePiTools.map((tool) => tool.id));
12193
+ const nextSelection = sideQuestionUi.toolIds.filter((id) => available.has(id)).slice(0, 12);
12194
+ if (nextSelection.length !== sideQuestionUi.toolIds.length || nextSelection.some((id, index) => id !== sideQuestionUi.toolIds[index])) {
12195
+ sideQuestionUi.toolIds = nextSelection;
12196
+ persistSideQuestionToolSelection();
12197
+ }
12198
+ }
12199
+
12200
+ function normalizeSideQuestionState(value) {
12201
+ const state = value && typeof value === "object" ? value : {};
12202
+ const messages = Array.isArray(state.messages) ? state.messages.map((entry) => ({
12203
+ id: typeof entry.id === "string" ? entry.id : makeRequestId(),
12204
+ role: entry.role === "assistant" ? "assistant" : "user",
12205
+ text: typeof entry.text === "string" ? entry.text : "",
12206
+ createdAt: typeof entry.createdAt === "number" ? entry.createdAt : Date.now(),
12207
+ status: entry.status === "streaming" || entry.status === "error" ? entry.status : "complete",
12208
+ })).slice(-24) : [];
12209
+ const activity = Array.isArray(state.activity) ? state.activity.map((entry) => ({
12210
+ id: typeof entry.id === "string" ? entry.id : makeRequestId(),
12211
+ toolName: typeof entry.toolName === "string" ? entry.toolName : "tool",
12212
+ label: typeof entry.label === "string" ? entry.label : "Gathering context",
12213
+ status: entry.status === "running" || entry.status === "error" ? entry.status : "complete",
12214
+ })).slice(-40) : [];
12215
+ const context = state.context && typeof state.context === "object" ? {
12216
+ focusKind: String(state.context.focusKind || "editor"),
12217
+ focusLabel: String(state.context.focusLabel || "Studio editor context"),
12218
+ gatherScope: String(state.context.gatherScope || "none"),
12219
+ contextRoot: String(state.context.contextRoot || ""),
12220
+ includeConversation: state.context.includeConversation === true,
12221
+ gitContextRequested: state.context.gitContextRequested === true,
12222
+ gitSnapshot: state.context.gitSnapshot && typeof state.context.gitSnapshot === "object" ? {
12223
+ capturedAt: Number.isFinite(state.context.gitSnapshot.capturedAt) ? state.context.gitSnapshot.capturedAt : null,
12224
+ branch: String(state.context.gitSnapshot.branch || ""),
12225
+ head: String(state.context.gitSnapshot.head || ""),
12226
+ changeCount: Number.isFinite(state.context.gitSnapshot.changeCount) ? Math.max(0, Math.floor(state.context.gitSnapshot.changeCount)) : 0,
12227
+ recentCommitCount: Number.isFinite(state.context.gitSnapshot.recentCommitCount) ? Math.max(0, Math.floor(state.context.gitSnapshot.recentCommitCount)) : 0,
12228
+ truncated: state.context.gitSnapshot.truncated === true,
12229
+ } : null,
12230
+ webSearchRequested: state.context.webSearchRequested === true,
12231
+ webSearchAvailable: state.context.webSearchAvailable === true,
12232
+ tools: Array.isArray(state.context.tools) ? state.context.tools.flatMap((tool) => {
12233
+ if (!tool || typeof tool !== "object" || typeof tool.name !== "string") return [];
12234
+ return [{
12235
+ id: typeof tool.id === "string" ? tool.id : "",
12236
+ name: tool.name,
12237
+ description: typeof tool.description === "string" ? tool.description : "",
12238
+ source: typeof tool.source === "string" ? tool.source : "Extension tool",
12239
+ gateway: tool.gateway === true,
12240
+ }];
12241
+ }).slice(0, 12) : [],
12242
+ } : null;
12243
+ return {
12244
+ threadId: typeof state.threadId === "string" && state.threadId ? state.threadId : null,
12245
+ status: state.status === "running" || state.status === "error" ? state.status : "idle",
12246
+ requestId: typeof state.requestId === "string" ? state.requestId : null,
12247
+ createdAt: Number.isFinite(state.createdAt) ? state.createdAt : null,
12248
+ updatedAt: Number.isFinite(state.updatedAt) ? state.updatedAt : null,
12249
+ context,
12250
+ modelLabel: String(state.modelLabel || ""),
12251
+ thinking: String(state.thinking || "low"),
12252
+ messages,
12253
+ activity,
12254
+ error: String(state.error || ""),
12255
+ };
12256
+ }
12257
+
12258
+ function getLatestCompletedSideQuestionAnswer() {
12259
+ if (!sideQuestionState || !Array.isArray(sideQuestionState.messages)) return null;
12260
+ return [...sideQuestionState.messages].reverse().find((entry) => entry.role === "assistant" && entry.status === "complete" && entry.text.trim()) || null;
12261
+ }
12262
+
12263
+ function canExportSideQuestionTranscript() {
12264
+ return Boolean(
12265
+ sideQuestionState
12266
+ && sideQuestionState.threadId
12267
+ && sideQuestionState.status !== "running"
12268
+ && Array.isArray(sideQuestionState.messages)
12269
+ && sideQuestionState.messages.length > 0
12270
+ );
12271
+ }
12272
+
12273
+ function buildCurrentSideQuestionTranscriptMarkdown(exportedAt) {
12274
+ if (!canExportSideQuestionTranscript()) return "";
12275
+ return sideQuestionHelpers.buildStudioSideQuestionTranscriptMarkdown(sideQuestionState, {
12276
+ exportedAt: exportedAt instanceof Date ? exportedAt : new Date(),
12277
+ });
12278
+ }
12279
+
12280
+ function getSideQuestionTranscriptFilename(date) {
12281
+ return sideQuestionHelpers.formatStudioSideQuestionTranscriptFilename(date instanceof Date ? date : new Date());
12282
+ }
12283
+
12284
+ function getSideQuestionTranscriptSuggestedPath(date) {
12285
+ const filename = getSideQuestionTranscriptFilename(date);
12286
+ const contextRoot = sideQuestionState && sideQuestionState.context ? String(sideQuestionState.context.contextRoot || "") : "";
12287
+ const directory = getCurrentResourceDirValue() || contextRoot || ".";
12288
+ return directory.replace(/[\\/]$/, "") + "/" + filename;
12289
+ }
12290
+
12291
+ function getSideQuestionEditorLineRange(focus) {
12292
+ if (!focus || !Number.isFinite(focus.start) || !Number.isFinite(focus.end)) return "";
12293
+ const source = sourceTextEl.value || "";
12294
+ const start = Math.max(0, Math.min(source.length, Math.floor(focus.start)));
12295
+ const end = Math.max(start, Math.min(source.length, Math.floor(focus.end)));
12296
+ const firstLine = source.slice(0, start).split("\n").length;
12297
+ const lastOffset = end > start ? end - 1 : start;
12298
+ const lastLine = source.slice(0, lastOffset).split("\n").length;
12299
+ return firstLine === lastLine ? "line " + firstLine : "lines " + firstLine + "–" + lastLine;
12300
+ }
12301
+
12302
+ function getSideQuestionContextSummary() {
12303
+ const focus = getSideQuestionFocus();
12304
+ const scope = getSideQuestionGatherScope();
12305
+ const sourcePath = getEffectiveSavePath() || sourceState.path || "";
12306
+ const resourceDir = getCurrentResourceDirValue();
12307
+ const rootHint = scope === "custom"
12308
+ ? sideQuestionUi.customPath
12309
+ : (scope === "repo"
12310
+ ? "Current repository"
12311
+ : (sourcePath ? dirnameForDisplayPath(sourcePath) : (resourceDir || "current Pi working directory")));
12312
+ const lineRange = getSideQuestionEditorLineRange(focus);
12313
+ const attachment = focus.focusKind === "none"
12314
+ ? focus.focusLabel
12315
+ : focus.focusLabel + (lineRange ? " · " + lineRange : "") + " · " + String(focus.focusText.length).toLocaleString("en-US") + " chars";
12316
+ return {
12317
+ focus,
12318
+ scope,
12319
+ rootHint,
12320
+ sourcePath,
12321
+ resourceDir,
12322
+ attachmentText: attachment,
12323
+ relatedFilesText: scope === "none" ? "None" : (rootHint || scope) + " · read only as needed",
12324
+ gitContextText: scope === "repo" && sideQuestionUi.gitContext
12325
+ ? "Status, staged and unstaged changes, and up to 20 recent commits · read only · frozen when thread starts"
12326
+ : "",
12327
+ };
12328
+ }
12329
+
12330
+ function buildSideQuestionContextPayload() {
12331
+ const summary = getSideQuestionContextSummary();
12332
+ return {
12333
+ focusKind: summary.focus.focusKind,
12334
+ focusLabel: summary.focus.focusLabel,
12335
+ focusText: summary.focus.focusText,
12336
+ sourcePath: summary.sourcePath || undefined,
12337
+ resourceDir: summary.resourceDir || undefined,
12338
+ gatherScope: summary.scope,
12339
+ contextPath: summary.scope === "custom" ? sideQuestionUi.customPath.trim() : undefined,
12340
+ includeConversation: sideQuestionUi.includeConversation,
12341
+ gitContext: summary.scope === "repo" && sideQuestionUi.gitContext,
12342
+ webSearch: sideQuestionUi.webSearch && sideQuestionWebSearchAvailable,
12343
+ toolIds: sideQuestionUi.toolIds.slice(0, 12),
12344
+ thinking: sideQuestionUi.thinking,
12345
+ };
12346
+ }
12347
+
12348
+ async function renderSideQuestionMarkdownToHtml(markdown) {
12349
+ const source = String(markdown || "");
12350
+ if (sideQuestionMarkdownRenderCache.has(source)) return sideQuestionMarkdownRenderCache.get(source);
12351
+ const renderedHtml = await renderMarkdownWithPandoc(source, { includeEditorLanguage: false });
12352
+ const sanitized = sanitizeRenderedHtml(renderedHtml, source, { stripMarkdownHtmlComments: true });
12353
+ sideQuestionMarkdownRenderCache.set(source, sanitized);
12354
+ while (sideQuestionMarkdownRenderCache.size > 60) {
12355
+ const firstKey = sideQuestionMarkdownRenderCache.keys().next().value;
12356
+ if (!firstKey) break;
12357
+ sideQuestionMarkdownRenderCache.delete(firstKey);
12358
+ }
12359
+ return sanitized;
12360
+ }
12361
+
12362
+ async function renderSideQuestionMarkdownFields(nonce) {
12363
+ if (!critiqueViewEl || rightView !== "side-questions") return;
12364
+ const targets = Array.from(critiqueViewEl.querySelectorAll("[data-side-question-markdown]")).filter((target) => target instanceof HTMLElement);
12365
+ for (const target of targets) {
12366
+ const markdown = target.getAttribute("data-side-question-markdown") || "";
12367
+ if (!markdown.trim()) continue;
12368
+ try {
12369
+ const html = await renderSideQuestionMarkdownToHtml(markdown);
12370
+ if (nonce !== sideQuestionPreviewRenderNonce || rightView !== "side-questions" || !critiqueViewEl.contains(target)) return;
12371
+ target.innerHTML = html;
12372
+ await renderAnnotationMathInElement(target);
12373
+ await renderMermaidInElement(target);
12374
+ await renderMathFallbackInElement(target);
12375
+ decorateCopyablePreviewBlocks(target);
12376
+ decoratePreviewImages(target);
12377
+ } catch (error) {
12378
+ console.error("Side-question markdown preview failed:", error);
12379
+ target.classList.add("side-question-markdown-failed");
12380
+ }
12381
+ }
12382
+ }
12383
+
12384
+ function sideQuestionSelectOptions(values, current) {
12385
+ return values.map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (value === current ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
12386
+ }
12387
+
12388
+ function renderSideQuestionPiToolPicker() {
12389
+ const selected = new Set(sideQuestionUi.toolIds);
12390
+ const selectedCount = sideQuestionUi.toolIds.length;
12391
+ if (!sideQuestionAvailablePiTools.length) {
12392
+ return "<div class='side-question-tool-empty'><strong>Additional Pi tools</strong><span>No eligible extension tools are currently available.</span></div>";
12393
+ }
12394
+ const groups = new Map();
12395
+ for (const tool of sideQuestionAvailablePiTools) {
12396
+ if (!groups.has(tool.source)) groups.set(tool.source, []);
12397
+ groups.get(tool.source).push(tool);
12398
+ }
12399
+ const groupHtml = [...groups.entries()].map(([source, tools]) => {
12400
+ const toolHtml = tools.map((tool) => {
12401
+ const description = tool.description || "No description supplied by this extension.";
12402
+ return "<label class='side-question-tool-option' title='" + escapeHtml(description) + "'>"
12403
+ + "<input type='checkbox' data-side-question-tool='" + escapeHtml(tool.id) + "' data-side-question-tool-name='" + escapeHtml(tool.name) + "'" + (selected.has(tool.id) ? " checked" : "") + ">"
12404
+ + "<span><code>" + escapeHtml(tool.name) + "</code><small>" + escapeHtml(description) + "</small></span>"
12405
+ + (tool.gateway ? "<em>gateway</em>" : "")
12406
+ + "</label>";
12407
+ }).join("");
12408
+ return "<section class='side-question-tool-group'><h3>" + escapeHtml(source) + "</h3>" + toolHtml + "</section>";
12409
+ }).join("");
12410
+ return "<details class='side-question-tool-picker'" + (selectedCount ? " open" : "") + ">"
12411
+ + "<summary>Additional Pi tools · " + selectedCount + " selected</summary>"
12412
+ + "<p>Selecting a tool loads its owning extension into the isolated side runtime. Selection is remembered, then frozen; a gateway may expose further configured services.</p>"
12413
+ + "<div class='side-question-tool-groups'>" + groupHtml + "</div>"
12414
+ + "</details>";
12415
+ }
12416
+
12417
+ function renderSideQuestionSetup() {
12418
+ const summary = getSideQuestionContextSummary();
12419
+ const scope = summary.scope;
12420
+ const webDisabled = !sideQuestionWebSearchAvailable;
12421
+ return "<div class='side-question-empty'>"
12422
+ + "<div class='side-question-intro'><h2>Side question</h2><p>Ask something without adding it to the main Pi conversation. Choose the starting text and whether Studio may look through related files.</p></div>"
12423
+ + "<div class='side-question-context-grid'>"
12424
+ + "<label>Starting text<select data-side-question-field='focusMode' aria-describedby='sideQuestionContextRule'>" + sideQuestionSelectOptions([
12425
+ ["auto", "Automatic"], ["selection", "Editor selection only"], ["section", "Heading block or nearby text at cursor"], ["editor", "Whole editor document"], ["response", "Displayed response"], ["none", "No starting text"],
12426
+ ], sideQuestionUi.focusMode) + "</select></label>"
12427
+ + "<label>Also use files from<select data-side-question-field='gatherScope'>" + sideQuestionSelectOptions([
12428
+ ["none", "No other files"], ["folder", "Same folder as document"], ["repo", "Repository"], ["custom", "Choose a folder"],
12429
+ ], scope) + "</select></label>"
12430
+ + "<label>Thinking<select data-side-question-field='thinking'>" + sideQuestionSelectOptions([
12431
+ ["off", "Off"], ["minimal", "Minimal"], ["low", "Low"], ["medium", "Medium"], ["high", "High"],
12432
+ ], sideQuestionUi.thinking) + "</select></label>"
12433
+ + "</div>"
12434
+ + "<details class='side-question-context-rule'><summary id='sideQuestionContextRule'>Automatic: selection → heading block at cursor → nearby text</summary><p>A heading block starts at the nearest Markdown/LaTeX heading above the cursor and ends before the next heading of the same or higher level. With no heading, Studio uses the surrounding text block or a nearby excerpt.</p></details>"
12435
+ + (scope === "custom" ? "<label class='side-question-path-label'>Folder path<input data-side-question-field='customPath' type='text' value='" + escapeHtml(sideQuestionUi.customPath) + "' placeholder='Folder on the computer running Pi'></label>" : "")
12436
+ + "<div class='side-question-checks'>"
12437
+ + "<label><input data-side-question-field='includeConversation' type='checkbox'" + (sideQuestionUi.includeConversation ? " checked" : "") + "> Include the current main conversation snapshot</label>"
12438
+ + (scope === "repo" ? "<label title='Capture Git status, staged and unstaged changes, and up to 20 recent commits when this thread starts.'><input data-side-question-field='gitContext' type='checkbox'" + (sideQuestionUi.gitContext ? " checked" : "") + "> Include Git context</label>" : "")
12439
+ + "<label title='" + (webDisabled ? "Set BRAVE_API_KEY before starting Pi to enable web search." : "Allow this side thread to search the web when useful.") + "'><input data-side-question-field='webSearch' type='checkbox'" + (sideQuestionUi.webSearch ? " checked" : "") + (webDisabled ? " disabled" : "") + "> Allow web search" + (webDisabled ? " (unavailable)" : "") + "</label>"
12440
+ + "</div>"
12441
+ + renderSideQuestionPiToolPicker()
12442
+ + "<dl class='side-question-context-summary'><div><dt>Starting text</dt><dd>" + escapeHtml(summary.attachmentText) + "</dd></div><div><dt>Related files</dt><dd>" + escapeHtml(summary.relatedFilesText) + "</dd></div>" + (summary.gitContextText ? "<div><dt>Git context</dt><dd>" + escapeHtml(summary.gitContextText) + "</dd></div>" : "") + "</dl>"
12443
+ + "<label class='side-question-composer-label'>Question<textarea data-side-question-field='draft' rows='4' title='Enter adds a new line. Cmd/Ctrl+Enter asks the side question.' placeholder='Ask about the starting text or anything you want checked…'>" + escapeHtml(sideQuestionUi.draft) + "</textarea></label>"
12444
+ + (sideQuestionState && sideQuestionState.error ? "<div class='side-question-error'>" + escapeHtml(sideQuestionState.error) + "</div>" : "")
12445
+ + "<div class='side-question-actions'><button type='button' class='side-question-primary' data-side-question-action='ask' aria-keyshortcuts='Meta+Enter Control+Enter' title='Ask side question (Cmd/Ctrl+Enter)'" + (!isSideQuestionConnectionReady() || (sideQuestionState && sideQuestionState.status === "running") || !sideQuestionUi.draft.trim() || (scope === "custom" && !sideQuestionUi.customPath.trim()) ? " disabled" : "") + ">" + (sideQuestionState && sideQuestionState.status === "running" ? "Preparing side thread…" : "Ask side question") + "</button></div>"
12446
+ + "</div>";
12447
+ }
12448
+
12449
+ function renderSideQuestionThread() {
12450
+ const state = sideQuestionState;
12451
+ const context = state.context || {};
12452
+ const latestAnswer = getLatestCompletedSideQuestionAnswer();
12453
+ const messageHtml = state.messages.map((message) => {
12454
+ const roleLabel = message.role === "user" ? "You" : "Side answer";
12455
+ const body = message.role === "assistant" && message.status === "complete"
12456
+ ? "<div class='side-question-message-body rendered-markdown' data-side-question-markdown='" + escapeHtml(message.text) + "'><div class='side-question-markdown-fallback'>" + escapeHtml(message.text) + "</div></div>"
12457
+ : "<div class='side-question-message-body side-question-plain'>" + escapeHtml(message.text || (message.status === "streaming" ? "Thinking…" : "")) + "</div>";
12458
+ return "<article class='side-question-message side-question-message-" + message.role + " side-question-status-" + message.status + "'>"
12459
+ + "<div class='side-question-message-label'>" + roleLabel + (message.status === "streaming" ? " <span class='side-question-live'>●</span>" : "") + "</div>" + body + "</article>";
12460
+ }).join("");
12461
+ const activityHtml = state.activity.length
12462
+ ? "<details class='side-question-activity'" + (state.status === "running" ? " open" : "") + "><summary>Gathered context · " + state.activity.length + " action" + (state.activity.length === 1 ? "" : "s") + "</summary><ul>"
12463
+ + state.activity.map((entry) => "<li class='side-question-activity-" + entry.status + "'><span>" + (entry.status === "running" ? "●" : (entry.status === "error" ? "!" : "✓")) + "</span>" + escapeHtml(entry.label) + "</li>").join("") + "</ul></details>"
12464
+ : "";
12465
+ const webLabel = context.webSearchRequested
12466
+ ? (context.webSearchAvailable ? "web allowed" : "web unavailable")
12467
+ : "web off";
12468
+ const selectedToolLabel = Array.isArray(context.tools) && context.tools.length
12469
+ ? "Additional Pi tools: " + context.tools.map((tool) => tool.name).join(", ")
12470
+ : "Additional Pi tools off";
12471
+ const gitSnapshot = context.gitSnapshot && typeof context.gitSnapshot === "object" ? context.gitSnapshot : null;
12472
+ const gitCapturedLabel = gitSnapshot && gitSnapshot.capturedAt ? formatReferenceTime(gitSnapshot.capturedAt) : "";
12473
+ const gitLabel = gitSnapshot
12474
+ ? "Git snapshot: " + (gitSnapshot.branch || "repository") + " · " + gitSnapshot.changeCount + " change" + (gitSnapshot.changeCount === 1 ? "" : "s") + " · " + gitSnapshot.recentCommitCount + " commit" + (gitSnapshot.recentCommitCount === 1 ? "" : "s") + (gitCapturedLabel ? " · captured " + gitCapturedLabel : "") + (gitSnapshot.truncated ? " · truncated" : "")
12475
+ : "";
12476
+ return "<div class='side-question-thread'>"
12477
+ + "<div class='side-question-thread-header'><div><h2>Side questions</h2><p>Separate from the main Pi conversation.</p></div><button type='button' data-side-question-action='new'" + (state.status === "running" ? " disabled" : "") + ">New thread</button></div>"
12478
+ + "<div class='side-question-context-chips'><span>" + escapeHtml(context.focusLabel || "Editor context") + "</span><span>" + escapeHtml(context.gatherScope === "none" ? "no other files" : (context.contextRoot || context.gatherScope || "local context")) + "</span>"
12479
+ + (context.includeConversation ? "<span>main conversation snapshot</span>" : "") + (gitLabel ? "<span>" + escapeHtml(gitLabel) + "</span>" : "") + "<span>" + escapeHtml(webLabel) + "</span><span>" + escapeHtml(selectedToolLabel) + "</span><span>" + escapeHtml(state.modelLabel + " · " + state.thinking) + "</span></div>"
12480
+ + "<div class='side-question-transcript'>" + messageHtml + "</div>"
12481
+ + activityHtml
12482
+ + (state.error ? "<div class='side-question-error'>" + escapeHtml(state.error) + "</div>" : "")
12483
+ + (latestAnswer ? "<div class='side-question-result-actions'><button type='button' data-side-question-action='copy'>Copy latest answer</button><button type='button' data-side-question-action='insert'>Insert at editor cursor</button><button type='button' data-side-question-action='promote'>Bring to main conversation</button></div>" : "")
12484
+ + "<label class='side-question-composer-label'>Follow-up<textarea data-side-question-field='draft' rows='3' title='Enter adds a new line. Cmd/Ctrl+Enter asks the follow-up.' placeholder='Ask a follow-up in this side thread…'" + (state.status === "running" ? " disabled" : "") + ">" + escapeHtml(sideQuestionUi.draft) + "</textarea></label>"
12485
+ + "<div class='side-question-actions'>"
12486
+ + (state.status === "running"
12487
+ ? "<button type='button' class='side-question-stop' data-side-question-action='stop'>Stop</button>"
12488
+ : "<button type='button' class='side-question-primary' data-side-question-action='ask' aria-keyshortcuts='Meta+Enter Control+Enter' title='Ask follow-up (Cmd/Ctrl+Enter)'" + (!isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() ? " disabled" : "") + ">Ask follow-up</button>")
12489
+ + "</div></div>";
12490
+ }
12491
+
12492
+ function scheduleSideQuestionContextRefresh() {
12493
+ if (rightView !== "side-questions" || (sideQuestionState && sideQuestionState.threadId) || sideQuestionContextRefreshHandle !== null) return;
12494
+ sideQuestionContextRefreshHandle = window.setTimeout(() => {
12495
+ sideQuestionContextRefreshHandle = null;
12496
+ if (rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) renderSideQuestionView();
12497
+ }, 40);
12498
+ }
12499
+
12500
+ function renderSideQuestionView(options) {
12501
+ if (!critiqueViewEl || rightView !== "side-questions") return;
12502
+ finishPreviewRender(critiqueViewEl);
12503
+ const scrollTop = critiqueViewEl.scrollTop;
12504
+ const nearBottom = critiqueViewEl.scrollHeight - critiqueViewEl.clientHeight - critiqueViewEl.scrollTop < 100;
12505
+ critiqueViewEl.innerHTML = sideQuestionState && sideQuestionState.threadId ? renderSideQuestionThread() : renderSideQuestionSetup();
12506
+ const nonce = ++sideQuestionPreviewRenderNonce;
12507
+ void renderSideQuestionMarkdownFields(nonce);
12508
+ if ((options && options.followBottom) || nearBottom) {
12509
+ critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
12510
+ window.requestAnimationFrame(() => { if (rightView === "side-questions") critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight; });
12511
+ } else {
12512
+ critiqueViewEl.scrollTop = scrollTop;
12513
+ }
12514
+ }
12515
+
12516
+ function submitSideQuestion() {
12517
+ const question = String(sideQuestionUi.draft || "").trim();
12518
+ if (!question) {
12519
+ setStatus("Enter a side question first.", "warning");
12520
+ return;
12521
+ }
12522
+ if (!isSideQuestionConnectionReady()) {
12523
+ setStatus("Studio is disconnected.", "warning");
12524
+ return;
12525
+ }
12526
+ const requestId = makeRequestId();
12527
+ const message = {
12528
+ type: "side_question_ask_request",
12529
+ requestId,
12530
+ question,
12531
+ };
12532
+ if (sideQuestionState && sideQuestionState.threadId) {
12533
+ message.threadId = sideQuestionState.threadId;
12534
+ } else {
12535
+ message.context = buildSideQuestionContextPayload();
12536
+ if (message.context.gatherScope === "custom" && !String(message.context.contextPath || "").trim()) {
12537
+ setStatus("Choose a custom context path first.", "warning");
12538
+ return;
12539
+ }
12540
+ }
12541
+ if (!sendMessage(message)) return;
12542
+ sideQuestionUi.draft = "";
12543
+ if (!sideQuestionState) sideQuestionState = normalizeSideQuestionState(null);
12544
+ sideQuestionState.status = "running";
12545
+ sideQuestionState.requestId = requestId;
12546
+ sideQuestionState.error = "";
12547
+ renderSideQuestionView({ followBottom: true });
12548
+ updateReferenceBadge();
12549
+ syncAskAsideButton();
12550
+ updateResultActionButtons();
12551
+ setStatus("Side question running independently of the main conversation…", "warning");
12552
+ }
12553
+
12554
+ function sendSideQuestionMarkdownExport(path, content, overwrite) {
12555
+ if (sideQuestionMarkdownExportRequest) {
12556
+ setStatus("A side-thread Markdown export is already in progress.", "warning");
12557
+ return false;
12558
+ }
12559
+ if (!isSideQuestionConnectionReady()) {
12560
+ setStatus("Studio is disconnected.", "warning");
12561
+ return false;
12562
+ }
12563
+ const requestId = makeRequestId();
12564
+ sideQuestionMarkdownExportRequest = {
12565
+ requestId,
12566
+ threadId: sideQuestionState && sideQuestionState.threadId ? sideQuestionState.threadId : "",
12567
+ path: String(path || ""),
12568
+ content: String(content || ""),
12569
+ };
12570
+ const sent = sendMessage({
12571
+ type: "side_question_export_markdown_request",
12572
+ requestId,
12573
+ threadId: sideQuestionMarkdownExportRequest.threadId,
12574
+ path: String(path || ""),
12575
+ content: String(content || ""),
12576
+ overwrite: overwrite === true,
12577
+ });
12578
+ if (!sent) sideQuestionMarkdownExportRequest = null;
12579
+ updateResultActionButtons();
12580
+ if (sent) setStatus(overwrite ? "Replacing side-thread Markdown export…" : "Saving side-thread Markdown…", "warning");
12581
+ return sent;
12582
+ }
12583
+
12584
+ async function saveSideQuestionTranscriptMarkdown() {
12585
+ const exportedAt = new Date();
12586
+ const markdown = buildCurrentSideQuestionTranscriptMarkdown(exportedAt);
12587
+ if (!markdown.trim()) {
12588
+ setStatus("No completed side discussion is available to save.", "warning");
12589
+ return;
12590
+ }
12591
+ const path = await requestStudioTextInput(
12592
+ "Save the visible side-thread transcript and context summary as Markdown. Hidden source text and raw tool output are not included.",
12593
+ getSideQuestionTranscriptSuggestedPath(exportedAt),
12594
+ {
12595
+ title: "Save side-question transcript",
12596
+ inputLabel: "Markdown path on computer running Pi",
12597
+ confirmLabel: "Save Markdown",
12598
+ },
12599
+ );
12600
+ if (!path) return;
12601
+ sendSideQuestionMarkdownExport(path, markdown, false);
12602
+ }
12603
+
12604
+ async function copySideQuestionTranscriptMarkdown() {
12605
+ const markdown = buildCurrentSideQuestionTranscriptMarkdown(new Date());
12606
+ if (!markdown.trim()) {
12607
+ setStatus("No completed side discussion is available to copy.", "warning");
12608
+ return;
12609
+ }
12610
+ const copied = await writeTextToClipboard(markdown);
12611
+ setStatus(copied ? "Copied side-thread Markdown." : "Clipboard write failed.", copied ? "success" : "warning");
12612
+ }
12613
+
12614
+ function openSideQuestionTranscriptInEditor() {
12615
+ const exportedAt = new Date();
12616
+ const markdown = buildCurrentSideQuestionTranscriptMarkdown(exportedAt);
12617
+ if (!markdown.trim()) {
12618
+ setStatus("No completed side discussion is available to open.", "warning");
12619
+ return;
12620
+ }
12621
+ if (markdown.length > SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS) {
12622
+ setStatus("This side-thread transcript is too large for a companion editor. Save or copy the Markdown instead.", "warning");
12623
+ return;
12624
+ }
12625
+ requestOpenEditorOnlyDocument(markdown, {
12626
+ label: getSideQuestionTranscriptFilename(exportedAt),
12627
+ resourceDir: getCurrentResourceDirValue() || (sideQuestionState.context && sideQuestionState.context.contextRoot) || undefined,
12628
+ });
12629
+ }
12630
+
12631
+ function insertLatestSideQuestionAnswer() {
12632
+ const answer = getLatestCompletedSideQuestionAnswer();
12633
+ if (!answer) return;
12634
+ const current = sourceTextEl.value || "";
12635
+ const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : current.length;
12636
+ const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
12637
+ const safeStart = Math.max(0, Math.min(start, current.length));
12638
+ const safeEnd = Math.max(safeStart, Math.min(end, current.length));
12639
+ const next = current.slice(0, safeStart) + answer.text + current.slice(safeEnd);
12640
+ setEditorText(next, { preserveScroll: false, preserveSelection: false });
12641
+ const caret = safeStart + answer.text.length;
12642
+ sourceTextEl.setSelectionRange(caret, caret);
12643
+ setActivePane("left");
12644
+ focusSourceTextNoScroll();
12645
+ setStatus("Inserted the latest side answer at the editor cursor.", "success");
12646
+ }
12647
+
12648
+ async function handleSideQuestionClick(event) {
12649
+ if (rightView !== "side-questions") return;
12650
+ const target = event && event.target instanceof Element ? event.target.closest("[data-side-question-action]") : null;
12651
+ if (!target || !critiqueViewEl.contains(target)) return;
12652
+ event.preventDefault();
12653
+ const action = target.getAttribute("data-side-question-action");
12654
+ if (action === "ask") {
12655
+ submitSideQuestion();
12656
+ } else if (action === "stop") {
12657
+ if (sideQuestionState && sideQuestionState.threadId && sideQuestionState.requestId) {
12658
+ sendMessage({ type: "side_question_cancel_request", threadId: sideQuestionState.threadId, requestId: sideQuestionState.requestId });
12659
+ setStatus("Stopping side question…", "warning");
12660
+ }
12661
+ } else if (action === "new") {
12662
+ const confirmed = !sideQuestionState || !sideQuestionState.messages.length || await requestStudioConfirmation(
12663
+ "Clear this ephemeral side thread and choose fresh context? Nothing has been added to the main conversation.",
12664
+ { title: "Start a new side thread?", confirmLabel: "New thread", destructive: true },
12665
+ );
12666
+ if (confirmed) {
12667
+ sendMessage({ type: "side_question_clear_request", threadId: sideQuestionState && sideQuestionState.threadId ? sideQuestionState.threadId : undefined });
12668
+ sideQuestionState = null;
12669
+ sideQuestionUi = {
12670
+ ...sideQuestionUi,
12671
+ focusMode: "auto",
12672
+ customPath: "",
12673
+ includeConversation: false,
12674
+ gitContext: false,
12675
+ webSearch: false,
12676
+ draft: "",
12677
+ };
12678
+ renderSideQuestionView();
12679
+ updateResultActionButtons();
12680
+ }
12681
+ } else if (action === "copy") {
12682
+ const answer = getLatestCompletedSideQuestionAnswer();
12683
+ if (answer) {
12684
+ const copied = await writeTextToClipboard(answer.text);
12685
+ setStatus(copied ? "Copied latest side answer." : "Could not copy side answer.", copied ? "success" : "error");
12686
+ }
12687
+ } else if (action === "insert") {
12688
+ insertLatestSideQuestionAnswer();
12689
+ } else if (action === "promote") {
12690
+ if (sideQuestionState && sideQuestionState.threadId) {
12691
+ const confirmed = await requestStudioConfirmation(
12692
+ "Send the latest side question and answer into the main Pi conversation? This is the only action that adds the side thread to main context.",
12693
+ { title: "Bring side answer to main?", confirmLabel: "Bring to main" },
12694
+ );
12695
+ if (confirmed) sendMessage({ type: "side_question_promote_request", threadId: sideQuestionState.threadId });
12696
+ }
12697
+ }
12698
+ }
12699
+
12700
+ function handleSideQuestionInput(event) {
12701
+ if (rightView !== "side-questions") return;
12702
+ const target = event && event.target;
12703
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) return;
12704
+ const field = target.getAttribute("data-side-question-field");
12705
+ if (field === "draft") sideQuestionUi.draft = target.value;
12706
+ if (field === "customPath") sideQuestionUi.customPath = target.value;
12707
+ const askButton = critiqueViewEl.querySelector("[data-side-question-action='ask']");
12708
+ if (askButton) askButton.disabled = !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
12709
+ }
12710
+
12711
+ function handleSideQuestionKeydown(event) {
12712
+ if (rightView !== "side-questions" || !event || event.isComposing) return;
12713
+ const target = event.target;
12714
+ if (!(target instanceof HTMLTextAreaElement) || target.getAttribute("data-side-question-field") !== "draft") return;
12715
+ const submitShortcut = event.key === "Enter"
12716
+ && (event.metaKey || event.ctrlKey)
12717
+ && !event.altKey
12718
+ && !event.shiftKey;
12719
+ if (!submitShortcut) return;
12720
+ event.preventDefault();
12721
+ event.stopPropagation();
12722
+ if (!sideQuestionState || sideQuestionState.status !== "running") submitSideQuestion();
12723
+ }
12724
+
12725
+ async function handleSideQuestionChange(event) {
12726
+ if (rightView !== "side-questions") return;
12727
+ const target = event && event.target;
12728
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) return;
12729
+ const toolId = target.getAttribute("data-side-question-tool");
12730
+ if (toolId) {
12731
+ const tool = sideQuestionAvailablePiTools.find((candidate) => candidate.id === toolId);
12732
+ if (!tool) return;
12733
+ const selected = new Set(sideQuestionUi.toolIds);
12734
+ if (target.checked) {
12735
+ if (tool.gateway && !selected.has(toolId)) {
12736
+ const confirmed = await requestStudioConfirmation(
12737
+ `“${tool.name}” is a gateway tool. Selecting it may expose additional services and actions configured behind that extension, subject to the extension's own permissions.`,
12738
+ { title: "Allow gateway tool in side questions?", confirmLabel: "Allow gateway" },
12739
+ );
12740
+ if (!confirmed) {
12741
+ target.checked = false;
12742
+ renderSideQuestionView();
12743
+ return;
12744
+ }
12745
+ }
12746
+ if (selected.size >= 12 && !selected.has(toolId)) {
12747
+ setStatus("Select at most 12 additional Pi tools.", "warning");
12748
+ renderSideQuestionView();
12749
+ return;
12750
+ }
12751
+ selected.add(toolId);
12752
+ } else {
12753
+ selected.delete(toolId);
12754
+ }
12755
+ sideQuestionUi.toolIds = [...selected];
12756
+ persistSideQuestionToolSelection();
12757
+ renderSideQuestionView();
12758
+ return;
12759
+ }
12760
+ const field = target.getAttribute("data-side-question-field");
12761
+ if (!field) return;
12762
+ if (field === "focusMode") sideQuestionUi.focusMode = target.value;
12763
+ if (field === "gatherScope") {
12764
+ sideQuestionUi.gatherScope = target.value;
12765
+ if (sideQuestionUi.gatherScope !== "repo") sideQuestionUi.gitContext = false;
12766
+ try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_GATHER_STORAGE_KEY, sideQuestionUi.gatherScope); } catch {}
12767
+ }
12768
+ if (field === "thinking") {
12769
+ sideQuestionUi.thinking = target.value;
12770
+ try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_THINKING_STORAGE_KEY, sideQuestionUi.thinking); } catch {}
12771
+ }
12772
+ if (field === "includeConversation") sideQuestionUi.includeConversation = target.checked;
12773
+ if (field === "gitContext") sideQuestionUi.gitContext = target.checked && getSideQuestionGatherScope() === "repo";
12774
+ if (field === "webSearch") sideQuestionUi.webSearch = target.checked;
12775
+ renderSideQuestionView();
12776
+ }
12777
+
11776
12778
  function renderActiveResult() {
11777
12779
  if (critiqueViewEl) {
11778
12780
  critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
11779
12781
  critiqueViewEl.classList.toggle("quarto-preview-host", rightView === "editor-quarto-preview");
12782
+ critiqueViewEl.classList.toggle("side-question-host", rightView === "side-questions");
12783
+ }
12784
+ if (rightView === "side-questions") {
12785
+ renderSideQuestionView();
12786
+ return;
11780
12787
  }
11781
12788
  if (rightView === "editor-quarto-preview") {
11782
12789
  renderQuartoPreviewView();
@@ -11880,7 +12887,7 @@
11880
12887
  : normalizeForCompare(sourceTextEl.value);
11881
12888
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
11882
12889
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
11883
- const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes" || rightView === "editor-quarto-preview";
12890
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes" || rightView === "editor-quarto-preview" || rightView === "side-questions";
11884
12891
 
11885
12892
  if (responseWrapEl) {
11886
12893
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -11918,28 +12925,61 @@
11918
12925
 
11919
12926
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
11920
12927
  const exportingReplJournal = rightView === "repl";
12928
+ const exportingSideThread = rightView === "side-questions";
11921
12929
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
12930
+ const sideThreadExportText = exportingSideThread && canExportSideQuestionTranscript()
12931
+ ? buildCurrentSideQuestionTranscriptMarkdown(new Date())
12932
+ : "";
11922
12933
  const exportText = exportingReplJournal
11923
12934
  ? (replJournalExportEntries.length ? buildReplJournalMarkdown(replJournalExportEntries) : "")
11924
- : (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown);
11925
- const canExportPreview = (rightPaneShowsPreview || exportingReplJournal) && Boolean(String(exportText || "").trim());
11926
- const htmlArtifactExportSource = canExportPreview && !exportingReplJournal ? getRightPaneHtmlArtifactSource() : "";
12935
+ : (exportingSideThread
12936
+ ? sideThreadExportText
12937
+ : (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown));
12938
+ const canExportPreview = (rightPaneShowsPreview || exportingReplJournal || exportingSideThread) && Boolean(String(exportText || "").trim());
12939
+ const htmlArtifactExportSource = canExportPreview && !exportingReplJournal && !exportingSideThread ? getRightPaneHtmlArtifactSource() : "";
11927
12940
  const isHtmlArtifactPreview = Boolean(htmlArtifactExportSource);
12941
+ const sideThreadRenderTooLarge = exportingSideThread && sideThreadExportText.length > SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS;
12942
+ const exportBusy = previewExportInProgress || Boolean(sideQuestionMarkdownExportRequest) || (!exportingSideThread && uiBusy);
12943
+ if (exportSideThreadMarkdownSaveBtn) {
12944
+ exportSideThreadMarkdownSaveBtn.hidden = !exportingSideThread;
12945
+ exportSideThreadMarkdownSaveBtn.disabled = Boolean(sideQuestionMarkdownExportRequest) || !canExportPreview;
12946
+ exportSideThreadMarkdownSaveBtn.title = "Save the visible discussion and context summary as a Markdown file on the computer running Pi.";
12947
+ }
12948
+ if (exportSideThreadMarkdownCopyBtn) {
12949
+ exportSideThreadMarkdownCopyBtn.hidden = !exportingSideThread;
12950
+ exportSideThreadMarkdownCopyBtn.disabled = Boolean(sideQuestionMarkdownExportRequest) || !canExportPreview;
12951
+ exportSideThreadMarkdownCopyBtn.title = "Copy the visible discussion and context summary as Markdown.";
12952
+ }
12953
+ if (exportSideThreadMarkdownEditorBtn) {
12954
+ exportSideThreadMarkdownEditorBtn.hidden = !exportingSideThread;
12955
+ exportSideThreadMarkdownEditorBtn.disabled = uiBusy || Boolean(sideQuestionMarkdownExportRequest) || !canExportPreview || sideThreadRenderTooLarge;
12956
+ exportSideThreadMarkdownEditorBtn.title = sideThreadRenderTooLarge
12957
+ ? "This transcript is too large for a companion editor; save or copy the Markdown instead."
12958
+ : "Open the Markdown transcript as an unsaved copy in a new Studio editor tab.";
12959
+ }
11928
12960
  if (exportPdfBtn) {
11929
- exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
12961
+ exportPdfBtn.disabled = exportBusy || !canExportPreview;
11930
12962
  exportPdfBtn.textContent = previewExportInProgress
11931
12963
  ? "Exporting…"
11932
- : (exportingReplJournal ? "Export record" : "Export right preview");
12964
+ : (sideQuestionMarkdownExportRequest
12965
+ ? "Saving…"
12966
+ : (exportingSideThread ? "Export thread" : (exportingReplJournal ? "Export record" : "Export right preview")));
11933
12967
  if (rightView === "trace") {
11934
12968
  exportPdfBtn.title = "Working view does not support preview export.";
11935
12969
  } else if (rightView === "files") {
11936
12970
  exportPdfBtn.title = "Files view does not support preview export.";
11937
12971
  } else if (rightView === "changes") {
11938
12972
  exportPdfBtn.title = "Changes view does not support preview export.";
12973
+ } else if (exportingSideThread && sideQuestionState && sideQuestionState.status === "running") {
12974
+ exportPdfBtn.title = "Wait for the current side answer before exporting the thread.";
12975
+ } else if (exportingSideThread && !canExportPreview) {
12976
+ exportPdfBtn.title = "No completed side discussion is available to export yet.";
12977
+ } else if (exportingSideThread) {
12978
+ exportPdfBtn.title = "Save or copy Markdown, open it in an editor, or export the visible side discussion as PDF or HTML.";
11939
12979
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
11940
12980
  exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
11941
12981
  } else if (rightView === "markdown") {
11942
- exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL to export.";
12982
+ exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export.";
11943
12983
  } else if (!canExportPreview) {
11944
12984
  exportPdfBtn.title = "Nothing to export yet.";
11945
12985
  } else if (isHtmlArtifactPreview) {
@@ -11950,39 +12990,52 @@
11950
12990
  exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
11951
12991
  }
11952
12992
  }
12993
+ if (exportSideThreadRenderSeparatorEl) exportSideThreadRenderSeparatorEl.hidden = !exportingSideThread;
11953
12994
  if (exportPreviewPdfStudioBtn) {
11954
- exportPreviewPdfStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
11955
- exportPreviewPdfStudioBtn.title = isHtmlArtifactPreview
11956
- ? "Interactive HTML preview PDF export is not available yet."
11957
- : (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in Studio." : "Export the current right-pane preview as PDF and open it in Studio.");
12995
+ exportPreviewPdfStudioBtn.disabled = exportBusy || !canExportPreview || isHtmlArtifactPreview || sideThreadRenderTooLarge;
12996
+ exportPreviewPdfStudioBtn.title = sideThreadRenderTooLarge
12997
+ ? "This transcript is too large for rendered export; save or copy the Markdown instead."
12998
+ : (isHtmlArtifactPreview
12999
+ ? "Interactive HTML preview PDF export is not available yet."
13000
+ : (exportingSideThread ? "Export the side-thread transcript as PDF and open it in Studio." : (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in Studio." : "Export the current right-pane preview as PDF and open it in Studio.")));
11958
13001
  }
11959
13002
  if (exportPreviewPdfBtn) {
11960
- exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
11961
- exportPreviewPdfBtn.title = isHtmlArtifactPreview
11962
- ? "Interactive HTML preview PDF export is not available yet."
11963
- : (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in the default PDF viewer." : "Export the current right-pane preview as PDF and open it in the default PDF viewer.");
13003
+ exportPreviewPdfBtn.disabled = exportBusy || !canExportPreview || isHtmlArtifactPreview || sideThreadRenderTooLarge;
13004
+ exportPreviewPdfBtn.title = sideThreadRenderTooLarge
13005
+ ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13006
+ : (isHtmlArtifactPreview
13007
+ ? "Interactive HTML preview PDF export is not available yet."
13008
+ : (exportingSideThread ? "Export the side-thread transcript as PDF and open it in the default PDF viewer." : (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in the default PDF viewer." : "Export the current right-pane preview as PDF and open it in the default PDF viewer.")));
11964
13009
  }
11965
13010
  if (exportPreviewHtmlStudioBtn) {
11966
- exportPreviewHtmlStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
11967
- exportPreviewHtmlStudioBtn.title = isHtmlArtifactPreview
11968
- ? "Export the authored HTML preview and open it in a new Studio editor tab."
11969
- : (exportingReplJournal ? "Export the Studio REPL record as standalone HTML and open it in a new Studio editor tab." : "Export the current right-pane preview as standalone HTML and open it in a new Studio editor tab.");
13011
+ exportPreviewHtmlStudioBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
13012
+ exportPreviewHtmlStudioBtn.title = sideThreadRenderTooLarge
13013
+ ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13014
+ : (isHtmlArtifactPreview
13015
+ ? "Export the authored HTML preview and open it in a new Studio editor tab."
13016
+ : (exportingSideThread ? "Export the side-thread transcript as standalone HTML and open it in a new Studio editor tab." : (exportingReplJournal ? "Export the Studio REPL record as standalone HTML and open it in a new Studio editor tab." : "Export the current right-pane preview as standalone HTML and open it in a new Studio editor tab.")));
11970
13017
  }
11971
13018
  if (exportPreviewHtmlBtn) {
11972
- exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
11973
- exportPreviewHtmlBtn.title = isHtmlArtifactPreview
11974
- ? "Export the authored HTML preview and open it in the default browser."
11975
- : (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.");
13019
+ exportPreviewHtmlBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
13020
+ exportPreviewHtmlBtn.title = sideThreadRenderTooLarge
13021
+ ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13022
+ : (isHtmlArtifactPreview
13023
+ ? "Export the authored HTML preview and open it in the default browser."
13024
+ : (exportingSideThread ? "Export the side-thread transcript as standalone HTML and open it in the default browser." : (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.")));
11976
13025
  }
11977
13026
  if (exportPreviewControlsEl) {
11978
13027
  exportPreviewControlsEl.hidden = rightView === "editor-quarto-preview";
11979
13028
  exportPreviewControlsEl.title = canExportPreview
11980
- ? (exportingReplJournal
11981
- ? "Choose a format and export destination for the Studio REPL record."
11982
- : (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview."))
11983
- : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
11984
- }
11985
- if (!canExportPreview || previewExportInProgress) {
13029
+ ? (exportingSideThread
13030
+ ? "Choose a durable export for the visible side discussion."
13031
+ : (exportingReplJournal
13032
+ ? "Choose a format and export destination for the Studio REPL record."
13033
+ : (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview.")))
13034
+ : (exportingSideThread
13035
+ ? "No completed side discussion is available to export yet."
13036
+ : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting."));
13037
+ }
13038
+ if (!canExportPreview || previewExportInProgress || sideQuestionMarkdownExportRequest) {
11986
13039
  closeExportPreviewMenu();
11987
13040
  }
11988
13041
 
@@ -11992,6 +13045,7 @@
11992
13045
  updateSyncBadge(normalizedEditor);
11993
13046
  syncStudioQuartoDirtyUi();
11994
13047
  syncShowMeButton();
13048
+ syncAskAsideButton();
11995
13049
  }
11996
13050
 
11997
13051
  function refreshResponseUi() {
@@ -13160,9 +14214,19 @@
13160
14214
  if (rightView === "editor-quarto-preview") {
13161
14215
  requestStudioQuartoPreviewCheck(false);
13162
14216
  }
14217
+ if (rightView === "side-questions" && previousView !== "side-questions") {
14218
+ if (!sideQuestionUi.gatherScope) sideQuestionUi.gatherScope = getSideQuestionGatherScope();
14219
+ sendMessage({ type: "side_question_get_state" });
14220
+ }
13163
14221
 
13164
14222
  refreshResponseUi();
13165
14223
  syncActionButtons();
14224
+ if (rightView === "side-questions" && previousView !== "side-questions") {
14225
+ window.setTimeout(() => {
14226
+ const composer = critiqueViewEl && critiqueViewEl.querySelector("[data-side-question-field='draft']");
14227
+ if (composer instanceof HTMLTextAreaElement) composer.focus({ preventScroll: true });
14228
+ }, 0);
14229
+ }
13166
14230
  scheduleWorkspacePersistence();
13167
14231
  }
13168
14232
 
@@ -14184,9 +15248,11 @@
14184
15248
  await renderAnnotationMathInElement(target);
14185
15249
  decoratePdfEmbeds(target);
14186
15250
  await renderPdfPreviewsInElement(target);
15251
+ decoratePreviewPdfFigures(target);
14187
15252
  await renderMermaidInElement(target);
14188
15253
  await renderMathFallbackInElement(target);
14189
15254
  decorateCopyablePreviewBlocks(target);
15255
+ decoratePreviewImages(target);
14190
15256
  if (preserveScroll) restoreQuizScrollTopSoon(scrollTop);
14191
15257
  } catch (error) {
14192
15258
  console.error("Quiz markdown preview render failed:", error);
@@ -20283,6 +21349,7 @@
20283
21349
  quizBtn.disabled = true;
20284
21350
  quizBtn.title = "Quiz is unavailable in editor-only mode.";
20285
21351
  }
21352
+ syncAskAsideButton();
20286
21353
  syncStudioUiRefreshReviewTrigger();
20287
21354
  return;
20288
21355
  }
@@ -20357,6 +21424,7 @@
20357
21424
  : "Open an active quiz for the current editor selection or document.");
20358
21425
  }
20359
21426
  syncShowMeButton();
21427
+ syncAskAsideButton();
20360
21428
  syncStudioUiRefreshReviewTrigger();
20361
21429
  }
20362
21430
 
@@ -20517,6 +21585,82 @@
20517
21585
  return;
20518
21586
  }
20519
21587
 
21588
+ if (message.type === "side_question_state") {
21589
+ sideQuestionWebSearchAvailable = message.webSearchAvailable === true;
21590
+ if (Array.isArray(message.availablePiTools)) applySideQuestionToolCatalog(message.availablePiTools);
21591
+ const previousStatus = sideQuestionState && sideQuestionState.status;
21592
+ sideQuestionState = normalizeSideQuestionState(message.state);
21593
+ if (rightView === "side-questions") renderSideQuestionView();
21594
+ updateReferenceBadge();
21595
+ syncAskAsideButton();
21596
+ updateResultActionButtons();
21597
+ if (rightView === "side-questions" && previousStatus === "running" && sideQuestionState.status === "idle") {
21598
+ setStatus(agentBusyFromServer
21599
+ ? "Side answer ready; the main Pi turn is still running. Main conversation unchanged."
21600
+ : "Side answer ready. Main conversation unchanged.", "success");
21601
+ }
21602
+ return;
21603
+ }
21604
+
21605
+ if (message.type === "side_question_markdown_exported") {
21606
+ if (!sideQuestionMarkdownExportRequest || sideQuestionMarkdownExportRequest.requestId !== message.requestId) return;
21607
+ sideQuestionMarkdownExportRequest = null;
21608
+ updateResultActionButtons();
21609
+ setStatus(typeof message.message === "string" ? message.message : "Saved side-question transcript.", "success");
21610
+ return;
21611
+ }
21612
+
21613
+ if (message.type === "side_question_markdown_export_conflict") {
21614
+ if (!sideQuestionMarkdownExportRequest || sideQuestionMarkdownExportRequest.requestId !== message.requestId) return;
21615
+ const pendingExport = sideQuestionMarkdownExportRequest;
21616
+ sideQuestionMarkdownExportRequest = null;
21617
+ updateResultActionButtons();
21618
+ void (async () => {
21619
+ const displayPath = typeof message.path === "string" && message.path ? message.path : pendingExport.path;
21620
+ const confirmed = await requestStudioConfirmation("Replace the existing Markdown transcript at " + displayPath + "?", {
21621
+ title: "Replace transcript file?",
21622
+ confirmLabel: "Replace",
21623
+ destructive: true,
21624
+ });
21625
+ if (!confirmed) {
21626
+ setStatus("Side-thread Markdown export cancelled.", "warning");
21627
+ return;
21628
+ }
21629
+ if (!sideQuestionState || sideQuestionState.threadId !== pendingExport.threadId) {
21630
+ setStatus("That side thread is no longer active; export cancelled.", "warning");
21631
+ return;
21632
+ }
21633
+ sendSideQuestionMarkdownExport(pendingExport.path, pendingExport.content, true);
21634
+ })();
21635
+ return;
21636
+ }
21637
+
21638
+ if (message.type === "side_question_markdown_export_error") {
21639
+ if (!sideQuestionMarkdownExportRequest || sideQuestionMarkdownExportRequest.requestId !== message.requestId) return;
21640
+ sideQuestionMarkdownExportRequest = null;
21641
+ updateResultActionButtons();
21642
+ setStatus(typeof message.message === "string" ? message.message : "Could not save side-question transcript.", "error");
21643
+ return;
21644
+ }
21645
+
21646
+ if (message.type === "side_question_error") {
21647
+ if (!sideQuestionState) sideQuestionState = normalizeSideQuestionState(null);
21648
+ sideQuestionState.status = "error";
21649
+ sideQuestionState.requestId = null;
21650
+ sideQuestionState.error = typeof message.message === "string" ? message.message : "Side question failed.";
21651
+ if (rightView === "side-questions") renderSideQuestionView();
21652
+ updateReferenceBadge();
21653
+ syncAskAsideButton();
21654
+ updateResultActionButtons();
21655
+ setStatus(sideQuestionState.error, "error");
21656
+ return;
21657
+ }
21658
+
21659
+ if (message.type === "side_question_promoted") {
21660
+ setStatus(typeof message.message === "string" ? message.message : "Side answer sent to the main conversation.", "success");
21661
+ return;
21662
+ }
21663
+
20520
21664
  if (message.type === "quarto_preview_context") {
20521
21665
  const requestId = typeof message.requestId === "string" ? message.requestId : "";
20522
21666
  const context = normalizeStudioQuartoContext(message.context);
@@ -20598,6 +21742,11 @@
20598
21742
  if (typeof message.modelLabel === "string") {
20599
21743
  modelLabel = message.modelLabel;
20600
21744
  }
21745
+ sideQuestionWebSearchAvailable = message.webSearchAvailable === true;
21746
+ if (Array.isArray(message.availablePiTools)) applySideQuestionToolCatalog(message.availablePiTools);
21747
+ if (message.sideQuestion && typeof message.sideQuestion === "object") {
21748
+ sideQuestionState = normalizeSideQuestionState(message.sideQuestion);
21749
+ }
20601
21750
  if (Array.isArray(message.suggestionModels)) {
20602
21751
  updateCompletionSuggestionModelOptions(message.suggestionModels);
20603
21752
  }
@@ -20696,6 +21845,8 @@
20696
21845
  requestStudioQuartoPreviewCheck(false);
20697
21846
  renderQuartoPreviewView();
20698
21847
  }
21848
+ if (rightView === "side-questions") renderSideQuestionView();
21849
+ syncAskAsideButton();
20699
21850
 
20700
21851
  if (pendingRequestId) {
20701
21852
  if (busy) {
@@ -21419,6 +22570,7 @@
21419
22570
  quartoPreviewCheckRequestId = null;
21420
22571
  quartoPreviewCheckSourcePath = "";
21421
22572
  quartoPreviewActionRequestId = null;
22573
+ sideQuestionMarkdownExportRequest = null;
21422
22574
  failAllPendingCompanionLaunches("The originating Studio connection was lost before the companion editor was ready.");
21423
22575
  if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
21424
22576
  setBusy(true);
@@ -21911,6 +23063,7 @@
21911
23063
  updateReviewNotesUi();
21912
23064
  }
21913
23065
  scheduleWorkspacePersistence();
23066
+ scheduleSideQuestionContextRefresh();
21914
23067
  });
21915
23068
 
21916
23069
  sourceTextEl.addEventListener("select", () => {
@@ -21924,14 +23077,17 @@
21924
23077
  }
21925
23078
  updateEditorSelectionCommentUi();
21926
23079
  syncShowMeButton();
23080
+ scheduleSideQuestionContextRefresh();
21927
23081
  });
21928
23082
 
21929
23083
  sourceTextEl.addEventListener("keyup", () => {
21930
23084
  updateEditorSelectionCommentUi();
23085
+ scheduleSideQuestionContextRefresh();
21931
23086
  });
21932
23087
 
21933
23088
  sourceTextEl.addEventListener("mouseup", () => {
21934
23089
  updateEditorSelectionCommentUi();
23090
+ scheduleSideQuestionContextRefresh();
21935
23091
  });
21936
23092
 
21937
23093
  sourceTextEl.addEventListener("focus", () => {
@@ -22045,6 +23201,13 @@
22045
23201
  });
22046
23202
  }
22047
23203
 
23204
+ if (askAsideBtn) {
23205
+ askAsideBtn.addEventListener("click", () => {
23206
+ setRightView("side-questions");
23207
+ setActivePane("right");
23208
+ });
23209
+ }
23210
+
22048
23211
  if (quizBtn) {
22049
23212
  quizBtn.addEventListener("click", () => {
22050
23213
  openQuizOverlay();
@@ -22624,6 +23787,9 @@
22624
23787
  });
22625
23788
  }
22626
23789
 
23790
+ document.addEventListener("click", handleStudioPreviewMediaActivation, true);
23791
+ document.addEventListener("keydown", handleStudioPreviewMediaKeydown, true);
23792
+
22627
23793
  document.addEventListener("click", (event) => {
22628
23794
  const target = event.target;
22629
23795
  const focusBtn = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;