pi-studio 0.5.12 → 0.5.13

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/index.ts +89 -23
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.13] — 2026-03-15
8
+
9
+ ### Fixed
10
+ - Studio `Editor (Preview)` PDF export now fences non-markdown editor content such as diff/code before Pandoc export, preventing LaTeX failures on raw diff/code text.
11
+ - Non-markdown editor preview modes such as `diff` now support inline `[an: ...]` markers and render them as compact note pills.
12
+ - The editor highlight overlay keeps exact annotation source text/width, preserving cursor and text alignment while preview-only panes use the compact annotation-pill rendering.
13
+
7
14
  ## [0.5.12] — 2026-03-15
8
15
 
9
16
  ### Added
package/index.ts CHANGED
@@ -855,7 +855,9 @@ function readStudioFile(pathArg: string, cwd: string):
855
855
  | { ok: true; text: string; label: string; resolvedPath: string }
856
856
  | { ok: false; message: string } {
857
857
  const resolved = resolveStudioPath(pathArg, cwd);
858
- if (!resolved.ok) return resolved;
858
+ if (resolved.ok === false) {
859
+ return { ok: false, message: resolved.message };
860
+ }
859
861
 
860
862
  try {
861
863
  const stats = statSync(resolved.resolved);
@@ -899,7 +901,9 @@ function writeStudioFile(pathArg: string, cwd: string, content: string):
899
901
  | { ok: true; label: string; resolvedPath: string }
900
902
  | { ok: false; message: string } {
901
903
  const resolved = resolveStudioPath(pathArg, cwd);
902
- if (!resolved.ok) return resolved;
904
+ if (resolved.ok === false) {
905
+ return { ok: false, message: resolved.message };
906
+ }
903
907
 
904
908
  try {
905
909
  writeFileSync(resolved.resolved, content, "utf-8");
@@ -4662,6 +4666,24 @@ ${cssVarsBlock}
4662
4666
  return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
4663
4667
  }
4664
4668
 
4669
+ function wrapAsFencedCodeBlock(text, language) {
4670
+ const source = String(text || "").trimEnd();
4671
+ const lang = String(language || "").trim();
4672
+ const backtickFence = "\x60\x60\x60";
4673
+ const newline = "\\n";
4674
+ const marker = source.includes(backtickFence) ? "~~~" : backtickFence;
4675
+ return marker + (lang ? lang : "") + newline + source + newline + marker;
4676
+ }
4677
+
4678
+ function prepareEditorTextForPdfExport(text) {
4679
+ const prepared = prepareEditorTextForPreview(text);
4680
+ const lang = normalizeFenceLanguage(editorLanguage || "");
4681
+ if (lang && lang !== "markdown" && lang !== "latex") {
4682
+ return wrapAsFencedCodeBlock(prepared, lang);
4683
+ }
4684
+ return prepared;
4685
+ }
4686
+
4665
4687
  function updateSyncBadge(normalizedEditorText) {
4666
4688
  if (!syncBadgeEl) return;
4667
4689
 
@@ -5020,7 +5042,7 @@ ${cssVarsBlock}
5020
5042
  return;
5021
5043
  }
5022
5044
 
5023
- const markdown = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
5045
+ const markdown = rightView === "editor-preview" ? prepareEditorTextForPdfExport(sourceTextEl.value) : latestResponseMarkdown;
5024
5046
  if (!markdown || !markdown.trim()) {
5025
5047
  setStatus("Nothing to export yet.", "warning");
5026
5048
  return;
@@ -5028,7 +5050,8 @@ ${cssVarsBlock}
5028
5050
 
5029
5051
  const sourcePath = sourceState.path || "";
5030
5052
  const resourceDir = (!sourceState.path && resourceDirInput) ? resourceDirInput.value.trim() : "";
5031
- const isLatex = /\\\\documentclass\\b|\\\\begin\\{document\\}/.test(markdown);
5053
+ const editorPdfLanguage = rightView === "editor-preview" ? normalizeFenceLanguage(editorLanguage || "") : "";
5054
+ const isLatex = editorPdfLanguage === "latex" || /\\\\documentclass\\b|\\\\begin\\{document\\}/.test(markdown);
5032
5055
  let filenameHint = rightView === "editor-preview" ? "studio-editor-preview.pdf" : "studio-response-preview.pdf";
5033
5056
  if (sourceState.path) {
5034
5057
  const baseName = sourceState.path.split(/[\\\\/]/).pop() || "studio";
@@ -5150,7 +5173,7 @@ ${cssVarsBlock}
5150
5173
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
5151
5174
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
5152
5175
  finishPreviewRender(sourcePreviewEl);
5153
- sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(text, editorLanguage) + "</div>";
5176
+ sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(text, editorLanguage, "preview") + "</div>";
5154
5177
  return;
5155
5178
  }
5156
5179
  const nonce = ++sourcePreviewRenderNonce;
@@ -5215,7 +5238,7 @@ ${cssVarsBlock}
5215
5238
  }
5216
5239
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
5217
5240
  finishPreviewRender(critiqueViewEl);
5218
- critiqueViewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(editorText, editorLanguage) + "</div>";
5241
+ critiqueViewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
5219
5242
  return;
5220
5243
  }
5221
5244
  const nonce = ++responsePreviewRenderNonce;
@@ -5537,6 +5560,47 @@ ${cssVarsBlock}
5537
5560
  return "<span class='" + className + "'>" + escapeHtml(String(text || "")) + "</span>";
5538
5561
  }
5539
5562
 
5563
+ function wrapHighlightWithTitle(className, text, title) {
5564
+ const titleAttr = title ? " title='" + escapeHtml(String(title)) + "'" : "";
5565
+ return "<span class='" + className + "'" + titleAttr + ">" + escapeHtml(String(text || "")) + "</span>";
5566
+ }
5567
+
5568
+ function highlightInlineAnnotations(text, mode) {
5569
+ const source = String(text || "");
5570
+ const renderMode = mode === "preview" ? "preview" : "overlay";
5571
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
5572
+ let lastIndex = 0;
5573
+ let out = "";
5574
+
5575
+ let match;
5576
+ while ((match = ANNOTATION_MARKER_REGEX.exec(source)) !== null) {
5577
+ const token = match[0] || "";
5578
+ const start = typeof match.index === "number" ? match.index : 0;
5579
+ const markerText = typeof match[1] === "string" ? match[1].trim() : token;
5580
+
5581
+ if (start > lastIndex) {
5582
+ out += escapeHtml(source.slice(lastIndex, start));
5583
+ }
5584
+
5585
+ if (renderMode === "preview") {
5586
+ out += wrapHighlightWithTitle("annotation-preview-marker", markerText || token, token);
5587
+ } else {
5588
+ out += wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
5589
+ }
5590
+ lastIndex = start + token.length;
5591
+ if (token.length === 0) {
5592
+ ANNOTATION_MARKER_REGEX.lastIndex += 1;
5593
+ }
5594
+ }
5595
+
5596
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
5597
+ if (lastIndex < source.length) {
5598
+ out += escapeHtml(source.slice(lastIndex));
5599
+ }
5600
+
5601
+ return out;
5602
+ }
5603
+
5540
5604
  function highlightInlineMarkdown(text) {
5541
5605
  const source = String(text || "");
5542
5606
  const pattern = /(\\x60[^\\x60]*\\x60)|(\\[[^\\]]+\\]\\([^)]+\\))|(\\[an:\\s*[^\\]]+\\])/gi;
@@ -5563,7 +5627,7 @@ ${cssVarsBlock}
5563
5627
  out += escapeHtml(token);
5564
5628
  }
5565
5629
  } else if (match[3]) {
5566
- out += wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
5630
+ out += highlightInlineAnnotations(token);
5567
5631
  } else {
5568
5632
  out += escapeHtml(token);
5569
5633
  }
@@ -5635,9 +5699,10 @@ ${cssVarsBlock}
5635
5699
  return out;
5636
5700
  }
5637
5701
 
5638
- function highlightCodeLine(line, language) {
5702
+ function highlightCodeLine(line, language, annotationRenderMode) {
5639
5703
  const source = String(line || "");
5640
5704
  const lang = normalizeFenceLanguage(language);
5705
+ const renderMode = annotationRenderMode === "preview" ? "preview" : "overlay";
5641
5706
 
5642
5707
  if (!lang) {
5643
5708
  return wrapHighlight("hl-code", source);
@@ -5781,14 +5846,14 @@ ${cssVarsBlock}
5781
5846
  }
5782
5847
 
5783
5848
  if (lang === "diff") {
5784
- var escaped = escapeHtml(source);
5785
- if (/^@@/.test(source)) return "<span class=\\"hl-code-fn\\">" + escaped + "</span>";
5786
- if (/^\\+\\+\\+|^---/.test(source)) return "<span class=\\"hl-code-kw\\">" + escaped + "</span>";
5787
- if (/^\\+/.test(source)) return "<span class=\\"hl-diff-add\\">" + escaped + "</span>";
5788
- if (/^-/.test(source)) return "<span class=\\"hl-diff-del\\">" + escaped + "</span>";
5789
- if (/^diff /.test(source)) return "<span class=\\"hl-code-kw\\">" + escaped + "</span>";
5790
- if (/^index /.test(source)) return "<span class=\\"hl-code-com\\">" + escaped + "</span>";
5791
- return escaped;
5849
+ var highlightedDiff = highlightInlineAnnotations(source, renderMode);
5850
+ if (/^@@/.test(source)) return "<span class=\\"hl-code-fn\\">" + highlightedDiff + "</span>";
5851
+ if (/^\\+\\+\\+|^---/.test(source)) return "<span class=\\"hl-code-kw\\">" + highlightedDiff + "</span>";
5852
+ if (/^\\+/.test(source)) return "<span class=\\"hl-diff-add\\">" + highlightedDiff + "</span>";
5853
+ if (/^-/.test(source)) return "<span class=\\"hl-diff-del\\">" + highlightedDiff + "</span>";
5854
+ if (/^diff /.test(source)) return "<span class=\\"hl-code-kw\\">" + highlightedDiff + "</span>";
5855
+ if (/^index /.test(source)) return "<span class=\\"hl-code-com\\">" + highlightedDiff + "</span>";
5856
+ return highlightedDiff;
5792
5857
  }
5793
5858
 
5794
5859
  return wrapHighlight("hl-code", source);
@@ -5864,15 +5929,16 @@ ${cssVarsBlock}
5864
5929
  return out.join("<br>");
5865
5930
  }
5866
5931
 
5867
- function highlightCode(text, language) {
5932
+ function highlightCode(text, language, annotationRenderMode) {
5868
5933
  const lines = String(text || "").replace(/\\r\\n/g, "\\n").split("\\n");
5869
5934
  const lang = normalizeFenceLanguage(language);
5935
+ const renderMode = annotationRenderMode === "preview" ? "preview" : "overlay";
5870
5936
  const out = [];
5871
5937
  for (const line of lines) {
5872
5938
  if (line.length === 0) {
5873
5939
  out.push(EMPTY_OVERLAY_LINE);
5874
5940
  } else if (lang) {
5875
- out.push(highlightCodeLine(line, lang));
5941
+ out.push(highlightCodeLine(line, lang, renderMode));
5876
5942
  } else {
5877
5943
  out.push(escapeHtml(line));
5878
5944
  }
@@ -8005,7 +8071,7 @@ export default function (pi: ExtensionAPI) {
8005
8071
 
8006
8072
  const baseDir = resolveStudioGitDiffBaseDir(msg.sourcePath, msg.resourceDir, studioCwd);
8007
8073
  const diffResult = readStudioGitDiff(baseDir);
8008
- if (!diffResult.ok) {
8074
+ if (diffResult.ok === false) {
8009
8075
  sendToClient(client, {
8010
8076
  type: "info",
8011
8077
  requestId: msg.requestId,
@@ -8037,7 +8103,7 @@ export default function (pi: ExtensionAPI) {
8037
8103
  }
8038
8104
 
8039
8105
  const result = cancelActiveRequest(msg.requestId);
8040
- if (!result.ok) {
8106
+ if (result.ok === false) {
8041
8107
  sendToClient(client, { type: "error", requestId: msg.requestId, message: result.message });
8042
8108
  }
8043
8109
  return;
@@ -8235,7 +8301,7 @@ export default function (pi: ExtensionAPI) {
8235
8301
  }
8236
8302
 
8237
8303
  const result = writeStudioFile(msg.path, studioCwd, msg.content);
8238
- if (!result.ok) {
8304
+ if (result.ok === false) {
8239
8305
  sendToClient(client, { type: "error", requestId: msg.requestId, message: result.message });
8240
8306
  return;
8241
8307
  }
@@ -9099,7 +9165,7 @@ export default function (pi: ExtensionAPI) {
9099
9165
  }
9100
9166
 
9101
9167
  const file = readStudioFile(pathArg, ctx.cwd);
9102
- if (!file.ok) {
9168
+ if (file.ok === false) {
9103
9169
  ctx.ui.notify(file.message, "error");
9104
9170
  return;
9105
9171
  }
@@ -9165,7 +9231,7 @@ export default function (pi: ExtensionAPI) {
9165
9231
  }
9166
9232
 
9167
9233
  const file = readStudioFile(pathArg, ctx.cwd);
9168
- if (!file.ok) {
9234
+ if (file.ok === false) {
9169
9235
  ctx.ui.notify(file.message, "error");
9170
9236
  return;
9171
9237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",