pi-studio 0.9.58 → 0.9.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +7 -0
- package/ROADMAP.md +10 -0
- package/client/studio-client.js +171 -12
- package/client/studio.css +1 -0
- package/index.ts +93 -4
- package/package.json +1 -1
- package/shared/studio-pi-editor-draft.js +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.59] — 2026-09-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Add `Cmd/Ctrl+Shift+L` for loading the current Pi terminal input draft into Studio without clearing it.
|
|
11
|
+
- Add a confirmed **Clear Pi editor text…** action for removing text waiting in Pi's terminal input editor without changing Studio text or conversation history.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Treat Pi terminal drafts loaded into Studio or staged from Studio as transient linked handoffs. An accepted **Run editor text** now clears the terminal draft only when its SHA-256 fingerprint and byte length still match; later terminal typing, added `pi-nvim-context` context, another client change, and failed submissions all preserve it.
|
|
15
|
+
|
|
7
16
|
## [0.9.58] — 2026-09-03
|
|
8
17
|
|
|
9
18
|
### Added
|
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ _The video shows an earlier version of the Studio interface. The basic workflow
|
|
|
35
35
|
- Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you want extra editing/preview surfaces; editor-only views can also browse files and use the Studio REPL send controls without taking over the full Studio session view
|
|
36
36
|
- Includes a global **Zen** mode that hides the Studio header and secondary chrome without changing the current pane layout; the header can also be hidden independently and restored from the top-right edge
|
|
37
37
|
- Runs editor text directly, asks for structured critique (auto/writing/code focus), offers explicit **Show me** actions for a one-turn compact visual or structural explanation of the editor selection/document, displayed response, or current conversation topic, provides a manual **Suggest completion** action for short cursor-aware continuations (`Option/Alt+Tab` where available or `Cmd/Ctrl+Shift+Space` from the editor, `Tab` to insert a visible suggestion) with an optional editor-plus-latest-response context mode, or opens **Quiz me** for a Studio-native active-recall loop over the current editor text, selection, current file, folder, or repo, with optional focus guidance for shaping question selection
|
|
38
|
+
- Treats text copied from or staged in Pi's terminal input editor as a linked draft: ordinary Studio edits retain the link, and an accepted **Run editor text** clears the terminal draft only when it has not changed in the meantime; a confirmed **Clear Pi editor text…** action remains available explicitly
|
|
38
39
|
- Adds a right-pane **Side questions** thread for contextual questions that stay outside the main Pi conversation unless explicitly promoted. It can start with the editor selection, the Markdown/LaTeX heading block at the cursor, nearby unstructured text, the whole editor, the displayed response, or no starting text; independently, it can use read-only tools to map, search, and read the document folder, repository, or another chosen folder—including extracted PDF, DOCX, ODT, and EPUB text—and can optionally search the web through Brave Search.
|
|
39
40
|
- Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters, image previews for image-producing tool outputs, plus **Load visible into editor** and **Copy visible** actions; optional browser-stored **Follow activity** (off by default under **View → Workspace**) switches to Working when the main Pi agent starts generating and to Response Preview when it completes, unless you manually choose another view during that request. When cycling response history, Working follows saved working details for the selected response when available, and `Cmd/Ctrl+Alt+1–8` switches directly between right-pane views while `Cmd/Ctrl+Alt+P` / `Cmd/Ctrl+Alt+E` / `Cmd/Ctrl+Alt+W` / `Cmd/Ctrl+Alt+F` / `Cmd/Ctrl+Alt+R` / `Cmd/Ctrl+Alt+Q` keep quick mnemonic shortcuts for Response Preview, Editor Preview, Working, Files, REPL, and Side questions
|
|
40
41
|
- Includes a right-pane **Changes** view for browsing the current git diff by file, previewing per-file diffs, opening changed files, loading the full diff into the editor, and copying the diff
|
|
@@ -66,6 +67,12 @@ _The video shows an earlier version of the Studio interface. The basic workflow
|
|
|
66
67
|
- Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf` or `/studio-html <path>` to `<name>.studio.html`; without a path, those commands export the last model response to a timestamped file. Agent tools `studio_export_pdf` and `studio_export_html` expose the same export pipeline for remote/Telegram-style sessions.
|
|
67
68
|
- Shows model/session/context usage in the footer, plus compact-context and active-theme controls
|
|
68
69
|
|
|
70
|
+
## Pi input-draft handoff
|
|
71
|
+
|
|
72
|
+
Tools such as [`pi-nvim-context`](https://github.com/omaclaren/pi-nvim-context) deliberately accumulate visible context in Pi's terminal input draft without submitting it. **Load from pi editor** (`Cmd/Ctrl+Shift+L`) copies that draft into Studio without clearing it and records only a transient SHA-256 fingerprint and byte length for the handoff—no additional snapshot copy beyond the text already in Studio. You can then edit the text freely in Studio.
|
|
73
|
+
|
|
74
|
+
When Pi accepts **Run editor text**, Studio clears the linked terminal draft only if its current fingerprint and byte length still match those captured at load time. Terminal typing, additional Neovim context, or another client changing the draft causes Studio to preserve it silently. A failed or rejected run also leaves it intact. **Send current text to Pi editor** establishes the same safe link, while **Clear Pi editor text…** provides a separate confirmed, unconditional cleanup action. Studio never clears terminal history or its own editor through this handoff.
|
|
75
|
+
|
|
69
76
|
## Commands
|
|
70
77
|
|
|
71
78
|
| Command | Description |
|
package/ROADMAP.md
CHANGED
|
@@ -106,6 +106,16 @@ A small interaction increment before the buffer-first architecture:
|
|
|
106
106
|
- add opt-in activity following from Working during main-agent generation to Response Preview on completion, disabled by default and cancelled when the user manually changes views;
|
|
107
107
|
- keep Quick send separate from the durable editor, the raw tmux mirror read-only, and protocol-v1 authority unchanged.
|
|
108
108
|
|
|
109
|
+
## 0.9.59 — Safe Pi input-draft handoff (shipped 2026-09-03)
|
|
110
|
+
|
|
111
|
+
A small workflow refinement for moving Neovim-provided context through Pi and Studio:
|
|
112
|
+
|
|
113
|
+
- load or stage Pi's terminal input draft without clearing it, retaining only a transient SHA-256 fingerprint and byte length for provenance;
|
|
114
|
+
- keep that link through unrestricted Studio edits, then clear the original Pi draft only after Pi accepts the Studio run and the current terminal text still matches exactly;
|
|
115
|
+
- silently preserve later terminal typing, added `pi-nvim-context` context, or another client's changes, and never clear after a rejected submission;
|
|
116
|
+
- provide `Cmd/Ctrl+Shift+L` for loading and a confirmed **Clear Pi editor text…** fallback without changing Studio text or conversation history;
|
|
117
|
+
- keep `pi-nvim-context`, protocol v1, networking, and the buffer-first architecture unchanged.
|
|
118
|
+
|
|
109
119
|
## 0.10.0 — Buffer-first editing
|
|
110
120
|
|
|
111
121
|
The first architectural `0.10` release should add:
|
package/client/studio-client.js
CHANGED
|
@@ -121,6 +121,7 @@
|
|
|
121
121
|
const refreshFromDiskBtn = document.getElementById("refreshFromDiskBtn");
|
|
122
122
|
const clearWorkspaceBtn = document.getElementById("clearWorkspaceBtn");
|
|
123
123
|
const sendEditorBtn = document.getElementById("sendEditorBtn");
|
|
124
|
+
const clearPiEditorBtn = document.getElementById("clearPiEditorBtn");
|
|
124
125
|
const openCompanionBtn = document.getElementById("openCompanionBtn");
|
|
125
126
|
const getEditorBtn = document.getElementById("getEditorBtn");
|
|
126
127
|
const studioHeaderEl = document.getElementById("studioHeader");
|
|
@@ -361,6 +362,9 @@
|
|
|
361
362
|
const pendingSaveOperations = new Map();
|
|
362
363
|
const pendingCompanionLaunches = new Map();
|
|
363
364
|
const activeStudioTabLaunches = new Set();
|
|
365
|
+
// Deliberately page-memory-only: never persist Pi input-draft handoff fingerprints.
|
|
366
|
+
const pendingPiEditorDraftSnapshots = new Map();
|
|
367
|
+
let linkedPiEditorDraftSnapshot = null;
|
|
364
368
|
let sourceOriginSummaryEl = null;
|
|
365
369
|
let sourceResetOriginBtn = null;
|
|
366
370
|
let sourceOpenCurrentFileTabBtn = null;
|
|
@@ -3186,7 +3190,9 @@
|
|
|
3186
3190
|
if (sendEditorBtn) sendEditorBtn.textContent = "Send current text to Pi editor";
|
|
3187
3191
|
appendStudioUiRefreshMenuSection(contextMenu.menu, "Document", [sourceOriginSummaryEl, sourceResetOriginBtn, sourceOpenCurrentFileTabBtn, sourceOpenCurrentTextCopyTabBtn]);
|
|
3188
3192
|
appendStudioUiRefreshMenuSection(contextMenu.menu, "Working directory", [resourceDirBtn, resourceDirLabel, resourceDirInputWrap]);
|
|
3189
|
-
if (!isEditorOnlyMode
|
|
3193
|
+
if (!isEditorOnlyMode) {
|
|
3194
|
+
appendStudioUiRefreshMenuSection(contextMenu.menu, "Pi editor", [sendEditorBtn, clearPiEditorBtn]);
|
|
3195
|
+
}
|
|
3190
3196
|
const cursorContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor only");
|
|
3191
3197
|
cursorContextBtn.type = "button";
|
|
3192
3198
|
cursorContextBtn.setAttribute("data-completion-context-mode", "cursor");
|
|
@@ -3545,6 +3551,7 @@
|
|
|
3545
3551
|
if (kind === "compact") return "compacting context";
|
|
3546
3552
|
if (kind === "send_to_editor") return "sending to pi editor";
|
|
3547
3553
|
if (kind === "get_from_editor") return "loading from pi editor";
|
|
3554
|
+
if (kind === "clear_pi_editor") return "clearing Pi editor text";
|
|
3548
3555
|
if (kind === "load_git_diff") return "loading git diff";
|
|
3549
3556
|
if (kind === "open_editor_only") return "opening companion editor";
|
|
3550
3557
|
if (kind === "refresh_from_disk") return "refreshing from disk";
|
|
@@ -3803,6 +3810,7 @@
|
|
|
3803
3810
|
if (kind === "compact") return "Compacting…";
|
|
3804
3811
|
if (kind === "send_to_editor") return "Sending to editor…";
|
|
3805
3812
|
if (kind === "get_from_editor") return "Loading from editor…";
|
|
3813
|
+
if (kind === "clear_pi_editor") return "Clearing Pi editor text…";
|
|
3806
3814
|
if (kind === "load_git_diff") return "Loading git diff…";
|
|
3807
3815
|
if (kind === "refresh_from_disk") return "Refreshing from disk…";
|
|
3808
3816
|
if (kind === "save_as" || kind === "save_over") return "Saving…";
|
|
@@ -4464,6 +4472,58 @@
|
|
|
4464
4472
|
return /^sha256:[a-f0-9]{64}$/.test(revision) ? revision : null;
|
|
4465
4473
|
}
|
|
4466
4474
|
|
|
4475
|
+
function normalizePiEditorDraftSnapshot(value) {
|
|
4476
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4477
|
+
const fingerprint = normalizeStudioDiskRevision(value.fingerprint);
|
|
4478
|
+
const byteLength = value.byteLength;
|
|
4479
|
+
if (!fingerprint || typeof byteLength !== "number" || !Number.isSafeInteger(byteLength) || byteLength < 0) return null;
|
|
4480
|
+
return { fingerprint, byteLength };
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
function piEditorDraftSnapshotsMatch(left, right) {
|
|
4484
|
+
const normalizedLeft = normalizePiEditorDraftSnapshot(left);
|
|
4485
|
+
const normalizedRight = normalizePiEditorDraftSnapshot(right);
|
|
4486
|
+
return Boolean(
|
|
4487
|
+
normalizedLeft
|
|
4488
|
+
&& normalizedRight
|
|
4489
|
+
&& normalizedLeft.fingerprint === normalizedRight.fingerprint
|
|
4490
|
+
&& normalizedLeft.byteLength === normalizedRight.byteLength
|
|
4491
|
+
);
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
function setLinkedPiEditorDraftSnapshot(value, hasContent) {
|
|
4495
|
+
linkedPiEditorDraftSnapshot = hasContent ? normalizePiEditorDraftSnapshot(value) : null;
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
function clearLinkedPiEditorDraftSnapshot() {
|
|
4499
|
+
linkedPiEditorDraftSnapshot = null;
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
function reserveLinkedPiEditorDraftSnapshot(requestId) {
|
|
4503
|
+
const snapshot = normalizePiEditorDraftSnapshot(linkedPiEditorDraftSnapshot);
|
|
4504
|
+
if (!snapshot || typeof requestId !== "string" || !requestId) return null;
|
|
4505
|
+
linkedPiEditorDraftSnapshot = null;
|
|
4506
|
+
pendingPiEditorDraftSnapshots.set(requestId, snapshot);
|
|
4507
|
+
return snapshot;
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
function restoreReservedPiEditorDraftSnapshot(requestId) {
|
|
4511
|
+
if (typeof requestId !== "string" || !requestId) return;
|
|
4512
|
+
const snapshot = pendingPiEditorDraftSnapshots.get(requestId);
|
|
4513
|
+
if (!snapshot) return;
|
|
4514
|
+
pendingPiEditorDraftSnapshots.delete(requestId);
|
|
4515
|
+
if (!linkedPiEditorDraftSnapshot) linkedPiEditorDraftSnapshot = snapshot;
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
function settleReservedPiEditorDraftSnapshot(requestId, reportedSnapshot) {
|
|
4519
|
+
if (typeof requestId !== "string" || !requestId) return false;
|
|
4520
|
+
const snapshot = pendingPiEditorDraftSnapshots.get(requestId);
|
|
4521
|
+
if (!snapshot) return false;
|
|
4522
|
+
if (reportedSnapshot && !piEditorDraftSnapshotsMatch(snapshot, reportedSnapshot)) return false;
|
|
4523
|
+
pendingPiEditorDraftSnapshots.delete(requestId);
|
|
4524
|
+
return true;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4467
4527
|
function markFileBackedBaseline(text, diskRevision) {
|
|
4468
4528
|
fileBackedBaselineText = String(text || "");
|
|
4469
4529
|
fileBackedDiskRevision = normalizeStudioDiskRevision(diskRevision);
|
|
@@ -5098,6 +5158,20 @@
|
|
|
5098
5158
|
return true;
|
|
5099
5159
|
}
|
|
5100
5160
|
|
|
5161
|
+
function triggerLoadFromPiEditorShortcut() {
|
|
5162
|
+
if (isWatchedFilePreview) {
|
|
5163
|
+
setStatus("This preview is read-only. Open full Studio to load the Pi draft.", "warning");
|
|
5164
|
+
return false;
|
|
5165
|
+
}
|
|
5166
|
+
if (!getEditorBtn || getEditorBtn.hidden) return false;
|
|
5167
|
+
if (getEditorBtn.disabled) {
|
|
5168
|
+
setStatus("Load from Pi editor is unavailable while Studio is busy.", "warning");
|
|
5169
|
+
return false;
|
|
5170
|
+
}
|
|
5171
|
+
getEditorBtn.click();
|
|
5172
|
+
return true;
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5101
5175
|
function triggerEditorSaveShortcut() {
|
|
5102
5176
|
if (hasRefreshableFilePath() && saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
|
|
5103
5177
|
saveOverBtn.click();
|
|
@@ -5459,6 +5533,18 @@
|
|
|
5459
5533
|
return;
|
|
5460
5534
|
}
|
|
5461
5535
|
|
|
5536
|
+
const isLoadFromPiEditorShortcut =
|
|
5537
|
+
(key.toLowerCase() === "l" || code === "KeyL")
|
|
5538
|
+
&& (event.metaKey || event.ctrlKey)
|
|
5539
|
+
&& !event.altKey
|
|
5540
|
+
&& event.shiftKey;
|
|
5541
|
+
|
|
5542
|
+
if (isLoadFromPiEditorShortcut) {
|
|
5543
|
+
event.preventDefault();
|
|
5544
|
+
triggerLoadFromPiEditorShortcut();
|
|
5545
|
+
return;
|
|
5546
|
+
}
|
|
5547
|
+
|
|
5462
5548
|
const isSaveAsShortcut =
|
|
5463
5549
|
key.toLowerCase() === "s"
|
|
5464
5550
|
&& (event.metaKey || event.ctrlKey)
|
|
@@ -13888,7 +13974,7 @@
|
|
|
13888
13974
|
const safeStart = Math.max(0, Math.min(start, current.length));
|
|
13889
13975
|
const safeEnd = Math.max(safeStart, Math.min(end, current.length));
|
|
13890
13976
|
const next = current.slice(0, safeStart) + answer.text + current.slice(safeEnd);
|
|
13891
|
-
setEditorText(next, { preserveScroll: false, preserveSelection: false });
|
|
13977
|
+
setEditorText(next, { preserveScroll: false, preserveSelection: false, preservePiEditorDraftLink: true });
|
|
13892
13978
|
const caret = safeStart + answer.text.length;
|
|
13893
13979
|
sourceTextEl.setSelectionRange(caret, caret);
|
|
13894
13980
|
setActivePane("left");
|
|
@@ -14504,6 +14590,7 @@
|
|
|
14504
14590
|
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || isWatchedFilePreview || !canRefreshFromDisk;
|
|
14505
14591
|
if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
14506
14592
|
sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
14593
|
+
if (clearPiEditorBtn) clearPiEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
14507
14594
|
if (getEditorBtn) getEditorBtn.disabled = uiBusy || isWatchedFilePreview;
|
|
14508
14595
|
if (watchedOpenEditableBtn) {
|
|
14509
14596
|
watchedOpenEditableBtn.hidden = !isWatchedFilePreview;
|
|
@@ -14885,6 +14972,9 @@
|
|
|
14885
14972
|
const value = String(nextText || "");
|
|
14886
14973
|
const preserveScroll = Boolean(options && options.preserveScroll);
|
|
14887
14974
|
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
14975
|
+
if (!(options && options.preservePiEditorDraftLink === true)) {
|
|
14976
|
+
clearLinkedPiEditorDraftSnapshot();
|
|
14977
|
+
}
|
|
14888
14978
|
if (activePreviewCommentSelection) {
|
|
14889
14979
|
clearPreviewCommentSelection();
|
|
14890
14980
|
}
|
|
@@ -22387,7 +22477,7 @@
|
|
|
22387
22477
|
? current.slice(0, inlineState.range.end) + current.slice(inlineState.range.end + inlineState.markerText.length)
|
|
22388
22478
|
: current.slice(0, inlineState.range.end) + inlineState.markerText + current.slice(inlineState.range.end);
|
|
22389
22479
|
setEditorView("markdown");
|
|
22390
|
-
setEditorText(next, { preserveScroll: true, preserveSelection: true });
|
|
22480
|
+
setEditorText(next, { preserveScroll: true, preserveSelection: true, preservePiEditorDraftLink: true });
|
|
22391
22481
|
pendingReviewNoteInlineFocusId = note.id;
|
|
22392
22482
|
renderReviewNotesList();
|
|
22393
22483
|
updateReviewNotesUi();
|
|
@@ -22432,7 +22522,7 @@
|
|
|
22432
22522
|
}
|
|
22433
22523
|
|
|
22434
22524
|
setEditorView("markdown");
|
|
22435
|
-
setEditorText(currentText, { preserveScroll: true, preserveSelection: true });
|
|
22525
|
+
setEditorText(currentText, { preserveScroll: true, preserveSelection: true, preservePiEditorDraftLink: true });
|
|
22436
22526
|
renderReviewNotesList();
|
|
22437
22527
|
updateReviewNotesUi();
|
|
22438
22528
|
if (reviewNotesInlineAllBtn && typeof reviewNotesInlineAllBtn.focus === "function") {
|
|
@@ -22618,7 +22708,7 @@
|
|
|
22618
22708
|
const safeStart = Math.max(0, Math.min(start, current.length));
|
|
22619
22709
|
const safeEnd = Math.max(safeStart, Math.min(end, current.length));
|
|
22620
22710
|
const next = current.slice(0, safeStart) + content + current.slice(safeEnd);
|
|
22621
|
-
setEditorText(next, { preserveScroll: false, preserveSelection: false });
|
|
22711
|
+
setEditorText(next, { preserveScroll: false, preserveSelection: false, preservePiEditorDraftLink: true });
|
|
22622
22712
|
const caret = safeStart + content.length;
|
|
22623
22713
|
sourceTextEl.setSelectionRange(caret, caret);
|
|
22624
22714
|
setActivePane("left");
|
|
@@ -22821,11 +22911,14 @@
|
|
|
22821
22911
|
const replHint = rightView === "repl" && getActiveReplSessionForCurrentRuntime()
|
|
22822
22912
|
? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
|
|
22823
22913
|
: "";
|
|
22914
|
+
const piEditorDraftHint = linkedPiEditorDraftSnapshot
|
|
22915
|
+
? " The linked Pi terminal draft will be cleared after Pi accepts this run only if that draft remains unchanged."
|
|
22916
|
+
: "";
|
|
22824
22917
|
sendRunBtn.title = directIsStop
|
|
22825
22918
|
? "Stop the active run. Shortcut: Esc."
|
|
22826
22919
|
: (annotationsEnabled
|
|
22827
|
-
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + replHint
|
|
22828
|
-
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + replHint);
|
|
22920
|
+
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + piEditorDraftHint + replHint
|
|
22921
|
+
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + piEditorDraftHint + replHint);
|
|
22829
22922
|
}
|
|
22830
22923
|
|
|
22831
22924
|
if (queueSteerBtn) {
|
|
@@ -23670,6 +23763,7 @@
|
|
|
23670
23763
|
}
|
|
23671
23764
|
|
|
23672
23765
|
const completedRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
23766
|
+
if (completedRequestId) pendingPiEditorDraftSnapshots.delete(completedRequestId);
|
|
23673
23767
|
const responseKind =
|
|
23674
23768
|
typeof message.kind === "string"
|
|
23675
23769
|
? message.kind
|
|
@@ -23804,17 +23898,46 @@
|
|
|
23804
23898
|
return;
|
|
23805
23899
|
}
|
|
23806
23900
|
|
|
23901
|
+
if (message.type === "pi_editor_draft_result") {
|
|
23902
|
+
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
23903
|
+
if (!settleReservedPiEditorDraftSnapshot(requestId, message.piEditorDraftSnapshot)) return;
|
|
23904
|
+
const outcome = String(message.outcome || "");
|
|
23905
|
+
syncActionButtons();
|
|
23906
|
+
if (outcome === "cleared") {
|
|
23907
|
+
setStatus("Submission accepted; cleared the unchanged Pi draft.", "success");
|
|
23908
|
+
} else if (outcome === "already-empty") {
|
|
23909
|
+
setStatus("Submission accepted; the Pi draft was already empty.", "success");
|
|
23910
|
+
} else if (outcome !== "changed") {
|
|
23911
|
+
setStatus("Submission accepted, but Studio could not confirm Pi-draft cleanup; check the terminal draft.", "warning");
|
|
23912
|
+
}
|
|
23913
|
+
return;
|
|
23914
|
+
}
|
|
23915
|
+
|
|
23807
23916
|
if (message.type === "editor_loaded") {
|
|
23808
23917
|
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
23809
23918
|
pendingRequestId = null;
|
|
23810
23919
|
pendingKind = null;
|
|
23811
23920
|
}
|
|
23921
|
+
const snapshot = normalizePiEditorDraftSnapshot(message.piEditorDraftSnapshot);
|
|
23922
|
+
setLinkedPiEditorDraftSnapshot(snapshot, Boolean(snapshot && snapshot.byteLength > 0));
|
|
23812
23923
|
setBusy(false);
|
|
23813
23924
|
setWsState("Ready");
|
|
23814
23925
|
setStatus(typeof message.message === "string" ? message.message : "Loaded into pi editor.", "success");
|
|
23815
23926
|
return;
|
|
23816
23927
|
}
|
|
23817
23928
|
|
|
23929
|
+
if (message.type === "pi_editor_cleared") {
|
|
23930
|
+
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
23931
|
+
pendingRequestId = null;
|
|
23932
|
+
pendingKind = null;
|
|
23933
|
+
}
|
|
23934
|
+
clearLinkedPiEditorDraftSnapshot();
|
|
23935
|
+
setBusy(false);
|
|
23936
|
+
setWsState("Ready");
|
|
23937
|
+
setStatus(message.cleared ? "Cleared Pi editor text." : "Pi editor text was already empty.", "success");
|
|
23938
|
+
return;
|
|
23939
|
+
}
|
|
23940
|
+
|
|
23818
23941
|
if (message.type === "editor_snapshot") {
|
|
23819
23942
|
if (isWatchedFilePreview) {
|
|
23820
23943
|
setStatus("Ignored editor snapshot because this preview follows its watched file on disk.", "warning");
|
|
@@ -23830,12 +23953,14 @@
|
|
|
23830
23953
|
|
|
23831
23954
|
const content = typeof message.content === "string" ? message.content : "";
|
|
23832
23955
|
setEditorText(content, { preserveScroll: false, preserveSelection: false });
|
|
23956
|
+
const snapshot = normalizePiEditorDraftSnapshot(message.piEditorDraftSnapshot);
|
|
23957
|
+
setLinkedPiEditorDraftSnapshot(snapshot, Boolean(content.length > 0 && snapshot));
|
|
23833
23958
|
setSourceState({ source: "pi-editor", label: "pi editor draft", path: null });
|
|
23834
23959
|
setBusy(false);
|
|
23835
23960
|
setWsState("Ready");
|
|
23836
23961
|
setStatus(
|
|
23837
23962
|
content.trim()
|
|
23838
|
-
? "Loaded draft from pi editor."
|
|
23963
|
+
? "Loaded draft from pi editor. A Studio Run will clear it if the Pi draft remains unchanged."
|
|
23839
23964
|
: "pi editor is empty. Loaded blank text.",
|
|
23840
23965
|
content.trim() ? "success" : "warning",
|
|
23841
23966
|
);
|
|
@@ -24032,6 +24157,7 @@
|
|
|
24032
24157
|
|
|
24033
24158
|
if (message.type === "busy") {
|
|
24034
24159
|
if (typeof message.requestId === "string") {
|
|
24160
|
+
restoreReservedPiEditorDraftSnapshot(message.requestId);
|
|
24035
24161
|
pendingSaveOperations.delete(message.requestId);
|
|
24036
24162
|
failPendingCompanionLaunch(message.requestId, "Studio could not start the companion editor because another request was busy.");
|
|
24037
24163
|
}
|
|
@@ -24055,6 +24181,7 @@
|
|
|
24055
24181
|
|
|
24056
24182
|
if (message.type === "error") {
|
|
24057
24183
|
if (typeof message.requestId === "string") {
|
|
24184
|
+
restoreReservedPiEditorDraftSnapshot(message.requestId);
|
|
24058
24185
|
pendingSaveOperations.delete(message.requestId);
|
|
24059
24186
|
failPendingCompanionLaunch(message.requestId, "Studio could not prepare the companion editor. Return to the originating Studio page for details.");
|
|
24060
24187
|
}
|
|
@@ -24203,6 +24330,7 @@
|
|
|
24203
24330
|
quartoPreviewCheckSourcePath = "";
|
|
24204
24331
|
quartoPreviewActionRequestId = null;
|
|
24205
24332
|
sideQuestionMarkdownExportRequest = null;
|
|
24333
|
+
pendingPiEditorDraftSnapshots.clear();
|
|
24206
24334
|
failAllPendingCompanionLaunches("The originating Studio connection was lost before the companion editor was ready.");
|
|
24207
24335
|
if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
|
|
24208
24336
|
setBusy(true);
|
|
@@ -24473,7 +24601,7 @@
|
|
|
24473
24601
|
|
|
24474
24602
|
if (stripped.hadHeader) {
|
|
24475
24603
|
const updated = stripped.body;
|
|
24476
|
-
setEditorText(updated, { preserveScroll: true, preserveSelection: true });
|
|
24604
|
+
setEditorText(updated, { preserveScroll: true, preserveSelection: true, preservePiEditorDraftLink: true });
|
|
24477
24605
|
updateResultActionButtons();
|
|
24478
24606
|
setStatus("Removed annotated reply header.", "success");
|
|
24479
24607
|
return;
|
|
@@ -24486,7 +24614,7 @@
|
|
|
24486
24614
|
return;
|
|
24487
24615
|
}
|
|
24488
24616
|
|
|
24489
|
-
setEditorText(updated, { preserveScroll: true, preserveSelection: true });
|
|
24617
|
+
setEditorText(updated, { preserveScroll: true, preserveSelection: true, preservePiEditorDraftLink: true });
|
|
24490
24618
|
updateResultActionButtons();
|
|
24491
24619
|
setStatus("Inserted annotated reply header.", "success");
|
|
24492
24620
|
}
|
|
@@ -25356,6 +25484,29 @@
|
|
|
25356
25484
|
}
|
|
25357
25485
|
});
|
|
25358
25486
|
|
|
25487
|
+
if (clearPiEditorBtn) {
|
|
25488
|
+
clearPiEditorBtn.addEventListener("click", async () => {
|
|
25489
|
+
closeStudioUiRefreshMenus();
|
|
25490
|
+
const confirmed = await requestStudioConfirmation(
|
|
25491
|
+
"Clear the current Pi input draft? This removes text waiting in the terminal editor, including context added from Neovim. Studio text and Pi conversation history will not change.",
|
|
25492
|
+
{ title: "Clear Pi editor text?", confirmLabel: "Clear text", destructive: true },
|
|
25493
|
+
);
|
|
25494
|
+
if (!confirmed) {
|
|
25495
|
+
setStatus("Pi editor text was not cleared.");
|
|
25496
|
+
return;
|
|
25497
|
+
}
|
|
25498
|
+
|
|
25499
|
+
const requestId = beginUiAction("clear_pi_editor");
|
|
25500
|
+
if (!requestId) return;
|
|
25501
|
+
const sent = sendMessage({ type: "clear_pi_editor_request", requestId });
|
|
25502
|
+
if (!sent) {
|
|
25503
|
+
pendingRequestId = null;
|
|
25504
|
+
pendingKind = null;
|
|
25505
|
+
setBusy(false);
|
|
25506
|
+
}
|
|
25507
|
+
});
|
|
25508
|
+
}
|
|
25509
|
+
|
|
25359
25510
|
if (openCompanionBtn) {
|
|
25360
25511
|
openCompanionBtn.addEventListener("click", () => {
|
|
25361
25512
|
requestOpenEditorOnlyDocument("", {
|
|
@@ -25423,14 +25574,17 @@
|
|
|
25423
25574
|
|
|
25424
25575
|
const requestId = beginUiAction("direct");
|
|
25425
25576
|
if (!requestId) return;
|
|
25577
|
+
const piEditorDraftSnapshot = reserveLinkedPiEditorDraftSnapshot(requestId);
|
|
25426
25578
|
|
|
25427
25579
|
const sent = sendMessage({
|
|
25428
25580
|
type: "send_run_request",
|
|
25429
25581
|
requestId,
|
|
25430
25582
|
text: prepared,
|
|
25583
|
+
piEditorDraftSnapshot: piEditorDraftSnapshot || undefined,
|
|
25431
25584
|
});
|
|
25432
25585
|
|
|
25433
25586
|
if (!sent) {
|
|
25587
|
+
restoreReservedPiEditorDraftSnapshot(requestId);
|
|
25434
25588
|
pendingRequestId = null;
|
|
25435
25589
|
pendingKind = null;
|
|
25436
25590
|
setBusy(false);
|
|
@@ -25451,12 +25605,17 @@
|
|
|
25451
25605
|
|
|
25452
25606
|
const requestId = makeRequestId();
|
|
25453
25607
|
clearTitleAttention();
|
|
25608
|
+
const piEditorDraftSnapshot = reserveLinkedPiEditorDraftSnapshot(requestId);
|
|
25454
25609
|
const sent = sendMessage({
|
|
25455
25610
|
type: "send_run_request",
|
|
25456
25611
|
requestId,
|
|
25457
25612
|
text: prepared,
|
|
25613
|
+
piEditorDraftSnapshot: piEditorDraftSnapshot || undefined,
|
|
25458
25614
|
});
|
|
25459
|
-
if (!sent)
|
|
25615
|
+
if (!sent) {
|
|
25616
|
+
restoreReservedPiEditorDraftSnapshot(requestId);
|
|
25617
|
+
return;
|
|
25618
|
+
}
|
|
25460
25619
|
setStatus("Queueing steering…", "warning");
|
|
25461
25620
|
});
|
|
25462
25621
|
}
|
|
@@ -25892,7 +26051,7 @@
|
|
|
25892
26051
|
if (!confirmed) return;
|
|
25893
26052
|
|
|
25894
26053
|
const strippedContent = stripAnnotationMarkers(content);
|
|
25895
|
-
setEditorText(strippedContent, { preserveScroll: true, preserveSelection: false });
|
|
26054
|
+
setEditorText(strippedContent, { preserveScroll: true, preserveSelection: false, preservePiEditorDraftLink: true });
|
|
25896
26055
|
setStatus("Removed annotation markers from editor text.", "success");
|
|
25897
26056
|
});
|
|
25898
26057
|
}
|
package/client/studio.css
CHANGED
|
@@ -569,6 +569,7 @@
|
|
|
569
569
|
body[data-studio-mode="editor-only"] #sendRunBtn,
|
|
570
570
|
body[data-studio-mode="editor-only"] #queueSteerBtn,
|
|
571
571
|
body[data-studio-mode="editor-only"] #sendEditorBtn,
|
|
572
|
+
body[data-studio-mode="editor-only"] #clearPiEditorBtn,
|
|
572
573
|
body[data-studio-mode="editor-only"] #lensSelect,
|
|
573
574
|
body[data-studio-mode="editor-only"] #critiqueBtn,
|
|
574
575
|
body[data-studio-mode="editor-only"] #showMeBtn,
|
package/index.ts
CHANGED
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
saveStudioDiskFileAs,
|
|
30
30
|
saveStudioDiskFileIfRevision,
|
|
31
31
|
} from "./shared/studio-disk-revisions.js";
|
|
32
|
+
import {
|
|
33
|
+
consumeStudioPiEditorDraftSnapshot,
|
|
34
|
+
createStudioPiEditorDraftSnapshot,
|
|
35
|
+
normalizeStudioPiEditorDraftSnapshot,
|
|
36
|
+
} from "./shared/studio-pi-editor-draft.js";
|
|
32
37
|
import { normalizeStudioMarkdownSmartFences } from "./shared/studio-markdown-fences.js";
|
|
33
38
|
import {
|
|
34
39
|
extractStandaloneLatexDefinitionsFromMarkdown,
|
|
@@ -646,10 +651,16 @@ interface AnnotationRequestMessage {
|
|
|
646
651
|
text: string;
|
|
647
652
|
}
|
|
648
653
|
|
|
654
|
+
interface StudioPiEditorDraftSnapshot {
|
|
655
|
+
fingerprint: string;
|
|
656
|
+
byteLength: number;
|
|
657
|
+
}
|
|
658
|
+
|
|
649
659
|
interface SendRunRequestMessage {
|
|
650
660
|
type: "send_run_request";
|
|
651
661
|
requestId: string;
|
|
652
662
|
text: string;
|
|
663
|
+
piEditorDraftSnapshot?: StudioPiEditorDraftSnapshot;
|
|
653
664
|
}
|
|
654
665
|
|
|
655
666
|
interface CompletionSuggestionRequestMessage {
|
|
@@ -839,6 +850,11 @@ interface GetFromEditorRequestMessage {
|
|
|
839
850
|
requestId: string;
|
|
840
851
|
}
|
|
841
852
|
|
|
853
|
+
interface ClearPiEditorRequestMessage {
|
|
854
|
+
type: "clear_pi_editor_request";
|
|
855
|
+
requestId: string;
|
|
856
|
+
}
|
|
857
|
+
|
|
842
858
|
interface QuartoPreviewCheckRequestMessage {
|
|
843
859
|
type: "quarto_preview_check_request";
|
|
844
860
|
requestId: string;
|
|
@@ -922,6 +938,7 @@ type IncomingStudioMessage =
|
|
|
922
938
|
| WatchFileSubscribeMessage
|
|
923
939
|
| SendToEditorRequestMessage
|
|
924
940
|
| GetFromEditorRequestMessage
|
|
941
|
+
| ClearPiEditorRequestMessage
|
|
925
942
|
| QuartoPreviewCheckRequestMessage
|
|
926
943
|
| QuartoPreviewStartRequestMessage
|
|
927
944
|
| QuartoPreviewStopRequestMessage
|
|
@@ -9917,10 +9934,15 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
9917
9934
|
}
|
|
9918
9935
|
|
|
9919
9936
|
if (msg.type === "send_run_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
|
|
9937
|
+
const piEditorDraftSnapshot = msg.piEditorDraftSnapshot === undefined
|
|
9938
|
+
? undefined
|
|
9939
|
+
: normalizeStudioPiEditorDraftSnapshot(msg.piEditorDraftSnapshot);
|
|
9940
|
+
if (msg.piEditorDraftSnapshot !== undefined && !piEditorDraftSnapshot) return null;
|
|
9920
9941
|
return {
|
|
9921
9942
|
type: "send_run_request",
|
|
9922
9943
|
requestId: msg.requestId,
|
|
9923
9944
|
text: msg.text,
|
|
9945
|
+
piEditorDraftSnapshot: piEditorDraftSnapshot ?? undefined,
|
|
9924
9946
|
};
|
|
9925
9947
|
}
|
|
9926
9948
|
|
|
@@ -10200,6 +10222,13 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
10200
10222
|
};
|
|
10201
10223
|
}
|
|
10202
10224
|
|
|
10225
|
+
if (msg.type === "clear_pi_editor_request" && typeof msg.requestId === "string") {
|
|
10226
|
+
return {
|
|
10227
|
+
type: "clear_pi_editor_request",
|
|
10228
|
+
requestId: msg.requestId,
|
|
10229
|
+
};
|
|
10230
|
+
}
|
|
10231
|
+
|
|
10203
10232
|
if (
|
|
10204
10233
|
msg.type === "quarto_preview_check_request"
|
|
10205
10234
|
&& typeof msg.requestId === "string"
|
|
@@ -12385,7 +12414,7 @@ ${cssVarsBlock}
|
|
|
12385
12414
|
<button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
|
|
12386
12415
|
<button id="importFileBtn" type="button" title="Import a file as an editable copy.">Import file copy…</button>
|
|
12387
12416
|
<input id="fileInput" class="file-input-hidden" type="file" tabindex="-1" aria-hidden="true" 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" />
|
|
12388
|
-
<button id="getEditorBtn" type="button" title="
|
|
12417
|
+
<button id="getEditorBtn" type="button" aria-keyshortcuts="Meta+Shift+L Control+Shift+L" title="Copy the current Pi terminal input draft into Studio without clearing it. A later Studio Run clears that draft only if it remains unchanged. Shortcut: Cmd/Ctrl+Shift+L.">Load from pi editor</button>
|
|
12389
12418
|
<button id="hideStudioHeaderBtn" class="header-visibility-btn" type="button" aria-controls="studioHeader" title="Hide the global Studio header. Restore it from the top-right edge.">Hide header</button>
|
|
12390
12419
|
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide the Studio header and secondary controls. Shortcut: F9.">Zen</button>
|
|
12391
12420
|
</div>
|
|
@@ -12426,7 +12455,7 @@ ${cssVarsBlock}
|
|
|
12426
12455
|
</div>
|
|
12427
12456
|
<div class="source-actions">
|
|
12428
12457
|
<div class="source-actions-row">
|
|
12429
|
-
<button id="sendRunBtn" type="button" title="Run editor text. While a direct run is active, this button becomes Stop. Cmd/Ctrl+Enter queues steering from the current editor text. Stop the active request with Esc.">Run editor text</button>
|
|
12458
|
+
<button id="sendRunBtn" type="button" title="Run editor text. If Studio loaded or staged an unchanged Pi terminal draft, clear that draft after Pi accepts the run. While a direct run is active, this button becomes Stop. Cmd/Ctrl+Enter queues steering from the current editor text. Stop the active request with Esc.">Run editor text</button>
|
|
12430
12459
|
<button id="queueSteerBtn" type="button" title="Queue steering is available while Run editor text is active." disabled>Queue steering</button>
|
|
12431
12460
|
</div>
|
|
12432
12461
|
<div class="source-actions-row repl-action-line" hidden>
|
|
@@ -12453,7 +12482,8 @@ ${cssVarsBlock}
|
|
|
12453
12482
|
<option value="current" selected>Suggestion model: current Pi model</option>
|
|
12454
12483
|
</select>
|
|
12455
12484
|
<button id="openCompanionBtn" type="button" title="Open a blank editor-only Studio tab.">New editor tab</button>
|
|
12456
|
-
<button id="sendEditorBtn" type="button">Send current text to Pi editor</button>
|
|
12485
|
+
<button id="sendEditorBtn" type="button" title="Replace the Pi terminal input draft with the current Studio text. A later Studio Run clears that staged draft only if it remains unchanged.">Send current text to Pi editor</button>
|
|
12486
|
+
<button id="clearPiEditorBtn" type="button" title="Clear text currently waiting in the Pi terminal input editor without changing Studio text or conversation history.">Clear Pi editor text…</button>
|
|
12457
12487
|
</div>
|
|
12458
12488
|
<div class="source-actions-row">
|
|
12459
12489
|
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
|
|
@@ -12759,6 +12789,7 @@ ${cssVarsBlock}
|
|
|
12759
12789
|
<dl>
|
|
12760
12790
|
<div><dt>Cmd/Ctrl+S</dt><dd>Save editor when the disk revision still matches</dd></div>
|
|
12761
12791
|
<div><dt>Cmd/Ctrl+Shift+S</dt><dd>Save editor as a new file</dd></div>
|
|
12792
|
+
<div><dt>Cmd/Ctrl+Shift+L</dt><dd>Load the current Pi terminal input draft without clearing it</dd></div>
|
|
12762
12793
|
<div class="shortcuts-full-only"><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
12763
12794
|
<div><dt>Option/Alt+Tab or Cmd/Ctrl+Shift+Space</dt><dd>Suggest a completion at the editor cursor</dd></div>
|
|
12764
12795
|
<div><dt>Tab</dt><dd>Insert a visible completion suggestion; otherwise indent selected editor text</dd></div>
|
|
@@ -13649,6 +13680,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
13649
13680
|
}
|
|
13650
13681
|
};
|
|
13651
13682
|
|
|
13683
|
+
const reportPiEditorDraftDisposition = (
|
|
13684
|
+
client: WebSocket,
|
|
13685
|
+
requestId: string,
|
|
13686
|
+
snapshot: StudioPiEditorDraftSnapshot | undefined,
|
|
13687
|
+
) => {
|
|
13688
|
+
if (!snapshot) return;
|
|
13689
|
+
const result = consumeStudioPiEditorDraftSnapshot(lastCommandCtx?.ui, snapshot);
|
|
13690
|
+
if (result.status === "not-requested") return;
|
|
13691
|
+
emitDebugEvent("pi_editor_draft_disposition", { requestId, outcome: result.status });
|
|
13692
|
+
sendToClient(client, {
|
|
13693
|
+
type: "pi_editor_draft_result",
|
|
13694
|
+
requestId,
|
|
13695
|
+
outcome: result.status,
|
|
13696
|
+
piEditorDraftSnapshot: snapshot,
|
|
13697
|
+
});
|
|
13698
|
+
};
|
|
13699
|
+
|
|
13652
13700
|
const broadcast = (payload: unknown) => {
|
|
13653
13701
|
if (!serverState) return;
|
|
13654
13702
|
const serialized = JSON.stringify(payload);
|
|
@@ -15743,6 +15791,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15743
15791
|
queuedSteeringCount: getQueuedStudioSteeringCount(),
|
|
15744
15792
|
});
|
|
15745
15793
|
broadcastState();
|
|
15794
|
+
reportPiEditorDraftDisposition(client, msg.requestId, msg.piEditorDraftSnapshot);
|
|
15746
15795
|
} catch (error) {
|
|
15747
15796
|
queuedStudioDirectRequests = queuedStudioDirectRequests.filter((request) => request.requestId !== msg.requestId);
|
|
15748
15797
|
if (studioDirectRunChain?.steeringPrompts.length) {
|
|
@@ -15766,6 +15815,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15766
15815
|
|
|
15767
15816
|
try {
|
|
15768
15817
|
pi.sendUserMessage(msg.text);
|
|
15818
|
+
reportPiEditorDraftDisposition(client, msg.requestId, msg.piEditorDraftSnapshot);
|
|
15769
15819
|
} catch (error) {
|
|
15770
15820
|
clearStudioDirectRunState();
|
|
15771
15821
|
clearActiveRequest();
|
|
@@ -16510,7 +16560,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
16510
16560
|
sendToClient(client, {
|
|
16511
16561
|
type: "editor_loaded",
|
|
16512
16562
|
requestId: msg.requestId,
|
|
16513
|
-
message: "Draft loaded into pi editor.",
|
|
16563
|
+
message: "Draft loaded into pi editor. A Studio Run will clear it if it remains unchanged.",
|
|
16564
|
+
piEditorDraftSnapshot: createStudioPiEditorDraftSnapshot(msg.content),
|
|
16514
16565
|
});
|
|
16515
16566
|
} catch (error) {
|
|
16516
16567
|
sendToClient(client, {
|
|
@@ -16546,6 +16597,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16546
16597
|
type: "editor_snapshot",
|
|
16547
16598
|
requestId: msg.requestId,
|
|
16548
16599
|
content,
|
|
16600
|
+
piEditorDraftSnapshot: createStudioPiEditorDraftSnapshot(content),
|
|
16549
16601
|
});
|
|
16550
16602
|
} catch (error) {
|
|
16551
16603
|
sendToClient(client, {
|
|
@@ -16556,6 +16608,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
16556
16608
|
}
|
|
16557
16609
|
return;
|
|
16558
16610
|
}
|
|
16611
|
+
|
|
16612
|
+
if (msg.type === "clear_pi_editor_request") {
|
|
16613
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
16614
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
16615
|
+
return;
|
|
16616
|
+
}
|
|
16617
|
+
if (isStudioBusy()) {
|
|
16618
|
+
sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
|
|
16619
|
+
return;
|
|
16620
|
+
}
|
|
16621
|
+
if (!lastCommandCtx || !lastCommandCtx.hasUI) {
|
|
16622
|
+
sendToClient(client, {
|
|
16623
|
+
type: "error",
|
|
16624
|
+
requestId: msg.requestId,
|
|
16625
|
+
message: "No interactive pi editor context is available.",
|
|
16626
|
+
});
|
|
16627
|
+
return;
|
|
16628
|
+
}
|
|
16629
|
+
|
|
16630
|
+
try {
|
|
16631
|
+
const hadContent = lastCommandCtx.ui.getEditorText().length > 0;
|
|
16632
|
+
if (hadContent) lastCommandCtx.ui.setEditorText("");
|
|
16633
|
+
lastCommandCtx.ui.notify(hadContent ? "Pi editor text cleared from Studio." : "Pi editor text is already empty.", "info");
|
|
16634
|
+
sendToClient(client, {
|
|
16635
|
+
type: "pi_editor_cleared",
|
|
16636
|
+
requestId: msg.requestId,
|
|
16637
|
+
cleared: hadContent,
|
|
16638
|
+
});
|
|
16639
|
+
} catch (error) {
|
|
16640
|
+
sendToClient(client, {
|
|
16641
|
+
type: "error",
|
|
16642
|
+
requestId: msg.requestId,
|
|
16643
|
+
message: `Failed to clear pi editor text: ${error instanceof Error ? error.message : String(error)}`,
|
|
16644
|
+
});
|
|
16645
|
+
}
|
|
16646
|
+
return;
|
|
16647
|
+
}
|
|
16559
16648
|
};
|
|
16560
16649
|
|
|
16561
16650
|
const disposePreparedPdfExport = (entry: PreparedStudioPdfExport | null | undefined) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.59",
|
|
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",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const STUDIO_PI_EDITOR_DRAFT_FINGERPRINT_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
4
|
+
|
|
5
|
+
export function createStudioPiEditorDraftSnapshot(content) {
|
|
6
|
+
const buffer = Buffer.from(String(content ?? ""), "utf8");
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
fingerprint: `sha256:${createHash("sha256").update(buffer).digest("hex")}`,
|
|
9
|
+
byteLength: buffer.length,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeStudioPiEditorDraftSnapshot(value) {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
15
|
+
const fingerprint = typeof value.fingerprint === "string"
|
|
16
|
+
? value.fingerprint.trim().toLowerCase()
|
|
17
|
+
: "";
|
|
18
|
+
const byteLength = value.byteLength;
|
|
19
|
+
if (!STUDIO_PI_EDITOR_DRAFT_FINGERPRINT_PATTERN.test(fingerprint)) return null;
|
|
20
|
+
if (typeof byteLength !== "number" || !Number.isSafeInteger(byteLength) || byteLength < 0) return null;
|
|
21
|
+
return Object.freeze({ fingerprint, byteLength });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function studioPiEditorDraftMatchesSnapshot(content, snapshot) {
|
|
25
|
+
const expected = normalizeStudioPiEditorDraftSnapshot(snapshot);
|
|
26
|
+
if (!expected) return false;
|
|
27
|
+
const current = createStudioPiEditorDraftSnapshot(content);
|
|
28
|
+
return current.byteLength === expected.byteLength && current.fingerprint === expected.fingerprint;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function consumeStudioPiEditorDraftSnapshot(ui, snapshot) {
|
|
32
|
+
const expected = normalizeStudioPiEditorDraftSnapshot(snapshot);
|
|
33
|
+
if (!expected) return Object.freeze({ status: "not-requested" });
|
|
34
|
+
if (!ui || typeof ui.getEditorText !== "function" || typeof ui.setEditorText !== "function") {
|
|
35
|
+
return Object.freeze({ status: "unavailable" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const current = String(ui.getEditorText() ?? "");
|
|
40
|
+
if (current.length === 0) return Object.freeze({ status: "already-empty" });
|
|
41
|
+
if (!studioPiEditorDraftMatchesSnapshot(current, expected)) {
|
|
42
|
+
return Object.freeze({ status: "changed" });
|
|
43
|
+
}
|
|
44
|
+
ui.setEditorText("");
|
|
45
|
+
return Object.freeze({ status: "cleared" });
|
|
46
|
+
} catch {
|
|
47
|
+
return Object.freeze({ status: "unavailable" });
|
|
48
|
+
}
|
|
49
|
+
}
|