pi-studio 0.7.1 → 0.8.0
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 +7 -0
- package/README.md +5 -4
- package/client/studio-client.js +489 -29
- package/client/studio.css +61 -0
- package/index.ts +1120 -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 } {
|
|
@@ -3593,7 +3729,7 @@ interface StudioPdfRenderOptions {
|
|
|
3593
3729
|
}
|
|
3594
3730
|
|
|
3595
3731
|
interface StudioParsedPdfCommandArgs {
|
|
3596
|
-
pathArg: string;
|
|
3732
|
+
pathArg: string | null;
|
|
3597
3733
|
options: StudioPdfRenderOptions;
|
|
3598
3734
|
}
|
|
3599
3735
|
|
|
@@ -3925,7 +4061,6 @@ function parseStudioPdfCommandArgs(args: string): StudioParsedPdfCommandArgs | {
|
|
|
3925
4061
|
const parsed = tokenizeStudioCommandArgs(args);
|
|
3926
4062
|
if (parsed.error) return { error: parsed.error };
|
|
3927
4063
|
const tokens = parsed.tokens;
|
|
3928
|
-
if (tokens.length === 0) return { error: "Missing file path." };
|
|
3929
4064
|
|
|
3930
4065
|
const options: StudioPdfRenderOptions = {};
|
|
3931
4066
|
let pathArg: string | null = null;
|
|
@@ -4031,7 +4166,6 @@ function parseStudioPdfCommandArgs(args: string): StudioParsedPdfCommandArgs | {
|
|
|
4031
4166
|
}
|
|
4032
4167
|
}
|
|
4033
4168
|
|
|
4034
|
-
if (!pathArg) return { error: "Missing file path." };
|
|
4035
4169
|
if (options.geometry && (options.margin || options.marginTop || options.marginRight || options.marginBottom || options.marginLeft || options.footskip)) {
|
|
4036
4170
|
return { error: "Use either --geometry or the --margin/--margin-*/--footskip flags, not both." };
|
|
4037
4171
|
}
|
|
@@ -4489,6 +4623,365 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
4489
4623
|
return renderedHtml;
|
|
4490
4624
|
}
|
|
4491
4625
|
|
|
4626
|
+
function escapeStudioRegExpLiteral(text: string): string {
|
|
4627
|
+
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
function parseStudioHtmlPdfBlockOptions(body: string): StudioHtmlPdfBlockOptions {
|
|
4631
|
+
const options: StudioHtmlPdfBlockOptions = { path: "", title: "", caption: "", page: "", height: "" };
|
|
4632
|
+
String(body ?? "").split(/\r?\n/).forEach((line) => {
|
|
4633
|
+
const raw = String(line ?? "").trim();
|
|
4634
|
+
if (!raw || raw.startsWith("#")) return;
|
|
4635
|
+
const match = raw.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*([\s\S]*)$/);
|
|
4636
|
+
if (match) {
|
|
4637
|
+
const key = String(match[1] ?? "").toLowerCase();
|
|
4638
|
+
const value = stripMatchingPathQuotes(String(match[2] ?? ""));
|
|
4639
|
+
if (key === "path" || key === "src" || key === "file") options.path = value;
|
|
4640
|
+
else if (key === "title") options.title = value;
|
|
4641
|
+
else if (key === "caption") options.caption = value;
|
|
4642
|
+
else if (key === "page") options.page = value;
|
|
4643
|
+
else if (key === "height") options.height = value;
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
if (!options.path) options.path = stripMatchingPathQuotes(raw);
|
|
4647
|
+
});
|
|
4648
|
+
return options;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
function prepareStudioPdfBlocksForHtml(markdown: string): { markdown: string; blocks: StudioHtmlPdfBlock[] } {
|
|
4652
|
+
const blocks: StudioHtmlPdfBlock[] = [];
|
|
4653
|
+
const prefix = `PISTUDIOHTMLPDF${Date.now().toString(36)}${randomUUID().replace(/-/g, "")}TOKEN`;
|
|
4654
|
+
const source = String(markdown ?? "");
|
|
4655
|
+
const blockPattern = /(^|\n)([ \t]{0,3})(`{3,}|~{3,})[ \t]*studio-pdf[^\n]*\n([\s\S]*?)\n[ \t]*\3[ \t]*(?=\n|$)/g;
|
|
4656
|
+
const nextMarkdown = source.replace(blockPattern, (_match, leadingNewline: string, _indent: string, _fence: string, body: string) => {
|
|
4657
|
+
const placeholder = `${prefix}${blocks.length}`;
|
|
4658
|
+
blocks.push({ placeholder, options: parseStudioHtmlPdfBlockOptions(body) });
|
|
4659
|
+
return `${String(leadingNewline ?? "")}${placeholder}\n`;
|
|
4660
|
+
});
|
|
4661
|
+
return { markdown: nextMarkdown, blocks };
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
function prepareStudioAnnotationMarkersForHtml(markdown: string): { markdown: string; placeholders: StudioHtmlAnnotationPlaceholder[] } {
|
|
4665
|
+
const placeholders: StudioHtmlAnnotationPlaceholder[] = [];
|
|
4666
|
+
if (!hasStudioMarkdownAnnotationMarkers(markdown)) return { markdown: String(markdown ?? ""), placeholders };
|
|
4667
|
+
|
|
4668
|
+
const prefix = `PISTUDIOHTMLANNOT${Date.now().toString(36)}${randomUUID().replace(/-/g, "")}TOKEN`;
|
|
4669
|
+
const prepared = transformStudioMarkdownOutsideFences(markdown, (segment: string) => replaceStudioInlineAnnotationMarkers(segment, (marker: { body?: unknown }) => {
|
|
4670
|
+
const label = normalizeStudioAnnotationText(String(marker.body ?? ""));
|
|
4671
|
+
if (!label) return "";
|
|
4672
|
+
const token = `${prefix}${placeholders.length}`;
|
|
4673
|
+
placeholders.push({ token, text: label, title: `[an: ${label}]` });
|
|
4674
|
+
return token;
|
|
4675
|
+
}));
|
|
4676
|
+
return { markdown: prepared, placeholders };
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
function applyStudioAnnotationPlaceholdersToHtml(html: string, placeholders: StudioHtmlAnnotationPlaceholder[]): string {
|
|
4680
|
+
let transformed = String(html ?? "");
|
|
4681
|
+
for (const placeholder of placeholders) {
|
|
4682
|
+
const tokenPattern = new RegExp(escapeStudioRegExpLiteral(placeholder.token), "g");
|
|
4683
|
+
const markerHtml = `<span class="annotation-preview-marker" title="${escapeStudioHtmlText(placeholder.title)}">${escapeStudioHtmlText(placeholder.text)}</span>`;
|
|
4684
|
+
transformed = transformed.replace(tokenPattern, markerHtml);
|
|
4685
|
+
}
|
|
4686
|
+
return transformed;
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
function normalizeStudioHtmlPdfHeight(value: string): number {
|
|
4690
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
4691
|
+
if (!Number.isFinite(parsed)) return 680;
|
|
4692
|
+
return Math.max(240, Math.min(1400, parsed));
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
function normalizeStudioHtmlPdfPage(value: string): number {
|
|
4696
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
4697
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
function renderStudioHtmlPdfBlock(block: StudioHtmlPdfBlock, sourcePath: string | undefined, resourcePath: string | undefined): string {
|
|
4701
|
+
const options = block.options;
|
|
4702
|
+
const pdfPath = String(options.path || "").trim();
|
|
4703
|
+
const title = String(options.title || pdfPath || "Embedded PDF").trim();
|
|
4704
|
+
const caption = String(options.caption || "").trim();
|
|
4705
|
+
const height = normalizeStudioHtmlPdfHeight(options.height);
|
|
4706
|
+
const page = normalizeStudioHtmlPdfPage(options.page);
|
|
4707
|
+
|
|
4708
|
+
let iframeSrc = "";
|
|
4709
|
+
let linkHref = "";
|
|
4710
|
+
let error = "";
|
|
4711
|
+
if (!pdfPath) {
|
|
4712
|
+
error = "PDF block needs a local path.";
|
|
4713
|
+
} else {
|
|
4714
|
+
try {
|
|
4715
|
+
const baseDir = resourcePath || (sourcePath ? dirname(sourcePath) : process.cwd());
|
|
4716
|
+
const resolvedPath = resolveStudioPdfResourceFile(pdfPath, baseDir);
|
|
4717
|
+
const pdfBuffer = readFileSync(resolvedPath);
|
|
4718
|
+
iframeSrc = `data:application/pdf;base64,${pdfBuffer.toString("base64")}`;
|
|
4719
|
+
linkHref = pathToFileURL(resolvedPath).href;
|
|
4720
|
+
} catch (readError) {
|
|
4721
|
+
error = `PDF resource unavailable: ${readError instanceof Error ? readError.message : String(readError)}`;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
const viewerSrc = iframeSrc && page ? `${iframeSrc}#page=${encodeURIComponent(String(page))}` : iframeSrc;
|
|
4726
|
+
const openLink = linkHref
|
|
4727
|
+
? `<a class="studio-pdf-card-link" href="${escapeStudioHtmlText(linkHref)}" target="_blank" rel="noopener noreferrer">Open PDF</a>`
|
|
4728
|
+
: "";
|
|
4729
|
+
const captionHtml = caption
|
|
4730
|
+
? `<div class="studio-pdf-card-caption">${escapeStudioHtmlText(caption)}</div>`
|
|
4731
|
+
: "";
|
|
4732
|
+
const bodyHtml = viewerSrc
|
|
4733
|
+
? `<iframe class="studio-pdf-frame" src="${escapeStudioHtmlText(viewerSrc)}" title="${escapeStudioHtmlText(title)}" loading="lazy" style="height: ${height}px"></iframe>`
|
|
4734
|
+
: `<div class="studio-pdf-card-error">${escapeStudioHtmlText(error || "PDF resource unavailable.")}</div>`;
|
|
4735
|
+
|
|
4736
|
+
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>`;
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
function renderStudioPdfBlocksInHtml(html: string, blocks: StudioHtmlPdfBlock[], sourcePath: string | undefined, resourcePath: string | undefined): string {
|
|
4740
|
+
let transformed = String(html ?? "");
|
|
4741
|
+
for (const block of blocks) {
|
|
4742
|
+
const replacement = renderStudioHtmlPdfBlock(block, sourcePath, resourcePath);
|
|
4743
|
+
const paragraphPattern = new RegExp(`<p>\\s*${escapeStudioRegExpLiteral(block.placeholder)}\\s*<\\/p>`, "g");
|
|
4744
|
+
transformed = transformed.replace(paragraphPattern, replacement);
|
|
4745
|
+
transformed = transformed.replace(new RegExp(escapeStudioRegExpLiteral(block.placeholder), "g"), replacement);
|
|
4746
|
+
}
|
|
4747
|
+
return transformed;
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
function parseStudioThemeVarsJson(json: string | undefined): Record<string, string> | null {
|
|
4751
|
+
const raw = String(json ?? "").trim();
|
|
4752
|
+
if (!raw) return null;
|
|
4753
|
+
try {
|
|
4754
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
4755
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
4756
|
+
const vars: Record<string, string> = {};
|
|
4757
|
+
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
4758
|
+
if ((key === "color-scheme" || key.startsWith("--")) && typeof value === "string") {
|
|
4759
|
+
vars[key] = value;
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
return Object.keys(vars).length > 0 ? vars : null;
|
|
4763
|
+
} catch {
|
|
4764
|
+
return null;
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
function buildStudioCssVarsBlock(vars: Record<string, string>): string {
|
|
4769
|
+
return Object.entries(vars).map(([key, value]) => ` ${key}: ${value};`).join("\n");
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
function buildStudioHtmlExportBaseHref(resourcePath: string | undefined): string {
|
|
4773
|
+
const base = String(resourcePath ?? "").trim();
|
|
4774
|
+
if (!base) return "";
|
|
4775
|
+
try {
|
|
4776
|
+
return pathToFileURL(base.endsWith("/") ? base : `${base}/`).href;
|
|
4777
|
+
} catch {
|
|
4778
|
+
return "";
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
function buildStudioHtmlMermaidConfig(vars: Record<string, string>): Record<string, unknown> {
|
|
4783
|
+
return {
|
|
4784
|
+
startOnLoad: false,
|
|
4785
|
+
theme: "base",
|
|
4786
|
+
fontFamily: vars["--font-mono"] ?? "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
4787
|
+
flowchart: {
|
|
4788
|
+
curve: "basis",
|
|
4789
|
+
},
|
|
4790
|
+
themeVariables: {
|
|
4791
|
+
background: vars["--bg"] ?? "#ffffff",
|
|
4792
|
+
primaryColor: vars["--panel-2"] ?? "#f6f8fa",
|
|
4793
|
+
primaryTextColor: vars["--text"] ?? "#111827",
|
|
4794
|
+
primaryBorderColor: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4795
|
+
secondaryColor: vars["--panel"] ?? "#ffffff",
|
|
4796
|
+
secondaryTextColor: vars["--text"] ?? "#111827",
|
|
4797
|
+
secondaryBorderColor: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4798
|
+
tertiaryColor: vars["--panel"] ?? "#ffffff",
|
|
4799
|
+
tertiaryTextColor: vars["--text"] ?? "#111827",
|
|
4800
|
+
tertiaryBorderColor: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4801
|
+
lineColor: vars["--md-quote"] ?? vars["--text"] ?? "#111827",
|
|
4802
|
+
textColor: vars["--text"] ?? "#111827",
|
|
4803
|
+
edgeLabelBackground: vars["--panel-2"] ?? "#f6f8fa",
|
|
4804
|
+
nodeBorder: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4805
|
+
clusterBkg: vars["--panel"] ?? "#ffffff",
|
|
4806
|
+
clusterBorder: vars["--md-codeblock-border"] ?? vars["--border"] ?? "#d0d7de",
|
|
4807
|
+
titleColor: vars["--md-heading"] ?? vars["--text"] ?? "#111827",
|
|
4808
|
+
},
|
|
4809
|
+
};
|
|
4810
|
+
}
|
|
4811
|
+
|
|
4812
|
+
function buildStudioStandaloneHtmlMermaidScript(vars: Record<string, string>): string {
|
|
4813
|
+
const mermaidConfigJson = JSON.stringify(buildStudioHtmlMermaidConfig(vars)).replace(/</g, "\\u003c");
|
|
4814
|
+
return `<script>
|
|
4815
|
+
(() => {
|
|
4816
|
+
const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
4817
|
+
const MERMAID_CONFIG = ${mermaidConfigJson};
|
|
4818
|
+
|
|
4819
|
+
function appendMermaidWarning(message) {
|
|
4820
|
+
const documentEl = document.querySelector(".studio-export-document") || document.body;
|
|
4821
|
+
if (!documentEl || documentEl.querySelector(".preview-mermaid-warning")) return;
|
|
4822
|
+
const warningEl = document.createElement("div");
|
|
4823
|
+
warningEl.className = "preview-warning preview-mermaid-warning";
|
|
4824
|
+
warningEl.textContent = message || "Mermaid renderer unavailable. Showing mermaid blocks as code.";
|
|
4825
|
+
documentEl.appendChild(warningEl);
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
function prepareMermaidBlocks() {
|
|
4829
|
+
const preBlocks = Array.from(document.querySelectorAll("pre.mermaid"));
|
|
4830
|
+
preBlocks.forEach((preEl) => {
|
|
4831
|
+
const source = preEl.querySelector("code") ? preEl.querySelector("code").textContent : preEl.textContent;
|
|
4832
|
+
const wrapper = document.createElement("div");
|
|
4833
|
+
wrapper.className = "mermaid-container";
|
|
4834
|
+
const diagramEl = document.createElement("div");
|
|
4835
|
+
diagramEl.className = "mermaid";
|
|
4836
|
+
diagramEl.textContent = source || "";
|
|
4837
|
+
wrapper.appendChild(diagramEl);
|
|
4838
|
+
preEl.replaceWith(wrapper);
|
|
4839
|
+
});
|
|
4840
|
+
return Array.from(document.querySelectorAll(".mermaid"));
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
async function renderMermaid() {
|
|
4844
|
+
const nodes = prepareMermaidBlocks();
|
|
4845
|
+
if (nodes.length === 0) return;
|
|
4846
|
+
try {
|
|
4847
|
+
const module = await import(MERMAID_CDN_URL);
|
|
4848
|
+
const mermaidApi = module && module.default ? module.default : null;
|
|
4849
|
+
if (!mermaidApi) throw new Error("Mermaid module did not expose a default export.");
|
|
4850
|
+
mermaidApi.initialize(MERMAID_CONFIG);
|
|
4851
|
+
await mermaidApi.run({ nodes });
|
|
4852
|
+
} catch (error) {
|
|
4853
|
+
console.error("Mermaid render failed:", error);
|
|
4854
|
+
appendMermaidWarning("Mermaid renderer unavailable. Showing mermaid source text.");
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
if (document.readyState === "loading") {
|
|
4859
|
+
document.addEventListener("DOMContentLoaded", () => { void renderMermaid(); }, { once: true });
|
|
4860
|
+
} else {
|
|
4861
|
+
void renderMermaid();
|
|
4862
|
+
}
|
|
4863
|
+
})();
|
|
4864
|
+
</script>`;
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
function buildStudioStandaloneHtmlDocument(contentHtml: string, resourcePath: string | undefined, options?: StudioHtmlRenderOptions): string {
|
|
4868
|
+
const title = String(options?.title || "pi Studio preview").trim() || "pi Studio preview";
|
|
4869
|
+
const vars = options?.themeVars ?? buildThemeCssVars(getStudioThemeStyle());
|
|
4870
|
+
const cssVarsBlock = buildStudioCssVarsBlock(vars);
|
|
4871
|
+
const stylesheet = readFileSync(STUDIO_CSS_URL, "utf-8");
|
|
4872
|
+
const mermaidScript = buildStudioStandaloneHtmlMermaidScript(vars);
|
|
4873
|
+
const baseHref = buildStudioHtmlExportBaseHref(resourcePath);
|
|
4874
|
+
const baseTag = baseHref ? ` <base href="${escapeStudioHtmlText(baseHref)}" />\n` : "";
|
|
4875
|
+
const generatedAt = new Date().toISOString();
|
|
4876
|
+
const sourceLabel = String(options?.sourceLabel || "").trim();
|
|
4877
|
+
const sourceMeta = sourceLabel ? ` data-source-label="${escapeStudioHtmlText(sourceLabel)}"` : "";
|
|
4878
|
+
const exportCss = `
|
|
4879
|
+
body.studio-html-export {
|
|
4880
|
+
display: block;
|
|
4881
|
+
min-height: 100%;
|
|
4882
|
+
padding: 0;
|
|
4883
|
+
background: var(--bg);
|
|
4884
|
+
color: var(--text);
|
|
4885
|
+
}
|
|
4886
|
+
body.studio-html-export .studio-export-shell {
|
|
4887
|
+
display: block;
|
|
4888
|
+
flex: none;
|
|
4889
|
+
width: 100%;
|
|
4890
|
+
max-width: 1180px;
|
|
4891
|
+
min-height: auto;
|
|
4892
|
+
margin: 0 auto;
|
|
4893
|
+
padding: 32px clamp(16px, 4vw, 48px) 56px;
|
|
4894
|
+
}
|
|
4895
|
+
body.studio-html-export .studio-export-document {
|
|
4896
|
+
display: block;
|
|
4897
|
+
width: 100%;
|
|
4898
|
+
overflow: visible;
|
|
4899
|
+
padding: 28px;
|
|
4900
|
+
border: 1px solid var(--panel-border);
|
|
4901
|
+
border-radius: 14px;
|
|
4902
|
+
background: var(--panel);
|
|
4903
|
+
box-shadow: var(--panel-shadow);
|
|
4904
|
+
}
|
|
4905
|
+
body.studio-html-export .studio-export-document > :first-child {
|
|
4906
|
+
margin-top: 0;
|
|
4907
|
+
}
|
|
4908
|
+
body.studio-html-export .studio-export-document > :last-child {
|
|
4909
|
+
margin-bottom: 0;
|
|
4910
|
+
}
|
|
4911
|
+
body.studio-html-export .preview-selection-actions,
|
|
4912
|
+
body.studio-html-export .studio-copy-block-btn {
|
|
4913
|
+
display: none !important;
|
|
4914
|
+
}
|
|
4915
|
+
@media print {
|
|
4916
|
+
body.studio-html-export {
|
|
4917
|
+
background: #fff;
|
|
4918
|
+
color: #111;
|
|
4919
|
+
}
|
|
4920
|
+
body.studio-html-export .studio-export-shell {
|
|
4921
|
+
max-width: none;
|
|
4922
|
+
padding: 0;
|
|
4923
|
+
}
|
|
4924
|
+
body.studio-html-export .studio-export-document {
|
|
4925
|
+
border: 0;
|
|
4926
|
+
border-radius: 0;
|
|
4927
|
+
box-shadow: none;
|
|
4928
|
+
padding: 0;
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
`;
|
|
4932
|
+
|
|
4933
|
+
return `<!doctype html>
|
|
4934
|
+
<html>
|
|
4935
|
+
<head>
|
|
4936
|
+
<meta charset="utf-8" />
|
|
4937
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
4938
|
+
<meta name="generator" content="pi Studio" />
|
|
4939
|
+
<meta name="pi-studio-exported-at" content="${escapeStudioHtmlText(generatedAt)}" />
|
|
4940
|
+
${baseTag} <title>${escapeStudioHtmlText(title)}</title>
|
|
4941
|
+
<style>
|
|
4942
|
+
:root {
|
|
4943
|
+
${cssVarsBlock}
|
|
4944
|
+
}
|
|
4945
|
+
${stylesheet}
|
|
4946
|
+
${exportCss}
|
|
4947
|
+
</style>
|
|
4948
|
+
</head>
|
|
4949
|
+
<body class="studio-html-export"${sourceMeta}>
|
|
4950
|
+
<main class="studio-export-shell">
|
|
4951
|
+
<article class="panel-scroll rendered-markdown studio-export-document">
|
|
4952
|
+
${contentHtml}
|
|
4953
|
+
</article>
|
|
4954
|
+
</main>
|
|
4955
|
+
${mermaidScript}
|
|
4956
|
+
</body>
|
|
4957
|
+
</html>`;
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4960
|
+
async function renderStudioStandaloneHtmlWithPandoc(
|
|
4961
|
+
markdown: string,
|
|
4962
|
+
isLatex?: boolean,
|
|
4963
|
+
resourcePath?: string,
|
|
4964
|
+
editorLanguage?: string,
|
|
4965
|
+
sourcePath?: string,
|
|
4966
|
+
options?: StudioHtmlRenderOptions,
|
|
4967
|
+
): Promise<{ html: Buffer; warning?: string }> {
|
|
4968
|
+
const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorLanguage);
|
|
4969
|
+
const source = !isLatex
|
|
4970
|
+
&& effectiveEditorLanguage
|
|
4971
|
+
&& effectiveEditorLanguage !== "markdown"
|
|
4972
|
+
&& effectiveEditorLanguage !== "latex"
|
|
4973
|
+
&& !isStudioSingleFencedCodeBlock(markdown)
|
|
4974
|
+
? wrapStudioCodeAsMarkdown(markdown, effectiveEditorLanguage)
|
|
4975
|
+
: markdown;
|
|
4976
|
+
const annotationPrepared = prepareStudioAnnotationMarkersForHtml(source);
|
|
4977
|
+
const pdfPrepared = prepareStudioPdfBlocksForHtml(annotationPrepared.markdown);
|
|
4978
|
+
let renderedHtml = await renderStudioMarkdownWithPandoc(pdfPrepared.markdown, isLatex, resourcePath, sourcePath);
|
|
4979
|
+
renderedHtml = renderStudioPdfBlocksInHtml(renderedHtml, pdfPrepared.blocks, sourcePath, resourcePath);
|
|
4980
|
+
renderedHtml = applyStudioAnnotationPlaceholdersToHtml(renderedHtml, annotationPrepared.placeholders);
|
|
4981
|
+
const standaloneHtml = buildStudioStandaloneHtmlDocument(renderedHtml, resourcePath, options);
|
|
4982
|
+
return { html: Buffer.from(standaloneHtml, "utf-8") };
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4492
4985
|
async function renderStudioLiteralTextPdf(text: string, title = "Studio export", options?: StudioPdfRenderOptions): Promise<Buffer> {
|
|
4493
4986
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
4494
4987
|
const tempDir = join(tmpdir(), `pi-studio-text-pdf-${Date.now()}-${randomUUID()}`);
|
|
@@ -5667,8 +6160,23 @@ function parseEntryTimestamp(timestamp: unknown): number {
|
|
|
5667
6160
|
return Date.now();
|
|
5668
6161
|
}
|
|
5669
6162
|
|
|
6163
|
+
function getStudioResponseHistoryContentHash(markdown: string): string {
|
|
6164
|
+
return createHash("sha256").update(String(markdown ?? ""), "utf-8").digest("hex").slice(0, 16);
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
function buildStudioResponseHistoryId(contentHash: string, occurrenceIndex: number): string {
|
|
6168
|
+
return `response-${contentHash}-${String(Math.max(0, occurrenceIndex) + 1).padStart(3, "0")}`;
|
|
6169
|
+
}
|
|
6170
|
+
|
|
6171
|
+
function buildNextStudioResponseHistoryId(markdown: string, existingItems: StudioResponseHistoryItem[]): string {
|
|
6172
|
+
const contentHash = getStudioResponseHistoryContentHash(markdown);
|
|
6173
|
+
const occurrenceIndex = existingItems.filter((item) => getStudioResponseHistoryContentHash(item.markdown) === contentHash).length;
|
|
6174
|
+
return buildStudioResponseHistoryId(contentHash, occurrenceIndex);
|
|
6175
|
+
}
|
|
6176
|
+
|
|
5670
6177
|
function buildResponseHistoryFromEntries(entries: SessionEntry[], limit = RESPONSE_HISTORY_LIMIT): StudioResponseHistoryItem[] {
|
|
5671
6178
|
const history: StudioResponseHistoryItem[] = [];
|
|
6179
|
+
const occurrenceCountsByHash = new Map<string, number>();
|
|
5672
6180
|
let lastUserPrompt: string | null = null;
|
|
5673
6181
|
let pendingPromptDescriptor: StudioPromptDescriptor | null = null;
|
|
5674
6182
|
|
|
@@ -5700,8 +6208,11 @@ function buildResponseHistoryFromEntries(entries: SessionEntry[], limit = RESPON
|
|
|
5700
6208
|
if (!markdown) continue;
|
|
5701
6209
|
const thinking = extractAssistantThinking(message);
|
|
5702
6210
|
const promptDescriptor = pendingPromptDescriptor ?? buildStudioPromptDescriptor(lastUserPrompt);
|
|
6211
|
+
const contentHash = getStudioResponseHistoryContentHash(markdown);
|
|
6212
|
+
const occurrenceIndex = occurrenceCountsByHash.get(contentHash) ?? 0;
|
|
6213
|
+
occurrenceCountsByHash.set(contentHash, occurrenceIndex + 1);
|
|
5703
6214
|
history.push({
|
|
5704
|
-
id:
|
|
6215
|
+
id: buildStudioResponseHistoryId(contentHash, occurrenceIndex),
|
|
5705
6216
|
markdown,
|
|
5706
6217
|
thinking,
|
|
5707
6218
|
timestamp: parseEntryTimestamp((entry as { timestamp?: unknown }).timestamp),
|
|
@@ -5769,6 +6280,12 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
5769
6280
|
if (msg.type === "hello") return { type: "hello" };
|
|
5770
6281
|
if (msg.type === "ping") return { type: "ping" };
|
|
5771
6282
|
if (msg.type === "get_latest_response") return { type: "get_latest_response" };
|
|
6283
|
+
if (msg.type === "get_trace_snapshot" && typeof msg.responseHistoryId === "string") {
|
|
6284
|
+
return {
|
|
6285
|
+
type: "get_trace_snapshot",
|
|
6286
|
+
responseHistoryId: msg.responseHistoryId,
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
5772
6289
|
|
|
5773
6290
|
if (
|
|
5774
6291
|
msg.type === "critique_request" &&
|
|
@@ -6040,6 +6557,68 @@ function createEmptyStudioTraceState(): StudioTraceState {
|
|
|
6040
6557
|
};
|
|
6041
6558
|
}
|
|
6042
6559
|
|
|
6560
|
+
function truncateStudioTraceSnapshotText(text: string, maxChars = STUDIO_TRACE_SNAPSHOT_MAX_FIELD_CHARS): { text: string; truncated: boolean } {
|
|
6561
|
+
const value = String(text ?? "");
|
|
6562
|
+
if (value.length <= maxChars) return { text: value, truncated: false };
|
|
6563
|
+
const keepHead = Math.max(0, Math.floor(maxChars * 0.62));
|
|
6564
|
+
const keepTail = Math.max(0, maxChars - keepHead);
|
|
6565
|
+
const omitted = value.length - keepHead - keepTail;
|
|
6566
|
+
return {
|
|
6567
|
+
text: `${value.slice(0, keepHead)}\n\n… ${omitted} chars omitted from saved Working view …\n\n${value.slice(value.length - keepTail)}`,
|
|
6568
|
+
truncated: true,
|
|
6569
|
+
};
|
|
6570
|
+
}
|
|
6571
|
+
|
|
6572
|
+
function createStudioTraceSnapshot(source: StudioTraceState): { traceState: StudioTraceState; truncated: boolean } {
|
|
6573
|
+
let truncated = false;
|
|
6574
|
+
const sourceEntries = Array.isArray(source.entries) ? source.entries : [];
|
|
6575
|
+
const entries = sourceEntries.slice(-STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES).map((entry) => {
|
|
6576
|
+
if (entry.type === "assistant") {
|
|
6577
|
+
const thinking = truncateStudioTraceSnapshotText(entry.thinking);
|
|
6578
|
+
const text = truncateStudioTraceSnapshotText(entry.text);
|
|
6579
|
+
truncated = truncated || thinking.truncated || text.truncated;
|
|
6580
|
+
return {
|
|
6581
|
+
...entry,
|
|
6582
|
+
thinking: thinking.text,
|
|
6583
|
+
text: text.text,
|
|
6584
|
+
};
|
|
6585
|
+
}
|
|
6586
|
+
const argsSummary = truncateStudioTraceSnapshotText(entry.argsSummary ?? "");
|
|
6587
|
+
const output = truncateStudioTraceSnapshotText(entry.output);
|
|
6588
|
+
truncated = truncated || argsSummary.truncated || output.truncated;
|
|
6589
|
+
return {
|
|
6590
|
+
...entry,
|
|
6591
|
+
argsSummary: argsSummary.text || null,
|
|
6592
|
+
output: output.text,
|
|
6593
|
+
};
|
|
6594
|
+
});
|
|
6595
|
+
if (sourceEntries.length > entries.length) truncated = true;
|
|
6596
|
+
|
|
6597
|
+
return {
|
|
6598
|
+
traceState: {
|
|
6599
|
+
runId: source.runId,
|
|
6600
|
+
requestId: source.requestId,
|
|
6601
|
+
requestKind: source.requestKind,
|
|
6602
|
+
status: source.status,
|
|
6603
|
+
startedAt: source.startedAt,
|
|
6604
|
+
updatedAt: source.updatedAt,
|
|
6605
|
+
entries,
|
|
6606
|
+
},
|
|
6607
|
+
truncated,
|
|
6608
|
+
};
|
|
6609
|
+
}
|
|
6610
|
+
|
|
6611
|
+
function summarizeStudioTraceSnapshot(traceState: StudioTraceState, truncated = false): StudioTraceSnapshotSummary {
|
|
6612
|
+
return {
|
|
6613
|
+
hasTrace: Array.isArray(traceState.entries) && traceState.entries.length > 0,
|
|
6614
|
+
entryCount: Array.isArray(traceState.entries) ? traceState.entries.length : 0,
|
|
6615
|
+
startedAt: traceState.startedAt,
|
|
6616
|
+
updatedAt: traceState.updatedAt,
|
|
6617
|
+
status: traceState.status,
|
|
6618
|
+
truncated,
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
|
|
6043
6622
|
function sanitizeStudioTraceOutputText(text: string): string {
|
|
6044
6623
|
return String(text || "")
|
|
6045
6624
|
.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 +6893,23 @@ function sanitizePdfFilename(input: string | undefined): string {
|
|
|
6314
6893
|
return `${ensuredExt.slice(0, 156)}.pdf`;
|
|
6315
6894
|
}
|
|
6316
6895
|
|
|
6896
|
+
function sanitizeHtmlFilename(input: string | undefined): string {
|
|
6897
|
+
const fallback = "studio-preview.html";
|
|
6898
|
+
const raw = String(input ?? "").trim();
|
|
6899
|
+
if (!raw) return fallback;
|
|
6900
|
+
|
|
6901
|
+
const noPath = raw.split(/[\\/]/).pop() ?? raw;
|
|
6902
|
+
const cleaned = noPath
|
|
6903
|
+
.replace(/[\x00-\x1f\x7f]+/g, "")
|
|
6904
|
+
.replace(/[<>:"|?*]+/g, "-")
|
|
6905
|
+
.trim();
|
|
6906
|
+
if (!cleaned) return fallback;
|
|
6907
|
+
|
|
6908
|
+
const ensuredExt = /\.html?$/i.test(cleaned) ? cleaned : `${cleaned}.html`;
|
|
6909
|
+
if (ensuredExt.length <= 160) return ensuredExt;
|
|
6910
|
+
return `${ensuredExt.slice(0, 155)}.html`;
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6317
6913
|
function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
|
|
6318
6914
|
const shadowColor = style.mode === "light"
|
|
6319
6915
|
? withAlpha(style.palette.text, 0.10, "rgba(15, 23, 42, 0.08)")
|
|
@@ -6753,7 +7349,13 @@ ${cssVarsBlock}
|
|
|
6753
7349
|
</div>
|
|
6754
7350
|
<div class="section-header-actions">
|
|
6755
7351
|
<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
|
-
<
|
|
7352
|
+
<span id="exportPreviewControls" class="export-preview-controls">
|
|
7353
|
+
<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>
|
|
7354
|
+
<div id="exportPreviewMenu" class="export-preview-menu" role="menu" hidden>
|
|
7355
|
+
<button id="exportPreviewPdfBtn" type="button" role="menuitem" data-export-preview-format="pdf">Export as PDF</button>
|
|
7356
|
+
<button id="exportPreviewHtmlBtn" type="button" role="menuitem" data-export-preview-format="html">Export as HTML</button>
|
|
7357
|
+
</div>
|
|
7358
|
+
</span>
|
|
6757
7359
|
</div>
|
|
6758
7360
|
</div>
|
|
6759
7361
|
<div class="reference-meta">
|
|
@@ -6852,6 +7454,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6852
7454
|
let pendingStudioPromptMetadata: StudioPromptDescriptor | null = null;
|
|
6853
7455
|
let lastStudioResponse: LastStudioResponse | null = null;
|
|
6854
7456
|
let preparedPdfExports = new Map<string, PreparedStudioPdfExport>();
|
|
7457
|
+
let preparedHtmlExports = new Map<string, PreparedStudioHtmlExport>();
|
|
6855
7458
|
let initialStudioDocument: InitialStudioDocument | null = null;
|
|
6856
7459
|
let studioCwd = process.cwd();
|
|
6857
7460
|
let lastCommandCtx: ExtensionCommandContext | null = null;
|
|
@@ -6871,6 +7474,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6871
7474
|
let latestSessionUserPrompt: string | null = null;
|
|
6872
7475
|
let pendingTurnPrompt: string | null = null;
|
|
6873
7476
|
let studioTraceState: StudioTraceState = createEmptyStudioTraceState();
|
|
7477
|
+
let studioTraceHistory = new Map<string, { traceState: StudioTraceState; summary: StudioTraceSnapshotSummary }>();
|
|
6874
7478
|
let activeStudioTraceAssistantEntryId: string | null = null;
|
|
6875
7479
|
const studioTraceToolEntryIds = new Map<string, string>();
|
|
6876
7480
|
let contextUsageSnapshot: StudioContextUsageSnapshot = {
|
|
@@ -7222,6 +7826,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
7222
7826
|
notifyStudioTerminal(message, "info");
|
|
7223
7827
|
};
|
|
7224
7828
|
|
|
7829
|
+
const attachStudioTraceSummariesToHistory = (items: StudioResponseHistoryItem[]): StudioResponseHistoryItem[] => items.map((item) => {
|
|
7830
|
+
const stored = studioTraceHistory.get(item.id);
|
|
7831
|
+
return stored ? { ...item, traceSummary: stored.summary } : item;
|
|
7832
|
+
});
|
|
7833
|
+
|
|
7834
|
+
const pruneStudioTraceHistory = () => {
|
|
7835
|
+
const liveIds = new Set(studioResponseHistory.map((item) => item.id));
|
|
7836
|
+
for (const key of Array.from(studioTraceHistory.keys())) {
|
|
7837
|
+
if (!liveIds.has(key)) studioTraceHistory.delete(key);
|
|
7838
|
+
}
|
|
7839
|
+
while (studioTraceHistory.size > MAX_STUDIO_TRACE_SNAPSHOTS) {
|
|
7840
|
+
const oldestKey = studioTraceHistory.keys().next().value;
|
|
7841
|
+
if (!oldestKey) break;
|
|
7842
|
+
studioTraceHistory.delete(oldestKey);
|
|
7843
|
+
}
|
|
7844
|
+
};
|
|
7845
|
+
|
|
7846
|
+
const storeStudioTraceSnapshotForResponse = (responseHistoryId: string | null | undefined): StudioTraceSnapshotSummary | null => {
|
|
7847
|
+
const id = String(responseHistoryId ?? "").trim();
|
|
7848
|
+
if (!id) return null;
|
|
7849
|
+
if (!Array.isArray(studioTraceState.entries) || studioTraceState.entries.length === 0) return null;
|
|
7850
|
+
const snapshot = createStudioTraceSnapshot(studioTraceState);
|
|
7851
|
+
const summary = summarizeStudioTraceSnapshot(snapshot.traceState, snapshot.truncated);
|
|
7852
|
+
if (!summary.hasTrace) return null;
|
|
7853
|
+
studioTraceHistory.set(id, { traceState: snapshot.traceState, summary });
|
|
7854
|
+
studioResponseHistory = studioResponseHistory.map((item) => item.id === id ? { ...item, traceSummary: summary } : item);
|
|
7855
|
+
pruneStudioTraceHistory();
|
|
7856
|
+
return summary;
|
|
7857
|
+
};
|
|
7858
|
+
|
|
7225
7859
|
const refreshContextUsage = (
|
|
7226
7860
|
ctx?: { getContextUsage(): { tokens: number | null; contextWindow: number; percent: number | null } | undefined },
|
|
7227
7861
|
): StudioContextUsageSnapshot => {
|
|
@@ -7239,7 +7873,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7239
7873
|
|
|
7240
7874
|
const syncStudioResponseHistory = (entries: SessionEntry[]) => {
|
|
7241
7875
|
latestSessionUserPrompt = findLatestUserPrompt(entries);
|
|
7242
|
-
studioResponseHistory = buildResponseHistoryFromEntries(entries, RESPONSE_HISTORY_LIMIT);
|
|
7876
|
+
studioResponseHistory = attachStudioTraceSummariesToHistory(buildResponseHistoryFromEntries(entries, RESPONSE_HISTORY_LIMIT));
|
|
7877
|
+
pruneStudioTraceHistory();
|
|
7243
7878
|
const latest = studioResponseHistory[studioResponseHistory.length - 1];
|
|
7244
7879
|
if (!latest) {
|
|
7245
7880
|
lastStudioResponse = null;
|
|
@@ -7865,6 +8500,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7865
8500
|
return;
|
|
7866
8501
|
}
|
|
7867
8502
|
|
|
8503
|
+
if (msg.type === "get_trace_snapshot") {
|
|
8504
|
+
const responseHistoryId = String(msg.responseHistoryId ?? "").trim();
|
|
8505
|
+
const stored = responseHistoryId ? studioTraceHistory.get(responseHistoryId) : null;
|
|
8506
|
+
sendToClient(client, {
|
|
8507
|
+
type: "trace_snapshot",
|
|
8508
|
+
responseHistoryId,
|
|
8509
|
+
traceState: stored?.traceState ?? createEmptyStudioTraceState(),
|
|
8510
|
+
summary: stored?.summary ?? summarizeStudioTraceSnapshot(createEmptyStudioTraceState()),
|
|
8511
|
+
});
|
|
8512
|
+
return;
|
|
8513
|
+
}
|
|
8514
|
+
|
|
7868
8515
|
if (msg.type === "load_git_diff_request") {
|
|
7869
8516
|
if (!isValidRequestId(msg.requestId)) {
|
|
7870
8517
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
@@ -8467,6 +9114,100 @@ export default function (pi: ExtensionAPI) {
|
|
|
8467
9114
|
res.end(prepared.pdf);
|
|
8468
9115
|
};
|
|
8469
9116
|
|
|
9117
|
+
const disposePreparedHtmlExport = (entry: PreparedStudioHtmlExport | null | undefined) => {
|
|
9118
|
+
if (!entry?.tempDirPath) return;
|
|
9119
|
+
void rm(entry.tempDirPath, { recursive: true, force: true }).catch(() => undefined);
|
|
9120
|
+
};
|
|
9121
|
+
|
|
9122
|
+
const clearPreparedHtmlExports = () => {
|
|
9123
|
+
for (const entry of preparedHtmlExports.values()) {
|
|
9124
|
+
disposePreparedHtmlExport(entry);
|
|
9125
|
+
}
|
|
9126
|
+
preparedHtmlExports.clear();
|
|
9127
|
+
};
|
|
9128
|
+
|
|
9129
|
+
const prunePreparedHtmlExports = () => {
|
|
9130
|
+
const now = Date.now();
|
|
9131
|
+
for (const [id, entry] of preparedHtmlExports) {
|
|
9132
|
+
if (entry.createdAt + PREPARED_HTML_EXPORT_TTL_MS <= now) {
|
|
9133
|
+
preparedHtmlExports.delete(id);
|
|
9134
|
+
disposePreparedHtmlExport(entry);
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
while (preparedHtmlExports.size > MAX_PREPARED_HTML_EXPORTS) {
|
|
9138
|
+
const oldestKey = preparedHtmlExports.keys().next().value;
|
|
9139
|
+
if (!oldestKey) break;
|
|
9140
|
+
const oldestEntry = preparedHtmlExports.get(oldestKey);
|
|
9141
|
+
preparedHtmlExports.delete(oldestKey);
|
|
9142
|
+
disposePreparedHtmlExport(oldestEntry);
|
|
9143
|
+
}
|
|
9144
|
+
};
|
|
9145
|
+
|
|
9146
|
+
const storePreparedHtmlExport = (html: Buffer, filename: string, warning?: string): string => {
|
|
9147
|
+
prunePreparedHtmlExports();
|
|
9148
|
+
const exportId = randomUUID();
|
|
9149
|
+
preparedHtmlExports.set(exportId, {
|
|
9150
|
+
html,
|
|
9151
|
+
filename,
|
|
9152
|
+
warning,
|
|
9153
|
+
createdAt: Date.now(),
|
|
9154
|
+
});
|
|
9155
|
+
return exportId;
|
|
9156
|
+
};
|
|
9157
|
+
|
|
9158
|
+
const ensurePreparedHtmlExportFile = async (exportId: string): Promise<PreparedStudioHtmlExport | null> => {
|
|
9159
|
+
prunePreparedHtmlExports();
|
|
9160
|
+
const entry = preparedHtmlExports.get(exportId);
|
|
9161
|
+
if (!entry) return null;
|
|
9162
|
+
if (entry.filePath && entry.tempDirPath) return entry;
|
|
9163
|
+
|
|
9164
|
+
const tempDirPath = join(tmpdir(), `pi-studio-prepared-html-${Date.now()}-${randomUUID()}`);
|
|
9165
|
+
const filePath = join(tempDirPath, sanitizeHtmlFilename(entry.filename));
|
|
9166
|
+
await mkdir(tempDirPath, { recursive: true });
|
|
9167
|
+
await writeFile(filePath, entry.html);
|
|
9168
|
+
entry.tempDirPath = tempDirPath;
|
|
9169
|
+
entry.filePath = filePath;
|
|
9170
|
+
preparedHtmlExports.set(exportId, entry);
|
|
9171
|
+
return entry;
|
|
9172
|
+
};
|
|
9173
|
+
|
|
9174
|
+
const getPreparedHtmlExport = (exportId: string): PreparedStudioHtmlExport | null => {
|
|
9175
|
+
prunePreparedHtmlExports();
|
|
9176
|
+
return preparedHtmlExports.get(exportId) ?? null;
|
|
9177
|
+
};
|
|
9178
|
+
|
|
9179
|
+
const handlePreparedHtmlDownloadRequest = (requestUrl: URL, res: ServerResponse) => {
|
|
9180
|
+
const exportId = requestUrl.searchParams.get("id") ?? "";
|
|
9181
|
+
if (!exportId) {
|
|
9182
|
+
respondText(res, 400, "Missing HTML export id.");
|
|
9183
|
+
return;
|
|
9184
|
+
}
|
|
9185
|
+
|
|
9186
|
+
const prepared = getPreparedHtmlExport(exportId);
|
|
9187
|
+
if (!prepared) {
|
|
9188
|
+
respondText(res, 404, "HTML export is no longer available. Re-export the document.");
|
|
9189
|
+
return;
|
|
9190
|
+
}
|
|
9191
|
+
|
|
9192
|
+
const safeAsciiName = prepared.filename
|
|
9193
|
+
.replace(/[\x00-\x1f\x7f]/g, "")
|
|
9194
|
+
.replace(/[;"\\]/g, "_")
|
|
9195
|
+
.replace(/\s+/g, " ")
|
|
9196
|
+
.trim() || "studio-preview.html";
|
|
9197
|
+
|
|
9198
|
+
const headers: Record<string, string> = {
|
|
9199
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9200
|
+
"Cache-Control": "no-store",
|
|
9201
|
+
"X-Content-Type-Options": "nosniff",
|
|
9202
|
+
"Content-Disposition": `inline; filename="${safeAsciiName}"; filename*=UTF-8''${encodeURIComponent(prepared.filename)}`,
|
|
9203
|
+
"Content-Length": String(prepared.html.length),
|
|
9204
|
+
};
|
|
9205
|
+
if (prepared.warning) headers["X-Pi-Studio-Export-Warning"] = prepared.warning;
|
|
9206
|
+
|
|
9207
|
+
res.writeHead(200, headers);
|
|
9208
|
+
res.end(prepared.html);
|
|
9209
|
+
};
|
|
9210
|
+
|
|
8470
9211
|
const handleScratchpadStateRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
8471
9212
|
const method = (req.method ?? "GET").toUpperCase();
|
|
8472
9213
|
if (method === "GET") {
|
|
@@ -8784,6 +9525,117 @@ export default function (pi: ExtensionAPI) {
|
|
|
8784
9525
|
}
|
|
8785
9526
|
};
|
|
8786
9527
|
|
|
9528
|
+
const handleExportHtmlRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
9529
|
+
let rawBody = "";
|
|
9530
|
+
try {
|
|
9531
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
9532
|
+
} catch (error) {
|
|
9533
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9534
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
9535
|
+
respondJson(res, status, { ok: false, error: message });
|
|
9536
|
+
return;
|
|
9537
|
+
}
|
|
9538
|
+
|
|
9539
|
+
let parsedBody: unknown;
|
|
9540
|
+
try {
|
|
9541
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
9542
|
+
} catch {
|
|
9543
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
9544
|
+
return;
|
|
9545
|
+
}
|
|
9546
|
+
|
|
9547
|
+
const markdown =
|
|
9548
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { markdown?: unknown }).markdown === "string"
|
|
9549
|
+
? (parsedBody as { markdown: string }).markdown
|
|
9550
|
+
: null;
|
|
9551
|
+
if (markdown === null) {
|
|
9552
|
+
respondJson(res, 400, { ok: false, error: "Missing markdown string in request body." });
|
|
9553
|
+
return;
|
|
9554
|
+
}
|
|
9555
|
+
|
|
9556
|
+
if (markdown.length > HTML_EXPORT_MAX_CHARS) {
|
|
9557
|
+
respondJson(res, 413, {
|
|
9558
|
+
ok: false,
|
|
9559
|
+
error: `HTML export text exceeds ${HTML_EXPORT_MAX_CHARS} characters.`,
|
|
9560
|
+
});
|
|
9561
|
+
return;
|
|
9562
|
+
}
|
|
9563
|
+
|
|
9564
|
+
const sourcePath =
|
|
9565
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { sourcePath?: unknown }).sourcePath === "string"
|
|
9566
|
+
? (parsedBody as { sourcePath: string }).sourcePath
|
|
9567
|
+
: "";
|
|
9568
|
+
const userResourceDir =
|
|
9569
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
|
|
9570
|
+
? (parsedBody as { resourceDir: string }).resourceDir
|
|
9571
|
+
: "";
|
|
9572
|
+
const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
|
|
9573
|
+
const requestedIsLatex =
|
|
9574
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { isLatex?: unknown }).isLatex === "boolean"
|
|
9575
|
+
? (parsedBody as { isLatex: boolean }).isLatex
|
|
9576
|
+
: null;
|
|
9577
|
+
const requestedFilename =
|
|
9578
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { filenameHint?: unknown }).filenameHint === "string"
|
|
9579
|
+
? (parsedBody as { filenameHint: string }).filenameHint
|
|
9580
|
+
: "";
|
|
9581
|
+
const requestedTitle =
|
|
9582
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { title?: unknown }).title === "string"
|
|
9583
|
+
? (parsedBody as { title: string }).title
|
|
9584
|
+
: "";
|
|
9585
|
+
const requestedEditorHtmlLanguage =
|
|
9586
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorHtmlLanguage?: unknown }).editorHtmlLanguage === "string"
|
|
9587
|
+
? (parsedBody as { editorHtmlLanguage: string }).editorHtmlLanguage
|
|
9588
|
+
: "";
|
|
9589
|
+
const editorHtmlLanguage = inferStudioPdfLanguage(markdown, requestedEditorHtmlLanguage);
|
|
9590
|
+
const isLatex = editorHtmlLanguage === "latex"
|
|
9591
|
+
|| (
|
|
9592
|
+
(editorHtmlLanguage === undefined || editorHtmlLanguage === "markdown")
|
|
9593
|
+
&& (requestedIsLatex ?? isLikelyStandaloneLatexPreview(markdown))
|
|
9594
|
+
);
|
|
9595
|
+
const filename = sanitizeHtmlFilename(requestedFilename || (isLatex ? "studio-latex-preview.html" : "studio-preview.html"));
|
|
9596
|
+
const themeVars = parseStudioThemeVarsJson(lastThemeVarsJson) ?? buildThemeCssVars(getStudioThemeStyle(lastCommandCtx?.ui?.theme));
|
|
9597
|
+
|
|
9598
|
+
try {
|
|
9599
|
+
const { html, warning } = await renderStudioStandaloneHtmlWithPandoc(
|
|
9600
|
+
markdown,
|
|
9601
|
+
isLatex,
|
|
9602
|
+
resourcePath,
|
|
9603
|
+
editorHtmlLanguage,
|
|
9604
|
+
sourcePath || undefined,
|
|
9605
|
+
{
|
|
9606
|
+
title: requestedTitle || filename,
|
|
9607
|
+
sourceLabel: sourcePath || userResourceDir || "right preview",
|
|
9608
|
+
themeVars,
|
|
9609
|
+
},
|
|
9610
|
+
);
|
|
9611
|
+
const exportId = storePreparedHtmlExport(html, filename, warning);
|
|
9612
|
+
const token = serverState?.token ?? "";
|
|
9613
|
+
let openedExternal = false;
|
|
9614
|
+
let openError: string | null = null;
|
|
9615
|
+
try {
|
|
9616
|
+
const prepared = await ensurePreparedHtmlExportFile(exportId);
|
|
9617
|
+
if (!prepared?.filePath) {
|
|
9618
|
+
throw new Error("Prepared HTML file was not available for external open.");
|
|
9619
|
+
}
|
|
9620
|
+
await openPathInDefaultViewer(prepared.filePath);
|
|
9621
|
+
openedExternal = true;
|
|
9622
|
+
} catch (viewerError) {
|
|
9623
|
+
openError = viewerError instanceof Error ? viewerError.message : String(viewerError);
|
|
9624
|
+
}
|
|
9625
|
+
respondJson(res, 200, {
|
|
9626
|
+
ok: true,
|
|
9627
|
+
filename,
|
|
9628
|
+
warning: warning ?? null,
|
|
9629
|
+
openedExternal,
|
|
9630
|
+
openError,
|
|
9631
|
+
downloadUrl: `/export-html?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
9632
|
+
});
|
|
9633
|
+
} catch (error) {
|
|
9634
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9635
|
+
respondJson(res, 500, { ok: false, error: `HTML export failed: ${message}` });
|
|
9636
|
+
}
|
|
9637
|
+
};
|
|
9638
|
+
|
|
8787
9639
|
const handleHttpRequest = (req: IncomingMessage, res: ServerResponse) => {
|
|
8788
9640
|
if (!serverState) {
|
|
8789
9641
|
respondText(res, 503, "Studio server not ready");
|
|
@@ -8975,6 +9827,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
8975
9827
|
return;
|
|
8976
9828
|
}
|
|
8977
9829
|
|
|
9830
|
+
if (requestUrl.pathname === "/export-html") {
|
|
9831
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
9832
|
+
if (token !== serverState.token) {
|
|
9833
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
9834
|
+
if (method === "GET") {
|
|
9835
|
+
respondText(res, 403, "Invalid or expired studio token. Re-run /studio.");
|
|
9836
|
+
} else {
|
|
9837
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
9838
|
+
}
|
|
9839
|
+
return;
|
|
9840
|
+
}
|
|
9841
|
+
|
|
9842
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
9843
|
+
if (method === "GET") {
|
|
9844
|
+
handlePreparedHtmlDownloadRequest(requestUrl, res);
|
|
9845
|
+
return;
|
|
9846
|
+
}
|
|
9847
|
+
if (method !== "POST") {
|
|
9848
|
+
res.setHeader("Allow", "GET, POST");
|
|
9849
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use GET or POST." });
|
|
9850
|
+
return;
|
|
9851
|
+
}
|
|
9852
|
+
|
|
9853
|
+
void handleExportHtmlRequest(req, res).catch((error) => {
|
|
9854
|
+
respondJson(res, 500, {
|
|
9855
|
+
ok: false,
|
|
9856
|
+
error: `HTML export failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
9857
|
+
});
|
|
9858
|
+
});
|
|
9859
|
+
return;
|
|
9860
|
+
}
|
|
9861
|
+
|
|
8978
9862
|
if (requestUrl.pathname === "/pdf-resource") {
|
|
8979
9863
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
8980
9864
|
if (token !== serverState.token) {
|
|
@@ -9174,6 +10058,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9174
10058
|
clearActiveRequest();
|
|
9175
10059
|
clearPendingStudioCompletion();
|
|
9176
10060
|
clearPreparedPdfExports();
|
|
10061
|
+
clearPreparedHtmlExports();
|
|
9177
10062
|
clearCompactionState();
|
|
9178
10063
|
closeAllClients(1001, "Server shutting down");
|
|
9179
10064
|
|
|
@@ -9199,6 +10084,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9199
10084
|
clearStudioDirectRunState();
|
|
9200
10085
|
if (isSessionReplacement) {
|
|
9201
10086
|
clearActiveRequest({ notify: "Session switched. Studio request state cleared.", level: "warning" });
|
|
10087
|
+
studioTraceHistory.clear();
|
|
9202
10088
|
lastCommandCtx = null;
|
|
9203
10089
|
}
|
|
9204
10090
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
@@ -9206,6 +10092,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9206
10092
|
agentBusy = false;
|
|
9207
10093
|
clearPendingStudioCompletion();
|
|
9208
10094
|
clearPreparedPdfExports();
|
|
10095
|
+
clearPreparedHtmlExports();
|
|
9209
10096
|
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
9210
10097
|
refreshContextUsage(ctx);
|
|
9211
10098
|
emitDebugEvent("session_start", {
|
|
@@ -9382,7 +10269,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9382
10269
|
? getPromptDescriptorForActiveRequest(activeRequest)
|
|
9383
10270
|
: buildStudioPromptDescriptor(pendingTurnPrompt ?? latestSessionUserPrompt ?? null);
|
|
9384
10271
|
const fallbackHistoryItem: StudioResponseHistoryItem = {
|
|
9385
|
-
id:
|
|
10272
|
+
id: buildNextStudioResponseHistoryId(markdown, studioResponseHistory),
|
|
9386
10273
|
markdown,
|
|
9387
10274
|
thinking,
|
|
9388
10275
|
timestamp: Date.now(),
|
|
@@ -9401,6 +10288,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
9401
10288
|
const responseTimestamp = latestItem?.timestamp ?? Date.now();
|
|
9402
10289
|
const responseThinking = latestItem?.thinking ?? thinking ?? null;
|
|
9403
10290
|
pendingTurnPrompt = null;
|
|
10291
|
+
setStudioTraceRunStatus("complete");
|
|
10292
|
+
if (latestItem) {
|
|
10293
|
+
storeStudioTraceSnapshotForResponse(latestItem.id);
|
|
10294
|
+
}
|
|
9404
10295
|
|
|
9405
10296
|
if (activeRequest) {
|
|
9406
10297
|
const requestId = activeRequest.id;
|
|
@@ -9498,6 +10389,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
9498
10389
|
clearStudioDirectRunState();
|
|
9499
10390
|
clearPendingStudioCompletion();
|
|
9500
10391
|
clearPreparedPdfExports();
|
|
10392
|
+
clearPreparedHtmlExports();
|
|
10393
|
+
studioTraceHistory.clear();
|
|
9501
10394
|
transientStudioDocuments.clear();
|
|
9502
10395
|
clearCompactionState();
|
|
9503
10396
|
clearStudioTrace();
|
|
@@ -9594,6 +10487,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
9594
10487
|
};
|
|
9595
10488
|
};
|
|
9596
10489
|
|
|
10490
|
+
const resolveLastModelResponseForExport = (ctx: ExtensionCommandContext): { markdown: string } | null => {
|
|
10491
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
10492
|
+
syncStudioResponseHistory(branchEntries);
|
|
10493
|
+
const markdown =
|
|
10494
|
+
extractLatestAssistantFromEntries(branchEntries)
|
|
10495
|
+
?? extractLatestAssistantFromEntries(ctx.sessionManager.getEntries())
|
|
10496
|
+
?? lastStudioResponse?.markdown
|
|
10497
|
+
?? "";
|
|
10498
|
+
return markdown.trim() ? { markdown } : null;
|
|
10499
|
+
};
|
|
10500
|
+
|
|
9597
10501
|
const openStudioView = async (
|
|
9598
10502
|
trimmed: string,
|
|
9599
10503
|
ctx: ExtensionCommandContext,
|
|
@@ -9700,7 +10604,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
9700
10604
|
+ " /studio-replace [path] Replace the current full Studio view with a new one\n"
|
|
9701
10605
|
+ " /studio-editor-only [path] Open another Studio tab in editor-only mode\n"
|
|
9702
10606
|
+ " /studio-current <path> Load a file into currently open Studio tab(s)\n"
|
|
9703
|
-
+ " /studio-pdf
|
|
10607
|
+
+ " /studio-pdf [path] Export a file or last response via Studio PDF\n"
|
|
10608
|
+
+ " /studio-html [path] Export a file or last response via Studio preview HTML",
|
|
9704
10609
|
"info",
|
|
9705
10610
|
);
|
|
9706
10611
|
return;
|
|
@@ -9757,13 +10662,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
9757
10662
|
});
|
|
9758
10663
|
|
|
9759
10664
|
pi.registerCommand("studio-pdf", {
|
|
9760
|
-
description: "Export a file to PDF via the Studio PDF pipeline (/studio-pdf
|
|
10665
|
+
description: "Export a file or the last model response to PDF via the Studio PDF pipeline (/studio-pdf [file])",
|
|
9761
10666
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
9762
10667
|
const trimmed = args.trim();
|
|
9763
|
-
if (
|
|
10668
|
+
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
9764
10669
|
ctx.ui.notify(
|
|
9765
|
-
"Usage: /studio-pdf
|
|
9766
|
-
+ "
|
|
10670
|
+
"Usage: /studio-pdf [path] [options]\n"
|
|
10671
|
+
+ " Without a path, export the last model response to studio-response-<timestamp>.studio.pdf.\n"
|
|
10672
|
+
+ " With a path, export a local Markdown/LaTeX/code file to <name>.studio.pdf using the Studio PDF pipeline.\n"
|
|
9767
10673
|
+ "Options:\n"
|
|
9768
10674
|
+ " --fontsize <value> e.g. 12pt\n"
|
|
9769
10675
|
+ " --section-size <value> e.g. 24pt\n"
|
|
@@ -9796,6 +10702,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
9796
10702
|
}
|
|
9797
10703
|
const { pathArg, options: pdfOptions } = parsedArgs;
|
|
9798
10704
|
|
|
10705
|
+
if (!pathArg) {
|
|
10706
|
+
await ctx.waitForIdle();
|
|
10707
|
+
const response = resolveLastModelResponseForExport(ctx);
|
|
10708
|
+
if (!response) {
|
|
10709
|
+
ctx.ui.notify("No last model response to export. Use /studio-pdf <path> or run a prompt first.", "warning");
|
|
10710
|
+
return;
|
|
10711
|
+
}
|
|
10712
|
+
if (response.markdown.length > PDF_EXPORT_MAX_CHARS) {
|
|
10713
|
+
ctx.ui.notify(`PDF export text exceeds ${PDF_EXPORT_MAX_CHARS} characters.`, "error");
|
|
10714
|
+
return;
|
|
10715
|
+
}
|
|
10716
|
+
|
|
10717
|
+
const editorPdfLanguage = inferStudioPdfLanguage(response.markdown);
|
|
10718
|
+
const isLatex = editorPdfLanguage === "latex"
|
|
10719
|
+
|| (
|
|
10720
|
+
(editorPdfLanguage === undefined || editorPdfLanguage === "markdown")
|
|
10721
|
+
&& /\\documentclass\b|\\begin\{document\}/.test(response.markdown)
|
|
10722
|
+
);
|
|
10723
|
+
const resourcePath = resolveStudioBaseDir(undefined, undefined, ctx.cwd);
|
|
10724
|
+
const outputPath = buildStudioResponseExportOutputPath(ctx.cwd, "pdf");
|
|
10725
|
+
|
|
10726
|
+
try {
|
|
10727
|
+
const { pdf, warning } = await renderStudioPdfWithPandoc(
|
|
10728
|
+
response.markdown,
|
|
10729
|
+
isLatex,
|
|
10730
|
+
resourcePath,
|
|
10731
|
+
editorPdfLanguage,
|
|
10732
|
+
undefined,
|
|
10733
|
+
pdfOptions,
|
|
10734
|
+
);
|
|
10735
|
+
await writeFile(outputPath, pdf);
|
|
10736
|
+
|
|
10737
|
+
let openError: string | null = null;
|
|
10738
|
+
try {
|
|
10739
|
+
await openPathInDefaultViewer(outputPath);
|
|
10740
|
+
} catch (error) {
|
|
10741
|
+
openError = error instanceof Error ? error.message : String(error);
|
|
10742
|
+
}
|
|
10743
|
+
|
|
10744
|
+
ctx.ui.notify(`Exported last response Studio PDF: ${outputPath}`, "info");
|
|
10745
|
+
if (warning) {
|
|
10746
|
+
ctx.ui.notify(warning, "warning");
|
|
10747
|
+
}
|
|
10748
|
+
if (openError) {
|
|
10749
|
+
ctx.ui.notify(`PDF was exported but could not be opened automatically: ${openError}`, "warning");
|
|
10750
|
+
}
|
|
10751
|
+
} catch (error) {
|
|
10752
|
+
ctx.ui.notify(
|
|
10753
|
+
`Studio PDF export failed for last response: ${error instanceof Error ? error.message : String(error)}`,
|
|
10754
|
+
"error",
|
|
10755
|
+
);
|
|
10756
|
+
}
|
|
10757
|
+
return;
|
|
10758
|
+
}
|
|
10759
|
+
|
|
9799
10760
|
const file = readStudioFile(pathArg, ctx.cwd);
|
|
9800
10761
|
if (file.ok === false) {
|
|
9801
10762
|
ctx.ui.notify(file.message, "error");
|
|
@@ -9853,6 +10814,148 @@ export default function (pi: ExtensionAPI) {
|
|
|
9853
10814
|
},
|
|
9854
10815
|
});
|
|
9855
10816
|
|
|
10817
|
+
pi.registerCommand("studio-html", {
|
|
10818
|
+
description: "Export a file or the last model response to standalone HTML via the Studio preview pipeline (/studio-html [file])",
|
|
10819
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
10820
|
+
const trimmed = args.trim();
|
|
10821
|
+
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
10822
|
+
ctx.ui.notify(
|
|
10823
|
+
"Usage: /studio-html [path]\n"
|
|
10824
|
+
+ " Without a path, export the last model response to studio-response-<timestamp>.studio.html.\n"
|
|
10825
|
+
+ " With a path, export a local Markdown/LaTeX/code file to <name>.studio.html using the Studio preview HTML pipeline.",
|
|
10826
|
+
"info",
|
|
10827
|
+
);
|
|
10828
|
+
return;
|
|
10829
|
+
}
|
|
10830
|
+
|
|
10831
|
+
if (!trimmed) {
|
|
10832
|
+
await ctx.waitForIdle();
|
|
10833
|
+
const response = resolveLastModelResponseForExport(ctx);
|
|
10834
|
+
if (!response) {
|
|
10835
|
+
ctx.ui.notify("No last model response to export. Use /studio-html <path> or run a prompt first.", "warning");
|
|
10836
|
+
return;
|
|
10837
|
+
}
|
|
10838
|
+
if (response.markdown.length > HTML_EXPORT_MAX_CHARS) {
|
|
10839
|
+
ctx.ui.notify(`HTML export text exceeds ${HTML_EXPORT_MAX_CHARS} characters.`, "error");
|
|
10840
|
+
return;
|
|
10841
|
+
}
|
|
10842
|
+
|
|
10843
|
+
const editorHtmlLanguage = inferStudioPdfLanguage(response.markdown);
|
|
10844
|
+
const isLatex = editorHtmlLanguage === "latex"
|
|
10845
|
+
|| (
|
|
10846
|
+
(editorHtmlLanguage === undefined || editorHtmlLanguage === "markdown")
|
|
10847
|
+
&& isLikelyStandaloneLatexPreview(response.markdown)
|
|
10848
|
+
);
|
|
10849
|
+
const resourcePath = resolveStudioBaseDir(undefined, undefined, ctx.cwd);
|
|
10850
|
+
const outputPath = buildStudioResponseExportOutputPath(ctx.cwd, "html");
|
|
10851
|
+
const themeVars = buildThemeCssVars(getStudioThemeStyle(ctx.ui.theme));
|
|
10852
|
+
|
|
10853
|
+
try {
|
|
10854
|
+
const { html, warning } = await renderStudioStandaloneHtmlWithPandoc(
|
|
10855
|
+
response.markdown,
|
|
10856
|
+
isLatex,
|
|
10857
|
+
resourcePath,
|
|
10858
|
+
editorHtmlLanguage,
|
|
10859
|
+
undefined,
|
|
10860
|
+
{
|
|
10861
|
+
title: basename(outputPath),
|
|
10862
|
+
sourceLabel: "last model response",
|
|
10863
|
+
themeVars,
|
|
10864
|
+
},
|
|
10865
|
+
);
|
|
10866
|
+
await writeFile(outputPath, html);
|
|
10867
|
+
|
|
10868
|
+
let openError: string | null = null;
|
|
10869
|
+
try {
|
|
10870
|
+
await openPathInDefaultViewer(outputPath);
|
|
10871
|
+
} catch (error) {
|
|
10872
|
+
openError = error instanceof Error ? error.message : String(error);
|
|
10873
|
+
}
|
|
10874
|
+
|
|
10875
|
+
ctx.ui.notify(`Exported last response Studio HTML: ${outputPath}`, "info");
|
|
10876
|
+
if (warning) {
|
|
10877
|
+
ctx.ui.notify(warning, "warning");
|
|
10878
|
+
}
|
|
10879
|
+
if (openError) {
|
|
10880
|
+
ctx.ui.notify(`HTML was exported but could not be opened automatically: ${openError}`, "warning");
|
|
10881
|
+
}
|
|
10882
|
+
} catch (error) {
|
|
10883
|
+
ctx.ui.notify(
|
|
10884
|
+
`Studio HTML export failed for last response: ${error instanceof Error ? error.message : String(error)}`,
|
|
10885
|
+
"error",
|
|
10886
|
+
);
|
|
10887
|
+
}
|
|
10888
|
+
return;
|
|
10889
|
+
}
|
|
10890
|
+
|
|
10891
|
+
const pathArg = parsePathArgument(trimmed);
|
|
10892
|
+
if (!pathArg) {
|
|
10893
|
+
ctx.ui.notify("Invalid file path argument.", "error");
|
|
10894
|
+
return;
|
|
10895
|
+
}
|
|
10896
|
+
|
|
10897
|
+
const file = readStudioFile(pathArg, ctx.cwd);
|
|
10898
|
+
if (file.ok === false) {
|
|
10899
|
+
ctx.ui.notify(file.message, "error");
|
|
10900
|
+
return;
|
|
10901
|
+
}
|
|
10902
|
+
|
|
10903
|
+
if (file.text.length > HTML_EXPORT_MAX_CHARS) {
|
|
10904
|
+
ctx.ui.notify(`HTML export text exceeds ${HTML_EXPORT_MAX_CHARS} characters.`, "error");
|
|
10905
|
+
return;
|
|
10906
|
+
}
|
|
10907
|
+
|
|
10908
|
+
await ctx.waitForIdle();
|
|
10909
|
+
const pathHtmlLanguage = inferStudioPdfLanguageFromPath(file.resolvedPath);
|
|
10910
|
+
const editorHtmlLanguage = pathHtmlLanguage ?? inferStudioPdfLanguage(file.text);
|
|
10911
|
+
const isLatex = editorHtmlLanguage === "latex"
|
|
10912
|
+
|| (
|
|
10913
|
+
!pathHtmlLanguage
|
|
10914
|
+
&& (editorHtmlLanguage === undefined || editorHtmlLanguage === "markdown")
|
|
10915
|
+
&& isLikelyStandaloneLatexPreview(file.text)
|
|
10916
|
+
);
|
|
10917
|
+
const resourcePath = resolveStudioBaseDir(file.resolvedPath, undefined, ctx.cwd);
|
|
10918
|
+
const outputPath = buildStudioHtmlOutputPath(file.resolvedPath);
|
|
10919
|
+
const themeVars = buildThemeCssVars(getStudioThemeStyle(ctx.ui.theme));
|
|
10920
|
+
|
|
10921
|
+
try {
|
|
10922
|
+
const { html, warning } = await renderStudioStandaloneHtmlWithPandoc(
|
|
10923
|
+
file.text,
|
|
10924
|
+
isLatex,
|
|
10925
|
+
resourcePath,
|
|
10926
|
+
editorHtmlLanguage,
|
|
10927
|
+
file.resolvedPath,
|
|
10928
|
+
{
|
|
10929
|
+
title: basename(outputPath),
|
|
10930
|
+
sourceLabel: file.resolvedPath,
|
|
10931
|
+
themeVars,
|
|
10932
|
+
},
|
|
10933
|
+
);
|
|
10934
|
+
await writeFile(outputPath, html);
|
|
10935
|
+
|
|
10936
|
+
let openError: string | null = null;
|
|
10937
|
+
try {
|
|
10938
|
+
await openPathInDefaultViewer(outputPath);
|
|
10939
|
+
} catch (error) {
|
|
10940
|
+
openError = error instanceof Error ? error.message : String(error);
|
|
10941
|
+
}
|
|
10942
|
+
|
|
10943
|
+
ctx.ui.notify(`Exported Studio HTML: ${outputPath}`, "info");
|
|
10944
|
+
if (warning) {
|
|
10945
|
+
ctx.ui.notify(warning, "warning");
|
|
10946
|
+
}
|
|
10947
|
+
if (openError) {
|
|
10948
|
+
ctx.ui.notify(`HTML was exported but could not be opened automatically: ${openError}`, "warning");
|
|
10949
|
+
}
|
|
10950
|
+
} catch (error) {
|
|
10951
|
+
ctx.ui.notify(
|
|
10952
|
+
`Studio HTML export failed for ${file.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
10953
|
+
"error",
|
|
10954
|
+
);
|
|
10955
|
+
}
|
|
10956
|
+
},
|
|
10957
|
+
});
|
|
10958
|
+
|
|
9856
10959
|
pi.registerCommand("studio-current", {
|
|
9857
10960
|
description: "Load a file into current open Studio tab(s) without opening a new browser session",
|
|
9858
10961
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|