pi-studio 0.9.17 → 0.9.19
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 +4 -4
- package/client/studio-client.js +945 -102
- package/client/studio.css +190 -3
- package/index.ts +394 -56
- package/package.json +1 -1
package/client/studio-client.js
CHANGED
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
const shortcutsBtn = document.getElementById("shortcutsBtn");
|
|
132
132
|
const shortcutsOverlayEl = document.getElementById("shortcutsOverlay");
|
|
133
133
|
const shortcutsDialogEl = document.getElementById("shortcutsDialog");
|
|
134
|
+
const shortcutsBodyEl = document.getElementById("shortcutsBody");
|
|
134
135
|
const shortcutsCloseBtn = document.getElementById("shortcutsCloseBtn");
|
|
135
136
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
136
137
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
@@ -211,6 +212,18 @@
|
|
|
211
212
|
let studioHtmlFocusFullscreenBtn = null;
|
|
212
213
|
let studioHtmlFocusLastFocusedEl = null;
|
|
213
214
|
let studioHtmlFocusRestoreState = null;
|
|
215
|
+
let studioImageFocusOverlayEl = null;
|
|
216
|
+
let studioImageFocusDialogEl = null;
|
|
217
|
+
let studioImageFocusSlotEl = null;
|
|
218
|
+
let studioImageFocusImgEl = null;
|
|
219
|
+
let studioImageFocusTitleEl = null;
|
|
220
|
+
let studioImageFocusOpenLinkEl = null;
|
|
221
|
+
let studioImageFocusFullscreenBtn = null;
|
|
222
|
+
let studioImageFocusCloseBtn = null;
|
|
223
|
+
let studioImageFocusZoomLabelEl = null;
|
|
224
|
+
let studioImageFocusLastFocusedEl = null;
|
|
225
|
+
let studioImageFocusZoomMode = "fit";
|
|
226
|
+
let studioImageFocusZoom = 1;
|
|
214
227
|
let pendingRequestId = null;
|
|
215
228
|
let pendingKind = null;
|
|
216
229
|
let stickyStudioKind = null;
|
|
@@ -567,6 +580,7 @@
|
|
|
567
580
|
toolName: typeof entry.toolName === "string" ? entry.toolName : "tool",
|
|
568
581
|
label: parseNonEmptyString(entry.label),
|
|
569
582
|
argsSummary: parseNonEmptyString(entry.argsSummary),
|
|
583
|
+
args: parseNonEmptyString(entry.args),
|
|
570
584
|
output: typeof entry.output === "string" ? entry.output : "",
|
|
571
585
|
images: Array.isArray(entry.images)
|
|
572
586
|
? entry.images.map((image, imageIndex) => normalizeTraceImage(image, imageIndex)).filter(Boolean)
|
|
@@ -847,8 +861,9 @@
|
|
|
847
861
|
? ("Tool: " + String(entry.toolName || "tool") + " — " + entry.label)
|
|
848
862
|
: ("Tool: " + String(entry.toolName || "tool"));
|
|
849
863
|
const parts = [header];
|
|
850
|
-
|
|
851
|
-
|
|
864
|
+
const inputText = String(entry.args || entry.argsSummary || "").trim();
|
|
865
|
+
if (inputText) {
|
|
866
|
+
parts.push("Input:\n" + inputText);
|
|
852
867
|
}
|
|
853
868
|
if (String(entry.output || "").trim()) {
|
|
854
869
|
parts.push("Output:\n" + String(entry.output || "").trim());
|
|
@@ -1914,6 +1929,8 @@
|
|
|
1914
1929
|
matlab: { label: "MATLAB", exts: ["m"] },
|
|
1915
1930
|
latex: { label: "LaTeX", exts: ["tex", "latex"] },
|
|
1916
1931
|
diff: { label: "Diff", exts: ["diff", "patch"] },
|
|
1932
|
+
csv: { label: "CSV", exts: ["csv"] },
|
|
1933
|
+
tsv: { label: "TSV", exts: ["tsv"] },
|
|
1917
1934
|
// Languages accepted for upload/detect but without syntax highlighting
|
|
1918
1935
|
java: { label: "Java", exts: ["java"] },
|
|
1919
1936
|
go: { label: "Go", exts: ["go"] },
|
|
@@ -1941,6 +1958,9 @@
|
|
|
1941
1958
|
const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
|
|
1942
1959
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
1943
1960
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
1961
|
+
const DELIMITED_PREVIEW_MAX_DATA_ROWS = 200;
|
|
1962
|
+
const DELIMITED_PREVIEW_MAX_COLUMNS = 50;
|
|
1963
|
+
const DELIMITED_PREVIEW_MAX_CELL_CHARS = 500;
|
|
1944
1964
|
const previewPendingTimers = new WeakMap();
|
|
1945
1965
|
const htmlArtifactFramesById = new Map();
|
|
1946
1966
|
let sourcePreviewRenderTimer = null;
|
|
@@ -2591,6 +2611,9 @@
|
|
|
2591
2611
|
}
|
|
2592
2612
|
|
|
2593
2613
|
function getIdleStatus() {
|
|
2614
|
+
if (isEditorOnlyMode) {
|
|
2615
|
+
return "Editor-only mode: edit, load, annotate, preview, save, suggest, or refresh file-backed text.";
|
|
2616
|
+
}
|
|
2594
2617
|
return "Edit, load, or annotate text, then run, save, send to pi editor, or critique.";
|
|
2595
2618
|
}
|
|
2596
2619
|
|
|
@@ -3158,7 +3181,7 @@
|
|
|
3158
3181
|
|
|
3159
3182
|
function updateSourceBadge() {
|
|
3160
3183
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
3161
|
-
sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label;
|
|
3184
|
+
sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label + (hasRefreshableFilePath() ? " · file" : "");
|
|
3162
3185
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
3163
3186
|
if (sourceBadgeEl) {
|
|
3164
3187
|
sourceBadgeEl.title = descriptor.fileBacked
|
|
@@ -3170,9 +3193,11 @@
|
|
|
3170
3193
|
if (isFileBacked) {
|
|
3171
3194
|
var fileBackedResourceDir = getCurrentResourceDirValue() || dirnameForDisplayPath(sourceState.path);
|
|
3172
3195
|
if (resourceDirInput) resourceDirInput.value = fileBackedResourceDir;
|
|
3173
|
-
if (resourceDirLabel)
|
|
3196
|
+
if (resourceDirLabel) {
|
|
3197
|
+
resourceDirLabel.textContent = fileBackedResourceDir ? ("Resource dir: " + fileBackedResourceDir) : "Resource dir: file directory";
|
|
3198
|
+
resourceDirLabel.hidden = false;
|
|
3199
|
+
}
|
|
3174
3200
|
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
3175
|
-
if (resourceDirLabel) resourceDirLabel.hidden = true;
|
|
3176
3201
|
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
3177
3202
|
} else {
|
|
3178
3203
|
// Restore to label if dir is set, otherwise show button
|
|
@@ -3666,6 +3691,12 @@
|
|
|
3666
3691
|
&& typeof studioHtmlFocusShellEl.contains === "function"
|
|
3667
3692
|
&& studioHtmlFocusShellEl.contains(event.target)
|
|
3668
3693
|
);
|
|
3694
|
+
const imageFocusOwnsEvent = Boolean(
|
|
3695
|
+
studioImageFocusDialogEl
|
|
3696
|
+
&& event.target
|
|
3697
|
+
&& typeof studioImageFocusDialogEl.contains === "function"
|
|
3698
|
+
&& studioImageFocusDialogEl.contains(event.target)
|
|
3699
|
+
);
|
|
3669
3700
|
const quizOwnsEvent = Boolean(
|
|
3670
3701
|
quizDialogEl
|
|
3671
3702
|
&& event.target
|
|
@@ -3691,6 +3722,14 @@
|
|
|
3691
3722
|
return;
|
|
3692
3723
|
}
|
|
3693
3724
|
|
|
3725
|
+
if (isStudioImageFocusOpen() && plainEscape) {
|
|
3726
|
+
event.preventDefault();
|
|
3727
|
+
closeStudioImageFocusViewer();
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
if (handleStudioImageFocusShortcut(event)) return;
|
|
3732
|
+
|
|
3694
3733
|
if (isScratchpadOpen() && plainEscape) {
|
|
3695
3734
|
event.preventDefault();
|
|
3696
3735
|
closeScratchpad();
|
|
@@ -3703,6 +3742,8 @@
|
|
|
3703
3742
|
return;
|
|
3704
3743
|
}
|
|
3705
3744
|
|
|
3745
|
+
if (handleShortcutsScrollShortcut(event)) return;
|
|
3746
|
+
|
|
3706
3747
|
if (isReviewNotesOpen() && plainEscape) {
|
|
3707
3748
|
event.preventDefault();
|
|
3708
3749
|
closeReviewNotes();
|
|
@@ -3715,7 +3756,7 @@
|
|
|
3715
3756
|
return;
|
|
3716
3757
|
}
|
|
3717
3758
|
|
|
3718
|
-
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
|
|
3759
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || imageFocusOwnsEvent || quizOwnsEvent) {
|
|
3719
3760
|
return;
|
|
3720
3761
|
}
|
|
3721
3762
|
|
|
@@ -3890,6 +3931,22 @@
|
|
|
3890
3931
|
}
|
|
3891
3932
|
}
|
|
3892
3933
|
|
|
3934
|
+
function formatStudioExportTimestamp(date) {
|
|
3935
|
+
const value = date instanceof Date ? date : new Date();
|
|
3936
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
3937
|
+
try {
|
|
3938
|
+
return String(value.getFullYear())
|
|
3939
|
+
+ pad(value.getMonth() + 1)
|
|
3940
|
+
+ pad(value.getDate())
|
|
3941
|
+
+ "-"
|
|
3942
|
+
+ pad(value.getHours())
|
|
3943
|
+
+ pad(value.getMinutes())
|
|
3944
|
+
+ pad(value.getSeconds());
|
|
3945
|
+
} catch {
|
|
3946
|
+
return String(Date.now());
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3893
3950
|
function normalizeHistoryKind(kind) {
|
|
3894
3951
|
return kind === "critique" ? "critique" : "annotation";
|
|
3895
3952
|
}
|
|
@@ -4257,9 +4314,202 @@
|
|
|
4257
4314
|
return marker + (lang ? lang : "") + newline + source + newline + marker;
|
|
4258
4315
|
}
|
|
4259
4316
|
|
|
4317
|
+
function getDelimitedTextPreviewConfig(language) {
|
|
4318
|
+
const lang = normalizeFenceLanguage(language || "");
|
|
4319
|
+
if (lang === "csv") return { kind: "csv", label: "CSV", delimiter: "," };
|
|
4320
|
+
if (lang === "tsv") return { kind: "tsv", label: "TSV", delimiter: "\t" };
|
|
4321
|
+
return null;
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
function parseDelimitedTextRows(text, delimiter, maxRows) {
|
|
4325
|
+
const source = String(text || "").replace(/^\uFEFF/, "");
|
|
4326
|
+
const limit = Math.max(1, Number(maxRows) || (DELIMITED_PREVIEW_MAX_DATA_ROWS + 1));
|
|
4327
|
+
const rows = [];
|
|
4328
|
+
let row = [];
|
|
4329
|
+
let cell = "";
|
|
4330
|
+
let inQuotes = false;
|
|
4331
|
+
let truncatedRows = false;
|
|
4332
|
+
|
|
4333
|
+
const pushCell = () => {
|
|
4334
|
+
row.push(cell);
|
|
4335
|
+
cell = "";
|
|
4336
|
+
};
|
|
4337
|
+
const pushRow = (index) => {
|
|
4338
|
+
pushCell();
|
|
4339
|
+
rows.push(row);
|
|
4340
|
+
row = [];
|
|
4341
|
+
if (rows.length >= limit) {
|
|
4342
|
+
truncatedRows = index < source.length - 1;
|
|
4343
|
+
return true;
|
|
4344
|
+
}
|
|
4345
|
+
return false;
|
|
4346
|
+
};
|
|
4347
|
+
|
|
4348
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
4349
|
+
if (rows.length >= limit) {
|
|
4350
|
+
truncatedRows = true;
|
|
4351
|
+
break;
|
|
4352
|
+
}
|
|
4353
|
+
const ch = source[i];
|
|
4354
|
+
if (inQuotes) {
|
|
4355
|
+
if (ch === '"') {
|
|
4356
|
+
if (source[i + 1] === '"') {
|
|
4357
|
+
cell += '"';
|
|
4358
|
+
i += 1;
|
|
4359
|
+
} else {
|
|
4360
|
+
inQuotes = false;
|
|
4361
|
+
}
|
|
4362
|
+
} else {
|
|
4363
|
+
cell += ch;
|
|
4364
|
+
}
|
|
4365
|
+
continue;
|
|
4366
|
+
}
|
|
4367
|
+
if (ch === '"' && cell === "") {
|
|
4368
|
+
inQuotes = true;
|
|
4369
|
+
continue;
|
|
4370
|
+
}
|
|
4371
|
+
if (ch === delimiter) {
|
|
4372
|
+
pushCell();
|
|
4373
|
+
continue;
|
|
4374
|
+
}
|
|
4375
|
+
if (ch === "\n") {
|
|
4376
|
+
if (pushRow(i)) break;
|
|
4377
|
+
continue;
|
|
4378
|
+
}
|
|
4379
|
+
if (ch === "\r") {
|
|
4380
|
+
if (source[i + 1] === "\n") i += 1;
|
|
4381
|
+
if (pushRow(i)) break;
|
|
4382
|
+
continue;
|
|
4383
|
+
}
|
|
4384
|
+
cell += ch;
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
if (!truncatedRows && rows.length < limit && (cell.length > 0 || row.length > 0)) {
|
|
4388
|
+
pushCell();
|
|
4389
|
+
rows.push(row);
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4392
|
+
return { rows, truncatedRows };
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
function buildDelimitedTextPreviewModel(text, language) {
|
|
4396
|
+
const config = getDelimitedTextPreviewConfig(language);
|
|
4397
|
+
if (!config) return null;
|
|
4398
|
+
const parsed = parseDelimitedTextRows(text, config.delimiter, DELIMITED_PREVIEW_MAX_DATA_ROWS + 1);
|
|
4399
|
+
const rows = parsed.rows;
|
|
4400
|
+
const rawColumnCount = rows.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0);
|
|
4401
|
+
const columnCount = Math.min(rawColumnCount, DELIMITED_PREVIEW_MAX_COLUMNS);
|
|
4402
|
+
const header = rows[0] || [];
|
|
4403
|
+
const dataRows = rows.slice(1);
|
|
4404
|
+
return {
|
|
4405
|
+
...config,
|
|
4406
|
+
rows,
|
|
4407
|
+
header,
|
|
4408
|
+
dataRows,
|
|
4409
|
+
rawColumnCount,
|
|
4410
|
+
columnCount,
|
|
4411
|
+
truncatedColumns: rawColumnCount > columnCount,
|
|
4412
|
+
truncatedRows: parsed.truncatedRows,
|
|
4413
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
function getDelimitedHeaderLabel(header, index) {
|
|
4417
|
+
const value = String((header && header[index]) || "").trim();
|
|
4418
|
+
return value || ("Column " + (index + 1));
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
function formatDelimitedPreviewCellHtml(value) {
|
|
4422
|
+
const raw = String(value ?? "");
|
|
4423
|
+
if (raw.length <= DELIMITED_PREVIEW_MAX_CELL_CHARS) return escapeHtml(raw);
|
|
4424
|
+
return escapeHtml(raw.slice(0, DELIMITED_PREVIEW_MAX_CELL_CHARS)) + "<span class='delimited-preview-truncation'>…</span>";
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
function formatDelimitedMarkdownCell(value) {
|
|
4428
|
+
const raw = String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4429
|
+
const shortened = raw.length > DELIMITED_PREVIEW_MAX_CELL_CHARS
|
|
4430
|
+
? raw.slice(0, DELIMITED_PREVIEW_MAX_CELL_CHARS) + "…"
|
|
4431
|
+
: raw;
|
|
4432
|
+
return shortened.replace(/\n/g, "<br>").replace(/\|/g, "\\|").trim() || " ";
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
function buildDelimitedTextPreviewHtml(text, language) {
|
|
4436
|
+
const model = buildDelimitedTextPreviewModel(text, language);
|
|
4437
|
+
if (!model) return "";
|
|
4438
|
+
if (!model.rows.length || model.columnCount <= 0) {
|
|
4439
|
+
return "<div class='delimited-preview rendered-markdown'><div class='delimited-preview-header'><strong>" + escapeHtml(model.label) + " preview</strong></div><pre class='plain-markdown'>No tabular data to preview.</pre></div>";
|
|
4440
|
+
}
|
|
4441
|
+
const columnIndexes = Array.from({ length: model.columnCount }, (_, index) => index);
|
|
4442
|
+
const headerHtml = columnIndexes.map((index) => "<th scope='col'>" + escapeHtml(getDelimitedHeaderLabel(model.header, index)) + "</th>").join("");
|
|
4443
|
+
const bodyHtml = model.dataRows.length
|
|
4444
|
+
? model.dataRows.map((row, rowIndex) => {
|
|
4445
|
+
const cells = columnIndexes.map((index) => {
|
|
4446
|
+
const raw = String((row && row[index]) ?? "");
|
|
4447
|
+
const emptyClass = raw.length === 0 ? " delimited-preview-empty-cell" : "";
|
|
4448
|
+
return "<td class='" + emptyClass.trim() + "'>" + formatDelimitedPreviewCellHtml(raw) + "</td>";
|
|
4449
|
+
}).join("");
|
|
4450
|
+
return "<tr><th scope='row' class='delimited-preview-row-number'>" + String(rowIndex + 1) + "</th>" + cells + "</tr>";
|
|
4451
|
+
}).join("")
|
|
4452
|
+
: "<tr><td colspan='" + String(model.columnCount + 1) + "' class='delimited-preview-empty'>No data rows after the header.</td></tr>";
|
|
4453
|
+
const notices = [];
|
|
4454
|
+
if (model.truncatedRows) notices.push("Showing first " + String(Math.max(0, model.dataRows.length)) + " data rows.");
|
|
4455
|
+
if (model.truncatedColumns) notices.push("Showing first " + String(model.columnCount) + " of " + String(model.rawColumnCount) + " columns.");
|
|
4456
|
+
const noticeHtml = notices.length ? "<div class='preview-warning delimited-preview-notice'>" + escapeHtml(notices.join(" ")) + "</div>" : "";
|
|
4457
|
+
const summaryParts = [String(model.dataRows.length) + (model.truncatedRows ? "+" : "") + " data rows", String(model.rawColumnCount) + " columns"];
|
|
4458
|
+
return "<div class='delimited-preview rendered-markdown'>"
|
|
4459
|
+
+ "<div class='delimited-preview-header'><div><strong>" + escapeHtml(model.label) + " preview</strong><span>" + escapeHtml(summaryParts.join(" · ")) + "</span></div></div>"
|
|
4460
|
+
+ noticeHtml
|
|
4461
|
+
+ "<div class='delimited-preview-table-wrap'><table>"
|
|
4462
|
+
+ "<thead><tr><th scope='col' class='delimited-preview-row-number'>#</th>" + headerHtml + "</tr></thead>"
|
|
4463
|
+
+ "<tbody>" + bodyHtml + "</tbody>"
|
|
4464
|
+
+ "</table></div>"
|
|
4465
|
+
+ "</div>";
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
function buildDelimitedTextPreviewMarkdown(text, language) {
|
|
4469
|
+
const model = buildDelimitedTextPreviewModel(text, language);
|
|
4470
|
+
if (!model) return "";
|
|
4471
|
+
if (!model.rows.length || model.columnCount <= 0) return "_No tabular data to preview._";
|
|
4472
|
+
const columnIndexes = Array.from({ length: model.columnCount }, (_, index) => index);
|
|
4473
|
+
const lines = ["**" + model.label + " preview**", ""];
|
|
4474
|
+
const notices = [];
|
|
4475
|
+
if (model.truncatedRows) notices.push("showing first " + String(Math.max(0, model.dataRows.length)) + " data rows");
|
|
4476
|
+
if (model.truncatedColumns) notices.push("showing first " + String(model.columnCount) + " of " + String(model.rawColumnCount) + " columns");
|
|
4477
|
+
if (notices.length) lines.push("_" + notices.join("; ") + "._", "");
|
|
4478
|
+
lines.push("| " + columnIndexes.map((index) => formatDelimitedMarkdownCell(getDelimitedHeaderLabel(model.header, index))).join(" | ") + " |");
|
|
4479
|
+
lines.push("| " + columnIndexes.map(() => "---").join(" | ") + " |");
|
|
4480
|
+
if (model.dataRows.length) {
|
|
4481
|
+
model.dataRows.forEach((row) => {
|
|
4482
|
+
lines.push("| " + columnIndexes.map((index) => formatDelimitedMarkdownCell(row && row[index])).join(" | ") + " |");
|
|
4483
|
+
});
|
|
4484
|
+
} else {
|
|
4485
|
+
lines.push("| " + columnIndexes.map(() => " ").join(" | ") + " |");
|
|
4486
|
+
}
|
|
4487
|
+
return lines.join("\n");
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
function renderDelimitedTextPreview(targetEl, text, pane, language) {
|
|
4491
|
+
const html = buildDelimitedTextPreviewHtml(text, language || editorLanguage || "");
|
|
4492
|
+
if (!html || !targetEl) return false;
|
|
4493
|
+
if (pane === "source") {
|
|
4494
|
+
sourcePreviewRenderNonce += 1;
|
|
4495
|
+
} else if (pane === "response") {
|
|
4496
|
+
responsePreviewRenderNonce += 1;
|
|
4497
|
+
}
|
|
4498
|
+
clearPreviewJumpHighlight(targetEl);
|
|
4499
|
+
finishPreviewRender(targetEl);
|
|
4500
|
+
targetEl.innerHTML = html;
|
|
4501
|
+
if (pane === "response") {
|
|
4502
|
+
applyPendingResponseScrollReset();
|
|
4503
|
+
scheduleResponsePaneRepaintNudge();
|
|
4504
|
+
}
|
|
4505
|
+
return true;
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4260
4508
|
function prepareEditorTextForPdfExport(text) {
|
|
4261
4509
|
const prepared = prepareEditorTextForPreview(text);
|
|
4262
4510
|
const lang = normalizeFenceLanguage(editorLanguage || "");
|
|
4511
|
+
const delimitedPreview = buildDelimitedTextPreviewMarkdown(prepared, lang);
|
|
4512
|
+
if (delimitedPreview) return delimitedPreview;
|
|
4263
4513
|
if (lang && lang !== "markdown" && lang !== "latex") {
|
|
4264
4514
|
return wrapAsFencedCodeBlock(prepared, lang);
|
|
4265
4515
|
}
|
|
@@ -4269,6 +4519,8 @@
|
|
|
4269
4519
|
function prepareEditorTextForHtmlExport(text) {
|
|
4270
4520
|
const prepared = prepareEditorTextForPreview(text);
|
|
4271
4521
|
const lang = normalizeFenceLanguage(editorLanguage || "");
|
|
4522
|
+
const delimitedPreview = buildDelimitedTextPreviewMarkdown(prepared, lang);
|
|
4523
|
+
if (delimitedPreview) return delimitedPreview;
|
|
4272
4524
|
if (lang && lang !== "markdown" && lang !== "latex") {
|
|
4273
4525
|
return wrapAsFencedCodeBlock(prepared, lang);
|
|
4274
4526
|
}
|
|
@@ -5096,13 +5348,12 @@
|
|
|
5096
5348
|
return;
|
|
5097
5349
|
}
|
|
5098
5350
|
if (kind === "image") {
|
|
5099
|
-
|
|
5100
|
-
void openPreviewImageLink(context.href, context.title, context, pendingWindow).catch((error) => {
|
|
5351
|
+
void openPreviewImageLink(context.href, context.title, context).catch((error) => {
|
|
5101
5352
|
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
5102
5353
|
});
|
|
5103
5354
|
return;
|
|
5104
5355
|
}
|
|
5105
|
-
if (kind === "text") {
|
|
5356
|
+
if (kind === "text" || kind === "office") {
|
|
5106
5357
|
const pendingWindow = window.open("", "_blank");
|
|
5107
5358
|
void openPreviewDocumentInNewEditor(context.href, pendingWindow, context).catch((error) => {
|
|
5108
5359
|
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
@@ -5772,6 +6023,394 @@
|
|
|
5772
6023
|
}
|
|
5773
6024
|
}
|
|
5774
6025
|
|
|
6026
|
+
function isStudioImageFocusOpen() {
|
|
6027
|
+
return Boolean(studioImageFocusOverlayEl && studioImageFocusOverlayEl.hidden === false);
|
|
6028
|
+
}
|
|
6029
|
+
|
|
6030
|
+
function isStudioImageFocusSrcAllowed(src) {
|
|
6031
|
+
const value = String(src || "").trim();
|
|
6032
|
+
if (!value) return false;
|
|
6033
|
+
if (/^javascript:/i.test(value)) return false;
|
|
6034
|
+
return /^(?:data:image\/|blob:|https?:|file:|\/|\.\/|\.\.\/)/i.test(value);
|
|
6035
|
+
}
|
|
6036
|
+
|
|
6037
|
+
function clampStudioImageFocusZoom(value) {
|
|
6038
|
+
const parsed = Number(value);
|
|
6039
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 1;
|
|
6040
|
+
return Math.max(0.1, Math.min(8, parsed));
|
|
6041
|
+
}
|
|
6042
|
+
|
|
6043
|
+
function getStudioImageFocusFitScale() {
|
|
6044
|
+
const img = studioImageFocusImgEl;
|
|
6045
|
+
const slot = studioImageFocusSlotEl;
|
|
6046
|
+
if (!img || !slot) return 1;
|
|
6047
|
+
const naturalWidth = Number(img.naturalWidth) || 0;
|
|
6048
|
+
const naturalHeight = Number(img.naturalHeight) || 0;
|
|
6049
|
+
if (naturalWidth <= 0 || naturalHeight <= 0) return 1;
|
|
6050
|
+
let paddingX = 0;
|
|
6051
|
+
let paddingY = 0;
|
|
6052
|
+
try {
|
|
6053
|
+
const style = window.getComputedStyle(slot);
|
|
6054
|
+
paddingX = (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0);
|
|
6055
|
+
paddingY = (Number.parseFloat(style.paddingTop) || 0) + (Number.parseFloat(style.paddingBottom) || 0);
|
|
6056
|
+
} catch {}
|
|
6057
|
+
const availableWidth = Math.max(1, (slot.clientWidth || 0) - paddingX);
|
|
6058
|
+
const availableHeight = Math.max(1, (slot.clientHeight || 0) - paddingY);
|
|
6059
|
+
return clampStudioImageFocusZoom(Math.min(1, availableWidth / naturalWidth, availableHeight / naturalHeight));
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
function getStudioImageFocusDisplayScale() {
|
|
6063
|
+
return studioImageFocusZoomMode === "fit"
|
|
6064
|
+
? getStudioImageFocusFitScale()
|
|
6065
|
+
: clampStudioImageFocusZoom(studioImageFocusZoom);
|
|
6066
|
+
}
|
|
6067
|
+
|
|
6068
|
+
function syncStudioImageFocusZoom() {
|
|
6069
|
+
if (!studioImageFocusImgEl || !studioImageFocusSlotEl) return;
|
|
6070
|
+
const fitMode = studioImageFocusZoomMode === "fit";
|
|
6071
|
+
studioImageFocusSlotEl.classList.toggle("is-fit", fitMode);
|
|
6072
|
+
studioImageFocusSlotEl.classList.toggle("is-zoomed", !fitMode);
|
|
6073
|
+
if (fitMode) {
|
|
6074
|
+
studioImageFocusImgEl.style.width = "";
|
|
6075
|
+
studioImageFocusImgEl.style.height = "";
|
|
6076
|
+
studioImageFocusImgEl.style.maxWidth = "100%";
|
|
6077
|
+
studioImageFocusImgEl.style.maxHeight = "100%";
|
|
6078
|
+
} else {
|
|
6079
|
+
const zoom = clampStudioImageFocusZoom(studioImageFocusZoom);
|
|
6080
|
+
const naturalWidth = Number(studioImageFocusImgEl.naturalWidth) || 0;
|
|
6081
|
+
studioImageFocusImgEl.style.maxWidth = "none";
|
|
6082
|
+
studioImageFocusImgEl.style.maxHeight = "none";
|
|
6083
|
+
studioImageFocusImgEl.style.height = "auto";
|
|
6084
|
+
studioImageFocusImgEl.style.width = naturalWidth > 0 ? Math.max(1, Math.round(naturalWidth * zoom)) + "px" : Math.round(zoom * 100) + "%";
|
|
6085
|
+
}
|
|
6086
|
+
if (studioImageFocusZoomLabelEl) {
|
|
6087
|
+
studioImageFocusZoomLabelEl.textContent = Math.round(getStudioImageFocusDisplayScale() * 100) + "%";
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
|
|
6091
|
+
function getStudioImageFocusViewportCenter() {
|
|
6092
|
+
const slot = studioImageFocusSlotEl;
|
|
6093
|
+
if (!slot) return { x: 0.5, y: 0.5 };
|
|
6094
|
+
const scrollWidth = Math.max(slot.scrollWidth || 0, slot.clientWidth || 0, 1);
|
|
6095
|
+
const scrollHeight = Math.max(slot.scrollHeight || 0, slot.clientHeight || 0, 1);
|
|
6096
|
+
return {
|
|
6097
|
+
x: Math.max(0, Math.min(1, (slot.scrollLeft + (slot.clientWidth || 0) / 2) / scrollWidth)),
|
|
6098
|
+
y: Math.max(0, Math.min(1, (slot.scrollTop + (slot.clientHeight || 0) / 2) / scrollHeight)),
|
|
6099
|
+
};
|
|
6100
|
+
}
|
|
6101
|
+
|
|
6102
|
+
function restoreStudioImageFocusViewportCenter(center) {
|
|
6103
|
+
const slot = studioImageFocusSlotEl;
|
|
6104
|
+
if (!slot || !center) return;
|
|
6105
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
6106
|
+
? window.requestAnimationFrame.bind(window)
|
|
6107
|
+
: (callback) => window.setTimeout(callback, 0);
|
|
6108
|
+
schedule(() => {
|
|
6109
|
+
if (!slot.isConnected || studioImageFocusZoomMode === "fit") return;
|
|
6110
|
+
const maxLeft = Math.max(0, (slot.scrollWidth || 0) - (slot.clientWidth || 0));
|
|
6111
|
+
const maxTop = Math.max(0, (slot.scrollHeight || 0) - (slot.clientHeight || 0));
|
|
6112
|
+
slot.scrollLeft = Math.max(0, Math.min(maxLeft, (slot.scrollWidth || 0) * center.x - (slot.clientWidth || 0) / 2));
|
|
6113
|
+
slot.scrollTop = Math.max(0, Math.min(maxTop, (slot.scrollHeight || 0) * center.y - (slot.clientHeight || 0) / 2));
|
|
6114
|
+
});
|
|
6115
|
+
}
|
|
6116
|
+
|
|
6117
|
+
function getStudioImageFocusPointerCenter(event) {
|
|
6118
|
+
const slot = studioImageFocusSlotEl;
|
|
6119
|
+
if (!slot || !event || typeof slot.getBoundingClientRect !== "function") return getStudioImageFocusViewportCenter();
|
|
6120
|
+
const rect = slot.getBoundingClientRect();
|
|
6121
|
+
const scrollWidth = Math.max(slot.scrollWidth || 0, slot.clientWidth || 0, 1);
|
|
6122
|
+
const scrollHeight = Math.max(slot.scrollHeight || 0, slot.clientHeight || 0, 1);
|
|
6123
|
+
return {
|
|
6124
|
+
x: Math.max(0, Math.min(1, (slot.scrollLeft + (Number(event.clientX) || rect.left + rect.width / 2) - rect.left) / scrollWidth)),
|
|
6125
|
+
y: Math.max(0, Math.min(1, (slot.scrollTop + (Number(event.clientY) || rect.top + rect.height / 2) - rect.top) / scrollHeight)),
|
|
6126
|
+
};
|
|
6127
|
+
}
|
|
6128
|
+
|
|
6129
|
+
function setStudioImageFocusZoom(mode, zoom, options) {
|
|
6130
|
+
const center = options && options.center ? options.center : getStudioImageFocusViewportCenter();
|
|
6131
|
+
studioImageFocusZoomMode = mode === "fit" ? "fit" : "custom";
|
|
6132
|
+
studioImageFocusZoom = clampStudioImageFocusZoom(zoom);
|
|
6133
|
+
syncStudioImageFocusZoom();
|
|
6134
|
+
if (studioImageFocusZoomMode !== "fit") restoreStudioImageFocusViewportCenter(center);
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
function zoomStudioImageFocus(factor, options) {
|
|
6138
|
+
const base = studioImageFocusZoomMode === "fit" ? getStudioImageFocusFitScale() : studioImageFocusZoom;
|
|
6139
|
+
setStudioImageFocusZoom("custom", clampStudioImageFocusZoom(base * factor), options);
|
|
6140
|
+
}
|
|
6141
|
+
|
|
6142
|
+
function handleStudioImageFocusWheel(event) {
|
|
6143
|
+
if (!isStudioImageFocusOpen() || !event) return;
|
|
6144
|
+
if (!event.altKey && !event.ctrlKey && !event.metaKey) return;
|
|
6145
|
+
event.preventDefault();
|
|
6146
|
+
event.stopPropagation();
|
|
6147
|
+
const delta = Number(event.deltaY) || 0;
|
|
6148
|
+
const factor = delta < 0 ? 1.12 : 1 / 1.12;
|
|
6149
|
+
zoomStudioImageFocus(factor, { center: getStudioImageFocusPointerCenter(event) });
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
function handleStudioImageFocusShortcut(event) {
|
|
6153
|
+
if (!isStudioImageFocusOpen() || !event) return false;
|
|
6154
|
+
if (isTextEntryShortcutTarget(event.target)) return false;
|
|
6155
|
+
const key = typeof event.key === "string" ? event.key : "";
|
|
6156
|
+
const code = typeof event.code === "string" ? event.code : "";
|
|
6157
|
+
if (!event.altKey || event.metaKey || event.ctrlKey) return false;
|
|
6158
|
+
if (code === "Equal" || code === "NumpadAdd" || key === "=" || key === "+") {
|
|
6159
|
+
event.preventDefault();
|
|
6160
|
+
zoomStudioImageFocus(1.25);
|
|
6161
|
+
return true;
|
|
6162
|
+
}
|
|
6163
|
+
if (code === "Minus" || code === "NumpadSubtract" || key === "-" || key === "_") {
|
|
6164
|
+
event.preventDefault();
|
|
6165
|
+
zoomStudioImageFocus(1 / 1.25);
|
|
6166
|
+
return true;
|
|
6167
|
+
}
|
|
6168
|
+
if (code === "Digit0" || code === "Numpad0" || key === "0") {
|
|
6169
|
+
event.preventDefault();
|
|
6170
|
+
setStudioImageFocusZoom("fit", 1);
|
|
6171
|
+
return true;
|
|
6172
|
+
}
|
|
6173
|
+
return false;
|
|
6174
|
+
}
|
|
6175
|
+
|
|
6176
|
+
function syncStudioImageFocusFullscreenButton() {
|
|
6177
|
+
if (!studioImageFocusFullscreenBtn) return;
|
|
6178
|
+
const isFullscreen = Boolean(document.fullscreenElement && studioImageFocusDialogEl && document.fullscreenElement === studioImageFocusDialogEl);
|
|
6179
|
+
studioImageFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
|
|
6180
|
+
const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
|
|
6181
|
+
studioImageFocusFullscreenBtn.title = isFullscreen
|
|
6182
|
+
? "Exit browser fullscreen and keep the image focus viewer open."
|
|
6183
|
+
: "Ask the browser to make this image viewer fullscreen.";
|
|
6184
|
+
studioImageFocusFullscreenBtn.setAttribute("aria-label", label);
|
|
6185
|
+
studioImageFocusFullscreenBtn.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
|
|
6186
|
+
}
|
|
6187
|
+
|
|
6188
|
+
async function toggleStudioImageFocusFullscreen() {
|
|
6189
|
+
const dialog = studioImageFocusDialogEl;
|
|
6190
|
+
if (!dialog) return;
|
|
6191
|
+
const isFullscreen = Boolean(document.fullscreenElement && document.fullscreenElement === dialog);
|
|
6192
|
+
if (isFullscreen) {
|
|
6193
|
+
try {
|
|
6194
|
+
if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
|
|
6195
|
+
} catch (error) {
|
|
6196
|
+
setStatus("Could not exit image fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
6197
|
+
} finally {
|
|
6198
|
+
syncStudioImageFocusFullscreenButton();
|
|
6199
|
+
}
|
|
6200
|
+
return;
|
|
6201
|
+
}
|
|
6202
|
+
if (typeof dialog.requestFullscreen !== "function") {
|
|
6203
|
+
setStatus("Browser fullscreen is not available for this image viewer.", "warning");
|
|
6204
|
+
return;
|
|
6205
|
+
}
|
|
6206
|
+
try {
|
|
6207
|
+
await dialog.requestFullscreen();
|
|
6208
|
+
} catch (error) {
|
|
6209
|
+
setStatus("Could not enter image fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
6210
|
+
} finally {
|
|
6211
|
+
syncStudioImageFocusFullscreenButton();
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
function appendStudioImageFocusTextButton(parent, label, title, onClick) {
|
|
6216
|
+
const button = document.createElement("button");
|
|
6217
|
+
button.type = "button";
|
|
6218
|
+
button.className = "studio-pdf-focus-btn studio-image-focus-zoom-btn";
|
|
6219
|
+
button.textContent = label;
|
|
6220
|
+
button.title = title;
|
|
6221
|
+
button.addEventListener("click", onClick);
|
|
6222
|
+
parent.appendChild(button);
|
|
6223
|
+
return button;
|
|
6224
|
+
}
|
|
6225
|
+
|
|
6226
|
+
function ensureStudioImageFocusViewer() {
|
|
6227
|
+
if (studioImageFocusOverlayEl) return studioImageFocusOverlayEl;
|
|
6228
|
+
|
|
6229
|
+
const overlay = document.createElement("div");
|
|
6230
|
+
overlay.className = "studio-pdf-focus-overlay studio-image-focus-overlay";
|
|
6231
|
+
overlay.hidden = true;
|
|
6232
|
+
overlay.setAttribute("role", "dialog");
|
|
6233
|
+
overlay.setAttribute("aria-modal", "true");
|
|
6234
|
+
overlay.setAttribute("aria-labelledby", "studioImageFocusTitle");
|
|
6235
|
+
|
|
6236
|
+
const dialog = document.createElement("div");
|
|
6237
|
+
dialog.className = "studio-pdf-focus-dialog studio-image-focus-dialog";
|
|
6238
|
+
|
|
6239
|
+
const header = document.createElement("div");
|
|
6240
|
+
header.className = "studio-pdf-focus-header studio-image-focus-header";
|
|
6241
|
+
|
|
6242
|
+
const titleGroup = document.createElement("div");
|
|
6243
|
+
titleGroup.className = "studio-pdf-focus-title-group";
|
|
6244
|
+
|
|
6245
|
+
const closeBtn = document.createElement("button");
|
|
6246
|
+
closeBtn.type = "button";
|
|
6247
|
+
closeBtn.className = "studio-pdf-focus-btn studio-pdf-focus-close";
|
|
6248
|
+
closeBtn.title = "Exit image focus view.";
|
|
6249
|
+
closeBtn.setAttribute("aria-label", "Exit image focus view");
|
|
6250
|
+
closeBtn.appendChild(makeStudioUiRefreshIcon("focus-exit"));
|
|
6251
|
+
closeBtn.addEventListener("click", () => closeStudioImageFocusViewer());
|
|
6252
|
+
titleGroup.appendChild(closeBtn);
|
|
6253
|
+
|
|
6254
|
+
const titleEl = document.createElement("div");
|
|
6255
|
+
titleEl.id = "studioImageFocusTitle";
|
|
6256
|
+
titleEl.className = "studio-pdf-focus-title";
|
|
6257
|
+
titleEl.textContent = "Image preview";
|
|
6258
|
+
titleGroup.appendChild(titleEl);
|
|
6259
|
+
header.appendChild(titleGroup);
|
|
6260
|
+
|
|
6261
|
+
const actions = document.createElement("div");
|
|
6262
|
+
actions.className = "studio-pdf-focus-actions studio-image-focus-actions";
|
|
6263
|
+
|
|
6264
|
+
const openLink = document.createElement("a");
|
|
6265
|
+
openLink.className = "studio-pdf-focus-link";
|
|
6266
|
+
openLink.target = "_blank";
|
|
6267
|
+
openLink.rel = "noopener noreferrer";
|
|
6268
|
+
openLink.textContent = "Open image";
|
|
6269
|
+
actions.appendChild(openLink);
|
|
6270
|
+
|
|
6271
|
+
appendStudioImageFocusTextButton(actions, "Fit", "Fit the image to the viewer.", () => setStudioImageFocusZoom("fit", 1));
|
|
6272
|
+
appendStudioImageFocusTextButton(actions, "100%", "Show the image at its natural pixel size.", () => setStudioImageFocusZoom("custom", 1));
|
|
6273
|
+
appendStudioImageFocusTextButton(actions, "−", "Zoom out.", () => zoomStudioImageFocus(1 / 1.25));
|
|
6274
|
+
const zoomLabel = document.createElement("span");
|
|
6275
|
+
zoomLabel.className = "studio-image-focus-zoom-label";
|
|
6276
|
+
zoomLabel.textContent = "100%";
|
|
6277
|
+
actions.appendChild(zoomLabel);
|
|
6278
|
+
appendStudioImageFocusTextButton(actions, "+", "Zoom in.", () => zoomStudioImageFocus(1.25));
|
|
6279
|
+
appendStudioImageFocusTextButton(actions, "Reset", "Reset image zoom to fit.", () => setStudioImageFocusZoom("fit", 1));
|
|
6280
|
+
|
|
6281
|
+
const fullscreenBtn = document.createElement("button");
|
|
6282
|
+
fullscreenBtn.type = "button";
|
|
6283
|
+
fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
|
|
6284
|
+
fullscreenBtn.addEventListener("click", () => {
|
|
6285
|
+
void toggleStudioImageFocusFullscreen();
|
|
6286
|
+
});
|
|
6287
|
+
actions.appendChild(fullscreenBtn);
|
|
6288
|
+
|
|
6289
|
+
header.appendChild(actions);
|
|
6290
|
+
dialog.appendChild(header);
|
|
6291
|
+
|
|
6292
|
+
const slot = document.createElement("div");
|
|
6293
|
+
slot.className = "studio-image-focus-slot is-fit";
|
|
6294
|
+
const img = document.createElement("img");
|
|
6295
|
+
img.className = "studio-image-focus-img";
|
|
6296
|
+
img.alt = "Image preview";
|
|
6297
|
+
img.addEventListener("load", syncStudioImageFocusZoom);
|
|
6298
|
+
slot.addEventListener("wheel", handleStudioImageFocusWheel, { passive: false });
|
|
6299
|
+
slot.appendChild(img);
|
|
6300
|
+
dialog.appendChild(slot);
|
|
6301
|
+
|
|
6302
|
+
overlay.appendChild(dialog);
|
|
6303
|
+
overlay.addEventListener("click", (event) => {
|
|
6304
|
+
if (event.target === overlay) closeStudioImageFocusViewer();
|
|
6305
|
+
});
|
|
6306
|
+
document.addEventListener("fullscreenchange", syncStudioImageFocusFullscreenButton);
|
|
6307
|
+
|
|
6308
|
+
document.body.appendChild(overlay);
|
|
6309
|
+
studioImageFocusOverlayEl = overlay;
|
|
6310
|
+
studioImageFocusDialogEl = dialog;
|
|
6311
|
+
studioImageFocusSlotEl = slot;
|
|
6312
|
+
studioImageFocusImgEl = img;
|
|
6313
|
+
studioImageFocusTitleEl = titleEl;
|
|
6314
|
+
studioImageFocusOpenLinkEl = openLink;
|
|
6315
|
+
studioImageFocusFullscreenBtn = fullscreenBtn;
|
|
6316
|
+
studioImageFocusCloseBtn = closeBtn;
|
|
6317
|
+
studioImageFocusZoomLabelEl = zoomLabel;
|
|
6318
|
+
syncStudioImageFocusFullscreenButton();
|
|
6319
|
+
return overlay;
|
|
6320
|
+
}
|
|
6321
|
+
|
|
6322
|
+
function openStudioImageFocusViewer(src, title) {
|
|
6323
|
+
const imageSrc = String(src || "").trim();
|
|
6324
|
+
if (!isStudioImageFocusSrcAllowed(imageSrc)) return false;
|
|
6325
|
+
ensureStudioImageFocusViewer();
|
|
6326
|
+
studioImageFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
6327
|
+
const label = String(title || "Image preview").trim() || "Image preview";
|
|
6328
|
+
if (studioImageFocusTitleEl) studioImageFocusTitleEl.textContent = label;
|
|
6329
|
+
if (studioImageFocusOpenLinkEl) studioImageFocusOpenLinkEl.href = imageSrc;
|
|
6330
|
+
if (studioImageFocusImgEl) {
|
|
6331
|
+
studioImageFocusImgEl.alt = label;
|
|
6332
|
+
studioImageFocusImgEl.src = imageSrc;
|
|
6333
|
+
}
|
|
6334
|
+
studioImageFocusZoomMode = "fit";
|
|
6335
|
+
studioImageFocusZoom = 1;
|
|
6336
|
+
syncStudioImageFocusZoom();
|
|
6337
|
+
if (document.body) document.body.classList.add("studio-image-focus-open");
|
|
6338
|
+
if (studioImageFocusOverlayEl) studioImageFocusOverlayEl.hidden = false;
|
|
6339
|
+
syncStudioImageFocusFullscreenButton();
|
|
6340
|
+
closeStudioUiRefreshMenus();
|
|
6341
|
+
closeExportPreviewMenu();
|
|
6342
|
+
closePreviewLinkMenu();
|
|
6343
|
+
window.setTimeout(() => {
|
|
6344
|
+
if (studioImageFocusCloseBtn && typeof studioImageFocusCloseBtn.focus === "function") {
|
|
6345
|
+
studioImageFocusCloseBtn.focus();
|
|
6346
|
+
}
|
|
6347
|
+
}, 0);
|
|
6348
|
+
return true;
|
|
6349
|
+
}
|
|
6350
|
+
|
|
6351
|
+
function closeStudioImageFocusViewer() {
|
|
6352
|
+
if (!isStudioImageFocusOpen()) return false;
|
|
6353
|
+
if (document.fullscreenElement && studioImageFocusDialogEl && studioImageFocusDialogEl.contains(document.fullscreenElement)) {
|
|
6354
|
+
try {
|
|
6355
|
+
const exitResult = document.exitFullscreen && document.exitFullscreen();
|
|
6356
|
+
if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
|
|
6357
|
+
} catch {}
|
|
6358
|
+
}
|
|
6359
|
+
if (studioImageFocusOverlayEl) studioImageFocusOverlayEl.hidden = true;
|
|
6360
|
+
if (studioImageFocusImgEl) studioImageFocusImgEl.removeAttribute("src");
|
|
6361
|
+
if (studioImageFocusOpenLinkEl) studioImageFocusOpenLinkEl.removeAttribute("href");
|
|
6362
|
+
if (document.body) document.body.classList.remove("studio-image-focus-open");
|
|
6363
|
+
syncStudioImageFocusFullscreenButton();
|
|
6364
|
+
const focusTarget = studioImageFocusLastFocusedEl;
|
|
6365
|
+
studioImageFocusLastFocusedEl = null;
|
|
6366
|
+
if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
|
|
6367
|
+
window.setTimeout(() => focusTarget.focus(), 0);
|
|
6368
|
+
}
|
|
6369
|
+
return true;
|
|
6370
|
+
}
|
|
6371
|
+
|
|
6372
|
+
function getPreviewImageElementTitle(imageEl) {
|
|
6373
|
+
if (!imageEl) return "Image preview";
|
|
6374
|
+
const alt = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("alt") || "").trim() : "";
|
|
6375
|
+
const title = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("title") || "").trim() : "";
|
|
6376
|
+
const src = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("src") || "").trim() : "";
|
|
6377
|
+
const srcLabel = /^data:image\//i.test(src) ? "" : (src.length > 120 ? src.slice(0, 117) + "…" : src);
|
|
6378
|
+
return alt || title || srcLabel || "Image preview";
|
|
6379
|
+
}
|
|
6380
|
+
|
|
6381
|
+
function openPreviewImageElementInFocus(imageEl) {
|
|
6382
|
+
if (!imageEl) return false;
|
|
6383
|
+
const src = String(imageEl.currentSrc || imageEl.src || imageEl.getAttribute("src") || "").trim();
|
|
6384
|
+
if (!src) return false;
|
|
6385
|
+
return openStudioImageFocusViewer(src, getPreviewImageElementTitle(imageEl));
|
|
6386
|
+
}
|
|
6387
|
+
|
|
6388
|
+
function decoratePreviewImages(targetEl) {
|
|
6389
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
6390
|
+
const images = Array.from(targetEl.querySelectorAll("img[src]"));
|
|
6391
|
+
images.forEach((imageEl) => {
|
|
6392
|
+
if (!(imageEl instanceof HTMLImageElement)) return;
|
|
6393
|
+
if (imageEl.dataset && imageEl.dataset.studioImageFocusDecorated === "1") return;
|
|
6394
|
+
if (imageEl.closest && imageEl.closest("a[href], button, .studio-html-artifact-shell, .studio-pdf-card")) return;
|
|
6395
|
+
if (!isStudioImageFocusSrcAllowed(imageEl.currentSrc || imageEl.src || imageEl.getAttribute("src") || "")) return;
|
|
6396
|
+
imageEl.classList.add("studio-image-focus-target");
|
|
6397
|
+
imageEl.tabIndex = imageEl.tabIndex >= 0 ? imageEl.tabIndex : 0;
|
|
6398
|
+
imageEl.setAttribute("role", "button");
|
|
6399
|
+
imageEl.setAttribute("aria-label", "Open image focus viewer");
|
|
6400
|
+
if (imageEl.dataset) imageEl.dataset.studioImageFocusDecorated = "1";
|
|
6401
|
+
imageEl.addEventListener("click", (event) => {
|
|
6402
|
+
event.preventDefault();
|
|
6403
|
+
event.stopPropagation();
|
|
6404
|
+
if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
|
|
6405
|
+
});
|
|
6406
|
+
imageEl.addEventListener("keydown", (event) => {
|
|
6407
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
6408
|
+
event.preventDefault();
|
|
6409
|
+
if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
|
|
6410
|
+
});
|
|
6411
|
+
});
|
|
6412
|
+
}
|
|
6413
|
+
|
|
5775
6414
|
function createStudioPdfCard(block, useEditorResourceContext) {
|
|
5776
6415
|
const options = block && block.options ? block.options : {};
|
|
5777
6416
|
const path = String(options.path || "").trim();
|
|
@@ -7003,15 +7642,16 @@
|
|
|
7003
7642
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
7004
7643
|
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
7005
7644
|
const isEditorPreview = rightView === "editor-preview";
|
|
7006
|
-
const
|
|
7645
|
+
const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
|
|
7646
|
+
const editorPdfLanguage = isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "";
|
|
7007
7647
|
const isLatex = isEditorPreview
|
|
7008
7648
|
? editorPdfLanguage === "latex"
|
|
7009
7649
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
7010
|
-
let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-
|
|
7650
|
+
let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.pdf"));
|
|
7011
7651
|
if (sourcePath) {
|
|
7012
7652
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
7013
7653
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
7014
|
-
filenameHint = stem + "
|
|
7654
|
+
filenameHint = stem + ".studio.pdf";
|
|
7015
7655
|
}
|
|
7016
7656
|
|
|
7017
7657
|
previewExportInProgress = true;
|
|
@@ -7059,6 +7699,8 @@
|
|
|
7059
7699
|
|
|
7060
7700
|
const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
|
|
7061
7701
|
const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
|
|
7702
|
+
const writeError = typeof payload.writeError === "string" ? payload.writeError.trim() : "";
|
|
7703
|
+
const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
7062
7704
|
const openedExternal = payload.openedExternal === true;
|
|
7063
7705
|
let downloadName = typeof payload.filename === "string" && payload.filename.trim()
|
|
7064
7706
|
? payload.filename.trim()
|
|
@@ -7068,10 +7710,12 @@
|
|
|
7068
7710
|
}
|
|
7069
7711
|
|
|
7070
7712
|
if (openedExternal) {
|
|
7071
|
-
if (
|
|
7713
|
+
if (writeError) {
|
|
7714
|
+
setStatus("Opened PDF in default viewer, but could not write project file: " + writeError, "warning");
|
|
7715
|
+
} else if (exportWarning) {
|
|
7072
7716
|
setStatus("Opened PDF in default viewer with warning: " + exportWarning, "warning");
|
|
7073
7717
|
} else {
|
|
7074
|
-
setStatus("Opened PDF in default viewer: " + downloadName, "success");
|
|
7718
|
+
setStatus("Opened PDF in default viewer: " + (exportPath || downloadName), "success");
|
|
7075
7719
|
}
|
|
7076
7720
|
return;
|
|
7077
7721
|
}
|
|
@@ -7090,10 +7734,12 @@
|
|
|
7090
7734
|
} else {
|
|
7091
7735
|
setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
|
|
7092
7736
|
}
|
|
7737
|
+
} else if (writeError) {
|
|
7738
|
+
setStatus("Exported PDF to browser fallback; could not write project file: " + writeError, "warning");
|
|
7093
7739
|
} else if (exportWarning) {
|
|
7094
|
-
setStatus("Exported PDF with warning: " + exportWarning, "warning");
|
|
7740
|
+
setStatus("Exported PDF with warning" + (exportPath ? " to " + exportPath : ": " + exportWarning), "warning");
|
|
7095
7741
|
} else {
|
|
7096
|
-
setStatus("Exported PDF: " + downloadName, "success");
|
|
7742
|
+
setStatus("Exported PDF: " + (exportPath || downloadName), "success");
|
|
7097
7743
|
}
|
|
7098
7744
|
return;
|
|
7099
7745
|
}
|
|
@@ -7169,16 +7815,17 @@
|
|
|
7169
7815
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
7170
7816
|
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
7171
7817
|
const isEditorPreview = rightView === "editor-preview";
|
|
7172
|
-
const
|
|
7818
|
+
const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
|
|
7819
|
+
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "");
|
|
7173
7820
|
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
7174
7821
|
? editorHtmlLanguage === "latex"
|
|
7175
7822
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown));
|
|
7176
|
-
let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-
|
|
7823
|
+
let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html"));
|
|
7177
7824
|
let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
|
|
7178
7825
|
if (sourcePath) {
|
|
7179
7826
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
7180
7827
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
7181
|
-
filenameHint = stem + "
|
|
7828
|
+
filenameHint = stem + ".studio.html";
|
|
7182
7829
|
titleHint = stem + " preview";
|
|
7183
7830
|
}
|
|
7184
7831
|
|
|
@@ -7228,6 +7875,8 @@
|
|
|
7228
7875
|
|
|
7229
7876
|
const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
|
|
7230
7877
|
const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
|
|
7878
|
+
const writeError = typeof payload.writeError === "string" ? payload.writeError.trim() : "";
|
|
7879
|
+
const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
7231
7880
|
const openedExternal = payload.openedExternal === true;
|
|
7232
7881
|
let downloadName = typeof payload.filename === "string" && payload.filename.trim()
|
|
7233
7882
|
? payload.filename.trim()
|
|
@@ -7237,10 +7886,12 @@
|
|
|
7237
7886
|
}
|
|
7238
7887
|
|
|
7239
7888
|
if (openedExternal) {
|
|
7240
|
-
if (
|
|
7889
|
+
if (writeError) {
|
|
7890
|
+
setStatus("Opened HTML in default browser, but could not write project file: " + writeError, "warning");
|
|
7891
|
+
} else if (exportWarning) {
|
|
7241
7892
|
setStatus("Opened HTML in default browser with warning: " + exportWarning, "warning");
|
|
7242
7893
|
} else {
|
|
7243
|
-
setStatus("Opened HTML in default browser: " + downloadName, "success");
|
|
7894
|
+
setStatus("Opened HTML in default browser: " + (exportPath || downloadName), "success");
|
|
7244
7895
|
}
|
|
7245
7896
|
return;
|
|
7246
7897
|
}
|
|
@@ -7259,10 +7910,12 @@
|
|
|
7259
7910
|
} else {
|
|
7260
7911
|
setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
|
|
7261
7912
|
}
|
|
7913
|
+
} else if (writeError) {
|
|
7914
|
+
setStatus("Exported HTML to browser fallback; could not write project file: " + writeError, "warning");
|
|
7262
7915
|
} else if (exportWarning) {
|
|
7263
|
-
setStatus("Exported HTML with warning: " + exportWarning, "warning");
|
|
7916
|
+
setStatus("Exported HTML with warning" + (exportPath ? " to " + exportPath : ": " + exportWarning), "warning");
|
|
7264
7917
|
} else {
|
|
7265
|
-
setStatus("Exported HTML: " + downloadName, "success");
|
|
7918
|
+
setStatus("Exported HTML: " + (exportPath || downloadName), "success");
|
|
7266
7919
|
}
|
|
7267
7920
|
return;
|
|
7268
7921
|
}
|
|
@@ -7554,6 +8207,7 @@
|
|
|
7554
8207
|
decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
|
|
7555
8208
|
}
|
|
7556
8209
|
decorateCopyablePreviewBlocks(targetEl);
|
|
8210
|
+
decoratePreviewImages(targetEl);
|
|
7557
8211
|
|
|
7558
8212
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
7559
8213
|
if (!sourceState.path && !getCurrentResourceDirValue()) {
|
|
@@ -7589,12 +8243,16 @@
|
|
|
7589
8243
|
function renderSourcePreviewNow() {
|
|
7590
8244
|
if (editorView !== "preview") return;
|
|
7591
8245
|
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
7592
|
-
|
|
8246
|
+
const previewLanguage = getEditorLanguageForPreview();
|
|
8247
|
+
if (isHtmlArtifactPreviewText(text, previewLanguage)) {
|
|
7593
8248
|
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
7594
8249
|
return;
|
|
7595
8250
|
}
|
|
7596
|
-
if (
|
|
7597
|
-
|
|
8251
|
+
if (renderDelimitedTextPreview(sourcePreviewEl, text, "source", previewLanguage)) {
|
|
8252
|
+
return;
|
|
8253
|
+
}
|
|
8254
|
+
if (supportsCodePreviewCommentsForLanguage(previewLanguage)) {
|
|
8255
|
+
renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source", previewLanguage);
|
|
7598
8256
|
return;
|
|
7599
8257
|
}
|
|
7600
8258
|
const nonce = ++sourcePreviewRenderNonce;
|
|
@@ -7718,9 +8376,12 @@
|
|
|
7718
8376
|
return { text: preview, truncated: true, hiddenChars, hiddenLines };
|
|
7719
8377
|
}
|
|
7720
8378
|
|
|
7721
|
-
function renderTraceOutput(text, outputKey) {
|
|
8379
|
+
function renderTraceOutput(text, outputKey, options) {
|
|
7722
8380
|
const value = String(text || "");
|
|
7723
8381
|
const key = String(outputKey || "trace-output");
|
|
8382
|
+
const label = options && typeof options.label === "string" && options.label.trim()
|
|
8383
|
+
? options.label.trim()
|
|
8384
|
+
: "Output";
|
|
7724
8385
|
const isExpanded = traceExpandedOutputs.has(key);
|
|
7725
8386
|
const preview = getTraceOutputPreview(value);
|
|
7726
8387
|
const visibleText = isExpanded || !preview.truncated ? value : preview.text;
|
|
@@ -7730,10 +8391,11 @@
|
|
|
7730
8391
|
const hiddenParts = [];
|
|
7731
8392
|
if (preview.hiddenLines > 0) hiddenParts.push(preview.hiddenLines + " more line" + (preview.hiddenLines === 1 ? "" : "s"));
|
|
7732
8393
|
if (preview.hiddenChars > 0) hiddenParts.push(formatCompactNumber(preview.hiddenChars) + " chars hidden");
|
|
8394
|
+
const labelLower = label.toLowerCase();
|
|
7733
8395
|
const summary = isExpanded
|
|
7734
|
-
? "Showing full
|
|
7735
|
-
: "
|
|
7736
|
-
const buttonLabel = isExpanded ? "Collapse" : "Show full";
|
|
8396
|
+
? "Showing full " + labelLower + " (" + formatTraceOutputSize(value) + ")."
|
|
8397
|
+
: label + " truncated — " + (hiddenParts.join(", ") || "more hidden") + ".";
|
|
8398
|
+
const buttonLabel = isExpanded ? "Collapse " + labelLower : "Show full " + labelLower;
|
|
7737
8399
|
return "<div class='trace-output-wrap" + (isExpanded ? " is-expanded" : " is-truncated") + "'>"
|
|
7738
8400
|
+ body
|
|
7739
8401
|
+ "<div class='trace-output-truncation'>"
|
|
@@ -8107,15 +8769,16 @@
|
|
|
8107
8769
|
}
|
|
8108
8770
|
|
|
8109
8771
|
const title = entry.label || entry.toolName || "tool";
|
|
8110
|
-
const
|
|
8111
|
-
|
|
8772
|
+
const inputText = entry.args || entry.argsSummary || "";
|
|
8773
|
+
const argsSummary = inputText
|
|
8774
|
+
? "<div class='trace-section trace-section-input'><div class='trace-section-label'>Input</div>" + renderTraceOutput(inputText, entry.id + ":input", { label: "Input" }) + "</div>"
|
|
8112
8775
|
: "";
|
|
8113
8776
|
const imageOutput = renderTraceImages(entry.images);
|
|
8114
8777
|
const outputPieces = [];
|
|
8115
|
-
if (entry.output) outputPieces.push(renderTraceOutput(entry.output, entry.id + ":output"));
|
|
8778
|
+
if (entry.output) outputPieces.push(renderTraceOutput(entry.output, entry.id + ":output", { label: "Output" }));
|
|
8116
8779
|
if (imageOutput) outputPieces.push(imageOutput);
|
|
8117
8780
|
const output = outputPieces.length
|
|
8118
|
-
? "<div class='trace-section'><div class='trace-section-label'>Output</div>" + outputPieces.join("") + "</div>"
|
|
8781
|
+
? "<div class='trace-section trace-section-output'><div class='trace-section-label'>Output</div>" + outputPieces.join("") + "</div>"
|
|
8119
8782
|
: "<div class='trace-empty-inline'>No output yet.</div>";
|
|
8120
8783
|
const toolStatusLabel = entry.isError
|
|
8121
8784
|
? "Error"
|
|
@@ -8141,6 +8804,7 @@
|
|
|
8141
8804
|
const previousScrollTop = critiqueViewEl.scrollTop;
|
|
8142
8805
|
finishPreviewRender(critiqueViewEl);
|
|
8143
8806
|
critiqueViewEl.innerHTML = buildTracePanelHtml();
|
|
8807
|
+
decoratePreviewImages(critiqueViewEl);
|
|
8144
8808
|
critiqueViewEl.classList.remove("response-scroll-resetting");
|
|
8145
8809
|
if (shouldStick) {
|
|
8146
8810
|
critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
|
|
@@ -8197,6 +8861,7 @@
|
|
|
8197
8861
|
function getFileBrowserKindLabel(entry) {
|
|
8198
8862
|
if (!entry || entry.type === "directory") return "folder";
|
|
8199
8863
|
if (entry.kind === "text") return "document";
|
|
8864
|
+
if (entry.kind === "office") return "document";
|
|
8200
8865
|
if (entry.kind === "pdf") return "PDF";
|
|
8201
8866
|
if (entry.kind === "image") return "image";
|
|
8202
8867
|
return entry.extension ? entry.extension.replace(/^\./, "") : "file";
|
|
@@ -8213,18 +8878,24 @@
|
|
|
8213
8878
|
? entries.map((entry) => {
|
|
8214
8879
|
const type = entry.type === "directory" ? "directory" : "file";
|
|
8215
8880
|
const kind = entry.kind || (type === "directory" ? "directory" : "other");
|
|
8216
|
-
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" ? "📝" : "📦")));
|
|
8881
|
+
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
|
|
8217
8882
|
const metaParts = [];
|
|
8218
8883
|
metaParts.push(getFileBrowserKindLabel(entry));
|
|
8219
8884
|
if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
|
|
8220
8885
|
const time = formatFileBrowserTime(entry.mtimeMs);
|
|
8221
8886
|
if (time) metaParts.push(time);
|
|
8222
|
-
const
|
|
8223
|
-
? "
|
|
8887
|
+
const newTabAction = kind === "text" || kind === "office"
|
|
8888
|
+
? "open-new"
|
|
8889
|
+
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
8890
|
+
const newTabLabel = kind === "text"
|
|
8891
|
+
? "Open file tab"
|
|
8892
|
+
: (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
|
|
8893
|
+
const textActions = newTabAction
|
|
8894
|
+
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
8224
8895
|
: "";
|
|
8225
8896
|
const openTitle = type === "directory"
|
|
8226
8897
|
? "Open folder"
|
|
8227
|
-
: (kind === "text" ? "Open in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file")));
|
|
8898
|
+
: (kind === "text" ? "Open in editor" : (kind === "office" ? "Convert to Markdown in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
8228
8899
|
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
8229
8900
|
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(entry.path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
8230
8901
|
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
@@ -8249,6 +8920,8 @@
|
|
|
8249
8920
|
+ "<div class='files-toolbar-actions'>"
|
|
8250
8921
|
+ "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
|
|
8251
8922
|
+ "<button type='button' data-files-action='refresh'>Refresh</button>"
|
|
8923
|
+
+ (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
|
|
8924
|
+
+ (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
|
|
8252
8925
|
+ (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
|
|
8253
8926
|
+ "</div>"
|
|
8254
8927
|
+ "</div>"
|
|
@@ -8341,10 +9014,38 @@
|
|
|
8341
9014
|
}
|
|
8342
9015
|
}
|
|
8343
9016
|
|
|
9017
|
+
function basenameForStudioPath(path) {
|
|
9018
|
+
const value = stripPreviewLocalLinkUrlSuffix(path || "").replace(/\\/g, "/");
|
|
9019
|
+
const parts = value.split("/");
|
|
9020
|
+
return parts.pop() || value || "file";
|
|
9021
|
+
}
|
|
9022
|
+
|
|
9023
|
+
function ensureCurrentEditorFileBackedFromFilesPath(path) {
|
|
9024
|
+
const cleanPath = stripPreviewLocalLinkUrlSuffix(path || "").trim();
|
|
9025
|
+
if (!isLikelyAbsoluteStudioPath(cleanPath)) return;
|
|
9026
|
+
if (sourceState && sourceState.path === cleanPath) return;
|
|
9027
|
+
const resourceDir = normalizeStudioResourceDirValue(fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(cleanPath));
|
|
9028
|
+
if (resourceDirInput && resourceDir) resourceDirInput.value = resourceDir;
|
|
9029
|
+
setSourceState({
|
|
9030
|
+
source: "file",
|
|
9031
|
+
label: sourceState && sourceState.label && sourceState.label !== "blank" ? sourceState.label : basenameForStudioPath(cleanPath),
|
|
9032
|
+
path: cleanPath,
|
|
9033
|
+
});
|
|
9034
|
+
markFileBackedBaseline(sourceTextEl.value);
|
|
9035
|
+
}
|
|
9036
|
+
|
|
8344
9037
|
async function openFileBrowserEntry(path, kind) {
|
|
8345
9038
|
const context = getFileBrowserLocalLinkContext();
|
|
8346
9039
|
if (kind === "text") {
|
|
8347
|
-
await openPreviewDocumentHere(path, context);
|
|
9040
|
+
await openPreviewDocumentHere(path, context, { fallbackPath: path, fileBackedIntent: true });
|
|
9041
|
+
ensureCurrentEditorFileBackedFromFilesPath(path);
|
|
9042
|
+
if (sourceState && sourceState.path) {
|
|
9043
|
+
setStatus("Opened file-backed document in editor: " + (sourceState.label || sourceState.path), "success");
|
|
9044
|
+
}
|
|
9045
|
+
return;
|
|
9046
|
+
}
|
|
9047
|
+
if (kind === "office") {
|
|
9048
|
+
await openPreviewDocumentHere(path, context, { fallbackPath: path });
|
|
8348
9049
|
return;
|
|
8349
9050
|
}
|
|
8350
9051
|
if (kind === "pdf") {
|
|
@@ -8358,6 +9059,19 @@
|
|
|
8358
9059
|
setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
|
|
8359
9060
|
}
|
|
8360
9061
|
|
|
9062
|
+
function setFileBrowserCurrentDirectoryAsWorkingDir(path) {
|
|
9063
|
+
const nextDir = normalizeStudioResourceDirValue(path || fileBrowserState.currentDir || "");
|
|
9064
|
+
if (!nextDir) {
|
|
9065
|
+
setStatus("No current folder to use as working directory.", "warning");
|
|
9066
|
+
return;
|
|
9067
|
+
}
|
|
9068
|
+
if (resourceDirInput) resourceDirInput.value = nextDir;
|
|
9069
|
+
applyResourceDir();
|
|
9070
|
+
fileBrowserState = { ...fileBrowserState, contextKey: "" };
|
|
9071
|
+
if (rightView === "files") renderFilesView();
|
|
9072
|
+
setStatus("Working dir set to current folder.", "success");
|
|
9073
|
+
}
|
|
9074
|
+
|
|
8361
9075
|
async function handleFilesPaneClick(event) {
|
|
8362
9076
|
if (rightView !== "files") return;
|
|
8363
9077
|
const target = event.target;
|
|
@@ -8388,11 +9102,19 @@
|
|
|
8388
9102
|
await openPreviewDocumentInNewEditor(path, null, getFileBrowserLocalLinkContext());
|
|
8389
9103
|
return;
|
|
8390
9104
|
}
|
|
8391
|
-
if (action === "
|
|
9105
|
+
if (action === "open-preview-new") {
|
|
9106
|
+
await openPreviewResourceInNewEditor(path, null, getFileBrowserLocalLinkContext());
|
|
9107
|
+
return;
|
|
9108
|
+
}
|
|
9109
|
+
if (action === "copy-path" || action === "copy-root" || action === "copy-current") {
|
|
8392
9110
|
const ok = await writeTextToClipboard(path);
|
|
8393
9111
|
setStatus(ok ? "Copied path." : "Clipboard write failed.", ok ? "success" : "warning");
|
|
8394
9112
|
return;
|
|
8395
9113
|
}
|
|
9114
|
+
if (action === "use-working-dir") {
|
|
9115
|
+
setFileBrowserCurrentDirectoryAsWorkingDir(path);
|
|
9116
|
+
return;
|
|
9117
|
+
}
|
|
8396
9118
|
if (action === "reveal") {
|
|
8397
9119
|
await revealPreviewLocalLink(path, getFileBrowserLocalLinkContext());
|
|
8398
9120
|
}
|
|
@@ -8425,12 +9147,16 @@
|
|
|
8425
9147
|
scheduleResponsePaneRepaintNudge();
|
|
8426
9148
|
return;
|
|
8427
9149
|
}
|
|
8428
|
-
|
|
9150
|
+
const previewLanguage = getEditorLanguageForPreview();
|
|
9151
|
+
if (isHtmlArtifactPreviewText(editorText, previewLanguage)) {
|
|
8429
9152
|
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
8430
9153
|
return;
|
|
8431
9154
|
}
|
|
8432
|
-
if (
|
|
8433
|
-
|
|
9155
|
+
if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response", previewLanguage)) {
|
|
9156
|
+
return;
|
|
9157
|
+
}
|
|
9158
|
+
if (supportsCodePreviewCommentsForLanguage(previewLanguage)) {
|
|
9159
|
+
renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response", previewLanguage);
|
|
8434
9160
|
return;
|
|
8435
9161
|
}
|
|
8436
9162
|
const nonce = ++responsePreviewRenderNonce;
|
|
@@ -8615,13 +9341,17 @@
|
|
|
8615
9341
|
return resourceDirInput ? normalizeStudioResourceDirValue(resourceDirInput.value) : "";
|
|
8616
9342
|
}
|
|
8617
9343
|
|
|
9344
|
+
function stripImportedFileLabel(label) {
|
|
9345
|
+
return String(label || "").replace(/^(?:upload|imported copy):\s*/i, "");
|
|
9346
|
+
}
|
|
9347
|
+
|
|
8618
9348
|
function getEffectiveSavePath() {
|
|
8619
9349
|
// File-backed: use the original path
|
|
8620
9350
|
if (sourceState.path) return sourceState.path;
|
|
8621
|
-
//
|
|
9351
|
+
// Browser-imported copy with working dir + filename: derive path
|
|
8622
9352
|
const resourceDir = getCurrentResourceDirValue();
|
|
8623
9353
|
if (sourceState.source === "upload" && sourceState.label && resourceDir) {
|
|
8624
|
-
var name = sourceState.label
|
|
9354
|
+
var name = stripImportedFileLabel(sourceState.label);
|
|
8625
9355
|
if (name) return resourceDir.replace(/\/$/, "") + "/" + name;
|
|
8626
9356
|
}
|
|
8627
9357
|
return null;
|
|
@@ -8646,7 +9376,7 @@
|
|
|
8646
9376
|
return dir + stem + ".annotated.md";
|
|
8647
9377
|
}
|
|
8648
9378
|
|
|
8649
|
-
const rawLabel = sourceState.label ? sourceState.label
|
|
9379
|
+
const rawLabel = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
|
|
8650
9380
|
const stem = rawLabel.replace(/\.[^.]+$/, "") || "draft";
|
|
8651
9381
|
const suggestedDir = getCurrentResourceDirValue()
|
|
8652
9382
|
? getCurrentResourceDirValue().replace(/\/$/, "") + "/"
|
|
@@ -8674,7 +9404,7 @@
|
|
|
8674
9404
|
return;
|
|
8675
9405
|
}
|
|
8676
9406
|
|
|
8677
|
-
refreshFromDiskBtn.title = "Refresh from disk is
|
|
9407
|
+
refreshFromDiskBtn.title = "Refresh from disk is available after opening a file from disk. Use Files → Open here, Files → Open file tab, or /studio-editor-only <path> for a refreshable editor tab.";
|
|
8678
9408
|
}
|
|
8679
9409
|
|
|
8680
9410
|
function syncActionButtons() {
|
|
@@ -8888,7 +9618,10 @@
|
|
|
8888
9618
|
resourceDirInput.value = nextResourceDir;
|
|
8889
9619
|
updateSourceBadge();
|
|
8890
9620
|
}
|
|
8891
|
-
|
|
9621
|
+
const detectedPersistedPathLanguage = detectLanguageFromName(nextSourceState.path || nextSourceState.label || "");
|
|
9622
|
+
if (getDelimitedTextPreviewConfig(detectedPersistedPathLanguage)) {
|
|
9623
|
+
setEditorLanguage(detectedPersistedPathLanguage);
|
|
9624
|
+
} else if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
|
|
8892
9625
|
setEditorLanguage(state.editorLanguage.trim());
|
|
8893
9626
|
}
|
|
8894
9627
|
editorView = state.editorView === "preview" ? "preview" : "markdown";
|
|
@@ -9791,6 +10524,7 @@
|
|
|
9791
10524
|
".diff", ".patch",
|
|
9792
10525
|
]);
|
|
9793
10526
|
const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
10527
|
+
const PREVIEW_LOCAL_OFFICE_LINK_EXTENSIONS = new Set([".docx", ".odt"]);
|
|
9794
10528
|
const PREVIEW_LOCAL_TEXT_LINK_FILENAMES = new Set([
|
|
9795
10529
|
".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
|
|
9796
10530
|
".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
|
|
@@ -9855,6 +10589,7 @@
|
|
|
9855
10589
|
if (ext === ".pdf") return "pdf";
|
|
9856
10590
|
if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext) || PREVIEW_LOCAL_TEXT_LINK_FILENAMES.has(name)) return "text";
|
|
9857
10591
|
if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
|
|
10592
|
+
if (PREVIEW_LOCAL_OFFICE_LINK_EXTENSIONS.has(ext)) return "office";
|
|
9858
10593
|
return "other";
|
|
9859
10594
|
}
|
|
9860
10595
|
|
|
@@ -9948,11 +10683,16 @@
|
|
|
9948
10683
|
};
|
|
9949
10684
|
if (kind === "pdf") {
|
|
9950
10685
|
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
10686
|
+
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
9951
10687
|
} else if (kind === "text") {
|
|
9952
10688
|
appendPreviewLinkMenuButton(menu, "Open in new editor", "open-new");
|
|
9953
10689
|
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
10690
|
+
} else if (kind === "office") {
|
|
10691
|
+
appendPreviewLinkMenuButton(menu, "Convert in new editor", "open-new");
|
|
10692
|
+
appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
9954
10693
|
} else if (kind === "image") {
|
|
9955
10694
|
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
10695
|
+
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
9956
10696
|
}
|
|
9957
10697
|
appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
|
|
9958
10698
|
appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
|
|
@@ -9989,40 +10729,18 @@
|
|
|
9989
10729
|
}
|
|
9990
10730
|
|
|
9991
10731
|
async function openPreviewImageLink(href, title, contextOverride, pendingWindow) {
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
if (popup && popup.document && popup.document.body) {
|
|
9995
|
-
popup.document.title = "Opening image…";
|
|
9996
|
-
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening image…</p>";
|
|
9997
|
-
}
|
|
9998
|
-
} catch {}
|
|
9999
|
-
try {
|
|
10000
|
-
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
10001
|
-
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
10002
|
-
});
|
|
10003
|
-
const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
|
|
10004
|
-
if (!dataUrl) throw new Error("Studio did not return image data.");
|
|
10005
|
-
const safeTitle = escapeHtml(String(title || href || "Local image"));
|
|
10006
|
-
const safeSrc = escapeHtml(dataUrl);
|
|
10007
|
-
const html = "<!doctype html><html><head><meta charset='utf-8'><title>" + safeTitle + "</title>"
|
|
10008
|
-
+ "<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee;font:13px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}img{max-width:100vw;max-height:100vh;object-fit:contain;}header{position:fixed;left:0;right:0;top:0;padding:8px 10px;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);}</style>"
|
|
10009
|
-
+ "</head><body><header>" + safeTitle + "</header><img src='" + safeSrc + "' alt='" + safeTitle + "'></body></html>";
|
|
10010
|
-
if (popup && !popup.closed && popup.document) {
|
|
10011
|
-
popup.document.open();
|
|
10012
|
-
popup.document.write(html);
|
|
10013
|
-
popup.document.close();
|
|
10014
|
-
setStatus("Opened local image preview.", "success");
|
|
10015
|
-
return;
|
|
10016
|
-
}
|
|
10017
|
-
const opened = window.open(dataUrl, "_blank");
|
|
10018
|
-
if (!opened) throw new Error("Popup blocked while opening image preview.");
|
|
10019
|
-
setStatus("Opened local image preview.", "success");
|
|
10020
|
-
} catch (error) {
|
|
10021
|
-
if (popup && !popup.closed) {
|
|
10022
|
-
try { popup.close(); } catch {}
|
|
10023
|
-
}
|
|
10024
|
-
throw error;
|
|
10732
|
+
if (pendingWindow && !pendingWindow.closed) {
|
|
10733
|
+
try { pendingWindow.close(); } catch {}
|
|
10025
10734
|
}
|
|
10735
|
+
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
10736
|
+
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
10737
|
+
});
|
|
10738
|
+
const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
|
|
10739
|
+
if (!dataUrl) throw new Error("Studio did not return image data.");
|
|
10740
|
+
if (!openStudioImageFocusViewer(dataUrl, title || href || "Local image")) {
|
|
10741
|
+
throw new Error("Could not open image focus view.");
|
|
10742
|
+
}
|
|
10743
|
+
setStatus("Opened local image preview.", "success");
|
|
10026
10744
|
}
|
|
10027
10745
|
|
|
10028
10746
|
function editorHasPotentialUnsavedContent() {
|
|
@@ -10032,28 +10750,80 @@
|
|
|
10032
10750
|
return true;
|
|
10033
10751
|
}
|
|
10034
10752
|
|
|
10035
|
-
|
|
10753
|
+
function getPreviewOfficeConversionLabel(href) {
|
|
10754
|
+
const cleanPath = stripPreviewLocalLinkUrlSuffix(href || "");
|
|
10755
|
+
const rawName = cleanPath.split(/[\\/]/).pop() || cleanPath || "this document";
|
|
10756
|
+
try {
|
|
10757
|
+
return decodeURIComponent(rawName) || rawName;
|
|
10758
|
+
} catch {
|
|
10759
|
+
return rawName;
|
|
10760
|
+
}
|
|
10761
|
+
}
|
|
10762
|
+
|
|
10763
|
+
function confirmPreviewOfficeConversion(href, destination) {
|
|
10764
|
+
if (getPreviewLocalLinkKind(href) !== "office") return true;
|
|
10765
|
+
const label = getPreviewOfficeConversionLabel(href);
|
|
10766
|
+
const target = destination === "here"
|
|
10767
|
+
? "replace the current editor contents with an editable Markdown copy"
|
|
10768
|
+
: "open an editable Markdown copy in a new Studio tab";
|
|
10769
|
+
const confirmed = window.confirm(
|
|
10770
|
+
"Convert " + label + " to Markdown?\n\n"
|
|
10771
|
+
+ "Studio will use Pandoc to " + target + ". Some layout or formatting may change. "
|
|
10772
|
+
+ "The original DOCX/ODT file will not be overwritten, and edits will not round-trip back to it."
|
|
10773
|
+
);
|
|
10774
|
+
if (!confirmed) setStatus("Document conversion cancelled.", "warning");
|
|
10775
|
+
return confirmed;
|
|
10776
|
+
}
|
|
10777
|
+
|
|
10778
|
+
function isLikelyAbsoluteStudioPath(path) {
|
|
10779
|
+
const value = stripPreviewLocalLinkUrlSuffix(path || "").trim();
|
|
10780
|
+
return Boolean(value && (/^\//.test(value) || /^[A-Za-z]:[\\/]/.test(value)));
|
|
10781
|
+
}
|
|
10782
|
+
|
|
10783
|
+
async function openPreviewDocumentHere(href, contextOverride, options) {
|
|
10784
|
+
if (!confirmPreviewOfficeConversion(href, "here")) return;
|
|
10036
10785
|
if (editorHasPotentialUnsavedContent()) {
|
|
10037
|
-
const
|
|
10786
|
+
const kind = getPreviewLocalLinkKind(href);
|
|
10787
|
+
const prompt = kind === "office"
|
|
10788
|
+
? "Replace the current editor contents with this converted Markdown copy? Unsaved editor changes may be lost."
|
|
10789
|
+
: "Open this file-backed document in the current editor?\n\nThis will replace the current editor contents and attach the editor to the file on disk, so Save editor and Refresh from disk use that file. Unsaved editor changes may be lost.";
|
|
10790
|
+
const confirmed = window.confirm(prompt);
|
|
10038
10791
|
if (!confirmed) return;
|
|
10039
10792
|
}
|
|
10040
10793
|
const payload = await fetchPreviewLocalLink("document", href, contextOverride);
|
|
10041
10794
|
if (typeof payload.text !== "string") throw new Error("Studio did not return document text.");
|
|
10042
|
-
const
|
|
10795
|
+
const responsePath = typeof payload.path === "string" ? payload.path : "";
|
|
10796
|
+
const fallbackPath = options && typeof options.fallbackPath === "string" && isLikelyAbsoluteStudioPath(options.fallbackPath)
|
|
10797
|
+
? stripPreviewLocalLinkUrlSuffix(options.fallbackPath).trim()
|
|
10798
|
+
: "";
|
|
10799
|
+
const path = responsePath || fallbackPath;
|
|
10043
10800
|
const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
|
|
10044
10801
|
const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
|
|
10802
|
+
const converted = payload && payload.converted === true;
|
|
10045
10803
|
if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
|
|
10046
10804
|
setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10805
|
+
if (converted || !path) {
|
|
10806
|
+
setSourceState({ source: "blank", label, path: null });
|
|
10807
|
+
} else {
|
|
10808
|
+
setSourceState({ source: "file", label, path });
|
|
10809
|
+
markFileBackedBaseline(payload.text);
|
|
10810
|
+
}
|
|
10811
|
+
const detected = converted ? "markdown" : detectLanguageFromName(path || label);
|
|
10050
10812
|
if (detected) setEditorLanguage(detected);
|
|
10051
10813
|
setEditorView("markdown");
|
|
10052
10814
|
setActivePane("left");
|
|
10053
|
-
setStatus(
|
|
10815
|
+
setStatus(converted
|
|
10816
|
+
? ("Converted document into editor: " + label)
|
|
10817
|
+
: (path ? ("Opened file-backed document in editor: " + label) : ("Opened linked file copy in editor: " + label)), "success");
|
|
10054
10818
|
}
|
|
10055
10819
|
|
|
10056
10820
|
async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
|
|
10821
|
+
if (!confirmPreviewOfficeConversion(href, "new")) {
|
|
10822
|
+
if (pendingWindow && !pendingWindow.closed) {
|
|
10823
|
+
try { pendingWindow.close(); } catch {}
|
|
10824
|
+
}
|
|
10825
|
+
return;
|
|
10826
|
+
}
|
|
10057
10827
|
const popup = pendingWindow || window.open("", "_blank");
|
|
10058
10828
|
try {
|
|
10059
10829
|
if (popup && popup.document && popup.document.body) {
|
|
@@ -10071,12 +10841,44 @@
|
|
|
10071
10841
|
try {
|
|
10072
10842
|
popup.opener = null;
|
|
10073
10843
|
popup.location.href = targetUrl;
|
|
10074
|
-
setStatus("Opening linked file in a new editor.", "success");
|
|
10844
|
+
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
|
|
10845
|
+
return;
|
|
10846
|
+
} catch {}
|
|
10847
|
+
}
|
|
10848
|
+
window.open(targetUrl, "_blank", "noopener");
|
|
10849
|
+
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
|
|
10850
|
+
} catch (error) {
|
|
10851
|
+
if (popup && !popup.closed) {
|
|
10852
|
+
try { popup.close(); } catch {}
|
|
10853
|
+
}
|
|
10854
|
+
throw error;
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
|
|
10858
|
+
async function openPreviewResourceInNewEditor(href, pendingWindow, contextOverride) {
|
|
10859
|
+
const popup = pendingWindow || window.open("", "_blank");
|
|
10860
|
+
try {
|
|
10861
|
+
if (popup && popup.document && popup.document.body) {
|
|
10862
|
+
popup.document.title = "Opening preview…";
|
|
10863
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening preview…</p>";
|
|
10864
|
+
}
|
|
10865
|
+
} catch {}
|
|
10866
|
+
try {
|
|
10867
|
+
const payload = await fetchPreviewLocalLink("preview-url", href, contextOverride);
|
|
10868
|
+
const targetUrl = payload && typeof payload.relativeUrl === "string"
|
|
10869
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
10870
|
+
: (payload && typeof payload.url === "string" ? payload.url : "");
|
|
10871
|
+
if (!targetUrl) throw new Error("Studio did not return a preview URL.");
|
|
10872
|
+
if (popup && !popup.closed) {
|
|
10873
|
+
try {
|
|
10874
|
+
popup.opener = null;
|
|
10875
|
+
popup.location.href = targetUrl;
|
|
10876
|
+
setStatus("Opening preview in a new Studio tab.", "success");
|
|
10075
10877
|
return;
|
|
10076
10878
|
} catch {}
|
|
10077
10879
|
}
|
|
10078
10880
|
window.open(targetUrl, "_blank", "noopener");
|
|
10079
|
-
setStatus("Opening
|
|
10881
|
+
setStatus("Opening preview in a new Studio tab.", "success");
|
|
10080
10882
|
} catch (error) {
|
|
10081
10883
|
if (popup && !popup.closed) {
|
|
10082
10884
|
try { popup.close(); } catch {}
|
|
@@ -10115,6 +10917,10 @@
|
|
|
10115
10917
|
await openPreviewDocumentInNewEditor(href, null, context);
|
|
10116
10918
|
return;
|
|
10117
10919
|
}
|
|
10920
|
+
if (action === "open-preview-new") {
|
|
10921
|
+
await openPreviewResourceInNewEditor(href, null, context);
|
|
10922
|
+
return;
|
|
10923
|
+
}
|
|
10118
10924
|
if (action === "open-here") {
|
|
10119
10925
|
await openPreviewDocumentHere(href, context);
|
|
10120
10926
|
return;
|
|
@@ -10149,14 +10955,13 @@
|
|
|
10149
10955
|
return;
|
|
10150
10956
|
}
|
|
10151
10957
|
if (kind === "image") {
|
|
10152
|
-
|
|
10153
|
-
void openPreviewImageLink(href, title, null, pendingWindow).catch((error) => {
|
|
10958
|
+
void openPreviewImageLink(href, title).catch((error) => {
|
|
10154
10959
|
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
10155
10960
|
});
|
|
10156
10961
|
return;
|
|
10157
10962
|
}
|
|
10158
|
-
if (kind === "text") {
|
|
10159
|
-
const pendingWindow = window.open("", "_blank");
|
|
10963
|
+
if (kind === "text" || kind === "office") {
|
|
10964
|
+
const pendingWindow = kind === "office" ? null : window.open("", "_blank");
|
|
10160
10965
|
void openPreviewDocumentInNewEditor(href, pendingWindow).catch((error) => {
|
|
10161
10966
|
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
10162
10967
|
});
|
|
@@ -11404,8 +12209,21 @@
|
|
|
11404
12209
|
return out.join("<br>");
|
|
11405
12210
|
}
|
|
11406
12211
|
|
|
12212
|
+
function getEditorLanguageForPreview() {
|
|
12213
|
+
const detected = detectLanguageFromName((sourceState && (sourceState.path || sourceState.label)) || "");
|
|
12214
|
+
if (detected && (!editorLanguage || editorLanguage === "markdown" || editorLanguage === "text")) {
|
|
12215
|
+
return detected;
|
|
12216
|
+
}
|
|
12217
|
+
return editorLanguage || detected || "";
|
|
12218
|
+
}
|
|
12219
|
+
|
|
12220
|
+
function supportsCodePreviewCommentsForLanguage(language) {
|
|
12221
|
+
const lang = normalizeFenceLanguage(language || "");
|
|
12222
|
+
return Boolean(lang) && lang !== "markdown" && lang !== "latex" && !getDelimitedTextPreviewConfig(lang);
|
|
12223
|
+
}
|
|
12224
|
+
|
|
11407
12225
|
function supportsCodePreviewCommentsForCurrentEditor() {
|
|
11408
|
-
return
|
|
12226
|
+
return supportsCodePreviewCommentsForLanguage(getEditorLanguageForPreview());
|
|
11409
12227
|
}
|
|
11410
12228
|
|
|
11411
12229
|
function getCodePreviewCommentKind(language) {
|
|
@@ -11450,11 +12268,11 @@
|
|
|
11450
12268
|
return "<div class='response-markdown-highlight preview-code-lines'>" + html.join("") + "</div>";
|
|
11451
12269
|
}
|
|
11452
12270
|
|
|
11453
|
-
function renderCodePreviewWithCommentBlocks(targetEl, text, pane) {
|
|
12271
|
+
function renderCodePreviewWithCommentBlocks(targetEl, text, pane, language) {
|
|
11454
12272
|
if (!targetEl) return;
|
|
11455
12273
|
clearPreviewJumpHighlight(targetEl);
|
|
11456
12274
|
finishPreviewRender(targetEl);
|
|
11457
|
-
targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
|
|
12275
|
+
targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, language || editorLanguage || "");
|
|
11458
12276
|
ensurePreviewSelectionActions(targetEl);
|
|
11459
12277
|
updatePreviewCommentBlocksForElement(targetEl);
|
|
11460
12278
|
decorateCopyablePreviewBlocks(targetEl);
|
|
@@ -11606,6 +12424,26 @@
|
|
|
11606
12424
|
return Boolean(shortcutsOverlayEl && !shortcutsOverlayEl.hidden);
|
|
11607
12425
|
}
|
|
11608
12426
|
|
|
12427
|
+
function handleShortcutsScrollShortcut(event) {
|
|
12428
|
+
if (!isShortcutsOpen() || !shortcutsBodyEl || !event) return false;
|
|
12429
|
+
if (isTextEntryShortcutTarget(event.target)) return false;
|
|
12430
|
+
const key = typeof event.key === "string" ? event.key : "";
|
|
12431
|
+
let delta = 0;
|
|
12432
|
+
let targetTop = null;
|
|
12433
|
+
if (key === "ArrowDown") delta = 42;
|
|
12434
|
+
else if (key === "ArrowUp") delta = -42;
|
|
12435
|
+
else if (key === "PageDown") delta = Math.max(120, Math.round((shortcutsBodyEl.clientHeight || 0) * 0.85));
|
|
12436
|
+
else if (key === "PageUp") delta = -Math.max(120, Math.round((shortcutsBodyEl.clientHeight || 0) * 0.85));
|
|
12437
|
+
else if (key === "Home") targetTop = 0;
|
|
12438
|
+
else if (key === "End") targetTop = shortcutsBodyEl.scrollHeight;
|
|
12439
|
+
else return false;
|
|
12440
|
+
event.preventDefault();
|
|
12441
|
+
event.stopPropagation();
|
|
12442
|
+
if (targetTop !== null) shortcutsBodyEl.scrollTop = targetTop;
|
|
12443
|
+
else shortcutsBodyEl.scrollTop += delta;
|
|
12444
|
+
return true;
|
|
12445
|
+
}
|
|
12446
|
+
|
|
11609
12447
|
function isScratchpadOpen() {
|
|
11610
12448
|
return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
|
|
11611
12449
|
}
|
|
@@ -15841,7 +16679,8 @@
|
|
|
15841
16679
|
? window.requestAnimationFrame.bind(window)
|
|
15842
16680
|
: (cb) => window.setTimeout(cb, 16);
|
|
15843
16681
|
schedule(() => {
|
|
15844
|
-
if (
|
|
16682
|
+
if (shortcutsBodyEl && typeof shortcutsBodyEl.focus === "function") shortcutsBodyEl.focus({ preventScroll: true });
|
|
16683
|
+
else if (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
|
|
15845
16684
|
});
|
|
15846
16685
|
}
|
|
15847
16686
|
|
|
@@ -16039,6 +16878,9 @@
|
|
|
16039
16878
|
if (editorView === "preview") {
|
|
16040
16879
|
scheduleSourcePreviewRender(0);
|
|
16041
16880
|
}
|
|
16881
|
+
if (rightView === "editor-preview") {
|
|
16882
|
+
scheduleResponseEditorPreviewRender(0);
|
|
16883
|
+
}
|
|
16042
16884
|
updateOutlineUi();
|
|
16043
16885
|
scheduleWorkspacePersistence();
|
|
16044
16886
|
}
|
|
@@ -17797,7 +18639,7 @@
|
|
|
17797
18639
|
return;
|
|
17798
18640
|
}
|
|
17799
18641
|
|
|
17800
|
-
var suggestedName = sourceState.label ? sourceState.label
|
|
18642
|
+
var suggestedName = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
|
|
17801
18643
|
var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
|
|
17802
18644
|
const suggested = sourceState.path || (suggestedDir + suggestedName);
|
|
17803
18645
|
const path = window.prompt("Save editor content as:", suggested);
|
|
@@ -17852,7 +18694,7 @@
|
|
|
17852
18694
|
if (refreshFromDiskBtn) {
|
|
17853
18695
|
refreshFromDiskBtn.addEventListener("click", () => {
|
|
17854
18696
|
if (!hasRefreshableFilePath()) {
|
|
17855
|
-
setStatus("Refresh from disk
|
|
18697
|
+
setStatus("Refresh from disk needs a file path. Use Files → Open here, Files → Open file tab, or /studio-editor-only <path> for a refreshable editor tab.", "warning");
|
|
17856
18698
|
return;
|
|
17857
18699
|
}
|
|
17858
18700
|
|
|
@@ -17867,6 +18709,7 @@
|
|
|
17867
18709
|
const sent = sendMessage({
|
|
17868
18710
|
type: "refresh_from_disk_request",
|
|
17869
18711
|
requestId,
|
|
18712
|
+
path: sourceState.path,
|
|
17870
18713
|
});
|
|
17871
18714
|
|
|
17872
18715
|
if (!sent) {
|
|
@@ -18487,7 +19330,7 @@
|
|
|
18487
19330
|
setEditorText(text, { preserveScroll: false, preserveSelection: false });
|
|
18488
19331
|
setSourceState({
|
|
18489
19332
|
source: "upload",
|
|
18490
|
-
label: "
|
|
19333
|
+
label: "imported copy: " + file.name,
|
|
18491
19334
|
path: null,
|
|
18492
19335
|
});
|
|
18493
19336
|
refreshResponseUi();
|
|
@@ -18495,7 +19338,7 @@
|
|
|
18495
19338
|
if (detectedLang) {
|
|
18496
19339
|
setEditorLanguage(detectedLang);
|
|
18497
19340
|
}
|
|
18498
|
-
setStatus("
|
|
19341
|
+
setStatus("Imported file copy: " + file.name + ".", "success");
|
|
18499
19342
|
};
|
|
18500
19343
|
reader.onerror = () => {
|
|
18501
19344
|
setStatus("Failed to read file.", "error");
|