pi-studio 0.4.2 → 0.5.0
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 +27 -3
- package/README.md +36 -90
- package/WORKFLOW.md +6 -5
- package/index.ts +2019 -122
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -2,14 +2,16 @@ import type { ExtensionAPI, ExtensionCommandContext, SessionEntry, Theme } from
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
6
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
6
8
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
9
|
import { URL } from "node:url";
|
|
8
10
|
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
|
9
11
|
|
|
10
12
|
type Lens = "writing" | "code";
|
|
11
13
|
type RequestedLens = Lens | "auto";
|
|
12
|
-
type StudioRequestKind = "critique" | "annotation" | "direct";
|
|
14
|
+
type StudioRequestKind = "critique" | "annotation" | "direct" | "compact";
|
|
13
15
|
type StudioSourceKind = "file" | "last-response" | "blank";
|
|
14
16
|
type TerminalActivityPhase = "idle" | "running" | "tool" | "responding";
|
|
15
17
|
|
|
@@ -34,6 +36,20 @@ interface LastStudioResponse {
|
|
|
34
36
|
kind: StudioRequestKind;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
interface StudioResponseHistoryItem {
|
|
40
|
+
id: string;
|
|
41
|
+
markdown: string;
|
|
42
|
+
timestamp: number;
|
|
43
|
+
kind: StudioRequestKind;
|
|
44
|
+
prompt: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface StudioContextUsageSnapshot {
|
|
48
|
+
tokens: number | null;
|
|
49
|
+
contextWindow: number | null;
|
|
50
|
+
percent: number | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
interface InitialStudioDocument {
|
|
38
54
|
text: string;
|
|
39
55
|
label: string;
|
|
@@ -72,6 +88,12 @@ interface SendRunRequestMessage {
|
|
|
72
88
|
text: string;
|
|
73
89
|
}
|
|
74
90
|
|
|
91
|
+
interface CompactRequestMessage {
|
|
92
|
+
type: "compact_request";
|
|
93
|
+
requestId: string;
|
|
94
|
+
customInstructions?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
interface SaveAsRequestMessage {
|
|
76
98
|
type: "save_as_request";
|
|
77
99
|
requestId: string;
|
|
@@ -103,6 +125,7 @@ type IncomingStudioMessage =
|
|
|
103
125
|
| CritiqueRequestMessage
|
|
104
126
|
| AnnotationRequestMessage
|
|
105
127
|
| SendRunRequestMessage
|
|
128
|
+
| CompactRequestMessage
|
|
106
129
|
| SaveAsRequestMessage
|
|
107
130
|
| SaveOverRequestMessage
|
|
108
131
|
| SendToEditorRequestMessage
|
|
@@ -110,7 +133,22 @@ type IncomingStudioMessage =
|
|
|
110
133
|
|
|
111
134
|
const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
112
135
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
136
|
+
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
113
137
|
const REQUEST_BODY_MAX_BYTES = 1_000_000;
|
|
138
|
+
const RESPONSE_HISTORY_LIMIT = 30;
|
|
139
|
+
const UPDATE_CHECK_TIMEOUT_MS = 1800;
|
|
140
|
+
|
|
141
|
+
const PDF_PREAMBLE = `\\usepackage{titlesec}
|
|
142
|
+
\\titleformat{\\section}{\\Large\\bfseries\\sffamily}{}{0pt}{}[\\vspace{2pt}\\titlerule]
|
|
143
|
+
\\titleformat{\\subsection}{\\large\\bfseries\\sffamily}{}{0pt}{}
|
|
144
|
+
\\titleformat{\\subsubsection}{\\normalsize\\bfseries\\sffamily}{}{0pt}{}
|
|
145
|
+
\\titlespacing*{\\section}{0pt}{1.5ex plus 0.5ex minus 0.2ex}{1ex plus 0.2ex}
|
|
146
|
+
\\titlespacing*{\\subsection}{0pt}{1.2ex plus 0.4ex minus 0.2ex}{0.6ex plus 0.1ex}
|
|
147
|
+
\\usepackage{enumitem}
|
|
148
|
+
\\setlist[itemize]{nosep, leftmargin=1.5em}
|
|
149
|
+
\\setlist[enumerate]{nosep, leftmargin=1.5em}
|
|
150
|
+
\\usepackage{parskip}
|
|
151
|
+
`;
|
|
114
152
|
|
|
115
153
|
type StudioThemeMode = "dark" | "light";
|
|
116
154
|
|
|
@@ -659,6 +697,93 @@ function writeStudioFile(pathArg: string, cwd: string, content: string):
|
|
|
659
697
|
}
|
|
660
698
|
}
|
|
661
699
|
|
|
700
|
+
function readLocalPackageMetadata(): { name: string; version: string } | null {
|
|
701
|
+
try {
|
|
702
|
+
const raw = readFileSync(new URL("./package.json", import.meta.url), "utf-8");
|
|
703
|
+
const parsed = JSON.parse(raw) as { name?: unknown; version?: unknown };
|
|
704
|
+
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
705
|
+
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
706
|
+
if (!name || !version) return null;
|
|
707
|
+
return { name, version };
|
|
708
|
+
} catch {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
interface ParsedSemver {
|
|
714
|
+
major: number;
|
|
715
|
+
minor: number;
|
|
716
|
+
patch: number;
|
|
717
|
+
prerelease: string | null;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function parseSemverLoose(version: string): ParsedSemver | null {
|
|
721
|
+
const normalized = String(version || "").trim().replace(/^v/i, "");
|
|
722
|
+
const match = normalized.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/);
|
|
723
|
+
if (!match) return null;
|
|
724
|
+
const major = Number.parseInt(match[1] ?? "", 10);
|
|
725
|
+
const minor = Number.parseInt(match[2] ?? "0", 10);
|
|
726
|
+
const patch = Number.parseInt(match[3] ?? "0", 10);
|
|
727
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) return null;
|
|
728
|
+
const prerelease = typeof match[4] === "string" && match[4].trim() ? match[4].trim() : null;
|
|
729
|
+
return { major, minor, patch, prerelease };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function compareSemverLoose(a: string, b: string): number {
|
|
733
|
+
const pa = parseSemverLoose(a);
|
|
734
|
+
const pb = parseSemverLoose(b);
|
|
735
|
+
if (!pa || !pb) {
|
|
736
|
+
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
|
|
737
|
+
}
|
|
738
|
+
if (pa.major !== pb.major) return pa.major - pb.major;
|
|
739
|
+
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
|
|
740
|
+
if (pa.patch !== pb.patch) return pa.patch - pb.patch;
|
|
741
|
+
if (pa.prerelease && !pb.prerelease) return -1;
|
|
742
|
+
if (!pa.prerelease && pb.prerelease) return 1;
|
|
743
|
+
if (!pa.prerelease && !pb.prerelease) return 0;
|
|
744
|
+
return (pa.prerelease ?? "").localeCompare(pb.prerelease ?? "", undefined, {
|
|
745
|
+
numeric: true,
|
|
746
|
+
sensitivity: "base",
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function isVersionBehind(installedVersion: string, latestVersion: string): boolean {
|
|
751
|
+
return compareSemverLoose(installedVersion, latestVersion) < 0;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function fetchLatestNpmVersion(packageName: string, timeoutMs = UPDATE_CHECK_TIMEOUT_MS): Promise<string | null> {
|
|
755
|
+
const pkg = String(packageName || "").trim();
|
|
756
|
+
if (!pkg) return null;
|
|
757
|
+
const encodedPackage = encodeURIComponent(pkg).replace(/^%40/, "@");
|
|
758
|
+
const endpoint = `https://registry.npmjs.org/${encodedPackage}/latest`;
|
|
759
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
760
|
+
const timer = controller
|
|
761
|
+
? setTimeout(() => {
|
|
762
|
+
try {
|
|
763
|
+
controller.abort();
|
|
764
|
+
} catch {
|
|
765
|
+
// ignore abort race
|
|
766
|
+
}
|
|
767
|
+
}, timeoutMs)
|
|
768
|
+
: null;
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
const response = await fetch(endpoint, {
|
|
772
|
+
method: "GET",
|
|
773
|
+
headers: { Accept: "application/json" },
|
|
774
|
+
signal: controller?.signal,
|
|
775
|
+
});
|
|
776
|
+
if (!response.ok) return null;
|
|
777
|
+
const payload = await response.json() as { version?: unknown };
|
|
778
|
+
const version = typeof payload.version === "string" ? payload.version.trim() : "";
|
|
779
|
+
return version || null;
|
|
780
|
+
} catch {
|
|
781
|
+
return null;
|
|
782
|
+
} finally {
|
|
783
|
+
if (timer) clearTimeout(timer);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
662
787
|
function normalizeMathDelimitersInSegment(markdown: string): string {
|
|
663
788
|
let normalized = markdown.replace(/\\\[\s*([\s\S]*?)\s*\\\]/g, (_match, expr: string) => {
|
|
664
789
|
const content = expr.trim();
|
|
@@ -800,6 +925,85 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
800
925
|
});
|
|
801
926
|
}
|
|
802
927
|
|
|
928
|
+
async function renderStudioPdfWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string): Promise<Buffer> {
|
|
929
|
+
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
930
|
+
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
931
|
+
const inputFormat = isLatex
|
|
932
|
+
? "latex"
|
|
933
|
+
: "gfm+tex_math_dollars+autolink_bare_uris+superscript+subscript-raw_html";
|
|
934
|
+
const normalizedMarkdown = isLatex ? markdown : normalizeObsidianImages(normalizeMathDelimiters(markdown));
|
|
935
|
+
|
|
936
|
+
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
937
|
+
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
938
|
+
const outputPath = join(tempDir, "studio-export.pdf");
|
|
939
|
+
|
|
940
|
+
await mkdir(tempDir, { recursive: true });
|
|
941
|
+
await writeFile(preamblePath, PDF_PREAMBLE, "utf-8");
|
|
942
|
+
|
|
943
|
+
const args = [
|
|
944
|
+
"-f", inputFormat,
|
|
945
|
+
"-o", outputPath,
|
|
946
|
+
`--pdf-engine=${pdfEngine}`,
|
|
947
|
+
"-V", "geometry:margin=2.2cm",
|
|
948
|
+
"-V", "fontsize=11pt",
|
|
949
|
+
"-V", "linestretch=1.25",
|
|
950
|
+
"-V", "urlcolor=blue",
|
|
951
|
+
"-V", "linkcolor=blue",
|
|
952
|
+
"--include-in-header", preamblePath,
|
|
953
|
+
];
|
|
954
|
+
if (resourcePath) args.push(`--resource-path=${resourcePath}`);
|
|
955
|
+
|
|
956
|
+
try {
|
|
957
|
+
await new Promise<void>((resolve, reject) => {
|
|
958
|
+
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
959
|
+
const stderrChunks: Buffer[] = [];
|
|
960
|
+
let settled = false;
|
|
961
|
+
|
|
962
|
+
const fail = (error: Error) => {
|
|
963
|
+
if (settled) return;
|
|
964
|
+
settled = true;
|
|
965
|
+
reject(error);
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
969
|
+
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
child.once("error", (error) => {
|
|
973
|
+
const errno = error as NodeJS.ErrnoException;
|
|
974
|
+
if (errno.code === "ENOENT") {
|
|
975
|
+
const commandHint = pandocCommand === "pandoc"
|
|
976
|
+
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
977
|
+
: `${pandocCommand} was not found. Check PANDOC_PATH.`;
|
|
978
|
+
fail(new Error(commandHint));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
fail(error);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
child.once("close", (code) => {
|
|
985
|
+
if (settled) return;
|
|
986
|
+
if (code === 0) {
|
|
987
|
+
settled = true;
|
|
988
|
+
resolve();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
992
|
+
const hint = stderr.includes("not found") || stderr.includes("xelatex") || stderr.includes("pdflatex")
|
|
993
|
+
? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
|
|
994
|
+
: "";
|
|
995
|
+
fail(new Error(`pandoc PDF export failed with exit code ${code}${stderr ? `: ${stderr}` : ""}${hint}`));
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
child.stdin.end(normalizedMarkdown);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
return await readFile(outputPath);
|
|
1002
|
+
} finally {
|
|
1003
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
803
1007
|
function readRequestBody(req: IncomingMessage, maxBytes: number): Promise<string> {
|
|
804
1008
|
return new Promise((resolve, reject) => {
|
|
805
1009
|
const chunks: Buffer[] = [];
|
|
@@ -1065,6 +1269,116 @@ function extractLatestAssistantFromEntries(entries: SessionEntry[]): string | nu
|
|
|
1065
1269
|
return null;
|
|
1066
1270
|
}
|
|
1067
1271
|
|
|
1272
|
+
function extractUserText(message: unknown): string | null {
|
|
1273
|
+
const msg = message as {
|
|
1274
|
+
role?: string;
|
|
1275
|
+
content?: Array<{ type?: string; text?: string | { value?: string } }> | string;
|
|
1276
|
+
};
|
|
1277
|
+
if (!msg || msg.role !== "user") return null;
|
|
1278
|
+
|
|
1279
|
+
if (typeof msg.content === "string") {
|
|
1280
|
+
const text = msg.content.trim();
|
|
1281
|
+
return text.length > 0 ? text : null;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (!Array.isArray(msg.content)) return null;
|
|
1285
|
+
|
|
1286
|
+
const blocks: string[] = [];
|
|
1287
|
+
for (const part of msg.content) {
|
|
1288
|
+
if (!part || typeof part !== "object") continue;
|
|
1289
|
+
const partType = typeof part.type === "string" ? part.type : "";
|
|
1290
|
+
if (typeof part.text === "string") {
|
|
1291
|
+
if (!partType || partType === "text" || partType === "input_text") {
|
|
1292
|
+
blocks.push(part.text);
|
|
1293
|
+
}
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (part.text && typeof part.text === "object" && typeof part.text.value === "string") {
|
|
1297
|
+
if (!partType || partType === "text" || partType === "input_text") {
|
|
1298
|
+
blocks.push(part.text.value);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const text = blocks.join("\n\n").trim();
|
|
1304
|
+
return text.length > 0 ? text : null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function parseEntryTimestamp(timestamp: unknown): number {
|
|
1308
|
+
if (typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0) {
|
|
1309
|
+
return timestamp;
|
|
1310
|
+
}
|
|
1311
|
+
if (typeof timestamp === "string" && timestamp.trim()) {
|
|
1312
|
+
const parsed = Date.parse(timestamp);
|
|
1313
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
1314
|
+
}
|
|
1315
|
+
return Date.now();
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function buildResponseHistoryFromEntries(entries: SessionEntry[], limit = RESPONSE_HISTORY_LIMIT): StudioResponseHistoryItem[] {
|
|
1319
|
+
const history: StudioResponseHistoryItem[] = [];
|
|
1320
|
+
let lastUserPrompt: string | null = null;
|
|
1321
|
+
|
|
1322
|
+
for (const entry of entries) {
|
|
1323
|
+
if (!entry || entry.type !== "message") continue;
|
|
1324
|
+
const message = (entry as { message?: unknown }).message;
|
|
1325
|
+
const role = (message as { role?: string } | undefined)?.role;
|
|
1326
|
+
if (role === "user") {
|
|
1327
|
+
lastUserPrompt = extractUserText(message);
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (role !== "assistant") continue;
|
|
1331
|
+
const markdown = extractAssistantText(message);
|
|
1332
|
+
if (!markdown) continue;
|
|
1333
|
+
history.push({
|
|
1334
|
+
id: typeof (entry as { id?: unknown }).id === "string" ? (entry as { id: string }).id : randomUUID(),
|
|
1335
|
+
markdown,
|
|
1336
|
+
timestamp: parseEntryTimestamp((entry as { timestamp?: unknown }).timestamp),
|
|
1337
|
+
kind: inferStudioResponseKind(markdown),
|
|
1338
|
+
prompt: lastUserPrompt,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (history.length <= limit) return history;
|
|
1343
|
+
return history.slice(-limit);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function normalizeContextUsageSnapshot(usage: { tokens: number | null; contextWindow: number; percent: number | null } | undefined): StudioContextUsageSnapshot {
|
|
1347
|
+
if (!usage) {
|
|
1348
|
+
return {
|
|
1349
|
+
tokens: null,
|
|
1350
|
+
contextWindow: null,
|
|
1351
|
+
percent: null,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const contextWindow =
|
|
1356
|
+
typeof usage.contextWindow === "number" && Number.isFinite(usage.contextWindow) && usage.contextWindow > 0
|
|
1357
|
+
? usage.contextWindow
|
|
1358
|
+
: null;
|
|
1359
|
+
const tokens = typeof usage.tokens === "number" && Number.isFinite(usage.tokens) && usage.tokens >= 0
|
|
1360
|
+
? usage.tokens
|
|
1361
|
+
: null;
|
|
1362
|
+
|
|
1363
|
+
let percent = typeof usage.percent === "number" && Number.isFinite(usage.percent)
|
|
1364
|
+
? usage.percent
|
|
1365
|
+
: null;
|
|
1366
|
+
if (percent === null && tokens !== null && contextWindow) {
|
|
1367
|
+
percent = (tokens / contextWindow) * 100;
|
|
1368
|
+
}
|
|
1369
|
+
if (typeof percent === "number" && Number.isFinite(percent)) {
|
|
1370
|
+
percent = Math.max(0, Math.min(100, percent));
|
|
1371
|
+
} else {
|
|
1372
|
+
percent = null;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
return {
|
|
1376
|
+
tokens,
|
|
1377
|
+
contextWindow,
|
|
1378
|
+
percent,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1068
1382
|
function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
1069
1383
|
let parsed: unknown;
|
|
1070
1384
|
try {
|
|
@@ -1110,6 +1424,18 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
1110
1424
|
};
|
|
1111
1425
|
}
|
|
1112
1426
|
|
|
1427
|
+
if (
|
|
1428
|
+
msg.type === "compact_request" &&
|
|
1429
|
+
typeof msg.requestId === "string" &&
|
|
1430
|
+
(msg.customInstructions === undefined || typeof msg.customInstructions === "string")
|
|
1431
|
+
) {
|
|
1432
|
+
return {
|
|
1433
|
+
type: "compact_request",
|
|
1434
|
+
requestId: msg.requestId,
|
|
1435
|
+
customInstructions: typeof msg.customInstructions === "string" ? msg.customInstructions : undefined,
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1113
1439
|
if (
|
|
1114
1440
|
msg.type === "save_as_request" &&
|
|
1115
1441
|
typeof msg.requestId === "string" &&
|
|
@@ -1292,6 +1618,50 @@ function buildStudioUrl(port: number, token: string): string {
|
|
|
1292
1618
|
return `http://127.0.0.1:${port}/?token=${encoded}`;
|
|
1293
1619
|
}
|
|
1294
1620
|
|
|
1621
|
+
function formatModelLabel(model: { provider?: string; id?: string } | undefined): string {
|
|
1622
|
+
const provider = typeof model?.provider === "string" ? model.provider.trim() : "";
|
|
1623
|
+
const id = typeof model?.id === "string" ? model.id.trim() : "";
|
|
1624
|
+
if (provider && id) return `${provider}/${id}`;
|
|
1625
|
+
if (id) return id;
|
|
1626
|
+
return "none";
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function formatModelLabelWithThinking(modelLabel: string, thinkingLevel?: string): string {
|
|
1630
|
+
const base = String(modelLabel || "").replace(/\s*\([^)]*\)\s*$/, "").trim() || "none";
|
|
1631
|
+
if (base === "none") return "none";
|
|
1632
|
+
const level = String(thinkingLevel ?? "").trim();
|
|
1633
|
+
if (!level) return base;
|
|
1634
|
+
return `${base} (${level})`;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function buildTerminalSessionLabel(cwd: string, sessionName?: string): string {
|
|
1638
|
+
const cwdBase = basename(cwd || process.cwd() || "") || cwd || "~";
|
|
1639
|
+
const termProgram = String(process.env.TERM_PROGRAM ?? "").trim();
|
|
1640
|
+
const name = String(sessionName ?? "").trim();
|
|
1641
|
+
const parts: string[] = [];
|
|
1642
|
+
if (termProgram) parts.push(termProgram);
|
|
1643
|
+
if (name) parts.push(name);
|
|
1644
|
+
parts.push(cwdBase);
|
|
1645
|
+
return parts.join(" · ");
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function sanitizePdfFilename(input: string | undefined): string {
|
|
1649
|
+
const fallback = "studio-preview.pdf";
|
|
1650
|
+
const raw = String(input ?? "").trim();
|
|
1651
|
+
if (!raw) return fallback;
|
|
1652
|
+
|
|
1653
|
+
const noPath = raw.split(/[\\/]/).pop() ?? raw;
|
|
1654
|
+
const cleaned = noPath
|
|
1655
|
+
.replace(/[\x00-\x1f\x7f]+/g, "")
|
|
1656
|
+
.replace(/[<>:"|?*]+/g, "-")
|
|
1657
|
+
.trim();
|
|
1658
|
+
if (!cleaned) return fallback;
|
|
1659
|
+
|
|
1660
|
+
const ensuredExt = cleaned.toLowerCase().endsWith(".pdf") ? cleaned : `${cleaned}.pdf`;
|
|
1661
|
+
if (ensuredExt.length <= 160) return ensuredExt;
|
|
1662
|
+
return `${ensuredExt.slice(0, 156)}.pdf`;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1295
1665
|
function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
|
|
1296
1666
|
const panelShadow =
|
|
1297
1667
|
style.mode === "light"
|
|
@@ -1358,11 +1728,31 @@ function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
|
|
|
1358
1728
|
};
|
|
1359
1729
|
}
|
|
1360
1730
|
|
|
1361
|
-
function buildStudioHtml(
|
|
1731
|
+
function buildStudioHtml(
|
|
1732
|
+
initialDocument: InitialStudioDocument | null,
|
|
1733
|
+
theme?: Theme,
|
|
1734
|
+
initialModelLabel?: string,
|
|
1735
|
+
initialTerminalLabel?: string,
|
|
1736
|
+
initialContextUsage?: StudioContextUsageSnapshot,
|
|
1737
|
+
): string {
|
|
1362
1738
|
const initialText = escapeHtmlForInline(initialDocument?.text ?? "");
|
|
1363
1739
|
const initialSource = initialDocument?.source ?? "blank";
|
|
1364
1740
|
const initialLabel = escapeHtmlForInline(initialDocument?.label ?? "blank");
|
|
1365
1741
|
const initialPath = escapeHtmlForInline(initialDocument?.path ?? "");
|
|
1742
|
+
const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
|
|
1743
|
+
const initialTerminal = escapeHtmlForInline(initialTerminalLabel ?? "unknown");
|
|
1744
|
+
const initialContextTokens =
|
|
1745
|
+
typeof initialContextUsage?.tokens === "number" && Number.isFinite(initialContextUsage.tokens)
|
|
1746
|
+
? String(initialContextUsage.tokens)
|
|
1747
|
+
: "";
|
|
1748
|
+
const initialContextWindow =
|
|
1749
|
+
typeof initialContextUsage?.contextWindow === "number" && Number.isFinite(initialContextUsage.contextWindow)
|
|
1750
|
+
? String(initialContextUsage.contextWindow)
|
|
1751
|
+
: "";
|
|
1752
|
+
const initialContextPercent =
|
|
1753
|
+
typeof initialContextUsage?.percent === "number" && Number.isFinite(initialContextUsage.percent)
|
|
1754
|
+
? String(initialContextUsage.percent)
|
|
1755
|
+
: "";
|
|
1366
1756
|
const style = getStudioThemeStyle(theme);
|
|
1367
1757
|
const vars = buildThemeCssVars(style);
|
|
1368
1758
|
const mermaidConfig = {
|
|
@@ -1399,7 +1789,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1399
1789
|
<head>
|
|
1400
1790
|
<meta charset="utf-8" />
|
|
1401
1791
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
1402
|
-
<title>
|
|
1792
|
+
<title>pi Studio</title>
|
|
1403
1793
|
<style>
|
|
1404
1794
|
:root {
|
|
1405
1795
|
${cssVarsBlock}
|
|
@@ -1593,6 +1983,7 @@ ${cssVarsBlock}
|
|
|
1593
1983
|
padding: 10px;
|
|
1594
1984
|
font-size: 13px;
|
|
1595
1985
|
line-height: 1.45;
|
|
1986
|
+
tab-size: 2;
|
|
1596
1987
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1597
1988
|
resize: vertical;
|
|
1598
1989
|
}
|
|
@@ -1731,6 +2122,7 @@ ${cssVarsBlock}
|
|
|
1731
2122
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1732
2123
|
font-size: 13px;
|
|
1733
2124
|
line-height: 1.45;
|
|
2125
|
+
tab-size: 2;
|
|
1734
2126
|
color: var(--text);
|
|
1735
2127
|
background: transparent;
|
|
1736
2128
|
}
|
|
@@ -1830,6 +2222,27 @@ ${cssVarsBlock}
|
|
|
1830
2222
|
color: var(--md-link-url);
|
|
1831
2223
|
}
|
|
1832
2224
|
|
|
2225
|
+
.hl-annotation {
|
|
2226
|
+
color: var(--accent);
|
|
2227
|
+
background: var(--accent-soft);
|
|
2228
|
+
border: 1px solid var(--marker-border);
|
|
2229
|
+
border-radius: 4px;
|
|
2230
|
+
padding: 0 3px;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
.hl-annotation-muted {
|
|
2234
|
+
color: var(--muted);
|
|
2235
|
+
opacity: 0.65;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
.annotation-preview-marker {
|
|
2239
|
+
color: var(--accent);
|
|
2240
|
+
background: var(--accent-soft);
|
|
2241
|
+
border: 1px solid var(--marker-border);
|
|
2242
|
+
border-radius: 4px;
|
|
2243
|
+
padding: 0 4px;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
1833
2246
|
#sourcePreview {
|
|
1834
2247
|
flex: 1 1 auto;
|
|
1835
2248
|
min-height: 0;
|
|
@@ -2186,28 +2599,133 @@ ${cssVarsBlock}
|
|
|
2186
2599
|
font-size: 12px;
|
|
2187
2600
|
min-height: 32px;
|
|
2188
2601
|
background: var(--panel);
|
|
2189
|
-
display:
|
|
2602
|
+
display: grid;
|
|
2603
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
2604
|
+
grid-template-areas:
|
|
2605
|
+
"status status"
|
|
2606
|
+
"meta hint";
|
|
2607
|
+
column-gap: 12px;
|
|
2608
|
+
row-gap: 3px;
|
|
2609
|
+
align-items: start;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
#statusLine {
|
|
2613
|
+
grid-area: status;
|
|
2614
|
+
display: inline-flex;
|
|
2190
2615
|
align-items: center;
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2616
|
+
gap: 0;
|
|
2617
|
+
min-width: 0;
|
|
2618
|
+
justify-self: start;
|
|
2619
|
+
text-align: left;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
#statusLine.with-spinner {
|
|
2623
|
+
gap: 6px;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
#statusSpinner {
|
|
2627
|
+
width: 0;
|
|
2628
|
+
max-width: 0;
|
|
2629
|
+
overflow: hidden;
|
|
2630
|
+
opacity: 0;
|
|
2631
|
+
text-align: center;
|
|
2632
|
+
color: var(--accent);
|
|
2633
|
+
font-family: var(--font-mono);
|
|
2634
|
+
flex: 0 0 auto;
|
|
2635
|
+
transition: opacity 120ms ease;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
#statusLine.with-spinner #statusSpinner {
|
|
2639
|
+
width: 1.1em;
|
|
2640
|
+
max-width: 1.1em;
|
|
2641
|
+
opacity: 1;
|
|
2194
2642
|
}
|
|
2195
2643
|
|
|
2196
2644
|
#status {
|
|
2197
|
-
|
|
2198
|
-
|
|
2645
|
+
min-width: 0;
|
|
2646
|
+
white-space: nowrap;
|
|
2647
|
+
overflow: hidden;
|
|
2648
|
+
text-overflow: ellipsis;
|
|
2649
|
+
text-align: left;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
.footer-meta {
|
|
2653
|
+
grid-area: meta;
|
|
2654
|
+
justify-self: start;
|
|
2655
|
+
color: var(--muted);
|
|
2656
|
+
font-size: 11px;
|
|
2657
|
+
text-align: left;
|
|
2658
|
+
max-width: 100%;
|
|
2659
|
+
display: inline-flex;
|
|
2660
|
+
align-items: center;
|
|
2661
|
+
gap: 8px;
|
|
2662
|
+
min-width: 0;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
.footer-meta-text {
|
|
2666
|
+
min-width: 0;
|
|
2667
|
+
white-space: nowrap;
|
|
2668
|
+
overflow: hidden;
|
|
2669
|
+
text-overflow: ellipsis;
|
|
2199
2670
|
}
|
|
2200
2671
|
|
|
2201
2672
|
.shortcut-hint {
|
|
2673
|
+
grid-area: hint;
|
|
2674
|
+
justify-self: end;
|
|
2675
|
+
align-self: center;
|
|
2202
2676
|
color: var(--muted);
|
|
2203
2677
|
font-size: 11px;
|
|
2204
2678
|
white-space: nowrap;
|
|
2679
|
+
text-align: right;
|
|
2205
2680
|
font-style: normal;
|
|
2681
|
+
opacity: 0.9;
|
|
2682
|
+
display: inline-flex;
|
|
2683
|
+
align-items: center;
|
|
2684
|
+
gap: 8px;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
.footer-compact-btn {
|
|
2688
|
+
padding: 4px 8px;
|
|
2689
|
+
font-size: 11px;
|
|
2690
|
+
line-height: 1.2;
|
|
2691
|
+
border-radius: 999px;
|
|
2692
|
+
border: 1px solid var(--border-muted);
|
|
2693
|
+
background: var(--panel-2);
|
|
2694
|
+
color: var(--text);
|
|
2695
|
+
white-space: nowrap;
|
|
2696
|
+
flex: 0 0 auto;
|
|
2206
2697
|
}
|
|
2207
2698
|
|
|
2208
|
-
footer
|
|
2209
|
-
|
|
2210
|
-
|
|
2699
|
+
.footer-compact-btn:not(:disabled):hover {
|
|
2700
|
+
background: var(--panel);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
#status.error { color: var(--error); }
|
|
2704
|
+
#status.warning { color: var(--warn); }
|
|
2705
|
+
#status.success { color: var(--ok); }
|
|
2706
|
+
|
|
2707
|
+
@media (max-width: 980px) {
|
|
2708
|
+
footer {
|
|
2709
|
+
grid-template-columns: 1fr;
|
|
2710
|
+
grid-template-areas:
|
|
2711
|
+
"status"
|
|
2712
|
+
"meta"
|
|
2713
|
+
"hint";
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
.footer-meta {
|
|
2717
|
+
justify-self: start;
|
|
2718
|
+
max-width: 100%;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
.shortcut-hint {
|
|
2722
|
+
justify-self: start;
|
|
2723
|
+
text-align: left;
|
|
2724
|
+
white-space: normal;
|
|
2725
|
+
flex-wrap: wrap;
|
|
2726
|
+
gap: 6px;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2211
2729
|
|
|
2212
2730
|
@media (max-width: 1080px) {
|
|
2213
2731
|
main {
|
|
@@ -2216,9 +2734,9 @@ ${cssVarsBlock}
|
|
|
2216
2734
|
}
|
|
2217
2735
|
</style>
|
|
2218
2736
|
</head>
|
|
2219
|
-
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}">
|
|
2737
|
+
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}">
|
|
2220
2738
|
<header>
|
|
2221
|
-
<h1><span class="app-logo" aria-hidden="true">π</span>
|
|
2739
|
+
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">Editor & Response Workspace</span></h1>
|
|
2222
2740
|
<div class="controls">
|
|
2223
2741
|
<button id="saveAsBtn" type="button" title="Save editor content to a new file path.">Save editor as…</button>
|
|
2224
2742
|
<button id="saveOverBtn" type="button" title="Overwrite current file with editor content." disabled>Save editor</button>
|
|
@@ -2248,7 +2766,11 @@ ${cssVarsBlock}
|
|
|
2248
2766
|
</div>
|
|
2249
2767
|
<div class="source-actions">
|
|
2250
2768
|
<button id="sendRunBtn" type="button" title="Send editor text directly to the model as-is. Shortcut: Cmd/Ctrl+Enter when editor pane is active.">Run editor text</button>
|
|
2251
|
-
<button id="insertHeaderBtn" type="button" title="
|
|
2769
|
+
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (includes source metadata and [an: ...] syntax hint).">Insert annotated reply header</button>
|
|
2770
|
+
<select id="annotationModeSelect" aria-label="Annotation visibility mode" title="On: keep and send [an: ...] markers. Hidden: keep markers in editor, hide in preview, and strip before Run/Critique.">
|
|
2771
|
+
<option value="on" selected>Annotations: On</option>
|
|
2772
|
+
<option value="off">Annotations: Hidden</option>
|
|
2773
|
+
</select>
|
|
2252
2774
|
<select id="lensSelect" aria-label="Critique focus">
|
|
2253
2775
|
<option value="auto" selected>Critique focus: Auto</option>
|
|
2254
2776
|
<option value="writing">Critique focus: Writing</option>
|
|
@@ -2258,6 +2780,8 @@ ${cssVarsBlock}
|
|
|
2258
2780
|
<button id="sendEditorBtn" type="button">Send to pi editor</button>
|
|
2259
2781
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
2260
2782
|
<button id="copyDraftBtn" type="button">Copy editor text</button>
|
|
2783
|
+
<button id="saveAnnotatedBtn" type="button" title="Save full editor content (including [an: ...] markers) as a .annotated.md file.">Save .annotated.md</button>
|
|
2784
|
+
<button id="stripAnnotationsBtn" type="button" title="Destructively remove all [an: ...] markers from editor text.">Strip annotations…</button>
|
|
2261
2785
|
<select id="highlightSelect" aria-label="Editor syntax highlighting">
|
|
2262
2786
|
<option value="off">Syntax highlight: Off</option>
|
|
2263
2787
|
<option value="on" selected>Syntax highlight: On</option>
|
|
@@ -2323,17 +2847,23 @@ ${cssVarsBlock}
|
|
|
2323
2847
|
<option value="on" selected>Syntax highlight: On</option>
|
|
2324
2848
|
</select>
|
|
2325
2849
|
<button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
|
|
2850
|
+
<button id="historyPrevBtn" type="button" title="Show previous response in history.">◀ Prev response</button>
|
|
2851
|
+
<span id="historyIndexBadge" class="source-badge">History: 0/0</span>
|
|
2852
|
+
<button id="historyNextBtn" type="button" title="Show next response in history.">Next response ▶</button>
|
|
2853
|
+
<button id="loadHistoryPromptBtn" type="button" title="Load the prompt that generated the selected response into the editor.">Load response prompt into editor</button>
|
|
2326
2854
|
<button id="loadResponseBtn" type="button">Load response into editor</button>
|
|
2327
2855
|
<button id="loadCritiqueNotesBtn" type="button" hidden>Load critique notes into editor</button>
|
|
2328
2856
|
<button id="loadCritiqueFullBtn" type="button" hidden>Load full critique into editor</button>
|
|
2329
2857
|
<button id="copyResponseBtn" type="button">Copy response text</button>
|
|
2858
|
+
<button id="exportPdfBtn" type="button" title="Export the current right-pane preview as PDF via pandoc + xelatex.">Export right preview as PDF</button>
|
|
2330
2859
|
</div>
|
|
2331
2860
|
</div>
|
|
2332
2861
|
</section>
|
|
2333
2862
|
</main>
|
|
2334
2863
|
|
|
2335
2864
|
<footer>
|
|
2336
|
-
<span id="status">Booting studio…</span>
|
|
2865
|
+
<span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
|
|
2866
|
+
<span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text">Model: ${initialModel} · Terminal: ${initialTerminal} · Context: unknown</span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact context</button></span>
|
|
2337
2867
|
<span class="shortcut-hint">Focus pane: Cmd/Ctrl+Esc (or F10), Esc to exit · Run editor text: Cmd/Ctrl+Enter</span>
|
|
2338
2868
|
</footer>
|
|
2339
2869
|
|
|
@@ -2341,15 +2871,32 @@ ${cssVarsBlock}
|
|
|
2341
2871
|
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
|
|
2342
2872
|
<script>
|
|
2343
2873
|
(() => {
|
|
2874
|
+
const statusLineEl = document.getElementById("statusLine");
|
|
2344
2875
|
const statusEl = document.getElementById("status");
|
|
2876
|
+
const statusSpinnerEl = document.getElementById("statusSpinner");
|
|
2877
|
+
const footerMetaEl = document.getElementById("footerMeta");
|
|
2878
|
+
const footerMetaTextEl = document.getElementById("footerMetaText");
|
|
2879
|
+
const BRAILLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2880
|
+
let spinnerTimer = null;
|
|
2881
|
+
let spinnerFrameIndex = 0;
|
|
2345
2882
|
if (statusEl) {
|
|
2346
|
-
statusEl.textContent = "
|
|
2883
|
+
statusEl.textContent = "Connecting · Studio script starting…";
|
|
2347
2884
|
}
|
|
2348
2885
|
|
|
2349
2886
|
function hardFail(prefix, error) {
|
|
2350
2887
|
const details = error && error.message ? error.message : String(error || "unknown error");
|
|
2888
|
+
if (spinnerTimer) {
|
|
2889
|
+
window.clearInterval(spinnerTimer);
|
|
2890
|
+
spinnerTimer = null;
|
|
2891
|
+
}
|
|
2892
|
+
if (statusLineEl && statusLineEl.classList) {
|
|
2893
|
+
statusLineEl.classList.remove("with-spinner");
|
|
2894
|
+
}
|
|
2895
|
+
if (statusSpinnerEl) {
|
|
2896
|
+
statusSpinnerEl.textContent = "";
|
|
2897
|
+
}
|
|
2351
2898
|
if (statusEl) {
|
|
2352
|
-
statusEl.textContent = "
|
|
2899
|
+
statusEl.textContent = "Disconnected · " + prefix + ": " + details;
|
|
2353
2900
|
statusEl.className = "error";
|
|
2354
2901
|
}
|
|
2355
2902
|
}
|
|
@@ -2391,14 +2938,23 @@ ${cssVarsBlock}
|
|
|
2391
2938
|
const loadCritiqueNotesBtn = document.getElementById("loadCritiqueNotesBtn");
|
|
2392
2939
|
const loadCritiqueFullBtn = document.getElementById("loadCritiqueFullBtn");
|
|
2393
2940
|
const copyResponseBtn = document.getElementById("copyResponseBtn");
|
|
2941
|
+
const exportPdfBtn = document.getElementById("exportPdfBtn");
|
|
2942
|
+
const historyPrevBtn = document.getElementById("historyPrevBtn");
|
|
2943
|
+
const historyNextBtn = document.getElementById("historyNextBtn");
|
|
2944
|
+
const historyIndexBadgeEl = document.getElementById("historyIndexBadge");
|
|
2945
|
+
const loadHistoryPromptBtn = document.getElementById("loadHistoryPromptBtn");
|
|
2394
2946
|
const saveAsBtn = document.getElementById("saveAsBtn");
|
|
2395
2947
|
const saveOverBtn = document.getElementById("saveOverBtn");
|
|
2396
2948
|
const sendEditorBtn = document.getElementById("sendEditorBtn");
|
|
2397
2949
|
const getEditorBtn = document.getElementById("getEditorBtn");
|
|
2398
2950
|
const sendRunBtn = document.getElementById("sendRunBtn");
|
|
2399
2951
|
const copyDraftBtn = document.getElementById("copyDraftBtn");
|
|
2952
|
+
const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
|
|
2953
|
+
const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
|
|
2400
2954
|
const highlightSelect = document.getElementById("highlightSelect");
|
|
2401
2955
|
const langSelect = document.getElementById("langSelect");
|
|
2956
|
+
const annotationModeSelect = document.getElementById("annotationModeSelect");
|
|
2957
|
+
const compactBtn = document.getElementById("compactBtn");
|
|
2402
2958
|
|
|
2403
2959
|
const initialSourceState = {
|
|
2404
2960
|
source: (document.body && document.body.dataset && document.body.dataset.initialSource) || "blank",
|
|
@@ -2408,7 +2964,7 @@ ${cssVarsBlock}
|
|
|
2408
2964
|
|
|
2409
2965
|
let ws = null;
|
|
2410
2966
|
let wsState = "Connecting";
|
|
2411
|
-
let statusMessage = "Studio script starting…";
|
|
2967
|
+
let statusMessage = "Connecting · Studio script starting…";
|
|
2412
2968
|
let statusLevel = "";
|
|
2413
2969
|
let pendingRequestId = null;
|
|
2414
2970
|
let pendingKind = null;
|
|
@@ -2426,12 +2982,32 @@ ${cssVarsBlock}
|
|
|
2426
2982
|
let latestResponseNormalized = "";
|
|
2427
2983
|
let latestCritiqueNotes = "";
|
|
2428
2984
|
let latestCritiqueNotesNormalized = "";
|
|
2985
|
+
let responseHistory = [];
|
|
2986
|
+
let responseHistoryIndex = -1;
|
|
2429
2987
|
let agentBusyFromServer = false;
|
|
2430
2988
|
let terminalActivityPhase = "idle";
|
|
2431
2989
|
let terminalActivityToolName = "";
|
|
2432
2990
|
let terminalActivityLabel = "";
|
|
2433
2991
|
let lastSpecificToolLabel = "";
|
|
2434
2992
|
let uiBusy = false;
|
|
2993
|
+
let pdfExportInProgress = false;
|
|
2994
|
+
let compactInProgress = false;
|
|
2995
|
+
let modelLabel = (document.body && document.body.dataset && document.body.dataset.modelLabel) || "none";
|
|
2996
|
+
let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
|
|
2997
|
+
let contextTokens = null;
|
|
2998
|
+
let contextWindow = null;
|
|
2999
|
+
let contextPercent = null;
|
|
3000
|
+
|
|
3001
|
+
function parseFiniteNumber(value) {
|
|
3002
|
+
if (value == null || value === "") return null;
|
|
3003
|
+
const parsed = Number(value);
|
|
3004
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
|
|
3008
|
+
contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
|
|
3009
|
+
contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
|
|
3010
|
+
|
|
2435
3011
|
let sourceState = {
|
|
2436
3012
|
source: initialSourceState.source,
|
|
2437
3013
|
label: initialSourceState.label,
|
|
@@ -2482,6 +3058,7 @@ ${cssVarsBlock}
|
|
|
2482
3058
|
var SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
|
|
2483
3059
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
2484
3060
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
3061
|
+
const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
|
|
2485
3062
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
2486
3063
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
2487
3064
|
const previewPendingTimers = new WeakMap();
|
|
@@ -2494,6 +3071,8 @@ ${cssVarsBlock}
|
|
|
2494
3071
|
let editorLanguage = "markdown";
|
|
2495
3072
|
let responseHighlightEnabled = false;
|
|
2496
3073
|
let editorHighlightRenderRaf = null;
|
|
3074
|
+
let annotationsEnabled = true;
|
|
3075
|
+
const ANNOTATION_MARKER_REGEX = /\\[an:\\s*([^\\]\\n]+?)\\]/gi;
|
|
2497
3076
|
const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
2498
3077
|
const MERMAID_CONFIG = ${JSON.stringify(mermaidConfig)};
|
|
2499
3078
|
const MERMAID_UNAVAILABLE_MESSAGE = "Mermaid renderer unavailable. Showing mermaid blocks as code.";
|
|
@@ -2547,15 +3126,23 @@ ${cssVarsBlock}
|
|
|
2547
3126
|
if (typeof message.terminalPhase === "string") summary.terminalPhase = message.terminalPhase;
|
|
2548
3127
|
if (typeof message.terminalToolName === "string") summary.terminalToolName = message.terminalToolName;
|
|
2549
3128
|
if (typeof message.terminalActivityLabel === "string") summary.terminalActivityLabel = message.terminalActivityLabel;
|
|
3129
|
+
if (typeof message.modelLabel === "string") summary.modelLabel = message.modelLabel;
|
|
3130
|
+
if (typeof message.terminalSessionLabel === "string") summary.terminalSessionLabel = message.terminalSessionLabel;
|
|
3131
|
+
if (typeof message.contextTokens === "number") summary.contextTokens = message.contextTokens;
|
|
3132
|
+
if (typeof message.contextWindow === "number") summary.contextWindow = message.contextWindow;
|
|
3133
|
+
if (typeof message.contextPercent === "number") summary.contextPercent = message.contextPercent;
|
|
3134
|
+
if (typeof message.compactInProgress === "boolean") summary.compactInProgress = message.compactInProgress;
|
|
2550
3135
|
if (typeof message.stopReason === "string") summary.stopReason = message.stopReason;
|
|
2551
3136
|
if (typeof message.markdown === "string") summary.markdownLength = message.markdown.length;
|
|
2552
3137
|
if (typeof message.label === "string") summary.label = message.label;
|
|
3138
|
+
if (Array.isArray(message.responseHistory)) summary.responseHistoryCount = message.responseHistory.length;
|
|
3139
|
+
if (Array.isArray(message.items)) summary.itemsCount = message.items.length;
|
|
2553
3140
|
if (typeof message.details === "object" && message.details !== null) summary.details = message.details;
|
|
2554
3141
|
return summary;
|
|
2555
3142
|
}
|
|
2556
3143
|
|
|
2557
3144
|
function getIdleStatus() {
|
|
2558
|
-
return "
|
|
3145
|
+
return "Edit, load, or annotate text, then run, save, send to pi editor, or critique.";
|
|
2559
3146
|
}
|
|
2560
3147
|
|
|
2561
3148
|
function normalizeTerminalPhase(phase) {
|
|
@@ -2595,6 +3182,8 @@ ${cssVarsBlock}
|
|
|
2595
3182
|
if (terminalActivityPhase === "idle") {
|
|
2596
3183
|
lastSpecificToolLabel = "";
|
|
2597
3184
|
}
|
|
3185
|
+
|
|
3186
|
+
syncFooterSpinnerState();
|
|
2598
3187
|
}
|
|
2599
3188
|
|
|
2600
3189
|
function getTerminalBusyStatus() {
|
|
@@ -2622,6 +3211,7 @@ ${cssVarsBlock}
|
|
|
2622
3211
|
if (kind === "annotation") return "sending annotated reply";
|
|
2623
3212
|
if (kind === "critique") return "running critique";
|
|
2624
3213
|
if (kind === "direct") return "running editor text";
|
|
3214
|
+
if (kind === "compact") return "compacting context";
|
|
2625
3215
|
if (kind === "send_to_editor") return "sending to pi editor";
|
|
2626
3216
|
if (kind === "get_from_editor") return "loading from pi editor";
|
|
2627
3217
|
if (kind === "save_as" || kind === "save_over") return "saving editor text";
|
|
@@ -2650,43 +3240,184 @@ ${cssVarsBlock}
|
|
|
2650
3240
|
return "Studio: " + action + "…";
|
|
2651
3241
|
}
|
|
2652
3242
|
|
|
2653
|
-
function
|
|
2654
|
-
|
|
2655
|
-
statusEl.textContent = prefix + " · " + statusMessage;
|
|
2656
|
-
statusEl.className = statusLevel || "";
|
|
3243
|
+
function shouldAnimateFooterSpinner() {
|
|
3244
|
+
return wsState !== "Disconnected" && (uiBusy || agentBusyFromServer || terminalActivityPhase !== "idle");
|
|
2657
3245
|
}
|
|
2658
3246
|
|
|
2659
|
-
function
|
|
2660
|
-
|
|
2661
|
-
|
|
3247
|
+
function formatNumber(value) {
|
|
3248
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "?";
|
|
3249
|
+
try {
|
|
3250
|
+
return new Intl.NumberFormat().format(Math.round(value));
|
|
3251
|
+
} catch {
|
|
3252
|
+
return String(Math.round(value));
|
|
3253
|
+
}
|
|
2662
3254
|
}
|
|
2663
3255
|
|
|
2664
|
-
function
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
message: statusMessage,
|
|
2671
|
-
level: statusLevel,
|
|
2672
|
-
pendingRequestId,
|
|
2673
|
-
pendingKind,
|
|
2674
|
-
uiBusy,
|
|
2675
|
-
agentBusyFromServer,
|
|
2676
|
-
terminalPhase: terminalActivityPhase,
|
|
2677
|
-
terminalToolName: terminalActivityToolName,
|
|
2678
|
-
terminalActivityLabel,
|
|
2679
|
-
lastSpecificToolLabel,
|
|
2680
|
-
});
|
|
2681
|
-
}
|
|
3256
|
+
function formatContextUsageText() {
|
|
3257
|
+
const hasWindow = typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0;
|
|
3258
|
+
const hasTokens = typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens >= 0;
|
|
3259
|
+
let percentValue = typeof contextPercent === "number" && Number.isFinite(contextPercent)
|
|
3260
|
+
? contextPercent
|
|
3261
|
+
: null;
|
|
2682
3262
|
|
|
2683
|
-
|
|
3263
|
+
if (percentValue == null && hasTokens && hasWindow) {
|
|
3264
|
+
percentValue = (contextTokens / contextWindow) * 100;
|
|
3265
|
+
}
|
|
2684
3266
|
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
3267
|
+
if (!hasTokens && !hasWindow) {
|
|
3268
|
+
return "Context: unknown";
|
|
3269
|
+
}
|
|
3270
|
+
if (!hasTokens && hasWindow) {
|
|
3271
|
+
return "Context: ? / " + formatNumber(contextWindow);
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
let text = "Context: " + formatNumber(contextTokens);
|
|
3275
|
+
if (hasWindow) {
|
|
3276
|
+
text += " / " + formatNumber(contextWindow);
|
|
3277
|
+
}
|
|
3278
|
+
if (percentValue != null && Number.isFinite(percentValue)) {
|
|
3279
|
+
const bounded = Math.max(0, Math.min(100, percentValue));
|
|
3280
|
+
text += " (" + bounded.toFixed(1) + "%)";
|
|
3281
|
+
}
|
|
3282
|
+
return text;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function applyContextUsageFromMessage(message) {
|
|
3286
|
+
if (!message || typeof message !== "object") return false;
|
|
3287
|
+
|
|
3288
|
+
let changed = false;
|
|
3289
|
+
|
|
3290
|
+
if (Object.prototype.hasOwnProperty.call(message, "contextTokens")) {
|
|
3291
|
+
const next = typeof message.contextTokens === "number" && Number.isFinite(message.contextTokens) && message.contextTokens >= 0
|
|
3292
|
+
? message.contextTokens
|
|
3293
|
+
: null;
|
|
3294
|
+
if (next !== contextTokens) {
|
|
3295
|
+
contextTokens = next;
|
|
3296
|
+
changed = true;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
if (Object.prototype.hasOwnProperty.call(message, "contextWindow")) {
|
|
3301
|
+
const next = typeof message.contextWindow === "number" && Number.isFinite(message.contextWindow) && message.contextWindow > 0
|
|
3302
|
+
? message.contextWindow
|
|
3303
|
+
: null;
|
|
3304
|
+
if (next !== contextWindow) {
|
|
3305
|
+
contextWindow = next;
|
|
3306
|
+
changed = true;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
if (Object.prototype.hasOwnProperty.call(message, "contextPercent")) {
|
|
3311
|
+
const next = typeof message.contextPercent === "number" && Number.isFinite(message.contextPercent)
|
|
3312
|
+
? Math.max(0, Math.min(100, message.contextPercent))
|
|
3313
|
+
: null;
|
|
3314
|
+
if (next !== contextPercent) {
|
|
3315
|
+
contextPercent = next;
|
|
3316
|
+
changed = true;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
return changed;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
function updateDocumentTitle() {
|
|
3324
|
+
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
3325
|
+
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
3326
|
+
const titleParts = ["pi Studio"];
|
|
3327
|
+
if (terminalText && terminalText !== "unknown") titleParts.push(terminalText);
|
|
3328
|
+
if (modelText && modelText !== "none") titleParts.push(modelText);
|
|
3329
|
+
document.title = titleParts.join(" · ");
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
function updateFooterMeta() {
|
|
3333
|
+
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
3334
|
+
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
3335
|
+
const contextText = formatContextUsageText();
|
|
3336
|
+
const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
|
|
3337
|
+
if (footerMetaTextEl) {
|
|
3338
|
+
footerMetaTextEl.textContent = text;
|
|
3339
|
+
footerMetaTextEl.title = text;
|
|
3340
|
+
} else if (footerMetaEl) {
|
|
3341
|
+
footerMetaEl.textContent = text;
|
|
3342
|
+
footerMetaEl.title = text;
|
|
3343
|
+
}
|
|
3344
|
+
updateDocumentTitle();
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
function stopFooterSpinner() {
|
|
3348
|
+
if (spinnerTimer) {
|
|
3349
|
+
window.clearInterval(spinnerTimer);
|
|
3350
|
+
spinnerTimer = null;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
function startFooterSpinner() {
|
|
3355
|
+
if (spinnerTimer) return;
|
|
3356
|
+
spinnerTimer = window.setInterval(() => {
|
|
3357
|
+
spinnerFrameIndex = (spinnerFrameIndex + 1) % BRAILLE_SPINNER_FRAMES.length;
|
|
3358
|
+
renderStatus();
|
|
3359
|
+
}, 80);
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
function syncFooterSpinnerState() {
|
|
3363
|
+
if (shouldAnimateFooterSpinner()) {
|
|
3364
|
+
startFooterSpinner();
|
|
3365
|
+
} else {
|
|
3366
|
+
stopFooterSpinner();
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
function renderStatus() {
|
|
3371
|
+
statusEl.textContent = statusMessage;
|
|
3372
|
+
statusEl.className = statusLevel || "";
|
|
3373
|
+
|
|
3374
|
+
const spinnerActive = shouldAnimateFooterSpinner();
|
|
3375
|
+
if (statusLineEl && statusLineEl.classList) {
|
|
3376
|
+
statusLineEl.classList.toggle("with-spinner", spinnerActive);
|
|
3377
|
+
}
|
|
3378
|
+
if (statusSpinnerEl) {
|
|
3379
|
+
statusSpinnerEl.textContent = spinnerActive
|
|
3380
|
+
? (BRAILLE_SPINNER_FRAMES[spinnerFrameIndex % BRAILLE_SPINNER_FRAMES.length] || "")
|
|
3381
|
+
: "";
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
updateFooterMeta();
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
function setWsState(nextState) {
|
|
3388
|
+
wsState = nextState || "Disconnected";
|
|
3389
|
+
syncFooterSpinnerState();
|
|
3390
|
+
renderStatus();
|
|
3391
|
+
syncActionButtons();
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
function setStatus(message, level) {
|
|
3395
|
+
statusMessage = message;
|
|
3396
|
+
statusLevel = level || "";
|
|
3397
|
+
syncFooterSpinnerState();
|
|
3398
|
+
renderStatus();
|
|
3399
|
+
debugTrace("status", {
|
|
3400
|
+
wsState,
|
|
3401
|
+
message: statusMessage,
|
|
3402
|
+
level: statusLevel,
|
|
3403
|
+
pendingRequestId,
|
|
3404
|
+
pendingKind,
|
|
3405
|
+
uiBusy,
|
|
3406
|
+
agentBusyFromServer,
|
|
3407
|
+
terminalPhase: terminalActivityPhase,
|
|
3408
|
+
terminalToolName: terminalActivityToolName,
|
|
3409
|
+
terminalActivityLabel,
|
|
3410
|
+
lastSpecificToolLabel,
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
renderStatus();
|
|
3415
|
+
|
|
3416
|
+
function updateSourceBadge() {
|
|
3417
|
+
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
3418
|
+
sourceBadgeEl.textContent = "Editor origin: " + label;
|
|
3419
|
+
// Show "Set working dir" button when not file-backed
|
|
3420
|
+
var isFileBacked = sourceState.source === "file" && Boolean(sourceState.path);
|
|
2690
3421
|
if (isFileBacked) {
|
|
2691
3422
|
if (resourceDirInput) resourceDirInput.value = "";
|
|
2692
3423
|
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
@@ -2810,6 +3541,151 @@ ${cssVarsBlock}
|
|
|
2810
3541
|
}
|
|
2811
3542
|
}
|
|
2812
3543
|
|
|
3544
|
+
function normalizeHistoryKind(kind) {
|
|
3545
|
+
return kind === "critique" ? "critique" : "annotation";
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
function normalizeHistoryItem(item, fallbackIndex) {
|
|
3549
|
+
if (!item || typeof item !== "object") return null;
|
|
3550
|
+
if (typeof item.markdown !== "string") return null;
|
|
3551
|
+
const markdown = item.markdown;
|
|
3552
|
+
if (!markdown.trim()) return null;
|
|
3553
|
+
|
|
3554
|
+
const id = typeof item.id === "string" && item.id.trim()
|
|
3555
|
+
? item.id.trim()
|
|
3556
|
+
: ("history-" + fallbackIndex + "-" + Date.now());
|
|
3557
|
+
const timestamp = typeof item.timestamp === "number" && Number.isFinite(item.timestamp) && item.timestamp > 0
|
|
3558
|
+
? item.timestamp
|
|
3559
|
+
: Date.now();
|
|
3560
|
+
const prompt = typeof item.prompt === "string"
|
|
3561
|
+
? item.prompt
|
|
3562
|
+
: (item.prompt == null ? null : String(item.prompt));
|
|
3563
|
+
|
|
3564
|
+
return {
|
|
3565
|
+
id,
|
|
3566
|
+
markdown,
|
|
3567
|
+
timestamp,
|
|
3568
|
+
kind: normalizeHistoryKind(item.kind),
|
|
3569
|
+
prompt,
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
function getSelectedHistoryItem() {
|
|
3574
|
+
if (!Array.isArray(responseHistory) || responseHistory.length === 0) return null;
|
|
3575
|
+
if (responseHistoryIndex < 0 || responseHistoryIndex >= responseHistory.length) return null;
|
|
3576
|
+
return responseHistory[responseHistoryIndex] || null;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
function clearActiveResponseView() {
|
|
3580
|
+
latestResponseMarkdown = "";
|
|
3581
|
+
latestResponseKind = "annotation";
|
|
3582
|
+
latestResponseTimestamp = 0;
|
|
3583
|
+
latestResponseIsStructuredCritique = false;
|
|
3584
|
+
latestResponseHasContent = false;
|
|
3585
|
+
latestResponseNormalized = "";
|
|
3586
|
+
latestCritiqueNotes = "";
|
|
3587
|
+
latestCritiqueNotesNormalized = "";
|
|
3588
|
+
refreshResponseUi();
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
function updateHistoryControls() {
|
|
3592
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
3593
|
+
const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
|
|
3594
|
+
? responseHistoryIndex + 1
|
|
3595
|
+
: 0;
|
|
3596
|
+
if (historyIndexBadgeEl) {
|
|
3597
|
+
historyIndexBadgeEl.textContent = "History: " + selected + "/" + total;
|
|
3598
|
+
}
|
|
3599
|
+
if (historyPrevBtn) {
|
|
3600
|
+
historyPrevBtn.disabled = uiBusy || total <= 1 || responseHistoryIndex <= 0;
|
|
3601
|
+
}
|
|
3602
|
+
if (historyNextBtn) {
|
|
3603
|
+
historyNextBtn.disabled = uiBusy || total <= 1 || responseHistoryIndex < 0 || responseHistoryIndex >= total - 1;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
const selectedItem = getSelectedHistoryItem();
|
|
3607
|
+
const hasPrompt = Boolean(selectedItem && typeof selectedItem.prompt === "string" && selectedItem.prompt.trim());
|
|
3608
|
+
if (loadHistoryPromptBtn) {
|
|
3609
|
+
loadHistoryPromptBtn.disabled = uiBusy || !hasPrompt;
|
|
3610
|
+
loadHistoryPromptBtn.textContent = hasPrompt
|
|
3611
|
+
? "Load response prompt into editor"
|
|
3612
|
+
: "Response prompt unavailable";
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
function applySelectedHistoryItem() {
|
|
3617
|
+
const item = getSelectedHistoryItem();
|
|
3618
|
+
if (!item) {
|
|
3619
|
+
clearActiveResponseView();
|
|
3620
|
+
return false;
|
|
3621
|
+
}
|
|
3622
|
+
handleIncomingResponse(item.markdown, item.kind, item.timestamp);
|
|
3623
|
+
return true;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
function selectHistoryIndex(index, options) {
|
|
3627
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
3628
|
+
if (total === 0) {
|
|
3629
|
+
responseHistoryIndex = -1;
|
|
3630
|
+
clearActiveResponseView();
|
|
3631
|
+
updateHistoryControls();
|
|
3632
|
+
return false;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
const nextIndex = Math.max(0, Math.min(total - 1, Number(index) || 0));
|
|
3636
|
+
responseHistoryIndex = nextIndex;
|
|
3637
|
+
const applied = applySelectedHistoryItem();
|
|
3638
|
+
updateHistoryControls();
|
|
3639
|
+
|
|
3640
|
+
if (applied && !(options && options.silent)) {
|
|
3641
|
+
const item = getSelectedHistoryItem();
|
|
3642
|
+
if (item) {
|
|
3643
|
+
const responseLabel = item.kind === "critique" ? "critique" : "response";
|
|
3644
|
+
setStatus("Viewing " + responseLabel + " history " + (nextIndex + 1) + "/" + total + ".");
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
return applied;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
function setResponseHistory(items, options) {
|
|
3651
|
+
const normalized = Array.isArray(items)
|
|
3652
|
+
? items
|
|
3653
|
+
.map((item, index) => normalizeHistoryItem(item, index))
|
|
3654
|
+
.filter((item) => item && typeof item === "object")
|
|
3655
|
+
: [];
|
|
3656
|
+
|
|
3657
|
+
const previousItem = getSelectedHistoryItem();
|
|
3658
|
+
const previousId = previousItem && typeof previousItem.id === "string" ? previousItem.id : null;
|
|
3659
|
+
|
|
3660
|
+
responseHistory = normalized;
|
|
3661
|
+
|
|
3662
|
+
if (!responseHistory.length) {
|
|
3663
|
+
responseHistoryIndex = -1;
|
|
3664
|
+
clearActiveResponseView();
|
|
3665
|
+
updateHistoryControls();
|
|
3666
|
+
return false;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
let targetIndex = responseHistory.length - 1;
|
|
3670
|
+
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
3671
|
+
const autoSelectLatest = options && Object.prototype.hasOwnProperty.call(options, "autoSelectLatest")
|
|
3672
|
+
? Boolean(options.autoSelectLatest)
|
|
3673
|
+
: true;
|
|
3674
|
+
|
|
3675
|
+
if (preserveSelection && previousId) {
|
|
3676
|
+
const preservedIndex = responseHistory.findIndex((item) => item.id === previousId);
|
|
3677
|
+
if (preservedIndex >= 0) {
|
|
3678
|
+
targetIndex = preservedIndex;
|
|
3679
|
+
} else if (!autoSelectLatest && responseHistoryIndex >= 0 && responseHistoryIndex < responseHistory.length) {
|
|
3680
|
+
targetIndex = responseHistoryIndex;
|
|
3681
|
+
}
|
|
3682
|
+
} else if (!autoSelectLatest && responseHistoryIndex >= 0 && responseHistoryIndex < responseHistory.length) {
|
|
3683
|
+
targetIndex = responseHistoryIndex;
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
return selectHistoryIndex(targetIndex, { silent: Boolean(options && options.silent) });
|
|
3687
|
+
}
|
|
3688
|
+
|
|
2813
3689
|
function updateReferenceBadge() {
|
|
2814
3690
|
if (!referenceBadgeEl) return;
|
|
2815
3691
|
|
|
@@ -2833,9 +3709,14 @@ ${cssVarsBlock}
|
|
|
2833
3709
|
|
|
2834
3710
|
const time = formatReferenceTime(latestResponseTimestamp);
|
|
2835
3711
|
const responseLabel = latestResponseKind === "critique" ? "assistant critique" : "assistant response";
|
|
3712
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
3713
|
+
const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
|
|
3714
|
+
? responseHistoryIndex + 1
|
|
3715
|
+
: 0;
|
|
3716
|
+
const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
|
|
2836
3717
|
referenceBadgeEl.textContent = time
|
|
2837
|
-
?
|
|
2838
|
-
:
|
|
3718
|
+
? historyPrefix + responseLabel + " · " + time
|
|
3719
|
+
: historyPrefix + responseLabel;
|
|
2839
3720
|
}
|
|
2840
3721
|
|
|
2841
3722
|
function normalizeForCompare(text) {
|
|
@@ -2846,6 +3727,28 @@ ${cssVarsBlock}
|
|
|
2846
3727
|
return normalizeForCompare(a) === normalizeForCompare(b);
|
|
2847
3728
|
}
|
|
2848
3729
|
|
|
3730
|
+
function hasAnnotationMarkers(text) {
|
|
3731
|
+
const source = String(text || "");
|
|
3732
|
+
ANNOTATION_MARKER_REGEX.lastIndex = 0;
|
|
3733
|
+
const hasMarker = ANNOTATION_MARKER_REGEX.test(source);
|
|
3734
|
+
ANNOTATION_MARKER_REGEX.lastIndex = 0;
|
|
3735
|
+
return hasMarker;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
function stripAnnotationMarkers(text) {
|
|
3739
|
+
return String(text || "").replace(ANNOTATION_MARKER_REGEX, "");
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
function prepareEditorTextForSend(text) {
|
|
3743
|
+
const raw = String(text || "");
|
|
3744
|
+
return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
function prepareEditorTextForPreview(text) {
|
|
3748
|
+
const raw = String(text || "");
|
|
3749
|
+
return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
|
|
3750
|
+
}
|
|
3751
|
+
|
|
2849
3752
|
function updateSyncBadge(normalizedEditorText) {
|
|
2850
3753
|
if (!syncBadgeEl) return;
|
|
2851
3754
|
|
|
@@ -2896,6 +3799,67 @@ ${cssVarsBlock}
|
|
|
2896
3799
|
return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
|
|
2897
3800
|
}
|
|
2898
3801
|
|
|
3802
|
+
function applyAnnotationMarkersToElement(targetEl, mode) {
|
|
3803
|
+
if (!targetEl || mode === "none") return;
|
|
3804
|
+
if (typeof document.createTreeWalker !== "function") return;
|
|
3805
|
+
|
|
3806
|
+
const walker = document.createTreeWalker(targetEl, NodeFilter.SHOW_TEXT);
|
|
3807
|
+
const textNodes = [];
|
|
3808
|
+
let node = walker.nextNode();
|
|
3809
|
+
while (node) {
|
|
3810
|
+
const textNode = node;
|
|
3811
|
+
const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
|
|
3812
|
+
if (value && value.toLowerCase().indexOf("[an:") !== -1) {
|
|
3813
|
+
const parent = textNode.parentElement;
|
|
3814
|
+
const tag = parent && parent.tagName ? parent.tagName.toUpperCase() : "";
|
|
3815
|
+
if (tag !== "CODE" && tag !== "PRE" && tag !== "SCRIPT" && tag !== "STYLE" && tag !== "TEXTAREA") {
|
|
3816
|
+
textNodes.push(textNode);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
node = walker.nextNode();
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
for (const textNode of textNodes) {
|
|
3823
|
+
const text = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
|
|
3824
|
+
if (!text) continue;
|
|
3825
|
+
ANNOTATION_MARKER_REGEX.lastIndex = 0;
|
|
3826
|
+
if (!ANNOTATION_MARKER_REGEX.test(text)) continue;
|
|
3827
|
+
ANNOTATION_MARKER_REGEX.lastIndex = 0;
|
|
3828
|
+
|
|
3829
|
+
const fragment = document.createDocumentFragment();
|
|
3830
|
+
let lastIndex = 0;
|
|
3831
|
+
let match;
|
|
3832
|
+
while ((match = ANNOTATION_MARKER_REGEX.exec(text)) !== null) {
|
|
3833
|
+
const token = match[0] || "";
|
|
3834
|
+
const start = typeof match.index === "number" ? match.index : 0;
|
|
3835
|
+
if (start > lastIndex) {
|
|
3836
|
+
fragment.appendChild(document.createTextNode(text.slice(lastIndex, start)));
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
if (mode === "highlight") {
|
|
3840
|
+
const markerEl = document.createElement("span");
|
|
3841
|
+
markerEl.className = "annotation-preview-marker";
|
|
3842
|
+
markerEl.textContent = typeof match[1] === "string" ? match[1].trim() : token;
|
|
3843
|
+
markerEl.title = token;
|
|
3844
|
+
fragment.appendChild(markerEl);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
lastIndex = start + token.length;
|
|
3848
|
+
if (token.length === 0) {
|
|
3849
|
+
ANNOTATION_MARKER_REGEX.lastIndex += 1;
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
if (lastIndex < text.length) {
|
|
3854
|
+
fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
if (textNode.parentNode) {
|
|
3858
|
+
textNode.parentNode.replaceChild(fragment, textNode);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
|
|
2899
3863
|
function appendMermaidNotice(targetEl, message) {
|
|
2900
3864
|
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
|
|
2901
3865
|
return;
|
|
@@ -3097,6 +4061,126 @@ ${cssVarsBlock}
|
|
|
3097
4061
|
return payload.html;
|
|
3098
4062
|
}
|
|
3099
4063
|
|
|
4064
|
+
function parseContentDispositionFilename(headerValue) {
|
|
4065
|
+
if (!headerValue || typeof headerValue !== "string") return "";
|
|
4066
|
+
|
|
4067
|
+
const utfMatch = headerValue.match(/filename\\*=UTF-8''([^;]+)/i);
|
|
4068
|
+
if (utfMatch && utfMatch[1]) {
|
|
4069
|
+
try {
|
|
4070
|
+
return decodeURIComponent(utfMatch[1].trim());
|
|
4071
|
+
} catch {
|
|
4072
|
+
return utfMatch[1].trim();
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
const quotedMatch = headerValue.match(/filename="([^"]+)"/i);
|
|
4077
|
+
if (quotedMatch && quotedMatch[1]) return quotedMatch[1].trim();
|
|
4078
|
+
|
|
4079
|
+
const plainMatch = headerValue.match(/filename=([^;]+)/i);
|
|
4080
|
+
if (plainMatch && plainMatch[1]) return plainMatch[1].trim();
|
|
4081
|
+
|
|
4082
|
+
return "";
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
async function exportRightPanePdf() {
|
|
4086
|
+
if (uiBusy || pdfExportInProgress) {
|
|
4087
|
+
setStatus("Studio is busy.", "warning");
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
const token = getToken();
|
|
4092
|
+
if (!token) {
|
|
4093
|
+
setStatus("Missing Studio token in URL. Re-run /studio.", "error");
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4098
|
+
if (!rightPaneShowsPreview) {
|
|
4099
|
+
setStatus("Switch right pane to Response (Preview) or Editor (Preview) to export PDF.", "warning");
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
const markdown = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
|
|
4104
|
+
if (!markdown || !markdown.trim()) {
|
|
4105
|
+
setStatus("Nothing to export yet.", "warning");
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
const sourcePath = sourceState.path || "";
|
|
4110
|
+
const resourceDir = (!sourceState.path && resourceDirInput) ? resourceDirInput.value.trim() : "";
|
|
4111
|
+
const isLatex = /\\\\documentclass\\b|\\\\begin\\{document\\}/.test(markdown);
|
|
4112
|
+
let filenameHint = rightView === "editor-preview" ? "studio-editor-preview.pdf" : "studio-response-preview.pdf";
|
|
4113
|
+
if (sourceState.path) {
|
|
4114
|
+
const baseName = sourceState.path.split(/[\\\\/]/).pop() || "studio";
|
|
4115
|
+
const stem = baseName.replace(/\\.[^.]+$/, "") || "studio";
|
|
4116
|
+
filenameHint = stem + "-preview.pdf";
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
pdfExportInProgress = true;
|
|
4120
|
+
updateResultActionButtons();
|
|
4121
|
+
setStatus("Exporting PDF…", "warning");
|
|
4122
|
+
|
|
4123
|
+
try {
|
|
4124
|
+
const response = await fetch("/export-pdf?token=" + encodeURIComponent(token), {
|
|
4125
|
+
method: "POST",
|
|
4126
|
+
headers: {
|
|
4127
|
+
"Content-Type": "application/json",
|
|
4128
|
+
},
|
|
4129
|
+
body: JSON.stringify({
|
|
4130
|
+
markdown: String(markdown || ""),
|
|
4131
|
+
sourcePath: sourcePath,
|
|
4132
|
+
resourceDir: resourceDir,
|
|
4133
|
+
isLatex: isLatex,
|
|
4134
|
+
filenameHint: filenameHint,
|
|
4135
|
+
}),
|
|
4136
|
+
});
|
|
4137
|
+
|
|
4138
|
+
if (!response.ok) {
|
|
4139
|
+
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
4140
|
+
let message = "PDF export failed with HTTP " + response.status + ".";
|
|
4141
|
+
if (contentType.includes("application/json")) {
|
|
4142
|
+
const payload = await response.json().catch(() => null);
|
|
4143
|
+
if (payload && typeof payload.error === "string") {
|
|
4144
|
+
message = payload.error;
|
|
4145
|
+
}
|
|
4146
|
+
} else {
|
|
4147
|
+
const text = await response.text().catch(() => "");
|
|
4148
|
+
if (text && text.trim()) {
|
|
4149
|
+
message = text.trim();
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
throw new Error(message);
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
const blob = await response.blob();
|
|
4156
|
+
const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
|
|
4157
|
+
let downloadName = headerFilename || filenameHint || "studio-preview.pdf";
|
|
4158
|
+
if (!/\\.pdf$/i.test(downloadName)) {
|
|
4159
|
+
downloadName += ".pdf";
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
4163
|
+
const link = document.createElement("a");
|
|
4164
|
+
link.href = blobUrl;
|
|
4165
|
+
link.download = downloadName;
|
|
4166
|
+
link.rel = "noopener";
|
|
4167
|
+
document.body.appendChild(link);
|
|
4168
|
+
link.click();
|
|
4169
|
+
link.remove();
|
|
4170
|
+
window.setTimeout(() => {
|
|
4171
|
+
URL.revokeObjectURL(blobUrl);
|
|
4172
|
+
}, 1800);
|
|
4173
|
+
|
|
4174
|
+
setStatus("Exported PDF: " + downloadName, "success");
|
|
4175
|
+
} catch (error) {
|
|
4176
|
+
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
4177
|
+
setStatus("PDF export failed: " + detail, "error");
|
|
4178
|
+
} finally {
|
|
4179
|
+
pdfExportInProgress = false;
|
|
4180
|
+
updateResultActionButtons();
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
|
|
3100
4184
|
async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
|
|
3101
4185
|
try {
|
|
3102
4186
|
const renderedHtml = await renderMarkdownWithPandoc(markdown);
|
|
@@ -3109,6 +4193,10 @@ ${cssVarsBlock}
|
|
|
3109
4193
|
|
|
3110
4194
|
finishPreviewRender(targetEl);
|
|
3111
4195
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
4196
|
+
const annotationMode = (pane === "source" || pane === "response")
|
|
4197
|
+
? (annotationsEnabled ? "highlight" : "hide")
|
|
4198
|
+
: "none";
|
|
4199
|
+
applyAnnotationMarkersToElement(targetEl, annotationMode);
|
|
3112
4200
|
await renderMermaidInElement(targetEl);
|
|
3113
4201
|
|
|
3114
4202
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
@@ -3134,7 +4222,7 @@ ${cssVarsBlock}
|
|
|
3134
4222
|
|
|
3135
4223
|
function renderSourcePreviewNow() {
|
|
3136
4224
|
if (editorView !== "preview") return;
|
|
3137
|
-
const text = sourceTextEl.value || "";
|
|
4225
|
+
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
3138
4226
|
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
3139
4227
|
finishPreviewRender(sourcePreviewEl);
|
|
3140
4228
|
sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(text, editorLanguage) + "</div>";
|
|
@@ -3194,7 +4282,7 @@ ${cssVarsBlock}
|
|
|
3194
4282
|
|
|
3195
4283
|
function renderActiveResult() {
|
|
3196
4284
|
if (rightView === "editor-preview") {
|
|
3197
|
-
const editorText = sourceTextEl.value || "";
|
|
4285
|
+
const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
3198
4286
|
if (!editorText.trim()) {
|
|
3199
4287
|
finishPreviewRender(critiqueViewEl);
|
|
3200
4288
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
|
|
@@ -3270,6 +4358,20 @@ ${cssVarsBlock}
|
|
|
3270
4358
|
|
|
3271
4359
|
copyResponseBtn.disabled = uiBusy || !hasResponse;
|
|
3272
4360
|
|
|
4361
|
+
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4362
|
+
const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
|
|
4363
|
+
const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
|
|
4364
|
+
if (exportPdfBtn) {
|
|
4365
|
+
exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
|
|
4366
|
+
if (rightView === "markdown") {
|
|
4367
|
+
exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
|
|
4368
|
+
} else if (!canExportPdf) {
|
|
4369
|
+
exportPdfBtn.title = "Nothing to export yet.";
|
|
4370
|
+
} else {
|
|
4371
|
+
exportPdfBtn.title = "Export the current right-pane preview as PDF via pandoc + xelatex.";
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
|
|
3273
4375
|
pullLatestBtn.disabled = uiBusy || followLatest;
|
|
3274
4376
|
pullLatestBtn.textContent = queuedLatestResponse ? "Get latest response *" : "Get latest response";
|
|
3275
4377
|
|
|
@@ -3280,6 +4382,7 @@ ${cssVarsBlock}
|
|
|
3280
4382
|
updateSourceBadge();
|
|
3281
4383
|
updateReferenceBadge();
|
|
3282
4384
|
renderActiveResult();
|
|
4385
|
+
updateHistoryControls();
|
|
3283
4386
|
updateResultActionButtons();
|
|
3284
4387
|
}
|
|
3285
4388
|
|
|
@@ -3294,6 +4397,24 @@ ${cssVarsBlock}
|
|
|
3294
4397
|
return null;
|
|
3295
4398
|
}
|
|
3296
4399
|
|
|
4400
|
+
function buildAnnotatedSaveSuggestion() {
|
|
4401
|
+
const effectivePath = getEffectiveSavePath() || sourceState.path || "";
|
|
4402
|
+
if (effectivePath) {
|
|
4403
|
+
const parts = String(effectivePath).split(/[/\\\\]/);
|
|
4404
|
+
const fileName = parts.pop() || "draft.md";
|
|
4405
|
+
const dir = parts.length > 0 ? parts.join("/") + "/" : "";
|
|
4406
|
+
const stem = fileName.replace(/\\.[^.]+$/, "") || "draft";
|
|
4407
|
+
return dir + stem + ".annotated.md";
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\\s*/i, "") : "draft.md";
|
|
4411
|
+
const stem = rawLabel.replace(/\\.[^.]+$/, "") || "draft";
|
|
4412
|
+
const suggestedDir = resourceDirInput && resourceDirInput.value.trim()
|
|
4413
|
+
? resourceDirInput.value.trim().replace(/\\/$/, "") + "/"
|
|
4414
|
+
: "./";
|
|
4415
|
+
return suggestedDir + stem + ".annotated.md";
|
|
4416
|
+
}
|
|
4417
|
+
|
|
3297
4418
|
function updateSaveFileTooltip() {
|
|
3298
4419
|
if (!saveOverBtn) return;
|
|
3299
4420
|
|
|
@@ -3318,6 +4439,10 @@ ${cssVarsBlock}
|
|
|
3318
4439
|
copyDraftBtn.disabled = uiBusy;
|
|
3319
4440
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
3320
4441
|
if (langSelect) langSelect.disabled = uiBusy;
|
|
4442
|
+
if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
|
|
4443
|
+
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
|
|
4444
|
+
if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
4445
|
+
if (compactBtn) compactBtn.disabled = uiBusy || compactInProgress || wsState === "Disconnected";
|
|
3321
4446
|
editorViewSelect.disabled = uiBusy;
|
|
3322
4447
|
rightViewSelect.disabled = uiBusy;
|
|
3323
4448
|
followSelect.disabled = uiBusy;
|
|
@@ -3326,11 +4451,14 @@ ${cssVarsBlock}
|
|
|
3326
4451
|
critiqueBtn.disabled = uiBusy;
|
|
3327
4452
|
lensSelect.disabled = uiBusy;
|
|
3328
4453
|
updateSaveFileTooltip();
|
|
4454
|
+
updateHistoryControls();
|
|
3329
4455
|
updateResultActionButtons();
|
|
3330
4456
|
}
|
|
3331
4457
|
|
|
3332
4458
|
function setBusy(busy) {
|
|
3333
4459
|
uiBusy = Boolean(busy);
|
|
4460
|
+
syncFooterSpinnerState();
|
|
4461
|
+
renderStatus();
|
|
3334
4462
|
syncActionButtons();
|
|
3335
4463
|
}
|
|
3336
4464
|
|
|
@@ -3344,6 +4472,47 @@ ${cssVarsBlock}
|
|
|
3344
4472
|
syncActionButtons();
|
|
3345
4473
|
}
|
|
3346
4474
|
|
|
4475
|
+
function setEditorText(nextText, options) {
|
|
4476
|
+
const value = String(nextText || "");
|
|
4477
|
+
const preserveScroll = Boolean(options && options.preserveScroll);
|
|
4478
|
+
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
4479
|
+
const previousScrollTop = sourceTextEl.scrollTop;
|
|
4480
|
+
const previousScrollLeft = sourceTextEl.scrollLeft;
|
|
4481
|
+
const previousSelectionStart = sourceTextEl.selectionStart;
|
|
4482
|
+
const previousSelectionEnd = sourceTextEl.selectionEnd;
|
|
4483
|
+
|
|
4484
|
+
sourceTextEl.value = value;
|
|
4485
|
+
|
|
4486
|
+
if (preserveSelection) {
|
|
4487
|
+
const maxIndex = value.length;
|
|
4488
|
+
const start = Math.max(0, Math.min(previousSelectionStart || 0, maxIndex));
|
|
4489
|
+
const end = Math.max(start, Math.min(previousSelectionEnd || start, maxIndex));
|
|
4490
|
+
sourceTextEl.setSelectionRange(start, end);
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
if (preserveScroll) {
|
|
4494
|
+
sourceTextEl.scrollTop = previousScrollTop;
|
|
4495
|
+
sourceTextEl.scrollLeft = previousScrollLeft;
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
syncEditorHighlightScroll();
|
|
4499
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
4500
|
+
? window.requestAnimationFrame.bind(window)
|
|
4501
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
4502
|
+
schedule(() => {
|
|
4503
|
+
syncEditorHighlightScroll();
|
|
4504
|
+
});
|
|
4505
|
+
|
|
4506
|
+
updateAnnotatedReplyHeaderButton();
|
|
4507
|
+
|
|
4508
|
+
if (!options || options.updatePreview !== false) {
|
|
4509
|
+
renderSourcePreview();
|
|
4510
|
+
}
|
|
4511
|
+
if (!options || options.updateMeta !== false) {
|
|
4512
|
+
scheduleEditorMetaUpdate();
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
|
|
3347
4516
|
function setEditorView(nextView) {
|
|
3348
4517
|
editorView = nextView === "preview" ? "preview" : "markdown";
|
|
3349
4518
|
editorViewSelect.value = editorView;
|
|
@@ -3412,7 +4581,7 @@ ${cssVarsBlock}
|
|
|
3412
4581
|
|
|
3413
4582
|
function highlightInlineMarkdown(text) {
|
|
3414
4583
|
const source = String(text || "");
|
|
3415
|
-
const pattern = /(\\x60[^\\x60]*\\x60)|(\\[[^\\]]+\\]\\([^)]+\\))/
|
|
4584
|
+
const pattern = /(\\x60[^\\x60]*\\x60)|(\\[[^\\]]+\\]\\([^)]+\\))|(\\[an:\\s*[^\\]\\n]+\\])/gi;
|
|
3416
4585
|
let lastIndex = 0;
|
|
3417
4586
|
let out = "";
|
|
3418
4587
|
|
|
@@ -3435,6 +4604,8 @@ ${cssVarsBlock}
|
|
|
3435
4604
|
} else {
|
|
3436
4605
|
out += escapeHtml(token);
|
|
3437
4606
|
}
|
|
4607
|
+
} else if (match[3]) {
|
|
4608
|
+
out += wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
|
|
3438
4609
|
} else {
|
|
3439
4610
|
out += escapeHtml(token);
|
|
3440
4611
|
}
|
|
@@ -3805,6 +4976,10 @@ ${cssVarsBlock}
|
|
|
3805
4976
|
function runEditorMetaUpdateNow() {
|
|
3806
4977
|
const normalizedEditor = normalizeForCompare(sourceTextEl.value);
|
|
3807
4978
|
updateResultActionButtons(normalizedEditor);
|
|
4979
|
+
updateAnnotatedReplyHeaderButton();
|
|
4980
|
+
if (stripAnnotationsBtn) {
|
|
4981
|
+
stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
4982
|
+
}
|
|
3808
4983
|
}
|
|
3809
4984
|
|
|
3810
4985
|
function scheduleEditorMetaUpdate() {
|
|
@@ -3856,6 +5031,10 @@ ${cssVarsBlock}
|
|
|
3856
5031
|
return readStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY);
|
|
3857
5032
|
}
|
|
3858
5033
|
|
|
5034
|
+
function readStoredAnnotationsEnabled() {
|
|
5035
|
+
return readStoredToggle(ANNOTATION_MODE_STORAGE_KEY);
|
|
5036
|
+
}
|
|
5037
|
+
|
|
3859
5038
|
function persistEditorHighlightEnabled(enabled) {
|
|
3860
5039
|
persistStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled);
|
|
3861
5040
|
}
|
|
@@ -3864,6 +5043,10 @@ ${cssVarsBlock}
|
|
|
3864
5043
|
persistStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY, enabled);
|
|
3865
5044
|
}
|
|
3866
5045
|
|
|
5046
|
+
function persistAnnotationsEnabled(enabled) {
|
|
5047
|
+
persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
|
|
5048
|
+
}
|
|
5049
|
+
|
|
3867
5050
|
function updateEditorHighlightState() {
|
|
3868
5051
|
const enabled = editorHighlightEnabled && editorView === "markdown";
|
|
3869
5052
|
|
|
@@ -3953,6 +5136,38 @@ ${cssVarsBlock}
|
|
|
3953
5136
|
renderActiveResult();
|
|
3954
5137
|
}
|
|
3955
5138
|
|
|
5139
|
+
function updateAnnotationModeUi() {
|
|
5140
|
+
if (annotationModeSelect) {
|
|
5141
|
+
annotationModeSelect.value = annotationsEnabled ? "on" : "off";
|
|
5142
|
+
annotationModeSelect.title = annotationsEnabled
|
|
5143
|
+
? "Annotations On: keep and send [an: ...] markers."
|
|
5144
|
+
: "Annotations Hidden: keep markers in editor, hide in preview, and strip before Run/Critique.";
|
|
5145
|
+
}
|
|
5146
|
+
|
|
5147
|
+
if (sendRunBtn) {
|
|
5148
|
+
sendRunBtn.title = annotationsEnabled
|
|
5149
|
+
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter."
|
|
5150
|
+
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter.";
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
if (critiqueBtn) {
|
|
5154
|
+
critiqueBtn.title = annotationsEnabled
|
|
5155
|
+
? "Critique editor text as-is (includes [an: ...] markers)."
|
|
5156
|
+
: "Critique editor text with [an: ...] markers stripped.";
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
function setAnnotationsEnabled(enabled, _options) {
|
|
5161
|
+
annotationsEnabled = Boolean(enabled);
|
|
5162
|
+
persistAnnotationsEnabled(annotationsEnabled);
|
|
5163
|
+
updateAnnotationModeUi();
|
|
5164
|
+
|
|
5165
|
+
if (editorHighlightEnabled && editorView === "markdown") {
|
|
5166
|
+
scheduleEditorHighlightRender();
|
|
5167
|
+
}
|
|
5168
|
+
renderSourcePreview();
|
|
5169
|
+
}
|
|
5170
|
+
|
|
3956
5171
|
function extractSection(markdown, title) {
|
|
3957
5172
|
if (!markdown || !title) return "";
|
|
3958
5173
|
|
|
@@ -4049,6 +5264,10 @@ ${cssVarsBlock}
|
|
|
4049
5264
|
|
|
4050
5265
|
debugTrace("server_message", summarizeServerMessage(message));
|
|
4051
5266
|
|
|
5267
|
+
if (applyContextUsageFromMessage(message)) {
|
|
5268
|
+
updateFooterMeta();
|
|
5269
|
+
}
|
|
5270
|
+
|
|
4052
5271
|
if (message.type === "debug_event") {
|
|
4053
5272
|
debugTrace("server_debug_event", summarizeServerMessage(message));
|
|
4054
5273
|
return;
|
|
@@ -4058,6 +5277,13 @@ ${cssVarsBlock}
|
|
|
4058
5277
|
const busy = Boolean(message.busy);
|
|
4059
5278
|
agentBusyFromServer = Boolean(message.agentBusy);
|
|
4060
5279
|
updateTerminalActivityState(message.terminalPhase, message.terminalToolName, message.terminalActivityLabel);
|
|
5280
|
+
if (typeof message.modelLabel === "string") {
|
|
5281
|
+
modelLabel = message.modelLabel;
|
|
5282
|
+
}
|
|
5283
|
+
if (typeof message.terminalSessionLabel === "string") {
|
|
5284
|
+
terminalSessionLabel = message.terminalSessionLabel;
|
|
5285
|
+
}
|
|
5286
|
+
updateFooterMeta();
|
|
4061
5287
|
setBusy(busy);
|
|
4062
5288
|
setWsState(busy ? "Submitting" : "Ready");
|
|
4063
5289
|
if (typeof message.activeRequestId === "string" && message.activeRequestId.length > 0) {
|
|
@@ -4073,13 +5299,21 @@ ${cssVarsBlock}
|
|
|
4073
5299
|
pendingKind = null;
|
|
4074
5300
|
}
|
|
4075
5301
|
|
|
5302
|
+
if (typeof message.compactInProgress === "boolean") {
|
|
5303
|
+
compactInProgress = message.compactInProgress;
|
|
5304
|
+
} else if (pendingKind === "compact") {
|
|
5305
|
+
compactInProgress = true;
|
|
5306
|
+
} else if (!busy) {
|
|
5307
|
+
compactInProgress = false;
|
|
5308
|
+
}
|
|
5309
|
+
|
|
4076
5310
|
let loadedInitialDocument = false;
|
|
4077
5311
|
if (
|
|
4078
5312
|
!initialDocumentApplied &&
|
|
4079
5313
|
message.initialDocument &&
|
|
4080
5314
|
typeof message.initialDocument.text === "string"
|
|
4081
5315
|
) {
|
|
4082
|
-
|
|
5316
|
+
setEditorText(message.initialDocument.text, { preserveScroll: false, preserveSelection: false });
|
|
4083
5317
|
initialDocumentApplied = true;
|
|
4084
5318
|
loadedInitialDocument = true;
|
|
4085
5319
|
setSourceState({
|
|
@@ -4088,13 +5322,21 @@ ${cssVarsBlock}
|
|
|
4088
5322
|
path: message.initialDocument.path || null,
|
|
4089
5323
|
});
|
|
4090
5324
|
refreshResponseUi();
|
|
4091
|
-
renderSourcePreview();
|
|
4092
5325
|
if (typeof message.initialDocument.label === "string" && message.initialDocument.label.length > 0) {
|
|
4093
5326
|
setStatus("Loaded " + message.initialDocument.label + ".", "success");
|
|
4094
5327
|
}
|
|
4095
5328
|
}
|
|
4096
5329
|
|
|
4097
|
-
|
|
5330
|
+
let appliedHistory = false;
|
|
5331
|
+
if (Array.isArray(message.responseHistory)) {
|
|
5332
|
+
appliedHistory = setResponseHistory(message.responseHistory, {
|
|
5333
|
+
autoSelectLatest: true,
|
|
5334
|
+
preserveSelection: false,
|
|
5335
|
+
silent: true,
|
|
5336
|
+
});
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
if (!appliedHistory && message.lastResponse && typeof message.lastResponse.markdown === "string") {
|
|
4098
5340
|
const lastMarkdown = message.lastResponse.markdown;
|
|
4099
5341
|
const lastResponseKind =
|
|
4100
5342
|
message.lastResponse.kind === "critique"
|
|
@@ -4133,15 +5375,46 @@ ${cssVarsBlock}
|
|
|
4133
5375
|
pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
4134
5376
|
pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
|
|
4135
5377
|
stickyStudioKind = pendingKind;
|
|
5378
|
+
if (pendingKind === "compact") {
|
|
5379
|
+
compactInProgress = true;
|
|
5380
|
+
}
|
|
4136
5381
|
setBusy(true);
|
|
4137
5382
|
setWsState("Submitting");
|
|
4138
5383
|
setStatus(getStudioBusyStatus(pendingKind), "warning");
|
|
4139
5384
|
return;
|
|
4140
5385
|
}
|
|
4141
5386
|
|
|
4142
|
-
if (message.type === "
|
|
4143
|
-
if (
|
|
4144
|
-
|
|
5387
|
+
if (message.type === "compaction_completed") {
|
|
5388
|
+
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
5389
|
+
pendingRequestId = null;
|
|
5390
|
+
pendingKind = null;
|
|
5391
|
+
}
|
|
5392
|
+
compactInProgress = false;
|
|
5393
|
+
stickyStudioKind = null;
|
|
5394
|
+
const busy = Boolean(message.busy);
|
|
5395
|
+
setBusy(busy);
|
|
5396
|
+
setWsState(busy ? "Submitting" : "Ready");
|
|
5397
|
+
setStatus(typeof message.message === "string" ? message.message : "Compaction completed.", "success");
|
|
5398
|
+
return;
|
|
5399
|
+
}
|
|
5400
|
+
|
|
5401
|
+
if (message.type === "compaction_error") {
|
|
5402
|
+
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
5403
|
+
pendingRequestId = null;
|
|
5404
|
+
pendingKind = null;
|
|
5405
|
+
}
|
|
5406
|
+
compactInProgress = false;
|
|
5407
|
+
stickyStudioKind = null;
|
|
5408
|
+
const busy = Boolean(message.busy);
|
|
5409
|
+
setBusy(busy);
|
|
5410
|
+
setWsState(busy ? "Submitting" : "Ready");
|
|
5411
|
+
setStatus(typeof message.message === "string" ? message.message : "Compaction failed.", "error");
|
|
5412
|
+
return;
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
if (message.type === "response") {
|
|
5416
|
+
if (pendingRequestId && typeof message.requestId === "string" && message.requestId !== pendingRequestId) {
|
|
5417
|
+
return;
|
|
4145
5418
|
}
|
|
4146
5419
|
|
|
4147
5420
|
const responseKind =
|
|
@@ -4152,23 +5425,45 @@ ${cssVarsBlock}
|
|
|
4152
5425
|
stickyStudioKind = responseKind;
|
|
4153
5426
|
pendingRequestId = null;
|
|
4154
5427
|
pendingKind = null;
|
|
5428
|
+
queuedLatestResponse = null;
|
|
4155
5429
|
setBusy(false);
|
|
4156
5430
|
setWsState("Ready");
|
|
4157
|
-
|
|
5431
|
+
|
|
5432
|
+
let appliedFromHistory = false;
|
|
5433
|
+
if (Array.isArray(message.responseHistory)) {
|
|
5434
|
+
appliedFromHistory = setResponseHistory(message.responseHistory, {
|
|
5435
|
+
autoSelectLatest: true,
|
|
5436
|
+
preserveSelection: false,
|
|
5437
|
+
silent: true,
|
|
5438
|
+
});
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
if (!appliedFromHistory && typeof message.markdown === "string") {
|
|
4158
5442
|
handleIncomingResponse(message.markdown, responseKind, message.timestamp);
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5445
|
+
if (responseKind === "critique") {
|
|
5446
|
+
setStatus("Critique ready.", "success");
|
|
5447
|
+
} else if (responseKind === "direct") {
|
|
5448
|
+
setStatus("Model response ready.", "success");
|
|
5449
|
+
} else {
|
|
5450
|
+
setStatus("Response ready.", "success");
|
|
4166
5451
|
}
|
|
4167
5452
|
return;
|
|
4168
5453
|
}
|
|
4169
5454
|
|
|
4170
5455
|
if (message.type === "latest_response") {
|
|
4171
5456
|
if (pendingRequestId) return;
|
|
5457
|
+
|
|
5458
|
+
const hasHistory = Array.isArray(message.responseHistory);
|
|
5459
|
+
if (hasHistory) {
|
|
5460
|
+
setResponseHistory(message.responseHistory, {
|
|
5461
|
+
autoSelectLatest: followLatest,
|
|
5462
|
+
preserveSelection: !followLatest,
|
|
5463
|
+
silent: true,
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
5466
|
+
|
|
4172
5467
|
if (typeof message.markdown === "string") {
|
|
4173
5468
|
const payload = {
|
|
4174
5469
|
kind: message.kind === "critique" ? "critique" : "annotation",
|
|
@@ -4183,15 +5478,29 @@ ${cssVarsBlock}
|
|
|
4183
5478
|
return;
|
|
4184
5479
|
}
|
|
4185
5480
|
|
|
4186
|
-
if (applyLatestPayload(payload)) {
|
|
5481
|
+
if (!hasHistory && applyLatestPayload(payload)) {
|
|
4187
5482
|
queuedLatestResponse = null;
|
|
4188
5483
|
updateResultActionButtons();
|
|
4189
5484
|
setStatus("Updated from latest response.", "success");
|
|
5485
|
+
return;
|
|
4190
5486
|
}
|
|
5487
|
+
|
|
5488
|
+
queuedLatestResponse = null;
|
|
5489
|
+
updateResultActionButtons();
|
|
5490
|
+
setStatus("Updated from latest response.", "success");
|
|
4191
5491
|
}
|
|
4192
5492
|
return;
|
|
4193
5493
|
}
|
|
4194
5494
|
|
|
5495
|
+
if (message.type === "response_history") {
|
|
5496
|
+
setResponseHistory(message.items, {
|
|
5497
|
+
autoSelectLatest: followLatest,
|
|
5498
|
+
preserveSelection: !followLatest,
|
|
5499
|
+
silent: true,
|
|
5500
|
+
});
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5503
|
+
|
|
4195
5504
|
if (message.type === "saved") {
|
|
4196
5505
|
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
4197
5506
|
pendingRequestId = null;
|
|
@@ -4231,8 +5540,7 @@ ${cssVarsBlock}
|
|
|
4231
5540
|
}
|
|
4232
5541
|
|
|
4233
5542
|
const content = typeof message.content === "string" ? message.content : "";
|
|
4234
|
-
|
|
4235
|
-
renderSourcePreview();
|
|
5543
|
+
setEditorText(content, { preserveScroll: false, preserveSelection: false });
|
|
4236
5544
|
setSourceState({ source: "pi-editor", label: "pi editor draft", path: null });
|
|
4237
5545
|
setBusy(false);
|
|
4238
5546
|
setWsState("Ready");
|
|
@@ -4249,6 +5557,13 @@ ${cssVarsBlock}
|
|
|
4249
5557
|
const busy = Boolean(message.busy);
|
|
4250
5558
|
agentBusyFromServer = Boolean(message.agentBusy);
|
|
4251
5559
|
updateTerminalActivityState(message.terminalPhase, message.terminalToolName, message.terminalActivityLabel);
|
|
5560
|
+
if (typeof message.modelLabel === "string") {
|
|
5561
|
+
modelLabel = message.modelLabel;
|
|
5562
|
+
}
|
|
5563
|
+
if (typeof message.terminalSessionLabel === "string") {
|
|
5564
|
+
terminalSessionLabel = message.terminalSessionLabel;
|
|
5565
|
+
}
|
|
5566
|
+
updateFooterMeta();
|
|
4252
5567
|
|
|
4253
5568
|
if (typeof message.activeRequestId === "string" && message.activeRequestId.length > 0) {
|
|
4254
5569
|
pendingRequestId = message.activeRequestId;
|
|
@@ -4263,6 +5578,14 @@ ${cssVarsBlock}
|
|
|
4263
5578
|
pendingKind = null;
|
|
4264
5579
|
}
|
|
4265
5580
|
|
|
5581
|
+
if (typeof message.compactInProgress === "boolean") {
|
|
5582
|
+
compactInProgress = message.compactInProgress;
|
|
5583
|
+
} else if (pendingKind === "compact") {
|
|
5584
|
+
compactInProgress = true;
|
|
5585
|
+
} else if (!busy) {
|
|
5586
|
+
compactInProgress = false;
|
|
5587
|
+
}
|
|
5588
|
+
|
|
4266
5589
|
setBusy(busy);
|
|
4267
5590
|
setWsState(busy ? "Submitting" : "Ready");
|
|
4268
5591
|
|
|
@@ -4291,6 +5614,9 @@ ${cssVarsBlock}
|
|
|
4291
5614
|
|
|
4292
5615
|
if (message.type === "busy") {
|
|
4293
5616
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
5617
|
+
if (pendingKind === "compact") {
|
|
5618
|
+
compactInProgress = false;
|
|
5619
|
+
}
|
|
4294
5620
|
pendingRequestId = null;
|
|
4295
5621
|
pendingKind = null;
|
|
4296
5622
|
}
|
|
@@ -4303,6 +5629,9 @@ ${cssVarsBlock}
|
|
|
4303
5629
|
|
|
4304
5630
|
if (message.type === "error") {
|
|
4305
5631
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
5632
|
+
if (pendingKind === "compact") {
|
|
5633
|
+
compactInProgress = false;
|
|
5634
|
+
}
|
|
4306
5635
|
pendingRequestId = null;
|
|
4307
5636
|
pendingKind = null;
|
|
4308
5637
|
}
|
|
@@ -4421,7 +5750,8 @@ ${cssVarsBlock}
|
|
|
4421
5750
|
function buildAnnotationHeader() {
|
|
4422
5751
|
const sourceDescriptor = describeSourceForAnnotation();
|
|
4423
5752
|
let header = "annotated reply below:\\n";
|
|
4424
|
-
header += "original source: " + sourceDescriptor + "\\n
|
|
5753
|
+
header += "original source: " + sourceDescriptor + "\\n";
|
|
5754
|
+
header += "annotation syntax: [an: your note]\\n\\n---\\n\\n";
|
|
4425
5755
|
return header;
|
|
4426
5756
|
}
|
|
4427
5757
|
|
|
@@ -4447,19 +5777,38 @@ ${cssVarsBlock}
|
|
|
4447
5777
|
};
|
|
4448
5778
|
}
|
|
4449
5779
|
|
|
4450
|
-
function
|
|
5780
|
+
function updateAnnotatedReplyHeaderButton() {
|
|
5781
|
+
if (!insertHeaderBtn) return;
|
|
5782
|
+
const hasHeader = stripAnnotationHeader(sourceTextEl.value).hadHeader;
|
|
5783
|
+
if (hasHeader) {
|
|
5784
|
+
insertHeaderBtn.textContent = "Remove annotated reply header";
|
|
5785
|
+
insertHeaderBtn.title = "Remove annotated-reply protocol header while keeping body text.";
|
|
5786
|
+
return;
|
|
5787
|
+
}
|
|
5788
|
+
insertHeaderBtn.textContent = "Insert annotated reply header";
|
|
5789
|
+
insertHeaderBtn.title = "Insert annotated-reply protocol header (includes source metadata and [an: ...] syntax hint).";
|
|
5790
|
+
}
|
|
5791
|
+
|
|
5792
|
+
function toggleAnnotatedReplyHeader() {
|
|
4451
5793
|
const stripped = stripAnnotationHeader(sourceTextEl.value);
|
|
4452
|
-
const updated = buildAnnotationHeader() + stripped.body;
|
|
4453
5794
|
|
|
5795
|
+
if (stripped.hadHeader) {
|
|
5796
|
+
const updated = stripped.body;
|
|
5797
|
+
setEditorText(updated, { preserveScroll: true, preserveSelection: true });
|
|
5798
|
+
updateResultActionButtons();
|
|
5799
|
+
setStatus("Removed annotated reply header.", "success");
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
const updated = buildAnnotationHeader() + stripped.body;
|
|
4454
5804
|
if (isTextEquivalent(sourceTextEl.value, updated)) {
|
|
4455
|
-
setStatus("
|
|
5805
|
+
setStatus("Annotated reply header already present.");
|
|
4456
5806
|
return;
|
|
4457
5807
|
}
|
|
4458
5808
|
|
|
4459
|
-
|
|
4460
|
-
renderSourcePreview();
|
|
5809
|
+
setEditorText(updated, { preserveScroll: true, preserveSelection: true });
|
|
4461
5810
|
updateResultActionButtons();
|
|
4462
|
-
setStatus(
|
|
5811
|
+
setStatus("Inserted annotated reply header.", "success");
|
|
4463
5812
|
}
|
|
4464
5813
|
|
|
4465
5814
|
function requestLatestResponse() {
|
|
@@ -4479,6 +5828,9 @@ ${cssVarsBlock}
|
|
|
4479
5828
|
}
|
|
4480
5829
|
|
|
4481
5830
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
5831
|
+
window.addEventListener("beforeunload", () => {
|
|
5832
|
+
stopFooterSpinner();
|
|
5833
|
+
});
|
|
4482
5834
|
|
|
4483
5835
|
editorViewSelect.addEventListener("change", () => {
|
|
4484
5836
|
setEditorView(editorViewSelect.value);
|
|
@@ -4491,7 +5843,11 @@ ${cssVarsBlock}
|
|
|
4491
5843
|
followSelect.addEventListener("change", () => {
|
|
4492
5844
|
followLatest = followSelect.value !== "off";
|
|
4493
5845
|
if (followLatest && queuedLatestResponse) {
|
|
4494
|
-
if (
|
|
5846
|
+
if (responseHistory.length > 0) {
|
|
5847
|
+
selectHistoryIndex(responseHistory.length - 1, { silent: true });
|
|
5848
|
+
queuedLatestResponse = null;
|
|
5849
|
+
setStatus("Applied queued response.", "success");
|
|
5850
|
+
} else if (applyLatestPayload(queuedLatestResponse)) {
|
|
4495
5851
|
queuedLatestResponse = null;
|
|
4496
5852
|
setStatus("Applied queued response.", "success");
|
|
4497
5853
|
}
|
|
@@ -4519,9 +5875,90 @@ ${cssVarsBlock}
|
|
|
4519
5875
|
});
|
|
4520
5876
|
}
|
|
4521
5877
|
|
|
5878
|
+
if (annotationModeSelect) {
|
|
5879
|
+
annotationModeSelect.addEventListener("change", () => {
|
|
5880
|
+
setAnnotationsEnabled(annotationModeSelect.value !== "off");
|
|
5881
|
+
});
|
|
5882
|
+
}
|
|
5883
|
+
|
|
5884
|
+
if (compactBtn) {
|
|
5885
|
+
compactBtn.addEventListener("click", () => {
|
|
5886
|
+
if (compactInProgress) {
|
|
5887
|
+
setStatus("Compaction is already running.", "warning");
|
|
5888
|
+
return;
|
|
5889
|
+
}
|
|
5890
|
+
if (uiBusy) {
|
|
5891
|
+
setStatus("Studio is busy.", "warning");
|
|
5892
|
+
return;
|
|
5893
|
+
}
|
|
5894
|
+
|
|
5895
|
+
const requestId = makeRequestId();
|
|
5896
|
+
pendingRequestId = requestId;
|
|
5897
|
+
pendingKind = "compact";
|
|
5898
|
+
stickyStudioKind = "compact";
|
|
5899
|
+
compactInProgress = true;
|
|
5900
|
+
setBusy(true);
|
|
5901
|
+
setWsState("Submitting");
|
|
5902
|
+
|
|
5903
|
+
const sent = sendMessage({ type: "compact_request", requestId });
|
|
5904
|
+
if (!sent) {
|
|
5905
|
+
compactInProgress = false;
|
|
5906
|
+
if (pendingRequestId === requestId) {
|
|
5907
|
+
pendingRequestId = null;
|
|
5908
|
+
pendingKind = null;
|
|
5909
|
+
}
|
|
5910
|
+
stickyStudioKind = null;
|
|
5911
|
+
setBusy(false);
|
|
5912
|
+
return;
|
|
5913
|
+
}
|
|
5914
|
+
|
|
5915
|
+
setStatus("Studio: compacting context…", "warning");
|
|
5916
|
+
});
|
|
5917
|
+
}
|
|
5918
|
+
|
|
5919
|
+
if (historyPrevBtn) {
|
|
5920
|
+
historyPrevBtn.addEventListener("click", () => {
|
|
5921
|
+
if (!responseHistory.length) {
|
|
5922
|
+
setStatus("No response history available yet.", "warning");
|
|
5923
|
+
return;
|
|
5924
|
+
}
|
|
5925
|
+
selectHistoryIndex(responseHistoryIndex - 1);
|
|
5926
|
+
});
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5929
|
+
if (historyNextBtn) {
|
|
5930
|
+
historyNextBtn.addEventListener("click", () => {
|
|
5931
|
+
if (!responseHistory.length) {
|
|
5932
|
+
setStatus("No response history available yet.", "warning");
|
|
5933
|
+
return;
|
|
5934
|
+
}
|
|
5935
|
+
selectHistoryIndex(responseHistoryIndex + 1);
|
|
5936
|
+
});
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5939
|
+
if (loadHistoryPromptBtn) {
|
|
5940
|
+
loadHistoryPromptBtn.addEventListener("click", () => {
|
|
5941
|
+
const item = getSelectedHistoryItem();
|
|
5942
|
+
const prompt = item && typeof item.prompt === "string" ? item.prompt : "";
|
|
5943
|
+
if (!prompt.trim()) {
|
|
5944
|
+
setStatus("Prompt unavailable for the selected response.", "warning");
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
setEditorText(prompt, { preserveScroll: false, preserveSelection: false });
|
|
5949
|
+
setSourceState({ source: "blank", label: "response prompt", path: null });
|
|
5950
|
+
setStatus("Loaded response prompt into editor.", "success");
|
|
5951
|
+
});
|
|
5952
|
+
}
|
|
5953
|
+
|
|
4522
5954
|
pullLatestBtn.addEventListener("click", () => {
|
|
4523
5955
|
if (queuedLatestResponse) {
|
|
4524
|
-
if (
|
|
5956
|
+
if (responseHistory.length > 0) {
|
|
5957
|
+
selectHistoryIndex(responseHistory.length - 1, { silent: true });
|
|
5958
|
+
queuedLatestResponse = null;
|
|
5959
|
+
setStatus("Pulled latest response from history.", "success");
|
|
5960
|
+
updateResultActionButtons();
|
|
5961
|
+
} else if (applyLatestPayload(queuedLatestResponse)) {
|
|
4525
5962
|
queuedLatestResponse = null;
|
|
4526
5963
|
setStatus("Pulled queued response.", "success");
|
|
4527
5964
|
updateResultActionButtons();
|
|
@@ -4541,12 +5978,28 @@ ${cssVarsBlock}
|
|
|
4541
5978
|
syncEditorHighlightScroll();
|
|
4542
5979
|
});
|
|
4543
5980
|
|
|
5981
|
+
sourceTextEl.addEventListener("keyup", () => {
|
|
5982
|
+
if (!editorHighlightEnabled || editorView !== "markdown") return;
|
|
5983
|
+
syncEditorHighlightScroll();
|
|
5984
|
+
});
|
|
5985
|
+
|
|
5986
|
+
sourceTextEl.addEventListener("mouseup", () => {
|
|
5987
|
+
if (!editorHighlightEnabled || editorView !== "markdown") return;
|
|
5988
|
+
syncEditorHighlightScroll();
|
|
5989
|
+
});
|
|
5990
|
+
|
|
5991
|
+
window.addEventListener("resize", () => {
|
|
5992
|
+
if (!editorHighlightEnabled || editorView !== "markdown") return;
|
|
5993
|
+
syncEditorHighlightScroll();
|
|
5994
|
+
});
|
|
5995
|
+
|
|
4544
5996
|
insertHeaderBtn.addEventListener("click", () => {
|
|
4545
|
-
|
|
5997
|
+
toggleAnnotatedReplyHeader();
|
|
4546
5998
|
});
|
|
4547
5999
|
|
|
4548
6000
|
critiqueBtn.addEventListener("click", () => {
|
|
4549
|
-
const
|
|
6001
|
+
const preparedDocumentText = prepareEditorTextForSend(sourceTextEl.value);
|
|
6002
|
+
const documentText = preparedDocumentText.trim();
|
|
4550
6003
|
if (!documentText) {
|
|
4551
6004
|
setStatus("Add editor text before critique.", "warning");
|
|
4552
6005
|
return;
|
|
@@ -4574,8 +6027,7 @@ ${cssVarsBlock}
|
|
|
4574
6027
|
setStatus("No response available yet.", "warning");
|
|
4575
6028
|
return;
|
|
4576
6029
|
}
|
|
4577
|
-
|
|
4578
|
-
renderSourcePreview();
|
|
6030
|
+
setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
|
|
4579
6031
|
setSourceState({ source: "last-response", label: "last model response", path: null });
|
|
4580
6032
|
setStatus("Loaded response into editor.", "success");
|
|
4581
6033
|
});
|
|
@@ -4592,8 +6044,7 @@ ${cssVarsBlock}
|
|
|
4592
6044
|
return;
|
|
4593
6045
|
}
|
|
4594
6046
|
|
|
4595
|
-
|
|
4596
|
-
renderSourcePreview();
|
|
6047
|
+
setEditorText(notes, { preserveScroll: false, preserveSelection: false });
|
|
4597
6048
|
setSourceState({ source: "blank", label: "critique notes", path: null });
|
|
4598
6049
|
setStatus("Loaded critique notes into editor.", "success");
|
|
4599
6050
|
});
|
|
@@ -4604,8 +6055,7 @@ ${cssVarsBlock}
|
|
|
4604
6055
|
return;
|
|
4605
6056
|
}
|
|
4606
6057
|
|
|
4607
|
-
|
|
4608
|
-
renderSourcePreview();
|
|
6058
|
+
setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
|
|
4609
6059
|
setSourceState({ source: "blank", label: "full critique", path: null });
|
|
4610
6060
|
setStatus("Loaded full critique into editor.", "success");
|
|
4611
6061
|
});
|
|
@@ -4624,6 +6074,12 @@ ${cssVarsBlock}
|
|
|
4624
6074
|
}
|
|
4625
6075
|
});
|
|
4626
6076
|
|
|
6077
|
+
if (exportPdfBtn) {
|
|
6078
|
+
exportPdfBtn.addEventListener("click", () => {
|
|
6079
|
+
void exportRightPanePdf();
|
|
6080
|
+
});
|
|
6081
|
+
}
|
|
6082
|
+
|
|
4627
6083
|
saveAsBtn.addEventListener("click", () => {
|
|
4628
6084
|
const content = sourceTextEl.value;
|
|
4629
6085
|
if (!content.trim()) {
|
|
@@ -4725,8 +6181,8 @@ ${cssVarsBlock}
|
|
|
4725
6181
|
}
|
|
4726
6182
|
|
|
4727
6183
|
sendRunBtn.addEventListener("click", () => {
|
|
4728
|
-
const
|
|
4729
|
-
if (!
|
|
6184
|
+
const prepared = prepareEditorTextForSend(sourceTextEl.value);
|
|
6185
|
+
if (!prepared.trim()) {
|
|
4730
6186
|
setStatus("Editor is empty. Nothing to run.", "warning");
|
|
4731
6187
|
return;
|
|
4732
6188
|
}
|
|
@@ -4737,7 +6193,7 @@ ${cssVarsBlock}
|
|
|
4737
6193
|
const sent = sendMessage({
|
|
4738
6194
|
type: "send_run_request",
|
|
4739
6195
|
requestId,
|
|
4740
|
-
text:
|
|
6196
|
+
text: prepared,
|
|
4741
6197
|
});
|
|
4742
6198
|
|
|
4743
6199
|
if (!sent) {
|
|
@@ -4762,6 +6218,53 @@ ${cssVarsBlock}
|
|
|
4762
6218
|
}
|
|
4763
6219
|
});
|
|
4764
6220
|
|
|
6221
|
+
if (saveAnnotatedBtn) {
|
|
6222
|
+
saveAnnotatedBtn.addEventListener("click", () => {
|
|
6223
|
+
const content = sourceTextEl.value;
|
|
6224
|
+
if (!content.trim()) {
|
|
6225
|
+
setStatus("Editor is empty. Nothing to save.", "warning");
|
|
6226
|
+
return;
|
|
6227
|
+
}
|
|
6228
|
+
|
|
6229
|
+
const suggested = buildAnnotatedSaveSuggestion();
|
|
6230
|
+
const path = window.prompt("Save annotated editor content as:", suggested);
|
|
6231
|
+
if (!path) return;
|
|
6232
|
+
|
|
6233
|
+
const requestId = beginUiAction("save_as");
|
|
6234
|
+
if (!requestId) return;
|
|
6235
|
+
|
|
6236
|
+
const sent = sendMessage({
|
|
6237
|
+
type: "save_as_request",
|
|
6238
|
+
requestId,
|
|
6239
|
+
path,
|
|
6240
|
+
content,
|
|
6241
|
+
});
|
|
6242
|
+
|
|
6243
|
+
if (!sent) {
|
|
6244
|
+
pendingRequestId = null;
|
|
6245
|
+
pendingKind = null;
|
|
6246
|
+
setBusy(false);
|
|
6247
|
+
}
|
|
6248
|
+
});
|
|
6249
|
+
}
|
|
6250
|
+
|
|
6251
|
+
if (stripAnnotationsBtn) {
|
|
6252
|
+
stripAnnotationsBtn.addEventListener("click", () => {
|
|
6253
|
+
const content = sourceTextEl.value;
|
|
6254
|
+
if (!hasAnnotationMarkers(content)) {
|
|
6255
|
+
setStatus("No [an: ...] markers found in editor.", "warning");
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
const confirmed = window.confirm("Remove all [an: ...] markers from editor text? This cannot be undone.");
|
|
6260
|
+
if (!confirmed) return;
|
|
6261
|
+
|
|
6262
|
+
const strippedContent = stripAnnotationMarkers(content);
|
|
6263
|
+
setEditorText(strippedContent, { preserveScroll: true, preserveSelection: false });
|
|
6264
|
+
setStatus("Removed annotation markers from editor text.", "success");
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
|
|
4765
6268
|
// Working directory controls — three states: button | input | label
|
|
4766
6269
|
function showResourceDirState(state) {
|
|
4767
6270
|
// state: "button" | "input" | "label"
|
|
@@ -4830,8 +6333,7 @@ ${cssVarsBlock}
|
|
|
4830
6333
|
const reader = new FileReader();
|
|
4831
6334
|
reader.onload = () => {
|
|
4832
6335
|
const text = typeof reader.result === "string" ? reader.result : "";
|
|
4833
|
-
|
|
4834
|
-
renderSourcePreview();
|
|
6336
|
+
setEditorText(text, { preserveScroll: false, preserveSelection: false });
|
|
4835
6337
|
setSourceState({
|
|
4836
6338
|
source: "blank",
|
|
4837
6339
|
label: "upload: " + file.name,
|
|
@@ -4852,6 +6354,7 @@ ${cssVarsBlock}
|
|
|
4852
6354
|
|
|
4853
6355
|
setSourceState(initialSourceState);
|
|
4854
6356
|
refreshResponseUi();
|
|
6357
|
+
updateAnnotatedReplyHeaderButton();
|
|
4855
6358
|
setActivePane("left");
|
|
4856
6359
|
|
|
4857
6360
|
const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
|
|
@@ -4866,6 +6369,10 @@ ${cssVarsBlock}
|
|
|
4866
6369
|
const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
|
|
4867
6370
|
setResponseHighlightEnabled(initialResponseHighlightEnabled);
|
|
4868
6371
|
|
|
6372
|
+
const storedAnnotationsEnabled = readStoredAnnotationsEnabled();
|
|
6373
|
+
const initialAnnotationsEnabled = storedAnnotationsEnabled ?? Boolean(annotationModeSelect ? annotationModeSelect.value !== "off" : true);
|
|
6374
|
+
setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
|
|
6375
|
+
|
|
4869
6376
|
setEditorView(editorView);
|
|
4870
6377
|
setRightView(rightView);
|
|
4871
6378
|
renderSourcePreview();
|
|
@@ -4892,14 +6399,121 @@ export default function (pi: ExtensionAPI) {
|
|
|
4892
6399
|
let terminalActivityToolName: string | null = null;
|
|
4893
6400
|
let terminalActivityLabel: string | null = null;
|
|
4894
6401
|
let lastSpecificToolActivityLabel: string | null = null;
|
|
6402
|
+
let currentModel: { provider?: string; id?: string } | undefined;
|
|
6403
|
+
let currentModelLabel = "none";
|
|
6404
|
+
let terminalSessionLabel = buildTerminalSessionLabel(studioCwd);
|
|
6405
|
+
let studioResponseHistory: StudioResponseHistoryItem[] = [];
|
|
6406
|
+
let contextUsageSnapshot: StudioContextUsageSnapshot = {
|
|
6407
|
+
tokens: null,
|
|
6408
|
+
contextWindow: null,
|
|
6409
|
+
percent: null,
|
|
6410
|
+
};
|
|
6411
|
+
let compactInProgress = false;
|
|
6412
|
+
let compactRequestId: string | null = null;
|
|
6413
|
+
let updateCheckStarted = false;
|
|
6414
|
+
let updateCheckCompleted = false;
|
|
6415
|
+
const packageMetadata = readLocalPackageMetadata();
|
|
6416
|
+
|
|
6417
|
+
const isStudioBusy = () => agentBusy || activeRequest !== null || compactInProgress;
|
|
6418
|
+
|
|
6419
|
+
const getSessionNameSafe = (): string | undefined => {
|
|
6420
|
+
try {
|
|
6421
|
+
return pi.getSessionName();
|
|
6422
|
+
} catch {
|
|
6423
|
+
return undefined;
|
|
6424
|
+
}
|
|
6425
|
+
};
|
|
6426
|
+
|
|
6427
|
+
const getThinkingLevelSafe = (): string | undefined => {
|
|
6428
|
+
try {
|
|
6429
|
+
return pi.getThinkingLevel();
|
|
6430
|
+
} catch {
|
|
6431
|
+
return undefined;
|
|
6432
|
+
}
|
|
6433
|
+
};
|
|
4895
6434
|
|
|
4896
|
-
const
|
|
6435
|
+
const refreshRuntimeMetadata = (ctx?: { cwd?: string; model?: { provider?: string; id?: string } | undefined }) => {
|
|
6436
|
+
if (ctx?.cwd) {
|
|
6437
|
+
studioCwd = ctx.cwd;
|
|
6438
|
+
}
|
|
6439
|
+
if (ctx && Object.prototype.hasOwnProperty.call(ctx, "model")) {
|
|
6440
|
+
if (ctx.model) {
|
|
6441
|
+
currentModel = {
|
|
6442
|
+
provider: ctx.model.provider,
|
|
6443
|
+
id: ctx.model.id,
|
|
6444
|
+
};
|
|
6445
|
+
} else {
|
|
6446
|
+
currentModel = undefined;
|
|
6447
|
+
}
|
|
6448
|
+
} else if (!currentModel && lastCommandCtx?.model) {
|
|
6449
|
+
currentModel = {
|
|
6450
|
+
provider: lastCommandCtx.model.provider,
|
|
6451
|
+
id: lastCommandCtx.model.id,
|
|
6452
|
+
};
|
|
6453
|
+
}
|
|
6454
|
+
const baseModelLabel = formatModelLabel(currentModel);
|
|
6455
|
+
currentModelLabel = formatModelLabelWithThinking(baseModelLabel, getThinkingLevelSafe());
|
|
6456
|
+
terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
|
|
6457
|
+
};
|
|
4897
6458
|
|
|
4898
6459
|
const notifyStudio = (message: string, level: "info" | "warning" | "error" = "info") => {
|
|
4899
6460
|
if (!lastCommandCtx) return;
|
|
4900
6461
|
lastCommandCtx.ui.notify(message, level);
|
|
4901
6462
|
};
|
|
4902
6463
|
|
|
6464
|
+
const refreshContextUsage = (
|
|
6465
|
+
ctx?: { getContextUsage(): { tokens: number | null; contextWindow: number; percent: number | null } | undefined },
|
|
6466
|
+
): StudioContextUsageSnapshot => {
|
|
6467
|
+
const usage = ctx?.getContextUsage?.() ?? lastCommandCtx?.getContextUsage?.();
|
|
6468
|
+
if (usage === undefined) return contextUsageSnapshot;
|
|
6469
|
+
contextUsageSnapshot = normalizeContextUsageSnapshot(usage);
|
|
6470
|
+
return contextUsageSnapshot;
|
|
6471
|
+
};
|
|
6472
|
+
|
|
6473
|
+
const clearCompactionState = () => {
|
|
6474
|
+
compactInProgress = false;
|
|
6475
|
+
compactRequestId = null;
|
|
6476
|
+
};
|
|
6477
|
+
|
|
6478
|
+
const syncStudioResponseHistory = (entries: SessionEntry[]) => {
|
|
6479
|
+
studioResponseHistory = buildResponseHistoryFromEntries(entries, RESPONSE_HISTORY_LIMIT);
|
|
6480
|
+
const latest = studioResponseHistory[studioResponseHistory.length - 1];
|
|
6481
|
+
if (!latest) {
|
|
6482
|
+
lastStudioResponse = null;
|
|
6483
|
+
return;
|
|
6484
|
+
}
|
|
6485
|
+
lastStudioResponse = {
|
|
6486
|
+
markdown: latest.markdown,
|
|
6487
|
+
timestamp: latest.timestamp,
|
|
6488
|
+
kind: latest.kind,
|
|
6489
|
+
};
|
|
6490
|
+
};
|
|
6491
|
+
|
|
6492
|
+
const broadcastResponseHistory = () => {
|
|
6493
|
+
broadcast({
|
|
6494
|
+
type: "response_history",
|
|
6495
|
+
items: studioResponseHistory,
|
|
6496
|
+
});
|
|
6497
|
+
};
|
|
6498
|
+
|
|
6499
|
+
const maybeNotifyUpdateAvailable = async (ctx: ExtensionCommandContext) => {
|
|
6500
|
+
if (updateCheckStarted || updateCheckCompleted) return;
|
|
6501
|
+
updateCheckStarted = true;
|
|
6502
|
+
try {
|
|
6503
|
+
const metadata = packageMetadata;
|
|
6504
|
+
if (!metadata) return;
|
|
6505
|
+
const latest = await fetchLatestNpmVersion(metadata.name, UPDATE_CHECK_TIMEOUT_MS);
|
|
6506
|
+
if (!latest) return;
|
|
6507
|
+
if (!isVersionBehind(metadata.version, latest)) return;
|
|
6508
|
+
ctx.ui.notify(
|
|
6509
|
+
`Update available for ${metadata.name}: ${metadata.version} → ${latest}. Run: pi install npm:${metadata.name}`,
|
|
6510
|
+
"info",
|
|
6511
|
+
);
|
|
6512
|
+
} finally {
|
|
6513
|
+
updateCheckCompleted = true;
|
|
6514
|
+
}
|
|
6515
|
+
};
|
|
6516
|
+
|
|
4903
6517
|
const sendToClient = (client: WebSocket, payload: unknown) => {
|
|
4904
6518
|
if (client.readyState !== WebSocket.OPEN) return;
|
|
4905
6519
|
try {
|
|
@@ -4978,14 +6592,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
4978
6592
|
label: terminalActivityLabel,
|
|
4979
6593
|
baseLabel,
|
|
4980
6594
|
lastSpecificToolActivityLabel,
|
|
4981
|
-
activeRequestId: activeRequest?.id ?? null,
|
|
4982
|
-
activeRequestKind: activeRequest?.kind ?? null,
|
|
6595
|
+
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
6596
|
+
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
4983
6597
|
agentBusy,
|
|
4984
6598
|
});
|
|
4985
6599
|
broadcastState();
|
|
4986
6600
|
};
|
|
4987
6601
|
|
|
4988
6602
|
const broadcastState = () => {
|
|
6603
|
+
terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
|
|
6604
|
+
currentModelLabel = formatModelLabelWithThinking(formatModelLabel(currentModel), getThinkingLevelSafe());
|
|
6605
|
+
refreshContextUsage();
|
|
4989
6606
|
broadcast({
|
|
4990
6607
|
type: "studio_state",
|
|
4991
6608
|
busy: isStudioBusy(),
|
|
@@ -4993,8 +6610,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
4993
6610
|
terminalPhase: terminalActivityPhase,
|
|
4994
6611
|
terminalToolName: terminalActivityToolName,
|
|
4995
6612
|
terminalActivityLabel,
|
|
4996
|
-
|
|
4997
|
-
|
|
6613
|
+
modelLabel: currentModelLabel,
|
|
6614
|
+
terminalSessionLabel,
|
|
6615
|
+
contextTokens: contextUsageSnapshot.tokens,
|
|
6616
|
+
contextWindow: contextUsageSnapshot.contextWindow,
|
|
6617
|
+
contextPercent: contextUsageSnapshot.percent,
|
|
6618
|
+
compactInProgress,
|
|
6619
|
+
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
6620
|
+
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
4998
6621
|
});
|
|
4999
6622
|
};
|
|
5000
6623
|
|
|
@@ -5027,6 +6650,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
5027
6650
|
broadcast({ type: "busy", requestId, message: "A studio request is already in progress." });
|
|
5028
6651
|
return false;
|
|
5029
6652
|
}
|
|
6653
|
+
if (compactInProgress) {
|
|
6654
|
+
broadcast({ type: "busy", requestId, message: "Context compaction is currently running." });
|
|
6655
|
+
return false;
|
|
6656
|
+
}
|
|
5030
6657
|
if (agentBusy) {
|
|
5031
6658
|
broadcast({ type: "busy", requestId, message: "pi is currently busy. Wait for the current turn to finish." });
|
|
5032
6659
|
return false;
|
|
@@ -5079,6 +6706,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5079
6706
|
});
|
|
5080
6707
|
|
|
5081
6708
|
if (msg.type === "hello") {
|
|
6709
|
+
refreshContextUsage();
|
|
5082
6710
|
sendToClient(client, {
|
|
5083
6711
|
type: "hello_ack",
|
|
5084
6712
|
busy: isStudioBusy(),
|
|
@@ -5086,9 +6714,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
5086
6714
|
terminalPhase: terminalActivityPhase,
|
|
5087
6715
|
terminalToolName: terminalActivityToolName,
|
|
5088
6716
|
terminalActivityLabel,
|
|
5089
|
-
|
|
5090
|
-
|
|
6717
|
+
modelLabel: currentModelLabel,
|
|
6718
|
+
terminalSessionLabel,
|
|
6719
|
+
contextTokens: contextUsageSnapshot.tokens,
|
|
6720
|
+
contextWindow: contextUsageSnapshot.contextWindow,
|
|
6721
|
+
contextPercent: contextUsageSnapshot.percent,
|
|
6722
|
+
compactInProgress,
|
|
6723
|
+
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
6724
|
+
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
5091
6725
|
lastResponse: lastStudioResponse,
|
|
6726
|
+
responseHistory: studioResponseHistory,
|
|
5092
6727
|
initialDocument: initialStudioDocument,
|
|
5093
6728
|
});
|
|
5094
6729
|
return;
|
|
@@ -5104,6 +6739,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5104
6739
|
kind: lastStudioResponse.kind,
|
|
5105
6740
|
markdown: lastStudioResponse.markdown,
|
|
5106
6741
|
timestamp: lastStudioResponse.timestamp,
|
|
6742
|
+
responseHistory: studioResponseHistory,
|
|
5107
6743
|
});
|
|
5108
6744
|
return;
|
|
5109
6745
|
}
|
|
@@ -5201,6 +6837,90 @@ export default function (pi: ExtensionAPI) {
|
|
|
5201
6837
|
return;
|
|
5202
6838
|
}
|
|
5203
6839
|
|
|
6840
|
+
if (msg.type === "compact_request") {
|
|
6841
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
6842
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
6843
|
+
return;
|
|
6844
|
+
}
|
|
6845
|
+
if (isStudioBusy()) {
|
|
6846
|
+
sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
|
|
6847
|
+
return;
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6850
|
+
const compactCtx = lastCommandCtx;
|
|
6851
|
+
if (!compactCtx) {
|
|
6852
|
+
sendToClient(client, {
|
|
6853
|
+
type: "error",
|
|
6854
|
+
requestId: msg.requestId,
|
|
6855
|
+
message: "No interactive pi context is available to run compaction.",
|
|
6856
|
+
});
|
|
6857
|
+
return;
|
|
6858
|
+
}
|
|
6859
|
+
|
|
6860
|
+
const customInstructions = typeof msg.customInstructions === "string" && msg.customInstructions.trim()
|
|
6861
|
+
? msg.customInstructions.trim()
|
|
6862
|
+
: undefined;
|
|
6863
|
+
if (customInstructions && customInstructions.length > 2000) {
|
|
6864
|
+
sendToClient(client, {
|
|
6865
|
+
type: "error",
|
|
6866
|
+
requestId: msg.requestId,
|
|
6867
|
+
message: "Compaction instructions are too long (max 2000 characters).",
|
|
6868
|
+
});
|
|
6869
|
+
return;
|
|
6870
|
+
}
|
|
6871
|
+
|
|
6872
|
+
compactInProgress = true;
|
|
6873
|
+
compactRequestId = msg.requestId;
|
|
6874
|
+
refreshContextUsage(compactCtx);
|
|
6875
|
+
emitDebugEvent("compact_start", {
|
|
6876
|
+
requestId: msg.requestId,
|
|
6877
|
+
hasCustomInstructions: Boolean(customInstructions),
|
|
6878
|
+
});
|
|
6879
|
+
broadcast({ type: "request_started", requestId: msg.requestId, kind: "compact" });
|
|
6880
|
+
broadcastState();
|
|
6881
|
+
|
|
6882
|
+
const finishCompaction = (result: { type: "compaction_completed" | "compaction_error"; message: string }) => {
|
|
6883
|
+
if (!compactInProgress || compactRequestId !== msg.requestId) return;
|
|
6884
|
+
clearCompactionState();
|
|
6885
|
+
refreshContextUsage(compactCtx);
|
|
6886
|
+
emitDebugEvent(result.type, { requestId: msg.requestId, message: result.message });
|
|
6887
|
+
broadcast({
|
|
6888
|
+
type: result.type,
|
|
6889
|
+
requestId: msg.requestId,
|
|
6890
|
+
message: result.message,
|
|
6891
|
+
busy: isStudioBusy(),
|
|
6892
|
+
contextTokens: contextUsageSnapshot.tokens,
|
|
6893
|
+
contextWindow: contextUsageSnapshot.contextWindow,
|
|
6894
|
+
contextPercent: contextUsageSnapshot.percent,
|
|
6895
|
+
});
|
|
6896
|
+
broadcastState();
|
|
6897
|
+
};
|
|
6898
|
+
|
|
6899
|
+
try {
|
|
6900
|
+
compactCtx.compact({
|
|
6901
|
+
customInstructions,
|
|
6902
|
+
onComplete: () => {
|
|
6903
|
+
finishCompaction({
|
|
6904
|
+
type: "compaction_completed",
|
|
6905
|
+
message: "Compaction completed.",
|
|
6906
|
+
});
|
|
6907
|
+
},
|
|
6908
|
+
onError: (error) => {
|
|
6909
|
+
finishCompaction({
|
|
6910
|
+
type: "compaction_error",
|
|
6911
|
+
message: `Compaction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6912
|
+
});
|
|
6913
|
+
},
|
|
6914
|
+
});
|
|
6915
|
+
} catch (error) {
|
|
6916
|
+
finishCompaction({
|
|
6917
|
+
type: "compaction_error",
|
|
6918
|
+
message: `Failed to start compaction: ${error instanceof Error ? error.message : String(error)}`,
|
|
6919
|
+
});
|
|
6920
|
+
}
|
|
6921
|
+
return;
|
|
6922
|
+
}
|
|
6923
|
+
|
|
5204
6924
|
if (msg.type === "save_as_request") {
|
|
5205
6925
|
if (!isValidRequestId(msg.requestId)) {
|
|
5206
6926
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
@@ -5412,6 +7132,84 @@ export default function (pi: ExtensionAPI) {
|
|
|
5412
7132
|
}
|
|
5413
7133
|
};
|
|
5414
7134
|
|
|
7135
|
+
const handleExportPdfRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
7136
|
+
let rawBody = "";
|
|
7137
|
+
try {
|
|
7138
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7139
|
+
} catch (error) {
|
|
7140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7141
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
7142
|
+
respondJson(res, status, { ok: false, error: message });
|
|
7143
|
+
return;
|
|
7144
|
+
}
|
|
7145
|
+
|
|
7146
|
+
let parsedBody: unknown;
|
|
7147
|
+
try {
|
|
7148
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
7149
|
+
} catch {
|
|
7150
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7151
|
+
return;
|
|
7152
|
+
}
|
|
7153
|
+
|
|
7154
|
+
const markdown =
|
|
7155
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { markdown?: unknown }).markdown === "string"
|
|
7156
|
+
? (parsedBody as { markdown: string }).markdown
|
|
7157
|
+
: null;
|
|
7158
|
+
if (markdown === null) {
|
|
7159
|
+
respondJson(res, 400, { ok: false, error: "Missing markdown string in request body." });
|
|
7160
|
+
return;
|
|
7161
|
+
}
|
|
7162
|
+
|
|
7163
|
+
if (markdown.length > PDF_EXPORT_MAX_CHARS) {
|
|
7164
|
+
respondJson(res, 413, {
|
|
7165
|
+
ok: false,
|
|
7166
|
+
error: `PDF export text exceeds ${PDF_EXPORT_MAX_CHARS} characters.`,
|
|
7167
|
+
});
|
|
7168
|
+
return;
|
|
7169
|
+
}
|
|
7170
|
+
|
|
7171
|
+
const sourcePath =
|
|
7172
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { sourcePath?: unknown }).sourcePath === "string"
|
|
7173
|
+
? (parsedBody as { sourcePath: string }).sourcePath
|
|
7174
|
+
: "";
|
|
7175
|
+
const userResourceDir =
|
|
7176
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
|
|
7177
|
+
? (parsedBody as { resourceDir: string }).resourceDir
|
|
7178
|
+
: "";
|
|
7179
|
+
const resourcePath = sourcePath ? dirname(sourcePath) : (userResourceDir || studioCwd || undefined);
|
|
7180
|
+
const requestedIsLatex =
|
|
7181
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { isLatex?: unknown }).isLatex === "boolean"
|
|
7182
|
+
? (parsedBody as { isLatex: boolean }).isLatex
|
|
7183
|
+
: null;
|
|
7184
|
+
const isLatex = requestedIsLatex ?? /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
7185
|
+
const requestedFilename =
|
|
7186
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { filenameHint?: unknown }).filenameHint === "string"
|
|
7187
|
+
? (parsedBody as { filenameHint: string }).filenameHint
|
|
7188
|
+
: "";
|
|
7189
|
+
const filename = sanitizePdfFilename(requestedFilename || (isLatex ? "studio-latex-preview.pdf" : "studio-preview.pdf"));
|
|
7190
|
+
|
|
7191
|
+
try {
|
|
7192
|
+
const pdf = await renderStudioPdfWithPandoc(markdown, isLatex, resourcePath);
|
|
7193
|
+
const safeAsciiName = filename
|
|
7194
|
+
.replace(/[\x00-\x1f\x7f]/g, "")
|
|
7195
|
+
.replace(/[;"\\]/g, "_")
|
|
7196
|
+
.replace(/\s+/g, " ")
|
|
7197
|
+
.trim() || "studio-preview.pdf";
|
|
7198
|
+
|
|
7199
|
+
res.writeHead(200, {
|
|
7200
|
+
"Content-Type": "application/pdf",
|
|
7201
|
+
"Cache-Control": "no-store",
|
|
7202
|
+
"X-Content-Type-Options": "nosniff",
|
|
7203
|
+
"Content-Disposition": `attachment; filename="${safeAsciiName}"; filename*=UTF-8''${encodeURIComponent(filename)}`,
|
|
7204
|
+
"Content-Length": String(pdf.length),
|
|
7205
|
+
});
|
|
7206
|
+
res.end(pdf);
|
|
7207
|
+
} catch (error) {
|
|
7208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7209
|
+
respondJson(res, 500, { ok: false, error: `PDF export failed: ${message}` });
|
|
7210
|
+
}
|
|
7211
|
+
};
|
|
7212
|
+
|
|
5415
7213
|
const handleHttpRequest = (req: IncomingMessage, res: ServerResponse) => {
|
|
5416
7214
|
if (!serverState) {
|
|
5417
7215
|
respondText(res, 503, "Studio server not ready");
|
|
@@ -5461,6 +7259,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
5461
7259
|
return;
|
|
5462
7260
|
}
|
|
5463
7261
|
|
|
7262
|
+
if (requestUrl.pathname === "/export-pdf") {
|
|
7263
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
7264
|
+
if (token !== serverState.token) {
|
|
7265
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
7266
|
+
return;
|
|
7267
|
+
}
|
|
7268
|
+
|
|
7269
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7270
|
+
if (method !== "POST") {
|
|
7271
|
+
res.setHeader("Allow", "POST");
|
|
7272
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
7273
|
+
return;
|
|
7274
|
+
}
|
|
7275
|
+
|
|
7276
|
+
void handleExportPdfRequest(req, res).catch((error) => {
|
|
7277
|
+
respondJson(res, 500, {
|
|
7278
|
+
ok: false,
|
|
7279
|
+
error: `PDF export failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7280
|
+
});
|
|
7281
|
+
});
|
|
7282
|
+
return;
|
|
7283
|
+
}
|
|
7284
|
+
|
|
5464
7285
|
if (requestUrl.pathname !== "/") {
|
|
5465
7286
|
respondText(res, 404, "Not found");
|
|
5466
7287
|
return;
|
|
@@ -5480,7 +7301,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
5480
7301
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
5481
7302
|
"Cross-Origin-Resource-Policy": "same-origin",
|
|
5482
7303
|
});
|
|
5483
|
-
|
|
7304
|
+
refreshContextUsage();
|
|
7305
|
+
res.end(buildStudioHtml(initialStudioDocument, lastCommandCtx?.ui.theme, currentModelLabel, terminalSessionLabel, contextUsageSnapshot));
|
|
5484
7306
|
};
|
|
5485
7307
|
|
|
5486
7308
|
const ensureServer = async (): Promise<StudioServerState> => {
|
|
@@ -5572,9 +7394,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
5572
7394
|
|
|
5573
7395
|
serverState = state;
|
|
5574
7396
|
|
|
5575
|
-
// Periodically check for theme changes and push to all clients
|
|
7397
|
+
// Periodically check for theme/model metadata changes and push to all clients
|
|
5576
7398
|
const themeCheckInterval = setInterval(() => {
|
|
5577
|
-
if (!
|
|
7399
|
+
if (!serverState || serverState.clients.size === 0) return;
|
|
7400
|
+
|
|
7401
|
+
try {
|
|
7402
|
+
const previousModelLabel = currentModelLabel;
|
|
7403
|
+
const previousTerminalLabel = terminalSessionLabel;
|
|
7404
|
+
refreshRuntimeMetadata();
|
|
7405
|
+
if (currentModelLabel !== previousModelLabel || terminalSessionLabel !== previousTerminalLabel) {
|
|
7406
|
+
broadcastState();
|
|
7407
|
+
}
|
|
7408
|
+
} catch {
|
|
7409
|
+
// Ignore metadata read errors
|
|
7410
|
+
}
|
|
7411
|
+
|
|
7412
|
+
if (!lastCommandCtx?.ui?.theme) return;
|
|
5578
7413
|
try {
|
|
5579
7414
|
const style = getStudioThemeStyle(lastCommandCtx.ui.theme);
|
|
5580
7415
|
const vars = buildThemeCssVars(style);
|
|
@@ -5598,6 +7433,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5598
7433
|
const stopServer = async () => {
|
|
5599
7434
|
if (!serverState) return;
|
|
5600
7435
|
clearActiveRequest();
|
|
7436
|
+
clearCompactionState();
|
|
5601
7437
|
closeAllClients(1001, "Server shutting down");
|
|
5602
7438
|
|
|
5603
7439
|
const state = serverState;
|
|
@@ -5620,29 +7456,58 @@ export default function (pi: ExtensionAPI) {
|
|
|
5620
7456
|
};
|
|
5621
7457
|
|
|
5622
7458
|
const hydrateLatestAssistant = (entries: SessionEntry[]) => {
|
|
5623
|
-
|
|
5624
|
-
if (!latest) return;
|
|
5625
|
-
lastStudioResponse = {
|
|
5626
|
-
markdown: latest,
|
|
5627
|
-
timestamp: Date.now(),
|
|
5628
|
-
kind: inferStudioResponseKind(latest),
|
|
5629
|
-
};
|
|
7459
|
+
syncStudioResponseHistory(entries);
|
|
5630
7460
|
};
|
|
5631
7461
|
|
|
5632
7462
|
pi.on("session_start", async (_event, ctx) => {
|
|
5633
7463
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
7464
|
+
clearCompactionState();
|
|
5634
7465
|
agentBusy = false;
|
|
5635
|
-
|
|
7466
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
7467
|
+
refreshContextUsage(ctx);
|
|
7468
|
+
emitDebugEvent("session_start", {
|
|
7469
|
+
entryCount: ctx.sessionManager.getBranch().length,
|
|
7470
|
+
modelLabel: currentModelLabel,
|
|
7471
|
+
terminalSessionLabel,
|
|
7472
|
+
});
|
|
5636
7473
|
setTerminalActivity("idle");
|
|
7474
|
+
broadcastResponseHistory();
|
|
5637
7475
|
});
|
|
5638
7476
|
|
|
5639
7477
|
pi.on("session_switch", async (_event, ctx) => {
|
|
5640
7478
|
clearActiveRequest({ notify: "Session switched. Studio request state cleared.", level: "warning" });
|
|
7479
|
+
clearCompactionState();
|
|
5641
7480
|
lastCommandCtx = null;
|
|
5642
7481
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
5643
7482
|
agentBusy = false;
|
|
5644
|
-
|
|
7483
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
7484
|
+
refreshContextUsage(ctx);
|
|
7485
|
+
emitDebugEvent("session_switch", {
|
|
7486
|
+
entryCount: ctx.sessionManager.getBranch().length,
|
|
7487
|
+
modelLabel: currentModelLabel,
|
|
7488
|
+
terminalSessionLabel,
|
|
7489
|
+
});
|
|
5645
7490
|
setTerminalActivity("idle");
|
|
7491
|
+
broadcastResponseHistory();
|
|
7492
|
+
});
|
|
7493
|
+
|
|
7494
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
7495
|
+
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
7496
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
7497
|
+
refreshContextUsage(ctx);
|
|
7498
|
+
broadcastResponseHistory();
|
|
7499
|
+
broadcastState();
|
|
7500
|
+
});
|
|
7501
|
+
|
|
7502
|
+
pi.on("model_select", async (event, ctx) => {
|
|
7503
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: event.model });
|
|
7504
|
+
refreshContextUsage(ctx);
|
|
7505
|
+
emitDebugEvent("model_select", {
|
|
7506
|
+
modelLabel: currentModelLabel,
|
|
7507
|
+
source: event.source,
|
|
7508
|
+
previousModel: formatModelLabel(event.previousModel),
|
|
7509
|
+
});
|
|
7510
|
+
broadcastState();
|
|
5646
7511
|
});
|
|
5647
7512
|
|
|
5648
7513
|
pi.on("agent_start", async () => {
|
|
@@ -5682,7 +7547,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5682
7547
|
}
|
|
5683
7548
|
});
|
|
5684
7549
|
|
|
5685
|
-
pi.on("message_end", async (event) => {
|
|
7550
|
+
pi.on("message_end", async (event, ctx) => {
|
|
5686
7551
|
const message = event.message as { stopReason?: string; role?: string };
|
|
5687
7552
|
const stopReason = typeof message.stopReason === "string" ? message.stopReason : "";
|
|
5688
7553
|
const role = typeof message.role === "string" ? message.role : "";
|
|
@@ -5708,12 +7573,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
5708
7573
|
|
|
5709
7574
|
if (!markdown) return;
|
|
5710
7575
|
|
|
7576
|
+
syncStudioResponseHistory(ctx.sessionManager.getBranch());
|
|
7577
|
+
refreshContextUsage(ctx);
|
|
7578
|
+
const latestHistoryItem = studioResponseHistory[studioResponseHistory.length - 1];
|
|
7579
|
+
if (!latestHistoryItem || latestHistoryItem.markdown !== markdown) {
|
|
7580
|
+
const fallbackPrompt = studioResponseHistory.length > 0
|
|
7581
|
+
? studioResponseHistory[studioResponseHistory.length - 1]?.prompt ?? null
|
|
7582
|
+
: null;
|
|
7583
|
+
const fallbackHistoryItem: StudioResponseHistoryItem = {
|
|
7584
|
+
id: randomUUID(),
|
|
7585
|
+
markdown,
|
|
7586
|
+
timestamp: Date.now(),
|
|
7587
|
+
kind: inferStudioResponseKind(markdown),
|
|
7588
|
+
prompt: fallbackPrompt,
|
|
7589
|
+
};
|
|
7590
|
+
const nextHistory = [...studioResponseHistory, fallbackHistoryItem];
|
|
7591
|
+
studioResponseHistory = nextHistory.slice(-RESPONSE_HISTORY_LIMIT);
|
|
7592
|
+
}
|
|
7593
|
+
|
|
7594
|
+
const latestItem = studioResponseHistory[studioResponseHistory.length - 1];
|
|
7595
|
+
const responseTimestamp = latestItem?.timestamp ?? Date.now();
|
|
7596
|
+
|
|
5711
7597
|
if (activeRequest) {
|
|
5712
7598
|
const requestId = activeRequest.id;
|
|
5713
7599
|
const kind = activeRequest.kind;
|
|
5714
7600
|
lastStudioResponse = {
|
|
5715
7601
|
markdown,
|
|
5716
|
-
timestamp:
|
|
7602
|
+
timestamp: responseTimestamp,
|
|
5717
7603
|
kind,
|
|
5718
7604
|
};
|
|
5719
7605
|
emitDebugEvent("broadcast_response", {
|
|
@@ -5728,7 +7614,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
5728
7614
|
kind,
|
|
5729
7615
|
markdown,
|
|
5730
7616
|
timestamp: lastStudioResponse.timestamp,
|
|
7617
|
+
responseHistory: studioResponseHistory,
|
|
5731
7618
|
});
|
|
7619
|
+
broadcastResponseHistory();
|
|
5732
7620
|
clearActiveRequest();
|
|
5733
7621
|
return;
|
|
5734
7622
|
}
|
|
@@ -5736,7 +7624,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5736
7624
|
const inferredKind = inferStudioResponseKind(markdown);
|
|
5737
7625
|
lastStudioResponse = {
|
|
5738
7626
|
markdown,
|
|
5739
|
-
timestamp:
|
|
7627
|
+
timestamp: responseTimestamp,
|
|
5740
7628
|
kind: inferredKind,
|
|
5741
7629
|
};
|
|
5742
7630
|
emitDebugEvent("broadcast_latest_response", {
|
|
@@ -5749,11 +7637,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
5749
7637
|
kind: inferredKind,
|
|
5750
7638
|
markdown,
|
|
5751
7639
|
timestamp: lastStudioResponse.timestamp,
|
|
7640
|
+
responseHistory: studioResponseHistory,
|
|
5752
7641
|
});
|
|
7642
|
+
broadcastResponseHistory();
|
|
5753
7643
|
});
|
|
5754
7644
|
|
|
5755
7645
|
pi.on("agent_end", async () => {
|
|
5756
7646
|
agentBusy = false;
|
|
7647
|
+
refreshContextUsage();
|
|
5757
7648
|
emitDebugEvent("agent_end", { activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
|
|
5758
7649
|
setTerminalActivity("idle");
|
|
5759
7650
|
if (activeRequest) {
|
|
@@ -5770,12 +7661,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
5770
7661
|
pi.on("session_shutdown", async () => {
|
|
5771
7662
|
lastCommandCtx = null;
|
|
5772
7663
|
agentBusy = false;
|
|
7664
|
+
clearCompactionState();
|
|
5773
7665
|
setTerminalActivity("idle");
|
|
5774
7666
|
await stopServer();
|
|
5775
7667
|
});
|
|
5776
7668
|
|
|
5777
7669
|
pi.registerCommand("studio", {
|
|
5778
|
-
description: "Open
|
|
7670
|
+
description: "Open pi Studio browser UI (/studio, /studio <file>, /studio --blank, /studio --last)",
|
|
5779
7671
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
5780
7672
|
const trimmed = args.trim();
|
|
5781
7673
|
|
|
@@ -5813,7 +7705,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
5813
7705
|
|
|
5814
7706
|
await ctx.waitForIdle();
|
|
5815
7707
|
lastCommandCtx = ctx;
|
|
5816
|
-
|
|
7708
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
7709
|
+
refreshContextUsage(ctx);
|
|
7710
|
+
syncStudioResponseHistory(ctx.sessionManager.getBranch());
|
|
7711
|
+
broadcastState();
|
|
7712
|
+
broadcastResponseHistory();
|
|
7713
|
+
void maybeNotifyUpdateAvailable(ctx);
|
|
5817
7714
|
// Seed theme vars so first ping doesn't trigger a false update
|
|
5818
7715
|
try {
|
|
5819
7716
|
const currentStyle = getStudioThemeStyle(ctx.ui.theme);
|
|
@@ -5901,14 +7798,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
5901
7798
|
try {
|
|
5902
7799
|
await openUrlInDefaultBrowser(url);
|
|
5903
7800
|
if (initialStudioDocument?.source === "file") {
|
|
5904
|
-
ctx.ui.notify(`Opened
|
|
7801
|
+
ctx.ui.notify(`Opened pi Studio with file loaded: ${initialStudioDocument.label}`, "info");
|
|
5905
7802
|
} else if (initialStudioDocument?.source === "last-response") {
|
|
5906
7803
|
ctx.ui.notify(
|
|
5907
|
-
`Opened
|
|
7804
|
+
`Opened pi Studio with last model response (${initialStudioDocument.text.length} chars).`,
|
|
5908
7805
|
"info",
|
|
5909
7806
|
);
|
|
5910
7807
|
} else {
|
|
5911
|
-
ctx.ui.notify("Opened
|
|
7808
|
+
ctx.ui.notify("Opened pi Studio with blank editor.", "info");
|
|
5912
7809
|
}
|
|
5913
7810
|
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
5914
7811
|
} catch (error) {
|