pi-studio 0.9.48 → 0.9.50
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 +17 -0
- package/README.md +9 -5
- package/client/studio-client.js +494 -20
- package/client/studio-preview-resource-helpers.js +52 -0
- package/client/studio.css +86 -13
- package/index.ts +148 -75
- package/package.json +1 -1
- package/shared/studio-local-preview-path.js +46 -0
package/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ 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";
|
|
33
34
|
import { prepareStudioLatexForPandoc } from "./shared/studio-latex-pandoc-compat.js";
|
|
@@ -276,6 +277,21 @@ interface InitialStudioDocument {
|
|
|
276
277
|
resourceDir?: string;
|
|
277
278
|
}
|
|
278
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
|
+
|
|
279
295
|
type PersistedStudioReviewNoteAnchorKind = "source" | "html-selection" | "html-element" | "html-page";
|
|
280
296
|
|
|
281
297
|
interface PersistedStudioReviewNote {
|
|
@@ -2949,35 +2965,6 @@ function decodeStudioHtmlPreviewResourcePath(resourcePath: string): string {
|
|
|
2949
2965
|
}
|
|
2950
2966
|
}
|
|
2951
2967
|
|
|
2952
|
-
function parseStudioLocalPreviewResourcePage(resourcePath: string): number | null {
|
|
2953
|
-
const raw = String(resourcePath || "");
|
|
2954
|
-
const parts: string[] = [];
|
|
2955
|
-
const queryIndex = raw.indexOf("?");
|
|
2956
|
-
if (queryIndex >= 0) {
|
|
2957
|
-
const queryEnd = raw.indexOf("#", queryIndex);
|
|
2958
|
-
parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
|
|
2959
|
-
}
|
|
2960
|
-
const hashIndex = raw.indexOf("#");
|
|
2961
|
-
if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
|
|
2962
|
-
for (const part of parts) {
|
|
2963
|
-
try {
|
|
2964
|
-
const params = new URLSearchParams(part);
|
|
2965
|
-
const rawPage = params.get("page") || params.get("p");
|
|
2966
|
-
if (rawPage) {
|
|
2967
|
-
const page = Number.parseInt(rawPage, 10);
|
|
2968
|
-
if (Number.isFinite(page) && page > 0) return page;
|
|
2969
|
-
}
|
|
2970
|
-
} catch {
|
|
2971
|
-
const match = part.match(/(?:^|[&;])page=(\d+)/i) || part.match(/^page=(\d+)$/i);
|
|
2972
|
-
if (match && match[1]) {
|
|
2973
|
-
const page = Number.parseInt(match[1], 10);
|
|
2974
|
-
if (Number.isFinite(page) && page > 0) return page;
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
return null;
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
2968
|
function getStudioLocalPreviewResourceKind(extension: string, filePathOrName?: string): StudioLocalPreviewResourceKind {
|
|
2982
2969
|
const ext = extension.toLowerCase();
|
|
2983
2970
|
const name = basename(String(filePathOrName || "")).toLowerCase();
|
|
@@ -3022,7 +3009,7 @@ function resolveStudioLocalPreviewResourcePath(
|
|
|
3022
3009
|
label: rel && rel !== "" ? rel : basename(candidateReal),
|
|
3023
3010
|
extension,
|
|
3024
3011
|
kind: getStudioLocalPreviewResourceKind(extension, candidateReal),
|
|
3025
|
-
page:
|
|
3012
|
+
page: parseStudioLocalPreviewPage(rawPath),
|
|
3026
3013
|
resourceDir: boundaryReal,
|
|
3027
3014
|
};
|
|
3028
3015
|
}
|
|
@@ -7369,16 +7356,31 @@ function respondPdfFile(req: IncomingMessage, res: ServerResponse, filePath: str
|
|
|
7369
7356
|
return;
|
|
7370
7357
|
}
|
|
7371
7358
|
|
|
7372
|
-
const
|
|
7373
|
-
|
|
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 = {
|
|
7374
7367
|
"Content-Type": "application/pdf",
|
|
7375
|
-
"Content-Length": String(pdf.length),
|
|
7376
7368
|
"Content-Disposition": `inline; filename="${basename(filePath).replace(/["\\]/g, "") || "document.pdf"}"`,
|
|
7377
7369
|
"Cache-Control": "no-store",
|
|
7378
7370
|
"X-Content-Type-Options": "nosniff",
|
|
7379
7371
|
"Cross-Origin-Resource-Policy": "same-origin",
|
|
7380
|
-
|
|
7381
|
-
|
|
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);
|
|
7382
7384
|
}
|
|
7383
7385
|
|
|
7384
7386
|
function respondHtmlPreviewResourceJson(req: IncomingMessage, res: ServerResponse, filePath: string, mimeType: string): void {
|
|
@@ -7406,7 +7408,7 @@ function sanitizeStudioPreviewBlockLine(value: string): string {
|
|
|
7406
7408
|
return String(value || "").replace(/[\r\n]+/g, " ").trim();
|
|
7407
7409
|
}
|
|
7408
7410
|
|
|
7409
|
-
function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewResource): InitialStudioDocument {
|
|
7411
|
+
function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewResource, options?: { watchPdf?: boolean }): InitialStudioDocument {
|
|
7410
7412
|
const label = basename(resource.filePath) || resource.label || "local preview";
|
|
7411
7413
|
const resourcePath = resource.label || basename(resource.filePath) || resource.filePath;
|
|
7412
7414
|
const title = sanitizeStudioPreviewBlockLine(label);
|
|
@@ -7415,6 +7417,8 @@ function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewRes
|
|
|
7415
7417
|
text = "```studio-pdf\n"
|
|
7416
7418
|
+ `path: ${sanitizeStudioPreviewBlockLine(resourcePath)}\n`
|
|
7417
7419
|
+ `title: ${title || "PDF preview"}\n`
|
|
7420
|
+
+ (resource.page ? `page: ${resource.page}\n` : "")
|
|
7421
|
+
+ (options?.watchPdf ? "watch: true\n" : "")
|
|
7418
7422
|
+ "height: 820\n"
|
|
7419
7423
|
+ "```\n";
|
|
7420
7424
|
} else if (resource.kind === "image") {
|
|
@@ -10283,7 +10287,7 @@ function buildStudioRelativeUrl(
|
|
|
10283
10287
|
mode: StudioUiMode = "full",
|
|
10284
10288
|
doc?: InitialStudioDocument | null,
|
|
10285
10289
|
docId?: string,
|
|
10286
|
-
options?:
|
|
10290
|
+
options?: StudioUrlOptions,
|
|
10287
10291
|
): string {
|
|
10288
10292
|
const params = new URLSearchParams({ token });
|
|
10289
10293
|
if (mode !== "full") params.set("mode", mode);
|
|
@@ -10294,6 +10298,7 @@ function buildStudioRelativeUrl(
|
|
|
10294
10298
|
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
10295
10299
|
if (doc?.resourceDir) params.set("resourceDir", doc.resourceDir);
|
|
10296
10300
|
if (options?.skipWorkspaceRestore) params.set("skipWorkspaceRestore", "1");
|
|
10301
|
+
if (options?.paneFocus) params.set("paneFocus", options.paneFocus);
|
|
10297
10302
|
return `/?${params.toString()}`;
|
|
10298
10303
|
}
|
|
10299
10304
|
|
|
@@ -10303,7 +10308,7 @@ function buildStudioUrl(
|
|
|
10303
10308
|
mode: StudioUiMode = "full",
|
|
10304
10309
|
doc?: InitialStudioDocument | null,
|
|
10305
10310
|
docId?: string,
|
|
10306
|
-
options?:
|
|
10311
|
+
options?: StudioUrlOptions,
|
|
10307
10312
|
): string {
|
|
10308
10313
|
return `http://127.0.0.1:${port}${buildStudioRelativeUrl(token, mode, doc, docId, options)}`;
|
|
10309
10314
|
}
|
|
@@ -10312,16 +10317,18 @@ interface StudioLaunchFlags {
|
|
|
10312
10317
|
args: string;
|
|
10313
10318
|
openRemoteBrowser: boolean;
|
|
10314
10319
|
noBrowser: boolean;
|
|
10320
|
+
watchPdf: boolean;
|
|
10315
10321
|
port?: number;
|
|
10316
10322
|
error?: string;
|
|
10317
10323
|
}
|
|
10318
10324
|
|
|
10319
10325
|
function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
10320
10326
|
const parsed = tokenizeStudioCommandArgs(rawArgs);
|
|
10321
|
-
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 };
|
|
10322
10328
|
const remaining: string[] = [];
|
|
10323
10329
|
let openRemoteBrowser = false;
|
|
10324
10330
|
let noBrowser = false;
|
|
10331
|
+
let watchPdf = false;
|
|
10325
10332
|
let port: number | undefined;
|
|
10326
10333
|
for (let i = 0; i < parsed.tokens.length; i += 1) {
|
|
10327
10334
|
const token = parsed.tokens[i]!;
|
|
@@ -10333,14 +10340,18 @@ function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
|
10333
10340
|
noBrowser = true;
|
|
10334
10341
|
continue;
|
|
10335
10342
|
}
|
|
10343
|
+
if (token === "--watch" || token === "--auto-refresh") {
|
|
10344
|
+
watchPdf = true;
|
|
10345
|
+
continue;
|
|
10346
|
+
}
|
|
10336
10347
|
if (token === "--port" || token.startsWith("--port=")) {
|
|
10337
10348
|
const rawPort = token.startsWith("--port=") ? token.slice("--port=".length) : parsed.tokens[++i];
|
|
10338
10349
|
if (!rawPort) {
|
|
10339
|
-
return { args: rawArgs, openRemoteBrowser, noBrowser, error: "Missing value for --port." };
|
|
10350
|
+
return { args: rawArgs, openRemoteBrowser, noBrowser, watchPdf, error: "Missing value for --port." };
|
|
10340
10351
|
}
|
|
10341
10352
|
const requestedPort = Number(rawPort);
|
|
10342
10353
|
if (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65535) {
|
|
10343
|
-
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.` };
|
|
10344
10355
|
}
|
|
10345
10356
|
port = requestedPort;
|
|
10346
10357
|
continue;
|
|
@@ -10348,9 +10359,9 @@ function parseStudioLaunchOpenFlags(rawArgs: string): StudioLaunchFlags {
|
|
|
10348
10359
|
remaining.push(token);
|
|
10349
10360
|
}
|
|
10350
10361
|
if (openRemoteBrowser && noBrowser) {
|
|
10351
|
-
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." };
|
|
10352
10363
|
}
|
|
10353
|
-
return { args: remaining.join(" "), openRemoteBrowser, noBrowser, port };
|
|
10364
|
+
return { args: remaining.join(" "), openRemoteBrowser, noBrowser, watchPdf, port };
|
|
10354
10365
|
}
|
|
10355
10366
|
|
|
10356
10367
|
function shouldAutoOpenStudioBrowser(options?: { openRemoteBrowser?: boolean; noBrowser?: boolean }): boolean {
|
|
@@ -10745,7 +10756,7 @@ ${cssVarsBlock}
|
|
|
10745
10756
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
10746
10757
|
</head>
|
|
10747
10758
|
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-initial-resource-dir="${initialResourceDir}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-terminal-detail="${initialTerminalDetailAttr}" data-theme-name="${initialTheme}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}" data-ssh-session="${initialSshSession}">
|
|
10748
|
-
<header>
|
|
10759
|
+
<header id="studioHeader">
|
|
10749
10760
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
10750
10761
|
<div class="controls">
|
|
10751
10762
|
<button id="saveAsBtn" type="button" title="Save editor content to a new file path. Cmd/Ctrl+S falls back here when no direct save path is available.">Save editor as…</button>
|
|
@@ -10755,9 +10766,13 @@ ${cssVarsBlock}
|
|
|
10755
10766
|
<button id="importFileBtn" type="button" title="Import a file as an editable copy.">Import file copy…</button>
|
|
10756
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" />
|
|
10757
10768
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
10758
|
-
<button id="
|
|
10769
|
+
<button id="hideStudioHeaderBtn" class="header-visibility-btn" type="button" aria-controls="studioHeader" title="Hide the global Studio header. Restore it from the top-right edge.">Hide header</button>
|
|
10770
|
+
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide the Studio header and secondary controls. Shortcut: F9.">Zen</button>
|
|
10759
10771
|
</div>
|
|
10760
10772
|
</header>
|
|
10773
|
+
<div id="studioHeaderReveal" class="studio-header-reveal" hidden>
|
|
10774
|
+
<button id="studioHeaderRevealBtn" type="button" aria-controls="studioHeader" title="Show the Studio header.">Show header</button>
|
|
10775
|
+
</div>
|
|
10761
10776
|
|
|
10762
10777
|
<main>
|
|
10763
10778
|
<section id="leftPane">
|
|
@@ -11070,7 +11085,7 @@ ${cssVarsBlock}
|
|
|
11070
11085
|
<div><dt>Cmd/Ctrl+Alt+W</dt><dd>Switch the right pane directly to Working</dd></div>
|
|
11071
11086
|
<div><dt>F8</dt><dd>Focus editor text</dd></div>
|
|
11072
11087
|
<div><dt>Shift+F8</dt><dd>Focus right-pane content</dd></div>
|
|
11073
|
-
<div><dt>F9</dt><dd>Toggle Zen mode</dd></div>
|
|
11088
|
+
<div><dt>F9</dt><dd>Toggle Zen mode and hide or restore the Studio header</dd></div>
|
|
11074
11089
|
<div><dt>F10</dt><dd>Focus or unfocus the active pane</dd></div>
|
|
11075
11090
|
<div><dt>Esc</dt><dd>Close overlays, exit pane focus, or stop an active request</dd></div>
|
|
11076
11091
|
<div><dt>?</dt><dd>Show keyboard shortcuts when not editing text</dd></div>
|
|
@@ -11082,6 +11097,7 @@ ${cssVarsBlock}
|
|
|
11082
11097
|
<div><dt>Alt/Option+=</dt><dd>Increase the active pane's text size when not editing text</dd></div>
|
|
11083
11098
|
<div><dt>Alt/Option+-</dt><dd>Decrease the active pane's text size when not editing text</dd></div>
|
|
11084
11099
|
<div><dt>Alt/Option+0</dt><dd>Reset the active pane's text size when not editing text</dd></div>
|
|
11100
|
+
<div><dt>Cmd/Ctrl+Alt+R</dt><dd>Refresh the focused or visible PDF preview from disk</dd></div>
|
|
11085
11101
|
</dl>
|
|
11086
11102
|
</section>
|
|
11087
11103
|
<section class="shortcuts-group">
|
|
@@ -15744,10 +15760,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
15744
15760
|
const resolveStudioLaunchDocument = (
|
|
15745
15761
|
trimmed: string,
|
|
15746
15762
|
ctx: ExtensionCommandContext,
|
|
15747
|
-
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string },
|
|
15748
|
-
):
|
|
15763
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; allowPdfPreview?: boolean; watchPdf?: boolean },
|
|
15764
|
+
): StudioLaunchSelection | null => {
|
|
15749
15765
|
const defaultSource = options?.defaultSource === "blank" ? "blank" : "last-response";
|
|
15750
15766
|
const commandLabel = options?.commandLabel ?? "/studio";
|
|
15767
|
+
const selectDocument = (document: InitialStudioDocument): StudioLaunchSelection => ({ document, kind: "document" });
|
|
15751
15768
|
const latestAssistant =
|
|
15752
15769
|
extractLatestAssistantFromEntries(ctx.sessionManager.getBranch())
|
|
15753
15770
|
?? extractLatestAssistantFromEntries(ctx.sessionManager.getEntries())
|
|
@@ -15756,51 +15773,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
15756
15773
|
|
|
15757
15774
|
if (!trimmed) {
|
|
15758
15775
|
if (defaultSource === "last-response" && latestAssistant) {
|
|
15759
|
-
return {
|
|
15776
|
+
return selectDocument({
|
|
15760
15777
|
text: latestAssistant,
|
|
15761
15778
|
label: "last model response",
|
|
15762
15779
|
source: "last-response",
|
|
15763
15780
|
draftId: createStudioDraftId(),
|
|
15764
15781
|
resourceDir: ctx.cwd,
|
|
15765
|
-
};
|
|
15782
|
+
});
|
|
15766
15783
|
}
|
|
15767
|
-
return {
|
|
15784
|
+
return selectDocument({
|
|
15768
15785
|
text: "",
|
|
15769
15786
|
label: "blank",
|
|
15770
15787
|
source: "blank",
|
|
15771
15788
|
draftId: createStudioDraftId(),
|
|
15772
15789
|
resourceDir: ctx.cwd,
|
|
15773
|
-
};
|
|
15790
|
+
});
|
|
15774
15791
|
}
|
|
15775
15792
|
|
|
15776
15793
|
if (trimmed === "--blank" || trimmed === "blank") {
|
|
15777
|
-
return {
|
|
15794
|
+
return selectDocument({
|
|
15778
15795
|
text: "",
|
|
15779
15796
|
label: "blank",
|
|
15780
15797
|
source: "blank",
|
|
15781
15798
|
draftId: createStudioDraftId(),
|
|
15782
15799
|
resourceDir: ctx.cwd,
|
|
15783
|
-
};
|
|
15800
|
+
});
|
|
15784
15801
|
}
|
|
15785
15802
|
|
|
15786
15803
|
if (trimmed === "--last" || trimmed === "last") {
|
|
15787
15804
|
if (!latestAssistant) {
|
|
15788
15805
|
ctx.ui.notify("No assistant response found; opening blank studio.", "warning");
|
|
15789
|
-
return {
|
|
15806
|
+
return selectDocument({
|
|
15790
15807
|
text: "",
|
|
15791
15808
|
label: "blank",
|
|
15792
15809
|
source: "blank",
|
|
15793
15810
|
draftId: createStudioDraftId(),
|
|
15794
15811
|
resourceDir: ctx.cwd,
|
|
15795
|
-
};
|
|
15812
|
+
});
|
|
15796
15813
|
}
|
|
15797
|
-
return {
|
|
15814
|
+
return selectDocument({
|
|
15798
15815
|
text: latestAssistant,
|
|
15799
15816
|
label: "last model response",
|
|
15800
15817
|
source: "last-response",
|
|
15801
15818
|
draftId: createStudioDraftId(),
|
|
15802
15819
|
resourceDir: ctx.cwd,
|
|
15803
|
-
};
|
|
15820
|
+
});
|
|
15804
15821
|
}
|
|
15805
15822
|
|
|
15806
15823
|
if (trimmed.startsWith("-")) {
|
|
@@ -15814,6 +15831,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
15814
15831
|
return null;
|
|
15815
15832
|
}
|
|
15816
15833
|
|
|
15834
|
+
const pdfTarget = options?.allowPdfPreview ? parseStudioPdfLaunchTarget(normalizePathInput(pathArg)) : null;
|
|
15835
|
+
if (pdfTarget) {
|
|
15836
|
+
const resolved = resolveStudioPath(pdfTarget.path, ctx.cwd);
|
|
15837
|
+
if (resolved.ok === false) {
|
|
15838
|
+
ctx.ui.notify(resolved.message, "error");
|
|
15839
|
+
return null;
|
|
15840
|
+
}
|
|
15841
|
+
try {
|
|
15842
|
+
const resource = resolveStudioLocalPreviewResourcePath(
|
|
15843
|
+
pdfTarget.page ? `${resolved.resolved}#page=${pdfTarget.page}` : resolved.resolved,
|
|
15844
|
+
resolved.resolved,
|
|
15845
|
+
dirname(resolved.resolved),
|
|
15846
|
+
ctx.cwd,
|
|
15847
|
+
);
|
|
15848
|
+
if (resource.kind !== "pdf") throw new Error("Only local .pdf files can open in the Studio PDF viewer.");
|
|
15849
|
+
return {
|
|
15850
|
+
document: buildStudioLocalResourcePreviewDocument(resource, { watchPdf: options?.watchPdf }),
|
|
15851
|
+
kind: "pdf-preview",
|
|
15852
|
+
mode: "editor-only",
|
|
15853
|
+
transient: true,
|
|
15854
|
+
skipWorkspaceRestore: true,
|
|
15855
|
+
paneFocus: "right",
|
|
15856
|
+
resourcePath: resource.filePath,
|
|
15857
|
+
};
|
|
15858
|
+
} catch (error) {
|
|
15859
|
+
ctx.ui.notify(`Could not open PDF preview: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
15860
|
+
return null;
|
|
15861
|
+
}
|
|
15862
|
+
}
|
|
15863
|
+
|
|
15817
15864
|
const file = readStudioFile(pathArg, ctx.cwd);
|
|
15818
15865
|
if (file.ok === false) {
|
|
15819
15866
|
ctx.ui.notify(file.message, "error");
|
|
@@ -15827,13 +15874,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
15827
15874
|
);
|
|
15828
15875
|
}
|
|
15829
15876
|
|
|
15830
|
-
return {
|
|
15877
|
+
return selectDocument({
|
|
15831
15878
|
text: file.text,
|
|
15832
15879
|
label: file.label,
|
|
15833
15880
|
source: "file",
|
|
15834
15881
|
path: file.resolvedPath,
|
|
15835
15882
|
resourceDir: ctx.cwd,
|
|
15836
|
-
};
|
|
15883
|
+
});
|
|
15837
15884
|
};
|
|
15838
15885
|
|
|
15839
15886
|
const resolveLastModelResponseForExport = (ctx: ExtensionContext): { markdown: string } | null => {
|
|
@@ -16090,7 +16137,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16090
16137
|
trimmed: string,
|
|
16091
16138
|
ctx: ExtensionCommandContext,
|
|
16092
16139
|
mode: StudioUiMode,
|
|
16093
|
-
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean },
|
|
16140
|
+
options?: { defaultSource?: "blank" | "last-response"; commandLabel?: string; replaceExistingFull?: boolean; allowPdfPreview?: boolean; watchPdf?: boolean },
|
|
16094
16141
|
) => {
|
|
16095
16142
|
const launchOpenFlags = parseStudioLaunchOpenFlags(trimmed);
|
|
16096
16143
|
if (launchOpenFlags.error) {
|
|
@@ -16101,7 +16148,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
16101
16148
|
if (serverState && launchOpenFlags.port && serverState.port !== launchOpenFlags.port) {
|
|
16102
16149
|
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");
|
|
16103
16150
|
}
|
|
16104
|
-
|
|
16151
|
+
|
|
16152
|
+
const parsedLaunchPath = options?.allowPdfPreview ? parsePathArgument(launchArgs) : null;
|
|
16153
|
+
const launchesPdfPreview = parsedLaunchPath
|
|
16154
|
+
? Boolean(parseStudioPdfLaunchTarget(normalizePathInput(parsedLaunchPath)))
|
|
16155
|
+
: false;
|
|
16156
|
+
if (launchOpenFlags.watchPdf && !launchesPdfPreview) {
|
|
16157
|
+
ctx.ui.notify("--watch requires a local PDF path, for example: /studio --watch main.pdf", "error");
|
|
16158
|
+
return;
|
|
16159
|
+
}
|
|
16160
|
+
const requestedLaunchMode: StudioUiMode = launchesPdfPreview ? "editor-only" : mode;
|
|
16161
|
+
if (requestedLaunchMode === "full" && hasConnectedFullStudioView()) {
|
|
16105
16162
|
if (options?.replaceExistingFull) {
|
|
16106
16163
|
closeStudioClientsByMode("full", 4001, "Full Studio replaced");
|
|
16107
16164
|
} else {
|
|
@@ -16132,9 +16189,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
16132
16189
|
// ignore theme read errors
|
|
16133
16190
|
}
|
|
16134
16191
|
|
|
16135
|
-
const
|
|
16136
|
-
|
|
16137
|
-
|
|
16192
|
+
const selection = resolveStudioLaunchDocument(launchArgs, ctx, {
|
|
16193
|
+
...options,
|
|
16194
|
+
watchPdf: launchOpenFlags.watchPdf,
|
|
16195
|
+
});
|
|
16196
|
+
if (!selection) return;
|
|
16197
|
+
const selected = selection.document;
|
|
16198
|
+
const launchMode = selection.mode ?? requestedLaunchMode;
|
|
16199
|
+
if (!selection.transient) initialStudioDocument = selected;
|
|
16138
16200
|
|
|
16139
16201
|
let state: StudioServerState;
|
|
16140
16202
|
try {
|
|
@@ -16145,10 +16207,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
16145
16207
|
ctx.ui.notify(`Failed to start Studio server${portText}: ${message}`, "error");
|
|
16146
16208
|
return;
|
|
16147
16209
|
}
|
|
16148
|
-
const
|
|
16210
|
+
const docId = selection.transient ? storeTransientStudioDocument(selected) : undefined;
|
|
16211
|
+
const url = buildStudioUrl(state.port, state.token, launchMode, selected, docId, {
|
|
16212
|
+
skipWorkspaceRestore: selection.skipWorkspaceRestore,
|
|
16213
|
+
paneFocus: selection.paneFocus,
|
|
16214
|
+
});
|
|
16149
16215
|
const tunnelHint = buildStudioSshTunnelHint(state.port, url)
|
|
16150
16216
|
?? (launchOpenFlags.noBrowser ? buildStudioForwardingHint(state.port, url, { prefix: "Browser auto-open was skipped because --no-browser was used." }) : null);
|
|
16151
|
-
const openedLabel =
|
|
16217
|
+
const openedLabel = selection.kind === "pdf-preview"
|
|
16218
|
+
? "pi Studio PDF preview"
|
|
16219
|
+
: (launchMode === "editor-only" ? "pi Studio editor-only view" : "pi Studio");
|
|
16152
16220
|
|
|
16153
16221
|
const shouldOpenBrowser = shouldAutoOpenStudioBrowser({
|
|
16154
16222
|
openRemoteBrowser: launchOpenFlags.openRemoteBrowser,
|
|
@@ -16160,7 +16228,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
16160
16228
|
ctx.ui.notify(`${openedLabel} is ready. Browser auto-open was skipped because ${skipReason}.`, "info");
|
|
16161
16229
|
} else {
|
|
16162
16230
|
await openStudioUrlInBrowser(url);
|
|
16163
|
-
if (
|
|
16231
|
+
if (selection.kind === "pdf-preview") {
|
|
16232
|
+
const watchLabel = launchOpenFlags.watchPdf ? " (auto-refresh on)" : "";
|
|
16233
|
+
ctx.ui.notify(`Opened ${openedLabel}${watchLabel}: ${selection.resourcePath ?? selected.label}`, "info");
|
|
16234
|
+
} else if (selected.source === "file") {
|
|
16164
16235
|
ctx.ui.notify(`Opened ${openedLabel} with file loaded: ${selected.label}`, "info");
|
|
16165
16236
|
} else if (selected.source === "last-response") {
|
|
16166
16237
|
ctx.ui.notify(`Opened ${openedLabel} with last model response (${selected.text.length} chars).`, "info");
|
|
@@ -16182,7 +16253,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16182
16253
|
};
|
|
16183
16254
|
|
|
16184
16255
|
pi.registerCommand("studio", {
|
|
16185
|
-
description: "Open pi Studio browser UI (/studio, /studio <file>, /studio --blank, /studio --last, /studio --no-browser
|
|
16256
|
+
description: "Open pi Studio browser UI or a PDF preview (/studio, /studio <file>, /studio --watch <pdf>, /studio --blank, /studio --last, /studio --no-browser)",
|
|
16186
16257
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
16187
16258
|
const trimmed = args.trim();
|
|
16188
16259
|
|
|
@@ -16212,7 +16283,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
16212
16283
|
ctx.ui.notify(
|
|
16213
16284
|
"Usage: /studio [path|--blank|--last]\n"
|
|
16214
16285
|
+ " /studio Open studio with last model response (fallback: blank)\n"
|
|
16215
|
-
+ " /studio <path> Open
|
|
16286
|
+
+ " /studio <path> Open a text file in Studio, or a PDF in a read-only companion preview\n"
|
|
16287
|
+
+ " /studio --watch <pdf> Open a PDF with auto-refresh enabled\n"
|
|
16216
16288
|
+ " /studio --blank Open with blank editor\n"
|
|
16217
16289
|
+ " /studio --last Open with last model response\n"
|
|
16218
16290
|
+ " /studio --no-browser Print the Studio URL without opening a browser\n"
|
|
@@ -16220,7 +16292,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16220
16292
|
+ " /studio --open-remote Over SSH, open the remote browser anyway\n"
|
|
16221
16293
|
+ " /studio --status Show studio status\n"
|
|
16222
16294
|
+ " /studio --stop Stop studio server\n"
|
|
16223
|
-
+ " Note: only one full /studio view is allowed per Pi session.\n"
|
|
16295
|
+
+ " Note: only one full /studio view is allowed per Pi session; PDF previews open as companions.\n"
|
|
16224
16296
|
+ " /studio-replace [path] Replace the current full Studio view with a new one\n"
|
|
16225
16297
|
+ " /studio-editor-only [path] Open another Studio tab in editor-only mode\n"
|
|
16226
16298
|
+ " /studio-current <path> Load a file into currently open Studio tab(s)\n"
|
|
@@ -16231,7 +16303,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16231
16303
|
return;
|
|
16232
16304
|
}
|
|
16233
16305
|
|
|
16234
|
-
await openStudioView(trimmed, ctx, "full", { defaultSource: "last-response", commandLabel: "/studio" });
|
|
16306
|
+
await openStudioView(trimmed, ctx, "full", { defaultSource: "last-response", commandLabel: "/studio", allowPdfPreview: true });
|
|
16235
16307
|
},
|
|
16236
16308
|
});
|
|
16237
16309
|
|
|
@@ -16263,14 +16335,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
16263
16335
|
});
|
|
16264
16336
|
|
|
16265
16337
|
pi.registerCommand("studio-editor-only", {
|
|
16266
|
-
description: "Open pi Studio in editor-only mode (/studio-editor-only, /studio-editor-only <file>, /studio-editor-only --
|
|
16338
|
+
description: "Open pi Studio in editor-only mode or preview a PDF (/studio-editor-only, /studio-editor-only <file>, /studio-editor-only --watch <pdf>)",
|
|
16267
16339
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
16268
16340
|
const trimmed = args.trim();
|
|
16269
16341
|
if (trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
16270
16342
|
ctx.ui.notify(
|
|
16271
16343
|
"Usage: /studio-editor-only [path|--blank|--last]\n"
|
|
16272
16344
|
+ " /studio-editor-only Open an editor-only Studio view (default: blank editor)\n"
|
|
16273
|
-
+ " /studio-editor-only <path> Open
|
|
16345
|
+
+ " /studio-editor-only <path> Open a text file for editing, or a PDF in a read-only preview\n"
|
|
16346
|
+
+ " /studio-editor-only --watch <pdf> Open a PDF with auto-refresh enabled\n"
|
|
16274
16347
|
+ " /studio-editor-only --blank Open with blank editor\n"
|
|
16275
16348
|
+ " /studio-editor-only --last Open with last model response loaded into the editor\n"
|
|
16276
16349
|
+ " /studio-editor-only --no-browser Print URL without opening a browser\n"
|
|
@@ -16281,7 +16354,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
16281
16354
|
return;
|
|
16282
16355
|
}
|
|
16283
16356
|
|
|
16284
|
-
await openStudioView(trimmed, ctx, "editor-only", { defaultSource: "blank", commandLabel: "/studio-editor-only" });
|
|
16357
|
+
await openStudioView(trimmed, ctx, "editor-only", { defaultSource: "blank", commandLabel: "/studio-editor-only", allowPdfPreview: true });
|
|
16285
16358
|
},
|
|
16286
16359
|
});
|
|
16287
16360
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.50",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function parseStudioLocalPreviewPage(resourcePath) {
|
|
2
|
+
const raw = String(resourcePath || "");
|
|
3
|
+
const parts = [];
|
|
4
|
+
const queryIndex = raw.indexOf("?");
|
|
5
|
+
if (queryIndex >= 0) {
|
|
6
|
+
const queryEnd = raw.indexOf("#", queryIndex);
|
|
7
|
+
parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
|
|
8
|
+
}
|
|
9
|
+
const hashIndex = raw.indexOf("#");
|
|
10
|
+
if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
try {
|
|
13
|
+
const params = new URLSearchParams(part);
|
|
14
|
+
const rawPage = params.get("page") || params.get("p");
|
|
15
|
+
if (rawPage) {
|
|
16
|
+
const page = Number.parseInt(rawPage, 10);
|
|
17
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
const match = part.match(/(?:^|[&;])page=(\d+)/i) || part.match(/^page=(\d+)$/i);
|
|
21
|
+
if (match && match[1]) {
|
|
22
|
+
const page = Number.parseInt(match[1], 10);
|
|
23
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseStudioPdfLaunchTarget(pathInput) {
|
|
31
|
+
const raw = String(pathInput || "").trim();
|
|
32
|
+
if (!raw || /\0/.test(raw) || /^\/\//.test(raw)) return null;
|
|
33
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(raw) && !/^[a-z]:[\\/]/i.test(raw)) return null;
|
|
34
|
+
|
|
35
|
+
const match = raw.match(/^(.*?\.pdf)(?:(?:\?[^#]*)?(?:#.*)?)?$/i);
|
|
36
|
+
if (!match || !match[1]) return null;
|
|
37
|
+
return {
|
|
38
|
+
path: match[1],
|
|
39
|
+
page: parseStudioLocalPreviewPage(raw),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
parseStudioLocalPreviewPage,
|
|
45
|
+
parseStudioPdfLaunchTarget,
|
|
46
|
+
};
|