pi-studio 0.9.41 → 0.9.42
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 +5 -0
- package/README.md +2 -1
- package/index.ts +3 -30
- package/package.json +1 -1
- package/shared/studio-browser-launcher.js +139 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.42] — 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Open Studio as a focused cmux browser surface in the caller’s workspace when launched from cmux, with a bounded system-browser fallback when cmux is unavailable or declines the request.
|
|
11
|
+
|
|
7
12
|
## [0.9.41] — 2026-08-11
|
|
8
13
|
|
|
9
14
|
### Fixed
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ _The video shows an earlier version of the Studio interface. The basic workflow
|
|
|
63
63
|
|
|
64
64
|
| Command | Description |
|
|
65
65
|
|---|---|
|
|
66
|
-
| `/studio` | Open with last assistant response (fallback: blank) |
|
|
66
|
+
| `/studio` | Open in cmux when available, otherwise the system browser, with the last assistant response (fallback: blank) |
|
|
67
67
|
| `/studio <path>` | Open with file preloaded |
|
|
68
68
|
| `/studio --last` | Force last response |
|
|
69
69
|
| `/studio --blank` | Force blank editor |
|
|
@@ -150,6 +150,7 @@ Studio only passes icon-pack arguments when a diagram actually references `lucid
|
|
|
150
150
|
## Notes
|
|
151
151
|
|
|
152
152
|
- Local-only server (`127.0.0.1`) with tokenized Studio URLs.
|
|
153
|
+
- When Pi runs inside cmux, Studio opens as a focused cmux browser surface in the caller’s workspace. If cmux is unavailable or declines the request, Studio falls back to the system browser.
|
|
153
154
|
- 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. The SSH hint repeats the full URL so it is visible even if your terminal only shows the latest notification. Open that URL through the tunnel, preserving the `?token=...` parameter. If SSH is not auto-detected, use `/studio --no-browser`; for stable forwarding, use `/studio --port <port>` or combine them, e.g. `/studio --no-browser --port 3417`.
|
|
154
155
|
- 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.
|
|
155
156
|
- Studio is designed as a complement to terminal pi, not a replacement.
|
package/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "./shared/studio-markdown-latex-literals.js";
|
|
30
30
|
import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
|
|
31
31
|
import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
|
|
32
|
+
import { isStudioCmuxSession, openStudioUrlInBrowser } from "./shared/studio-browser-launcher.js";
|
|
32
33
|
import { buildStudioReplTmuxStartArgs } from "./shared/studio-repl-tmux.js";
|
|
33
34
|
import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
|
|
34
35
|
import {
|
|
@@ -7544,27 +7545,6 @@ async function handleRevealLocalPreviewResourceRequest(req: IncomingMessage, res
|
|
|
7544
7545
|
}
|
|
7545
7546
|
}
|
|
7546
7547
|
|
|
7547
|
-
function openUrlInDefaultBrowser(url: string): Promise<void> {
|
|
7548
|
-
const openCommand =
|
|
7549
|
-
process.platform === "darwin"
|
|
7550
|
-
? { command: "open", args: [url] }
|
|
7551
|
-
: process.platform === "win32"
|
|
7552
|
-
? { command: "cmd", args: ["/c", "start", "", url] }
|
|
7553
|
-
: { command: "xdg-open", args: [url] };
|
|
7554
|
-
|
|
7555
|
-
return new Promise<void>((resolve, reject) => {
|
|
7556
|
-
const child = spawn(openCommand.command, openCommand.args, {
|
|
7557
|
-
stdio: "ignore",
|
|
7558
|
-
detached: true,
|
|
7559
|
-
});
|
|
7560
|
-
child.once("error", reject);
|
|
7561
|
-
child.once("spawn", () => {
|
|
7562
|
-
child.unref();
|
|
7563
|
-
resolve();
|
|
7564
|
-
});
|
|
7565
|
-
});
|
|
7566
|
-
}
|
|
7567
|
-
|
|
7568
7548
|
function openPathInDefaultViewer(path: string): Promise<void> {
|
|
7569
7549
|
const openCommand =
|
|
7570
7550
|
process.platform === "darwin"
|
|
@@ -11328,14 +11308,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
11328
11308
|
return null;
|
|
11329
11309
|
};
|
|
11330
11310
|
|
|
11331
|
-
const isProbablyCmuxSession = (): boolean =>
|
|
11332
|
-
const workspaceId = String(process.env.CMUX_WORKSPACE_ID ?? "").trim();
|
|
11333
|
-
if (workspaceId) return true;
|
|
11334
|
-
const termProgram = String(process.env.TERM_PROGRAM ?? "").trim().toLowerCase();
|
|
11335
|
-
if (termProgram === "cmux") return true;
|
|
11336
|
-
const term = String(process.env.TERM ?? "").trim().toLowerCase();
|
|
11337
|
-
return term.includes("cmux");
|
|
11338
|
-
};
|
|
11311
|
+
const isProbablyCmuxSession = (): boolean => isStudioCmuxSession(process.env);
|
|
11339
11312
|
|
|
11340
11313
|
const sanitizeTerminalNotificationText = (value: string, maxLength = 240): string => {
|
|
11341
11314
|
const sanitized = String(value)
|
|
@@ -15836,7 +15809,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15836
15809
|
const skipReason = launchOpenFlags.noBrowser ? "--no-browser was used" : "SSH was detected";
|
|
15837
15810
|
ctx.ui.notify(`${openedLabel} is ready. Browser auto-open was skipped because ${skipReason}.`, "info");
|
|
15838
15811
|
} else {
|
|
15839
|
-
await
|
|
15812
|
+
await openStudioUrlInBrowser(url);
|
|
15840
15813
|
if (selected.source === "file") {
|
|
15841
15814
|
ctx.ui.notify(`Opened ${openedLabel} with file loaded: ${selected.label}`, "info");
|
|
15842
15815
|
} else if (selected.source === "last-response") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.42",
|
|
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",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const STUDIO_CMUX_BROWSER_OPEN_TIMEOUT_MS = 5_000;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect whether the current process is running inside cmux.
|
|
7
|
+
*
|
|
8
|
+
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function isStudioCmuxSession(env = process.env) {
|
|
12
|
+
const workspaceId = String(env.CMUX_WORKSPACE_ID ?? "").trim();
|
|
13
|
+
const termProgram = String(env.TERM_PROGRAM ?? "").trim().toLowerCase();
|
|
14
|
+
const term = String(env.TERM ?? "").trim().toLowerCase();
|
|
15
|
+
const bundleId = String(env.CMUX_BUNDLE_ID ?? "").trim().toLowerCase();
|
|
16
|
+
return Boolean(workspaceId || termProgram === "cmux" || term.includes("cmux") || bundleId.includes("cmux"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the cmux CLI invocation for opening Studio in the caller's workspace.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} target
|
|
23
|
+
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
|
|
24
|
+
* @returns {{ command: string, args: string[] } | undefined}
|
|
25
|
+
*/
|
|
26
|
+
export function getStudioCmuxBrowserOpenCommand(target, env = process.env) {
|
|
27
|
+
if (!isStudioCmuxSession(env)) return undefined;
|
|
28
|
+
|
|
29
|
+
const workspaceId = String(env.CMUX_WORKSPACE_ID ?? "").trim();
|
|
30
|
+
const command = String(env.CMUX_BUNDLED_CLI_PATH ?? "").trim() || "cmux";
|
|
31
|
+
const args = ["browser", "open", target];
|
|
32
|
+
if (workspaceId) args.push("--workspace", workspaceId);
|
|
33
|
+
args.push("--focus", "true");
|
|
34
|
+
return { command, args };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the platform-native system-browser invocation.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} target
|
|
41
|
+
* @param {NodeJS.Platform} [platform]
|
|
42
|
+
* @returns {{ command: string, args: string[] }}
|
|
43
|
+
*/
|
|
44
|
+
export function getStudioDefaultBrowserOpenCommand(target, platform = process.platform) {
|
|
45
|
+
if (platform === "darwin") return { command: "open", args: [target] };
|
|
46
|
+
if (platform === "win32") return { command: "cmd", args: ["/c", "start", "", target] };
|
|
47
|
+
return { command: "xdg-open", args: [target] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {{ command: string, args: string[] }} openCommand
|
|
52
|
+
* @param {typeof spawn} spawnProcess
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
function spawnDetachedBrowser(openCommand, spawnProcess) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let child;
|
|
58
|
+
try {
|
|
59
|
+
child = spawnProcess(openCommand.command, openCommand.args, {
|
|
60
|
+
stdio: "ignore",
|
|
61
|
+
detached: true,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
reject(error);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
child.once("error", reject);
|
|
68
|
+
child.once("spawn", () => {
|
|
69
|
+
child.unref();
|
|
70
|
+
resolve();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Try to open Studio in a focused cmux browser surface.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} target
|
|
79
|
+
* @param {{
|
|
80
|
+
* env?: NodeJS.ProcessEnv | Record<string, string | undefined>,
|
|
81
|
+
* spawnProcess?: typeof spawn,
|
|
82
|
+
* timeoutMs?: number,
|
|
83
|
+
* }} [options]
|
|
84
|
+
* @returns {Promise<boolean>}
|
|
85
|
+
*/
|
|
86
|
+
export async function tryOpenStudioUrlInCmuxBrowser(target, options = {}) {
|
|
87
|
+
const openCommand = getStudioCmuxBrowserOpenCommand(target, options.env ?? process.env);
|
|
88
|
+
if (!openCommand) return false;
|
|
89
|
+
|
|
90
|
+
const spawnProcess = options.spawnProcess ?? spawn;
|
|
91
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs >= 0
|
|
92
|
+
? options.timeoutMs
|
|
93
|
+
: STUDIO_CMUX_BROWSER_OPEN_TIMEOUT_MS;
|
|
94
|
+
|
|
95
|
+
return await new Promise((resolve) => {
|
|
96
|
+
let settled = false;
|
|
97
|
+
let child;
|
|
98
|
+
const finish = (opened) => {
|
|
99
|
+
if (settled) return;
|
|
100
|
+
settled = true;
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
resolve(opened);
|
|
103
|
+
};
|
|
104
|
+
const timeout = setTimeout(() => {
|
|
105
|
+
child?.kill();
|
|
106
|
+
finish(false);
|
|
107
|
+
}, timeoutMs);
|
|
108
|
+
timeout.unref?.();
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
child = spawnProcess(openCommand.command, openCommand.args, { stdio: "ignore" });
|
|
112
|
+
} catch {
|
|
113
|
+
finish(false);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
child.once("error", () => finish(false));
|
|
117
|
+
child.once("close", (code) => finish(code === 0));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Open Studio in cmux when available, falling back to the system browser.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} target
|
|
125
|
+
* @param {{
|
|
126
|
+
* env?: NodeJS.ProcessEnv | Record<string, string | undefined>,
|
|
127
|
+
* platform?: NodeJS.Platform,
|
|
128
|
+
* spawnProcess?: typeof spawn,
|
|
129
|
+
* timeoutMs?: number,
|
|
130
|
+
* }} [options]
|
|
131
|
+
* @returns {Promise<"cmux" | "system">}
|
|
132
|
+
*/
|
|
133
|
+
export async function openStudioUrlInBrowser(target, options = {}) {
|
|
134
|
+
if (await tryOpenStudioUrlInCmuxBrowser(target, options)) return "cmux";
|
|
135
|
+
|
|
136
|
+
const openCommand = getStudioDefaultBrowserOpenCommand(target, options.platform ?? process.platform);
|
|
137
|
+
await spawnDetachedBrowser(openCommand, options.spawnProcess ?? spawn);
|
|
138
|
+
return "system";
|
|
139
|
+
}
|