pi-studio 0.5.19 → 0.5.20

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,13 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.20] — 2026-03-19
8
+
9
+ ### Fixed
10
+ - LaTeX preview/PDF export now runs pandoc from the resolved source/working directory, so project-relative `\input{...}` files, shared macros, and similar local assets resolve more reliably for multi-file documents.
11
+ - LaTeX preview/PDF export now also detects basic bibliography directives such as `\bibliography{...}` and `\addbibresource{...}` and passes the resolved `.bib` files to pandoc citeproc, so references show up more often in Studio without a full `latexmk` build.
12
+ - Display-math blocks in preview are now styled to center more naturally, and the raw-editor highlight cutoff is bumped to `100_000` characters so moderately large `.tex` files still get inline syntax colouring.
13
+
7
14
  ## [0.5.19] — 2026-03-19
8
15
 
9
16
  ### Fixed
@@ -163,7 +163,7 @@
163
163
  };
164
164
  let activePane = "left";
165
165
  let paneFocusTarget = "off";
166
- const EDITOR_HIGHLIGHT_MAX_CHARS = 80_000;
166
+ const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
167
167
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
168
168
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
169
169
  // Single source of truth: language -> file extensions (and display label)
package/client/studio.css CHANGED
@@ -758,9 +758,14 @@
758
758
  max-width: 100%;
759
759
  }
760
760
 
761
+ .rendered-markdown math {
762
+ font-family: "STIX Two Math", "Cambria Math", "Latin Modern Math", "STIXGeneral", serif;
763
+ }
764
+
761
765
  .rendered-markdown math[display="block"] {
762
766
  display: block;
763
767
  margin: 1em 0;
768
+ text-align: center;
764
769
  overflow-x: auto;
765
770
  overflow-y: hidden;
766
771
  }
package/index.ts CHANGED
@@ -994,20 +994,111 @@ function buildStudioSyntheticNewFileDiff(filePath: string, content: string): str
994
994
  return diffLines.join("\n");
995
995
  }
996
996
 
997
- function resolveStudioGitDiffBaseDir(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
997
+ function resolveStudioBaseDir(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
998
998
  const source = typeof sourcePath === "string" ? sourcePath.trim() : "";
999
999
  if (source) {
1000
- return dirname(source);
1000
+ const expanded = expandHome(source);
1001
+ return dirname(isAbsolute(expanded) ? expanded : resolve(fallbackCwd, expanded));
1001
1002
  }
1002
1003
 
1003
1004
  const resource = typeof resourceDir === "string" ? resourceDir.trim() : "";
1004
1005
  if (resource) {
1005
- return isAbsolute(resource) ? resource : resolve(fallbackCwd, resource);
1006
+ const expanded = expandHome(resource);
1007
+ return isAbsolute(expanded) ? expanded : resolve(fallbackCwd, expanded);
1006
1008
  }
1007
1009
 
1008
1010
  return fallbackCwd;
1009
1011
  }
1010
1012
 
1013
+ function resolveStudioGitDiffBaseDir(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
1014
+ return resolveStudioBaseDir(sourcePath, resourceDir, fallbackCwd);
1015
+ }
1016
+
1017
+ function resolveStudioPandocWorkingDir(baseDir: string | undefined): string | undefined {
1018
+ const normalized = typeof baseDir === "string" ? baseDir.trim() : "";
1019
+ if (!normalized) return undefined;
1020
+ try {
1021
+ return statSync(normalized).isDirectory() ? normalized : undefined;
1022
+ } catch {
1023
+ return undefined;
1024
+ }
1025
+ }
1026
+
1027
+ function stripStudioLatexComments(text: string): string {
1028
+ const lines = String(text ?? "").replace(/\r\n/g, "\n").split("\n");
1029
+ return lines.map((line) => {
1030
+ let out = "";
1031
+ let backslashRun = 0;
1032
+ for (let i = 0; i < line.length; i++) {
1033
+ const ch = line[i]!;
1034
+ if (ch === "%" && backslashRun % 2 === 0) break;
1035
+ out += ch;
1036
+ if (ch === "\\") backslashRun++;
1037
+ else backslashRun = 0;
1038
+ }
1039
+ return out;
1040
+ }).join("\n");
1041
+ }
1042
+
1043
+ function collectStudioLatexBibliographyCandidates(markdown: string): string[] {
1044
+ const stripped = stripStudioLatexComments(markdown);
1045
+ const candidates: string[] = [];
1046
+ const seen = new Set<string>();
1047
+ const pushCandidate = (raw: string) => {
1048
+ let candidate = String(raw ?? "").trim().replace(/^file:/i, "").replace(/^['"]|['"]$/g, "");
1049
+ if (!candidate) return;
1050
+ if (!/\.[A-Za-z0-9]+$/.test(candidate)) candidate += ".bib";
1051
+ if (seen.has(candidate)) return;
1052
+ seen.add(candidate);
1053
+ candidates.push(candidate);
1054
+ };
1055
+
1056
+ for (const match of stripped.matchAll(/\\bibliography\s*\{([^}]+)\}/g)) {
1057
+ const rawList = match[1] ?? "";
1058
+ for (const part of rawList.split(",")) {
1059
+ pushCandidate(part);
1060
+ }
1061
+ }
1062
+
1063
+ for (const match of stripped.matchAll(/\\addbibresource(?:\[[^\]]*\])?\s*\{([^}]+)\}/g)) {
1064
+ pushCandidate(match[1] ?? "");
1065
+ }
1066
+
1067
+ return candidates;
1068
+ }
1069
+
1070
+ function resolveStudioLatexBibliographyPaths(markdown: string, baseDir: string | undefined): string[] {
1071
+ const workingDir = resolveStudioPandocWorkingDir(baseDir);
1072
+ if (!workingDir) return [];
1073
+ const resolvedPaths: string[] = [];
1074
+ const seen = new Set<string>();
1075
+
1076
+ for (const candidate of collectStudioLatexBibliographyCandidates(markdown)) {
1077
+ const expanded = expandHome(candidate);
1078
+ const resolvedPath = isAbsolute(expanded) ? expanded : resolve(workingDir, expanded);
1079
+ try {
1080
+ if (!statSync(resolvedPath).isFile()) continue;
1081
+ if (seen.has(resolvedPath)) continue;
1082
+ seen.add(resolvedPath);
1083
+ resolvedPaths.push(resolvedPath);
1084
+ } catch {
1085
+ // Ignore missing bibliography files; pandoc can still render the document body.
1086
+ }
1087
+ }
1088
+
1089
+ return resolvedPaths;
1090
+ }
1091
+
1092
+ function buildStudioPandocBibliographyArgs(markdown: string, isLatex: boolean | undefined, baseDir: string | undefined): string[] {
1093
+ if (!isLatex) return [];
1094
+ const bibliographyPaths = resolveStudioLatexBibliographyPaths(markdown, baseDir);
1095
+ if (bibliographyPaths.length === 0) return [];
1096
+ return [
1097
+ "--citeproc",
1098
+ ...bibliographyPaths.flatMap((path) => ["--bibliography", path]),
1099
+ ];
1100
+ }
1101
+
1011
1102
  function readStudioGitDiff(baseDir: string):
1012
1103
  | { ok: true; text: string; label: string }
1013
1104
  | { ok: false; level: "info" | "warning" | "error"; message: string } {
@@ -1593,16 +1684,18 @@ async function preprocessStudioMermaidForPdf(markdown: string, workDir: string):
1593
1684
  async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string): Promise<string> {
1594
1685
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
1595
1686
  const inputFormat = isLatex ? "latex" : "markdown+lists_without_preceding_blankline+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris-raw_html";
1596
- const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none"];
1687
+ const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
1688
+ const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
1597
1689
  if (resourcePath) {
1598
1690
  args.push(`--resource-path=${resourcePath}`);
1599
1691
  // Embed images as data URIs so they render in the browser preview
1600
1692
  args.push("--embed-resources", "--standalone");
1601
1693
  }
1602
1694
  const normalizedMarkdown = isLatex ? markdown : normalizeObsidianImages(normalizeMathDelimiters(markdown));
1695
+ const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
1603
1696
 
1604
1697
  return await new Promise<string>((resolve, reject) => {
1605
- const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
1698
+ const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
1606
1699
  const stdoutChunks: Buffer[] = [];
1607
1700
  const stderrChunks: Buffer[] = [];
1608
1701
  let settled = false;
@@ -1743,6 +1836,8 @@ async function renderStudioPdfWithPandoc(
1743
1836
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
1744
1837
  const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
1745
1838
  const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorPdfLanguage);
1839
+ const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
1840
+ const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
1746
1841
 
1747
1842
  const runPandocPdfExport = async (
1748
1843
  inputFormat: string,
@@ -1766,12 +1861,13 @@ async function renderStudioPdfWithPandoc(
1766
1861
  "-V", "urlcolor=blue",
1767
1862
  "-V", "linkcolor=blue",
1768
1863
  "--include-in-header", preamblePath,
1864
+ ...bibliographyArgs,
1769
1865
  ];
1770
1866
  if (resourcePath) args.push(`--resource-path=${resourcePath}`);
1771
1867
 
1772
1868
  try {
1773
1869
  await new Promise<void>((resolve, reject) => {
1774
- const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
1870
+ const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
1775
1871
  const stderrChunks: Buffer[] = [];
1776
1872
  let settled = false;
1777
1873
 
@@ -1862,12 +1958,13 @@ async function renderStudioPdfWithPandoc(
1862
1958
  "-V", "urlcolor=blue",
1863
1959
  "-V", "linkcolor=blue",
1864
1960
  "--include-in-header", preamblePath,
1961
+ ...bibliographyArgs,
1865
1962
  ];
1866
1963
  if (resourcePath) args.push(`--resource-path=${resourcePath}`);
1867
1964
 
1868
1965
  try {
1869
1966
  await new Promise<void>((resolve, reject) => {
1870
- const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
1967
+ const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
1871
1968
  const stderrChunks: Buffer[] = [];
1872
1969
  let settled = false;
1873
1970
 
@@ -4054,7 +4151,7 @@ export default function (pi: ExtensionAPI) {
4054
4151
  parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
4055
4152
  ? (parsedBody as { resourceDir: string }).resourceDir
4056
4153
  : "";
4057
- const resourcePath = sourcePath ? dirname(sourcePath) : (userResourceDir || studioCwd || undefined);
4154
+ const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
4058
4155
  const isLatex = /\\documentclass\b|\\begin\{document\}/.test(markdown);
4059
4156
  const html = await renderStudioMarkdownWithPandoc(markdown, isLatex, resourcePath);
4060
4157
  respondJson(res, 200, { ok: true, html, renderer: "pandoc" });
@@ -4108,7 +4205,7 @@ export default function (pi: ExtensionAPI) {
4108
4205
  parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
4109
4206
  ? (parsedBody as { resourceDir: string }).resourceDir
4110
4207
  : "";
4111
- const resourcePath = sourcePath ? dirname(sourcePath) : (userResourceDir || studioCwd || undefined);
4208
+ const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
4112
4209
  const requestedIsLatex =
4113
4210
  parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { isLatex?: unknown }).isLatex === "boolean"
4114
4211
  ? (parsedBody as { isLatex: boolean }).isLatex
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.19",
3
+ "version": "0.5.20",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",