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/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>Pi Studio: Feedback Workspace</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 hint"
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> Pi Studio <span class="app-subtitle">Editor & Response Workspace</span></h1>
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
- <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>
2472
- <button id="insertHeaderBtn" type="button" title="Prepends/updates the annotated-reply header in the editor.">Insert annotation header</button>
2473
- <select id="lensSelect" aria-label="Critique focus">
2474
- <option value="auto" selected>Critique focus: Auto</option>
2475
- <option value="writing">Critique focus: Writing</option>
2476
- <option value="code">Critique focus: Code</option>
2477
- </select>
2478
- <button id="critiqueBtn" type="button">Critique editor text</button>
2479
- <button id="sendEditorBtn" type="button">Send to pi editor</button>
2480
- <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
2481
- <button id="copyDraftBtn" type="button">Copy editor text</button>
2482
- <select id="highlightSelect" aria-label="Editor syntax highlighting">
2483
- <option value="off">Syntax highlight: Off</option>
2484
- <option value="on" selected>Syntax highlight: On</option>
2485
- </select>
2486
- <select id="langSelect" aria-label="Highlight language">
2487
- <option value="markdown" selected>Lang: Markdown</option>
2488
- <option value="javascript">Lang: JavaScript</option>
2489
- <option value="typescript">Lang: TypeScript</option>
2490
- <option value="python">Lang: Python</option>
2491
- <option value="bash">Lang: Bash</option>
2492
- <option value="json">Lang: JSON</option>
2493
- <option value="rust">Lang: Rust</option>
2494
- <option value="c">Lang: C</option>
2495
- <option value="cpp">Lang: C++</option>
2496
- <option value="julia">Lang: Julia</option>
2497
- <option value="fortran">Lang: Fortran</option>
2498
- <option value="r">Lang: R</option>
2499
- <option value="matlab">Lang: MATLAB</option>
2500
- <option value="latex">Lang: LaTeX</option>
2501
- <option value="diff">Lang: Diff</option>
2502
- <option value="java">Lang: Java</option>
2503
- <option value="go">Lang: Go</option>
2504
- <option value="ruby">Lang: Ruby</option>
2505
- <option value="swift">Lang: Swift</option>
2506
- <option value="html">Lang: HTML</option>
2507
- <option value="css">Lang: CSS</option>
2508
- <option value="xml">Lang: XML</option>
2509
- <option value="yaml">Lang: YAML</option>
2510
- <option value="toml">Lang: TOML</option>
2511
- <option value="lua">Lang: Lua</option>
2512
- <option value="text">Lang: Plain Text</option>
2513
- </select>
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
- <select id="rightViewSelect" aria-label="Response view mode">
2527
- <option value="markdown">Response (Raw)</option>
2528
- <option value="preview" selected>Response (Preview)</option>
2529
- <option value="editor-preview">Editor (Preview)</option>
2530
- </select>
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
- <select id="followSelect" aria-label="Auto-update response">
2539
- <option value="on" selected>Auto-update response: On</option>
2540
- <option value="off">Auto-update response: Off</option>
2541
- </select>
2542
- <select id="responseHighlightSelect" aria-label="Response markdown highlighting">
2543
- <option value="off">Syntax highlight: Off</option>
2544
- <option value="on" selected>Syntax highlight: On</option>
2545
- </select>
2546
- <button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
2547
- <button id="loadResponseBtn" type="button">Load response into editor</button>
2548
- <button id="loadCritiqueNotesBtn" type="button" hidden>Load critique notes into editor</button>
2549
- <button id="loadCritiqueFullBtn" type="button" hidden>Load full critique into editor</button>
2550
- <button id="copyResponseBtn" type="button">Copy response text</button>
2551
- <button id="exportPdfBtn" type="button" title="Export the current right-pane preview as PDF via pandoc + xelatex.">Export right preview as PDF</button>
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
- footerMetaEl.textContent = "Model: " + modelText + " · Terminal: " + terminalText;
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 updateReferenceBadge() {
3108
- if (!referenceBadgeEl) return;
3612
+ function normalizeHistoryKind(kind) {
3613
+ return kind === "critique" ? "critique" : "annotation";
3614
+ }
3109
3615
 
3110
- if (rightView === "editor-preview") {
3111
- const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
3112
- if (hasResponse) {
3113
- const time = formatReferenceTime(latestResponseTimestamp);
3114
- const suffix = time ? " · response updated " + time : " · response available";
3115
- referenceBadgeEl.textContent = "Previewing: editor text" + suffix;
3116
- } else {
3117
- referenceBadgeEl.textContent = "Previewing: editor text";
3118
- }
3119
- return;
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
- const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
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
- ? "Latest response: " + responseLabel + " · " + time
3132
- : "Latest response: " + responseLabel;
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)|(\\[[^\\]]+\\]\\([^)]+\\))/g;
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
- sourceTextEl.value = message.initialDocument.text;
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
- if (message.lastResponse && typeof message.lastResponse.markdown === "string") {
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
- if (typeof message.markdown === "string") {
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
- if (responseKind === "critique") {
4597
- setStatus("Critique ready.", "success");
4598
- } else if (responseKind === "direct") {
4599
- setStatus("Model response ready.", "success");
4600
- } else {
4601
- setStatus("Response ready.", "success");
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
- sourceTextEl.value = content;
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\\n---\\n\\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 insertOrUpdateAnnotationHeader() {
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("Annotation header already up to date.");
5879
+ setStatus("Annotated reply header already present.");
4900
5880
  return;
4901
5881
  }
4902
5882
 
4903
- sourceTextEl.value = updated;
4904
- renderSourcePreview();
5883
+ setEditorText(updated, { preserveScroll: true, preserveSelection: true });
4905
5884
  updateResultActionButtons();
4906
- setStatus(stripped.hadHeader ? "Updated annotation header source." : "Inserted annotation header.", "success");
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 (applyLatestPayload(queuedLatestResponse)) {
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 (applyLatestPayload(queuedLatestResponse)) {
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
- insertOrUpdateAnnotationHeader();
6071
+ toggleAnnotatedReplyHeader();
4993
6072
  });
4994
6073
 
4995
6074
  critiqueBtn.addEventListener("click", () => {
4996
- const documentText = sourceTextEl.value.trim();
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
- sourceTextEl.value = latestResponseMarkdown;
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
- sourceTextEl.value = notes;
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
- sourceTextEl.value = latestResponseMarkdown;
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 content = sourceTextEl.value;
5182
- if (!content.trim()) {
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: content,
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
- sourceTextEl.value = text;
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
- const model = ctx?.model ?? lastCommandCtx?.model;
5374
- const baseModelLabel = formatModelLabel(model);
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(currentModelLabel, getThinkingLevelSafe());
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
- activeRequestId: activeRequest?.id ?? null,
5482
- activeRequestKind: activeRequest?.kind ?? null,
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
- activeRequestId: activeRequest?.id ?? null,
5577
- activeRequestKind: activeRequest?.kind ?? null,
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
- res.end(buildStudioHtml(initialStudioDocument, lastCommandCtx?.ui.theme, currentModelLabel, terminalSessionLabel));
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
- const latest = extractLatestAssistantFromEntries(entries);
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: Date.now(),
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: Date.now(),
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 Pi Studio browser UI (/studio, /studio <file>, /studio --blank, /studio --last)",
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 Pi Studio with file loaded: ${initialStudioDocument.label}`, "info");
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 Pi Studio with last model response (${initialStudioDocument.text.length} chars).`,
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 Pi Studio with blank editor.", "info");
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) {