pi-studio 0.9.53 → 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 +14 -0
- package/README.md +7 -3
- package/ROADMAP.md +4 -4
- package/client/studio-client.js +794 -144
- package/client/studio.css +50 -0
- package/index.ts +445 -97
- package/package.json +1 -1
- package/shared/studio-disk-revisions.js +558 -0
- package/shared/studio-file-watcher.js +181 -0
- 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 = {
|
|
@@ -282,6 +284,16 @@
|
|
|
282
284
|
};
|
|
283
285
|
const initialResourceDir = initialQueryParams.get("resourceDir")
|
|
284
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 };
|
|
285
297
|
|
|
286
298
|
let ws = null;
|
|
287
299
|
let wsState = "Connecting";
|
|
@@ -333,6 +345,7 @@
|
|
|
333
345
|
let studioDecisionMessageEl = null;
|
|
334
346
|
let studioDecisionInputEl = null;
|
|
335
347
|
let studioDecisionCancelBtn = null;
|
|
348
|
+
let studioDecisionTertiaryBtn = null;
|
|
336
349
|
let studioDecisionSecondaryBtn = null;
|
|
337
350
|
let studioDecisionConfirmBtn = null;
|
|
338
351
|
let studioDecisionState = null;
|
|
@@ -340,6 +353,7 @@
|
|
|
340
353
|
let pendingRequestId = null;
|
|
341
354
|
let pendingKind = null;
|
|
342
355
|
let stickyStudioKind = null;
|
|
356
|
+
const pendingSaveOperations = new Map();
|
|
343
357
|
const pendingCompanionLaunches = new Map();
|
|
344
358
|
const activeStudioTabLaunches = new Set();
|
|
345
359
|
let sourceOriginSummaryEl = null;
|
|
@@ -369,6 +383,7 @@
|
|
|
369
383
|
|
|
370
384
|
function normalizeRightViewValue(nextView) {
|
|
371
385
|
const normalized = canonicalRightViewValue(nextView);
|
|
386
|
+
if (isWatchedFilePreview && normalized !== "editor-preview") return "editor-preview";
|
|
372
387
|
if (normalized === "editor-quarto-preview" && !isCurrentStudioQuartoDocument()) {
|
|
373
388
|
return "editor-preview";
|
|
374
389
|
}
|
|
@@ -380,6 +395,7 @@
|
|
|
380
395
|
|
|
381
396
|
function isRightViewAvailableInCurrentMode(view) {
|
|
382
397
|
const normalized = canonicalRightViewValue(view);
|
|
398
|
+
if (isWatchedFilePreview) return normalized === "editor-preview";
|
|
383
399
|
if (normalized === "editor-quarto-preview" && !isCurrentStudioQuartoDocument()) return false;
|
|
384
400
|
return !isEditorOnlyMode || EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(normalized);
|
|
385
401
|
}
|
|
@@ -399,12 +415,17 @@
|
|
|
399
415
|
Array.from(rightViewSelect.options).forEach((option) => {
|
|
400
416
|
if (!option) return;
|
|
401
417
|
const isQuartoOption = option.value === "editor-quarto-preview";
|
|
418
|
+
if (isWatchedFilePreview && option.value === "editor-preview") option.textContent = "Watched preview";
|
|
402
419
|
if (isQuartoOption) option.hidden = !quartoRelevant;
|
|
403
|
-
option.disabled = (
|
|
420
|
+
option.disabled = (isWatchedFilePreview && option.value !== "editor-preview")
|
|
421
|
+
|| (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value))
|
|
422
|
+
|| (isQuartoOption && !quartoRelevant);
|
|
404
423
|
});
|
|
405
|
-
rightViewSelect.title =
|
|
424
|
+
rightViewSelect.title = isWatchedFilePreview
|
|
425
|
+
? "Read-only watched preview follows this file on disk."
|
|
426
|
+
: (isEditorOnlyMode
|
|
406
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."
|
|
407
|
-
: "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.";
|
|
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.");
|
|
408
429
|
}
|
|
409
430
|
|
|
410
431
|
function getInitialRightView(source) {
|
|
@@ -2164,6 +2185,7 @@
|
|
|
2164
2185
|
actionRequestId: null,
|
|
2165
2186
|
};
|
|
2166
2187
|
let fileBackedBaselineText = null;
|
|
2188
|
+
let fileBackedDiskRevision = null;
|
|
2167
2189
|
let activePane = initialPaneFocusTarget === "right" ? "right" : "left";
|
|
2168
2190
|
let paneFocusTarget = initialPaneFocusTarget;
|
|
2169
2191
|
let paneSplitPercent = 50;
|
|
@@ -2923,6 +2945,7 @@
|
|
|
2923
2945
|
rightTitleGroupEl.appendChild(rightViewSelect);
|
|
2924
2946
|
rightIdentityEl.appendChild(rightTitleGroupEl);
|
|
2925
2947
|
const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
|
|
2948
|
+
if (watchedOpenEditableBtn && isWatchedFilePreview) rightToolsEl.appendChild(watchedOpenEditableBtn);
|
|
2926
2949
|
if (exportPreviewControlsEl) {
|
|
2927
2950
|
rightToolsEl.appendChild(exportPreviewControlsEl);
|
|
2928
2951
|
} else if (exportPdfBtn) {
|
|
@@ -3846,7 +3869,7 @@
|
|
|
3846
3869
|
}
|
|
3847
3870
|
|
|
3848
3871
|
function getStudioDecisionFocusableElements() {
|
|
3849
|
-
return [studioDecisionInputEl, studioDecisionCancelBtn, studioDecisionSecondaryBtn, studioDecisionConfirmBtn]
|
|
3872
|
+
return [studioDecisionInputEl, studioDecisionCancelBtn, studioDecisionTertiaryBtn, studioDecisionSecondaryBtn, studioDecisionConfirmBtn]
|
|
3850
3873
|
.filter((element) => element && !element.hidden && !element.disabled);
|
|
3851
3874
|
}
|
|
3852
3875
|
|
|
@@ -3894,6 +3917,30 @@
|
|
|
3894
3917
|
cancelBtn.addEventListener("click", () => finishStudioDecision(null));
|
|
3895
3918
|
actions.appendChild(cancelBtn);
|
|
3896
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
|
+
|
|
3897
3944
|
const secondaryBtn = document.createElement("button");
|
|
3898
3945
|
secondaryBtn.type = "button";
|
|
3899
3946
|
secondaryBtn.className = "studio-decision-secondary";
|
|
@@ -3964,6 +4011,7 @@
|
|
|
3964
4011
|
studioDecisionMessageEl = message;
|
|
3965
4012
|
studioDecisionInputEl = input;
|
|
3966
4013
|
studioDecisionCancelBtn = cancelBtn;
|
|
4014
|
+
studioDecisionTertiaryBtn = tertiaryBtn;
|
|
3967
4015
|
studioDecisionSecondaryBtn = secondaryBtn;
|
|
3968
4016
|
studioDecisionConfirmBtn = confirmBtn;
|
|
3969
4017
|
return overlay;
|
|
@@ -3976,6 +4024,7 @@
|
|
|
3976
4024
|
if (studioDecisionState) finishStudioDecision(null, false);
|
|
3977
4025
|
const returnFocusEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
3978
4026
|
|
|
4027
|
+
const tertiaryLabel = String(settings.tertiaryLabel || "").trim();
|
|
3979
4028
|
const secondaryLabel = String(settings.secondaryLabel || "").trim();
|
|
3980
4029
|
studioDecisionTitleEl.textContent = String(settings.title || (mode === "prompt" ? "Enter a value" : "Confirm action"));
|
|
3981
4030
|
studioDecisionMessageEl.textContent = String(settings.message || "");
|
|
@@ -3984,9 +4033,13 @@
|
|
|
3984
4033
|
studioDecisionInputEl.placeholder = mode === "prompt" ? String(settings.placeholder || "") : "";
|
|
3985
4034
|
studioDecisionInputEl.setAttribute("aria-label", String(settings.inputLabel || "Value"));
|
|
3986
4035
|
studioDecisionCancelBtn.textContent = String(settings.cancelLabel || "Cancel");
|
|
4036
|
+
studioDecisionTertiaryBtn.hidden = !tertiaryLabel;
|
|
4037
|
+
studioDecisionTertiaryBtn.disabled = settings.tertiaryDisabled === true;
|
|
4038
|
+
studioDecisionTertiaryBtn.textContent = tertiaryLabel;
|
|
3987
4039
|
studioDecisionSecondaryBtn.hidden = !secondaryLabel;
|
|
3988
4040
|
studioDecisionSecondaryBtn.disabled = settings.secondaryDisabled === true;
|
|
3989
4041
|
studioDecisionSecondaryBtn.textContent = secondaryLabel;
|
|
4042
|
+
studioDecisionConfirmBtn.disabled = settings.confirmDisabled === true;
|
|
3990
4043
|
studioDecisionConfirmBtn.textContent = String(settings.confirmLabel || (mode === "prompt" ? "Continue" : "Confirm"));
|
|
3991
4044
|
studioDecisionConfirmBtn.classList.toggle("is-destructive", settings.destructive === true);
|
|
3992
4045
|
studioDecisionDialogEl.classList.toggle("is-destructive", settings.destructive === true);
|
|
@@ -3998,6 +4051,9 @@
|
|
|
3998
4051
|
mode,
|
|
3999
4052
|
resolve,
|
|
4000
4053
|
returnFocusEl,
|
|
4054
|
+
onTertiary: typeof settings.onTertiary === "function" ? settings.onTertiary : null,
|
|
4055
|
+
hasTertiaryValue: Object.prototype.hasOwnProperty.call(settings, "tertiaryValue"),
|
|
4056
|
+
tertiaryValue: settings.tertiaryValue,
|
|
4001
4057
|
onSecondary: typeof settings.onSecondary === "function" ? settings.onSecondary : null,
|
|
4002
4058
|
hasSecondaryValue: Object.prototype.hasOwnProperty.call(settings, "secondaryValue"),
|
|
4003
4059
|
secondaryValue: settings.secondaryValue,
|
|
@@ -4059,12 +4115,20 @@
|
|
|
4059
4115
|
}
|
|
4060
4116
|
});
|
|
4061
4117
|
|
|
4062
|
-
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) {
|
|
4063
4124
|
fileBackedBaselineText = String(text || "");
|
|
4125
|
+
fileBackedDiskRevision = normalizeStudioDiskRevision(diskRevision);
|
|
4126
|
+
scheduleWorkspacePersistence();
|
|
4064
4127
|
}
|
|
4065
4128
|
|
|
4066
4129
|
function clearFileBackedBaseline() {
|
|
4067
4130
|
fileBackedBaselineText = null;
|
|
4131
|
+
fileBackedDiskRevision = null;
|
|
4068
4132
|
}
|
|
4069
4133
|
|
|
4070
4134
|
function hasRefreshableFilePath() {
|
|
@@ -4079,13 +4143,17 @@
|
|
|
4079
4143
|
|
|
4080
4144
|
function updateSourceBadge() {
|
|
4081
4145
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
4082
|
-
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" : ""));
|
|
4083
4149
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4084
4150
|
if (sourceBadgeEl) {
|
|
4085
4151
|
sourceBadgeEl.textContent = originText;
|
|
4086
|
-
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
|
|
4087
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.")
|
|
4088
|
-
: ("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."));
|
|
4089
4157
|
}
|
|
4090
4158
|
if (sourceOriginSummaryEl) {
|
|
4091
4159
|
sourceOriginSummaryEl.textContent = originText;
|
|
@@ -4544,10 +4612,14 @@
|
|
|
4544
4612
|
}
|
|
4545
4613
|
|
|
4546
4614
|
function triggerEditorSaveShortcut() {
|
|
4547
|
-
if (saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
|
|
4615
|
+
if (hasRefreshableFilePath() && saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
|
|
4548
4616
|
saveOverBtn.click();
|
|
4549
4617
|
return true;
|
|
4550
4618
|
}
|
|
4619
|
+
return triggerEditorSaveAsShortcut();
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
function triggerEditorSaveAsShortcut() {
|
|
4551
4623
|
if (saveAsBtn && !saveAsBtn.disabled && !saveAsBtn.hidden) {
|
|
4552
4624
|
saveAsBtn.click();
|
|
4553
4625
|
return true;
|
|
@@ -4890,6 +4962,22 @@
|
|
|
4890
4962
|
return;
|
|
4891
4963
|
}
|
|
4892
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
|
+
|
|
4893
4981
|
const isSaveShortcut =
|
|
4894
4982
|
key.toLowerCase() === "s"
|
|
4895
4983
|
&& (event.metaKey || event.ctrlKey)
|
|
@@ -4898,6 +4986,10 @@
|
|
|
4898
4986
|
|
|
4899
4987
|
if (isSaveShortcut) {
|
|
4900
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
|
+
}
|
|
4901
4993
|
triggerEditorSaveShortcut();
|
|
4902
4994
|
return;
|
|
4903
4995
|
}
|
|
@@ -5644,6 +5736,8 @@
|
|
|
5644
5736
|
clearPreviewJumpHighlight(targetEl);
|
|
5645
5737
|
finishPreviewRender(targetEl);
|
|
5646
5738
|
targetEl.innerHTML = html;
|
|
5739
|
+
clearWatchedPreviewRenderError();
|
|
5740
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, text);
|
|
5647
5741
|
if (pane === "response") {
|
|
5648
5742
|
applyPendingResponseScrollReset();
|
|
5649
5743
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -7123,6 +7217,8 @@
|
|
|
7123
7217
|
});
|
|
7124
7218
|
|
|
7125
7219
|
targetEl.appendChild(shell);
|
|
7220
|
+
clearWatchedPreviewRenderError();
|
|
7221
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, html);
|
|
7126
7222
|
|
|
7127
7223
|
if (pane === "response") {
|
|
7128
7224
|
applyPendingResponseScrollReset();
|
|
@@ -9364,11 +9460,119 @@
|
|
|
9364
9460
|
|
|
9365
9461
|
function hasMeaningfulPreviewContent(targetEl) {
|
|
9366
9462
|
if (!targetEl || typeof targetEl.querySelector !== "function") return false;
|
|
9463
|
+
if (targetEl.dataset && targetEl.dataset.studioPreviewCommitted === "1") return true;
|
|
9367
9464
|
if (targetEl.querySelector(".preview-loading")) return false;
|
|
9368
9465
|
const text = typeof targetEl.textContent === "string" ? targetEl.textContent.trim() : "";
|
|
9369
9466
|
return text.length > 0;
|
|
9370
9467
|
}
|
|
9371
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
|
+
|
|
9372
9576
|
function beginPreviewRender(targetEl) {
|
|
9373
9577
|
if (!targetEl || !targetEl.classList) return;
|
|
9374
9578
|
|
|
@@ -10695,6 +10899,28 @@
|
|
|
10695
10899
|
);
|
|
10696
10900
|
}
|
|
10697
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
|
+
|
|
10698
10924
|
async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
|
|
10699
10925
|
const previewPrepared = annotationsEnabled
|
|
10700
10926
|
? prepareMarkdownForPandocPreview(markdown)
|
|
@@ -10705,35 +10931,44 @@
|
|
|
10705
10931
|
};
|
|
10706
10932
|
const pdfPrepared = prepareStudioPdfBlocksForPreview(previewPrepared.markdown);
|
|
10707
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
|
+
};
|
|
10708
10943
|
|
|
10709
10944
|
try {
|
|
10710
10945
|
const renderedHtml = await renderMarkdownWithPandoc(pdfPrepared.markdown, {
|
|
10711
10946
|
includeEditorLanguage: pane === "source" || rightView === "editor-preview",
|
|
10712
10947
|
resourceContext: previewResourceContext,
|
|
10713
10948
|
});
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
await renderPdfPreviewsInElement(targetEl);
|
|
10730
|
-
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);
|
|
10731
10964
|
const annotationMode = (pane === "source" || pane === "response")
|
|
10732
10965
|
? (annotationsEnabled ? "highlight" : "hide")
|
|
10733
10966
|
: "none";
|
|
10734
|
-
applyAnnotationMarkersToElement(
|
|
10735
|
-
await renderMermaidInElement(
|
|
10736
|
-
|
|
10967
|
+
applyAnnotationMarkersToElement(staging, annotationMode);
|
|
10968
|
+
await renderMermaidInElement(staging);
|
|
10969
|
+
if (abandonIfStale()) return;
|
|
10970
|
+
await renderMathFallbackInElement(staging);
|
|
10971
|
+
if (abandonIfStale()) return;
|
|
10737
10972
|
|
|
10738
10973
|
const shouldDecoratePreviewComments = supportsPreviewCommentsForCurrentEditor()
|
|
10739
10974
|
&& (
|
|
@@ -10741,35 +10976,56 @@
|
|
|
10741
10976
|
|| (pane === "response" && rightView === "editor-preview")
|
|
10742
10977
|
);
|
|
10743
10978
|
if (shouldDecoratePreviewComments) {
|
|
10744
|
-
decorateRenderedEditorPreviewComments(
|
|
10979
|
+
decorateRenderedEditorPreviewComments(staging, sourceTextEl.value || "");
|
|
10745
10980
|
}
|
|
10746
|
-
decorateCopyablePreviewBlocks(
|
|
10747
|
-
decoratePreviewImages(
|
|
10981
|
+
decorateCopyablePreviewBlocks(staging);
|
|
10982
|
+
decoratePreviewImages(staging);
|
|
10748
10983
|
|
|
10749
|
-
// 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).
|
|
10750
10985
|
if (!sourceState.path && !getCurrentResourceDirValue()) {
|
|
10751
10986
|
var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
|
|
10752
10987
|
var hasLatexImages = /\\includegraphics/.test(markdown || "");
|
|
10753
10988
|
if (hasRelativeImages || hasLatexImages) {
|
|
10754
|
-
appendPreviewNotice(
|
|
10989
|
+
appendPreviewNotice(staging, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
|
|
10755
10990
|
}
|
|
10756
10991
|
}
|
|
10992
|
+
if (abandonIfStale()) return;
|
|
10757
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);
|
|
10758
11003
|
if (pane === "response") {
|
|
10759
11004
|
applyPendingResponseScrollReset();
|
|
10760
11005
|
scheduleResponsePaneRepaintNudge();
|
|
10761
11006
|
}
|
|
10762
11007
|
} catch (error) {
|
|
10763
|
-
if (
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
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;
|
|
10767
11013
|
}
|
|
11014
|
+
if (!stillCurrent()) return;
|
|
10768
11015
|
|
|
10769
11016
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
10770
11017
|
clearPreviewJumpHighlight(targetEl);
|
|
10771
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
|
+
}
|
|
10772
11027
|
targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown, previewFallbackOptions);
|
|
11028
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, markdown);
|
|
10773
11029
|
if (pane === "response") {
|
|
10774
11030
|
applyPendingResponseScrollReset();
|
|
10775
11031
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -11614,6 +11870,9 @@
|
|
|
11614
11870
|
const newTabButton = newTabAction
|
|
11615
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>"
|
|
11616
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
|
+
: "";
|
|
11617
11876
|
const openTitle = type === "directory"
|
|
11618
11877
|
? "Open folder"
|
|
11619
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"))));
|
|
@@ -11624,6 +11883,7 @@
|
|
|
11624
11883
|
+ "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
|
|
11625
11884
|
+ "</button>"
|
|
11626
11885
|
+ "<span class='files-actions'>"
|
|
11886
|
+
+ watchButton
|
|
11627
11887
|
+ newTabButton
|
|
11628
11888
|
+ "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(path) + "'>Copy path</button>"
|
|
11629
11889
|
+ (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(path) + "'>Reveal</button>" : "")
|
|
@@ -11797,7 +12057,7 @@
|
|
|
11797
12057
|
function ensureCurrentEditorFileBackedFromFilesPath(path) {
|
|
11798
12058
|
const cleanPath = stripPreviewLocalLinkUrlSuffix(path || "").trim();
|
|
11799
12059
|
if (!isLikelyAbsoluteStudioPath(cleanPath)) return;
|
|
11800
|
-
if (sourceState && sourceState.path
|
|
12060
|
+
if (sourceState && sourceState.path) return;
|
|
11801
12061
|
const resourceDir = normalizeStudioResourceDirValue(fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(cleanPath));
|
|
11802
12062
|
if (resourceDirInput && resourceDir) resourceDirInput.value = resourceDir;
|
|
11803
12063
|
setSourceState({
|
|
@@ -11805,7 +12065,7 @@
|
|
|
11805
12065
|
label: sourceState && sourceState.label && sourceState.label !== "blank" ? sourceState.label : basenameForStudioPath(cleanPath),
|
|
11806
12066
|
path: cleanPath,
|
|
11807
12067
|
});
|
|
11808
|
-
markFileBackedBaseline(sourceTextEl.value);
|
|
12068
|
+
markFileBackedBaseline(sourceTextEl.value, null);
|
|
11809
12069
|
}
|
|
11810
12070
|
|
|
11811
12071
|
async function openFileBrowserEntry(path, kind) {
|
|
@@ -11945,6 +12205,10 @@
|
|
|
11945
12205
|
await openPreviewDocumentInNewEditor(path, getFileBrowserLocalLinkContext());
|
|
11946
12206
|
return;
|
|
11947
12207
|
}
|
|
12208
|
+
if (action === "watch-new") {
|
|
12209
|
+
await openPreviewDocumentInWatchedPreview(path, getFileBrowserLocalLinkContext());
|
|
12210
|
+
return;
|
|
12211
|
+
}
|
|
11948
12212
|
if (action === "open-preview-new") {
|
|
11949
12213
|
await openPreviewResourceInNewEditor(path, getFileBrowserLocalLinkContext());
|
|
11950
12214
|
return;
|
|
@@ -13575,13 +13839,13 @@
|
|
|
13575
13839
|
function updateSaveFileTooltip() {
|
|
13576
13840
|
if (!saveOverBtn) return;
|
|
13577
13841
|
|
|
13578
|
-
var effectivePath =
|
|
13842
|
+
var effectivePath = sourceState && sourceState.path ? sourceState.path : "";
|
|
13579
13843
|
if (effectivePath) {
|
|
13580
|
-
saveOverBtn.title = "
|
|
13844
|
+
saveOverBtn.title = "Save file when its disk revision still matches: " + effectivePath + " · Shortcut: Cmd/Ctrl+S.";
|
|
13581
13845
|
return;
|
|
13582
13846
|
}
|
|
13583
13847
|
|
|
13584
|
-
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.";
|
|
13585
13849
|
}
|
|
13586
13850
|
|
|
13587
13851
|
function updateRefreshFromDiskTooltip() {
|
|
@@ -13596,13 +13860,13 @@
|
|
|
13596
13860
|
}
|
|
13597
13861
|
|
|
13598
13862
|
function syncActionButtons() {
|
|
13599
|
-
const canSaveOver =
|
|
13863
|
+
const canSaveOver = hasRefreshableFilePath();
|
|
13600
13864
|
const canRefreshFromDisk = hasRefreshableFilePath();
|
|
13601
13865
|
|
|
13602
|
-
fileInput.disabled = uiBusy;
|
|
13603
|
-
if (importFileBtn) importFileBtn.disabled = uiBusy;
|
|
13604
|
-
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
|
|
13605
|
-
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;
|
|
13606
13870
|
if (sourceOpenCurrentFileTabBtn) {
|
|
13607
13871
|
sourceOpenCurrentFileTabBtn.disabled = uiBusy || !hasRefreshableFilePath();
|
|
13608
13872
|
sourceOpenCurrentFileTabBtn.title = hasRefreshableFilePath()
|
|
@@ -13610,17 +13874,21 @@
|
|
|
13610
13874
|
: "Available after opening a file-backed document.";
|
|
13611
13875
|
}
|
|
13612
13876
|
if (sourceOpenCurrentTextCopyTabBtn) sourceOpenCurrentTextCopyTabBtn.disabled = uiBusy || wsState !== "Ready" || !String(sourceTextEl.value || "").trim();
|
|
13613
|
-
saveAsBtn.disabled = uiBusy;
|
|
13614
|
-
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
13615
|
-
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
13616
|
-
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;
|
|
13617
13881
|
sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
13618
|
-
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
|
+
}
|
|
13619
13887
|
syncRunAndCritiqueButtons();
|
|
13620
13888
|
copyDraftBtn.disabled = uiBusy;
|
|
13621
13889
|
if (suggestCompletionBtn) {
|
|
13622
13890
|
const hasSuggestionForCurrentText = Boolean(completionSuggestionState && sourceTextEl && sourceTextEl.value === completionSuggestionState.baseText);
|
|
13623
|
-
suggestCompletionBtn.disabled = wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
|
|
13891
|
+
suggestCompletionBtn.disabled = isWatchedFilePreview || wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
|
|
13624
13892
|
suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Stop" : (hasSuggestionForCurrentText ? "Try another" : "Suggest");
|
|
13625
13893
|
suggestCompletionBtn.title = completionSuggestionInFlight
|
|
13626
13894
|
? "Stop the current suggestion request."
|
|
@@ -13636,15 +13904,15 @@
|
|
|
13636
13904
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
13637
13905
|
if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
|
|
13638
13906
|
if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
|
|
13639
|
-
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
|
|
13640
|
-
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);
|
|
13641
13909
|
if (compactBtn) compactBtn.disabled = isEditorOnlyMode || uiBusy || compactInProgress || wsState === "Disconnected";
|
|
13642
13910
|
editorViewSelect.disabled = isEditorOnlyMode;
|
|
13643
13911
|
syncRightViewModeOptions();
|
|
13644
|
-
rightViewSelect.disabled =
|
|
13912
|
+
rightViewSelect.disabled = isWatchedFilePreview;
|
|
13645
13913
|
followSelect.disabled = isEditorOnlyMode || uiBusy;
|
|
13646
13914
|
if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
|
|
13647
|
-
insertHeaderBtn.disabled = uiBusy;
|
|
13915
|
+
insertHeaderBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13648
13916
|
lensSelect.disabled = uiBusy || isEditorOnlyMode;
|
|
13649
13917
|
updateSaveFileTooltip();
|
|
13650
13918
|
updateRefreshFromDiskTooltip();
|
|
@@ -13661,9 +13929,14 @@
|
|
|
13661
13929
|
|
|
13662
13930
|
function setSourceState(next, options) {
|
|
13663
13931
|
const previousDescriptor = getCurrentStudioDocumentDescriptor();
|
|
13932
|
+
const previousPath = sourceState && sourceState.path ? sourceState.path : null;
|
|
13664
13933
|
const previousQuartoPath = getCurrentStudioQuartoSourcePath();
|
|
13665
13934
|
const previousPreviewResourceContext = getHtmlPreviewResourceContextOptions();
|
|
13666
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
|
+
}
|
|
13667
13940
|
sourceState = {
|
|
13668
13941
|
source: next && next.source ? next.source : "blank",
|
|
13669
13942
|
label: next && next.label ? next.label : "blank",
|
|
@@ -13681,7 +13954,7 @@
|
|
|
13681
13954
|
quartoPreviewActionRequestId = null;
|
|
13682
13955
|
quartoPreviewLogVisible = false;
|
|
13683
13956
|
}
|
|
13684
|
-
if (!sourceState.path) {
|
|
13957
|
+
if (!sourceState.path || sourceState.path !== previousPath) {
|
|
13685
13958
|
clearFileBackedBaseline();
|
|
13686
13959
|
}
|
|
13687
13960
|
syncRightViewModeOptions();
|
|
@@ -13788,6 +14061,7 @@
|
|
|
13788
14061
|
version: 1,
|
|
13789
14062
|
savedAt: lastWorkspacePersistenceSavedAt,
|
|
13790
14063
|
sourceState: normalizeWorkspaceSourceState(sourceState),
|
|
14064
|
+
diskRevision: fileBackedDiskRevision,
|
|
13791
14065
|
resourceDir: getCurrentResourceDirValue(),
|
|
13792
14066
|
editorView,
|
|
13793
14067
|
rightView: normalizeRightViewValue(rightView),
|
|
@@ -13823,7 +14097,7 @@
|
|
|
13823
14097
|
}
|
|
13824
14098
|
|
|
13825
14099
|
function persistWorkspaceStateNow(options) {
|
|
13826
|
-
if (!workspacePersistenceReady) return;
|
|
14100
|
+
if (!workspacePersistenceReady || isWatchedFilePreview) return;
|
|
13827
14101
|
try {
|
|
13828
14102
|
const payload = buildWorkspacePersistencePayload();
|
|
13829
14103
|
if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
|
|
@@ -13845,7 +14119,7 @@
|
|
|
13845
14119
|
}
|
|
13846
14120
|
|
|
13847
14121
|
function scheduleWorkspacePersistence() {
|
|
13848
|
-
if (!workspacePersistenceReady || workspacePersistTimer !== null) return;
|
|
14122
|
+
if (!workspacePersistenceReady || isWatchedFilePreview || workspacePersistTimer !== null) return;
|
|
13849
14123
|
workspacePersistTimer = window.setTimeout(() => {
|
|
13850
14124
|
workspacePersistTimer = null;
|
|
13851
14125
|
persistWorkspaceStateNow();
|
|
@@ -13853,6 +14127,7 @@
|
|
|
13853
14127
|
}
|
|
13854
14128
|
|
|
13855
14129
|
function flushWorkspacePersistence(options) {
|
|
14130
|
+
if (isWatchedFilePreview) return;
|
|
13856
14131
|
if (workspacePersistTimer !== null) {
|
|
13857
14132
|
window.clearTimeout(workspacePersistTimer);
|
|
13858
14133
|
workspacePersistTimer = null;
|
|
@@ -13882,9 +14157,17 @@
|
|
|
13882
14157
|
if (!shouldRestorePersistedWorkspaceState(state)) return false;
|
|
13883
14158
|
const nextSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
13884
14159
|
const nextResourceDir = normalizeStudioResourceDirValue(typeof state.resourceDir === "string" ? state.resourceDir : "");
|
|
14160
|
+
const currentBaselineText = fileBackedBaselineText;
|
|
14161
|
+
const currentDiskRevision = fileBackedDiskRevision;
|
|
14162
|
+
const persistedDiskRevision = normalizeStudioDiskRevision(state.diskRevision);
|
|
13885
14163
|
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
13886
14164
|
setEditorText(state.text, { preserveScroll: false, preserveSelection: false });
|
|
13887
14165
|
setSourceState(nextSourceState);
|
|
14166
|
+
if (nextSourceState.path) {
|
|
14167
|
+
fileBackedBaselineText = currentBaselineText;
|
|
14168
|
+
fileBackedDiskRevision = persistedDiskRevision
|
|
14169
|
+
|| (currentBaselineText !== null && state.text === currentBaselineText ? currentDiskRevision : null);
|
|
14170
|
+
}
|
|
13888
14171
|
if (resourceDirInput && nextResourceDir) {
|
|
13889
14172
|
resourceDirInput.value = nextResourceDir;
|
|
13890
14173
|
updateSourceBadge();
|
|
@@ -13970,6 +14253,10 @@
|
|
|
13970
14253
|
}
|
|
13971
14254
|
|
|
13972
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
|
+
}
|
|
13973
14260
|
const value = String(nextText || "");
|
|
13974
14261
|
const preserveScroll = Boolean(options && options.preserveScroll);
|
|
13975
14262
|
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
@@ -14017,9 +14304,14 @@
|
|
|
14017
14304
|
updateEditorSelectionCommentUi();
|
|
14018
14305
|
updateOutlineUi();
|
|
14019
14306
|
scheduleWorkspacePersistence();
|
|
14307
|
+
return true;
|
|
14020
14308
|
}
|
|
14021
14309
|
|
|
14022
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
|
+
}
|
|
14023
14315
|
const value = String(nextText || "");
|
|
14024
14316
|
sourceTextEl.value = value;
|
|
14025
14317
|
const maxIndex = value.length;
|
|
@@ -14031,6 +14323,7 @@
|
|
|
14031
14323
|
if (editorView === "markdown") {
|
|
14032
14324
|
scheduleEditorLineNumberRender();
|
|
14033
14325
|
}
|
|
14326
|
+
return true;
|
|
14034
14327
|
}
|
|
14035
14328
|
|
|
14036
14329
|
function readCompletionSuggestionContextMode() {
|
|
@@ -15191,11 +15484,12 @@
|
|
|
15191
15484
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
15192
15485
|
appendPreviewLinkMenuButton(menu, "Open in system viewer", "open-system");
|
|
15193
15486
|
} else if (kind === "text") {
|
|
15487
|
+
appendPreviewLinkMenuButton(menu, "Preview file (follow changes)", "watch-new");
|
|
15194
15488
|
appendPreviewLinkMenuButton(menu, "Open file tab", "open-new");
|
|
15195
|
-
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
15489
|
+
if (!isWatchedFilePreview) appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
15196
15490
|
} else if (kind === "office") {
|
|
15197
15491
|
appendPreviewLinkMenuButton(menu, "Convert tab", "open-new");
|
|
15198
|
-
appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
15492
|
+
if (!isWatchedFilePreview) appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
15199
15493
|
} else if (kind === "image") {
|
|
15200
15494
|
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
15201
15495
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
@@ -15291,9 +15585,15 @@
|
|
|
15291
15585
|
}
|
|
15292
15586
|
|
|
15293
15587
|
async function fetchPreviewLocalLink(action, href, contextOverride, options) {
|
|
15294
|
-
const request = () =>
|
|
15295
|
-
query
|
|
15296
|
-
|
|
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
|
+
};
|
|
15297
15597
|
try {
|
|
15298
15598
|
return await request();
|
|
15299
15599
|
} catch (error) {
|
|
@@ -15385,6 +15685,9 @@
|
|
|
15385
15685
|
}
|
|
15386
15686
|
|
|
15387
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
|
+
}
|
|
15388
15691
|
if (!(await confirmPreviewOfficeConversion(href, "here"))) return;
|
|
15389
15692
|
if (editorHasPotentialUnsavedContent()) {
|
|
15390
15693
|
const kind = getPreviewLocalLinkKind(href);
|
|
@@ -15414,7 +15717,7 @@
|
|
|
15414
15717
|
setSourceState({ source: "blank", label, path: null });
|
|
15415
15718
|
} else {
|
|
15416
15719
|
setSourceState({ source: "file", label, path });
|
|
15417
|
-
markFileBackedBaseline(payload.text);
|
|
15720
|
+
markFileBackedBaseline(payload.text, payload.diskRevision);
|
|
15418
15721
|
}
|
|
15419
15722
|
const detected = converted ? "markdown" : detectLanguageFromName(path || label);
|
|
15420
15723
|
if (detected) setEditorLanguage(detected);
|
|
@@ -15445,6 +15748,25 @@
|
|
|
15445
15748
|
}
|
|
15446
15749
|
}
|
|
15447
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
|
+
}
|
|
15766
|
+
throw error;
|
|
15767
|
+
}
|
|
15768
|
+
}
|
|
15769
|
+
|
|
15448
15770
|
async function openPreviewResourceInNewEditor(href, contextOverride) {
|
|
15449
15771
|
let launch = null;
|
|
15450
15772
|
try {
|
|
@@ -15500,6 +15822,10 @@
|
|
|
15500
15822
|
await openPreviewDocumentInNewEditor(href, context);
|
|
15501
15823
|
return;
|
|
15502
15824
|
}
|
|
15825
|
+
if (action === "watch-new") {
|
|
15826
|
+
await openPreviewDocumentInWatchedPreview(href, context);
|
|
15827
|
+
return;
|
|
15828
|
+
}
|
|
15503
15829
|
if (action === "open-preview-new") {
|
|
15504
15830
|
await openPreviewResourceInNewEditor(href, context);
|
|
15505
15831
|
return;
|
|
@@ -16861,6 +17187,8 @@
|
|
|
16861
17187
|
ensurePreviewSelectionActions(targetEl);
|
|
16862
17188
|
updatePreviewCommentBlocksForElement(targetEl);
|
|
16863
17189
|
decorateCopyablePreviewBlocks(targetEl);
|
|
17190
|
+
clearWatchedPreviewRenderError();
|
|
17191
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, text);
|
|
16864
17192
|
if (pane === "response") {
|
|
16865
17193
|
applyPendingResponseScrollReset();
|
|
16866
17194
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -21734,7 +22062,11 @@
|
|
|
21734
22062
|
} catch {}
|
|
21735
22063
|
}
|
|
21736
22064
|
|
|
21737
|
-
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
|
+
}
|
|
21738
22070
|
editorLanguage = (lang && SUPPORTED_LANGUAGES.indexOf(lang) !== -1) ? lang : "markdown";
|
|
21739
22071
|
persistEditorLanguage(editorLanguage);
|
|
21740
22072
|
syncHighlightSelectUi();
|
|
@@ -21749,6 +22081,7 @@
|
|
|
21749
22081
|
}
|
|
21750
22082
|
updateOutlineUi();
|
|
21751
22083
|
scheduleWorkspacePersistence();
|
|
22084
|
+
return true;
|
|
21752
22085
|
}
|
|
21753
22086
|
|
|
21754
22087
|
function setEditorHighlightMode(mode) {
|
|
@@ -22050,11 +22383,90 @@
|
|
|
22050
22383
|
return true;
|
|
22051
22384
|
}
|
|
22052
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
|
+
|
|
22053
22452
|
function handleServerMessage(message) {
|
|
22054
22453
|
if (!message || typeof message !== "object") return;
|
|
22055
22454
|
|
|
22056
22455
|
debugTrace("server_message", summarizeServerMessage(message));
|
|
22057
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
|
+
|
|
22058
22470
|
const contextChanged = applyContextUsageFromMessage(message);
|
|
22059
22471
|
if (contextChanged) {
|
|
22060
22472
|
updateFooterMeta();
|
|
@@ -22291,7 +22703,11 @@
|
|
|
22291
22703
|
message.initialDocument &&
|
|
22292
22704
|
typeof message.initialDocument.text === "string"
|
|
22293
22705
|
) {
|
|
22294
|
-
setEditorText(message.initialDocument.text, {
|
|
22706
|
+
setEditorText(message.initialDocument.text, {
|
|
22707
|
+
preserveScroll: false,
|
|
22708
|
+
preserveSelection: false,
|
|
22709
|
+
allowWatchedFileUpdate: isWatchedFilePreview,
|
|
22710
|
+
});
|
|
22295
22711
|
initialDocumentApplied = true;
|
|
22296
22712
|
loadedInitialDocument = true;
|
|
22297
22713
|
setSourceState({
|
|
@@ -22303,7 +22719,7 @@
|
|
|
22303
22719
|
: (initialSourceState.draftId || null),
|
|
22304
22720
|
});
|
|
22305
22721
|
if (message.initialDocument.path) {
|
|
22306
|
-
markFileBackedBaseline(message.initialDocument.text);
|
|
22722
|
+
markFileBackedBaseline(message.initialDocument.text, message.initialDocument.diskRevision);
|
|
22307
22723
|
}
|
|
22308
22724
|
refreshResponseUi();
|
|
22309
22725
|
if (typeof message.initialDocument.label === "string" && message.initialDocument.label.length > 0) {
|
|
@@ -22664,7 +23080,19 @@
|
|
|
22664
23080
|
return;
|
|
22665
23081
|
}
|
|
22666
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
|
+
|
|
22667
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);
|
|
22668
23096
|
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
22669
23097
|
pendingRequestId = null;
|
|
22670
23098
|
pendingKind = null;
|
|
@@ -22683,7 +23111,7 @@
|
|
|
22683
23111
|
}, {
|
|
22684
23112
|
carryCurrentMetadataToNewDocument: true,
|
|
22685
23113
|
});
|
|
22686
|
-
markFileBackedBaseline(sourceTextEl.value);
|
|
23114
|
+
markFileBackedBaseline(savedOperation && typeof savedOperation.content === "string" ? savedOperation.content : sourceTextEl.value, message.diskRevision);
|
|
22687
23115
|
}
|
|
22688
23116
|
setBusy(false);
|
|
22689
23117
|
setWsState("Ready");
|
|
@@ -22703,6 +23131,10 @@
|
|
|
22703
23131
|
}
|
|
22704
23132
|
|
|
22705
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
|
+
}
|
|
22706
23138
|
if (typeof message.requestId === "string" && pendingRequestId && message.requestId !== pendingRequestId) {
|
|
22707
23139
|
return;
|
|
22708
23140
|
}
|
|
@@ -22726,6 +23158,10 @@
|
|
|
22726
23158
|
}
|
|
22727
23159
|
|
|
22728
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
|
+
}
|
|
22729
23165
|
const nextDoc = message.document;
|
|
22730
23166
|
if (!nextDoc || typeof nextDoc !== "object" || typeof nextDoc.text !== "string") {
|
|
22731
23167
|
return;
|
|
@@ -22763,7 +23199,7 @@
|
|
|
22763
23199
|
draftId: typeof nextDoc.draftId === "string" && nextDoc.draftId.trim() ? nextDoc.draftId.trim() : null,
|
|
22764
23200
|
});
|
|
22765
23201
|
if (nextPath) {
|
|
22766
|
-
markFileBackedBaseline(nextDoc.text);
|
|
23202
|
+
markFileBackedBaseline(nextDoc.text, nextDoc.diskRevision);
|
|
22767
23203
|
}
|
|
22768
23204
|
refreshResponseUi();
|
|
22769
23205
|
setStatus(
|
|
@@ -22906,6 +23342,7 @@
|
|
|
22906
23342
|
|
|
22907
23343
|
if (message.type === "busy") {
|
|
22908
23344
|
if (typeof message.requestId === "string") {
|
|
23345
|
+
pendingSaveOperations.delete(message.requestId);
|
|
22909
23346
|
failPendingCompanionLaunch(message.requestId, "Studio could not start the companion editor because another request was busy.");
|
|
22910
23347
|
}
|
|
22911
23348
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
@@ -22927,6 +23364,7 @@
|
|
|
22927
23364
|
|
|
22928
23365
|
if (message.type === "error") {
|
|
22929
23366
|
if (typeof message.requestId === "string") {
|
|
23367
|
+
pendingSaveOperations.delete(message.requestId);
|
|
22930
23368
|
failPendingCompanionLaunch(message.requestId, "Studio could not prepare the companion editor. Return to the originating Studio page for details.");
|
|
22931
23369
|
}
|
|
22932
23370
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
@@ -23033,6 +23471,11 @@
|
|
|
23033
23471
|
if (studioMode !== "full") {
|
|
23034
23472
|
wsParams.set("mode", studioMode);
|
|
23035
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
|
+
}
|
|
23036
23479
|
if (DEBUG_ENABLED) {
|
|
23037
23480
|
wsParams.set("debug", "1");
|
|
23038
23481
|
}
|
|
@@ -23083,6 +23526,14 @@
|
|
|
23083
23526
|
return;
|
|
23084
23527
|
}
|
|
23085
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
|
+
|
|
23086
23537
|
if (kind === "shutdown") {
|
|
23087
23538
|
clearScheduledReconnect();
|
|
23088
23539
|
reconnectAttempt = 0;
|
|
@@ -23098,14 +23549,28 @@
|
|
|
23098
23549
|
};
|
|
23099
23550
|
|
|
23100
23551
|
socket.addEventListener("open", () => {
|
|
23552
|
+
if (ws !== socket) {
|
|
23553
|
+
try { socket.close(); } catch {}
|
|
23554
|
+
return;
|
|
23555
|
+
}
|
|
23101
23556
|
window.clearTimeout(connectWatchdog);
|
|
23102
23557
|
setWsState("Ready");
|
|
23103
23558
|
setStatus(wasReconnect ? "Reconnected. Syncing…" : "Connected. Syncing…");
|
|
23104
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
|
+
}
|
|
23105
23569
|
reconnectAttempt = 0;
|
|
23106
23570
|
});
|
|
23107
23571
|
|
|
23108
23572
|
socket.addEventListener("message", (event) => {
|
|
23573
|
+
if (ws !== socket) return;
|
|
23109
23574
|
try {
|
|
23110
23575
|
const message = JSON.parse(event.data);
|
|
23111
23576
|
handleServerMessage(message);
|
|
@@ -23116,6 +23581,7 @@
|
|
|
23116
23581
|
});
|
|
23117
23582
|
|
|
23118
23583
|
socket.addEventListener("close", (event) => {
|
|
23584
|
+
if (ws !== socket && !disconnectHandled) return;
|
|
23119
23585
|
if (event && event.code === 4001) {
|
|
23120
23586
|
handleDisconnect("invalidated", 4001);
|
|
23121
23587
|
return;
|
|
@@ -23124,6 +23590,10 @@
|
|
|
23124
23590
|
handleDisconnect("full_conflict", 4004);
|
|
23125
23591
|
return;
|
|
23126
23592
|
}
|
|
23593
|
+
if (event && event.code === 4003) {
|
|
23594
|
+
handleDisconnect("watch_unauthorized", 4003);
|
|
23595
|
+
return;
|
|
23596
|
+
}
|
|
23127
23597
|
if (event && event.code === 1001) {
|
|
23128
23598
|
handleDisconnect("shutdown", 1001);
|
|
23129
23599
|
return;
|
|
@@ -23133,6 +23603,7 @@
|
|
|
23133
23603
|
});
|
|
23134
23604
|
|
|
23135
23605
|
socket.addEventListener("error", () => {
|
|
23606
|
+
if (ws !== socket) return;
|
|
23136
23607
|
handleDisconnect("error");
|
|
23137
23608
|
});
|
|
23138
23609
|
}
|
|
@@ -23361,6 +23832,25 @@
|
|
|
23361
23832
|
});
|
|
23362
23833
|
}
|
|
23363
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
|
+
|
|
23364
23854
|
updatePaneFocusButtons();
|
|
23365
23855
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
23366
23856
|
window.addEventListener("pagehide", () => {
|
|
@@ -23882,100 +24372,249 @@
|
|
|
23882
24372
|
setFooterThemeMenuOpen(false);
|
|
23883
24373
|
});
|
|
23884
24374
|
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
if (
|
|
23888
|
-
|
|
23889
|
-
|
|
24375
|
+
function abandonPendingSaveRequest(requestId) {
|
|
24376
|
+
pendingSaveOperations.delete(requestId);
|
|
24377
|
+
if (requestId) clearArmedTitleAttention(requestId);
|
|
24378
|
+
if (pendingRequestId === requestId) {
|
|
24379
|
+
pendingRequestId = null;
|
|
24380
|
+
pendingKind = null;
|
|
24381
|
+
}
|
|
24382
|
+
stickyStudioKind = null;
|
|
24383
|
+
setBusy(false);
|
|
24384
|
+
setWsState("Ready");
|
|
24385
|
+
}
|
|
24386
|
+
|
|
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;
|
|
23890
24413
|
}
|
|
24414
|
+
return true;
|
|
24415
|
+
}
|
|
23891
24416
|
|
|
23892
|
-
|
|
23893
|
-
|
|
23894
|
-
const
|
|
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"));
|
|
23895
24426
|
const path = await requestStudioTextInput("Save editor content as:", suggested, {
|
|
23896
24427
|
title: "Save editor as",
|
|
23897
24428
|
confirmLabel: "Save",
|
|
24429
|
+
inputLabel: "File path",
|
|
23898
24430
|
});
|
|
23899
|
-
if (
|
|
23900
|
-
|
|
23901
|
-
|
|
23902
|
-
|
|
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
|
+
}
|
|
23903
24440
|
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
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",
|
|
23907
24454
|
path,
|
|
23908
|
-
content,
|
|
23909
|
-
|
|
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
|
+
}
|
|
23910
24477
|
|
|
23911
|
-
|
|
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 })) {
|
|
23912
24498
|
pendingRequestId = null;
|
|
23913
24499
|
pendingKind = null;
|
|
24500
|
+
stickyStudioKind = null;
|
|
23914
24501
|
setBusy(false);
|
|
24502
|
+
setWsState("Ready");
|
|
24503
|
+
return false;
|
|
23915
24504
|
}
|
|
23916
|
-
|
|
23917
|
-
|
|
23918
|
-
saveOverBtn.addEventListener("click", async () => {
|
|
23919
|
-
var effectivePath = getEffectiveSavePath();
|
|
23920
|
-
if (!effectivePath) {
|
|
23921
|
-
setStatus("Save editor requires a file path. Open via /studio <path>, set a working dir, or use Save editor as…", "warning");
|
|
23922
|
-
return;
|
|
23923
|
-
}
|
|
24505
|
+
return true;
|
|
24506
|
+
}
|
|
23924
24507
|
|
|
23925
|
-
|
|
23926
|
-
|
|
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",
|
|
23927
24536
|
confirmLabel: "Overwrite",
|
|
24537
|
+
confirmDisabled: !canOverwrite,
|
|
23928
24538
|
destructive: true,
|
|
23929
24539
|
});
|
|
23930
|
-
if (
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
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
|
+
}
|
|
23934
24559
|
|
|
23935
|
-
|
|
23936
|
-
const
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
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 : "",
|
|
23940
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,
|
|
23941
24594
|
});
|
|
23942
|
-
|
|
23943
|
-
|
|
23944
|
-
|
|
23945
|
-
pendingKind = null;
|
|
23946
|
-
setBusy(false);
|
|
24595
|
+
if (decision === "choose-another") {
|
|
24596
|
+
await openEditorSaveAsDialog({ content: operation.content, suggestedPath: conflictPath, reportCancellation: true });
|
|
24597
|
+
return;
|
|
23947
24598
|
}
|
|
23948
|
-
|
|
23949
|
-
|
|
23950
|
-
|
|
23951
|
-
|
|
23952
|
-
|
|
23953
|
-
|
|
23954
|
-
return;
|
|
23955
|
-
}
|
|
23956
|
-
|
|
23957
|
-
if (editorDiffersFromFileBackedBaseline()) {
|
|
23958
|
-
const confirmed = await requestStudioConfirmation(
|
|
23959
|
-
"Replace current editor contents with the latest version from disk?",
|
|
23960
|
-
{ title: "Refresh from disk?", confirmLabel: "Replace", destructive: true },
|
|
23961
|
-
);
|
|
23962
|
-
if (!confirmed) return;
|
|
23963
|
-
}
|
|
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
|
+
}
|
|
23964
24605
|
|
|
23965
|
-
|
|
23966
|
-
|
|
24606
|
+
saveAsBtn.addEventListener("click", () => {
|
|
24607
|
+
void openEditorSaveAsDialog();
|
|
24608
|
+
});
|
|
23967
24609
|
|
|
23968
|
-
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
});
|
|
24610
|
+
saveOverBtn.addEventListener("click", () => {
|
|
24611
|
+
if (uiBusy) return;
|
|
24612
|
+
sendEditorSaveOverRequest();
|
|
24613
|
+
});
|
|
23973
24614
|
|
|
23974
|
-
|
|
23975
|
-
|
|
23976
|
-
|
|
23977
|
-
setBusy(false);
|
|
23978
|
-
}
|
|
24615
|
+
if (refreshFromDiskBtn) {
|
|
24616
|
+
refreshFromDiskBtn.addEventListener("click", () => {
|
|
24617
|
+
void requestEditorRefreshFromDisk();
|
|
23979
24618
|
});
|
|
23980
24619
|
}
|
|
23981
24620
|
|
|
@@ -24709,7 +25348,16 @@
|
|
|
24709
25348
|
resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
|
|
24710
25349
|
}
|
|
24711
25350
|
setSourceState(initialSourceState);
|
|
24712
|
-
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
|
+
}
|
|
24713
25361
|
refreshResponseUi();
|
|
24714
25362
|
updateAnnotatedReplyHeaderButton();
|
|
24715
25363
|
setActivePane(initialPaneFocusTarget === "off" ? "left" : initialPaneFocusTarget);
|
|
@@ -24722,7 +25370,9 @@
|
|
|
24722
25370
|
|
|
24723
25371
|
const initialDetectedLang = detectLanguageFromName(initialSourceState.path || initialSourceState.label || "");
|
|
24724
25372
|
const storedLang = readStoredEditorLanguage();
|
|
24725
|
-
setEditorLanguage(initialDetectedLang || storedLang || "markdown"
|
|
25373
|
+
setEditorLanguage(initialDetectedLang || storedLang || "markdown", {
|
|
25374
|
+
allowWatchedFileUpdate: isWatchedFilePreview,
|
|
25375
|
+
});
|
|
24726
25376
|
|
|
24727
25377
|
const storedLineNumbersEnabled = readStoredEditorLineNumbersEnabled();
|
|
24728
25378
|
const initialLineNumbersEnabled = storedLineNumbersEnabled ?? Boolean(lineNumbersSelect && lineNumbersSelect.value === "on");
|