pi-studio 0.5.36 → 0.5.38
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 +21 -0
- package/README.md +6 -1
- package/client/studio-client.js +282 -20
- package/client/studio.css +163 -0
- package/index.ts +288 -121
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.38] — 2026-03-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Studio now supports `/studio-editor-only` for opening additional editor/preview companion views from the same Pi session, without taking over the main full Studio workspace.
|
|
11
|
+
- Studio now supports `/studio-replace` for explicitly replacing the current full Studio view while leaving editor-only views open.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Full Studio is now treated as a singleton per Pi session: `/studio` opens the canonical full workspace, while attempts to open another full Studio now guide you toward `/studio-replace` or `/studio-editor-only` instead of silently invalidating the existing full Studio tab.
|
|
15
|
+
- README/help text and Studio status/warning messages now describe the updated full-vs-editor-only session model more clearly.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Editor-only Studio views now hide remaining critique/history controls, including the critique-focus dropdown, and keep their WebSocket mode metadata aligned with the server-side full/editor-only view tracking.
|
|
19
|
+
|
|
20
|
+
## [0.5.37] — 2026-03-29
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Studio now includes a local persistent scratchpad for parking quick thoughts while you work. The scratchpad opens as an integrated modal, keeps its contents after closing, and provides copy / clear / insert-into-editor actions.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Scratchpad UI text and actions now make the persistence semantics explicit: closing keeps the current notes unless you actively clear them.
|
|
27
|
+
|
|
7
28
|
## [0.5.36] — 2026-03-28
|
|
8
29
|
|
|
9
30
|
### Changed
|
package/README.md
CHANGED
|
@@ -15,7 +15,9 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
15
15
|
## What it does
|
|
16
16
|
|
|
17
17
|
- Opens a two-pane browser workspace: **Editor** (left) + **Response/Thinking/Editor Preview** (right)
|
|
18
|
+
- Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces
|
|
18
19
|
- Runs editor text directly, or asks for structured critique (auto/writing/code focus)
|
|
20
|
+
- Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them
|
|
19
21
|
- Browses response history (`Prev/Next/Last`) and loads either:
|
|
20
22
|
- response text
|
|
21
23
|
- critique notes/full critique
|
|
@@ -42,6 +44,8 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
42
44
|
| `/studio --status` | Show studio server status |
|
|
43
45
|
| `/studio --stop` | Stop studio server |
|
|
44
46
|
| `/studio --help` | Show help |
|
|
47
|
+
| `/studio-replace [path\|--blank\|--last]` | Replace the current full Studio view with a new full Studio view |
|
|
48
|
+
| `/studio-editor-only [path\|--blank\|--last]` | Open an editor-only Studio view; multiple editor-only views may be open at once |
|
|
45
49
|
| `/studio-current <path>` | Load a file into currently open Studio tab(s) without opening a new browser window |
|
|
46
50
|
| `/studio-pdf <path> [options]` | Export a local file to `<name>.studio.pdf` via the Studio PDF pipeline, with optional layout controls |
|
|
47
51
|
|
|
@@ -63,7 +67,8 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
63
67
|
|
|
64
68
|
## Notes
|
|
65
69
|
|
|
66
|
-
- Local-only server (`127.0.0.1`) with
|
|
70
|
+
- Local-only server (`127.0.0.1`) with tokenized Studio URLs.
|
|
71
|
+
- 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.
|
|
67
72
|
- Studio is designed as a complement to terminal pi, not a replacement.
|
|
68
73
|
- Editor/code font uses a best-effort terminal-monospace match when the current terminal config exposes it; set `PI_STUDIO_FONT_MONO` to force a specific CSS `font-family` stack.
|
|
69
74
|
- Full preview/PDF quality depends on `pandoc` (and `xelatex` for PDF):
|
package/client/studio-client.js
CHANGED
|
@@ -88,6 +88,21 @@
|
|
|
88
88
|
const compactBtn = document.getElementById("compactBtn");
|
|
89
89
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
90
90
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
91
|
+
const scratchpadBtn = document.getElementById("scratchpadBtn");
|
|
92
|
+
const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
|
|
93
|
+
const scratchpadDialogEl = document.getElementById("scratchpadDialog");
|
|
94
|
+
const scratchpadTextEl = document.getElementById("scratchpadText");
|
|
95
|
+
const scratchpadMetaEl = document.getElementById("scratchpadMeta");
|
|
96
|
+
const scratchpadInsertBtn = document.getElementById("scratchpadInsertBtn");
|
|
97
|
+
const scratchpadCopyBtn = document.getElementById("scratchpadCopyBtn");
|
|
98
|
+
const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
|
|
99
|
+
const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
|
|
100
|
+
const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
|
|
101
|
+
|
|
102
|
+
const studioMode = (document.body && document.body.dataset && document.body.dataset.studioMode) === "editor-only"
|
|
103
|
+
? "editor-only"
|
|
104
|
+
: "full";
|
|
105
|
+
const isEditorOnlyMode = studioMode === "editor-only";
|
|
91
106
|
|
|
92
107
|
const initialSourceState = {
|
|
93
108
|
source: (document.body && document.body.dataset && document.body.dataset.initialSource) || "blank",
|
|
@@ -105,9 +120,9 @@
|
|
|
105
120
|
let pendingKind = null;
|
|
106
121
|
let stickyStudioKind = null;
|
|
107
122
|
let initialDocumentApplied = false;
|
|
108
|
-
let editorView = "markdown";
|
|
109
|
-
let rightView = "preview";
|
|
110
|
-
let followLatest =
|
|
123
|
+
let editorView = isEditorOnlyMode ? "markdown" : "markdown";
|
|
124
|
+
let rightView = isEditorOnlyMode ? "editor-preview" : "preview";
|
|
125
|
+
let followLatest = !isEditorOnlyMode;
|
|
111
126
|
let queuedLatestResponse = null;
|
|
112
127
|
let latestResponseMarkdown = "";
|
|
113
128
|
let latestResponseThinking = "";
|
|
@@ -227,6 +242,7 @@
|
|
|
227
242
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
228
243
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
229
244
|
const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
|
|
245
|
+
const SCRATCHPAD_STORAGE_KEY = "piStudio.scratchpad";
|
|
230
246
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
231
247
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
232
248
|
const previewPendingTimers = new WeakMap();
|
|
@@ -241,6 +257,8 @@
|
|
|
241
257
|
let responseHighlightEnabled = false;
|
|
242
258
|
let editorHighlightRenderRaf = null;
|
|
243
259
|
let annotationsEnabled = true;
|
|
260
|
+
let scratchpadText = "";
|
|
261
|
+
let scratchpadReturnFocusEl = null;
|
|
244
262
|
const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
|
|
245
263
|
const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
|
|
246
264
|
if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
|
|
@@ -864,6 +882,28 @@
|
|
|
864
882
|
if (!event || event.defaultPrevented) return;
|
|
865
883
|
|
|
866
884
|
const key = typeof event.key === "string" ? event.key : "";
|
|
885
|
+
const plainEscape = key === "Escape"
|
|
886
|
+
&& !event.metaKey
|
|
887
|
+
&& !event.ctrlKey
|
|
888
|
+
&& !event.altKey
|
|
889
|
+
&& !event.shiftKey;
|
|
890
|
+
const scratchpadOwnsEvent = Boolean(
|
|
891
|
+
scratchpadDialogEl
|
|
892
|
+
&& event.target
|
|
893
|
+
&& typeof scratchpadDialogEl.contains === "function"
|
|
894
|
+
&& scratchpadDialogEl.contains(event.target)
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
if (isScratchpadOpen() && plainEscape) {
|
|
898
|
+
event.preventDefault();
|
|
899
|
+
closeScratchpad();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (scratchpadOwnsEvent) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
867
907
|
const isToggleShortcut =
|
|
868
908
|
(key === "Escape" && (event.metaKey || event.ctrlKey))
|
|
869
909
|
|| key === "F10";
|
|
@@ -874,13 +914,7 @@
|
|
|
874
914
|
return;
|
|
875
915
|
}
|
|
876
916
|
|
|
877
|
-
if (
|
|
878
|
-
key === "Escape"
|
|
879
|
-
&& !event.metaKey
|
|
880
|
-
&& !event.ctrlKey
|
|
881
|
-
&& !event.altKey
|
|
882
|
-
&& !event.shiftKey
|
|
883
|
-
) {
|
|
917
|
+
if (plainEscape) {
|
|
884
918
|
const activeKind = getAbortablePendingKind();
|
|
885
919
|
if (activeKind === "direct" || activeKind === "critique") {
|
|
886
920
|
event.preventDefault();
|
|
@@ -899,6 +933,7 @@
|
|
|
899
933
|
&& !event.altKey
|
|
900
934
|
&& !event.shiftKey
|
|
901
935
|
&& activePane === "left"
|
|
936
|
+
&& !isEditorOnlyMode
|
|
902
937
|
) {
|
|
903
938
|
if (queueSteerBtn && !queueSteerBtn.disabled) {
|
|
904
939
|
event.preventDefault();
|
|
@@ -2503,7 +2538,7 @@
|
|
|
2503
2538
|
fileInput.disabled = uiBusy;
|
|
2504
2539
|
saveAsBtn.disabled = uiBusy;
|
|
2505
2540
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
2506
|
-
sendEditorBtn.disabled = uiBusy;
|
|
2541
|
+
sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
2507
2542
|
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
2508
2543
|
if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
|
|
2509
2544
|
syncRunAndCritiqueButtons();
|
|
@@ -2513,13 +2548,13 @@
|
|
|
2513
2548
|
if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
|
|
2514
2549
|
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
|
|
2515
2550
|
if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
2516
|
-
if (compactBtn) compactBtn.disabled = uiBusy || compactInProgress || wsState === "Disconnected";
|
|
2517
|
-
editorViewSelect.disabled =
|
|
2518
|
-
rightViewSelect.disabled =
|
|
2519
|
-
followSelect.disabled = uiBusy;
|
|
2520
|
-
if (responseHighlightSelect) responseHighlightSelect.disabled = rightView !== "markdown";
|
|
2521
|
-
insertHeaderBtn.disabled = uiBusy;
|
|
2522
|
-
lensSelect.disabled = uiBusy;
|
|
2551
|
+
if (compactBtn) compactBtn.disabled = isEditorOnlyMode || uiBusy || compactInProgress || wsState === "Disconnected";
|
|
2552
|
+
editorViewSelect.disabled = isEditorOnlyMode;
|
|
2553
|
+
rightViewSelect.disabled = isEditorOnlyMode;
|
|
2554
|
+
followSelect.disabled = isEditorOnlyMode || uiBusy;
|
|
2555
|
+
if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
|
|
2556
|
+
insertHeaderBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
2557
|
+
lensSelect.disabled = uiBusy || isEditorOnlyMode;
|
|
2523
2558
|
updateSaveFileTooltip();
|
|
2524
2559
|
updateHistoryControls();
|
|
2525
2560
|
updateResultActionButtons();
|
|
@@ -3209,6 +3244,126 @@
|
|
|
3209
3244
|
persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
|
|
3210
3245
|
}
|
|
3211
3246
|
|
|
3247
|
+
function readStoredText(storageKey) {
|
|
3248
|
+
if (!window.localStorage) return null;
|
|
3249
|
+
try {
|
|
3250
|
+
const value = window.localStorage.getItem(storageKey);
|
|
3251
|
+
return typeof value === "string" ? value : null;
|
|
3252
|
+
} catch {
|
|
3253
|
+
return null;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
function persistStoredText(storageKey, value) {
|
|
3258
|
+
if (!window.localStorage) return;
|
|
3259
|
+
try {
|
|
3260
|
+
window.localStorage.setItem(storageKey, String(value ?? ""));
|
|
3261
|
+
} catch {
|
|
3262
|
+
// ignore storage failures
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
function isScratchpadOpen() {
|
|
3267
|
+
return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
function readStoredScratchpadText() {
|
|
3271
|
+
return readStoredText(SCRATCHPAD_STORAGE_KEY);
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
function persistScratchpadText(value) {
|
|
3275
|
+
persistStoredText(SCRATCHPAD_STORAGE_KEY, value);
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function updateScratchpadUi() {
|
|
3279
|
+
const normalized = String(scratchpadText || "");
|
|
3280
|
+
const hasContent = Boolean(normalized.trim());
|
|
3281
|
+
if (scratchpadBtn) {
|
|
3282
|
+
scratchpadBtn.textContent = hasContent ? "Scratchpad •" : "Scratchpad";
|
|
3283
|
+
scratchpadBtn.classList.toggle("has-content", hasContent);
|
|
3284
|
+
scratchpadBtn.title = hasContent
|
|
3285
|
+
? "Open your local persistent scratchpad. Current notes persist after closing until you edit or clear them."
|
|
3286
|
+
: "Open a local persistent scratchpad for quick notes. Anything you type will persist after closing until you edit or clear it.";
|
|
3287
|
+
}
|
|
3288
|
+
if (scratchpadMetaEl) {
|
|
3289
|
+
scratchpadMetaEl.textContent = hasContent
|
|
3290
|
+
? "Saved locally · persists after close · " + normalized.length + " chars"
|
|
3291
|
+
: "Empty · local only";
|
|
3292
|
+
}
|
|
3293
|
+
if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
|
|
3294
|
+
if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
|
|
3295
|
+
if (scratchpadClearBtn) scratchpadClearBtn.disabled = !normalized.length;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
function setScratchpadText(nextText, options) {
|
|
3299
|
+
scratchpadText = String(nextText || "");
|
|
3300
|
+
if (scratchpadTextEl && scratchpadTextEl.value !== scratchpadText) {
|
|
3301
|
+
scratchpadTextEl.value = scratchpadText;
|
|
3302
|
+
}
|
|
3303
|
+
if (!options || options.persist !== false) {
|
|
3304
|
+
persistScratchpadText(scratchpadText);
|
|
3305
|
+
}
|
|
3306
|
+
updateScratchpadUi();
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function closeScratchpad(options) {
|
|
3310
|
+
if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
|
|
3311
|
+
scratchpadOverlayEl.hidden = true;
|
|
3312
|
+
document.body.classList.remove("scratchpad-open");
|
|
3313
|
+
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
3314
|
+
? options.focusTarget
|
|
3315
|
+
: (scratchpadReturnFocusEl || scratchpadBtn || sourceTextEl);
|
|
3316
|
+
scratchpadReturnFocusEl = null;
|
|
3317
|
+
if (focusTarget && typeof focusTarget.focus === "function") {
|
|
3318
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3319
|
+
? window.requestAnimationFrame.bind(window)
|
|
3320
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
3321
|
+
schedule(() => focusTarget.focus());
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
function openScratchpad() {
|
|
3326
|
+
if (!scratchpadOverlayEl) return;
|
|
3327
|
+
scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
3328
|
+
? document.activeElement
|
|
3329
|
+
: sourceTextEl;
|
|
3330
|
+
scratchpadOverlayEl.hidden = false;
|
|
3331
|
+
document.body.classList.add("scratchpad-open");
|
|
3332
|
+
if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
|
|
3333
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3334
|
+
? window.requestAnimationFrame.bind(window)
|
|
3335
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
3336
|
+
schedule(() => {
|
|
3337
|
+
scratchpadTextEl.focus();
|
|
3338
|
+
if (typeof scratchpadTextEl.selectionStart === "number") {
|
|
3339
|
+
const end = scratchpadTextEl.value.length;
|
|
3340
|
+
scratchpadTextEl.setSelectionRange(end, end);
|
|
3341
|
+
}
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
function insertScratchpadIntoEditor() {
|
|
3347
|
+
const content = String(scratchpadText || "");
|
|
3348
|
+
if (!content.trim()) {
|
|
3349
|
+
setStatus("Scratchpad is empty.", "warning");
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
const current = sourceTextEl.value || "";
|
|
3354
|
+
const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : current.length;
|
|
3355
|
+
const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
|
|
3356
|
+
const safeStart = Math.max(0, Math.min(start, current.length));
|
|
3357
|
+
const safeEnd = Math.max(safeStart, Math.min(end, current.length));
|
|
3358
|
+
const next = current.slice(0, safeStart) + content + current.slice(safeEnd);
|
|
3359
|
+
setEditorText(next, { preserveScroll: false, preserveSelection: false });
|
|
3360
|
+
const caret = safeStart + content.length;
|
|
3361
|
+
sourceTextEl.setSelectionRange(caret, caret);
|
|
3362
|
+
setActivePane("left");
|
|
3363
|
+
closeScratchpad({ focusTarget: sourceTextEl });
|
|
3364
|
+
setStatus("Inserted scratchpad into editor.", "success");
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3212
3367
|
function updateEditorHighlightState() {
|
|
3213
3368
|
const enabled = editorHighlightEnabled && editorView === "markdown";
|
|
3214
3369
|
|
|
@@ -3323,6 +3478,28 @@
|
|
|
3323
3478
|
const critiqueIsStop = activeKind === "critique";
|
|
3324
3479
|
const canQueueSteering = studioRunChainActive && !critiqueIsStop;
|
|
3325
3480
|
|
|
3481
|
+
if (isEditorOnlyMode) {
|
|
3482
|
+
if (sendRunBtn) {
|
|
3483
|
+
sendRunBtn.textContent = "Run editor text";
|
|
3484
|
+
sendRunBtn.classList.remove("request-stop-active");
|
|
3485
|
+
sendRunBtn.disabled = true;
|
|
3486
|
+
sendRunBtn.title = "Run is unavailable in editor-only mode.";
|
|
3487
|
+
}
|
|
3488
|
+
if (queueSteerBtn) {
|
|
3489
|
+
queueSteerBtn.hidden = false;
|
|
3490
|
+
queueSteerBtn.disabled = true;
|
|
3491
|
+
queueSteerBtn.classList.remove("request-stop-active");
|
|
3492
|
+
queueSteerBtn.title = "Queue steering is unavailable in editor-only mode.";
|
|
3493
|
+
}
|
|
3494
|
+
if (critiqueBtn) {
|
|
3495
|
+
critiqueBtn.textContent = "Critique editor text";
|
|
3496
|
+
critiqueBtn.classList.remove("request-stop-active");
|
|
3497
|
+
critiqueBtn.disabled = true;
|
|
3498
|
+
critiqueBtn.title = "Critique is unavailable in editor-only mode.";
|
|
3499
|
+
}
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3326
3503
|
if (sendRunBtn) {
|
|
3327
3504
|
sendRunBtn.textContent = directIsStop ? "Stop" : "Run editor text";
|
|
3328
3505
|
sendRunBtn.classList.toggle("request-stop-active", directIsStop);
|
|
@@ -4025,7 +4202,14 @@
|
|
|
4025
4202
|
}
|
|
4026
4203
|
|
|
4027
4204
|
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
4028
|
-
const
|
|
4205
|
+
const wsParams = new URLSearchParams({ token: token });
|
|
4206
|
+
if (studioMode !== "full") {
|
|
4207
|
+
wsParams.set("mode", studioMode);
|
|
4208
|
+
}
|
|
4209
|
+
if (DEBUG_ENABLED) {
|
|
4210
|
+
wsParams.set("debug", "1");
|
|
4211
|
+
}
|
|
4212
|
+
const wsUrl = wsProtocol + "://" + window.location.host + "/ws?" + wsParams.toString();
|
|
4029
4213
|
const wasReconnect = reconnectAttempt > 0;
|
|
4030
4214
|
let disconnectHandled = false;
|
|
4031
4215
|
|
|
@@ -4054,7 +4238,15 @@
|
|
|
4054
4238
|
clearScheduledReconnect();
|
|
4055
4239
|
reconnectAttempt = 0;
|
|
4056
4240
|
setWsState("Disconnected");
|
|
4057
|
-
setStatus("This tab was
|
|
4241
|
+
setStatus("This full Studio tab was replaced by a newer Studio session.", "warning");
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
if (kind === "full_conflict") {
|
|
4246
|
+
clearScheduledReconnect();
|
|
4247
|
+
reconnectAttempt = 0;
|
|
4248
|
+
setWsState("Disconnected");
|
|
4249
|
+
setStatus("Another full Studio view is already active for this session. Use /studio-replace for a fresh full Studio view, or /studio-editor-only for a concurrent editor-only Studio view.", "warning");
|
|
4058
4250
|
return;
|
|
4059
4251
|
}
|
|
4060
4252
|
|
|
@@ -4095,6 +4287,10 @@
|
|
|
4095
4287
|
handleDisconnect("invalidated", 4001);
|
|
4096
4288
|
return;
|
|
4097
4289
|
}
|
|
4290
|
+
if (event && event.code === 4004) {
|
|
4291
|
+
handleDisconnect("full_conflict", 4004);
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4098
4294
|
if (event && event.code === 1001) {
|
|
4099
4295
|
handleDisconnect("shutdown", 1001);
|
|
4100
4296
|
return;
|
|
@@ -4715,6 +4911,71 @@
|
|
|
4715
4911
|
}
|
|
4716
4912
|
});
|
|
4717
4913
|
|
|
4914
|
+
if (scratchpadBtn) {
|
|
4915
|
+
scratchpadBtn.addEventListener("click", () => {
|
|
4916
|
+
openScratchpad();
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
if (scratchpadCloseBtn) {
|
|
4921
|
+
scratchpadCloseBtn.addEventListener("click", () => {
|
|
4922
|
+
closeScratchpad();
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
if (scratchpadDoneBtn) {
|
|
4927
|
+
scratchpadDoneBtn.addEventListener("click", () => {
|
|
4928
|
+
closeScratchpad();
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4932
|
+
if (scratchpadOverlayEl) {
|
|
4933
|
+
scratchpadOverlayEl.addEventListener("click", (event) => {
|
|
4934
|
+
if (event.target === scratchpadOverlayEl) {
|
|
4935
|
+
closeScratchpad();
|
|
4936
|
+
}
|
|
4937
|
+
});
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4940
|
+
if (scratchpadTextEl) {
|
|
4941
|
+
scratchpadTextEl.addEventListener("input", () => {
|
|
4942
|
+
setScratchpadText(scratchpadTextEl.value);
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
if (scratchpadInsertBtn) {
|
|
4947
|
+
scratchpadInsertBtn.addEventListener("click", () => {
|
|
4948
|
+
insertScratchpadIntoEditor();
|
|
4949
|
+
});
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
if (scratchpadCopyBtn) {
|
|
4953
|
+
scratchpadCopyBtn.addEventListener("click", async () => {
|
|
4954
|
+
if (!String(scratchpadText || "").trim()) {
|
|
4955
|
+
setStatus("Scratchpad is empty.", "warning");
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
try {
|
|
4960
|
+
await navigator.clipboard.writeText(String(scratchpadText || ""));
|
|
4961
|
+
setStatus("Copied scratchpad text.", "success");
|
|
4962
|
+
} catch (error) {
|
|
4963
|
+
setStatus("Clipboard write failed.", "warning");
|
|
4964
|
+
}
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
if (scratchpadClearBtn) {
|
|
4969
|
+
scratchpadClearBtn.addEventListener("click", () => {
|
|
4970
|
+
if (!String(scratchpadText || "").length) return;
|
|
4971
|
+
const confirmed = window.confirm("Clear scratchpad text?");
|
|
4972
|
+
if (!confirmed) return;
|
|
4973
|
+
setScratchpadText("");
|
|
4974
|
+
if (scratchpadTextEl) scratchpadTextEl.focus();
|
|
4975
|
+
setStatus("Cleared scratchpad.", "success");
|
|
4976
|
+
});
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4718
4979
|
if (saveAnnotatedBtn) {
|
|
4719
4980
|
saveAnnotatedBtn.addEventListener("click", () => {
|
|
4720
4981
|
const content = sourceTextEl.value;
|
|
@@ -4853,6 +5114,7 @@
|
|
|
4853
5114
|
refreshResponseUi();
|
|
4854
5115
|
updateAnnotatedReplyHeaderButton();
|
|
4855
5116
|
setActivePane("left");
|
|
5117
|
+
setScratchpadText(readStoredScratchpadText() || "", { persist: false });
|
|
4856
5118
|
|
|
4857
5119
|
const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
|
|
4858
5120
|
const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value === "on");
|
package/client/studio.css
CHANGED
|
@@ -220,6 +220,12 @@
|
|
|
220
220
|
filter: brightness(0.95);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
#scratchpadBtn.has-content {
|
|
224
|
+
border-color: var(--accent);
|
|
225
|
+
color: var(--accent);
|
|
226
|
+
font-weight: 600;
|
|
227
|
+
}
|
|
228
|
+
|
|
223
229
|
.section-header select {
|
|
224
230
|
font-weight: 600;
|
|
225
231
|
font-size: 14px;
|
|
@@ -234,6 +240,44 @@
|
|
|
234
240
|
appearance: menulist;
|
|
235
241
|
}
|
|
236
242
|
|
|
243
|
+
body[data-studio-mode="editor-only"] #editorViewSelect,
|
|
244
|
+
body[data-studio-mode="editor-only"] #rightViewSelect,
|
|
245
|
+
body[data-studio-mode="editor-only"] #sendRunBtn,
|
|
246
|
+
body[data-studio-mode="editor-only"] #queueSteerBtn,
|
|
247
|
+
body[data-studio-mode="editor-only"] #sendEditorBtn,
|
|
248
|
+
body[data-studio-mode="editor-only"] #insertHeaderBtn,
|
|
249
|
+
body[data-studio-mode="editor-only"] #lensSelect,
|
|
250
|
+
body[data-studio-mode="editor-only"] #critiqueBtn,
|
|
251
|
+
body[data-studio-mode="editor-only"] #followSelect,
|
|
252
|
+
body[data-studio-mode="editor-only"] #responseHighlightSelect,
|
|
253
|
+
body[data-studio-mode="editor-only"] #pullLatestBtn,
|
|
254
|
+
body[data-studio-mode="editor-only"] #historyPrevBtn,
|
|
255
|
+
body[data-studio-mode="editor-only"] #historyIndexBadge,
|
|
256
|
+
body[data-studio-mode="editor-only"] #historyNextBtn,
|
|
257
|
+
body[data-studio-mode="editor-only"] #historyLastBtn,
|
|
258
|
+
body[data-studio-mode="editor-only"] #loadResponseBtn,
|
|
259
|
+
body[data-studio-mode="editor-only"] #loadCritiqueNotesBtn,
|
|
260
|
+
body[data-studio-mode="editor-only"] #loadCritiqueFullBtn,
|
|
261
|
+
body[data-studio-mode="editor-only"] #loadHistoryPromptBtn,
|
|
262
|
+
body[data-studio-mode="editor-only"] #copyResponseBtn,
|
|
263
|
+
body[data-studio-mode="editor-only"] #compactBtn,
|
|
264
|
+
body[data-studio-mode="editor-only"] .reference-meta,
|
|
265
|
+
body[data-studio-mode="editor-only"] #responseActions {
|
|
266
|
+
display: none !important;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
body[data-studio-mode="editor-only"] #leftSectionHeader .section-header-main::before {
|
|
270
|
+
content: "Editor";
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
font-size: 14px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
body[data-studio-mode="editor-only"] #rightSectionHeader .section-header-main::before {
|
|
276
|
+
content: "Preview";
|
|
277
|
+
font-weight: 600;
|
|
278
|
+
font-size: 14px;
|
|
279
|
+
}
|
|
280
|
+
|
|
237
281
|
.reference-meta {
|
|
238
282
|
padding: 8px 10px;
|
|
239
283
|
border-bottom: 1px solid var(--border-muted);
|
|
@@ -1339,6 +1383,125 @@
|
|
|
1339
1383
|
background: var(--panel);
|
|
1340
1384
|
}
|
|
1341
1385
|
|
|
1386
|
+
body.scratchpad-open {
|
|
1387
|
+
overflow: hidden;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
.scratchpad-overlay {
|
|
1391
|
+
position: fixed;
|
|
1392
|
+
inset: 0;
|
|
1393
|
+
z-index: 50;
|
|
1394
|
+
display: flex;
|
|
1395
|
+
align-items: center;
|
|
1396
|
+
justify-content: center;
|
|
1397
|
+
padding: 24px;
|
|
1398
|
+
background: rgba(0, 0, 0, 0.48);
|
|
1399
|
+
backdrop-filter: blur(2px);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
.scratchpad-overlay[hidden] {
|
|
1403
|
+
display: none !important;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
.scratchpad-dialog {
|
|
1407
|
+
width: min(860px, 100%);
|
|
1408
|
+
max-height: min(82vh, 900px);
|
|
1409
|
+
border: 1px solid var(--border);
|
|
1410
|
+
border-radius: 14px;
|
|
1411
|
+
background: var(--panel);
|
|
1412
|
+
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
|
|
1413
|
+
display: flex;
|
|
1414
|
+
flex-direction: column;
|
|
1415
|
+
overflow: hidden;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.scratchpad-header {
|
|
1419
|
+
display: flex;
|
|
1420
|
+
align-items: flex-start;
|
|
1421
|
+
justify-content: space-between;
|
|
1422
|
+
gap: 12px;
|
|
1423
|
+
padding: 16px 18px 12px;
|
|
1424
|
+
border-bottom: 1px solid var(--border-muted);
|
|
1425
|
+
background: var(--panel-2);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
.scratchpad-header > div {
|
|
1429
|
+
flex: 1 1 auto;
|
|
1430
|
+
min-width: 0;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.scratchpad-header h2 {
|
|
1434
|
+
margin: 0;
|
|
1435
|
+
font-size: 17px;
|
|
1436
|
+
font-weight: 600;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.scratchpad-description {
|
|
1440
|
+
margin: 6px 0 0;
|
|
1441
|
+
font-size: 12px;
|
|
1442
|
+
line-height: 1.45;
|
|
1443
|
+
color: var(--muted);
|
|
1444
|
+
max-width: none;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
.scratchpad-close-btn {
|
|
1448
|
+
padding: 6px 10px;
|
|
1449
|
+
line-height: 1;
|
|
1450
|
+
flex: 0 0 auto;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.scratchpad-textarea {
|
|
1454
|
+
width: 100%;
|
|
1455
|
+
min-height: 280px;
|
|
1456
|
+
flex: 1 1 auto;
|
|
1457
|
+
border: 0;
|
|
1458
|
+
border-bottom: 1px solid var(--border-muted);
|
|
1459
|
+
border-radius: 0;
|
|
1460
|
+
margin: 0;
|
|
1461
|
+
padding: 16px 18px;
|
|
1462
|
+
background: var(--panel);
|
|
1463
|
+
color: var(--text);
|
|
1464
|
+
font-family: var(--font-mono);
|
|
1465
|
+
font-size: 13px;
|
|
1466
|
+
line-height: 1.55;
|
|
1467
|
+
resize: vertical;
|
|
1468
|
+
outline: none;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.scratchpad-footer {
|
|
1472
|
+
display: flex;
|
|
1473
|
+
align-items: center;
|
|
1474
|
+
justify-content: space-between;
|
|
1475
|
+
gap: 12px;
|
|
1476
|
+
flex-wrap: wrap;
|
|
1477
|
+
padding: 12px 18px 16px;
|
|
1478
|
+
background: var(--panel);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.scratchpad-meta {
|
|
1482
|
+
font-size: 12px;
|
|
1483
|
+
color: var(--muted);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.scratchpad-actions {
|
|
1487
|
+
display: inline-flex;
|
|
1488
|
+
align-items: center;
|
|
1489
|
+
gap: 8px;
|
|
1490
|
+
flex-wrap: wrap;
|
|
1491
|
+
justify-content: flex-end;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
#scratchpadDoneBtn:not(:disabled) {
|
|
1495
|
+
background: var(--accent);
|
|
1496
|
+
border-color: var(--accent);
|
|
1497
|
+
color: var(--accent-contrast);
|
|
1498
|
+
font-weight: 600;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
#scratchpadDoneBtn:not(:disabled):hover {
|
|
1502
|
+
filter: brightness(0.95);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1342
1505
|
#status.error { color: var(--error); }
|
|
1343
1506
|
#status.warning { color: var(--warn); }
|
|
1344
1507
|
#status.success { color: var(--ok); }
|
package/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js"
|
|
|
22
22
|
type Lens = "writing" | "code";
|
|
23
23
|
type RequestedLens = Lens | "auto";
|
|
24
24
|
type StudioRequestKind = "critique" | "annotation" | "direct" | "compact";
|
|
25
|
+
type StudioUiMode = "full" | "editor-only";
|
|
25
26
|
type StudioSourceKind = "file" | "last-response" | "blank";
|
|
26
27
|
type TerminalActivityPhase = "idle" | "running" | "tool" | "responding";
|
|
27
28
|
type StudioPromptMode = "response" | "run" | "effective";
|
|
@@ -35,6 +36,7 @@ interface StudioServerState {
|
|
|
35
36
|
server: Server;
|
|
36
37
|
wsServer: WebSocketServer;
|
|
37
38
|
clients: Set<WebSocket>;
|
|
39
|
+
clientModes: Map<WebSocket, StudioUiMode>;
|
|
38
40
|
port: number;
|
|
39
41
|
token: string;
|
|
40
42
|
}
|
|
@@ -5511,9 +5513,14 @@ function isAllowedOrigin(_origin: string | undefined, _port: number): boolean {
|
|
|
5511
5513
|
return true;
|
|
5512
5514
|
}
|
|
5513
5515
|
|
|
5514
|
-
function
|
|
5515
|
-
|
|
5516
|
-
|
|
5516
|
+
function normalizeStudioUiMode(raw: string | null | undefined): StudioUiMode {
|
|
5517
|
+
return raw === "editor-only" ? "editor-only" : "full";
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
function buildStudioUrl(port: number, token: string, mode: StudioUiMode = "full"): string {
|
|
5521
|
+
const params = new URLSearchParams({ token });
|
|
5522
|
+
if (mode !== "full") params.set("mode", mode);
|
|
5523
|
+
return `http://127.0.0.1:${port}/?${params.toString()}`;
|
|
5517
5524
|
}
|
|
5518
5525
|
|
|
5519
5526
|
function formatModelLabel(model: { provider?: string; id?: string } | undefined): string {
|
|
@@ -5647,6 +5654,7 @@ function buildStudioHtml(
|
|
|
5647
5654
|
initialModelLabel?: string,
|
|
5648
5655
|
initialTerminalLabel?: string,
|
|
5649
5656
|
initialContextUsage?: StudioContextUsageSnapshot,
|
|
5657
|
+
studioMode: StudioUiMode = "full",
|
|
5650
5658
|
): string {
|
|
5651
5659
|
const initialText = escapeHtmlForInline(initialDocument?.text ?? "");
|
|
5652
5660
|
const initialSource = initialDocument?.source ?? "blank";
|
|
@@ -5702,13 +5710,16 @@ function buildStudioHtml(
|
|
|
5702
5710
|
const clientScriptHref = `/studio-client.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
5703
5711
|
const faviconHref = buildStudioFaviconDataUri(style);
|
|
5704
5712
|
const bootConfigJson = JSON.stringify({ mermaidConfig }).replace(/</g, "\\u003c");
|
|
5713
|
+
const isEditorOnlyMode = studioMode === "editor-only";
|
|
5714
|
+
const appTitle = isEditorOnlyMode ? "π Studio — Editor" : "π Studio";
|
|
5715
|
+
const appSubtitle = isEditorOnlyMode ? "Editor Workspace" : "Editor & Response Workspace";
|
|
5705
5716
|
|
|
5706
5717
|
return `<!doctype html>
|
|
5707
5718
|
<html>
|
|
5708
5719
|
<head>
|
|
5709
5720
|
<meta charset="utf-8" />
|
|
5710
5721
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
5711
|
-
<title
|
|
5722
|
+
<title>${appTitle}</title>
|
|
5712
5723
|
<link rel="icon" href="${faviconHref}" type="image/svg+xml" />
|
|
5713
5724
|
<style>
|
|
5714
5725
|
:root {
|
|
@@ -5717,9 +5728,9 @@ ${cssVarsBlock}
|
|
|
5717
5728
|
</style>
|
|
5718
5729
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
5719
5730
|
</head>
|
|
5720
|
-
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}">
|
|
5731
|
+
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}">
|
|
5721
5732
|
<header>
|
|
5722
|
-
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle"
|
|
5733
|
+
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
5723
5734
|
<div class="controls">
|
|
5724
5735
|
<button id="saveAsBtn" type="button" title="Save editor content to a new file path.">Save editor as…</button>
|
|
5725
5736
|
<button id="saveOverBtn" type="button" title="Overwrite current file with editor content." disabled>Save editor</button>
|
|
@@ -5740,6 +5751,7 @@ ${cssVarsBlock}
|
|
|
5740
5751
|
</div>
|
|
5741
5752
|
<div class="section-header-actions">
|
|
5742
5753
|
<button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
|
|
5754
|
+
<button id="scratchpadBtn" type="button" title="Open a local persistent scratchpad for quick notes. Scratchpad text is never run, critiqued, or exported unless you explicitly insert it into the editor.">Scratchpad</button>
|
|
5743
5755
|
</div>
|
|
5744
5756
|
</div>
|
|
5745
5757
|
<div class="source-wrap">
|
|
@@ -5875,6 +5887,28 @@ ${cssVarsBlock}
|
|
|
5875
5887
|
<span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
|
|
5876
5888
|
</footer>
|
|
5877
5889
|
|
|
5890
|
+
<div id="scratchpadOverlay" class="scratchpad-overlay" hidden>
|
|
5891
|
+
<div id="scratchpadDialog" class="scratchpad-dialog" role="dialog" aria-modal="true" aria-labelledby="scratchpadTitle">
|
|
5892
|
+
<div class="scratchpad-header">
|
|
5893
|
+
<div>
|
|
5894
|
+
<h2 id="scratchpadTitle">Scratchpad</h2>
|
|
5895
|
+
<p class="scratchpad-description">Local persistent notes for thoughts you want to park while working. Closing the scratchpad does not clear it: notes persist locally until you edit or clear them. Scratchpad text is not run, critiqued, sent, or exported unless you explicitly insert it into the editor.</p>
|
|
5896
|
+
</div>
|
|
5897
|
+
<button id="scratchpadCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Keep current scratchpad text and close scratchpad" title="Keep current scratchpad text and close scratchpad">✕</button>
|
|
5898
|
+
</div>
|
|
5899
|
+
<textarea id="scratchpadText" class="scratchpad-textarea" placeholder="Jot quick thoughts, TODOs, or prompt ideas here..."></textarea>
|
|
5900
|
+
<div class="scratchpad-footer">
|
|
5901
|
+
<span id="scratchpadMeta" class="scratchpad-meta">Empty · local only</span>
|
|
5902
|
+
<div class="scratchpad-actions">
|
|
5903
|
+
<button id="scratchpadInsertBtn" type="button" title="Insert the scratchpad text into the editor at the current selection, or append it if no editor selection is available.">Insert into editor</button>
|
|
5904
|
+
<button id="scratchpadCopyBtn" type="button" title="Copy scratchpad text to the clipboard.">Copy</button>
|
|
5905
|
+
<button id="scratchpadClearBtn" type="button" title="Clear scratchpad text.">Clear</button>
|
|
5906
|
+
<button id="scratchpadDoneBtn" type="button" title="Keep the current scratchpad text and close the scratchpad.">Keep and close</button>
|
|
5907
|
+
</div>
|
|
5908
|
+
</div>
|
|
5909
|
+
</div>
|
|
5910
|
+
</div>
|
|
5911
|
+
|
|
5878
5912
|
<!-- Defer sanitizer script so studio can boot/connect even if CDN is slow or blocked. -->
|
|
5879
5913
|
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
|
|
5880
5914
|
<script>
|
|
@@ -5926,6 +5960,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
5926
5960
|
|
|
5927
5961
|
const isStudioDirectRunChainActive = () => Boolean(studioDirectRunChain);
|
|
5928
5962
|
const getQueuedStudioSteeringCount = () => queuedStudioDirectRequests.length;
|
|
5963
|
+
const getStudioClientCounts = (): { full: number; editorOnly: number } => {
|
|
5964
|
+
if (!serverState) return { full: 0, editorOnly: 0 };
|
|
5965
|
+
let full = 0;
|
|
5966
|
+
let editorOnly = 0;
|
|
5967
|
+
for (const client of serverState.clients) {
|
|
5968
|
+
if (client.readyState !== WebSocket.OPEN) continue;
|
|
5969
|
+
const mode = serverState.clientModes.get(client) ?? "full";
|
|
5970
|
+
if (mode === "editor-only") {
|
|
5971
|
+
editorOnly += 1;
|
|
5972
|
+
} else {
|
|
5973
|
+
full += 1;
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
return { full, editorOnly };
|
|
5977
|
+
};
|
|
5978
|
+
const hasConnectedFullStudioView = () => getStudioClientCounts().full > 0;
|
|
5929
5979
|
const canQueueStudioSteeringRequest = () => {
|
|
5930
5980
|
if (compactInProgress) return false;
|
|
5931
5981
|
if (!agentBusy) return false;
|
|
@@ -6641,6 +6691,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
6641
6691
|
}
|
|
6642
6692
|
}
|
|
6643
6693
|
serverState.clients.clear();
|
|
6694
|
+
serverState.clientModes.clear();
|
|
6695
|
+
};
|
|
6696
|
+
|
|
6697
|
+
const closeStudioClientsByMode = (mode: StudioUiMode, code = 4001, reason = "Session invalidated"): number => {
|
|
6698
|
+
if (!serverState) return 0;
|
|
6699
|
+
let closed = 0;
|
|
6700
|
+
for (const client of Array.from(serverState.clients)) {
|
|
6701
|
+
if (client.readyState !== WebSocket.OPEN) continue;
|
|
6702
|
+
const clientMode = serverState.clientModes.get(client) ?? "full";
|
|
6703
|
+
if (clientMode !== mode) continue;
|
|
6704
|
+
serverState.clients.delete(client);
|
|
6705
|
+
serverState.clientModes.delete(client);
|
|
6706
|
+
closed += 1;
|
|
6707
|
+
try {
|
|
6708
|
+
client.close(code, reason);
|
|
6709
|
+
} catch {
|
|
6710
|
+
// Ignore close errors
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
return closed;
|
|
6644
6714
|
};
|
|
6645
6715
|
|
|
6646
6716
|
const handleStudioMessage = (client: WebSocket, msg: IncomingStudioMessage) => {
|
|
@@ -7537,7 +7607,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7537
7607
|
"Cross-Origin-Resource-Policy": "same-origin",
|
|
7538
7608
|
});
|
|
7539
7609
|
refreshContextUsage();
|
|
7540
|
-
|
|
7610
|
+
const studioMode = normalizeStudioUiMode(requestUrl.searchParams.get("mode"));
|
|
7611
|
+
res.end(buildStudioHtml(initialStudioDocument, serverState.token, lastCommandCtx?.ui.theme, currentModelLabel, terminalSessionLabel, contextUsageSnapshot, studioMode));
|
|
7541
7612
|
};
|
|
7542
7613
|
|
|
7543
7614
|
const ensureServer = async (): Promise<StudioServerState> => {
|
|
@@ -7546,11 +7617,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
7546
7617
|
const server = createServer(handleHttpRequest);
|
|
7547
7618
|
const wsServer = new WebSocketServer({ noServer: true });
|
|
7548
7619
|
const clients = new Set<WebSocket>();
|
|
7620
|
+
const clientModes = new Map<WebSocket, StudioUiMode>();
|
|
7549
7621
|
|
|
7550
7622
|
const state: StudioServerState = {
|
|
7551
7623
|
server,
|
|
7552
7624
|
wsServer,
|
|
7553
7625
|
clients,
|
|
7626
|
+
clientModes,
|
|
7554
7627
|
port: 0,
|
|
7555
7628
|
token: createSessionToken(),
|
|
7556
7629
|
};
|
|
@@ -7583,9 +7656,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
7583
7656
|
});
|
|
7584
7657
|
});
|
|
7585
7658
|
|
|
7586
|
-
wsServer.on("connection", (ws) => {
|
|
7659
|
+
wsServer.on("connection", (ws, req) => {
|
|
7660
|
+
const host = req.headers.host ?? `127.0.0.1:${state.port}`;
|
|
7661
|
+
const requestUrl = new URL(req.url ?? "/ws", `http://${host}`);
|
|
7662
|
+
const clientMode = normalizeStudioUiMode(requestUrl.searchParams.get("mode"));
|
|
7663
|
+
if (clientMode === "full") {
|
|
7664
|
+
for (const client of clients) {
|
|
7665
|
+
if (client.readyState !== WebSocket.OPEN) continue;
|
|
7666
|
+
const existingMode = clientModes.get(client) ?? "full";
|
|
7667
|
+
if (existingMode !== "full") continue;
|
|
7668
|
+
try {
|
|
7669
|
+
ws.close(4004, "Full Studio already active");
|
|
7670
|
+
} catch {
|
|
7671
|
+
// Ignore close errors
|
|
7672
|
+
}
|
|
7673
|
+
return;
|
|
7674
|
+
}
|
|
7675
|
+
}
|
|
7587
7676
|
clients.add(ws);
|
|
7588
|
-
|
|
7677
|
+
clientModes.set(ws, clientMode);
|
|
7678
|
+
emitDebugEvent("studio_ws_connected", { clients: clients.size, mode: clientMode });
|
|
7589
7679
|
broadcastState();
|
|
7590
7680
|
|
|
7591
7681
|
ws.on("message", (data) => {
|
|
@@ -7599,11 +7689,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
7599
7689
|
|
|
7600
7690
|
ws.on("close", () => {
|
|
7601
7691
|
clients.delete(ws);
|
|
7692
|
+
clientModes.delete(ws);
|
|
7602
7693
|
emitDebugEvent("studio_ws_disconnected", { clients: clients.size });
|
|
7603
7694
|
});
|
|
7604
7695
|
|
|
7605
7696
|
ws.on("error", () => {
|
|
7606
7697
|
clients.delete(ws);
|
|
7698
|
+
clientModes.delete(ws);
|
|
7607
7699
|
});
|
|
7608
7700
|
});
|
|
7609
7701
|
|
|
@@ -7687,14 +7779,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
7687
7779
|
});
|
|
7688
7780
|
};
|
|
7689
7781
|
|
|
7690
|
-
const rotateToken = () => {
|
|
7691
|
-
if (!serverState) return;
|
|
7692
|
-
serverState.token = createSessionToken();
|
|
7693
|
-
clearPreparedPdfExports();
|
|
7694
|
-
closeAllClients(4001, "Session invalidated");
|
|
7695
|
-
broadcastState();
|
|
7696
|
-
};
|
|
7697
|
-
|
|
7698
7782
|
const hydrateLatestAssistant = (entries: SessionEntry[]) => {
|
|
7699
7783
|
syncStudioResponseHistory(entries);
|
|
7700
7784
|
};
|
|
@@ -7985,6 +8069,147 @@ export default function (pi: ExtensionAPI) {
|
|
|
7985
8069
|
await stopServer();
|
|
7986
8070
|
});
|
|
7987
8071
|
|
|
8072
|
+
const resolveStudioLaunchDocument = (
|
|
8073
|
+
trimmed: string,
|
|
8074
|
+
ctx: ExtensionCommandContext,
|
|
8075
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string },
|
|
8076
|
+
): InitialStudioDocument | null => {
|
|
8077
|
+
const defaultSource = options?.defaultSource === "blank" ? "blank" : "last-response";
|
|
8078
|
+
const commandLabel = options?.commandLabel ?? "/studio";
|
|
8079
|
+
const latestAssistant =
|
|
8080
|
+
extractLatestAssistantFromEntries(ctx.sessionManager.getBranch())
|
|
8081
|
+
?? extractLatestAssistantFromEntries(ctx.sessionManager.getEntries())
|
|
8082
|
+
?? lastStudioResponse?.markdown
|
|
8083
|
+
?? null;
|
|
8084
|
+
|
|
8085
|
+
if (!trimmed) {
|
|
8086
|
+
if (defaultSource === "last-response" && latestAssistant) {
|
|
8087
|
+
return {
|
|
8088
|
+
text: latestAssistant,
|
|
8089
|
+
label: "last model response",
|
|
8090
|
+
source: "last-response",
|
|
8091
|
+
};
|
|
8092
|
+
}
|
|
8093
|
+
return {
|
|
8094
|
+
text: "",
|
|
8095
|
+
label: "blank",
|
|
8096
|
+
source: "blank",
|
|
8097
|
+
};
|
|
8098
|
+
}
|
|
8099
|
+
|
|
8100
|
+
if (trimmed === "--blank" || trimmed === "blank") {
|
|
8101
|
+
return {
|
|
8102
|
+
text: "",
|
|
8103
|
+
label: "blank",
|
|
8104
|
+
source: "blank",
|
|
8105
|
+
};
|
|
8106
|
+
}
|
|
8107
|
+
|
|
8108
|
+
if (trimmed === "--last" || trimmed === "last") {
|
|
8109
|
+
if (!latestAssistant) {
|
|
8110
|
+
ctx.ui.notify("No assistant response found; opening blank studio.", "warning");
|
|
8111
|
+
return {
|
|
8112
|
+
text: "",
|
|
8113
|
+
label: "blank",
|
|
8114
|
+
source: "blank",
|
|
8115
|
+
};
|
|
8116
|
+
}
|
|
8117
|
+
return {
|
|
8118
|
+
text: latestAssistant,
|
|
8119
|
+
label: "last model response",
|
|
8120
|
+
source: "last-response",
|
|
8121
|
+
};
|
|
8122
|
+
}
|
|
8123
|
+
|
|
8124
|
+
if (trimmed.startsWith("-")) {
|
|
8125
|
+
ctx.ui.notify(`Unknown flag: ${trimmed}. Use ${commandLabel} --help`, "error");
|
|
8126
|
+
return null;
|
|
8127
|
+
}
|
|
8128
|
+
|
|
8129
|
+
const pathArg = parsePathArgument(trimmed);
|
|
8130
|
+
if (!pathArg) {
|
|
8131
|
+
ctx.ui.notify("Invalid file path argument.", "error");
|
|
8132
|
+
return null;
|
|
8133
|
+
}
|
|
8134
|
+
|
|
8135
|
+
const file = readStudioFile(pathArg, ctx.cwd);
|
|
8136
|
+
if (file.ok === false) {
|
|
8137
|
+
ctx.ui.notify(file.message, "error");
|
|
8138
|
+
return null;
|
|
8139
|
+
}
|
|
8140
|
+
|
|
8141
|
+
if (file.text.length > 200_000) {
|
|
8142
|
+
ctx.ui.notify(
|
|
8143
|
+
"Loaded a large file. Studio critique requests currently reject documents over 200k characters.",
|
|
8144
|
+
"warning",
|
|
8145
|
+
);
|
|
8146
|
+
}
|
|
8147
|
+
|
|
8148
|
+
return {
|
|
8149
|
+
text: file.text,
|
|
8150
|
+
label: file.label,
|
|
8151
|
+
source: "file",
|
|
8152
|
+
path: file.resolvedPath,
|
|
8153
|
+
};
|
|
8154
|
+
};
|
|
8155
|
+
|
|
8156
|
+
const openStudioView = async (
|
|
8157
|
+
trimmed: string,
|
|
8158
|
+
ctx: ExtensionCommandContext,
|
|
8159
|
+
mode: StudioUiMode,
|
|
8160
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean },
|
|
8161
|
+
) => {
|
|
8162
|
+
if (mode === "full" && hasConnectedFullStudioView()) {
|
|
8163
|
+
if (options?.replaceExistingFull) {
|
|
8164
|
+
closeStudioClientsByMode("full", 4001, "Full Studio replaced");
|
|
8165
|
+
} else {
|
|
8166
|
+
ctx.ui.notify("A full pi Studio view is already open for this session. Close it first, use /studio-replace for a fresh full Studio view, or use /studio-editor-only for a concurrent editor-only Studio view.", "warning");
|
|
8167
|
+
if (serverState) {
|
|
8168
|
+
ctx.ui.notify(`Studio URL: ${buildStudioUrl(serverState.port, serverState.token, "full")}`, "info");
|
|
8169
|
+
}
|
|
8170
|
+
return;
|
|
8171
|
+
}
|
|
8172
|
+
}
|
|
8173
|
+
|
|
8174
|
+
await ctx.waitForIdle();
|
|
8175
|
+
lastCommandCtx = ctx;
|
|
8176
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
8177
|
+
refreshContextUsage(ctx);
|
|
8178
|
+
syncStudioResponseHistory(ctx.sessionManager.getBranch());
|
|
8179
|
+
broadcastState();
|
|
8180
|
+
broadcastResponseHistory();
|
|
8181
|
+
try {
|
|
8182
|
+
const currentStyle = getStudioThemeStyle(ctx.ui.theme);
|
|
8183
|
+
lastThemeVarsJson = JSON.stringify(buildThemeCssVars(currentStyle));
|
|
8184
|
+
} catch {
|
|
8185
|
+
// ignore theme read errors
|
|
8186
|
+
}
|
|
8187
|
+
|
|
8188
|
+
const selected = resolveStudioLaunchDocument(trimmed, ctx, options);
|
|
8189
|
+
if (!selected) return;
|
|
8190
|
+
initialStudioDocument = selected;
|
|
8191
|
+
|
|
8192
|
+
const state = await ensureServer();
|
|
8193
|
+
const url = buildStudioUrl(state.port, state.token, mode);
|
|
8194
|
+
const openedLabel = mode === "editor-only" ? "pi Studio editor-only view" : "pi Studio";
|
|
8195
|
+
|
|
8196
|
+
try {
|
|
8197
|
+
await openUrlInDefaultBrowser(url);
|
|
8198
|
+
if (selected.source === "file") {
|
|
8199
|
+
ctx.ui.notify(`Opened ${openedLabel} with file loaded: ${selected.label}`, "info");
|
|
8200
|
+
} else if (selected.source === "last-response") {
|
|
8201
|
+
ctx.ui.notify(`Opened ${openedLabel} with last model response (${selected.text.length} chars).`, "info");
|
|
8202
|
+
} else {
|
|
8203
|
+
ctx.ui.notify(`Opened ${openedLabel} with blank editor.`, "info");
|
|
8204
|
+
}
|
|
8205
|
+
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
8206
|
+
} catch (error) {
|
|
8207
|
+
ctx.ui.notify(`Failed to open browser: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
8208
|
+
} finally {
|
|
8209
|
+
void maybeNotifyUpdateAvailable(ctx);
|
|
8210
|
+
}
|
|
8211
|
+
};
|
|
8212
|
+
|
|
7988
8213
|
pi.registerCommand("studio", {
|
|
7989
8214
|
description: "Open pi Studio browser UI (/studio, /studio <file>, /studio --blank, /studio --last)",
|
|
7990
8215
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -8001,8 +8226,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
8001
8226
|
ctx.ui.notify("Studio server is not running.", "info");
|
|
8002
8227
|
return;
|
|
8003
8228
|
}
|
|
8229
|
+
const counts = getStudioClientCounts();
|
|
8004
8230
|
ctx.ui.notify(
|
|
8005
|
-
`Studio running at http://127.0.0.1:${serverState.port}/ (busy: ${isStudioBusy() ? "yes" : "no"})`,
|
|
8231
|
+
`Studio running at http://127.0.0.1:${serverState.port}/ (busy: ${isStudioBusy() ? "yes" : "no"}; full views: ${counts.full}; editor-only views: ${counts.editorOnly})`,
|
|
8006
8232
|
"info",
|
|
8007
8233
|
);
|
|
8008
8234
|
return;
|
|
@@ -8017,6 +8243,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
8017
8243
|
+ " /studio --last Open with last model response\n"
|
|
8018
8244
|
+ " /studio --status Show studio status\n"
|
|
8019
8245
|
+ " /studio --stop Stop studio server\n"
|
|
8246
|
+
+ " Note: only one full /studio view is allowed per Pi session.\n"
|
|
8247
|
+
+ " /studio-replace [path] Replace the current full Studio view with a new one\n"
|
|
8248
|
+
+ " /studio-editor-only [path] Open another Studio tab in editor-only mode\n"
|
|
8020
8249
|
+ " /studio-current <path> Load a file into currently open Studio tab(s)\n"
|
|
8021
8250
|
+ " /studio-pdf <path> Export a file to <name>.studio.pdf via Studio PDF",
|
|
8022
8251
|
"info",
|
|
@@ -8024,115 +8253,53 @@ export default function (pi: ExtensionAPI) {
|
|
|
8024
8253
|
return;
|
|
8025
8254
|
}
|
|
8026
8255
|
|
|
8027
|
-
await ctx
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
refreshContextUsage(ctx);
|
|
8031
|
-
syncStudioResponseHistory(ctx.sessionManager.getBranch());
|
|
8032
|
-
broadcastState();
|
|
8033
|
-
broadcastResponseHistory();
|
|
8034
|
-
// Seed theme vars so first ping doesn't trigger a false update
|
|
8035
|
-
try {
|
|
8036
|
-
const currentStyle = getStudioThemeStyle(ctx.ui.theme);
|
|
8037
|
-
lastThemeVarsJson = JSON.stringify(buildThemeCssVars(currentStyle));
|
|
8038
|
-
} catch { /* ignore */ }
|
|
8256
|
+
await openStudioView(trimmed, ctx, "full", { defaultSource: "last-response", commandLabel: "/studio" });
|
|
8257
|
+
},
|
|
8258
|
+
});
|
|
8039
8259
|
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
} else {
|
|
8055
|
-
selected = {
|
|
8056
|
-
text: "",
|
|
8057
|
-
label: "blank",
|
|
8058
|
-
source: "blank",
|
|
8059
|
-
};
|
|
8060
|
-
}
|
|
8061
|
-
} else if (trimmed === "--blank" || trimmed === "blank") {
|
|
8062
|
-
selected = {
|
|
8063
|
-
text: "",
|
|
8064
|
-
label: "blank",
|
|
8065
|
-
source: "blank",
|
|
8066
|
-
};
|
|
8067
|
-
} else if (trimmed === "--last" || trimmed === "last") {
|
|
8068
|
-
if (!latestAssistant) {
|
|
8069
|
-
ctx.ui.notify("No assistant response found; opening blank studio.", "warning");
|
|
8070
|
-
selected = {
|
|
8071
|
-
text: "",
|
|
8072
|
-
label: "blank",
|
|
8073
|
-
source: "blank",
|
|
8074
|
-
};
|
|
8075
|
-
} else {
|
|
8076
|
-
selected = {
|
|
8077
|
-
text: latestAssistant,
|
|
8078
|
-
label: "last model response",
|
|
8079
|
-
source: "last-response",
|
|
8080
|
-
};
|
|
8081
|
-
}
|
|
8082
|
-
} else if (trimmed.startsWith("-")) {
|
|
8083
|
-
ctx.ui.notify(`Unknown flag: ${trimmed}. Use /studio --help`, "error");
|
|
8260
|
+
pi.registerCommand("studio-replace", {
|
|
8261
|
+
description: "Replace the current full pi Studio view (/studio-replace, /studio-replace <file>)",
|
|
8262
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
8263
|
+
const trimmed = args.trim();
|
|
8264
|
+
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
8265
|
+
ctx.ui.notify(
|
|
8266
|
+
"Usage: /studio-replace [path|--blank|--last]\n"
|
|
8267
|
+
+ " /studio-replace Replace the current full Studio view (default: last response, fallback: blank)\n"
|
|
8268
|
+
+ " /studio-replace <path> Replace the current full Studio view with file preloaded\n"
|
|
8269
|
+
+ " /studio-replace --blank Replace with blank editor\n"
|
|
8270
|
+
+ " /studio-replace --last Replace with last model response\n"
|
|
8271
|
+
+ "Editor-only Studio views stay open.",
|
|
8272
|
+
"info",
|
|
8273
|
+
);
|
|
8084
8274
|
return;
|
|
8085
|
-
} else {
|
|
8086
|
-
const pathArg = parsePathArgument(trimmed);
|
|
8087
|
-
if (!pathArg) {
|
|
8088
|
-
ctx.ui.notify("Invalid file path argument.", "error");
|
|
8089
|
-
return;
|
|
8090
|
-
}
|
|
8091
|
-
|
|
8092
|
-
const file = readStudioFile(pathArg, ctx.cwd);
|
|
8093
|
-
if (file.ok === false) {
|
|
8094
|
-
ctx.ui.notify(file.message, "error");
|
|
8095
|
-
return;
|
|
8096
|
-
}
|
|
8097
|
-
|
|
8098
|
-
selected = {
|
|
8099
|
-
text: file.text,
|
|
8100
|
-
label: file.label,
|
|
8101
|
-
source: "file",
|
|
8102
|
-
path: file.resolvedPath,
|
|
8103
|
-
};
|
|
8104
|
-
if (file.text.length > 200_000) {
|
|
8105
|
-
ctx.ui.notify(
|
|
8106
|
-
"Loaded a large file. Studio critique requests currently reject documents over 200k characters.",
|
|
8107
|
-
"warning",
|
|
8108
|
-
);
|
|
8109
|
-
}
|
|
8110
8275
|
}
|
|
8111
8276
|
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8277
|
+
await openStudioView(trimmed, ctx, "full", {
|
|
8278
|
+
defaultSource: "last-response",
|
|
8279
|
+
commandLabel: "/studio-replace",
|
|
8280
|
+
replaceExistingFull: true,
|
|
8281
|
+
});
|
|
8282
|
+
},
|
|
8283
|
+
});
|
|
8117
8284
|
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
"
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
} finally {
|
|
8134
|
-
void maybeNotifyUpdateAvailable(ctx);
|
|
8285
|
+
pi.registerCommand("studio-editor-only", {
|
|
8286
|
+
description: "Open pi Studio in editor-only mode (/studio-editor-only, /studio-editor-only <file>)",
|
|
8287
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
8288
|
+
const trimmed = args.trim();
|
|
8289
|
+
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
8290
|
+
ctx.ui.notify(
|
|
8291
|
+
"Usage: /studio-editor-only [path|--blank|--last]\n"
|
|
8292
|
+
+ " /studio-editor-only Open an editor-only Studio view (default: blank editor)\n"
|
|
8293
|
+
+ " /studio-editor-only <path> Open an editor-only Studio view with file preloaded\n"
|
|
8294
|
+
+ " /studio-editor-only --blank Open with blank editor\n"
|
|
8295
|
+
+ " /studio-editor-only --last Open with last model response loaded into the editor\n"
|
|
8296
|
+
+ "Multiple editor-only views are allowed in the same Pi session.",
|
|
8297
|
+
"info",
|
|
8298
|
+
);
|
|
8299
|
+
return;
|
|
8135
8300
|
}
|
|
8301
|
+
|
|
8302
|
+
await openStudioView(trimmed, ctx, "editor-only", { defaultSource: "blank", commandLabel: "/studio-editor-only" });
|
|
8136
8303
|
},
|
|
8137
8304
|
});
|
|
8138
8305
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.38",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|