pi-studio 0.5.56 → 0.5.57

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,17 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.57] — 2026-04-20
8
+
9
+ ### Changed
10
+ - The inserted annotated-reply scaffold now renders more cleanly in Markdown preview, using `annotated reply: below` plus a short metadata list instead of relying on hard line-break spacing.
11
+ - Local review-note comment boxes now use normal multiline textarea behavior, so `Enter` inserts a newline while edits continue saving automatically as you type.
12
+
13
+ ### Fixed
14
+ - Markdown preview now preserves standalone LaTeX math definition lines such as `\newcommand`, `\def`, and `\DeclareMathOperator` so custom math macros/operators can work in Markdown documents instead of showing up as literal preview text.
15
+ - Markdown PDF export now moves standalone LaTeX math definition lines into the generated PDF preamble when needed, avoiding LaTeX errors like `Can be used only in preamble` for commands such as `\DeclareMathOperator`.
16
+ - Annotated-reply header detection now accepts both the older `annotated reply below:` form and the newer `annotated reply: below` form when removing/reapplying the scaffold.
17
+
7
18
  ## [0.5.56] — 2026-04-15
8
19
 
9
20
  ### Removed
package/WORKFLOW.md CHANGED
@@ -20,9 +20,11 @@ Studio uses a **single workspace**:
20
20
  Adds/updates an `annotated-reply` compatible scaffold in the editor:
21
21
 
22
22
  ```md
23
- annotated reply below:
24
- original source: <last model response | file <path> | studio editor>
25
- annotation syntax: [an: your note]
23
+ annotated reply: below
24
+
25
+ - original source: <last model response | file <path> | studio editor>
26
+ - user annotation syntax: [an: your note]
27
+ - precedence: later messages supersede these annotations unless user explicitly references them
26
28
 
27
29
  ---
28
30
 
@@ -8103,7 +8103,7 @@
8103
8103
  const textarea = document.createElement("textarea");
8104
8104
  textarea.value = String(note.text || "");
8105
8105
  textarea.placeholder = "Write a local comment here…";
8106
- textarea.title = "Write a local comment. Press Enter to finish editing, or Shift+Enter for a new line.";
8106
+ textarea.title = "Write a local comment. Enter inserts a new line; changes save automatically as you type.";
8107
8107
  card.appendChild(textarea);
8108
8108
 
8109
8109
  const footer = document.createElement("div");
@@ -8169,22 +8169,6 @@
8169
8169
  updateReviewNotesUi();
8170
8170
  });
8171
8171
 
8172
- textarea.addEventListener("keydown", (event) => {
8173
- if (
8174
- event.key === "Enter"
8175
- && !event.shiftKey
8176
- && !event.altKey
8177
- && !event.ctrlKey
8178
- && !event.metaKey
8179
- ) {
8180
- event.preventDefault();
8181
- textarea.blur();
8182
- if (!convertBtn.disabled) {
8183
- convertBtn.focus();
8184
- }
8185
- }
8186
- });
8187
-
8188
8172
  reviewNotesListEl.appendChild(card);
8189
8173
 
8190
8174
  if (pendingReviewNoteInlineFocusId && pendingReviewNoteInlineFocusId === note.id && isReviewNotesOpen()) {
@@ -9684,10 +9668,10 @@
9684
9668
 
9685
9669
  function buildAnnotationHeader() {
9686
9670
  const sourceDescriptor = describeSourceForAnnotation();
9687
- let header = "annotated reply below:\n";
9688
- header += "original source: " + sourceDescriptor + "\n";
9689
- header += "user annotation syntax: [an: note]\n";
9690
- header += "precedence: later messages supersede these annotations unless user explicitly references them\n\n---\n\n";
9671
+ let header = "annotated reply: below\n\n";
9672
+ header += "- original source: " + sourceDescriptor + "\n";
9673
+ header += "- user annotation syntax: [an: note]\n";
9674
+ header += "- precedence: later messages supersede these annotations unless user explicitly references them\n\n---\n\n";
9691
9675
  return header;
9692
9676
  }
9693
9677
 
@@ -9697,7 +9681,8 @@
9697
9681
 
9698
9682
  function stripAnnotationHeader(text) {
9699
9683
  const normalized = String(text || "").replace(/\r\n/g, "\n");
9700
- if (!normalized.toLowerCase().startsWith("annotated reply below:")) {
9684
+ const lower = normalized.toLowerCase();
9685
+ if (!lower.startsWith("annotated reply: below") && !lower.startsWith("annotated reply below:")) {
9701
9686
  return { hadHeader: false, body: normalized };
9702
9687
  }
9703
9688
 
package/index.ts CHANGED
@@ -20,7 +20,10 @@ import {
20
20
  transformStudioMarkdownOutsideFences,
21
21
  } from "./shared/studio-annotation-scanner.js";
22
22
  import { stripStudioMarkdownHtmlComments } from "./shared/studio-markdown-html-comments.js";
23
- import { preserveLiteralLatexCommandsInMarkdown } from "./shared/studio-markdown-latex-literals.js";
23
+ import {
24
+ extractStandaloneLatexDefinitionsFromMarkdown,
25
+ preserveLiteralLatexCommandsInMarkdown,
26
+ } from "./shared/studio-markdown-latex-literals.js";
24
27
  import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
25
28
 
26
29
  type Lens = "writing" | "code";
@@ -477,7 +480,7 @@ function buildStudioPdfCalloutTitleSizeCommand(options?: StudioPdfRenderOptions)
477
480
  return "\\footnotesize";
478
481
  }
479
482
 
480
- function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
483
+ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions, extraPreamble = ""): string {
481
484
  const sectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.sectionSize, "\\Large");
482
485
  const subsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsectionSize, "\\large");
483
486
  const subsubsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsubsectionSize, "\\normalsize");
@@ -543,7 +546,7 @@ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
543
546
  \\RecustomVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere,bgcolor=StudioCodeBlockBg,framesep=2mm}%
544
547
  }
545
548
  \\makeatother
546
- `;
549
+ ${extraPreamble ? `${extraPreamble.trim()}\n` : ""}`;
547
550
  }
548
551
 
549
552
  type StudioThemeMode = "dark" | "light";
@@ -4478,6 +4481,7 @@ async function renderStudioPdfFromGeneratedLatex(
4478
4481
  calloutBlocks: StudioPdfMarkdownCalloutBlock[] = [],
4479
4482
  alignedImageBlocks: StudioPdfAlignedImageBlock[] = [],
4480
4483
  pdfOptions?: StudioPdfRenderOptions,
4484
+ extraPreamble = "",
4481
4485
  ): Promise<{ pdf: Buffer; warning?: string }> {
4482
4486
  const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
4483
4487
  const preamblePath = join(tempDir, "_pdf_preamble.tex");
@@ -4485,7 +4489,7 @@ async function renderStudioPdfFromGeneratedLatex(
4485
4489
  const outputPath = join(tempDir, "studio-export.pdf");
4486
4490
 
4487
4491
  await mkdir(tempDir, { recursive: true });
4488
- await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
4492
+ await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPreamble), "utf-8");
4489
4493
 
4490
4494
  const pandocArgs = [
4491
4495
  "-f", inputFormat,
@@ -4767,17 +4771,22 @@ async function renderStudioPdfWithPandoc(
4767
4771
  ? "latex"
4768
4772
  : "markdown+lists_without_preceding_blankline-blank_before_blockquote-blank_before_header+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris+superscript+subscript-raw_html";
4769
4773
  const normalizedMarkdown = prepareStudioPdfMarkdown(pdfAlignedImageTransform.markdown, isLatex, effectiveEditorLanguage);
4774
+ const markdownPreambleSplit = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
4775
+ ? extractStandaloneLatexDefinitionsFromMarkdown(normalizedMarkdown)
4776
+ : { body: normalizedMarkdown, definitions: [], preamble: "" };
4777
+ const normalizedMarkdownBody = markdownPreambleSplit.body;
4778
+ const extraPdfPreamble = markdownPreambleSplit.preamble;
4770
4779
 
4771
4780
  const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
4772
4781
  const preamblePath = join(tempDir, "_pdf_preamble.tex");
4773
4782
  const outputPath = join(tempDir, "studio-export.pdf");
4774
4783
 
4775
4784
  await mkdir(tempDir, { recursive: true });
4776
- await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
4785
+ await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPdfPreamble), "utf-8");
4777
4786
 
4778
4787
  const mermaidPrepared: StudioMermaidPdfPreprocessResult = isLatex
4779
- ? { markdown: normalizedMarkdown, found: 0, replaced: 0, failed: 0, missingCli: false }
4780
- : await preprocessStudioMermaidForPdf(normalizedMarkdown, tempDir);
4788
+ ? { markdown: normalizedMarkdownBody, found: 0, replaced: 0, failed: 0, missingCli: false }
4789
+ : await preprocessStudioMermaidForPdf(normalizedMarkdownBody, tempDir);
4781
4790
  const markdownForPdf = mermaidPrepared.markdown;
4782
4791
  const hasDiffBlocks = !isLatex && hasStudioMarkdownDiffFence(markdownForPdf);
4783
4792
 
@@ -4795,6 +4804,7 @@ async function renderStudioPdfWithPandoc(
4795
4804
  pdfCalloutTransform.blocks,
4796
4805
  pdfAlignedImageTransform.blocks,
4797
4806
  pdfOptions,
4807
+ extraPdfPreamble,
4798
4808
  );
4799
4809
  await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
4800
4810
  return { pdf: rendered.pdf, warning: mermaidPrepared.warning ?? rendered.warning };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.56",
3
+ "version": "0.5.57",
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",
@@ -51,6 +51,18 @@ const STUDIO_LITERAL_MARKDOWN_LATEX_COMMANDS = new Set([
51
51
  "tex",
52
52
  ]);
53
53
 
54
+ const STUDIO_STANDALONE_MARKDOWN_LATEX_DEFINITION_COMMANDS = new Set([
55
+ "newcommand",
56
+ "renewcommand",
57
+ "providecommand",
58
+ "declaremathoperator",
59
+ "def",
60
+ "gdef",
61
+ "edef",
62
+ "xdef",
63
+ "let",
64
+ ]);
65
+
54
66
  function isEscapedAt(text, index) {
55
67
  let slashCount = 0;
56
68
  for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
@@ -70,12 +82,40 @@ function findClosingUnescapedDelimiter(text, startIndex, delimiter) {
70
82
  return -1;
71
83
  }
72
84
 
85
+ function isStandaloneLatexDefinitionLine(line) {
86
+ const commandMatch = String(line || "").match(/^[ \t]{0,3}\\([A-Za-z@]+)\*?(?=\s|\\|\{|\[|$)/);
87
+ if (!commandMatch) return false;
88
+
89
+ const commandName = String(commandMatch[1] || "").toLowerCase();
90
+ return STUDIO_STANDALONE_MARKDOWN_LATEX_DEFINITION_COMMANDS.has(commandName);
91
+ }
92
+
93
+ function readStandaloneLatexDefinitionLine(text, startIndex) {
94
+ if (startIndex > 0 && text[startIndex - 1] !== "\n") return null;
95
+
96
+ const lineEndIndex = text.indexOf("\n", startIndex);
97
+ const sliceEnd = lineEndIndex >= 0 ? lineEndIndex : text.length;
98
+ const line = text.slice(startIndex, sliceEnd);
99
+ if (!isStandaloneLatexDefinitionLine(line)) return null;
100
+
101
+ return {
102
+ text: lineEndIndex >= 0 ? text.slice(startIndex, lineEndIndex + 1) : line,
103
+ nextIndex: lineEndIndex >= 0 ? lineEndIndex + 1 : text.length,
104
+ };
105
+ }
106
+
73
107
  function preserveLiteralLatexCommandsInMarkdownSegment(markdown) {
74
108
  const source = String(markdown || "");
75
109
  let out = "";
76
110
  let index = 0;
77
111
 
78
112
  while (index < source.length) {
113
+ const standaloneDefinitionLine = readStandaloneLatexDefinitionLine(source, index);
114
+ if (standaloneDefinitionLine) {
115
+ out += standaloneDefinitionLine.text;
116
+ index = standaloneDefinitionLine.nextIndex;
117
+ continue;
118
+ }
79
119
  if (source[index] === "`") {
80
120
  let tickCount = 1;
81
121
  while (source[index + tickCount] === "`") tickCount += 1;
@@ -201,3 +241,49 @@ export function preserveLiteralLatexCommandsInMarkdown(markdown) {
201
241
  flushPlain();
202
242
  return out.join("\n");
203
243
  }
244
+
245
+ export function extractStandaloneLatexDefinitionsFromMarkdown(markdown) {
246
+ const lines = String(markdown || "").split("\n");
247
+ const bodyLines = [];
248
+ const definitions = [];
249
+ let inFence = false;
250
+ let fenceChar;
251
+ let fenceLength = 0;
252
+
253
+ for (const line of lines) {
254
+ const trimmed = line.trimStart();
255
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
256
+
257
+ if (fenceMatch) {
258
+ const marker = fenceMatch[1];
259
+ const markerChar = marker[0];
260
+ const markerLength = marker.length;
261
+
262
+ if (!inFence) {
263
+ inFence = true;
264
+ fenceChar = markerChar;
265
+ fenceLength = markerLength;
266
+ } else if (fenceChar === markerChar && markerLength >= fenceLength) {
267
+ inFence = false;
268
+ fenceChar = undefined;
269
+ fenceLength = 0;
270
+ }
271
+
272
+ bodyLines.push(line);
273
+ continue;
274
+ }
275
+
276
+ if (!inFence && isStandaloneLatexDefinitionLine(line)) {
277
+ definitions.push(line);
278
+ continue;
279
+ }
280
+
281
+ bodyLines.push(line);
282
+ }
283
+
284
+ return {
285
+ body: bodyLines.join("\n"),
286
+ definitions,
287
+ preamble: definitions.join("\n"),
288
+ };
289
+ }