pi-studio 0.5.28 → 0.5.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.29] — 2026-03-21
8
+
9
+ ### Changed
10
+ - Studio keyboard shortcuts now keep `Cmd/Ctrl+Enter` for running editor text while using `Esc` to stop an active request, and the focus-pane hint/button copy now describes focus mode as a toggle via `F10` or `Cmd/Ctrl+Esc`.
11
+ - While **Run editor text** is active, Studio now exposes a separate **Queue steering** action (and `Cmd/Ctrl+Enter` queues steering) while preserving a visible **Stop** control, and response-history prompt loading now preserves the effective prompt chain for steered responses rather than only the last correction message.
12
+
7
13
  ## [0.5.28] — 2026-03-21
8
14
 
9
15
  ### Changed
@@ -78,6 +78,7 @@
78
78
  const getEditorBtn = document.getElementById("getEditorBtn");
79
79
  const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
80
80
  const sendRunBtn = document.getElementById("sendRunBtn");
81
+ const queueSteerBtn = document.getElementById("queueSteerBtn");
81
82
  const copyDraftBtn = document.getElementById("copyDraftBtn");
82
83
  const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
83
84
  const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
@@ -120,6 +121,8 @@
120
121
  let latestCritiqueNotesNormalized = "";
121
122
  let responseHistory = [];
122
123
  let responseHistoryIndex = -1;
124
+ let studioRunChainActive = false;
125
+ let queuedSteeringCount = 0;
123
126
  let agentBusyFromServer = false;
124
127
  let terminalActivityPhase = "idle";
125
128
  let terminalActivityToolName = "";
@@ -152,6 +155,23 @@
152
155
  return trimmed ? trimmed : null;
153
156
  }
154
157
 
158
+ function applyStudioRunQueueStateFromMessage(message) {
159
+ if (!message || typeof message !== "object") return false;
160
+ let changed = false;
161
+ if (typeof message.studioRunChainActive === "boolean" && studioRunChainActive !== message.studioRunChainActive) {
162
+ studioRunChainActive = message.studioRunChainActive;
163
+ changed = true;
164
+ }
165
+ if (typeof message.queuedSteeringCount === "number" && Number.isFinite(message.queuedSteeringCount)) {
166
+ const nextCount = Math.max(0, Math.floor(message.queuedSteeringCount));
167
+ if (queuedSteeringCount !== nextCount) {
168
+ queuedSteeringCount = nextCount;
169
+ changed = true;
170
+ }
171
+ }
172
+ return changed;
173
+ }
174
+
155
175
  contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
156
176
  contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
157
177
  contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
@@ -383,26 +403,80 @@
383
403
  return "submitting request";
384
404
  }
385
405
 
406
+ function formatQueuedSteeringSuffix() {
407
+ if (!queuedSteeringCount) return "";
408
+ return queuedSteeringCount === 1
409
+ ? " · 1 steering queued"
410
+ : " · " + queuedSteeringCount + " steering queued";
411
+ }
412
+
386
413
  function getStudioBusyStatus(kind) {
387
414
  const action = getStudioActionLabel(kind);
415
+ const queueSuffix = studioRunChainActive ? formatQueuedSteeringSuffix() : "";
388
416
  if (terminalActivityPhase === "tool") {
389
417
  if (terminalActivityLabel) {
390
- return "Studio: " + withEllipsis(terminalActivityLabel);
418
+ return "Studio: " + withEllipsis(terminalActivityLabel) + queueSuffix;
391
419
  }
392
420
  return terminalActivityToolName
393
- ? "Studio: " + action + " (tool: " + terminalActivityToolName + ")…"
394
- : "Studio: " + action + " (running tool)…";
421
+ ? "Studio: " + action + " (tool: " + terminalActivityToolName + ")…" + queueSuffix
422
+ : "Studio: " + action + " (running tool)…" + queueSuffix;
395
423
  }
396
424
  if (terminalActivityPhase === "responding") {
397
425
  if (lastSpecificToolLabel) {
398
- return "Studio: " + lastSpecificToolLabel + " (generating response)…";
426
+ return "Studio: " + lastSpecificToolLabel + " (generating response)…" + queueSuffix;
399
427
  }
400
- return "Studio: " + action + " (generating response)…";
428
+ return "Studio: " + action + " (generating response)…" + queueSuffix;
401
429
  }
402
430
  if (terminalActivityPhase === "running" && lastSpecificToolLabel) {
403
- return "Studio: " + withEllipsis(lastSpecificToolLabel);
431
+ return "Studio: " + withEllipsis(lastSpecificToolLabel) + queueSuffix;
404
432
  }
405
- return "Studio: " + action + "…";
433
+ return "Studio: " + action + "…" + queueSuffix;
434
+ }
435
+
436
+ function getHistoryPromptSourceLabel(item) {
437
+ if (!item || !item.promptMode) return null;
438
+ const steeringCount = typeof item.promptSteeringCount === "number" && Number.isFinite(item.promptSteeringCount)
439
+ ? Math.max(0, Math.floor(item.promptSteeringCount))
440
+ : 0;
441
+ if (item.promptMode === "run") return "original run";
442
+ if (item.promptMode !== "effective") return null;
443
+ if (steeringCount <= 0) return "original run";
444
+ return steeringCount === 1
445
+ ? "original run + 1 steering message"
446
+ : "original run + " + steeringCount + " steering messages";
447
+ }
448
+
449
+ function getHistoryPromptButtonLabel(item) {
450
+ if (!item || !item.prompt || !String(item.prompt).trim()) {
451
+ return "Response prompt unavailable";
452
+ }
453
+ if (item.promptMode === "effective") {
454
+ return "Load effective prompt into editor";
455
+ }
456
+ if (item.promptMode === "run") {
457
+ return "Load run prompt into editor";
458
+ }
459
+ return "Load response prompt into editor";
460
+ }
461
+
462
+ function getHistoryPromptLoadedStatus(item) {
463
+ if (!item || !item.prompt || !String(item.prompt).trim()) {
464
+ return "Prompt unavailable for the selected response.";
465
+ }
466
+ if (item.promptMode === "effective") {
467
+ return "Loaded effective prompt into editor.";
468
+ }
469
+ if (item.promptMode === "run") {
470
+ return "Loaded run prompt into editor.";
471
+ }
472
+ return "Loaded response prompt into editor.";
473
+ }
474
+
475
+ function getHistoryPromptSourceStateLabel(item) {
476
+ if (!item || !item.prompt || !String(item.prompt).trim()) return "response prompt";
477
+ if (item.promptMode === "effective") return "effective prompt";
478
+ if (item.promptMode === "run") return "run prompt";
479
+ return "response prompt";
406
480
  }
407
481
 
408
482
  function shouldAnimateFooterSpinner() {
@@ -715,8 +789,8 @@
715
789
  btn.setAttribute("aria-pressed", isFocusedPane ? "true" : "false");
716
790
  btn.textContent = isFocusedPane ? "Exit focus" : "Focus pane";
717
791
  btn.title = isFocusedPane
718
- ? "Exit focus mode for the " + paneName + " pane."
719
- : "Show only the " + paneName + " pane. Shortcut: Cmd/Ctrl+Esc or F10.";
792
+ ? "Return to the two-pane layout. Shortcut: F10 or Cmd/Ctrl+Esc."
793
+ : "Show only the " + paneName + " pane. Shortcut: F10 or Cmd/Ctrl+Esc.";
720
794
  });
721
795
  }
722
796
 
@@ -754,7 +828,7 @@
754
828
  setActivePane(pane);
755
829
  paneFocusTarget = pane;
756
830
  applyPaneFocusClasses();
757
- setStatus("Focus mode: " + paneLabel(pane) + " pane (Esc to exit).");
831
+ setStatus("Focus mode: " + paneLabel(pane) + " pane. Toggle with F10 or Cmd/Ctrl+Esc.");
758
832
  }
759
833
 
760
834
  function togglePaneFocus() {
@@ -777,7 +851,7 @@
777
851
  }
778
852
 
779
853
  function handlePaneShortcut(event) {
780
- if (!event) return;
854
+ if (!event || event.defaultPrevented) return;
781
855
 
782
856
  const key = typeof event.key === "string" ? event.key : "";
783
857
  const isToggleShortcut =
@@ -797,9 +871,16 @@
797
871
  && !event.altKey
798
872
  && !event.shiftKey
799
873
  ) {
874
+ const activeKind = getAbortablePendingKind();
875
+ if (activeKind === "direct" || activeKind === "critique") {
876
+ event.preventDefault();
877
+ requestCancelForPendingRequest(activeKind);
878
+ return;
879
+ }
800
880
  if (exitPaneFocus()) {
801
881
  event.preventDefault();
802
882
  }
883
+ return;
803
884
  }
804
885
 
805
886
  if (
@@ -808,11 +889,16 @@
808
889
  && !event.altKey
809
890
  && !event.shiftKey
810
891
  && activePane === "left"
811
- && sendRunBtn
812
- && !sendRunBtn.disabled
813
892
  ) {
814
- event.preventDefault();
815
- sendRunBtn.click();
893
+ if (queueSteerBtn && !queueSteerBtn.disabled) {
894
+ event.preventDefault();
895
+ queueSteerBtn.click();
896
+ return;
897
+ }
898
+ if (sendRunBtn && !sendRunBtn.disabled) {
899
+ event.preventDefault();
900
+ sendRunBtn.click();
901
+ }
816
902
  }
817
903
  }
818
904
 
@@ -851,6 +937,18 @@
851
937
  const thinking = typeof item.thinking === "string"
852
938
  ? item.thinking
853
939
  : (item.thinking == null ? null : String(item.thinking));
940
+ const promptMode = item.promptMode === "run" || item.promptMode === "effective"
941
+ ? item.promptMode
942
+ : "response";
943
+ const promptTriggerKind = item.promptTriggerKind === "run" || item.promptTriggerKind === "steer"
944
+ ? item.promptTriggerKind
945
+ : null;
946
+ const promptSteeringCount = typeof item.promptSteeringCount === "number" && Number.isFinite(item.promptSteeringCount)
947
+ ? Math.max(0, Math.floor(item.promptSteeringCount))
948
+ : 0;
949
+ const promptTriggerText = typeof item.promptTriggerText === "string"
950
+ ? item.promptTriggerText
951
+ : (item.promptTriggerText == null ? null : String(item.promptTriggerText));
854
952
 
855
953
  return {
856
954
  id,
@@ -859,6 +957,10 @@
859
957
  timestamp,
860
958
  kind: normalizeHistoryKind(item.kind),
861
959
  prompt,
960
+ promptMode,
961
+ promptTriggerKind,
962
+ promptSteeringCount,
963
+ promptTriggerText,
862
964
  };
863
965
  }
864
966
 
@@ -904,9 +1006,13 @@
904
1006
  const hasPrompt = Boolean(selectedItem && typeof selectedItem.prompt === "string" && selectedItem.prompt.trim());
905
1007
  if (loadHistoryPromptBtn) {
906
1008
  loadHistoryPromptBtn.disabled = uiBusy || !hasPrompt;
907
- loadHistoryPromptBtn.textContent = hasPrompt
908
- ? "Load response prompt into editor"
909
- : "Response prompt unavailable";
1009
+ loadHistoryPromptBtn.textContent = getHistoryPromptButtonLabel(selectedItem);
1010
+ const promptSourceLabel = getHistoryPromptSourceLabel(selectedItem);
1011
+ loadHistoryPromptBtn.title = hasPrompt
1012
+ ? (promptSourceLabel
1013
+ ? "Load the " + promptSourceLabel + " prompt chain that generated the selected response into the editor."
1014
+ : "Load the prompt that generated the selected response into the editor.")
1015
+ : "Prompt unavailable for the selected response.";
910
1016
  }
911
1017
  }
912
1018
 
@@ -2806,29 +2912,43 @@
2806
2912
 
2807
2913
  function syncRunAndCritiqueButtons() {
2808
2914
  const activeKind = getAbortablePendingKind();
2809
- const sendRunIsStop = activeKind === "direct";
2915
+ const directIsStop = activeKind === "direct";
2810
2916
  const critiqueIsStop = activeKind === "critique";
2917
+ const canQueueSteering = studioRunChainActive && !critiqueIsStop;
2811
2918
 
2812
2919
  if (sendRunBtn) {
2813
- sendRunBtn.textContent = sendRunIsStop ? "Stop" : "Run editor text";
2814
- sendRunBtn.classList.toggle("request-stop-active", sendRunIsStop);
2815
- sendRunBtn.disabled = sendRunIsStop ? wsState === "Disconnected" : (uiBusy || critiqueIsStop);
2816
- sendRunBtn.title = sendRunIsStop
2817
- ? "Stop the running editor-text request."
2920
+ sendRunBtn.textContent = directIsStop ? "Stop" : "Run editor text";
2921
+ sendRunBtn.classList.toggle("request-stop-active", directIsStop);
2922
+ sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
2923
+ sendRunBtn.title = directIsStop
2924
+ ? "Stop the active run. Shortcut: Esc."
2818
2925
  : (annotationsEnabled
2819
- ? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter."
2820
- : "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter.");
2926
+ ? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc."
2927
+ : "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc.");
2928
+ }
2929
+
2930
+ if (queueSteerBtn) {
2931
+ queueSteerBtn.hidden = false;
2932
+ queueSteerBtn.disabled = wsState === "Disconnected" || !canQueueSteering;
2933
+ queueSteerBtn.classList.remove("request-stop-active");
2934
+ queueSteerBtn.title = canQueueSteering
2935
+ ? (annotationsEnabled
2936
+ ? "Queue the current editor text as a steering message for the active run. Shortcut: Cmd/Ctrl+Enter."
2937
+ : "Queue the current editor text as a steering message for the active run after stripping [an: ...] markers. Shortcut: Cmd/Ctrl+Enter.")
2938
+ : "Queue steering is available while Run editor text is active.";
2821
2939
  }
2822
2940
 
2823
2941
  if (critiqueBtn) {
2824
2942
  critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique editor text";
2825
2943
  critiqueBtn.classList.toggle("request-stop-active", critiqueIsStop);
2826
- critiqueBtn.disabled = critiqueIsStop ? wsState === "Disconnected" : (uiBusy || sendRunIsStop);
2944
+ critiqueBtn.disabled = critiqueIsStop ? wsState === "Disconnected" : (uiBusy || canQueueSteering);
2827
2945
  critiqueBtn.title = critiqueIsStop
2828
- ? "Stop the running critique request."
2829
- : (annotationsEnabled
2830
- ? "Critique editor text as-is (includes [an: ...] markers)."
2831
- : "Critique editor text with [an: ...] markers stripped.");
2946
+ ? "Stop the running critique request. Shortcut: Esc."
2947
+ : (canQueueSteering
2948
+ ? "Critique queueing is not supported while Run editor text is active."
2949
+ : (annotationsEnabled
2950
+ ? "Critique editor text as-is (includes [an: ...] markers)."
2951
+ : "Critique editor text with [an: ...] markers stripped."));
2832
2952
  }
2833
2953
  }
2834
2954
 
@@ -2973,6 +3093,7 @@
2973
3093
  if (typeof message.terminalSessionLabel === "string") {
2974
3094
  terminalSessionLabel = message.terminalSessionLabel;
2975
3095
  }
3096
+ applyStudioRunQueueStateFromMessage(message);
2976
3097
  updateFooterMeta();
2977
3098
  setBusy(busy);
2978
3099
  setWsState(busy ? "Submitting" : "Ready");
@@ -3045,6 +3166,8 @@
3045
3166
  if (busy) {
3046
3167
  if (agentBusyFromServer && stickyStudioKind) {
3047
3168
  setStatus(getStudioBusyStatus(stickyStudioKind), "warning");
3169
+ } else if (agentBusyFromServer && studioRunChainActive) {
3170
+ setStatus(getStudioBusyStatus("direct"), "warning");
3048
3171
  } else if (agentBusyFromServer) {
3049
3172
  setStatus(getTerminalBusyStatus(), "warning");
3050
3173
  } else {
@@ -3065,6 +3188,9 @@
3065
3188
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
3066
3189
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
3067
3190
  stickyStudioKind = pendingKind;
3191
+ if (pendingKind === "direct") {
3192
+ studioRunChainActive = true;
3193
+ }
3068
3194
  if (pendingKind === "compact") {
3069
3195
  compactInProgress = true;
3070
3196
  }
@@ -3074,6 +3200,14 @@
3074
3200
  return;
3075
3201
  }
3076
3202
 
3203
+ if (message.type === "request_queued") {
3204
+ studioRunChainActive = true;
3205
+ applyStudioRunQueueStateFromMessage(message);
3206
+ syncActionButtons();
3207
+ setStatus("Steering queued.", "success");
3208
+ return;
3209
+ }
3210
+
3077
3211
  if (message.type === "compaction_completed") {
3078
3212
  if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
3079
3213
  pendingRequestId = null;
@@ -3310,6 +3444,7 @@
3310
3444
  if (typeof message.terminalSessionLabel === "string") {
3311
3445
  terminalSessionLabel = message.terminalSessionLabel;
3312
3446
  }
3447
+ applyStudioRunQueueStateFromMessage(message);
3313
3448
  updateFooterMeta();
3314
3449
 
3315
3450
  if (typeof message.activeRequestId === "string" && message.activeRequestId.length > 0) {
@@ -3346,6 +3481,8 @@
3346
3481
  if (busy) {
3347
3482
  if (agentBusyFromServer && stickyStudioKind) {
3348
3483
  setStatus(getStudioBusyStatus(stickyStudioKind), "warning");
3484
+ } else if (agentBusyFromServer && studioRunChainActive) {
3485
+ setStatus(getStudioBusyStatus("direct"), "warning");
3349
3486
  } else if (agentBusyFromServer) {
3350
3487
  setStatus(getTerminalBusyStatus(), "warning");
3351
3488
  } else {
@@ -3816,8 +3953,8 @@
3816
3953
  }
3817
3954
 
3818
3955
  setEditorText(prompt, { preserveScroll: false, preserveSelection: false });
3819
- setSourceState({ source: "blank", label: "response prompt", path: null });
3820
- setStatus("Loaded response prompt into editor.", "success");
3956
+ setSourceState({ source: "blank", label: getHistoryPromptSourceStateLabel(item), path: null });
3957
+ setStatus(getHistoryPromptLoadedStatus(item), "success");
3821
3958
  });
3822
3959
  }
3823
3960
 
@@ -4118,6 +4255,30 @@
4118
4255
  }
4119
4256
  });
4120
4257
 
4258
+ if (queueSteerBtn) {
4259
+ queueSteerBtn.addEventListener("click", () => {
4260
+ const prepared = prepareEditorTextForSend(sourceTextEl.value);
4261
+ if (!prepared.trim()) {
4262
+ setStatus("Editor is empty. Nothing to queue.", "warning");
4263
+ return;
4264
+ }
4265
+ if (!studioRunChainActive) {
4266
+ setStatus("Queue steering is only available while Run editor text is active.", "warning");
4267
+ return;
4268
+ }
4269
+
4270
+ const requestId = makeRequestId();
4271
+ clearTitleAttention();
4272
+ const sent = sendMessage({
4273
+ type: "send_run_request",
4274
+ requestId,
4275
+ text: prepared,
4276
+ });
4277
+ if (!sent) return;
4278
+ setStatus("Queueing steering…", "warning");
4279
+ });
4280
+ }
4281
+
4121
4282
  copyDraftBtn.addEventListener("click", async () => {
4122
4283
  const content = sourceTextEl.value;
4123
4284
  if (!content.trim()) {
package/client/studio.css CHANGED
@@ -87,6 +87,7 @@
87
87
  }
88
88
 
89
89
  #sendRunBtn,
90
+ #queueSteerBtn,
90
91
  #critiqueBtn {
91
92
  min-width: 10rem;
92
93
  display: inline-flex;
@@ -95,6 +96,7 @@
95
96
  }
96
97
 
97
98
  #sendRunBtn:not(:disabled):not(.request-stop-active),
99
+ #queueSteerBtn:not(:disabled),
98
100
  #loadResponseBtn:not(:disabled):not([hidden]) {
99
101
  background: var(--accent);
100
102
  border-color: var(--accent);
@@ -103,6 +105,7 @@
103
105
  }
104
106
 
105
107
  #sendRunBtn:not(:disabled):not(.request-stop-active):hover,
108
+ #queueSteerBtn:not(:disabled):hover,
106
109
  #loadResponseBtn:not(:disabled):not([hidden]):hover {
107
110
  filter: brightness(0.95);
108
111
  }
package/index.ts CHANGED
@@ -14,6 +14,8 @@ type RequestedLens = Lens | "auto";
14
14
  type StudioRequestKind = "critique" | "annotation" | "direct" | "compact";
15
15
  type StudioSourceKind = "file" | "last-response" | "blank";
16
16
  type TerminalActivityPhase = "idle" | "running" | "tool" | "responding";
17
+ type StudioPromptMode = "response" | "run" | "effective";
18
+ type StudioPromptTriggerKind = "run" | "steer";
17
19
 
18
20
  const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
19
21
  const STUDIO_CLIENT_URL = new URL("./client/studio-client.js", import.meta.url);
@@ -26,10 +28,17 @@ interface StudioServerState {
26
28
  token: string;
27
29
  }
28
30
 
29
- interface ActiveStudioRequest {
31
+ interface StudioPromptDescriptor {
32
+ prompt: string | null;
33
+ promptMode: StudioPromptMode;
34
+ promptTriggerKind: StudioPromptTriggerKind | null;
35
+ promptSteeringCount: number;
36
+ promptTriggerText: string | null;
37
+ }
38
+
39
+ interface ActiveStudioRequest extends StudioPromptDescriptor {
30
40
  id: string;
31
41
  kind: StudioRequestKind;
32
- prompt: string | null;
33
42
  timer: NodeJS.Timeout;
34
43
  startedAt: number;
35
44
  }
@@ -41,13 +50,28 @@ interface LastStudioResponse {
41
50
  kind: StudioRequestKind;
42
51
  }
43
52
 
44
- interface StudioResponseHistoryItem {
53
+ interface StudioResponseHistoryItem extends StudioPromptDescriptor {
45
54
  id: string;
46
55
  markdown: string;
47
56
  thinking: string | null;
48
57
  timestamp: number;
49
58
  kind: StudioRequestKind;
50
- prompt: string | null;
59
+ }
60
+
61
+ interface StudioDirectRunChain {
62
+ id: string;
63
+ basePrompt: string;
64
+ steeringPrompts: string[];
65
+ }
66
+
67
+ interface QueuedStudioDirectRequest extends StudioPromptDescriptor {
68
+ requestId: string;
69
+ queuedAt: number;
70
+ }
71
+
72
+ interface PersistedStudioPromptMetadata extends StudioPromptDescriptor {
73
+ version: 1;
74
+ requestKind: "direct";
51
75
  }
52
76
 
53
77
  interface StudioContextUsageSnapshot {
@@ -173,6 +197,7 @@ const STUDIO_TERMINAL_NOTIFY_TITLE = "pi Studio";
173
197
  const CMUX_STUDIO_STATUS_KEY = "pi_studio";
174
198
  const CMUX_STUDIO_STATUS_COLOR_DARK = "#5ea1ff";
175
199
  const CMUX_STUDIO_STATUS_COLOR_LIGHT = "#0047ab";
200
+ const STUDIO_PROMPT_METADATA_CUSTOM_TYPE = "pi-studio/direct-prompt";
176
201
 
177
202
  const PDF_PREAMBLE = `\\usepackage{titlesec}
178
203
  \\titleformat{\\section}{\\Large\\bfseries\\sffamily}{}{0pt}{}[\\vspace{3pt}\\titlerule\\vspace{12pt}]
@@ -3617,6 +3642,95 @@ function normalizePromptText(text: string | null | undefined): string | null {
3617
3642
  return trimmed.length > 0 ? trimmed : null;
3618
3643
  }
3619
3644
 
3645
+ function buildStudioPromptDescriptor(
3646
+ prompt: string | null,
3647
+ promptMode: StudioPromptMode = "response",
3648
+ promptTriggerKind: StudioPromptTriggerKind | null = null,
3649
+ promptSteeringCount = 0,
3650
+ promptTriggerText: string | null = null,
3651
+ ): StudioPromptDescriptor {
3652
+ return {
3653
+ prompt: normalizePromptText(prompt),
3654
+ promptMode,
3655
+ promptTriggerKind,
3656
+ promptSteeringCount: Number.isFinite(promptSteeringCount) && promptSteeringCount > 0
3657
+ ? Math.max(0, Math.floor(promptSteeringCount))
3658
+ : 0,
3659
+ promptTriggerText: normalizePromptText(promptTriggerText),
3660
+ };
3661
+ }
3662
+
3663
+ function buildStudioEffectivePrompt(basePrompt: string | null | undefined, steeringPrompts: Array<string | null | undefined>): string | null {
3664
+ const normalizedBasePrompt = normalizePromptText(basePrompt);
3665
+ const normalizedSteeringPrompts = steeringPrompts
3666
+ .map((prompt) => normalizePromptText(prompt))
3667
+ .filter((prompt): prompt is string => Boolean(prompt));
3668
+
3669
+ if (!normalizedBasePrompt) {
3670
+ if (normalizedSteeringPrompts.length === 0) return null;
3671
+ return normalizedSteeringPrompts.join("\n\n");
3672
+ }
3673
+ if (normalizedSteeringPrompts.length === 0) return normalizedBasePrompt;
3674
+
3675
+ const sections = ["## Original run prompt\n\n" + normalizedBasePrompt];
3676
+ for (let i = 0; i < normalizedSteeringPrompts.length; i++) {
3677
+ sections.push(`## Steering ${i + 1}\n\n${normalizedSteeringPrompts[i]}`);
3678
+ }
3679
+ return sections.join("\n\n").trim();
3680
+ }
3681
+
3682
+ function buildStudioDirectRunPromptDescriptor(prompt: string): StudioPromptDescriptor {
3683
+ const normalizedPrompt = normalizePromptText(prompt);
3684
+ return buildStudioPromptDescriptor(normalizedPrompt, "run", "run", 0, normalizedPrompt);
3685
+ }
3686
+
3687
+ function buildStudioQueuedSteerPromptDescriptor(chain: StudioDirectRunChain, triggerPrompt: string): StudioPromptDescriptor {
3688
+ const normalizedTriggerPrompt = normalizePromptText(triggerPrompt);
3689
+ const steeringPrompts = [...chain.steeringPrompts, normalizedTriggerPrompt].filter((prompt): prompt is string => Boolean(prompt));
3690
+ const effectivePrompt = buildStudioEffectivePrompt(chain.basePrompt, steeringPrompts);
3691
+ return buildStudioPromptDescriptor(effectivePrompt, "effective", "steer", steeringPrompts.length, normalizedTriggerPrompt);
3692
+ }
3693
+
3694
+ function buildPersistedStudioPromptMetadata(promptDescriptor: StudioPromptDescriptor): PersistedStudioPromptMetadata {
3695
+ return {
3696
+ version: 1,
3697
+ requestKind: "direct",
3698
+ prompt: promptDescriptor.prompt,
3699
+ promptMode: promptDescriptor.promptMode,
3700
+ promptTriggerKind: promptDescriptor.promptTriggerKind,
3701
+ promptSteeringCount: promptDescriptor.promptSteeringCount,
3702
+ promptTriggerText: promptDescriptor.promptTriggerText,
3703
+ };
3704
+ }
3705
+
3706
+ function extractPersistedStudioPromptMetadata(entry: SessionEntry): PersistedStudioPromptMetadata | null {
3707
+ if (!entry || entry.type !== "custom") return null;
3708
+ const customEntry = entry as { customType?: unknown; data?: unknown };
3709
+ if (customEntry.customType !== STUDIO_PROMPT_METADATA_CUSTOM_TYPE) return null;
3710
+ const data = customEntry.data as Partial<PersistedStudioPromptMetadata> | undefined;
3711
+ if (!data || data.requestKind !== "direct") return null;
3712
+ return {
3713
+ version: data.version === 1 ? 1 : 1,
3714
+ requestKind: "direct",
3715
+ ...buildStudioPromptDescriptor(
3716
+ typeof data.prompt === "string" ? data.prompt : null,
3717
+ data.promptMode === "run" || data.promptMode === "effective" ? data.promptMode : "response",
3718
+ data.promptTriggerKind === "run" || data.promptTriggerKind === "steer" ? data.promptTriggerKind : null,
3719
+ typeof data.promptSteeringCount === "number" ? data.promptSteeringCount : 0,
3720
+ typeof data.promptTriggerText === "string" ? data.promptTriggerText : null,
3721
+ ),
3722
+ };
3723
+ }
3724
+
3725
+ function getStudioPromptSourceLabel(promptMode: StudioPromptMode, promptSteeringCount: number): string | null {
3726
+ if (promptMode === "run") return "original run";
3727
+ if (promptMode !== "effective") return null;
3728
+ if (promptSteeringCount <= 0) return "original run";
3729
+ return promptSteeringCount === 1
3730
+ ? "original run + 1 steering message"
3731
+ : `original run + ${promptSteeringCount} steering messages`;
3732
+ }
3733
+
3620
3734
  function extractUserText(message: unknown): string | null {
3621
3735
  const msg = message as {
3622
3736
  role?: string;
@@ -3673,27 +3787,49 @@ function parseEntryTimestamp(timestamp: unknown): number {
3673
3787
  function buildResponseHistoryFromEntries(entries: SessionEntry[], limit = RESPONSE_HISTORY_LIMIT): StudioResponseHistoryItem[] {
3674
3788
  const history: StudioResponseHistoryItem[] = [];
3675
3789
  let lastUserPrompt: string | null = null;
3790
+ let pendingPromptDescriptor: StudioPromptDescriptor | null = null;
3676
3791
 
3677
3792
  for (const entry of entries) {
3678
- if (!entry || entry.type !== "message") continue;
3793
+ if (!entry) continue;
3794
+
3795
+ const persistedPromptMetadata = extractPersistedStudioPromptMetadata(entry);
3796
+ if (persistedPromptMetadata) {
3797
+ pendingPromptDescriptor = buildStudioPromptDescriptor(
3798
+ persistedPromptMetadata.prompt,
3799
+ persistedPromptMetadata.promptMode,
3800
+ persistedPromptMetadata.promptTriggerKind,
3801
+ persistedPromptMetadata.promptSteeringCount,
3802
+ persistedPromptMetadata.promptTriggerText,
3803
+ );
3804
+ continue;
3805
+ }
3806
+
3807
+ if (entry.type !== "message") continue;
3679
3808
  const message = (entry as { message?: unknown }).message;
3680
3809
  const role = (message as { role?: string } | undefined)?.role;
3681
3810
  if (role === "user") {
3682
3811
  lastUserPrompt = extractUserText(message);
3812
+ pendingPromptDescriptor = null;
3683
3813
  continue;
3684
3814
  }
3685
3815
  if (role !== "assistant") continue;
3686
3816
  const markdown = extractAssistantText(message);
3687
3817
  if (!markdown) continue;
3688
3818
  const thinking = extractAssistantThinking(message);
3819
+ const promptDescriptor = pendingPromptDescriptor ?? buildStudioPromptDescriptor(lastUserPrompt);
3689
3820
  history.push({
3690
3821
  id: typeof (entry as { id?: unknown }).id === "string" ? (entry as { id: string }).id : randomUUID(),
3691
3822
  markdown,
3692
3823
  thinking,
3693
3824
  timestamp: parseEntryTimestamp((entry as { timestamp?: unknown }).timestamp),
3694
3825
  kind: inferStudioResponseKind(markdown),
3695
- prompt: lastUserPrompt,
3826
+ prompt: promptDescriptor.prompt,
3827
+ promptMode: promptDescriptor.promptMode,
3828
+ promptTriggerKind: promptDescriptor.promptTriggerKind,
3829
+ promptSteeringCount: promptDescriptor.promptSteeringCount,
3830
+ promptTriggerText: promptDescriptor.promptTriggerText,
3696
3831
  });
3832
+ pendingPromptDescriptor = null;
3697
3833
  }
3698
3834
 
3699
3835
  if (history.length <= limit) return history;
@@ -4206,7 +4342,7 @@ ${cssVarsBlock}
4206
4342
  </select>
4207
4343
  </div>
4208
4344
  <div class="section-header-actions">
4209
- <button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: Cmd/Ctrl+Esc or F10.">Focus pane</button>
4345
+ <button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
4210
4346
  </div>
4211
4347
  </div>
4212
4348
  <div class="source-wrap">
@@ -4223,7 +4359,8 @@ ${cssVarsBlock}
4223
4359
  </div>
4224
4360
  <div class="source-actions">
4225
4361
  <div class="source-actions-row">
4226
- <button id="sendRunBtn" type="button" title="Send editor text directly to the model as-is. Shortcut: Cmd/Ctrl+Enter when editor pane is active.">Run editor text</button>
4362
+ <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>
4363
+ <button id="queueSteerBtn" type="button" title="Queue steering is available while Run editor text is active." disabled>Queue steering</button>
4227
4364
  <button id="copyDraftBtn" type="button">Copy editor text</button>
4228
4365
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
4229
4366
  </div>
@@ -4296,7 +4433,7 @@ ${cssVarsBlock}
4296
4433
  </select>
4297
4434
  </div>
4298
4435
  <div class="section-header-actions">
4299
- <button id="rightFocusBtn" class="pane-focus-btn" type="button" title="Show only the response pane. Shortcut: Cmd/Ctrl+Esc or F10.">Focus pane</button>
4436
+ <button id="rightFocusBtn" class="pane-focus-btn" type="button" title="Show only the response pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
4300
4437
  <button id="exportPdfBtn" type="button" title="Export the current right-pane preview as PDF via pandoc + xelatex.">Export right preview as PDF</button>
4301
4438
  </div>
4302
4439
  </div>
@@ -4338,7 +4475,7 @@ ${cssVarsBlock}
4338
4475
  <footer>
4339
4476
  <span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
4340
4477
  <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text">Model: ${initialModel} · Terminal: ${initialTerminal} · Context: unknown</span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
4341
- <span class="shortcut-hint">Focus pane: Cmd/Ctrl+Esc (or F10), Esc to exit · Run editor text: Cmd/Ctrl+Enter</span>
4478
+ <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
4342
4479
  </footer>
4343
4480
 
4344
4481
  <!-- Defer sanitizer script so studio can boot/connect even if CDN is slow or blocked. -->
@@ -4354,6 +4491,9 @@ ${cssVarsBlock}
4354
4491
  export default function (pi: ExtensionAPI) {
4355
4492
  let serverState: StudioServerState | null = null;
4356
4493
  let activeRequest: ActiveStudioRequest | null = null;
4494
+ let studioDirectRunChain: StudioDirectRunChain | null = null;
4495
+ let queuedStudioDirectRequests: QueuedStudioDirectRequest[] = [];
4496
+ let pendingStudioPromptMetadata: StudioPromptDescriptor | null = null;
4357
4497
  let lastStudioResponse: LastStudioResponse | null = null;
4358
4498
  let preparedPdfExports = new Map<string, PreparedStudioPdfExport>();
4359
4499
  let initialStudioDocument: InitialStudioDocument | null = null;
@@ -4386,6 +4526,20 @@ export default function (pi: ExtensionAPI) {
4386
4526
  const installedPackageVersion = packageMetadata?.version ?? null;
4387
4527
  let updateAvailableLatestVersion: string | null = null;
4388
4528
 
4529
+ const isStudioDirectRunChainActive = () => Boolean(studioDirectRunChain);
4530
+ const getQueuedStudioSteeringCount = () => queuedStudioDirectRequests.length;
4531
+ const canQueueStudioSteeringRequest = () => {
4532
+ if (compactInProgress) return false;
4533
+ if (!agentBusy) return false;
4534
+ if (!studioDirectRunChain) return false;
4535
+ return !activeRequest || activeRequest.kind === "direct";
4536
+ };
4537
+ const clearStudioDirectRunState = () => {
4538
+ studioDirectRunChain = null;
4539
+ queuedStudioDirectRequests = [];
4540
+ pendingStudioPromptMetadata = null;
4541
+ };
4542
+
4389
4543
  const isStudioBusy = () => agentBusy || activeRequest !== null || compactInProgress;
4390
4544
 
4391
4545
  const getSessionNameSafe = (): string | undefined => {
@@ -4862,6 +5016,8 @@ export default function (pi: ExtensionAPI) {
4862
5016
  compactInProgress,
4863
5017
  activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
4864
5018
  activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
5019
+ studioRunChainActive: isStudioDirectRunChainActive(),
5020
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
4865
5021
  });
4866
5022
  };
4867
5023
 
@@ -4916,19 +5072,67 @@ export default function (pi: ExtensionAPI) {
4916
5072
  };
4917
5073
  }
4918
5074
 
5075
+ if (kind === "direct") {
5076
+ clearStudioDirectRunState();
5077
+ }
4919
5078
  suppressedStudioResponse = { requestId, kind };
4920
- emitDebugEvent("cancel_active_request", { requestId, kind });
5079
+ emitDebugEvent("cancel_active_request", { requestId, kind, queuedSteeringCount: getQueuedStudioSteeringCount() });
4921
5080
  clearActiveRequest({ notify: "Cancelled request.", level: "warning" });
4922
5081
  return { ok: true, kind };
4923
5082
  };
4924
5083
 
4925
- const beginRequest = (requestId: string, kind: StudioRequestKind, prompt?: string | null): boolean => {
5084
+ const activateRequest = (
5085
+ requestId: string,
5086
+ kind: StudioRequestKind,
5087
+ promptDescriptor?: StudioPromptDescriptor | null,
5088
+ options?: { skipNotificationCleanup?: boolean },
5089
+ ): boolean => {
5090
+ const descriptor = promptDescriptor ?? buildStudioPromptDescriptor(null);
5091
+ const timer = setTimeout(() => {
5092
+ if (!activeRequest || activeRequest.id !== requestId) return;
5093
+ emitDebugEvent("request_timeout", { requestId, kind });
5094
+ broadcast({ type: "error", requestId, message: "Studio request timed out. Please try again." });
5095
+ clearActiveRequest();
5096
+ }, REQUEST_TIMEOUT_MS);
5097
+
5098
+ activeRequest = {
5099
+ id: requestId,
5100
+ kind,
5101
+ prompt: descriptor.prompt,
5102
+ promptMode: descriptor.promptMode,
5103
+ promptTriggerKind: descriptor.promptTriggerKind,
5104
+ promptSteeringCount: descriptor.promptSteeringCount,
5105
+ promptTriggerText: descriptor.promptTriggerText,
5106
+ startedAt: Date.now(),
5107
+ timer,
5108
+ };
5109
+ if (!options?.skipNotificationCleanup) {
5110
+ maybeClearStaleCmuxStudioNotifications();
5111
+ }
5112
+ syncCmuxStudioStatus();
5113
+
5114
+ emitDebugEvent("begin_request", {
5115
+ requestId,
5116
+ kind,
5117
+ promptMode: descriptor.promptMode,
5118
+ promptTriggerKind: descriptor.promptTriggerKind,
5119
+ promptSteeringCount: descriptor.promptSteeringCount,
5120
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
5121
+ });
5122
+ broadcast({ type: "request_started", requestId, kind });
5123
+ broadcastState();
5124
+ return true;
5125
+ };
5126
+
5127
+ const beginRequest = (requestId: string, kind: StudioRequestKind, promptDescriptor?: StudioPromptDescriptor | null): boolean => {
4926
5128
  suppressedStudioResponse = null;
4927
5129
  emitDebugEvent("begin_request_attempt", {
4928
5130
  requestId,
4929
5131
  kind,
4930
5132
  hasActiveRequest: Boolean(activeRequest),
4931
5133
  agentBusy,
5134
+ studioDirectRunChainActive: isStudioDirectRunChainActive(),
5135
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
4932
5136
  });
4933
5137
  if (activeRequest) {
4934
5138
  broadcast({ type: "busy", requestId, message: "A studio request is already in progress." });
@@ -4942,28 +5146,91 @@ export default function (pi: ExtensionAPI) {
4942
5146
  broadcast({ type: "busy", requestId, message: "pi is currently busy. Wait for the current turn to finish." });
4943
5147
  return false;
4944
5148
  }
5149
+ return activateRequest(requestId, kind, promptDescriptor);
5150
+ };
4945
5151
 
4946
- const timer = setTimeout(() => {
4947
- if (!activeRequest || activeRequest.id !== requestId) return;
4948
- emitDebugEvent("request_timeout", { requestId, kind });
4949
- broadcast({ type: "error", requestId, message: "Studio request timed out. Please try again." });
4950
- clearActiveRequest();
4951
- }, REQUEST_TIMEOUT_MS);
5152
+ const getPromptDescriptorForActiveRequest = (request: ActiveStudioRequest | null | undefined): StudioPromptDescriptor => {
5153
+ return buildStudioPromptDescriptor(
5154
+ request?.prompt ?? null,
5155
+ request?.promptMode ?? "response",
5156
+ request?.promptTriggerKind ?? null,
5157
+ request?.promptSteeringCount ?? 0,
5158
+ request?.promptTriggerText ?? null,
5159
+ );
5160
+ };
4952
5161
 
4953
- activeRequest = {
4954
- id: requestId,
4955
- kind,
4956
- prompt: normalizePromptText(prompt),
4957
- startedAt: Date.now(),
4958
- timer,
5162
+ const startStudioDirectRunChain = (prompt: string): StudioPromptDescriptor => {
5163
+ const normalizedPrompt = normalizePromptText(prompt) ?? prompt.trim();
5164
+ studioDirectRunChain = {
5165
+ id: randomUUID(),
5166
+ basePrompt: normalizedPrompt,
5167
+ steeringPrompts: [],
4959
5168
  };
4960
- maybeClearStaleCmuxStudioNotifications();
4961
- syncCmuxStudioStatus();
5169
+ queuedStudioDirectRequests = [];
5170
+ pendingStudioPromptMetadata = null;
5171
+ return buildStudioDirectRunPromptDescriptor(normalizedPrompt);
5172
+ };
4962
5173
 
4963
- emitDebugEvent("begin_request", { requestId, kind });
4964
- broadcast({ type: "request_started", requestId, kind });
4965
- broadcastState();
4966
- return true;
5174
+ const enqueueStudioDirectSteeringRequest = (requestId: string, prompt: string): QueuedStudioDirectRequest | null => {
5175
+ if (!studioDirectRunChain) return null;
5176
+ const normalizedPrompt = normalizePromptText(prompt);
5177
+ if (!normalizedPrompt) return null;
5178
+ const descriptor = buildStudioQueuedSteerPromptDescriptor(studioDirectRunChain, normalizedPrompt);
5179
+ studioDirectRunChain.steeringPrompts.push(normalizedPrompt);
5180
+ const queuedRequest: QueuedStudioDirectRequest = {
5181
+ requestId,
5182
+ queuedAt: Date.now(),
5183
+ prompt: descriptor.prompt,
5184
+ promptMode: descriptor.promptMode,
5185
+ promptTriggerKind: descriptor.promptTriggerKind,
5186
+ promptSteeringCount: descriptor.promptSteeringCount,
5187
+ promptTriggerText: descriptor.promptTriggerText,
5188
+ };
5189
+ queuedStudioDirectRequests.push(queuedRequest);
5190
+ return queuedRequest;
5191
+ };
5192
+
5193
+ const claimQueuedStudioDirectRequestForPrompt = (_prompt: string | null): QueuedStudioDirectRequest | null => {
5194
+ if (queuedStudioDirectRequests.length === 0) return null;
5195
+ return queuedStudioDirectRequests.shift() ?? null;
5196
+ };
5197
+
5198
+ const activateQueuedStudioDirectRequestForPrompt = (prompt: string | null): QueuedStudioDirectRequest | null => {
5199
+ if (activeRequest) return null;
5200
+ const queuedRequest = claimQueuedStudioDirectRequestForPrompt(prompt);
5201
+ if (!queuedRequest) return null;
5202
+ activateRequest(queuedRequest.requestId, "direct", queuedRequest, { skipNotificationCleanup: true });
5203
+ return queuedRequest;
5204
+ };
5205
+
5206
+ const stageStudioPromptMetadata = (promptDescriptor: StudioPromptDescriptor | null | undefined) => {
5207
+ const descriptor = promptDescriptor ? buildStudioPromptDescriptor(
5208
+ promptDescriptor.prompt,
5209
+ promptDescriptor.promptMode,
5210
+ promptDescriptor.promptTriggerKind,
5211
+ promptDescriptor.promptSteeringCount,
5212
+ promptDescriptor.promptTriggerText,
5213
+ ) : null;
5214
+ pendingStudioPromptMetadata = descriptor && descriptor.prompt ? descriptor : null;
5215
+ };
5216
+
5217
+ const persistPendingStudioPromptMetadata = () => {
5218
+ if (!pendingStudioPromptMetadata) return;
5219
+ const metadata = buildPersistedStudioPromptMetadata(pendingStudioPromptMetadata);
5220
+ try {
5221
+ pi.appendEntry(STUDIO_PROMPT_METADATA_CUSTOM_TYPE, metadata);
5222
+ emitDebugEvent("persist_prompt_metadata", {
5223
+ promptMode: metadata.promptMode,
5224
+ promptTriggerKind: metadata.promptTriggerKind,
5225
+ promptSteeringCount: metadata.promptSteeringCount,
5226
+ });
5227
+ } catch (error) {
5228
+ emitDebugEvent("persist_prompt_metadata_error", {
5229
+ message: error instanceof Error ? error.message : String(error),
5230
+ });
5231
+ } finally {
5232
+ pendingStudioPromptMetadata = null;
5233
+ }
4967
5234
  };
4968
5235
 
4969
5236
  const closeAllClients = (code = 4001, reason = "Session invalidated") => {
@@ -5011,6 +5278,8 @@ export default function (pi: ExtensionAPI) {
5011
5278
  compactInProgress,
5012
5279
  activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
5013
5280
  activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
5281
+ studioRunChainActive: isStudioDirectRunChainActive(),
5282
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
5014
5283
  lastResponse: lastStudioResponse,
5015
5284
  responseHistory: studioResponseHistory,
5016
5285
  initialDocument: initialStudioDocument,
@@ -5107,7 +5376,7 @@ export default function (pi: ExtensionAPI) {
5107
5376
 
5108
5377
  const lens = resolveLens(msg.lens, document);
5109
5378
  const prompt = buildCritiquePrompt(document, lens);
5110
- if (!beginRequest(msg.requestId, "critique", prompt)) return;
5379
+ if (!beginRequest(msg.requestId, "critique", buildStudioPromptDescriptor(prompt))) return;
5111
5380
 
5112
5381
  try {
5113
5382
  pi.sendUserMessage(prompt);
@@ -5134,7 +5403,7 @@ export default function (pi: ExtensionAPI) {
5134
5403
  return;
5135
5404
  }
5136
5405
 
5137
- if (!beginRequest(msg.requestId, "annotation", text)) return;
5406
+ if (!beginRequest(msg.requestId, "annotation", buildStudioPromptDescriptor(text))) return;
5138
5407
 
5139
5408
  try {
5140
5409
  pi.sendUserMessage(text);
@@ -5161,11 +5430,53 @@ export default function (pi: ExtensionAPI) {
5161
5430
  return;
5162
5431
  }
5163
5432
 
5164
- if (!beginRequest(msg.requestId, "direct", msg.text)) return;
5433
+ if (canQueueStudioSteeringRequest()) {
5434
+ const queuedRequest = enqueueStudioDirectSteeringRequest(msg.requestId, msg.text);
5435
+ if (!queuedRequest) {
5436
+ sendToClient(client, {
5437
+ type: "error",
5438
+ requestId: msg.requestId,
5439
+ message: "Could not queue steering for the current run.",
5440
+ });
5441
+ return;
5442
+ }
5443
+
5444
+ try {
5445
+ pi.sendUserMessage(msg.text, { deliverAs: "steer" });
5446
+ broadcast({
5447
+ type: "request_queued",
5448
+ requestId: msg.requestId,
5449
+ kind: "direct",
5450
+ queueKind: "steer",
5451
+ studioRunChainActive: isStudioDirectRunChainActive(),
5452
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
5453
+ });
5454
+ broadcastState();
5455
+ } catch (error) {
5456
+ queuedStudioDirectRequests = queuedStudioDirectRequests.filter((request) => request.requestId !== msg.requestId);
5457
+ if (studioDirectRunChain?.steeringPrompts.length) {
5458
+ studioDirectRunChain.steeringPrompts.pop();
5459
+ }
5460
+ sendToClient(client, {
5461
+ type: "error",
5462
+ requestId: msg.requestId,
5463
+ message: `Failed to queue steering request: ${error instanceof Error ? error.message : String(error)}`,
5464
+ });
5465
+ broadcastState();
5466
+ }
5467
+ return;
5468
+ }
5469
+
5470
+ const promptDescriptor = startStudioDirectRunChain(msg.text);
5471
+ if (!beginRequest(msg.requestId, "direct", promptDescriptor)) {
5472
+ clearStudioDirectRunState();
5473
+ return;
5474
+ }
5165
5475
 
5166
5476
  try {
5167
5477
  pi.sendUserMessage(msg.text);
5168
5478
  } catch (error) {
5479
+ clearStudioDirectRunState();
5169
5480
  clearActiveRequest();
5170
5481
  sendToClient(client, {
5171
5482
  type: "error",
@@ -5952,6 +6263,7 @@ export default function (pi: ExtensionAPI) {
5952
6263
 
5953
6264
  const stopServer = async () => {
5954
6265
  if (!serverState) return;
6266
+ clearStudioDirectRunState();
5955
6267
  clearActiveRequest();
5956
6268
  clearPendingStudioCompletion();
5957
6269
  clearPreparedPdfExports();
@@ -5984,6 +6296,7 @@ export default function (pi: ExtensionAPI) {
5984
6296
 
5985
6297
  pi.on("session_start", async (_event, ctx) => {
5986
6298
  pendingTurnPrompt = null;
6299
+ clearStudioDirectRunState();
5987
6300
  hydrateLatestAssistant(ctx.sessionManager.getBranch());
5988
6301
  clearCompactionState();
5989
6302
  agentBusy = false;
@@ -6001,6 +6314,7 @@ export default function (pi: ExtensionAPI) {
6001
6314
  });
6002
6315
 
6003
6316
  pi.on("session_switch", async (_event, ctx) => {
6317
+ clearStudioDirectRunState();
6004
6318
  clearActiveRequest({ notify: "Session switched. Studio request state cleared.", level: "warning" });
6005
6319
  clearCompactionState();
6006
6320
  pendingTurnPrompt = null;
@@ -6071,6 +6385,9 @@ export default function (pi: ExtensionAPI) {
6071
6385
  pi.on("message_start", async (event) => {
6072
6386
  const role = (event.message as { role?: string } | undefined)?.role;
6073
6387
  emitDebugEvent("message_start", { role: role ?? "", activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
6388
+ if (role === "assistant") {
6389
+ persistPendingStudioPromptMetadata();
6390
+ }
6074
6391
  if (agentBusy && role === "assistant") {
6075
6392
  setTerminalActivity("responding");
6076
6393
  }
@@ -6094,7 +6411,21 @@ export default function (pi: ExtensionAPI) {
6094
6411
  });
6095
6412
 
6096
6413
  if (role === "user") {
6097
- pendingTurnPrompt = extractUserText(event.message);
6414
+ const userPrompt = extractUserText(event.message);
6415
+ pendingTurnPrompt = userPrompt;
6416
+ const activatedQueuedRequest = activateQueuedStudioDirectRequestForPrompt(userPrompt);
6417
+ if (activatedQueuedRequest) {
6418
+ emitDebugEvent("activate_queued_request", {
6419
+ requestId: activatedQueuedRequest.requestId,
6420
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
6421
+ promptSteeringCount: activatedQueuedRequest.promptSteeringCount,
6422
+ });
6423
+ }
6424
+ if (activeRequest?.kind === "direct") {
6425
+ stageStudioPromptMetadata(getPromptDescriptorForActiveRequest(activeRequest));
6426
+ } else {
6427
+ pendingStudioPromptMetadata = null;
6428
+ }
6098
6429
  return;
6099
6430
  }
6100
6431
 
@@ -6125,14 +6456,20 @@ export default function (pi: ExtensionAPI) {
6125
6456
  refreshContextUsage(ctx);
6126
6457
  const latestHistoryItem = studioResponseHistory[studioResponseHistory.length - 1];
6127
6458
  if (!latestHistoryItem || latestHistoryItem.markdown !== markdown) {
6128
- const fallbackPrompt = activeRequest?.prompt ?? pendingTurnPrompt ?? latestSessionUserPrompt ?? null;
6459
+ const fallbackPromptDescriptor = activeRequest
6460
+ ? getPromptDescriptorForActiveRequest(activeRequest)
6461
+ : buildStudioPromptDescriptor(pendingTurnPrompt ?? latestSessionUserPrompt ?? null);
6129
6462
  const fallbackHistoryItem: StudioResponseHistoryItem = {
6130
6463
  id: randomUUID(),
6131
6464
  markdown,
6132
6465
  thinking,
6133
6466
  timestamp: Date.now(),
6134
6467
  kind: inferStudioResponseKind(markdown),
6135
- prompt: fallbackPrompt,
6468
+ prompt: fallbackPromptDescriptor.prompt,
6469
+ promptMode: fallbackPromptDescriptor.promptMode,
6470
+ promptTriggerKind: fallbackPromptDescriptor.promptTriggerKind,
6471
+ promptSteeringCount: fallbackPromptDescriptor.promptSteeringCount,
6472
+ promptTriggerText: fallbackPromptDescriptor.promptTriggerText,
6136
6473
  };
6137
6474
  const nextHistory = [...studioResponseHistory, fallbackHistoryItem];
6138
6475
  studioResponseHistory = nextHistory.slice(-RESPONSE_HISTORY_LIMIT);
@@ -6201,6 +6538,9 @@ export default function (pi: ExtensionAPI) {
6201
6538
  pi.on("agent_end", async () => {
6202
6539
  agentBusy = false;
6203
6540
  pendingTurnPrompt = null;
6541
+ pendingStudioPromptMetadata = null;
6542
+ const hadStudioDirectRunChain = isStudioDirectRunChainActive();
6543
+ const queuedSteeringCount = getQueuedStudioSteeringCount();
6204
6544
  refreshContextUsage();
6205
6545
  emitDebugEvent("agent_end", {
6206
6546
  activeRequestId: activeRequest?.id ?? null,
@@ -6208,7 +6548,10 @@ export default function (pi: ExtensionAPI) {
6208
6548
  suppressedRequestId: suppressedStudioResponse?.requestId ?? null,
6209
6549
  suppressedRequestKind: suppressedStudioResponse?.kind ?? null,
6210
6550
  pendingCompletionKind: pendingStudioCompletionKind,
6551
+ hadStudioDirectRunChain,
6552
+ queuedSteeringCount,
6211
6553
  });
6554
+ clearStudioDirectRunState();
6212
6555
  setTerminalActivity("idle");
6213
6556
  if (activeRequest) {
6214
6557
  const requestId = activeRequest.id;
@@ -6221,6 +6564,7 @@ export default function (pi: ExtensionAPI) {
6221
6564
  clearPendingStudioCompletion();
6222
6565
  } else {
6223
6566
  flushPendingStudioCompletionNotification();
6567
+ broadcastState();
6224
6568
  }
6225
6569
  suppressedStudioResponse = null;
6226
6570
  });
@@ -6228,6 +6572,7 @@ export default function (pi: ExtensionAPI) {
6228
6572
  pi.on("session_shutdown", async () => {
6229
6573
  lastCommandCtx = null;
6230
6574
  agentBusy = false;
6575
+ clearStudioDirectRunState();
6231
6576
  clearPendingStudioCompletion();
6232
6577
  clearPreparedPdfExports();
6233
6578
  clearCompactionState();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.28",
3
+ "version": "0.5.29",
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",