pi-studio 0.5.5 → 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 +6 -0
- package/README.md +1 -0
- package/index.ts +363 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -88,6 +88,12 @@ 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
|
+
|
|
91
97
|
## [0.5.5] — 2026-03-09
|
|
92
98
|
|
|
93
99
|
### Fixed
|
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
|
|
|
@@ -2181,7 +2419,7 @@ ${cssVarsBlock}
|
|
|
2181
2419
|
word-break: normal;
|
|
2182
2420
|
overflow-wrap: break-word;
|
|
2183
2421
|
overscroll-behavior: none;
|
|
2184
|
-
font-family:
|
|
2422
|
+
font-family: var(--font-mono);
|
|
2185
2423
|
font-size: 13px;
|
|
2186
2424
|
line-height: 1.45;
|
|
2187
2425
|
tab-size: 2;
|
|
@@ -2424,7 +2662,7 @@ ${cssVarsBlock}
|
|
|
2424
2662
|
}
|
|
2425
2663
|
|
|
2426
2664
|
.rendered-markdown code {
|
|
2427
|
-
font-family:
|
|
2665
|
+
font-family: var(--font-mono);
|
|
2428
2666
|
font-size: 0.9em;
|
|
2429
2667
|
color: var(--md-code);
|
|
2430
2668
|
}
|
|
@@ -2571,7 +2809,7 @@ ${cssVarsBlock}
|
|
|
2571
2809
|
margin: 0;
|
|
2572
2810
|
white-space: pre-wrap;
|
|
2573
2811
|
word-break: break-word;
|
|
2574
|
-
font-family:
|
|
2812
|
+
font-family: var(--font-mono);
|
|
2575
2813
|
font-size: 13px;
|
|
2576
2814
|
line-height: 1.5;
|
|
2577
2815
|
}
|
|
@@ -2580,7 +2818,7 @@ ${cssVarsBlock}
|
|
|
2580
2818
|
margin: 0;
|
|
2581
2819
|
white-space: pre-wrap;
|
|
2582
2820
|
word-break: break-word;
|
|
2583
|
-
font-family:
|
|
2821
|
+
font-family: var(--font-mono);
|
|
2584
2822
|
font-size: 13px;
|
|
2585
2823
|
line-height: 1.5;
|
|
2586
2824
|
}
|
|
@@ -4642,7 +4880,7 @@ ${cssVarsBlock}
|
|
|
4642
4880
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
4643
4881
|
sendEditorBtn.disabled = uiBusy;
|
|
4644
4882
|
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
4645
|
-
|
|
4883
|
+
syncRunAndCritiqueButtons();
|
|
4646
4884
|
copyDraftBtn.disabled = uiBusy;
|
|
4647
4885
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
4648
4886
|
if (langSelect) langSelect.disabled = uiBusy;
|
|
@@ -4655,7 +4893,6 @@ ${cssVarsBlock}
|
|
|
4655
4893
|
followSelect.disabled = uiBusy;
|
|
4656
4894
|
if (responseHighlightSelect) responseHighlightSelect.disabled = uiBusy || rightView !== "markdown";
|
|
4657
4895
|
insertHeaderBtn.disabled = uiBusy;
|
|
4658
|
-
critiqueBtn.disabled = uiBusy;
|
|
4659
4896
|
lensSelect.disabled = uiBusy;
|
|
4660
4897
|
updateSaveFileTooltip();
|
|
4661
4898
|
updateHistoryControls();
|
|
@@ -5352,6 +5589,51 @@ ${cssVarsBlock}
|
|
|
5352
5589
|
renderActiveResult();
|
|
5353
5590
|
}
|
|
5354
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
|
+
|
|
5355
5637
|
function updateAnnotationModeUi() {
|
|
5356
5638
|
if (annotationModeSelect) {
|
|
5357
5639
|
annotationModeSelect.value = annotationsEnabled ? "on" : "off";
|
|
@@ -5360,17 +5642,7 @@ ${cssVarsBlock}
|
|
|
5360
5642
|
: "Annotations Hidden: keep markers in editor, hide in preview, and strip before Run/Critique.";
|
|
5361
5643
|
}
|
|
5362
5644
|
|
|
5363
|
-
|
|
5364
|
-
sendRunBtn.title = annotationsEnabled
|
|
5365
|
-
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter."
|
|
5366
|
-
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter.";
|
|
5367
|
-
}
|
|
5368
|
-
|
|
5369
|
-
if (critiqueBtn) {
|
|
5370
|
-
critiqueBtn.title = annotationsEnabled
|
|
5371
|
-
? "Critique editor text as-is (includes [an: ...] markers)."
|
|
5372
|
-
: "Critique editor text with [an: ...] markers stripped.";
|
|
5373
|
-
}
|
|
5645
|
+
syncRunAndCritiqueButtons();
|
|
5374
5646
|
}
|
|
5375
5647
|
|
|
5376
5648
|
function setAnnotationsEnabled(enabled, _options) {
|
|
@@ -6254,6 +6526,11 @@ ${cssVarsBlock}
|
|
|
6254
6526
|
});
|
|
6255
6527
|
|
|
6256
6528
|
critiqueBtn.addEventListener("click", () => {
|
|
6529
|
+
if (getAbortablePendingKind() === "critique") {
|
|
6530
|
+
requestCancelForPendingRequest("critique");
|
|
6531
|
+
return;
|
|
6532
|
+
}
|
|
6533
|
+
|
|
6257
6534
|
const preparedDocumentText = prepareEditorTextForSend(sourceTextEl.value);
|
|
6258
6535
|
const documentText = preparedDocumentText.trim();
|
|
6259
6536
|
if (!documentText) {
|
|
@@ -6449,6 +6726,11 @@ ${cssVarsBlock}
|
|
|
6449
6726
|
}
|
|
6450
6727
|
|
|
6451
6728
|
sendRunBtn.addEventListener("click", () => {
|
|
6729
|
+
if (getAbortablePendingKind() === "direct") {
|
|
6730
|
+
requestCancelForPendingRequest("direct");
|
|
6731
|
+
return;
|
|
6732
|
+
}
|
|
6733
|
+
|
|
6452
6734
|
const prepared = prepareEditorTextForSend(sourceTextEl.value);
|
|
6453
6735
|
if (!prepared.trim()) {
|
|
6454
6736
|
setStatus("Editor is empty. Nothing to run.", "warning");
|
|
@@ -6662,6 +6944,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6662
6944
|
let studioCwd = process.cwd();
|
|
6663
6945
|
let lastCommandCtx: ExtensionCommandContext | null = null;
|
|
6664
6946
|
let lastThemeVarsJson = "";
|
|
6947
|
+
let suppressedStudioResponse: { requestId: string; kind: StudioRequestKind } | null = null;
|
|
6665
6948
|
let agentBusy = false;
|
|
6666
6949
|
let terminalActivityPhase: TerminalActivityPhase = "idle";
|
|
6667
6950
|
let terminalActivityToolName: string | null = null;
|
|
@@ -6916,7 +7199,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
6916
7199
|
}
|
|
6917
7200
|
};
|
|
6918
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
|
+
|
|
6919
7229
|
const beginRequest = (requestId: string, kind: StudioRequestKind): boolean => {
|
|
7230
|
+
suppressedStudioResponse = null;
|
|
6920
7231
|
emitDebugEvent("begin_request_attempt", {
|
|
6921
7232
|
requestId,
|
|
6922
7233
|
kind,
|
|
@@ -7024,6 +7335,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
7024
7335
|
return;
|
|
7025
7336
|
}
|
|
7026
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
|
+
|
|
7027
7351
|
if (msg.type === "critique_request") {
|
|
7028
7352
|
if (!isValidRequestId(msg.requestId)) {
|
|
7029
7353
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
@@ -7856,6 +8180,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
7856
8180
|
|
|
7857
8181
|
if (!markdown) return;
|
|
7858
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
|
+
|
|
7859
8193
|
syncStudioResponseHistory(ctx.sessionManager.getBranch());
|
|
7860
8194
|
refreshContextUsage(ctx);
|
|
7861
8195
|
const latestHistoryItem = studioResponseHistory[studioResponseHistory.length - 1];
|
|
@@ -7936,7 +8270,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
7936
8270
|
pi.on("agent_end", async () => {
|
|
7937
8271
|
agentBusy = false;
|
|
7938
8272
|
refreshContextUsage();
|
|
7939
|
-
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
|
+
});
|
|
7940
8279
|
setTerminalActivity("idle");
|
|
7941
8280
|
if (activeRequest) {
|
|
7942
8281
|
const requestId = activeRequest.id;
|
|
@@ -7947,6 +8286,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7947
8286
|
});
|
|
7948
8287
|
clearActiveRequest();
|
|
7949
8288
|
}
|
|
8289
|
+
suppressedStudioResponse = null;
|
|
7950
8290
|
});
|
|
7951
8291
|
|
|
7952
8292
|
pi.on("session_shutdown", async () => {
|