pi-studio 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,6 +88,10 @@
88
88
  const loadCritiqueNotesBtn = document.getElementById("loadCritiqueNotesBtn");
89
89
  const loadCritiqueFullBtn = document.getElementById("loadCritiqueFullBtn");
90
90
  const copyResponseBtn = document.getElementById("copyResponseBtn");
91
+ const exportPreviewControlsEl = document.getElementById("exportPreviewControls");
92
+ const exportPreviewMenuEl = document.getElementById("exportPreviewMenu");
93
+ const exportPreviewPdfBtn = document.getElementById("exportPreviewPdfBtn");
94
+ const exportPreviewHtmlBtn = document.getElementById("exportPreviewHtmlBtn");
91
95
  const exportPdfBtn = document.getElementById("exportPdfBtn");
92
96
  const historyPrevBtn = document.getElementById("historyPrevBtn");
93
97
  const historyNextBtn = document.getElementById("historyNextBtn");
@@ -201,6 +205,9 @@
201
205
  let responseHistory = [];
202
206
  let responseHistoryIndex = -1;
203
207
  let traceState = null;
208
+ let liveTraceState = null;
209
+ const traceSnapshotCache = new Map();
210
+ let traceDisplayContext = { mode: "live", responseId: null, historyIndex: -1, total: 0, summary: null };
204
211
  let traceFilter = "all";
205
212
  let traceAutoScroll = true;
206
213
  let traceRenderRaf = null;
@@ -215,7 +222,7 @@
215
222
  let terminalActivityLabel = "";
216
223
  let lastSpecificToolLabel = "";
217
224
  let uiBusy = false;
218
- let pdfExportInProgress = false;
225
+ let previewExportInProgress = false;
219
226
  let compactInProgress = false;
220
227
  let modelLabel = (document.body && document.body.dataset && document.body.dataset.modelLabel) || "none";
221
228
  let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
@@ -409,6 +416,102 @@
409
416
  renderTraceViewIfActive();
410
417
  }
411
418
 
419
+ function shouldDisplayLiveTrace() {
420
+ return !Array.isArray(responseHistory)
421
+ || responseHistory.length === 0
422
+ || responseHistoryIndex < 0
423
+ || responseHistoryIndex >= responseHistory.length - 1;
424
+ }
425
+
426
+ function setTraceDisplayContext(nextContext) {
427
+ const fallback = { mode: "live", responseId: null, historyIndex: -1, total: 0, summary: null };
428
+ traceDisplayContext = Object.assign(fallback, nextContext && typeof nextContext === "object" ? nextContext : {});
429
+ }
430
+
431
+ function ensureLiveTraceState() {
432
+ if (!liveTraceState) liveTraceState = createEmptyTraceState();
433
+ return liveTraceState;
434
+ }
435
+
436
+ function upsertTraceEntryInState(state, entry) {
437
+ const normalized = normalizeTraceEntry(entry, Array.isArray(state.entries) ? state.entries.length : 0);
438
+ if (!normalized) return null;
439
+ if (!Array.isArray(state.entries)) state.entries = [];
440
+ const index = state.entries.findIndex((candidate) => candidate.id === normalized.id);
441
+ if (index >= 0) {
442
+ state.entries[index] = normalized;
443
+ } else {
444
+ state.entries.push(normalized);
445
+ }
446
+ state.updatedAt = normalized.updatedAt;
447
+ return normalized;
448
+ }
449
+
450
+ function replaceLiveTraceState(nextState) {
451
+ liveTraceState = normalizeTraceState(nextState);
452
+ if (shouldDisplayLiveTrace()) {
453
+ setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: responseHistoryIndex, total: responseHistory.length, summary: null });
454
+ replaceTraceState(liveTraceState);
455
+ }
456
+ }
457
+
458
+ function upsertLiveTraceEntry(entry) {
459
+ const normalized = upsertTraceEntryInState(ensureLiveTraceState(), entry);
460
+ if (!normalized) return;
461
+ if (shouldDisplayLiveTrace()) {
462
+ setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: responseHistoryIndex, total: responseHistory.length, summary: null });
463
+ upsertTraceEntry(normalized);
464
+ }
465
+ }
466
+
467
+ function appendLiveTraceAssistantDelta(entryId, deltaKind, delta, updatedAt) {
468
+ if (typeof delta !== "string" || !delta) return;
469
+ const state = ensureLiveTraceState();
470
+ const targetId = typeof entryId === "string" && entryId.trim() ? entryId.trim() : null;
471
+ let entry = targetId ? state.entries.find((candidate) => candidate.id === targetId) : null;
472
+ if (!entry || entry.type !== "assistant") {
473
+ entry = normalizeTraceEntry({
474
+ id: targetId || ("trace-assistant-live-" + Date.now()),
475
+ type: "assistant",
476
+ startedAt: updatedAt,
477
+ updatedAt,
478
+ thinking: "",
479
+ text: "",
480
+ status: "streaming",
481
+ stopReason: null,
482
+ }, state.entries.length);
483
+ if (!entry) return;
484
+ state.entries.push(entry);
485
+ }
486
+ if (deltaKind === "thinking") {
487
+ entry.thinking += delta;
488
+ } else {
489
+ entry.text += delta;
490
+ }
491
+ entry.status = "streaming";
492
+ entry.updatedAt = parseFiniteNumber(updatedAt) || Date.now();
493
+ state.updatedAt = entry.updatedAt;
494
+ if (shouldDisplayLiveTrace()) {
495
+ setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: responseHistoryIndex, total: responseHistory.length, summary: null });
496
+ appendTraceAssistantDelta(entryId, deltaKind, delta, updatedAt);
497
+ }
498
+ }
499
+
500
+ function updateLiveTraceStatusFromMessage(message) {
501
+ if (!message || typeof message !== "object") return;
502
+ const state = ensureLiveTraceState();
503
+ state.runId = parseNonEmptyString(message.runId) || state.runId;
504
+ if (Object.prototype.hasOwnProperty.call(message, "requestId")) state.requestId = parseNonEmptyString(message.requestId);
505
+ if (Object.prototype.hasOwnProperty.call(message, "requestKind")) state.requestKind = parseNonEmptyString(message.requestKind);
506
+ if (Object.prototype.hasOwnProperty.call(message, "startedAt")) state.startedAt = parseFiniteNumber(message.startedAt);
507
+ if (Object.prototype.hasOwnProperty.call(message, "updatedAt")) state.updatedAt = parseFiniteNumber(message.updatedAt);
508
+ if (Object.prototype.hasOwnProperty.call(message, "status")) state.status = normalizeTraceStatus(message.status);
509
+ if (shouldDisplayLiveTrace()) {
510
+ setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: responseHistoryIndex, total: responseHistory.length, summary: null });
511
+ updateTraceStatusFromMessage(message);
512
+ }
513
+ }
514
+
412
515
  function normalizeTraceFilter(filter) {
413
516
  return filter === "thinking" || filter === "tools" ? filter : "all";
414
517
  }
@@ -1034,7 +1137,11 @@
1034
1137
  }
1035
1138
  rightIdentityEl.appendChild(rightTitleGroupEl);
1036
1139
  const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
1037
- if (exportPdfBtn) rightToolsEl.appendChild(exportPdfBtn);
1140
+ if (exportPreviewControlsEl) {
1141
+ rightToolsEl.appendChild(exportPreviewControlsEl);
1142
+ } else if (exportPdfBtn) {
1143
+ rightToolsEl.appendChild(exportPdfBtn);
1144
+ }
1038
1145
  rightHeaderEl.replaceChildren(rightIdentityEl, rightToolsEl);
1039
1146
  }
1040
1147
 
@@ -2027,6 +2134,20 @@
2027
2134
  return kind === "critique" ? "critique" : "annotation";
2028
2135
  }
2029
2136
 
2137
+ function normalizeTraceSummary(summary) {
2138
+ if (!summary || typeof summary !== "object") return null;
2139
+ return {
2140
+ hasTrace: summary.hasTrace === true,
2141
+ entryCount: typeof summary.entryCount === "number" && Number.isFinite(summary.entryCount)
2142
+ ? Math.max(0, Math.floor(summary.entryCount))
2143
+ : 0,
2144
+ startedAt: parseFiniteNumber(summary.startedAt),
2145
+ updatedAt: parseFiniteNumber(summary.updatedAt),
2146
+ status: normalizeTraceStatus(summary.status),
2147
+ truncated: summary.truncated === true,
2148
+ };
2149
+ }
2150
+
2030
2151
  function normalizeHistoryItem(item, fallbackIndex) {
2031
2152
  if (!item || typeof item !== "object") return null;
2032
2153
  if (typeof item.markdown !== "string") return null;
@@ -2057,6 +2178,7 @@
2057
2178
  const promptTriggerText = typeof item.promptTriggerText === "string"
2058
2179
  ? item.promptTriggerText
2059
2180
  : (item.promptTriggerText == null ? null : String(item.promptTriggerText));
2181
+ const traceSummary = normalizeTraceSummary(item.traceSummary);
2060
2182
 
2061
2183
  return {
2062
2184
  id,
@@ -2069,6 +2191,7 @@
2069
2191
  promptTriggerKind,
2070
2192
  promptSteeringCount,
2071
2193
  promptTriggerText,
2194
+ traceSummary,
2072
2195
  };
2073
2196
  }
2074
2197
 
@@ -2078,6 +2201,48 @@
2078
2201
  return responseHistory[responseHistoryIndex] || null;
2079
2202
  }
2080
2203
 
2204
+ function syncTraceForSelectedHistoryItem() {
2205
+ const item = getSelectedHistoryItem();
2206
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
2207
+ const index = responseHistoryIndex;
2208
+ if (!item) {
2209
+ setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: index, total, summary: null });
2210
+ replaceTraceState(liveTraceState || createEmptyTraceState());
2211
+ return;
2212
+ }
2213
+ if (index >= total - 1) {
2214
+ setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: index, total, summary: item.traceSummary || null });
2215
+ replaceTraceState(liveTraceState || createEmptyTraceState());
2216
+ return;
2217
+ }
2218
+
2219
+ const summary = item.traceSummary || null;
2220
+ if (!summary || !summary.hasTrace) {
2221
+ setTraceDisplayContext({ mode: "missing", responseId: item.id, historyIndex: index, total, summary });
2222
+ replaceTraceState(createEmptyTraceState());
2223
+ return;
2224
+ }
2225
+
2226
+ const cached = traceSnapshotCache.get(item.id);
2227
+ if (cached) {
2228
+ setTraceDisplayContext({ mode: "history", responseId: item.id, historyIndex: index, total, summary });
2229
+ replaceTraceState(cached);
2230
+ return;
2231
+ }
2232
+
2233
+ setTraceDisplayContext({ mode: "loading", responseId: item.id, historyIndex: index, total, summary });
2234
+ replaceTraceState({
2235
+ runId: null,
2236
+ requestId: null,
2237
+ requestKind: null,
2238
+ status: "idle",
2239
+ startedAt: summary.startedAt || null,
2240
+ updatedAt: summary.updatedAt || null,
2241
+ entries: [],
2242
+ });
2243
+ sendMessage({ type: "get_trace_snapshot", responseHistoryId: item.id });
2244
+ }
2245
+
2081
2246
  function clearActiveResponseView() {
2082
2247
  pendingResponseScrollReset = false;
2083
2248
  latestResponseMarkdown = "";
@@ -2152,6 +2317,7 @@
2152
2317
  const nextId = nextItem && typeof nextItem.id === "string" ? nextItem.id : null;
2153
2318
  const applied = applySelectedHistoryItem({ resetScroll: previousId !== nextId });
2154
2319
  updateHistoryControls();
2320
+ syncTraceForSelectedHistoryItem();
2155
2321
 
2156
2322
  if (applied && !(options && options.silent)) {
2157
2323
  const item = getSelectedHistoryItem();
@@ -2202,20 +2368,43 @@
2202
2368
  return selectHistoryIndex(targetIndex, { silent: Boolean(options && options.silent) });
2203
2369
  }
2204
2370
 
2371
+ function getTraceHistoryContextLabel() {
2372
+ const context = traceDisplayContext || {};
2373
+ const total = typeof context.total === "number" && Number.isFinite(context.total) ? context.total : responseHistory.length;
2374
+ const index = typeof context.historyIndex === "number" && Number.isFinite(context.historyIndex) ? context.historyIndex : responseHistoryIndex;
2375
+ if (context.mode === "history" || context.mode === "missing" || context.mode === "loading") {
2376
+ return total > 0 && index >= 0 ? ("response " + (index + 1) + "/" + total) : "selected response";
2377
+ }
2378
+ return "live";
2379
+ }
2380
+
2205
2381
  function updateReferenceBadge() {
2206
2382
  if (!referenceBadgeEl) return;
2207
2383
 
2208
2384
  if (rightView === "trace") {
2209
2385
  const state = traceState || createEmptyTraceState();
2386
+ const context = traceDisplayContext || {};
2210
2387
  const entryCount = getTraceEntriesForFilter(traceFilter).length;
2211
2388
  const time = formatReferenceTime(state.startedAt || state.updatedAt);
2389
+ if (context.mode === "loading") {
2390
+ referenceBadgeEl.textContent = "Working: loading " + getTraceHistoryContextLabel();
2391
+ return;
2392
+ }
2393
+ if (context.mode === "missing") {
2394
+ referenceBadgeEl.textContent = "Working: no saved working for " + getTraceHistoryContextLabel();
2395
+ return;
2396
+ }
2212
2397
  if (state.status === "idle") {
2213
2398
  referenceBadgeEl.textContent = "Working: no active run yet";
2214
2399
  return;
2215
2400
  }
2216
- const statusLabel = state.status === "running" ? "live" : "complete";
2401
+ const statusLabel = context.mode === "history"
2402
+ ? "saved"
2403
+ : (state.status === "running" ? "live" : "complete");
2217
2404
  referenceBadgeEl.textContent = "Working: " + statusLabel
2405
+ + (context.mode === "history" ? (" · " + getTraceHistoryContextLabel()) : "")
2218
2406
  + (entryCount ? (" · " + entryCount + " entr" + (entryCount === 1 ? "y" : "ies")) : "")
2407
+ + (context.summary && context.summary.truncated ? " · truncated" : "")
2219
2408
  + (time ? (" · " + time) : "");
2220
2409
  return;
2221
2410
  }
@@ -2305,6 +2494,15 @@
2305
2494
  return prepared;
2306
2495
  }
2307
2496
 
2497
+ function prepareEditorTextForHtmlExport(text) {
2498
+ const prepared = prepareEditorTextForPreview(text);
2499
+ const lang = normalizeFenceLanguage(editorLanguage || "");
2500
+ if (lang && lang !== "markdown" && lang !== "latex") {
2501
+ return wrapAsFencedCodeBlock(prepared, lang);
2502
+ }
2503
+ return prepared;
2504
+ }
2505
+
2308
2506
  function updateSyncBadge(normalizedEditorText) {
2309
2507
  if (!syncBadgeEl) return;
2310
2508
 
@@ -3387,7 +3585,7 @@
3387
3585
  }
3388
3586
 
3389
3587
  async function exportRightPanePdf() {
3390
- if (uiBusy || pdfExportInProgress) {
3588
+ if (uiBusy || previewExportInProgress) {
3391
3589
  setStatus("Studio is busy.", "warning");
3392
3590
  return;
3393
3591
  }
@@ -3427,7 +3625,7 @@
3427
3625
  filenameHint = stem + "-preview.pdf";
3428
3626
  }
3429
3627
 
3430
- pdfExportInProgress = true;
3628
+ previewExportInProgress = true;
3431
3629
  updateResultActionButtons();
3432
3630
  setStatus("Exporting PDF…", "warning");
3433
3631
 
@@ -3540,11 +3738,201 @@
3540
3738
  const detail = error && error.message ? error.message : String(error || "unknown error");
3541
3739
  setStatus("PDF export failed: " + detail, "error");
3542
3740
  } finally {
3543
- pdfExportInProgress = false;
3741
+ previewExportInProgress = false;
3544
3742
  updateResultActionButtons();
3545
3743
  }
3546
3744
  }
3547
3745
 
3746
+ async function exportRightPaneHtml() {
3747
+ if (uiBusy || previewExportInProgress) {
3748
+ setStatus("Studio is busy.", "warning");
3749
+ return;
3750
+ }
3751
+
3752
+ const token = getToken();
3753
+ if (!token) {
3754
+ setStatus("Missing Studio token in URL. Re-run /studio.", "error");
3755
+ return;
3756
+ }
3757
+
3758
+ const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
3759
+ if (!rightPaneShowsPreview) {
3760
+ setStatus("Switch right pane to Response (Preview) or Editor (Preview) to export HTML.", "warning");
3761
+ return;
3762
+ }
3763
+
3764
+ const markdown = rightView === "editor-preview"
3765
+ ? prepareEditorTextForHtmlExport(sourceTextEl.value)
3766
+ : prepareEditorTextForPreview(latestResponseMarkdown);
3767
+ if (!markdown || !markdown.trim()) {
3768
+ setStatus("Nothing to export yet.", "warning");
3769
+ return;
3770
+ }
3771
+
3772
+ const effectivePath = getEffectiveSavePath();
3773
+ const sourcePath = effectivePath || sourceState.path || "";
3774
+ const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
3775
+ const isEditorPreview = rightView === "editor-preview";
3776
+ const editorHtmlLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
3777
+ const isLatex = isEditorPreview
3778
+ ? editorHtmlLanguage === "latex"
3779
+ : /\\documentclass\b|\\begin\{document\}/.test(markdown);
3780
+ let filenameHint = isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html";
3781
+ let titleHint = isEditorPreview ? "Studio editor preview" : "Studio response preview";
3782
+ if (sourcePath) {
3783
+ const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
3784
+ const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
3785
+ filenameHint = stem + "-preview.html";
3786
+ titleHint = stem + " preview";
3787
+ }
3788
+
3789
+ previewExportInProgress = true;
3790
+ updateResultActionButtons();
3791
+ setStatus("Exporting HTML…", "warning");
3792
+
3793
+ try {
3794
+ const response = await fetch("/export-html?token=" + encodeURIComponent(token), {
3795
+ method: "POST",
3796
+ headers: {
3797
+ "Content-Type": "application/json",
3798
+ },
3799
+ body: JSON.stringify({
3800
+ markdown: String(markdown || ""),
3801
+ sourcePath: sourcePath,
3802
+ resourceDir: resourceDir,
3803
+ isLatex: isLatex,
3804
+ editorHtmlLanguage: editorHtmlLanguage,
3805
+ filenameHint: filenameHint,
3806
+ title: titleHint,
3807
+ }),
3808
+ });
3809
+
3810
+ const contentType = String(response.headers.get("content-type") || "").toLowerCase();
3811
+ if (!response.ok) {
3812
+ let message = "HTML export failed with HTTP " + response.status + ".";
3813
+ if (contentType.includes("application/json")) {
3814
+ const payload = await response.json().catch(() => null);
3815
+ if (payload && typeof payload.error === "string") {
3816
+ message = payload.error;
3817
+ }
3818
+ } else {
3819
+ const text = await response.text().catch(() => "");
3820
+ if (text && text.trim()) {
3821
+ message = text.trim();
3822
+ }
3823
+ }
3824
+ throw new Error(message);
3825
+ }
3826
+
3827
+ if (contentType.includes("application/json")) {
3828
+ const payload = await response.json().catch(() => null);
3829
+ if (!payload || typeof payload.downloadUrl !== "string") {
3830
+ throw new Error("HTML export prepared successfully, but Studio did not receive a download URL.");
3831
+ }
3832
+
3833
+ const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
3834
+ const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
3835
+ const openedExternal = payload.openedExternal === true;
3836
+ let downloadName = typeof payload.filename === "string" && payload.filename.trim()
3837
+ ? payload.filename.trim()
3838
+ : (filenameHint || "studio-preview.html");
3839
+ if (!/\.html?$/i.test(downloadName)) {
3840
+ downloadName += ".html";
3841
+ }
3842
+
3843
+ if (openedExternal) {
3844
+ if (exportWarning) {
3845
+ setStatus("Opened HTML in default browser with warning: " + exportWarning, "warning");
3846
+ } else {
3847
+ setStatus("Opened HTML in default browser: " + downloadName, "success");
3848
+ }
3849
+ return;
3850
+ }
3851
+
3852
+ const link = document.createElement("a");
3853
+ link.href = payload.downloadUrl;
3854
+ link.download = downloadName;
3855
+ link.rel = "noopener";
3856
+ document.body.appendChild(link);
3857
+ link.click();
3858
+ link.remove();
3859
+
3860
+ if (openError) {
3861
+ if (exportWarning) {
3862
+ setStatus("Opened browser fallback because external viewer failed (" + openError + "). Warning: " + exportWarning, "warning");
3863
+ } else {
3864
+ setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
3865
+ }
3866
+ } else if (exportWarning) {
3867
+ setStatus("Exported HTML with warning: " + exportWarning, "warning");
3868
+ } else {
3869
+ setStatus("Exported HTML: " + downloadName, "success");
3870
+ }
3871
+ return;
3872
+ }
3873
+
3874
+ const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
3875
+ const blob = await response.blob();
3876
+ const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
3877
+ let downloadName = headerFilename || filenameHint || "studio-preview.html";
3878
+ if (!/\.html?$/i.test(downloadName)) {
3879
+ downloadName += ".html";
3880
+ }
3881
+
3882
+ const blobUrl = URL.createObjectURL(blob);
3883
+ const link = document.createElement("a");
3884
+ link.href = blobUrl;
3885
+ link.download = downloadName;
3886
+ link.rel = "noopener";
3887
+ document.body.appendChild(link);
3888
+ link.click();
3889
+ link.remove();
3890
+ window.setTimeout(() => {
3891
+ URL.revokeObjectURL(blobUrl);
3892
+ }, 1800);
3893
+
3894
+ if (exportWarning) {
3895
+ setStatus("Exported HTML with warning: " + exportWarning, "warning");
3896
+ } else {
3897
+ setStatus("Exported HTML: " + downloadName, "success");
3898
+ }
3899
+ } catch (error) {
3900
+ const detail = error && error.message ? error.message : String(error || "unknown error");
3901
+ setStatus("HTML export failed: " + detail, "error");
3902
+ } finally {
3903
+ previewExportInProgress = false;
3904
+ updateResultActionButtons();
3905
+ }
3906
+ }
3907
+
3908
+ function closeExportPreviewMenu() {
3909
+ if (!exportPreviewMenuEl) return;
3910
+ exportPreviewMenuEl.hidden = true;
3911
+ if (exportPdfBtn) {
3912
+ exportPdfBtn.classList.remove("is-open");
3913
+ exportPdfBtn.setAttribute("aria-expanded", "false");
3914
+ }
3915
+ }
3916
+
3917
+ function toggleExportPreviewMenu() {
3918
+ if (!exportPreviewMenuEl || !exportPdfBtn || exportPdfBtn.disabled) return;
3919
+ if (typeof closeStudioUiRefreshMenus === "function") {
3920
+ closeStudioUiRefreshMenus();
3921
+ }
3922
+ const willOpen = exportPreviewMenuEl.hidden;
3923
+ exportPreviewMenuEl.hidden = !willOpen;
3924
+ exportPdfBtn.classList.toggle("is-open", willOpen);
3925
+ exportPdfBtn.setAttribute("aria-expanded", willOpen ? "true" : "false");
3926
+ }
3927
+
3928
+ function exportRightPaneFormat(format) {
3929
+ closeExportPreviewMenu();
3930
+ if (format === "html") {
3931
+ return exportRightPaneHtml();
3932
+ }
3933
+ return exportRightPanePdf();
3934
+ }
3935
+
3548
3936
  function normalizeCopyableBlockText(text) {
3549
3937
  return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
3550
3938
  }
@@ -3857,17 +4245,27 @@
3857
4245
  const visibleWorking = buildVisibleWorkingText(filter);
3858
4246
  const hasVisibleContent = Boolean(visibleWorking.trim());
3859
4247
  const started = formatReferenceTime(state.startedAt || state.updatedAt);
3860
- const statusLabel = state.status === "running"
3861
- ? "Live"
3862
- : (state.status === "complete" ? "Complete" : "Idle");
4248
+ const context = traceDisplayContext || {};
4249
+ const statusLabel = context.mode === "history"
4250
+ ? "Saved"
4251
+ : (context.mode === "loading"
4252
+ ? "Loading"
4253
+ : (context.mode === "missing"
4254
+ ? "Not saved"
4255
+ : (state.status === "running" ? "Live" : (state.status === "complete" ? "Complete" : "Idle"))));
3863
4256
  const filterMeta = filter === "thinking"
3864
4257
  ? "Thinking only"
3865
4258
  : (filter === "tools" ? "Tools only" : null);
4259
+ const historyMeta = (context.mode === "history" || context.mode === "missing" || context.mode === "loading")
4260
+ ? getTraceHistoryContextLabel()
4261
+ : null;
3866
4262
  const toolbar = "<div class='trace-toolbar'>"
3867
4263
  + "<div class='trace-summary'>"
3868
4264
  + "<span class='trace-summary-badge'>Working</span>"
3869
4265
  + "<span class='trace-summary-status trace-status-" + escapeHtml(String(state.status || "idle")) + "'>" + escapeHtml(statusLabel) + "</span>"
4266
+ + (historyMeta ? ("<span class='trace-summary-meta'>" + escapeHtml(historyMeta) + "</span>") : "")
3870
4267
  + (started ? ("<span class='trace-summary-meta'>Started " + escapeHtml(started) + "</span>") : "")
4268
+ + (context.summary && context.summary.truncated ? "<span class='trace-summary-meta'>Truncated</span>" : "")
3871
4269
  + (filterMeta ? ("<span class='trace-summary-meta'>" + escapeHtml(filterMeta) + "</span>") : "")
3872
4270
  + "</div>"
3873
4271
  + "<div class='trace-controls'>"
@@ -3882,13 +4280,17 @@
3882
4280
  + "</div>";
3883
4281
 
3884
4282
  if (!entries.length) {
3885
- const emptyMessage = filter === "thinking"
3886
- ? "No thinking steps in this working view yet."
3887
- : (filter === "tools"
3888
- ? "No tool steps in this working view yet."
3889
- : (state.status === "running"
3890
- ? "Waiting for the first model or tool update…"
3891
- : "No live working view yet. Start a run or critique to watch working details here."));
4283
+ const emptyMessage = context.mode === "loading"
4284
+ ? "Loading saved working for this response…"
4285
+ : (context.mode === "missing"
4286
+ ? "No working was saved for this response."
4287
+ : (filter === "thinking"
4288
+ ? "No thinking steps in this working view yet."
4289
+ : (filter === "tools"
4290
+ ? "No tool steps in this working view yet."
4291
+ : (state.status === "running"
4292
+ ? "Waiting for the first model or tool update…"
4293
+ : "No live working view yet. Start a run or critique to watch working details here."))));
3892
4294
  return "<div class='trace-panel'>" + toolbar + "<div class='trace-empty'>" + escapeHtml(emptyMessage) + "</div></div>";
3893
4295
  }
3894
4296
 
@@ -4065,19 +4467,34 @@
4065
4467
 
4066
4468
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
4067
4469
  const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
4068
- const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
4470
+ const canExportPreview = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
4069
4471
  if (exportPdfBtn) {
4070
- exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
4472
+ exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
4473
+ exportPdfBtn.textContent = previewExportInProgress ? "Exporting…" : "Export right preview";
4071
4474
  if (rightView === "trace") {
4072
- exportPdfBtn.title = "Working view does not support PDF export.";
4475
+ exportPdfBtn.title = "Working view does not support preview export.";
4073
4476
  } else if (rightView === "markdown") {
4074
- exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
4075
- } else if (!canExportPdf) {
4477
+ exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export.";
4478
+ } else if (!canExportPreview) {
4076
4479
  exportPdfBtn.title = "Nothing to export yet.";
4077
4480
  } else {
4078
- exportPdfBtn.title = "Export the current right-pane preview as PDF via pandoc + xelatex.";
4481
+ exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
4079
4482
  }
4080
4483
  }
4484
+ if (exportPreviewPdfBtn) {
4485
+ exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
4486
+ }
4487
+ if (exportPreviewHtmlBtn) {
4488
+ exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
4489
+ }
4490
+ if (exportPreviewControlsEl) {
4491
+ exportPreviewControlsEl.title = canExportPreview
4492
+ ? "Choose a format and export the current right-pane preview."
4493
+ : "Switch right pane to a non-empty preview before exporting.";
4494
+ }
4495
+ if (!canExportPreview || previewExportInProgress) {
4496
+ closeExportPreviewMenu();
4497
+ }
4081
4498
 
4082
4499
  pullLatestBtn.disabled = uiBusy || followLatest;
4083
4500
  pullLatestBtn.textContent = queuedLatestResponse ? "Fetch latest response *" : "Fetch latest response";
@@ -10014,7 +10431,7 @@
10014
10431
  }
10015
10432
 
10016
10433
  if (message.traceState) {
10017
- replaceTraceState(message.traceState);
10434
+ replaceLiveTraceState(message.traceState);
10018
10435
  }
10019
10436
 
10020
10437
  let appliedHistory = false;
@@ -10064,22 +10481,41 @@
10064
10481
  }
10065
10482
 
10066
10483
  if (message.type === "trace_reset") {
10067
- replaceTraceState(message.trace);
10484
+ replaceLiveTraceState(message.trace);
10068
10485
  return;
10069
10486
  }
10070
10487
 
10071
10488
  if (message.type === "trace_status") {
10072
- updateTraceStatusFromMessage(message);
10489
+ updateLiveTraceStatusFromMessage(message);
10073
10490
  return;
10074
10491
  }
10075
10492
 
10076
10493
  if (message.type === "trace_entry_upsert") {
10077
- upsertTraceEntry(message.entry);
10494
+ upsertLiveTraceEntry(message.entry);
10078
10495
  return;
10079
10496
  }
10080
10497
 
10081
10498
  if (message.type === "trace_assistant_delta") {
10082
- appendTraceAssistantDelta(message.entryId, message.deltaKind, message.delta, message.updatedAt);
10499
+ appendLiveTraceAssistantDelta(message.entryId, message.deltaKind, message.delta, message.updatedAt);
10500
+ return;
10501
+ }
10502
+
10503
+ if (message.type === "trace_snapshot") {
10504
+ const responseId = typeof message.responseHistoryId === "string" ? message.responseHistoryId.trim() : "";
10505
+ if (responseId && message.traceState) {
10506
+ const normalizedSnapshot = normalizeTraceState(message.traceState);
10507
+ traceSnapshotCache.set(responseId, normalizedSnapshot);
10508
+ if (traceDisplayContext && traceDisplayContext.responseId === responseId) {
10509
+ setTraceDisplayContext({
10510
+ mode: "history",
10511
+ responseId,
10512
+ historyIndex: responseHistoryIndex,
10513
+ total: responseHistory.length,
10514
+ summary: normalizeTraceSummary(message.summary) || (getSelectedHistoryItem() ? getSelectedHistoryItem().traceSummary : null),
10515
+ });
10516
+ replaceTraceState(normalizedSnapshot);
10517
+ }
10518
+ }
10083
10519
  return;
10084
10520
  }
10085
10521
 
@@ -11189,11 +11625,35 @@
11189
11625
  });
11190
11626
 
11191
11627
  if (exportPdfBtn) {
11192
- exportPdfBtn.addEventListener("click", () => {
11193
- void exportRightPanePdf();
11628
+ exportPdfBtn.addEventListener("click", (event) => {
11629
+ event.preventDefault();
11630
+ event.stopPropagation();
11631
+ toggleExportPreviewMenu();
11632
+ });
11633
+ }
11634
+
11635
+ if (exportPreviewMenuEl) {
11636
+ exportPreviewMenuEl.addEventListener("click", (event) => {
11637
+ const target = event.target;
11638
+ const actionBtn = target instanceof Element ? target.closest("[data-export-preview-format]") : null;
11639
+ if (!actionBtn) return;
11640
+ event.preventDefault();
11641
+ event.stopPropagation();
11642
+ if (actionBtn.disabled) return;
11643
+ const format = String(actionBtn.getAttribute("data-export-preview-format") || "pdf").toLowerCase();
11644
+ void exportRightPaneFormat(format === "html" ? "html" : "pdf");
11194
11645
  });
11195
11646
  }
11196
11647
 
11648
+ document.addEventListener("click", (event) => {
11649
+ const target = event.target;
11650
+ if (target instanceof Element && target.closest("#exportPreviewControls")) return;
11651
+ closeExportPreviewMenu();
11652
+ });
11653
+ document.addEventListener("keydown", (event) => {
11654
+ if (event.key === "Escape") closeExportPreviewMenu();
11655
+ });
11656
+
11197
11657
  saveAsBtn.addEventListener("click", () => {
11198
11658
  const content = sourceTextEl.value;
11199
11659
  if (!content.trim()) {