pi-studio 0.7.1 → 0.8.1
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/README.md +6 -4
- package/client/studio-client.js +796 -29
- package/client/studio.css +169 -0
- package/index.ts +1151 -17
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, SessionEntry, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
@@ -71,12 +71,22 @@ interface LastStudioResponse {
|
|
|
71
71
|
kind: StudioRequestKind;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
interface StudioTraceSnapshotSummary {
|
|
75
|
+
hasTrace: boolean;
|
|
76
|
+
entryCount: number;
|
|
77
|
+
startedAt: number | null;
|
|
78
|
+
updatedAt: number | null;
|
|
79
|
+
status: StudioTraceRunStatus;
|
|
80
|
+
truncated: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
interface StudioResponseHistoryItem extends StudioPromptDescriptor {
|
|
75
84
|
id: string;
|
|
76
85
|
markdown: string;
|
|
77
86
|
thinking: string | null;
|
|
78
87
|
timestamp: number;
|
|
79
88
|
kind: StudioRequestKind;
|
|
89
|
+
traceSummary?: StudioTraceSnapshotSummary;
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
interface StudioDirectRunChain {
|
|
@@ -110,6 +120,40 @@ interface PreparedStudioPdfExport {
|
|
|
110
120
|
tempDirPath?: string;
|
|
111
121
|
}
|
|
112
122
|
|
|
123
|
+
interface PreparedStudioHtmlExport {
|
|
124
|
+
html: Buffer;
|
|
125
|
+
filename: string;
|
|
126
|
+
warning?: string;
|
|
127
|
+
createdAt: number;
|
|
128
|
+
filePath?: string;
|
|
129
|
+
tempDirPath?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface StudioHtmlAnnotationPlaceholder {
|
|
133
|
+
token: string;
|
|
134
|
+
text: string;
|
|
135
|
+
title: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface StudioHtmlPdfBlockOptions {
|
|
139
|
+
path: string;
|
|
140
|
+
title: string;
|
|
141
|
+
caption: string;
|
|
142
|
+
page: string;
|
|
143
|
+
height: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface StudioHtmlPdfBlock {
|
|
147
|
+
placeholder: string;
|
|
148
|
+
options: StudioHtmlPdfBlockOptions;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface StudioHtmlRenderOptions {
|
|
152
|
+
title?: string;
|
|
153
|
+
sourceLabel?: string;
|
|
154
|
+
themeVars?: Record<string, string>;
|
|
155
|
+
}
|
|
156
|
+
|
|
113
157
|
interface InitialStudioDocument {
|
|
114
158
|
text: string;
|
|
115
159
|
label: string;
|
|
@@ -190,6 +234,11 @@ interface GetLatestResponseMessage {
|
|
|
190
234
|
type: "get_latest_response";
|
|
191
235
|
}
|
|
192
236
|
|
|
237
|
+
interface GetTraceSnapshotMessage {
|
|
238
|
+
type: "get_trace_snapshot";
|
|
239
|
+
responseHistoryId: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
193
242
|
interface CritiqueRequestMessage {
|
|
194
243
|
type: "critique_request";
|
|
195
244
|
requestId: string;
|
|
@@ -269,6 +318,7 @@ type IncomingStudioMessage =
|
|
|
269
318
|
| HelloMessage
|
|
270
319
|
| PingMessage
|
|
271
320
|
| GetLatestResponseMessage
|
|
321
|
+
| GetTraceSnapshotMessage
|
|
272
322
|
| CritiqueRequestMessage
|
|
273
323
|
| AnnotationRequestMessage
|
|
274
324
|
| SendRunRequestMessage
|
|
@@ -285,11 +335,17 @@ type IncomingStudioMessage =
|
|
|
285
335
|
const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
286
336
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
287
337
|
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
338
|
+
const HTML_EXPORT_MAX_CHARS = 400_000;
|
|
288
339
|
const REQUEST_BODY_MAX_BYTES = 1_000_000;
|
|
289
340
|
const RESPONSE_HISTORY_LIMIT = 30;
|
|
290
341
|
const CMUX_NOTIFY_TIMEOUT_MS = 1200;
|
|
291
342
|
const PREPARED_PDF_EXPORT_TTL_MS = 5 * 60 * 1000;
|
|
343
|
+
const PREPARED_HTML_EXPORT_TTL_MS = 5 * 60 * 1000;
|
|
292
344
|
const MAX_PREPARED_PDF_EXPORTS = 8;
|
|
345
|
+
const MAX_PREPARED_HTML_EXPORTS = 8;
|
|
346
|
+
const STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES = 80;
|
|
347
|
+
const STUDIO_TRACE_SNAPSHOT_MAX_FIELD_CHARS = 20_000;
|
|
348
|
+
const MAX_STUDIO_TRACE_SNAPSHOTS = RESPONSE_HISTORY_LIMIT;
|
|
293
349
|
const TRANSIENT_STUDIO_DOCUMENT_TTL_MS = 30 * 60 * 1000;
|
|
294
350
|
const MAX_TRANSIENT_STUDIO_DOCUMENTS = 16;
|
|
295
351
|
const STUDIO_TERMINAL_NOTIFY_TITLE = "pi Studio";
|
|
@@ -1538,10 +1594,64 @@ function readStudioFile(pathArg: string, cwd: string):
|
|
|
1538
1594
|
|
|
1539
1595
|
function inferStudioPdfLanguageFromPath(pathInput: string): string | undefined {
|
|
1540
1596
|
const extension = extname(pathInput).toLowerCase();
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1597
|
+
const languageByExtension: Record<string, string> = {
|
|
1598
|
+
".md": "markdown",
|
|
1599
|
+
".markdown": "markdown",
|
|
1600
|
+
".mdx": "markdown",
|
|
1601
|
+
".qmd": "markdown",
|
|
1602
|
+
".tex": "latex",
|
|
1603
|
+
".latex": "latex",
|
|
1604
|
+
".diff": "diff",
|
|
1605
|
+
".patch": "diff",
|
|
1606
|
+
".js": "javascript",
|
|
1607
|
+
".mjs": "javascript",
|
|
1608
|
+
".cjs": "javascript",
|
|
1609
|
+
".jsx": "javascript",
|
|
1610
|
+
".ts": "typescript",
|
|
1611
|
+
".mts": "typescript",
|
|
1612
|
+
".cts": "typescript",
|
|
1613
|
+
".tsx": "typescript",
|
|
1614
|
+
".py": "python",
|
|
1615
|
+
".pyw": "python",
|
|
1616
|
+
".sh": "bash",
|
|
1617
|
+
".bash": "bash",
|
|
1618
|
+
".zsh": "bash",
|
|
1619
|
+
".json": "json",
|
|
1620
|
+
".jsonc": "json",
|
|
1621
|
+
".json5": "json",
|
|
1622
|
+
".rs": "rust",
|
|
1623
|
+
".c": "c",
|
|
1624
|
+
".h": "c",
|
|
1625
|
+
".cpp": "cpp",
|
|
1626
|
+
".cxx": "cpp",
|
|
1627
|
+
".cc": "cpp",
|
|
1628
|
+
".hpp": "cpp",
|
|
1629
|
+
".hxx": "cpp",
|
|
1630
|
+
".jl": "julia",
|
|
1631
|
+
".f90": "fortran",
|
|
1632
|
+
".f95": "fortran",
|
|
1633
|
+
".f03": "fortran",
|
|
1634
|
+
".f": "fortran",
|
|
1635
|
+
".for": "fortran",
|
|
1636
|
+
".r": "r",
|
|
1637
|
+
".m": "matlab",
|
|
1638
|
+
".java": "java",
|
|
1639
|
+
".go": "go",
|
|
1640
|
+
".rb": "ruby",
|
|
1641
|
+
".swift": "swift",
|
|
1642
|
+
".html": "html",
|
|
1643
|
+
".htm": "html",
|
|
1644
|
+
".css": "css",
|
|
1645
|
+
".xml": "xml",
|
|
1646
|
+
".yaml": "yaml",
|
|
1647
|
+
".yml": "yaml",
|
|
1648
|
+
".toml": "toml",
|
|
1649
|
+
".lua": "lua",
|
|
1650
|
+
".txt": "text",
|
|
1651
|
+
".rst": "text",
|
|
1652
|
+
".adoc": "text",
|
|
1653
|
+
};
|
|
1654
|
+
return languageByExtension[extension];
|
|
1545
1655
|
}
|
|
1546
1656
|
|
|
1547
1657
|
function buildStudioPdfOutputPath(sourcePath: string): string {
|
|
@@ -1553,6 +1663,32 @@ function buildStudioPdfOutputPath(sourcePath: string): string {
|
|
|
1553
1663
|
return join(sourceDir, `${outputStem}.studio.pdf`);
|
|
1554
1664
|
}
|
|
1555
1665
|
|
|
1666
|
+
function buildStudioHtmlOutputPath(sourcePath: string): string {
|
|
1667
|
+
const sourceDir = dirname(sourcePath);
|
|
1668
|
+
const sourceName = basename(sourcePath);
|
|
1669
|
+
const sourceExt = extname(sourceName);
|
|
1670
|
+
const sourceStem = sourceExt ? sourceName.slice(0, -sourceExt.length) : sourceName;
|
|
1671
|
+
const outputStem = sourceStem || sourceName || "studio-export";
|
|
1672
|
+
return join(sourceDir, `${outputStem}.studio.html`);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function formatStudioExportTimestamp(date = new Date()): string {
|
|
1676
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
1677
|
+
return [
|
|
1678
|
+
String(date.getFullYear()),
|
|
1679
|
+
pad(date.getMonth() + 1),
|
|
1680
|
+
pad(date.getDate()),
|
|
1681
|
+
"-",
|
|
1682
|
+
pad(date.getHours()),
|
|
1683
|
+
pad(date.getMinutes()),
|
|
1684
|
+
pad(date.getSeconds()),
|
|
1685
|
+
].join("");
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function buildStudioResponseExportOutputPath(cwd: string, extension: "pdf" | "html"): string {
|
|
1689
|
+
return join(cwd || process.cwd(), `studio-response-${formatStudioExportTimestamp()}.studio.${extension}`);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1556
1692
|
function writeStudioFile(pathArg: string, cwd: string, content: string):
|
|
1557
1693
|
| { ok: true; label: string; resolvedPath: string }
|
|
1558
1694
|
| { ok: false; message: string } {
|
|
@@ -3160,6 +3296,33 @@ function normalizeStudioEditorLanguage(language: string | undefined): string | u
|
|
|
3160
3296
|
return trimmed;
|
|
3161
3297
|
}
|
|
3162
3298
|
|
|
3299
|
+
function stripLeadingStudioHtmlTrivia(text: string): string {
|
|
3300
|
+
let source = String(text ?? "").replace(/^\uFEFF/, "").trimStart();
|
|
3301
|
+
let previous = "";
|
|
3302
|
+
while (source && source !== previous) {
|
|
3303
|
+
previous = source;
|
|
3304
|
+
source = source.replace(/^<!--[\s\S]*?-->\s*/, "").trimStart();
|
|
3305
|
+
}
|
|
3306
|
+
return source;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function isStudioHtmlMarkup(text: string): boolean {
|
|
3310
|
+
return /<[A-Za-z][A-Za-z0-9:-]*(?:\s[^<>]*)?>/.test(String(text ?? ""));
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
function isLikelyStandaloneStudioHtml(text: string, editorLanguage?: string): boolean {
|
|
3314
|
+
const source = String(text ?? "");
|
|
3315
|
+
if (!source.trim()) return false;
|
|
3316
|
+
if (parseStudioSingleFencedCodeBlock(source)) return false;
|
|
3317
|
+
|
|
3318
|
+
const leading = stripLeadingStudioHtmlTrivia(source);
|
|
3319
|
+
if (/^<!doctype\s+html\b/i.test(leading)) return true;
|
|
3320
|
+
if (/^<html(?:\s|>|$)/i.test(leading)) return true;
|
|
3321
|
+
if (/^<body(?:\s|>|$)/i.test(leading) && /<\/body\s*>/i.test(leading)) return true;
|
|
3322
|
+
|
|
3323
|
+
return normalizeStudioEditorLanguage(editorLanguage) === "html" && isStudioHtmlMarkup(source);
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3163
3326
|
function parseStudioSingleFencedCodeBlock(markdown: string): { info: string; content: string } | null {
|
|
3164
3327
|
const trimmed = markdown.trim();
|
|
3165
3328
|
if (!trimmed) return null;
|
|
@@ -3332,6 +3495,7 @@ function isLikelyRawStudioGitDiff(markdown: string): boolean {
|
|
|
3332
3495
|
function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): string | undefined {
|
|
3333
3496
|
const normalizedEditorLanguage = normalizeStudioEditorLanguage(editorLanguage);
|
|
3334
3497
|
if (normalizedEditorLanguage) return normalizedEditorLanguage;
|
|
3498
|
+
if (isLikelyStandaloneStudioHtml(markdown)) return "html";
|
|
3335
3499
|
|
|
3336
3500
|
const fenced = parseStudioSingleFencedCodeBlock(markdown);
|
|
3337
3501
|
if (fenced) {
|
|
@@ -3593,7 +3757,7 @@ interface StudioPdfRenderOptions {
|
|
|
3593
3757
|
}
|
|
3594
3758
|
|
|
3595
3759
|
interface StudioParsedPdfCommandArgs {
|
|
3596
|
-
pathArg: string;
|
|
3760
|
+
pathArg: string | null;
|
|
3597
3761
|
options: StudioPdfRenderOptions;
|
|
3598
3762
|
}
|
|
3599
3763
|
|
|
@@ -3925,7 +4089,6 @@ function parseStudioPdfCommandArgs(args: string): StudioParsedPdfCommandArgs | {
|
|
|
3925
4089
|
const parsed = tokenizeStudioCommandArgs(args);
|
|
3926
4090
|
if (parsed.error) return { error: parsed.error };
|
|
3927
4091
|
const tokens = parsed.tokens;
|
|
3928
|
-
if (tokens.length === 0) return { error: "Missing file path." };
|
|
3929
4092
|
|
|
3930
4093
|
const options: StudioPdfRenderOptions = {};
|
|
3931
4094
|
let pathArg: string | null = null;
|
|
@@ -4031,7 +4194,6 @@ function parseStudioPdfCommandArgs(args: string): StudioParsedPdfCommandArgs | {
|
|
|
4031
4194
|
}
|
|
4032
4195
|
}
|
|
4033
4196
|
|
|
4034
|
-
if (!pathArg) return { error: "Missing file path." };
|
|
4035
4197
|
if (options.geometry && (options.margin || options.marginTop || options.marginRight || options.marginBottom || options.marginLeft || options.footskip)) {
|
|
4036
4198
|
return { error: "Use either --geometry or the --margin/--margin-*/--footskip flags, not both." };
|
|
4037
4199
|
}
|
|
@@ -4489,6 +4651,368 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
4489
4651
|
return renderedHtml;
|
|
4490
4652
|
}
|
|
4491
4653
|
|
|
4654
|
+
function escapeStudioRegExpLiteral(text: string): string {
|
|
4655
|
+
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
function parseStudioHtmlPdfBlockOptions(body: string): StudioHtmlPdfBlockOptions {
|
|
4659
|
+
const options: StudioHtmlPdfBlockOptions = { path: "", title: "", caption: "", page: "", height: "" };
|
|
4660
|
+
String(body ?? "").split(/\r?\n/).forEach((line) => {
|
|
4661
|
+
const raw = String(line ?? "").trim();
|
|
4662
|
+
if (!raw || raw.startsWith("#")) return;
|
|
4663
|
+
const match = raw.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*([\s\S]*)$/);
|
|
4664
|
+
if (match) {
|
|
4665
|
+
const key = String(match[1] ?? "").toLowerCase();
|
|
4666
|
+
const value = stripMatchingPathQuotes(String(match[2] ?? ""));
|
|
4667
|
+
if (key === "path" || key === "src" || key === "file") options.path = value;
|
|
4668
|
+
else if (key === "title") options.title = value;
|
|
4669
|
+
else if (key === "caption") options.caption = value;
|
|
4670
|
+
else if (key === "page") options.page = value;
|
|
4671
|
+
else if (key === "height") options.height = value;
|
|
4672
|
+
return;
|
|
4673
|
+
}
|
|
4674
|
+
if (!options.path) options.path = stripMatchingPathQuotes(raw);
|
|
4675
|
+
});
|
|
4676
|
+
return options;
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
function prepareStudioPdfBlocksForHtml(markdown: string): { markdown: string; blocks: StudioHtmlPdfBlock[] } {
|
|
4680
|
+
const blocks: StudioHtmlPdfBlock[] = [];
|
|
4681
|
+
const prefix = `PISTUDIOHTMLPDF${Date.now().toString(36)}${randomUUID().replace(/-/g, "")}TOKEN`;
|
|
4682
|
+
const source = String(markdown ?? "");
|
|
4683
|
+
const blockPattern = /(^|\n)([ \t]{0,3})(`{3,}|~{3,})[ \t]*studio-pdf[^\n]*\n([\s\S]*?)\n[ \t]*\3[ \t]*(?=\n|$)/g;
|
|
4684
|
+
const nextMarkdown = source.replace(blockPattern, (_match, leadingNewline: string, _indent: string, _fence: string, body: string) => {
|
|
4685
|
+
const placeholder = `${prefix}${blocks.length}`;
|
|
4686
|
+
blocks.push({ placeholder, options: parseStudioHtmlPdfBlockOptions(body) });
|
|
4687
|
+
return `${String(leadingNewline ?? "")}${placeholder}\n`;
|
|
4688
|
+
});
|
|
4689
|
+
return { markdown: nextMarkdown, blocks };
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
function prepareStudioAnnotationMarkersForHtml(markdown: string): { markdown: string; placeholders: StudioHtmlAnnotationPlaceholder[] } {
|
|
4693
|
+
const placeholders: StudioHtmlAnnotationPlaceholder[] = [];
|
|
4694
|
+
if (!hasStudioMarkdownAnnotationMarkers(markdown)) return { markdown: String(markdown ?? ""), placeholders };
|
|
4695
|
+
|
|
4696
|
+
const prefix = `PISTUDIOHTMLANNOT${Date.now().toString(36)}${randomUUID().replace(/-/g, "")}TOKEN`;
|
|
4697
|
+
const prepared = transformStudioMarkdownOutsideFences(markdown, (segment: string) => replaceStudioInlineAnnotationMarkers(segment, (marker: { body?: unknown }) => {
|
|
4698
|
+
const label = normalizeStudioAnnotationText(String(marker.body ?? ""));
|
|
4699
|
+
if (!label) return "";
|
|
4700
|
+
const token = `${prefix}${placeholders.length}`;
|
|
4701
|
+
placeholders.push({ token, text: label, title: `[an: ${label}]` });
|
|
4702
|
+
return token;
|
|
4703
|
+
}));
|
|
4704
|
+
return { markdown: prepared, placeholders };
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
function applyStudioAnnotationPlaceholdersToHtml(html: string, placeholders: StudioHtmlAnnotationPlaceholder[]): string {
|
|
4708
|
+
let transformed = String(html ?? "");
|
|
4709
|
+
for (const placeholder of placeholders) {
|
|
4710
|
+
const tokenPattern = new RegExp(escapeStudioRegExpLiteral(placeholder.token), "g");
|
|
4711
|
+
const markerHtml = `<span class="annotation-preview-marker" title="${escapeStudioHtmlText(placeholder.title)}">${escapeStudioHtmlText(placeholder.text)}</span>`;
|
|
4712
|
+
transformed = transformed.replace(tokenPattern, markerHtml);
|
|
4713
|
+
}
|
|
4714
|
+
return transformed;
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
function normalizeStudioHtmlPdfHeight(value: string): number {
|
|
4718
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
4719
|
+
if (!Number.isFinite(parsed)) return 680;
|
|
4720
|
+
return Math.max(240, Math.min(1400, parsed));
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
function normalizeStudioHtmlPdfPage(value: string): number {
|
|
4724
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
4725
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
function renderStudioHtmlPdfBlock(block: StudioHtmlPdfBlock, sourcePath: string | undefined, resourcePath: string | undefined): string {
|
|
4729
|
+
const options = block.options;
|
|
4730
|
+
const pdfPath = String(options.path || "").trim();
|
|
4731
|
+
const title = String(options.title || pdfPath || "Embedded PDF").trim();
|
|
4732
|
+
const caption = String(options.caption || "").trim();
|
|
4733
|
+
const height = normalizeStudioHtmlPdfHeight(options.height);
|
|
4734
|
+
const page = normalizeStudioHtmlPdfPage(options.page);
|
|
4735
|
+
|
|
4736
|
+
let iframeSrc = "";
|
|
4737
|
+
let linkHref = "";
|
|
4738
|
+
let error = "";
|
|
4739
|
+
if (!pdfPath) {
|
|
4740
|
+
error = "PDF block needs a local path.";
|
|
4741
|
+
} else {
|
|
4742
|
+
try {
|
|
4743
|
+
const baseDir = resourcePath || (sourcePath ? dirname(sourcePath) : process.cwd());
|
|
4744
|
+
const resolvedPath = resolveStudioPdfResourceFile(pdfPath, baseDir);
|
|
4745
|
+
const pdfBuffer = readFileSync(resolvedPath);
|
|
4746
|
+
iframeSrc = `data:application/pdf;base64,${pdfBuffer.toString("base64")}`;
|
|
4747
|
+
linkHref = pathToFileURL(resolvedPath).href;
|
|
4748
|
+
} catch (readError) {
|
|
4749
|
+
error = `PDF resource unavailable: ${readError instanceof Error ? readError.message : String(readError)}`;
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
const viewerSrc = iframeSrc && page ? `${iframeSrc}#page=${encodeURIComponent(String(page))}` : iframeSrc;
|
|
4754
|
+
const openLink = linkHref
|
|
4755
|
+
? `<a class="studio-pdf-card-link" href="${escapeStudioHtmlText(linkHref)}" target="_blank" rel="noopener noreferrer">Open PDF</a>`
|
|
4756
|
+
: "";
|
|
4757
|
+
const captionHtml = caption
|
|
4758
|
+
? `<div class="studio-pdf-card-caption">${escapeStudioHtmlText(caption)}</div>`
|
|
4759
|
+
: "";
|
|
4760
|
+
const bodyHtml = viewerSrc
|
|
4761
|
+
? `<iframe class="studio-pdf-frame" src="${escapeStudioHtmlText(viewerSrc)}" title="${escapeStudioHtmlText(title)}" loading="lazy" style="height: ${height}px"></iframe>`
|
|
4762
|
+
: `<div class="studio-pdf-card-error">${escapeStudioHtmlText(error || "PDF resource unavailable.")}</div>`;
|
|
4763
|
+
|
|
4764
|
+
return `<figure class="studio-pdf-card"><figcaption class="studio-pdf-card-header"><div class="studio-pdf-card-title">${escapeStudioHtmlText(title)}</div>${openLink}</figcaption>${captionHtml}${bodyHtml}</figure>`;
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
function renderStudioPdfBlocksInHtml(html: string, blocks: StudioHtmlPdfBlock[], sourcePath: string | undefined, resourcePath: string | undefined): string {
|
|
4768
|
+
let transformed = String(html ?? "");
|
|
4769
|
+
for (const block of blocks) {
|
|
4770
|
+
const replacement = renderStudioHtmlPdfBlock(block, sourcePath, resourcePath);
|
|
4771
|
+
const paragraphPattern = new RegExp(`<p>\\s*${escapeStudioRegExpLiteral(block.placeholder)}\\s*<\\/p>`, "g");
|
|
4772
|
+
transformed = transformed.replace(paragraphPattern, replacement);
|
|
4773
|
+
transformed = transformed.replace(new RegExp(escapeStudioRegExpLiteral(block.placeholder), "g"), replacement);
|
|
4774
|
+
}
|
|
4775
|
+
return transformed;
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
function parseStudioThemeVarsJson(json: string | undefined): Record<string, string> | null {
|
|
4779
|
+
const raw = String(json ?? "").trim();
|
|
4780
|
+
if (!raw) return null;
|
|
4781
|
+
try {
|
|
4782
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
4783
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
4784
|
+
const vars: Record<string, string> = {};
|
|
4785
|
+
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
4786
|
+
if ((key === "color-scheme" || key.startsWith("--")) && typeof value === "string") {
|
|
4787
|
+
vars[key] = value;
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
return Object.keys(vars).length > 0 ? vars : null;
|
|
4791
|
+
} catch {
|
|
4792
|
+
return null;
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
|
|
4796
|
+
function buildStudioCssVarsBlock(vars: Record<string, string>): string {
|
|
4797
|
+
return Object.entries(vars).map(([key, value]) => ` ${key}: ${value};`).join("\n");
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
function buildStudioHtmlExportBaseHref(resourcePath: string | undefined): string {
|
|
4801
|
+
const base = String(resourcePath ?? "").trim();
|
|
4802
|
+
if (!base) return "";
|
|
4803
|
+
try {
|
|
4804
|
+
return pathToFileURL(base.endsWith("/") ? base : `${base}/`).href;
|
|
4805
|
+
} catch {
|
|
4806
|
+
return "";
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
function buildStudioHtmlMermaidConfig(vars: Record<string, string>): Record<string, unknown> {
|
|
4811
|
+
return {
|
|
4812
|
+
startOnLoad: false,
|
|
4813
|
+
theme: "base",
|
|
4814
|
+
fontFamily: vars["--font-mono"] ?? "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
4815
|
+
flowchart: {
|
|
4816
|
+
curve: "basis",
|
|
4817
|
+
},
|
|
4818
|
+
themeVariables: {
|
|
4819
|
+
background: vars["--bg"] ?? "#ffffff",
|
|
4820
|
+
primaryColor: vars["--panel-2"] ?? "#f6f8fa",
|
|
4821
|
+
primaryTextColor: vars["--text"] ?? "#111827",
|
|
4822
|
+
primaryBorderColor: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4823
|
+
secondaryColor: vars["--panel"] ?? "#ffffff",
|
|
4824
|
+
secondaryTextColor: vars["--text"] ?? "#111827",
|
|
4825
|
+
secondaryBorderColor: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4826
|
+
tertiaryColor: vars["--panel"] ?? "#ffffff",
|
|
4827
|
+
tertiaryTextColor: vars["--text"] ?? "#111827",
|
|
4828
|
+
tertiaryBorderColor: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4829
|
+
lineColor: vars["--md-quote"] ?? vars["--text"] ?? "#111827",
|
|
4830
|
+
textColor: vars["--text"] ?? "#111827",
|
|
4831
|
+
edgeLabelBackground: vars["--panel-2"] ?? "#f6f8fa",
|
|
4832
|
+
nodeBorder: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4833
|
+
clusterBkg: vars["--panel"] ?? "#ffffff",
|
|
4834
|
+
clusterBorder: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4835
|
+
titleColor: vars["--md-heading"] ?? vars["--text"] ?? "#111827",
|
|
4836
|
+
},
|
|
4837
|
+
};
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
function buildStudioStandaloneHtmlMermaidScript(vars: Record<string, string>): string {
|
|
4841
|
+
const mermaidConfigJson = JSON.stringify(buildStudioHtmlMermaidConfig(vars)).replace(/</g, "\\u003c");
|
|
4842
|
+
return `<script>
|
|
4843
|
+
(() => {
|
|
4844
|
+
const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
4845
|
+
const MERMAID_CONFIG = ${mermaidConfigJson};
|
|
4846
|
+
|
|
4847
|
+
function appendMermaidWarning(message) {
|
|
4848
|
+
const documentEl = document.querySelector(".studio-export-document") || document.body;
|
|
4849
|
+
if (!documentEl || documentEl.querySelector(".preview-mermaid-warning")) return;
|
|
4850
|
+
const warningEl = document.createElement("div");
|
|
4851
|
+
warningEl.className = "preview-warning preview-mermaid-warning";
|
|
4852
|
+
warningEl.textContent = message || "Mermaid renderer unavailable. Showing mermaid blocks as code.";
|
|
4853
|
+
documentEl.appendChild(warningEl);
|
|
4854
|
+
}
|
|
4855
|
+
|
|
4856
|
+
function prepareMermaidBlocks() {
|
|
4857
|
+
const preBlocks = Array.from(document.querySelectorAll("pre.mermaid"));
|
|
4858
|
+
preBlocks.forEach((preEl) => {
|
|
4859
|
+
const source = preEl.querySelector("code") ? preEl.querySelector("code").textContent : preEl.textContent;
|
|
4860
|
+
const wrapper = document.createElement("div");
|
|
4861
|
+
wrapper.className = "mermaid-container";
|
|
4862
|
+
const diagramEl = document.createElement("div");
|
|
4863
|
+
diagramEl.className = "mermaid";
|
|
4864
|
+
diagramEl.textContent = source || "";
|
|
4865
|
+
wrapper.appendChild(diagramEl);
|
|
4866
|
+
preEl.replaceWith(wrapper);
|
|
4867
|
+
});
|
|
4868
|
+
return Array.from(document.querySelectorAll(".mermaid"));
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4871
|
+
async function renderMermaid() {
|
|
4872
|
+
const nodes = prepareMermaidBlocks();
|
|
4873
|
+
if (nodes.length === 0) return;
|
|
4874
|
+
try {
|
|
4875
|
+
const module = await import(MERMAID_CDN_URL);
|
|
4876
|
+
const mermaidApi = module && module.default ? module.default : null;
|
|
4877
|
+
if (!mermaidApi) throw new Error("Mermaid module did not expose a default export.");
|
|
4878
|
+
mermaidApi.initialize(MERMAID_CONFIG);
|
|
4879
|
+
await mermaidApi.run({ nodes });
|
|
4880
|
+
} catch (error) {
|
|
4881
|
+
console.error("Mermaid render failed:", error);
|
|
4882
|
+
appendMermaidWarning("Mermaid renderer unavailable. Showing mermaid source text.");
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4886
|
+
if (document.readyState === "loading") {
|
|
4887
|
+
document.addEventListener("DOMContentLoaded", () => { void renderMermaid(); }, { once: true });
|
|
4888
|
+
} else {
|
|
4889
|
+
void renderMermaid();
|
|
4890
|
+
}
|
|
4891
|
+
})();
|
|
4892
|
+
</script>`;
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
function buildStudioStandaloneHtmlDocument(contentHtml: string, resourcePath: string | undefined, options?: StudioHtmlRenderOptions): string {
|
|
4896
|
+
const title = String(options?.title || "pi Studio preview").trim() || "pi Studio preview";
|
|
4897
|
+
const vars = options?.themeVars ?? buildThemeCssVars(getStudioThemeStyle());
|
|
4898
|
+
const cssVarsBlock = buildStudioCssVarsBlock(vars);
|
|
4899
|
+
const stylesheet = readFileSync(STUDIO_CSS_URL, "utf-8");
|
|
4900
|
+
const mermaidScript = buildStudioStandaloneHtmlMermaidScript(vars);
|
|
4901
|
+
const baseHref = buildStudioHtmlExportBaseHref(resourcePath);
|
|
4902
|
+
const baseTag = baseHref ? ` <base href="${escapeStudioHtmlText(baseHref)}" />\n` : "";
|
|
4903
|
+
const generatedAt = new Date().toISOString();
|
|
4904
|
+
const sourceLabel = String(options?.sourceLabel || "").trim();
|
|
4905
|
+
const sourceMeta = sourceLabel ? ` data-source-label="${escapeStudioHtmlText(sourceLabel)}"` : "";
|
|
4906
|
+
const exportCss = `
|
|
4907
|
+
body.studio-html-export {
|
|
4908
|
+
display: block;
|
|
4909
|
+
min-height: 100%;
|
|
4910
|
+
padding: 0;
|
|
4911
|
+
background: var(--bg);
|
|
4912
|
+
color: var(--text);
|
|
4913
|
+
}
|
|
4914
|
+
body.studio-html-export .studio-export-shell {
|
|
4915
|
+
display: block;
|
|
4916
|
+
flex: none;
|
|
4917
|
+
width: 100%;
|
|
4918
|
+
max-width: 1180px;
|
|
4919
|
+
min-height: auto;
|
|
4920
|
+
margin: 0 auto;
|
|
4921
|
+
padding: 32px clamp(16px, 4vw, 48px) 56px;
|
|
4922
|
+
}
|
|
4923
|
+
body.studio-html-export .studio-export-document {
|
|
4924
|
+
display: block;
|
|
4925
|
+
width: 100%;
|
|
4926
|
+
overflow: visible;
|
|
4927
|
+
padding: 28px;
|
|
4928
|
+
border: 1px solid var(--panel-border);
|
|
4929
|
+
border-radius: 14px;
|
|
4930
|
+
background: var(--panel);
|
|
4931
|
+
box-shadow: var(--panel-shadow);
|
|
4932
|
+
}
|
|
4933
|
+
body.studio-html-export .studio-export-document > :first-child {
|
|
4934
|
+
margin-top: 0;
|
|
4935
|
+
}
|
|
4936
|
+
body.studio-html-export .studio-export-document > :last-child {
|
|
4937
|
+
margin-bottom: 0;
|
|
4938
|
+
}
|
|
4939
|
+
body.studio-html-export .preview-selection-actions,
|
|
4940
|
+
body.studio-html-export .studio-copy-block-btn {
|
|
4941
|
+
display: none !important;
|
|
4942
|
+
}
|
|
4943
|
+
@media print {
|
|
4944
|
+
body.studio-html-export {
|
|
4945
|
+
background: #fff;
|
|
4946
|
+
color: #111;
|
|
4947
|
+
}
|
|
4948
|
+
body.studio-html-export .studio-export-shell {
|
|
4949
|
+
max-width: none;
|
|
4950
|
+
padding: 0;
|
|
4951
|
+
}
|
|
4952
|
+
body.studio-html-export .studio-export-document {
|
|
4953
|
+
border: 0;
|
|
4954
|
+
border-radius: 0;
|
|
4955
|
+
box-shadow: none;
|
|
4956
|
+
padding: 0;
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
`;
|
|
4960
|
+
|
|
4961
|
+
return `<!doctype html>
|
|
4962
|
+
<html>
|
|
4963
|
+
<head>
|
|
4964
|
+
<meta charset="utf-8" />
|
|
4965
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
4966
|
+
<meta name="generator" content="pi Studio" />
|
|
4967
|
+
<meta name="pi-studio-exported-at" content="${escapeStudioHtmlText(generatedAt)}" />
|
|
4968
|
+
${baseTag} <title>${escapeStudioHtmlText(title)}</title>
|
|
4969
|
+
<style>
|
|
4970
|
+
:root {
|
|
4971
|
+
${cssVarsBlock}
|
|
4972
|
+
}
|
|
4973
|
+
${stylesheet}
|
|
4974
|
+
${exportCss}
|
|
4975
|
+
</style>
|
|
4976
|
+
</head>
|
|
4977
|
+
<body class="studio-html-export"${sourceMeta}>
|
|
4978
|
+
<main class="studio-export-shell">
|
|
4979
|
+
<article class="panel-scroll rendered-markdown studio-export-document">
|
|
4980
|
+
${contentHtml}
|
|
4981
|
+
</article>
|
|
4982
|
+
</main>
|
|
4983
|
+
${mermaidScript}
|
|
4984
|
+
</body>
|
|
4985
|
+
</html>`;
|
|
4986
|
+
}
|
|
4987
|
+
|
|
4988
|
+
async function renderStudioStandaloneHtmlWithPandoc(
|
|
4989
|
+
markdown: string,
|
|
4990
|
+
isLatex?: boolean,
|
|
4991
|
+
resourcePath?: string,
|
|
4992
|
+
editorLanguage?: string,
|
|
4993
|
+
sourcePath?: string,
|
|
4994
|
+
options?: StudioHtmlRenderOptions,
|
|
4995
|
+
): Promise<{ html: Buffer; warning?: string }> {
|
|
4996
|
+
const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorLanguage);
|
|
4997
|
+
if (!isLatex && isLikelyStandaloneStudioHtml(markdown, effectiveEditorLanguage)) {
|
|
4998
|
+
return { html: Buffer.from(String(markdown ?? ""), "utf-8") };
|
|
4999
|
+
}
|
|
5000
|
+
const source = !isLatex
|
|
5001
|
+
&& effectiveEditorLanguage
|
|
5002
|
+
&& effectiveEditorLanguage !== "markdown"
|
|
5003
|
+
&& effectiveEditorLanguage !== "latex"
|
|
5004
|
+
&& !isStudioSingleFencedCodeBlock(markdown)
|
|
5005
|
+
? wrapStudioCodeAsMarkdown(markdown, effectiveEditorLanguage)
|
|
5006
|
+
: markdown;
|
|
5007
|
+
const annotationPrepared = prepareStudioAnnotationMarkersForHtml(source);
|
|
5008
|
+
const pdfPrepared = prepareStudioPdfBlocksForHtml(annotationPrepared.markdown);
|
|
5009
|
+
let renderedHtml = await renderStudioMarkdownWithPandoc(pdfPrepared.markdown, isLatex, resourcePath, sourcePath);
|
|
5010
|
+
renderedHtml = renderStudioPdfBlocksInHtml(renderedHtml, pdfPrepared.blocks, sourcePath, resourcePath);
|
|
5011
|
+
renderedHtml = applyStudioAnnotationPlaceholdersToHtml(renderedHtml, annotationPrepared.placeholders);
|
|
5012
|
+
const standaloneHtml = buildStudioStandaloneHtmlDocument(renderedHtml, resourcePath, options);
|
|
5013
|
+
return { html: Buffer.from(standaloneHtml, "utf-8") };
|
|
5014
|
+
}
|
|
5015
|
+
|
|
4492
5016
|
async function renderStudioLiteralTextPdf(text: string, title = "Studio export", options?: StudioPdfRenderOptions): Promise<Buffer> {
|
|
4493
5017
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
4494
5018
|
const tempDir = join(tmpdir(), `pi-studio-text-pdf-${Date.now()}-${randomUUID()}`);
|
|
@@ -5667,8 +6191,23 @@ function parseEntryTimestamp(timestamp: unknown): number {
|
|
|
5667
6191
|
return Date.now();
|
|
5668
6192
|
}
|
|
5669
6193
|
|
|
6194
|
+
function getStudioResponseHistoryContentHash(markdown: string): string {
|
|
6195
|
+
return createHash("sha256").update(String(markdown ?? ""), "utf-8").digest("hex").slice(0, 16);
|
|
6196
|
+
}
|
|
6197
|
+
|
|
6198
|
+
function buildStudioResponseHistoryId(contentHash: string, occurrenceIndex: number): string {
|
|
6199
|
+
return `response-${contentHash}-${String(Math.max(0, occurrenceIndex) + 1).padStart(3, "0")}`;
|
|
6200
|
+
}
|
|
6201
|
+
|
|
6202
|
+
function buildNextStudioResponseHistoryId(markdown: string, existingItems: StudioResponseHistoryItem[]): string {
|
|
6203
|
+
const contentHash = getStudioResponseHistoryContentHash(markdown);
|
|
6204
|
+
const occurrenceIndex = existingItems.filter((item) => getStudioResponseHistoryContentHash(item.markdown) === contentHash).length;
|
|
6205
|
+
return buildStudioResponseHistoryId(contentHash, occurrenceIndex);
|
|
6206
|
+
}
|
|
6207
|
+
|
|
5670
6208
|
function buildResponseHistoryFromEntries(entries: SessionEntry[], limit = RESPONSE_HISTORY_LIMIT): StudioResponseHistoryItem[] {
|
|
5671
6209
|
const history: StudioResponseHistoryItem[] = [];
|
|
6210
|
+
const occurrenceCountsByHash = new Map<string, number>();
|
|
5672
6211
|
let lastUserPrompt: string | null = null;
|
|
5673
6212
|
let pendingPromptDescriptor: StudioPromptDescriptor | null = null;
|
|
5674
6213
|
|
|
@@ -5700,8 +6239,11 @@ function buildResponseHistoryFromEntries(entries: SessionEntry[], limit = RESPON
|
|
|
5700
6239
|
if (!markdown) continue;
|
|
5701
6240
|
const thinking = extractAssistantThinking(message);
|
|
5702
6241
|
const promptDescriptor = pendingPromptDescriptor ?? buildStudioPromptDescriptor(lastUserPrompt);
|
|
6242
|
+
const contentHash = getStudioResponseHistoryContentHash(markdown);
|
|
6243
|
+
const occurrenceIndex = occurrenceCountsByHash.get(contentHash) ?? 0;
|
|
6244
|
+
occurrenceCountsByHash.set(contentHash, occurrenceIndex + 1);
|
|
5703
6245
|
history.push({
|
|
5704
|
-
id:
|
|
6246
|
+
id: buildStudioResponseHistoryId(contentHash, occurrenceIndex),
|
|
5705
6247
|
markdown,
|
|
5706
6248
|
thinking,
|
|
5707
6249
|
timestamp: parseEntryTimestamp((entry as { timestamp?: unknown }).timestamp),
|
|
@@ -5769,6 +6311,12 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
5769
6311
|
if (msg.type === "hello") return { type: "hello" };
|
|
5770
6312
|
if (msg.type === "ping") return { type: "ping" };
|
|
5771
6313
|
if (msg.type === "get_latest_response") return { type: "get_latest_response" };
|
|
6314
|
+
if (msg.type === "get_trace_snapshot" && typeof msg.responseHistoryId === "string") {
|
|
6315
|
+
return {
|
|
6316
|
+
type: "get_trace_snapshot",
|
|
6317
|
+
responseHistoryId: msg.responseHistoryId,
|
|
6318
|
+
};
|
|
6319
|
+
}
|
|
5772
6320
|
|
|
5773
6321
|
if (
|
|
5774
6322
|
msg.type === "critique_request" &&
|
|
@@ -6040,6 +6588,68 @@ function createEmptyStudioTraceState(): StudioTraceState {
|
|
|
6040
6588
|
};
|
|
6041
6589
|
}
|
|
6042
6590
|
|
|
6591
|
+
function truncateStudioTraceSnapshotText(text: string, maxChars = STUDIO_TRACE_SNAPSHOT_MAX_FIELD_CHARS): { text: string; truncated: boolean } {
|
|
6592
|
+
const value = String(text ?? "");
|
|
6593
|
+
if (value.length <= maxChars) return { text: value, truncated: false };
|
|
6594
|
+
const keepHead = Math.max(0, Math.floor(maxChars * 0.62));
|
|
6595
|
+
const keepTail = Math.max(0, maxChars - keepHead);
|
|
6596
|
+
const omitted = value.length - keepHead - keepTail;
|
|
6597
|
+
return {
|
|
6598
|
+
text: `${value.slice(0, keepHead)}\n\n… ${omitted} chars omitted from saved Working view …\n\n${value.slice(value.length - keepTail)}`,
|
|
6599
|
+
truncated: true,
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
function createStudioTraceSnapshot(source: StudioTraceState): { traceState: StudioTraceState; truncated: boolean } {
|
|
6604
|
+
let truncated = false;
|
|
6605
|
+
const sourceEntries = Array.isArray(source.entries) ? source.entries : [];
|
|
6606
|
+
const entries = sourceEntries.slice(-STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES).map((entry) => {
|
|
6607
|
+
if (entry.type === "assistant") {
|
|
6608
|
+
const thinking = truncateStudioTraceSnapshotText(entry.thinking);
|
|
6609
|
+
const text = truncateStudioTraceSnapshotText(entry.text);
|
|
6610
|
+
truncated = truncated || thinking.truncated || text.truncated;
|
|
6611
|
+
return {
|
|
6612
|
+
...entry,
|
|
6613
|
+
thinking: thinking.text,
|
|
6614
|
+
text: text.text,
|
|
6615
|
+
};
|
|
6616
|
+
}
|
|
6617
|
+
const argsSummary = truncateStudioTraceSnapshotText(entry.argsSummary ?? "");
|
|
6618
|
+
const output = truncateStudioTraceSnapshotText(entry.output);
|
|
6619
|
+
truncated = truncated || argsSummary.truncated || output.truncated;
|
|
6620
|
+
return {
|
|
6621
|
+
...entry,
|
|
6622
|
+
argsSummary: argsSummary.text || null,
|
|
6623
|
+
output: output.text,
|
|
6624
|
+
};
|
|
6625
|
+
});
|
|
6626
|
+
if (sourceEntries.length > entries.length) truncated = true;
|
|
6627
|
+
|
|
6628
|
+
return {
|
|
6629
|
+
traceState: {
|
|
6630
|
+
runId: source.runId,
|
|
6631
|
+
requestId: source.requestId,
|
|
6632
|
+
requestKind: source.requestKind,
|
|
6633
|
+
status: source.status,
|
|
6634
|
+
startedAt: source.startedAt,
|
|
6635
|
+
updatedAt: source.updatedAt,
|
|
6636
|
+
entries,
|
|
6637
|
+
},
|
|
6638
|
+
truncated,
|
|
6639
|
+
};
|
|
6640
|
+
}
|
|
6641
|
+
|
|
6642
|
+
function summarizeStudioTraceSnapshot(traceState: StudioTraceState, truncated = false): StudioTraceSnapshotSummary {
|
|
6643
|
+
return {
|
|
6644
|
+
hasTrace: Array.isArray(traceState.entries) && traceState.entries.length > 0,
|
|
6645
|
+
entryCount: Array.isArray(traceState.entries) ? traceState.entries.length : 0,
|
|
6646
|
+
startedAt: traceState.startedAt,
|
|
6647
|
+
updatedAt: traceState.updatedAt,
|
|
6648
|
+
status: traceState.status,
|
|
6649
|
+
truncated,
|
|
6650
|
+
};
|
|
6651
|
+
}
|
|
6652
|
+
|
|
6043
6653
|
function sanitizeStudioTraceOutputText(text: string): string {
|
|
6044
6654
|
return String(text || "")
|
|
6045
6655
|
.replace(/data:image\/([a-zA-Z0-9.+-]+);base64,[A-Za-z0-9+/=\r\n]+/g, (_match, subtype: string) => `[Image: image/${subtype || "unknown"} data omitted]`)
|
|
@@ -6314,6 +6924,23 @@ function sanitizePdfFilename(input: string | undefined): string {
|
|
|
6314
6924
|
return `${ensuredExt.slice(0, 156)}.pdf`;
|
|
6315
6925
|
}
|
|
6316
6926
|
|
|
6927
|
+
function sanitizeHtmlFilename(input: string | undefined): string {
|
|
6928
|
+
const fallback = "studio-preview.html";
|
|
6929
|
+
const raw = String(input ?? "").trim();
|
|
6930
|
+
if (!raw) return fallback;
|
|
6931
|
+
|
|
6932
|
+
const noPath = raw.split(/[\\/]/).pop() ?? raw;
|
|
6933
|
+
const cleaned = noPath
|
|
6934
|
+
.replace(/[\x00-\x1f\x7f]+/g, "")
|
|
6935
|
+
.replace(/[<>:"|?*]+/g, "-")
|
|
6936
|
+
.trim();
|
|
6937
|
+
if (!cleaned) return fallback;
|
|
6938
|
+
|
|
6939
|
+
const ensuredExt = /\.html?$/i.test(cleaned) ? cleaned : `${cleaned}.html`;
|
|
6940
|
+
if (ensuredExt.length <= 160) return ensuredExt;
|
|
6941
|
+
return `${ensuredExt.slice(0, 155)}.html`;
|
|
6942
|
+
}
|
|
6943
|
+
|
|
6317
6944
|
function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
|
|
6318
6945
|
const shadowColor = style.mode === "light"
|
|
6319
6946
|
? withAlpha(style.palette.text, 0.10, "rgba(15, 23, 42, 0.08)")
|
|
@@ -6753,7 +7380,13 @@ ${cssVarsBlock}
|
|
|
6753
7380
|
</div>
|
|
6754
7381
|
<div class="section-header-actions">
|
|
6755
7382
|
<button id="rightFocusBtn" class="pane-focus-btn" type="button" title="Show only the response pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
|
|
6756
|
-
<
|
|
7383
|
+
<span id="exportPreviewControls" class="export-preview-controls">
|
|
7384
|
+
<button id="exportPdfBtn" class="export-preview-trigger" type="button" aria-haspopup="menu" aria-expanded="false" title="Choose a format and export the current right-pane preview.">Export right preview</button>
|
|
7385
|
+
<div id="exportPreviewMenu" class="export-preview-menu" role="menu" hidden>
|
|
7386
|
+
<button id="exportPreviewPdfBtn" type="button" role="menuitem" data-export-preview-format="pdf">Export as PDF</button>
|
|
7387
|
+
<button id="exportPreviewHtmlBtn" type="button" role="menuitem" data-export-preview-format="html">Export as HTML</button>
|
|
7388
|
+
</div>
|
|
7389
|
+
</span>
|
|
6757
7390
|
</div>
|
|
6758
7391
|
</div>
|
|
6759
7392
|
<div class="reference-meta">
|
|
@@ -6852,6 +7485,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6852
7485
|
let pendingStudioPromptMetadata: StudioPromptDescriptor | null = null;
|
|
6853
7486
|
let lastStudioResponse: LastStudioResponse | null = null;
|
|
6854
7487
|
let preparedPdfExports = new Map<string, PreparedStudioPdfExport>();
|
|
7488
|
+
let preparedHtmlExports = new Map<string, PreparedStudioHtmlExport>();
|
|
6855
7489
|
let initialStudioDocument: InitialStudioDocument | null = null;
|
|
6856
7490
|
let studioCwd = process.cwd();
|
|
6857
7491
|
let lastCommandCtx: ExtensionCommandContext | null = null;
|
|
@@ -6871,6 +7505,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6871
7505
|
let latestSessionUserPrompt: string | null = null;
|
|
6872
7506
|
let pendingTurnPrompt: string | null = null;
|
|
6873
7507
|
let studioTraceState: StudioTraceState = createEmptyStudioTraceState();
|
|
7508
|
+
let studioTraceHistory = new Map<string, { traceState: StudioTraceState; summary: StudioTraceSnapshotSummary }>();
|
|
6874
7509
|
let activeStudioTraceAssistantEntryId: string | null = null;
|
|
6875
7510
|
const studioTraceToolEntryIds = new Map<string, string>();
|
|
6876
7511
|
let contextUsageSnapshot: StudioContextUsageSnapshot = {
|
|
@@ -7222,6 +7857,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
7222
7857
|
notifyStudioTerminal(message, "info");
|
|
7223
7858
|
};
|
|
7224
7859
|
|
|
7860
|
+
const attachStudioTraceSummariesToHistory = (items: StudioResponseHistoryItem[]): StudioResponseHistoryItem[] => items.map((item) => {
|
|
7861
|
+
const stored = studioTraceHistory.get(item.id);
|
|
7862
|
+
return stored ? { ...item, traceSummary: stored.summary } : item;
|
|
7863
|
+
});
|
|
7864
|
+
|
|
7865
|
+
const pruneStudioTraceHistory = () => {
|
|
7866
|
+
const liveIds = new Set(studioResponseHistory.map((item) => item.id));
|
|
7867
|
+
for (const key of Array.from(studioTraceHistory.keys())) {
|
|
7868
|
+
if (!liveIds.has(key)) studioTraceHistory.delete(key);
|
|
7869
|
+
}
|
|
7870
|
+
while (studioTraceHistory.size > MAX_STUDIO_TRACE_SNAPSHOTS) {
|
|
7871
|
+
const oldestKey = studioTraceHistory.keys().next().value;
|
|
7872
|
+
if (!oldestKey) break;
|
|
7873
|
+
studioTraceHistory.delete(oldestKey);
|
|
7874
|
+
}
|
|
7875
|
+
};
|
|
7876
|
+
|
|
7877
|
+
const storeStudioTraceSnapshotForResponse = (responseHistoryId: string | null | undefined): StudioTraceSnapshotSummary | null => {
|
|
7878
|
+
const id = String(responseHistoryId ?? "").trim();
|
|
7879
|
+
if (!id) return null;
|
|
7880
|
+
if (!Array.isArray(studioTraceState.entries) || studioTraceState.entries.length === 0) return null;
|
|
7881
|
+
const snapshot = createStudioTraceSnapshot(studioTraceState);
|
|
7882
|
+
const summary = summarizeStudioTraceSnapshot(snapshot.traceState, snapshot.truncated);
|
|
7883
|
+
if (!summary.hasTrace) return null;
|
|
7884
|
+
studioTraceHistory.set(id, { traceState: snapshot.traceState, summary });
|
|
7885
|
+
studioResponseHistory = studioResponseHistory.map((item) => item.id === id ? { ...item, traceSummary: summary } : item);
|
|
7886
|
+
pruneStudioTraceHistory();
|
|
7887
|
+
return summary;
|
|
7888
|
+
};
|
|
7889
|
+
|
|
7225
7890
|
const refreshContextUsage = (
|
|
7226
7891
|
ctx?: { getContextUsage(): { tokens: number | null; contextWindow: number; percent: number | null } | undefined },
|
|
7227
7892
|
): StudioContextUsageSnapshot => {
|
|
@@ -7239,7 +7904,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7239
7904
|
|
|
7240
7905
|
const syncStudioResponseHistory = (entries: SessionEntry[]) => {
|
|
7241
7906
|
latestSessionUserPrompt = findLatestUserPrompt(entries);
|
|
7242
|
-
studioResponseHistory = buildResponseHistoryFromEntries(entries, RESPONSE_HISTORY_LIMIT);
|
|
7907
|
+
studioResponseHistory = attachStudioTraceSummariesToHistory(buildResponseHistoryFromEntries(entries, RESPONSE_HISTORY_LIMIT));
|
|
7908
|
+
pruneStudioTraceHistory();
|
|
7243
7909
|
const latest = studioResponseHistory[studioResponseHistory.length - 1];
|
|
7244
7910
|
if (!latest) {
|
|
7245
7911
|
lastStudioResponse = null;
|
|
@@ -7865,6 +8531,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7865
8531
|
return;
|
|
7866
8532
|
}
|
|
7867
8533
|
|
|
8534
|
+
if (msg.type === "get_trace_snapshot") {
|
|
8535
|
+
const responseHistoryId = String(msg.responseHistoryId ?? "").trim();
|
|
8536
|
+
const stored = responseHistoryId ? studioTraceHistory.get(responseHistoryId) : null;
|
|
8537
|
+
sendToClient(client, {
|
|
8538
|
+
type: "trace_snapshot",
|
|
8539
|
+
responseHistoryId,
|
|
8540
|
+
traceState: stored?.traceState ?? createEmptyStudioTraceState(),
|
|
8541
|
+
summary: stored?.summary ?? summarizeStudioTraceSnapshot(createEmptyStudioTraceState()),
|
|
8542
|
+
});
|
|
8543
|
+
return;
|
|
8544
|
+
}
|
|
8545
|
+
|
|
7868
8546
|
if (msg.type === "load_git_diff_request") {
|
|
7869
8547
|
if (!isValidRequestId(msg.requestId)) {
|
|
7870
8548
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
@@ -8467,6 +9145,100 @@ export default function (pi: ExtensionAPI) {
|
|
|
8467
9145
|
res.end(prepared.pdf);
|
|
8468
9146
|
};
|
|
8469
9147
|
|
|
9148
|
+
const disposePreparedHtmlExport = (entry: PreparedStudioHtmlExport | null | undefined) => {
|
|
9149
|
+
if (!entry?.tempDirPath) return;
|
|
9150
|
+
void rm(entry.tempDirPath, { recursive: true, force: true }).catch(() => undefined);
|
|
9151
|
+
};
|
|
9152
|
+
|
|
9153
|
+
const clearPreparedHtmlExports = () => {
|
|
9154
|
+
for (const entry of preparedHtmlExports.values()) {
|
|
9155
|
+
disposePreparedHtmlExport(entry);
|
|
9156
|
+
}
|
|
9157
|
+
preparedHtmlExports.clear();
|
|
9158
|
+
};
|
|
9159
|
+
|
|
9160
|
+
const prunePreparedHtmlExports = () => {
|
|
9161
|
+
const now = Date.now();
|
|
9162
|
+
for (const [id, entry] of preparedHtmlExports) {
|
|
9163
|
+
if (entry.createdAt + PREPARED_HTML_EXPORT_TTL_MS <= now) {
|
|
9164
|
+
preparedHtmlExports.delete(id);
|
|
9165
|
+
disposePreparedHtmlExport(entry);
|
|
9166
|
+
}
|
|
9167
|
+
}
|
|
9168
|
+
while (preparedHtmlExports.size > MAX_PREPARED_HTML_EXPORTS) {
|
|
9169
|
+
const oldestKey = preparedHtmlExports.keys().next().value;
|
|
9170
|
+
if (!oldestKey) break;
|
|
9171
|
+
const oldestEntry = preparedHtmlExports.get(oldestKey);
|
|
9172
|
+
preparedHtmlExports.delete(oldestKey);
|
|
9173
|
+
disposePreparedHtmlExport(oldestEntry);
|
|
9174
|
+
}
|
|
9175
|
+
};
|
|
9176
|
+
|
|
9177
|
+
const storePreparedHtmlExport = (html: Buffer, filename: string, warning?: string): string => {
|
|
9178
|
+
prunePreparedHtmlExports();
|
|
9179
|
+
const exportId = randomUUID();
|
|
9180
|
+
preparedHtmlExports.set(exportId, {
|
|
9181
|
+
html,
|
|
9182
|
+
filename,
|
|
9183
|
+
warning,
|
|
9184
|
+
createdAt: Date.now(),
|
|
9185
|
+
});
|
|
9186
|
+
return exportId;
|
|
9187
|
+
};
|
|
9188
|
+
|
|
9189
|
+
const ensurePreparedHtmlExportFile = async (exportId: string): Promise<PreparedStudioHtmlExport | null> => {
|
|
9190
|
+
prunePreparedHtmlExports();
|
|
9191
|
+
const entry = preparedHtmlExports.get(exportId);
|
|
9192
|
+
if (!entry) return null;
|
|
9193
|
+
if (entry.filePath && entry.tempDirPath) return entry;
|
|
9194
|
+
|
|
9195
|
+
const tempDirPath = join(tmpdir(), `pi-studio-prepared-html-${Date.now()}-${randomUUID()}`);
|
|
9196
|
+
const filePath = join(tempDirPath, sanitizeHtmlFilename(entry.filename));
|
|
9197
|
+
await mkdir(tempDirPath, { recursive: true });
|
|
9198
|
+
await writeFile(filePath, entry.html);
|
|
9199
|
+
entry.tempDirPath = tempDirPath;
|
|
9200
|
+
entry.filePath = filePath;
|
|
9201
|
+
preparedHtmlExports.set(exportId, entry);
|
|
9202
|
+
return entry;
|
|
9203
|
+
};
|
|
9204
|
+
|
|
9205
|
+
const getPreparedHtmlExport = (exportId: string): PreparedStudioHtmlExport | null => {
|
|
9206
|
+
prunePreparedHtmlExports();
|
|
9207
|
+
return preparedHtmlExports.get(exportId) ?? null;
|
|
9208
|
+
};
|
|
9209
|
+
|
|
9210
|
+
const handlePreparedHtmlDownloadRequest = (requestUrl: URL, res: ServerResponse) => {
|
|
9211
|
+
const exportId = requestUrl.searchParams.get("id") ?? "";
|
|
9212
|
+
if (!exportId) {
|
|
9213
|
+
respondText(res, 400, "Missing HTML export id.");
|
|
9214
|
+
return;
|
|
9215
|
+
}
|
|
9216
|
+
|
|
9217
|
+
const prepared = getPreparedHtmlExport(exportId);
|
|
9218
|
+
if (!prepared) {
|
|
9219
|
+
respondText(res, 404, "HTML export is no longer available. Re-export the document.");
|
|
9220
|
+
return;
|
|
9221
|
+
}
|
|
9222
|
+
|
|
9223
|
+
const safeAsciiName = prepared.filename
|
|
9224
|
+
.replace(/[\x00-\x1f\x7f]/g, "")
|
|
9225
|
+
.replace(/[;"\\]/g, "_")
|
|
9226
|
+
.replace(/\s+/g, " ")
|
|
9227
|
+
.trim() || "studio-preview.html";
|
|
9228
|
+
|
|
9229
|
+
const headers: Record<string, string> = {
|
|
9230
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9231
|
+
"Cache-Control": "no-store",
|
|
9232
|
+
"X-Content-Type-Options": "nosniff",
|
|
9233
|
+
"Content-Disposition": `inline; filename="${safeAsciiName}"; filename*=UTF-8''${encodeURIComponent(prepared.filename)}`,
|
|
9234
|
+
"Content-Length": String(prepared.html.length),
|
|
9235
|
+
};
|
|
9236
|
+
if (prepared.warning) headers["X-Pi-Studio-Export-Warning"] = prepared.warning;
|
|
9237
|
+
|
|
9238
|
+
res.writeHead(200, headers);
|
|
9239
|
+
res.end(prepared.html);
|
|
9240
|
+
};
|
|
9241
|
+
|
|
8470
9242
|
const handleScratchpadStateRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
8471
9243
|
const method = (req.method ?? "GET").toUpperCase();
|
|
8472
9244
|
if (method === "GET") {
|
|
@@ -8784,6 +9556,117 @@ export default function (pi: ExtensionAPI) {
|
|
|
8784
9556
|
}
|
|
8785
9557
|
};
|
|
8786
9558
|
|
|
9559
|
+
const handleExportHtmlRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
9560
|
+
let rawBody = "";
|
|
9561
|
+
try {
|
|
9562
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
9563
|
+
} catch (error) {
|
|
9564
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9565
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
9566
|
+
respondJson(res, status, { ok: false, error: message });
|
|
9567
|
+
return;
|
|
9568
|
+
}
|
|
9569
|
+
|
|
9570
|
+
let parsedBody: unknown;
|
|
9571
|
+
try {
|
|
9572
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
9573
|
+
} catch {
|
|
9574
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
9575
|
+
return;
|
|
9576
|
+
}
|
|
9577
|
+
|
|
9578
|
+
const markdown =
|
|
9579
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { markdown?: unknown }).markdown === "string"
|
|
9580
|
+
? (parsedBody as { markdown: string }).markdown
|
|
9581
|
+
: null;
|
|
9582
|
+
if (markdown === null) {
|
|
9583
|
+
respondJson(res, 400, { ok: false, error: "Missing markdown string in request body." });
|
|
9584
|
+
return;
|
|
9585
|
+
}
|
|
9586
|
+
|
|
9587
|
+
if (markdown.length > HTML_EXPORT_MAX_CHARS) {
|
|
9588
|
+
respondJson(res, 413, {
|
|
9589
|
+
ok: false,
|
|
9590
|
+
error: `HTML export text exceeds ${HTML_EXPORT_MAX_CHARS} characters.`,
|
|
9591
|
+
});
|
|
9592
|
+
return;
|
|
9593
|
+
}
|
|
9594
|
+
|
|
9595
|
+
const sourcePath =
|
|
9596
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { sourcePath?: unknown }).sourcePath === "string"
|
|
9597
|
+
? (parsedBody as { sourcePath: string }).sourcePath
|
|
9598
|
+
: "";
|
|
9599
|
+
const userResourceDir =
|
|
9600
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
|
|
9601
|
+
? (parsedBody as { resourceDir: string }).resourceDir
|
|
9602
|
+
: "";
|
|
9603
|
+
const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
|
|
9604
|
+
const requestedIsLatex =
|
|
9605
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { isLatex?: unknown }).isLatex === "boolean"
|
|
9606
|
+
? (parsedBody as { isLatex: boolean }).isLatex
|
|
9607
|
+
: null;
|
|
9608
|
+
const requestedFilename =
|
|
9609
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { filenameHint?: unknown }).filenameHint === "string"
|
|
9610
|
+
? (parsedBody as { filenameHint: string }).filenameHint
|
|
9611
|
+
: "";
|
|
9612
|
+
const requestedTitle =
|
|
9613
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { title?: unknown }).title === "string"
|
|
9614
|
+
? (parsedBody as { title: string }).title
|
|
9615
|
+
: "";
|
|
9616
|
+
const requestedEditorHtmlLanguage =
|
|
9617
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorHtmlLanguage?: unknown }).editorHtmlLanguage === "string"
|
|
9618
|
+
? (parsedBody as { editorHtmlLanguage: string }).editorHtmlLanguage
|
|
9619
|
+
: "";
|
|
9620
|
+
const editorHtmlLanguage = inferStudioPdfLanguage(markdown, requestedEditorHtmlLanguage);
|
|
9621
|
+
const isLatex = editorHtmlLanguage === "latex"
|
|
9622
|
+
|| (
|
|
9623
|
+
(editorHtmlLanguage === undefined || editorHtmlLanguage === "markdown")
|
|
9624
|
+
&& (requestedIsLatex ?? isLikelyStandaloneLatexPreview(markdown))
|
|
9625
|
+
);
|
|
9626
|
+
const filename = sanitizeHtmlFilename(requestedFilename || (isLatex ? "studio-latex-preview.html" : "studio-preview.html"));
|
|
9627
|
+
const themeVars = parseStudioThemeVarsJson(lastThemeVarsJson) ?? buildThemeCssVars(getStudioThemeStyle(lastCommandCtx?.ui?.theme));
|
|
9628
|
+
|
|
9629
|
+
try {
|
|
9630
|
+
const { html, warning } = await renderStudioStandaloneHtmlWithPandoc(
|
|
9631
|
+
markdown,
|
|
9632
|
+
isLatex,
|
|
9633
|
+
resourcePath,
|
|
9634
|
+
editorHtmlLanguage,
|
|
9635
|
+
sourcePath || undefined,
|
|
9636
|
+
{
|
|
9637
|
+
title: requestedTitle || filename,
|
|
9638
|
+
sourceLabel: sourcePath || userResourceDir || "right preview",
|
|
9639
|
+
themeVars,
|
|
9640
|
+
},
|
|
9641
|
+
);
|
|
9642
|
+
const exportId = storePreparedHtmlExport(html, filename, warning);
|
|
9643
|
+
const token = serverState?.token ?? "";
|
|
9644
|
+
let openedExternal = false;
|
|
9645
|
+
let openError: string | null = null;
|
|
9646
|
+
try {
|
|
9647
|
+
const prepared = await ensurePreparedHtmlExportFile(exportId);
|
|
9648
|
+
if (!prepared?.filePath) {
|
|
9649
|
+
throw new Error("Prepared HTML file was not available for external open.");
|
|
9650
|
+
}
|
|
9651
|
+
await openPathInDefaultViewer(prepared.filePath);
|
|
9652
|
+
openedExternal = true;
|
|
9653
|
+
} catch (viewerError) {
|
|
9654
|
+
openError = viewerError instanceof Error ? viewerError.message : String(viewerError);
|
|
9655
|
+
}
|
|
9656
|
+
respondJson(res, 200, {
|
|
9657
|
+
ok: true,
|
|
9658
|
+
filename,
|
|
9659
|
+
warning: warning ?? null,
|
|
9660
|
+
openedExternal,
|
|
9661
|
+
openError,
|
|
9662
|
+
downloadUrl: `/export-html?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
9663
|
+
});
|
|
9664
|
+
} catch (error) {
|
|
9665
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9666
|
+
respondJson(res, 500, { ok: false, error: `HTML export failed: ${message}` });
|
|
9667
|
+
}
|
|
9668
|
+
};
|
|
9669
|
+
|
|
8787
9670
|
const handleHttpRequest = (req: IncomingMessage, res: ServerResponse) => {
|
|
8788
9671
|
if (!serverState) {
|
|
8789
9672
|
respondText(res, 503, "Studio server not ready");
|
|
@@ -8975,6 +9858,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
8975
9858
|
return;
|
|
8976
9859
|
}
|
|
8977
9860
|
|
|
9861
|
+
if (requestUrl.pathname === "/export-html") {
|
|
9862
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
9863
|
+
if (token !== serverState.token) {
|
|
9864
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
9865
|
+
if (method === "GET") {
|
|
9866
|
+
respondText(res, 403, "Invalid or expired studio token. Re-run /studio.");
|
|
9867
|
+
} else {
|
|
9868
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
9869
|
+
}
|
|
9870
|
+
return;
|
|
9871
|
+
}
|
|
9872
|
+
|
|
9873
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
9874
|
+
if (method === "GET") {
|
|
9875
|
+
handlePreparedHtmlDownloadRequest(requestUrl, res);
|
|
9876
|
+
return;
|
|
9877
|
+
}
|
|
9878
|
+
if (method !== "POST") {
|
|
9879
|
+
res.setHeader("Allow", "GET, POST");
|
|
9880
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use GET or POST." });
|
|
9881
|
+
return;
|
|
9882
|
+
}
|
|
9883
|
+
|
|
9884
|
+
void handleExportHtmlRequest(req, res).catch((error) => {
|
|
9885
|
+
respondJson(res, 500, {
|
|
9886
|
+
ok: false,
|
|
9887
|
+
error: `HTML export failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
9888
|
+
});
|
|
9889
|
+
});
|
|
9890
|
+
return;
|
|
9891
|
+
}
|
|
9892
|
+
|
|
8978
9893
|
if (requestUrl.pathname === "/pdf-resource") {
|
|
8979
9894
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
8980
9895
|
if (token !== serverState.token) {
|
|
@@ -9174,6 +10089,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9174
10089
|
clearActiveRequest();
|
|
9175
10090
|
clearPendingStudioCompletion();
|
|
9176
10091
|
clearPreparedPdfExports();
|
|
10092
|
+
clearPreparedHtmlExports();
|
|
9177
10093
|
clearCompactionState();
|
|
9178
10094
|
closeAllClients(1001, "Server shutting down");
|
|
9179
10095
|
|
|
@@ -9199,6 +10115,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9199
10115
|
clearStudioDirectRunState();
|
|
9200
10116
|
if (isSessionReplacement) {
|
|
9201
10117
|
clearActiveRequest({ notify: "Session switched. Studio request state cleared.", level: "warning" });
|
|
10118
|
+
studioTraceHistory.clear();
|
|
9202
10119
|
lastCommandCtx = null;
|
|
9203
10120
|
}
|
|
9204
10121
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
@@ -9206,6 +10123,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9206
10123
|
agentBusy = false;
|
|
9207
10124
|
clearPendingStudioCompletion();
|
|
9208
10125
|
clearPreparedPdfExports();
|
|
10126
|
+
clearPreparedHtmlExports();
|
|
9209
10127
|
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
9210
10128
|
refreshContextUsage(ctx);
|
|
9211
10129
|
emitDebugEvent("session_start", {
|
|
@@ -9382,7 +10300,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9382
10300
|
? getPromptDescriptorForActiveRequest(activeRequest)
|
|
9383
10301
|
: buildStudioPromptDescriptor(pendingTurnPrompt ?? latestSessionUserPrompt ?? null);
|
|
9384
10302
|
const fallbackHistoryItem: StudioResponseHistoryItem = {
|
|
9385
|
-
id:
|
|
10303
|
+
id: buildNextStudioResponseHistoryId(markdown, studioResponseHistory),
|
|
9386
10304
|
markdown,
|
|
9387
10305
|
thinking,
|
|
9388
10306
|
timestamp: Date.now(),
|
|
@@ -9401,6 +10319,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
9401
10319
|
const responseTimestamp = latestItem?.timestamp ?? Date.now();
|
|
9402
10320
|
const responseThinking = latestItem?.thinking ?? thinking ?? null;
|
|
9403
10321
|
pendingTurnPrompt = null;
|
|
10322
|
+
setStudioTraceRunStatus("complete");
|
|
10323
|
+
if (latestItem) {
|
|
10324
|
+
storeStudioTraceSnapshotForResponse(latestItem.id);
|
|
10325
|
+
}
|
|
9404
10326
|
|
|
9405
10327
|
if (activeRequest) {
|
|
9406
10328
|
const requestId = activeRequest.id;
|
|
@@ -9498,6 +10420,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
9498
10420
|
clearStudioDirectRunState();
|
|
9499
10421
|
clearPendingStudioCompletion();
|
|
9500
10422
|
clearPreparedPdfExports();
|
|
10423
|
+
clearPreparedHtmlExports();
|
|
10424
|
+
studioTraceHistory.clear();
|
|
9501
10425
|
transientStudioDocuments.clear();
|
|
9502
10426
|
clearCompactionState();
|
|
9503
10427
|
clearStudioTrace();
|
|
@@ -9594,6 +10518,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
9594
10518
|
};
|
|
9595
10519
|
};
|
|
9596
10520
|
|
|
10521
|
+
const resolveLastModelResponseForExport = (ctx: ExtensionCommandContext): { markdown: string } | null => {
|
|
10522
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
10523
|
+
syncStudioResponseHistory(branchEntries);
|
|
10524
|
+
const markdown =
|
|
10525
|
+
extractLatestAssistantFromEntries(branchEntries)
|
|
10526
|
+
?? extractLatestAssistantFromEntries(ctx.sessionManager.getEntries())
|
|
10527
|
+
?? lastStudioResponse?.markdown
|
|
10528
|
+
?? "";
|
|
10529
|
+
return markdown.trim() ? { markdown } : null;
|
|
10530
|
+
};
|
|
10531
|
+
|
|
9597
10532
|
const openStudioView = async (
|
|
9598
10533
|
trimmed: string,
|
|
9599
10534
|
ctx: ExtensionCommandContext,
|
|
@@ -9700,7 +10635,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
9700
10635
|
+ " /studio-replace [path] Replace the current full Studio view with a new one\n"
|
|
9701
10636
|
+ " /studio-editor-only [path] Open another Studio tab in editor-only mode\n"
|
|
9702
10637
|
+ " /studio-current <path> Load a file into currently open Studio tab(s)\n"
|
|
9703
|
-
+ " /studio-pdf
|
|
10638
|
+
+ " /studio-pdf [path] Export a file or last response via Studio PDF\n"
|
|
10639
|
+
+ " /studio-html [path] Export a file or last response via Studio preview HTML",
|
|
9704
10640
|
"info",
|
|
9705
10641
|
);
|
|
9706
10642
|
return;
|
|
@@ -9757,13 +10693,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
9757
10693
|
});
|
|
9758
10694
|
|
|
9759
10695
|
pi.registerCommand("studio-pdf", {
|
|
9760
|
-
description: "Export a file to PDF via the Studio PDF pipeline (/studio-pdf
|
|
10696
|
+
description: "Export a file or the last model response to PDF via the Studio PDF pipeline (/studio-pdf [file])",
|
|
9761
10697
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
9762
10698
|
const trimmed = args.trim();
|
|
9763
|
-
if (
|
|
10699
|
+
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
9764
10700
|
ctx.ui.notify(
|
|
9765
|
-
"Usage: /studio-pdf
|
|
9766
|
-
+ "
|
|
10701
|
+
"Usage: /studio-pdf [path] [options]\n"
|
|
10702
|
+
+ " Without a path, export the last model response to studio-response-<timestamp>.studio.pdf.\n"
|
|
10703
|
+
+ " With a path, export a local Markdown/LaTeX/code file to <name>.studio.pdf using the Studio PDF pipeline.\n"
|
|
9767
10704
|
+ "Options:\n"
|
|
9768
10705
|
+ " --fontsize <value> e.g. 12pt\n"
|
|
9769
10706
|
+ " --section-size <value> e.g. 24pt\n"
|
|
@@ -9796,6 +10733,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
9796
10733
|
}
|
|
9797
10734
|
const { pathArg, options: pdfOptions } = parsedArgs;
|
|
9798
10735
|
|
|
10736
|
+
if (!pathArg) {
|
|
10737
|
+
await ctx.waitForIdle();
|
|
10738
|
+
const response = resolveLastModelResponseForExport(ctx);
|
|
10739
|
+
if (!response) {
|
|
10740
|
+
ctx.ui.notify("No last model response to export. Use /studio-pdf <path> or run a prompt first.", "warning");
|
|
10741
|
+
return;
|
|
10742
|
+
}
|
|
10743
|
+
if (response.markdown.length > PDF_EXPORT_MAX_CHARS) {
|
|
10744
|
+
ctx.ui.notify(`PDF export text exceeds ${PDF_EXPORT_MAX_CHARS} characters.`, "error");
|
|
10745
|
+
return;
|
|
10746
|
+
}
|
|
10747
|
+
|
|
10748
|
+
const editorPdfLanguage = inferStudioPdfLanguage(response.markdown);
|
|
10749
|
+
const isLatex = editorPdfLanguage === "latex"
|
|
10750
|
+
|| (
|
|
10751
|
+
(editorPdfLanguage === undefined || editorPdfLanguage === "markdown")
|
|
10752
|
+
&& /\\documentclass\b|\\begin\{document\}/.test(response.markdown)
|
|
10753
|
+
);
|
|
10754
|
+
const resourcePath = resolveStudioBaseDir(undefined, undefined, ctx.cwd);
|
|
10755
|
+
const outputPath = buildStudioResponseExportOutputPath(ctx.cwd, "pdf");
|
|
10756
|
+
|
|
10757
|
+
try {
|
|
10758
|
+
const { pdf, warning } = await renderStudioPdfWithPandoc(
|
|
10759
|
+
response.markdown,
|
|
10760
|
+
isLatex,
|
|
10761
|
+
resourcePath,
|
|
10762
|
+
editorPdfLanguage,
|
|
10763
|
+
undefined,
|
|
10764
|
+
pdfOptions,
|
|
10765
|
+
);
|
|
10766
|
+
await writeFile(outputPath, pdf);
|
|
10767
|
+
|
|
10768
|
+
let openError: string | null = null;
|
|
10769
|
+
try {
|
|
10770
|
+
await openPathInDefaultViewer(outputPath);
|
|
10771
|
+
} catch (error) {
|
|
10772
|
+
openError = error instanceof Error ? error.message : String(error);
|
|
10773
|
+
}
|
|
10774
|
+
|
|
10775
|
+
ctx.ui.notify(`Exported last response Studio PDF: ${outputPath}`, "info");
|
|
10776
|
+
if (warning) {
|
|
10777
|
+
ctx.ui.notify(warning, "warning");
|
|
10778
|
+
}
|
|
10779
|
+
if (openError) {
|
|
10780
|
+
ctx.ui.notify(`PDF was exported but could not be opened automatically: ${openError}`, "warning");
|
|
10781
|
+
}
|
|
10782
|
+
} catch (error) {
|
|
10783
|
+
ctx.ui.notify(
|
|
10784
|
+
`Studio PDF export failed for last response: ${error instanceof Error ? error.message : String(error)}`,
|
|
10785
|
+
"error",
|
|
10786
|
+
);
|
|
10787
|
+
}
|
|
10788
|
+
return;
|
|
10789
|
+
}
|
|
10790
|
+
|
|
9799
10791
|
const file = readStudioFile(pathArg, ctx.cwd);
|
|
9800
10792
|
if (file.ok === false) {
|
|
9801
10793
|
ctx.ui.notify(file.message, "error");
|
|
@@ -9853,6 +10845,148 @@ export default function (pi: ExtensionAPI) {
|
|
|
9853
10845
|
},
|
|
9854
10846
|
});
|
|
9855
10847
|
|
|
10848
|
+
pi.registerCommand("studio-html", {
|
|
10849
|
+
description: "Export a file or the last model response to standalone HTML via the Studio preview pipeline (/studio-html [file])",
|
|
10850
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
10851
|
+
const trimmed = args.trim();
|
|
10852
|
+
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
10853
|
+
ctx.ui.notify(
|
|
10854
|
+
"Usage: /studio-html [path]\n"
|
|
10855
|
+
+ " Without a path, export the last model response to studio-response-<timestamp>.studio.html.\n"
|
|
10856
|
+
+ " With a path, export a local Markdown/LaTeX/code file to <name>.studio.html using the Studio preview HTML pipeline.",
|
|
10857
|
+
"info",
|
|
10858
|
+
);
|
|
10859
|
+
return;
|
|
10860
|
+
}
|
|
10861
|
+
|
|
10862
|
+
if (!trimmed) {
|
|
10863
|
+
await ctx.waitForIdle();
|
|
10864
|
+
const response = resolveLastModelResponseForExport(ctx);
|
|
10865
|
+
if (!response) {
|
|
10866
|
+
ctx.ui.notify("No last model response to export. Use /studio-html <path> or run a prompt first.", "warning");
|
|
10867
|
+
return;
|
|
10868
|
+
}
|
|
10869
|
+
if (response.markdown.length > HTML_EXPORT_MAX_CHARS) {
|
|
10870
|
+
ctx.ui.notify(`HTML export text exceeds ${HTML_EXPORT_MAX_CHARS} characters.`, "error");
|
|
10871
|
+
return;
|
|
10872
|
+
}
|
|
10873
|
+
|
|
10874
|
+
const editorHtmlLanguage = inferStudioPdfLanguage(response.markdown);
|
|
10875
|
+
const isLatex = editorHtmlLanguage === "latex"
|
|
10876
|
+
|| (
|
|
10877
|
+
(editorHtmlLanguage === undefined || editorHtmlLanguage === "markdown")
|
|
10878
|
+
&& isLikelyStandaloneLatexPreview(response.markdown)
|
|
10879
|
+
);
|
|
10880
|
+
const resourcePath = resolveStudioBaseDir(undefined, undefined, ctx.cwd);
|
|
10881
|
+
const outputPath = buildStudioResponseExportOutputPath(ctx.cwd, "html");
|
|
10882
|
+
const themeVars = buildThemeCssVars(getStudioThemeStyle(ctx.ui.theme));
|
|
10883
|
+
|
|
10884
|
+
try {
|
|
10885
|
+
const { html, warning } = await renderStudioStandaloneHtmlWithPandoc(
|
|
10886
|
+
response.markdown,
|
|
10887
|
+
isLatex,
|
|
10888
|
+
resourcePath,
|
|
10889
|
+
editorHtmlLanguage,
|
|
10890
|
+
undefined,
|
|
10891
|
+
{
|
|
10892
|
+
title: basename(outputPath),
|
|
10893
|
+
sourceLabel: "last model response",
|
|
10894
|
+
themeVars,
|
|
10895
|
+
},
|
|
10896
|
+
);
|
|
10897
|
+
await writeFile(outputPath, html);
|
|
10898
|
+
|
|
10899
|
+
let openError: string | null = null;
|
|
10900
|
+
try {
|
|
10901
|
+
await openPathInDefaultViewer(outputPath);
|
|
10902
|
+
} catch (error) {
|
|
10903
|
+
openError = error instanceof Error ? error.message : String(error);
|
|
10904
|
+
}
|
|
10905
|
+
|
|
10906
|
+
ctx.ui.notify(`Exported last response Studio HTML: ${outputPath}`, "info");
|
|
10907
|
+
if (warning) {
|
|
10908
|
+
ctx.ui.notify(warning, "warning");
|
|
10909
|
+
}
|
|
10910
|
+
if (openError) {
|
|
10911
|
+
ctx.ui.notify(`HTML was exported but could not be opened automatically: ${openError}`, "warning");
|
|
10912
|
+
}
|
|
10913
|
+
} catch (error) {
|
|
10914
|
+
ctx.ui.notify(
|
|
10915
|
+
`Studio HTML export failed for last response: ${error instanceof Error ? error.message : String(error)}`,
|
|
10916
|
+
"error",
|
|
10917
|
+
);
|
|
10918
|
+
}
|
|
10919
|
+
return;
|
|
10920
|
+
}
|
|
10921
|
+
|
|
10922
|
+
const pathArg = parsePathArgument(trimmed);
|
|
10923
|
+
if (!pathArg) {
|
|
10924
|
+
ctx.ui.notify("Invalid file path argument.", "error");
|
|
10925
|
+
return;
|
|
10926
|
+
}
|
|
10927
|
+
|
|
10928
|
+
const file = readStudioFile(pathArg, ctx.cwd);
|
|
10929
|
+
if (file.ok === false) {
|
|
10930
|
+
ctx.ui.notify(file.message, "error");
|
|
10931
|
+
return;
|
|
10932
|
+
}
|
|
10933
|
+
|
|
10934
|
+
if (file.text.length > HTML_EXPORT_MAX_CHARS) {
|
|
10935
|
+
ctx.ui.notify(`HTML export text exceeds ${HTML_EXPORT_MAX_CHARS} characters.`, "error");
|
|
10936
|
+
return;
|
|
10937
|
+
}
|
|
10938
|
+
|
|
10939
|
+
await ctx.waitForIdle();
|
|
10940
|
+
const pathHtmlLanguage = inferStudioPdfLanguageFromPath(file.resolvedPath);
|
|
10941
|
+
const editorHtmlLanguage = pathHtmlLanguage ?? inferStudioPdfLanguage(file.text);
|
|
10942
|
+
const isLatex = editorHtmlLanguage === "latex"
|
|
10943
|
+
|| (
|
|
10944
|
+
!pathHtmlLanguage
|
|
10945
|
+
&& (editorHtmlLanguage === undefined || editorHtmlLanguage === "markdown")
|
|
10946
|
+
&& isLikelyStandaloneLatexPreview(file.text)
|
|
10947
|
+
);
|
|
10948
|
+
const resourcePath = resolveStudioBaseDir(file.resolvedPath, undefined, ctx.cwd);
|
|
10949
|
+
const outputPath = buildStudioHtmlOutputPath(file.resolvedPath);
|
|
10950
|
+
const themeVars = buildThemeCssVars(getStudioThemeStyle(ctx.ui.theme));
|
|
10951
|
+
|
|
10952
|
+
try {
|
|
10953
|
+
const { html, warning } = await renderStudioStandaloneHtmlWithPandoc(
|
|
10954
|
+
file.text,
|
|
10955
|
+
isLatex,
|
|
10956
|
+
resourcePath,
|
|
10957
|
+
editorHtmlLanguage,
|
|
10958
|
+
file.resolvedPath,
|
|
10959
|
+
{
|
|
10960
|
+
title: basename(outputPath),
|
|
10961
|
+
sourceLabel: file.resolvedPath,
|
|
10962
|
+
themeVars,
|
|
10963
|
+
},
|
|
10964
|
+
);
|
|
10965
|
+
await writeFile(outputPath, html);
|
|
10966
|
+
|
|
10967
|
+
let openError: string | null = null;
|
|
10968
|
+
try {
|
|
10969
|
+
await openPathInDefaultViewer(outputPath);
|
|
10970
|
+
} catch (error) {
|
|
10971
|
+
openError = error instanceof Error ? error.message : String(error);
|
|
10972
|
+
}
|
|
10973
|
+
|
|
10974
|
+
ctx.ui.notify(`Exported Studio HTML: ${outputPath}`, "info");
|
|
10975
|
+
if (warning) {
|
|
10976
|
+
ctx.ui.notify(warning, "warning");
|
|
10977
|
+
}
|
|
10978
|
+
if (openError) {
|
|
10979
|
+
ctx.ui.notify(`HTML was exported but could not be opened automatically: ${openError}`, "warning");
|
|
10980
|
+
}
|
|
10981
|
+
} catch (error) {
|
|
10982
|
+
ctx.ui.notify(
|
|
10983
|
+
`Studio HTML export failed for ${file.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
10984
|
+
"error",
|
|
10985
|
+
);
|
|
10986
|
+
}
|
|
10987
|
+
},
|
|
10988
|
+
});
|
|
10989
|
+
|
|
9856
10990
|
pi.registerCommand("studio-current", {
|
|
9857
10991
|
description: "Load a file into current open Studio tab(s) without opening a new browser session",
|
|
9858
10992
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|