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.
- package/CHANGELOG.md +13 -1
- package/README.md +4 -4
- package/client/studio-annotation-helpers.js +20 -1
- package/client/studio-client.js +847 -82
- package/client/studio.css +107 -2
- package/index.ts +176 -21
- package/package.json +1 -1
- package/shared/studio-markdown-fences.js +22 -0
- package/shared/studio-markdown-fences.ts +22 -0
package/client/studio-client.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
2459
|
-
if (
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
10180
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
16250
|
-
? "
|
|
16251
|
-
:
|
|
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 =
|
|
16280
|
-
? "
|
|
16281
|
-
:
|
|
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 =
|
|
16946
|
-
sendReplBtn.disabled =
|
|
16947
|
-
sendReplBtn.classList.
|
|
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 =
|
|
17694
|
+
if (replActionLine instanceof HTMLElement) replActionLine.hidden = !showReplSend;
|
|
16950
17695
|
}
|
|
16951
17696
|
if (replSendModeSelect) {
|
|
16952
|
-
replSendModeSelect.hidden =
|
|
16953
|
-
replSendModeSelect.disabled =
|
|
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
|
|
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();
|