pi-studio 0.5.9 → 0.5.11
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 +10 -0
- package/index.ts +136 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.11] — 2026-03-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Studio tabs now show a title attention marker like `● Response ready` or `● Critique ready` when a Studio-started model request finishes while the tab is unfocused, and clear that marker when the tab regains focus or the next Studio request starts.
|
|
11
|
+
|
|
12
|
+
## [0.5.10] — 2026-03-14
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Studio preview/PDF math normalization is now more robust for model-emitted `\(...\)` / `\[...\]` math, including malformed mixed delimiters like `$\(...\)$`, optional spacing around those mixed delimiters, and multiline display-math line-break formatting that previously leaked raw/broken `$$` output into preview.
|
|
16
|
+
|
|
7
17
|
## [0.5.9] — 2026-03-13
|
|
8
18
|
|
|
9
19
|
### Fixed
|
package/index.ts
CHANGED
|
@@ -991,13 +991,58 @@ async function fetchLatestNpmVersion(packageName: string, timeoutMs = UPDATE_CHE
|
|
|
991
991
|
}
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
function isLikelyMathExpression(expr: string): boolean {
|
|
995
|
+
const content = expr.trim();
|
|
996
|
+
if (content.length === 0) return false;
|
|
997
|
+
|
|
998
|
+
if (/\\[a-zA-Z]+/.test(content)) return true; // LaTeX commands like \frac, \alpha
|
|
999
|
+
if (/[0-9]/.test(content)) return true;
|
|
1000
|
+
if (/[=+\-*/^_<>≤≥±×÷]/u.test(content)) return true;
|
|
1001
|
+
if (/[{}]/.test(content)) return true;
|
|
1002
|
+
if (/[α-ωΑ-Ω]/u.test(content)) return true;
|
|
1003
|
+
if (/^[A-Za-z]$/.test(content)) return true; // single-variable forms like \(x\)
|
|
1004
|
+
|
|
1005
|
+
// Plain words (e.g. escaped markdown like \[not a link\]) are not math.
|
|
1006
|
+
if (/^[A-Za-z][A-Za-z\s'".,:;!?-]*[A-Za-z]$/.test(content)) return false;
|
|
1007
|
+
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function collapseDisplayMathContent(expr: string): string {
|
|
1012
|
+
let content = expr.trim();
|
|
1013
|
+
if (content.includes("\\\\") || content.includes("\n")) {
|
|
1014
|
+
content = content.replace(/\\\\\s*/g, " ");
|
|
1015
|
+
content = content.replace(/\s*\n\s*/g, " ");
|
|
1016
|
+
content = content.replace(/\s{2,}/g, " ").trim();
|
|
1017
|
+
}
|
|
1018
|
+
return content;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
994
1021
|
function normalizeMathDelimitersInSegment(markdown: string): string {
|
|
995
|
-
let normalized = markdown.replace(
|
|
1022
|
+
let normalized = markdown.replace(/\$\s*\\\(([\s\S]*?)\\\)\s*\$/g, (match, expr: string) => {
|
|
1023
|
+
if (!isLikelyMathExpression(expr)) return match;
|
|
1024
|
+
const content = expr.trim();
|
|
1025
|
+
return content.length > 0 ? `\\(${content}\\)` : "\\(\\)";
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
normalized = normalized.replace(/\$\s*\\\[\s*([\s\S]*?)\s*\\\]\s*\$/g, (match, expr: string) => {
|
|
1029
|
+
if (!isLikelyMathExpression(expr)) return match;
|
|
1030
|
+
const content = collapseDisplayMathContent(expr);
|
|
1031
|
+
return content.length > 0 ? `\\[${content}\\]` : "\\[\\]";
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
normalized = normalized.replace(/\\\[\s*([\s\S]*?)\s*\\\]/g, (match, expr: string) => {
|
|
1035
|
+
if (!isLikelyMathExpression(expr)) return `[${expr.trim()}]`;
|
|
1036
|
+
const content = collapseDisplayMathContent(expr);
|
|
1037
|
+
return content.length > 0 ? `\\[${content}\\]` : "\\[\\]";
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
normalized = normalized.replace(/\\\(([\s\S]*?)\\\)/g, (match, expr: string) => {
|
|
1041
|
+
if (!isLikelyMathExpression(expr)) return `(${expr})`;
|
|
996
1042
|
const content = expr.trim();
|
|
997
|
-
return content.length > 0 ?
|
|
1043
|
+
return content.length > 0 ? `\\(${content}\\)` : "\\(\\)";
|
|
998
1044
|
});
|
|
999
1045
|
|
|
1000
|
-
normalized = normalized.replace(/\\\(([\s\S]*?)\\\)/g, (_match, expr: string) => `$${expr}$`);
|
|
1001
1046
|
return normalized;
|
|
1002
1047
|
}
|
|
1003
1048
|
|
|
@@ -1216,7 +1261,7 @@ async function preprocessStudioMermaidForPdf(markdown: string, workDir: string):
|
|
|
1216
1261
|
|
|
1217
1262
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string): Promise<string> {
|
|
1218
1263
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
1219
|
-
const inputFormat = isLatex ? "latex" : "markdown+tex_math_dollars+autolink_bare_uris-raw_html";
|
|
1264
|
+
const inputFormat = isLatex ? "latex" : "markdown+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris-raw_html";
|
|
1220
1265
|
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none"];
|
|
1221
1266
|
if (resourcePath) {
|
|
1222
1267
|
args.push(`--resource-path=${resourcePath}`);
|
|
@@ -1288,7 +1333,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
1288
1333
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
1289
1334
|
const inputFormat = isLatex
|
|
1290
1335
|
? "latex"
|
|
1291
|
-
: "markdown+tex_math_dollars+autolink_bare_uris+superscript+subscript-raw_html";
|
|
1336
|
+
: "markdown+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris+superscript+subscript-raw_html";
|
|
1292
1337
|
const normalizedMarkdown = isLatex ? markdown : normalizeObsidianImages(normalizeMathDelimiters(markdown));
|
|
1293
1338
|
|
|
1294
1339
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
@@ -3494,6 +3539,10 @@ ${cssVarsBlock}
|
|
|
3494
3539
|
let contextPercent = null;
|
|
3495
3540
|
let updateInstalledVersion = null;
|
|
3496
3541
|
let updateLatestVersion = null;
|
|
3542
|
+
let windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
3543
|
+
let titleAttentionMessage = "";
|
|
3544
|
+
let titleAttentionRequestId = null;
|
|
3545
|
+
let titleAttentionRequestKind = null;
|
|
3497
3546
|
|
|
3498
3547
|
function parseFiniteNumber(value) {
|
|
3499
3548
|
if (value == null || value === "") return null;
|
|
@@ -3854,12 +3903,63 @@ ${cssVarsBlock}
|
|
|
3854
3903
|
return changed;
|
|
3855
3904
|
}
|
|
3856
3905
|
|
|
3906
|
+
function isTitleAttentionRequestKind(kind) {
|
|
3907
|
+
return kind === "annotation" || kind === "critique" || kind === "direct";
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
function armTitleAttentionForRequest(requestId, kind) {
|
|
3911
|
+
if (typeof requestId !== "string" || !isTitleAttentionRequestKind(kind)) {
|
|
3912
|
+
titleAttentionRequestId = null;
|
|
3913
|
+
titleAttentionRequestKind = null;
|
|
3914
|
+
return;
|
|
3915
|
+
}
|
|
3916
|
+
titleAttentionRequestId = requestId;
|
|
3917
|
+
titleAttentionRequestKind = kind;
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
function clearArmedTitleAttention(requestId) {
|
|
3921
|
+
if (typeof requestId === "string" && titleAttentionRequestId && requestId !== titleAttentionRequestId) {
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
titleAttentionRequestId = null;
|
|
3925
|
+
titleAttentionRequestKind = null;
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
function clearTitleAttention() {
|
|
3929
|
+
if (!titleAttentionMessage) return;
|
|
3930
|
+
titleAttentionMessage = "";
|
|
3931
|
+
updateDocumentTitle();
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
function shouldShowTitleAttention() {
|
|
3935
|
+
const focused = typeof document.hasFocus === "function" ? document.hasFocus() : windowHasFocus;
|
|
3936
|
+
return Boolean(document.hidden) || !focused;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
function getTitleAttentionMessage(kind) {
|
|
3940
|
+
if (kind === "critique") return "● Critique ready";
|
|
3941
|
+
if (kind === "direct") return "● Response ready";
|
|
3942
|
+
return "● Reply ready";
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
function maybeShowTitleAttentionForCompletedRequest(requestId, kind) {
|
|
3946
|
+
const matchedRequest = typeof requestId === "string" && titleAttentionRequestId && requestId === titleAttentionRequestId;
|
|
3947
|
+
const completedKind = isTitleAttentionRequestKind(kind) ? kind : titleAttentionRequestKind;
|
|
3948
|
+
clearArmedTitleAttention(requestId);
|
|
3949
|
+
if (!matchedRequest || !completedKind || !shouldShowTitleAttention()) {
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
titleAttentionMessage = getTitleAttentionMessage(completedKind);
|
|
3953
|
+
updateDocumentTitle();
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3857
3956
|
function updateDocumentTitle() {
|
|
3858
3957
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
3859
3958
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
3860
3959
|
const titleParts = ["pi Studio"];
|
|
3861
3960
|
if (terminalText && terminalText !== "unknown") titleParts.push(terminalText);
|
|
3862
3961
|
if (modelText && modelText !== "none") titleParts.push(modelText);
|
|
3962
|
+
if (titleAttentionMessage) titleParts.unshift(titleAttentionMessage);
|
|
3863
3963
|
document.title = titleParts.join(" · ");
|
|
3864
3964
|
}
|
|
3865
3965
|
|
|
@@ -3953,6 +4053,24 @@ ${cssVarsBlock}
|
|
|
3953
4053
|
|
|
3954
4054
|
renderStatus();
|
|
3955
4055
|
|
|
4056
|
+
window.addEventListener("focus", () => {
|
|
4057
|
+
windowHasFocus = true;
|
|
4058
|
+
clearTitleAttention();
|
|
4059
|
+
});
|
|
4060
|
+
|
|
4061
|
+
window.addEventListener("blur", () => {
|
|
4062
|
+
windowHasFocus = false;
|
|
4063
|
+
});
|
|
4064
|
+
|
|
4065
|
+
document.addEventListener("visibilitychange", () => {
|
|
4066
|
+
if (!document.hidden) {
|
|
4067
|
+
windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : windowHasFocus;
|
|
4068
|
+
if (windowHasFocus) {
|
|
4069
|
+
clearTitleAttention();
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
4073
|
+
|
|
3956
4074
|
function updateSourceBadge() {
|
|
3957
4075
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
3958
4076
|
sourceBadgeEl.textContent = "Editor origin: " + label;
|
|
@@ -5765,8 +5883,10 @@ ${cssVarsBlock}
|
|
|
5765
5883
|
setStatus("No matching Studio request is running.", "warning");
|
|
5766
5884
|
return false;
|
|
5767
5885
|
}
|
|
5768
|
-
const
|
|
5886
|
+
const requestId = pendingRequestId;
|
|
5887
|
+
const sent = sendMessage({ type: "cancel_request", requestId });
|
|
5769
5888
|
if (!sent) return false;
|
|
5889
|
+
clearArmedTitleAttention(requestId);
|
|
5770
5890
|
setStatus("Stopping request…", "warning");
|
|
5771
5891
|
return true;
|
|
5772
5892
|
}
|
|
@@ -6074,6 +6194,7 @@ ${cssVarsBlock}
|
|
|
6074
6194
|
return;
|
|
6075
6195
|
}
|
|
6076
6196
|
|
|
6197
|
+
const completedRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
6077
6198
|
const responseKind =
|
|
6078
6199
|
typeof message.kind === "string"
|
|
6079
6200
|
? message.kind
|
|
@@ -6106,6 +6227,7 @@ ${cssVarsBlock}
|
|
|
6106
6227
|
} else {
|
|
6107
6228
|
setStatus("Response ready.", "success");
|
|
6108
6229
|
}
|
|
6230
|
+
maybeShowTitleAttentionForCompletedRequest(completedRequestId, responseKind);
|
|
6109
6231
|
return;
|
|
6110
6232
|
}
|
|
6111
6233
|
|
|
@@ -6307,6 +6429,9 @@ ${cssVarsBlock}
|
|
|
6307
6429
|
pendingRequestId = null;
|
|
6308
6430
|
pendingKind = null;
|
|
6309
6431
|
}
|
|
6432
|
+
if (typeof message.requestId === "string") {
|
|
6433
|
+
clearArmedTitleAttention(message.requestId);
|
|
6434
|
+
}
|
|
6310
6435
|
stickyStudioKind = null;
|
|
6311
6436
|
setBusy(false);
|
|
6312
6437
|
setWsState("Ready");
|
|
@@ -6322,6 +6447,9 @@ ${cssVarsBlock}
|
|
|
6322
6447
|
pendingRequestId = null;
|
|
6323
6448
|
pendingKind = null;
|
|
6324
6449
|
}
|
|
6450
|
+
if (typeof message.requestId === "string") {
|
|
6451
|
+
clearArmedTitleAttention(message.requestId);
|
|
6452
|
+
}
|
|
6325
6453
|
stickyStudioKind = null;
|
|
6326
6454
|
setBusy(false);
|
|
6327
6455
|
setWsState("Ready");
|
|
@@ -6480,10 +6608,12 @@ ${cssVarsBlock}
|
|
|
6480
6608
|
setStatus("Studio is busy.", "warning");
|
|
6481
6609
|
return null;
|
|
6482
6610
|
}
|
|
6611
|
+
clearTitleAttention();
|
|
6483
6612
|
const requestId = makeRequestId();
|
|
6484
6613
|
pendingRequestId = requestId;
|
|
6485
6614
|
pendingKind = kind;
|
|
6486
6615
|
stickyStudioKind = kind;
|
|
6616
|
+
armTitleAttentionForRequest(requestId, kind);
|
|
6487
6617
|
setBusy(true);
|
|
6488
6618
|
setWsState("Submitting");
|
|
6489
6619
|
setStatus(getStudioBusyStatus(kind), "warning");
|