pi-studio 0.9.57 → 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 +25 -0
- package/README.md +19 -5
- package/ROADMAP.md +20 -1
- package/client/studio-client.js +695 -64
- package/client/studio.css +172 -0
- package/index.ts +110 -9
- package/package.json +1 -1
- package/shared/studio-pi-editor-draft.js +49 -0
package/client/studio.css
CHANGED
|
@@ -362,6 +362,55 @@
|
|
|
362
362
|
user-select: none;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
body[data-studio-layout="editor-top"] main,
|
|
366
|
+
body[data-studio-layout="response-top"] main {
|
|
367
|
+
grid-template-columns: minmax(0, 1fr);
|
|
368
|
+
gap: 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
body[data-studio-layout="editor-top"] main {
|
|
372
|
+
grid-template-rows: minmax(140px, var(--studio-left-pane-fr, 1fr)) 10px minmax(140px, var(--studio-right-pane-fr, 1fr));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
body[data-studio-layout="response-top"] main {
|
|
376
|
+
grid-template-rows: minmax(140px, var(--studio-right-pane-fr, 1fr)) 10px minmax(140px, var(--studio-left-pane-fr, 1fr));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
body[data-studio-layout="editor-top"] .pane-resize-handle,
|
|
380
|
+
body[data-studio-layout="response-top"] .pane-resize-handle {
|
|
381
|
+
display: block;
|
|
382
|
+
align-self: stretch;
|
|
383
|
+
min-width: 0;
|
|
384
|
+
min-height: 10px;
|
|
385
|
+
cursor: row-resize;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
body[data-studio-layout="editor-top"] .pane-resize-handle::before,
|
|
389
|
+
body[data-studio-layout="response-top"] .pane-resize-handle::before {
|
|
390
|
+
top: 50%;
|
|
391
|
+
right: 8px;
|
|
392
|
+
bottom: auto;
|
|
393
|
+
left: 8px;
|
|
394
|
+
width: auto;
|
|
395
|
+
height: 2px;
|
|
396
|
+
transform: translateY(-50%);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
body[data-studio-layout="editor-top"] .pane-resize-handle:hover::before,
|
|
400
|
+
body[data-studio-layout="editor-top"] .pane-resize-handle:focus-visible::before,
|
|
401
|
+
body[data-studio-layout="editor-top"].pane-resizing .pane-resize-handle::before,
|
|
402
|
+
body[data-studio-layout="response-top"] .pane-resize-handle:hover::before,
|
|
403
|
+
body[data-studio-layout="response-top"] .pane-resize-handle:focus-visible::before,
|
|
404
|
+
body[data-studio-layout="response-top"].pane-resizing .pane-resize-handle::before {
|
|
405
|
+
width: auto;
|
|
406
|
+
height: 3px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
body[data-studio-layout="editor-top"].pane-resizing,
|
|
410
|
+
body[data-studio-layout="response-top"].pane-resizing {
|
|
411
|
+
cursor: row-resize;
|
|
412
|
+
}
|
|
413
|
+
|
|
365
414
|
section {
|
|
366
415
|
border: 1px solid var(--panel-border);
|
|
367
416
|
border-radius: 10px;
|
|
@@ -387,6 +436,13 @@
|
|
|
387
436
|
gap: 12px;
|
|
388
437
|
}
|
|
389
438
|
|
|
439
|
+
body[data-studio-layout="editor-top"].pane-focus-left main,
|
|
440
|
+
body[data-studio-layout="editor-top"].pane-focus-right main,
|
|
441
|
+
body[data-studio-layout="response-top"].pane-focus-left main,
|
|
442
|
+
body[data-studio-layout="response-top"].pane-focus-right main {
|
|
443
|
+
grid-template-rows: minmax(0, 1fr);
|
|
444
|
+
}
|
|
445
|
+
|
|
390
446
|
body.pane-focus-left #rightPane,
|
|
391
447
|
body.pane-focus-right #leftPane,
|
|
392
448
|
body.pane-focus-left #paneResizeHandle,
|
|
@@ -513,6 +569,7 @@
|
|
|
513
569
|
body[data-studio-mode="editor-only"] #sendRunBtn,
|
|
514
570
|
body[data-studio-mode="editor-only"] #queueSteerBtn,
|
|
515
571
|
body[data-studio-mode="editor-only"] #sendEditorBtn,
|
|
572
|
+
body[data-studio-mode="editor-only"] #clearPiEditorBtn,
|
|
516
573
|
body[data-studio-mode="editor-only"] #lensSelect,
|
|
517
574
|
body[data-studio-mode="editor-only"] #critiqueBtn,
|
|
518
575
|
body[data-studio-mode="editor-only"] #showMeBtn,
|
|
@@ -4579,6 +4636,107 @@
|
|
|
4579
4636
|
background: var(--error-soft);
|
|
4580
4637
|
}
|
|
4581
4638
|
|
|
4639
|
+
.repl-quick-composer {
|
|
4640
|
+
display: flex;
|
|
4641
|
+
flex-direction: column;
|
|
4642
|
+
gap: 8px;
|
|
4643
|
+
padding: 10px;
|
|
4644
|
+
border: 1px solid var(--panel-border);
|
|
4645
|
+
border-radius: 8px;
|
|
4646
|
+
background: color-mix(in srgb, var(--panel-2) 58%, var(--panel));
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
.repl-quick-header,
|
|
4650
|
+
.repl-quick-actions {
|
|
4651
|
+
display: flex;
|
|
4652
|
+
align-items: center;
|
|
4653
|
+
justify-content: space-between;
|
|
4654
|
+
gap: 10px;
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
.repl-quick-header > div,
|
|
4658
|
+
.repl-quick-actions > div {
|
|
4659
|
+
display: flex;
|
|
4660
|
+
align-items: center;
|
|
4661
|
+
gap: 8px;
|
|
4662
|
+
min-width: 0;
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
.repl-quick-target {
|
|
4666
|
+
display: inline-flex;
|
|
4667
|
+
align-items: center;
|
|
4668
|
+
gap: 4px;
|
|
4669
|
+
max-width: min(48vw, 460px);
|
|
4670
|
+
min-height: 0;
|
|
4671
|
+
margin-left: 5px;
|
|
4672
|
+
padding: 2px 4px;
|
|
4673
|
+
overflow: hidden;
|
|
4674
|
+
border-color: transparent;
|
|
4675
|
+
border-radius: 5px;
|
|
4676
|
+
background: transparent;
|
|
4677
|
+
color: var(--muted);
|
|
4678
|
+
font-family: var(--font-mono);
|
|
4679
|
+
font-size: 11px;
|
|
4680
|
+
line-height: 1.25;
|
|
4681
|
+
vertical-align: bottom;
|
|
4682
|
+
white-space: nowrap;
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
.repl-quick-target:not(:disabled):hover,
|
|
4686
|
+
.repl-quick-target:focus-visible {
|
|
4687
|
+
border-color: transparent;
|
|
4688
|
+
background: var(--studio-header-action-hover-bg, var(--panel-2));
|
|
4689
|
+
color: var(--text);
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
.repl-quick-target-label {
|
|
4693
|
+
min-width: 0;
|
|
4694
|
+
overflow: hidden;
|
|
4695
|
+
text-overflow: ellipsis;
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
.repl-quick-target-arrow {
|
|
4699
|
+
flex: 0 0 auto;
|
|
4700
|
+
color: var(--studio-info-text, var(--muted));
|
|
4701
|
+
font-family: var(--font-ui);
|
|
4702
|
+
font-size: 10px;
|
|
4703
|
+
font-weight: 700;
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
.repl-quick-hint,
|
|
4707
|
+
.repl-quick-actions > span {
|
|
4708
|
+
color: var(--muted);
|
|
4709
|
+
font-size: 11px;
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
.repl-quick-composer textarea {
|
|
4713
|
+
box-sizing: border-box;
|
|
4714
|
+
min-height: 72px;
|
|
4715
|
+
max-height: 260px;
|
|
4716
|
+
margin: 0;
|
|
4717
|
+
resize: vertical;
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
.repl-quick-composer textarea:focus-visible {
|
|
4721
|
+
border-color: color-mix(in srgb, var(--accent) 65%, var(--control-border));
|
|
4722
|
+
outline: 2px solid color-mix(in srgb, var(--accent) 28%, transparent);
|
|
4723
|
+
outline-offset: 1px;
|
|
4724
|
+
}
|
|
4725
|
+
|
|
4726
|
+
.repl-quick-actions {
|
|
4727
|
+
align-items: flex-end;
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
.repl-quick-actions > span {
|
|
4731
|
+
line-height: 1.35;
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
.repl-quick-send-btn {
|
|
4735
|
+
border-color: color-mix(in srgb, var(--accent) 58%, var(--control-border));
|
|
4736
|
+
background: color-mix(in srgb, var(--accent) 14%, var(--panel));
|
|
4737
|
+
color: var(--text);
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4582
4740
|
.repl-journal,
|
|
4583
4741
|
.repl-mirror {
|
|
4584
4742
|
border: 1px solid var(--panel-border);
|
|
@@ -5726,6 +5884,20 @@
|
|
|
5726
5884
|
.pane-resize-handle {
|
|
5727
5885
|
display: none;
|
|
5728
5886
|
}
|
|
5887
|
+
|
|
5888
|
+
.repl-quick-header,
|
|
5889
|
+
.repl-quick-actions {
|
|
5890
|
+
align-items: stretch;
|
|
5891
|
+
flex-direction: column;
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
.repl-quick-target {
|
|
5895
|
+
max-width: 66vw;
|
|
5896
|
+
}
|
|
5897
|
+
|
|
5898
|
+
.repl-quick-actions > div {
|
|
5899
|
+
align-self: flex-end;
|
|
5900
|
+
}
|
|
5729
5901
|
}
|
|
5730
5902
|
|
|
5731
5903
|
/* Default Studio layout. The legacy classic layout remains available only via ?uiRefresh=0. */
|
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"
|
|
@@ -11530,10 +11559,11 @@ function stripStudioReplSubmissionEcho(output: string): string {
|
|
|
11530
11559
|
let value = String(output || "").replace(/^\s+/, "");
|
|
11531
11560
|
// The raw tmux mirror should stay raw, but Studio/tool result output should not
|
|
11532
11561
|
// expose the temp-file wrapper used to submit multiline snippets safely. The
|
|
11533
|
-
//
|
|
11534
|
-
// control paths
|
|
11562
|
+
// root fragment catches both legacy long Studio paths and compact private
|
|
11563
|
+
// control paths. Match the Python wrapper by its final double close rather
|
|
11564
|
+
// than the word `globals`, which IPython may split across continuation lines.
|
|
11535
11565
|
const submissionEchoPatterns = [
|
|
11536
|
-
/^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]
|
|
11566
|
+
/^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\)\)\s*$/gm,
|
|
11537
11567
|
/^.*include\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.jl"\)\s*$/gm,
|
|
11538
11568
|
/^.*source\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
|
|
11539
11569
|
/^.*:script\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.ghci"?\s*$/gm,
|
|
@@ -12384,7 +12414,7 @@ ${cssVarsBlock}
|
|
|
12384
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>
|
|
12385
12415
|
<button id="importFileBtn" type="button" title="Import a file as an editable copy.">Import file copy…</button>
|
|
12386
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" />
|
|
12387
|
-
<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>
|
|
12388
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>
|
|
12389
12419
|
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide the Studio header and secondary controls. Shortcut: F9.">Zen</button>
|
|
12390
12420
|
</div>
|
|
@@ -12425,7 +12455,7 @@ ${cssVarsBlock}
|
|
|
12425
12455
|
</div>
|
|
12426
12456
|
<div class="source-actions">
|
|
12427
12457
|
<div class="source-actions-row">
|
|
12428
|
-
<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>
|
|
12429
12459
|
<button id="queueSteerBtn" type="button" title="Queue steering is available while Run editor text is active." disabled>Queue steering</button>
|
|
12430
12460
|
</div>
|
|
12431
12461
|
<div class="source-actions-row repl-action-line" hidden>
|
|
@@ -12452,7 +12482,8 @@ ${cssVarsBlock}
|
|
|
12452
12482
|
<option value="current" selected>Suggestion model: current Pi model</option>
|
|
12453
12483
|
</select>
|
|
12454
12484
|
<button id="openCompanionBtn" type="button" title="Open a blank editor-only Studio tab.">New editor tab</button>
|
|
12455
|
-
<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>
|
|
12456
12487
|
</div>
|
|
12457
12488
|
<div class="source-actions-row">
|
|
12458
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>
|
|
@@ -12508,6 +12539,15 @@ ${cssVarsBlock}
|
|
|
12508
12539
|
<option value="off">Line numbers: Off</option>
|
|
12509
12540
|
<option value="on" selected>Line numbers: On</option>
|
|
12510
12541
|
</select>
|
|
12542
|
+
<select id="studioPaneLayoutSelect" class="studio-flat-select" aria-label="Studio pane layout" title="Choose whether the two main panes sit side by side or in a vertical stack.">
|
|
12543
|
+
<option value="side-by-side">Layout: Side by side</option>
|
|
12544
|
+
<option value="editor-top">Layout: Editor above</option>
|
|
12545
|
+
<option value="response-top">Layout: Response above</option>
|
|
12546
|
+
</select>
|
|
12547
|
+
<select id="activityTrackingSelect" class="studio-flat-select" aria-label="Follow main Pi activity" title="Optionally follow main Pi activity from Working to the response view; off by default.">
|
|
12548
|
+
<option value="off">Follow activity: Off</option>
|
|
12549
|
+
<option value="on">Follow activity: On</option>
|
|
12550
|
+
</select>
|
|
12511
12551
|
<select id="editorFontSizeSelect" class="studio-flat-select" aria-label="Editor text size" title="Adjust raw editor text size.">
|
|
12512
12552
|
<option value="10">Editor text: 10px</option>
|
|
12513
12553
|
<option value="11">Editor text: 11px</option>
|
|
@@ -12607,7 +12647,7 @@ ${cssVarsBlock}
|
|
|
12607
12647
|
<div id="rightSectionHeader" class="section-header">
|
|
12608
12648
|
<div class="section-header-main">
|
|
12609
12649
|
<span id="rightViewSelectWrap" class="studio-header-select-wrap">
|
|
12610
|
-
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+1–8 switches directly between all right-pane views. Cmd/Ctrl+Alt+P/E/W/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.">
|
|
12650
|
+
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+1–8 switches directly between all right-pane views. Cmd/Ctrl+Alt+P/E/W/F/R/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, REPL, and Side questions.">
|
|
12611
12651
|
<option value="markdown">Response (Raw)</option>
|
|
12612
12652
|
<option value="preview" selected>Response (Preview)</option>
|
|
12613
12653
|
<option value="editor-preview">Editor (Preview)</option>
|
|
@@ -12725,6 +12765,7 @@ ${cssVarsBlock}
|
|
|
12725
12765
|
<div><dt>Cmd/Ctrl+Alt+E</dt><dd>Switch the right pane directly to Editor Preview</dd></div>
|
|
12726
12766
|
<div><dt>Cmd/Ctrl+Alt+W</dt><dd>Switch the right pane directly to Working</dd></div>
|
|
12727
12767
|
<div><dt>Cmd/Ctrl+Alt+F</dt><dd>Switch the right pane directly to Files</dd></div>
|
|
12768
|
+
<div><dt>Cmd/Ctrl+Alt+R</dt><dd>Switch the right pane directly to REPL and focus Quick send when a session is selected</dd></div>
|
|
12728
12769
|
<div><dt>Cmd/Ctrl+Alt+Q</dt><dd>Switch the right pane directly to Side questions</dd></div>
|
|
12729
12770
|
<div><dt>F8</dt><dd>Focus editor text</dd></div>
|
|
12730
12771
|
<div><dt>Shift+F8</dt><dd>Focus right-pane content</dd></div>
|
|
@@ -12740,7 +12781,7 @@ ${cssVarsBlock}
|
|
|
12740
12781
|
<div><dt>Alt/Option+=</dt><dd>Increase the active pane's text size when not editing text</dd></div>
|
|
12741
12782
|
<div><dt>Alt/Option+-</dt><dd>Decrease the active pane's text size when not editing text</dd></div>
|
|
12742
12783
|
<div><dt>Alt/Option+0</dt><dd>Reset the active pane's text size when not editing text</dd></div>
|
|
12743
|
-
<div><dt>Cmd/Ctrl+Alt+R</dt><dd>Refresh the focused or visible PDF preview from disk</dd></div>
|
|
12784
|
+
<div><dt>Cmd/Ctrl+Alt+Shift+R</dt><dd>Refresh the focused or visible PDF preview from disk</dd></div>
|
|
12744
12785
|
</dl>
|
|
12745
12786
|
</section>
|
|
12746
12787
|
<section class="shortcuts-group">
|
|
@@ -12748,6 +12789,7 @@ ${cssVarsBlock}
|
|
|
12748
12789
|
<dl>
|
|
12749
12790
|
<div><dt>Cmd/Ctrl+S</dt><dd>Save editor when the disk revision still matches</dd></div>
|
|
12750
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>
|
|
12751
12793
|
<div class="shortcuts-full-only"><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
12752
12794
|
<div><dt>Option/Alt+Tab or Cmd/Ctrl+Shift+Space</dt><dd>Suggest a completion at the editor cursor</dd></div>
|
|
12753
12795
|
<div><dt>Tab</dt><dd>Insert a visible completion suggestion; otherwise indent selected editor text</dd></div>
|
|
@@ -12772,6 +12814,7 @@ ${cssVarsBlock}
|
|
|
12772
12814
|
<section class="shortcuts-group">
|
|
12773
12815
|
<h3>REPL</h3>
|
|
12774
12816
|
<dl>
|
|
12817
|
+
<div><dt>Cmd/Ctrl+Enter</dt><dd>Send exact Quick send text while its REPL composer is focused; Enter adds a new line</dd></div>
|
|
12775
12818
|
<div><dt>Cmd/Ctrl+Shift+Enter</dt><dd>Send selection, chunks, or editor text to the active REPL when the right pane is REPL</dd></div>
|
|
12776
12819
|
</dl>
|
|
12777
12820
|
</section>
|
|
@@ -13637,6 +13680,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
13637
13680
|
}
|
|
13638
13681
|
};
|
|
13639
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
|
+
|
|
13640
13700
|
const broadcast = (payload: unknown) => {
|
|
13641
13701
|
if (!serverState) return;
|
|
13642
13702
|
const serialized = JSON.stringify(payload);
|
|
@@ -15731,6 +15791,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15731
15791
|
queuedSteeringCount: getQueuedStudioSteeringCount(),
|
|
15732
15792
|
});
|
|
15733
15793
|
broadcastState();
|
|
15794
|
+
reportPiEditorDraftDisposition(client, msg.requestId, msg.piEditorDraftSnapshot);
|
|
15734
15795
|
} catch (error) {
|
|
15735
15796
|
queuedStudioDirectRequests = queuedStudioDirectRequests.filter((request) => request.requestId !== msg.requestId);
|
|
15736
15797
|
if (studioDirectRunChain?.steeringPrompts.length) {
|
|
@@ -15754,6 +15815,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15754
15815
|
|
|
15755
15816
|
try {
|
|
15756
15817
|
pi.sendUserMessage(msg.text);
|
|
15818
|
+
reportPiEditorDraftDisposition(client, msg.requestId, msg.piEditorDraftSnapshot);
|
|
15757
15819
|
} catch (error) {
|
|
15758
15820
|
clearStudioDirectRunState();
|
|
15759
15821
|
clearActiveRequest();
|
|
@@ -16498,7 +16560,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
16498
16560
|
sendToClient(client, {
|
|
16499
16561
|
type: "editor_loaded",
|
|
16500
16562
|
requestId: msg.requestId,
|
|
16501
|
-
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),
|
|
16502
16565
|
});
|
|
16503
16566
|
} catch (error) {
|
|
16504
16567
|
sendToClient(client, {
|
|
@@ -16534,6 +16597,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16534
16597
|
type: "editor_snapshot",
|
|
16535
16598
|
requestId: msg.requestId,
|
|
16536
16599
|
content,
|
|
16600
|
+
piEditorDraftSnapshot: createStudioPiEditorDraftSnapshot(content),
|
|
16537
16601
|
});
|
|
16538
16602
|
} catch (error) {
|
|
16539
16603
|
sendToClient(client, {
|
|
@@ -16544,6 +16608,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
16544
16608
|
}
|
|
16545
16609
|
return;
|
|
16546
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
|
+
}
|
|
16547
16648
|
};
|
|
16548
16649
|
|
|
16549
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
|
+
}
|