pi-studio 0.9.49 → 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");
@@ -116,6 +121,10 @@
116
121
  const sendEditorBtn = document.getElementById("sendEditorBtn");
117
122
  const openCompanionBtn = document.getElementById("openCompanionBtn");
118
123
  const getEditorBtn = document.getElementById("getEditorBtn");
124
+ const studioHeaderEl = document.getElementById("studioHeader");
125
+ const hideStudioHeaderBtn = document.getElementById("hideStudioHeaderBtn");
126
+ const studioHeaderRevealEl = document.getElementById("studioHeaderReveal");
127
+ const studioHeaderRevealBtn = document.getElementById("studioHeaderRevealBtn");
119
128
  const zenModeBtn = document.getElementById("zenModeBtn");
120
129
  const sendRunBtn = document.getElementById("sendRunBtn");
121
130
  const queueSteerBtn = document.getElementById("queueSteerBtn");
@@ -184,7 +193,7 @@
184
193
  : "full";
185
194
  const isEditorOnlyMode = studioMode === "editor-only";
186
195
  const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
187
- 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"]);
188
197
  const RIGHT_VIEW_LABELS = {
189
198
  markdown: "Response (Raw)",
190
199
  preview: "Response (Preview)",
@@ -194,6 +203,7 @@
194
203
  changes: "Changes",
195
204
  files: "Files",
196
205
  repl: "REPL",
206
+ "side-questions": "Side questions",
197
207
  };
198
208
  const RIGHT_VIEW_NUMERIC_SHORTCUTS = {
199
209
  Digit1: "markdown",
@@ -203,6 +213,7 @@
203
213
  Digit5: "changes",
204
214
  Digit6: "files",
205
215
  Digit7: "repl",
216
+ Digit8: "side-questions",
206
217
  Numpad1: "markdown",
207
218
  Numpad2: "preview",
208
219
  Numpad3: "editor-preview",
@@ -210,6 +221,7 @@
210
221
  Numpad5: "changes",
211
222
  Numpad6: "files",
212
223
  Numpad7: "repl",
224
+ Numpad8: "side-questions",
213
225
  };
214
226
 
215
227
  const navigationHelpers = globalThis.PiStudioNavigationHelpers;
@@ -238,6 +250,16 @@
238
250
  if (!showMeHelpers || typeof showMeHelpers.chooseStudioShowMeFocus !== "function") {
239
251
  throw new Error("Studio Show me helpers failed to load.");
240
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
+ }
241
263
  const studioTabStateId = navigationHelpers.ensureStudioTabStateId(window);
242
264
  const initialQueryParams = new URLSearchParams(window.location.search || "");
243
265
  const initialPaneFocusTarget = navigationHelpers.readPaneFocusTarget(window.location);
@@ -331,13 +353,15 @@
331
353
  ? "editor-preview"
332
354
  : (raw === "editor-quarto-preview"
333
355
  ? "editor-quarto-preview"
334
- : (raw === "repl"
335
- ? "repl"
336
- : (raw === "files"
356
+ : (raw === "side-questions"
357
+ ? "side-questions"
358
+ : (raw === "repl"
359
+ ? "repl"
360
+ : (raw === "files"
337
361
  ? "files"
338
362
  : (raw === "changes"
339
363
  ? "changes"
340
- : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown"))))));
364
+ : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown")))))));
341
365
  }
342
366
 
343
367
  function normalizeRightViewValue(nextView) {
@@ -376,8 +400,8 @@
376
400
  option.disabled = (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value)) || (isQuartoOption && !quartoRelevant);
377
401
  });
378
402
  rightViewSelect.title = isEditorOnlyMode
379
- ? "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."
380
- : "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.";
381
405
  }
382
406
 
383
407
  function getInitialRightView(source) {
@@ -437,12 +461,49 @@
437
461
  const RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS = 8_000;
438
462
  const EDITOR_TAB_TEXT = " ";
439
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;
440
468
  const COMPLETION_CONTEXT_STORAGE_KEY = "piStudio.completionContextMode";
441
469
  const COMPLETION_MODEL_STORAGE_KEY = "piStudio.completionModel";
442
470
  const COMPLETION_CONTEXT_MAX_CHARS = 12000;
443
471
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
444
472
  const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
445
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
+ };
446
507
  let quizOverlayEl = null;
447
508
  let quizDialogEl = null;
448
509
  let quizPreviewRenderNonce = 0;
@@ -2187,6 +2248,7 @@
2187
2248
  let lineNumbersRenderRaf = null;
2188
2249
  let annotationsEnabled = true;
2189
2250
  const STUDIO_ZEN_MODE_STORAGE_KEY = "piStudio.zenMode";
2251
+ const STUDIO_HEADER_HIDDEN_STORAGE_KEY = "piStudio.headerHidden";
2190
2252
  const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
2191
2253
  const EDITOR_FONT_SIZE_OPTIONS = [10, 11, 12, 13, 14, 15, 16, 18];
2192
2254
  const RESPONSE_FONT_SIZE_OPTIONS = [11, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 18, 20];
@@ -2239,12 +2301,17 @@
2239
2301
  let fileBrowserLoadNonce = 0;
2240
2302
  let studioUiRefreshUi = null;
2241
2303
  let studioZenModeEnabled = readStudioZenModeEnabled();
2304
+ let studioHeaderHidden = readStudioHeaderHiddenEnabled();
2305
+ let studioHeaderRevealTimer = null;
2242
2306
  if (studioUiRefreshEnabled && document.body) {
2243
2307
  document.body.classList.add("studio-ui-refresh");
2244
2308
  }
2245
2309
  if (studioZenModeEnabled && document.body) {
2246
2310
  document.body.classList.add("studio-zen-mode");
2247
2311
  }
2312
+ if ((studioZenModeEnabled || studioHeaderHidden) && document.body) {
2313
+ document.body.classList.add("studio-header-hidden");
2314
+ }
2248
2315
  let scratchpadText = "";
2249
2316
  let scratchpadReturnFocusEl = null;
2250
2317
  let scratchpadPersistTimer = null;
@@ -2308,22 +2375,129 @@
2308
2375
  }
2309
2376
  }
2310
2377
 
2311
- function syncStudioZenModeUi() {
2312
- if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
2313
- if (!zenModeBtn) return;
2314
- zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "Zen";
2315
- zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls. Shortcut: F9." : "Hide secondary Studio controls. Shortcut: F9.";
2316
- zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
2378
+ function readStudioHeaderHiddenEnabled() {
2379
+ const normalize = (value) => String(value == null ? "" : value).trim().toLowerCase();
2380
+ const isTruthy = (value) => ["1", "true", "yes", "on", "hidden"].indexOf(normalize(value)) !== -1;
2381
+ try {
2382
+ const stored = window.localStorage ? window.localStorage.getItem(STUDIO_HEADER_HIDDEN_STORAGE_KEY) : null;
2383
+ if (stored === null) return false;
2384
+ return isTruthy(stored);
2385
+ } catch {
2386
+ return false;
2387
+ }
2317
2388
  }
2318
2389
 
2319
- function setStudioZenMode(enabled) {
2320
- studioZenModeEnabled = Boolean(enabled);
2390
+ function persistStudioHeaderHiddenEnabled() {
2391
+ try {
2392
+ window.localStorage && window.localStorage.setItem(STUDIO_HEADER_HIDDEN_STORAGE_KEY, studioHeaderHidden ? "1" : "0");
2393
+ } catch {}
2394
+ }
2395
+
2396
+ function focusStudioChromeControl(control) {
2397
+ if (!control || typeof control.focus !== "function") return;
2398
+ window.setTimeout(() => {
2399
+ try {
2400
+ control.focus({ preventScroll: true });
2401
+ } catch {
2402
+ try { control.focus(); } catch {}
2403
+ }
2404
+ }, 0);
2405
+ }
2406
+
2407
+ function showStudioHeaderRevealControl() {
2408
+ if (!studioHeaderRevealEl || !studioHeaderRevealBtn) return;
2409
+ studioHeaderRevealEl.classList.add("is-open");
2410
+ if (studioHeaderRevealTimer) window.clearTimeout(studioHeaderRevealTimer);
2411
+ studioHeaderRevealTimer = window.setTimeout(() => {
2412
+ studioHeaderRevealTimer = null;
2413
+ studioHeaderRevealEl.classList.remove("is-open");
2414
+ }, 1800);
2415
+ focusStudioChromeControl(studioHeaderRevealBtn);
2416
+ }
2417
+
2418
+ function syncStudioHeaderVisibilityUi() {
2419
+ const hidden = Boolean(studioZenModeEnabled || studioHeaderHidden);
2420
+ if (document.body) document.body.classList.toggle("studio-header-hidden", hidden);
2421
+ if (studioHeaderRevealEl) {
2422
+ studioHeaderRevealEl.hidden = !hidden;
2423
+ studioHeaderRevealEl.dataset.mode = studioZenModeEnabled ? "zen" : "header";
2424
+ if (!hidden) studioHeaderRevealEl.classList.remove("is-open");
2425
+ }
2426
+ if (!hidden && studioHeaderRevealTimer) {
2427
+ window.clearTimeout(studioHeaderRevealTimer);
2428
+ studioHeaderRevealTimer = null;
2429
+ }
2430
+ if (studioHeaderRevealBtn) {
2431
+ const label = studioZenModeEnabled ? "Exit Zen" : "Show header";
2432
+ studioHeaderRevealBtn.textContent = label;
2433
+ studioHeaderRevealBtn.setAttribute("aria-label", label);
2434
+ studioHeaderRevealBtn.title = studioZenModeEnabled
2435
+ ? "Exit Zen and restore the Studio header. Shortcut: F9."
2436
+ : "Restore the Studio header.";
2437
+ }
2438
+ }
2439
+
2440
+ function syncStudioZenModeUi() {
2441
+ if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
2442
+ if (zenModeBtn) {
2443
+ zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "Zen";
2444
+ zenModeBtn.title = studioZenModeEnabled
2445
+ ? "Exit Zen and restore the Studio header. Shortcut: F9."
2446
+ : "Hide the Studio header and secondary controls. Shortcut: F9.";
2447
+ zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
2448
+ }
2449
+ syncStudioHeaderVisibilityUi();
2450
+ }
2451
+
2452
+ function setStudioHeaderHidden(enabled, options) {
2453
+ const nextHidden = Boolean(enabled);
2454
+ const moveFocusToReveal = Boolean(
2455
+ nextHidden
2456
+ && (
2457
+ (options && options.focusReveal)
2458
+ || (studioHeaderEl && document.activeElement && studioHeaderEl.contains(document.activeElement))
2459
+ )
2460
+ );
2461
+ studioHeaderHidden = nextHidden;
2462
+ persistStudioHeaderHiddenEnabled();
2463
+ closeStudioUiRefreshMenus();
2464
+ closeExportPreviewMenu();
2465
+ syncStudioHeaderVisibilityUi();
2466
+ if (moveFocusToReveal) showStudioHeaderRevealControl();
2467
+ }
2468
+
2469
+ function setStudioZenMode(enabled, options) {
2470
+ const nextEnabled = Boolean(enabled);
2471
+ const moveFocusToReveal = Boolean(
2472
+ nextEnabled
2473
+ && (
2474
+ (options && options.focusReveal)
2475
+ || (studioHeaderEl && document.activeElement && studioHeaderEl.contains(document.activeElement))
2476
+ )
2477
+ );
2478
+ studioZenModeEnabled = nextEnabled;
2479
+ if (!studioZenModeEnabled) {
2480
+ studioHeaderHidden = false;
2481
+ persistStudioHeaderHiddenEnabled();
2482
+ }
2321
2483
  try {
2322
2484
  window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, studioZenModeEnabled ? "1" : "0");
2323
2485
  } catch {}
2324
2486
  closeStudioUiRefreshMenus();
2325
2487
  closeExportPreviewMenu();
2326
2488
  syncStudioZenModeUi();
2489
+ if (moveFocusToReveal) showStudioHeaderRevealControl();
2490
+ }
2491
+
2492
+ function restoreStudioHeaderFromReveal() {
2493
+ const wasZenMode = studioZenModeEnabled;
2494
+ if (wasZenMode) {
2495
+ setStudioZenMode(false);
2496
+ } else {
2497
+ setStudioHeaderHidden(false);
2498
+ }
2499
+ focusStudioChromeControl(wasZenMode ? zenModeBtn : hideStudioHeaderBtn);
2500
+ setStatus(wasZenMode ? "Zen mode off. Studio header restored." : "Studio header shown.");
2327
2501
  }
2328
2502
 
2329
2503
  function makeStudioUiRefreshElement(tagName, className, text) {
@@ -2600,7 +2774,7 @@
2600
2774
  if (!isEditorOnlyMode && critiqueBtn && lensSelect) {
2601
2775
  const reviewButton = makeStudioUiRefreshElement("button", "studio-refresh-tool-tab studio-refresh-review-btn", "Review");
2602
2776
  reviewMenu = makeStudioUiRefreshMenu(reviewButton, "review", "studio-refresh-review-anchor");
2603
- appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, showMeBtn, showMeResponseBtn, quizBtn]);
2777
+ appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, showMeBtn, showMeResponseBtn, askAsideBtn, quizBtn]);
2604
2778
  appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
2605
2779
  }
2606
2780
 
@@ -2724,6 +2898,7 @@
2724
2898
  if (reviewNotesBtn) headerToolsEl.appendChild(reviewNotesBtn);
2725
2899
  if (outlineBtn) headerToolsEl.appendChild(outlineBtn);
2726
2900
  if (scratchpadBtn) headerToolsEl.appendChild(scratchpadBtn);
2901
+ if (isEditorOnlyMode && askAsideBtn) headerToolsEl.appendChild(askAsideBtn);
2727
2902
  if (reviewMenu) headerToolsEl.appendChild(reviewMenu.anchor);
2728
2903
  headerTopEl.appendChild(headerToolsEl);
2729
2904
 
@@ -2924,9 +3099,9 @@
2924
3099
 
2925
3100
  function getIdleStatus() {
2926
3101
  if (isEditorOnlyMode) {
2927
- 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.";
2928
3103
  }
2929
- 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.";
2930
3105
  }
2931
3106
 
2932
3107
  function normalizeTerminalPhase(phase) {
@@ -4644,6 +4819,16 @@
4644
4819
  return;
4645
4820
  }
4646
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
+
4647
4832
  const isContentFocusShortcut = key === "F8" && !event.metaKey && !event.ctrlKey && !event.altKey;
4648
4833
  if (isContentFocusShortcut) {
4649
4834
  event.preventDefault();
@@ -4658,8 +4843,17 @@
4658
4843
  const isZenModeShortcut = key === "F9" && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
4659
4844
  if (isZenModeShortcut) {
4660
4845
  event.preventDefault();
4661
- setStudioZenMode(!studioZenModeEnabled);
4662
- setStatus(studioZenModeEnabled ? "Zen mode on." : "Zen mode off.");
4846
+ const restoringHeaderOnly = Boolean(!studioZenModeEnabled && studioHeaderHidden);
4847
+ if (studioZenModeEnabled) {
4848
+ setStudioZenMode(false);
4849
+ } else if (studioHeaderHidden) {
4850
+ setStudioHeaderHidden(false);
4851
+ } else {
4852
+ setStudioZenMode(true);
4853
+ }
4854
+ setStatus(studioZenModeEnabled
4855
+ ? "Zen mode on. Studio header hidden; press F9 to exit."
4856
+ : (restoringHeaderOnly ? "Studio header restored." : "Zen mode off. Studio header restored."));
4663
4857
  return;
4664
4858
  }
4665
4859
 
@@ -4893,6 +5087,19 @@
4893
5087
  }
4894
5088
  }
4895
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
+
4896
5103
  function syncTraceForSelectedHistoryItem() {
4897
5104
  const item = getSelectedHistoryItem();
4898
5105
  const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
@@ -5096,6 +5303,18 @@
5096
5303
  return;
5097
5304
  }
5098
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
+
5099
5318
  if (rightView === "trace") {
5100
5319
  const state = traceState || createEmptyTraceState();
5101
5320
  const context = traceDisplayContext || {};
@@ -9009,6 +9228,10 @@
9009
9228
  critiqueViewEl.addEventListener("click", handleFilesPaneClick);
9010
9229
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
9011
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);
9012
9235
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
9013
9236
  critiqueViewEl.addEventListener("change", (event) => {
9014
9237
  void handleFilesPaneChange(event);
@@ -9034,7 +9257,7 @@
9034
9257
 
9035
9258
  function applyPendingResponseScrollReset() {
9036
9259
  if (!pendingResponseScrollReset || !critiqueViewEl) return false;
9037
- if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return false;
9260
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview" || rightView === "side-questions") return false;
9038
9261
 
9039
9262
  pendingResponseScrollReset = false;
9040
9263
  let targetEl = replaceResponsePaneWithClone();
@@ -9043,7 +9266,7 @@
9043
9266
  : (cb) => window.setTimeout(cb, 16);
9044
9267
  const resetScroll = () => {
9045
9268
  if (!targetEl || !targetEl.isConnected) return;
9046
- if (rightView === "editor-preview" || rightView === "editor-quarto-preview") return;
9269
+ if (rightView === "editor-preview" || rightView === "editor-quarto-preview" || rightView === "side-questions") return;
9047
9270
  targetEl.scrollTop = 0;
9048
9271
  targetEl.scrollLeft = 0;
9049
9272
  };
@@ -9301,8 +9524,10 @@
9301
9524
  async function exportRightPanePdf(options) {
9302
9525
  const exportOptions = options && typeof options === "object" ? options : {};
9303
9526
  const openTarget = exportOptions.openTarget === "studio" ? "studio" : "default";
9527
+ const exportingSideThread = rightView === "side-questions";
9528
+ const sideThreadExportedAt = exportingSideThread ? new Date() : null;
9304
9529
  let studioLaunch = null;
9305
- if (uiBusy || previewExportInProgress) {
9530
+ if ((!exportingSideThread && uiBusy) || previewExportInProgress || sideQuestionMarkdownExportRequest) {
9306
9531
  setStatus("Studio is busy.", "warning");
9307
9532
  return;
9308
9533
  }
@@ -9315,8 +9540,8 @@
9315
9540
 
9316
9541
  const exportingReplJournal = rightView === "repl";
9317
9542
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
9318
- if (!rightPaneShowsPreview && !exportingReplJournal) {
9319
- 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");
9320
9545
  return;
9321
9546
  }
9322
9547
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
@@ -9325,7 +9550,7 @@
9325
9550
  return;
9326
9551
  }
9327
9552
 
9328
- const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
9553
+ const htmlArtifactSource = exportingReplJournal || exportingSideThread ? "" : getRightPaneHtmlArtifactSource();
9329
9554
  if (htmlArtifactSource) {
9330
9555
  setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
9331
9556
  return;
@@ -9333,24 +9558,34 @@
9333
9558
 
9334
9559
  const markdown = exportingReplJournal
9335
9560
  ? buildReplJournalMarkdown(replJournalExportEntries)
9336
- : (rightView === "editor-preview"
9337
- ? prepareEditorTextForPdfExport(sourceTextEl.value)
9338
- : prepareEditorTextForPreview(latestResponseMarkdown));
9561
+ : (exportingSideThread
9562
+ ? buildCurrentSideQuestionTranscriptMarkdown(sideThreadExportedAt)
9563
+ : (rightView === "editor-preview"
9564
+ ? prepareEditorTextForPdfExport(sourceTextEl.value)
9565
+ : prepareEditorTextForPreview(latestResponseMarkdown)));
9339
9566
  if (!markdown || !markdown.trim()) {
9340
9567
  setStatus("Nothing to export yet.", "warning");
9341
9568
  return;
9342
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
+ }
9343
9574
 
9344
9575
  const effectivePath = getEffectiveSavePath();
9345
- const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
9576
+ const sourcePath = exportingReplJournal || exportingSideThread ? "" : (effectivePath || sourceState.path || "");
9346
9577
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
9347
9578
  const isEditorPreview = rightView === "editor-preview";
9348
9579
  const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
9349
9580
  const editorPdfLanguage = isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "";
9350
- const isLatex = isEditorPreview
9351
- ? editorPdfLanguage === "latex"
9352
- : /\\documentclass\b|\\begin\{document\}/.test(markdown);
9353
- 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")));
9354
9589
  if (sourcePath) {
9355
9590
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
9356
9591
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -9367,7 +9602,9 @@
9367
9602
  }
9368
9603
  previewExportInProgress = true;
9369
9604
  updateResultActionButtons();
9370
- 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");
9371
9608
 
9372
9609
  try {
9373
9610
  const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
@@ -9532,8 +9769,10 @@
9532
9769
  async function exportRightPaneHtml(options) {
9533
9770
  const exportOptions = options && typeof options === "object" ? options : {};
9534
9771
  const openTarget = exportOptions.openTarget === "studio" ? "studio" : "browser";
9772
+ const exportingSideThread = rightView === "side-questions";
9773
+ const sideThreadExportedAt = exportingSideThread ? new Date() : null;
9535
9774
  let studioLaunch = null;
9536
- if (uiBusy || previewExportInProgress) {
9775
+ if ((!exportingSideThread && uiBusy) || previewExportInProgress || sideQuestionMarkdownExportRequest) {
9537
9776
  setStatus("Studio is busy.", "warning");
9538
9777
  return;
9539
9778
  }
@@ -9546,8 +9785,8 @@
9546
9785
 
9547
9786
  const exportingReplJournal = rightView === "repl";
9548
9787
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
9549
- if (!rightPaneShowsPreview && !exportingReplJournal) {
9550
- 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");
9551
9790
  return;
9552
9791
  }
9553
9792
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
@@ -9556,26 +9795,36 @@
9556
9795
  return;
9557
9796
  }
9558
9797
 
9559
- const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
9560
- const markdown = exportingReplJournal ? buildReplJournalMarkdown(replJournalExportEntries) : (htmlArtifactSource || (rightView === "editor-preview"
9561
- ? prepareEditorTextForHtmlExport(sourceTextEl.value)
9562
- : 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))));
9563
9806
  if (!markdown || !markdown.trim()) {
9564
9807
  setStatus("Nothing to export yet.", "warning");
9565
9808
  return;
9566
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
+ }
9567
9814
 
9568
9815
  const effectivePath = getEffectiveSavePath();
9569
- const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
9816
+ const sourcePath = exportingReplJournal || exportingSideThread ? "" : (effectivePath || sourceState.path || "");
9570
9817
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
9571
9818
  const isEditorPreview = rightView === "editor-preview";
9572
9819
  const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
9573
9820
  const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "");
9574
- const isLatex = htmlArtifactSource ? false : (isEditorPreview
9821
+ const isLatex = exportingSideThread ? false : (htmlArtifactSource ? false : (isEditorPreview
9575
9822
  ? editorHtmlLanguage === "latex"
9576
- : /\\documentclass\b|\\begin\{document\}/.test(markdown));
9577
- let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html"));
9578
- 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"));
9579
9828
  if (sourcePath) {
9580
9829
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
9581
9830
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -9593,7 +9842,9 @@
9593
9842
  }
9594
9843
  previewExportInProgress = true;
9595
9844
  updateResultActionButtons();
9596
- 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");
9597
9848
 
9598
9849
  try {
9599
9850
  const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
@@ -9768,6 +10019,15 @@
9768
10019
 
9769
10020
  function exportRightPaneFormat(format) {
9770
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
+ }
9771
10031
  if (format === "html-studio") {
9772
10032
  return exportRightPaneHtml({ openTarget: "studio" });
9773
10033
  }
@@ -11647,10 +11907,650 @@
11647
11907
  }
11648
11908
  }
11649
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
+
11650
12545
  function renderActiveResult() {
11651
12546
  if (critiqueViewEl) {
11652
12547
  critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
11653
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;
11654
12554
  }
11655
12555
  if (rightView === "editor-quarto-preview") {
11656
12556
  renderQuartoPreviewView();
@@ -11754,7 +12654,7 @@
11754
12654
  : normalizeForCompare(sourceTextEl.value);
11755
12655
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
11756
12656
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
11757
- 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";
11758
12658
 
11759
12659
  if (responseWrapEl) {
11760
12660
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -11792,28 +12692,61 @@
11792
12692
 
11793
12693
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
11794
12694
  const exportingReplJournal = rightView === "repl";
12695
+ const exportingSideThread = rightView === "side-questions";
11795
12696
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
12697
+ const sideThreadExportText = exportingSideThread && canExportSideQuestionTranscript()
12698
+ ? buildCurrentSideQuestionTranscriptMarkdown(new Date())
12699
+ : "";
11796
12700
  const exportText = exportingReplJournal
11797
12701
  ? (replJournalExportEntries.length ? buildReplJournalMarkdown(replJournalExportEntries) : "")
11798
- : (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown);
11799
- const canExportPreview = (rightPaneShowsPreview || exportingReplJournal) && Boolean(String(exportText || "").trim());
11800
- 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() : "";
11801
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
+ }
11802
12727
  if (exportPdfBtn) {
11803
- exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
12728
+ exportPdfBtn.disabled = exportBusy || !canExportPreview;
11804
12729
  exportPdfBtn.textContent = previewExportInProgress
11805
12730
  ? "Exporting…"
11806
- : (exportingReplJournal ? "Export record" : "Export right preview");
12731
+ : (sideQuestionMarkdownExportRequest
12732
+ ? "Saving…"
12733
+ : (exportingSideThread ? "Export thread" : (exportingReplJournal ? "Export record" : "Export right preview")));
11807
12734
  if (rightView === "trace") {
11808
12735
  exportPdfBtn.title = "Working view does not support preview export.";
11809
12736
  } else if (rightView === "files") {
11810
12737
  exportPdfBtn.title = "Files view does not support preview export.";
11811
12738
  } else if (rightView === "changes") {
11812
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.";
11813
12746
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
11814
12747
  exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
11815
12748
  } else if (rightView === "markdown") {
11816
- 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.";
11817
12750
  } else if (!canExportPreview) {
11818
12751
  exportPdfBtn.title = "Nothing to export yet.";
11819
12752
  } else if (isHtmlArtifactPreview) {
@@ -11824,39 +12757,52 @@
11824
12757
  exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
11825
12758
  }
11826
12759
  }
12760
+ if (exportSideThreadRenderSeparatorEl) exportSideThreadRenderSeparatorEl.hidden = !exportingSideThread;
11827
12761
  if (exportPreviewPdfStudioBtn) {
11828
- exportPreviewPdfStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
11829
- exportPreviewPdfStudioBtn.title = isHtmlArtifactPreview
11830
- ? "Interactive HTML preview PDF export is not available yet."
11831
- : (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.")));
11832
12768
  }
11833
12769
  if (exportPreviewPdfBtn) {
11834
- exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
11835
- exportPreviewPdfBtn.title = isHtmlArtifactPreview
11836
- ? "Interactive HTML preview PDF export is not available yet."
11837
- : (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.")));
11838
12776
  }
11839
12777
  if (exportPreviewHtmlStudioBtn) {
11840
- exportPreviewHtmlStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
11841
- exportPreviewHtmlStudioBtn.title = isHtmlArtifactPreview
11842
- ? "Export the authored HTML preview and open it in a new Studio editor tab."
11843
- : (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.")));
11844
12784
  }
11845
12785
  if (exportPreviewHtmlBtn) {
11846
- exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
11847
- exportPreviewHtmlBtn.title = isHtmlArtifactPreview
11848
- ? "Export the authored HTML preview and open it in the default browser."
11849
- : (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.")));
11850
12792
  }
11851
12793
  if (exportPreviewControlsEl) {
11852
12794
  exportPreviewControlsEl.hidden = rightView === "editor-quarto-preview";
11853
12795
  exportPreviewControlsEl.title = canExportPreview
11854
- ? (exportingReplJournal
11855
- ? "Choose a format and export destination for the Studio REPL record."
11856
- : (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview."))
11857
- : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
11858
- }
11859
- 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) {
11860
12806
  closeExportPreviewMenu();
11861
12807
  }
11862
12808
 
@@ -11866,6 +12812,7 @@
11866
12812
  updateSyncBadge(normalizedEditor);
11867
12813
  syncStudioQuartoDirtyUi();
11868
12814
  syncShowMeButton();
12815
+ syncAskAsideButton();
11869
12816
  }
11870
12817
 
11871
12818
  function refreshResponseUi() {
@@ -13034,9 +13981,19 @@
13034
13981
  if (rightView === "editor-quarto-preview") {
13035
13982
  requestStudioQuartoPreviewCheck(false);
13036
13983
  }
13984
+ if (rightView === "side-questions" && previousView !== "side-questions") {
13985
+ if (!sideQuestionUi.gatherScope) sideQuestionUi.gatherScope = getSideQuestionGatherScope();
13986
+ sendMessage({ type: "side_question_get_state" });
13987
+ }
13037
13988
 
13038
13989
  refreshResponseUi();
13039
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
+ }
13040
13997
  scheduleWorkspacePersistence();
13041
13998
  }
13042
13999
 
@@ -20157,6 +21114,7 @@
20157
21114
  quizBtn.disabled = true;
20158
21115
  quizBtn.title = "Quiz is unavailable in editor-only mode.";
20159
21116
  }
21117
+ syncAskAsideButton();
20160
21118
  syncStudioUiRefreshReviewTrigger();
20161
21119
  return;
20162
21120
  }
@@ -20231,6 +21189,7 @@
20231
21189
  : "Open an active quiz for the current editor selection or document.");
20232
21190
  }
20233
21191
  syncShowMeButton();
21192
+ syncAskAsideButton();
20234
21193
  syncStudioUiRefreshReviewTrigger();
20235
21194
  }
20236
21195
 
@@ -20391,6 +21350,82 @@
20391
21350
  return;
20392
21351
  }
20393
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
+
20394
21429
  if (message.type === "quarto_preview_context") {
20395
21430
  const requestId = typeof message.requestId === "string" ? message.requestId : "";
20396
21431
  const context = normalizeStudioQuartoContext(message.context);
@@ -20472,6 +21507,11 @@
20472
21507
  if (typeof message.modelLabel === "string") {
20473
21508
  modelLabel = message.modelLabel;
20474
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
+ }
20475
21515
  if (Array.isArray(message.suggestionModels)) {
20476
21516
  updateCompletionSuggestionModelOptions(message.suggestionModels);
20477
21517
  }
@@ -20570,6 +21610,8 @@
20570
21610
  requestStudioQuartoPreviewCheck(false);
20571
21611
  renderQuartoPreviewView();
20572
21612
  }
21613
+ if (rightView === "side-questions") renderSideQuestionView();
21614
+ syncAskAsideButton();
20573
21615
 
20574
21616
  if (pendingRequestId) {
20575
21617
  if (busy) {
@@ -21293,6 +22335,7 @@
21293
22335
  quartoPreviewCheckRequestId = null;
21294
22336
  quartoPreviewCheckSourcePath = "";
21295
22337
  quartoPreviewActionRequestId = null;
22338
+ sideQuestionMarkdownExportRequest = null;
21296
22339
  failAllPendingCompanionLaunches("The originating Studio connection was lost before the companion editor was ready.");
21297
22340
  if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
21298
22341
  setBusy(true);
@@ -21785,6 +22828,7 @@
21785
22828
  updateReviewNotesUi();
21786
22829
  }
21787
22830
  scheduleWorkspacePersistence();
22831
+ scheduleSideQuestionContextRefresh();
21788
22832
  });
21789
22833
 
21790
22834
  sourceTextEl.addEventListener("select", () => {
@@ -21798,14 +22842,17 @@
21798
22842
  }
21799
22843
  updateEditorSelectionCommentUi();
21800
22844
  syncShowMeButton();
22845
+ scheduleSideQuestionContextRefresh();
21801
22846
  });
21802
22847
 
21803
22848
  sourceTextEl.addEventListener("keyup", () => {
21804
22849
  updateEditorSelectionCommentUi();
22850
+ scheduleSideQuestionContextRefresh();
21805
22851
  });
21806
22852
 
21807
22853
  sourceTextEl.addEventListener("mouseup", () => {
21808
22854
  updateEditorSelectionCommentUi();
22855
+ scheduleSideQuestionContextRefresh();
21809
22856
  });
21810
22857
 
21811
22858
  sourceTextEl.addEventListener("focus", () => {
@@ -21919,6 +22966,13 @@
21919
22966
  });
21920
22967
  }
21921
22968
 
22969
+ if (askAsideBtn) {
22970
+ askAsideBtn.addEventListener("click", () => {
22971
+ setRightView("side-questions");
22972
+ setActivePane("right");
22973
+ });
22974
+ }
22975
+
21922
22976
  if (quizBtn) {
21923
22977
  quizBtn.addEventListener("click", () => {
21924
22978
  openQuizOverlay();
@@ -22254,9 +23308,29 @@
22254
23308
  });
22255
23309
  }
22256
23310
 
23311
+ if (hideStudioHeaderBtn) {
23312
+ hideStudioHeaderBtn.addEventListener("click", () => {
23313
+ setStudioHeaderHidden(true, { focusReveal: true });
23314
+ setStatus("Studio header hidden. Move to the top-right edge to restore it.");
23315
+ });
23316
+ }
23317
+
23318
+ if (studioHeaderRevealEl) {
23319
+ studioHeaderRevealEl.addEventListener("click", (event) => {
23320
+ if (event.target === studioHeaderRevealEl) restoreStudioHeaderFromReveal();
23321
+ });
23322
+ }
23323
+
23324
+ if (studioHeaderRevealBtn) {
23325
+ studioHeaderRevealBtn.addEventListener("click", restoreStudioHeaderFromReveal);
23326
+ }
23327
+
22257
23328
  if (zenModeBtn) {
22258
23329
  zenModeBtn.addEventListener("click", () => {
22259
- setStudioZenMode(!studioZenModeEnabled);
23330
+ setStudioZenMode(!studioZenModeEnabled, { focusReveal: !studioZenModeEnabled });
23331
+ setStatus(studioZenModeEnabled
23332
+ ? "Zen mode on. Studio header hidden; press F9 to exit."
23333
+ : "Zen mode off. Studio header restored.");
22260
23334
  });
22261
23335
  }
22262
23336