pi-studio 0.4.3 → 0.5.1
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 +37 -3
- package/README.md +36 -93
- package/WORKFLOW.md +16 -5
- package/assets/screenshots/dark-workspace.png +0 -0
- package/assets/screenshots/light-workspace.png +0 -0
- package/index.ts +1508 -159
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { WebSocketServer, WebSocket, type RawData } from "ws";
|
|
|
11
11
|
|
|
12
12
|
type Lens = "writing" | "code";
|
|
13
13
|
type RequestedLens = Lens | "auto";
|
|
14
|
-
type StudioRequestKind = "critique" | "annotation" | "direct";
|
|
14
|
+
type StudioRequestKind = "critique" | "annotation" | "direct" | "compact";
|
|
15
15
|
type StudioSourceKind = "file" | "last-response" | "blank";
|
|
16
16
|
type TerminalActivityPhase = "idle" | "running" | "tool" | "responding";
|
|
17
17
|
|
|
@@ -36,6 +36,20 @@ interface LastStudioResponse {
|
|
|
36
36
|
kind: StudioRequestKind;
|
|
37
37
|
}
|
|
38
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
|
+
|
|
39
53
|
interface InitialStudioDocument {
|
|
40
54
|
text: string;
|
|
41
55
|
label: string;
|
|
@@ -74,6 +88,12 @@ interface SendRunRequestMessage {
|
|
|
74
88
|
text: string;
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
interface CompactRequestMessage {
|
|
92
|
+
type: "compact_request";
|
|
93
|
+
requestId: string;
|
|
94
|
+
customInstructions?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
interface SaveAsRequestMessage {
|
|
78
98
|
type: "save_as_request";
|
|
79
99
|
requestId: string;
|
|
@@ -105,6 +125,7 @@ type IncomingStudioMessage =
|
|
|
105
125
|
| CritiqueRequestMessage
|
|
106
126
|
| AnnotationRequestMessage
|
|
107
127
|
| SendRunRequestMessage
|
|
128
|
+
| CompactRequestMessage
|
|
108
129
|
| SaveAsRequestMessage
|
|
109
130
|
| SaveOverRequestMessage
|
|
110
131
|
| SendToEditorRequestMessage
|
|
@@ -114,6 +135,8 @@ const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
114
135
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
115
136
|
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
116
137
|
const REQUEST_BODY_MAX_BYTES = 1_000_000;
|
|
138
|
+
const RESPONSE_HISTORY_LIMIT = 30;
|
|
139
|
+
const UPDATE_CHECK_TIMEOUT_MS = 1800;
|
|
117
140
|
|
|
118
141
|
const PDF_PREAMBLE = `\\usepackage{titlesec}
|
|
119
142
|
\\titleformat{\\section}{\\Large\\bfseries\\sffamily}{}{0pt}{}[\\vspace{2pt}\\titlerule]
|
|
@@ -674,6 +697,93 @@ function writeStudioFile(pathArg: string, cwd: string, content: string):
|
|
|
674
697
|
}
|
|
675
698
|
}
|
|
676
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
|
+
|
|
677
787
|
function normalizeMathDelimitersInSegment(markdown: string): string {
|
|
678
788
|
let normalized = markdown.replace(/\\\[\s*([\s\S]*?)\s*\\\]/g, (_match, expr: string) => {
|
|
679
789
|
const content = expr.trim();
|
|
@@ -1159,6 +1269,116 @@ function extractLatestAssistantFromEntries(entries: SessionEntry[]): string | nu
|
|
|
1159
1269
|
return null;
|
|
1160
1270
|
}
|
|
1161
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
|
+
|
|
1162
1382
|
function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
1163
1383
|
let parsed: unknown;
|
|
1164
1384
|
try {
|
|
@@ -1204,6 +1424,18 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
1204
1424
|
};
|
|
1205
1425
|
}
|
|
1206
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
|
+
|
|
1207
1439
|
if (
|
|
1208
1440
|
msg.type === "save_as_request" &&
|
|
1209
1441
|
typeof msg.requestId === "string" &&
|
|
@@ -1501,6 +1733,7 @@ function buildStudioHtml(
|
|
|
1501
1733
|
theme?: Theme,
|
|
1502
1734
|
initialModelLabel?: string,
|
|
1503
1735
|
initialTerminalLabel?: string,
|
|
1736
|
+
initialContextUsage?: StudioContextUsageSnapshot,
|
|
1504
1737
|
): string {
|
|
1505
1738
|
const initialText = escapeHtmlForInline(initialDocument?.text ?? "");
|
|
1506
1739
|
const initialSource = initialDocument?.source ?? "blank";
|
|
@@ -1508,6 +1741,18 @@ function buildStudioHtml(
|
|
|
1508
1741
|
const initialPath = escapeHtmlForInline(initialDocument?.path ?? "");
|
|
1509
1742
|
const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
|
|
1510
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
|
+
: "";
|
|
1511
1756
|
const style = getStudioThemeStyle(theme);
|
|
1512
1757
|
const vars = buildThemeCssVars(style);
|
|
1513
1758
|
const mermaidConfig = {
|
|
@@ -1544,7 +1789,7 @@ function buildStudioHtml(
|
|
|
1544
1789
|
<head>
|
|
1545
1790
|
<meta charset="utf-8" />
|
|
1546
1791
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
1547
|
-
<title>
|
|
1792
|
+
<title>pi Studio</title>
|
|
1548
1793
|
<style>
|
|
1549
1794
|
:root {
|
|
1550
1795
|
${cssVarsBlock}
|
|
@@ -1707,6 +1952,31 @@ ${cssVarsBlock}
|
|
|
1707
1952
|
background: var(--panel-2);
|
|
1708
1953
|
font-weight: 600;
|
|
1709
1954
|
font-size: 14px;
|
|
1955
|
+
display: flex;
|
|
1956
|
+
align-items: center;
|
|
1957
|
+
justify-content: space-between;
|
|
1958
|
+
gap: 8px;
|
|
1959
|
+
flex-wrap: wrap;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
.section-header-main {
|
|
1963
|
+
display: inline-flex;
|
|
1964
|
+
align-items: center;
|
|
1965
|
+
min-width: 0;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
.section-header-actions {
|
|
1969
|
+
display: inline-flex;
|
|
1970
|
+
align-items: center;
|
|
1971
|
+
gap: 8px;
|
|
1972
|
+
flex-wrap: wrap;
|
|
1973
|
+
justify-content: flex-end;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
.section-header-actions button {
|
|
1977
|
+
padding: 6px 9px;
|
|
1978
|
+
font-size: 12px;
|
|
1979
|
+
border-radius: 7px;
|
|
1710
1980
|
}
|
|
1711
1981
|
|
|
1712
1982
|
.section-header select {
|
|
@@ -1738,6 +2008,7 @@ ${cssVarsBlock}
|
|
|
1738
2008
|
padding: 10px;
|
|
1739
2009
|
font-size: 13px;
|
|
1740
2010
|
line-height: 1.45;
|
|
2011
|
+
tab-size: 2;
|
|
1741
2012
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1742
2013
|
resize: vertical;
|
|
1743
2014
|
}
|
|
@@ -1788,10 +2059,19 @@ ${cssVarsBlock}
|
|
|
1788
2059
|
}
|
|
1789
2060
|
|
|
1790
2061
|
.source-actions {
|
|
2062
|
+
display: flex;
|
|
2063
|
+
flex-direction: column;
|
|
2064
|
+
gap: 6px;
|
|
2065
|
+
align-items: stretch;
|
|
2066
|
+
width: 100%;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
.source-actions-row {
|
|
1791
2070
|
display: flex;
|
|
1792
2071
|
gap: 6px;
|
|
1793
2072
|
flex-wrap: wrap;
|
|
1794
2073
|
align-items: center;
|
|
2074
|
+
min-width: 0;
|
|
1795
2075
|
}
|
|
1796
2076
|
|
|
1797
2077
|
.source-actions button,
|
|
@@ -1876,6 +2156,7 @@ ${cssVarsBlock}
|
|
|
1876
2156
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1877
2157
|
font-size: 13px;
|
|
1878
2158
|
line-height: 1.45;
|
|
2159
|
+
tab-size: 2;
|
|
1879
2160
|
color: var(--text);
|
|
1880
2161
|
background: transparent;
|
|
1881
2162
|
}
|
|
@@ -1975,6 +2256,27 @@ ${cssVarsBlock}
|
|
|
1975
2256
|
color: var(--md-link-url);
|
|
1976
2257
|
}
|
|
1977
2258
|
|
|
2259
|
+
.hl-annotation {
|
|
2260
|
+
color: var(--accent);
|
|
2261
|
+
background: var(--accent-soft);
|
|
2262
|
+
border: 1px solid var(--marker-border);
|
|
2263
|
+
border-radius: 4px;
|
|
2264
|
+
padding: 0 3px;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
.hl-annotation-muted {
|
|
2268
|
+
color: var(--muted);
|
|
2269
|
+
opacity: 0.65;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
.annotation-preview-marker {
|
|
2273
|
+
color: var(--accent);
|
|
2274
|
+
background: var(--accent-soft);
|
|
2275
|
+
border: 1px solid var(--marker-border);
|
|
2276
|
+
border-radius: 4px;
|
|
2277
|
+
padding: 0 4px;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
1978
2280
|
#sourcePreview {
|
|
1979
2281
|
flex: 1 1 auto;
|
|
1980
2282
|
min-height: 0;
|
|
@@ -2317,11 +2619,29 @@ ${cssVarsBlock}
|
|
|
2317
2619
|
}
|
|
2318
2620
|
|
|
2319
2621
|
.response-actions {
|
|
2622
|
+
display: flex;
|
|
2623
|
+
flex-direction: column;
|
|
2624
|
+
align-items: stretch;
|
|
2625
|
+
gap: 8px;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
.response-actions-row {
|
|
2320
2629
|
display: flex;
|
|
2321
2630
|
align-items: center;
|
|
2322
|
-
justify-content: flex-start;
|
|
2323
2631
|
gap: 8px;
|
|
2324
2632
|
flex-wrap: wrap;
|
|
2633
|
+
min-width: 0;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
.response-actions-row.history-row {
|
|
2637
|
+
flex-wrap: nowrap;
|
|
2638
|
+
overflow-x: auto;
|
|
2639
|
+
padding-bottom: 2px;
|
|
2640
|
+
scrollbar-width: thin;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
.response-actions-row.history-row > * {
|
|
2644
|
+
flex: 0 0 auto;
|
|
2325
2645
|
}
|
|
2326
2646
|
|
|
2327
2647
|
footer {
|
|
@@ -2334,7 +2654,7 @@ ${cssVarsBlock}
|
|
|
2334
2654
|
display: grid;
|
|
2335
2655
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
2336
2656
|
grid-template-areas:
|
|
2337
|
-
"status
|
|
2657
|
+
"status status"
|
|
2338
2658
|
"meta hint";
|
|
2339
2659
|
column-gap: 12px;
|
|
2340
2660
|
row-gap: 3px;
|
|
@@ -2386,11 +2706,19 @@ ${cssVarsBlock}
|
|
|
2386
2706
|
justify-self: start;
|
|
2387
2707
|
color: var(--muted);
|
|
2388
2708
|
font-size: 11px;
|
|
2709
|
+
text-align: left;
|
|
2710
|
+
max-width: 100%;
|
|
2711
|
+
display: inline-flex;
|
|
2712
|
+
align-items: center;
|
|
2713
|
+
gap: 8px;
|
|
2714
|
+
min-width: 0;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
.footer-meta-text {
|
|
2718
|
+
min-width: 0;
|
|
2389
2719
|
white-space: nowrap;
|
|
2390
2720
|
overflow: hidden;
|
|
2391
2721
|
text-overflow: ellipsis;
|
|
2392
|
-
text-align: left;
|
|
2393
|
-
max-width: 100%;
|
|
2394
2722
|
}
|
|
2395
2723
|
|
|
2396
2724
|
.shortcut-hint {
|
|
@@ -2403,6 +2731,25 @@ ${cssVarsBlock}
|
|
|
2403
2731
|
text-align: right;
|
|
2404
2732
|
font-style: normal;
|
|
2405
2733
|
opacity: 0.9;
|
|
2734
|
+
display: inline-flex;
|
|
2735
|
+
align-items: center;
|
|
2736
|
+
gap: 8px;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
.footer-compact-btn {
|
|
2740
|
+
padding: 4px 8px;
|
|
2741
|
+
font-size: 11px;
|
|
2742
|
+
line-height: 1.2;
|
|
2743
|
+
border-radius: 999px;
|
|
2744
|
+
border: 1px solid var(--border-muted);
|
|
2745
|
+
background: var(--panel-2);
|
|
2746
|
+
color: var(--text);
|
|
2747
|
+
white-space: nowrap;
|
|
2748
|
+
flex: 0 0 auto;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
.footer-compact-btn:not(:disabled):hover {
|
|
2752
|
+
background: var(--panel);
|
|
2406
2753
|
}
|
|
2407
2754
|
|
|
2408
2755
|
#status.error { color: var(--error); }
|
|
@@ -2427,6 +2774,8 @@ ${cssVarsBlock}
|
|
|
2427
2774
|
justify-self: start;
|
|
2428
2775
|
text-align: left;
|
|
2429
2776
|
white-space: normal;
|
|
2777
|
+
flex-wrap: wrap;
|
|
2778
|
+
gap: 6px;
|
|
2430
2779
|
}
|
|
2431
2780
|
}
|
|
2432
2781
|
|
|
@@ -2437,9 +2786,9 @@ ${cssVarsBlock}
|
|
|
2437
2786
|
}
|
|
2438
2787
|
</style>
|
|
2439
2788
|
</head>
|
|
2440
|
-
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}">
|
|
2789
|
+
<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}">
|
|
2441
2790
|
<header>
|
|
2442
|
-
<h1><span class="app-logo" aria-hidden="true">π</span>
|
|
2791
|
+
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">Editor & Response Workspace</span></h1>
|
|
2443
2792
|
<div class="controls">
|
|
2444
2793
|
<button id="saveAsBtn" type="button" title="Save editor content to a new file path.">Save editor as…</button>
|
|
2445
2794
|
<button id="saveOverBtn" type="button" title="Overwrite current file with editor content." disabled>Save editor</button>
|
|
@@ -2468,49 +2817,61 @@ ${cssVarsBlock}
|
|
|
2468
2817
|
<span id="syncBadge" class="source-badge sync-badge">No response loaded</span>
|
|
2469
2818
|
</div>
|
|
2470
2819
|
<div class="source-actions">
|
|
2471
|
-
<
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
<
|
|
2475
|
-
<
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
<
|
|
2484
|
-
<
|
|
2485
|
-
</
|
|
2486
|
-
<
|
|
2487
|
-
<
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
<
|
|
2493
|
-
<
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
<
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2820
|
+
<div class="source-actions-row">
|
|
2821
|
+
<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>
|
|
2822
|
+
<button id="copyDraftBtn" type="button">Copy editor text</button>
|
|
2823
|
+
<button id="sendEditorBtn" type="button">Send to pi editor</button>
|
|
2824
|
+
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
2825
|
+
</div>
|
|
2826
|
+
<div class="source-actions-row">
|
|
2827
|
+
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Insert annotated reply header</button>
|
|
2828
|
+
<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.">
|
|
2829
|
+
<option value="on" selected>Annotations: On</option>
|
|
2830
|
+
<option value="off">Annotations: Hidden</option>
|
|
2831
|
+
</select>
|
|
2832
|
+
<button id="stripAnnotationsBtn" type="button" title="Destructively remove all [an: ...] markers from editor text.">Strip annotations…</button>
|
|
2833
|
+
<button id="saveAnnotatedBtn" type="button" title="Save full editor content (including [an: ...] markers) as a .annotated.md file.">Save .annotated.md</button>
|
|
2834
|
+
</div>
|
|
2835
|
+
<div class="source-actions-row">
|
|
2836
|
+
<select id="lensSelect" aria-label="Critique focus">
|
|
2837
|
+
<option value="auto" selected>Critique focus: Auto</option>
|
|
2838
|
+
<option value="writing">Critique focus: Writing</option>
|
|
2839
|
+
<option value="code">Critique focus: Code</option>
|
|
2840
|
+
</select>
|
|
2841
|
+
<button id="critiqueBtn" type="button">Critique editor text</button>
|
|
2842
|
+
<select id="highlightSelect" aria-label="Editor syntax highlighting">
|
|
2843
|
+
<option value="off">Syntax highlight: Off</option>
|
|
2844
|
+
<option value="on" selected>Syntax highlight: On</option>
|
|
2845
|
+
</select>
|
|
2846
|
+
<select id="langSelect" aria-label="Highlight language">
|
|
2847
|
+
<option value="markdown" selected>Lang: Markdown</option>
|
|
2848
|
+
<option value="javascript">Lang: JavaScript</option>
|
|
2849
|
+
<option value="typescript">Lang: TypeScript</option>
|
|
2850
|
+
<option value="python">Lang: Python</option>
|
|
2851
|
+
<option value="bash">Lang: Bash</option>
|
|
2852
|
+
<option value="json">Lang: JSON</option>
|
|
2853
|
+
<option value="rust">Lang: Rust</option>
|
|
2854
|
+
<option value="c">Lang: C</option>
|
|
2855
|
+
<option value="cpp">Lang: C++</option>
|
|
2856
|
+
<option value="julia">Lang: Julia</option>
|
|
2857
|
+
<option value="fortran">Lang: Fortran</option>
|
|
2858
|
+
<option value="r">Lang: R</option>
|
|
2859
|
+
<option value="matlab">Lang: MATLAB</option>
|
|
2860
|
+
<option value="latex">Lang: LaTeX</option>
|
|
2861
|
+
<option value="diff">Lang: Diff</option>
|
|
2862
|
+
<option value="java">Lang: Java</option>
|
|
2863
|
+
<option value="go">Lang: Go</option>
|
|
2864
|
+
<option value="ruby">Lang: Ruby</option>
|
|
2865
|
+
<option value="swift">Lang: Swift</option>
|
|
2866
|
+
<option value="html">Lang: HTML</option>
|
|
2867
|
+
<option value="css">Lang: CSS</option>
|
|
2868
|
+
<option value="xml">Lang: XML</option>
|
|
2869
|
+
<option value="yaml">Lang: YAML</option>
|
|
2870
|
+
<option value="toml">Lang: TOML</option>
|
|
2871
|
+
<option value="lua">Lang: Lua</option>
|
|
2872
|
+
<option value="text">Lang: Plain Text</option>
|
|
2873
|
+
</select>
|
|
2874
|
+
</div>
|
|
2514
2875
|
</div>
|
|
2515
2876
|
</div>
|
|
2516
2877
|
<div id="sourceEditorWrap" class="editor-highlight-wrap">
|
|
@@ -2523,11 +2884,16 @@ ${cssVarsBlock}
|
|
|
2523
2884
|
|
|
2524
2885
|
<section id="rightPane">
|
|
2525
2886
|
<div id="rightSectionHeader" class="section-header">
|
|
2526
|
-
<
|
|
2527
|
-
<
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2887
|
+
<div class="section-header-main">
|
|
2888
|
+
<select id="rightViewSelect" aria-label="Response view mode">
|
|
2889
|
+
<option value="markdown">Response (Raw)</option>
|
|
2890
|
+
<option value="preview" selected>Response (Preview)</option>
|
|
2891
|
+
<option value="editor-preview">Editor (Preview)</option>
|
|
2892
|
+
</select>
|
|
2893
|
+
</div>
|
|
2894
|
+
<div class="section-header-actions">
|
|
2895
|
+
<button id="exportPdfBtn" type="button" title="Export the current right-pane preview as PDF via pandoc + xelatex.">Export right preview as PDF</button>
|
|
2896
|
+
</div>
|
|
2531
2897
|
</div>
|
|
2532
2898
|
<div class="reference-meta">
|
|
2533
2899
|
<span id="referenceBadge" class="source-badge">Latest response: none</span>
|
|
@@ -2535,20 +2901,29 @@ ${cssVarsBlock}
|
|
|
2535
2901
|
<div id="critiqueView" class="panel-scroll rendered-markdown"><pre class="plain-markdown">No response yet.</pre></div>
|
|
2536
2902
|
<div class="response-wrap">
|
|
2537
2903
|
<div id="responseActions" class="response-actions">
|
|
2538
|
-
<
|
|
2539
|
-
<
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
<
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
<
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2904
|
+
<div class="response-actions-row">
|
|
2905
|
+
<select id="followSelect" aria-label="Auto-update response">
|
|
2906
|
+
<option value="on" selected>Auto-update response: On</option>
|
|
2907
|
+
<option value="off">Auto-update response: Off</option>
|
|
2908
|
+
</select>
|
|
2909
|
+
<select id="responseHighlightSelect" aria-label="Response markdown highlighting">
|
|
2910
|
+
<option value="off">Syntax highlight: Off</option>
|
|
2911
|
+
<option value="on" selected>Syntax highlight: On</option>
|
|
2912
|
+
</select>
|
|
2913
|
+
</div>
|
|
2914
|
+
<div class="response-actions-row history-row">
|
|
2915
|
+
<button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
|
|
2916
|
+
<button id="historyPrevBtn" type="button" title="Show previous response in history.">◀ Prev response</button>
|
|
2917
|
+
<span id="historyIndexBadge" class="source-badge">History: 0/0</span>
|
|
2918
|
+
<button id="historyNextBtn" type="button" title="Show next response in history.">Next response ▶</button>
|
|
2919
|
+
</div>
|
|
2920
|
+
<div class="response-actions-row">
|
|
2921
|
+
<button id="loadHistoryPromptBtn" type="button" title="Load the prompt that generated the selected response into the editor.">Load response prompt into editor</button>
|
|
2922
|
+
<button id="loadResponseBtn" type="button">Load response into editor</button>
|
|
2923
|
+
<button id="loadCritiqueNotesBtn" type="button" hidden>Load critique notes into editor</button>
|
|
2924
|
+
<button id="loadCritiqueFullBtn" type="button" hidden>Load full critique into editor</button>
|
|
2925
|
+
<button id="copyResponseBtn" type="button">Copy response text</button>
|
|
2926
|
+
</div>
|
|
2552
2927
|
</div>
|
|
2553
2928
|
</div>
|
|
2554
2929
|
</section>
|
|
@@ -2556,7 +2931,7 @@ ${cssVarsBlock}
|
|
|
2556
2931
|
|
|
2557
2932
|
<footer>
|
|
2558
2933
|
<span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
|
|
2559
|
-
<span id="footerMeta" class="footer-meta">Model: ${initialModel} · Terminal: ${initialTerminal}</span>
|
|
2934
|
+
<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>
|
|
2560
2935
|
<span class="shortcut-hint">Focus pane: Cmd/Ctrl+Esc (or F10), Esc to exit · Run editor text: Cmd/Ctrl+Enter</span>
|
|
2561
2936
|
</footer>
|
|
2562
2937
|
|
|
@@ -2568,6 +2943,7 @@ ${cssVarsBlock}
|
|
|
2568
2943
|
const statusEl = document.getElementById("status");
|
|
2569
2944
|
const statusSpinnerEl = document.getElementById("statusSpinner");
|
|
2570
2945
|
const footerMetaEl = document.getElementById("footerMeta");
|
|
2946
|
+
const footerMetaTextEl = document.getElementById("footerMetaText");
|
|
2571
2947
|
const BRAILLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2572
2948
|
let spinnerTimer = null;
|
|
2573
2949
|
let spinnerFrameIndex = 0;
|
|
@@ -2631,14 +3007,22 @@ ${cssVarsBlock}
|
|
|
2631
3007
|
const loadCritiqueFullBtn = document.getElementById("loadCritiqueFullBtn");
|
|
2632
3008
|
const copyResponseBtn = document.getElementById("copyResponseBtn");
|
|
2633
3009
|
const exportPdfBtn = document.getElementById("exportPdfBtn");
|
|
3010
|
+
const historyPrevBtn = document.getElementById("historyPrevBtn");
|
|
3011
|
+
const historyNextBtn = document.getElementById("historyNextBtn");
|
|
3012
|
+
const historyIndexBadgeEl = document.getElementById("historyIndexBadge");
|
|
3013
|
+
const loadHistoryPromptBtn = document.getElementById("loadHistoryPromptBtn");
|
|
2634
3014
|
const saveAsBtn = document.getElementById("saveAsBtn");
|
|
2635
3015
|
const saveOverBtn = document.getElementById("saveOverBtn");
|
|
2636
3016
|
const sendEditorBtn = document.getElementById("sendEditorBtn");
|
|
2637
3017
|
const getEditorBtn = document.getElementById("getEditorBtn");
|
|
2638
3018
|
const sendRunBtn = document.getElementById("sendRunBtn");
|
|
2639
3019
|
const copyDraftBtn = document.getElementById("copyDraftBtn");
|
|
3020
|
+
const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
|
|
3021
|
+
const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
|
|
2640
3022
|
const highlightSelect = document.getElementById("highlightSelect");
|
|
2641
3023
|
const langSelect = document.getElementById("langSelect");
|
|
3024
|
+
const annotationModeSelect = document.getElementById("annotationModeSelect");
|
|
3025
|
+
const compactBtn = document.getElementById("compactBtn");
|
|
2642
3026
|
|
|
2643
3027
|
const initialSourceState = {
|
|
2644
3028
|
source: (document.body && document.body.dataset && document.body.dataset.initialSource) || "blank",
|
|
@@ -2666,6 +3050,8 @@ ${cssVarsBlock}
|
|
|
2666
3050
|
let latestResponseNormalized = "";
|
|
2667
3051
|
let latestCritiqueNotes = "";
|
|
2668
3052
|
let latestCritiqueNotesNormalized = "";
|
|
3053
|
+
let responseHistory = [];
|
|
3054
|
+
let responseHistoryIndex = -1;
|
|
2669
3055
|
let agentBusyFromServer = false;
|
|
2670
3056
|
let terminalActivityPhase = "idle";
|
|
2671
3057
|
let terminalActivityToolName = "";
|
|
@@ -2673,8 +3059,23 @@ ${cssVarsBlock}
|
|
|
2673
3059
|
let lastSpecificToolLabel = "";
|
|
2674
3060
|
let uiBusy = false;
|
|
2675
3061
|
let pdfExportInProgress = false;
|
|
3062
|
+
let compactInProgress = false;
|
|
2676
3063
|
let modelLabel = (document.body && document.body.dataset && document.body.dataset.modelLabel) || "none";
|
|
2677
3064
|
let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
|
|
3065
|
+
let contextTokens = null;
|
|
3066
|
+
let contextWindow = null;
|
|
3067
|
+
let contextPercent = null;
|
|
3068
|
+
|
|
3069
|
+
function parseFiniteNumber(value) {
|
|
3070
|
+
if (value == null || value === "") return null;
|
|
3071
|
+
const parsed = Number(value);
|
|
3072
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
|
|
3076
|
+
contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
|
|
3077
|
+
contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
|
|
3078
|
+
|
|
2678
3079
|
let sourceState = {
|
|
2679
3080
|
source: initialSourceState.source,
|
|
2680
3081
|
label: initialSourceState.label,
|
|
@@ -2725,6 +3126,7 @@ ${cssVarsBlock}
|
|
|
2725
3126
|
var SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
|
|
2726
3127
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
2727
3128
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
3129
|
+
const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
|
|
2728
3130
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
2729
3131
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
2730
3132
|
const previewPendingTimers = new WeakMap();
|
|
@@ -2737,6 +3139,8 @@ ${cssVarsBlock}
|
|
|
2737
3139
|
let editorLanguage = "markdown";
|
|
2738
3140
|
let responseHighlightEnabled = false;
|
|
2739
3141
|
let editorHighlightRenderRaf = null;
|
|
3142
|
+
let annotationsEnabled = true;
|
|
3143
|
+
const ANNOTATION_MARKER_REGEX = /\\[an:\\s*([^\\]\\n]+?)\\]/gi;
|
|
2740
3144
|
const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
2741
3145
|
const MERMAID_CONFIG = ${JSON.stringify(mermaidConfig)};
|
|
2742
3146
|
const MERMAID_UNAVAILABLE_MESSAGE = "Mermaid renderer unavailable. Showing mermaid blocks as code.";
|
|
@@ -2792,9 +3196,15 @@ ${cssVarsBlock}
|
|
|
2792
3196
|
if (typeof message.terminalActivityLabel === "string") summary.terminalActivityLabel = message.terminalActivityLabel;
|
|
2793
3197
|
if (typeof message.modelLabel === "string") summary.modelLabel = message.modelLabel;
|
|
2794
3198
|
if (typeof message.terminalSessionLabel === "string") summary.terminalSessionLabel = message.terminalSessionLabel;
|
|
3199
|
+
if (typeof message.contextTokens === "number") summary.contextTokens = message.contextTokens;
|
|
3200
|
+
if (typeof message.contextWindow === "number") summary.contextWindow = message.contextWindow;
|
|
3201
|
+
if (typeof message.contextPercent === "number") summary.contextPercent = message.contextPercent;
|
|
3202
|
+
if (typeof message.compactInProgress === "boolean") summary.compactInProgress = message.compactInProgress;
|
|
2795
3203
|
if (typeof message.stopReason === "string") summary.stopReason = message.stopReason;
|
|
2796
3204
|
if (typeof message.markdown === "string") summary.markdownLength = message.markdown.length;
|
|
2797
3205
|
if (typeof message.label === "string") summary.label = message.label;
|
|
3206
|
+
if (Array.isArray(message.responseHistory)) summary.responseHistoryCount = message.responseHistory.length;
|
|
3207
|
+
if (Array.isArray(message.items)) summary.itemsCount = message.items.length;
|
|
2798
3208
|
if (typeof message.details === "object" && message.details !== null) summary.details = message.details;
|
|
2799
3209
|
return summary;
|
|
2800
3210
|
}
|
|
@@ -2869,6 +3279,7 @@ ${cssVarsBlock}
|
|
|
2869
3279
|
if (kind === "annotation") return "sending annotated reply";
|
|
2870
3280
|
if (kind === "critique") return "running critique";
|
|
2871
3281
|
if (kind === "direct") return "running editor text";
|
|
3282
|
+
if (kind === "compact") return "compacting context";
|
|
2872
3283
|
if (kind === "send_to_editor") return "sending to pi editor";
|
|
2873
3284
|
if (kind === "get_from_editor") return "loading from pi editor";
|
|
2874
3285
|
if (kind === "save_as" || kind === "save_over") return "saving editor text";
|
|
@@ -2901,11 +3312,104 @@ ${cssVarsBlock}
|
|
|
2901
3312
|
return wsState !== "Disconnected" && (uiBusy || agentBusyFromServer || terminalActivityPhase !== "idle");
|
|
2902
3313
|
}
|
|
2903
3314
|
|
|
3315
|
+
function formatNumber(value) {
|
|
3316
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "?";
|
|
3317
|
+
try {
|
|
3318
|
+
return new Intl.NumberFormat().format(Math.round(value));
|
|
3319
|
+
} catch {
|
|
3320
|
+
return String(Math.round(value));
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function formatContextUsageText() {
|
|
3325
|
+
const hasWindow = typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0;
|
|
3326
|
+
const hasTokens = typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens >= 0;
|
|
3327
|
+
let percentValue = typeof contextPercent === "number" && Number.isFinite(contextPercent)
|
|
3328
|
+
? contextPercent
|
|
3329
|
+
: null;
|
|
3330
|
+
|
|
3331
|
+
if (percentValue == null && hasTokens && hasWindow) {
|
|
3332
|
+
percentValue = (contextTokens / contextWindow) * 100;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
if (!hasTokens && !hasWindow) {
|
|
3336
|
+
return "Context: unknown";
|
|
3337
|
+
}
|
|
3338
|
+
if (!hasTokens && hasWindow) {
|
|
3339
|
+
return "Context: ? / " + formatNumber(contextWindow);
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
let text = "Context: " + formatNumber(contextTokens);
|
|
3343
|
+
if (hasWindow) {
|
|
3344
|
+
text += " / " + formatNumber(contextWindow);
|
|
3345
|
+
}
|
|
3346
|
+
if (percentValue != null && Number.isFinite(percentValue)) {
|
|
3347
|
+
const bounded = Math.max(0, Math.min(100, percentValue));
|
|
3348
|
+
text += " (" + bounded.toFixed(1) + "%)";
|
|
3349
|
+
}
|
|
3350
|
+
return text;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
function applyContextUsageFromMessage(message) {
|
|
3354
|
+
if (!message || typeof message !== "object") return false;
|
|
3355
|
+
|
|
3356
|
+
let changed = false;
|
|
3357
|
+
|
|
3358
|
+
if (Object.prototype.hasOwnProperty.call(message, "contextTokens")) {
|
|
3359
|
+
const next = typeof message.contextTokens === "number" && Number.isFinite(message.contextTokens) && message.contextTokens >= 0
|
|
3360
|
+
? message.contextTokens
|
|
3361
|
+
: null;
|
|
3362
|
+
if (next !== contextTokens) {
|
|
3363
|
+
contextTokens = next;
|
|
3364
|
+
changed = true;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
if (Object.prototype.hasOwnProperty.call(message, "contextWindow")) {
|
|
3369
|
+
const next = typeof message.contextWindow === "number" && Number.isFinite(message.contextWindow) && message.contextWindow > 0
|
|
3370
|
+
? message.contextWindow
|
|
3371
|
+
: null;
|
|
3372
|
+
if (next !== contextWindow) {
|
|
3373
|
+
contextWindow = next;
|
|
3374
|
+
changed = true;
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
if (Object.prototype.hasOwnProperty.call(message, "contextPercent")) {
|
|
3379
|
+
const next = typeof message.contextPercent === "number" && Number.isFinite(message.contextPercent)
|
|
3380
|
+
? Math.max(0, Math.min(100, message.contextPercent))
|
|
3381
|
+
: null;
|
|
3382
|
+
if (next !== contextPercent) {
|
|
3383
|
+
contextPercent = next;
|
|
3384
|
+
changed = true;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
return changed;
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
function updateDocumentTitle() {
|
|
3392
|
+
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
3393
|
+
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
3394
|
+
const titleParts = ["pi Studio"];
|
|
3395
|
+
if (terminalText && terminalText !== "unknown") titleParts.push(terminalText);
|
|
3396
|
+
if (modelText && modelText !== "none") titleParts.push(modelText);
|
|
3397
|
+
document.title = titleParts.join(" · ");
|
|
3398
|
+
}
|
|
3399
|
+
|
|
2904
3400
|
function updateFooterMeta() {
|
|
2905
|
-
if (!footerMetaEl) return;
|
|
2906
3401
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
2907
3402
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
2908
|
-
|
|
3403
|
+
const contextText = formatContextUsageText();
|
|
3404
|
+
const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
|
|
3405
|
+
if (footerMetaTextEl) {
|
|
3406
|
+
footerMetaTextEl.textContent = text;
|
|
3407
|
+
footerMetaTextEl.title = text;
|
|
3408
|
+
} else if (footerMetaEl) {
|
|
3409
|
+
footerMetaEl.textContent = text;
|
|
3410
|
+
footerMetaEl.title = text;
|
|
3411
|
+
}
|
|
3412
|
+
updateDocumentTitle();
|
|
2909
3413
|
}
|
|
2910
3414
|
|
|
2911
3415
|
function stopFooterSpinner() {
|
|
@@ -2952,6 +3456,7 @@ ${cssVarsBlock}
|
|
|
2952
3456
|
wsState = nextState || "Disconnected";
|
|
2953
3457
|
syncFooterSpinnerState();
|
|
2954
3458
|
renderStatus();
|
|
3459
|
+
syncActionButtons();
|
|
2955
3460
|
}
|
|
2956
3461
|
|
|
2957
3462
|
function setStatus(message, level) {
|
|
@@ -3104,22 +3609,167 @@ ${cssVarsBlock}
|
|
|
3104
3609
|
}
|
|
3105
3610
|
}
|
|
3106
3611
|
|
|
3107
|
-
function
|
|
3108
|
-
|
|
3612
|
+
function normalizeHistoryKind(kind) {
|
|
3613
|
+
return kind === "critique" ? "critique" : "annotation";
|
|
3614
|
+
}
|
|
3109
3615
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3616
|
+
function normalizeHistoryItem(item, fallbackIndex) {
|
|
3617
|
+
if (!item || typeof item !== "object") return null;
|
|
3618
|
+
if (typeof item.markdown !== "string") return null;
|
|
3619
|
+
const markdown = item.markdown;
|
|
3620
|
+
if (!markdown.trim()) return null;
|
|
3621
|
+
|
|
3622
|
+
const id = typeof item.id === "string" && item.id.trim()
|
|
3623
|
+
? item.id.trim()
|
|
3624
|
+
: ("history-" + fallbackIndex + "-" + Date.now());
|
|
3625
|
+
const timestamp = typeof item.timestamp === "number" && Number.isFinite(item.timestamp) && item.timestamp > 0
|
|
3626
|
+
? item.timestamp
|
|
3627
|
+
: Date.now();
|
|
3628
|
+
const prompt = typeof item.prompt === "string"
|
|
3629
|
+
? item.prompt
|
|
3630
|
+
: (item.prompt == null ? null : String(item.prompt));
|
|
3121
3631
|
|
|
3122
|
-
|
|
3632
|
+
return {
|
|
3633
|
+
id,
|
|
3634
|
+
markdown,
|
|
3635
|
+
timestamp,
|
|
3636
|
+
kind: normalizeHistoryKind(item.kind),
|
|
3637
|
+
prompt,
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
function getSelectedHistoryItem() {
|
|
3642
|
+
if (!Array.isArray(responseHistory) || responseHistory.length === 0) return null;
|
|
3643
|
+
if (responseHistoryIndex < 0 || responseHistoryIndex >= responseHistory.length) return null;
|
|
3644
|
+
return responseHistory[responseHistoryIndex] || null;
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
function clearActiveResponseView() {
|
|
3648
|
+
latestResponseMarkdown = "";
|
|
3649
|
+
latestResponseKind = "annotation";
|
|
3650
|
+
latestResponseTimestamp = 0;
|
|
3651
|
+
latestResponseIsStructuredCritique = false;
|
|
3652
|
+
latestResponseHasContent = false;
|
|
3653
|
+
latestResponseNormalized = "";
|
|
3654
|
+
latestCritiqueNotes = "";
|
|
3655
|
+
latestCritiqueNotesNormalized = "";
|
|
3656
|
+
refreshResponseUi();
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
function updateHistoryControls() {
|
|
3660
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
3661
|
+
const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
|
|
3662
|
+
? responseHistoryIndex + 1
|
|
3663
|
+
: 0;
|
|
3664
|
+
if (historyIndexBadgeEl) {
|
|
3665
|
+
historyIndexBadgeEl.textContent = "History: " + selected + "/" + total;
|
|
3666
|
+
}
|
|
3667
|
+
if (historyPrevBtn) {
|
|
3668
|
+
historyPrevBtn.disabled = uiBusy || total <= 1 || responseHistoryIndex <= 0;
|
|
3669
|
+
}
|
|
3670
|
+
if (historyNextBtn) {
|
|
3671
|
+
historyNextBtn.disabled = uiBusy || total <= 1 || responseHistoryIndex < 0 || responseHistoryIndex >= total - 1;
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
const selectedItem = getSelectedHistoryItem();
|
|
3675
|
+
const hasPrompt = Boolean(selectedItem && typeof selectedItem.prompt === "string" && selectedItem.prompt.trim());
|
|
3676
|
+
if (loadHistoryPromptBtn) {
|
|
3677
|
+
loadHistoryPromptBtn.disabled = uiBusy || !hasPrompt;
|
|
3678
|
+
loadHistoryPromptBtn.textContent = hasPrompt
|
|
3679
|
+
? "Load response prompt into editor"
|
|
3680
|
+
: "Response prompt unavailable";
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
function applySelectedHistoryItem() {
|
|
3685
|
+
const item = getSelectedHistoryItem();
|
|
3686
|
+
if (!item) {
|
|
3687
|
+
clearActiveResponseView();
|
|
3688
|
+
return false;
|
|
3689
|
+
}
|
|
3690
|
+
handleIncomingResponse(item.markdown, item.kind, item.timestamp);
|
|
3691
|
+
return true;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
function selectHistoryIndex(index, options) {
|
|
3695
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
3696
|
+
if (total === 0) {
|
|
3697
|
+
responseHistoryIndex = -1;
|
|
3698
|
+
clearActiveResponseView();
|
|
3699
|
+
updateHistoryControls();
|
|
3700
|
+
return false;
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
const nextIndex = Math.max(0, Math.min(total - 1, Number(index) || 0));
|
|
3704
|
+
responseHistoryIndex = nextIndex;
|
|
3705
|
+
const applied = applySelectedHistoryItem();
|
|
3706
|
+
updateHistoryControls();
|
|
3707
|
+
|
|
3708
|
+
if (applied && !(options && options.silent)) {
|
|
3709
|
+
const item = getSelectedHistoryItem();
|
|
3710
|
+
if (item) {
|
|
3711
|
+
const responseLabel = item.kind === "critique" ? "critique" : "response";
|
|
3712
|
+
setStatus("Viewing " + responseLabel + " history " + (nextIndex + 1) + "/" + total + ".");
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
return applied;
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
function setResponseHistory(items, options) {
|
|
3719
|
+
const normalized = Array.isArray(items)
|
|
3720
|
+
? items
|
|
3721
|
+
.map((item, index) => normalizeHistoryItem(item, index))
|
|
3722
|
+
.filter((item) => item && typeof item === "object")
|
|
3723
|
+
: [];
|
|
3724
|
+
|
|
3725
|
+
const previousItem = getSelectedHistoryItem();
|
|
3726
|
+
const previousId = previousItem && typeof previousItem.id === "string" ? previousItem.id : null;
|
|
3727
|
+
|
|
3728
|
+
responseHistory = normalized;
|
|
3729
|
+
|
|
3730
|
+
if (!responseHistory.length) {
|
|
3731
|
+
responseHistoryIndex = -1;
|
|
3732
|
+
clearActiveResponseView();
|
|
3733
|
+
updateHistoryControls();
|
|
3734
|
+
return false;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
let targetIndex = responseHistory.length - 1;
|
|
3738
|
+
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
3739
|
+
const autoSelectLatest = options && Object.prototype.hasOwnProperty.call(options, "autoSelectLatest")
|
|
3740
|
+
? Boolean(options.autoSelectLatest)
|
|
3741
|
+
: true;
|
|
3742
|
+
|
|
3743
|
+
if (preserveSelection && previousId) {
|
|
3744
|
+
const preservedIndex = responseHistory.findIndex((item) => item.id === previousId);
|
|
3745
|
+
if (preservedIndex >= 0) {
|
|
3746
|
+
targetIndex = preservedIndex;
|
|
3747
|
+
} else if (!autoSelectLatest && responseHistoryIndex >= 0 && responseHistoryIndex < responseHistory.length) {
|
|
3748
|
+
targetIndex = responseHistoryIndex;
|
|
3749
|
+
}
|
|
3750
|
+
} else if (!autoSelectLatest && responseHistoryIndex >= 0 && responseHistoryIndex < responseHistory.length) {
|
|
3751
|
+
targetIndex = responseHistoryIndex;
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
return selectHistoryIndex(targetIndex, { silent: Boolean(options && options.silent) });
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
function updateReferenceBadge() {
|
|
3758
|
+
if (!referenceBadgeEl) return;
|
|
3759
|
+
|
|
3760
|
+
if (rightView === "editor-preview") {
|
|
3761
|
+
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
3762
|
+
if (hasResponse) {
|
|
3763
|
+
const time = formatReferenceTime(latestResponseTimestamp);
|
|
3764
|
+
const suffix = time ? " · response updated " + time : " · response available";
|
|
3765
|
+
referenceBadgeEl.textContent = "Previewing: editor text" + suffix;
|
|
3766
|
+
} else {
|
|
3767
|
+
referenceBadgeEl.textContent = "Previewing: editor text";
|
|
3768
|
+
}
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
3123
3773
|
if (!hasResponse) {
|
|
3124
3774
|
referenceBadgeEl.textContent = "Latest response: none";
|
|
3125
3775
|
return;
|
|
@@ -3127,9 +3777,14 @@ ${cssVarsBlock}
|
|
|
3127
3777
|
|
|
3128
3778
|
const time = formatReferenceTime(latestResponseTimestamp);
|
|
3129
3779
|
const responseLabel = latestResponseKind === "critique" ? "assistant critique" : "assistant response";
|
|
3780
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
3781
|
+
const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
|
|
3782
|
+
? responseHistoryIndex + 1
|
|
3783
|
+
: 0;
|
|
3784
|
+
const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
|
|
3130
3785
|
referenceBadgeEl.textContent = time
|
|
3131
|
-
?
|
|
3132
|
-
:
|
|
3786
|
+
? historyPrefix + responseLabel + " · " + time
|
|
3787
|
+
: historyPrefix + responseLabel;
|
|
3133
3788
|
}
|
|
3134
3789
|
|
|
3135
3790
|
function normalizeForCompare(text) {
|
|
@@ -3140,6 +3795,28 @@ ${cssVarsBlock}
|
|
|
3140
3795
|
return normalizeForCompare(a) === normalizeForCompare(b);
|
|
3141
3796
|
}
|
|
3142
3797
|
|
|
3798
|
+
function hasAnnotationMarkers(text) {
|
|
3799
|
+
const source = String(text || "");
|
|
3800
|
+
ANNOTATION_MARKER_REGEX.lastIndex = 0;
|
|
3801
|
+
const hasMarker = ANNOTATION_MARKER_REGEX.test(source);
|
|
3802
|
+
ANNOTATION_MARKER_REGEX.lastIndex = 0;
|
|
3803
|
+
return hasMarker;
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
function stripAnnotationMarkers(text) {
|
|
3807
|
+
return String(text || "").replace(ANNOTATION_MARKER_REGEX, "");
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
function prepareEditorTextForSend(text) {
|
|
3811
|
+
const raw = String(text || "");
|
|
3812
|
+
return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
function prepareEditorTextForPreview(text) {
|
|
3816
|
+
const raw = String(text || "");
|
|
3817
|
+
return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3143
3820
|
function updateSyncBadge(normalizedEditorText) {
|
|
3144
3821
|
if (!syncBadgeEl) return;
|
|
3145
3822
|
|
|
@@ -3190,6 +3867,67 @@ ${cssVarsBlock}
|
|
|
3190
3867
|
return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
|
|
3191
3868
|
}
|
|
3192
3869
|
|
|
3870
|
+
function applyAnnotationMarkersToElement(targetEl, mode) {
|
|
3871
|
+
if (!targetEl || mode === "none") return;
|
|
3872
|
+
if (typeof document.createTreeWalker !== "function") return;
|
|
3873
|
+
|
|
3874
|
+
const walker = document.createTreeWalker(targetEl, NodeFilter.SHOW_TEXT);
|
|
3875
|
+
const textNodes = [];
|
|
3876
|
+
let node = walker.nextNode();
|
|
3877
|
+
while (node) {
|
|
3878
|
+
const textNode = node;
|
|
3879
|
+
const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
|
|
3880
|
+
if (value && value.toLowerCase().indexOf("[an:") !== -1) {
|
|
3881
|
+
const parent = textNode.parentElement;
|
|
3882
|
+
const tag = parent && parent.tagName ? parent.tagName.toUpperCase() : "";
|
|
3883
|
+
if (tag !== "CODE" && tag !== "PRE" && tag !== "SCRIPT" && tag !== "STYLE" && tag !== "TEXTAREA") {
|
|
3884
|
+
textNodes.push(textNode);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
node = walker.nextNode();
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
for (const textNode of textNodes) {
|
|
3891
|
+
const text = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
|
|
3892
|
+
if (!text) continue;
|
|
3893
|
+
ANNOTATION_MARKER_REGEX.lastIndex = 0;
|
|
3894
|
+
if (!ANNOTATION_MARKER_REGEX.test(text)) continue;
|
|
3895
|
+
ANNOTATION_MARKER_REGEX.lastIndex = 0;
|
|
3896
|
+
|
|
3897
|
+
const fragment = document.createDocumentFragment();
|
|
3898
|
+
let lastIndex = 0;
|
|
3899
|
+
let match;
|
|
3900
|
+
while ((match = ANNOTATION_MARKER_REGEX.exec(text)) !== null) {
|
|
3901
|
+
const token = match[0] || "";
|
|
3902
|
+
const start = typeof match.index === "number" ? match.index : 0;
|
|
3903
|
+
if (start > lastIndex) {
|
|
3904
|
+
fragment.appendChild(document.createTextNode(text.slice(lastIndex, start)));
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
if (mode === "highlight") {
|
|
3908
|
+
const markerEl = document.createElement("span");
|
|
3909
|
+
markerEl.className = "annotation-preview-marker";
|
|
3910
|
+
markerEl.textContent = typeof match[1] === "string" ? match[1].trim() : token;
|
|
3911
|
+
markerEl.title = token;
|
|
3912
|
+
fragment.appendChild(markerEl);
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
lastIndex = start + token.length;
|
|
3916
|
+
if (token.length === 0) {
|
|
3917
|
+
ANNOTATION_MARKER_REGEX.lastIndex += 1;
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
if (lastIndex < text.length) {
|
|
3922
|
+
fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
if (textNode.parentNode) {
|
|
3926
|
+
textNode.parentNode.replaceChild(fragment, textNode);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3193
3931
|
function appendMermaidNotice(targetEl, message) {
|
|
3194
3932
|
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
|
|
3195
3933
|
return;
|
|
@@ -3430,7 +4168,7 @@ ${cssVarsBlock}
|
|
|
3430
4168
|
return;
|
|
3431
4169
|
}
|
|
3432
4170
|
|
|
3433
|
-
const markdown = rightView === "editor-preview" ? sourceTextEl.value : latestResponseMarkdown;
|
|
4171
|
+
const markdown = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
|
|
3434
4172
|
if (!markdown || !markdown.trim()) {
|
|
3435
4173
|
setStatus("Nothing to export yet.", "warning");
|
|
3436
4174
|
return;
|
|
@@ -3523,6 +4261,10 @@ ${cssVarsBlock}
|
|
|
3523
4261
|
|
|
3524
4262
|
finishPreviewRender(targetEl);
|
|
3525
4263
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
4264
|
+
const annotationMode = (pane === "source" || pane === "response")
|
|
4265
|
+
? (annotationsEnabled ? "highlight" : "hide")
|
|
4266
|
+
: "none";
|
|
4267
|
+
applyAnnotationMarkersToElement(targetEl, annotationMode);
|
|
3526
4268
|
await renderMermaidInElement(targetEl);
|
|
3527
4269
|
|
|
3528
4270
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
@@ -3548,7 +4290,7 @@ ${cssVarsBlock}
|
|
|
3548
4290
|
|
|
3549
4291
|
function renderSourcePreviewNow() {
|
|
3550
4292
|
if (editorView !== "preview") return;
|
|
3551
|
-
const text = sourceTextEl.value || "";
|
|
4293
|
+
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
3552
4294
|
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
3553
4295
|
finishPreviewRender(sourcePreviewEl);
|
|
3554
4296
|
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>";
|
|
@@ -3608,7 +4350,7 @@ ${cssVarsBlock}
|
|
|
3608
4350
|
|
|
3609
4351
|
function renderActiveResult() {
|
|
3610
4352
|
if (rightView === "editor-preview") {
|
|
3611
|
-
const editorText = sourceTextEl.value || "";
|
|
4353
|
+
const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
3612
4354
|
if (!editorText.trim()) {
|
|
3613
4355
|
finishPreviewRender(critiqueViewEl);
|
|
3614
4356
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
|
|
@@ -3685,7 +4427,7 @@ ${cssVarsBlock}
|
|
|
3685
4427
|
copyResponseBtn.disabled = uiBusy || !hasResponse;
|
|
3686
4428
|
|
|
3687
4429
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
3688
|
-
const exportText = rightView === "editor-preview" ? sourceTextEl.value : latestResponseMarkdown;
|
|
4430
|
+
const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
|
|
3689
4431
|
const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
|
|
3690
4432
|
if (exportPdfBtn) {
|
|
3691
4433
|
exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
|
|
@@ -3708,6 +4450,7 @@ ${cssVarsBlock}
|
|
|
3708
4450
|
updateSourceBadge();
|
|
3709
4451
|
updateReferenceBadge();
|
|
3710
4452
|
renderActiveResult();
|
|
4453
|
+
updateHistoryControls();
|
|
3711
4454
|
updateResultActionButtons();
|
|
3712
4455
|
}
|
|
3713
4456
|
|
|
@@ -3722,6 +4465,24 @@ ${cssVarsBlock}
|
|
|
3722
4465
|
return null;
|
|
3723
4466
|
}
|
|
3724
4467
|
|
|
4468
|
+
function buildAnnotatedSaveSuggestion() {
|
|
4469
|
+
const effectivePath = getEffectiveSavePath() || sourceState.path || "";
|
|
4470
|
+
if (effectivePath) {
|
|
4471
|
+
const parts = String(effectivePath).split(/[/\\\\]/);
|
|
4472
|
+
const fileName = parts.pop() || "draft.md";
|
|
4473
|
+
const dir = parts.length > 0 ? parts.join("/") + "/" : "";
|
|
4474
|
+
const stem = fileName.replace(/\\.[^.]+$/, "") || "draft";
|
|
4475
|
+
return dir + stem + ".annotated.md";
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\\s*/i, "") : "draft.md";
|
|
4479
|
+
const stem = rawLabel.replace(/\\.[^.]+$/, "") || "draft";
|
|
4480
|
+
const suggestedDir = resourceDirInput && resourceDirInput.value.trim()
|
|
4481
|
+
? resourceDirInput.value.trim().replace(/\\/$/, "") + "/"
|
|
4482
|
+
: "./";
|
|
4483
|
+
return suggestedDir + stem + ".annotated.md";
|
|
4484
|
+
}
|
|
4485
|
+
|
|
3725
4486
|
function updateSaveFileTooltip() {
|
|
3726
4487
|
if (!saveOverBtn) return;
|
|
3727
4488
|
|
|
@@ -3746,6 +4507,10 @@ ${cssVarsBlock}
|
|
|
3746
4507
|
copyDraftBtn.disabled = uiBusy;
|
|
3747
4508
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
3748
4509
|
if (langSelect) langSelect.disabled = uiBusy;
|
|
4510
|
+
if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
|
|
4511
|
+
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
|
|
4512
|
+
if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
4513
|
+
if (compactBtn) compactBtn.disabled = uiBusy || compactInProgress || wsState === "Disconnected";
|
|
3749
4514
|
editorViewSelect.disabled = uiBusy;
|
|
3750
4515
|
rightViewSelect.disabled = uiBusy;
|
|
3751
4516
|
followSelect.disabled = uiBusy;
|
|
@@ -3754,6 +4519,7 @@ ${cssVarsBlock}
|
|
|
3754
4519
|
critiqueBtn.disabled = uiBusy;
|
|
3755
4520
|
lensSelect.disabled = uiBusy;
|
|
3756
4521
|
updateSaveFileTooltip();
|
|
4522
|
+
updateHistoryControls();
|
|
3757
4523
|
updateResultActionButtons();
|
|
3758
4524
|
}
|
|
3759
4525
|
|
|
@@ -3774,6 +4540,47 @@ ${cssVarsBlock}
|
|
|
3774
4540
|
syncActionButtons();
|
|
3775
4541
|
}
|
|
3776
4542
|
|
|
4543
|
+
function setEditorText(nextText, options) {
|
|
4544
|
+
const value = String(nextText || "");
|
|
4545
|
+
const preserveScroll = Boolean(options && options.preserveScroll);
|
|
4546
|
+
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
4547
|
+
const previousScrollTop = sourceTextEl.scrollTop;
|
|
4548
|
+
const previousScrollLeft = sourceTextEl.scrollLeft;
|
|
4549
|
+
const previousSelectionStart = sourceTextEl.selectionStart;
|
|
4550
|
+
const previousSelectionEnd = sourceTextEl.selectionEnd;
|
|
4551
|
+
|
|
4552
|
+
sourceTextEl.value = value;
|
|
4553
|
+
|
|
4554
|
+
if (preserveSelection) {
|
|
4555
|
+
const maxIndex = value.length;
|
|
4556
|
+
const start = Math.max(0, Math.min(previousSelectionStart || 0, maxIndex));
|
|
4557
|
+
const end = Math.max(start, Math.min(previousSelectionEnd || start, maxIndex));
|
|
4558
|
+
sourceTextEl.setSelectionRange(start, end);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
if (preserveScroll) {
|
|
4562
|
+
sourceTextEl.scrollTop = previousScrollTop;
|
|
4563
|
+
sourceTextEl.scrollLeft = previousScrollLeft;
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
syncEditorHighlightScroll();
|
|
4567
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
4568
|
+
? window.requestAnimationFrame.bind(window)
|
|
4569
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
4570
|
+
schedule(() => {
|
|
4571
|
+
syncEditorHighlightScroll();
|
|
4572
|
+
});
|
|
4573
|
+
|
|
4574
|
+
updateAnnotatedReplyHeaderButton();
|
|
4575
|
+
|
|
4576
|
+
if (!options || options.updatePreview !== false) {
|
|
4577
|
+
renderSourcePreview();
|
|
4578
|
+
}
|
|
4579
|
+
if (!options || options.updateMeta !== false) {
|
|
4580
|
+
scheduleEditorMetaUpdate();
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
|
|
3777
4584
|
function setEditorView(nextView) {
|
|
3778
4585
|
editorView = nextView === "preview" ? "preview" : "markdown";
|
|
3779
4586
|
editorViewSelect.value = editorView;
|
|
@@ -3842,7 +4649,7 @@ ${cssVarsBlock}
|
|
|
3842
4649
|
|
|
3843
4650
|
function highlightInlineMarkdown(text) {
|
|
3844
4651
|
const source = String(text || "");
|
|
3845
|
-
const pattern = /(\\x60[^\\x60]*\\x60)|(\\[[^\\]]+\\]\\([^)]+\\))/
|
|
4652
|
+
const pattern = /(\\x60[^\\x60]*\\x60)|(\\[[^\\]]+\\]\\([^)]+\\))|(\\[an:\\s*[^\\]\\n]+\\])/gi;
|
|
3846
4653
|
let lastIndex = 0;
|
|
3847
4654
|
let out = "";
|
|
3848
4655
|
|
|
@@ -3865,6 +4672,8 @@ ${cssVarsBlock}
|
|
|
3865
4672
|
} else {
|
|
3866
4673
|
out += escapeHtml(token);
|
|
3867
4674
|
}
|
|
4675
|
+
} else if (match[3]) {
|
|
4676
|
+
out += wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
|
|
3868
4677
|
} else {
|
|
3869
4678
|
out += escapeHtml(token);
|
|
3870
4679
|
}
|
|
@@ -4235,6 +5044,10 @@ ${cssVarsBlock}
|
|
|
4235
5044
|
function runEditorMetaUpdateNow() {
|
|
4236
5045
|
const normalizedEditor = normalizeForCompare(sourceTextEl.value);
|
|
4237
5046
|
updateResultActionButtons(normalizedEditor);
|
|
5047
|
+
updateAnnotatedReplyHeaderButton();
|
|
5048
|
+
if (stripAnnotationsBtn) {
|
|
5049
|
+
stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
5050
|
+
}
|
|
4238
5051
|
}
|
|
4239
5052
|
|
|
4240
5053
|
function scheduleEditorMetaUpdate() {
|
|
@@ -4286,6 +5099,10 @@ ${cssVarsBlock}
|
|
|
4286
5099
|
return readStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY);
|
|
4287
5100
|
}
|
|
4288
5101
|
|
|
5102
|
+
function readStoredAnnotationsEnabled() {
|
|
5103
|
+
return readStoredToggle(ANNOTATION_MODE_STORAGE_KEY);
|
|
5104
|
+
}
|
|
5105
|
+
|
|
4289
5106
|
function persistEditorHighlightEnabled(enabled) {
|
|
4290
5107
|
persistStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled);
|
|
4291
5108
|
}
|
|
@@ -4294,6 +5111,10 @@ ${cssVarsBlock}
|
|
|
4294
5111
|
persistStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY, enabled);
|
|
4295
5112
|
}
|
|
4296
5113
|
|
|
5114
|
+
function persistAnnotationsEnabled(enabled) {
|
|
5115
|
+
persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
|
|
5116
|
+
}
|
|
5117
|
+
|
|
4297
5118
|
function updateEditorHighlightState() {
|
|
4298
5119
|
const enabled = editorHighlightEnabled && editorView === "markdown";
|
|
4299
5120
|
|
|
@@ -4383,6 +5204,38 @@ ${cssVarsBlock}
|
|
|
4383
5204
|
renderActiveResult();
|
|
4384
5205
|
}
|
|
4385
5206
|
|
|
5207
|
+
function updateAnnotationModeUi() {
|
|
5208
|
+
if (annotationModeSelect) {
|
|
5209
|
+
annotationModeSelect.value = annotationsEnabled ? "on" : "off";
|
|
5210
|
+
annotationModeSelect.title = annotationsEnabled
|
|
5211
|
+
? "Annotations On: keep and send [an: ...] markers."
|
|
5212
|
+
: "Annotations Hidden: keep markers in editor, hide in preview, and strip before Run/Critique.";
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
if (sendRunBtn) {
|
|
5216
|
+
sendRunBtn.title = annotationsEnabled
|
|
5217
|
+
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter."
|
|
5218
|
+
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter.";
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
if (critiqueBtn) {
|
|
5222
|
+
critiqueBtn.title = annotationsEnabled
|
|
5223
|
+
? "Critique editor text as-is (includes [an: ...] markers)."
|
|
5224
|
+
: "Critique editor text with [an: ...] markers stripped.";
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
function setAnnotationsEnabled(enabled, _options) {
|
|
5229
|
+
annotationsEnabled = Boolean(enabled);
|
|
5230
|
+
persistAnnotationsEnabled(annotationsEnabled);
|
|
5231
|
+
updateAnnotationModeUi();
|
|
5232
|
+
|
|
5233
|
+
if (editorHighlightEnabled && editorView === "markdown") {
|
|
5234
|
+
scheduleEditorHighlightRender();
|
|
5235
|
+
}
|
|
5236
|
+
renderSourcePreview();
|
|
5237
|
+
}
|
|
5238
|
+
|
|
4386
5239
|
function extractSection(markdown, title) {
|
|
4387
5240
|
if (!markdown || !title) return "";
|
|
4388
5241
|
|
|
@@ -4479,6 +5332,10 @@ ${cssVarsBlock}
|
|
|
4479
5332
|
|
|
4480
5333
|
debugTrace("server_message", summarizeServerMessage(message));
|
|
4481
5334
|
|
|
5335
|
+
if (applyContextUsageFromMessage(message)) {
|
|
5336
|
+
updateFooterMeta();
|
|
5337
|
+
}
|
|
5338
|
+
|
|
4482
5339
|
if (message.type === "debug_event") {
|
|
4483
5340
|
debugTrace("server_debug_event", summarizeServerMessage(message));
|
|
4484
5341
|
return;
|
|
@@ -4510,13 +5367,21 @@ ${cssVarsBlock}
|
|
|
4510
5367
|
pendingKind = null;
|
|
4511
5368
|
}
|
|
4512
5369
|
|
|
5370
|
+
if (typeof message.compactInProgress === "boolean") {
|
|
5371
|
+
compactInProgress = message.compactInProgress;
|
|
5372
|
+
} else if (pendingKind === "compact") {
|
|
5373
|
+
compactInProgress = true;
|
|
5374
|
+
} else if (!busy) {
|
|
5375
|
+
compactInProgress = false;
|
|
5376
|
+
}
|
|
5377
|
+
|
|
4513
5378
|
let loadedInitialDocument = false;
|
|
4514
5379
|
if (
|
|
4515
5380
|
!initialDocumentApplied &&
|
|
4516
5381
|
message.initialDocument &&
|
|
4517
5382
|
typeof message.initialDocument.text === "string"
|
|
4518
5383
|
) {
|
|
4519
|
-
|
|
5384
|
+
setEditorText(message.initialDocument.text, { preserveScroll: false, preserveSelection: false });
|
|
4520
5385
|
initialDocumentApplied = true;
|
|
4521
5386
|
loadedInitialDocument = true;
|
|
4522
5387
|
setSourceState({
|
|
@@ -4525,13 +5390,21 @@ ${cssVarsBlock}
|
|
|
4525
5390
|
path: message.initialDocument.path || null,
|
|
4526
5391
|
});
|
|
4527
5392
|
refreshResponseUi();
|
|
4528
|
-
renderSourcePreview();
|
|
4529
5393
|
if (typeof message.initialDocument.label === "string" && message.initialDocument.label.length > 0) {
|
|
4530
5394
|
setStatus("Loaded " + message.initialDocument.label + ".", "success");
|
|
4531
5395
|
}
|
|
4532
5396
|
}
|
|
4533
5397
|
|
|
4534
|
-
|
|
5398
|
+
let appliedHistory = false;
|
|
5399
|
+
if (Array.isArray(message.responseHistory)) {
|
|
5400
|
+
appliedHistory = setResponseHistory(message.responseHistory, {
|
|
5401
|
+
autoSelectLatest: true,
|
|
5402
|
+
preserveSelection: false,
|
|
5403
|
+
silent: true,
|
|
5404
|
+
});
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
if (!appliedHistory && message.lastResponse && typeof message.lastResponse.markdown === "string") {
|
|
4535
5408
|
const lastMarkdown = message.lastResponse.markdown;
|
|
4536
5409
|
const lastResponseKind =
|
|
4537
5410
|
message.lastResponse.kind === "critique"
|
|
@@ -4570,12 +5443,43 @@ ${cssVarsBlock}
|
|
|
4570
5443
|
pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
4571
5444
|
pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
|
|
4572
5445
|
stickyStudioKind = pendingKind;
|
|
5446
|
+
if (pendingKind === "compact") {
|
|
5447
|
+
compactInProgress = true;
|
|
5448
|
+
}
|
|
4573
5449
|
setBusy(true);
|
|
4574
5450
|
setWsState("Submitting");
|
|
4575
5451
|
setStatus(getStudioBusyStatus(pendingKind), "warning");
|
|
4576
5452
|
return;
|
|
4577
5453
|
}
|
|
4578
5454
|
|
|
5455
|
+
if (message.type === "compaction_completed") {
|
|
5456
|
+
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
5457
|
+
pendingRequestId = null;
|
|
5458
|
+
pendingKind = null;
|
|
5459
|
+
}
|
|
5460
|
+
compactInProgress = false;
|
|
5461
|
+
stickyStudioKind = null;
|
|
5462
|
+
const busy = Boolean(message.busy);
|
|
5463
|
+
setBusy(busy);
|
|
5464
|
+
setWsState(busy ? "Submitting" : "Ready");
|
|
5465
|
+
setStatus(typeof message.message === "string" ? message.message : "Compaction completed.", "success");
|
|
5466
|
+
return;
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
if (message.type === "compaction_error") {
|
|
5470
|
+
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
5471
|
+
pendingRequestId = null;
|
|
5472
|
+
pendingKind = null;
|
|
5473
|
+
}
|
|
5474
|
+
compactInProgress = false;
|
|
5475
|
+
stickyStudioKind = null;
|
|
5476
|
+
const busy = Boolean(message.busy);
|
|
5477
|
+
setBusy(busy);
|
|
5478
|
+
setWsState(busy ? "Submitting" : "Ready");
|
|
5479
|
+
setStatus(typeof message.message === "string" ? message.message : "Compaction failed.", "error");
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
|
|
4579
5483
|
if (message.type === "response") {
|
|
4580
5484
|
if (pendingRequestId && typeof message.requestId === "string" && message.requestId !== pendingRequestId) {
|
|
4581
5485
|
return;
|
|
@@ -4589,23 +5493,45 @@ ${cssVarsBlock}
|
|
|
4589
5493
|
stickyStudioKind = responseKind;
|
|
4590
5494
|
pendingRequestId = null;
|
|
4591
5495
|
pendingKind = null;
|
|
5496
|
+
queuedLatestResponse = null;
|
|
4592
5497
|
setBusy(false);
|
|
4593
5498
|
setWsState("Ready");
|
|
4594
|
-
|
|
5499
|
+
|
|
5500
|
+
let appliedFromHistory = false;
|
|
5501
|
+
if (Array.isArray(message.responseHistory)) {
|
|
5502
|
+
appliedFromHistory = setResponseHistory(message.responseHistory, {
|
|
5503
|
+
autoSelectLatest: true,
|
|
5504
|
+
preserveSelection: false,
|
|
5505
|
+
silent: true,
|
|
5506
|
+
});
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5509
|
+
if (!appliedFromHistory && typeof message.markdown === "string") {
|
|
4595
5510
|
handleIncomingResponse(message.markdown, responseKind, message.timestamp);
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
if (responseKind === "critique") {
|
|
5514
|
+
setStatus("Critique ready.", "success");
|
|
5515
|
+
} else if (responseKind === "direct") {
|
|
5516
|
+
setStatus("Model response ready.", "success");
|
|
5517
|
+
} else {
|
|
5518
|
+
setStatus("Response ready.", "success");
|
|
4603
5519
|
}
|
|
4604
5520
|
return;
|
|
4605
5521
|
}
|
|
4606
5522
|
|
|
4607
5523
|
if (message.type === "latest_response") {
|
|
4608
5524
|
if (pendingRequestId) return;
|
|
5525
|
+
|
|
5526
|
+
const hasHistory = Array.isArray(message.responseHistory);
|
|
5527
|
+
if (hasHistory) {
|
|
5528
|
+
setResponseHistory(message.responseHistory, {
|
|
5529
|
+
autoSelectLatest: followLatest,
|
|
5530
|
+
preserveSelection: !followLatest,
|
|
5531
|
+
silent: true,
|
|
5532
|
+
});
|
|
5533
|
+
}
|
|
5534
|
+
|
|
4609
5535
|
if (typeof message.markdown === "string") {
|
|
4610
5536
|
const payload = {
|
|
4611
5537
|
kind: message.kind === "critique" ? "critique" : "annotation",
|
|
@@ -4620,15 +5546,29 @@ ${cssVarsBlock}
|
|
|
4620
5546
|
return;
|
|
4621
5547
|
}
|
|
4622
5548
|
|
|
4623
|
-
if (applyLatestPayload(payload)) {
|
|
5549
|
+
if (!hasHistory && applyLatestPayload(payload)) {
|
|
4624
5550
|
queuedLatestResponse = null;
|
|
4625
5551
|
updateResultActionButtons();
|
|
4626
5552
|
setStatus("Updated from latest response.", "success");
|
|
5553
|
+
return;
|
|
4627
5554
|
}
|
|
5555
|
+
|
|
5556
|
+
queuedLatestResponse = null;
|
|
5557
|
+
updateResultActionButtons();
|
|
5558
|
+
setStatus("Updated from latest response.", "success");
|
|
4628
5559
|
}
|
|
4629
5560
|
return;
|
|
4630
5561
|
}
|
|
4631
5562
|
|
|
5563
|
+
if (message.type === "response_history") {
|
|
5564
|
+
setResponseHistory(message.items, {
|
|
5565
|
+
autoSelectLatest: followLatest,
|
|
5566
|
+
preserveSelection: !followLatest,
|
|
5567
|
+
silent: true,
|
|
5568
|
+
});
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5571
|
+
|
|
4632
5572
|
if (message.type === "saved") {
|
|
4633
5573
|
if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
|
|
4634
5574
|
pendingRequestId = null;
|
|
@@ -4668,8 +5608,7 @@ ${cssVarsBlock}
|
|
|
4668
5608
|
}
|
|
4669
5609
|
|
|
4670
5610
|
const content = typeof message.content === "string" ? message.content : "";
|
|
4671
|
-
|
|
4672
|
-
renderSourcePreview();
|
|
5611
|
+
setEditorText(content, { preserveScroll: false, preserveSelection: false });
|
|
4673
5612
|
setSourceState({ source: "pi-editor", label: "pi editor draft", path: null });
|
|
4674
5613
|
setBusy(false);
|
|
4675
5614
|
setWsState("Ready");
|
|
@@ -4707,6 +5646,14 @@ ${cssVarsBlock}
|
|
|
4707
5646
|
pendingKind = null;
|
|
4708
5647
|
}
|
|
4709
5648
|
|
|
5649
|
+
if (typeof message.compactInProgress === "boolean") {
|
|
5650
|
+
compactInProgress = message.compactInProgress;
|
|
5651
|
+
} else if (pendingKind === "compact") {
|
|
5652
|
+
compactInProgress = true;
|
|
5653
|
+
} else if (!busy) {
|
|
5654
|
+
compactInProgress = false;
|
|
5655
|
+
}
|
|
5656
|
+
|
|
4710
5657
|
setBusy(busy);
|
|
4711
5658
|
setWsState(busy ? "Submitting" : "Ready");
|
|
4712
5659
|
|
|
@@ -4735,6 +5682,9 @@ ${cssVarsBlock}
|
|
|
4735
5682
|
|
|
4736
5683
|
if (message.type === "busy") {
|
|
4737
5684
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
5685
|
+
if (pendingKind === "compact") {
|
|
5686
|
+
compactInProgress = false;
|
|
5687
|
+
}
|
|
4738
5688
|
pendingRequestId = null;
|
|
4739
5689
|
pendingKind = null;
|
|
4740
5690
|
}
|
|
@@ -4747,6 +5697,9 @@ ${cssVarsBlock}
|
|
|
4747
5697
|
|
|
4748
5698
|
if (message.type === "error") {
|
|
4749
5699
|
if (message.requestId && pendingRequestId === message.requestId) {
|
|
5700
|
+
if (pendingKind === "compact") {
|
|
5701
|
+
compactInProgress = false;
|
|
5702
|
+
}
|
|
4750
5703
|
pendingRequestId = null;
|
|
4751
5704
|
pendingKind = null;
|
|
4752
5705
|
}
|
|
@@ -4865,10 +5818,16 @@ ${cssVarsBlock}
|
|
|
4865
5818
|
function buildAnnotationHeader() {
|
|
4866
5819
|
const sourceDescriptor = describeSourceForAnnotation();
|
|
4867
5820
|
let header = "annotated reply below:\\n";
|
|
4868
|
-
header += "original source: " + sourceDescriptor + "\\n
|
|
5821
|
+
header += "original source: " + sourceDescriptor + "\\n";
|
|
5822
|
+
header += "annotation syntax: [an: your note]\\n";
|
|
5823
|
+
header += "precedence: later messages supersede these annotations unless user explicitly references them\\n\\n---\\n\\n";
|
|
4869
5824
|
return header;
|
|
4870
5825
|
}
|
|
4871
5826
|
|
|
5827
|
+
function stripAnnotationBoundaryMarker(text) {
|
|
5828
|
+
return String(text || "").replace(/\\n{0,2}--- end annotations ---\\s*$/i, "");
|
|
5829
|
+
}
|
|
5830
|
+
|
|
4872
5831
|
function stripAnnotationHeader(text) {
|
|
4873
5832
|
const normalized = String(text || "").replace(/\\r\\n/g, "\\n");
|
|
4874
5833
|
if (!normalized.toLowerCase().startsWith("annotated reply below:")) {
|
|
@@ -4887,23 +5846,43 @@ ${cssVarsBlock}
|
|
|
4887
5846
|
|
|
4888
5847
|
return {
|
|
4889
5848
|
hadHeader: true,
|
|
4890
|
-
body: normalized.slice(cursor),
|
|
5849
|
+
body: stripAnnotationBoundaryMarker(normalized.slice(cursor)),
|
|
4891
5850
|
};
|
|
4892
5851
|
}
|
|
4893
5852
|
|
|
4894
|
-
function
|
|
5853
|
+
function updateAnnotatedReplyHeaderButton() {
|
|
5854
|
+
if (!insertHeaderBtn) return;
|
|
5855
|
+
const hasHeader = stripAnnotationHeader(sourceTextEl.value).hadHeader;
|
|
5856
|
+
if (hasHeader) {
|
|
5857
|
+
insertHeaderBtn.textContent = "Remove annotated reply header";
|
|
5858
|
+
insertHeaderBtn.title = "Remove annotated-reply protocol header while keeping body text.";
|
|
5859
|
+
return;
|
|
5860
|
+
}
|
|
5861
|
+
insertHeaderBtn.textContent = "Insert annotated reply header";
|
|
5862
|
+
insertHeaderBtn.title = "Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).";
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
function toggleAnnotatedReplyHeader() {
|
|
4895
5866
|
const stripped = stripAnnotationHeader(sourceTextEl.value);
|
|
4896
|
-
const updated = buildAnnotationHeader() + stripped.body;
|
|
4897
5867
|
|
|
5868
|
+
if (stripped.hadHeader) {
|
|
5869
|
+
const updated = stripped.body;
|
|
5870
|
+
setEditorText(updated, { preserveScroll: true, preserveSelection: true });
|
|
5871
|
+
updateResultActionButtons();
|
|
5872
|
+
setStatus("Removed annotated reply header.", "success");
|
|
5873
|
+
return;
|
|
5874
|
+
}
|
|
5875
|
+
|
|
5876
|
+
const cleanedBody = stripAnnotationBoundaryMarker(stripped.body);
|
|
5877
|
+
const updated = buildAnnotationHeader() + cleanedBody + "\\n\\n--- end annotations ---\\n\\n";
|
|
4898
5878
|
if (isTextEquivalent(sourceTextEl.value, updated)) {
|
|
4899
|
-
setStatus("
|
|
5879
|
+
setStatus("Annotated reply header already present.");
|
|
4900
5880
|
return;
|
|
4901
5881
|
}
|
|
4902
5882
|
|
|
4903
|
-
|
|
4904
|
-
renderSourcePreview();
|
|
5883
|
+
setEditorText(updated, { preserveScroll: true, preserveSelection: true });
|
|
4905
5884
|
updateResultActionButtons();
|
|
4906
|
-
setStatus(
|
|
5885
|
+
setStatus("Inserted annotated reply header.", "success");
|
|
4907
5886
|
}
|
|
4908
5887
|
|
|
4909
5888
|
function requestLatestResponse() {
|
|
@@ -4938,7 +5917,11 @@ ${cssVarsBlock}
|
|
|
4938
5917
|
followSelect.addEventListener("change", () => {
|
|
4939
5918
|
followLatest = followSelect.value !== "off";
|
|
4940
5919
|
if (followLatest && queuedLatestResponse) {
|
|
4941
|
-
if (
|
|
5920
|
+
if (responseHistory.length > 0) {
|
|
5921
|
+
selectHistoryIndex(responseHistory.length - 1, { silent: true });
|
|
5922
|
+
queuedLatestResponse = null;
|
|
5923
|
+
setStatus("Applied queued response.", "success");
|
|
5924
|
+
} else if (applyLatestPayload(queuedLatestResponse)) {
|
|
4942
5925
|
queuedLatestResponse = null;
|
|
4943
5926
|
setStatus("Applied queued response.", "success");
|
|
4944
5927
|
}
|
|
@@ -4966,9 +5949,90 @@ ${cssVarsBlock}
|
|
|
4966
5949
|
});
|
|
4967
5950
|
}
|
|
4968
5951
|
|
|
5952
|
+
if (annotationModeSelect) {
|
|
5953
|
+
annotationModeSelect.addEventListener("change", () => {
|
|
5954
|
+
setAnnotationsEnabled(annotationModeSelect.value !== "off");
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
if (compactBtn) {
|
|
5959
|
+
compactBtn.addEventListener("click", () => {
|
|
5960
|
+
if (compactInProgress) {
|
|
5961
|
+
setStatus("Compaction is already running.", "warning");
|
|
5962
|
+
return;
|
|
5963
|
+
}
|
|
5964
|
+
if (uiBusy) {
|
|
5965
|
+
setStatus("Studio is busy.", "warning");
|
|
5966
|
+
return;
|
|
5967
|
+
}
|
|
5968
|
+
|
|
5969
|
+
const requestId = makeRequestId();
|
|
5970
|
+
pendingRequestId = requestId;
|
|
5971
|
+
pendingKind = "compact";
|
|
5972
|
+
stickyStudioKind = "compact";
|
|
5973
|
+
compactInProgress = true;
|
|
5974
|
+
setBusy(true);
|
|
5975
|
+
setWsState("Submitting");
|
|
5976
|
+
|
|
5977
|
+
const sent = sendMessage({ type: "compact_request", requestId });
|
|
5978
|
+
if (!sent) {
|
|
5979
|
+
compactInProgress = false;
|
|
5980
|
+
if (pendingRequestId === requestId) {
|
|
5981
|
+
pendingRequestId = null;
|
|
5982
|
+
pendingKind = null;
|
|
5983
|
+
}
|
|
5984
|
+
stickyStudioKind = null;
|
|
5985
|
+
setBusy(false);
|
|
5986
|
+
return;
|
|
5987
|
+
}
|
|
5988
|
+
|
|
5989
|
+
setStatus("Studio: compacting context…", "warning");
|
|
5990
|
+
});
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
if (historyPrevBtn) {
|
|
5994
|
+
historyPrevBtn.addEventListener("click", () => {
|
|
5995
|
+
if (!responseHistory.length) {
|
|
5996
|
+
setStatus("No response history available yet.", "warning");
|
|
5997
|
+
return;
|
|
5998
|
+
}
|
|
5999
|
+
selectHistoryIndex(responseHistoryIndex - 1);
|
|
6000
|
+
});
|
|
6001
|
+
}
|
|
6002
|
+
|
|
6003
|
+
if (historyNextBtn) {
|
|
6004
|
+
historyNextBtn.addEventListener("click", () => {
|
|
6005
|
+
if (!responseHistory.length) {
|
|
6006
|
+
setStatus("No response history available yet.", "warning");
|
|
6007
|
+
return;
|
|
6008
|
+
}
|
|
6009
|
+
selectHistoryIndex(responseHistoryIndex + 1);
|
|
6010
|
+
});
|
|
6011
|
+
}
|
|
6012
|
+
|
|
6013
|
+
if (loadHistoryPromptBtn) {
|
|
6014
|
+
loadHistoryPromptBtn.addEventListener("click", () => {
|
|
6015
|
+
const item = getSelectedHistoryItem();
|
|
6016
|
+
const prompt = item && typeof item.prompt === "string" ? item.prompt : "";
|
|
6017
|
+
if (!prompt.trim()) {
|
|
6018
|
+
setStatus("Prompt unavailable for the selected response.", "warning");
|
|
6019
|
+
return;
|
|
6020
|
+
}
|
|
6021
|
+
|
|
6022
|
+
setEditorText(prompt, { preserveScroll: false, preserveSelection: false });
|
|
6023
|
+
setSourceState({ source: "blank", label: "response prompt", path: null });
|
|
6024
|
+
setStatus("Loaded response prompt into editor.", "success");
|
|
6025
|
+
});
|
|
6026
|
+
}
|
|
6027
|
+
|
|
4969
6028
|
pullLatestBtn.addEventListener("click", () => {
|
|
4970
6029
|
if (queuedLatestResponse) {
|
|
4971
|
-
if (
|
|
6030
|
+
if (responseHistory.length > 0) {
|
|
6031
|
+
selectHistoryIndex(responseHistory.length - 1, { silent: true });
|
|
6032
|
+
queuedLatestResponse = null;
|
|
6033
|
+
setStatus("Pulled latest response from history.", "success");
|
|
6034
|
+
updateResultActionButtons();
|
|
6035
|
+
} else if (applyLatestPayload(queuedLatestResponse)) {
|
|
4972
6036
|
queuedLatestResponse = null;
|
|
4973
6037
|
setStatus("Pulled queued response.", "success");
|
|
4974
6038
|
updateResultActionButtons();
|
|
@@ -4988,12 +6052,28 @@ ${cssVarsBlock}
|
|
|
4988
6052
|
syncEditorHighlightScroll();
|
|
4989
6053
|
});
|
|
4990
6054
|
|
|
6055
|
+
sourceTextEl.addEventListener("keyup", () => {
|
|
6056
|
+
if (!editorHighlightEnabled || editorView !== "markdown") return;
|
|
6057
|
+
syncEditorHighlightScroll();
|
|
6058
|
+
});
|
|
6059
|
+
|
|
6060
|
+
sourceTextEl.addEventListener("mouseup", () => {
|
|
6061
|
+
if (!editorHighlightEnabled || editorView !== "markdown") return;
|
|
6062
|
+
syncEditorHighlightScroll();
|
|
6063
|
+
});
|
|
6064
|
+
|
|
6065
|
+
window.addEventListener("resize", () => {
|
|
6066
|
+
if (!editorHighlightEnabled || editorView !== "markdown") return;
|
|
6067
|
+
syncEditorHighlightScroll();
|
|
6068
|
+
});
|
|
6069
|
+
|
|
4991
6070
|
insertHeaderBtn.addEventListener("click", () => {
|
|
4992
|
-
|
|
6071
|
+
toggleAnnotatedReplyHeader();
|
|
4993
6072
|
});
|
|
4994
6073
|
|
|
4995
6074
|
critiqueBtn.addEventListener("click", () => {
|
|
4996
|
-
const
|
|
6075
|
+
const preparedDocumentText = prepareEditorTextForSend(sourceTextEl.value);
|
|
6076
|
+
const documentText = preparedDocumentText.trim();
|
|
4997
6077
|
if (!documentText) {
|
|
4998
6078
|
setStatus("Add editor text before critique.", "warning");
|
|
4999
6079
|
return;
|
|
@@ -5021,8 +6101,7 @@ ${cssVarsBlock}
|
|
|
5021
6101
|
setStatus("No response available yet.", "warning");
|
|
5022
6102
|
return;
|
|
5023
6103
|
}
|
|
5024
|
-
|
|
5025
|
-
renderSourcePreview();
|
|
6104
|
+
setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
|
|
5026
6105
|
setSourceState({ source: "last-response", label: "last model response", path: null });
|
|
5027
6106
|
setStatus("Loaded response into editor.", "success");
|
|
5028
6107
|
});
|
|
@@ -5039,8 +6118,7 @@ ${cssVarsBlock}
|
|
|
5039
6118
|
return;
|
|
5040
6119
|
}
|
|
5041
6120
|
|
|
5042
|
-
|
|
5043
|
-
renderSourcePreview();
|
|
6121
|
+
setEditorText(notes, { preserveScroll: false, preserveSelection: false });
|
|
5044
6122
|
setSourceState({ source: "blank", label: "critique notes", path: null });
|
|
5045
6123
|
setStatus("Loaded critique notes into editor.", "success");
|
|
5046
6124
|
});
|
|
@@ -5051,8 +6129,7 @@ ${cssVarsBlock}
|
|
|
5051
6129
|
return;
|
|
5052
6130
|
}
|
|
5053
6131
|
|
|
5054
|
-
|
|
5055
|
-
renderSourcePreview();
|
|
6132
|
+
setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
|
|
5056
6133
|
setSourceState({ source: "blank", label: "full critique", path: null });
|
|
5057
6134
|
setStatus("Loaded full critique into editor.", "success");
|
|
5058
6135
|
});
|
|
@@ -5178,8 +6255,8 @@ ${cssVarsBlock}
|
|
|
5178
6255
|
}
|
|
5179
6256
|
|
|
5180
6257
|
sendRunBtn.addEventListener("click", () => {
|
|
5181
|
-
const
|
|
5182
|
-
if (!
|
|
6258
|
+
const prepared = prepareEditorTextForSend(sourceTextEl.value);
|
|
6259
|
+
if (!prepared.trim()) {
|
|
5183
6260
|
setStatus("Editor is empty. Nothing to run.", "warning");
|
|
5184
6261
|
return;
|
|
5185
6262
|
}
|
|
@@ -5190,7 +6267,7 @@ ${cssVarsBlock}
|
|
|
5190
6267
|
const sent = sendMessage({
|
|
5191
6268
|
type: "send_run_request",
|
|
5192
6269
|
requestId,
|
|
5193
|
-
text:
|
|
6270
|
+
text: prepared,
|
|
5194
6271
|
});
|
|
5195
6272
|
|
|
5196
6273
|
if (!sent) {
|
|
@@ -5215,6 +6292,53 @@ ${cssVarsBlock}
|
|
|
5215
6292
|
}
|
|
5216
6293
|
});
|
|
5217
6294
|
|
|
6295
|
+
if (saveAnnotatedBtn) {
|
|
6296
|
+
saveAnnotatedBtn.addEventListener("click", () => {
|
|
6297
|
+
const content = sourceTextEl.value;
|
|
6298
|
+
if (!content.trim()) {
|
|
6299
|
+
setStatus("Editor is empty. Nothing to save.", "warning");
|
|
6300
|
+
return;
|
|
6301
|
+
}
|
|
6302
|
+
|
|
6303
|
+
const suggested = buildAnnotatedSaveSuggestion();
|
|
6304
|
+
const path = window.prompt("Save annotated editor content as:", suggested);
|
|
6305
|
+
if (!path) return;
|
|
6306
|
+
|
|
6307
|
+
const requestId = beginUiAction("save_as");
|
|
6308
|
+
if (!requestId) return;
|
|
6309
|
+
|
|
6310
|
+
const sent = sendMessage({
|
|
6311
|
+
type: "save_as_request",
|
|
6312
|
+
requestId,
|
|
6313
|
+
path,
|
|
6314
|
+
content,
|
|
6315
|
+
});
|
|
6316
|
+
|
|
6317
|
+
if (!sent) {
|
|
6318
|
+
pendingRequestId = null;
|
|
6319
|
+
pendingKind = null;
|
|
6320
|
+
setBusy(false);
|
|
6321
|
+
}
|
|
6322
|
+
});
|
|
6323
|
+
}
|
|
6324
|
+
|
|
6325
|
+
if (stripAnnotationsBtn) {
|
|
6326
|
+
stripAnnotationsBtn.addEventListener("click", () => {
|
|
6327
|
+
const content = sourceTextEl.value;
|
|
6328
|
+
if (!hasAnnotationMarkers(content)) {
|
|
6329
|
+
setStatus("No [an: ...] markers found in editor.", "warning");
|
|
6330
|
+
return;
|
|
6331
|
+
}
|
|
6332
|
+
|
|
6333
|
+
const confirmed = window.confirm("Remove all [an: ...] markers from editor text? This cannot be undone.");
|
|
6334
|
+
if (!confirmed) return;
|
|
6335
|
+
|
|
6336
|
+
const strippedContent = stripAnnotationMarkers(content);
|
|
6337
|
+
setEditorText(strippedContent, { preserveScroll: true, preserveSelection: false });
|
|
6338
|
+
setStatus("Removed annotation markers from editor text.", "success");
|
|
6339
|
+
});
|
|
6340
|
+
}
|
|
6341
|
+
|
|
5218
6342
|
// Working directory controls — three states: button | input | label
|
|
5219
6343
|
function showResourceDirState(state) {
|
|
5220
6344
|
// state: "button" | "input" | "label"
|
|
@@ -5283,8 +6407,7 @@ ${cssVarsBlock}
|
|
|
5283
6407
|
const reader = new FileReader();
|
|
5284
6408
|
reader.onload = () => {
|
|
5285
6409
|
const text = typeof reader.result === "string" ? reader.result : "";
|
|
5286
|
-
|
|
5287
|
-
renderSourcePreview();
|
|
6410
|
+
setEditorText(text, { preserveScroll: false, preserveSelection: false });
|
|
5288
6411
|
setSourceState({
|
|
5289
6412
|
source: "blank",
|
|
5290
6413
|
label: "upload: " + file.name,
|
|
@@ -5305,6 +6428,7 @@ ${cssVarsBlock}
|
|
|
5305
6428
|
|
|
5306
6429
|
setSourceState(initialSourceState);
|
|
5307
6430
|
refreshResponseUi();
|
|
6431
|
+
updateAnnotatedReplyHeaderButton();
|
|
5308
6432
|
setActivePane("left");
|
|
5309
6433
|
|
|
5310
6434
|
const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
|
|
@@ -5319,6 +6443,10 @@ ${cssVarsBlock}
|
|
|
5319
6443
|
const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
|
|
5320
6444
|
setResponseHighlightEnabled(initialResponseHighlightEnabled);
|
|
5321
6445
|
|
|
6446
|
+
const storedAnnotationsEnabled = readStoredAnnotationsEnabled();
|
|
6447
|
+
const initialAnnotationsEnabled = storedAnnotationsEnabled ?? Boolean(annotationModeSelect ? annotationModeSelect.value !== "off" : true);
|
|
6448
|
+
setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
|
|
6449
|
+
|
|
5322
6450
|
setEditorView(editorView);
|
|
5323
6451
|
setRightView(rightView);
|
|
5324
6452
|
renderSourcePreview();
|
|
@@ -5345,10 +6473,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
5345
6473
|
let terminalActivityToolName: string | null = null;
|
|
5346
6474
|
let terminalActivityLabel: string | null = null;
|
|
5347
6475
|
let lastSpecificToolActivityLabel: string | null = null;
|
|
6476
|
+
let currentModel: { provider?: string; id?: string } | undefined;
|
|
5348
6477
|
let currentModelLabel = "none";
|
|
5349
6478
|
let terminalSessionLabel = buildTerminalSessionLabel(studioCwd);
|
|
6479
|
+
let studioResponseHistory: StudioResponseHistoryItem[] = [];
|
|
6480
|
+
let contextUsageSnapshot: StudioContextUsageSnapshot = {
|
|
6481
|
+
tokens: null,
|
|
6482
|
+
contextWindow: null,
|
|
6483
|
+
percent: null,
|
|
6484
|
+
};
|
|
6485
|
+
let compactInProgress = false;
|
|
6486
|
+
let compactRequestId: string | null = null;
|
|
6487
|
+
let updateCheckStarted = false;
|
|
6488
|
+
let updateCheckCompleted = false;
|
|
6489
|
+
const packageMetadata = readLocalPackageMetadata();
|
|
5350
6490
|
|
|
5351
|
-
const isStudioBusy = () => agentBusy || activeRequest !== null;
|
|
6491
|
+
const isStudioBusy = () => agentBusy || activeRequest !== null || compactInProgress;
|
|
5352
6492
|
|
|
5353
6493
|
const getSessionNameSafe = (): string | undefined => {
|
|
5354
6494
|
try {
|
|
@@ -5370,8 +6510,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
5370
6510
|
if (ctx?.cwd) {
|
|
5371
6511
|
studioCwd = ctx.cwd;
|
|
5372
6512
|
}
|
|
5373
|
-
|
|
5374
|
-
|
|
6513
|
+
if (ctx && Object.prototype.hasOwnProperty.call(ctx, "model")) {
|
|
6514
|
+
if (ctx.model) {
|
|
6515
|
+
currentModel = {
|
|
6516
|
+
provider: ctx.model.provider,
|
|
6517
|
+
id: ctx.model.id,
|
|
6518
|
+
};
|
|
6519
|
+
} else {
|
|
6520
|
+
currentModel = undefined;
|
|
6521
|
+
}
|
|
6522
|
+
} else if (!currentModel && lastCommandCtx?.model) {
|
|
6523
|
+
currentModel = {
|
|
6524
|
+
provider: lastCommandCtx.model.provider,
|
|
6525
|
+
id: lastCommandCtx.model.id,
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6528
|
+
const baseModelLabel = formatModelLabel(currentModel);
|
|
5375
6529
|
currentModelLabel = formatModelLabelWithThinking(baseModelLabel, getThinkingLevelSafe());
|
|
5376
6530
|
terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
|
|
5377
6531
|
};
|
|
@@ -5381,6 +6535,59 @@ export default function (pi: ExtensionAPI) {
|
|
|
5381
6535
|
lastCommandCtx.ui.notify(message, level);
|
|
5382
6536
|
};
|
|
5383
6537
|
|
|
6538
|
+
const refreshContextUsage = (
|
|
6539
|
+
ctx?: { getContextUsage(): { tokens: number | null; contextWindow: number; percent: number | null } | undefined },
|
|
6540
|
+
): StudioContextUsageSnapshot => {
|
|
6541
|
+
const usage = ctx?.getContextUsage?.() ?? lastCommandCtx?.getContextUsage?.();
|
|
6542
|
+
if (usage === undefined) return contextUsageSnapshot;
|
|
6543
|
+
contextUsageSnapshot = normalizeContextUsageSnapshot(usage);
|
|
6544
|
+
return contextUsageSnapshot;
|
|
6545
|
+
};
|
|
6546
|
+
|
|
6547
|
+
const clearCompactionState = () => {
|
|
6548
|
+
compactInProgress = false;
|
|
6549
|
+
compactRequestId = null;
|
|
6550
|
+
};
|
|
6551
|
+
|
|
6552
|
+
const syncStudioResponseHistory = (entries: SessionEntry[]) => {
|
|
6553
|
+
studioResponseHistory = buildResponseHistoryFromEntries(entries, RESPONSE_HISTORY_LIMIT);
|
|
6554
|
+
const latest = studioResponseHistory[studioResponseHistory.length - 1];
|
|
6555
|
+
if (!latest) {
|
|
6556
|
+
lastStudioResponse = null;
|
|
6557
|
+
return;
|
|
6558
|
+
}
|
|
6559
|
+
lastStudioResponse = {
|
|
6560
|
+
markdown: latest.markdown,
|
|
6561
|
+
timestamp: latest.timestamp,
|
|
6562
|
+
kind: latest.kind,
|
|
6563
|
+
};
|
|
6564
|
+
};
|
|
6565
|
+
|
|
6566
|
+
const broadcastResponseHistory = () => {
|
|
6567
|
+
broadcast({
|
|
6568
|
+
type: "response_history",
|
|
6569
|
+
items: studioResponseHistory,
|
|
6570
|
+
});
|
|
6571
|
+
};
|
|
6572
|
+
|
|
6573
|
+
const maybeNotifyUpdateAvailable = async (ctx: ExtensionCommandContext) => {
|
|
6574
|
+
if (updateCheckStarted || updateCheckCompleted) return;
|
|
6575
|
+
updateCheckStarted = true;
|
|
6576
|
+
try {
|
|
6577
|
+
const metadata = packageMetadata;
|
|
6578
|
+
if (!metadata) return;
|
|
6579
|
+
const latest = await fetchLatestNpmVersion(metadata.name, UPDATE_CHECK_TIMEOUT_MS);
|
|
6580
|
+
if (!latest) return;
|
|
6581
|
+
if (!isVersionBehind(metadata.version, latest)) return;
|
|
6582
|
+
ctx.ui.notify(
|
|
6583
|
+
`Update available for ${metadata.name}: ${metadata.version} → ${latest}. Run: pi install npm:${metadata.name}`,
|
|
6584
|
+
"info",
|
|
6585
|
+
);
|
|
6586
|
+
} finally {
|
|
6587
|
+
updateCheckCompleted = true;
|
|
6588
|
+
}
|
|
6589
|
+
};
|
|
6590
|
+
|
|
5384
6591
|
const sendToClient = (client: WebSocket, payload: unknown) => {
|
|
5385
6592
|
if (client.readyState !== WebSocket.OPEN) return;
|
|
5386
6593
|
try {
|
|
@@ -5459,8 +6666,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
5459
6666
|
label: terminalActivityLabel,
|
|
5460
6667
|
baseLabel,
|
|
5461
6668
|
lastSpecificToolActivityLabel,
|
|
5462
|
-
activeRequestId: activeRequest?.id ?? null,
|
|
5463
|
-
activeRequestKind: activeRequest?.kind ?? null,
|
|
6669
|
+
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
6670
|
+
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
5464
6671
|
agentBusy,
|
|
5465
6672
|
});
|
|
5466
6673
|
broadcastState();
|
|
@@ -5468,7 +6675,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
5468
6675
|
|
|
5469
6676
|
const broadcastState = () => {
|
|
5470
6677
|
terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
|
|
5471
|
-
currentModelLabel = formatModelLabelWithThinking(
|
|
6678
|
+
currentModelLabel = formatModelLabelWithThinking(formatModelLabel(currentModel), getThinkingLevelSafe());
|
|
6679
|
+
refreshContextUsage();
|
|
5472
6680
|
broadcast({
|
|
5473
6681
|
type: "studio_state",
|
|
5474
6682
|
busy: isStudioBusy(),
|
|
@@ -5478,8 +6686,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
5478
6686
|
terminalActivityLabel,
|
|
5479
6687
|
modelLabel: currentModelLabel,
|
|
5480
6688
|
terminalSessionLabel,
|
|
5481
|
-
|
|
5482
|
-
|
|
6689
|
+
contextTokens: contextUsageSnapshot.tokens,
|
|
6690
|
+
contextWindow: contextUsageSnapshot.contextWindow,
|
|
6691
|
+
contextPercent: contextUsageSnapshot.percent,
|
|
6692
|
+
compactInProgress,
|
|
6693
|
+
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
6694
|
+
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
5483
6695
|
});
|
|
5484
6696
|
};
|
|
5485
6697
|
|
|
@@ -5512,6 +6724,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
5512
6724
|
broadcast({ type: "busy", requestId, message: "A studio request is already in progress." });
|
|
5513
6725
|
return false;
|
|
5514
6726
|
}
|
|
6727
|
+
if (compactInProgress) {
|
|
6728
|
+
broadcast({ type: "busy", requestId, message: "Context compaction is currently running." });
|
|
6729
|
+
return false;
|
|
6730
|
+
}
|
|
5515
6731
|
if (agentBusy) {
|
|
5516
6732
|
broadcast({ type: "busy", requestId, message: "pi is currently busy. Wait for the current turn to finish." });
|
|
5517
6733
|
return false;
|
|
@@ -5564,6 +6780,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5564
6780
|
});
|
|
5565
6781
|
|
|
5566
6782
|
if (msg.type === "hello") {
|
|
6783
|
+
refreshContextUsage();
|
|
5567
6784
|
sendToClient(client, {
|
|
5568
6785
|
type: "hello_ack",
|
|
5569
6786
|
busy: isStudioBusy(),
|
|
@@ -5573,9 +6790,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
5573
6790
|
terminalActivityLabel,
|
|
5574
6791
|
modelLabel: currentModelLabel,
|
|
5575
6792
|
terminalSessionLabel,
|
|
5576
|
-
|
|
5577
|
-
|
|
6793
|
+
contextTokens: contextUsageSnapshot.tokens,
|
|
6794
|
+
contextWindow: contextUsageSnapshot.contextWindow,
|
|
6795
|
+
contextPercent: contextUsageSnapshot.percent,
|
|
6796
|
+
compactInProgress,
|
|
6797
|
+
activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
|
|
6798
|
+
activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
|
|
5578
6799
|
lastResponse: lastStudioResponse,
|
|
6800
|
+
responseHistory: studioResponseHistory,
|
|
5579
6801
|
initialDocument: initialStudioDocument,
|
|
5580
6802
|
});
|
|
5581
6803
|
return;
|
|
@@ -5591,6 +6813,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5591
6813
|
kind: lastStudioResponse.kind,
|
|
5592
6814
|
markdown: lastStudioResponse.markdown,
|
|
5593
6815
|
timestamp: lastStudioResponse.timestamp,
|
|
6816
|
+
responseHistory: studioResponseHistory,
|
|
5594
6817
|
});
|
|
5595
6818
|
return;
|
|
5596
6819
|
}
|
|
@@ -5688,6 +6911,90 @@ export default function (pi: ExtensionAPI) {
|
|
|
5688
6911
|
return;
|
|
5689
6912
|
}
|
|
5690
6913
|
|
|
6914
|
+
if (msg.type === "compact_request") {
|
|
6915
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
6916
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
6917
|
+
return;
|
|
6918
|
+
}
|
|
6919
|
+
if (isStudioBusy()) {
|
|
6920
|
+
sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
|
|
6921
|
+
return;
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
const compactCtx = lastCommandCtx;
|
|
6925
|
+
if (!compactCtx) {
|
|
6926
|
+
sendToClient(client, {
|
|
6927
|
+
type: "error",
|
|
6928
|
+
requestId: msg.requestId,
|
|
6929
|
+
message: "No interactive pi context is available to run compaction.",
|
|
6930
|
+
});
|
|
6931
|
+
return;
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
const customInstructions = typeof msg.customInstructions === "string" && msg.customInstructions.trim()
|
|
6935
|
+
? msg.customInstructions.trim()
|
|
6936
|
+
: undefined;
|
|
6937
|
+
if (customInstructions && customInstructions.length > 2000) {
|
|
6938
|
+
sendToClient(client, {
|
|
6939
|
+
type: "error",
|
|
6940
|
+
requestId: msg.requestId,
|
|
6941
|
+
message: "Compaction instructions are too long (max 2000 characters).",
|
|
6942
|
+
});
|
|
6943
|
+
return;
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6946
|
+
compactInProgress = true;
|
|
6947
|
+
compactRequestId = msg.requestId;
|
|
6948
|
+
refreshContextUsage(compactCtx);
|
|
6949
|
+
emitDebugEvent("compact_start", {
|
|
6950
|
+
requestId: msg.requestId,
|
|
6951
|
+
hasCustomInstructions: Boolean(customInstructions),
|
|
6952
|
+
});
|
|
6953
|
+
broadcast({ type: "request_started", requestId: msg.requestId, kind: "compact" });
|
|
6954
|
+
broadcastState();
|
|
6955
|
+
|
|
6956
|
+
const finishCompaction = (result: { type: "compaction_completed" | "compaction_error"; message: string }) => {
|
|
6957
|
+
if (!compactInProgress || compactRequestId !== msg.requestId) return;
|
|
6958
|
+
clearCompactionState();
|
|
6959
|
+
refreshContextUsage(compactCtx);
|
|
6960
|
+
emitDebugEvent(result.type, { requestId: msg.requestId, message: result.message });
|
|
6961
|
+
broadcast({
|
|
6962
|
+
type: result.type,
|
|
6963
|
+
requestId: msg.requestId,
|
|
6964
|
+
message: result.message,
|
|
6965
|
+
busy: isStudioBusy(),
|
|
6966
|
+
contextTokens: contextUsageSnapshot.tokens,
|
|
6967
|
+
contextWindow: contextUsageSnapshot.contextWindow,
|
|
6968
|
+
contextPercent: contextUsageSnapshot.percent,
|
|
6969
|
+
});
|
|
6970
|
+
broadcastState();
|
|
6971
|
+
};
|
|
6972
|
+
|
|
6973
|
+
try {
|
|
6974
|
+
compactCtx.compact({
|
|
6975
|
+
customInstructions,
|
|
6976
|
+
onComplete: () => {
|
|
6977
|
+
finishCompaction({
|
|
6978
|
+
type: "compaction_completed",
|
|
6979
|
+
message: "Compaction completed.",
|
|
6980
|
+
});
|
|
6981
|
+
},
|
|
6982
|
+
onError: (error) => {
|
|
6983
|
+
finishCompaction({
|
|
6984
|
+
type: "compaction_error",
|
|
6985
|
+
message: `Compaction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6986
|
+
});
|
|
6987
|
+
},
|
|
6988
|
+
});
|
|
6989
|
+
} catch (error) {
|
|
6990
|
+
finishCompaction({
|
|
6991
|
+
type: "compaction_error",
|
|
6992
|
+
message: `Failed to start compaction: ${error instanceof Error ? error.message : String(error)}`,
|
|
6993
|
+
});
|
|
6994
|
+
}
|
|
6995
|
+
return;
|
|
6996
|
+
}
|
|
6997
|
+
|
|
5691
6998
|
if (msg.type === "save_as_request") {
|
|
5692
6999
|
if (!isValidRequestId(msg.requestId)) {
|
|
5693
7000
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
@@ -6068,7 +7375,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6068
7375
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
6069
7376
|
"Cross-Origin-Resource-Policy": "same-origin",
|
|
6070
7377
|
});
|
|
6071
|
-
|
|
7378
|
+
refreshContextUsage();
|
|
7379
|
+
res.end(buildStudioHtml(initialStudioDocument, lastCommandCtx?.ui.theme, currentModelLabel, terminalSessionLabel, contextUsageSnapshot));
|
|
6072
7380
|
};
|
|
6073
7381
|
|
|
6074
7382
|
const ensureServer = async (): Promise<StudioServerState> => {
|
|
@@ -6199,6 +7507,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6199
7507
|
const stopServer = async () => {
|
|
6200
7508
|
if (!serverState) return;
|
|
6201
7509
|
clearActiveRequest();
|
|
7510
|
+
clearCompactionState();
|
|
6202
7511
|
closeAllClients(1001, "Server shutting down");
|
|
6203
7512
|
|
|
6204
7513
|
const state = serverState;
|
|
@@ -6221,43 +7530,52 @@ export default function (pi: ExtensionAPI) {
|
|
|
6221
7530
|
};
|
|
6222
7531
|
|
|
6223
7532
|
const hydrateLatestAssistant = (entries: SessionEntry[]) => {
|
|
6224
|
-
|
|
6225
|
-
if (!latest) return;
|
|
6226
|
-
lastStudioResponse = {
|
|
6227
|
-
markdown: latest,
|
|
6228
|
-
timestamp: Date.now(),
|
|
6229
|
-
kind: inferStudioResponseKind(latest),
|
|
6230
|
-
};
|
|
7533
|
+
syncStudioResponseHistory(entries);
|
|
6231
7534
|
};
|
|
6232
7535
|
|
|
6233
7536
|
pi.on("session_start", async (_event, ctx) => {
|
|
6234
7537
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
7538
|
+
clearCompactionState();
|
|
6235
7539
|
agentBusy = false;
|
|
6236
|
-
refreshRuntimeMetadata(ctx);
|
|
7540
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
7541
|
+
refreshContextUsage(ctx);
|
|
6237
7542
|
emitDebugEvent("session_start", {
|
|
6238
7543
|
entryCount: ctx.sessionManager.getBranch().length,
|
|
6239
7544
|
modelLabel: currentModelLabel,
|
|
6240
7545
|
terminalSessionLabel,
|
|
6241
7546
|
});
|
|
6242
7547
|
setTerminalActivity("idle");
|
|
7548
|
+
broadcastResponseHistory();
|
|
6243
7549
|
});
|
|
6244
7550
|
|
|
6245
7551
|
pi.on("session_switch", async (_event, ctx) => {
|
|
6246
7552
|
clearActiveRequest({ notify: "Session switched. Studio request state cleared.", level: "warning" });
|
|
7553
|
+
clearCompactionState();
|
|
6247
7554
|
lastCommandCtx = null;
|
|
6248
7555
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
6249
7556
|
agentBusy = false;
|
|
6250
|
-
refreshRuntimeMetadata(ctx);
|
|
7557
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
7558
|
+
refreshContextUsage(ctx);
|
|
6251
7559
|
emitDebugEvent("session_switch", {
|
|
6252
7560
|
entryCount: ctx.sessionManager.getBranch().length,
|
|
6253
7561
|
modelLabel: currentModelLabel,
|
|
6254
7562
|
terminalSessionLabel,
|
|
6255
7563
|
});
|
|
6256
7564
|
setTerminalActivity("idle");
|
|
7565
|
+
broadcastResponseHistory();
|
|
7566
|
+
});
|
|
7567
|
+
|
|
7568
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
7569
|
+
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
7570
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
7571
|
+
refreshContextUsage(ctx);
|
|
7572
|
+
broadcastResponseHistory();
|
|
7573
|
+
broadcastState();
|
|
6257
7574
|
});
|
|
6258
7575
|
|
|
6259
7576
|
pi.on("model_select", async (event, ctx) => {
|
|
6260
7577
|
refreshRuntimeMetadata({ cwd: ctx.cwd, model: event.model });
|
|
7578
|
+
refreshContextUsage(ctx);
|
|
6261
7579
|
emitDebugEvent("model_select", {
|
|
6262
7580
|
modelLabel: currentModelLabel,
|
|
6263
7581
|
source: event.source,
|
|
@@ -6303,7 +7621,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6303
7621
|
}
|
|
6304
7622
|
});
|
|
6305
7623
|
|
|
6306
|
-
pi.on("message_end", async (event) => {
|
|
7624
|
+
pi.on("message_end", async (event, ctx) => {
|
|
6307
7625
|
const message = event.message as { stopReason?: string; role?: string };
|
|
6308
7626
|
const stopReason = typeof message.stopReason === "string" ? message.stopReason : "";
|
|
6309
7627
|
const role = typeof message.role === "string" ? message.role : "";
|
|
@@ -6329,12 +7647,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
6329
7647
|
|
|
6330
7648
|
if (!markdown) return;
|
|
6331
7649
|
|
|
7650
|
+
syncStudioResponseHistory(ctx.sessionManager.getBranch());
|
|
7651
|
+
refreshContextUsage(ctx);
|
|
7652
|
+
const latestHistoryItem = studioResponseHistory[studioResponseHistory.length - 1];
|
|
7653
|
+
if (!latestHistoryItem || latestHistoryItem.markdown !== markdown) {
|
|
7654
|
+
const fallbackPrompt = studioResponseHistory.length > 0
|
|
7655
|
+
? studioResponseHistory[studioResponseHistory.length - 1]?.prompt ?? null
|
|
7656
|
+
: null;
|
|
7657
|
+
const fallbackHistoryItem: StudioResponseHistoryItem = {
|
|
7658
|
+
id: randomUUID(),
|
|
7659
|
+
markdown,
|
|
7660
|
+
timestamp: Date.now(),
|
|
7661
|
+
kind: inferStudioResponseKind(markdown),
|
|
7662
|
+
prompt: fallbackPrompt,
|
|
7663
|
+
};
|
|
7664
|
+
const nextHistory = [...studioResponseHistory, fallbackHistoryItem];
|
|
7665
|
+
studioResponseHistory = nextHistory.slice(-RESPONSE_HISTORY_LIMIT);
|
|
7666
|
+
}
|
|
7667
|
+
|
|
7668
|
+
const latestItem = studioResponseHistory[studioResponseHistory.length - 1];
|
|
7669
|
+
const responseTimestamp = latestItem?.timestamp ?? Date.now();
|
|
7670
|
+
|
|
6332
7671
|
if (activeRequest) {
|
|
6333
7672
|
const requestId = activeRequest.id;
|
|
6334
7673
|
const kind = activeRequest.kind;
|
|
6335
7674
|
lastStudioResponse = {
|
|
6336
7675
|
markdown,
|
|
6337
|
-
timestamp:
|
|
7676
|
+
timestamp: responseTimestamp,
|
|
6338
7677
|
kind,
|
|
6339
7678
|
};
|
|
6340
7679
|
emitDebugEvent("broadcast_response", {
|
|
@@ -6349,7 +7688,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
6349
7688
|
kind,
|
|
6350
7689
|
markdown,
|
|
6351
7690
|
timestamp: lastStudioResponse.timestamp,
|
|
7691
|
+
responseHistory: studioResponseHistory,
|
|
6352
7692
|
});
|
|
7693
|
+
broadcastResponseHistory();
|
|
6353
7694
|
clearActiveRequest();
|
|
6354
7695
|
return;
|
|
6355
7696
|
}
|
|
@@ -6357,7 +7698,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6357
7698
|
const inferredKind = inferStudioResponseKind(markdown);
|
|
6358
7699
|
lastStudioResponse = {
|
|
6359
7700
|
markdown,
|
|
6360
|
-
timestamp:
|
|
7701
|
+
timestamp: responseTimestamp,
|
|
6361
7702
|
kind: inferredKind,
|
|
6362
7703
|
};
|
|
6363
7704
|
emitDebugEvent("broadcast_latest_response", {
|
|
@@ -6370,11 +7711,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
6370
7711
|
kind: inferredKind,
|
|
6371
7712
|
markdown,
|
|
6372
7713
|
timestamp: lastStudioResponse.timestamp,
|
|
7714
|
+
responseHistory: studioResponseHistory,
|
|
6373
7715
|
});
|
|
7716
|
+
broadcastResponseHistory();
|
|
6374
7717
|
});
|
|
6375
7718
|
|
|
6376
7719
|
pi.on("agent_end", async () => {
|
|
6377
7720
|
agentBusy = false;
|
|
7721
|
+
refreshContextUsage();
|
|
6378
7722
|
emitDebugEvent("agent_end", { activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
|
|
6379
7723
|
setTerminalActivity("idle");
|
|
6380
7724
|
if (activeRequest) {
|
|
@@ -6391,12 +7735,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
6391
7735
|
pi.on("session_shutdown", async () => {
|
|
6392
7736
|
lastCommandCtx = null;
|
|
6393
7737
|
agentBusy = false;
|
|
7738
|
+
clearCompactionState();
|
|
6394
7739
|
setTerminalActivity("idle");
|
|
6395
7740
|
await stopServer();
|
|
6396
7741
|
});
|
|
6397
7742
|
|
|
6398
7743
|
pi.registerCommand("studio", {
|
|
6399
|
-
description: "Open
|
|
7744
|
+
description: "Open pi Studio browser UI (/studio, /studio <file>, /studio --blank, /studio --last)",
|
|
6400
7745
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
6401
7746
|
const trimmed = args.trim();
|
|
6402
7747
|
|
|
@@ -6434,8 +7779,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
6434
7779
|
|
|
6435
7780
|
await ctx.waitForIdle();
|
|
6436
7781
|
lastCommandCtx = ctx;
|
|
6437
|
-
refreshRuntimeMetadata(ctx);
|
|
7782
|
+
refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
|
|
7783
|
+
refreshContextUsage(ctx);
|
|
7784
|
+
syncStudioResponseHistory(ctx.sessionManager.getBranch());
|
|
6438
7785
|
broadcastState();
|
|
7786
|
+
broadcastResponseHistory();
|
|
7787
|
+
void maybeNotifyUpdateAvailable(ctx);
|
|
6439
7788
|
// Seed theme vars so first ping doesn't trigger a false update
|
|
6440
7789
|
try {
|
|
6441
7790
|
const currentStyle = getStudioThemeStyle(ctx.ui.theme);
|
|
@@ -6523,14 +7872,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
6523
7872
|
try {
|
|
6524
7873
|
await openUrlInDefaultBrowser(url);
|
|
6525
7874
|
if (initialStudioDocument?.source === "file") {
|
|
6526
|
-
ctx.ui.notify(`Opened
|
|
7875
|
+
ctx.ui.notify(`Opened pi Studio with file loaded: ${initialStudioDocument.label}`, "info");
|
|
6527
7876
|
} else if (initialStudioDocument?.source === "last-response") {
|
|
6528
7877
|
ctx.ui.notify(
|
|
6529
|
-
`Opened
|
|
7878
|
+
`Opened pi Studio with last model response (${initialStudioDocument.text.length} chars).`,
|
|
6530
7879
|
"info",
|
|
6531
7880
|
);
|
|
6532
7881
|
} else {
|
|
6533
|
-
ctx.ui.notify("Opened
|
|
7882
|
+
ctx.ui.notify("Opened pi Studio with blank editor.", "info");
|
|
6534
7883
|
}
|
|
6535
7884
|
ctx.ui.notify(`Studio URL: ${url}`, "info");
|
|
6536
7885
|
} catch (error) {
|