pi-studio 0.9.44 → 0.9.45

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
@@ -4,6 +4,11 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.45] — 2026-08-20
8
+
9
+ ### Changed
10
+ - Open Studio in Muxy’s built-in browser when launched from a Muxy pane, using Muxy’s pane-local environment and CLI without global shell hooks, while retaining the bounded cmux and system-browser fallbacks.
11
+
7
12
  ## [0.9.44] — 2026-08-14
8
13
 
9
14
  ### Added
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 in cmux when available, otherwise the system browser, with the last assistant response (fallback: blank) |
66
+ | `/studio` | Open in Muxy or 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,7 +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
+ - When Pi runs inside Muxy or cmux, Studio opens in that terminal app’s built-in browser. Muxy is detected from its pane/socket environment without installing global hooks; cmux targets and focuses the caller’s workspace. If the detected terminal browser is unavailable, disabled, or declines the request, Studio falls back once to the system browser.
154
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`.
155
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.
156
156
  - Studio is designed as a complement to terminal pi, not a replacement.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.44",
3
+ "version": "0.9.45",
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",
@@ -1,6 +1,40 @@
1
1
  import { spawn } from "node:child_process";
2
2
 
3
- export const STUDIO_CMUX_BROWSER_OPEN_TIMEOUT_MS = 5_000;
3
+ export const STUDIO_TERMINAL_BROWSER_OPEN_TIMEOUT_MS = 5_000;
4
+ export const STUDIO_MUXY_CLI_TIMEOUT_SECONDS = 4;
5
+ // Retain the original exported name for callers that imported the cmux-only helper.
6
+ export const STUDIO_CMUX_BROWSER_OPEN_TIMEOUT_MS = STUDIO_TERMINAL_BROWSER_OPEN_TIMEOUT_MS;
7
+
8
+ /** @param {NodeJS.ProcessEnv | Record<string, string | undefined>} env */
9
+ function isStudioNestedZedTerminal(env) {
10
+ const zedTerm = String(env.ZED_TERM ?? "").trim().toLowerCase();
11
+ return zedTerm === "1" || zedTerm === "true";
12
+ }
13
+
14
+ /**
15
+ * Detect whether the current process is running inside Muxy itself rather than
16
+ * a nested application terminal that inherited Muxy's environment.
17
+ *
18
+ * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
19
+ * @returns {boolean}
20
+ */
21
+ export function isStudioMuxySession(env = process.env) {
22
+ const paneId = String(env.MUXY_PANE_ID ?? "").trim();
23
+ const socketPath = String(env.MUXY_SOCKET_PATH ?? "").trim();
24
+ return Boolean(paneId && socketPath && !isStudioNestedZedTerminal(env));
25
+ }
26
+
27
+ /**
28
+ * Build the Muxy CLI invocation for opening Studio in its built-in browser.
29
+ *
30
+ * @param {string} target
31
+ * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
32
+ * @returns {{ command: string, args: string[] } | undefined}
33
+ */
34
+ export function getStudioMuxyBrowserOpenCommand(target, env = process.env) {
35
+ if (!isStudioMuxySession(env)) return undefined;
36
+ return { command: "muxy", args: ["browser", "open", target] };
37
+ }
4
38
 
5
39
  /**
6
40
  * Detect whether the current process is running inside cmux.
@@ -9,6 +43,7 @@ export const STUDIO_CMUX_BROWSER_OPEN_TIMEOUT_MS = 5_000;
9
43
  * @returns {boolean}
10
44
  */
11
45
  export function isStudioCmuxSession(env = process.env) {
46
+ if (isStudioNestedZedTerminal(env)) return false;
12
47
  const workspaceId = String(env.CMUX_WORKSPACE_ID ?? "").trim();
13
48
  const termProgram = String(env.TERM_PROGRAM ?? "").trim().toLowerCase();
14
49
  const term = String(env.TERM ?? "").trim().toLowerCase();
@@ -73,24 +108,23 @@ function spawnDetachedBrowser(openCommand, spawnProcess) {
73
108
  }
74
109
 
75
110
  /**
76
- * Try to open Studio in a focused cmux browser surface.
111
+ * Try a terminal application's bounded browser-open command.
77
112
  *
78
- * @param {string} target
113
+ * @param {{ command: string, args: string[] } | undefined} openCommand
79
114
  * @param {{
80
- * env?: NodeJS.ProcessEnv | Record<string, string | undefined>,
81
115
  * spawnProcess?: typeof spawn,
82
116
  * timeoutMs?: number,
117
+ * spawnEnv?: NodeJS.ProcessEnv | Record<string, string | undefined>,
83
118
  * }} [options]
84
119
  * @returns {Promise<boolean>}
85
120
  */
86
- export async function tryOpenStudioUrlInCmuxBrowser(target, options = {}) {
87
- const openCommand = getStudioCmuxBrowserOpenCommand(target, options.env ?? process.env);
121
+ async function tryOpenStudioUrlWithTerminalBrowser(openCommand, options = {}) {
88
122
  if (!openCommand) return false;
89
123
 
90
124
  const spawnProcess = options.spawnProcess ?? spawn;
91
125
  const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs >= 0
92
126
  ? options.timeoutMs
93
- : STUDIO_CMUX_BROWSER_OPEN_TIMEOUT_MS;
127
+ : STUDIO_TERMINAL_BROWSER_OPEN_TIMEOUT_MS;
94
128
 
95
129
  return await new Promise((resolve) => {
96
130
  let settled = false;
@@ -108,7 +142,10 @@ export async function tryOpenStudioUrlInCmuxBrowser(target, options = {}) {
108
142
  timeout.unref?.();
109
143
 
110
144
  try {
111
- child = spawnProcess(openCommand.command, openCommand.args, { stdio: "ignore" });
145
+ const spawnOptions = options.spawnEnv
146
+ ? { stdio: "ignore", env: options.spawnEnv }
147
+ : { stdio: "ignore" };
148
+ child = spawnProcess(openCommand.command, openCommand.args, spawnOptions);
112
149
  } catch {
113
150
  finish(false);
114
151
  return;
@@ -119,7 +156,47 @@ export async function tryOpenStudioUrlInCmuxBrowser(target, options = {}) {
119
156
  }
120
157
 
121
158
  /**
122
- * Open Studio in cmux when available, falling back to the system browser.
159
+ * Try to open Studio in Muxy's built-in browser.
160
+ *
161
+ * @param {string} target
162
+ * @param {{
163
+ * env?: NodeJS.ProcessEnv | Record<string, string | undefined>,
164
+ * spawnProcess?: typeof spawn,
165
+ * timeoutMs?: number,
166
+ * }} [options]
167
+ * @returns {Promise<boolean>}
168
+ */
169
+ export async function tryOpenStudioUrlInMuxyBrowser(target, options = {}) {
170
+ const env = options.env ?? process.env;
171
+ const openCommand = getStudioMuxyBrowserOpenCommand(target, env);
172
+ return await tryOpenStudioUrlWithTerminalBrowser(openCommand, {
173
+ ...options,
174
+ spawnEnv: {
175
+ ...env,
176
+ MUXY_CLI_TIMEOUT: String(STUDIO_MUXY_CLI_TIMEOUT_SECONDS),
177
+ },
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Try to open Studio in a focused cmux browser surface.
183
+ *
184
+ * @param {string} target
185
+ * @param {{
186
+ * env?: NodeJS.ProcessEnv | Record<string, string | undefined>,
187
+ * spawnProcess?: typeof spawn,
188
+ * timeoutMs?: number,
189
+ * }} [options]
190
+ * @returns {Promise<boolean>}
191
+ */
192
+ export async function tryOpenStudioUrlInCmuxBrowser(target, options = {}) {
193
+ const openCommand = getStudioCmuxBrowserOpenCommand(target, options.env ?? process.env);
194
+ return await tryOpenStudioUrlWithTerminalBrowser(openCommand, options);
195
+ }
196
+
197
+ /**
198
+ * Open Studio in the caller's supported terminal browser when available,
199
+ * falling back once to the system browser.
123
200
  *
124
201
  * @param {string} target
125
202
  * @param {{
@@ -128,10 +205,15 @@ export async function tryOpenStudioUrlInCmuxBrowser(target, options = {}) {
128
205
  * spawnProcess?: typeof spawn,
129
206
  * timeoutMs?: number,
130
207
  * }} [options]
131
- * @returns {Promise<"cmux" | "system">}
208
+ * @returns {Promise<"muxy" | "cmux" | "system">}
132
209
  */
133
210
  export async function openStudioUrlInBrowser(target, options = {}) {
134
- if (await tryOpenStudioUrlInCmuxBrowser(target, options)) return "cmux";
211
+ const env = options.env ?? process.env;
212
+ if (isStudioMuxySession(env)) {
213
+ if (await tryOpenStudioUrlInMuxyBrowser(target, options)) return "muxy";
214
+ } else if (isStudioCmuxSession(env)) {
215
+ if (await tryOpenStudioUrlInCmuxBrowser(target, options)) return "cmux";
216
+ }
135
217
 
136
218
  const openCommand = getStudioDefaultBrowserOpenCommand(target, options.platform ?? process.platform);
137
219
  await spawnDetachedBrowser(openCommand, options.spawnProcess ?? spawn);