pi-studio 0.9.47 → 0.9.49
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 +19 -0
- package/README.md +10 -4
- package/client/studio-client.js +847 -82
- package/client/studio-preview-resource-helpers.js +52 -0
- package/client/studio.css +144 -17
- package/index.ts +348 -89
- package/package.json +1 -1
- package/shared/studio-latex-pandoc-compat.js +122 -0
- package/shared/studio-local-preview-path.js +46 -0
package/index.ts
CHANGED
|
@@ -28,8 +28,10 @@ import {
|
|
|
28
28
|
preserveLiteralLatexCommandsInMarkdown,
|
|
29
29
|
} from "./shared/studio-markdown-latex-literals.js";
|
|
30
30
|
import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
|
|
31
|
+
import { parseStudioLocalPreviewPage, parseStudioPdfLaunchTarget } from "./shared/studio-local-preview-path.js";
|
|
31
32
|
import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
|
|
32
33
|
import { createStudioPandocHtmlResourceFlagResolver } from "./shared/studio-pandoc-resource-flag.js";
|
|
34
|
+
import { prepareStudioLatexForPandoc } from "./shared/studio-latex-pandoc-compat.js";
|
|
33
35
|
import { isStudioCmuxSession, openStudioUrlInBrowser } from "./shared/studio-browser-launcher.js";
|
|
34
36
|
import { buildStudioReplTmuxStartArgs } from "./shared/studio-repl-tmux.js";
|
|
35
37
|
import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
|
|
@@ -275,6 +277,21 @@ interface InitialStudioDocument {
|
|
|
275
277
|
resourceDir?: string;
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
interface StudioLaunchSelection {
|
|
281
|
+
document: InitialStudioDocument;
|
|
282
|
+
kind: "document" | "pdf-preview";
|
|
283
|
+
mode?: StudioUiMode;
|
|
284
|
+
transient?: boolean;
|
|
285
|
+
skipWorkspaceRestore?: boolean;
|
|
286
|
+
paneFocus?: "left" | "right";
|
|
287
|
+
resourcePath?: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface StudioUrlOptions {
|
|
291
|
+
skipWorkspaceRestore?: boolean;
|
|
292
|
+
paneFocus?: "left" | "right";
|
|
293
|
+
}
|
|
294
|
+
|
|
278
295
|
type PersistedStudioReviewNoteAnchorKind = "source" | "html-selection" | "html-element" | "html-page";
|
|
279
296
|
|
|
280
297
|
interface PersistedStudioReviewNote {
|
|
@@ -648,6 +665,7 @@ const STUDIO_QUIZ_CONTEXT_MAX_FILES = 18;
|
|
|
648
665
|
const STUDIO_QUIZ_SNIPPET_MAX_CHARS = 8_000;
|
|
649
666
|
const STUDIO_QUIZ_DISCUSSION_MAX_CHARS = 6_000;
|
|
650
667
|
const REQUEST_BODY_MAX_BYTES = 1_000_000;
|
|
668
|
+
const STUDIO_IMPORT_FILE_MAX_BYTES = 10_000_000;
|
|
651
669
|
const STUDIO_WORKSPACE_STATE_REQUEST_MAX_BYTES = 4_000_000;
|
|
652
670
|
const RESPONSE_HISTORY_LIMIT = 30;
|
|
653
671
|
const CMUX_NOTIFY_TIMEOUT_MS = 1200;
|
|
@@ -2947,35 +2965,6 @@ function decodeStudioHtmlPreviewResourcePath(resourcePath: string): string {
|
|
|
2947
2965
|
}
|
|
2948
2966
|
}
|
|
2949
2967
|
|
|
2950
|
-
function parseStudioLocalPreviewResourcePage(resourcePath: string): number | null {
|
|
2951
|
-
const raw = String(resourcePath || "");
|
|
2952
|
-
const parts: string[] = [];
|
|
2953
|
-
const queryIndex = raw.indexOf("?");
|
|
2954
|
-
if (queryIndex >= 0) {
|
|
2955
|
-
const queryEnd = raw.indexOf("#", queryIndex);
|
|
2956
|
-
parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
|
|
2957
|
-
}
|
|
2958
|
-
const hashIndex = raw.indexOf("#");
|
|
2959
|
-
if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
|
|
2960
|
-
for (const part of parts) {
|
|
2961
|
-
try {
|
|
2962
|
-
const params = new URLSearchParams(part);
|
|
2963
|
-
const rawPage = params.get("page") || params.get("p");
|
|
2964
|
-
if (rawPage) {
|
|
2965
|
-
const page = Number.parseInt(rawPage, 10);
|
|
2966
|
-
if (Number.isFinite(page) && page > 0) return page;
|
|
2967
|
-
}
|
|
2968
|
-
} catch {
|
|
2969
|
-
const match = part.match(/(?:^|[&;])page=(\d+)/i) || part.match(/^page=(\d+)$/i);
|
|
2970
|
-
if (match && match[1]) {
|
|
2971
|
-
const page = Number.parseInt(match[1], 10);
|
|
2972
|
-
if (Number.isFinite(page) && page > 0) return page;
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
return null;
|
|
2977
|
-
}
|
|
2978
|
-
|
|
2979
2968
|
function getStudioLocalPreviewResourceKind(extension: string, filePathOrName?: string): StudioLocalPreviewResourceKind {
|
|
2980
2969
|
const ext = extension.toLowerCase();
|
|
2981
2970
|
const name = basename(String(filePathOrName || "")).toLowerCase();
|
|
@@ -3020,7 +3009,7 @@ function resolveStudioLocalPreviewResourcePath(
|
|
|
3020
3009
|
label: rel && rel !== "" ? rel : basename(candidateReal),
|
|
3021
3010
|
extension,
|
|
3022
3011
|
kind: getStudioLocalPreviewResourceKind(extension, candidateReal),
|
|
3023
|
-
page:
|
|
3012
|
+
page: parseStudioLocalPreviewPage(rawPath),
|
|
3024
3013
|
resourceDir: boundaryReal,
|
|
3025
3014
|
};
|
|
3026
3015
|
}
|
|
@@ -6127,6 +6116,31 @@ function decorateStudioPandocSyntaxHtml(html: string): string {
|
|
|
6127
6116
|
);
|
|
6128
6117
|
}
|
|
6129
6118
|
|
|
6119
|
+
function buildStudioLatexPandocCompatibilityWarning(omittedPackages: Array<{ name: string }>): string | undefined {
|
|
6120
|
+
const packageNames = Array.from(new Set(
|
|
6121
|
+
omittedPackages
|
|
6122
|
+
.map((entry) => String(entry?.name ?? "").trim())
|
|
6123
|
+
.filter(Boolean),
|
|
6124
|
+
));
|
|
6125
|
+
if (packageNames.length === 0) return undefined;
|
|
6126
|
+
const packageLabel = packageNames
|
|
6127
|
+
.map((name) => name.toLowerCase().endsWith(".sty") ? name : `${name}.sty`)
|
|
6128
|
+
.join(", ");
|
|
6129
|
+
const noun = packageNames.length === 1 ? "package" : "packages";
|
|
6130
|
+
const subject = packageNames.length === 1 ? "it redefines" : "they redefine";
|
|
6131
|
+
return `Studio omitted local LaTeX ${noun} ${packageLabel} from this Pandoc rendering because ${subject} document startup. Package-specific layout may be absent; compile the source directly with LaTeX for authoritative output.`;
|
|
6132
|
+
}
|
|
6133
|
+
|
|
6134
|
+
function combineStudioWarnings(...warnings: Array<string | undefined>): string | undefined {
|
|
6135
|
+
const messages = Array.from(new Set(warnings.map((warning) => String(warning ?? "").trim()).filter(Boolean)));
|
|
6136
|
+
return messages.length > 0 ? messages.join(" ") : undefined;
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
function renderStudioLatexPandocCompatibilityWarningHtml(warning: string | undefined): string {
|
|
6140
|
+
if (!warning) return "";
|
|
6141
|
+
return `<div class="preview-warning studio-latex-compatibility-warning" role="note">${escapeStudioHtmlText(warning)}</div>`;
|
|
6142
|
+
}
|
|
6143
|
+
|
|
6130
6144
|
const resolveStudioPandocHtmlResourceFlag = createStudioPandocHtmlResourceFlagResolver(async (pandocCommand: string) => {
|
|
6131
6145
|
const result = await runStudioSubprocess(pandocCommand, ["--help"], {
|
|
6132
6146
|
timeoutMs: 5_000,
|
|
@@ -6150,8 +6164,15 @@ function preprocessStudioLatexFootnotemarksForPreview(latex: string): string {
|
|
|
6150
6164
|
|
|
6151
6165
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
6152
6166
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
6153
|
-
const
|
|
6154
|
-
|
|
6167
|
+
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
|
|
6168
|
+
?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
|
|
6169
|
+
const latexPandocCompatibility = isLatex
|
|
6170
|
+
? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
|
|
6171
|
+
: { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
|
|
6172
|
+
const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
|
|
6173
|
+
const pandocInputSource = latexPandocCompatibility.source;
|
|
6174
|
+
const latexPreviewSource = isLatex ? preprocessStudioLatexFootnotemarksForPreview(pandocInputSource) : pandocInputSource;
|
|
6175
|
+
const markdownWithNormalizedFences = isLatex ? latexPreviewSource : normalizeStudioMarkdownSmartFences(pandocInputSource);
|
|
6155
6176
|
const markdownWithoutHtmlComments = isLatex ? markdownWithNormalizedFences : stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdownWithNormalizedFences);
|
|
6156
6177
|
const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
|
|
6157
6178
|
const latexSubfigurePreviewTransform = isLatex
|
|
@@ -6164,7 +6185,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
6164
6185
|
? preprocessStudioLatexReferences(latexAlgorithmPreviewTransform.markdown, sourcePath, resourcePath)
|
|
6165
6186
|
: markdownWithPreviewPageBreaks;
|
|
6166
6187
|
const inputFormat = isLatex ? "latex" : "markdown+lists_without_preceding_blankline-blank_before_blockquote-blank_before_header+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris-raw_html";
|
|
6167
|
-
const bibliographyArgs = buildStudioPandocBibliographyArgs(
|
|
6188
|
+
const bibliographyArgs = buildStudioPandocBibliographyArgs(pandocInputSource, isLatex, resourcePath);
|
|
6168
6189
|
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
|
|
6169
6190
|
let htmlTemplateDir: string | null = null;
|
|
6170
6191
|
const useStudioHtmlTemplate = Boolean(resourcePath || isLatex);
|
|
@@ -6185,7 +6206,6 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
6185
6206
|
const normalizedMarkdown = isLatex
|
|
6186
6207
|
? sourceWithResolvedRefs
|
|
6187
6208
|
: normalizeStudioMarkdownFencedBlocks(prepareStudioMarkdownForPandoc(sourceWithResolvedRefs));
|
|
6188
|
-
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
6189
6209
|
|
|
6190
6210
|
let pandocResult: StudioSubprocessResult;
|
|
6191
6211
|
try {
|
|
@@ -6230,6 +6250,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
6230
6250
|
renderedHtml = decorateStudioPreviewPageBreakHtml(renderedHtml);
|
|
6231
6251
|
}
|
|
6232
6252
|
renderedHtml = decorateStudioPandocSyntaxHtml(renderedHtml);
|
|
6253
|
+
renderedHtml = renderStudioLatexPandocCompatibilityWarningHtml(latexPandocCompatibilityWarning) + renderedHtml;
|
|
6233
6254
|
return stripMathMlAnnotationTags(renderedHtml);
|
|
6234
6255
|
}
|
|
6235
6256
|
|
|
@@ -6616,8 +6637,15 @@ async function renderStudioStandaloneHtmlWithPandoc(
|
|
|
6616
6637
|
sourcePath?: string,
|
|
6617
6638
|
options?: StudioHtmlRenderOptions,
|
|
6618
6639
|
): Promise<{ html: Buffer; warning?: string }> {
|
|
6619
|
-
const
|
|
6620
|
-
|
|
6640
|
+
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
|
|
6641
|
+
?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
|
|
6642
|
+
const latexPandocCompatibility = isLatex
|
|
6643
|
+
? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
|
|
6644
|
+
: { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
|
|
6645
|
+
const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
|
|
6646
|
+
const pandocCompatibleMarkdown = latexPandocCompatibility.source;
|
|
6647
|
+
const delimitedMarkdown = isLatex ? null : formatStudioDelimitedTextAsMarkdown(pandocCompatibleMarkdown, editorLanguage);
|
|
6648
|
+
const input = delimitedMarkdown ?? pandocCompatibleMarkdown;
|
|
6621
6649
|
const effectiveEditorLanguage = delimitedMarkdown ? "markdown" : inferStudioPdfLanguage(input, editorLanguage);
|
|
6622
6650
|
if (!isLatex && isLikelyStandaloneStudioHtml(input, effectiveEditorLanguage)) {
|
|
6623
6651
|
return { html: Buffer.from(String(input ?? ""), "utf-8") };
|
|
@@ -6634,8 +6662,12 @@ async function renderStudioStandaloneHtmlWithPandoc(
|
|
|
6634
6662
|
let renderedHtml = await renderStudioMarkdownWithPandoc(pdfPrepared.markdown, isLatex, resourcePath, sourcePath);
|
|
6635
6663
|
renderedHtml = renderStudioPdfBlocksInHtml(renderedHtml, pdfPrepared.blocks, sourcePath, resourcePath);
|
|
6636
6664
|
renderedHtml = applyStudioAnnotationPlaceholdersToHtml(renderedHtml, annotationPrepared.placeholders);
|
|
6665
|
+
renderedHtml = renderStudioLatexPandocCompatibilityWarningHtml(latexPandocCompatibilityWarning) + renderedHtml;
|
|
6637
6666
|
const standaloneHtml = buildStudioStandaloneHtmlDocument(renderedHtml, resourcePath, options);
|
|
6638
|
-
return {
|
|
6667
|
+
return {
|
|
6668
|
+
html: Buffer.from(standaloneHtml, "utf-8"),
|
|
6669
|
+
warning: latexPandocCompatibilityWarning,
|
|
6670
|
+
};
|
|
6639
6671
|
}
|
|
6640
6672
|
|
|
6641
6673
|
async function renderStudioLiteralTextPdf(text: string, title = "Studio export", options?: StudioPdfRenderOptions): Promise<Buffer> {
|
|
@@ -7024,19 +7056,26 @@ async function renderStudioPdfWithPandoc(
|
|
|
7024
7056
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
7025
7057
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
7026
7058
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
7059
|
+
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
|
|
7060
|
+
?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
|
|
7061
|
+
const latexPandocCompatibility = isLatex
|
|
7062
|
+
? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
|
|
7063
|
+
: { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
|
|
7064
|
+
const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
|
|
7065
|
+
const pandocInputSource = latexPandocCompatibility.source;
|
|
7027
7066
|
const latexSubfigurePdfTransform = isLatex
|
|
7028
|
-
? preprocessStudioLatexSubfiguresForPdf(
|
|
7029
|
-
: { markdown, groups: [] };
|
|
7067
|
+
? preprocessStudioLatexSubfiguresForPdf(pandocInputSource)
|
|
7068
|
+
: { markdown: pandocInputSource, groups: [] };
|
|
7030
7069
|
const latexPdfSource = isLatex
|
|
7031
7070
|
? preprocessStudioLatexAlgorithmsForPdf(
|
|
7032
7071
|
latexSubfigurePdfTransform.markdown,
|
|
7033
7072
|
sourcePath,
|
|
7034
7073
|
resourcePath,
|
|
7035
7074
|
)
|
|
7036
|
-
:
|
|
7075
|
+
: pandocInputSource;
|
|
7037
7076
|
const sourceWithResolvedRefs = isLatex
|
|
7038
7077
|
? injectStudioLatexEquationTags(preprocessStudioLatexReferences(latexPdfSource, sourcePath, resourcePath), sourcePath, resourcePath)
|
|
7039
|
-
:
|
|
7078
|
+
: pandocInputSource;
|
|
7040
7079
|
const effectiveEditorLanguage = inferStudioPdfLanguage(sourceWithResolvedRefs, editorPdfLanguage);
|
|
7041
7080
|
const pdfCalloutTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
|
|
7042
7081
|
? preprocessStudioMarkdownCalloutsForPdf(sourceWithResolvedRefs)
|
|
@@ -7044,8 +7083,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
7044
7083
|
const pdfAlignedImageTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
|
|
7045
7084
|
? preprocessStudioMarkdownImageAlignmentForPdf(pdfCalloutTransform.markdown)
|
|
7046
7085
|
: { markdown: pdfCalloutTransform.markdown, blocks: [] as StudioPdfAlignedImageBlock[] };
|
|
7047
|
-
const
|
|
7048
|
-
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
7086
|
+
const bibliographyArgs = buildStudioPandocBibliographyArgs(pandocInputSource, isLatex, resourcePath);
|
|
7049
7087
|
|
|
7050
7088
|
const runPandocPdfExport = async (
|
|
7051
7089
|
inputFormat: string,
|
|
@@ -7098,7 +7136,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
7098
7136
|
};
|
|
7099
7137
|
|
|
7100
7138
|
if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || collectStudioInlineAnnotationMarkers(sourceWithResolvedRefs).length > 0)) {
|
|
7101
|
-
|
|
7139
|
+
const rendered = await renderStudioPdfFromGeneratedLatex(
|
|
7102
7140
|
sourceWithResolvedRefs,
|
|
7103
7141
|
pandocCommand,
|
|
7104
7142
|
pdfEngine,
|
|
@@ -7114,6 +7152,10 @@ async function renderStudioPdfWithPandoc(
|
|
|
7114
7152
|
"",
|
|
7115
7153
|
themeStyle,
|
|
7116
7154
|
);
|
|
7155
|
+
return {
|
|
7156
|
+
pdf: rendered.pdf,
|
|
7157
|
+
warning: combineStudioWarnings(latexPandocCompatibilityWarning, rendered.warning),
|
|
7158
|
+
};
|
|
7117
7159
|
}
|
|
7118
7160
|
|
|
7119
7161
|
if (!isLatex && effectiveEditorLanguage === "diff") {
|
|
@@ -7187,7 +7229,10 @@ async function renderStudioPdfWithPandoc(
|
|
|
7187
7229
|
themeStyle,
|
|
7188
7230
|
);
|
|
7189
7231
|
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
7190
|
-
return {
|
|
7232
|
+
return {
|
|
7233
|
+
pdf: rendered.pdf,
|
|
7234
|
+
warning: combineStudioWarnings(latexPandocCompatibilityWarning, mermaidPrepared.warning, rendered.warning),
|
|
7235
|
+
};
|
|
7191
7236
|
}
|
|
7192
7237
|
|
|
7193
7238
|
const hasYamlHeaderIncludesForPdf = !isLatex && hasStudioYamlHeaderIncludes(markdownForPdf);
|
|
@@ -7222,7 +7267,10 @@ async function renderStudioPdfWithPandoc(
|
|
|
7222
7267
|
throw new Error(`pandoc PDF export failed with exit code ${pandocResult.code}${stderr ? `: ${stderr}` : ""}${hint}`);
|
|
7223
7268
|
}
|
|
7224
7269
|
|
|
7225
|
-
return {
|
|
7270
|
+
return {
|
|
7271
|
+
pdf: await readFile(outputPath),
|
|
7272
|
+
warning: combineStudioWarnings(latexPandocCompatibilityWarning, mermaidPrepared.warning),
|
|
7273
|
+
};
|
|
7226
7274
|
} finally {
|
|
7227
7275
|
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
7228
7276
|
}
|
|
@@ -7308,16 +7356,31 @@ function respondPdfFile(req: IncomingMessage, res: ServerResponse, filePath: str
|
|
|
7308
7356
|
return;
|
|
7309
7357
|
}
|
|
7310
7358
|
|
|
7311
|
-
const
|
|
7312
|
-
|
|
7359
|
+
const stats = statSync(filePath);
|
|
7360
|
+
const etag = `W/"${[
|
|
7361
|
+
stats.size,
|
|
7362
|
+
Math.trunc(stats.mtimeMs),
|
|
7363
|
+
Math.trunc(stats.ctimeMs),
|
|
7364
|
+
stats.ino,
|
|
7365
|
+
].map((value) => Number(value).toString(16)).join("-")}"`;
|
|
7366
|
+
const commonHeaders = {
|
|
7313
7367
|
"Content-Type": "application/pdf",
|
|
7314
|
-
"Content-Length": String(pdf.length),
|
|
7315
7368
|
"Content-Disposition": `inline; filename="${basename(filePath).replace(/["\\]/g, "") || "document.pdf"}"`,
|
|
7316
7369
|
"Cache-Control": "no-store",
|
|
7317
7370
|
"X-Content-Type-Options": "nosniff",
|
|
7318
7371
|
"Cross-Origin-Resource-Policy": "same-origin",
|
|
7319
|
-
|
|
7320
|
-
|
|
7372
|
+
"ETag": etag,
|
|
7373
|
+
"Last-Modified": stats.mtime.toUTCString(),
|
|
7374
|
+
};
|
|
7375
|
+
if (method === "HEAD") {
|
|
7376
|
+
res.writeHead(200, { ...commonHeaders, "Content-Length": String(stats.size) });
|
|
7377
|
+
res.end();
|
|
7378
|
+
return;
|
|
7379
|
+
}
|
|
7380
|
+
|
|
7381
|
+
const pdf = readFileSync(filePath);
|
|
7382
|
+
res.writeHead(200, { ...commonHeaders, "Content-Length": String(pdf.length) });
|
|
7383
|
+
res.end(pdf);
|
|
7321
7384
|
}
|
|
7322
7385
|
|
|
7323
7386
|
function respondHtmlPreviewResourceJson(req: IncomingMessage, res: ServerResponse, filePath: string, mimeType: string): void {
|
|
@@ -7345,7 +7408,7 @@ function sanitizeStudioPreviewBlockLine(value: string): string {
|
|
|
7345
7408
|
return String(value || "").replace(/[\r\n]+/g, " ").trim();
|
|
7346
7409
|
}
|
|
7347
7410
|
|
|
7348
|
-
function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewResource): InitialStudioDocument {
|
|
7411
|
+
function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewResource, options?: { watchPdf?: boolean }): InitialStudioDocument {
|
|
7349
7412
|
const label = basename(resource.filePath) || resource.label || "local preview";
|
|
7350
7413
|
const resourcePath = resource.label || basename(resource.filePath) || resource.filePath;
|
|
7351
7414
|
const title = sanitizeStudioPreviewBlockLine(label);
|
|
@@ -7354,6 +7417,8 @@ function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewRes
|
|
|
7354
7417
|
text = "```studio-pdf\n"
|
|
7355
7418
|
+ `path: ${sanitizeStudioPreviewBlockLine(resourcePath)}\n`
|
|
7356
7419
|
+ `title: ${title || "PDF preview"}\n`
|
|
7420
|
+
+ (resource.page ? `page: ${resource.page}\n` : "")
|
|
7421
|
+
+ (options?.watchPdf ? "watch: true\n" : "")
|
|
7357
7422
|
+ "height: 820\n"
|
|
7358
7423
|
+ "```\n";
|
|
7359
7424
|
} else if (resource.kind === "image") {
|
|
@@ -7532,6 +7597,64 @@ function revealStudioLocalFile(filePath: string): { ok: true; message: string }
|
|
|
7532
7597
|
return { ok: true, message: process.platform === "linux" ? "Opened containing folder." : "Revealed resource in file manager." };
|
|
7533
7598
|
}
|
|
7534
7599
|
|
|
7600
|
+
async function handleImportStudioFileCopyRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
|
|
7601
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7602
|
+
if (method !== "POST") {
|
|
7603
|
+
res.setHeader("Allow", "POST");
|
|
7604
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
7605
|
+
return;
|
|
7606
|
+
}
|
|
7607
|
+
|
|
7608
|
+
const rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7609
|
+
let payload: Record<string, unknown> = {};
|
|
7610
|
+
try {
|
|
7611
|
+
payload = rawBody ? JSON.parse(rawBody) : {};
|
|
7612
|
+
} catch {
|
|
7613
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7614
|
+
return;
|
|
7615
|
+
}
|
|
7616
|
+
|
|
7617
|
+
const requestedPath = typeof payload.path === "string" ? payload.path : "";
|
|
7618
|
+
const resolved = resolveStudioPath(requestedPath, studioCwd);
|
|
7619
|
+
if (resolved.ok === false) {
|
|
7620
|
+
respondJson(res, 400, { ok: false, error: resolved.message });
|
|
7621
|
+
return;
|
|
7622
|
+
}
|
|
7623
|
+
|
|
7624
|
+
try {
|
|
7625
|
+
const stats = statSync(resolved.resolved);
|
|
7626
|
+
if (!stats.isFile()) {
|
|
7627
|
+
respondJson(res, 400, { ok: false, error: `Path is not a file: ${resolved.label}` });
|
|
7628
|
+
return;
|
|
7629
|
+
}
|
|
7630
|
+
if (stats.size > STUDIO_IMPORT_FILE_MAX_BYTES) {
|
|
7631
|
+
respondJson(res, 413, {
|
|
7632
|
+
ok: false,
|
|
7633
|
+
error: `File is too large to import into Studio (${stats.size} bytes; limit ${STUDIO_IMPORT_FILE_MAX_BYTES} bytes).`,
|
|
7634
|
+
});
|
|
7635
|
+
return;
|
|
7636
|
+
}
|
|
7637
|
+
} catch (error) {
|
|
7638
|
+
respondJson(res, 404, {
|
|
7639
|
+
ok: false,
|
|
7640
|
+
error: `Could not access file: ${resolved.label} (${error instanceof Error ? error.message : String(error)})`,
|
|
7641
|
+
});
|
|
7642
|
+
return;
|
|
7643
|
+
}
|
|
7644
|
+
|
|
7645
|
+
const file = readStudioFile(resolved.resolved, studioCwd);
|
|
7646
|
+
if (file.ok === false) {
|
|
7647
|
+
respondJson(res, 400, { ok: false, error: file.message });
|
|
7648
|
+
return;
|
|
7649
|
+
}
|
|
7650
|
+
respondJson(res, 200, {
|
|
7651
|
+
ok: true,
|
|
7652
|
+
text: file.text,
|
|
7653
|
+
filename: basename(file.resolvedPath),
|
|
7654
|
+
resolvedPath: file.resolvedPath,
|
|
7655
|
+
});
|
|
7656
|
+
}
|
|
7657
|
+
|
|
7535
7658
|
async function handleRevealLocalPreviewResourceRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
|
|
7536
7659
|
const method = (req.method ?? "GET").toUpperCase();
|
|
7537
7660
|
if (method !== "POST") {
|
|
@@ -7567,6 +7690,50 @@ async function handleRevealLocalPreviewResourceRequest(req: IncomingMessage, res
|
|
|
7567
7690
|
}
|
|
7568
7691
|
}
|
|
7569
7692
|
|
|
7693
|
+
async function handleOpenLocalPreviewResourceRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
|
|
7694
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7695
|
+
if (method !== "POST") {
|
|
7696
|
+
res.setHeader("Allow", "POST");
|
|
7697
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
7698
|
+
return;
|
|
7699
|
+
}
|
|
7700
|
+
if (isSshSession()) {
|
|
7701
|
+
respondJson(res, 409, { ok: false, error: "Cannot open a system PDF viewer from an SSH/headless Studio session. Copy the path instead." });
|
|
7702
|
+
return;
|
|
7703
|
+
}
|
|
7704
|
+
|
|
7705
|
+
const rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7706
|
+
let payload: Record<string, unknown> = {};
|
|
7707
|
+
try {
|
|
7708
|
+
payload = rawBody ? JSON.parse(rawBody) : {};
|
|
7709
|
+
} catch {
|
|
7710
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7711
|
+
return;
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
try {
|
|
7715
|
+
const resource = resolveStudioLocalPreviewResourcePath(
|
|
7716
|
+
typeof payload.path === "string" ? payload.path : "",
|
|
7717
|
+
typeof payload.sourcePath === "string" ? payload.sourcePath : undefined,
|
|
7718
|
+
typeof payload.resourceDir === "string" ? payload.resourceDir : undefined,
|
|
7719
|
+
studioCwd,
|
|
7720
|
+
);
|
|
7721
|
+
if (resource.kind !== "pdf") {
|
|
7722
|
+
respondJson(res, 400, { ok: false, error: "Only local PDF previews can be opened in the system PDF viewer." });
|
|
7723
|
+
return;
|
|
7724
|
+
}
|
|
7725
|
+
await openPathInDefaultViewer(resource.filePath);
|
|
7726
|
+
respondJson(res, 200, {
|
|
7727
|
+
ok: true,
|
|
7728
|
+
message: "Opened PDF in the system viewer.",
|
|
7729
|
+
path: resource.filePath,
|
|
7730
|
+
label: resource.label,
|
|
7731
|
+
});
|
|
7732
|
+
} catch (error) {
|
|
7733
|
+
respondJson(res, 404, { ok: false, error: `Local PDF unavailable: ${error instanceof Error ? error.message : String(error)}` });
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7570
7737
|
function openPathInDefaultViewer(path: string): Promise<void> {
|
|
7571
7738
|
const openCommand =
|
|
7572
7739
|
process.platform === "darwin"
|
|
@@ -10120,7 +10287,7 @@ function buildStudioRelativeUrl(
|
|
|
10120
10287
|
mode: StudioUiMode = "full",
|
|
10121
10288
|
doc?: InitialStudioDocument | null,
|
|
10122
10289
|
docId?: string,
|
|
10123
|
-
options?:
|
|
10290
|
+
options?: StudioUrlOptions,
|
|
10124
10291
|
): string {
|
|
10125
10292
|
const params = new URLSearchParams({ token });
|
|
10126
10293
|
if (mode !== "full") params.set("mode", mode);
|
|
@@ -10131,6 +10298,7 @@ function buildStudioRelativeUrl(
|
|
|
10131
10298
|
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
10132
10299
|
if (doc?.resourceDir) params.set("resourceDir", doc.resourceDir);
|
|
10133
10300
|
if (options?.skipWorkspaceRestore) params.set("skipWorkspaceRestore", "1");
|
|
10301
|
+
if (options?.paneFocus) params.set("paneFocus", options.paneFocus);
|
|
10134
10302
|
return `/?${params.toString()}`;
|
|
10135
10303
|
}
|
|
10136
10304
|
|
|
@@ -10140,7 +10308,7 @@ function buildStudioUrl(
|
|
|
10140
10308
|
mode: StudioUiMode = "full",
|
|
10141
10309
|
doc?: InitialStudioDocument | null,
|
|
10142
10310
|
docId?: string,
|
|
10143
|
-
options?:
|
|
10311
|
+
options?: StudioUrlOptions,
|
|
10144
10312
|
): string {
|
|
10145
10313
|
return `http://127.0.0.1:${port}${buildStudioRelativeUrl(token, mode, doc, docId, options)}`;
|
|
10146
10314
|
}
|
|
@@ -10149,16 +10317,18 @@ interface StudioLaunchFlags {
|
|
|
10149
10317
|
args: string;
|
|
10150
10318
|
openRemoteBrowser: boolean;
|
|
10151
10319
|
noBrowser: boolean;
|
|
10320
|
+
watchPdf: boolean;
|
|
10152
10321
|
port?: number;
|
|
10153
10322
|
error?: string;
|
|
10154
10323
|
}
|
|
10155
10324
|
|
|
10156
10325
|
function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
10157
10326
|
const parsed = tokenizeStudioCommandArgs(rawArgs);
|
|
10158
|
-
if (parsed.error) return { args: rawArgs, openRemoteBrowser: false, noBrowser: false, error: parsed.error };
|
|
10327
|
+
if (parsed.error) return { args: rawArgs, openRemoteBrowser: false, noBrowser: false, watchPdf: false, error: parsed.error };
|
|
10159
10328
|
const remaining: string[] = [];
|
|
10160
10329
|
let openRemoteBrowser = false;
|
|
10161
10330
|
let noBrowser = false;
|
|
10331
|
+
let watchPdf = false;
|
|
10162
10332
|
let port: number | undefined;
|
|
10163
10333
|
for (let i = 0; i < parsed.tokens.length; i += 1) {
|
|
10164
10334
|
const token = parsed.tokens[i]!;
|
|
@@ -10170,14 +10340,18 @@ function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
|
10170
10340
|
noBrowser = true;
|
|
10171
10341
|
continue;
|
|
10172
10342
|
}
|
|
10343
|
+
if (token === "--watch" || token === "--auto-refresh") {
|
|
10344
|
+
watchPdf = true;
|
|
10345
|
+
continue;
|
|
10346
|
+
}
|
|
10173
10347
|
if (token === "--port" || token.startsWith("--port=")) {
|
|
10174
10348
|
const rawPort = token.startsWith("--port=") ? token.slice("--port=".length) : parsed.tokens[++i];
|
|
10175
10349
|
if (!rawPort) {
|
|
10176
|
-
return { args: rawArgs, openRemoteBrowser, noBrowser, error: "Missing value for --port." };
|
|
10350
|
+
return { args: rawArgs, openRemoteBrowser, noBrowser, watchPdf, error: "Missing value for --port." };
|
|
10177
10351
|
}
|
|
10178
10352
|
const requestedPort = Number(rawPort);
|
|
10179
10353
|
if (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65535) {
|
|
10180
|
-
return { args: rawArgs, openRemoteBrowser, noBrowser, error: `Invalid --port value: ${rawPort}. Use an integer from 1 to 65535.` };
|
|
10354
|
+
return { args: rawArgs, openRemoteBrowser, noBrowser, watchPdf, error: `Invalid --port value: ${rawPort}. Use an integer from 1 to 65535.` };
|
|
10181
10355
|
}
|
|
10182
10356
|
port = requestedPort;
|
|
10183
10357
|
continue;
|
|
@@ -10185,9 +10359,9 @@ function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
|
10185
10359
|
remaining.push(token);
|
|
10186
10360
|
}
|
|
10187
10361
|
if (openRemoteBrowser && noBrowser) {
|
|
10188
|
-
return { args: rawArgs, openRemoteBrowser, noBrowser, port, error: "Use either --no-browser or --open-browser, not both." };
|
|
10362
|
+
return { args: rawArgs, openRemoteBrowser, noBrowser, watchPdf, port, error: "Use either --no-browser or --open-browser, not both." };
|
|
10189
10363
|
}
|
|
10190
|
-
return { args: remaining.join(" "), openRemoteBrowser, noBrowser, port };
|
|
10364
|
+
return { args: remaining.join(" "), openRemoteBrowser, noBrowser, watchPdf, port };
|
|
10191
10365
|
}
|
|
10192
10366
|
|
|
10193
10367
|
function shouldAutoOpenStudioBrowser(options?: { openRemoteBrowser?: boolean; noBrowser?: boolean }): boolean {
|
|
@@ -10589,7 +10763,8 @@ ${cssVarsBlock}
|
|
|
10589
10763
|
<button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
|
|
10590
10764
|
<button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
|
|
10591
10765
|
<button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
|
|
10592
|
-
<
|
|
10766
|
+
<button id="importFileBtn" type="button" title="Import a file as an editable copy.">Import file copy…</button>
|
|
10767
|
+
<input id="fileInput" class="file-input-hidden" type="file" tabindex="-1" aria-hidden="true" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" />
|
|
10593
10768
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
10594
10769
|
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
|
|
10595
10770
|
</div>
|
|
@@ -10918,6 +11093,7 @@ ${cssVarsBlock}
|
|
|
10918
11093
|
<div><dt>Alt/Option+=</dt><dd>Increase the active pane's text size when not editing text</dd></div>
|
|
10919
11094
|
<div><dt>Alt/Option+-</dt><dd>Decrease the active pane's text size when not editing text</dd></div>
|
|
10920
11095
|
<div><dt>Alt/Option+0</dt><dd>Reset the active pane's text size when not editing text</dd></div>
|
|
11096
|
+
<div><dt>Cmd/Ctrl+Alt+R</dt><dd>Refresh the focused or visible PDF preview from disk</dd></div>
|
|
10921
11097
|
</dl>
|
|
10922
11098
|
</section>
|
|
10923
11099
|
<section class="shortcuts-group">
|
|
@@ -14909,6 +15085,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
14909
15085
|
return;
|
|
14910
15086
|
}
|
|
14911
15087
|
|
|
15088
|
+
if (requestUrl.pathname === "/import-file-copy") {
|
|
15089
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
15090
|
+
if (token !== serverState.token) {
|
|
15091
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
15092
|
+
return;
|
|
15093
|
+
}
|
|
15094
|
+
|
|
15095
|
+
void handleImportStudioFileCopyRequest(req, res, studioCwd).catch((error) => {
|
|
15096
|
+
respondJson(res, 500, { ok: false, error: `File import failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
15097
|
+
});
|
|
15098
|
+
return;
|
|
15099
|
+
}
|
|
15100
|
+
|
|
14912
15101
|
if (requestUrl.pathname === "/local-preview-link") {
|
|
14913
15102
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
14914
15103
|
if (token !== serverState.token) {
|
|
@@ -14945,6 +15134,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
14945
15134
|
return;
|
|
14946
15135
|
}
|
|
14947
15136
|
|
|
15137
|
+
if (requestUrl.pathname === "/open-local-resource") {
|
|
15138
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
15139
|
+
if (token !== serverState.token) {
|
|
15140
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
15141
|
+
return;
|
|
15142
|
+
}
|
|
15143
|
+
|
|
15144
|
+
void handleOpenLocalPreviewResourceRequest(req, res, studioCwd).catch((error) => {
|
|
15145
|
+
respondJson(res, 500, { ok: false, error: `Open in system viewer failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
15146
|
+
});
|
|
15147
|
+
return;
|
|
15148
|
+
}
|
|
15149
|
+
|
|
14948
15150
|
if (requestUrl.pathname === "/pdf-resource") {
|
|
14949
15151
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
14950
15152
|
if (token !== serverState.token) {
|
|
@@ -15554,10 +15756,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
15554
15756
|
const resolveStudioLaunchDocument = (
|
|
15555
15757
|
trimmed: string,
|
|
15556
15758
|
ctx: ExtensionCommandContext,
|
|
15557
|
-
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string },
|
|
15558
|
-
):
|
|
15759
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; allowPdfPreview?: boolean; watchPdf?: boolean },
|
|
15760
|
+
): StudioLaunchSelection | null => {
|
|
15559
15761
|
const defaultSource = options?.defaultSource === "blank" ? "blank" : "last-response";
|
|
15560
15762
|
const commandLabel = options?.commandLabel ?? "/studio";
|
|
15763
|
+
const selectDocument = (document: InitialStudioDocument): StudioLaunchSelection => ({ document, kind: "document" });
|
|
15561
15764
|
const latestAssistant =
|
|
15562
15765
|
extractLatestAssistantFromEntries(ctx.sessionManager.getBranch())
|
|
15563
15766
|
?? extractLatestAssistantFromEntries(ctx.sessionManager.getEntries())
|
|
@@ -15566,51 +15769,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
15566
15769
|
|
|
15567
15770
|
if (!trimmed) {
|
|
15568
15771
|
if (defaultSource === "last-response" && latestAssistant) {
|
|
15569
|
-
return {
|
|
15772
|
+
return selectDocument({
|
|
15570
15773
|
text: latestAssistant,
|
|
15571
15774
|
label: "last model response",
|
|
15572
15775
|
source: "last-response",
|
|
15573
15776
|
draftId: createStudioDraftId(),
|
|
15574
15777
|
resourceDir: ctx.cwd,
|
|
15575
|
-
};
|
|
15778
|
+
});
|
|
15576
15779
|
}
|
|
15577
|
-
return {
|
|
15780
|
+
return selectDocument({
|
|
15578
15781
|
text: "",
|
|
15579
15782
|
label: "blank",
|
|
15580
15783
|
source: "blank",
|
|
15581
15784
|
draftId: createStudioDraftId(),
|
|
15582
15785
|
resourceDir: ctx.cwd,
|
|
15583
|
-
};
|
|
15786
|
+
});
|
|
15584
15787
|
}
|
|
15585
15788
|
|
|
15586
15789
|
if (trimmed === "--blank" || trimmed === "blank") {
|
|
15587
|
-
return {
|
|
15790
|
+
return selectDocument({
|
|
15588
15791
|
text: "",
|
|
15589
15792
|
label: "blank",
|
|
15590
15793
|
source: "blank",
|
|
15591
15794
|
draftId: createStudioDraftId(),
|
|
15592
15795
|
resourceDir: ctx.cwd,
|
|
15593
|
-
};
|
|
15796
|
+
});
|
|
15594
15797
|
}
|
|
15595
15798
|
|
|
15596
15799
|
if (trimmed === "--last" || trimmed === "last") {
|
|
15597
15800
|
if (!latestAssistant) {
|
|
15598
15801
|
ctx.ui.notify("No assistant response found; opening blank studio.", "warning");
|
|
15599
|
-
return {
|
|
15802
|
+
return selectDocument({
|
|
15600
15803
|
text: "",
|
|
15601
15804
|
label: "blank",
|
|
15602
15805
|
source: "blank",
|
|
15603
15806
|
draftId: createStudioDraftId(),
|
|
15604
15807
|
resourceDir: ctx.cwd,
|
|
15605
|
-
};
|
|
15808
|
+
});
|
|
15606
15809
|
}
|
|
15607
|
-
return {
|
|
15810
|
+
return selectDocument({
|
|
15608
15811
|
text: latestAssistant,
|
|
15609
15812
|
label: "last model response",
|
|
15610
15813
|
source: "last-response",
|
|
15611
15814
|
draftId: createStudioDraftId(),
|
|
15612
15815
|
resourceDir: ctx.cwd,
|
|
15613
|
-
};
|
|
15816
|
+
});
|
|
15614
15817
|
}
|
|
15615
15818
|
|
|
15616
15819
|
if (trimmed.startsWith("-")) {
|
|
@@ -15624,6 +15827,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
15624
15827
|
return null;
|
|
15625
15828
|
}
|
|
15626
15829
|
|
|
15830
|
+
const pdfTarget = options?.allowPdfPreview ? parseStudioPdfLaunchTarget(normalizePathInput(pathArg)) : null;
|
|
15831
|
+
if (pdfTarget) {
|
|
15832
|
+
const resolved = resolveStudioPath(pdfTarget.path, ctx.cwd);
|
|
15833
|
+
if (resolved.ok === false) {
|
|
15834
|
+
ctx.ui.notify(resolved.message, "error");
|
|
15835
|
+
return null;
|
|
15836
|
+
}
|
|
15837
|
+
try {
|
|
15838
|
+
const resource = resolveStudioLocalPreviewResourcePath(
|
|
15839
|
+
pdfTarget.page ? `${resolved.resolved}#page=${pdfTarget.page}` : resolved.resolved,
|
|
15840
|
+
resolved.resolved,
|
|
15841
|
+
dirname(resolved.resolved),
|
|
15842
|
+
ctx.cwd,
|
|
15843
|
+
);
|
|
15844
|
+
if (resource.kind !== "pdf") throw new Error("Only local .pdf files can open in the Studio PDF viewer.");
|
|
15845
|
+
return {
|
|
15846
|
+
document: buildStudioLocalResourcePreviewDocument(resource, { watchPdf: options?.watchPdf }),
|
|
15847
|
+
kind: "pdf-preview",
|
|
15848
|
+
mode: "editor-only",
|
|
15849
|
+
transient: true,
|
|
15850
|
+
skipWorkspaceRestore: true,
|
|
15851
|
+
paneFocus: "right",
|
|
15852
|
+
resourcePath: resource.filePath,
|
|
15853
|
+
};
|
|
15854
|
+
} catch (error) {
|
|
15855
|
+
ctx.ui.notify(`Could not open PDF preview: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
15856
|
+
return null;
|
|
15857
|
+
}
|
|
15858
|
+
}
|
|
15859
|
+
|
|
15627
15860
|
const file = readStudioFile(pathArg, ctx.cwd);
|
|
15628
15861
|
if (file.ok === false) {
|
|
15629
15862
|
ctx.ui.notify(file.message, "error");
|
|
@@ -15637,13 +15870,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
15637
15870
|
);
|
|
15638
15871
|
}
|
|
15639
15872
|
|
|
15640
|
-
return {
|
|
15873
|
+
return selectDocument({
|
|
15641
15874
|
text: file.text,
|
|
15642
15875
|
label: file.label,
|
|
15643
15876
|
source: "file",
|
|
15644
15877
|
path: file.resolvedPath,
|
|
15645
15878
|
resourceDir: ctx.cwd,
|
|
15646
|
-
};
|
|
15879
|
+
});
|
|
15647
15880
|
};
|
|
15648
15881
|
|
|
15649
15882
|
const resolveLastModelResponseForExport = (ctx: ExtensionContext): { markdown: string } | null => {
|
|
@@ -15900,7 +16133,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15900
16133
|
trimmed: string,
|
|
15901
16134
|
ctx: ExtensionCommandContext,
|
|
15902
16135
|
mode: StudioUiMode,
|
|
15903
|
-
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean },
|
|
16136
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean; allowPdfPreview?: boolean; watchPdf?: boolean },
|
|
15904
16137
|
) => {
|
|
15905
16138
|
const launchOpenFlags = parseStudioLaunchOpenFlags(trimmed);
|
|
15906
16139
|
if (launchOpenFlags.error) {
|
|
@@ -15911,7 +16144,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
15911
16144
|
if (serverState && launchOpenFlags.port && serverState.port !== launchOpenFlags.port) {
|
|
15912
16145
|
ctx.ui.notify(`Studio server is already running on port ${serverState.port}; requested port ${launchOpenFlags.port}. Use /studio --stop, then restart Studio with --port ${launchOpenFlags.port} to change it.`, "warning");
|
|
15913
16146
|
}
|
|
15914
|
-
|
|
16147
|
+
|
|
16148
|
+
const parsedLaunchPath = options?.allowPdfPreview ? parsePathArgument(launchArgs) : null;
|
|
16149
|
+
const launchesPdfPreview = parsedLaunchPath
|
|
16150
|
+
? Boolean(parseStudioPdfLaunchTarget(normalizePathInput(parsedLaunchPath)))
|
|
16151
|
+
: false;
|
|
16152
|
+
if (launchOpenFlags.watchPdf && !launchesPdfPreview) {
|
|
16153
|
+
ctx.ui.notify("--watch requires a local PDF path, for example: /studio --watch main.pdf", "error");
|
|
16154
|
+
return;
|
|
16155
|
+
}
|
|
16156
|
+
const requestedLaunchMode: StudioUiMode = launchesPdfPreview ? "editor-only" : mode;
|
|
16157
|
+
if (requestedLaunchMode === "full" && hasConnectedFullStudioView()) {
|
|
15915
16158
|
if (options?.replaceExistingFull) {
|
|
15916
16159
|
closeStudioClientsByMode("full", 4001, "Full Studio replaced");
|
|
15917
16160
|
} else {
|
|
@@ -15942,9 +16185,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
15942
16185
|
// ignore theme read errors
|
|
15943
16186
|
}
|
|
15944
16187
|
|
|
15945
|
-
const
|
|
15946
|
-
|
|
15947
|
-
|
|
16188
|
+
const selection = resolveStudioLaunchDocument(launchArgs, ctx, {
|
|
16189
|
+
...options,
|
|
16190
|
+
watchPdf: launchOpenFlags.watchPdf,
|
|
16191
|
+
});
|
|
16192
|
+
if (!selection) return;
|
|
16193
|
+
const selected = selection.document;
|
|
16194
|
+
const launchMode = selection.mode ?? requestedLaunchMode;
|
|
16195
|
+
if (!selection.transient) initialStudioDocument = selected;
|
|
15948
16196
|
|
|
15949
16197
|
let state: StudioServerState;
|
|
15950
16198
|
try {
|
|
@@ -15955,10 +16203,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
15955
16203
|
ctx.ui.notify(`Failed to start Studio server${portText}: ${message}`, "error");
|
|
15956
16204
|
return;
|
|
15957
16205
|
}
|
|
15958
|
-
const
|
|
16206
|
+
const docId = selection.transient ? storeTransientStudioDocument(selected) : undefined;
|
|
16207
|
+
const url = buildStudioUrl(state.port, state.token, launchMode, selected, docId, {
|
|
16208
|
+
skipWorkspaceRestore: selection.skipWorkspaceRestore,
|
|
16209
|
+
paneFocus: selection.paneFocus,
|
|
16210
|
+
});
|
|
15959
16211
|
const tunnelHint = buildStudioSshTunnelHint(state.port, url)
|
|
15960
16212
|
?? (launchOpenFlags.noBrowser ? buildStudioForwardingHint(state.port, url, { prefix: "Browser auto-open was skipped because --no-browser was used." }) : null);
|
|
15961
|
-
const openedLabel =
|
|
16213
|
+
const openedLabel = selection.kind === "pdf-preview"
|
|
16214
|
+
? "pi Studio PDF preview"
|
|
16215
|
+
: (launchMode === "editor-only" ? "pi Studio editor-only view" : "pi Studio");
|
|
15962
16216
|
|
|
15963
16217
|
const shouldOpenBrowser = shouldAutoOpenStudioBrowser({
|
|
15964
16218
|
openRemoteBrowser: launchOpenFlags.openRemoteBrowser,
|
|
@@ -15970,7 +16224,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
15970
16224
|
ctx.ui.notify(`${openedLabel} is ready. Browser auto-open was skipped because ${skipReason}.`, "info");
|
|
15971
16225
|
} else {
|
|
15972
16226
|
await openStudioUrlInBrowser(url);
|
|
15973
|
-
if (
|
|
16227
|
+
if (selection.kind === "pdf-preview") {
|
|
16228
|
+
const watchLabel = launchOpenFlags.watchPdf ? " (auto-refresh on)" : "";
|
|
16229
|
+
ctx.ui.notify(`Opened ${openedLabel}${watchLabel}: ${selection.resourcePath ?? selected.label}`, "info");
|
|
16230
|
+
} else if (selected.source === "file") {
|
|
15974
16231
|
ctx.ui.notify(`Opened ${openedLabel} with file loaded: ${selected.label}`, "info");
|
|
15975
16232
|
} else if (selected.source === "last-response") {
|
|
15976
16233
|
ctx.ui.notify(`Opened ${openedLabel} with last model response (${selected.text.length} chars).`, "info");
|
|
@@ -15992,7 +16249,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15992
16249
|
};
|
|
15993
16250
|
|
|
15994
16251
|
pi.registerCommand("studio", {
|
|
15995
|
-
description: "Open pi Studio browser UI (/studio, /studio <file>, /studio --blank, /studio --last, /studio --no-browser
|
|
16252
|
+
description: "Open pi Studio browser UI or a PDF preview (/studio, /studio <file>, /studio --watch <pdf>, /studio --blank, /studio --last, /studio --no-browser)",
|
|
15996
16253
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
15997
16254
|
const trimmed = args.trim();
|
|
15998
16255
|
|
|
@@ -16022,7 +16279,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
16022
16279
|
ctx.ui.notify(
|
|
16023
16280
|
"Usage: /studio [path|--blank|--last]\n"
|
|
16024
16281
|
+ " /studio Open studio with last model response (fallback: blank)\n"
|
|
16025
|
-
+ " /studio <path> Open
|
|
16282
|
+
+ " /studio <path> Open a text file in Studio, or a PDF in a read-only companion preview\n"
|
|
16283
|
+
+ " /studio --watch <pdf> Open a PDF with auto-refresh enabled\n"
|
|
16026
16284
|
+ " /studio --blank Open with blank editor\n"
|
|
16027
16285
|
+ " /studio --last Open with last model response\n"
|
|
16028
16286
|
+ " /studio --no-browser Print the Studio URL without opening a browser\n"
|
|
@@ -16030,7 +16288,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16030
16288
|
+ " /studio --open-remote Over SSH, open the remote browser anyway\n"
|
|
16031
16289
|
+ " /studio --status Show studio status\n"
|
|
16032
16290
|
+ " /studio --stop Stop studio server\n"
|
|
16033
|
-
+ " Note: only one full /studio view is allowed per Pi session.\n"
|
|
16291
|
+
+ " Note: only one full /studio view is allowed per Pi session; PDF previews open as companions.\n"
|
|
16034
16292
|
+ " /studio-replace [path] Replace the current full Studio view with a new one\n"
|
|
16035
16293
|
+ " /studio-editor-only [path] Open another Studio tab in editor-only mode\n"
|
|
16036
16294
|
+ " /studio-current <path> Load a file into currently open Studio tab(s)\n"
|
|
@@ -16041,7 +16299,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16041
16299
|
return;
|
|
16042
16300
|
}
|
|
16043
16301
|
|
|
16044
|
-
await openStudioView(trimmed, ctx, "full", { defaultSource: "last-response", commandLabel: "/studio" });
|
|
16302
|
+
await openStudioView(trimmed, ctx, "full", { defaultSource: "last-response", commandLabel: "/studio", allowPdfPreview: true });
|
|
16045
16303
|
},
|
|
16046
16304
|
});
|
|
16047
16305
|
|
|
@@ -16073,14 +16331,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
16073
16331
|
});
|
|
16074
16332
|
|
|
16075
16333
|
pi.registerCommand("studio-editor-only", {
|
|
16076
|
-
description: "Open pi Studio in editor-only mode (/studio-editor-only, /studio-editor-only <file>, /studio-editor-only --
|
|
16334
|
+
description: "Open pi Studio in editor-only mode or preview a PDF (/studio-editor-only, /studio-editor-only <file>, /studio-editor-only --watch <pdf>)",
|
|
16077
16335
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
16078
16336
|
const trimmed = args.trim();
|
|
16079
16337
|
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
16080
16338
|
ctx.ui.notify(
|
|
16081
16339
|
"Usage: /studio-editor-only [path|--blank|--last]\n"
|
|
16082
16340
|
+ " /studio-editor-only Open an editor-only Studio view (default: blank editor)\n"
|
|
16083
|
-
+ " /studio-editor-only <path> Open
|
|
16341
|
+
+ " /studio-editor-only <path> Open a text file for editing, or a PDF in a read-only preview\n"
|
|
16342
|
+
+ " /studio-editor-only --watch <pdf> Open a PDF with auto-refresh enabled\n"
|
|
16084
16343
|
+ " /studio-editor-only --blank Open with blank editor\n"
|
|
16085
16344
|
+ " /studio-editor-only --last Open with last model response loaded into the editor\n"
|
|
16086
16345
|
+ " /studio-editor-only --no-browser Print URL without opening a browser\n"
|
|
@@ -16091,7 +16350,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16091
16350
|
return;
|
|
16092
16351
|
}
|
|
16093
16352
|
|
|
16094
|
-
await openStudioView(trimmed, ctx, "editor-only", { defaultSource: "blank", commandLabel: "/studio-editor-only" });
|
|
16353
|
+
await openStudioView(trimmed, ctx, "editor-only", { defaultSource: "blank", commandLabel: "/studio-editor-only", allowPdfPreview: true });
|
|
16095
16354
|
},
|
|
16096
16355
|
});
|
|
16097
16356
|
|