pi-studio 0.9.44 → 0.9.46
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/README.md +2 -2
- package/client/studio-client.js +69 -18
- package/client/studio-preview-resource-helpers.js +52 -0
- package/index.ts +14 -6
- package/package.json +1 -1
- package/shared/studio-browser-launcher.js +93 -11
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.46] — 2026-08-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Re-resolve local Markdown images and refresh active previews when their file or working-directory context changes, preventing stale relative image URLs from rendering as broken links in Muxy and other browser hosts.
|
|
11
|
+
|
|
12
|
+
## [0.9.45] — 2026-08-20
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- 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.
|
|
16
|
+
|
|
7
17
|
## [0.9.44] — 2026-08-14
|
|
8
18
|
|
|
9
19
|
### 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
|
|
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/client/studio-client.js
CHANGED
|
@@ -222,6 +222,14 @@
|
|
|
222
222
|
) {
|
|
223
223
|
throw new Error("Studio navigation helpers failed to load.");
|
|
224
224
|
}
|
|
225
|
+
const previewResourceHelpers = globalThis.PiStudioPreviewResourceHelpers;
|
|
226
|
+
if (
|
|
227
|
+
!previewResourceHelpers
|
|
228
|
+
|| typeof previewResourceHelpers.areStudioPreviewResourceContextsEqual !== "function"
|
|
229
|
+
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalImages !== "function"
|
|
230
|
+
) {
|
|
231
|
+
throw new Error("Studio preview resource helpers failed to load.");
|
|
232
|
+
}
|
|
225
233
|
const showMeHelpers = globalThis.PiStudioShowMeHelpers;
|
|
226
234
|
if (!showMeHelpers || typeof showMeHelpers.chooseStudioShowMeFocus !== "function") {
|
|
227
235
|
throw new Error("Studio Show me helpers failed to load.");
|
|
@@ -403,6 +411,7 @@
|
|
|
403
411
|
const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
|
|
404
412
|
const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
|
|
405
413
|
const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
|
|
414
|
+
const RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS = 8_000;
|
|
406
415
|
const EDITOR_TAB_TEXT = " ";
|
|
407
416
|
const QUIZ_DEFAULT_COUNT = 5;
|
|
408
417
|
const COMPLETION_CONTEXT_STORAGE_KEY = "piStudio.completionContextMode";
|
|
@@ -6035,19 +6044,33 @@
|
|
|
6035
6044
|
}).filter(Boolean);
|
|
6036
6045
|
}
|
|
6037
6046
|
|
|
6038
|
-
function
|
|
6047
|
+
function buildLocalPreviewResourceFetchUrl(context, resourceUrl) {
|
|
6039
6048
|
const token = getToken();
|
|
6040
6049
|
if (!token) return "";
|
|
6041
6050
|
const params = new URLSearchParams({ token, path: String(resourceUrl || "") });
|
|
6042
|
-
if (
|
|
6043
|
-
params.set("sourcePath",
|
|
6051
|
+
if (context && context.sourcePath) {
|
|
6052
|
+
params.set("sourcePath", context.sourcePath);
|
|
6044
6053
|
}
|
|
6045
|
-
if (
|
|
6046
|
-
params.set("resourceDir",
|
|
6054
|
+
if (context && context.resourceDir) {
|
|
6055
|
+
params.set("resourceDir", context.resourceDir);
|
|
6047
6056
|
}
|
|
6048
6057
|
return "/html-preview-resource?" + params.toString();
|
|
6049
6058
|
}
|
|
6050
6059
|
|
|
6060
|
+
async function fetchLocalPreviewResourceDataUrl(context, resourceUrl, timeoutMs, timeoutLabel) {
|
|
6061
|
+
const fetchUrl = buildLocalPreviewResourceFetchUrl(context, resourceUrl);
|
|
6062
|
+
if (!fetchUrl) throw new Error("Missing Studio token in URL.");
|
|
6063
|
+
const response = await fetchWithTimeout(fetchUrl, { method: "GET" }, timeoutMs, timeoutLabel);
|
|
6064
|
+
const payload = await response.json().catch(() => null);
|
|
6065
|
+
if (!response.ok || !payload || payload.ok !== true || typeof payload.dataUrl !== "string") {
|
|
6066
|
+
const message = payload && typeof payload.error === "string"
|
|
6067
|
+
? payload.error
|
|
6068
|
+
: (timeoutLabel || "Local preview resource load") + " failed with HTTP " + response.status + ".";
|
|
6069
|
+
throw new Error(message);
|
|
6070
|
+
}
|
|
6071
|
+
return payload.dataUrl;
|
|
6072
|
+
}
|
|
6073
|
+
|
|
6051
6074
|
function postHtmlArtifactResourceResults(record, results) {
|
|
6052
6075
|
if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
|
|
6053
6076
|
try {
|
|
@@ -6064,15 +6087,13 @@
|
|
|
6064
6087
|
async function fetchHtmlArtifactResource(record, item) {
|
|
6065
6088
|
const resourceId = item && item.resourceId ? item.resourceId : "";
|
|
6066
6089
|
try {
|
|
6067
|
-
const
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
}
|
|
6075
|
-
return { resourceId, ok: true, dataUrl: payload.dataUrl };
|
|
6090
|
+
const dataUrl = await fetchLocalPreviewResourceDataUrl(
|
|
6091
|
+
record,
|
|
6092
|
+
item.url,
|
|
6093
|
+
HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS,
|
|
6094
|
+
"HTML preview resource load",
|
|
6095
|
+
);
|
|
6096
|
+
return { resourceId, ok: true, dataUrl };
|
|
6076
6097
|
} catch (error) {
|
|
6077
6098
|
return { resourceId, ok: false, error: error && error.message ? error.message : String(error || "HTML preview resource load failed.") };
|
|
6078
6099
|
}
|
|
@@ -8513,15 +8534,23 @@
|
|
|
8513
8534
|
const timeoutId = controller ? window.setTimeout(() => controller.abort(), 8000) : null;
|
|
8514
8535
|
|
|
8515
8536
|
const previewOptions = options && typeof options === "object" ? options : {};
|
|
8537
|
+
const requestedResourceContext = previewOptions.resourceContext && typeof previewOptions.resourceContext === "object"
|
|
8538
|
+
? previewOptions.resourceContext
|
|
8539
|
+
: null;
|
|
8516
8540
|
|
|
8517
8541
|
let response;
|
|
8518
8542
|
try {
|
|
8519
8543
|
const effectivePath = getEffectiveSavePath();
|
|
8520
|
-
const sourcePath =
|
|
8544
|
+
const sourcePath = requestedResourceContext
|
|
8545
|
+
? String(requestedResourceContext.sourcePath || "")
|
|
8546
|
+
: (effectivePath || sourceState.path || "");
|
|
8547
|
+
const resourceDir = requestedResourceContext
|
|
8548
|
+
? String(requestedResourceContext.resourceDir || "")
|
|
8549
|
+
: ((resourceDirInput && !sourcePath) ? getCurrentResourceDirValue() : "");
|
|
8521
8550
|
const payload = {
|
|
8522
8551
|
markdown: String(markdown || ""),
|
|
8523
8552
|
sourcePath: sourcePath,
|
|
8524
|
-
resourceDir:
|
|
8553
|
+
resourceDir: sourcePath ? "" : resourceDir,
|
|
8525
8554
|
};
|
|
8526
8555
|
if (previewOptions.includeEditorLanguage) {
|
|
8527
8556
|
payload.editorLanguage = String(editorLanguage || "");
|
|
@@ -9282,10 +9311,12 @@
|
|
|
9282
9311
|
stripMarkdownHtmlComments: !previewingEditorText || editorLanguage !== "latex",
|
|
9283
9312
|
};
|
|
9284
9313
|
const pdfPrepared = prepareStudioPdfBlocksForPreview(previewPrepared.markdown);
|
|
9314
|
+
const previewResourceContext = getHtmlPreviewResourceContextOptions();
|
|
9285
9315
|
|
|
9286
9316
|
try {
|
|
9287
9317
|
const renderedHtml = await renderMarkdownWithPandoc(pdfPrepared.markdown, {
|
|
9288
9318
|
includeEditorLanguage: pane === "source" || rightView === "editor-preview",
|
|
9319
|
+
resourceContext: previewResourceContext,
|
|
9289
9320
|
});
|
|
9290
9321
|
|
|
9291
9322
|
if (pane === "source") {
|
|
@@ -9297,6 +9328,14 @@
|
|
|
9297
9328
|
clearPreviewJumpHighlight(targetEl);
|
|
9298
9329
|
finishPreviewRender(targetEl);
|
|
9299
9330
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
|
|
9331
|
+
await previewResourceHelpers.hydrateStudioPreviewLocalImages(targetEl, (resourceUrl) => (
|
|
9332
|
+
fetchLocalPreviewResourceDataUrl(
|
|
9333
|
+
previewResourceContext,
|
|
9334
|
+
resourceUrl,
|
|
9335
|
+
RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS,
|
|
9336
|
+
"Preview image load",
|
|
9337
|
+
)
|
|
9338
|
+
));
|
|
9300
9339
|
renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
|
|
9301
9340
|
applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
|
|
9302
9341
|
await renderAnnotationMathInElement(targetEl);
|
|
@@ -9406,6 +9445,13 @@
|
|
|
9406
9445
|
}
|
|
9407
9446
|
}
|
|
9408
9447
|
|
|
9448
|
+
function refreshPreviewsForResourceContextChange() {
|
|
9449
|
+
renderSourcePreview();
|
|
9450
|
+
if (rightView === "preview") {
|
|
9451
|
+
renderActiveResult();
|
|
9452
|
+
}
|
|
9453
|
+
}
|
|
9454
|
+
|
|
9409
9455
|
function scheduleResponseEditorPreviewRender(delayMs) {
|
|
9410
9456
|
if (responseEditorPreviewTimer) {
|
|
9411
9457
|
window.clearTimeout(responseEditorPreviewTimer);
|
|
@@ -11414,6 +11460,7 @@
|
|
|
11414
11460
|
function setSourceState(next, options) {
|
|
11415
11461
|
const previousDescriptor = getCurrentStudioDocumentDescriptor();
|
|
11416
11462
|
const previousQuartoPath = getCurrentStudioQuartoSourcePath();
|
|
11463
|
+
const previousPreviewResourceContext = getHtmlPreviewResourceContextOptions();
|
|
11417
11464
|
const nextPath = next && next.path ? next.path : null;
|
|
11418
11465
|
sourceState = {
|
|
11419
11466
|
source: next && next.source ? next.source : "blank",
|
|
@@ -11457,6 +11504,10 @@
|
|
|
11457
11504
|
const refreshChangedQuartoView = rightView === "editor-quarto-preview" && quartoSourceChanged && isCurrentStudioQuartoDocument();
|
|
11458
11505
|
if (refreshChangedQuartoView) requestStudioQuartoPreviewCheck(false);
|
|
11459
11506
|
if (leavingUnavailableQuartoView || refreshChangedQuartoView) refreshResponseUi();
|
|
11507
|
+
const nextPreviewResourceContext = getHtmlPreviewResourceContextOptions();
|
|
11508
|
+
if (!previewResourceHelpers.areStudioPreviewResourceContextsEqual(previousPreviewResourceContext, nextPreviewResourceContext)) {
|
|
11509
|
+
refreshPreviewsForResourceContextChange();
|
|
11510
|
+
}
|
|
11460
11511
|
scheduleWorkspacePersistence();
|
|
11461
11512
|
}
|
|
11462
11513
|
|
|
@@ -21993,7 +22044,7 @@
|
|
|
21993
22044
|
}
|
|
21994
22045
|
updateSaveFileTooltip();
|
|
21995
22046
|
syncActionButtons();
|
|
21996
|
-
|
|
22047
|
+
refreshPreviewsForResourceContextChange();
|
|
21997
22048
|
scheduleWorkspacePersistence();
|
|
21998
22049
|
}
|
|
21999
22050
|
if (sourceBadgeEl) {
|
|
@@ -22036,7 +22087,7 @@
|
|
|
22036
22087
|
showResourceDirState("button");
|
|
22037
22088
|
updateSaveFileTooltip();
|
|
22038
22089
|
syncActionButtons();
|
|
22039
|
-
|
|
22090
|
+
refreshPreviewsForResourceContextChange();
|
|
22040
22091
|
scheduleWorkspacePersistence();
|
|
22041
22092
|
});
|
|
22042
22093
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const STUDIO_PREVIEW_LOCAL_IMAGE_LIMIT = 100;
|
|
3
|
+
const STUDIO_PREVIEW_IMAGE_DATA_URL_PATTERN = /^data:image\/(?:png|jpeg|gif|webp);base64,/i;
|
|
4
|
+
|
|
5
|
+
function isResolvableStudioPreviewImageSource(value) {
|
|
6
|
+
const source = String(value || "").trim();
|
|
7
|
+
if (!source || source.startsWith("#") || source.startsWith("//")) return false;
|
|
8
|
+
if (/^(?:data|blob|https?|about|javascript):/i.test(source)) return false;
|
|
9
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(source) && !/^[a-z]:[\\/]/i.test(source)) return false;
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function areStudioPreviewResourceContextsEqual(left, right) {
|
|
14
|
+
const a = left && typeof left === "object" ? left : {};
|
|
15
|
+
const b = right && typeof right === "object" ? right : {};
|
|
16
|
+
return String(a.sourcePath || "") === String(b.sourcePath || "")
|
|
17
|
+
&& String(a.resourceDir || "") === String(b.resourceDir || "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function hydrateStudioPreviewLocalImages(target, resolveResource) {
|
|
21
|
+
if (!target || typeof target.querySelectorAll !== "function" || typeof resolveResource !== "function") {
|
|
22
|
+
return { attempted: 0, resolved: 0 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const images = Array.from(target.querySelectorAll("img[src]"))
|
|
26
|
+
.filter((image) => image && typeof image.getAttribute === "function" && isResolvableStudioPreviewImageSource(image.getAttribute("src")))
|
|
27
|
+
.slice(0, STUDIO_PREVIEW_LOCAL_IMAGE_LIMIT);
|
|
28
|
+
|
|
29
|
+
let resolved = 0;
|
|
30
|
+
await Promise.all(images.map(async (image) => {
|
|
31
|
+
const originalSource = String(image.getAttribute("src") || "").trim();
|
|
32
|
+
try {
|
|
33
|
+
const dataUrl = await resolveResource(originalSource);
|
|
34
|
+
if (!STUDIO_PREVIEW_IMAGE_DATA_URL_PATTERN.test(String(dataUrl || ""))) return;
|
|
35
|
+
if (image.isConnected === false || String(image.getAttribute("src") || "").trim() !== originalSource) return;
|
|
36
|
+
image.setAttribute("src", dataUrl);
|
|
37
|
+
resolved += 1;
|
|
38
|
+
} catch {
|
|
39
|
+
// Leave unresolved images unchanged so authored browser URLs retain their normal behavior.
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
return { attempted: images.length, resolved };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
globalThis.PiStudioPreviewResourceHelpers = Object.freeze({
|
|
47
|
+
STUDIO_PREVIEW_LOCAL_IMAGE_LIMIT,
|
|
48
|
+
areStudioPreviewResourceContextsEqual,
|
|
49
|
+
hydrateStudioPreviewLocalImages,
|
|
50
|
+
isResolvableStudioPreviewImageSource,
|
|
51
|
+
});
|
|
52
|
+
})();
|
package/index.ts
CHANGED
|
@@ -108,6 +108,7 @@ const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
|
|
|
108
108
|
const STUDIO_ANNOTATION_HELPERS_URL = new URL("./client/studio-annotation-helpers.js", import.meta.url);
|
|
109
109
|
const STUDIO_MERMAID_HELPERS_URL = new URL("./client/studio-mermaid-helpers.js", import.meta.url);
|
|
110
110
|
const STUDIO_NAVIGATION_HELPERS_URL = new URL("./client/studio-navigation-helpers.js", import.meta.url);
|
|
111
|
+
const STUDIO_PREVIEW_RESOURCE_HELPERS_URL = new URL("./client/studio-preview-resource-helpers.js", import.meta.url);
|
|
111
112
|
const STUDIO_SHOW_ME_HELPERS_URL = new URL("./client/studio-show-me-helpers.js", import.meta.url);
|
|
112
113
|
const STUDIO_CLIENT_URL = new URL("./client/studio-client.js", import.meta.url);
|
|
113
114
|
|
|
@@ -10563,6 +10564,7 @@ function buildStudioHtml(
|
|
|
10563
10564
|
const annotationHelpersScriptHref = `/studio-annotation-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10564
10565
|
const mermaidHelpersScriptHref = `/studio-mermaid-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10565
10566
|
const navigationHelpersScriptHref = `/studio-navigation-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10567
|
+
const previewResourceHelpersScriptHref = `/studio-preview-resource-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10566
10568
|
const showMeHelpersScriptHref = `/studio-show-me-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10567
10569
|
const clientScriptHref = `/studio-client.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10568
10570
|
const faviconHref = buildStudioFaviconDataUri(style);
|
|
@@ -10986,6 +10988,7 @@ ${cssVarsBlock}
|
|
|
10986
10988
|
<script src="${annotationHelpersScriptHref}"></script>
|
|
10987
10989
|
<script src="${mermaidHelpersScriptHref}"></script>
|
|
10988
10990
|
<script src="${navigationHelpersScriptHref}"></script>
|
|
10991
|
+
<script src="${previewResourceHelpersScriptHref}"></script>
|
|
10989
10992
|
<script src="${showMeHelpersScriptHref}"></script>
|
|
10990
10993
|
<script src="${clientScriptHref}"></script>
|
|
10991
10994
|
</body>
|
|
@@ -14601,6 +14604,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14601
14604
|
requestUrl.pathname === "/studio-annotation-helpers.js"
|
|
14602
14605
|
|| requestUrl.pathname === "/studio-mermaid-helpers.js"
|
|
14603
14606
|
|| requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14607
|
+
|| requestUrl.pathname === "/studio-preview-resource-helpers.js"
|
|
14604
14608
|
|| requestUrl.pathname === "/studio-show-me-helpers.js"
|
|
14605
14609
|
|| requestUrl.pathname === "/studio-client.js"
|
|
14606
14610
|
) {
|
|
@@ -14623,18 +14627,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
14623
14627
|
? STUDIO_MERMAID_HELPERS_URL
|
|
14624
14628
|
: requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14625
14629
|
? STUDIO_NAVIGATION_HELPERS_URL
|
|
14626
|
-
: requestUrl.pathname === "/studio-
|
|
14627
|
-
?
|
|
14628
|
-
:
|
|
14630
|
+
: requestUrl.pathname === "/studio-preview-resource-helpers.js"
|
|
14631
|
+
? STUDIO_PREVIEW_RESOURCE_HELPERS_URL
|
|
14632
|
+
: requestUrl.pathname === "/studio-show-me-helpers.js"
|
|
14633
|
+
? STUDIO_SHOW_ME_HELPERS_URL
|
|
14634
|
+
: STUDIO_CLIENT_URL;
|
|
14629
14635
|
const targetLabel = requestUrl.pathname === "/studio-annotation-helpers.js"
|
|
14630
14636
|
? "studio annotation helper script"
|
|
14631
14637
|
: requestUrl.pathname === "/studio-mermaid-helpers.js"
|
|
14632
14638
|
? "studio Mermaid helper script"
|
|
14633
14639
|
: requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14634
14640
|
? "studio navigation helper script"
|
|
14635
|
-
: requestUrl.pathname === "/studio-
|
|
14636
|
-
? "studio
|
|
14637
|
-
:
|
|
14641
|
+
: requestUrl.pathname === "/studio-preview-resource-helpers.js"
|
|
14642
|
+
? "studio preview resource helper script"
|
|
14643
|
+
: requestUrl.pathname === "/studio-show-me-helpers.js"
|
|
14644
|
+
? "studio Show me helper script"
|
|
14645
|
+
: "studio client script";
|
|
14638
14646
|
|
|
14639
14647
|
try {
|
|
14640
14648
|
const clientScript = readFileSync(targetUrl, "utf-8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.46",
|
|
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
|
|
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
|
|
111
|
+
* Try a terminal application's bounded browser-open command.
|
|
77
112
|
*
|
|
78
|
-
* @param {string}
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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);
|