pi-studio 0.5.18 → 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,18 @@ 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
+
14
+ ## [0.5.19] — 2026-03-19
15
+
16
+ ### Fixed
17
+ - Studio now waits until `agent_end` before emitting the terminal/cmux “response ready” notification for completed requests, and it keeps the cmux `running…` status pill visible until that same turn fully finishes.
18
+
7
19
  ## [0.5.18] — 2026-03-17
8
20
 
9
21
  ### 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
 
@@ -2952,6 +3049,7 @@ export default function (pi: ExtensionAPI) {
2952
3049
  let lastCommandCtx: ExtensionCommandContext | null = null;
2953
3050
  let lastThemeVarsJson = "";
2954
3051
  let suppressedStudioResponse: { requestId: string; kind: StudioRequestKind } | null = null;
3052
+ let pendingStudioCompletionKind: StudioRequestKind | null = null;
2955
3053
  let agentBusy = false;
2956
3054
  let terminalActivityPhase: TerminalActivityPhase = "idle";
2957
3055
  let terminalActivityToolName: string | null = null;
@@ -3150,7 +3248,7 @@ export default function (pi: ExtensionAPI) {
3150
3248
  if (!shouldUseCmuxTerminalIntegration()) return;
3151
3249
  const workspaceArgs = getCmuxWorkspaceArgs();
3152
3250
  const statusColor = getCmuxStudioStatusColor();
3153
- if (activeRequest) {
3251
+ if (activeRequest || (pendingStudioCompletionKind && agentBusy)) {
3154
3252
  runCmuxCommand([
3155
3253
  "set-status",
3156
3254
  CMUX_STUDIO_STATUS_KEY,
@@ -3269,6 +3367,23 @@ export default function (pi: ExtensionAPI) {
3269
3367
  return "Studio: response ready.";
3270
3368
  };
3271
3369
 
3370
+ const clearPendingStudioCompletion = () => {
3371
+ if (!pendingStudioCompletionKind) return;
3372
+ pendingStudioCompletionKind = null;
3373
+ syncCmuxStudioStatus();
3374
+ };
3375
+
3376
+ const flushPendingStudioCompletionNotification = () => {
3377
+ if (!pendingStudioCompletionKind) return;
3378
+ const kind = pendingStudioCompletionKind;
3379
+ pendingStudioCompletionKind = null;
3380
+ syncCmuxStudioStatus();
3381
+ const message = getStudioRequestCompletionNotification(kind);
3382
+ emitDebugEvent("studio_completion_notification", { kind });
3383
+ notifyStudio(message, "info");
3384
+ notifyStudioTerminal(message, "info");
3385
+ };
3386
+
3272
3387
  const refreshContextUsage = (
3273
3388
  ctx?: { getContextUsage(): { tokens: number | null; contextWindow: number; percent: number | null } | undefined },
3274
3389
  ): StudioContextUsageSnapshot => {
@@ -4036,7 +4151,7 @@ export default function (pi: ExtensionAPI) {
4036
4151
  parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
4037
4152
  ? (parsedBody as { resourceDir: string }).resourceDir
4038
4153
  : "";
4039
- const resourcePath = sourcePath ? dirname(sourcePath) : (userResourceDir || studioCwd || undefined);
4154
+ const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
4040
4155
  const isLatex = /\\documentclass\b|\\begin\{document\}/.test(markdown);
4041
4156
  const html = await renderStudioMarkdownWithPandoc(markdown, isLatex, resourcePath);
4042
4157
  respondJson(res, 200, { ok: true, html, renderer: "pandoc" });
@@ -4090,7 +4205,7 @@ export default function (pi: ExtensionAPI) {
4090
4205
  parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
4091
4206
  ? (parsedBody as { resourceDir: string }).resourceDir
4092
4207
  : "";
4093
- const resourcePath = sourcePath ? dirname(sourcePath) : (userResourceDir || studioCwd || undefined);
4208
+ const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
4094
4209
  const requestedIsLatex =
4095
4210
  parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { isLatex?: unknown }).isLatex === "boolean"
4096
4211
  ? (parsedBody as { isLatex: boolean }).isLatex
@@ -4418,6 +4533,7 @@ export default function (pi: ExtensionAPI) {
4418
4533
  const stopServer = async () => {
4419
4534
  if (!serverState) return;
4420
4535
  clearActiveRequest();
4536
+ clearPendingStudioCompletion();
4421
4537
  clearCompactionState();
4422
4538
  closeAllClients(1001, "Server shutting down");
4423
4539
 
@@ -4449,6 +4565,7 @@ export default function (pi: ExtensionAPI) {
4449
4565
  hydrateLatestAssistant(ctx.sessionManager.getBranch());
4450
4566
  clearCompactionState();
4451
4567
  agentBusy = false;
4568
+ clearPendingStudioCompletion();
4452
4569
  refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
4453
4570
  refreshContextUsage(ctx);
4454
4571
  emitDebugEvent("session_start", {
@@ -4467,6 +4584,7 @@ export default function (pi: ExtensionAPI) {
4467
4584
  lastCommandCtx = null;
4468
4585
  hydrateLatestAssistant(ctx.sessionManager.getBranch());
4469
4586
  agentBusy = false;
4587
+ clearPendingStudioCompletion();
4470
4588
  refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
4471
4589
  refreshContextUsage(ctx);
4472
4590
  emitDebugEvent("session_switch", {
@@ -4627,10 +4745,8 @@ export default function (pi: ExtensionAPI) {
4627
4745
  responseHistory: studioResponseHistory,
4628
4746
  });
4629
4747
  broadcastResponseHistory();
4630
- clearActiveRequest({
4631
- terminalNotify: getStudioRequestCompletionNotification(kind),
4632
- terminalNotifyLevel: "info",
4633
- });
4748
+ pendingStudioCompletionKind = kind;
4749
+ clearActiveRequest();
4634
4750
  return;
4635
4751
  }
4636
4752
 
@@ -4667,6 +4783,7 @@ export default function (pi: ExtensionAPI) {
4667
4783
  activeRequestKind: activeRequest?.kind ?? null,
4668
4784
  suppressedRequestId: suppressedStudioResponse?.requestId ?? null,
4669
4785
  suppressedRequestKind: suppressedStudioResponse?.kind ?? null,
4786
+ pendingCompletionKind: pendingStudioCompletionKind,
4670
4787
  });
4671
4788
  setTerminalActivity("idle");
4672
4789
  if (activeRequest) {
@@ -4677,6 +4794,9 @@ export default function (pi: ExtensionAPI) {
4677
4794
  message: "Request ended without a complete assistant response.",
4678
4795
  });
4679
4796
  clearActiveRequest();
4797
+ clearPendingStudioCompletion();
4798
+ } else {
4799
+ flushPendingStudioCompletionNotification();
4680
4800
  }
4681
4801
  suppressedStudioResponse = null;
4682
4802
  });
@@ -4684,6 +4804,7 @@ export default function (pi: ExtensionAPI) {
4684
4804
  pi.on("session_shutdown", async () => {
4685
4805
  lastCommandCtx = null;
4686
4806
  agentBusy = false;
4807
+ clearPendingStudioCompletion();
4687
4808
  clearCompactionState();
4688
4809
  setTerminalActivity("idle");
4689
4810
  await stopServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.18",
3
+ "version": "0.5.20",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",