pi-studio 0.5.51 → 0.5.53

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.
@@ -61,6 +61,10 @@
61
61
  const sourceBadgeEl = document.getElementById("sourceBadge");
62
62
  const syncBadgeEl = document.getElementById("syncBadge");
63
63
  let critiqueViewEl = document.getElementById("critiqueView");
64
+ const responseActionsEl = document.getElementById("responseActions");
65
+ const responseWrapEl = responseActionsEl && typeof responseActionsEl.closest === "function"
66
+ ? responseActionsEl.closest(".response-wrap")
67
+ : null;
64
68
  const referenceBadgeEl = document.getElementById("referenceBadge");
65
69
  const editorViewSelect = document.getElementById("editorViewSelect");
66
70
  const rightViewSelect = document.getElementById("rightViewSelect");
@@ -104,6 +108,7 @@
104
108
  const leftFocusBtn = document.getElementById("leftFocusBtn");
105
109
  const rightFocusBtn = document.getElementById("rightFocusBtn");
106
110
  const reviewNotesBtn = document.getElementById("reviewNotesBtn");
111
+ const outlineBtn = document.getElementById("outlineBtn");
107
112
  const scratchpadBtn = document.getElementById("scratchpadBtn");
108
113
  const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
109
114
  const scratchpadDialogEl = document.getElementById("scratchpadDialog");
@@ -114,6 +119,13 @@
114
119
  const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
115
120
  const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
116
121
  const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
122
+ const outlineOverlayEl = document.getElementById("outlineOverlay");
123
+ const outlineDialogEl = document.getElementById("outlineDialog");
124
+ const outlineMetaEl = document.getElementById("outlineMeta");
125
+ const outlineListEl = document.getElementById("outlineList");
126
+ const outlineEmptyStateEl = document.getElementById("outlineEmptyState");
127
+ const outlineCloseBtn = document.getElementById("outlineCloseBtn");
128
+ const outlineDoneBtn = document.getElementById("outlineDoneBtn");
117
129
  const reviewNotesOverlayEl = document.getElementById("reviewNotesOverlay");
118
130
  const reviewNotesDialogEl = document.getElementById("reviewNotesDialog");
119
131
  const reviewNotesMetaEl = document.getElementById("reviewNotesMeta");
@@ -171,6 +183,10 @@
171
183
  let latestCritiqueNotesNormalized = "";
172
184
  let responseHistory = [];
173
185
  let responseHistoryIndex = -1;
186
+ let traceState = null;
187
+ let traceFilter = "all";
188
+ let traceAutoScroll = true;
189
+ let traceRenderRaf = null;
174
190
  let studioRunChainActive = false;
175
191
  let queuedSteeringCount = 0;
176
192
  let agentBusyFromServer = false;
@@ -223,6 +239,262 @@
223
239
  return changed;
224
240
  }
225
241
 
242
+ function createEmptyTraceState() {
243
+ return {
244
+ runId: null,
245
+ requestId: null,
246
+ requestKind: null,
247
+ status: "idle",
248
+ startedAt: null,
249
+ updatedAt: null,
250
+ entries: [],
251
+ };
252
+ }
253
+
254
+ function normalizeTraceStatus(status) {
255
+ return status === "running" || status === "complete" ? status : "idle";
256
+ }
257
+
258
+ function normalizeTraceEntryStatus(status) {
259
+ return status === "streaming" || status === "pending" || status === "complete" || status === "error"
260
+ ? status
261
+ : "pending";
262
+ }
263
+
264
+ function normalizeTraceEntry(entry, fallbackIndex) {
265
+ if (!entry || typeof entry !== "object") return null;
266
+ if (entry.type === "assistant") {
267
+ return {
268
+ id: typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : ("trace-assistant-" + fallbackIndex),
269
+ type: "assistant",
270
+ startedAt: parseFiniteNumber(entry.startedAt) || Date.now(),
271
+ updatedAt: parseFiniteNumber(entry.updatedAt) || Date.now(),
272
+ thinking: typeof entry.thinking === "string" ? entry.thinking : "",
273
+ text: typeof entry.text === "string" ? entry.text : "",
274
+ status: normalizeTraceEntryStatus(entry.status),
275
+ stopReason: typeof entry.stopReason === "string" && entry.stopReason.trim() ? entry.stopReason.trim() : null,
276
+ };
277
+ }
278
+ if (entry.type === "tool") {
279
+ return {
280
+ id: typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : ("trace-tool-" + fallbackIndex),
281
+ type: "tool",
282
+ toolCallId: typeof entry.toolCallId === "string" ? entry.toolCallId : ("tool-" + fallbackIndex),
283
+ toolName: typeof entry.toolName === "string" ? entry.toolName : "tool",
284
+ label: parseNonEmptyString(entry.label),
285
+ argsSummary: parseNonEmptyString(entry.argsSummary),
286
+ output: typeof entry.output === "string" ? entry.output : "",
287
+ startedAt: parseFiniteNumber(entry.startedAt) || Date.now(),
288
+ updatedAt: parseFiniteNumber(entry.updatedAt) || Date.now(),
289
+ status: normalizeTraceEntryStatus(entry.status),
290
+ isError: Boolean(entry.isError),
291
+ };
292
+ }
293
+ return null;
294
+ }
295
+
296
+ function normalizeTraceState(raw) {
297
+ const fallback = createEmptyTraceState();
298
+ if (!raw || typeof raw !== "object") return fallback;
299
+ const entries = Array.isArray(raw.entries)
300
+ ? raw.entries.map((entry, index) => normalizeTraceEntry(entry, index)).filter(Boolean)
301
+ : [];
302
+ return {
303
+ runId: parseNonEmptyString(raw.runId),
304
+ requestId: parseNonEmptyString(raw.requestId),
305
+ requestKind: parseNonEmptyString(raw.requestKind),
306
+ status: normalizeTraceStatus(raw.status),
307
+ startedAt: parseFiniteNumber(raw.startedAt),
308
+ updatedAt: parseFiniteNumber(raw.updatedAt),
309
+ entries,
310
+ };
311
+ }
312
+
313
+ function ensureTraceState() {
314
+ if (!traceState) traceState = createEmptyTraceState();
315
+ return traceState;
316
+ }
317
+
318
+ function replaceTraceState(nextState) {
319
+ traceState = normalizeTraceState(nextState);
320
+ renderTraceViewIfActive();
321
+ }
322
+
323
+ function upsertTraceEntry(entry) {
324
+ const normalized = normalizeTraceEntry(entry, ensureTraceState().entries.length);
325
+ if (!normalized) return;
326
+ const state = ensureTraceState();
327
+ const index = state.entries.findIndex((candidate) => candidate.id === normalized.id);
328
+ if (index >= 0) {
329
+ state.entries[index] = normalized;
330
+ } else {
331
+ state.entries.push(normalized);
332
+ }
333
+ state.updatedAt = normalized.updatedAt;
334
+ renderTraceViewIfActive();
335
+ }
336
+
337
+ function appendTraceAssistantDelta(entryId, deltaKind, delta, updatedAt) {
338
+ if (typeof delta !== "string" || !delta) return;
339
+ const state = ensureTraceState();
340
+ const targetId = typeof entryId === "string" && entryId.trim() ? entryId.trim() : null;
341
+ let entry = targetId ? state.entries.find((candidate) => candidate.id === targetId) : null;
342
+ if (!entry || entry.type !== "assistant") {
343
+ entry = normalizeTraceEntry({
344
+ id: targetId || ("trace-assistant-live-" + Date.now()),
345
+ type: "assistant",
346
+ startedAt: updatedAt,
347
+ updatedAt,
348
+ thinking: "",
349
+ text: "",
350
+ status: "streaming",
351
+ stopReason: null,
352
+ }, state.entries.length);
353
+ if (!entry) return;
354
+ state.entries.push(entry);
355
+ }
356
+ if (deltaKind === "thinking") {
357
+ entry.thinking += delta;
358
+ } else {
359
+ entry.text += delta;
360
+ }
361
+ entry.status = "streaming";
362
+ entry.updatedAt = parseFiniteNumber(updatedAt) || Date.now();
363
+ state.updatedAt = entry.updatedAt;
364
+ renderTraceViewIfActive();
365
+ }
366
+
367
+ function updateTraceStatusFromMessage(message) {
368
+ if (!message || typeof message !== "object") return;
369
+ const state = ensureTraceState();
370
+ state.runId = parseNonEmptyString(message.runId) || state.runId;
371
+ if (Object.prototype.hasOwnProperty.call(message, "requestId")) {
372
+ state.requestId = parseNonEmptyString(message.requestId);
373
+ }
374
+ if (Object.prototype.hasOwnProperty.call(message, "requestKind")) {
375
+ state.requestKind = parseNonEmptyString(message.requestKind);
376
+ }
377
+ if (Object.prototype.hasOwnProperty.call(message, "startedAt")) {
378
+ state.startedAt = parseFiniteNumber(message.startedAt);
379
+ }
380
+ if (Object.prototype.hasOwnProperty.call(message, "updatedAt")) {
381
+ state.updatedAt = parseFiniteNumber(message.updatedAt);
382
+ }
383
+ if (Object.prototype.hasOwnProperty.call(message, "status")) {
384
+ state.status = normalizeTraceStatus(message.status);
385
+ }
386
+ renderTraceViewIfActive();
387
+ }
388
+
389
+ function normalizeTraceFilter(filter) {
390
+ return filter === "thinking" || filter === "tools" ? filter : "all";
391
+ }
392
+
393
+ function setTraceFilter(nextFilter) {
394
+ const normalized = normalizeTraceFilter(nextFilter);
395
+ if (traceFilter === normalized) return;
396
+ traceFilter = normalized;
397
+ traceAutoScroll = true;
398
+ renderTraceViewIfActive();
399
+ }
400
+
401
+ function getTraceEntriesForFilter(filterOverride) {
402
+ const state = traceState || createEmptyTraceState();
403
+ const filter = normalizeTraceFilter(filterOverride || traceFilter);
404
+ const entries = Array.isArray(state.entries) ? state.entries : [];
405
+ if (filter === "tools") {
406
+ return entries.filter((entry) => entry.type === "tool");
407
+ }
408
+ if (filter === "thinking") {
409
+ return entries.filter((entry) => entry.type === "assistant" && String(entry.thinking || "").trim());
410
+ }
411
+ return entries.filter((entry) => {
412
+ if (entry.type === "assistant") {
413
+ return Boolean(String(entry.thinking || "").trim() || String(entry.text || "").trim());
414
+ }
415
+ return true;
416
+ });
417
+ }
418
+
419
+ function buildVisibleWorkingText(filterOverride) {
420
+ const filter = normalizeTraceFilter(filterOverride || traceFilter);
421
+ const entries = getTraceEntriesForFilter(filter);
422
+ if (!entries.length) return "";
423
+
424
+ if (filter === "thinking") {
425
+ return entries
426
+ .map((entry) => entry && entry.type === "assistant" ? String(entry.thinking || "").trim() : "")
427
+ .filter(Boolean)
428
+ .join("\n\n");
429
+ }
430
+
431
+ return entries.map((entry) => {
432
+ if (entry.type === "assistant") {
433
+ const parts = [];
434
+ if (String(entry.thinking || "").trim()) {
435
+ parts.push("[Thinking]\n" + String(entry.thinking || "").trim());
436
+ }
437
+ if (filter === "all" && String(entry.text || "").trim()) {
438
+ parts.push("[Response]\n" + String(entry.text || "").trim());
439
+ }
440
+ return ["Assistant", ...parts].join("\n\n").trim();
441
+ }
442
+
443
+ const header = entry.label && entry.label !== entry.toolName
444
+ ? ("Tool: " + String(entry.toolName || "tool") + " — " + entry.label)
445
+ : ("Tool: " + String(entry.toolName || "tool"));
446
+ const parts = [header];
447
+ if (String(entry.argsSummary || "").trim()) {
448
+ parts.push("Input:\n" + String(entry.argsSummary || "").trim());
449
+ }
450
+ if (String(entry.output || "").trim()) {
451
+ parts.push("Output:\n" + String(entry.output || "").trim());
452
+ }
453
+ return parts.join("\n\n").trim();
454
+ }).filter(Boolean).join("\n\n---\n\n");
455
+ }
456
+
457
+ function getWorkingDocumentLabel(filterOverride) {
458
+ const filter = normalizeTraceFilter(filterOverride || traceFilter);
459
+ if (filter === "thinking") return "working (thinking)";
460
+ if (filter === "tools") return "working (tools)";
461
+ return "working";
462
+ }
463
+
464
+ async function copyVisibleWorkingToClipboard() {
465
+ const content = buildVisibleWorkingText();
466
+ if (!content.trim()) {
467
+ setStatus("No visible working details to copy yet.", "warning");
468
+ return;
469
+ }
470
+ try {
471
+ await navigator.clipboard.writeText(content);
472
+ setStatus("Copied visible working text.", "success");
473
+ } catch {
474
+ setStatus("Clipboard write failed.", "warning");
475
+ }
476
+ }
477
+
478
+ function loadVisibleWorkingIntoEditor() {
479
+ const content = buildVisibleWorkingText();
480
+ if (!content.trim()) {
481
+ setStatus("No visible working details to load yet.", "warning");
482
+ return;
483
+ }
484
+ setEditorText(content, { preserveScroll: false, preserveSelection: false });
485
+ setSourceState({ source: "blank", label: getWorkingDocumentLabel(), path: null });
486
+ setStatus("Loaded visible working into editor.", "success");
487
+ }
488
+
489
+ function renderTraceViewIfActive() {
490
+ if (rightView !== "trace") return;
491
+ if (traceRenderRaf !== null) return;
492
+ traceRenderRaf = window.requestAnimationFrame(() => {
493
+ traceRenderRaf = null;
494
+ refreshResponseUi();
495
+ });
496
+ }
497
+
226
498
  contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
227
499
  contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
228
500
  contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
@@ -305,6 +577,8 @@
305
577
  let reviewNotesReturnFocusEl = null;
306
578
  let reviewNotesPersistTimer = null;
307
579
  let reviewNotesLoadNonce = 0;
580
+ let outlineEntries = [];
581
+ let outlineReturnFocusEl = null;
308
582
  let pendingReviewNoteFocusId = null;
309
583
  let pendingReviewNoteInlineFocusId = null;
310
584
  let activePreviewCommentSelection = null;
@@ -402,6 +676,17 @@
402
676
  if (typeof message.label === "string") summary.label = message.label;
403
677
  if (Array.isArray(message.responseHistory)) summary.responseHistoryCount = message.responseHistory.length;
404
678
  if (Array.isArray(message.items)) summary.itemsCount = message.items.length;
679
+ if (message.traceState && typeof message.traceState === "object" && Array.isArray(message.traceState.entries)) {
680
+ summary.traceEntries = message.traceState.entries.length;
681
+ summary.traceStatus = message.traceState.status;
682
+ }
683
+ if (message.trace && typeof message.trace === "object" && Array.isArray(message.trace.entries)) {
684
+ summary.traceEntries = message.trace.entries.length;
685
+ summary.traceStatus = message.trace.status;
686
+ }
687
+ if (typeof message.entryId === "string") summary.entryId = message.entryId;
688
+ if (typeof message.deltaKind === "string") summary.deltaKind = message.deltaKind;
689
+ if (typeof message.delta === "string") summary.deltaLength = message.delta.length;
405
690
  if (typeof message.details === "object" && message.details !== null) summary.details = message.details;
406
691
  return summary;
407
692
  }
@@ -1130,6 +1415,12 @@
1130
1415
  && typeof reviewNotesDialogEl.contains === "function"
1131
1416
  && reviewNotesDialogEl.contains(event.target)
1132
1417
  );
1418
+ const outlineOwnsEvent = Boolean(
1419
+ outlineDialogEl
1420
+ && event.target
1421
+ && typeof outlineDialogEl.contains === "function"
1422
+ && outlineDialogEl.contains(event.target)
1423
+ );
1133
1424
 
1134
1425
  if (isScratchpadOpen() && plainEscape) {
1135
1426
  event.preventDefault();
@@ -1143,7 +1434,13 @@
1143
1434
  return;
1144
1435
  }
1145
1436
 
1146
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent) {
1437
+ if (isOutlineOpen() && plainEscape) {
1438
+ event.preventDefault();
1439
+ closeOutline();
1440
+ return;
1441
+ }
1442
+
1443
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent) {
1147
1444
  return;
1148
1445
  }
1149
1446
 
@@ -1397,6 +1694,21 @@
1397
1694
  function updateReferenceBadge() {
1398
1695
  if (!referenceBadgeEl) return;
1399
1696
 
1697
+ if (rightView === "trace") {
1698
+ const state = traceState || createEmptyTraceState();
1699
+ const entryCount = getTraceEntriesForFilter(traceFilter).length;
1700
+ const time = formatReferenceTime(state.startedAt || state.updatedAt);
1701
+ if (state.status === "idle") {
1702
+ referenceBadgeEl.textContent = "Working: no active run yet";
1703
+ return;
1704
+ }
1705
+ const statusLabel = state.status === "running" ? "live" : "complete";
1706
+ referenceBadgeEl.textContent = "Working: " + statusLabel
1707
+ + (entryCount ? (" · " + entryCount + " entr" + (entryCount === 1 ? "y" : "ies")) : "")
1708
+ + (time ? (" · " + time) : "");
1709
+ return;
1710
+ }
1711
+
1400
1712
  if (rightView === "editor-preview") {
1401
1713
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
1402
1714
  if (hasResponse) {
@@ -1410,26 +1722,6 @@
1410
1722
  }
1411
1723
 
1412
1724
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
1413
- const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
1414
- if (rightView === "thinking") {
1415
- if (!hasResponse && !hasThinking) {
1416
- referenceBadgeEl.textContent = "Thinking: none";
1417
- return;
1418
- }
1419
-
1420
- const time = formatReferenceTime(latestResponseTimestamp);
1421
- const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
1422
- const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
1423
- ? responseHistoryIndex + 1
1424
- : 0;
1425
- const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
1426
- const thinkingLabel = hasThinking ? "assistant thinking" : "assistant thinking unavailable";
1427
- referenceBadgeEl.textContent = time
1428
- ? historyPrefix + thinkingLabel + " · " + time
1429
- : historyPrefix + thinkingLabel;
1430
- return;
1431
- }
1432
-
1433
1725
  if (!hasResponse) {
1434
1726
  referenceBadgeEl.textContent = "Latest response: none";
1435
1727
  return;
@@ -1505,14 +1797,15 @@
1505
1797
  function updateSyncBadge(normalizedEditorText) {
1506
1798
  if (!syncBadgeEl) return;
1507
1799
 
1508
- const showingThinking = rightView === "thinking";
1509
- const hasComparableContent = showingThinking
1510
- ? Boolean(latestResponseThinking && latestResponseThinking.trim())
1511
- : latestResponseHasContent;
1800
+ if (rightView === "trace") {
1801
+ syncBadgeEl.hidden = true;
1802
+ syncBadgeEl.classList.remove("sync");
1803
+ return;
1804
+ }
1512
1805
 
1513
- if (!hasComparableContent) {
1806
+ if (!latestResponseHasContent) {
1514
1807
  syncBadgeEl.hidden = true;
1515
- syncBadgeEl.textContent = showingThinking ? "In sync with thinking" : "In sync with response";
1808
+ syncBadgeEl.textContent = "In sync with response";
1516
1809
  syncBadgeEl.classList.remove("sync");
1517
1810
  return;
1518
1811
  }
@@ -1520,10 +1813,9 @@
1520
1813
  const normalizedEditor = typeof normalizedEditorText === "string"
1521
1814
  ? normalizedEditorText
1522
1815
  : normalizeForCompare(sourceTextEl.value);
1523
- const targetNormalized = showingThinking ? latestResponseThinkingNormalized : latestResponseNormalized;
1524
- const inSync = normalizedEditor === targetNormalized;
1816
+ const inSync = normalizedEditor === latestResponseNormalized;
1525
1817
  syncBadgeEl.hidden = !inSync;
1526
- syncBadgeEl.textContent = showingThinking ? "In sync with thinking" : "In sync with response";
1818
+ syncBadgeEl.textContent = "In sync with response";
1527
1819
 
1528
1820
  if (inSync) {
1529
1821
  syncBadgeEl.classList.add("sync");
@@ -2172,6 +2464,40 @@
2172
2464
  });
2173
2465
  }
2174
2466
 
2467
+ function handleTracePaneScroll() {
2468
+ if (rightView !== "trace") return;
2469
+ traceAutoScroll = shouldStickTraceToBottom();
2470
+ }
2471
+
2472
+ async function handleTracePaneClick(event) {
2473
+ if (rightView !== "trace") return;
2474
+ const target = event.target;
2475
+ const filterBtn = target instanceof Element ? target.closest("[data-trace-filter]") : null;
2476
+ if (filterBtn) {
2477
+ event.preventDefault();
2478
+ const nextFilter = filterBtn.getAttribute("data-trace-filter") || "all";
2479
+ setTraceFilter(nextFilter);
2480
+ return;
2481
+ }
2482
+ const actionBtn = target instanceof Element ? target.closest("[data-trace-action]") : null;
2483
+ if (!actionBtn) return;
2484
+ event.preventDefault();
2485
+ const action = actionBtn.getAttribute("data-trace-action") || "";
2486
+ if (action === "copy") {
2487
+ await copyVisibleWorkingToClipboard();
2488
+ return;
2489
+ }
2490
+ if (action === "load") {
2491
+ loadVisibleWorkingIntoEditor();
2492
+ }
2493
+ }
2494
+
2495
+ function attachResponsePaneInteractionHandlers() {
2496
+ if (!critiqueViewEl) return;
2497
+ critiqueViewEl.addEventListener("scroll", handleTracePaneScroll);
2498
+ critiqueViewEl.addEventListener("click", handleTracePaneClick);
2499
+ }
2500
+
2175
2501
  function replaceResponsePaneWithClone() {
2176
2502
  const currentEl = critiqueViewEl;
2177
2503
  if (!currentEl || !currentEl.parentNode || typeof currentEl.cloneNode !== "function") {
@@ -2185,6 +2511,7 @@
2185
2511
 
2186
2512
  currentEl.parentNode.replaceChild(replacement, currentEl);
2187
2513
  critiqueViewEl = replacement;
2514
+ attachResponsePaneInteractionHandlers();
2188
2515
  return critiqueViewEl;
2189
2516
  }
2190
2517
 
@@ -2680,7 +3007,134 @@
2680
3007
  }, delay);
2681
3008
  }
2682
3009
 
3010
+ function shouldStickTraceToBottom() {
3011
+ if (!critiqueViewEl) return true;
3012
+ const remaining = critiqueViewEl.scrollHeight - critiqueViewEl.scrollTop - critiqueViewEl.clientHeight;
3013
+ return remaining < 56;
3014
+ }
3015
+
3016
+ function buildTracePanelHtml() {
3017
+ const state = traceState || createEmptyTraceState();
3018
+ const filter = normalizeTraceFilter(traceFilter);
3019
+ const entries = getTraceEntriesForFilter(filter);
3020
+ const visibleWorking = buildVisibleWorkingText(filter);
3021
+ const hasVisibleContent = Boolean(visibleWorking.trim());
3022
+ const started = formatReferenceTime(state.startedAt || state.updatedAt);
3023
+ const statusLabel = state.status === "running"
3024
+ ? "Live"
3025
+ : (state.status === "complete" ? "Complete" : "Idle");
3026
+ const filterMeta = filter === "thinking"
3027
+ ? "Thinking only"
3028
+ : (filter === "tools" ? "Tools only" : null);
3029
+ const toolbar = "<div class='trace-toolbar'>"
3030
+ + "<div class='trace-summary'>"
3031
+ + "<span class='trace-summary-badge'>Working</span>"
3032
+ + "<span class='trace-summary-status trace-status-" + escapeHtml(String(state.status || "idle")) + "'>" + escapeHtml(statusLabel) + "</span>"
3033
+ + (started ? ("<span class='trace-summary-meta'>Started " + escapeHtml(started) + "</span>") : "")
3034
+ + (filterMeta ? ("<span class='trace-summary-meta'>" + escapeHtml(filterMeta) + "</span>") : "")
3035
+ + "</div>"
3036
+ + "<div class='trace-controls'>"
3037
+ + "<div class='trace-filter-group' role='tablist' aria-label='Working components'>"
3038
+ + "<button type='button' class='trace-filter-btn" + (filter === "all" ? " is-active" : "") + "' data-trace-filter='all' aria-pressed='" + (filter === "all" ? "true" : "false") + "'>All</button>"
3039
+ + "<button type='button' class='trace-filter-btn" + (filter === "thinking" ? " is-active" : "") + "' data-trace-filter='thinking' aria-pressed='" + (filter === "thinking" ? "true" : "false") + "'>Thinking</button>"
3040
+ + "<button type='button' class='trace-filter-btn" + (filter === "tools" ? " is-active" : "") + "' data-trace-filter='tools' aria-pressed='" + (filter === "tools" ? "true" : "false") + "'>Tools</button>"
3041
+ + "</div>"
3042
+ + "<button type='button' class='trace-action-btn' data-trace-action='load'" + (hasVisibleContent ? "" : " disabled") + ">Load visible into editor</button>"
3043
+ + "<button type='button' class='trace-action-btn' data-trace-action='copy'" + (hasVisibleContent ? "" : " disabled") + ">Copy visible</button>"
3044
+ + "</div>"
3045
+ + "</div>";
3046
+
3047
+ if (!entries.length) {
3048
+ const emptyMessage = filter === "thinking"
3049
+ ? "No thinking steps in this working view yet."
3050
+ : (filter === "tools"
3051
+ ? "No tool steps in this working view yet."
3052
+ : (state.status === "running"
3053
+ ? "Waiting for the first model or tool update…"
3054
+ : "No live working view yet. Start a run or critique to watch working details here."));
3055
+ return "<div class='trace-panel'>" + toolbar + "<div class='trace-empty'>" + escapeHtml(emptyMessage) + "</div></div>";
3056
+ }
3057
+
3058
+ const cards = entries.map((entry) => {
3059
+ if (entry.type === "assistant") {
3060
+ const sections = [];
3061
+ if (String(entry.thinking || "").trim()) {
3062
+ sections.push(
3063
+ "<div class='trace-section'>"
3064
+ + "<div class='trace-section-label'>Thinking</div>"
3065
+ + "<pre class='plain-markdown trace-output'>" + escapeHtml(entry.thinking) + "</pre>"
3066
+ + "</div>"
3067
+ );
3068
+ }
3069
+ if (filter === "all" && String(entry.text || "").trim()) {
3070
+ sections.push(
3071
+ "<div class='trace-section'>"
3072
+ + "<div class='trace-section-label'>Response</div>"
3073
+ + "<pre class='plain-markdown trace-output'>" + escapeHtml(entry.text) + "</pre>"
3074
+ + "</div>"
3075
+ );
3076
+ }
3077
+ if (!sections.length) {
3078
+ sections.push("<div class='trace-empty-inline'>Waiting for streamed content…</div>");
3079
+ }
3080
+ return "<article class='trace-card trace-card-assistant'>"
3081
+ + "<div class='trace-card-header'>"
3082
+ + "<span class='trace-kind-badge'>" + escapeHtml(filter === "thinking" ? "Thinking" : "Assistant") + "</span>"
3083
+ + "<span class='trace-card-meta'>" + escapeHtml(formatReferenceTime(entry.updatedAt) || "live") + "</span>"
3084
+ + "<span class='trace-entry-status trace-entry-status-" + escapeHtml(entry.status) + "'>" + escapeHtml(entry.status === "streaming" ? "Live" : "Complete") + "</span>"
3085
+ + (entry.stopReason ? ("<span class='trace-card-meta'>stop: " + escapeHtml(entry.stopReason) + "</span>") : "")
3086
+ + "</div>"
3087
+ + sections.join("")
3088
+ + "</article>";
3089
+ }
3090
+
3091
+ const title = entry.label || entry.toolName || "tool";
3092
+ const argsSummary = entry.argsSummary
3093
+ ? "<div class='trace-section'><div class='trace-section-label'>Input</div><pre class='plain-markdown trace-output'>" + escapeHtml(entry.argsSummary) + "</pre></div>"
3094
+ : "";
3095
+ const output = entry.output
3096
+ ? "<div class='trace-section'><div class='trace-section-label'>Output</div><pre class='plain-markdown trace-output'>" + escapeHtml(entry.output) + "</pre></div>"
3097
+ : "<div class='trace-empty-inline'>No output yet.</div>";
3098
+ const toolStatusLabel = entry.isError
3099
+ ? "Error"
3100
+ : (entry.status === "streaming" || entry.status === "pending" ? "Live" : "Complete");
3101
+ return "<article class='trace-card trace-card-tool'>"
3102
+ + "<div class='trace-card-header'>"
3103
+ + "<span class='trace-kind-badge'>" + escapeHtml(entry.toolName || "tool") + "</span>"
3104
+ + "<span class='trace-card-title'>" + escapeHtml(title) + "</span>"
3105
+ + "<span class='trace-card-meta'>" + escapeHtml(formatReferenceTime(entry.updatedAt) || "live") + "</span>"
3106
+ + "<span class='trace-entry-status trace-entry-status-" + escapeHtml(entry.status) + "'>" + escapeHtml(toolStatusLabel) + "</span>"
3107
+ + "</div>"
3108
+ + argsSummary
3109
+ + output
3110
+ + "</article>";
3111
+ }).join("");
3112
+
3113
+ return "<div class='trace-panel'>" + toolbar + "<div class='trace-list'>" + cards + "</div></div>";
3114
+ }
3115
+
3116
+ function renderTraceView() {
3117
+ if (!critiqueViewEl) return;
3118
+ const shouldStick = traceAutoScroll || shouldStickTraceToBottom();
3119
+ const previousScrollTop = critiqueViewEl.scrollTop;
3120
+ finishPreviewRender(critiqueViewEl);
3121
+ critiqueViewEl.innerHTML = buildTracePanelHtml();
3122
+ critiqueViewEl.classList.remove("response-scroll-resetting");
3123
+ if (shouldStick) {
3124
+ critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
3125
+ traceAutoScroll = true;
3126
+ } else {
3127
+ critiqueViewEl.scrollTop = previousScrollTop;
3128
+ }
3129
+ scheduleResponsePaneRepaintNudge();
3130
+ }
3131
+
2683
3132
  function renderActiveResult() {
3133
+ if (rightView === "trace") {
3134
+ renderTraceView();
3135
+ return;
3136
+ }
3137
+
2684
3138
  if (rightView === "editor-preview") {
2685
3139
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
2686
3140
  if (!editorText.trim()) {
@@ -2699,17 +3153,6 @@
2699
3153
  return;
2700
3154
  }
2701
3155
 
2702
- if (rightView === "thinking") {
2703
- const thinking = latestResponseThinking;
2704
- finishPreviewRender(critiqueViewEl);
2705
- critiqueViewEl.innerHTML = thinking && thinking.trim()
2706
- ? buildPlainMarkdownHtml(thinking)
2707
- : "<pre class='plain-markdown'>No thinking available for this response.</pre>";
2708
- applyPendingResponseScrollReset();
2709
- scheduleResponsePaneRepaintNudge();
2710
- return;
2711
- }
2712
-
2713
3156
  const markdown = latestResponseMarkdown;
2714
3157
  if (!markdown || !markdown.trim()) {
2715
3158
  finishPreviewRender(critiqueViewEl);
@@ -2753,55 +3196,43 @@
2753
3196
 
2754
3197
  function updateResultActionButtons(normalizedEditorText) {
2755
3198
  const hasResponse = latestResponseHasContent;
2756
- const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
2757
3199
  const normalizedEditor = typeof normalizedEditorText === "string"
2758
3200
  ? normalizedEditorText
2759
3201
  : normalizeForCompare(sourceTextEl.value);
2760
3202
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
2761
- const thinkingLoaded = hasThinking && normalizedEditor === latestResponseThinkingNormalized;
2762
3203
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
2763
- const showingThinking = rightView === "thinking";
3204
+ const showingTrace = rightView === "trace";
3205
+
3206
+ if (responseWrapEl) {
3207
+ responseWrapEl.hidden = showingTrace;
3208
+ }
2764
3209
 
2765
3210
  const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
2766
3211
  const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
2767
3212
 
2768
- if (showingThinking) {
2769
- loadResponseBtn.hidden = false;
2770
- loadCritiqueNotesBtn.hidden = true;
2771
- loadCritiqueFullBtn.hidden = true;
2772
-
2773
- loadResponseBtn.disabled = uiBusy || !hasThinking || thinkingLoaded;
2774
- loadResponseBtn.textContent = !hasThinking
2775
- ? "Thinking unavailable"
2776
- : (thinkingLoaded ? "Thinking already in editor" : "Load thinking into editor");
3213
+ loadResponseBtn.hidden = isCritiqueResponse;
3214
+ loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
3215
+ loadCritiqueFullBtn.hidden = !isCritiqueResponse;
2777
3216
 
2778
- copyResponseBtn.disabled = uiBusy || !hasThinking;
2779
- copyResponseBtn.textContent = "Copy thinking text";
2780
- } else {
2781
- loadResponseBtn.hidden = isCritiqueResponse;
2782
- loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
2783
- loadCritiqueFullBtn.hidden = !isCritiqueResponse;
3217
+ loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
3218
+ loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
2784
3219
 
2785
- loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
2786
- loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
3220
+ loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
3221
+ loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
2787
3222
 
2788
- loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
2789
- loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
3223
+ loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
3224
+ loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
2790
3225
 
2791
- loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
2792
- loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
2793
-
2794
- copyResponseBtn.disabled = uiBusy || !hasResponse;
2795
- copyResponseBtn.textContent = "Copy response text";
2796
- }
3226
+ copyResponseBtn.disabled = uiBusy || !hasResponse;
3227
+ copyResponseBtn.textContent = "Copy response text";
2797
3228
 
2798
3229
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
2799
3230
  const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
2800
3231
  const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
2801
3232
  if (exportPdfBtn) {
2802
3233
  exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
2803
- if (rightView === "thinking") {
2804
- exportPdfBtn.title = "Thinking view does not support PDF export yet.";
3234
+ if (rightView === "trace") {
3235
+ exportPdfBtn.title = "Working view does not support PDF export.";
2805
3236
  } else if (rightView === "markdown") {
2806
3237
  exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
2807
3238
  } else if (!canExportPdf) {
@@ -2991,6 +3422,7 @@
2991
3422
  scheduleEditorMetaUpdate();
2992
3423
  }
2993
3424
  updateEditorSelectionCommentUi();
3425
+ updateOutlineUi();
2994
3426
  }
2995
3427
 
2996
3428
  function setEditorView(nextView) {
@@ -3025,15 +3457,20 @@
3025
3457
  }
3026
3458
  updateReviewNotesUi();
3027
3459
  updateEditorSelectionCommentUi();
3460
+ updateOutlineUi();
3028
3461
  }
3029
3462
 
3030
3463
  function setRightView(nextView) {
3464
+ const previousView = rightView;
3031
3465
  rightView = nextView === "preview"
3032
3466
  ? "preview"
3033
3467
  : (nextView === "editor-preview"
3034
3468
  ? "editor-preview"
3035
- : (nextView === "thinking" ? "thinking" : "markdown"));
3469
+ : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"));
3036
3470
  rightViewSelect.value = rightView;
3471
+ if (rightView === "trace" && previousView !== "trace") {
3472
+ traceAutoScroll = true;
3473
+ }
3037
3474
 
3038
3475
  if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
3039
3476
  window.clearTimeout(responseEditorPreviewTimer);
@@ -4025,6 +4462,10 @@
4025
4462
  return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
4026
4463
  }
4027
4464
 
4465
+ function isOutlineOpen() {
4466
+ return Boolean(outlineOverlayEl && !outlineOverlayEl.hidden);
4467
+ }
4468
+
4028
4469
  function isReviewNotesOpen() {
4029
4470
  return Boolean(reviewNotesOverlayEl && !reviewNotesOverlayEl.hidden);
4030
4471
  }
@@ -4198,6 +4639,400 @@
4198
4639
  };
4199
4640
  }
4200
4641
 
4642
+ function buildOutlineLineIndex(text) {
4643
+ const source = String(text || "").replace(/\r\n/g, "\n");
4644
+ const lines = source.split("\n");
4645
+ const lineOffsets = [];
4646
+ let runningOffset = 0;
4647
+ for (const line of lines) {
4648
+ lineOffsets.push(runningOffset);
4649
+ runningOffset += line.length + 1;
4650
+ }
4651
+ return { source, lines, lineOffsets };
4652
+ }
4653
+
4654
+ function makeOutlineEntry(options) {
4655
+ const entry = options && typeof options === "object" ? options : {};
4656
+ const label = typeof entry.label === "string" ? entry.label.trim() : "";
4657
+ if (!label) return null;
4658
+ const selectionStart = Math.max(0, Math.floor(Number(entry.selectionStart) || 0));
4659
+ const selectionEnd = Math.max(selectionStart, Math.floor(Number(entry.selectionEnd) || selectionStart));
4660
+ return {
4661
+ id: typeof entry.id === "string" && entry.id ? entry.id : makeRequestId(),
4662
+ kind: typeof entry.kind === "string" && entry.kind ? entry.kind : "section",
4663
+ depth: Math.max(1, Math.floor(Number(entry.depth) || 1)),
4664
+ label,
4665
+ lineStart: Math.max(1, Math.floor(Number(entry.lineStart) || 1)),
4666
+ lineEnd: Math.max(Math.max(1, Math.floor(Number(entry.lineStart) || 1)), Math.floor(Number(entry.lineEnd) || Math.max(1, Math.floor(Number(entry.lineStart) || 1)))),
4667
+ selectionStart,
4668
+ selectionEnd,
4669
+ selectedText: typeof entry.selectedText === "string" ? entry.selectedText : "",
4670
+ selectedDisplayText: typeof entry.selectedDisplayText === "string" && entry.selectedDisplayText ? entry.selectedDisplayText : label,
4671
+ };
4672
+ }
4673
+
4674
+ function getOutlineKindLabel(kind) {
4675
+ switch (String(kind || "")) {
4676
+ case "heading": return "Heading";
4677
+ case "section": return "Section";
4678
+ case "subsection": return "Subsection";
4679
+ case "subsubsection": return "Subsubsection";
4680
+ case "paragraph": return "Paragraph";
4681
+ case "subparagraph": return "Subparagraph";
4682
+ case "class": return "Class";
4683
+ case "function": return "Function";
4684
+ case "interface": return "Interface";
4685
+ case "enum": return "Enum";
4686
+ case "type": return "Type";
4687
+ case "struct": return "Struct";
4688
+ case "module": return "Module";
4689
+ case "macro": return "Macro";
4690
+ case "file": return "File";
4691
+ case "hunk": return "Hunk";
4692
+ default: return "Item";
4693
+ }
4694
+ }
4695
+
4696
+ function getOutlineKindBadge(kind) {
4697
+ switch (String(kind || "")) {
4698
+ case "section": return "§";
4699
+ case "subsection": return "§§";
4700
+ case "subsubsection": return "§3";
4701
+ case "paragraph": return "¶";
4702
+ case "subparagraph": return "¶2";
4703
+ case "class": return "class";
4704
+ case "function": return "def";
4705
+ case "interface": return "iface";
4706
+ case "enum": return "enum";
4707
+ case "type": return "type";
4708
+ case "struct": return "struct";
4709
+ case "module": return "mod";
4710
+ case "macro": return "macro";
4711
+ case "file": return "file";
4712
+ case "hunk": return "@@";
4713
+ default: return "#";
4714
+ }
4715
+ }
4716
+
4717
+ function scanMarkdownOutlineEntries(text) {
4718
+ const { source, lines, lineOffsets } = buildOutlineLineIndex(text);
4719
+ const entries = [];
4720
+ let activeFence = null;
4721
+
4722
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4723
+ const line = String(lines[lineIndex] || "");
4724
+ const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
4725
+ if (fenceMatch) {
4726
+ if (!activeFence) {
4727
+ activeFence = fenceMatch[1];
4728
+ } else if (fenceMatch[1][0] === activeFence[0] && fenceMatch[1].length >= activeFence.length) {
4729
+ activeFence = null;
4730
+ }
4731
+ continue;
4732
+ }
4733
+ if (activeFence) continue;
4734
+
4735
+ const atxMatch = line.match(/^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/);
4736
+ if (atxMatch) {
4737
+ const label = normalizeVisiblePreviewText(atxMatch[2] || "");
4738
+ const entry = makeOutlineEntry({
4739
+ kind: atxMatch[1].length === 1 ? "section" : atxMatch[1].length === 2 ? "subsection" : atxMatch[1].length === 3 ? "subsubsection" : "heading",
4740
+ depth: atxMatch[1].length,
4741
+ label,
4742
+ lineStart: lineIndex + 1,
4743
+ lineEnd: lineIndex + 1,
4744
+ selectionStart: lineOffsets[lineIndex] || 0,
4745
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4746
+ selectedText: line,
4747
+ selectedDisplayText: label,
4748
+ });
4749
+ if (entry) entries.push(entry);
4750
+ continue;
4751
+ }
4752
+
4753
+ const nextLine = lineIndex + 1 < lines.length ? String(lines[lineIndex + 1] || "") : "";
4754
+ const setextMatch = nextLine.match(/^ {0,3}(=+|-+)\s*$/);
4755
+ if (setextMatch && normalizeVisiblePreviewText(line)) {
4756
+ const depth = setextMatch[1][0] === "=" ? 1 : 2;
4757
+ const label = normalizeVisiblePreviewText(line);
4758
+ const entry = makeOutlineEntry({
4759
+ kind: depth === 1 ? "section" : "subsection",
4760
+ depth,
4761
+ label,
4762
+ lineStart: lineIndex + 1,
4763
+ lineEnd: lineIndex + 1,
4764
+ selectionStart: lineOffsets[lineIndex] || 0,
4765
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4766
+ selectedText: line,
4767
+ selectedDisplayText: label,
4768
+ });
4769
+ if (entry) entries.push(entry);
4770
+ lineIndex += 1;
4771
+ }
4772
+ }
4773
+
4774
+ return entries;
4775
+ }
4776
+
4777
+ const LATEX_OUTLINE_LEVEL_BY_COMMAND = {
4778
+ part: 1,
4779
+ chapter: 1,
4780
+ section: 1,
4781
+ subsection: 2,
4782
+ subsubsection: 3,
4783
+ paragraph: 4,
4784
+ subparagraph: 5,
4785
+ };
4786
+
4787
+ function scanLatexOutlineEntries(text) {
4788
+ const source = String(text || "").replace(/\r\n/g, "\n");
4789
+ const bodyRange = findLatexDocumentBodyRange(source);
4790
+ const bodyStart = Math.max(0, Math.min(bodyRange.start, source.length));
4791
+ const bodyEnd = Math.max(bodyStart, Math.min(bodyRange.end, source.length));
4792
+ const bodyText = source.slice(bodyStart, bodyEnd);
4793
+ const { lines, lineOffsets } = buildOutlineLineIndex(bodyText);
4794
+ const entries = [];
4795
+
4796
+ function getLine(index) {
4797
+ return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
4798
+ }
4799
+
4800
+ function getStrippedLine(index) {
4801
+ return stripLatexPreviewComments(getLine(index)).trim();
4802
+ }
4803
+
4804
+ function isBibliographyCommandLine(index) {
4805
+ return /^\\(?:bibliographystyle|bibliography|printbibliography)\b/i.test(getStrippedLine(index));
4806
+ }
4807
+
4808
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4809
+ let chunk = getLine(lineIndex);
4810
+ let endLineIndex = lineIndex;
4811
+ let heading = readLatexHeadingChunk(chunk);
4812
+ if (/^\s*\\(?:part|chapter|section|subsection|subsubsection|paragraph|subparagraph)\b/.test(chunk)) {
4813
+ while (!heading && endLineIndex + 1 < lines.length && endLineIndex < lineIndex + 5) {
4814
+ endLineIndex += 1;
4815
+ chunk += "\n" + getLine(endLineIndex);
4816
+ heading = readLatexHeadingChunk(chunk);
4817
+ }
4818
+ }
4819
+ if (heading) {
4820
+ const label = extractLatexPreviewVisibleText(heading.titleText || "");
4821
+ const kind = String(heading.commandName || "section").replace(/\*$/, "").toLowerCase();
4822
+ const entry = makeOutlineEntry({
4823
+ kind,
4824
+ depth: LATEX_OUTLINE_LEVEL_BY_COMMAND[kind] || 1,
4825
+ label,
4826
+ lineStart: lineIndex + 1,
4827
+ lineEnd: endLineIndex + 1,
4828
+ selectionStart: bodyStart + (lineOffsets[lineIndex] || 0),
4829
+ selectionEnd: bodyStart + (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length,
4830
+ selectedText: source.slice(bodyStart + (lineOffsets[lineIndex] || 0), bodyStart + (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length),
4831
+ selectedDisplayText: label,
4832
+ });
4833
+ if (entry) entries.push(entry);
4834
+ lineIndex = endLineIndex;
4835
+ continue;
4836
+ }
4837
+
4838
+ if (isBibliographyCommandLine(lineIndex)) {
4839
+ let endLine = lineIndex;
4840
+ while (endLine + 1 < lines.length && isBibliographyCommandLine(endLine + 1)) {
4841
+ endLine += 1;
4842
+ }
4843
+ const entry = makeOutlineEntry({
4844
+ kind: "section",
4845
+ depth: 1,
4846
+ label: "References",
4847
+ lineStart: lineIndex + 1,
4848
+ lineEnd: endLine + 1,
4849
+ selectionStart: bodyStart + (lineOffsets[lineIndex] || 0),
4850
+ selectionEnd: bodyStart + (lineOffsets[endLine] || 0) + getLine(endLine).length,
4851
+ selectedText: source.slice(bodyStart + (lineOffsets[lineIndex] || 0), bodyStart + (lineOffsets[endLine] || 0) + getLine(endLine).length),
4852
+ selectedDisplayText: "References",
4853
+ });
4854
+ if (entry) entries.push(entry);
4855
+ lineIndex = endLine;
4856
+ }
4857
+ }
4858
+
4859
+ return entries;
4860
+ }
4861
+
4862
+ function scanPythonOutlineEntries(text) {
4863
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4864
+ const entries = [];
4865
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4866
+ const line = String(lines[lineIndex] || "");
4867
+ const classMatch = line.match(/^(\s*)class\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
4868
+ const defMatch = line.match(/^(\s*)(?:async\s+def|def)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
4869
+ const match = classMatch || defMatch;
4870
+ if (!match) continue;
4871
+ const indent = String(match[1] || "").replace(/\t/g, " ").length;
4872
+ const label = String(match[2] || "");
4873
+ const kind = classMatch ? "class" : "function";
4874
+ const entry = makeOutlineEntry({
4875
+ kind,
4876
+ depth: Math.max(1, Math.floor(indent / 4) + 1),
4877
+ label,
4878
+ lineStart: lineIndex + 1,
4879
+ lineEnd: lineIndex + 1,
4880
+ selectionStart: lineOffsets[lineIndex] || 0,
4881
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4882
+ selectedText: line,
4883
+ selectedDisplayText: label,
4884
+ });
4885
+ if (entry) entries.push(entry);
4886
+ }
4887
+ return entries;
4888
+ }
4889
+
4890
+ function scanJsLikeOutlineEntries(text) {
4891
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4892
+ const entries = [];
4893
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4894
+ const line = String(lines[lineIndex] || "");
4895
+ const patterns = [
4896
+ { kind: "class", match: line.match(/^(\s*)(?:export\s+)?(?:default\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
4897
+ { kind: "function", match: line.match(/^(\s*)(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/) },
4898
+ { kind: "function", match: line.match(/^(\s*)(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/) },
4899
+ { kind: "interface", match: line.match(/^(\s*)(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
4900
+ { kind: "enum", match: line.match(/^(\s*)(?:export\s+)?enum\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
4901
+ { kind: "type", match: line.match(/^(\s*)(?:export\s+)?type\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
4902
+ ];
4903
+ const found = patterns.find((entry) => entry.match);
4904
+ if (!found || !found.match) continue;
4905
+ const indent = String(found.match[1] || "").replace(/\t/g, " ").length;
4906
+ const label = String(found.match[2] || "");
4907
+ const entry = makeOutlineEntry({
4908
+ kind: found.kind,
4909
+ depth: Math.max(1, Math.floor(indent / 2) + 1),
4910
+ label,
4911
+ lineStart: lineIndex + 1,
4912
+ lineEnd: lineIndex + 1,
4913
+ selectionStart: lineOffsets[lineIndex] || 0,
4914
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4915
+ selectedText: line,
4916
+ selectedDisplayText: label,
4917
+ });
4918
+ if (entry) entries.push(entry);
4919
+ }
4920
+ return entries;
4921
+ }
4922
+
4923
+ function scanJuliaOutlineEntries(text) {
4924
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4925
+ const entries = [];
4926
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4927
+ const line = String(lines[lineIndex] || "");
4928
+ const patterns = [
4929
+ { kind: "module", match: line.match(/^(\s*)module\s+([A-Za-z_][A-Za-z0-9_]*)\b/) },
4930
+ { kind: "struct", match: line.match(/^(\s*)(?:mutable\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)\b/) },
4931
+ { kind: "function", match: line.match(/^(\s*)function\s+([A-Za-z_][A-Za-z0-9_!]*)\s*\(/) },
4932
+ { kind: "macro", match: line.match(/^(\s*)macro\s+([A-Za-z_][A-Za-z0-9_!]*)\b/) },
4933
+ ];
4934
+ const found = patterns.find((entry) => entry.match);
4935
+ if (!found || !found.match) continue;
4936
+ const indent = String(found.match[1] || "").replace(/\t/g, " ").length;
4937
+ const label = String(found.match[2] || "");
4938
+ const entry = makeOutlineEntry({
4939
+ kind: found.kind,
4940
+ depth: Math.max(1, Math.floor(indent / 2) + 1),
4941
+ label,
4942
+ lineStart: lineIndex + 1,
4943
+ lineEnd: lineIndex + 1,
4944
+ selectionStart: lineOffsets[lineIndex] || 0,
4945
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4946
+ selectedText: line,
4947
+ selectedDisplayText: label,
4948
+ });
4949
+ if (entry) entries.push(entry);
4950
+ }
4951
+ return entries;
4952
+ }
4953
+
4954
+ function scanBashOutlineEntries(text) {
4955
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4956
+ const entries = [];
4957
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4958
+ const line = String(lines[lineIndex] || "");
4959
+ const match = line.match(/^(\s*)(?:function\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*\{/);
4960
+ if (!match) continue;
4961
+ const indent = String(match[1] || "").replace(/\t/g, " ").length;
4962
+ const label = String(match[2] || "");
4963
+ const entry = makeOutlineEntry({
4964
+ kind: "function",
4965
+ depth: Math.max(1, Math.floor(indent / 2) + 1),
4966
+ label,
4967
+ lineStart: lineIndex + 1,
4968
+ lineEnd: lineIndex + 1,
4969
+ selectionStart: lineOffsets[lineIndex] || 0,
4970
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4971
+ selectedText: line,
4972
+ selectedDisplayText: label,
4973
+ });
4974
+ if (entry) entries.push(entry);
4975
+ }
4976
+ return entries;
4977
+ }
4978
+
4979
+ function scanDiffOutlineEntries(text) {
4980
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4981
+ const entries = [];
4982
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4983
+ const line = String(lines[lineIndex] || "");
4984
+ let kind = "";
4985
+ let label = "";
4986
+ let depth = 1;
4987
+ const fileMatch = line.match(/^diff\s+--git\s+a\/([^\s]+)\s+b\/([^\s]+)/);
4988
+ if (fileMatch) {
4989
+ kind = "file";
4990
+ label = String(fileMatch[2] || fileMatch[1] || "");
4991
+ depth = 1;
4992
+ } else if (/^@@/.test(line)) {
4993
+ kind = "hunk";
4994
+ label = line.replace(/^@@\s*|\s*@@.*$/g, "").trim() || line.trim();
4995
+ depth = 2;
4996
+ }
4997
+ if (!kind || !label) continue;
4998
+ const entry = makeOutlineEntry({
4999
+ kind,
5000
+ depth,
5001
+ label,
5002
+ lineStart: lineIndex + 1,
5003
+ lineEnd: lineIndex + 1,
5004
+ selectionStart: lineOffsets[lineIndex] || 0,
5005
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
5006
+ selectedText: line,
5007
+ selectedDisplayText: label,
5008
+ });
5009
+ if (entry) entries.push(entry);
5010
+ }
5011
+ return entries;
5012
+ }
5013
+
5014
+ function scanOutlineEntries(text, language) {
5015
+ switch (String(language || "").toLowerCase()) {
5016
+ case "markdown":
5017
+ return scanMarkdownOutlineEntries(text);
5018
+ case "latex":
5019
+ return scanLatexOutlineEntries(text);
5020
+ case "python":
5021
+ return scanPythonOutlineEntries(text);
5022
+ case "javascript":
5023
+ case "typescript":
5024
+ return scanJsLikeOutlineEntries(text);
5025
+ case "julia":
5026
+ return scanJuliaOutlineEntries(text);
5027
+ case "bash":
5028
+ return scanBashOutlineEntries(text);
5029
+ case "diff":
5030
+ return scanDiffOutlineEntries(text);
5031
+ default:
5032
+ return [];
5033
+ }
5034
+ }
5035
+
4201
5036
  function cloneReviewNotes(notes) {
4202
5037
  return Array.isArray(notes)
4203
5038
  ? notes
@@ -7011,6 +7846,138 @@
7011
7846
  updateEditorSelectionCommentUi();
7012
7847
  }
7013
7848
 
7849
+ function getOutlineEntriesForCurrentEditor() {
7850
+ return scanOutlineEntries(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "", editorLanguage || "markdown");
7851
+ }
7852
+
7853
+ function updateOutlineUi() {
7854
+ outlineEntries = getOutlineEntriesForCurrentEditor();
7855
+ const descriptor = getCurrentStudioDocumentDescriptor();
7856
+ const count = outlineEntries.length;
7857
+ const hasEntries = count > 0;
7858
+ const isOpen = isOutlineOpen();
7859
+ if (outlineBtn) {
7860
+ outlineBtn.textContent = "Outline";
7861
+ outlineBtn.classList.remove("has-content");
7862
+ outlineBtn.classList.toggle("is-active", isOpen);
7863
+ outlineBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
7864
+ outlineBtn.title = isOpen
7865
+ ? "Hide document outline."
7866
+ : (hasEntries
7867
+ ? (count + " outline entr" + (count === 1 ? "y" : "ies") + " for " + descriptor.label + ". Open the outline rail.")
7868
+ : "Open document outline for the current editor text.");
7869
+ }
7870
+ if (outlineMetaEl) {
7871
+ outlineMetaEl.textContent = hasEntries
7872
+ ? (count + " entr" + (count === 1 ? "y" : "ies") + " · " + (editorLanguage || "text") + " · " + descriptor.label)
7873
+ : ("No outline entries · " + (editorLanguage || "text"));
7874
+ }
7875
+ if (outlineDoneBtn) {
7876
+ outlineDoneBtn.disabled = !isOpen;
7877
+ }
7878
+ if (outlineEmptyStateEl) {
7879
+ outlineEmptyStateEl.hidden = hasEntries;
7880
+ }
7881
+ renderOutlineList();
7882
+ }
7883
+
7884
+ function renderOutlineList() {
7885
+ if (!outlineListEl) return;
7886
+ outlineListEl.innerHTML = "";
7887
+ for (const entry of outlineEntries) {
7888
+ const itemBtn = document.createElement("button");
7889
+ itemBtn.type = "button";
7890
+ itemBtn.className = "outline-entry";
7891
+ itemBtn.dataset.outlineId = String(entry.id || "");
7892
+ itemBtn.style.paddingLeft = (10 + Math.max(0, (entry.depth || 1) - 1) * 14) + "px";
7893
+ itemBtn.title = getOutlineKindLabel(entry.kind) + " · line " + String(entry.lineStart || 1) + "\n" + String(entry.label || "");
7894
+
7895
+ const kindEl = document.createElement("span");
7896
+ kindEl.className = "outline-entry-kind";
7897
+ kindEl.textContent = getOutlineKindBadge(entry.kind);
7898
+ itemBtn.appendChild(kindEl);
7899
+
7900
+ const titleEl = document.createElement("span");
7901
+ titleEl.className = "outline-entry-title";
7902
+ titleEl.textContent = String(entry.label || "");
7903
+ itemBtn.appendChild(titleEl);
7904
+
7905
+ const metaEl = document.createElement("span");
7906
+ metaEl.className = "outline-entry-meta";
7907
+ metaEl.textContent = "L" + String(entry.lineStart || 1);
7908
+ itemBtn.appendChild(metaEl);
7909
+
7910
+ outlineListEl.appendChild(itemBtn);
7911
+ }
7912
+ }
7913
+
7914
+ function buildOutlineEntryAnchor(entry) {
7915
+ if (!entry) return null;
7916
+ return normalizeReviewNote({
7917
+ selectionStart: entry.selectionStart,
7918
+ selectionEnd: entry.selectionEnd,
7919
+ lineStart: entry.lineStart,
7920
+ lineEnd: entry.lineEnd,
7921
+ selectedText: entry.selectedText,
7922
+ selectedDisplayText: entry.selectedDisplayText || entry.label,
7923
+ });
7924
+ }
7925
+
7926
+ function jumpToOutlineEntry(entryId) {
7927
+ const entry = outlineEntries.find((candidate) => candidate && String(candidate.id || "") === String(entryId || ""));
7928
+ if (!entry) return false;
7929
+ const anchor = buildOutlineEntryAnchor(entry);
7930
+ if (!anchor) return false;
7931
+ return jumpToReviewAnchor(anchor, {
7932
+ statusMessage: "Jumped to outline entry.",
7933
+ afterJump: () => {
7934
+ revealReviewNoteInPreview(anchor);
7935
+ },
7936
+ });
7937
+ }
7938
+
7939
+ function closeOutline(options) {
7940
+ if (!outlineOverlayEl || outlineOverlayEl.hidden) return;
7941
+ outlineOverlayEl.hidden = true;
7942
+ updateOutlineUi();
7943
+ if (editorView === "markdown") {
7944
+ scheduleEditorLineNumberRender();
7945
+ }
7946
+ const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
7947
+ ? options.focusTarget
7948
+ : (outlineReturnFocusEl || outlineBtn || sourceTextEl);
7949
+ outlineReturnFocusEl = null;
7950
+ if (focusTarget && typeof focusTarget.focus === "function") {
7951
+ const schedule = typeof window.requestAnimationFrame === "function"
7952
+ ? window.requestAnimationFrame.bind(window)
7953
+ : (cb) => window.setTimeout(cb, 16);
7954
+ schedule(() => focusTarget.focus());
7955
+ }
7956
+ }
7957
+
7958
+ function openOutline() {
7959
+ if (!outlineOverlayEl) return;
7960
+ if (isReviewNotesOpen()) {
7961
+ closeReviewNotes({ focusTarget: null });
7962
+ }
7963
+ outlineReturnFocusEl = document.activeElement && document.activeElement !== document.body
7964
+ ? document.activeElement
7965
+ : sourceTextEl;
7966
+ outlineOverlayEl.hidden = false;
7967
+ updateOutlineUi();
7968
+ if (editorView === "markdown") {
7969
+ scheduleEditorLineNumberRender();
7970
+ }
7971
+ }
7972
+
7973
+ function toggleOutline() {
7974
+ if (isOutlineOpen()) {
7975
+ closeOutline({ focusTarget: outlineBtn || sourceTextEl });
7976
+ } else {
7977
+ openOutline();
7978
+ }
7979
+ }
7980
+
7014
7981
  function updateReviewNotesUi() {
7015
7982
  const descriptor = getCurrentStudioDocumentDescriptor();
7016
7983
  const count = reviewNotes.length;
@@ -7382,6 +8349,18 @@
7382
8349
  selection.removeAllRanges();
7383
8350
  }
7384
8351
  clearPreviewCommentSelection();
8352
+ const current = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
8353
+ const range = resolveReviewNoteRange(previewNote, current);
8354
+ if (range && sourceTextEl) {
8355
+ try {
8356
+ sourceTextEl.focus({ preventScroll: true });
8357
+ } catch {
8358
+ sourceTextEl.focus();
8359
+ }
8360
+ if (typeof sourceTextEl.setSelectionRange === "function") {
8361
+ sourceTextEl.setSelectionRange(range.start, range.end);
8362
+ }
8363
+ }
7385
8364
  });
7386
8365
  },
7387
8366
  });
@@ -7537,6 +8516,9 @@
7537
8516
  if (isReviewNotesOpen()) {
7538
8517
  closeReviewNotes({ focusTarget: null });
7539
8518
  }
8519
+ if (isOutlineOpen()) {
8520
+ closeOutline({ focusTarget: null });
8521
+ }
7540
8522
  scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
7541
8523
  ? document.activeElement
7542
8524
  : sourceTextEl;
@@ -7580,6 +8562,9 @@
7580
8562
  if (isScratchpadOpen()) {
7581
8563
  closeScratchpad({ focusTarget: null });
7582
8564
  }
8565
+ if (isOutlineOpen()) {
8566
+ closeOutline({ focusTarget: null });
8567
+ }
7583
8568
  reviewNotesReturnFocusEl = document.activeElement && document.activeElement !== document.body
7584
8569
  ? document.activeElement
7585
8570
  : sourceTextEl;
@@ -7697,6 +8682,7 @@
7697
8682
  if (editorView === "preview") {
7698
8683
  scheduleSourcePreviewRender(0);
7699
8684
  }
8685
+ updateOutlineUi();
7700
8686
  }
7701
8687
 
7702
8688
  function setEditorHighlightMode(mode) {
@@ -8007,6 +8993,10 @@
8007
8993
  }
8008
8994
  }
8009
8995
 
8996
+ if (message.traceState) {
8997
+ replaceTraceState(message.traceState);
8998
+ }
8999
+
8010
9000
  let appliedHistory = false;
8011
9001
  if (Array.isArray(message.responseHistory)) {
8012
9002
  appliedHistory = setResponseHistory(message.responseHistory, {
@@ -8053,6 +9043,26 @@
8053
9043
  return;
8054
9044
  }
8055
9045
 
9046
+ if (message.type === "trace_reset") {
9047
+ replaceTraceState(message.trace);
9048
+ return;
9049
+ }
9050
+
9051
+ if (message.type === "trace_status") {
9052
+ updateTraceStatusFromMessage(message);
9053
+ return;
9054
+ }
9055
+
9056
+ if (message.type === "trace_entry_upsert") {
9057
+ upsertTraceEntry(message.entry);
9058
+ return;
9059
+ }
9060
+
9061
+ if (message.type === "trace_assistant_delta") {
9062
+ appendTraceAssistantDelta(message.entryId, message.deltaKind, message.delta, message.updatedAt);
9063
+ return;
9064
+ }
9065
+
8056
9066
  if (message.type === "request_started") {
8057
9067
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
8058
9068
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
@@ -8750,6 +9760,8 @@
8750
9760
  setRightView(rightViewSelect.value);
8751
9761
  });
8752
9762
 
9763
+ attachResponsePaneInteractionHandlers();
9764
+
8753
9765
  followSelect.addEventListener("change", () => {
8754
9766
  followLatest = followSelect.value !== "off";
8755
9767
  if (followLatest && queuedLatestResponse) {
@@ -8896,6 +9908,7 @@
8896
9908
  renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
8897
9909
  scheduleEditorMetaUpdate();
8898
9910
  updateEditorSelectionCommentUi();
9911
+ updateOutlineUi();
8899
9912
  if (isReviewNotesOpen() && reviewNotes.length > 0) {
8900
9913
  renderReviewNotesList();
8901
9914
  updateReviewNotesUi();
@@ -8991,17 +10004,6 @@
8991
10004
  });
8992
10005
 
8993
10006
  loadResponseBtn.addEventListener("click", () => {
8994
- if (rightView === "thinking") {
8995
- if (!latestResponseThinking.trim()) {
8996
- setStatus("No thinking available for the selected response.", "warning");
8997
- return;
8998
- }
8999
- setEditorText(latestResponseThinking, { preserveScroll: false, preserveSelection: false });
9000
- setSourceState({ source: "blank", label: "assistant thinking", path: null });
9001
- setStatus("Loaded thinking into editor.", "success");
9002
- return;
9003
- }
9004
-
9005
10007
  if (!latestResponseMarkdown.trim()) {
9006
10008
  setStatus("No response available yet.", "warning");
9007
10009
  return;
@@ -9040,15 +10042,15 @@
9040
10042
  });
9041
10043
 
9042
10044
  copyResponseBtn.addEventListener("click", async () => {
9043
- const content = rightView === "thinking" ? latestResponseThinking : latestResponseMarkdown;
10045
+ const content = latestResponseMarkdown;
9044
10046
  if (!content.trim()) {
9045
- setStatus(rightView === "thinking" ? "No thinking available yet." : "No response available yet.", "warning");
10047
+ setStatus("No response available yet.", "warning");
9046
10048
  return;
9047
10049
  }
9048
10050
 
9049
10051
  try {
9050
10052
  await navigator.clipboard.writeText(content);
9051
- setStatus(rightView === "thinking" ? "Copied thinking text." : "Copied response text.", "success");
10053
+ setStatus("Copied response text.", "success");
9052
10054
  } catch (error) {
9053
10055
  setStatus("Clipboard write failed.", "warning");
9054
10056
  }
@@ -9284,6 +10286,35 @@
9284
10286
  });
9285
10287
  }
9286
10288
 
10289
+ if (outlineBtn) {
10290
+ outlineBtn.addEventListener("click", () => {
10291
+ toggleOutline();
10292
+ });
10293
+ }
10294
+
10295
+ if (outlineCloseBtn) {
10296
+ outlineCloseBtn.addEventListener("click", () => {
10297
+ closeOutline();
10298
+ });
10299
+ }
10300
+
10301
+ if (outlineDoneBtn) {
10302
+ outlineDoneBtn.addEventListener("click", () => {
10303
+ closeOutline();
10304
+ });
10305
+ }
10306
+
10307
+ if (outlineListEl) {
10308
+ outlineListEl.addEventListener("click", (event) => {
10309
+ const target = event.target;
10310
+ const entryBtn = target instanceof Element ? target.closest(".outline-entry") : null;
10311
+ if (!entryBtn) return;
10312
+ const outlineId = entryBtn.getAttribute("data-outline-id") || "";
10313
+ if (!outlineId) return;
10314
+ jumpToOutlineEntry(outlineId);
10315
+ });
10316
+ }
10317
+
9287
10318
  if (reviewNotesCloseBtn) {
9288
10319
  reviewNotesCloseBtn.addEventListener("click", () => {
9289
10320
  closeReviewNotes();