pi-studio 0.5.55 → 0.5.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -0
- package/WORKFLOW.md +5 -3
- package/client/studio-client.js +9 -59
- package/index.ts +39 -129
- package/package.json +1 -1
- package/shared/studio-markdown-latex-literals.js +86 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.57] — 2026-04-20
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- The inserted annotated-reply scaffold now renders more cleanly in Markdown preview, using `annotated reply: below` plus a short metadata list instead of relying on hard line-break spacing.
|
|
11
|
+
- Local review-note comment boxes now use normal multiline textarea behavior, so `Enter` inserts a newline while edits continue saving automatically as you type.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Markdown preview now preserves standalone LaTeX math definition lines such as `\newcommand`, `\def`, and `\DeclareMathOperator` so custom math macros/operators can work in Markdown documents instead of showing up as literal preview text.
|
|
15
|
+
- Markdown PDF export now moves standalone LaTeX math definition lines into the generated PDF preamble when needed, avoiding LaTeX errors like `Can be used only in preamble` for commands such as `\DeclareMathOperator`.
|
|
16
|
+
- Annotated-reply header detection now accepts both the older `annotated reply below:` form and the newer `annotated reply: below` form when removing/reapplying the scaffold.
|
|
17
|
+
|
|
18
|
+
## [0.5.56] — 2026-04-15
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
- Removed Studio's standalone npm update checker and footer update badge now that package update tracking is handled in pi.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Studio now always prints its localhost URL even when automatic browser open fails, and SSH sessions get a localhost-only port-forwarding hint instead of needing non-local binding.
|
|
25
|
+
|
|
7
26
|
## [0.5.54] — 2026-04-13
|
|
8
27
|
|
|
9
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
74
74
|
## Notes
|
|
75
75
|
|
|
76
76
|
- Local-only server (`127.0.0.1`) with tokenized Studio URLs.
|
|
77
|
+
- For remote SSH sessions, keep Studio bound to localhost and use SSH local port forwarding; `/studio` prints the localhost URL and an SSH tunnel hint when SSH is detected.
|
|
77
78
|
- Full Studio is a singleton per Pi session: use `/studio` to open it, `/studio-replace` to explicitly replace it, and `/studio-editor-only` for extra editing/preview tabs that do not take over the full Studio session view.
|
|
78
79
|
- Studio is designed as a complement to terminal pi, not a replacement.
|
|
79
80
|
- Editor/code font uses a best-effort terminal-monospace match when the current terminal config exposes it; set `PI_STUDIO_FONT_MONO` to force a specific CSS `font-family` stack.
|
package/WORKFLOW.md
CHANGED
|
@@ -20,9 +20,11 @@ Studio uses a **single workspace**:
|
|
|
20
20
|
Adds/updates an `annotated-reply` compatible scaffold in the editor:
|
|
21
21
|
|
|
22
22
|
```md
|
|
23
|
-
annotated reply below
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
annotated reply: below
|
|
24
|
+
|
|
25
|
+
- original source: <last model response | file <path> | studio editor>
|
|
26
|
+
- user annotation syntax: [an: your note]
|
|
27
|
+
- precedence: later messages supersede these annotations unless user explicitly references them
|
|
26
28
|
|
|
27
29
|
---
|
|
28
30
|
|
package/client/studio-client.js
CHANGED
|
@@ -202,8 +202,6 @@
|
|
|
202
202
|
let contextTokens = null;
|
|
203
203
|
let contextWindow = null;
|
|
204
204
|
let contextPercent = null;
|
|
205
|
-
let updateInstalledVersion = null;
|
|
206
|
-
let updateLatestVersion = null;
|
|
207
205
|
let windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
208
206
|
let titleAttentionMessage = "";
|
|
209
207
|
let titleAttentionRequestId = null;
|
|
@@ -664,8 +662,6 @@
|
|
|
664
662
|
if (typeof message.contextTokens === "number") summary.contextTokens = message.contextTokens;
|
|
665
663
|
if (typeof message.contextWindow === "number") summary.contextWindow = message.contextWindow;
|
|
666
664
|
if (typeof message.contextPercent === "number") summary.contextPercent = message.contextPercent;
|
|
667
|
-
if (typeof message.updateInstalledVersion === "string") summary.updateInstalledVersion = message.updateInstalledVersion;
|
|
668
|
-
if (typeof message.updateLatestVersion === "string") summary.updateLatestVersion = message.updateLatestVersion;
|
|
669
665
|
if (message.document && typeof message.document === "object" && typeof message.document.text === "string") {
|
|
670
666
|
summary.documentLength = message.document.text.length;
|
|
671
667
|
if (typeof message.document.label === "string") summary.documentLabel = message.document.label;
|
|
@@ -926,30 +922,6 @@
|
|
|
926
922
|
return changed;
|
|
927
923
|
}
|
|
928
924
|
|
|
929
|
-
function applyUpdateInfoFromMessage(message) {
|
|
930
|
-
if (!message || typeof message !== "object") return false;
|
|
931
|
-
|
|
932
|
-
let changed = false;
|
|
933
|
-
|
|
934
|
-
if (Object.prototype.hasOwnProperty.call(message, "updateInstalledVersion")) {
|
|
935
|
-
const nextInstalled = parseNonEmptyString(message.updateInstalledVersion);
|
|
936
|
-
if (nextInstalled !== updateInstalledVersion) {
|
|
937
|
-
updateInstalledVersion = nextInstalled;
|
|
938
|
-
changed = true;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (Object.prototype.hasOwnProperty.call(message, "updateLatestVersion")) {
|
|
943
|
-
const nextLatest = parseNonEmptyString(message.updateLatestVersion);
|
|
944
|
-
if (nextLatest !== updateLatestVersion) {
|
|
945
|
-
updateLatestVersion = nextLatest;
|
|
946
|
-
changed = true;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
return changed;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
925
|
function isTitleAttentionRequestKind(kind) {
|
|
954
926
|
return kind === "annotation" || kind === "critique" || kind === "direct";
|
|
955
927
|
}
|
|
@@ -1134,13 +1106,7 @@
|
|
|
1134
1106
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
1135
1107
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
1136
1108
|
const contextText = formatContextUsageText();
|
|
1137
|
-
|
|
1138
|
-
if (updateLatestVersion) {
|
|
1139
|
-
updateText = updateInstalledVersion
|
|
1140
|
-
? "Update: " + updateInstalledVersion + " → " + updateLatestVersion
|
|
1141
|
-
: "Update: " + updateLatestVersion + " available";
|
|
1142
|
-
}
|
|
1143
|
-
const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText + (updateText ? " · " + updateText : "");
|
|
1109
|
+
const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
|
|
1144
1110
|
if (footerMetaTextEl) {
|
|
1145
1111
|
footerMetaTextEl.textContent = text;
|
|
1146
1112
|
footerMetaTextEl.title = text;
|
|
@@ -8137,7 +8103,7 @@
|
|
|
8137
8103
|
const textarea = document.createElement("textarea");
|
|
8138
8104
|
textarea.value = String(note.text || "");
|
|
8139
8105
|
textarea.placeholder = "Write a local comment here…";
|
|
8140
|
-
textarea.title = "Write a local comment.
|
|
8106
|
+
textarea.title = "Write a local comment. Enter inserts a new line; changes save automatically as you type.";
|
|
8141
8107
|
card.appendChild(textarea);
|
|
8142
8108
|
|
|
8143
8109
|
const footer = document.createElement("div");
|
|
@@ -8203,22 +8169,6 @@
|
|
|
8203
8169
|
updateReviewNotesUi();
|
|
8204
8170
|
});
|
|
8205
8171
|
|
|
8206
|
-
textarea.addEventListener("keydown", (event) => {
|
|
8207
|
-
if (
|
|
8208
|
-
event.key === "Enter"
|
|
8209
|
-
&& !event.shiftKey
|
|
8210
|
-
&& !event.altKey
|
|
8211
|
-
&& !event.ctrlKey
|
|
8212
|
-
&& !event.metaKey
|
|
8213
|
-
) {
|
|
8214
|
-
event.preventDefault();
|
|
8215
|
-
textarea.blur();
|
|
8216
|
-
if (!convertBtn.disabled) {
|
|
8217
|
-
convertBtn.focus();
|
|
8218
|
-
}
|
|
8219
|
-
}
|
|
8220
|
-
});
|
|
8221
|
-
|
|
8222
8172
|
reviewNotesListEl.appendChild(card);
|
|
8223
8173
|
|
|
8224
8174
|
if (pendingReviewNoteInlineFocusId && pendingReviewNoteInlineFocusId === note.id && isReviewNotesOpen()) {
|
|
@@ -9000,8 +8950,7 @@
|
|
|
9000
8950
|
debugTrace("server_message", summarizeServerMessage(message));
|
|
9001
8951
|
|
|
9002
8952
|
const contextChanged = applyContextUsageFromMessage(message);
|
|
9003
|
-
|
|
9004
|
-
if (contextChanged || updateInfoChanged) {
|
|
8953
|
+
if (contextChanged) {
|
|
9005
8954
|
updateFooterMeta();
|
|
9006
8955
|
}
|
|
9007
8956
|
|
|
@@ -9719,10 +9668,10 @@
|
|
|
9719
9668
|
|
|
9720
9669
|
function buildAnnotationHeader() {
|
|
9721
9670
|
const sourceDescriptor = describeSourceForAnnotation();
|
|
9722
|
-
let header = "annotated reply below
|
|
9723
|
-
header += "original source: " + sourceDescriptor + "\n";
|
|
9724
|
-
header += "user annotation syntax: [an: note]\n";
|
|
9725
|
-
header += "precedence: later messages supersede these annotations unless user explicitly references them\n\n---\n\n";
|
|
9671
|
+
let header = "annotated reply: below\n\n";
|
|
9672
|
+
header += "- original source: " + sourceDescriptor + "\n";
|
|
9673
|
+
header += "- user annotation syntax: [an: note]\n";
|
|
9674
|
+
header += "- precedence: later messages supersede these annotations unless user explicitly references them\n\n---\n\n";
|
|
9726
9675
|
return header;
|
|
9727
9676
|
}
|
|
9728
9677
|
|
|
@@ -9732,7 +9681,8 @@
|
|
|
9732
9681
|
|
|
9733
9682
|
function stripAnnotationHeader(text) {
|
|
9734
9683
|
const normalized = String(text || "").replace(/\r\n/g, "\n");
|
|
9735
|
-
|
|
9684
|
+
const lower = normalized.toLowerCase();
|
|
9685
|
+
if (!lower.startsWith("annotated reply: below") && !lower.startsWith("annotated reply below:")) {
|
|
9736
9686
|
return { hadHeader: false, body: normalized };
|
|
9737
9687
|
}
|
|
9738
9688
|
|
package/index.ts
CHANGED
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
transformStudioMarkdownOutsideFences,
|
|
21
21
|
} from "./shared/studio-annotation-scanner.js";
|
|
22
22
|
import { stripStudioMarkdownHtmlComments } from "./shared/studio-markdown-html-comments.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
extractStandaloneLatexDefinitionsFromMarkdown,
|
|
25
|
+
preserveLiteralLatexCommandsInMarkdown,
|
|
26
|
+
} from "./shared/studio-markdown-latex-literals.js";
|
|
24
27
|
import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
|
|
25
28
|
|
|
26
29
|
type Lens = "writing" | "code";
|
|
@@ -272,7 +275,6 @@ const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
|
272
275
|
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
273
276
|
const REQUEST_BODY_MAX_BYTES = 1_000_000;
|
|
274
277
|
const RESPONSE_HISTORY_LIMIT = 30;
|
|
275
|
-
const UPDATE_CHECK_TIMEOUT_MS = 1800;
|
|
276
278
|
const CMUX_NOTIFY_TIMEOUT_MS = 1200;
|
|
277
279
|
const PREPARED_PDF_EXPORT_TTL_MS = 5 * 60 * 1000;
|
|
278
280
|
const MAX_PREPARED_PDF_EXPORTS = 8;
|
|
@@ -478,7 +480,7 @@ function buildStudioPdfCalloutTitleSizeCommand(options?: StudioPdfRenderOptions)
|
|
|
478
480
|
return "\\footnotesize";
|
|
479
481
|
}
|
|
480
482
|
|
|
481
|
-
function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
|
|
483
|
+
function buildStudioPdfPreamble(options?: StudioPdfRenderOptions, extraPreamble = ""): string {
|
|
482
484
|
const sectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.sectionSize, "\\Large");
|
|
483
485
|
const subsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsectionSize, "\\large");
|
|
484
486
|
const subsubsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsubsectionSize, "\\normalsize");
|
|
@@ -544,7 +546,7 @@ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
|
|
|
544
546
|
\\RecustomVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere,bgcolor=StudioCodeBlockBg,framesep=2mm}%
|
|
545
547
|
}
|
|
546
548
|
\\makeatother
|
|
547
|
-
`;
|
|
549
|
+
${extraPreamble ? `${extraPreamble.trim()}\n` : ""}`;
|
|
548
550
|
}
|
|
549
551
|
|
|
550
552
|
type StudioThemeMode = "dark" | "light";
|
|
@@ -2737,93 +2739,6 @@ function readStudioGitDiff(baseDir: string):
|
|
|
2737
2739
|
return { ok: true, text: fullDiff, label };
|
|
2738
2740
|
}
|
|
2739
2741
|
|
|
2740
|
-
function readLocalPackageMetadata(): { name: string; version: string } | null {
|
|
2741
|
-
try {
|
|
2742
|
-
const raw = readFileSync(new URL("./package.json", import.meta.url), "utf-8");
|
|
2743
|
-
const parsed = JSON.parse(raw) as { name?: unknown; version?: unknown };
|
|
2744
|
-
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
2745
|
-
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
2746
|
-
if (!name || !version) return null;
|
|
2747
|
-
return { name, version };
|
|
2748
|
-
} catch {
|
|
2749
|
-
return null;
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
interface ParsedSemver {
|
|
2754
|
-
major: number;
|
|
2755
|
-
minor: number;
|
|
2756
|
-
patch: number;
|
|
2757
|
-
prerelease: string | null;
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
function parseSemverLoose(version: string): ParsedSemver | null {
|
|
2761
|
-
const normalized = String(version || "").trim().replace(/^v/i, "");
|
|
2762
|
-
const match = normalized.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/);
|
|
2763
|
-
if (!match) return null;
|
|
2764
|
-
const major = Number.parseInt(match[1] ?? "", 10);
|
|
2765
|
-
const minor = Number.parseInt(match[2] ?? "0", 10);
|
|
2766
|
-
const patch = Number.parseInt(match[3] ?? "0", 10);
|
|
2767
|
-
if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) return null;
|
|
2768
|
-
const prerelease = typeof match[4] === "string" && match[4].trim() ? match[4].trim() : null;
|
|
2769
|
-
return { major, minor, patch, prerelease };
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
function compareSemverLoose(a: string, b: string): number {
|
|
2773
|
-
const pa = parseSemverLoose(a);
|
|
2774
|
-
const pb = parseSemverLoose(b);
|
|
2775
|
-
if (!pa || !pb) {
|
|
2776
|
-
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
|
|
2777
|
-
}
|
|
2778
|
-
if (pa.major !== pb.major) return pa.major - pb.major;
|
|
2779
|
-
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
|
|
2780
|
-
if (pa.patch !== pb.patch) return pa.patch - pb.patch;
|
|
2781
|
-
if (pa.prerelease && !pb.prerelease) return -1;
|
|
2782
|
-
if (!pa.prerelease && pb.prerelease) return 1;
|
|
2783
|
-
if (!pa.prerelease && !pb.prerelease) return 0;
|
|
2784
|
-
return (pa.prerelease ?? "").localeCompare(pb.prerelease ?? "", undefined, {
|
|
2785
|
-
numeric: true,
|
|
2786
|
-
sensitivity: "base",
|
|
2787
|
-
});
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
function isVersionBehind(installedVersion: string, latestVersion: string): boolean {
|
|
2791
|
-
return compareSemverLoose(installedVersion, latestVersion) < 0;
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
async function fetchLatestNpmVersion(packageName: string, timeoutMs = UPDATE_CHECK_TIMEOUT_MS): Promise<string | null> {
|
|
2795
|
-
const pkg = String(packageName || "").trim();
|
|
2796
|
-
if (!pkg) return null;
|
|
2797
|
-
const encodedPackage = encodeURIComponent(pkg).replace(/^%40/, "@");
|
|
2798
|
-
const endpoint = `https://registry.npmjs.org/${encodedPackage}/latest`;
|
|
2799
|
-
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
2800
|
-
const timer = controller
|
|
2801
|
-
? setTimeout(() => {
|
|
2802
|
-
try {
|
|
2803
|
-
controller.abort();
|
|
2804
|
-
} catch {
|
|
2805
|
-
// ignore abort race
|
|
2806
|
-
}
|
|
2807
|
-
}, timeoutMs)
|
|
2808
|
-
: null;
|
|
2809
|
-
|
|
2810
|
-
try {
|
|
2811
|
-
const response = await fetch(endpoint, {
|
|
2812
|
-
method: "GET",
|
|
2813
|
-
headers: { Accept: "application/json" },
|
|
2814
|
-
signal: controller?.signal,
|
|
2815
|
-
});
|
|
2816
|
-
if (!response.ok) return null;
|
|
2817
|
-
const payload = await response.json() as { version?: unknown };
|
|
2818
|
-
const version = typeof payload.version === "string" ? payload.version.trim() : "";
|
|
2819
|
-
return version || null;
|
|
2820
|
-
} catch {
|
|
2821
|
-
return null;
|
|
2822
|
-
} finally {
|
|
2823
|
-
if (timer) clearTimeout(timer);
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
|
|
2827
2742
|
function isLikelyMathExpression(expr: string): boolean {
|
|
2828
2743
|
const content = expr.trim();
|
|
2829
2744
|
if (content.length === 0) return false;
|
|
@@ -4566,6 +4481,7 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
4566
4481
|
calloutBlocks: StudioPdfMarkdownCalloutBlock[] = [],
|
|
4567
4482
|
alignedImageBlocks: StudioPdfAlignedImageBlock[] = [],
|
|
4568
4483
|
pdfOptions?: StudioPdfRenderOptions,
|
|
4484
|
+
extraPreamble = "",
|
|
4569
4485
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
4570
4486
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
4571
4487
|
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
@@ -4573,7 +4489,7 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
4573
4489
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
4574
4490
|
|
|
4575
4491
|
await mkdir(tempDir, { recursive: true });
|
|
4576
|
-
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
4492
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPreamble), "utf-8");
|
|
4577
4493
|
|
|
4578
4494
|
const pandocArgs = [
|
|
4579
4495
|
"-f", inputFormat,
|
|
@@ -4855,17 +4771,22 @@ async function renderStudioPdfWithPandoc(
|
|
|
4855
4771
|
? "latex"
|
|
4856
4772
|
: "markdown+lists_without_preceding_blankline-blank_before_blockquote-blank_before_header+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris+superscript+subscript-raw_html";
|
|
4857
4773
|
const normalizedMarkdown = prepareStudioPdfMarkdown(pdfAlignedImageTransform.markdown, isLatex, effectiveEditorLanguage);
|
|
4774
|
+
const markdownPreambleSplit = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
|
|
4775
|
+
? extractStandaloneLatexDefinitionsFromMarkdown(normalizedMarkdown)
|
|
4776
|
+
: { body: normalizedMarkdown, definitions: [], preamble: "" };
|
|
4777
|
+
const normalizedMarkdownBody = markdownPreambleSplit.body;
|
|
4778
|
+
const extraPdfPreamble = markdownPreambleSplit.preamble;
|
|
4858
4779
|
|
|
4859
4780
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
4860
4781
|
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
4861
4782
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
4862
4783
|
|
|
4863
4784
|
await mkdir(tempDir, { recursive: true });
|
|
4864
|
-
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
4785
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPdfPreamble), "utf-8");
|
|
4865
4786
|
|
|
4866
4787
|
const mermaidPrepared: StudioMermaidPdfPreprocessResult = isLatex
|
|
4867
|
-
? { markdown:
|
|
4868
|
-
: await preprocessStudioMermaidForPdf(
|
|
4788
|
+
? { markdown: normalizedMarkdownBody, found: 0, replaced: 0, failed: 0, missingCli: false }
|
|
4789
|
+
: await preprocessStudioMermaidForPdf(normalizedMarkdownBody, tempDir);
|
|
4869
4790
|
const markdownForPdf = mermaidPrepared.markdown;
|
|
4870
4791
|
const hasDiffBlocks = !isLatex && hasStudioMarkdownDiffFence(markdownForPdf);
|
|
4871
4792
|
|
|
@@ -4883,6 +4804,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
4883
4804
|
pdfCalloutTransform.blocks,
|
|
4884
4805
|
pdfAlignedImageTransform.blocks,
|
|
4885
4806
|
pdfOptions,
|
|
4807
|
+
extraPdfPreamble,
|
|
4886
4808
|
);
|
|
4887
4809
|
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
4888
4810
|
return { pdf: rendered.pdf, warning: mermaidPrepared.warning ?? rendered.warning };
|
|
@@ -5842,6 +5764,17 @@ function buildStudioUrl(
|
|
|
5842
5764
|
return `http://127.0.0.1:${port}/?${params.toString()}`;
|
|
5843
5765
|
}
|
|
5844
5766
|
|
|
5767
|
+
function isSshSession(): boolean {
|
|
5768
|
+
return Boolean(
|
|
5769
|
+
String(process.env.SSH_CONNECTION ?? process.env.SSH_CLIENT ?? process.env.SSH_TTY ?? "").trim(),
|
|
5770
|
+
);
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
function buildStudioSshTunnelHint(port: number): string | null {
|
|
5774
|
+
if (!isSshSession()) return null;
|
|
5775
|
+
return `SSH detected. If Studio is running on a remote machine, forward the port from your local machine, then open the Studio URL locally: ssh -L ${port}:127.0.0.1:${port} <remote-host>`;
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5845
5778
|
function resolveRequestedStudioDocumentFromUrl(
|
|
5846
5779
|
requestUrl: URL,
|
|
5847
5780
|
fallback: InitialStudioDocument | null,
|
|
@@ -6382,11 +6315,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
6382
6315
|
};
|
|
6383
6316
|
let compactInProgress = false;
|
|
6384
6317
|
let compactRequestId: string | null = null;
|
|
6385
|
-
let updateCheckStarted = false;
|
|
6386
|
-
let updateCheckCompleted = false;
|
|
6387
|
-
const packageMetadata = readLocalPackageMetadata();
|
|
6388
|
-
const installedPackageVersion = packageMetadata?.version ?? null;
|
|
6389
|
-
let updateAvailableLatestVersion: string | null = null;
|
|
6390
6318
|
|
|
6391
6319
|
const isStudioDirectRunChainActive = () => Boolean(studioDirectRunChain);
|
|
6392
6320
|
const getQueuedStudioSteeringCount = () => queuedStudioDirectRequests.length;
|
|
@@ -6766,28 +6694,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
6766
6694
|
});
|
|
6767
6695
|
};
|
|
6768
6696
|
|
|
6769
|
-
const maybeNotifyUpdateAvailable = async (ctx: ExtensionCommandContext) => {
|
|
6770
|
-
if (updateCheckStarted || updateCheckCompleted) return;
|
|
6771
|
-
updateCheckStarted = true;
|
|
6772
|
-
try {
|
|
6773
|
-
const metadata = packageMetadata;
|
|
6774
|
-
if (!metadata) return;
|
|
6775
|
-
const latest = await fetchLatestNpmVersion(metadata.name, UPDATE_CHECK_TIMEOUT_MS);
|
|
6776
|
-
if (!latest) return;
|
|
6777
|
-
if (!isVersionBehind(metadata.version, latest)) return;
|
|
6778
|
-
|
|
6779
|
-
updateAvailableLatestVersion = latest;
|
|
6780
|
-
broadcastState();
|
|
6781
|
-
|
|
6782
|
-
const notification =
|
|
6783
|
-
`Update available for ${metadata.name}: ${metadata.version} → ${latest}. Run: pi install npm:${metadata.name}`;
|
|
6784
|
-
ctx.ui.notify(notification, "info");
|
|
6785
|
-
broadcast({ type: "info", message: notification, level: "info" });
|
|
6786
|
-
} finally {
|
|
6787
|
-
updateCheckCompleted = true;
|
|
6788
|
-
}
|
|
6789
|
-
};
|
|
6790
|
-
|
|
6791
6697
|
const sendToClient = (client: WebSocket, payload: unknown) => {
|
|
6792
6698
|
if (client.readyState !== WebSocket.OPEN) return;
|
|
6793
6699
|
try {
|
|
@@ -7067,8 +6973,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
7067
6973
|
contextTokens: contextUsageSnapshot.tokens,
|
|
7068
6974
|
contextWindow: contextUsageSnapshot.contextWindow,
|
|
7069
6975
|
contextPercent: contextUsageSnapshot.percent,
|
|
7070
|
-
updateInstalledVersion: installedPackageVersion,
|
|
7071
|
-
updateLatestVersion: updateAvailableLatestVersion,
|
|
7072
6976
|
compactInProgress,
|
|
7073
6977
|
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
7074
6978
|
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
@@ -7349,8 +7253,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
7349
7253
|
contextTokens: contextUsageSnapshot.tokens,
|
|
7350
7254
|
contextWindow: contextUsageSnapshot.contextWindow,
|
|
7351
7255
|
contextPercent: contextUsageSnapshot.percent,
|
|
7352
|
-
updateInstalledVersion: installedPackageVersion,
|
|
7353
|
-
updateLatestVersion: updateAvailableLatestVersion,
|
|
7354
7256
|
compactInProgress,
|
|
7355
7257
|
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
7356
7258
|
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
@@ -9018,6 +8920,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
9018
8920
|
ctx.ui.notify("A full pi Studio view is already open for this session. Close it first, use /studio-replace for a fresh full Studio view, or use /studio-editor-only for a concurrent editor-only Studio view.", "warning");
|
|
9019
8921
|
if (serverState) {
|
|
9020
8922
|
ctx.ui.notify(`Studio URL: ${buildStudioUrl(serverState.port, serverState.token, "full")}`, "info");
|
|
8923
|
+
const sshTunnelHint = buildStudioSshTunnelHint(serverState.port);
|
|
8924
|
+
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
9021
8925
|
}
|
|
9022
8926
|
return;
|
|
9023
8927
|
}
|
|
@@ -9043,6 +8947,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9043
8947
|
|
|
9044
8948
|
const state = await ensureServer();
|
|
9045
8949
|
const url = buildStudioUrl(state.port, state.token, mode, selected);
|
|
8950
|
+
const sshTunnelHint = buildStudioSshTunnelHint(state.port);
|
|
9046
8951
|
const openedLabel = mode === "editor-only" ? "pi Studio editor-only view" : "pi Studio";
|
|
9047
8952
|
|
|
9048
8953
|
try {
|
|
@@ -9054,11 +8959,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
9054
8959
|
} else {
|
|
9055
8960
|
ctx.ui.notify(`Opened ${openedLabel} with blank editor.`, "info");
|
|
9056
8961
|
}
|
|
9057
|
-
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
9058
8962
|
} catch (error) {
|
|
9059
|
-
|
|
8963
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8964
|
+
if (isSshSession()) {
|
|
8965
|
+
ctx.ui.notify(`Failed to open browser automatically over SSH: ${message}`, "warning");
|
|
8966
|
+
} else {
|
|
8967
|
+
ctx.ui.notify(`Failed to open browser: ${message}`, "error");
|
|
8968
|
+
}
|
|
9060
8969
|
} finally {
|
|
9061
|
-
|
|
8970
|
+
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
8971
|
+
if (sshTunnelHint) ctx.ui.notify(sshTunnelHint, "info");
|
|
9062
8972
|
}
|
|
9063
8973
|
};
|
|
9064
8974
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.57",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,6 +51,18 @@ const STUDIO_LITERAL_MARKDOWN_LATEX_COMMANDS = new Set([
|
|
|
51
51
|
"tex",
|
|
52
52
|
]);
|
|
53
53
|
|
|
54
|
+
const STUDIO_STANDALONE_MARKDOWN_LATEX_DEFINITION_COMMANDS = new Set([
|
|
55
|
+
"newcommand",
|
|
56
|
+
"renewcommand",
|
|
57
|
+
"providecommand",
|
|
58
|
+
"declaremathoperator",
|
|
59
|
+
"def",
|
|
60
|
+
"gdef",
|
|
61
|
+
"edef",
|
|
62
|
+
"xdef",
|
|
63
|
+
"let",
|
|
64
|
+
]);
|
|
65
|
+
|
|
54
66
|
function isEscapedAt(text, index) {
|
|
55
67
|
let slashCount = 0;
|
|
56
68
|
for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
|
|
@@ -70,12 +82,40 @@ function findClosingUnescapedDelimiter(text, startIndex, delimiter) {
|
|
|
70
82
|
return -1;
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
function isStandaloneLatexDefinitionLine(line) {
|
|
86
|
+
const commandMatch = String(line || "").match(/^[ \t]{0,3}\\([A-Za-z@]+)\*?(?=\s|\\|\{|\[|$)/);
|
|
87
|
+
if (!commandMatch) return false;
|
|
88
|
+
|
|
89
|
+
const commandName = String(commandMatch[1] || "").toLowerCase();
|
|
90
|
+
return STUDIO_STANDALONE_MARKDOWN_LATEX_DEFINITION_COMMANDS.has(commandName);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readStandaloneLatexDefinitionLine(text, startIndex) {
|
|
94
|
+
if (startIndex > 0 && text[startIndex - 1] !== "\n") return null;
|
|
95
|
+
|
|
96
|
+
const lineEndIndex = text.indexOf("\n", startIndex);
|
|
97
|
+
const sliceEnd = lineEndIndex >= 0 ? lineEndIndex : text.length;
|
|
98
|
+
const line = text.slice(startIndex, sliceEnd);
|
|
99
|
+
if (!isStandaloneLatexDefinitionLine(line)) return null;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
text: lineEndIndex >= 0 ? text.slice(startIndex, lineEndIndex + 1) : line,
|
|
103
|
+
nextIndex: lineEndIndex >= 0 ? lineEndIndex + 1 : text.length,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
function preserveLiteralLatexCommandsInMarkdownSegment(markdown) {
|
|
74
108
|
const source = String(markdown || "");
|
|
75
109
|
let out = "";
|
|
76
110
|
let index = 0;
|
|
77
111
|
|
|
78
112
|
while (index < source.length) {
|
|
113
|
+
const standaloneDefinitionLine = readStandaloneLatexDefinitionLine(source, index);
|
|
114
|
+
if (standaloneDefinitionLine) {
|
|
115
|
+
out += standaloneDefinitionLine.text;
|
|
116
|
+
index = standaloneDefinitionLine.nextIndex;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
79
119
|
if (source[index] === "`") {
|
|
80
120
|
let tickCount = 1;
|
|
81
121
|
while (source[index + tickCount] === "`") tickCount += 1;
|
|
@@ -201,3 +241,49 @@ export function preserveLiteralLatexCommandsInMarkdown(markdown) {
|
|
|
201
241
|
flushPlain();
|
|
202
242
|
return out.join("\n");
|
|
203
243
|
}
|
|
244
|
+
|
|
245
|
+
export function extractStandaloneLatexDefinitionsFromMarkdown(markdown) {
|
|
246
|
+
const lines = String(markdown || "").split("\n");
|
|
247
|
+
const bodyLines = [];
|
|
248
|
+
const definitions = [];
|
|
249
|
+
let inFence = false;
|
|
250
|
+
let fenceChar;
|
|
251
|
+
let fenceLength = 0;
|
|
252
|
+
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
const trimmed = line.trimStart();
|
|
255
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
256
|
+
|
|
257
|
+
if (fenceMatch) {
|
|
258
|
+
const marker = fenceMatch[1];
|
|
259
|
+
const markerChar = marker[0];
|
|
260
|
+
const markerLength = marker.length;
|
|
261
|
+
|
|
262
|
+
if (!inFence) {
|
|
263
|
+
inFence = true;
|
|
264
|
+
fenceChar = markerChar;
|
|
265
|
+
fenceLength = markerLength;
|
|
266
|
+
} else if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
267
|
+
inFence = false;
|
|
268
|
+
fenceChar = undefined;
|
|
269
|
+
fenceLength = 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
bodyLines.push(line);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!inFence && isStandaloneLatexDefinitionLine(line)) {
|
|
277
|
+
definitions.push(line);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
bodyLines.push(line);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
body: bodyLines.join("\n"),
|
|
286
|
+
definitions,
|
|
287
|
+
preamble: definitions.join("\n"),
|
|
288
|
+
};
|
|
289
|
+
}
|