pi-studio 0.9.8 → 0.9.10
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 +11 -0
- package/client/studio-client.js +245 -13
- package/client/studio.css +95 -13
- package/index.ts +52 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.10] — 2026-05-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added keyboard shortcuts for Studio pane navigation: F6 switches panes, F7/Shift+F7 cycles the active pane view, F8 focuses editor text, Shift+F8 focuses right-pane content, F9 toggles Zen mode, and F10 keeps pane focus/unfocus.
|
|
11
|
+
- Added a compact **Shortcuts (?)** footer button with a keyboard-shortcuts overlay; `?` opens it when focus is not in a text-entry field.
|
|
12
|
+
|
|
13
|
+
## [0.9.9] — 2026-05-19
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- In SSH-launched Studio sessions, copy actions now use the local browser clipboard instead of writing to the remote host clipboard.
|
|
17
|
+
|
|
7
18
|
## [0.9.8] — 2026-05-19
|
|
8
19
|
|
|
9
20
|
### Fixed
|
package/client/studio-client.js
CHANGED
|
@@ -119,6 +119,10 @@
|
|
|
119
119
|
const editorFontSizeSelect = document.getElementById("editorFontSizeSelect");
|
|
120
120
|
const annotationModeSelect = document.getElementById("annotationModeSelect");
|
|
121
121
|
const compactBtn = document.getElementById("compactBtn");
|
|
122
|
+
const shortcutsBtn = document.getElementById("shortcutsBtn");
|
|
123
|
+
const shortcutsOverlayEl = document.getElementById("shortcutsOverlay");
|
|
124
|
+
const shortcutsDialogEl = document.getElementById("shortcutsDialog");
|
|
125
|
+
const shortcutsCloseBtn = document.getElementById("shortcutsCloseBtn");
|
|
122
126
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
123
127
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
124
128
|
const reviewNotesBtn = document.getElementById("reviewNotesBtn");
|
|
@@ -156,6 +160,7 @@
|
|
|
156
160
|
? "editor-only"
|
|
157
161
|
: "full";
|
|
158
162
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
163
|
+
const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
|
|
159
164
|
|
|
160
165
|
const initialQueryParams = new URLSearchParams(window.location.search || "");
|
|
161
166
|
const explicitDocumentIdentityFromUrl = initialQueryParams.has("docId")
|
|
@@ -846,16 +851,18 @@
|
|
|
846
851
|
async function writeTextToClipboard(text) {
|
|
847
852
|
const content = String(text || "");
|
|
848
853
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
854
|
+
if (!isSshStudioSession) {
|
|
855
|
+
try {
|
|
856
|
+
await fetchStudioJson("/clipboard", {
|
|
857
|
+
method: "POST",
|
|
858
|
+
body: JSON.stringify({ text: content }),
|
|
859
|
+
});
|
|
860
|
+
return true;
|
|
861
|
+
} catch {
|
|
862
|
+
// Fall back to browser clipboard APIs. The server-side clipboard path
|
|
863
|
+
// is most reliable for local Studio, but may be unavailable on systems
|
|
864
|
+
// without a clipboard command.
|
|
865
|
+
}
|
|
859
866
|
}
|
|
860
867
|
|
|
861
868
|
// Prefer a copy-event payload first. It runs synchronously inside the
|
|
@@ -1902,7 +1909,7 @@
|
|
|
1902
1909
|
if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
|
|
1903
1910
|
if (!zenModeBtn) return;
|
|
1904
1911
|
zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "Zen";
|
|
1905
|
-
zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls." : "Hide secondary Studio controls.";
|
|
1912
|
+
zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls. Shortcut: F9." : "Hide secondary Studio controls. Shortcut: F9.";
|
|
1906
1913
|
zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
|
|
1907
1914
|
}
|
|
1908
1915
|
|
|
@@ -3081,6 +3088,110 @@
|
|
|
3081
3088
|
}
|
|
3082
3089
|
}
|
|
3083
3090
|
|
|
3091
|
+
function focusPaneViewControl(pane) {
|
|
3092
|
+
const control = pane === "right" ? rightViewSelect : editorViewSelect;
|
|
3093
|
+
if (!control || control.disabled || control.hidden) return false;
|
|
3094
|
+
try {
|
|
3095
|
+
control.focus({ preventScroll: true });
|
|
3096
|
+
} catch {
|
|
3097
|
+
try { control.focus(); } catch { return false; }
|
|
3098
|
+
}
|
|
3099
|
+
return true;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
function activatePaneFromShortcut(nextPane) {
|
|
3103
|
+
const pane = nextPane === "right" ? "right" : "left";
|
|
3104
|
+
if (isEditorOnlyMode && pane === "right") {
|
|
3105
|
+
setStatus("Only the editor pane is available in editor-only Studio.", "warning");
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
const snapshot = snapshotStudioScrollablePositions();
|
|
3109
|
+
setActivePane(pane);
|
|
3110
|
+
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3111
|
+
focusPaneViewControl(pane);
|
|
3112
|
+
setStatus("Active pane: " + paneLabel(pane) + ". F7 cycles this pane's view.");
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
function getSelectEnabledValues(selectEl) {
|
|
3116
|
+
if (!selectEl || !selectEl.options) return [];
|
|
3117
|
+
return Array.from(selectEl.options)
|
|
3118
|
+
.filter((option) => option && !option.disabled)
|
|
3119
|
+
.map((option) => option.value)
|
|
3120
|
+
.filter((value) => typeof value === "string" && value);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
function getCycledSelectValue(selectEl, currentValue, direction) {
|
|
3124
|
+
const values = getSelectEnabledValues(selectEl);
|
|
3125
|
+
if (!values.length) return null;
|
|
3126
|
+
const currentIndex = values.indexOf(currentValue);
|
|
3127
|
+
const startIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
3128
|
+
const step = direction < 0 ? -1 : 1;
|
|
3129
|
+
return values[(startIndex + step + values.length) % values.length];
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
function focusEditorTextFromShortcut() {
|
|
3133
|
+
const snapshot = snapshotStudioScrollablePositions();
|
|
3134
|
+
setActivePane("left");
|
|
3135
|
+
if (editorView !== "markdown") setEditorView("markdown");
|
|
3136
|
+
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3137
|
+
window.setTimeout(() => {
|
|
3138
|
+
if (sourceTextEl && typeof sourceTextEl.focus === "function") {
|
|
3139
|
+
try {
|
|
3140
|
+
sourceTextEl.focus({ preventScroll: true });
|
|
3141
|
+
} catch {
|
|
3142
|
+
try { sourceTextEl.focus(); } catch {}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}, 0);
|
|
3146
|
+
setStatus("Editor text focused.");
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
function focusRightContentFromShortcut() {
|
|
3150
|
+
if (isEditorOnlyMode) {
|
|
3151
|
+
setStatus("Only the editor pane is available in editor-only Studio.", "warning");
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
const snapshot = snapshotStudioScrollablePositions();
|
|
3155
|
+
setActivePane("right");
|
|
3156
|
+
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3157
|
+
window.setTimeout(() => {
|
|
3158
|
+
if (critiqueViewEl && typeof critiqueViewEl.focus === "function") {
|
|
3159
|
+
if (!critiqueViewEl.hasAttribute("tabindex")) critiqueViewEl.setAttribute("tabindex", "-1");
|
|
3160
|
+
try {
|
|
3161
|
+
critiqueViewEl.focus({ preventScroll: true });
|
|
3162
|
+
} catch {
|
|
3163
|
+
try { critiqueViewEl.focus(); } catch {}
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}, 0);
|
|
3167
|
+
setStatus("Right pane content focused.");
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function cycleActivePaneView(direction) {
|
|
3171
|
+
if (activePane === "right") {
|
|
3172
|
+
if (isEditorOnlyMode || !rightViewSelect || rightViewSelect.disabled) {
|
|
3173
|
+
setStatus("The right-pane view selector is unavailable.", "warning");
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
const nextView = getCycledSelectValue(rightViewSelect, rightView, direction);
|
|
3177
|
+
if (!nextView) return;
|
|
3178
|
+
setRightView(nextView);
|
|
3179
|
+
focusPaneViewControl("right");
|
|
3180
|
+
setStatus("Right pane view: " + (rightViewSelect.selectedOptions && rightViewSelect.selectedOptions[0] ? rightViewSelect.selectedOptions[0].textContent : nextView) + ".");
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
if (!editorViewSelect || editorViewSelect.disabled) {
|
|
3185
|
+
setStatus("The editor view selector is unavailable.", "warning");
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
const nextView = getCycledSelectValue(editorViewSelect, editorView, direction);
|
|
3189
|
+
if (!nextView) return;
|
|
3190
|
+
setEditorView(nextView);
|
|
3191
|
+
focusPaneViewControl("left");
|
|
3192
|
+
setStatus("Editor view: " + (editorViewSelect.selectedOptions && editorViewSelect.selectedOptions[0] ? editorViewSelect.selectedOptions[0].textContent : nextView) + ".");
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3084
3195
|
function paneLabel(pane) {
|
|
3085
3196
|
if (pane === "right") {
|
|
3086
3197
|
return "Response";
|
|
@@ -3128,6 +3239,17 @@
|
|
|
3128
3239
|
return false;
|
|
3129
3240
|
}
|
|
3130
3241
|
|
|
3242
|
+
function isTextEntryShortcutTarget(target) {
|
|
3243
|
+
if (!(target instanceof Element)) return false;
|
|
3244
|
+
const editable = target.closest("input, textarea, select, [contenteditable]");
|
|
3245
|
+
if (!editable) return false;
|
|
3246
|
+
if (editable.hasAttribute && editable.hasAttribute("contenteditable")) {
|
|
3247
|
+
const value = String(editable.getAttribute("contenteditable") || "").toLowerCase();
|
|
3248
|
+
return value !== "false";
|
|
3249
|
+
}
|
|
3250
|
+
return true;
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3131
3253
|
function handlePaneShortcut(event) {
|
|
3132
3254
|
if (!event || event.defaultPrevented) return;
|
|
3133
3255
|
|
|
@@ -3155,6 +3277,12 @@
|
|
|
3155
3277
|
&& typeof outlineDialogEl.contains === "function"
|
|
3156
3278
|
&& outlineDialogEl.contains(event.target)
|
|
3157
3279
|
);
|
|
3280
|
+
const shortcutsOwnsEvent = Boolean(
|
|
3281
|
+
shortcutsDialogEl
|
|
3282
|
+
&& event.target
|
|
3283
|
+
&& typeof shortcutsDialogEl.contains === "function"
|
|
3284
|
+
&& shortcutsDialogEl.contains(event.target)
|
|
3285
|
+
);
|
|
3158
3286
|
const pdfFocusOwnsEvent = Boolean(
|
|
3159
3287
|
studioPdfFocusDialogEl
|
|
3160
3288
|
&& event.target
|
|
@@ -3198,6 +3326,12 @@
|
|
|
3198
3326
|
return;
|
|
3199
3327
|
}
|
|
3200
3328
|
|
|
3329
|
+
if (isShortcutsOpen() && plainEscape) {
|
|
3330
|
+
event.preventDefault();
|
|
3331
|
+
closeShortcuts();
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3201
3335
|
if (isReviewNotesOpen() && plainEscape) {
|
|
3202
3336
|
event.preventDefault();
|
|
3203
3337
|
closeReviewNotes();
|
|
@@ -3210,7 +3344,46 @@
|
|
|
3210
3344
|
return;
|
|
3211
3345
|
}
|
|
3212
3346
|
|
|
3213
|
-
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
|
|
3347
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
if ((key === "?" || (key === "/" && event.shiftKey)) && !event.metaKey && !event.ctrlKey && !event.altKey && !isTextEntryShortcutTarget(event.target)) {
|
|
3352
|
+
event.preventDefault();
|
|
3353
|
+
toggleShortcuts();
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3358
|
+
if (isPaneSwitchShortcut) {
|
|
3359
|
+
event.preventDefault();
|
|
3360
|
+
activatePaneFromShortcut(activePane === "right" ? "left" : "right");
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
const isViewCycleShortcut = key === "F7" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3365
|
+
if (isViewCycleShortcut) {
|
|
3366
|
+
event.preventDefault();
|
|
3367
|
+
cycleActivePaneView(event.shiftKey ? -1 : 1);
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
const isContentFocusShortcut = key === "F8" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3372
|
+
if (isContentFocusShortcut) {
|
|
3373
|
+
event.preventDefault();
|
|
3374
|
+
if (event.shiftKey) {
|
|
3375
|
+
focusRightContentFromShortcut();
|
|
3376
|
+
} else {
|
|
3377
|
+
focusEditorTextFromShortcut();
|
|
3378
|
+
}
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
const isZenModeShortcut = key === "F9" && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
|
|
3383
|
+
if (isZenModeShortcut) {
|
|
3384
|
+
event.preventDefault();
|
|
3385
|
+
setStudioZenMode(!studioZenModeEnabled);
|
|
3386
|
+
setStatus(studioZenModeEnabled ? "Zen mode on." : "Zen mode off.");
|
|
3214
3387
|
return;
|
|
3215
3388
|
}
|
|
3216
3389
|
|
|
@@ -9136,6 +9309,10 @@
|
|
|
9136
9309
|
persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
|
|
9137
9310
|
}
|
|
9138
9311
|
|
|
9312
|
+
function isShortcutsOpen() {
|
|
9313
|
+
return Boolean(shortcutsOverlayEl && !shortcutsOverlayEl.hidden);
|
|
9314
|
+
}
|
|
9315
|
+
|
|
9139
9316
|
function isScratchpadOpen() {
|
|
9140
9317
|
return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
|
|
9141
9318
|
}
|
|
@@ -9149,7 +9326,7 @@
|
|
|
9149
9326
|
}
|
|
9150
9327
|
|
|
9151
9328
|
function syncModalOpenState() {
|
|
9152
|
-
document.body.classList.toggle("scratchpad-open", isScratchpadOpen());
|
|
9329
|
+
document.body.classList.toggle("scratchpad-open", isScratchpadOpen() || isShortcutsOpen());
|
|
9153
9330
|
}
|
|
9154
9331
|
|
|
9155
9332
|
function describeStudioDocument(state) {
|
|
@@ -13345,6 +13522,41 @@
|
|
|
13345
13522
|
updateScratchpadUi();
|
|
13346
13523
|
}
|
|
13347
13524
|
|
|
13525
|
+
function closeShortcuts(options) {
|
|
13526
|
+
if (!shortcutsOverlayEl || shortcutsOverlayEl.hidden) return;
|
|
13527
|
+
shortcutsOverlayEl.hidden = true;
|
|
13528
|
+
syncModalOpenState();
|
|
13529
|
+
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
13530
|
+
? options.focusTarget
|
|
13531
|
+
: (shortcutsBtn || sourceTextEl);
|
|
13532
|
+
if (focusTarget && typeof focusTarget.focus === "function") {
|
|
13533
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
13534
|
+
? window.requestAnimationFrame.bind(window)
|
|
13535
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
13536
|
+
schedule(() => focusTarget.focus());
|
|
13537
|
+
}
|
|
13538
|
+
}
|
|
13539
|
+
|
|
13540
|
+
function openShortcuts() {
|
|
13541
|
+
if (!shortcutsOverlayEl) return;
|
|
13542
|
+
if (isScratchpadOpen()) closeScratchpad({ focusTarget: null });
|
|
13543
|
+
if (isReviewNotesOpen()) closeReviewNotes({ focusTarget: null });
|
|
13544
|
+
if (isOutlineOpen()) closeOutline({ focusTarget: null });
|
|
13545
|
+
shortcutsOverlayEl.hidden = false;
|
|
13546
|
+
syncModalOpenState();
|
|
13547
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
13548
|
+
? window.requestAnimationFrame.bind(window)
|
|
13549
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
13550
|
+
schedule(() => {
|
|
13551
|
+
if (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
|
|
13552
|
+
});
|
|
13553
|
+
}
|
|
13554
|
+
|
|
13555
|
+
function toggleShortcuts() {
|
|
13556
|
+
if (isShortcutsOpen()) closeShortcuts({ focusTarget: shortcutsBtn || sourceTextEl });
|
|
13557
|
+
else openShortcuts();
|
|
13558
|
+
}
|
|
13559
|
+
|
|
13348
13560
|
function closeScratchpad(options) {
|
|
13349
13561
|
if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
|
|
13350
13562
|
scratchpadOverlayEl.hidden = true;
|
|
@@ -15696,6 +15908,26 @@
|
|
|
15696
15908
|
});
|
|
15697
15909
|
}
|
|
15698
15910
|
|
|
15911
|
+
if (shortcutsBtn) {
|
|
15912
|
+
shortcutsBtn.addEventListener("click", () => {
|
|
15913
|
+
toggleShortcuts();
|
|
15914
|
+
});
|
|
15915
|
+
}
|
|
15916
|
+
|
|
15917
|
+
if (shortcutsCloseBtn) {
|
|
15918
|
+
shortcutsCloseBtn.addEventListener("click", () => {
|
|
15919
|
+
closeShortcuts();
|
|
15920
|
+
});
|
|
15921
|
+
}
|
|
15922
|
+
|
|
15923
|
+
if (shortcutsOverlayEl) {
|
|
15924
|
+
shortcutsOverlayEl.addEventListener("click", (event) => {
|
|
15925
|
+
if (event.target === shortcutsOverlayEl) {
|
|
15926
|
+
closeShortcuts();
|
|
15927
|
+
}
|
|
15928
|
+
});
|
|
15929
|
+
}
|
|
15930
|
+
|
|
15699
15931
|
if (scratchpadBtn) {
|
|
15700
15932
|
scratchpadBtn.addEventListener("click", () => {
|
|
15701
15933
|
openScratchpad();
|
package/client/studio.css
CHANGED
|
@@ -3326,15 +3326,26 @@
|
|
|
3326
3326
|
grid-area: hint;
|
|
3327
3327
|
justify-self: end;
|
|
3328
3328
|
align-self: center;
|
|
3329
|
+
display: inline-flex;
|
|
3330
|
+
align-items: center;
|
|
3331
|
+
gap: 6px;
|
|
3332
|
+
padding: 4px 8px;
|
|
3333
|
+
border-radius: 999px;
|
|
3334
|
+
border: 1px solid transparent;
|
|
3335
|
+
background: transparent;
|
|
3329
3336
|
color: var(--studio-footer-text, var(--muted));
|
|
3330
3337
|
font-size: 11px;
|
|
3338
|
+
line-height: 1.2;
|
|
3331
3339
|
white-space: nowrap;
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3340
|
+
opacity: 0.9;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
.shortcut-hint:not(:disabled):hover,
|
|
3344
|
+
.shortcut-hint:focus-visible {
|
|
3345
|
+
background: var(--panel-2);
|
|
3346
|
+
border-color: var(--control-border);
|
|
3347
|
+
color: var(--text);
|
|
3348
|
+
opacity: 1;
|
|
3338
3349
|
}
|
|
3339
3350
|
|
|
3340
3351
|
.footer-compact-btn {
|
|
@@ -3369,7 +3380,8 @@
|
|
|
3369
3380
|
overflow: hidden;
|
|
3370
3381
|
}
|
|
3371
3382
|
|
|
3372
|
-
.scratchpad-overlay
|
|
3383
|
+
.scratchpad-overlay,
|
|
3384
|
+
.shortcuts-overlay {
|
|
3373
3385
|
position: fixed;
|
|
3374
3386
|
inset: 0;
|
|
3375
3387
|
z-index: 50;
|
|
@@ -3381,10 +3393,28 @@
|
|
|
3381
3393
|
backdrop-filter: blur(2px);
|
|
3382
3394
|
}
|
|
3383
3395
|
|
|
3384
|
-
.scratchpad-overlay[hidden]
|
|
3396
|
+
.scratchpad-overlay[hidden],
|
|
3397
|
+
.shortcuts-overlay[hidden] {
|
|
3385
3398
|
display: none !important;
|
|
3386
3399
|
}
|
|
3387
3400
|
|
|
3401
|
+
.scratchpad-dialog,
|
|
3402
|
+
.shortcuts-dialog {
|
|
3403
|
+
width: min(860px, 100%);
|
|
3404
|
+
max-height: min(82vh, 900px);
|
|
3405
|
+
border: 1px solid var(--panel-border);
|
|
3406
|
+
border-radius: 14px;
|
|
3407
|
+
background: var(--panel);
|
|
3408
|
+
box-shadow: 0 18px 50px var(--shadow-color);
|
|
3409
|
+
display: flex;
|
|
3410
|
+
flex-direction: column;
|
|
3411
|
+
overflow: hidden;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
.shortcuts-dialog {
|
|
3415
|
+
width: min(720px, 100%);
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3388
3418
|
.scratchpad-dialog {
|
|
3389
3419
|
width: min(860px, 100%);
|
|
3390
3420
|
max-height: min(82vh, 900px);
|
|
@@ -3397,7 +3427,8 @@
|
|
|
3397
3427
|
overflow: hidden;
|
|
3398
3428
|
}
|
|
3399
3429
|
|
|
3400
|
-
.scratchpad-header
|
|
3430
|
+
.scratchpad-header,
|
|
3431
|
+
.shortcuts-header {
|
|
3401
3432
|
display: flex;
|
|
3402
3433
|
align-items: flex-start;
|
|
3403
3434
|
justify-content: space-between;
|
|
@@ -3407,18 +3438,21 @@
|
|
|
3407
3438
|
background: var(--scratchpad-header-bg, var(--panel-2));
|
|
3408
3439
|
}
|
|
3409
3440
|
|
|
3410
|
-
.scratchpad-header > div
|
|
3441
|
+
.scratchpad-header > div,
|
|
3442
|
+
.shortcuts-header > div {
|
|
3411
3443
|
flex: 1 1 auto;
|
|
3412
3444
|
min-width: 0;
|
|
3413
3445
|
}
|
|
3414
3446
|
|
|
3415
|
-
.scratchpad-header h2
|
|
3447
|
+
.scratchpad-header h2,
|
|
3448
|
+
.shortcuts-header h2 {
|
|
3416
3449
|
margin: 0;
|
|
3417
3450
|
font-size: 17px;
|
|
3418
3451
|
font-weight: 600;
|
|
3419
3452
|
}
|
|
3420
3453
|
|
|
3421
|
-
.scratchpad-description
|
|
3454
|
+
.scratchpad-description,
|
|
3455
|
+
.shortcuts-description {
|
|
3422
3456
|
margin: 6px 0 0;
|
|
3423
3457
|
font-size: 12px;
|
|
3424
3458
|
line-height: 1.45;
|
|
@@ -3426,12 +3460,60 @@
|
|
|
3426
3460
|
max-width: none;
|
|
3427
3461
|
}
|
|
3428
3462
|
|
|
3429
|
-
.scratchpad-close-btn
|
|
3463
|
+
.scratchpad-close-btn,
|
|
3464
|
+
.shortcuts-close-btn {
|
|
3430
3465
|
padding: 6px 10px;
|
|
3431
3466
|
line-height: 1;
|
|
3432
3467
|
flex: 0 0 auto;
|
|
3433
3468
|
}
|
|
3434
3469
|
|
|
3470
|
+
.shortcuts-body {
|
|
3471
|
+
display: grid;
|
|
3472
|
+
gap: 14px;
|
|
3473
|
+
padding: 16px 18px 18px;
|
|
3474
|
+
overflow: auto;
|
|
3475
|
+
background: var(--panel);
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
.shortcuts-group h3 {
|
|
3479
|
+
margin: 0 0 8px;
|
|
3480
|
+
font-size: 12px;
|
|
3481
|
+
font-weight: 700;
|
|
3482
|
+
color: var(--studio-info-text, var(--muted));
|
|
3483
|
+
text-transform: uppercase;
|
|
3484
|
+
letter-spacing: 0.04em;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
.shortcuts-group dl {
|
|
3488
|
+
display: grid;
|
|
3489
|
+
gap: 6px;
|
|
3490
|
+
margin: 0;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
.shortcuts-group dl > div {
|
|
3494
|
+
display: grid;
|
|
3495
|
+
grid-template-columns: minmax(130px, max-content) minmax(0, 1fr);
|
|
3496
|
+
gap: 12px;
|
|
3497
|
+
align-items: baseline;
|
|
3498
|
+
padding: 6px 0;
|
|
3499
|
+
border-top: 1px solid var(--border-subtle);
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
.shortcuts-group dt {
|
|
3503
|
+
margin: 0;
|
|
3504
|
+
font-family: var(--font-mono);
|
|
3505
|
+
font-size: 12px;
|
|
3506
|
+
color: var(--text);
|
|
3507
|
+
white-space: nowrap;
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
.shortcuts-group dd {
|
|
3511
|
+
margin: 0;
|
|
3512
|
+
color: var(--studio-info-text, var(--muted));
|
|
3513
|
+
font-size: 12px;
|
|
3514
|
+
line-height: 1.35;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3435
3517
|
.scratchpad-textarea {
|
|
3436
3518
|
width: 100%;
|
|
3437
3519
|
min-height: 280px;
|
package/index.ts
CHANGED
|
@@ -8646,6 +8646,7 @@ function buildStudioHtml(
|
|
|
8646
8646
|
const clientScriptHref = `/studio-client.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
8647
8647
|
const faviconHref = buildStudioFaviconDataUri(style);
|
|
8648
8648
|
const bootConfigJson = JSON.stringify({ mermaidConfig }).replace(/</g, "\\u003c");
|
|
8649
|
+
const initialSshSession = isSshSession() ? "1" : "0";
|
|
8649
8650
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
8650
8651
|
const appTitle = isEditorOnlyMode ? "π Studio — Editor" : "π Studio";
|
|
8651
8652
|
const appSubtitle = isEditorOnlyMode ? "Editor Workspace" : "Editor & Response Workspace";
|
|
@@ -8664,7 +8665,7 @@ ${cssVarsBlock}
|
|
|
8664
8665
|
</style>
|
|
8665
8666
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
8666
8667
|
</head>
|
|
8667
|
-
<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}">
|
|
8668
8669
|
<header>
|
|
8669
8670
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
8670
8671
|
<div class="controls">
|
|
@@ -8674,7 +8675,7 @@ ${cssVarsBlock}
|
|
|
8674
8675
|
<label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
|
|
8675
8676
|
<button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
|
|
8676
8677
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
8677
|
-
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls.">Zen</button>
|
|
8678
|
+
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
|
|
8678
8679
|
</div>
|
|
8679
8680
|
</header>
|
|
8680
8681
|
|
|
@@ -8682,7 +8683,7 @@ ${cssVarsBlock}
|
|
|
8682
8683
|
<section id="leftPane">
|
|
8683
8684
|
<div id="leftSectionHeader" class="section-header">
|
|
8684
8685
|
<div class="section-header-main">
|
|
8685
|
-
<select id="editorViewSelect" aria-label="Editor view mode">
|
|
8686
|
+
<select id="editorViewSelect" aria-label="Editor view mode" title="Editor view mode. Shortcut: F7 when the editor pane is active; F6 switches panes.">
|
|
8686
8687
|
<option value="markdown" selected>Editor (Raw)</option>
|
|
8687
8688
|
<option value="preview">Editor (Preview)</option>
|
|
8688
8689
|
</select>
|
|
@@ -8857,7 +8858,7 @@ ${cssVarsBlock}
|
|
|
8857
8858
|
<section id="rightPane">
|
|
8858
8859
|
<div id="rightSectionHeader" class="section-header">
|
|
8859
8860
|
<div class="section-header-main">
|
|
8860
|
-
<select id="rightViewSelect" aria-label="Response view mode">
|
|
8861
|
+
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. Shortcut: F7 when the right pane is active; F6 switches panes.">
|
|
8861
8862
|
<option value="markdown">Response (Raw)</option>
|
|
8862
8863
|
<option value="preview" selected>Response (Preview)</option>
|
|
8863
8864
|
<option value="editor-preview">Editor (Preview)</option>
|
|
@@ -8928,9 +8929,50 @@ ${cssVarsBlock}
|
|
|
8928
8929
|
<footer>
|
|
8929
8930
|
<span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
|
|
8930
8931
|
<span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><span id="footerMetaModel" class="footer-meta-part footer-meta-model">${initialModel}</span><span class="footer-meta-sep">·</span><span id="footerMetaTerminal" class="footer-meta-part footer-meta-terminal">${initialTerminal}</span><span class="footer-meta-sep">·</span><span id="footerMetaContext" class="footer-meta-part footer-meta-context">unknown</span></span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
|
|
8931
|
-
<
|
|
8932
|
+
<button id="shortcutsBtn" class="shortcut-hint" type="button" title="Show Studio keyboard shortcuts. Press ? when not editing text.">Shortcuts (?)</button>
|
|
8932
8933
|
</footer>
|
|
8933
8934
|
|
|
8935
|
+
<div id="shortcutsOverlay" class="shortcuts-overlay" hidden>
|
|
8936
|
+
<div id="shortcutsDialog" class="shortcuts-dialog" role="dialog" aria-modal="true" aria-labelledby="shortcutsTitle">
|
|
8937
|
+
<div class="shortcuts-header">
|
|
8938
|
+
<div>
|
|
8939
|
+
<h2 id="shortcutsTitle">Keyboard shortcuts</h2>
|
|
8940
|
+
<p class="shortcuts-description">Studio navigation and high-frequency actions.</p>
|
|
8941
|
+
</div>
|
|
8942
|
+
<button id="shortcutsCloseBtn" class="shortcuts-close-btn" type="button" aria-label="Close keyboard shortcuts">Close</button>
|
|
8943
|
+
</div>
|
|
8944
|
+
<div class="shortcuts-body">
|
|
8945
|
+
<section class="shortcuts-group">
|
|
8946
|
+
<h3>Navigation</h3>
|
|
8947
|
+
<dl>
|
|
8948
|
+
<div><dt>F6</dt><dd>Switch between editor and right pane</dd></div>
|
|
8949
|
+
<div><dt>F7 / Shift+F7</dt><dd>Cycle the active pane's view</dd></div>
|
|
8950
|
+
<div><dt>F8</dt><dd>Focus editor text</dd></div>
|
|
8951
|
+
<div><dt>Shift+F8</dt><dd>Focus right-pane content</dd></div>
|
|
8952
|
+
<div><dt>F9</dt><dd>Toggle Zen mode</dd></div>
|
|
8953
|
+
<div><dt>F10</dt><dd>Focus or unfocus the active pane</dd></div>
|
|
8954
|
+
<div><dt>Esc</dt><dd>Close overlays, exit pane focus, or stop an active request</dd></div>
|
|
8955
|
+
<div><dt>?</dt><dd>Show keyboard shortcuts when not editing text</dd></div>
|
|
8956
|
+
</dl>
|
|
8957
|
+
</section>
|
|
8958
|
+
<section class="shortcuts-group">
|
|
8959
|
+
<h3>Editor</h3>
|
|
8960
|
+
<dl>
|
|
8961
|
+
<div><dt>Cmd/Ctrl+S</dt><dd>Save editor</dd></div>
|
|
8962
|
+
<div><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
8963
|
+
<div><dt>Tab / Shift+Tab</dt><dd>Indent or unindent selected editor text</dd></div>
|
|
8964
|
+
</dl>
|
|
8965
|
+
</section>
|
|
8966
|
+
<section class="shortcuts-group">
|
|
8967
|
+
<h3>REPL</h3>
|
|
8968
|
+
<dl>
|
|
8969
|
+
<div><dt>Cmd/Ctrl+Shift+Enter</dt><dd>Send selection, chunks, or editor text to the active REPL when the right pane is REPL</dd></div>
|
|
8970
|
+
</dl>
|
|
8971
|
+
</section>
|
|
8972
|
+
</div>
|
|
8973
|
+
</div>
|
|
8974
|
+
</div>
|
|
8975
|
+
|
|
8934
8976
|
<div id="scratchpadOverlay" class="scratchpad-overlay" hidden>
|
|
8935
8977
|
<div id="scratchpadDialog" class="scratchpad-dialog" role="dialog" aria-modal="true" aria-labelledby="scratchpadTitle">
|
|
8936
8978
|
<div class="scratchpad-header">
|
|
@@ -11302,6 +11344,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
11302
11344
|
return;
|
|
11303
11345
|
}
|
|
11304
11346
|
|
|
11347
|
+
if (isSshSession()) {
|
|
11348
|
+
respondJson(res, 409, { ok: false, error: "Server clipboard is disabled for SSH Studio sessions; use the browser clipboard." });
|
|
11349
|
+
return;
|
|
11350
|
+
}
|
|
11351
|
+
|
|
11305
11352
|
const result = await writeStudioSystemClipboard(text);
|
|
11306
11353
|
if (result.ok) {
|
|
11307
11354
|
respondJson(res, 200, { ok: true, method: result.method });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
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",
|