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