pi-studio 0.9.53 → 0.9.55
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 +23 -0
- package/README.md +8 -3
- package/ROADMAP.md +12 -4
- package/client/studio-client.js +890 -166
- package/client/studio.css +191 -5
- package/index.ts +517 -147
- package/package.json +1 -1
- package/shared/studio-disk-revisions.js +558 -0
- package/shared/studio-file-watcher.js +181 -0
- package/shared/studio-side-question.js +32 -3
- package/shared/studio-workspace-state.js +4 -0
package/client/studio-client.js
CHANGED
|
@@ -74,7 +74,9 @@
|
|
|
74
74
|
: null;
|
|
75
75
|
const referenceBadgeEl = document.getElementById("referenceBadge");
|
|
76
76
|
const editorViewSelect = document.getElementById("editorViewSelect");
|
|
77
|
+
const editorViewSelectWrap = document.getElementById("editorViewSelectWrap");
|
|
77
78
|
const rightViewSelect = document.getElementById("rightViewSelect");
|
|
79
|
+
const rightViewSelectWrap = document.getElementById("rightViewSelectWrap");
|
|
78
80
|
const followSelect = document.getElementById("followSelect");
|
|
79
81
|
const responseHighlightSelect = document.getElementById("responseHighlightSelect");
|
|
80
82
|
const responseFontSizeSelect = document.getElementById("responseFontSizeSelect");
|
|
@@ -155,6 +157,7 @@
|
|
|
155
157
|
const shortcutsCloseBtn = document.getElementById("shortcutsCloseBtn");
|
|
156
158
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
157
159
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
160
|
+
const watchedOpenEditableBtn = document.getElementById("watchedOpenEditableBtn");
|
|
158
161
|
const reviewNotesBtn = document.getElementById("reviewNotesBtn");
|
|
159
162
|
const outlineBtn = document.getElementById("outlineBtn");
|
|
160
163
|
const scratchpadBtn = document.getElementById("scratchpadBtn");
|
|
@@ -192,6 +195,7 @@
|
|
|
192
195
|
? "editor-only"
|
|
193
196
|
: "full";
|
|
194
197
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
198
|
+
const isWatchedFilePreview = Boolean(document.body && document.body.dataset && document.body.dataset.watchedFilePreview === "1");
|
|
195
199
|
const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
|
|
196
200
|
const EDITOR_ONLY_RIGHT_VIEW_ALLOWED = new Set(["editor-preview", "editor-quarto-preview", "files", "changes", "repl", "side-questions"]);
|
|
197
201
|
const RIGHT_VIEW_LABELS = {
|
|
@@ -282,6 +286,16 @@
|
|
|
282
286
|
};
|
|
283
287
|
const initialResourceDir = initialQueryParams.get("resourceDir")
|
|
284
288
|
|| ((document.body && document.body.dataset && document.body.dataset.initialResourceDir) || "");
|
|
289
|
+
const initialDiskRevision = (document.body && document.body.dataset && document.body.dataset.initialDiskRevision) || "";
|
|
290
|
+
let watchedFilePreviewState = {
|
|
291
|
+
enabled: isWatchedFilePreview,
|
|
292
|
+
path: isWatchedFilePreview && initialSourceState.path ? initialSourceState.path : "",
|
|
293
|
+
diskRevision: isWatchedFilePreview ? initialDiskRevision : "",
|
|
294
|
+
generation: 0,
|
|
295
|
+
lastError: "",
|
|
296
|
+
renderError: "",
|
|
297
|
+
};
|
|
298
|
+
const watchedFilePreviewReadingPositions = { source: null, response: null };
|
|
285
299
|
|
|
286
300
|
let ws = null;
|
|
287
301
|
let wsState = "Connecting";
|
|
@@ -333,6 +347,7 @@
|
|
|
333
347
|
let studioDecisionMessageEl = null;
|
|
334
348
|
let studioDecisionInputEl = null;
|
|
335
349
|
let studioDecisionCancelBtn = null;
|
|
350
|
+
let studioDecisionTertiaryBtn = null;
|
|
336
351
|
let studioDecisionSecondaryBtn = null;
|
|
337
352
|
let studioDecisionConfirmBtn = null;
|
|
338
353
|
let studioDecisionState = null;
|
|
@@ -340,6 +355,7 @@
|
|
|
340
355
|
let pendingRequestId = null;
|
|
341
356
|
let pendingKind = null;
|
|
342
357
|
let stickyStudioKind = null;
|
|
358
|
+
const pendingSaveOperations = new Map();
|
|
343
359
|
const pendingCompanionLaunches = new Map();
|
|
344
360
|
const activeStudioTabLaunches = new Set();
|
|
345
361
|
let sourceOriginSummaryEl = null;
|
|
@@ -369,6 +385,7 @@
|
|
|
369
385
|
|
|
370
386
|
function normalizeRightViewValue(nextView) {
|
|
371
387
|
const normalized = canonicalRightViewValue(nextView);
|
|
388
|
+
if (isWatchedFilePreview && normalized !== "editor-preview") return "editor-preview";
|
|
372
389
|
if (normalized === "editor-quarto-preview" && !isCurrentStudioQuartoDocument()) {
|
|
373
390
|
return "editor-preview";
|
|
374
391
|
}
|
|
@@ -380,6 +397,7 @@
|
|
|
380
397
|
|
|
381
398
|
function isRightViewAvailableInCurrentMode(view) {
|
|
382
399
|
const normalized = canonicalRightViewValue(view);
|
|
400
|
+
if (isWatchedFilePreview) return normalized === "editor-preview";
|
|
383
401
|
if (normalized === "editor-quarto-preview" && !isCurrentStudioQuartoDocument()) return false;
|
|
384
402
|
return !isEditorOnlyMode || EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(normalized);
|
|
385
403
|
}
|
|
@@ -399,12 +417,17 @@
|
|
|
399
417
|
Array.from(rightViewSelect.options).forEach((option) => {
|
|
400
418
|
if (!option) return;
|
|
401
419
|
const isQuartoOption = option.value === "editor-quarto-preview";
|
|
420
|
+
if (isWatchedFilePreview && option.value === "editor-preview") option.textContent = "Watched preview";
|
|
402
421
|
if (isQuartoOption) option.hidden = !quartoRelevant;
|
|
403
|
-
option.disabled = (
|
|
422
|
+
option.disabled = (isWatchedFilePreview && option.value !== "editor-preview")
|
|
423
|
+
|| (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value))
|
|
424
|
+
|| (isQuartoOption && !quartoRelevant);
|
|
404
425
|
});
|
|
405
|
-
rightViewSelect.title =
|
|
426
|
+
rightViewSelect.title = isWatchedFilePreview
|
|
427
|
+
? "Read-only watched preview follows this file on disk."
|
|
428
|
+
: (isEditorOnlyMode
|
|
406
429
|
? "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.";
|
|
430
|
+
: "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
431
|
}
|
|
409
432
|
|
|
410
433
|
function getInitialRightView(source) {
|
|
@@ -465,6 +488,7 @@
|
|
|
465
488
|
const EDITOR_TAB_TEXT = " ";
|
|
466
489
|
const QUIZ_DEFAULT_COUNT = 5;
|
|
467
490
|
const SIDE_QUESTION_THINKING_STORAGE_KEY = "piStudio.sideQuestionThinking";
|
|
491
|
+
const SIDE_QUESTION_THINKING_LEVELS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
468
492
|
const SIDE_QUESTION_GATHER_STORAGE_KEY = "piStudio.sideQuestionGatherScope";
|
|
469
493
|
const SIDE_QUESTION_TOOLS_STORAGE_KEY = "piStudio.sideQuestionTools";
|
|
470
494
|
const SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS = 400_000;
|
|
@@ -475,6 +499,7 @@
|
|
|
475
499
|
const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
|
|
476
500
|
const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
477
501
|
let sideQuestionState = null;
|
|
502
|
+
let sideQuestionThinkingLevels = [...SIDE_QUESTION_THINKING_LEVELS];
|
|
478
503
|
let sideQuestionWebSearchAvailable = false;
|
|
479
504
|
let sideQuestionAvailablePiTools = [];
|
|
480
505
|
let sideQuestionPreviewRenderNonce = 0;
|
|
@@ -503,11 +528,12 @@
|
|
|
503
528
|
thinking: (() => {
|
|
504
529
|
try {
|
|
505
530
|
const value = window.localStorage && window.localStorage.getItem(SIDE_QUESTION_THINKING_STORAGE_KEY);
|
|
506
|
-
return
|
|
531
|
+
return SIDE_QUESTION_THINKING_LEVELS.includes(value) ? value : "low";
|
|
507
532
|
} catch { return "low"; }
|
|
508
533
|
})(),
|
|
509
534
|
draft: "",
|
|
510
535
|
};
|
|
536
|
+
let sideQuestionPreferredThinking = sideQuestionUi.thinking;
|
|
511
537
|
let quizOverlayEl = null;
|
|
512
538
|
let quizDialogEl = null;
|
|
513
539
|
let quizPreviewRenderNonce = 0;
|
|
@@ -2164,6 +2190,7 @@
|
|
|
2164
2190
|
actionRequestId: null,
|
|
2165
2191
|
};
|
|
2166
2192
|
let fileBackedBaselineText = null;
|
|
2193
|
+
let fileBackedDiskRevision = null;
|
|
2167
2194
|
let activePane = initialPaneFocusTarget === "right" ? "right" : "left";
|
|
2168
2195
|
let paneFocusTarget = initialPaneFocusTarget;
|
|
2169
2196
|
let paneSplitPercent = 50;
|
|
@@ -2892,9 +2919,28 @@
|
|
|
2892
2919
|
}
|
|
2893
2920
|
titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
2894
2921
|
if (isEditorOnlyMode) {
|
|
2895
|
-
|
|
2922
|
+
const staticTitleEl = makeStudioUiRefreshElement(
|
|
2923
|
+
"span",
|
|
2924
|
+
"studio-refresh-static-title",
|
|
2925
|
+
isWatchedFilePreview ? "Source" : "Editor (Raw)",
|
|
2926
|
+
);
|
|
2927
|
+
if (isWatchedFilePreview) {
|
|
2928
|
+
staticTitleEl.classList.add("studio-watched-source-title");
|
|
2929
|
+
staticTitleEl.setAttribute("aria-label", "Source, read-only. Follows the file on disk.");
|
|
2930
|
+
staticTitleEl.title = "This source is read-only and follows the file on disk.";
|
|
2931
|
+
}
|
|
2932
|
+
titleGroupEl.appendChild(staticTitleEl);
|
|
2933
|
+
if (isWatchedFilePreview) {
|
|
2934
|
+
const readOnlyBadgeEl = makeStudioUiRefreshElement(
|
|
2935
|
+
"span",
|
|
2936
|
+
"studio-watched-source-read-only-badge",
|
|
2937
|
+
"Read-only · follows disk",
|
|
2938
|
+
);
|
|
2939
|
+
readOnlyBadgeEl.setAttribute("aria-hidden", "true");
|
|
2940
|
+
titleGroupEl.appendChild(readOnlyBadgeEl);
|
|
2941
|
+
}
|
|
2896
2942
|
} else if (editorViewSelect) {
|
|
2897
|
-
titleGroupEl.appendChild(editorViewSelect);
|
|
2943
|
+
titleGroupEl.appendChild(editorViewSelectWrap || editorViewSelect);
|
|
2898
2944
|
}
|
|
2899
2945
|
if (contextMenu) {
|
|
2900
2946
|
titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
@@ -2920,9 +2966,10 @@
|
|
|
2920
2966
|
rightTitleGroupEl.appendChild(rightFocusBtn);
|
|
2921
2967
|
rightTitleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
2922
2968
|
}
|
|
2923
|
-
rightTitleGroupEl.appendChild(rightViewSelect);
|
|
2969
|
+
rightTitleGroupEl.appendChild(rightViewSelectWrap || rightViewSelect);
|
|
2924
2970
|
rightIdentityEl.appendChild(rightTitleGroupEl);
|
|
2925
2971
|
const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
|
|
2972
|
+
if (watchedOpenEditableBtn && isWatchedFilePreview) rightToolsEl.appendChild(watchedOpenEditableBtn);
|
|
2926
2973
|
if (exportPreviewControlsEl) {
|
|
2927
2974
|
rightToolsEl.appendChild(exportPreviewControlsEl);
|
|
2928
2975
|
} else if (exportPdfBtn) {
|
|
@@ -3592,6 +3639,9 @@
|
|
|
3592
3639
|
if (typeof message.thinkingLevel === "string") {
|
|
3593
3640
|
piThinkingLevel = message.thinkingLevel.trim();
|
|
3594
3641
|
}
|
|
3642
|
+
if (Array.isArray(message.sideQuestionThinkingLevels)) {
|
|
3643
|
+
applySideQuestionThinkingLevels(message.sideQuestionThinkingLevels);
|
|
3644
|
+
}
|
|
3595
3645
|
renderFooterModelMenu();
|
|
3596
3646
|
}
|
|
3597
3647
|
|
|
@@ -3626,8 +3676,8 @@
|
|
|
3626
3676
|
});
|
|
3627
3677
|
footerModelMenuEl.innerHTML = ""
|
|
3628
3678
|
+ "<div class='footer-model-menu-heading'>Pi model & thinking</div>"
|
|
3629
|
-
+ "<label class='footer-model-menu-field'><span>Pi model</span><select id='footerPiModelSelect'>" + modelOptionsHtml.join("") + "</select></label>"
|
|
3630
|
-
+ "<label class='footer-model-menu-field'><span>Thinking</span><select id='footerPiThinkingSelect'>" + thinkingOptionsHtml.join("") + "</select></label>"
|
|
3679
|
+
+ "<label class='footer-model-menu-field'><span>Pi model</span><span class='studio-menu-select-wrap'><select id='footerPiModelSelect'>" + modelOptionsHtml.join("") + "</select></span></label>"
|
|
3680
|
+
+ "<label class='footer-model-menu-field'><span>Thinking</span><span class='studio-menu-select-wrap'><select id='footerPiThinkingSelect'>" + thinkingOptionsHtml.join("") + "</select></span></label>"
|
|
3631
3681
|
+ "<div class='footer-model-menu-note'>Affects future Pi turns. Studio Suggest has its own model setting.</div>";
|
|
3632
3682
|
}
|
|
3633
3683
|
|
|
@@ -3707,7 +3757,7 @@
|
|
|
3707
3757
|
footerThemeMenuEl.innerHTML = ""
|
|
3708
3758
|
+ "<div class='footer-model-menu-heading'>Pi theme</div>"
|
|
3709
3759
|
+ (optionsHtml.length
|
|
3710
|
-
? "<label class='footer-model-menu-field'><span>Active theme</span><select id='footerPiThemeSelect'>" + optionsHtml.join("") + "</select></label>"
|
|
3760
|
+
? "<label class='footer-model-menu-field'><span>Active theme</span><span class='studio-menu-select-wrap'><select id='footerPiThemeSelect'>" + optionsHtml.join("") + "</select></span></label>"
|
|
3711
3761
|
: "<div class='footer-model-menu-note'>No Pi themes are available yet.</div>")
|
|
3712
3762
|
+ "<div class='footer-model-menu-note'>Switches the active Pi theme and persists it to Pi settings.</div>";
|
|
3713
3763
|
}
|
|
@@ -3846,7 +3896,7 @@
|
|
|
3846
3896
|
}
|
|
3847
3897
|
|
|
3848
3898
|
function getStudioDecisionFocusableElements() {
|
|
3849
|
-
return [studioDecisionInputEl, studioDecisionCancelBtn, studioDecisionSecondaryBtn, studioDecisionConfirmBtn]
|
|
3899
|
+
return [studioDecisionInputEl, studioDecisionCancelBtn, studioDecisionTertiaryBtn, studioDecisionSecondaryBtn, studioDecisionConfirmBtn]
|
|
3850
3900
|
.filter((element) => element && !element.hidden && !element.disabled);
|
|
3851
3901
|
}
|
|
3852
3902
|
|
|
@@ -3894,6 +3944,30 @@
|
|
|
3894
3944
|
cancelBtn.addEventListener("click", () => finishStudioDecision(null));
|
|
3895
3945
|
actions.appendChild(cancelBtn);
|
|
3896
3946
|
|
|
3947
|
+
const tertiaryBtn = document.createElement("button");
|
|
3948
|
+
tertiaryBtn.type = "button";
|
|
3949
|
+
tertiaryBtn.className = "studio-decision-tertiary";
|
|
3950
|
+
tertiaryBtn.hidden = true;
|
|
3951
|
+
tertiaryBtn.addEventListener("click", () => {
|
|
3952
|
+
const state = studioDecisionState;
|
|
3953
|
+
const handler = state && state.onTertiary;
|
|
3954
|
+
if (typeof handler !== "function") {
|
|
3955
|
+
if (state && state.hasTertiaryValue) finishStudioDecision(state.tertiaryValue);
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
try {
|
|
3959
|
+
const result = handler();
|
|
3960
|
+
if (result && typeof result.catch === "function") {
|
|
3961
|
+
result.catch((error) => {
|
|
3962
|
+
setStatus("Action failed: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
setStatus("Action failed: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3967
|
+
}
|
|
3968
|
+
});
|
|
3969
|
+
actions.appendChild(tertiaryBtn);
|
|
3970
|
+
|
|
3897
3971
|
const secondaryBtn = document.createElement("button");
|
|
3898
3972
|
secondaryBtn.type = "button";
|
|
3899
3973
|
secondaryBtn.className = "studio-decision-secondary";
|
|
@@ -3964,6 +4038,7 @@
|
|
|
3964
4038
|
studioDecisionMessageEl = message;
|
|
3965
4039
|
studioDecisionInputEl = input;
|
|
3966
4040
|
studioDecisionCancelBtn = cancelBtn;
|
|
4041
|
+
studioDecisionTertiaryBtn = tertiaryBtn;
|
|
3967
4042
|
studioDecisionSecondaryBtn = secondaryBtn;
|
|
3968
4043
|
studioDecisionConfirmBtn = confirmBtn;
|
|
3969
4044
|
return overlay;
|
|
@@ -3976,6 +4051,7 @@
|
|
|
3976
4051
|
if (studioDecisionState) finishStudioDecision(null, false);
|
|
3977
4052
|
const returnFocusEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
3978
4053
|
|
|
4054
|
+
const tertiaryLabel = String(settings.tertiaryLabel || "").trim();
|
|
3979
4055
|
const secondaryLabel = String(settings.secondaryLabel || "").trim();
|
|
3980
4056
|
studioDecisionTitleEl.textContent = String(settings.title || (mode === "prompt" ? "Enter a value" : "Confirm action"));
|
|
3981
4057
|
studioDecisionMessageEl.textContent = String(settings.message || "");
|
|
@@ -3984,9 +4060,13 @@
|
|
|
3984
4060
|
studioDecisionInputEl.placeholder = mode === "prompt" ? String(settings.placeholder || "") : "";
|
|
3985
4061
|
studioDecisionInputEl.setAttribute("aria-label", String(settings.inputLabel || "Value"));
|
|
3986
4062
|
studioDecisionCancelBtn.textContent = String(settings.cancelLabel || "Cancel");
|
|
4063
|
+
studioDecisionTertiaryBtn.hidden = !tertiaryLabel;
|
|
4064
|
+
studioDecisionTertiaryBtn.disabled = settings.tertiaryDisabled === true;
|
|
4065
|
+
studioDecisionTertiaryBtn.textContent = tertiaryLabel;
|
|
3987
4066
|
studioDecisionSecondaryBtn.hidden = !secondaryLabel;
|
|
3988
4067
|
studioDecisionSecondaryBtn.disabled = settings.secondaryDisabled === true;
|
|
3989
4068
|
studioDecisionSecondaryBtn.textContent = secondaryLabel;
|
|
4069
|
+
studioDecisionConfirmBtn.disabled = settings.confirmDisabled === true;
|
|
3990
4070
|
studioDecisionConfirmBtn.textContent = String(settings.confirmLabel || (mode === "prompt" ? "Continue" : "Confirm"));
|
|
3991
4071
|
studioDecisionConfirmBtn.classList.toggle("is-destructive", settings.destructive === true);
|
|
3992
4072
|
studioDecisionDialogEl.classList.toggle("is-destructive", settings.destructive === true);
|
|
@@ -3998,6 +4078,9 @@
|
|
|
3998
4078
|
mode,
|
|
3999
4079
|
resolve,
|
|
4000
4080
|
returnFocusEl,
|
|
4081
|
+
onTertiary: typeof settings.onTertiary === "function" ? settings.onTertiary : null,
|
|
4082
|
+
hasTertiaryValue: Object.prototype.hasOwnProperty.call(settings, "tertiaryValue"),
|
|
4083
|
+
tertiaryValue: settings.tertiaryValue,
|
|
4001
4084
|
onSecondary: typeof settings.onSecondary === "function" ? settings.onSecondary : null,
|
|
4002
4085
|
hasSecondaryValue: Object.prototype.hasOwnProperty.call(settings, "secondaryValue"),
|
|
4003
4086
|
secondaryValue: settings.secondaryValue,
|
|
@@ -4059,12 +4142,20 @@
|
|
|
4059
4142
|
}
|
|
4060
4143
|
});
|
|
4061
4144
|
|
|
4062
|
-
function
|
|
4145
|
+
function normalizeStudioDiskRevision(value) {
|
|
4146
|
+
const revision = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
4147
|
+
return /^sha256:[a-f0-9]{64}$/.test(revision) ? revision : null;
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
function markFileBackedBaseline(text, diskRevision) {
|
|
4063
4151
|
fileBackedBaselineText = String(text || "");
|
|
4152
|
+
fileBackedDiskRevision = normalizeStudioDiskRevision(diskRevision);
|
|
4153
|
+
scheduleWorkspacePersistence();
|
|
4064
4154
|
}
|
|
4065
4155
|
|
|
4066
4156
|
function clearFileBackedBaseline() {
|
|
4067
4157
|
fileBackedBaselineText = null;
|
|
4158
|
+
fileBackedDiskRevision = null;
|
|
4068
4159
|
}
|
|
4069
4160
|
|
|
4070
4161
|
function hasRefreshableFilePath() {
|
|
@@ -4079,13 +4170,17 @@
|
|
|
4079
4170
|
|
|
4080
4171
|
function updateSourceBadge() {
|
|
4081
4172
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
4082
|
-
const originText =
|
|
4173
|
+
const originText = isWatchedFilePreview
|
|
4174
|
+
? ("Watching: " + label + " · read-only" + (watchedFilePreviewState.lastError || watchedFilePreviewState.renderError ? " · last good preview" : ""))
|
|
4175
|
+
: ((studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label + (hasRefreshableFilePath() ? " · file" : ""));
|
|
4083
4176
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4084
4177
|
if (sourceBadgeEl) {
|
|
4085
4178
|
sourceBadgeEl.textContent = originText;
|
|
4086
|
-
sourceBadgeEl.title =
|
|
4179
|
+
sourceBadgeEl.title = isWatchedFilePreview
|
|
4180
|
+
? ("Read-only watched file: " + (descriptor.label || label) + "\nStudio follows settled disk changes and keeps the last good rendered preview through temporary failures.")
|
|
4181
|
+
: (descriptor.fileBacked
|
|
4087
4182
|
? ("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.");
|
|
4183
|
+
: ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes."));
|
|
4089
4184
|
}
|
|
4090
4185
|
if (sourceOriginSummaryEl) {
|
|
4091
4186
|
sourceOriginSummaryEl.textContent = originText;
|
|
@@ -4291,7 +4386,9 @@
|
|
|
4291
4386
|
].forEach(([btn, pane]) => {
|
|
4292
4387
|
if (!btn) return;
|
|
4293
4388
|
const isFocusedPane = paneFocusTarget === pane;
|
|
4294
|
-
const paneName = pane === "right"
|
|
4389
|
+
const paneName = pane === "right"
|
|
4390
|
+
? (isWatchedFilePreview ? "watched preview" : "response")
|
|
4391
|
+
: (isWatchedFilePreview ? "read-only source" : "editor");
|
|
4295
4392
|
btn.classList.toggle("is-active", isFocusedPane);
|
|
4296
4393
|
btn.setAttribute("aria-pressed", isFocusedPane ? "true" : "false");
|
|
4297
4394
|
btn.textContent = isFocusedPane ? "Exit focus" : "Focus pane";
|
|
@@ -4544,10 +4641,14 @@
|
|
|
4544
4641
|
}
|
|
4545
4642
|
|
|
4546
4643
|
function triggerEditorSaveShortcut() {
|
|
4547
|
-
if (saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
|
|
4644
|
+
if (hasRefreshableFilePath() && saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
|
|
4548
4645
|
saveOverBtn.click();
|
|
4549
4646
|
return true;
|
|
4550
4647
|
}
|
|
4648
|
+
return triggerEditorSaveAsShortcut();
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
function triggerEditorSaveAsShortcut() {
|
|
4551
4652
|
if (saveAsBtn && !saveAsBtn.disabled && !saveAsBtn.hidden) {
|
|
4552
4653
|
saveAsBtn.click();
|
|
4553
4654
|
return true;
|
|
@@ -4890,6 +4991,22 @@
|
|
|
4890
4991
|
return;
|
|
4891
4992
|
}
|
|
4892
4993
|
|
|
4994
|
+
const isSaveAsShortcut =
|
|
4995
|
+
key.toLowerCase() === "s"
|
|
4996
|
+
&& (event.metaKey || event.ctrlKey)
|
|
4997
|
+
&& !event.altKey
|
|
4998
|
+
&& event.shiftKey;
|
|
4999
|
+
|
|
5000
|
+
if (isSaveAsShortcut) {
|
|
5001
|
+
event.preventDefault();
|
|
5002
|
+
if (isWatchedFilePreview) {
|
|
5003
|
+
setStatus("This preview is read-only. Open a file tab to edit or save a copy.", "warning");
|
|
5004
|
+
return;
|
|
5005
|
+
}
|
|
5006
|
+
triggerEditorSaveAsShortcut();
|
|
5007
|
+
return;
|
|
5008
|
+
}
|
|
5009
|
+
|
|
4893
5010
|
const isSaveShortcut =
|
|
4894
5011
|
key.toLowerCase() === "s"
|
|
4895
5012
|
&& (event.metaKey || event.ctrlKey)
|
|
@@ -4898,6 +5015,10 @@
|
|
|
4898
5015
|
|
|
4899
5016
|
if (isSaveShortcut) {
|
|
4900
5017
|
event.preventDefault();
|
|
5018
|
+
if (isWatchedFilePreview) {
|
|
5019
|
+
setStatus("This preview follows disk and cannot save. Open a file tab to edit safely.", "warning");
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
4901
5022
|
triggerEditorSaveShortcut();
|
|
4902
5023
|
return;
|
|
4903
5024
|
}
|
|
@@ -5644,6 +5765,8 @@
|
|
|
5644
5765
|
clearPreviewJumpHighlight(targetEl);
|
|
5645
5766
|
finishPreviewRender(targetEl);
|
|
5646
5767
|
targetEl.innerHTML = html;
|
|
5768
|
+
clearWatchedPreviewRenderError();
|
|
5769
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, text);
|
|
5647
5770
|
if (pane === "response") {
|
|
5648
5771
|
applyPendingResponseScrollReset();
|
|
5649
5772
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -7123,6 +7246,8 @@
|
|
|
7123
7246
|
});
|
|
7124
7247
|
|
|
7125
7248
|
targetEl.appendChild(shell);
|
|
7249
|
+
clearWatchedPreviewRenderError();
|
|
7250
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, html);
|
|
7126
7251
|
|
|
7127
7252
|
if (pane === "response") {
|
|
7128
7253
|
applyPendingResponseScrollReset();
|
|
@@ -9364,11 +9489,119 @@
|
|
|
9364
9489
|
|
|
9365
9490
|
function hasMeaningfulPreviewContent(targetEl) {
|
|
9366
9491
|
if (!targetEl || typeof targetEl.querySelector !== "function") return false;
|
|
9492
|
+
if (targetEl.dataset && targetEl.dataset.studioPreviewCommitted === "1") return true;
|
|
9367
9493
|
if (targetEl.querySelector(".preview-loading")) return false;
|
|
9368
9494
|
const text = typeof targetEl.textContent === "string" ? targetEl.textContent.trim() : "";
|
|
9369
9495
|
return text.length > 0;
|
|
9370
9496
|
}
|
|
9371
9497
|
|
|
9498
|
+
function getWatchedPreviewAnchorSignature(element) {
|
|
9499
|
+
if (!element || !element.tagName) return "";
|
|
9500
|
+
const text = String(element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 180);
|
|
9501
|
+
if (!text) return "";
|
|
9502
|
+
return String(element.tagName).toLowerCase() + ":" + text;
|
|
9503
|
+
}
|
|
9504
|
+
|
|
9505
|
+
function captureWatchedPreviewReadingPosition(targetEl) {
|
|
9506
|
+
if (!isWatchedFilePreview || !targetEl || typeof targetEl.querySelectorAll !== "function") return null;
|
|
9507
|
+
const maxScroll = Math.max(0, Number(targetEl.scrollHeight || 0) - Number(targetEl.clientHeight || 0));
|
|
9508
|
+
const ratio = maxScroll > 0 ? Math.max(0, Math.min(1, Number(targetEl.scrollTop || 0) / maxScroll)) : 0;
|
|
9509
|
+
if (typeof targetEl.getBoundingClientRect !== "function") return { ratio };
|
|
9510
|
+
const targetRect = targetEl.getBoundingClientRect();
|
|
9511
|
+
const anchorLine = Number(targetRect.top || 0) + Math.max(20, Math.min(Number(targetEl.clientHeight || 0) * 0.22, 140));
|
|
9512
|
+
const candidates = Array.from(targetEl.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,pre,table,blockquote,figure"));
|
|
9513
|
+
let anchor = null;
|
|
9514
|
+
for (const candidate of candidates) {
|
|
9515
|
+
if (!candidate || typeof candidate.getBoundingClientRect !== "function") continue;
|
|
9516
|
+
const rect = candidate.getBoundingClientRect();
|
|
9517
|
+
if (Number(rect.bottom || rect.top || 0) >= anchorLine) {
|
|
9518
|
+
anchor = candidate;
|
|
9519
|
+
break;
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9522
|
+
if (!anchor && candidates.length) anchor = candidates[candidates.length - 1];
|
|
9523
|
+
const signature = getWatchedPreviewAnchorSignature(anchor);
|
|
9524
|
+
if (!anchor || !signature) return { ratio };
|
|
9525
|
+
let occurrence = 0;
|
|
9526
|
+
for (const candidate of candidates) {
|
|
9527
|
+
if (candidate === anchor) break;
|
|
9528
|
+
if (getWatchedPreviewAnchorSignature(candidate) === signature) occurrence += 1;
|
|
9529
|
+
}
|
|
9530
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
9531
|
+
return {
|
|
9532
|
+
ratio,
|
|
9533
|
+
signature,
|
|
9534
|
+
occurrence,
|
|
9535
|
+
offset: Number(anchorRect.top || 0) - Number(targetRect.top || 0),
|
|
9536
|
+
};
|
|
9537
|
+
}
|
|
9538
|
+
|
|
9539
|
+
function restoreWatchedPreviewReadingPosition(targetEl, snapshot) {
|
|
9540
|
+
if (!isWatchedFilePreview || !targetEl || !snapshot) return;
|
|
9541
|
+
const candidates = typeof targetEl.querySelectorAll === "function"
|
|
9542
|
+
? Array.from(targetEl.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,pre,table,blockquote,figure"))
|
|
9543
|
+
: [];
|
|
9544
|
+
const matching = snapshot.signature
|
|
9545
|
+
? candidates.filter((candidate) => getWatchedPreviewAnchorSignature(candidate) === snapshot.signature)
|
|
9546
|
+
: [];
|
|
9547
|
+
const anchor = matching[Math.max(0, Number(snapshot.occurrence) || 0)] || null;
|
|
9548
|
+
if (anchor && typeof anchor.getBoundingClientRect === "function" && typeof targetEl.getBoundingClientRect === "function") {
|
|
9549
|
+
const targetRect = targetEl.getBoundingClientRect();
|
|
9550
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
9551
|
+
const delta = (Number(anchorRect.top || 0) - Number(targetRect.top || 0)) - (Number(snapshot.offset) || 0);
|
|
9552
|
+
targetEl.scrollTop = Math.max(0, Number(targetEl.scrollTop || 0) + delta);
|
|
9553
|
+
return;
|
|
9554
|
+
}
|
|
9555
|
+
const maxScroll = Math.max(0, Number(targetEl.scrollHeight || 0) - Number(targetEl.clientHeight || 0));
|
|
9556
|
+
targetEl.scrollTop = Math.max(0, Math.min(maxScroll, maxScroll * Math.max(0, Math.min(1, Number(snapshot.ratio) || 0))));
|
|
9557
|
+
}
|
|
9558
|
+
|
|
9559
|
+
function scheduleWatchedPreviewReadingPositionRestore(targetEl, renderedText) {
|
|
9560
|
+
if (!isWatchedFilePreview || !targetEl) return;
|
|
9561
|
+
const pane = targetEl === sourcePreviewEl ? "source" : (targetEl === critiqueViewEl ? "response" : "");
|
|
9562
|
+
if (!pane || !watchedFilePreviewReadingPositions[pane]) return;
|
|
9563
|
+
const pending = watchedFilePreviewReadingPositions[pane];
|
|
9564
|
+
if (String(pending.text || "") !== String(renderedText || "")) return;
|
|
9565
|
+
const snapshot = pending.snapshot;
|
|
9566
|
+
watchedFilePreviewReadingPositions[pane] = null;
|
|
9567
|
+
let restored = false;
|
|
9568
|
+
const applyRestore = () => {
|
|
9569
|
+
if (restored) return;
|
|
9570
|
+
restored = true;
|
|
9571
|
+
restoreWatchedPreviewReadingPosition(targetEl, snapshot);
|
|
9572
|
+
};
|
|
9573
|
+
if (typeof window.requestAnimationFrame === "function") {
|
|
9574
|
+
window.requestAnimationFrame(applyRestore);
|
|
9575
|
+
}
|
|
9576
|
+
// Hidden embedded/headless surfaces can suspend animation frames entirely.
|
|
9577
|
+
window.setTimeout(applyRestore, 80);
|
|
9578
|
+
}
|
|
9579
|
+
|
|
9580
|
+
function showWatchedPreviewRenderError(targetEl, message) {
|
|
9581
|
+
if (!isWatchedFilePreview || !targetEl || typeof targetEl.appendChild !== "function") return false;
|
|
9582
|
+
const existing = typeof targetEl.querySelector === "function" ? targetEl.querySelector(".studio-watched-preview-error") : null;
|
|
9583
|
+
if (existing && existing.remove) existing.remove();
|
|
9584
|
+
const notice = document.createElement("div");
|
|
9585
|
+
notice.className = "preview-warning studio-watched-preview-error";
|
|
9586
|
+
notice.setAttribute("role", "status");
|
|
9587
|
+
notice.appendChild(document.createTextNode("Could not render the latest disk revision; keeping the last good preview. " + String(message || "Preview renderer unavailable.") + " "));
|
|
9588
|
+
const retry = document.createElement("button");
|
|
9589
|
+
retry.type = "button";
|
|
9590
|
+
retry.textContent = "Retry";
|
|
9591
|
+
retry.addEventListener("click", () => renderActiveResult());
|
|
9592
|
+
notice.appendChild(retry);
|
|
9593
|
+
targetEl.appendChild(notice);
|
|
9594
|
+
return true;
|
|
9595
|
+
}
|
|
9596
|
+
|
|
9597
|
+
function clearWatchedPreviewRenderError() {
|
|
9598
|
+
if (!isWatchedFilePreview) return;
|
|
9599
|
+
const recovered = Boolean(watchedFilePreviewState.renderError);
|
|
9600
|
+
watchedFilePreviewState.renderError = "";
|
|
9601
|
+
updateSourceBadge();
|
|
9602
|
+
if (recovered) setStatus("Rendered the latest watched file revision.", "success");
|
|
9603
|
+
}
|
|
9604
|
+
|
|
9372
9605
|
function beginPreviewRender(targetEl) {
|
|
9373
9606
|
if (!targetEl || !targetEl.classList) return;
|
|
9374
9607
|
|
|
@@ -10695,6 +10928,28 @@
|
|
|
10695
10928
|
);
|
|
10696
10929
|
}
|
|
10697
10930
|
|
|
10931
|
+
function isCurrentStudioPreviewRender(pane, nonce) {
|
|
10932
|
+
if (pane === "source") {
|
|
10933
|
+
return nonce === sourcePreviewRenderNonce && editorView === "preview";
|
|
10934
|
+
}
|
|
10935
|
+
return nonce === responsePreviewRenderNonce && (rightView === "preview" || rightView === "editor-preview");
|
|
10936
|
+
}
|
|
10937
|
+
|
|
10938
|
+
function createStudioPreviewStagingElement(targetEl) {
|
|
10939
|
+
const staging = document.createElement("div");
|
|
10940
|
+
staging.className = String(targetEl && targetEl.className ? targetEl.className : "rendered-markdown") + " studio-preview-staging";
|
|
10941
|
+
staging.setAttribute("aria-hidden", "true");
|
|
10942
|
+
staging.style.width = Math.max(320, Number(targetEl && targetEl.clientWidth) || 0) + "px";
|
|
10943
|
+
document.body.appendChild(staging);
|
|
10944
|
+
return staging;
|
|
10945
|
+
}
|
|
10946
|
+
|
|
10947
|
+
function commitStudioPreviewStagingElement(targetEl, staging) {
|
|
10948
|
+
const nodes = Array.from(staging.childNodes || []);
|
|
10949
|
+
targetEl.replaceChildren(...nodes);
|
|
10950
|
+
staging.remove();
|
|
10951
|
+
}
|
|
10952
|
+
|
|
10698
10953
|
async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
|
|
10699
10954
|
const previewPrepared = annotationsEnabled
|
|
10700
10955
|
? prepareMarkdownForPandocPreview(markdown)
|
|
@@ -10705,35 +10960,44 @@
|
|
|
10705
10960
|
};
|
|
10706
10961
|
const pdfPrepared = prepareStudioPdfBlocksForPreview(previewPrepared.markdown);
|
|
10707
10962
|
const previewResourceContext = getHtmlPreviewResourceContextOptions();
|
|
10963
|
+
let staging = null;
|
|
10964
|
+
let previewCommitted = false;
|
|
10965
|
+
const stillCurrent = () => isCurrentStudioPreviewRender(pane, nonce);
|
|
10966
|
+
const abandonIfStale = () => {
|
|
10967
|
+
if (stillCurrent()) return false;
|
|
10968
|
+
if (staging && staging.remove) staging.remove();
|
|
10969
|
+
staging = null;
|
|
10970
|
+
return true;
|
|
10971
|
+
};
|
|
10708
10972
|
|
|
10709
10973
|
try {
|
|
10710
10974
|
const renderedHtml = await renderMarkdownWithPandoc(pdfPrepared.markdown, {
|
|
10711
10975
|
includeEditorLanguage: pane === "source" || rightView === "editor-preview",
|
|
10712
10976
|
resourceContext: previewResourceContext,
|
|
10713
10977
|
});
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
await renderPdfPreviewsInElement(targetEl);
|
|
10730
|
-
decoratePreviewPdfFigures(targetEl);
|
|
10978
|
+
if (abandonIfStale()) return;
|
|
10979
|
+
|
|
10980
|
+
staging = createStudioPreviewStagingElement(targetEl);
|
|
10981
|
+
staging.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
|
|
10982
|
+
await hydrateStudioPreviewLocalMedia(staging, previewResourceContext);
|
|
10983
|
+
if (abandonIfStale()) return;
|
|
10984
|
+
await renderStudioPdfBlocksInElement(staging, pdfPrepared.blocks, previewingEditorText);
|
|
10985
|
+
if (abandonIfStale()) return;
|
|
10986
|
+
applyPreviewAnnotationPlaceholdersToElement(staging, previewPrepared.placeholders);
|
|
10987
|
+
await renderAnnotationMathInElement(staging);
|
|
10988
|
+
if (abandonIfStale()) return;
|
|
10989
|
+
decoratePdfEmbeds(staging);
|
|
10990
|
+
await renderPdfPreviewsInElement(staging);
|
|
10991
|
+
if (abandonIfStale()) return;
|
|
10992
|
+
decoratePreviewPdfFigures(staging);
|
|
10731
10993
|
const annotationMode = (pane === "source" || pane === "response")
|
|
10732
10994
|
? (annotationsEnabled ? "highlight" : "hide")
|
|
10733
10995
|
: "none";
|
|
10734
|
-
applyAnnotationMarkersToElement(
|
|
10735
|
-
await renderMermaidInElement(
|
|
10736
|
-
|
|
10996
|
+
applyAnnotationMarkersToElement(staging, annotationMode);
|
|
10997
|
+
await renderMermaidInElement(staging);
|
|
10998
|
+
if (abandonIfStale()) return;
|
|
10999
|
+
await renderMathFallbackInElement(staging);
|
|
11000
|
+
if (abandonIfStale()) return;
|
|
10737
11001
|
|
|
10738
11002
|
const shouldDecoratePreviewComments = supportsPreviewCommentsForCurrentEditor()
|
|
10739
11003
|
&& (
|
|
@@ -10741,35 +11005,56 @@
|
|
|
10741
11005
|
|| (pane === "response" && rightView === "editor-preview")
|
|
10742
11006
|
);
|
|
10743
11007
|
if (shouldDecoratePreviewComments) {
|
|
10744
|
-
decorateRenderedEditorPreviewComments(
|
|
11008
|
+
decorateRenderedEditorPreviewComments(staging, sourceTextEl.value || "");
|
|
10745
11009
|
}
|
|
10746
|
-
decorateCopyablePreviewBlocks(
|
|
10747
|
-
decoratePreviewImages(
|
|
11010
|
+
decorateCopyablePreviewBlocks(staging);
|
|
11011
|
+
decoratePreviewImages(staging);
|
|
10748
11012
|
|
|
10749
|
-
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
11013
|
+
// Warn if relative images are present but unlikely to resolve (non-file-backed content).
|
|
10750
11014
|
if (!sourceState.path && !getCurrentResourceDirValue()) {
|
|
10751
11015
|
var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
|
|
10752
11016
|
var hasLatexImages = /\\includegraphics/.test(markdown || "");
|
|
10753
11017
|
if (hasRelativeImages || hasLatexImages) {
|
|
10754
|
-
appendPreviewNotice(
|
|
11018
|
+
appendPreviewNotice(staging, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
|
|
10755
11019
|
}
|
|
10756
11020
|
}
|
|
11021
|
+
if (abandonIfStale()) return;
|
|
10757
11022
|
|
|
11023
|
+
clearPreviewJumpHighlight(targetEl);
|
|
11024
|
+
finishPreviewRender(targetEl);
|
|
11025
|
+
commitStudioPreviewStagingElement(targetEl, staging);
|
|
11026
|
+
staging = null;
|
|
11027
|
+
if (targetEl.dataset) targetEl.dataset.studioPreviewCommitted = "1";
|
|
11028
|
+
previewCommitted = true;
|
|
11029
|
+
if (shouldDecoratePreviewComments) updatePreviewCommentBlocksForElement(targetEl);
|
|
11030
|
+
clearWatchedPreviewRenderError();
|
|
11031
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, markdown);
|
|
10758
11032
|
if (pane === "response") {
|
|
10759
11033
|
applyPendingResponseScrollReset();
|
|
10760
11034
|
scheduleResponsePaneRepaintNudge();
|
|
10761
11035
|
}
|
|
10762
11036
|
} catch (error) {
|
|
10763
|
-
if (
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
11037
|
+
if (staging && staging.remove) staging.remove();
|
|
11038
|
+
staging = null;
|
|
11039
|
+
if (previewCommitted) {
|
|
11040
|
+
console.error("Preview post-render update failed after the staged document was committed:", error);
|
|
11041
|
+
return;
|
|
10767
11042
|
}
|
|
11043
|
+
if (!stillCurrent()) return;
|
|
10768
11044
|
|
|
10769
11045
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
10770
11046
|
clearPreviewJumpHighlight(targetEl);
|
|
10771
11047
|
finishPreviewRender(targetEl);
|
|
11048
|
+
if (isWatchedFilePreview && hasMeaningfulPreviewContent(targetEl)) {
|
|
11049
|
+
watchedFilePreviewState.renderError = detail;
|
|
11050
|
+
showWatchedPreviewRenderError(targetEl, detail);
|
|
11051
|
+
updateSourceBadge();
|
|
11052
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, markdown);
|
|
11053
|
+
setStatus("Could not render the latest disk revision; keeping the last good preview.", "warning");
|
|
11054
|
+
return;
|
|
11055
|
+
}
|
|
10772
11056
|
targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown, previewFallbackOptions);
|
|
11057
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, markdown);
|
|
10773
11058
|
if (pane === "response") {
|
|
10774
11059
|
applyPendingResponseScrollReset();
|
|
10775
11060
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -11360,10 +11645,10 @@
|
|
|
11360
11645
|
return "<div class='repl-panel'>"
|
|
11361
11646
|
+ "<div class='repl-toolbar'>"
|
|
11362
11647
|
+ "<div class='repl-controls'>"
|
|
11363
|
-
+ "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
|
|
11648
|
+
+ "<label class='repl-control-label'>Runtime <select class='studio-flat-select' data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
|
|
11364
11649
|
+ "<label class='repl-control-label repl-command-label'>Start cmd <input data-repl-command type='text' value='" + escapeHtml(replCommand) + "' placeholder='default' aria-label='REPL start command' title='Command used by Start for this runtime. Leave blank for the default; use this for envs, e.g. .venv/bin/python, uv run python, or conda run --no-capture-output -n env python.'></label>"
|
|
11365
11650
|
+ "<button class='repl-start-btn' type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or switch to a " + escapeHtml(runtimeLabel) + " session using the selected runtime and start command.'>Start</button>"
|
|
11366
|
-
+ "<label class='repl-control-label repl-session-label'>Session <select data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
|
|
11651
|
+
+ "<label class='repl-control-label repl-session-label'>Session <select class='studio-flat-select' data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
|
|
11367
11652
|
+ "<details class='repl-more-controls'>"
|
|
11368
11653
|
+ "<summary title='More REPL actions'>More</summary>"
|
|
11369
11654
|
+ "<div class='repl-more-menu'>"
|
|
@@ -11589,7 +11874,7 @@
|
|
|
11589
11874
|
const options = getFileBrowserSortOptions().map((option) => {
|
|
11590
11875
|
return "<option value='" + escapeHtml(option.value) + "'" + (option.value === currentSort ? " selected" : "") + ">" + escapeHtml(option.label) + "</option>";
|
|
11591
11876
|
}).join("");
|
|
11592
|
-
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>";
|
|
11877
|
+
return "<select class='files-sort-select studio-flat-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
|
|
11593
11878
|
}
|
|
11594
11879
|
|
|
11595
11880
|
function buildFileBrowserEntryRowHtml(entry) {
|
|
@@ -11614,6 +11899,9 @@
|
|
|
11614
11899
|
const newTabButton = newTabAction
|
|
11615
11900
|
? "<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
11901
|
: "";
|
|
11902
|
+
const watchButton = kind === "text"
|
|
11903
|
+
? "<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>"
|
|
11904
|
+
: "";
|
|
11617
11905
|
const openTitle = type === "directory"
|
|
11618
11906
|
? "Open folder"
|
|
11619
11907
|
: (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 +11912,7 @@
|
|
|
11624
11912
|
+ "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
|
|
11625
11913
|
+ "</button>"
|
|
11626
11914
|
+ "<span class='files-actions'>"
|
|
11915
|
+
+ watchButton
|
|
11627
11916
|
+ newTabButton
|
|
11628
11917
|
+ "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(path) + "'>Copy path</button>"
|
|
11629
11918
|
+ (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(path) + "'>Reveal</button>" : "")
|
|
@@ -11640,7 +11929,7 @@
|
|
|
11640
11929
|
const label = String((location && location.label) || basenameForStudioPath(path) || path);
|
|
11641
11930
|
return "<option value='" + escapeHtml(path) + "'" + (path === rootDir ? " selected" : "") + ">" + escapeHtml(label + " — " + path) + "</option>";
|
|
11642
11931
|
}).join("");
|
|
11643
|
-
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>";
|
|
11932
|
+
return "<select class='files-location-select studio-flat-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>";
|
|
11644
11933
|
}
|
|
11645
11934
|
|
|
11646
11935
|
function buildFileBrowserPanelHtml() {
|
|
@@ -11797,7 +12086,7 @@
|
|
|
11797
12086
|
function ensureCurrentEditorFileBackedFromFilesPath(path) {
|
|
11798
12087
|
const cleanPath = stripPreviewLocalLinkUrlSuffix(path || "").trim();
|
|
11799
12088
|
if (!isLikelyAbsoluteStudioPath(cleanPath)) return;
|
|
11800
|
-
if (sourceState && sourceState.path
|
|
12089
|
+
if (sourceState && sourceState.path) return;
|
|
11801
12090
|
const resourceDir = normalizeStudioResourceDirValue(fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(cleanPath));
|
|
11802
12091
|
if (resourceDirInput && resourceDir) resourceDirInput.value = resourceDir;
|
|
11803
12092
|
setSourceState({
|
|
@@ -11805,7 +12094,7 @@
|
|
|
11805
12094
|
label: sourceState && sourceState.label && sourceState.label !== "blank" ? sourceState.label : basenameForStudioPath(cleanPath),
|
|
11806
12095
|
path: cleanPath,
|
|
11807
12096
|
});
|
|
11808
|
-
markFileBackedBaseline(sourceTextEl.value);
|
|
12097
|
+
markFileBackedBaseline(sourceTextEl.value, null);
|
|
11809
12098
|
}
|
|
11810
12099
|
|
|
11811
12100
|
async function openFileBrowserEntry(path, kind) {
|
|
@@ -11945,6 +12234,10 @@
|
|
|
11945
12234
|
await openPreviewDocumentInNewEditor(path, getFileBrowserLocalLinkContext());
|
|
11946
12235
|
return;
|
|
11947
12236
|
}
|
|
12237
|
+
if (action === "watch-new") {
|
|
12238
|
+
await openPreviewDocumentInWatchedPreview(path, getFileBrowserLocalLinkContext());
|
|
12239
|
+
return;
|
|
12240
|
+
}
|
|
11948
12241
|
if (action === "open-preview-new") {
|
|
11949
12242
|
await openPreviewResourceInNewEditor(path, getFileBrowserLocalLinkContext());
|
|
11950
12243
|
return;
|
|
@@ -12726,6 +13019,49 @@
|
|
|
12726
13019
|
return values.map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (value === current ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
|
|
12727
13020
|
}
|
|
12728
13021
|
|
|
13022
|
+
function clampSideQuestionThinkingLevel(value, levels) {
|
|
13023
|
+
const available = Array.isArray(levels) && levels.length ? levels : [...SIDE_QUESTION_THINKING_LEVELS];
|
|
13024
|
+
if (available.includes(value)) return value;
|
|
13025
|
+
const requestedIndex = SIDE_QUESTION_THINKING_LEVELS.indexOf(value);
|
|
13026
|
+
if (requestedIndex < 0) return available.includes("low") ? "low" : available[0];
|
|
13027
|
+
for (let index = requestedIndex; index < SIDE_QUESTION_THINKING_LEVELS.length; index += 1) {
|
|
13028
|
+
const candidate = SIDE_QUESTION_THINKING_LEVELS[index];
|
|
13029
|
+
if (available.includes(candidate)) return candidate;
|
|
13030
|
+
}
|
|
13031
|
+
for (let index = requestedIndex - 1; index >= 0; index -= 1) {
|
|
13032
|
+
const candidate = SIDE_QUESTION_THINKING_LEVELS[index];
|
|
13033
|
+
if (available.includes(candidate)) return candidate;
|
|
13034
|
+
}
|
|
13035
|
+
return "off";
|
|
13036
|
+
}
|
|
13037
|
+
|
|
13038
|
+
function applySideQuestionThinkingLevels(value) {
|
|
13039
|
+
const seen = new Set();
|
|
13040
|
+
const normalized = (Array.isArray(value) ? value : []).filter((level) => {
|
|
13041
|
+
if (typeof level !== "string" || !SIDE_QUESTION_THINKING_LEVELS.includes(level) || seen.has(level)) return false;
|
|
13042
|
+
seen.add(level);
|
|
13043
|
+
return true;
|
|
13044
|
+
});
|
|
13045
|
+
const next = normalized.length ? normalized : [...SIDE_QUESTION_THINKING_LEVELS];
|
|
13046
|
+
const levelsChanged = next.length !== sideQuestionThinkingLevels.length
|
|
13047
|
+
|| next.some((level, index) => level !== sideQuestionThinkingLevels[index]);
|
|
13048
|
+
sideQuestionThinkingLevels = next;
|
|
13049
|
+
const nextThinking = clampSideQuestionThinkingLevel(sideQuestionPreferredThinking, next);
|
|
13050
|
+
const thinkingChanged = nextThinking !== sideQuestionUi.thinking;
|
|
13051
|
+
if (thinkingChanged) sideQuestionUi.thinking = nextThinking;
|
|
13052
|
+
if ((levelsChanged || thinkingChanged) && rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) {
|
|
13053
|
+
renderSideQuestionView();
|
|
13054
|
+
}
|
|
13055
|
+
}
|
|
13056
|
+
|
|
13057
|
+
function getSideQuestionThinkingOptions() {
|
|
13058
|
+
return sideQuestionThinkingLevels.map((level) => {
|
|
13059
|
+
if (level === "xhigh") return [level, "X-high (slower)"];
|
|
13060
|
+
if (level === "max") return [level, "Max (slowest)"];
|
|
13061
|
+
return [level, level.charAt(0).toUpperCase() + level.slice(1)];
|
|
13062
|
+
});
|
|
13063
|
+
}
|
|
13064
|
+
|
|
12729
13065
|
function renderSideQuestionPiToolPicker() {
|
|
12730
13066
|
const selected = new Set(sideQuestionUi.toolIds);
|
|
12731
13067
|
const selectedCount = sideQuestionUi.toolIds.length;
|
|
@@ -12762,15 +13098,15 @@
|
|
|
12762
13098
|
return "<div class='side-question-empty'>"
|
|
12763
13099
|
+ "<div class='side-question-intro'><h2>Side question</h2><p>Ask something without adding it to the main Pi conversation. Choose the starting text and whether Studio may look through related files.</p></div>"
|
|
12764
13100
|
+ "<div class='side-question-context-grid'>"
|
|
12765
|
-
+ "<label>Starting text<select data-side-question-field='focusMode' aria-describedby='sideQuestionContextRule'>" + sideQuestionSelectOptions([
|
|
13101
|
+
+ "<label>Starting text<select class='studio-flat-select' data-side-question-field='focusMode' aria-describedby='sideQuestionContextRule'>" + sideQuestionSelectOptions([
|
|
12766
13102
|
["auto", "Automatic"], ["selection", "Editor selection only"], ["section", "Heading block or nearby text at cursor"], ["editor", "Whole editor document"], ["response", "Displayed response"], ["none", "No starting text"],
|
|
12767
13103
|
], sideQuestionUi.focusMode) + "</select></label>"
|
|
12768
|
-
+ "<label>Also use files from<select data-side-question-field='gatherScope'>" + sideQuestionSelectOptions([
|
|
13104
|
+
+ "<label>Also use files from<select class='studio-flat-select' data-side-question-field='gatherScope'>" + sideQuestionSelectOptions([
|
|
12769
13105
|
["none", "No other files"], ["folder", "Same folder as document"], ["repo", "Repository"], ["custom", "Choose a folder"],
|
|
12770
13106
|
], scope) + "</select></label>"
|
|
12771
|
-
+ "<label>Thinking<select data-side-question-field='thinking'>" + sideQuestionSelectOptions(
|
|
12772
|
-
|
|
12773
|
-
|
|
13107
|
+
+ "<label>Thinking<select class='studio-flat-select' data-side-question-field='thinking'>" + sideQuestionSelectOptions(
|
|
13108
|
+
getSideQuestionThinkingOptions(), sideQuestionUi.thinking
|
|
13109
|
+
) + "</select></label>"
|
|
12774
13110
|
+ "</div>"
|
|
12775
13111
|
+ "<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>"
|
|
12776
13112
|
+ (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>")
|
|
@@ -13128,9 +13464,10 @@
|
|
|
13128
13464
|
if (sideQuestionUi.gatherScope !== "repo") sideQuestionUi.gitContext = false;
|
|
13129
13465
|
try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_GATHER_STORAGE_KEY, sideQuestionUi.gatherScope); } catch {}
|
|
13130
13466
|
}
|
|
13131
|
-
if (field === "thinking") {
|
|
13467
|
+
if (field === "thinking" && sideQuestionThinkingLevels.includes(target.value)) {
|
|
13468
|
+
sideQuestionPreferredThinking = target.value;
|
|
13132
13469
|
sideQuestionUi.thinking = target.value;
|
|
13133
|
-
try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_THINKING_STORAGE_KEY,
|
|
13470
|
+
try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_THINKING_STORAGE_KEY, sideQuestionPreferredThinking); } catch {}
|
|
13134
13471
|
}
|
|
13135
13472
|
if (field === "includeConversation") sideQuestionUi.includeConversation = target.checked;
|
|
13136
13473
|
if (field === "gitContext") sideQuestionUi.gitContext = target.checked && getSideQuestionGatherScope() === "repo";
|
|
@@ -13575,13 +13912,13 @@
|
|
|
13575
13912
|
function updateSaveFileTooltip() {
|
|
13576
13913
|
if (!saveOverBtn) return;
|
|
13577
13914
|
|
|
13578
|
-
var effectivePath =
|
|
13915
|
+
var effectivePath = sourceState && sourceState.path ? sourceState.path : "";
|
|
13579
13916
|
if (effectivePath) {
|
|
13580
|
-
saveOverBtn.title = "
|
|
13917
|
+
saveOverBtn.title = "Save file when its disk revision still matches: " + effectivePath + " · Shortcut: Cmd/Ctrl+S.";
|
|
13581
13918
|
return;
|
|
13582
13919
|
}
|
|
13583
13920
|
|
|
13584
|
-
saveOverBtn.title = "Save editor is available after opening a file
|
|
13921
|
+
saveOverBtn.title = "Save editor is available after opening a file-backed document. Use Save editor as… for a new file.";
|
|
13585
13922
|
}
|
|
13586
13923
|
|
|
13587
13924
|
function updateRefreshFromDiskTooltip() {
|
|
@@ -13596,13 +13933,13 @@
|
|
|
13596
13933
|
}
|
|
13597
13934
|
|
|
13598
13935
|
function syncActionButtons() {
|
|
13599
|
-
const canSaveOver =
|
|
13936
|
+
const canSaveOver = hasRefreshableFilePath();
|
|
13600
13937
|
const canRefreshFromDisk = hasRefreshableFilePath();
|
|
13601
13938
|
|
|
13602
|
-
fileInput.disabled = uiBusy;
|
|
13603
|
-
if (importFileBtn) importFileBtn.disabled = uiBusy;
|
|
13604
|
-
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
|
|
13605
|
-
if (sourceResetOriginBtn) sourceResetOriginBtn.disabled = uiBusy;
|
|
13939
|
+
fileInput.disabled = uiBusy || isWatchedFilePreview;
|
|
13940
|
+
if (importFileBtn) importFileBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13941
|
+
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy || isWatchedFilePreview;
|
|
13942
|
+
if (sourceResetOriginBtn) sourceResetOriginBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13606
13943
|
if (sourceOpenCurrentFileTabBtn) {
|
|
13607
13944
|
sourceOpenCurrentFileTabBtn.disabled = uiBusy || !hasRefreshableFilePath();
|
|
13608
13945
|
sourceOpenCurrentFileTabBtn.title = hasRefreshableFilePath()
|
|
@@ -13610,17 +13947,21 @@
|
|
|
13610
13947
|
: "Available after opening a file-backed document.";
|
|
13611
13948
|
}
|
|
13612
13949
|
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;
|
|
13950
|
+
saveAsBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13951
|
+
saveOverBtn.disabled = uiBusy || isWatchedFilePreview || !canSaveOver;
|
|
13952
|
+
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || isWatchedFilePreview || !canRefreshFromDisk;
|
|
13953
|
+
if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13617
13954
|
sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
13618
|
-
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
13955
|
+
if (getEditorBtn) getEditorBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13956
|
+
if (watchedOpenEditableBtn) {
|
|
13957
|
+
watchedOpenEditableBtn.hidden = !isWatchedFilePreview;
|
|
13958
|
+
watchedOpenEditableBtn.disabled = uiBusy || !watchedFilePreviewState.path;
|
|
13959
|
+
}
|
|
13619
13960
|
syncRunAndCritiqueButtons();
|
|
13620
13961
|
copyDraftBtn.disabled = uiBusy;
|
|
13621
13962
|
if (suggestCompletionBtn) {
|
|
13622
13963
|
const hasSuggestionForCurrentText = Boolean(completionSuggestionState && sourceTextEl && sourceTextEl.value === completionSuggestionState.baseText);
|
|
13623
|
-
suggestCompletionBtn.disabled = wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
|
|
13964
|
+
suggestCompletionBtn.disabled = isWatchedFilePreview || wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
|
|
13624
13965
|
suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Stop" : (hasSuggestionForCurrentText ? "Try another" : "Suggest");
|
|
13625
13966
|
suggestCompletionBtn.title = completionSuggestionInFlight
|
|
13626
13967
|
? "Stop the current suggestion request."
|
|
@@ -13636,15 +13977,15 @@
|
|
|
13636
13977
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
13637
13978
|
if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
|
|
13638
13979
|
if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
|
|
13639
|
-
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
|
|
13640
|
-
if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
13980
|
+
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13981
|
+
if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || isWatchedFilePreview || !hasAnnotationMarkers(sourceTextEl.value);
|
|
13641
13982
|
if (compactBtn) compactBtn.disabled = isEditorOnlyMode || uiBusy || compactInProgress || wsState === "Disconnected";
|
|
13642
13983
|
editorViewSelect.disabled = isEditorOnlyMode;
|
|
13643
13984
|
syncRightViewModeOptions();
|
|
13644
|
-
rightViewSelect.disabled =
|
|
13985
|
+
rightViewSelect.disabled = isWatchedFilePreview;
|
|
13645
13986
|
followSelect.disabled = isEditorOnlyMode || uiBusy;
|
|
13646
13987
|
if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
|
|
13647
|
-
insertHeaderBtn.disabled = uiBusy;
|
|
13988
|
+
insertHeaderBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
13648
13989
|
lensSelect.disabled = uiBusy || isEditorOnlyMode;
|
|
13649
13990
|
updateSaveFileTooltip();
|
|
13650
13991
|
updateRefreshFromDiskTooltip();
|
|
@@ -13661,9 +14002,14 @@
|
|
|
13661
14002
|
|
|
13662
14003
|
function setSourceState(next, options) {
|
|
13663
14004
|
const previousDescriptor = getCurrentStudioDocumentDescriptor();
|
|
14005
|
+
const previousPath = sourceState && sourceState.path ? sourceState.path : null;
|
|
13664
14006
|
const previousQuartoPath = getCurrentStudioQuartoSourcePath();
|
|
13665
14007
|
const previousPreviewResourceContext = getHtmlPreviewResourceContextOptions();
|
|
13666
14008
|
const nextPath = next && next.path ? next.path : null;
|
|
14009
|
+
if (isWatchedFilePreview && watchedFilePreviewState.path && nextPath !== watchedFilePreviewState.path) {
|
|
14010
|
+
setStatus("This read-only preview remains bound to its watched file.", "warning");
|
|
14011
|
+
return false;
|
|
14012
|
+
}
|
|
13667
14013
|
sourceState = {
|
|
13668
14014
|
source: next && next.source ? next.source : "blank",
|
|
13669
14015
|
label: next && next.label ? next.label : "blank",
|
|
@@ -13681,7 +14027,7 @@
|
|
|
13681
14027
|
quartoPreviewActionRequestId = null;
|
|
13682
14028
|
quartoPreviewLogVisible = false;
|
|
13683
14029
|
}
|
|
13684
|
-
if (!sourceState.path) {
|
|
14030
|
+
if (!sourceState.path || sourceState.path !== previousPath) {
|
|
13685
14031
|
clearFileBackedBaseline();
|
|
13686
14032
|
}
|
|
13687
14033
|
syncRightViewModeOptions();
|
|
@@ -13788,6 +14134,7 @@
|
|
|
13788
14134
|
version: 1,
|
|
13789
14135
|
savedAt: lastWorkspacePersistenceSavedAt,
|
|
13790
14136
|
sourceState: normalizeWorkspaceSourceState(sourceState),
|
|
14137
|
+
diskRevision: fileBackedDiskRevision,
|
|
13791
14138
|
resourceDir: getCurrentResourceDirValue(),
|
|
13792
14139
|
editorView,
|
|
13793
14140
|
rightView: normalizeRightViewValue(rightView),
|
|
@@ -13823,7 +14170,7 @@
|
|
|
13823
14170
|
}
|
|
13824
14171
|
|
|
13825
14172
|
function persistWorkspaceStateNow(options) {
|
|
13826
|
-
if (!workspacePersistenceReady) return;
|
|
14173
|
+
if (!workspacePersistenceReady || isWatchedFilePreview) return;
|
|
13827
14174
|
try {
|
|
13828
14175
|
const payload = buildWorkspacePersistencePayload();
|
|
13829
14176
|
if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
|
|
@@ -13845,7 +14192,7 @@
|
|
|
13845
14192
|
}
|
|
13846
14193
|
|
|
13847
14194
|
function scheduleWorkspacePersistence() {
|
|
13848
|
-
if (!workspacePersistenceReady || workspacePersistTimer !== null) return;
|
|
14195
|
+
if (!workspacePersistenceReady || isWatchedFilePreview || workspacePersistTimer !== null) return;
|
|
13849
14196
|
workspacePersistTimer = window.setTimeout(() => {
|
|
13850
14197
|
workspacePersistTimer = null;
|
|
13851
14198
|
persistWorkspaceStateNow();
|
|
@@ -13853,6 +14200,7 @@
|
|
|
13853
14200
|
}
|
|
13854
14201
|
|
|
13855
14202
|
function flushWorkspacePersistence(options) {
|
|
14203
|
+
if (isWatchedFilePreview) return;
|
|
13856
14204
|
if (workspacePersistTimer !== null) {
|
|
13857
14205
|
window.clearTimeout(workspacePersistTimer);
|
|
13858
14206
|
workspacePersistTimer = null;
|
|
@@ -13882,9 +14230,17 @@
|
|
|
13882
14230
|
if (!shouldRestorePersistedWorkspaceState(state)) return false;
|
|
13883
14231
|
const nextSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
13884
14232
|
const nextResourceDir = normalizeStudioResourceDirValue(typeof state.resourceDir === "string" ? state.resourceDir : "");
|
|
14233
|
+
const currentBaselineText = fileBackedBaselineText;
|
|
14234
|
+
const currentDiskRevision = fileBackedDiskRevision;
|
|
14235
|
+
const persistedDiskRevision = normalizeStudioDiskRevision(state.diskRevision);
|
|
13885
14236
|
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
13886
14237
|
setEditorText(state.text, { preserveScroll: false, preserveSelection: false });
|
|
13887
14238
|
setSourceState(nextSourceState);
|
|
14239
|
+
if (nextSourceState.path) {
|
|
14240
|
+
fileBackedBaselineText = currentBaselineText;
|
|
14241
|
+
fileBackedDiskRevision = persistedDiskRevision
|
|
14242
|
+
|| (currentBaselineText !== null && state.text === currentBaselineText ? currentDiskRevision : null);
|
|
14243
|
+
}
|
|
13888
14244
|
if (resourceDirInput && nextResourceDir) {
|
|
13889
14245
|
resourceDirInput.value = nextResourceDir;
|
|
13890
14246
|
updateSourceBadge();
|
|
@@ -13970,6 +14326,10 @@
|
|
|
13970
14326
|
}
|
|
13971
14327
|
|
|
13972
14328
|
function setEditorText(nextText, options) {
|
|
14329
|
+
if (isWatchedFilePreview && !(options && options.allowWatchedFileUpdate === true)) {
|
|
14330
|
+
setStatus("This preview is read-only and follows its watched file on disk.", "warning");
|
|
14331
|
+
return false;
|
|
14332
|
+
}
|
|
13973
14333
|
const value = String(nextText || "");
|
|
13974
14334
|
const preserveScroll = Boolean(options && options.preserveScroll);
|
|
13975
14335
|
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
@@ -14017,9 +14377,14 @@
|
|
|
14017
14377
|
updateEditorSelectionCommentUi();
|
|
14018
14378
|
updateOutlineUi();
|
|
14019
14379
|
scheduleWorkspacePersistence();
|
|
14380
|
+
return true;
|
|
14020
14381
|
}
|
|
14021
14382
|
|
|
14022
14383
|
function applySourceTextEdit(nextText, selectionStart, selectionEnd) {
|
|
14384
|
+
if (isWatchedFilePreview) {
|
|
14385
|
+
setStatus("This preview is read-only and follows its watched file on disk.", "warning");
|
|
14386
|
+
return false;
|
|
14387
|
+
}
|
|
14023
14388
|
const value = String(nextText || "");
|
|
14024
14389
|
sourceTextEl.value = value;
|
|
14025
14390
|
const maxIndex = value.length;
|
|
@@ -14031,6 +14396,7 @@
|
|
|
14031
14396
|
if (editorView === "markdown") {
|
|
14032
14397
|
scheduleEditorLineNumberRender();
|
|
14033
14398
|
}
|
|
14399
|
+
return true;
|
|
14034
14400
|
}
|
|
14035
14401
|
|
|
14036
14402
|
function readCompletionSuggestionContextMode() {
|
|
@@ -15191,11 +15557,12 @@
|
|
|
15191
15557
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
15192
15558
|
appendPreviewLinkMenuButton(menu, "Open in system viewer", "open-system");
|
|
15193
15559
|
} else if (kind === "text") {
|
|
15560
|
+
appendPreviewLinkMenuButton(menu, "Preview file (follow changes)", "watch-new");
|
|
15194
15561
|
appendPreviewLinkMenuButton(menu, "Open file tab", "open-new");
|
|
15195
|
-
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
15562
|
+
if (!isWatchedFilePreview) appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
15196
15563
|
} else if (kind === "office") {
|
|
15197
15564
|
appendPreviewLinkMenuButton(menu, "Convert tab", "open-new");
|
|
15198
|
-
appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
15565
|
+
if (!isWatchedFilePreview) appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
15199
15566
|
} else if (kind === "image") {
|
|
15200
15567
|
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
15201
15568
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
@@ -15291,9 +15658,15 @@
|
|
|
15291
15658
|
}
|
|
15292
15659
|
|
|
15293
15660
|
async function fetchPreviewLocalLink(action, href, contextOverride, options) {
|
|
15294
|
-
const request = () =>
|
|
15295
|
-
query
|
|
15296
|
-
|
|
15661
|
+
const request = () => {
|
|
15662
|
+
const query = { ...getPreviewLinkResourceQuery(href, contextOverride), action };
|
|
15663
|
+
if (isWatchedFilePreview) {
|
|
15664
|
+
query.watchedFile = "1";
|
|
15665
|
+
const watchedDocId = initialQueryParams.get("docId") || "";
|
|
15666
|
+
if (watchedDocId) query.docId = watchedDocId;
|
|
15667
|
+
}
|
|
15668
|
+
return fetchStudioJson("/local-preview-link", { query });
|
|
15669
|
+
};
|
|
15297
15670
|
try {
|
|
15298
15671
|
return await request();
|
|
15299
15672
|
} catch (error) {
|
|
@@ -15385,6 +15758,9 @@
|
|
|
15385
15758
|
}
|
|
15386
15759
|
|
|
15387
15760
|
async function openPreviewDocumentHere(href, contextOverride, options) {
|
|
15761
|
+
if (isWatchedFilePreview) {
|
|
15762
|
+
throw new Error("This preview follows one disk file and cannot open another document here. Open a new file tab instead.");
|
|
15763
|
+
}
|
|
15388
15764
|
if (!(await confirmPreviewOfficeConversion(href, "here"))) return;
|
|
15389
15765
|
if (editorHasPotentialUnsavedContent()) {
|
|
15390
15766
|
const kind = getPreviewLocalLinkKind(href);
|
|
@@ -15414,7 +15790,7 @@
|
|
|
15414
15790
|
setSourceState({ source: "blank", label, path: null });
|
|
15415
15791
|
} else {
|
|
15416
15792
|
setSourceState({ source: "file", label, path });
|
|
15417
|
-
markFileBackedBaseline(payload.text);
|
|
15793
|
+
markFileBackedBaseline(payload.text, payload.diskRevision);
|
|
15418
15794
|
}
|
|
15419
15795
|
const detected = converted ? "markdown" : detectLanguageFromName(path || label);
|
|
15420
15796
|
if (detected) setEditorLanguage(detected);
|
|
@@ -15445,6 +15821,25 @@
|
|
|
15445
15821
|
}
|
|
15446
15822
|
}
|
|
15447
15823
|
|
|
15824
|
+
async function openPreviewDocumentInWatchedPreview(href, contextOverride) {
|
|
15825
|
+
let launch = null;
|
|
15826
|
+
try {
|
|
15827
|
+
launch = openPendingStudioTab("preview");
|
|
15828
|
+
const payload = await fetchPreviewLocalLink("watch-url", href, contextOverride);
|
|
15829
|
+
const relativeUrl = payload && typeof payload.relativeUrl === "string" ? payload.relativeUrl : "";
|
|
15830
|
+
if (!relativeUrl) throw new Error("Studio did not return a watched-preview URL.");
|
|
15831
|
+
navigatePendingStudioTab(launch, relativeUrl);
|
|
15832
|
+
setStatus("Opening read-only preview that follows disk changes.");
|
|
15833
|
+
} catch (error) {
|
|
15834
|
+
if (error && error.studioCancelled) {
|
|
15835
|
+
cancelPendingStudioTab(launch, "Local resource access was cancelled.");
|
|
15836
|
+
} else {
|
|
15837
|
+
failPendingStudioTab(launch, "Studio could not prepare this watched preview. Return to the originating Studio page for details.");
|
|
15838
|
+
}
|
|
15839
|
+
throw error;
|
|
15840
|
+
}
|
|
15841
|
+
}
|
|
15842
|
+
|
|
15448
15843
|
async function openPreviewResourceInNewEditor(href, contextOverride) {
|
|
15449
15844
|
let launch = null;
|
|
15450
15845
|
try {
|
|
@@ -15500,6 +15895,10 @@
|
|
|
15500
15895
|
await openPreviewDocumentInNewEditor(href, context);
|
|
15501
15896
|
return;
|
|
15502
15897
|
}
|
|
15898
|
+
if (action === "watch-new") {
|
|
15899
|
+
await openPreviewDocumentInWatchedPreview(href, context);
|
|
15900
|
+
return;
|
|
15901
|
+
}
|
|
15503
15902
|
if (action === "open-preview-new") {
|
|
15504
15903
|
await openPreviewResourceInNewEditor(href, context);
|
|
15505
15904
|
return;
|
|
@@ -15995,13 +16394,13 @@
|
|
|
15995
16394
|
return "<div class='studio-quiz-setup'>"
|
|
15996
16395
|
+ "<p class='studio-quiz-copy'>A short active-recall loop: answer one question, check it, ask about the card if useful, then move on.</p>"
|
|
15997
16396
|
+ "<div class='studio-quiz-fields'>"
|
|
15998
|
-
+ "<label>Scope<select data-quiz-field='scope'>"
|
|
16397
|
+
+ "<label>Scope<select class='studio-flat-select' data-quiz-field='scope'>"
|
|
15999
16398
|
+ QUIZ_SCOPES.map((candidate) => candidate === "selection" && !hasSelection ? "" : renderQuizOption(candidate, scope, getQuizScopeLabel(candidate))).join("")
|
|
16000
16399
|
+ "</select></label>"
|
|
16001
|
-
+ "<label>Angle<select data-quiz-field='angle'>"
|
|
16400
|
+
+ "<label>Angle<select class='studio-flat-select' data-quiz-field='angle'>"
|
|
16002
16401
|
+ QUIZ_ANGLES.map((candidate) => renderQuizOption(candidate, angle, getQuizAngleLabel(candidate))).join("")
|
|
16003
16402
|
+ "</select></label>"
|
|
16004
|
-
+ "<label>Thinking<select data-quiz-field='thinking'>"
|
|
16403
|
+
+ "<label>Thinking<select class='studio-flat-select' data-quiz-field='thinking'>"
|
|
16005
16404
|
+ QUIZ_THINKING_LEVELS.map((candidate) => renderQuizOption(candidate, thinking, getQuizThinkingLabel(candidate))).join("")
|
|
16006
16405
|
+ "</select></label>"
|
|
16007
16406
|
+ "<label>Questions<input data-quiz-field='count' type='number' min='1' max='8' value='" + String(count) + "'></label>"
|
|
@@ -16861,6 +17260,8 @@
|
|
|
16861
17260
|
ensurePreviewSelectionActions(targetEl);
|
|
16862
17261
|
updatePreviewCommentBlocksForElement(targetEl);
|
|
16863
17262
|
decorateCopyablePreviewBlocks(targetEl);
|
|
17263
|
+
clearWatchedPreviewRenderError();
|
|
17264
|
+
scheduleWatchedPreviewReadingPositionRestore(targetEl, text);
|
|
16864
17265
|
if (pane === "response") {
|
|
16865
17266
|
applyPendingResponseScrollReset();
|
|
16866
17267
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -21734,7 +22135,11 @@
|
|
|
21734
22135
|
} catch {}
|
|
21735
22136
|
}
|
|
21736
22137
|
|
|
21737
|
-
function setEditorLanguage(lang) {
|
|
22138
|
+
function setEditorLanguage(lang, options) {
|
|
22139
|
+
if (isWatchedFilePreview && !(options && options.allowWatchedFileUpdate === true)) {
|
|
22140
|
+
setStatus("The watched preview language follows its file path.", "warning");
|
|
22141
|
+
return false;
|
|
22142
|
+
}
|
|
21738
22143
|
editorLanguage = (lang && SUPPORTED_LANGUAGES.indexOf(lang) !== -1) ? lang : "markdown";
|
|
21739
22144
|
persistEditorLanguage(editorLanguage);
|
|
21740
22145
|
syncHighlightSelectUi();
|
|
@@ -21749,6 +22154,7 @@
|
|
|
21749
22154
|
}
|
|
21750
22155
|
updateOutlineUi();
|
|
21751
22156
|
scheduleWorkspacePersistence();
|
|
22157
|
+
return true;
|
|
21752
22158
|
}
|
|
21753
22159
|
|
|
21754
22160
|
function setEditorHighlightMode(mode) {
|
|
@@ -22050,11 +22456,90 @@
|
|
|
22050
22456
|
return true;
|
|
22051
22457
|
}
|
|
22052
22458
|
|
|
22459
|
+
function handleWatchedFileUpdate(message) {
|
|
22460
|
+
if (!isWatchedFilePreview || !message || typeof message.text !== "string") return;
|
|
22461
|
+
const messagePath = String(message.path || "");
|
|
22462
|
+
if (!watchedFilePreviewState.path || messagePath !== watchedFilePreviewState.path) return;
|
|
22463
|
+
const generation = Math.max(0, Number(message.generation) || 0);
|
|
22464
|
+
if (generation < watchedFilePreviewState.generation) return;
|
|
22465
|
+
|
|
22466
|
+
const expectedPreviewText = prepareEditorTextForPreview(message.text);
|
|
22467
|
+
watchedFilePreviewReadingPositions.source = {
|
|
22468
|
+
snapshot: captureWatchedPreviewReadingPosition(sourcePreviewEl),
|
|
22469
|
+
text: expectedPreviewText,
|
|
22470
|
+
};
|
|
22471
|
+
watchedFilePreviewReadingPositions.response = {
|
|
22472
|
+
snapshot: captureWatchedPreviewReadingPosition(critiqueViewEl),
|
|
22473
|
+
text: expectedPreviewText,
|
|
22474
|
+
};
|
|
22475
|
+
const textareaMaxScroll = Math.max(0, Number(sourceTextEl.scrollHeight || 0) - Number(sourceTextEl.clientHeight || 0));
|
|
22476
|
+
const textareaScrollRatio = textareaMaxScroll > 0 ? Number(sourceTextEl.scrollTop || 0) / textareaMaxScroll : 0;
|
|
22477
|
+
const selectionStart = Math.max(0, Number(sourceTextEl.selectionStart) || 0);
|
|
22478
|
+
const selectionEnd = Math.max(selectionStart, Number(sourceTextEl.selectionEnd) || selectionStart);
|
|
22479
|
+
|
|
22480
|
+
sourceTextEl.value = message.text;
|
|
22481
|
+
const nextMaxScroll = Math.max(0, Number(sourceTextEl.scrollHeight || 0) - Number(sourceTextEl.clientHeight || 0));
|
|
22482
|
+
sourceTextEl.scrollTop = Math.max(0, Math.min(nextMaxScroll, nextMaxScroll * textareaScrollRatio));
|
|
22483
|
+
try {
|
|
22484
|
+
sourceTextEl.setSelectionRange(
|
|
22485
|
+
Math.min(selectionStart, message.text.length),
|
|
22486
|
+
Math.min(selectionEnd, message.text.length),
|
|
22487
|
+
);
|
|
22488
|
+
} catch {
|
|
22489
|
+
// Selection APIs are not guaranteed in every embedded browser.
|
|
22490
|
+
}
|
|
22491
|
+
|
|
22492
|
+
watchedFilePreviewState.generation = generation;
|
|
22493
|
+
watchedFilePreviewState.diskRevision = normalizeStudioDiskRevision(message.diskRevision) || watchedFilePreviewState.diskRevision;
|
|
22494
|
+
watchedFilePreviewState.lastError = "";
|
|
22495
|
+
fileBackedBaselineText = message.text;
|
|
22496
|
+
fileBackedDiskRevision = watchedFilePreviewState.diskRevision || null;
|
|
22497
|
+
editorLanguage = detectLanguageFromName(watchedFilePreviewState.path) || editorLanguage;
|
|
22498
|
+
syncHighlightSelectUi();
|
|
22499
|
+
scheduleEditorHighlightRender();
|
|
22500
|
+
renderSourcePreview({ previewDelayMs: 0 });
|
|
22501
|
+
renderActiveResult();
|
|
22502
|
+
updateSourceBadge();
|
|
22503
|
+
setStatus(message.message || "Watched preview updated from disk.", "success");
|
|
22504
|
+
}
|
|
22505
|
+
|
|
22506
|
+
function handleWatchedFileError(message) {
|
|
22507
|
+
if (!isWatchedFilePreview || !message) return;
|
|
22508
|
+
const messagePath = String(message.path || "");
|
|
22509
|
+
if (watchedFilePreviewState.path && messagePath && messagePath !== watchedFilePreviewState.path) return;
|
|
22510
|
+
watchedFilePreviewState.lastError = String(message.message || "Could not refresh the watched file.");
|
|
22511
|
+
updateSourceBadge();
|
|
22512
|
+
setStatus(watchedFilePreviewState.lastError, "warning");
|
|
22513
|
+
}
|
|
22514
|
+
|
|
22515
|
+
function handleWatchedFileReady(message) {
|
|
22516
|
+
if (!isWatchedFilePreview || !message) return;
|
|
22517
|
+
const messagePath = String(message.path || "");
|
|
22518
|
+
if (watchedFilePreviewState.path && messagePath && messagePath !== watchedFilePreviewState.path) return;
|
|
22519
|
+
watchedFilePreviewState.diskRevision = normalizeStudioDiskRevision(message.diskRevision) || watchedFilePreviewState.diskRevision;
|
|
22520
|
+
watchedFilePreviewState.lastError = "";
|
|
22521
|
+
updateSourceBadge();
|
|
22522
|
+
setStatus(message.message || "Watching file for disk changes.", "success");
|
|
22523
|
+
}
|
|
22524
|
+
|
|
22053
22525
|
function handleServerMessage(message) {
|
|
22054
22526
|
if (!message || typeof message !== "object") return;
|
|
22055
22527
|
|
|
22056
22528
|
debugTrace("server_message", summarizeServerMessage(message));
|
|
22057
22529
|
|
|
22530
|
+
if (message.type === "watched_file_update") {
|
|
22531
|
+
handleWatchedFileUpdate(message);
|
|
22532
|
+
return;
|
|
22533
|
+
}
|
|
22534
|
+
if (message.type === "watched_file_error") {
|
|
22535
|
+
handleWatchedFileError(message);
|
|
22536
|
+
return;
|
|
22537
|
+
}
|
|
22538
|
+
if (message.type === "watched_file_ready") {
|
|
22539
|
+
handleWatchedFileReady(message);
|
|
22540
|
+
return;
|
|
22541
|
+
}
|
|
22542
|
+
|
|
22058
22543
|
const contextChanged = applyContextUsageFromMessage(message);
|
|
22059
22544
|
if (contextChanged) {
|
|
22060
22545
|
updateFooterMeta();
|
|
@@ -22078,6 +22563,7 @@
|
|
|
22078
22563
|
}
|
|
22079
22564
|
|
|
22080
22565
|
if (message.type === "side_question_state") {
|
|
22566
|
+
if (Array.isArray(message.sideQuestionThinkingLevels)) applySideQuestionThinkingLevels(message.sideQuestionThinkingLevels);
|
|
22081
22567
|
sideQuestionWebSearchAvailable = message.webSearchAvailable === true;
|
|
22082
22568
|
if (Array.isArray(message.availablePiTools)) applySideQuestionToolCatalog(message.availablePiTools);
|
|
22083
22569
|
const previousStatus = sideQuestionState && sideQuestionState.status;
|
|
@@ -22291,7 +22777,11 @@
|
|
|
22291
22777
|
message.initialDocument &&
|
|
22292
22778
|
typeof message.initialDocument.text === "string"
|
|
22293
22779
|
) {
|
|
22294
|
-
setEditorText(message.initialDocument.text, {
|
|
22780
|
+
setEditorText(message.initialDocument.text, {
|
|
22781
|
+
preserveScroll: false,
|
|
22782
|
+
preserveSelection: false,
|
|
22783
|
+
allowWatchedFileUpdate: isWatchedFilePreview,
|
|
22784
|
+
});
|
|
22295
22785
|
initialDocumentApplied = true;
|
|
22296
22786
|
loadedInitialDocument = true;
|
|
22297
22787
|
setSourceState({
|
|
@@ -22303,7 +22793,7 @@
|
|
|
22303
22793
|
: (initialSourceState.draftId || null),
|
|
22304
22794
|
});
|
|
22305
22795
|
if (message.initialDocument.path) {
|
|
22306
|
-
markFileBackedBaseline(message.initialDocument.text);
|
|
22796
|
+
markFileBackedBaseline(message.initialDocument.text, message.initialDocument.diskRevision);
|
|
22307
22797
|
}
|
|
22308
22798
|
refreshResponseUi();
|
|
22309
22799
|
if (typeof message.initialDocument.label === "string" && message.initialDocument.label.length > 0) {
|
|
@@ -22664,7 +23154,19 @@
|
|
|
22664
23154
|
return;
|
|
22665
23155
|
}
|
|
22666
23156
|
|
|
23157
|
+
if (message.type === "save_conflict") {
|
|
23158
|
+
void handleEditorSaveConflict(message);
|
|
23159
|
+
return;
|
|
23160
|
+
}
|
|
23161
|
+
|
|
23162
|
+
if (message.type === "save_as_conflict") {
|
|
23163
|
+
void handleEditorSaveAsConflict(message);
|
|
23164
|
+
return;
|
|
23165
|
+
}
|
|
23166
|
+
|
|
22667
23167
|
if (message.type === "saved") {
|
|
23168
|
+
const savedOperation = typeof message.requestId === "string" ? pendingSaveOperations.get(message.requestId) : null;
|
|
23169
|
+
if (typeof message.requestId === "string") pendingSaveOperations.delete(message.requestId);
|
|
22668
23170
|
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
22669
23171
|
pendingRequestId = null;
|
|
22670
23172
|
pendingKind = null;
|
|
@@ -22683,7 +23185,7 @@
|
|
|
22683
23185
|
}, {
|
|
22684
23186
|
carryCurrentMetadataToNewDocument: true,
|
|
22685
23187
|
});
|
|
22686
|
-
markFileBackedBaseline(sourceTextEl.value);
|
|
23188
|
+
markFileBackedBaseline(savedOperation && typeof savedOperation.content === "string" ? savedOperation.content : sourceTextEl.value, message.diskRevision);
|
|
22687
23189
|
}
|
|
22688
23190
|
setBusy(false);
|
|
22689
23191
|
setWsState("Ready");
|
|
@@ -22703,6 +23205,10 @@
|
|
|
22703
23205
|
}
|
|
22704
23206
|
|
|
22705
23207
|
if (message.type === "editor_snapshot") {
|
|
23208
|
+
if (isWatchedFilePreview) {
|
|
23209
|
+
setStatus("Ignored editor snapshot because this preview follows its watched file on disk.", "warning");
|
|
23210
|
+
return;
|
|
23211
|
+
}
|
|
22706
23212
|
if (typeof message.requestId === "string" && pendingRequestId && message.requestId !== pendingRequestId) {
|
|
22707
23213
|
return;
|
|
22708
23214
|
}
|
|
@@ -22726,6 +23232,10 @@
|
|
|
22726
23232
|
}
|
|
22727
23233
|
|
|
22728
23234
|
if (message.type === "studio_document") {
|
|
23235
|
+
if (isWatchedFilePreview) {
|
|
23236
|
+
setStatus("Ignored document replacement because this preview follows its watched file on disk.", "warning");
|
|
23237
|
+
return;
|
|
23238
|
+
}
|
|
22729
23239
|
const nextDoc = message.document;
|
|
22730
23240
|
if (!nextDoc || typeof nextDoc !== "object" || typeof nextDoc.text !== "string") {
|
|
22731
23241
|
return;
|
|
@@ -22763,7 +23273,7 @@
|
|
|
22763
23273
|
draftId: typeof nextDoc.draftId === "string" && nextDoc.draftId.trim() ? nextDoc.draftId.trim() : null,
|
|
22764
23274
|
});
|
|
22765
23275
|
if (nextPath) {
|
|
22766
|
-
markFileBackedBaseline(nextDoc.text);
|
|
23276
|
+
markFileBackedBaseline(nextDoc.text, nextDoc.diskRevision);
|
|
22767
23277
|
}
|
|
22768
23278
|
refreshResponseUi();
|
|
22769
23279
|
setStatus(
|
|
@@ -22906,6 +23416,7 @@
|
|
|
22906
23416
|
|
|
22907
23417
|
if (message.type === "busy") {
|
|
22908
23418
|
if (typeof message.requestId === "string") {
|
|
23419
|
+
pendingSaveOperations.delete(message.requestId);
|
|
22909
23420
|
failPendingCompanionLaunch(message.requestId, "Studio could not start the companion editor because another request was busy.");
|
|
22910
23421
|
}
|
|
22911
23422
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
@@ -22927,6 +23438,7 @@
|
|
|
22927
23438
|
|
|
22928
23439
|
if (message.type === "error") {
|
|
22929
23440
|
if (typeof message.requestId === "string") {
|
|
23441
|
+
pendingSaveOperations.delete(message.requestId);
|
|
22930
23442
|
failPendingCompanionLaunch(message.requestId, "Studio could not prepare the companion editor. Return to the originating Studio page for details.");
|
|
22931
23443
|
}
|
|
22932
23444
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
@@ -23033,6 +23545,11 @@
|
|
|
23033
23545
|
if (studioMode !== "full") {
|
|
23034
23546
|
wsParams.set("mode", studioMode);
|
|
23035
23547
|
}
|
|
23548
|
+
if (isWatchedFilePreview && watchedFilePreviewState.path) {
|
|
23549
|
+
wsParams.set("watchPath", watchedFilePreviewState.path);
|
|
23550
|
+
const watchedDocId = initialQueryParams.get("docId") || "";
|
|
23551
|
+
if (watchedDocId) wsParams.set("docId", watchedDocId);
|
|
23552
|
+
}
|
|
23036
23553
|
if (DEBUG_ENABLED) {
|
|
23037
23554
|
wsParams.set("debug", "1");
|
|
23038
23555
|
}
|
|
@@ -23083,6 +23600,14 @@
|
|
|
23083
23600
|
return;
|
|
23084
23601
|
}
|
|
23085
23602
|
|
|
23603
|
+
if (kind === "watch_unauthorized") {
|
|
23604
|
+
clearScheduledReconnect();
|
|
23605
|
+
reconnectAttempt = 0;
|
|
23606
|
+
setWsState("Disconnected");
|
|
23607
|
+
setStatus("Watched preview authorization is unavailable. Reload this tab or open a new watched preview from Studio.", "warning");
|
|
23608
|
+
return;
|
|
23609
|
+
}
|
|
23610
|
+
|
|
23086
23611
|
if (kind === "shutdown") {
|
|
23087
23612
|
clearScheduledReconnect();
|
|
23088
23613
|
reconnectAttempt = 0;
|
|
@@ -23098,14 +23623,28 @@
|
|
|
23098
23623
|
};
|
|
23099
23624
|
|
|
23100
23625
|
socket.addEventListener("open", () => {
|
|
23626
|
+
if (ws !== socket) {
|
|
23627
|
+
try { socket.close(); } catch {}
|
|
23628
|
+
return;
|
|
23629
|
+
}
|
|
23101
23630
|
window.clearTimeout(connectWatchdog);
|
|
23102
23631
|
setWsState("Ready");
|
|
23103
23632
|
setStatus(wasReconnect ? "Reconnected. Syncing…" : "Connected. Syncing…");
|
|
23104
23633
|
sendMessage({ type: "hello" });
|
|
23634
|
+
if (isWatchedFilePreview && watchedFilePreviewState.path) {
|
|
23635
|
+
watchedFilePreviewState.generation = 0;
|
|
23636
|
+
sendMessage({
|
|
23637
|
+
type: "watch_file_subscribe",
|
|
23638
|
+
path: watchedFilePreviewState.path,
|
|
23639
|
+
revision: watchedFilePreviewState.diskRevision || undefined,
|
|
23640
|
+
});
|
|
23641
|
+
setStatus(wasReconnect ? "Reconnected. Resuming watched preview…" : "Connected. Starting watched preview…");
|
|
23642
|
+
}
|
|
23105
23643
|
reconnectAttempt = 0;
|
|
23106
23644
|
});
|
|
23107
23645
|
|
|
23108
23646
|
socket.addEventListener("message", (event) => {
|
|
23647
|
+
if (ws !== socket) return;
|
|
23109
23648
|
try {
|
|
23110
23649
|
const message = JSON.parse(event.data);
|
|
23111
23650
|
handleServerMessage(message);
|
|
@@ -23116,6 +23655,7 @@
|
|
|
23116
23655
|
});
|
|
23117
23656
|
|
|
23118
23657
|
socket.addEventListener("close", (event) => {
|
|
23658
|
+
if (ws !== socket && !disconnectHandled) return;
|
|
23119
23659
|
if (event && event.code === 4001) {
|
|
23120
23660
|
handleDisconnect("invalidated", 4001);
|
|
23121
23661
|
return;
|
|
@@ -23124,6 +23664,10 @@
|
|
|
23124
23664
|
handleDisconnect("full_conflict", 4004);
|
|
23125
23665
|
return;
|
|
23126
23666
|
}
|
|
23667
|
+
if (event && event.code === 4003) {
|
|
23668
|
+
handleDisconnect("watch_unauthorized", 4003);
|
|
23669
|
+
return;
|
|
23670
|
+
}
|
|
23127
23671
|
if (event && event.code === 1001) {
|
|
23128
23672
|
handleDisconnect("shutdown", 1001);
|
|
23129
23673
|
return;
|
|
@@ -23133,6 +23677,7 @@
|
|
|
23133
23677
|
});
|
|
23134
23678
|
|
|
23135
23679
|
socket.addEventListener("error", () => {
|
|
23680
|
+
if (ws !== socket) return;
|
|
23136
23681
|
handleDisconnect("error");
|
|
23137
23682
|
});
|
|
23138
23683
|
}
|
|
@@ -23361,6 +23906,25 @@
|
|
|
23361
23906
|
});
|
|
23362
23907
|
}
|
|
23363
23908
|
|
|
23909
|
+
if (watchedOpenEditableBtn) {
|
|
23910
|
+
watchedOpenEditableBtn.addEventListener("click", () => {
|
|
23911
|
+
const path = watchedFilePreviewState.path;
|
|
23912
|
+
if (!path) {
|
|
23913
|
+
setStatus("This watched preview no longer has a file path.", "warning");
|
|
23914
|
+
return;
|
|
23915
|
+
}
|
|
23916
|
+
try {
|
|
23917
|
+
openFileBackedStudioEditorTab(path, {
|
|
23918
|
+
label: sourceState && sourceState.label ? sourceState.label : basenameForStudioPath(path),
|
|
23919
|
+
resourceDir: getCurrentResourceDirValue() || dirnameForDisplayPath(path),
|
|
23920
|
+
});
|
|
23921
|
+
setStatus("Opening watched file in a separate editable tab.");
|
|
23922
|
+
} catch (error) {
|
|
23923
|
+
setStatus(error && error.message ? error.message : String(error || "Could not open editable tab."), "warning");
|
|
23924
|
+
}
|
|
23925
|
+
});
|
|
23926
|
+
}
|
|
23927
|
+
|
|
23364
23928
|
updatePaneFocusButtons();
|
|
23365
23929
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
23366
23930
|
window.addEventListener("pagehide", () => {
|
|
@@ -23882,100 +24446,249 @@
|
|
|
23882
24446
|
setFooterThemeMenuOpen(false);
|
|
23883
24447
|
});
|
|
23884
24448
|
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
if (
|
|
23888
|
-
|
|
23889
|
-
|
|
24449
|
+
function abandonPendingSaveRequest(requestId) {
|
|
24450
|
+
pendingSaveOperations.delete(requestId);
|
|
24451
|
+
if (requestId) clearArmedTitleAttention(requestId);
|
|
24452
|
+
if (pendingRequestId === requestId) {
|
|
24453
|
+
pendingRequestId = null;
|
|
24454
|
+
pendingKind = null;
|
|
23890
24455
|
}
|
|
24456
|
+
stickyStudioKind = null;
|
|
24457
|
+
setBusy(false);
|
|
24458
|
+
setWsState("Ready");
|
|
24459
|
+
}
|
|
24460
|
+
|
|
24461
|
+
function sendEditorSaveAsRequest(path, content, overwrite, expectedRevision) {
|
|
24462
|
+
const cleanPath = String(path || "").trim();
|
|
24463
|
+
if (!cleanPath) {
|
|
24464
|
+
setStatus("Save cancelled: path is required.", "warning");
|
|
24465
|
+
return false;
|
|
24466
|
+
}
|
|
24467
|
+
const requestId = beginUiAction("save_as");
|
|
24468
|
+
if (!requestId) return false;
|
|
24469
|
+
const operation = {
|
|
24470
|
+
kind: "save_as",
|
|
24471
|
+
path: cleanPath,
|
|
24472
|
+
content: String(content ?? ""),
|
|
24473
|
+
overwrite: overwrite === true,
|
|
24474
|
+
expectedRevision: normalizeStudioDiskRevision(expectedRevision),
|
|
24475
|
+
};
|
|
24476
|
+
pendingSaveOperations.set(requestId, operation);
|
|
24477
|
+
if (!sendMessage({
|
|
24478
|
+
type: "save_as_request",
|
|
24479
|
+
requestId,
|
|
24480
|
+
path: operation.path,
|
|
24481
|
+
content: operation.content,
|
|
24482
|
+
overwrite: operation.overwrite,
|
|
24483
|
+
expectedRevision: operation.expectedRevision || undefined,
|
|
24484
|
+
})) {
|
|
24485
|
+
abandonPendingSaveRequest(requestId);
|
|
24486
|
+
return false;
|
|
24487
|
+
}
|
|
24488
|
+
return true;
|
|
24489
|
+
}
|
|
23891
24490
|
|
|
23892
|
-
|
|
23893
|
-
|
|
23894
|
-
const
|
|
24491
|
+
async function openEditorSaveAsDialog(options) {
|
|
24492
|
+
if (uiBusy) return false;
|
|
24493
|
+
const settings = options && typeof options === "object" ? options : {};
|
|
24494
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
24495
|
+
const currentPath = getEffectiveSavePath();
|
|
24496
|
+
const label = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
|
|
24497
|
+
const suggested = typeof settings.suggestedPath === "string" && settings.suggestedPath.trim()
|
|
24498
|
+
? settings.suggestedPath.trim()
|
|
24499
|
+
: (currentPath || (resourceDir ? resourceDir.replace(/\/$/, "") + "/" + label : "./draft.md"));
|
|
23895
24500
|
const path = await requestStudioTextInput("Save editor content as:", suggested, {
|
|
23896
24501
|
title: "Save editor as",
|
|
23897
24502
|
confirmLabel: "Save",
|
|
24503
|
+
inputLabel: "File path",
|
|
23898
24504
|
});
|
|
23899
|
-
if (
|
|
23900
|
-
|
|
23901
|
-
|
|
23902
|
-
|
|
24505
|
+
if (path === null) {
|
|
24506
|
+
if (settings.reportCancellation === true) setStatus("Save As cancelled; editor changes were kept.", "warning");
|
|
24507
|
+
return false;
|
|
24508
|
+
}
|
|
24509
|
+
const content = Object.prototype.hasOwnProperty.call(settings, "content")
|
|
24510
|
+
? String(settings.content ?? "")
|
|
24511
|
+
: sourceTextEl.value;
|
|
24512
|
+
return sendEditorSaveAsRequest(path, content, false);
|
|
24513
|
+
}
|
|
23903
24514
|
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
24515
|
+
function sendEditorSaveOverRequest(options) {
|
|
24516
|
+
const settings = options && typeof options === "object" ? options : {};
|
|
24517
|
+
const path = typeof settings.path === "string" && settings.path.trim()
|
|
24518
|
+
? settings.path.trim()
|
|
24519
|
+
: (sourceState && sourceState.path ? sourceState.path : "");
|
|
24520
|
+
if (!path) {
|
|
24521
|
+
setStatus("Save editor requires a file-backed document. Use Save editor as… for a new file.", "warning");
|
|
24522
|
+
return false;
|
|
24523
|
+
}
|
|
24524
|
+
const requestId = beginUiAction("save_over");
|
|
24525
|
+
if (!requestId) return false;
|
|
24526
|
+
const operation = {
|
|
24527
|
+
kind: "save_over",
|
|
23907
24528
|
path,
|
|
23908
|
-
content,
|
|
23909
|
-
|
|
24529
|
+
content: Object.prototype.hasOwnProperty.call(settings, "content")
|
|
24530
|
+
? String(settings.content ?? "")
|
|
24531
|
+
: sourceTextEl.value,
|
|
24532
|
+
expectedRevision: Object.prototype.hasOwnProperty.call(settings, "expectedRevision")
|
|
24533
|
+
? normalizeStudioDiskRevision(settings.expectedRevision)
|
|
24534
|
+
: fileBackedDiskRevision,
|
|
24535
|
+
force: settings.force === true,
|
|
24536
|
+
};
|
|
24537
|
+
pendingSaveOperations.set(requestId, operation);
|
|
24538
|
+
if (!sendMessage({
|
|
24539
|
+
type: "save_over_request",
|
|
24540
|
+
requestId,
|
|
24541
|
+
path: operation.path,
|
|
24542
|
+
content: operation.content,
|
|
24543
|
+
expectedRevision: operation.expectedRevision || undefined,
|
|
24544
|
+
force: operation.force,
|
|
24545
|
+
})) {
|
|
24546
|
+
abandonPendingSaveRequest(requestId);
|
|
24547
|
+
return false;
|
|
24548
|
+
}
|
|
24549
|
+
return true;
|
|
24550
|
+
}
|
|
23910
24551
|
|
|
23911
|
-
|
|
24552
|
+
async function requestEditorRefreshFromDisk(options) {
|
|
24553
|
+
const settings = options && typeof options === "object" ? options : {};
|
|
24554
|
+
if (uiBusy) return false;
|
|
24555
|
+
const path = typeof settings.path === "string" && settings.path.trim()
|
|
24556
|
+
? settings.path.trim()
|
|
24557
|
+
: (sourceState && sourceState.path ? sourceState.path : "");
|
|
24558
|
+
if (!path) {
|
|
24559
|
+
setStatus("Refresh from disk requires a file-backed editor. Open one from Files or use /studio-editor-only <path>.", "warning");
|
|
24560
|
+
return false;
|
|
24561
|
+
}
|
|
24562
|
+
if (settings.skipConfirm !== true && editorDiffersFromFileBackedBaseline()) {
|
|
24563
|
+
const confirmed = await requestStudioConfirmation(
|
|
24564
|
+
"Replace the current editor contents with the latest version from disk? Unsaved editor changes will be lost.\n\n" + path,
|
|
24565
|
+
{ title: "Refresh from disk?", confirmLabel: "Replace", destructive: true },
|
|
24566
|
+
);
|
|
24567
|
+
if (!confirmed) return false;
|
|
24568
|
+
}
|
|
24569
|
+
const requestId = beginUiAction("refresh_from_disk");
|
|
24570
|
+
if (!requestId) return false;
|
|
24571
|
+
if (!sendMessage({ type: "refresh_from_disk_request", requestId, path })) {
|
|
23912
24572
|
pendingRequestId = null;
|
|
23913
24573
|
pendingKind = null;
|
|
24574
|
+
stickyStudioKind = null;
|
|
23914
24575
|
setBusy(false);
|
|
24576
|
+
setWsState("Ready");
|
|
24577
|
+
return false;
|
|
23915
24578
|
}
|
|
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
|
-
}
|
|
24579
|
+
return true;
|
|
24580
|
+
}
|
|
23924
24581
|
|
|
23925
|
-
|
|
23926
|
-
|
|
24582
|
+
async function handleEditorSaveConflict(message) {
|
|
24583
|
+
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
24584
|
+
const operation = pendingSaveOperations.get(requestId) || {
|
|
24585
|
+
kind: "save_over",
|
|
24586
|
+
path: typeof message.path === "string" ? message.path : (sourceState.path || ""),
|
|
24587
|
+
content: sourceTextEl.value,
|
|
24588
|
+
expectedRevision: fileBackedDiskRevision,
|
|
24589
|
+
force: false,
|
|
24590
|
+
};
|
|
24591
|
+
abandonPendingSaveRequest(requestId);
|
|
24592
|
+
const conflictPath = typeof message.path === "string" && message.path.trim() ? message.path.trim() : operation.path;
|
|
24593
|
+
const canOverwrite = message.canOverwrite !== false;
|
|
24594
|
+
const detail = (typeof message.message === "string" && message.message.trim()
|
|
24595
|
+
? message.message.trim()
|
|
24596
|
+
: "The file changed on disk after Studio loaded it.")
|
|
24597
|
+
+ "\n\n" + conflictPath
|
|
24598
|
+
+ "\n\nReload replaces the editor with disk content. Save As keeps both versions."
|
|
24599
|
+
+ (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.");
|
|
24600
|
+
setStatus("Save paused because the file changed on disk.", "warning");
|
|
24601
|
+
const decision = await openStudioDecision({
|
|
24602
|
+
mode: "confirm",
|
|
24603
|
+
title: "File changed on disk",
|
|
24604
|
+
message: detail,
|
|
24605
|
+
cancelLabel: "Cancel",
|
|
24606
|
+
tertiaryLabel: "Reload",
|
|
24607
|
+
tertiaryValue: "reload",
|
|
24608
|
+
secondaryLabel: "Save As…",
|
|
24609
|
+
secondaryValue: "save-as",
|
|
23927
24610
|
confirmLabel: "Overwrite",
|
|
24611
|
+
confirmDisabled: !canOverwrite,
|
|
23928
24612
|
destructive: true,
|
|
23929
24613
|
});
|
|
23930
|
-
if (
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
24614
|
+
if (decision === "reload") {
|
|
24615
|
+
await requestEditorRefreshFromDisk({ path: conflictPath, skipConfirm: true });
|
|
24616
|
+
return;
|
|
24617
|
+
}
|
|
24618
|
+
if (decision === "save-as") {
|
|
24619
|
+
await openEditorSaveAsDialog({ content: operation.content, suggestedPath: conflictPath, reportCancellation: true });
|
|
24620
|
+
return;
|
|
24621
|
+
}
|
|
24622
|
+
if (decision === true && canOverwrite) {
|
|
24623
|
+
sendEditorSaveOverRequest({
|
|
24624
|
+
path: conflictPath,
|
|
24625
|
+
content: operation.content,
|
|
24626
|
+
expectedRevision: message.currentRevision,
|
|
24627
|
+
force: true,
|
|
24628
|
+
});
|
|
24629
|
+
return;
|
|
24630
|
+
}
|
|
24631
|
+
setStatus("Save cancelled; editor changes were kept.", "warning");
|
|
24632
|
+
}
|
|
23934
24633
|
|
|
23935
|
-
|
|
23936
|
-
const
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
path:
|
|
24634
|
+
async function handleEditorSaveAsConflict(message) {
|
|
24635
|
+
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
24636
|
+
const operation = pendingSaveOperations.get(requestId) || {
|
|
24637
|
+
kind: "save_as",
|
|
24638
|
+
path: typeof message.path === "string" ? message.path : "",
|
|
23940
24639
|
content: sourceTextEl.value,
|
|
24640
|
+
overwrite: false,
|
|
24641
|
+
expectedRevision: null,
|
|
24642
|
+
};
|
|
24643
|
+
abandonPendingSaveRequest(requestId);
|
|
24644
|
+
const conflictPath = typeof message.path === "string" && message.path.trim() ? message.path.trim() : operation.path;
|
|
24645
|
+
const targetExists = Boolean(normalizeStudioDiskRevision(message.currentRevision));
|
|
24646
|
+
const unsafeReplacement = message.reason === "location-changed" || message.reason === "hard-linked-file";
|
|
24647
|
+
const canCommitHere = !unsafeReplacement;
|
|
24648
|
+
setStatus(unsafeReplacement
|
|
24649
|
+
? "Save As cannot replace this target safely; choose another path."
|
|
24650
|
+
: (targetExists
|
|
24651
|
+
? "Save As paused because the target already exists."
|
|
24652
|
+
: "Save As paused because the target changed while confirmation was open."), "warning");
|
|
24653
|
+
const decision = await openStudioDecision({
|
|
24654
|
+
mode: "confirm",
|
|
24655
|
+
title: unsafeReplacement ? "Cannot replace existing file" : (targetExists ? "Replace existing file?" : "Create file at this path?"),
|
|
24656
|
+
message: (typeof message.message === "string" && message.message.trim()
|
|
24657
|
+
? message.message.trim()
|
|
24658
|
+
: (targetExists ? "A file already exists at this location." : "The previous replacement target is no longer present."))
|
|
24659
|
+
+ (unsafeReplacement
|
|
24660
|
+
? "\n\nStudio will not replace a symlink, moved path, or hard-linked file. Choose another location instead."
|
|
24661
|
+
: (targetExists ? "\n\nReplacing it cannot be undone." : "\n\nCreating it will keep the current editor text at this path.")),
|
|
24662
|
+
cancelLabel: "Cancel",
|
|
24663
|
+
secondaryLabel: "Choose another…",
|
|
24664
|
+
secondaryValue: "choose-another",
|
|
24665
|
+
confirmLabel: targetExists ? "Replace" : "Create",
|
|
24666
|
+
confirmDisabled: !canCommitHere,
|
|
24667
|
+
destructive: targetExists,
|
|
23941
24668
|
});
|
|
23942
|
-
|
|
23943
|
-
|
|
23944
|
-
|
|
23945
|
-
pendingKind = null;
|
|
23946
|
-
setBusy(false);
|
|
24669
|
+
if (decision === "choose-another") {
|
|
24670
|
+
await openEditorSaveAsDialog({ content: operation.content, suggestedPath: conflictPath, reportCancellation: true });
|
|
24671
|
+
return;
|
|
23947
24672
|
}
|
|
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
|
-
}
|
|
24673
|
+
if (decision === true && canCommitHere) {
|
|
24674
|
+
sendEditorSaveAsRequest(conflictPath, operation.content, true, message.currentRevision);
|
|
24675
|
+
return;
|
|
24676
|
+
}
|
|
24677
|
+
setStatus("Save As cancelled; editor changes were kept.", "warning");
|
|
24678
|
+
}
|
|
23964
24679
|
|
|
23965
|
-
|
|
23966
|
-
|
|
24680
|
+
saveAsBtn.addEventListener("click", () => {
|
|
24681
|
+
void openEditorSaveAsDialog();
|
|
24682
|
+
});
|
|
23967
24683
|
|
|
23968
|
-
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
});
|
|
24684
|
+
saveOverBtn.addEventListener("click", () => {
|
|
24685
|
+
if (uiBusy) return;
|
|
24686
|
+
sendEditorSaveOverRequest();
|
|
24687
|
+
});
|
|
23973
24688
|
|
|
23974
|
-
|
|
23975
|
-
|
|
23976
|
-
|
|
23977
|
-
setBusy(false);
|
|
23978
|
-
}
|
|
24689
|
+
if (refreshFromDiskBtn) {
|
|
24690
|
+
refreshFromDiskBtn.addEventListener("click", () => {
|
|
24691
|
+
void requestEditorRefreshFromDisk();
|
|
23979
24692
|
});
|
|
23980
24693
|
}
|
|
23981
24694
|
|
|
@@ -24709,7 +25422,16 @@
|
|
|
24709
25422
|
resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
|
|
24710
25423
|
}
|
|
24711
25424
|
setSourceState(initialSourceState);
|
|
24712
|
-
if (initialSourceState.path) markFileBackedBaseline(sourceTextEl.value);
|
|
25425
|
+
if (initialSourceState.path) markFileBackedBaseline(sourceTextEl.value, initialDiskRevision);
|
|
25426
|
+
if (isWatchedFilePreview) {
|
|
25427
|
+
sourceTextEl.readOnly = true;
|
|
25428
|
+
sourceTextEl.setAttribute("aria-readonly", "true");
|
|
25429
|
+
sourceTextEl.title = "Read-only disk-backed source. Open a file tab to edit and save safely.";
|
|
25430
|
+
editorView = "markdown";
|
|
25431
|
+
rightView = "editor-preview";
|
|
25432
|
+
followLatest = false;
|
|
25433
|
+
if (document.body && document.body.classList) document.body.classList.add("watched-file-preview");
|
|
25434
|
+
}
|
|
24713
25435
|
refreshResponseUi();
|
|
24714
25436
|
updateAnnotatedReplyHeaderButton();
|
|
24715
25437
|
setActivePane(initialPaneFocusTarget === "off" ? "left" : initialPaneFocusTarget);
|
|
@@ -24722,7 +25444,9 @@
|
|
|
24722
25444
|
|
|
24723
25445
|
const initialDetectedLang = detectLanguageFromName(initialSourceState.path || initialSourceState.label || "");
|
|
24724
25446
|
const storedLang = readStoredEditorLanguage();
|
|
24725
|
-
setEditorLanguage(initialDetectedLang || storedLang || "markdown"
|
|
25447
|
+
setEditorLanguage(initialDetectedLang || storedLang || "markdown", {
|
|
25448
|
+
allowWatchedFileUpdate: isWatchedFilePreview,
|
|
25449
|
+
});
|
|
24726
25450
|
|
|
24727
25451
|
const storedLineNumbersEnabled = readStoredEditorLineNumbersEnabled();
|
|
24728
25452
|
const initialLineNumbersEnabled = storedLineNumbersEnabled ?? Boolean(lineNumbersSelect && lineNumbersSelect.value === "on");
|