pi-studio 0.8.3 → 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 +6 -0
- package/README.md +2 -2
- package/client/studio-client.js +157 -3
- package/client/studio.css +46 -0
- package/index.ts +168 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ 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
|
+
|
|
7
13
|
## [0.8.3] — 2026-05-13
|
|
8
14
|
|
|
9
15
|
### Changed
|
package/README.md
CHANGED
|
@@ -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,7 +34,7 @@ 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
|
|
37
|
+
- Renders Markdown/LaTeX/code previews (math + Mermaid), theme-synced with pi, with copy buttons for code blocks and blockquotes
|
|
38
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
|
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
|
}
|
|
@@ -4267,6 +4324,78 @@
|
|
|
4267
4324
|
return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
|
|
4268
4325
|
}
|
|
4269
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
|
+
|
|
4270
4399
|
function getCopyablePreviewBlockText(blockEl) {
|
|
4271
4400
|
if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
|
|
4272
4401
|
if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
|
|
@@ -4277,6 +4406,10 @@
|
|
|
4277
4406
|
);
|
|
4278
4407
|
}
|
|
4279
4408
|
|
|
4409
|
+
if (blockEl.matches && blockEl.matches("blockquote")) {
|
|
4410
|
+
return getCopyableBlockquoteText(blockEl);
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4280
4413
|
const codeEl = typeof blockEl.querySelector === "function"
|
|
4281
4414
|
? blockEl.querySelector("pre code, code")
|
|
4282
4415
|
: null;
|
|
@@ -4334,7 +4467,7 @@
|
|
|
4334
4467
|
|
|
4335
4468
|
function decorateCopyablePreviewBlocks(targetEl) {
|
|
4336
4469
|
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
4337
|
-
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"));
|
|
4338
4471
|
blocks.forEach((blockEl) => {
|
|
4339
4472
|
if (!blockEl || !(blockEl instanceof Element)) return;
|
|
4340
4473
|
if (blockEl.dataset && blockEl.dataset.studioCopyDecorated === "1") return;
|
|
@@ -4572,6 +4705,23 @@
|
|
|
4572
4705
|
+ "</div>";
|
|
4573
4706
|
}
|
|
4574
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
|
+
|
|
4575
4725
|
function buildTracePanelHtml() {
|
|
4576
4726
|
const state = traceState || createEmptyTraceState();
|
|
4577
4727
|
const filter = normalizeTraceFilter(traceFilter);
|
|
@@ -4665,8 +4815,12 @@
|
|
|
4665
4815
|
const argsSummary = entry.argsSummary
|
|
4666
4816
|
? "<div class='trace-section'><div class='trace-section-label'>Input</div>" + renderTraceOutput(entry.argsSummary, entry.id + ":input") + "</div>"
|
|
4667
4817
|
: "";
|
|
4668
|
-
const
|
|
4669
|
-
|
|
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>"
|
|
4670
4824
|
: "<div class='trace-empty-inline'>No output yet.</div>";
|
|
4671
4825
|
const toolStatusLabel = entry.isError
|
|
4672
4826
|
? "Error"
|
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;
|
|
@@ -6649,9 +6663,44 @@ function truncateStudioTraceSnapshotText(text: string, maxChars = STUDIO_TRACE_S
|
|
|
6649
6663
|
};
|
|
6650
6664
|
}
|
|
6651
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
|
+
|
|
6652
6697
|
function createStudioTraceSnapshot(source: StudioTraceState): { traceState: StudioTraceState; truncated: boolean } {
|
|
6653
6698
|
let truncated = false;
|
|
6654
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
|
+
};
|
|
6655
6704
|
const entries = sourceEntries.slice(-STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES).map((entry) => {
|
|
6656
6705
|
if (entry.type === "assistant") {
|
|
6657
6706
|
const thinking = truncateStudioTraceSnapshotText(entry.thinking);
|
|
@@ -6665,11 +6714,16 @@ function createStudioTraceSnapshot(source: StudioTraceState): { traceState: Stud
|
|
|
6665
6714
|
}
|
|
6666
6715
|
const argsSummary = truncateStudioTraceSnapshotText(entry.argsSummary ?? "");
|
|
6667
6716
|
const output = truncateStudioTraceSnapshotText(entry.output);
|
|
6668
|
-
|
|
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
|
+
: "";
|
|
6669
6722
|
return {
|
|
6670
6723
|
...entry,
|
|
6671
6724
|
argsSummary: argsSummary.text || null,
|
|
6672
|
-
output: output.text,
|
|
6725
|
+
output: [output.text, omittedImageNote].filter(Boolean).join("\n"),
|
|
6726
|
+
images: snapshotImages.images,
|
|
6673
6727
|
};
|
|
6674
6728
|
});
|
|
6675
6729
|
if (sourceEntries.length > entries.length) truncated = true;
|
|
@@ -6706,29 +6760,102 @@ function sanitizeStudioTraceOutputText(text: string): string {
|
|
|
6706
6760
|
.replace(/\b[A-Za-z0-9+/]{3000,}={0,2}\b/g, "[base64 data omitted]");
|
|
6707
6761
|
}
|
|
6708
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
|
+
|
|
6709
6781
|
function isStudioTraceImageBlock(block: unknown): boolean {
|
|
6710
6782
|
if (!block || typeof block !== "object") return false;
|
|
6711
6783
|
const payload = block as Record<string, unknown>;
|
|
6712
6784
|
const type = typeof payload.type === "string" ? payload.type.toLowerCase() : "";
|
|
6713
6785
|
if (type.includes("image")) return true;
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
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;
|
|
6718
6797
|
const source = payload.source && typeof payload.source === "object" ? payload.source as Record<string, unknown> : null;
|
|
6719
|
-
|
|
6720
|
-
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;
|
|
6806
|
+
}
|
|
6807
|
+
|
|
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);
|
|
6721
6813
|
}
|
|
6722
6814
|
|
|
6723
|
-
function
|
|
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
|
+
}
|
|
6724
6846
|
const payload = (block && typeof block === "object") ? block as Record<string, unknown> : {};
|
|
6725
|
-
const
|
|
6726
|
-
const
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
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 "";
|
|
6732
6859
|
}
|
|
6733
6860
|
|
|
6734
6861
|
function stringifyStudioTraceObject(value: unknown): string {
|
|
@@ -6745,19 +6872,19 @@ function stringifyStudioTraceObject(value: unknown): string {
|
|
|
6745
6872
|
}
|
|
6746
6873
|
}
|
|
6747
6874
|
|
|
6748
|
-
function
|
|
6875
|
+
function formatStudioTraceOutputPart(result: unknown, images: StudioTraceImage[]): string {
|
|
6749
6876
|
if (result == null) return "";
|
|
6750
6877
|
if (typeof result === "string") return sanitizeStudioTraceOutputText(result);
|
|
6751
6878
|
if (Array.isArray(result)) {
|
|
6752
|
-
return result.map((item) =>
|
|
6879
|
+
return result.map((item) => formatStudioTraceOutputPart(item, images)).filter(Boolean).join("\n");
|
|
6753
6880
|
}
|
|
6754
6881
|
if (typeof result === "object") {
|
|
6755
|
-
if (isStudioTraceImageBlock(result)) return
|
|
6882
|
+
if (isStudioTraceImageBlock(result)) return collectStudioTraceImageBlock(result, images);
|
|
6756
6883
|
const payload = result as { content?: Array<{ type?: string; text?: string }> };
|
|
6757
6884
|
if (Array.isArray(payload.content)) {
|
|
6758
6885
|
return payload.content
|
|
6759
6886
|
.map((block) => {
|
|
6760
|
-
if (isStudioTraceImageBlock(block)) return
|
|
6887
|
+
if (isStudioTraceImageBlock(block)) return collectStudioTraceImageBlock(block, images);
|
|
6761
6888
|
if (block && block.type === "text" && typeof block.text === "string") return sanitizeStudioTraceOutputText(block.text);
|
|
6762
6889
|
return stringifyStudioTraceObject(block);
|
|
6763
6890
|
})
|
|
@@ -6769,6 +6896,18 @@ function formatStudioTraceOutput(result: unknown): string {
|
|
|
6769
6896
|
return sanitizeStudioTraceOutputText(String(result));
|
|
6770
6897
|
}
|
|
6771
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
|
+
|
|
6772
6911
|
function summarizeStudioTraceToolArgs(toolName: string, args: unknown): string | null {
|
|
6773
6912
|
const normalizedTool = String(toolName || "").trim().toLowerCase();
|
|
6774
6913
|
const payload = (args && typeof args === "object") ? (args as Record<string, unknown>) : {};
|
|
@@ -8151,6 +8290,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8151
8290
|
label: deriveToolActivityLabel(toolName, args),
|
|
8152
8291
|
argsSummary: summarizeStudioTraceToolArgs(toolName, args),
|
|
8153
8292
|
output: "",
|
|
8293
|
+
images: [],
|
|
8154
8294
|
startedAt: now,
|
|
8155
8295
|
updatedAt: now,
|
|
8156
8296
|
status: "pending",
|
|
@@ -8168,9 +8308,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
8168
8308
|
output: string,
|
|
8169
8309
|
status: StudioTraceEntryStatus,
|
|
8170
8310
|
isError: boolean,
|
|
8311
|
+
images?: StudioTraceImage[],
|
|
8171
8312
|
) => {
|
|
8172
8313
|
const entry = ensureStudioTraceToolEntry(toolCallId, toolName, args);
|
|
8173
8314
|
entry.output = output;
|
|
8315
|
+
if (Array.isArray(images)) entry.images = images;
|
|
8174
8316
|
entry.status = status;
|
|
8175
8317
|
entry.isError = isError;
|
|
8176
8318
|
entry.updatedAt = Date.now();
|
|
@@ -10233,25 +10375,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
10233
10375
|
|
|
10234
10376
|
pi.on("tool_execution_update", async (event) => {
|
|
10235
10377
|
if (!agentBusy) return;
|
|
10378
|
+
const formatted = formatStudioTraceToolResult(event.partialResult);
|
|
10236
10379
|
updateStudioTraceToolEntry(
|
|
10237
10380
|
event.toolCallId,
|
|
10238
10381
|
event.toolName,
|
|
10239
10382
|
event.args,
|
|
10240
|
-
|
|
10383
|
+
formatted.output,
|
|
10241
10384
|
"streaming",
|
|
10242
10385
|
false,
|
|
10386
|
+
formatted.images,
|
|
10243
10387
|
);
|
|
10244
10388
|
});
|
|
10245
10389
|
|
|
10246
10390
|
pi.on("tool_execution_end", async (event) => {
|
|
10247
10391
|
if (!agentBusy) return;
|
|
10392
|
+
const formatted = formatStudioTraceToolResult(event.result);
|
|
10248
10393
|
updateStudioTraceToolEntry(
|
|
10249
10394
|
event.toolCallId,
|
|
10250
10395
|
event.toolName,
|
|
10251
10396
|
undefined,
|
|
10252
|
-
|
|
10397
|
+
formatted.output,
|
|
10253
10398
|
event.isError ? "error" : "complete",
|
|
10254
10399
|
Boolean(event.isError),
|
|
10400
|
+
formatted.images,
|
|
10255
10401
|
);
|
|
10256
10402
|
emitDebugEvent("tool_execution_end", { toolName: event.toolName, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
|
|
10257
10403
|
// Keep tool phase visible until the next tool call, assistant response phase,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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",
|