pi-studio 0.9.7 → 0.9.9
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 +10 -0
- package/client/studio-client.js +13 -10
- package/index.ts +11 -20
- package/package.json +1 -1
- package/shared/studio-ssh-hint.js +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.9] — 2026-05-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- In SSH-launched Studio sessions, copy actions now use the local browser clipboard instead of writing to the remote host clipboard.
|
|
11
|
+
|
|
12
|
+
## [0.9.8] — 2026-05-19
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Restored the full tokenized Studio URL inside SSH tunnel hints so the local browser URL remains visible even when only the latest notification is shown.
|
|
16
|
+
|
|
7
17
|
## [0.9.7] — 2026-05-19
|
|
8
18
|
|
|
9
19
|
### Added
|
package/client/studio-client.js
CHANGED
|
@@ -156,6 +156,7 @@
|
|
|
156
156
|
? "editor-only"
|
|
157
157
|
: "full";
|
|
158
158
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
159
|
+
const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
|
|
159
160
|
|
|
160
161
|
const initialQueryParams = new URLSearchParams(window.location.search || "");
|
|
161
162
|
const explicitDocumentIdentityFromUrl = initialQueryParams.has("docId")
|
|
@@ -846,16 +847,18 @@
|
|
|
846
847
|
async function writeTextToClipboard(text) {
|
|
847
848
|
const content = String(text || "");
|
|
848
849
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
850
|
+
if (!isSshStudioSession) {
|
|
851
|
+
try {
|
|
852
|
+
await fetchStudioJson("/clipboard", {
|
|
853
|
+
method: "POST",
|
|
854
|
+
body: JSON.stringify({ text: content }),
|
|
855
|
+
});
|
|
856
|
+
return true;
|
|
857
|
+
} catch {
|
|
858
|
+
// Fall back to browser clipboard APIs. The server-side clipboard path
|
|
859
|
+
// is most reliable for local Studio, but may be unavailable on systems
|
|
860
|
+
// without a clipboard command.
|
|
861
|
+
}
|
|
859
862
|
}
|
|
860
863
|
|
|
861
864
|
// Prefer a copy-event payload first. It runs synchronously inside the
|
package/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "./shared/studio-markdown-latex-literals.js";
|
|
29
29
|
import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
|
|
30
30
|
import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
|
|
31
|
+
import { buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
|
|
31
32
|
|
|
32
33
|
type Lens = "writing" | "code";
|
|
33
34
|
type RequestedLens = Lens | "auto";
|
|
@@ -8270,12 +8271,6 @@ function buildStudioUrl(
|
|
|
8270
8271
|
return `http://127.0.0.1:${port}/?${params.toString()}`;
|
|
8271
8272
|
}
|
|
8272
8273
|
|
|
8273
|
-
function isSshSession(): boolean {
|
|
8274
|
-
return Boolean(
|
|
8275
|
-
String(process.env.SSH_CONNECTION ?? process.env.SSH_CLIENT ?? process.env.SSH_TTY ?? "").trim(),
|
|
8276
|
-
);
|
|
8277
|
-
}
|
|
8278
|
-
|
|
8279
8274
|
function parseStudioLaunchOpenFlags(rawArgs: string): { args: string; openRemoteBrowser: boolean; error?: string } {
|
|
8280
8275
|
const parsed = tokenizeStudioCommandArgs(rawArgs);
|
|
8281
8276
|
if (parsed.error) return { args: rawArgs, openRemoteBrowser: false, error: parsed.error };
|
|
@@ -8295,16 +8290,6 @@ function shouldAutoOpenStudioBrowser(options?: { openRemoteBrowser?: boolean }):
|
|
|
8295
8290
|
return !isSshSession() || Boolean(options?.openRemoteBrowser);
|
|
8296
8291
|
}
|
|
8297
8292
|
|
|
8298
|
-
function buildStudioSshTunnelHint(port: number): string | null {
|
|
8299
|
-
if (!isSshSession()) return null;
|
|
8300
|
-
return [
|
|
8301
|
-
"SSH detected. Studio was not opened in the remote browser.",
|
|
8302
|
-
"To open it locally, run this on your local machine:",
|
|
8303
|
-
` ssh -L ${port}:127.0.0.1:${port} <remote-host>`,
|
|
8304
|
-
"Then open the Studio URL above in your local browser.",
|
|
8305
|
-
].join("\n");
|
|
8306
|
-
}
|
|
8307
|
-
|
|
8308
8293
|
function resolveRequestedStudioDocumentFromUrl(
|
|
8309
8294
|
requestUrl: URL,
|
|
8310
8295
|
fallback: InitialStudioDocument | null,
|
|
@@ -8661,6 +8646,7 @@ function buildStudioHtml(
|
|
|
8661
8646
|
const clientScriptHref = `/studio-client.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
8662
8647
|
const faviconHref = buildStudioFaviconDataUri(style);
|
|
8663
8648
|
const bootConfigJson = JSON.stringify({ mermaidConfig }).replace(/</g, "\\u003c");
|
|
8649
|
+
const initialSshSession = isSshSession() ? "1" : "0";
|
|
8664
8650
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
8665
8651
|
const appTitle = isEditorOnlyMode ? "π Studio — Editor" : "π Studio";
|
|
8666
8652
|
const appSubtitle = isEditorOnlyMode ? "Editor Workspace" : "Editor & Response Workspace";
|
|
@@ -8679,7 +8665,7 @@ ${cssVarsBlock}
|
|
|
8679
8665
|
</style>
|
|
8680
8666
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
8681
8667
|
</head>
|
|
8682
|
-
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-initial-resource-dir="${initialResourceDir}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-terminal-detail="${initialTerminalDetailAttr}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}">
|
|
8668
|
+
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-initial-resource-dir="${initialResourceDir}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-terminal-detail="${initialTerminalDetailAttr}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}" data-ssh-session="${initialSshSession}">
|
|
8683
8669
|
<header>
|
|
8684
8670
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
8685
8671
|
<div class="controls">
|
|
@@ -11317,6 +11303,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
11317
11303
|
return;
|
|
11318
11304
|
}
|
|
11319
11305
|
|
|
11306
|
+
if (isSshSession()) {
|
|
11307
|
+
respondJson(res, 409, { ok: false, error: "Server clipboard is disabled for SSH Studio sessions; use the browser clipboard." });
|
|
11308
|
+
return;
|
|
11309
|
+
}
|
|
11310
|
+
|
|
11320
11311
|
const result = await writeStudioSystemClipboard(text);
|
|
11321
11312
|
if (result.ok) {
|
|
11322
11313
|
respondJson(res, 200, { ok: true, method: result.method });
|
|
@@ -12550,7 +12541,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12550
12541
|
if (serverState) {
|
|
12551
12542
|
const url = buildStudioUrl(serverState.port, serverState.token, "full");
|
|
12552
12543
|
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
12553
|
-
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port);
|
|
12544
|
+
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port, url);
|
|
12554
12545
|
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
12555
12546
|
}
|
|
12556
12547
|
return;
|
|
@@ -12578,7 +12569,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12578
12569
|
|
|
12579
12570
|
const state = await ensureServer();
|
|
12580
12571
|
const url = buildStudioUrl(state.port, state.token, mode, selected);
|
|
12581
|
-
const sshTunnelHint = buildStudioSshTunnelHint(state.port);
|
|
12572
|
+
const sshTunnelHint = buildStudioSshTunnelHint(state.port, url);
|
|
12582
12573
|
const openedLabel = mode === "editor-only" ? "pi Studio editor-only view" : "pi Studio";
|
|
12583
12574
|
|
|
12584
12575
|
const shouldOpenBrowser = shouldAutoOpenStudioBrowser({
|
|
@@ -12632,7 +12623,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12632
12623
|
`Studio running at ${url} (busy: ${isStudioBusy() ? "yes" : "no"}; full views: ${counts.full}; editor-only views: ${counts.editorOnly})`,
|
|
12633
12624
|
"info",
|
|
12634
12625
|
);
|
|
12635
|
-
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port);
|
|
12626
|
+
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port, url);
|
|
12636
12627
|
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
12637
12628
|
return;
|
|
12638
12629
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
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,19 @@
|
|
|
1
|
+
export function isStudioSshSession(env = process.env) {
|
|
2
|
+
return Boolean(
|
|
3
|
+
String(env.SSH_CONNECTION ?? env.SSH_CLIENT ?? env.SSH_TTY ?? "").trim(),
|
|
4
|
+
);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildStudioSshTunnelHint(port, studioUrl, env = process.env) {
|
|
8
|
+
if (!isStudioSshSession(env)) return null;
|
|
9
|
+
const normalizedPort = Number(port);
|
|
10
|
+
const remotePort = Number.isInteger(normalizedPort) && normalizedPort > 0 ? normalizedPort : port;
|
|
11
|
+
const url = String(studioUrl || "").trim();
|
|
12
|
+
return [
|
|
13
|
+
"SSH detected. Studio was not opened in the remote browser.",
|
|
14
|
+
"To open it locally, run this on your local machine:",
|
|
15
|
+
` ssh -L ${remotePort}:127.0.0.1:${remotePort} <remote-host>`,
|
|
16
|
+
"Then open this Studio URL in your local browser:",
|
|
17
|
+
` ${url}`,
|
|
18
|
+
].join("\n");
|
|
19
|
+
}
|