pi-studio 0.9.24 → 0.9.26
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 +10 -0
- package/client/studio-client.js +163 -3
- package/client/studio.css +112 -0
- package/index.ts +24 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `pi-studio` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.9.26] — 2026-06-04
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Made Studio Markdown previews compatible with older Pandoc versions by falling back to `--self-contained` when `--embed-resources` is unavailable.
|
|
9
|
+
|
|
10
|
+
## [0.9.25] — 2026-06-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Render common Working-view tool inputs more readably, with summaries for edit/write/read/bash calls and raw inputs available behind collapsible details.
|
|
14
|
+
|
|
5
15
|
## [0.9.24] — 2026-06-01
|
|
6
16
|
|
|
7
17
|
### Added
|
package/client/studio-client.js
CHANGED
|
@@ -9018,6 +9018,166 @@
|
|
|
9018
9018
|
+ "</div>";
|
|
9019
9019
|
}
|
|
9020
9020
|
|
|
9021
|
+
function parseTraceToolArgsObject(inputText) {
|
|
9022
|
+
const value = String(inputText || "").trim();
|
|
9023
|
+
if (!value || (value[0] !== "{" && value[0] !== "[")) return null;
|
|
9024
|
+
try {
|
|
9025
|
+
const parsed = JSON.parse(value);
|
|
9026
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
9027
|
+
} catch {
|
|
9028
|
+
return null;
|
|
9029
|
+
}
|
|
9030
|
+
}
|
|
9031
|
+
|
|
9032
|
+
function countTraceTextLines(text) {
|
|
9033
|
+
const value = String(text || "");
|
|
9034
|
+
return value ? value.split(/\n/).length : 0;
|
|
9035
|
+
}
|
|
9036
|
+
|
|
9037
|
+
function formatTraceTextMetrics(text) {
|
|
9038
|
+
const value = String(text || "");
|
|
9039
|
+
const lines = countTraceTextLines(value);
|
|
9040
|
+
const chars = value.length;
|
|
9041
|
+
return formatCompactNumber(lines) + " line" + (lines === 1 ? "" : "s")
|
|
9042
|
+
+ ", " + formatCompactNumber(chars) + " char" + (chars === 1 ? "" : "s");
|
|
9043
|
+
}
|
|
9044
|
+
|
|
9045
|
+
function renderTraceToolField(label, value, className) {
|
|
9046
|
+
const text = String(value || "").trim();
|
|
9047
|
+
if (!text) return "";
|
|
9048
|
+
const extraClass = className ? " " + String(className) : "";
|
|
9049
|
+
return "<div class='trace-tool-field'>"
|
|
9050
|
+
+ "<span class='trace-tool-field-label'>" + escapeHtml(label) + "</span>"
|
|
9051
|
+
+ "<code class='trace-tool-field-value" + extraClass + "' title='" + escapeHtml(text) + "'>" + escapeHtml(text) + "</code>"
|
|
9052
|
+
+ "</div>";
|
|
9053
|
+
}
|
|
9054
|
+
|
|
9055
|
+
function renderTraceRawInputDetails(inputText, outputKey) {
|
|
9056
|
+
const value = String(inputText || "").trim();
|
|
9057
|
+
if (!value) return "";
|
|
9058
|
+
const rawKey = outputKey + ":raw-input";
|
|
9059
|
+
const openAttr = traceExpandedOutputs.has(rawKey) ? " open" : "";
|
|
9060
|
+
return "<details class='trace-tool-details trace-tool-raw-input'" + openAttr + ">"
|
|
9061
|
+
+ "<summary>Raw input</summary>"
|
|
9062
|
+
+ "<div class='trace-tool-details-body'>"
|
|
9063
|
+
+ renderTraceOutput(value, rawKey, { label: "Raw input" })
|
|
9064
|
+
+ "</div>"
|
|
9065
|
+
+ "</details>";
|
|
9066
|
+
}
|
|
9067
|
+
|
|
9068
|
+
function renderTraceToolTextDetails(summary, text, outputKey, label, options) {
|
|
9069
|
+
const value = String(text || "");
|
|
9070
|
+
const emptyText = options && typeof options.emptyText === "string" ? options.emptyText : "[empty]";
|
|
9071
|
+
const openAttr = traceExpandedOutputs.has(outputKey) ? " open" : "";
|
|
9072
|
+
return "<details class='trace-tool-details" + (options && options.className ? " " + escapeHtml(options.className) : "") + "'" + openAttr + ">"
|
|
9073
|
+
+ "<summary>" + escapeHtml(summary) + "</summary>"
|
|
9074
|
+
+ "<div class='trace-tool-details-body'>"
|
|
9075
|
+
+ renderTraceOutput(value || emptyText, outputKey, { label })
|
|
9076
|
+
+ "</div>"
|
|
9077
|
+
+ "</details>";
|
|
9078
|
+
}
|
|
9079
|
+
|
|
9080
|
+
function renderTraceEditInput(entry, payload, inputText) {
|
|
9081
|
+
const path = payload && typeof payload.path === "string" ? payload.path : "";
|
|
9082
|
+
const edits = payload && Array.isArray(payload.edits) ? payload.edits : [];
|
|
9083
|
+
const replacements = edits
|
|
9084
|
+
.map((edit, index) => {
|
|
9085
|
+
const item = edit && typeof edit === "object" ? edit : {};
|
|
9086
|
+
return {
|
|
9087
|
+
index,
|
|
9088
|
+
oldText: typeof item.oldText === "string" ? item.oldText : "",
|
|
9089
|
+
newText: typeof item.newText === "string" ? item.newText : "",
|
|
9090
|
+
};
|
|
9091
|
+
})
|
|
9092
|
+
.filter((edit) => edit.oldText || edit.newText);
|
|
9093
|
+
const replacementCount = replacements.length || edits.length;
|
|
9094
|
+
const fields = "<div class='trace-tool-fields'>"
|
|
9095
|
+
+ renderTraceToolField("Path", path, "trace-tool-path")
|
|
9096
|
+
+ renderTraceToolField("Changes", replacementCount + " replacement" + (replacementCount === 1 ? "" : "s"), "")
|
|
9097
|
+
+ "</div>";
|
|
9098
|
+
const changes = replacements.length
|
|
9099
|
+
? "<div class='trace-tool-change-list'>" + replacements.map((edit, displayIndex) => {
|
|
9100
|
+
const oldMetrics = formatTraceTextMetrics(edit.oldText);
|
|
9101
|
+
const newMetrics = formatTraceTextMetrics(edit.newText);
|
|
9102
|
+
const oldKey = entry.id + ":edit:" + edit.index + ":old";
|
|
9103
|
+
const newKey = entry.id + ":edit:" + edit.index + ":new";
|
|
9104
|
+
const openAttr = traceExpandedOutputs.has(oldKey) || traceExpandedOutputs.has(newKey) ? " open" : "";
|
|
9105
|
+
return "<details class='trace-tool-details trace-tool-change'" + openAttr + ">"
|
|
9106
|
+
+ "<summary>Replacement " + escapeHtml(String(displayIndex + 1)) + " · " + escapeHtml(oldMetrics) + " → " + escapeHtml(newMetrics) + "</summary>"
|
|
9107
|
+
+ "<div class='trace-tool-change-body'>"
|
|
9108
|
+
+ "<div class='trace-tool-change-grid'>"
|
|
9109
|
+
+ "<div class='trace-tool-change-column'><div class='trace-tool-code-label'>Old text</div>"
|
|
9110
|
+
+ renderTraceOutput(edit.oldText || "[empty]", oldKey, { label: "Old text" })
|
|
9111
|
+
+ "</div>"
|
|
9112
|
+
+ "<div class='trace-tool-change-column'><div class='trace-tool-code-label'>New text</div>"
|
|
9113
|
+
+ renderTraceOutput(edit.newText || "[empty]", newKey, { label: "New text" })
|
|
9114
|
+
+ "</div>"
|
|
9115
|
+
+ "</div>"
|
|
9116
|
+
+ "</div>"
|
|
9117
|
+
+ "</details>";
|
|
9118
|
+
}).join("") + "</div>"
|
|
9119
|
+
: "";
|
|
9120
|
+
return "<div class='trace-tool-input trace-tool-input-edit'>"
|
|
9121
|
+
+ fields
|
|
9122
|
+
+ changes
|
|
9123
|
+
+ renderTraceRawInputDetails(inputText, entry.id + ":input")
|
|
9124
|
+
+ "</div>";
|
|
9125
|
+
}
|
|
9126
|
+
|
|
9127
|
+
function renderTraceWriteInput(entry, payload, inputText) {
|
|
9128
|
+
const path = payload && typeof payload.path === "string" ? payload.path : "";
|
|
9129
|
+
const content = payload && typeof payload.content === "string" ? payload.content : null;
|
|
9130
|
+
const fields = "<div class='trace-tool-fields'>"
|
|
9131
|
+
+ renderTraceToolField("Path", path, "trace-tool-path")
|
|
9132
|
+
+ (content !== null ? renderTraceToolField("Content", formatTraceTextMetrics(content), "") : "")
|
|
9133
|
+
+ "</div>";
|
|
9134
|
+
const contentDetails = content !== null
|
|
9135
|
+
? renderTraceToolTextDetails("Content · " + formatTraceTextMetrics(content), content, entry.id + ":write:content", "Content", { className: "trace-tool-content" })
|
|
9136
|
+
: "";
|
|
9137
|
+
return "<div class='trace-tool-input trace-tool-input-write'>"
|
|
9138
|
+
+ fields
|
|
9139
|
+
+ contentDetails
|
|
9140
|
+
+ renderTraceRawInputDetails(inputText, entry.id + ":input")
|
|
9141
|
+
+ "</div>";
|
|
9142
|
+
}
|
|
9143
|
+
|
|
9144
|
+
function renderTraceReadInput(entry, payload, inputText) {
|
|
9145
|
+
const path = payload && typeof payload.path === "string" ? payload.path : "";
|
|
9146
|
+
const offset = payload && (typeof payload.offset === "number" || typeof payload.offset === "string") ? String(payload.offset) : "";
|
|
9147
|
+
const limit = payload && (typeof payload.limit === "number" || typeof payload.limit === "string") ? String(payload.limit) : "";
|
|
9148
|
+
const fields = "<div class='trace-tool-fields'>"
|
|
9149
|
+
+ renderTraceToolField("Path", path, "trace-tool-path")
|
|
9150
|
+
+ renderTraceToolField("Offset", offset ? "line " + offset : "", "")
|
|
9151
|
+
+ renderTraceToolField("Limit", limit ? limit + " lines" : "", "")
|
|
9152
|
+
+ "</div>";
|
|
9153
|
+
return "<div class='trace-tool-input trace-tool-input-read'>"
|
|
9154
|
+
+ fields
|
|
9155
|
+
+ renderTraceRawInputDetails(inputText, entry.id + ":input")
|
|
9156
|
+
+ "</div>";
|
|
9157
|
+
}
|
|
9158
|
+
|
|
9159
|
+
function renderTraceCommandInput(entry, inputText, label) {
|
|
9160
|
+
const value = String(inputText || "").trim();
|
|
9161
|
+
if (!value) return "";
|
|
9162
|
+
return "<div class='trace-tool-input trace-tool-input-command'>"
|
|
9163
|
+
+ "<div class='trace-tool-code-label'>" + escapeHtml(label || "Command") + "</div>"
|
|
9164
|
+
+ renderTraceOutput(value, entry.id + ":input", { label: label || "Command" })
|
|
9165
|
+
+ "</div>";
|
|
9166
|
+
}
|
|
9167
|
+
|
|
9168
|
+
function renderTraceToolInput(entry) {
|
|
9169
|
+
const inputText = String(entry.args || entry.argsSummary || "").trim();
|
|
9170
|
+
if (!inputText) return "";
|
|
9171
|
+
const toolName = String(entry.toolName || "").trim().toLowerCase();
|
|
9172
|
+
if (toolName === "bash") return renderTraceCommandInput(entry, inputText, "Command");
|
|
9173
|
+
if (toolName === "repl_send" || toolName === "studio_repl_send") return renderTraceCommandInput(entry, inputText, "Code");
|
|
9174
|
+
const payload = parseTraceToolArgsObject(inputText);
|
|
9175
|
+
if (payload && toolName === "edit") return renderTraceEditInput(entry, payload, inputText);
|
|
9176
|
+
if (payload && toolName === "write") return renderTraceWriteInput(entry, payload, inputText);
|
|
9177
|
+
if (payload && toolName === "read") return renderTraceReadInput(entry, payload, inputText);
|
|
9178
|
+
return renderTraceOutput(inputText, entry.id + ":input", { label: "Input" });
|
|
9179
|
+
}
|
|
9180
|
+
|
|
9021
9181
|
function renderTraceImages(images) {
|
|
9022
9182
|
const normalizedImages = Array.isArray(images)
|
|
9023
9183
|
? images.map((image, index) => normalizeTraceImage(image, index)).filter(Boolean)
|
|
@@ -9382,9 +9542,9 @@
|
|
|
9382
9542
|
}
|
|
9383
9543
|
|
|
9384
9544
|
const title = entry.label || entry.toolName || "tool";
|
|
9385
|
-
const
|
|
9386
|
-
const argsSummary =
|
|
9387
|
-
? "<div class='trace-section trace-section-input'><div class='trace-section-label'>Input</div>" +
|
|
9545
|
+
const inputHtml = renderTraceToolInput(entry);
|
|
9546
|
+
const argsSummary = inputHtml
|
|
9547
|
+
? "<div class='trace-section trace-section-input'><div class='trace-section-label'>Input</div>" + inputHtml + "</div>"
|
|
9388
9548
|
: "";
|
|
9389
9549
|
const imageOutput = renderTraceImages(entry.images);
|
|
9390
9550
|
const outputPieces = [];
|
package/client/studio.css
CHANGED
|
@@ -3549,6 +3549,118 @@
|
|
|
3549
3549
|
background: var(--panel);
|
|
3550
3550
|
}
|
|
3551
3551
|
|
|
3552
|
+
.trace-tool-input {
|
|
3553
|
+
display: flex;
|
|
3554
|
+
flex-direction: column;
|
|
3555
|
+
gap: 8px;
|
|
3556
|
+
min-width: 0;
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
.trace-tool-fields {
|
|
3560
|
+
display: grid;
|
|
3561
|
+
gap: 5px;
|
|
3562
|
+
padding: 8px 9px;
|
|
3563
|
+
border: 1px solid var(--panel-border);
|
|
3564
|
+
border-radius: 8px;
|
|
3565
|
+
background: var(--panel);
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
.trace-tool-field {
|
|
3569
|
+
display: grid;
|
|
3570
|
+
grid-template-columns: minmax(58px, auto) minmax(0, 1fr);
|
|
3571
|
+
gap: 8px;
|
|
3572
|
+
align-items: baseline;
|
|
3573
|
+
min-width: 0;
|
|
3574
|
+
font-size: 12px;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
.trace-tool-field-label,
|
|
3578
|
+
.trace-tool-code-label {
|
|
3579
|
+
color: var(--muted);
|
|
3580
|
+
font-size: 11px;
|
|
3581
|
+
font-weight: 600;
|
|
3582
|
+
letter-spacing: 0.04em;
|
|
3583
|
+
text-transform: uppercase;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
.trace-tool-field-value {
|
|
3587
|
+
min-width: 0;
|
|
3588
|
+
color: var(--text);
|
|
3589
|
+
overflow-wrap: anywhere;
|
|
3590
|
+
white-space: pre-wrap;
|
|
3591
|
+
font-family: var(--font-mono);
|
|
3592
|
+
font-size: 12px;
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
.trace-tool-path {
|
|
3596
|
+
color: var(--studio-info-text, var(--muted));
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
.trace-tool-change-list {
|
|
3600
|
+
display: flex;
|
|
3601
|
+
flex-direction: column;
|
|
3602
|
+
gap: 7px;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
.trace-tool-details {
|
|
3606
|
+
border: 1px solid var(--panel-border);
|
|
3607
|
+
border-radius: 8px;
|
|
3608
|
+
background: var(--panel);
|
|
3609
|
+
overflow: hidden;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
.trace-tool-details > summary {
|
|
3613
|
+
cursor: pointer;
|
|
3614
|
+
user-select: none;
|
|
3615
|
+
padding: 7px 9px;
|
|
3616
|
+
color: var(--studio-info-text, var(--muted));
|
|
3617
|
+
font-size: 12px;
|
|
3618
|
+
font-weight: 600;
|
|
3619
|
+
line-height: 1.35;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
.trace-tool-details[open] > summary {
|
|
3623
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
3624
|
+
background: var(--panel-2);
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
.trace-tool-details-body,
|
|
3628
|
+
.trace-tool-change-body {
|
|
3629
|
+
padding: 8px;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
.trace-tool-change-grid {
|
|
3633
|
+
display: grid;
|
|
3634
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
3635
|
+
gap: 8px;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
.trace-tool-change-column {
|
|
3639
|
+
display: flex;
|
|
3640
|
+
flex-direction: column;
|
|
3641
|
+
gap: 5px;
|
|
3642
|
+
min-width: 0;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
.trace-tool-input-command > .trace-output,
|
|
3646
|
+
.trace-tool-details-body .trace-output,
|
|
3647
|
+
.trace-tool-change-column .trace-output {
|
|
3648
|
+
border: 1px solid var(--panel-border);
|
|
3649
|
+
border-radius: 8px;
|
|
3650
|
+
background: var(--panel);
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
.trace-tool-raw-input > summary {
|
|
3654
|
+
color: var(--muted);
|
|
3655
|
+
font-weight: 500;
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
@container (max-width: 720px) {
|
|
3659
|
+
.trace-tool-change-grid {
|
|
3660
|
+
grid-template-columns: 1fr;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3552
3664
|
.trace-image-gallery {
|
|
3553
3665
|
display: grid;
|
|
3554
3666
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
|
package/index.ts
CHANGED
|
@@ -5898,6 +5898,28 @@ function decorateStudioPandocSyntaxHtml(html: string): string {
|
|
|
5898
5898
|
);
|
|
5899
5899
|
}
|
|
5900
5900
|
|
|
5901
|
+
const studioPandocHtmlResourceFlagCache = new Map<string, Promise<"--embed-resources" | "--self-contained">>();
|
|
5902
|
+
|
|
5903
|
+
async function getStudioPandocHtmlResourceFlag(pandocCommand: string): Promise<"--embed-resources" | "--self-contained"> {
|
|
5904
|
+
let cached = studioPandocHtmlResourceFlagCache.get(pandocCommand);
|
|
5905
|
+
if (!cached) {
|
|
5906
|
+
cached = runStudioSubprocess(pandocCommand, ["--help"], {
|
|
5907
|
+
timeoutMs: 5_000,
|
|
5908
|
+
stdoutMaxBytes: 250_000,
|
|
5909
|
+
stderrMaxBytes: 20_000,
|
|
5910
|
+
label: "pandoc capability probe",
|
|
5911
|
+
notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
|
|
5912
|
+
}).then((result) => {
|
|
5913
|
+
if (result.code !== 0) {
|
|
5914
|
+
throw new Error(`pandoc capability probe failed with exit code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`);
|
|
5915
|
+
}
|
|
5916
|
+
return result.stdout.includes("--embed-resources") ? "--embed-resources" : "--self-contained";
|
|
5917
|
+
});
|
|
5918
|
+
studioPandocHtmlResourceFlagCache.set(pandocCommand, cached);
|
|
5919
|
+
}
|
|
5920
|
+
return cached;
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5901
5923
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
5902
5924
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
5903
5925
|
const markdownWithNormalizedFences = isLatex ? markdown : normalizeStudioMarkdownSmartFences(markdown);
|
|
@@ -5925,7 +5947,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
5925
5947
|
await mkdir(htmlTemplateDir, { recursive: true });
|
|
5926
5948
|
const htmlTemplatePath = join(htmlTemplateDir, "template.html");
|
|
5927
5949
|
await writeFile(htmlTemplatePath, STUDIO_PANDOC_HTML_FRAGMENT_TEMPLATE, "utf-8");
|
|
5928
|
-
args.push(
|
|
5950
|
+
args.push(await getStudioPandocHtmlResourceFlag(pandocCommand), "--standalone", `--template=${htmlTemplatePath}`);
|
|
5929
5951
|
}
|
|
5930
5952
|
const normalizedMarkdown = isLatex
|
|
5931
5953
|
? sourceWithResolvedRefs
|
|
@@ -5955,7 +5977,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
5955
5977
|
}
|
|
5956
5978
|
|
|
5957
5979
|
let renderedHtml = pandocResult.stdout;
|
|
5958
|
-
// When --standalone
|
|
5980
|
+
// When --standalone is used for embedded resources, extract only the <body> content.
|
|
5959
5981
|
if (resourcePath) {
|
|
5960
5982
|
const bodyMatch = renderedHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
5961
5983
|
if (!bodyMatch) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.26",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|