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