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 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
@@ -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 inputText = entry.args || entry.argsSummary || "";
9386
- const argsSummary = inputText
9387
- ? "<div class='trace-section trace-section-input'><div class='trace-section-label'>Input</div>" + renderTraceOutput(inputText, entry.id + ":input", { 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("--embed-resources", "--standalone", `--template=${htmlTemplatePath}`);
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 was used for --embed-resources, extract only the <body> content.
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.24",
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",