pi-studio 0.5.12 → 0.5.14

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.
@@ -0,0 +1,4032 @@
1
+ (() => {
2
+ const statusLineEl = document.getElementById("statusLine");
3
+ const statusEl = document.getElementById("status");
4
+ const statusSpinnerEl = document.getElementById("statusSpinner");
5
+ const footerMetaEl = document.getElementById("footerMeta");
6
+ const footerMetaTextEl = document.getElementById("footerMetaText");
7
+ const BRAILLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
8
+ let spinnerTimer = null;
9
+ let spinnerFrameIndex = 0;
10
+ if (statusEl) {
11
+ statusEl.textContent = "Connecting · Studio script starting…";
12
+ }
13
+
14
+ function hardFail(prefix, error) {
15
+ const details = error && error.message ? error.message : String(error || "unknown error");
16
+ if (spinnerTimer) {
17
+ window.clearInterval(spinnerTimer);
18
+ spinnerTimer = null;
19
+ }
20
+ if (statusLineEl && statusLineEl.classList) {
21
+ statusLineEl.classList.remove("with-spinner");
22
+ }
23
+ if (statusSpinnerEl) {
24
+ statusSpinnerEl.textContent = "";
25
+ }
26
+ if (statusEl) {
27
+ statusEl.textContent = "Disconnected · " + prefix + ": " + details;
28
+ statusEl.className = "error";
29
+ }
30
+ }
31
+
32
+ window.addEventListener("error", (event) => {
33
+ hardFail("Studio UI script error", event && event.error ? event.error : event.message);
34
+ });
35
+
36
+ window.addEventListener("unhandledrejection", (event) => {
37
+ hardFail("Studio UI promise error", event ? event.reason : "unknown rejection");
38
+ });
39
+
40
+ try {
41
+ const sourceEditorWrapEl = document.getElementById("sourceEditorWrap");
42
+ const sourceTextEl = document.getElementById("sourceText");
43
+ const sourceHighlightEl = document.getElementById("sourceHighlight");
44
+ const sourcePreviewEl = document.getElementById("sourcePreview");
45
+ const leftPaneEl = document.getElementById("leftPane");
46
+ const rightPaneEl = document.getElementById("rightPane");
47
+ const sourceBadgeEl = document.getElementById("sourceBadge");
48
+ const syncBadgeEl = document.getElementById("syncBadge");
49
+ const critiqueViewEl = document.getElementById("critiqueView");
50
+ const referenceBadgeEl = document.getElementById("referenceBadge");
51
+ const editorViewSelect = document.getElementById("editorViewSelect");
52
+ const rightViewSelect = document.getElementById("rightViewSelect");
53
+ const followSelect = document.getElementById("followSelect");
54
+ const responseHighlightSelect = document.getElementById("responseHighlightSelect");
55
+ const pullLatestBtn = document.getElementById("pullLatestBtn");
56
+ const insertHeaderBtn = document.getElementById("insertHeaderBtn");
57
+ const critiqueBtn = document.getElementById("critiqueBtn");
58
+ const lensSelect = document.getElementById("lensSelect");
59
+ const fileInput = document.getElementById("fileInput");
60
+ const resourceDirBtn = document.getElementById("resourceDirBtn");
61
+ const resourceDirLabel = document.getElementById("resourceDirLabel");
62
+ const resourceDirInputWrap = document.getElementById("resourceDirInputWrap");
63
+ const resourceDirInput = document.getElementById("resourceDirInput");
64
+ const resourceDirClearBtn = document.getElementById("resourceDirClearBtn");
65
+ const loadResponseBtn = document.getElementById("loadResponseBtn");
66
+ const loadCritiqueNotesBtn = document.getElementById("loadCritiqueNotesBtn");
67
+ const loadCritiqueFullBtn = document.getElementById("loadCritiqueFullBtn");
68
+ const copyResponseBtn = document.getElementById("copyResponseBtn");
69
+ const exportPdfBtn = document.getElementById("exportPdfBtn");
70
+ const historyPrevBtn = document.getElementById("historyPrevBtn");
71
+ const historyNextBtn = document.getElementById("historyNextBtn");
72
+ const historyLastBtn = document.getElementById("historyLastBtn");
73
+ const historyIndexBadgeEl = document.getElementById("historyIndexBadge");
74
+ const loadHistoryPromptBtn = document.getElementById("loadHistoryPromptBtn");
75
+ const saveAsBtn = document.getElementById("saveAsBtn");
76
+ const saveOverBtn = document.getElementById("saveOverBtn");
77
+ const sendEditorBtn = document.getElementById("sendEditorBtn");
78
+ const getEditorBtn = document.getElementById("getEditorBtn");
79
+ const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
80
+ const sendRunBtn = document.getElementById("sendRunBtn");
81
+ const copyDraftBtn = document.getElementById("copyDraftBtn");
82
+ const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
83
+ const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
84
+ const highlightSelect = document.getElementById("highlightSelect");
85
+ const langSelect = document.getElementById("langSelect");
86
+ const annotationModeSelect = document.getElementById("annotationModeSelect");
87
+ const compactBtn = document.getElementById("compactBtn");
88
+
89
+ const initialSourceState = {
90
+ source: (document.body && document.body.dataset && document.body.dataset.initialSource) || "blank",
91
+ label: (document.body && document.body.dataset && document.body.dataset.initialLabel) || "blank",
92
+ path: (document.body && document.body.dataset && document.body.dataset.initialPath) || null,
93
+ };
94
+
95
+ let ws = null;
96
+ let wsState = "Connecting";
97
+ let statusMessage = "Connecting · Studio script starting…";
98
+ let statusLevel = "";
99
+ let reconnectTimer = null;
100
+ let reconnectAttempt = 0;
101
+ let pendingRequestId = null;
102
+ let pendingKind = null;
103
+ let stickyStudioKind = null;
104
+ let initialDocumentApplied = false;
105
+ let editorView = "markdown";
106
+ let rightView = "preview";
107
+ let followLatest = true;
108
+ let queuedLatestResponse = null;
109
+ let latestResponseMarkdown = "";
110
+ let latestResponseThinking = "";
111
+ let latestResponseTimestamp = 0;
112
+ let latestResponseKind = "annotation";
113
+ let latestResponseIsStructuredCritique = false;
114
+ let latestResponseHasContent = false;
115
+ let latestResponseNormalized = "";
116
+ let latestResponseThinkingNormalized = "";
117
+ let latestCritiqueNotes = "";
118
+ let latestCritiqueNotesNormalized = "";
119
+ let responseHistory = [];
120
+ let responseHistoryIndex = -1;
121
+ let agentBusyFromServer = false;
122
+ let terminalActivityPhase = "idle";
123
+ let terminalActivityToolName = "";
124
+ let terminalActivityLabel = "";
125
+ let lastSpecificToolLabel = "";
126
+ let uiBusy = false;
127
+ let pdfExportInProgress = false;
128
+ let compactInProgress = false;
129
+ let modelLabel = (document.body && document.body.dataset && document.body.dataset.modelLabel) || "none";
130
+ let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
131
+ let contextTokens = null;
132
+ let contextWindow = null;
133
+ let contextPercent = null;
134
+ let updateInstalledVersion = null;
135
+ let updateLatestVersion = null;
136
+ let windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : true;
137
+ let titleAttentionMessage = "";
138
+ let titleAttentionRequestId = null;
139
+ let titleAttentionRequestKind = null;
140
+
141
+ function parseFiniteNumber(value) {
142
+ if (value == null || value === "") return null;
143
+ const parsed = Number(value);
144
+ return Number.isFinite(parsed) ? parsed : null;
145
+ }
146
+
147
+ function parseNonEmptyString(value) {
148
+ if (typeof value !== "string") return null;
149
+ const trimmed = value.trim();
150
+ return trimmed ? trimmed : null;
151
+ }
152
+
153
+ contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
154
+ contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
155
+ contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
156
+
157
+ let sourceState = {
158
+ source: initialSourceState.source,
159
+ label: initialSourceState.label,
160
+ path: initialSourceState.path,
161
+ };
162
+ let activePane = "left";
163
+ let paneFocusTarget = "off";
164
+ const EDITOR_HIGHLIGHT_MAX_CHARS = 80_000;
165
+ const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
166
+ const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
167
+ // Single source of truth: language -> file extensions (and display label)
168
+ var LANG_EXT_MAP = {
169
+ markdown: { label: "Markdown", exts: ["md", "markdown", "mdx"] },
170
+ javascript: { label: "JavaScript", exts: ["js", "mjs", "cjs", "jsx"] },
171
+ typescript: { label: "TypeScript", exts: ["ts", "mts", "cts", "tsx"] },
172
+ python: { label: "Python", exts: ["py", "pyw"] },
173
+ bash: { label: "Bash", exts: ["sh", "bash", "zsh"] },
174
+ json: { label: "JSON", exts: ["json", "jsonc", "json5"] },
175
+ rust: { label: "Rust", exts: ["rs"] },
176
+ c: { label: "C", exts: ["c", "h"] },
177
+ cpp: { label: "C++", exts: ["cpp", "cxx", "cc", "hpp", "hxx"] },
178
+ julia: { label: "Julia", exts: ["jl"] },
179
+ fortran: { label: "Fortran", exts: ["f90", "f95", "f03", "f", "for"] },
180
+ r: { label: "R", exts: ["r", "R"] },
181
+ matlab: { label: "MATLAB", exts: ["m"] },
182
+ latex: { label: "LaTeX", exts: ["tex", "latex"] },
183
+ diff: { label: "Diff", exts: ["diff", "patch"] },
184
+ // Languages accepted for upload/detect but without syntax highlighting
185
+ java: { label: "Java", exts: ["java"] },
186
+ go: { label: "Go", exts: ["go"] },
187
+ ruby: { label: "Ruby", exts: ["rb"] },
188
+ swift: { label: "Swift", exts: ["swift"] },
189
+ html: { label: "HTML", exts: ["html", "htm"] },
190
+ css: { label: "CSS", exts: ["css"] },
191
+ xml: { label: "XML", exts: ["xml"] },
192
+ yaml: { label: "YAML", exts: ["yaml", "yml"] },
193
+ toml: { label: "TOML", exts: ["toml"] },
194
+ lua: { label: "Lua", exts: ["lua"] },
195
+ text: { label: "Plain Text", exts: ["txt", "rst", "adoc"] },
196
+ };
197
+ // Build reverse map: extension -> language
198
+ var EXT_TO_LANG = {};
199
+ Object.keys(LANG_EXT_MAP).forEach(function(lang) {
200
+ LANG_EXT_MAP[lang].exts.forEach(function(ext) { EXT_TO_LANG[ext.toLowerCase()] = lang; });
201
+ });
202
+ // Languages that have syntax highlighting support
203
+ var HIGHLIGHTED_LANGUAGES = ["markdown", "javascript", "typescript", "python", "bash", "json", "rust", "c", "cpp", "julia", "fortran", "r", "matlab", "latex"];
204
+ var SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
205
+ const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
206
+ const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
207
+ const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
208
+ const PREVIEW_INPUT_DEBOUNCE_MS = 0;
209
+ const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
210
+ const previewPendingTimers = new WeakMap();
211
+ let sourcePreviewRenderTimer = null;
212
+ let sourcePreviewRenderNonce = 0;
213
+ let responsePreviewRenderNonce = 0;
214
+ let responseEditorPreviewTimer = null;
215
+ let editorMetaUpdateRaf = null;
216
+ let editorHighlightEnabled = false;
217
+ let editorLanguage = "markdown";
218
+ let responseHighlightEnabled = false;
219
+ let editorHighlightRenderRaf = null;
220
+ let annotationsEnabled = true;
221
+ const ANNOTATION_MARKER_REGEX = /\[an:\s*([^\]]+?)\]/gi;
222
+ const EMPTY_OVERLAY_LINE = "\u200b";
223
+ const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
224
+ const BOOT = (typeof window.__PI_STUDIO_BOOT__ === "object" && window.__PI_STUDIO_BOOT__)
225
+ ? window.__PI_STUDIO_BOOT__
226
+ : {};
227
+ const MERMAID_CONFIG = (BOOT.mermaidConfig && typeof BOOT.mermaidConfig === "object")
228
+ ? BOOT.mermaidConfig
229
+ : {};
230
+ const MERMAID_UNAVAILABLE_MESSAGE = "Mermaid renderer unavailable. Showing mermaid blocks as code.";
231
+ const MERMAID_RENDER_FAIL_MESSAGE = "Mermaid render failed. Showing diagram source text.";
232
+ let mermaidModulePromise = null;
233
+ let mermaidInitialized = false;
234
+
235
+ const DEBUG_ENABLED = (() => {
236
+ try {
237
+ const query = new URLSearchParams(window.location.search || "");
238
+ const hash = new URLSearchParams((window.location.hash || "").replace(/^#/, ""));
239
+ const value = String(query.get("debug") || hash.get("debug") || "").trim().toLowerCase();
240
+ return value === "1" || value === "true" || value === "yes" || value === "on";
241
+ } catch {
242
+ return false;
243
+ }
244
+ })();
245
+ const DEBUG_LOG_MAX = 400;
246
+ const debugLog = [];
247
+
248
+ function debugTrace(eventName, payload) {
249
+ if (!DEBUG_ENABLED) return;
250
+ const entry = {
251
+ ts: Date.now(),
252
+ event: String(eventName || ""),
253
+ payload: payload || null,
254
+ };
255
+ debugLog.push(entry);
256
+ if (debugLog.length > DEBUG_LOG_MAX) debugLog.shift();
257
+ window.__piStudioDebugLog = debugLog.slice();
258
+ try {
259
+ console.debug("[pi-studio]", new Date(entry.ts).toISOString(), entry.event, entry.payload);
260
+ } catch {
261
+ // ignore console errors
262
+ }
263
+ }
264
+
265
+ function summarizeServerMessage(message) {
266
+ if (!message || typeof message !== "object") return { type: "invalid" };
267
+ const summary = {
268
+ type: typeof message.type === "string" ? message.type : "unknown",
269
+ };
270
+ if (typeof message.requestId === "string") summary.requestId = message.requestId;
271
+ if (typeof message.activeRequestId === "string") summary.activeRequestId = message.activeRequestId;
272
+ if (typeof message.activeRequestKind === "string") summary.activeRequestKind = message.activeRequestKind;
273
+ if (typeof message.kind === "string") summary.kind = message.kind;
274
+ if (typeof message.event === "string") summary.event = message.event;
275
+ if (typeof message.timestamp === "number") summary.timestamp = message.timestamp;
276
+ if (typeof message.busy === "boolean") summary.busy = message.busy;
277
+ if (typeof message.agentBusy === "boolean") summary.agentBusy = message.agentBusy;
278
+ if (typeof message.terminalPhase === "string") summary.terminalPhase = message.terminalPhase;
279
+ if (typeof message.terminalToolName === "string") summary.terminalToolName = message.terminalToolName;
280
+ if (typeof message.terminalActivityLabel === "string") summary.terminalActivityLabel = message.terminalActivityLabel;
281
+ if (typeof message.modelLabel === "string") summary.modelLabel = message.modelLabel;
282
+ if (typeof message.terminalSessionLabel === "string") summary.terminalSessionLabel = message.terminalSessionLabel;
283
+ if (typeof message.contextTokens === "number") summary.contextTokens = message.contextTokens;
284
+ if (typeof message.contextWindow === "number") summary.contextWindow = message.contextWindow;
285
+ if (typeof message.contextPercent === "number") summary.contextPercent = message.contextPercent;
286
+ if (typeof message.updateInstalledVersion === "string") summary.updateInstalledVersion = message.updateInstalledVersion;
287
+ if (typeof message.updateLatestVersion === "string") summary.updateLatestVersion = message.updateLatestVersion;
288
+ if (message.document && typeof message.document === "object" && typeof message.document.text === "string") {
289
+ summary.documentLength = message.document.text.length;
290
+ if (typeof message.document.label === "string") summary.documentLabel = message.document.label;
291
+ }
292
+ if (typeof message.compactInProgress === "boolean") summary.compactInProgress = message.compactInProgress;
293
+ if (typeof message.stopReason === "string") summary.stopReason = message.stopReason;
294
+ if (typeof message.markdown === "string") summary.markdownLength = message.markdown.length;
295
+ if (typeof message.label === "string") summary.label = message.label;
296
+ if (Array.isArray(message.responseHistory)) summary.responseHistoryCount = message.responseHistory.length;
297
+ if (Array.isArray(message.items)) summary.itemsCount = message.items.length;
298
+ if (typeof message.details === "object" && message.details !== null) summary.details = message.details;
299
+ return summary;
300
+ }
301
+
302
+ function getIdleStatus() {
303
+ return "Edit, load, or annotate text, then run, save, send to pi editor, or critique.";
304
+ }
305
+
306
+ function normalizeTerminalPhase(phase) {
307
+ if (phase === "running" || phase === "tool" || phase === "responding") return phase;
308
+ return "idle";
309
+ }
310
+
311
+ function normalizeActivityLabel(label) {
312
+ if (typeof label !== "string") return "";
313
+ return label.replace(/\s+/g, " ").trim();
314
+ }
315
+
316
+ function isGenericToolLabel(label) {
317
+ const normalized = normalizeActivityLabel(label).toLowerCase();
318
+ if (!normalized) return true;
319
+ return normalized.startsWith("running ")
320
+ || normalized === "reading file"
321
+ || normalized === "writing file"
322
+ || normalized === "editing file";
323
+ }
324
+
325
+ function withEllipsis(text) {
326
+ const value = String(text || "").trim();
327
+ if (!value) return "";
328
+ if (/[….!?]$/.test(value)) return value;
329
+ return value + "…";
330
+ }
331
+
332
+ function updateTerminalActivityState(phase, toolName, label) {
333
+ terminalActivityPhase = normalizeTerminalPhase(phase);
334
+ terminalActivityToolName = typeof toolName === "string" ? toolName.trim() : "";
335
+ terminalActivityLabel = normalizeActivityLabel(label);
336
+
337
+ if (terminalActivityPhase === "tool" && terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
338
+ lastSpecificToolLabel = terminalActivityLabel;
339
+ }
340
+ if (terminalActivityPhase === "idle") {
341
+ lastSpecificToolLabel = "";
342
+ }
343
+
344
+ syncFooterSpinnerState();
345
+ }
346
+
347
+ function getTerminalBusyStatus() {
348
+ if (terminalActivityPhase === "tool") {
349
+ if (terminalActivityLabel) {
350
+ return "Terminal: " + withEllipsis(terminalActivityLabel);
351
+ }
352
+ return terminalActivityToolName
353
+ ? "Terminal: running tool: " + terminalActivityToolName + "…"
354
+ : "Terminal: running tool…";
355
+ }
356
+ if (terminalActivityPhase === "responding") {
357
+ if (lastSpecificToolLabel) {
358
+ return "Terminal: " + lastSpecificToolLabel + " (generating response)…";
359
+ }
360
+ return "Terminal: generating response…";
361
+ }
362
+ if (terminalActivityPhase === "running" && lastSpecificToolLabel) {
363
+ return "Terminal: " + withEllipsis(lastSpecificToolLabel);
364
+ }
365
+ return "Terminal: running…";
366
+ }
367
+
368
+ function getStudioActionLabel(kind) {
369
+ if (kind === "annotation") return "sending annotated reply";
370
+ if (kind === "critique") return "running critique";
371
+ if (kind === "direct") return "running editor text";
372
+ if (kind === "compact") return "compacting context";
373
+ if (kind === "send_to_editor") return "sending to pi editor";
374
+ if (kind === "get_from_editor") return "loading from pi editor";
375
+ if (kind === "load_git_diff") return "loading git diff";
376
+ if (kind === "save_as" || kind === "save_over") return "saving editor text";
377
+ return "submitting request";
378
+ }
379
+
380
+ function getStudioBusyStatus(kind) {
381
+ const action = getStudioActionLabel(kind);
382
+ if (terminalActivityPhase === "tool") {
383
+ if (terminalActivityLabel) {
384
+ return "Studio: " + withEllipsis(terminalActivityLabel);
385
+ }
386
+ return terminalActivityToolName
387
+ ? "Studio: " + action + " (tool: " + terminalActivityToolName + ")…"
388
+ : "Studio: " + action + " (running tool)…";
389
+ }
390
+ if (terminalActivityPhase === "responding") {
391
+ if (lastSpecificToolLabel) {
392
+ return "Studio: " + lastSpecificToolLabel + " (generating response)…";
393
+ }
394
+ return "Studio: " + action + " (generating response)…";
395
+ }
396
+ if (terminalActivityPhase === "running" && lastSpecificToolLabel) {
397
+ return "Studio: " + withEllipsis(lastSpecificToolLabel);
398
+ }
399
+ return "Studio: " + action + "…";
400
+ }
401
+
402
+ function shouldAnimateFooterSpinner() {
403
+ return wsState !== "Disconnected" && (uiBusy || agentBusyFromServer || terminalActivityPhase !== "idle");
404
+ }
405
+
406
+ function formatNumber(value) {
407
+ if (typeof value !== "number" || !Number.isFinite(value)) return "?";
408
+ try {
409
+ return new Intl.NumberFormat().format(Math.round(value));
410
+ } catch {
411
+ return String(Math.round(value));
412
+ }
413
+ }
414
+
415
+ function formatContextUsageText() {
416
+ const hasWindow = typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0;
417
+ const hasTokens = typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens >= 0;
418
+ let percentValue = typeof contextPercent === "number" && Number.isFinite(contextPercent)
419
+ ? contextPercent
420
+ : null;
421
+
422
+ if (percentValue == null && hasTokens && hasWindow) {
423
+ percentValue = (contextTokens / contextWindow) * 100;
424
+ }
425
+
426
+ if (!hasTokens && !hasWindow) {
427
+ return "Context: unknown";
428
+ }
429
+ if (!hasTokens && hasWindow) {
430
+ return "Context: ? / " + formatNumber(contextWindow);
431
+ }
432
+
433
+ let text = "Context: " + formatNumber(contextTokens);
434
+ if (hasWindow) {
435
+ text += " / " + formatNumber(contextWindow);
436
+ }
437
+ if (percentValue != null && Number.isFinite(percentValue)) {
438
+ const bounded = Math.max(0, Math.min(100, percentValue));
439
+ text += " (" + bounded.toFixed(1) + "%)";
440
+ }
441
+ return text;
442
+ }
443
+
444
+ function applyContextUsageFromMessage(message) {
445
+ if (!message || typeof message !== "object") return false;
446
+
447
+ let changed = false;
448
+
449
+ if (Object.prototype.hasOwnProperty.call(message, "contextTokens")) {
450
+ const next = typeof message.contextTokens === "number" && Number.isFinite(message.contextTokens) && message.contextTokens >= 0
451
+ ? message.contextTokens
452
+ : null;
453
+ if (next !== contextTokens) {
454
+ contextTokens = next;
455
+ changed = true;
456
+ }
457
+ }
458
+
459
+ if (Object.prototype.hasOwnProperty.call(message, "contextWindow")) {
460
+ const next = typeof message.contextWindow === "number" && Number.isFinite(message.contextWindow) && message.contextWindow > 0
461
+ ? message.contextWindow
462
+ : null;
463
+ if (next !== contextWindow) {
464
+ contextWindow = next;
465
+ changed = true;
466
+ }
467
+ }
468
+
469
+ if (Object.prototype.hasOwnProperty.call(message, "contextPercent")) {
470
+ const next = typeof message.contextPercent === "number" && Number.isFinite(message.contextPercent)
471
+ ? Math.max(0, Math.min(100, message.contextPercent))
472
+ : null;
473
+ if (next !== contextPercent) {
474
+ contextPercent = next;
475
+ changed = true;
476
+ }
477
+ }
478
+
479
+ return changed;
480
+ }
481
+
482
+ function applyUpdateInfoFromMessage(message) {
483
+ if (!message || typeof message !== "object") return false;
484
+
485
+ let changed = false;
486
+
487
+ if (Object.prototype.hasOwnProperty.call(message, "updateInstalledVersion")) {
488
+ const nextInstalled = parseNonEmptyString(message.updateInstalledVersion);
489
+ if (nextInstalled !== updateInstalledVersion) {
490
+ updateInstalledVersion = nextInstalled;
491
+ changed = true;
492
+ }
493
+ }
494
+
495
+ if (Object.prototype.hasOwnProperty.call(message, "updateLatestVersion")) {
496
+ const nextLatest = parseNonEmptyString(message.updateLatestVersion);
497
+ if (nextLatest !== updateLatestVersion) {
498
+ updateLatestVersion = nextLatest;
499
+ changed = true;
500
+ }
501
+ }
502
+
503
+ return changed;
504
+ }
505
+
506
+ function isTitleAttentionRequestKind(kind) {
507
+ return kind === "annotation" || kind === "critique" || kind === "direct";
508
+ }
509
+
510
+ function armTitleAttentionForRequest(requestId, kind) {
511
+ if (typeof requestId !== "string" || !isTitleAttentionRequestKind(kind)) {
512
+ titleAttentionRequestId = null;
513
+ titleAttentionRequestKind = null;
514
+ return;
515
+ }
516
+ titleAttentionRequestId = requestId;
517
+ titleAttentionRequestKind = kind;
518
+ }
519
+
520
+ function clearArmedTitleAttention(requestId) {
521
+ if (typeof requestId === "string" && titleAttentionRequestId && requestId !== titleAttentionRequestId) {
522
+ return;
523
+ }
524
+ titleAttentionRequestId = null;
525
+ titleAttentionRequestKind = null;
526
+ }
527
+
528
+ function clearTitleAttention() {
529
+ if (!titleAttentionMessage) return;
530
+ titleAttentionMessage = "";
531
+ updateDocumentTitle();
532
+ }
533
+
534
+ function shouldShowTitleAttention() {
535
+ const focused = typeof document.hasFocus === "function" ? document.hasFocus() : windowHasFocus;
536
+ return Boolean(document.hidden) || !focused;
537
+ }
538
+
539
+ function getTitleAttentionMessage(kind) {
540
+ if (kind === "critique") return "● Critique ready";
541
+ if (kind === "direct") return "● Response ready";
542
+ return "● Reply ready";
543
+ }
544
+
545
+ function maybeShowTitleAttentionForCompletedRequest(requestId, kind) {
546
+ const matchedRequest = typeof requestId === "string" && titleAttentionRequestId && requestId === titleAttentionRequestId;
547
+ const completedKind = isTitleAttentionRequestKind(kind) ? kind : titleAttentionRequestKind;
548
+ clearArmedTitleAttention(requestId);
549
+ if (!matchedRequest || !completedKind || !shouldShowTitleAttention()) {
550
+ return;
551
+ }
552
+ titleAttentionMessage = getTitleAttentionMessage(completedKind);
553
+ updateDocumentTitle();
554
+ }
555
+
556
+ function updateDocumentTitle() {
557
+ const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
558
+ const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
559
+ const titleParts = ["pi Studio"];
560
+ if (terminalText && terminalText !== "unknown") titleParts.push(terminalText);
561
+ if (modelText && modelText !== "none") titleParts.push(modelText);
562
+ if (titleAttentionMessage) titleParts.unshift(titleAttentionMessage);
563
+ document.title = titleParts.join(" · ");
564
+ }
565
+
566
+ function updateFooterMeta() {
567
+ const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
568
+ const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
569
+ const contextText = formatContextUsageText();
570
+ let updateText = "";
571
+ if (updateLatestVersion) {
572
+ updateText = updateInstalledVersion
573
+ ? "Update: " + updateInstalledVersion + " → " + updateLatestVersion
574
+ : "Update: " + updateLatestVersion + " available";
575
+ }
576
+ const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText + (updateText ? " · " + updateText : "");
577
+ if (footerMetaTextEl) {
578
+ footerMetaTextEl.textContent = text;
579
+ footerMetaTextEl.title = text;
580
+ } else if (footerMetaEl) {
581
+ footerMetaEl.textContent = text;
582
+ footerMetaEl.title = text;
583
+ }
584
+ updateDocumentTitle();
585
+ }
586
+
587
+ function stopFooterSpinner() {
588
+ if (spinnerTimer) {
589
+ window.clearInterval(spinnerTimer);
590
+ spinnerTimer = null;
591
+ }
592
+ }
593
+
594
+ function startFooterSpinner() {
595
+ if (spinnerTimer) return;
596
+ spinnerTimer = window.setInterval(() => {
597
+ spinnerFrameIndex = (spinnerFrameIndex + 1) % BRAILLE_SPINNER_FRAMES.length;
598
+ renderStatus();
599
+ }, 80);
600
+ }
601
+
602
+ function syncFooterSpinnerState() {
603
+ if (shouldAnimateFooterSpinner()) {
604
+ startFooterSpinner();
605
+ } else {
606
+ stopFooterSpinner();
607
+ }
608
+ }
609
+
610
+ function renderStatus() {
611
+ statusEl.textContent = statusMessage;
612
+ statusEl.className = statusLevel || "";
613
+
614
+ const spinnerActive = shouldAnimateFooterSpinner();
615
+ if (statusLineEl && statusLineEl.classList) {
616
+ statusLineEl.classList.toggle("with-spinner", spinnerActive);
617
+ }
618
+ if (statusSpinnerEl) {
619
+ statusSpinnerEl.textContent = spinnerActive
620
+ ? (BRAILLE_SPINNER_FRAMES[spinnerFrameIndex % BRAILLE_SPINNER_FRAMES.length] || "")
621
+ : "";
622
+ }
623
+
624
+ updateFooterMeta();
625
+ }
626
+
627
+ function setWsState(nextState) {
628
+ wsState = nextState || "Disconnected";
629
+ syncFooterSpinnerState();
630
+ renderStatus();
631
+ syncActionButtons();
632
+ }
633
+
634
+ function setStatus(message, level) {
635
+ statusMessage = message;
636
+ statusLevel = level || "";
637
+ syncFooterSpinnerState();
638
+ renderStatus();
639
+ debugTrace("status", {
640
+ wsState,
641
+ message: statusMessage,
642
+ level: statusLevel,
643
+ pendingRequestId,
644
+ pendingKind,
645
+ uiBusy,
646
+ agentBusyFromServer,
647
+ terminalPhase: terminalActivityPhase,
648
+ terminalToolName: terminalActivityToolName,
649
+ terminalActivityLabel,
650
+ lastSpecificToolLabel,
651
+ });
652
+ }
653
+
654
+ renderStatus();
655
+
656
+ window.addEventListener("focus", () => {
657
+ windowHasFocus = true;
658
+ clearTitleAttention();
659
+ });
660
+
661
+ window.addEventListener("blur", () => {
662
+ windowHasFocus = false;
663
+ });
664
+
665
+ document.addEventListener("visibilitychange", () => {
666
+ if (!document.hidden) {
667
+ windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : windowHasFocus;
668
+ if (windowHasFocus) {
669
+ clearTitleAttention();
670
+ }
671
+ }
672
+ });
673
+
674
+ function updateSourceBadge() {
675
+ const label = sourceState && sourceState.label ? sourceState.label : "blank";
676
+ sourceBadgeEl.textContent = "Editor origin: " + label;
677
+ // Show "Set working dir" button when not file-backed
678
+ var isFileBacked = sourceState.source === "file" && Boolean(sourceState.path);
679
+ if (isFileBacked) {
680
+ if (resourceDirInput) resourceDirInput.value = "";
681
+ if (resourceDirLabel) resourceDirLabel.textContent = "";
682
+ if (resourceDirBtn) resourceDirBtn.hidden = true;
683
+ if (resourceDirLabel) resourceDirLabel.hidden = true;
684
+ if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
685
+ } else {
686
+ // Restore to label if dir is set, otherwise show button
687
+ var dir = resourceDirInput ? resourceDirInput.value.trim() : "";
688
+ if (dir) {
689
+ if (resourceDirBtn) resourceDirBtn.hidden = true;
690
+ if (resourceDirLabel) { resourceDirLabel.textContent = "Working dir: " + dir; resourceDirLabel.hidden = false; }
691
+ if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
692
+ } else {
693
+ if (resourceDirBtn) resourceDirBtn.hidden = false;
694
+ if (resourceDirLabel) resourceDirLabel.hidden = true;
695
+ if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
696
+ }
697
+ }
698
+ }
699
+
700
+ function applyPaneFocusClasses() {
701
+ document.body.classList.remove("pane-focus-left", "pane-focus-right");
702
+ if (paneFocusTarget === "left") {
703
+ document.body.classList.add("pane-focus-left");
704
+ } else if (paneFocusTarget === "right") {
705
+ document.body.classList.add("pane-focus-right");
706
+ }
707
+ }
708
+
709
+ function setActivePane(nextPane) {
710
+ activePane = nextPane === "right" ? "right" : "left";
711
+
712
+ if (leftPaneEl) leftPaneEl.classList.toggle("pane-active", activePane === "left");
713
+ if (rightPaneEl) rightPaneEl.classList.toggle("pane-active", activePane === "right");
714
+
715
+ if (paneFocusTarget !== "off" && paneFocusTarget !== activePane) {
716
+ paneFocusTarget = activePane;
717
+ applyPaneFocusClasses();
718
+ }
719
+ }
720
+
721
+ function paneLabel(pane) {
722
+ if (pane === "right") {
723
+ return "Response";
724
+ }
725
+ return "Editor";
726
+ }
727
+
728
+ function togglePaneFocus() {
729
+ if (paneFocusTarget === activePane) {
730
+ paneFocusTarget = "off";
731
+ applyPaneFocusClasses();
732
+ setStatus("Focus mode off.");
733
+ return;
734
+ }
735
+
736
+ paneFocusTarget = activePane;
737
+ applyPaneFocusClasses();
738
+ setStatus("Focus mode: " + paneLabel(activePane) + " pane (Esc to exit).");
739
+ }
740
+
741
+ function exitPaneFocus() {
742
+ if (paneFocusTarget === "off") return false;
743
+ paneFocusTarget = "off";
744
+ applyPaneFocusClasses();
745
+ setStatus("Focus mode off.");
746
+ return true;
747
+ }
748
+
749
+ function handlePaneShortcut(event) {
750
+ if (!event) return;
751
+
752
+ const key = typeof event.key === "string" ? event.key : "";
753
+ const isToggleShortcut =
754
+ (key === "Escape" && (event.metaKey || event.ctrlKey))
755
+ || key === "F10";
756
+
757
+ if (isToggleShortcut) {
758
+ event.preventDefault();
759
+ togglePaneFocus();
760
+ return;
761
+ }
762
+
763
+ if (
764
+ key === "Escape"
765
+ && !event.metaKey
766
+ && !event.ctrlKey
767
+ && !event.altKey
768
+ && !event.shiftKey
769
+ ) {
770
+ if (exitPaneFocus()) {
771
+ event.preventDefault();
772
+ }
773
+ }
774
+
775
+ if (
776
+ key === "Enter"
777
+ && (event.metaKey || event.ctrlKey)
778
+ && !event.altKey
779
+ && !event.shiftKey
780
+ && activePane === "left"
781
+ && sendRunBtn
782
+ && !sendRunBtn.disabled
783
+ ) {
784
+ event.preventDefault();
785
+ sendRunBtn.click();
786
+ }
787
+ }
788
+
789
+ function formatReferenceTime(timestamp) {
790
+ if (typeof timestamp !== "number" || !Number.isFinite(timestamp) || timestamp <= 0) return "";
791
+ try {
792
+ return new Date(timestamp).toLocaleTimeString([], {
793
+ hour: "2-digit",
794
+ minute: "2-digit",
795
+ second: "2-digit",
796
+ });
797
+ } catch {
798
+ return "";
799
+ }
800
+ }
801
+
802
+ function normalizeHistoryKind(kind) {
803
+ return kind === "critique" ? "critique" : "annotation";
804
+ }
805
+
806
+ function normalizeHistoryItem(item, fallbackIndex) {
807
+ if (!item || typeof item !== "object") return null;
808
+ if (typeof item.markdown !== "string") return null;
809
+ const markdown = item.markdown;
810
+ if (!markdown.trim()) return null;
811
+
812
+ const id = typeof item.id === "string" && item.id.trim()
813
+ ? item.id.trim()
814
+ : ("history-" + fallbackIndex + "-" + Date.now());
815
+ const timestamp = typeof item.timestamp === "number" && Number.isFinite(item.timestamp) && item.timestamp > 0
816
+ ? item.timestamp
817
+ : Date.now();
818
+ const prompt = typeof item.prompt === "string"
819
+ ? item.prompt
820
+ : (item.prompt == null ? null : String(item.prompt));
821
+ const thinking = typeof item.thinking === "string"
822
+ ? item.thinking
823
+ : (item.thinking == null ? null : String(item.thinking));
824
+
825
+ return {
826
+ id,
827
+ markdown,
828
+ thinking,
829
+ timestamp,
830
+ kind: normalizeHistoryKind(item.kind),
831
+ prompt,
832
+ };
833
+ }
834
+
835
+ function getSelectedHistoryItem() {
836
+ if (!Array.isArray(responseHistory) || responseHistory.length === 0) return null;
837
+ if (responseHistoryIndex < 0 || responseHistoryIndex >= responseHistory.length) return null;
838
+ return responseHistory[responseHistoryIndex] || null;
839
+ }
840
+
841
+ function clearActiveResponseView() {
842
+ latestResponseMarkdown = "";
843
+ latestResponseThinking = "";
844
+ latestResponseKind = "annotation";
845
+ latestResponseTimestamp = 0;
846
+ latestResponseIsStructuredCritique = false;
847
+ latestResponseHasContent = false;
848
+ latestResponseNormalized = "";
849
+ latestResponseThinkingNormalized = "";
850
+ latestCritiqueNotes = "";
851
+ latestCritiqueNotesNormalized = "";
852
+ refreshResponseUi();
853
+ }
854
+
855
+ function updateHistoryControls() {
856
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
857
+ const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
858
+ ? responseHistoryIndex + 1
859
+ : 0;
860
+ if (historyIndexBadgeEl) {
861
+ historyIndexBadgeEl.textContent = "History: " + selected + "/" + total;
862
+ }
863
+ if (historyPrevBtn) {
864
+ historyPrevBtn.disabled = total <= 1 || responseHistoryIndex <= 0;
865
+ }
866
+ if (historyNextBtn) {
867
+ historyNextBtn.disabled = total <= 1 || responseHistoryIndex < 0 || responseHistoryIndex >= total - 1;
868
+ }
869
+ if (historyLastBtn) {
870
+ historyLastBtn.disabled = total <= 1 || responseHistoryIndex < 0 || responseHistoryIndex >= total - 1;
871
+ }
872
+
873
+ const selectedItem = getSelectedHistoryItem();
874
+ const hasPrompt = Boolean(selectedItem && typeof selectedItem.prompt === "string" && selectedItem.prompt.trim());
875
+ if (loadHistoryPromptBtn) {
876
+ loadHistoryPromptBtn.disabled = uiBusy || !hasPrompt;
877
+ loadHistoryPromptBtn.textContent = hasPrompt
878
+ ? "Load response prompt into editor"
879
+ : "Response prompt unavailable";
880
+ }
881
+ }
882
+
883
+ function applySelectedHistoryItem() {
884
+ const item = getSelectedHistoryItem();
885
+ if (!item) {
886
+ clearActiveResponseView();
887
+ return false;
888
+ }
889
+ handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking);
890
+ return true;
891
+ }
892
+
893
+ function selectHistoryIndex(index, options) {
894
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
895
+ if (total === 0) {
896
+ responseHistoryIndex = -1;
897
+ clearActiveResponseView();
898
+ updateHistoryControls();
899
+ return false;
900
+ }
901
+
902
+ const nextIndex = Math.max(0, Math.min(total - 1, Number(index) || 0));
903
+ responseHistoryIndex = nextIndex;
904
+ const applied = applySelectedHistoryItem();
905
+ updateHistoryControls();
906
+
907
+ if (applied && !(options && options.silent)) {
908
+ const item = getSelectedHistoryItem();
909
+ if (item) {
910
+ const responseLabel = item.kind === "critique" ? "critique" : "response";
911
+ setStatus("Viewing " + responseLabel + " history " + (nextIndex + 1) + "/" + total + ".");
912
+ }
913
+ }
914
+ return applied;
915
+ }
916
+
917
+ function setResponseHistory(items, options) {
918
+ const normalized = Array.isArray(items)
919
+ ? items
920
+ .map((item, index) => normalizeHistoryItem(item, index))
921
+ .filter((item) => item && typeof item === "object")
922
+ : [];
923
+
924
+ const previousItem = getSelectedHistoryItem();
925
+ const previousId = previousItem && typeof previousItem.id === "string" ? previousItem.id : null;
926
+
927
+ responseHistory = normalized;
928
+
929
+ if (!responseHistory.length) {
930
+ responseHistoryIndex = -1;
931
+ clearActiveResponseView();
932
+ updateHistoryControls();
933
+ return false;
934
+ }
935
+
936
+ let targetIndex = responseHistory.length - 1;
937
+ const preserveSelection = Boolean(options && options.preserveSelection);
938
+ const autoSelectLatest = options && Object.prototype.hasOwnProperty.call(options, "autoSelectLatest")
939
+ ? Boolean(options.autoSelectLatest)
940
+ : true;
941
+
942
+ if (preserveSelection && previousId) {
943
+ const preservedIndex = responseHistory.findIndex((item) => item.id === previousId);
944
+ if (preservedIndex >= 0) {
945
+ targetIndex = preservedIndex;
946
+ } else if (!autoSelectLatest && responseHistoryIndex >= 0 && responseHistoryIndex < responseHistory.length) {
947
+ targetIndex = responseHistoryIndex;
948
+ }
949
+ } else if (!autoSelectLatest && responseHistoryIndex >= 0 && responseHistoryIndex < responseHistory.length) {
950
+ targetIndex = responseHistoryIndex;
951
+ }
952
+
953
+ return selectHistoryIndex(targetIndex, { silent: Boolean(options && options.silent) });
954
+ }
955
+
956
+ function updateReferenceBadge() {
957
+ if (!referenceBadgeEl) return;
958
+
959
+ if (rightView === "editor-preview") {
960
+ const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
961
+ if (hasResponse) {
962
+ const time = formatReferenceTime(latestResponseTimestamp);
963
+ const suffix = time ? " · response updated " + time : " · response available";
964
+ referenceBadgeEl.textContent = "Previewing: editor text" + suffix;
965
+ } else {
966
+ referenceBadgeEl.textContent = "Previewing: editor text";
967
+ }
968
+ return;
969
+ }
970
+
971
+ const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
972
+ const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
973
+ if (rightView === "thinking") {
974
+ if (!hasResponse && !hasThinking) {
975
+ referenceBadgeEl.textContent = "Thinking: none";
976
+ return;
977
+ }
978
+
979
+ const time = formatReferenceTime(latestResponseTimestamp);
980
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
981
+ const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
982
+ ? responseHistoryIndex + 1
983
+ : 0;
984
+ const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
985
+ const thinkingLabel = hasThinking ? "assistant thinking" : "assistant thinking unavailable";
986
+ referenceBadgeEl.textContent = time
987
+ ? historyPrefix + thinkingLabel + " · " + time
988
+ : historyPrefix + thinkingLabel;
989
+ return;
990
+ }
991
+
992
+ if (!hasResponse) {
993
+ referenceBadgeEl.textContent = "Latest response: none";
994
+ return;
995
+ }
996
+
997
+ const time = formatReferenceTime(latestResponseTimestamp);
998
+ const responseLabel = latestResponseKind === "critique" ? "assistant critique" : "assistant response";
999
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
1000
+ const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
1001
+ ? responseHistoryIndex + 1
1002
+ : 0;
1003
+ const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
1004
+ referenceBadgeEl.textContent = time
1005
+ ? historyPrefix + responseLabel + " · " + time
1006
+ : historyPrefix + responseLabel;
1007
+ }
1008
+
1009
+ function normalizeForCompare(text) {
1010
+ return String(text || "").replace(/\r\n/g, "\n").trimEnd();
1011
+ }
1012
+
1013
+ function isTextEquivalent(a, b) {
1014
+ return normalizeForCompare(a) === normalizeForCompare(b);
1015
+ }
1016
+
1017
+ function hasAnnotationMarkers(text) {
1018
+ const source = String(text || "");
1019
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1020
+ const hasMarker = ANNOTATION_MARKER_REGEX.test(source);
1021
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1022
+ return hasMarker;
1023
+ }
1024
+
1025
+ function stripAnnotationMarkers(text) {
1026
+ return String(text || "").replace(ANNOTATION_MARKER_REGEX, "");
1027
+ }
1028
+
1029
+ function prepareEditorTextForSend(text) {
1030
+ const raw = String(text || "");
1031
+ return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
1032
+ }
1033
+
1034
+ function prepareEditorTextForPreview(text) {
1035
+ const raw = String(text || "");
1036
+ return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
1037
+ }
1038
+
1039
+ function wrapAsFencedCodeBlock(text, language) {
1040
+ const source = String(text || "").trimEnd();
1041
+ const lang = String(language || "").trim();
1042
+ const backtickFence = "```";
1043
+ const newline = "\n";
1044
+ const marker = source.includes(backtickFence) ? "~~~" : backtickFence;
1045
+ return marker + (lang ? lang : "") + newline + source + newline + marker;
1046
+ }
1047
+
1048
+ function prepareEditorTextForPdfExport(text) {
1049
+ const prepared = prepareEditorTextForPreview(text);
1050
+ const lang = normalizeFenceLanguage(editorLanguage || "");
1051
+ if (lang && lang !== "markdown" && lang !== "latex") {
1052
+ return wrapAsFencedCodeBlock(prepared, lang);
1053
+ }
1054
+ return prepared;
1055
+ }
1056
+
1057
+ function updateSyncBadge(normalizedEditorText) {
1058
+ if (!syncBadgeEl) return;
1059
+
1060
+ const showingThinking = rightView === "thinking";
1061
+ const hasComparableContent = showingThinking
1062
+ ? Boolean(latestResponseThinking && latestResponseThinking.trim())
1063
+ : latestResponseHasContent;
1064
+
1065
+ if (!hasComparableContent) {
1066
+ syncBadgeEl.hidden = true;
1067
+ syncBadgeEl.textContent = showingThinking ? "In sync with thinking" : "In sync with response";
1068
+ syncBadgeEl.classList.remove("sync");
1069
+ return;
1070
+ }
1071
+
1072
+ const normalizedEditor = typeof normalizedEditorText === "string"
1073
+ ? normalizedEditorText
1074
+ : normalizeForCompare(sourceTextEl.value);
1075
+ const targetNormalized = showingThinking ? latestResponseThinkingNormalized : latestResponseNormalized;
1076
+ const inSync = normalizedEditor === targetNormalized;
1077
+ syncBadgeEl.hidden = !inSync;
1078
+ syncBadgeEl.textContent = showingThinking ? "In sync with thinking" : "In sync with response";
1079
+
1080
+ if (inSync) {
1081
+ syncBadgeEl.classList.add("sync");
1082
+ return;
1083
+ }
1084
+
1085
+ syncBadgeEl.classList.remove("sync");
1086
+ }
1087
+
1088
+ function buildPlainMarkdownHtml(markdown) {
1089
+ return "<pre class='plain-markdown'>" + escapeHtml(String(markdown || "")) + "</pre>";
1090
+ }
1091
+
1092
+ function buildPreviewErrorHtml(message, markdown) {
1093
+ return "<div class='preview-error'>" + escapeHtml(String(message || "Preview rendering failed.")) + "</div>" + buildPlainMarkdownHtml(markdown);
1094
+ }
1095
+
1096
+ function sanitizeRenderedHtml(html, markdown) {
1097
+ const rawHtml = typeof html === "string" ? html : "";
1098
+ const mathAnnotationStripped = rawHtml
1099
+ .replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
1100
+ .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
1101
+
1102
+ if (window.DOMPurify && typeof window.DOMPurify.sanitize === "function") {
1103
+ return window.DOMPurify.sanitize(mathAnnotationStripped, {
1104
+ USE_PROFILES: {
1105
+ html: true,
1106
+ mathMl: true,
1107
+ svg: true,
1108
+ },
1109
+ });
1110
+ }
1111
+ return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
1112
+ }
1113
+
1114
+ function applyAnnotationMarkersToElement(targetEl, mode) {
1115
+ if (!targetEl || mode === "none") return;
1116
+ if (typeof document.createTreeWalker !== "function") return;
1117
+
1118
+ const walker = document.createTreeWalker(targetEl, NodeFilter.SHOW_TEXT);
1119
+ const textNodes = [];
1120
+ let node = walker.nextNode();
1121
+ while (node) {
1122
+ const textNode = node;
1123
+ const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
1124
+ if (value && value.toLowerCase().indexOf("[an:") !== -1) {
1125
+ const parent = textNode.parentElement;
1126
+ const tag = parent && parent.tagName ? parent.tagName.toUpperCase() : "";
1127
+ if (tag !== "CODE" && tag !== "PRE" && tag !== "SCRIPT" && tag !== "STYLE" && tag !== "TEXTAREA") {
1128
+ textNodes.push(textNode);
1129
+ }
1130
+ }
1131
+ node = walker.nextNode();
1132
+ }
1133
+
1134
+ for (const textNode of textNodes) {
1135
+ const text = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
1136
+ if (!text) continue;
1137
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1138
+ if (!ANNOTATION_MARKER_REGEX.test(text)) continue;
1139
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1140
+
1141
+ const fragment = document.createDocumentFragment();
1142
+ let lastIndex = 0;
1143
+ let match;
1144
+ while ((match = ANNOTATION_MARKER_REGEX.exec(text)) !== null) {
1145
+ const token = match[0] || "";
1146
+ const start = typeof match.index === "number" ? match.index : 0;
1147
+ if (start > lastIndex) {
1148
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex, start)));
1149
+ }
1150
+
1151
+ if (mode === "highlight") {
1152
+ const markerEl = document.createElement("span");
1153
+ markerEl.className = "annotation-preview-marker";
1154
+ markerEl.textContent = typeof match[1] === "string" ? match[1].trim() : token;
1155
+ markerEl.title = token;
1156
+ fragment.appendChild(markerEl);
1157
+ }
1158
+
1159
+ lastIndex = start + token.length;
1160
+ if (token.length === 0) {
1161
+ ANNOTATION_MARKER_REGEX.lastIndex += 1;
1162
+ }
1163
+ }
1164
+
1165
+ if (lastIndex < text.length) {
1166
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
1167
+ }
1168
+
1169
+ if (textNode.parentNode) {
1170
+ textNode.parentNode.replaceChild(fragment, textNode);
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ function appendMermaidNotice(targetEl, message) {
1176
+ if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
1177
+ return;
1178
+ }
1179
+
1180
+ if (targetEl.querySelector(".preview-mermaid-warning")) {
1181
+ return;
1182
+ }
1183
+
1184
+ const warningEl = document.createElement("div");
1185
+ warningEl.className = "preview-warning preview-mermaid-warning";
1186
+ warningEl.textContent = String(message || MERMAID_RENDER_FAIL_MESSAGE);
1187
+ targetEl.appendChild(warningEl);
1188
+ }
1189
+
1190
+ function appendPreviewNotice(targetEl, message) {
1191
+ if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") return;
1192
+ if (targetEl.querySelector(".preview-image-warning")) return;
1193
+ const el = document.createElement("div");
1194
+ el.className = "preview-warning preview-image-warning";
1195
+ el.textContent = String(message || "");
1196
+ targetEl.appendChild(el);
1197
+ }
1198
+
1199
+ function hasMeaningfulPreviewContent(targetEl) {
1200
+ if (!targetEl || typeof targetEl.querySelector !== "function") return false;
1201
+ if (targetEl.querySelector(".preview-loading")) return false;
1202
+ const text = typeof targetEl.textContent === "string" ? targetEl.textContent.trim() : "";
1203
+ return text.length > 0;
1204
+ }
1205
+
1206
+ function beginPreviewRender(targetEl) {
1207
+ if (!targetEl || !targetEl.classList) return;
1208
+
1209
+ const pendingTimer = previewPendingTimers.get(targetEl);
1210
+ if (pendingTimer !== undefined) {
1211
+ window.clearTimeout(pendingTimer);
1212
+ previewPendingTimers.delete(targetEl);
1213
+ }
1214
+
1215
+ if (hasMeaningfulPreviewContent(targetEl)) {
1216
+ targetEl.classList.remove("preview-pending");
1217
+ const timerId = window.setTimeout(() => {
1218
+ previewPendingTimers.delete(targetEl);
1219
+ if (!targetEl || !targetEl.classList) return;
1220
+ if (!hasMeaningfulPreviewContent(targetEl)) return;
1221
+ targetEl.classList.add("preview-pending");
1222
+ }, PREVIEW_PENDING_BADGE_DELAY_MS);
1223
+ previewPendingTimers.set(targetEl, timerId);
1224
+ return;
1225
+ }
1226
+
1227
+ targetEl.classList.remove("preview-pending");
1228
+ targetEl.innerHTML = "<div class='preview-loading'>Rendering preview…</div>";
1229
+ }
1230
+
1231
+ function finishPreviewRender(targetEl) {
1232
+ if (!targetEl || !targetEl.classList) return;
1233
+ const pendingTimer = previewPendingTimers.get(targetEl);
1234
+ if (pendingTimer !== undefined) {
1235
+ window.clearTimeout(pendingTimer);
1236
+ previewPendingTimers.delete(targetEl);
1237
+ }
1238
+ targetEl.classList.remove("preview-pending");
1239
+ }
1240
+
1241
+ async function getMermaidApi() {
1242
+ if (mermaidModulePromise) {
1243
+ return mermaidModulePromise;
1244
+ }
1245
+
1246
+ mermaidModulePromise = import(MERMAID_CDN_URL)
1247
+ .then((module) => {
1248
+ const mermaidApi = module && module.default ? module.default : null;
1249
+ if (!mermaidApi) {
1250
+ throw new Error("Mermaid module did not expose a default export.");
1251
+ }
1252
+
1253
+ if (!mermaidInitialized) {
1254
+ mermaidApi.initialize(MERMAID_CONFIG);
1255
+ mermaidInitialized = true;
1256
+ }
1257
+
1258
+ return mermaidApi;
1259
+ })
1260
+ .catch((error) => {
1261
+ mermaidModulePromise = null;
1262
+ throw error;
1263
+ });
1264
+
1265
+ return mermaidModulePromise;
1266
+ }
1267
+
1268
+ async function renderMermaidInElement(targetEl) {
1269
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
1270
+
1271
+ const mermaidBlocks = targetEl.querySelectorAll("pre.mermaid");
1272
+ if (!mermaidBlocks || mermaidBlocks.length === 0) return;
1273
+
1274
+ let mermaidApi;
1275
+ try {
1276
+ mermaidApi = await getMermaidApi();
1277
+ } catch (error) {
1278
+ console.error("Mermaid module load failed:", error);
1279
+ appendMermaidNotice(targetEl, MERMAID_UNAVAILABLE_MESSAGE);
1280
+ return;
1281
+ }
1282
+
1283
+ mermaidBlocks.forEach((preEl) => {
1284
+ const codeEl = preEl.querySelector("code");
1285
+ const source = codeEl ? codeEl.textContent : preEl.textContent;
1286
+
1287
+ const wrapper = document.createElement("div");
1288
+ wrapper.className = "mermaid-container";
1289
+
1290
+ const diagramEl = document.createElement("div");
1291
+ diagramEl.className = "mermaid";
1292
+ diagramEl.textContent = source || "";
1293
+
1294
+ wrapper.appendChild(diagramEl);
1295
+ preEl.replaceWith(wrapper);
1296
+ });
1297
+
1298
+ const diagramNodes = Array.from(targetEl.querySelectorAll(".mermaid"));
1299
+ if (diagramNodes.length === 0) return;
1300
+
1301
+ try {
1302
+ await mermaidApi.run({ nodes: diagramNodes });
1303
+ } catch (error) {
1304
+ try {
1305
+ await mermaidApi.run();
1306
+ } catch (fallbackError) {
1307
+ console.error("Mermaid render failed:", fallbackError || error);
1308
+ appendMermaidNotice(targetEl, MERMAID_RENDER_FAIL_MESSAGE);
1309
+ }
1310
+ }
1311
+ }
1312
+
1313
+ async function renderMarkdownWithPandoc(markdown) {
1314
+ const token = getToken();
1315
+ if (!token) {
1316
+ throw new Error("Missing Studio token in URL.");
1317
+ }
1318
+
1319
+ if (typeof fetch !== "function") {
1320
+ throw new Error("Browser fetch API is unavailable.");
1321
+ }
1322
+
1323
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
1324
+ const timeoutId = controller ? window.setTimeout(() => controller.abort(), 8000) : null;
1325
+
1326
+ let response;
1327
+ try {
1328
+ response = await fetch("/render-preview?token=" + encodeURIComponent(token), {
1329
+ method: "POST",
1330
+ headers: {
1331
+ "Content-Type": "application/json",
1332
+ },
1333
+ body: JSON.stringify({
1334
+ markdown: String(markdown || ""),
1335
+ sourcePath: sourceState.path || "",
1336
+ resourceDir: (!sourceState.path && resourceDirInput) ? resourceDirInput.value.trim() : "",
1337
+ }),
1338
+ signal: controller ? controller.signal : undefined,
1339
+ });
1340
+ } catch (error) {
1341
+ if (error && error.name === "AbortError") {
1342
+ throw new Error("Preview request timed out.");
1343
+ }
1344
+ throw error;
1345
+ } finally {
1346
+ if (timeoutId) {
1347
+ window.clearTimeout(timeoutId);
1348
+ }
1349
+ }
1350
+
1351
+ const rawBody = await response.text();
1352
+ let payload = null;
1353
+ try {
1354
+ payload = rawBody ? JSON.parse(rawBody) : null;
1355
+ } catch {
1356
+ payload = null;
1357
+ }
1358
+
1359
+ if (!response.ok) {
1360
+ const message = payload && typeof payload.error === "string"
1361
+ ? payload.error
1362
+ : "Preview request failed with HTTP " + response.status + ".";
1363
+ throw new Error(message);
1364
+ }
1365
+
1366
+ if (!payload || payload.ok !== true || typeof payload.html !== "string") {
1367
+ const message = payload && typeof payload.error === "string"
1368
+ ? payload.error
1369
+ : "Preview renderer returned an invalid payload.";
1370
+ throw new Error(message);
1371
+ }
1372
+
1373
+ return payload.html;
1374
+ }
1375
+
1376
+ function parseContentDispositionFilename(headerValue) {
1377
+ if (!headerValue || typeof headerValue !== "string") return "";
1378
+
1379
+ const utfMatch = headerValue.match(/filename\*=UTF-8''([^;]+)/i);
1380
+ if (utfMatch && utfMatch[1]) {
1381
+ try {
1382
+ return decodeURIComponent(utfMatch[1].trim());
1383
+ } catch {
1384
+ return utfMatch[1].trim();
1385
+ }
1386
+ }
1387
+
1388
+ const quotedMatch = headerValue.match(/filename="([^"]+)"/i);
1389
+ if (quotedMatch && quotedMatch[1]) return quotedMatch[1].trim();
1390
+
1391
+ const plainMatch = headerValue.match(/filename=([^;]+)/i);
1392
+ if (plainMatch && plainMatch[1]) return plainMatch[1].trim();
1393
+
1394
+ return "";
1395
+ }
1396
+
1397
+ async function exportRightPanePdf() {
1398
+ if (uiBusy || pdfExportInProgress) {
1399
+ setStatus("Studio is busy.", "warning");
1400
+ return;
1401
+ }
1402
+
1403
+ const token = getToken();
1404
+ if (!token) {
1405
+ setStatus("Missing Studio token in URL. Re-run /studio.", "error");
1406
+ return;
1407
+ }
1408
+
1409
+ const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
1410
+ if (!rightPaneShowsPreview) {
1411
+ setStatus("Switch right pane to Response (Preview) or Editor (Preview) to export PDF.", "warning");
1412
+ return;
1413
+ }
1414
+
1415
+ const markdown = rightView === "editor-preview" ? prepareEditorTextForPdfExport(sourceTextEl.value) : latestResponseMarkdown;
1416
+ if (!markdown || !markdown.trim()) {
1417
+ setStatus("Nothing to export yet.", "warning");
1418
+ return;
1419
+ }
1420
+
1421
+ const sourcePath = sourceState.path || "";
1422
+ const resourceDir = (!sourceState.path && resourceDirInput) ? resourceDirInput.value.trim() : "";
1423
+ const isEditorPreview = rightView === "editor-preview";
1424
+ const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
1425
+ const isLatex = isEditorPreview
1426
+ ? editorPdfLanguage === "latex"
1427
+ : /\\documentclass\b|\\begin\{document\}/.test(markdown);
1428
+ let filenameHint = isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf";
1429
+ if (sourceState.path) {
1430
+ const baseName = sourceState.path.split(/[\\/]/).pop() || "studio";
1431
+ const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
1432
+ filenameHint = stem + "-preview.pdf";
1433
+ }
1434
+
1435
+ pdfExportInProgress = true;
1436
+ updateResultActionButtons();
1437
+ setStatus("Exporting PDF…", "warning");
1438
+
1439
+ try {
1440
+ const response = await fetch("/export-pdf?token=" + encodeURIComponent(token), {
1441
+ method: "POST",
1442
+ headers: {
1443
+ "Content-Type": "application/json",
1444
+ },
1445
+ body: JSON.stringify({
1446
+ markdown: String(markdown || ""),
1447
+ sourcePath: sourcePath,
1448
+ resourceDir: resourceDir,
1449
+ isLatex: isLatex,
1450
+ editorPdfLanguage: editorPdfLanguage,
1451
+ filenameHint: filenameHint,
1452
+ }),
1453
+ });
1454
+
1455
+ if (!response.ok) {
1456
+ const contentType = String(response.headers.get("content-type") || "").toLowerCase();
1457
+ let message = "PDF export failed with HTTP " + response.status + ".";
1458
+ if (contentType.includes("application/json")) {
1459
+ const payload = await response.json().catch(() => null);
1460
+ if (payload && typeof payload.error === "string") {
1461
+ message = payload.error;
1462
+ }
1463
+ } else {
1464
+ const text = await response.text().catch(() => "");
1465
+ if (text && text.trim()) {
1466
+ message = text.trim();
1467
+ }
1468
+ }
1469
+ throw new Error(message);
1470
+ }
1471
+
1472
+ const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
1473
+ const blob = await response.blob();
1474
+ const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
1475
+ let downloadName = headerFilename || filenameHint || "studio-preview.pdf";
1476
+ if (!/\.pdf$/i.test(downloadName)) {
1477
+ downloadName += ".pdf";
1478
+ }
1479
+
1480
+ const blobUrl = URL.createObjectURL(blob);
1481
+ const link = document.createElement("a");
1482
+ link.href = blobUrl;
1483
+ link.download = downloadName;
1484
+ link.rel = "noopener";
1485
+ document.body.appendChild(link);
1486
+ link.click();
1487
+ link.remove();
1488
+ window.setTimeout(() => {
1489
+ URL.revokeObjectURL(blobUrl);
1490
+ }, 1800);
1491
+
1492
+ if (exportWarning) {
1493
+ setStatus("Exported PDF with warning: " + exportWarning, "warning");
1494
+ } else {
1495
+ setStatus("Exported PDF: " + downloadName, "success");
1496
+ }
1497
+ } catch (error) {
1498
+ const detail = error && error.message ? error.message : String(error || "unknown error");
1499
+ setStatus("PDF export failed: " + detail, "error");
1500
+ } finally {
1501
+ pdfExportInProgress = false;
1502
+ updateResultActionButtons();
1503
+ }
1504
+ }
1505
+
1506
+ async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
1507
+ try {
1508
+ const renderedHtml = await renderMarkdownWithPandoc(markdown);
1509
+
1510
+ if (pane === "source") {
1511
+ if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
1512
+ } else {
1513
+ if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
1514
+ }
1515
+
1516
+ finishPreviewRender(targetEl);
1517
+ targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
1518
+ const annotationMode = (pane === "source" || pane === "response")
1519
+ ? (annotationsEnabled ? "highlight" : "hide")
1520
+ : "none";
1521
+ applyAnnotationMarkersToElement(targetEl, annotationMode);
1522
+ await renderMermaidInElement(targetEl);
1523
+
1524
+ // Warn if relative images are present but unlikely to resolve (non-file-backed content)
1525
+ if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
1526
+ var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
1527
+ var hasLatexImages = /\\includegraphics/.test(markdown || "");
1528
+ if (hasRelativeImages || hasLatexImages) {
1529
+ appendPreviewNotice(targetEl, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
1530
+ }
1531
+ }
1532
+ } catch (error) {
1533
+ if (pane === "source") {
1534
+ if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
1535
+ } else {
1536
+ if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
1537
+ }
1538
+
1539
+ const detail = error && error.message ? error.message : String(error || "unknown error");
1540
+ finishPreviewRender(targetEl);
1541
+ targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
1542
+ }
1543
+ }
1544
+
1545
+ function renderSourcePreviewNow() {
1546
+ if (editorView !== "preview") return;
1547
+ const text = prepareEditorTextForPreview(sourceTextEl.value || "");
1548
+ if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
1549
+ finishPreviewRender(sourcePreviewEl);
1550
+ sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(text, editorLanguage, "preview") + "</div>";
1551
+ return;
1552
+ }
1553
+ const nonce = ++sourcePreviewRenderNonce;
1554
+ beginPreviewRender(sourcePreviewEl);
1555
+ void applyRenderedMarkdown(sourcePreviewEl, text, "source", nonce);
1556
+ }
1557
+
1558
+ function scheduleSourcePreviewRender(delayMs) {
1559
+ if (sourcePreviewRenderTimer) {
1560
+ window.clearTimeout(sourcePreviewRenderTimer);
1561
+ sourcePreviewRenderTimer = null;
1562
+ }
1563
+
1564
+ if (editorView !== "preview") return;
1565
+
1566
+ const delay = typeof delayMs === "number" ? Math.max(0, delayMs) : 180;
1567
+ sourcePreviewRenderTimer = window.setTimeout(() => {
1568
+ sourcePreviewRenderTimer = null;
1569
+ renderSourcePreviewNow();
1570
+ }, delay);
1571
+ }
1572
+
1573
+ function renderSourcePreview(options) {
1574
+ const previewDelayMs =
1575
+ options && typeof options.previewDelayMs === "number"
1576
+ ? Math.max(0, options.previewDelayMs)
1577
+ : 0;
1578
+
1579
+ if (editorView === "preview") {
1580
+ scheduleSourcePreviewRender(previewDelayMs);
1581
+ }
1582
+ if (editorHighlightEnabled && editorView === "markdown") {
1583
+ scheduleEditorHighlightRender();
1584
+ }
1585
+ if (rightView === "editor-preview") {
1586
+ scheduleResponseEditorPreviewRender(previewDelayMs);
1587
+ }
1588
+ }
1589
+
1590
+ function scheduleResponseEditorPreviewRender(delayMs) {
1591
+ if (responseEditorPreviewTimer) {
1592
+ window.clearTimeout(responseEditorPreviewTimer);
1593
+ responseEditorPreviewTimer = null;
1594
+ }
1595
+
1596
+ if (rightView !== "editor-preview") return;
1597
+
1598
+ const delay = typeof delayMs === "number" ? Math.max(0, delayMs) : 180;
1599
+ responseEditorPreviewTimer = window.setTimeout(() => {
1600
+ responseEditorPreviewTimer = null;
1601
+ renderActiveResult();
1602
+ }, delay);
1603
+ }
1604
+
1605
+ function renderActiveResult() {
1606
+ if (rightView === "editor-preview") {
1607
+ const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
1608
+ if (!editorText.trim()) {
1609
+ finishPreviewRender(critiqueViewEl);
1610
+ critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
1611
+ return;
1612
+ }
1613
+ if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
1614
+ finishPreviewRender(critiqueViewEl);
1615
+ critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
1616
+ return;
1617
+ }
1618
+ const nonce = ++responsePreviewRenderNonce;
1619
+ beginPreviewRender(critiqueViewEl);
1620
+ void applyRenderedMarkdown(critiqueViewEl, editorText, "response", nonce);
1621
+ return;
1622
+ }
1623
+
1624
+ if (rightView === "thinking") {
1625
+ const thinking = latestResponseThinking;
1626
+ finishPreviewRender(critiqueViewEl);
1627
+ critiqueViewEl.innerHTML = thinking && thinking.trim()
1628
+ ? buildPlainMarkdownHtml(thinking)
1629
+ : "<pre class='plain-markdown'>No thinking available for this response.</pre>";
1630
+ return;
1631
+ }
1632
+
1633
+ const markdown = latestResponseMarkdown;
1634
+ if (!markdown || !markdown.trim()) {
1635
+ finishPreviewRender(critiqueViewEl);
1636
+ critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
1637
+ return;
1638
+ }
1639
+
1640
+ if (rightView === "preview") {
1641
+ const nonce = ++responsePreviewRenderNonce;
1642
+ beginPreviewRender(critiqueViewEl);
1643
+ void applyRenderedMarkdown(critiqueViewEl, markdown, "response", nonce);
1644
+ return;
1645
+ }
1646
+
1647
+ if (responseHighlightEnabled) {
1648
+ if (markdown.length > RESPONSE_HIGHLIGHT_MAX_CHARS) {
1649
+ finishPreviewRender(critiqueViewEl);
1650
+ critiqueViewEl.innerHTML = buildPreviewErrorHtml(
1651
+ "Response is too large for markdown highlighting. Showing plain markdown.",
1652
+ markdown,
1653
+ );
1654
+ return;
1655
+ }
1656
+
1657
+ finishPreviewRender(critiqueViewEl);
1658
+ critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
1659
+ return;
1660
+ }
1661
+
1662
+ finishPreviewRender(critiqueViewEl);
1663
+ critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
1664
+ }
1665
+
1666
+ function updateResultActionButtons(normalizedEditorText) {
1667
+ const hasResponse = latestResponseHasContent;
1668
+ const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
1669
+ const normalizedEditor = typeof normalizedEditorText === "string"
1670
+ ? normalizedEditorText
1671
+ : normalizeForCompare(sourceTextEl.value);
1672
+ const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
1673
+ const thinkingLoaded = hasThinking && normalizedEditor === latestResponseThinkingNormalized;
1674
+ const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
1675
+ const showingThinking = rightView === "thinking";
1676
+
1677
+ const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
1678
+ const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
1679
+
1680
+ if (showingThinking) {
1681
+ loadResponseBtn.hidden = false;
1682
+ loadCritiqueNotesBtn.hidden = true;
1683
+ loadCritiqueFullBtn.hidden = true;
1684
+
1685
+ loadResponseBtn.disabled = uiBusy || !hasThinking || thinkingLoaded;
1686
+ loadResponseBtn.textContent = !hasThinking
1687
+ ? "Thinking unavailable"
1688
+ : (thinkingLoaded ? "Thinking already in editor" : "Load thinking into editor");
1689
+
1690
+ copyResponseBtn.disabled = uiBusy || !hasThinking;
1691
+ copyResponseBtn.textContent = "Copy thinking text";
1692
+ } else {
1693
+ loadResponseBtn.hidden = isCritiqueResponse;
1694
+ loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
1695
+ loadCritiqueFullBtn.hidden = !isCritiqueResponse;
1696
+
1697
+ loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
1698
+ loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
1699
+
1700
+ loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
1701
+ loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
1702
+
1703
+ loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
1704
+ loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
1705
+
1706
+ copyResponseBtn.disabled = uiBusy || !hasResponse;
1707
+ copyResponseBtn.textContent = "Copy response text";
1708
+ }
1709
+
1710
+ const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
1711
+ const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
1712
+ const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
1713
+ if (exportPdfBtn) {
1714
+ exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
1715
+ if (rightView === "thinking") {
1716
+ exportPdfBtn.title = "Thinking view does not support PDF export yet.";
1717
+ } else if (rightView === "markdown") {
1718
+ exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
1719
+ } else if (!canExportPdf) {
1720
+ exportPdfBtn.title = "Nothing to export yet.";
1721
+ } else {
1722
+ exportPdfBtn.title = "Export the current right-pane preview as PDF via pandoc + xelatex.";
1723
+ }
1724
+ }
1725
+
1726
+ pullLatestBtn.disabled = uiBusy || followLatest;
1727
+ pullLatestBtn.textContent = queuedLatestResponse ? "Fetch latest response *" : "Fetch latest response";
1728
+
1729
+ updateSyncBadge(normalizedEditor);
1730
+ }
1731
+
1732
+ function refreshResponseUi() {
1733
+ updateSourceBadge();
1734
+ updateReferenceBadge();
1735
+ renderActiveResult();
1736
+ updateHistoryControls();
1737
+ updateResultActionButtons();
1738
+ }
1739
+
1740
+ function getEffectiveSavePath() {
1741
+ // File-backed: use the original path
1742
+ if (sourceState.source === "file" && sourceState.path) return sourceState.path;
1743
+ // Upload with working dir + filename: derive path
1744
+ if (sourceState.source === "upload" && sourceState.label && resourceDirInput && resourceDirInput.value.trim()) {
1745
+ var name = sourceState.label.replace(/^upload:\s*/i, "");
1746
+ if (name) return resourceDirInput.value.trim().replace(/\/$/, "") + "/" + name;
1747
+ }
1748
+ return null;
1749
+ }
1750
+
1751
+ function buildAnnotatedSaveSuggestion() {
1752
+ const effectivePath = getEffectiveSavePath() || sourceState.path || "";
1753
+ if (effectivePath) {
1754
+ const parts = String(effectivePath).split(/[/\\]/);
1755
+ const fileName = parts.pop() || "draft.md";
1756
+ const dir = parts.length > 0 ? parts.join("/") + "/" : "";
1757
+ const stem = fileName.replace(/\.[^.]+$/, "") || "draft";
1758
+ return dir + stem + ".annotated.md";
1759
+ }
1760
+
1761
+ const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
1762
+ const stem = rawLabel.replace(/\.[^.]+$/, "") || "draft";
1763
+ const suggestedDir = resourceDirInput && resourceDirInput.value.trim()
1764
+ ? resourceDirInput.value.trim().replace(/\/$/, "") + "/"
1765
+ : "./";
1766
+ return suggestedDir + stem + ".annotated.md";
1767
+ }
1768
+
1769
+ function updateSaveFileTooltip() {
1770
+ if (!saveOverBtn) return;
1771
+
1772
+ var effectivePath = getEffectiveSavePath();
1773
+ if (effectivePath) {
1774
+ saveOverBtn.title = "Overwrite file: " + effectivePath;
1775
+ return;
1776
+ }
1777
+
1778
+ saveOverBtn.title = "Save editor is available after opening a file, setting a working dir, or using Save editor as…";
1779
+ }
1780
+
1781
+ function syncActionButtons() {
1782
+ const canSaveOver = Boolean(getEffectiveSavePath());
1783
+
1784
+ fileInput.disabled = uiBusy;
1785
+ saveAsBtn.disabled = uiBusy;
1786
+ saveOverBtn.disabled = uiBusy || !canSaveOver;
1787
+ sendEditorBtn.disabled = uiBusy;
1788
+ if (getEditorBtn) getEditorBtn.disabled = uiBusy;
1789
+ if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
1790
+ syncRunAndCritiqueButtons();
1791
+ copyDraftBtn.disabled = uiBusy;
1792
+ if (highlightSelect) highlightSelect.disabled = uiBusy;
1793
+ if (langSelect) langSelect.disabled = uiBusy;
1794
+ if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
1795
+ if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
1796
+ if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
1797
+ if (compactBtn) compactBtn.disabled = uiBusy || compactInProgress || wsState === "Disconnected";
1798
+ editorViewSelect.disabled = false;
1799
+ rightViewSelect.disabled = false;
1800
+ followSelect.disabled = uiBusy;
1801
+ if (responseHighlightSelect) responseHighlightSelect.disabled = rightView !== "markdown";
1802
+ insertHeaderBtn.disabled = uiBusy;
1803
+ lensSelect.disabled = uiBusy;
1804
+ updateSaveFileTooltip();
1805
+ updateHistoryControls();
1806
+ updateResultActionButtons();
1807
+ }
1808
+
1809
+ function setBusy(busy) {
1810
+ uiBusy = Boolean(busy);
1811
+ syncFooterSpinnerState();
1812
+ renderStatus();
1813
+ syncActionButtons();
1814
+ }
1815
+
1816
+ function setSourceState(next) {
1817
+ sourceState = {
1818
+ source: next && next.source ? next.source : "blank",
1819
+ label: next && next.label ? next.label : "blank",
1820
+ path: next && next.path ? next.path : null,
1821
+ };
1822
+ updateSourceBadge();
1823
+ syncActionButtons();
1824
+ }
1825
+
1826
+ function setEditorText(nextText, options) {
1827
+ const value = String(nextText || "");
1828
+ const preserveScroll = Boolean(options && options.preserveScroll);
1829
+ const preserveSelection = Boolean(options && options.preserveSelection);
1830
+ const previousScrollTop = sourceTextEl.scrollTop;
1831
+ const previousScrollLeft = sourceTextEl.scrollLeft;
1832
+ const previousSelectionStart = sourceTextEl.selectionStart;
1833
+ const previousSelectionEnd = sourceTextEl.selectionEnd;
1834
+
1835
+ sourceTextEl.value = value;
1836
+
1837
+ if (preserveSelection) {
1838
+ const maxIndex = value.length;
1839
+ const start = Math.max(0, Math.min(previousSelectionStart || 0, maxIndex));
1840
+ const end = Math.max(start, Math.min(previousSelectionEnd || start, maxIndex));
1841
+ sourceTextEl.setSelectionRange(start, end);
1842
+ }
1843
+
1844
+ if (preserveScroll) {
1845
+ sourceTextEl.scrollTop = previousScrollTop;
1846
+ sourceTextEl.scrollLeft = previousScrollLeft;
1847
+ }
1848
+
1849
+ syncEditorHighlightScroll();
1850
+ const schedule = typeof window.requestAnimationFrame === "function"
1851
+ ? window.requestAnimationFrame.bind(window)
1852
+ : (cb) => window.setTimeout(cb, 16);
1853
+ schedule(() => {
1854
+ syncEditorHighlightScroll();
1855
+ });
1856
+
1857
+ updateAnnotatedReplyHeaderButton();
1858
+
1859
+ if (!options || options.updatePreview !== false) {
1860
+ renderSourcePreview();
1861
+ }
1862
+ if (!options || options.updateMeta !== false) {
1863
+ scheduleEditorMetaUpdate();
1864
+ }
1865
+ }
1866
+
1867
+ function setEditorView(nextView) {
1868
+ editorView = nextView === "preview" ? "preview" : "markdown";
1869
+ editorViewSelect.value = editorView;
1870
+
1871
+ const showPreview = editorView === "preview";
1872
+ if (sourceEditorWrapEl) {
1873
+ sourceEditorWrapEl.style.display = showPreview ? "none" : "flex";
1874
+ }
1875
+ sourcePreviewEl.hidden = !showPreview;
1876
+
1877
+ if (!showPreview && sourcePreviewRenderTimer) {
1878
+ window.clearTimeout(sourcePreviewRenderTimer);
1879
+ sourcePreviewRenderTimer = null;
1880
+ }
1881
+
1882
+ if (!showPreview) {
1883
+ finishPreviewRender(sourcePreviewEl);
1884
+ }
1885
+
1886
+ if (showPreview) {
1887
+ renderSourcePreview();
1888
+ }
1889
+
1890
+ updateEditorHighlightState();
1891
+ updateLangSelectVisibility();
1892
+ }
1893
+
1894
+ function setRightView(nextView) {
1895
+ rightView = nextView === "preview"
1896
+ ? "preview"
1897
+ : (nextView === "editor-preview"
1898
+ ? "editor-preview"
1899
+ : (nextView === "thinking" ? "thinking" : "markdown"));
1900
+ rightViewSelect.value = rightView;
1901
+
1902
+ if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
1903
+ window.clearTimeout(responseEditorPreviewTimer);
1904
+ responseEditorPreviewTimer = null;
1905
+ }
1906
+
1907
+ refreshResponseUi();
1908
+ syncActionButtons();
1909
+ }
1910
+
1911
+ function getToken() {
1912
+ const query = new URLSearchParams(window.location.search || "");
1913
+ const hash = new URLSearchParams((window.location.hash || "").replace(/^#/, ""));
1914
+ return query.get("token") || hash.get("token") || "";
1915
+ }
1916
+
1917
+ function makeRequestId() {
1918
+ if (window.crypto && typeof window.crypto.randomUUID === "function") {
1919
+ return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
1920
+ }
1921
+ return "req_" + Date.now() + "_" + Math.random().toString(36).slice(2, 10);
1922
+ }
1923
+
1924
+ function escapeHtml(text) {
1925
+ return text
1926
+ .replace(/&/g, "&amp;")
1927
+ .replace(/</g, "&lt;")
1928
+ .replace(/>/g, "&gt;")
1929
+ .replace(/"/g, "&quot;")
1930
+ .replace(/'/g, "&#39;");
1931
+ }
1932
+
1933
+ function wrapHighlight(className, text) {
1934
+ return "<span class='" + className + "'>" + escapeHtml(String(text || "")) + "</span>";
1935
+ }
1936
+
1937
+ function wrapHighlightWithTitle(className, text, title) {
1938
+ const titleAttr = title ? " title='" + escapeHtml(String(title)) + "'" : "";
1939
+ return "<span class='" + className + "'" + titleAttr + ">" + escapeHtml(String(text || "")) + "</span>";
1940
+ }
1941
+
1942
+ function highlightInlineAnnotations(text, mode) {
1943
+ const source = String(text || "");
1944
+ const renderMode = mode === "preview" ? "preview" : "overlay";
1945
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1946
+ let lastIndex = 0;
1947
+ let out = "";
1948
+
1949
+ let match;
1950
+ while ((match = ANNOTATION_MARKER_REGEX.exec(source)) !== null) {
1951
+ const token = match[0] || "";
1952
+ const start = typeof match.index === "number" ? match.index : 0;
1953
+ const markerText = typeof match[1] === "string" ? match[1].trim() : token;
1954
+
1955
+ if (start > lastIndex) {
1956
+ out += escapeHtml(source.slice(lastIndex, start));
1957
+ }
1958
+
1959
+ if (renderMode === "preview") {
1960
+ out += wrapHighlightWithTitle("annotation-preview-marker", markerText || token, token);
1961
+ } else {
1962
+ out += wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
1963
+ }
1964
+ lastIndex = start + token.length;
1965
+ if (token.length === 0) {
1966
+ ANNOTATION_MARKER_REGEX.lastIndex += 1;
1967
+ }
1968
+ }
1969
+
1970
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1971
+ if (lastIndex < source.length) {
1972
+ out += escapeHtml(source.slice(lastIndex));
1973
+ }
1974
+
1975
+ return out;
1976
+ }
1977
+
1978
+ function highlightInlineMarkdown(text) {
1979
+ const source = String(text || "");
1980
+ const pattern = /(\x60[^\x60]*\x60)|(\[[^\]]+\]\([^)]+\))|(\[an:\s*[^\]]+\])/gi;
1981
+ let lastIndex = 0;
1982
+ let out = "";
1983
+
1984
+ let match;
1985
+ while ((match = pattern.exec(source)) !== null) {
1986
+ const token = match[0] || "";
1987
+ const start = typeof match.index === "number" ? match.index : 0;
1988
+
1989
+ if (start > lastIndex) {
1990
+ out += escapeHtml(source.slice(lastIndex, start));
1991
+ }
1992
+
1993
+ if (match[1]) {
1994
+ out += wrapHighlight("hl-code", token);
1995
+ } else if (match[2]) {
1996
+ const linkMatch = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
1997
+ if (linkMatch) {
1998
+ out += wrapHighlight("hl-link", "[" + linkMatch[1] + "]");
1999
+ out += "(" + wrapHighlight("hl-url", linkMatch[2]) + ")";
2000
+ } else {
2001
+ out += escapeHtml(token);
2002
+ }
2003
+ } else if (match[3]) {
2004
+ out += highlightInlineAnnotations(token);
2005
+ } else {
2006
+ out += escapeHtml(token);
2007
+ }
2008
+
2009
+ lastIndex = start + token.length;
2010
+ }
2011
+
2012
+ if (lastIndex < source.length) {
2013
+ out += escapeHtml(source.slice(lastIndex));
2014
+ }
2015
+
2016
+ return out;
2017
+ }
2018
+
2019
+ function normalizeFenceLanguage(info) {
2020
+ const raw = String(info || "").trim();
2021
+ if (!raw) return "";
2022
+
2023
+ const first = raw.split(/\s+/)[0].replace(/^\./, "").toLowerCase();
2024
+
2025
+ // Explicit aliases that don't match extension names
2026
+ if (first === "js" || first === "javascript" || first === "jsx" || first === "node") return "javascript";
2027
+ if (first === "ts" || first === "typescript" || first === "tsx") return "typescript";
2028
+ if (first === "py" || first === "python") return "python";
2029
+ if (first === "sh" || first === "bash" || first === "zsh" || first === "shell") return "bash";
2030
+ if (first === "json" || first === "jsonc") return "json";
2031
+ if (first === "rust" || first === "rs") return "rust";
2032
+ if (first === "c" || first === "h") return "c";
2033
+ if (first === "cpp" || first === "c++" || first === "cxx" || first === "hpp") return "cpp";
2034
+ if (first === "julia" || first === "jl") return "julia";
2035
+ if (first === "fortran" || first === "f90" || first === "f95" || first === "f03" || first === "f" || first === "for") return "fortran";
2036
+ if (first === "r") return "r";
2037
+ if (first === "matlab" || first === "m") return "matlab";
2038
+ if (first === "latex" || first === "tex") return "latex";
2039
+ if (first === "diff" || first === "patch" || first === "udiff") return "diff";
2040
+
2041
+ // Fall back to the unified extension->language map
2042
+ return EXT_TO_LANG[first] || "";
2043
+ }
2044
+
2045
+ function highlightCodeTokens(line, pattern, classifyMatch) {
2046
+ const source = String(line || "");
2047
+ let out = "";
2048
+ let lastIndex = 0;
2049
+ pattern.lastIndex = 0;
2050
+
2051
+ let match;
2052
+ while ((match = pattern.exec(source)) !== null) {
2053
+ const token = match[0] || "";
2054
+ const start = typeof match.index === "number" ? match.index : 0;
2055
+
2056
+ if (start > lastIndex) {
2057
+ out += escapeHtml(source.slice(lastIndex, start));
2058
+ }
2059
+
2060
+ const className = classifyMatch(match) || "hl-code";
2061
+ out += wrapHighlight(className, token);
2062
+
2063
+ lastIndex = start + token.length;
2064
+ if (token.length === 0) {
2065
+ pattern.lastIndex += 1;
2066
+ }
2067
+ }
2068
+
2069
+ if (lastIndex < source.length) {
2070
+ out += escapeHtml(source.slice(lastIndex));
2071
+ }
2072
+
2073
+ return out;
2074
+ }
2075
+
2076
+ function highlightCodeLine(line, language, annotationRenderMode) {
2077
+ const source = String(line || "");
2078
+ const lang = normalizeFenceLanguage(language);
2079
+ const renderMode = annotationRenderMode === "preview" ? "preview" : "overlay";
2080
+
2081
+ if (!lang) {
2082
+ return wrapHighlight("hl-code", source);
2083
+ }
2084
+
2085
+ if (lang === "javascript" || lang === "typescript") {
2086
+ const jsPattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:const|let|var|function|return|if|else|for|while|switch|case|break|continue|try|catch|finally|throw|new|class|extends|import|from|export|default|async|await|true|false|null|undefined|typeof|instanceof)\b)|(\b\d+(?:\.\d+)?\b)/g;
2087
+ const highlighted = highlightCodeTokens(source, jsPattern, (match) => {
2088
+ if (match[1]) return "hl-code-com";
2089
+ if (match[2]) return "hl-code-str";
2090
+ if (match[3]) return "hl-code-kw";
2091
+ if (match[4]) return "hl-code-num";
2092
+ return "hl-code";
2093
+ });
2094
+ return "<span class='hl-code'>" + highlighted + "</span>";
2095
+ }
2096
+
2097
+ if (lang === "python") {
2098
+ const pyPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:def|class|return|if|elif|else|for|while|try|except|finally|import|from|as|with|lambda|yield|True|False|None|and|or|not|in|is|pass|break|continue|raise|global|nonlocal|assert)\b)|(\b\d+(?:\.\d+)?\b)/g;
2099
+ const highlighted = highlightCodeTokens(source, pyPattern, (match) => {
2100
+ if (match[1]) return "hl-code-com";
2101
+ if (match[2]) return "hl-code-str";
2102
+ if (match[3]) return "hl-code-kw";
2103
+ if (match[4]) return "hl-code-num";
2104
+ return "hl-code";
2105
+ });
2106
+ return "<span class='hl-code'>" + highlighted + "</span>";
2107
+ }
2108
+
2109
+ if (lang === "bash") {
2110
+ const shPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'[^']*')|(\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*)|(\b(?:if|then|else|fi|for|in|do|done|case|esac|function|local|export|readonly|return|break|continue|while|until)\b)|(\b\d+\b)/g;
2111
+ const highlighted = highlightCodeTokens(source, shPattern, (match) => {
2112
+ if (match[1]) return "hl-code-com";
2113
+ if (match[2]) return "hl-code-str";
2114
+ if (match[3]) return "hl-code-var";
2115
+ if (match[4]) return "hl-code-kw";
2116
+ if (match[5]) return "hl-code-num";
2117
+ return "hl-code";
2118
+ });
2119
+ return "<span class='hl-code'>" + highlighted + "</span>";
2120
+ }
2121
+
2122
+ if (lang === "json") {
2123
+ const jsonPattern = /("(?:[^"\\]|\\.)*"\s*:)|("(?:[^"\\]|\\.)*")|(\b(?:true|false|null)\b)|(\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/g;
2124
+ const highlighted = highlightCodeTokens(source, jsonPattern, (match) => {
2125
+ if (match[1]) return "hl-code-key";
2126
+ if (match[2]) return "hl-code-str";
2127
+ if (match[3]) return "hl-code-kw";
2128
+ if (match[4]) return "hl-code-num";
2129
+ return "hl-code";
2130
+ });
2131
+ return "<span class='hl-code'>" + highlighted + "</span>";
2132
+ }
2133
+
2134
+ if (lang === "rust") {
2135
+ const rustPattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*")|(\b(?:fn|let|mut|const|struct|enum|impl|trait|pub|mod|use|crate|self|super|match|if|else|for|while|loop|return|break|continue|where|as|in|ref|move|async|await|unsafe|extern|type|static|true|false|Some|None|Ok|Err|Self)\b)|(\b\d[\d_]*(?:\.\d[\d_]*)?(?:f32|f64|u8|u16|u32|u64|u128|usize|i8|i16|i32|i64|i128|isize)?\b)/g;
2136
+ const highlighted = highlightCodeTokens(source, rustPattern, (match) => {
2137
+ if (match[1]) return "hl-code-com";
2138
+ if (match[2]) return "hl-code-str";
2139
+ if (match[3]) return "hl-code-kw";
2140
+ if (match[4]) return "hl-code-num";
2141
+ return "hl-code";
2142
+ });
2143
+ return "<span class='hl-code'>" + highlighted + "</span>";
2144
+ }
2145
+
2146
+ if (lang === "c" || lang === "cpp") {
2147
+ const cPattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)')|(#\s*\w+)|(\b(?:if|else|for|while|do|switch|case|break|continue|return|goto|struct|union|enum|typedef|sizeof|void|int|char|short|long|float|double|unsigned|signed|const|static|extern|volatile|register|inline|auto|restrict|true|false|NULL|nullptr|class|public|private|protected|virtual|override|template|typename|namespace|using|new|delete|try|catch|throw|noexcept|constexpr|auto|decltype|static_cast|dynamic_cast|reinterpret_cast|const_cast|std|include|define|ifdef|ifndef|endif|pragma)\b)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[fFlLuU]*\b)/g;
2148
+ const highlighted = highlightCodeTokens(source, cPattern, (match) => {
2149
+ if (match[1]) return "hl-code-com";
2150
+ if (match[2]) return "hl-code-str";
2151
+ if (match[3]) return "hl-code-kw";
2152
+ if (match[4]) return "hl-code-kw";
2153
+ if (match[5]) return "hl-code-num";
2154
+ return "hl-code";
2155
+ });
2156
+ return "<span class='hl-code'>" + highlighted + "</span>";
2157
+ }
2158
+
2159
+ if (lang === "julia") {
2160
+ const jlPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:function|end|if|elseif|else|for|while|begin|let|local|global|const|return|break|continue|do|try|catch|finally|throw|module|import|using|export|struct|mutable|abstract|primitive|where|macro|quote|true|false|nothing|missing|in|isa|typeof)\b)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/g;
2161
+ const highlighted = highlightCodeTokens(source, jlPattern, (match) => {
2162
+ if (match[1]) return "hl-code-com";
2163
+ if (match[2]) return "hl-code-str";
2164
+ if (match[3]) return "hl-code-kw";
2165
+ if (match[4]) return "hl-code-num";
2166
+ return "hl-code";
2167
+ });
2168
+ return "<span class='hl-code'>" + highlighted + "</span>";
2169
+ }
2170
+
2171
+ if (lang === "fortran") {
2172
+ const fPattern = /(!.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:program|end|subroutine|function|module|use|implicit|none|integer|real|double|precision|complex|character|logical|dimension|allocatable|intent|in|out|inout|parameter|data|do|if|then|else|elseif|endif|enddo|call|return|write|read|print|format|stop|contains|type|class|select|case|where|forall|associate|block|procedure|interface|abstract|extends|allocate|deallocate|cycle|exit|go|to|common|equivalence|save|external|intrinsic)\b)|(\b\d+(?:\.\d+)?(?:[dDeE][+-]?\d+)?\b)/gi;
2173
+ const highlighted = highlightCodeTokens(source, fPattern, (match) => {
2174
+ if (match[1]) return "hl-code-com";
2175
+ if (match[2]) return "hl-code-str";
2176
+ if (match[3]) return "hl-code-kw";
2177
+ if (match[4]) return "hl-code-num";
2178
+ return "hl-code";
2179
+ });
2180
+ return "<span class='hl-code'>" + highlighted + "</span>";
2181
+ }
2182
+
2183
+ if (lang === "r") {
2184
+ const rPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:function|if|else|for|while|repeat|in|next|break|return|TRUE|FALSE|NULL|NA|NA_integer_|NA_real_|NA_complex_|NA_character_|Inf|NaN|library|require|source|local|switch)\b)|(<-|->|<<-|->>)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[Li]?\b)/g;
2185
+ const highlighted = highlightCodeTokens(source, rPattern, (match) => {
2186
+ if (match[1]) return "hl-code-com";
2187
+ if (match[2]) return "hl-code-str";
2188
+ if (match[3]) return "hl-code-kw";
2189
+ if (match[4]) return "hl-code-kw";
2190
+ if (match[5]) return "hl-code-num";
2191
+ return "hl-code";
2192
+ });
2193
+ return "<span class='hl-code'>" + highlighted + "</span>";
2194
+ }
2195
+
2196
+ if (lang === "matlab") {
2197
+ const matPattern = /(%.*$)|('(?:[^']|'')*'|"(?:[^"\\]|\\.)*")|(\b(?:function|end|if|elseif|else|for|while|switch|case|otherwise|try|catch|return|break|continue|global|persistent|classdef|properties|methods|events|enumeration|true|false)\b)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[i]?\b)/g;
2198
+ const highlighted = highlightCodeTokens(source, matPattern, (match) => {
2199
+ if (match[1]) return "hl-code-com";
2200
+ if (match[2]) return "hl-code-str";
2201
+ if (match[3]) return "hl-code-kw";
2202
+ if (match[4]) return "hl-code-num";
2203
+ return "hl-code";
2204
+ });
2205
+ return "<span class='hl-code'>" + highlighted + "</span>";
2206
+ }
2207
+
2208
+ if (lang === "latex") {
2209
+ const texPattern = /(%.*$)|(\\(?:documentclass|usepackage|newtheorem|begin|end|section|subsection|subsubsection|chapter|part|title|author|date|maketitle|tableofcontents|includegraphics|caption|label|ref|eqref|cite|textbf|textit|texttt|emph|footnote|centering|newcommand|renewcommand|providecommand|bibliography|bibliographystyle|bibitem|item|input|include)\b)|(\\[A-Za-z]+)|(\{|\})|(\$\$?(?:[^$\\]|\\.)+\$\$?)|(\[(?:.*?)\])/g;
2210
+ const highlighted = highlightCodeTokens(source, texPattern, (match) => {
2211
+ if (match[1]) return "hl-code-com";
2212
+ if (match[2]) return "hl-code-kw";
2213
+ if (match[3]) return "hl-code-fn";
2214
+ if (match[4]) return "hl-code-op";
2215
+ if (match[5]) return "hl-code-str";
2216
+ if (match[6]) return "hl-code-num";
2217
+ return "hl-code";
2218
+ });
2219
+ return highlighted;
2220
+ }
2221
+
2222
+ if (lang === "diff") {
2223
+ var highlightedDiff = highlightInlineAnnotations(source, renderMode);
2224
+ if (/^@@/.test(source)) return "<span class=\"hl-code-fn\">" + highlightedDiff + "</span>";
2225
+ if (/^\+\+\+|^---/.test(source)) return "<span class=\"hl-code-kw\">" + highlightedDiff + "</span>";
2226
+ if (/^\+/.test(source)) return "<span class=\"hl-diff-add\">" + highlightedDiff + "</span>";
2227
+ if (/^-/.test(source)) return "<span class=\"hl-diff-del\">" + highlightedDiff + "</span>";
2228
+ if (/^diff /.test(source)) return "<span class=\"hl-code-kw\">" + highlightedDiff + "</span>";
2229
+ if (/^index /.test(source)) return "<span class=\"hl-code-com\">" + highlightedDiff + "</span>";
2230
+ return highlightedDiff;
2231
+ }
2232
+
2233
+ return wrapHighlight("hl-code", source);
2234
+ }
2235
+
2236
+ function highlightMarkdown(text) {
2237
+ const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
2238
+ const out = [];
2239
+ let inFence = false;
2240
+ let fenceChar = null;
2241
+ let fenceLength = 0;
2242
+ let fenceLanguage = "";
2243
+
2244
+ for (const line of lines) {
2245
+ const fenceMatch = line.match(/^(\s*)([\x60]{3,}|~{3,})(.*)$/);
2246
+ if (fenceMatch) {
2247
+ const marker = fenceMatch[2] || "";
2248
+ const markerChar = marker.charAt(0);
2249
+ const markerLength = marker.length;
2250
+
2251
+ if (!inFence) {
2252
+ inFence = true;
2253
+ fenceChar = markerChar;
2254
+ fenceLength = markerLength;
2255
+ fenceLanguage = normalizeFenceLanguage(fenceMatch[3] || "");
2256
+ } else if (fenceChar === markerChar && markerLength >= fenceLength) {
2257
+ inFence = false;
2258
+ fenceChar = null;
2259
+ fenceLength = 0;
2260
+ fenceLanguage = "";
2261
+ }
2262
+
2263
+ out.push(wrapHighlight("hl-fence", line));
2264
+ continue;
2265
+ }
2266
+
2267
+ if (inFence) {
2268
+ out.push(line.length > 0 ? highlightCodeLine(line, fenceLanguage) : EMPTY_OVERLAY_LINE);
2269
+ continue;
2270
+ }
2271
+
2272
+ if (line.length === 0) {
2273
+ out.push(EMPTY_OVERLAY_LINE);
2274
+ continue;
2275
+ }
2276
+
2277
+ const headingMatch = line.match(/^(\s{0,3})(#{1,6}\s+)(.*)$/);
2278
+ if (headingMatch) {
2279
+ out.push(escapeHtml(headingMatch[1] || "") + wrapHighlight("hl-heading", (headingMatch[2] || "") + (headingMatch[3] || "")));
2280
+ continue;
2281
+ }
2282
+
2283
+ const quoteMatch = line.match(/^(\s{0,3}>\s?)(.*)$/);
2284
+ if (quoteMatch) {
2285
+ out.push(wrapHighlight("hl-quote", quoteMatch[1] || "") + highlightInlineMarkdown(quoteMatch[2] || ""));
2286
+ continue;
2287
+ }
2288
+
2289
+ const listMatch = line.match(/^(\s*)([-*+]|\d+\.)(\s+)(.*)$/);
2290
+ if (listMatch) {
2291
+ out.push(
2292
+ escapeHtml(listMatch[1] || "")
2293
+ + wrapHighlight("hl-list", listMatch[2] || "")
2294
+ + escapeHtml(listMatch[3] || "")
2295
+ + highlightInlineMarkdown(listMatch[4] || ""),
2296
+ );
2297
+ continue;
2298
+ }
2299
+
2300
+ out.push(highlightInlineMarkdown(line));
2301
+ }
2302
+
2303
+ return out.join("<br>");
2304
+ }
2305
+
2306
+ function highlightCode(text, language, annotationRenderMode) {
2307
+ const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
2308
+ const lang = normalizeFenceLanguage(language);
2309
+ const renderMode = annotationRenderMode === "preview" ? "preview" : "overlay";
2310
+ const out = [];
2311
+ for (const line of lines) {
2312
+ if (line.length === 0) {
2313
+ out.push(EMPTY_OVERLAY_LINE);
2314
+ } else if (lang) {
2315
+ out.push(highlightCodeLine(line, lang, renderMode));
2316
+ } else {
2317
+ out.push(escapeHtml(line));
2318
+ }
2319
+ }
2320
+ return out.join("<br>");
2321
+ }
2322
+
2323
+ function detectLanguageFromName(name) {
2324
+ if (!name) return "";
2325
+ var dot = name.lastIndexOf(".");
2326
+ if (dot < 0) return "";
2327
+ var ext = name.slice(dot + 1).toLowerCase();
2328
+ return EXT_TO_LANG[ext] || "";
2329
+ }
2330
+
2331
+ function renderEditorHighlightNow() {
2332
+ if (!sourceHighlightEl) return;
2333
+ if (!editorHighlightEnabled || editorView !== "markdown") {
2334
+ sourceHighlightEl.innerHTML = "";
2335
+ return;
2336
+ }
2337
+
2338
+ const text = sourceTextEl.value || "";
2339
+ if (text.length > EDITOR_HIGHLIGHT_MAX_CHARS) {
2340
+ sourceHighlightEl.textContent = text;
2341
+ syncEditorHighlightScroll();
2342
+ return;
2343
+ }
2344
+
2345
+ if (editorLanguage === "markdown" || !editorLanguage) {
2346
+ sourceHighlightEl.innerHTML = highlightMarkdown(text);
2347
+ } else {
2348
+ sourceHighlightEl.innerHTML = highlightCode(text, editorLanguage);
2349
+ }
2350
+ syncEditorHighlightScroll();
2351
+ }
2352
+
2353
+ function scheduleEditorHighlightRender() {
2354
+ if (editorHighlightRenderRaf !== null) {
2355
+ if (typeof window.cancelAnimationFrame === "function") {
2356
+ window.cancelAnimationFrame(editorHighlightRenderRaf);
2357
+ } else {
2358
+ window.clearTimeout(editorHighlightRenderRaf);
2359
+ }
2360
+ editorHighlightRenderRaf = null;
2361
+ }
2362
+
2363
+ const schedule = typeof window.requestAnimationFrame === "function"
2364
+ ? window.requestAnimationFrame.bind(window)
2365
+ : (cb) => window.setTimeout(cb, 16);
2366
+
2367
+ editorHighlightRenderRaf = schedule(() => {
2368
+ editorHighlightRenderRaf = null;
2369
+ renderEditorHighlightNow();
2370
+ });
2371
+ }
2372
+
2373
+ function syncEditorHighlightScroll() {
2374
+ if (!sourceHighlightEl) return;
2375
+ sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
2376
+ sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
2377
+ }
2378
+
2379
+ function runEditorMetaUpdateNow() {
2380
+ const normalizedEditor = normalizeForCompare(sourceTextEl.value);
2381
+ updateResultActionButtons(normalizedEditor);
2382
+ updateAnnotatedReplyHeaderButton();
2383
+ if (stripAnnotationsBtn) {
2384
+ stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
2385
+ }
2386
+ }
2387
+
2388
+ function scheduleEditorMetaUpdate() {
2389
+ if (editorMetaUpdateRaf !== null) {
2390
+ if (typeof window.cancelAnimationFrame === "function") {
2391
+ window.cancelAnimationFrame(editorMetaUpdateRaf);
2392
+ } else {
2393
+ window.clearTimeout(editorMetaUpdateRaf);
2394
+ }
2395
+ editorMetaUpdateRaf = null;
2396
+ }
2397
+
2398
+ const schedule = typeof window.requestAnimationFrame === "function"
2399
+ ? window.requestAnimationFrame.bind(window)
2400
+ : (cb) => window.setTimeout(cb, 16);
2401
+
2402
+ editorMetaUpdateRaf = schedule(() => {
2403
+ editorMetaUpdateRaf = null;
2404
+ runEditorMetaUpdateNow();
2405
+ });
2406
+ }
2407
+
2408
+ function readStoredToggle(storageKey) {
2409
+ if (!window.localStorage) return null;
2410
+ try {
2411
+ const value = window.localStorage.getItem(storageKey);
2412
+ if (value === "on") return true;
2413
+ if (value === "off") return false;
2414
+ return null;
2415
+ } catch {
2416
+ return null;
2417
+ }
2418
+ }
2419
+
2420
+ function persistStoredToggle(storageKey, enabled) {
2421
+ if (!window.localStorage) return;
2422
+ try {
2423
+ window.localStorage.setItem(storageKey, enabled ? "on" : "off");
2424
+ } catch {
2425
+ // ignore storage failures
2426
+ }
2427
+ }
2428
+
2429
+ function readStoredEditorHighlightEnabled() {
2430
+ return readStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY);
2431
+ }
2432
+
2433
+ function readStoredResponseHighlightEnabled() {
2434
+ return readStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY);
2435
+ }
2436
+
2437
+ function readStoredAnnotationsEnabled() {
2438
+ return readStoredToggle(ANNOTATION_MODE_STORAGE_KEY);
2439
+ }
2440
+
2441
+ function persistEditorHighlightEnabled(enabled) {
2442
+ persistStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled);
2443
+ }
2444
+
2445
+ function persistResponseHighlightEnabled(enabled) {
2446
+ persistStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY, enabled);
2447
+ }
2448
+
2449
+ function persistAnnotationsEnabled(enabled) {
2450
+ persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
2451
+ }
2452
+
2453
+ function updateEditorHighlightState() {
2454
+ const enabled = editorHighlightEnabled && editorView === "markdown";
2455
+
2456
+ sourceTextEl.classList.toggle("highlight-active", enabled);
2457
+
2458
+ if (sourceHighlightEl) {
2459
+ sourceHighlightEl.hidden = !enabled;
2460
+ }
2461
+
2462
+ if (!enabled) {
2463
+ if (editorHighlightRenderRaf !== null) {
2464
+ if (typeof window.cancelAnimationFrame === "function") {
2465
+ window.cancelAnimationFrame(editorHighlightRenderRaf);
2466
+ } else {
2467
+ window.clearTimeout(editorHighlightRenderRaf);
2468
+ }
2469
+ editorHighlightRenderRaf = null;
2470
+ }
2471
+
2472
+ if (sourceHighlightEl) {
2473
+ sourceHighlightEl.innerHTML = "";
2474
+ sourceHighlightEl.scrollTop = 0;
2475
+ sourceHighlightEl.scrollLeft = 0;
2476
+ }
2477
+ return;
2478
+ }
2479
+
2480
+ scheduleEditorHighlightRender();
2481
+ syncEditorHighlightScroll();
2482
+ }
2483
+
2484
+ function setEditorHighlightEnabled(enabled) {
2485
+ editorHighlightEnabled = Boolean(enabled);
2486
+ persistEditorHighlightEnabled(editorHighlightEnabled);
2487
+ if (highlightSelect) {
2488
+ highlightSelect.value = editorHighlightEnabled ? "on" : "off";
2489
+ }
2490
+ updateEditorHighlightState();
2491
+ updateLangSelectVisibility();
2492
+ }
2493
+
2494
+ function readStoredEditorLanguage() {
2495
+ if (!window.localStorage) return null;
2496
+ try {
2497
+ const value = window.localStorage.getItem(EDITOR_LANGUAGE_STORAGE_KEY);
2498
+ if (value && SUPPORTED_LANGUAGES.indexOf(value) !== -1) return value;
2499
+ return null;
2500
+ } catch {
2501
+ return null;
2502
+ }
2503
+ }
2504
+
2505
+ function persistEditorLanguage(lang) {
2506
+ if (!window.localStorage) return;
2507
+ try {
2508
+ window.localStorage.setItem(EDITOR_LANGUAGE_STORAGE_KEY, lang || "markdown");
2509
+ } catch {}
2510
+ }
2511
+
2512
+ function setEditorLanguage(lang) {
2513
+ editorLanguage = (lang && SUPPORTED_LANGUAGES.indexOf(lang) !== -1) ? lang : "markdown";
2514
+ persistEditorLanguage(editorLanguage);
2515
+ if (langSelect) {
2516
+ langSelect.value = editorLanguage;
2517
+ }
2518
+ if (editorHighlightEnabled && editorView === "markdown") {
2519
+ scheduleEditorHighlightRender();
2520
+ }
2521
+ if (editorView === "preview") {
2522
+ scheduleSourcePreviewRender(0);
2523
+ }
2524
+ }
2525
+
2526
+ function updateLangSelectVisibility() {
2527
+ if (!langSelect) return;
2528
+ const highlightActive = editorHighlightEnabled && editorView === "markdown";
2529
+ const previewActive = editorView === "preview";
2530
+ langSelect.hidden = !(highlightActive || previewActive);
2531
+ }
2532
+
2533
+ function setResponseHighlightEnabled(enabled) {
2534
+ responseHighlightEnabled = Boolean(enabled);
2535
+ persistResponseHighlightEnabled(responseHighlightEnabled);
2536
+ if (responseHighlightSelect) {
2537
+ responseHighlightSelect.value = responseHighlightEnabled ? "on" : "off";
2538
+ }
2539
+ renderActiveResult();
2540
+ }
2541
+
2542
+ function getAbortablePendingKind() {
2543
+ if (!pendingRequestId) return null;
2544
+ return pendingKind === "direct" || pendingKind === "critique" ? pendingKind : null;
2545
+ }
2546
+
2547
+ function requestCancelForPendingRequest(expectedKind) {
2548
+ const activeKind = getAbortablePendingKind();
2549
+ if (!activeKind || activeKind !== expectedKind || !pendingRequestId) {
2550
+ setStatus("No matching Studio request is running.", "warning");
2551
+ return false;
2552
+ }
2553
+ const requestId = pendingRequestId;
2554
+ const sent = sendMessage({ type: "cancel_request", requestId });
2555
+ if (!sent) return false;
2556
+ clearArmedTitleAttention(requestId);
2557
+ setStatus("Stopping request…", "warning");
2558
+ return true;
2559
+ }
2560
+
2561
+ function syncRunAndCritiqueButtons() {
2562
+ const activeKind = getAbortablePendingKind();
2563
+ const sendRunIsStop = activeKind === "direct";
2564
+ const critiqueIsStop = activeKind === "critique";
2565
+
2566
+ if (sendRunBtn) {
2567
+ sendRunBtn.textContent = sendRunIsStop ? "Stop" : "Run editor text";
2568
+ sendRunBtn.classList.toggle("request-stop-active", sendRunIsStop);
2569
+ sendRunBtn.disabled = sendRunIsStop ? wsState === "Disconnected" : (uiBusy || critiqueIsStop);
2570
+ sendRunBtn.title = sendRunIsStop
2571
+ ? "Stop the running editor-text request."
2572
+ : (annotationsEnabled
2573
+ ? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter."
2574
+ : "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter.");
2575
+ }
2576
+
2577
+ if (critiqueBtn) {
2578
+ critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique editor text";
2579
+ critiqueBtn.classList.toggle("request-stop-active", critiqueIsStop);
2580
+ critiqueBtn.disabled = critiqueIsStop ? wsState === "Disconnected" : (uiBusy || sendRunIsStop);
2581
+ critiqueBtn.title = critiqueIsStop
2582
+ ? "Stop the running critique request."
2583
+ : (annotationsEnabled
2584
+ ? "Critique editor text as-is (includes [an: ...] markers)."
2585
+ : "Critique editor text with [an: ...] markers stripped.");
2586
+ }
2587
+ }
2588
+
2589
+ function updateAnnotationModeUi() {
2590
+ if (annotationModeSelect) {
2591
+ annotationModeSelect.value = annotationsEnabled ? "on" : "off";
2592
+ annotationModeSelect.title = annotationsEnabled
2593
+ ? "Annotations On: keep and send [an: ...] markers."
2594
+ : "Annotations Hidden: keep markers in editor, hide in preview, and strip before Run/Critique.";
2595
+ }
2596
+
2597
+ syncRunAndCritiqueButtons();
2598
+ }
2599
+
2600
+ function setAnnotationsEnabled(enabled, _options) {
2601
+ annotationsEnabled = Boolean(enabled);
2602
+ persistAnnotationsEnabled(annotationsEnabled);
2603
+ updateAnnotationModeUi();
2604
+
2605
+ if (editorHighlightEnabled && editorView === "markdown") {
2606
+ scheduleEditorHighlightRender();
2607
+ }
2608
+ renderSourcePreview();
2609
+ }
2610
+
2611
+ function extractSection(markdown, title) {
2612
+ if (!markdown || !title) return "";
2613
+
2614
+ const lines = String(markdown).split("\n");
2615
+ const heading = "## " + String(title).trim().toLowerCase();
2616
+ let start = -1;
2617
+
2618
+ for (let i = 0; i < lines.length; i++) {
2619
+ const normalized = lines[i].trim().toLowerCase();
2620
+ if (normalized === heading) {
2621
+ start = i + 1;
2622
+ break;
2623
+ }
2624
+ }
2625
+
2626
+ if (start < 0) return "";
2627
+
2628
+ const collected = [];
2629
+ for (let i = start; i < lines.length; i++) {
2630
+ const line = lines[i];
2631
+ if (line.trim().startsWith("## ")) break;
2632
+ collected.push(line);
2633
+ }
2634
+
2635
+ return collected.join("\n").trim();
2636
+ }
2637
+
2638
+ function buildCritiqueNotesMarkdown(markdown) {
2639
+ if (!markdown || typeof markdown !== "string") return "";
2640
+
2641
+ const assessment = extractSection(markdown, "Assessment");
2642
+ const critiques = extractSection(markdown, "Critiques");
2643
+ const parts = [];
2644
+
2645
+ if (assessment) {
2646
+ parts.push("## Assessment\n\n" + assessment);
2647
+ }
2648
+ if (critiques) {
2649
+ parts.push("## Critiques\n\n" + critiques);
2650
+ }
2651
+
2652
+ return parts.join("\n\n").trim();
2653
+ }
2654
+
2655
+ function isStructuredCritique(markdown) {
2656
+ if (!markdown || typeof markdown !== "string") return false;
2657
+ const lower = markdown.toLowerCase();
2658
+ return lower.indexOf("## critiques") !== -1 && lower.indexOf("## document") !== -1;
2659
+ }
2660
+
2661
+ function handleIncomingResponse(markdown, kind, timestamp, thinking) {
2662
+ const responseTimestamp =
2663
+ typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0
2664
+ ? timestamp
2665
+ : Date.now();
2666
+
2667
+ latestResponseMarkdown = markdown;
2668
+ latestResponseThinking = typeof thinking === "string" ? thinking : "";
2669
+ latestResponseKind = kind === "critique" ? "critique" : "annotation";
2670
+ latestResponseTimestamp = responseTimestamp;
2671
+ latestResponseIsStructuredCritique = isStructuredCritique(markdown);
2672
+ latestResponseHasContent = Boolean(markdown && markdown.trim());
2673
+ latestResponseNormalized = normalizeForCompare(markdown);
2674
+ latestResponseThinkingNormalized = normalizeForCompare(latestResponseThinking);
2675
+
2676
+ if (latestResponseIsStructuredCritique) {
2677
+ latestCritiqueNotes = buildCritiqueNotesMarkdown(markdown);
2678
+ latestCritiqueNotesNormalized = normalizeForCompare(latestCritiqueNotes);
2679
+ } else {
2680
+ latestCritiqueNotes = "";
2681
+ latestCritiqueNotesNormalized = "";
2682
+ }
2683
+
2684
+ refreshResponseUi();
2685
+ }
2686
+
2687
+ function applyLatestPayload(payload) {
2688
+ if (!payload || typeof payload.markdown !== "string") return false;
2689
+ const responseKind = payload.kind === "critique" ? "critique" : "annotation";
2690
+ handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking);
2691
+ return true;
2692
+ }
2693
+
2694
+ function sendMessage(message) {
2695
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
2696
+ setWsState("Disconnected");
2697
+ setStatus("Not connected to Studio server.", "error");
2698
+ return false;
2699
+ }
2700
+ ws.send(JSON.stringify(message));
2701
+ return true;
2702
+ }
2703
+
2704
+ function handleServerMessage(message) {
2705
+ if (!message || typeof message !== "object") return;
2706
+
2707
+ debugTrace("server_message", summarizeServerMessage(message));
2708
+
2709
+ const contextChanged = applyContextUsageFromMessage(message);
2710
+ const updateInfoChanged = applyUpdateInfoFromMessage(message);
2711
+ if (contextChanged || updateInfoChanged) {
2712
+ updateFooterMeta();
2713
+ }
2714
+
2715
+ if (message.type === "debug_event") {
2716
+ debugTrace("server_debug_event", summarizeServerMessage(message));
2717
+ return;
2718
+ }
2719
+
2720
+ if (message.type === "hello_ack") {
2721
+ const busy = Boolean(message.busy);
2722
+ agentBusyFromServer = Boolean(message.agentBusy);
2723
+ updateTerminalActivityState(message.terminalPhase, message.terminalToolName, message.terminalActivityLabel);
2724
+ if (typeof message.modelLabel === "string") {
2725
+ modelLabel = message.modelLabel;
2726
+ }
2727
+ if (typeof message.terminalSessionLabel === "string") {
2728
+ terminalSessionLabel = message.terminalSessionLabel;
2729
+ }
2730
+ updateFooterMeta();
2731
+ setBusy(busy);
2732
+ setWsState(busy ? "Submitting" : "Ready");
2733
+ if (typeof message.activeRequestId === "string" && message.activeRequestId.length > 0) {
2734
+ pendingRequestId = message.activeRequestId;
2735
+ if (typeof message.activeRequestKind === "string" && message.activeRequestKind.length > 0) {
2736
+ pendingKind = message.activeRequestKind;
2737
+ } else if (!pendingKind) {
2738
+ pendingKind = "unknown";
2739
+ }
2740
+ stickyStudioKind = pendingKind;
2741
+ } else {
2742
+ pendingRequestId = null;
2743
+ pendingKind = null;
2744
+ }
2745
+
2746
+ if (typeof message.compactInProgress === "boolean") {
2747
+ compactInProgress = message.compactInProgress;
2748
+ } else if (pendingKind === "compact") {
2749
+ compactInProgress = true;
2750
+ } else if (!busy) {
2751
+ compactInProgress = false;
2752
+ }
2753
+
2754
+ let loadedInitialDocument = false;
2755
+ if (
2756
+ !initialDocumentApplied &&
2757
+ message.initialDocument &&
2758
+ typeof message.initialDocument.text === "string"
2759
+ ) {
2760
+ setEditorText(message.initialDocument.text, { preserveScroll: false, preserveSelection: false });
2761
+ initialDocumentApplied = true;
2762
+ loadedInitialDocument = true;
2763
+ setSourceState({
2764
+ source: message.initialDocument.source || "blank",
2765
+ label: message.initialDocument.label || "blank",
2766
+ path: message.initialDocument.path || null,
2767
+ });
2768
+ refreshResponseUi();
2769
+ if (typeof message.initialDocument.label === "string" && message.initialDocument.label.length > 0) {
2770
+ setStatus("Loaded " + message.initialDocument.label + ".", "success");
2771
+ }
2772
+ }
2773
+
2774
+ let appliedHistory = false;
2775
+ if (Array.isArray(message.responseHistory)) {
2776
+ appliedHistory = setResponseHistory(message.responseHistory, {
2777
+ autoSelectLatest: !initialDocumentApplied,
2778
+ preserveSelection: initialDocumentApplied,
2779
+ silent: true,
2780
+ });
2781
+ }
2782
+
2783
+ if (!appliedHistory && message.lastResponse && typeof message.lastResponse.markdown === "string") {
2784
+ const lastMarkdown = message.lastResponse.markdown;
2785
+ const lastResponseKind =
2786
+ message.lastResponse.kind === "critique"
2787
+ ? "critique"
2788
+ : (isStructuredCritique(lastMarkdown) ? "critique" : "annotation");
2789
+ handleIncomingResponse(lastMarkdown, lastResponseKind, message.lastResponse.timestamp, message.lastResponse.thinking);
2790
+ }
2791
+
2792
+ if (pendingRequestId) {
2793
+ if (busy) {
2794
+ setStatus(getStudioBusyStatus(pendingKind), "warning");
2795
+ }
2796
+ return;
2797
+ }
2798
+
2799
+ if (busy) {
2800
+ if (agentBusyFromServer && stickyStudioKind) {
2801
+ setStatus(getStudioBusyStatus(stickyStudioKind), "warning");
2802
+ } else if (agentBusyFromServer) {
2803
+ setStatus(getTerminalBusyStatus(), "warning");
2804
+ } else {
2805
+ setStatus("Studio is busy.", "warning");
2806
+ }
2807
+ return;
2808
+ }
2809
+
2810
+ stickyStudioKind = null;
2811
+ if (!loadedInitialDocument) {
2812
+ refreshResponseUi();
2813
+ setStatus(getIdleStatus());
2814
+ }
2815
+ return;
2816
+ }
2817
+
2818
+ if (message.type === "request_started") {
2819
+ pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
2820
+ pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
2821
+ stickyStudioKind = pendingKind;
2822
+ if (pendingKind === "compact") {
2823
+ compactInProgress = true;
2824
+ }
2825
+ setBusy(true);
2826
+ setWsState("Submitting");
2827
+ setStatus(getStudioBusyStatus(pendingKind), "warning");
2828
+ return;
2829
+ }
2830
+
2831
+ if (message.type === "compaction_completed") {
2832
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
2833
+ pendingRequestId = null;
2834
+ pendingKind = null;
2835
+ }
2836
+ compactInProgress = false;
2837
+ stickyStudioKind = null;
2838
+ const busy = Boolean(message.busy);
2839
+ setBusy(busy);
2840
+ setWsState(busy ? "Submitting" : "Ready");
2841
+ setStatus(typeof message.message === "string" ? message.message : "Compaction completed.", "success");
2842
+ return;
2843
+ }
2844
+
2845
+ if (message.type === "compaction_error") {
2846
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
2847
+ pendingRequestId = null;
2848
+ pendingKind = null;
2849
+ }
2850
+ compactInProgress = false;
2851
+ stickyStudioKind = null;
2852
+ const busy = Boolean(message.busy);
2853
+ setBusy(busy);
2854
+ setWsState(busy ? "Submitting" : "Ready");
2855
+ setStatus(typeof message.message === "string" ? message.message : "Compaction failed.", "error");
2856
+ return;
2857
+ }
2858
+
2859
+ if (message.type === "response") {
2860
+ if (pendingRequestId && typeof message.requestId === "string" && message.requestId !== pendingRequestId) {
2861
+ return;
2862
+ }
2863
+
2864
+ const completedRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
2865
+ const responseKind =
2866
+ typeof message.kind === "string"
2867
+ ? message.kind
2868
+ : (pendingKind === "critique" ? "critique" : "annotation");
2869
+
2870
+ stickyStudioKind = responseKind;
2871
+ pendingRequestId = null;
2872
+ pendingKind = null;
2873
+ queuedLatestResponse = null;
2874
+ setBusy(false);
2875
+ setWsState("Ready");
2876
+
2877
+ let appliedFromHistory = false;
2878
+ if (Array.isArray(message.responseHistory)) {
2879
+ appliedFromHistory = setResponseHistory(message.responseHistory, {
2880
+ autoSelectLatest: true,
2881
+ preserveSelection: false,
2882
+ silent: true,
2883
+ });
2884
+ }
2885
+
2886
+ if (!appliedFromHistory && typeof message.markdown === "string") {
2887
+ handleIncomingResponse(message.markdown, responseKind, message.timestamp, message.thinking);
2888
+ }
2889
+
2890
+ if (responseKind === "critique") {
2891
+ setStatus("Critique ready.", "success");
2892
+ } else if (responseKind === "direct") {
2893
+ setStatus("Model response ready.", "success");
2894
+ } else {
2895
+ setStatus("Response ready.", "success");
2896
+ }
2897
+ maybeShowTitleAttentionForCompletedRequest(completedRequestId, responseKind);
2898
+ return;
2899
+ }
2900
+
2901
+ if (message.type === "latest_response") {
2902
+ if (pendingRequestId) return;
2903
+
2904
+ const hasHistory = Array.isArray(message.responseHistory);
2905
+ if (hasHistory) {
2906
+ setResponseHistory(message.responseHistory, {
2907
+ autoSelectLatest: followLatest,
2908
+ preserveSelection: !followLatest,
2909
+ silent: true,
2910
+ });
2911
+ }
2912
+
2913
+ if (typeof message.markdown === "string") {
2914
+ const payload = {
2915
+ kind: message.kind === "critique" ? "critique" : "annotation",
2916
+ markdown: message.markdown,
2917
+ thinking: typeof message.thinking === "string" ? message.thinking : null,
2918
+ timestamp: message.timestamp,
2919
+ };
2920
+
2921
+ if (!followLatest) {
2922
+ queuedLatestResponse = payload;
2923
+ updateResultActionButtons();
2924
+ setStatus("New response available — click Fetch latest response.", "warning");
2925
+ return;
2926
+ }
2927
+
2928
+ if (!hasHistory && applyLatestPayload(payload)) {
2929
+ queuedLatestResponse = null;
2930
+ updateResultActionButtons();
2931
+ setStatus("Updated from latest response.", "success");
2932
+ return;
2933
+ }
2934
+
2935
+ queuedLatestResponse = null;
2936
+ updateResultActionButtons();
2937
+ setStatus("Updated from latest response.", "success");
2938
+ }
2939
+ return;
2940
+ }
2941
+
2942
+ if (message.type === "response_history") {
2943
+ setResponseHistory(message.items, {
2944
+ autoSelectLatest: followLatest,
2945
+ preserveSelection: !followLatest,
2946
+ silent: true,
2947
+ });
2948
+ return;
2949
+ }
2950
+
2951
+ if (message.type === "saved") {
2952
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
2953
+ pendingRequestId = null;
2954
+ pendingKind = null;
2955
+ }
2956
+ if (message.path) {
2957
+ setSourceState({
2958
+ source: "file",
2959
+ label: message.label || message.path,
2960
+ path: message.path,
2961
+ });
2962
+ }
2963
+ setBusy(false);
2964
+ setWsState("Ready");
2965
+ setStatus(typeof message.message === "string" ? message.message : "Saved.", "success");
2966
+ return;
2967
+ }
2968
+
2969
+ if (message.type === "editor_loaded") {
2970
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
2971
+ pendingRequestId = null;
2972
+ pendingKind = null;
2973
+ }
2974
+ setBusy(false);
2975
+ setWsState("Ready");
2976
+ setStatus(typeof message.message === "string" ? message.message : "Loaded into pi editor.", "success");
2977
+ return;
2978
+ }
2979
+
2980
+ if (message.type === "editor_snapshot") {
2981
+ if (typeof message.requestId === "string" && pendingRequestId && message.requestId !== pendingRequestId) {
2982
+ return;
2983
+ }
2984
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
2985
+ pendingRequestId = null;
2986
+ pendingKind = null;
2987
+ }
2988
+
2989
+ const content = typeof message.content === "string" ? message.content : "";
2990
+ setEditorText(content, { preserveScroll: false, preserveSelection: false });
2991
+ setSourceState({ source: "pi-editor", label: "pi editor draft", path: null });
2992
+ setBusy(false);
2993
+ setWsState("Ready");
2994
+ setStatus(
2995
+ content.trim()
2996
+ ? "Loaded draft from pi editor."
2997
+ : "pi editor is empty. Loaded blank text.",
2998
+ content.trim() ? "success" : "warning",
2999
+ );
3000
+ return;
3001
+ }
3002
+
3003
+ if (message.type === "studio_document") {
3004
+ const nextDoc = message.document;
3005
+ if (!nextDoc || typeof nextDoc !== "object" || typeof nextDoc.text !== "string") {
3006
+ return;
3007
+ }
3008
+
3009
+ const nextSource =
3010
+ nextDoc.source === "file" || nextDoc.source === "last-response"
3011
+ ? nextDoc.source
3012
+ : "blank";
3013
+ const nextLabel = typeof nextDoc.label === "string" && nextDoc.label.trim()
3014
+ ? nextDoc.label.trim()
3015
+ : (nextSource === "file" ? "file" : "studio document");
3016
+ const nextPath = typeof nextDoc.path === "string" && nextDoc.path.trim()
3017
+ ? nextDoc.path
3018
+ : null;
3019
+
3020
+ setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
3021
+ setSourceState({ source: nextSource, label: nextLabel, path: nextPath });
3022
+ refreshResponseUi();
3023
+ setStatus(
3024
+ typeof message.message === "string" && message.message.trim()
3025
+ ? message.message
3026
+ : "Loaded document from terminal.",
3027
+ "success",
3028
+ );
3029
+ return;
3030
+ }
3031
+
3032
+ if (message.type === "git_diff_snapshot") {
3033
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
3034
+ pendingRequestId = null;
3035
+ pendingKind = null;
3036
+ }
3037
+
3038
+ const content = typeof message.content === "string" ? message.content : "";
3039
+ const label = typeof message.label === "string" && message.label.trim()
3040
+ ? message.label.trim()
3041
+ : "git diff";
3042
+ setEditorText(content, { preserveScroll: false, preserveSelection: false });
3043
+ setSourceState({ source: "blank", label, path: null });
3044
+ setEditorLanguage("diff");
3045
+ setBusy(false);
3046
+ setWsState("Ready");
3047
+ refreshResponseUi();
3048
+ setStatus(
3049
+ typeof message.message === "string" && message.message.trim()
3050
+ ? message.message
3051
+ : "Loaded current git diff.",
3052
+ "success",
3053
+ );
3054
+ return;
3055
+ }
3056
+
3057
+ if (message.type === "studio_state") {
3058
+ const busy = Boolean(message.busy);
3059
+ agentBusyFromServer = Boolean(message.agentBusy);
3060
+ updateTerminalActivityState(message.terminalPhase, message.terminalToolName, message.terminalActivityLabel);
3061
+ if (typeof message.modelLabel === "string") {
3062
+ modelLabel = message.modelLabel;
3063
+ }
3064
+ if (typeof message.terminalSessionLabel === "string") {
3065
+ terminalSessionLabel = message.terminalSessionLabel;
3066
+ }
3067
+ updateFooterMeta();
3068
+
3069
+ if (typeof message.activeRequestId === "string" && message.activeRequestId.length > 0) {
3070
+ pendingRequestId = message.activeRequestId;
3071
+ if (typeof message.activeRequestKind === "string" && message.activeRequestKind.length > 0) {
3072
+ pendingKind = message.activeRequestKind;
3073
+ } else if (!pendingKind) {
3074
+ pendingKind = "unknown";
3075
+ }
3076
+ stickyStudioKind = pendingKind;
3077
+ } else {
3078
+ pendingRequestId = null;
3079
+ pendingKind = null;
3080
+ }
3081
+
3082
+ if (typeof message.compactInProgress === "boolean") {
3083
+ compactInProgress = message.compactInProgress;
3084
+ } else if (pendingKind === "compact") {
3085
+ compactInProgress = true;
3086
+ } else if (!busy) {
3087
+ compactInProgress = false;
3088
+ }
3089
+
3090
+ setBusy(busy);
3091
+ setWsState(busy ? "Submitting" : "Ready");
3092
+
3093
+ if (pendingRequestId) {
3094
+ if (busy) {
3095
+ setStatus(getStudioBusyStatus(pendingKind), "warning");
3096
+ }
3097
+ return;
3098
+ }
3099
+
3100
+ if (busy) {
3101
+ if (agentBusyFromServer && stickyStudioKind) {
3102
+ setStatus(getStudioBusyStatus(stickyStudioKind), "warning");
3103
+ } else if (agentBusyFromServer) {
3104
+ setStatus(getTerminalBusyStatus(), "warning");
3105
+ } else {
3106
+ setStatus("Studio is busy.", "warning");
3107
+ }
3108
+ return;
3109
+ }
3110
+
3111
+ stickyStudioKind = null;
3112
+ setStatus(getIdleStatus());
3113
+ return;
3114
+ }
3115
+
3116
+ if (message.type === "busy") {
3117
+ if (message.requestId && pendingRequestId === message.requestId) {
3118
+ if (pendingKind === "compact") {
3119
+ compactInProgress = false;
3120
+ }
3121
+ pendingRequestId = null;
3122
+ pendingKind = null;
3123
+ }
3124
+ if (typeof message.requestId === "string") {
3125
+ clearArmedTitleAttention(message.requestId);
3126
+ }
3127
+ stickyStudioKind = null;
3128
+ setBusy(false);
3129
+ setWsState("Ready");
3130
+ setStatus(typeof message.message === "string" ? message.message : "Studio is busy.", "warning");
3131
+ return;
3132
+ }
3133
+
3134
+ if (message.type === "error") {
3135
+ if (message.requestId && pendingRequestId === message.requestId) {
3136
+ if (pendingKind === "compact") {
3137
+ compactInProgress = false;
3138
+ }
3139
+ pendingRequestId = null;
3140
+ pendingKind = null;
3141
+ }
3142
+ if (typeof message.requestId === "string") {
3143
+ clearArmedTitleAttention(message.requestId);
3144
+ }
3145
+ stickyStudioKind = null;
3146
+ setBusy(false);
3147
+ setWsState("Ready");
3148
+ setStatus(typeof message.message === "string" ? message.message : "Request failed.", "error");
3149
+ return;
3150
+ }
3151
+
3152
+ if (message.type === "info") {
3153
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
3154
+ pendingRequestId = null;
3155
+ pendingKind = null;
3156
+ setBusy(false);
3157
+ setWsState("Ready");
3158
+ }
3159
+ if (typeof message.message === "string") {
3160
+ setStatus(
3161
+ message.message,
3162
+ typeof message.level === "string" ? message.level : undefined,
3163
+ );
3164
+ }
3165
+ }
3166
+
3167
+ if (message.type === "theme_update" && message.vars && typeof message.vars === "object") {
3168
+ var root = document.documentElement;
3169
+ Object.keys(message.vars).forEach(function(key) {
3170
+ if (key === "color-scheme") {
3171
+ root.style.colorScheme = message.vars[key];
3172
+ } else {
3173
+ root.style.setProperty(key, message.vars[key]);
3174
+ }
3175
+ });
3176
+ }
3177
+ }
3178
+
3179
+ function clearScheduledReconnect() {
3180
+ if (reconnectTimer !== null) {
3181
+ window.clearTimeout(reconnectTimer);
3182
+ reconnectTimer = null;
3183
+ }
3184
+ }
3185
+
3186
+ function formatReconnectDelay(delayMs) {
3187
+ const delay = Math.max(0, Number(delayMs) || 0);
3188
+ if (delay < 1000) return delay + "ms";
3189
+ const seconds = delay / 1000;
3190
+ return (Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1)) + "s";
3191
+ }
3192
+
3193
+ function scheduleReconnect(reasonMessage) {
3194
+ if (reconnectTimer !== null) return;
3195
+
3196
+ reconnectAttempt += 1;
3197
+ const delayMs = Math.min(8000, 600 * Math.pow(2, Math.max(0, reconnectAttempt - 1)));
3198
+ setBusy(true);
3199
+ setWsState("Connecting");
3200
+ setStatus((reasonMessage || "Connection lost.") + " Reconnecting in " + formatReconnectDelay(delayMs) + "…", "warning");
3201
+
3202
+ reconnectTimer = window.setTimeout(() => {
3203
+ reconnectTimer = null;
3204
+ connect();
3205
+ }, delayMs);
3206
+ }
3207
+
3208
+ function connect() {
3209
+ clearScheduledReconnect();
3210
+
3211
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
3212
+ return;
3213
+ }
3214
+
3215
+ const token = getToken();
3216
+ if (!token) {
3217
+ setWsState("Disconnected");
3218
+ setStatus("Missing Studio token in URL. Re-run /studio.", "error");
3219
+ setBusy(true);
3220
+ return;
3221
+ }
3222
+
3223
+ const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
3224
+ const wsUrl = wsProtocol + "://" + window.location.host + "/ws?token=" + encodeURIComponent(token) + (DEBUG_ENABLED ? "&debug=1" : "");
3225
+ const wasReconnect = reconnectAttempt > 0;
3226
+ let disconnectHandled = false;
3227
+
3228
+ setWsState("Connecting");
3229
+ setStatus(wasReconnect ? "Reconnecting to Studio server…" : "Connecting to Studio server…");
3230
+ const socket = new WebSocket(wsUrl);
3231
+ ws = socket;
3232
+
3233
+ const connectWatchdog = window.setTimeout(() => {
3234
+ if (ws === socket && socket.readyState === WebSocket.CONNECTING) {
3235
+ setWsState("Connecting");
3236
+ setStatus(wasReconnect ? "Still reconnecting…" : "Still connecting…", "warning");
3237
+ }
3238
+ }, 3000);
3239
+
3240
+ const handleDisconnect = (kind, code) => {
3241
+ if (disconnectHandled) return;
3242
+ disconnectHandled = true;
3243
+ window.clearTimeout(connectWatchdog);
3244
+ if (ws === socket) {
3245
+ ws = null;
3246
+ }
3247
+ setBusy(true);
3248
+
3249
+ if (kind === "invalidated") {
3250
+ clearScheduledReconnect();
3251
+ reconnectAttempt = 0;
3252
+ setWsState("Disconnected");
3253
+ setStatus("This tab was invalidated by a newer /studio session.", "warning");
3254
+ return;
3255
+ }
3256
+
3257
+ if (kind === "shutdown") {
3258
+ clearScheduledReconnect();
3259
+ reconnectAttempt = 0;
3260
+ setWsState("Disconnected");
3261
+ setStatus("Studio server shut down. Re-run /studio.", "warning");
3262
+ return;
3263
+ }
3264
+
3265
+ const detail = typeof code === "number" && code > 0
3266
+ ? "Disconnected (code " + code + ")."
3267
+ : (kind === "error" ? "WebSocket error." : "Connection lost.");
3268
+ scheduleReconnect(detail);
3269
+ };
3270
+
3271
+ socket.addEventListener("open", () => {
3272
+ window.clearTimeout(connectWatchdog);
3273
+ setWsState("Ready");
3274
+ setStatus(wasReconnect ? "Reconnected. Syncing…" : "Connected. Syncing…");
3275
+ sendMessage({ type: "hello" });
3276
+ reconnectAttempt = 0;
3277
+ });
3278
+
3279
+ socket.addEventListener("message", (event) => {
3280
+ try {
3281
+ const message = JSON.parse(event.data);
3282
+ handleServerMessage(message);
3283
+ } catch (error) {
3284
+ setWsState("Ready");
3285
+ setStatus("Received invalid server message.", "error");
3286
+ }
3287
+ });
3288
+
3289
+ socket.addEventListener("close", (event) => {
3290
+ if (event && event.code === 4001) {
3291
+ handleDisconnect("invalidated", 4001);
3292
+ return;
3293
+ }
3294
+ if (event && event.code === 1001) {
3295
+ handleDisconnect("shutdown", 1001);
3296
+ return;
3297
+ }
3298
+ const code = event && typeof event.code === "number" ? event.code : 0;
3299
+ handleDisconnect("close", code);
3300
+ });
3301
+
3302
+ socket.addEventListener("error", () => {
3303
+ handleDisconnect("error");
3304
+ });
3305
+ }
3306
+
3307
+ function beginUiAction(kind) {
3308
+ if (uiBusy) {
3309
+ setStatus("Studio is busy.", "warning");
3310
+ return null;
3311
+ }
3312
+ clearTitleAttention();
3313
+ const requestId = makeRequestId();
3314
+ pendingRequestId = requestId;
3315
+ pendingKind = kind;
3316
+ stickyStudioKind = kind;
3317
+ armTitleAttentionForRequest(requestId, kind);
3318
+ setBusy(true);
3319
+ setWsState("Submitting");
3320
+ setStatus(getStudioBusyStatus(kind), "warning");
3321
+ return requestId;
3322
+ }
3323
+
3324
+ function describeSourceForAnnotation() {
3325
+ if (sourceState.source === "file" && sourceState.label) {
3326
+ return "file " + sourceState.label;
3327
+ }
3328
+ if (sourceState.source === "last-response") {
3329
+ return "last model response";
3330
+ }
3331
+ if (sourceState.label && sourceState.label !== "blank") {
3332
+ return sourceState.label;
3333
+ }
3334
+ return "studio editor";
3335
+ }
3336
+
3337
+ function buildAnnotationHeader() {
3338
+ const sourceDescriptor = describeSourceForAnnotation();
3339
+ let header = "annotated reply below:\n";
3340
+ header += "original source: " + sourceDescriptor + "\n";
3341
+ header += "annotation syntax: [an: your note]\n";
3342
+ header += "precedence: later messages supersede these annotations unless user explicitly references them\n\n---\n\n";
3343
+ return header;
3344
+ }
3345
+
3346
+ function stripAnnotationBoundaryMarker(text) {
3347
+ return String(text || "").replace(/\n{0,2}--- end annotations ---\s*$/i, "");
3348
+ }
3349
+
3350
+ function stripAnnotationHeader(text) {
3351
+ const normalized = String(text || "").replace(/\r\n/g, "\n");
3352
+ if (!normalized.toLowerCase().startsWith("annotated reply below:")) {
3353
+ return { hadHeader: false, body: normalized };
3354
+ }
3355
+
3356
+ const dividerIndex = normalized.indexOf("\n---");
3357
+ if (dividerIndex < 0) {
3358
+ return { hadHeader: false, body: normalized };
3359
+ }
3360
+
3361
+ let cursor = dividerIndex + 4;
3362
+ while (cursor < normalized.length && normalized[cursor] === "\n") {
3363
+ cursor += 1;
3364
+ }
3365
+
3366
+ return {
3367
+ hadHeader: true,
3368
+ body: stripAnnotationBoundaryMarker(normalized.slice(cursor)),
3369
+ };
3370
+ }
3371
+
3372
+ function updateAnnotatedReplyHeaderButton() {
3373
+ if (!insertHeaderBtn) return;
3374
+ const hasHeader = stripAnnotationHeader(sourceTextEl.value).hadHeader;
3375
+ if (hasHeader) {
3376
+ insertHeaderBtn.textContent = "Remove annotated reply header";
3377
+ insertHeaderBtn.title = "Remove annotated-reply protocol header while keeping body text.";
3378
+ return;
3379
+ }
3380
+ insertHeaderBtn.textContent = "Insert annotated reply header";
3381
+ insertHeaderBtn.title = "Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).";
3382
+ }
3383
+
3384
+ function toggleAnnotatedReplyHeader() {
3385
+ const stripped = stripAnnotationHeader(sourceTextEl.value);
3386
+
3387
+ if (stripped.hadHeader) {
3388
+ const updated = stripped.body;
3389
+ setEditorText(updated, { preserveScroll: true, preserveSelection: true });
3390
+ updateResultActionButtons();
3391
+ setStatus("Removed annotated reply header.", "success");
3392
+ return;
3393
+ }
3394
+
3395
+ const cleanedBody = stripAnnotationBoundaryMarker(stripped.body);
3396
+ const updated = buildAnnotationHeader() + cleanedBody + "\n\n--- end annotations ---\n\n";
3397
+ if (isTextEquivalent(sourceTextEl.value, updated)) {
3398
+ setStatus("Annotated reply header already present.");
3399
+ return;
3400
+ }
3401
+
3402
+ setEditorText(updated, { preserveScroll: true, preserveSelection: true });
3403
+ updateResultActionButtons();
3404
+ setStatus("Inserted annotated reply header.", "success");
3405
+ }
3406
+
3407
+ function requestLatestResponse() {
3408
+ const sent = sendMessage({ type: "get_latest_response" });
3409
+ if (!sent) return;
3410
+ setStatus("Fetching latest response…");
3411
+ }
3412
+
3413
+ if (leftPaneEl) {
3414
+ leftPaneEl.addEventListener("mousedown", () => setActivePane("left"));
3415
+ leftPaneEl.addEventListener("focusin", () => setActivePane("left"));
3416
+ }
3417
+
3418
+ if (rightPaneEl) {
3419
+ rightPaneEl.addEventListener("mousedown", () => setActivePane("right"));
3420
+ rightPaneEl.addEventListener("focusin", () => setActivePane("right"));
3421
+ }
3422
+
3423
+ window.addEventListener("keydown", handlePaneShortcut);
3424
+ window.addEventListener("beforeunload", () => {
3425
+ stopFooterSpinner();
3426
+ });
3427
+
3428
+ editorViewSelect.addEventListener("change", () => {
3429
+ setEditorView(editorViewSelect.value);
3430
+ });
3431
+
3432
+ rightViewSelect.addEventListener("change", () => {
3433
+ setRightView(rightViewSelect.value);
3434
+ });
3435
+
3436
+ followSelect.addEventListener("change", () => {
3437
+ followLatest = followSelect.value !== "off";
3438
+ if (followLatest && queuedLatestResponse) {
3439
+ if (responseHistory.length > 0) {
3440
+ selectHistoryIndex(responseHistory.length - 1, { silent: true });
3441
+ queuedLatestResponse = null;
3442
+ setStatus("Applied queued response.", "success");
3443
+ } else if (applyLatestPayload(queuedLatestResponse)) {
3444
+ queuedLatestResponse = null;
3445
+ setStatus("Applied queued response.", "success");
3446
+ }
3447
+ } else if (!followLatest) {
3448
+ setStatus("Auto-update is off. Use Fetch latest response.");
3449
+ }
3450
+ updateResultActionButtons();
3451
+ });
3452
+
3453
+ if (highlightSelect) {
3454
+ highlightSelect.addEventListener("change", () => {
3455
+ setEditorHighlightEnabled(highlightSelect.value === "on");
3456
+ });
3457
+ }
3458
+
3459
+ if (responseHighlightSelect) {
3460
+ responseHighlightSelect.addEventListener("change", () => {
3461
+ setResponseHighlightEnabled(responseHighlightSelect.value === "on");
3462
+ });
3463
+ }
3464
+
3465
+ if (langSelect) {
3466
+ langSelect.addEventListener("change", () => {
3467
+ setEditorLanguage(langSelect.value);
3468
+ });
3469
+ }
3470
+
3471
+ if (annotationModeSelect) {
3472
+ annotationModeSelect.addEventListener("change", () => {
3473
+ setAnnotationsEnabled(annotationModeSelect.value !== "off");
3474
+ });
3475
+ }
3476
+
3477
+ if (compactBtn) {
3478
+ compactBtn.addEventListener("click", () => {
3479
+ if (compactInProgress) {
3480
+ setStatus("Compaction is already running.", "warning");
3481
+ return;
3482
+ }
3483
+ if (uiBusy) {
3484
+ setStatus("Studio is busy.", "warning");
3485
+ return;
3486
+ }
3487
+
3488
+ const requestId = makeRequestId();
3489
+ pendingRequestId = requestId;
3490
+ pendingKind = "compact";
3491
+ stickyStudioKind = "compact";
3492
+ compactInProgress = true;
3493
+ setBusy(true);
3494
+ setWsState("Submitting");
3495
+
3496
+ const sent = sendMessage({ type: "compact_request", requestId });
3497
+ if (!sent) {
3498
+ compactInProgress = false;
3499
+ if (pendingRequestId === requestId) {
3500
+ pendingRequestId = null;
3501
+ pendingKind = null;
3502
+ }
3503
+ stickyStudioKind = null;
3504
+ setBusy(false);
3505
+ return;
3506
+ }
3507
+
3508
+ setStatus("Studio: compacting context…", "warning");
3509
+ });
3510
+ }
3511
+
3512
+ if (historyPrevBtn) {
3513
+ historyPrevBtn.addEventListener("click", () => {
3514
+ if (!responseHistory.length) {
3515
+ setStatus("No response history available yet.", "warning");
3516
+ return;
3517
+ }
3518
+ selectHistoryIndex(responseHistoryIndex - 1);
3519
+ });
3520
+ }
3521
+
3522
+ if (historyNextBtn) {
3523
+ historyNextBtn.addEventListener("click", () => {
3524
+ if (!responseHistory.length) {
3525
+ setStatus("No response history available yet.", "warning");
3526
+ return;
3527
+ }
3528
+ selectHistoryIndex(responseHistoryIndex + 1);
3529
+ });
3530
+ }
3531
+
3532
+ if (historyLastBtn) {
3533
+ historyLastBtn.addEventListener("click", () => {
3534
+ if (!responseHistory.length) {
3535
+ setStatus("No response history available yet.", "warning");
3536
+ return;
3537
+ }
3538
+ selectHistoryIndex(responseHistory.length - 1);
3539
+ });
3540
+ }
3541
+
3542
+ if (loadHistoryPromptBtn) {
3543
+ loadHistoryPromptBtn.addEventListener("click", () => {
3544
+ const item = getSelectedHistoryItem();
3545
+ const prompt = item && typeof item.prompt === "string" ? item.prompt : "";
3546
+ if (!prompt.trim()) {
3547
+ setStatus("Prompt unavailable for the selected response.", "warning");
3548
+ return;
3549
+ }
3550
+
3551
+ setEditorText(prompt, { preserveScroll: false, preserveSelection: false });
3552
+ setSourceState({ source: "blank", label: "response prompt", path: null });
3553
+ setStatus("Loaded response prompt into editor.", "success");
3554
+ });
3555
+ }
3556
+
3557
+ pullLatestBtn.addEventListener("click", () => {
3558
+ if (queuedLatestResponse) {
3559
+ if (responseHistory.length > 0) {
3560
+ selectHistoryIndex(responseHistory.length - 1, { silent: true });
3561
+ queuedLatestResponse = null;
3562
+ setStatus("Pulled latest response from history.", "success");
3563
+ updateResultActionButtons();
3564
+ } else if (applyLatestPayload(queuedLatestResponse)) {
3565
+ queuedLatestResponse = null;
3566
+ setStatus("Pulled queued response.", "success");
3567
+ updateResultActionButtons();
3568
+ }
3569
+ return;
3570
+ }
3571
+ requestLatestResponse();
3572
+ });
3573
+
3574
+ sourceTextEl.addEventListener("input", () => {
3575
+ renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
3576
+ scheduleEditorMetaUpdate();
3577
+ });
3578
+
3579
+ sourceTextEl.addEventListener("scroll", () => {
3580
+ if (!editorHighlightEnabled || editorView !== "markdown") return;
3581
+ syncEditorHighlightScroll();
3582
+ });
3583
+
3584
+ sourceTextEl.addEventListener("keyup", () => {
3585
+ if (!editorHighlightEnabled || editorView !== "markdown") return;
3586
+ syncEditorHighlightScroll();
3587
+ });
3588
+
3589
+ sourceTextEl.addEventListener("mouseup", () => {
3590
+ if (!editorHighlightEnabled || editorView !== "markdown") return;
3591
+ syncEditorHighlightScroll();
3592
+ });
3593
+
3594
+ window.addEventListener("resize", () => {
3595
+ if (!editorHighlightEnabled || editorView !== "markdown") return;
3596
+ syncEditorHighlightScroll();
3597
+ });
3598
+
3599
+ insertHeaderBtn.addEventListener("click", () => {
3600
+ toggleAnnotatedReplyHeader();
3601
+ });
3602
+
3603
+ critiqueBtn.addEventListener("click", () => {
3604
+ if (getAbortablePendingKind() === "critique") {
3605
+ requestCancelForPendingRequest("critique");
3606
+ return;
3607
+ }
3608
+
3609
+ const preparedDocumentText = prepareEditorTextForSend(sourceTextEl.value);
3610
+ const documentText = preparedDocumentText.trim();
3611
+ if (!documentText) {
3612
+ setStatus("Add editor text before critique.", "warning");
3613
+ return;
3614
+ }
3615
+
3616
+ const requestId = beginUiAction("critique");
3617
+ if (!requestId) return;
3618
+
3619
+ const sent = sendMessage({
3620
+ type: "critique_request",
3621
+ requestId,
3622
+ document: documentText,
3623
+ lens: lensSelect.value,
3624
+ });
3625
+
3626
+ if (!sent) {
3627
+ pendingRequestId = null;
3628
+ pendingKind = null;
3629
+ setBusy(false);
3630
+ }
3631
+ });
3632
+
3633
+ loadResponseBtn.addEventListener("click", () => {
3634
+ if (rightView === "thinking") {
3635
+ if (!latestResponseThinking.trim()) {
3636
+ setStatus("No thinking available for the selected response.", "warning");
3637
+ return;
3638
+ }
3639
+ setEditorText(latestResponseThinking, { preserveScroll: false, preserveSelection: false });
3640
+ setSourceState({ source: "blank", label: "assistant thinking", path: null });
3641
+ setStatus("Loaded thinking into editor.", "success");
3642
+ return;
3643
+ }
3644
+
3645
+ if (!latestResponseMarkdown.trim()) {
3646
+ setStatus("No response available yet.", "warning");
3647
+ return;
3648
+ }
3649
+ setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
3650
+ setSourceState({ source: "last-response", label: "last model response", path: null });
3651
+ setStatus("Loaded response into editor.", "success");
3652
+ });
3653
+
3654
+ loadCritiqueNotesBtn.addEventListener("click", () => {
3655
+ if (!latestResponseIsStructuredCritique || !latestResponseMarkdown.trim()) {
3656
+ setStatus("Latest response is not a structured critique response.", "warning");
3657
+ return;
3658
+ }
3659
+
3660
+ const notes = buildCritiqueNotesMarkdown(latestResponseMarkdown);
3661
+ if (!notes) {
3662
+ setStatus("No critique notes (Assessment/Critiques) found in latest response.", "warning");
3663
+ return;
3664
+ }
3665
+
3666
+ setEditorText(notes, { preserveScroll: false, preserveSelection: false });
3667
+ setSourceState({ source: "blank", label: "critique notes", path: null });
3668
+ setStatus("Loaded critique notes into editor.", "success");
3669
+ });
3670
+
3671
+ loadCritiqueFullBtn.addEventListener("click", () => {
3672
+ if (!latestResponseIsStructuredCritique || !latestResponseMarkdown.trim()) {
3673
+ setStatus("Latest response is not a structured critique response.", "warning");
3674
+ return;
3675
+ }
3676
+
3677
+ setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
3678
+ setSourceState({ source: "blank", label: "full critique", path: null });
3679
+ setStatus("Loaded full critique into editor.", "success");
3680
+ });
3681
+
3682
+ copyResponseBtn.addEventListener("click", async () => {
3683
+ const content = rightView === "thinking" ? latestResponseThinking : latestResponseMarkdown;
3684
+ if (!content.trim()) {
3685
+ setStatus(rightView === "thinking" ? "No thinking available yet." : "No response available yet.", "warning");
3686
+ return;
3687
+ }
3688
+
3689
+ try {
3690
+ await navigator.clipboard.writeText(content);
3691
+ setStatus(rightView === "thinking" ? "Copied thinking text." : "Copied response text.", "success");
3692
+ } catch (error) {
3693
+ setStatus("Clipboard write failed.", "warning");
3694
+ }
3695
+ });
3696
+
3697
+ if (exportPdfBtn) {
3698
+ exportPdfBtn.addEventListener("click", () => {
3699
+ void exportRightPanePdf();
3700
+ });
3701
+ }
3702
+
3703
+ saveAsBtn.addEventListener("click", () => {
3704
+ const content = sourceTextEl.value;
3705
+ if (!content.trim()) {
3706
+ setStatus("Editor is empty. Nothing to save.", "warning");
3707
+ return;
3708
+ }
3709
+
3710
+ var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
3711
+ var suggestedDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim().replace(/\/$/, "") + "/" : "./";
3712
+ const suggested = sourceState.path || (suggestedDir + suggestedName);
3713
+ const path = window.prompt("Save editor content as:", suggested);
3714
+ if (!path) return;
3715
+
3716
+ const requestId = beginUiAction("save_as");
3717
+ if (!requestId) return;
3718
+
3719
+ const sent = sendMessage({
3720
+ type: "save_as_request",
3721
+ requestId,
3722
+ path,
3723
+ content,
3724
+ });
3725
+
3726
+ if (!sent) {
3727
+ pendingRequestId = null;
3728
+ pendingKind = null;
3729
+ setBusy(false);
3730
+ }
3731
+ });
3732
+
3733
+ saveOverBtn.addEventListener("click", () => {
3734
+ var effectivePath = getEffectiveSavePath();
3735
+ if (!effectivePath) {
3736
+ setStatus("Save editor requires a file path. Open via /studio <path>, set a working dir, or use Save editor as…", "warning");
3737
+ return;
3738
+ }
3739
+
3740
+ if (!window.confirm("Overwrite " + effectivePath + "?")) {
3741
+ return;
3742
+ }
3743
+
3744
+ const requestId = beginUiAction("save_over");
3745
+ if (!requestId) return;
3746
+
3747
+ // Use save_as with the effective path for both file-backed and derived paths
3748
+ const sent = sendMessage({
3749
+ type: "save_as_request",
3750
+ requestId,
3751
+ path: effectivePath,
3752
+ content: sourceTextEl.value,
3753
+ });
3754
+
3755
+ if (!sent) {
3756
+ pendingRequestId = null;
3757
+ pendingKind = null;
3758
+ setBusy(false);
3759
+ }
3760
+ });
3761
+
3762
+ sendEditorBtn.addEventListener("click", () => {
3763
+ const content = sourceTextEl.value;
3764
+ if (!content.trim()) {
3765
+ setStatus("Editor is empty. Nothing to send.", "warning");
3766
+ return;
3767
+ }
3768
+
3769
+ const requestId = beginUiAction("send_to_editor");
3770
+ if (!requestId) return;
3771
+
3772
+ const sent = sendMessage({
3773
+ type: "send_to_editor_request",
3774
+ requestId,
3775
+ content,
3776
+ });
3777
+
3778
+ if (!sent) {
3779
+ pendingRequestId = null;
3780
+ pendingKind = null;
3781
+ setBusy(false);
3782
+ }
3783
+ });
3784
+
3785
+ if (getEditorBtn) {
3786
+ getEditorBtn.addEventListener("click", () => {
3787
+ const requestId = beginUiAction("get_from_editor");
3788
+ if (!requestId) return;
3789
+
3790
+ const sent = sendMessage({
3791
+ type: "get_from_editor_request",
3792
+ requestId,
3793
+ });
3794
+
3795
+ if (!sent) {
3796
+ pendingRequestId = null;
3797
+ pendingKind = null;
3798
+ setBusy(false);
3799
+ }
3800
+ });
3801
+ }
3802
+
3803
+ if (loadGitDiffBtn) {
3804
+ loadGitDiffBtn.addEventListener("click", () => {
3805
+ const requestId = beginUiAction("load_git_diff");
3806
+ if (!requestId) return;
3807
+
3808
+ const effectivePath = getEffectiveSavePath();
3809
+ const sent = sendMessage({
3810
+ type: "load_git_diff_request",
3811
+ requestId,
3812
+ sourcePath: effectivePath || sourceState.path || undefined,
3813
+ resourceDir: resourceDirInput && resourceDirInput.value.trim()
3814
+ ? resourceDirInput.value.trim()
3815
+ : undefined,
3816
+ });
3817
+
3818
+ if (!sent) {
3819
+ pendingRequestId = null;
3820
+ pendingKind = null;
3821
+ setBusy(false);
3822
+ }
3823
+ });
3824
+ }
3825
+
3826
+ sendRunBtn.addEventListener("click", () => {
3827
+ if (getAbortablePendingKind() === "direct") {
3828
+ requestCancelForPendingRequest("direct");
3829
+ return;
3830
+ }
3831
+
3832
+ const prepared = prepareEditorTextForSend(sourceTextEl.value);
3833
+ if (!prepared.trim()) {
3834
+ setStatus("Editor is empty. Nothing to run.", "warning");
3835
+ return;
3836
+ }
3837
+
3838
+ const requestId = beginUiAction("direct");
3839
+ if (!requestId) return;
3840
+
3841
+ const sent = sendMessage({
3842
+ type: "send_run_request",
3843
+ requestId,
3844
+ text: prepared,
3845
+ });
3846
+
3847
+ if (!sent) {
3848
+ pendingRequestId = null;
3849
+ pendingKind = null;
3850
+ setBusy(false);
3851
+ }
3852
+ });
3853
+
3854
+ copyDraftBtn.addEventListener("click", async () => {
3855
+ const content = sourceTextEl.value;
3856
+ if (!content.trim()) {
3857
+ setStatus("Editor is empty. Nothing to copy.", "warning");
3858
+ return;
3859
+ }
3860
+
3861
+ try {
3862
+ await navigator.clipboard.writeText(content);
3863
+ setStatus("Copied editor text.", "success");
3864
+ } catch (error) {
3865
+ setStatus("Clipboard write failed.", "warning");
3866
+ }
3867
+ });
3868
+
3869
+ if (saveAnnotatedBtn) {
3870
+ saveAnnotatedBtn.addEventListener("click", () => {
3871
+ const content = sourceTextEl.value;
3872
+ if (!content.trim()) {
3873
+ setStatus("Editor is empty. Nothing to save.", "warning");
3874
+ return;
3875
+ }
3876
+
3877
+ const suggested = buildAnnotatedSaveSuggestion();
3878
+ const path = window.prompt("Save annotated editor content as:", suggested);
3879
+ if (!path) return;
3880
+
3881
+ const requestId = beginUiAction("save_as");
3882
+ if (!requestId) return;
3883
+
3884
+ const sent = sendMessage({
3885
+ type: "save_as_request",
3886
+ requestId,
3887
+ path,
3888
+ content,
3889
+ });
3890
+
3891
+ if (!sent) {
3892
+ pendingRequestId = null;
3893
+ pendingKind = null;
3894
+ setBusy(false);
3895
+ }
3896
+ });
3897
+ }
3898
+
3899
+ if (stripAnnotationsBtn) {
3900
+ stripAnnotationsBtn.addEventListener("click", () => {
3901
+ const content = sourceTextEl.value;
3902
+ if (!hasAnnotationMarkers(content)) {
3903
+ setStatus("No [an: ...] markers found in editor.", "warning");
3904
+ return;
3905
+ }
3906
+
3907
+ const confirmed = window.confirm("Remove all [an: ...] markers from editor text? This cannot be undone.");
3908
+ if (!confirmed) return;
3909
+
3910
+ const strippedContent = stripAnnotationMarkers(content);
3911
+ setEditorText(strippedContent, { preserveScroll: true, preserveSelection: false });
3912
+ setStatus("Removed annotation markers from editor text.", "success");
3913
+ });
3914
+ }
3915
+
3916
+ // Working directory controls — three states: button | input | label
3917
+ function showResourceDirState(state) {
3918
+ // state: "button" | "input" | "label"
3919
+ if (resourceDirBtn) resourceDirBtn.hidden = state !== "button";
3920
+ if (resourceDirInputWrap) {
3921
+ if (state === "input") resourceDirInputWrap.classList.add("visible");
3922
+ else resourceDirInputWrap.classList.remove("visible");
3923
+ }
3924
+ if (resourceDirLabel) resourceDirLabel.hidden = state !== "label";
3925
+ }
3926
+ function applyResourceDir() {
3927
+ var dir = resourceDirInput ? resourceDirInput.value.trim() : "";
3928
+ if (dir) {
3929
+ if (resourceDirLabel) resourceDirLabel.textContent = "Working dir: " + dir;
3930
+ showResourceDirState("label");
3931
+ } else {
3932
+ showResourceDirState("button");
3933
+ }
3934
+ updateSaveFileTooltip();
3935
+ syncActionButtons();
3936
+ renderSourcePreview();
3937
+ }
3938
+ if (resourceDirBtn) {
3939
+ resourceDirBtn.addEventListener("click", () => {
3940
+ showResourceDirState("input");
3941
+ if (resourceDirInput) resourceDirInput.focus();
3942
+ });
3943
+ }
3944
+ if (resourceDirLabel) {
3945
+ resourceDirLabel.addEventListener("click", () => {
3946
+ showResourceDirState("input");
3947
+ if (resourceDirInput) resourceDirInput.focus();
3948
+ });
3949
+ }
3950
+ if (resourceDirInput) {
3951
+ resourceDirInput.addEventListener("keydown", (e) => {
3952
+ if (e.key === "Enter") {
3953
+ e.preventDefault();
3954
+ applyResourceDir();
3955
+ } else if (e.key === "Escape") {
3956
+ e.preventDefault();
3957
+ var dir = resourceDirInput.value.trim();
3958
+ if (dir) {
3959
+ showResourceDirState("label");
3960
+ } else {
3961
+ showResourceDirState("button");
3962
+ }
3963
+ }
3964
+ });
3965
+ }
3966
+ if (resourceDirClearBtn) {
3967
+ resourceDirClearBtn.addEventListener("click", () => {
3968
+ if (resourceDirInput) resourceDirInput.value = "";
3969
+ if (resourceDirLabel) resourceDirLabel.textContent = "";
3970
+ showResourceDirState("button");
3971
+ updateSaveFileTooltip();
3972
+ syncActionButtons();
3973
+ renderSourcePreview();
3974
+ });
3975
+ }
3976
+
3977
+ fileInput.addEventListener("change", () => {
3978
+ const file = fileInput.files && fileInput.files[0];
3979
+ if (!file) return;
3980
+
3981
+ const reader = new FileReader();
3982
+ reader.onload = () => {
3983
+ const text = typeof reader.result === "string" ? reader.result : "";
3984
+ setEditorText(text, { preserveScroll: false, preserveSelection: false });
3985
+ setSourceState({
3986
+ source: "blank",
3987
+ label: "upload: " + file.name,
3988
+ path: null,
3989
+ });
3990
+ refreshResponseUi();
3991
+ const detectedLang = detectLanguageFromName(file.name);
3992
+ if (detectedLang) {
3993
+ setEditorLanguage(detectedLang);
3994
+ }
3995
+ setStatus("Loaded file " + file.name + ".", "success");
3996
+ };
3997
+ reader.onerror = () => {
3998
+ setStatus("Failed to read file.", "error");
3999
+ };
4000
+ reader.readAsText(file);
4001
+ });
4002
+
4003
+ setSourceState(initialSourceState);
4004
+ refreshResponseUi();
4005
+ updateAnnotatedReplyHeaderButton();
4006
+ setActivePane("left");
4007
+
4008
+ const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
4009
+ const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value === "on");
4010
+ setEditorHighlightEnabled(initialHighlightEnabled);
4011
+
4012
+ const initialDetectedLang = detectLanguageFromName(initialSourceState.path || initialSourceState.label || "");
4013
+ const storedLang = readStoredEditorLanguage();
4014
+ setEditorLanguage(initialDetectedLang || storedLang || "markdown");
4015
+
4016
+ const storedResponseHighlightEnabled = readStoredResponseHighlightEnabled();
4017
+ const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
4018
+ setResponseHighlightEnabled(initialResponseHighlightEnabled);
4019
+
4020
+ const storedAnnotationsEnabled = readStoredAnnotationsEnabled();
4021
+ const initialAnnotationsEnabled = storedAnnotationsEnabled ?? Boolean(annotationModeSelect ? annotationModeSelect.value !== "off" : true);
4022
+ setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
4023
+
4024
+ setEditorView(editorView);
4025
+ setRightView(rightView);
4026
+ renderSourcePreview();
4027
+ connect();
4028
+ } catch (error) {
4029
+ hardFail("Studio UI init failed", error);
4030
+ }
4031
+ })();
4032
+