pi-studio 0.8.2 → 0.8.4
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 +18 -2
- package/README.md +5 -5
- package/client/studio-client.js +224 -26
- package/client/studio.css +46 -0
- package/index.ts +228 -30
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,15 +4,31 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.4] — 2026-05-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Working view now renders image outputs from tools such as `read`, including bounded image previews in saved Working history.
|
|
11
|
+
- Rendered blockquotes now have hover/focus copy buttons that copy the quote text without Markdown `>` markers.
|
|
12
|
+
|
|
13
|
+
## [0.8.3] — 2026-05-13
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Updated package, README, and UI wording to describe the feature as interactive HTML preview.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Open new editor** now opens a blank companion editor when the current editor is empty.
|
|
20
|
+
- PDF export now preserves Pandoc YAML front matter while applying Studio Markdown transforms and lets Markdown documents with `header-includes` control their own LaTeX preamble, fixing exports that use YAML-defined commands such as `\firstpageletterhead`.
|
|
21
|
+
- Reduced scroll snap-back after using browser Find by preserving Studio pane scroll positions during pane activation.
|
|
22
|
+
|
|
7
23
|
## [0.8.2] — 2026-05-13
|
|
8
24
|
|
|
9
25
|
### Changed
|
|
10
|
-
- Improved dark-theme contrast for Studio dropdown arrows and the HTML
|
|
26
|
+
- Improved dark-theme contrast for Studio dropdown arrows and the interactive HTML preview zoom percentage by using the stronger Studio info text colour token.
|
|
11
27
|
|
|
12
28
|
## [0.8.1] — 2026-05-13
|
|
13
29
|
|
|
14
30
|
### Added
|
|
15
|
-
- Added first-cut HTML
|
|
31
|
+
- Added first-cut interactive HTML preview for straight, unfenced HTML in Studio preview panes, rendered in a sandboxed iframe with inline scripts enabled, network requests blocked by CSP, fit/capped sizing, and toolbar zoom controls; HTML export preserves authored HTML previews instead of converting them through Markdown.
|
|
16
32
|
|
|
17
33
|
## [0.8.0] — 2026-05-12
|
|
18
34
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-studio
|
|
2
2
|
|
|
3
|
-
Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace for working with prompts, responses, live working details, Markdown and LaTeX documents, code files, and other common text-based files side by side. Annotate responses and files, add local comments, write, edit, and run prompts, browse prompt and response history, request critiques, and use live preview for code, Markdown, and
|
|
3
|
+
Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace for working with prompts, responses, live working details, Markdown and LaTeX documents, interactive HTML previews, code files, and other common text-based files side by side. Annotate responses and files, add local comments, write, edit, and run prompts, browse prompt and response history, request critiques, and use live preview for code, Markdown, LaTeX, and interactive HTML.
|
|
4
4
|
|
|
5
5
|
## Quick demo
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
21
21
|
- Opens a two-pane browser workspace: **Editor** (left) + **Response/Working/Editor Preview** (right)
|
|
22
22
|
- Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces; the editor toolbar can open a detached copy of the current editor text as a companion view
|
|
23
23
|
- Runs editor text directly, or asks for structured critique (auto/writing/code focus)
|
|
24
|
-
- Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters plus **Load visible into editor** and **Copy visible** actions; when cycling response history, Working follows saved working details for the selected response when available
|
|
24
|
+
- Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters, image previews for image-producing tool outputs, plus **Load visible into editor** and **Copy visible** actions; when cycling response history, Working follows saved working details for the selected response when available
|
|
25
25
|
- Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them
|
|
26
26
|
- Includes a docked **Outline** rail for navigating document structure in the current editor text, with clickable entries that jump in the raw editor and reveal matching preview locations when available
|
|
27
27
|
- Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with transient **Comment** / **Jump** actions from raw-editor selections plus editor-preview selections for Markdown, LaTeX, and code/text/diff previews, alongside optional inline `[an: ...]` toggles when you want comments reflected in the document text
|
|
@@ -34,11 +34,11 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
34
34
|
- shows/hides annotation markers in preview
|
|
35
35
|
- strips markers before send (optional)
|
|
36
36
|
- saves `.annotated.md`
|
|
37
|
-
- Renders Markdown/LaTeX/code previews (math + Mermaid), theme-synced with pi
|
|
38
|
-
- Renders straight, unfenced HTML
|
|
37
|
+
- Renders Markdown/LaTeX/code previews (math + Mermaid), theme-synced with pi, with copy buttons for code blocks and blockquotes
|
|
38
|
+
- Renders straight, unfenced interactive HTML in preview via a sandboxed browser iframe with zoom controls, while fenced `html` blocks remain source code
|
|
39
39
|
- Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks
|
|
40
40
|
- Ships optional `pi-studio-dark` and `pi-studio-light` themes tuned for Studio's browser workspace
|
|
41
|
-
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML, preserving authored HTML
|
|
41
|
+
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML, preserving authored HTML previews as HTML
|
|
42
42
|
- Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf` or `/studio-html <path>` to `<name>.studio.html`; without a path, those commands export the last model response to a timestamped file
|
|
43
43
|
- Shows model/session/context usage in the footer, plus a compact-context action
|
|
44
44
|
|
package/client/studio-client.js
CHANGED
|
@@ -214,6 +214,7 @@
|
|
|
214
214
|
const traceExpandedOutputs = new Set();
|
|
215
215
|
const TRACE_OUTPUT_PREVIEW_MAX_LINES = 50;
|
|
216
216
|
const TRACE_OUTPUT_PREVIEW_MAX_CHARS = 8000;
|
|
217
|
+
const TRACE_IMAGE_SAFE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
217
218
|
let studioRunChainActive = false;
|
|
218
219
|
let queuedSteeringCount = 0;
|
|
219
220
|
let agentBusyFromServer = false;
|
|
@@ -287,6 +288,37 @@
|
|
|
287
288
|
: "pending";
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
function normalizeTraceImageMimeType(value) {
|
|
292
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isTraceImageSafeMimeType(mimeType) {
|
|
296
|
+
return TRACE_IMAGE_SAFE_MIME_TYPES.has(normalizeTraceImageMimeType(mimeType));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeTraceImage(image, fallbackIndex) {
|
|
300
|
+
if (!image || typeof image !== "object") return null;
|
|
301
|
+
const mimeType = normalizeTraceImageMimeType(image.mimeType);
|
|
302
|
+
if (!isTraceImageSafeMimeType(mimeType)) return null;
|
|
303
|
+
const data = typeof image.data === "string" ? image.data.replace(/\s+/g, "") : "";
|
|
304
|
+
if (!data || !/^[A-Za-z0-9+/]*={0,2}$/.test(data)) return null;
|
|
305
|
+
const byteLength = parseFiniteNumber(image.byteLength);
|
|
306
|
+
return {
|
|
307
|
+
id: typeof image.id === "string" && image.id.trim() ? image.id.trim() : ("trace-image-" + fallbackIndex),
|
|
308
|
+
mimeType,
|
|
309
|
+
data,
|
|
310
|
+
byteLength: byteLength == null ? estimateTraceImageByteLength(data) : byteLength,
|
|
311
|
+
label: parseNonEmptyString(image.label),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function estimateTraceImageByteLength(data) {
|
|
316
|
+
const compact = String(data || "").replace(/\s+/g, "");
|
|
317
|
+
if (!compact) return null;
|
|
318
|
+
const padding = compact.endsWith("==") ? 2 : (compact.endsWith("=") ? 1 : 0);
|
|
319
|
+
return Math.max(0, Math.floor((compact.length * 3) / 4) - padding);
|
|
320
|
+
}
|
|
321
|
+
|
|
290
322
|
function normalizeTraceEntry(entry, fallbackIndex) {
|
|
291
323
|
if (!entry || typeof entry !== "object") return null;
|
|
292
324
|
if (entry.type === "assistant") {
|
|
@@ -310,6 +342,9 @@
|
|
|
310
342
|
label: parseNonEmptyString(entry.label),
|
|
311
343
|
argsSummary: parseNonEmptyString(entry.argsSummary),
|
|
312
344
|
output: typeof entry.output === "string" ? entry.output : "",
|
|
345
|
+
images: Array.isArray(entry.images)
|
|
346
|
+
? entry.images.map((image, imageIndex) => normalizeTraceImage(image, imageIndex)).filter(Boolean)
|
|
347
|
+
: [],
|
|
313
348
|
startedAt: parseFiniteNumber(entry.startedAt) || Date.now(),
|
|
314
349
|
updatedAt: parseFiniteNumber(entry.updatedAt) || Date.now(),
|
|
315
350
|
status: normalizeTraceEntryStatus(entry.status),
|
|
@@ -542,6 +577,22 @@
|
|
|
542
577
|
});
|
|
543
578
|
}
|
|
544
579
|
|
|
580
|
+
function formatTraceImageSize(byteLength) {
|
|
581
|
+
if (typeof byteLength !== "number" || !Number.isFinite(byteLength) || byteLength < 0) return "unknown size";
|
|
582
|
+
if (byteLength < 1024) return formatNumber(byteLength) + " B";
|
|
583
|
+
if (byteLength < 1024 * 1024) return (byteLength / 1024).toFixed(byteLength >= 100 * 1024 ? 0 : 1).replace(/\.0$/, "") + " KB";
|
|
584
|
+
return (byteLength / (1024 * 1024)).toFixed(byteLength >= 100 * 1024 * 1024 ? 0 : 1).replace(/\.0$/, "") + " MB";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function describeTraceImageForText(image) {
|
|
588
|
+
if (!image || typeof image !== "object") return "";
|
|
589
|
+
const parts = [];
|
|
590
|
+
if (image.label) parts.push(String(image.label));
|
|
591
|
+
parts.push(String(image.mimeType || "image"));
|
|
592
|
+
parts.push(formatTraceImageSize(image.byteLength));
|
|
593
|
+
return parts.filter(Boolean).join(" — ");
|
|
594
|
+
}
|
|
595
|
+
|
|
545
596
|
function buildVisibleWorkingText(filterOverride) {
|
|
546
597
|
const filter = normalizeTraceFilter(filterOverride || traceFilter);
|
|
547
598
|
const entries = getTraceEntriesForFilter(filter);
|
|
@@ -576,6 +627,12 @@
|
|
|
576
627
|
if (String(entry.output || "").trim()) {
|
|
577
628
|
parts.push("Output:\n" + String(entry.output || "").trim());
|
|
578
629
|
}
|
|
630
|
+
const imageSummaries = Array.isArray(entry.images)
|
|
631
|
+
? entry.images.map(describeTraceImageForText).filter(Boolean)
|
|
632
|
+
: [];
|
|
633
|
+
if (imageSummaries.length) {
|
|
634
|
+
parts.push("Images:\n" + imageSummaries.map((summary) => "- " + summary).join("\n"));
|
|
635
|
+
}
|
|
579
636
|
return parts.join("\n\n").trim();
|
|
580
637
|
}).filter(Boolean).join("\n\n---\n\n");
|
|
581
638
|
}
|
|
@@ -1954,6 +2011,51 @@
|
|
|
1954
2011
|
updatePaneFocusButtons();
|
|
1955
2012
|
}
|
|
1956
2013
|
|
|
2014
|
+
function snapshotStudioScrollablePositions() {
|
|
2015
|
+
return [sourceTextEl, sourcePreviewEl, critiqueViewEl]
|
|
2016
|
+
.filter((el) => el && typeof el.scrollTop === "number" && typeof el.scrollLeft === "number")
|
|
2017
|
+
.map((el) => ({ el, top: el.scrollTop, left: el.scrollLeft }));
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function restoreStudioScrollablePositions(snapshot) {
|
|
2021
|
+
if (!Array.isArray(snapshot)) return;
|
|
2022
|
+
snapshot.forEach((entry) => {
|
|
2023
|
+
const el = entry && entry.el;
|
|
2024
|
+
if (!el || !el.isConnected) return;
|
|
2025
|
+
if (typeof entry.top === "number") el.scrollTop = entry.top;
|
|
2026
|
+
if (typeof entry.left === "number") el.scrollLeft = entry.left;
|
|
2027
|
+
});
|
|
2028
|
+
syncEditorHighlightScroll();
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function scheduleStudioScrollablePositionRestore(snapshot) {
|
|
2032
|
+
if (!Array.isArray(snapshot) || snapshot.length === 0) return;
|
|
2033
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
2034
|
+
? window.requestAnimationFrame.bind(window)
|
|
2035
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
2036
|
+
window.setTimeout(() => restoreStudioScrollablePositions(snapshot), 0);
|
|
2037
|
+
schedule(() => {
|
|
2038
|
+
restoreStudioScrollablePositions(snapshot);
|
|
2039
|
+
schedule(() => restoreStudioScrollablePositions(snapshot));
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function shouldPreserveScrollForPaneActivationEvent(event) {
|
|
2044
|
+
const target = event && event.target;
|
|
2045
|
+
if (!(target instanceof Element)) return true;
|
|
2046
|
+
if (target.closest("button, select, input, a, [role='button'], .studio-copy-block-btn, .preview-comment-add, .preview-comment-jump")) {
|
|
2047
|
+
return false;
|
|
2048
|
+
}
|
|
2049
|
+
return true;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function activatePaneFromInteraction(nextPane, event) {
|
|
2053
|
+
const shouldPreserveScroll = shouldPreserveScrollForPaneActivationEvent(event);
|
|
2054
|
+
const snapshot = shouldPreserveScroll ? snapshotStudioScrollablePositions() : [];
|
|
2055
|
+
setActivePane(nextPane);
|
|
2056
|
+
if (shouldPreserveScroll) scheduleStudioScrollablePositionRestore(snapshot);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
1957
2059
|
function setActivePane(nextPane) {
|
|
1958
2060
|
activePane = nextPane === "right" ? "right" : "left";
|
|
1959
2061
|
|
|
@@ -2711,7 +2813,7 @@
|
|
|
2711
2813
|
record.shell.classList.toggle("is-height-capped", capped);
|
|
2712
2814
|
}
|
|
2713
2815
|
if (record.detail) {
|
|
2714
|
-
record.detail.textContent = "HTML
|
|
2816
|
+
record.detail.textContent = "HTML preview";
|
|
2715
2817
|
}
|
|
2716
2818
|
}
|
|
2717
2819
|
|
|
@@ -2719,7 +2821,7 @@
|
|
|
2719
2821
|
|
|
2720
2822
|
function renderHtmlArtifactPreview(targetEl, html, pane, options) {
|
|
2721
2823
|
if (!targetEl) return;
|
|
2722
|
-
const title = options && options.title ? String(options.title) : "HTML
|
|
2824
|
+
const title = options && options.title ? String(options.title) : "HTML preview";
|
|
2723
2825
|
const previewId = "html_artifact_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10);
|
|
2724
2826
|
pruneDisconnectedHtmlArtifactFrames();
|
|
2725
2827
|
clearPreviewJumpHighlight(targetEl);
|
|
@@ -2736,7 +2838,7 @@
|
|
|
2736
2838
|
label.textContent = title;
|
|
2737
2839
|
const detail = document.createElement("span");
|
|
2738
2840
|
detail.className = "studio-html-artifact-detail";
|
|
2739
|
-
detail.textContent = "HTML
|
|
2841
|
+
detail.textContent = "HTML preview";
|
|
2740
2842
|
|
|
2741
2843
|
const tools = document.createElement("span");
|
|
2742
2844
|
tools.className = "studio-html-artifact-tools";
|
|
@@ -2783,10 +2885,10 @@
|
|
|
2783
2885
|
});
|
|
2784
2886
|
return button;
|
|
2785
2887
|
};
|
|
2786
|
-
const zoomOutBtn = makeZoomButton("−", "Zoom out HTML
|
|
2787
|
-
const zoomResetBtn = makeZoomButton("100%", "Reset HTML
|
|
2888
|
+
const zoomOutBtn = makeZoomButton("−", "Zoom out HTML preview", () => setArtifactZoom(artifactZoom - HTML_ARTIFACT_ZOOM_STEP));
|
|
2889
|
+
const zoomResetBtn = makeZoomButton("100%", "Reset HTML preview zoom", () => setArtifactZoom(1));
|
|
2788
2890
|
zoomResetBtn.classList.add("studio-html-artifact-zoom-reset");
|
|
2789
|
-
const zoomInBtn = makeZoomButton("+", "Zoom in HTML
|
|
2891
|
+
const zoomInBtn = makeZoomButton("+", "Zoom in HTML preview", () => setArtifactZoom(artifactZoom + HTML_ARTIFACT_ZOOM_STEP));
|
|
2790
2892
|
zoomControls.appendChild(zoomOutBtn);
|
|
2791
2893
|
zoomControls.appendChild(zoomResetBtn);
|
|
2792
2894
|
zoomControls.appendChild(zoomInBtn);
|
|
@@ -3882,7 +3984,7 @@
|
|
|
3882
3984
|
|
|
3883
3985
|
const htmlArtifactSource = getRightPaneHtmlArtifactSource();
|
|
3884
3986
|
if (htmlArtifactSource) {
|
|
3885
|
-
setStatus("PDF export does not support HTML
|
|
3987
|
+
setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
|
|
3886
3988
|
return;
|
|
3887
3989
|
}
|
|
3888
3990
|
|
|
@@ -4222,6 +4324,78 @@
|
|
|
4222
4324
|
return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
|
|
4223
4325
|
}
|
|
4224
4326
|
|
|
4327
|
+
function getCopyableBlockquoteText(blockEl) {
|
|
4328
|
+
const clone = blockEl && typeof blockEl.cloneNode === "function" ? blockEl.cloneNode(true) : null;
|
|
4329
|
+
const sourceEl = clone && typeof clone.querySelectorAll === "function" ? clone : blockEl;
|
|
4330
|
+
if (!sourceEl) return "";
|
|
4331
|
+
if (typeof sourceEl.querySelectorAll === "function") {
|
|
4332
|
+
Array.from(sourceEl.querySelectorAll(".studio-copy-block-btn")).forEach((buttonEl) => {
|
|
4333
|
+
if (buttonEl && buttonEl.parentNode) buttonEl.parentNode.removeChild(buttonEl);
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
const blockTags = new Set(["ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "DIV", "FIGCAPTION", "FIGURE", "FOOTER", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", "LI", "OL", "P", "PRE", "SECTION", "TABLE", "TBODY", "TD", "TH", "THEAD", "TR", "UL"]);
|
|
4338
|
+
const isElementBlock = (node) => node && node.nodeType === 1 && blockTags.has(String(node.tagName || "").toUpperCase());
|
|
4339
|
+
|
|
4340
|
+
const collectInlineText = (node) => {
|
|
4341
|
+
if (!node) return "";
|
|
4342
|
+
if (node.nodeType === Node.TEXT_NODE) return node.nodeValue || "";
|
|
4343
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
|
4344
|
+
const tag = String(node.tagName || "").toUpperCase();
|
|
4345
|
+
if (tag === "SCRIPT" || tag === "STYLE" || tag === "BUTTON") return "";
|
|
4346
|
+
if (tag === "BR") return "\n";
|
|
4347
|
+
const childText = Array.from(node.childNodes || []).map(collectInlineText).join("");
|
|
4348
|
+
return isElementBlock(node) ? childText.trim() : childText;
|
|
4349
|
+
};
|
|
4350
|
+
|
|
4351
|
+
const collectBlocks = (node) => {
|
|
4352
|
+
if (!node) return [];
|
|
4353
|
+
const parts = [];
|
|
4354
|
+
let inlineBuffer = "";
|
|
4355
|
+
const flushInline = () => {
|
|
4356
|
+
const text = inlineBuffer.replace(/[ \t]+\n/g, "\n").trim();
|
|
4357
|
+
if (text) parts.push(text);
|
|
4358
|
+
inlineBuffer = "";
|
|
4359
|
+
};
|
|
4360
|
+
|
|
4361
|
+
Array.from(node.childNodes || []).forEach((child) => {
|
|
4362
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
4363
|
+
inlineBuffer += child.nodeValue || "";
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
if (child.nodeType !== Node.ELEMENT_NODE) return;
|
|
4367
|
+
const tag = String(child.tagName || "").toUpperCase();
|
|
4368
|
+
if (tag === "SCRIPT" || tag === "STYLE" || tag === "BUTTON") return;
|
|
4369
|
+
if (tag === "BR") {
|
|
4370
|
+
inlineBuffer += "\n";
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
if (isElementBlock(child)) {
|
|
4374
|
+
flushInline();
|
|
4375
|
+
if (tag === "UL" || tag === "OL") {
|
|
4376
|
+
Array.from(child.children || []).forEach((item, itemIndex) => {
|
|
4377
|
+
if (!item || String(item.tagName || "").toUpperCase() !== "LI") return;
|
|
4378
|
+
const prefix = tag === "OL" ? (String(itemIndex + 1) + ". ") : "- ";
|
|
4379
|
+
const itemText = collectInlineText(item).trim();
|
|
4380
|
+
if (itemText) parts.push(prefix + itemText);
|
|
4381
|
+
});
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
const blockText = tag === "BLOCKQUOTE"
|
|
4385
|
+
? collectBlocks(child).join("\n\n").trim()
|
|
4386
|
+
: collectInlineText(child).trim();
|
|
4387
|
+
if (blockText) parts.push(blockText);
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
inlineBuffer += collectInlineText(child);
|
|
4391
|
+
});
|
|
4392
|
+
flushInline();
|
|
4393
|
+
return parts;
|
|
4394
|
+
};
|
|
4395
|
+
|
|
4396
|
+
return normalizeCopyableBlockText(collectBlocks(sourceEl).join("\n\n")).trim();
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4225
4399
|
function getCopyablePreviewBlockText(blockEl) {
|
|
4226
4400
|
if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
|
|
4227
4401
|
if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
|
|
@@ -4232,6 +4406,10 @@
|
|
|
4232
4406
|
);
|
|
4233
4407
|
}
|
|
4234
4408
|
|
|
4409
|
+
if (blockEl.matches && blockEl.matches("blockquote")) {
|
|
4410
|
+
return getCopyableBlockquoteText(blockEl);
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4235
4413
|
const codeEl = typeof blockEl.querySelector === "function"
|
|
4236
4414
|
? blockEl.querySelector("pre code, code")
|
|
4237
4415
|
: null;
|
|
@@ -4289,7 +4467,7 @@
|
|
|
4289
4467
|
|
|
4290
4468
|
function decorateCopyablePreviewBlocks(targetEl) {
|
|
4291
4469
|
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
4292
|
-
const blocks = Array.from(targetEl.querySelectorAll("div.sourceCode, pre, .preview-code-lines"));
|
|
4470
|
+
const blocks = Array.from(targetEl.querySelectorAll("div.sourceCode, pre, .preview-code-lines, blockquote"));
|
|
4293
4471
|
blocks.forEach((blockEl) => {
|
|
4294
4472
|
if (!blockEl || !(blockEl instanceof Element)) return;
|
|
4295
4473
|
if (blockEl.dataset && blockEl.dataset.studioCopyDecorated === "1") return;
|
|
@@ -4400,7 +4578,7 @@
|
|
|
4400
4578
|
if (editorView !== "preview") return;
|
|
4401
4579
|
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
4402
4580
|
if (isHtmlArtifactPreviewText(text, editorLanguage)) {
|
|
4403
|
-
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML
|
|
4581
|
+
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview" });
|
|
4404
4582
|
return;
|
|
4405
4583
|
}
|
|
4406
4584
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
@@ -4527,6 +4705,23 @@
|
|
|
4527
4705
|
+ "</div>";
|
|
4528
4706
|
}
|
|
4529
4707
|
|
|
4708
|
+
function renderTraceImages(images) {
|
|
4709
|
+
const normalizedImages = Array.isArray(images)
|
|
4710
|
+
? images.map((image, index) => normalizeTraceImage(image, index)).filter(Boolean)
|
|
4711
|
+
: [];
|
|
4712
|
+
if (!normalizedImages.length) return "";
|
|
4713
|
+
const cards = normalizedImages.map((image) => {
|
|
4714
|
+
const src = "data:" + image.mimeType + ";base64," + image.data;
|
|
4715
|
+
const caption = describeTraceImageForText(image);
|
|
4716
|
+
const alt = image.label || ("Working output image: " + image.mimeType);
|
|
4717
|
+
return "<figure class='trace-image-card'>"
|
|
4718
|
+
+ "<img src='" + escapeHtml(src) + "' alt='" + escapeHtml(alt) + "' loading='lazy' decoding='async' />"
|
|
4719
|
+
+ "<figcaption class='trace-image-caption'>" + escapeHtml(caption) + "</figcaption>"
|
|
4720
|
+
+ "</figure>";
|
|
4721
|
+
}).join("");
|
|
4722
|
+
return "<div class='trace-image-gallery'>" + cards + "</div>";
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4530
4725
|
function buildTracePanelHtml() {
|
|
4531
4726
|
const state = traceState || createEmptyTraceState();
|
|
4532
4727
|
const filter = normalizeTraceFilter(traceFilter);
|
|
@@ -4620,8 +4815,12 @@
|
|
|
4620
4815
|
const argsSummary = entry.argsSummary
|
|
4621
4816
|
? "<div class='trace-section'><div class='trace-section-label'>Input</div>" + renderTraceOutput(entry.argsSummary, entry.id + ":input") + "</div>"
|
|
4622
4817
|
: "";
|
|
4623
|
-
const
|
|
4624
|
-
|
|
4818
|
+
const imageOutput = renderTraceImages(entry.images);
|
|
4819
|
+
const outputPieces = [];
|
|
4820
|
+
if (entry.output) outputPieces.push(renderTraceOutput(entry.output, entry.id + ":output"));
|
|
4821
|
+
if (imageOutput) outputPieces.push(imageOutput);
|
|
4822
|
+
const output = outputPieces.length
|
|
4823
|
+
? "<div class='trace-section'><div class='trace-section-label'>Output</div>" + outputPieces.join("") + "</div>"
|
|
4625
4824
|
: "<div class='trace-empty-inline'>No output yet.</div>";
|
|
4626
4825
|
const toolStatusLabel = entry.isError
|
|
4627
4826
|
? "Error"
|
|
@@ -4672,7 +4871,7 @@
|
|
|
4672
4871
|
return;
|
|
4673
4872
|
}
|
|
4674
4873
|
if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
|
|
4675
|
-
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML
|
|
4874
|
+
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview" });
|
|
4676
4875
|
return;
|
|
4677
4876
|
}
|
|
4678
4877
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
@@ -4696,7 +4895,7 @@
|
|
|
4696
4895
|
|
|
4697
4896
|
if (rightView === "preview") {
|
|
4698
4897
|
if (isHtmlArtifactPreviewText(markdown, "")) {
|
|
4699
|
-
renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML
|
|
4898
|
+
renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview" });
|
|
4700
4899
|
return;
|
|
4701
4900
|
}
|
|
4702
4901
|
const nonce = ++responsePreviewRenderNonce;
|
|
@@ -4777,7 +4976,7 @@
|
|
|
4777
4976
|
} else if (!canExportPreview) {
|
|
4778
4977
|
exportPdfBtn.title = "Nothing to export yet.";
|
|
4779
4978
|
} else if (isHtmlArtifactPreview) {
|
|
4780
|
-
exportPdfBtn.title = "This is an HTML
|
|
4979
|
+
exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
|
|
4781
4980
|
} else {
|
|
4782
4981
|
exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
|
|
4783
4982
|
}
|
|
@@ -4785,18 +4984,18 @@
|
|
|
4785
4984
|
if (exportPreviewPdfBtn) {
|
|
4786
4985
|
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
4787
4986
|
exportPreviewPdfBtn.title = isHtmlArtifactPreview
|
|
4788
|
-
? "HTML
|
|
4987
|
+
? "Interactive HTML preview PDF export is not available yet."
|
|
4789
4988
|
: "Export the current right-pane preview as PDF.";
|
|
4790
4989
|
}
|
|
4791
4990
|
if (exportPreviewHtmlBtn) {
|
|
4792
4991
|
exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4793
4992
|
exportPreviewHtmlBtn.title = isHtmlArtifactPreview
|
|
4794
|
-
? "Export the authored HTML
|
|
4993
|
+
? "Export the authored HTML preview."
|
|
4795
4994
|
: "Export the current right-pane preview as standalone HTML.";
|
|
4796
4995
|
}
|
|
4797
4996
|
if (exportPreviewControlsEl) {
|
|
4798
4997
|
exportPreviewControlsEl.title = canExportPreview
|
|
4799
|
-
? (isHtmlArtifactPreview ? "Export this HTML
|
|
4998
|
+
? (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview.")
|
|
4800
4999
|
: "Switch right pane to a non-empty preview before exporting.";
|
|
4801
5000
|
}
|
|
4802
5001
|
if (!canExportPreview || previewExportInProgress) {
|
|
@@ -11114,9 +11313,12 @@
|
|
|
11114
11313
|
setWsState("Ready");
|
|
11115
11314
|
const targetUrl = resolveCompanionEditorTargetUrl(message);
|
|
11116
11315
|
const opened = navigatePendingCompanionWindow(responseRequestId, targetUrl);
|
|
11316
|
+
const readyMessage = typeof message.message === "string" && message.message.trim()
|
|
11317
|
+
? message.message.trim()
|
|
11318
|
+
: "Opened companion editor with a detached copy of the current editor text.";
|
|
11117
11319
|
setStatus(
|
|
11118
11320
|
opened
|
|
11119
|
-
?
|
|
11321
|
+
? readyMessage
|
|
11120
11322
|
: (targetUrl ? "Companion editor ready: " + targetUrl : "Companion editor is ready, but Studio did not receive a URL."),
|
|
11121
11323
|
opened ? "success" : "warning",
|
|
11122
11324
|
);
|
|
@@ -11578,13 +11780,13 @@
|
|
|
11578
11780
|
}
|
|
11579
11781
|
|
|
11580
11782
|
if (leftPaneEl) {
|
|
11581
|
-
leftPaneEl.addEventListener("mousedown", () =>
|
|
11582
|
-
leftPaneEl.addEventListener("focusin", () =>
|
|
11783
|
+
leftPaneEl.addEventListener("mousedown", (event) => activatePaneFromInteraction("left", event));
|
|
11784
|
+
leftPaneEl.addEventListener("focusin", (event) => activatePaneFromInteraction("left", event));
|
|
11583
11785
|
}
|
|
11584
11786
|
|
|
11585
11787
|
if (rightPaneEl) {
|
|
11586
|
-
rightPaneEl.addEventListener("mousedown", () =>
|
|
11587
|
-
rightPaneEl.addEventListener("focusin", () =>
|
|
11788
|
+
rightPaneEl.addEventListener("mousedown", (event) => activatePaneFromInteraction("right", event));
|
|
11789
|
+
rightPaneEl.addEventListener("focusin", (event) => activatePaneFromInteraction("right", event));
|
|
11588
11790
|
}
|
|
11589
11791
|
|
|
11590
11792
|
if (leftFocusBtn) {
|
|
@@ -12074,10 +12276,6 @@
|
|
|
12074
12276
|
if (openCompanionBtn) {
|
|
12075
12277
|
openCompanionBtn.addEventListener("click", () => {
|
|
12076
12278
|
const content = sourceTextEl.value;
|
|
12077
|
-
if (!content.trim()) {
|
|
12078
|
-
setStatus("Editor is empty. Nothing to copy into a companion view.", "warning");
|
|
12079
|
-
return;
|
|
12080
|
-
}
|
|
12081
12279
|
|
|
12082
12280
|
const requestId = beginUiAction("open_editor_only");
|
|
12083
12281
|
if (!requestId) return;
|
package/client/studio.css
CHANGED
|
@@ -1232,6 +1232,10 @@
|
|
|
1232
1232
|
position: relative;
|
|
1233
1233
|
}
|
|
1234
1234
|
|
|
1235
|
+
.rendered-markdown blockquote.studio-copyable-block {
|
|
1236
|
+
padding-right: 3.6rem;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1235
1239
|
.rendered-markdown .studio-copy-block-btn {
|
|
1236
1240
|
position: absolute;
|
|
1237
1241
|
top: 8px;
|
|
@@ -2088,6 +2092,48 @@
|
|
|
2088
2092
|
background: var(--panel);
|
|
2089
2093
|
}
|
|
2090
2094
|
|
|
2095
|
+
.trace-image-gallery {
|
|
2096
|
+
display: grid;
|
|
2097
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
|
|
2098
|
+
gap: 8px;
|
|
2099
|
+
padding: 8px;
|
|
2100
|
+
border: 1px solid var(--panel-border);
|
|
2101
|
+
border-radius: 8px;
|
|
2102
|
+
background: var(--panel);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
.trace-output-wrap + .trace-image-gallery,
|
|
2106
|
+
.trace-output + .trace-image-gallery {
|
|
2107
|
+
margin-top: 4px;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
.trace-image-card {
|
|
2111
|
+
display: flex;
|
|
2112
|
+
flex-direction: column;
|
|
2113
|
+
gap: 5px;
|
|
2114
|
+
min-width: 0;
|
|
2115
|
+
margin: 0;
|
|
2116
|
+
padding: 7px;
|
|
2117
|
+
border: 1px solid var(--border-subtle);
|
|
2118
|
+
border-radius: 7px;
|
|
2119
|
+
background: var(--panel-2);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
.trace-image-card img {
|
|
2123
|
+
display: block;
|
|
2124
|
+
width: 100%;
|
|
2125
|
+
max-height: 520px;
|
|
2126
|
+
object-fit: contain;
|
|
2127
|
+
border-radius: 5px;
|
|
2128
|
+
background: var(--panel);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
.trace-image-caption {
|
|
2132
|
+
color: var(--muted);
|
|
2133
|
+
font-size: 11px;
|
|
2134
|
+
overflow-wrap: anywhere;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2091
2137
|
.trace-output-truncation {
|
|
2092
2138
|
display: flex;
|
|
2093
2139
|
align-items: center;
|
package/index.ts
CHANGED
|
@@ -196,6 +196,14 @@ interface StudioTraceAssistantEntry {
|
|
|
196
196
|
stopReason: string | null;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
interface StudioTraceImage {
|
|
200
|
+
id: string;
|
|
201
|
+
mimeType: string;
|
|
202
|
+
data: string;
|
|
203
|
+
byteLength: number | null;
|
|
204
|
+
label: string | null;
|
|
205
|
+
}
|
|
206
|
+
|
|
199
207
|
interface StudioTraceToolEntry {
|
|
200
208
|
id: string;
|
|
201
209
|
type: "tool";
|
|
@@ -204,6 +212,7 @@ interface StudioTraceToolEntry {
|
|
|
204
212
|
label: string | null;
|
|
205
213
|
argsSummary: string | null;
|
|
206
214
|
output: string;
|
|
215
|
+
images: StudioTraceImage[];
|
|
207
216
|
startedAt: number;
|
|
208
217
|
updatedAt: number;
|
|
209
218
|
status: StudioTraceEntryStatus;
|
|
@@ -345,6 +354,11 @@ const MAX_PREPARED_PDF_EXPORTS = 8;
|
|
|
345
354
|
const MAX_PREPARED_HTML_EXPORTS = 8;
|
|
346
355
|
const STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES = 80;
|
|
347
356
|
const STUDIO_TRACE_SNAPSHOT_MAX_FIELD_CHARS = 20_000;
|
|
357
|
+
const STUDIO_TRACE_IMAGE_MAX_COUNT = 8;
|
|
358
|
+
const STUDIO_TRACE_IMAGE_MAX_BASE64_CHARS = 2_500_000;
|
|
359
|
+
const STUDIO_TRACE_SNAPSHOT_MAX_IMAGES = 12;
|
|
360
|
+
const STUDIO_TRACE_SNAPSHOT_MAX_IMAGE_BASE64_CHARS = 6_000_000;
|
|
361
|
+
const STUDIO_TRACE_IMAGE_SAFE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
348
362
|
const MAX_STUDIO_TRACE_SNAPSHOTS = RESPONSE_HISTORY_LIMIT;
|
|
349
363
|
const TRANSIENT_STUDIO_DOCUMENT_TTL_MS = 30 * 60 * 1000;
|
|
350
364
|
const MAX_TRANSIENT_STUDIO_DOCUMENTS = 16;
|
|
@@ -3457,6 +3471,49 @@ function normalizeStudioMarkdownFencedBlocks(markdown: string): string {
|
|
|
3457
3471
|
return out.join("\n");
|
|
3458
3472
|
}
|
|
3459
3473
|
|
|
3474
|
+
interface StudioYamlFrontMatterSplit {
|
|
3475
|
+
frontMatter: string;
|
|
3476
|
+
body: string;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
function splitStudioYamlFrontMatter(markdown: string): StudioYamlFrontMatterSplit | null {
|
|
3480
|
+
const source = String(markdown ?? "");
|
|
3481
|
+
const match = source.match(/^(\uFEFF?---[ \t]*(?:\r?\n)[\s\S]*?(?:\r?\n)---[ \t]*(?:\r?\n|$))([\s\S]*)$/);
|
|
3482
|
+
if (!match) return null;
|
|
3483
|
+
return {
|
|
3484
|
+
frontMatter: match[1] ?? "",
|
|
3485
|
+
body: match[2] ?? "",
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
function mapStudioMarkdownBodyPreservingYamlFrontMatter(markdown: string, transformBody: (body: string) => string): string {
|
|
3490
|
+
const source = String(markdown ?? "");
|
|
3491
|
+
const split = splitStudioYamlFrontMatter(source);
|
|
3492
|
+
if (!split) return transformBody(source);
|
|
3493
|
+
return `${split.frontMatter}${transformBody(split.body)}`;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
function stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdown: string): string {
|
|
3497
|
+
return mapStudioMarkdownBodyPreservingYamlFrontMatter(markdown, (body) => stripStudioMarkdownHtmlComments(body));
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
function hasStudioYamlHeaderIncludes(markdown: string): boolean {
|
|
3501
|
+
const split = splitStudioYamlFrontMatter(markdown);
|
|
3502
|
+
if (!split) return false;
|
|
3503
|
+
return /^\s*header-includes\s*:/im.test(split.frontMatter);
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
function prepareStudioMarkdownForPandoc(markdown: string, options?: { preserveLiteralLatexCommands?: boolean }): string {
|
|
3507
|
+
const shouldPreserveLiteralLatexCommands = options?.preserveLiteralLatexCommands !== false;
|
|
3508
|
+
return mapStudioMarkdownBodyPreservingYamlFrontMatter(markdown, (body) => {
|
|
3509
|
+
const normalizedMath = normalizeMathDelimiters(body);
|
|
3510
|
+
const latexReady = shouldPreserveLiteralLatexCommands
|
|
3511
|
+
? preserveLiteralLatexCommandsInMarkdown(normalizedMath)
|
|
3512
|
+
: normalizedMath;
|
|
3513
|
+
return normalizeObsidianImages(latexReady);
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3460
3517
|
function hasStudioMarkdownDiffFence(markdown: string): boolean {
|
|
3461
3518
|
const lines = String(markdown ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
3462
3519
|
|
|
@@ -4300,8 +4357,10 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
|
|
|
4300
4357
|
const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
|
|
4301
4358
|
? replaceStudioAnnotationMarkersForPdf(source)
|
|
4302
4359
|
: source;
|
|
4303
|
-
const commentStrippedSource =
|
|
4304
|
-
return
|
|
4360
|
+
const commentStrippedSource = stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(annotationReadySource);
|
|
4361
|
+
return prepareStudioMarkdownForPandoc(commentStrippedSource, {
|
|
4362
|
+
preserveLiteralLatexCommands: !hasStudioYamlHeaderIncludes(annotationReadySource),
|
|
4363
|
+
});
|
|
4305
4364
|
}
|
|
4306
4365
|
|
|
4307
4366
|
function stripMathMlAnnotationTags(html: string): string {
|
|
@@ -4559,7 +4618,7 @@ function decorateStudioPandocSyntaxHtml(html: string): string {
|
|
|
4559
4618
|
|
|
4560
4619
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
4561
4620
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
4562
|
-
const markdownWithoutHtmlComments = isLatex ? markdown :
|
|
4621
|
+
const markdownWithoutHtmlComments = isLatex ? markdown : stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdown);
|
|
4563
4622
|
const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
|
|
4564
4623
|
const latexSubfigurePreviewTransform = isLatex
|
|
4565
4624
|
? preprocessStudioLatexSubfiguresForPreview(markdownWithPreviewPageBreaks)
|
|
@@ -4580,7 +4639,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
4580
4639
|
}
|
|
4581
4640
|
const normalizedMarkdown = isLatex
|
|
4582
4641
|
? sourceWithResolvedRefs
|
|
4583
|
-
: normalizeStudioMarkdownFencedBlocks(
|
|
4642
|
+
: normalizeStudioMarkdownFencedBlocks(prepareStudioMarkdownForPandoc(sourceWithResolvedRefs));
|
|
4584
4643
|
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
4585
4644
|
|
|
4586
4645
|
let renderedHtml = await new Promise<string>((resolve, reject) => {
|
|
@@ -5501,6 +5560,8 @@ async function renderStudioPdfWithPandoc(
|
|
|
5501
5560
|
await mkdir(tempDir, { recursive: true });
|
|
5502
5561
|
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
5503
5562
|
|
|
5563
|
+
const hasYamlHeaderIncludesForPdf = inputFormat !== "latex" && hasStudioYamlHeaderIncludes(markdownForPdf);
|
|
5564
|
+
const headerIncludeArgs = hasYamlHeaderIncludesForPdf ? [] : ["--include-in-header", preamblePath];
|
|
5504
5565
|
const args = [
|
|
5505
5566
|
"-f", inputFormat,
|
|
5506
5567
|
"-o", outputPath,
|
|
@@ -5508,7 +5569,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
5508
5569
|
...buildStudioPdfPandocVariableArgs(pdfOptions, inputFormat !== "latex"),
|
|
5509
5570
|
"-V", "urlcolor=blue",
|
|
5510
5571
|
"-V", "linkcolor=blue",
|
|
5511
|
-
|
|
5572
|
+
...headerIncludeArgs,
|
|
5512
5573
|
...bibliographyArgs,
|
|
5513
5574
|
];
|
|
5514
5575
|
if (resourcePath) args.push(`--resource-path=${resourcePath}`);
|
|
@@ -5652,6 +5713,8 @@ async function renderStudioPdfWithPandoc(
|
|
|
5652
5713
|
return { pdf: rendered.pdf, warning: mermaidPrepared.warning ?? rendered.warning };
|
|
5653
5714
|
}
|
|
5654
5715
|
|
|
5716
|
+
const hasYamlHeaderIncludesForPdf = !isLatex && hasStudioYamlHeaderIncludes(markdownForPdf);
|
|
5717
|
+
const headerIncludeArgs = hasYamlHeaderIncludesForPdf ? [] : ["--include-in-header", preamblePath];
|
|
5655
5718
|
const args = [
|
|
5656
5719
|
"-f", inputFormat,
|
|
5657
5720
|
"-o", outputPath,
|
|
@@ -5659,7 +5722,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
5659
5722
|
...buildStudioPdfPandocVariableArgs(pdfOptions, !isLatex),
|
|
5660
5723
|
"-V", "urlcolor=blue",
|
|
5661
5724
|
"-V", "linkcolor=blue",
|
|
5662
|
-
|
|
5725
|
+
...headerIncludeArgs,
|
|
5663
5726
|
...bibliographyArgs,
|
|
5664
5727
|
];
|
|
5665
5728
|
if (resourcePath) args.push(`--resource-path=${resourcePath}`);
|
|
@@ -6600,9 +6663,44 @@ function truncateStudioTraceSnapshotText(text: string, maxChars = STUDIO_TRACE_S
|
|
|
6600
6663
|
};
|
|
6601
6664
|
}
|
|
6602
6665
|
|
|
6666
|
+
function copyStudioTraceImagesForSnapshot(
|
|
6667
|
+
images: StudioTraceImage[] | undefined,
|
|
6668
|
+
budget: { remainingImages: number; remainingBase64Chars: number },
|
|
6669
|
+
): { images: StudioTraceImage[]; omitted: number } {
|
|
6670
|
+
const copied: StudioTraceImage[] = [];
|
|
6671
|
+
let omitted = 0;
|
|
6672
|
+
for (const image of Array.isArray(images) ? images : []) {
|
|
6673
|
+
if (!image || typeof image !== "object") continue;
|
|
6674
|
+
const mimeType = normalizeStudioTraceImageMimeType(image.mimeType);
|
|
6675
|
+
const data = typeof image.data === "string" ? image.data : "";
|
|
6676
|
+
if (!data || !isStudioTraceSafeImageMimeType(mimeType)) {
|
|
6677
|
+
omitted += 1;
|
|
6678
|
+
continue;
|
|
6679
|
+
}
|
|
6680
|
+
if (budget.remainingImages <= 0 || data.length > budget.remainingBase64Chars) {
|
|
6681
|
+
omitted += 1;
|
|
6682
|
+
continue;
|
|
6683
|
+
}
|
|
6684
|
+
copied.push({
|
|
6685
|
+
id: typeof image.id === "string" && image.id.trim() ? image.id : `trace-image-snapshot-${copied.length + 1}`,
|
|
6686
|
+
mimeType,
|
|
6687
|
+
data,
|
|
6688
|
+
byteLength: typeof image.byteLength === "number" && Number.isFinite(image.byteLength) ? image.byteLength : estimateStudioTraceBase64ByteLength(data),
|
|
6689
|
+
label: typeof image.label === "string" && image.label.trim() ? image.label : null,
|
|
6690
|
+
});
|
|
6691
|
+
budget.remainingImages -= 1;
|
|
6692
|
+
budget.remainingBase64Chars -= data.length;
|
|
6693
|
+
}
|
|
6694
|
+
return { images: copied, omitted };
|
|
6695
|
+
}
|
|
6696
|
+
|
|
6603
6697
|
function createStudioTraceSnapshot(source: StudioTraceState): { traceState: StudioTraceState; truncated: boolean } {
|
|
6604
6698
|
let truncated = false;
|
|
6605
6699
|
const sourceEntries = Array.isArray(source.entries) ? source.entries : [];
|
|
6700
|
+
const imageBudget = {
|
|
6701
|
+
remainingImages: STUDIO_TRACE_SNAPSHOT_MAX_IMAGES,
|
|
6702
|
+
remainingBase64Chars: STUDIO_TRACE_SNAPSHOT_MAX_IMAGE_BASE64_CHARS,
|
|
6703
|
+
};
|
|
6606
6704
|
const entries = sourceEntries.slice(-STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES).map((entry) => {
|
|
6607
6705
|
if (entry.type === "assistant") {
|
|
6608
6706
|
const thinking = truncateStudioTraceSnapshotText(entry.thinking);
|
|
@@ -6616,11 +6714,16 @@ function createStudioTraceSnapshot(source: StudioTraceState): { traceState: Stud
|
|
|
6616
6714
|
}
|
|
6617
6715
|
const argsSummary = truncateStudioTraceSnapshotText(entry.argsSummary ?? "");
|
|
6618
6716
|
const output = truncateStudioTraceSnapshotText(entry.output);
|
|
6619
|
-
|
|
6717
|
+
const snapshotImages = copyStudioTraceImagesForSnapshot(entry.images, imageBudget);
|
|
6718
|
+
truncated = truncated || argsSummary.truncated || output.truncated || snapshotImages.omitted > 0;
|
|
6719
|
+
const omittedImageNote = snapshotImages.omitted > 0
|
|
6720
|
+
? `[${snapshotImages.omitted} image preview${snapshotImages.omitted === 1 ? "" : "s"} omitted from saved Working view to keep history bounded.]`
|
|
6721
|
+
: "";
|
|
6620
6722
|
return {
|
|
6621
6723
|
...entry,
|
|
6622
6724
|
argsSummary: argsSummary.text || null,
|
|
6623
|
-
output: output.text,
|
|
6725
|
+
output: [output.text, omittedImageNote].filter(Boolean).join("\n"),
|
|
6726
|
+
images: snapshotImages.images,
|
|
6624
6727
|
};
|
|
6625
6728
|
});
|
|
6626
6729
|
if (sourceEntries.length > entries.length) truncated = true;
|
|
@@ -6657,29 +6760,102 @@ function sanitizeStudioTraceOutputText(text: string): string {
|
|
|
6657
6760
|
.replace(/\b[A-Za-z0-9+/]{3000,}={0,2}\b/g, "[base64 data omitted]");
|
|
6658
6761
|
}
|
|
6659
6762
|
|
|
6763
|
+
function normalizeStudioTraceImageMimeType(value: unknown): string {
|
|
6764
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
6765
|
+
}
|
|
6766
|
+
|
|
6767
|
+
function getStudioTraceImageMimeType(block: unknown): string {
|
|
6768
|
+
if (!block || typeof block !== "object") return "";
|
|
6769
|
+
const payload = block as Record<string, unknown>;
|
|
6770
|
+
const source = payload.source && typeof payload.source === "object" ? payload.source as Record<string, unknown> : null;
|
|
6771
|
+
return normalizeStudioTraceImageMimeType(
|
|
6772
|
+
payload.mimeType
|
|
6773
|
+
?? payload.mediaType
|
|
6774
|
+
?? payload.media_type
|
|
6775
|
+
?? source?.mimeType
|
|
6776
|
+
?? source?.mediaType
|
|
6777
|
+
?? source?.media_type,
|
|
6778
|
+
);
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6660
6781
|
function isStudioTraceImageBlock(block: unknown): boolean {
|
|
6661
6782
|
if (!block || typeof block !== "object") return false;
|
|
6662
6783
|
const payload = block as Record<string, unknown>;
|
|
6663
6784
|
const type = typeof payload.type === "string" ? payload.type.toLowerCase() : "";
|
|
6664
6785
|
if (type.includes("image")) return true;
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6786
|
+
return getStudioTraceImageMimeType(block).startsWith("image/");
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6789
|
+
function isStudioTraceSafeImageMimeType(mimeType: string): boolean {
|
|
6790
|
+
return STUDIO_TRACE_IMAGE_SAFE_MIME_TYPES.has(normalizeStudioTraceImageMimeType(mimeType));
|
|
6791
|
+
}
|
|
6792
|
+
|
|
6793
|
+
function getStudioTraceImageData(block: unknown): string | null {
|
|
6794
|
+
if (!block || typeof block !== "object") return null;
|
|
6795
|
+
const payload = block as Record<string, unknown>;
|
|
6796
|
+
if (typeof payload.data === "string") return payload.data;
|
|
6669
6797
|
const source = payload.source && typeof payload.source === "object" ? payload.source as Record<string, unknown> : null;
|
|
6670
|
-
|
|
6671
|
-
return
|
|
6798
|
+
if (source && typeof source.data === "string") return source.data;
|
|
6799
|
+
return null;
|
|
6800
|
+
}
|
|
6801
|
+
|
|
6802
|
+
function normalizeStudioTraceBase64Data(data: string): string | null {
|
|
6803
|
+
const compact = String(data || "").replace(/\s+/g, "");
|
|
6804
|
+
if (!compact || !/^[A-Za-z0-9+/]*={0,2}$/.test(compact)) return null;
|
|
6805
|
+
return compact;
|
|
6672
6806
|
}
|
|
6673
6807
|
|
|
6674
|
-
function
|
|
6808
|
+
function estimateStudioTraceBase64ByteLength(data: string): number | null {
|
|
6809
|
+
const compact = normalizeStudioTraceBase64Data(data);
|
|
6810
|
+
if (!compact) return null;
|
|
6811
|
+
const padding = compact.endsWith("==") ? 2 : (compact.endsWith("=") ? 1 : 0);
|
|
6812
|
+
return Math.max(0, Math.floor((compact.length * 3) / 4) - padding);
|
|
6813
|
+
}
|
|
6814
|
+
|
|
6815
|
+
function formatStudioTraceByteSize(bytes: number | null): string {
|
|
6816
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) return "unknown size";
|
|
6817
|
+
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
|
6818
|
+
const kib = bytes / 1024;
|
|
6819
|
+
if (kib < 1024) return `${kib.toFixed(kib >= 100 ? 0 : 1).replace(/\.0$/, "")} KB`;
|
|
6820
|
+
const mib = kib / 1024;
|
|
6821
|
+
return `${mib.toFixed(mib >= 100 ? 0 : 1).replace(/\.0$/, "")} MB`;
|
|
6822
|
+
}
|
|
6823
|
+
|
|
6824
|
+
function describeStudioTraceImageBlock(block: unknown, reason?: string): string {
|
|
6825
|
+
const mime = getStudioTraceImageMimeType(block) || "image";
|
|
6826
|
+
return `[Image: ${mime}${reason ? ` ${reason}` : ""}]`;
|
|
6827
|
+
}
|
|
6828
|
+
|
|
6829
|
+
function collectStudioTraceImageBlock(block: unknown, images: StudioTraceImage[]): string {
|
|
6830
|
+
const mimeType = getStudioTraceImageMimeType(block) || "image/unknown";
|
|
6831
|
+
if (!isStudioTraceSafeImageMimeType(mimeType)) {
|
|
6832
|
+
return describeStudioTraceImageBlock(block, "omitted from Working view: unsupported image type");
|
|
6833
|
+
}
|
|
6834
|
+
if (images.length >= STUDIO_TRACE_IMAGE_MAX_COUNT) {
|
|
6835
|
+
return describeStudioTraceImageBlock(block, "omitted from Working view: image count limit reached");
|
|
6836
|
+
}
|
|
6837
|
+
const data = getStudioTraceImageData(block);
|
|
6838
|
+
const normalizedData = data ? normalizeStudioTraceBase64Data(data) : null;
|
|
6839
|
+
if (!normalizedData) {
|
|
6840
|
+
return describeStudioTraceImageBlock(block, "omitted from Working view: no base64 data");
|
|
6841
|
+
}
|
|
6842
|
+
if (normalizedData.length > STUDIO_TRACE_IMAGE_MAX_BASE64_CHARS) {
|
|
6843
|
+
const estimatedBytes = estimateStudioTraceBase64ByteLength(normalizedData);
|
|
6844
|
+
return describeStudioTraceImageBlock(block, `omitted from Working view: ${formatStudioTraceByteSize(estimatedBytes)} exceeds image preview limit`);
|
|
6845
|
+
}
|
|
6675
6846
|
const payload = (block && typeof block === "object") ? block as Record<string, unknown> : {};
|
|
6676
|
-
const
|
|
6677
|
-
const
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6847
|
+
const hash = createHash("sha256").update(mimeType).update(normalizedData).digest("hex").slice(0, 16);
|
|
6848
|
+
const image: StudioTraceImage = {
|
|
6849
|
+
id: `trace-image-${hash}-${images.length + 1}`,
|
|
6850
|
+
mimeType,
|
|
6851
|
+
data: normalizedData,
|
|
6852
|
+
byteLength: estimateStudioTraceBase64ByteLength(normalizedData),
|
|
6853
|
+
label: typeof payload.label === "string" && payload.label.trim()
|
|
6854
|
+
? payload.label.trim()
|
|
6855
|
+
: (typeof payload.alt === "string" && payload.alt.trim() ? payload.alt.trim() : null),
|
|
6856
|
+
};
|
|
6857
|
+
images.push(image);
|
|
6858
|
+
return "";
|
|
6683
6859
|
}
|
|
6684
6860
|
|
|
6685
6861
|
function stringifyStudioTraceObject(value: unknown): string {
|
|
@@ -6696,19 +6872,19 @@ function stringifyStudioTraceObject(value: unknown): string {
|
|
|
6696
6872
|
}
|
|
6697
6873
|
}
|
|
6698
6874
|
|
|
6699
|
-
function
|
|
6875
|
+
function formatStudioTraceOutputPart(result: unknown, images: StudioTraceImage[]): string {
|
|
6700
6876
|
if (result == null) return "";
|
|
6701
6877
|
if (typeof result === "string") return sanitizeStudioTraceOutputText(result);
|
|
6702
6878
|
if (Array.isArray(result)) {
|
|
6703
|
-
return result.map((item) =>
|
|
6879
|
+
return result.map((item) => formatStudioTraceOutputPart(item, images)).filter(Boolean).join("\n");
|
|
6704
6880
|
}
|
|
6705
6881
|
if (typeof result === "object") {
|
|
6706
|
-
if (isStudioTraceImageBlock(result)) return
|
|
6882
|
+
if (isStudioTraceImageBlock(result)) return collectStudioTraceImageBlock(result, images);
|
|
6707
6883
|
const payload = result as { content?: Array<{ type?: string; text?: string }> };
|
|
6708
6884
|
if (Array.isArray(payload.content)) {
|
|
6709
6885
|
return payload.content
|
|
6710
6886
|
.map((block) => {
|
|
6711
|
-
if (isStudioTraceImageBlock(block)) return
|
|
6887
|
+
if (isStudioTraceImageBlock(block)) return collectStudioTraceImageBlock(block, images);
|
|
6712
6888
|
if (block && block.type === "text" && typeof block.text === "string") return sanitizeStudioTraceOutputText(block.text);
|
|
6713
6889
|
return stringifyStudioTraceObject(block);
|
|
6714
6890
|
})
|
|
@@ -6720,6 +6896,18 @@ function formatStudioTraceOutput(result: unknown): string {
|
|
|
6720
6896
|
return sanitizeStudioTraceOutputText(String(result));
|
|
6721
6897
|
}
|
|
6722
6898
|
|
|
6899
|
+
function formatStudioTraceToolResult(result: unknown): { output: string; images: StudioTraceImage[] } {
|
|
6900
|
+
const images: StudioTraceImage[] = [];
|
|
6901
|
+
return {
|
|
6902
|
+
output: formatStudioTraceOutputPart(result, images),
|
|
6903
|
+
images,
|
|
6904
|
+
};
|
|
6905
|
+
}
|
|
6906
|
+
|
|
6907
|
+
function formatStudioTraceOutput(result: unknown): string {
|
|
6908
|
+
return formatStudioTraceToolResult(result).output;
|
|
6909
|
+
}
|
|
6910
|
+
|
|
6723
6911
|
function summarizeStudioTraceToolArgs(toolName: string, args: unknown): string | null {
|
|
6724
6912
|
const normalizedTool = String(toolName || "").trim().toLowerCase();
|
|
6725
6913
|
const payload = (args && typeof args === "object") ? (args as Record<string, unknown>) : {};
|
|
@@ -8102,6 +8290,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8102
8290
|
label: deriveToolActivityLabel(toolName, args),
|
|
8103
8291
|
argsSummary: summarizeStudioTraceToolArgs(toolName, args),
|
|
8104
8292
|
output: "",
|
|
8293
|
+
images: [],
|
|
8105
8294
|
startedAt: now,
|
|
8106
8295
|
updatedAt: now,
|
|
8107
8296
|
status: "pending",
|
|
@@ -8119,9 +8308,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
8119
8308
|
output: string,
|
|
8120
8309
|
status: StudioTraceEntryStatus,
|
|
8121
8310
|
isError: boolean,
|
|
8311
|
+
images?: StudioTraceImage[],
|
|
8122
8312
|
) => {
|
|
8123
8313
|
const entry = ensureStudioTraceToolEntry(toolCallId, toolName, args);
|
|
8124
8314
|
entry.output = output;
|
|
8315
|
+
if (Array.isArray(images)) entry.images = images;
|
|
8125
8316
|
entry.status = status;
|
|
8126
8317
|
entry.isError = isError;
|
|
8127
8318
|
entry.updatedAt = Date.now();
|
|
@@ -8599,9 +8790,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
8599
8790
|
}
|
|
8600
8791
|
|
|
8601
8792
|
const resourceDir = resolveStudioCompanionResourceDir(msg.path, msg.resourceDir, studioCwd);
|
|
8793
|
+
const hasContent = msg.content.trim().length > 0;
|
|
8602
8794
|
const document: InitialStudioDocument = {
|
|
8603
8795
|
text: msg.content,
|
|
8604
|
-
label: buildStudioCompanionLabel(msg.label),
|
|
8796
|
+
label: hasContent ? buildStudioCompanionLabel(msg.label) : "blank companion editor",
|
|
8605
8797
|
source: "blank",
|
|
8606
8798
|
draftId: createStudioDraftId(),
|
|
8607
8799
|
resourceDir,
|
|
@@ -8614,7 +8806,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
8614
8806
|
requestId: msg.requestId,
|
|
8615
8807
|
url,
|
|
8616
8808
|
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
8617
|
-
message:
|
|
8809
|
+
message: hasContent
|
|
8810
|
+
? "Companion editor is ready with a detached copy of the current editor text."
|
|
8811
|
+
: "Blank companion editor is ready.",
|
|
8618
8812
|
});
|
|
8619
8813
|
return;
|
|
8620
8814
|
}
|
|
@@ -10181,25 +10375,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
10181
10375
|
|
|
10182
10376
|
pi.on("tool_execution_update", async (event) => {
|
|
10183
10377
|
if (!agentBusy) return;
|
|
10378
|
+
const formatted = formatStudioTraceToolResult(event.partialResult);
|
|
10184
10379
|
updateStudioTraceToolEntry(
|
|
10185
10380
|
event.toolCallId,
|
|
10186
10381
|
event.toolName,
|
|
10187
10382
|
event.args,
|
|
10188
|
-
|
|
10383
|
+
formatted.output,
|
|
10189
10384
|
"streaming",
|
|
10190
10385
|
false,
|
|
10386
|
+
formatted.images,
|
|
10191
10387
|
);
|
|
10192
10388
|
});
|
|
10193
10389
|
|
|
10194
10390
|
pi.on("tool_execution_end", async (event) => {
|
|
10195
10391
|
if (!agentBusy) return;
|
|
10392
|
+
const formatted = formatStudioTraceToolResult(event.result);
|
|
10196
10393
|
updateStudioTraceToolEntry(
|
|
10197
10394
|
event.toolCallId,
|
|
10198
10395
|
event.toolName,
|
|
10199
10396
|
undefined,
|
|
10200
|
-
|
|
10397
|
+
formatted.output,
|
|
10201
10398
|
event.isError ? "error" : "complete",
|
|
10202
10399
|
Boolean(event.isError),
|
|
10400
|
+
formatted.images,
|
|
10203
10401
|
);
|
|
10204
10402
|
emitDebugEvent("tool_execution_end", { toolName: event.toolName, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
|
|
10205
10403
|
// Keep tool phase visible until the next tool call, assistant response phase,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
|
|
3
|
+
"version": "0.8.4",
|
|
4
|
+
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code/interactive HTML preview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|