pi-studio 0.9.26 → 0.9.28
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 +27 -0
- package/README.md +1 -1
- package/client/studio-client.js +338 -16
- package/client/studio.css +97 -2
- package/index.ts +328 -41
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { completeSimple, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
+
import { completeSimple, type ModelThinkingLevel, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -339,6 +339,9 @@ interface CompletionSuggestionRequestMessage {
|
|
|
339
339
|
path?: string;
|
|
340
340
|
contextMode?: "cursor" | "session";
|
|
341
341
|
contextText?: string;
|
|
342
|
+
previousSuggestion?: string;
|
|
343
|
+
suggestionModelProvider?: string;
|
|
344
|
+
suggestionModelId?: string;
|
|
342
345
|
}
|
|
343
346
|
|
|
344
347
|
interface CompletionSuggestionCancelRequestMessage {
|
|
@@ -346,6 +349,17 @@ interface CompletionSuggestionCancelRequestMessage {
|
|
|
346
349
|
requestId: string;
|
|
347
350
|
}
|
|
348
351
|
|
|
352
|
+
interface PiModelSelectRequestMessage {
|
|
353
|
+
type: "pi_model_select_request";
|
|
354
|
+
provider: string;
|
|
355
|
+
id: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface PiThinkingLevelRequestMessage {
|
|
359
|
+
type: "pi_thinking_level_request";
|
|
360
|
+
level: ModelThinkingLevel;
|
|
361
|
+
}
|
|
362
|
+
|
|
349
363
|
interface QuizGenerateRequestMessage {
|
|
350
364
|
type: "quiz_generate_request";
|
|
351
365
|
requestId: string;
|
|
@@ -489,6 +503,8 @@ type IncomingStudioMessage =
|
|
|
489
503
|
| SendRunRequestMessage
|
|
490
504
|
| CompletionSuggestionRequestMessage
|
|
491
505
|
| CompletionSuggestionCancelRequestMessage
|
|
506
|
+
| PiModelSelectRequestMessage
|
|
507
|
+
| PiThinkingLevelRequestMessage
|
|
492
508
|
| QuizGenerateRequestMessage
|
|
493
509
|
| QuizAnswerRequestMessage
|
|
494
510
|
| QuizDiscussRequestMessage
|
|
@@ -754,6 +770,17 @@ function buildStudioPandocPdfEngineOptArgs(pdfEngine: string): string[] {
|
|
|
754
770
|
];
|
|
755
771
|
}
|
|
756
772
|
|
|
773
|
+
function getStudioMissingLatexEngineHint(stderr: string, pdfEngine: string): string {
|
|
774
|
+
const text = String(stderr || "");
|
|
775
|
+
const lower = text.toLowerCase();
|
|
776
|
+
const engine = basename(String(pdfEngine || "")).toLowerCase();
|
|
777
|
+
const engineMentioned = [engine, "xelatex", "pdflatex", "lualatex", "tectonic"].filter(Boolean).some((name) => lower.includes(name));
|
|
778
|
+
const missingEnginePattern = /(?:command not found|not found|no such file|could not find|cannot find|is not installed|not installed)/i;
|
|
779
|
+
return engineMentioned && missingEnginePattern.test(text)
|
|
780
|
+
? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
|
|
781
|
+
: "";
|
|
782
|
+
}
|
|
783
|
+
|
|
757
784
|
const STUDIO_PANDOC_HTML_FRAGMENT_TEMPLATE = `<!doctype html>
|
|
758
785
|
<html>
|
|
759
786
|
<head>
|
|
@@ -761,6 +788,26 @@ const STUDIO_PANDOC_HTML_FRAGMENT_TEMPLATE = `<!doctype html>
|
|
|
761
788
|
<title>pi Studio preview</title>
|
|
762
789
|
</head>
|
|
763
790
|
<body>
|
|
791
|
+
$if(title)$
|
|
792
|
+
<header id="title-block-header">
|
|
793
|
+
<h1 class="title">$title$</h1>
|
|
794
|
+
$if(subtitle)$
|
|
795
|
+
<p class="subtitle">$subtitle$</p>
|
|
796
|
+
$endif$
|
|
797
|
+
$for(author)$
|
|
798
|
+
<p class="author">$author$</p>
|
|
799
|
+
$endfor$
|
|
800
|
+
$if(date)$
|
|
801
|
+
<p class="date">$date$</p>
|
|
802
|
+
$endif$
|
|
803
|
+
$if(abstract)$
|
|
804
|
+
<div class="abstract">
|
|
805
|
+
<div class="abstract-title">Abstract</div>
|
|
806
|
+
$abstract$
|
|
807
|
+
</div>
|
|
808
|
+
$endif$
|
|
809
|
+
</header>
|
|
810
|
+
$endif$
|
|
764
811
|
$body$
|
|
765
812
|
</body>
|
|
766
813
|
</html>
|
|
@@ -4279,12 +4326,14 @@ function normalizeMathDelimitersInSegment(markdown: string): string {
|
|
|
4279
4326
|
});
|
|
4280
4327
|
|
|
4281
4328
|
normalized = normalized.replace(/\$\s*\\\[\s*([\s\S]*?)\s*\\\]\s*\$/g, (match, expr: string) => {
|
|
4329
|
+
if (/\n\s{0,3}>/.test(match)) return match;
|
|
4282
4330
|
if (!isLikelyMathExpression(expr)) return match;
|
|
4283
4331
|
const content = collapseDisplayMathContent(expr);
|
|
4284
4332
|
return content.length > 0 ? `\\[${content}\\]` : "\\[\\]";
|
|
4285
4333
|
});
|
|
4286
4334
|
|
|
4287
4335
|
normalized = normalized.replace(/\\\[\s*([\s\S]*?)\s*\\\]/g, (match, expr: string) => {
|
|
4336
|
+
if (/\n\s{0,3}>/.test(match)) return match;
|
|
4288
4337
|
if (!isLikelyMathExpression(expr)) return `[${expr.trim()}]`;
|
|
4289
4338
|
const content = collapseDisplayMathContent(expr);
|
|
4290
4339
|
return content.length > 0 ? `\\[${content}\\]` : "\\[\\]";
|
|
@@ -5920,9 +5969,17 @@ async function getStudioPandocHtmlResourceFlag(pandocCommand: string): Promise<"
|
|
|
5920
5969
|
return cached;
|
|
5921
5970
|
}
|
|
5922
5971
|
|
|
5972
|
+
function preprocessStudioLatexFootnotemarksForPreview(latex: string): string {
|
|
5973
|
+
return String(latex ?? "").replace(/\\footnotemark\s*\[\s*([^\]\r\n]+?)\s*\]/g, (_match, marker: string) => {
|
|
5974
|
+
const value = String(marker || "").trim();
|
|
5975
|
+
return /^\d+$/.test(value) ? `\\href{#fn${value}}{\\textsuperscript{${value}}}` : (value ? `\\textsuperscript{${value}}` : "");
|
|
5976
|
+
});
|
|
5977
|
+
}
|
|
5978
|
+
|
|
5923
5979
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
5924
5980
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
5925
|
-
const
|
|
5981
|
+
const latexPreviewSource = isLatex ? preprocessStudioLatexFootnotemarksForPreview(markdown) : markdown;
|
|
5982
|
+
const markdownWithNormalizedFences = isLatex ? latexPreviewSource : normalizeStudioMarkdownSmartFences(markdown);
|
|
5926
5983
|
const markdownWithoutHtmlComments = isLatex ? markdownWithNormalizedFences : stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdownWithNormalizedFences);
|
|
5927
5984
|
const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
|
|
5928
5985
|
const latexSubfigurePreviewTransform = isLatex
|
|
@@ -5938,16 +5995,20 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
5938
5995
|
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
5939
5996
|
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
|
|
5940
5997
|
let htmlTemplateDir: string | null = null;
|
|
5998
|
+
const useStudioHtmlTemplate = Boolean(resourcePath || isLatex);
|
|
5941
5999
|
if (resourcePath) {
|
|
5942
6000
|
args.push(`--resource-path=${resourcePath}`);
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
//
|
|
6001
|
+
}
|
|
6002
|
+
if (useStudioHtmlTemplate) {
|
|
6003
|
+
// Use standalone mode for embedded resources and LaTeX metadata. A minimal
|
|
6004
|
+
// Studio template keeps Pandoc's default standalone CSS out of the pane while
|
|
6005
|
+
// still rendering LaTeX title/author/abstract metadata.
|
|
5946
6006
|
htmlTemplateDir = join(tmpdir(), `pi-studio-pandoc-html-${randomUUID()}`);
|
|
5947
6007
|
await mkdir(htmlTemplateDir, { recursive: true });
|
|
5948
6008
|
const htmlTemplatePath = join(htmlTemplateDir, "template.html");
|
|
5949
6009
|
await writeFile(htmlTemplatePath, STUDIO_PANDOC_HTML_FRAGMENT_TEMPLATE, "utf-8");
|
|
5950
|
-
args.push(await getStudioPandocHtmlResourceFlag(pandocCommand)
|
|
6010
|
+
if (resourcePath) args.push(await getStudioPandocHtmlResourceFlag(pandocCommand));
|
|
6011
|
+
args.push("--standalone", `--template=${htmlTemplatePath}`);
|
|
5951
6012
|
}
|
|
5952
6013
|
const normalizedMarkdown = isLatex
|
|
5953
6014
|
? sourceWithResolvedRefs
|
|
@@ -5977,8 +6038,8 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
5977
6038
|
}
|
|
5978
6039
|
|
|
5979
6040
|
let renderedHtml = pandocResult.stdout;
|
|
5980
|
-
// When --standalone is used for embedded resources, extract only the <body> content.
|
|
5981
|
-
if (
|
|
6041
|
+
// When --standalone is used for embedded resources or LaTeX metadata, extract only the <body> content.
|
|
6042
|
+
if (useStudioHtmlTemplate) {
|
|
5982
6043
|
const bodyMatch = renderedHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
5983
6044
|
if (!bodyMatch) {
|
|
5984
6045
|
throw new Error("pandoc HTML render did not include a complete body element.");
|
|
@@ -6787,9 +6848,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
6787
6848
|
});
|
|
6788
6849
|
if (pandocResult.code !== 0) {
|
|
6789
6850
|
const stderr = pandocResult.stderr;
|
|
6790
|
-
const hint =
|
|
6791
|
-
? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
|
|
6792
|
-
: "";
|
|
6851
|
+
const hint = getStudioMissingLatexEngineHint(stderr, pdfEngine);
|
|
6793
6852
|
throw new Error(`pandoc PDF export failed with exit code ${pandocResult.code}${stderr ? `: ${stderr}` : ""}${hint}`);
|
|
6794
6853
|
}
|
|
6795
6854
|
|
|
@@ -6915,9 +6974,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
6915
6974
|
});
|
|
6916
6975
|
if (pandocResult.code !== 0) {
|
|
6917
6976
|
const stderr = pandocResult.stderr;
|
|
6918
|
-
const hint =
|
|
6919
|
-
? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
|
|
6920
|
-
: "";
|
|
6977
|
+
const hint = getStudioMissingLatexEngineHint(stderr, pdfEngine);
|
|
6921
6978
|
throw new Error(`pandoc PDF export failed with exit code ${pandocResult.code}${stderr ? `: ${stderr}` : ""}${hint}`);
|
|
6922
6979
|
}
|
|
6923
6980
|
|
|
@@ -7303,6 +7360,41 @@ function openPathInDefaultViewer(path: string): Promise<void> {
|
|
|
7303
7360
|
});
|
|
7304
7361
|
}
|
|
7305
7362
|
|
|
7363
|
+
async function handleOpenStudioFileBrowserDirectoryRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
|
|
7364
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7365
|
+
if (method !== "POST") {
|
|
7366
|
+
res.setHeader("Allow", "POST");
|
|
7367
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
7368
|
+
return;
|
|
7369
|
+
}
|
|
7370
|
+
if (isSshSession()) {
|
|
7371
|
+
respondJson(res, 409, { ok: false, error: "Cannot open local file manager from an SSH/headless Studio session. Copy the path instead." });
|
|
7372
|
+
return;
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
const rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7376
|
+
let payload: Record<string, unknown> = {};
|
|
7377
|
+
try {
|
|
7378
|
+
payload = rawBody ? JSON.parse(rawBody) : {};
|
|
7379
|
+
} catch {
|
|
7380
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7381
|
+
return;
|
|
7382
|
+
}
|
|
7383
|
+
|
|
7384
|
+
try {
|
|
7385
|
+
const directory = resolveStudioFileBrowserDirectory(
|
|
7386
|
+
typeof payload.dir === "string" ? payload.dir : undefined,
|
|
7387
|
+
typeof payload.sourcePath === "string" ? payload.sourcePath : undefined,
|
|
7388
|
+
typeof payload.resourceDir === "string" ? payload.resourceDir : undefined,
|
|
7389
|
+
studioCwd,
|
|
7390
|
+
);
|
|
7391
|
+
await openPathInDefaultViewer(directory.currentDir);
|
|
7392
|
+
respondJson(res, 200, { ok: true, message: "Opened folder in file manager.", path: directory.currentDir, rootDir: directory.rootDir });
|
|
7393
|
+
} catch (error) {
|
|
7394
|
+
respondJson(res, 404, { ok: false, error: `Could not open file-browser folder: ${error instanceof Error ? error.message : String(error)}` });
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
|
|
7306
7398
|
function detectLensFromText(text: string): Lens {
|
|
7307
7399
|
const lines = text.split("\n");
|
|
7308
7400
|
const fencedCodeBlocks = (text.match(/```[\w-]*\n[\s\S]*?```/g) ?? []).length;
|
|
@@ -7627,12 +7719,13 @@ function getStudioQuizReasoning(model: NonNullable<ExtensionContext["model"]>, t
|
|
|
7627
7719
|
async function runStudioModelText(
|
|
7628
7720
|
ctx: StudioModelRequestContext,
|
|
7629
7721
|
prompt: string,
|
|
7630
|
-
options?: { systemPrompt?: string; maxTokens?: number; signal?: AbortSignal; reasoning?: ThinkingLevel; timeoutMs?: number; trim?: boolean },
|
|
7722
|
+
options?: { systemPrompt?: string; maxTokens?: number; signal?: AbortSignal; reasoning?: ThinkingLevel; timeoutMs?: number; trim?: boolean; model?: NonNullable<ExtensionContext["model"]> },
|
|
7631
7723
|
): Promise<string> {
|
|
7632
|
-
|
|
7633
|
-
|
|
7724
|
+
const model = options?.model ?? ctx.model;
|
|
7725
|
+
if (!model) throw new Error("No active model selected.");
|
|
7726
|
+
const auth = await resolveStudioModelRequestAuth(ctx, model);
|
|
7634
7727
|
const response = await completeSimple(
|
|
7635
|
-
|
|
7728
|
+
model,
|
|
7636
7729
|
{
|
|
7637
7730
|
systemPrompt: options?.systemPrompt ?? "You are a concise assistant inside pi Studio. Return exactly the requested format.",
|
|
7638
7731
|
messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }],
|
|
@@ -7692,6 +7785,37 @@ async function runStudioQuizModelJson(
|
|
|
7692
7785
|
throw lastError ?? new Error("Model did not return valid JSON.");
|
|
7693
7786
|
}
|
|
7694
7787
|
|
|
7788
|
+
function isStudioCompletionCodeLanguage(language: string | undefined): boolean {
|
|
7789
|
+
const normalized = String(language || "").trim().toLowerCase();
|
|
7790
|
+
return new Set([
|
|
7791
|
+
"javascript",
|
|
7792
|
+
"typescript",
|
|
7793
|
+
"python",
|
|
7794
|
+
"bash",
|
|
7795
|
+
"json",
|
|
7796
|
+
"rust",
|
|
7797
|
+
"c",
|
|
7798
|
+
"cpp",
|
|
7799
|
+
"julia",
|
|
7800
|
+
"fortran",
|
|
7801
|
+
"r",
|
|
7802
|
+
"matlab",
|
|
7803
|
+
"diff",
|
|
7804
|
+
"csv",
|
|
7805
|
+
"tsv",
|
|
7806
|
+
"java",
|
|
7807
|
+
"go",
|
|
7808
|
+
"ruby",
|
|
7809
|
+
"swift",
|
|
7810
|
+
"html",
|
|
7811
|
+
"css",
|
|
7812
|
+
"xml",
|
|
7813
|
+
"yaml",
|
|
7814
|
+
"toml",
|
|
7815
|
+
"lua",
|
|
7816
|
+
]).has(normalized);
|
|
7817
|
+
}
|
|
7818
|
+
|
|
7695
7819
|
function buildStudioCompletionSuggestionPrompt(options: {
|
|
7696
7820
|
text: string;
|
|
7697
7821
|
selectionStart: number;
|
|
@@ -7701,6 +7825,7 @@ function buildStudioCompletionSuggestionPrompt(options: {
|
|
|
7701
7825
|
path?: string;
|
|
7702
7826
|
contextMode?: "cursor" | "session";
|
|
7703
7827
|
contextText?: string;
|
|
7828
|
+
previousSuggestion?: string;
|
|
7704
7829
|
}): string {
|
|
7705
7830
|
const text = String(options.text || "");
|
|
7706
7831
|
const start = Math.max(0, Math.min(Math.floor(options.selectionStart || 0), text.length));
|
|
@@ -7711,31 +7836,51 @@ function buildStudioCompletionSuggestionPrompt(options: {
|
|
|
7711
7836
|
const language = String(options.language || "").trim() || "unknown";
|
|
7712
7837
|
const label = String(options.label || options.path || "Studio editor").trim();
|
|
7713
7838
|
const contextText = String(options.contextText || "").trim().slice(-STUDIO_COMPLETION_MAX_CONTEXT_CHARS);
|
|
7839
|
+
const previousSuggestion = String(options.previousSuggestion || "").trim().slice(-4000);
|
|
7840
|
+
const editorExcerpt = selected
|
|
7841
|
+
? `${prefix}⟦SELECTION_START⟧${selected}⟦SELECTION_END⟧${suffix}`
|
|
7842
|
+
: `${prefix}⟦CURSOR⟧${suffix}`;
|
|
7843
|
+
const isCodeCompletion = isStudioCompletionCodeLanguage(language);
|
|
7844
|
+
const modeInstructions = isCodeCompletion
|
|
7845
|
+
? [
|
|
7846
|
+
"You are acting as a tab-completion model for a code editor.",
|
|
7847
|
+
"Return only the exact code/text that should be inserted if the user presses Tab. Do not wrap it in Markdown fences. Do not explain.",
|
|
7848
|
+
"Preserve syntax, indentation, delimiters, local names, comments, and the surrounding coding style.",
|
|
7849
|
+
"Partial identifiers, expressions, arguments, statements, or structured-data fragments are allowed when they are syntactically natural at the marker.",
|
|
7850
|
+
"If the marker is inside a string, comment, docstring, or markup text node, continue that local text naturally rather than applying prose sentence rules globally.",
|
|
7851
|
+
"Keep the completion local and short unless the surrounding code clearly calls for a larger block.",
|
|
7852
|
+
]
|
|
7853
|
+
: [
|
|
7854
|
+
"You are acting as a tab-completion model for a text editor.",
|
|
7855
|
+
"Return only the exact text that should be inserted if the user presses Tab. Do not wrap it in Markdown fences. Do not explain.",
|
|
7856
|
+
"Do not return a sentence fragment, dependent clause, or lowercase noun phrase unless it is grammatically valid immediately at the marker.",
|
|
7857
|
+
"If the marker follows a completed sentence and you continue with prose, begin with any needed whitespace and a complete new sentence using normal capitalization.",
|
|
7858
|
+
"Return a non-empty completion. If the cursor is at the end of a sentence or paragraph, continue with a plausible complete sentence rather than a fragment.",
|
|
7859
|
+
"Match the surrounding language, style, indentation, and register.",
|
|
7860
|
+
"Keep the suggestion short unless the context clearly asks for a longer continuation.",
|
|
7861
|
+
];
|
|
7714
7862
|
return [
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7863
|
+
...modeInstructions,
|
|
7864
|
+
selected
|
|
7865
|
+
? "The text between ⟦SELECTION_START⟧ and ⟦SELECTION_END⟧ is selected. Your answer will replace only that selected text."
|
|
7866
|
+
: "The cursor is marked by ⟦CURSOR⟧. Your answer will replace only that marker.",
|
|
7867
|
+
"The text before the marker is already written. Do not rewrite it, paraphrase it, or continue from an earlier point in the excerpt.",
|
|
7868
|
+
"After replacing the marker or selected range with your answer, the excerpt must read naturally at that exact position.",
|
|
7869
|
+
"Include any needed leading whitespace or punctuation; do not assume the editor will add it.",
|
|
7719
7870
|
contextText
|
|
7720
7871
|
? "Use the extra session context only as background. Do not continue the extra context directly unless the editor cursor calls for it."
|
|
7721
7872
|
: "Use only the cursor-local editor context below.",
|
|
7722
|
-
|
|
7723
|
-
? "The selected text will be replaced by the completion."
|
|
7724
|
-
: "The completion will be inserted at the cursor.",
|
|
7873
|
+
previousSuggestion ? "The user asked for another suggestion. Avoid repeating the previous suggestion; offer a materially different continuation that still fits the same cursor context." : "",
|
|
7725
7874
|
"",
|
|
7726
7875
|
`File/context label: ${label}`,
|
|
7727
7876
|
`Language mode: ${language}`,
|
|
7728
7877
|
`Suggestion context mode: ${contextText ? "editor plus latest response" : "editor only"}`,
|
|
7729
7878
|
contextText ? ["", "<extra_context>", contextText, "</extra_context>"].join("\n") : "",
|
|
7879
|
+
previousSuggestion ? ["", "<previous_suggestion>", previousSuggestion, "</previous_suggestion>"].join("\n") : "",
|
|
7730
7880
|
"",
|
|
7731
|
-
"<
|
|
7732
|
-
|
|
7733
|
-
"</
|
|
7734
|
-
selected ? ["", "<selected>", selected, "</selected>"].join("\n") : "",
|
|
7735
|
-
"",
|
|
7736
|
-
"<suffix>",
|
|
7737
|
-
suffix,
|
|
7738
|
-
"</suffix>",
|
|
7881
|
+
"<editor_excerpt>",
|
|
7882
|
+
editorExcerpt,
|
|
7883
|
+
"</editor_excerpt>",
|
|
7739
7884
|
].filter((part) => part !== "").join("\n");
|
|
7740
7885
|
}
|
|
7741
7886
|
|
|
@@ -7754,13 +7899,19 @@ async function runStudioCompletionSuggestion(ctx: StudioModelRequestContext, opt
|
|
|
7754
7899
|
path?: string;
|
|
7755
7900
|
contextMode?: "cursor" | "session";
|
|
7756
7901
|
contextText?: string;
|
|
7902
|
+
previousSuggestion?: string;
|
|
7903
|
+
model?: NonNullable<ExtensionContext["model"]>;
|
|
7757
7904
|
signal?: AbortSignal;
|
|
7758
7905
|
}): Promise<string> {
|
|
7759
7906
|
const prompt = buildStudioCompletionSuggestionPrompt(options);
|
|
7907
|
+
const systemPrompt = isStudioCompletionCodeLanguage(options.language)
|
|
7908
|
+
? "You are a code tab-completion engine inside pi Studio. Return only the exact code/text that replaces the cursor marker or selected range in the provided editor excerpt. The resulting excerpt must be syntactically natural at that exact position. Include needed leading whitespace. Never explain. Never include Markdown fences unless literal fences are the intended insertion."
|
|
7909
|
+
: "You are a prose tab-completion engine inside pi Studio. Return only the exact text that replaces the cursor marker or selected range in the provided editor excerpt. The resulting excerpt must read naturally at that exact position. Include needed leading whitespace. Never explain. Never include Markdown fences unless literal fences are the intended insertion.";
|
|
7760
7910
|
// Intentionally omit `reasoning`: pi-ai treats absent reasoning as off/disabled
|
|
7761
7911
|
// where supported. Passing "minimal" would still enable a reasoning path and slow completions.
|
|
7762
7912
|
const suggestion = cleanStudioCompletionSuggestion(await runStudioModelText(ctx, prompt, {
|
|
7763
|
-
systemPrompt
|
|
7913
|
+
systemPrompt,
|
|
7914
|
+
model: options.model,
|
|
7764
7915
|
maxTokens: 650,
|
|
7765
7916
|
timeoutMs: 60_000,
|
|
7766
7917
|
trim: false,
|
|
@@ -8186,6 +8337,24 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
8186
8337
|
};
|
|
8187
8338
|
}
|
|
8188
8339
|
|
|
8340
|
+
if (msg.type === "pi_model_select_request" && typeof msg.provider === "string" && typeof msg.id === "string") {
|
|
8341
|
+
return {
|
|
8342
|
+
type: "pi_model_select_request",
|
|
8343
|
+
provider: msg.provider,
|
|
8344
|
+
id: msg.id,
|
|
8345
|
+
};
|
|
8346
|
+
}
|
|
8347
|
+
|
|
8348
|
+
if (msg.type === "pi_thinking_level_request" && typeof msg.level === "string") {
|
|
8349
|
+
const level = msg.level.trim().toLowerCase();
|
|
8350
|
+
if (level === "off" || level === "minimal" || level === "low" || level === "medium" || level === "high" || level === "xhigh") {
|
|
8351
|
+
return {
|
|
8352
|
+
type: "pi_thinking_level_request",
|
|
8353
|
+
level,
|
|
8354
|
+
};
|
|
8355
|
+
}
|
|
8356
|
+
}
|
|
8357
|
+
|
|
8189
8358
|
if (msg.type === "completion_suggestion_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
|
|
8190
8359
|
const textLength = msg.text.length;
|
|
8191
8360
|
const rawStart = typeof msg.selectionStart === "number" && Number.isFinite(msg.selectionStart) ? msg.selectionStart : textLength;
|
|
@@ -8204,6 +8373,9 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
8204
8373
|
path: typeof msg.path === "string" ? msg.path : undefined,
|
|
8205
8374
|
contextMode,
|
|
8206
8375
|
contextText: contextMode === "session" && typeof msg.contextText === "string" ? msg.contextText.slice(-STUDIO_COMPLETION_MAX_CONTEXT_CHARS) : undefined,
|
|
8376
|
+
previousSuggestion: typeof msg.previousSuggestion === "string" ? msg.previousSuggestion.slice(-4000) : undefined,
|
|
8377
|
+
suggestionModelProvider: typeof msg.suggestionModelProvider === "string" ? msg.suggestionModelProvider : undefined,
|
|
8378
|
+
suggestionModelId: typeof msg.suggestionModelId === "string" ? msg.suggestionModelId : undefined,
|
|
8207
8379
|
};
|
|
8208
8380
|
}
|
|
8209
8381
|
|
|
@@ -9760,6 +9932,12 @@ function formatModelLabelWithThinking(modelLabel: string, thinkingLevel?: string
|
|
|
9760
9932
|
return `${base} (${level})`;
|
|
9761
9933
|
}
|
|
9762
9934
|
|
|
9935
|
+
function formatStudioModelOptionLabel(model: { provider?: string; id?: string; name?: string } | undefined): string {
|
|
9936
|
+
const base = formatModelLabel(model);
|
|
9937
|
+
const name = typeof model?.name === "string" ? model.name.trim() : "";
|
|
9938
|
+
return name && name !== model?.id ? `${name} (${base})` : base;
|
|
9939
|
+
}
|
|
9940
|
+
|
|
9763
9941
|
function buildTerminalSessionLabel(cwd: string, sessionName?: string): string {
|
|
9764
9942
|
const cwdBase = basename(cwd || process.cwd() || "") || cwd || "~";
|
|
9765
9943
|
const termProgram = String(process.env.TERM_PROGRAM ?? "").trim();
|
|
@@ -10073,7 +10251,7 @@ ${cssVarsBlock}
|
|
|
10073
10251
|
<button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
|
|
10074
10252
|
<button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
|
|
10075
10253
|
<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>
|
|
10076
|
-
<label class="file-label" title="
|
|
10254
|
+
<label class="file-label" title="Browser import: load a selected text file as a detached copy. Use Save editor as… to attach this copy to a file path and make it file-backed, or use the Files view to open a refreshable file-backed document directly.">Import file copy…<input id="fileInput" type="file" 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" /></label>
|
|
10077
10255
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
10078
10256
|
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
|
|
10079
10257
|
</div>
|
|
@@ -10127,6 +10305,9 @@ ${cssVarsBlock}
|
|
|
10127
10305
|
<option value="cursor" selected>Context: editor only</option>
|
|
10128
10306
|
<option value="session">Context: editor + latest response</option>
|
|
10129
10307
|
</select>
|
|
10308
|
+
<select id="completionModelSelect" hidden aria-label="Suggestion model" title="Choose the model used for Suggest. Suggestions use direct completion with thinking off and do not change the main Pi model.">
|
|
10309
|
+
<option value="current" selected>Suggestion model: current Pi model</option>
|
|
10310
|
+
</select>
|
|
10130
10311
|
<button id="openCompanionBtn" type="button" title="Open a blank editor-only Studio tab.">New editor tab</button>
|
|
10131
10312
|
<button id="sendEditorBtn" type="button">Send current text to Pi editor</button>
|
|
10132
10313
|
</div>
|
|
@@ -10213,11 +10394,12 @@ ${cssVarsBlock}
|
|
|
10213
10394
|
</div>
|
|
10214
10395
|
<div id="completionSuggestionPanel" class="completion-suggestion-panel" hidden>
|
|
10215
10396
|
<div class="completion-suggestion-header">
|
|
10216
|
-
<strong>Suggested completion</strong>
|
|
10397
|
+
<div><strong>Suggested completion</strong><span id="completionSuggestionMeta" class="completion-suggestion-meta"></span></div>
|
|
10217
10398
|
<button id="completionSuggestionDismissBtn" type="button" title="Dismiss this suggestion">Dismiss</button>
|
|
10218
10399
|
</div>
|
|
10219
10400
|
<pre id="completionSuggestionText" class="completion-suggestion-text"></pre>
|
|
10220
10401
|
<div class="completion-suggestion-actions">
|
|
10402
|
+
<button id="completionSuggestionRegenerateBtn" type="button" title="Ask for a different suggestion at the same cursor position.">Try another</button>
|
|
10221
10403
|
<button id="completionSuggestionInsertBtn" type="button" title="Insert this suggestion at the cursor or original selection. You can also press Tab while the editor is focused.">Insert suggestion (Tab)</button>
|
|
10222
10404
|
</div>
|
|
10223
10405
|
</div>
|
|
@@ -10352,7 +10534,8 @@ ${cssVarsBlock}
|
|
|
10352
10534
|
|
|
10353
10535
|
<footer>
|
|
10354
10536
|
<span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
|
|
10355
|
-
<span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><
|
|
10537
|
+
<span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><button id="footerMetaModel" class="footer-meta-part footer-meta-model footer-model-btn" type="button" aria-haspopup="menu" aria-expanded="false">${initialModel}</button><span class="footer-meta-sep">·</span><span id="footerMetaTerminal" class="footer-meta-part footer-meta-terminal">${initialTerminal}</span><span class="footer-meta-sep">·</span><span id="footerMetaContext" class="footer-meta-part footer-meta-context">unknown</span></span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
|
|
10538
|
+
<div id="footerModelMenu" class="footer-model-menu" hidden></div>
|
|
10356
10539
|
<button id="shortcutsBtn" class="shortcut-hint" type="button" title="Show Studio keyboard shortcuts. Press ? when not editing text.">Shortcuts (?)</button>
|
|
10357
10540
|
</footer>
|
|
10358
10541
|
|
|
@@ -10473,7 +10656,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
10473
10656
|
let terminalActivityToolName: string | null = null;
|
|
10474
10657
|
let terminalActivityLabel: string | null = null;
|
|
10475
10658
|
let lastSpecificToolActivityLabel: string | null = null;
|
|
10476
|
-
let currentModel: { provider?: string; id?: string } | undefined;
|
|
10659
|
+
let currentModel: { provider?: string; id?: string; name?: string; reasoning?: boolean } | undefined;
|
|
10477
10660
|
let currentModelLabel = "none";
|
|
10478
10661
|
let terminalSessionLabel = buildTerminalSessionLabel(studioCwd);
|
|
10479
10662
|
let terminalSessionDetail = buildTerminalSessionDetail(studioCwd);
|
|
@@ -10746,15 +10929,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
10746
10929
|
}
|
|
10747
10930
|
};
|
|
10748
10931
|
|
|
10749
|
-
const getThinkingLevelSafe = ():
|
|
10932
|
+
const getThinkingLevelSafe = (): ModelThinkingLevel | undefined => {
|
|
10750
10933
|
try {
|
|
10751
|
-
return pi.getThinkingLevel();
|
|
10934
|
+
return pi.getThinkingLevel() as ModelThinkingLevel;
|
|
10752
10935
|
} catch {
|
|
10753
10936
|
return undefined;
|
|
10754
10937
|
}
|
|
10755
10938
|
};
|
|
10756
10939
|
|
|
10757
|
-
const
|
|
10940
|
+
const setThinkingLevelSafe = (level: ModelThinkingLevel) => {
|
|
10941
|
+
// Pi's CLI/model config support "off" as a thinking level; some extension API typings still expose the narrower reasoning-only type.
|
|
10942
|
+
(pi.setThinkingLevel as (nextLevel: ModelThinkingLevel) => void)(level);
|
|
10943
|
+
};
|
|
10944
|
+
|
|
10945
|
+
const refreshRuntimeMetadata = (ctx?: { cwd?: string; model?: { provider?: string; id?: string; name?: string; reasoning?: boolean } | undefined }) => {
|
|
10758
10946
|
if (ctx?.cwd) {
|
|
10759
10947
|
studioCwd = ctx.cwd;
|
|
10760
10948
|
}
|
|
@@ -10763,6 +10951,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
10763
10951
|
currentModel = {
|
|
10764
10952
|
provider: ctx.model.provider,
|
|
10765
10953
|
id: ctx.model.id,
|
|
10954
|
+
name: ctx.model.name,
|
|
10955
|
+
reasoning: Boolean(ctx.model.reasoning),
|
|
10766
10956
|
};
|
|
10767
10957
|
} else {
|
|
10768
10958
|
currentModel = undefined;
|
|
@@ -10771,6 +10961,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
10771
10961
|
currentModel = {
|
|
10772
10962
|
provider: lastCommandCtx.model.provider,
|
|
10773
10963
|
id: lastCommandCtx.model.id,
|
|
10964
|
+
name: lastCommandCtx.model.name,
|
|
10965
|
+
reasoning: Boolean(lastCommandCtx.model.reasoning),
|
|
10774
10966
|
};
|
|
10775
10967
|
}
|
|
10776
10968
|
const baseModelLabel = formatModelLabel(currentModel);
|
|
@@ -11455,11 +11647,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
11455
11647
|
broadcastState();
|
|
11456
11648
|
};
|
|
11457
11649
|
|
|
11650
|
+
const getStudioModelOptions = () => {
|
|
11651
|
+
const registry = lastCommandCtx?.modelRegistry ?? latestModelRequestCtx?.modelRegistry;
|
|
11652
|
+
if (!registry || typeof registry.getAvailable !== "function") return [];
|
|
11653
|
+
return registry.getAvailable().map((model) => ({
|
|
11654
|
+
provider: model.provider,
|
|
11655
|
+
id: model.id,
|
|
11656
|
+
label: formatStudioModelOptionLabel(model),
|
|
11657
|
+
reasoning: Boolean(model.reasoning),
|
|
11658
|
+
}));
|
|
11659
|
+
};
|
|
11660
|
+
|
|
11661
|
+
const getCurrentStudioModelDescriptor = () => currentModel
|
|
11662
|
+
? {
|
|
11663
|
+
provider: currentModel.provider,
|
|
11664
|
+
id: currentModel.id,
|
|
11665
|
+
label: formatStudioModelOptionLabel(currentModel),
|
|
11666
|
+
reasoning: Boolean(currentModel.reasoning),
|
|
11667
|
+
}
|
|
11668
|
+
: null;
|
|
11669
|
+
|
|
11458
11670
|
const broadcastState = () => {
|
|
11459
11671
|
terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
|
|
11460
11672
|
terminalSessionDetail = buildTerminalSessionDetail(studioCwd, getSessionNameSafe());
|
|
11461
11673
|
currentModelLabel = formatModelLabelWithThinking(formatModelLabel(currentModel), getThinkingLevelSafe());
|
|
11462
11674
|
refreshContextUsage();
|
|
11675
|
+
const modelOptions = getStudioModelOptions();
|
|
11463
11676
|
broadcast({
|
|
11464
11677
|
type: "studio_state",
|
|
11465
11678
|
busy: isStudioBusy(),
|
|
@@ -11468,6 +11681,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
11468
11681
|
terminalToolName: terminalActivityToolName,
|
|
11469
11682
|
terminalActivityLabel,
|
|
11470
11683
|
modelLabel: currentModelLabel,
|
|
11684
|
+
currentModel: getCurrentStudioModelDescriptor(),
|
|
11685
|
+
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
11686
|
+
piModels: modelOptions,
|
|
11687
|
+
suggestionModels: modelOptions,
|
|
11471
11688
|
terminalSessionLabel,
|
|
11472
11689
|
terminalSessionDetail,
|
|
11473
11690
|
contextTokens: contextUsageSnapshot.tokens,
|
|
@@ -11763,6 +11980,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
11763
11980
|
terminalToolName: terminalActivityToolName,
|
|
11764
11981
|
terminalActivityLabel,
|
|
11765
11982
|
modelLabel: currentModelLabel,
|
|
11983
|
+
currentModel: getCurrentStudioModelDescriptor(),
|
|
11984
|
+
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
11985
|
+
piModels: getStudioModelOptions(),
|
|
11986
|
+
suggestionModels: getStudioModelOptions(),
|
|
11766
11987
|
terminalSessionLabel,
|
|
11767
11988
|
terminalSessionDetail,
|
|
11768
11989
|
contextTokens: contextUsageSnapshot.tokens,
|
|
@@ -11781,6 +12002,47 @@ export default function (pi: ExtensionAPI) {
|
|
|
11781
12002
|
return;
|
|
11782
12003
|
}
|
|
11783
12004
|
|
|
12005
|
+
if (msg.type === "pi_model_select_request") {
|
|
12006
|
+
void (async () => {
|
|
12007
|
+
const registry = lastCommandCtx?.modelRegistry ?? latestModelRequestCtx?.modelRegistry;
|
|
12008
|
+
if (!registry || typeof registry.find !== "function") {
|
|
12009
|
+
sendToClient(client, { type: "info", level: "warning", message: "Pi model registry is not available yet." });
|
|
12010
|
+
return;
|
|
12011
|
+
}
|
|
12012
|
+
const model = registry.find(msg.provider, msg.id);
|
|
12013
|
+
if (!model) {
|
|
12014
|
+
sendToClient(client, { type: "info", level: "warning", message: `Pi model not found: ${msg.provider}/${msg.id}` });
|
|
12015
|
+
return;
|
|
12016
|
+
}
|
|
12017
|
+
try {
|
|
12018
|
+
const ok = await pi.setModel(model);
|
|
12019
|
+
if (!ok) {
|
|
12020
|
+
sendToClient(client, { type: "info", level: "warning", message: `Could not switch to ${formatStudioModelOptionLabel(model)}; credentials may be unavailable.` });
|
|
12021
|
+
return;
|
|
12022
|
+
}
|
|
12023
|
+
latestModelRequestCtx = { model, modelRegistry: registry };
|
|
12024
|
+
refreshRuntimeMetadata({ model });
|
|
12025
|
+
broadcastState();
|
|
12026
|
+
sendToClient(client, { type: "info", level: "info", message: `Pi model switched to ${formatStudioModelOptionLabel(model)}.` });
|
|
12027
|
+
} catch (error) {
|
|
12028
|
+
sendToClient(client, { type: "info", level: "error", message: `Model switch failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
12029
|
+
}
|
|
12030
|
+
})();
|
|
12031
|
+
return;
|
|
12032
|
+
}
|
|
12033
|
+
|
|
12034
|
+
if (msg.type === "pi_thinking_level_request") {
|
|
12035
|
+
try {
|
|
12036
|
+
setThinkingLevelSafe(msg.level);
|
|
12037
|
+
refreshRuntimeMetadata({ model: lastCommandCtx?.model ?? latestModelRequestCtx?.model });
|
|
12038
|
+
broadcastState();
|
|
12039
|
+
sendToClient(client, { type: "info", level: "info", message: `Pi thinking level set to ${getThinkingLevelSafe() ?? msg.level}.` });
|
|
12040
|
+
} catch (error) {
|
|
12041
|
+
sendToClient(client, { type: "info", level: "error", message: `Thinking level change failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
12042
|
+
}
|
|
12043
|
+
return;
|
|
12044
|
+
}
|
|
12045
|
+
|
|
11784
12046
|
if (msg.type === "get_latest_response") {
|
|
11785
12047
|
if (!lastStudioResponse) {
|
|
11786
12048
|
sendToClient(client, { type: "info", message: "No latest assistant response is available yet." });
|
|
@@ -12063,10 +12325,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
12063
12325
|
return;
|
|
12064
12326
|
}
|
|
12065
12327
|
sendToClient(client, { type: "completion_suggestion_progress", requestId: msg.requestId, message: "Generating suggestion…" });
|
|
12328
|
+
let suggestionModel: NonNullable<ExtensionContext["model"]> | undefined;
|
|
12329
|
+
if (msg.suggestionModelProvider && msg.suggestionModelId) {
|
|
12330
|
+
suggestionModel = ctx.modelRegistry.find(msg.suggestionModelProvider, msg.suggestionModelId);
|
|
12331
|
+
if (!suggestionModel) {
|
|
12332
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: `Suggestion model not found: ${msg.suggestionModelProvider}/${msg.suggestionModelId}` });
|
|
12333
|
+
return;
|
|
12334
|
+
}
|
|
12335
|
+
}
|
|
12066
12336
|
const completionController = new AbortController();
|
|
12067
12337
|
activeCompletionSuggestions.set(msg.requestId, completionController);
|
|
12068
12338
|
void (async () => {
|
|
12069
12339
|
try {
|
|
12340
|
+
const activeSuggestionModel = suggestionModel ?? ctx.model;
|
|
12070
12341
|
const suggestion = await runStudioCompletionSuggestion(ctx, {
|
|
12071
12342
|
text: msg.text,
|
|
12072
12343
|
selectionStart: msg.selectionStart,
|
|
@@ -12076,12 +12347,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
12076
12347
|
path: msg.path,
|
|
12077
12348
|
contextMode: msg.contextMode,
|
|
12078
12349
|
contextText: msg.contextText,
|
|
12350
|
+
previousSuggestion: msg.previousSuggestion,
|
|
12351
|
+
model: suggestionModel,
|
|
12079
12352
|
signal: completionController.signal,
|
|
12080
12353
|
});
|
|
12081
12354
|
sendToClient(client, {
|
|
12082
12355
|
type: "completion_suggestion_result",
|
|
12083
12356
|
requestId: msg.requestId,
|
|
12084
12357
|
suggestion,
|
|
12358
|
+
modelLabel: formatStudioModelOptionLabel(activeSuggestionModel),
|
|
12085
12359
|
selectionStart: msg.selectionStart,
|
|
12086
12360
|
selectionEnd: msg.selectionEnd,
|
|
12087
12361
|
});
|
|
@@ -13706,6 +13980,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
13706
13980
|
return;
|
|
13707
13981
|
}
|
|
13708
13982
|
|
|
13983
|
+
if (requestUrl.pathname === "/file-browser-open") {
|
|
13984
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
13985
|
+
if (token !== serverState.token) {
|
|
13986
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
13987
|
+
return;
|
|
13988
|
+
}
|
|
13989
|
+
|
|
13990
|
+
void handleOpenStudioFileBrowserDirectoryRequest(req, res, studioCwd).catch((error) => {
|
|
13991
|
+
respondJson(res, 500, { ok: false, error: `Open folder failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
13992
|
+
});
|
|
13993
|
+
return;
|
|
13994
|
+
}
|
|
13995
|
+
|
|
13709
13996
|
if (requestUrl.pathname === "/local-preview-link") {
|
|
13710
13997
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
13711
13998
|
if (token !== serverState.token) {
|