pi-studio 0.4.0 → 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 +24 -0
- package/README.md +4 -2
- package/index.ts +794 -50
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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
|
+
|
|
22
|
+
## [0.4.1] — 2026-03-03
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Editor input keeps preview refreshes immediate (no added typing debounce) while keeping editor syntax highlighting immediate in Raw view.
|
|
26
|
+
- Response/sync state checks now reuse cached normalized response data and critique-note extracts instead of recomputing on each keystroke.
|
|
27
|
+
- Editor action/sync UI updates are now coalesced with `requestAnimationFrame` during typing.
|
|
28
|
+
|
|
5
29
|
## [0.3.0] — 2026-03-02
|
|
6
30
|
|
|
7
31
|
### Added
|
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
|
|
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";
|
|
@@ -2261,6 +2422,15 @@ ${cssVarsBlock}
|
|
|
2261
2422
|
let latestResponseTimestamp = 0;
|
|
2262
2423
|
let latestResponseKind = "annotation";
|
|
2263
2424
|
let latestResponseIsStructuredCritique = false;
|
|
2425
|
+
let latestResponseHasContent = false;
|
|
2426
|
+
let latestResponseNormalized = "";
|
|
2427
|
+
let latestCritiqueNotes = "";
|
|
2428
|
+
let latestCritiqueNotesNormalized = "";
|
|
2429
|
+
let agentBusyFromServer = false;
|
|
2430
|
+
let terminalActivityPhase = "idle";
|
|
2431
|
+
let terminalActivityToolName = "";
|
|
2432
|
+
let terminalActivityLabel = "";
|
|
2433
|
+
let lastSpecificToolLabel = "";
|
|
2264
2434
|
let uiBusy = false;
|
|
2265
2435
|
let sourceState = {
|
|
2266
2436
|
source: initialSourceState.source,
|
|
@@ -2312,10 +2482,14 @@ ${cssVarsBlock}
|
|
|
2312
2482
|
var SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
|
|
2313
2483
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
2314
2484
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
2485
|
+
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
2486
|
+
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
2487
|
+
const previewPendingTimers = new WeakMap();
|
|
2315
2488
|
let sourcePreviewRenderTimer = null;
|
|
2316
2489
|
let sourcePreviewRenderNonce = 0;
|
|
2317
2490
|
let responsePreviewRenderNonce = 0;
|
|
2318
2491
|
let responseEditorPreviewTimer = null;
|
|
2492
|
+
let editorMetaUpdateRaf = null;
|
|
2319
2493
|
let editorHighlightEnabled = false;
|
|
2320
2494
|
let editorLanguage = "markdown";
|
|
2321
2495
|
let responseHighlightEnabled = false;
|
|
@@ -2327,8 +2501,153 @@ ${cssVarsBlock}
|
|
|
2327
2501
|
let mermaidModulePromise = null;
|
|
2328
2502
|
let mermaidInitialized = false;
|
|
2329
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
|
+
|
|
2330
2557
|
function getIdleStatus() {
|
|
2331
|
-
return "Ready. Edit text, then run
|
|
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 + "…";
|
|
2332
2651
|
}
|
|
2333
2652
|
|
|
2334
2653
|
function renderStatus() {
|
|
@@ -2346,6 +2665,19 @@ ${cssVarsBlock}
|
|
|
2346
2665
|
statusMessage = message;
|
|
2347
2666
|
statusLevel = level || "";
|
|
2348
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
|
+
});
|
|
2349
2681
|
}
|
|
2350
2682
|
|
|
2351
2683
|
renderStatus();
|
|
@@ -2514,23 +2846,19 @@ ${cssVarsBlock}
|
|
|
2514
2846
|
return normalizeForCompare(a) === normalizeForCompare(b);
|
|
2515
2847
|
}
|
|
2516
2848
|
|
|
2517
|
-
function
|
|
2518
|
-
return latestResponseMarkdown;
|
|
2519
|
-
}
|
|
2520
|
-
|
|
2521
|
-
function updateSyncBadge() {
|
|
2849
|
+
function updateSyncBadge(normalizedEditorText) {
|
|
2522
2850
|
if (!syncBadgeEl) return;
|
|
2523
2851
|
|
|
2524
|
-
|
|
2525
|
-
const hasResponse = Boolean(response && response.trim());
|
|
2526
|
-
|
|
2527
|
-
if (!hasResponse) {
|
|
2852
|
+
if (!latestResponseHasContent) {
|
|
2528
2853
|
syncBadgeEl.textContent = "No response loaded";
|
|
2529
2854
|
syncBadgeEl.classList.remove("sync", "edited");
|
|
2530
2855
|
return;
|
|
2531
2856
|
}
|
|
2532
2857
|
|
|
2533
|
-
const
|
|
2858
|
+
const normalizedEditor = typeof normalizedEditorText === "string"
|
|
2859
|
+
? normalizedEditorText
|
|
2860
|
+
: normalizeForCompare(sourceTextEl.value);
|
|
2861
|
+
const inSync = normalizedEditor === latestResponseNormalized;
|
|
2534
2862
|
if (inSync) {
|
|
2535
2863
|
syncBadgeEl.textContent = "In sync with response";
|
|
2536
2864
|
syncBadgeEl.classList.add("sync");
|
|
@@ -2592,6 +2920,48 @@ ${cssVarsBlock}
|
|
|
2592
2920
|
targetEl.appendChild(el);
|
|
2593
2921
|
}
|
|
2594
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
|
+
|
|
2595
2965
|
async function getMermaidApi() {
|
|
2596
2966
|
if (mermaidModulePromise) {
|
|
2597
2967
|
return mermaidModulePromise;
|
|
@@ -2737,6 +3107,7 @@ ${cssVarsBlock}
|
|
|
2737
3107
|
if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
|
|
2738
3108
|
}
|
|
2739
3109
|
|
|
3110
|
+
finishPreviewRender(targetEl);
|
|
2740
3111
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
2741
3112
|
await renderMermaidInElement(targetEl);
|
|
2742
3113
|
|
|
@@ -2756,6 +3127,7 @@ ${cssVarsBlock}
|
|
|
2756
3127
|
}
|
|
2757
3128
|
|
|
2758
3129
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
3130
|
+
finishPreviewRender(targetEl);
|
|
2759
3131
|
targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
|
|
2760
3132
|
}
|
|
2761
3133
|
}
|
|
@@ -2764,11 +3136,12 @@ ${cssVarsBlock}
|
|
|
2764
3136
|
if (editorView !== "preview") return;
|
|
2765
3137
|
const text = sourceTextEl.value || "";
|
|
2766
3138
|
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
3139
|
+
finishPreviewRender(sourcePreviewEl);
|
|
2767
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>";
|
|
2768
3141
|
return;
|
|
2769
3142
|
}
|
|
2770
3143
|
const nonce = ++sourcePreviewRenderNonce;
|
|
2771
|
-
sourcePreviewEl
|
|
3144
|
+
beginPreviewRender(sourcePreviewEl);
|
|
2772
3145
|
void applyRenderedMarkdown(sourcePreviewEl, text, "source", nonce);
|
|
2773
3146
|
}
|
|
2774
3147
|
|
|
@@ -2787,15 +3160,20 @@ ${cssVarsBlock}
|
|
|
2787
3160
|
}, delay);
|
|
2788
3161
|
}
|
|
2789
3162
|
|
|
2790
|
-
function renderSourcePreview() {
|
|
3163
|
+
function renderSourcePreview(options) {
|
|
3164
|
+
const previewDelayMs =
|
|
3165
|
+
options && typeof options.previewDelayMs === "number"
|
|
3166
|
+
? Math.max(0, options.previewDelayMs)
|
|
3167
|
+
: 0;
|
|
3168
|
+
|
|
2791
3169
|
if (editorView === "preview") {
|
|
2792
|
-
scheduleSourcePreviewRender(
|
|
3170
|
+
scheduleSourcePreviewRender(previewDelayMs);
|
|
2793
3171
|
}
|
|
2794
3172
|
if (editorHighlightEnabled && editorView === "markdown") {
|
|
2795
3173
|
scheduleEditorHighlightRender();
|
|
2796
3174
|
}
|
|
2797
3175
|
if (rightView === "editor-preview") {
|
|
2798
|
-
scheduleResponseEditorPreviewRender(
|
|
3176
|
+
scheduleResponseEditorPreviewRender(previewDelayMs);
|
|
2799
3177
|
}
|
|
2800
3178
|
}
|
|
2801
3179
|
|
|
@@ -2818,34 +3196,38 @@ ${cssVarsBlock}
|
|
|
2818
3196
|
if (rightView === "editor-preview") {
|
|
2819
3197
|
const editorText = sourceTextEl.value || "";
|
|
2820
3198
|
if (!editorText.trim()) {
|
|
3199
|
+
finishPreviewRender(critiqueViewEl);
|
|
2821
3200
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
|
|
2822
3201
|
return;
|
|
2823
3202
|
}
|
|
2824
3203
|
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
3204
|
+
finishPreviewRender(critiqueViewEl);
|
|
2825
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>";
|
|
2826
3206
|
return;
|
|
2827
3207
|
}
|
|
2828
3208
|
const nonce = ++responsePreviewRenderNonce;
|
|
2829
|
-
critiqueViewEl
|
|
3209
|
+
beginPreviewRender(critiqueViewEl);
|
|
2830
3210
|
void applyRenderedMarkdown(critiqueViewEl, editorText, "response", nonce);
|
|
2831
3211
|
return;
|
|
2832
3212
|
}
|
|
2833
3213
|
|
|
2834
3214
|
const markdown = latestResponseMarkdown;
|
|
2835
3215
|
if (!markdown || !markdown.trim()) {
|
|
3216
|
+
finishPreviewRender(critiqueViewEl);
|
|
2836
3217
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
|
|
2837
3218
|
return;
|
|
2838
3219
|
}
|
|
2839
3220
|
|
|
2840
3221
|
if (rightView === "preview") {
|
|
2841
3222
|
const nonce = ++responsePreviewRenderNonce;
|
|
2842
|
-
critiqueViewEl
|
|
3223
|
+
beginPreviewRender(critiqueViewEl);
|
|
2843
3224
|
void applyRenderedMarkdown(critiqueViewEl, markdown, "response", nonce);
|
|
2844
3225
|
return;
|
|
2845
3226
|
}
|
|
2846
3227
|
|
|
2847
3228
|
if (responseHighlightEnabled) {
|
|
2848
3229
|
if (markdown.length > RESPONSE_HIGHLIGHT_MAX_CHARS) {
|
|
3230
|
+
finishPreviewRender(critiqueViewEl);
|
|
2849
3231
|
critiqueViewEl.innerHTML = buildPreviewErrorHtml(
|
|
2850
3232
|
"Response is too large for markdown highlighting. Showing plain markdown.",
|
|
2851
3233
|
markdown,
|
|
@@ -2853,21 +3235,25 @@ ${cssVarsBlock}
|
|
|
2853
3235
|
return;
|
|
2854
3236
|
}
|
|
2855
3237
|
|
|
3238
|
+
finishPreviewRender(critiqueViewEl);
|
|
2856
3239
|
critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
|
|
2857
3240
|
return;
|
|
2858
3241
|
}
|
|
2859
3242
|
|
|
3243
|
+
finishPreviewRender(critiqueViewEl);
|
|
2860
3244
|
critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
|
|
2861
3245
|
}
|
|
2862
3246
|
|
|
2863
|
-
function updateResultActionButtons() {
|
|
2864
|
-
const
|
|
2865
|
-
const
|
|
2866
|
-
|
|
3247
|
+
function updateResultActionButtons(normalizedEditorText) {
|
|
3248
|
+
const hasResponse = latestResponseHasContent;
|
|
3249
|
+
const normalizedEditor = typeof normalizedEditorText === "string"
|
|
3250
|
+
? normalizedEditorText
|
|
3251
|
+
: normalizeForCompare(sourceTextEl.value);
|
|
3252
|
+
const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
|
|
2867
3253
|
const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
|
|
2868
3254
|
|
|
2869
|
-
const critiqueNotes = isCritiqueResponse ?
|
|
2870
|
-
const critiqueNotesLoaded = Boolean(critiqueNotes) &&
|
|
3255
|
+
const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
|
|
3256
|
+
const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
|
|
2871
3257
|
|
|
2872
3258
|
loadResponseBtn.hidden = isCritiqueResponse;
|
|
2873
3259
|
loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
|
|
@@ -2887,7 +3273,7 @@ ${cssVarsBlock}
|
|
|
2887
3273
|
pullLatestBtn.disabled = uiBusy || followLatest;
|
|
2888
3274
|
pullLatestBtn.textContent = queuedLatestResponse ? "Get latest response *" : "Get latest response";
|
|
2889
3275
|
|
|
2890
|
-
updateSyncBadge();
|
|
3276
|
+
updateSyncBadge(normalizedEditor);
|
|
2891
3277
|
}
|
|
2892
3278
|
|
|
2893
3279
|
function refreshResponseUi() {
|
|
@@ -2927,6 +3313,7 @@ ${cssVarsBlock}
|
|
|
2927
3313
|
saveAsBtn.disabled = uiBusy;
|
|
2928
3314
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
2929
3315
|
sendEditorBtn.disabled = uiBusy;
|
|
3316
|
+
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
2930
3317
|
sendRunBtn.disabled = uiBusy;
|
|
2931
3318
|
copyDraftBtn.disabled = uiBusy;
|
|
2932
3319
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
@@ -2972,6 +3359,10 @@ ${cssVarsBlock}
|
|
|
2972
3359
|
sourcePreviewRenderTimer = null;
|
|
2973
3360
|
}
|
|
2974
3361
|
|
|
3362
|
+
if (!showPreview) {
|
|
3363
|
+
finishPreviewRender(sourcePreviewEl);
|
|
3364
|
+
}
|
|
3365
|
+
|
|
2975
3366
|
if (showPreview) {
|
|
2976
3367
|
renderSourcePreview();
|
|
2977
3368
|
}
|
|
@@ -3411,6 +3802,31 @@ ${cssVarsBlock}
|
|
|
3411
3802
|
sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
|
|
3412
3803
|
}
|
|
3413
3804
|
|
|
3805
|
+
function runEditorMetaUpdateNow() {
|
|
3806
|
+
const normalizedEditor = normalizeForCompare(sourceTextEl.value);
|
|
3807
|
+
updateResultActionButtons(normalizedEditor);
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
function scheduleEditorMetaUpdate() {
|
|
3811
|
+
if (editorMetaUpdateRaf !== null) {
|
|
3812
|
+
if (typeof window.cancelAnimationFrame === "function") {
|
|
3813
|
+
window.cancelAnimationFrame(editorMetaUpdateRaf);
|
|
3814
|
+
} else {
|
|
3815
|
+
window.clearTimeout(editorMetaUpdateRaf);
|
|
3816
|
+
}
|
|
3817
|
+
editorMetaUpdateRaf = null;
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3821
|
+
? window.requestAnimationFrame.bind(window)
|
|
3822
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
3823
|
+
|
|
3824
|
+
editorMetaUpdateRaf = schedule(() => {
|
|
3825
|
+
editorMetaUpdateRaf = null;
|
|
3826
|
+
runEditorMetaUpdateNow();
|
|
3827
|
+
});
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3414
3830
|
function readStoredToggle(storageKey) {
|
|
3415
3831
|
if (!window.localStorage) return null;
|
|
3416
3832
|
try {
|
|
@@ -3597,9 +4013,18 @@ ${cssVarsBlock}
|
|
|
3597
4013
|
latestResponseKind = kind === "critique" ? "critique" : "annotation";
|
|
3598
4014
|
latestResponseTimestamp = responseTimestamp;
|
|
3599
4015
|
latestResponseIsStructuredCritique = isStructuredCritique(markdown);
|
|
4016
|
+
latestResponseHasContent = Boolean(markdown && markdown.trim());
|
|
4017
|
+
latestResponseNormalized = normalizeForCompare(markdown);
|
|
4018
|
+
|
|
4019
|
+
if (latestResponseIsStructuredCritique) {
|
|
4020
|
+
latestCritiqueNotes = buildCritiqueNotesMarkdown(markdown);
|
|
4021
|
+
latestCritiqueNotesNormalized = normalizeForCompare(latestCritiqueNotes);
|
|
4022
|
+
} else {
|
|
4023
|
+
latestCritiqueNotes = "";
|
|
4024
|
+
latestCritiqueNotesNormalized = "";
|
|
4025
|
+
}
|
|
3600
4026
|
|
|
3601
4027
|
refreshResponseUi();
|
|
3602
|
-
syncActionButtons();
|
|
3603
4028
|
}
|
|
3604
4029
|
|
|
3605
4030
|
function applyLatestPayload(payload) {
|
|
@@ -3622,14 +4047,30 @@ ${cssVarsBlock}
|
|
|
3622
4047
|
function handleServerMessage(message) {
|
|
3623
4048
|
if (!message || typeof message !== "object") return;
|
|
3624
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
|
+
|
|
3625
4057
|
if (message.type === "hello_ack") {
|
|
3626
4058
|
const busy = Boolean(message.busy);
|
|
4059
|
+
agentBusyFromServer = Boolean(message.agentBusy);
|
|
4060
|
+
updateTerminalActivityState(message.terminalPhase, message.terminalToolName, message.terminalActivityLabel);
|
|
3627
4061
|
setBusy(busy);
|
|
3628
4062
|
setWsState(busy ? "Submitting" : "Ready");
|
|
3629
|
-
if (message.activeRequestId) {
|
|
3630
|
-
pendingRequestId =
|
|
3631
|
-
|
|
3632
|
-
|
|
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;
|
|
3633
4074
|
}
|
|
3634
4075
|
|
|
3635
4076
|
let loadedInitialDocument = false;
|
|
@@ -3662,7 +4103,26 @@ ${cssVarsBlock}
|
|
|
3662
4103
|
handleIncomingResponse(lastMarkdown, lastResponseKind, message.lastResponse.timestamp);
|
|
3663
4104
|
}
|
|
3664
4105
|
|
|
3665
|
-
if (
|
|
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) {
|
|
3666
4126
|
refreshResponseUi();
|
|
3667
4127
|
setStatus(getIdleStatus());
|
|
3668
4128
|
}
|
|
@@ -3672,17 +4132,10 @@ ${cssVarsBlock}
|
|
|
3672
4132
|
if (message.type === "request_started") {
|
|
3673
4133
|
pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
3674
4134
|
pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
|
|
4135
|
+
stickyStudioKind = pendingKind;
|
|
3675
4136
|
setBusy(true);
|
|
3676
4137
|
setWsState("Submitting");
|
|
3677
|
-
|
|
3678
|
-
setStatus("Sending annotated reply…", "warning");
|
|
3679
|
-
} else if (pendingKind === "critique") {
|
|
3680
|
-
setStatus("Running critique…", "warning");
|
|
3681
|
-
} else if (pendingKind === "direct") {
|
|
3682
|
-
setStatus("Running editor text…", "warning");
|
|
3683
|
-
} else {
|
|
3684
|
-
setStatus("Submitting…", "warning");
|
|
3685
|
-
}
|
|
4138
|
+
setStatus(getStudioBusyStatus(pendingKind), "warning");
|
|
3686
4139
|
return;
|
|
3687
4140
|
}
|
|
3688
4141
|
|
|
@@ -3696,6 +4149,7 @@ ${cssVarsBlock}
|
|
|
3696
4149
|
? message.kind
|
|
3697
4150
|
: (pendingKind === "critique" ? "critique" : "annotation");
|
|
3698
4151
|
|
|
4152
|
+
stickyStudioKind = responseKind;
|
|
3699
4153
|
pendingRequestId = null;
|
|
3700
4154
|
pendingKind = null;
|
|
3701
4155
|
setBusy(false);
|
|
@@ -3767,13 +4221,71 @@ ${cssVarsBlock}
|
|
|
3767
4221
|
return;
|
|
3768
4222
|
}
|
|
3769
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
|
+
|
|
3770
4248
|
if (message.type === "studio_state") {
|
|
3771
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
|
+
|
|
3772
4266
|
setBusy(busy);
|
|
3773
4267
|
setWsState(busy ? "Submitting" : "Ready");
|
|
3774
|
-
|
|
3775
|
-
|
|
4268
|
+
|
|
4269
|
+
if (pendingRequestId) {
|
|
4270
|
+
if (busy) {
|
|
4271
|
+
setStatus(getStudioBusyStatus(pendingKind), "warning");
|
|
4272
|
+
}
|
|
4273
|
+
return;
|
|
3776
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());
|
|
3777
4289
|
return;
|
|
3778
4290
|
}
|
|
3779
4291
|
|
|
@@ -3782,6 +4294,7 @@ ${cssVarsBlock}
|
|
|
3782
4294
|
pendingRequestId = null;
|
|
3783
4295
|
pendingKind = null;
|
|
3784
4296
|
}
|
|
4297
|
+
stickyStudioKind = null;
|
|
3785
4298
|
setBusy(false);
|
|
3786
4299
|
setWsState("Ready");
|
|
3787
4300
|
setStatus(typeof message.message === "string" ? message.message : "Studio is busy.", "warning");
|
|
@@ -3793,6 +4306,7 @@ ${cssVarsBlock}
|
|
|
3793
4306
|
pendingRequestId = null;
|
|
3794
4307
|
pendingKind = null;
|
|
3795
4308
|
}
|
|
4309
|
+
stickyStudioKind = null;
|
|
3796
4310
|
setBusy(false);
|
|
3797
4311
|
setWsState("Ready");
|
|
3798
4312
|
setStatus(typeof message.message === "string" ? message.message : "Request failed.", "error");
|
|
@@ -3827,7 +4341,7 @@ ${cssVarsBlock}
|
|
|
3827
4341
|
}
|
|
3828
4342
|
|
|
3829
4343
|
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
3830
|
-
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" : "");
|
|
3831
4345
|
|
|
3832
4346
|
setWsState("Connecting");
|
|
3833
4347
|
setStatus("Connecting to Studio server…");
|
|
@@ -3884,8 +4398,10 @@ ${cssVarsBlock}
|
|
|
3884
4398
|
const requestId = makeRequestId();
|
|
3885
4399
|
pendingRequestId = requestId;
|
|
3886
4400
|
pendingKind = kind;
|
|
4401
|
+
stickyStudioKind = kind;
|
|
3887
4402
|
setBusy(true);
|
|
3888
4403
|
setWsState("Submitting");
|
|
4404
|
+
setStatus(getStudioBusyStatus(kind), "warning");
|
|
3889
4405
|
return requestId;
|
|
3890
4406
|
}
|
|
3891
4407
|
|
|
@@ -4016,8 +4532,8 @@ ${cssVarsBlock}
|
|
|
4016
4532
|
});
|
|
4017
4533
|
|
|
4018
4534
|
sourceTextEl.addEventListener("input", () => {
|
|
4019
|
-
renderSourcePreview();
|
|
4020
|
-
|
|
4535
|
+
renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
|
|
4536
|
+
scheduleEditorMetaUpdate();
|
|
4021
4537
|
});
|
|
4022
4538
|
|
|
4023
4539
|
sourceTextEl.addEventListener("scroll", () => {
|
|
@@ -4190,6 +4706,24 @@ ${cssVarsBlock}
|
|
|
4190
4706
|
}
|
|
4191
4707
|
});
|
|
4192
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
|
+
|
|
4193
4727
|
sendRunBtn.addEventListener("click", () => {
|
|
4194
4728
|
const content = sourceTextEl.value;
|
|
4195
4729
|
if (!content.trim()) {
|
|
@@ -4354,6 +4888,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
4354
4888
|
let lastCommandCtx: ExtensionCommandContext | null = null;
|
|
4355
4889
|
let lastThemeVarsJson = "";
|
|
4356
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;
|
|
4357
4895
|
|
|
4358
4896
|
const isStudioBusy = () => agentBusy || activeRequest !== null;
|
|
4359
4897
|
|
|
@@ -4384,18 +4922,94 @@ export default function (pi: ExtensionAPI) {
|
|
|
4384
4922
|
}
|
|
4385
4923
|
};
|
|
4386
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
|
+
|
|
4387
4988
|
const broadcastState = () => {
|
|
4388
4989
|
broadcast({
|
|
4389
4990
|
type: "studio_state",
|
|
4390
4991
|
busy: isStudioBusy(),
|
|
4992
|
+
agentBusy,
|
|
4993
|
+
terminalPhase: terminalActivityPhase,
|
|
4994
|
+
terminalToolName: terminalActivityToolName,
|
|
4995
|
+
terminalActivityLabel,
|
|
4391
4996
|
activeRequestId: activeRequest?.id ?? null,
|
|
4997
|
+
activeRequestKind: activeRequest?.kind ?? null,
|
|
4392
4998
|
});
|
|
4393
4999
|
};
|
|
4394
5000
|
|
|
4395
5001
|
const clearActiveRequest = (options?: { notify?: string; level?: "info" | "warning" | "error" }) => {
|
|
4396
5002
|
if (!activeRequest) return;
|
|
5003
|
+
const completedRequestId = activeRequest.id;
|
|
5004
|
+
const completedKind = activeRequest.kind;
|
|
4397
5005
|
clearTimeout(activeRequest.timer);
|
|
4398
5006
|
activeRequest = null;
|
|
5007
|
+
emitDebugEvent("clear_active_request", {
|
|
5008
|
+
requestId: completedRequestId,
|
|
5009
|
+
kind: completedKind,
|
|
5010
|
+
notify: options?.notify ?? null,
|
|
5011
|
+
agentBusy,
|
|
5012
|
+
});
|
|
4399
5013
|
broadcastState();
|
|
4400
5014
|
if (options?.notify) {
|
|
4401
5015
|
broadcast({ type: "info", message: options.notify, level: options.level ?? "info" });
|
|
@@ -4403,6 +5017,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4403
5017
|
};
|
|
4404
5018
|
|
|
4405
5019
|
const beginRequest = (requestId: string, kind: StudioRequestKind): boolean => {
|
|
5020
|
+
emitDebugEvent("begin_request_attempt", {
|
|
5021
|
+
requestId,
|
|
5022
|
+
kind,
|
|
5023
|
+
hasActiveRequest: Boolean(activeRequest),
|
|
5024
|
+
agentBusy,
|
|
5025
|
+
});
|
|
4406
5026
|
if (activeRequest) {
|
|
4407
5027
|
broadcast({ type: "busy", requestId, message: "A studio request is already in progress." });
|
|
4408
5028
|
return false;
|
|
@@ -4414,6 +5034,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4414
5034
|
|
|
4415
5035
|
const timer = setTimeout(() => {
|
|
4416
5036
|
if (!activeRequest || activeRequest.id !== requestId) return;
|
|
5037
|
+
emitDebugEvent("request_timeout", { requestId, kind });
|
|
4417
5038
|
broadcast({ type: "error", requestId, message: "Studio request timed out. Please try again." });
|
|
4418
5039
|
clearActiveRequest();
|
|
4419
5040
|
}, REQUEST_TIMEOUT_MS);
|
|
@@ -4425,6 +5046,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4425
5046
|
timer,
|
|
4426
5047
|
};
|
|
4427
5048
|
|
|
5049
|
+
emitDebugEvent("begin_request", { requestId, kind });
|
|
4428
5050
|
broadcast({ type: "request_started", requestId, kind });
|
|
4429
5051
|
broadcastState();
|
|
4430
5052
|
return true;
|
|
@@ -4448,11 +5070,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
4448
5070
|
return;
|
|
4449
5071
|
}
|
|
4450
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
|
+
|
|
4451
5081
|
if (msg.type === "hello") {
|
|
4452
5082
|
sendToClient(client, {
|
|
4453
5083
|
type: "hello_ack",
|
|
4454
5084
|
busy: isStudioBusy(),
|
|
5085
|
+
agentBusy,
|
|
5086
|
+
terminalPhase: terminalActivityPhase,
|
|
5087
|
+
terminalToolName: terminalActivityToolName,
|
|
5088
|
+
terminalActivityLabel,
|
|
4455
5089
|
activeRequestId: activeRequest?.id ?? null,
|
|
5090
|
+
activeRequestKind: activeRequest?.kind ?? null,
|
|
4456
5091
|
lastResponse: lastStudioResponse,
|
|
4457
5092
|
initialDocument: initialStudioDocument,
|
|
4458
5093
|
});
|
|
@@ -4684,6 +5319,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
4684
5319
|
}
|
|
4685
5320
|
return;
|
|
4686
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
|
+
}
|
|
4687
5357
|
};
|
|
4688
5358
|
|
|
4689
5359
|
const handleRenderPreviewRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
@@ -4961,21 +5631,81 @@ export default function (pi: ExtensionAPI) {
|
|
|
4961
5631
|
|
|
4962
5632
|
pi.on("session_start", async (_event, ctx) => {
|
|
4963
5633
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
5634
|
+
agentBusy = false;
|
|
5635
|
+
emitDebugEvent("session_start", { entryCount: ctx.sessionManager.getBranch().length });
|
|
5636
|
+
setTerminalActivity("idle");
|
|
4964
5637
|
});
|
|
4965
5638
|
|
|
4966
5639
|
pi.on("session_switch", async (_event, ctx) => {
|
|
4967
5640
|
clearActiveRequest({ notify: "Session switched. Studio request state cleared.", level: "warning" });
|
|
4968
5641
|
lastCommandCtx = null;
|
|
4969
5642
|
hydrateLatestAssistant(ctx.sessionManager.getBranch());
|
|
5643
|
+
agentBusy = false;
|
|
5644
|
+
emitDebugEvent("session_switch", { entryCount: ctx.sessionManager.getBranch().length });
|
|
5645
|
+
setTerminalActivity("idle");
|
|
4970
5646
|
});
|
|
4971
5647
|
|
|
4972
5648
|
pi.on("agent_start", async () => {
|
|
4973
5649
|
agentBusy = true;
|
|
4974
|
-
|
|
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
|
+
}
|
|
4975
5683
|
});
|
|
4976
5684
|
|
|
4977
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 : "";
|
|
4978
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
|
+
|
|
4979
5709
|
if (!markdown) return;
|
|
4980
5710
|
|
|
4981
5711
|
if (activeRequest) {
|
|
@@ -4986,6 +5716,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
4986
5716
|
timestamp: Date.now(),
|
|
4987
5717
|
kind,
|
|
4988
5718
|
};
|
|
5719
|
+
emitDebugEvent("broadcast_response", {
|
|
5720
|
+
requestId,
|
|
5721
|
+
kind,
|
|
5722
|
+
markdownLength: markdown.length,
|
|
5723
|
+
stopReason,
|
|
5724
|
+
});
|
|
4989
5725
|
broadcast({
|
|
4990
5726
|
type: "response",
|
|
4991
5727
|
requestId,
|
|
@@ -5003,6 +5739,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
5003
5739
|
timestamp: Date.now(),
|
|
5004
5740
|
kind: inferredKind,
|
|
5005
5741
|
};
|
|
5742
|
+
emitDebugEvent("broadcast_latest_response", {
|
|
5743
|
+
kind: inferredKind,
|
|
5744
|
+
markdownLength: markdown.length,
|
|
5745
|
+
stopReason,
|
|
5746
|
+
});
|
|
5006
5747
|
broadcast({
|
|
5007
5748
|
type: "latest_response",
|
|
5008
5749
|
kind: inferredKind,
|
|
@@ -5013,7 +5754,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
5013
5754
|
|
|
5014
5755
|
pi.on("agent_end", async () => {
|
|
5015
5756
|
agentBusy = false;
|
|
5016
|
-
|
|
5757
|
+
emitDebugEvent("agent_end", { activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
|
|
5758
|
+
setTerminalActivity("idle");
|
|
5017
5759
|
if (activeRequest) {
|
|
5018
5760
|
const requestId = activeRequest.id;
|
|
5019
5761
|
broadcast({
|
|
@@ -5027,6 +5769,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
5027
5769
|
|
|
5028
5770
|
pi.on("session_shutdown", async () => {
|
|
5029
5771
|
lastCommandCtx = null;
|
|
5772
|
+
agentBusy = false;
|
|
5773
|
+
setTerminalActivity("idle");
|
|
5030
5774
|
await stopServer();
|
|
5031
5775
|
});
|
|
5032
5776
|
|