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.
@@ -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
- if (String(entry.argsSummary || "").trim()) {
851
- parts.push("Input:\n" + String(entry.argsSummary || "").trim());
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) resourceDirLabel.textContent = "";
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
- const pendingWindow = window.open("", "_blank");
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 editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
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-preview.pdf");
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 + "-preview.pdf";
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 (exportWarning) {
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 editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
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-preview.html");
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 + "-preview.html";
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 (exportWarning) {
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
- if (isHtmlArtifactPreviewText(text, editorLanguage)) {
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 (supportsCodePreviewCommentsForCurrentEditor()) {
7597
- renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
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 output (" + formatTraceOutputSize(value) + ")."
7735
- : "Output truncated — " + (hiddenParts.join(", ") || "more hidden") + ".";
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 argsSummary = entry.argsSummary
8111
- ? "<div class='trace-section'><div class='trace-section-label'>Input</div>" + renderTraceOutput(entry.argsSummary, entry.id + ":input") + "</div>"
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 textActions = kind === "text"
8223
- ? "<button type='button' data-files-action='open-new' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
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 === "copy-path" || action === "copy-root") {
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
- if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
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 (supportsCodePreviewCommentsForCurrentEditor()) {
8433
- renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
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
- // Upload with working dir + filename: derive path
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.replace(/^upload:\s*/i, "");
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.replace(/^upload:\s*/i, "") : "draft.md";
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 only available for documents that currently have a file path.";
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
- if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
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
- const popup = pendingWindow || window.open("", "_blank");
9993
- try {
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
- async function openPreviewDocumentHere(href, contextOverride) {
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 confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
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 path = typeof payload.path === "string" ? payload.path : "";
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
- setSourceState({ source: "file", label, path });
10048
- markFileBackedBaseline(payload.text);
10049
- const detected = detectLanguageFromName(path || label);
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("Opened linked file in editor: " + label, "success");
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 linked file in a new editor.", "success");
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
- const pendingWindow = window.open("", "_blank");
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 Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex";
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 (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
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.replace(/^upload:\s*/i, "") : "draft.md";
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 is only available for file-backed documents.", "warning");
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: "upload: " + file.name,
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("Loaded file " + file.name + ".", "success");
19341
+ setStatus("Imported file copy: " + file.name + ".", "success");
18499
19342
  };
18500
19343
  reader.onerror = () => {
18501
19344
  setStatus("Failed to read file.", "error");