pi-studio 0.9.52 → 0.9.54
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 +15 -9
- package/ROADMAP.md +15 -2
- package/client/studio-client.js +1387 -245
- package/client/studio-preview-resource-helpers.js +61 -12
- package/client/studio.css +180 -3
- package/index.ts +991 -166
- package/package.json +1 -1
- package/shared/studio-disk-revisions.js +558 -0
- package/shared/studio-file-watcher.js +181 -0
- package/shared/studio-resource-grants.js +157 -0
- package/shared/studio-side-question-context.js +17 -4
- package/shared/studio-workspace-state.js +4 -0
package/client/studio-client.js
CHANGED
|
@@ -155,6 +155,7 @@
|
|
|
155
155
|
const shortcutsCloseBtn = document.getElementById("shortcutsCloseBtn");
|
|
156
156
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
157
157
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
158
|
+
const watchedOpenEditableBtn = document.getElementById("watchedOpenEditableBtn");
|
|
158
159
|
const reviewNotesBtn = document.getElementById("reviewNotesBtn");
|
|
159
160
|
const outlineBtn = document.getElementById("outlineBtn");
|
|
160
161
|
const scratchpadBtn = document.getElementById("scratchpadBtn");
|
|
@@ -192,6 +193,7 @@
|
|
|
192
193
|
? "editor-only"
|
|
193
194
|
: "full";
|
|
194
195
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
196
|
+
const isWatchedFilePreview = Boolean(document.body && document.body.dataset && document.body.dataset.watchedFilePreview === "1");
|
|
195
197
|
const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
|
|
196
198
|
const EDITOR_ONLY_RIGHT_VIEW_ALLOWED = new Set(["editor-preview", "editor-quarto-preview", "files", "changes", "repl", "side-questions"]);
|
|
197
199
|
const RIGHT_VIEW_LABELS = {
|
|
@@ -242,6 +244,7 @@
|
|
|
242
244
|
|| typeof previewResourceHelpers.buildStudioPdfVersionSignature !== "function"
|
|
243
245
|
|| typeof previewResourceHelpers.createStudioPdfVersionObservationState !== "function"
|
|
244
246
|
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalImages !== "function"
|
|
247
|
+
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalPdfEmbeds !== "function"
|
|
245
248
|
|| typeof previewResourceHelpers.observeStudioPdfVersion !== "function"
|
|
246
249
|
) {
|
|
247
250
|
throw new Error("Studio preview resource helpers failed to load.");
|
|
@@ -281,6 +284,16 @@
|
|
|
281
284
|
};
|
|
282
285
|
const initialResourceDir = initialQueryParams.get("resourceDir")
|
|
283
286
|
|| ((document.body && document.body.dataset && document.body.dataset.initialResourceDir) || "");
|
|
287
|
+
const initialDiskRevision = (document.body && document.body.dataset && document.body.dataset.initialDiskRevision) || "";
|
|
288
|
+
let watchedFilePreviewState = {
|
|
289
|
+
enabled: isWatchedFilePreview,
|
|
290
|
+
path: isWatchedFilePreview && initialSourceState.path ? initialSourceState.path : "",
|
|
291
|
+
diskRevision: isWatchedFilePreview ? initialDiskRevision : "",
|
|
292
|
+
generation: 0,
|
|
293
|
+
lastError: "",
|
|
294
|
+
renderError: "",
|
|
295
|
+
};
|
|
296
|
+
const watchedFilePreviewReadingPositions = { source: null, response: null };
|
|
284
297
|
|
|
285
298
|
let ws = null;
|
|
286
299
|
let wsState = "Connecting";
|
|
@@ -332,6 +345,7 @@
|
|
|
332
345
|
let studioDecisionMessageEl = null;
|
|
333
346
|
let studioDecisionInputEl = null;
|
|
334
347
|
let studioDecisionCancelBtn = null;
|
|
348
|
+
let studioDecisionTertiaryBtn = null;
|
|
335
349
|
let studioDecisionSecondaryBtn = null;
|
|
336
350
|
let studioDecisionConfirmBtn = null;
|
|
337
351
|
let studioDecisionState = null;
|
|
@@ -339,6 +353,7 @@
|
|
|
339
353
|
let pendingRequestId = null;
|
|
340
354
|
let pendingKind = null;
|
|
341
355
|
let stickyStudioKind = null;
|
|
356
|
+
const pendingSaveOperations = new Map();
|
|
342
357
|
const pendingCompanionLaunches = new Map();
|
|
343
358
|
const activeStudioTabLaunches = new Set();
|
|
344
359
|
let sourceOriginSummaryEl = null;
|
|
@@ -368,6 +383,7 @@
|
|
|
368
383
|
|
|
369
384
|
function normalizeRightViewValue(nextView) {
|
|
370
385
|
const normalized = canonicalRightViewValue(nextView);
|
|
386
|
+
if (isWatchedFilePreview && normalized !== "editor-preview") return "editor-preview";
|
|
371
387
|
if (normalized === "editor-quarto-preview" && !isCurrentStudioQuartoDocument()) {
|
|
372
388
|
return "editor-preview";
|
|
373
389
|
}
|
|
@@ -379,6 +395,7 @@
|
|
|
379
395
|
|
|
380
396
|
function isRightViewAvailableInCurrentMode(view) {
|
|
381
397
|
const normalized = canonicalRightViewValue(view);
|
|
398
|
+
if (isWatchedFilePreview) return normalized === "editor-preview";
|
|
382
399
|
if (normalized === "editor-quarto-preview" && !isCurrentStudioQuartoDocument()) return false;
|
|
383
400
|
return !isEditorOnlyMode || EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(normalized);
|
|
384
401
|
}
|
|
@@ -398,12 +415,17 @@
|
|
|
398
415
|
Array.from(rightViewSelect.options).forEach((option) => {
|
|
399
416
|
if (!option) return;
|
|
400
417
|
const isQuartoOption = option.value === "editor-quarto-preview";
|
|
418
|
+
if (isWatchedFilePreview && option.value === "editor-preview") option.textContent = "Watched preview";
|
|
401
419
|
if (isQuartoOption) option.hidden = !quartoRelevant;
|
|
402
|
-
option.disabled = (
|
|
420
|
+
option.disabled = (isWatchedFilePreview && option.value !== "editor-preview")
|
|
421
|
+
|| (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value))
|
|
422
|
+
|| (isQuartoOption && !quartoRelevant);
|
|
403
423
|
});
|
|
404
|
-
rightViewSelect.title =
|
|
405
|
-
? "
|
|
406
|
-
:
|
|
424
|
+
rightViewSelect.title = isWatchedFilePreview
|
|
425
|
+
? "Read-only watched preview follows this file on disk."
|
|
426
|
+
: (isEditorOnlyMode
|
|
427
|
+
? "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+F/Q open Files/Side questions."
|
|
428
|
+
: "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/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.");
|
|
407
429
|
}
|
|
408
430
|
|
|
409
431
|
function getInitialRightView(source) {
|
|
@@ -478,6 +500,7 @@
|
|
|
478
500
|
let sideQuestionAvailablePiTools = [];
|
|
479
501
|
let sideQuestionPreviewRenderNonce = 0;
|
|
480
502
|
let sideQuestionContextRefreshHandle = null;
|
|
503
|
+
let sideQuestionContextGrantPending = false;
|
|
481
504
|
let sideQuestionMarkdownExportRequest = null;
|
|
482
505
|
const sideQuestionMarkdownRenderCache = new Map();
|
|
483
506
|
let sideQuestionUi = {
|
|
@@ -2162,6 +2185,7 @@
|
|
|
2162
2185
|
actionRequestId: null,
|
|
2163
2186
|
};
|
|
2164
2187
|
let fileBackedBaselineText = null;
|
|
2188
|
+
let fileBackedDiskRevision = null;
|
|
2165
2189
|
let activePane = initialPaneFocusTarget === "right" ? "right" : "left";
|
|
2166
2190
|
let paneFocusTarget = initialPaneFocusTarget;
|
|
2167
2191
|
let paneSplitPercent = 50;
|
|
@@ -2293,6 +2317,9 @@
|
|
|
2293
2317
|
relativeDir: "",
|
|
2294
2318
|
parentDir: null,
|
|
2295
2319
|
entries: [],
|
|
2320
|
+
exactFiles: [],
|
|
2321
|
+
locations: [],
|
|
2322
|
+
grantRequiredPath: "",
|
|
2296
2323
|
omitted: 0,
|
|
2297
2324
|
omittedIgnored: 0,
|
|
2298
2325
|
sort: fileBrowserSortMode,
|
|
@@ -2918,6 +2945,7 @@
|
|
|
2918
2945
|
rightTitleGroupEl.appendChild(rightViewSelect);
|
|
2919
2946
|
rightIdentityEl.appendChild(rightTitleGroupEl);
|
|
2920
2947
|
const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
|
|
2948
|
+
if (watchedOpenEditableBtn && isWatchedFilePreview) rightToolsEl.appendChild(watchedOpenEditableBtn);
|
|
2921
2949
|
if (exportPreviewControlsEl) {
|
|
2922
2950
|
rightToolsEl.appendChild(exportPreviewControlsEl);
|
|
2923
2951
|
} else if (exportPdfBtn) {
|
|
@@ -3841,7 +3869,7 @@
|
|
|
3841
3869
|
}
|
|
3842
3870
|
|
|
3843
3871
|
function getStudioDecisionFocusableElements() {
|
|
3844
|
-
return [studioDecisionInputEl, studioDecisionCancelBtn, studioDecisionSecondaryBtn, studioDecisionConfirmBtn]
|
|
3872
|
+
return [studioDecisionInputEl, studioDecisionCancelBtn, studioDecisionTertiaryBtn, studioDecisionSecondaryBtn, studioDecisionConfirmBtn]
|
|
3845
3873
|
.filter((element) => element && !element.hidden && !element.disabled);
|
|
3846
3874
|
}
|
|
3847
3875
|
|
|
@@ -3889,13 +3917,41 @@
|
|
|
3889
3917
|
cancelBtn.addEventListener("click", () => finishStudioDecision(null));
|
|
3890
3918
|
actions.appendChild(cancelBtn);
|
|
3891
3919
|
|
|
3920
|
+
const tertiaryBtn = document.createElement("button");
|
|
3921
|
+
tertiaryBtn.type = "button";
|
|
3922
|
+
tertiaryBtn.className = "studio-decision-tertiary";
|
|
3923
|
+
tertiaryBtn.hidden = true;
|
|
3924
|
+
tertiaryBtn.addEventListener("click", () => {
|
|
3925
|
+
const state = studioDecisionState;
|
|
3926
|
+
const handler = state && state.onTertiary;
|
|
3927
|
+
if (typeof handler !== "function") {
|
|
3928
|
+
if (state && state.hasTertiaryValue) finishStudioDecision(state.tertiaryValue);
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
try {
|
|
3932
|
+
const result = handler();
|
|
3933
|
+
if (result && typeof result.catch === "function") {
|
|
3934
|
+
result.catch((error) => {
|
|
3935
|
+
setStatus("Action failed: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
} catch (error) {
|
|
3939
|
+
setStatus("Action failed: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
3942
|
+
actions.appendChild(tertiaryBtn);
|
|
3943
|
+
|
|
3892
3944
|
const secondaryBtn = document.createElement("button");
|
|
3893
3945
|
secondaryBtn.type = "button";
|
|
3894
3946
|
secondaryBtn.className = "studio-decision-secondary";
|
|
3895
3947
|
secondaryBtn.hidden = true;
|
|
3896
3948
|
secondaryBtn.addEventListener("click", () => {
|
|
3897
|
-
const
|
|
3898
|
-
|
|
3949
|
+
const state = studioDecisionState;
|
|
3950
|
+
const handler = state && state.onSecondary;
|
|
3951
|
+
if (typeof handler !== "function") {
|
|
3952
|
+
if (state && state.hasSecondaryValue) finishStudioDecision(state.secondaryValue);
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3899
3955
|
try {
|
|
3900
3956
|
const result = handler();
|
|
3901
3957
|
if (result && typeof result.catch === "function") {
|
|
@@ -3955,6 +4011,7 @@
|
|
|
3955
4011
|
studioDecisionMessageEl = message;
|
|
3956
4012
|
studioDecisionInputEl = input;
|
|
3957
4013
|
studioDecisionCancelBtn = cancelBtn;
|
|
4014
|
+
studioDecisionTertiaryBtn = tertiaryBtn;
|
|
3958
4015
|
studioDecisionSecondaryBtn = secondaryBtn;
|
|
3959
4016
|
studioDecisionConfirmBtn = confirmBtn;
|
|
3960
4017
|
return overlay;
|
|
@@ -3967,6 +4024,7 @@
|
|
|
3967
4024
|
if (studioDecisionState) finishStudioDecision(null, false);
|
|
3968
4025
|
const returnFocusEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
3969
4026
|
|
|
4027
|
+
const tertiaryLabel = String(settings.tertiaryLabel || "").trim();
|
|
3970
4028
|
const secondaryLabel = String(settings.secondaryLabel || "").trim();
|
|
3971
4029
|
studioDecisionTitleEl.textContent = String(settings.title || (mode === "prompt" ? "Enter a value" : "Confirm action"));
|
|
3972
4030
|
studioDecisionMessageEl.textContent = String(settings.message || "");
|
|
@@ -3975,9 +4033,13 @@
|
|
|
3975
4033
|
studioDecisionInputEl.placeholder = mode === "prompt" ? String(settings.placeholder || "") : "";
|
|
3976
4034
|
studioDecisionInputEl.setAttribute("aria-label", String(settings.inputLabel || "Value"));
|
|
3977
4035
|
studioDecisionCancelBtn.textContent = String(settings.cancelLabel || "Cancel");
|
|
4036
|
+
studioDecisionTertiaryBtn.hidden = !tertiaryLabel;
|
|
4037
|
+
studioDecisionTertiaryBtn.disabled = settings.tertiaryDisabled === true;
|
|
4038
|
+
studioDecisionTertiaryBtn.textContent = tertiaryLabel;
|
|
3978
4039
|
studioDecisionSecondaryBtn.hidden = !secondaryLabel;
|
|
3979
4040
|
studioDecisionSecondaryBtn.disabled = settings.secondaryDisabled === true;
|
|
3980
4041
|
studioDecisionSecondaryBtn.textContent = secondaryLabel;
|
|
4042
|
+
studioDecisionConfirmBtn.disabled = settings.confirmDisabled === true;
|
|
3981
4043
|
studioDecisionConfirmBtn.textContent = String(settings.confirmLabel || (mode === "prompt" ? "Continue" : "Confirm"));
|
|
3982
4044
|
studioDecisionConfirmBtn.classList.toggle("is-destructive", settings.destructive === true);
|
|
3983
4045
|
studioDecisionDialogEl.classList.toggle("is-destructive", settings.destructive === true);
|
|
@@ -3989,7 +4051,12 @@
|
|
|
3989
4051
|
mode,
|
|
3990
4052
|
resolve,
|
|
3991
4053
|
returnFocusEl,
|
|
4054
|
+
onTertiary: typeof settings.onTertiary === "function" ? settings.onTertiary : null,
|
|
4055
|
+
hasTertiaryValue: Object.prototype.hasOwnProperty.call(settings, "tertiaryValue"),
|
|
4056
|
+
tertiaryValue: settings.tertiaryValue,
|
|
3992
4057
|
onSecondary: typeof settings.onSecondary === "function" ? settings.onSecondary : null,
|
|
4058
|
+
hasSecondaryValue: Object.prototype.hasOwnProperty.call(settings, "secondaryValue"),
|
|
4059
|
+
secondaryValue: settings.secondaryValue,
|
|
3993
4060
|
};
|
|
3994
4061
|
studioDecisionState = decisionState;
|
|
3995
4062
|
const schedule = typeof window.requestAnimationFrame === "function"
|
|
@@ -4048,12 +4115,20 @@
|
|
|
4048
4115
|
}
|
|
4049
4116
|
});
|
|
4050
4117
|
|
|
4051
|
-
function
|
|
4118
|
+
function normalizeStudioDiskRevision(value) {
|
|
4119
|
+
const revision = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
4120
|
+
return /^sha256:[a-f0-9]{64}$/.test(revision) ? revision : null;
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
function markFileBackedBaseline(text, diskRevision) {
|
|
4052
4124
|
fileBackedBaselineText = String(text || "");
|
|
4125
|
+
fileBackedDiskRevision = normalizeStudioDiskRevision(diskRevision);
|
|
4126
|
+
scheduleWorkspacePersistence();
|
|
4053
4127
|
}
|
|
4054
4128
|
|
|
4055
4129
|
function clearFileBackedBaseline() {
|
|
4056
4130
|
fileBackedBaselineText = null;
|
|
4131
|
+
fileBackedDiskRevision = null;
|
|
4057
4132
|
}
|
|
4058
4133
|
|
|
4059
4134
|
function hasRefreshableFilePath() {
|
|
@@ -4068,13 +4143,17 @@
|
|
|
4068
4143
|
|
|
4069
4144
|
function updateSourceBadge() {
|
|
4070
4145
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
4071
|
-
const originText =
|
|
4146
|
+
const originText = isWatchedFilePreview
|
|
4147
|
+
? ("Watching: " + label + " · read-only" + (watchedFilePreviewState.lastError || watchedFilePreviewState.renderError ? " · last good preview" : ""))
|
|
4148
|
+
: ((studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label + (hasRefreshableFilePath() ? " · file" : ""));
|
|
4072
4149
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4073
4150
|
if (sourceBadgeEl) {
|
|
4074
4151
|
sourceBadgeEl.textContent = originText;
|
|
4075
|
-
sourceBadgeEl.title =
|
|
4152
|
+
sourceBadgeEl.title = isWatchedFilePreview
|
|
4153
|
+
? ("Read-only watched file: " + (descriptor.label || label) + "\nStudio follows settled disk changes and keeps the last good rendered preview through temporary failures.")
|
|
4154
|
+
: (descriptor.fileBacked
|
|
4076
4155
|
? ("Editor origin: " + label + "\nClick to reset origin and detach the current editor text into a new draft. The file on disk will not be changed.")
|
|
4077
|
-
: ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes.");
|
|
4156
|
+
: ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes."));
|
|
4078
4157
|
}
|
|
4079
4158
|
if (sourceOriginSummaryEl) {
|
|
4080
4159
|
sourceOriginSummaryEl.textContent = originText;
|
|
@@ -4533,10 +4612,14 @@
|
|
|
4533
4612
|
}
|
|
4534
4613
|
|
|
4535
4614
|
function triggerEditorSaveShortcut() {
|
|
4536
|
-
if (saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
|
|
4615
|
+
if (hasRefreshableFilePath() && saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
|
|
4537
4616
|
saveOverBtn.click();
|
|
4538
4617
|
return true;
|
|
4539
4618
|
}
|
|
4619
|
+
return triggerEditorSaveAsShortcut();
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
function triggerEditorSaveAsShortcut() {
|
|
4540
4623
|
if (saveAsBtn && !saveAsBtn.disabled && !saveAsBtn.hidden) {
|
|
4541
4624
|
saveAsBtn.click();
|
|
4542
4625
|
return true;
|
|
@@ -4821,6 +4904,16 @@
|
|
|
4821
4904
|
return;
|
|
4822
4905
|
}
|
|
4823
4906
|
|
|
4907
|
+
const isFilesShortcut = (key.toLowerCase() === "f" || code === "KeyF")
|
|
4908
|
+
&& (event.metaKey || event.ctrlKey)
|
|
4909
|
+
&& event.altKey
|
|
4910
|
+
&& !event.shiftKey;
|
|
4911
|
+
if (isFilesShortcut) {
|
|
4912
|
+
event.preventDefault();
|
|
4913
|
+
switchRightPaneToView("files");
|
|
4914
|
+
return;
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4824
4917
|
const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
|
|
4825
4918
|
&& (event.metaKey || event.ctrlKey)
|
|
4826
4919
|
&& event.altKey
|
|
@@ -4869,6 +4962,22 @@
|
|
|
4869
4962
|
return;
|
|
4870
4963
|
}
|
|
4871
4964
|
|
|
4965
|
+
const isSaveAsShortcut =
|
|
4966
|
+
key.toLowerCase() === "s"
|
|
4967
|
+
&& (event.metaKey || event.ctrlKey)
|
|
4968
|
+
&& !event.altKey
|
|
4969
|
+
&& event.shiftKey;
|
|
4970
|
+
|
|
4971
|
+
if (isSaveAsShortcut) {
|
|
4972
|
+
event.preventDefault();
|
|
4973
|
+
if (isWatchedFilePreview) {
|
|
4974
|
+
setStatus("This preview is read-only. Open a file tab to edit or save a copy.", "warning");
|
|
4975
|
+
return;
|
|
4976
|
+
}
|
|
4977
|
+
triggerEditorSaveAsShortcut();
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4872
4981
|
const isSaveShortcut =
|
|
4873
4982
|
key.toLowerCase() === "s"
|
|
4874
4983
|
&& (event.metaKey || event.ctrlKey)
|
|
@@ -4877,6 +4986,10 @@
|
|
|
4877
4986
|
|
|
4878
4987
|
if (isSaveShortcut) {
|
|
4879
4988
|
event.preventDefault();
|
|
4989
|
+
if (isWatchedFilePreview) {
|
|
4990
|
+
setStatus("This preview follows disk and cannot save. Open a file tab to edit safely.", "warning");
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4880
4993
|
triggerEditorSaveShortcut();
|
|
4881
4994
|
return;
|
|
4882
4995
|
}
|
|
@@ -5623,6 +5736,8 @@
|
|
|
5623
5736
|
clearPreviewJumpHighlight(targetEl);
|
|
5624
5737
|
finishPreviewRender(targetEl);
|
|
5625
5738
|
targetEl.innerHTML = html;
|
|
5739
|
+
clearWatchedPreviewRenderError();
|
|
5740
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, text);
|
|
5626
5741
|
if (pane === "response") {
|
|
5627
5742
|
applyPendingResponseScrollReset();
|
|
5628
5743
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -6041,7 +6156,8 @@
|
|
|
6041
6156
|
+ " const images = Array.prototype.slice.call(document.querySelectorAll('img[src]'));\n"
|
|
6042
6157
|
+ " images.forEach((image) => {\n"
|
|
6043
6158
|
+ " if (!image || !image.getAttribute) return;\n"
|
|
6044
|
-
+ "
|
|
6159
|
+
+ " const resourceState = image.getAttribute('data-pi-studio-html-resource-resolved') || '';\n"
|
|
6160
|
+
+ " if (resourceState === 'true' || resourceState === 'blocked' || resourceState === 'failed') return;\n"
|
|
6045
6161
|
+ " const raw = String(image.getAttribute('src') || '').trim();\n"
|
|
6046
6162
|
+ " if (!shouldResolveHtmlPreviewResourceUrl(raw)) return;\n"
|
|
6047
6163
|
+ " let resourceId = image.getAttribute('data-pi-studio-html-resource-id') || '';\n"
|
|
@@ -6073,6 +6189,7 @@
|
|
|
6073
6189
|
+ " image.setAttribute('data-pi-studio-html-resource-resolved', 'true');\n"
|
|
6074
6190
|
+ " } else if (typeof result.error === 'string' && result.error) {\n"
|
|
6075
6191
|
+ " image.setAttribute('title', result.error);\n"
|
|
6192
|
+
+ " image.setAttribute('data-pi-studio-html-resource-resolved', result.blocked === true ? 'blocked' : 'failed');\n"
|
|
6076
6193
|
+ " }\n"
|
|
6077
6194
|
+ " htmlResourcePlaceholders.delete(resourceId);\n"
|
|
6078
6195
|
+ " });\n"
|
|
@@ -6523,18 +6640,28 @@
|
|
|
6523
6640
|
const message = payload && typeof payload.error === "string"
|
|
6524
6641
|
? payload.error
|
|
6525
6642
|
: (timeoutLabel || "Local preview resource load") + " failed with HTTP " + response.status + ".";
|
|
6526
|
-
|
|
6643
|
+
const requestError = new Error(message);
|
|
6644
|
+
requestError.studioPayload = payload && typeof payload === "object" ? payload : null;
|
|
6645
|
+
requestError.studioStatus = response.status;
|
|
6646
|
+
throw requestError;
|
|
6527
6647
|
}
|
|
6528
6648
|
return payload.dataUrl;
|
|
6529
6649
|
}
|
|
6530
6650
|
|
|
6531
6651
|
function postHtmlArtifactResourceResults(record, results) {
|
|
6532
6652
|
if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
|
|
6653
|
+
const publicResults = Array.isArray(results) ? results.map((result) => ({
|
|
6654
|
+
resourceId: result && result.resourceId ? result.resourceId : "",
|
|
6655
|
+
ok: Boolean(result && result.ok === true),
|
|
6656
|
+
dataUrl: result && typeof result.dataUrl === "string" ? result.dataUrl : undefined,
|
|
6657
|
+
error: result && typeof result.error === "string" ? result.error : undefined,
|
|
6658
|
+
blocked: Boolean(result && result.blocked === true),
|
|
6659
|
+
})) : [];
|
|
6533
6660
|
try {
|
|
6534
6661
|
record.iframe.contentWindow.postMessage({
|
|
6535
6662
|
type: "pi-studio-html-artifact-resources-resolved",
|
|
6536
6663
|
id: record.id || "",
|
|
6537
|
-
results:
|
|
6664
|
+
results: publicResults,
|
|
6538
6665
|
}, "*");
|
|
6539
6666
|
} catch {
|
|
6540
6667
|
// Ignore iframe postMessage failures.
|
|
@@ -6552,14 +6679,58 @@
|
|
|
6552
6679
|
);
|
|
6553
6680
|
return { resourceId, ok: true, dataUrl };
|
|
6554
6681
|
} catch (error) {
|
|
6555
|
-
|
|
6682
|
+
const grantRequest = getStudioResourceGrantRequest(error);
|
|
6683
|
+
return {
|
|
6684
|
+
resourceId,
|
|
6685
|
+
ok: false,
|
|
6686
|
+
error: error && error.message ? error.message : String(error || "HTML preview resource load failed."),
|
|
6687
|
+
blocked: Boolean(grantRequest),
|
|
6688
|
+
grantRequest,
|
|
6689
|
+
sourceUrl: item && item.url ? String(item.url) : "",
|
|
6690
|
+
};
|
|
6556
6691
|
}
|
|
6557
6692
|
}
|
|
6558
6693
|
|
|
6694
|
+
function renderHtmlArtifactBlockedResources(record) {
|
|
6695
|
+
if (!record || !record.shell) return;
|
|
6696
|
+
const existing = record.shell.querySelector(".studio-html-artifact-blocked-media");
|
|
6697
|
+
if (existing) existing.remove();
|
|
6698
|
+
const blocked = record.blockedResources instanceof Map ? Array.from(record.blockedResources.values()) : [];
|
|
6699
|
+
if (!blocked.length) return;
|
|
6700
|
+
const container = document.createElement("div");
|
|
6701
|
+
container.className = "studio-html-artifact-blocked-media";
|
|
6702
|
+
blocked.forEach((entry) => {
|
|
6703
|
+
const notice = createStudioBlockedMediaNoticeForRequest(entry.grantRequest, "image", entry.sourceUrl);
|
|
6704
|
+
if (notice) container.appendChild(notice);
|
|
6705
|
+
});
|
|
6706
|
+
if (!container.childNodes.length) return;
|
|
6707
|
+
const frame = record.iframe && record.iframe.parentNode === record.shell ? record.iframe : null;
|
|
6708
|
+
record.shell.insertBefore(container, frame);
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6711
|
+
function syncHtmlArtifactBlockedResources(record, results) {
|
|
6712
|
+
if (!record) return;
|
|
6713
|
+
if (!(record.blockedResources instanceof Map)) record.blockedResources = new Map();
|
|
6714
|
+
(Array.isArray(results) ? results : []).forEach((result) => {
|
|
6715
|
+
const resourceId = result && result.resourceId ? String(result.resourceId) : "";
|
|
6716
|
+
if (!resourceId) return;
|
|
6717
|
+
if (result.blocked === true && result.grantRequest) {
|
|
6718
|
+
record.blockedResources.set(resourceId, {
|
|
6719
|
+
grantRequest: result.grantRequest,
|
|
6720
|
+
sourceUrl: result.sourceUrl || "local image",
|
|
6721
|
+
});
|
|
6722
|
+
} else {
|
|
6723
|
+
record.blockedResources.delete(resourceId);
|
|
6724
|
+
}
|
|
6725
|
+
});
|
|
6726
|
+
renderHtmlArtifactBlockedResources(record);
|
|
6727
|
+
}
|
|
6728
|
+
|
|
6559
6729
|
async function resolveHtmlArtifactResources(record, items) {
|
|
6560
6730
|
if (!record || !Array.isArray(items) || items.length === 0) return;
|
|
6561
6731
|
if (record.detail) record.detail.textContent = "HTML preview · loading local images";
|
|
6562
6732
|
const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
|
|
6733
|
+
syncHtmlArtifactBlockedResources(record, results);
|
|
6563
6734
|
postHtmlArtifactResourceResults(record, results);
|
|
6564
6735
|
setHtmlArtifactDetailText(record, "HTML preview");
|
|
6565
6736
|
}
|
|
@@ -6620,12 +6791,14 @@
|
|
|
6620
6791
|
const action = typeof data.action === "string" ? data.action : "open";
|
|
6621
6792
|
if (action === "contextmenu") {
|
|
6622
6793
|
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
6623
|
-
showPreviewLinkMenu(null, point, context);
|
|
6794
|
+
void showPreviewLinkMenu(null, point, context);
|
|
6624
6795
|
return;
|
|
6625
6796
|
}
|
|
6626
6797
|
const kind = getPreviewLocalLinkKind(context.href);
|
|
6627
6798
|
if (kind === "pdf") {
|
|
6628
|
-
openPreviewPdfLink(context.href, context.title, context)
|
|
6799
|
+
void openPreviewPdfLink(context.href, context.title, context).catch((error) => {
|
|
6800
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked PDF."), "warning");
|
|
6801
|
+
});
|
|
6629
6802
|
return;
|
|
6630
6803
|
}
|
|
6631
6804
|
if (kind === "image") {
|
|
@@ -6635,9 +6808,8 @@
|
|
|
6635
6808
|
return;
|
|
6636
6809
|
}
|
|
6637
6810
|
if (kind === "text" || kind === "office") {
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
});
|
|
6811
|
+
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
6812
|
+
void showPreviewLinkMenu(null, point, context);
|
|
6641
6813
|
return;
|
|
6642
6814
|
}
|
|
6643
6815
|
setStatus("Right-click this local HTML preview link for file actions.", "warning");
|
|
@@ -7041,9 +7213,12 @@
|
|
|
7041
7213
|
mathRenderItemCount: 0,
|
|
7042
7214
|
resourceResolveBatchCount: 0,
|
|
7043
7215
|
resourceResolveItemCount: 0,
|
|
7216
|
+
blockedResources: new Map(),
|
|
7044
7217
|
});
|
|
7045
7218
|
|
|
7046
7219
|
targetEl.appendChild(shell);
|
|
7220
|
+
clearWatchedPreviewRenderError();
|
|
7221
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, html);
|
|
7047
7222
|
|
|
7048
7223
|
if (pane === "response") {
|
|
7049
7224
|
applyPendingResponseScrollReset();
|
|
@@ -7927,6 +8102,13 @@
|
|
|
7927
8102
|
const target = event && event.target;
|
|
7928
8103
|
if (!(target instanceof Element)) return;
|
|
7929
8104
|
|
|
8105
|
+
const blockedMediaButton = target.closest("button.studio-blocked-media-allow");
|
|
8106
|
+
if (blockedMediaButton) {
|
|
8107
|
+
consumeStudioPreviewMediaEvent(event);
|
|
8108
|
+
void handleStudioBlockedMediaAllowButton(blockedMediaButton);
|
|
8109
|
+
return;
|
|
8110
|
+
}
|
|
8111
|
+
|
|
7930
8112
|
const imageEl = target.closest("img.studio-image-focus-target");
|
|
7931
8113
|
if (imageEl) {
|
|
7932
8114
|
consumeStudioPreviewMediaEvent(event);
|
|
@@ -8400,7 +8582,53 @@
|
|
|
8400
8582
|
});
|
|
8401
8583
|
}
|
|
8402
8584
|
|
|
8403
|
-
function
|
|
8585
|
+
function createStudioPdfStatusCard(block, content) {
|
|
8586
|
+
const options = block && block.options ? block.options : {};
|
|
8587
|
+
const path = String(options.path || "").trim();
|
|
8588
|
+
const title = String(options.title || path || "Embedded PDF").trim();
|
|
8589
|
+
const caption = String(options.caption || "").trim();
|
|
8590
|
+
const card = document.createElement("figure");
|
|
8591
|
+
card.className = "studio-pdf-card studio-pdf-card-status";
|
|
8592
|
+
const header = document.createElement("figcaption");
|
|
8593
|
+
header.className = "studio-pdf-card-header";
|
|
8594
|
+
const titleEl = document.createElement("div");
|
|
8595
|
+
titleEl.className = "studio-pdf-card-title";
|
|
8596
|
+
titleEl.textContent = title;
|
|
8597
|
+
header.appendChild(titleEl);
|
|
8598
|
+
card.appendChild(header);
|
|
8599
|
+
if (caption) {
|
|
8600
|
+
const captionEl = document.createElement("div");
|
|
8601
|
+
captionEl.className = "studio-pdf-card-caption";
|
|
8602
|
+
captionEl.textContent = caption;
|
|
8603
|
+
card.appendChild(captionEl);
|
|
8604
|
+
}
|
|
8605
|
+
if (content instanceof Element) {
|
|
8606
|
+
card.appendChild(content);
|
|
8607
|
+
} else {
|
|
8608
|
+
const errorEl = document.createElement("div");
|
|
8609
|
+
errorEl.className = "studio-pdf-card-error";
|
|
8610
|
+
errorEl.textContent = String(content || "PDF resource unavailable.");
|
|
8611
|
+
card.appendChild(errorEl);
|
|
8612
|
+
}
|
|
8613
|
+
return card;
|
|
8614
|
+
}
|
|
8615
|
+
|
|
8616
|
+
async function createStudioPdfCard(block, useEditorResourceContext) {
|
|
8617
|
+
const options = block && block.options ? block.options : {};
|
|
8618
|
+
const path = String(options.path || "").trim();
|
|
8619
|
+
if (!path) return createAuthorizedStudioPdfCard(block, useEditorResourceContext);
|
|
8620
|
+
const resourceQuery = buildStudioPdfResourceQuery(options, useEditorResourceContext);
|
|
8621
|
+
try {
|
|
8622
|
+
await fetchPreviewLocalLink("resolve", path, resourceQuery, { skipGrantPrompt: true });
|
|
8623
|
+
return createAuthorizedStudioPdfCard(block, useEditorResourceContext);
|
|
8624
|
+
} catch (error) {
|
|
8625
|
+
const notice = createStudioBlockedMediaNotice(error, "pdf", path);
|
|
8626
|
+
if (notice) return createStudioPdfStatusCard(block, notice);
|
|
8627
|
+
return createStudioPdfStatusCard(block, error && error.message ? error.message : "PDF resource unavailable.");
|
|
8628
|
+
}
|
|
8629
|
+
}
|
|
8630
|
+
|
|
8631
|
+
function createAuthorizedStudioPdfCard(block, useEditorResourceContext) {
|
|
8404
8632
|
const options = block && block.options ? block.options : {};
|
|
8405
8633
|
const path = String(options.path || "").trim();
|
|
8406
8634
|
const title = String(options.title || path || "Embedded PDF").trim();
|
|
@@ -8563,17 +8791,17 @@
|
|
|
8563
8791
|
return card;
|
|
8564
8792
|
}
|
|
8565
8793
|
|
|
8566
|
-
function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
|
|
8794
|
+
async function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
|
|
8567
8795
|
if (!targetEl || !Array.isArray(blocks) || blocks.length === 0) return;
|
|
8568
8796
|
const candidates = Array.from(targetEl.querySelectorAll("p, pre, div"));
|
|
8569
|
-
|
|
8797
|
+
for (const block of blocks) {
|
|
8570
8798
|
const placeholder = block && block.placeholder ? block.placeholder : "";
|
|
8571
|
-
if (!placeholder)
|
|
8799
|
+
if (!placeholder) continue;
|
|
8572
8800
|
const match = candidates.find((el) => String(el.textContent || "").trim() === placeholder);
|
|
8573
|
-
if (match
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
}
|
|
8801
|
+
if (!match || !match.parentNode) continue;
|
|
8802
|
+
const card = await createStudioPdfCard(block, useEditorResourceContext);
|
|
8803
|
+
if (match.isConnected && match.parentNode) match.replaceWith(card);
|
|
8804
|
+
}
|
|
8577
8805
|
}
|
|
8578
8806
|
|
|
8579
8807
|
function sanitizeRenderedHtml(html, markdown, options) {
|
|
@@ -9145,13 +9373,206 @@
|
|
|
9145
9373
|
targetEl.appendChild(el);
|
|
9146
9374
|
}
|
|
9147
9375
|
|
|
9376
|
+
function createStudioBlockedMediaNotice(error, mediaKind, sourceLabel) {
|
|
9377
|
+
return createStudioBlockedMediaNoticeForRequest(getStudioResourceGrantRequest(error), mediaKind, sourceLabel);
|
|
9378
|
+
}
|
|
9379
|
+
|
|
9380
|
+
function createStudioBlockedMediaNoticeForRequest(grantRequest, mediaKind, sourceLabel) {
|
|
9381
|
+
if (!grantRequest) return null;
|
|
9382
|
+
const kind = mediaKind === "pdf" ? "PDF" : "image";
|
|
9383
|
+
const notice = document.createElement("span");
|
|
9384
|
+
notice.className = "studio-blocked-media studio-blocked-media-" + (mediaKind === "pdf" ? "pdf" : "image");
|
|
9385
|
+
notice.setAttribute("role", "note");
|
|
9386
|
+
|
|
9387
|
+
const text = document.createElement("span");
|
|
9388
|
+
text.className = "studio-blocked-media-text";
|
|
9389
|
+
const title = document.createElement("strong");
|
|
9390
|
+
title.textContent = "Local " + kind + " blocked";
|
|
9391
|
+
const detail = document.createElement("span");
|
|
9392
|
+
detail.textContent = "This media is outside the locations allowed for the current Studio session.";
|
|
9393
|
+
const path = document.createElement("code");
|
|
9394
|
+
path.textContent = grantRequest.path || String(sourceLabel || "local media");
|
|
9395
|
+
path.title = "Path on the computer running Pi";
|
|
9396
|
+
text.appendChild(title);
|
|
9397
|
+
text.appendChild(detail);
|
|
9398
|
+
text.appendChild(path);
|
|
9399
|
+
notice.appendChild(text);
|
|
9400
|
+
|
|
9401
|
+
const allowButton = document.createElement("button");
|
|
9402
|
+
allowButton.type = "button";
|
|
9403
|
+
allowButton.className = "studio-blocked-media-allow";
|
|
9404
|
+
allowButton.textContent = "Allow local " + kind + "…";
|
|
9405
|
+
allowButton.title = "Choose whether to allow only this file or its containing folder for the current Studio session.";
|
|
9406
|
+
allowButton.dataset.studioBlockedMediaPath = grantRequest.path;
|
|
9407
|
+
allowButton.dataset.studioBlockedMediaDirectory = grantRequest.directoryPath;
|
|
9408
|
+
allowButton.dataset.studioBlockedMediaLabel = grantRequest.label;
|
|
9409
|
+
allowButton.dataset.studioBlockedMediaKind = mediaKind === "pdf" ? "pdf" : "image";
|
|
9410
|
+
notice.appendChild(allowButton);
|
|
9411
|
+
return notice;
|
|
9412
|
+
}
|
|
9413
|
+
|
|
9414
|
+
function replaceStudioBlockedMediaElement(element, source, error, mediaKind) {
|
|
9415
|
+
if (!element || !element.parentNode) return false;
|
|
9416
|
+
const notice = createStudioBlockedMediaNotice(error, mediaKind, source);
|
|
9417
|
+
if (!notice) return false;
|
|
9418
|
+
element.replaceWith(notice);
|
|
9419
|
+
return true;
|
|
9420
|
+
}
|
|
9421
|
+
|
|
9422
|
+
function refreshStudioPassiveMediaAfterGrant() {
|
|
9423
|
+
renderSourcePreview({ previewDelayMs: 0 });
|
|
9424
|
+
if (rightView === "preview" || rightView === "editor-preview" || rightView === "side-questions") {
|
|
9425
|
+
renderActiveResult();
|
|
9426
|
+
}
|
|
9427
|
+
if (isQuizOpen()) renderQuizOverlay({ preserveScroll: true });
|
|
9428
|
+
}
|
|
9429
|
+
|
|
9430
|
+
async function handleStudioBlockedMediaAllowButton(button) {
|
|
9431
|
+
if (!(button instanceof HTMLButtonElement) || button.dataset.studioBlockedMediaBusy === "1") return false;
|
|
9432
|
+
const request = {
|
|
9433
|
+
path: String(button.dataset.studioBlockedMediaPath || "").trim(),
|
|
9434
|
+
directoryPath: String(button.dataset.studioBlockedMediaDirectory || "").trim(),
|
|
9435
|
+
label: String(button.dataset.studioBlockedMediaLabel || "local media").trim() || "local media",
|
|
9436
|
+
};
|
|
9437
|
+
if (!request.path || !request.directoryPath) {
|
|
9438
|
+
setStatus("Could not resolve this blocked media path.", "warning");
|
|
9439
|
+
return false;
|
|
9440
|
+
}
|
|
9441
|
+
button.dataset.studioBlockedMediaBusy = "1";
|
|
9442
|
+
button.disabled = true;
|
|
9443
|
+
const baselineText = button.textContent;
|
|
9444
|
+
button.textContent = "Choosing…";
|
|
9445
|
+
try {
|
|
9446
|
+
if (!(await requestStudioResourceGrant(request))) return false;
|
|
9447
|
+
refreshStudioPassiveMediaAfterGrant();
|
|
9448
|
+
return true;
|
|
9449
|
+
} catch (error) {
|
|
9450
|
+
setStatus("Could not allow local media: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
9451
|
+
return false;
|
|
9452
|
+
} finally {
|
|
9453
|
+
if (button.isConnected) {
|
|
9454
|
+
delete button.dataset.studioBlockedMediaBusy;
|
|
9455
|
+
button.disabled = false;
|
|
9456
|
+
button.textContent = baselineText;
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
}
|
|
9460
|
+
|
|
9148
9461
|
function hasMeaningfulPreviewContent(targetEl) {
|
|
9149
9462
|
if (!targetEl || typeof targetEl.querySelector !== "function") return false;
|
|
9463
|
+
if (targetEl.dataset && targetEl.dataset.studioPreviewCommitted === "1") return true;
|
|
9150
9464
|
if (targetEl.querySelector(".preview-loading")) return false;
|
|
9151
9465
|
const text = typeof targetEl.textContent === "string" ? targetEl.textContent.trim() : "";
|
|
9152
9466
|
return text.length > 0;
|
|
9153
9467
|
}
|
|
9154
9468
|
|
|
9469
|
+
function getWatchedPreviewAnchorSignature(element) {
|
|
9470
|
+
if (!element || !element.tagName) return "";
|
|
9471
|
+
const text = String(element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 180);
|
|
9472
|
+
if (!text) return "";
|
|
9473
|
+
return String(element.tagName).toLowerCase() + ":" + text;
|
|
9474
|
+
}
|
|
9475
|
+
|
|
9476
|
+
function captureWatchedPreviewReadingPosition(targetEl) {
|
|
9477
|
+
if (!isWatchedFilePreview || !targetEl || typeof targetEl.querySelectorAll !== "function") return null;
|
|
9478
|
+
const maxScroll = Math.max(0, Number(targetEl.scrollHeight || 0) - Number(targetEl.clientHeight || 0));
|
|
9479
|
+
const ratio = maxScroll > 0 ? Math.max(0, Math.min(1, Number(targetEl.scrollTop || 0) / maxScroll)) : 0;
|
|
9480
|
+
if (typeof targetEl.getBoundingClientRect !== "function") return { ratio };
|
|
9481
|
+
const targetRect = targetEl.getBoundingClientRect();
|
|
9482
|
+
const anchorLine = Number(targetRect.top || 0) + Math.max(20, Math.min(Number(targetEl.clientHeight || 0) * 0.22, 140));
|
|
9483
|
+
const candidates = Array.from(targetEl.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,pre,table,blockquote,figure"));
|
|
9484
|
+
let anchor = null;
|
|
9485
|
+
for (const candidate of candidates) {
|
|
9486
|
+
if (!candidate || typeof candidate.getBoundingClientRect !== "function") continue;
|
|
9487
|
+
const rect = candidate.getBoundingClientRect();
|
|
9488
|
+
if (Number(rect.bottom || rect.top || 0) >= anchorLine) {
|
|
9489
|
+
anchor = candidate;
|
|
9490
|
+
break;
|
|
9491
|
+
}
|
|
9492
|
+
}
|
|
9493
|
+
if (!anchor && candidates.length) anchor = candidates[candidates.length - 1];
|
|
9494
|
+
const signature = getWatchedPreviewAnchorSignature(anchor);
|
|
9495
|
+
if (!anchor || !signature) return { ratio };
|
|
9496
|
+
let occurrence = 0;
|
|
9497
|
+
for (const candidate of candidates) {
|
|
9498
|
+
if (candidate === anchor) break;
|
|
9499
|
+
if (getWatchedPreviewAnchorSignature(candidate) === signature) occurrence += 1;
|
|
9500
|
+
}
|
|
9501
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
9502
|
+
return {
|
|
9503
|
+
ratio,
|
|
9504
|
+
signature,
|
|
9505
|
+
occurrence,
|
|
9506
|
+
offset: Number(anchorRect.top || 0) - Number(targetRect.top || 0),
|
|
9507
|
+
};
|
|
9508
|
+
}
|
|
9509
|
+
|
|
9510
|
+
function restoreWatchedPreviewReadingPosition(targetEl, snapshot) {
|
|
9511
|
+
if (!isWatchedFilePreview || !targetEl || !snapshot) return;
|
|
9512
|
+
const candidates = typeof targetEl.querySelectorAll === "function"
|
|
9513
|
+
? Array.from(targetEl.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,pre,table,blockquote,figure"))
|
|
9514
|
+
: [];
|
|
9515
|
+
const matching = snapshot.signature
|
|
9516
|
+
? candidates.filter((candidate) => getWatchedPreviewAnchorSignature(candidate) === snapshot.signature)
|
|
9517
|
+
: [];
|
|
9518
|
+
const anchor = matching[Math.max(0, Number(snapshot.occurrence) || 0)] || null;
|
|
9519
|
+
if (anchor && typeof anchor.getBoundingClientRect === "function" && typeof targetEl.getBoundingClientRect === "function") {
|
|
9520
|
+
const targetRect = targetEl.getBoundingClientRect();
|
|
9521
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
9522
|
+
const delta = (Number(anchorRect.top || 0) - Number(targetRect.top || 0)) - (Number(snapshot.offset) || 0);
|
|
9523
|
+
targetEl.scrollTop = Math.max(0, Number(targetEl.scrollTop || 0) + delta);
|
|
9524
|
+
return;
|
|
9525
|
+
}
|
|
9526
|
+
const maxScroll = Math.max(0, Number(targetEl.scrollHeight || 0) - Number(targetEl.clientHeight || 0));
|
|
9527
|
+
targetEl.scrollTop = Math.max(0, Math.min(maxScroll, maxScroll * Math.max(0, Math.min(1, Number(snapshot.ratio) || 0))));
|
|
9528
|
+
}
|
|
9529
|
+
|
|
9530
|
+
function scheduleWatchedPreviewReadingPositionRestore(targetEl, renderedText) {
|
|
9531
|
+
if (!isWatchedFilePreview || !targetEl) return;
|
|
9532
|
+
const pane = targetEl === sourcePreviewEl ? "source" : (targetEl === critiqueViewEl ? "response" : "");
|
|
9533
|
+
if (!pane || !watchedFilePreviewReadingPositions[pane]) return;
|
|
9534
|
+
const pending = watchedFilePreviewReadingPositions[pane];
|
|
9535
|
+
if (String(pending.text || "") !== String(renderedText || "")) return;
|
|
9536
|
+
const snapshot = pending.snapshot;
|
|
9537
|
+
watchedFilePreviewReadingPositions[pane] = null;
|
|
9538
|
+
let restored = false;
|
|
9539
|
+
const applyRestore = () => {
|
|
9540
|
+
if (restored) return;
|
|
9541
|
+
restored = true;
|
|
9542
|
+
restoreWatchedPreviewReadingPosition(targetEl, snapshot);
|
|
9543
|
+
};
|
|
9544
|
+
if (typeof window.requestAnimationFrame === "function") {
|
|
9545
|
+
window.requestAnimationFrame(applyRestore);
|
|
9546
|
+
}
|
|
9547
|
+
// Hidden embedded/headless surfaces can suspend animation frames entirely.
|
|
9548
|
+
window.setTimeout(applyRestore, 80);
|
|
9549
|
+
}
|
|
9550
|
+
|
|
9551
|
+
function showWatchedPreviewRenderError(targetEl, message) {
|
|
9552
|
+
if (!isWatchedFilePreview || !targetEl || typeof targetEl.appendChild !== "function") return false;
|
|
9553
|
+
const existing = typeof targetEl.querySelector === "function" ? targetEl.querySelector(".studio-watched-preview-error") : null;
|
|
9554
|
+
if (existing && existing.remove) existing.remove();
|
|
9555
|
+
const notice = document.createElement("div");
|
|
9556
|
+
notice.className = "preview-warning studio-watched-preview-error";
|
|
9557
|
+
notice.setAttribute("role", "status");
|
|
9558
|
+
notice.appendChild(document.createTextNode("Could not render the latest disk revision; keeping the last good preview. " + String(message || "Preview renderer unavailable.") + " "));
|
|
9559
|
+
const retry = document.createElement("button");
|
|
9560
|
+
retry.type = "button";
|
|
9561
|
+
retry.textContent = "Retry";
|
|
9562
|
+
retry.addEventListener("click", () => renderActiveResult());
|
|
9563
|
+
notice.appendChild(retry);
|
|
9564
|
+
targetEl.appendChild(notice);
|
|
9565
|
+
return true;
|
|
9566
|
+
}
|
|
9567
|
+
|
|
9568
|
+
function clearWatchedPreviewRenderError() {
|
|
9569
|
+
if (!isWatchedFilePreview) return;
|
|
9570
|
+
const recovered = Boolean(watchedFilePreviewState.renderError);
|
|
9571
|
+
watchedFilePreviewState.renderError = "";
|
|
9572
|
+
updateSourceBadge();
|
|
9573
|
+
if (recovered) setStatus("Rendered the latest watched file revision.", "success");
|
|
9574
|
+
}
|
|
9575
|
+
|
|
9155
9576
|
function beginPreviewRender(targetEl) {
|
|
9156
9577
|
if (!targetEl || !targetEl.classList) return;
|
|
9157
9578
|
|
|
@@ -10451,6 +10872,55 @@
|
|
|
10451
10872
|
});
|
|
10452
10873
|
}
|
|
10453
10874
|
|
|
10875
|
+
async function hydrateStudioPreviewLocalMedia(targetEl, resourceContext) {
|
|
10876
|
+
const fetchResource = (resourceUrl, timeoutLabel) => fetchLocalPreviewResourceDataUrl(
|
|
10877
|
+
resourceContext,
|
|
10878
|
+
resourceUrl,
|
|
10879
|
+
RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS,
|
|
10880
|
+
timeoutLabel,
|
|
10881
|
+
);
|
|
10882
|
+
await previewResourceHelpers.hydrateStudioPreviewLocalImages(
|
|
10883
|
+
targetEl,
|
|
10884
|
+
(resourceUrl) => fetchResource(resourceUrl, "Preview image load"),
|
|
10885
|
+
{
|
|
10886
|
+
onError: (imageEl, resourceUrl, error) => {
|
|
10887
|
+
replaceStudioBlockedMediaElement(imageEl, resourceUrl, error, "image");
|
|
10888
|
+
},
|
|
10889
|
+
},
|
|
10890
|
+
);
|
|
10891
|
+
await previewResourceHelpers.hydrateStudioPreviewLocalPdfEmbeds(
|
|
10892
|
+
targetEl,
|
|
10893
|
+
(resourceUrl) => fetchResource(resourceUrl, "Preview PDF load"),
|
|
10894
|
+
{
|
|
10895
|
+
onError: (embedEl, resourceUrl, error) => {
|
|
10896
|
+
replaceStudioBlockedMediaElement(embedEl, resourceUrl, error, "pdf");
|
|
10897
|
+
},
|
|
10898
|
+
},
|
|
10899
|
+
);
|
|
10900
|
+
}
|
|
10901
|
+
|
|
10902
|
+
function isCurrentStudioPreviewRender(pane, nonce) {
|
|
10903
|
+
if (pane === "source") {
|
|
10904
|
+
return nonce === sourcePreviewRenderNonce && editorView === "preview";
|
|
10905
|
+
}
|
|
10906
|
+
return nonce === responsePreviewRenderNonce && (rightView === "preview" || rightView === "editor-preview");
|
|
10907
|
+
}
|
|
10908
|
+
|
|
10909
|
+
function createStudioPreviewStagingElement(targetEl) {
|
|
10910
|
+
const staging = document.createElement("div");
|
|
10911
|
+
staging.className = String(targetEl && targetEl.className ? targetEl.className : "rendered-markdown") + " studio-preview-staging";
|
|
10912
|
+
staging.setAttribute("aria-hidden", "true");
|
|
10913
|
+
staging.style.width = Math.max(320, Number(targetEl && targetEl.clientWidth) || 0) + "px";
|
|
10914
|
+
document.body.appendChild(staging);
|
|
10915
|
+
return staging;
|
|
10916
|
+
}
|
|
10917
|
+
|
|
10918
|
+
function commitStudioPreviewStagingElement(targetEl, staging) {
|
|
10919
|
+
const nodes = Array.from(staging.childNodes || []);
|
|
10920
|
+
targetEl.replaceChildren(...nodes);
|
|
10921
|
+
staging.remove();
|
|
10922
|
+
}
|
|
10923
|
+
|
|
10454
10924
|
async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
|
|
10455
10925
|
const previewPrepared = annotationsEnabled
|
|
10456
10926
|
? prepareMarkdownForPandocPreview(markdown)
|
|
@@ -10461,42 +10931,44 @@
|
|
|
10461
10931
|
};
|
|
10462
10932
|
const pdfPrepared = prepareStudioPdfBlocksForPreview(previewPrepared.markdown);
|
|
10463
10933
|
const previewResourceContext = getHtmlPreviewResourceContextOptions();
|
|
10934
|
+
let staging = null;
|
|
10935
|
+
let previewCommitted = false;
|
|
10936
|
+
const stillCurrent = () => isCurrentStudioPreviewRender(pane, nonce);
|
|
10937
|
+
const abandonIfStale = () => {
|
|
10938
|
+
if (stillCurrent()) return false;
|
|
10939
|
+
if (staging && staging.remove) staging.remove();
|
|
10940
|
+
staging = null;
|
|
10941
|
+
return true;
|
|
10942
|
+
};
|
|
10464
10943
|
|
|
10465
10944
|
try {
|
|
10466
10945
|
const renderedHtml = await renderMarkdownWithPandoc(pdfPrepared.markdown, {
|
|
10467
10946
|
includeEditorLanguage: pane === "source" || rightView === "editor-preview",
|
|
10468
10947
|
resourceContext: previewResourceContext,
|
|
10469
10948
|
});
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
"Preview image load",
|
|
10486
|
-
)
|
|
10487
|
-
));
|
|
10488
|
-
renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
|
|
10489
|
-
applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
|
|
10490
|
-
await renderAnnotationMathInElement(targetEl);
|
|
10491
|
-
decoratePdfEmbeds(targetEl);
|
|
10492
|
-
await renderPdfPreviewsInElement(targetEl);
|
|
10493
|
-
decoratePreviewPdfFigures(targetEl);
|
|
10949
|
+
if (abandonIfStale()) return;
|
|
10950
|
+
|
|
10951
|
+
staging = createStudioPreviewStagingElement(targetEl);
|
|
10952
|
+
staging.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
|
|
10953
|
+
await hydrateStudioPreviewLocalMedia(staging, previewResourceContext);
|
|
10954
|
+
if (abandonIfStale()) return;
|
|
10955
|
+
await renderStudioPdfBlocksInElement(staging, pdfPrepared.blocks, previewingEditorText);
|
|
10956
|
+
if (abandonIfStale()) return;
|
|
10957
|
+
applyPreviewAnnotationPlaceholdersToElement(staging, previewPrepared.placeholders);
|
|
10958
|
+
await renderAnnotationMathInElement(staging);
|
|
10959
|
+
if (abandonIfStale()) return;
|
|
10960
|
+
decoratePdfEmbeds(staging);
|
|
10961
|
+
await renderPdfPreviewsInElement(staging);
|
|
10962
|
+
if (abandonIfStale()) return;
|
|
10963
|
+
decoratePreviewPdfFigures(staging);
|
|
10494
10964
|
const annotationMode = (pane === "source" || pane === "response")
|
|
10495
10965
|
? (annotationsEnabled ? "highlight" : "hide")
|
|
10496
10966
|
: "none";
|
|
10497
|
-
applyAnnotationMarkersToElement(
|
|
10498
|
-
await renderMermaidInElement(
|
|
10499
|
-
|
|
10967
|
+
applyAnnotationMarkersToElement(staging, annotationMode);
|
|
10968
|
+
await renderMermaidInElement(staging);
|
|
10969
|
+
if (abandonIfStale()) return;
|
|
10970
|
+
await renderMathFallbackInElement(staging);
|
|
10971
|
+
if (abandonIfStale()) return;
|
|
10500
10972
|
|
|
10501
10973
|
const shouldDecoratePreviewComments = supportsPreviewCommentsForCurrentEditor()
|
|
10502
10974
|
&& (
|
|
@@ -10504,35 +10976,56 @@
|
|
|
10504
10976
|
|| (pane === "response" && rightView === "editor-preview")
|
|
10505
10977
|
);
|
|
10506
10978
|
if (shouldDecoratePreviewComments) {
|
|
10507
|
-
decorateRenderedEditorPreviewComments(
|
|
10979
|
+
decorateRenderedEditorPreviewComments(staging, sourceTextEl.value || "");
|
|
10508
10980
|
}
|
|
10509
|
-
decorateCopyablePreviewBlocks(
|
|
10510
|
-
decoratePreviewImages(
|
|
10981
|
+
decorateCopyablePreviewBlocks(staging);
|
|
10982
|
+
decoratePreviewImages(staging);
|
|
10511
10983
|
|
|
10512
|
-
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
10984
|
+
// Warn if relative images are present but unlikely to resolve (non-file-backed content).
|
|
10513
10985
|
if (!sourceState.path && !getCurrentResourceDirValue()) {
|
|
10514
10986
|
var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
|
|
10515
10987
|
var hasLatexImages = /\\includegraphics/.test(markdown || "");
|
|
10516
10988
|
if (hasRelativeImages || hasLatexImages) {
|
|
10517
|
-
appendPreviewNotice(
|
|
10989
|
+
appendPreviewNotice(staging, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
|
|
10518
10990
|
}
|
|
10519
10991
|
}
|
|
10992
|
+
if (abandonIfStale()) return;
|
|
10520
10993
|
|
|
10994
|
+
clearPreviewJumpHighlight(targetEl);
|
|
10995
|
+
finishPreviewRender(targetEl);
|
|
10996
|
+
commitStudioPreviewStagingElement(targetEl, staging);
|
|
10997
|
+
staging = null;
|
|
10998
|
+
if (targetEl.dataset) targetEl.dataset.studioPreviewCommitted = "1";
|
|
10999
|
+
previewCommitted = true;
|
|
11000
|
+
if (shouldDecoratePreviewComments) updatePreviewCommentBlocksForElement(targetEl);
|
|
11001
|
+
clearWatchedPreviewRenderError();
|
|
11002
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, markdown);
|
|
10521
11003
|
if (pane === "response") {
|
|
10522
11004
|
applyPendingResponseScrollReset();
|
|
10523
11005
|
scheduleResponsePaneRepaintNudge();
|
|
10524
11006
|
}
|
|
10525
11007
|
} catch (error) {
|
|
10526
|
-
if (
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
11008
|
+
if (staging && staging.remove) staging.remove();
|
|
11009
|
+
staging = null;
|
|
11010
|
+
if (previewCommitted) {
|
|
11011
|
+
console.error("Preview post-render update failed after the staged document was committed:", error);
|
|
11012
|
+
return;
|
|
10530
11013
|
}
|
|
11014
|
+
if (!stillCurrent()) return;
|
|
10531
11015
|
|
|
10532
11016
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
10533
11017
|
clearPreviewJumpHighlight(targetEl);
|
|
10534
11018
|
finishPreviewRender(targetEl);
|
|
11019
|
+
if (isWatchedFilePreview && hasMeaningfulPreviewContent(targetEl)) {
|
|
11020
|
+
watchedFilePreviewState.renderError = detail;
|
|
11021
|
+
showWatchedPreviewRenderError(targetEl, detail);
|
|
11022
|
+
updateSourceBadge();
|
|
11023
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, markdown);
|
|
11024
|
+
setStatus("Could not render the latest disk revision; keeping the last good preview.", "warning");
|
|
11025
|
+
return;
|
|
11026
|
+
}
|
|
10535
11027
|
targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown, previewFallbackOptions);
|
|
11028
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, markdown);
|
|
10536
11029
|
if (pane === "response") {
|
|
10537
11030
|
applyPendingResponseScrollReset();
|
|
10538
11031
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -11355,71 +11848,96 @@
|
|
|
11355
11848
|
return "<select class='files-sort-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
|
|
11356
11849
|
}
|
|
11357
11850
|
|
|
11851
|
+
function buildFileBrowserEntryRowHtml(entry) {
|
|
11852
|
+
const type = entry && entry.type === "directory" ? "directory" : "file";
|
|
11853
|
+
const kind = (entry && entry.kind) || (type === "directory" ? "directory" : "other");
|
|
11854
|
+
const path = String((entry && entry.path) || "");
|
|
11855
|
+
const name = String((entry && entry.name) || basenameForStudioPath(path));
|
|
11856
|
+
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
|
|
11857
|
+
const metaParts = [getFileBrowserKindLabel(entry)];
|
|
11858
|
+
if (type === "file") metaParts.push(formatFileBrowserSize(entry && entry.size));
|
|
11859
|
+
const time = formatFileBrowserTime(entry && entry.mtimeMs);
|
|
11860
|
+
if (time) metaParts.push(time);
|
|
11861
|
+
const newTabAction = kind === "text" || kind === "office"
|
|
11862
|
+
? "open-new"
|
|
11863
|
+
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
11864
|
+
const newTabLabel = kind === "text"
|
|
11865
|
+
? "Open file-backed tab"
|
|
11866
|
+
: (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
|
|
11867
|
+
const newTabTitle = kind === "text"
|
|
11868
|
+
? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
|
|
11869
|
+
: (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
|
|
11870
|
+
const newTabButton = newTabAction
|
|
11871
|
+
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
11872
|
+
: "";
|
|
11873
|
+
const watchButton = kind === "text"
|
|
11874
|
+
? "<button type='button' data-files-action='watch-new' data-files-path='" + escapeHtml(path) + "' data-files-kind='text' title='Open a read-only rendered preview that follows this file on disk.'>Preview (follow)</button>"
|
|
11875
|
+
: "";
|
|
11876
|
+
const openTitle = type === "directory"
|
|
11877
|
+
? "Open folder"
|
|
11878
|
+
: (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
11879
|
+
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
11880
|
+
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
11881
|
+
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
11882
|
+
+ "<span class='files-name'>" + escapeHtml(name) + "</span>"
|
|
11883
|
+
+ "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
|
|
11884
|
+
+ "</button>"
|
|
11885
|
+
+ "<span class='files-actions'>"
|
|
11886
|
+
+ watchButton
|
|
11887
|
+
+ newTabButton
|
|
11888
|
+
+ "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(path) + "'>Copy path</button>"
|
|
11889
|
+
+ (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(path) + "'>Reveal</button>" : "")
|
|
11890
|
+
+ "</span>"
|
|
11891
|
+
+ "</div>";
|
|
11892
|
+
}
|
|
11893
|
+
|
|
11894
|
+
function buildFileBrowserLocationSelectHtml(state) {
|
|
11895
|
+
const locations = Array.isArray(state && state.locations) ? state.locations : [];
|
|
11896
|
+
if (!locations.length) return "<span class='files-no-locations'>No allowed folders</span>";
|
|
11897
|
+
const rootDir = String((state && state.rootDir) || "");
|
|
11898
|
+
const options = locations.map((location) => {
|
|
11899
|
+
const path = String((location && location.path) || "");
|
|
11900
|
+
const label = String((location && location.label) || basenameForStudioPath(path) || path);
|
|
11901
|
+
return "<option value='" + escapeHtml(path) + "'" + (path === rootDir ? " selected" : "") + ">" + escapeHtml(label + " — " + path) + "</option>";
|
|
11902
|
+
}).join("");
|
|
11903
|
+
return "<select class='files-location-select' data-files-location aria-label='Allowed Files location' title='Choose a folder allowed for this Studio session on the computer running Pi.'>" + options + "</select>";
|
|
11904
|
+
}
|
|
11905
|
+
|
|
11358
11906
|
function buildFileBrowserPanelHtml() {
|
|
11359
11907
|
const state = fileBrowserState || {};
|
|
11360
11908
|
const entries = Array.isArray(state.entries) ? state.entries : [];
|
|
11909
|
+
const exactFiles = Array.isArray(state.exactFiles) ? state.exactFiles : [];
|
|
11361
11910
|
const currentDir = state.currentDir || "";
|
|
11362
11911
|
const rootDir = state.rootDir || "";
|
|
11363
11912
|
const relativeDir = state.relativeDir || ".";
|
|
11364
11913
|
const parentDisabled = state.parentDir ? "" : " disabled";
|
|
11365
11914
|
const rows = entries.length
|
|
11366
|
-
? entries.map((
|
|
11367
|
-
const type = entry.type === "directory" ? "directory" : "file";
|
|
11368
|
-
const kind = entry.kind || (type === "directory" ? "directory" : "other");
|
|
11369
|
-
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
|
|
11370
|
-
const metaParts = [];
|
|
11371
|
-
metaParts.push(getFileBrowserKindLabel(entry));
|
|
11372
|
-
if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
|
|
11373
|
-
const time = formatFileBrowserTime(entry.mtimeMs);
|
|
11374
|
-
if (time) metaParts.push(time);
|
|
11375
|
-
const newTabAction = kind === "text" || kind === "office"
|
|
11376
|
-
? "open-new"
|
|
11377
|
-
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
11378
|
-
const newTabLabel = kind === "text"
|
|
11379
|
-
? "Open file-backed tab"
|
|
11380
|
-
: (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
|
|
11381
|
-
const newTabTitle = kind === "text"
|
|
11382
|
-
? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
|
|
11383
|
-
: (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
|
|
11384
|
-
const textActions = newTabAction
|
|
11385
|
-
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
11386
|
-
: "";
|
|
11387
|
-
const openTitle = type === "directory"
|
|
11388
|
-
? "Open folder"
|
|
11389
|
-
: (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
11390
|
-
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
11391
|
-
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(entry.path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
11392
|
-
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
11393
|
-
+ "<span class='files-name'>" + escapeHtml(entry.name) + "</span>"
|
|
11394
|
-
+ "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
|
|
11395
|
-
+ "</button>"
|
|
11396
|
-
+ "<span class='files-actions'>"
|
|
11397
|
-
+ textActions
|
|
11398
|
-
+ "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(entry.path) + "'>Copy path</button>"
|
|
11399
|
-
+ (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(entry.path) + "'>Reveal</button>" : "")
|
|
11400
|
-
+ "</span>"
|
|
11401
|
-
+ "</div>";
|
|
11402
|
-
}).join("")
|
|
11915
|
+
? entries.map(buildFileBrowserEntryRowHtml).join("")
|
|
11403
11916
|
: "<div class='files-empty'>" + (state.loading ? "Loading files…" : "This folder is empty.") + "</div>";
|
|
11917
|
+
const exactRows = exactFiles.map(buildFileBrowserEntryRowHtml).join("");
|
|
11404
11918
|
const notices = [];
|
|
11405
11919
|
if (state.error) notices.push("<div class='files-notice files-notice-error'>" + escapeHtml(state.error) + "</div>");
|
|
11920
|
+
if (state.grantRequiredPath) notices.push("<div class='files-notice'>Allow that folder to browse it in Files. Exact-file grants do not expose their parent folders.</div>");
|
|
11406
11921
|
if (state.omitted) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omitted)) + " item" + (state.omitted === 1 ? "" : "s") + " omitted.</div>");
|
|
11407
11922
|
if (state.omittedIgnored) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omittedIgnored)) + " heavy/cache folder" + (state.omittedIgnored === 1 ? "" : "s") + " hidden.</div>");
|
|
11408
11923
|
return "<div class='files-panel'>"
|
|
11409
11924
|
+ "<div class='files-toolbar'>"
|
|
11410
11925
|
+ "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
|
|
11411
11926
|
+ "<div class='files-toolbar-actions'>"
|
|
11927
|
+
+ buildFileBrowserLocationSelectHtml(state)
|
|
11412
11928
|
+ buildFileBrowserSortSelectHtml()
|
|
11929
|
+
+ "<button type='button' data-files-action='allow-folder'>Allow folder…</button>"
|
|
11413
11930
|
+ "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
|
|
11414
11931
|
+ "<button type='button' data-files-action='refresh'>Refresh</button>"
|
|
11415
11932
|
+ (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
|
|
11416
11933
|
+ (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
|
|
11417
|
-
+ (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager.'>Open root</button>" : "")
|
|
11934
|
+
+ (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager on the computer running Pi.'>Open root</button>" : "")
|
|
11418
11935
|
+ (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
|
|
11419
11936
|
+ "</div>"
|
|
11420
11937
|
+ "</div>"
|
|
11421
|
-
+ "<div class='files-subtitle'>
|
|
11938
|
+
+ "<div class='files-subtitle'>Allowed root on the computer running Pi: <span title='" + escapeHtml(rootDir) + "'>" + escapeHtml(rootDir || "none selected") + "</span></div>"
|
|
11422
11939
|
+ notices.join("")
|
|
11940
|
+
+ (exactRows ? "<section class='files-exact-section'><div class='files-section-title'>Allowed exact files</div><div class='files-list' role='list'>" + exactRows + "</div></section>" : "")
|
|
11423
11941
|
+ "<div class='files-list' role='list'>" + rows + "</div>"
|
|
11424
11942
|
+ "</div>";
|
|
11425
11943
|
}
|
|
@@ -11434,6 +11952,9 @@
|
|
|
11434
11952
|
relativeDir: "",
|
|
11435
11953
|
parentDir: null,
|
|
11436
11954
|
entries: [],
|
|
11955
|
+
exactFiles: [],
|
|
11956
|
+
locations: [],
|
|
11957
|
+
grantRequiredPath: "",
|
|
11437
11958
|
omitted: 0,
|
|
11438
11959
|
omittedIgnored: 0,
|
|
11439
11960
|
sort: fileBrowserSortMode,
|
|
@@ -11455,12 +11976,17 @@
|
|
|
11455
11976
|
async function loadFileBrowserDirectory(dir, options) {
|
|
11456
11977
|
const context = getHtmlPreviewResourceContextOptions();
|
|
11457
11978
|
const contextKey = getFileBrowserContextKey();
|
|
11979
|
+
const config = options && typeof options === "object" ? options : {};
|
|
11980
|
+
const requestedRoot = Object.prototype.hasOwnProperty.call(config, "root")
|
|
11981
|
+
? normalizeStudioResourceDirValue(config.root || "")
|
|
11982
|
+
: normalizeStudioResourceDirValue(fileBrowserState.rootDir || "");
|
|
11458
11983
|
const nonce = ++fileBrowserLoadNonce;
|
|
11459
11984
|
fileBrowserState = {
|
|
11460
11985
|
...fileBrowserState,
|
|
11461
11986
|
contextKey,
|
|
11462
11987
|
loading: true,
|
|
11463
11988
|
error: "",
|
|
11989
|
+
grantRequiredPath: "",
|
|
11464
11990
|
};
|
|
11465
11991
|
if (rightView === "files") {
|
|
11466
11992
|
finishPreviewRender(critiqueViewEl);
|
|
@@ -11469,6 +11995,7 @@
|
|
|
11469
11995
|
try {
|
|
11470
11996
|
const query = {};
|
|
11471
11997
|
if (dir) query.dir = String(dir);
|
|
11998
|
+
if (requestedRoot) query.root = requestedRoot;
|
|
11472
11999
|
if (context.sourcePath) query.sourcePath = context.sourcePath;
|
|
11473
12000
|
if (context.resourceDir) query.resourceDir = context.resourceDir;
|
|
11474
12001
|
query.sort = normalizeFileBrowserSortMode(fileBrowserSortMode);
|
|
@@ -11481,6 +12008,9 @@
|
|
|
11481
12008
|
relativeDir: typeof payload.relativeDir === "string" ? payload.relativeDir : ".",
|
|
11482
12009
|
parentDir: typeof payload.parentDir === "string" ? payload.parentDir : null,
|
|
11483
12010
|
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
12011
|
+
exactFiles: Array.isArray(payload.exactFiles) ? payload.exactFiles : [],
|
|
12012
|
+
locations: Array.isArray(payload.locations) ? payload.locations : [],
|
|
12013
|
+
grantRequiredPath: "",
|
|
11484
12014
|
omitted: Number(payload.omitted) || 0,
|
|
11485
12015
|
omittedIgnored: Number(payload.omittedIgnored) || 0,
|
|
11486
12016
|
sort: normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode),
|
|
@@ -11494,11 +12024,18 @@
|
|
|
11494
12024
|
critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
|
|
11495
12025
|
scheduleResponsePaneRepaintNudge();
|
|
11496
12026
|
}
|
|
11497
|
-
if (
|
|
12027
|
+
if (config.user) setStatus("Loaded file list.", "success");
|
|
11498
12028
|
} catch (error) {
|
|
11499
12029
|
if (nonce !== fileBrowserLoadNonce) return;
|
|
12030
|
+
const errorPayload = error && error.studioPayload && typeof error.studioPayload === "object" ? error.studioPayload : null;
|
|
12031
|
+
const grantRequiredPath = errorPayload && errorPayload.code === "studio-resource-directory-grant-required" && typeof errorPayload.directoryPath === "string"
|
|
12032
|
+
? normalizeStudioResourceDirValue(errorPayload.directoryPath)
|
|
12033
|
+
: "";
|
|
11500
12034
|
fileBrowserState = {
|
|
11501
12035
|
...fileBrowserState,
|
|
12036
|
+
exactFiles: errorPayload && Array.isArray(errorPayload.exactFiles) ? errorPayload.exactFiles : fileBrowserState.exactFiles,
|
|
12037
|
+
locations: errorPayload && Array.isArray(errorPayload.locations) ? errorPayload.locations : fileBrowserState.locations,
|
|
12038
|
+
grantRequiredPath,
|
|
11502
12039
|
loading: false,
|
|
11503
12040
|
error: (error && error.message) ? error.message : String(error || "Could not load files."),
|
|
11504
12041
|
loaded: true,
|
|
@@ -11520,7 +12057,7 @@
|
|
|
11520
12057
|
function ensureCurrentEditorFileBackedFromFilesPath(path) {
|
|
11521
12058
|
const cleanPath = stripPreviewLocalLinkUrlSuffix(path || "").trim();
|
|
11522
12059
|
if (!isLikelyAbsoluteStudioPath(cleanPath)) return;
|
|
11523
|
-
if (sourceState && sourceState.path
|
|
12060
|
+
if (sourceState && sourceState.path) return;
|
|
11524
12061
|
const resourceDir = normalizeStudioResourceDirValue(fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(cleanPath));
|
|
11525
12062
|
if (resourceDirInput && resourceDir) resourceDirInput.value = resourceDir;
|
|
11526
12063
|
setSourceState({
|
|
@@ -11528,7 +12065,7 @@
|
|
|
11528
12065
|
label: sourceState && sourceState.label && sourceState.label !== "blank" ? sourceState.label : basenameForStudioPath(cleanPath),
|
|
11529
12066
|
path: cleanPath,
|
|
11530
12067
|
});
|
|
11531
|
-
markFileBackedBaseline(sourceTextEl.value);
|
|
12068
|
+
markFileBackedBaseline(sourceTextEl.value, null);
|
|
11532
12069
|
}
|
|
11533
12070
|
|
|
11534
12071
|
async function openFileBrowserEntry(path, kind) {
|
|
@@ -11546,7 +12083,7 @@
|
|
|
11546
12083
|
return;
|
|
11547
12084
|
}
|
|
11548
12085
|
if (kind === "pdf") {
|
|
11549
|
-
openPreviewPdfLink(path, path, context);
|
|
12086
|
+
await openPreviewPdfLink(path, path, context);
|
|
11550
12087
|
return;
|
|
11551
12088
|
}
|
|
11552
12089
|
if (kind === "image") {
|
|
@@ -11556,6 +12093,34 @@
|
|
|
11556
12093
|
setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
|
|
11557
12094
|
}
|
|
11558
12095
|
|
|
12096
|
+
async function allowFileBrowserFolder() {
|
|
12097
|
+
const context = getHtmlPreviewResourceContextOptions();
|
|
12098
|
+
const suggested = normalizeStudioResourceDirValue(
|
|
12099
|
+
fileBrowserState.grantRequiredPath
|
|
12100
|
+
|| fileBrowserState.currentDir
|
|
12101
|
+
|| fileBrowserState.rootDir
|
|
12102
|
+
|| context.resourceDir
|
|
12103
|
+
|| (context.sourcePath ? dirnameForDisplayPath(context.sourcePath) : "")
|
|
12104
|
+
|| "./",
|
|
12105
|
+
);
|
|
12106
|
+
const path = await requestStudioTextInput(
|
|
12107
|
+
"Allow a folder on the computer running Pi for this Studio session:",
|
|
12108
|
+
suggested,
|
|
12109
|
+
{ title: "Allow folder", confirmLabel: "Allow folder", inputLabel: "Folder path" },
|
|
12110
|
+
);
|
|
12111
|
+
if (!path) return;
|
|
12112
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
12113
|
+
method: "POST",
|
|
12114
|
+
body: JSON.stringify({ grantKind: "directory", path }),
|
|
12115
|
+
});
|
|
12116
|
+
const grantedPath = payload && payload.grant && payload.grant.kind === "directory" && typeof payload.grant.path === "string"
|
|
12117
|
+
? payload.grant.path
|
|
12118
|
+
: "";
|
|
12119
|
+
if (!grantedPath) throw new Error("Studio did not return the allowed folder.");
|
|
12120
|
+
await loadFileBrowserDirectory("", { root: grantedPath });
|
|
12121
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed folder for this Studio session.", "success");
|
|
12122
|
+
}
|
|
12123
|
+
|
|
11559
12124
|
function setFileBrowserCurrentDirectoryAsWorkingDir(path) {
|
|
11560
12125
|
const nextDir = normalizeStudioResourceDirValue(path || fileBrowserState.currentDir || "");
|
|
11561
12126
|
if (!nextDir) {
|
|
@@ -11577,6 +12142,7 @@
|
|
|
11577
12142
|
}
|
|
11578
12143
|
const context = getHtmlPreviewResourceContextOptions();
|
|
11579
12144
|
const body = { dir: targetDir };
|
|
12145
|
+
if (fileBrowserState.rootDir) body.root = fileBrowserState.rootDir;
|
|
11580
12146
|
if (context.sourcePath) body.sourcePath = context.sourcePath;
|
|
11581
12147
|
if (context.resourceDir) body.resourceDir = context.resourceDir;
|
|
11582
12148
|
const payload = await fetchStudioJson("/file-browser-open", {
|
|
@@ -11589,6 +12155,14 @@
|
|
|
11589
12155
|
async function handleFilesPaneChange(event) {
|
|
11590
12156
|
if (rightView !== "files") return;
|
|
11591
12157
|
const target = event.target;
|
|
12158
|
+
const locationSelect = target instanceof Element ? target.closest("[data-files-location]") : null;
|
|
12159
|
+
if (locationSelect && "value" in locationSelect) {
|
|
12160
|
+
const nextRoot = normalizeStudioResourceDirValue(locationSelect.value || "");
|
|
12161
|
+
if (nextRoot && nextRoot !== fileBrowserState.rootDir) {
|
|
12162
|
+
await loadFileBrowserDirectory("", { root: nextRoot, user: true });
|
|
12163
|
+
}
|
|
12164
|
+
return;
|
|
12165
|
+
}
|
|
11592
12166
|
const sortSelect = target instanceof Element ? target.closest("[data-files-sort]") : null;
|
|
11593
12167
|
if (!sortSelect || !("value" in sortSelect)) return;
|
|
11594
12168
|
const nextSort = writeFileBrowserSortMode(sortSelect.value);
|
|
@@ -11607,6 +12181,10 @@
|
|
|
11607
12181
|
const path = actionEl.getAttribute("data-files-path") || "";
|
|
11608
12182
|
const kind = actionEl.getAttribute("data-files-kind") || getPreviewLocalLinkKind(path);
|
|
11609
12183
|
try {
|
|
12184
|
+
if (action === "allow-folder") {
|
|
12185
|
+
await allowFileBrowserFolder();
|
|
12186
|
+
return;
|
|
12187
|
+
}
|
|
11610
12188
|
if (action === "parent") {
|
|
11611
12189
|
if (fileBrowserState.parentDir) await loadFileBrowserDirectory(fileBrowserState.parentDir, { user: true });
|
|
11612
12190
|
return;
|
|
@@ -11624,17 +12202,13 @@
|
|
|
11624
12202
|
return;
|
|
11625
12203
|
}
|
|
11626
12204
|
if (action === "open-new") {
|
|
11627
|
-
if (kind === "text" && isLikelyAbsoluteStudioPath(path)) {
|
|
11628
|
-
openFileBackedStudioEditorTab(path, {
|
|
11629
|
-
label: basenameForStudioPath(path),
|
|
11630
|
-
resourceDir: fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(path),
|
|
11631
|
-
});
|
|
11632
|
-
setStatus("Opening file-backed document in a new editor.");
|
|
11633
|
-
return;
|
|
11634
|
-
}
|
|
11635
12205
|
await openPreviewDocumentInNewEditor(path, getFileBrowserLocalLinkContext());
|
|
11636
12206
|
return;
|
|
11637
12207
|
}
|
|
12208
|
+
if (action === "watch-new") {
|
|
12209
|
+
await openPreviewDocumentInWatchedPreview(path, getFileBrowserLocalLinkContext());
|
|
12210
|
+
return;
|
|
12211
|
+
}
|
|
11638
12212
|
if (action === "open-preview-new") {
|
|
11639
12213
|
await openPreviewResourceInNewEditor(path, getFileBrowserLocalLinkContext());
|
|
11640
12214
|
return;
|
|
@@ -12345,6 +12919,33 @@
|
|
|
12345
12919
|
};
|
|
12346
12920
|
}
|
|
12347
12921
|
|
|
12922
|
+
async function ensureSideQuestionContextRootAuthorized(context) {
|
|
12923
|
+
if (!context || context.gatherScope === "none") return { contextRoot: "" };
|
|
12924
|
+
const body = JSON.stringify({
|
|
12925
|
+
gatherScope: context.gatherScope,
|
|
12926
|
+
sourcePath: context.sourcePath,
|
|
12927
|
+
resourceDir: context.resourceDir,
|
|
12928
|
+
contextPath: context.contextPath,
|
|
12929
|
+
});
|
|
12930
|
+
const check = () => fetchStudioJson("/side-question-context-root", { method: "POST", body });
|
|
12931
|
+
try {
|
|
12932
|
+
return await check();
|
|
12933
|
+
} catch (error) {
|
|
12934
|
+
const request = getStudioDirectoryGrantRequest(error);
|
|
12935
|
+
if (!request) throw error;
|
|
12936
|
+
const allowed = await requestStudioDirectoryGrant(request, {
|
|
12937
|
+
message: "Side questions can map, search, and read supported files beneath this folder through read-only context tools. Allow that access for this Studio session?",
|
|
12938
|
+
cancelStatus: "Side-question folder access cancelled.",
|
|
12939
|
+
});
|
|
12940
|
+
if (!allowed) {
|
|
12941
|
+
const cancelled = new Error("Side-question folder access cancelled.");
|
|
12942
|
+
cancelled.studioCancelled = true;
|
|
12943
|
+
throw cancelled;
|
|
12944
|
+
}
|
|
12945
|
+
return check();
|
|
12946
|
+
}
|
|
12947
|
+
}
|
|
12948
|
+
|
|
12348
12949
|
async function renderSideQuestionMarkdownToHtml(markdown) {
|
|
12349
12950
|
const source = String(markdown || "");
|
|
12350
12951
|
if (sideQuestionMarkdownRenderCache.has(source)) return sideQuestionMarkdownRenderCache.get(source);
|
|
@@ -12369,7 +12970,11 @@
|
|
|
12369
12970
|
const html = await renderSideQuestionMarkdownToHtml(markdown);
|
|
12370
12971
|
if (nonce !== sideQuestionPreviewRenderNonce || rightView !== "side-questions" || !critiqueViewEl.contains(target)) return;
|
|
12371
12972
|
target.innerHTML = html;
|
|
12973
|
+
await hydrateStudioPreviewLocalMedia(target, getHtmlPreviewResourceContextOptions());
|
|
12372
12974
|
await renderAnnotationMathInElement(target);
|
|
12975
|
+
decoratePdfEmbeds(target);
|
|
12976
|
+
await renderPdfPreviewsInElement(target);
|
|
12977
|
+
decoratePreviewPdfFigures(target);
|
|
12373
12978
|
await renderMermaidInElement(target);
|
|
12374
12979
|
await renderMathFallbackInElement(target);
|
|
12375
12980
|
decorateCopyablePreviewBlocks(target);
|
|
@@ -12432,6 +13037,7 @@
|
|
|
12432
13037
|
], sideQuestionUi.thinking) + "</select></label>"
|
|
12433
13038
|
+ "</div>"
|
|
12434
13039
|
+ "<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>"
|
|
13040
|
+
+ (scope === "none" ? "" : "<p class='side-question-context-access-note'>Related-file access is limited to folders allowed for this Studio session. Studio asks before starting if this folder is not already allowed.</p>")
|
|
12435
13041
|
+ (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
13042
|
+ "<div class='side-question-checks'>"
|
|
12437
13043
|
+ "<label><input data-side-question-field='includeConversation' type='checkbox'" + (sideQuestionUi.includeConversation ? " checked" : "") + "> Include the current main conversation snapshot</label>"
|
|
@@ -12442,7 +13048,7 @@
|
|
|
12442
13048
|
+ "<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
13049
|
+ "<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
13050
|
+ (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>"
|
|
13051
|
+
+ "<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)'" + (sideQuestionContextGrantPending || !isSideQuestionConnectionReady() || (sideQuestionState && sideQuestionState.status === "running") || !sideQuestionUi.draft.trim() || (scope === "custom" && !sideQuestionUi.customPath.trim()) ? " disabled" : "") + ">" + (sideQuestionContextGrantPending ? "Checking related-file access…" : (sideQuestionState && sideQuestionState.status === "running" ? "Preparing side thread…" : "Ask side question")) + "</button></div>"
|
|
12446
13052
|
+ "</div>";
|
|
12447
13053
|
}
|
|
12448
13054
|
|
|
@@ -12513,7 +13119,8 @@
|
|
|
12513
13119
|
}
|
|
12514
13120
|
}
|
|
12515
13121
|
|
|
12516
|
-
function submitSideQuestion() {
|
|
13122
|
+
async function submitSideQuestion() {
|
|
13123
|
+
if (sideQuestionContextGrantPending) return;
|
|
12517
13124
|
const question = String(sideQuestionUi.draft || "").trim();
|
|
12518
13125
|
if (!question) {
|
|
12519
13126
|
setStatus("Enter a side question first.", "warning");
|
|
@@ -12532,11 +13139,31 @@
|
|
|
12532
13139
|
if (sideQuestionState && sideQuestionState.threadId) {
|
|
12533
13140
|
message.threadId = sideQuestionState.threadId;
|
|
12534
13141
|
} else {
|
|
12535
|
-
|
|
12536
|
-
if (
|
|
13142
|
+
const context = buildSideQuestionContextPayload();
|
|
13143
|
+
if (context.gatherScope === "custom" && !String(context.contextPath || "").trim()) {
|
|
12537
13144
|
setStatus("Choose a custom context path first.", "warning");
|
|
12538
13145
|
return;
|
|
12539
13146
|
}
|
|
13147
|
+
if (context.gatherScope !== "none") {
|
|
13148
|
+
sideQuestionContextGrantPending = true;
|
|
13149
|
+
if (rightView === "side-questions") renderSideQuestionView();
|
|
13150
|
+
setStatus("Checking side-question folder access…", "warning");
|
|
13151
|
+
try {
|
|
13152
|
+
await ensureSideQuestionContextRootAuthorized(context);
|
|
13153
|
+
} catch (error) {
|
|
13154
|
+
if (!(error && error.studioCancelled)) {
|
|
13155
|
+
const messageText = error && error.message ? error.message : String(error || "Could not authorize side-question context.");
|
|
13156
|
+
if (!sideQuestionState) sideQuestionState = normalizeSideQuestionState(null);
|
|
13157
|
+
sideQuestionState.error = messageText;
|
|
13158
|
+
setStatus("Could not use related files: " + messageText, "warning");
|
|
13159
|
+
}
|
|
13160
|
+
return;
|
|
13161
|
+
} finally {
|
|
13162
|
+
sideQuestionContextGrantPending = false;
|
|
13163
|
+
if (rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) renderSideQuestionView();
|
|
13164
|
+
}
|
|
13165
|
+
}
|
|
13166
|
+
message.context = context;
|
|
12540
13167
|
}
|
|
12541
13168
|
if (!sendMessage(message)) return;
|
|
12542
13169
|
sideQuestionUi.draft = "";
|
|
@@ -12652,7 +13279,7 @@
|
|
|
12652
13279
|
event.preventDefault();
|
|
12653
13280
|
const action = target.getAttribute("data-side-question-action");
|
|
12654
13281
|
if (action === "ask") {
|
|
12655
|
-
submitSideQuestion();
|
|
13282
|
+
await submitSideQuestion();
|
|
12656
13283
|
} else if (action === "stop") {
|
|
12657
13284
|
if (sideQuestionState && sideQuestionState.threadId && sideQuestionState.requestId) {
|
|
12658
13285
|
sendMessage({ type: "side_question_cancel_request", threadId: sideQuestionState.threadId, requestId: sideQuestionState.requestId });
|
|
@@ -12705,7 +13332,7 @@
|
|
|
12705
13332
|
if (field === "draft") sideQuestionUi.draft = target.value;
|
|
12706
13333
|
if (field === "customPath") sideQuestionUi.customPath = target.value;
|
|
12707
13334
|
const askButton = critiqueViewEl.querySelector("[data-side-question-action='ask']");
|
|
12708
|
-
if (askButton) askButton.disabled = !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
|
|
13335
|
+
if (askButton) askButton.disabled = sideQuestionContextGrantPending || !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
|
|
12709
13336
|
}
|
|
12710
13337
|
|
|
12711
13338
|
function handleSideQuestionKeydown(event) {
|
|
@@ -12719,7 +13346,7 @@
|
|
|
12719
13346
|
if (!submitShortcut) return;
|
|
12720
13347
|
event.preventDefault();
|
|
12721
13348
|
event.stopPropagation();
|
|
12722
|
-
if (!sideQuestionState || sideQuestionState.status !== "running") submitSideQuestion();
|
|
13349
|
+
if (!sideQuestionState || sideQuestionState.status !== "running") void submitSideQuestion();
|
|
12723
13350
|
}
|
|
12724
13351
|
|
|
12725
13352
|
async function handleSideQuestionChange(event) {
|
|
@@ -13212,13 +13839,13 @@
|
|
|
13212
13839
|
function updateSaveFileTooltip() {
|
|
13213
13840
|
if (!saveOverBtn) return;
|
|
13214
13841
|
|
|
13215
|
-
var effectivePath =
|
|
13842
|
+
var effectivePath = sourceState && sourceState.path ? sourceState.path : "";
|
|
13216
13843
|
if (effectivePath) {
|
|
13217
|
-
saveOverBtn.title = "
|
|
13844
|
+
saveOverBtn.title = "Save file when its disk revision still matches: " + effectivePath + " · Shortcut: Cmd/Ctrl+S.";
|
|
13218
13845
|
return;
|
|
13219
13846
|
}
|
|
13220
13847
|
|
|
13221
|
-
saveOverBtn.title = "Save editor is available after opening a file
|
|
13848
|
+
saveOverBtn.title = "Save editor is available after opening a file-backed document. Use Save editor as… for a new file.";
|
|
13222
13849
|
}
|
|
13223
13850
|
|
|
13224
13851
|
function updateRefreshFromDiskTooltip() {
|
|
@@ -13233,13 +13860,13 @@
|
|
|
13233
13860
|
}
|
|
13234
13861
|
|
|
13235
13862
|
function syncActionButtons() {
|
|
13236
|
-
const canSaveOver =
|
|
13863
|
+
const canSaveOver = hasRefreshableFilePath();
|
|
13237
13864
|
const canRefreshFromDisk = hasRefreshableFilePath();
|
|
13238
13865
|
|
|
13239
|
-
fileInput.disabled = uiBusy;
|
|
13240
|
-
if (importFileBtn) importFileBtn.disabled = uiBusy;
|
|
13241
|
-
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
|
|
13242
|
-
if (sourceResetOriginBtn) sourceResetOriginBtn.disabled = uiBusy;
|
|
13866
|
+
fileInput.disabled = uiBusy || isWatchedFilePreview;
|
|
13867
|
+
if (importFileBtn) importFileBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13868
|
+
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy || isWatchedFilePreview;
|
|
13869
|
+
if (sourceResetOriginBtn) sourceResetOriginBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13243
13870
|
if (sourceOpenCurrentFileTabBtn) {
|
|
13244
13871
|
sourceOpenCurrentFileTabBtn.disabled = uiBusy || !hasRefreshableFilePath();
|
|
13245
13872
|
sourceOpenCurrentFileTabBtn.title = hasRefreshableFilePath()
|
|
@@ -13247,17 +13874,21 @@
|
|
|
13247
13874
|
: "Available after opening a file-backed document.";
|
|
13248
13875
|
}
|
|
13249
13876
|
if (sourceOpenCurrentTextCopyTabBtn) sourceOpenCurrentTextCopyTabBtn.disabled = uiBusy || wsState !== "Ready" || !String(sourceTextEl.value || "").trim();
|
|
13250
|
-
saveAsBtn.disabled = uiBusy;
|
|
13251
|
-
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
13252
|
-
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
13253
|
-
if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy;
|
|
13877
|
+
saveAsBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13878
|
+
saveOverBtn.disabled = uiBusy || isWatchedFilePreview || !canSaveOver;
|
|
13879
|
+
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || isWatchedFilePreview || !canRefreshFromDisk;
|
|
13880
|
+
if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13254
13881
|
sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
13255
|
-
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
13882
|
+
if (getEditorBtn) getEditorBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13883
|
+
if (watchedOpenEditableBtn) {
|
|
13884
|
+
watchedOpenEditableBtn.hidden = !isWatchedFilePreview;
|
|
13885
|
+
watchedOpenEditableBtn.disabled = uiBusy || !watchedFilePreviewState.path;
|
|
13886
|
+
}
|
|
13256
13887
|
syncRunAndCritiqueButtons();
|
|
13257
13888
|
copyDraftBtn.disabled = uiBusy;
|
|
13258
13889
|
if (suggestCompletionBtn) {
|
|
13259
13890
|
const hasSuggestionForCurrentText = Boolean(completionSuggestionState && sourceTextEl && sourceTextEl.value === completionSuggestionState.baseText);
|
|
13260
|
-
suggestCompletionBtn.disabled = wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
|
|
13891
|
+
suggestCompletionBtn.disabled = isWatchedFilePreview || wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
|
|
13261
13892
|
suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Stop" : (hasSuggestionForCurrentText ? "Try another" : "Suggest");
|
|
13262
13893
|
suggestCompletionBtn.title = completionSuggestionInFlight
|
|
13263
13894
|
? "Stop the current suggestion request."
|
|
@@ -13273,15 +13904,15 @@
|
|
|
13273
13904
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
13274
13905
|
if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
|
|
13275
13906
|
if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
|
|
13276
|
-
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
|
|
13277
|
-
if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
13907
|
+
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13908
|
+
if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || isWatchedFilePreview || !hasAnnotationMarkers(sourceTextEl.value);
|
|
13278
13909
|
if (compactBtn) compactBtn.disabled = isEditorOnlyMode || uiBusy || compactInProgress || wsState === "Disconnected";
|
|
13279
13910
|
editorViewSelect.disabled = isEditorOnlyMode;
|
|
13280
13911
|
syncRightViewModeOptions();
|
|
13281
|
-
rightViewSelect.disabled =
|
|
13912
|
+
rightViewSelect.disabled = isWatchedFilePreview;
|
|
13282
13913
|
followSelect.disabled = isEditorOnlyMode || uiBusy;
|
|
13283
13914
|
if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
|
|
13284
|
-
insertHeaderBtn.disabled = uiBusy;
|
|
13915
|
+
insertHeaderBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13285
13916
|
lensSelect.disabled = uiBusy || isEditorOnlyMode;
|
|
13286
13917
|
updateSaveFileTooltip();
|
|
13287
13918
|
updateRefreshFromDiskTooltip();
|
|
@@ -13298,9 +13929,14 @@
|
|
|
13298
13929
|
|
|
13299
13930
|
function setSourceState(next, options) {
|
|
13300
13931
|
const previousDescriptor = getCurrentStudioDocumentDescriptor();
|
|
13932
|
+
const previousPath = sourceState && sourceState.path ? sourceState.path : null;
|
|
13301
13933
|
const previousQuartoPath = getCurrentStudioQuartoSourcePath();
|
|
13302
13934
|
const previousPreviewResourceContext = getHtmlPreviewResourceContextOptions();
|
|
13303
13935
|
const nextPath = next && next.path ? next.path : null;
|
|
13936
|
+
if (isWatchedFilePreview && watchedFilePreviewState.path && nextPath !== watchedFilePreviewState.path) {
|
|
13937
|
+
setStatus("This read-only preview remains bound to its watched file.", "warning");
|
|
13938
|
+
return false;
|
|
13939
|
+
}
|
|
13304
13940
|
sourceState = {
|
|
13305
13941
|
source: next && next.source ? next.source : "blank",
|
|
13306
13942
|
label: next && next.label ? next.label : "blank",
|
|
@@ -13318,7 +13954,7 @@
|
|
|
13318
13954
|
quartoPreviewActionRequestId = null;
|
|
13319
13955
|
quartoPreviewLogVisible = false;
|
|
13320
13956
|
}
|
|
13321
|
-
if (!sourceState.path) {
|
|
13957
|
+
if (!sourceState.path || sourceState.path !== previousPath) {
|
|
13322
13958
|
clearFileBackedBaseline();
|
|
13323
13959
|
}
|
|
13324
13960
|
syncRightViewModeOptions();
|
|
@@ -13425,6 +14061,7 @@
|
|
|
13425
14061
|
version: 1,
|
|
13426
14062
|
savedAt: lastWorkspacePersistenceSavedAt,
|
|
13427
14063
|
sourceState: normalizeWorkspaceSourceState(sourceState),
|
|
14064
|
+
diskRevision: fileBackedDiskRevision,
|
|
13428
14065
|
resourceDir: getCurrentResourceDirValue(),
|
|
13429
14066
|
editorView,
|
|
13430
14067
|
rightView: normalizeRightViewValue(rightView),
|
|
@@ -13460,7 +14097,7 @@
|
|
|
13460
14097
|
}
|
|
13461
14098
|
|
|
13462
14099
|
function persistWorkspaceStateNow(options) {
|
|
13463
|
-
if (!workspacePersistenceReady) return;
|
|
14100
|
+
if (!workspacePersistenceReady || isWatchedFilePreview) return;
|
|
13464
14101
|
try {
|
|
13465
14102
|
const payload = buildWorkspacePersistencePayload();
|
|
13466
14103
|
if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
|
|
@@ -13482,7 +14119,7 @@
|
|
|
13482
14119
|
}
|
|
13483
14120
|
|
|
13484
14121
|
function scheduleWorkspacePersistence() {
|
|
13485
|
-
if (!workspacePersistenceReady || workspacePersistTimer !== null) return;
|
|
14122
|
+
if (!workspacePersistenceReady || isWatchedFilePreview || workspacePersistTimer !== null) return;
|
|
13486
14123
|
workspacePersistTimer = window.setTimeout(() => {
|
|
13487
14124
|
workspacePersistTimer = null;
|
|
13488
14125
|
persistWorkspaceStateNow();
|
|
@@ -13490,6 +14127,7 @@
|
|
|
13490
14127
|
}
|
|
13491
14128
|
|
|
13492
14129
|
function flushWorkspacePersistence(options) {
|
|
14130
|
+
if (isWatchedFilePreview) return;
|
|
13493
14131
|
if (workspacePersistTimer !== null) {
|
|
13494
14132
|
window.clearTimeout(workspacePersistTimer);
|
|
13495
14133
|
workspacePersistTimer = null;
|
|
@@ -13519,9 +14157,17 @@
|
|
|
13519
14157
|
if (!shouldRestorePersistedWorkspaceState(state)) return false;
|
|
13520
14158
|
const nextSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
13521
14159
|
const nextResourceDir = normalizeStudioResourceDirValue(typeof state.resourceDir === "string" ? state.resourceDir : "");
|
|
14160
|
+
const currentBaselineText = fileBackedBaselineText;
|
|
14161
|
+
const currentDiskRevision = fileBackedDiskRevision;
|
|
14162
|
+
const persistedDiskRevision = normalizeStudioDiskRevision(state.diskRevision);
|
|
13522
14163
|
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
13523
14164
|
setEditorText(state.text, { preserveScroll: false, preserveSelection: false });
|
|
13524
14165
|
setSourceState(nextSourceState);
|
|
14166
|
+
if (nextSourceState.path) {
|
|
14167
|
+
fileBackedBaselineText = currentBaselineText;
|
|
14168
|
+
fileBackedDiskRevision = persistedDiskRevision
|
|
14169
|
+
|| (currentBaselineText !== null && state.text === currentBaselineText ? currentDiskRevision : null);
|
|
14170
|
+
}
|
|
13525
14171
|
if (resourceDirInput && nextResourceDir) {
|
|
13526
14172
|
resourceDirInput.value = nextResourceDir;
|
|
13527
14173
|
updateSourceBadge();
|
|
@@ -13607,6 +14253,10 @@
|
|
|
13607
14253
|
}
|
|
13608
14254
|
|
|
13609
14255
|
function setEditorText(nextText, options) {
|
|
14256
|
+
if (isWatchedFilePreview && !(options && options.allowWatchedFileUpdate === true)) {
|
|
14257
|
+
setStatus("This preview is read-only and follows its watched file on disk.", "warning");
|
|
14258
|
+
return false;
|
|
14259
|
+
}
|
|
13610
14260
|
const value = String(nextText || "");
|
|
13611
14261
|
const preserveScroll = Boolean(options && options.preserveScroll);
|
|
13612
14262
|
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
@@ -13654,9 +14304,14 @@
|
|
|
13654
14304
|
updateEditorSelectionCommentUi();
|
|
13655
14305
|
updateOutlineUi();
|
|
13656
14306
|
scheduleWorkspacePersistence();
|
|
14307
|
+
return true;
|
|
13657
14308
|
}
|
|
13658
14309
|
|
|
13659
14310
|
function applySourceTextEdit(nextText, selectionStart, selectionEnd) {
|
|
14311
|
+
if (isWatchedFilePreview) {
|
|
14312
|
+
setStatus("This preview is read-only and follows its watched file on disk.", "warning");
|
|
14313
|
+
return false;
|
|
14314
|
+
}
|
|
13660
14315
|
const value = String(nextText || "");
|
|
13661
14316
|
sourceTextEl.value = value;
|
|
13662
14317
|
const maxIndex = value.length;
|
|
@@ -13668,6 +14323,7 @@
|
|
|
13668
14323
|
if (editorView === "markdown") {
|
|
13669
14324
|
scheduleEditorLineNumberRender();
|
|
13670
14325
|
}
|
|
14326
|
+
return true;
|
|
13671
14327
|
}
|
|
13672
14328
|
|
|
13673
14329
|
function readCompletionSuggestionContextMode() {
|
|
@@ -14538,6 +15194,11 @@
|
|
|
14538
15194
|
return launch.fail(message || "Studio could not prepare this tab. Return to the originating Studio page for details.");
|
|
14539
15195
|
}
|
|
14540
15196
|
|
|
15197
|
+
function cancelPendingStudioTab(launch, message) {
|
|
15198
|
+
if (!launch || typeof launch.cancel !== "function") return false;
|
|
15199
|
+
return launch.cancel(message || "Studio tab launch was cancelled.");
|
|
15200
|
+
}
|
|
15201
|
+
|
|
14541
15202
|
function abandonAllStudioTabLaunches(message) {
|
|
14542
15203
|
const launches = Array.from(activeStudioTabLaunches);
|
|
14543
15204
|
activeStudioTabLaunches.clear();
|
|
@@ -14616,7 +15277,10 @@
|
|
|
14616
15277
|
const message = payload && typeof payload.error === "string"
|
|
14617
15278
|
? payload.error
|
|
14618
15279
|
: (response.status + " " + response.statusText).trim();
|
|
14619
|
-
|
|
15280
|
+
const requestError = new Error(message || (method + " " + pathname + " failed."));
|
|
15281
|
+
requestError.studioPayload = payload && typeof payload === "object" ? payload : null;
|
|
15282
|
+
requestError.studioStatus = response.status;
|
|
15283
|
+
throw requestError;
|
|
14620
15284
|
}
|
|
14621
15285
|
return payload;
|
|
14622
15286
|
}
|
|
@@ -14649,6 +15313,7 @@
|
|
|
14649
15313
|
]);
|
|
14650
15314
|
let previewLinkMenuEl = null;
|
|
14651
15315
|
let activePreviewLinkContext = null;
|
|
15316
|
+
let previewLinkMenuRequestId = 0;
|
|
14652
15317
|
|
|
14653
15318
|
function stripPreviewLocalLinkUrlSuffix(href) {
|
|
14654
15319
|
const raw = String(href || "").trim();
|
|
@@ -14715,7 +15380,7 @@
|
|
|
14715
15380
|
if (!raw || raw.charAt(0) === "#") return false;
|
|
14716
15381
|
if (/^\/\//.test(raw)) return false;
|
|
14717
15382
|
if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
|
|
14718
|
-
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
15383
|
+
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|resource-grants|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
14719
15384
|
return true;
|
|
14720
15385
|
}
|
|
14721
15386
|
|
|
@@ -14785,36 +15450,53 @@
|
|
|
14785
15450
|
menu.style.top = y + "px";
|
|
14786
15451
|
}
|
|
14787
15452
|
|
|
14788
|
-
function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
15453
|
+
async function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
14789
15454
|
const href = String(anchor && anchor.getAttribute ? anchor.getAttribute("href") || "" : (contextOverride && contextOverride.href ? contextOverride.href : "")).trim();
|
|
14790
15455
|
if (!isStudioLocalPreviewHref(href)) return false;
|
|
14791
15456
|
const kind = getPreviewLocalLinkKind(href);
|
|
14792
|
-
const menu = ensurePreviewLinkMenu();
|
|
14793
|
-
menu.innerHTML = "";
|
|
14794
15457
|
const linkContext = getEffectivePreviewLinkContext(contextOverride);
|
|
14795
|
-
|
|
15458
|
+
const nextContext = {
|
|
14796
15459
|
href,
|
|
14797
15460
|
title: String((contextOverride && contextOverride.title) || (anchor && anchor.textContent) || href || "local link").trim() || href,
|
|
14798
15461
|
sourcePath: linkContext.sourcePath,
|
|
14799
15462
|
resourceDir: linkContext.resourceDir,
|
|
14800
15463
|
};
|
|
15464
|
+
const menuPoint = {
|
|
15465
|
+
clientX: event && event.clientX,
|
|
15466
|
+
clientY: event && event.clientY,
|
|
15467
|
+
};
|
|
15468
|
+
closePreviewLinkMenu();
|
|
15469
|
+
const menuRequestId = ++previewLinkMenuRequestId;
|
|
15470
|
+
try {
|
|
15471
|
+
await fetchPreviewLocalLink("resolve", href, nextContext);
|
|
15472
|
+
} catch (error) {
|
|
15473
|
+
if (!(error && error.studioCancelled)) {
|
|
15474
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not inspect this local resource."), "warning");
|
|
15475
|
+
}
|
|
15476
|
+
return false;
|
|
15477
|
+
}
|
|
15478
|
+
if (menuRequestId !== previewLinkMenuRequestId) return false;
|
|
15479
|
+
const menu = ensurePreviewLinkMenu();
|
|
15480
|
+
menu.innerHTML = "";
|
|
15481
|
+
activePreviewLinkContext = nextContext;
|
|
14801
15482
|
if (kind === "pdf") {
|
|
14802
15483
|
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
14803
15484
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
14804
15485
|
appendPreviewLinkMenuButton(menu, "Open in system viewer", "open-system");
|
|
14805
15486
|
} else if (kind === "text") {
|
|
15487
|
+
appendPreviewLinkMenuButton(menu, "Preview file (follow changes)", "watch-new");
|
|
14806
15488
|
appendPreviewLinkMenuButton(menu, "Open file tab", "open-new");
|
|
14807
|
-
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
15489
|
+
if (!isWatchedFilePreview) appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
14808
15490
|
} else if (kind === "office") {
|
|
14809
15491
|
appendPreviewLinkMenuButton(menu, "Convert tab", "open-new");
|
|
14810
|
-
appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
15492
|
+
if (!isWatchedFilePreview) appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
14811
15493
|
} else if (kind === "image") {
|
|
14812
15494
|
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
14813
15495
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
14814
15496
|
}
|
|
14815
15497
|
appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
|
|
14816
15498
|
appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
|
|
14817
|
-
positionPreviewLinkMenu(menu,
|
|
15499
|
+
positionPreviewLinkMenu(menu, menuPoint.clientX, menuPoint.clientY);
|
|
14818
15500
|
const firstButton = menu.querySelector("button");
|
|
14819
15501
|
if (firstButton && typeof firstButton.focus === "function") {
|
|
14820
15502
|
window.setTimeout(() => firstButton.focus({ preventScroll: true }), 0);
|
|
@@ -14822,10 +15504,108 @@
|
|
|
14822
15504
|
return true;
|
|
14823
15505
|
}
|
|
14824
15506
|
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
15507
|
+
function getStudioResourceGrantRequest(error) {
|
|
15508
|
+
const payload = error && error.studioPayload && typeof error.studioPayload === "object"
|
|
15509
|
+
? error.studioPayload
|
|
15510
|
+
: null;
|
|
15511
|
+
if (!payload || payload.code !== "studio-resource-grant-required") return null;
|
|
15512
|
+
const path = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
15513
|
+
const directoryPath = typeof payload.directoryPath === "string" ? payload.directoryPath.trim() : "";
|
|
15514
|
+
if (!path || !directoryPath) return null;
|
|
15515
|
+
return {
|
|
15516
|
+
path,
|
|
15517
|
+
directoryPath,
|
|
15518
|
+
label: typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : basenameForStudioPath(path),
|
|
15519
|
+
};
|
|
15520
|
+
}
|
|
15521
|
+
|
|
15522
|
+
function getStudioDirectoryGrantRequest(error) {
|
|
15523
|
+
const payload = error && error.studioPayload && typeof error.studioPayload === "object"
|
|
15524
|
+
? error.studioPayload
|
|
15525
|
+
: null;
|
|
15526
|
+
if (!payload || payload.code !== "studio-resource-directory-grant-required") return null;
|
|
15527
|
+
const directoryPath = typeof payload.directoryPath === "string" ? payload.directoryPath.trim() : "";
|
|
15528
|
+
if (!directoryPath) return null;
|
|
15529
|
+
return {
|
|
15530
|
+
directoryPath,
|
|
15531
|
+
label: typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : basenameForStudioPath(directoryPath),
|
|
15532
|
+
};
|
|
15533
|
+
}
|
|
15534
|
+
|
|
15535
|
+
async function requestStudioDirectoryGrant(request, options) {
|
|
15536
|
+
if (!request || !request.directoryPath) return false;
|
|
15537
|
+
const config = options && typeof options === "object" ? options : {};
|
|
15538
|
+
const choice = await openStudioDecision({
|
|
15539
|
+
mode: "confirm",
|
|
15540
|
+
title: "Allow " + (request.label || "this folder") + "?",
|
|
15541
|
+
message: (config.message || "Allow read-only access beneath this folder for the current Studio session?")
|
|
15542
|
+
+ "\n\nFolder on the computer running Pi:\n" + request.directoryPath,
|
|
15543
|
+
cancelLabel: "Cancel",
|
|
15544
|
+
confirmLabel: "Allow this folder for this Studio session",
|
|
14828
15545
|
});
|
|
15546
|
+
if (choice !== true) {
|
|
15547
|
+
setStatus(config.cancelStatus || "Folder access cancelled.", "warning");
|
|
15548
|
+
return false;
|
|
15549
|
+
}
|
|
15550
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
15551
|
+
method: "POST",
|
|
15552
|
+
body: JSON.stringify({ grantKind: "directory", path: request.directoryPath }),
|
|
15553
|
+
});
|
|
15554
|
+
fileBrowserState = { ...fileBrowserState, contextKey: "", loaded: false };
|
|
15555
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed this folder for the current Studio session.", "success");
|
|
15556
|
+
return true;
|
|
15557
|
+
}
|
|
15558
|
+
|
|
15559
|
+
async function requestStudioResourceGrant(request) {
|
|
15560
|
+
if (!request) return false;
|
|
15561
|
+
const choice = await openStudioDecision({
|
|
15562
|
+
mode: "confirm",
|
|
15563
|
+
title: "Allow " + request.label + "?",
|
|
15564
|
+
message: "This local resource is outside the locations currently available to Studio.\n\n"
|
|
15565
|
+
+ "File on the computer running Pi:\n" + request.path + "\n\n"
|
|
15566
|
+
+ "Allow only this file, or allow its containing folder for this Studio session?",
|
|
15567
|
+
cancelLabel: "Cancel",
|
|
15568
|
+
secondaryLabel: "Allow this folder for this Studio session",
|
|
15569
|
+
secondaryValue: "directory",
|
|
15570
|
+
confirmLabel: "Allow this file",
|
|
15571
|
+
});
|
|
15572
|
+
const grantKind = choice === true ? "file" : choice === "directory" ? "directory" : "";
|
|
15573
|
+
if (!grantKind) {
|
|
15574
|
+
setStatus("Local resource access cancelled.", "warning");
|
|
15575
|
+
return false;
|
|
15576
|
+
}
|
|
15577
|
+
const grantPath = grantKind === "directory" ? request.directoryPath : request.path;
|
|
15578
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
15579
|
+
method: "POST",
|
|
15580
|
+
body: JSON.stringify({ grantKind, path: grantPath }),
|
|
15581
|
+
});
|
|
15582
|
+
fileBrowserState = { ...fileBrowserState, contextKey: "", loaded: false };
|
|
15583
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed local resource for this Studio session.", "success");
|
|
15584
|
+
return true;
|
|
15585
|
+
}
|
|
15586
|
+
|
|
15587
|
+
async function fetchPreviewLocalLink(action, href, contextOverride, options) {
|
|
15588
|
+
const request = () => {
|
|
15589
|
+
const query = { ...getPreviewLinkResourceQuery(href, contextOverride), action };
|
|
15590
|
+
if (isWatchedFilePreview) {
|
|
15591
|
+
query.watchedFile = "1";
|
|
15592
|
+
const watchedDocId = initialQueryParams.get("docId") || "";
|
|
15593
|
+
if (watchedDocId) query.docId = watchedDocId;
|
|
15594
|
+
}
|
|
15595
|
+
return fetchStudioJson("/local-preview-link", { query });
|
|
15596
|
+
};
|
|
15597
|
+
try {
|
|
15598
|
+
return await request();
|
|
15599
|
+
} catch (error) {
|
|
15600
|
+
const grantRequest = getStudioResourceGrantRequest(error);
|
|
15601
|
+
if (!grantRequest || (options && options.skipGrantPrompt === true)) throw error;
|
|
15602
|
+
if (!(await requestStudioResourceGrant(grantRequest))) {
|
|
15603
|
+
const cancelled = new Error("Local resource access cancelled.");
|
|
15604
|
+
cancelled.studioCancelled = true;
|
|
15605
|
+
throw cancelled;
|
|
15606
|
+
}
|
|
15607
|
+
return request();
|
|
15608
|
+
}
|
|
14829
15609
|
}
|
|
14830
15610
|
|
|
14831
15611
|
function getPreviewPdfViewerUrl(href, contextOverride) {
|
|
@@ -14836,16 +15616,16 @@
|
|
|
14836
15616
|
return resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
14837
15617
|
}
|
|
14838
15618
|
|
|
14839
|
-
function openPreviewPdfLink(href, title, contextOverride) {
|
|
15619
|
+
async function openPreviewPdfLink(href, title, contextOverride) {
|
|
15620
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14840
15621
|
const viewerUrl = getPreviewPdfViewerUrl(href, contextOverride);
|
|
14841
15622
|
if (!viewerUrl) {
|
|
14842
15623
|
setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
|
|
14843
15624
|
return false;
|
|
14844
15625
|
}
|
|
14845
|
-
const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
|
|
14846
15626
|
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
|
|
14847
15627
|
const resourceQuery = buildStudioPdfResourceQuery({
|
|
14848
|
-
path:
|
|
15628
|
+
path: stripPreviewLocalLinkUrlSuffix(href),
|
|
14849
15629
|
sourcePath: context.sourcePath || "",
|
|
14850
15630
|
resourceDir: context.resourceDir || "",
|
|
14851
15631
|
}, true);
|
|
@@ -14854,6 +15634,7 @@
|
|
|
14854
15634
|
}
|
|
14855
15635
|
|
|
14856
15636
|
async function openPreviewImageLink(href, title, contextOverride) {
|
|
15637
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14857
15638
|
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
14858
15639
|
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
14859
15640
|
});
|
|
@@ -14904,6 +15685,9 @@
|
|
|
14904
15685
|
}
|
|
14905
15686
|
|
|
14906
15687
|
async function openPreviewDocumentHere(href, contextOverride, options) {
|
|
15688
|
+
if (isWatchedFilePreview) {
|
|
15689
|
+
throw new Error("This preview follows one disk file and cannot open another document here. Open a new file tab instead.");
|
|
15690
|
+
}
|
|
14907
15691
|
if (!(await confirmPreviewOfficeConversion(href, "here"))) return;
|
|
14908
15692
|
if (editorHasPotentialUnsavedContent()) {
|
|
14909
15693
|
const kind = getPreviewLocalLinkKind(href);
|
|
@@ -14933,7 +15717,7 @@
|
|
|
14933
15717
|
setSourceState({ source: "blank", label, path: null });
|
|
14934
15718
|
} else {
|
|
14935
15719
|
setSourceState({ source: "file", label, path });
|
|
14936
|
-
markFileBackedBaseline(payload.text);
|
|
15720
|
+
markFileBackedBaseline(payload.text, payload.diskRevision);
|
|
14937
15721
|
}
|
|
14938
15722
|
const detected = converted ? "markdown" : detectLanguageFromName(path || label);
|
|
14939
15723
|
if (detected) setEditorLanguage(detected);
|
|
@@ -14955,7 +15739,30 @@
|
|
|
14955
15739
|
navigatePendingStudioTab(launch, relativeUrl);
|
|
14956
15740
|
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.");
|
|
14957
15741
|
} catch (error) {
|
|
14958
|
-
|
|
15742
|
+
if (error && error.studioCancelled) {
|
|
15743
|
+
cancelPendingStudioTab(launch, "Local resource access was cancelled.");
|
|
15744
|
+
} else {
|
|
15745
|
+
failPendingStudioTab(launch, "Studio could not prepare this document tab. Return to the originating Studio page for details.");
|
|
15746
|
+
}
|
|
15747
|
+
throw error;
|
|
15748
|
+
}
|
|
15749
|
+
}
|
|
15750
|
+
|
|
15751
|
+
async function openPreviewDocumentInWatchedPreview(href, contextOverride) {
|
|
15752
|
+
let launch = null;
|
|
15753
|
+
try {
|
|
15754
|
+
launch = openPendingStudioTab("preview");
|
|
15755
|
+
const payload = await fetchPreviewLocalLink("watch-url", href, contextOverride);
|
|
15756
|
+
const relativeUrl = payload && typeof payload.relativeUrl === "string" ? payload.relativeUrl : "";
|
|
15757
|
+
if (!relativeUrl) throw new Error("Studio did not return a watched-preview URL.");
|
|
15758
|
+
navigatePendingStudioTab(launch, relativeUrl);
|
|
15759
|
+
setStatus("Opening read-only preview that follows disk changes.");
|
|
15760
|
+
} catch (error) {
|
|
15761
|
+
if (error && error.studioCancelled) {
|
|
15762
|
+
cancelPendingStudioTab(launch, "Local resource access was cancelled.");
|
|
15763
|
+
} else {
|
|
15764
|
+
failPendingStudioTab(launch, "Studio could not prepare this watched preview. Return to the originating Studio page for details.");
|
|
15765
|
+
}
|
|
14959
15766
|
throw error;
|
|
14960
15767
|
}
|
|
14961
15768
|
}
|
|
@@ -14970,7 +15777,11 @@
|
|
|
14970
15777
|
navigatePendingStudioTab(launch, relativeUrl);
|
|
14971
15778
|
setStatus("Opening preview in a new Studio tab.");
|
|
14972
15779
|
} catch (error) {
|
|
14973
|
-
|
|
15780
|
+
if (error && error.studioCancelled) {
|
|
15781
|
+
cancelPendingStudioTab(launch, "Local resource access was cancelled.");
|
|
15782
|
+
} else {
|
|
15783
|
+
failPendingStudioTab(launch, "Studio could not prepare this preview tab. Return to the originating Studio page for details.");
|
|
15784
|
+
}
|
|
14974
15785
|
throw error;
|
|
14975
15786
|
}
|
|
14976
15787
|
}
|
|
@@ -14985,6 +15796,7 @@
|
|
|
14985
15796
|
}
|
|
14986
15797
|
|
|
14987
15798
|
async function revealPreviewLocalLink(href, contextOverride) {
|
|
15799
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14988
15800
|
const query = getPreviewLinkResourceQuery(href, contextOverride);
|
|
14989
15801
|
const payload = await fetchStudioJson("/reveal-local-resource", {
|
|
14990
15802
|
method: "POST",
|
|
@@ -14998,10 +15810,11 @@
|
|
|
14998
15810
|
if (!href) return;
|
|
14999
15811
|
try {
|
|
15000
15812
|
if (action === "open-pdf") {
|
|
15001
|
-
openPreviewPdfLink(href, context.title || href, context);
|
|
15813
|
+
await openPreviewPdfLink(href, context.title || href, context);
|
|
15002
15814
|
return;
|
|
15003
15815
|
}
|
|
15004
15816
|
if (action === "open-system") {
|
|
15817
|
+
await fetchPreviewLocalLink("resolve", href, context);
|
|
15005
15818
|
await runStudioPdfLocalAction("system-viewer", getPreviewLinkResourceQuery(href, context));
|
|
15006
15819
|
return;
|
|
15007
15820
|
}
|
|
@@ -15009,6 +15822,10 @@
|
|
|
15009
15822
|
await openPreviewDocumentInNewEditor(href, context);
|
|
15010
15823
|
return;
|
|
15011
15824
|
}
|
|
15825
|
+
if (action === "watch-new") {
|
|
15826
|
+
await openPreviewDocumentInWatchedPreview(href, context);
|
|
15827
|
+
return;
|
|
15828
|
+
}
|
|
15012
15829
|
if (action === "open-preview-new") {
|
|
15013
15830
|
await openPreviewResourceInNewEditor(href, context);
|
|
15014
15831
|
return;
|
|
@@ -15043,7 +15860,9 @@
|
|
|
15043
15860
|
closePreviewLinkMenu();
|
|
15044
15861
|
const title = String(anchor.textContent || href).trim() || href;
|
|
15045
15862
|
if (kind === "pdf") {
|
|
15046
|
-
openPreviewPdfLink(href, title)
|
|
15863
|
+
void openPreviewPdfLink(href, title).catch((error) => {
|
|
15864
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked PDF."), "warning");
|
|
15865
|
+
});
|
|
15047
15866
|
return;
|
|
15048
15867
|
}
|
|
15049
15868
|
if (kind === "image") {
|
|
@@ -15053,9 +15872,7 @@
|
|
|
15053
15872
|
return;
|
|
15054
15873
|
}
|
|
15055
15874
|
if (kind === "text" || kind === "office") {
|
|
15056
|
-
void
|
|
15057
|
-
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
15058
|
-
});
|
|
15875
|
+
void showPreviewLinkMenu(anchor, event);
|
|
15059
15876
|
return;
|
|
15060
15877
|
}
|
|
15061
15878
|
setStatus("Right-click this local link for file actions.", "warning");
|
|
@@ -15066,7 +15883,7 @@
|
|
|
15066
15883
|
if (!anchor) return;
|
|
15067
15884
|
event.preventDefault();
|
|
15068
15885
|
event.stopPropagation();
|
|
15069
|
-
showPreviewLinkMenu(anchor, event);
|
|
15886
|
+
void showPreviewLinkMenu(anchor, event);
|
|
15070
15887
|
}
|
|
15071
15888
|
|
|
15072
15889
|
function makeRequestId() {
|
|
@@ -15245,6 +16062,7 @@
|
|
|
15245
16062
|
const html = await renderQuizMarkdownToHtml(markdown);
|
|
15246
16063
|
if (nonce !== quizPreviewRenderNonce || !quizDialogEl || !quizDialogEl.contains(target)) return;
|
|
15247
16064
|
target.innerHTML = html;
|
|
16065
|
+
await hydrateStudioPreviewLocalMedia(target, getHtmlPreviewResourceContextOptions());
|
|
15248
16066
|
await renderAnnotationMathInElement(target);
|
|
15249
16067
|
decoratePdfEmbeds(target);
|
|
15250
16068
|
await renderPdfPreviewsInElement(target);
|
|
@@ -16369,6 +17187,8 @@
|
|
|
16369
17187
|
ensurePreviewSelectionActions(targetEl);
|
|
16370
17188
|
updatePreviewCommentBlocksForElement(targetEl);
|
|
16371
17189
|
decorateCopyablePreviewBlocks(targetEl);
|
|
17190
|
+
clearWatchedPreviewRenderError();
|
|
17191
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, text);
|
|
16372
17192
|
if (pane === "response") {
|
|
16373
17193
|
applyPendingResponseScrollReset();
|
|
16374
17194
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -21242,7 +22062,11 @@
|
|
|
21242
22062
|
} catch {}
|
|
21243
22063
|
}
|
|
21244
22064
|
|
|
21245
|
-
function setEditorLanguage(lang) {
|
|
22065
|
+
function setEditorLanguage(lang, options) {
|
|
22066
|
+
if (isWatchedFilePreview && !(options && options.allowWatchedFileUpdate === true)) {
|
|
22067
|
+
setStatus("The watched preview language follows its file path.", "warning");
|
|
22068
|
+
return false;
|
|
22069
|
+
}
|
|
21246
22070
|
editorLanguage = (lang && SUPPORTED_LANGUAGES.indexOf(lang) !== -1) ? lang : "markdown";
|
|
21247
22071
|
persistEditorLanguage(editorLanguage);
|
|
21248
22072
|
syncHighlightSelectUi();
|
|
@@ -21257,6 +22081,7 @@
|
|
|
21257
22081
|
}
|
|
21258
22082
|
updateOutlineUi();
|
|
21259
22083
|
scheduleWorkspacePersistence();
|
|
22084
|
+
return true;
|
|
21260
22085
|
}
|
|
21261
22086
|
|
|
21262
22087
|
function setEditorHighlightMode(mode) {
|
|
@@ -21558,11 +22383,90 @@
|
|
|
21558
22383
|
return true;
|
|
21559
22384
|
}
|
|
21560
22385
|
|
|
22386
|
+
function handleWatchedFileUpdate(message) {
|
|
22387
|
+
if (!isWatchedFilePreview || !message || typeof message.text !== "string") return;
|
|
22388
|
+
const messagePath = String(message.path || "");
|
|
22389
|
+
if (!watchedFilePreviewState.path || messagePath !== watchedFilePreviewState.path) return;
|
|
22390
|
+
const generation = Math.max(0, Number(message.generation) || 0);
|
|
22391
|
+
if (generation < watchedFilePreviewState.generation) return;
|
|
22392
|
+
|
|
22393
|
+
const expectedPreviewText = prepareEditorTextForPreview(message.text);
|
|
22394
|
+
watchedFilePreviewReadingPositions.source = {
|
|
22395
|
+
snapshot: captureWatchedPreviewReadingPosition(sourcePreviewEl),
|
|
22396
|
+
text: expectedPreviewText,
|
|
22397
|
+
};
|
|
22398
|
+
watchedFilePreviewReadingPositions.response = {
|
|
22399
|
+
snapshot: captureWatchedPreviewReadingPosition(critiqueViewEl),
|
|
22400
|
+
text: expectedPreviewText,
|
|
22401
|
+
};
|
|
22402
|
+
const textareaMaxScroll = Math.max(0, Number(sourceTextEl.scrollHeight || 0) - Number(sourceTextEl.clientHeight || 0));
|
|
22403
|
+
const textareaScrollRatio = textareaMaxScroll > 0 ? Number(sourceTextEl.scrollTop || 0) / textareaMaxScroll : 0;
|
|
22404
|
+
const selectionStart = Math.max(0, Number(sourceTextEl.selectionStart) || 0);
|
|
22405
|
+
const selectionEnd = Math.max(selectionStart, Number(sourceTextEl.selectionEnd) || selectionStart);
|
|
22406
|
+
|
|
22407
|
+
sourceTextEl.value = message.text;
|
|
22408
|
+
const nextMaxScroll = Math.max(0, Number(sourceTextEl.scrollHeight || 0) - Number(sourceTextEl.clientHeight || 0));
|
|
22409
|
+
sourceTextEl.scrollTop = Math.max(0, Math.min(nextMaxScroll, nextMaxScroll * textareaScrollRatio));
|
|
22410
|
+
try {
|
|
22411
|
+
sourceTextEl.setSelectionRange(
|
|
22412
|
+
Math.min(selectionStart, message.text.length),
|
|
22413
|
+
Math.min(selectionEnd, message.text.length),
|
|
22414
|
+
);
|
|
22415
|
+
} catch {
|
|
22416
|
+
// Selection APIs are not guaranteed in every embedded browser.
|
|
22417
|
+
}
|
|
22418
|
+
|
|
22419
|
+
watchedFilePreviewState.generation = generation;
|
|
22420
|
+
watchedFilePreviewState.diskRevision = normalizeStudioDiskRevision(message.diskRevision) || watchedFilePreviewState.diskRevision;
|
|
22421
|
+
watchedFilePreviewState.lastError = "";
|
|
22422
|
+
fileBackedBaselineText = message.text;
|
|
22423
|
+
fileBackedDiskRevision = watchedFilePreviewState.diskRevision || null;
|
|
22424
|
+
editorLanguage = detectLanguageFromName(watchedFilePreviewState.path) || editorLanguage;
|
|
22425
|
+
syncHighlightSelectUi();
|
|
22426
|
+
scheduleEditorHighlightRender();
|
|
22427
|
+
renderSourcePreview({ previewDelayMs: 0 });
|
|
22428
|
+
renderActiveResult();
|
|
22429
|
+
updateSourceBadge();
|
|
22430
|
+
setStatus(message.message || "Watched preview updated from disk.", "success");
|
|
22431
|
+
}
|
|
22432
|
+
|
|
22433
|
+
function handleWatchedFileError(message) {
|
|
22434
|
+
if (!isWatchedFilePreview || !message) return;
|
|
22435
|
+
const messagePath = String(message.path || "");
|
|
22436
|
+
if (watchedFilePreviewState.path && messagePath && messagePath !== watchedFilePreviewState.path) return;
|
|
22437
|
+
watchedFilePreviewState.lastError = String(message.message || "Could not refresh the watched file.");
|
|
22438
|
+
updateSourceBadge();
|
|
22439
|
+
setStatus(watchedFilePreviewState.lastError, "warning");
|
|
22440
|
+
}
|
|
22441
|
+
|
|
22442
|
+
function handleWatchedFileReady(message) {
|
|
22443
|
+
if (!isWatchedFilePreview || !message) return;
|
|
22444
|
+
const messagePath = String(message.path || "");
|
|
22445
|
+
if (watchedFilePreviewState.path && messagePath && messagePath !== watchedFilePreviewState.path) return;
|
|
22446
|
+
watchedFilePreviewState.diskRevision = normalizeStudioDiskRevision(message.diskRevision) || watchedFilePreviewState.diskRevision;
|
|
22447
|
+
watchedFilePreviewState.lastError = "";
|
|
22448
|
+
updateSourceBadge();
|
|
22449
|
+
setStatus(message.message || "Watching file for disk changes.", "success");
|
|
22450
|
+
}
|
|
22451
|
+
|
|
21561
22452
|
function handleServerMessage(message) {
|
|
21562
22453
|
if (!message || typeof message !== "object") return;
|
|
21563
22454
|
|
|
21564
22455
|
debugTrace("server_message", summarizeServerMessage(message));
|
|
21565
22456
|
|
|
22457
|
+
if (message.type === "watched_file_update") {
|
|
22458
|
+
handleWatchedFileUpdate(message);
|
|
22459
|
+
return;
|
|
22460
|
+
}
|
|
22461
|
+
if (message.type === "watched_file_error") {
|
|
22462
|
+
handleWatchedFileError(message);
|
|
22463
|
+
return;
|
|
22464
|
+
}
|
|
22465
|
+
if (message.type === "watched_file_ready") {
|
|
22466
|
+
handleWatchedFileReady(message);
|
|
22467
|
+
return;
|
|
22468
|
+
}
|
|
22469
|
+
|
|
21566
22470
|
const contextChanged = applyContextUsageFromMessage(message);
|
|
21567
22471
|
if (contextChanged) {
|
|
21568
22472
|
updateFooterMeta();
|
|
@@ -21799,7 +22703,11 @@
|
|
|
21799
22703
|
message.initialDocument &&
|
|
21800
22704
|
typeof message.initialDocument.text === "string"
|
|
21801
22705
|
) {
|
|
21802
|
-
setEditorText(message.initialDocument.text, {
|
|
22706
|
+
setEditorText(message.initialDocument.text, {
|
|
22707
|
+
preserveScroll: false,
|
|
22708
|
+
preserveSelection: false,
|
|
22709
|
+
allowWatchedFileUpdate: isWatchedFilePreview,
|
|
22710
|
+
});
|
|
21803
22711
|
initialDocumentApplied = true;
|
|
21804
22712
|
loadedInitialDocument = true;
|
|
21805
22713
|
setSourceState({
|
|
@@ -21811,7 +22719,7 @@
|
|
|
21811
22719
|
: (initialSourceState.draftId || null),
|
|
21812
22720
|
});
|
|
21813
22721
|
if (message.initialDocument.path) {
|
|
21814
|
-
markFileBackedBaseline(message.initialDocument.text);
|
|
22722
|
+
markFileBackedBaseline(message.initialDocument.text, message.initialDocument.diskRevision);
|
|
21815
22723
|
}
|
|
21816
22724
|
refreshResponseUi();
|
|
21817
22725
|
if (typeof message.initialDocument.label === "string" && message.initialDocument.label.length > 0) {
|
|
@@ -22172,7 +23080,19 @@
|
|
|
22172
23080
|
return;
|
|
22173
23081
|
}
|
|
22174
23082
|
|
|
23083
|
+
if (message.type === "save_conflict") {
|
|
23084
|
+
void handleEditorSaveConflict(message);
|
|
23085
|
+
return;
|
|
23086
|
+
}
|
|
23087
|
+
|
|
23088
|
+
if (message.type === "save_as_conflict") {
|
|
23089
|
+
void handleEditorSaveAsConflict(message);
|
|
23090
|
+
return;
|
|
23091
|
+
}
|
|
23092
|
+
|
|
22175
23093
|
if (message.type === "saved") {
|
|
23094
|
+
const savedOperation = typeof message.requestId === "string" ? pendingSaveOperations.get(message.requestId) : null;
|
|
23095
|
+
if (typeof message.requestId === "string") pendingSaveOperations.delete(message.requestId);
|
|
22176
23096
|
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
22177
23097
|
pendingRequestId = null;
|
|
22178
23098
|
pendingKind = null;
|
|
@@ -22191,7 +23111,7 @@
|
|
|
22191
23111
|
}, {
|
|
22192
23112
|
carryCurrentMetadataToNewDocument: true,
|
|
22193
23113
|
});
|
|
22194
|
-
markFileBackedBaseline(sourceTextEl.value);
|
|
23114
|
+
markFileBackedBaseline(savedOperation && typeof savedOperation.content === "string" ? savedOperation.content : sourceTextEl.value, message.diskRevision);
|
|
22195
23115
|
}
|
|
22196
23116
|
setBusy(false);
|
|
22197
23117
|
setWsState("Ready");
|
|
@@ -22211,6 +23131,10 @@
|
|
|
22211
23131
|
}
|
|
22212
23132
|
|
|
22213
23133
|
if (message.type === "editor_snapshot") {
|
|
23134
|
+
if (isWatchedFilePreview) {
|
|
23135
|
+
setStatus("Ignored editor snapshot because this preview follows its watched file on disk.", "warning");
|
|
23136
|
+
return;
|
|
23137
|
+
}
|
|
22214
23138
|
if (typeof message.requestId === "string" && pendingRequestId && message.requestId !== pendingRequestId) {
|
|
22215
23139
|
return;
|
|
22216
23140
|
}
|
|
@@ -22234,6 +23158,10 @@
|
|
|
22234
23158
|
}
|
|
22235
23159
|
|
|
22236
23160
|
if (message.type === "studio_document") {
|
|
23161
|
+
if (isWatchedFilePreview) {
|
|
23162
|
+
setStatus("Ignored document replacement because this preview follows its watched file on disk.", "warning");
|
|
23163
|
+
return;
|
|
23164
|
+
}
|
|
22237
23165
|
const nextDoc = message.document;
|
|
22238
23166
|
if (!nextDoc || typeof nextDoc !== "object" || typeof nextDoc.text !== "string") {
|
|
22239
23167
|
return;
|
|
@@ -22271,7 +23199,7 @@
|
|
|
22271
23199
|
draftId: typeof nextDoc.draftId === "string" && nextDoc.draftId.trim() ? nextDoc.draftId.trim() : null,
|
|
22272
23200
|
});
|
|
22273
23201
|
if (nextPath) {
|
|
22274
|
-
markFileBackedBaseline(nextDoc.text);
|
|
23202
|
+
markFileBackedBaseline(nextDoc.text, nextDoc.diskRevision);
|
|
22275
23203
|
}
|
|
22276
23204
|
refreshResponseUi();
|
|
22277
23205
|
setStatus(
|
|
@@ -22414,6 +23342,7 @@
|
|
|
22414
23342
|
|
|
22415
23343
|
if (message.type === "busy") {
|
|
22416
23344
|
if (typeof message.requestId === "string") {
|
|
23345
|
+
pendingSaveOperations.delete(message.requestId);
|
|
22417
23346
|
failPendingCompanionLaunch(message.requestId, "Studio could not start the companion editor because another request was busy.");
|
|
22418
23347
|
}
|
|
22419
23348
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
@@ -22435,6 +23364,7 @@
|
|
|
22435
23364
|
|
|
22436
23365
|
if (message.type === "error") {
|
|
22437
23366
|
if (typeof message.requestId === "string") {
|
|
23367
|
+
pendingSaveOperations.delete(message.requestId);
|
|
22438
23368
|
failPendingCompanionLaunch(message.requestId, "Studio could not prepare the companion editor. Return to the originating Studio page for details.");
|
|
22439
23369
|
}
|
|
22440
23370
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
@@ -22541,6 +23471,11 @@
|
|
|
22541
23471
|
if (studioMode !== "full") {
|
|
22542
23472
|
wsParams.set("mode", studioMode);
|
|
22543
23473
|
}
|
|
23474
|
+
if (isWatchedFilePreview && watchedFilePreviewState.path) {
|
|
23475
|
+
wsParams.set("watchPath", watchedFilePreviewState.path);
|
|
23476
|
+
const watchedDocId = initialQueryParams.get("docId") || "";
|
|
23477
|
+
if (watchedDocId) wsParams.set("docId", watchedDocId);
|
|
23478
|
+
}
|
|
22544
23479
|
if (DEBUG_ENABLED) {
|
|
22545
23480
|
wsParams.set("debug", "1");
|
|
22546
23481
|
}
|
|
@@ -22591,6 +23526,14 @@
|
|
|
22591
23526
|
return;
|
|
22592
23527
|
}
|
|
22593
23528
|
|
|
23529
|
+
if (kind === "watch_unauthorized") {
|
|
23530
|
+
clearScheduledReconnect();
|
|
23531
|
+
reconnectAttempt = 0;
|
|
23532
|
+
setWsState("Disconnected");
|
|
23533
|
+
setStatus("Watched preview authorization is unavailable. Reload this tab or open a new watched preview from Studio.", "warning");
|
|
23534
|
+
return;
|
|
23535
|
+
}
|
|
23536
|
+
|
|
22594
23537
|
if (kind === "shutdown") {
|
|
22595
23538
|
clearScheduledReconnect();
|
|
22596
23539
|
reconnectAttempt = 0;
|
|
@@ -22606,14 +23549,28 @@
|
|
|
22606
23549
|
};
|
|
22607
23550
|
|
|
22608
23551
|
socket.addEventListener("open", () => {
|
|
23552
|
+
if (ws !== socket) {
|
|
23553
|
+
try { socket.close(); } catch {}
|
|
23554
|
+
return;
|
|
23555
|
+
}
|
|
22609
23556
|
window.clearTimeout(connectWatchdog);
|
|
22610
23557
|
setWsState("Ready");
|
|
22611
23558
|
setStatus(wasReconnect ? "Reconnected. Syncing…" : "Connected. Syncing…");
|
|
22612
23559
|
sendMessage({ type: "hello" });
|
|
23560
|
+
if (isWatchedFilePreview && watchedFilePreviewState.path) {
|
|
23561
|
+
watchedFilePreviewState.generation = 0;
|
|
23562
|
+
sendMessage({
|
|
23563
|
+
type: "watch_file_subscribe",
|
|
23564
|
+
path: watchedFilePreviewState.path,
|
|
23565
|
+
revision: watchedFilePreviewState.diskRevision || undefined,
|
|
23566
|
+
});
|
|
23567
|
+
setStatus(wasReconnect ? "Reconnected. Resuming watched preview…" : "Connected. Starting watched preview…");
|
|
23568
|
+
}
|
|
22613
23569
|
reconnectAttempt = 0;
|
|
22614
23570
|
});
|
|
22615
23571
|
|
|
22616
23572
|
socket.addEventListener("message", (event) => {
|
|
23573
|
+
if (ws !== socket) return;
|
|
22617
23574
|
try {
|
|
22618
23575
|
const message = JSON.parse(event.data);
|
|
22619
23576
|
handleServerMessage(message);
|
|
@@ -22624,6 +23581,7 @@
|
|
|
22624
23581
|
});
|
|
22625
23582
|
|
|
22626
23583
|
socket.addEventListener("close", (event) => {
|
|
23584
|
+
if (ws !== socket && !disconnectHandled) return;
|
|
22627
23585
|
if (event && event.code === 4001) {
|
|
22628
23586
|
handleDisconnect("invalidated", 4001);
|
|
22629
23587
|
return;
|
|
@@ -22632,6 +23590,10 @@
|
|
|
22632
23590
|
handleDisconnect("full_conflict", 4004);
|
|
22633
23591
|
return;
|
|
22634
23592
|
}
|
|
23593
|
+
if (event && event.code === 4003) {
|
|
23594
|
+
handleDisconnect("watch_unauthorized", 4003);
|
|
23595
|
+
return;
|
|
23596
|
+
}
|
|
22635
23597
|
if (event && event.code === 1001) {
|
|
22636
23598
|
handleDisconnect("shutdown", 1001);
|
|
22637
23599
|
return;
|
|
@@ -22641,6 +23603,7 @@
|
|
|
22641
23603
|
});
|
|
22642
23604
|
|
|
22643
23605
|
socket.addEventListener("error", () => {
|
|
23606
|
+
if (ws !== socket) return;
|
|
22644
23607
|
handleDisconnect("error");
|
|
22645
23608
|
});
|
|
22646
23609
|
}
|
|
@@ -22869,6 +23832,25 @@
|
|
|
22869
23832
|
});
|
|
22870
23833
|
}
|
|
22871
23834
|
|
|
23835
|
+
if (watchedOpenEditableBtn) {
|
|
23836
|
+
watchedOpenEditableBtn.addEventListener("click", () => {
|
|
23837
|
+
const path = watchedFilePreviewState.path;
|
|
23838
|
+
if (!path) {
|
|
23839
|
+
setStatus("This watched preview no longer has a file path.", "warning");
|
|
23840
|
+
return;
|
|
23841
|
+
}
|
|
23842
|
+
try {
|
|
23843
|
+
openFileBackedStudioEditorTab(path, {
|
|
23844
|
+
label: sourceState && sourceState.label ? sourceState.label : basenameForStudioPath(path),
|
|
23845
|
+
resourceDir: getCurrentResourceDirValue() || dirnameForDisplayPath(path),
|
|
23846
|
+
});
|
|
23847
|
+
setStatus("Opening watched file in a separate editable tab.");
|
|
23848
|
+
} catch (error) {
|
|
23849
|
+
setStatus(error && error.message ? error.message : String(error || "Could not open editable tab."), "warning");
|
|
23850
|
+
}
|
|
23851
|
+
});
|
|
23852
|
+
}
|
|
23853
|
+
|
|
22872
23854
|
updatePaneFocusButtons();
|
|
22873
23855
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
22874
23856
|
window.addEventListener("pagehide", () => {
|
|
@@ -23390,100 +24372,249 @@
|
|
|
23390
24372
|
setFooterThemeMenuOpen(false);
|
|
23391
24373
|
});
|
|
23392
24374
|
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
if (
|
|
23396
|
-
|
|
23397
|
-
|
|
24375
|
+
function abandonPendingSaveRequest(requestId) {
|
|
24376
|
+
pendingSaveOperations.delete(requestId);
|
|
24377
|
+
if (requestId) clearArmedTitleAttention(requestId);
|
|
24378
|
+
if (pendingRequestId === requestId) {
|
|
24379
|
+
pendingRequestId = null;
|
|
24380
|
+
pendingKind = null;
|
|
23398
24381
|
}
|
|
24382
|
+
stickyStudioKind = null;
|
|
24383
|
+
setBusy(false);
|
|
24384
|
+
setWsState("Ready");
|
|
24385
|
+
}
|
|
23399
24386
|
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
24387
|
+
function sendEditorSaveAsRequest(path, content, overwrite, expectedRevision) {
|
|
24388
|
+
const cleanPath = String(path || "").trim();
|
|
24389
|
+
if (!cleanPath) {
|
|
24390
|
+
setStatus("Save cancelled: path is required.", "warning");
|
|
24391
|
+
return false;
|
|
24392
|
+
}
|
|
24393
|
+
const requestId = beginUiAction("save_as");
|
|
24394
|
+
if (!requestId) return false;
|
|
24395
|
+
const operation = {
|
|
24396
|
+
kind: "save_as",
|
|
24397
|
+
path: cleanPath,
|
|
24398
|
+
content: String(content ?? ""),
|
|
24399
|
+
overwrite: overwrite === true,
|
|
24400
|
+
expectedRevision: normalizeStudioDiskRevision(expectedRevision),
|
|
24401
|
+
};
|
|
24402
|
+
pendingSaveOperations.set(requestId, operation);
|
|
24403
|
+
if (!sendMessage({
|
|
24404
|
+
type: "save_as_request",
|
|
24405
|
+
requestId,
|
|
24406
|
+
path: operation.path,
|
|
24407
|
+
content: operation.content,
|
|
24408
|
+
overwrite: operation.overwrite,
|
|
24409
|
+
expectedRevision: operation.expectedRevision || undefined,
|
|
24410
|
+
})) {
|
|
24411
|
+
abandonPendingSaveRequest(requestId);
|
|
24412
|
+
return false;
|
|
24413
|
+
}
|
|
24414
|
+
return true;
|
|
24415
|
+
}
|
|
24416
|
+
|
|
24417
|
+
async function openEditorSaveAsDialog(options) {
|
|
24418
|
+
if (uiBusy) return false;
|
|
24419
|
+
const settings = options && typeof options === "object" ? options : {};
|
|
24420
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
24421
|
+
const currentPath = getEffectiveSavePath();
|
|
24422
|
+
const label = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
|
|
24423
|
+
const suggested = typeof settings.suggestedPath === "string" && settings.suggestedPath.trim()
|
|
24424
|
+
? settings.suggestedPath.trim()
|
|
24425
|
+
: (currentPath || (resourceDir ? resourceDir.replace(/\/$/, "") + "/" + label : "./draft.md"));
|
|
23403
24426
|
const path = await requestStudioTextInput("Save editor content as:", suggested, {
|
|
23404
24427
|
title: "Save editor as",
|
|
23405
24428
|
confirmLabel: "Save",
|
|
24429
|
+
inputLabel: "File path",
|
|
23406
24430
|
});
|
|
23407
|
-
if (
|
|
23408
|
-
|
|
23409
|
-
|
|
23410
|
-
|
|
24431
|
+
if (path === null) {
|
|
24432
|
+
if (settings.reportCancellation === true) setStatus("Save As cancelled; editor changes were kept.", "warning");
|
|
24433
|
+
return false;
|
|
24434
|
+
}
|
|
24435
|
+
const content = Object.prototype.hasOwnProperty.call(settings, "content")
|
|
24436
|
+
? String(settings.content ?? "")
|
|
24437
|
+
: sourceTextEl.value;
|
|
24438
|
+
return sendEditorSaveAsRequest(path, content, false);
|
|
24439
|
+
}
|
|
23411
24440
|
|
|
23412
|
-
|
|
23413
|
-
|
|
23414
|
-
|
|
24441
|
+
function sendEditorSaveOverRequest(options) {
|
|
24442
|
+
const settings = options && typeof options === "object" ? options : {};
|
|
24443
|
+
const path = typeof settings.path === "string" && settings.path.trim()
|
|
24444
|
+
? settings.path.trim()
|
|
24445
|
+
: (sourceState && sourceState.path ? sourceState.path : "");
|
|
24446
|
+
if (!path) {
|
|
24447
|
+
setStatus("Save editor requires a file-backed document. Use Save editor as… for a new file.", "warning");
|
|
24448
|
+
return false;
|
|
24449
|
+
}
|
|
24450
|
+
const requestId = beginUiAction("save_over");
|
|
24451
|
+
if (!requestId) return false;
|
|
24452
|
+
const operation = {
|
|
24453
|
+
kind: "save_over",
|
|
23415
24454
|
path,
|
|
23416
|
-
content,
|
|
23417
|
-
|
|
24455
|
+
content: Object.prototype.hasOwnProperty.call(settings, "content")
|
|
24456
|
+
? String(settings.content ?? "")
|
|
24457
|
+
: sourceTextEl.value,
|
|
24458
|
+
expectedRevision: Object.prototype.hasOwnProperty.call(settings, "expectedRevision")
|
|
24459
|
+
? normalizeStudioDiskRevision(settings.expectedRevision)
|
|
24460
|
+
: fileBackedDiskRevision,
|
|
24461
|
+
force: settings.force === true,
|
|
24462
|
+
};
|
|
24463
|
+
pendingSaveOperations.set(requestId, operation);
|
|
24464
|
+
if (!sendMessage({
|
|
24465
|
+
type: "save_over_request",
|
|
24466
|
+
requestId,
|
|
24467
|
+
path: operation.path,
|
|
24468
|
+
content: operation.content,
|
|
24469
|
+
expectedRevision: operation.expectedRevision || undefined,
|
|
24470
|
+
force: operation.force,
|
|
24471
|
+
})) {
|
|
24472
|
+
abandonPendingSaveRequest(requestId);
|
|
24473
|
+
return false;
|
|
24474
|
+
}
|
|
24475
|
+
return true;
|
|
24476
|
+
}
|
|
23418
24477
|
|
|
23419
|
-
|
|
24478
|
+
async function requestEditorRefreshFromDisk(options) {
|
|
24479
|
+
const settings = options && typeof options === "object" ? options : {};
|
|
24480
|
+
if (uiBusy) return false;
|
|
24481
|
+
const path = typeof settings.path === "string" && settings.path.trim()
|
|
24482
|
+
? settings.path.trim()
|
|
24483
|
+
: (sourceState && sourceState.path ? sourceState.path : "");
|
|
24484
|
+
if (!path) {
|
|
24485
|
+
setStatus("Refresh from disk requires a file-backed editor. Open one from Files or use /studio-editor-only <path>.", "warning");
|
|
24486
|
+
return false;
|
|
24487
|
+
}
|
|
24488
|
+
if (settings.skipConfirm !== true && editorDiffersFromFileBackedBaseline()) {
|
|
24489
|
+
const confirmed = await requestStudioConfirmation(
|
|
24490
|
+
"Replace the current editor contents with the latest version from disk? Unsaved editor changes will be lost.\n\n" + path,
|
|
24491
|
+
{ title: "Refresh from disk?", confirmLabel: "Replace", destructive: true },
|
|
24492
|
+
);
|
|
24493
|
+
if (!confirmed) return false;
|
|
24494
|
+
}
|
|
24495
|
+
const requestId = beginUiAction("refresh_from_disk");
|
|
24496
|
+
if (!requestId) return false;
|
|
24497
|
+
if (!sendMessage({ type: "refresh_from_disk_request", requestId, path })) {
|
|
23420
24498
|
pendingRequestId = null;
|
|
23421
24499
|
pendingKind = null;
|
|
24500
|
+
stickyStudioKind = null;
|
|
23422
24501
|
setBusy(false);
|
|
24502
|
+
setWsState("Ready");
|
|
24503
|
+
return false;
|
|
23423
24504
|
}
|
|
23424
|
-
|
|
23425
|
-
|
|
23426
|
-
saveOverBtn.addEventListener("click", async () => {
|
|
23427
|
-
var effectivePath = getEffectiveSavePath();
|
|
23428
|
-
if (!effectivePath) {
|
|
23429
|
-
setStatus("Save editor requires a file path. Open via /studio <path>, set a working dir, or use Save editor as…", "warning");
|
|
23430
|
-
return;
|
|
23431
|
-
}
|
|
24505
|
+
return true;
|
|
24506
|
+
}
|
|
23432
24507
|
|
|
23433
|
-
|
|
23434
|
-
|
|
24508
|
+
async function handleEditorSaveConflict(message) {
|
|
24509
|
+
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
24510
|
+
const operation = pendingSaveOperations.get(requestId) || {
|
|
24511
|
+
kind: "save_over",
|
|
24512
|
+
path: typeof message.path === "string" ? message.path : (sourceState.path || ""),
|
|
24513
|
+
content: sourceTextEl.value,
|
|
24514
|
+
expectedRevision: fileBackedDiskRevision,
|
|
24515
|
+
force: false,
|
|
24516
|
+
};
|
|
24517
|
+
abandonPendingSaveRequest(requestId);
|
|
24518
|
+
const conflictPath = typeof message.path === "string" && message.path.trim() ? message.path.trim() : operation.path;
|
|
24519
|
+
const canOverwrite = message.canOverwrite !== false;
|
|
24520
|
+
const detail = (typeof message.message === "string" && message.message.trim()
|
|
24521
|
+
? message.message.trim()
|
|
24522
|
+
: "The file changed on disk after Studio loaded it.")
|
|
24523
|
+
+ "\n\n" + conflictPath
|
|
24524
|
+
+ "\n\nReload replaces the editor with disk content. Save As keeps both versions."
|
|
24525
|
+
+ (canOverwrite ? " Overwrite replaces the reported disk revision with the current editor text." : " Overwrite is unavailable for this file location; use Save As to preserve link and path safety.");
|
|
24526
|
+
setStatus("Save paused because the file changed on disk.", "warning");
|
|
24527
|
+
const decision = await openStudioDecision({
|
|
24528
|
+
mode: "confirm",
|
|
24529
|
+
title: "File changed on disk",
|
|
24530
|
+
message: detail,
|
|
24531
|
+
cancelLabel: "Cancel",
|
|
24532
|
+
tertiaryLabel: "Reload",
|
|
24533
|
+
tertiaryValue: "reload",
|
|
24534
|
+
secondaryLabel: "Save As…",
|
|
24535
|
+
secondaryValue: "save-as",
|
|
23435
24536
|
confirmLabel: "Overwrite",
|
|
24537
|
+
confirmDisabled: !canOverwrite,
|
|
23436
24538
|
destructive: true,
|
|
23437
24539
|
});
|
|
23438
|
-
if (
|
|
23439
|
-
|
|
23440
|
-
|
|
23441
|
-
|
|
24540
|
+
if (decision === "reload") {
|
|
24541
|
+
await requestEditorRefreshFromDisk({ path: conflictPath, skipConfirm: true });
|
|
24542
|
+
return;
|
|
24543
|
+
}
|
|
24544
|
+
if (decision === "save-as") {
|
|
24545
|
+
await openEditorSaveAsDialog({ content: operation.content, suggestedPath: conflictPath, reportCancellation: true });
|
|
24546
|
+
return;
|
|
24547
|
+
}
|
|
24548
|
+
if (decision === true && canOverwrite) {
|
|
24549
|
+
sendEditorSaveOverRequest({
|
|
24550
|
+
path: conflictPath,
|
|
24551
|
+
content: operation.content,
|
|
24552
|
+
expectedRevision: message.currentRevision,
|
|
24553
|
+
force: true,
|
|
24554
|
+
});
|
|
24555
|
+
return;
|
|
24556
|
+
}
|
|
24557
|
+
setStatus("Save cancelled; editor changes were kept.", "warning");
|
|
24558
|
+
}
|
|
23442
24559
|
|
|
23443
|
-
|
|
23444
|
-
const
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
path:
|
|
24560
|
+
async function handleEditorSaveAsConflict(message) {
|
|
24561
|
+
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
24562
|
+
const operation = pendingSaveOperations.get(requestId) || {
|
|
24563
|
+
kind: "save_as",
|
|
24564
|
+
path: typeof message.path === "string" ? message.path : "",
|
|
23448
24565
|
content: sourceTextEl.value,
|
|
24566
|
+
overwrite: false,
|
|
24567
|
+
expectedRevision: null,
|
|
24568
|
+
};
|
|
24569
|
+
abandonPendingSaveRequest(requestId);
|
|
24570
|
+
const conflictPath = typeof message.path === "string" && message.path.trim() ? message.path.trim() : operation.path;
|
|
24571
|
+
const targetExists = Boolean(normalizeStudioDiskRevision(message.currentRevision));
|
|
24572
|
+
const unsafeReplacement = message.reason === "location-changed" || message.reason === "hard-linked-file";
|
|
24573
|
+
const canCommitHere = !unsafeReplacement;
|
|
24574
|
+
setStatus(unsafeReplacement
|
|
24575
|
+
? "Save As cannot replace this target safely; choose another path."
|
|
24576
|
+
: (targetExists
|
|
24577
|
+
? "Save As paused because the target already exists."
|
|
24578
|
+
: "Save As paused because the target changed while confirmation was open."), "warning");
|
|
24579
|
+
const decision = await openStudioDecision({
|
|
24580
|
+
mode: "confirm",
|
|
24581
|
+
title: unsafeReplacement ? "Cannot replace existing file" : (targetExists ? "Replace existing file?" : "Create file at this path?"),
|
|
24582
|
+
message: (typeof message.message === "string" && message.message.trim()
|
|
24583
|
+
? message.message.trim()
|
|
24584
|
+
: (targetExists ? "A file already exists at this location." : "The previous replacement target is no longer present."))
|
|
24585
|
+
+ (unsafeReplacement
|
|
24586
|
+
? "\n\nStudio will not replace a symlink, moved path, or hard-linked file. Choose another location instead."
|
|
24587
|
+
: (targetExists ? "\n\nReplacing it cannot be undone." : "\n\nCreating it will keep the current editor text at this path.")),
|
|
24588
|
+
cancelLabel: "Cancel",
|
|
24589
|
+
secondaryLabel: "Choose another…",
|
|
24590
|
+
secondaryValue: "choose-another",
|
|
24591
|
+
confirmLabel: targetExists ? "Replace" : "Create",
|
|
24592
|
+
confirmDisabled: !canCommitHere,
|
|
24593
|
+
destructive: targetExists,
|
|
23449
24594
|
});
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
pendingKind = null;
|
|
23454
|
-
setBusy(false);
|
|
24595
|
+
if (decision === "choose-another") {
|
|
24596
|
+
await openEditorSaveAsDialog({ content: operation.content, suggestedPath: conflictPath, reportCancellation: true });
|
|
24597
|
+
return;
|
|
23455
24598
|
}
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
|
|
23459
|
-
|
|
23460
|
-
|
|
23461
|
-
|
|
23462
|
-
return;
|
|
23463
|
-
}
|
|
23464
|
-
|
|
23465
|
-
if (editorDiffersFromFileBackedBaseline()) {
|
|
23466
|
-
const confirmed = await requestStudioConfirmation(
|
|
23467
|
-
"Replace current editor contents with the latest version from disk?",
|
|
23468
|
-
{ title: "Refresh from disk?", confirmLabel: "Replace", destructive: true },
|
|
23469
|
-
);
|
|
23470
|
-
if (!confirmed) return;
|
|
23471
|
-
}
|
|
24599
|
+
if (decision === true && canCommitHere) {
|
|
24600
|
+
sendEditorSaveAsRequest(conflictPath, operation.content, true, message.currentRevision);
|
|
24601
|
+
return;
|
|
24602
|
+
}
|
|
24603
|
+
setStatus("Save As cancelled; editor changes were kept.", "warning");
|
|
24604
|
+
}
|
|
23472
24605
|
|
|
23473
|
-
|
|
23474
|
-
|
|
24606
|
+
saveAsBtn.addEventListener("click", () => {
|
|
24607
|
+
void openEditorSaveAsDialog();
|
|
24608
|
+
});
|
|
23475
24609
|
|
|
23476
|
-
|
|
23477
|
-
|
|
23478
|
-
|
|
23479
|
-
|
|
23480
|
-
});
|
|
24610
|
+
saveOverBtn.addEventListener("click", () => {
|
|
24611
|
+
if (uiBusy) return;
|
|
24612
|
+
sendEditorSaveOverRequest();
|
|
24613
|
+
});
|
|
23481
24614
|
|
|
23482
|
-
|
|
23483
|
-
|
|
23484
|
-
|
|
23485
|
-
setBusy(false);
|
|
23486
|
-
}
|
|
24615
|
+
if (refreshFromDiskBtn) {
|
|
24616
|
+
refreshFromDiskBtn.addEventListener("click", () => {
|
|
24617
|
+
void requestEditorRefreshFromDisk();
|
|
23487
24618
|
});
|
|
23488
24619
|
}
|
|
23489
24620
|
|
|
@@ -24217,7 +25348,16 @@
|
|
|
24217
25348
|
resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
|
|
24218
25349
|
}
|
|
24219
25350
|
setSourceState(initialSourceState);
|
|
24220
|
-
if (initialSourceState.path) markFileBackedBaseline(sourceTextEl.value);
|
|
25351
|
+
if (initialSourceState.path) markFileBackedBaseline(sourceTextEl.value, initialDiskRevision);
|
|
25352
|
+
if (isWatchedFilePreview) {
|
|
25353
|
+
sourceTextEl.readOnly = true;
|
|
25354
|
+
sourceTextEl.setAttribute("aria-readonly", "true");
|
|
25355
|
+
sourceTextEl.title = "Read-only disk-backed source. Open a file tab to edit and save safely.";
|
|
25356
|
+
editorView = "markdown";
|
|
25357
|
+
rightView = "editor-preview";
|
|
25358
|
+
followLatest = false;
|
|
25359
|
+
if (document.body && document.body.classList) document.body.classList.add("watched-file-preview");
|
|
25360
|
+
}
|
|
24221
25361
|
refreshResponseUi();
|
|
24222
25362
|
updateAnnotatedReplyHeaderButton();
|
|
24223
25363
|
setActivePane(initialPaneFocusTarget === "off" ? "left" : initialPaneFocusTarget);
|
|
@@ -24230,7 +25370,9 @@
|
|
|
24230
25370
|
|
|
24231
25371
|
const initialDetectedLang = detectLanguageFromName(initialSourceState.path || initialSourceState.label || "");
|
|
24232
25372
|
const storedLang = readStoredEditorLanguage();
|
|
24233
|
-
setEditorLanguage(initialDetectedLang || storedLang || "markdown"
|
|
25373
|
+
setEditorLanguage(initialDetectedLang || storedLang || "markdown", {
|
|
25374
|
+
allowWatchedFileUpdate: isWatchedFilePreview,
|
|
25375
|
+
});
|
|
24234
25376
|
|
|
24235
25377
|
const storedLineNumbersEnabled = readStoredEditorLineNumbersEnabled();
|
|
24236
25378
|
const initialLineNumbersEnabled = storedLineNumbersEnabled ?? Boolean(lineNumbersSelect && lineNumbersSelect.value === "on");
|