pi-studio 0.9.19 → 0.9.21

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.
@@ -92,7 +92,9 @@
92
92
  const copyResponseBtn = document.getElementById("copyResponseBtn");
93
93
  const exportPreviewControlsEl = document.getElementById("exportPreviewControls");
94
94
  const exportPreviewMenuEl = document.getElementById("exportPreviewMenu");
95
+ const exportPreviewPdfStudioBtn = document.getElementById("exportPreviewPdfStudioBtn");
95
96
  const exportPreviewPdfBtn = document.getElementById("exportPreviewPdfBtn");
97
+ const exportPreviewHtmlStudioBtn = document.getElementById("exportPreviewHtmlStudioBtn");
96
98
  const exportPreviewHtmlBtn = document.getElementById("exportPreviewHtmlBtn");
97
99
  const exportPdfBtn = document.getElementById("exportPdfBtn");
98
100
  const historyPrevBtn = document.getElementById("historyPrevBtn");
@@ -142,6 +144,8 @@
142
144
  const scratchpadDialogEl = document.getElementById("scratchpadDialog");
143
145
  const scratchpadTextEl = document.getElementById("scratchpadText");
144
146
  const scratchpadMetaEl = document.getElementById("scratchpadMeta");
147
+ const scratchpadRecentBtn = document.getElementById("scratchpadRecentBtn");
148
+ const scratchpadRecentPanelEl = document.getElementById("scratchpadRecentPanel");
145
149
  const scratchpadInsertBtn = document.getElementById("scratchpadInsertBtn");
146
150
  const scratchpadCopyBtn = document.getElementById("scratchpadCopyBtn");
147
151
  const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
@@ -229,6 +233,35 @@
229
233
  let stickyStudioKind = null;
230
234
  const pendingCompanionWindows = new Map();
231
235
  let initialDocumentApplied = false;
236
+ function normalizeRightViewValue(nextView) {
237
+ const raw = String(nextView || "").trim();
238
+ const normalized = raw === "preview"
239
+ ? "preview"
240
+ : (raw === "editor-preview"
241
+ ? "editor-preview"
242
+ : (raw === "repl"
243
+ ? "repl"
244
+ : (raw === "files"
245
+ ? "files"
246
+ : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown"))));
247
+ if (isEditorOnlyMode && normalized !== "editor-preview" && normalized !== "files" && normalized !== "repl") {
248
+ return "editor-preview";
249
+ }
250
+ return normalized;
251
+ }
252
+
253
+ function syncRightViewModeOptions() {
254
+ if (!rightViewSelect || !rightViewSelect.options) return;
255
+ const editorOnlyAllowed = new Set(["editor-preview", "files", "repl"]);
256
+ Array.from(rightViewSelect.options).forEach((option) => {
257
+ if (!option) return;
258
+ option.disabled = isEditorOnlyMode && !editorOnlyAllowed.has(option.value);
259
+ });
260
+ rightViewSelect.title = isEditorOnlyMode
261
+ ? "Editor-only views: editor preview, Files, or REPL. Shortcut: F7 when the right pane is active; F6 switches panes."
262
+ : "Right pane view mode. Shortcut: F7 when the right pane is active; F6 switches panes.";
263
+ }
264
+
232
265
  function getInitialRightView(source) {
233
266
  if (isEditorOnlyMode) return "editor-preview";
234
267
  return String(source || "").trim() === "file" ? "editor-preview" : "preview";
@@ -2015,6 +2048,9 @@
2015
2048
  let scratchpadReturnFocusEl = null;
2016
2049
  let scratchpadPersistTimer = null;
2017
2050
  let scratchpadLoadNonce = 0;
2051
+ let scratchpadRecentEntries = [];
2052
+ let scratchpadRecentVisible = false;
2053
+ let scratchpadRecentLoading = false;
2018
2054
  let reviewNotes = [];
2019
2055
  let reviewNotesReturnFocusEl = null;
2020
2056
  let reviewNotesPersistTimer = null;
@@ -2427,11 +2463,7 @@
2427
2463
  rightTitleGroupEl.appendChild(rightFocusBtn);
2428
2464
  rightTitleGroupEl.appendChild(makeStudioUiRefreshSeparator());
2429
2465
  }
2430
- if (isEditorOnlyMode) {
2431
- rightTitleGroupEl.appendChild(makeStudioUiRefreshElement("span", "studio-refresh-static-title", "Editor (Preview)"));
2432
- } else {
2433
- rightTitleGroupEl.appendChild(rightViewSelect);
2434
- }
2466
+ rightTitleGroupEl.appendChild(rightViewSelect);
2435
2467
  rightIdentityEl.appendChild(rightTitleGroupEl);
2436
2468
  const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
2437
2469
  if (exportPreviewControlsEl) {
@@ -2455,8 +2487,8 @@
2455
2487
  if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2456
2488
  const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2457
2489
  replActionLineEl.hidden = true;
2458
- if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2459
- if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2490
+ if (sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2491
+ if (replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2460
2492
  if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
2461
2493
  actionsEl.appendChild(actionLineTwoEl);
2462
2494
  if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
@@ -2612,7 +2644,7 @@
2612
2644
 
2613
2645
  function getIdleStatus() {
2614
2646
  if (isEditorOnlyMode) {
2615
- return "Editor-only mode: edit, load, annotate, preview, save, suggest, or refresh file-backed text.";
2647
+ return "Editor-only mode: edit, browse files, annotate, preview, save, suggest, refresh file-backed text, or send to a REPL.";
2616
2648
  }
2617
2649
  return "Edit, load, or annotate text, then run, save, send to pi editor, or critique.";
2618
2650
  }
@@ -3530,7 +3562,7 @@
3530
3562
 
3531
3563
  function cycleActivePaneView(direction) {
3532
3564
  if (activePane === "right") {
3533
- if (isEditorOnlyMode || !rightViewSelect || rightViewSelect.disabled) {
3565
+ if (!rightViewSelect || rightViewSelect.disabled) {
3534
3566
  setStatus("The right-pane view selector is unavailable.", "warning");
3535
3567
  return;
3536
3568
  }
@@ -3886,7 +3918,6 @@
3886
3918
  && !event.altKey
3887
3919
  && event.shiftKey
3888
3920
  && activePane === "left"
3889
- && !isEditorOnlyMode
3890
3921
  && rightView === "repl"
3891
3922
  ) {
3892
3923
  event.preventDefault();
@@ -4953,6 +4984,132 @@
4953
4984
  + " });\n"
4954
4985
  + " scheduleHeight();\n"
4955
4986
  + " }\n"
4987
+ + " let htmlCommentMode = false;\n"
4988
+ + " let htmlCommentHoverEl = null;\n"
4989
+ + " let htmlCommentHighlightTimer = null;\n"
4990
+ + " let htmlCommentLastPostAt = 0;\n"
4991
+ + " function htmlCommentCssEscape(value) {\n"
4992
+ + " const text = String(value || '');\n"
4993
+ + " try { if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text); } catch {}\n"
4994
+ + " return text.replace(/[^A-Za-z0-9_-]/g, function(ch) { return '\\\\' + ch; });\n"
4995
+ + " }\n"
4996
+ + " function getHtmlCommentSelector(element) {\n"
4997
+ + " if (!element || element.nodeType !== 1) return '';\n"
4998
+ + " if (element.id) return '#' + htmlCommentCssEscape(element.id);\n"
4999
+ + " const parts = [];\n"
5000
+ + " let el = element;\n"
5001
+ + " while (el && el.nodeType === 1 && el !== document.documentElement) {\n"
5002
+ + " const tag = el.tagName ? el.tagName.toLowerCase() : '';\n"
5003
+ + " if (!tag) break;\n"
5004
+ + " if (el.id) { parts.unshift(tag + '#' + htmlCommentCssEscape(el.id)); break; }\n"
5005
+ + " let index = 1;\n"
5006
+ + " let sibling = el.previousElementSibling;\n"
5007
+ + " while (sibling) { if ((sibling.tagName || '').toLowerCase() === tag) index += 1; sibling = sibling.previousElementSibling; }\n"
5008
+ + " parts.unshift(tag + ':nth-of-type(' + index + ')');\n"
5009
+ + " if (tag === 'body') break;\n"
5010
+ + " el = el.parentElement;\n"
5011
+ + " }\n"
5012
+ + " return parts.join(' > ');\n"
5013
+ + " }\n"
5014
+ + " function normalizeHtmlCommentText(value, maxLength) {\n"
5015
+ + " const text = String(value || '').replace(/\\s+/g, ' ').trim();\n"
5016
+ + " const limit = Math.max(24, Number(maxLength) || 200);\n"
5017
+ + " return text.length > limit ? text.slice(0, limit - 1).trimEnd() + '…' : text;\n"
5018
+ + " }\n"
5019
+ + " function getHtmlCommentElementLabel(element) {\n"
5020
+ + " if (!element || element.nodeType !== 1) return '';\n"
5021
+ + " const attrText = element.getAttribute('aria-label') || element.getAttribute('alt') || element.getAttribute('title') || '';\n"
5022
+ + " if (attrText) return normalizeHtmlCommentText(attrText, 220);\n"
5023
+ + " const tag = (element.tagName || '').toLowerCase();\n"
5024
+ + " if (tag === 'img') {\n"
5025
+ + " const src = String(element.getAttribute('src') || '').split(/[?#]/)[0].split('/').pop() || 'image';\n"
5026
+ + " return normalizeHtmlCommentText(src, 220);\n"
5027
+ + " }\n"
5028
+ + " return normalizeHtmlCommentText(element.innerText || element.textContent || '', 220);\n"
5029
+ + " }\n"
5030
+ + " function getHtmlCommentTarget(target) {\n"
5031
+ + " let node = target;\n"
5032
+ + " if (node && node.nodeType === 3) node = node.parentElement;\n"
5033
+ + " if (!node || node.nodeType !== 1) return document.body || document.documentElement;\n"
5034
+ + " if (typeof node.closest === 'function') {\n"
5035
+ + " 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"
5036
+ + " }\n"
5037
+ + " return node;\n"
5038
+ + " }\n"
5039
+ + " function getHtmlCommentSelectionText() {\n"
5040
+ + " const selection = typeof window.getSelection === 'function' ? window.getSelection() : null;\n"
5041
+ + " if (!selection || selection.rangeCount <= 0 || selection.isCollapsed) return '';\n"
5042
+ + " return normalizeHtmlCommentText(selection.toString(), 1000);\n"
5043
+ + " }\n"
5044
+ + " function getHtmlCommentSelectionElement() {\n"
5045
+ + " const selection = typeof window.getSelection === 'function' ? window.getSelection() : null;\n"
5046
+ + " if (!selection || selection.rangeCount <= 0) return null;\n"
5047
+ + " const range = selection.getRangeAt(0);\n"
5048
+ + " let node = range.commonAncestorContainer;\n"
5049
+ + " if (node && node.nodeType === 3) node = node.parentElement;\n"
5050
+ + " return node && node.nodeType === 1 ? node : null;\n"
5051
+ + " }\n"
5052
+ + " function postHtmlCommentTarget(kind, element, event, selectedText) {\n"
5053
+ + " const target = getHtmlCommentTarget(element || (event && event.target));\n"
5054
+ + " if (!target) return false;\n"
5055
+ + " htmlCommentLastPostAt = Date.now();\n"
5056
+ + " try {\n"
5057
+ + " parent.postMessage({\n"
5058
+ + " type: 'pi-studio-html-artifact-comment-target',\n"
5059
+ + " id: PREVIEW_ID,\n"
5060
+ + " kind: kind === 'selection' ? 'selection' : 'element',\n"
5061
+ + " selector: getHtmlCommentSelector(target),\n"
5062
+ + " tag: (target.tagName || '').toLowerCase(),\n"
5063
+ + " label: getHtmlCommentElementLabel(target),\n"
5064
+ + " text: normalizeHtmlCommentText(selectedText || '', 1000),\n"
5065
+ + " clientX: event && event.clientX || 0,\n"
5066
+ + " clientY: event && event.clientY || 0\n"
5067
+ + " }, '*');\n"
5068
+ + " return true;\n"
5069
+ + " } catch { return false; }\n"
5070
+ + " }\n"
5071
+ + " function clearHtmlCommentHover() {\n"
5072
+ + " if (htmlCommentHoverEl && htmlCommentHoverEl.classList) htmlCommentHoverEl.classList.remove('pi-studio-html-comment-hover');\n"
5073
+ + " htmlCommentHoverEl = null;\n"
5074
+ + " }\n"
5075
+ + " function setHtmlCommentMode(enabled) {\n"
5076
+ + " htmlCommentMode = Boolean(enabled);\n"
5077
+ + " if (document.documentElement && document.documentElement.classList) document.documentElement.classList.toggle('pi-studio-html-comment-mode', htmlCommentMode);\n"
5078
+ + " if (!htmlCommentMode) clearHtmlCommentHover();\n"
5079
+ + " }\n"
5080
+ + " function handleHtmlCommentMouseMove(event) {\n"
5081
+ + " if (!htmlCommentMode) return;\n"
5082
+ + " const target = getHtmlCommentTarget(event && event.target);\n"
5083
+ + " if (target === htmlCommentHoverEl) return;\n"
5084
+ + " clearHtmlCommentHover();\n"
5085
+ + " htmlCommentHoverEl = target;\n"
5086
+ + " if (htmlCommentHoverEl && htmlCommentHoverEl.classList) htmlCommentHoverEl.classList.add('pi-studio-html-comment-hover');\n"
5087
+ + " }\n"
5088
+ + " function handleHtmlCommentMouseUp(event) {\n"
5089
+ + " if (!htmlCommentMode) return;\n"
5090
+ + " const selectedText = getHtmlCommentSelectionText();\n"
5091
+ + " if (!selectedText) return;\n"
5092
+ + " postHtmlCommentTarget('selection', getHtmlCommentSelectionElement() || (event && event.target), event, selectedText);\n"
5093
+ + " if (event) { event.preventDefault(); event.stopPropagation(); }\n"
5094
+ + " }\n"
5095
+ + " function handleHtmlCommentClick(event) {\n"
5096
+ + " if (!htmlCommentMode) return;\n"
5097
+ + " if (Date.now() - htmlCommentLastPostAt < 450) { event.preventDefault(); event.stopPropagation(); return; }\n"
5098
+ + " postHtmlCommentTarget('element', event && event.target, event, '');\n"
5099
+ + " event.preventDefault();\n"
5100
+ + " event.stopPropagation();\n"
5101
+ + " }\n"
5102
+ + " function highlightHtmlCommentTarget(selector, anchorKind) {\n"
5103
+ + " if (htmlCommentHighlightTimer) { clearTimeout(htmlCommentHighlightTimer); htmlCommentHighlightTimer = null; }\n"
5104
+ + " Array.prototype.slice.call(document.querySelectorAll('.pi-studio-html-comment-highlight')).forEach(function(el) { el.classList.remove('pi-studio-html-comment-highlight'); });\n"
5105
+ + " if (anchorKind === 'html-page' || !selector) { try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch { window.scrollTo(0, 0); } return; }\n"
5106
+ + " let target = null;\n"
5107
+ + " try { target = document.querySelector(String(selector || '')); } catch {}\n"
5108
+ + " if (!target) return;\n"
5109
+ + " try { target.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); } catch { try { target.scrollIntoView(true); } catch {} }\n"
5110
+ + " if (target.classList) target.classList.add('pi-studio-html-comment-highlight');\n"
5111
+ + " htmlCommentHighlightTimer = setTimeout(function() { if (target && target.classList) target.classList.remove('pi-studio-html-comment-highlight'); }, 2400);\n"
5112
+ + " }\n"
4956
5113
  + " window.addEventListener('message', (event) => {\n"
4957
5114
  + " const data = event && event.data;\n"
4958
5115
  + " if (!data || typeof data !== 'object' || data.id !== PREVIEW_ID) return;\n"
@@ -4966,11 +5123,23 @@
4966
5123
  + " }\n"
4967
5124
  + " if (data.type === 'pi-studio-html-artifact-resources-resolved') {\n"
4968
5125
  + " applyResolvedHtmlPreviewResources(data.results);\n"
5126
+ + " return;\n"
5127
+ + " }\n"
5128
+ + " if (data.type === 'pi-studio-html-artifact-comment-mode') {\n"
5129
+ + " setHtmlCommentMode(data.enabled);\n"
5130
+ + " return;\n"
5131
+ + " }\n"
5132
+ + " if (data.type === 'pi-studio-html-artifact-highlight-comment') {\n"
5133
+ + " highlightHtmlCommentTarget(data.selector, data.anchorKind);\n"
4969
5134
  + " }\n"
4970
5135
  + " });\n"
4971
5136
  + " document.addEventListener('click', handleFragmentAnchorClick);\n"
4972
5137
  + " document.addEventListener('click', handleHtmlPreviewLocalLinkClick);\n"
4973
5138
  + " document.addEventListener('contextmenu', handleHtmlPreviewLocalLinkContextMenu);\n"
5139
+ + " document.addEventListener('mousemove', handleHtmlCommentMouseMove, true);\n"
5140
+ + " document.addEventListener('mouseleave', clearHtmlCommentHover, true);\n"
5141
+ + " document.addEventListener('mouseup', handleHtmlCommentMouseUp, true);\n"
5142
+ + " document.addEventListener('click', handleHtmlCommentClick, true);\n"
4974
5143
  + " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4975
5144
  + " window.addEventListener('hashchange', () => {\n"
4976
5145
  + " const hash = String(window.location && window.location.hash || '');\n"
@@ -5002,6 +5171,9 @@
5002
5171
  + ".pi-studio-html-math-display{display:block;margin:0.75em 0;overflow-x:auto;text-align:center;}\n"
5003
5172
  + ".pi-studio-html-math-display>math{display:block;margin:0 auto;}\n"
5004
5173
  + ".pi-studio-html-math-inline>math{vertical-align:-0.15em;}\n"
5174
+ + "html.pi-studio-html-comment-mode,html.pi-studio-html-comment-mode body{cursor:crosshair!important;}\n"
5175
+ + ".pi-studio-html-comment-hover{outline:2px solid #0f8b8d!important;outline-offset:3px!important;}\n"
5176
+ + ".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"
5005
5177
  + "</style>\n";
5006
5178
  }
5007
5179
 
@@ -5036,6 +5208,11 @@
5036
5208
  });
5037
5209
  }
5038
5210
 
5211
+ function setHtmlArtifactDetailText(record, text) {
5212
+ if (!record || !record.detail) return;
5213
+ record.detail.textContent = record.commentMode ? "HTML preview · comment mode" : (text || "HTML preview");
5214
+ }
5215
+
5039
5216
  function handleHtmlArtifactFrameSizeMessage(event) {
5040
5217
  const data = event && event.data;
5041
5218
  if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-size") return;
@@ -5047,7 +5224,7 @@
5047
5224
  }
5048
5225
  if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
5049
5226
  if (record.shell && record.shell.classList && record.shell.classList.contains("is-focused")) {
5050
- if (record.detail) record.detail.textContent = "HTML preview";
5227
+ setHtmlArtifactDetailText(record, "HTML preview");
5051
5228
  return;
5052
5229
  }
5053
5230
  const rawHeight = Number(data.height);
@@ -5064,9 +5241,7 @@
5064
5241
  record.shell.style.minHeight = "0";
5065
5242
  record.shell.classList.toggle("is-height-capped", capped);
5066
5243
  }
5067
- if (record.detail) {
5068
- record.detail.textContent = "HTML preview";
5069
- }
5244
+ setHtmlArtifactDetailText(record, "HTML preview");
5070
5245
  }
5071
5246
 
5072
5247
  function handleHtmlArtifactFrameFragmentMessage(event) {
@@ -5190,7 +5365,7 @@
5190
5365
  error: error && error.message ? error.message : String(error || "HTML preview math render failed."),
5191
5366
  })));
5192
5367
  } finally {
5193
- if (record.detail) record.detail.textContent = "HTML preview";
5368
+ setHtmlArtifactDetailText(record, "HTML preview");
5194
5369
  }
5195
5370
  }
5196
5371
 
@@ -5280,7 +5455,7 @@
5280
5455
  if (record.detail) record.detail.textContent = "HTML preview · loading local images";
5281
5456
  const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
5282
5457
  postHtmlArtifactResourceResults(record, results);
5283
- if (record.detail) record.detail.textContent = "HTML preview";
5458
+ setHtmlArtifactDetailText(record, "HTML preview");
5284
5459
  }
5285
5460
 
5286
5461
  function handleHtmlArtifactFrameResourceMessage(event) {
@@ -5363,11 +5538,36 @@
5363
5538
  setStatus("Right-click this local HTML preview link for file actions.", "warning");
5364
5539
  }
5365
5540
 
5541
+ function handleHtmlArtifactFrameCommentTargetMessage(event) {
5542
+ const data = event && event.data;
5543
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-comment-target") return;
5544
+ const id = typeof data.id === "string" ? data.id : "";
5545
+ const record = id ? htmlArtifactFramesById.get(id) : null;
5546
+ if (!record || !record.iframe || !record.iframe.isConnected) {
5547
+ if (id) htmlArtifactFramesById.delete(id);
5548
+ return;
5549
+ }
5550
+ if (!record.commentable) return;
5551
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
5552
+ const note = addReviewNoteFromHtmlArtifactTarget(record, data);
5553
+ if (note && record.iframe && record.iframe.contentWindow) {
5554
+ try {
5555
+ record.iframe.contentWindow.postMessage({
5556
+ type: "pi-studio-html-artifact-highlight-comment",
5557
+ id: record.id || "",
5558
+ selector: note.htmlSelector || "",
5559
+ anchorKind: note.anchorKind || "html-element",
5560
+ }, "*");
5561
+ } catch {}
5562
+ }
5563
+ }
5564
+
5366
5565
  window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
5367
5566
  window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
5368
5567
  window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
5369
5568
  window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
5370
5569
  window.addEventListener("message", handleHtmlArtifactFrameLocalLinkMessage);
5570
+ window.addEventListener("message", handleHtmlArtifactFrameCommentTargetMessage);
5371
5571
 
5372
5572
  function isStudioHtmlFocusOpen() {
5373
5573
  return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
@@ -5595,6 +5795,36 @@
5595
5795
 
5596
5796
  const tools = document.createElement("span");
5597
5797
  tools.className = "studio-html-artifact-tools";
5798
+ const commentable = Boolean(options && options.commentable);
5799
+
5800
+ let commentBtn = null;
5801
+ let pageCommentBtn = null;
5802
+ const makeCommentButton = (text, title, onClick) => {
5803
+ const button = document.createElement("button");
5804
+ button.type = "button";
5805
+ button.className = "studio-html-artifact-comment-btn";
5806
+ button.textContent = text;
5807
+ button.title = title;
5808
+ button.addEventListener("pointerdown", (event) => { event.stopPropagation(); });
5809
+ button.addEventListener("mousedown", (event) => { event.stopPropagation(); });
5810
+ button.addEventListener("click", (event) => {
5811
+ event.preventDefault();
5812
+ event.stopPropagation();
5813
+ onClick();
5814
+ });
5815
+ return button;
5816
+ };
5817
+ if (commentable) {
5818
+ commentBtn = makeCommentButton("Comment mode", "Turn on HTML preview comment mode. Select text or click an element in the preview to add a local comment.", () => {
5819
+ const record = htmlArtifactFramesById.get(previewId);
5820
+ setHtmlArtifactRecordCommentMode(record, !(record && record.commentMode));
5821
+ });
5822
+ commentBtn.setAttribute("aria-pressed", "false");
5823
+ pageCommentBtn = makeCommentButton("Page", "Add a page-level local comment for this HTML preview.", () => {
5824
+ const record = htmlArtifactFramesById.get(previewId);
5825
+ addReviewNoteFromHtmlArtifactPage(record || null);
5826
+ });
5827
+ }
5598
5828
 
5599
5829
  const zoomControls = document.createElement("span");
5600
5830
  zoomControls.className = "studio-html-artifact-zoom-controls";
@@ -5658,6 +5888,8 @@
5658
5888
  fullscreenBtn.appendChild(makeStudioUiRefreshIcon("fullscreen"));
5659
5889
  updateZoomUi();
5660
5890
  tools.appendChild(detail);
5891
+ if (commentBtn) tools.appendChild(commentBtn);
5892
+ if (pageCommentBtn) tools.appendChild(pageCommentBtn);
5661
5893
  tools.appendChild(zoomControls);
5662
5894
  tools.appendChild(fullscreenBtn);
5663
5895
 
@@ -5672,7 +5904,7 @@
5672
5904
  iframe.referrerPolicy = "no-referrer";
5673
5905
  iframe.setAttribute("sandbox", "allow-scripts allow-modals");
5674
5906
  iframe.setAttribute("allow", "clipboard-write");
5675
- iframe.addEventListener("load", () => { postArtifactZoom(); });
5907
+ iframe.addEventListener("load", () => { postArtifactZoom(); postHtmlArtifactCommentMode(htmlArtifactFramesById.get(previewId)); });
5676
5908
  iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
5677
5909
  shell.appendChild(iframe);
5678
5910
  htmlArtifactFramesById.set(previewId, {
@@ -5681,6 +5913,11 @@
5681
5913
  shell,
5682
5914
  detail,
5683
5915
  zoomControls,
5916
+ commentBtn,
5917
+ pageCommentBtn,
5918
+ commentMode: false,
5919
+ commentable,
5920
+ title,
5684
5921
  sourcePath: options && options.sourcePath ? String(options.sourcePath) : "",
5685
5922
  resourceDir: options && options.resourceDir ? String(options.resourceDir) : "",
5686
5923
  mathRenderBatchCount: 0,
@@ -5697,6 +5934,33 @@
5697
5934
  }
5698
5935
  }
5699
5936
 
5937
+ function postHtmlArtifactCommentMode(record) {
5938
+ if (!record || !record.iframe || !record.iframe.contentWindow) return;
5939
+ try {
5940
+ record.iframe.contentWindow.postMessage({
5941
+ type: "pi-studio-html-artifact-comment-mode",
5942
+ id: record.id || "",
5943
+ enabled: Boolean(record.commentMode),
5944
+ }, "*");
5945
+ } catch {}
5946
+ }
5947
+
5948
+ function setHtmlArtifactRecordCommentMode(record, enabled) {
5949
+ if (!record) return;
5950
+ record.commentMode = Boolean(enabled);
5951
+ if (record.shell && record.shell.classList) record.shell.classList.toggle("is-comment-mode", record.commentMode);
5952
+ if (record.commentBtn) {
5953
+ record.commentBtn.classList.toggle("is-active", record.commentMode);
5954
+ record.commentBtn.setAttribute("aria-pressed", record.commentMode ? "true" : "false");
5955
+ record.commentBtn.textContent = "Comment mode";
5956
+ record.commentBtn.title = record.commentMode
5957
+ ? "HTML comment mode is on. Select text or click an element in the preview to add a local comment."
5958
+ : "Turn on HTML preview comment mode. Select text or click an element in the preview to add a local comment.";
5959
+ }
5960
+ if (record.detail) record.detail.textContent = record.commentMode ? "HTML preview · comment mode" : "HTML preview";
5961
+ postHtmlArtifactCommentMode(record);
5962
+ }
5963
+
5700
5964
  function getRightPaneHtmlArtifactSource() {
5701
5965
  if (rightView === "editor-preview") {
5702
5966
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
@@ -5814,6 +6078,15 @@
5814
6078
  openLink.textContent = "Open PDF";
5815
6079
  actions.appendChild(openLink);
5816
6080
 
6081
+ const refreshBtn = document.createElement("button");
6082
+ refreshBtn.type = "button";
6083
+ refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
6084
+ refreshBtn.textContent = "Refresh";
6085
+ refreshBtn.title = "Reload this PDF preview from disk.";
6086
+ refreshBtn.setAttribute("aria-label", "Refresh PDF preview from disk");
6087
+ refreshBtn.addEventListener("click", () => refreshStudioPdfFocusViewer());
6088
+ actions.appendChild(refreshBtn);
6089
+
5817
6090
  const fullscreenBtn = document.createElement("button");
5818
6091
  fullscreenBtn.type = "button";
5819
6092
  fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
@@ -5935,6 +6208,70 @@
5935
6208
  return "/pdf-resource?" + params.toString();
5936
6209
  }
5937
6210
 
6211
+ function buildRefreshedStudioPdfViewerUrl(value) {
6212
+ const raw = String(value || "").trim();
6213
+ if (!raw) return "";
6214
+ const hashIndex = raw.indexOf("#");
6215
+ const base = hashIndex >= 0 ? raw.slice(0, hashIndex) : raw;
6216
+ const hash = hashIndex >= 0 ? raw.slice(hashIndex) : "";
6217
+ const nonce = Date.now().toString(36);
6218
+ try {
6219
+ const url = new URL(base || window.location.href, window.location.href);
6220
+ url.searchParams.set("_studioPdfRefresh", nonce);
6221
+ return url.href + hash;
6222
+ } catch {
6223
+ const separator = base.indexOf("?") >= 0 ? "&" : "?";
6224
+ return base + separator + "_studioPdfRefresh=" + encodeURIComponent(nonce) + hash;
6225
+ }
6226
+ }
6227
+
6228
+ function syncStudioPdfCardViewerUrl(card, viewerUrl) {
6229
+ if (!card) return;
6230
+ const nextUrl = String(viewerUrl || "").trim();
6231
+ if (!nextUrl) return;
6232
+ if (card.dataset) card.dataset.studioPdfViewerUrl = nextUrl;
6233
+ const frame = typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
6234
+ if (frame) frame.src = nextUrl;
6235
+ const openLink = typeof card.querySelector === "function" ? card.querySelector("a.studio-pdf-card-link") : null;
6236
+ if (openLink) openLink.href = nextUrl;
6237
+ const focusBtn = typeof card.querySelector === "function" ? card.querySelector("button.studio-pdf-card-focus") : null;
6238
+ if (focusBtn && focusBtn.dataset) focusBtn.dataset.studioPdfViewerUrl = nextUrl;
6239
+ }
6240
+
6241
+ function refreshStudioPdfCard(card) {
6242
+ if (!card) return false;
6243
+ const frame = typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
6244
+ const currentUrl = String(card.dataset && card.dataset.studioPdfViewerUrl ? card.dataset.studioPdfViewerUrl : "").trim()
6245
+ || String(frame && frame.src ? frame.src : "").trim();
6246
+ const nextUrl = buildRefreshedStudioPdfViewerUrl(currentUrl);
6247
+ if (!nextUrl) return false;
6248
+ syncStudioPdfCardViewerUrl(card, nextUrl);
6249
+ setStatus("Refreshed PDF preview from disk.", "success");
6250
+ return true;
6251
+ }
6252
+
6253
+ function getStudioPdfFocusActiveFrame() {
6254
+ if (studioPdfFocusMovedFrameState && studioPdfFocusMovedFrameState.frame && studioPdfFocusMovedFrameState.frame.isConnected) {
6255
+ return studioPdfFocusMovedFrameState.frame;
6256
+ }
6257
+ return studioPdfFocusFrameEl;
6258
+ }
6259
+
6260
+ function refreshStudioPdfFocusViewer() {
6261
+ const frame = getStudioPdfFocusActiveFrame();
6262
+ const currentUrl = String(frame && frame.src ? frame.src : "").trim()
6263
+ || String(studioPdfFocusOpenLinkEl && studioPdfFocusOpenLinkEl.href ? studioPdfFocusOpenLinkEl.href : "").trim();
6264
+ const nextUrl = buildRefreshedStudioPdfViewerUrl(currentUrl);
6265
+ if (!nextUrl) {
6266
+ setStatus("Could not refresh this PDF preview.", "warning");
6267
+ return false;
6268
+ }
6269
+ if (frame) frame.src = nextUrl;
6270
+ if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = nextUrl;
6271
+ setStatus("Refreshed PDF preview from disk.", "success");
6272
+ return true;
6273
+ }
6274
+
5938
6275
  function syncStudioPdfFocusFullscreenButton() {
5939
6276
  if (!studioPdfFocusFullscreenBtn) return;
5940
6277
  const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
@@ -6465,6 +6802,18 @@
6465
6802
  openLink.textContent = "Open PDF";
6466
6803
  actions.appendChild(openLink);
6467
6804
 
6805
+ const refreshBtn = document.createElement("button");
6806
+ refreshBtn.type = "button";
6807
+ refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
6808
+ refreshBtn.textContent = "Refresh";
6809
+ refreshBtn.title = "Reload this PDF preview from disk.";
6810
+ refreshBtn.addEventListener("click", (event) => {
6811
+ event.preventDefault();
6812
+ event.stopPropagation();
6813
+ if (!refreshStudioPdfCard(card)) setStatus("Could not refresh this PDF preview.", "warning");
6814
+ });
6815
+ actions.appendChild(refreshBtn);
6816
+
6468
6817
  header.appendChild(actions);
6469
6818
  }
6470
6819
  card.appendChild(header);
@@ -7598,14 +7947,32 @@
7598
7947
  }
7599
7948
  }
7600
7949
 
7601
- async function exportRightPanePdf() {
7950
+ function getStudioPdfViewerUrlForExportPayload(payload) {
7951
+ if (!payload || typeof payload !== "object") return "";
7952
+ const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
7953
+ if (exportPath) {
7954
+ const resourceUrl = buildStudioPdfResourceUrl({ path: exportPath, resourceDir: exportPath.split(/[\\/]/).slice(0, -1).join("/") }, false);
7955
+ if (resourceUrl) return resourceUrl;
7956
+ }
7957
+ return typeof payload.downloadUrl === "string" ? payload.downloadUrl : "";
7958
+ }
7959
+
7960
+ async function exportRightPanePdf(options) {
7961
+ const exportOptions = options && typeof options === "object" ? options : {};
7962
+ const openTarget = exportOptions.openTarget === "studio" ? "studio" : "default";
7963
+ let studioPopup = null;
7964
+ if (openTarget === "studio") {
7965
+ studioPopup = openExportStudioPlaceholderWindow("PDF");
7966
+ }
7602
7967
  if (uiBusy || previewExportInProgress) {
7968
+ closeExportStudioWindow(studioPopup);
7603
7969
  setStatus("Studio is busy.", "warning");
7604
7970
  return;
7605
7971
  }
7606
7972
 
7607
7973
  const token = getToken();
7608
7974
  if (!token) {
7975
+ closeExportStudioWindow(studioPopup);
7609
7976
  setStatus("Missing Studio token in URL. Re-run /studio.", "error");
7610
7977
  return;
7611
7978
  }
@@ -7613,17 +7980,20 @@
7613
7980
  const exportingReplJournal = rightView === "repl";
7614
7981
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
7615
7982
  if (!rightPaneShowsPreview && !exportingReplJournal) {
7983
+ closeExportStudioWindow(studioPopup);
7616
7984
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
7617
7985
  return;
7618
7986
  }
7619
7987
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
7620
7988
  if (exportingReplJournal && !replJournalExportEntries.length) {
7989
+ closeExportStudioWindow(studioPopup);
7621
7990
  setStatus("No Studio REPL record entries to export for this session yet.", "warning");
7622
7991
  return;
7623
7992
  }
7624
7993
 
7625
7994
  const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
7626
7995
  if (htmlArtifactSource) {
7996
+ closeExportStudioWindow(studioPopup);
7627
7997
  setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
7628
7998
  return;
7629
7999
  }
@@ -7634,6 +8004,7 @@
7634
8004
  ? prepareEditorTextForPdfExport(sourceTextEl.value)
7635
8005
  : prepareEditorTextForPreview(latestResponseMarkdown));
7636
8006
  if (!markdown || !markdown.trim()) {
8007
+ closeExportStudioWindow(studioPopup);
7637
8008
  setStatus("Nothing to export yet.", "warning");
7638
8009
  return;
7639
8010
  }
@@ -7656,7 +8027,7 @@
7656
8027
 
7657
8028
  previewExportInProgress = true;
7658
8029
  updateResultActionButtons();
7659
- setStatus("Exporting PDF…", "warning");
8030
+ setStatus(openTarget === "studio" ? "Exporting PDF for Studio…" : "Exporting PDF…", "warning");
7660
8031
 
7661
8032
  try {
7662
8033
  const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
@@ -7671,6 +8042,7 @@
7671
8042
  isLatex: isLatex,
7672
8043
  editorPdfLanguage: editorPdfLanguage,
7673
8044
  filenameHint: filenameHint,
8045
+ openTarget: openTarget,
7674
8046
  }),
7675
8047
  }, PDF_EXPORT_FETCH_TIMEOUT_MS, "PDF export");
7676
8048
 
@@ -7709,6 +8081,35 @@
7709
8081
  downloadName += ".pdf";
7710
8082
  }
7711
8083
 
8084
+ if (openTarget === "studio") {
8085
+ const targetUrl = typeof payload.relativeUrl === "string" && payload.relativeUrl
8086
+ ? new URL(payload.relativeUrl, window.location.href).href
8087
+ : (typeof payload.url === "string" ? payload.url : "");
8088
+ const openedStudio = navigateExportStudioWindow(studioPopup, targetUrl);
8089
+ if (!openedStudio) {
8090
+ closeExportStudioWindow(studioPopup);
8091
+ const viewerUrl = getStudioPdfViewerUrlForExportPayload(payload);
8092
+ if (viewerUrl) openStudioPdfFocusViewer(viewerUrl, downloadName);
8093
+ }
8094
+ if (writeError) {
8095
+ setStatus(openedStudio
8096
+ ? "Opened exported PDF in a Studio preview tab, but could not write project file: " + writeError
8097
+ : "Exported PDF, but could not open a Studio preview tab and could not write project file: " + writeError,
8098
+ "warning");
8099
+ } else if (exportWarning) {
8100
+ setStatus(openedStudio
8101
+ ? "Opened exported PDF in a Studio preview tab with warning: " + exportWarning
8102
+ : "Exported PDF, but could not open a Studio preview tab. Warning: " + exportWarning,
8103
+ "warning");
8104
+ } else {
8105
+ setStatus(openedStudio
8106
+ ? "Opened exported PDF in a Studio preview tab: " + (exportPath || downloadName)
8107
+ : "Exported PDF, but could not open a Studio preview tab" + (targetUrl ? ": " + targetUrl : "."),
8108
+ openedStudio ? "success" : "warning");
8109
+ }
8110
+ return;
8111
+ }
8112
+
7712
8113
  if (openedExternal) {
7713
8114
  if (writeError) {
7714
8115
  setStatus("Opened PDF in default viewer, but could not write project file: " + writeError, "warning");
@@ -7770,6 +8171,7 @@
7770
8171
  setStatus("Exported PDF: " + downloadName, "success");
7771
8172
  }
7772
8173
  } catch (error) {
8174
+ closeExportStudioWindow(studioPopup);
7773
8175
  const detail = error && error.message ? error.message : String(error || "unknown error");
7774
8176
  setStatus("PDF export failed: " + detail, "error");
7775
8177
  } finally {
@@ -7778,14 +8180,58 @@
7778
8180
  }
7779
8181
  }
7780
8182
 
7781
- async function exportRightPaneHtml() {
8183
+ function openExportStudioPlaceholderWindow(formatLabel) {
8184
+ const label = String(formatLabel || "preview").trim() || "preview";
8185
+ let popup = null;
8186
+ try {
8187
+ popup = window.open("", "_blank");
8188
+ if (popup && popup.document && popup.document.body) {
8189
+ popup.document.title = "Opening " + label + " in Studio…";
8190
+ popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Exporting " + escapeHtml(label) + " and opening it in Studio…</p>";
8191
+ }
8192
+ } catch {
8193
+ popup = null;
8194
+ }
8195
+ return popup;
8196
+ }
8197
+
8198
+ function navigateExportStudioWindow(popup, targetUrl) {
8199
+ if (!targetUrl) return false;
8200
+ if (popup && !popup.closed) {
8201
+ try {
8202
+ popup.opener = null;
8203
+ popup.location.href = targetUrl;
8204
+ return true;
8205
+ } catch {}
8206
+ }
8207
+ try {
8208
+ return Boolean(window.open(targetUrl, "_blank", "noopener"));
8209
+ } catch {
8210
+ return false;
8211
+ }
8212
+ }
8213
+
8214
+ function closeExportStudioWindow(popup) {
8215
+ if (!popup || popup.closed) return;
8216
+ try { popup.close(); } catch {}
8217
+ }
8218
+
8219
+ async function exportRightPaneHtml(options) {
8220
+ const exportOptions = options && typeof options === "object" ? options : {};
8221
+ const openTarget = exportOptions.openTarget === "studio" ? "studio" : "browser";
8222
+ let studioPopup = null;
8223
+ if (openTarget === "studio") {
8224
+ studioPopup = openExportStudioPlaceholderWindow("HTML");
8225
+ }
7782
8226
  if (uiBusy || previewExportInProgress) {
8227
+ closeExportStudioWindow(studioPopup);
7783
8228
  setStatus("Studio is busy.", "warning");
7784
8229
  return;
7785
8230
  }
7786
8231
 
7787
8232
  const token = getToken();
7788
8233
  if (!token) {
8234
+ closeExportStudioWindow(studioPopup);
7789
8235
  setStatus("Missing Studio token in URL. Re-run /studio.", "error");
7790
8236
  return;
7791
8237
  }
@@ -7793,11 +8239,13 @@
7793
8239
  const exportingReplJournal = rightView === "repl";
7794
8240
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
7795
8241
  if (!rightPaneShowsPreview && !exportingReplJournal) {
8242
+ closeExportStudioWindow(studioPopup);
7796
8243
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
7797
8244
  return;
7798
8245
  }
7799
8246
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
7800
8247
  if (exportingReplJournal && !replJournalExportEntries.length) {
8248
+ closeExportStudioWindow(studioPopup);
7801
8249
  setStatus("No Studio REPL record entries to export for this session yet.", "warning");
7802
8250
  return;
7803
8251
  }
@@ -7807,6 +8255,7 @@
7807
8255
  ? prepareEditorTextForHtmlExport(sourceTextEl.value)
7808
8256
  : prepareEditorTextForPreview(latestResponseMarkdown)));
7809
8257
  if (!markdown || !markdown.trim()) {
8258
+ closeExportStudioWindow(studioPopup);
7810
8259
  setStatus("Nothing to export yet.", "warning");
7811
8260
  return;
7812
8261
  }
@@ -7831,7 +8280,7 @@
7831
8280
 
7832
8281
  previewExportInProgress = true;
7833
8282
  updateResultActionButtons();
7834
- setStatus("Exporting HTML…", "warning");
8283
+ setStatus(openTarget === "studio" ? "Exporting HTML for Studio…" : "Exporting HTML…", "warning");
7835
8284
 
7836
8285
  try {
7837
8286
  const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
@@ -7847,6 +8296,7 @@
7847
8296
  editorHtmlLanguage: editorHtmlLanguage,
7848
8297
  filenameHint: filenameHint,
7849
8298
  title: titleHint,
8299
+ openTarget: openTarget,
7850
8300
  }),
7851
8301
  }, HTML_EXPORT_FETCH_TIMEOUT_MS, "HTML export");
7852
8302
 
@@ -7885,6 +8335,31 @@
7885
8335
  downloadName += ".html";
7886
8336
  }
7887
8337
 
8338
+ if (openTarget === "studio") {
8339
+ const targetUrl = typeof payload.relativeUrl === "string" && payload.relativeUrl
8340
+ ? new URL(payload.relativeUrl, window.location.href).href
8341
+ : (typeof payload.url === "string" ? payload.url : "");
8342
+ const openedStudio = navigateExportStudioWindow(studioPopup, targetUrl);
8343
+ if (!openedStudio) closeExportStudioWindow(studioPopup);
8344
+ if (writeError) {
8345
+ setStatus(openedStudio
8346
+ ? "Opened exported HTML in Studio as an unsaved copy; could not write project file: " + writeError
8347
+ : "Exported HTML for Studio, but the popup was blocked and the project file could not be written: " + writeError,
8348
+ "warning");
8349
+ } else if (exportWarning) {
8350
+ setStatus(openedStudio
8351
+ ? "Opened exported HTML in Studio with warning: " + exportWarning
8352
+ : "Exported HTML for Studio, but the popup was blocked. Warning: " + exportWarning,
8353
+ "warning");
8354
+ } else {
8355
+ setStatus(openedStudio
8356
+ ? "Opened exported HTML in Studio: " + (exportPath || downloadName)
8357
+ : (targetUrl ? "Exported HTML for Studio: " + targetUrl : "Exported HTML, but Studio did not receive an editor URL."),
8358
+ openedStudio ? "success" : "warning");
8359
+ }
8360
+ return;
8361
+ }
8362
+
7888
8363
  if (openedExternal) {
7889
8364
  if (writeError) {
7890
8365
  setStatus("Opened HTML in default browser, but could not write project file: " + writeError, "warning");
@@ -7920,6 +8395,7 @@
7920
8395
  return;
7921
8396
  }
7922
8397
 
8398
+ closeExportStudioWindow(studioPopup);
7923
8399
  const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
7924
8400
  const blob = await response.blob();
7925
8401
  const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
@@ -7946,6 +8422,7 @@
7946
8422
  setStatus("Exported HTML: " + downloadName, "success");
7947
8423
  }
7948
8424
  } catch (error) {
8425
+ closeExportStudioWindow(studioPopup);
7949
8426
  const detail = error && error.message ? error.message : String(error || "unknown error");
7950
8427
  setStatus("HTML export failed: " + detail, "error");
7951
8428
  } finally {
@@ -7976,10 +8453,16 @@
7976
8453
 
7977
8454
  function exportRightPaneFormat(format) {
7978
8455
  closeExportPreviewMenu();
7979
- if (format === "html") {
7980
- return exportRightPaneHtml();
8456
+ if (format === "html-studio") {
8457
+ return exportRightPaneHtml({ openTarget: "studio" });
8458
+ }
8459
+ if (format === "html" || format === "html-browser") {
8460
+ return exportRightPaneHtml({ openTarget: "browser" });
8461
+ }
8462
+ if (format === "pdf-studio") {
8463
+ return exportRightPanePdf({ openTarget: "studio" });
7981
8464
  }
7982
- return exportRightPanePdf();
8465
+ return exportRightPanePdf({ openTarget: "default" });
7983
8466
  }
7984
8467
 
7985
8468
  function normalizeCopyableBlockText(text) {
@@ -8245,7 +8728,7 @@
8245
8728
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
8246
8729
  const previewLanguage = getEditorLanguageForPreview();
8247
8730
  if (isHtmlArtifactPreviewText(text, previewLanguage)) {
8248
- renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
8731
+ renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", commentable: true, ...getHtmlPreviewResourceContextOptions() });
8249
8732
  return;
8250
8733
  }
8251
8734
  if (renderDelimitedTextPreview(sourcePreviewEl, text, "source", previewLanguage)) {
@@ -9149,7 +9632,7 @@
9149
9632
  }
9150
9633
  const previewLanguage = getEditorLanguageForPreview();
9151
9634
  if (isHtmlArtifactPreviewText(editorText, previewLanguage)) {
9152
- renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
9635
+ renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", commentable: true, ...getHtmlPreviewResourceContextOptions() });
9153
9636
  return;
9154
9637
  }
9155
9638
  if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response", previewLanguage)) {
@@ -9269,28 +9752,40 @@
9269
9752
  } else if (isHtmlArtifactPreview) {
9270
9753
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
9271
9754
  } else if (exportingReplJournal) {
9272
- exportPdfBtn.title = "Choose PDF or HTML and export the Studio REPL record.";
9755
+ exportPdfBtn.title = "Choose PDF export or an HTML export destination for the Studio REPL record.";
9273
9756
  } else {
9274
- exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
9757
+ exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
9275
9758
  }
9276
9759
  }
9760
+ if (exportPreviewPdfStudioBtn) {
9761
+ exportPreviewPdfStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
9762
+ exportPreviewPdfStudioBtn.title = isHtmlArtifactPreview
9763
+ ? "Interactive HTML preview PDF export is not available yet."
9764
+ : (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in Studio." : "Export the current right-pane preview as PDF and open it in Studio.");
9765
+ }
9277
9766
  if (exportPreviewPdfBtn) {
9278
9767
  exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
9279
9768
  exportPreviewPdfBtn.title = isHtmlArtifactPreview
9280
9769
  ? "Interactive HTML preview PDF export is not available yet."
9281
- : (exportingReplJournal ? "Export the Studio REPL record as PDF." : "Export the current right-pane preview as PDF.");
9770
+ : (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in the default PDF viewer." : "Export the current right-pane preview as PDF and open it in the default PDF viewer.");
9771
+ }
9772
+ if (exportPreviewHtmlStudioBtn) {
9773
+ exportPreviewHtmlStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
9774
+ exportPreviewHtmlStudioBtn.title = isHtmlArtifactPreview
9775
+ ? "Export the authored HTML preview and open it in a new Studio editor tab."
9776
+ : (exportingReplJournal ? "Export the Studio REPL record as standalone HTML and open it in a new Studio editor tab." : "Export the current right-pane preview as standalone HTML and open it in a new Studio editor tab.");
9282
9777
  }
9283
9778
  if (exportPreviewHtmlBtn) {
9284
9779
  exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
9285
9780
  exportPreviewHtmlBtn.title = isHtmlArtifactPreview
9286
- ? "Export the authored HTML preview."
9287
- : (exportingReplJournal ? "Export the Studio REPL record as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
9781
+ ? "Export the authored HTML preview and open it in the default browser."
9782
+ : (exportingReplJournal ? "Export the Studio REPL record as standalone HTML and open it in the default browser." : "Export the current right-pane preview as standalone HTML and open it in the default browser.");
9288
9783
  }
9289
9784
  if (exportPreviewControlsEl) {
9290
9785
  exportPreviewControlsEl.title = canExportPreview
9291
9786
  ? (exportingReplJournal
9292
- ? "Choose a format and export the Studio REPL record."
9293
- : (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
9787
+ ? "Choose a format and export destination for the Studio REPL record."
9788
+ : (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview."))
9294
9789
  : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
9295
9790
  }
9296
9791
  if (!canExportPreview || previewExportInProgress) {
@@ -9439,7 +9934,8 @@
9439
9934
  if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
9440
9935
  if (compactBtn) compactBtn.disabled = isEditorOnlyMode || uiBusy || compactInProgress || wsState === "Disconnected";
9441
9936
  editorViewSelect.disabled = isEditorOnlyMode;
9442
- rightViewSelect.disabled = isEditorOnlyMode;
9937
+ syncRightViewModeOptions();
9938
+ rightViewSelect.disabled = false;
9443
9939
  followSelect.disabled = isEditorOnlyMode || uiBusy;
9444
9940
  if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
9445
9941
  insertHeaderBtn.disabled = uiBusy;
@@ -9550,7 +10046,7 @@
9550
10046
  sourceState: normalizeWorkspaceSourceState(sourceState),
9551
10047
  resourceDir: getCurrentResourceDirValue(),
9552
10048
  editorView,
9553
- rightView: isEditorOnlyMode ? "editor-preview" : rightView,
10049
+ rightView: normalizeRightViewValue(rightView),
9554
10050
  editorLanguage,
9555
10051
  followLatest,
9556
10052
  responseHistoryIndex,
@@ -9625,15 +10121,7 @@
9625
10121
  setEditorLanguage(state.editorLanguage.trim());
9626
10122
  }
9627
10123
  editorView = state.editorView === "preview" ? "preview" : "markdown";
9628
- rightView = isEditorOnlyMode
9629
- ? "editor-preview"
9630
- : (state.rightView === "preview"
9631
- ? "preview"
9632
- : (state.rightView === "editor-preview"
9633
- ? "editor-preview"
9634
- : (state.rightView === "repl"
9635
- ? "repl"
9636
- : (state.rightView === "files" ? "files" : ((state.rightView === "trace" || state.rightView === "thinking") ? "trace" : "markdown")))));
10124
+ rightView = normalizeRightViewValue(state.rightView);
9637
10125
  if (typeof state.followLatest === "boolean") {
9638
10126
  followLatest = state.followLatest;
9639
10127
  }
@@ -10176,15 +10664,8 @@
10176
10664
 
10177
10665
  function setRightView(nextView) {
10178
10666
  const previousView = rightView;
10179
- rightView = nextView === "preview"
10180
- ? "preview"
10181
- : (nextView === "editor-preview"
10182
- ? "editor-preview"
10183
- : (nextView === "repl"
10184
- ? "repl"
10185
- : (nextView === "files"
10186
- ? "files"
10187
- : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"))));
10667
+ rightView = normalizeRightViewValue(nextView);
10668
+ syncRightViewModeOptions();
10188
10669
  rightViewSelect.value = rightView;
10189
10670
  if (rightView === "trace" && previousView !== "trace") {
10190
10671
  traceAutoScroll = true;
@@ -12495,6 +12976,110 @@
12495
12976
  return describeStudioDocument(sourceState);
12496
12977
  }
12497
12978
 
12979
+ function formatScratchpadRecentTime(timestamp) {
12980
+ const value = Number(timestamp) || 0;
12981
+ if (!value) return "unknown time";
12982
+ try {
12983
+ return new Date(value).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
12984
+ } catch {
12985
+ return "unknown time";
12986
+ }
12987
+ }
12988
+
12989
+ function renderScratchpadRecentPanel() {
12990
+ if (!scratchpadRecentPanelEl) return;
12991
+ scratchpadRecentPanelEl.hidden = !scratchpadRecentVisible;
12992
+ if (!scratchpadRecentVisible) return;
12993
+ if (scratchpadRecentLoading) {
12994
+ scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-loading'>Loading recent scratchpads…</div>";
12995
+ return;
12996
+ }
12997
+ const currentKey = getCurrentStudioDocumentDescriptor().key;
12998
+ const entries = Array.isArray(scratchpadRecentEntries) ? scratchpadRecentEntries : [];
12999
+ if (!entries.length) {
13000
+ scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-empty'>No other saved scratchpads yet.</div>";
13001
+ return;
13002
+ }
13003
+ scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-list'>" + entries.map((entry) => {
13004
+ const key = String(entry && entry.documentKey ? entry.documentKey : "");
13005
+ const isCurrent = key === currentKey;
13006
+ const label = String(entry && entry.label ? entry.label : key || "scratchpad");
13007
+ const kind = String(entry && entry.kind ? entry.kind : "Scratchpad");
13008
+ const textLength = Math.max(0, Number(entry && entry.textLength) || 0);
13009
+ const preview = String(entry && entry.textPreview ? entry.textPreview : "");
13010
+ const meta = (isCurrent ? "Current · " : "") + kind + " · " + String(textLength) + " chars · " + formatScratchpadRecentTime(entry && entry.updatedAt);
13011
+ return "<div class='scratchpad-recent-item' data-scratchpad-key='" + escapeHtml(key) + "'>"
13012
+ + "<div class='scratchpad-recent-main'>"
13013
+ + "<div class='scratchpad-recent-title' title='" + escapeHtml(label) + "'>" + escapeHtml(label) + "</div>"
13014
+ + "<div class='scratchpad-recent-meta'>" + escapeHtml(meta) + "</div>"
13015
+ + (preview ? "<div class='scratchpad-recent-preview'>" + escapeHtml(preview) + "</div>" : "")
13016
+ + "</div>"
13017
+ + "<div class='scratchpad-recent-actions'>"
13018
+ + "<button type='button' data-scratchpad-recent-action='load' data-scratchpad-key='" + escapeHtml(key) + "'" + (isCurrent ? " disabled" : "") + ">Load</button>"
13019
+ + "<button type='button' data-scratchpad-recent-action='append' data-scratchpad-key='" + escapeHtml(key) + "'" + (isCurrent ? " disabled" : "") + ">Append</button>"
13020
+ + "<button type='button' data-scratchpad-recent-action='copy' data-scratchpad-key='" + escapeHtml(key) + "'>Copy</button>"
13021
+ + "</div>"
13022
+ + "</div>";
13023
+ }).join("") + "</div>";
13024
+ }
13025
+
13026
+ async function loadScratchpadRecentEntries() {
13027
+ scratchpadRecentLoading = true;
13028
+ renderScratchpadRecentPanel();
13029
+ try {
13030
+ const payload = await fetchStudioJson("/scratchpad-state", { query: { action: "recent", limit: "20" } });
13031
+ scratchpadRecentEntries = Array.isArray(payload && payload.scratchpads) ? payload.scratchpads : [];
13032
+ } catch (error) {
13033
+ scratchpadRecentEntries = [];
13034
+ setStatus("Could not load recent scratchpads: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
13035
+ } finally {
13036
+ scratchpadRecentLoading = false;
13037
+ renderScratchpadRecentPanel();
13038
+ }
13039
+ }
13040
+
13041
+ function toggleScratchpadRecentPanel() {
13042
+ scratchpadRecentVisible = !scratchpadRecentVisible;
13043
+ if (scratchpadRecentVisible) {
13044
+ void loadScratchpadRecentEntries();
13045
+ } else {
13046
+ renderScratchpadRecentPanel();
13047
+ }
13048
+ updateScratchpadUi();
13049
+ }
13050
+
13051
+ async function applyScratchpadRecentAction(action, documentKey) {
13052
+ const key = String(documentKey || "").trim();
13053
+ if (!key) return;
13054
+ const mode = action === "append" ? "append" : (action === "copy" ? "copy" : "load");
13055
+ try {
13056
+ const text = await fetchScratchpadTextForDocumentKey(key);
13057
+ if (!String(text || "").trim()) {
13058
+ setStatus("That scratchpad is empty.", "warning");
13059
+ return;
13060
+ }
13061
+ if (mode === "copy") {
13062
+ const ok = await writeTextToClipboard(text);
13063
+ setStatus(ok ? "Copied recent scratchpad." : "Could not copy recent scratchpad.", ok ? "success" : "warning");
13064
+ return;
13065
+ }
13066
+ if (mode === "append") {
13067
+ const separator = scratchpadText && !scratchpadText.endsWith("\n") ? "\n\n" : (scratchpadText ? "\n" : "");
13068
+ setScratchpadText(String(scratchpadText || "") + separator + String(text || ""));
13069
+ setStatus("Appended recent scratchpad.", "success");
13070
+ return;
13071
+ }
13072
+ if (String(scratchpadText || "").trim() && String(scratchpadText || "") !== String(text || "")) {
13073
+ const confirmed = window.confirm("Replace the current scratchpad with this recent scratchpad? Current scratchpad text will remain saved under its current document/draft identity, but this panel will show the loaded text for the current document.");
13074
+ if (!confirmed) return;
13075
+ }
13076
+ setScratchpadText(text);
13077
+ setStatus("Loaded recent scratchpad into current scratchpad.", "success");
13078
+ } catch (error) {
13079
+ setStatus("Could not use recent scratchpad: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
13080
+ }
13081
+ }
13082
+
12498
13083
  async function fetchScratchpadTextForDocumentKey(documentKey) {
12499
13084
  const payload = await fetchStudioJson("/scratchpad-state", {
12500
13085
  query: { documentKey: documentKey },
@@ -12502,9 +13087,9 @@
12502
13087
  return payload && typeof payload.text === "string" ? payload.text : "";
12503
13088
  }
12504
13089
 
12505
- function flushScratchpadPersistence(documentKeyOverride, textOverride) {
13090
+ function flushScratchpadPersistence(documentKeyOverride, textOverride, labelOverride) {
12506
13091
  const descriptor = documentKeyOverride
12507
- ? { key: String(documentKeyOverride || "").trim() }
13092
+ ? { key: String(documentKeyOverride || "").trim(), label: String(labelOverride || "").trim() }
12508
13093
  : getCurrentStudioDocumentDescriptor();
12509
13094
  const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
12510
13095
  if (!key) return;
@@ -12513,27 +13098,29 @@
12513
13098
  scratchpadPersistTimer = null;
12514
13099
  }
12515
13100
  const snapshot = String(arguments.length >= 2 ? textOverride : scratchpadText || "");
12516
- if (trySendStudioJsonBeacon("/scratchpad-state", { documentKey: key, text: snapshot })) {
13101
+ const label = String(descriptor && descriptor.label ? descriptor.label : "").trim();
13102
+ if (trySendStudioJsonBeacon("/scratchpad-state", { documentKey: key, text: snapshot, label })) {
12517
13103
  return;
12518
13104
  }
12519
13105
  void fetchStudioJson("/scratchpad-state", {
12520
13106
  method: "POST",
12521
- body: JSON.stringify({ documentKey: key, text: snapshot }),
13107
+ body: JSON.stringify({ documentKey: key, text: snapshot, label }),
12522
13108
  }).catch(() => {
12523
13109
  // Ignore scratchpad persistence failures for now.
12524
13110
  });
12525
13111
  }
12526
13112
 
12527
- function scheduleScratchpadPersistence(text, documentKey) {
13113
+ function scheduleScratchpadPersistence(text, documentKey, label) {
12528
13114
  if (scratchpadPersistTimer !== null) {
12529
13115
  window.clearTimeout(scratchpadPersistTimer);
12530
13116
  }
12531
13117
  const snapshot = String(text || "");
12532
13118
  const key = String(documentKey || "").trim();
13119
+ const labelSnapshot = String(label || "").trim();
12533
13120
  if (!key) return;
12534
13121
  scratchpadPersistTimer = window.setTimeout(() => {
12535
13122
  scratchpadPersistTimer = null;
12536
- flushScratchpadPersistence(key, snapshot);
13123
+ flushScratchpadPersistence(key, snapshot, labelSnapshot);
12537
13124
  }, 180);
12538
13125
  }
12539
13126
 
@@ -12565,7 +13152,7 @@
12565
13152
  if (String(existing || "").trim()) return;
12566
13153
  await fetchStudioJson("/scratchpad-state", {
12567
13154
  method: "POST",
12568
- body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot }),
13155
+ body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot, label: nextDescriptor.label }),
12569
13156
  });
12570
13157
  } catch {
12571
13158
  // Ignore carry-over failures and just fall back to normal scope loading.
@@ -12586,13 +13173,24 @@
12586
13173
 
12587
13174
  function persistScratchpadText(value) {
12588
13175
  const descriptor = getCurrentStudioDocumentDescriptor();
12589
- scheduleScratchpadPersistence(value, descriptor.key);
13176
+ scheduleScratchpadPersistence(value, descriptor.key, descriptor.label);
13177
+ }
13178
+
13179
+ function normalizeReviewNoteAnchorKind(value) {
13180
+ const raw = typeof value === "string" ? value.trim().toLowerCase() : "";
13181
+ if (raw === "html-selection" || raw === "html-element" || raw === "html-page") return raw;
13182
+ return "source";
13183
+ }
13184
+
13185
+ function isReviewNoteDomAnchor(note) {
13186
+ return Boolean(note && normalizeReviewNoteAnchorKind(note.anchorKind) !== "source");
12590
13187
  }
12591
13188
 
12592
13189
  function normalizeReviewNote(note) {
12593
13190
  if (!note || typeof note !== "object") return null;
12594
13191
  const id = typeof note.id === "string" && note.id.trim() ? note.id : makeRequestId();
12595
13192
  const text = typeof note.text === "string" ? note.text : "";
13193
+ const anchorKind = normalizeReviewNoteAnchorKind(note.anchorKind);
12596
13194
  const createdAt = typeof note.createdAt === "number" && Number.isFinite(note.createdAt)
12597
13195
  ? note.createdAt
12598
13196
  : Date.now();
@@ -12622,6 +13220,11 @@
12622
13220
  lineEnd,
12623
13221
  selectedText: typeof note.selectedText === "string" ? note.selectedText : "",
12624
13222
  selectedDisplayText: typeof note.selectedDisplayText === "string" ? note.selectedDisplayText : "",
13223
+ anchorKind,
13224
+ htmlSelector: typeof note.htmlSelector === "string" ? note.htmlSelector : "",
13225
+ htmlTag: typeof note.htmlTag === "string" ? note.htmlTag : "",
13226
+ htmlLabel: typeof note.htmlLabel === "string" ? note.htmlLabel : "",
13227
+ htmlPreviewTitle: typeof note.htmlPreviewTitle === "string" ? note.htmlPreviewTitle : "",
12625
13228
  };
12626
13229
  }
12627
13230
 
@@ -13120,17 +13723,26 @@
13120
13723
  }
13121
13724
  }
13122
13725
 
13726
+ function formatHtmlReviewNoteAnchorLabel(note) {
13727
+ const kind = normalizeReviewNoteAnchorKind(note && note.anchorKind);
13728
+ const tag = String(note && note.htmlTag ? note.htmlTag : "").trim().toLowerCase();
13729
+ if (kind === "html-selection") return "HTML selection";
13730
+ if (kind === "html-page") return "HTML page";
13731
+ return tag ? ("HTML <" + tag + ">") : "HTML element";
13732
+ }
13733
+
13123
13734
  function summarizeReviewNoteAnchor(note) {
13735
+ if (isReviewNoteDomAnchor(note)) return formatHtmlReviewNoteAnchorLabel(note);
13124
13736
  const start = Math.max(1, Number(note && note.lineStart) || 1);
13125
13737
  const end = Math.max(start, Number(note && note.lineEnd) || start);
13126
13738
  return start === end ? "Line " + start : ("Lines " + start + "–" + end);
13127
13739
  }
13128
13740
 
13129
13741
  function summarizeReviewNoteQuote(note) {
13130
- const normalized = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
13742
+ const normalized = String(note && (note.selectedDisplayText || note.selectedText || note.htmlLabel || note.htmlSelector) ? (note.selectedDisplayText || note.selectedText || note.htmlLabel || note.htmlSelector) : "")
13131
13743
  .replace(/\s+/g, " ")
13132
13744
  .trim();
13133
- if (!normalized) return "Anchor: current line / empty selection";
13745
+ if (!normalized) return isReviewNoteDomAnchor(note) ? "Anchor: HTML preview" : "Anchor: current line / empty selection";
13134
13746
  return normalized.length > 140 ? normalized.slice(0, 137) + "…" : normalized;
13135
13747
  }
13136
13748
 
@@ -13218,6 +13830,7 @@
13218
13830
  }
13219
13831
 
13220
13832
  function resolveReviewNoteRange(note, text) {
13833
+ if (isReviewNoteDomAnchor(note)) return null;
13221
13834
  const source = String(text || "");
13222
13835
  const safeStart = Math.max(0, Math.min(Number(note && note.selectionStart) || 0, source.length));
13223
13836
  const safeEnd = Math.max(safeStart, Math.min(Number(note && note.selectionEnd) || safeStart, source.length));
@@ -13281,6 +13894,7 @@
13281
13894
  }
13282
13895
 
13283
13896
  function formatReviewNotePromptLineRange(bounds, note) {
13897
+ if (isReviewNoteDomAnchor(note)) return summarizeReviewNoteAnchor(note);
13284
13898
  const start = bounds ? bounds.lineStart : Math.max(1, Number(note && note.lineStart) || 1);
13285
13899
  const end = bounds ? bounds.lineEnd : Math.max(start, Number(note && note.lineEnd) || start);
13286
13900
  return start === end ? "L" + start : ("L" + start + "-L" + end);
@@ -13294,7 +13908,7 @@
13294
13908
  const descriptor = getCurrentStudioDocumentDescriptor();
13295
13909
  const documentLabel = descriptor && descriptor.label ? descriptor.label : (sourceState && sourceState.label ? sourceState.label : "Studio document");
13296
13910
  const parts = [
13297
- "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.",
13911
+ "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.",
13298
13912
  "Document: " + documentLabel,
13299
13913
  "",
13300
13914
  "## Comments",
@@ -13316,6 +13930,9 @@
13316
13930
  if (anchor) {
13317
13931
  parts.push("", "> " + anchor.replace(/\n/g, "\n> "));
13318
13932
  }
13933
+ if (isReviewNoteDomAnchor(note) && note.htmlSelector) {
13934
+ parts.push("", "Preview selector: `" + String(note.htmlSelector).replace(/`/g, "\\`") + "`");
13935
+ }
13319
13936
  parts.push("");
13320
13937
  });
13321
13938
 
@@ -13337,6 +13954,7 @@
13337
13954
  const source = String(text || "");
13338
13955
  const lineMap = new Map();
13339
13956
  for (const note of reviewNotes) {
13957
+ if (isReviewNoteDomAnchor(note)) continue;
13340
13958
  const bounds = getResolvedReviewNoteLineBounds(note, source);
13341
13959
  if (!bounds) continue;
13342
13960
  for (let line = bounds.lineStart; line <= bounds.lineEnd; line += 1) {
@@ -15875,8 +16493,8 @@
15875
16493
  return reviewNotes.slice().sort((left, right) => {
15876
16494
  const leftBounds = getResolvedReviewNoteLineBounds(left, source);
15877
16495
  const rightBounds = getResolvedReviewNoteLineBounds(right, source);
15878
- const leftLine = leftBounds ? leftBounds.lineStart : Math.max(1, Number(left && left.lineStart) || 1);
15879
- const rightLine = rightBounds ? rightBounds.lineStart : Math.max(1, Number(right && right.lineStart) || 1);
16496
+ const leftLine = leftBounds ? leftBounds.lineStart : (isReviewNoteDomAnchor(left) ? Number.MAX_SAFE_INTEGER : Math.max(1, Number(left && left.lineStart) || 1));
16497
+ const rightLine = rightBounds ? rightBounds.lineStart : (isReviewNoteDomAnchor(right) ? Number.MAX_SAFE_INTEGER : Math.max(1, Number(right && right.lineStart) || 1));
15880
16498
  if (leftLine !== rightLine) return leftLine - rightLine;
15881
16499
 
15882
16500
  const leftStart = leftBounds ? leftBounds.start : Math.max(0, Number(left && left.selectionStart) || 0);
@@ -15908,6 +16526,15 @@
15908
16526
  function getReviewNoteInlineState(note, text) {
15909
16527
  const source = String(text || "");
15910
16528
  const annotationBody = escapeReviewNoteAnnotationText(note && note.text);
16529
+ if (isReviewNoteDomAnchor(note)) {
16530
+ return {
16531
+ annotationBody,
16532
+ range: null,
16533
+ markerText: "",
16534
+ exists: false,
16535
+ canToggle: false,
16536
+ };
16537
+ }
15911
16538
  if (!annotationBody) {
15912
16539
  return {
15913
16540
  annotationBody: "",
@@ -16233,7 +16860,9 @@
16233
16860
  const jumpBtn = document.createElement("button");
16234
16861
  jumpBtn.type = "button";
16235
16862
  jumpBtn.textContent = "Jump";
16236
- jumpBtn.title = "Jump to this comment's anchored location in the editor.";
16863
+ jumpBtn.title = isReviewNoteDomAnchor(note)
16864
+ ? "Jump to this comment's HTML preview anchor."
16865
+ : "Jump to this comment's anchored location in the editor.";
16237
16866
  jumpBtn.addEventListener("click", () => {
16238
16867
  jumpToReviewNote(note.id);
16239
16868
  });
@@ -16246,9 +16875,11 @@
16246
16875
  convertBtn.textContent = inlineState.exists ? "Inline: On" : "Inline: Off";
16247
16876
  convertBtn.setAttribute("aria-pressed", inlineState.exists ? "true" : "false");
16248
16877
  convertBtn.disabled = !inlineState.canToggle || uiBusy;
16249
- convertBtn.title = inlineState.exists
16250
- ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
16251
- : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
16878
+ convertBtn.title = isReviewNoteDomAnchor(note)
16879
+ ? "Inline annotations are only available for comments anchored to source text."
16880
+ : (inlineState.exists
16881
+ ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
16882
+ : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.");
16252
16883
  convertBtn.addEventListener("click", () => {
16253
16884
  convertReviewNoteToAnnotation(note.id);
16254
16885
  });
@@ -16276,9 +16907,11 @@
16276
16907
  convertBtn.disabled = !nextInlineState.canToggle || uiBusy;
16277
16908
  convertBtn.textContent = nextInlineState.exists ? "Inline: On" : "Inline: Off";
16278
16909
  convertBtn.setAttribute("aria-pressed", nextInlineState.exists ? "true" : "false");
16279
- convertBtn.title = nextInlineState.exists
16280
- ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
16281
- : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
16910
+ convertBtn.title = isReviewNoteDomAnchor(note)
16911
+ ? "Inline annotations are only available for comments anchored to source text."
16912
+ : (nextInlineState.exists
16913
+ ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
16914
+ : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.");
16282
16915
  scheduleReviewNotesPersistence();
16283
16916
  updateReviewNotesUi();
16284
16917
  });
@@ -16350,6 +16983,55 @@
16350
16983
  return note;
16351
16984
  }
16352
16985
 
16986
+ function addReviewNoteFromHtmlArtifactTarget(record, data) {
16987
+ if (!record || !record.commentable) return null;
16988
+ const kind = data && data.kind === "selection" ? "html-selection" : "html-element";
16989
+ const selector = typeof data.selector === "string" ? data.selector : "";
16990
+ const tag = typeof data.tag === "string" ? data.tag : "";
16991
+ const text = typeof data.text === "string" ? data.text : "";
16992
+ const label = typeof data.label === "string" ? data.label : "";
16993
+ const display = text || label || selector || (tag ? ("<" + tag + ">") : "HTML element");
16994
+ return addReviewNoteFromAnchor({
16995
+ selectionStart: 0,
16996
+ selectionEnd: 0,
16997
+ lineStart: 1,
16998
+ lineEnd: 1,
16999
+ selectedText: "",
17000
+ selectedDisplayText: display,
17001
+ anchorKind: kind,
17002
+ htmlSelector: selector,
17003
+ htmlTag: tag,
17004
+ htmlLabel: label,
17005
+ htmlPreviewTitle: record.title || "HTML preview",
17006
+ }, {
17007
+ statusMessage: kind === "html-selection"
17008
+ ? "Added local comment from HTML preview selection."
17009
+ : "Added local comment from HTML preview element.",
17010
+ });
17011
+ }
17012
+
17013
+ function addReviewNoteFromHtmlArtifactPage(record) {
17014
+ if (!record || !record.commentable) {
17015
+ setStatus("HTML preview comments are only available for editor previews.", "warning");
17016
+ return null;
17017
+ }
17018
+ return addReviewNoteFromAnchor({
17019
+ selectionStart: 0,
17020
+ selectionEnd: 0,
17021
+ lineStart: 1,
17022
+ lineEnd: 1,
17023
+ selectedText: "",
17024
+ selectedDisplayText: record.title || "HTML preview",
17025
+ anchorKind: "html-page",
17026
+ htmlSelector: "",
17027
+ htmlTag: "",
17028
+ htmlLabel: record.title || "HTML preview",
17029
+ htmlPreviewTitle: record.title || "HTML preview",
17030
+ }, {
17031
+ statusMessage: "Added page-level local comment for HTML preview.",
17032
+ });
17033
+ }
17034
+
16353
17035
  function addReviewNoteFromAnchor(anchor, options) {
16354
17036
  if (!anchor || typeof anchor !== "object") return null;
16355
17037
  const note = normalizeReviewNote({
@@ -16363,6 +17045,11 @@
16363
17045
  lineEnd: anchor.lineEnd,
16364
17046
  selectedText: anchor.selectedText,
16365
17047
  selectedDisplayText: typeof anchor.selectedDisplayText === "string" ? anchor.selectedDisplayText : (typeof anchor.selectedText === "string" ? anchor.selectedText : ""),
17048
+ anchorKind: anchor.anchorKind,
17049
+ htmlSelector: anchor.htmlSelector,
17050
+ htmlTag: anchor.htmlTag,
17051
+ htmlLabel: anchor.htmlLabel,
17052
+ htmlPreviewTitle: anchor.htmlPreviewTitle,
16366
17053
  });
16367
17054
  if (!note) return null;
16368
17055
  if (editorSelectionCommentBtn) {
@@ -16510,9 +17197,55 @@
16510
17197
  return jumped;
16511
17198
  }
16512
17199
 
17200
+ function getConnectedHtmlArtifactRecords() {
17201
+ const records = [];
17202
+ htmlArtifactFramesById.forEach((record, id) => {
17203
+ if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) {
17204
+ if (id) htmlArtifactFramesById.delete(id);
17205
+ return;
17206
+ }
17207
+ records.push(record);
17208
+ });
17209
+ return records;
17210
+ }
17211
+
17212
+ function jumpToHtmlReviewNote(note) {
17213
+ if (!isReviewNoteDomAnchor(note)) return false;
17214
+ const records = getConnectedHtmlArtifactRecords().filter((record) => record && record.commentable);
17215
+ if (records.length === 0) {
17216
+ setStatus("Open the HTML preview before jumping to this comment.", "warning");
17217
+ return false;
17218
+ }
17219
+ const record = records[0];
17220
+ if (record.shell && typeof record.shell.scrollIntoView === "function") {
17221
+ try {
17222
+ record.shell.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
17223
+ } catch {
17224
+ try { record.shell.scrollIntoView(false); } catch {}
17225
+ }
17226
+ }
17227
+ try {
17228
+ record.iframe.contentWindow.postMessage({
17229
+ type: "pi-studio-html-artifact-highlight-comment",
17230
+ id: record.id || "",
17231
+ selector: note.htmlSelector || "",
17232
+ anchorKind: normalizeReviewNoteAnchorKind(note.anchorKind),
17233
+ }, "*");
17234
+ setStatus("Jumped to HTML preview comment anchor.", "success");
17235
+ return true;
17236
+ } catch {
17237
+ setStatus("Could not jump to this HTML preview comment.", "warning");
17238
+ return false;
17239
+ }
17240
+ }
17241
+
16513
17242
  function jumpToReviewNote(noteId) {
16514
17243
  const note = reviewNotes.find((entry) => entry && entry.id === noteId);
16515
17244
  if (!note) return;
17245
+ if (isReviewNoteDomAnchor(note)) {
17246
+ jumpToHtmlReviewNote(note);
17247
+ return;
17248
+ }
16516
17249
  jumpToReviewAnchor(note, {
16517
17250
  status: false,
16518
17251
  notFoundStatusMessage: "Could not find the anchored location for this comment.",
@@ -16637,6 +17370,10 @@
16637
17370
  ? ("Saved locally for this document/draft · " + normalized.length + " chars")
16638
17371
  : "Empty · local to this document/draft";
16639
17372
  }
17373
+ if (scratchpadRecentBtn) {
17374
+ scratchpadRecentBtn.textContent = scratchpadRecentVisible ? "Hide recent" : "Recent…";
17375
+ scratchpadRecentBtn.setAttribute("aria-expanded", scratchpadRecentVisible ? "true" : "false");
17376
+ }
16640
17377
  if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
16641
17378
  if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
16642
17379
  if (scratchpadClearBtn) scratchpadClearBtn.disabled = !normalized.length;
@@ -16927,6 +17664,8 @@
16927
17664
  const directIsStop = activeKind === "direct";
16928
17665
  const critiqueIsStop = activeKind === "critique";
16929
17666
  const canQueueSteering = studioRunChainActive && !critiqueIsStop;
17667
+ const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
17668
+ const showReplSend = rightView === "repl";
16930
17669
 
16931
17670
  if (isEditorOnlyMode) {
16932
17671
  if (sendRunBtn) {
@@ -16942,15 +17681,25 @@
16942
17681
  queueSteerBtn.title = "Queue steering is unavailable in editor-only mode.";
16943
17682
  }
16944
17683
  if (sendReplBtn) {
16945
- sendReplBtn.hidden = true;
16946
- sendReplBtn.disabled = true;
16947
- sendReplBtn.classList.remove("repl-primary-action");
17684
+ sendReplBtn.hidden = !showReplSend;
17685
+ sendReplBtn.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy || !hasReplSession;
17686
+ sendReplBtn.classList.toggle("repl-primary-action", showReplSend);
17687
+ sendReplBtn.textContent = showReplSend ? withStudioShortcutLabel(replSendMode === "literate" ? "Send selection/chunks" : "Send to REPL", "repl-send") : "Send to REPL";
17688
+ sendReplBtn.title = hasReplSession
17689
+ ? (replSendMode === "literate"
17690
+ ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
17691
+ : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
17692
+ : "Start or select a REPL session in the right pane first.";
16948
17693
  const replActionLine = sendReplBtn.closest(".repl-action-line");
16949
- if (replActionLine instanceof HTMLElement) replActionLine.hidden = true;
17694
+ if (replActionLine instanceof HTMLElement) replActionLine.hidden = !showReplSend;
16950
17695
  }
16951
17696
  if (replSendModeSelect) {
16952
- replSendModeSelect.hidden = true;
16953
- replSendModeSelect.disabled = true;
17697
+ replSendModeSelect.hidden = !showReplSend;
17698
+ replSendModeSelect.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy;
17699
+ replSendModeSelect.value = replSendMode;
17700
+ replSendModeSelect.title = replSendMode === "literate"
17701
+ ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
17702
+ : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
16954
17703
  }
16955
17704
  if (critiqueBtn) {
16956
17705
  critiqueBtn.textContent = "Critique text";
@@ -16992,9 +17741,7 @@
16992
17741
  : "Queue steering is available while Run editor text is active.";
16993
17742
  }
16994
17743
 
16995
- const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
16996
17744
  if (sendReplBtn) {
16997
- const showReplSend = rightView === "repl";
16998
17745
  sendReplBtn.hidden = !showReplSend;
16999
17746
  sendReplBtn.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy || !hasReplSession;
17000
17747
  sendReplBtn.classList.toggle("repl-primary-action", showReplSend);
@@ -18616,7 +19363,7 @@
18616
19363
  event.stopPropagation();
18617
19364
  if (actionBtn.disabled) return;
18618
19365
  const format = String(actionBtn.getAttribute("data-export-preview-format") || "pdf").toLowerCase();
18619
- void exportRightPaneFormat(format === "html" ? "html" : "pdf");
19366
+ void exportRightPaneFormat(format);
18620
19367
  });
18621
19368
  }
18622
19369
 
@@ -19167,6 +19914,24 @@
19167
19914
  });
19168
19915
  }
19169
19916
 
19917
+ if (scratchpadRecentBtn) {
19918
+ scratchpadRecentBtn.addEventListener("click", () => {
19919
+ toggleScratchpadRecentPanel();
19920
+ });
19921
+ }
19922
+
19923
+ if (scratchpadRecentPanelEl) {
19924
+ scratchpadRecentPanelEl.addEventListener("click", (event) => {
19925
+ const target = event.target;
19926
+ const actionEl = target instanceof Element ? target.closest("[data-scratchpad-recent-action]") : null;
19927
+ if (!actionEl) return;
19928
+ event.preventDefault();
19929
+ const action = String(actionEl.getAttribute("data-scratchpad-recent-action") || "load");
19930
+ const key = String(actionEl.getAttribute("data-scratchpad-key") || "");
19931
+ void applyScratchpadRecentAction(action, key);
19932
+ });
19933
+ }
19934
+
19170
19935
  if (scratchpadInsertBtn) {
19171
19936
  scratchpadInsertBtn.addEventListener("click", () => {
19172
19937
  insertScratchpadIntoEditor();