pi-studio-opencode 0.1.0

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.
Files changed (60) hide show
  1. package/ARCHITECTURE.md +122 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/dist/demo-host-pi.d.ts +1 -0
  5. package/dist/demo-host-pi.js +71 -0
  6. package/dist/demo-host-pi.js.map +1 -0
  7. package/dist/demo-host.d.ts +1 -0
  8. package/dist/demo-host.js +154 -0
  9. package/dist/demo-host.js.map +1 -0
  10. package/dist/host-opencode-plugin.d.ts +52 -0
  11. package/dist/host-opencode-plugin.js +396 -0
  12. package/dist/host-opencode-plugin.js.map +1 -0
  13. package/dist/host-opencode.d.ts +154 -0
  14. package/dist/host-opencode.js +627 -0
  15. package/dist/host-opencode.js.map +1 -0
  16. package/dist/host-pi.d.ts +45 -0
  17. package/dist/host-pi.js +258 -0
  18. package/dist/host-pi.js.map +1 -0
  19. package/dist/install-config.d.ts +36 -0
  20. package/dist/install-config.js +136 -0
  21. package/dist/install-config.js.map +1 -0
  22. package/dist/install.d.ts +16 -0
  23. package/dist/install.js +168 -0
  24. package/dist/install.js.map +1 -0
  25. package/dist/launcher.d.ts +2 -0
  26. package/dist/launcher.js +124 -0
  27. package/dist/launcher.js.map +1 -0
  28. package/dist/main.d.ts +1 -0
  29. package/dist/main.js +732 -0
  30. package/dist/main.js.map +1 -0
  31. package/dist/mock-pi-session.d.ts +27 -0
  32. package/dist/mock-pi-session.js +138 -0
  33. package/dist/mock-pi-session.js.map +1 -0
  34. package/dist/open-browser.d.ts +1 -0
  35. package/dist/open-browser.js +29 -0
  36. package/dist/open-browser.js.map +1 -0
  37. package/dist/opencode-plugin.d.ts +3 -0
  38. package/dist/opencode-plugin.js +326 -0
  39. package/dist/opencode-plugin.js.map +1 -0
  40. package/dist/prototype-pdf.d.ts +12 -0
  41. package/dist/prototype-pdf.js +991 -0
  42. package/dist/prototype-pdf.js.map +1 -0
  43. package/dist/prototype-server.d.ts +88 -0
  44. package/dist/prototype-server.js +1002 -0
  45. package/dist/prototype-server.js.map +1 -0
  46. package/dist/prototype-theme.d.ts +36 -0
  47. package/dist/prototype-theme.js +1471 -0
  48. package/dist/prototype-theme.js.map +1 -0
  49. package/dist/studio-core.d.ts +63 -0
  50. package/dist/studio-core.js +251 -0
  51. package/dist/studio-core.js.map +1 -0
  52. package/dist/studio-host-types.d.ts +50 -0
  53. package/dist/studio-host-types.js +14 -0
  54. package/dist/studio-host-types.js.map +1 -0
  55. package/examples/opencode/INSTALL.md +67 -0
  56. package/examples/opencode/opencode.local-path.jsonc +16 -0
  57. package/package.json +68 -0
  58. package/static/prototype.css +1277 -0
  59. package/static/prototype.html +173 -0
  60. package/static/prototype.js +3198 -0
@@ -0,0 +1,3198 @@
1
+ const BRAILLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
+ const BOOT_CONFIG = typeof window !== "undefined" && window.__PI_STUDIO_OPENCODE_BOOT__ && typeof window.__PI_STUDIO_OPENCODE_BOOT__ === "object"
3
+ ? window.__PI_STUDIO_OPENCODE_BOOT__
4
+ : {};
5
+ const STUDIO_ACCESS_TOKEN = typeof BOOT_CONFIG.token === "string" ? BOOT_CONFIG.token : "";
6
+
7
+ const state = {
8
+ snapshot: null,
9
+ stableCurrentModel: null,
10
+ selectedPromptId: null,
11
+ activePane: "left",
12
+ paneFocusTarget: "off",
13
+ busy: false,
14
+ followLatest: true,
15
+ diagnosticsOpen: false,
16
+ rightView: "preview",
17
+ editorOriginLabel: "studio editor",
18
+ sourcePath: null,
19
+ workingDir: "",
20
+ editorHighlightEnabled: true,
21
+ responseHighlightEnabled: true,
22
+ editorLanguage: "markdown",
23
+ annotationsEnabled: true,
24
+ editorHighlightRenderRaf: null,
25
+ lastLoadedIntoEditorNormalized: "",
26
+ windowHasFocus: typeof document !== "undefined" && typeof document.hasFocus === "function" ? document.hasFocus() : true,
27
+ titleAttentionMessage: "",
28
+ titleAttentionTimer: null,
29
+ lastAppliedDocumentTitle: "",
30
+ initialSnapshotLoaded: false,
31
+ lastCompletedTurnKey: "",
32
+ transientStatus: null,
33
+ transientStatusTimer: null,
34
+ spinnerTimer: null,
35
+ spinnerFrameIndex: 0,
36
+ responsePreviewRenderNonce: 0,
37
+ responsePreviewTimer: null,
38
+ currentRenderedPreviewKey: "",
39
+ pendingResponseScrollReset: false,
40
+ lastResponseIdentityKey: "",
41
+ snapshotPollTimer: null,
42
+ snapshotPollInFlight: false,
43
+ pdfExportInProgress: false,
44
+ };
45
+
46
+ const MATHJAX_CDN_URL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
47
+ const PDFJS_CDN_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/legacy/build/pdf.min.mjs";
48
+ const PDFJS_WORKER_CDN_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/legacy/build/pdf.worker.min.mjs";
49
+ const MATHJAX_UNAVAILABLE_MESSAGE = "Math fallback unavailable. Some unsupported equations may remain as raw TeX.";
50
+ const MATHJAX_RENDER_FAIL_MESSAGE = "Math fallback could not render some unsupported equations.";
51
+ const PDF_PREVIEW_UNAVAILABLE_MESSAGE = "PDF figure preview unavailable. Inline PDF rendering is not supported in this browser environment.";
52
+ const PDF_PREVIEW_RENDER_FAIL_MESSAGE = "PDF figure preview could not be rendered.";
53
+ let mathJaxPromise = null;
54
+ let pdfJsPromise = null;
55
+
56
+ const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
57
+ const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
58
+ const EDITOR_HIGHLIGHT_STORAGE_KEY = "studioPrototype.editorHighlightEnabled";
59
+ const RESPONSE_HIGHLIGHT_STORAGE_KEY = "studioPrototype.responseHighlightEnabled";
60
+ const EDITOR_LANGUAGE_STORAGE_KEY = "studioPrototype.editorLanguage";
61
+ const ANNOTATION_MODE_STORAGE_KEY = "studioPrototype.annotationsEnabled";
62
+ const EMPTY_OVERLAY_LINE = "\u200b";
63
+ const ANNOTATION_MARKER_REGEX = /\[an:\s*([^\]]+?)\]/gi;
64
+ const LANG_EXT_MAP = {
65
+ markdown: { label: "Markdown", exts: ["md", "markdown", "mdx", "qmd"] },
66
+ javascript: { label: "JavaScript", exts: ["js", "mjs", "cjs", "jsx"] },
67
+ typescript: { label: "TypeScript", exts: ["ts", "mts", "cts", "tsx"] },
68
+ python: { label: "Python", exts: ["py", "pyw"] },
69
+ bash: { label: "Bash", exts: ["sh", "bash", "zsh"] },
70
+ json: { label: "JSON", exts: ["json", "jsonc", "json5"] },
71
+ rust: { label: "Rust", exts: ["rs"] },
72
+ c: { label: "C", exts: ["c", "h"] },
73
+ cpp: { label: "C++", exts: ["cpp", "cxx", "cc", "hpp", "hxx"] },
74
+ julia: { label: "Julia", exts: ["jl"] },
75
+ fortran: { label: "Fortran", exts: ["f90", "f95", "f03", "f", "for"] },
76
+ r: { label: "R", exts: ["r"] },
77
+ matlab: { label: "MATLAB", exts: ["m"] },
78
+ latex: { label: "LaTeX", exts: ["tex", "latex"] },
79
+ diff: { label: "Diff", exts: ["diff", "patch"] },
80
+ html: { label: "HTML", exts: ["html", "htm"] },
81
+ css: { label: "CSS", exts: ["css"] },
82
+ xml: { label: "XML", exts: ["xml"] },
83
+ yaml: { label: "YAML", exts: ["yaml", "yml"] },
84
+ toml: { label: "TOML", exts: ["toml"] },
85
+ lua: { label: "Lua", exts: ["lua"] },
86
+ text: { label: "Plain Text", exts: ["txt", "rst", "adoc"] },
87
+ };
88
+ const EXT_TO_LANG = {};
89
+ Object.keys(LANG_EXT_MAP).forEach((lang) => {
90
+ LANG_EXT_MAP[lang].exts.forEach((ext) => {
91
+ EXT_TO_LANG[ext.toLowerCase()] = lang;
92
+ });
93
+ });
94
+ const HIGHLIGHTED_LANGUAGES = ["markdown", "javascript", "typescript", "python", "bash", "json", "rust", "c", "cpp", "julia", "fortran", "r", "matlab", "latex", "diff"];
95
+ const SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
96
+
97
+ const elements = {
98
+ leftPane: document.getElementById("leftPane"),
99
+ rightPane: document.getElementById("rightPane"),
100
+ leftFocusBtn: document.getElementById("leftFocusBtn"),
101
+ rightFocusBtn: document.getElementById("rightFocusBtn"),
102
+ saveAsBtn: document.getElementById("saveAsBtn"),
103
+ saveBtn: document.getElementById("saveBtn"),
104
+ loadFileBtn: document.getElementById("loadFileBtn"),
105
+ refreshBtn: document.getElementById("refreshBtn"),
106
+ diagnosticsBtn: document.getElementById("diagnosticsBtn"),
107
+ sourceBadge: document.getElementById("sourceBadge"),
108
+ resourceDirBtn: document.getElementById("resourceDirBtn"),
109
+ resourceDirLabel: document.getElementById("resourceDirLabel"),
110
+ syncBadge: document.getElementById("syncBadge"),
111
+ queueBadge: document.getElementById("queueBadge"),
112
+ composerStatusBadge: document.getElementById("composerStatusBadge"),
113
+ backendStatusBadge: document.getElementById("backendStatusBadge"),
114
+ historyCountBadge: document.getElementById("historyCountBadge"),
115
+ insertHeaderBtn: document.getElementById("insertHeaderBtn"),
116
+ annotationModeSelect: document.getElementById("annotationModeSelect"),
117
+ stripAnnotationsBtn: document.getElementById("stripAnnotationsBtn"),
118
+ saveAnnotatedBtn: document.getElementById("saveAnnotatedBtn"),
119
+ highlightSelect: document.getElementById("highlightSelect"),
120
+ langSelect: document.getElementById("langSelect"),
121
+ sourceHighlight: document.getElementById("sourceHighlight"),
122
+ promptInput: document.getElementById("promptInput"),
123
+ rightViewSelect: document.getElementById("rightViewSelect"),
124
+ responseHighlightSelect: document.getElementById("responseHighlightSelect"),
125
+ runBtn: document.getElementById("runBtn"),
126
+ queueBtn: document.getElementById("queueBtn"),
127
+ copyDraftBtn: document.getElementById("copyDraftBtn"),
128
+ referenceBadge: document.getElementById("referenceBadge"),
129
+ responseView: document.getElementById("responseView"),
130
+ responseText: document.getElementById("responseText"),
131
+ followSelect: document.getElementById("followSelect"),
132
+ historyPrevBtn: document.getElementById("historyPrevBtn"),
133
+ historyNextBtn: document.getElementById("historyNextBtn"),
134
+ historyLastBtn: document.getElementById("historyLastBtn"),
135
+ historyIndexBadge: document.getElementById("historyIndexBadge"),
136
+ loadResponseBtn: document.getElementById("loadResponseBtn"),
137
+ loadHistoryPromptBtn: document.getElementById("loadHistoryPromptBtn"),
138
+ copyResponseBtn: document.getElementById("copyResponseBtn"),
139
+ exportPdfBtn: document.getElementById("exportPdfBtn"),
140
+ diagnosticsPanel: document.getElementById("diagnosticsPanel"),
141
+ activeTurnPanel: document.getElementById("activeTurnPanel"),
142
+ lastTurnPanel: document.getElementById("lastTurnPanel"),
143
+ selectionPanel: document.getElementById("selectionPanel"),
144
+ historyList: document.getElementById("historyList"),
145
+ logSummary: document.getElementById("logSummary"),
146
+ logOutput: document.getElementById("logOutput"),
147
+ statusLine: document.getElementById("statusLine"),
148
+ statusSpinner: document.getElementById("statusSpinner"),
149
+ status: document.getElementById("status"),
150
+ footerMeta: document.getElementById("footerMeta"),
151
+ footerMetaText: document.getElementById("footerMetaText"),
152
+ };
153
+
154
+ function getStudioThemeInfo() {
155
+ return BOOT_CONFIG && typeof BOOT_CONFIG.theme === "object" && BOOT_CONFIG.theme ? BOOT_CONFIG.theme : null;
156
+ }
157
+
158
+ function buildAuthenticatedPath(path) {
159
+ const url = new URL(path, window.location.origin);
160
+ if (STUDIO_ACCESS_TOKEN) {
161
+ url.searchParams.set("token", STUDIO_ACCESS_TOKEN);
162
+ }
163
+ return `${url.pathname}${url.search}`;
164
+ }
165
+
166
+ function buildAuthenticatedHeaders(init = undefined) {
167
+ const headers = new Headers(init || {});
168
+ if (STUDIO_ACCESS_TOKEN) {
169
+ headers.set("X-PI-STUDIO-TOKEN", STUDIO_ACCESS_TOKEN);
170
+ }
171
+ return headers;
172
+ }
173
+
174
+ function normalizedText(value) {
175
+ return String(value || "").replace(/\r\n/g, "\n").trim();
176
+ }
177
+
178
+ function getHistory() {
179
+ return Array.isArray(state.snapshot?.history) ? state.snapshot.history : [];
180
+ }
181
+
182
+ function getLatestHistoryItem() {
183
+ const history = getHistory();
184
+ return history.length ? history.at(-1) : null;
185
+ }
186
+
187
+ function ensureSelectedHistoryItem() {
188
+ const history = getHistory();
189
+ if (!history.length) {
190
+ state.selectedPromptId = null;
191
+ return;
192
+ }
193
+ if (state.followLatest) {
194
+ state.selectedPromptId = history.at(-1).localPromptId;
195
+ return;
196
+ }
197
+ if (!history.some((item) => item.localPromptId === state.selectedPromptId)) {
198
+ state.selectedPromptId = history.at(-1).localPromptId;
199
+ }
200
+ }
201
+
202
+ function getSelectedHistoryItem() {
203
+ ensureSelectedHistoryItem();
204
+ const history = getHistory();
205
+ if (!history.length) return null;
206
+ return history.find((item) => item.localPromptId === state.selectedPromptId) || history.at(-1) || null;
207
+ }
208
+
209
+ function getSelectedHistoryIndex() {
210
+ const history = getHistory();
211
+ if (!history.length) return -1;
212
+ return history.findIndex((item) => item.localPromptId === state.selectedPromptId);
213
+ }
214
+
215
+ function groupHistoryByChain(history) {
216
+ const groups = new Map();
217
+ for (const item of history) {
218
+ const key = String(item.chainIndex);
219
+ if (!groups.has(key)) {
220
+ groups.set(key, { chainIndex: item.chainIndex, items: [] });
221
+ }
222
+ groups.get(key).items.push(item);
223
+ }
224
+ return Array.from(groups.values()).sort((a, b) => b.chainIndex - a.chainIndex);
225
+ }
226
+
227
+ function formatRelativeDuration(start, end) {
228
+ if (!start || !end || end < start) return "-";
229
+ const ms = end - start;
230
+ if (ms < 1000) return `${ms} ms`;
231
+ return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)} s`;
232
+ }
233
+
234
+ function formatAbsoluteTime(value) {
235
+ return value ? new Date(value).toLocaleTimeString() : "-";
236
+ }
237
+
238
+ function formatReferenceTime(value) {
239
+ if (!value) return "";
240
+ try {
241
+ return new Date(value).toLocaleTimeString([], {
242
+ hour: "2-digit",
243
+ minute: "2-digit",
244
+ second: "2-digit",
245
+ });
246
+ } catch {
247
+ return "";
248
+ }
249
+ }
250
+
251
+ function formatProviderLabel(providerID) {
252
+ const raw = String(providerID || "").trim().toLowerCase();
253
+ if (!raw) return "";
254
+ const known = {
255
+ openai: "OpenAI",
256
+ anthropic: "Anthropic",
257
+ google: "Google",
258
+ "github-copilot": "GitHub Copilot",
259
+ opencode: "OpenCode",
260
+ "opencode-go": "OpenCode Go",
261
+ togetherai: "TogetherAI",
262
+ };
263
+ return known[raw] || providerID;
264
+ }
265
+
266
+ function formatModel(snapshot) {
267
+ const model = snapshot?.currentModel;
268
+ if (!model || !model.providerID || !model.modelID) return "-";
269
+ return `${model.providerID}/${model.modelID}`;
270
+ }
271
+
272
+ function formatModelSummary(snapshot) {
273
+ const model = snapshot?.currentModel;
274
+ if (!model || !model.providerID || !model.modelID) return "Model: unknown";
275
+ const parts = [model.modelID];
276
+ const providerLabel = formatProviderLabel(model.providerID);
277
+ if (providerLabel) parts.push(providerLabel);
278
+ const variant = String(model.variant || "").trim();
279
+ if (variant) parts.push(variant);
280
+ return `Model: ${parts.join(" · ")}`;
281
+ }
282
+
283
+ function formatAgentLabel(snapshot) {
284
+ const raw = String(snapshot?.currentModel?.agent || "").trim();
285
+ if (!raw) return "";
286
+ return raw.replace(/[-_]+/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase());
287
+ }
288
+
289
+ function getTokenUsageTotal(snapshot) {
290
+ return getTokenUsageTotalValue(snapshot?.currentModel?.tokenUsage);
291
+ }
292
+
293
+ function formatCompactTokenCount(value) {
294
+ const count = Number(value);
295
+ if (!Number.isFinite(count) || count < 0) return "-";
296
+ if (count < 1000) return `${Math.round(count)}`;
297
+ if (count < 10_000) return `${(count / 1000).toFixed(1)}k`;
298
+ if (count < 1_000_000) return `${Math.round(count / 1000)}k`;
299
+ return `${(count / 1_000_000).toFixed(count < 10_000_000 ? 1 : 0)}M`;
300
+ }
301
+
302
+ function formatContextUsage(snapshot) {
303
+ const total = getTokenUsageTotal(snapshot);
304
+ if (total == null) return "Context: unknown";
305
+ const limit = snapshot?.currentModel?.contextLimit;
306
+ if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
307
+ const percent = Math.max(0, Math.round((total / limit) * 100));
308
+ return `Context: ${formatCompactTokenCount(total)} / ${formatCompactTokenCount(limit)} (${percent}%)`;
309
+ }
310
+ return `Context: ${formatCompactTokenCount(total)} tokens`;
311
+ }
312
+
313
+ function formatSessionLabel(snapshot) {
314
+ const sessionId = String(snapshot?.state?.sessionId || "").trim();
315
+ const sessionTitle = String(snapshot?.state?.sessionTitle || "").trim();
316
+ const useTitle = sessionTitle && !/^Studio host\b/i.test(sessionTitle) && sessionTitle !== "π Studio" && sessionTitle !== "Studio";
317
+ if (useTitle && sessionId) {
318
+ return `${sessionTitle} (${sessionId})`;
319
+ }
320
+ if (useTitle) {
321
+ return sessionTitle;
322
+ }
323
+ return sessionId;
324
+ }
325
+
326
+ function formatProjectLabel(snapshot) {
327
+ const directory = String(snapshot?.launchContext?.directory || "").trim();
328
+ if (!directory) return "";
329
+ const normalized = directory.replace(/[\\/]+$/, "");
330
+ const parts = normalized.split(/[\\/]/).filter(Boolean);
331
+ return parts.length ? parts[parts.length - 1] : normalized;
332
+ }
333
+
334
+ function cloneTokenUsage(usage) {
335
+ if (!usage || typeof usage !== "object") return undefined;
336
+ const clone = {};
337
+ if (typeof usage.total === "number") clone.total = usage.total;
338
+ if (typeof usage.input === "number") clone.input = usage.input;
339
+ if (typeof usage.output === "number") clone.output = usage.output;
340
+ if (typeof usage.reasoning === "number") clone.reasoning = usage.reasoning;
341
+ return Object.keys(clone).length ? clone : undefined;
342
+ }
343
+
344
+ function cloneModelSnapshot(model) {
345
+ if (!model || typeof model !== "object") return null;
346
+ return {
347
+ ...model,
348
+ tokenUsage: cloneTokenUsage(model.tokenUsage),
349
+ };
350
+ }
351
+
352
+ function getTokenUsageTotalValue(usage) {
353
+ if (!usage || typeof usage !== "object") return null;
354
+ if (typeof usage.total === "number" && Number.isFinite(usage.total) && usage.total >= 0) {
355
+ return usage.total;
356
+ }
357
+ const parts = [usage.input, usage.output, usage.reasoning].filter((value) => typeof value === "number" && Number.isFinite(value) && value >= 0);
358
+ if (!parts.length) return null;
359
+ return parts.reduce((sum, value) => sum + value, 0);
360
+ }
361
+
362
+ function hasMeaningfulTokenUsage(usage) {
363
+ const total = getTokenUsageTotalValue(usage);
364
+ return total != null && total > 0;
365
+ }
366
+
367
+ function modelSnapshotKey(model) {
368
+ const providerID = String(model?.providerID || "").trim();
369
+ const modelID = String(model?.modelID || "").trim();
370
+ return providerID && modelID ? `${providerID}/${modelID}` : "";
371
+ }
372
+
373
+ function mergeSnapshotForDisplay(snapshot) {
374
+ if (!snapshot || typeof snapshot !== "object") return snapshot;
375
+
376
+ const stable = cloneModelSnapshot(state.stableCurrentModel);
377
+ const incoming = cloneModelSnapshot(snapshot.currentModel);
378
+
379
+ if (incoming) {
380
+ const sameModel = Boolean(stable) && modelSnapshotKey(stable) === modelSnapshotKey(incoming);
381
+ if (sameModel) {
382
+ incoming.agent = incoming.agent || stable.agent;
383
+ incoming.variant = incoming.variant || stable.variant;
384
+ incoming.contextLimit = incoming.contextLimit || stable.contextLimit;
385
+
386
+ const running = snapshot?.state?.runState === "running" || snapshot?.state?.runState === "stopping";
387
+ const incomingTotal = getTokenUsageTotalValue(incoming.tokenUsage);
388
+ const stableTotal = getTokenUsageTotalValue(stable.tokenUsage);
389
+ const shouldKeepStableUsage = running
390
+ ? (hasMeaningfulTokenUsage(stable.tokenUsage) && (incomingTotal == null || incomingTotal <= 0 || incomingTotal < stableTotal))
391
+ : !hasMeaningfulTokenUsage(incoming.tokenUsage);
392
+
393
+ if (shouldKeepStableUsage) {
394
+ incoming.tokenUsage = stable.tokenUsage;
395
+ }
396
+ }
397
+ snapshot.currentModel = incoming;
398
+ state.stableCurrentModel = cloneModelSnapshot(incoming);
399
+ return snapshot;
400
+ }
401
+
402
+ if (stable) {
403
+ snapshot.currentModel = stable;
404
+ }
405
+ return snapshot;
406
+ }
407
+
408
+ function buildBaseDocumentTitle() {
409
+ const projectLabel = formatProjectLabel(state.snapshot);
410
+ return projectLabel
411
+ ? `πₒ Studio · ${projectLabel}`
412
+ : "πₒ Studio · OpenCode";
413
+ }
414
+
415
+ function shouldShowTitleAttention() {
416
+ const focused = typeof document !== "undefined" && typeof document.hasFocus === "function"
417
+ ? document.hasFocus()
418
+ : state.windowHasFocus;
419
+ return Boolean(typeof document !== "undefined" && document.hidden) || !focused;
420
+ }
421
+
422
+ function getComputedDocumentTitle() {
423
+ const baseTitle = buildBaseDocumentTitle();
424
+ return state.titleAttentionMessage
425
+ ? `${state.titleAttentionMessage} · ${baseTitle}`
426
+ : baseTitle;
427
+ }
428
+
429
+ function stopTitleAttentionTimer() {
430
+ if (!state.titleAttentionTimer) return;
431
+ window.clearInterval(state.titleAttentionTimer);
432
+ state.titleAttentionTimer = null;
433
+ }
434
+
435
+ function syncTitleAttentionTimer() {
436
+ if (!state.titleAttentionMessage || !shouldShowTitleAttention()) {
437
+ stopTitleAttentionTimer();
438
+ return;
439
+ }
440
+ if (state.titleAttentionTimer) return;
441
+ state.titleAttentionTimer = window.setInterval(() => {
442
+ if (!state.titleAttentionMessage || !shouldShowTitleAttention()) {
443
+ stopTitleAttentionTimer();
444
+ return;
445
+ }
446
+ const title = getComputedDocumentTitle();
447
+ if (document.title !== title) {
448
+ document.title = title;
449
+ }
450
+ state.lastAppliedDocumentTitle = title;
451
+ }, 1500);
452
+ }
453
+
454
+ function updateDocumentTitle(force = false) {
455
+ const title = getComputedDocumentTitle();
456
+ if (force || state.lastAppliedDocumentTitle !== title) {
457
+ document.title = title;
458
+ state.lastAppliedDocumentTitle = title;
459
+ }
460
+ syncTitleAttentionTimer();
461
+ }
462
+
463
+ function clearTitleAttention() {
464
+ if (!state.titleAttentionMessage) {
465
+ stopTitleAttentionTimer();
466
+ updateDocumentTitle();
467
+ return;
468
+ }
469
+ state.titleAttentionMessage = "";
470
+ stopTitleAttentionTimer();
471
+ updateDocumentTitle(true);
472
+ }
473
+
474
+ function armTitleAttention(message) {
475
+ const nextMessage = String(message || "").trim();
476
+ if (!nextMessage) return;
477
+ state.titleAttentionMessage = nextMessage;
478
+ updateDocumentTitle(true);
479
+ }
480
+
481
+ function getCompletedTurnKey(turn) {
482
+ if (!turn) return "";
483
+ const localPromptId = String(turn.localPromptId || "").trim();
484
+ const completedAt = typeof turn.completedAt === "number" && Number.isFinite(turn.completedAt)
485
+ ? turn.completedAt
486
+ : 0;
487
+ if (localPromptId && completedAt > 0) return `${localPromptId}:${completedAt}`;
488
+ if (localPromptId) return localPromptId;
489
+ if (completedAt > 0) return `completed:${completedAt}`;
490
+ return "";
491
+ }
492
+
493
+ function maybeArmCompletionTitleAttention(snapshot, { initial = false } = {}) {
494
+ const nextKey = getCompletedTurnKey(snapshot?.lastCompletedTurn);
495
+ if (initial || !state.initialSnapshotLoaded) {
496
+ state.initialSnapshotLoaded = true;
497
+ state.lastCompletedTurnKey = nextKey;
498
+ return;
499
+ }
500
+
501
+ if (!nextKey || nextKey === state.lastCompletedTurnKey) {
502
+ state.lastCompletedTurnKey = nextKey;
503
+ return;
504
+ }
505
+
506
+ state.lastCompletedTurnKey = nextKey;
507
+ if (!shouldShowTitleAttention()) return;
508
+ armTitleAttention("● Response ready");
509
+ }
510
+
511
+ function applySnapshot(snapshot, options = {}) {
512
+ state.snapshot = mergeSnapshotForDisplay(snapshot);
513
+ maybeArmCompletionTitleAttention(state.snapshot, options);
514
+ }
515
+
516
+ function getHistoryPromptButtonLabel(item) {
517
+ if (!item) return "Load response prompt into editor";
518
+ if (item.promptMode === "steer") return "Load effective prompt into editor";
519
+ if (item.promptMode === "run") return "Load run prompt into editor";
520
+ return "Load response prompt into editor";
521
+ }
522
+
523
+ function getHistoryPromptLoadedStatus(item) {
524
+ if (!item) return "Prompt unavailable for the selected response.";
525
+ if (item.promptMode === "steer") return "Loaded effective prompt into editor.";
526
+ if (item.promptMode === "run") return "Loaded run prompt into editor.";
527
+ return "Loaded response prompt into editor.";
528
+ }
529
+
530
+ function getHistoryPromptSourceStateLabel(item) {
531
+ if (!item) return "response prompt";
532
+ if (item.promptMode === "steer") return "effective prompt";
533
+ if (item.promptMode === "run") return "run prompt";
534
+ return "response prompt";
535
+ }
536
+
537
+ function getHistoryItemTitle(item) {
538
+ if (!item) return "Response";
539
+ if (item.promptMode === "run") return "Run";
540
+ if (item.promptMode === "steer") return `Steer ${item.promptSteeringCount}`;
541
+ return "Response";
542
+ }
543
+
544
+ function readStoredToggle(storageKey) {
545
+ if (!window.localStorage) return null;
546
+ try {
547
+ const value = window.localStorage.getItem(storageKey);
548
+ if (value === "on") return true;
549
+ if (value === "off") return false;
550
+ return null;
551
+ } catch {
552
+ return null;
553
+ }
554
+ }
555
+
556
+ function persistStoredToggle(storageKey, enabled) {
557
+ if (!window.localStorage) return;
558
+ try {
559
+ window.localStorage.setItem(storageKey, enabled ? "on" : "off");
560
+ } catch {}
561
+ }
562
+
563
+ function readStoredEditorLanguage() {
564
+ if (!window.localStorage) return null;
565
+ try {
566
+ const value = window.localStorage.getItem(EDITOR_LANGUAGE_STORAGE_KEY);
567
+ if (value && SUPPORTED_LANGUAGES.includes(value)) return value;
568
+ return null;
569
+ } catch {
570
+ return null;
571
+ }
572
+ }
573
+
574
+ function persistEditorLanguage(lang) {
575
+ if (!window.localStorage) return;
576
+ try {
577
+ window.localStorage.setItem(EDITOR_LANGUAGE_STORAGE_KEY, lang || "markdown");
578
+ } catch {}
579
+ }
580
+
581
+ function preferredExtensionForLanguage(lang) {
582
+ const entry = LANG_EXT_MAP[lang];
583
+ return entry && entry.exts && entry.exts.length ? entry.exts[0] : "md";
584
+ }
585
+
586
+ function populateLanguageOptions() {
587
+ if (!elements.langSelect || elements.langSelect.options.length > 0) return;
588
+ for (const lang of SUPPORTED_LANGUAGES) {
589
+ const option = document.createElement("option");
590
+ option.value = lang;
591
+ option.textContent = `Lang: ${LANG_EXT_MAP[lang].label}`;
592
+ elements.langSelect.appendChild(option);
593
+ }
594
+ }
595
+
596
+ function normalizeFenceLanguage(info) {
597
+ const raw = String(info || "").trim();
598
+ if (!raw) return "";
599
+ const first = raw.split(/\s+/)[0].replace(/^\./, "").toLowerCase();
600
+ if (first === "js" || first === "javascript" || first === "jsx" || first === "node") return "javascript";
601
+ if (first === "ts" || first === "typescript" || first === "tsx") return "typescript";
602
+ if (first === "py" || first === "python") return "python";
603
+ if (first === "sh" || first === "bash" || first === "zsh" || first === "shell") return "bash";
604
+ if (first === "json" || first === "jsonc") return "json";
605
+ if (first === "rust" || first === "rs") return "rust";
606
+ if (first === "c" || first === "h") return "c";
607
+ if (first === "cpp" || first === "c++" || first === "cxx" || first === "hpp") return "cpp";
608
+ if (first === "julia" || first === "jl") return "julia";
609
+ if (first === "fortran" || first === "f90" || first === "f95" || first === "f03" || first === "f" || first === "for") return "fortran";
610
+ if (first === "r") return "r";
611
+ if (first === "matlab" || first === "m") return "matlab";
612
+ if (first === "latex" || first === "tex") return "latex";
613
+ if (first === "diff" || first === "patch" || first === "udiff") return "diff";
614
+ return EXT_TO_LANG[first] || "";
615
+ }
616
+
617
+ function detectLanguageFromName(name) {
618
+ if (!name) return "";
619
+ const dot = name.lastIndexOf(".");
620
+ if (dot < 0) return "";
621
+ return EXT_TO_LANG[name.slice(dot + 1).toLowerCase()] || "";
622
+ }
623
+
624
+ function escapeHtml(value) {
625
+ return String(value || "")
626
+ .replace(/&/g, "&amp;")
627
+ .replace(/</g, "&lt;")
628
+ .replace(/>/g, "&gt;")
629
+ .replace(/\"/g, "&quot;")
630
+ .replace(/'/g, "&#39;");
631
+ }
632
+
633
+ function wrapHighlight(className, text) {
634
+ return `<span class="${className}">${escapeHtml(String(text || ""))}</span>`;
635
+ }
636
+
637
+ function highlightInlineAnnotations(text) {
638
+ const source = String(text || "");
639
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
640
+ let lastIndex = 0;
641
+ let out = "";
642
+ let match;
643
+ while ((match = ANNOTATION_MARKER_REGEX.exec(source)) !== null) {
644
+ const token = match[0] || "";
645
+ const start = typeof match.index === "number" ? match.index : 0;
646
+ if (start > lastIndex) {
647
+ out += escapeHtml(source.slice(lastIndex, start));
648
+ }
649
+ out += wrapHighlight(state.annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
650
+ lastIndex = start + token.length;
651
+ if (token.length === 0) ANNOTATION_MARKER_REGEX.lastIndex += 1;
652
+ }
653
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
654
+ if (lastIndex < source.length) {
655
+ out += escapeHtml(source.slice(lastIndex));
656
+ }
657
+ return out;
658
+ }
659
+
660
+ function highlightInlineMarkdown(text) {
661
+ const source = String(text || "");
662
+ const pattern = /(\x60[^\x60]*\x60)|(\[[^\]]+\]\([^)]+\))|(\[an:\s*[^\]]+\])/gi;
663
+ let lastIndex = 0;
664
+ let out = "";
665
+ let match;
666
+ while ((match = pattern.exec(source)) !== null) {
667
+ const token = match[0] || "";
668
+ const start = typeof match.index === "number" ? match.index : 0;
669
+ if (start > lastIndex) {
670
+ out += escapeHtml(source.slice(lastIndex, start));
671
+ }
672
+ if (match[1]) {
673
+ out += wrapHighlight("hl-code", token);
674
+ } else if (match[2]) {
675
+ const linkMatch = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
676
+ if (linkMatch) {
677
+ out += wrapHighlight("hl-link", `[${linkMatch[1]}]`);
678
+ out += `(${wrapHighlight("hl-url", linkMatch[2])})`;
679
+ } else {
680
+ out += escapeHtml(token);
681
+ }
682
+ } else if (match[3]) {
683
+ out += highlightInlineAnnotations(token);
684
+ } else {
685
+ out += escapeHtml(token);
686
+ }
687
+ lastIndex = start + token.length;
688
+ }
689
+ if (lastIndex < source.length) {
690
+ out += escapeHtml(source.slice(lastIndex));
691
+ }
692
+ return out;
693
+ }
694
+
695
+ function highlightCodeTokens(line, pattern, classifyMatch) {
696
+ const source = String(line || "");
697
+ let out = "";
698
+ let lastIndex = 0;
699
+ pattern.lastIndex = 0;
700
+ let match;
701
+ while ((match = pattern.exec(source)) !== null) {
702
+ const token = match[0] || "";
703
+ const start = typeof match.index === "number" ? match.index : 0;
704
+ if (start > lastIndex) {
705
+ out += escapeHtml(source.slice(lastIndex, start));
706
+ }
707
+ out += wrapHighlight(classifyMatch(match) || "hl-code", token);
708
+ lastIndex = start + token.length;
709
+ if (token.length === 0) pattern.lastIndex += 1;
710
+ }
711
+ if (lastIndex < source.length) {
712
+ out += escapeHtml(source.slice(lastIndex));
713
+ }
714
+ return out;
715
+ }
716
+
717
+ function highlightCodeLine(line, language) {
718
+ const source = String(line || "");
719
+ const lang = normalizeFenceLanguage(language);
720
+ if (!lang) return wrapHighlight("hl-code", source);
721
+
722
+ if (lang === "javascript" || lang === "typescript") {
723
+ const pattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\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;
724
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] ? "hl-code-kw" : m[4] ? "hl-code-num" : "hl-code")}</span>`;
725
+ }
726
+
727
+ if (lang === "python") {
728
+ const pattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\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;
729
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] ? "hl-code-kw" : m[4] ? "hl-code-num" : "hl-code")}</span>`;
730
+ }
731
+
732
+ if (lang === "bash") {
733
+ const pattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'[^']*')|(\$\{[^}]+\}|\$[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;
734
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] ? "hl-code-var" : m[4] ? "hl-code-kw" : m[5] ? "hl-code-num" : "hl-code")}</span>`;
735
+ }
736
+
737
+ if (lang === "json") {
738
+ const pattern = /("(?:[^"\\]|\\.)*"\s*:)|("(?:[^"\\]|\\.)*")|(\b(?:true|false|null)\b)|(\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/g;
739
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-key" : m[2] ? "hl-code-str" : m[3] ? "hl-code-kw" : m[4] ? "hl-code-num" : "hl-code")}</span>`;
740
+ }
741
+
742
+ if (lang === "rust") {
743
+ const pattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*")|(\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;
744
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] ? "hl-code-kw" : m[4] ? "hl-code-num" : "hl-code")}</span>`;
745
+ }
746
+
747
+ if (lang === "c" || lang === "cpp") {
748
+ const pattern = /(\/\/.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)')|(#\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|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;
749
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] || m[4] ? "hl-code-kw" : m[5] ? "hl-code-num" : "hl-code")}</span>`;
750
+ }
751
+
752
+ if (lang === "julia") {
753
+ const pattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\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;
754
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] ? "hl-code-kw" : m[4] ? "hl-code-num" : "hl-code")}</span>`;
755
+ }
756
+
757
+ if (lang === "fortran") {
758
+ const pattern = /(!.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\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;
759
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] ? "hl-code-kw" : m[4] ? "hl-code-num" : "hl-code")}</span>`;
760
+ }
761
+
762
+ if (lang === "r") {
763
+ const pattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\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;
764
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] || m[4] ? "hl-code-kw" : m[5] ? "hl-code-num" : "hl-code")}</span>`;
765
+ }
766
+
767
+ if (lang === "matlab") {
768
+ const pattern = /(%.*$)|('(?:[^']|'')*'|"(?:[^"\\]|\\.)*")|(\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;
769
+ return `<span class="hl-code">${highlightCodeTokens(source, pattern, (m) => m[1] ? "hl-code-com" : m[2] ? "hl-code-str" : m[3] ? "hl-code-kw" : m[4] ? "hl-code-num" : "hl-code")}</span>`;
770
+ }
771
+
772
+ if (lang === "latex") {
773
+ const pattern = /(%.*$)|(\[an:\s*[^\]]+\])|(\\(?: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]+)|(\{|\})|(\$\$?(?:[^$\\]|\\.)+\$\$?)|(\[(?:.*?)\])/gi;
774
+ let out = "";
775
+ let lastIndex = 0;
776
+ let match;
777
+ pattern.lastIndex = 0;
778
+ while ((match = pattern.exec(source)) !== null) {
779
+ const token = match[0] || "";
780
+ const start = typeof match.index === "number" ? match.index : 0;
781
+ if (start > lastIndex) out += escapeHtml(source.slice(lastIndex, start));
782
+ if (match[1]) out += wrapHighlight("hl-code-com", token);
783
+ else if (match[2]) out += highlightInlineAnnotations(token);
784
+ else if (match[3]) out += wrapHighlight("hl-code-kw", token);
785
+ else if (match[4]) out += wrapHighlight("hl-code-fn", token);
786
+ else if (match[5]) out += wrapHighlight("hl-code-op", token);
787
+ else if (match[6]) out += wrapHighlight("hl-code-str", token);
788
+ else if (match[7]) out += wrapHighlight("hl-code-num", token);
789
+ else out += escapeHtml(token);
790
+ lastIndex = start + token.length;
791
+ if (token.length === 0) pattern.lastIndex += 1;
792
+ }
793
+ if (lastIndex < source.length) out += escapeHtml(source.slice(lastIndex));
794
+ return out;
795
+ }
796
+
797
+ if (lang === "diff") {
798
+ const highlightedDiff = highlightInlineAnnotations(source);
799
+ if (/^@@/.test(source)) return `<span class="hl-code-fn">${highlightedDiff}</span>`;
800
+ if (/^\+\+\+|^---/.test(source)) return `<span class="hl-code-kw">${highlightedDiff}</span>`;
801
+ if (/^\+/.test(source)) return `<span class="hl-diff-add">${highlightedDiff}</span>`;
802
+ if (/^-/.test(source)) return `<span class="hl-diff-del">${highlightedDiff}</span>`;
803
+ if (/^diff /.test(source)) return `<span class="hl-code-kw">${highlightedDiff}</span>`;
804
+ if (/^index /.test(source)) return `<span class="hl-code-com">${highlightedDiff}</span>`;
805
+ return highlightedDiff;
806
+ }
807
+
808
+ return wrapHighlight("hl-code", source);
809
+ }
810
+
811
+ function highlightMarkdown(text) {
812
+ const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
813
+ const out = [];
814
+ let inFence = false;
815
+ let fenceChar = null;
816
+ let fenceLength = 0;
817
+ let fenceLanguage = "";
818
+
819
+ for (const line of lines) {
820
+ const fenceMatch = line.match(/^(\s*)([`]{3,}|~{3,})(.*)$/);
821
+ if (fenceMatch) {
822
+ const marker = fenceMatch[2] || "";
823
+ const markerChar = marker.charAt(0);
824
+ const markerLength = marker.length;
825
+ if (!inFence) {
826
+ inFence = true;
827
+ fenceChar = markerChar;
828
+ fenceLength = markerLength;
829
+ fenceLanguage = normalizeFenceLanguage(fenceMatch[3] || "");
830
+ } else if (fenceChar === markerChar && markerLength >= fenceLength) {
831
+ inFence = false;
832
+ fenceChar = null;
833
+ fenceLength = 0;
834
+ fenceLanguage = "";
835
+ }
836
+ out.push(wrapHighlight("hl-fence", line));
837
+ continue;
838
+ }
839
+
840
+ if (inFence) {
841
+ out.push(line.length > 0 ? highlightCodeLine(line, fenceLanguage) : EMPTY_OVERLAY_LINE);
842
+ continue;
843
+ }
844
+
845
+ if (line.length === 0) {
846
+ out.push(EMPTY_OVERLAY_LINE);
847
+ continue;
848
+ }
849
+
850
+ const headingMatch = line.match(/^(\s{0,3})(#{1,6}\s+)(.*)$/);
851
+ if (headingMatch) {
852
+ out.push(escapeHtml(headingMatch[1] || "") + wrapHighlight("hl-heading", (headingMatch[2] || "") + (headingMatch[3] || "")));
853
+ continue;
854
+ }
855
+
856
+ const quoteMatch = line.match(/^(\s{0,3}>\s?)(.*)$/);
857
+ if (quoteMatch) {
858
+ out.push(wrapHighlight("hl-quote", quoteMatch[1] || "") + highlightInlineMarkdown(quoteMatch[2] || ""));
859
+ continue;
860
+ }
861
+
862
+ const listMatch = line.match(/^(\s*)([-*+]|\d+\.)(\s+)(.*)$/);
863
+ if (listMatch) {
864
+ out.push(
865
+ escapeHtml(listMatch[1] || "")
866
+ + wrapHighlight("hl-list", listMatch[2] || "")
867
+ + escapeHtml(listMatch[3] || "")
868
+ + highlightInlineMarkdown(listMatch[4] || ""),
869
+ );
870
+ continue;
871
+ }
872
+
873
+ out.push(highlightInlineMarkdown(line));
874
+ }
875
+
876
+ return out.join("<br>");
877
+ }
878
+
879
+ function highlightCode(text, language) {
880
+ const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
881
+ const lang = normalizeFenceLanguage(language);
882
+ const out = [];
883
+ for (const line of lines) {
884
+ if (line.length === 0) out.push(EMPTY_OVERLAY_LINE);
885
+ else if (lang) out.push(highlightCodeLine(line, lang));
886
+ else out.push(escapeHtml(line));
887
+ }
888
+ return out.join("<br>");
889
+ }
890
+
891
+ function syncEditorHighlightScroll() {
892
+ if (!elements.sourceHighlight) return;
893
+ elements.sourceHighlight.scrollTop = elements.promptInput.scrollTop;
894
+ elements.sourceHighlight.scrollLeft = elements.promptInput.scrollLeft;
895
+ }
896
+
897
+ function renderEditorHighlightNow() {
898
+ if (!elements.sourceHighlight) return;
899
+ if (!state.editorHighlightEnabled) {
900
+ elements.sourceHighlight.innerHTML = "";
901
+ return;
902
+ }
903
+ const text = elements.promptInput.value || "";
904
+ if (text.length > EDITOR_HIGHLIGHT_MAX_CHARS) {
905
+ elements.sourceHighlight.textContent = text;
906
+ syncEditorHighlightScroll();
907
+ return;
908
+ }
909
+ elements.sourceHighlight.innerHTML = state.editorLanguage === "markdown"
910
+ ? highlightMarkdown(text)
911
+ : highlightCode(text, state.editorLanguage);
912
+ syncEditorHighlightScroll();
913
+ }
914
+
915
+ function scheduleEditorHighlightRender() {
916
+ if (state.editorHighlightRenderRaf !== null) {
917
+ if (typeof window.cancelAnimationFrame === "function") {
918
+ window.cancelAnimationFrame(state.editorHighlightRenderRaf);
919
+ } else {
920
+ window.clearTimeout(state.editorHighlightRenderRaf);
921
+ }
922
+ state.editorHighlightRenderRaf = null;
923
+ }
924
+ const schedule = typeof window.requestAnimationFrame === "function"
925
+ ? window.requestAnimationFrame.bind(window)
926
+ : (cb) => window.setTimeout(cb, 16);
927
+ state.editorHighlightRenderRaf = schedule(() => {
928
+ state.editorHighlightRenderRaf = null;
929
+ renderEditorHighlightNow();
930
+ });
931
+ }
932
+
933
+ function setEditorHighlightEnabled(enabled) {
934
+ state.editorHighlightEnabled = Boolean(enabled);
935
+ persistStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY, state.editorHighlightEnabled);
936
+ if (elements.highlightSelect) {
937
+ elements.highlightSelect.value = state.editorHighlightEnabled ? "on" : "off";
938
+ }
939
+ if (elements.sourceHighlight) {
940
+ elements.sourceHighlight.hidden = !state.editorHighlightEnabled;
941
+ }
942
+ elements.promptInput.classList.toggle("highlight-active", state.editorHighlightEnabled);
943
+ if (state.editorHighlightEnabled) {
944
+ scheduleEditorHighlightRender();
945
+ } else if (elements.sourceHighlight) {
946
+ elements.sourceHighlight.innerHTML = "";
947
+ elements.sourceHighlight.scrollTop = 0;
948
+ elements.sourceHighlight.scrollLeft = 0;
949
+ }
950
+ }
951
+
952
+ function setEditorLanguage(lang) {
953
+ state.editorLanguage = SUPPORTED_LANGUAGES.includes(lang) ? lang : "markdown";
954
+ persistEditorLanguage(state.editorLanguage);
955
+ if (elements.langSelect) {
956
+ elements.langSelect.value = state.editorLanguage;
957
+ }
958
+ if (state.editorHighlightEnabled) {
959
+ scheduleEditorHighlightRender();
960
+ }
961
+ }
962
+
963
+ function setResponseHighlightEnabled(enabled) {
964
+ state.responseHighlightEnabled = Boolean(enabled);
965
+ persistStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY, state.responseHighlightEnabled);
966
+ if (elements.responseHighlightSelect) {
967
+ elements.responseHighlightSelect.value = state.responseHighlightEnabled ? "on" : "off";
968
+ }
969
+ }
970
+
971
+ function setAnnotationsEnabled(enabled) {
972
+ state.annotationsEnabled = Boolean(enabled);
973
+ persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, state.annotationsEnabled);
974
+ if (elements.annotationModeSelect) {
975
+ elements.annotationModeSelect.value = state.annotationsEnabled ? "on" : "off";
976
+ }
977
+ if (state.editorHighlightEnabled) {
978
+ scheduleEditorHighlightRender();
979
+ }
980
+ state.currentRenderedPreviewKey = "";
981
+ }
982
+
983
+ function buildPlainMarkdownHtml(markdown) {
984
+ return `<pre class="plain-markdown">${escapeHtml(String(markdown || ""))}</pre>`;
985
+ }
986
+
987
+ function buildPreviewErrorHtml(message, markdown) {
988
+ return `<div class="preview-error">${escapeHtml(String(message || "Preview rendering failed."))}</div>${buildPlainMarkdownHtml(markdown)}`;
989
+ }
990
+
991
+ function sanitizeRenderedHtml(html, markdown) {
992
+ const rawHtml = typeof html === "string" ? html : "";
993
+ const mathAnnotationStripped = rawHtml
994
+ .replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
995
+ .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
996
+
997
+ if (window.DOMPurify && typeof window.DOMPurify.sanitize === "function") {
998
+ return window.DOMPurify.sanitize(mathAnnotationStripped, {
999
+ USE_PROFILES: {
1000
+ html: true,
1001
+ mathMl: true,
1002
+ svg: true,
1003
+ },
1004
+ ADD_TAGS: ["embed"],
1005
+ ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align"],
1006
+ ADD_DATA_URI_TAGS: ["embed"],
1007
+ });
1008
+ }
1009
+
1010
+ return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
1011
+ }
1012
+
1013
+ function isPdfPreviewSource(src) {
1014
+ return Boolean(src) && (/^data:application\/pdf(?:;|,)/i.test(src) || /\.pdf(?:$|[?#])/i.test(src));
1015
+ }
1016
+
1017
+ function decoratePdfEmbeds(targetEl) {
1018
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") {
1019
+ return;
1020
+ }
1021
+
1022
+ const embeds = targetEl.querySelectorAll("embed[src]");
1023
+ embeds.forEach((embedEl) => {
1024
+ const src = typeof embedEl.getAttribute === "function" ? (embedEl.getAttribute("src") || "") : "";
1025
+ if (!isPdfPreviewSource(src)) {
1026
+ return;
1027
+ }
1028
+ if (!embedEl.getAttribute("type")) {
1029
+ embedEl.setAttribute("type", "application/pdf");
1030
+ }
1031
+ if (!embedEl.getAttribute("title")) {
1032
+ embedEl.setAttribute("title", "Embedded PDF figure");
1033
+ }
1034
+ });
1035
+ }
1036
+
1037
+ function decodePdfDataUri(src) {
1038
+ const match = String(src || "").match(/^data:application\/pdf(?:;[^,]*)?,([A-Za-z0-9+/=\s]+)$/i);
1039
+ if (!match) return null;
1040
+ const payload = (match[1] || "").replace(/\s+/g, "");
1041
+ if (!payload) return null;
1042
+ const binary = window.atob(payload);
1043
+ const bytes = new Uint8Array(binary.length);
1044
+ for (let i = 0; i < binary.length; i += 1) {
1045
+ bytes[i] = binary.charCodeAt(i);
1046
+ }
1047
+ return bytes;
1048
+ }
1049
+
1050
+ function ensurePdfJs() {
1051
+ if (window.pdfjsLib && typeof window.pdfjsLib.getDocument === "function") {
1052
+ return Promise.resolve(window.pdfjsLib);
1053
+ }
1054
+ if (pdfJsPromise) {
1055
+ return pdfJsPromise;
1056
+ }
1057
+
1058
+ pdfJsPromise = import(PDFJS_CDN_URL)
1059
+ .then((module) => {
1060
+ const api = module && typeof module.getDocument === "function"
1061
+ ? module
1062
+ : (module && module.default && typeof module.default.getDocument === "function" ? module.default : null);
1063
+ if (!api || typeof api.getDocument !== "function") {
1064
+ throw new Error("pdf.js did not initialize.");
1065
+ }
1066
+ if (api.GlobalWorkerOptions && !api.GlobalWorkerOptions.workerSrc) {
1067
+ api.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_CDN_URL;
1068
+ }
1069
+ window.pdfjsLib = api;
1070
+ return api;
1071
+ })
1072
+ .catch((error) => {
1073
+ pdfJsPromise = null;
1074
+ throw error;
1075
+ });
1076
+
1077
+ return pdfJsPromise;
1078
+ }
1079
+
1080
+ function appendPdfPreviewNotice(targetEl, message) {
1081
+ if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
1082
+ return;
1083
+ }
1084
+ if (targetEl.querySelector(".preview-pdf-warning")) {
1085
+ return;
1086
+ }
1087
+ const warningEl = document.createElement("div");
1088
+ warningEl.className = "preview-warning preview-pdf-warning";
1089
+ warningEl.textContent = String(message || PDF_PREVIEW_UNAVAILABLE_MESSAGE);
1090
+ targetEl.appendChild(warningEl);
1091
+ }
1092
+
1093
+ async function loadPdfDocumentSource(src) {
1094
+ const embedded = decodePdfDataUri(src);
1095
+ if (embedded) {
1096
+ return { data: embedded };
1097
+ }
1098
+ const response = await fetch(src);
1099
+ if (!response.ok) {
1100
+ throw new Error("Failed to fetch PDF figure for preview.");
1101
+ }
1102
+ const bytes = new Uint8Array(await response.arrayBuffer());
1103
+ return { data: bytes };
1104
+ }
1105
+
1106
+ async function renderSinglePdfPreviewEmbed(embedEl, pdfjsLib) {
1107
+ if (!embedEl || embedEl.dataset.studioPdfPreviewRendered === "1") {
1108
+ return false;
1109
+ }
1110
+
1111
+ const src = embedEl.getAttribute("src") || "";
1112
+ if (!isPdfPreviewSource(src)) {
1113
+ return false;
1114
+ }
1115
+
1116
+ const measuredWidth = Math.max(1, Math.round(embedEl.getBoundingClientRect().width || 0));
1117
+ const styleText = embedEl.getAttribute("style") || "";
1118
+ const widthAttr = embedEl.getAttribute("width") || "";
1119
+ const figAlign = embedEl.getAttribute("data-fig-align") || "";
1120
+ const pdfSource = await loadPdfDocumentSource(src);
1121
+ const loadingTask = pdfjsLib.getDocument(pdfSource);
1122
+ const pdfDocument = await loadingTask.promise;
1123
+
1124
+ try {
1125
+ const page = await pdfDocument.getPage(1);
1126
+ const baseViewport = page.getViewport({ scale: 1 });
1127
+ const cssWidth = Math.max(1, measuredWidth || Math.round(baseViewport.width));
1128
+ const renderScale = Math.max(0.25, cssWidth / baseViewport.width) * Math.min(window.devicePixelRatio || 1, 2);
1129
+ const viewport = page.getViewport({ scale: renderScale });
1130
+ const canvas = document.createElement("canvas");
1131
+ const context = canvas.getContext("2d", { alpha: false });
1132
+ if (!context) {
1133
+ throw new Error("Canvas 2D context unavailable.");
1134
+ }
1135
+
1136
+ canvas.width = Math.max(1, Math.ceil(viewport.width));
1137
+ canvas.height = Math.max(1, Math.ceil(viewport.height));
1138
+ canvas.style.width = "100%";
1139
+ canvas.style.height = "auto";
1140
+ canvas.setAttribute("aria-label", "PDF figure preview");
1141
+
1142
+ await page.render({
1143
+ canvasContext: context,
1144
+ viewport,
1145
+ }).promise;
1146
+
1147
+ const wrapper = document.createElement("div");
1148
+ wrapper.className = "studio-pdf-preview";
1149
+ if (styleText) {
1150
+ wrapper.style.cssText = styleText;
1151
+ } else if (widthAttr) {
1152
+ wrapper.style.width = /^\d+(?:\.\d+)?$/.test(widthAttr) ? (widthAttr + "px") : widthAttr;
1153
+ } else {
1154
+ wrapper.style.width = "100%";
1155
+ }
1156
+ if (figAlign) {
1157
+ wrapper.setAttribute("data-fig-align", figAlign);
1158
+ }
1159
+ wrapper.title = "PDF figure preview (page 1)";
1160
+ wrapper.appendChild(canvas);
1161
+ embedEl.dataset.studioPdfPreviewRendered = "1";
1162
+ embedEl.replaceWith(wrapper);
1163
+ return true;
1164
+ } finally {
1165
+ if (typeof pdfDocument.cleanup === "function") {
1166
+ try { pdfDocument.cleanup(); } catch {}
1167
+ }
1168
+ if (typeof pdfDocument.destroy === "function") {
1169
+ try { await pdfDocument.destroy(); } catch {}
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ async function renderPdfPreviewsInElement(targetEl) {
1175
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") {
1176
+ return;
1177
+ }
1178
+
1179
+ const embeds = Array.from(targetEl.querySelectorAll("embed[src]"))
1180
+ .filter((embedEl) => isPdfPreviewSource(embedEl.getAttribute("src") || ""));
1181
+ if (embeds.length === 0) {
1182
+ return;
1183
+ }
1184
+
1185
+ let pdfjsLib;
1186
+ try {
1187
+ pdfjsLib = await ensurePdfJs();
1188
+ } catch (error) {
1189
+ console.error("pdf.js load failed:", error);
1190
+ appendPdfPreviewNotice(targetEl, PDF_PREVIEW_UNAVAILABLE_MESSAGE);
1191
+ return;
1192
+ }
1193
+
1194
+ let hadFailure = false;
1195
+ for (const embedEl of embeds) {
1196
+ try {
1197
+ await renderSinglePdfPreviewEmbed(embedEl, pdfjsLib);
1198
+ } catch (error) {
1199
+ hadFailure = true;
1200
+ console.error("PDF preview render failed:", error);
1201
+ }
1202
+ }
1203
+
1204
+ if (hadFailure) {
1205
+ appendPdfPreviewNotice(targetEl, PDF_PREVIEW_RENDER_FAIL_MESSAGE);
1206
+ }
1207
+ }
1208
+
1209
+ function applyAnnotationMarkersToElement(targetEl, mode) {
1210
+ if (!targetEl || mode === "none") return;
1211
+ if (typeof document.createTreeWalker !== "function") return;
1212
+
1213
+ const walker = document.createTreeWalker(targetEl, NodeFilter.SHOW_TEXT);
1214
+ const textNodes = [];
1215
+ let node = walker.nextNode();
1216
+ while (node) {
1217
+ const textNode = node;
1218
+ const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
1219
+ if (value && value.toLowerCase().includes("[an:")) {
1220
+ const parent = textNode.parentElement;
1221
+ const tag = parent && parent.tagName ? parent.tagName.toUpperCase() : "";
1222
+ if (!["CODE", "PRE", "SCRIPT", "STYLE", "TEXTAREA"].includes(tag)) {
1223
+ textNodes.push(textNode);
1224
+ }
1225
+ }
1226
+ node = walker.nextNode();
1227
+ }
1228
+
1229
+ for (const textNode of textNodes) {
1230
+ const text = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
1231
+ if (!text) continue;
1232
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1233
+ if (!ANNOTATION_MARKER_REGEX.test(text)) continue;
1234
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1235
+
1236
+ const fragment = document.createDocumentFragment();
1237
+ let lastIndex = 0;
1238
+ let match;
1239
+ while ((match = ANNOTATION_MARKER_REGEX.exec(text)) !== null) {
1240
+ const token = match[0] || "";
1241
+ const start = typeof match.index === "number" ? match.index : 0;
1242
+ if (start > lastIndex) {
1243
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex, start)));
1244
+ }
1245
+ if (mode === "highlight") {
1246
+ const markerEl = document.createElement("span");
1247
+ markerEl.className = "annotation-preview-marker";
1248
+ markerEl.textContent = typeof match[1] === "string" ? match[1].trim() : token;
1249
+ markerEl.title = token;
1250
+ fragment.appendChild(markerEl);
1251
+ }
1252
+ lastIndex = start + token.length;
1253
+ if (token.length === 0) ANNOTATION_MARKER_REGEX.lastIndex += 1;
1254
+ }
1255
+ if (lastIndex < text.length) {
1256
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
1257
+ }
1258
+ if (textNode.parentNode) {
1259
+ textNode.parentNode.replaceChild(fragment, textNode);
1260
+ }
1261
+ }
1262
+ }
1263
+
1264
+ function appendMathFallbackNotice(targetEl, message) {
1265
+ if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
1266
+ return;
1267
+ }
1268
+
1269
+ if (targetEl.querySelector(".preview-math-warning")) {
1270
+ return;
1271
+ }
1272
+
1273
+ const warningEl = document.createElement("div");
1274
+ warningEl.className = "preview-warning preview-math-warning";
1275
+ warningEl.textContent = String(message || MATHJAX_UNAVAILABLE_MESSAGE);
1276
+ targetEl.appendChild(warningEl);
1277
+ }
1278
+
1279
+ function extractMathFallbackTex(text, displayMode) {
1280
+ const source = typeof text === "string" ? text.trim() : "";
1281
+ if (!source) return "";
1282
+
1283
+ if (displayMode) {
1284
+ if (source.startsWith("$$") && source.endsWith("$$") && source.length >= 4) {
1285
+ return source.slice(2, -2).replace(/^\s+|\s+$/g, "");
1286
+ }
1287
+ if (source.startsWith("\\[") && source.endsWith("\\]") && source.length >= 4) {
1288
+ return source.slice(2, -2).replace(/^\s+|\s+$/g, "");
1289
+ }
1290
+ return source;
1291
+ }
1292
+
1293
+ if (source.startsWith("\\(") && source.endsWith("\\)") && source.length >= 4) {
1294
+ return source.slice(2, -2).trim();
1295
+ }
1296
+ if (source.startsWith("$") && source.endsWith("$") && source.length >= 2) {
1297
+ return source.slice(1, -1).trim();
1298
+ }
1299
+ return source;
1300
+ }
1301
+
1302
+ function collectMathFallbackTargets(targetEl) {
1303
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
1304
+
1305
+ const nodes = Array.from(targetEl.querySelectorAll(".math.display, .math.inline"));
1306
+ const targets = [];
1307
+ const seenTargets = new Set();
1308
+
1309
+ nodes.forEach((node) => {
1310
+ if (!node || !node.classList) return;
1311
+ const displayMode = node.classList.contains("display");
1312
+ const rawText = typeof node.textContent === "string" ? node.textContent : "";
1313
+ const tex = extractMathFallbackTex(rawText, displayMode);
1314
+ if (!tex) return;
1315
+
1316
+ let renderTarget = node;
1317
+ if (displayMode) {
1318
+ const parent = node.parentElement;
1319
+ const parentText = parent && typeof parent.textContent === "string" ? parent.textContent.trim() : "";
1320
+ if (parent && parent.tagName === "P" && parentText === rawText.trim()) {
1321
+ renderTarget = parent;
1322
+ }
1323
+ }
1324
+
1325
+ if (seenTargets.has(renderTarget)) return;
1326
+ seenTargets.add(renderTarget);
1327
+ targets.push({ node, renderTarget, displayMode, tex });
1328
+ });
1329
+
1330
+ return targets;
1331
+ }
1332
+
1333
+ function ensureMathJax() {
1334
+ if (window.MathJax && typeof window.MathJax.typesetPromise === "function") {
1335
+ return Promise.resolve(window.MathJax);
1336
+ }
1337
+
1338
+ if (mathJaxPromise) {
1339
+ return mathJaxPromise;
1340
+ }
1341
+
1342
+ mathJaxPromise = new Promise((resolve, reject) => {
1343
+ const globalMathJax = (window.MathJax && typeof window.MathJax === "object") ? window.MathJax : {};
1344
+ const texConfig = (globalMathJax.tex && typeof globalMathJax.tex === "object") ? globalMathJax.tex : {};
1345
+ const loaderConfig = (globalMathJax.loader && typeof globalMathJax.loader === "object") ? globalMathJax.loader : {};
1346
+ const startupConfig = (globalMathJax.startup && typeof globalMathJax.startup === "object") ? globalMathJax.startup : {};
1347
+ const optionsConfig = (globalMathJax.options && typeof globalMathJax.options === "object") ? globalMathJax.options : {};
1348
+ const loaderEntries = Array.isArray(loaderConfig.load) ? loaderConfig.load.slice() : [];
1349
+ ["[tex]/ams", "[tex]/noerrors", "[tex]/noundefined"].forEach((entry) => {
1350
+ if (loaderEntries.indexOf(entry) === -1) loaderEntries.push(entry);
1351
+ });
1352
+
1353
+ window.MathJax = Object.assign({}, globalMathJax, {
1354
+ loader: Object.assign({}, loaderConfig, {
1355
+ load: loaderEntries,
1356
+ }),
1357
+ tex: Object.assign({}, texConfig, {
1358
+ inlineMath: [["\\(", "\\)"], ["$", "$"]],
1359
+ displayMath: [["\\[", "\\]"], ["$$", "$$"]],
1360
+ packages: Object.assign({}, texConfig.packages || {}, { "[+]": ["ams", "noerrors", "noundefined"] }),
1361
+ }),
1362
+ options: Object.assign({}, optionsConfig, {
1363
+ skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
1364
+ }),
1365
+ startup: Object.assign({}, startupConfig, {
1366
+ typeset: false,
1367
+ }),
1368
+ });
1369
+
1370
+ const script = document.createElement("script");
1371
+ script.src = MATHJAX_CDN_URL;
1372
+ script.async = true;
1373
+ script.dataset.piStudioMathjax = "1";
1374
+ script.onload = () => {
1375
+ const api = window.MathJax;
1376
+ if (api && api.startup && api.startup.promise && typeof api.startup.promise.then === "function") {
1377
+ api.startup.promise.then(() => resolve(api)).catch(reject);
1378
+ return;
1379
+ }
1380
+ if (api && typeof api.typesetPromise === "function") {
1381
+ resolve(api);
1382
+ return;
1383
+ }
1384
+ reject(new Error("MathJax did not initialize."));
1385
+ };
1386
+ script.onerror = () => {
1387
+ reject(new Error("Failed to load MathJax."));
1388
+ };
1389
+ document.head.appendChild(script);
1390
+ }).catch((error) => {
1391
+ mathJaxPromise = null;
1392
+ throw error;
1393
+ });
1394
+
1395
+ return mathJaxPromise;
1396
+ }
1397
+
1398
+ async function renderMathFallbackInElement(targetEl) {
1399
+ const fallbackTargets = collectMathFallbackTargets(targetEl);
1400
+ if (fallbackTargets.length === 0) return;
1401
+
1402
+ fallbackTargets.forEach((entry) => {
1403
+ entry.renderTarget.classList.add("studio-mathjax-fallback");
1404
+ if (entry.displayMode) {
1405
+ entry.renderTarget.classList.add("studio-mathjax-fallback-display");
1406
+ entry.renderTarget.textContent = "\\[\n" + entry.tex + "\n\\]";
1407
+ } else {
1408
+ entry.renderTarget.textContent = "\\(" + entry.tex + "\\)";
1409
+ }
1410
+ });
1411
+
1412
+ let mathJax;
1413
+ try {
1414
+ mathJax = await ensureMathJax();
1415
+ } catch (error) {
1416
+ console.error("MathJax load failed:", error);
1417
+ appendMathFallbackNotice(targetEl, MATHJAX_UNAVAILABLE_MESSAGE);
1418
+ return;
1419
+ }
1420
+
1421
+ try {
1422
+ await mathJax.typesetPromise(fallbackTargets.map((entry) => entry.renderTarget));
1423
+ } catch (error) {
1424
+ console.error("MathJax fallback render failed:", error);
1425
+ appendMathFallbackNotice(targetEl, MATHJAX_RENDER_FAIL_MESSAGE);
1426
+ }
1427
+ }
1428
+
1429
+ async function renderMarkdownWithPandoc(markdown) {
1430
+ if (typeof fetch !== "function") {
1431
+ throw new Error("Browser fetch API is unavailable.");
1432
+ }
1433
+
1434
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
1435
+ const timeoutId = controller ? window.setTimeout(() => controller.abort(), 8000) : null;
1436
+
1437
+ let response;
1438
+ try {
1439
+ response = await fetch(buildAuthenticatedPath("/api/render-preview"), {
1440
+ method: "POST",
1441
+ headers: buildAuthenticatedHeaders({
1442
+ "Content-Type": "application/json",
1443
+ }),
1444
+ body: JSON.stringify({
1445
+ markdown: String(markdown || ""),
1446
+ sourcePath: state.sourcePath || "",
1447
+ resourceDir: (!state.sourcePath && state.workingDir) ? state.workingDir : "",
1448
+ }),
1449
+ signal: controller ? controller.signal : undefined,
1450
+ });
1451
+ } catch (error) {
1452
+ if (error && error.name === "AbortError") {
1453
+ throw new Error("Preview request timed out.");
1454
+ }
1455
+ throw error;
1456
+ } finally {
1457
+ if (timeoutId) {
1458
+ window.clearTimeout(timeoutId);
1459
+ }
1460
+ }
1461
+
1462
+ const rawBody = await response.text();
1463
+ let payload = null;
1464
+ try {
1465
+ payload = rawBody ? JSON.parse(rawBody) : null;
1466
+ } catch {
1467
+ payload = null;
1468
+ }
1469
+
1470
+ if (!response.ok) {
1471
+ const message = payload && typeof payload.error === "string"
1472
+ ? payload.error
1473
+ : `Preview request failed with HTTP ${response.status}.`;
1474
+ throw new Error(message);
1475
+ }
1476
+
1477
+ if (!payload || payload.ok !== true || typeof payload.html !== "string") {
1478
+ const message = payload && typeof payload.error === "string"
1479
+ ? payload.error
1480
+ : "Preview renderer returned an invalid payload.";
1481
+ throw new Error(message);
1482
+ }
1483
+
1484
+ return payload.html;
1485
+ }
1486
+
1487
+ function parseContentDispositionFilename(headerValue) {
1488
+ if (!headerValue || typeof headerValue !== "string") return "";
1489
+
1490
+ const utfMatch = headerValue.match(/filename\*=UTF-8''([^;]+)/i);
1491
+ if (utfMatch && utfMatch[1]) {
1492
+ try {
1493
+ return decodeURIComponent(utfMatch[1].trim());
1494
+ } catch {
1495
+ return utfMatch[1].trim();
1496
+ }
1497
+ }
1498
+
1499
+ const quotedMatch = headerValue.match(/filename="([^"]+)"/i);
1500
+ if (quotedMatch && quotedMatch[1]) return quotedMatch[1].trim();
1501
+
1502
+ const plainMatch = headerValue.match(/filename=([^;]+)/i);
1503
+ if (plainMatch && plainMatch[1]) return plainMatch[1].trim();
1504
+
1505
+ return "";
1506
+ }
1507
+
1508
+ function hasAnnotationMarkers(text) {
1509
+ const source = String(text || "");
1510
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1511
+ const hasMarker = ANNOTATION_MARKER_REGEX.test(source);
1512
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
1513
+ return hasMarker;
1514
+ }
1515
+
1516
+ function stripAnnotationMarkers(text) {
1517
+ return String(text || "").replace(ANNOTATION_MARKER_REGEX, "");
1518
+ }
1519
+
1520
+ function prepareEditorTextForSend(text) {
1521
+ const raw = String(text || "");
1522
+ return state.annotationsEnabled ? raw : stripAnnotationMarkers(raw);
1523
+ }
1524
+
1525
+ function prepareEditorTextForPreview(text) {
1526
+ const raw = String(text || "");
1527
+ return state.annotationsEnabled ? raw : stripAnnotationMarkers(raw);
1528
+ }
1529
+
1530
+ function beginPreviewRender(targetEl) {
1531
+ if (!targetEl) return;
1532
+ targetEl.classList.add("preview-pending");
1533
+ }
1534
+
1535
+ function finishPreviewRender(targetEl) {
1536
+ if (!targetEl) return;
1537
+ targetEl.classList.remove("preview-pending");
1538
+ }
1539
+
1540
+ function scheduleResponsePaneRepaintNudge() {
1541
+ if (!elements.responseView || typeof elements.responseView.getBoundingClientRect !== "function") return;
1542
+ const schedule = typeof window.requestAnimationFrame === "function"
1543
+ ? window.requestAnimationFrame.bind(window)
1544
+ : (cb) => window.setTimeout(cb, 16);
1545
+
1546
+ schedule(() => {
1547
+ if (!elements.responseView || !elements.responseView.isConnected) return;
1548
+ void elements.responseView.getBoundingClientRect();
1549
+ if (!elements.responseView.classList) return;
1550
+ elements.responseView.classList.add("response-repaint-nudge");
1551
+ schedule(() => {
1552
+ if (!elements.responseView || !elements.responseView.classList) return;
1553
+ elements.responseView.classList.remove("response-repaint-nudge");
1554
+ });
1555
+ });
1556
+ }
1557
+
1558
+ function applyPendingResponseScrollReset() {
1559
+ if (!state.pendingResponseScrollReset || !elements.responseView) return false;
1560
+ if (state.rightView === "editor-preview") return false;
1561
+ elements.responseView.scrollTop = 0;
1562
+ elements.responseView.scrollLeft = 0;
1563
+ state.pendingResponseScrollReset = false;
1564
+ return true;
1565
+ }
1566
+
1567
+ function getDisplayedResponseIdentityKey(display) {
1568
+ if (!display) return "";
1569
+ if (display.kind === "history" && display.item?.localPromptId) {
1570
+ return `response:${display.item.localPromptId}`;
1571
+ }
1572
+ if (display.kind === "active" && display.turn?.localPromptId) {
1573
+ return `response:${display.turn.localPromptId}`;
1574
+ }
1575
+ return "";
1576
+ }
1577
+
1578
+ function updatePendingResponseScrollReset(display) {
1579
+ if (state.rightView === "editor-preview") {
1580
+ state.pendingResponseScrollReset = false;
1581
+ return;
1582
+ }
1583
+
1584
+ const nextKey = getDisplayedResponseIdentityKey(display);
1585
+ if (!nextKey) {
1586
+ state.lastResponseIdentityKey = "";
1587
+ state.pendingResponseScrollReset = false;
1588
+ return;
1589
+ }
1590
+ if (state.lastResponseIdentityKey !== nextKey) {
1591
+ state.pendingResponseScrollReset = true;
1592
+ }
1593
+ state.lastResponseIdentityKey = nextKey;
1594
+ }
1595
+
1596
+ function getPreviewSource() {
1597
+ if (state.rightView === "editor-preview") {
1598
+ const markdown = prepareEditorTextForPreview(elements.promptInput.value || "");
1599
+ const latest = getLatestHistoryItem();
1600
+ const latestTime = formatReferenceTime(latest?.completedAt ?? latest?.submittedAt ?? 0);
1601
+ const suffix = latest
1602
+ ? (latestTime ? ` · response updated ${latestTime}` : " · response available")
1603
+ : "";
1604
+ return {
1605
+ mode: "editor-preview",
1606
+ markdown,
1607
+ emptyMessage: "Editor is empty.",
1608
+ key: `editor-preview\u0000${markdown}`,
1609
+ referenceLabel: `Previewing: editor text${suffix}`,
1610
+ previewWarning: "",
1611
+ };
1612
+ }
1613
+
1614
+ const display = getDisplayedResponse();
1615
+ const responseId = display.kind === "history"
1616
+ ? display.item?.localPromptId || "none"
1617
+ : (display.kind === "active" ? display.turn?.localPromptId || "active" : "none");
1618
+ const previewMarkdown = state.annotationsEnabled ? display.markdown : stripAnnotationMarkers(display.markdown);
1619
+ return {
1620
+ mode: "preview",
1621
+ markdown: previewMarkdown,
1622
+ emptyMessage: display.text,
1623
+ key: `preview\u0000${display.kind}\u0000${responseId}\u0000${previewMarkdown}\u0000${display.previewWarning || ""}`,
1624
+ referenceLabel: getResponseReferenceLabel(display),
1625
+ previewWarning: display.previewWarning || "",
1626
+ };
1627
+ }
1628
+
1629
+ function getPdfExportSource() {
1630
+ if (state.rightView !== "preview" && state.rightView !== "editor-preview") {
1631
+ return null;
1632
+ }
1633
+
1634
+ const previewSource = getPreviewSource();
1635
+ const markdown = String(previewSource?.markdown || "");
1636
+ if (!normalizedText(markdown)) {
1637
+ return null;
1638
+ }
1639
+
1640
+ const sourcePath = state.sourcePath || "";
1641
+ const resourceDir = (!sourcePath && state.workingDir) ? state.workingDir : "";
1642
+ const editorPdfLanguage = state.rightView === "editor-preview" ? String(state.editorLanguage || "") : "";
1643
+ const isLatex = state.rightView === "editor-preview"
1644
+ ? editorPdfLanguage === "latex"
1645
+ : /\\documentclass\b|\\begin\{document\}/.test(markdown);
1646
+
1647
+ let filenameHint = state.rightView === "editor-preview"
1648
+ ? "studio-editor-preview.pdf"
1649
+ : "studio-response-preview.pdf";
1650
+ if (sourcePath) {
1651
+ const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
1652
+ const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
1653
+ filenameHint = `${stem}-preview.pdf`;
1654
+ }
1655
+
1656
+ return {
1657
+ markdown,
1658
+ sourcePath,
1659
+ resourceDir,
1660
+ editorPdfLanguage,
1661
+ isLatex,
1662
+ filenameHint,
1663
+ };
1664
+ }
1665
+
1666
+ function setResponseViewHtml(html) {
1667
+ elements.responseView.innerHTML = html;
1668
+ }
1669
+
1670
+ function appendPreviewWarning(targetEl, message) {
1671
+ if (!targetEl || !message) return;
1672
+ const warningEl = document.createElement("div");
1673
+ warningEl.className = "preview-warning";
1674
+ warningEl.textContent = String(message);
1675
+ targetEl.append(warningEl);
1676
+ }
1677
+
1678
+ function cancelScheduledResponsePreviewRender() {
1679
+ if (!state.responsePreviewTimer) return;
1680
+ window.clearTimeout(state.responsePreviewTimer);
1681
+ state.responsePreviewTimer = null;
1682
+ }
1683
+
1684
+ function scheduleResponsePreviewRender(delayMs = 0) {
1685
+ cancelScheduledResponsePreviewRender();
1686
+ const delay = Math.max(0, delayMs);
1687
+ state.responsePreviewTimer = window.setTimeout(() => {
1688
+ state.responsePreviewTimer = null;
1689
+ void renderResponsePreviewNow();
1690
+ }, delay);
1691
+ }
1692
+
1693
+ async function renderResponsePreviewNow() {
1694
+ if (state.rightView !== "preview" && state.rightView !== "editor-preview") {
1695
+ return;
1696
+ }
1697
+
1698
+ const source = getPreviewSource();
1699
+ if (!source.markdown.trim()) {
1700
+ state.currentRenderedPreviewKey = source.key;
1701
+ finishPreviewRender(elements.responseView);
1702
+ setResponseViewHtml(buildPlainMarkdownHtml(source.emptyMessage));
1703
+ if (source.previewWarning) {
1704
+ appendPreviewWarning(elements.responseView, source.previewWarning);
1705
+ }
1706
+ applyPendingResponseScrollReset();
1707
+ scheduleResponsePaneRepaintNudge();
1708
+ elements.referenceBadge.textContent = source.referenceLabel;
1709
+ return;
1710
+ }
1711
+
1712
+ if (state.currentRenderedPreviewKey === source.key) {
1713
+ elements.referenceBadge.textContent = source.referenceLabel;
1714
+ return;
1715
+ }
1716
+
1717
+ const nonce = ++state.responsePreviewRenderNonce;
1718
+ beginPreviewRender(elements.responseView);
1719
+ elements.referenceBadge.textContent = source.referenceLabel;
1720
+
1721
+ try {
1722
+ const renderedHtml = await renderMarkdownWithPandoc(source.markdown);
1723
+ if (nonce !== state.responsePreviewRenderNonce || state.rightView !== source.mode) return;
1724
+ finishPreviewRender(elements.responseView);
1725
+ setResponseViewHtml(sanitizeRenderedHtml(renderedHtml, source.markdown));
1726
+ decoratePdfEmbeds(elements.responseView);
1727
+ await renderPdfPreviewsInElement(elements.responseView);
1728
+ applyAnnotationMarkersToElement(elements.responseView, state.annotationsEnabled ? "highlight" : "hide");
1729
+ await renderMathFallbackInElement(elements.responseView);
1730
+ if (source.previewWarning) {
1731
+ appendPreviewWarning(elements.responseView, source.previewWarning);
1732
+ }
1733
+ applyPendingResponseScrollReset();
1734
+ scheduleResponsePaneRepaintNudge();
1735
+ elements.referenceBadge.textContent = source.referenceLabel;
1736
+ state.currentRenderedPreviewKey = source.key;
1737
+ } catch (error) {
1738
+ if (nonce !== state.responsePreviewRenderNonce || state.rightView !== source.mode) return;
1739
+ const detail = error && error.message ? error.message : String(error || "unknown error");
1740
+ finishPreviewRender(elements.responseView);
1741
+ setResponseViewHtml(buildPreviewErrorHtml(`Preview renderer unavailable (${detail}). Showing plain markdown.`, source.markdown));
1742
+ if (source.previewWarning) {
1743
+ appendPreviewWarning(elements.responseView, source.previewWarning);
1744
+ }
1745
+ applyPendingResponseScrollReset();
1746
+ scheduleResponsePaneRepaintNudge();
1747
+ elements.referenceBadge.textContent = source.referenceLabel;
1748
+ state.currentRenderedPreviewKey = source.key;
1749
+ }
1750
+ }
1751
+
1752
+ function formatPromptDescriptor(turn) {
1753
+ if (!turn) return "-";
1754
+ return turn.promptMode === "run"
1755
+ ? `chain ${turn.chainIndex} · run`
1756
+ : `chain ${turn.chainIndex} · steer ${turn.promptSteeringCount}`;
1757
+ }
1758
+
1759
+ function buildResponseDisplayText(item) {
1760
+ if (!item) return "No response yet. Run editor text to generate a response.";
1761
+ const responseText = String(item.responseText || "");
1762
+ if (item.responseError) {
1763
+ return responseText.trim()
1764
+ ? `error: ${item.responseError}\n\n${responseText}`
1765
+ : `error: ${item.responseError}`;
1766
+ }
1767
+ return responseText.trim() ? responseText : "(empty response)";
1768
+ }
1769
+
1770
+ function getActiveTurnMarkdown(turn) {
1771
+ if (!turn) return "";
1772
+ return String(turn.outputPreview || turn.responseText || "");
1773
+ }
1774
+
1775
+ function getResponseReferenceLabel(display) {
1776
+ if (!display) return "Latest response: none";
1777
+ if (display.kind === "history" && display.item) {
1778
+ const selectedIndex = getSelectedHistoryIndex();
1779
+ const total = getHistory().length;
1780
+ const selectedLabel = total > 0 && selectedIndex >= 0 ? `${selectedIndex + 1}/${total}` : `0/${total}`;
1781
+ const item = display.item;
1782
+ const time = formatReferenceTime(item.completedAt ?? item.submittedAt ?? 0);
1783
+ return time
1784
+ ? `Response history ${selectedLabel} · assistant response · ${time}`
1785
+ : `Response history ${selectedLabel} · assistant response`;
1786
+ }
1787
+ if (display.kind === "active" && display.turn) {
1788
+ const time = formatReferenceTime(display.turn.firstOutputTextAt ?? display.turn.firstAssistantMessageAt ?? display.turn.submittedAt ?? 0);
1789
+ return time
1790
+ ? `Assistant response in progress · ${time}`
1791
+ : "Assistant response in progress";
1792
+ }
1793
+ return "Latest response: none";
1794
+ }
1795
+
1796
+ function getDisplayedResponse() {
1797
+ const activeTurn = state.followLatest ? state.snapshot?.activeTurn : null;
1798
+ const activeMarkdown = activeTurn ? getActiveTurnMarkdown(activeTurn) : "";
1799
+ const activeHasContent = Boolean(normalizedText(activeMarkdown));
1800
+
1801
+ if (activeTurn && activeHasContent) {
1802
+ return {
1803
+ kind: "active",
1804
+ text: activeMarkdown,
1805
+ markdown: activeMarkdown,
1806
+ hasContent: true,
1807
+ turn: activeTurn,
1808
+ previewWarning: "",
1809
+ };
1810
+ }
1811
+
1812
+ const selected = getSelectedHistoryItem();
1813
+ if (selected) {
1814
+ const markdown = String(selected.responseText || "");
1815
+ return {
1816
+ kind: "history",
1817
+ text: buildResponseDisplayText(selected),
1818
+ markdown,
1819
+ hasContent: Boolean(normalizedText(markdown)),
1820
+ previewWarning: selected.responseError ? `Response ended with error: ${selected.responseError}` : "",
1821
+ item: selected,
1822
+ };
1823
+ }
1824
+
1825
+ if (activeTurn) {
1826
+ return {
1827
+ kind: "active",
1828
+ text: "Waiting for the active turn to produce a response.",
1829
+ markdown: "",
1830
+ hasContent: false,
1831
+ turn: activeTurn,
1832
+ previewWarning: "",
1833
+ };
1834
+ }
1835
+
1836
+ return {
1837
+ kind: "empty",
1838
+ text: "No response yet. Run editor text to generate a response.",
1839
+ markdown: "",
1840
+ hasContent: false,
1841
+ previewWarning: "",
1842
+ };
1843
+ }
1844
+
1845
+ function appendMetaRow(container, label, value) {
1846
+ const row = document.createElement("div");
1847
+ row.className = "meta";
1848
+
1849
+ const labelNode = document.createElement("span");
1850
+ labelNode.textContent = `${label}: `;
1851
+
1852
+ const valueNode = document.createElement("strong");
1853
+ valueNode.textContent = value;
1854
+
1855
+ row.append(labelNode, valueNode);
1856
+ container.append(row);
1857
+ }
1858
+
1859
+ function buildDetailBlock(title, text) {
1860
+ const block = document.createElement("div");
1861
+ block.className = "detail-block";
1862
+
1863
+ const heading = document.createElement("h4");
1864
+ heading.textContent = title;
1865
+
1866
+ const body = document.createElement("pre");
1867
+ body.textContent = text || "";
1868
+
1869
+ block.append(heading, body);
1870
+ return block;
1871
+ }
1872
+
1873
+ function formatTurnSummary(turn) {
1874
+ if (!turn) return [];
1875
+ const now = state.snapshot?.now || Date.now();
1876
+ const effectiveEnd = turn.completedAt || now;
1877
+ return [
1878
+ ["Prompt", formatPromptDescriptor(turn)],
1879
+ ["Submitted", formatAbsoluteTime(turn.submittedAt)],
1880
+ ["To busy", formatRelativeDuration(turn.submittedAt, turn.backendBusyAt)],
1881
+ ["To first assistant", formatRelativeDuration(turn.submittedAt, turn.firstAssistantMessageAt)],
1882
+ ["To first output", formatRelativeDuration(turn.submittedAt, turn.firstOutputTextAt)],
1883
+ ["Elapsed", formatRelativeDuration(turn.submittedAt, effectiveEnd)],
1884
+ ["Latest message", turn.latestAssistantMessageId || "-"],
1885
+ ["Latest part", turn.latestPartType || "-"],
1886
+ ];
1887
+ }
1888
+
1889
+ function renderTurnPanel(container, turn, emptyText) {
1890
+ if (!turn) {
1891
+ container.textContent = emptyText;
1892
+ container.className = "panel-scroll diagnostics-body empty-state";
1893
+ return;
1894
+ }
1895
+
1896
+ const wrapper = document.createElement("div");
1897
+ wrapper.className = "detail-body";
1898
+
1899
+ const metaBlock = document.createElement("div");
1900
+ metaBlock.className = "detail-block";
1901
+ const heading = document.createElement("h4");
1902
+ heading.textContent = "Timing";
1903
+ metaBlock.append(heading);
1904
+ for (const [label, value] of formatTurnSummary(turn)) {
1905
+ appendMetaRow(metaBlock, label, value);
1906
+ }
1907
+
1908
+ wrapper.append(
1909
+ metaBlock,
1910
+ buildDetailBlock("Live output preview", turn.outputPreview || "(no output text observed yet)"),
1911
+ buildDetailBlock(
1912
+ "Completion",
1913
+ turn.completedAt
1914
+ ? `${turn.responseError ? `error: ${turn.responseError}` : "completed"}\n${turn.responseText || ""}`.trim()
1915
+ : "Still running or waiting for completion.",
1916
+ ),
1917
+ buildDetailBlock("Prompt text", turn.promptText),
1918
+ );
1919
+
1920
+ container.innerHTML = "";
1921
+ container.className = "panel-scroll diagnostics-body";
1922
+ container.append(wrapper);
1923
+ }
1924
+
1925
+ function renderSelectionPanel() {
1926
+ const item = getSelectedHistoryItem();
1927
+ if (!item) {
1928
+ elements.selectionPanel.textContent = "No response selected yet.";
1929
+ elements.selectionPanel.className = "panel-scroll diagnostics-body empty-state";
1930
+ return;
1931
+ }
1932
+
1933
+ const wrapper = document.createElement("div");
1934
+ wrapper.className = "detail-body";
1935
+
1936
+ const metaBlock = document.createElement("div");
1937
+ metaBlock.className = "detail-block";
1938
+ const metaHeading = document.createElement("h4");
1939
+ metaHeading.textContent = "Metadata";
1940
+ metaBlock.append(metaHeading);
1941
+ appendMetaRow(metaBlock, "chain", String(item.chainIndex));
1942
+ appendMetaRow(metaBlock, "mode", item.promptMode);
1943
+ appendMetaRow(metaBlock, "steering count", String(item.promptSteeringCount));
1944
+ appendMetaRow(metaBlock, "queued while busy", item.queuedWhileBusy ? "yes" : "no");
1945
+ appendMetaRow(metaBlock, "submitted", formatAbsoluteTime(item.submittedAt));
1946
+ appendMetaRow(metaBlock, "completed", item.completedAt ? formatAbsoluteTime(item.completedAt) : "-");
1947
+ appendMetaRow(metaBlock, "duration", item.completedAt ? formatRelativeDuration(item.submittedAt, item.completedAt) : "-");
1948
+ appendMetaRow(metaBlock, "user message", item.userMessageId || "-");
1949
+ appendMetaRow(metaBlock, "response message", item.responseMessageId || "-");
1950
+ appendMetaRow(metaBlock, "response error", item.responseError || "-");
1951
+
1952
+ wrapper.append(
1953
+ metaBlock,
1954
+ buildDetailBlock("Prompt text", item.promptText),
1955
+ buildDetailBlock("Effective prompt", item.effectivePrompt),
1956
+ buildDetailBlock("Response text", buildResponseDisplayText(item)),
1957
+ );
1958
+
1959
+ elements.selectionPanel.innerHTML = "";
1960
+ elements.selectionPanel.className = "panel-scroll diagnostics-body";
1961
+ elements.selectionPanel.append(wrapper);
1962
+ }
1963
+
1964
+ function renderHistoryDiagnostics() {
1965
+ const history = getHistory();
1966
+ if (!history.length) {
1967
+ elements.historyList.innerHTML = '<div class="empty-state">No responses yet.</div>';
1968
+ return;
1969
+ }
1970
+
1971
+ const groups = groupHistoryByChain(history);
1972
+ const fragment = document.createDocumentFragment();
1973
+
1974
+ for (const group of groups) {
1975
+ const section = document.createElement("section");
1976
+ section.className = "history-group";
1977
+
1978
+ const header = document.createElement("div");
1979
+ header.className = "history-group-header";
1980
+
1981
+ const title = document.createElement("strong");
1982
+ title.textContent = `Chain ${group.chainIndex}`;
1983
+
1984
+ const summary = document.createElement("span");
1985
+ const steerCount = group.items.filter((item) => item.promptMode === "steer").length;
1986
+ const latest = group.items.at(-1);
1987
+ summary.textContent = `${group.items.length} item${group.items.length === 1 ? "" : "s"} · ${steerCount} steer${steerCount === 1 ? "" : "s"} · latest ${latest ? formatAbsoluteTime(latest.completedAt ?? latest.submittedAt) : "-"}`;
1988
+
1989
+ header.append(title, summary);
1990
+
1991
+ const itemsWrap = document.createElement("div");
1992
+ itemsWrap.className = "history-group-items";
1993
+
1994
+ for (const item of group.items) {
1995
+ const card = document.createElement("div");
1996
+ card.className = `history-item${item.localPromptId === state.selectedPromptId ? " selected" : ""}`;
1997
+ card.addEventListener("click", () => {
1998
+ state.followLatest = false;
1999
+ state.selectedPromptId = item.localPromptId;
2000
+ render();
2001
+ });
2002
+
2003
+ const topRow = document.createElement("div");
2004
+ topRow.className = "row";
2005
+
2006
+ const itemTitle = document.createElement("strong");
2007
+ itemTitle.textContent = getHistoryItemTitle(item);
2008
+
2009
+ const time = document.createElement("span");
2010
+ time.className = "meta";
2011
+ time.textContent = formatAbsoluteTime(item.submittedAt);
2012
+
2013
+ topRow.append(itemTitle, time);
2014
+
2015
+ const badges = document.createElement("div");
2016
+ badges.className = "badges";
2017
+ for (const [text, className] of [
2018
+ [item.promptMode, item.promptMode],
2019
+ [`steers:${item.promptSteeringCount}`, ""],
2020
+ [item.queuedWhileBusy ? "queued-busy" : "direct", ""],
2021
+ [item.responseError ? "error" : "ok", item.responseError ? "error" : ""],
2022
+ ]) {
2023
+ const badge = document.createElement("span");
2024
+ badge.className = `badge ${className}`.trim();
2025
+ badge.textContent = text;
2026
+ badges.append(badge);
2027
+ }
2028
+
2029
+ const snippet = document.createElement("p");
2030
+ snippet.className = "snippet";
2031
+ snippet.textContent = normalizedText(item.responseText || item.responseError || item.promptText).replace(/\s+/g, " ").slice(0, 180) || "(empty response)";
2032
+
2033
+ const meta = document.createElement("div");
2034
+ meta.className = "meta";
2035
+ meta.textContent = `prompt=${item.localPromptId.slice(0, 12)} · response=${item.responseMessageId ? item.responseMessageId.slice(0, 12) : "-"}`;
2036
+
2037
+ card.append(topRow, badges, snippet, meta);
2038
+ itemsWrap.append(card);
2039
+ }
2040
+
2041
+ section.append(header, itemsWrap);
2042
+ fragment.append(section);
2043
+ }
2044
+
2045
+ elements.historyList.innerHTML = "";
2046
+ elements.historyList.append(fragment);
2047
+ }
2048
+
2049
+ function renderLogs() {
2050
+ const lines = Array.isArray(state.snapshot?.logs) ? state.snapshot.logs : [];
2051
+ elements.logSummary.textContent = `${lines.length} line${lines.length === 1 ? "" : "s"}`;
2052
+ elements.logOutput.textContent = lines
2053
+ .map((entry) => `[${formatAbsoluteTime(entry.at)}] ${entry.line}`)
2054
+ .join("\n");
2055
+ }
2056
+
2057
+ function renderDiagnostics() {
2058
+ elements.diagnosticsPanel.hidden = !state.diagnosticsOpen;
2059
+ elements.diagnosticsBtn.classList.toggle("active", state.diagnosticsOpen);
2060
+ if (!state.diagnosticsOpen) return;
2061
+ renderTurnPanel(elements.activeTurnPanel, state.snapshot?.activeTurn ?? null, "No active turn yet.");
2062
+ renderTurnPanel(elements.lastTurnPanel, state.snapshot?.lastCompletedTurn ?? null, "No completed turn yet.");
2063
+ renderSelectionPanel();
2064
+ renderHistoryDiagnostics();
2065
+ renderLogs();
2066
+ }
2067
+
2068
+ function renderResponsePane() {
2069
+ const history = getHistory();
2070
+ const selectedIndex = getSelectedHistoryIndex();
2071
+ const display = getDisplayedResponse();
2072
+ updatePendingResponseScrollReset(display);
2073
+
2074
+ if (elements.rightViewSelect) {
2075
+ elements.rightViewSelect.value = state.rightView;
2076
+ }
2077
+
2078
+ const selected = history.length && selectedIndex >= 0 ? selectedIndex + 1 : 0;
2079
+ elements.historyIndexBadge.textContent = `History: ${selected}/${history.length}`;
2080
+
2081
+ if (state.rightView === "markdown") {
2082
+ cancelScheduledResponsePreviewRender();
2083
+ finishPreviewRender(elements.responseView);
2084
+ state.responsePreviewRenderNonce += 1;
2085
+ const responseText = String(display.text || "");
2086
+ state.currentRenderedPreviewKey = `raw\u0000${state.responseHighlightEnabled ? "highlight" : "plain"}\u0000${display.kind}\u0000${responseText}`;
2087
+
2088
+ if (!normalizedText(responseText)) {
2089
+ setResponseViewHtml(buildPlainMarkdownHtml(responseText));
2090
+ } else if (state.responseHighlightEnabled) {
2091
+ if (responseText.length > RESPONSE_HIGHLIGHT_MAX_CHARS) {
2092
+ setResponseViewHtml(buildPreviewErrorHtml(
2093
+ "Response is too large for markdown highlighting. Showing plain markdown.",
2094
+ responseText,
2095
+ ));
2096
+ } else {
2097
+ setResponseViewHtml(`<div class="response-markdown-highlight">${highlightMarkdown(responseText)}</div>`);
2098
+ }
2099
+ } else {
2100
+ setResponseViewHtml(buildPlainMarkdownHtml(responseText));
2101
+ }
2102
+
2103
+ applyPendingResponseScrollReset();
2104
+ scheduleResponsePaneRepaintNudge();
2105
+ elements.referenceBadge.textContent = getResponseReferenceLabel(display);
2106
+ return;
2107
+ }
2108
+
2109
+ scheduleResponsePreviewRender(state.rightView === "editor-preview" ? 120 : 0);
2110
+ }
2111
+
2112
+ function renderEditorMeta() {
2113
+ const snapshot = state.snapshot;
2114
+ const history = getHistory();
2115
+ const selectedIndex = getSelectedHistoryIndex();
2116
+ const display = getDisplayedResponse();
2117
+ const editorTextNormalized = normalizedText(elements.promptInput.value);
2118
+ const displayedResponseNormalized = display?.hasContent ? normalizedText(display.markdown) : "";
2119
+ const inSync = Boolean(displayedResponseNormalized) && editorTextNormalized === displayedResponseNormalized;
2120
+
2121
+ elements.sourceBadge.textContent = `Editor origin: ${state.editorOriginLabel}`;
2122
+ if (elements.resourceDirBtn) {
2123
+ elements.resourceDirBtn.hidden = Boolean(state.sourcePath);
2124
+ }
2125
+ if (elements.resourceDirLabel) {
2126
+ elements.resourceDirLabel.hidden = Boolean(state.sourcePath) || !state.workingDir;
2127
+ elements.resourceDirLabel.textContent = state.workingDir ? `Working dir: ${state.workingDir}` : "";
2128
+ }
2129
+ elements.queueBadge.textContent = `Queue: ${snapshot?.state?.queueLength ?? 0}`;
2130
+ elements.composerStatusBadge.textContent = `Run state: ${snapshot?.state?.runState ?? "-"}`;
2131
+ elements.backendStatusBadge.textContent = `Backend: ${snapshot?.state?.lastBackendStatus ?? "-"}`;
2132
+ elements.historyCountBadge.textContent = `History: ${history.length && selectedIndex >= 0 ? selectedIndex + 1 : 0}/${history.length}`;
2133
+ elements.syncBadge.hidden = !inSync;
2134
+ elements.syncBadge.classList.toggle("sync", inSync);
2135
+ if (elements.annotationModeSelect) {
2136
+ elements.annotationModeSelect.value = state.annotationsEnabled ? "on" : "off";
2137
+ elements.annotationModeSelect.title = state.annotationsEnabled
2138
+ ? "Annotations On: keep and send [an: ...] markers."
2139
+ : "Annotations Hidden: keep markers in editor, hide in preview, and strip before Run / Queue steering.";
2140
+ }
2141
+ if (elements.highlightSelect) {
2142
+ elements.highlightSelect.value = state.editorHighlightEnabled ? "on" : "off";
2143
+ }
2144
+ if (elements.langSelect) {
2145
+ elements.langSelect.value = state.editorLanguage;
2146
+ }
2147
+ if (elements.sourceHighlight) {
2148
+ elements.sourceHighlight.hidden = !state.editorHighlightEnabled;
2149
+ }
2150
+ if (elements.stripAnnotationsBtn) {
2151
+ elements.stripAnnotationsBtn.disabled = state.busy || !hasAnnotationMarkers(elements.promptInput.value);
2152
+ }
2153
+ if (elements.saveAnnotatedBtn) {
2154
+ elements.saveAnnotatedBtn.disabled = state.busy || !normalizedText(elements.promptInput.value);
2155
+ }
2156
+ updateAnnotatedReplyHeaderButton();
2157
+ elements.promptInput.classList.toggle("highlight-active", state.editorHighlightEnabled);
2158
+ }
2159
+
2160
+ function startSpinner() {
2161
+ if (state.spinnerTimer) return;
2162
+ state.spinnerFrameIndex = 0;
2163
+ elements.statusLine.classList.add("with-spinner");
2164
+ elements.statusSpinner.textContent = BRAILLE_SPINNER_FRAMES[state.spinnerFrameIndex];
2165
+ state.spinnerTimer = window.setInterval(() => {
2166
+ state.spinnerFrameIndex = (state.spinnerFrameIndex + 1) % BRAILLE_SPINNER_FRAMES.length;
2167
+ elements.statusSpinner.textContent = BRAILLE_SPINNER_FRAMES[state.spinnerFrameIndex];
2168
+ }, 90);
2169
+ }
2170
+
2171
+ function stopSpinner() {
2172
+ if (state.spinnerTimer) {
2173
+ window.clearInterval(state.spinnerTimer);
2174
+ state.spinnerTimer = null;
2175
+ }
2176
+ elements.statusLine.classList.remove("with-spinner");
2177
+ elements.statusSpinner.textContent = "";
2178
+ }
2179
+
2180
+ function applyStatus(message, level = "", spinning = false) {
2181
+ elements.status.textContent = message;
2182
+ elements.status.className = level || "";
2183
+ if (spinning) {
2184
+ startSpinner();
2185
+ } else {
2186
+ stopSpinner();
2187
+ }
2188
+ }
2189
+
2190
+ function deriveStatus() {
2191
+ const snapshot = state.snapshot;
2192
+ if (!snapshot) {
2193
+ return { message: "Connecting · Studio bridge starting…", level: "", spinning: true };
2194
+ }
2195
+ if (snapshot.state.lastError) {
2196
+ return { message: `Error · ${snapshot.state.lastError}`, level: "error", spinning: false };
2197
+ }
2198
+ if (state.busy) {
2199
+ return { message: "Studio: sending request to the attached session…", level: "", spinning: true };
2200
+ }
2201
+ if (snapshot.state.runState === "stopping") {
2202
+ return { message: "Studio: stopping current run…", level: "warning", spinning: true };
2203
+ }
2204
+ if (snapshot.state.runState === "running") {
2205
+ const activeTurn = snapshot.activeTurn;
2206
+ const elapsed = activeTurn ? formatRelativeDuration(activeTurn.submittedAt, snapshot.now) : "-";
2207
+ const queueLength = snapshot.state.queueLength ?? 0;
2208
+ const queueSuffix = queueLength > 0 ? ` · ${queueLength} queued` : "";
2209
+ const variant = String(snapshot.currentModel?.variant || "").trim();
2210
+ const variantSuffix = variant ? ` · ${variant}` : "";
2211
+ let action = "Studio: waiting for queued steering";
2212
+ if (activeTurn) {
2213
+ if (activeTurn.promptMode === "run") {
2214
+ action = activeTurn.firstOutputTextAt
2215
+ ? "Studio: generating response"
2216
+ : "Studio: running editor text";
2217
+ } else {
2218
+ action = activeTurn.firstOutputTextAt
2219
+ ? `Studio: generating steering ${activeTurn.promptSteeringCount}`
2220
+ : `Studio: queueing steering ${activeTurn.promptSteeringCount}`;
2221
+ }
2222
+ }
2223
+ const elapsedSuffix = elapsed !== "-" ? ` · ${elapsed}` : "";
2224
+ return { message: `${action}…${elapsedSuffix}${queueSuffix}${variantSuffix}`, level: "", spinning: true };
2225
+ }
2226
+ if (snapshot.state.lastBackendStatus === "busy") {
2227
+ const variant = String(snapshot.currentModel?.variant || "").trim();
2228
+ const variantSuffix = variant ? ` · ${variant}` : "";
2229
+ const agentLabel = formatAgentLabel(snapshot);
2230
+ const source = snapshot.currentModel?.source;
2231
+ const action = source === "assistant"
2232
+ ? "Attached terminal: generating response"
2233
+ : (agentLabel ? `Attached terminal: ${agentLabel} running` : "Attached terminal: running");
2234
+ return { message: `${action}…${variantSuffix}`, level: "", spinning: true };
2235
+ }
2236
+
2237
+ const history = getHistory();
2238
+ const latest = getLatestHistoryItem();
2239
+ if (latest) {
2240
+ const selectedIndex = getSelectedHistoryIndex();
2241
+ const selected = history.length && selectedIndex >= 0 ? selectedIndex + 1 : history.length;
2242
+ const time = formatReferenceTime(latest.completedAt ?? latest.submittedAt ?? 0);
2243
+ const suffix = time ? ` · ${time}` : "";
2244
+ return {
2245
+ message: `Ready · response ${selected}/${history.length}${suffix}`,
2246
+ level: "",
2247
+ spinning: false,
2248
+ };
2249
+ }
2250
+
2251
+ return {
2252
+ message: "Ready · attached to active session.",
2253
+ level: "",
2254
+ spinning: false,
2255
+ };
2256
+ }
2257
+
2258
+ function renderStatusLine() {
2259
+ const transient = state.transientStatus;
2260
+ if (transient && transient.expiresAt > Date.now()) {
2261
+ applyStatus(transient.message, transient.level, false);
2262
+ return;
2263
+ }
2264
+ state.transientStatus = null;
2265
+ const derived = deriveStatus();
2266
+ applyStatus(derived.message, derived.level, derived.spinning);
2267
+ }
2268
+
2269
+ function setTransientStatus(message, level = "", durationMs = 2200) {
2270
+ state.transientStatus = {
2271
+ message,
2272
+ level,
2273
+ expiresAt: Date.now() + durationMs,
2274
+ };
2275
+ if (state.transientStatusTimer) {
2276
+ window.clearTimeout(state.transientStatusTimer);
2277
+ }
2278
+ state.transientStatusTimer = window.setTimeout(() => {
2279
+ state.transientStatus = null;
2280
+ renderStatusLine();
2281
+ }, durationMs + 20);
2282
+ renderStatusLine();
2283
+ }
2284
+
2285
+ function renderFooterMeta() {
2286
+ const history = getHistory();
2287
+ const selectedIndex = getSelectedHistoryIndex();
2288
+ const selected = history.length && selectedIndex >= 0 ? selectedIndex + 1 : 0;
2289
+ const queue = state.snapshot?.state?.queueLength ?? 0;
2290
+ const parts = [];
2291
+
2292
+ parts.push(formatModelSummary(state.snapshot));
2293
+
2294
+ const projectLabel = formatProjectLabel(state.snapshot);
2295
+ if (projectLabel) {
2296
+ parts.push(`Project: ${projectLabel}`);
2297
+ }
2298
+
2299
+ const agentLabel = formatAgentLabel(state.snapshot);
2300
+ if (agentLabel) {
2301
+ parts.push(`Agent: ${agentLabel}`);
2302
+ }
2303
+
2304
+ parts.push(formatContextUsage(state.snapshot));
2305
+
2306
+ elements.footerMetaText.textContent = parts.join(" · ");
2307
+ if (elements.footerMeta) {
2308
+ const titleParts = [];
2309
+ const directory = String(state.snapshot?.launchContext?.directory || "").trim();
2310
+ if (directory) titleParts.push(`Project directory: ${directory}`);
2311
+ const sessionLabel = formatSessionLabel(state.snapshot);
2312
+ if (sessionLabel) titleParts.push(`Session: ${sessionLabel}`);
2313
+ const sessionId = String(state.snapshot?.state?.sessionId || "").trim();
2314
+ if (sessionId) titleParts.push(`Session ID: ${sessionId}`);
2315
+ const baseUrl = String(state.snapshot?.launchContext?.baseUrl || "").trim();
2316
+ if (baseUrl) titleParts.push(`Opencode server: ${baseUrl}`);
2317
+ titleParts.push(`History: ${selected}/${history.length}`);
2318
+ titleParts.push(`Queue: ${queue}`);
2319
+ const model = formatModel(state.snapshot);
2320
+ if (model && model !== "-") titleParts.push(`Model ID: ${model}`);
2321
+ const variant = String(state.snapshot?.currentModel?.variant || "").trim();
2322
+ if (variant) titleParts.push(`Thinking level: ${variant}`);
2323
+ const tokenUsage = state.snapshot?.currentModel?.tokenUsage;
2324
+ if (tokenUsage) {
2325
+ if (typeof tokenUsage.total === "number") titleParts.push(`Tokens total: ${tokenUsage.total}`);
2326
+ if (typeof tokenUsage.input === "number") titleParts.push(`Tokens input: ${tokenUsage.input}`);
2327
+ if (typeof tokenUsage.output === "number") titleParts.push(`Tokens output: ${tokenUsage.output}`);
2328
+ if (typeof tokenUsage.reasoning === "number") titleParts.push(`Tokens reasoning: ${tokenUsage.reasoning}`);
2329
+ }
2330
+ const contextLimit = state.snapshot?.currentModel?.contextLimit;
2331
+ if (typeof contextLimit === "number") titleParts.push(`Context limit: ${contextLimit}`);
2332
+ const themeInfo = state.snapshot?.launchContext?.theme || getStudioThemeInfo();
2333
+ const themeRaw = String(themeInfo?.raw || "").trim();
2334
+ const themePreference = String(themeInfo?.preference || "").trim();
2335
+ if (themeRaw) titleParts.push(`Theme: ${themeRaw}`);
2336
+ else if (themePreference) titleParts.push(`Theme mode: ${themePreference}`);
2337
+ elements.footerMeta.title = titleParts.join("\n");
2338
+ }
2339
+ updateDocumentTitle();
2340
+ }
2341
+
2342
+ function updateActionState() {
2343
+ const snapshot = state.snapshot;
2344
+ const hasPrompt = Boolean(normalizedText(elements.promptInput.value));
2345
+ const runState = snapshot?.state?.runState ?? "idle";
2346
+ const running = runState === "running" || runState === "stopping";
2347
+ const selectedItem = getSelectedHistoryItem();
2348
+ const displayed = getDisplayedResponse();
2349
+ const history = getHistory();
2350
+ const selectedIndex = getSelectedHistoryIndex();
2351
+ const displayedResponseText = displayed?.hasContent ? normalizedText(displayed.markdown) : "";
2352
+
2353
+ elements.runBtn.textContent = runState === "stopping" ? "Stopping…" : (running ? "Stop" : "Run editor text");
2354
+ elements.runBtn.classList.toggle("request-stop-active", running);
2355
+ elements.runBtn.disabled = !snapshot || state.busy || (!running && !hasPrompt) || runState === "stopping";
2356
+
2357
+ elements.queueBtn.disabled = !snapshot || state.busy || runState !== "running" || !hasPrompt;
2358
+ elements.copyDraftBtn.disabled = !hasPrompt;
2359
+ if (elements.saveAsBtn) elements.saveAsBtn.disabled = state.busy || !hasPrompt;
2360
+ if (elements.saveBtn) elements.saveBtn.disabled = state.busy || !hasPrompt || !state.sourcePath;
2361
+ if (elements.loadFileBtn) elements.loadFileBtn.disabled = state.busy;
2362
+ if (elements.resourceDirBtn) elements.resourceDirBtn.disabled = state.busy || Boolean(state.sourcePath);
2363
+ if (elements.insertHeaderBtn) elements.insertHeaderBtn.disabled = state.busy;
2364
+ if (elements.annotationModeSelect) elements.annotationModeSelect.disabled = state.busy;
2365
+ if (elements.stripAnnotationsBtn) elements.stripAnnotationsBtn.disabled = state.busy || !hasAnnotationMarkers(elements.promptInput.value);
2366
+ if (elements.saveAnnotatedBtn) elements.saveAnnotatedBtn.disabled = state.busy || !hasPrompt;
2367
+ if (elements.highlightSelect) elements.highlightSelect.disabled = state.busy;
2368
+ if (elements.langSelect) elements.langSelect.disabled = state.busy;
2369
+ if (elements.responseHighlightSelect) {
2370
+ elements.responseHighlightSelect.value = state.responseHighlightEnabled ? "on" : "off";
2371
+ elements.responseHighlightSelect.disabled = state.rightView !== "markdown";
2372
+ }
2373
+
2374
+ elements.followSelect.value = state.followLatest ? "on" : "off";
2375
+ elements.historyPrevBtn.disabled = history.length === 0 || (!state.followLatest && selectedIndex <= 0) || (state.followLatest && history.length <= 1);
2376
+ elements.historyNextBtn.disabled = history.length === 0 || state.followLatest || selectedIndex < 0 || selectedIndex >= history.length - 1;
2377
+ elements.historyLastBtn.disabled = history.length === 0 || (state.followLatest && selectedIndex === history.length - 1);
2378
+
2379
+ elements.loadResponseBtn.disabled = state.busy || !displayedResponseText;
2380
+ elements.loadResponseBtn.textContent = "Load response into editor";
2381
+ if (!selectedItem) {
2382
+ elements.loadHistoryPromptBtn.disabled = true;
2383
+ elements.loadHistoryPromptBtn.textContent = getHistoryPromptButtonLabel(null);
2384
+ } else {
2385
+ const promptSource = selectedItem.promptMode === "steer" ? selectedItem.effectivePrompt : selectedItem.promptText;
2386
+ elements.loadHistoryPromptBtn.disabled = state.busy || !normalizedText(promptSource);
2387
+ elements.loadHistoryPromptBtn.textContent = getHistoryPromptButtonLabel(selectedItem);
2388
+ }
2389
+
2390
+ elements.copyResponseBtn.disabled = !displayedResponseText;
2391
+
2392
+ if (elements.exportPdfBtn) {
2393
+ const exportSource = getPdfExportSource();
2394
+ const canExportPdf = Boolean(exportSource && normalizedText(exportSource.markdown));
2395
+ elements.exportPdfBtn.disabled = state.pdfExportInProgress || !canExportPdf;
2396
+ if (state.rightView === "markdown") {
2397
+ elements.exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
2398
+ } else if (!canExportPdf) {
2399
+ elements.exportPdfBtn.title = "Nothing to export yet.";
2400
+ } else if (state.pdfExportInProgress) {
2401
+ elements.exportPdfBtn.title = "Exporting the current right-pane preview as PDF…";
2402
+ } else {
2403
+ elements.exportPdfBtn.title = "Export the current right-pane preview as PDF via pandoc + xelatex.";
2404
+ }
2405
+ }
2406
+ }
2407
+
2408
+ function render() {
2409
+ if (!state.snapshot) {
2410
+ renderStatusLine();
2411
+ return;
2412
+ }
2413
+ ensureSelectedHistoryItem();
2414
+ renderEditorMeta();
2415
+ if (state.editorHighlightEnabled) {
2416
+ scheduleEditorHighlightRender();
2417
+ }
2418
+ renderResponsePane();
2419
+ renderDiagnostics();
2420
+ renderFooterMeta();
2421
+ updateActionState();
2422
+ renderStatusLine();
2423
+ }
2424
+
2425
+ async function fetchSnapshot() {
2426
+ const response = await fetch(buildAuthenticatedPath("/api/snapshot"), {
2427
+ headers: buildAuthenticatedHeaders(),
2428
+ });
2429
+ if (!response.ok) {
2430
+ const message = response.status === 403
2431
+ ? "Studio access expired. Re-run /studio."
2432
+ : `Snapshot request failed with ${response.status}`;
2433
+ throw new Error(message);
2434
+ }
2435
+ applySnapshot(await response.json(), { initial: !state.initialSnapshotLoaded });
2436
+ render();
2437
+ }
2438
+
2439
+ async function postJson(path, payload = {}) {
2440
+ state.busy = true;
2441
+ render();
2442
+ try {
2443
+ const response = await fetch(buildAuthenticatedPath(path), {
2444
+ method: "POST",
2445
+ headers: buildAuthenticatedHeaders({ "Content-Type": "application/json" }),
2446
+ body: JSON.stringify(payload),
2447
+ });
2448
+ const data = await response.json();
2449
+ if (!response.ok) {
2450
+ throw new Error(data.error || `Request failed with ${response.status}`);
2451
+ }
2452
+ if (data.snapshot) {
2453
+ applySnapshot(data.snapshot);
2454
+ }
2455
+ render();
2456
+ restartSnapshotPolling(true);
2457
+ return data;
2458
+ } finally {
2459
+ state.busy = false;
2460
+ render();
2461
+ }
2462
+ }
2463
+
2464
+ function getSnapshotPollDelayMs() {
2465
+ if (typeof document !== "undefined" && document.hidden) {
2466
+ return 1200;
2467
+ }
2468
+ const runState = state.snapshot?.state?.runState ?? "idle";
2469
+ if (runState === "running" || runState === "stopping") {
2470
+ return 120;
2471
+ }
2472
+ return 500;
2473
+ }
2474
+
2475
+ function cancelSnapshotPolling() {
2476
+ if (!state.snapshotPollTimer) return;
2477
+ window.clearTimeout(state.snapshotPollTimer);
2478
+ state.snapshotPollTimer = null;
2479
+ }
2480
+
2481
+ function scheduleSnapshotPolling(delayMs = getSnapshotPollDelayMs()) {
2482
+ cancelSnapshotPolling();
2483
+ state.snapshotPollTimer = window.setTimeout(() => {
2484
+ state.snapshotPollTimer = null;
2485
+ void pollSnapshotOnce();
2486
+ }, Math.max(0, delayMs));
2487
+ }
2488
+
2489
+ function restartSnapshotPolling(immediate = false) {
2490
+ scheduleSnapshotPolling(immediate ? 0 : getSnapshotPollDelayMs());
2491
+ }
2492
+
2493
+ async function pollSnapshotOnce() {
2494
+ if (state.snapshotPollInFlight) {
2495
+ restartSnapshotPolling();
2496
+ return;
2497
+ }
2498
+ state.snapshotPollInFlight = true;
2499
+ try {
2500
+ await fetchSnapshot();
2501
+ } catch (error) {
2502
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2503
+ } finally {
2504
+ state.snapshotPollInFlight = false;
2505
+ restartSnapshotPolling();
2506
+ }
2507
+ }
2508
+
2509
+ function setEditorText(text, originLabel, options = {}) {
2510
+ elements.promptInput.value = text;
2511
+ state.editorOriginLabel = originLabel;
2512
+ if (Object.prototype.hasOwnProperty.call(options, "sourcePath")) {
2513
+ state.sourcePath = options.sourcePath ? String(options.sourcePath) : null;
2514
+ }
2515
+ if (Object.prototype.hasOwnProperty.call(options, "workingDir")) {
2516
+ state.workingDir = options.workingDir ? String(options.workingDir) : "";
2517
+ }
2518
+ if (options.language) {
2519
+ setEditorLanguage(String(options.language));
2520
+ }
2521
+ state.lastLoadedIntoEditorNormalized = normalizedText(text);
2522
+ scheduleEditorHighlightRender();
2523
+ render();
2524
+ }
2525
+
2526
+ function describeSourceForAnnotation() {
2527
+ if (state.sourcePath) {
2528
+ return `file ${state.sourcePath.split(/[\\/]/).pop() || state.sourcePath}`;
2529
+ }
2530
+ if (/response/i.test(state.editorOriginLabel)) {
2531
+ return "last model response";
2532
+ }
2533
+ return state.editorOriginLabel || "studio editor";
2534
+ }
2535
+
2536
+ function buildAnnotationHeader() {
2537
+ let header = "annotated reply below:\n";
2538
+ header += `original source: ${describeSourceForAnnotation()}\n`;
2539
+ header += "user annotation syntax: [an: note]\n";
2540
+ header += "precedence: later messages supersede these annotations unless user explicitly references them\n\n---\n\n";
2541
+ return header;
2542
+ }
2543
+
2544
+ function stripAnnotationBoundaryMarker(text) {
2545
+ return String(text || "").replace(/\n{0,2}--- end annotations ---\s*$/i, "");
2546
+ }
2547
+
2548
+ function stripAnnotationHeader(text) {
2549
+ const normalized = String(text || "").replace(/\r\n/g, "\n");
2550
+ if (!normalized.toLowerCase().startsWith("annotated reply below:")) {
2551
+ return { hadHeader: false, body: normalized };
2552
+ }
2553
+ const dividerIndex = normalized.indexOf("\n---");
2554
+ if (dividerIndex < 0) {
2555
+ return { hadHeader: false, body: normalized };
2556
+ }
2557
+ let cursor = dividerIndex + 4;
2558
+ while (cursor < normalized.length && normalized[cursor] === "\n") {
2559
+ cursor += 1;
2560
+ }
2561
+ return {
2562
+ hadHeader: true,
2563
+ body: stripAnnotationBoundaryMarker(normalized.slice(cursor)),
2564
+ };
2565
+ }
2566
+
2567
+ function buildAnnotatedSaveSuggestion() {
2568
+ const effectivePath = state.sourcePath || "";
2569
+ if (effectivePath) {
2570
+ const parts = String(effectivePath).split(/[\\/]/);
2571
+ const fileName = parts.pop() || "draft.md";
2572
+ const dir = parts.length > 0 ? parts.join("/") + "/" : "";
2573
+ const stem = fileName.replace(/\.[^.]+$/, "") || "draft";
2574
+ return dir + stem + ".annotated.md";
2575
+ }
2576
+ const baseName = `draft.annotated.md`;
2577
+ if (state.workingDir) {
2578
+ return state.workingDir.replace(/\/$/, "") + "/" + baseName;
2579
+ }
2580
+ return "./" + baseName;
2581
+ }
2582
+
2583
+ function updateAnnotatedReplyHeaderButton() {
2584
+ if (!elements.insertHeaderBtn) return;
2585
+ const hasHeader = stripAnnotationHeader(elements.promptInput.value).hadHeader;
2586
+ elements.insertHeaderBtn.textContent = hasHeader ? "Remove annotated reply header" : "Insert annotated reply header";
2587
+ }
2588
+
2589
+ function buildSuggestedSavePath() {
2590
+ if (state.sourcePath) return state.sourcePath;
2591
+ const ext = preferredExtensionForLanguage(state.editorLanguage);
2592
+ const baseName = `draft.${ext}`;
2593
+ if (state.workingDir) {
2594
+ return state.workingDir.replace(/\/$/, "") + "/" + baseName;
2595
+ }
2596
+ return "./" + baseName;
2597
+ }
2598
+
2599
+ async function loadFileContent() {
2600
+ const suggested = state.sourcePath || (state.workingDir ? state.workingDir.replace(/\/$/, "") + "/" : "./");
2601
+ const path = window.prompt("Load file content from:", suggested);
2602
+ if (!path) return;
2603
+ try {
2604
+ const data = await postJson("/api/file/load", { path, baseDir: state.workingDir || undefined });
2605
+ const language = detectLanguageFromName(data.path || data.label || "") || state.editorLanguage;
2606
+ setEditorText(data.content || "", data.label || data.path || path, { sourcePath: data.path || null, language, workingDir: "" });
2607
+ setTransientStatus(`Loaded ${data.label || data.path || path}.`, "success");
2608
+ } catch (error) {
2609
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2610
+ }
2611
+ }
2612
+
2613
+ async function saveEditorAs() {
2614
+ const content = elements.promptInput.value;
2615
+ if (!normalizedText(content)) {
2616
+ setTransientStatus("Editor is empty. Nothing to save.", "warning");
2617
+ return;
2618
+ }
2619
+ const path = window.prompt("Save editor content as:", buildSuggestedSavePath());
2620
+ if (!path) return;
2621
+ try {
2622
+ const data = await postJson("/api/file/save", { path, content, baseDir: state.workingDir || undefined });
2623
+ const language = detectLanguageFromName(data.path || data.label || path) || state.editorLanguage;
2624
+ setEditorText(content, data.label || data.path || path, { sourcePath: data.path || null, language, workingDir: "" });
2625
+ setTransientStatus(`Saved editor text to ${data.label || data.path || path}.`, "success");
2626
+ } catch (error) {
2627
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2628
+ }
2629
+ }
2630
+
2631
+ async function saveEditor() {
2632
+ const content = elements.promptInput.value;
2633
+ if (!normalizedText(content)) {
2634
+ setTransientStatus("Editor is empty. Nothing to save.", "warning");
2635
+ return;
2636
+ }
2637
+ if (!state.sourcePath) {
2638
+ setTransientStatus("Save editor requires a file path. Use Save editor as… or load a file first.", "warning");
2639
+ return;
2640
+ }
2641
+ try {
2642
+ const data = await postJson("/api/file/save", { path: state.sourcePath, content });
2643
+ setEditorText(content, data.label || state.sourcePath, { sourcePath: data.path || state.sourcePath });
2644
+ setTransientStatus(`Saved ${data.label || state.sourcePath}.`, "success");
2645
+ } catch (error) {
2646
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2647
+ }
2648
+ }
2649
+
2650
+ function toggleAnnotatedReplyHeader() {
2651
+ const stripped = stripAnnotationHeader(elements.promptInput.value);
2652
+ if (stripped.hadHeader) {
2653
+ setEditorText(stripped.body, state.editorOriginLabel, { sourcePath: state.sourcePath, workingDir: state.workingDir, language: state.editorLanguage });
2654
+ setTransientStatus("Removed annotated reply header.", "success");
2655
+ return;
2656
+ }
2657
+ const cleanedBody = stripAnnotationBoundaryMarker(stripped.body);
2658
+ const updated = buildAnnotationHeader() + cleanedBody + "\n\n--- end annotations ---\n\n";
2659
+ setEditorText(updated, state.editorOriginLabel, { sourcePath: state.sourcePath, workingDir: state.workingDir, language: state.editorLanguage });
2660
+ setTransientStatus("Inserted annotated reply header.", "success");
2661
+ }
2662
+
2663
+ function stripAllAnnotations() {
2664
+ const content = elements.promptInput.value;
2665
+ if (!hasAnnotationMarkers(content)) {
2666
+ setTransientStatus("No [an: ...] markers found in editor.", "warning");
2667
+ return;
2668
+ }
2669
+ const confirmed = window.confirm("Remove all [an: ...] markers from editor text? This cannot be undone.");
2670
+ if (!confirmed) return;
2671
+ const stripped = stripAnnotationMarkers(content);
2672
+ setEditorText(stripped, state.editorOriginLabel, { sourcePath: state.sourcePath, workingDir: state.workingDir, language: state.editorLanguage });
2673
+ setTransientStatus("Removed annotation markers from editor text.", "success");
2674
+ }
2675
+
2676
+ async function saveAnnotatedCopy() {
2677
+ const content = elements.promptInput.value;
2678
+ if (!normalizedText(content)) {
2679
+ setTransientStatus("Editor is empty. Nothing to save.", "warning");
2680
+ return;
2681
+ }
2682
+ const path = window.prompt("Save annotated editor content as:", buildAnnotatedSaveSuggestion());
2683
+ if (!path) return;
2684
+ try {
2685
+ const data = await postJson("/api/file/save", { path, content, baseDir: state.workingDir || undefined });
2686
+ setTransientStatus(`Saved annotated editor text to ${data.label || data.path || path}.`, "success");
2687
+ } catch (error) {
2688
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2689
+ }
2690
+ }
2691
+
2692
+ function chooseWorkingDir() {
2693
+ if (state.sourcePath) {
2694
+ setTransientStatus("Working dir is only needed for non-file-backed editor content.", "warning");
2695
+ return;
2696
+ }
2697
+ const suggested = state.workingDir || "./";
2698
+ const result = window.prompt("Set working directory for preview/resource resolution (leave blank to clear):", suggested);
2699
+ if (result === null) return;
2700
+ const trimmed = result.trim();
2701
+ state.workingDir = trimmed;
2702
+ render();
2703
+ setTransientStatus(trimmed ? `Working dir set to ${trimmed}.` : "Working dir cleared.", "success");
2704
+ }
2705
+
2706
+ async function exportRightPanePdf() {
2707
+ if (state.pdfExportInProgress) {
2708
+ setTransientStatus("PDF export is already in progress.", "warning");
2709
+ return;
2710
+ }
2711
+
2712
+ const exportSource = getPdfExportSource();
2713
+ if (!exportSource) {
2714
+ setTransientStatus("Switch right pane to Response (Preview) or Editor (Preview) before exporting PDF.", "warning");
2715
+ return;
2716
+ }
2717
+
2718
+ state.pdfExportInProgress = true;
2719
+ render();
2720
+ setTransientStatus("Exporting PDF…", "", 30_000);
2721
+
2722
+ try {
2723
+ const response = await fetch(buildAuthenticatedPath("/api/export-pdf"), {
2724
+ method: "POST",
2725
+ headers: buildAuthenticatedHeaders({
2726
+ "Content-Type": "application/json",
2727
+ }),
2728
+ body: JSON.stringify(exportSource),
2729
+ });
2730
+
2731
+ if (!response.ok) {
2732
+ const contentType = String(response.headers.get("content-type") || "").toLowerCase();
2733
+ let message = `PDF export failed with HTTP ${response.status}.`;
2734
+ if (contentType.includes("application/json")) {
2735
+ const payload = await response.json().catch(() => null);
2736
+ if (payload && typeof payload.error === "string") {
2737
+ message = payload.error;
2738
+ }
2739
+ } else {
2740
+ const text = await response.text().catch(() => "");
2741
+ if (text && text.trim()) {
2742
+ message = text.trim();
2743
+ }
2744
+ }
2745
+ throw new Error(message);
2746
+ }
2747
+
2748
+ const warning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
2749
+ const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
2750
+ let downloadName = headerFilename || exportSource.filenameHint || "studio-preview.pdf";
2751
+ if (!/\.pdf$/i.test(downloadName)) {
2752
+ downloadName += ".pdf";
2753
+ }
2754
+
2755
+ const blob = await response.blob();
2756
+ const blobUrl = URL.createObjectURL(blob);
2757
+ const link = document.createElement("a");
2758
+ link.href = blobUrl;
2759
+ link.download = downloadName;
2760
+ link.rel = "noopener";
2761
+ document.body.appendChild(link);
2762
+ link.click();
2763
+ link.remove();
2764
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
2765
+
2766
+ if (warning) {
2767
+ setTransientStatus(`Exported PDF with warning: ${warning}`, "warning", 5000);
2768
+ } else {
2769
+ setTransientStatus(`Exported PDF: ${downloadName}`, "success", 3200);
2770
+ }
2771
+ } catch (error) {
2772
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error", 5000);
2773
+ } finally {
2774
+ state.pdfExportInProgress = false;
2775
+ render();
2776
+ }
2777
+ }
2778
+
2779
+ async function copyText(text, successMessage) {
2780
+ await navigator.clipboard.writeText(text);
2781
+ setTransientStatus(successMessage, "success");
2782
+ }
2783
+
2784
+ async function runOrStop() {
2785
+ const snapshot = state.snapshot;
2786
+ if (!snapshot) return;
2787
+ if (snapshot.state.runState === "running") {
2788
+ try {
2789
+ await postJson("/api/stop");
2790
+ setTransientStatus("Stop requested.", "success");
2791
+ } catch (error) {
2792
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2793
+ }
2794
+ return;
2795
+ }
2796
+
2797
+ const prompt = normalizedText(prepareEditorTextForSend(elements.promptInput.value));
2798
+ if (!prompt) {
2799
+ setTransientStatus("Add editor text before running.", "warning");
2800
+ return;
2801
+ }
2802
+
2803
+ clearTitleAttention();
2804
+ try {
2805
+ await postJson("/api/run", { prompt });
2806
+ setTransientStatus("Running editor text.", "success");
2807
+ } catch (error) {
2808
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2809
+ }
2810
+ }
2811
+
2812
+ async function queueSteering() {
2813
+ const prompt = normalizedText(prepareEditorTextForSend(elements.promptInput.value));
2814
+ if (!prompt) {
2815
+ setTransientStatus("Add editor text before queueing steering.", "warning");
2816
+ return;
2817
+ }
2818
+ clearTitleAttention();
2819
+ try {
2820
+ await postJson("/api/steer", { prompt });
2821
+ setTransientStatus("Queued steering.", "success");
2822
+ } catch (error) {
2823
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2824
+ }
2825
+ }
2826
+
2827
+ function selectHistoryIndex(index) {
2828
+ const history = getHistory();
2829
+ if (!history.length) return;
2830
+ const clamped = Math.max(0, Math.min(history.length - 1, index));
2831
+ const item = history[clamped];
2832
+ if (!item) return;
2833
+ state.selectedPromptId = item.localPromptId;
2834
+ render();
2835
+ }
2836
+
2837
+ function handleHistoryPrev() {
2838
+ const history = getHistory();
2839
+ if (!history.length) {
2840
+ setTransientStatus("No response history available yet.", "warning");
2841
+ return;
2842
+ }
2843
+ const currentIndex = getSelectedHistoryIndex();
2844
+ if (state.followLatest) {
2845
+ state.followLatest = false;
2846
+ selectHistoryIndex(Math.max(0, history.length - 2));
2847
+ setTransientStatus("Viewing previous response.", "success");
2848
+ return;
2849
+ }
2850
+ selectHistoryIndex(currentIndex - 1);
2851
+ setTransientStatus("Viewing previous response.", "success");
2852
+ }
2853
+
2854
+ function handleHistoryNext() {
2855
+ const history = getHistory();
2856
+ if (!history.length) {
2857
+ setTransientStatus("No response history available yet.", "warning");
2858
+ return;
2859
+ }
2860
+ if (state.followLatest) return;
2861
+ const currentIndex = getSelectedHistoryIndex();
2862
+ selectHistoryIndex(currentIndex + 1);
2863
+ setTransientStatus("Viewing next response.", "success");
2864
+ }
2865
+
2866
+ function handleHistoryLast() {
2867
+ const latest = getLatestHistoryItem();
2868
+ if (!latest) {
2869
+ setTransientStatus("No response history available yet.", "warning");
2870
+ return;
2871
+ }
2872
+ state.followLatest = true;
2873
+ state.selectedPromptId = latest.localPromptId;
2874
+ render();
2875
+ setTransientStatus("Viewing latest response.", "success");
2876
+ }
2877
+
2878
+ function loadSelectedResponse() {
2879
+ const display = getDisplayedResponse();
2880
+ const responseText = display?.hasContent ? String(display.markdown || "") : "";
2881
+ if (!normalizedText(responseText)) {
2882
+ setTransientStatus("No response available yet.", "warning");
2883
+ return;
2884
+ }
2885
+ const originLabel = display.kind === "active" ? "live response" : "selected response";
2886
+ setEditorText(responseText, originLabel, { sourcePath: null });
2887
+ setTransientStatus("Loaded response into editor.", "success");
2888
+ }
2889
+
2890
+ function loadSelectedPrompt() {
2891
+ const item = getSelectedHistoryItem();
2892
+ if (!item) {
2893
+ setTransientStatus("Prompt unavailable for the selected response.", "warning");
2894
+ return;
2895
+ }
2896
+ const promptSource = item.promptMode === "steer" ? (item.effectivePrompt || item.promptText) : item.promptText;
2897
+ if (!normalizedText(promptSource)) {
2898
+ setTransientStatus("Prompt unavailable for the selected response.", "warning");
2899
+ return;
2900
+ }
2901
+ setEditorText(promptSource, getHistoryPromptSourceStateLabel(item), { sourcePath: null });
2902
+ setTransientStatus(getHistoryPromptLoadedStatus(item), "success");
2903
+ }
2904
+
2905
+ async function copySelectedResponse() {
2906
+ const display = getDisplayedResponse();
2907
+ const responseText = display?.hasContent ? String(display.markdown || "") : "";
2908
+ if (!normalizedText(responseText)) {
2909
+ setTransientStatus("No response available yet.", "warning");
2910
+ return;
2911
+ }
2912
+ try {
2913
+ await copyText(responseText, display.kind === "active" ? "Copied live response preview." : "Copied response text.");
2914
+ } catch (error) {
2915
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
2916
+ }
2917
+ }
2918
+
2919
+ function toggleDiagnostics() {
2920
+ state.diagnosticsOpen = !state.diagnosticsOpen;
2921
+ render();
2922
+ setTransientStatus(`Diagnostics ${state.diagnosticsOpen ? "shown" : "hidden"}.`, "success", 1400);
2923
+ }
2924
+
2925
+ function handlePromptInputChange() {
2926
+ const normalized = normalizedText(elements.promptInput.value);
2927
+ if (!state.sourcePath && normalized !== state.lastLoadedIntoEditorNormalized) {
2928
+ state.editorOriginLabel = normalized ? "studio editor draft" : "studio editor";
2929
+ }
2930
+ if (state.editorHighlightEnabled) {
2931
+ scheduleEditorHighlightRender();
2932
+ }
2933
+ render();
2934
+ }
2935
+
2936
+ function handleFollowLatestChange() {
2937
+ state.followLatest = elements.followSelect.value === "on";
2938
+ if (state.followLatest) {
2939
+ const latest = getLatestHistoryItem();
2940
+ state.selectedPromptId = latest?.localPromptId ?? null;
2941
+ }
2942
+ render();
2943
+ }
2944
+
2945
+ function updatePaneFocusButtons() {
2946
+ [
2947
+ [elements.leftFocusBtn, "left"],
2948
+ [elements.rightFocusBtn, "right"],
2949
+ ].forEach(([btn, pane]) => {
2950
+ if (!btn) return;
2951
+ const isFocusedPane = state.paneFocusTarget === pane;
2952
+ const paneName = pane === "right" ? "response" : "editor";
2953
+ btn.classList.toggle("is-active", isFocusedPane);
2954
+ btn.setAttribute("aria-pressed", isFocusedPane ? "true" : "false");
2955
+ btn.textContent = isFocusedPane ? "Exit focus" : "Focus pane";
2956
+ btn.title = isFocusedPane
2957
+ ? "Return to the two-pane layout. Shortcut: F10 or Cmd/Ctrl+Esc."
2958
+ : `Show only the ${paneName} pane. Shortcut: F10 or Cmd/Ctrl+Esc.`;
2959
+ });
2960
+ }
2961
+
2962
+ function applyPaneFocusClasses() {
2963
+ document.body.classList.remove("pane-focus-left", "pane-focus-right");
2964
+ if (state.paneFocusTarget === "left") {
2965
+ document.body.classList.add("pane-focus-left");
2966
+ } else if (state.paneFocusTarget === "right") {
2967
+ document.body.classList.add("pane-focus-right");
2968
+ }
2969
+ updatePaneFocusButtons();
2970
+ }
2971
+
2972
+ function setActivePane(nextPane) {
2973
+ state.activePane = nextPane === "right" ? "right" : "left";
2974
+ if (elements.leftPane) elements.leftPane.classList.toggle("pane-active", state.activePane === "left");
2975
+ if (elements.rightPane) elements.rightPane.classList.toggle("pane-active", state.activePane === "right");
2976
+ if (state.paneFocusTarget !== "off" && state.paneFocusTarget !== state.activePane) {
2977
+ state.paneFocusTarget = state.activePane;
2978
+ applyPaneFocusClasses();
2979
+ }
2980
+ }
2981
+
2982
+ function paneLabel(pane) {
2983
+ return pane === "right" ? "Response" : "Editor";
2984
+ }
2985
+
2986
+ function enterPaneFocus(nextPane) {
2987
+ const pane = nextPane === "right" ? "right" : "left";
2988
+ setActivePane(pane);
2989
+ state.paneFocusTarget = pane;
2990
+ applyPaneFocusClasses();
2991
+ setTransientStatus(`Focus mode: ${paneLabel(pane)} pane.`, "success", 1500);
2992
+ }
2993
+
2994
+ function togglePaneFocus() {
2995
+ if (state.paneFocusTarget === state.activePane) {
2996
+ state.paneFocusTarget = "off";
2997
+ applyPaneFocusClasses();
2998
+ setTransientStatus("Focus mode off.", "success", 1200);
2999
+ return;
3000
+ }
3001
+ enterPaneFocus(state.activePane);
3002
+ }
3003
+
3004
+ function exitPaneFocus() {
3005
+ if (state.paneFocusTarget === "off") return false;
3006
+ state.paneFocusTarget = "off";
3007
+ applyPaneFocusClasses();
3008
+ setTransientStatus("Focus mode off.", "success", 1200);
3009
+ return true;
3010
+ }
3011
+
3012
+ function handleGlobalShortcuts(event) {
3013
+ if (event.defaultPrevented || event.isComposing) return;
3014
+ const metaEnter = (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === "Enter";
3015
+ const plainEscape = !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "Escape";
3016
+ const togglePaneShortcut = ((event.metaKey || event.ctrlKey) && event.key === "Escape") || event.key === "F10";
3017
+
3018
+ if (togglePaneShortcut) {
3019
+ event.preventDefault();
3020
+ togglePaneFocus();
3021
+ return;
3022
+ }
3023
+
3024
+ if (metaEnter) {
3025
+ event.preventDefault();
3026
+ if (state.snapshot?.state?.runState === "running" && !elements.queueBtn.disabled) {
3027
+ void queueSteering();
3028
+ return;
3029
+ }
3030
+ if (!elements.runBtn.disabled) {
3031
+ void runOrStop();
3032
+ }
3033
+ return;
3034
+ }
3035
+
3036
+ if (plainEscape && state.snapshot?.state?.runState === "running" && !elements.runBtn.disabled) {
3037
+ event.preventDefault();
3038
+ void runOrStop();
3039
+ return;
3040
+ }
3041
+
3042
+ if (plainEscape && exitPaneFocus()) {
3043
+ event.preventDefault();
3044
+ }
3045
+ }
3046
+
3047
+ function wireEvents() {
3048
+ if (elements.saveAsBtn) {
3049
+ elements.saveAsBtn.addEventListener("click", () => void saveEditorAs());
3050
+ }
3051
+ if (elements.saveBtn) {
3052
+ elements.saveBtn.addEventListener("click", () => void saveEditor());
3053
+ }
3054
+ if (elements.loadFileBtn) {
3055
+ elements.loadFileBtn.addEventListener("click", () => void loadFileContent());
3056
+ }
3057
+ if (elements.resourceDirBtn) {
3058
+ elements.resourceDirBtn.addEventListener("click", () => chooseWorkingDir());
3059
+ }
3060
+ if (elements.resourceDirLabel) {
3061
+ elements.resourceDirLabel.addEventListener("click", () => chooseWorkingDir());
3062
+ }
3063
+ if (elements.insertHeaderBtn) {
3064
+ elements.insertHeaderBtn.addEventListener("click", () => toggleAnnotatedReplyHeader());
3065
+ }
3066
+ if (elements.annotationModeSelect) {
3067
+ elements.annotationModeSelect.addEventListener("change", () => {
3068
+ setAnnotationsEnabled(elements.annotationModeSelect.value !== "off");
3069
+ render();
3070
+ });
3071
+ }
3072
+ if (elements.stripAnnotationsBtn) {
3073
+ elements.stripAnnotationsBtn.addEventListener("click", () => stripAllAnnotations());
3074
+ }
3075
+ if (elements.saveAnnotatedBtn) {
3076
+ elements.saveAnnotatedBtn.addEventListener("click", () => void saveAnnotatedCopy());
3077
+ }
3078
+ elements.refreshBtn.addEventListener("click", () => {
3079
+ void fetchSnapshot().then(() => {
3080
+ setTransientStatus("Refreshed snapshot.", "success", 1200);
3081
+ }).catch((error) => {
3082
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
3083
+ });
3084
+ });
3085
+
3086
+ elements.diagnosticsBtn.addEventListener("click", () => toggleDiagnostics());
3087
+ elements.runBtn.addEventListener("click", () => void runOrStop());
3088
+ elements.queueBtn.addEventListener("click", () => void queueSteering());
3089
+ elements.copyDraftBtn.addEventListener("click", () => {
3090
+ void copyText(elements.promptInput.value, "Copied editor text.").catch((error) => {
3091
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error");
3092
+ });
3093
+ });
3094
+ elements.followSelect.addEventListener("change", () => handleFollowLatestChange());
3095
+ if (elements.highlightSelect) {
3096
+ elements.highlightSelect.addEventListener("change", () => {
3097
+ setEditorHighlightEnabled(elements.highlightSelect.value === "on");
3098
+ render();
3099
+ });
3100
+ }
3101
+ if (elements.responseHighlightSelect) {
3102
+ elements.responseHighlightSelect.addEventListener("change", () => {
3103
+ setResponseHighlightEnabled(elements.responseHighlightSelect.value === "on");
3104
+ render();
3105
+ });
3106
+ }
3107
+ if (elements.langSelect) {
3108
+ elements.langSelect.addEventListener("change", () => {
3109
+ setEditorLanguage(elements.langSelect.value);
3110
+ render();
3111
+ });
3112
+ }
3113
+ if (elements.rightViewSelect) {
3114
+ elements.rightViewSelect.addEventListener("change", () => {
3115
+ state.rightView = elements.rightViewSelect.value === "editor-preview"
3116
+ ? "editor-preview"
3117
+ : (elements.rightViewSelect.value === "markdown" ? "markdown" : "preview");
3118
+ state.currentRenderedPreviewKey = "";
3119
+ render();
3120
+ });
3121
+ }
3122
+ if (elements.leftPane) {
3123
+ elements.leftPane.addEventListener("mousedown", () => setActivePane("left"));
3124
+ elements.leftPane.addEventListener("focusin", () => setActivePane("left"));
3125
+ }
3126
+ if (elements.rightPane) {
3127
+ elements.rightPane.addEventListener("mousedown", () => setActivePane("right"));
3128
+ elements.rightPane.addEventListener("focusin", () => setActivePane("right"));
3129
+ }
3130
+ if (elements.leftFocusBtn) {
3131
+ elements.leftFocusBtn.addEventListener("click", () => {
3132
+ if (state.paneFocusTarget === "left") {
3133
+ exitPaneFocus();
3134
+ return;
3135
+ }
3136
+ enterPaneFocus("left");
3137
+ });
3138
+ }
3139
+ if (elements.rightFocusBtn) {
3140
+ elements.rightFocusBtn.addEventListener("click", () => {
3141
+ if (state.paneFocusTarget === "right") {
3142
+ exitPaneFocus();
3143
+ return;
3144
+ }
3145
+ enterPaneFocus("right");
3146
+ });
3147
+ }
3148
+ elements.historyPrevBtn.addEventListener("click", () => handleHistoryPrev());
3149
+ elements.historyNextBtn.addEventListener("click", () => handleHistoryNext());
3150
+ elements.historyLastBtn.addEventListener("click", () => handleHistoryLast());
3151
+ elements.loadResponseBtn.addEventListener("click", () => loadSelectedResponse());
3152
+ elements.loadHistoryPromptBtn.addEventListener("click", () => loadSelectedPrompt());
3153
+ elements.copyResponseBtn.addEventListener("click", () => void copySelectedResponse());
3154
+ if (elements.exportPdfBtn) {
3155
+ elements.exportPdfBtn.addEventListener("click", () => void exportRightPanePdf());
3156
+ }
3157
+ elements.promptInput.addEventListener("input", () => handlePromptInputChange());
3158
+ elements.promptInput.addEventListener("scroll", () => syncEditorHighlightScroll());
3159
+ elements.promptInput.addEventListener("keyup", () => syncEditorHighlightScroll());
3160
+ elements.promptInput.addEventListener("mouseup", () => syncEditorHighlightScroll());
3161
+ window.addEventListener("resize", () => syncEditorHighlightScroll());
3162
+ window.addEventListener("keydown", handleGlobalShortcuts);
3163
+ }
3164
+
3165
+ async function main() {
3166
+ populateLanguageOptions();
3167
+ setEditorLanguage(readStoredEditorLanguage() || state.editorLanguage);
3168
+ setAnnotationsEnabled(readStoredToggle(ANNOTATION_MODE_STORAGE_KEY) ?? true);
3169
+ setEditorHighlightEnabled(readStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY) ?? true);
3170
+ setResponseHighlightEnabled(readStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY) ?? true);
3171
+ setActivePane("left");
3172
+ applyPaneFocusClasses();
3173
+ wireEvents();
3174
+ await fetchSnapshot();
3175
+ restartSnapshotPolling();
3176
+ document.addEventListener("visibilitychange", () => {
3177
+ if (!document.hidden) {
3178
+ state.windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : true;
3179
+ clearTitleAttention();
3180
+ }
3181
+ restartSnapshotPolling(!document.hidden);
3182
+ });
3183
+ window.addEventListener("focus", () => {
3184
+ state.windowHasFocus = true;
3185
+ clearTitleAttention();
3186
+ restartSnapshotPolling(true);
3187
+ });
3188
+ window.addEventListener("blur", () => {
3189
+ state.windowHasFocus = false;
3190
+ });
3191
+ window.addEventListener("beforeunload", () => {
3192
+ stopTitleAttentionTimer();
3193
+ });
3194
+ }
3195
+
3196
+ void main().catch((error) => {
3197
+ setTransientStatus(error instanceof Error ? error.message : String(error), "error", 4000);
3198
+ });