pi-studio 0.5.28 → 0.5.30

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/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}]
@@ -180,6 +205,11 @@ const PDF_PREAMBLE = `\\usepackage{titlesec}
180
205
  \\titleformat{\\subsubsection}{\\normalsize\\bfseries\\sffamily}{}{0pt}{}
181
206
  \\titlespacing*{\\section}{0pt}{1.5ex plus 0.5ex minus 0.2ex}{1ex plus 0.2ex}
182
207
  \\titlespacing*{\\subsection}{0pt}{1.2ex plus 0.4ex minus 0.2ex}{0.6ex plus 0.1ex}
208
+ \\usepackage{xcolor}
209
+ \\definecolor{StudioAnnotationBg}{HTML}{EAF3FF}
210
+ \\definecolor{StudioAnnotationBorder}{HTML}{8CB8FF}
211
+ \\definecolor{StudioAnnotationText}{HTML}{1F5FBF}
212
+ \\newcommand{\\studioannotation}[1]{\\begingroup\\setlength{\\fboxsep}{1.5pt}\\fcolorbox{StudioAnnotationBorder}{StudioAnnotationBg}{\\textcolor{StudioAnnotationText}{\\sffamily\\footnotesize\\strut #1}}\\endgroup}
183
213
  \\usepackage{caption}
184
214
  \\captionsetup[figure]{justification=raggedright,singlelinecheck=false}
185
215
  \\usepackage{enumitem}
@@ -2441,6 +2471,9 @@ function isLikelyMathExpression(expr: string): boolean {
2441
2471
 
2442
2472
  function collapseDisplayMathContent(expr: string): string {
2443
2473
  let content = expr.trim();
2474
+ if (/\\begin\{[^}]+\}|\\end\{[^}]+\}/.test(content)) {
2475
+ return content;
2476
+ }
2444
2477
  if (content.includes("\\\\") || content.includes("\n")) {
2445
2478
  content = content.replace(/\\\\\s*/g, " ");
2446
2479
  content = content.replace(/\s*\n\s*/g, " ");
@@ -2621,6 +2654,85 @@ function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): stri
2621
2654
  return undefined;
2622
2655
  }
2623
2656
 
2657
+ function escapeStudioPdfLatexText(text: string): string {
2658
+ return String(text ?? "")
2659
+ .replace(/\r\n/g, "\n")
2660
+ .replace(/\s*\n\s*/g, " ")
2661
+ .trim()
2662
+ .replace(/\\/g, "\\textbackslash{}")
2663
+ .replace(/([{}%#$&_])/g, "\\$1")
2664
+ .replace(/~/g, "\\textasciitilde{}")
2665
+ .replace(/\^/g, "\\textasciicircum{}")
2666
+ .replace(/\s{2,}/g, " ");
2667
+ }
2668
+
2669
+ function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
2670
+ return String(text ?? "")
2671
+ .replace(/\[an:\s*([^\]]+?)\]/gi, (_match, markerText: string) => {
2672
+ const cleaned = escapeStudioPdfLatexText(markerText);
2673
+ if (!cleaned) return "";
2674
+ return `\\studioannotation{${cleaned}}`;
2675
+ })
2676
+ .replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText: string) => {
2677
+ const cleaned = escapeStudioPdfLatexText(markerText);
2678
+ if (!cleaned) return "";
2679
+ return `\\studioannotation{${cleaned}}`;
2680
+ });
2681
+ }
2682
+
2683
+ function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
2684
+ const lines = String(markdown ?? "").split("\n");
2685
+ const out: string[] = [];
2686
+ let plainBuffer: string[] = [];
2687
+ let inFence = false;
2688
+ let fenceChar: "`" | "~" | undefined;
2689
+ let fenceLength = 0;
2690
+
2691
+ const flushPlain = () => {
2692
+ if (plainBuffer.length === 0) return;
2693
+ out.push(replaceStudioAnnotationMarkersForPdfInSegment(plainBuffer.join("\n")));
2694
+ plainBuffer = [];
2695
+ };
2696
+
2697
+ for (const line of lines) {
2698
+ const trimmed = line.trimStart();
2699
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
2700
+
2701
+ if (fenceMatch) {
2702
+ const marker = fenceMatch[1]!;
2703
+ const markerChar = marker[0] as "`" | "~";
2704
+ const markerLength = marker.length;
2705
+
2706
+ if (!inFence) {
2707
+ flushPlain();
2708
+ inFence = true;
2709
+ fenceChar = markerChar;
2710
+ fenceLength = markerLength;
2711
+ out.push(line);
2712
+ continue;
2713
+ }
2714
+
2715
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
2716
+ inFence = false;
2717
+ fenceChar = undefined;
2718
+ fenceLength = 0;
2719
+ }
2720
+
2721
+ out.push(line);
2722
+ continue;
2723
+ }
2724
+
2725
+ if (inFence) {
2726
+ out.push(line);
2727
+ } else {
2728
+ plainBuffer.push(line);
2729
+ }
2730
+ }
2731
+
2732
+ flushPlain();
2733
+ return out.join("\n");
2734
+ }
2735
+
2624
2736
  function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLanguage?: string): string {
2625
2737
  if (isLatex) return markdown;
2626
2738
  const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorLanguage);
@@ -2628,7 +2740,10 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
2628
2740
  && !isStudioSingleFencedCodeBlock(markdown)
2629
2741
  ? wrapStudioCodeAsMarkdown(markdown, effectiveEditorLanguage)
2630
2742
  : markdown;
2631
- return normalizeObsidianImages(normalizeMathDelimiters(source));
2743
+ const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
2744
+ ? replaceStudioAnnotationMarkersForPdf(source)
2745
+ : source;
2746
+ return normalizeObsidianImages(normalizeMathDelimiters(annotationReadySource));
2632
2747
  }
2633
2748
 
2634
2749
  function stripMathMlAnnotationTags(html: string): string {
@@ -2955,6 +3070,46 @@ async function renderStudioLiteralTextPdf(text: string, title = "Studio export")
2955
3070
  }
2956
3071
  }
2957
3072
 
3073
+ function replaceStudioAnnotationMarkersInGeneratedLatex(latex: string): string {
3074
+ const lines = String(latex ?? "").split("\n");
3075
+ const out: string[] = [];
3076
+ const rawEnvStack: string[] = [];
3077
+ const rawEnvNames = new Set(["verbatim", "Verbatim", "Highlighting", "lstlisting"]);
3078
+
3079
+ const updateRawEnvStack = (line: string) => {
3080
+ const envPattern = /\\(begin|end)\{([^}]+)\}/g;
3081
+ let match: RegExpExecArray | null;
3082
+ while ((match = envPattern.exec(line)) !== null) {
3083
+ const kind = match[1];
3084
+ const envName = match[2];
3085
+ if (!envName || !rawEnvNames.has(envName)) continue;
3086
+ if (kind === "begin") {
3087
+ rawEnvStack.push(envName);
3088
+ } else {
3089
+ for (let i = rawEnvStack.length - 1; i >= 0; i -= 1) {
3090
+ if (rawEnvStack[i] === envName) {
3091
+ rawEnvStack.splice(i, 1);
3092
+ break;
3093
+ }
3094
+ }
3095
+ }
3096
+ }
3097
+ };
3098
+
3099
+ for (const line of lines) {
3100
+ if (rawEnvStack.length > 0) {
3101
+ out.push(line);
3102
+ updateRawEnvStack(line);
3103
+ continue;
3104
+ }
3105
+
3106
+ out.push(replaceStudioAnnotationMarkersForPdfInSegment(line));
3107
+ updateRawEnvStack(line);
3108
+ }
3109
+
3110
+ return out.join("\n");
3111
+ }
3112
+
2958
3113
  async function renderStudioPdfFromGeneratedLatex(
2959
3114
  markdown: string,
2960
3115
  pandocCommand: string,
@@ -3032,7 +3187,8 @@ async function renderStudioPdfFromGeneratedLatex(
3032
3187
 
3033
3188
  const generatedLatex = await readFile(latexPath, "utf-8");
3034
3189
  const injectedLatex = injectStudioLatexPdfSubfigureBlocks(generatedLatex, subfigureGroups, sourcePath, resourcePath);
3035
- const normalizedLatex = normalizeStudioGeneratedFigureCaptions(injectedLatex);
3190
+ const annotationReadyLatex = replaceStudioAnnotationMarkersInGeneratedLatex(injectedLatex);
3191
+ const normalizedLatex = normalizeStudioGeneratedFigureCaptions(annotationReadyLatex);
3036
3192
  await writeFile(latexPath, normalizedLatex, "utf-8");
3037
3193
 
3038
3194
  await new Promise<void>((resolve, reject) => {
@@ -3194,7 +3350,7 @@ async function renderStudioPdfWithPandoc(
3194
3350
  }
3195
3351
  };
3196
3352
 
3197
- if (isLatex && latexSubfigurePdfTransform.groups.length > 0) {
3353
+ if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || /\[an:\s*[^\]]+\]/i.test(sourceWithResolvedRefs))) {
3198
3354
  return await renderStudioPdfFromGeneratedLatex(
3199
3355
  sourceWithResolvedRefs,
3200
3356
  pandocCommand,
@@ -3617,6 +3773,95 @@ function normalizePromptText(text: string | null | undefined): string | null {
3617
3773
  return trimmed.length > 0 ? trimmed : null;
3618
3774
  }
3619
3775
 
3776
+ function buildStudioPromptDescriptor(
3777
+ prompt: string | null,
3778
+ promptMode: StudioPromptMode = "response",
3779
+ promptTriggerKind: StudioPromptTriggerKind | null = null,
3780
+ promptSteeringCount = 0,
3781
+ promptTriggerText: string | null = null,
3782
+ ): StudioPromptDescriptor {
3783
+ return {
3784
+ prompt: normalizePromptText(prompt),
3785
+ promptMode,
3786
+ promptTriggerKind,
3787
+ promptSteeringCount: Number.isFinite(promptSteeringCount) && promptSteeringCount > 0
3788
+ ? Math.max(0, Math.floor(promptSteeringCount))
3789
+ : 0,
3790
+ promptTriggerText: normalizePromptText(promptTriggerText),
3791
+ };
3792
+ }
3793
+
3794
+ function buildStudioEffectivePrompt(basePrompt: string | null | undefined, steeringPrompts: Array<string | null | undefined>): string | null {
3795
+ const normalizedBasePrompt = normalizePromptText(basePrompt);
3796
+ const normalizedSteeringPrompts = steeringPrompts
3797
+ .map((prompt) => normalizePromptText(prompt))
3798
+ .filter((prompt): prompt is string => Boolean(prompt));
3799
+
3800
+ if (!normalizedBasePrompt) {
3801
+ if (normalizedSteeringPrompts.length === 0) return null;
3802
+ return normalizedSteeringPrompts.join("\n\n");
3803
+ }
3804
+ if (normalizedSteeringPrompts.length === 0) return normalizedBasePrompt;
3805
+
3806
+ const sections = ["## Original run prompt\n\n" + normalizedBasePrompt];
3807
+ for (let i = 0; i < normalizedSteeringPrompts.length; i++) {
3808
+ sections.push(`## Steering ${i + 1}\n\n${normalizedSteeringPrompts[i]}`);
3809
+ }
3810
+ return sections.join("\n\n").trim();
3811
+ }
3812
+
3813
+ function buildStudioDirectRunPromptDescriptor(prompt: string): StudioPromptDescriptor {
3814
+ const normalizedPrompt = normalizePromptText(prompt);
3815
+ return buildStudioPromptDescriptor(normalizedPrompt, "run", "run", 0, normalizedPrompt);
3816
+ }
3817
+
3818
+ function buildStudioQueuedSteerPromptDescriptor(chain: StudioDirectRunChain, triggerPrompt: string): StudioPromptDescriptor {
3819
+ const normalizedTriggerPrompt = normalizePromptText(triggerPrompt);
3820
+ const steeringPrompts = [...chain.steeringPrompts, normalizedTriggerPrompt].filter((prompt): prompt is string => Boolean(prompt));
3821
+ const effectivePrompt = buildStudioEffectivePrompt(chain.basePrompt, steeringPrompts);
3822
+ return buildStudioPromptDescriptor(effectivePrompt, "effective", "steer", steeringPrompts.length, normalizedTriggerPrompt);
3823
+ }
3824
+
3825
+ function buildPersistedStudioPromptMetadata(promptDescriptor: StudioPromptDescriptor): PersistedStudioPromptMetadata {
3826
+ return {
3827
+ version: 1,
3828
+ requestKind: "direct",
3829
+ prompt: promptDescriptor.prompt,
3830
+ promptMode: promptDescriptor.promptMode,
3831
+ promptTriggerKind: promptDescriptor.promptTriggerKind,
3832
+ promptSteeringCount: promptDescriptor.promptSteeringCount,
3833
+ promptTriggerText: promptDescriptor.promptTriggerText,
3834
+ };
3835
+ }
3836
+
3837
+ function extractPersistedStudioPromptMetadata(entry: SessionEntry): PersistedStudioPromptMetadata | null {
3838
+ if (!entry || entry.type !== "custom") return null;
3839
+ const customEntry = entry as { customType?: unknown; data?: unknown };
3840
+ if (customEntry.customType !== STUDIO_PROMPT_METADATA_CUSTOM_TYPE) return null;
3841
+ const data = customEntry.data as Partial<PersistedStudioPromptMetadata> | undefined;
3842
+ if (!data || data.requestKind !== "direct") return null;
3843
+ return {
3844
+ version: data.version === 1 ? 1 : 1,
3845
+ requestKind: "direct",
3846
+ ...buildStudioPromptDescriptor(
3847
+ typeof data.prompt === "string" ? data.prompt : null,
3848
+ data.promptMode === "run" || data.promptMode === "effective" ? data.promptMode : "response",
3849
+ data.promptTriggerKind === "run" || data.promptTriggerKind === "steer" ? data.promptTriggerKind : null,
3850
+ typeof data.promptSteeringCount === "number" ? data.promptSteeringCount : 0,
3851
+ typeof data.promptTriggerText === "string" ? data.promptTriggerText : null,
3852
+ ),
3853
+ };
3854
+ }
3855
+
3856
+ function getStudioPromptSourceLabel(promptMode: StudioPromptMode, promptSteeringCount: number): string | null {
3857
+ if (promptMode === "run") return "original run";
3858
+ if (promptMode !== "effective") return null;
3859
+ if (promptSteeringCount <= 0) return "original run";
3860
+ return promptSteeringCount === 1
3861
+ ? "original run + 1 steering message"
3862
+ : `original run + ${promptSteeringCount} steering messages`;
3863
+ }
3864
+
3620
3865
  function extractUserText(message: unknown): string | null {
3621
3866
  const msg = message as {
3622
3867
  role?: string;
@@ -3673,27 +3918,49 @@ function parseEntryTimestamp(timestamp: unknown): number {
3673
3918
  function buildResponseHistoryFromEntries(entries: SessionEntry[], limit = RESPONSE_HISTORY_LIMIT): StudioResponseHistoryItem[] {
3674
3919
  const history: StudioResponseHistoryItem[] = [];
3675
3920
  let lastUserPrompt: string | null = null;
3921
+ let pendingPromptDescriptor: StudioPromptDescriptor | null = null;
3676
3922
 
3677
3923
  for (const entry of entries) {
3678
- if (!entry || entry.type !== "message") continue;
3924
+ if (!entry) continue;
3925
+
3926
+ const persistedPromptMetadata = extractPersistedStudioPromptMetadata(entry);
3927
+ if (persistedPromptMetadata) {
3928
+ pendingPromptDescriptor = buildStudioPromptDescriptor(
3929
+ persistedPromptMetadata.prompt,
3930
+ persistedPromptMetadata.promptMode,
3931
+ persistedPromptMetadata.promptTriggerKind,
3932
+ persistedPromptMetadata.promptSteeringCount,
3933
+ persistedPromptMetadata.promptTriggerText,
3934
+ );
3935
+ continue;
3936
+ }
3937
+
3938
+ if (entry.type !== "message") continue;
3679
3939
  const message = (entry as { message?: unknown }).message;
3680
3940
  const role = (message as { role?: string } | undefined)?.role;
3681
3941
  if (role === "user") {
3682
3942
  lastUserPrompt = extractUserText(message);
3943
+ pendingPromptDescriptor = null;
3683
3944
  continue;
3684
3945
  }
3685
3946
  if (role !== "assistant") continue;
3686
3947
  const markdown = extractAssistantText(message);
3687
3948
  if (!markdown) continue;
3688
3949
  const thinking = extractAssistantThinking(message);
3950
+ const promptDescriptor = pendingPromptDescriptor ?? buildStudioPromptDescriptor(lastUserPrompt);
3689
3951
  history.push({
3690
3952
  id: typeof (entry as { id?: unknown }).id === "string" ? (entry as { id: string }).id : randomUUID(),
3691
3953
  markdown,
3692
3954
  thinking,
3693
3955
  timestamp: parseEntryTimestamp((entry as { timestamp?: unknown }).timestamp),
3694
3956
  kind: inferStudioResponseKind(markdown),
3695
- prompt: lastUserPrompt,
3957
+ prompt: promptDescriptor.prompt,
3958
+ promptMode: promptDescriptor.promptMode,
3959
+ promptTriggerKind: promptDescriptor.promptTriggerKind,
3960
+ promptSteeringCount: promptDescriptor.promptSteeringCount,
3961
+ promptTriggerText: promptDescriptor.promptTriggerText,
3696
3962
  });
3963
+ pendingPromptDescriptor = null;
3697
3964
  }
3698
3965
 
3699
3966
  if (history.length <= limit) return history;
@@ -4206,7 +4473,7 @@ ${cssVarsBlock}
4206
4473
  </select>
4207
4474
  </div>
4208
4475
  <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>
4476
+ <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
4477
  </div>
4211
4478
  </div>
4212
4479
  <div class="source-wrap">
@@ -4223,7 +4490,8 @@ ${cssVarsBlock}
4223
4490
  </div>
4224
4491
  <div class="source-actions">
4225
4492
  <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>
4493
+ <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>
4494
+ <button id="queueSteerBtn" type="button" title="Queue steering is available while Run editor text is active." disabled>Queue steering</button>
4227
4495
  <button id="copyDraftBtn" type="button">Copy editor text</button>
4228
4496
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
4229
4497
  </div>
@@ -4296,7 +4564,7 @@ ${cssVarsBlock}
4296
4564
  </select>
4297
4565
  </div>
4298
4566
  <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>
4567
+ <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
4568
  <button id="exportPdfBtn" type="button" title="Export the current right-pane preview as PDF via pandoc + xelatex.">Export right preview as PDF</button>
4301
4569
  </div>
4302
4570
  </div>
@@ -4338,7 +4606,7 @@ ${cssVarsBlock}
4338
4606
  <footer>
4339
4607
  <span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
4340
4608
  <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>
4609
+ <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
4342
4610
  </footer>
4343
4611
 
4344
4612
  <!-- Defer sanitizer script so studio can boot/connect even if CDN is slow or blocked. -->
@@ -4354,6 +4622,9 @@ ${cssVarsBlock}
4354
4622
  export default function (pi: ExtensionAPI) {
4355
4623
  let serverState: StudioServerState | null = null;
4356
4624
  let activeRequest: ActiveStudioRequest | null = null;
4625
+ let studioDirectRunChain: StudioDirectRunChain | null = null;
4626
+ let queuedStudioDirectRequests: QueuedStudioDirectRequest[] = [];
4627
+ let pendingStudioPromptMetadata: StudioPromptDescriptor | null = null;
4357
4628
  let lastStudioResponse: LastStudioResponse | null = null;
4358
4629
  let preparedPdfExports = new Map<string, PreparedStudioPdfExport>();
4359
4630
  let initialStudioDocument: InitialStudioDocument | null = null;
@@ -4386,6 +4657,20 @@ export default function (pi: ExtensionAPI) {
4386
4657
  const installedPackageVersion = packageMetadata?.version ?? null;
4387
4658
  let updateAvailableLatestVersion: string | null = null;
4388
4659
 
4660
+ const isStudioDirectRunChainActive = () => Boolean(studioDirectRunChain);
4661
+ const getQueuedStudioSteeringCount = () => queuedStudioDirectRequests.length;
4662
+ const canQueueStudioSteeringRequest = () => {
4663
+ if (compactInProgress) return false;
4664
+ if (!agentBusy) return false;
4665
+ if (!studioDirectRunChain) return false;
4666
+ return !activeRequest || activeRequest.kind === "direct";
4667
+ };
4668
+ const clearStudioDirectRunState = () => {
4669
+ studioDirectRunChain = null;
4670
+ queuedStudioDirectRequests = [];
4671
+ pendingStudioPromptMetadata = null;
4672
+ };
4673
+
4389
4674
  const isStudioBusy = () => agentBusy || activeRequest !== null || compactInProgress;
4390
4675
 
4391
4676
  const getSessionNameSafe = (): string | undefined => {
@@ -4862,6 +5147,8 @@ export default function (pi: ExtensionAPI) {
4862
5147
  compactInProgress,
4863
5148
  activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
4864
5149
  activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
5150
+ studioRunChainActive: isStudioDirectRunChainActive(),
5151
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
4865
5152
  });
4866
5153
  };
4867
5154
 
@@ -4916,19 +5203,67 @@ export default function (pi: ExtensionAPI) {
4916
5203
  };
4917
5204
  }
4918
5205
 
5206
+ if (kind === "direct") {
5207
+ clearStudioDirectRunState();
5208
+ }
4919
5209
  suppressedStudioResponse = { requestId, kind };
4920
- emitDebugEvent("cancel_active_request", { requestId, kind });
5210
+ emitDebugEvent("cancel_active_request", { requestId, kind, queuedSteeringCount: getQueuedStudioSteeringCount() });
4921
5211
  clearActiveRequest({ notify: "Cancelled request.", level: "warning" });
4922
5212
  return { ok: true, kind };
4923
5213
  };
4924
5214
 
4925
- const beginRequest = (requestId: string, kind: StudioRequestKind, prompt?: string | null): boolean => {
5215
+ const activateRequest = (
5216
+ requestId: string,
5217
+ kind: StudioRequestKind,
5218
+ promptDescriptor?: StudioPromptDescriptor | null,
5219
+ options?: { skipNotificationCleanup?: boolean },
5220
+ ): boolean => {
5221
+ const descriptor = promptDescriptor ?? buildStudioPromptDescriptor(null);
5222
+ const timer = setTimeout(() => {
5223
+ if (!activeRequest || activeRequest.id !== requestId) return;
5224
+ emitDebugEvent("request_timeout", { requestId, kind });
5225
+ broadcast({ type: "error", requestId, message: "Studio request timed out. Please try again." });
5226
+ clearActiveRequest();
5227
+ }, REQUEST_TIMEOUT_MS);
5228
+
5229
+ activeRequest = {
5230
+ id: requestId,
5231
+ kind,
5232
+ prompt: descriptor.prompt,
5233
+ promptMode: descriptor.promptMode,
5234
+ promptTriggerKind: descriptor.promptTriggerKind,
5235
+ promptSteeringCount: descriptor.promptSteeringCount,
5236
+ promptTriggerText: descriptor.promptTriggerText,
5237
+ startedAt: Date.now(),
5238
+ timer,
5239
+ };
5240
+ if (!options?.skipNotificationCleanup) {
5241
+ maybeClearStaleCmuxStudioNotifications();
5242
+ }
5243
+ syncCmuxStudioStatus();
5244
+
5245
+ emitDebugEvent("begin_request", {
5246
+ requestId,
5247
+ kind,
5248
+ promptMode: descriptor.promptMode,
5249
+ promptTriggerKind: descriptor.promptTriggerKind,
5250
+ promptSteeringCount: descriptor.promptSteeringCount,
5251
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
5252
+ });
5253
+ broadcast({ type: "request_started", requestId, kind });
5254
+ broadcastState();
5255
+ return true;
5256
+ };
5257
+
5258
+ const beginRequest = (requestId: string, kind: StudioRequestKind, promptDescriptor?: StudioPromptDescriptor | null): boolean => {
4926
5259
  suppressedStudioResponse = null;
4927
5260
  emitDebugEvent("begin_request_attempt", {
4928
5261
  requestId,
4929
5262
  kind,
4930
5263
  hasActiveRequest: Boolean(activeRequest),
4931
5264
  agentBusy,
5265
+ studioDirectRunChainActive: isStudioDirectRunChainActive(),
5266
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
4932
5267
  });
4933
5268
  if (activeRequest) {
4934
5269
  broadcast({ type: "busy", requestId, message: "A studio request is already in progress." });
@@ -4942,28 +5277,91 @@ export default function (pi: ExtensionAPI) {
4942
5277
  broadcast({ type: "busy", requestId, message: "pi is currently busy. Wait for the current turn to finish." });
4943
5278
  return false;
4944
5279
  }
5280
+ return activateRequest(requestId, kind, promptDescriptor);
5281
+ };
4945
5282
 
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);
5283
+ const getPromptDescriptorForActiveRequest = (request: ActiveStudioRequest | null | undefined): StudioPromptDescriptor => {
5284
+ return buildStudioPromptDescriptor(
5285
+ request?.prompt ?? null,
5286
+ request?.promptMode ?? "response",
5287
+ request?.promptTriggerKind ?? null,
5288
+ request?.promptSteeringCount ?? 0,
5289
+ request?.promptTriggerText ?? null,
5290
+ );
5291
+ };
4952
5292
 
4953
- activeRequest = {
4954
- id: requestId,
4955
- kind,
4956
- prompt: normalizePromptText(prompt),
4957
- startedAt: Date.now(),
4958
- timer,
5293
+ const startStudioDirectRunChain = (prompt: string): StudioPromptDescriptor => {
5294
+ const normalizedPrompt = normalizePromptText(prompt) ?? prompt.trim();
5295
+ studioDirectRunChain = {
5296
+ id: randomUUID(),
5297
+ basePrompt: normalizedPrompt,
5298
+ steeringPrompts: [],
4959
5299
  };
4960
- maybeClearStaleCmuxStudioNotifications();
4961
- syncCmuxStudioStatus();
5300
+ queuedStudioDirectRequests = [];
5301
+ pendingStudioPromptMetadata = null;
5302
+ return buildStudioDirectRunPromptDescriptor(normalizedPrompt);
5303
+ };
4962
5304
 
4963
- emitDebugEvent("begin_request", { requestId, kind });
4964
- broadcast({ type: "request_started", requestId, kind });
4965
- broadcastState();
4966
- return true;
5305
+ const enqueueStudioDirectSteeringRequest = (requestId: string, prompt: string): QueuedStudioDirectRequest | null => {
5306
+ if (!studioDirectRunChain) return null;
5307
+ const normalizedPrompt = normalizePromptText(prompt);
5308
+ if (!normalizedPrompt) return null;
5309
+ const descriptor = buildStudioQueuedSteerPromptDescriptor(studioDirectRunChain, normalizedPrompt);
5310
+ studioDirectRunChain.steeringPrompts.push(normalizedPrompt);
5311
+ const queuedRequest: QueuedStudioDirectRequest = {
5312
+ requestId,
5313
+ queuedAt: Date.now(),
5314
+ prompt: descriptor.prompt,
5315
+ promptMode: descriptor.promptMode,
5316
+ promptTriggerKind: descriptor.promptTriggerKind,
5317
+ promptSteeringCount: descriptor.promptSteeringCount,
5318
+ promptTriggerText: descriptor.promptTriggerText,
5319
+ };
5320
+ queuedStudioDirectRequests.push(queuedRequest);
5321
+ return queuedRequest;
5322
+ };
5323
+
5324
+ const claimQueuedStudioDirectRequestForPrompt = (_prompt: string | null): QueuedStudioDirectRequest | null => {
5325
+ if (queuedStudioDirectRequests.length === 0) return null;
5326
+ return queuedStudioDirectRequests.shift() ?? null;
5327
+ };
5328
+
5329
+ const activateQueuedStudioDirectRequestForPrompt = (prompt: string | null): QueuedStudioDirectRequest | null => {
5330
+ if (activeRequest) return null;
5331
+ const queuedRequest = claimQueuedStudioDirectRequestForPrompt(prompt);
5332
+ if (!queuedRequest) return null;
5333
+ activateRequest(queuedRequest.requestId, "direct", queuedRequest, { skipNotificationCleanup: true });
5334
+ return queuedRequest;
5335
+ };
5336
+
5337
+ const stageStudioPromptMetadata = (promptDescriptor: StudioPromptDescriptor | null | undefined) => {
5338
+ const descriptor = promptDescriptor ? buildStudioPromptDescriptor(
5339
+ promptDescriptor.prompt,
5340
+ promptDescriptor.promptMode,
5341
+ promptDescriptor.promptTriggerKind,
5342
+ promptDescriptor.promptSteeringCount,
5343
+ promptDescriptor.promptTriggerText,
5344
+ ) : null;
5345
+ pendingStudioPromptMetadata = descriptor && descriptor.prompt ? descriptor : null;
5346
+ };
5347
+
5348
+ const persistPendingStudioPromptMetadata = () => {
5349
+ if (!pendingStudioPromptMetadata) return;
5350
+ const metadata = buildPersistedStudioPromptMetadata(pendingStudioPromptMetadata);
5351
+ try {
5352
+ pi.appendEntry(STUDIO_PROMPT_METADATA_CUSTOM_TYPE, metadata);
5353
+ emitDebugEvent("persist_prompt_metadata", {
5354
+ promptMode: metadata.promptMode,
5355
+ promptTriggerKind: metadata.promptTriggerKind,
5356
+ promptSteeringCount: metadata.promptSteeringCount,
5357
+ });
5358
+ } catch (error) {
5359
+ emitDebugEvent("persist_prompt_metadata_error", {
5360
+ message: error instanceof Error ? error.message : String(error),
5361
+ });
5362
+ } finally {
5363
+ pendingStudioPromptMetadata = null;
5364
+ }
4967
5365
  };
4968
5366
 
4969
5367
  const closeAllClients = (code = 4001, reason = "Session invalidated") => {
@@ -5011,6 +5409,8 @@ export default function (pi: ExtensionAPI) {
5011
5409
  compactInProgress,
5012
5410
  activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
5013
5411
  activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
5412
+ studioRunChainActive: isStudioDirectRunChainActive(),
5413
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
5014
5414
  lastResponse: lastStudioResponse,
5015
5415
  responseHistory: studioResponseHistory,
5016
5416
  initialDocument: initialStudioDocument,
@@ -5107,7 +5507,7 @@ export default function (pi: ExtensionAPI) {
5107
5507
 
5108
5508
  const lens = resolveLens(msg.lens, document);
5109
5509
  const prompt = buildCritiquePrompt(document, lens);
5110
- if (!beginRequest(msg.requestId, "critique", prompt)) return;
5510
+ if (!beginRequest(msg.requestId, "critique", buildStudioPromptDescriptor(prompt))) return;
5111
5511
 
5112
5512
  try {
5113
5513
  pi.sendUserMessage(prompt);
@@ -5134,7 +5534,7 @@ export default function (pi: ExtensionAPI) {
5134
5534
  return;
5135
5535
  }
5136
5536
 
5137
- if (!beginRequest(msg.requestId, "annotation", text)) return;
5537
+ if (!beginRequest(msg.requestId, "annotation", buildStudioPromptDescriptor(text))) return;
5138
5538
 
5139
5539
  try {
5140
5540
  pi.sendUserMessage(text);
@@ -5161,11 +5561,53 @@ export default function (pi: ExtensionAPI) {
5161
5561
  return;
5162
5562
  }
5163
5563
 
5164
- if (!beginRequest(msg.requestId, "direct", msg.text)) return;
5564
+ if (canQueueStudioSteeringRequest()) {
5565
+ const queuedRequest = enqueueStudioDirectSteeringRequest(msg.requestId, msg.text);
5566
+ if (!queuedRequest) {
5567
+ sendToClient(client, {
5568
+ type: "error",
5569
+ requestId: msg.requestId,
5570
+ message: "Could not queue steering for the current run.",
5571
+ });
5572
+ return;
5573
+ }
5574
+
5575
+ try {
5576
+ pi.sendUserMessage(msg.text, { deliverAs: "steer" });
5577
+ broadcast({
5578
+ type: "request_queued",
5579
+ requestId: msg.requestId,
5580
+ kind: "direct",
5581
+ queueKind: "steer",
5582
+ studioRunChainActive: isStudioDirectRunChainActive(),
5583
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
5584
+ });
5585
+ broadcastState();
5586
+ } catch (error) {
5587
+ queuedStudioDirectRequests = queuedStudioDirectRequests.filter((request) => request.requestId !== msg.requestId);
5588
+ if (studioDirectRunChain?.steeringPrompts.length) {
5589
+ studioDirectRunChain.steeringPrompts.pop();
5590
+ }
5591
+ sendToClient(client, {
5592
+ type: "error",
5593
+ requestId: msg.requestId,
5594
+ message: `Failed to queue steering request: ${error instanceof Error ? error.message : String(error)}`,
5595
+ });
5596
+ broadcastState();
5597
+ }
5598
+ return;
5599
+ }
5600
+
5601
+ const promptDescriptor = startStudioDirectRunChain(msg.text);
5602
+ if (!beginRequest(msg.requestId, "direct", promptDescriptor)) {
5603
+ clearStudioDirectRunState();
5604
+ return;
5605
+ }
5165
5606
 
5166
5607
  try {
5167
5608
  pi.sendUserMessage(msg.text);
5168
5609
  } catch (error) {
5610
+ clearStudioDirectRunState();
5169
5611
  clearActiveRequest();
5170
5612
  sendToClient(client, {
5171
5613
  type: "error",
@@ -5952,6 +6394,7 @@ export default function (pi: ExtensionAPI) {
5952
6394
 
5953
6395
  const stopServer = async () => {
5954
6396
  if (!serverState) return;
6397
+ clearStudioDirectRunState();
5955
6398
  clearActiveRequest();
5956
6399
  clearPendingStudioCompletion();
5957
6400
  clearPreparedPdfExports();
@@ -5984,6 +6427,7 @@ export default function (pi: ExtensionAPI) {
5984
6427
 
5985
6428
  pi.on("session_start", async (_event, ctx) => {
5986
6429
  pendingTurnPrompt = null;
6430
+ clearStudioDirectRunState();
5987
6431
  hydrateLatestAssistant(ctx.sessionManager.getBranch());
5988
6432
  clearCompactionState();
5989
6433
  agentBusy = false;
@@ -6001,6 +6445,7 @@ export default function (pi: ExtensionAPI) {
6001
6445
  });
6002
6446
 
6003
6447
  pi.on("session_switch", async (_event, ctx) => {
6448
+ clearStudioDirectRunState();
6004
6449
  clearActiveRequest({ notify: "Session switched. Studio request state cleared.", level: "warning" });
6005
6450
  clearCompactionState();
6006
6451
  pendingTurnPrompt = null;
@@ -6071,6 +6516,9 @@ export default function (pi: ExtensionAPI) {
6071
6516
  pi.on("message_start", async (event) => {
6072
6517
  const role = (event.message as { role?: string } | undefined)?.role;
6073
6518
  emitDebugEvent("message_start", { role: role ?? "", activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
6519
+ if (role === "assistant") {
6520
+ persistPendingStudioPromptMetadata();
6521
+ }
6074
6522
  if (agentBusy && role === "assistant") {
6075
6523
  setTerminalActivity("responding");
6076
6524
  }
@@ -6094,7 +6542,21 @@ export default function (pi: ExtensionAPI) {
6094
6542
  });
6095
6543
 
6096
6544
  if (role === "user") {
6097
- pendingTurnPrompt = extractUserText(event.message);
6545
+ const userPrompt = extractUserText(event.message);
6546
+ pendingTurnPrompt = userPrompt;
6547
+ const activatedQueuedRequest = activateQueuedStudioDirectRequestForPrompt(userPrompt);
6548
+ if (activatedQueuedRequest) {
6549
+ emitDebugEvent("activate_queued_request", {
6550
+ requestId: activatedQueuedRequest.requestId,
6551
+ queuedSteeringCount: getQueuedStudioSteeringCount(),
6552
+ promptSteeringCount: activatedQueuedRequest.promptSteeringCount,
6553
+ });
6554
+ }
6555
+ if (activeRequest?.kind === "direct") {
6556
+ stageStudioPromptMetadata(getPromptDescriptorForActiveRequest(activeRequest));
6557
+ } else {
6558
+ pendingStudioPromptMetadata = null;
6559
+ }
6098
6560
  return;
6099
6561
  }
6100
6562
 
@@ -6125,14 +6587,20 @@ export default function (pi: ExtensionAPI) {
6125
6587
  refreshContextUsage(ctx);
6126
6588
  const latestHistoryItem = studioResponseHistory[studioResponseHistory.length - 1];
6127
6589
  if (!latestHistoryItem || latestHistoryItem.markdown !== markdown) {
6128
- const fallbackPrompt = activeRequest?.prompt ?? pendingTurnPrompt ?? latestSessionUserPrompt ?? null;
6590
+ const fallbackPromptDescriptor = activeRequest
6591
+ ? getPromptDescriptorForActiveRequest(activeRequest)
6592
+ : buildStudioPromptDescriptor(pendingTurnPrompt ?? latestSessionUserPrompt ?? null);
6129
6593
  const fallbackHistoryItem: StudioResponseHistoryItem = {
6130
6594
  id: randomUUID(),
6131
6595
  markdown,
6132
6596
  thinking,
6133
6597
  timestamp: Date.now(),
6134
6598
  kind: inferStudioResponseKind(markdown),
6135
- prompt: fallbackPrompt,
6599
+ prompt: fallbackPromptDescriptor.prompt,
6600
+ promptMode: fallbackPromptDescriptor.promptMode,
6601
+ promptTriggerKind: fallbackPromptDescriptor.promptTriggerKind,
6602
+ promptSteeringCount: fallbackPromptDescriptor.promptSteeringCount,
6603
+ promptTriggerText: fallbackPromptDescriptor.promptTriggerText,
6136
6604
  };
6137
6605
  const nextHistory = [...studioResponseHistory, fallbackHistoryItem];
6138
6606
  studioResponseHistory = nextHistory.slice(-RESPONSE_HISTORY_LIMIT);
@@ -6201,6 +6669,9 @@ export default function (pi: ExtensionAPI) {
6201
6669
  pi.on("agent_end", async () => {
6202
6670
  agentBusy = false;
6203
6671
  pendingTurnPrompt = null;
6672
+ pendingStudioPromptMetadata = null;
6673
+ const hadStudioDirectRunChain = isStudioDirectRunChainActive();
6674
+ const queuedSteeringCount = getQueuedStudioSteeringCount();
6204
6675
  refreshContextUsage();
6205
6676
  emitDebugEvent("agent_end", {
6206
6677
  activeRequestId: activeRequest?.id ?? null,
@@ -6208,7 +6679,10 @@ export default function (pi: ExtensionAPI) {
6208
6679
  suppressedRequestId: suppressedStudioResponse?.requestId ?? null,
6209
6680
  suppressedRequestKind: suppressedStudioResponse?.kind ?? null,
6210
6681
  pendingCompletionKind: pendingStudioCompletionKind,
6682
+ hadStudioDirectRunChain,
6683
+ queuedSteeringCount,
6211
6684
  });
6685
+ clearStudioDirectRunState();
6212
6686
  setTerminalActivity("idle");
6213
6687
  if (activeRequest) {
6214
6688
  const requestId = activeRequest.id;
@@ -6221,6 +6695,7 @@ export default function (pi: ExtensionAPI) {
6221
6695
  clearPendingStudioCompletion();
6222
6696
  } else {
6223
6697
  flushPendingStudioCompletionNotification();
6698
+ broadcastState();
6224
6699
  }
6225
6700
  suppressedStudioResponse = null;
6226
6701
  });
@@ -6228,6 +6703,7 @@ export default function (pi: ExtensionAPI) {
6228
6703
  pi.on("session_shutdown", async () => {
6229
6704
  lastCommandCtx = null;
6230
6705
  agentBusy = false;
6706
+ clearStudioDirectRunState();
6231
6707
  clearPendingStudioCompletion();
6232
6708
  clearPreparedPdfExports();
6233
6709
  clearCompactionState();