pi-studio 0.5.7 → 0.5.9
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/index.ts +266 -31
- 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.9] — 2026-03-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Studio preview now uses Pandoc's `markdown` reader (matching `pi-markdown-preview`) instead of `gfm` for math-aware rendering, preventing currency amounts like `$135.00` from being misparsed as inline math in preview/PDF.
|
|
11
|
+
- Studio PDF export now preprocesses fenced Mermaid blocks via Mermaid CLI (`mmdc`) before Pandoc export, so Mermaid diagrams render as diagrams in exported PDFs instead of falling back to raw code fences.
|
|
12
|
+
|
|
13
|
+
## [0.5.8] — 2026-03-12
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Studio browser tabs now auto-reconnect after unexpected websocket disconnects (for example transient local connection loss or sleep/wake), while intentional invalidation/shutdown still requires a fresh `/studio`.
|
|
17
|
+
- Same-tab reconnect now preserves the currently selected response-history item instead of jumping back to the latest response on every `hello_ack` resync.
|
|
18
|
+
|
|
7
19
|
## [0.5.7] — 2026-03-12
|
|
8
20
|
|
|
9
21
|
### Changed
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
7
7
|
import { homedir, tmpdir } from "node:os";
|
|
8
8
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
9
|
-
import { URL } from "node:url";
|
|
9
|
+
import { URL, pathToFileURL } from "node:url";
|
|
10
10
|
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
|
11
11
|
|
|
12
12
|
type Lens = "writing" | "code";
|
|
@@ -1067,9 +1067,156 @@ function normalizeObsidianImages(markdown: string): string {
|
|
|
1067
1067
|
.replace(/!\[\[([^\]]+)\]\]/g, (_m, path) => ``);
|
|
1068
1068
|
}
|
|
1069
1069
|
|
|
1070
|
+
class MermaidCliMissingError extends Error {}
|
|
1071
|
+
|
|
1072
|
+
interface StudioMermaidPdfPreprocessResult {
|
|
1073
|
+
markdown: string;
|
|
1074
|
+
found: number;
|
|
1075
|
+
replaced: number;
|
|
1076
|
+
failed: number;
|
|
1077
|
+
missingCli: boolean;
|
|
1078
|
+
warning?: string;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function getStudioMermaidPdfTheme(): "default" | "forest" | "dark" | "neutral" {
|
|
1082
|
+
const requested = process.env.MERMAID_PDF_THEME?.trim().toLowerCase();
|
|
1083
|
+
if (requested === "default" || requested === "forest" || requested === "dark" || requested === "neutral") {
|
|
1084
|
+
return requested;
|
|
1085
|
+
}
|
|
1086
|
+
return "default";
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function renderStudioMermaidDiagramForPdf(source: string, workDir: string, blockNumber: number): Promise<string> {
|
|
1090
|
+
const mermaidCommand = process.env.MERMAID_CLI_PATH?.trim() || "mmdc";
|
|
1091
|
+
const mermaidTheme = getStudioMermaidPdfTheme();
|
|
1092
|
+
const inputPath = join(workDir, `mermaid-diagram-${blockNumber}.mmd`);
|
|
1093
|
+
const outputPath = join(workDir, `mermaid-diagram-${blockNumber}.pdf`);
|
|
1094
|
+
|
|
1095
|
+
await writeFile(inputPath, source, "utf-8");
|
|
1096
|
+
await new Promise<void>((resolve, reject) => {
|
|
1097
|
+
const args = ["-i", inputPath, "-o", outputPath, "-t", mermaidTheme, "-f"];
|
|
1098
|
+
const child = spawn(mermaidCommand, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1099
|
+
const stderrChunks: Buffer[] = [];
|
|
1100
|
+
let settled = false;
|
|
1101
|
+
|
|
1102
|
+
const fail = (error: Error) => {
|
|
1103
|
+
if (settled) return;
|
|
1104
|
+
settled = true;
|
|
1105
|
+
reject(error);
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
1109
|
+
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
child.once("error", (error) => {
|
|
1113
|
+
const errno = error as NodeJS.ErrnoException;
|
|
1114
|
+
if (errno.code === "ENOENT") {
|
|
1115
|
+
fail(
|
|
1116
|
+
new MermaidCliMissingError(
|
|
1117
|
+
"Mermaid CLI (mmdc) not found. Install with `npm install -g @mermaid-js/mermaid-cli` or set MERMAID_CLI_PATH.",
|
|
1118
|
+
),
|
|
1119
|
+
);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
fail(error);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
child.once("close", (code) => {
|
|
1126
|
+
if (settled) return;
|
|
1127
|
+
settled = true;
|
|
1128
|
+
if (code === 0) {
|
|
1129
|
+
resolve();
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
1133
|
+
reject(new Error(`Mermaid CLI failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`));
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
return outputPath;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function preprocessStudioMermaidForPdf(markdown: string, workDir: string): Promise<StudioMermaidPdfPreprocessResult> {
|
|
1141
|
+
const mermaidRegex = /```mermaid[^\n]*\n([\s\S]*?)```/gi;
|
|
1142
|
+
const matches: Array<{ start: number; end: number; raw: string; source: string; number: number }> = [];
|
|
1143
|
+
let match: RegExpExecArray | null;
|
|
1144
|
+
let blockNumber = 1;
|
|
1145
|
+
|
|
1146
|
+
while ((match = mermaidRegex.exec(markdown)) !== null) {
|
|
1147
|
+
const raw = match[0]!;
|
|
1148
|
+
const source = (match[1] ?? "").trimEnd();
|
|
1149
|
+
matches.push({
|
|
1150
|
+
start: match.index,
|
|
1151
|
+
end: match.index + raw.length,
|
|
1152
|
+
raw,
|
|
1153
|
+
source,
|
|
1154
|
+
number: blockNumber++,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (matches.length === 0) {
|
|
1159
|
+
return {
|
|
1160
|
+
markdown,
|
|
1161
|
+
found: 0,
|
|
1162
|
+
replaced: 0,
|
|
1163
|
+
failed: 0,
|
|
1164
|
+
missingCli: false,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
let transformed = "";
|
|
1169
|
+
let cursor = 0;
|
|
1170
|
+
let replaced = 0;
|
|
1171
|
+
let failed = 0;
|
|
1172
|
+
let missingCli = false;
|
|
1173
|
+
|
|
1174
|
+
for (const block of matches) {
|
|
1175
|
+
transformed += markdown.slice(cursor, block.start);
|
|
1176
|
+
if (missingCli) {
|
|
1177
|
+
failed++;
|
|
1178
|
+
transformed += block.raw;
|
|
1179
|
+
cursor = block.end;
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
try {
|
|
1184
|
+
const renderedPath = await renderStudioMermaidDiagramForPdf(block.source, workDir, block.number);
|
|
1185
|
+
const imageRef = pathToFileURL(renderedPath).href;
|
|
1186
|
+
transformed += `\n\n`;
|
|
1187
|
+
replaced++;
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
if (error instanceof MermaidCliMissingError) {
|
|
1190
|
+
missingCli = true;
|
|
1191
|
+
}
|
|
1192
|
+
failed++;
|
|
1193
|
+
transformed += block.raw;
|
|
1194
|
+
}
|
|
1195
|
+
cursor = block.end;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
transformed += markdown.slice(cursor);
|
|
1199
|
+
|
|
1200
|
+
let warning: string | undefined;
|
|
1201
|
+
if (missingCli) {
|
|
1202
|
+
warning = "Mermaid CLI (mmdc) not found; Mermaid blocks are kept as code in PDF. Install @mermaid-js/mermaid-cli or set MERMAID_CLI_PATH.";
|
|
1203
|
+
} else if (failed > 0) {
|
|
1204
|
+
warning = `Failed to render ${failed} Mermaid block${failed === 1 ? "" : "s"} for PDF. Unrendered blocks are kept as code.`;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return {
|
|
1208
|
+
markdown: transformed,
|
|
1209
|
+
found: matches.length,
|
|
1210
|
+
replaced,
|
|
1211
|
+
failed,
|
|
1212
|
+
missingCli,
|
|
1213
|
+
warning,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1070
1217
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string): Promise<string> {
|
|
1071
1218
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
1072
|
-
const inputFormat = isLatex ? "latex" : "
|
|
1219
|
+
const inputFormat = isLatex ? "latex" : "markdown+tex_math_dollars+autolink_bare_uris-raw_html";
|
|
1073
1220
|
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none"];
|
|
1074
1221
|
if (resourcePath) {
|
|
1075
1222
|
args.push(`--resource-path=${resourcePath}`);
|
|
@@ -1132,12 +1279,16 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
1132
1279
|
});
|
|
1133
1280
|
}
|
|
1134
1281
|
|
|
1135
|
-
async function renderStudioPdfWithPandoc(
|
|
1282
|
+
async function renderStudioPdfWithPandoc(
|
|
1283
|
+
markdown: string,
|
|
1284
|
+
isLatex?: boolean,
|
|
1285
|
+
resourcePath?: string,
|
|
1286
|
+
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
1136
1287
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
1137
1288
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
1138
1289
|
const inputFormat = isLatex
|
|
1139
1290
|
? "latex"
|
|
1140
|
-
: "
|
|
1291
|
+
: "markdown+tex_math_dollars+autolink_bare_uris+superscript+subscript-raw_html";
|
|
1141
1292
|
const normalizedMarkdown = isLatex ? markdown : normalizeObsidianImages(normalizeMathDelimiters(markdown));
|
|
1142
1293
|
|
|
1143
1294
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
@@ -1147,6 +1298,11 @@ async function renderStudioPdfWithPandoc(markdown: string, isLatex?: boolean, re
|
|
|
1147
1298
|
await mkdir(tempDir, { recursive: true });
|
|
1148
1299
|
await writeFile(preamblePath, PDF_PREAMBLE, "utf-8");
|
|
1149
1300
|
|
|
1301
|
+
const mermaidPrepared: StudioMermaidPdfPreprocessResult = isLatex
|
|
1302
|
+
? { markdown: normalizedMarkdown, found: 0, replaced: 0, failed: 0, missingCli: false }
|
|
1303
|
+
: await preprocessStudioMermaidForPdf(normalizedMarkdown, tempDir);
|
|
1304
|
+
const markdownForPdf = mermaidPrepared.markdown;
|
|
1305
|
+
|
|
1150
1306
|
const args = [
|
|
1151
1307
|
"-f", inputFormat,
|
|
1152
1308
|
"-o", outputPath,
|
|
@@ -1202,10 +1358,10 @@ async function renderStudioPdfWithPandoc(markdown: string, isLatex?: boolean, re
|
|
|
1202
1358
|
fail(new Error(`pandoc PDF export failed with exit code ${code}${stderr ? `: ${stderr}` : ""}${hint}`));
|
|
1203
1359
|
});
|
|
1204
1360
|
|
|
1205
|
-
child.stdin.end(
|
|
1361
|
+
child.stdin.end(markdownForPdf);
|
|
1206
1362
|
});
|
|
1207
1363
|
|
|
1208
|
-
return await readFile(outputPath);
|
|
1364
|
+
return { pdf: await readFile(outputPath), warning: mermaidPrepared.warning };
|
|
1209
1365
|
} finally {
|
|
1210
1366
|
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
1211
1367
|
}
|
|
@@ -3301,6 +3457,8 @@ ${cssVarsBlock}
|
|
|
3301
3457
|
let wsState = "Connecting";
|
|
3302
3458
|
let statusMessage = "Connecting · Studio script starting…";
|
|
3303
3459
|
let statusLevel = "";
|
|
3460
|
+
let reconnectTimer = null;
|
|
3461
|
+
let reconnectAttempt = 0;
|
|
3304
3462
|
let pendingRequestId = null;
|
|
3305
3463
|
let pendingKind = null;
|
|
3306
3464
|
let stickyStudioKind = null;
|
|
@@ -4570,6 +4728,7 @@ ${cssVarsBlock}
|
|
|
4570
4728
|
throw new Error(message);
|
|
4571
4729
|
}
|
|
4572
4730
|
|
|
4731
|
+
const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
|
|
4573
4732
|
const blob = await response.blob();
|
|
4574
4733
|
const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
|
|
4575
4734
|
let downloadName = headerFilename || filenameHint || "studio-preview.pdf";
|
|
@@ -4589,7 +4748,11 @@ ${cssVarsBlock}
|
|
|
4589
4748
|
URL.revokeObjectURL(blobUrl);
|
|
4590
4749
|
}, 1800);
|
|
4591
4750
|
|
|
4592
|
-
|
|
4751
|
+
if (exportWarning) {
|
|
4752
|
+
setStatus("Exported PDF with warning: " + exportWarning, "warning");
|
|
4753
|
+
} else {
|
|
4754
|
+
setStatus("Exported PDF: " + downloadName, "success");
|
|
4755
|
+
}
|
|
4593
4756
|
} catch (error) {
|
|
4594
4757
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
4595
4758
|
setStatus("PDF export failed: " + detail, "error");
|
|
@@ -5824,8 +5987,8 @@ ${cssVarsBlock}
|
|
|
5824
5987
|
let appliedHistory = false;
|
|
5825
5988
|
if (Array.isArray(message.responseHistory)) {
|
|
5826
5989
|
appliedHistory = setResponseHistory(message.responseHistory, {
|
|
5827
|
-
autoSelectLatest:
|
|
5828
|
-
preserveSelection:
|
|
5990
|
+
autoSelectLatest: !initialDocumentApplied,
|
|
5991
|
+
preserveSelection: initialDocumentApplied,
|
|
5829
5992
|
silent: true,
|
|
5830
5993
|
});
|
|
5831
5994
|
}
|
|
@@ -6184,7 +6347,42 @@ ${cssVarsBlock}
|
|
|
6184
6347
|
}
|
|
6185
6348
|
}
|
|
6186
6349
|
|
|
6350
|
+
function clearScheduledReconnect() {
|
|
6351
|
+
if (reconnectTimer !== null) {
|
|
6352
|
+
window.clearTimeout(reconnectTimer);
|
|
6353
|
+
reconnectTimer = null;
|
|
6354
|
+
}
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6357
|
+
function formatReconnectDelay(delayMs) {
|
|
6358
|
+
const delay = Math.max(0, Number(delayMs) || 0);
|
|
6359
|
+
if (delay < 1000) return delay + "ms";
|
|
6360
|
+
const seconds = delay / 1000;
|
|
6361
|
+
return (Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1)) + "s";
|
|
6362
|
+
}
|
|
6363
|
+
|
|
6364
|
+
function scheduleReconnect(reasonMessage) {
|
|
6365
|
+
if (reconnectTimer !== null) return;
|
|
6366
|
+
|
|
6367
|
+
reconnectAttempt += 1;
|
|
6368
|
+
const delayMs = Math.min(8000, 600 * Math.pow(2, Math.max(0, reconnectAttempt - 1)));
|
|
6369
|
+
setBusy(true);
|
|
6370
|
+
setWsState("Connecting");
|
|
6371
|
+
setStatus((reasonMessage || "Connection lost.") + " Reconnecting in " + formatReconnectDelay(delayMs) + "…", "warning");
|
|
6372
|
+
|
|
6373
|
+
reconnectTimer = window.setTimeout(() => {
|
|
6374
|
+
reconnectTimer = null;
|
|
6375
|
+
connect();
|
|
6376
|
+
}, delayMs);
|
|
6377
|
+
}
|
|
6378
|
+
|
|
6187
6379
|
function connect() {
|
|
6380
|
+
clearScheduledReconnect();
|
|
6381
|
+
|
|
6382
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
6383
|
+
return;
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6188
6386
|
const token = getToken();
|
|
6189
6387
|
if (!token) {
|
|
6190
6388
|
setWsState("Disconnected");
|
|
@@ -6195,26 +6393,61 @@ ${cssVarsBlock}
|
|
|
6195
6393
|
|
|
6196
6394
|
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
6197
6395
|
const wsUrl = wsProtocol + "://" + window.location.host + "/ws?token=" + encodeURIComponent(token) + (DEBUG_ENABLED ? "&debug=1" : "");
|
|
6396
|
+
const wasReconnect = reconnectAttempt > 0;
|
|
6397
|
+
let disconnectHandled = false;
|
|
6198
6398
|
|
|
6199
6399
|
setWsState("Connecting");
|
|
6200
|
-
setStatus("Connecting to Studio server…");
|
|
6201
|
-
|
|
6400
|
+
setStatus(wasReconnect ? "Reconnecting to Studio server…" : "Connecting to Studio server…");
|
|
6401
|
+
const socket = new WebSocket(wsUrl);
|
|
6402
|
+
ws = socket;
|
|
6202
6403
|
|
|
6203
6404
|
const connectWatchdog = window.setTimeout(() => {
|
|
6204
|
-
if (ws &&
|
|
6405
|
+
if (ws === socket && socket.readyState === WebSocket.CONNECTING) {
|
|
6205
6406
|
setWsState("Connecting");
|
|
6206
|
-
setStatus("Still connecting…", "warning");
|
|
6407
|
+
setStatus(wasReconnect ? "Still reconnecting…" : "Still connecting…", "warning");
|
|
6207
6408
|
}
|
|
6208
6409
|
}, 3000);
|
|
6209
6410
|
|
|
6210
|
-
|
|
6411
|
+
const handleDisconnect = (kind, code) => {
|
|
6412
|
+
if (disconnectHandled) return;
|
|
6413
|
+
disconnectHandled = true;
|
|
6414
|
+
window.clearTimeout(connectWatchdog);
|
|
6415
|
+
if (ws === socket) {
|
|
6416
|
+
ws = null;
|
|
6417
|
+
}
|
|
6418
|
+
setBusy(true);
|
|
6419
|
+
|
|
6420
|
+
if (kind === "invalidated") {
|
|
6421
|
+
clearScheduledReconnect();
|
|
6422
|
+
reconnectAttempt = 0;
|
|
6423
|
+
setWsState("Disconnected");
|
|
6424
|
+
setStatus("This tab was invalidated by a newer /studio session.", "warning");
|
|
6425
|
+
return;
|
|
6426
|
+
}
|
|
6427
|
+
|
|
6428
|
+
if (kind === "shutdown") {
|
|
6429
|
+
clearScheduledReconnect();
|
|
6430
|
+
reconnectAttempt = 0;
|
|
6431
|
+
setWsState("Disconnected");
|
|
6432
|
+
setStatus("Studio server shut down. Re-run /studio.", "warning");
|
|
6433
|
+
return;
|
|
6434
|
+
}
|
|
6435
|
+
|
|
6436
|
+
const detail = typeof code === "number" && code > 0
|
|
6437
|
+
? "Disconnected (code " + code + ")."
|
|
6438
|
+
: (kind === "error" ? "WebSocket error." : "Connection lost.");
|
|
6439
|
+
scheduleReconnect(detail);
|
|
6440
|
+
};
|
|
6441
|
+
|
|
6442
|
+
socket.addEventListener("open", () => {
|
|
6211
6443
|
window.clearTimeout(connectWatchdog);
|
|
6212
6444
|
setWsState("Ready");
|
|
6213
|
-
setStatus("Connected. Syncing…");
|
|
6445
|
+
setStatus(wasReconnect ? "Reconnected. Syncing…" : "Connected. Syncing…");
|
|
6214
6446
|
sendMessage({ type: "hello" });
|
|
6447
|
+
reconnectAttempt = 0;
|
|
6215
6448
|
});
|
|
6216
6449
|
|
|
6217
|
-
|
|
6450
|
+
socket.addEventListener("message", (event) => {
|
|
6218
6451
|
try {
|
|
6219
6452
|
const message = JSON.parse(event.data);
|
|
6220
6453
|
handleServerMessage(message);
|
|
@@ -6224,22 +6457,21 @@ ${cssVarsBlock}
|
|
|
6224
6457
|
}
|
|
6225
6458
|
});
|
|
6226
6459
|
|
|
6227
|
-
|
|
6228
|
-
window.clearTimeout(connectWatchdog);
|
|
6229
|
-
setBusy(true);
|
|
6230
|
-
setWsState("Disconnected");
|
|
6460
|
+
socket.addEventListener("close", (event) => {
|
|
6231
6461
|
if (event && event.code === 4001) {
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
const code = event && typeof event.code === "number" ? event.code : 0;
|
|
6235
|
-
setStatus("Disconnected (code " + code + "). Re-run /studio.", "error");
|
|
6462
|
+
handleDisconnect("invalidated", 4001);
|
|
6463
|
+
return;
|
|
6236
6464
|
}
|
|
6465
|
+
if (event && event.code === 1001) {
|
|
6466
|
+
handleDisconnect("shutdown", 1001);
|
|
6467
|
+
return;
|
|
6468
|
+
}
|
|
6469
|
+
const code = event && typeof event.code === "number" ? event.code : 0;
|
|
6470
|
+
handleDisconnect("close", code);
|
|
6237
6471
|
});
|
|
6238
6472
|
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
setWsState("Disconnected");
|
|
6242
|
-
setStatus("WebSocket error. Check /studio --status and reopen.", "error");
|
|
6473
|
+
socket.addEventListener("error", () => {
|
|
6474
|
+
handleDisconnect("error");
|
|
6243
6475
|
});
|
|
6244
6476
|
}
|
|
6245
6477
|
|
|
@@ -7805,20 +8037,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
7805
8037
|
const filename = sanitizePdfFilename(requestedFilename || (isLatex ? "studio-latex-preview.pdf" : "studio-preview.pdf"));
|
|
7806
8038
|
|
|
7807
8039
|
try {
|
|
7808
|
-
const pdf = await renderStudioPdfWithPandoc(markdown, isLatex, resourcePath);
|
|
8040
|
+
const { pdf, warning } = await renderStudioPdfWithPandoc(markdown, isLatex, resourcePath);
|
|
7809
8041
|
const safeAsciiName = filename
|
|
7810
8042
|
.replace(/[\x00-\x1f\x7f]/g, "")
|
|
7811
8043
|
.replace(/[;"\\]/g, "_")
|
|
7812
8044
|
.replace(/\s+/g, " ")
|
|
7813
8045
|
.trim() || "studio-preview.pdf";
|
|
7814
8046
|
|
|
7815
|
-
|
|
8047
|
+
const headers: Record<string, string> = {
|
|
7816
8048
|
"Content-Type": "application/pdf",
|
|
7817
8049
|
"Cache-Control": "no-store",
|
|
7818
8050
|
"X-Content-Type-Options": "nosniff",
|
|
7819
8051
|
"Content-Disposition": `attachment; filename="${safeAsciiName}"; filename*=UTF-8''${encodeURIComponent(filename)}`,
|
|
7820
8052
|
"Content-Length": String(pdf.length),
|
|
7821
|
-
}
|
|
8053
|
+
};
|
|
8054
|
+
if (warning) headers["X-Pi-Studio-Export-Warning"] = warning;
|
|
8055
|
+
|
|
8056
|
+
res.writeHead(200, headers);
|
|
7822
8057
|
res.end(pdf);
|
|
7823
8058
|
} catch (error) {
|
|
7824
8059
|
const message = error instanceof Error ? error.message : String(error);
|