pi-studio 0.9.16 → 0.9.18
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 +20 -0
- package/README.md +5 -5
- package/client/studio-client.js +1000 -112
- package/client/studio.css +227 -11
- package/index.ts +389 -47
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -162,6 +162,7 @@ interface PreparedStudioPdfExport {
|
|
|
162
162
|
createdAt: number;
|
|
163
163
|
filePath?: string;
|
|
164
164
|
tempDirPath?: string;
|
|
165
|
+
persistent?: boolean;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
interface PreparedStudioHtmlExport {
|
|
@@ -171,6 +172,7 @@ interface PreparedStudioHtmlExport {
|
|
|
171
172
|
createdAt: number;
|
|
172
173
|
filePath?: string;
|
|
173
174
|
tempDirPath?: string;
|
|
175
|
+
persistent?: boolean;
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
interface StudioHtmlAnnotationPlaceholder {
|
|
@@ -320,6 +322,13 @@ interface CompletionSuggestionRequestMessage {
|
|
|
320
322
|
language?: string;
|
|
321
323
|
label?: string;
|
|
322
324
|
path?: string;
|
|
325
|
+
contextMode?: "cursor" | "session";
|
|
326
|
+
contextText?: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface CompletionSuggestionCancelRequestMessage {
|
|
330
|
+
type: "completion_suggestion_cancel_request";
|
|
331
|
+
requestId: string;
|
|
323
332
|
}
|
|
324
333
|
|
|
325
334
|
interface QuizGenerateRequestMessage {
|
|
@@ -463,6 +472,7 @@ type IncomingStudioMessage =
|
|
|
463
472
|
| AnnotationRequestMessage
|
|
464
473
|
| SendRunRequestMessage
|
|
465
474
|
| CompletionSuggestionRequestMessage
|
|
475
|
+
| CompletionSuggestionCancelRequestMessage
|
|
466
476
|
| QuizGenerateRequestMessage
|
|
467
477
|
| QuizAnswerRequestMessage
|
|
468
478
|
| QuizDiscussRequestMessage
|
|
@@ -485,6 +495,7 @@ type IncomingStudioMessage =
|
|
|
485
495
|
const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
486
496
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
487
497
|
const STUDIO_COMPLETION_MAX_TEXT_CHARS = 250_000;
|
|
498
|
+
const STUDIO_COMPLETION_MAX_CONTEXT_CHARS = 12_000;
|
|
488
499
|
const STUDIO_COMPLETION_PREFIX_CHARS = 12_000;
|
|
489
500
|
const STUDIO_COMPLETION_SUFFIX_CHARS = 6_000;
|
|
490
501
|
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
@@ -2248,6 +2259,8 @@ function inferStudioPdfLanguageFromPath(pathInput: string): string | undefined {
|
|
|
2248
2259
|
".yml": "yaml",
|
|
2249
2260
|
".toml": "toml",
|
|
2250
2261
|
".lua": "lua",
|
|
2262
|
+
".csv": "csv",
|
|
2263
|
+
".tsv": "tsv",
|
|
2251
2264
|
".txt": "text",
|
|
2252
2265
|
".rst": "text",
|
|
2253
2266
|
".adoc": "text",
|
|
@@ -2290,6 +2303,33 @@ function buildStudioResponseExportOutputPath(cwd: string, extension: "pdf" | "ht
|
|
|
2290
2303
|
return join(cwd || process.cwd(), `studio-response-${formatStudioExportTimestamp()}.studio.${extension}`);
|
|
2291
2304
|
}
|
|
2292
2305
|
|
|
2306
|
+
function buildStudioPreviewExportPath(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string, filename: string): string | null {
|
|
2307
|
+
const cleanFilename = String(filename || "").trim();
|
|
2308
|
+
if (!cleanFilename) return null;
|
|
2309
|
+
const source = typeof sourcePath === "string" ? sourcePath.trim() : "";
|
|
2310
|
+
if (source) {
|
|
2311
|
+
const expanded = recoverLikelyDroppedLeadingSlashPath(expandHome(source));
|
|
2312
|
+
return join(dirname(isAbsolute(expanded) ? expanded : resolve(fallbackCwd, expanded)), cleanFilename);
|
|
2313
|
+
}
|
|
2314
|
+
const resource = normalizeStudioResourceDirectoryInput(typeof resourceDir === "string" ? resourceDir : "");
|
|
2315
|
+
if (resource) {
|
|
2316
|
+
const expanded = recoverLikelyDroppedLeadingSlashPath(expandHome(resource));
|
|
2317
|
+
return join(isAbsolute(expanded) ? expanded : resolve(fallbackCwd, expanded), cleanFilename);
|
|
2318
|
+
}
|
|
2319
|
+
return join(fallbackCwd || process.cwd(), cleanFilename);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
function writeStudioPreviewExportFile(path: string | null, data: Buffer): { filePath: string | null; error: string | null } {
|
|
2323
|
+
if (!path) return { filePath: null, error: "No export path was resolved." };
|
|
2324
|
+
try {
|
|
2325
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2326
|
+
writeFileSync(path, data);
|
|
2327
|
+
return { filePath: path, error: null };
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
return { filePath: null, error: error instanceof Error ? error.message : String(error) };
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2293
2333
|
function writeStudioFile(pathArg: string, cwd: string, content: string):
|
|
2294
2334
|
| { ok: true; label: string; resolvedPath: string }
|
|
2295
2335
|
| { ok: false; message: string } {
|
|
@@ -2457,6 +2497,7 @@ const STUDIO_LOCAL_LINK_TEXT_EXTENSIONS = new Set([
|
|
|
2457
2497
|
".diff", ".patch",
|
|
2458
2498
|
]);
|
|
2459
2499
|
const STUDIO_LOCAL_LINK_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
2500
|
+
const STUDIO_LOCAL_LINK_OFFICE_EXTENSIONS = new Set([".docx", ".odt"]);
|
|
2460
2501
|
const STUDIO_LOCAL_LINK_TEXT_FILENAMES = new Set([
|
|
2461
2502
|
".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
|
|
2462
2503
|
".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
|
|
@@ -2468,7 +2509,7 @@ const STUDIO_FILE_BROWSER_IGNORED_DIRS = new Set([
|
|
|
2468
2509
|
"__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
2469
2510
|
]);
|
|
2470
2511
|
|
|
2471
|
-
type StudioLocalPreviewResourceKind = "pdf" | "text" | "image" | "other";
|
|
2512
|
+
type StudioLocalPreviewResourceKind = "pdf" | "text" | "image" | "office" | "other";
|
|
2472
2513
|
|
|
2473
2514
|
interface StudioLocalPreviewResource {
|
|
2474
2515
|
filePath: string;
|
|
@@ -2564,6 +2605,7 @@ function getStudioLocalPreviewResourceKind(extension: string, filePathOrName?: s
|
|
|
2564
2605
|
if (ext === ".pdf") return "pdf";
|
|
2565
2606
|
if (STUDIO_LOCAL_LINK_TEXT_EXTENSIONS.has(ext) || STUDIO_LOCAL_LINK_TEXT_FILENAMES.has(name)) return "text";
|
|
2566
2607
|
if (STUDIO_LOCAL_LINK_IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
2608
|
+
if (STUDIO_LOCAL_LINK_OFFICE_EXTENSIONS.has(ext)) return "office";
|
|
2567
2609
|
return "other";
|
|
2568
2610
|
}
|
|
2569
2611
|
|
|
@@ -4295,6 +4337,125 @@ function wrapStudioCodeAsMarkdown(code: string, language?: string): string {
|
|
|
4295
4337
|
return `${marker}${lang}\n${source}\n${marker}`;
|
|
4296
4338
|
}
|
|
4297
4339
|
|
|
4340
|
+
const STUDIO_DELIMITED_PREVIEW_MAX_DATA_ROWS = 200;
|
|
4341
|
+
const STUDIO_DELIMITED_PREVIEW_MAX_COLUMNS = 50;
|
|
4342
|
+
const STUDIO_DELIMITED_PREVIEW_MAX_CELL_CHARS = 500;
|
|
4343
|
+
|
|
4344
|
+
function getStudioDelimitedTextConfig(language?: string): { label: string; delimiter: string } | null {
|
|
4345
|
+
const normalized = normalizeStudioEditorLanguage(language);
|
|
4346
|
+
if (normalized === "csv") return { label: "CSV", delimiter: "," };
|
|
4347
|
+
if (normalized === "tsv") return { label: "TSV", delimiter: "\t" };
|
|
4348
|
+
return null;
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
function parseStudioDelimitedTextRows(text: string, delimiter: string, maxRows: number): { rows: string[][]; truncatedRows: boolean } {
|
|
4352
|
+
const source = String(text ?? "").replace(/^\uFEFF/, "");
|
|
4353
|
+
const limit = Math.max(1, Math.floor(maxRows));
|
|
4354
|
+
const rows: string[][] = [];
|
|
4355
|
+
let row: string[] = [];
|
|
4356
|
+
let cell = "";
|
|
4357
|
+
let inQuotes = false;
|
|
4358
|
+
let truncatedRows = false;
|
|
4359
|
+
|
|
4360
|
+
const pushCell = () => {
|
|
4361
|
+
row.push(cell);
|
|
4362
|
+
cell = "";
|
|
4363
|
+
};
|
|
4364
|
+
const pushRow = (index: number): boolean => {
|
|
4365
|
+
pushCell();
|
|
4366
|
+
rows.push(row);
|
|
4367
|
+
row = [];
|
|
4368
|
+
if (rows.length >= limit) {
|
|
4369
|
+
truncatedRows = index < source.length - 1;
|
|
4370
|
+
return true;
|
|
4371
|
+
}
|
|
4372
|
+
return false;
|
|
4373
|
+
};
|
|
4374
|
+
|
|
4375
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
4376
|
+
if (rows.length >= limit) {
|
|
4377
|
+
truncatedRows = true;
|
|
4378
|
+
break;
|
|
4379
|
+
}
|
|
4380
|
+
const ch = source[i];
|
|
4381
|
+
if (inQuotes) {
|
|
4382
|
+
if (ch === '"') {
|
|
4383
|
+
if (source[i + 1] === '"') {
|
|
4384
|
+
cell += '"';
|
|
4385
|
+
i += 1;
|
|
4386
|
+
} else {
|
|
4387
|
+
inQuotes = false;
|
|
4388
|
+
}
|
|
4389
|
+
} else {
|
|
4390
|
+
cell += ch;
|
|
4391
|
+
}
|
|
4392
|
+
continue;
|
|
4393
|
+
}
|
|
4394
|
+
if (ch === '"' && cell === "") {
|
|
4395
|
+
inQuotes = true;
|
|
4396
|
+
continue;
|
|
4397
|
+
}
|
|
4398
|
+
if (ch === delimiter) {
|
|
4399
|
+
pushCell();
|
|
4400
|
+
continue;
|
|
4401
|
+
}
|
|
4402
|
+
if (ch === "\n") {
|
|
4403
|
+
if (pushRow(i)) break;
|
|
4404
|
+
continue;
|
|
4405
|
+
}
|
|
4406
|
+
if (ch === "\r") {
|
|
4407
|
+
if (source[i + 1] === "\n") i += 1;
|
|
4408
|
+
if (pushRow(i)) break;
|
|
4409
|
+
continue;
|
|
4410
|
+
}
|
|
4411
|
+
cell += ch;
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
if (!truncatedRows && rows.length < limit && (cell.length > 0 || row.length > 0)) {
|
|
4415
|
+
pushCell();
|
|
4416
|
+
rows.push(row);
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
return { rows, truncatedRows };
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
function formatStudioDelimitedMarkdownCell(value: string | undefined): string {
|
|
4423
|
+
const raw = String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4424
|
+
const shortened = raw.length > STUDIO_DELIMITED_PREVIEW_MAX_CELL_CHARS
|
|
4425
|
+
? `${raw.slice(0, STUDIO_DELIMITED_PREVIEW_MAX_CELL_CHARS)}…`
|
|
4426
|
+
: raw;
|
|
4427
|
+
return shortened.replace(/\n/g, "<br>").replace(/\|/g, "\\|").trim() || " ";
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
function formatStudioDelimitedTextAsMarkdown(text: string, language?: string): string | null {
|
|
4431
|
+
const config = getStudioDelimitedTextConfig(language);
|
|
4432
|
+
if (!config) return null;
|
|
4433
|
+
const parsed = parseStudioDelimitedTextRows(text, config.delimiter, STUDIO_DELIMITED_PREVIEW_MAX_DATA_ROWS + 1);
|
|
4434
|
+
const rows = parsed.rows;
|
|
4435
|
+
if (!rows.length) return `_${config.label} file has no tabular data to preview._`;
|
|
4436
|
+
const rawColumnCount = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
4437
|
+
const columnCount = Math.min(rawColumnCount, STUDIO_DELIMITED_PREVIEW_MAX_COLUMNS);
|
|
4438
|
+
if (columnCount <= 0) return `_${config.label} file has no tabular data to preview._`;
|
|
4439
|
+
const header = rows[0] ?? [];
|
|
4440
|
+
const dataRows = rows.slice(1);
|
|
4441
|
+
const columnIndexes = Array.from({ length: columnCount }, (_value, index) => index);
|
|
4442
|
+
const lines: string[] = [`**${config.label} preview**`, ""];
|
|
4443
|
+
const notices: string[] = [];
|
|
4444
|
+
if (parsed.truncatedRows) notices.push(`showing first ${Math.max(0, dataRows.length)} data rows`);
|
|
4445
|
+
if (rawColumnCount > columnCount) notices.push(`showing first ${columnCount} of ${rawColumnCount} columns`);
|
|
4446
|
+
if (notices.length) lines.push(`_${notices.join("; ")}._`, "");
|
|
4447
|
+
lines.push(`| ${columnIndexes.map((index) => formatStudioDelimitedMarkdownCell(header[index] || `Column ${index + 1}`)).join(" | ")} |`);
|
|
4448
|
+
lines.push(`| ${columnIndexes.map(() => "---").join(" | ")} |`);
|
|
4449
|
+
if (dataRows.length) {
|
|
4450
|
+
dataRows.forEach((row) => {
|
|
4451
|
+
lines.push(`| ${columnIndexes.map((index) => formatStudioDelimitedMarkdownCell(row[index])).join(" | ")} |`);
|
|
4452
|
+
});
|
|
4453
|
+
} else {
|
|
4454
|
+
lines.push(`| ${columnIndexes.map(() => " ").join(" | ")} |`);
|
|
4455
|
+
}
|
|
4456
|
+
return lines.join("\n");
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4298
4459
|
function extractStudioFenceInfoLanguage(info: string): string | undefined {
|
|
4299
4460
|
const firstToken = String(info ?? "").trim().split(/\s+/)[0]?.replace(/^\./, "") ?? "";
|
|
4300
4461
|
return normalizeStudioEditorLanguage(firstToken || undefined);
|
|
@@ -5245,11 +5406,13 @@ function buildStudioLiteralTextPdfTexConfig(options?: StudioPdfRenderOptions): {
|
|
|
5245
5406
|
|
|
5246
5407
|
function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLanguage?: string): string {
|
|
5247
5408
|
if (isLatex) return markdown;
|
|
5248
|
-
const
|
|
5409
|
+
const delimitedMarkdown = formatStudioDelimitedTextAsMarkdown(markdown, editorLanguage);
|
|
5410
|
+
const input = delimitedMarkdown ?? markdown;
|
|
5411
|
+
const effectiveEditorLanguage = delimitedMarkdown ? "markdown" : inferStudioPdfLanguage(input, editorLanguage);
|
|
5249
5412
|
const source = effectiveEditorLanguage && effectiveEditorLanguage !== "markdown" && effectiveEditorLanguage !== "latex"
|
|
5250
|
-
&& !isStudioSingleFencedCodeBlock(
|
|
5251
|
-
? wrapStudioCodeAsMarkdown(
|
|
5252
|
-
:
|
|
5413
|
+
&& !isStudioSingleFencedCodeBlock(input)
|
|
5414
|
+
? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
|
|
5415
|
+
: input;
|
|
5253
5416
|
const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
|
|
5254
5417
|
? replaceStudioAnnotationMarkersForPdf(source)
|
|
5255
5418
|
: source;
|
|
@@ -5970,17 +6133,19 @@ async function renderStudioStandaloneHtmlWithPandoc(
|
|
|
5970
6133
|
sourcePath?: string,
|
|
5971
6134
|
options?: StudioHtmlRenderOptions,
|
|
5972
6135
|
): Promise<{ html: Buffer; warning?: string }> {
|
|
5973
|
-
const
|
|
5974
|
-
|
|
5975
|
-
|
|
6136
|
+
const delimitedMarkdown = isLatex ? null : formatStudioDelimitedTextAsMarkdown(markdown, editorLanguage);
|
|
6137
|
+
const input = delimitedMarkdown ?? markdown;
|
|
6138
|
+
const effectiveEditorLanguage = delimitedMarkdown ? "markdown" : inferStudioPdfLanguage(input, editorLanguage);
|
|
6139
|
+
if (!isLatex && isLikelyStandaloneStudioHtml(input, effectiveEditorLanguage)) {
|
|
6140
|
+
return { html: Buffer.from(String(input ?? ""), "utf-8") };
|
|
5976
6141
|
}
|
|
5977
6142
|
const source = !isLatex
|
|
5978
6143
|
&& effectiveEditorLanguage
|
|
5979
6144
|
&& effectiveEditorLanguage !== "markdown"
|
|
5980
6145
|
&& effectiveEditorLanguage !== "latex"
|
|
5981
|
-
&& !isStudioSingleFencedCodeBlock(
|
|
5982
|
-
? wrapStudioCodeAsMarkdown(
|
|
5983
|
-
:
|
|
6146
|
+
&& !isStudioSingleFencedCodeBlock(input)
|
|
6147
|
+
? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
|
|
6148
|
+
: input;
|
|
5984
6149
|
const annotationPrepared = prepareStudioAnnotationMarkersForHtml(source);
|
|
5985
6150
|
const pdfPrepared = prepareStudioPdfBlocksForHtml(annotationPrepared.markdown);
|
|
5986
6151
|
let renderedHtml = await renderStudioMarkdownWithPandoc(pdfPrepared.markdown, isLatex, resourcePath, sourcePath);
|
|
@@ -6651,7 +6816,74 @@ function respondHtmlPreviewResourceJson(req: IncomingMessage, res: ServerRespons
|
|
|
6651
6816
|
});
|
|
6652
6817
|
}
|
|
6653
6818
|
|
|
6654
|
-
function
|
|
6819
|
+
function formatStudioMarkdownAngleTarget(pathText: string): string {
|
|
6820
|
+
return `<${String(pathText || "").replace(/>/g, "%3E")}>`;
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6823
|
+
function sanitizeStudioPreviewBlockLine(value: string): string {
|
|
6824
|
+
return String(value || "").replace(/[\r\n]+/g, " ").trim();
|
|
6825
|
+
}
|
|
6826
|
+
|
|
6827
|
+
function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewResource): InitialStudioDocument {
|
|
6828
|
+
const label = basename(resource.filePath) || resource.label || "local preview";
|
|
6829
|
+
const resourcePath = resource.label || basename(resource.filePath) || resource.filePath;
|
|
6830
|
+
const title = sanitizeStudioPreviewBlockLine(label);
|
|
6831
|
+
let text = "";
|
|
6832
|
+
if (resource.kind === "pdf") {
|
|
6833
|
+
text = "```studio-pdf\n"
|
|
6834
|
+
+ `path: ${sanitizeStudioPreviewBlockLine(resourcePath)}\n`
|
|
6835
|
+
+ `title: ${title || "PDF preview"}\n`
|
|
6836
|
+
+ "height: 820\n"
|
|
6837
|
+
+ "```\n";
|
|
6838
|
+
} else if (resource.kind === "image") {
|
|
6839
|
+
text = `})\n`;
|
|
6840
|
+
} else {
|
|
6841
|
+
throw new Error("This local resource cannot be opened as a preview document.");
|
|
6842
|
+
}
|
|
6843
|
+
return {
|
|
6844
|
+
text,
|
|
6845
|
+
label: `${label} preview`,
|
|
6846
|
+
source: "blank",
|
|
6847
|
+
resourceDir: resource.resourceDir,
|
|
6848
|
+
};
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6851
|
+
function getStudioOfficePandocInputFormat(extension: string): string {
|
|
6852
|
+
const ext = String(extension || "").toLowerCase();
|
|
6853
|
+
if (ext === ".docx") return "docx";
|
|
6854
|
+
if (ext === ".odt") return "odt";
|
|
6855
|
+
return ext.replace(/^\./, "") || "docx";
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6858
|
+
async function convertStudioOfficeDocumentToMarkdown(resource: StudioLocalPreviewResource): Promise<{ text: string; label: string }> {
|
|
6859
|
+
if (resource.kind !== "office") throw new Error("This local resource is not a supported convertible document.");
|
|
6860
|
+
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
6861
|
+
const inputFormat = getStudioOfficePandocInputFormat(resource.extension);
|
|
6862
|
+
const result = await runStudioSubprocess(pandocCommand, [
|
|
6863
|
+
"-f", inputFormat,
|
|
6864
|
+
"-t", "markdown",
|
|
6865
|
+
"--wrap=none",
|
|
6866
|
+
resource.filePath,
|
|
6867
|
+
], {
|
|
6868
|
+
cwd: dirname(resource.filePath),
|
|
6869
|
+
timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
|
|
6870
|
+
stdoutMaxBytes: STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES,
|
|
6871
|
+
label: "pandoc document conversion",
|
|
6872
|
+
notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to convert DOCX/ODT documents in Studio.",
|
|
6873
|
+
});
|
|
6874
|
+
if (result.code !== 0) {
|
|
6875
|
+
throw new Error(`pandoc failed with exit code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`);
|
|
6876
|
+
}
|
|
6877
|
+
if (result.stdoutTruncated) {
|
|
6878
|
+
throw new Error("Converted document exceeded Studio's import size limit.");
|
|
6879
|
+
}
|
|
6880
|
+
const label = `converted: ${resource.label || basename(resource.filePath) || "document"}`;
|
|
6881
|
+
const note = `<!-- ${label} from ${resource.filePath}. This is a Markdown conversion; saving will not update the original ${resource.extension || "document"} file. -->`;
|
|
6882
|
+
const body = result.stdout.trim();
|
|
6883
|
+
return { text: `${note}\n\n${body}\n`, label };
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResponse, requestUrl: URL, resource: StudioLocalPreviewResource, serverState: StudioServerState): Promise<void> {
|
|
6655
6887
|
const method = (req.method ?? "GET").toUpperCase();
|
|
6656
6888
|
if (method !== "GET" && method !== "HEAD") {
|
|
6657
6889
|
res.setHeader("Allow", "GET, HEAD");
|
|
@@ -6675,32 +6907,72 @@ function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResponse,
|
|
|
6675
6907
|
return;
|
|
6676
6908
|
}
|
|
6677
6909
|
|
|
6910
|
+
if (action === "preview-url") {
|
|
6911
|
+
if (resource.kind !== "pdf" && resource.kind !== "image") {
|
|
6912
|
+
respondJson(res, 400, { ok: false, error: "This local resource cannot be opened in a Studio preview tab." });
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6915
|
+
const document = buildStudioLocalResourcePreviewDocument(resource);
|
|
6916
|
+
const docId = storeTransientStudioDocument(document);
|
|
6917
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
6918
|
+
const parsedUrl = new URL(url);
|
|
6919
|
+
respondJson(res, 200, {
|
|
6920
|
+
...basePayload,
|
|
6921
|
+
url,
|
|
6922
|
+
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
6923
|
+
});
|
|
6924
|
+
return;
|
|
6925
|
+
}
|
|
6926
|
+
|
|
6678
6927
|
if (action !== "document" && action !== "editor-url") {
|
|
6679
6928
|
respondJson(res, 400, { ok: false, error: "Unsupported local link action." });
|
|
6680
6929
|
return;
|
|
6681
6930
|
}
|
|
6682
|
-
if (resource.kind !== "text") {
|
|
6683
|
-
respondJson(res, 400, { ok: false, error: "This local resource is not a
|
|
6931
|
+
if (resource.kind !== "text" && resource.kind !== "office") {
|
|
6932
|
+
respondJson(res, 400, { ok: false, error: "This local resource is not a document Studio can load into the editor." });
|
|
6684
6933
|
return;
|
|
6685
6934
|
}
|
|
6686
6935
|
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6936
|
+
let document: InitialStudioDocument;
|
|
6937
|
+
let responseText = "";
|
|
6938
|
+
let converted = false;
|
|
6939
|
+
if (resource.kind === "office") {
|
|
6940
|
+
let conversion: { text: string; label: string };
|
|
6941
|
+
try {
|
|
6942
|
+
conversion = await convertStudioOfficeDocumentToMarkdown(resource);
|
|
6943
|
+
} catch (error) {
|
|
6944
|
+
respondJson(res, 400, { ok: false, error: `Document conversion failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
6945
|
+
return;
|
|
6946
|
+
}
|
|
6947
|
+
converted = true;
|
|
6948
|
+
responseText = conversion.text;
|
|
6949
|
+
document = {
|
|
6950
|
+
text: conversion.text,
|
|
6951
|
+
label: conversion.label,
|
|
6952
|
+
source: "blank",
|
|
6953
|
+
resourceDir: resource.resourceDir,
|
|
6954
|
+
};
|
|
6955
|
+
} else {
|
|
6956
|
+
const file = readStudioFile(resource.filePath, dirname(resource.filePath));
|
|
6957
|
+
if (file.ok === false) {
|
|
6958
|
+
respondJson(res, 400, { ok: false, error: file.message });
|
|
6959
|
+
return;
|
|
6960
|
+
}
|
|
6961
|
+
responseText = file.text;
|
|
6962
|
+
document = {
|
|
6963
|
+
text: file.text,
|
|
6964
|
+
label: resource.label || file.label,
|
|
6965
|
+
source: "file",
|
|
6966
|
+
path: file.resolvedPath,
|
|
6967
|
+
resourceDir: resource.resourceDir,
|
|
6968
|
+
};
|
|
6691
6969
|
}
|
|
6692
|
-
|
|
6693
|
-
const document: InitialStudioDocument = {
|
|
6694
|
-
text: file.text,
|
|
6695
|
-
label: resource.label || file.label,
|
|
6696
|
-
source: "file",
|
|
6697
|
-
path: file.resolvedPath,
|
|
6698
|
-
resourceDir: resource.resourceDir,
|
|
6699
|
-
};
|
|
6700
6970
|
if (action === "document") {
|
|
6701
6971
|
respondJson(res, 200, {
|
|
6702
6972
|
...basePayload,
|
|
6703
|
-
text:
|
|
6973
|
+
text: responseText,
|
|
6974
|
+
label: document.label,
|
|
6975
|
+
converted,
|
|
6704
6976
|
resourceDir: resource.resourceDir,
|
|
6705
6977
|
});
|
|
6706
6978
|
return;
|
|
@@ -6711,6 +6983,7 @@ function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResponse,
|
|
|
6711
6983
|
const parsedUrl = new URL(url);
|
|
6712
6984
|
respondJson(res, 200, {
|
|
6713
6985
|
...basePayload,
|
|
6986
|
+
converted,
|
|
6714
6987
|
url,
|
|
6715
6988
|
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
6716
6989
|
});
|
|
@@ -7217,6 +7490,8 @@ function buildStudioCompletionSuggestionPrompt(options: {
|
|
|
7217
7490
|
language?: string;
|
|
7218
7491
|
label?: string;
|
|
7219
7492
|
path?: string;
|
|
7493
|
+
contextMode?: "cursor" | "session";
|
|
7494
|
+
contextText?: string;
|
|
7220
7495
|
}): string {
|
|
7221
7496
|
const text = String(options.text || "");
|
|
7222
7497
|
const start = Math.max(0, Math.min(Math.floor(options.selectionStart || 0), text.length));
|
|
@@ -7226,17 +7501,23 @@ function buildStudioCompletionSuggestionPrompt(options: {
|
|
|
7226
7501
|
const suffix = text.slice(end, Math.min(text.length, end + STUDIO_COMPLETION_SUFFIX_CHARS));
|
|
7227
7502
|
const language = String(options.language || "").trim() || "unknown";
|
|
7228
7503
|
const label = String(options.label || options.path || "Studio editor").trim();
|
|
7504
|
+
const contextText = String(options.contextText || "").trim().slice(-STUDIO_COMPLETION_MAX_CONTEXT_CHARS);
|
|
7229
7505
|
return [
|
|
7230
7506
|
"Generate an inline completion for the current editor cursor position.",
|
|
7231
7507
|
"Return only the exact text to insert. Do not wrap it in Markdown fences. Do not explain.",
|
|
7232
7508
|
"Match the surrounding language, style, indentation, and register.",
|
|
7233
7509
|
"Keep the suggestion short unless the context clearly asks for a longer continuation.",
|
|
7510
|
+
contextText
|
|
7511
|
+
? "Use the extra session context only as background. Do not continue the extra context directly unless the editor cursor calls for it."
|
|
7512
|
+
: "Use only the cursor-local editor context below.",
|
|
7234
7513
|
selected
|
|
7235
7514
|
? "The selected text will be replaced by the completion."
|
|
7236
7515
|
: "The completion will be inserted at the cursor.",
|
|
7237
7516
|
"",
|
|
7238
7517
|
`File/context label: ${label}`,
|
|
7239
7518
|
`Language mode: ${language}`,
|
|
7519
|
+
`Suggestion context mode: ${contextText ? "editor plus latest response" : "editor only"}`,
|
|
7520
|
+
contextText ? ["", "<extra_context>", contextText, "</extra_context>"].join("\n") : "",
|
|
7240
7521
|
"",
|
|
7241
7522
|
"<prefix>",
|
|
7242
7523
|
prefix,
|
|
@@ -7262,6 +7543,9 @@ async function runStudioCompletionSuggestion(ctx: StudioModelRequestContext, opt
|
|
|
7262
7543
|
language?: string;
|
|
7263
7544
|
label?: string;
|
|
7264
7545
|
path?: string;
|
|
7546
|
+
contextMode?: "cursor" | "session";
|
|
7547
|
+
contextText?: string;
|
|
7548
|
+
signal?: AbortSignal;
|
|
7265
7549
|
}): Promise<string> {
|
|
7266
7550
|
const prompt = buildStudioCompletionSuggestionPrompt(options);
|
|
7267
7551
|
// Intentionally omit `reasoning`: pi-ai treats absent reasoning as off/disabled
|
|
@@ -7271,6 +7555,7 @@ async function runStudioCompletionSuggestion(ctx: StudioModelRequestContext, opt
|
|
|
7271
7555
|
maxTokens: 650,
|
|
7272
7556
|
timeoutMs: 60_000,
|
|
7273
7557
|
trim: false,
|
|
7558
|
+
signal: options.signal,
|
|
7274
7559
|
}));
|
|
7275
7560
|
if (!suggestion.trim()) throw new Error("Model returned an empty completion suggestion.");
|
|
7276
7561
|
return suggestion;
|
|
@@ -7685,12 +7970,20 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
7685
7970
|
};
|
|
7686
7971
|
}
|
|
7687
7972
|
|
|
7973
|
+
if (msg.type === "completion_suggestion_cancel_request" && typeof msg.requestId === "string") {
|
|
7974
|
+
return {
|
|
7975
|
+
type: "completion_suggestion_cancel_request",
|
|
7976
|
+
requestId: msg.requestId,
|
|
7977
|
+
};
|
|
7978
|
+
}
|
|
7979
|
+
|
|
7688
7980
|
if (msg.type === "completion_suggestion_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
|
|
7689
7981
|
const textLength = msg.text.length;
|
|
7690
7982
|
const rawStart = typeof msg.selectionStart === "number" && Number.isFinite(msg.selectionStart) ? msg.selectionStart : textLength;
|
|
7691
7983
|
const rawEnd = typeof msg.selectionEnd === "number" && Number.isFinite(msg.selectionEnd) ? msg.selectionEnd : rawStart;
|
|
7692
7984
|
const selectionStart = Math.max(0, Math.min(Math.floor(rawStart), textLength));
|
|
7693
7985
|
const selectionEnd = Math.max(selectionStart, Math.min(Math.floor(rawEnd), textLength));
|
|
7986
|
+
const contextMode = msg.contextMode === "session" ? "session" : "cursor";
|
|
7694
7987
|
return {
|
|
7695
7988
|
type: "completion_suggestion_request",
|
|
7696
7989
|
requestId: msg.requestId,
|
|
@@ -7700,6 +7993,8 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
7700
7993
|
language: typeof msg.language === "string" ? msg.language : undefined,
|
|
7701
7994
|
label: typeof msg.label === "string" ? msg.label : undefined,
|
|
7702
7995
|
path: typeof msg.path === "string" ? msg.path : undefined,
|
|
7996
|
+
contextMode,
|
|
7997
|
+
contextText: contextMode === "session" && typeof msg.contextText === "string" ? msg.contextText.slice(-STUDIO_COMPLETION_MAX_CONTEXT_CHARS) : undefined,
|
|
7703
7998
|
};
|
|
7704
7999
|
}
|
|
7705
8000
|
|
|
@@ -9582,6 +9877,11 @@ ${cssVarsBlock}
|
|
|
9582
9877
|
<div class="source-actions-row">
|
|
9583
9878
|
<button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy</button>
|
|
9584
9879
|
<button id="suggestCompletionBtn" type="button" title="Ask the current model for a short completion at the editor cursor. Shortcut: Option/Alt+Tab where available, or Cmd/Ctrl+Shift+Space from the editor.">Suggest</button>
|
|
9880
|
+
<button id="suggestCompletionOptionsBtn" type="button" hidden title="Suggestion context options">▾</button>
|
|
9881
|
+
<select id="completionContextSelect" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
|
|
9882
|
+
<option value="cursor" selected>Context: editor only</option>
|
|
9883
|
+
<option value="session">Context: editor + latest response</option>
|
|
9884
|
+
</select>
|
|
9585
9885
|
<button id="openCompanionBtn" type="button" title="Open a detached copy of the current editor text in a new editor-only Studio tab.">New editor</button>
|
|
9586
9886
|
<button id="sendEditorBtn" type="button">Send to pi editor</button>
|
|
9587
9887
|
</div>
|
|
@@ -9608,6 +9908,7 @@ ${cssVarsBlock}
|
|
|
9608
9908
|
<option value="c">Syntax highlight: C</option>
|
|
9609
9909
|
<option value="cpp">Syntax highlight: C++</option>
|
|
9610
9910
|
<option value="css">Syntax highlight: CSS</option>
|
|
9911
|
+
<option value="csv">Syntax highlight: CSV</option>
|
|
9611
9912
|
<option value="diff">Syntax highlight: Diff</option>
|
|
9612
9913
|
<option value="fortran">Syntax highlight: Fortran</option>
|
|
9613
9914
|
<option value="go">Syntax highlight: Go</option>
|
|
@@ -9626,6 +9927,7 @@ ${cssVarsBlock}
|
|
|
9626
9927
|
<option value="rust">Syntax highlight: Rust</option>
|
|
9627
9928
|
<option value="swift">Syntax highlight: Swift</option>
|
|
9628
9929
|
<option value="toml">Syntax highlight: TOML</option>
|
|
9930
|
+
<option value="tsv">Syntax highlight: TSV</option>
|
|
9629
9931
|
<option value="typescript">Syntax highlight: TypeScript</option>
|
|
9630
9932
|
<option value="xml">Syntax highlight: XML</option>
|
|
9631
9933
|
<option value="yaml">Syntax highlight: YAML</option>
|
|
@@ -9811,11 +10113,11 @@ ${cssVarsBlock}
|
|
|
9811
10113
|
<div class="shortcuts-header">
|
|
9812
10114
|
<div>
|
|
9813
10115
|
<h2 id="shortcutsTitle">Keyboard shortcuts</h2>
|
|
9814
|
-
<p class="shortcuts-description">Studio navigation and high-frequency actions.</p>
|
|
10116
|
+
<p class="shortcuts-description">Studio navigation and high-frequency actions. Use arrow keys, Page Up/Down, Home/End, or mouse/trackpad to scroll.</p>
|
|
9815
10117
|
</div>
|
|
9816
10118
|
<button id="shortcutsCloseBtn" class="shortcuts-close-btn" type="button" aria-label="Close keyboard shortcuts">Close</button>
|
|
9817
10119
|
</div>
|
|
9818
|
-
<div class="shortcuts-body">
|
|
10120
|
+
<div id="shortcutsBody" class="shortcuts-body" tabindex="0" aria-label="Keyboard shortcuts list">
|
|
9819
10121
|
<section class="shortcuts-group">
|
|
9820
10122
|
<h3>Navigation</h3>
|
|
9821
10123
|
<dl>
|
|
@@ -9844,6 +10146,7 @@ ${cssVarsBlock}
|
|
|
9844
10146
|
<div><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
9845
10147
|
<div><dt>Option/Alt+Tab or Cmd/Ctrl+Shift+Space</dt><dd>Suggest a completion at the editor cursor</dd></div>
|
|
9846
10148
|
<div><dt>Tab</dt><dd>Insert a visible completion suggestion; otherwise indent selected editor text</dd></div>
|
|
10149
|
+
<div><dt>Esc</dt><dd>Dismiss a visible completion suggestion, close overlays, exit pane focus, or stop an active request</dd></div>
|
|
9847
10150
|
<div><dt>Shift+Tab</dt><dd>Unindent selected editor text</dd></div>
|
|
9848
10151
|
</dl>
|
|
9849
10152
|
</section>
|
|
@@ -9938,6 +10241,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9938
10241
|
let studioReplActiveSessionName: string | null = null;
|
|
9939
10242
|
let compactInProgress = false;
|
|
9940
10243
|
let compactRequestId: string | null = null;
|
|
10244
|
+
const activeCompletionSuggestions = new Map<string, AbortController>();
|
|
9941
10245
|
|
|
9942
10246
|
const selectStudioReplSessionForTool = (params: { sessionName?: string; target?: string }): { session: StudioReplSessionInfo | null; error?: string; sessions: StudioReplSessionInfo[] } => {
|
|
9943
10247
|
const state = listStudioReplSessions();
|
|
@@ -11424,6 +11728,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
11424
11728
|
return;
|
|
11425
11729
|
}
|
|
11426
11730
|
|
|
11731
|
+
if (msg.type === "completion_suggestion_cancel_request") {
|
|
11732
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
11733
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
11734
|
+
return;
|
|
11735
|
+
}
|
|
11736
|
+
const controller = activeCompletionSuggestions.get(msg.requestId);
|
|
11737
|
+
if (!controller) {
|
|
11738
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "No matching suggestion request is running." });
|
|
11739
|
+
return;
|
|
11740
|
+
}
|
|
11741
|
+
controller.abort();
|
|
11742
|
+
sendToClient(client, { type: "completion_suggestion_progress", requestId: msg.requestId, message: "Stopping suggestion…" });
|
|
11743
|
+
return;
|
|
11744
|
+
}
|
|
11745
|
+
|
|
11427
11746
|
if (msg.type === "completion_suggestion_request") {
|
|
11428
11747
|
if (!isValidRequestId(msg.requestId)) {
|
|
11429
11748
|
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
@@ -11443,6 +11762,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
11443
11762
|
return;
|
|
11444
11763
|
}
|
|
11445
11764
|
sendToClient(client, { type: "completion_suggestion_progress", requestId: msg.requestId, message: "Generating suggestion…" });
|
|
11765
|
+
const completionController = new AbortController();
|
|
11766
|
+
activeCompletionSuggestions.set(msg.requestId, completionController);
|
|
11446
11767
|
void (async () => {
|
|
11447
11768
|
try {
|
|
11448
11769
|
const suggestion = await runStudioCompletionSuggestion(ctx, {
|
|
@@ -11452,6 +11773,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
11452
11773
|
language: msg.language,
|
|
11453
11774
|
label: msg.label,
|
|
11454
11775
|
path: msg.path,
|
|
11776
|
+
contextMode: msg.contextMode,
|
|
11777
|
+
contextText: msg.contextText,
|
|
11778
|
+
signal: completionController.signal,
|
|
11455
11779
|
});
|
|
11456
11780
|
sendToClient(client, {
|
|
11457
11781
|
type: "completion_suggestion_result",
|
|
@@ -11464,8 +11788,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
11464
11788
|
sendToClient(client, {
|
|
11465
11789
|
type: "completion_suggestion_error",
|
|
11466
11790
|
requestId: msg.requestId,
|
|
11467
|
-
message:
|
|
11791
|
+
message: completionController.signal.aborted
|
|
11792
|
+
? "Suggestion stopped."
|
|
11793
|
+
: `Suggestion failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
11468
11794
|
});
|
|
11795
|
+
} finally {
|
|
11796
|
+
activeCompletionSuggestions.delete(msg.requestId);
|
|
11469
11797
|
}
|
|
11470
11798
|
})();
|
|
11471
11799
|
return;
|
|
@@ -12011,6 +12339,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12011
12339
|
};
|
|
12012
12340
|
|
|
12013
12341
|
const disposePreparedPdfExport = (entry: PreparedStudioPdfExport | null | undefined) => {
|
|
12342
|
+
if (entry?.persistent) return;
|
|
12014
12343
|
if (!entry?.tempDirPath) return;
|
|
12015
12344
|
void rm(entry.tempDirPath, { recursive: true, force: true }).catch(() => undefined);
|
|
12016
12345
|
};
|
|
@@ -12039,7 +12368,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12039
12368
|
}
|
|
12040
12369
|
};
|
|
12041
12370
|
|
|
12042
|
-
const storePreparedPdfExport = (pdf: Buffer, filename: string, warning?: string): string => {
|
|
12371
|
+
const storePreparedPdfExport = (pdf: Buffer, filename: string, warning?: string, filePath?: string): string => {
|
|
12043
12372
|
prunePreparedPdfExports();
|
|
12044
12373
|
const exportId = randomUUID();
|
|
12045
12374
|
preparedPdfExports.set(exportId, {
|
|
@@ -12047,6 +12376,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12047
12376
|
filename,
|
|
12048
12377
|
warning,
|
|
12049
12378
|
createdAt: Date.now(),
|
|
12379
|
+
filePath,
|
|
12380
|
+
persistent: Boolean(filePath),
|
|
12050
12381
|
});
|
|
12051
12382
|
return exportId;
|
|
12052
12383
|
};
|
|
@@ -12055,7 +12386,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12055
12386
|
prunePreparedPdfExports();
|
|
12056
12387
|
const entry = preparedPdfExports.get(exportId);
|
|
12057
12388
|
if (!entry) return null;
|
|
12058
|
-
if (entry.filePath && entry.tempDirPath) return entry;
|
|
12389
|
+
if (entry.filePath && (entry.tempDirPath || entry.persistent)) return entry;
|
|
12059
12390
|
|
|
12060
12391
|
const tempDirPath = join(tmpdir(), `pi-studio-prepared-pdf-${Date.now()}-${randomUUID()}`);
|
|
12061
12392
|
const filePath = join(tempDirPath, sanitizePdfFilename(entry.filename));
|
|
@@ -12105,6 +12436,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12105
12436
|
};
|
|
12106
12437
|
|
|
12107
12438
|
const disposePreparedHtmlExport = (entry: PreparedStudioHtmlExport | null | undefined) => {
|
|
12439
|
+
if (entry?.persistent) return;
|
|
12108
12440
|
if (!entry?.tempDirPath) return;
|
|
12109
12441
|
void rm(entry.tempDirPath, { recursive: true, force: true }).catch(() => undefined);
|
|
12110
12442
|
};
|
|
@@ -12133,7 +12465,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12133
12465
|
}
|
|
12134
12466
|
};
|
|
12135
12467
|
|
|
12136
|
-
const storePreparedHtmlExport = (html: Buffer, filename: string, warning?: string): string => {
|
|
12468
|
+
const storePreparedHtmlExport = (html: Buffer, filename: string, warning?: string, filePath?: string): string => {
|
|
12137
12469
|
prunePreparedHtmlExports();
|
|
12138
12470
|
const exportId = randomUUID();
|
|
12139
12471
|
preparedHtmlExports.set(exportId, {
|
|
@@ -12141,6 +12473,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12141
12473
|
filename,
|
|
12142
12474
|
warning,
|
|
12143
12475
|
createdAt: Date.now(),
|
|
12476
|
+
filePath,
|
|
12477
|
+
persistent: Boolean(filePath),
|
|
12144
12478
|
});
|
|
12145
12479
|
return exportId;
|
|
12146
12480
|
};
|
|
@@ -12149,7 +12483,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12149
12483
|
prunePreparedHtmlExports();
|
|
12150
12484
|
const entry = preparedHtmlExports.get(exportId);
|
|
12151
12485
|
if (!entry) return null;
|
|
12152
|
-
if (entry.filePath && entry.tempDirPath) return entry;
|
|
12486
|
+
if (entry.filePath && (entry.tempDirPath || entry.persistent)) return entry;
|
|
12153
12487
|
|
|
12154
12488
|
const tempDirPath = join(tmpdir(), `pi-studio-prepared-html-${Date.now()}-${randomUUID()}`);
|
|
12155
12489
|
const filePath = join(tempDirPath, sanitizeHtmlFilename(entry.filename));
|
|
@@ -12570,7 +12904,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12570
12904
|
|
|
12571
12905
|
try {
|
|
12572
12906
|
const { pdf, warning } = await renderStudioPdfWithPandoc(markdown, isLatex, resourcePath, editorPdfLanguage, sourcePath || undefined);
|
|
12573
|
-
const
|
|
12907
|
+
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), pdf);
|
|
12908
|
+
const exportId = storePreparedPdfExport(pdf, filename, warning, writeResult.filePath ?? undefined);
|
|
12574
12909
|
const token = serverState?.token ?? "";
|
|
12575
12910
|
let openedExternal = false;
|
|
12576
12911
|
let openError: string | null = null;
|
|
@@ -12587,6 +12922,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12587
12922
|
respondJson(res, 200, {
|
|
12588
12923
|
ok: true,
|
|
12589
12924
|
filename,
|
|
12925
|
+
path: writeResult.filePath,
|
|
12926
|
+
writeError: writeResult.error,
|
|
12590
12927
|
warning: warning ?? null,
|
|
12591
12928
|
openedExternal,
|
|
12592
12929
|
openError,
|
|
@@ -12681,7 +13018,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12681
13018
|
themeVars,
|
|
12682
13019
|
},
|
|
12683
13020
|
);
|
|
12684
|
-
const
|
|
13021
|
+
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), html);
|
|
13022
|
+
const exportId = storePreparedHtmlExport(html, filename, warning, writeResult.filePath ?? undefined);
|
|
12685
13023
|
const token = serverState?.token ?? "";
|
|
12686
13024
|
let openedExternal = false;
|
|
12687
13025
|
let openError: string | null = null;
|
|
@@ -12698,6 +13036,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12698
13036
|
respondJson(res, 200, {
|
|
12699
13037
|
ok: true,
|
|
12700
13038
|
filename,
|
|
13039
|
+
path: writeResult.filePath,
|
|
13040
|
+
writeError: writeResult.error,
|
|
12701
13041
|
warning: warning ?? null,
|
|
12702
13042
|
openedExternal,
|
|
12703
13043
|
openError,
|
|
@@ -12990,17 +13330,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
12990
13330
|
return;
|
|
12991
13331
|
}
|
|
12992
13332
|
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13333
|
+
void (async () => {
|
|
13334
|
+
try {
|
|
13335
|
+
const resource = resolveStudioLocalPreviewResourcePath(
|
|
13336
|
+
requestUrl.searchParams.get("path") ?? "",
|
|
13337
|
+
requestUrl.searchParams.get("sourcePath") ?? undefined,
|
|
13338
|
+
requestUrl.searchParams.get("resourceDir") ?? undefined,
|
|
13339
|
+
studioCwd,
|
|
13340
|
+
);
|
|
13341
|
+
await respondLocalPreviewLinkJson(req, res, requestUrl, resource, serverState);
|
|
13342
|
+
} catch (error) {
|
|
13343
|
+
respondJson(res, 404, { ok: false, error: `Local resource unavailable: ${error instanceof Error ? error.message : String(error)}` });
|
|
13344
|
+
}
|
|
13345
|
+
})();
|
|
13004
13346
|
return;
|
|
13005
13347
|
}
|
|
13006
13348
|
|