pi-studio 0.5.52 → 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");
@@ -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");
3213
+ loadResponseBtn.hidden = isCritiqueResponse;
3214
+ loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
3215
+ loadCritiqueFullBtn.hidden = !isCritiqueResponse;
2799
3216
 
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;
2806
-
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);
@@ -8580,6 +8993,10 @@
8580
8993
  }
8581
8994
  }
8582
8995
 
8996
+ if (message.traceState) {
8997
+ replaceTraceState(message.traceState);
8998
+ }
8999
+
8583
9000
  let appliedHistory = false;
8584
9001
  if (Array.isArray(message.responseHistory)) {
8585
9002
  appliedHistory = setResponseHistory(message.responseHistory, {
@@ -8626,6 +9043,26 @@
8626
9043
  return;
8627
9044
  }
8628
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
+
8629
9066
  if (message.type === "request_started") {
8630
9067
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
8631
9068
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
@@ -9323,6 +9760,8 @@
9323
9760
  setRightView(rightViewSelect.value);
9324
9761
  });
9325
9762
 
9763
+ attachResponsePaneInteractionHandlers();
9764
+
9326
9765
  followSelect.addEventListener("change", () => {
9327
9766
  followLatest = followSelect.value !== "off";
9328
9767
  if (followLatest && queuedLatestResponse) {
@@ -9565,17 +10004,6 @@
9565
10004
  });
9566
10005
 
9567
10006
  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
10007
  if (!latestResponseMarkdown.trim()) {
9580
10008
  setStatus("No response available yet.", "warning");
9581
10009
  return;
@@ -9614,15 +10042,15 @@
9614
10042
  });
9615
10043
 
9616
10044
  copyResponseBtn.addEventListener("click", async () => {
9617
- const content = rightView === "thinking" ? latestResponseThinking : latestResponseMarkdown;
10045
+ const content = latestResponseMarkdown;
9618
10046
  if (!content.trim()) {
9619
- setStatus(rightView === "thinking" ? "No thinking available yet." : "No response available yet.", "warning");
10047
+ setStatus("No response available yet.", "warning");
9620
10048
  return;
9621
10049
  }
9622
10050
 
9623
10051
  try {
9624
10052
  await navigator.clipboard.writeText(content);
9625
- setStatus(rightView === "thinking" ? "Copied thinking text." : "Copied response text.", "success");
10053
+ setStatus("Copied response text.", "success");
9626
10054
  } catch (error) {
9627
10055
  setStatus("Clipboard write failed.", "warning");
9628
10056
  }