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 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 openUrlInDefaultBrowser(url);
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.41",
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
+ }