pi-studio 0.5.52 → 0.5.54

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");
@@ -179,6 +183,10 @@
179
183
  let latestCritiqueNotesNormalized = "";
180
184
  let responseHistory = [];
181
185
  let responseHistoryIndex = -1;
186
+ let traceState = null;
187
+ let traceFilter = "all";
188
+ let traceAutoScroll = true;
189
+ let traceRenderRaf = null;
182
190
  let studioRunChainActive = false;
183
191
  let queuedSteeringCount = 0;
184
192
  let agentBusyFromServer = false;
@@ -231,6 +239,262 @@
231
239
  return changed;
232
240
  }
233
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
+
234
498
  contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
235
499
  contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
236
500
  contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
@@ -412,6 +676,17 @@
412
676
  if (typeof message.label === "string") summary.label = message.label;
413
677
  if (Array.isArray(message.responseHistory)) summary.responseHistoryCount = message.responseHistory.length;
414
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;
415
690
  if (typeof message.details === "object" && message.details !== null) summary.details = message.details;
416
691
  return summary;
417
692
  }
@@ -1419,6 +1694,21 @@
1419
1694
  function updateReferenceBadge() {
1420
1695
  if (!referenceBadgeEl) return;
1421
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
+
1422
1712
  if (rightView === "editor-preview") {
1423
1713
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
1424
1714
  if (hasResponse) {
@@ -1432,26 +1722,6 @@
1432
1722
  }
1433
1723
 
1434
1724
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
1435
- const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
1436
- if (rightView === "thinking") {
1437
- if (!hasResponse && !hasThinking) {
1438
- referenceBadgeEl.textContent = "Thinking: none";
1439
- return;
1440
- }
1441
-
1442
- const time = formatReferenceTime(latestResponseTimestamp);
1443
- const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
1444
- const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
1445
- ? responseHistoryIndex + 1
1446
- : 0;
1447
- const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
1448
- const thinkingLabel = hasThinking ? "assistant thinking" : "assistant thinking unavailable";
1449
- referenceBadgeEl.textContent = time
1450
- ? historyPrefix + thinkingLabel + " · " + time
1451
- : historyPrefix + thinkingLabel;
1452
- return;
1453
- }
1454
-
1455
1725
  if (!hasResponse) {
1456
1726
  referenceBadgeEl.textContent = "Latest response: none";
1457
1727
  return;
@@ -1527,14 +1797,15 @@
1527
1797
  function updateSyncBadge(normalizedEditorText) {
1528
1798
  if (!syncBadgeEl) return;
1529
1799
 
1530
- const showingThinking = rightView === "thinking";
1531
- const hasComparableContent = showingThinking
1532
- ? Boolean(latestResponseThinking && latestResponseThinking.trim())
1533
- : latestResponseHasContent;
1800
+ if (rightView === "trace") {
1801
+ syncBadgeEl.hidden = true;
1802
+ syncBadgeEl.classList.remove("sync");
1803
+ return;
1804
+ }
1534
1805
 
1535
- if (!hasComparableContent) {
1806
+ if (!latestResponseHasContent) {
1536
1807
  syncBadgeEl.hidden = true;
1537
- syncBadgeEl.textContent = showingThinking ? "In sync with thinking" : "In sync with response";
1808
+ syncBadgeEl.textContent = "In sync with response";
1538
1809
  syncBadgeEl.classList.remove("sync");
1539
1810
  return;
1540
1811
  }
@@ -1542,10 +1813,9 @@
1542
1813
  const normalizedEditor = typeof normalizedEditorText === "string"
1543
1814
  ? normalizedEditorText
1544
1815
  : normalizeForCompare(sourceTextEl.value);
1545
- const targetNormalized = showingThinking ? latestResponseThinkingNormalized : latestResponseNormalized;
1546
- const inSync = normalizedEditor === targetNormalized;
1816
+ const inSync = normalizedEditor === latestResponseNormalized;
1547
1817
  syncBadgeEl.hidden = !inSync;
1548
- syncBadgeEl.textContent = showingThinking ? "In sync with thinking" : "In sync with response";
1818
+ syncBadgeEl.textContent = "In sync with response";
1549
1819
 
1550
1820
  if (inSync) {
1551
1821
  syncBadgeEl.classList.add("sync");
@@ -2194,6 +2464,40 @@
2194
2464
  });
2195
2465
  }
2196
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
+
2197
2501
  function replaceResponsePaneWithClone() {
2198
2502
  const currentEl = critiqueViewEl;
2199
2503
  if (!currentEl || !currentEl.parentNode || typeof currentEl.cloneNode !== "function") {
@@ -2207,6 +2511,7 @@
2207
2511
 
2208
2512
  currentEl.parentNode.replaceChild(replacement, currentEl);
2209
2513
  critiqueViewEl = replacement;
2514
+ attachResponsePaneInteractionHandlers();
2210
2515
  return critiqueViewEl;
2211
2516
  }
2212
2517
 
@@ -2702,7 +3007,134 @@
2702
3007
  }, delay);
2703
3008
  }
2704
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
+
2705
3132
  function renderActiveResult() {
3133
+ if (rightView === "trace") {
3134
+ renderTraceView();
3135
+ return;
3136
+ }
3137
+
2706
3138
  if (rightView === "editor-preview") {
2707
3139
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
2708
3140
  if (!editorText.trim()) {
@@ -2721,17 +3153,6 @@
2721
3153
  return;
2722
3154
  }
2723
3155
 
2724
- if (rightView === "thinking") {
2725
- const thinking = latestResponseThinking;
2726
- finishPreviewRender(critiqueViewEl);
2727
- critiqueViewEl.innerHTML = thinking && thinking.trim()
2728
- ? buildPlainMarkdownHtml(thinking)
2729
- : "<pre class='plain-markdown'>No thinking available for this response.</pre>";
2730
- applyPendingResponseScrollReset();
2731
- scheduleResponsePaneRepaintNudge();
2732
- return;
2733
- }
2734
-
2735
3156
  const markdown = latestResponseMarkdown;
2736
3157
  if (!markdown || !markdown.trim()) {
2737
3158
  finishPreviewRender(critiqueViewEl);
@@ -2775,55 +3196,43 @@
2775
3196
 
2776
3197
  function updateResultActionButtons(normalizedEditorText) {
2777
3198
  const hasResponse = latestResponseHasContent;
2778
- const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
2779
3199
  const normalizedEditor = typeof normalizedEditorText === "string"
2780
3200
  ? normalizedEditorText
2781
3201
  : normalizeForCompare(sourceTextEl.value);
2782
3202
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
2783
- const thinkingLoaded = hasThinking && normalizedEditor === latestResponseThinkingNormalized;
2784
3203
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
2785
- const showingThinking = rightView === "thinking";
3204
+ const showingTrace = rightView === "trace";
3205
+
3206
+ if (responseWrapEl) {
3207
+ responseWrapEl.hidden = showingTrace;
3208
+ }
2786
3209
 
2787
3210
  const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
2788
3211
  const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
2789
3212
 
2790
- if (showingThinking) {
2791
- loadResponseBtn.hidden = false;
2792
- loadCritiqueNotesBtn.hidden = true;
2793
- loadCritiqueFullBtn.hidden = true;
2794
-
2795
- loadResponseBtn.disabled = uiBusy || !hasThinking || thinkingLoaded;
2796
- loadResponseBtn.textContent = !hasThinking
2797
- ? "Thinking unavailable"
2798
- : (thinkingLoaded ? "Thinking already in editor" : "Load thinking into editor");
2799
-
2800
- copyResponseBtn.disabled = uiBusy || !hasThinking;
2801
- copyResponseBtn.textContent = "Copy thinking text";
2802
- } else {
2803
- loadResponseBtn.hidden = isCritiqueResponse;
2804
- loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
2805
- loadCritiqueFullBtn.hidden = !isCritiqueResponse;
3213
+ loadResponseBtn.hidden = isCritiqueResponse;
3214
+ loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
3215
+ loadCritiqueFullBtn.hidden = !isCritiqueResponse;
2806
3216
 
2807
- loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
2808
- loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
3217
+ loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
3218
+ loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
2809
3219
 
2810
- loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
2811
- loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
3220
+ loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
3221
+ loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
2812
3222
 
2813
- loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
2814
- loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
3223
+ loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
3224
+ loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
2815
3225
 
2816
- copyResponseBtn.disabled = uiBusy || !hasResponse;
2817
- copyResponseBtn.textContent = "Copy response text";
2818
- }
3226
+ copyResponseBtn.disabled = uiBusy || !hasResponse;
3227
+ copyResponseBtn.textContent = "Copy response text";
2819
3228
 
2820
3229
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
2821
3230
  const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
2822
3231
  const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
2823
3232
  if (exportPdfBtn) {
2824
3233
  exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
2825
- if (rightView === "thinking") {
2826
- exportPdfBtn.title = "Thinking view does not support PDF export yet.";
3234
+ if (rightView === "trace") {
3235
+ exportPdfBtn.title = "Working view does not support PDF export.";
2827
3236
  } else if (rightView === "markdown") {
2828
3237
  exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
2829
3238
  } else if (!canExportPdf) {
@@ -3052,12 +3461,16 @@
3052
3461
  }
3053
3462
 
3054
3463
  function setRightView(nextView) {
3464
+ const previousView = rightView;
3055
3465
  rightView = nextView === "preview"
3056
3466
  ? "preview"
3057
3467
  : (nextView === "editor-preview"
3058
3468
  ? "editor-preview"
3059
- : (nextView === "thinking" ? "thinking" : "markdown"));
3469
+ : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"));
3060
3470
  rightViewSelect.value = rightView;
3471
+ if (rightView === "trace" && previousView !== "trace") {
3472
+ traceAutoScroll = true;
3473
+ }
3061
3474
 
3062
3475
  if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
3063
3476
  window.clearTimeout(responseEditorPreviewTimer);
@@ -4896,6 +5309,7 @@
4896
5309
  || kind === "blockquote"
4897
5310
  || kind === "list"
4898
5311
  || kind === "math"
5312
+ || kind === "code"
4899
5313
  || kind === "code-line"
4900
5314
  || kind === "diff-line"
4901
5315
  || kind === "text-line";
@@ -6009,7 +6423,7 @@
6009
6423
 
6010
6424
  function buildPreviewSelectionDisplayMap(blockText, kind) {
6011
6425
  const body = buildPreviewSelectionSourceBody(blockText, kind);
6012
- if (kind === "code-line" || kind === "diff-line" || kind === "text-line") {
6426
+ if (kind === "code" || kind === "code-line" || kind === "diff-line" || kind === "text-line") {
6013
6427
  return buildLiteralPreviewDisplayMap(body.text, body.rawOffsets);
6014
6428
  }
6015
6429
  const inlineMap = buildPreviewInlineDisplayMap(body.text, body.rawOffsets);
@@ -6281,6 +6695,15 @@
6281
6695
  };
6282
6696
  }
6283
6697
 
6698
+ function getChunkText(startLineIndex, endLineIndex) {
6699
+ const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
6700
+ const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
6701
+ return source.slice(
6702
+ lineOffsets[safeStartLine] || 0,
6703
+ (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length,
6704
+ );
6705
+ }
6706
+
6284
6707
  const blocks = [];
6285
6708
  let index = 0;
6286
6709
 
@@ -6413,7 +6836,11 @@
6413
6836
  }
6414
6837
  endParagraph = i;
6415
6838
  }
6416
- blocks.push(makeBlock("paragraph", index, endParagraph));
6839
+ const paragraphText = getChunkText(index, endParagraph);
6840
+ const markdownFigureCaption = annotationHelpers && typeof annotationHelpers.extractStandaloneMarkdownImageCaptionText === "function"
6841
+ ? annotationHelpers.extractStandaloneMarkdownImageCaptionText(paragraphText)
6842
+ : null;
6843
+ blocks.push(makeBlock(markdownFigureCaption != null ? "figure" : "paragraph", index, endParagraph));
6417
6844
  index = endParagraph + 1;
6418
6845
  }
6419
6846
 
@@ -6732,6 +7159,42 @@
6732
7159
  });
6733
7160
  }
6734
7161
 
7162
+ function isPreviewMediaOnlyParagraphElement(element) {
7163
+ if (!element || !(element instanceof Element)) return false;
7164
+ if ((element.tagName ? element.tagName.toUpperCase() : "") !== "P") return false;
7165
+
7166
+ let hasMedia = false;
7167
+ for (const childNode of Array.from(element.childNodes || [])) {
7168
+ if (!childNode) continue;
7169
+ if (childNode.nodeType === Node.TEXT_NODE) {
7170
+ if (normalizeVisiblePreviewText(childNode.nodeValue || "")) {
7171
+ return false;
7172
+ }
7173
+ continue;
7174
+ }
7175
+ if (!(childNode instanceof Element)) continue;
7176
+
7177
+ const childTag = childNode.tagName ? childNode.tagName.toUpperCase() : "";
7178
+ if (childTag === "BR") continue;
7179
+ if (childTag === "IMG" || childTag === "EMBED" || childTag === "OBJECT" || childTag === "IFRAME" || childTag === "CANVAS") {
7180
+ hasMedia = true;
7181
+ continue;
7182
+ }
7183
+
7184
+ const nestedMedia = typeof childNode.querySelector === "function"
7185
+ ? childNode.querySelector("img, embed, object, iframe, canvas")
7186
+ : null;
7187
+ if (nestedMedia && !buildNormalizedPreviewSearchText(childNode)) {
7188
+ hasMedia = true;
7189
+ continue;
7190
+ }
7191
+
7192
+ return false;
7193
+ }
7194
+
7195
+ return hasMedia;
7196
+ }
7197
+
6735
7198
  function getPreviewCommentTargetKind(element) {
6736
7199
  if (!element || !(element instanceof Element)) return "";
6737
7200
  if (element.classList && element.classList.contains("studio-mathjax-fallback-display")) {
@@ -6742,12 +7205,12 @@
6742
7205
  }
6743
7206
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
6744
7207
  if (/^H[1-6]$/.test(tag)) return "heading";
6745
- if (tag === "P") return "paragraph";
7208
+ if (tag === "P") return isPreviewMediaOnlyParagraphElement(element) ? "figure" : "paragraph";
6746
7209
  if (tag === "FIGURE") {
6747
7210
  if (element.classList && element.classList.contains("studio-algorithm-block")) {
6748
7211
  return "algorithm";
6749
7212
  }
6750
- return editorLanguage === "latex" ? "figure" : "";
7213
+ return "figure";
6751
7214
  }
6752
7215
  if (tag === "DIV" && element.classList) {
6753
7216
  if (element.classList.contains("studio-display-equation")) {
@@ -6854,6 +7317,14 @@
6854
7317
  const match = blockText.trim().match(/^\\(newpage|pagebreak|clearpage)/i);
6855
7318
  return match ? String(match[1] || "").toLowerCase() : "page-break";
6856
7319
  }
7320
+ if (sourceBlock.kind === "figure") {
7321
+ const figureCaption = annotationHelpers && typeof annotationHelpers.extractStandaloneMarkdownImageCaptionText === "function"
7322
+ ? annotationHelpers.extractStandaloneMarkdownImageCaptionText(blockText)
7323
+ : null;
7324
+ if (figureCaption != null) {
7325
+ return normalizeVisiblePreviewText(figureCaption);
7326
+ }
7327
+ }
6857
7328
  if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
6858
7329
  return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
6859
7330
  }
@@ -6874,6 +7345,23 @@
6874
7345
  return normalizeVisiblePreviewText(blockText);
6875
7346
  }
6876
7347
 
7348
+ function getPreviewFigureSearchText(element) {
7349
+ if (!element || !(element instanceof Element)) return "";
7350
+ const visibleText = buildNormalizedPreviewSearchText(element);
7351
+ if (visibleText) return visibleText;
7352
+
7353
+ const imageNodes = (element.tagName ? element.tagName.toUpperCase() : "") === "IMG"
7354
+ ? [element]
7355
+ : (typeof element.querySelectorAll === "function" ? Array.from(element.querySelectorAll("img[alt], img[title]")) : []);
7356
+ const altText = imageNodes
7357
+ .filter((imageEl) => imageEl instanceof Element)
7358
+ .map((imageEl) => imageEl.getAttribute("alt") || imageEl.getAttribute("title") || "")
7359
+ .map((text) => normalizeVisiblePreviewText(text))
7360
+ .filter(Boolean)
7361
+ .join(" ");
7362
+ return altText;
7363
+ }
7364
+
6877
7365
  function getNormalizedPreviewCommentTargetText(targetEntry) {
6878
7366
  if (!targetEntry) return "";
6879
7367
  if (typeof targetEntry.normalizedText === "string") return targetEntry.normalizedText;
@@ -6882,6 +7370,10 @@
6882
7370
  targetEntry.normalizedText = String(element && element.getAttribute ? (element.getAttribute("data-page-break-kind") || "page-break") : "page-break").toLowerCase();
6883
7371
  return targetEntry.normalizedText;
6884
7372
  }
7373
+ if (targetEntry.kind === "figure") {
7374
+ targetEntry.normalizedText = getPreviewFigureSearchText(targetEntry.element);
7375
+ return targetEntry.normalizedText;
7376
+ }
6885
7377
  targetEntry.normalizedText = buildNormalizedPreviewSearchText(targetEntry.element);
6886
7378
  return targetEntry.normalizedText;
6887
7379
  }
@@ -8580,6 +9072,10 @@
8580
9072
  }
8581
9073
  }
8582
9074
 
9075
+ if (message.traceState) {
9076
+ replaceTraceState(message.traceState);
9077
+ }
9078
+
8583
9079
  let appliedHistory = false;
8584
9080
  if (Array.isArray(message.responseHistory)) {
8585
9081
  appliedHistory = setResponseHistory(message.responseHistory, {
@@ -8626,6 +9122,26 @@
8626
9122
  return;
8627
9123
  }
8628
9124
 
9125
+ if (message.type === "trace_reset") {
9126
+ replaceTraceState(message.trace);
9127
+ return;
9128
+ }
9129
+
9130
+ if (message.type === "trace_status") {
9131
+ updateTraceStatusFromMessage(message);
9132
+ return;
9133
+ }
9134
+
9135
+ if (message.type === "trace_entry_upsert") {
9136
+ upsertTraceEntry(message.entry);
9137
+ return;
9138
+ }
9139
+
9140
+ if (message.type === "trace_assistant_delta") {
9141
+ appendTraceAssistantDelta(message.entryId, message.deltaKind, message.delta, message.updatedAt);
9142
+ return;
9143
+ }
9144
+
8629
9145
  if (message.type === "request_started") {
8630
9146
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
8631
9147
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
@@ -9323,6 +9839,8 @@
9323
9839
  setRightView(rightViewSelect.value);
9324
9840
  });
9325
9841
 
9842
+ attachResponsePaneInteractionHandlers();
9843
+
9326
9844
  followSelect.addEventListener("change", () => {
9327
9845
  followLatest = followSelect.value !== "off";
9328
9846
  if (followLatest && queuedLatestResponse) {
@@ -9565,17 +10083,6 @@
9565
10083
  });
9566
10084
 
9567
10085
  loadResponseBtn.addEventListener("click", () => {
9568
- if (rightView === "thinking") {
9569
- if (!latestResponseThinking.trim()) {
9570
- setStatus("No thinking available for the selected response.", "warning");
9571
- return;
9572
- }
9573
- setEditorText(latestResponseThinking, { preserveScroll: false, preserveSelection: false });
9574
- setSourceState({ source: "blank", label: "assistant thinking", path: null });
9575
- setStatus("Loaded thinking into editor.", "success");
9576
- return;
9577
- }
9578
-
9579
10086
  if (!latestResponseMarkdown.trim()) {
9580
10087
  setStatus("No response available yet.", "warning");
9581
10088
  return;
@@ -9614,15 +10121,15 @@
9614
10121
  });
9615
10122
 
9616
10123
  copyResponseBtn.addEventListener("click", async () => {
9617
- const content = rightView === "thinking" ? latestResponseThinking : latestResponseMarkdown;
10124
+ const content = latestResponseMarkdown;
9618
10125
  if (!content.trim()) {
9619
- setStatus(rightView === "thinking" ? "No thinking available yet." : "No response available yet.", "warning");
10126
+ setStatus("No response available yet.", "warning");
9620
10127
  return;
9621
10128
  }
9622
10129
 
9623
10130
  try {
9624
10131
  await navigator.clipboard.writeText(content);
9625
- setStatus(rightView === "thinking" ? "Copied thinking text." : "Copied response text.", "success");
10132
+ setStatus("Copied response text.", "success");
9626
10133
  } catch (error) {
9627
10134
  setStatus("Clipboard write failed.", "warning");
9628
10135
  }