pi-studio 0.9.1 → 0.9.2
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 +11 -0
- package/README.md +3 -1
- package/client/studio-client.js +393 -20
- package/client/studio.css +286 -2
- package/index.ts +288 -323
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -415,6 +415,11 @@ const STUDIO_REPL_SEND_MAX_CHARS = 200_000;
|
|
|
415
415
|
const STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS = 20_000;
|
|
416
416
|
const STUDIO_REPL_SEND_MAX_TIMEOUT_MS = 120_000;
|
|
417
417
|
const STUDIO_REPL_CONTROL_ROOT = join(tmpdir(), "pi-studio-repl");
|
|
418
|
+
const STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES = 2_000_000;
|
|
419
|
+
const STUDIO_PANDOC_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_PANDOC_TIMEOUT_MS", 120_000, 5_000, 15 * 60_000);
|
|
420
|
+
const STUDIO_LATEX_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_LATEX_TIMEOUT_MS", 120_000, 5_000, 15 * 60_000);
|
|
421
|
+
const STUDIO_MERMAID_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_MERMAID_TIMEOUT_MS", 60_000, 5_000, 10 * 60_000);
|
|
422
|
+
const STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES = readStudioPositiveEnvMs("PI_STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES", 50_000_000, 1_000_000, 500_000_000);
|
|
418
423
|
const STUDIO_REPL_RUNTIME_LABELS: Record<StudioReplRuntime, string> = {
|
|
419
424
|
shell: "Shell",
|
|
420
425
|
python: "Python",
|
|
@@ -446,6 +451,151 @@ const STUDIO_DEFAULT_SCRATCHPAD_DOCUMENT_KEY = "doc:blank:blank";
|
|
|
446
451
|
const STUDIO_PERSISTENT_STATE_DIR = join(getAgentDir(), "pi-studio");
|
|
447
452
|
const STUDIO_PERSISTENT_STATE_PATH = join(STUDIO_PERSISTENT_STATE_DIR, "local-state.json");
|
|
448
453
|
|
|
454
|
+
type StudioSubprocessResult = {
|
|
455
|
+
code: number | null;
|
|
456
|
+
signal: NodeJS.Signals | null;
|
|
457
|
+
stdout: string;
|
|
458
|
+
stderr: string;
|
|
459
|
+
stdoutTruncated: boolean;
|
|
460
|
+
stderrTruncated: boolean;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
type StudioSubprocessOptions = {
|
|
464
|
+
cwd?: string;
|
|
465
|
+
input?: string;
|
|
466
|
+
timeoutMs?: number;
|
|
467
|
+
stdoutMaxBytes?: number;
|
|
468
|
+
stderrMaxBytes?: number;
|
|
469
|
+
notFoundMessage?: string;
|
|
470
|
+
notFoundError?: () => Error;
|
|
471
|
+
label?: string;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
function readStudioPositiveEnvMs(name: string, fallback: number, min: number, max: number): number {
|
|
475
|
+
const parsed = Number.parseInt(String(process.env[name] ?? ""), 10);
|
|
476
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
477
|
+
return Math.max(min, Math.min(max, parsed));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function appendStudioSubprocessChunk(chunks: Buffer[], chunk: Buffer | string, state: { bytes: number; truncated: boolean }, maxBytes = STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES): void {
|
|
481
|
+
if (state.bytes >= maxBytes) {
|
|
482
|
+
state.truncated = true;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
486
|
+
const remaining = maxBytes - state.bytes;
|
|
487
|
+
if (buffer.length <= remaining) {
|
|
488
|
+
chunks.push(buffer);
|
|
489
|
+
state.bytes += buffer.length;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
chunks.push(buffer.subarray(0, remaining));
|
|
493
|
+
state.bytes += remaining;
|
|
494
|
+
state.truncated = true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function finalizeStudioSubprocessOutput(chunks: Buffer[], truncated: boolean): string {
|
|
498
|
+
const value = Buffer.concat(chunks).toString("utf-8").trim();
|
|
499
|
+
return truncated ? `${value}\n[output truncated by Studio]`.trim() : value;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function runStudioSubprocess(command: string, args: string[], options: StudioSubprocessOptions = {}): Promise<StudioSubprocessResult> {
|
|
503
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
504
|
+
const timeoutMs = Math.max(1_000, Math.floor(options.timeoutMs ?? STUDIO_PANDOC_TIMEOUT_MS));
|
|
505
|
+
const child = spawn(command, args, {
|
|
506
|
+
cwd: options.cwd,
|
|
507
|
+
stdio: [typeof options.input === "string" ? "pipe" : "ignore", "pipe", "pipe"],
|
|
508
|
+
});
|
|
509
|
+
const stdoutChunks: Buffer[] = [];
|
|
510
|
+
const stderrChunks: Buffer[] = [];
|
|
511
|
+
const stdoutMaxBytes = Math.max(1, Math.floor(options.stdoutMaxBytes ?? STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES));
|
|
512
|
+
const stderrMaxBytes = Math.max(1, Math.floor(options.stderrMaxBytes ?? STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES));
|
|
513
|
+
const stdoutState = { bytes: 0, truncated: false };
|
|
514
|
+
const stderrState = { bytes: 0, truncated: false };
|
|
515
|
+
let settled = false;
|
|
516
|
+
let timedOut = false;
|
|
517
|
+
let killTimer: NodeJS.Timeout | null = null;
|
|
518
|
+
|
|
519
|
+
const cleanup = () => {
|
|
520
|
+
clearTimeout(timeoutTimer);
|
|
521
|
+
if (killTimer) clearTimeout(killTimer);
|
|
522
|
+
};
|
|
523
|
+
const fail = (error: Error) => {
|
|
524
|
+
if (settled) return;
|
|
525
|
+
settled = true;
|
|
526
|
+
cleanup();
|
|
527
|
+
rejectPromise(error);
|
|
528
|
+
};
|
|
529
|
+
const succeed = (result: StudioSubprocessResult) => {
|
|
530
|
+
if (settled) return;
|
|
531
|
+
settled = true;
|
|
532
|
+
cleanup();
|
|
533
|
+
resolvePromise(result);
|
|
534
|
+
};
|
|
535
|
+
const label = options.label || basename(command) || command;
|
|
536
|
+
const timeoutTimer = setTimeout(() => {
|
|
537
|
+
timedOut = true;
|
|
538
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
539
|
+
killTimer = setTimeout(() => {
|
|
540
|
+
try { child.kill("SIGKILL"); } catch {}
|
|
541
|
+
}, 2_000);
|
|
542
|
+
}, timeoutMs);
|
|
543
|
+
|
|
544
|
+
child.stdout?.on("data", (chunk: Buffer | string) => appendStudioSubprocessChunk(stdoutChunks, chunk, stdoutState, stdoutMaxBytes));
|
|
545
|
+
child.stderr?.on("data", (chunk: Buffer | string) => appendStudioSubprocessChunk(stderrChunks, chunk, stderrState, stderrMaxBytes));
|
|
546
|
+
|
|
547
|
+
child.once("error", (error) => {
|
|
548
|
+
const errno = error as NodeJS.ErrnoException;
|
|
549
|
+
if (errno.code === "ENOENT") {
|
|
550
|
+
fail(options.notFoundError ? options.notFoundError() : new Error(options.notFoundMessage || `${command} was not found.`));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
fail(error);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
child.once("close", (code, signal) => {
|
|
557
|
+
if (timedOut) {
|
|
558
|
+
fail(new Error(`${label} timed out after ${Math.round(timeoutMs / 1000)}s.`));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
succeed({
|
|
562
|
+
code,
|
|
563
|
+
signal,
|
|
564
|
+
stdout: finalizeStudioSubprocessOutput(stdoutChunks, stdoutState.truncated),
|
|
565
|
+
stderr: finalizeStudioSubprocessOutput(stderrChunks, stderrState.truncated),
|
|
566
|
+
stdoutTruncated: stdoutState.truncated,
|
|
567
|
+
stderrTruncated: stderrState.truncated,
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
if (typeof options.input === "string") {
|
|
572
|
+
child.stdin?.end(options.input);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function buildStudioPandocPdfEngineOptArgs(pdfEngine: string): string[] {
|
|
578
|
+
const engineName = basename(String(pdfEngine || "")).toLowerCase();
|
|
579
|
+
if (!/^(?:pdf|xe|lua)?latex$/.test(engineName)) return [];
|
|
580
|
+
return [
|
|
581
|
+
"--pdf-engine-opt=-interaction=nonstopmode",
|
|
582
|
+
"--pdf-engine-opt=-halt-on-error",
|
|
583
|
+
"--pdf-engine-opt=-file-line-error",
|
|
584
|
+
];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const STUDIO_PANDOC_HTML_FRAGMENT_TEMPLATE = `<!doctype html>
|
|
588
|
+
<html>
|
|
589
|
+
<head>
|
|
590
|
+
<meta charset="utf-8" />
|
|
591
|
+
<title>pi Studio preview</title>
|
|
592
|
+
</head>
|
|
593
|
+
<body>
|
|
594
|
+
$body$
|
|
595
|
+
</body>
|
|
596
|
+
</html>
|
|
597
|
+
`;
|
|
598
|
+
|
|
449
599
|
let studioPersistentStateCache: StudioPersistentState | null = null;
|
|
450
600
|
let studioPersistentStateQueue: Promise<void> = Promise.resolve();
|
|
451
601
|
let transientStudioDocuments: Map<string, { document: InitialStudioDocument; createdAt: number }> = new Map();
|
|
@@ -4484,46 +4634,17 @@ async function renderStudioMermaidDiagramForPdf(source: string, workDir: string,
|
|
|
4484
4634
|
const outputPath = join(workDir, `mermaid-diagram-${blockNumber}.pdf`);
|
|
4485
4635
|
|
|
4486
4636
|
await writeFile(inputPath, source, "utf-8");
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
if (settled) return;
|
|
4495
|
-
settled = true;
|
|
4496
|
-
reject(error);
|
|
4497
|
-
};
|
|
4498
|
-
|
|
4499
|
-
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
4500
|
-
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
4501
|
-
});
|
|
4502
|
-
|
|
4503
|
-
child.once("error", (error) => {
|
|
4504
|
-
const errno = error as NodeJS.ErrnoException;
|
|
4505
|
-
if (errno.code === "ENOENT") {
|
|
4506
|
-
fail(
|
|
4507
|
-
new MermaidCliMissingError(
|
|
4508
|
-
"Mermaid CLI (mmdc) not found. Install with `npm install -g @mermaid-js/mermaid-cli` or set MERMAID_CLI_PATH.",
|
|
4509
|
-
),
|
|
4510
|
-
);
|
|
4511
|
-
return;
|
|
4512
|
-
}
|
|
4513
|
-
fail(error);
|
|
4514
|
-
});
|
|
4515
|
-
|
|
4516
|
-
child.once("close", (code) => {
|
|
4517
|
-
if (settled) return;
|
|
4518
|
-
settled = true;
|
|
4519
|
-
if (code === 0) {
|
|
4520
|
-
resolve();
|
|
4521
|
-
return;
|
|
4522
|
-
}
|
|
4523
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
4524
|
-
reject(new Error(`Mermaid CLI failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`));
|
|
4525
|
-
});
|
|
4637
|
+
const args = ["-i", inputPath, "-o", outputPath, "-t", mermaidTheme, "-f"];
|
|
4638
|
+
const result = await runStudioSubprocess(mermaidCommand, args, {
|
|
4639
|
+
timeoutMs: STUDIO_MERMAID_TIMEOUT_MS,
|
|
4640
|
+
label: "Mermaid CLI",
|
|
4641
|
+
notFoundError: () => new MermaidCliMissingError(
|
|
4642
|
+
"Mermaid CLI (mmdc) not found. Install with `npm install -g @mermaid-js/mermaid-cli` or set MERMAID_CLI_PATH.",
|
|
4643
|
+
),
|
|
4526
4644
|
});
|
|
4645
|
+
if (result.code !== 0) {
|
|
4646
|
+
throw new Error(`Mermaid CLI failed with exit code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`);
|
|
4647
|
+
}
|
|
4527
4648
|
|
|
4528
4649
|
return outputPath;
|
|
4529
4650
|
}
|
|
@@ -4708,82 +4829,67 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
4708
4829
|
const inputFormat = isLatex ? "latex" : "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-raw_html";
|
|
4709
4830
|
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
4710
4831
|
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
|
|
4832
|
+
let htmlTemplateDir: string | null = null;
|
|
4711
4833
|
if (resourcePath) {
|
|
4712
4834
|
args.push(`--resource-path=${resourcePath}`);
|
|
4713
|
-
// Embed images as data URIs so
|
|
4714
|
-
|
|
4835
|
+
// Embed images as data URIs so browser previews and exported HTML keep local figures.
|
|
4836
|
+
// A minimal template prevents Pandoc's standalone default CSS/title block from leaking
|
|
4837
|
+
// into Studio's own standalone export wrapper.
|
|
4838
|
+
htmlTemplateDir = join(tmpdir(), `pi-studio-pandoc-html-${randomUUID()}`);
|
|
4839
|
+
await mkdir(htmlTemplateDir, { recursive: true });
|
|
4840
|
+
const htmlTemplatePath = join(htmlTemplateDir, "template.html");
|
|
4841
|
+
await writeFile(htmlTemplatePath, STUDIO_PANDOC_HTML_FRAGMENT_TEMPLATE, "utf-8");
|
|
4842
|
+
args.push("--embed-resources", "--standalone", `--template=${htmlTemplatePath}`);
|
|
4715
4843
|
}
|
|
4716
4844
|
const normalizedMarkdown = isLatex
|
|
4717
4845
|
? sourceWithResolvedRefs
|
|
4718
4846
|
: normalizeStudioMarkdownFencedBlocks(prepareStudioMarkdownForPandoc(sourceWithResolvedRefs));
|
|
4719
4847
|
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
4720
4848
|
|
|
4721
|
-
let
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
reject(error);
|
|
4731
|
-
};
|
|
4732
|
-
|
|
4733
|
-
const succeed = (html: string) => {
|
|
4734
|
-
if (settled) return;
|
|
4735
|
-
settled = true;
|
|
4736
|
-
resolve(html);
|
|
4737
|
-
};
|
|
4738
|
-
|
|
4739
|
-
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
4740
|
-
stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
4741
|
-
});
|
|
4742
|
-
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
4743
|
-
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
4744
|
-
});
|
|
4745
|
-
|
|
4746
|
-
child.once("error", (error) => {
|
|
4747
|
-
const errno = error as NodeJS.ErrnoException;
|
|
4748
|
-
if (errno.code === "ENOENT") {
|
|
4749
|
-
fail(new Error("pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."));
|
|
4750
|
-
return;
|
|
4751
|
-
}
|
|
4752
|
-
fail(error);
|
|
4753
|
-
});
|
|
4754
|
-
|
|
4755
|
-
child.once("close", (code) => {
|
|
4756
|
-
if (settled) return;
|
|
4757
|
-
if (code === 0) {
|
|
4758
|
-
let html = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
4759
|
-
// When --standalone was used, extract only the <body> content
|
|
4760
|
-
if (resourcePath) {
|
|
4761
|
-
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
4762
|
-
if (bodyMatch) html = bodyMatch[1];
|
|
4763
|
-
}
|
|
4764
|
-
if (isLatex) {
|
|
4765
|
-
html = decorateStudioLatexRenderedHtml(
|
|
4766
|
-
html,
|
|
4767
|
-
sourcePath,
|
|
4768
|
-
resourcePath,
|
|
4769
|
-
latexSubfigurePreviewTransform.subfigureGroups,
|
|
4770
|
-
latexAlgorithmPreviewTransform.algorithmBlocks,
|
|
4771
|
-
);
|
|
4772
|
-
} else {
|
|
4773
|
-
html = decorateStudioPreviewPageBreakHtml(html);
|
|
4774
|
-
}
|
|
4775
|
-
html = decorateStudioPandocSyntaxHtml(html);
|
|
4776
|
-
succeed(stripMathMlAnnotationTags(html));
|
|
4777
|
-
return;
|
|
4778
|
-
}
|
|
4779
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
4780
|
-
fail(new Error(`pandoc failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`));
|
|
4849
|
+
let pandocResult: StudioSubprocessResult;
|
|
4850
|
+
try {
|
|
4851
|
+
pandocResult = await runStudioSubprocess(pandocCommand, args, {
|
|
4852
|
+
cwd: pandocWorkingDir,
|
|
4853
|
+
input: normalizedMarkdown,
|
|
4854
|
+
timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
|
|
4855
|
+
stdoutMaxBytes: STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES,
|
|
4856
|
+
label: "pandoc HTML render",
|
|
4857
|
+
notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
|
|
4781
4858
|
});
|
|
4859
|
+
} finally {
|
|
4860
|
+
if (htmlTemplateDir) {
|
|
4861
|
+
await rm(htmlTemplateDir, { recursive: true, force: true }).catch(() => undefined);
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
if (pandocResult.code !== 0) {
|
|
4865
|
+
throw new Error(`pandoc failed with exit code ${pandocResult.code}${pandocResult.stderr ? `: ${pandocResult.stderr}` : ""}`);
|
|
4866
|
+
}
|
|
4867
|
+
if (pandocResult.stdoutTruncated) {
|
|
4868
|
+
throw new Error(`pandoc HTML output exceeded ${Math.round(STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES / 1_000_000)} MB. Reduce embedded assets or set PI_STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES higher.`);
|
|
4869
|
+
}
|
|
4782
4870
|
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4871
|
+
let renderedHtml = pandocResult.stdout;
|
|
4872
|
+
// When --standalone was used for --embed-resources, extract only the <body> content.
|
|
4873
|
+
if (resourcePath) {
|
|
4874
|
+
const bodyMatch = renderedHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
4875
|
+
if (!bodyMatch) {
|
|
4876
|
+
throw new Error("pandoc HTML render did not include a complete body element.");
|
|
4877
|
+
}
|
|
4878
|
+
renderedHtml = bodyMatch[1];
|
|
4879
|
+
}
|
|
4880
|
+
if (isLatex) {
|
|
4881
|
+
renderedHtml = decorateStudioLatexRenderedHtml(
|
|
4882
|
+
renderedHtml,
|
|
4883
|
+
sourcePath,
|
|
4884
|
+
resourcePath,
|
|
4885
|
+
latexSubfigurePreviewTransform.subfigureGroups,
|
|
4886
|
+
latexAlgorithmPreviewTransform.algorithmBlocks,
|
|
4887
|
+
);
|
|
4888
|
+
} else {
|
|
4889
|
+
renderedHtml = decorateStudioPreviewPageBreakHtml(renderedHtml);
|
|
4890
|
+
}
|
|
4891
|
+
renderedHtml = decorateStudioPandocSyntaxHtml(renderedHtml);
|
|
4892
|
+
return stripMathMlAnnotationTags(renderedHtml);
|
|
4787
4893
|
}
|
|
4788
4894
|
|
|
4789
4895
|
function escapeStudioRegExpLiteral(text: string): string {
|
|
@@ -5175,54 +5281,22 @@ ${literalPdfConfig.fontSizeCommand}\\section*{${title.replace(/[{}\\]/g, "").tri
|
|
|
5175
5281
|
await writeFile(texPath, texDocument, "utf-8");
|
|
5176
5282
|
|
|
5177
5283
|
try {
|
|
5178
|
-
await
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
const fail = (error: Error) => {
|
|
5189
|
-
if (settled) return;
|
|
5190
|
-
settled = true;
|
|
5191
|
-
reject(error);
|
|
5192
|
-
};
|
|
5193
|
-
|
|
5194
|
-
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
5195
|
-
stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
5196
|
-
});
|
|
5197
|
-
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
5198
|
-
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
5199
|
-
});
|
|
5200
|
-
|
|
5201
|
-
child.once("error", (error) => {
|
|
5202
|
-
const errno = error as NodeJS.ErrnoException;
|
|
5203
|
-
if (errno.code === "ENOENT") {
|
|
5204
|
-
fail(new Error(
|
|
5205
|
-
`${pdfEngine} was not found. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE.`,
|
|
5206
|
-
));
|
|
5207
|
-
return;
|
|
5208
|
-
}
|
|
5209
|
-
fail(error);
|
|
5210
|
-
});
|
|
5211
|
-
|
|
5212
|
-
child.once("close", (code) => {
|
|
5213
|
-
if (settled) return;
|
|
5214
|
-
if (code === 0) {
|
|
5215
|
-
settled = true;
|
|
5216
|
-
resolve();
|
|
5217
|
-
return;
|
|
5218
|
-
}
|
|
5219
|
-
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
5220
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
5221
|
-
const errorMatch = stdout.match(/^! .+$/m);
|
|
5222
|
-
const hint = errorMatch ? `: ${errorMatch[0]}` : (stderr ? `: ${stderr}` : "");
|
|
5223
|
-
fail(new Error(`${pdfEngine} literal-text PDF export failed with exit code ${code}${hint}`));
|
|
5224
|
-
});
|
|
5284
|
+
const latexResult = await runStudioSubprocess(pdfEngine, [
|
|
5285
|
+
"-interaction=nonstopmode",
|
|
5286
|
+
"-halt-on-error",
|
|
5287
|
+
"-file-line-error",
|
|
5288
|
+
"input.tex",
|
|
5289
|
+
], {
|
|
5290
|
+
cwd: tempDir,
|
|
5291
|
+
timeoutMs: STUDIO_LATEX_TIMEOUT_MS,
|
|
5292
|
+
label: `${pdfEngine} literal-text PDF export`,
|
|
5293
|
+
notFoundMessage: `${pdfEngine} was not found. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE.`,
|
|
5225
5294
|
});
|
|
5295
|
+
if (latexResult.code !== 0) {
|
|
5296
|
+
const errorMatch = latexResult.stdout.match(/^! .+$/m);
|
|
5297
|
+
const hint = errorMatch ? `: ${errorMatch[0]}` : (latexResult.stderr ? `: ${latexResult.stderr}` : "");
|
|
5298
|
+
throw new Error(`${pdfEngine} literal-text PDF export failed with exit code ${latexResult.code}${hint}`);
|
|
5299
|
+
}
|
|
5226
5300
|
|
|
5227
5301
|
return await readFile(outputPath);
|
|
5228
5302
|
} finally {
|
|
@@ -5484,46 +5558,18 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
5484
5558
|
const pandocSource = inputFormat === "latex" ? markdown : normalizeStudioMarkdownFencedBlocks(markdown);
|
|
5485
5559
|
|
|
5486
5560
|
try {
|
|
5487
|
-
await
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
reject(error);
|
|
5496
|
-
};
|
|
5497
|
-
|
|
5498
|
-
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
5499
|
-
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
5500
|
-
});
|
|
5501
|
-
|
|
5502
|
-
child.once("error", (error) => {
|
|
5503
|
-
const errno = error as NodeJS.ErrnoException;
|
|
5504
|
-
if (errno.code === "ENOENT") {
|
|
5505
|
-
const commandHint = pandocCommand === "pandoc"
|
|
5506
|
-
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
5507
|
-
: `${pandocCommand} was not found. Check PANDOC_PATH.`;
|
|
5508
|
-
fail(new Error(commandHint));
|
|
5509
|
-
return;
|
|
5510
|
-
}
|
|
5511
|
-
fail(error);
|
|
5512
|
-
});
|
|
5513
|
-
|
|
5514
|
-
child.once("close", (code) => {
|
|
5515
|
-
if (settled) return;
|
|
5516
|
-
if (code === 0) {
|
|
5517
|
-
settled = true;
|
|
5518
|
-
resolve();
|
|
5519
|
-
return;
|
|
5520
|
-
}
|
|
5521
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
5522
|
-
fail(new Error(`pandoc LaTeX generation failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`));
|
|
5523
|
-
});
|
|
5524
|
-
|
|
5525
|
-
child.stdin.end(pandocSource);
|
|
5561
|
+
const pandocResult = await runStudioSubprocess(pandocCommand, pandocArgs, {
|
|
5562
|
+
cwd: pandocWorkingDir,
|
|
5563
|
+
input: pandocSource,
|
|
5564
|
+
timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
|
|
5565
|
+
label: "pandoc LaTeX generation",
|
|
5566
|
+
notFoundMessage: pandocCommand === "pandoc"
|
|
5567
|
+
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
5568
|
+
: `${pandocCommand} was not found. Check PANDOC_PATH.`,
|
|
5526
5569
|
});
|
|
5570
|
+
if (pandocResult.code !== 0) {
|
|
5571
|
+
throw new Error(`pandoc LaTeX generation failed with exit code ${pandocResult.code}${pandocResult.stderr ? `: ${pandocResult.stderr}` : ""}`);
|
|
5572
|
+
}
|
|
5527
5573
|
|
|
5528
5574
|
const generatedLatex = await readFile(latexPath, "utf-8");
|
|
5529
5575
|
const injectedLatex = injectStudioLatexPdfSubfigureBlocks(generatedLatex, subfigureGroups, sourcePath, resourcePath);
|
|
@@ -5534,55 +5580,23 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
5534
5580
|
const normalizedLatex = normalizeStudioGeneratedFigureCaptions(alignedReadyLatex);
|
|
5535
5581
|
await writeFile(latexPath, normalizedLatex, "utf-8");
|
|
5536
5582
|
|
|
5537
|
-
await
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
const fail = (error: Error) => {
|
|
5549
|
-
if (settled) return;
|
|
5550
|
-
settled = true;
|
|
5551
|
-
reject(error);
|
|
5552
|
-
};
|
|
5553
|
-
|
|
5554
|
-
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
5555
|
-
stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
5556
|
-
});
|
|
5557
|
-
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
5558
|
-
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
5559
|
-
});
|
|
5560
|
-
|
|
5561
|
-
child.once("error", (error) => {
|
|
5562
|
-
const errno = error as NodeJS.ErrnoException;
|
|
5563
|
-
if (errno.code === "ENOENT") {
|
|
5564
|
-
fail(new Error(
|
|
5565
|
-
`${pdfEngine} was not found. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE.`,
|
|
5566
|
-
));
|
|
5567
|
-
return;
|
|
5568
|
-
}
|
|
5569
|
-
fail(error);
|
|
5570
|
-
});
|
|
5571
|
-
|
|
5572
|
-
child.once("close", (code) => {
|
|
5573
|
-
if (settled) return;
|
|
5574
|
-
if (code === 0) {
|
|
5575
|
-
settled = true;
|
|
5576
|
-
resolve();
|
|
5577
|
-
return;
|
|
5578
|
-
}
|
|
5579
|
-
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
5580
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
5581
|
-
const errorMatch = stdout.match(/^! .+$/m);
|
|
5582
|
-
const hint = errorMatch ? `: ${errorMatch[0]}` : (stderr ? `: ${stderr}` : "");
|
|
5583
|
-
fail(new Error(`${pdfEngine} PDF export failed with exit code ${code}${hint}`));
|
|
5584
|
-
});
|
|
5583
|
+
const latexResult = await runStudioSubprocess(pdfEngine, [
|
|
5584
|
+
"-interaction=nonstopmode",
|
|
5585
|
+
"-halt-on-error",
|
|
5586
|
+
"-file-line-error",
|
|
5587
|
+
`-output-directory=${tempDir}`,
|
|
5588
|
+
latexPath,
|
|
5589
|
+
], {
|
|
5590
|
+
cwd: pandocWorkingDir,
|
|
5591
|
+
timeoutMs: STUDIO_LATEX_TIMEOUT_MS,
|
|
5592
|
+
label: `${pdfEngine} PDF export`,
|
|
5593
|
+
notFoundMessage: `${pdfEngine} was not found. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE.`,
|
|
5585
5594
|
});
|
|
5595
|
+
if (latexResult.code !== 0) {
|
|
5596
|
+
const errorMatch = latexResult.stdout.match(/^! .+$/m);
|
|
5597
|
+
const hint = errorMatch ? `: ${errorMatch[0]}` : (latexResult.stderr ? `: ${latexResult.stderr}` : "");
|
|
5598
|
+
throw new Error(`${pdfEngine} PDF export failed with exit code ${latexResult.code}${hint}`);
|
|
5599
|
+
}
|
|
5586
5600
|
|
|
5587
5601
|
return { pdf: await readFile(outputPath) };
|
|
5588
5602
|
} finally {
|
|
@@ -5642,6 +5656,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
5642
5656
|
"-f", inputFormat,
|
|
5643
5657
|
"-o", outputPath,
|
|
5644
5658
|
`--pdf-engine=${pdfEngine}`,
|
|
5659
|
+
...buildStudioPandocPdfEngineOptArgs(pdfEngine),
|
|
5645
5660
|
...buildStudioPdfPandocVariableArgs(pdfOptions, inputFormat !== "latex"),
|
|
5646
5661
|
"-V", "urlcolor=blue",
|
|
5647
5662
|
"-V", "linkcolor=blue",
|
|
@@ -5651,49 +5666,22 @@ async function renderStudioPdfWithPandoc(
|
|
|
5651
5666
|
if (resourcePath) args.push(`--resource-path=${resourcePath}`);
|
|
5652
5667
|
|
|
5653
5668
|
try {
|
|
5654
|
-
await
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
reject(error);
|
|
5663
|
-
};
|
|
5664
|
-
|
|
5665
|
-
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
5666
|
-
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
5667
|
-
});
|
|
5668
|
-
|
|
5669
|
-
child.once("error", (error) => {
|
|
5670
|
-
const errno = error as NodeJS.ErrnoException;
|
|
5671
|
-
if (errno.code === "ENOENT") {
|
|
5672
|
-
const commandHint = pandocCommand === "pandoc"
|
|
5673
|
-
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
5674
|
-
: `${pandocCommand} was not found. Check PANDOC_PATH.`;
|
|
5675
|
-
fail(new Error(commandHint));
|
|
5676
|
-
return;
|
|
5677
|
-
}
|
|
5678
|
-
fail(error);
|
|
5679
|
-
});
|
|
5680
|
-
|
|
5681
|
-
child.once("close", (code) => {
|
|
5682
|
-
if (settled) return;
|
|
5683
|
-
if (code === 0) {
|
|
5684
|
-
settled = true;
|
|
5685
|
-
resolve();
|
|
5686
|
-
return;
|
|
5687
|
-
}
|
|
5688
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
5689
|
-
const hint = stderr.includes("not found") || stderr.includes("xelatex") || stderr.includes("pdflatex")
|
|
5690
|
-
? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
|
|
5691
|
-
: "";
|
|
5692
|
-
fail(new Error(`pandoc PDF export failed with exit code ${code}${stderr ? `: ${stderr}` : ""}${hint}`));
|
|
5693
|
-
});
|
|
5694
|
-
|
|
5695
|
-
child.stdin.end(pandocSource);
|
|
5669
|
+
const pandocResult = await runStudioSubprocess(pandocCommand, args, {
|
|
5670
|
+
cwd: pandocWorkingDir,
|
|
5671
|
+
input: pandocSource,
|
|
5672
|
+
timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
|
|
5673
|
+
label: "pandoc PDF export",
|
|
5674
|
+
notFoundMessage: pandocCommand === "pandoc"
|
|
5675
|
+
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
5676
|
+
: `${pandocCommand} was not found. Check PANDOC_PATH.`,
|
|
5696
5677
|
});
|
|
5678
|
+
if (pandocResult.code !== 0) {
|
|
5679
|
+
const stderr = pandocResult.stderr;
|
|
5680
|
+
const hint = stderr.includes("not found") || stderr.includes("xelatex") || stderr.includes("pdflatex")
|
|
5681
|
+
? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
|
|
5682
|
+
: "";
|
|
5683
|
+
throw new Error(`pandoc PDF export failed with exit code ${pandocResult.code}${stderr ? `: ${stderr}` : ""}${hint}`);
|
|
5684
|
+
}
|
|
5697
5685
|
|
|
5698
5686
|
return { pdf: await readFile(outputPath), warning };
|
|
5699
5687
|
} finally {
|
|
@@ -5795,6 +5783,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
5795
5783
|
"-f", inputFormat,
|
|
5796
5784
|
"-o", outputPath,
|
|
5797
5785
|
`--pdf-engine=${pdfEngine}`,
|
|
5786
|
+
...buildStudioPandocPdfEngineOptArgs(pdfEngine),
|
|
5798
5787
|
...buildStudioPdfPandocVariableArgs(pdfOptions, !isLatex),
|
|
5799
5788
|
"-V", "urlcolor=blue",
|
|
5800
5789
|
"-V", "linkcolor=blue",
|
|
@@ -5805,49 +5794,22 @@ async function renderStudioPdfWithPandoc(
|
|
|
5805
5794
|
const pandocSource = isLatex ? markdownForPdf : normalizeStudioMarkdownFencedBlocks(markdownForPdf);
|
|
5806
5795
|
|
|
5807
5796
|
try {
|
|
5808
|
-
await
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
reject(error);
|
|
5817
|
-
};
|
|
5818
|
-
|
|
5819
|
-
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
5820
|
-
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
5821
|
-
});
|
|
5822
|
-
|
|
5823
|
-
child.once("error", (error) => {
|
|
5824
|
-
const errno = error as NodeJS.ErrnoException;
|
|
5825
|
-
if (errno.code === "ENOENT") {
|
|
5826
|
-
const commandHint = pandocCommand === "pandoc"
|
|
5827
|
-
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
5828
|
-
: `${pandocCommand} was not found. Check PANDOC_PATH.`;
|
|
5829
|
-
fail(new Error(commandHint));
|
|
5830
|
-
return;
|
|
5831
|
-
}
|
|
5832
|
-
fail(error);
|
|
5833
|
-
});
|
|
5834
|
-
|
|
5835
|
-
child.once("close", (code) => {
|
|
5836
|
-
if (settled) return;
|
|
5837
|
-
if (code === 0) {
|
|
5838
|
-
settled = true;
|
|
5839
|
-
resolve();
|
|
5840
|
-
return;
|
|
5841
|
-
}
|
|
5842
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
5843
|
-
const hint = stderr.includes("not found") || stderr.includes("xelatex") || stderr.includes("pdflatex")
|
|
5844
|
-
? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
|
|
5845
|
-
: "";
|
|
5846
|
-
fail(new Error(`pandoc PDF export failed with exit code ${code}${stderr ? `: ${stderr}` : ""}${hint}`));
|
|
5847
|
-
});
|
|
5848
|
-
|
|
5849
|
-
child.stdin.end(pandocSource);
|
|
5797
|
+
const pandocResult = await runStudioSubprocess(pandocCommand, args, {
|
|
5798
|
+
cwd: pandocWorkingDir,
|
|
5799
|
+
input: pandocSource,
|
|
5800
|
+
timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
|
|
5801
|
+
label: "pandoc PDF export",
|
|
5802
|
+
notFoundMessage: pandocCommand === "pandoc"
|
|
5803
|
+
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
5804
|
+
: `${pandocCommand} was not found. Check PANDOC_PATH.`,
|
|
5850
5805
|
});
|
|
5806
|
+
if (pandocResult.code !== 0) {
|
|
5807
|
+
const stderr = pandocResult.stderr;
|
|
5808
|
+
const hint = stderr.includes("not found") || stderr.includes("xelatex") || stderr.includes("pdflatex")
|
|
5809
|
+
? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
|
|
5810
|
+
: "";
|
|
5811
|
+
throw new Error(`pandoc PDF export failed with exit code ${pandocResult.code}${stderr ? `: ${stderr}` : ""}${hint}`);
|
|
5812
|
+
}
|
|
5851
5813
|
|
|
5852
5814
|
return { pdf: await readFile(outputPath), warning: mermaidPrepared.warning };
|
|
5853
5815
|
} finally {
|
|
@@ -8006,6 +7968,7 @@ ${cssVarsBlock}
|
|
|
8006
7968
|
<label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
|
|
8007
7969
|
<button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
|
|
8008
7970
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
7971
|
+
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls.">⊙ Zen</button>
|
|
8009
7972
|
</div>
|
|
8010
7973
|
</header>
|
|
8011
7974
|
|
|
@@ -11864,6 +11827,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
11864
11827
|
const outputPath = buildStudioResponseExportOutputPath(ctx.cwd, "pdf");
|
|
11865
11828
|
|
|
11866
11829
|
try {
|
|
11830
|
+
ctx.ui.notify("Exporting last response Studio PDF…", "info");
|
|
11867
11831
|
const { pdf, warning } = await renderStudioPdfWithPandoc(
|
|
11868
11832
|
response.markdown,
|
|
11869
11833
|
isLatex,
|
|
@@ -11921,6 +11885,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
11921
11885
|
const outputPath = buildStudioPdfOutputPath(file.resolvedPath);
|
|
11922
11886
|
|
|
11923
11887
|
try {
|
|
11888
|
+
ctx.ui.notify(`Exporting Studio PDF: ${outputPath}`, "info");
|
|
11924
11889
|
const { pdf, warning } = await renderStudioPdfWithPandoc(
|
|
11925
11890
|
file.text,
|
|
11926
11891
|
isLatex,
|