pi-studio 0.5.55 → 0.5.56
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 +8 -0
- package/README.md +1 -0
- package/client/studio-client.js +2 -37
- package/index.ts +22 -122
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.56] — 2026-04-15
|
|
8
|
+
|
|
9
|
+
### Removed
|
|
10
|
+
- Removed Studio's standalone npm update checker and footer update badge now that package update tracking is handled in pi.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Studio now always prints its localhost URL even when automatic browser open fails, and SSH sessions get a localhost-only port-forwarding hint instead of needing non-local binding.
|
|
14
|
+
|
|
7
15
|
## [0.5.54] — 2026-04-13
|
|
8
16
|
|
|
9
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
74
74
|
## Notes
|
|
75
75
|
|
|
76
76
|
- Local-only server (`127.0.0.1`) with tokenized Studio URLs.
|
|
77
|
+
- For remote SSH sessions, keep Studio bound to localhost and use SSH local port forwarding; `/studio` prints the localhost URL and an SSH tunnel hint when SSH is detected.
|
|
77
78
|
- 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.
|
|
78
79
|
- Studio is designed as a complement to terminal pi, not a replacement.
|
|
79
80
|
- Editor/code font uses a best-effort terminal-monospace match when the current terminal config exposes it; set `PI_STUDIO_FONT_MONO` to force a specific CSS `font-family` stack.
|
package/client/studio-client.js
CHANGED
|
@@ -202,8 +202,6 @@
|
|
|
202
202
|
let contextTokens = null;
|
|
203
203
|
let contextWindow = null;
|
|
204
204
|
let contextPercent = null;
|
|
205
|
-
let updateInstalledVersion = null;
|
|
206
|
-
let updateLatestVersion = null;
|
|
207
205
|
let windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
208
206
|
let titleAttentionMessage = "";
|
|
209
207
|
let titleAttentionRequestId = null;
|
|
@@ -664,8 +662,6 @@
|
|
|
664
662
|
if (typeof message.contextTokens === "number") summary.contextTokens = message.contextTokens;
|
|
665
663
|
if (typeof message.contextWindow === "number") summary.contextWindow = message.contextWindow;
|
|
666
664
|
if (typeof message.contextPercent === "number") summary.contextPercent = message.contextPercent;
|
|
667
|
-
if (typeof message.updateInstalledVersion === "string") summary.updateInstalledVersion = message.updateInstalledVersion;
|
|
668
|
-
if (typeof message.updateLatestVersion === "string") summary.updateLatestVersion = message.updateLatestVersion;
|
|
669
665
|
if (message.document && typeof message.document === "object" && typeof message.document.text === "string") {
|
|
670
666
|
summary.documentLength = message.document.text.length;
|
|
671
667
|
if (typeof message.document.label === "string") summary.documentLabel = message.document.label;
|
|
@@ -926,30 +922,6 @@
|
|
|
926
922
|
return changed;
|
|
927
923
|
}
|
|
928
924
|
|
|
929
|
-
function applyUpdateInfoFromMessage(message) {
|
|
930
|
-
if (!message || typeof message !== "object") return false;
|
|
931
|
-
|
|
932
|
-
let changed = false;
|
|
933
|
-
|
|
934
|
-
if (Object.prototype.hasOwnProperty.call(message, "updateInstalledVersion")) {
|
|
935
|
-
const nextInstalled = parseNonEmptyString(message.updateInstalledVersion);
|
|
936
|
-
if (nextInstalled !== updateInstalledVersion) {
|
|
937
|
-
updateInstalledVersion = nextInstalled;
|
|
938
|
-
changed = true;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (Object.prototype.hasOwnProperty.call(message, "updateLatestVersion")) {
|
|
943
|
-
const nextLatest = parseNonEmptyString(message.updateLatestVersion);
|
|
944
|
-
if (nextLatest !== updateLatestVersion) {
|
|
945
|
-
updateLatestVersion = nextLatest;
|
|
946
|
-
changed = true;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
return changed;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
925
|
function isTitleAttentionRequestKind(kind) {
|
|
954
926
|
return kind === "annotation" || kind === "critique" || kind === "direct";
|
|
955
927
|
}
|
|
@@ -1134,13 +1106,7 @@
|
|
|
1134
1106
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
1135
1107
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
1136
1108
|
const contextText = formatContextUsageText();
|
|
1137
|
-
|
|
1138
|
-
if (updateLatestVersion) {
|
|
1139
|
-
updateText = updateInstalledVersion
|
|
1140
|
-
? "Update: " + updateInstalledVersion + " → " + updateLatestVersion
|
|
1141
|
-
: "Update: " + updateLatestVersion + " available";
|
|
1142
|
-
}
|
|
1143
|
-
const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText + (updateText ? " · " + updateText : "");
|
|
1109
|
+
const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
|
|
1144
1110
|
if (footerMetaTextEl) {
|
|
1145
1111
|
footerMetaTextEl.textContent = text;
|
|
1146
1112
|
footerMetaTextEl.title = text;
|
|
@@ -9000,8 +8966,7 @@
|
|
|
9000
8966
|
debugTrace("server_message", summarizeServerMessage(message));
|
|
9001
8967
|
|
|
9002
8968
|
const contextChanged = applyContextUsageFromMessage(message);
|
|
9003
|
-
|
|
9004
|
-
if (contextChanged || updateInfoChanged) {
|
|
8969
|
+
if (contextChanged) {
|
|
9005
8970
|
updateFooterMeta();
|
|
9006
8971
|
}
|
|
9007
8972
|
|
package/index.ts
CHANGED
|
@@ -272,7 +272,6 @@ const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
|
272
272
|
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
273
273
|
const REQUEST_BODY_MAX_BYTES = 1_000_000;
|
|
274
274
|
const RESPONSE_HISTORY_LIMIT = 30;
|
|
275
|
-
const UPDATE_CHECK_TIMEOUT_MS = 1800;
|
|
276
275
|
const CMUX_NOTIFY_TIMEOUT_MS = 1200;
|
|
277
276
|
const PREPARED_PDF_EXPORT_TTL_MS = 5 * 60 * 1000;
|
|
278
277
|
const MAX_PREPARED_PDF_EXPORTS = 8;
|
|
@@ -2737,93 +2736,6 @@ function readStudioGitDiff(baseDir: string):
|
|
|
2737
2736
|
return { ok: true, text: fullDiff, label };
|
|
2738
2737
|
}
|
|
2739
2738
|
|
|
2740
|
-
function readLocalPackageMetadata(): { name: string; version: string } | null {
|
|
2741
|
-
try {
|
|
2742
|
-
const raw = readFileSync(new URL("./package.json", import.meta.url), "utf-8");
|
|
2743
|
-
const parsed = JSON.parse(raw) as { name?: unknown; version?: unknown };
|
|
2744
|
-
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
2745
|
-
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
2746
|
-
if (!name || !version) return null;
|
|
2747
|
-
return { name, version };
|
|
2748
|
-
} catch {
|
|
2749
|
-
return null;
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
interface ParsedSemver {
|
|
2754
|
-
major: number;
|
|
2755
|
-
minor: number;
|
|
2756
|
-
patch: number;
|
|
2757
|
-
prerelease: string | null;
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
function parseSemverLoose(version: string): ParsedSemver | null {
|
|
2761
|
-
const normalized = String(version || "").trim().replace(/^v/i, "");
|
|
2762
|
-
const match = normalized.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/);
|
|
2763
|
-
if (!match) return null;
|
|
2764
|
-
const major = Number.parseInt(match[1] ?? "", 10);
|
|
2765
|
-
const minor = Number.parseInt(match[2] ?? "0", 10);
|
|
2766
|
-
const patch = Number.parseInt(match[3] ?? "0", 10);
|
|
2767
|
-
if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) return null;
|
|
2768
|
-
const prerelease = typeof match[4] === "string" && match[4].trim() ? match[4].trim() : null;
|
|
2769
|
-
return { major, minor, patch, prerelease };
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
function compareSemverLoose(a: string, b: string): number {
|
|
2773
|
-
const pa = parseSemverLoose(a);
|
|
2774
|
-
const pb = parseSemverLoose(b);
|
|
2775
|
-
if (!pa || !pb) {
|
|
2776
|
-
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
|
|
2777
|
-
}
|
|
2778
|
-
if (pa.major !== pb.major) return pa.major - pb.major;
|
|
2779
|
-
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
|
|
2780
|
-
if (pa.patch !== pb.patch) return pa.patch - pb.patch;
|
|
2781
|
-
if (pa.prerelease && !pb.prerelease) return -1;
|
|
2782
|
-
if (!pa.prerelease && pb.prerelease) return 1;
|
|
2783
|
-
if (!pa.prerelease && !pb.prerelease) return 0;
|
|
2784
|
-
return (pa.prerelease ?? "").localeCompare(pb.prerelease ?? "", undefined, {
|
|
2785
|
-
numeric: true,
|
|
2786
|
-
sensitivity: "base",
|
|
2787
|
-
});
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
function isVersionBehind(installedVersion: string, latestVersion: string): boolean {
|
|
2791
|
-
return compareSemverLoose(installedVersion, latestVersion) < 0;
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
async function fetchLatestNpmVersion(packageName: string, timeoutMs = UPDATE_CHECK_TIMEOUT_MS): Promise<string | null> {
|
|
2795
|
-
const pkg = String(packageName || "").trim();
|
|
2796
|
-
if (!pkg) return null;
|
|
2797
|
-
const encodedPackage = encodeURIComponent(pkg).replace(/^%40/, "@");
|
|
2798
|
-
const endpoint = `https://registry.npmjs.org/${encodedPackage}/latest`;
|
|
2799
|
-
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
2800
|
-
const timer = controller
|
|
2801
|
-
? setTimeout(() => {
|
|
2802
|
-
try {
|
|
2803
|
-
controller.abort();
|
|
2804
|
-
} catch {
|
|
2805
|
-
// ignore abort race
|
|
2806
|
-
}
|
|
2807
|
-
}, timeoutMs)
|
|
2808
|
-
: null;
|
|
2809
|
-
|
|
2810
|
-
try {
|
|
2811
|
-
const response = await fetch(endpoint, {
|
|
2812
|
-
method: "GET",
|
|
2813
|
-
headers: { Accept: "application/json" },
|
|
2814
|
-
signal: controller?.signal,
|
|
2815
|
-
});
|
|
2816
|
-
if (!response.ok) return null;
|
|
2817
|
-
const payload = await response.json() as { version?: unknown };
|
|
2818
|
-
const version = typeof payload.version === "string" ? payload.version.trim() : "";
|
|
2819
|
-
return version || null;
|
|
2820
|
-
} catch {
|
|
2821
|
-
return null;
|
|
2822
|
-
} finally {
|
|
2823
|
-
if (timer) clearTimeout(timer);
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
|
|
2827
2739
|
function isLikelyMathExpression(expr: string): boolean {
|
|
2828
2740
|
const content = expr.trim();
|
|
2829
2741
|
if (content.length === 0) return false;
|
|
@@ -5842,6 +5754,17 @@ function buildStudioUrl(
|
|
|
5842
5754
|
return `http://127.0.0.1:${port}/?${params.toString()}`;
|
|
5843
5755
|
}
|
|
5844
5756
|
|
|
5757
|
+
function isSshSession(): boolean {
|
|
5758
|
+
return Boolean(
|
|
5759
|
+
String(process.env.SSH_CONNECTION ?? process.env.SSH_CLIENT ?? process.env.SSH_TTY ?? "").trim(),
|
|
5760
|
+
);
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
function buildStudioSshTunnelHint(port: number): string | null {
|
|
5764
|
+
if (!isSshSession()) return null;
|
|
5765
|
+
return `SSH detected. If Studio is running on a remote machine, forward the port from your local machine, then open the Studio URL locally: ssh -L ${port}:127.0.0.1:${port} <remote-host>`;
|
|
5766
|
+
}
|
|
5767
|
+
|
|
5845
5768
|
function resolveRequestedStudioDocumentFromUrl(
|
|
5846
5769
|
requestUrl: URL,
|
|
5847
5770
|
fallback: InitialStudioDocument | null,
|
|
@@ -6382,11 +6305,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
6382
6305
|
};
|
|
6383
6306
|
let compactInProgress = false;
|
|
6384
6307
|
let compactRequestId: string | null = null;
|
|
6385
|
-
let updateCheckStarted = false;
|
|
6386
|
-
let updateCheckCompleted = false;
|
|
6387
|
-
const packageMetadata = readLocalPackageMetadata();
|
|
6388
|
-
const installedPackageVersion = packageMetadata?.version ?? null;
|
|
6389
|
-
let updateAvailableLatestVersion: string | null = null;
|
|
6390
6308
|
|
|
6391
6309
|
const isStudioDirectRunChainActive = () => Boolean(studioDirectRunChain);
|
|
6392
6310
|
const getQueuedStudioSteeringCount = () => queuedStudioDirectRequests.length;
|
|
@@ -6766,28 +6684,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
6766
6684
|
});
|
|
6767
6685
|
};
|
|
6768
6686
|
|
|
6769
|
-
const maybeNotifyUpdateAvailable = async (ctx: ExtensionCommandContext) => {
|
|
6770
|
-
if (updateCheckStarted || updateCheckCompleted) return;
|
|
6771
|
-
updateCheckStarted = true;
|
|
6772
|
-
try {
|
|
6773
|
-
const metadata = packageMetadata;
|
|
6774
|
-
if (!metadata) return;
|
|
6775
|
-
const latest = await fetchLatestNpmVersion(metadata.name, UPDATE_CHECK_TIMEOUT_MS);
|
|
6776
|
-
if (!latest) return;
|
|
6777
|
-
if (!isVersionBehind(metadata.version, latest)) return;
|
|
6778
|
-
|
|
6779
|
-
updateAvailableLatestVersion = latest;
|
|
6780
|
-
broadcastState();
|
|
6781
|
-
|
|
6782
|
-
const notification =
|
|
6783
|
-
`Update available for ${metadata.name}: ${metadata.version} → ${latest}. Run: pi install npm:${metadata.name}`;
|
|
6784
|
-
ctx.ui.notify(notification, "info");
|
|
6785
|
-
broadcast({ type: "info", message: notification, level: "info" });
|
|
6786
|
-
} finally {
|
|
6787
|
-
updateCheckCompleted = true;
|
|
6788
|
-
}
|
|
6789
|
-
};
|
|
6790
|
-
|
|
6791
6687
|
const sendToClient = (client: WebSocket, payload: unknown) => {
|
|
6792
6688
|
if (client.readyState !== WebSocket.OPEN) return;
|
|
6793
6689
|
try {
|
|
@@ -7067,8 +6963,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
7067
6963
|
contextTokens: contextUsageSnapshot.tokens,
|
|
7068
6964
|
contextWindow: contextUsageSnapshot.contextWindow,
|
|
7069
6965
|
contextPercent: contextUsageSnapshot.percent,
|
|
7070
|
-
updateInstalledVersion: installedPackageVersion,
|
|
7071
|
-
updateLatestVersion: updateAvailableLatestVersion,
|
|
7072
6966
|
compactInProgress,
|
|
7073
6967
|
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
7074
6968
|
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
@@ -7349,8 +7243,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
7349
7243
|
contextTokens: contextUsageSnapshot.tokens,
|
|
7350
7244
|
contextWindow: contextUsageSnapshot.contextWindow,
|
|
7351
7245
|
contextPercent: contextUsageSnapshot.percent,
|
|
7352
|
-
updateInstalledVersion: installedPackageVersion,
|
|
7353
|
-
updateLatestVersion: updateAvailableLatestVersion,
|
|
7354
7246
|
compactInProgress,
|
|
7355
7247
|
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
7356
7248
|
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
@@ -9018,6 +8910,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
9018
8910
|
ctx.ui.notify("A full pi Studio view is already open for this session. Close it first, use /studio-replace for a fresh full Studio view, or use /studio-editor-only for a concurrent editor-only Studio view.", "warning");
|
|
9019
8911
|
if (serverState) {
|
|
9020
8912
|
ctx.ui.notify(`Studio URL: ${buildStudioUrl(serverState.port, serverState.token, "full")}`, "info");
|
|
8913
|
+
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port);
|
|
8914
|
+
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
9021
8915
|
}
|
|
9022
8916
|
return;
|
|
9023
8917
|
}
|
|
@@ -9043,6 +8937,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9043
8937
|
|
|
9044
8938
|
const state = await ensureServer();
|
|
9045
8939
|
const url = buildStudioUrl(state.port, state.token, mode, selected);
|
|
8940
|
+
const sshTunnelHint = buildStudioSshTunnelHint(state.port);
|
|
9046
8941
|
const openedLabel = mode === "editor-only" ? "pi Studio editor-only view" : "pi Studio";
|
|
9047
8942
|
|
|
9048
8943
|
try {
|
|
@@ -9054,11 +8949,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
9054
8949
|
} else {
|
|
9055
8950
|
ctx.ui.notify(`Opened ${openedLabel} with blank editor.`, "info");
|
|
9056
8951
|
}
|
|
9057
|
-
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
9058
8952
|
} catch (error) {
|
|
9059
|
-
|
|
8953
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8954
|
+
if (isSshSession()) {
|
|
8955
|
+
ctx.ui.notify(`Failed to open browser automatically over SSH: ${message}`, "warning");
|
|
8956
|
+
} else {
|
|
8957
|
+
ctx.ui.notify(`Failed to open browser: ${message}`, "error");
|
|
8958
|
+
}
|
|
9060
8959
|
} finally {
|
|
9061
|
-
|
|
8960
|
+
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
8961
|
+
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
9062
8962
|
}
|
|
9063
8963
|
};
|
|
9064
8964
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.56",
|
|
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",
|