pi-studio 0.9.50 → 0.9.51

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);
@@ -335,13 +353,15 @@
335
353
  ? "editor-preview"
336
354
  : (raw === "editor-quarto-preview"
337
355
  ? "editor-quarto-preview"
338
- : (raw === "repl"
339
- ? "repl"
340
- : (raw === "files"
356
+ : (raw === "side-questions"
357
+ ? "side-questions"
358
+ : (raw === "repl"
359
+ ? "repl"
360
+ : (raw === "files"
341
361
  ? "files"
342
362
  : (raw === "changes"
343
363
  ? "changes"
344
- : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown"))))));
364
+ : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown")))))));
345
365
  }
346
366
 
347
367
  function normalizeRightViewValue(nextView) {
@@ -380,8 +400,8 @@
380
400
  option.disabled = (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value)) || (isQuartoOption && !quartoRelevant);
381
401
  });
382
402
  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.";
403
+ ? "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."
404
+ : "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
405
  }
386
406
 
387
407
  function getInitialRightView(source) {
@@ -441,12 +461,49 @@
441
461
  const RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS = 8_000;
442
462
  const EDITOR_TAB_TEXT = " ";
443
463
  const QUIZ_DEFAULT_COUNT = 5;
464
+ const SIDE_QUESTION_THINKING_STORAGE_KEY = "piStudio.sideQuestionThinking";
465
+ const SIDE_QUESTION_GATHER_STORAGE_KEY = "piStudio.sideQuestionGatherScope";
466
+ const SIDE_QUESTION_TOOLS_STORAGE_KEY = "piStudio.sideQuestionTools";
467
+ const SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS = 400_000;
444
468
  const COMPLETION_CONTEXT_STORAGE_KEY = "piStudio.completionContextMode";
445
469
  const COMPLETION_MODEL_STORAGE_KEY = "piStudio.completionModel";
446
470
  const COMPLETION_CONTEXT_MAX_CHARS = 12000;
447
471
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
448
472
  const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
449
473
  const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
474
+ let sideQuestionState = null;
475
+ let sideQuestionWebSearchAvailable = false;
476
+ let sideQuestionAvailablePiTools = [];
477
+ let sideQuestionPreviewRenderNonce = 0;
478
+ let sideQuestionContextRefreshHandle = null;
479
+ let sideQuestionMarkdownExportRequest = null;
480
+ const sideQuestionMarkdownRenderCache = new Map();
481
+ let sideQuestionUi = {
482
+ focusMode: "auto",
483
+ gatherScope: (() => {
484
+ try {
485
+ const value = window.localStorage && window.localStorage.getItem(SIDE_QUESTION_GATHER_STORAGE_KEY);
486
+ return value === "none" || value === "folder" || value === "repo" || value === "custom" ? value : "";
487
+ } catch { return ""; }
488
+ })(),
489
+ customPath: "",
490
+ includeConversation: false,
491
+ gitContext: false,
492
+ webSearch: false,
493
+ toolIds: (() => {
494
+ try {
495
+ const parsed = JSON.parse((window.localStorage && window.localStorage.getItem(SIDE_QUESTION_TOOLS_STORAGE_KEY)) || "[]");
496
+ 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) : [];
497
+ } catch { return []; }
498
+ })(),
499
+ thinking: (() => {
500
+ try {
501
+ const value = window.localStorage && window.localStorage.getItem(SIDE_QUESTION_THINKING_STORAGE_KEY);
502
+ return ["off", "minimal", "low", "medium", "high"].includes(value) ? value : "low";
503
+ } catch { return "low"; }
504
+ })(),
505
+ draft: "",
506
+ };
450
507
  let quizOverlayEl = null;
451
508
  let quizDialogEl = null;
452
509
  let quizPreviewRenderNonce = 0;
@@ -2717,7 +2774,7 @@
2717
2774
  if (!isEditorOnlyMode && critiqueBtn && lensSelect) {
2718
2775
  const reviewButton = makeStudioUiRefreshElement("button", "studio-refresh-tool-tab studio-refresh-review-btn", "Review");
2719
2776
  reviewMenu = makeStudioUiRefreshMenu(reviewButton, "review", "studio-refresh-review-anchor");
2720
- appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, showMeBtn, showMeResponseBtn, quizBtn]);
2777
+ appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, showMeBtn, showMeResponseBtn, askAsideBtn, quizBtn]);
2721
2778
  appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
2722
2779
  }
2723
2780
 
@@ -2841,6 +2898,7 @@
2841
2898
  if (reviewNotesBtn) headerToolsEl.appendChild(reviewNotesBtn);
2842
2899
  if (outlineBtn) headerToolsEl.appendChild(outlineBtn);
2843
2900
  if (scratchpadBtn) headerToolsEl.appendChild(scratchpadBtn);
2901
+ if (isEditorOnlyMode && askAsideBtn) headerToolsEl.appendChild(askAsideBtn);
2844
2902
  if (reviewMenu) headerToolsEl.appendChild(reviewMenu.anchor);
2845
2903
  headerTopEl.appendChild(headerToolsEl);
2846
2904
 
@@ -3041,9 +3099,9 @@
3041
3099
 
3042
3100
  function getIdleStatus() {
3043
3101
  if (isEditorOnlyMode) {
3044
- return "Editor-only mode: edit, browse files, annotate, preview, save, suggest, refresh file-backed text, or send to a REPL.";
3102
+ 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
3103
  }
3046
- return "Edit, load, or annotate text, then run, save, review, or ask Studio to show the current structure.";
3104
+ return "Edit, load, or annotate text, then run, save, review, explain, or ask a side question.";
3047
3105
  }
3048
3106
 
3049
3107
  function normalizeTerminalPhase(phase) {
@@ -4761,6 +4819,16 @@
4761
4819
  return;
4762
4820
  }
4763
4821
 
4822
+ const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
4823
+ && (event.metaKey || event.ctrlKey)
4824
+ && event.altKey
4825
+ && !event.shiftKey;
4826
+ if (isSideQuestionsShortcut) {
4827
+ event.preventDefault();
4828
+ switchRightPaneToView("side-questions");
4829
+ return;
4830
+ }
4831
+
4764
4832
  const isContentFocusShortcut = key === "F8" && !event.metaKey && !event.ctrlKey && !event.altKey;
4765
4833
  if (isContentFocusShortcut) {
4766
4834
  event.preventDefault();
@@ -5019,6 +5087,19 @@
5019
5087
  }
5020
5088
  }
5021
5089
 
5090
+ function syncAskAsideButton() {
5091
+ if (!askAsideBtn) return;
5092
+ const running = Boolean(sideQuestionState && sideQuestionState.status === "running");
5093
+ askAsideBtn.disabled = wsState === "Disconnected";
5094
+ askAsideBtn.textContent = isEditorOnlyMode
5095
+ ? "Side questions"
5096
+ : (running ? "Side question running…" : (sideQuestionState && sideQuestionState.threadId ? "Open side questions" : "Side question"));
5097
+ askAsideBtn.classList.toggle("request-active", running);
5098
+ askAsideBtn.title = running
5099
+ ? "Open the independently running side question."
5100
+ : "Open side questions without adding them to the main Pi conversation.";
5101
+ }
5102
+
5022
5103
  function syncTraceForSelectedHistoryItem() {
5023
5104
  const item = getSelectedHistoryItem();
5024
5105
  const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
@@ -5222,6 +5303,18 @@
5222
5303
  return;
5223
5304
  }
5224
5305
 
5306
+ if (rightView === "side-questions") {
5307
+ if (sideQuestionState && sideQuestionState.threadId) {
5308
+ const count = sideQuestionState.messages.filter((entry) => entry.role === "assistant" && entry.status === "complete").length;
5309
+ referenceBadgeEl.textContent = "Side thread: " + (sideQuestionState.status === "running" ? "answering" : "ready")
5310
+ + " · " + count + " answer" + (count === 1 ? "" : "s")
5311
+ + " · outside main context";
5312
+ } else {
5313
+ referenceBadgeEl.textContent = "Side questions: no active thread · outside main context";
5314
+ }
5315
+ return;
5316
+ }
5317
+
5225
5318
  if (rightView === "trace") {
5226
5319
  const state = traceState || createEmptyTraceState();
5227
5320
  const context = traceDisplayContext || {};
@@ -9135,6 +9228,10 @@
9135
9228
  critiqueViewEl.addEventListener("click", handleFilesPaneClick);
9136
9229
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
9137
9230
  critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
9231
+ critiqueViewEl.addEventListener("click", (event) => { void handleSideQuestionClick(event); });
9232
+ critiqueViewEl.addEventListener("input", handleSideQuestionInput);
9233
+ critiqueViewEl.addEventListener("keydown", handleSideQuestionKeydown);
9234
+ critiqueViewEl.addEventListener("change", handleSideQuestionChange);
9138
9235
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
9139
9236
  critiqueViewEl.addEventListener("change", (event) => {
9140
9237
  void handleFilesPaneChange(event);
@@ -9160,7 +9257,7 @@
9160
9257
 
9161
9258
  function applyPendingResponseScrollReset() {
9162
9259
  if (!pendingResponseScrollReset || !critiqueViewEl) return false;
9163
- if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return false;
9260
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview" || rightView === "side-questions") return false;
9164
9261
 
9165
9262
  pendingResponseScrollReset = false;
9166
9263
  let targetEl = replaceResponsePaneWithClone();
@@ -9169,7 +9266,7 @@
9169
9266
  : (cb) => window.setTimeout(cb, 16);
9170
9267
  const resetScroll = () => {
9171
9268
  if (!targetEl || !targetEl.isConnected) return;
9172
- if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return;
9269
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview" || rightView === "side-questions") return;
9173
9270
  targetEl.scrollTop = 0;
9174
9271
  targetEl.scrollLeft = 0;
9175
9272
  };
@@ -9427,8 +9524,10 @@
9427
9524
  async function exportRightPanePdf(options) {
9428
9525
  const exportOptions = options && typeof options === "object" ? options : {};
9429
9526
  const openTarget = exportOptions.openTarget === "studio" ? "studio" : "default";
9527
+ const exportingSideThread = rightView === "side-questions";
9528
+ const sideThreadExportedAt = exportingSideThread ? new Date() : null;
9430
9529
  let studioLaunch = null;
9431
- if (uiBusy || previewExportInProgress) {
9530
+ if ((!exportingSideThread && uiBusy) || previewExportInProgress || sideQuestionMarkdownExportRequest) {
9432
9531
  setStatus("Studio is busy.", "warning");
9433
9532
  return;
9434
9533
  }
@@ -9441,8 +9540,8 @@
9441
9540
 
9442
9541
  const exportingReplJournal = rightView === "repl";
9443
9542
  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");
9543
+ if (!rightPaneShowsPreview && !exportingReplJournal && !exportingSideThread) {
9544
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export PDF.", "warning");
9446
9545
  return;
9447
9546
  }
9448
9547
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
@@ -9451,7 +9550,7 @@
9451
9550
  return;
9452
9551
  }
9453
9552
 
9454
- const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
9553
+ const htmlArtifactSource = exportingReplJournal || exportingSideThread ? "" : getRightPaneHtmlArtifactSource();
9455
9554
  if (htmlArtifactSource) {
9456
9555
  setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
9457
9556
  return;
@@ -9459,24 +9558,34 @@
9459
9558
 
9460
9559
  const markdown = exportingReplJournal
9461
9560
  ? buildReplJournalMarkdown(replJournalExportEntries)
9462
- : (rightView === "editor-preview"
9463
- ? prepareEditorTextForPdfExport(sourceTextEl.value)
9464
- : prepareEditorTextForPreview(latestResponseMarkdown));
9561
+ : (exportingSideThread
9562
+ ? buildCurrentSideQuestionTranscriptMarkdown(sideThreadExportedAt)
9563
+ : (rightView === "editor-preview"
9564
+ ? prepareEditorTextForPdfExport(sourceTextEl.value)
9565
+ : prepareEditorTextForPreview(latestResponseMarkdown)));
9465
9566
  if (!markdown || !markdown.trim()) {
9466
9567
  setStatus("Nothing to export yet.", "warning");
9467
9568
  return;
9468
9569
  }
9570
+ if (exportingSideThread && markdown.length > SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS) {
9571
+ setStatus("This side-thread transcript is too large for PDF export. Save or copy the Markdown instead.", "warning");
9572
+ return;
9573
+ }
9469
9574
 
9470
9575
  const effectivePath = getEffectiveSavePath();
9471
- const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
9576
+ const sourcePath = exportingReplJournal || exportingSideThread ? "" : (effectivePath || sourceState.path || "");
9472
9577
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
9473
9578
  const isEditorPreview = rightView === "editor-preview";
9474
9579
  const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
9475
9580
  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"));
9581
+ const isLatex = exportingSideThread
9582
+ ? false
9583
+ : (isEditorPreview
9584
+ ? editorPdfLanguage === "latex"
9585
+ : /\\documentclass\b|\\begin\{document\}/.test(markdown));
9586
+ let filenameHint = exportingSideThread
9587
+ ? getSideQuestionTranscriptFilename(sideThreadExportedAt).replace(/\.md$/i, ".pdf")
9588
+ : (exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.pdf")));
9480
9589
  if (sourcePath) {
9481
9590
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
9482
9591
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -9493,7 +9602,9 @@
9493
9602
  }
9494
9603
  previewExportInProgress = true;
9495
9604
  updateResultActionButtons();
9496
- setStatus(openTarget === "studio" ? "Exporting PDF for Studio…" : "Exporting PDF…", "warning");
9605
+ setStatus(exportingSideThread
9606
+ ? (openTarget === "studio" ? "Exporting side thread as PDF for Studio…" : "Exporting side thread as PDF…")
9607
+ : (openTarget === "studio" ? "Exporting PDF for Studio…" : "Exporting PDF…"), "warning");
9497
9608
 
9498
9609
  try {
9499
9610
  const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
@@ -9658,8 +9769,10 @@
9658
9769
  async function exportRightPaneHtml(options) {
9659
9770
  const exportOptions = options && typeof options === "object" ? options : {};
9660
9771
  const openTarget = exportOptions.openTarget === "studio" ? "studio" : "browser";
9772
+ const exportingSideThread = rightView === "side-questions";
9773
+ const sideThreadExportedAt = exportingSideThread ? new Date() : null;
9661
9774
  let studioLaunch = null;
9662
- if (uiBusy || previewExportInProgress) {
9775
+ if ((!exportingSideThread && uiBusy) || previewExportInProgress || sideQuestionMarkdownExportRequest) {
9663
9776
  setStatus("Studio is busy.", "warning");
9664
9777
  return;
9665
9778
  }
@@ -9672,8 +9785,8 @@
9672
9785
 
9673
9786
  const exportingReplJournal = rightView === "repl";
9674
9787
  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");
9788
+ if (!rightPaneShowsPreview && !exportingReplJournal && !exportingSideThread) {
9789
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export HTML.", "warning");
9677
9790
  return;
9678
9791
  }
9679
9792
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
@@ -9682,26 +9795,36 @@
9682
9795
  return;
9683
9796
  }
9684
9797
 
9685
- const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
9686
- const markdown = exportingReplJournal ? buildReplJournalMarkdown(replJournalExportEntries) : (htmlArtifactSource || (rightView === "editor-preview"
9687
- ? prepareEditorTextForHtmlExport(sourceTextEl.value)
9688
- : prepareEditorTextForPreview(latestResponseMarkdown)));
9798
+ const htmlArtifactSource = exportingReplJournal || exportingSideThread ? "" : getRightPaneHtmlArtifactSource();
9799
+ const markdown = exportingReplJournal
9800
+ ? buildReplJournalMarkdown(replJournalExportEntries)
9801
+ : (exportingSideThread
9802
+ ? buildCurrentSideQuestionTranscriptMarkdown(sideThreadExportedAt)
9803
+ : (htmlArtifactSource || (rightView === "editor-preview"
9804
+ ? prepareEditorTextForHtmlExport(sourceTextEl.value)
9805
+ : prepareEditorTextForPreview(latestResponseMarkdown))));
9689
9806
  if (!markdown || !markdown.trim()) {
9690
9807
  setStatus("Nothing to export yet.", "warning");
9691
9808
  return;
9692
9809
  }
9810
+ if (exportingSideThread && markdown.length > SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS) {
9811
+ setStatus("This side-thread transcript is too large for HTML export. Save or copy the Markdown instead.", "warning");
9812
+ return;
9813
+ }
9693
9814
 
9694
9815
  const effectivePath = getEffectiveSavePath();
9695
- const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
9816
+ const sourcePath = exportingReplJournal || exportingSideThread ? "" : (effectivePath || sourceState.path || "");
9696
9817
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
9697
9818
  const isEditorPreview = rightView === "editor-preview";
9698
9819
  const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
9699
9820
  const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "");
9700
- const isLatex = htmlArtifactSource ? false : (isEditorPreview
9821
+ const isLatex = exportingSideThread ? false : (htmlArtifactSource ? false : (isEditorPreview
9701
9822
  ? 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");
9823
+ : /\\documentclass\b|\\begin\{document\}/.test(markdown)));
9824
+ let filenameHint = exportingSideThread
9825
+ ? getSideQuestionTranscriptFilename(sideThreadExportedAt).replace(/\.md$/i, ".html")
9826
+ : (exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html")));
9827
+ let titleHint = exportingSideThread ? "Pi Studio side questions" : (exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview"));
9705
9828
  if (sourcePath) {
9706
9829
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
9707
9830
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -9719,7 +9842,9 @@
9719
9842
  }
9720
9843
  previewExportInProgress = true;
9721
9844
  updateResultActionButtons();
9722
- setStatus(openTarget === "studio" ? "Exporting HTML for Studio…" : "Exporting HTML…", "warning");
9845
+ setStatus(exportingSideThread
9846
+ ? (openTarget === "studio" ? "Exporting side thread as HTML for Studio…" : "Exporting side thread as HTML…")
9847
+ : (openTarget === "studio" ? "Exporting HTML for Studio…" : "Exporting HTML…"), "warning");
9723
9848
 
9724
9849
  try {
9725
9850
  const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
@@ -9894,6 +10019,15 @@
9894
10019
 
9895
10020
  function exportRightPaneFormat(format) {
9896
10021
  closeExportPreviewMenu();
10022
+ if (format === "side-markdown-save") {
10023
+ return saveSideQuestionTranscriptMarkdown();
10024
+ }
10025
+ if (format === "side-markdown-copy") {
10026
+ return copySideQuestionTranscriptMarkdown();
10027
+ }
10028
+ if (format === "side-markdown-editor") {
10029
+ return openSideQuestionTranscriptInEditor();
10030
+ }
9897
10031
  if (format === "html-studio") {
9898
10032
  return exportRightPaneHtml({ openTarget: "studio" });
9899
10033
  }
@@ -11773,10 +11907,650 @@
11773
11907
  }
11774
11908
  }
11775
11909
 
11910
+ function isSideQuestionConnectionReady() {
11911
+ return Boolean(ws && ws.readyState === WebSocket.OPEN);
11912
+ }
11913
+
11914
+ function getSideQuestionSelectedResponseText() {
11915
+ const selected = getSelectedHistoryItem();
11916
+ return selected && typeof selected.markdown === "string" ? selected.markdown : latestResponseMarkdown;
11917
+ }
11918
+
11919
+ function getSideQuestionGatherScope() {
11920
+ if (sideQuestionUi.gatherScope) return sideQuestionUi.gatherScope;
11921
+ return sideQuestionHelpers.getDefaultStudioSideQuestionGatherScope({
11922
+ sourcePath: getEffectiveSavePath() || sourceState.path || "",
11923
+ resourceDir: getCurrentResourceDirValue(),
11924
+ });
11925
+ }
11926
+
11927
+ function getSideQuestionFocus() {
11928
+ return sideQuestionHelpers.chooseStudioSideQuestionFocus({
11929
+ mode: sideQuestionUi.focusMode,
11930
+ editorText: sourceTextEl.value || "",
11931
+ responseText: getSideQuestionSelectedResponseText(),
11932
+ selectionStart: sourceTextEl.selectionStart,
11933
+ selectionEnd: sourceTextEl.selectionEnd,
11934
+ language: editorLanguage,
11935
+ });
11936
+ }
11937
+
11938
+ function persistSideQuestionToolSelection() {
11939
+ try {
11940
+ if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_TOOLS_STORAGE_KEY, JSON.stringify(sideQuestionUi.toolIds.slice(0, 12)));
11941
+ } catch {}
11942
+ }
11943
+
11944
+ function applySideQuestionToolCatalog(value) {
11945
+ const seen = new Set();
11946
+ sideQuestionAvailablePiTools = (Array.isArray(value) ? value : []).flatMap((entry) => {
11947
+ if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.name !== "string") return [];
11948
+ const id = entry.id.trim().toLowerCase();
11949
+ const name = entry.name.trim();
11950
+ if (!/^[a-f0-9]{24}$/.test(id) || !name || name.length > 200 || seen.has(id)) return [];
11951
+ seen.add(id);
11952
+ return [{
11953
+ id,
11954
+ name,
11955
+ description: typeof entry.description === "string" ? entry.description.trim().slice(0, 1000) : "",
11956
+ source: typeof entry.source === "string" ? entry.source.trim().slice(0, 500) : "Extension tool",
11957
+ gateway: entry.gateway === true,
11958
+ }];
11959
+ }).slice(0, 200);
11960
+ const available = new Set(sideQuestionAvailablePiTools.map((tool) => tool.id));
11961
+ const nextSelection = sideQuestionUi.toolIds.filter((id) => available.has(id)).slice(0, 12);
11962
+ if (nextSelection.length !== sideQuestionUi.toolIds.length || nextSelection.some((id, index) => id !== sideQuestionUi.toolIds[index])) {
11963
+ sideQuestionUi.toolIds = nextSelection;
11964
+ persistSideQuestionToolSelection();
11965
+ }
11966
+ }
11967
+
11968
+ function normalizeSideQuestionState(value) {
11969
+ const state = value && typeof value === "object" ? value : {};
11970
+ const messages = Array.isArray(state.messages) ? state.messages.map((entry) => ({
11971
+ id: typeof entry.id === "string" ? entry.id : makeRequestId(),
11972
+ role: entry.role === "assistant" ? "assistant" : "user",
11973
+ text: typeof entry.text === "string" ? entry.text : "",
11974
+ createdAt: typeof entry.createdAt === "number" ? entry.createdAt : Date.now(),
11975
+ status: entry.status === "streaming" || entry.status === "error" ? entry.status : "complete",
11976
+ })).slice(-24) : [];
11977
+ const activity = Array.isArray(state.activity) ? state.activity.map((entry) => ({
11978
+ id: typeof entry.id === "string" ? entry.id : makeRequestId(),
11979
+ toolName: typeof entry.toolName === "string" ? entry.toolName : "tool",
11980
+ label: typeof entry.label === "string" ? entry.label : "Gathering context",
11981
+ status: entry.status === "running" || entry.status === "error" ? entry.status : "complete",
11982
+ })).slice(-40) : [];
11983
+ const context = state.context && typeof state.context === "object" ? {
11984
+ focusKind: String(state.context.focusKind || "editor"),
11985
+ focusLabel: String(state.context.focusLabel || "Studio editor context"),
11986
+ gatherScope: String(state.context.gatherScope || "none"),
11987
+ contextRoot: String(state.context.contextRoot || ""),
11988
+ includeConversation: state.context.includeConversation === true,
11989
+ gitContextRequested: state.context.gitContextRequested === true,
11990
+ gitSnapshot: state.context.gitSnapshot && typeof state.context.gitSnapshot === "object" ? {
11991
+ capturedAt: Number.isFinite(state.context.gitSnapshot.capturedAt) ? state.context.gitSnapshot.capturedAt : null,
11992
+ branch: String(state.context.gitSnapshot.branch || ""),
11993
+ head: String(state.context.gitSnapshot.head || ""),
11994
+ changeCount: Number.isFinite(state.context.gitSnapshot.changeCount) ? Math.max(0, Math.floor(state.context.gitSnapshot.changeCount)) : 0,
11995
+ recentCommitCount: Number.isFinite(state.context.gitSnapshot.recentCommitCount) ? Math.max(0, Math.floor(state.context.gitSnapshot.recentCommitCount)) : 0,
11996
+ truncated: state.context.gitSnapshot.truncated === true,
11997
+ } : null,
11998
+ webSearchRequested: state.context.webSearchRequested === true,
11999
+ webSearchAvailable: state.context.webSearchAvailable === true,
12000
+ tools: Array.isArray(state.context.tools) ? state.context.tools.flatMap((tool) => {
12001
+ if (!tool || typeof tool !== "object" || typeof tool.name !== "string") return [];
12002
+ return [{
12003
+ id: typeof tool.id === "string" ? tool.id : "",
12004
+ name: tool.name,
12005
+ description: typeof tool.description === "string" ? tool.description : "",
12006
+ source: typeof tool.source === "string" ? tool.source : "Extension tool",
12007
+ gateway: tool.gateway === true,
12008
+ }];
12009
+ }).slice(0, 12) : [],
12010
+ } : null;
12011
+ return {
12012
+ threadId: typeof state.threadId === "string" && state.threadId ? state.threadId : null,
12013
+ status: state.status === "running" || state.status === "error" ? state.status : "idle",
12014
+ requestId: typeof state.requestId === "string" ? state.requestId : null,
12015
+ createdAt: Number.isFinite(state.createdAt) ? state.createdAt : null,
12016
+ updatedAt: Number.isFinite(state.updatedAt) ? state.updatedAt : null,
12017
+ context,
12018
+ modelLabel: String(state.modelLabel || ""),
12019
+ thinking: String(state.thinking || "low"),
12020
+ messages,
12021
+ activity,
12022
+ error: String(state.error || ""),
12023
+ };
12024
+ }
12025
+
12026
+ function getLatestCompletedSideQuestionAnswer() {
12027
+ if (!sideQuestionState || !Array.isArray(sideQuestionState.messages)) return null;
12028
+ return [...sideQuestionState.messages].reverse().find((entry) => entry.role === "assistant" && entry.status === "complete" && entry.text.trim()) || null;
12029
+ }
12030
+
12031
+ function canExportSideQuestionTranscript() {
12032
+ return Boolean(
12033
+ sideQuestionState
12034
+ && sideQuestionState.threadId
12035
+ && sideQuestionState.status !== "running"
12036
+ && Array.isArray(sideQuestionState.messages)
12037
+ && sideQuestionState.messages.length > 0
12038
+ );
12039
+ }
12040
+
12041
+ function buildCurrentSideQuestionTranscriptMarkdown(exportedAt) {
12042
+ if (!canExportSideQuestionTranscript()) return "";
12043
+ return sideQuestionHelpers.buildStudioSideQuestionTranscriptMarkdown(sideQuestionState, {
12044
+ exportedAt: exportedAt instanceof Date ? exportedAt : new Date(),
12045
+ });
12046
+ }
12047
+
12048
+ function getSideQuestionTranscriptFilename(date) {
12049
+ return sideQuestionHelpers.formatStudioSideQuestionTranscriptFilename(date instanceof Date ? date : new Date());
12050
+ }
12051
+
12052
+ function getSideQuestionTranscriptSuggestedPath(date) {
12053
+ const filename = getSideQuestionTranscriptFilename(date);
12054
+ const contextRoot = sideQuestionState && sideQuestionState.context ? String(sideQuestionState.context.contextRoot || "") : "";
12055
+ const directory = getCurrentResourceDirValue() || contextRoot || ".";
12056
+ return directory.replace(/[\\/]$/, "") + "/" + filename;
12057
+ }
12058
+
12059
+ function getSideQuestionEditorLineRange(focus) {
12060
+ if (!focus || !Number.isFinite(focus.start) || !Number.isFinite(focus.end)) return "";
12061
+ const source = sourceTextEl.value || "";
12062
+ const start = Math.max(0, Math.min(source.length, Math.floor(focus.start)));
12063
+ const end = Math.max(start, Math.min(source.length, Math.floor(focus.end)));
12064
+ const firstLine = source.slice(0, start).split("\n").length;
12065
+ const lastOffset = end > start ? end - 1 : start;
12066
+ const lastLine = source.slice(0, lastOffset).split("\n").length;
12067
+ return firstLine === lastLine ? "line " + firstLine : "lines " + firstLine + "–" + lastLine;
12068
+ }
12069
+
12070
+ function getSideQuestionContextSummary() {
12071
+ const focus = getSideQuestionFocus();
12072
+ const scope = getSideQuestionGatherScope();
12073
+ const sourcePath = getEffectiveSavePath() || sourceState.path || "";
12074
+ const resourceDir = getCurrentResourceDirValue();
12075
+ const rootHint = scope === "custom"
12076
+ ? sideQuestionUi.customPath
12077
+ : (scope === "repo"
12078
+ ? "Current repository"
12079
+ : (sourcePath ? dirnameForDisplayPath(sourcePath) : (resourceDir || "current Pi working directory")));
12080
+ const lineRange = getSideQuestionEditorLineRange(focus);
12081
+ const attachment = focus.focusKind === "none"
12082
+ ? focus.focusLabel
12083
+ : focus.focusLabel + (lineRange ? " · " + lineRange : "") + " · " + String(focus.focusText.length).toLocaleString("en-US") + " chars";
12084
+ return {
12085
+ focus,
12086
+ scope,
12087
+ rootHint,
12088
+ sourcePath,
12089
+ resourceDir,
12090
+ attachmentText: attachment,
12091
+ relatedFilesText: scope === "none" ? "None" : (rootHint || scope) + " · read only as needed",
12092
+ gitContextText: scope === "repo" && sideQuestionUi.gitContext
12093
+ ? "Status, staged and unstaged changes, and up to 20 recent commits · read only · frozen when thread starts"
12094
+ : "",
12095
+ };
12096
+ }
12097
+
12098
+ function buildSideQuestionContextPayload() {
12099
+ const summary = getSideQuestionContextSummary();
12100
+ return {
12101
+ focusKind: summary.focus.focusKind,
12102
+ focusLabel: summary.focus.focusLabel,
12103
+ focusText: summary.focus.focusText,
12104
+ sourcePath: summary.sourcePath || undefined,
12105
+ resourceDir: summary.resourceDir || undefined,
12106
+ gatherScope: summary.scope,
12107
+ contextPath: summary.scope === "custom" ? sideQuestionUi.customPath.trim() : undefined,
12108
+ includeConversation: sideQuestionUi.includeConversation,
12109
+ gitContext: summary.scope === "repo" && sideQuestionUi.gitContext,
12110
+ webSearch: sideQuestionUi.webSearch && sideQuestionWebSearchAvailable,
12111
+ toolIds: sideQuestionUi.toolIds.slice(0, 12),
12112
+ thinking: sideQuestionUi.thinking,
12113
+ };
12114
+ }
12115
+
12116
+ async function renderSideQuestionMarkdownToHtml(markdown) {
12117
+ const source = String(markdown || "");
12118
+ if (sideQuestionMarkdownRenderCache.has(source)) return sideQuestionMarkdownRenderCache.get(source);
12119
+ const renderedHtml = await renderMarkdownWithPandoc(source, { includeEditorLanguage: false });
12120
+ const sanitized = sanitizeRenderedHtml(renderedHtml, source, { stripMarkdownHtmlComments: true });
12121
+ sideQuestionMarkdownRenderCache.set(source, sanitized);
12122
+ while (sideQuestionMarkdownRenderCache.size > 60) {
12123
+ const firstKey = sideQuestionMarkdownRenderCache.keys().next().value;
12124
+ if (!firstKey) break;
12125
+ sideQuestionMarkdownRenderCache.delete(firstKey);
12126
+ }
12127
+ return sanitized;
12128
+ }
12129
+
12130
+ async function renderSideQuestionMarkdownFields(nonce) {
12131
+ if (!critiqueViewEl || rightView !== "side-questions") return;
12132
+ const targets = Array.from(critiqueViewEl.querySelectorAll("[data-side-question-markdown]")).filter((target) => target instanceof HTMLElement);
12133
+ for (const target of targets) {
12134
+ const markdown = target.getAttribute("data-side-question-markdown") || "";
12135
+ if (!markdown.trim()) continue;
12136
+ try {
12137
+ const html = await renderSideQuestionMarkdownToHtml(markdown);
12138
+ if (nonce !== sideQuestionPreviewRenderNonce || rightView !== "side-questions" || !critiqueViewEl.contains(target)) return;
12139
+ target.innerHTML = html;
12140
+ await renderAnnotationMathInElement(target);
12141
+ await renderMermaidInElement(target);
12142
+ await renderMathFallbackInElement(target);
12143
+ decorateCopyablePreviewBlocks(target);
12144
+ } catch (error) {
12145
+ console.error("Side-question markdown preview failed:", error);
12146
+ target.classList.add("side-question-markdown-failed");
12147
+ }
12148
+ }
12149
+ }
12150
+
12151
+ function sideQuestionSelectOptions(values, current) {
12152
+ return values.map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (value === current ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
12153
+ }
12154
+
12155
+ function renderSideQuestionPiToolPicker() {
12156
+ const selected = new Set(sideQuestionUi.toolIds);
12157
+ const selectedCount = sideQuestionUi.toolIds.length;
12158
+ if (!sideQuestionAvailablePiTools.length) {
12159
+ return "<div class='side-question-tool-empty'><strong>Additional Pi tools</strong><span>No eligible extension tools are currently available.</span></div>";
12160
+ }
12161
+ const groups = new Map();
12162
+ for (const tool of sideQuestionAvailablePiTools) {
12163
+ if (!groups.has(tool.source)) groups.set(tool.source, []);
12164
+ groups.get(tool.source).push(tool);
12165
+ }
12166
+ const groupHtml = [...groups.entries()].map(([source, tools]) => {
12167
+ const toolHtml = tools.map((tool) => {
12168
+ const description = tool.description || "No description supplied by this extension.";
12169
+ return "<label class='side-question-tool-option' title='" + escapeHtml(description) + "'>"
12170
+ + "<input type='checkbox' data-side-question-tool='" + escapeHtml(tool.id) + "' data-side-question-tool-name='" + escapeHtml(tool.name) + "'" + (selected.has(tool.id) ? " checked" : "") + ">"
12171
+ + "<span><code>" + escapeHtml(tool.name) + "</code><small>" + escapeHtml(description) + "</small></span>"
12172
+ + (tool.gateway ? "<em>gateway</em>" : "")
12173
+ + "</label>";
12174
+ }).join("");
12175
+ return "<section class='side-question-tool-group'><h3>" + escapeHtml(source) + "</h3>" + toolHtml + "</section>";
12176
+ }).join("");
12177
+ return "<details class='side-question-tool-picker'" + (selectedCount ? " open" : "") + ">"
12178
+ + "<summary>Additional Pi tools · " + selectedCount + " selected</summary>"
12179
+ + "<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>"
12180
+ + "<div class='side-question-tool-groups'>" + groupHtml + "</div>"
12181
+ + "</details>";
12182
+ }
12183
+
12184
+ function renderSideQuestionSetup() {
12185
+ const summary = getSideQuestionContextSummary();
12186
+ const scope = summary.scope;
12187
+ const webDisabled = !sideQuestionWebSearchAvailable;
12188
+ return "<div class='side-question-empty'>"
12189
+ + "<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>"
12190
+ + "<div class='side-question-context-grid'>"
12191
+ + "<label>Starting text<select data-side-question-field='focusMode' aria-describedby='sideQuestionContextRule'>" + sideQuestionSelectOptions([
12192
+ ["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"],
12193
+ ], sideQuestionUi.focusMode) + "</select></label>"
12194
+ + "<label>Also use files from<select data-side-question-field='gatherScope'>" + sideQuestionSelectOptions([
12195
+ ["none", "No other files"], ["folder", "Same folder as document"], ["repo", "Repository"], ["custom", "Choose a folder"],
12196
+ ], scope) + "</select></label>"
12197
+ + "<label>Thinking<select data-side-question-field='thinking'>" + sideQuestionSelectOptions([
12198
+ ["off", "Off"], ["minimal", "Minimal"], ["low", "Low"], ["medium", "Medium"], ["high", "High"],
12199
+ ], sideQuestionUi.thinking) + "</select></label>"
12200
+ + "</div>"
12201
+ + "<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>"
12202
+ + (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>" : "")
12203
+ + "<div class='side-question-checks'>"
12204
+ + "<label><input data-side-question-field='includeConversation' type='checkbox'" + (sideQuestionUi.includeConversation ? " checked" : "") + "> Include the current main conversation snapshot</label>"
12205
+ + (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>" : "")
12206
+ + "<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>"
12207
+ + "</div>"
12208
+ + renderSideQuestionPiToolPicker()
12209
+ + "<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>"
12210
+ + "<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>"
12211
+ + (sideQuestionState && sideQuestionState.error ? "<div class='side-question-error'>" + escapeHtml(sideQuestionState.error) + "</div>" : "")
12212
+ + "<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>"
12213
+ + "</div>";
12214
+ }
12215
+
12216
+ function renderSideQuestionThread() {
12217
+ const state = sideQuestionState;
12218
+ const context = state.context || {};
12219
+ const latestAnswer = getLatestCompletedSideQuestionAnswer();
12220
+ const messageHtml = state.messages.map((message) => {
12221
+ const roleLabel = message.role === "user" ? "You" : "Side answer";
12222
+ const body = message.role === "assistant" && message.status === "complete"
12223
+ ? "<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>"
12224
+ : "<div class='side-question-message-body side-question-plain'>" + escapeHtml(message.text || (message.status === "streaming" ? "Thinking…" : "")) + "</div>";
12225
+ return "<article class='side-question-message side-question-message-" + message.role + " side-question-status-" + message.status + "'>"
12226
+ + "<div class='side-question-message-label'>" + roleLabel + (message.status === "streaming" ? " <span class='side-question-live'>●</span>" : "") + "</div>" + body + "</article>";
12227
+ }).join("");
12228
+ const activityHtml = state.activity.length
12229
+ ? "<details class='side-question-activity'" + (state.status === "running" ? " open" : "") + "><summary>Gathered context · " + state.activity.length + " action" + (state.activity.length === 1 ? "" : "s") + "</summary><ul>"
12230
+ + 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>"
12231
+ : "";
12232
+ const webLabel = context.webSearchRequested
12233
+ ? (context.webSearchAvailable ? "web allowed" : "web unavailable")
12234
+ : "web off";
12235
+ const selectedToolLabel = Array.isArray(context.tools) && context.tools.length
12236
+ ? "Additional Pi tools: " + context.tools.map((tool) => tool.name).join(", ")
12237
+ : "Additional Pi tools off";
12238
+ const gitSnapshot = context.gitSnapshot && typeof context.gitSnapshot === "object" ? context.gitSnapshot : null;
12239
+ const gitCapturedLabel = gitSnapshot && gitSnapshot.capturedAt ? formatReferenceTime(gitSnapshot.capturedAt) : "";
12240
+ const gitLabel = gitSnapshot
12241
+ ? "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" : "")
12242
+ : "";
12243
+ return "<div class='side-question-thread'>"
12244
+ + "<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>"
12245
+ + "<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>"
12246
+ + (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>"
12247
+ + "<div class='side-question-transcript'>" + messageHtml + "</div>"
12248
+ + activityHtml
12249
+ + (state.error ? "<div class='side-question-error'>" + escapeHtml(state.error) + "</div>" : "")
12250
+ + (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>" : "")
12251
+ + "<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>"
12252
+ + "<div class='side-question-actions'>"
12253
+ + (state.status === "running"
12254
+ ? "<button type='button' class='side-question-stop' data-side-question-action='stop'>Stop</button>"
12255
+ : "<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>")
12256
+ + "</div></div>";
12257
+ }
12258
+
12259
+ function scheduleSideQuestionContextRefresh() {
12260
+ if (rightView !== "side-questions" || (sideQuestionState && sideQuestionState.threadId) || sideQuestionContextRefreshHandle !== null) return;
12261
+ sideQuestionContextRefreshHandle = window.setTimeout(() => {
12262
+ sideQuestionContextRefreshHandle = null;
12263
+ if (rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) renderSideQuestionView();
12264
+ }, 40);
12265
+ }
12266
+
12267
+ function renderSideQuestionView(options) {
12268
+ if (!critiqueViewEl || rightView !== "side-questions") return;
12269
+ finishPreviewRender(critiqueViewEl);
12270
+ const scrollTop = critiqueViewEl.scrollTop;
12271
+ const nearBottom = critiqueViewEl.scrollHeight - critiqueViewEl.clientHeight - critiqueViewEl.scrollTop < 100;
12272
+ critiqueViewEl.innerHTML = sideQuestionState && sideQuestionState.threadId ? renderSideQuestionThread() : renderSideQuestionSetup();
12273
+ const nonce = ++sideQuestionPreviewRenderNonce;
12274
+ void renderSideQuestionMarkdownFields(nonce);
12275
+ if ((options && options.followBottom) || nearBottom) {
12276
+ critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
12277
+ window.requestAnimationFrame(() => { if (rightView === "side-questions") critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight; });
12278
+ } else {
12279
+ critiqueViewEl.scrollTop = scrollTop;
12280
+ }
12281
+ }
12282
+
12283
+ function submitSideQuestion() {
12284
+ const question = String(sideQuestionUi.draft || "").trim();
12285
+ if (!question) {
12286
+ setStatus("Enter a side question first.", "warning");
12287
+ return;
12288
+ }
12289
+ if (!isSideQuestionConnectionReady()) {
12290
+ setStatus("Studio is disconnected.", "warning");
12291
+ return;
12292
+ }
12293
+ const requestId = makeRequestId();
12294
+ const message = {
12295
+ type: "side_question_ask_request",
12296
+ requestId,
12297
+ question,
12298
+ };
12299
+ if (sideQuestionState && sideQuestionState.threadId) {
12300
+ message.threadId = sideQuestionState.threadId;
12301
+ } else {
12302
+ message.context = buildSideQuestionContextPayload();
12303
+ if (message.context.gatherScope === "custom" && !String(message.context.contextPath || "").trim()) {
12304
+ setStatus("Choose a custom context path first.", "warning");
12305
+ return;
12306
+ }
12307
+ }
12308
+ if (!sendMessage(message)) return;
12309
+ sideQuestionUi.draft = "";
12310
+ if (!sideQuestionState) sideQuestionState = normalizeSideQuestionState(null);
12311
+ sideQuestionState.status = "running";
12312
+ sideQuestionState.requestId = requestId;
12313
+ sideQuestionState.error = "";
12314
+ renderSideQuestionView({ followBottom: true });
12315
+ updateReferenceBadge();
12316
+ syncAskAsideButton();
12317
+ updateResultActionButtons();
12318
+ setStatus("Side question running independently of the main conversation…", "warning");
12319
+ }
12320
+
12321
+ function sendSideQuestionMarkdownExport(path, content, overwrite) {
12322
+ if (sideQuestionMarkdownExportRequest) {
12323
+ setStatus("A side-thread Markdown export is already in progress.", "warning");
12324
+ return false;
12325
+ }
12326
+ if (!isSideQuestionConnectionReady()) {
12327
+ setStatus("Studio is disconnected.", "warning");
12328
+ return false;
12329
+ }
12330
+ const requestId = makeRequestId();
12331
+ sideQuestionMarkdownExportRequest = {
12332
+ requestId,
12333
+ threadId: sideQuestionState && sideQuestionState.threadId ? sideQuestionState.threadId : "",
12334
+ path: String(path || ""),
12335
+ content: String(content || ""),
12336
+ };
12337
+ const sent = sendMessage({
12338
+ type: "side_question_export_markdown_request",
12339
+ requestId,
12340
+ threadId: sideQuestionMarkdownExportRequest.threadId,
12341
+ path: String(path || ""),
12342
+ content: String(content || ""),
12343
+ overwrite: overwrite === true,
12344
+ });
12345
+ if (!sent) sideQuestionMarkdownExportRequest = null;
12346
+ updateResultActionButtons();
12347
+ if (sent) setStatus(overwrite ? "Replacing side-thread Markdown export…" : "Saving side-thread Markdown…", "warning");
12348
+ return sent;
12349
+ }
12350
+
12351
+ async function saveSideQuestionTranscriptMarkdown() {
12352
+ const exportedAt = new Date();
12353
+ const markdown = buildCurrentSideQuestionTranscriptMarkdown(exportedAt);
12354
+ if (!markdown.trim()) {
12355
+ setStatus("No completed side discussion is available to save.", "warning");
12356
+ return;
12357
+ }
12358
+ const path = await requestStudioTextInput(
12359
+ "Save the visible side-thread transcript and context summary as Markdown. Hidden source text and raw tool output are not included.",
12360
+ getSideQuestionTranscriptSuggestedPath(exportedAt),
12361
+ {
12362
+ title: "Save side-question transcript",
12363
+ inputLabel: "Markdown path on computer running Pi",
12364
+ confirmLabel: "Save Markdown",
12365
+ },
12366
+ );
12367
+ if (!path) return;
12368
+ sendSideQuestionMarkdownExport(path, markdown, false);
12369
+ }
12370
+
12371
+ async function copySideQuestionTranscriptMarkdown() {
12372
+ const markdown = buildCurrentSideQuestionTranscriptMarkdown(new Date());
12373
+ if (!markdown.trim()) {
12374
+ setStatus("No completed side discussion is available to copy.", "warning");
12375
+ return;
12376
+ }
12377
+ const copied = await writeTextToClipboard(markdown);
12378
+ setStatus(copied ? "Copied side-thread Markdown." : "Clipboard write failed.", copied ? "success" : "warning");
12379
+ }
12380
+
12381
+ function openSideQuestionTranscriptInEditor() {
12382
+ const exportedAt = new Date();
12383
+ const markdown = buildCurrentSideQuestionTranscriptMarkdown(exportedAt);
12384
+ if (!markdown.trim()) {
12385
+ setStatus("No completed side discussion is available to open.", "warning");
12386
+ return;
12387
+ }
12388
+ if (markdown.length > SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS) {
12389
+ setStatus("This side-thread transcript is too large for a companion editor. Save or copy the Markdown instead.", "warning");
12390
+ return;
12391
+ }
12392
+ requestOpenEditorOnlyDocument(markdown, {
12393
+ label: getSideQuestionTranscriptFilename(exportedAt),
12394
+ resourceDir: getCurrentResourceDirValue() || (sideQuestionState.context && sideQuestionState.context.contextRoot) || undefined,
12395
+ });
12396
+ }
12397
+
12398
+ function insertLatestSideQuestionAnswer() {
12399
+ const answer = getLatestCompletedSideQuestionAnswer();
12400
+ if (!answer) return;
12401
+ const current = sourceTextEl.value || "";
12402
+ const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : current.length;
12403
+ const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
12404
+ const safeStart = Math.max(0, Math.min(start, current.length));
12405
+ const safeEnd = Math.max(safeStart, Math.min(end, current.length));
12406
+ const next = current.slice(0, safeStart) + answer.text + current.slice(safeEnd);
12407
+ setEditorText(next, { preserveScroll: false, preserveSelection: false });
12408
+ const caret = safeStart + answer.text.length;
12409
+ sourceTextEl.setSelectionRange(caret, caret);
12410
+ setActivePane("left");
12411
+ focusSourceTextNoScroll();
12412
+ setStatus("Inserted the latest side answer at the editor cursor.", "success");
12413
+ }
12414
+
12415
+ async function handleSideQuestionClick(event) {
12416
+ if (rightView !== "side-questions") return;
12417
+ const target = event && event.target instanceof Element ? event.target.closest("[data-side-question-action]") : null;
12418
+ if (!target || !critiqueViewEl.contains(target)) return;
12419
+ event.preventDefault();
12420
+ const action = target.getAttribute("data-side-question-action");
12421
+ if (action === "ask") {
12422
+ submitSideQuestion();
12423
+ } else if (action === "stop") {
12424
+ if (sideQuestionState && sideQuestionState.threadId && sideQuestionState.requestId) {
12425
+ sendMessage({ type: "side_question_cancel_request", threadId: sideQuestionState.threadId, requestId: sideQuestionState.requestId });
12426
+ setStatus("Stopping side question…", "warning");
12427
+ }
12428
+ } else if (action === "new") {
12429
+ const confirmed = !sideQuestionState || !sideQuestionState.messages.length || await requestStudioConfirmation(
12430
+ "Clear this ephemeral side thread and choose fresh context? Nothing has been added to the main conversation.",
12431
+ { title: "Start a new side thread?", confirmLabel: "New thread", destructive: true },
12432
+ );
12433
+ if (confirmed) {
12434
+ sendMessage({ type: "side_question_clear_request", threadId: sideQuestionState && sideQuestionState.threadId ? sideQuestionState.threadId : undefined });
12435
+ sideQuestionState = null;
12436
+ sideQuestionUi = {
12437
+ ...sideQuestionUi,
12438
+ focusMode: "auto",
12439
+ customPath: "",
12440
+ includeConversation: false,
12441
+ gitContext: false,
12442
+ webSearch: false,
12443
+ draft: "",
12444
+ };
12445
+ renderSideQuestionView();
12446
+ updateResultActionButtons();
12447
+ }
12448
+ } else if (action === "copy") {
12449
+ const answer = getLatestCompletedSideQuestionAnswer();
12450
+ if (answer) {
12451
+ const copied = await writeTextToClipboard(answer.text);
12452
+ setStatus(copied ? "Copied latest side answer." : "Could not copy side answer.", copied ? "success" : "error");
12453
+ }
12454
+ } else if (action === "insert") {
12455
+ insertLatestSideQuestionAnswer();
12456
+ } else if (action === "promote") {
12457
+ if (sideQuestionState && sideQuestionState.threadId) {
12458
+ const confirmed = await requestStudioConfirmation(
12459
+ "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.",
12460
+ { title: "Bring side answer to main?", confirmLabel: "Bring to main" },
12461
+ );
12462
+ if (confirmed) sendMessage({ type: "side_question_promote_request", threadId: sideQuestionState.threadId });
12463
+ }
12464
+ }
12465
+ }
12466
+
12467
+ function handleSideQuestionInput(event) {
12468
+ if (rightView !== "side-questions") return;
12469
+ const target = event && event.target;
12470
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) return;
12471
+ const field = target.getAttribute("data-side-question-field");
12472
+ if (field === "draft") sideQuestionUi.draft = target.value;
12473
+ if (field === "customPath") sideQuestionUi.customPath = target.value;
12474
+ const askButton = critiqueViewEl.querySelector("[data-side-question-action='ask']");
12475
+ if (askButton) askButton.disabled = !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
12476
+ }
12477
+
12478
+ function handleSideQuestionKeydown(event) {
12479
+ if (rightView !== "side-questions" || !event || event.isComposing) return;
12480
+ const target = event.target;
12481
+ if (!(target instanceof HTMLTextAreaElement) || target.getAttribute("data-side-question-field") !== "draft") return;
12482
+ const submitShortcut = event.key === "Enter"
12483
+ && (event.metaKey || event.ctrlKey)
12484
+ && !event.altKey
12485
+ && !event.shiftKey;
12486
+ if (!submitShortcut) return;
12487
+ event.preventDefault();
12488
+ event.stopPropagation();
12489
+ if (!sideQuestionState || sideQuestionState.status !== "running") submitSideQuestion();
12490
+ }
12491
+
12492
+ async function handleSideQuestionChange(event) {
12493
+ if (rightView !== "side-questions") return;
12494
+ const target = event && event.target;
12495
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) return;
12496
+ const toolId = target.getAttribute("data-side-question-tool");
12497
+ if (toolId) {
12498
+ const tool = sideQuestionAvailablePiTools.find((candidate) => candidate.id === toolId);
12499
+ if (!tool) return;
12500
+ const selected = new Set(sideQuestionUi.toolIds);
12501
+ if (target.checked) {
12502
+ if (tool.gateway && !selected.has(toolId)) {
12503
+ const confirmed = await requestStudioConfirmation(
12504
+ `“${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.`,
12505
+ { title: "Allow gateway tool in side questions?", confirmLabel: "Allow gateway" },
12506
+ );
12507
+ if (!confirmed) {
12508
+ target.checked = false;
12509
+ renderSideQuestionView();
12510
+ return;
12511
+ }
12512
+ }
12513
+ if (selected.size >= 12 && !selected.has(toolId)) {
12514
+ setStatus("Select at most 12 additional Pi tools.", "warning");
12515
+ renderSideQuestionView();
12516
+ return;
12517
+ }
12518
+ selected.add(toolId);
12519
+ } else {
12520
+ selected.delete(toolId);
12521
+ }
12522
+ sideQuestionUi.toolIds = [...selected];
12523
+ persistSideQuestionToolSelection();
12524
+ renderSideQuestionView();
12525
+ return;
12526
+ }
12527
+ const field = target.getAttribute("data-side-question-field");
12528
+ if (!field) return;
12529
+ if (field === "focusMode") sideQuestionUi.focusMode = target.value;
12530
+ if (field === "gatherScope") {
12531
+ sideQuestionUi.gatherScope = target.value;
12532
+ if (sideQuestionUi.gatherScope !== "repo") sideQuestionUi.gitContext = false;
12533
+ try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_GATHER_STORAGE_KEY, sideQuestionUi.gatherScope); } catch {}
12534
+ }
12535
+ if (field === "thinking") {
12536
+ sideQuestionUi.thinking = target.value;
12537
+ try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_THINKING_STORAGE_KEY, sideQuestionUi.thinking); } catch {}
12538
+ }
12539
+ if (field === "includeConversation") sideQuestionUi.includeConversation = target.checked;
12540
+ if (field === "gitContext") sideQuestionUi.gitContext = target.checked && getSideQuestionGatherScope() === "repo";
12541
+ if (field === "webSearch") sideQuestionUi.webSearch = target.checked;
12542
+ renderSideQuestionView();
12543
+ }
12544
+
11776
12545
  function renderActiveResult() {
11777
12546
  if (critiqueViewEl) {
11778
12547
  critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
11779
12548
  critiqueViewEl.classList.toggle("quarto-preview-host", rightView === "editor-quarto-preview");
12549
+ critiqueViewEl.classList.toggle("side-question-host", rightView === "side-questions");
12550
+ }
12551
+ if (rightView === "side-questions") {
12552
+ renderSideQuestionView();
12553
+ return;
11780
12554
  }
11781
12555
  if (rightView === "editor-quarto-preview") {
11782
12556
  renderQuartoPreviewView();
@@ -11880,7 +12654,7 @@
11880
12654
  : normalizeForCompare(sourceTextEl.value);
11881
12655
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
11882
12656
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
11883
- const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes" || rightView === "editor-quarto-preview";
12657
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes" || rightView === "editor-quarto-preview" || rightView === "side-questions";
11884
12658
 
11885
12659
  if (responseWrapEl) {
11886
12660
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -11918,28 +12692,61 @@
11918
12692
 
11919
12693
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
11920
12694
  const exportingReplJournal = rightView === "repl";
12695
+ const exportingSideThread = rightView === "side-questions";
11921
12696
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
12697
+ const sideThreadExportText = exportingSideThread && canExportSideQuestionTranscript()
12698
+ ? buildCurrentSideQuestionTranscriptMarkdown(new Date())
12699
+ : "";
11922
12700
  const exportText = exportingReplJournal
11923
12701
  ? (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() : "";
12702
+ : (exportingSideThread
12703
+ ? sideThreadExportText
12704
+ : (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown));
12705
+ const canExportPreview = (rightPaneShowsPreview || exportingReplJournal || exportingSideThread) && Boolean(String(exportText || "").trim());
12706
+ const htmlArtifactExportSource = canExportPreview && !exportingReplJournal && !exportingSideThread ? getRightPaneHtmlArtifactSource() : "";
11927
12707
  const isHtmlArtifactPreview = Boolean(htmlArtifactExportSource);
12708
+ const sideThreadRenderTooLarge = exportingSideThread && sideThreadExportText.length > SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS;
12709
+ const exportBusy = previewExportInProgress || Boolean(sideQuestionMarkdownExportRequest) || (!exportingSideThread && uiBusy);
12710
+ if (exportSideThreadMarkdownSaveBtn) {
12711
+ exportSideThreadMarkdownSaveBtn.hidden = !exportingSideThread;
12712
+ exportSideThreadMarkdownSaveBtn.disabled = Boolean(sideQuestionMarkdownExportRequest) || !canExportPreview;
12713
+ exportSideThreadMarkdownSaveBtn.title = "Save the visible discussion and context summary as a Markdown file on the computer running Pi.";
12714
+ }
12715
+ if (exportSideThreadMarkdownCopyBtn) {
12716
+ exportSideThreadMarkdownCopyBtn.hidden = !exportingSideThread;
12717
+ exportSideThreadMarkdownCopyBtn.disabled = Boolean(sideQuestionMarkdownExportRequest) || !canExportPreview;
12718
+ exportSideThreadMarkdownCopyBtn.title = "Copy the visible discussion and context summary as Markdown.";
12719
+ }
12720
+ if (exportSideThreadMarkdownEditorBtn) {
12721
+ exportSideThreadMarkdownEditorBtn.hidden = !exportingSideThread;
12722
+ exportSideThreadMarkdownEditorBtn.disabled = uiBusy || Boolean(sideQuestionMarkdownExportRequest) || !canExportPreview || sideThreadRenderTooLarge;
12723
+ exportSideThreadMarkdownEditorBtn.title = sideThreadRenderTooLarge
12724
+ ? "This transcript is too large for a companion editor; save or copy the Markdown instead."
12725
+ : "Open the Markdown transcript as an unsaved copy in a new Studio editor tab.";
12726
+ }
11928
12727
  if (exportPdfBtn) {
11929
- exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
12728
+ exportPdfBtn.disabled = exportBusy || !canExportPreview;
11930
12729
  exportPdfBtn.textContent = previewExportInProgress
11931
12730
  ? "Exporting…"
11932
- : (exportingReplJournal ? "Export record" : "Export right preview");
12731
+ : (sideQuestionMarkdownExportRequest
12732
+ ? "Saving…"
12733
+ : (exportingSideThread ? "Export thread" : (exportingReplJournal ? "Export record" : "Export right preview")));
11933
12734
  if (rightView === "trace") {
11934
12735
  exportPdfBtn.title = "Working view does not support preview export.";
11935
12736
  } else if (rightView === "files") {
11936
12737
  exportPdfBtn.title = "Files view does not support preview export.";
11937
12738
  } else if (rightView === "changes") {
11938
12739
  exportPdfBtn.title = "Changes view does not support preview export.";
12740
+ } else if (exportingSideThread && sideQuestionState && sideQuestionState.status === "running") {
12741
+ exportPdfBtn.title = "Wait for the current side answer before exporting the thread.";
12742
+ } else if (exportingSideThread && !canExportPreview) {
12743
+ exportPdfBtn.title = "No completed side discussion is available to export yet.";
12744
+ } else if (exportingSideThread) {
12745
+ exportPdfBtn.title = "Save or copy Markdown, open it in an editor, or export the visible side discussion as PDF or HTML.";
11939
12746
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
11940
12747
  exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
11941
12748
  } else if (rightView === "markdown") {
11942
- exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL to export.";
12749
+ exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export.";
11943
12750
  } else if (!canExportPreview) {
11944
12751
  exportPdfBtn.title = "Nothing to export yet.";
11945
12752
  } else if (isHtmlArtifactPreview) {
@@ -11950,39 +12757,52 @@
11950
12757
  exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
11951
12758
  }
11952
12759
  }
12760
+ if (exportSideThreadRenderSeparatorEl) exportSideThreadRenderSeparatorEl.hidden = !exportingSideThread;
11953
12761
  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.");
12762
+ exportPreviewPdfStudioBtn.disabled = exportBusy || !canExportPreview || isHtmlArtifactPreview || sideThreadRenderTooLarge;
12763
+ exportPreviewPdfStudioBtn.title = sideThreadRenderTooLarge
12764
+ ? "This transcript is too large for rendered export; save or copy the Markdown instead."
12765
+ : (isHtmlArtifactPreview
12766
+ ? "Interactive HTML preview PDF export is not available yet."
12767
+ : (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
12768
  }
11959
12769
  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.");
12770
+ exportPreviewPdfBtn.disabled = exportBusy || !canExportPreview || isHtmlArtifactPreview || sideThreadRenderTooLarge;
12771
+ exportPreviewPdfBtn.title = sideThreadRenderTooLarge
12772
+ ? "This transcript is too large for rendered export; save or copy the Markdown instead."
12773
+ : (isHtmlArtifactPreview
12774
+ ? "Interactive HTML preview PDF export is not available yet."
12775
+ : (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
12776
  }
11965
12777
  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.");
12778
+ exportPreviewHtmlStudioBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
12779
+ exportPreviewHtmlStudioBtn.title = sideThreadRenderTooLarge
12780
+ ? "This transcript is too large for rendered export; save or copy the Markdown instead."
12781
+ : (isHtmlArtifactPreview
12782
+ ? "Export the authored HTML preview and open it in a new Studio editor tab."
12783
+ : (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
12784
  }
11971
12785
  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.");
12786
+ exportPreviewHtmlBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
12787
+ exportPreviewHtmlBtn.title = sideThreadRenderTooLarge
12788
+ ? "This transcript is too large for rendered export; save or copy the Markdown instead."
12789
+ : (isHtmlArtifactPreview
12790
+ ? "Export the authored HTML preview and open it in the default browser."
12791
+ : (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
12792
  }
11977
12793
  if (exportPreviewControlsEl) {
11978
12794
  exportPreviewControlsEl.hidden = rightView === "editor-quarto-preview";
11979
12795
  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) {
12796
+ ? (exportingSideThread
12797
+ ? "Choose a durable export for the visible side discussion."
12798
+ : (exportingReplJournal
12799
+ ? "Choose a format and export destination for the Studio REPL record."
12800
+ : (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview.")))
12801
+ : (exportingSideThread
12802
+ ? "No completed side discussion is available to export yet."
12803
+ : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting."));
12804
+ }
12805
+ if (!canExportPreview || previewExportInProgress || sideQuestionMarkdownExportRequest) {
11986
12806
  closeExportPreviewMenu();
11987
12807
  }
11988
12808
 
@@ -11992,6 +12812,7 @@
11992
12812
  updateSyncBadge(normalizedEditor);
11993
12813
  syncStudioQuartoDirtyUi();
11994
12814
  syncShowMeButton();
12815
+ syncAskAsideButton();
11995
12816
  }
11996
12817
 
11997
12818
  function refreshResponseUi() {
@@ -13160,9 +13981,19 @@
13160
13981
  if (rightView === "editor-quarto-preview") {
13161
13982
  requestStudioQuartoPreviewCheck(false);
13162
13983
  }
13984
+ if (rightView === "side-questions" && previousView !== "side-questions") {
13985
+ if (!sideQuestionUi.gatherScope) sideQuestionUi.gatherScope = getSideQuestionGatherScope();
13986
+ sendMessage({ type: "side_question_get_state" });
13987
+ }
13163
13988
 
13164
13989
  refreshResponseUi();
13165
13990
  syncActionButtons();
13991
+ if (rightView === "side-questions" && previousView !== "side-questions") {
13992
+ window.setTimeout(() => {
13993
+ const composer = critiqueViewEl && critiqueViewEl.querySelector("[data-side-question-field='draft']");
13994
+ if (composer instanceof HTMLTextAreaElement) composer.focus({ preventScroll: true });
13995
+ }, 0);
13996
+ }
13166
13997
  scheduleWorkspacePersistence();
13167
13998
  }
13168
13999
 
@@ -20283,6 +21114,7 @@
20283
21114
  quizBtn.disabled = true;
20284
21115
  quizBtn.title = "Quiz is unavailable in editor-only mode.";
20285
21116
  }
21117
+ syncAskAsideButton();
20286
21118
  syncStudioUiRefreshReviewTrigger();
20287
21119
  return;
20288
21120
  }
@@ -20357,6 +21189,7 @@
20357
21189
  : "Open an active quiz for the current editor selection or document.");
20358
21190
  }
20359
21191
  syncShowMeButton();
21192
+ syncAskAsideButton();
20360
21193
  syncStudioUiRefreshReviewTrigger();
20361
21194
  }
20362
21195
 
@@ -20517,6 +21350,82 @@
20517
21350
  return;
20518
21351
  }
20519
21352
 
21353
+ if (message.type === "side_question_state") {
21354
+ sideQuestionWebSearchAvailable = message.webSearchAvailable === true;
21355
+ if (Array.isArray(message.availablePiTools)) applySideQuestionToolCatalog(message.availablePiTools);
21356
+ const previousStatus = sideQuestionState && sideQuestionState.status;
21357
+ sideQuestionState = normalizeSideQuestionState(message.state);
21358
+ if (rightView === "side-questions") renderSideQuestionView();
21359
+ updateReferenceBadge();
21360
+ syncAskAsideButton();
21361
+ updateResultActionButtons();
21362
+ if (rightView === "side-questions" && previousStatus === "running" && sideQuestionState.status === "idle") {
21363
+ setStatus(agentBusyFromServer
21364
+ ? "Side answer ready; the main Pi turn is still running. Main conversation unchanged."
21365
+ : "Side answer ready. Main conversation unchanged.", "success");
21366
+ }
21367
+ return;
21368
+ }
21369
+
21370
+ if (message.type === "side_question_markdown_exported") {
21371
+ if (!sideQuestionMarkdownExportRequest || sideQuestionMarkdownExportRequest.requestId !== message.requestId) return;
21372
+ sideQuestionMarkdownExportRequest = null;
21373
+ updateResultActionButtons();
21374
+ setStatus(typeof message.message === "string" ? message.message : "Saved side-question transcript.", "success");
21375
+ return;
21376
+ }
21377
+
21378
+ if (message.type === "side_question_markdown_export_conflict") {
21379
+ if (!sideQuestionMarkdownExportRequest || sideQuestionMarkdownExportRequest.requestId !== message.requestId) return;
21380
+ const pendingExport = sideQuestionMarkdownExportRequest;
21381
+ sideQuestionMarkdownExportRequest = null;
21382
+ updateResultActionButtons();
21383
+ void (async () => {
21384
+ const displayPath = typeof message.path === "string" && message.path ? message.path : pendingExport.path;
21385
+ const confirmed = await requestStudioConfirmation("Replace the existing Markdown transcript at " + displayPath + "?", {
21386
+ title: "Replace transcript file?",
21387
+ confirmLabel: "Replace",
21388
+ destructive: true,
21389
+ });
21390
+ if (!confirmed) {
21391
+ setStatus("Side-thread Markdown export cancelled.", "warning");
21392
+ return;
21393
+ }
21394
+ if (!sideQuestionState || sideQuestionState.threadId !== pendingExport.threadId) {
21395
+ setStatus("That side thread is no longer active; export cancelled.", "warning");
21396
+ return;
21397
+ }
21398
+ sendSideQuestionMarkdownExport(pendingExport.path, pendingExport.content, true);
21399
+ })();
21400
+ return;
21401
+ }
21402
+
21403
+ if (message.type === "side_question_markdown_export_error") {
21404
+ if (!sideQuestionMarkdownExportRequest || sideQuestionMarkdownExportRequest.requestId !== message.requestId) return;
21405
+ sideQuestionMarkdownExportRequest = null;
21406
+ updateResultActionButtons();
21407
+ setStatus(typeof message.message === "string" ? message.message : "Could not save side-question transcript.", "error");
21408
+ return;
21409
+ }
21410
+
21411
+ if (message.type === "side_question_error") {
21412
+ if (!sideQuestionState) sideQuestionState = normalizeSideQuestionState(null);
21413
+ sideQuestionState.status = "error";
21414
+ sideQuestionState.requestId = null;
21415
+ sideQuestionState.error = typeof message.message === "string" ? message.message : "Side question failed.";
21416
+ if (rightView === "side-questions") renderSideQuestionView();
21417
+ updateReferenceBadge();
21418
+ syncAskAsideButton();
21419
+ updateResultActionButtons();
21420
+ setStatus(sideQuestionState.error, "error");
21421
+ return;
21422
+ }
21423
+
21424
+ if (message.type === "side_question_promoted") {
21425
+ setStatus(typeof message.message === "string" ? message.message : "Side answer sent to the main conversation.", "success");
21426
+ return;
21427
+ }
21428
+
20520
21429
  if (message.type === "quarto_preview_context") {
20521
21430
  const requestId = typeof message.requestId === "string" ? message.requestId : "";
20522
21431
  const context = normalizeStudioQuartoContext(message.context);
@@ -20598,6 +21507,11 @@
20598
21507
  if (typeof message.modelLabel === "string") {
20599
21508
  modelLabel = message.modelLabel;
20600
21509
  }
21510
+ sideQuestionWebSearchAvailable = message.webSearchAvailable === true;
21511
+ if (Array.isArray(message.availablePiTools)) applySideQuestionToolCatalog(message.availablePiTools);
21512
+ if (message.sideQuestion && typeof message.sideQuestion === "object") {
21513
+ sideQuestionState = normalizeSideQuestionState(message.sideQuestion);
21514
+ }
20601
21515
  if (Array.isArray(message.suggestionModels)) {
20602
21516
  updateCompletionSuggestionModelOptions(message.suggestionModels);
20603
21517
  }
@@ -20696,6 +21610,8 @@
20696
21610
  requestStudioQuartoPreviewCheck(false);
20697
21611
  renderQuartoPreviewView();
20698
21612
  }
21613
+ if (rightView === "side-questions") renderSideQuestionView();
21614
+ syncAskAsideButton();
20699
21615
 
20700
21616
  if (pendingRequestId) {
20701
21617
  if (busy) {
@@ -21419,6 +22335,7 @@
21419
22335
  quartoPreviewCheckRequestId = null;
21420
22336
  quartoPreviewCheckSourcePath = "";
21421
22337
  quartoPreviewActionRequestId = null;
22338
+ sideQuestionMarkdownExportRequest = null;
21422
22339
  failAllPendingCompanionLaunches("The originating Studio connection was lost before the companion editor was ready.");
21423
22340
  if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
21424
22341
  setBusy(true);
@@ -21911,6 +22828,7 @@
21911
22828
  updateReviewNotesUi();
21912
22829
  }
21913
22830
  scheduleWorkspacePersistence();
22831
+ scheduleSideQuestionContextRefresh();
21914
22832
  });
21915
22833
 
21916
22834
  sourceTextEl.addEventListener("select", () => {
@@ -21924,14 +22842,17 @@
21924
22842
  }
21925
22843
  updateEditorSelectionCommentUi();
21926
22844
  syncShowMeButton();
22845
+ scheduleSideQuestionContextRefresh();
21927
22846
  });
21928
22847
 
21929
22848
  sourceTextEl.addEventListener("keyup", () => {
21930
22849
  updateEditorSelectionCommentUi();
22850
+ scheduleSideQuestionContextRefresh();
21931
22851
  });
21932
22852
 
21933
22853
  sourceTextEl.addEventListener("mouseup", () => {
21934
22854
  updateEditorSelectionCommentUi();
22855
+ scheduleSideQuestionContextRefresh();
21935
22856
  });
21936
22857
 
21937
22858
  sourceTextEl.addEventListener("focus", () => {
@@ -22045,6 +22966,13 @@
22045
22966
  });
22046
22967
  }
22047
22968
 
22969
+ if (askAsideBtn) {
22970
+ askAsideBtn.addEventListener("click", () => {
22971
+ setRightView("side-questions");
22972
+ setActivePane("right");
22973
+ });
22974
+ }
22975
+
22048
22976
  if (quizBtn) {
22049
22977
  quizBtn.addEventListener("click", () => {
22050
22978
  openQuizOverlay();