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 +12 -0
- package/client/studio-client.js +1 -1
- package/client/studio.css +5 -0
- package/index.ts +135 -14
- package/package.json +1 -1
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
|
package/client/studio-client.js
CHANGED
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
};
|
|
164
164
|
let activePane = "left";
|
|
165
165
|
let paneFocusTarget = "off";
|
|
166
|
-
const EDITOR_HIGHLIGHT_MAX_CHARS =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4631
|
-
|
|
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();
|