pi-studio 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to `pi-studio` are documented here.
4
4
 
5
+ ## [0.4.2] — 2026-03-03
6
+
7
+ ### Added
8
+ - New editor action: **Load from pi editor** to pull the current terminal editor draft into Studio.
9
+ - Optional Studio debug tracing (`?debug=1`) with client/server lifecycle events for request/state/tool diagnostics.
10
+
11
+ ### Changed
12
+ - Footer busy status now reflects Studio-owned and terminal-owned activity phases more clearly (`running`, `tool`, `responding`).
13
+ - Tool activity labels are derived from tool calls/executions with improved command classification for shell workflows (including current/parent directory listings and listing-like `find` commands).
14
+ - Studio request ownership remains sticky during active/agent-busy phases to avoid confusing Studio → Terminal label flips mid-turn.
15
+ - Editor and response preview panes keep previous rendered content visible while a new render is in flight, using a subtle delayed **Updating** indicator instead of replacing content with a loading screen.
16
+ - Footer shortcut hint and run-button tooltip now explicitly document `Cmd/Ctrl+Enter` for **Run editor text**.
17
+
18
+ ### Fixed
19
+ - Studio requests are no longer cleared prematurely when assistant messages end with `stopReason: "toolUse"`.
20
+ - Embedded-script activity label normalization now preserves whitespace correctly (fixes corrupted labels caused by escaped regex mismatch).
21
+
5
22
  ## [0.4.1] — 2026-03-03
6
23
 
7
24
  ### Changed
package/README.md CHANGED
@@ -49,9 +49,9 @@ Experimental extension for [pi](https://github.com/badlogic/pi-mono) that opens
49
49
  - Response load helpers:
50
50
  - non-critique: **Load response into editor**
51
51
  - critique: **Load critique notes into editor** / **Load full critique into editor**
52
- - File actions: **Save editor as…**, **Save editor**, **Load file content**
52
+ - File/editor actions: **Save editor as…**, **Save editor**, **Load file content**, **Send to pi editor**, **Load from pi editor**
53
53
  - View toggles: panel header dropdowns for `Editor (Raw|Preview)` and `Response (Raw|Preview) | Editor (Preview)`
54
- - **Editor Preview in response pane**: side-by-side source/rendered view (Overleaf-style) — select `Right: Editor (Preview)` to render editor text in the right pane with live debounced updates
54
+ - **Editor Preview in response pane**: side-by-side source/rendered view (Overleaf-style) — select `Right: Editor (Preview)` to render editor text in the right pane with live updates
55
55
  - Preview mode supports MathML equations and Mermaid fenced diagrams
56
56
  - **Language-aware syntax highlighting** with selectable language mode:
57
57
  - Markdown (default): headings, links, code fences, lists, quotes, inline code
@@ -67,6 +67,8 @@ Experimental extension for [pi](https://github.com/badlogic/pi-mono) that opens
67
67
  - **Working directory**: "Set working dir" button for uploaded files — resolves relative image paths and enables "Save editor" for uploaded content
68
68
  - **Live theme sync**: changing the pi theme in the terminal updates the studio browser UI automatically (polled every 2 seconds)
69
69
  - Separate syntax highlight toggles for editor and response Raw views, with local preference persistence
70
+ - Keyboard shortcuts: `Cmd/Ctrl+Enter` runs **Run editor text** when editor pane is active; `Cmd/Ctrl+Esc` / `F10` toggles focus mode; `Esc` exits focus mode
71
+ - Footer status reflects Studio/terminal activity phases (connecting, ready, submitting, terminal activity)
70
72
  - Theme-aware browser UI derived from current pi theme
71
73
  - View mode selectors integrated into panel headers for a cleaner layout
72
74
 
package/index.ts CHANGED
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { readFileSync, statSync, writeFileSync } from "node:fs";
5
5
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
6
- import { dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { URL } from "node:url";
8
8
  import { WebSocketServer, WebSocket, type RawData } from "ws";
9
9
 
@@ -11,6 +11,7 @@ type Lens = "writing" | "code";
11
11
  type RequestedLens = Lens | "auto";
12
12
  type StudioRequestKind = "critique" | "annotation" | "direct";
13
13
  type StudioSourceKind = "file" | "last-response" | "blank";
14
+ type TerminalActivityPhase = "idle" | "running" | "tool" | "responding";
14
15
 
15
16
  interface StudioServerState {
16
17
  server: Server;
@@ -90,6 +91,11 @@ interface SendToEditorRequestMessage {
90
91
  content: string;
91
92
  }
92
93
 
94
+ interface GetFromEditorRequestMessage {
95
+ type: "get_from_editor_request";
96
+ requestId: string;
97
+ }
98
+
93
99
  type IncomingStudioMessage =
94
100
  | HelloMessage
95
101
  | PingMessage
@@ -99,7 +105,8 @@ type IncomingStudioMessage =
99
105
  | SendRunRequestMessage
100
106
  | SaveAsRequestMessage
101
107
  | SaveOverRequestMessage
102
- | SendToEditorRequestMessage;
108
+ | SendToEditorRequestMessage
109
+ | GetFromEditorRequestMessage;
103
110
 
104
111
  const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
105
112
  const PREVIEW_RENDER_MAX_CHARS = 400_000;
@@ -1133,9 +1140,146 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
1133
1140
  };
1134
1141
  }
1135
1142
 
1143
+ if (msg.type === "get_from_editor_request" && typeof msg.requestId === "string") {
1144
+ return {
1145
+ type: "get_from_editor_request",
1146
+ requestId: msg.requestId,
1147
+ };
1148
+ }
1149
+
1136
1150
  return null;
1137
1151
  }
1138
1152
 
1153
+ function normalizeActivityLabel(label: string): string | null {
1154
+ const compact = String(label || "").replace(/\s+/g, " ").trim();
1155
+ if (!compact) return null;
1156
+ if (compact.length <= 96) return compact;
1157
+ return `${compact.slice(0, 93).trimEnd()}…`;
1158
+ }
1159
+
1160
+ function isGenericToolActivityLabel(label: string | null | undefined): boolean {
1161
+ const normalized = String(label || "").trim().toLowerCase();
1162
+ if (!normalized) return true;
1163
+ return normalized.startsWith("running ")
1164
+ || normalized === "reading file"
1165
+ || normalized === "writing file"
1166
+ || normalized === "editing file";
1167
+ }
1168
+
1169
+ function deriveBashActivityLabel(command: string): string | null {
1170
+ const normalized = String(command || "").trim();
1171
+ if (!normalized) return null;
1172
+ const lower = normalized.toLowerCase();
1173
+
1174
+ const segments = lower
1175
+ .split(/(?:&&|\|\||;|\n)+/g)
1176
+ .map((segment) => segment.trim())
1177
+ .filter((segment) => segment.length > 0);
1178
+
1179
+ let hasPwd = false;
1180
+ let hasLsCurrent = false;
1181
+ let hasLsParent = false;
1182
+ let hasFind = false;
1183
+ let hasFindCurrentListing = false;
1184
+ let hasFindParentListing = false;
1185
+
1186
+ for (const segment of segments) {
1187
+ if (/\bpwd\b/.test(segment)) hasPwd = true;
1188
+
1189
+ if (/\bls\b/.test(segment)) {
1190
+ if (/\.\./.test(segment)) hasLsParent = true;
1191
+ else hasLsCurrent = true;
1192
+ }
1193
+
1194
+ if (/\bfind\b/.test(segment)) {
1195
+ hasFind = true;
1196
+ const pathMatch = segment.match(/\bfind\s+([^\s]+)/);
1197
+ const pathToken = pathMatch ? pathMatch[1] : "";
1198
+ const hasSelector = /-(?:name|iname|regex|path|ipath|newer|mtime|mmin|size|user|group)\b/.test(segment);
1199
+ const listingLike = /-maxdepth\s+\d+\b/.test(segment) && !hasSelector;
1200
+
1201
+ if (listingLike) {
1202
+ if (pathToken === ".." || pathToken === "../") {
1203
+ hasFindParentListing = true;
1204
+ } else if (pathToken === "." || pathToken === "./" || pathToken === "") {
1205
+ hasFindCurrentListing = true;
1206
+ }
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ const hasCurrentListing = hasLsCurrent || hasFindCurrentListing;
1212
+ const hasParentListing = hasLsParent || hasFindParentListing;
1213
+
1214
+ if (hasCurrentListing && hasParentListing) {
1215
+ return "Listing directory and parent directory files";
1216
+ }
1217
+ if (hasPwd && hasCurrentListing) {
1218
+ return "Listing current directory files";
1219
+ }
1220
+ if (hasParentListing) {
1221
+ return "Listing parent directory files";
1222
+ }
1223
+ if (hasCurrentListing || /\bls\b/.test(lower)) {
1224
+ return "Listing directory files";
1225
+ }
1226
+ if (hasFind || /\bfind\b/.test(lower)) {
1227
+ return "Searching files";
1228
+ }
1229
+ if (/\brg\b/.test(lower) || /\bgrep\b/.test(lower)) {
1230
+ return "Searching text in files";
1231
+ }
1232
+ if (/\bcat\b/.test(lower) || /\bsed\b/.test(lower) || /\bawk\b/.test(lower)) {
1233
+ return "Reading file content";
1234
+ }
1235
+ if (/\bgit\s+status\b/.test(lower)) {
1236
+ return "Checking git status";
1237
+ }
1238
+ if (/\bgit\s+diff\b/.test(lower)) {
1239
+ return "Reviewing git changes";
1240
+ }
1241
+ if (/\bgit\b/.test(lower)) {
1242
+ return "Running git command";
1243
+ }
1244
+ if (/\bnpm\b/.test(lower)) {
1245
+ return "Running npm command";
1246
+ }
1247
+ if (/\bpython3?\b/.test(lower)) {
1248
+ return "Running Python command";
1249
+ }
1250
+ if (/\bnode\b/.test(lower)) {
1251
+ return "Running Node.js command";
1252
+ }
1253
+ return "Running shell command";
1254
+ }
1255
+
1256
+ function deriveToolActivityLabel(toolName: string, args: unknown): string | null {
1257
+ const normalizedTool = String(toolName || "").trim().toLowerCase();
1258
+ const payload = (args && typeof args === "object") ? (args as Record<string, unknown>) : {};
1259
+
1260
+ if (normalizedTool === "bash") {
1261
+ const command = typeof payload.command === "string" ? payload.command : "";
1262
+ return deriveBashActivityLabel(command);
1263
+ }
1264
+ if (normalizedTool === "read") {
1265
+ const path = typeof payload.path === "string" ? payload.path : "";
1266
+ return path ? `Reading ${basename(path)}` : "Reading file";
1267
+ }
1268
+ if (normalizedTool === "write") {
1269
+ const path = typeof payload.path === "string" ? payload.path : "";
1270
+ return path ? `Writing ${basename(path)}` : "Writing file";
1271
+ }
1272
+ if (normalizedTool === "edit") {
1273
+ const path = typeof payload.path === "string" ? payload.path : "";
1274
+ return path ? `Editing ${basename(path)}` : "Editing file";
1275
+ }
1276
+ if (normalizedTool === "find") return "Searching files";
1277
+ if (normalizedTool === "grep") return "Searching text in files";
1278
+ if (normalizedTool === "ls") return "Listing directory files";
1279
+
1280
+ return normalizeActivityLabel(`Running ${normalizedTool || "tool"}`);
1281
+ }
1282
+
1139
1283
  function isAllowedOrigin(_origin: string | undefined, _port: number): boolean {
1140
1284
  // For local-only studio, token auth is the primary guard. In practice,
1141
1285
  // browser origin headers can vary (or be omitted) across wrappers/browsers,
@@ -1696,6 +1840,7 @@ ${cssVarsBlock}
1696
1840
  }
1697
1841
 
1698
1842
  .panel-scroll {
1843
+ position: relative;
1699
1844
  min-height: 0;
1700
1845
  overflow: auto;
1701
1846
  padding: 12px;
@@ -1968,6 +2113,19 @@ ${cssVarsBlock}
1968
2113
  font-style: italic;
1969
2114
  }
1970
2115
 
2116
+ .panel-scroll.preview-pending::after {
2117
+ content: "Updating";
2118
+ position: absolute;
2119
+ top: 10px;
2120
+ right: 12px;
2121
+ color: var(--muted);
2122
+ font-size: 10px;
2123
+ line-height: 1.2;
2124
+ letter-spacing: 0.01em;
2125
+ pointer-events: none;
2126
+ opacity: 0.64;
2127
+ }
2128
+
1971
2129
  .preview-error {
1972
2130
  color: var(--warn);
1973
2131
  margin-bottom: 0.75em;
@@ -2089,7 +2247,7 @@ ${cssVarsBlock}
2089
2247
  <span id="syncBadge" class="source-badge sync-badge">No response loaded</span>
2090
2248
  </div>
2091
2249
  <div class="source-actions">
2092
- <button id="sendRunBtn" type="button" title="Send editor text directly to the model as-is.">Run editor text</button>
2250
+ <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>
2093
2251
  <button id="insertHeaderBtn" type="button" title="Prepends/updates the annotated-reply header in the editor.">Insert annotation header</button>
2094
2252
  <select id="lensSelect" aria-label="Critique focus">
2095
2253
  <option value="auto" selected>Critique focus: Auto</option>
@@ -2098,6 +2256,7 @@ ${cssVarsBlock}
2098
2256
  </select>
2099
2257
  <button id="critiqueBtn" type="button">Critique editor text</button>
2100
2258
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
2259
+ <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
2101
2260
  <button id="copyDraftBtn" type="button">Copy editor text</button>
2102
2261
  <select id="highlightSelect" aria-label="Editor syntax highlighting">
2103
2262
  <option value="off">Syntax highlight: Off</option>
@@ -2175,7 +2334,7 @@ ${cssVarsBlock}
2175
2334
 
2176
2335
  <footer>
2177
2336
  <span id="status">Booting studio…</span>
2178
- <span class="shortcut-hint">Focus pane: Cmd/Ctrl+Esc (or F10), Esc to exit</span>
2337
+ <span class="shortcut-hint">Focus pane: Cmd/Ctrl+Esc (or F10), Esc to exit · Run editor text: Cmd/Ctrl+Enter</span>
2179
2338
  </footer>
2180
2339
 
2181
2340
  <!-- Defer sanitizer script so studio can boot/connect even if CDN is slow or blocked. -->
@@ -2235,6 +2394,7 @@ ${cssVarsBlock}
2235
2394
  const saveAsBtn = document.getElementById("saveAsBtn");
2236
2395
  const saveOverBtn = document.getElementById("saveOverBtn");
2237
2396
  const sendEditorBtn = document.getElementById("sendEditorBtn");
2397
+ const getEditorBtn = document.getElementById("getEditorBtn");
2238
2398
  const sendRunBtn = document.getElementById("sendRunBtn");
2239
2399
  const copyDraftBtn = document.getElementById("copyDraftBtn");
2240
2400
  const highlightSelect = document.getElementById("highlightSelect");
@@ -2252,6 +2412,7 @@ ${cssVarsBlock}
2252
2412
  let statusLevel = "";
2253
2413
  let pendingRequestId = null;
2254
2414
  let pendingKind = null;
2415
+ let stickyStudioKind = null;
2255
2416
  let initialDocumentApplied = false;
2256
2417
  let editorView = "markdown";
2257
2418
  let rightView = "preview";
@@ -2265,6 +2426,11 @@ ${cssVarsBlock}
2265
2426
  let latestResponseNormalized = "";
2266
2427
  let latestCritiqueNotes = "";
2267
2428
  let latestCritiqueNotesNormalized = "";
2429
+ let agentBusyFromServer = false;
2430
+ let terminalActivityPhase = "idle";
2431
+ let terminalActivityToolName = "";
2432
+ let terminalActivityLabel = "";
2433
+ let lastSpecificToolLabel = "";
2268
2434
  let uiBusy = false;
2269
2435
  let sourceState = {
2270
2436
  source: initialSourceState.source,
@@ -2317,6 +2483,8 @@ ${cssVarsBlock}
2317
2483
  const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
2318
2484
  const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
2319
2485
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
2486
+ const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
2487
+ const previewPendingTimers = new WeakMap();
2320
2488
  let sourcePreviewRenderTimer = null;
2321
2489
  let sourcePreviewRenderNonce = 0;
2322
2490
  let responsePreviewRenderNonce = 0;
@@ -2333,8 +2501,153 @@ ${cssVarsBlock}
2333
2501
  let mermaidModulePromise = null;
2334
2502
  let mermaidInitialized = false;
2335
2503
 
2504
+ const DEBUG_ENABLED = (() => {
2505
+ try {
2506
+ const query = new URLSearchParams(window.location.search || "");
2507
+ const hash = new URLSearchParams((window.location.hash || "").replace(/^#/, ""));
2508
+ const value = String(query.get("debug") || hash.get("debug") || "").trim().toLowerCase();
2509
+ return value === "1" || value === "true" || value === "yes" || value === "on";
2510
+ } catch {
2511
+ return false;
2512
+ }
2513
+ })();
2514
+ const DEBUG_LOG_MAX = 400;
2515
+ const debugLog = [];
2516
+
2517
+ function debugTrace(eventName, payload) {
2518
+ if (!DEBUG_ENABLED) return;
2519
+ const entry = {
2520
+ ts: Date.now(),
2521
+ event: String(eventName || ""),
2522
+ payload: payload || null,
2523
+ };
2524
+ debugLog.push(entry);
2525
+ if (debugLog.length > DEBUG_LOG_MAX) debugLog.shift();
2526
+ window.__piStudioDebugLog = debugLog.slice();
2527
+ try {
2528
+ console.debug("[pi-studio]", new Date(entry.ts).toISOString(), entry.event, entry.payload);
2529
+ } catch {
2530
+ // ignore console errors
2531
+ }
2532
+ }
2533
+
2534
+ function summarizeServerMessage(message) {
2535
+ if (!message || typeof message !== "object") return { type: "invalid" };
2536
+ const summary = {
2537
+ type: typeof message.type === "string" ? message.type : "unknown",
2538
+ };
2539
+ if (typeof message.requestId === "string") summary.requestId = message.requestId;
2540
+ if (typeof message.activeRequestId === "string") summary.activeRequestId = message.activeRequestId;
2541
+ if (typeof message.activeRequestKind === "string") summary.activeRequestKind = message.activeRequestKind;
2542
+ if (typeof message.kind === "string") summary.kind = message.kind;
2543
+ if (typeof message.event === "string") summary.event = message.event;
2544
+ if (typeof message.timestamp === "number") summary.timestamp = message.timestamp;
2545
+ if (typeof message.busy === "boolean") summary.busy = message.busy;
2546
+ if (typeof message.agentBusy === "boolean") summary.agentBusy = message.agentBusy;
2547
+ if (typeof message.terminalPhase === "string") summary.terminalPhase = message.terminalPhase;
2548
+ if (typeof message.terminalToolName === "string") summary.terminalToolName = message.terminalToolName;
2549
+ if (typeof message.terminalActivityLabel === "string") summary.terminalActivityLabel = message.terminalActivityLabel;
2550
+ if (typeof message.stopReason === "string") summary.stopReason = message.stopReason;
2551
+ if (typeof message.markdown === "string") summary.markdownLength = message.markdown.length;
2552
+ if (typeof message.label === "string") summary.label = message.label;
2553
+ if (typeof message.details === "object" && message.details !== null) summary.details = message.details;
2554
+ return summary;
2555
+ }
2556
+
2336
2557
  function getIdleStatus() {
2337
- return "Ready. Edit text, then run or critique (insert annotation header if needed).";
2558
+ return "Ready. Edit, load, or annotate text, then run, save, send to pi editor, or critique.";
2559
+ }
2560
+
2561
+ function normalizeTerminalPhase(phase) {
2562
+ if (phase === "running" || phase === "tool" || phase === "responding") return phase;
2563
+ return "idle";
2564
+ }
2565
+
2566
+ function normalizeActivityLabel(label) {
2567
+ if (typeof label !== "string") return "";
2568
+ return label.replace(/\\s+/g, " ").trim();
2569
+ }
2570
+
2571
+ function isGenericToolLabel(label) {
2572
+ const normalized = normalizeActivityLabel(label).toLowerCase();
2573
+ if (!normalized) return true;
2574
+ return normalized.startsWith("running ")
2575
+ || normalized === "reading file"
2576
+ || normalized === "writing file"
2577
+ || normalized === "editing file";
2578
+ }
2579
+
2580
+ function withEllipsis(text) {
2581
+ const value = String(text || "").trim();
2582
+ if (!value) return "";
2583
+ if (/[….!?]$/.test(value)) return value;
2584
+ return value + "…";
2585
+ }
2586
+
2587
+ function updateTerminalActivityState(phase, toolName, label) {
2588
+ terminalActivityPhase = normalizeTerminalPhase(phase);
2589
+ terminalActivityToolName = typeof toolName === "string" ? toolName.trim() : "";
2590
+ terminalActivityLabel = normalizeActivityLabel(label);
2591
+
2592
+ if (terminalActivityPhase === "tool" && terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
2593
+ lastSpecificToolLabel = terminalActivityLabel;
2594
+ }
2595
+ if (terminalActivityPhase === "idle") {
2596
+ lastSpecificToolLabel = "";
2597
+ }
2598
+ }
2599
+
2600
+ function getTerminalBusyStatus() {
2601
+ if (terminalActivityPhase === "tool") {
2602
+ if (terminalActivityLabel) {
2603
+ return "Terminal: " + withEllipsis(terminalActivityLabel);
2604
+ }
2605
+ return terminalActivityToolName
2606
+ ? "Terminal: running tool: " + terminalActivityToolName + "…"
2607
+ : "Terminal: running tool…";
2608
+ }
2609
+ if (terminalActivityPhase === "responding") {
2610
+ if (lastSpecificToolLabel) {
2611
+ return "Terminal: " + lastSpecificToolLabel + " (generating response)…";
2612
+ }
2613
+ return "Terminal: generating response…";
2614
+ }
2615
+ if (terminalActivityPhase === "running" && lastSpecificToolLabel) {
2616
+ return "Terminal: " + withEllipsis(lastSpecificToolLabel);
2617
+ }
2618
+ return "Terminal: running…";
2619
+ }
2620
+
2621
+ function getStudioActionLabel(kind) {
2622
+ if (kind === "annotation") return "sending annotated reply";
2623
+ if (kind === "critique") return "running critique";
2624
+ if (kind === "direct") return "running editor text";
2625
+ if (kind === "send_to_editor") return "sending to pi editor";
2626
+ if (kind === "get_from_editor") return "loading from pi editor";
2627
+ if (kind === "save_as" || kind === "save_over") return "saving editor text";
2628
+ return "submitting request";
2629
+ }
2630
+
2631
+ function getStudioBusyStatus(kind) {
2632
+ const action = getStudioActionLabel(kind);
2633
+ if (terminalActivityPhase === "tool") {
2634
+ if (terminalActivityLabel) {
2635
+ return "Studio: " + withEllipsis(terminalActivityLabel);
2636
+ }
2637
+ return terminalActivityToolName
2638
+ ? "Studio: " + action + " (tool: " + terminalActivityToolName + ")…"
2639
+ : "Studio: " + action + " (running tool)…";
2640
+ }
2641
+ if (terminalActivityPhase === "responding") {
2642
+ if (lastSpecificToolLabel) {
2643
+ return "Studio: " + lastSpecificToolLabel + " (generating response)…";
2644
+ }
2645
+ return "Studio: " + action + " (generating response)…";
2646
+ }
2647
+ if (terminalActivityPhase === "running" && lastSpecificToolLabel) {
2648
+ return "Studio: " + withEllipsis(lastSpecificToolLabel);
2649
+ }
2650
+ return "Studio: " + action + "…";
2338
2651
  }
2339
2652
 
2340
2653
  function renderStatus() {
@@ -2352,6 +2665,19 @@ ${cssVarsBlock}
2352
2665
  statusMessage = message;
2353
2666
  statusLevel = level || "";
2354
2667
  renderStatus();
2668
+ debugTrace("status", {
2669
+ wsState,
2670
+ message: statusMessage,
2671
+ level: statusLevel,
2672
+ pendingRequestId,
2673
+ pendingKind,
2674
+ uiBusy,
2675
+ agentBusyFromServer,
2676
+ terminalPhase: terminalActivityPhase,
2677
+ terminalToolName: terminalActivityToolName,
2678
+ terminalActivityLabel,
2679
+ lastSpecificToolLabel,
2680
+ });
2355
2681
  }
2356
2682
 
2357
2683
  renderStatus();
@@ -2594,6 +2920,48 @@ ${cssVarsBlock}
2594
2920
  targetEl.appendChild(el);
2595
2921
  }
2596
2922
 
2923
+ function hasMeaningfulPreviewContent(targetEl) {
2924
+ if (!targetEl || typeof targetEl.querySelector !== "function") return false;
2925
+ if (targetEl.querySelector(".preview-loading")) return false;
2926
+ const text = typeof targetEl.textContent === "string" ? targetEl.textContent.trim() : "";
2927
+ return text.length > 0;
2928
+ }
2929
+
2930
+ function beginPreviewRender(targetEl) {
2931
+ if (!targetEl || !targetEl.classList) return;
2932
+
2933
+ const pendingTimer = previewPendingTimers.get(targetEl);
2934
+ if (pendingTimer !== undefined) {
2935
+ window.clearTimeout(pendingTimer);
2936
+ previewPendingTimers.delete(targetEl);
2937
+ }
2938
+
2939
+ if (hasMeaningfulPreviewContent(targetEl)) {
2940
+ targetEl.classList.remove("preview-pending");
2941
+ const timerId = window.setTimeout(() => {
2942
+ previewPendingTimers.delete(targetEl);
2943
+ if (!targetEl || !targetEl.classList) return;
2944
+ if (!hasMeaningfulPreviewContent(targetEl)) return;
2945
+ targetEl.classList.add("preview-pending");
2946
+ }, PREVIEW_PENDING_BADGE_DELAY_MS);
2947
+ previewPendingTimers.set(targetEl, timerId);
2948
+ return;
2949
+ }
2950
+
2951
+ targetEl.classList.remove("preview-pending");
2952
+ targetEl.innerHTML = "<div class='preview-loading'>Rendering preview…</div>";
2953
+ }
2954
+
2955
+ function finishPreviewRender(targetEl) {
2956
+ if (!targetEl || !targetEl.classList) return;
2957
+ const pendingTimer = previewPendingTimers.get(targetEl);
2958
+ if (pendingTimer !== undefined) {
2959
+ window.clearTimeout(pendingTimer);
2960
+ previewPendingTimers.delete(targetEl);
2961
+ }
2962
+ targetEl.classList.remove("preview-pending");
2963
+ }
2964
+
2597
2965
  async function getMermaidApi() {
2598
2966
  if (mermaidModulePromise) {
2599
2967
  return mermaidModulePromise;
@@ -2739,6 +3107,7 @@ ${cssVarsBlock}
2739
3107
  if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
2740
3108
  }
2741
3109
 
3110
+ finishPreviewRender(targetEl);
2742
3111
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2743
3112
  await renderMermaidInElement(targetEl);
2744
3113
 
@@ -2758,6 +3127,7 @@ ${cssVarsBlock}
2758
3127
  }
2759
3128
 
2760
3129
  const detail = error && error.message ? error.message : String(error || "unknown error");
3130
+ finishPreviewRender(targetEl);
2761
3131
  targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
2762
3132
  }
2763
3133
  }
@@ -2766,11 +3136,12 @@ ${cssVarsBlock}
2766
3136
  if (editorView !== "preview") return;
2767
3137
  const text = sourceTextEl.value || "";
2768
3138
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
3139
+ finishPreviewRender(sourcePreviewEl);
2769
3140
  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>";
2770
3141
  return;
2771
3142
  }
2772
3143
  const nonce = ++sourcePreviewRenderNonce;
2773
- sourcePreviewEl.innerHTML = "<div class='preview-loading'>Rendering preview…</div>";
3144
+ beginPreviewRender(sourcePreviewEl);
2774
3145
  void applyRenderedMarkdown(sourcePreviewEl, text, "source", nonce);
2775
3146
  }
2776
3147
 
@@ -2825,34 +3196,38 @@ ${cssVarsBlock}
2825
3196
  if (rightView === "editor-preview") {
2826
3197
  const editorText = sourceTextEl.value || "";
2827
3198
  if (!editorText.trim()) {
3199
+ finishPreviewRender(critiqueViewEl);
2828
3200
  critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
2829
3201
  return;
2830
3202
  }
2831
3203
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
3204
+ finishPreviewRender(critiqueViewEl);
2832
3205
  critiqueViewEl.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(editorText, editorLanguage) + "</div>";
2833
3206
  return;
2834
3207
  }
2835
3208
  const nonce = ++responsePreviewRenderNonce;
2836
- critiqueViewEl.innerHTML = "<div class='preview-loading'>Rendering preview…</div>";
3209
+ beginPreviewRender(critiqueViewEl);
2837
3210
  void applyRenderedMarkdown(critiqueViewEl, editorText, "response", nonce);
2838
3211
  return;
2839
3212
  }
2840
3213
 
2841
3214
  const markdown = latestResponseMarkdown;
2842
3215
  if (!markdown || !markdown.trim()) {
3216
+ finishPreviewRender(critiqueViewEl);
2843
3217
  critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
2844
3218
  return;
2845
3219
  }
2846
3220
 
2847
3221
  if (rightView === "preview") {
2848
3222
  const nonce = ++responsePreviewRenderNonce;
2849
- critiqueViewEl.innerHTML = "<div class='preview-loading'>Rendering preview…</div>";
3223
+ beginPreviewRender(critiqueViewEl);
2850
3224
  void applyRenderedMarkdown(critiqueViewEl, markdown, "response", nonce);
2851
3225
  return;
2852
3226
  }
2853
3227
 
2854
3228
  if (responseHighlightEnabled) {
2855
3229
  if (markdown.length > RESPONSE_HIGHLIGHT_MAX_CHARS) {
3230
+ finishPreviewRender(critiqueViewEl);
2856
3231
  critiqueViewEl.innerHTML = buildPreviewErrorHtml(
2857
3232
  "Response is too large for markdown highlighting. Showing plain markdown.",
2858
3233
  markdown,
@@ -2860,10 +3235,12 @@ ${cssVarsBlock}
2860
3235
  return;
2861
3236
  }
2862
3237
 
3238
+ finishPreviewRender(critiqueViewEl);
2863
3239
  critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
2864
3240
  return;
2865
3241
  }
2866
3242
 
3243
+ finishPreviewRender(critiqueViewEl);
2867
3244
  critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
2868
3245
  }
2869
3246
 
@@ -2936,6 +3313,7 @@ ${cssVarsBlock}
2936
3313
  saveAsBtn.disabled = uiBusy;
2937
3314
  saveOverBtn.disabled = uiBusy || !canSaveOver;
2938
3315
  sendEditorBtn.disabled = uiBusy;
3316
+ if (getEditorBtn) getEditorBtn.disabled = uiBusy;
2939
3317
  sendRunBtn.disabled = uiBusy;
2940
3318
  copyDraftBtn.disabled = uiBusy;
2941
3319
  if (highlightSelect) highlightSelect.disabled = uiBusy;
@@ -2981,6 +3359,10 @@ ${cssVarsBlock}
2981
3359
  sourcePreviewRenderTimer = null;
2982
3360
  }
2983
3361
 
3362
+ if (!showPreview) {
3363
+ finishPreviewRender(sourcePreviewEl);
3364
+ }
3365
+
2984
3366
  if (showPreview) {
2985
3367
  renderSourcePreview();
2986
3368
  }
@@ -3665,14 +4047,30 @@ ${cssVarsBlock}
3665
4047
  function handleServerMessage(message) {
3666
4048
  if (!message || typeof message !== "object") return;
3667
4049
 
4050
+ debugTrace("server_message", summarizeServerMessage(message));
4051
+
4052
+ if (message.type === "debug_event") {
4053
+ debugTrace("server_debug_event", summarizeServerMessage(message));
4054
+ return;
4055
+ }
4056
+
3668
4057
  if (message.type === "hello_ack") {
3669
4058
  const busy = Boolean(message.busy);
4059
+ agentBusyFromServer = Boolean(message.agentBusy);
4060
+ updateTerminalActivityState(message.terminalPhase, message.terminalToolName, message.terminalActivityLabel);
3670
4061
  setBusy(busy);
3671
4062
  setWsState(busy ? "Submitting" : "Ready");
3672
- if (message.activeRequestId) {
3673
- pendingRequestId = String(message.activeRequestId);
3674
- pendingKind = "unknown";
3675
- setStatus("Request in progress…", "warning");
4063
+ if (typeof message.activeRequestId === "string" && message.activeRequestId.length > 0) {
4064
+ pendingRequestId = message.activeRequestId;
4065
+ if (typeof message.activeRequestKind === "string" && message.activeRequestKind.length > 0) {
4066
+ pendingKind = message.activeRequestKind;
4067
+ } else if (!pendingKind) {
4068
+ pendingKind = "unknown";
4069
+ }
4070
+ stickyStudioKind = pendingKind;
4071
+ } else {
4072
+ pendingRequestId = null;
4073
+ pendingKind = null;
3676
4074
  }
3677
4075
 
3678
4076
  let loadedInitialDocument = false;
@@ -3705,7 +4103,26 @@ ${cssVarsBlock}
3705
4103
  handleIncomingResponse(lastMarkdown, lastResponseKind, message.lastResponse.timestamp);
3706
4104
  }
3707
4105
 
3708
- if (!busy && !loadedInitialDocument) {
4106
+ if (pendingRequestId) {
4107
+ if (busy) {
4108
+ setStatus(getStudioBusyStatus(pendingKind), "warning");
4109
+ }
4110
+ return;
4111
+ }
4112
+
4113
+ if (busy) {
4114
+ if (agentBusyFromServer && stickyStudioKind) {
4115
+ setStatus(getStudioBusyStatus(stickyStudioKind), "warning");
4116
+ } else if (agentBusyFromServer) {
4117
+ setStatus(getTerminalBusyStatus(), "warning");
4118
+ } else {
4119
+ setStatus("Studio is busy.", "warning");
4120
+ }
4121
+ return;
4122
+ }
4123
+
4124
+ stickyStudioKind = null;
4125
+ if (!loadedInitialDocument) {
3709
4126
  refreshResponseUi();
3710
4127
  setStatus(getIdleStatus());
3711
4128
  }
@@ -3715,17 +4132,10 @@ ${cssVarsBlock}
3715
4132
  if (message.type === "request_started") {
3716
4133
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
3717
4134
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
4135
+ stickyStudioKind = pendingKind;
3718
4136
  setBusy(true);
3719
4137
  setWsState("Submitting");
3720
- if (pendingKind === "annotation") {
3721
- setStatus("Sending annotated reply…", "warning");
3722
- } else if (pendingKind === "critique") {
3723
- setStatus("Running critique…", "warning");
3724
- } else if (pendingKind === "direct") {
3725
- setStatus("Running editor text…", "warning");
3726
- } else {
3727
- setStatus("Submitting…", "warning");
3728
- }
4138
+ setStatus(getStudioBusyStatus(pendingKind), "warning");
3729
4139
  return;
3730
4140
  }
3731
4141
 
@@ -3739,6 +4149,7 @@ ${cssVarsBlock}
3739
4149
  ? message.kind
3740
4150
  : (pendingKind === "critique" ? "critique" : "annotation");
3741
4151
 
4152
+ stickyStudioKind = responseKind;
3742
4153
  pendingRequestId = null;
3743
4154
  pendingKind = null;
3744
4155
  setBusy(false);
@@ -3810,13 +4221,71 @@ ${cssVarsBlock}
3810
4221
  return;
3811
4222
  }
3812
4223
 
4224
+ if (message.type === "editor_snapshot") {
4225
+ if (typeof message.requestId === "string" && pendingRequestId && message.requestId !== pendingRequestId) {
4226
+ return;
4227
+ }
4228
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
4229
+ pendingRequestId = null;
4230
+ pendingKind = null;
4231
+ }
4232
+
4233
+ const content = typeof message.content === "string" ? message.content : "";
4234
+ sourceTextEl.value = content;
4235
+ renderSourcePreview();
4236
+ setSourceState({ source: "pi-editor", label: "pi editor draft", path: null });
4237
+ setBusy(false);
4238
+ setWsState("Ready");
4239
+ setStatus(
4240
+ content.trim()
4241
+ ? "Loaded draft from pi editor."
4242
+ : "pi editor is empty. Loaded blank text.",
4243
+ content.trim() ? "success" : "warning",
4244
+ );
4245
+ return;
4246
+ }
4247
+
3813
4248
  if (message.type === "studio_state") {
3814
4249
  const busy = Boolean(message.busy);
4250
+ agentBusyFromServer = Boolean(message.agentBusy);
4251
+ updateTerminalActivityState(message.terminalPhase, message.terminalToolName, message.terminalActivityLabel);
4252
+
4253
+ if (typeof message.activeRequestId === "string" && message.activeRequestId.length > 0) {
4254
+ pendingRequestId = message.activeRequestId;
4255
+ if (typeof message.activeRequestKind === "string" && message.activeRequestKind.length > 0) {
4256
+ pendingKind = message.activeRequestKind;
4257
+ } else if (!pendingKind) {
4258
+ pendingKind = "unknown";
4259
+ }
4260
+ stickyStudioKind = pendingKind;
4261
+ } else {
4262
+ pendingRequestId = null;
4263
+ pendingKind = null;
4264
+ }
4265
+
3815
4266
  setBusy(busy);
3816
4267
  setWsState(busy ? "Submitting" : "Ready");
3817
- if (!busy && !pendingRequestId) {
3818
- setStatus(getIdleStatus());
4268
+
4269
+ if (pendingRequestId) {
4270
+ if (busy) {
4271
+ setStatus(getStudioBusyStatus(pendingKind), "warning");
4272
+ }
4273
+ return;
3819
4274
  }
4275
+
4276
+ if (busy) {
4277
+ if (agentBusyFromServer && stickyStudioKind) {
4278
+ setStatus(getStudioBusyStatus(stickyStudioKind), "warning");
4279
+ } else if (agentBusyFromServer) {
4280
+ setStatus(getTerminalBusyStatus(), "warning");
4281
+ } else {
4282
+ setStatus("Studio is busy.", "warning");
4283
+ }
4284
+ return;
4285
+ }
4286
+
4287
+ stickyStudioKind = null;
4288
+ setStatus(getIdleStatus());
3820
4289
  return;
3821
4290
  }
3822
4291
 
@@ -3825,6 +4294,7 @@ ${cssVarsBlock}
3825
4294
  pendingRequestId = null;
3826
4295
  pendingKind = null;
3827
4296
  }
4297
+ stickyStudioKind = null;
3828
4298
  setBusy(false);
3829
4299
  setWsState("Ready");
3830
4300
  setStatus(typeof message.message === "string" ? message.message : "Studio is busy.", "warning");
@@ -3836,6 +4306,7 @@ ${cssVarsBlock}
3836
4306
  pendingRequestId = null;
3837
4307
  pendingKind = null;
3838
4308
  }
4309
+ stickyStudioKind = null;
3839
4310
  setBusy(false);
3840
4311
  setWsState("Ready");
3841
4312
  setStatus(typeof message.message === "string" ? message.message : "Request failed.", "error");
@@ -3870,7 +4341,7 @@ ${cssVarsBlock}
3870
4341
  }
3871
4342
 
3872
4343
  const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
3873
- const wsUrl = wsProtocol + "://" + window.location.host + "/ws?token=" + encodeURIComponent(token);
4344
+ const wsUrl = wsProtocol + "://" + window.location.host + "/ws?token=" + encodeURIComponent(token) + (DEBUG_ENABLED ? "&debug=1" : "");
3874
4345
 
3875
4346
  setWsState("Connecting");
3876
4347
  setStatus("Connecting to Studio server…");
@@ -3927,8 +4398,10 @@ ${cssVarsBlock}
3927
4398
  const requestId = makeRequestId();
3928
4399
  pendingRequestId = requestId;
3929
4400
  pendingKind = kind;
4401
+ stickyStudioKind = kind;
3930
4402
  setBusy(true);
3931
4403
  setWsState("Submitting");
4404
+ setStatus(getStudioBusyStatus(kind), "warning");
3932
4405
  return requestId;
3933
4406
  }
3934
4407
 
@@ -4233,6 +4706,24 @@ ${cssVarsBlock}
4233
4706
  }
4234
4707
  });
4235
4708
 
4709
+ if (getEditorBtn) {
4710
+ getEditorBtn.addEventListener("click", () => {
4711
+ const requestId = beginUiAction("get_from_editor");
4712
+ if (!requestId) return;
4713
+
4714
+ const sent = sendMessage({
4715
+ type: "get_from_editor_request",
4716
+ requestId,
4717
+ });
4718
+
4719
+ if (!sent) {
4720
+ pendingRequestId = null;
4721
+ pendingKind = null;
4722
+ setBusy(false);
4723
+ }
4724
+ });
4725
+ }
4726
+
4236
4727
  sendRunBtn.addEventListener("click", () => {
4237
4728
  const content = sourceTextEl.value;
4238
4729
  if (!content.trim()) {
@@ -4397,6 +4888,10 @@ export default function (pi: ExtensionAPI) {
4397
4888
  let lastCommandCtx: ExtensionCommandContext | null = null;
4398
4889
  let lastThemeVarsJson = "";
4399
4890
  let agentBusy = false;
4891
+ let terminalActivityPhase: TerminalActivityPhase = "idle";
4892
+ let terminalActivityToolName: string | null = null;
4893
+ let terminalActivityLabel: string | null = null;
4894
+ let lastSpecificToolActivityLabel: string | null = null;
4400
4895
 
4401
4896
  const isStudioBusy = () => agentBusy || activeRequest !== null;
4402
4897
 
@@ -4427,18 +4922,94 @@ export default function (pi: ExtensionAPI) {
4427
4922
  }
4428
4923
  };
4429
4924
 
4925
+ const emitDebugEvent = (event: string, details?: Record<string, unknown>) => {
4926
+ broadcast({
4927
+ type: "debug_event",
4928
+ event,
4929
+ timestamp: Date.now(),
4930
+ details: details ?? null,
4931
+ });
4932
+ };
4933
+
4934
+ const setTerminalActivity = (phase: TerminalActivityPhase, toolName?: string | null, label?: string | null) => {
4935
+ const nextPhase: TerminalActivityPhase =
4936
+ phase === "running" || phase === "tool" || phase === "responding"
4937
+ ? phase
4938
+ : "idle";
4939
+ const nextToolName = nextPhase === "tool" ? (toolName?.trim() || null) : null;
4940
+ const baseLabel = nextPhase === "tool" ? normalizeActivityLabel(label || "") : null;
4941
+ let nextLabel: string | null = null;
4942
+
4943
+ if (nextPhase === "tool") {
4944
+ if (baseLabel && !isGenericToolActivityLabel(baseLabel)) {
4945
+ if (
4946
+ lastSpecificToolActivityLabel
4947
+ && lastSpecificToolActivityLabel !== baseLabel
4948
+ && !isGenericToolActivityLabel(lastSpecificToolActivityLabel)
4949
+ ) {
4950
+ nextLabel = normalizeActivityLabel(`${lastSpecificToolActivityLabel} → ${baseLabel}`);
4951
+ } else {
4952
+ nextLabel = baseLabel;
4953
+ }
4954
+ lastSpecificToolActivityLabel = baseLabel;
4955
+ } else {
4956
+ nextLabel = baseLabel;
4957
+ }
4958
+ } else {
4959
+ nextLabel = null;
4960
+ if (nextPhase === "idle") {
4961
+ lastSpecificToolActivityLabel = null;
4962
+ }
4963
+ }
4964
+
4965
+ if (
4966
+ terminalActivityPhase === nextPhase
4967
+ && terminalActivityToolName === nextToolName
4968
+ && terminalActivityLabel === nextLabel
4969
+ ) {
4970
+ return;
4971
+ }
4972
+ terminalActivityPhase = nextPhase;
4973
+ terminalActivityToolName = nextToolName;
4974
+ terminalActivityLabel = nextLabel;
4975
+ emitDebugEvent("terminal_activity", {
4976
+ phase: terminalActivityPhase,
4977
+ toolName: terminalActivityToolName,
4978
+ label: terminalActivityLabel,
4979
+ baseLabel,
4980
+ lastSpecificToolActivityLabel,
4981
+ activeRequestId: activeRequest?.id ?? null,
4982
+ activeRequestKind: activeRequest?.kind ?? null,
4983
+ agentBusy,
4984
+ });
4985
+ broadcastState();
4986
+ };
4987
+
4430
4988
  const broadcastState = () => {
4431
4989
  broadcast({
4432
4990
  type: "studio_state",
4433
4991
  busy: isStudioBusy(),
4992
+ agentBusy,
4993
+ terminalPhase: terminalActivityPhase,
4994
+ terminalToolName: terminalActivityToolName,
4995
+ terminalActivityLabel,
4434
4996
  activeRequestId: activeRequest?.id ?? null,
4997
+ activeRequestKind: activeRequest?.kind ?? null,
4435
4998
  });
4436
4999
  };
4437
5000
 
4438
5001
  const clearActiveRequest = (options?: { notify?: string; level?: "info" | "warning" | "error" }) => {
4439
5002
  if (!activeRequest) return;
5003
+ const completedRequestId = activeRequest.id;
5004
+ const completedKind = activeRequest.kind;
4440
5005
  clearTimeout(activeRequest.timer);
4441
5006
  activeRequest = null;
5007
+ emitDebugEvent("clear_active_request", {
5008
+ requestId: completedRequestId,
5009
+ kind: completedKind,
5010
+ notify: options?.notify ?? null,
5011
+ agentBusy,
5012
+ });
4442
5013
  broadcastState();
4443
5014
  if (options?.notify) {
4444
5015
  broadcast({ type: "info", message: options.notify, level: options.level ?? "info" });
@@ -4446,6 +5017,12 @@ export default function (pi: ExtensionAPI) {
4446
5017
  };
4447
5018
 
4448
5019
  const beginRequest = (requestId: string, kind: StudioRequestKind): boolean => {
5020
+ emitDebugEvent("begin_request_attempt", {
5021
+ requestId,
5022
+ kind,
5023
+ hasActiveRequest: Boolean(activeRequest),
5024
+ agentBusy,
5025
+ });
4449
5026
  if (activeRequest) {
4450
5027
  broadcast({ type: "busy", requestId, message: "A studio request is already in progress." });
4451
5028
  return false;
@@ -4457,6 +5034,7 @@ export default function (pi: ExtensionAPI) {
4457
5034
 
4458
5035
  const timer = setTimeout(() => {
4459
5036
  if (!activeRequest || activeRequest.id !== requestId) return;
5037
+ emitDebugEvent("request_timeout", { requestId, kind });
4460
5038
  broadcast({ type: "error", requestId, message: "Studio request timed out. Please try again." });
4461
5039
  clearActiveRequest();
4462
5040
  }, REQUEST_TIMEOUT_MS);
@@ -4468,6 +5046,7 @@ export default function (pi: ExtensionAPI) {
4468
5046
  timer,
4469
5047
  };
4470
5048
 
5049
+ emitDebugEvent("begin_request", { requestId, kind });
4471
5050
  broadcast({ type: "request_started", requestId, kind });
4472
5051
  broadcastState();
4473
5052
  return true;
@@ -4491,11 +5070,24 @@ export default function (pi: ExtensionAPI) {
4491
5070
  return;
4492
5071
  }
4493
5072
 
5073
+ emitDebugEvent("studio_message", {
5074
+ type: msg.type,
5075
+ requestId: "requestId" in msg ? msg.requestId : null,
5076
+ activeRequestId: activeRequest?.id ?? null,
5077
+ activeRequestKind: activeRequest?.kind ?? null,
5078
+ agentBusy,
5079
+ });
5080
+
4494
5081
  if (msg.type === "hello") {
4495
5082
  sendToClient(client, {
4496
5083
  type: "hello_ack",
4497
5084
  busy: isStudioBusy(),
5085
+ agentBusy,
5086
+ terminalPhase: terminalActivityPhase,
5087
+ terminalToolName: terminalActivityToolName,
5088
+ terminalActivityLabel,
4498
5089
  activeRequestId: activeRequest?.id ?? null,
5090
+ activeRequestKind: activeRequest?.kind ?? null,
4499
5091
  lastResponse: lastStudioResponse,
4500
5092
  initialDocument: initialStudioDocument,
4501
5093
  });
@@ -4727,6 +5319,41 @@ export default function (pi: ExtensionAPI) {
4727
5319
  }
4728
5320
  return;
4729
5321
  }
5322
+
5323
+ if (msg.type === "get_from_editor_request") {
5324
+ if (!isValidRequestId(msg.requestId)) {
5325
+ sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
5326
+ return;
5327
+ }
5328
+ if (isStudioBusy()) {
5329
+ sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
5330
+ return;
5331
+ }
5332
+ if (!lastCommandCtx || !lastCommandCtx.hasUI) {
5333
+ sendToClient(client, {
5334
+ type: "error",
5335
+ requestId: msg.requestId,
5336
+ message: "No interactive pi editor context is available.",
5337
+ });
5338
+ return;
5339
+ }
5340
+
5341
+ try {
5342
+ const content = lastCommandCtx.ui.getEditorText();
5343
+ sendToClient(client, {
5344
+ type: "editor_snapshot",
5345
+ requestId: msg.requestId,
5346
+ content,
5347
+ });
5348
+ } catch (error) {
5349
+ sendToClient(client, {
5350
+ type: "error",
5351
+ requestId: msg.requestId,
5352
+ message: `Failed to read pi editor text: ${error instanceof Error ? error.message : String(error)}`,
5353
+ });
5354
+ }
5355
+ return;
5356
+ }
4730
5357
  };
4731
5358
 
4732
5359
  const handleRenderPreviewRequest = async (req: IncomingMessage, res: ServerResponse) => {
@@ -5004,21 +5631,81 @@ export default function (pi: ExtensionAPI) {
5004
5631
 
5005
5632
  pi.on("session_start", async (_event, ctx) => {
5006
5633
  hydrateLatestAssistant(ctx.sessionManager.getBranch());
5634
+ agentBusy = false;
5635
+ emitDebugEvent("session_start", { entryCount: ctx.sessionManager.getBranch().length });
5636
+ setTerminalActivity("idle");
5007
5637
  });
5008
5638
 
5009
5639
  pi.on("session_switch", async (_event, ctx) => {
5010
5640
  clearActiveRequest({ notify: "Session switched. Studio request state cleared.", level: "warning" });
5011
5641
  lastCommandCtx = null;
5012
5642
  hydrateLatestAssistant(ctx.sessionManager.getBranch());
5643
+ agentBusy = false;
5644
+ emitDebugEvent("session_switch", { entryCount: ctx.sessionManager.getBranch().length });
5645
+ setTerminalActivity("idle");
5013
5646
  });
5014
5647
 
5015
5648
  pi.on("agent_start", async () => {
5016
5649
  agentBusy = true;
5017
- broadcastState();
5650
+ emitDebugEvent("agent_start", { activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
5651
+ setTerminalActivity("running");
5652
+ });
5653
+
5654
+ pi.on("tool_call", async (event) => {
5655
+ if (!agentBusy) return;
5656
+ const toolName = typeof event.toolName === "string" ? event.toolName : "";
5657
+ const input = (event as { input?: unknown }).input;
5658
+ const label = deriveToolActivityLabel(toolName, input);
5659
+ emitDebugEvent("tool_call", { toolName, label, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
5660
+ setTerminalActivity("tool", toolName, label);
5661
+ });
5662
+
5663
+ pi.on("tool_execution_start", async (event) => {
5664
+ if (!agentBusy) return;
5665
+ const label = deriveToolActivityLabel(event.toolName, event.args);
5666
+ emitDebugEvent("tool_execution_start", { toolName: event.toolName, label, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
5667
+ setTerminalActivity("tool", event.toolName, label);
5668
+ });
5669
+
5670
+ pi.on("tool_execution_end", async (event) => {
5671
+ if (!agentBusy) return;
5672
+ emitDebugEvent("tool_execution_end", { toolName: event.toolName, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
5673
+ // Keep tool phase visible until the next tool call, assistant response phase,
5674
+ // or agent_end. This avoids tool labels flashing too quickly to read.
5675
+ });
5676
+
5677
+ pi.on("message_start", async (event) => {
5678
+ const role = (event.message as { role?: string } | undefined)?.role;
5679
+ emitDebugEvent("message_start", { role: role ?? "", activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
5680
+ if (agentBusy && role === "assistant") {
5681
+ setTerminalActivity("responding");
5682
+ }
5018
5683
  });
5019
5684
 
5020
5685
  pi.on("message_end", async (event) => {
5686
+ const message = event.message as { stopReason?: string; role?: string };
5687
+ const stopReason = typeof message.stopReason === "string" ? message.stopReason : "";
5688
+ const role = typeof message.role === "string" ? message.role : "";
5021
5689
  const markdown = extractAssistantText(event.message);
5690
+ emitDebugEvent("message_end", {
5691
+ role,
5692
+ stopReason,
5693
+ hasMarkdown: Boolean(markdown),
5694
+ markdownLength: markdown ? markdown.length : 0,
5695
+ activeRequestId: activeRequest?.id ?? null,
5696
+ activeRequestKind: activeRequest?.kind ?? null,
5697
+ });
5698
+
5699
+ // Assistant is handing off to tool calls; request is still in progress.
5700
+ if (stopReason === "toolUse") {
5701
+ emitDebugEvent("message_end_tool_use", {
5702
+ role,
5703
+ activeRequestId: activeRequest?.id ?? null,
5704
+ activeRequestKind: activeRequest?.kind ?? null,
5705
+ });
5706
+ return;
5707
+ }
5708
+
5022
5709
  if (!markdown) return;
5023
5710
 
5024
5711
  if (activeRequest) {
@@ -5029,6 +5716,12 @@ export default function (pi: ExtensionAPI) {
5029
5716
  timestamp: Date.now(),
5030
5717
  kind,
5031
5718
  };
5719
+ emitDebugEvent("broadcast_response", {
5720
+ requestId,
5721
+ kind,
5722
+ markdownLength: markdown.length,
5723
+ stopReason,
5724
+ });
5032
5725
  broadcast({
5033
5726
  type: "response",
5034
5727
  requestId,
@@ -5046,6 +5739,11 @@ export default function (pi: ExtensionAPI) {
5046
5739
  timestamp: Date.now(),
5047
5740
  kind: inferredKind,
5048
5741
  };
5742
+ emitDebugEvent("broadcast_latest_response", {
5743
+ kind: inferredKind,
5744
+ markdownLength: markdown.length,
5745
+ stopReason,
5746
+ });
5049
5747
  broadcast({
5050
5748
  type: "latest_response",
5051
5749
  kind: inferredKind,
@@ -5056,7 +5754,8 @@ export default function (pi: ExtensionAPI) {
5056
5754
 
5057
5755
  pi.on("agent_end", async () => {
5058
5756
  agentBusy = false;
5059
- broadcastState();
5757
+ emitDebugEvent("agent_end", { activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
5758
+ setTerminalActivity("idle");
5060
5759
  if (activeRequest) {
5061
5760
  const requestId = activeRequest.id;
5062
5761
  broadcast({
@@ -5070,6 +5769,8 @@ export default function (pi: ExtensionAPI) {
5070
5769
 
5071
5770
  pi.on("session_shutdown", async () => {
5072
5771
  lastCommandCtx = null;
5772
+ agentBusy = false;
5773
+ setTerminalActivity("idle");
5073
5774
  await stopServer();
5074
5775
  });
5075
5776
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",