pi-studio 0.9.6 → 0.9.7
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 +9 -0
- package/client/studio-client.js +247 -3
- package/client/studio.css +88 -1
- package/index.ts +50 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.7] — 2026-05-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added Focus support for interactive HTML previews, using the existing sandboxed preview iframe in-place so form input and script state survive focus/unfocus.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `/studio` launched over SSH now skips opening the remote host browser by default, prints a local tunnel hint instead, and keeps local/non-SSH browser auto-open behavior unchanged. Use `/studio --open-remote` to request the previous remote-browser behavior.
|
|
14
|
+
- Moved the HTML preview Focus control next to the preview title to match PDF Focus placement.
|
|
15
|
+
|
|
7
16
|
## [0.9.6] — 2026-05-19
|
|
8
17
|
|
|
9
18
|
### Changed
|
package/client/studio-client.js
CHANGED
|
@@ -192,6 +192,11 @@
|
|
|
192
192
|
let studioPdfFocusCloseBtn = null;
|
|
193
193
|
let studioPdfFocusLastFocusedEl = null;
|
|
194
194
|
let studioPdfFocusMovedFrameState = null;
|
|
195
|
+
let studioHtmlFocusOverlayEl = null;
|
|
196
|
+
let studioHtmlFocusShellEl = null;
|
|
197
|
+
let studioHtmlFocusFullscreenBtn = null;
|
|
198
|
+
let studioHtmlFocusLastFocusedEl = null;
|
|
199
|
+
let studioHtmlFocusRestoreState = null;
|
|
195
200
|
let pendingRequestId = null;
|
|
196
201
|
let pendingKind = null;
|
|
197
202
|
let stickyStudioKind = null;
|
|
@@ -3156,6 +3161,12 @@
|
|
|
3156
3161
|
&& typeof studioPdfFocusDialogEl.contains === "function"
|
|
3157
3162
|
&& studioPdfFocusDialogEl.contains(event.target)
|
|
3158
3163
|
);
|
|
3164
|
+
const htmlFocusOwnsEvent = Boolean(
|
|
3165
|
+
studioHtmlFocusShellEl
|
|
3166
|
+
&& event.target
|
|
3167
|
+
&& typeof studioHtmlFocusShellEl.contains === "function"
|
|
3168
|
+
&& studioHtmlFocusShellEl.contains(event.target)
|
|
3169
|
+
);
|
|
3159
3170
|
const quizOwnsEvent = Boolean(
|
|
3160
3171
|
quizDialogEl
|
|
3161
3172
|
&& event.target
|
|
@@ -3175,6 +3186,12 @@
|
|
|
3175
3186
|
return;
|
|
3176
3187
|
}
|
|
3177
3188
|
|
|
3189
|
+
if (isStudioHtmlFocusOpen() && plainEscape) {
|
|
3190
|
+
event.preventDefault();
|
|
3191
|
+
closeStudioHtmlFocusViewer();
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3178
3195
|
if (isScratchpadOpen() && plainEscape) {
|
|
3179
3196
|
event.preventDefault();
|
|
3180
3197
|
closeScratchpad();
|
|
@@ -3193,7 +3210,7 @@
|
|
|
3193
3210
|
return;
|
|
3194
3211
|
}
|
|
3195
3212
|
|
|
3196
|
-
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent || quizOwnsEvent) {
|
|
3213
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
|
|
3197
3214
|
return;
|
|
3198
3215
|
}
|
|
3199
3216
|
|
|
@@ -3854,6 +3871,10 @@
|
|
|
3854
3871
|
return;
|
|
3855
3872
|
}
|
|
3856
3873
|
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
3874
|
+
if (record.shell && record.shell.classList && record.shell.classList.contains("is-focused")) {
|
|
3875
|
+
if (record.detail) record.detail.textContent = "HTML preview";
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3857
3878
|
const rawHeight = Number(data.height);
|
|
3858
3879
|
if (!Number.isFinite(rawHeight) || rawHeight <= 0) return;
|
|
3859
3880
|
const measuredHeight = Math.ceil(rawHeight + 2);
|
|
@@ -3875,30 +3896,232 @@
|
|
|
3875
3896
|
|
|
3876
3897
|
window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
|
|
3877
3898
|
|
|
3899
|
+
function isStudioHtmlFocusOpen() {
|
|
3900
|
+
return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
function ensureStudioHtmlFocusViewer() {
|
|
3904
|
+
if (studioHtmlFocusOverlayEl) return studioHtmlFocusOverlayEl;
|
|
3905
|
+
|
|
3906
|
+
const overlay = document.createElement("div");
|
|
3907
|
+
overlay.className = "studio-pdf-focus-overlay studio-html-focus-overlay";
|
|
3908
|
+
overlay.hidden = true;
|
|
3909
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
3910
|
+
overlay.addEventListener("click", (event) => {
|
|
3911
|
+
if (event.target === overlay) closeStudioHtmlFocusViewer();
|
|
3912
|
+
});
|
|
3913
|
+
document.addEventListener("fullscreenchange", syncStudioHtmlFocusFullscreenButton);
|
|
3914
|
+
document.body.appendChild(overlay);
|
|
3915
|
+
studioHtmlFocusOverlayEl = overlay;
|
|
3916
|
+
syncStudioHtmlFocusFullscreenButton();
|
|
3917
|
+
return overlay;
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
function getStudioHtmlFocusButton(shell) {
|
|
3921
|
+
return shell && typeof shell.querySelector === "function"
|
|
3922
|
+
? shell.querySelector(".studio-html-artifact-focus-btn")
|
|
3923
|
+
: null;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
function getStudioHtmlFullscreenButton(shell) {
|
|
3927
|
+
return shell && typeof shell.querySelector === "function"
|
|
3928
|
+
? shell.querySelector(".studio-html-artifact-fullscreen-btn")
|
|
3929
|
+
: null;
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
function setStudioHtmlFocusButtonMode(button, focused) {
|
|
3933
|
+
if (!button) return;
|
|
3934
|
+
button.replaceChildren(makeStudioUiRefreshIcon(focused ? "focus-exit" : "focus"));
|
|
3935
|
+
button.title = focused
|
|
3936
|
+
? "Exit HTML preview focus view."
|
|
3937
|
+
: "Open this HTML preview in a larger Studio overlay.";
|
|
3938
|
+
button.setAttribute("aria-label", focused ? "Exit HTML preview focus view" : "Focus HTML preview");
|
|
3939
|
+
button.setAttribute("aria-pressed", focused ? "true" : "false");
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
function syncStudioHtmlFocusFullscreenButton() {
|
|
3943
|
+
const button = studioHtmlFocusFullscreenBtn;
|
|
3944
|
+
if (!button) return;
|
|
3945
|
+
const shell = studioHtmlFocusShellEl;
|
|
3946
|
+
const isFullscreen = Boolean(shell && document.fullscreenElement && document.fullscreenElement === shell);
|
|
3947
|
+
button.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
|
|
3948
|
+
const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
|
|
3949
|
+
button.title = isFullscreen
|
|
3950
|
+
? "Exit browser fullscreen and keep the HTML preview focus view open."
|
|
3951
|
+
: "Ask the browser to make this HTML preview fullscreen.";
|
|
3952
|
+
button.setAttribute("aria-label", label);
|
|
3953
|
+
button.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
async function toggleStudioHtmlFocusFullscreen() {
|
|
3957
|
+
const shell = studioHtmlFocusShellEl;
|
|
3958
|
+
if (!shell) return;
|
|
3959
|
+
const isFullscreen = Boolean(document.fullscreenElement && document.fullscreenElement === shell);
|
|
3960
|
+
if (isFullscreen) {
|
|
3961
|
+
try {
|
|
3962
|
+
if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
|
|
3963
|
+
} catch (error) {
|
|
3964
|
+
setStatus("Could not exit HTML preview fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3965
|
+
} finally {
|
|
3966
|
+
syncStudioHtmlFocusFullscreenButton();
|
|
3967
|
+
}
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
if (typeof shell.requestFullscreen !== "function") {
|
|
3971
|
+
setStatus("Browser fullscreen is not available for this HTML preview.", "warning");
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
try {
|
|
3975
|
+
await shell.requestFullscreen();
|
|
3976
|
+
} catch (error) {
|
|
3977
|
+
setStatus("Could not enter HTML preview fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3978
|
+
} finally {
|
|
3979
|
+
syncStudioHtmlFocusFullscreenButton();
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
function openStudioHtmlFocusViewer(title, shell) {
|
|
3984
|
+
// Keep the existing sandboxed iframe in place. Reparenting srcdoc iframes can
|
|
3985
|
+
// recreate their browsing context and lose form/script state.
|
|
3986
|
+
const focusShell = shell instanceof HTMLElement ? shell : null;
|
|
3987
|
+
if (!focusShell || !focusShell.isConnected) return false;
|
|
3988
|
+
if (isStudioHtmlFocusOpen() && studioHtmlFocusShellEl === focusShell) return true;
|
|
3989
|
+
if (isStudioHtmlFocusOpen()) closeStudioHtmlFocusViewer();
|
|
3990
|
+
ensureStudioHtmlFocusViewer();
|
|
3991
|
+
|
|
3992
|
+
studioHtmlFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
3993
|
+
studioHtmlFocusShellEl = focusShell;
|
|
3994
|
+
studioHtmlFocusRestoreState = {
|
|
3995
|
+
role: focusShell.getAttribute("role"),
|
|
3996
|
+
ariaModal: focusShell.getAttribute("aria-modal"),
|
|
3997
|
+
ariaLabel: focusShell.getAttribute("aria-label"),
|
|
3998
|
+
};
|
|
3999
|
+
|
|
4000
|
+
focusShell.classList.add("is-focused");
|
|
4001
|
+
focusShell.setAttribute("role", "dialog");
|
|
4002
|
+
focusShell.setAttribute("aria-modal", "true");
|
|
4003
|
+
focusShell.setAttribute("aria-label", String(title || "HTML preview").trim() || "HTML preview");
|
|
4004
|
+
|
|
4005
|
+
const focusButton = getStudioHtmlFocusButton(focusShell);
|
|
4006
|
+
setStudioHtmlFocusButtonMode(focusButton, true);
|
|
4007
|
+
studioHtmlFocusFullscreenBtn = getStudioHtmlFullscreenButton(focusShell);
|
|
4008
|
+
if (studioHtmlFocusFullscreenBtn) studioHtmlFocusFullscreenBtn.hidden = false;
|
|
4009
|
+
|
|
4010
|
+
if (document.body) document.body.classList.add("studio-html-focus-open", "studio-pdf-focus-open");
|
|
4011
|
+
if (studioHtmlFocusOverlayEl) {
|
|
4012
|
+
studioHtmlFocusOverlayEl.hidden = false;
|
|
4013
|
+
studioHtmlFocusOverlayEl.setAttribute("aria-hidden", "false");
|
|
4014
|
+
}
|
|
4015
|
+
syncStudioHtmlFocusFullscreenButton();
|
|
4016
|
+
closeStudioUiRefreshMenus();
|
|
4017
|
+
closeExportPreviewMenu();
|
|
4018
|
+
window.setTimeout(() => {
|
|
4019
|
+
if (focusButton && typeof focusButton.focus === "function") focusButton.focus();
|
|
4020
|
+
}, 0);
|
|
4021
|
+
return true;
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
function closeStudioHtmlFocusViewer() {
|
|
4025
|
+
if (!isStudioHtmlFocusOpen()) return false;
|
|
4026
|
+
const shell = studioHtmlFocusShellEl;
|
|
4027
|
+
if (document.fullscreenElement && shell && document.fullscreenElement === shell) {
|
|
4028
|
+
try {
|
|
4029
|
+
const exitResult = document.exitFullscreen && document.exitFullscreen();
|
|
4030
|
+
if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
|
|
4031
|
+
} catch {}
|
|
4032
|
+
}
|
|
4033
|
+
if (studioHtmlFocusOverlayEl) {
|
|
4034
|
+
studioHtmlFocusOverlayEl.hidden = true;
|
|
4035
|
+
studioHtmlFocusOverlayEl.setAttribute("aria-hidden", "true");
|
|
4036
|
+
}
|
|
4037
|
+
if (shell) {
|
|
4038
|
+
shell.classList.remove("is-focused");
|
|
4039
|
+
const restore = studioHtmlFocusRestoreState || {};
|
|
4040
|
+
if (restore.role === null || typeof restore.role === "undefined") shell.removeAttribute("role");
|
|
4041
|
+
else shell.setAttribute("role", restore.role);
|
|
4042
|
+
if (restore.ariaModal === null || typeof restore.ariaModal === "undefined") shell.removeAttribute("aria-modal");
|
|
4043
|
+
else shell.setAttribute("aria-modal", restore.ariaModal);
|
|
4044
|
+
if (restore.ariaLabel === null || typeof restore.ariaLabel === "undefined") shell.removeAttribute("aria-label");
|
|
4045
|
+
else shell.setAttribute("aria-label", restore.ariaLabel);
|
|
4046
|
+
setStudioHtmlFocusButtonMode(getStudioHtmlFocusButton(shell), false);
|
|
4047
|
+
}
|
|
4048
|
+
if (studioHtmlFocusFullscreenBtn) studioHtmlFocusFullscreenBtn.hidden = true;
|
|
4049
|
+
studioHtmlFocusShellEl = null;
|
|
4050
|
+
studioHtmlFocusFullscreenBtn = null;
|
|
4051
|
+
studioHtmlFocusRestoreState = null;
|
|
4052
|
+
if (document.body) document.body.classList.remove("studio-html-focus-open", "studio-pdf-focus-open");
|
|
4053
|
+
const focusTarget = studioHtmlFocusLastFocusedEl;
|
|
4054
|
+
studioHtmlFocusLastFocusedEl = null;
|
|
4055
|
+
if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
|
|
4056
|
+
window.setTimeout(() => focusTarget.focus(), 0);
|
|
4057
|
+
}
|
|
4058
|
+
return true;
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
function openStudioHtmlFocusFromButton(buttonEl) {
|
|
4062
|
+
if (!buttonEl) return false;
|
|
4063
|
+
const shell = buttonEl.closest && buttonEl.closest(".studio-html-artifact-shell");
|
|
4064
|
+
if (!shell) return false;
|
|
4065
|
+
if (isStudioHtmlFocusOpen() && studioHtmlFocusShellEl === shell) {
|
|
4066
|
+
return closeStudioHtmlFocusViewer();
|
|
4067
|
+
}
|
|
4068
|
+
const title = String(shell.dataset && shell.dataset.studioHtmlTitle ? shell.dataset.studioHtmlTitle : "").trim()
|
|
4069
|
+
|| String(buttonEl.dataset && buttonEl.dataset.studioHtmlTitle ? buttonEl.dataset.studioHtmlTitle : "").trim()
|
|
4070
|
+
|| "HTML preview";
|
|
4071
|
+
return openStudioHtmlFocusViewer(title, shell);
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
function handleStudioHtmlFocusButtonClick(event) {
|
|
4075
|
+
const target = event && event.target;
|
|
4076
|
+
const buttonEl = target instanceof Element ? target.closest(".studio-html-artifact-focus-btn") : null;
|
|
4077
|
+
if (!buttonEl) return;
|
|
4078
|
+
event.preventDefault();
|
|
4079
|
+
event.stopPropagation();
|
|
4080
|
+
if (typeof event.stopImmediatePropagation === "function") {
|
|
4081
|
+
event.stopImmediatePropagation();
|
|
4082
|
+
}
|
|
4083
|
+
if (!openStudioHtmlFocusFromButton(buttonEl)) {
|
|
4084
|
+
setStatus("Could not open HTML preview focus view.", "warning");
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
|
|
3878
4088
|
function renderHtmlArtifactPreview(targetEl, html, pane, options) {
|
|
3879
4089
|
if (!targetEl) return;
|
|
3880
4090
|
const title = options && options.title ? String(options.title) : "HTML preview";
|
|
3881
4091
|
const previewId = "html_artifact_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10);
|
|
3882
4092
|
pruneDisconnectedHtmlArtifactFrames();
|
|
4093
|
+
if (isStudioHtmlFocusOpen()) closeStudioHtmlFocusViewer();
|
|
3883
4094
|
clearPreviewJumpHighlight(targetEl);
|
|
3884
4095
|
finishPreviewRender(targetEl);
|
|
3885
4096
|
targetEl.innerHTML = "";
|
|
3886
4097
|
|
|
3887
4098
|
const shell = document.createElement("div");
|
|
3888
4099
|
shell.className = "studio-html-artifact-shell";
|
|
4100
|
+
if (shell.dataset) shell.dataset.studioHtmlTitle = title;
|
|
3889
4101
|
|
|
3890
4102
|
const toolbar = document.createElement("div");
|
|
3891
4103
|
toolbar.className = "studio-html-artifact-toolbar";
|
|
4104
|
+
const titleGroup = document.createElement("div");
|
|
4105
|
+
titleGroup.className = "studio-html-artifact-title-group";
|
|
4106
|
+
const focusBtn = document.createElement("button");
|
|
4107
|
+
focusBtn.type = "button";
|
|
4108
|
+
focusBtn.className = "studio-html-artifact-focus-btn";
|
|
4109
|
+
focusBtn.title = "Open this HTML preview in a larger Studio overlay.";
|
|
4110
|
+
focusBtn.setAttribute("aria-label", "Focus HTML preview");
|
|
4111
|
+
if (focusBtn.dataset) focusBtn.dataset.studioHtmlTitle = title;
|
|
4112
|
+
focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
|
|
4113
|
+
focusBtn.addEventListener("click", handleStudioHtmlFocusButtonClick);
|
|
4114
|
+
titleGroup.appendChild(focusBtn);
|
|
3892
4115
|
const label = document.createElement("span");
|
|
3893
4116
|
label.className = "studio-html-artifact-label";
|
|
3894
4117
|
label.textContent = title;
|
|
4118
|
+
titleGroup.appendChild(label);
|
|
3895
4119
|
const detail = document.createElement("span");
|
|
3896
4120
|
detail.className = "studio-html-artifact-detail";
|
|
3897
4121
|
detail.textContent = "HTML preview";
|
|
3898
4122
|
|
|
3899
4123
|
const tools = document.createElement("span");
|
|
3900
4124
|
tools.className = "studio-html-artifact-tools";
|
|
3901
|
-
tools.appendChild(detail);
|
|
3902
4125
|
|
|
3903
4126
|
const zoomControls = document.createElement("span");
|
|
3904
4127
|
zoomControls.className = "studio-html-artifact-zoom-controls";
|
|
@@ -3948,10 +4171,24 @@
|
|
|
3948
4171
|
zoomControls.appendChild(zoomOutBtn);
|
|
3949
4172
|
zoomControls.appendChild(zoomResetBtn);
|
|
3950
4173
|
zoomControls.appendChild(zoomInBtn);
|
|
4174
|
+
const fullscreenBtn = document.createElement("button");
|
|
4175
|
+
fullscreenBtn.type = "button";
|
|
4176
|
+
fullscreenBtn.className = "studio-html-artifact-fullscreen-btn";
|
|
4177
|
+
fullscreenBtn.hidden = true;
|
|
4178
|
+
fullscreenBtn.addEventListener("pointerdown", (event) => { event.stopPropagation(); });
|
|
4179
|
+
fullscreenBtn.addEventListener("mousedown", (event) => { event.stopPropagation(); });
|
|
4180
|
+
fullscreenBtn.addEventListener("click", (event) => {
|
|
4181
|
+
event.preventDefault();
|
|
4182
|
+
event.stopPropagation();
|
|
4183
|
+
toggleStudioHtmlFocusFullscreen();
|
|
4184
|
+
});
|
|
4185
|
+
fullscreenBtn.appendChild(makeStudioUiRefreshIcon("fullscreen"));
|
|
3951
4186
|
updateZoomUi();
|
|
4187
|
+
tools.appendChild(detail);
|
|
3952
4188
|
tools.appendChild(zoomControls);
|
|
4189
|
+
tools.appendChild(fullscreenBtn);
|
|
3953
4190
|
|
|
3954
|
-
toolbar.appendChild(
|
|
4191
|
+
toolbar.appendChild(titleGroup);
|
|
3955
4192
|
toolbar.appendChild(tools);
|
|
3956
4193
|
shell.appendChild(toolbar);
|
|
3957
4194
|
|
|
@@ -15398,6 +15635,13 @@
|
|
|
15398
15635
|
handleStudioPdfFocusButtonClick(event);
|
|
15399
15636
|
}, true);
|
|
15400
15637
|
|
|
15638
|
+
document.addEventListener("click", (event) => {
|
|
15639
|
+
const target = event.target;
|
|
15640
|
+
const focusBtn = target instanceof Element ? target.closest(".studio-html-artifact-focus-btn") : null;
|
|
15641
|
+
if (!focusBtn) return;
|
|
15642
|
+
handleStudioHtmlFocusButtonClick(event);
|
|
15643
|
+
}, true);
|
|
15644
|
+
|
|
15401
15645
|
document.addEventListener("click", (event) => {
|
|
15402
15646
|
const target = event.target;
|
|
15403
15647
|
const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
|
package/client/studio.css
CHANGED
|
@@ -1635,7 +1635,8 @@
|
|
|
1635
1635
|
background: #fff;
|
|
1636
1636
|
}
|
|
1637
1637
|
|
|
1638
|
-
body.studio-pdf-focus-open
|
|
1638
|
+
body.studio-pdf-focus-open,
|
|
1639
|
+
body.studio-html-focus-open {
|
|
1639
1640
|
overflow: hidden;
|
|
1640
1641
|
}
|
|
1641
1642
|
|
|
@@ -2115,6 +2116,31 @@
|
|
|
2115
2116
|
box-shadow: 0 1px 2px var(--shadow-color);
|
|
2116
2117
|
}
|
|
2117
2118
|
|
|
2119
|
+
.rendered-markdown .studio-html-artifact-shell.is-focused {
|
|
2120
|
+
position: fixed;
|
|
2121
|
+
top: 50%;
|
|
2122
|
+
left: 50%;
|
|
2123
|
+
z-index: 71;
|
|
2124
|
+
width: min(96vw, 1440px);
|
|
2125
|
+
height: min(92vh, 1080px);
|
|
2126
|
+
min-height: 420px;
|
|
2127
|
+
margin: 0;
|
|
2128
|
+
transform: translate(-50%, -50%);
|
|
2129
|
+
border-radius: 14px;
|
|
2130
|
+
box-shadow: 0 24px 70px var(--shadow-color);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
.rendered-markdown .studio-html-artifact-shell.is-focused:fullscreen {
|
|
2134
|
+
top: 0;
|
|
2135
|
+
left: 0;
|
|
2136
|
+
width: 100vw;
|
|
2137
|
+
height: 100vh;
|
|
2138
|
+
min-height: 100vh;
|
|
2139
|
+
transform: none;
|
|
2140
|
+
border: 0;
|
|
2141
|
+
border-radius: 0;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2118
2144
|
.rendered-markdown .studio-html-artifact-toolbar {
|
|
2119
2145
|
display: flex;
|
|
2120
2146
|
align-items: center;
|
|
@@ -2129,6 +2155,20 @@
|
|
|
2129
2155
|
line-height: 1.25;
|
|
2130
2156
|
}
|
|
2131
2157
|
|
|
2158
|
+
.rendered-markdown .studio-html-artifact-shell.is-focused .studio-html-artifact-toolbar {
|
|
2159
|
+
gap: 12px;
|
|
2160
|
+
padding: 9px 12px;
|
|
2161
|
+
border-bottom-color: var(--panel-border);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
.rendered-markdown .studio-html-artifact-title-group {
|
|
2165
|
+
min-width: 0;
|
|
2166
|
+
flex: 1 1 auto;
|
|
2167
|
+
display: inline-flex;
|
|
2168
|
+
align-items: center;
|
|
2169
|
+
gap: 8px;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2132
2172
|
.rendered-markdown .studio-html-artifact-label {
|
|
2133
2173
|
min-width: 0;
|
|
2134
2174
|
overflow: hidden;
|
|
@@ -2138,6 +2178,48 @@
|
|
|
2138
2178
|
font-weight: 600;
|
|
2139
2179
|
}
|
|
2140
2180
|
|
|
2181
|
+
.rendered-markdown .studio-html-artifact-focus-btn,
|
|
2182
|
+
.rendered-markdown .studio-html-artifact-fullscreen-btn {
|
|
2183
|
+
flex: 0 0 auto;
|
|
2184
|
+
display: inline-flex;
|
|
2185
|
+
align-items: center;
|
|
2186
|
+
justify-content: center;
|
|
2187
|
+
width: 26px;
|
|
2188
|
+
min-width: 26px;
|
|
2189
|
+
height: 26px;
|
|
2190
|
+
padding: 0;
|
|
2191
|
+
border: 1px solid transparent;
|
|
2192
|
+
border-radius: 8px;
|
|
2193
|
+
background: transparent;
|
|
2194
|
+
color: var(--studio-info-text, var(--muted));
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
.rendered-markdown .studio-html-artifact-shell.is-focused .studio-html-artifact-focus-btn,
|
|
2198
|
+
.rendered-markdown .studio-html-artifact-shell.is-focused .studio-html-artifact-fullscreen-btn {
|
|
2199
|
+
width: 29px;
|
|
2200
|
+
min-width: 29px;
|
|
2201
|
+
height: 29px;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
.rendered-markdown .studio-html-artifact-fullscreen-btn[hidden] {
|
|
2205
|
+
display: none !important;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
.rendered-markdown .studio-html-artifact-focus-btn:not(:disabled):hover,
|
|
2209
|
+
.rendered-markdown .studio-html-artifact-focus-btn:focus-visible,
|
|
2210
|
+
.rendered-markdown .studio-html-artifact-fullscreen-btn:not(:disabled):hover,
|
|
2211
|
+
.rendered-markdown .studio-html-artifact-fullscreen-btn:focus-visible {
|
|
2212
|
+
background: var(--panel);
|
|
2213
|
+
border-color: var(--control-border);
|
|
2214
|
+
color: var(--text);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
.rendered-markdown .studio-html-artifact-focus-btn .studio-refresh-icon,
|
|
2218
|
+
.rendered-markdown .studio-html-artifact-fullscreen-btn .studio-refresh-icon {
|
|
2219
|
+
width: 14px;
|
|
2220
|
+
height: 14px;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2141
2223
|
.rendered-markdown .studio-html-artifact-tools {
|
|
2142
2224
|
flex: 0 0 auto;
|
|
2143
2225
|
display: inline-flex;
|
|
@@ -2204,6 +2286,11 @@
|
|
|
2204
2286
|
background: #fff;
|
|
2205
2287
|
}
|
|
2206
2288
|
|
|
2289
|
+
.rendered-markdown .studio-html-artifact-shell.is-focused .studio-html-artifact-frame {
|
|
2290
|
+
min-height: 0;
|
|
2291
|
+
height: auto !important;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2207
2294
|
.rendered-markdown .studio-html-artifact-shell.is-height-capped .studio-html-artifact-toolbar {
|
|
2208
2295
|
border-bottom-color: var(--control-border);
|
|
2209
2296
|
}
|
package/index.ts
CHANGED
|
@@ -8276,9 +8276,33 @@ function isSshSession(): boolean {
|
|
|
8276
8276
|
);
|
|
8277
8277
|
}
|
|
8278
8278
|
|
|
8279
|
-
function
|
|
8279
|
+
function parseStudioLaunchOpenFlags(rawArgs: string): { args: string; openRemoteBrowser: boolean; error?: string } {
|
|
8280
|
+
const parsed = tokenizeStudioCommandArgs(rawArgs);
|
|
8281
|
+
if (parsed.error) return { args: rawArgs, openRemoteBrowser: false, error: parsed.error };
|
|
8282
|
+
const remaining: string[] = [];
|
|
8283
|
+
let openRemoteBrowser = false;
|
|
8284
|
+
for (const token of parsed.tokens) {
|
|
8285
|
+
if (token === "--open-remote" || token === "--open-remote-browser") {
|
|
8286
|
+
openRemoteBrowser = true;
|
|
8287
|
+
continue;
|
|
8288
|
+
}
|
|
8289
|
+
remaining.push(token);
|
|
8290
|
+
}
|
|
8291
|
+
return { args: remaining.join(" "), openRemoteBrowser };
|
|
8292
|
+
}
|
|
8293
|
+
|
|
8294
|
+
function shouldAutoOpenStudioBrowser(options?: { openRemoteBrowser?: boolean }): boolean {
|
|
8295
|
+
return !isSshSession() || Boolean(options?.openRemoteBrowser);
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
function buildStudioSshTunnelHint(port: number): string | null {
|
|
8280
8299
|
if (!isSshSession()) return null;
|
|
8281
|
-
return
|
|
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");
|
|
8282
8306
|
}
|
|
8283
8307
|
|
|
8284
8308
|
function resolveRequestedStudioDocumentFromUrl(
|
|
@@ -12512,6 +12536,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
12512
12536
|
mode: StudioUiMode,
|
|
12513
12537
|
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean },
|
|
12514
12538
|
) => {
|
|
12539
|
+
const launchOpenFlags = parseStudioLaunchOpenFlags(trimmed);
|
|
12540
|
+
if (launchOpenFlags.error) {
|
|
12541
|
+
ctx.ui.notify(`${launchOpenFlags.error} Use ${options?.commandLabel ?? "/studio"} --help`, "error");
|
|
12542
|
+
return;
|
|
12543
|
+
}
|
|
12544
|
+
const launchArgs = launchOpenFlags.args;
|
|
12515
12545
|
if (mode === "full" && hasConnectedFullStudioView()) {
|
|
12516
12546
|
if (options?.replaceExistingFull) {
|
|
12517
12547
|
closeStudioClientsByMode("full", 4001, "Full Studio replaced");
|
|
@@ -12520,7 +12550,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12520
12550
|
if (serverState) {
|
|
12521
12551
|
const url = buildStudioUrl(serverState.port, serverState.token, "full");
|
|
12522
12552
|
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
12523
|
-
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port
|
|
12553
|
+
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port);
|
|
12524
12554
|
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
12525
12555
|
}
|
|
12526
12556
|
return;
|
|
@@ -12542,23 +12572,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
12542
12572
|
// ignore theme read errors
|
|
12543
12573
|
}
|
|
12544
12574
|
|
|
12545
|
-
const selected = resolveStudioLaunchDocument(
|
|
12575
|
+
const selected = resolveStudioLaunchDocument(launchArgs, ctx, options);
|
|
12546
12576
|
if (!selected) return;
|
|
12547
12577
|
initialStudioDocument = selected;
|
|
12548
12578
|
|
|
12549
12579
|
const state = await ensureServer();
|
|
12550
12580
|
const url = buildStudioUrl(state.port, state.token, mode, selected);
|
|
12551
|
-
const sshTunnelHint = buildStudioSshTunnelHint(state.port
|
|
12581
|
+
const sshTunnelHint = buildStudioSshTunnelHint(state.port);
|
|
12552
12582
|
const openedLabel = mode === "editor-only" ? "pi Studio editor-only view" : "pi Studio";
|
|
12553
12583
|
|
|
12584
|
+
const shouldOpenBrowser = shouldAutoOpenStudioBrowser({
|
|
12585
|
+
openRemoteBrowser: launchOpenFlags.openRemoteBrowser,
|
|
12586
|
+
});
|
|
12554
12587
|
try {
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
ctx.ui.notify(`Opened ${openedLabel} with file loaded: ${selected.label}`, "info");
|
|
12558
|
-
} else if (selected.source === "last-response") {
|
|
12559
|
-
ctx.ui.notify(`Opened ${openedLabel} with last model response (${selected.text.length} chars).`, "info");
|
|
12588
|
+
if (!shouldOpenBrowser) {
|
|
12589
|
+
ctx.ui.notify(`${openedLabel} is ready. Browser auto-open was skipped because SSH was detected.`, "info");
|
|
12560
12590
|
} else {
|
|
12561
|
-
|
|
12591
|
+
await openUrlInDefaultBrowser(url);
|
|
12592
|
+
if (selected.source === "file") {
|
|
12593
|
+
ctx.ui.notify(`Opened ${openedLabel} with file loaded: ${selected.label}`, "info");
|
|
12594
|
+
} else if (selected.source === "last-response") {
|
|
12595
|
+
ctx.ui.notify(`Opened ${openedLabel} with last model response (${selected.text.length} chars).`, "info");
|
|
12596
|
+
} else {
|
|
12597
|
+
ctx.ui.notify(`Opened ${openedLabel} with blank editor.`, "info");
|
|
12598
|
+
}
|
|
12562
12599
|
}
|
|
12563
12600
|
} catch (error) {
|
|
12564
12601
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -12595,7 +12632,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12595
12632
|
`Studio running at ${url} (busy: ${isStudioBusy() ? "yes" : "no"}; full views: ${counts.full}; editor-only views: ${counts.editorOnly})`,
|
|
12596
12633
|
"info",
|
|
12597
12634
|
);
|
|
12598
|
-
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port
|
|
12635
|
+
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port);
|
|
12599
12636
|
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
12600
12637
|
return;
|
|
12601
12638
|
}
|
|
@@ -12607,6 +12644,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12607
12644
|
+ " /studio <path> Open studio with file preloaded\n"
|
|
12608
12645
|
+ " /studio --blank Open with blank editor\n"
|
|
12609
12646
|
+ " /studio --last Open with last model response\n"
|
|
12647
|
+
+ " /studio --open-remote Over SSH, open the remote browser anyway\n"
|
|
12610
12648
|
+ " /studio --status Show studio status\n"
|
|
12611
12649
|
+ " /studio --stop Stop studio server\n"
|
|
12612
12650
|
+ " Note: only one full /studio view is allowed per Pi session.\n"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
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",
|