pi-studio 0.5.4 → 0.5.6
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 +15 -0
- package/README.md +1 -0
- package/index.ts +381 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -88,6 +88,21 @@ All notable changes to `pi-studio` are documented here.
|
|
|
88
88
|
|
|
89
89
|
## [Unreleased]
|
|
90
90
|
|
|
91
|
+
## [0.5.6] — 2026-03-10
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
- Studio monospace surfaces now use a shared `--font-mono` stack, with best-effort terminal-font detection (Ghostty/WezTerm/Kitty/Alacritty config when available) and `PI_STUDIO_FONT_MONO` as a manual override.
|
|
95
|
+
- In-flight **Run editor text** / **Critique editor text** requests now swap the triggering button into an in-place theme-aware **Stop** state while disabling the other action.
|
|
96
|
+
|
|
97
|
+
## [0.5.5] — 2026-03-09
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
- Improved raw-editor caret/overlay alignment in Syntax highlight mode:
|
|
101
|
+
- width-neutral annotation highlight styling
|
|
102
|
+
- more textarea-like wrap behavior in the highlight overlay
|
|
103
|
+
- preserved empty trailing lines in highlighted output so end-of-file blank lines stay aligned
|
|
104
|
+
- reduced raw overlay metric drift for comment/quote styling
|
|
105
|
+
|
|
91
106
|
## [0.5.4] — 2026-03-09
|
|
92
107
|
|
|
93
108
|
### Added
|
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
62
62
|
|
|
63
63
|
- Local-only server (`127.0.0.1`) with rotating tokenized URLs.
|
|
64
64
|
- Studio is designed as a complement to terminal pi, not a replacement.
|
|
65
|
+
- 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.
|
|
65
66
|
- Full preview/PDF quality depends on `pandoc` (and `xelatex` for PDF):
|
|
66
67
|
- `brew install pandoc`
|
|
67
68
|
- install TeX Live/MacTeX for PDF export
|
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
7
|
+
import { homedir, tmpdir } from "node:os";
|
|
8
8
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
9
9
|
import { URL } from "node:url";
|
|
10
10
|
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
|
@@ -120,6 +120,11 @@ interface GetFromEditorRequestMessage {
|
|
|
120
120
|
requestId: string;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
interface CancelRequestMessage {
|
|
124
|
+
type: "cancel_request";
|
|
125
|
+
requestId: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
123
128
|
type IncomingStudioMessage =
|
|
124
129
|
| HelloMessage
|
|
125
130
|
| PingMessage
|
|
@@ -131,7 +136,8 @@ type IncomingStudioMessage =
|
|
|
131
136
|
| SaveAsRequestMessage
|
|
132
137
|
| SaveOverRequestMessage
|
|
133
138
|
| SendToEditorRequestMessage
|
|
134
|
-
| GetFromEditorRequestMessage
|
|
139
|
+
| GetFromEditorRequestMessage
|
|
140
|
+
| CancelRequestMessage;
|
|
135
141
|
|
|
136
142
|
const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
137
143
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
@@ -453,6 +459,205 @@ interface ThemeExportPalette {
|
|
|
453
459
|
|
|
454
460
|
const themeExportPaletteCache = new Map<string, ThemeExportPalette | null>();
|
|
455
461
|
|
|
462
|
+
const DEFAULT_MONO_FONT_FAMILIES = [
|
|
463
|
+
"ui-monospace",
|
|
464
|
+
"SFMono-Regular",
|
|
465
|
+
"Menlo",
|
|
466
|
+
"Monaco",
|
|
467
|
+
"Consolas",
|
|
468
|
+
"Liberation Mono",
|
|
469
|
+
"Courier New",
|
|
470
|
+
"monospace",
|
|
471
|
+
] as const;
|
|
472
|
+
|
|
473
|
+
const CSS_GENERIC_FONT_FAMILIES = new Set([
|
|
474
|
+
"serif",
|
|
475
|
+
"sans-serif",
|
|
476
|
+
"monospace",
|
|
477
|
+
"cursive",
|
|
478
|
+
"fantasy",
|
|
479
|
+
"system-ui",
|
|
480
|
+
"emoji",
|
|
481
|
+
"math",
|
|
482
|
+
"fangsong",
|
|
483
|
+
"ui-serif",
|
|
484
|
+
"ui-sans-serif",
|
|
485
|
+
"ui-monospace",
|
|
486
|
+
"ui-rounded",
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
let cachedStudioMonoFontStack: string | null = null;
|
|
490
|
+
|
|
491
|
+
function getHomeDirectory(): string {
|
|
492
|
+
return process.env.HOME ?? homedir();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function getXdgConfigDirectory(): string {
|
|
496
|
+
const configured = process.env.XDG_CONFIG_HOME?.trim();
|
|
497
|
+
if (configured) return configured;
|
|
498
|
+
return join(getHomeDirectory(), ".config");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function sanitizeCssValue(value: string): string {
|
|
502
|
+
return value.replace(/[\r\n;]+/g, " ").trim();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function stripSimpleInlineComment(value: string): string {
|
|
506
|
+
let quote: '"' | "'" | null = null;
|
|
507
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
508
|
+
const char = value[i];
|
|
509
|
+
if (quote) {
|
|
510
|
+
if (char === quote && value[i - 1] !== "\\") quote = null;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (char === '"' || char === "'") {
|
|
514
|
+
quote = char;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (char === "#") {
|
|
518
|
+
return value.slice(0, i).trim();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return value.trim();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function normalizeConfiguredFontFamily(value: string | undefined): string | undefined {
|
|
525
|
+
if (!value) return undefined;
|
|
526
|
+
const sanitized = sanitizeCssValue(stripSimpleInlineComment(value));
|
|
527
|
+
if (!sanitized) return undefined;
|
|
528
|
+
const unquoted =
|
|
529
|
+
(sanitized.startsWith('"') && sanitized.endsWith('"'))
|
|
530
|
+
|| (sanitized.startsWith("'") && sanitized.endsWith("'"))
|
|
531
|
+
? sanitized.slice(1, -1).trim()
|
|
532
|
+
: sanitized;
|
|
533
|
+
return unquoted || undefined;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function formatCssFontFamilyToken(value: string): string {
|
|
537
|
+
const trimmed = sanitizeCssValue(value);
|
|
538
|
+
if (!trimmed) return "";
|
|
539
|
+
if (CSS_GENERIC_FONT_FAMILIES.has(trimmed.toLowerCase())) return trimmed;
|
|
540
|
+
if (
|
|
541
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
542
|
+
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
543
|
+
) {
|
|
544
|
+
return trimmed;
|
|
545
|
+
}
|
|
546
|
+
return `"${trimmed.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function readFirstExistingTextFile(paths: string[]): string | undefined {
|
|
550
|
+
for (const path of paths) {
|
|
551
|
+
try {
|
|
552
|
+
const text = readFileSync(path, "utf-8");
|
|
553
|
+
if (text.trim()) return text;
|
|
554
|
+
} catch {
|
|
555
|
+
// Ignore missing/unreadable files
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function detectGhosttyFontFamily(): string | undefined {
|
|
562
|
+
const home = getHomeDirectory();
|
|
563
|
+
const content = readFirstExistingTextFile([
|
|
564
|
+
join(getXdgConfigDirectory(), "ghostty", "config"),
|
|
565
|
+
join(home, "Library", "Application Support", "com.mitchellh.ghostty", "config"),
|
|
566
|
+
]);
|
|
567
|
+
if (!content) return undefined;
|
|
568
|
+
const match = content.match(/^\s*font-family\s*=\s*(.+?)\s*$/m);
|
|
569
|
+
return normalizeConfiguredFontFamily(match?.[1]);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function detectKittyFontFamily(): string | undefined {
|
|
573
|
+
const content = readFirstExistingTextFile([
|
|
574
|
+
join(getXdgConfigDirectory(), "kitty", "kitty.conf"),
|
|
575
|
+
]);
|
|
576
|
+
if (!content) return undefined;
|
|
577
|
+
const match = content.match(/^\s*font_family\s+(.+?)\s*$/m);
|
|
578
|
+
return normalizeConfiguredFontFamily(match?.[1]);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function detectWezTermFontFamily(): string | undefined {
|
|
582
|
+
const home = getHomeDirectory();
|
|
583
|
+
const content = readFirstExistingTextFile([
|
|
584
|
+
join(getXdgConfigDirectory(), "wezterm", "wezterm.lua"),
|
|
585
|
+
join(home, ".wezterm.lua"),
|
|
586
|
+
]);
|
|
587
|
+
if (!content) return undefined;
|
|
588
|
+
const patterns = [
|
|
589
|
+
/font_with_fallback\s*\(\s*\{[\s\S]*?["']([^"']+)["']/m,
|
|
590
|
+
/font\s*\(\s*["']([^"']+)["']/m,
|
|
591
|
+
/font\s*=\s*["']([^"']+)["']/m,
|
|
592
|
+
/family\s*=\s*["']([^"']+)["']/m,
|
|
593
|
+
];
|
|
594
|
+
for (const pattern of patterns) {
|
|
595
|
+
const family = normalizeConfiguredFontFamily(content.match(pattern)?.[1]);
|
|
596
|
+
if (family) return family;
|
|
597
|
+
}
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function detectAlacrittyFontFamily(): string | undefined {
|
|
602
|
+
const content = readFirstExistingTextFile([
|
|
603
|
+
join(getXdgConfigDirectory(), "alacritty", "alacritty.toml"),
|
|
604
|
+
join(getXdgConfigDirectory(), "alacritty.toml"),
|
|
605
|
+
join(getXdgConfigDirectory(), "alacritty", "alacritty.yml"),
|
|
606
|
+
join(getXdgConfigDirectory(), "alacritty", "alacritty.yaml"),
|
|
607
|
+
]);
|
|
608
|
+
if (!content) return undefined;
|
|
609
|
+
const patterns = [
|
|
610
|
+
/^\s*family\s*=\s*["']([^"']+)["']\s*$/m,
|
|
611
|
+
/^\s*family\s*:\s*["']?([^"'#\n]+)["']?\s*$/m,
|
|
612
|
+
];
|
|
613
|
+
for (const pattern of patterns) {
|
|
614
|
+
const family = normalizeConfiguredFontFamily(content.match(pattern)?.[1]);
|
|
615
|
+
if (family) return family;
|
|
616
|
+
}
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function detectTerminalMonospaceFontFamily(): string | undefined {
|
|
621
|
+
const termProgram = (process.env.TERM_PROGRAM ?? "").trim().toLowerCase();
|
|
622
|
+
const term = (process.env.TERM ?? "").trim().toLowerCase();
|
|
623
|
+
|
|
624
|
+
if (termProgram === "ghostty" || term.includes("ghostty")) return detectGhosttyFontFamily();
|
|
625
|
+
if (termProgram === "wezterm") return detectWezTermFontFamily();
|
|
626
|
+
if (termProgram === "kitty" || term.includes("kitty")) return detectKittyFontFamily();
|
|
627
|
+
if (termProgram === "alacritty") return detectAlacrittyFontFamily();
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function buildMonoFontStack(primaryFamily?: string): string {
|
|
632
|
+
const entries: string[] = [];
|
|
633
|
+
const seen = new Set<string>();
|
|
634
|
+
const push = (family: string) => {
|
|
635
|
+
const trimmed = family.trim();
|
|
636
|
+
if (!trimmed) return;
|
|
637
|
+
const key = trimmed.replace(/^['"]|['"]$/g, "").toLowerCase();
|
|
638
|
+
if (seen.has(key)) return;
|
|
639
|
+
seen.add(key);
|
|
640
|
+
entries.push(formatCssFontFamilyToken(trimmed));
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
if (primaryFamily) push(primaryFamily);
|
|
644
|
+
for (const family of DEFAULT_MONO_FONT_FAMILIES) push(family);
|
|
645
|
+
return entries.join(", ");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function getStudioMonoFontStack(): string {
|
|
649
|
+
if (cachedStudioMonoFontStack) return cachedStudioMonoFontStack;
|
|
650
|
+
|
|
651
|
+
const override = sanitizeCssValue(process.env.PI_STUDIO_FONT_MONO ?? "");
|
|
652
|
+
if (override) {
|
|
653
|
+
cachedStudioMonoFontStack = override;
|
|
654
|
+
return cachedStudioMonoFontStack;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
cachedStudioMonoFontStack = buildMonoFontStack(detectTerminalMonospaceFontFamily());
|
|
658
|
+
return cachedStudioMonoFontStack;
|
|
659
|
+
}
|
|
660
|
+
|
|
456
661
|
function resolveThemeExportValue(
|
|
457
662
|
value: string | number | undefined,
|
|
458
663
|
vars: Record<string, string | number>,
|
|
@@ -1498,6 +1703,13 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
1498
1703
|
};
|
|
1499
1704
|
}
|
|
1500
1705
|
|
|
1706
|
+
if (msg.type === "cancel_request" && typeof msg.requestId === "string") {
|
|
1707
|
+
return {
|
|
1708
|
+
type: "cancel_request",
|
|
1709
|
+
requestId: msg.requestId,
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1501
1713
|
return null;
|
|
1502
1714
|
}
|
|
1503
1715
|
|
|
@@ -1693,6 +1905,7 @@ function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
|
|
|
1693
1905
|
? "0 1px 2px rgba(15, 23, 42, 0.03), 0 4px 14px rgba(15, 23, 42, 0.04)"
|
|
1694
1906
|
: "0 1px 2px rgba(0, 0, 0, 0.36), 0 6px 18px rgba(0, 0, 0, 0.22)";
|
|
1695
1907
|
const accentContrast = style.mode === "light" ? "#ffffff" : "#0e1616";
|
|
1908
|
+
const errorContrast = style.mode === "light" ? "#ffffff" : "#0e1616";
|
|
1696
1909
|
const blockquoteBg = withAlpha(
|
|
1697
1910
|
style.palette.mdQuoteBorder,
|
|
1698
1911
|
style.mode === "light" ? 0.10 : 0.16,
|
|
@@ -1706,6 +1919,7 @@ function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
|
|
|
1706
1919
|
const editorBg = style.mode === "light"
|
|
1707
1920
|
? blendColors(style.palette.panel, "#ffffff", 0.5)
|
|
1708
1921
|
: style.palette.panel;
|
|
1922
|
+
const monoFontStack = getStudioMonoFontStack();
|
|
1709
1923
|
|
|
1710
1924
|
return {
|
|
1711
1925
|
"color-scheme": style.mode,
|
|
@@ -1747,9 +1961,11 @@ function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
|
|
|
1747
1961
|
"--syntax-punctuation": style.palette.syntaxPunctuation,
|
|
1748
1962
|
"--panel-shadow": panelShadow,
|
|
1749
1963
|
"--accent-contrast": accentContrast,
|
|
1964
|
+
"--error-contrast": errorContrast,
|
|
1750
1965
|
"--blockquote-bg": blockquoteBg,
|
|
1751
1966
|
"--table-alt-bg": tableAltBg,
|
|
1752
1967
|
"--editor-bg": editorBg,
|
|
1968
|
+
"--font-mono": monoFontStack,
|
|
1753
1969
|
};
|
|
1754
1970
|
}
|
|
1755
1971
|
|
|
@@ -1780,10 +1996,11 @@ function buildStudioHtml(
|
|
|
1780
1996
|
: "";
|
|
1781
1997
|
const style = getStudioThemeStyle(theme);
|
|
1782
1998
|
const vars = buildThemeCssVars(style);
|
|
1999
|
+
const monoFontStack = vars["--font-mono"] ?? buildMonoFontStack();
|
|
1783
2000
|
const mermaidConfig = {
|
|
1784
2001
|
startOnLoad: false,
|
|
1785
2002
|
theme: "base",
|
|
1786
|
-
fontFamily:
|
|
2003
|
+
fontFamily: monoFontStack,
|
|
1787
2004
|
flowchart: {
|
|
1788
2005
|
curve: "basis",
|
|
1789
2006
|
},
|
|
@@ -1909,6 +2126,14 @@ ${cssVarsBlock}
|
|
|
1909
2126
|
}
|
|
1910
2127
|
|
|
1911
2128
|
#sendRunBtn,
|
|
2129
|
+
#critiqueBtn {
|
|
2130
|
+
min-width: 10rem;
|
|
2131
|
+
display: inline-flex;
|
|
2132
|
+
justify-content: center;
|
|
2133
|
+
align-items: center;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
#sendRunBtn:not(:disabled):not(.request-stop-active),
|
|
1912
2137
|
#loadResponseBtn:not(:disabled):not([hidden]) {
|
|
1913
2138
|
background: var(--accent);
|
|
1914
2139
|
border-color: var(--accent);
|
|
@@ -1916,11 +2141,24 @@ ${cssVarsBlock}
|
|
|
1916
2141
|
font-weight: 600;
|
|
1917
2142
|
}
|
|
1918
2143
|
|
|
1919
|
-
#sendRunBtn:not(:disabled):hover,
|
|
2144
|
+
#sendRunBtn:not(:disabled):not(.request-stop-active):hover,
|
|
1920
2145
|
#loadResponseBtn:not(:disabled):not([hidden]):hover {
|
|
1921
2146
|
filter: brightness(0.95);
|
|
1922
2147
|
}
|
|
1923
2148
|
|
|
2149
|
+
#sendRunBtn.request-stop-active,
|
|
2150
|
+
#critiqueBtn.request-stop-active {
|
|
2151
|
+
background: var(--error);
|
|
2152
|
+
border-color: var(--error);
|
|
2153
|
+
color: var(--error-contrast);
|
|
2154
|
+
font-weight: 600;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
#sendRunBtn.request-stop-active:not(:disabled):hover,
|
|
2158
|
+
#critiqueBtn.request-stop-active:not(:disabled):hover {
|
|
2159
|
+
filter: brightness(0.95);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
1924
2162
|
.file-label {
|
|
1925
2163
|
cursor: pointer;
|
|
1926
2164
|
display: inline-flex;
|
|
@@ -2034,7 +2272,7 @@ ${cssVarsBlock}
|
|
|
2034
2272
|
font-size: 13px;
|
|
2035
2273
|
line-height: 1.45;
|
|
2036
2274
|
tab-size: 2;
|
|
2037
|
-
font-family:
|
|
2275
|
+
font-family: var(--font-mono);
|
|
2038
2276
|
resize: vertical;
|
|
2039
2277
|
}
|
|
2040
2278
|
|
|
@@ -2165,6 +2403,7 @@ ${cssVarsBlock}
|
|
|
2165
2403
|
border-radius: 8px;
|
|
2166
2404
|
background: var(--editor-bg);
|
|
2167
2405
|
overflow: hidden;
|
|
2406
|
+
overscroll-behavior: none;
|
|
2168
2407
|
}
|
|
2169
2408
|
|
|
2170
2409
|
.editor-highlight {
|
|
@@ -2177,8 +2416,10 @@ ${cssVarsBlock}
|
|
|
2177
2416
|
overflow: auto;
|
|
2178
2417
|
pointer-events: none;
|
|
2179
2418
|
white-space: pre-wrap;
|
|
2180
|
-
word-break:
|
|
2181
|
-
|
|
2419
|
+
word-break: normal;
|
|
2420
|
+
overflow-wrap: break-word;
|
|
2421
|
+
overscroll-behavior: none;
|
|
2422
|
+
font-family: var(--font-mono);
|
|
2182
2423
|
font-size: 13px;
|
|
2183
2424
|
line-height: 1.45;
|
|
2184
2425
|
tab-size: 2;
|
|
@@ -2197,6 +2438,7 @@ ${cssVarsBlock}
|
|
|
2197
2438
|
background: transparent;
|
|
2198
2439
|
resize: none;
|
|
2199
2440
|
outline: none;
|
|
2441
|
+
overscroll-behavior: none;
|
|
2200
2442
|
}
|
|
2201
2443
|
|
|
2202
2444
|
#sourceText.highlight-active {
|
|
@@ -2240,7 +2482,7 @@ ${cssVarsBlock}
|
|
|
2240
2482
|
|
|
2241
2483
|
.hl-code-com {
|
|
2242
2484
|
color: var(--syntax-comment);
|
|
2243
|
-
font-style:
|
|
2485
|
+
font-style: normal;
|
|
2244
2486
|
}
|
|
2245
2487
|
|
|
2246
2488
|
.hl-code-var,
|
|
@@ -2269,7 +2511,7 @@ ${cssVarsBlock}
|
|
|
2269
2511
|
|
|
2270
2512
|
.hl-quote {
|
|
2271
2513
|
color: var(--md-quote);
|
|
2272
|
-
font-style:
|
|
2514
|
+
font-style: normal;
|
|
2273
2515
|
}
|
|
2274
2516
|
|
|
2275
2517
|
.hl-link {
|
|
@@ -2284,9 +2526,10 @@ ${cssVarsBlock}
|
|
|
2284
2526
|
.hl-annotation {
|
|
2285
2527
|
color: var(--accent);
|
|
2286
2528
|
background: var(--accent-soft);
|
|
2287
|
-
border:
|
|
2529
|
+
border: 0;
|
|
2288
2530
|
border-radius: 4px;
|
|
2289
|
-
padding: 0
|
|
2531
|
+
padding: 0;
|
|
2532
|
+
box-shadow: inset 0 0 0 1px var(--marker-border);
|
|
2290
2533
|
}
|
|
2291
2534
|
|
|
2292
2535
|
.hl-annotation-muted {
|
|
@@ -2419,7 +2662,7 @@ ${cssVarsBlock}
|
|
|
2419
2662
|
}
|
|
2420
2663
|
|
|
2421
2664
|
.rendered-markdown code {
|
|
2422
|
-
font-family:
|
|
2665
|
+
font-family: var(--font-mono);
|
|
2423
2666
|
font-size: 0.9em;
|
|
2424
2667
|
color: var(--md-code);
|
|
2425
2668
|
}
|
|
@@ -2566,7 +2809,7 @@ ${cssVarsBlock}
|
|
|
2566
2809
|
margin: 0;
|
|
2567
2810
|
white-space: pre-wrap;
|
|
2568
2811
|
word-break: break-word;
|
|
2569
|
-
font-family:
|
|
2812
|
+
font-family: var(--font-mono);
|
|
2570
2813
|
font-size: 13px;
|
|
2571
2814
|
line-height: 1.5;
|
|
2572
2815
|
}
|
|
@@ -2575,7 +2818,7 @@ ${cssVarsBlock}
|
|
|
2575
2818
|
margin: 0;
|
|
2576
2819
|
white-space: pre-wrap;
|
|
2577
2820
|
word-break: break-word;
|
|
2578
|
-
font-family:
|
|
2821
|
+
font-family: var(--font-mono);
|
|
2579
2822
|
font-size: 13px;
|
|
2580
2823
|
line-height: 1.5;
|
|
2581
2824
|
}
|
|
@@ -3177,6 +3420,7 @@ ${cssVarsBlock}
|
|
|
3177
3420
|
let editorHighlightRenderRaf = null;
|
|
3178
3421
|
let annotationsEnabled = true;
|
|
3179
3422
|
const ANNOTATION_MARKER_REGEX = /\\[an:\\s*([^\\]\\n]+?)\\]/gi;
|
|
3423
|
+
const EMPTY_OVERLAY_LINE = "\\u200b";
|
|
3180
3424
|
const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
3181
3425
|
const MERMAID_CONFIG = ${JSON.stringify(mermaidConfig)};
|
|
3182
3426
|
const MERMAID_UNAVAILABLE_MESSAGE = "Mermaid renderer unavailable. Showing mermaid blocks as code.";
|
|
@@ -4636,7 +4880,7 @@ ${cssVarsBlock}
|
|
|
4636
4880
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
4637
4881
|
sendEditorBtn.disabled = uiBusy;
|
|
4638
4882
|
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
4639
|
-
|
|
4883
|
+
syncRunAndCritiqueButtons();
|
|
4640
4884
|
copyDraftBtn.disabled = uiBusy;
|
|
4641
4885
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
4642
4886
|
if (langSelect) langSelect.disabled = uiBusy;
|
|
@@ -4649,7 +4893,6 @@ ${cssVarsBlock}
|
|
|
4649
4893
|
followSelect.disabled = uiBusy;
|
|
4650
4894
|
if (responseHighlightSelect) responseHighlightSelect.disabled = uiBusy || rightView !== "markdown";
|
|
4651
4895
|
insertHeaderBtn.disabled = uiBusy;
|
|
4652
|
-
critiqueBtn.disabled = uiBusy;
|
|
4653
4896
|
lensSelect.disabled = uiBusy;
|
|
4654
4897
|
updateSaveFileTooltip();
|
|
4655
4898
|
updateHistoryControls();
|
|
@@ -5073,7 +5316,12 @@ ${cssVarsBlock}
|
|
|
5073
5316
|
}
|
|
5074
5317
|
|
|
5075
5318
|
if (inFence) {
|
|
5076
|
-
out.push(line.length > 0 ? highlightCodeLine(line, fenceLanguage) :
|
|
5319
|
+
out.push(line.length > 0 ? highlightCodeLine(line, fenceLanguage) : EMPTY_OVERLAY_LINE);
|
|
5320
|
+
continue;
|
|
5321
|
+
}
|
|
5322
|
+
|
|
5323
|
+
if (line.length === 0) {
|
|
5324
|
+
out.push(EMPTY_OVERLAY_LINE);
|
|
5077
5325
|
continue;
|
|
5078
5326
|
}
|
|
5079
5327
|
|
|
@@ -5112,7 +5360,7 @@ ${cssVarsBlock}
|
|
|
5112
5360
|
const out = [];
|
|
5113
5361
|
for (const line of lines) {
|
|
5114
5362
|
if (line.length === 0) {
|
|
5115
|
-
out.push(
|
|
5363
|
+
out.push(EMPTY_OVERLAY_LINE);
|
|
5116
5364
|
} else if (lang) {
|
|
5117
5365
|
out.push(highlightCodeLine(line, lang));
|
|
5118
5366
|
} else {
|
|
@@ -5341,6 +5589,51 @@ ${cssVarsBlock}
|
|
|
5341
5589
|
renderActiveResult();
|
|
5342
5590
|
}
|
|
5343
5591
|
|
|
5592
|
+
function getAbortablePendingKind() {
|
|
5593
|
+
if (!pendingRequestId) return null;
|
|
5594
|
+
return pendingKind === "direct" || pendingKind === "critique" ? pendingKind : null;
|
|
5595
|
+
}
|
|
5596
|
+
|
|
5597
|
+
function requestCancelForPendingRequest(expectedKind) {
|
|
5598
|
+
const activeKind = getAbortablePendingKind();
|
|
5599
|
+
if (!activeKind || activeKind !== expectedKind || !pendingRequestId) {
|
|
5600
|
+
setStatus("No matching Studio request is running.", "warning");
|
|
5601
|
+
return false;
|
|
5602
|
+
}
|
|
5603
|
+
const sent = sendMessage({ type: "cancel_request", requestId: pendingRequestId });
|
|
5604
|
+
if (!sent) return false;
|
|
5605
|
+
setStatus("Stopping request…", "warning");
|
|
5606
|
+
return true;
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5609
|
+
function syncRunAndCritiqueButtons() {
|
|
5610
|
+
const activeKind = getAbortablePendingKind();
|
|
5611
|
+
const sendRunIsStop = activeKind === "direct";
|
|
5612
|
+
const critiqueIsStop = activeKind === "critique";
|
|
5613
|
+
|
|
5614
|
+
if (sendRunBtn) {
|
|
5615
|
+
sendRunBtn.textContent = sendRunIsStop ? "Stop" : "Run editor text";
|
|
5616
|
+
sendRunBtn.classList.toggle("request-stop-active", sendRunIsStop);
|
|
5617
|
+
sendRunBtn.disabled = sendRunIsStop ? wsState === "Disconnected" : (uiBusy || critiqueIsStop);
|
|
5618
|
+
sendRunBtn.title = sendRunIsStop
|
|
5619
|
+
? "Stop the running editor-text request."
|
|
5620
|
+
: (annotationsEnabled
|
|
5621
|
+
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter."
|
|
5622
|
+
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter.");
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5625
|
+
if (critiqueBtn) {
|
|
5626
|
+
critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique editor text";
|
|
5627
|
+
critiqueBtn.classList.toggle("request-stop-active", critiqueIsStop);
|
|
5628
|
+
critiqueBtn.disabled = critiqueIsStop ? wsState === "Disconnected" : (uiBusy || sendRunIsStop);
|
|
5629
|
+
critiqueBtn.title = critiqueIsStop
|
|
5630
|
+
? "Stop the running critique request."
|
|
5631
|
+
: (annotationsEnabled
|
|
5632
|
+
? "Critique editor text as-is (includes [an: ...] markers)."
|
|
5633
|
+
: "Critique editor text with [an: ...] markers stripped.");
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5344
5637
|
function updateAnnotationModeUi() {
|
|
5345
5638
|
if (annotationModeSelect) {
|
|
5346
5639
|
annotationModeSelect.value = annotationsEnabled ? "on" : "off";
|
|
@@ -5349,17 +5642,7 @@ ${cssVarsBlock}
|
|
|
5349
5642
|
: "Annotations Hidden: keep markers in editor, hide in preview, and strip before Run/Critique.";
|
|
5350
5643
|
}
|
|
5351
5644
|
|
|
5352
|
-
|
|
5353
|
-
sendRunBtn.title = annotationsEnabled
|
|
5354
|
-
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter."
|
|
5355
|
-
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter.";
|
|
5356
|
-
}
|
|
5357
|
-
|
|
5358
|
-
if (critiqueBtn) {
|
|
5359
|
-
critiqueBtn.title = annotationsEnabled
|
|
5360
|
-
? "Critique editor text as-is (includes [an: ...] markers)."
|
|
5361
|
-
: "Critique editor text with [an: ...] markers stripped.";
|
|
5362
|
-
}
|
|
5645
|
+
syncRunAndCritiqueButtons();
|
|
5363
5646
|
}
|
|
5364
5647
|
|
|
5365
5648
|
function setAnnotationsEnabled(enabled, _options) {
|
|
@@ -6243,6 +6526,11 @@ ${cssVarsBlock}
|
|
|
6243
6526
|
});
|
|
6244
6527
|
|
|
6245
6528
|
critiqueBtn.addEventListener("click", () => {
|
|
6529
|
+
if (getAbortablePendingKind() === "critique") {
|
|
6530
|
+
requestCancelForPendingRequest("critique");
|
|
6531
|
+
return;
|
|
6532
|
+
}
|
|
6533
|
+
|
|
6246
6534
|
const preparedDocumentText = prepareEditorTextForSend(sourceTextEl.value);
|
|
6247
6535
|
const documentText = preparedDocumentText.trim();
|
|
6248
6536
|
if (!documentText) {
|
|
@@ -6438,6 +6726,11 @@ ${cssVarsBlock}
|
|
|
6438
6726
|
}
|
|
6439
6727
|
|
|
6440
6728
|
sendRunBtn.addEventListener("click", () => {
|
|
6729
|
+
if (getAbortablePendingKind() === "direct") {
|
|
6730
|
+
requestCancelForPendingRequest("direct");
|
|
6731
|
+
return;
|
|
6732
|
+
}
|
|
6733
|
+
|
|
6441
6734
|
const prepared = prepareEditorTextForSend(sourceTextEl.value);
|
|
6442
6735
|
if (!prepared.trim()) {
|
|
6443
6736
|
setStatus("Editor is empty. Nothing to run.", "warning");
|
|
@@ -6651,6 +6944,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6651
6944
|
let studioCwd = process.cwd();
|
|
6652
6945
|
let lastCommandCtx: ExtensionCommandContext | null = null;
|
|
6653
6946
|
let lastThemeVarsJson = "";
|
|
6947
|
+
let suppressedStudioResponse: { requestId: string; kind: StudioRequestKind } | null = null;
|
|
6654
6948
|
let agentBusy = false;
|
|
6655
6949
|
let terminalActivityPhase: TerminalActivityPhase = "idle";
|
|
6656
6950
|
let terminalActivityToolName: string | null = null;
|
|
@@ -6905,7 +7199,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
6905
7199
|
}
|
|
6906
7200
|
};
|
|
6907
7201
|
|
|
7202
|
+
const cancelActiveRequest = (requestId: string): { ok: true; kind: StudioRequestKind } | { ok: false; message: string } => {
|
|
7203
|
+
if (!activeRequest) {
|
|
7204
|
+
return { ok: false, message: "No studio request is currently running." };
|
|
7205
|
+
}
|
|
7206
|
+
if (activeRequest.id !== requestId) {
|
|
7207
|
+
return { ok: false, message: "That studio request is no longer active." };
|
|
7208
|
+
}
|
|
7209
|
+
if (!lastCommandCtx) {
|
|
7210
|
+
return { ok: false, message: "No interactive pi context is available to stop the request." };
|
|
7211
|
+
}
|
|
7212
|
+
|
|
7213
|
+
const kind = activeRequest.kind;
|
|
7214
|
+
try {
|
|
7215
|
+
lastCommandCtx.abort();
|
|
7216
|
+
} catch (error) {
|
|
7217
|
+
return {
|
|
7218
|
+
ok: false,
|
|
7219
|
+
message: `Failed to stop request: ${error instanceof Error ? error.message : String(error)}`,
|
|
7220
|
+
};
|
|
7221
|
+
}
|
|
7222
|
+
|
|
7223
|
+
suppressedStudioResponse = { requestId, kind };
|
|
7224
|
+
emitDebugEvent("cancel_active_request", { requestId, kind });
|
|
7225
|
+
clearActiveRequest({ notify: "Cancelled request.", level: "warning" });
|
|
7226
|
+
return { ok: true, kind };
|
|
7227
|
+
};
|
|
7228
|
+
|
|
6908
7229
|
const beginRequest = (requestId: string, kind: StudioRequestKind): boolean => {
|
|
7230
|
+
suppressedStudioResponse = null;
|
|
6909
7231
|
emitDebugEvent("begin_request_attempt", {
|
|
6910
7232
|
requestId,
|
|
6911
7233
|
kind,
|
|
@@ -7013,6 +7335,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
7013
7335
|
return;
|
|
7014
7336
|
}
|
|
7015
7337
|
|
|
7338
|
+
if (msg.type === "cancel_request") {
|
|
7339
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
7340
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
7341
|
+
return;
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7344
|
+
const result = cancelActiveRequest(msg.requestId);
|
|
7345
|
+
if (!result.ok) {
|
|
7346
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: result.message });
|
|
7347
|
+
}
|
|
7348
|
+
return;
|
|
7349
|
+
}
|
|
7350
|
+
|
|
7016
7351
|
if (msg.type === "critique_request") {
|
|
7017
7352
|
if (!isValidRequestId(msg.requestId)) {
|
|
7018
7353
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
@@ -7845,6 +8180,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
7845
8180
|
|
|
7846
8181
|
if (!markdown) return;
|
|
7847
8182
|
|
|
8183
|
+
if (suppressedStudioResponse) {
|
|
8184
|
+
emitDebugEvent("suppressed_cancelled_response", {
|
|
8185
|
+
requestId: suppressedStudioResponse.requestId,
|
|
8186
|
+
kind: suppressedStudioResponse.kind,
|
|
8187
|
+
markdownLength: markdown.length,
|
|
8188
|
+
thinkingLength: thinking ? thinking.length : 0,
|
|
8189
|
+
});
|
|
8190
|
+
return;
|
|
8191
|
+
}
|
|
8192
|
+
|
|
7848
8193
|
syncStudioResponseHistory(ctx.sessionManager.getBranch());
|
|
7849
8194
|
refreshContextUsage(ctx);
|
|
7850
8195
|
const latestHistoryItem = studioResponseHistory[studioResponseHistory.length - 1];
|
|
@@ -7925,7 +8270,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
7925
8270
|
pi.on("agent_end", async () => {
|
|
7926
8271
|
agentBusy = false;
|
|
7927
8272
|
refreshContextUsage();
|
|
7928
|
-
emitDebugEvent("agent_end", {
|
|
8273
|
+
emitDebugEvent("agent_end", {
|
|
8274
|
+
activeRequestId: activeRequest?.id ?? null,
|
|
8275
|
+
activeRequestKind: activeRequest?.kind ?? null,
|
|
8276
|
+
suppressedRequestId: suppressedStudioResponse?.requestId ?? null,
|
|
8277
|
+
suppressedRequestKind: suppressedStudioResponse?.kind ?? null,
|
|
8278
|
+
});
|
|
7929
8279
|
setTerminalActivity("idle");
|
|
7930
8280
|
if (activeRequest) {
|
|
7931
8281
|
const requestId = activeRequest.id;
|
|
@@ -7936,6 +8286,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7936
8286
|
});
|
|
7937
8287
|
clearActiveRequest();
|
|
7938
8288
|
}
|
|
8289
|
+
suppressedStudioResponse = null;
|
|
7939
8290
|
});
|
|
7940
8291
|
|
|
7941
8292
|
pi.on("session_shutdown", async () => {
|