pi-studio 0.9.18 → 0.9.20

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.
@@ -229,6 +229,35 @@
229
229
  let stickyStudioKind = null;
230
230
  const pendingCompanionWindows = new Map();
231
231
  let initialDocumentApplied = false;
232
+ function normalizeRightViewValue(nextView) {
233
+ const raw = String(nextView || "").trim();
234
+ const normalized = raw === "preview"
235
+ ? "preview"
236
+ : (raw === "editor-preview"
237
+ ? "editor-preview"
238
+ : (raw === "repl"
239
+ ? "repl"
240
+ : (raw === "files"
241
+ ? "files"
242
+ : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown"))));
243
+ if (isEditorOnlyMode && normalized !== "editor-preview" && normalized !== "files" && normalized !== "repl") {
244
+ return "editor-preview";
245
+ }
246
+ return normalized;
247
+ }
248
+
249
+ function syncRightViewModeOptions() {
250
+ if (!rightViewSelect || !rightViewSelect.options) return;
251
+ const editorOnlyAllowed = new Set(["editor-preview", "files", "repl"]);
252
+ Array.from(rightViewSelect.options).forEach((option) => {
253
+ if (!option) return;
254
+ option.disabled = isEditorOnlyMode && !editorOnlyAllowed.has(option.value);
255
+ });
256
+ rightViewSelect.title = isEditorOnlyMode
257
+ ? "Editor-only views: editor preview, Files, or REPL. Shortcut: F7 when the right pane is active; F6 switches panes."
258
+ : "Right pane view mode. Shortcut: F7 when the right pane is active; F6 switches panes.";
259
+ }
260
+
232
261
  function getInitialRightView(source) {
233
262
  if (isEditorOnlyMode) return "editor-preview";
234
263
  return String(source || "").trim() === "file" ? "editor-preview" : "preview";
@@ -580,6 +609,7 @@
580
609
  toolName: typeof entry.toolName === "string" ? entry.toolName : "tool",
581
610
  label: parseNonEmptyString(entry.label),
582
611
  argsSummary: parseNonEmptyString(entry.argsSummary),
612
+ args: parseNonEmptyString(entry.args),
583
613
  output: typeof entry.output === "string" ? entry.output : "",
584
614
  images: Array.isArray(entry.images)
585
615
  ? entry.images.map((image, imageIndex) => normalizeTraceImage(image, imageIndex)).filter(Boolean)
@@ -860,8 +890,9 @@
860
890
  ? ("Tool: " + String(entry.toolName || "tool") + " — " + entry.label)
861
891
  : ("Tool: " + String(entry.toolName || "tool"));
862
892
  const parts = [header];
863
- if (String(entry.argsSummary || "").trim()) {
864
- parts.push("Input:\n" + String(entry.argsSummary || "").trim());
893
+ const inputText = String(entry.args || entry.argsSummary || "").trim();
894
+ if (inputText) {
895
+ parts.push("Input:\n" + inputText);
865
896
  }
866
897
  if (String(entry.output || "").trim()) {
867
898
  parts.push("Output:\n" + String(entry.output || "").trim());
@@ -2425,11 +2456,7 @@
2425
2456
  rightTitleGroupEl.appendChild(rightFocusBtn);
2426
2457
  rightTitleGroupEl.appendChild(makeStudioUiRefreshSeparator());
2427
2458
  }
2428
- if (isEditorOnlyMode) {
2429
- rightTitleGroupEl.appendChild(makeStudioUiRefreshElement("span", "studio-refresh-static-title", "Editor (Preview)"));
2430
- } else {
2431
- rightTitleGroupEl.appendChild(rightViewSelect);
2432
- }
2459
+ rightTitleGroupEl.appendChild(rightViewSelect);
2433
2460
  rightIdentityEl.appendChild(rightTitleGroupEl);
2434
2461
  const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
2435
2462
  if (exportPreviewControlsEl) {
@@ -2453,8 +2480,8 @@
2453
2480
  if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2454
2481
  const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2455
2482
  replActionLineEl.hidden = true;
2456
- if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2457
- if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2483
+ if (sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2484
+ if (replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2458
2485
  if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
2459
2486
  actionsEl.appendChild(actionLineTwoEl);
2460
2487
  if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
@@ -2609,6 +2636,9 @@
2609
2636
  }
2610
2637
 
2611
2638
  function getIdleStatus() {
2639
+ if (isEditorOnlyMode) {
2640
+ return "Editor-only mode: edit, browse files, annotate, preview, save, suggest, refresh file-backed text, or send to a REPL.";
2641
+ }
2612
2642
  return "Edit, load, or annotate text, then run, save, send to pi editor, or critique.";
2613
2643
  }
2614
2644
 
@@ -3176,7 +3206,7 @@
3176
3206
 
3177
3207
  function updateSourceBadge() {
3178
3208
  const label = sourceState && sourceState.label ? sourceState.label : "blank";
3179
- sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label;
3209
+ sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label + (hasRefreshableFilePath() ? " · file" : "");
3180
3210
  const descriptor = getCurrentStudioDocumentDescriptor();
3181
3211
  if (sourceBadgeEl) {
3182
3212
  sourceBadgeEl.title = descriptor.fileBacked
@@ -3188,9 +3218,11 @@
3188
3218
  if (isFileBacked) {
3189
3219
  var fileBackedResourceDir = getCurrentResourceDirValue() || dirnameForDisplayPath(sourceState.path);
3190
3220
  if (resourceDirInput) resourceDirInput.value = fileBackedResourceDir;
3191
- if (resourceDirLabel) resourceDirLabel.textContent = "";
3221
+ if (resourceDirLabel) {
3222
+ resourceDirLabel.textContent = fileBackedResourceDir ? ("Resource dir: " + fileBackedResourceDir) : "Resource dir: file directory";
3223
+ resourceDirLabel.hidden = false;
3224
+ }
3192
3225
  if (resourceDirBtn) resourceDirBtn.hidden = true;
3193
- if (resourceDirLabel) resourceDirLabel.hidden = true;
3194
3226
  if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
3195
3227
  } else {
3196
3228
  // Restore to label if dir is set, otherwise show button
@@ -3523,7 +3555,7 @@
3523
3555
 
3524
3556
  function cycleActivePaneView(direction) {
3525
3557
  if (activePane === "right") {
3526
- if (isEditorOnlyMode || !rightViewSelect || rightViewSelect.disabled) {
3558
+ if (!rightViewSelect || rightViewSelect.disabled) {
3527
3559
  setStatus("The right-pane view selector is unavailable.", "warning");
3528
3560
  return;
3529
3561
  }
@@ -3879,7 +3911,6 @@
3879
3911
  && !event.altKey
3880
3912
  && event.shiftKey
3881
3913
  && activePane === "left"
3882
- && !isEditorOnlyMode
3883
3914
  && rightView === "repl"
3884
3915
  ) {
3885
3916
  event.preventDefault();
@@ -4480,8 +4511,8 @@
4480
4511
  return lines.join("\n");
4481
4512
  }
4482
4513
 
4483
- function renderDelimitedTextPreview(targetEl, text, pane) {
4484
- const html = buildDelimitedTextPreviewHtml(text, editorLanguage || "");
4514
+ function renderDelimitedTextPreview(targetEl, text, pane, language) {
4515
+ const html = buildDelimitedTextPreviewHtml(text, language || editorLanguage || "");
4485
4516
  if (!html || !targetEl) return false;
4486
4517
  if (pane === "source") {
4487
4518
  sourcePreviewRenderNonce += 1;
@@ -4946,6 +4977,132 @@
4946
4977
  + " });\n"
4947
4978
  + " scheduleHeight();\n"
4948
4979
  + " }\n"
4980
+ + " let htmlCommentMode = false;\n"
4981
+ + " let htmlCommentHoverEl = null;\n"
4982
+ + " let htmlCommentHighlightTimer = null;\n"
4983
+ + " let htmlCommentLastPostAt = 0;\n"
4984
+ + " function htmlCommentCssEscape(value) {\n"
4985
+ + " const text = String(value || '');\n"
4986
+ + " try { if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text); } catch {}\n"
4987
+ + " return text.replace(/[^A-Za-z0-9_-]/g, function(ch) { return '\\\\' + ch; });\n"
4988
+ + " }\n"
4989
+ + " function getHtmlCommentSelector(element) {\n"
4990
+ + " if (!element || element.nodeType !== 1) return '';\n"
4991
+ + " if (element.id) return '#' + htmlCommentCssEscape(element.id);\n"
4992
+ + " const parts = [];\n"
4993
+ + " let el = element;\n"
4994
+ + " while (el && el.nodeType === 1 && el !== document.documentElement) {\n"
4995
+ + " const tag = el.tagName ? el.tagName.toLowerCase() : '';\n"
4996
+ + " if (!tag) break;\n"
4997
+ + " if (el.id) { parts.unshift(tag + '#' + htmlCommentCssEscape(el.id)); break; }\n"
4998
+ + " let index = 1;\n"
4999
+ + " let sibling = el.previousElementSibling;\n"
5000
+ + " while (sibling) { if ((sibling.tagName || '').toLowerCase() === tag) index += 1; sibling = sibling.previousElementSibling; }\n"
5001
+ + " parts.unshift(tag + ':nth-of-type(' + index + ')');\n"
5002
+ + " if (tag === 'body') break;\n"
5003
+ + " el = el.parentElement;\n"
5004
+ + " }\n"
5005
+ + " return parts.join(' > ');\n"
5006
+ + " }\n"
5007
+ + " function normalizeHtmlCommentText(value, maxLength) {\n"
5008
+ + " const text = String(value || '').replace(/\\s+/g, ' ').trim();\n"
5009
+ + " const limit = Math.max(24, Number(maxLength) || 200);\n"
5010
+ + " return text.length > limit ? text.slice(0, limit - 1).trimEnd() + '…' : text;\n"
5011
+ + " }\n"
5012
+ + " function getHtmlCommentElementLabel(element) {\n"
5013
+ + " if (!element || element.nodeType !== 1) return '';\n"
5014
+ + " const attrText = element.getAttribute('aria-label') || element.getAttribute('alt') || element.getAttribute('title') || '';\n"
5015
+ + " if (attrText) return normalizeHtmlCommentText(attrText, 220);\n"
5016
+ + " const tag = (element.tagName || '').toLowerCase();\n"
5017
+ + " if (tag === 'img') {\n"
5018
+ + " const src = String(element.getAttribute('src') || '').split(/[?#]/)[0].split('/').pop() || 'image';\n"
5019
+ + " return normalizeHtmlCommentText(src, 220);\n"
5020
+ + " }\n"
5021
+ + " return normalizeHtmlCommentText(element.innerText || element.textContent || '', 220);\n"
5022
+ + " }\n"
5023
+ + " function getHtmlCommentTarget(target) {\n"
5024
+ + " let node = target;\n"
5025
+ + " if (node && node.nodeType === 3) node = node.parentElement;\n"
5026
+ + " if (!node || node.nodeType !== 1) return document.body || document.documentElement;\n"
5027
+ + " if (typeof node.closest === 'function') {\n"
5028
+ + " return node.closest('img,figure,table,section,article,main,aside,nav,header,footer,pre,blockquote,ul,ol,li,canvas,svg,h1,h2,h3,h4,h5,h6,p,button,a,input,textarea,select,div') || node;\n"
5029
+ + " }\n"
5030
+ + " return node;\n"
5031
+ + " }\n"
5032
+ + " function getHtmlCommentSelectionText() {\n"
5033
+ + " const selection = typeof window.getSelection === 'function' ? window.getSelection() : null;\n"
5034
+ + " if (!selection || selection.rangeCount <= 0 || selection.isCollapsed) return '';\n"
5035
+ + " return normalizeHtmlCommentText(selection.toString(), 1000);\n"
5036
+ + " }\n"
5037
+ + " function getHtmlCommentSelectionElement() {\n"
5038
+ + " const selection = typeof window.getSelection === 'function' ? window.getSelection() : null;\n"
5039
+ + " if (!selection || selection.rangeCount <= 0) return null;\n"
5040
+ + " const range = selection.getRangeAt(0);\n"
5041
+ + " let node = range.commonAncestorContainer;\n"
5042
+ + " if (node && node.nodeType === 3) node = node.parentElement;\n"
5043
+ + " return node && node.nodeType === 1 ? node : null;\n"
5044
+ + " }\n"
5045
+ + " function postHtmlCommentTarget(kind, element, event, selectedText) {\n"
5046
+ + " const target = getHtmlCommentTarget(element || (event && event.target));\n"
5047
+ + " if (!target) return false;\n"
5048
+ + " htmlCommentLastPostAt = Date.now();\n"
5049
+ + " try {\n"
5050
+ + " parent.postMessage({\n"
5051
+ + " type: 'pi-studio-html-artifact-comment-target',\n"
5052
+ + " id: PREVIEW_ID,\n"
5053
+ + " kind: kind === 'selection' ? 'selection' : 'element',\n"
5054
+ + " selector: getHtmlCommentSelector(target),\n"
5055
+ + " tag: (target.tagName || '').toLowerCase(),\n"
5056
+ + " label: getHtmlCommentElementLabel(target),\n"
5057
+ + " text: normalizeHtmlCommentText(selectedText || '', 1000),\n"
5058
+ + " clientX: event && event.clientX || 0,\n"
5059
+ + " clientY: event && event.clientY || 0\n"
5060
+ + " }, '*');\n"
5061
+ + " return true;\n"
5062
+ + " } catch { return false; }\n"
5063
+ + " }\n"
5064
+ + " function clearHtmlCommentHover() {\n"
5065
+ + " if (htmlCommentHoverEl && htmlCommentHoverEl.classList) htmlCommentHoverEl.classList.remove('pi-studio-html-comment-hover');\n"
5066
+ + " htmlCommentHoverEl = null;\n"
5067
+ + " }\n"
5068
+ + " function setHtmlCommentMode(enabled) {\n"
5069
+ + " htmlCommentMode = Boolean(enabled);\n"
5070
+ + " if (document.documentElement && document.documentElement.classList) document.documentElement.classList.toggle('pi-studio-html-comment-mode', htmlCommentMode);\n"
5071
+ + " if (!htmlCommentMode) clearHtmlCommentHover();\n"
5072
+ + " }\n"
5073
+ + " function handleHtmlCommentMouseMove(event) {\n"
5074
+ + " if (!htmlCommentMode) return;\n"
5075
+ + " const target = getHtmlCommentTarget(event && event.target);\n"
5076
+ + " if (target === htmlCommentHoverEl) return;\n"
5077
+ + " clearHtmlCommentHover();\n"
5078
+ + " htmlCommentHoverEl = target;\n"
5079
+ + " if (htmlCommentHoverEl && htmlCommentHoverEl.classList) htmlCommentHoverEl.classList.add('pi-studio-html-comment-hover');\n"
5080
+ + " }\n"
5081
+ + " function handleHtmlCommentMouseUp(event) {\n"
5082
+ + " if (!htmlCommentMode) return;\n"
5083
+ + " const selectedText = getHtmlCommentSelectionText();\n"
5084
+ + " if (!selectedText) return;\n"
5085
+ + " postHtmlCommentTarget('selection', getHtmlCommentSelectionElement() || (event && event.target), event, selectedText);\n"
5086
+ + " if (event) { event.preventDefault(); event.stopPropagation(); }\n"
5087
+ + " }\n"
5088
+ + " function handleHtmlCommentClick(event) {\n"
5089
+ + " if (!htmlCommentMode) return;\n"
5090
+ + " if (Date.now() - htmlCommentLastPostAt < 450) { event.preventDefault(); event.stopPropagation(); return; }\n"
5091
+ + " postHtmlCommentTarget('element', event && event.target, event, '');\n"
5092
+ + " event.preventDefault();\n"
5093
+ + " event.stopPropagation();\n"
5094
+ + " }\n"
5095
+ + " function highlightHtmlCommentTarget(selector, anchorKind) {\n"
5096
+ + " if (htmlCommentHighlightTimer) { clearTimeout(htmlCommentHighlightTimer); htmlCommentHighlightTimer = null; }\n"
5097
+ + " Array.prototype.slice.call(document.querySelectorAll('.pi-studio-html-comment-highlight')).forEach(function(el) { el.classList.remove('pi-studio-html-comment-highlight'); });\n"
5098
+ + " if (anchorKind === 'html-page' || !selector) { try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch { window.scrollTo(0, 0); } return; }\n"
5099
+ + " let target = null;\n"
5100
+ + " try { target = document.querySelector(String(selector || '')); } catch {}\n"
5101
+ + " if (!target) return;\n"
5102
+ + " try { target.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); } catch { try { target.scrollIntoView(true); } catch {} }\n"
5103
+ + " if (target.classList) target.classList.add('pi-studio-html-comment-highlight');\n"
5104
+ + " htmlCommentHighlightTimer = setTimeout(function() { if (target && target.classList) target.classList.remove('pi-studio-html-comment-highlight'); }, 2400);\n"
5105
+ + " }\n"
4949
5106
  + " window.addEventListener('message', (event) => {\n"
4950
5107
  + " const data = event && event.data;\n"
4951
5108
  + " if (!data || typeof data !== 'object' || data.id !== PREVIEW_ID) return;\n"
@@ -4959,11 +5116,23 @@
4959
5116
  + " }\n"
4960
5117
  + " if (data.type === 'pi-studio-html-artifact-resources-resolved') {\n"
4961
5118
  + " applyResolvedHtmlPreviewResources(data.results);\n"
5119
+ + " return;\n"
5120
+ + " }\n"
5121
+ + " if (data.type === 'pi-studio-html-artifact-comment-mode') {\n"
5122
+ + " setHtmlCommentMode(data.enabled);\n"
5123
+ + " return;\n"
5124
+ + " }\n"
5125
+ + " if (data.type === 'pi-studio-html-artifact-highlight-comment') {\n"
5126
+ + " highlightHtmlCommentTarget(data.selector, data.anchorKind);\n"
4962
5127
  + " }\n"
4963
5128
  + " });\n"
4964
5129
  + " document.addEventListener('click', handleFragmentAnchorClick);\n"
4965
5130
  + " document.addEventListener('click', handleHtmlPreviewLocalLinkClick);\n"
4966
5131
  + " document.addEventListener('contextmenu', handleHtmlPreviewLocalLinkContextMenu);\n"
5132
+ + " document.addEventListener('mousemove', handleHtmlCommentMouseMove, true);\n"
5133
+ + " document.addEventListener('mouseleave', clearHtmlCommentHover, true);\n"
5134
+ + " document.addEventListener('mouseup', handleHtmlCommentMouseUp, true);\n"
5135
+ + " document.addEventListener('click', handleHtmlCommentClick, true);\n"
4967
5136
  + " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4968
5137
  + " window.addEventListener('hashchange', () => {\n"
4969
5138
  + " const hash = String(window.location && window.location.hash || '');\n"
@@ -4995,6 +5164,9 @@
4995
5164
  + ".pi-studio-html-math-display{display:block;margin:0.75em 0;overflow-x:auto;text-align:center;}\n"
4996
5165
  + ".pi-studio-html-math-display>math{display:block;margin:0 auto;}\n"
4997
5166
  + ".pi-studio-html-math-inline>math{vertical-align:-0.15em;}\n"
5167
+ + "html.pi-studio-html-comment-mode,html.pi-studio-html-comment-mode body{cursor:crosshair!important;}\n"
5168
+ + ".pi-studio-html-comment-hover{outline:2px solid #0f8b8d!important;outline-offset:3px!important;}\n"
5169
+ + ".pi-studio-html-comment-highlight{outline:3px solid #d97706!important;outline-offset:4px!important;box-shadow:0 0 0 6px rgba(217,119,6,.18)!important;}\n"
4998
5170
  + "</style>\n";
4999
5171
  }
5000
5172
 
@@ -5029,6 +5201,11 @@
5029
5201
  });
5030
5202
  }
5031
5203
 
5204
+ function setHtmlArtifactDetailText(record, text) {
5205
+ if (!record || !record.detail) return;
5206
+ record.detail.textContent = record.commentMode ? "HTML preview · comment mode" : (text || "HTML preview");
5207
+ }
5208
+
5032
5209
  function handleHtmlArtifactFrameSizeMessage(event) {
5033
5210
  const data = event && event.data;
5034
5211
  if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-size") return;
@@ -5040,7 +5217,7 @@
5040
5217
  }
5041
5218
  if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
5042
5219
  if (record.shell && record.shell.classList && record.shell.classList.contains("is-focused")) {
5043
- if (record.detail) record.detail.textContent = "HTML preview";
5220
+ setHtmlArtifactDetailText(record, "HTML preview");
5044
5221
  return;
5045
5222
  }
5046
5223
  const rawHeight = Number(data.height);
@@ -5057,9 +5234,7 @@
5057
5234
  record.shell.style.minHeight = "0";
5058
5235
  record.shell.classList.toggle("is-height-capped", capped);
5059
5236
  }
5060
- if (record.detail) {
5061
- record.detail.textContent = "HTML preview";
5062
- }
5237
+ setHtmlArtifactDetailText(record, "HTML preview");
5063
5238
  }
5064
5239
 
5065
5240
  function handleHtmlArtifactFrameFragmentMessage(event) {
@@ -5183,7 +5358,7 @@
5183
5358
  error: error && error.message ? error.message : String(error || "HTML preview math render failed."),
5184
5359
  })));
5185
5360
  } finally {
5186
- if (record.detail) record.detail.textContent = "HTML preview";
5361
+ setHtmlArtifactDetailText(record, "HTML preview");
5187
5362
  }
5188
5363
  }
5189
5364
 
@@ -5273,7 +5448,7 @@
5273
5448
  if (record.detail) record.detail.textContent = "HTML preview · loading local images";
5274
5449
  const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
5275
5450
  postHtmlArtifactResourceResults(record, results);
5276
- if (record.detail) record.detail.textContent = "HTML preview";
5451
+ setHtmlArtifactDetailText(record, "HTML preview");
5277
5452
  }
5278
5453
 
5279
5454
  function handleHtmlArtifactFrameResourceMessage(event) {
@@ -5356,11 +5531,36 @@
5356
5531
  setStatus("Right-click this local HTML preview link for file actions.", "warning");
5357
5532
  }
5358
5533
 
5534
+ function handleHtmlArtifactFrameCommentTargetMessage(event) {
5535
+ const data = event && event.data;
5536
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-comment-target") return;
5537
+ const id = typeof data.id === "string" ? data.id : "";
5538
+ const record = id ? htmlArtifactFramesById.get(id) : null;
5539
+ if (!record || !record.iframe || !record.iframe.isConnected) {
5540
+ if (id) htmlArtifactFramesById.delete(id);
5541
+ return;
5542
+ }
5543
+ if (!record.commentable) return;
5544
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
5545
+ const note = addReviewNoteFromHtmlArtifactTarget(record, data);
5546
+ if (note && record.iframe && record.iframe.contentWindow) {
5547
+ try {
5548
+ record.iframe.contentWindow.postMessage({
5549
+ type: "pi-studio-html-artifact-highlight-comment",
5550
+ id: record.id || "",
5551
+ selector: note.htmlSelector || "",
5552
+ anchorKind: note.anchorKind || "html-element",
5553
+ }, "*");
5554
+ } catch {}
5555
+ }
5556
+ }
5557
+
5359
5558
  window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
5360
5559
  window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
5361
5560
  window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
5362
5561
  window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
5363
5562
  window.addEventListener("message", handleHtmlArtifactFrameLocalLinkMessage);
5563
+ window.addEventListener("message", handleHtmlArtifactFrameCommentTargetMessage);
5364
5564
 
5365
5565
  function isStudioHtmlFocusOpen() {
5366
5566
  return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
@@ -5588,6 +5788,36 @@
5588
5788
 
5589
5789
  const tools = document.createElement("span");
5590
5790
  tools.className = "studio-html-artifact-tools";
5791
+ const commentable = Boolean(options && options.commentable);
5792
+
5793
+ let commentBtn = null;
5794
+ let pageCommentBtn = null;
5795
+ const makeCommentButton = (text, title, onClick) => {
5796
+ const button = document.createElement("button");
5797
+ button.type = "button";
5798
+ button.className = "studio-html-artifact-comment-btn";
5799
+ button.textContent = text;
5800
+ button.title = title;
5801
+ button.addEventListener("pointerdown", (event) => { event.stopPropagation(); });
5802
+ button.addEventListener("mousedown", (event) => { event.stopPropagation(); });
5803
+ button.addEventListener("click", (event) => {
5804
+ event.preventDefault();
5805
+ event.stopPropagation();
5806
+ onClick();
5807
+ });
5808
+ return button;
5809
+ };
5810
+ if (commentable) {
5811
+ commentBtn = makeCommentButton("Comment mode", "Turn on HTML preview comment mode. Select text or click an element in the preview to add a local comment.", () => {
5812
+ const record = htmlArtifactFramesById.get(previewId);
5813
+ setHtmlArtifactRecordCommentMode(record, !(record && record.commentMode));
5814
+ });
5815
+ commentBtn.setAttribute("aria-pressed", "false");
5816
+ pageCommentBtn = makeCommentButton("Page", "Add a page-level local comment for this HTML preview.", () => {
5817
+ const record = htmlArtifactFramesById.get(previewId);
5818
+ addReviewNoteFromHtmlArtifactPage(record || null);
5819
+ });
5820
+ }
5591
5821
 
5592
5822
  const zoomControls = document.createElement("span");
5593
5823
  zoomControls.className = "studio-html-artifact-zoom-controls";
@@ -5651,6 +5881,8 @@
5651
5881
  fullscreenBtn.appendChild(makeStudioUiRefreshIcon("fullscreen"));
5652
5882
  updateZoomUi();
5653
5883
  tools.appendChild(detail);
5884
+ if (commentBtn) tools.appendChild(commentBtn);
5885
+ if (pageCommentBtn) tools.appendChild(pageCommentBtn);
5654
5886
  tools.appendChild(zoomControls);
5655
5887
  tools.appendChild(fullscreenBtn);
5656
5888
 
@@ -5665,7 +5897,7 @@
5665
5897
  iframe.referrerPolicy = "no-referrer";
5666
5898
  iframe.setAttribute("sandbox", "allow-scripts allow-modals");
5667
5899
  iframe.setAttribute("allow", "clipboard-write");
5668
- iframe.addEventListener("load", () => { postArtifactZoom(); });
5900
+ iframe.addEventListener("load", () => { postArtifactZoom(); postHtmlArtifactCommentMode(htmlArtifactFramesById.get(previewId)); });
5669
5901
  iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
5670
5902
  shell.appendChild(iframe);
5671
5903
  htmlArtifactFramesById.set(previewId, {
@@ -5674,6 +5906,11 @@
5674
5906
  shell,
5675
5907
  detail,
5676
5908
  zoomControls,
5909
+ commentBtn,
5910
+ pageCommentBtn,
5911
+ commentMode: false,
5912
+ commentable,
5913
+ title,
5677
5914
  sourcePath: options && options.sourcePath ? String(options.sourcePath) : "",
5678
5915
  resourceDir: options && options.resourceDir ? String(options.resourceDir) : "",
5679
5916
  mathRenderBatchCount: 0,
@@ -5690,6 +5927,33 @@
5690
5927
  }
5691
5928
  }
5692
5929
 
5930
+ function postHtmlArtifactCommentMode(record) {
5931
+ if (!record || !record.iframe || !record.iframe.contentWindow) return;
5932
+ try {
5933
+ record.iframe.contentWindow.postMessage({
5934
+ type: "pi-studio-html-artifact-comment-mode",
5935
+ id: record.id || "",
5936
+ enabled: Boolean(record.commentMode),
5937
+ }, "*");
5938
+ } catch {}
5939
+ }
5940
+
5941
+ function setHtmlArtifactRecordCommentMode(record, enabled) {
5942
+ if (!record) return;
5943
+ record.commentMode = Boolean(enabled);
5944
+ if (record.shell && record.shell.classList) record.shell.classList.toggle("is-comment-mode", record.commentMode);
5945
+ if (record.commentBtn) {
5946
+ record.commentBtn.classList.toggle("is-active", record.commentMode);
5947
+ record.commentBtn.setAttribute("aria-pressed", record.commentMode ? "true" : "false");
5948
+ record.commentBtn.textContent = "Comment mode";
5949
+ record.commentBtn.title = record.commentMode
5950
+ ? "HTML comment mode is on. Select text or click an element in the preview to add a local comment."
5951
+ : "Turn on HTML preview comment mode. Select text or click an element in the preview to add a local comment.";
5952
+ }
5953
+ if (record.detail) record.detail.textContent = record.commentMode ? "HTML preview · comment mode" : "HTML preview";
5954
+ postHtmlArtifactCommentMode(record);
5955
+ }
5956
+
5693
5957
  function getRightPaneHtmlArtifactSource() {
5694
5958
  if (rightView === "editor-preview") {
5695
5959
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
@@ -5807,6 +6071,15 @@
5807
6071
  openLink.textContent = "Open PDF";
5808
6072
  actions.appendChild(openLink);
5809
6073
 
6074
+ const refreshBtn = document.createElement("button");
6075
+ refreshBtn.type = "button";
6076
+ refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
6077
+ refreshBtn.textContent = "Refresh";
6078
+ refreshBtn.title = "Reload this PDF preview from disk.";
6079
+ refreshBtn.setAttribute("aria-label", "Refresh PDF preview from disk");
6080
+ refreshBtn.addEventListener("click", () => refreshStudioPdfFocusViewer());
6081
+ actions.appendChild(refreshBtn);
6082
+
5810
6083
  const fullscreenBtn = document.createElement("button");
5811
6084
  fullscreenBtn.type = "button";
5812
6085
  fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
@@ -5928,6 +6201,70 @@
5928
6201
  return "/pdf-resource?" + params.toString();
5929
6202
  }
5930
6203
 
6204
+ function buildRefreshedStudioPdfViewerUrl(value) {
6205
+ const raw = String(value || "").trim();
6206
+ if (!raw) return "";
6207
+ const hashIndex = raw.indexOf("#");
6208
+ const base = hashIndex >= 0 ? raw.slice(0, hashIndex) : raw;
6209
+ const hash = hashIndex >= 0 ? raw.slice(hashIndex) : "";
6210
+ const nonce = Date.now().toString(36);
6211
+ try {
6212
+ const url = new URL(base || window.location.href, window.location.href);
6213
+ url.searchParams.set("_studioPdfRefresh", nonce);
6214
+ return url.href + hash;
6215
+ } catch {
6216
+ const separator = base.indexOf("?") >= 0 ? "&" : "?";
6217
+ return base + separator + "_studioPdfRefresh=" + encodeURIComponent(nonce) + hash;
6218
+ }
6219
+ }
6220
+
6221
+ function syncStudioPdfCardViewerUrl(card, viewerUrl) {
6222
+ if (!card) return;
6223
+ const nextUrl = String(viewerUrl || "").trim();
6224
+ if (!nextUrl) return;
6225
+ if (card.dataset) card.dataset.studioPdfViewerUrl = nextUrl;
6226
+ const frame = typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
6227
+ if (frame) frame.src = nextUrl;
6228
+ const openLink = typeof card.querySelector === "function" ? card.querySelector("a.studio-pdf-card-link") : null;
6229
+ if (openLink) openLink.href = nextUrl;
6230
+ const focusBtn = typeof card.querySelector === "function" ? card.querySelector("button.studio-pdf-card-focus") : null;
6231
+ if (focusBtn && focusBtn.dataset) focusBtn.dataset.studioPdfViewerUrl = nextUrl;
6232
+ }
6233
+
6234
+ function refreshStudioPdfCard(card) {
6235
+ if (!card) return false;
6236
+ const frame = typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
6237
+ const currentUrl = String(card.dataset && card.dataset.studioPdfViewerUrl ? card.dataset.studioPdfViewerUrl : "").trim()
6238
+ || String(frame && frame.src ? frame.src : "").trim();
6239
+ const nextUrl = buildRefreshedStudioPdfViewerUrl(currentUrl);
6240
+ if (!nextUrl) return false;
6241
+ syncStudioPdfCardViewerUrl(card, nextUrl);
6242
+ setStatus("Refreshed PDF preview from disk.", "success");
6243
+ return true;
6244
+ }
6245
+
6246
+ function getStudioPdfFocusActiveFrame() {
6247
+ if (studioPdfFocusMovedFrameState && studioPdfFocusMovedFrameState.frame && studioPdfFocusMovedFrameState.frame.isConnected) {
6248
+ return studioPdfFocusMovedFrameState.frame;
6249
+ }
6250
+ return studioPdfFocusFrameEl;
6251
+ }
6252
+
6253
+ function refreshStudioPdfFocusViewer() {
6254
+ const frame = getStudioPdfFocusActiveFrame();
6255
+ const currentUrl = String(frame && frame.src ? frame.src : "").trim()
6256
+ || String(studioPdfFocusOpenLinkEl && studioPdfFocusOpenLinkEl.href ? studioPdfFocusOpenLinkEl.href : "").trim();
6257
+ const nextUrl = buildRefreshedStudioPdfViewerUrl(currentUrl);
6258
+ if (!nextUrl) {
6259
+ setStatus("Could not refresh this PDF preview.", "warning");
6260
+ return false;
6261
+ }
6262
+ if (frame) frame.src = nextUrl;
6263
+ if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = nextUrl;
6264
+ setStatus("Refreshed PDF preview from disk.", "success");
6265
+ return true;
6266
+ }
6267
+
5931
6268
  function syncStudioPdfFocusFullscreenButton() {
5932
6269
  if (!studioPdfFocusFullscreenBtn) return;
5933
6270
  const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
@@ -6458,6 +6795,18 @@
6458
6795
  openLink.textContent = "Open PDF";
6459
6796
  actions.appendChild(openLink);
6460
6797
 
6798
+ const refreshBtn = document.createElement("button");
6799
+ refreshBtn.type = "button";
6800
+ refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
6801
+ refreshBtn.textContent = "Refresh";
6802
+ refreshBtn.title = "Reload this PDF preview from disk.";
6803
+ refreshBtn.addEventListener("click", (event) => {
6804
+ event.preventDefault();
6805
+ event.stopPropagation();
6806
+ if (!refreshStudioPdfCard(card)) setStatus("Could not refresh this PDF preview.", "warning");
6807
+ });
6808
+ actions.appendChild(refreshBtn);
6809
+
6461
6810
  header.appendChild(actions);
6462
6811
  }
6463
6812
  card.appendChild(header);
@@ -8236,15 +8585,16 @@
8236
8585
  function renderSourcePreviewNow() {
8237
8586
  if (editorView !== "preview") return;
8238
8587
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
8239
- if (isHtmlArtifactPreviewText(text, editorLanguage)) {
8240
- renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
8588
+ const previewLanguage = getEditorLanguageForPreview();
8589
+ if (isHtmlArtifactPreviewText(text, previewLanguage)) {
8590
+ renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", commentable: true, ...getHtmlPreviewResourceContextOptions() });
8241
8591
  return;
8242
8592
  }
8243
- if (renderDelimitedTextPreview(sourcePreviewEl, text, "source")) {
8593
+ if (renderDelimitedTextPreview(sourcePreviewEl, text, "source", previewLanguage)) {
8244
8594
  return;
8245
8595
  }
8246
- if (supportsCodePreviewCommentsForCurrentEditor()) {
8247
- renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
8596
+ if (supportsCodePreviewCommentsForLanguage(previewLanguage)) {
8597
+ renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source", previewLanguage);
8248
8598
  return;
8249
8599
  }
8250
8600
  const nonce = ++sourcePreviewRenderNonce;
@@ -8368,9 +8718,12 @@
8368
8718
  return { text: preview, truncated: true, hiddenChars, hiddenLines };
8369
8719
  }
8370
8720
 
8371
- function renderTraceOutput(text, outputKey) {
8721
+ function renderTraceOutput(text, outputKey, options) {
8372
8722
  const value = String(text || "");
8373
8723
  const key = String(outputKey || "trace-output");
8724
+ const label = options && typeof options.label === "string" && options.label.trim()
8725
+ ? options.label.trim()
8726
+ : "Output";
8374
8727
  const isExpanded = traceExpandedOutputs.has(key);
8375
8728
  const preview = getTraceOutputPreview(value);
8376
8729
  const visibleText = isExpanded || !preview.truncated ? value : preview.text;
@@ -8380,10 +8733,11 @@
8380
8733
  const hiddenParts = [];
8381
8734
  if (preview.hiddenLines > 0) hiddenParts.push(preview.hiddenLines + " more line" + (preview.hiddenLines === 1 ? "" : "s"));
8382
8735
  if (preview.hiddenChars > 0) hiddenParts.push(formatCompactNumber(preview.hiddenChars) + " chars hidden");
8736
+ const labelLower = label.toLowerCase();
8383
8737
  const summary = isExpanded
8384
- ? "Showing full output (" + formatTraceOutputSize(value) + ")."
8385
- : "Output truncated — " + (hiddenParts.join(", ") || "more hidden") + ".";
8386
- const buttonLabel = isExpanded ? "Collapse" : "Show full";
8738
+ ? "Showing full " + labelLower + " (" + formatTraceOutputSize(value) + ")."
8739
+ : label + " truncated — " + (hiddenParts.join(", ") || "more hidden") + ".";
8740
+ const buttonLabel = isExpanded ? "Collapse " + labelLower : "Show full " + labelLower;
8387
8741
  return "<div class='trace-output-wrap" + (isExpanded ? " is-expanded" : " is-truncated") + "'>"
8388
8742
  + body
8389
8743
  + "<div class='trace-output-truncation'>"
@@ -8757,15 +9111,16 @@
8757
9111
  }
8758
9112
 
8759
9113
  const title = entry.label || entry.toolName || "tool";
8760
- const argsSummary = entry.argsSummary
8761
- ? "<div class='trace-section'><div class='trace-section-label'>Input</div>" + renderTraceOutput(entry.argsSummary, entry.id + ":input") + "</div>"
9114
+ const inputText = entry.args || entry.argsSummary || "";
9115
+ const argsSummary = inputText
9116
+ ? "<div class='trace-section trace-section-input'><div class='trace-section-label'>Input</div>" + renderTraceOutput(inputText, entry.id + ":input", { label: "Input" }) + "</div>"
8762
9117
  : "";
8763
9118
  const imageOutput = renderTraceImages(entry.images);
8764
9119
  const outputPieces = [];
8765
- if (entry.output) outputPieces.push(renderTraceOutput(entry.output, entry.id + ":output"));
9120
+ if (entry.output) outputPieces.push(renderTraceOutput(entry.output, entry.id + ":output", { label: "Output" }));
8766
9121
  if (imageOutput) outputPieces.push(imageOutput);
8767
9122
  const output = outputPieces.length
8768
- ? "<div class='trace-section'><div class='trace-section-label'>Output</div>" + outputPieces.join("") + "</div>"
9123
+ ? "<div class='trace-section trace-section-output'><div class='trace-section-label'>Output</div>" + outputPieces.join("") + "</div>"
8769
9124
  : "<div class='trace-empty-inline'>No output yet.</div>";
8770
9125
  const toolStatusLabel = entry.isError
8771
9126
  ? "Error"
@@ -8874,8 +9229,11 @@
8874
9229
  const newTabAction = kind === "text" || kind === "office"
8875
9230
  ? "open-new"
8876
9231
  : ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
9232
+ const newTabLabel = kind === "text"
9233
+ ? "Open file tab"
9234
+ : (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
8877
9235
  const textActions = newTabAction
8878
- ? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
9236
+ ? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>" + escapeHtml(newTabLabel) + "</button>"
8879
9237
  : "";
8880
9238
  const openTitle = type === "directory"
8881
9239
  ? "Open folder"
@@ -8998,10 +9356,38 @@
8998
9356
  }
8999
9357
  }
9000
9358
 
9359
+ function basenameForStudioPath(path) {
9360
+ const value = stripPreviewLocalLinkUrlSuffix(path || "").replace(/\\/g, "/");
9361
+ const parts = value.split("/");
9362
+ return parts.pop() || value || "file";
9363
+ }
9364
+
9365
+ function ensureCurrentEditorFileBackedFromFilesPath(path) {
9366
+ const cleanPath = stripPreviewLocalLinkUrlSuffix(path || "").trim();
9367
+ if (!isLikelyAbsoluteStudioPath(cleanPath)) return;
9368
+ if (sourceState && sourceState.path === cleanPath) return;
9369
+ const resourceDir = normalizeStudioResourceDirValue(fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(cleanPath));
9370
+ if (resourceDirInput && resourceDir) resourceDirInput.value = resourceDir;
9371
+ setSourceState({
9372
+ source: "file",
9373
+ label: sourceState && sourceState.label && sourceState.label !== "blank" ? sourceState.label : basenameForStudioPath(cleanPath),
9374
+ path: cleanPath,
9375
+ });
9376
+ markFileBackedBaseline(sourceTextEl.value);
9377
+ }
9378
+
9001
9379
  async function openFileBrowserEntry(path, kind) {
9002
9380
  const context = getFileBrowserLocalLinkContext();
9003
- if (kind === "text" || kind === "office") {
9004
- await openPreviewDocumentHere(path, context);
9381
+ if (kind === "text") {
9382
+ await openPreviewDocumentHere(path, context, { fallbackPath: path, fileBackedIntent: true });
9383
+ ensureCurrentEditorFileBackedFromFilesPath(path);
9384
+ if (sourceState && sourceState.path) {
9385
+ setStatus("Opened file-backed document in editor: " + (sourceState.label || sourceState.path), "success");
9386
+ }
9387
+ return;
9388
+ }
9389
+ if (kind === "office") {
9390
+ await openPreviewDocumentHere(path, context, { fallbackPath: path });
9005
9391
  return;
9006
9392
  }
9007
9393
  if (kind === "pdf") {
@@ -9103,15 +9489,16 @@
9103
9489
  scheduleResponsePaneRepaintNudge();
9104
9490
  return;
9105
9491
  }
9106
- if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
9107
- renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
9492
+ const previewLanguage = getEditorLanguageForPreview();
9493
+ if (isHtmlArtifactPreviewText(editorText, previewLanguage)) {
9494
+ renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", commentable: true, ...getHtmlPreviewResourceContextOptions() });
9108
9495
  return;
9109
9496
  }
9110
- if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response")) {
9497
+ if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response", previewLanguage)) {
9111
9498
  return;
9112
9499
  }
9113
- if (supportsCodePreviewCommentsForCurrentEditor()) {
9114
- renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
9500
+ if (supportsCodePreviewCommentsForLanguage(previewLanguage)) {
9501
+ renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response", previewLanguage);
9115
9502
  return;
9116
9503
  }
9117
9504
  const nonce = ++responsePreviewRenderNonce;
@@ -9296,13 +9683,17 @@
9296
9683
  return resourceDirInput ? normalizeStudioResourceDirValue(resourceDirInput.value) : "";
9297
9684
  }
9298
9685
 
9686
+ function stripImportedFileLabel(label) {
9687
+ return String(label || "").replace(/^(?:upload|imported copy):\s*/i, "");
9688
+ }
9689
+
9299
9690
  function getEffectiveSavePath() {
9300
9691
  // File-backed: use the original path
9301
9692
  if (sourceState.path) return sourceState.path;
9302
- // Upload with working dir + filename: derive path
9693
+ // Browser-imported copy with working dir + filename: derive path
9303
9694
  const resourceDir = getCurrentResourceDirValue();
9304
9695
  if (sourceState.source === "upload" && sourceState.label && resourceDir) {
9305
- var name = sourceState.label.replace(/^upload:\s*/i, "");
9696
+ var name = stripImportedFileLabel(sourceState.label);
9306
9697
  if (name) return resourceDir.replace(/\/$/, "") + "/" + name;
9307
9698
  }
9308
9699
  return null;
@@ -9327,7 +9718,7 @@
9327
9718
  return dir + stem + ".annotated.md";
9328
9719
  }
9329
9720
 
9330
- const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
9721
+ const rawLabel = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
9331
9722
  const stem = rawLabel.replace(/\.[^.]+$/, "") || "draft";
9332
9723
  const suggestedDir = getCurrentResourceDirValue()
9333
9724
  ? getCurrentResourceDirValue().replace(/\/$/, "") + "/"
@@ -9355,7 +9746,7 @@
9355
9746
  return;
9356
9747
  }
9357
9748
 
9358
- refreshFromDiskBtn.title = "Refresh from disk is only available for documents that currently have a file path.";
9749
+ 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.";
9359
9750
  }
9360
9751
 
9361
9752
  function syncActionButtons() {
@@ -9390,7 +9781,8 @@
9390
9781
  if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
9391
9782
  if (compactBtn) compactBtn.disabled = isEditorOnlyMode || uiBusy || compactInProgress || wsState === "Disconnected";
9392
9783
  editorViewSelect.disabled = isEditorOnlyMode;
9393
- rightViewSelect.disabled = isEditorOnlyMode;
9784
+ syncRightViewModeOptions();
9785
+ rightViewSelect.disabled = false;
9394
9786
  followSelect.disabled = isEditorOnlyMode || uiBusy;
9395
9787
  if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
9396
9788
  insertHeaderBtn.disabled = uiBusy;
@@ -9501,7 +9893,7 @@
9501
9893
  sourceState: normalizeWorkspaceSourceState(sourceState),
9502
9894
  resourceDir: getCurrentResourceDirValue(),
9503
9895
  editorView,
9504
- rightView: isEditorOnlyMode ? "editor-preview" : rightView,
9896
+ rightView: normalizeRightViewValue(rightView),
9505
9897
  editorLanguage,
9506
9898
  followLatest,
9507
9899
  responseHistoryIndex,
@@ -9576,15 +9968,7 @@
9576
9968
  setEditorLanguage(state.editorLanguage.trim());
9577
9969
  }
9578
9970
  editorView = state.editorView === "preview" ? "preview" : "markdown";
9579
- rightView = isEditorOnlyMode
9580
- ? "editor-preview"
9581
- : (state.rightView === "preview"
9582
- ? "preview"
9583
- : (state.rightView === "editor-preview"
9584
- ? "editor-preview"
9585
- : (state.rightView === "repl"
9586
- ? "repl"
9587
- : (state.rightView === "files" ? "files" : ((state.rightView === "trace" || state.rightView === "thinking") ? "trace" : "markdown")))));
9971
+ rightView = normalizeRightViewValue(state.rightView);
9588
9972
  if (typeof state.followLatest === "boolean") {
9589
9973
  followLatest = state.followLatest;
9590
9974
  }
@@ -10127,15 +10511,8 @@
10127
10511
 
10128
10512
  function setRightView(nextView) {
10129
10513
  const previousView = rightView;
10130
- rightView = nextView === "preview"
10131
- ? "preview"
10132
- : (nextView === "editor-preview"
10133
- ? "editor-preview"
10134
- : (nextView === "repl"
10135
- ? "repl"
10136
- : (nextView === "files"
10137
- ? "files"
10138
- : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"))));
10514
+ rightView = normalizeRightViewValue(nextView);
10515
+ syncRightViewModeOptions();
10139
10516
  rightViewSelect.value = rightView;
10140
10517
  if (rightView === "trace" && previousView !== "trace") {
10141
10518
  traceAutoScroll = true;
@@ -10726,21 +11103,34 @@
10726
11103
  return confirmed;
10727
11104
  }
10728
11105
 
10729
- async function openPreviewDocumentHere(href, contextOverride) {
11106
+ function isLikelyAbsoluteStudioPath(path) {
11107
+ const value = stripPreviewLocalLinkUrlSuffix(path || "").trim();
11108
+ return Boolean(value && (/^\//.test(value) || /^[A-Za-z]:[\\/]/.test(value)));
11109
+ }
11110
+
11111
+ async function openPreviewDocumentHere(href, contextOverride, options) {
10730
11112
  if (!confirmPreviewOfficeConversion(href, "here")) return;
10731
11113
  if (editorHasPotentialUnsavedContent()) {
10732
- const confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
11114
+ const kind = getPreviewLocalLinkKind(href);
11115
+ const prompt = kind === "office"
11116
+ ? "Replace the current editor contents with this converted Markdown copy? Unsaved editor changes may be lost."
11117
+ : "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.";
11118
+ const confirmed = window.confirm(prompt);
10733
11119
  if (!confirmed) return;
10734
11120
  }
10735
11121
  const payload = await fetchPreviewLocalLink("document", href, contextOverride);
10736
11122
  if (typeof payload.text !== "string") throw new Error("Studio did not return document text.");
10737
- const path = typeof payload.path === "string" ? payload.path : "";
11123
+ const responsePath = typeof payload.path === "string" ? payload.path : "";
11124
+ const fallbackPath = options && typeof options.fallbackPath === "string" && isLikelyAbsoluteStudioPath(options.fallbackPath)
11125
+ ? stripPreviewLocalLinkUrlSuffix(options.fallbackPath).trim()
11126
+ : "";
11127
+ const path = responsePath || fallbackPath;
10738
11128
  const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
10739
11129
  const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
10740
11130
  const converted = payload && payload.converted === true;
10741
11131
  if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
10742
11132
  setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
10743
- if (converted) {
11133
+ if (converted || !path) {
10744
11134
  setSourceState({ source: "blank", label, path: null });
10745
11135
  } else {
10746
11136
  setSourceState({ source: "file", label, path });
@@ -10750,7 +11140,9 @@
10750
11140
  if (detected) setEditorLanguage(detected);
10751
11141
  setEditorView("markdown");
10752
11142
  setActivePane("left");
10753
- setStatus(converted ? ("Converted document into editor: " + label) : ("Opened linked file in editor: " + label), "success");
11143
+ setStatus(converted
11144
+ ? ("Converted document into editor: " + label)
11145
+ : (path ? ("Opened file-backed document in editor: " + label) : ("Opened linked file copy in editor: " + label)), "success");
10754
11146
  }
10755
11147
 
10756
11148
  async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
@@ -12145,8 +12537,21 @@
12145
12537
  return out.join("<br>");
12146
12538
  }
12147
12539
 
12540
+ function getEditorLanguageForPreview() {
12541
+ const detected = detectLanguageFromName((sourceState && (sourceState.path || sourceState.label)) || "");
12542
+ if (detected && (!editorLanguage || editorLanguage === "markdown" || editorLanguage === "text")) {
12543
+ return detected;
12544
+ }
12545
+ return editorLanguage || detected || "";
12546
+ }
12547
+
12548
+ function supportsCodePreviewCommentsForLanguage(language) {
12549
+ const lang = normalizeFenceLanguage(language || "");
12550
+ return Boolean(lang) && lang !== "markdown" && lang !== "latex" && !getDelimitedTextPreviewConfig(lang);
12551
+ }
12552
+
12148
12553
  function supportsCodePreviewCommentsForCurrentEditor() {
12149
- return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex" && !getDelimitedTextPreviewConfig(editorLanguage);
12554
+ return supportsCodePreviewCommentsForLanguage(getEditorLanguageForPreview());
12150
12555
  }
12151
12556
 
12152
12557
  function getCodePreviewCommentKind(language) {
@@ -12191,11 +12596,11 @@
12191
12596
  return "<div class='response-markdown-highlight preview-code-lines'>" + html.join("") + "</div>";
12192
12597
  }
12193
12598
 
12194
- function renderCodePreviewWithCommentBlocks(targetEl, text, pane) {
12599
+ function renderCodePreviewWithCommentBlocks(targetEl, text, pane, language) {
12195
12600
  if (!targetEl) return;
12196
12601
  clearPreviewJumpHighlight(targetEl);
12197
12602
  finishPreviewRender(targetEl);
12198
- targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
12603
+ targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, language || editorLanguage || "");
12199
12604
  ensurePreviewSelectionActions(targetEl);
12200
12605
  updatePreviewCommentBlocksForElement(targetEl);
12201
12606
  decorateCopyablePreviewBlocks(targetEl);
@@ -12512,10 +12917,21 @@
12512
12917
  scheduleScratchpadPersistence(value, descriptor.key);
12513
12918
  }
12514
12919
 
12920
+ function normalizeReviewNoteAnchorKind(value) {
12921
+ const raw = typeof value === "string" ? value.trim().toLowerCase() : "";
12922
+ if (raw === "html-selection" || raw === "html-element" || raw === "html-page") return raw;
12923
+ return "source";
12924
+ }
12925
+
12926
+ function isReviewNoteDomAnchor(note) {
12927
+ return Boolean(note && normalizeReviewNoteAnchorKind(note.anchorKind) !== "source");
12928
+ }
12929
+
12515
12930
  function normalizeReviewNote(note) {
12516
12931
  if (!note || typeof note !== "object") return null;
12517
12932
  const id = typeof note.id === "string" && note.id.trim() ? note.id : makeRequestId();
12518
12933
  const text = typeof note.text === "string" ? note.text : "";
12934
+ const anchorKind = normalizeReviewNoteAnchorKind(note.anchorKind);
12519
12935
  const createdAt = typeof note.createdAt === "number" && Number.isFinite(note.createdAt)
12520
12936
  ? note.createdAt
12521
12937
  : Date.now();
@@ -12545,6 +12961,11 @@
12545
12961
  lineEnd,
12546
12962
  selectedText: typeof note.selectedText === "string" ? note.selectedText : "",
12547
12963
  selectedDisplayText: typeof note.selectedDisplayText === "string" ? note.selectedDisplayText : "",
12964
+ anchorKind,
12965
+ htmlSelector: typeof note.htmlSelector === "string" ? note.htmlSelector : "",
12966
+ htmlTag: typeof note.htmlTag === "string" ? note.htmlTag : "",
12967
+ htmlLabel: typeof note.htmlLabel === "string" ? note.htmlLabel : "",
12968
+ htmlPreviewTitle: typeof note.htmlPreviewTitle === "string" ? note.htmlPreviewTitle : "",
12548
12969
  };
12549
12970
  }
12550
12971
 
@@ -13043,17 +13464,26 @@
13043
13464
  }
13044
13465
  }
13045
13466
 
13467
+ function formatHtmlReviewNoteAnchorLabel(note) {
13468
+ const kind = normalizeReviewNoteAnchorKind(note && note.anchorKind);
13469
+ const tag = String(note && note.htmlTag ? note.htmlTag : "").trim().toLowerCase();
13470
+ if (kind === "html-selection") return "HTML selection";
13471
+ if (kind === "html-page") return "HTML page";
13472
+ return tag ? ("HTML <" + tag + ">") : "HTML element";
13473
+ }
13474
+
13046
13475
  function summarizeReviewNoteAnchor(note) {
13476
+ if (isReviewNoteDomAnchor(note)) return formatHtmlReviewNoteAnchorLabel(note);
13047
13477
  const start = Math.max(1, Number(note && note.lineStart) || 1);
13048
13478
  const end = Math.max(start, Number(note && note.lineEnd) || start);
13049
13479
  return start === end ? "Line " + start : ("Lines " + start + "–" + end);
13050
13480
  }
13051
13481
 
13052
13482
  function summarizeReviewNoteQuote(note) {
13053
- const normalized = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
13483
+ const normalized = String(note && (note.selectedDisplayText || note.selectedText || note.htmlLabel || note.htmlSelector) ? (note.selectedDisplayText || note.selectedText || note.htmlLabel || note.htmlSelector) : "")
13054
13484
  .replace(/\s+/g, " ")
13055
13485
  .trim();
13056
- if (!normalized) return "Anchor: current line / empty selection";
13486
+ if (!normalized) return isReviewNoteDomAnchor(note) ? "Anchor: HTML preview" : "Anchor: current line / empty selection";
13057
13487
  return normalized.length > 140 ? normalized.slice(0, 137) + "…" : normalized;
13058
13488
  }
13059
13489
 
@@ -13141,6 +13571,7 @@
13141
13571
  }
13142
13572
 
13143
13573
  function resolveReviewNoteRange(note, text) {
13574
+ if (isReviewNoteDomAnchor(note)) return null;
13144
13575
  const source = String(text || "");
13145
13576
  const safeStart = Math.max(0, Math.min(Number(note && note.selectionStart) || 0, source.length));
13146
13577
  const safeEnd = Math.max(safeStart, Math.min(Number(note && note.selectionEnd) || safeStart, source.length));
@@ -13204,6 +13635,7 @@
13204
13635
  }
13205
13636
 
13206
13637
  function formatReviewNotePromptLineRange(bounds, note) {
13638
+ if (isReviewNoteDomAnchor(note)) return summarizeReviewNoteAnchor(note);
13207
13639
  const start = bounds ? bounds.lineStart : Math.max(1, Number(note && note.lineStart) || 1);
13208
13640
  const end = bounds ? bounds.lineEnd : Math.max(start, Number(note && note.lineEnd) || start);
13209
13641
  return start === end ? "L" + start : ("L" + start + "-L" + end);
@@ -13217,7 +13649,7 @@
13217
13649
  const descriptor = getCurrentStudioDocumentDescriptor();
13218
13650
  const documentLabel = descriptor && descriptor.label ? descriptor.label : (sourceState && sourceState.label ? sourceState.label : "Studio document");
13219
13651
  const parts = [
13220
- "Please address the following Studio comments. Use the file names and line numbers as anchors. The full document is not included here, only the comments and their anchors.",
13652
+ "Please address the following Studio comments. Use file names, line numbers, and preview anchors to locate each comment. The full document is not included here, only the comments and their anchors.",
13221
13653
  "Document: " + documentLabel,
13222
13654
  "",
13223
13655
  "## Comments",
@@ -13239,6 +13671,9 @@
13239
13671
  if (anchor) {
13240
13672
  parts.push("", "> " + anchor.replace(/\n/g, "\n> "));
13241
13673
  }
13674
+ if (isReviewNoteDomAnchor(note) && note.htmlSelector) {
13675
+ parts.push("", "Preview selector: `" + String(note.htmlSelector).replace(/`/g, "\\`") + "`");
13676
+ }
13242
13677
  parts.push("");
13243
13678
  });
13244
13679
 
@@ -13260,6 +13695,7 @@
13260
13695
  const source = String(text || "");
13261
13696
  const lineMap = new Map();
13262
13697
  for (const note of reviewNotes) {
13698
+ if (isReviewNoteDomAnchor(note)) continue;
13263
13699
  const bounds = getResolvedReviewNoteLineBounds(note, source);
13264
13700
  if (!bounds) continue;
13265
13701
  for (let line = bounds.lineStart; line <= bounds.lineEnd; line += 1) {
@@ -15798,8 +16234,8 @@
15798
16234
  return reviewNotes.slice().sort((left, right) => {
15799
16235
  const leftBounds = getResolvedReviewNoteLineBounds(left, source);
15800
16236
  const rightBounds = getResolvedReviewNoteLineBounds(right, source);
15801
- const leftLine = leftBounds ? leftBounds.lineStart : Math.max(1, Number(left && left.lineStart) || 1);
15802
- const rightLine = rightBounds ? rightBounds.lineStart : Math.max(1, Number(right && right.lineStart) || 1);
16237
+ const leftLine = leftBounds ? leftBounds.lineStart : (isReviewNoteDomAnchor(left) ? Number.MAX_SAFE_INTEGER : Math.max(1, Number(left && left.lineStart) || 1));
16238
+ const rightLine = rightBounds ? rightBounds.lineStart : (isReviewNoteDomAnchor(right) ? Number.MAX_SAFE_INTEGER : Math.max(1, Number(right && right.lineStart) || 1));
15803
16239
  if (leftLine !== rightLine) return leftLine - rightLine;
15804
16240
 
15805
16241
  const leftStart = leftBounds ? leftBounds.start : Math.max(0, Number(left && left.selectionStart) || 0);
@@ -15831,6 +16267,15 @@
15831
16267
  function getReviewNoteInlineState(note, text) {
15832
16268
  const source = String(text || "");
15833
16269
  const annotationBody = escapeReviewNoteAnnotationText(note && note.text);
16270
+ if (isReviewNoteDomAnchor(note)) {
16271
+ return {
16272
+ annotationBody,
16273
+ range: null,
16274
+ markerText: "",
16275
+ exists: false,
16276
+ canToggle: false,
16277
+ };
16278
+ }
15834
16279
  if (!annotationBody) {
15835
16280
  return {
15836
16281
  annotationBody: "",
@@ -16156,7 +16601,9 @@
16156
16601
  const jumpBtn = document.createElement("button");
16157
16602
  jumpBtn.type = "button";
16158
16603
  jumpBtn.textContent = "Jump";
16159
- jumpBtn.title = "Jump to this comment's anchored location in the editor.";
16604
+ jumpBtn.title = isReviewNoteDomAnchor(note)
16605
+ ? "Jump to this comment's HTML preview anchor."
16606
+ : "Jump to this comment's anchored location in the editor.";
16160
16607
  jumpBtn.addEventListener("click", () => {
16161
16608
  jumpToReviewNote(note.id);
16162
16609
  });
@@ -16169,9 +16616,11 @@
16169
16616
  convertBtn.textContent = inlineState.exists ? "Inline: On" : "Inline: Off";
16170
16617
  convertBtn.setAttribute("aria-pressed", inlineState.exists ? "true" : "false");
16171
16618
  convertBtn.disabled = !inlineState.canToggle || uiBusy;
16172
- convertBtn.title = inlineState.exists
16173
- ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
16174
- : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
16619
+ convertBtn.title = isReviewNoteDomAnchor(note)
16620
+ ? "Inline annotations are only available for comments anchored to source text."
16621
+ : (inlineState.exists
16622
+ ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
16623
+ : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.");
16175
16624
  convertBtn.addEventListener("click", () => {
16176
16625
  convertReviewNoteToAnnotation(note.id);
16177
16626
  });
@@ -16199,9 +16648,11 @@
16199
16648
  convertBtn.disabled = !nextInlineState.canToggle || uiBusy;
16200
16649
  convertBtn.textContent = nextInlineState.exists ? "Inline: On" : "Inline: Off";
16201
16650
  convertBtn.setAttribute("aria-pressed", nextInlineState.exists ? "true" : "false");
16202
- convertBtn.title = nextInlineState.exists
16203
- ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
16204
- : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
16651
+ convertBtn.title = isReviewNoteDomAnchor(note)
16652
+ ? "Inline annotations are only available for comments anchored to source text."
16653
+ : (nextInlineState.exists
16654
+ ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
16655
+ : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.");
16205
16656
  scheduleReviewNotesPersistence();
16206
16657
  updateReviewNotesUi();
16207
16658
  });
@@ -16273,6 +16724,55 @@
16273
16724
  return note;
16274
16725
  }
16275
16726
 
16727
+ function addReviewNoteFromHtmlArtifactTarget(record, data) {
16728
+ if (!record || !record.commentable) return null;
16729
+ const kind = data && data.kind === "selection" ? "html-selection" : "html-element";
16730
+ const selector = typeof data.selector === "string" ? data.selector : "";
16731
+ const tag = typeof data.tag === "string" ? data.tag : "";
16732
+ const text = typeof data.text === "string" ? data.text : "";
16733
+ const label = typeof data.label === "string" ? data.label : "";
16734
+ const display = text || label || selector || (tag ? ("<" + tag + ">") : "HTML element");
16735
+ return addReviewNoteFromAnchor({
16736
+ selectionStart: 0,
16737
+ selectionEnd: 0,
16738
+ lineStart: 1,
16739
+ lineEnd: 1,
16740
+ selectedText: "",
16741
+ selectedDisplayText: display,
16742
+ anchorKind: kind,
16743
+ htmlSelector: selector,
16744
+ htmlTag: tag,
16745
+ htmlLabel: label,
16746
+ htmlPreviewTitle: record.title || "HTML preview",
16747
+ }, {
16748
+ statusMessage: kind === "html-selection"
16749
+ ? "Added local comment from HTML preview selection."
16750
+ : "Added local comment from HTML preview element.",
16751
+ });
16752
+ }
16753
+
16754
+ function addReviewNoteFromHtmlArtifactPage(record) {
16755
+ if (!record || !record.commentable) {
16756
+ setStatus("HTML preview comments are only available for editor previews.", "warning");
16757
+ return null;
16758
+ }
16759
+ return addReviewNoteFromAnchor({
16760
+ selectionStart: 0,
16761
+ selectionEnd: 0,
16762
+ lineStart: 1,
16763
+ lineEnd: 1,
16764
+ selectedText: "",
16765
+ selectedDisplayText: record.title || "HTML preview",
16766
+ anchorKind: "html-page",
16767
+ htmlSelector: "",
16768
+ htmlTag: "",
16769
+ htmlLabel: record.title || "HTML preview",
16770
+ htmlPreviewTitle: record.title || "HTML preview",
16771
+ }, {
16772
+ statusMessage: "Added page-level local comment for HTML preview.",
16773
+ });
16774
+ }
16775
+
16276
16776
  function addReviewNoteFromAnchor(anchor, options) {
16277
16777
  if (!anchor || typeof anchor !== "object") return null;
16278
16778
  const note = normalizeReviewNote({
@@ -16286,6 +16786,11 @@
16286
16786
  lineEnd: anchor.lineEnd,
16287
16787
  selectedText: anchor.selectedText,
16288
16788
  selectedDisplayText: typeof anchor.selectedDisplayText === "string" ? anchor.selectedDisplayText : (typeof anchor.selectedText === "string" ? anchor.selectedText : ""),
16789
+ anchorKind: anchor.anchorKind,
16790
+ htmlSelector: anchor.htmlSelector,
16791
+ htmlTag: anchor.htmlTag,
16792
+ htmlLabel: anchor.htmlLabel,
16793
+ htmlPreviewTitle: anchor.htmlPreviewTitle,
16289
16794
  });
16290
16795
  if (!note) return null;
16291
16796
  if (editorSelectionCommentBtn) {
@@ -16433,9 +16938,55 @@
16433
16938
  return jumped;
16434
16939
  }
16435
16940
 
16941
+ function getConnectedHtmlArtifactRecords() {
16942
+ const records = [];
16943
+ htmlArtifactFramesById.forEach((record, id) => {
16944
+ if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) {
16945
+ if (id) htmlArtifactFramesById.delete(id);
16946
+ return;
16947
+ }
16948
+ records.push(record);
16949
+ });
16950
+ return records;
16951
+ }
16952
+
16953
+ function jumpToHtmlReviewNote(note) {
16954
+ if (!isReviewNoteDomAnchor(note)) return false;
16955
+ const records = getConnectedHtmlArtifactRecords().filter((record) => record && record.commentable);
16956
+ if (records.length === 0) {
16957
+ setStatus("Open the HTML preview before jumping to this comment.", "warning");
16958
+ return false;
16959
+ }
16960
+ const record = records[0];
16961
+ if (record.shell && typeof record.shell.scrollIntoView === "function") {
16962
+ try {
16963
+ record.shell.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
16964
+ } catch {
16965
+ try { record.shell.scrollIntoView(false); } catch {}
16966
+ }
16967
+ }
16968
+ try {
16969
+ record.iframe.contentWindow.postMessage({
16970
+ type: "pi-studio-html-artifact-highlight-comment",
16971
+ id: record.id || "",
16972
+ selector: note.htmlSelector || "",
16973
+ anchorKind: normalizeReviewNoteAnchorKind(note.anchorKind),
16974
+ }, "*");
16975
+ setStatus("Jumped to HTML preview comment anchor.", "success");
16976
+ return true;
16977
+ } catch {
16978
+ setStatus("Could not jump to this HTML preview comment.", "warning");
16979
+ return false;
16980
+ }
16981
+ }
16982
+
16436
16983
  function jumpToReviewNote(noteId) {
16437
16984
  const note = reviewNotes.find((entry) => entry && entry.id === noteId);
16438
16985
  if (!note) return;
16986
+ if (isReviewNoteDomAnchor(note)) {
16987
+ jumpToHtmlReviewNote(note);
16988
+ return;
16989
+ }
16439
16990
  jumpToReviewAnchor(note, {
16440
16991
  status: false,
16441
16992
  notFoundStatusMessage: "Could not find the anchored location for this comment.",
@@ -16850,6 +17401,8 @@
16850
17401
  const directIsStop = activeKind === "direct";
16851
17402
  const critiqueIsStop = activeKind === "critique";
16852
17403
  const canQueueSteering = studioRunChainActive && !critiqueIsStop;
17404
+ const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
17405
+ const showReplSend = rightView === "repl";
16853
17406
 
16854
17407
  if (isEditorOnlyMode) {
16855
17408
  if (sendRunBtn) {
@@ -16865,15 +17418,25 @@
16865
17418
  queueSteerBtn.title = "Queue steering is unavailable in editor-only mode.";
16866
17419
  }
16867
17420
  if (sendReplBtn) {
16868
- sendReplBtn.hidden = true;
16869
- sendReplBtn.disabled = true;
16870
- sendReplBtn.classList.remove("repl-primary-action");
17421
+ sendReplBtn.hidden = !showReplSend;
17422
+ sendReplBtn.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy || !hasReplSession;
17423
+ sendReplBtn.classList.toggle("repl-primary-action", showReplSend);
17424
+ sendReplBtn.textContent = showReplSend ? withStudioShortcutLabel(replSendMode === "literate" ? "Send selection/chunks" : "Send to REPL", "repl-send") : "Send to REPL";
17425
+ sendReplBtn.title = hasReplSession
17426
+ ? (replSendMode === "literate"
17427
+ ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
17428
+ : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
17429
+ : "Start or select a REPL session in the right pane first.";
16871
17430
  const replActionLine = sendReplBtn.closest(".repl-action-line");
16872
- if (replActionLine instanceof HTMLElement) replActionLine.hidden = true;
17431
+ if (replActionLine instanceof HTMLElement) replActionLine.hidden = !showReplSend;
16873
17432
  }
16874
17433
  if (replSendModeSelect) {
16875
- replSendModeSelect.hidden = true;
16876
- replSendModeSelect.disabled = true;
17434
+ replSendModeSelect.hidden = !showReplSend;
17435
+ replSendModeSelect.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy;
17436
+ replSendModeSelect.value = replSendMode;
17437
+ replSendModeSelect.title = replSendMode === "literate"
17438
+ ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
17439
+ : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
16877
17440
  }
16878
17441
  if (critiqueBtn) {
16879
17442
  critiqueBtn.textContent = "Critique text";
@@ -16915,9 +17478,7 @@
16915
17478
  : "Queue steering is available while Run editor text is active.";
16916
17479
  }
16917
17480
 
16918
- const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
16919
17481
  if (sendReplBtn) {
16920
- const showReplSend = rightView === "repl";
16921
17482
  sendReplBtn.hidden = !showReplSend;
16922
17483
  sendReplBtn.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy || !hasReplSession;
16923
17484
  sendReplBtn.classList.toggle("repl-primary-action", showReplSend);
@@ -18562,7 +19123,7 @@
18562
19123
  return;
18563
19124
  }
18564
19125
 
18565
- var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
19126
+ var suggestedName = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
18566
19127
  var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
18567
19128
  const suggested = sourceState.path || (suggestedDir + suggestedName);
18568
19129
  const path = window.prompt("Save editor content as:", suggested);
@@ -18617,7 +19178,7 @@
18617
19178
  if (refreshFromDiskBtn) {
18618
19179
  refreshFromDiskBtn.addEventListener("click", () => {
18619
19180
  if (!hasRefreshableFilePath()) {
18620
- setStatus("Refresh from disk is only available for file-backed documents.", "warning");
19181
+ 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");
18621
19182
  return;
18622
19183
  }
18623
19184
 
@@ -18632,6 +19193,7 @@
18632
19193
  const sent = sendMessage({
18633
19194
  type: "refresh_from_disk_request",
18634
19195
  requestId,
19196
+ path: sourceState.path,
18635
19197
  });
18636
19198
 
18637
19199
  if (!sent) {
@@ -19252,7 +19814,7 @@
19252
19814
  setEditorText(text, { preserveScroll: false, preserveSelection: false });
19253
19815
  setSourceState({
19254
19816
  source: "upload",
19255
- label: "upload: " + file.name,
19817
+ label: "imported copy: " + file.name,
19256
19818
  path: null,
19257
19819
  });
19258
19820
  refreshResponseUi();
@@ -19260,7 +19822,7 @@
19260
19822
  if (detectedLang) {
19261
19823
  setEditorLanguage(detectedLang);
19262
19824
  }
19263
- setStatus("Loaded file " + file.name + ".", "success");
19825
+ setStatus("Imported file copy: " + file.name + ".", "success");
19264
19826
  };
19265
19827
  reader.onerror = () => {
19266
19828
  setStatus("Failed to read file.", "error");