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 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
@@ -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
- try {
850
- await fetchStudioJson("/clipboard", {
851
- method: "POST",
852
- body: JSON.stringify({ text: content }),
853
- });
854
- return true;
855
- } catch {
856
- // Fall back to browser clipboard APIs. The server-side clipboard path
857
- // is most reliable for local Studio, but may be unavailable over SSH
858
- // or on systems without a clipboard command.
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
- text-align: right;
3333
- font-style: normal;
3334
- opacity: 0.86;
3335
- display: inline-flex;
3336
- align-items: center;
3337
- gap: 8px;
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
- <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Save editor: Cmd/Ctrl+S · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
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.8",
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",