pi-studio 0.6.3 → 0.6.5
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 +11 -0
- package/README.md +1 -1
- package/client/studio-client.js +85 -4
- package/client/studio.css +37 -3
- package/index.ts +19 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.5] — 2026-04-30
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Working view output blocks now show a compact 50-line preview by default with per-block **Show full** / **Collapse** controls for longer thinking, responses, and tool input/output.
|
|
11
|
+
|
|
12
|
+
## [0.6.4] — 2026-04-29
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Fixed `@`-selected quoted file paths such as `@"folder/file.md"` so Studio commands strip the `@` prefix and surrounding quotes before resolving the file.
|
|
16
|
+
- `/studio --status` now prints the full tokenized Studio URL, and SSH tunnel hints explicitly say to preserve the `?token=...` parameter.
|
|
17
|
+
|
|
7
18
|
## [0.6.3] — 2026-04-29
|
|
8
19
|
|
|
9
20
|
### Changed
|
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
75
75
|
## Notes
|
|
76
76
|
|
|
77
77
|
- Local-only server (`127.0.0.1`) with tokenized Studio URLs.
|
|
78
|
-
- For remote SSH sessions, keep Studio bound to localhost and use SSH local port forwarding; `/studio`
|
|
78
|
+
- For remote SSH sessions, keep Studio bound to localhost and use SSH local port forwarding; `/studio` and `/studio --status` print the full tokenized localhost URL. Open that URL through the tunnel, preserving the `?token=...` parameter.
|
|
79
79
|
- Full Studio is a singleton per Pi session: use `/studio` to open it, `/studio-replace` to explicitly replace it, and `/studio-editor-only` for extra editing/preview tabs that do not take over the full Studio session view.
|
|
80
80
|
- Studio is designed as a complement to terminal pi, not a replacement.
|
|
81
81
|
- Installing pi-studio makes the optional `pi-studio-dark` and `pi-studio-light` themes available in pi's theme selector; it does not change your active theme.
|
package/client/studio-client.js
CHANGED
|
@@ -194,6 +194,9 @@
|
|
|
194
194
|
let traceFilter = "all";
|
|
195
195
|
let traceAutoScroll = true;
|
|
196
196
|
let traceRenderRaf = null;
|
|
197
|
+
const traceExpandedOutputs = new Set();
|
|
198
|
+
const TRACE_OUTPUT_PREVIEW_MAX_LINES = 50;
|
|
199
|
+
const TRACE_OUTPUT_PREVIEW_MAX_CHARS = 8000;
|
|
197
200
|
let studioRunChainActive = false;
|
|
198
201
|
let queuedSteeringCount = 0;
|
|
199
202
|
let agentBusyFromServer = false;
|
|
@@ -322,7 +325,11 @@
|
|
|
322
325
|
}
|
|
323
326
|
|
|
324
327
|
function replaceTraceState(nextState) {
|
|
328
|
+
const previousRunId = traceState && traceState.runId ? traceState.runId : null;
|
|
325
329
|
traceState = normalizeTraceState(nextState);
|
|
330
|
+
if ((traceState.runId || null) !== previousRunId) {
|
|
331
|
+
traceExpandedOutputs.clear();
|
|
332
|
+
}
|
|
326
333
|
renderTraceViewIfActive();
|
|
327
334
|
}
|
|
328
335
|
|
|
@@ -2971,6 +2978,21 @@
|
|
|
2971
2978
|
setTraceFilter(nextFilter);
|
|
2972
2979
|
return;
|
|
2973
2980
|
}
|
|
2981
|
+
const outputToggleBtn = target instanceof Element ? target.closest("[data-trace-output-key]") : null;
|
|
2982
|
+
if (outputToggleBtn) {
|
|
2983
|
+
event.preventDefault();
|
|
2984
|
+
const key = outputToggleBtn.getAttribute("data-trace-output-key") || "";
|
|
2985
|
+
if (key) {
|
|
2986
|
+
if (traceExpandedOutputs.has(key)) {
|
|
2987
|
+
traceExpandedOutputs.delete(key);
|
|
2988
|
+
} else {
|
|
2989
|
+
traceExpandedOutputs.add(key);
|
|
2990
|
+
}
|
|
2991
|
+
traceAutoScroll = false;
|
|
2992
|
+
renderTraceViewIfActive();
|
|
2993
|
+
}
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2974
2996
|
const actionBtn = target instanceof Element ? target.closest("[data-trace-action]") : null;
|
|
2975
2997
|
if (!actionBtn) return;
|
|
2976
2998
|
event.preventDefault();
|
|
@@ -3607,6 +3629,65 @@
|
|
|
3607
3629
|
return remaining < 56;
|
|
3608
3630
|
}
|
|
3609
3631
|
|
|
3632
|
+
function formatTraceOutputSize(text) {
|
|
3633
|
+
const value = String(text || "");
|
|
3634
|
+
const chars = value.length;
|
|
3635
|
+
const lines = value ? value.split(/\n/).length : 0;
|
|
3636
|
+
const compactChars = chars >= 1000 ? ((chars / 1000).toFixed(chars >= 10_000 ? 0 : 1) + "k") : String(chars);
|
|
3637
|
+
return lines + " line" + (lines === 1 ? "" : "s") + ", " + compactChars + " chars";
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
function getTraceOutputPreview(text) {
|
|
3641
|
+
const value = String(text || "");
|
|
3642
|
+
const lines = value.split(/\n/);
|
|
3643
|
+
let preview = value;
|
|
3644
|
+
let truncated = false;
|
|
3645
|
+
if (lines.length > TRACE_OUTPUT_PREVIEW_MAX_LINES) {
|
|
3646
|
+
preview = lines.slice(0, TRACE_OUTPUT_PREVIEW_MAX_LINES).join("\n");
|
|
3647
|
+
truncated = true;
|
|
3648
|
+
}
|
|
3649
|
+
if (preview.length > TRACE_OUTPUT_PREVIEW_MAX_CHARS) {
|
|
3650
|
+
preview = preview.slice(0, TRACE_OUTPUT_PREVIEW_MAX_CHARS);
|
|
3651
|
+
truncated = true;
|
|
3652
|
+
}
|
|
3653
|
+
if (!truncated && value.length <= TRACE_OUTPUT_PREVIEW_MAX_CHARS) {
|
|
3654
|
+
return { text: value, truncated: false, hiddenChars: 0, hiddenLines: 0 };
|
|
3655
|
+
}
|
|
3656
|
+
if (!truncated && value.length > TRACE_OUTPUT_PREVIEW_MAX_CHARS) {
|
|
3657
|
+
preview = value.slice(0, TRACE_OUTPUT_PREVIEW_MAX_CHARS);
|
|
3658
|
+
truncated = true;
|
|
3659
|
+
}
|
|
3660
|
+
const hiddenChars = Math.max(0, value.length - preview.length);
|
|
3661
|
+
const previewLineCount = preview ? preview.split(/\n/).length : 0;
|
|
3662
|
+
const hiddenLines = Math.max(0, lines.length - previewLineCount);
|
|
3663
|
+
return { text: preview, truncated: true, hiddenChars, hiddenLines };
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
function renderTraceOutput(text, outputKey) {
|
|
3667
|
+
const value = String(text || "");
|
|
3668
|
+
const key = String(outputKey || "trace-output");
|
|
3669
|
+
const isExpanded = traceExpandedOutputs.has(key);
|
|
3670
|
+
const preview = getTraceOutputPreview(value);
|
|
3671
|
+
const visibleText = isExpanded || !preview.truncated ? value : preview.text;
|
|
3672
|
+
const body = "<pre class='plain-markdown trace-output'>" + escapeHtml(visibleText) + "</pre>";
|
|
3673
|
+
if (!preview.truncated) return body;
|
|
3674
|
+
|
|
3675
|
+
const hiddenParts = [];
|
|
3676
|
+
if (preview.hiddenLines > 0) hiddenParts.push(preview.hiddenLines + " more line" + (preview.hiddenLines === 1 ? "" : "s"));
|
|
3677
|
+
if (preview.hiddenChars > 0) hiddenParts.push(formatCompactNumber(preview.hiddenChars) + " chars hidden");
|
|
3678
|
+
const summary = isExpanded
|
|
3679
|
+
? "Showing full output (" + formatTraceOutputSize(value) + ")."
|
|
3680
|
+
: "Output truncated — " + (hiddenParts.join(", ") || "more hidden") + ".";
|
|
3681
|
+
const buttonLabel = isExpanded ? "Collapse" : "Show full";
|
|
3682
|
+
return "<div class='trace-output-wrap" + (isExpanded ? " is-expanded" : " is-truncated") + "'>"
|
|
3683
|
+
+ body
|
|
3684
|
+
+ "<div class='trace-output-truncation'>"
|
|
3685
|
+
+ "<span>" + escapeHtml(summary) + "</span>"
|
|
3686
|
+
+ "<button type='button' class='trace-output-toggle' data-trace-output-key='" + escapeHtml(key) + "' aria-expanded='" + (isExpanded ? "true" : "false") + "'>" + escapeHtml(buttonLabel) + "</button>"
|
|
3687
|
+
+ "</div>"
|
|
3688
|
+
+ "</div>";
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3610
3691
|
function buildTracePanelHtml() {
|
|
3611
3692
|
const state = traceState || createEmptyTraceState();
|
|
3612
3693
|
const filter = normalizeTraceFilter(traceFilter);
|
|
@@ -3656,7 +3737,7 @@
|
|
|
3656
3737
|
sections.push(
|
|
3657
3738
|
"<div class='trace-section'>"
|
|
3658
3739
|
+ "<div class='trace-section-label'>Thinking</div>"
|
|
3659
|
-
+
|
|
3740
|
+
+ renderTraceOutput(entry.thinking, entry.id + ":thinking")
|
|
3660
3741
|
+ "</div>"
|
|
3661
3742
|
);
|
|
3662
3743
|
}
|
|
@@ -3664,7 +3745,7 @@
|
|
|
3664
3745
|
sections.push(
|
|
3665
3746
|
"<div class='trace-section'>"
|
|
3666
3747
|
+ "<div class='trace-section-label'>Response</div>"
|
|
3667
|
-
+
|
|
3748
|
+
+ renderTraceOutput(entry.text, entry.id + ":response")
|
|
3668
3749
|
+ "</div>"
|
|
3669
3750
|
);
|
|
3670
3751
|
}
|
|
@@ -3684,10 +3765,10 @@
|
|
|
3684
3765
|
|
|
3685
3766
|
const title = entry.label || entry.toolName || "tool";
|
|
3686
3767
|
const argsSummary = entry.argsSummary
|
|
3687
|
-
? "<div class='trace-section'><div class='trace-section-label'>Input</div
|
|
3768
|
+
? "<div class='trace-section'><div class='trace-section-label'>Input</div>" + renderTraceOutput(entry.argsSummary, entry.id + ":input") + "</div>"
|
|
3688
3769
|
: "";
|
|
3689
3770
|
const output = entry.output
|
|
3690
|
-
? "<div class='trace-section'><div class='trace-section-label'>Output</div
|
|
3771
|
+
? "<div class='trace-section'><div class='trace-section-label'>Output</div>" + renderTraceOutput(entry.output, entry.id + ":output") + "</div>"
|
|
3691
3772
|
: "<div class='trace-empty-inline'>No output yet.</div>";
|
|
3692
3773
|
const toolStatusLabel = entry.isError
|
|
3693
3774
|
? "Error"
|
package/client/studio.css
CHANGED
|
@@ -1822,17 +1822,51 @@
|
|
|
1822
1822
|
letter-spacing: 0.04em;
|
|
1823
1823
|
}
|
|
1824
1824
|
|
|
1825
|
-
.trace-output {
|
|
1826
|
-
padding: 10px 11px;
|
|
1827
|
-
border-radius: 8px;
|
|
1825
|
+
.trace-output-wrap {
|
|
1828
1826
|
border: 1px solid var(--panel-border);
|
|
1827
|
+
border-radius: 8px;
|
|
1829
1828
|
background: var(--panel);
|
|
1829
|
+
overflow: hidden;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
.trace-output {
|
|
1833
|
+
margin: 0;
|
|
1834
|
+
padding: 10px 11px;
|
|
1835
|
+
border: 0;
|
|
1836
|
+
border-radius: 0;
|
|
1837
|
+
background: transparent;
|
|
1830
1838
|
overflow-x: auto;
|
|
1831
1839
|
white-space: pre-wrap;
|
|
1832
1840
|
overflow-wrap: anywhere;
|
|
1833
1841
|
font-size: var(--studio-working-font-size);
|
|
1834
1842
|
}
|
|
1835
1843
|
|
|
1844
|
+
.trace-section > .trace-output {
|
|
1845
|
+
border: 1px solid var(--panel-border);
|
|
1846
|
+
border-radius: 8px;
|
|
1847
|
+
background: var(--panel);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
.trace-output-truncation {
|
|
1851
|
+
display: flex;
|
|
1852
|
+
align-items: center;
|
|
1853
|
+
justify-content: space-between;
|
|
1854
|
+
gap: 8px;
|
|
1855
|
+
padding: 6px 8px;
|
|
1856
|
+
border-top: 1px solid var(--border-subtle);
|
|
1857
|
+
color: var(--studio-info-text, var(--muted));
|
|
1858
|
+
font-size: 11px;
|
|
1859
|
+
background: var(--panel-2);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
.trace-output-toggle {
|
|
1863
|
+
flex: 0 0 auto;
|
|
1864
|
+
padding: 3px 7px;
|
|
1865
|
+
border-radius: 999px;
|
|
1866
|
+
font-size: 11px;
|
|
1867
|
+
line-height: 1.2;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1836
1870
|
.trace-empty,
|
|
1837
1871
|
.trace-empty-inline {
|
|
1838
1872
|
color: var(--muted);
|
package/index.ts
CHANGED
|
@@ -1382,20 +1382,27 @@ function isValidRequestId(id: string): boolean {
|
|
|
1382
1382
|
return /^[a-zA-Z0-9_-]{1,120}$/.test(id);
|
|
1383
1383
|
}
|
|
1384
1384
|
|
|
1385
|
-
function
|
|
1386
|
-
const trimmed =
|
|
1387
|
-
if (!trimmed) return null;
|
|
1388
|
-
|
|
1385
|
+
function stripMatchingPathQuotes(value: string): string {
|
|
1386
|
+
const trimmed = value.trim();
|
|
1389
1387
|
if (
|
|
1390
1388
|
(trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) ||
|
|
1391
1389
|
(trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2)
|
|
1392
1390
|
) {
|
|
1393
1391
|
return trimmed.slice(1, -1).trim();
|
|
1394
1392
|
}
|
|
1395
|
-
|
|
1396
1393
|
return trimmed;
|
|
1397
1394
|
}
|
|
1398
1395
|
|
|
1396
|
+
function parsePathArgument(args: string): string | null {
|
|
1397
|
+
const trimmed = args.trim();
|
|
1398
|
+
if (!trimmed) return null;
|
|
1399
|
+
|
|
1400
|
+
const hasAtPrefix = trimmed.startsWith("@");
|
|
1401
|
+
const pathPart = hasAtPrefix ? trimmed.slice(1).trim() : trimmed;
|
|
1402
|
+
const unquoted = stripMatchingPathQuotes(pathPart);
|
|
1403
|
+
return hasAtPrefix ? `@${unquoted}` : unquoted;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1399
1406
|
function tokenizeStudioCommandArgs(input: string): { tokens: string[]; error?: string } {
|
|
1400
1407
|
const tokens: string[] = [];
|
|
1401
1408
|
let current = "";
|
|
@@ -1445,8 +1452,8 @@ function tokenizeStudioCommandArgs(input: string): { tokens: string[]; error?: s
|
|
|
1445
1452
|
|
|
1446
1453
|
function normalizePathInput(pathInput: string): string {
|
|
1447
1454
|
const trimmed = pathInput.trim();
|
|
1448
|
-
if (trimmed.startsWith("@")) return trimmed.slice(1).trim();
|
|
1449
|
-
return trimmed;
|
|
1455
|
+
if (trimmed.startsWith("@")) return stripMatchingPathQuotes(trimmed.slice(1).trim());
|
|
1456
|
+
return stripMatchingPathQuotes(trimmed);
|
|
1450
1457
|
}
|
|
1451
1458
|
|
|
1452
1459
|
function expandHome(pathInput: string): string {
|
|
@@ -6083,7 +6090,7 @@ function isSshSession(): boolean {
|
|
|
6083
6090
|
|
|
6084
6091
|
function buildStudioSshTunnelHint(port: number): string | null {
|
|
6085
6092
|
if (!isSshSession()) return null;
|
|
6086
|
-
return `SSH detected.
|
|
6093
|
+
return `SSH detected. Forward the remote Studio port, then open the full Studio URL above locally, including its ?token=... parameter: ssh -L ${port}:127.0.0.1:${port} <remote-host>. If you choose a different local port, change only the port in the URL; keep the token.`;
|
|
6087
6094
|
}
|
|
6088
6095
|
|
|
6089
6096
|
function resolveRequestedStudioDocumentFromUrl(
|
|
@@ -9481,10 +9488,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
9481
9488
|
return;
|
|
9482
9489
|
}
|
|
9483
9490
|
const counts = getStudioClientCounts();
|
|
9491
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "full");
|
|
9484
9492
|
ctx.ui.notify(
|
|
9485
|
-
`Studio running at
|
|
9493
|
+
`Studio running at ${url} (busy: ${isStudioBusy() ? "yes" : "no"}; full views: ${counts.full}; editor-only views: ${counts.editorOnly})`,
|
|
9486
9494
|
"info",
|
|
9487
9495
|
);
|
|
9496
|
+
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port);
|
|
9497
|
+
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
9488
9498
|
return;
|
|
9489
9499
|
}
|
|
9490
9500
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|