pi-studio 0.5.29 → 0.5.31

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,19 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.31] — 2026-03-24
8
+
9
+ ### Fixed
10
+ - The right-pane response view now nudges the browser to repaint after response renders complete, reducing cases where freshly rendered response content stayed visually blank until the user scrolled or interacted with the pane.
11
+ - Newly selected or newly arrived responses now reset the right-pane scroll position to the top by default, while **Editor (Preview)** continues to preserve scroll position so in-place edit/preview workflows still feel natural.
12
+
13
+ ## [0.5.30] — 2026-03-24
14
+
15
+ ### Fixed
16
+ - LaTeX preview now preserves structured display-math environments such as `bmatrix` inside `\[ ... \]` instead of flattening their rows during Markdown math normalization, and preview display equations now center more robustly across browser engines.
17
+ - Studio now highlights custom `[an: ...]` markers in LaTeX editor syntax-highlighting mode, and PDF export renders those markers as styled annotation badges for both Markdown and LaTeX documents instead of leaving the raw bracket syntax in the final PDF.
18
+ - Right-pane response PDF export now also respects the current annotation-visibility mode, so hidden annotations do not leak into exported PDFs as raw `[an: ...]` text.
19
+
7
20
  ## [0.5.29] — 2026-03-21
8
21
 
9
22
  ### Changed
@@ -234,6 +234,7 @@
234
234
  let sourcePreviewRenderNonce = 0;
235
235
  let responsePreviewRenderNonce = 0;
236
236
  let responseEditorPreviewTimer = null;
237
+ let pendingResponseScrollReset = false;
237
238
  let editorMetaUpdateRaf = null;
238
239
  let editorHighlightEnabled = false;
239
240
  let editorLanguage = "markdown";
@@ -971,6 +972,7 @@
971
972
  }
972
973
 
973
974
  function clearActiveResponseView() {
975
+ pendingResponseScrollReset = false;
974
976
  latestResponseMarkdown = "";
975
977
  latestResponseThinking = "";
976
978
  latestResponseKind = "annotation";
@@ -1016,13 +1018,13 @@
1016
1018
  }
1017
1019
  }
1018
1020
 
1019
- function applySelectedHistoryItem() {
1021
+ function applySelectedHistoryItem(options) {
1020
1022
  const item = getSelectedHistoryItem();
1021
1023
  if (!item) {
1022
1024
  clearActiveResponseView();
1023
1025
  return false;
1024
1026
  }
1025
- handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking);
1027
+ handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking, options);
1026
1028
  return true;
1027
1029
  }
1028
1030
 
@@ -1035,9 +1037,13 @@
1035
1037
  return false;
1036
1038
  }
1037
1039
 
1040
+ const previousItem = getSelectedHistoryItem();
1041
+ const previousId = previousItem && typeof previousItem.id === "string" ? previousItem.id : null;
1038
1042
  const nextIndex = Math.max(0, Math.min(total - 1, Number(index) || 0));
1039
1043
  responseHistoryIndex = nextIndex;
1040
- const applied = applySelectedHistoryItem();
1044
+ const nextItem = getSelectedHistoryItem();
1045
+ const nextId = nextItem && typeof nextItem.id === "string" ? nextItem.id : null;
1046
+ const applied = applySelectedHistoryItem({ resetScroll: previousId !== nextId });
1041
1047
  updateHistoryControls();
1042
1048
 
1043
1049
  if (applied && !(options && options.silent)) {
@@ -1539,6 +1545,33 @@
1539
1545
  targetEl.classList.remove("preview-pending");
1540
1546
  }
1541
1547
 
1548
+ function scheduleResponsePaneRepaintNudge() {
1549
+ if (!critiqueViewEl || typeof critiqueViewEl.getBoundingClientRect !== "function") return;
1550
+ const schedule = typeof window.requestAnimationFrame === "function"
1551
+ ? window.requestAnimationFrame.bind(window)
1552
+ : (cb) => window.setTimeout(cb, 16);
1553
+
1554
+ schedule(() => {
1555
+ if (!critiqueViewEl || !critiqueViewEl.isConnected) return;
1556
+ void critiqueViewEl.getBoundingClientRect();
1557
+ if (!critiqueViewEl.classList) return;
1558
+ critiqueViewEl.classList.add("response-repaint-nudge");
1559
+ schedule(() => {
1560
+ if (!critiqueViewEl || !critiqueViewEl.classList) return;
1561
+ critiqueViewEl.classList.remove("response-repaint-nudge");
1562
+ });
1563
+ });
1564
+ }
1565
+
1566
+ function applyPendingResponseScrollReset() {
1567
+ if (!pendingResponseScrollReset || !critiqueViewEl) return false;
1568
+ if (rightView === "editor-preview") return false;
1569
+ critiqueViewEl.scrollTop = 0;
1570
+ critiqueViewEl.scrollLeft = 0;
1571
+ pendingResponseScrollReset = false;
1572
+ return true;
1573
+ }
1574
+
1542
1575
  async function getMermaidApi() {
1543
1576
  if (mermaidModulePromise) {
1544
1577
  return mermaidModulePromise;
@@ -1715,7 +1748,9 @@
1715
1748
  return;
1716
1749
  }
1717
1750
 
1718
- const markdown = rightView === "editor-preview" ? prepareEditorTextForPdfExport(sourceTextEl.value) : latestResponseMarkdown;
1751
+ const markdown = rightView === "editor-preview"
1752
+ ? prepareEditorTextForPdfExport(sourceTextEl.value)
1753
+ : prepareEditorTextForPreview(latestResponseMarkdown);
1719
1754
  if (!markdown || !markdown.trim()) {
1720
1755
  setStatus("Nothing to export yet.", "warning");
1721
1756
  return;
@@ -1881,6 +1916,11 @@
1881
1916
  appendPreviewNotice(targetEl, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
1882
1917
  }
1883
1918
  }
1919
+
1920
+ if (pane === "response") {
1921
+ applyPendingResponseScrollReset();
1922
+ scheduleResponsePaneRepaintNudge();
1923
+ }
1884
1924
  } catch (error) {
1885
1925
  if (pane === "source") {
1886
1926
  if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
@@ -1891,6 +1931,10 @@
1891
1931
  const detail = error && error.message ? error.message : String(error || "unknown error");
1892
1932
  finishPreviewRender(targetEl);
1893
1933
  targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
1934
+ if (pane === "response") {
1935
+ applyPendingResponseScrollReset();
1936
+ scheduleResponsePaneRepaintNudge();
1937
+ }
1894
1938
  }
1895
1939
  }
1896
1940
 
@@ -1960,11 +2004,13 @@
1960
2004
  if (!editorText.trim()) {
1961
2005
  finishPreviewRender(critiqueViewEl);
1962
2006
  critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
2007
+ scheduleResponsePaneRepaintNudge();
1963
2008
  return;
1964
2009
  }
1965
2010
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
1966
2011
  finishPreviewRender(critiqueViewEl);
1967
2012
  critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
2013
+ scheduleResponsePaneRepaintNudge();
1968
2014
  return;
1969
2015
  }
1970
2016
  const nonce = ++responsePreviewRenderNonce;
@@ -1979,6 +2025,8 @@
1979
2025
  critiqueViewEl.innerHTML = thinking && thinking.trim()
1980
2026
  ? buildPlainMarkdownHtml(thinking)
1981
2027
  : "<pre class='plain-markdown'>No thinking available for this response.</pre>";
2028
+ applyPendingResponseScrollReset();
2029
+ scheduleResponsePaneRepaintNudge();
1982
2030
  return;
1983
2031
  }
1984
2032
 
@@ -1986,6 +2034,8 @@
1986
2034
  if (!markdown || !markdown.trim()) {
1987
2035
  finishPreviewRender(critiqueViewEl);
1988
2036
  critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
2037
+ applyPendingResponseScrollReset();
2038
+ scheduleResponsePaneRepaintNudge();
1989
2039
  return;
1990
2040
  }
1991
2041
 
@@ -2003,16 +2053,22 @@
2003
2053
  "Response is too large for markdown highlighting. Showing plain markdown.",
2004
2054
  markdown,
2005
2055
  );
2056
+ applyPendingResponseScrollReset();
2057
+ scheduleResponsePaneRepaintNudge();
2006
2058
  return;
2007
2059
  }
2008
2060
 
2009
2061
  finishPreviewRender(critiqueViewEl);
2010
2062
  critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
2063
+ applyPendingResponseScrollReset();
2064
+ scheduleResponsePaneRepaintNudge();
2011
2065
  return;
2012
2066
  }
2013
2067
 
2014
2068
  finishPreviewRender(critiqueViewEl);
2015
2069
  critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
2070
+ applyPendingResponseScrollReset();
2071
+ scheduleResponsePaneRepaintNudge();
2016
2072
  }
2017
2073
 
2018
2074
  function updateResultActionButtons(normalizedEditorText) {
@@ -2558,17 +2614,49 @@
2558
2614
  }
2559
2615
 
2560
2616
  if (lang === "latex") {
2561
- const texPattern = /(%.*$)|(\\(?:documentclass|usepackage|newtheorem|begin|end|section|subsection|subsubsection|chapter|part|title|author|date|maketitle|tableofcontents|includegraphics|caption|label|ref|eqref|cite|textbf|textit|texttt|emph|footnote|centering|newcommand|renewcommand|providecommand|bibliography|bibliographystyle|bibitem|item|input|include)\b)|(\\[A-Za-z]+)|(\{|\})|(\$\$?(?:[^$\\]|\\.)+\$\$?)|(\[(?:.*?)\])/g;
2562
- const highlighted = highlightCodeTokens(source, texPattern, (match) => {
2563
- if (match[1]) return "hl-code-com";
2564
- if (match[2]) return "hl-code-kw";
2565
- if (match[3]) return "hl-code-fn";
2566
- if (match[4]) return "hl-code-op";
2567
- if (match[5]) return "hl-code-str";
2568
- if (match[6]) return "hl-code-num";
2569
- return "hl-code";
2570
- });
2571
- return highlighted;
2617
+ const texPattern = /(%.*$)|(\[an:\s*[^\]]+\])|(\\(?:documentclass|usepackage|newtheorem|begin|end|section|subsection|subsubsection|chapter|part|title|author|date|maketitle|tableofcontents|includegraphics|caption|label|ref|eqref|cite|textbf|textit|texttt|emph|footnote|centering|newcommand|renewcommand|providecommand|bibliography|bibliographystyle|bibitem|item|input|include)\b)|(\\[A-Za-z]+)|(\{|\})|(\$\$?(?:[^$\\]|\\.)+\$\$?)|(\[(?:.*?)\])/gi;
2618
+ let out = "";
2619
+ let lastIndex = 0;
2620
+ texPattern.lastIndex = 0;
2621
+
2622
+ let match;
2623
+ while ((match = texPattern.exec(source)) !== null) {
2624
+ const token = match[0] || "";
2625
+ const start = typeof match.index === "number" ? match.index : 0;
2626
+
2627
+ if (start > lastIndex) {
2628
+ out += escapeHtml(source.slice(lastIndex, start));
2629
+ }
2630
+
2631
+ if (match[1]) {
2632
+ out += wrapHighlight("hl-code-com", token);
2633
+ } else if (match[2]) {
2634
+ out += highlightInlineAnnotations(token, renderMode);
2635
+ } else if (match[3]) {
2636
+ out += wrapHighlight("hl-code-kw", token);
2637
+ } else if (match[4]) {
2638
+ out += wrapHighlight("hl-code-fn", token);
2639
+ } else if (match[5]) {
2640
+ out += wrapHighlight("hl-code-op", token);
2641
+ } else if (match[6]) {
2642
+ out += wrapHighlight("hl-code-str", token);
2643
+ } else if (match[7]) {
2644
+ out += wrapHighlight("hl-code-num", token);
2645
+ } else {
2646
+ out += escapeHtml(token);
2647
+ }
2648
+
2649
+ lastIndex = start + token.length;
2650
+ if (token.length === 0) {
2651
+ texPattern.lastIndex += 1;
2652
+ }
2653
+ }
2654
+
2655
+ if (lastIndex < source.length) {
2656
+ out += escapeHtml(source.slice(lastIndex));
2657
+ }
2658
+
2659
+ return out;
2572
2660
  }
2573
2661
 
2574
2662
  if (lang === "diff") {
@@ -3024,15 +3112,29 @@
3024
3112
  return lower.indexOf("## critiques") !== -1 && lower.indexOf("## document") !== -1;
3025
3113
  }
3026
3114
 
3027
- function handleIncomingResponse(markdown, kind, timestamp, thinking) {
3115
+ function handleIncomingResponse(markdown, kind, timestamp, thinking, options) {
3028
3116
  const responseTimestamp =
3029
3117
  typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0
3030
3118
  ? timestamp
3031
3119
  : Date.now();
3120
+ const responseThinking = typeof thinking === "string" ? thinking : "";
3121
+ const responseKind = kind === "critique" ? "critique" : "annotation";
3122
+ const resetScroll = options && Object.prototype.hasOwnProperty.call(options, "resetScroll")
3123
+ ? Boolean(options.resetScroll)
3124
+ : (
3125
+ latestResponseKind !== responseKind
3126
+ || latestResponseTimestamp !== responseTimestamp
3127
+ || latestResponseNormalized !== normalizeForCompare(markdown)
3128
+ || latestResponseThinkingNormalized !== normalizeForCompare(responseThinking)
3129
+ );
3130
+
3131
+ if (resetScroll) {
3132
+ pendingResponseScrollReset = true;
3133
+ }
3032
3134
 
3033
3135
  latestResponseMarkdown = markdown;
3034
- latestResponseThinking = typeof thinking === "string" ? thinking : "";
3035
- latestResponseKind = kind === "critique" ? "critique" : "annotation";
3136
+ latestResponseThinking = responseThinking;
3137
+ latestResponseKind = responseKind;
3036
3138
  latestResponseTimestamp = responseTimestamp;
3037
3139
  latestResponseIsStructuredCritique = isStructuredCritique(markdown);
3038
3140
  latestResponseHasContent = Boolean(markdown && markdown.trim());
@@ -3050,10 +3152,10 @@
3050
3152
  refreshResponseUi();
3051
3153
  }
3052
3154
 
3053
- function applyLatestPayload(payload) {
3155
+ function applyLatestPayload(payload, options) {
3054
3156
  if (!payload || typeof payload.markdown !== "string") return false;
3055
3157
  const responseKind = payload.kind === "critique" ? "critique" : "annotation";
3056
- handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking);
3158
+ handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking, options);
3057
3159
  return true;
3058
3160
  }
3059
3161
 
package/client/studio.css CHANGED
@@ -874,7 +874,9 @@
874
874
 
875
875
  .rendered-markdown mjx-container[display="true"] {
876
876
  display: block;
877
- margin: 1em 0;
877
+ width: fit-content;
878
+ max-width: 100%;
879
+ margin: 1em auto;
878
880
  text-align: center;
879
881
  overflow-x: auto;
880
882
  overflow-y: hidden;
@@ -893,7 +895,7 @@
893
895
 
894
896
  .rendered-markdown .studio-display-equation-body math[display="block"],
895
897
  .rendered-markdown .studio-display-equation-body mjx-container[display="true"] {
896
- margin: 0;
898
+ margin: 0 auto;
897
899
  }
898
900
 
899
901
  .rendered-markdown .studio-display-equation-number {
@@ -910,7 +912,9 @@
910
912
 
911
913
  .rendered-markdown math[display="block"] {
912
914
  display: block;
913
- margin: 1em 0;
915
+ width: fit-content;
916
+ max-width: 100%;
917
+ margin: 1em auto;
914
918
  text-align: center;
915
919
  overflow-x: auto;
916
920
  overflow-y: hidden;
@@ -964,6 +968,12 @@
964
968
  opacity: 0.64;
965
969
  }
966
970
 
971
+ .panel-scroll.response-repaint-nudge {
972
+ outline: 1px solid transparent;
973
+ -webkit-transform: translateZ(0);
974
+ transform: translateZ(0);
975
+ }
976
+
967
977
  .preview-error {
968
978
  color: var(--warn);
969
979
  margin-bottom: 0.75em;
package/index.ts CHANGED
@@ -205,6 +205,11 @@ const PDF_PREAMBLE = `\\usepackage{titlesec}
205
205
  \\titleformat{\\subsubsection}{\\normalsize\\bfseries\\sffamily}{}{0pt}{}
206
206
  \\titlespacing*{\\section}{0pt}{1.5ex plus 0.5ex minus 0.2ex}{1ex plus 0.2ex}
207
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}
208
213
  \\usepackage{caption}
209
214
  \\captionsetup[figure]{justification=raggedright,singlelinecheck=false}
210
215
  \\usepackage{enumitem}
@@ -2466,6 +2471,9 @@ function isLikelyMathExpression(expr: string): boolean {
2466
2471
 
2467
2472
  function collapseDisplayMathContent(expr: string): string {
2468
2473
  let content = expr.trim();
2474
+ if (/\\begin\{[^}]+\}|\\end\{[^}]+\}/.test(content)) {
2475
+ return content;
2476
+ }
2469
2477
  if (content.includes("\\\\") || content.includes("\n")) {
2470
2478
  content = content.replace(/\\\\\s*/g, " ");
2471
2479
  content = content.replace(/\s*\n\s*/g, " ");
@@ -2646,6 +2654,85 @@ function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): stri
2646
2654
  return undefined;
2647
2655
  }
2648
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
+
2649
2736
  function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLanguage?: string): string {
2650
2737
  if (isLatex) return markdown;
2651
2738
  const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorLanguage);
@@ -2653,7 +2740,10 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
2653
2740
  && !isStudioSingleFencedCodeBlock(markdown)
2654
2741
  ? wrapStudioCodeAsMarkdown(markdown, effectiveEditorLanguage)
2655
2742
  : markdown;
2656
- return normalizeObsidianImages(normalizeMathDelimiters(source));
2743
+ const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
2744
+ ? replaceStudioAnnotationMarkersForPdf(source)
2745
+ : source;
2746
+ return normalizeObsidianImages(normalizeMathDelimiters(annotationReadySource));
2657
2747
  }
2658
2748
 
2659
2749
  function stripMathMlAnnotationTags(html: string): string {
@@ -2980,6 +3070,46 @@ async function renderStudioLiteralTextPdf(text: string, title = "Studio export")
2980
3070
  }
2981
3071
  }
2982
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
+
2983
3113
  async function renderStudioPdfFromGeneratedLatex(
2984
3114
  markdown: string,
2985
3115
  pandocCommand: string,
@@ -3057,7 +3187,8 @@ async function renderStudioPdfFromGeneratedLatex(
3057
3187
 
3058
3188
  const generatedLatex = await readFile(latexPath, "utf-8");
3059
3189
  const injectedLatex = injectStudioLatexPdfSubfigureBlocks(generatedLatex, subfigureGroups, sourcePath, resourcePath);
3060
- const normalizedLatex = normalizeStudioGeneratedFigureCaptions(injectedLatex);
3190
+ const annotationReadyLatex = replaceStudioAnnotationMarkersInGeneratedLatex(injectedLatex);
3191
+ const normalizedLatex = normalizeStudioGeneratedFigureCaptions(annotationReadyLatex);
3061
3192
  await writeFile(latexPath, normalizedLatex, "utf-8");
3062
3193
 
3063
3194
  await new Promise<void>((resolve, reject) => {
@@ -3219,7 +3350,7 @@ async function renderStudioPdfWithPandoc(
3219
3350
  }
3220
3351
  };
3221
3352
 
3222
- if (isLatex && latexSubfigurePdfTransform.groups.length > 0) {
3353
+ if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || /\[an:\s*[^\]]+\]/i.test(sourceWithResolvedRefs))) {
3223
3354
  return await renderStudioPdfFromGeneratedLatex(
3224
3355
  sourceWithResolvedRefs,
3225
3356
  pandocCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.29",
3
+ "version": "0.5.31",
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",