pi-studio 0.7.1 → 0.8.1

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
  }
@@ -657,6 +760,7 @@
657
760
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
658
761
  const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
659
762
  const previewPendingTimers = new WeakMap();
763
+ const htmlArtifactFramesById = new Map();
660
764
  let sourcePreviewRenderTimer = null;
661
765
  let sourcePreviewRenderNonce = 0;
662
766
  let responsePreviewRenderNonce = 0;
@@ -1034,7 +1138,11 @@
1034
1138
  }
1035
1139
  rightIdentityEl.appendChild(rightTitleGroupEl);
1036
1140
  const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
1037
- if (exportPdfBtn) rightToolsEl.appendChild(exportPdfBtn);
1141
+ if (exportPreviewControlsEl) {
1142
+ rightToolsEl.appendChild(exportPreviewControlsEl);
1143
+ } else if (exportPdfBtn) {
1144
+ rightToolsEl.appendChild(exportPdfBtn);
1145
+ }
1038
1146
  rightHeaderEl.replaceChildren(rightIdentityEl, rightToolsEl);
1039
1147
  }
1040
1148
 
@@ -2027,6 +2135,20 @@
2027
2135
  return kind === "critique" ? "critique" : "annotation";
2028
2136
  }
2029
2137
 
2138
+ function normalizeTraceSummary(summary) {
2139
+ if (!summary || typeof summary !== "object") return null;
2140
+ return {
2141
+ hasTrace: summary.hasTrace === true,
2142
+ entryCount: typeof summary.entryCount === "number" && Number.isFinite(summary.entryCount)
2143
+ ? Math.max(0, Math.floor(summary.entryCount))
2144
+ : 0,
2145
+ startedAt: parseFiniteNumber(summary.startedAt),
2146
+ updatedAt: parseFiniteNumber(summary.updatedAt),
2147
+ status: normalizeTraceStatus(summary.status),
2148
+ truncated: summary.truncated === true,
2149
+ };
2150
+ }
2151
+
2030
2152
  function normalizeHistoryItem(item, fallbackIndex) {
2031
2153
  if (!item || typeof item !== "object") return null;
2032
2154
  if (typeof item.markdown !== "string") return null;
@@ -2057,6 +2179,7 @@
2057
2179
  const promptTriggerText = typeof item.promptTriggerText === "string"
2058
2180
  ? item.promptTriggerText
2059
2181
  : (item.promptTriggerText == null ? null : String(item.promptTriggerText));
2182
+ const traceSummary = normalizeTraceSummary(item.traceSummary);
2060
2183
 
2061
2184
  return {
2062
2185
  id,
@@ -2069,6 +2192,7 @@
2069
2192
  promptTriggerKind,
2070
2193
  promptSteeringCount,
2071
2194
  promptTriggerText,
2195
+ traceSummary,
2072
2196
  };
2073
2197
  }
2074
2198
 
@@ -2078,6 +2202,48 @@
2078
2202
  return responseHistory[responseHistoryIndex] || null;
2079
2203
  }
2080
2204
 
2205
+ function syncTraceForSelectedHistoryItem() {
2206
+ const item = getSelectedHistoryItem();
2207
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
2208
+ const index = responseHistoryIndex;
2209
+ if (!item) {
2210
+ setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: index, total, summary: null });
2211
+ replaceTraceState(liveTraceState || createEmptyTraceState());
2212
+ return;
2213
+ }
2214
+ if (index >= total - 1) {
2215
+ setTraceDisplayContext({ mode: "live", responseId: null, historyIndex: index, total, summary: item.traceSummary || null });
2216
+ replaceTraceState(liveTraceState || createEmptyTraceState());
2217
+ return;
2218
+ }
2219
+
2220
+ const summary = item.traceSummary || null;
2221
+ if (!summary || !summary.hasTrace) {
2222
+ setTraceDisplayContext({ mode: "missing", responseId: item.id, historyIndex: index, total, summary });
2223
+ replaceTraceState(createEmptyTraceState());
2224
+ return;
2225
+ }
2226
+
2227
+ const cached = traceSnapshotCache.get(item.id);
2228
+ if (cached) {
2229
+ setTraceDisplayContext({ mode: "history", responseId: item.id, historyIndex: index, total, summary });
2230
+ replaceTraceState(cached);
2231
+ return;
2232
+ }
2233
+
2234
+ setTraceDisplayContext({ mode: "loading", responseId: item.id, historyIndex: index, total, summary });
2235
+ replaceTraceState({
2236
+ runId: null,
2237
+ requestId: null,
2238
+ requestKind: null,
2239
+ status: "idle",
2240
+ startedAt: summary.startedAt || null,
2241
+ updatedAt: summary.updatedAt || null,
2242
+ entries: [],
2243
+ });
2244
+ sendMessage({ type: "get_trace_snapshot", responseHistoryId: item.id });
2245
+ }
2246
+
2081
2247
  function clearActiveResponseView() {
2082
2248
  pendingResponseScrollReset = false;
2083
2249
  latestResponseMarkdown = "";
@@ -2152,6 +2318,7 @@
2152
2318
  const nextId = nextItem && typeof nextItem.id === "string" ? nextItem.id : null;
2153
2319
  const applied = applySelectedHistoryItem({ resetScroll: previousId !== nextId });
2154
2320
  updateHistoryControls();
2321
+ syncTraceForSelectedHistoryItem();
2155
2322
 
2156
2323
  if (applied && !(options && options.silent)) {
2157
2324
  const item = getSelectedHistoryItem();
@@ -2202,20 +2369,43 @@
2202
2369
  return selectHistoryIndex(targetIndex, { silent: Boolean(options && options.silent) });
2203
2370
  }
2204
2371
 
2372
+ function getTraceHistoryContextLabel() {
2373
+ const context = traceDisplayContext || {};
2374
+ const total = typeof context.total === "number" && Number.isFinite(context.total) ? context.total : responseHistory.length;
2375
+ const index = typeof context.historyIndex === "number" && Number.isFinite(context.historyIndex) ? context.historyIndex : responseHistoryIndex;
2376
+ if (context.mode === "history" || context.mode === "missing" || context.mode === "loading") {
2377
+ return total > 0 && index >= 0 ? ("response " + (index + 1) + "/" + total) : "selected response";
2378
+ }
2379
+ return "live";
2380
+ }
2381
+
2205
2382
  function updateReferenceBadge() {
2206
2383
  if (!referenceBadgeEl) return;
2207
2384
 
2208
2385
  if (rightView === "trace") {
2209
2386
  const state = traceState || createEmptyTraceState();
2387
+ const context = traceDisplayContext || {};
2210
2388
  const entryCount = getTraceEntriesForFilter(traceFilter).length;
2211
2389
  const time = formatReferenceTime(state.startedAt || state.updatedAt);
2390
+ if (context.mode === "loading") {
2391
+ referenceBadgeEl.textContent = "Working: loading " + getTraceHistoryContextLabel();
2392
+ return;
2393
+ }
2394
+ if (context.mode === "missing") {
2395
+ referenceBadgeEl.textContent = "Working: no saved working for " + getTraceHistoryContextLabel();
2396
+ return;
2397
+ }
2212
2398
  if (state.status === "idle") {
2213
2399
  referenceBadgeEl.textContent = "Working: no active run yet";
2214
2400
  return;
2215
2401
  }
2216
- const statusLabel = state.status === "running" ? "live" : "complete";
2402
+ const statusLabel = context.mode === "history"
2403
+ ? "saved"
2404
+ : (state.status === "running" ? "live" : "complete");
2217
2405
  referenceBadgeEl.textContent = "Working: " + statusLabel
2406
+ + (context.mode === "history" ? (" · " + getTraceHistoryContextLabel()) : "")
2218
2407
  + (entryCount ? (" · " + entryCount + " entr" + (entryCount === 1 ? "y" : "ies")) : "")
2408
+ + (context.summary && context.summary.truncated ? " · truncated" : "")
2219
2409
  + (time ? (" · " + time) : "");
2220
2410
  return;
2221
2411
  }
@@ -2305,6 +2495,15 @@
2305
2495
  return prepared;
2306
2496
  }
2307
2497
 
2498
+ function prepareEditorTextForHtmlExport(text) {
2499
+ const prepared = prepareEditorTextForPreview(text);
2500
+ const lang = normalizeFenceLanguage(editorLanguage || "");
2501
+ if (lang && lang !== "markdown" && lang !== "latex") {
2502
+ return wrapAsFencedCodeBlock(prepared, lang);
2503
+ }
2504
+ return prepared;
2505
+ }
2506
+
2308
2507
  function updateSyncBadge(normalizedEditorText) {
2309
2508
  if (!syncBadgeEl) return;
2310
2509
 
@@ -2352,6 +2551,283 @@
2352
2551
  return "<div class='preview-error'>" + escapeHtml(String(message || "Preview rendering failed.")) + "</div>" + buildPlainMarkdownHtml(markdown, options);
2353
2552
  }
2354
2553
 
2554
+ function stripLeadingHtmlPreviewTrivia(text) {
2555
+ let source = String(text || "").replace(/^\uFEFF/, "").trimStart();
2556
+ let previous = "";
2557
+ while (source && source !== previous) {
2558
+ previous = source;
2559
+ source = source.replace(/^<!--[\s\S]*?-->\s*/, "").trimStart();
2560
+ }
2561
+ return source;
2562
+ }
2563
+
2564
+ function startsWithSingleFencedBlock(text) {
2565
+ const source = String(text || "").trimStart();
2566
+ return /^(`{3,}|~{3,})/.test(source);
2567
+ }
2568
+
2569
+ function isLikelyFullHtmlDocument(text) {
2570
+ const source = stripLeadingHtmlPreviewTrivia(text);
2571
+ if (!source) return false;
2572
+ if (/^<!doctype\s+html\b/i.test(source)) return true;
2573
+ if (/^<html(?:\s|>|$)/i.test(source)) return true;
2574
+ if (/^<body(?:\s|>|$)/i.test(source) && /<\/body\s*>/i.test(source)) return true;
2575
+ return false;
2576
+ }
2577
+
2578
+ function looksLikeHtmlMarkup(text) {
2579
+ return /<[A-Za-z][A-Za-z0-9:-]*(?:\s[^<>]*)?>/.test(String(text || ""));
2580
+ }
2581
+
2582
+ function isHtmlArtifactPreviewText(text, language) {
2583
+ const source = String(text || "");
2584
+ if (!source.trim()) return false;
2585
+ if (startsWithSingleFencedBlock(source)) return false;
2586
+ if (isLikelyFullHtmlDocument(source)) return true;
2587
+ return normalizeFenceLanguage(language || "") === "html" && looksLikeHtmlMarkup(source);
2588
+ }
2589
+
2590
+ const HTML_ARTIFACT_PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline' data: blob:; style-src 'unsafe-inline' data: blob:; img-src data: blob:; font-src data: blob:; connect-src 'none'; media-src data: blob:; object-src 'none'; frame-src data: blob:; child-src data: blob:; worker-src blob:; form-action 'none'; base-uri 'none'; navigate-to 'none'";
2591
+ const HTML_ARTIFACT_FRAME_MIN_HEIGHT = 360;
2592
+ const HTML_ARTIFACT_FRAME_FIT_CAP_HEIGHT = 1800;
2593
+ const HTML_ARTIFACT_ZOOM_MIN = 0.5;
2594
+ const HTML_ARTIFACT_ZOOM_MAX = 1.75;
2595
+ const HTML_ARTIFACT_ZOOM_STEP = 0.1;
2596
+
2597
+ function buildHtmlArtifactPreviewResizeScript(previewId) {
2598
+ const idJson = JSON.stringify(String(previewId || ""));
2599
+ return "<script>\n"
2600
+ + "(() => {\n"
2601
+ + " const PREVIEW_ID = " + idJson.replace(/<\//g, "<\\/") + ";\n"
2602
+ + " let lastHeight = 0;\n"
2603
+ + " let scheduled = false;\n"
2604
+ + " let currentZoom = 1;\n"
2605
+ + " function applyZoom(value) {\n"
2606
+ + " const next = Number(value);\n"
2607
+ + " if (!Number.isFinite(next) || next <= 0) return;\n"
2608
+ + " currentZoom = Math.max(0.25, Math.min(4, next));\n"
2609
+ + " document.documentElement.style.zoom = String(currentZoom);\n"
2610
+ + " lastHeight = 0;\n"
2611
+ + " scheduleHeight();\n"
2612
+ + " }\n"
2613
+ + " function measureHeight() {\n"
2614
+ + " const body = document.body;\n"
2615
+ + " const root = document.documentElement;\n"
2616
+ + " return Math.ceil(Math.max(\n"
2617
+ + " body ? body.scrollHeight : 0,\n"
2618
+ + " body ? body.offsetHeight : 0,\n"
2619
+ + " root ? root.scrollHeight : 0,\n"
2620
+ + " root ? root.offsetHeight : 0\n"
2621
+ + " ));\n"
2622
+ + " }\n"
2623
+ + " function sendHeight() {\n"
2624
+ + " scheduled = false;\n"
2625
+ + " const height = measureHeight();\n"
2626
+ + " if (!height || Math.abs(height - lastHeight) < 2) return;\n"
2627
+ + " lastHeight = height;\n"
2628
+ + " try { parent.postMessage({ type: 'pi-studio-html-artifact-size', id: PREVIEW_ID, height }, '*'); } catch {}\n"
2629
+ + " }\n"
2630
+ + " function scheduleHeight() {\n"
2631
+ + " if (scheduled) return;\n"
2632
+ + " scheduled = true;\n"
2633
+ + " requestAnimationFrame(sendHeight);\n"
2634
+ + " }\n"
2635
+ + " window.addEventListener('message', (event) => {\n"
2636
+ + " const data = event && event.data;\n"
2637
+ + " if (!data || typeof data !== 'object') return;\n"
2638
+ + " if (data.type !== 'pi-studio-html-artifact-zoom' || data.id !== PREVIEW_ID) return;\n"
2639
+ + " applyZoom(data.zoom);\n"
2640
+ + " });\n"
2641
+ + " window.addEventListener('load', scheduleHeight);\n"
2642
+ + " window.addEventListener('resize', scheduleHeight);\n"
2643
+ + " if (typeof ResizeObserver === 'function') {\n"
2644
+ + " const observer = new ResizeObserver(scheduleHeight);\n"
2645
+ + " observer.observe(document.documentElement);\n"
2646
+ + " if (document.body) observer.observe(document.body);\n"
2647
+ + " }\n"
2648
+ + " if (typeof MutationObserver === 'function') {\n"
2649
+ + " const observer = new MutationObserver(scheduleHeight);\n"
2650
+ + " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
2651
+ + " }\n"
2652
+ + " scheduleHeight();\n"
2653
+ + " setTimeout(scheduleHeight, 80);\n"
2654
+ + " setTimeout(scheduleHeight, 350);\n"
2655
+ + "})();\n"
2656
+ + "<\/script>";
2657
+ }
2658
+
2659
+ function buildHtmlArtifactPreviewHeadMarkup(previewId) {
2660
+ return "<meta charset=\"utf-8\">\n"
2661
+ + "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n"
2662
+ + "<meta http-equiv=\"Content-Security-Policy\" content=\"" + escapeHtml(HTML_ARTIFACT_PREVIEW_CSP) + "\">\n"
2663
+ + buildHtmlArtifactPreviewResizeScript(previewId);
2664
+ }
2665
+
2666
+ function buildHtmlArtifactSrcdoc(html, previewId) {
2667
+ const source = String(html || "");
2668
+ const headMarkup = buildHtmlArtifactPreviewHeadMarkup(previewId);
2669
+ if (/<head\b[^>]*>/i.test(source)) {
2670
+ return source.replace(/<head\b[^>]*>/i, (match) => match + "\n" + headMarkup + "\n");
2671
+ }
2672
+ if (/<body\b[^>]*>/i.test(source)) {
2673
+ return source.replace(/<body\b/i, "<head>\n" + headMarkup + "\n</head>\n<body");
2674
+ }
2675
+ if (/<html\b[^>]*>/i.test(source)) {
2676
+ return source.replace(/<html\b[^>]*>/i, (match) => match + "\n<head>\n" + headMarkup + "\n</head>\n");
2677
+ }
2678
+ return "<!doctype html>\n<html>\n<head>\n" + headMarkup + "\n</head>\n<body>\n" + source + "\n</body>\n</html>";
2679
+ }
2680
+
2681
+ function pruneDisconnectedHtmlArtifactFrames() {
2682
+ htmlArtifactFramesById.forEach((record, id) => {
2683
+ if (!record || !record.iframe || !record.iframe.isConnected) {
2684
+ htmlArtifactFramesById.delete(id);
2685
+ }
2686
+ });
2687
+ }
2688
+
2689
+ function handleHtmlArtifactFrameSizeMessage(event) {
2690
+ const data = event && event.data;
2691
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-size") return;
2692
+ const id = typeof data.id === "string" ? data.id : "";
2693
+ const record = id ? htmlArtifactFramesById.get(id) : null;
2694
+ if (!record || !record.iframe || !record.iframe.isConnected) {
2695
+ if (id) htmlArtifactFramesById.delete(id);
2696
+ return;
2697
+ }
2698
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
2699
+ const rawHeight = Number(data.height);
2700
+ if (!Number.isFinite(rawHeight) || rawHeight <= 0) return;
2701
+ const measuredHeight = Math.ceil(rawHeight + 2);
2702
+ const capped = measuredHeight > HTML_ARTIFACT_FRAME_FIT_CAP_HEIGHT;
2703
+ const nextHeight = Math.max(
2704
+ HTML_ARTIFACT_FRAME_MIN_HEIGHT,
2705
+ Math.min(HTML_ARTIFACT_FRAME_FIT_CAP_HEIGHT, measuredHeight),
2706
+ );
2707
+ record.iframe.style.height = nextHeight + "px";
2708
+ record.iframe.classList.toggle("is-height-capped", capped);
2709
+ if (record.shell && record.shell.style) {
2710
+ record.shell.style.minHeight = "0";
2711
+ record.shell.classList.toggle("is-height-capped", capped);
2712
+ }
2713
+ if (record.detail) {
2714
+ record.detail.textContent = "HTML artifact";
2715
+ }
2716
+ }
2717
+
2718
+ window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
2719
+
2720
+ function renderHtmlArtifactPreview(targetEl, html, pane, options) {
2721
+ if (!targetEl) return;
2722
+ const title = options && options.title ? String(options.title) : "HTML artifact preview";
2723
+ const previewId = "html_artifact_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10);
2724
+ pruneDisconnectedHtmlArtifactFrames();
2725
+ clearPreviewJumpHighlight(targetEl);
2726
+ finishPreviewRender(targetEl);
2727
+ targetEl.innerHTML = "";
2728
+
2729
+ const shell = document.createElement("div");
2730
+ shell.className = "studio-html-artifact-shell";
2731
+
2732
+ const toolbar = document.createElement("div");
2733
+ toolbar.className = "studio-html-artifact-toolbar";
2734
+ const label = document.createElement("span");
2735
+ label.className = "studio-html-artifact-label";
2736
+ label.textContent = title;
2737
+ const detail = document.createElement("span");
2738
+ detail.className = "studio-html-artifact-detail";
2739
+ detail.textContent = "HTML artifact";
2740
+
2741
+ const tools = document.createElement("span");
2742
+ tools.className = "studio-html-artifact-tools";
2743
+ tools.appendChild(detail);
2744
+
2745
+ const zoomControls = document.createElement("span");
2746
+ zoomControls.className = "studio-html-artifact-zoom-controls";
2747
+ let artifactZoom = 1;
2748
+ let iframe = null;
2749
+ const formatZoomLabel = () => Math.round(artifactZoom * 100) + "%";
2750
+ const postArtifactZoom = () => {
2751
+ if (!iframe || !iframe.contentWindow) return;
2752
+ try {
2753
+ iframe.contentWindow.postMessage({ type: "pi-studio-html-artifact-zoom", id: previewId, zoom: artifactZoom }, "*");
2754
+ } catch {
2755
+ // Ignore iframe postMessage failures.
2756
+ }
2757
+ };
2758
+ const updateZoomUi = () => {
2759
+ zoomResetBtn.textContent = formatZoomLabel();
2760
+ zoomOutBtn.disabled = artifactZoom <= HTML_ARTIFACT_ZOOM_MIN + 0.001;
2761
+ zoomInBtn.disabled = artifactZoom >= HTML_ARTIFACT_ZOOM_MAX - 0.001;
2762
+ };
2763
+ const setArtifactZoom = (nextZoom) => {
2764
+ artifactZoom = Math.max(
2765
+ HTML_ARTIFACT_ZOOM_MIN,
2766
+ Math.min(HTML_ARTIFACT_ZOOM_MAX, Math.round(Number(nextZoom || 1) * 100) / 100),
2767
+ );
2768
+ updateZoomUi();
2769
+ postArtifactZoom();
2770
+ };
2771
+ const makeZoomButton = (text, title, onClick) => {
2772
+ const button = document.createElement("button");
2773
+ button.type = "button";
2774
+ button.className = "studio-html-artifact-zoom-btn";
2775
+ button.textContent = text;
2776
+ button.title = title;
2777
+ button.addEventListener("pointerdown", (event) => { event.stopPropagation(); });
2778
+ button.addEventListener("mousedown", (event) => { event.stopPropagation(); });
2779
+ button.addEventListener("click", (event) => {
2780
+ event.preventDefault();
2781
+ event.stopPropagation();
2782
+ onClick();
2783
+ });
2784
+ return button;
2785
+ };
2786
+ const zoomOutBtn = makeZoomButton("−", "Zoom out HTML artifact", () => setArtifactZoom(artifactZoom - HTML_ARTIFACT_ZOOM_STEP));
2787
+ const zoomResetBtn = makeZoomButton("100%", "Reset HTML artifact zoom", () => setArtifactZoom(1));
2788
+ zoomResetBtn.classList.add("studio-html-artifact-zoom-reset");
2789
+ const zoomInBtn = makeZoomButton("+", "Zoom in HTML artifact", () => setArtifactZoom(artifactZoom + HTML_ARTIFACT_ZOOM_STEP));
2790
+ zoomControls.appendChild(zoomOutBtn);
2791
+ zoomControls.appendChild(zoomResetBtn);
2792
+ zoomControls.appendChild(zoomInBtn);
2793
+ updateZoomUi();
2794
+ tools.appendChild(zoomControls);
2795
+
2796
+ toolbar.appendChild(label);
2797
+ toolbar.appendChild(tools);
2798
+ shell.appendChild(toolbar);
2799
+
2800
+ iframe = document.createElement("iframe");
2801
+ iframe.className = "studio-html-artifact-frame";
2802
+ iframe.title = title;
2803
+ iframe.loading = "lazy";
2804
+ iframe.referrerPolicy = "no-referrer";
2805
+ iframe.setAttribute("sandbox", "allow-scripts allow-modals");
2806
+ iframe.setAttribute("allow", "clipboard-write");
2807
+ iframe.addEventListener("load", () => { postArtifactZoom(); });
2808
+ iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
2809
+ shell.appendChild(iframe);
2810
+ htmlArtifactFramesById.set(previewId, { iframe, shell, detail, zoomControls });
2811
+
2812
+ targetEl.appendChild(shell);
2813
+
2814
+ if (pane === "response") {
2815
+ applyPendingResponseScrollReset();
2816
+ scheduleResponsePaneRepaintNudge();
2817
+ }
2818
+ }
2819
+
2820
+ function getRightPaneHtmlArtifactSource() {
2821
+ if (rightView === "editor-preview") {
2822
+ const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
2823
+ return isHtmlArtifactPreviewText(editorText, editorLanguage) ? editorText : "";
2824
+ }
2825
+ if (rightView === "preview") {
2826
+ return isHtmlArtifactPreviewText(latestResponseMarkdown, "") ? latestResponseMarkdown : "";
2827
+ }
2828
+ return "";
2829
+ }
2830
+
2355
2831
  function stripMatchingQuotes(value) {
2356
2832
  const text = String(value || "").trim();
2357
2833
  if (text.length >= 2) {
@@ -3387,7 +3863,7 @@
3387
3863
  }
3388
3864
 
3389
3865
  async function exportRightPanePdf() {
3390
- if (uiBusy || pdfExportInProgress) {
3866
+ if (uiBusy || previewExportInProgress) {
3391
3867
  setStatus("Studio is busy.", "warning");
3392
3868
  return;
3393
3869
  }
@@ -3404,6 +3880,12 @@
3404
3880
  return;
3405
3881
  }
3406
3882
 
3883
+ const htmlArtifactSource = getRightPaneHtmlArtifactSource();
3884
+ if (htmlArtifactSource) {
3885
+ setStatus("PDF export does not support HTML artifacts yet. Export as HTML or use the browser print dialog inside the artifact.", "warning");
3886
+ return;
3887
+ }
3888
+
3407
3889
  const markdown = rightView === "editor-preview"
3408
3890
  ? prepareEditorTextForPdfExport(sourceTextEl.value)
3409
3891
  : prepareEditorTextForPreview(latestResponseMarkdown);
@@ -3427,7 +3909,7 @@
3427
3909
  filenameHint = stem + "-preview.pdf";
3428
3910
  }
3429
3911
 
3430
- pdfExportInProgress = true;
3912
+ previewExportInProgress = true;
3431
3913
  updateResultActionButtons();
3432
3914
  setStatus("Exporting PDF…", "warning");
3433
3915
 
@@ -3540,11 +4022,202 @@
3540
4022
  const detail = error && error.message ? error.message : String(error || "unknown error");
3541
4023
  setStatus("PDF export failed: " + detail, "error");
3542
4024
  } finally {
3543
- pdfExportInProgress = false;
4025
+ previewExportInProgress = false;
3544
4026
  updateResultActionButtons();
3545
4027
  }
3546
4028
  }
3547
4029
 
4030
+ async function exportRightPaneHtml() {
4031
+ if (uiBusy || previewExportInProgress) {
4032
+ setStatus("Studio is busy.", "warning");
4033
+ return;
4034
+ }
4035
+
4036
+ const token = getToken();
4037
+ if (!token) {
4038
+ setStatus("Missing Studio token in URL. Re-run /studio.", "error");
4039
+ return;
4040
+ }
4041
+
4042
+ const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
4043
+ if (!rightPaneShowsPreview) {
4044
+ setStatus("Switch right pane to Response (Preview) or Editor (Preview) to export HTML.", "warning");
4045
+ return;
4046
+ }
4047
+
4048
+ const htmlArtifactSource = getRightPaneHtmlArtifactSource();
4049
+ const markdown = htmlArtifactSource || (rightView === "editor-preview"
4050
+ ? prepareEditorTextForHtmlExport(sourceTextEl.value)
4051
+ : prepareEditorTextForPreview(latestResponseMarkdown));
4052
+ if (!markdown || !markdown.trim()) {
4053
+ setStatus("Nothing to export yet.", "warning");
4054
+ return;
4055
+ }
4056
+
4057
+ const effectivePath = getEffectiveSavePath();
4058
+ const sourcePath = effectivePath || sourceState.path || "";
4059
+ const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
4060
+ const isEditorPreview = rightView === "editor-preview";
4061
+ const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
4062
+ const isLatex = htmlArtifactSource ? false : (isEditorPreview
4063
+ ? editorHtmlLanguage === "latex"
4064
+ : /\\documentclass\b|\\begin\{document\}/.test(markdown));
4065
+ let filenameHint = isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html";
4066
+ let titleHint = isEditorPreview ? "Studio editor preview" : "Studio response preview";
4067
+ if (sourcePath) {
4068
+ const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
4069
+ const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
4070
+ filenameHint = stem + "-preview.html";
4071
+ titleHint = stem + " preview";
4072
+ }
4073
+
4074
+ previewExportInProgress = true;
4075
+ updateResultActionButtons();
4076
+ setStatus("Exporting HTML…", "warning");
4077
+
4078
+ try {
4079
+ const response = await fetch("/export-html?token=" + encodeURIComponent(token), {
4080
+ method: "POST",
4081
+ headers: {
4082
+ "Content-Type": "application/json",
4083
+ },
4084
+ body: JSON.stringify({
4085
+ markdown: String(markdown || ""),
4086
+ sourcePath: sourcePath,
4087
+ resourceDir: resourceDir,
4088
+ isLatex: isLatex,
4089
+ editorHtmlLanguage: editorHtmlLanguage,
4090
+ filenameHint: filenameHint,
4091
+ title: titleHint,
4092
+ }),
4093
+ });
4094
+
4095
+ const contentType = String(response.headers.get("content-type") || "").toLowerCase();
4096
+ if (!response.ok) {
4097
+ let message = "HTML export failed with HTTP " + response.status + ".";
4098
+ if (contentType.includes("application/json")) {
4099
+ const payload = await response.json().catch(() => null);
4100
+ if (payload && typeof payload.error === "string") {
4101
+ message = payload.error;
4102
+ }
4103
+ } else {
4104
+ const text = await response.text().catch(() => "");
4105
+ if (text && text.trim()) {
4106
+ message = text.trim();
4107
+ }
4108
+ }
4109
+ throw new Error(message);
4110
+ }
4111
+
4112
+ if (contentType.includes("application/json")) {
4113
+ const payload = await response.json().catch(() => null);
4114
+ if (!payload || typeof payload.downloadUrl !== "string") {
4115
+ throw new Error("HTML export prepared successfully, but Studio did not receive a download URL.");
4116
+ }
4117
+
4118
+ const exportWarning = typeof payload.warning === "string" ? payload.warning.trim() : "";
4119
+ const openError = typeof payload.openError === "string" ? payload.openError.trim() : "";
4120
+ const openedExternal = payload.openedExternal === true;
4121
+ let downloadName = typeof payload.filename === "string" && payload.filename.trim()
4122
+ ? payload.filename.trim()
4123
+ : (filenameHint || "studio-preview.html");
4124
+ if (!/\.html?$/i.test(downloadName)) {
4125
+ downloadName += ".html";
4126
+ }
4127
+
4128
+ if (openedExternal) {
4129
+ if (exportWarning) {
4130
+ setStatus("Opened HTML in default browser with warning: " + exportWarning, "warning");
4131
+ } else {
4132
+ setStatus("Opened HTML in default browser: " + downloadName, "success");
4133
+ }
4134
+ return;
4135
+ }
4136
+
4137
+ const link = document.createElement("a");
4138
+ link.href = payload.downloadUrl;
4139
+ link.download = downloadName;
4140
+ link.rel = "noopener";
4141
+ document.body.appendChild(link);
4142
+ link.click();
4143
+ link.remove();
4144
+
4145
+ if (openError) {
4146
+ if (exportWarning) {
4147
+ setStatus("Opened browser fallback because external viewer failed (" + openError + "). Warning: " + exportWarning, "warning");
4148
+ } else {
4149
+ setStatus("Opened browser fallback because external viewer failed (" + openError + ").", "warning");
4150
+ }
4151
+ } else if (exportWarning) {
4152
+ setStatus("Exported HTML with warning: " + exportWarning, "warning");
4153
+ } else {
4154
+ setStatus("Exported HTML: " + downloadName, "success");
4155
+ }
4156
+ return;
4157
+ }
4158
+
4159
+ const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
4160
+ const blob = await response.blob();
4161
+ const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
4162
+ let downloadName = headerFilename || filenameHint || "studio-preview.html";
4163
+ if (!/\.html?$/i.test(downloadName)) {
4164
+ downloadName += ".html";
4165
+ }
4166
+
4167
+ const blobUrl = URL.createObjectURL(blob);
4168
+ const link = document.createElement("a");
4169
+ link.href = blobUrl;
4170
+ link.download = downloadName;
4171
+ link.rel = "noopener";
4172
+ document.body.appendChild(link);
4173
+ link.click();
4174
+ link.remove();
4175
+ window.setTimeout(() => {
4176
+ URL.revokeObjectURL(blobUrl);
4177
+ }, 1800);
4178
+
4179
+ if (exportWarning) {
4180
+ setStatus("Exported HTML with warning: " + exportWarning, "warning");
4181
+ } else {
4182
+ setStatus("Exported HTML: " + downloadName, "success");
4183
+ }
4184
+ } catch (error) {
4185
+ const detail = error && error.message ? error.message : String(error || "unknown error");
4186
+ setStatus("HTML export failed: " + detail, "error");
4187
+ } finally {
4188
+ previewExportInProgress = false;
4189
+ updateResultActionButtons();
4190
+ }
4191
+ }
4192
+
4193
+ function closeExportPreviewMenu() {
4194
+ if (!exportPreviewMenuEl) return;
4195
+ exportPreviewMenuEl.hidden = true;
4196
+ if (exportPdfBtn) {
4197
+ exportPdfBtn.classList.remove("is-open");
4198
+ exportPdfBtn.setAttribute("aria-expanded", "false");
4199
+ }
4200
+ }
4201
+
4202
+ function toggleExportPreviewMenu() {
4203
+ if (!exportPreviewMenuEl || !exportPdfBtn || exportPdfBtn.disabled) return;
4204
+ if (typeof closeStudioUiRefreshMenus === "function") {
4205
+ closeStudioUiRefreshMenus();
4206
+ }
4207
+ const willOpen = exportPreviewMenuEl.hidden;
4208
+ exportPreviewMenuEl.hidden = !willOpen;
4209
+ exportPdfBtn.classList.toggle("is-open", willOpen);
4210
+ exportPdfBtn.setAttribute("aria-expanded", willOpen ? "true" : "false");
4211
+ }
4212
+
4213
+ function exportRightPaneFormat(format) {
4214
+ closeExportPreviewMenu();
4215
+ if (format === "html") {
4216
+ return exportRightPaneHtml();
4217
+ }
4218
+ return exportRightPanePdf();
4219
+ }
4220
+
3548
4221
  function normalizeCopyableBlockText(text) {
3549
4222
  return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
3550
4223
  }
@@ -3726,6 +4399,10 @@
3726
4399
  function renderSourcePreviewNow() {
3727
4400
  if (editorView !== "preview") return;
3728
4401
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
4402
+ if (isHtmlArtifactPreviewText(text, editorLanguage)) {
4403
+ renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML artifact preview" });
4404
+ return;
4405
+ }
3729
4406
  if (supportsCodePreviewCommentsForCurrentEditor()) {
3730
4407
  renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
3731
4408
  return;
@@ -3857,17 +4534,27 @@
3857
4534
  const visibleWorking = buildVisibleWorkingText(filter);
3858
4535
  const hasVisibleContent = Boolean(visibleWorking.trim());
3859
4536
  const started = formatReferenceTime(state.startedAt || state.updatedAt);
3860
- const statusLabel = state.status === "running"
3861
- ? "Live"
3862
- : (state.status === "complete" ? "Complete" : "Idle");
4537
+ const context = traceDisplayContext || {};
4538
+ const statusLabel = context.mode === "history"
4539
+ ? "Saved"
4540
+ : (context.mode === "loading"
4541
+ ? "Loading"
4542
+ : (context.mode === "missing"
4543
+ ? "Not saved"
4544
+ : (state.status === "running" ? "Live" : (state.status === "complete" ? "Complete" : "Idle"))));
3863
4545
  const filterMeta = filter === "thinking"
3864
4546
  ? "Thinking only"
3865
4547
  : (filter === "tools" ? "Tools only" : null);
4548
+ const historyMeta = (context.mode === "history" || context.mode === "missing" || context.mode === "loading")
4549
+ ? getTraceHistoryContextLabel()
4550
+ : null;
3866
4551
  const toolbar = "<div class='trace-toolbar'>"
3867
4552
  + "<div class='trace-summary'>"
3868
4553
  + "<span class='trace-summary-badge'>Working</span>"
3869
4554
  + "<span class='trace-summary-status trace-status-" + escapeHtml(String(state.status || "idle")) + "'>" + escapeHtml(statusLabel) + "</span>"
4555
+ + (historyMeta ? ("<span class='trace-summary-meta'>" + escapeHtml(historyMeta) + "</span>") : "")
3870
4556
  + (started ? ("<span class='trace-summary-meta'>Started " + escapeHtml(started) + "</span>") : "")
4557
+ + (context.summary && context.summary.truncated ? "<span class='trace-summary-meta'>Truncated</span>" : "")
3871
4558
  + (filterMeta ? ("<span class='trace-summary-meta'>" + escapeHtml(filterMeta) + "</span>") : "")
3872
4559
  + "</div>"
3873
4560
  + "<div class='trace-controls'>"
@@ -3882,13 +4569,17 @@
3882
4569
  + "</div>";
3883
4570
 
3884
4571
  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."));
4572
+ const emptyMessage = context.mode === "loading"
4573
+ ? "Loading saved working for this response…"
4574
+ : (context.mode === "missing"
4575
+ ? "No working was saved for this response."
4576
+ : (filter === "thinking"
4577
+ ? "No thinking steps in this working view yet."
4578
+ : (filter === "tools"
4579
+ ? "No tool steps in this working view yet."
4580
+ : (state.status === "running"
4581
+ ? "Waiting for the first model or tool update…"
4582
+ : "No live working view yet. Start a run or critique to watch working details here."))));
3892
4583
  return "<div class='trace-panel'>" + toolbar + "<div class='trace-empty'>" + escapeHtml(emptyMessage) + "</div></div>";
3893
4584
  }
3894
4585
 
@@ -3980,6 +4671,10 @@
3980
4671
  scheduleResponsePaneRepaintNudge();
3981
4672
  return;
3982
4673
  }
4674
+ if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
4675
+ renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML artifact preview" });
4676
+ return;
4677
+ }
3983
4678
  if (supportsCodePreviewCommentsForCurrentEditor()) {
3984
4679
  renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
3985
4680
  return;
@@ -4000,6 +4695,10 @@
4000
4695
  }
4001
4696
 
4002
4697
  if (rightView === "preview") {
4698
+ if (isHtmlArtifactPreviewText(markdown, "")) {
4699
+ renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML artifact preview" });
4700
+ return;
4701
+ }
4003
4702
  const nonce = ++responsePreviewRenderNonce;
4004
4703
  beginPreviewRender(critiqueViewEl);
4005
4704
  void applyRenderedMarkdown(critiqueViewEl, markdown, "response", nonce);
@@ -4065,19 +4764,44 @@
4065
4764
 
4066
4765
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
4067
4766
  const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
4068
- const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
4767
+ const canExportPreview = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
4768
+ const htmlArtifactExportSource = canExportPreview ? getRightPaneHtmlArtifactSource() : "";
4769
+ const isHtmlArtifactPreview = Boolean(htmlArtifactExportSource);
4069
4770
  if (exportPdfBtn) {
4070
- exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
4771
+ exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
4772
+ exportPdfBtn.textContent = previewExportInProgress ? "Exporting…" : "Export right preview";
4071
4773
  if (rightView === "trace") {
4072
- exportPdfBtn.title = "Working view does not support PDF export.";
4774
+ exportPdfBtn.title = "Working view does not support preview export.";
4073
4775
  } else if (rightView === "markdown") {
4074
- exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
4075
- } else if (!canExportPdf) {
4776
+ exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export.";
4777
+ } else if (!canExportPreview) {
4076
4778
  exportPdfBtn.title = "Nothing to export yet.";
4779
+ } else if (isHtmlArtifactPreview) {
4780
+ exportPdfBtn.title = "This is an HTML artifact preview. Export as HTML; PDF export is not available yet.";
4077
4781
  } else {
4078
- exportPdfBtn.title = "Export the current right-pane preview as PDF via pandoc + xelatex.";
4782
+ exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
4079
4783
  }
4080
4784
  }
4785
+ if (exportPreviewPdfBtn) {
4786
+ exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
4787
+ exportPreviewPdfBtn.title = isHtmlArtifactPreview
4788
+ ? "HTML artifact PDF export is not available yet."
4789
+ : "Export the current right-pane preview as PDF.";
4790
+ }
4791
+ if (exportPreviewHtmlBtn) {
4792
+ exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
4793
+ exportPreviewHtmlBtn.title = isHtmlArtifactPreview
4794
+ ? "Export the authored HTML artifact."
4795
+ : "Export the current right-pane preview as standalone HTML.";
4796
+ }
4797
+ if (exportPreviewControlsEl) {
4798
+ exportPreviewControlsEl.title = canExportPreview
4799
+ ? (isHtmlArtifactPreview ? "Export this HTML artifact." : "Choose a format and export the current right-pane preview.")
4800
+ : "Switch right pane to a non-empty preview before exporting.";
4801
+ }
4802
+ if (!canExportPreview || previewExportInProgress) {
4803
+ closeExportPreviewMenu();
4804
+ }
4081
4805
 
4082
4806
  pullLatestBtn.disabled = uiBusy || followLatest;
4083
4807
  pullLatestBtn.textContent = queuedLatestResponse ? "Fetch latest response *" : "Fetch latest response";
@@ -10014,7 +10738,7 @@
10014
10738
  }
10015
10739
 
10016
10740
  if (message.traceState) {
10017
- replaceTraceState(message.traceState);
10741
+ replaceLiveTraceState(message.traceState);
10018
10742
  }
10019
10743
 
10020
10744
  let appliedHistory = false;
@@ -10064,22 +10788,41 @@
10064
10788
  }
10065
10789
 
10066
10790
  if (message.type === "trace_reset") {
10067
- replaceTraceState(message.trace);
10791
+ replaceLiveTraceState(message.trace);
10068
10792
  return;
10069
10793
  }
10070
10794
 
10071
10795
  if (message.type === "trace_status") {
10072
- updateTraceStatusFromMessage(message);
10796
+ updateLiveTraceStatusFromMessage(message);
10073
10797
  return;
10074
10798
  }
10075
10799
 
10076
10800
  if (message.type === "trace_entry_upsert") {
10077
- upsertTraceEntry(message.entry);
10801
+ upsertLiveTraceEntry(message.entry);
10078
10802
  return;
10079
10803
  }
10080
10804
 
10081
10805
  if (message.type === "trace_assistant_delta") {
10082
- appendTraceAssistantDelta(message.entryId, message.deltaKind, message.delta, message.updatedAt);
10806
+ appendLiveTraceAssistantDelta(message.entryId, message.deltaKind, message.delta, message.updatedAt);
10807
+ return;
10808
+ }
10809
+
10810
+ if (message.type === "trace_snapshot") {
10811
+ const responseId = typeof message.responseHistoryId === "string" ? message.responseHistoryId.trim() : "";
10812
+ if (responseId && message.traceState) {
10813
+ const normalizedSnapshot = normalizeTraceState(message.traceState);
10814
+ traceSnapshotCache.set(responseId, normalizedSnapshot);
10815
+ if (traceDisplayContext && traceDisplayContext.responseId === responseId) {
10816
+ setTraceDisplayContext({
10817
+ mode: "history",
10818
+ responseId,
10819
+ historyIndex: responseHistoryIndex,
10820
+ total: responseHistory.length,
10821
+ summary: normalizeTraceSummary(message.summary) || (getSelectedHistoryItem() ? getSelectedHistoryItem().traceSummary : null),
10822
+ });
10823
+ replaceTraceState(normalizedSnapshot);
10824
+ }
10825
+ }
10083
10826
  return;
10084
10827
  }
10085
10828
 
@@ -11189,11 +11932,35 @@
11189
11932
  });
11190
11933
 
11191
11934
  if (exportPdfBtn) {
11192
- exportPdfBtn.addEventListener("click", () => {
11193
- void exportRightPanePdf();
11935
+ exportPdfBtn.addEventListener("click", (event) => {
11936
+ event.preventDefault();
11937
+ event.stopPropagation();
11938
+ toggleExportPreviewMenu();
11939
+ });
11940
+ }
11941
+
11942
+ if (exportPreviewMenuEl) {
11943
+ exportPreviewMenuEl.addEventListener("click", (event) => {
11944
+ const target = event.target;
11945
+ const actionBtn = target instanceof Element ? target.closest("[data-export-preview-format]") : null;
11946
+ if (!actionBtn) return;
11947
+ event.preventDefault();
11948
+ event.stopPropagation();
11949
+ if (actionBtn.disabled) return;
11950
+ const format = String(actionBtn.getAttribute("data-export-preview-format") || "pdf").toLowerCase();
11951
+ void exportRightPaneFormat(format === "html" ? "html" : "pdf");
11194
11952
  });
11195
11953
  }
11196
11954
 
11955
+ document.addEventListener("click", (event) => {
11956
+ const target = event.target;
11957
+ if (target instanceof Element && target.closest("#exportPreviewControls")) return;
11958
+ closeExportPreviewMenu();
11959
+ });
11960
+ document.addEventListener("keydown", (event) => {
11961
+ if (event.key === "Escape") closeExportPreviewMenu();
11962
+ });
11963
+
11197
11964
  saveAsBtn.addEventListener("click", () => {
11198
11965
  const content = sourceTextEl.value;
11199
11966
  if (!content.trim()) {