pi-studio 0.9.16 → 0.9.18

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.
@@ -115,6 +115,8 @@
115
115
  const replSendModeSelect = document.getElementById("replSendModeSelect");
116
116
  const copyDraftBtn = document.getElementById("copyDraftBtn");
117
117
  const suggestCompletionBtn = document.getElementById("suggestCompletionBtn");
118
+ const suggestCompletionOptionsBtn = document.getElementById("suggestCompletionOptionsBtn");
119
+ const completionContextSelect = document.getElementById("completionContextSelect");
118
120
  const completionSuggestionPanelEl = document.getElementById("completionSuggestionPanel");
119
121
  const completionSuggestionTextEl = document.getElementById("completionSuggestionText");
120
122
  const completionSuggestionInsertBtn = document.getElementById("completionSuggestionInsertBtn");
@@ -129,6 +131,7 @@
129
131
  const shortcutsBtn = document.getElementById("shortcutsBtn");
130
132
  const shortcutsOverlayEl = document.getElementById("shortcutsOverlay");
131
133
  const shortcutsDialogEl = document.getElementById("shortcutsDialog");
134
+ const shortcutsBodyEl = document.getElementById("shortcutsBody");
132
135
  const shortcutsCloseBtn = document.getElementById("shortcutsCloseBtn");
133
136
  const leftFocusBtn = document.getElementById("leftFocusBtn");
134
137
  const rightFocusBtn = document.getElementById("rightFocusBtn");
@@ -209,6 +212,18 @@
209
212
  let studioHtmlFocusFullscreenBtn = null;
210
213
  let studioHtmlFocusLastFocusedEl = null;
211
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;
212
227
  let pendingRequestId = null;
213
228
  let pendingKind = null;
214
229
  let stickyStudioKind = null;
@@ -256,6 +271,8 @@
256
271
  const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
257
272
  const EDITOR_TAB_TEXT = " ";
258
273
  const QUIZ_DEFAULT_COUNT = 5;
274
+ const COMPLETION_CONTEXT_STORAGE_KEY = "piStudio.completionContextMode";
275
+ const COMPLETION_CONTEXT_MAX_CHARS = 12000;
259
276
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
260
277
  const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
261
278
  const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
@@ -1910,6 +1927,8 @@
1910
1927
  matlab: { label: "MATLAB", exts: ["m"] },
1911
1928
  latex: { label: "LaTeX", exts: ["tex", "latex"] },
1912
1929
  diff: { label: "Diff", exts: ["diff", "patch"] },
1930
+ csv: { label: "CSV", exts: ["csv"] },
1931
+ tsv: { label: "TSV", exts: ["tsv"] },
1913
1932
  // Languages accepted for upload/detect but without syntax highlighting
1914
1933
  java: { label: "Java", exts: ["java"] },
1915
1934
  go: { label: "Go", exts: ["go"] },
@@ -1937,6 +1956,9 @@
1937
1956
  const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
1938
1957
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
1939
1958
  const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
1959
+ const DELIMITED_PREVIEW_MAX_DATA_ROWS = 200;
1960
+ const DELIMITED_PREVIEW_MAX_COLUMNS = 50;
1961
+ const DELIMITED_PREVIEW_MAX_CELL_CHARS = 500;
1940
1962
  const previewPendingTimers = new WeakMap();
1941
1963
  const htmlArtifactFramesById = new Map();
1942
1964
  let sourcePreviewRenderTimer = null;
@@ -1949,6 +1971,7 @@
1949
1971
  let editorLanguage = "markdown";
1950
1972
  let responseHighlightEnabled = false;
1951
1973
  let completionSuggestionState = null;
1974
+ let completionSuggestionContextMode = readCompletionSuggestionContextMode();
1952
1975
  let completionSuggestionInFlight = false;
1953
1976
  let completionSuggestionRequestId = null;
1954
1977
  let completionSuggestionPendingSnapshot = null;
@@ -2339,6 +2362,34 @@
2339
2362
  appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
2340
2363
  }
2341
2364
 
2365
+ let contextMenu = null;
2366
+ if (suggestCompletionOptionsBtn) {
2367
+ suggestCompletionOptionsBtn.hidden = false;
2368
+ if (completionContextSelect) completionContextSelect.hidden = true;
2369
+ contextMenu = makeStudioUiRefreshMenu(suggestCompletionOptionsBtn, "context", "studio-refresh-context-anchor");
2370
+ if (sourceBadgeEl) appendStudioUiRefreshMenuSection(contextMenu.menu, "Document", [sourceBadgeEl]);
2371
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Working directory", [resourceDirBtn, resourceDirLabel, resourceDirInputWrap]);
2372
+ const cursorContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor only");
2373
+ cursorContextBtn.type = "button";
2374
+ cursorContextBtn.setAttribute("data-completion-context-mode", "cursor");
2375
+ const sessionContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor + latest response");
2376
+ sessionContextBtn.type = "button";
2377
+ sessionContextBtn.setAttribute("data-completion-context-mode", "session");
2378
+ [cursorContextBtn, sessionContextBtn].forEach((button) => {
2379
+ button.addEventListener("click", (event) => {
2380
+ event.preventDefault();
2381
+ event.stopPropagation();
2382
+ setCompletionSuggestionContextMode(button.getAttribute("data-completion-context-mode") === "session" ? "session" : "cursor");
2383
+ syncActionButtons();
2384
+ });
2385
+ });
2386
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Suggestions", [cursorContextBtn, sessionContextBtn]);
2387
+ if (syncBadgeEl) {
2388
+ syncBadgeEl.hidden = false;
2389
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Status", [syncBadgeEl]);
2390
+ }
2391
+ }
2392
+
2342
2393
  const headerTopEl = makeStudioUiRefreshElement("div", "studio-refresh-header-top");
2343
2394
  const titleGroupEl = makeStudioUiRefreshElement("div", "studio-refresh-title-group");
2344
2395
  if (leftFocusBtn) {
@@ -2351,6 +2402,10 @@
2351
2402
  } else if (editorViewSelect) {
2352
2403
  titleGroupEl.appendChild(editorViewSelect);
2353
2404
  }
2405
+ if (contextMenu) {
2406
+ titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
2407
+ titleGroupEl.appendChild(contextMenu.anchor);
2408
+ }
2354
2409
  headerTopEl.appendChild(titleGroupEl);
2355
2410
  const headerToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
2356
2411
  if (reviewNotesBtn) headerToolsEl.appendChild(reviewNotesBtn);
@@ -2359,18 +2414,7 @@
2359
2414
  if (reviewMenu) headerToolsEl.appendChild(reviewMenu.anchor);
2360
2415
  headerTopEl.appendChild(headerToolsEl);
2361
2416
 
2362
- const headerUtilityEl = makeStudioUiRefreshElement("div", "studio-refresh-header-utility");
2363
- const utilityLeftEl = makeStudioUiRefreshElement("div", "studio-refresh-utility-left");
2364
- if (sourceBadgeEl) utilityLeftEl.appendChild(sourceBadgeEl);
2365
- if (sourceBadgeEl && (resourceDirBtn || resourceDirLabel || resourceDirInputWrap || syncBadgeEl)) {
2366
- utilityLeftEl.appendChild(makeStudioUiRefreshSeparator());
2367
- }
2368
- if (resourceDirBtn) utilityLeftEl.appendChild(resourceDirBtn);
2369
- if (resourceDirLabel) utilityLeftEl.appendChild(resourceDirLabel);
2370
- if (resourceDirInputWrap) utilityLeftEl.appendChild(resourceDirInputWrap);
2371
- if (syncBadgeEl) utilityLeftEl.appendChild(syncBadgeEl);
2372
- headerUtilityEl.appendChild(utilityLeftEl);
2373
- leftHeaderEl.replaceChildren(headerTopEl, headerUtilityEl);
2417
+ leftHeaderEl.replaceChildren(headerTopEl);
2374
2418
 
2375
2419
  const rightHeaderEl = document.getElementById("rightSectionHeader");
2376
2420
  if (rightHeaderEl && rightViewSelect) {
@@ -2402,18 +2446,18 @@
2402
2446
  const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2403
2447
  if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
2404
2448
  if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
2405
- const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2406
- replActionLineEl.hidden = true;
2407
- if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2408
- if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2409
- const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2449
+ const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line studio-refresh-utility-action-line");
2410
2450
  actionLineTwoEl.appendChild(copyDraftBtn);
2411
2451
  if (suggestCompletionBtn) actionLineTwoEl.appendChild(suggestCompletionBtn);
2412
2452
  if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
2413
2453
  if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2454
+ const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2455
+ replActionLineEl.hidden = true;
2456
+ if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2457
+ if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2414
2458
  if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
2415
- if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
2416
2459
  actionsEl.appendChild(actionLineTwoEl);
2460
+ if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
2417
2461
 
2418
2462
  const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
2419
2463
  const annotationsButton = makeStudioUiRefreshElement("button", "", "Annotations");
@@ -2435,7 +2479,9 @@
2435
2479
  annotationsButton,
2436
2480
  viewButton,
2437
2481
  reviewButton: reviewMenu ? reviewMenu.button : null,
2438
- menus: [annotationsMenu, viewMenu].concat(reviewMenu ? [reviewMenu] : []),
2482
+ menus: [annotationsMenu, viewMenu]
2483
+ .concat(contextMenu ? [contextMenu] : [])
2484
+ .concat(reviewMenu ? [reviewMenu] : []),
2439
2485
  };
2440
2486
 
2441
2487
  document.addEventListener("click", (event) => {
@@ -3638,6 +3684,12 @@
3638
3684
  && typeof studioHtmlFocusShellEl.contains === "function"
3639
3685
  && studioHtmlFocusShellEl.contains(event.target)
3640
3686
  );
3687
+ const imageFocusOwnsEvent = Boolean(
3688
+ studioImageFocusDialogEl
3689
+ && event.target
3690
+ && typeof studioImageFocusDialogEl.contains === "function"
3691
+ && studioImageFocusDialogEl.contains(event.target)
3692
+ );
3641
3693
  const quizOwnsEvent = Boolean(
3642
3694
  quizDialogEl
3643
3695
  && event.target
@@ -3663,6 +3715,14 @@
3663
3715
  return;
3664
3716
  }
3665
3717
 
3718
+ if (isStudioImageFocusOpen() && plainEscape) {
3719
+ event.preventDefault();
3720
+ closeStudioImageFocusViewer();
3721
+ return;
3722
+ }
3723
+
3724
+ if (handleStudioImageFocusShortcut(event)) return;
3725
+
3666
3726
  if (isScratchpadOpen() && plainEscape) {
3667
3727
  event.preventDefault();
3668
3728
  closeScratchpad();
@@ -3675,6 +3735,8 @@
3675
3735
  return;
3676
3736
  }
3677
3737
 
3738
+ if (handleShortcutsScrollShortcut(event)) return;
3739
+
3678
3740
  if (isReviewNotesOpen() && plainEscape) {
3679
3741
  event.preventDefault();
3680
3742
  closeReviewNotes();
@@ -3687,7 +3749,15 @@
3687
3749
  return;
3688
3750
  }
3689
3751
 
3690
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
3752
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || imageFocusOwnsEvent || quizOwnsEvent) {
3753
+ return;
3754
+ }
3755
+
3756
+ if (plainEscape && completionSuggestionState) {
3757
+ event.preventDefault();
3758
+ hideCompletionSuggestion();
3759
+ focusSourceTextNoScroll();
3760
+ setStatus("Dismissed completion suggestion.");
3691
3761
  return;
3692
3762
  }
3693
3763
 
@@ -3854,6 +3924,22 @@
3854
3924
  }
3855
3925
  }
3856
3926
 
3927
+ function formatStudioExportTimestamp(date) {
3928
+ const value = date instanceof Date ? date : new Date();
3929
+ const pad = (part) => String(part).padStart(2, "0");
3930
+ try {
3931
+ return String(value.getFullYear())
3932
+ + pad(value.getMonth() + 1)
3933
+ + pad(value.getDate())
3934
+ + "-"
3935
+ + pad(value.getHours())
3936
+ + pad(value.getMinutes())
3937
+ + pad(value.getSeconds());
3938
+ } catch {
3939
+ return String(Date.now());
3940
+ }
3941
+ }
3942
+
3857
3943
  function normalizeHistoryKind(kind) {
3858
3944
  return kind === "critique" ? "critique" : "annotation";
3859
3945
  }
@@ -4221,9 +4307,202 @@
4221
4307
  return marker + (lang ? lang : "") + newline + source + newline + marker;
4222
4308
  }
4223
4309
 
4310
+ function getDelimitedTextPreviewConfig(language) {
4311
+ const lang = normalizeFenceLanguage(language || "");
4312
+ if (lang === "csv") return { kind: "csv", label: "CSV", delimiter: "," };
4313
+ if (lang === "tsv") return { kind: "tsv", label: "TSV", delimiter: "\t" };
4314
+ return null;
4315
+ }
4316
+
4317
+ function parseDelimitedTextRows(text, delimiter, maxRows) {
4318
+ const source = String(text || "").replace(/^\uFEFF/, "");
4319
+ const limit = Math.max(1, Number(maxRows) || (DELIMITED_PREVIEW_MAX_DATA_ROWS + 1));
4320
+ const rows = [];
4321
+ let row = [];
4322
+ let cell = "";
4323
+ let inQuotes = false;
4324
+ let truncatedRows = false;
4325
+
4326
+ const pushCell = () => {
4327
+ row.push(cell);
4328
+ cell = "";
4329
+ };
4330
+ const pushRow = (index) => {
4331
+ pushCell();
4332
+ rows.push(row);
4333
+ row = [];
4334
+ if (rows.length >= limit) {
4335
+ truncatedRows = index < source.length - 1;
4336
+ return true;
4337
+ }
4338
+ return false;
4339
+ };
4340
+
4341
+ for (let i = 0; i < source.length; i += 1) {
4342
+ if (rows.length >= limit) {
4343
+ truncatedRows = true;
4344
+ break;
4345
+ }
4346
+ const ch = source[i];
4347
+ if (inQuotes) {
4348
+ if (ch === '"') {
4349
+ if (source[i + 1] === '"') {
4350
+ cell += '"';
4351
+ i += 1;
4352
+ } else {
4353
+ inQuotes = false;
4354
+ }
4355
+ } else {
4356
+ cell += ch;
4357
+ }
4358
+ continue;
4359
+ }
4360
+ if (ch === '"' && cell === "") {
4361
+ inQuotes = true;
4362
+ continue;
4363
+ }
4364
+ if (ch === delimiter) {
4365
+ pushCell();
4366
+ continue;
4367
+ }
4368
+ if (ch === "\n") {
4369
+ if (pushRow(i)) break;
4370
+ continue;
4371
+ }
4372
+ if (ch === "\r") {
4373
+ if (source[i + 1] === "\n") i += 1;
4374
+ if (pushRow(i)) break;
4375
+ continue;
4376
+ }
4377
+ cell += ch;
4378
+ }
4379
+
4380
+ if (!truncatedRows && rows.length < limit && (cell.length > 0 || row.length > 0)) {
4381
+ pushCell();
4382
+ rows.push(row);
4383
+ }
4384
+
4385
+ return { rows, truncatedRows };
4386
+ }
4387
+
4388
+ function buildDelimitedTextPreviewModel(text, language) {
4389
+ const config = getDelimitedTextPreviewConfig(language);
4390
+ if (!config) return null;
4391
+ const parsed = parseDelimitedTextRows(text, config.delimiter, DELIMITED_PREVIEW_MAX_DATA_ROWS + 1);
4392
+ const rows = parsed.rows;
4393
+ const rawColumnCount = rows.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0);
4394
+ const columnCount = Math.min(rawColumnCount, DELIMITED_PREVIEW_MAX_COLUMNS);
4395
+ const header = rows[0] || [];
4396
+ const dataRows = rows.slice(1);
4397
+ return {
4398
+ ...config,
4399
+ rows,
4400
+ header,
4401
+ dataRows,
4402
+ rawColumnCount,
4403
+ columnCount,
4404
+ truncatedColumns: rawColumnCount > columnCount,
4405
+ truncatedRows: parsed.truncatedRows,
4406
+ };
4407
+ }
4408
+
4409
+ function getDelimitedHeaderLabel(header, index) {
4410
+ const value = String((header && header[index]) || "").trim();
4411
+ return value || ("Column " + (index + 1));
4412
+ }
4413
+
4414
+ function formatDelimitedPreviewCellHtml(value) {
4415
+ const raw = String(value ?? "");
4416
+ if (raw.length <= DELIMITED_PREVIEW_MAX_CELL_CHARS) return escapeHtml(raw);
4417
+ return escapeHtml(raw.slice(0, DELIMITED_PREVIEW_MAX_CELL_CHARS)) + "<span class='delimited-preview-truncation'>…</span>";
4418
+ }
4419
+
4420
+ function formatDelimitedMarkdownCell(value) {
4421
+ const raw = String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
4422
+ const shortened = raw.length > DELIMITED_PREVIEW_MAX_CELL_CHARS
4423
+ ? raw.slice(0, DELIMITED_PREVIEW_MAX_CELL_CHARS) + "…"
4424
+ : raw;
4425
+ return shortened.replace(/\n/g, "<br>").replace(/\|/g, "\\|").trim() || " ";
4426
+ }
4427
+
4428
+ function buildDelimitedTextPreviewHtml(text, language) {
4429
+ const model = buildDelimitedTextPreviewModel(text, language);
4430
+ if (!model) return "";
4431
+ if (!model.rows.length || model.columnCount <= 0) {
4432
+ 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>";
4433
+ }
4434
+ const columnIndexes = Array.from({ length: model.columnCount }, (_, index) => index);
4435
+ const headerHtml = columnIndexes.map((index) => "<th scope='col'>" + escapeHtml(getDelimitedHeaderLabel(model.header, index)) + "</th>").join("");
4436
+ const bodyHtml = model.dataRows.length
4437
+ ? model.dataRows.map((row, rowIndex) => {
4438
+ const cells = columnIndexes.map((index) => {
4439
+ const raw = String((row && row[index]) ?? "");
4440
+ const emptyClass = raw.length === 0 ? " delimited-preview-empty-cell" : "";
4441
+ return "<td class='" + emptyClass.trim() + "'>" + formatDelimitedPreviewCellHtml(raw) + "</td>";
4442
+ }).join("");
4443
+ return "<tr><th scope='row' class='delimited-preview-row-number'>" + String(rowIndex + 1) + "</th>" + cells + "</tr>";
4444
+ }).join("")
4445
+ : "<tr><td colspan='" + String(model.columnCount + 1) + "' class='delimited-preview-empty'>No data rows after the header.</td></tr>";
4446
+ const notices = [];
4447
+ if (model.truncatedRows) notices.push("Showing first " + String(Math.max(0, model.dataRows.length)) + " data rows.");
4448
+ if (model.truncatedColumns) notices.push("Showing first " + String(model.columnCount) + " of " + String(model.rawColumnCount) + " columns.");
4449
+ const noticeHtml = notices.length ? "<div class='preview-warning delimited-preview-notice'>" + escapeHtml(notices.join(" ")) + "</div>" : "";
4450
+ const summaryParts = [String(model.dataRows.length) + (model.truncatedRows ? "+" : "") + " data rows", String(model.rawColumnCount) + " columns"];
4451
+ return "<div class='delimited-preview rendered-markdown'>"
4452
+ + "<div class='delimited-preview-header'><div><strong>" + escapeHtml(model.label) + " preview</strong><span>" + escapeHtml(summaryParts.join(" · ")) + "</span></div></div>"
4453
+ + noticeHtml
4454
+ + "<div class='delimited-preview-table-wrap'><table>"
4455
+ + "<thead><tr><th scope='col' class='delimited-preview-row-number'>#</th>" + headerHtml + "</tr></thead>"
4456
+ + "<tbody>" + bodyHtml + "</tbody>"
4457
+ + "</table></div>"
4458
+ + "</div>";
4459
+ }
4460
+
4461
+ function buildDelimitedTextPreviewMarkdown(text, language) {
4462
+ const model = buildDelimitedTextPreviewModel(text, language);
4463
+ if (!model) return "";
4464
+ if (!model.rows.length || model.columnCount <= 0) return "_No tabular data to preview._";
4465
+ const columnIndexes = Array.from({ length: model.columnCount }, (_, index) => index);
4466
+ const lines = ["**" + model.label + " preview**", ""];
4467
+ const notices = [];
4468
+ if (model.truncatedRows) notices.push("showing first " + String(Math.max(0, model.dataRows.length)) + " data rows");
4469
+ if (model.truncatedColumns) notices.push("showing first " + String(model.columnCount) + " of " + String(model.rawColumnCount) + " columns");
4470
+ if (notices.length) lines.push("_" + notices.join("; ") + "._", "");
4471
+ lines.push("| " + columnIndexes.map((index) => formatDelimitedMarkdownCell(getDelimitedHeaderLabel(model.header, index))).join(" | ") + " |");
4472
+ lines.push("| " + columnIndexes.map(() => "---").join(" | ") + " |");
4473
+ if (model.dataRows.length) {
4474
+ model.dataRows.forEach((row) => {
4475
+ lines.push("| " + columnIndexes.map((index) => formatDelimitedMarkdownCell(row && row[index])).join(" | ") + " |");
4476
+ });
4477
+ } else {
4478
+ lines.push("| " + columnIndexes.map(() => " ").join(" | ") + " |");
4479
+ }
4480
+ return lines.join("\n");
4481
+ }
4482
+
4483
+ function renderDelimitedTextPreview(targetEl, text, pane) {
4484
+ const html = buildDelimitedTextPreviewHtml(text, editorLanguage || "");
4485
+ if (!html || !targetEl) return false;
4486
+ if (pane === "source") {
4487
+ sourcePreviewRenderNonce += 1;
4488
+ } else if (pane === "response") {
4489
+ responsePreviewRenderNonce += 1;
4490
+ }
4491
+ clearPreviewJumpHighlight(targetEl);
4492
+ finishPreviewRender(targetEl);
4493
+ targetEl.innerHTML = html;
4494
+ if (pane === "response") {
4495
+ applyPendingResponseScrollReset();
4496
+ scheduleResponsePaneRepaintNudge();
4497
+ }
4498
+ return true;
4499
+ }
4500
+
4224
4501
  function prepareEditorTextForPdfExport(text) {
4225
4502
  const prepared = prepareEditorTextForPreview(text);
4226
4503
  const lang = normalizeFenceLanguage(editorLanguage || "");
4504
+ const delimitedPreview = buildDelimitedTextPreviewMarkdown(prepared, lang);
4505
+ if (delimitedPreview) return delimitedPreview;
4227
4506
  if (lang && lang !== "markdown" && lang !== "latex") {
4228
4507
  return wrapAsFencedCodeBlock(prepared, lang);
4229
4508
  }
@@ -4233,6 +4512,8 @@
4233
4512
  function prepareEditorTextForHtmlExport(text) {
4234
4513
  const prepared = prepareEditorTextForPreview(text);
4235
4514
  const lang = normalizeFenceLanguage(editorLanguage || "");
4515
+ const delimitedPreview = buildDelimitedTextPreviewMarkdown(prepared, lang);
4516
+ if (delimitedPreview) return delimitedPreview;
4236
4517
  if (lang && lang !== "markdown" && lang !== "latex") {
4237
4518
  return wrapAsFencedCodeBlock(prepared, lang);
4238
4519
  }
@@ -4244,20 +4525,15 @@
4244
4525
 
4245
4526
  if (isEditorOnlyMode) {
4246
4527
  syncBadgeEl.hidden = true;
4247
- syncBadgeEl.classList.remove("sync");
4248
- return;
4249
- }
4250
-
4251
- if (rightView === "trace") {
4252
- syncBadgeEl.hidden = true;
4253
- syncBadgeEl.classList.remove("sync");
4528
+ syncBadgeEl.textContent = "Editor-only tab";
4529
+ syncBadgeEl.classList.remove("sync", "out-of-sync");
4254
4530
  return;
4255
4531
  }
4256
4532
 
4257
4533
  if (!latestResponseHasContent) {
4258
- syncBadgeEl.hidden = true;
4259
- syncBadgeEl.textContent = "In sync with response";
4260
- syncBadgeEl.classList.remove("sync");
4534
+ syncBadgeEl.hidden = false;
4535
+ syncBadgeEl.textContent = "No latest response";
4536
+ syncBadgeEl.classList.remove("sync", "out-of-sync");
4261
4537
  return;
4262
4538
  }
4263
4539
 
@@ -4265,15 +4541,10 @@
4265
4541
  ? normalizedEditorText
4266
4542
  : normalizeForCompare(sourceTextEl.value);
4267
4543
  const inSync = normalizedEditor === latestResponseNormalized;
4268
- syncBadgeEl.hidden = !inSync;
4269
- syncBadgeEl.textContent = "In sync with response";
4270
-
4271
- if (inSync) {
4272
- syncBadgeEl.classList.add("sync");
4273
- return;
4274
- }
4275
-
4276
- syncBadgeEl.classList.remove("sync");
4544
+ syncBadgeEl.hidden = false;
4545
+ syncBadgeEl.textContent = inSync ? "In sync with response" : "Editor differs from latest response";
4546
+ syncBadgeEl.classList.toggle("sync", inSync);
4547
+ syncBadgeEl.classList.toggle("out-of-sync", !inSync);
4277
4548
  }
4278
4549
 
4279
4550
  function buildPlainMarkdownHtml(markdown, options) {
@@ -5070,13 +5341,12 @@
5070
5341
  return;
5071
5342
  }
5072
5343
  if (kind === "image") {
5073
- const pendingWindow = window.open("", "_blank");
5074
- void openPreviewImageLink(context.href, context.title, context, pendingWindow).catch((error) => {
5344
+ void openPreviewImageLink(context.href, context.title, context).catch((error) => {
5075
5345
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
5076
5346
  });
5077
5347
  return;
5078
5348
  }
5079
- if (kind === "text") {
5349
+ if (kind === "text" || kind === "office") {
5080
5350
  const pendingWindow = window.open("", "_blank");
5081
5351
  void openPreviewDocumentInNewEditor(context.href, pendingWindow, context).catch((error) => {
5082
5352
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
@@ -5746,6 +6016,394 @@
5746
6016
  }
5747
6017
  }
5748
6018
 
6019
+ function isStudioImageFocusOpen() {
6020
+ return Boolean(studioImageFocusOverlayEl && studioImageFocusOverlayEl.hidden === false);
6021
+ }
6022
+
6023
+ function isStudioImageFocusSrcAllowed(src) {
6024
+ const value = String(src || "").trim();
6025
+ if (!value) return false;
6026
+ if (/^javascript:/i.test(value)) return false;
6027
+ return /^(?:data:image\/|blob:|https?:|file:|\/|\.\/|\.\.\/)/i.test(value);
6028
+ }
6029
+
6030
+ function clampStudioImageFocusZoom(value) {
6031
+ const parsed = Number(value);
6032
+ if (!Number.isFinite(parsed) || parsed <= 0) return 1;
6033
+ return Math.max(0.1, Math.min(8, parsed));
6034
+ }
6035
+
6036
+ function getStudioImageFocusFitScale() {
6037
+ const img = studioImageFocusImgEl;
6038
+ const slot = studioImageFocusSlotEl;
6039
+ if (!img || !slot) return 1;
6040
+ const naturalWidth = Number(img.naturalWidth) || 0;
6041
+ const naturalHeight = Number(img.naturalHeight) || 0;
6042
+ if (naturalWidth <= 0 || naturalHeight <= 0) return 1;
6043
+ let paddingX = 0;
6044
+ let paddingY = 0;
6045
+ try {
6046
+ const style = window.getComputedStyle(slot);
6047
+ paddingX = (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0);
6048
+ paddingY = (Number.parseFloat(style.paddingTop) || 0) + (Number.parseFloat(style.paddingBottom) || 0);
6049
+ } catch {}
6050
+ const availableWidth = Math.max(1, (slot.clientWidth || 0) - paddingX);
6051
+ const availableHeight = Math.max(1, (slot.clientHeight || 0) - paddingY);
6052
+ return clampStudioImageFocusZoom(Math.min(1, availableWidth / naturalWidth, availableHeight / naturalHeight));
6053
+ }
6054
+
6055
+ function getStudioImageFocusDisplayScale() {
6056
+ return studioImageFocusZoomMode === "fit"
6057
+ ? getStudioImageFocusFitScale()
6058
+ : clampStudioImageFocusZoom(studioImageFocusZoom);
6059
+ }
6060
+
6061
+ function syncStudioImageFocusZoom() {
6062
+ if (!studioImageFocusImgEl || !studioImageFocusSlotEl) return;
6063
+ const fitMode = studioImageFocusZoomMode === "fit";
6064
+ studioImageFocusSlotEl.classList.toggle("is-fit", fitMode);
6065
+ studioImageFocusSlotEl.classList.toggle("is-zoomed", !fitMode);
6066
+ if (fitMode) {
6067
+ studioImageFocusImgEl.style.width = "";
6068
+ studioImageFocusImgEl.style.height = "";
6069
+ studioImageFocusImgEl.style.maxWidth = "100%";
6070
+ studioImageFocusImgEl.style.maxHeight = "100%";
6071
+ } else {
6072
+ const zoom = clampStudioImageFocusZoom(studioImageFocusZoom);
6073
+ const naturalWidth = Number(studioImageFocusImgEl.naturalWidth) || 0;
6074
+ studioImageFocusImgEl.style.maxWidth = "none";
6075
+ studioImageFocusImgEl.style.maxHeight = "none";
6076
+ studioImageFocusImgEl.style.height = "auto";
6077
+ studioImageFocusImgEl.style.width = naturalWidth > 0 ? Math.max(1, Math.round(naturalWidth * zoom)) + "px" : Math.round(zoom * 100) + "%";
6078
+ }
6079
+ if (studioImageFocusZoomLabelEl) {
6080
+ studioImageFocusZoomLabelEl.textContent = Math.round(getStudioImageFocusDisplayScale() * 100) + "%";
6081
+ }
6082
+ }
6083
+
6084
+ function getStudioImageFocusViewportCenter() {
6085
+ const slot = studioImageFocusSlotEl;
6086
+ if (!slot) return { x: 0.5, y: 0.5 };
6087
+ const scrollWidth = Math.max(slot.scrollWidth || 0, slot.clientWidth || 0, 1);
6088
+ const scrollHeight = Math.max(slot.scrollHeight || 0, slot.clientHeight || 0, 1);
6089
+ return {
6090
+ x: Math.max(0, Math.min(1, (slot.scrollLeft + (slot.clientWidth || 0) / 2) / scrollWidth)),
6091
+ y: Math.max(0, Math.min(1, (slot.scrollTop + (slot.clientHeight || 0) / 2) / scrollHeight)),
6092
+ };
6093
+ }
6094
+
6095
+ function restoreStudioImageFocusViewportCenter(center) {
6096
+ const slot = studioImageFocusSlotEl;
6097
+ if (!slot || !center) return;
6098
+ const schedule = typeof window.requestAnimationFrame === "function"
6099
+ ? window.requestAnimationFrame.bind(window)
6100
+ : (callback) => window.setTimeout(callback, 0);
6101
+ schedule(() => {
6102
+ if (!slot.isConnected || studioImageFocusZoomMode === "fit") return;
6103
+ const maxLeft = Math.max(0, (slot.scrollWidth || 0) - (slot.clientWidth || 0));
6104
+ const maxTop = Math.max(0, (slot.scrollHeight || 0) - (slot.clientHeight || 0));
6105
+ slot.scrollLeft = Math.max(0, Math.min(maxLeft, (slot.scrollWidth || 0) * center.x - (slot.clientWidth || 0) / 2));
6106
+ slot.scrollTop = Math.max(0, Math.min(maxTop, (slot.scrollHeight || 0) * center.y - (slot.clientHeight || 0) / 2));
6107
+ });
6108
+ }
6109
+
6110
+ function getStudioImageFocusPointerCenter(event) {
6111
+ const slot = studioImageFocusSlotEl;
6112
+ if (!slot || !event || typeof slot.getBoundingClientRect !== "function") return getStudioImageFocusViewportCenter();
6113
+ const rect = slot.getBoundingClientRect();
6114
+ const scrollWidth = Math.max(slot.scrollWidth || 0, slot.clientWidth || 0, 1);
6115
+ const scrollHeight = Math.max(slot.scrollHeight || 0, slot.clientHeight || 0, 1);
6116
+ return {
6117
+ x: Math.max(0, Math.min(1, (slot.scrollLeft + (Number(event.clientX) || rect.left + rect.width / 2) - rect.left) / scrollWidth)),
6118
+ y: Math.max(0, Math.min(1, (slot.scrollTop + (Number(event.clientY) || rect.top + rect.height / 2) - rect.top) / scrollHeight)),
6119
+ };
6120
+ }
6121
+
6122
+ function setStudioImageFocusZoom(mode, zoom, options) {
6123
+ const center = options && options.center ? options.center : getStudioImageFocusViewportCenter();
6124
+ studioImageFocusZoomMode = mode === "fit" ? "fit" : "custom";
6125
+ studioImageFocusZoom = clampStudioImageFocusZoom(zoom);
6126
+ syncStudioImageFocusZoom();
6127
+ if (studioImageFocusZoomMode !== "fit") restoreStudioImageFocusViewportCenter(center);
6128
+ }
6129
+
6130
+ function zoomStudioImageFocus(factor, options) {
6131
+ const base = studioImageFocusZoomMode === "fit" ? getStudioImageFocusFitScale() : studioImageFocusZoom;
6132
+ setStudioImageFocusZoom("custom", clampStudioImageFocusZoom(base * factor), options);
6133
+ }
6134
+
6135
+ function handleStudioImageFocusWheel(event) {
6136
+ if (!isStudioImageFocusOpen() || !event) return;
6137
+ if (!event.altKey && !event.ctrlKey && !event.metaKey) return;
6138
+ event.preventDefault();
6139
+ event.stopPropagation();
6140
+ const delta = Number(event.deltaY) || 0;
6141
+ const factor = delta < 0 ? 1.12 : 1 / 1.12;
6142
+ zoomStudioImageFocus(factor, { center: getStudioImageFocusPointerCenter(event) });
6143
+ }
6144
+
6145
+ function handleStudioImageFocusShortcut(event) {
6146
+ if (!isStudioImageFocusOpen() || !event) return false;
6147
+ if (isTextEntryShortcutTarget(event.target)) return false;
6148
+ const key = typeof event.key === "string" ? event.key : "";
6149
+ const code = typeof event.code === "string" ? event.code : "";
6150
+ if (!event.altKey || event.metaKey || event.ctrlKey) return false;
6151
+ if (code === "Equal" || code === "NumpadAdd" || key === "=" || key === "+") {
6152
+ event.preventDefault();
6153
+ zoomStudioImageFocus(1.25);
6154
+ return true;
6155
+ }
6156
+ if (code === "Minus" || code === "NumpadSubtract" || key === "-" || key === "_") {
6157
+ event.preventDefault();
6158
+ zoomStudioImageFocus(1 / 1.25);
6159
+ return true;
6160
+ }
6161
+ if (code === "Digit0" || code === "Numpad0" || key === "0") {
6162
+ event.preventDefault();
6163
+ setStudioImageFocusZoom("fit", 1);
6164
+ return true;
6165
+ }
6166
+ return false;
6167
+ }
6168
+
6169
+ function syncStudioImageFocusFullscreenButton() {
6170
+ if (!studioImageFocusFullscreenBtn) return;
6171
+ const isFullscreen = Boolean(document.fullscreenElement && studioImageFocusDialogEl && document.fullscreenElement === studioImageFocusDialogEl);
6172
+ studioImageFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
6173
+ const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
6174
+ studioImageFocusFullscreenBtn.title = isFullscreen
6175
+ ? "Exit browser fullscreen and keep the image focus viewer open."
6176
+ : "Ask the browser to make this image viewer fullscreen.";
6177
+ studioImageFocusFullscreenBtn.setAttribute("aria-label", label);
6178
+ studioImageFocusFullscreenBtn.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
6179
+ }
6180
+
6181
+ async function toggleStudioImageFocusFullscreen() {
6182
+ const dialog = studioImageFocusDialogEl;
6183
+ if (!dialog) return;
6184
+ const isFullscreen = Boolean(document.fullscreenElement && document.fullscreenElement === dialog);
6185
+ if (isFullscreen) {
6186
+ try {
6187
+ if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
6188
+ } catch (error) {
6189
+ setStatus("Could not exit image fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
6190
+ } finally {
6191
+ syncStudioImageFocusFullscreenButton();
6192
+ }
6193
+ return;
6194
+ }
6195
+ if (typeof dialog.requestFullscreen !== "function") {
6196
+ setStatus("Browser fullscreen is not available for this image viewer.", "warning");
6197
+ return;
6198
+ }
6199
+ try {
6200
+ await dialog.requestFullscreen();
6201
+ } catch (error) {
6202
+ setStatus("Could not enter image fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
6203
+ } finally {
6204
+ syncStudioImageFocusFullscreenButton();
6205
+ }
6206
+ }
6207
+
6208
+ function appendStudioImageFocusTextButton(parent, label, title, onClick) {
6209
+ const button = document.createElement("button");
6210
+ button.type = "button";
6211
+ button.className = "studio-pdf-focus-btn studio-image-focus-zoom-btn";
6212
+ button.textContent = label;
6213
+ button.title = title;
6214
+ button.addEventListener("click", onClick);
6215
+ parent.appendChild(button);
6216
+ return button;
6217
+ }
6218
+
6219
+ function ensureStudioImageFocusViewer() {
6220
+ if (studioImageFocusOverlayEl) return studioImageFocusOverlayEl;
6221
+
6222
+ const overlay = document.createElement("div");
6223
+ overlay.className = "studio-pdf-focus-overlay studio-image-focus-overlay";
6224
+ overlay.hidden = true;
6225
+ overlay.setAttribute("role", "dialog");
6226
+ overlay.setAttribute("aria-modal", "true");
6227
+ overlay.setAttribute("aria-labelledby", "studioImageFocusTitle");
6228
+
6229
+ const dialog = document.createElement("div");
6230
+ dialog.className = "studio-pdf-focus-dialog studio-image-focus-dialog";
6231
+
6232
+ const header = document.createElement("div");
6233
+ header.className = "studio-pdf-focus-header studio-image-focus-header";
6234
+
6235
+ const titleGroup = document.createElement("div");
6236
+ titleGroup.className = "studio-pdf-focus-title-group";
6237
+
6238
+ const closeBtn = document.createElement("button");
6239
+ closeBtn.type = "button";
6240
+ closeBtn.className = "studio-pdf-focus-btn studio-pdf-focus-close";
6241
+ closeBtn.title = "Exit image focus view.";
6242
+ closeBtn.setAttribute("aria-label", "Exit image focus view");
6243
+ closeBtn.appendChild(makeStudioUiRefreshIcon("focus-exit"));
6244
+ closeBtn.addEventListener("click", () => closeStudioImageFocusViewer());
6245
+ titleGroup.appendChild(closeBtn);
6246
+
6247
+ const titleEl = document.createElement("div");
6248
+ titleEl.id = "studioImageFocusTitle";
6249
+ titleEl.className = "studio-pdf-focus-title";
6250
+ titleEl.textContent = "Image preview";
6251
+ titleGroup.appendChild(titleEl);
6252
+ header.appendChild(titleGroup);
6253
+
6254
+ const actions = document.createElement("div");
6255
+ actions.className = "studio-pdf-focus-actions studio-image-focus-actions";
6256
+
6257
+ const openLink = document.createElement("a");
6258
+ openLink.className = "studio-pdf-focus-link";
6259
+ openLink.target = "_blank";
6260
+ openLink.rel = "noopener noreferrer";
6261
+ openLink.textContent = "Open image";
6262
+ actions.appendChild(openLink);
6263
+
6264
+ appendStudioImageFocusTextButton(actions, "Fit", "Fit the image to the viewer.", () => setStudioImageFocusZoom("fit", 1));
6265
+ appendStudioImageFocusTextButton(actions, "100%", "Show the image at its natural pixel size.", () => setStudioImageFocusZoom("custom", 1));
6266
+ appendStudioImageFocusTextButton(actions, "−", "Zoom out.", () => zoomStudioImageFocus(1 / 1.25));
6267
+ const zoomLabel = document.createElement("span");
6268
+ zoomLabel.className = "studio-image-focus-zoom-label";
6269
+ zoomLabel.textContent = "100%";
6270
+ actions.appendChild(zoomLabel);
6271
+ appendStudioImageFocusTextButton(actions, "+", "Zoom in.", () => zoomStudioImageFocus(1.25));
6272
+ appendStudioImageFocusTextButton(actions, "Reset", "Reset image zoom to fit.", () => setStudioImageFocusZoom("fit", 1));
6273
+
6274
+ const fullscreenBtn = document.createElement("button");
6275
+ fullscreenBtn.type = "button";
6276
+ fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
6277
+ fullscreenBtn.addEventListener("click", () => {
6278
+ void toggleStudioImageFocusFullscreen();
6279
+ });
6280
+ actions.appendChild(fullscreenBtn);
6281
+
6282
+ header.appendChild(actions);
6283
+ dialog.appendChild(header);
6284
+
6285
+ const slot = document.createElement("div");
6286
+ slot.className = "studio-image-focus-slot is-fit";
6287
+ const img = document.createElement("img");
6288
+ img.className = "studio-image-focus-img";
6289
+ img.alt = "Image preview";
6290
+ img.addEventListener("load", syncStudioImageFocusZoom);
6291
+ slot.addEventListener("wheel", handleStudioImageFocusWheel, { passive: false });
6292
+ slot.appendChild(img);
6293
+ dialog.appendChild(slot);
6294
+
6295
+ overlay.appendChild(dialog);
6296
+ overlay.addEventListener("click", (event) => {
6297
+ if (event.target === overlay) closeStudioImageFocusViewer();
6298
+ });
6299
+ document.addEventListener("fullscreenchange", syncStudioImageFocusFullscreenButton);
6300
+
6301
+ document.body.appendChild(overlay);
6302
+ studioImageFocusOverlayEl = overlay;
6303
+ studioImageFocusDialogEl = dialog;
6304
+ studioImageFocusSlotEl = slot;
6305
+ studioImageFocusImgEl = img;
6306
+ studioImageFocusTitleEl = titleEl;
6307
+ studioImageFocusOpenLinkEl = openLink;
6308
+ studioImageFocusFullscreenBtn = fullscreenBtn;
6309
+ studioImageFocusCloseBtn = closeBtn;
6310
+ studioImageFocusZoomLabelEl = zoomLabel;
6311
+ syncStudioImageFocusFullscreenButton();
6312
+ return overlay;
6313
+ }
6314
+
6315
+ function openStudioImageFocusViewer(src, title) {
6316
+ const imageSrc = String(src || "").trim();
6317
+ if (!isStudioImageFocusSrcAllowed(imageSrc)) return false;
6318
+ ensureStudioImageFocusViewer();
6319
+ studioImageFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
6320
+ const label = String(title || "Image preview").trim() || "Image preview";
6321
+ if (studioImageFocusTitleEl) studioImageFocusTitleEl.textContent = label;
6322
+ if (studioImageFocusOpenLinkEl) studioImageFocusOpenLinkEl.href = imageSrc;
6323
+ if (studioImageFocusImgEl) {
6324
+ studioImageFocusImgEl.alt = label;
6325
+ studioImageFocusImgEl.src = imageSrc;
6326
+ }
6327
+ studioImageFocusZoomMode = "fit";
6328
+ studioImageFocusZoom = 1;
6329
+ syncStudioImageFocusZoom();
6330
+ if (document.body) document.body.classList.add("studio-image-focus-open");
6331
+ if (studioImageFocusOverlayEl) studioImageFocusOverlayEl.hidden = false;
6332
+ syncStudioImageFocusFullscreenButton();
6333
+ closeStudioUiRefreshMenus();
6334
+ closeExportPreviewMenu();
6335
+ closePreviewLinkMenu();
6336
+ window.setTimeout(() => {
6337
+ if (studioImageFocusCloseBtn && typeof studioImageFocusCloseBtn.focus === "function") {
6338
+ studioImageFocusCloseBtn.focus();
6339
+ }
6340
+ }, 0);
6341
+ return true;
6342
+ }
6343
+
6344
+ function closeStudioImageFocusViewer() {
6345
+ if (!isStudioImageFocusOpen()) return false;
6346
+ if (document.fullscreenElement && studioImageFocusDialogEl && studioImageFocusDialogEl.contains(document.fullscreenElement)) {
6347
+ try {
6348
+ const exitResult = document.exitFullscreen && document.exitFullscreen();
6349
+ if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
6350
+ } catch {}
6351
+ }
6352
+ if (studioImageFocusOverlayEl) studioImageFocusOverlayEl.hidden = true;
6353
+ if (studioImageFocusImgEl) studioImageFocusImgEl.removeAttribute("src");
6354
+ if (studioImageFocusOpenLinkEl) studioImageFocusOpenLinkEl.removeAttribute("href");
6355
+ if (document.body) document.body.classList.remove("studio-image-focus-open");
6356
+ syncStudioImageFocusFullscreenButton();
6357
+ const focusTarget = studioImageFocusLastFocusedEl;
6358
+ studioImageFocusLastFocusedEl = null;
6359
+ if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
6360
+ window.setTimeout(() => focusTarget.focus(), 0);
6361
+ }
6362
+ return true;
6363
+ }
6364
+
6365
+ function getPreviewImageElementTitle(imageEl) {
6366
+ if (!imageEl) return "Image preview";
6367
+ const alt = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("alt") || "").trim() : "";
6368
+ const title = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("title") || "").trim() : "";
6369
+ const src = typeof imageEl.getAttribute === "function" ? String(imageEl.getAttribute("src") || "").trim() : "";
6370
+ const srcLabel = /^data:image\//i.test(src) ? "" : (src.length > 120 ? src.slice(0, 117) + "…" : src);
6371
+ return alt || title || srcLabel || "Image preview";
6372
+ }
6373
+
6374
+ function openPreviewImageElementInFocus(imageEl) {
6375
+ if (!imageEl) return false;
6376
+ const src = String(imageEl.currentSrc || imageEl.src || imageEl.getAttribute("src") || "").trim();
6377
+ if (!src) return false;
6378
+ return openStudioImageFocusViewer(src, getPreviewImageElementTitle(imageEl));
6379
+ }
6380
+
6381
+ function decoratePreviewImages(targetEl) {
6382
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
6383
+ const images = Array.from(targetEl.querySelectorAll("img[src]"));
6384
+ images.forEach((imageEl) => {
6385
+ if (!(imageEl instanceof HTMLImageElement)) return;
6386
+ if (imageEl.dataset && imageEl.dataset.studioImageFocusDecorated === "1") return;
6387
+ if (imageEl.closest && imageEl.closest("a[href], button, .studio-html-artifact-shell, .studio-pdf-card")) return;
6388
+ if (!isStudioImageFocusSrcAllowed(imageEl.currentSrc || imageEl.src || imageEl.getAttribute("src") || "")) return;
6389
+ imageEl.classList.add("studio-image-focus-target");
6390
+ imageEl.tabIndex = imageEl.tabIndex >= 0 ? imageEl.tabIndex : 0;
6391
+ imageEl.setAttribute("role", "button");
6392
+ imageEl.setAttribute("aria-label", "Open image focus viewer");
6393
+ if (imageEl.dataset) imageEl.dataset.studioImageFocusDecorated = "1";
6394
+ imageEl.addEventListener("click", (event) => {
6395
+ event.preventDefault();
6396
+ event.stopPropagation();
6397
+ if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
6398
+ });
6399
+ imageEl.addEventListener("keydown", (event) => {
6400
+ if (event.key !== "Enter" && event.key !== " ") return;
6401
+ event.preventDefault();
6402
+ if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
6403
+ });
6404
+ });
6405
+ }
6406
+
5749
6407
  function createStudioPdfCard(block, useEditorResourceContext) {
5750
6408
  const options = block && block.options ? block.options : {};
5751
6409
  const path = String(options.path || "").trim();
@@ -6977,15 +7635,16 @@
6977
7635
  const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
6978
7636
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
6979
7637
  const isEditorPreview = rightView === "editor-preview";
6980
- const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
7638
+ const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
7639
+ const editorPdfLanguage = isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "";
6981
7640
  const isLatex = isEditorPreview
6982
7641
  ? editorPdfLanguage === "latex"
6983
7642
  : /\\documentclass\b|\\begin\{document\}/.test(markdown);
6984
- let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf");
7643
+ let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.pdf"));
6985
7644
  if (sourcePath) {
6986
7645
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
6987
7646
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
6988
- filenameHint = stem + "-preview.pdf";
7647
+ filenameHint = stem + ".studio.pdf";
6989
7648
  }
6990
7649
 
6991
7650
  previewExportInProgress = true;
@@ -7033,6 +7692,8 @@
7033
7692
 
7034
7693
  const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
7035
7694
  const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
7695
+ const writeError = typeof payload.writeError === "string" ? payload.writeError.trim() : "";
7696
+ const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
7036
7697
  const openedExternal = payload.openedExternal === true;
7037
7698
  let downloadName = typeof payload.filename === "string" && payload.filename.trim()
7038
7699
  ? payload.filename.trim()
@@ -7042,10 +7703,12 @@
7042
7703
  }
7043
7704
 
7044
7705
  if (openedExternal) {
7045
- if (exportWarning) {
7706
+ if (writeError) {
7707
+ setStatus("Opened PDF in default viewer, but could not write project file: " + writeError, "warning");
7708
+ } else if (exportWarning) {
7046
7709
  setStatus("Opened PDF in default viewer with warning: " + exportWarning, "warning");
7047
7710
  } else {
7048
- setStatus("Opened PDF in default viewer: " + downloadName, "success");
7711
+ setStatus("Opened PDF in default viewer: " + (exportPath || downloadName), "success");
7049
7712
  }
7050
7713
  return;
7051
7714
  }
@@ -7064,10 +7727,12 @@
7064
7727
  } else {
7065
7728
  setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
7066
7729
  }
7730
+ } else if (writeError) {
7731
+ setStatus("Exported PDF to browser fallback; could not write project file: " + writeError, "warning");
7067
7732
  } else if (exportWarning) {
7068
- setStatus("Exported PDF with warning: " + exportWarning, "warning");
7733
+ setStatus("Exported PDF with warning" + (exportPath ? " to " + exportPath : ": " + exportWarning), "warning");
7069
7734
  } else {
7070
- setStatus("Exported PDF: " + downloadName, "success");
7735
+ setStatus("Exported PDF: " + (exportPath || downloadName), "success");
7071
7736
  }
7072
7737
  return;
7073
7738
  }
@@ -7143,16 +7808,17 @@
7143
7808
  const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
7144
7809
  const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
7145
7810
  const isEditorPreview = rightView === "editor-preview";
7146
- const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
7811
+ const editorIsDelimitedPreview = isEditorPreview && Boolean(getDelimitedTextPreviewConfig(editorLanguage || ""));
7812
+ const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? (editorIsDelimitedPreview ? "markdown" : normalizeFenceLanguage(editorLanguage || "")) : "");
7147
7813
  const isLatex = htmlArtifactSource ? false : (isEditorPreview
7148
7814
  ? editorHtmlLanguage === "latex"
7149
7815
  : /\\documentclass\b|\\begin\{document\}/.test(markdown));
7150
- let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
7816
+ let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html"));
7151
7817
  let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
7152
7818
  if (sourcePath) {
7153
7819
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
7154
7820
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
7155
- filenameHint = stem + "-preview.html";
7821
+ filenameHint = stem + ".studio.html";
7156
7822
  titleHint = stem + " preview";
7157
7823
  }
7158
7824
 
@@ -7202,6 +7868,8 @@
7202
7868
 
7203
7869
  const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
7204
7870
  const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
7871
+ const writeError = typeof payload.writeError === "string" ? payload.writeError.trim() : "";
7872
+ const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
7205
7873
  const openedExternal = payload.openedExternal === true;
7206
7874
  let downloadName = typeof payload.filename === "string" && payload.filename.trim()
7207
7875
  ? payload.filename.trim()
@@ -7211,10 +7879,12 @@
7211
7879
  }
7212
7880
 
7213
7881
  if (openedExternal) {
7214
- if (exportWarning) {
7882
+ if (writeError) {
7883
+ setStatus("Opened HTML in default browser, but could not write project file: " + writeError, "warning");
7884
+ } else if (exportWarning) {
7215
7885
  setStatus("Opened HTML in default browser with warning: " + exportWarning, "warning");
7216
7886
  } else {
7217
- setStatus("Opened HTML in default browser: " + downloadName, "success");
7887
+ setStatus("Opened HTML in default browser: " + (exportPath || downloadName), "success");
7218
7888
  }
7219
7889
  return;
7220
7890
  }
@@ -7233,10 +7903,12 @@
7233
7903
  } else {
7234
7904
  setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
7235
7905
  }
7906
+ } else if (writeError) {
7907
+ setStatus("Exported HTML to browser fallback; could not write project file: " + writeError, "warning");
7236
7908
  } else if (exportWarning) {
7237
- setStatus("Exported HTML with warning: " + exportWarning, "warning");
7909
+ setStatus("Exported HTML with warning" + (exportPath ? " to " + exportPath : ": " + exportWarning), "warning");
7238
7910
  } else {
7239
- setStatus("Exported HTML: " + downloadName, "success");
7911
+ setStatus("Exported HTML: " + (exportPath || downloadName), "success");
7240
7912
  }
7241
7913
  return;
7242
7914
  }
@@ -7528,6 +8200,7 @@
7528
8200
  decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
7529
8201
  }
7530
8202
  decorateCopyablePreviewBlocks(targetEl);
8203
+ decoratePreviewImages(targetEl);
7531
8204
 
7532
8205
  // Warn if relative images are present but unlikely to resolve (non-file-backed content)
7533
8206
  if (!sourceState.path && !getCurrentResourceDirValue()) {
@@ -7567,6 +8240,9 @@
7567
8240
  renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
7568
8241
  return;
7569
8242
  }
8243
+ if (renderDelimitedTextPreview(sourcePreviewEl, text, "source")) {
8244
+ return;
8245
+ }
7570
8246
  if (supportsCodePreviewCommentsForCurrentEditor()) {
7571
8247
  renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
7572
8248
  return;
@@ -8115,6 +8791,7 @@
8115
8791
  const previousScrollTop = critiqueViewEl.scrollTop;
8116
8792
  finishPreviewRender(critiqueViewEl);
8117
8793
  critiqueViewEl.innerHTML = buildTracePanelHtml();
8794
+ decoratePreviewImages(critiqueViewEl);
8118
8795
  critiqueViewEl.classList.remove("response-scroll-resetting");
8119
8796
  if (shouldStick) {
8120
8797
  critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
@@ -8171,6 +8848,7 @@
8171
8848
  function getFileBrowserKindLabel(entry) {
8172
8849
  if (!entry || entry.type === "directory") return "folder";
8173
8850
  if (entry.kind === "text") return "document";
8851
+ if (entry.kind === "office") return "document";
8174
8852
  if (entry.kind === "pdf") return "PDF";
8175
8853
  if (entry.kind === "image") return "image";
8176
8854
  return entry.extension ? entry.extension.replace(/^\./, "") : "file";
@@ -8187,18 +8865,21 @@
8187
8865
  ? entries.map((entry) => {
8188
8866
  const type = entry.type === "directory" ? "directory" : "file";
8189
8867
  const kind = entry.kind || (type === "directory" ? "directory" : "other");
8190
- const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" ? "📝" : "📦")));
8868
+ const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
8191
8869
  const metaParts = [];
8192
8870
  metaParts.push(getFileBrowserKindLabel(entry));
8193
8871
  if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
8194
8872
  const time = formatFileBrowserTime(entry.mtimeMs);
8195
8873
  if (time) metaParts.push(time);
8196
- const textActions = kind === "text"
8197
- ? "<button type='button' data-files-action='open-new' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
8874
+ const newTabAction = kind === "text" || kind === "office"
8875
+ ? "open-new"
8876
+ : ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
8877
+ const textActions = newTabAction
8878
+ ? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
8198
8879
  : "";
8199
8880
  const openTitle = type === "directory"
8200
8881
  ? "Open folder"
8201
- : (kind === "text" ? "Open in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file")));
8882
+ : (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"))));
8202
8883
  return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
8203
8884
  + "<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) + "'>"
8204
8885
  + "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
@@ -8223,6 +8904,8 @@
8223
8904
  + "<div class='files-toolbar-actions'>"
8224
8905
  + "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
8225
8906
  + "<button type='button' data-files-action='refresh'>Refresh</button>"
8907
+ + (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
8908
+ + (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
8226
8909
  + (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
8227
8910
  + "</div>"
8228
8911
  + "</div>"
@@ -8317,7 +9000,7 @@
8317
9000
 
8318
9001
  async function openFileBrowserEntry(path, kind) {
8319
9002
  const context = getFileBrowserLocalLinkContext();
8320
- if (kind === "text") {
9003
+ if (kind === "text" || kind === "office") {
8321
9004
  await openPreviewDocumentHere(path, context);
8322
9005
  return;
8323
9006
  }
@@ -8332,6 +9015,19 @@
8332
9015
  setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
8333
9016
  }
8334
9017
 
9018
+ function setFileBrowserCurrentDirectoryAsWorkingDir(path) {
9019
+ const nextDir = normalizeStudioResourceDirValue(path || fileBrowserState.currentDir || "");
9020
+ if (!nextDir) {
9021
+ setStatus("No current folder to use as working directory.", "warning");
9022
+ return;
9023
+ }
9024
+ if (resourceDirInput) resourceDirInput.value = nextDir;
9025
+ applyResourceDir();
9026
+ fileBrowserState = { ...fileBrowserState, contextKey: "" };
9027
+ if (rightView === "files") renderFilesView();
9028
+ setStatus("Working dir set to current folder.", "success");
9029
+ }
9030
+
8335
9031
  async function handleFilesPaneClick(event) {
8336
9032
  if (rightView !== "files") return;
8337
9033
  const target = event.target;
@@ -8362,11 +9058,19 @@
8362
9058
  await openPreviewDocumentInNewEditor(path, null, getFileBrowserLocalLinkContext());
8363
9059
  return;
8364
9060
  }
8365
- if (action === "copy-path" || action === "copy-root") {
9061
+ if (action === "open-preview-new") {
9062
+ await openPreviewResourceInNewEditor(path, null, getFileBrowserLocalLinkContext());
9063
+ return;
9064
+ }
9065
+ if (action === "copy-path" || action === "copy-root" || action === "copy-current") {
8366
9066
  const ok = await writeTextToClipboard(path);
8367
9067
  setStatus(ok ? "Copied path." : "Clipboard write failed.", ok ? "success" : "warning");
8368
9068
  return;
8369
9069
  }
9070
+ if (action === "use-working-dir") {
9071
+ setFileBrowserCurrentDirectoryAsWorkingDir(path);
9072
+ return;
9073
+ }
8370
9074
  if (action === "reveal") {
8371
9075
  await revealPreviewLocalLink(path, getFileBrowserLocalLinkContext());
8372
9076
  }
@@ -8403,6 +9107,9 @@
8403
9107
  renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
8404
9108
  return;
8405
9109
  }
9110
+ if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response")) {
9111
+ return;
9112
+ }
8406
9113
  if (supportsCodePreviewCommentsForCurrentEditor()) {
8407
9114
  renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
8408
9115
  return;
@@ -8667,9 +9374,14 @@
8667
9374
  syncRunAndCritiqueButtons();
8668
9375
  copyDraftBtn.disabled = uiBusy;
8669
9376
  if (suggestCompletionBtn) {
8670
- suggestCompletionBtn.disabled = uiBusy || completionSuggestionInFlight || wsState !== "Ready" || !String(sourceTextEl.value || "").trim();
8671
- suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Suggesting…" : "Suggest";
8672
- }
9377
+ suggestCompletionBtn.disabled = wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
9378
+ suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Stop" : "Suggest";
9379
+ suggestCompletionBtn.title = completionSuggestionInFlight
9380
+ ? "Stop the current suggestion request."
9381
+ : "Ask the current model for a short completion at the editor cursor. Shortcut: Option/Alt+Tab where available, or Cmd/Ctrl+Shift+Space from the editor.";
9382
+ }
9383
+ if (suggestCompletionOptionsBtn) suggestCompletionOptionsBtn.disabled = uiBusy || completionSuggestionInFlight;
9384
+ syncCompletionSuggestionContextUi();
8673
9385
  if (openCompanionBtn) openCompanionBtn.disabled = uiBusy || wsState !== "Ready";
8674
9386
  if (highlightSelect) highlightSelect.disabled = uiBusy;
8675
9387
  if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
@@ -8857,7 +9569,10 @@
8857
9569
  resourceDirInput.value = nextResourceDir;
8858
9570
  updateSourceBadge();
8859
9571
  }
8860
- if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
9572
+ const detectedPersistedPathLanguage = detectLanguageFromName(nextSourceState.path || nextSourceState.label || "");
9573
+ if (getDelimitedTextPreviewConfig(detectedPersistedPathLanguage)) {
9574
+ setEditorLanguage(detectedPersistedPathLanguage);
9575
+ } else if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
8861
9576
  setEditorLanguage(state.editorLanguage.trim());
8862
9577
  }
8863
9578
  editorView = state.editorView === "preview" ? "preview" : "markdown";
@@ -9003,6 +9718,69 @@
9003
9718
  }
9004
9719
  }
9005
9720
 
9721
+ function readCompletionSuggestionContextMode() {
9722
+ try {
9723
+ const stored = window.localStorage ? String(window.localStorage.getItem(COMPLETION_CONTEXT_STORAGE_KEY) || "") : "";
9724
+ return stored === "session" ? "session" : "cursor";
9725
+ } catch {
9726
+ return "cursor";
9727
+ }
9728
+ }
9729
+
9730
+ function setCompletionSuggestionContextMode(mode) {
9731
+ completionSuggestionContextMode = mode === "session" ? "session" : "cursor";
9732
+ if (completionContextSelect) completionContextSelect.value = completionSuggestionContextMode;
9733
+ try {
9734
+ if (window.localStorage) window.localStorage.setItem(COMPLETION_CONTEXT_STORAGE_KEY, completionSuggestionContextMode);
9735
+ } catch {}
9736
+ setStatus(completionSuggestionContextMode === "session"
9737
+ ? "Suggestions will include the latest response as context."
9738
+ : "Suggestions will use cursor-local editor context only.");
9739
+ }
9740
+
9741
+ function syncCompletionSuggestionContextUi() {
9742
+ if (completionContextSelect) completionContextSelect.value = completionSuggestionContextMode;
9743
+ if (suggestCompletionOptionsBtn) {
9744
+ suggestCompletionOptionsBtn.textContent = "Source & context";
9745
+ suggestCompletionOptionsBtn.title = completionSuggestionContextMode === "session"
9746
+ ? "Document source, working directory, status, and suggestion context. Suggestions include editor plus latest response."
9747
+ : "Document source, working directory, status, and suggestion context. Suggestions use editor-only context.";
9748
+ suggestCompletionOptionsBtn.setAttribute("aria-label", suggestCompletionOptionsBtn.title);
9749
+ }
9750
+ document.querySelectorAll("[data-completion-context-mode]").forEach((button) => {
9751
+ if (!(button instanceof HTMLElement)) return;
9752
+ const mode = button.getAttribute("data-completion-context-mode") === "session" ? "session" : "cursor";
9753
+ const selected = mode === completionSuggestionContextMode;
9754
+ button.classList.toggle("is-selected", selected);
9755
+ button.setAttribute("aria-pressed", selected ? "true" : "false");
9756
+ button.textContent = (selected ? "✓ " : " ") + (mode === "session" ? "Editor + latest response" : "Editor only");
9757
+ });
9758
+ }
9759
+
9760
+ function trimCompletionContextText(text) {
9761
+ const value = String(text || "").trim();
9762
+ if (value.length <= COMPLETION_CONTEXT_MAX_CHARS) return value;
9763
+ return value.slice(value.length - COMPLETION_CONTEXT_MAX_CHARS);
9764
+ }
9765
+
9766
+ function getCompletionSuggestionContextText() {
9767
+ if (completionSuggestionContextMode !== "session") return "";
9768
+ const selected = getSelectedHistoryItem ? getSelectedHistoryItem() : null;
9769
+ const responseText = selected && typeof selected.markdown === "string" && selected.markdown.trim()
9770
+ ? selected.markdown
9771
+ : latestResponseMarkdown;
9772
+ const parts = [];
9773
+ if (selected && typeof selected.promptTriggerText === "string" && selected.promptTriggerText.trim()) {
9774
+ parts.push("Latest request/steering:\n" + trimCompletionContextText(selected.promptTriggerText));
9775
+ } else if (selected && typeof selected.prompt === "string" && selected.prompt.trim()) {
9776
+ parts.push("Latest prompt:\n" + trimCompletionContextText(selected.prompt));
9777
+ }
9778
+ if (String(responseText || "").trim()) {
9779
+ parts.push("Latest response:\n" + trimCompletionContextText(responseText));
9780
+ }
9781
+ return trimCompletionContextText(parts.join("\n\n---\n\n"));
9782
+ }
9783
+
9006
9784
  function hideCompletionSuggestion() {
9007
9785
  completionSuggestionState = null;
9008
9786
  if (completionSuggestionTextEl) completionSuggestionTextEl.textContent = "";
@@ -9073,10 +9851,29 @@
9073
9851
  || Boolean(completionSuggestionPanelEl && activeEl instanceof Element && completionSuggestionPanelEl.contains(activeEl));
9074
9852
  }
9075
9853
 
9854
+ function cancelCompletionSuggestion() {
9855
+ if (!completionSuggestionInFlight || !completionSuggestionRequestId) {
9856
+ setStatus("No suggestion request is running.", "warning");
9857
+ return;
9858
+ }
9859
+ setStatus("Stopping suggestion…", "warning");
9860
+ const sent = sendMessage({
9861
+ type: "completion_suggestion_cancel_request",
9862
+ requestId: completionSuggestionRequestId,
9863
+ });
9864
+ if (!sent) {
9865
+ completionSuggestionInFlight = false;
9866
+ completionSuggestionRequestId = null;
9867
+ completionSuggestionPendingSnapshot = null;
9868
+ completionSuggestionRefocusEditorOnResult = false;
9869
+ syncActionButtons();
9870
+ }
9871
+ }
9872
+
9076
9873
  function requestCompletionSuggestion() {
9077
9874
  if (isEditorOnlyMode && !sourceTextEl) return;
9078
9875
  if (completionSuggestionInFlight) {
9079
- setStatus("Suggestion request already in progress.", "warning");
9876
+ cancelCompletionSuggestion();
9080
9877
  return;
9081
9878
  }
9082
9879
  const text = String(sourceTextEl.value || "");
@@ -9086,6 +9883,7 @@
9086
9883
  }
9087
9884
  const selectionStart = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : text.length;
9088
9885
  const selectionEnd = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : selectionStart;
9886
+ const contextText = getCompletionSuggestionContextText();
9089
9887
  const requestId = makeRequestId();
9090
9888
  completionSuggestionInFlight = true;
9091
9889
  completionSuggestionRequestId = requestId;
@@ -9103,6 +9901,8 @@
9103
9901
  language: editorLanguage || "",
9104
9902
  label: sourceState && sourceState.label ? sourceState.label : "Studio editor",
9105
9903
  path: sourceState && sourceState.path ? sourceState.path : undefined,
9904
+ contextMode: completionSuggestionContextMode,
9905
+ contextText: contextText || undefined,
9106
9906
  });
9107
9907
  if (!sent) {
9108
9908
  completionSuggestionInFlight = false;
@@ -9675,6 +10475,7 @@
9675
10475
  ".diff", ".patch",
9676
10476
  ]);
9677
10477
  const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
10478
+ const PREVIEW_LOCAL_OFFICE_LINK_EXTENSIONS = new Set([".docx", ".odt"]);
9678
10479
  const PREVIEW_LOCAL_TEXT_LINK_FILENAMES = new Set([
9679
10480
  ".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
9680
10481
  ".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
@@ -9739,6 +10540,7 @@
9739
10540
  if (ext === ".pdf") return "pdf";
9740
10541
  if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext) || PREVIEW_LOCAL_TEXT_LINK_FILENAMES.has(name)) return "text";
9741
10542
  if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
10543
+ if (PREVIEW_LOCAL_OFFICE_LINK_EXTENSIONS.has(ext)) return "office";
9742
10544
  return "other";
9743
10545
  }
9744
10546
 
@@ -9832,11 +10634,16 @@
9832
10634
  };
9833
10635
  if (kind === "pdf") {
9834
10636
  appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
10637
+ appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
9835
10638
  } else if (kind === "text") {
9836
10639
  appendPreviewLinkMenuButton(menu, "Open in new editor", "open-new");
9837
10640
  appendPreviewLinkMenuButton(menu, "Open here", "open-here");
10641
+ } else if (kind === "office") {
10642
+ appendPreviewLinkMenuButton(menu, "Convert in new editor", "open-new");
10643
+ appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
9838
10644
  } else if (kind === "image") {
9839
10645
  appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
10646
+ appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
9840
10647
  }
9841
10648
  appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
9842
10649
  appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
@@ -9873,40 +10680,18 @@
9873
10680
  }
9874
10681
 
9875
10682
  async function openPreviewImageLink(href, title, contextOverride, pendingWindow) {
9876
- const popup = pendingWindow || window.open("", "_blank");
9877
- try {
9878
- if (popup && popup.document && popup.document.body) {
9879
- popup.document.title = "Opening image…";
9880
- popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening image…</p>";
9881
- }
9882
- } catch {}
9883
- try {
9884
- const payload = await fetchStudioJson("/html-preview-resource", {
9885
- query: getPreviewLinkResourceQuery(href, contextOverride),
9886
- });
9887
- const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
9888
- if (!dataUrl) throw new Error("Studio did not return image data.");
9889
- const safeTitle = escapeHtml(String(title || href || "Local image"));
9890
- const safeSrc = escapeHtml(dataUrl);
9891
- const html = "<!doctype html><html><head><meta charset='utf-8'><title>" + safeTitle + "</title>"
9892
- + "<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>"
9893
- + "</head><body><header>" + safeTitle + "</header><img src='" + safeSrc + "' alt='" + safeTitle + "'></body></html>";
9894
- if (popup && !popup.closed && popup.document) {
9895
- popup.document.open();
9896
- popup.document.write(html);
9897
- popup.document.close();
9898
- setStatus("Opened local image preview.", "success");
9899
- return;
9900
- }
9901
- const opened = window.open(dataUrl, "_blank");
9902
- if (!opened) throw new Error("Popup blocked while opening image preview.");
9903
- setStatus("Opened local image preview.", "success");
9904
- } catch (error) {
9905
- if (popup && !popup.closed) {
9906
- try { popup.close(); } catch {}
9907
- }
9908
- throw error;
10683
+ if (pendingWindow && !pendingWindow.closed) {
10684
+ try { pendingWindow.close(); } catch {}
9909
10685
  }
10686
+ const payload = await fetchStudioJson("/html-preview-resource", {
10687
+ query: getPreviewLinkResourceQuery(href, contextOverride),
10688
+ });
10689
+ const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
10690
+ if (!dataUrl) throw new Error("Studio did not return image data.");
10691
+ if (!openStudioImageFocusViewer(dataUrl, title || href || "Local image")) {
10692
+ throw new Error("Could not open image focus view.");
10693
+ }
10694
+ setStatus("Opened local image preview.", "success");
9910
10695
  }
9911
10696
 
9912
10697
  function editorHasPotentialUnsavedContent() {
@@ -9916,7 +10701,33 @@
9916
10701
  return true;
9917
10702
  }
9918
10703
 
10704
+ function getPreviewOfficeConversionLabel(href) {
10705
+ const cleanPath = stripPreviewLocalLinkUrlSuffix(href || "");
10706
+ const rawName = cleanPath.split(/[\\/]/).pop() || cleanPath || "this document";
10707
+ try {
10708
+ return decodeURIComponent(rawName) || rawName;
10709
+ } catch {
10710
+ return rawName;
10711
+ }
10712
+ }
10713
+
10714
+ function confirmPreviewOfficeConversion(href, destination) {
10715
+ if (getPreviewLocalLinkKind(href) !== "office") return true;
10716
+ const label = getPreviewOfficeConversionLabel(href);
10717
+ const target = destination === "here"
10718
+ ? "replace the current editor contents with an editable Markdown copy"
10719
+ : "open an editable Markdown copy in a new Studio tab";
10720
+ const confirmed = window.confirm(
10721
+ "Convert " + label + " to Markdown?\n\n"
10722
+ + "Studio will use Pandoc to " + target + ". Some layout or formatting may change. "
10723
+ + "The original DOCX/ODT file will not be overwritten, and edits will not round-trip back to it."
10724
+ );
10725
+ if (!confirmed) setStatus("Document conversion cancelled.", "warning");
10726
+ return confirmed;
10727
+ }
10728
+
9919
10729
  async function openPreviewDocumentHere(href, contextOverride) {
10730
+ if (!confirmPreviewOfficeConversion(href, "here")) return;
9920
10731
  if (editorHasPotentialUnsavedContent()) {
9921
10732
  const confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
9922
10733
  if (!confirmed) return;
@@ -9926,18 +10737,29 @@
9926
10737
  const path = typeof payload.path === "string" ? payload.path : "";
9927
10738
  const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
9928
10739
  const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
10740
+ const converted = payload && payload.converted === true;
9929
10741
  if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
9930
10742
  setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
9931
- setSourceState({ source: "file", label, path });
9932
- markFileBackedBaseline(payload.text);
9933
- const detected = detectLanguageFromName(path || label);
10743
+ if (converted) {
10744
+ setSourceState({ source: "blank", label, path: null });
10745
+ } else {
10746
+ setSourceState({ source: "file", label, path });
10747
+ markFileBackedBaseline(payload.text);
10748
+ }
10749
+ const detected = converted ? "markdown" : detectLanguageFromName(path || label);
9934
10750
  if (detected) setEditorLanguage(detected);
9935
10751
  setEditorView("markdown");
9936
10752
  setActivePane("left");
9937
- setStatus("Opened linked file in editor: " + label, "success");
10753
+ setStatus(converted ? ("Converted document into editor: " + label) : ("Opened linked file in editor: " + label), "success");
9938
10754
  }
9939
10755
 
9940
10756
  async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
10757
+ if (!confirmPreviewOfficeConversion(href, "new")) {
10758
+ if (pendingWindow && !pendingWindow.closed) {
10759
+ try { pendingWindow.close(); } catch {}
10760
+ }
10761
+ return;
10762
+ }
9941
10763
  const popup = pendingWindow || window.open("", "_blank");
9942
10764
  try {
9943
10765
  if (popup && popup.document && popup.document.body) {
@@ -9955,12 +10777,44 @@
9955
10777
  try {
9956
10778
  popup.opener = null;
9957
10779
  popup.location.href = targetUrl;
9958
- setStatus("Opening linked file in a new editor.", "success");
10780
+ setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
10781
+ return;
10782
+ } catch {}
10783
+ }
10784
+ window.open(targetUrl, "_blank", "noopener");
10785
+ setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
10786
+ } catch (error) {
10787
+ if (popup && !popup.closed) {
10788
+ try { popup.close(); } catch {}
10789
+ }
10790
+ throw error;
10791
+ }
10792
+ }
10793
+
10794
+ async function openPreviewResourceInNewEditor(href, pendingWindow, contextOverride) {
10795
+ const popup = pendingWindow || window.open("", "_blank");
10796
+ try {
10797
+ if (popup && popup.document && popup.document.body) {
10798
+ popup.document.title = "Opening preview…";
10799
+ popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening preview…</p>";
10800
+ }
10801
+ } catch {}
10802
+ try {
10803
+ const payload = await fetchPreviewLocalLink("preview-url", href, contextOverride);
10804
+ const targetUrl = payload && typeof payload.relativeUrl === "string"
10805
+ ? new URL(payload.relativeUrl, window.location.href).href
10806
+ : (payload && typeof payload.url === "string" ? payload.url : "");
10807
+ if (!targetUrl) throw new Error("Studio did not return a preview URL.");
10808
+ if (popup && !popup.closed) {
10809
+ try {
10810
+ popup.opener = null;
10811
+ popup.location.href = targetUrl;
10812
+ setStatus("Opening preview in a new Studio tab.", "success");
9959
10813
  return;
9960
10814
  } catch {}
9961
10815
  }
9962
10816
  window.open(targetUrl, "_blank", "noopener");
9963
- setStatus("Opening linked file in a new editor.", "success");
10817
+ setStatus("Opening preview in a new Studio tab.", "success");
9964
10818
  } catch (error) {
9965
10819
  if (popup && !popup.closed) {
9966
10820
  try { popup.close(); } catch {}
@@ -9999,6 +10853,10 @@
9999
10853
  await openPreviewDocumentInNewEditor(href, null, context);
10000
10854
  return;
10001
10855
  }
10856
+ if (action === "open-preview-new") {
10857
+ await openPreviewResourceInNewEditor(href, null, context);
10858
+ return;
10859
+ }
10002
10860
  if (action === "open-here") {
10003
10861
  await openPreviewDocumentHere(href, context);
10004
10862
  return;
@@ -10033,14 +10891,13 @@
10033
10891
  return;
10034
10892
  }
10035
10893
  if (kind === "image") {
10036
- const pendingWindow = window.open("", "_blank");
10037
- void openPreviewImageLink(href, title, null, pendingWindow).catch((error) => {
10894
+ void openPreviewImageLink(href, title).catch((error) => {
10038
10895
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
10039
10896
  });
10040
10897
  return;
10041
10898
  }
10042
- if (kind === "text") {
10043
- const pendingWindow = window.open("", "_blank");
10899
+ if (kind === "text" || kind === "office") {
10900
+ const pendingWindow = kind === "office" ? null : window.open("", "_blank");
10044
10901
  void openPreviewDocumentInNewEditor(href, pendingWindow).catch((error) => {
10045
10902
  setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
10046
10903
  });
@@ -11289,7 +12146,7 @@
11289
12146
  }
11290
12147
 
11291
12148
  function supportsCodePreviewCommentsForCurrentEditor() {
11292
- return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex";
12149
+ return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex" && !getDelimitedTextPreviewConfig(editorLanguage);
11293
12150
  }
11294
12151
 
11295
12152
  function getCodePreviewCommentKind(language) {
@@ -11490,6 +12347,26 @@
11490
12347
  return Boolean(shortcutsOverlayEl && !shortcutsOverlayEl.hidden);
11491
12348
  }
11492
12349
 
12350
+ function handleShortcutsScrollShortcut(event) {
12351
+ if (!isShortcutsOpen() || !shortcutsBodyEl || !event) return false;
12352
+ if (isTextEntryShortcutTarget(event.target)) return false;
12353
+ const key = typeof event.key === "string" ? event.key : "";
12354
+ let delta = 0;
12355
+ let targetTop = null;
12356
+ if (key === "ArrowDown") delta = 42;
12357
+ else if (key === "ArrowUp") delta = -42;
12358
+ else if (key === "PageDown") delta = Math.max(120, Math.round((shortcutsBodyEl.clientHeight || 0) * 0.85));
12359
+ else if (key === "PageUp") delta = -Math.max(120, Math.round((shortcutsBodyEl.clientHeight || 0) * 0.85));
12360
+ else if (key === "Home") targetTop = 0;
12361
+ else if (key === "End") targetTop = shortcutsBodyEl.scrollHeight;
12362
+ else return false;
12363
+ event.preventDefault();
12364
+ event.stopPropagation();
12365
+ if (targetTop !== null) shortcutsBodyEl.scrollTop = targetTop;
12366
+ else shortcutsBodyEl.scrollTop += delta;
12367
+ return true;
12368
+ }
12369
+
11493
12370
  function isScratchpadOpen() {
11494
12371
  return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
11495
12372
  }
@@ -15725,7 +16602,8 @@
15725
16602
  ? window.requestAnimationFrame.bind(window)
15726
16603
  : (cb) => window.setTimeout(cb, 16);
15727
16604
  schedule(() => {
15728
- if (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
16605
+ if (shortcutsBodyEl && typeof shortcutsBodyEl.focus === "function") shortcutsBodyEl.focus({ preventScroll: true });
16606
+ else if (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
15729
16607
  });
15730
16608
  }
15731
16609
 
@@ -15923,6 +16801,9 @@
15923
16801
  if (editorView === "preview") {
15924
16802
  scheduleSourcePreviewRender(0);
15925
16803
  }
16804
+ if (rightView === "editor-preview") {
16805
+ scheduleResponseEditorPreviewRender(0);
16806
+ }
15926
16807
  updateOutlineUi();
15927
16808
  scheduleWorkspacePersistence();
15928
16809
  }
@@ -17947,6 +18828,13 @@
17947
18828
  requestCompletionSuggestion();
17948
18829
  });
17949
18830
  }
18831
+ if (completionContextSelect) {
18832
+ completionContextSelect.value = completionSuggestionContextMode;
18833
+ completionContextSelect.addEventListener("change", () => {
18834
+ setCompletionSuggestionContextMode(completionContextSelect.value);
18835
+ syncActionButtons();
18836
+ });
18837
+ }
17950
18838
  if (completionSuggestionInsertBtn) {
17951
18839
  completionSuggestionInsertBtn.addEventListener("click", () => {
17952
18840
  insertCompletionSuggestion();