pi-studio 0.9.23 → 0.9.24
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 +13 -0
- package/README.md +9 -1
- package/client/studio-client.js +331 -69
- package/client/studio.css +364 -28
- package/index.ts +414 -30
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -458,8 +458,8 @@ interface GetFromEditorRequestMessage {
|
|
|
458
458
|
requestId: string;
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
interface
|
|
462
|
-
type: "
|
|
461
|
+
interface GitChangesRequestMessage {
|
|
462
|
+
type: "git_changes_request";
|
|
463
463
|
requestId: string;
|
|
464
464
|
sourcePath?: string;
|
|
465
465
|
resourceDir?: string;
|
|
@@ -504,7 +504,7 @@ type IncomingStudioMessage =
|
|
|
504
504
|
| RefreshFromDiskRequestMessage
|
|
505
505
|
| SendToEditorRequestMessage
|
|
506
506
|
| GetFromEditorRequestMessage
|
|
507
|
-
|
|
|
507
|
+
| GitChangesRequestMessage
|
|
508
508
|
| OpenEditorOnlyRequestMessage
|
|
509
509
|
| CancelRequestMessage;
|
|
510
510
|
|
|
@@ -569,6 +569,46 @@ const STUDIO_REPL_STATUS_TOOL_PARAMS = Type.Object({
|
|
|
569
569
|
sessionName: Type.Optional(Type.String({ description: "Exact Studio/pi-repl tmux session name to inspect." })),
|
|
570
570
|
target: Type.Optional(Type.String({ description: "Optional runtime target: shell, python, ipython, julia, r, ghci, or clojure. If omitted, report all Studio-visible REPL sessions." })),
|
|
571
571
|
});
|
|
572
|
+
const STUDIO_EXPORT_INPUT_FORMAT_DESCRIPTION = "Optional input format: auto, markdown, or latex. Defaults to auto.";
|
|
573
|
+
const STUDIO_EXPORT_PDF_OPTIONS_TOOL_PARAMS = Type.Object({
|
|
574
|
+
fontsize: Type.Optional(Type.String({ description: "PDF body font size, e.g. 12pt." })),
|
|
575
|
+
margin: Type.Optional(Type.String({ description: "PDF page margin, e.g. 25mm." })),
|
|
576
|
+
marginTop: Type.Optional(Type.String({ description: "PDF top margin, e.g. 30mm." })),
|
|
577
|
+
marginRight: Type.Optional(Type.String({ description: "PDF right margin, e.g. 25mm." })),
|
|
578
|
+
marginBottom: Type.Optional(Type.String({ description: "PDF bottom margin, e.g. 30mm." })),
|
|
579
|
+
marginLeft: Type.Optional(Type.String({ description: "PDF left margin, e.g. 25mm." })),
|
|
580
|
+
footskip: Type.Optional(Type.String({ description: "PDF footer skip, e.g. 12mm." })),
|
|
581
|
+
linestretch: Type.Optional(Type.String({ description: "PDF line stretch, e.g. 1.2." })),
|
|
582
|
+
mainfont: Type.Optional(Type.String({ description: "PDF main font, e.g. TeX Gyre Pagella." })),
|
|
583
|
+
papersize: Type.Optional(Type.String({ description: "PDF paper size, e.g. a4 or letter." })),
|
|
584
|
+
geometry: Type.Optional(Type.String({ description: "Pandoc geometry spec. Use instead of margin fields." })),
|
|
585
|
+
sectionSize: Type.Optional(Type.String({ description: "PDF section heading size, e.g. 24pt." })),
|
|
586
|
+
subsectionSize: Type.Optional(Type.String({ description: "PDF subsection heading size, e.g. 18pt." })),
|
|
587
|
+
subsubsectionSize: Type.Optional(Type.String({ description: "PDF subsubsection heading size, e.g. 14pt." })),
|
|
588
|
+
sectionSpaceBefore: Type.Optional(Type.String({ description: "Space before section headings, e.g. 10mm." })),
|
|
589
|
+
sectionSpaceAfter: Type.Optional(Type.String({ description: "Space after section headings, e.g. 6mm." })),
|
|
590
|
+
subsectionSpaceBefore: Type.Optional(Type.String({ description: "Space before subsection headings, e.g. 8mm." })),
|
|
591
|
+
subsectionSpaceAfter: Type.Optional(Type.String({ description: "Space after subsection headings, e.g. 4mm." })),
|
|
592
|
+
});
|
|
593
|
+
const STUDIO_EXPORT_PDF_TOOL_PARAMS = Type.Object({
|
|
594
|
+
path: Type.Optional(Type.String({ description: "Local Markdown/LaTeX/code file to export. Omit to export markdown or the last model response." })),
|
|
595
|
+
markdown: Type.Optional(Type.String({ description: "Markdown or LaTeX content to export directly. Omit when exporting a file or the last model response." })),
|
|
596
|
+
outputPath: Type.Optional(Type.String({ description: "Output PDF path. Relative paths resolve against the current working directory." })),
|
|
597
|
+
resourceDir: Type.Optional(Type.String({ description: "Base directory for resolving relative images/assets when exporting direct markdown." })),
|
|
598
|
+
title: Type.Optional(Type.String({ description: "Title/source label for direct markdown exports." })),
|
|
599
|
+
inputFormat: Type.Optional(Type.String({ description: STUDIO_EXPORT_INPUT_FORMAT_DESCRIPTION })),
|
|
600
|
+
open: Type.Optional(Type.Boolean({ description: "Open the exported PDF locally after writing it. Defaults to false for tool use." })),
|
|
601
|
+
pdfOptions: Type.Optional(STUDIO_EXPORT_PDF_OPTIONS_TOOL_PARAMS),
|
|
602
|
+
});
|
|
603
|
+
const STUDIO_EXPORT_HTML_TOOL_PARAMS = Type.Object({
|
|
604
|
+
path: Type.Optional(Type.String({ description: "Local Markdown/LaTeX/code file to export. Omit to export markdown or the last model response." })),
|
|
605
|
+
markdown: Type.Optional(Type.String({ description: "Markdown or LaTeX content to export directly. Omit when exporting a file or the last model response." })),
|
|
606
|
+
outputPath: Type.Optional(Type.String({ description: "Output HTML path. Relative paths resolve against the current working directory." })),
|
|
607
|
+
resourceDir: Type.Optional(Type.String({ description: "Base directory for resolving relative images/assets when exporting direct markdown." })),
|
|
608
|
+
title: Type.Optional(Type.String({ description: "HTML document title/source label for direct markdown exports." })),
|
|
609
|
+
inputFormat: Type.Optional(Type.String({ description: STUDIO_EXPORT_INPUT_FORMAT_DESCRIPTION })),
|
|
610
|
+
open: Type.Optional(Type.Boolean({ description: "Open the exported HTML locally after writing it. Defaults to false for tool use." })),
|
|
611
|
+
});
|
|
572
612
|
const MAX_STUDIO_TRACE_SNAPSHOTS = RESPONSE_HISTORY_LIMIT;
|
|
573
613
|
const TRANSIENT_STUDIO_DOCUMENT_TTL_MS = 30 * 60 * 1000;
|
|
574
614
|
const MAX_TRANSIENT_STUDIO_DOCUMENTS = 16;
|
|
@@ -2484,6 +2524,62 @@ function buildStudioSyntheticNewFileDiff(filePath: string, content: string): str
|
|
|
2484
2524
|
return diffLines.join("\n");
|
|
2485
2525
|
}
|
|
2486
2526
|
|
|
2527
|
+
interface StudioGitChangedFile {
|
|
2528
|
+
path: string;
|
|
2529
|
+
oldPath?: string;
|
|
2530
|
+
status: "modified" | "added" | "deleted" | "renamed" | "untracked" | "binary";
|
|
2531
|
+
additions: number;
|
|
2532
|
+
deletions: number;
|
|
2533
|
+
diff: string;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
function unquoteStudioGitPath(path: string): string {
|
|
2537
|
+
const value = String(path ?? "").trim();
|
|
2538
|
+
if (!value.startsWith('"') || !value.endsWith('"')) return value;
|
|
2539
|
+
try {
|
|
2540
|
+
return JSON.parse(value) as string;
|
|
2541
|
+
} catch {
|
|
2542
|
+
return value.slice(1, -1);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
function summarizeStudioGitDiffFiles(diffText: string, untrackedPaths: Set<string>): StudioGitChangedFile[] {
|
|
2547
|
+
const matches = Array.from(String(diffText ?? "").matchAll(/^diff --git a\/(.*?) b\/(.*?)$/gm));
|
|
2548
|
+
const files: StudioGitChangedFile[] = [];
|
|
2549
|
+
for (let i = 0; i < matches.length; i += 1) {
|
|
2550
|
+
const match = matches[i]!;
|
|
2551
|
+
const start = match.index ?? 0;
|
|
2552
|
+
const end = i + 1 < matches.length ? (matches[i + 1]!.index ?? diffText.length) : diffText.length;
|
|
2553
|
+
const section = diffText.slice(start, end).trimEnd();
|
|
2554
|
+
const oldPath = unquoteStudioGitPath(match[1] ?? "");
|
|
2555
|
+
let path = unquoteStudioGitPath(match[2] ?? oldPath);
|
|
2556
|
+
const renameTo = section.match(/^rename to\s+(.+)$/m);
|
|
2557
|
+
if (renameTo) path = unquoteStudioGitPath(renameTo[1] ?? path);
|
|
2558
|
+
let status: StudioGitChangedFile["status"] = "modified";
|
|
2559
|
+
if (untrackedPaths.has(path)) status = "untracked";
|
|
2560
|
+
else if (/^rename from\s+/m.test(section) || /^rename to\s+/m.test(section)) status = "renamed";
|
|
2561
|
+
else if (/^deleted file mode\s+/m.test(section) || /^\+\+\+ \/dev\/null$/m.test(section)) status = "deleted";
|
|
2562
|
+
else if (/^new file mode\s+/m.test(section) || /^--- \/dev\/null$/m.test(section)) status = "added";
|
|
2563
|
+
else if (/^Binary files\s+/m.test(section)) status = "binary";
|
|
2564
|
+
let additions = 0;
|
|
2565
|
+
let deletions = 0;
|
|
2566
|
+
for (const line of section.split("\n")) {
|
|
2567
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
2568
|
+
if (line.startsWith("+")) additions += 1;
|
|
2569
|
+
else if (line.startsWith("-")) deletions += 1;
|
|
2570
|
+
}
|
|
2571
|
+
files.push({
|
|
2572
|
+
path,
|
|
2573
|
+
oldPath: oldPath && oldPath !== path ? oldPath : undefined,
|
|
2574
|
+
status,
|
|
2575
|
+
additions,
|
|
2576
|
+
deletions,
|
|
2577
|
+
diff: section,
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
return files;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2487
2583
|
function resolveStudioBaseDir(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
|
|
2488
2584
|
const source = typeof sourcePath === "string" ? sourcePath.trim() : "";
|
|
2489
2585
|
if (source) {
|
|
@@ -4006,7 +4102,7 @@ function injectStudioLatexEquationTags(markdown: string, sourcePath: string | un
|
|
|
4006
4102
|
}
|
|
4007
4103
|
|
|
4008
4104
|
function readStudioGitDiff(baseDir: string):
|
|
4009
|
-
| { ok: true; text: string; label: string }
|
|
4105
|
+
| { ok: true; text: string; label: string; repoRoot: string; branch: string; hasHead: boolean; files: StudioGitChangedFile[] }
|
|
4010
4106
|
| { ok: false; level: "info" | "warning" | "error"; message: string } {
|
|
4011
4107
|
const repoRootArgs = ["rev-parse", "--show-toplevel"];
|
|
4012
4108
|
const repoRootResult = spawnSync("git", repoRootArgs, {
|
|
@@ -4021,11 +4117,24 @@ function readStudioGitDiff(baseDir: string):
|
|
|
4021
4117
|
};
|
|
4022
4118
|
}
|
|
4023
4119
|
const repoRoot = repoRootResult.stdout.trim();
|
|
4120
|
+
const branchResult = spawnSync("git", ["branch", "--show-current"], {
|
|
4121
|
+
cwd: repoRoot,
|
|
4122
|
+
encoding: "utf-8",
|
|
4123
|
+
});
|
|
4124
|
+
let branch = branchResult.status === 0 ? branchResult.stdout.trim() : "";
|
|
4024
4125
|
|
|
4025
4126
|
const hasHead = spawnSync("git", ["rev-parse", "--verify", "HEAD"], {
|
|
4026
4127
|
cwd: repoRoot,
|
|
4027
4128
|
encoding: "utf-8",
|
|
4028
4129
|
}).status === 0;
|
|
4130
|
+
if (!branch && hasHead) {
|
|
4131
|
+
const revResult = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
|
|
4132
|
+
cwd: repoRoot,
|
|
4133
|
+
encoding: "utf-8",
|
|
4134
|
+
});
|
|
4135
|
+
branch = revResult.status === 0 && revResult.stdout.trim() ? `detached ${revResult.stdout.trim()}` : "detached HEAD";
|
|
4136
|
+
}
|
|
4137
|
+
if (!branch) branch = "unknown branch";
|
|
4029
4138
|
|
|
4030
4139
|
const untrackedArgs = ["ls-files", "--others", "--exclude-standard"];
|
|
4031
4140
|
const untrackedResult = spawnSync("git", untrackedArgs, {
|
|
@@ -4128,7 +4237,8 @@ function readStudioGitDiff(baseDir: string):
|
|
|
4128
4237
|
|
|
4129
4238
|
const labelBase = hasHead ? "git diff HEAD" : "git diff (no commits yet)";
|
|
4130
4239
|
const label = summaryParts.length > 0 ? `${labelBase} (${summaryParts.join(", ")})` : labelBase;
|
|
4131
|
-
|
|
4240
|
+
const files = summarizeStudioGitDiffFiles(fullDiff, new Set(untrackedPaths));
|
|
4241
|
+
return { ok: true, text: fullDiff, label, repoRoot, branch, hasHead, files };
|
|
4132
4242
|
}
|
|
4133
4243
|
|
|
4134
4244
|
function isLikelyMathExpression(expr: string): boolean {
|
|
@@ -8245,13 +8355,13 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
8245
8355
|
}
|
|
8246
8356
|
|
|
8247
8357
|
if (
|
|
8248
|
-
msg.type === "
|
|
8358
|
+
msg.type === "git_changes_request"
|
|
8249
8359
|
&& typeof msg.requestId === "string"
|
|
8250
8360
|
&& (msg.sourcePath === undefined || typeof msg.sourcePath === "string")
|
|
8251
8361
|
&& (msg.resourceDir === undefined || typeof msg.resourceDir === "string")
|
|
8252
8362
|
) {
|
|
8253
8363
|
return {
|
|
8254
|
-
type: "
|
|
8364
|
+
type: "git_changes_request",
|
|
8255
8365
|
requestId: msg.requestId,
|
|
8256
8366
|
sourcePath: typeof msg.sourcePath === "string" ? msg.sourcePath : undefined,
|
|
8257
8367
|
resourceDir: typeof msg.resourceDir === "string" ? msg.resourceDir : undefined,
|
|
@@ -9942,7 +10052,6 @@ ${cssVarsBlock}
|
|
|
9942
10052
|
<button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
|
|
9943
10053
|
<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>
|
|
9944
10054
|
<label class="file-label" title="Import a browser-selected text file into the editor as an unsaved copy. It will not be refreshable from disk until you save it.">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>
|
|
9945
|
-
<button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
|
|
9946
10055
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
9947
10056
|
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
|
|
9948
10057
|
</div>
|
|
@@ -10152,6 +10261,7 @@ ${cssVarsBlock}
|
|
|
10152
10261
|
<option value="preview" selected>Response (Preview)</option>
|
|
10153
10262
|
<option value="editor-preview">Editor (Preview)</option>
|
|
10154
10263
|
<option value="trace">Working</option>
|
|
10264
|
+
<option value="changes">Changes</option>
|
|
10155
10265
|
<option value="files">Files</option>
|
|
10156
10266
|
<option value="repl">REPL</option>
|
|
10157
10267
|
</select>
|
|
@@ -10201,10 +10311,10 @@ ${cssVarsBlock}
|
|
|
10201
10311
|
</div>
|
|
10202
10312
|
<div class="response-actions-row history-row">
|
|
10203
10313
|
<button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Fetch latest response</button>
|
|
10204
|
-
<button id="historyPrevBtn" type="button" title="Show previous response in history.">◀ Prev response</button>
|
|
10205
|
-
<span id="historyIndexBadge" class="source-badge">
|
|
10206
|
-
<button id="historyNextBtn" type="button" title="Show next response in history.">Next response ▶</button>
|
|
10207
|
-
<button id="historyLastBtn" type="button" title="Jump to the latest loaded response in history.">Last response ▶|</button>
|
|
10314
|
+
<button id="historyPrevBtn" type="button" title="Show previous response in the current branch history.">◀ Prev response</button>
|
|
10315
|
+
<span id="historyIndexBadge" class="source-badge">Branch history: 0/0</span>
|
|
10316
|
+
<button id="historyNextBtn" type="button" title="Show next response in the current branch history.">Next response ▶</button>
|
|
10317
|
+
<button id="historyLastBtn" type="button" title="Jump to the latest loaded response in the current branch history.">Last response ▶|</button>
|
|
10208
10318
|
</div>
|
|
10209
10319
|
<div class="response-actions-row response-result-row">
|
|
10210
10320
|
<button id="loadResponseBtn" type="button">Load response into editor</button>
|
|
@@ -10299,7 +10409,7 @@ ${cssVarsBlock}
|
|
|
10299
10409
|
<div class="scratchpad-footer">
|
|
10300
10410
|
<span id="scratchpadMeta" class="scratchpad-meta">Empty · local only</span>
|
|
10301
10411
|
<div class="scratchpad-actions">
|
|
10302
|
-
<button id="scratchpadRecentBtn" type="button" title="Show recent non-empty scratchpads saved for other files and drafts.">Recent…</button>
|
|
10412
|
+
<button id="scratchpadRecentBtn" type="button" aria-expanded="false" title="Show recent non-empty scratchpads saved for other files and drafts.">Recent…</button>
|
|
10303
10413
|
<button id="scratchpadInsertBtn" type="button" title="Insert the scratchpad text into the editor at the current selection, or append it if no editor selection is available.">Insert into editor</button>
|
|
10304
10414
|
<button id="scratchpadCopyBtn" type="button" title="Copy scratchpad text to the clipboard.">Copy</button>
|
|
10305
10415
|
<button id="scratchpadClearBtn" type="button" title="Clear scratchpad text.">Clear</button>
|
|
@@ -10532,6 +10642,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
10532
10642
|
},
|
|
10533
10643
|
});
|
|
10534
10644
|
|
|
10645
|
+
pi.registerTool({
|
|
10646
|
+
name: "studio_export_pdf",
|
|
10647
|
+
label: "Studio PDF export",
|
|
10648
|
+
description: "Export Markdown/LaTeX content, a local file, or the last model response to PDF using the Studio PDF pipeline.",
|
|
10649
|
+
promptSnippet: "Export Markdown/LaTeX, a local file, or the last model response to PDF with Studio's PDF pipeline.",
|
|
10650
|
+
promptGuidelines: [
|
|
10651
|
+
"Use studio_export_pdf when the user asks to make/export/render content as a PDF using Studio.",
|
|
10652
|
+
"For remote or Telegram sessions, leave open=false and report the generated file path unless a separate upload/send-file tool is available.",
|
|
10653
|
+
"Pass markdown directly when exporting content composed in the current assistant turn; omit markdown and path only when exporting the previous model response.",
|
|
10654
|
+
],
|
|
10655
|
+
parameters: STUDIO_EXPORT_PDF_TOOL_PARAMS,
|
|
10656
|
+
executionMode: "sequential",
|
|
10657
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
10658
|
+
const result = await exportStudioPdfForTool(params, ctx);
|
|
10659
|
+
return {
|
|
10660
|
+
content: [{ type: "text", text: formatStudioExportToolText(result) }],
|
|
10661
|
+
details: result as Record<string, unknown>,
|
|
10662
|
+
};
|
|
10663
|
+
},
|
|
10664
|
+
});
|
|
10665
|
+
|
|
10666
|
+
pi.registerTool({
|
|
10667
|
+
name: "studio_export_html",
|
|
10668
|
+
label: "Studio HTML export",
|
|
10669
|
+
description: "Export Markdown/LaTeX content, a local file, or the last model response to standalone HTML using the Studio preview pipeline.",
|
|
10670
|
+
promptSnippet: "Export Markdown/LaTeX, a local file, or the last model response to standalone HTML with Studio's preview pipeline.",
|
|
10671
|
+
promptGuidelines: [
|
|
10672
|
+
"Use studio_export_html when the user asks to make/export/render content as HTML using Studio.",
|
|
10673
|
+
"For remote or Telegram sessions, leave open=false and report the generated file path unless a separate upload/send-file tool is available.",
|
|
10674
|
+
"Pass markdown directly when exporting content composed in the current assistant turn; omit markdown and path only when exporting the previous model response.",
|
|
10675
|
+
],
|
|
10676
|
+
parameters: STUDIO_EXPORT_HTML_TOOL_PARAMS,
|
|
10677
|
+
executionMode: "sequential",
|
|
10678
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
10679
|
+
const result = await exportStudioHtmlForTool(params, ctx);
|
|
10680
|
+
return {
|
|
10681
|
+
content: [{ type: "text", text: formatStudioExportToolText(result) }],
|
|
10682
|
+
details: result as Record<string, unknown>,
|
|
10683
|
+
};
|
|
10684
|
+
},
|
|
10685
|
+
});
|
|
10686
|
+
|
|
10535
10687
|
const isStudioDirectRunChainActive = () => Boolean(studioDirectRunChain);
|
|
10536
10688
|
const getQueuedStudioSteeringCount = () => queuedStudioDirectRequests.length;
|
|
10537
10689
|
const getStudioClientCounts = (): { full: number; editorOnly: number } => {
|
|
@@ -11635,39 +11787,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
11635
11787
|
return;
|
|
11636
11788
|
}
|
|
11637
11789
|
|
|
11638
|
-
if (msg.type === "
|
|
11790
|
+
if (msg.type === "git_changes_request") {
|
|
11639
11791
|
if (!isValidRequestId(msg.requestId)) {
|
|
11640
11792
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
11641
11793
|
return;
|
|
11642
11794
|
}
|
|
11643
|
-
if (isStudioBusy()) {
|
|
11644
|
-
sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
|
|
11645
|
-
return;
|
|
11646
|
-
}
|
|
11647
|
-
|
|
11648
11795
|
const baseDir = resolveStudioGitDiffBaseDir(msg.sourcePath, msg.resourceDir, studioCwd);
|
|
11649
11796
|
const diffResult = readStudioGitDiff(baseDir);
|
|
11650
11797
|
if (diffResult.ok === false) {
|
|
11651
11798
|
sendToClient(client, {
|
|
11652
|
-
type: "
|
|
11799
|
+
type: "git_changes_snapshot",
|
|
11653
11800
|
requestId: msg.requestId,
|
|
11801
|
+
ok: false,
|
|
11654
11802
|
message: diffResult.message,
|
|
11655
11803
|
level: diffResult.level,
|
|
11656
11804
|
});
|
|
11657
11805
|
return;
|
|
11658
11806
|
}
|
|
11659
|
-
|
|
11660
|
-
initialStudioDocument = {
|
|
11661
|
-
text: diffResult.text,
|
|
11662
|
-
label: diffResult.label,
|
|
11663
|
-
source: "blank",
|
|
11664
|
-
};
|
|
11665
11807
|
sendToClient(client, {
|
|
11666
|
-
type: "
|
|
11808
|
+
type: "git_changes_snapshot",
|
|
11667
11809
|
requestId: msg.requestId,
|
|
11810
|
+
ok: true,
|
|
11668
11811
|
content: diffResult.text,
|
|
11669
11812
|
label: diffResult.label,
|
|
11670
|
-
|
|
11813
|
+
repoRoot: diffResult.repoRoot,
|
|
11814
|
+
branch: diffResult.branch,
|
|
11815
|
+
hasHead: diffResult.hasHead,
|
|
11816
|
+
files: diffResult.files,
|
|
11671
11817
|
});
|
|
11672
11818
|
return;
|
|
11673
11819
|
}
|
|
@@ -13865,7 +14011,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13865
14011
|
type: "info",
|
|
13866
14012
|
level: "info",
|
|
13867
14013
|
message: studioResponseHistory.length > 0
|
|
13868
|
-
? "Pi session tree changed; Studio
|
|
14014
|
+
? "Pi session tree changed; Studio branch history now follows the current branch. Editor text was left unchanged."
|
|
13869
14015
|
: "Pi session tree changed; this branch has no assistant responses yet. Editor text was left unchanged.",
|
|
13870
14016
|
});
|
|
13871
14017
|
});
|
|
@@ -14265,7 +14411,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14265
14411
|
};
|
|
14266
14412
|
};
|
|
14267
14413
|
|
|
14268
|
-
const resolveLastModelResponseForExport = (ctx:
|
|
14414
|
+
const resolveLastModelResponseForExport = (ctx: ExtensionContext): { markdown: string } | null => {
|
|
14269
14415
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
14270
14416
|
syncStudioResponseHistory(branchEntries);
|
|
14271
14417
|
const markdown =
|
|
@@ -14276,6 +14422,244 @@ export default function (pi: ExtensionAPI) {
|
|
|
14276
14422
|
return markdown.trim() ? { markdown } : null;
|
|
14277
14423
|
};
|
|
14278
14424
|
|
|
14425
|
+
type StudioExportInputFormat = "auto" | "markdown" | "latex";
|
|
14426
|
+
type StudioExportCommonToolParams = {
|
|
14427
|
+
path?: string;
|
|
14428
|
+
markdown?: string;
|
|
14429
|
+
outputPath?: string;
|
|
14430
|
+
resourceDir?: string;
|
|
14431
|
+
title?: string;
|
|
14432
|
+
inputFormat?: string;
|
|
14433
|
+
open?: boolean;
|
|
14434
|
+
};
|
|
14435
|
+
type StudioExportPdfToolParams = StudioExportCommonToolParams & { pdfOptions?: StudioPdfRenderOptions };
|
|
14436
|
+
type StudioExportSource = {
|
|
14437
|
+
text: string;
|
|
14438
|
+
sourceLabel: string;
|
|
14439
|
+
sourcePath?: string;
|
|
14440
|
+
resourcePath: string;
|
|
14441
|
+
outputPath: string;
|
|
14442
|
+
inputFormat: StudioExportInputFormat;
|
|
14443
|
+
};
|
|
14444
|
+
type StudioExportResult = {
|
|
14445
|
+
ok: boolean;
|
|
14446
|
+
format: "pdf" | "html";
|
|
14447
|
+
path?: string;
|
|
14448
|
+
source?: string;
|
|
14449
|
+
bytes?: number;
|
|
14450
|
+
warning?: string;
|
|
14451
|
+
openError?: string;
|
|
14452
|
+
error?: string;
|
|
14453
|
+
};
|
|
14454
|
+
|
|
14455
|
+
const parseStudioExportInputFormat = (value: unknown): StudioExportInputFormat | { error: string } => {
|
|
14456
|
+
const normalized = String(value ?? "auto").trim().toLowerCase();
|
|
14457
|
+
if (!normalized || normalized === "auto") return "auto";
|
|
14458
|
+
if (normalized === "markdown" || normalized === "md") return "markdown";
|
|
14459
|
+
if (normalized === "latex" || normalized === "tex") return "latex";
|
|
14460
|
+
return { error: "Invalid inputFormat. Use auto, markdown, or latex." };
|
|
14461
|
+
};
|
|
14462
|
+
|
|
14463
|
+
const resolveStudioExportOutputPath = (outputPath: string | undefined, cwd: string): string | null => {
|
|
14464
|
+
const raw = typeof outputPath === "string" ? outputPath.trim() : "";
|
|
14465
|
+
if (!raw) return null;
|
|
14466
|
+
const resolved = resolveStudioPath(raw, cwd);
|
|
14467
|
+
return resolved.ok ? resolved.resolved : null;
|
|
14468
|
+
};
|
|
14469
|
+
|
|
14470
|
+
const validateStudioPdfOptionsForTool = (input: StudioPdfRenderOptions | undefined): StudioPdfRenderOptions | { error: string } => {
|
|
14471
|
+
if (!input || typeof input !== "object") return {};
|
|
14472
|
+
const options: StudioPdfRenderOptions = {};
|
|
14473
|
+
const setOption = (key: keyof StudioPdfRenderOptions, value: string) => {
|
|
14474
|
+
(options as Record<string, string>)[key] = value;
|
|
14475
|
+
};
|
|
14476
|
+
const requireLength = (key: keyof StudioPdfRenderOptions, label: string, example: string): { error: string } | null => {
|
|
14477
|
+
const value = String(input[key] ?? "").trim();
|
|
14478
|
+
if (!value) return null;
|
|
14479
|
+
if (!isValidStudioPdfLength(value)) return { error: `Invalid ${label} value. Example: ${example}` };
|
|
14480
|
+
setOption(key, value);
|
|
14481
|
+
return null;
|
|
14482
|
+
};
|
|
14483
|
+
for (const [key, label, example] of [
|
|
14484
|
+
["fontsize", "fontsize", "12pt"],
|
|
14485
|
+
["sectionSize", "sectionSize", "24pt"],
|
|
14486
|
+
["subsectionSize", "subsectionSize", "18pt"],
|
|
14487
|
+
["subsubsectionSize", "subsubsectionSize", "14pt"],
|
|
14488
|
+
["sectionSpaceBefore", "sectionSpaceBefore", "10mm"],
|
|
14489
|
+
["sectionSpaceAfter", "sectionSpaceAfter", "6mm"],
|
|
14490
|
+
["subsectionSpaceBefore", "subsectionSpaceBefore", "8mm"],
|
|
14491
|
+
["subsectionSpaceAfter", "subsectionSpaceAfter", "4mm"],
|
|
14492
|
+
["margin", "margin", "25mm"],
|
|
14493
|
+
["marginTop", "marginTop", "30mm"],
|
|
14494
|
+
["marginRight", "marginRight", "25mm"],
|
|
14495
|
+
["marginBottom", "marginBottom", "30mm"],
|
|
14496
|
+
["marginLeft", "marginLeft", "25mm"],
|
|
14497
|
+
["footskip", "footskip", "12mm"],
|
|
14498
|
+
] as Array<[keyof StudioPdfRenderOptions, string, string]>) {
|
|
14499
|
+
const error = requireLength(key, label, example);
|
|
14500
|
+
if (error) return error;
|
|
14501
|
+
}
|
|
14502
|
+
const linestretch = String(input.linestretch ?? "").trim();
|
|
14503
|
+
if (linestretch) {
|
|
14504
|
+
if (!isValidStudioPdfLineStretch(linestretch)) return { error: "Invalid linestretch value. Example: 1.2" };
|
|
14505
|
+
options.linestretch = linestretch;
|
|
14506
|
+
}
|
|
14507
|
+
const papersize = String(input.papersize ?? "").trim();
|
|
14508
|
+
if (papersize) {
|
|
14509
|
+
if (!isValidStudioPdfPaperSize(papersize)) return { error: "Invalid papersize value. Example: a4" };
|
|
14510
|
+
options.papersize = papersize;
|
|
14511
|
+
}
|
|
14512
|
+
const mainfont = sanitizeStudioPdfFreeformOption(String(input.mainfont ?? ""));
|
|
14513
|
+
if (mainfont) options.mainfont = mainfont;
|
|
14514
|
+
const geometry = sanitizeStudioPdfFreeformOption(String(input.geometry ?? ""));
|
|
14515
|
+
if (geometry) options.geometry = geometry;
|
|
14516
|
+
if (options.geometry && (options.margin || options.marginTop || options.marginRight || options.marginBottom || options.marginLeft || options.footskip)) {
|
|
14517
|
+
return { error: "Use either geometry or the margin/margin*/footskip options, not both." };
|
|
14518
|
+
}
|
|
14519
|
+
return options;
|
|
14520
|
+
};
|
|
14521
|
+
|
|
14522
|
+
const resolveStudioExportSource = (
|
|
14523
|
+
params: StudioExportCommonToolParams,
|
|
14524
|
+
ctx: ExtensionContext,
|
|
14525
|
+
format: "pdf" | "html",
|
|
14526
|
+
): StudioExportSource | { error: string } => {
|
|
14527
|
+
const inputFormat = parseStudioExportInputFormat(params.inputFormat);
|
|
14528
|
+
if (typeof inputFormat !== "string") return inputFormat;
|
|
14529
|
+
const pathArg = String(params.path ?? "").trim();
|
|
14530
|
+
const directMarkdown = typeof params.markdown === "string" ? params.markdown : "";
|
|
14531
|
+
const hasDirectMarkdown = directMarkdown.trim().length > 0;
|
|
14532
|
+
if (pathArg && hasDirectMarkdown) return { error: "Use either path or markdown, not both." };
|
|
14533
|
+
|
|
14534
|
+
if (hasDirectMarkdown) {
|
|
14535
|
+
const outputPath = resolveStudioExportOutputPath(params.outputPath, ctx.cwd)
|
|
14536
|
+
?? buildStudioResponseExportOutputPath(ctx.cwd, format);
|
|
14537
|
+
const title = String(params.title ?? "").trim();
|
|
14538
|
+
return {
|
|
14539
|
+
text: directMarkdown,
|
|
14540
|
+
sourceLabel: title || "provided markdown",
|
|
14541
|
+
resourcePath: resolveStudioBaseDir(undefined, params.resourceDir, ctx.cwd),
|
|
14542
|
+
outputPath,
|
|
14543
|
+
inputFormat,
|
|
14544
|
+
};
|
|
14545
|
+
}
|
|
14546
|
+
|
|
14547
|
+
if (pathArg) {
|
|
14548
|
+
const file = readStudioFile(pathArg, ctx.cwd);
|
|
14549
|
+
if (file.ok === false) return { error: file.message };
|
|
14550
|
+
const outputPath = resolveStudioExportOutputPath(params.outputPath, ctx.cwd)
|
|
14551
|
+
?? (format === "pdf" ? buildStudioPdfOutputPath(file.resolvedPath) : buildStudioHtmlOutputPath(file.resolvedPath));
|
|
14552
|
+
return {
|
|
14553
|
+
text: file.text,
|
|
14554
|
+
sourceLabel: file.label,
|
|
14555
|
+
sourcePath: file.resolvedPath,
|
|
14556
|
+
resourcePath: resolveStudioBaseDir(file.resolvedPath, params.resourceDir, ctx.cwd),
|
|
14557
|
+
outputPath,
|
|
14558
|
+
inputFormat,
|
|
14559
|
+
};
|
|
14560
|
+
}
|
|
14561
|
+
|
|
14562
|
+
const response = resolveLastModelResponseForExport(ctx);
|
|
14563
|
+
if (!response) return { error: "No last model response to export. Provide path or markdown, or run a prompt first." };
|
|
14564
|
+
return {
|
|
14565
|
+
text: response.markdown,
|
|
14566
|
+
sourceLabel: "last model response",
|
|
14567
|
+
resourcePath: resolveStudioBaseDir(undefined, params.resourceDir, ctx.cwd),
|
|
14568
|
+
outputPath: resolveStudioExportOutputPath(params.outputPath, ctx.cwd) ?? buildStudioResponseExportOutputPath(ctx.cwd, format),
|
|
14569
|
+
inputFormat,
|
|
14570
|
+
};
|
|
14571
|
+
};
|
|
14572
|
+
|
|
14573
|
+
const resolveStudioExportLanguage = (source: StudioExportSource): string | undefined => {
|
|
14574
|
+
if (source.inputFormat === "latex") return "latex";
|
|
14575
|
+
if (source.inputFormat === "markdown") return "markdown";
|
|
14576
|
+
return (source.sourcePath ? inferStudioPdfLanguageFromPath(source.sourcePath) : undefined)
|
|
14577
|
+
?? inferStudioPdfLanguage(source.text);
|
|
14578
|
+
};
|
|
14579
|
+
|
|
14580
|
+
const maybeOpenStudioExportPath = async (path: string, open: boolean | undefined): Promise<string | null> => {
|
|
14581
|
+
if (!open) return null;
|
|
14582
|
+
try {
|
|
14583
|
+
await openPathInDefaultViewer(path);
|
|
14584
|
+
return null;
|
|
14585
|
+
} catch (error) {
|
|
14586
|
+
return error instanceof Error ? error.message : String(error);
|
|
14587
|
+
}
|
|
14588
|
+
};
|
|
14589
|
+
|
|
14590
|
+
const formatStudioExportToolText = (result: StudioExportResult): string => {
|
|
14591
|
+
if (!result.ok) return result.error ?? "Studio export failed.";
|
|
14592
|
+
const lines = [`Exported Studio ${result.format.toUpperCase()}: ${result.path}`];
|
|
14593
|
+
if (result.source) lines.push(`Source: ${result.source}`);
|
|
14594
|
+
if (result.bytes != null) lines.push(`Bytes: ${result.bytes}`);
|
|
14595
|
+
if (result.warning) lines.push(`Warning: ${result.warning}`);
|
|
14596
|
+
if (result.openError) lines.push(`Open warning: ${result.openError}`);
|
|
14597
|
+
return lines.join("\n");
|
|
14598
|
+
};
|
|
14599
|
+
|
|
14600
|
+
const exportStudioPdfForTool = async (params: StudioExportPdfToolParams, ctx: ExtensionContext): Promise<StudioExportResult> => {
|
|
14601
|
+
const source = resolveStudioExportSource(params, ctx, "pdf");
|
|
14602
|
+
if ("error" in source) return { ok: false, format: "pdf", error: source.error };
|
|
14603
|
+
if (source.text.length > PDF_EXPORT_MAX_CHARS) return { ok: false, format: "pdf", error: `PDF export text exceeds ${PDF_EXPORT_MAX_CHARS} characters.` };
|
|
14604
|
+
const pdfOptions = validateStudioPdfOptionsForTool(params.pdfOptions);
|
|
14605
|
+
if ("error" in pdfOptions) return { ok: false, format: "pdf", error: pdfOptions.error };
|
|
14606
|
+
const editorPdfLanguage = resolveStudioExportLanguage(source);
|
|
14607
|
+
const isLatex = editorPdfLanguage === "latex"
|
|
14608
|
+
|| (
|
|
14609
|
+
source.inputFormat !== "markdown"
|
|
14610
|
+
&& (editorPdfLanguage === undefined || editorPdfLanguage === "markdown")
|
|
14611
|
+
&& /\\documentclass\b|\\begin\{document\}/.test(source.text)
|
|
14612
|
+
);
|
|
14613
|
+
try {
|
|
14614
|
+
const { pdf, warning } = await renderStudioPdfWithPandoc(
|
|
14615
|
+
source.text,
|
|
14616
|
+
isLatex,
|
|
14617
|
+
source.resourcePath,
|
|
14618
|
+
editorPdfLanguage,
|
|
14619
|
+
source.sourcePath,
|
|
14620
|
+
pdfOptions,
|
|
14621
|
+
);
|
|
14622
|
+
await writeFile(source.outputPath, pdf);
|
|
14623
|
+
const openError = await maybeOpenStudioExportPath(source.outputPath, params.open);
|
|
14624
|
+
return { ok: true, format: "pdf", path: source.outputPath, source: source.sourceLabel, bytes: pdf.length, warning, openError: openError ?? undefined };
|
|
14625
|
+
} catch (error) {
|
|
14626
|
+
return { ok: false, format: "pdf", error: `Studio PDF export failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
14627
|
+
}
|
|
14628
|
+
};
|
|
14629
|
+
|
|
14630
|
+
const exportStudioHtmlForTool = async (params: StudioExportCommonToolParams, ctx: ExtensionContext): Promise<StudioExportResult> => {
|
|
14631
|
+
const source = resolveStudioExportSource(params, ctx, "html");
|
|
14632
|
+
if ("error" in source) return { ok: false, format: "html", error: source.error };
|
|
14633
|
+
if (source.text.length > HTML_EXPORT_MAX_CHARS) return { ok: false, format: "html", error: `HTML export text exceeds ${HTML_EXPORT_MAX_CHARS} characters.` };
|
|
14634
|
+
const editorHtmlLanguage = resolveStudioExportLanguage(source);
|
|
14635
|
+
const isLatex = editorHtmlLanguage === "latex"
|
|
14636
|
+
|| (
|
|
14637
|
+
source.inputFormat !== "markdown"
|
|
14638
|
+
&& (editorHtmlLanguage === undefined || editorHtmlLanguage === "markdown")
|
|
14639
|
+
&& isLikelyStandaloneLatexPreview(source.text)
|
|
14640
|
+
);
|
|
14641
|
+
try {
|
|
14642
|
+
const themeVars = buildThemeCssVars(getStudioThemeStyle(ctx.ui.theme));
|
|
14643
|
+
const { html, warning } = await renderStudioStandaloneHtmlWithPandoc(
|
|
14644
|
+
source.text,
|
|
14645
|
+
isLatex,
|
|
14646
|
+
source.resourcePath,
|
|
14647
|
+
editorHtmlLanguage,
|
|
14648
|
+
source.sourcePath,
|
|
14649
|
+
{
|
|
14650
|
+
title: String(params.title ?? "").trim() || basename(source.outputPath),
|
|
14651
|
+
sourceLabel: source.sourcePath ?? source.sourceLabel,
|
|
14652
|
+
themeVars,
|
|
14653
|
+
},
|
|
14654
|
+
);
|
|
14655
|
+
await writeFile(source.outputPath, html);
|
|
14656
|
+
const openError = await maybeOpenStudioExportPath(source.outputPath, params.open);
|
|
14657
|
+
return { ok: true, format: "html", path: source.outputPath, source: source.sourceLabel, bytes: html.length, warning, openError: openError ?? undefined };
|
|
14658
|
+
} catch (error) {
|
|
14659
|
+
return { ok: false, format: "html", error: `Studio HTML export failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
14660
|
+
}
|
|
14661
|
+
};
|
|
14662
|
+
|
|
14279
14663
|
const openStudioView = async (
|
|
14280
14664
|
trimmed: string,
|
|
14281
14665
|
ctx: ExtensionCommandContext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.24",
|
|
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",
|