pi-studio 0.5.30 → 0.5.32

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.32] — 2026-03-25
8
+
9
+ ### Added
10
+ - `/studio-pdf <path>` now accepts a curated set of advanced layout controls for file-based exports, including font size, margins, line stretch, main font, paper size, geometry, heading sizes, heading spacing, and footer skip.
11
+
12
+ ### Changed
13
+ - Large-font Markdown/QMD Studio PDF exports now switch to a more suitable LaTeX document class and use a safer default footer skip unless you explicitly override the geometry.
14
+ - PDF callout blocks now render more compactly, reducing extra vertical whitespace around note/tip/warning content.
15
+
16
+ ### Fixed
17
+ - Studio preview/PDF preparation now treats `.qmd` files like Markdown, strips HTML comments more narrowly, shows standalone LaTeX page-break commands as subtle preview dividers, and supports common Quarto-style callout and `fig-align` patterns in preview/PDF output.
18
+ - Markdown/QMD preview now renders embedded local PDF figures more reliably via `pdf.js`, avoiding grey-box browser embed failures in the Studio preview surface.
19
+
20
+ ## [0.5.31] — 2026-03-24
21
+
22
+ ### Fixed
23
+ - The right-pane response view now nudges the browser to repaint after response renders complete, reducing cases where freshly rendered response content stayed visually blank until the user scrolled or interacted with the pane.
24
+ - Newly selected or newly arrived responses now reset the right-pane scroll position to the top by default, while **Editor (Preview)** continues to preserve scroll position so in-place edit/preview workflows still feel natural.
25
+
7
26
  ## [0.5.30] — 2026-03-24
8
27
 
9
28
  ### Fixed
package/README.md CHANGED
@@ -43,7 +43,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
43
43
  | `/studio --stop` | Stop studio server |
44
44
  | `/studio --help` | Show help |
45
45
  | `/studio-current <path>` | Load a file into currently open Studio tab(s) without opening a new browser window |
46
- | `/studio-pdf <path>` | Export a local file to `<name>.studio.pdf` via the Studio PDF pipeline |
46
+ | `/studio-pdf <path> [options]` | Export a local file to `<name>.studio.pdf` via the Studio PDF pipeline, with optional layout controls |
47
47
 
48
48
  ## Install
49
49
 
@@ -188,7 +188,7 @@
188
188
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
189
189
  // Single source of truth: language -> file extensions (and display label)
190
190
  var LANG_EXT_MAP = {
191
- markdown: { label: "Markdown", exts: ["md", "markdown", "mdx"] },
191
+ markdown: { label: "Markdown", exts: ["md", "markdown", "mdx", "qmd"] },
192
192
  javascript: { label: "JavaScript", exts: ["js", "mjs", "cjs", "jsx"] },
193
193
  typescript: { label: "TypeScript", exts: ["ts", "mts", "cts", "tsx"] },
194
194
  python: { label: "Python", exts: ["py", "pyw"] },
@@ -234,6 +234,7 @@
234
234
  let sourcePreviewRenderNonce = 0;
235
235
  let responsePreviewRenderNonce = 0;
236
236
  let responseEditorPreviewTimer = null;
237
+ let pendingResponseScrollReset = false;
237
238
  let editorMetaUpdateRaf = null;
238
239
  let editorHighlightEnabled = false;
239
240
  let editorLanguage = "markdown";
@@ -244,6 +245,8 @@
244
245
  const EMPTY_OVERLAY_LINE = "\u200b";
245
246
  const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
246
247
  const MATHJAX_CDN_URL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
248
+ const PDFJS_CDN_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/legacy/build/pdf.min.mjs";
249
+ const PDFJS_WORKER_CDN_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/legacy/build/pdf.worker.min.mjs";
247
250
  const BOOT = (typeof window.__PI_STUDIO_BOOT__ === "object" && window.__PI_STUDIO_BOOT__)
248
251
  ? window.__PI_STUDIO_BOOT__
249
252
  : {};
@@ -254,9 +257,12 @@
254
257
  const MERMAID_RENDER_FAIL_MESSAGE = "Mermaid render failed. Showing diagram source text.";
255
258
  const MATHJAX_UNAVAILABLE_MESSAGE = "Math fallback unavailable. Some unsupported equations may remain as raw TeX.";
256
259
  const MATHJAX_RENDER_FAIL_MESSAGE = "Math fallback could not render some unsupported equations.";
260
+ const PDF_PREVIEW_UNAVAILABLE_MESSAGE = "PDF figure preview unavailable. Inline PDF rendering is not supported in this Studio browser environment.";
261
+ const PDF_PREVIEW_RENDER_FAIL_MESSAGE = "PDF figure preview could not be rendered.";
257
262
  let mermaidModulePromise = null;
258
263
  let mermaidInitialized = false;
259
264
  let mathJaxPromise = null;
265
+ let pdfJsPromise = null;
260
266
 
261
267
  const DEBUG_ENABLED = (() => {
262
268
  try {
@@ -971,6 +977,7 @@
971
977
  }
972
978
 
973
979
  function clearActiveResponseView() {
980
+ pendingResponseScrollReset = false;
974
981
  latestResponseMarkdown = "";
975
982
  latestResponseThinking = "";
976
983
  latestResponseKind = "annotation";
@@ -1016,13 +1023,13 @@
1016
1023
  }
1017
1024
  }
1018
1025
 
1019
- function applySelectedHistoryItem() {
1026
+ function applySelectedHistoryItem(options) {
1020
1027
  const item = getSelectedHistoryItem();
1021
1028
  if (!item) {
1022
1029
  clearActiveResponseView();
1023
1030
  return false;
1024
1031
  }
1025
- handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking);
1032
+ handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking, options);
1026
1033
  return true;
1027
1034
  }
1028
1035
 
@@ -1035,9 +1042,13 @@
1035
1042
  return false;
1036
1043
  }
1037
1044
 
1045
+ const previousItem = getSelectedHistoryItem();
1046
+ const previousId = previousItem && typeof previousItem.id === "string" ? previousItem.id : null;
1038
1047
  const nextIndex = Math.max(0, Math.min(total - 1, Number(index) || 0));
1039
1048
  responseHistoryIndex = nextIndex;
1040
- const applied = applySelectedHistoryItem();
1049
+ const nextItem = getSelectedHistoryItem();
1050
+ const nextId = nextItem && typeof nextItem.id === "string" ? nextItem.id : null;
1051
+ const applied = applySelectedHistoryItem({ resetScroll: previousId !== nextId });
1041
1052
  updateHistoryControls();
1042
1053
 
1043
1054
  if (applied && !(options && options.silent)) {
@@ -1242,11 +1253,210 @@
1242
1253
  mathMl: true,
1243
1254
  svg: true,
1244
1255
  },
1256
+ ADD_TAGS: ["embed"],
1257
+ ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align"],
1258
+ ADD_DATA_URI_TAGS: ["embed"],
1245
1259
  });
1246
1260
  }
1247
1261
  return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
1248
1262
  }
1249
1263
 
1264
+ function isPdfPreviewSource(src) {
1265
+ return Boolean(src) && (/^data:application\/pdf(?:;|,)/i.test(src) || /\.pdf(?:$|[?#])/i.test(src));
1266
+ }
1267
+
1268
+ function decoratePdfEmbeds(targetEl) {
1269
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") {
1270
+ return;
1271
+ }
1272
+
1273
+ const embeds = targetEl.querySelectorAll("embed[src]");
1274
+ embeds.forEach(function(embedEl) {
1275
+ const src = typeof embedEl.getAttribute === "function" ? (embedEl.getAttribute("src") || "") : "";
1276
+ if (!isPdfPreviewSource(src)) {
1277
+ return;
1278
+ }
1279
+ if (!embedEl.getAttribute("type")) {
1280
+ embedEl.setAttribute("type", "application/pdf");
1281
+ }
1282
+ if (!embedEl.getAttribute("title")) {
1283
+ embedEl.setAttribute("title", "Embedded PDF figure");
1284
+ }
1285
+ });
1286
+ }
1287
+
1288
+ function decodePdfDataUri(src) {
1289
+ const match = String(src || "").match(/^data:application\/pdf(?:;[^,]*)?,([A-Za-z0-9+/=\s]+)$/i);
1290
+ if (!match) return null;
1291
+ const payload = (match[1] || "").replace(/\s+/g, "");
1292
+ if (!payload) return null;
1293
+ const binary = window.atob(payload);
1294
+ const bytes = new Uint8Array(binary.length);
1295
+ for (let i = 0; i < binary.length; i += 1) {
1296
+ bytes[i] = binary.charCodeAt(i);
1297
+ }
1298
+ return bytes;
1299
+ }
1300
+
1301
+ function ensurePdfJs() {
1302
+ if (window.pdfjsLib && typeof window.pdfjsLib.getDocument === "function") {
1303
+ return Promise.resolve(window.pdfjsLib);
1304
+ }
1305
+ if (pdfJsPromise) {
1306
+ return pdfJsPromise;
1307
+ }
1308
+
1309
+ pdfJsPromise = import(PDFJS_CDN_URL)
1310
+ .then((module) => {
1311
+ const api = module && typeof module.getDocument === "function"
1312
+ ? module
1313
+ : (module && module.default && typeof module.default.getDocument === "function" ? module.default : null);
1314
+ if (!api || typeof api.getDocument !== "function") {
1315
+ throw new Error("pdf.js did not initialize.");
1316
+ }
1317
+ if (api.GlobalWorkerOptions && !api.GlobalWorkerOptions.workerSrc) {
1318
+ api.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_CDN_URL;
1319
+ }
1320
+ window.pdfjsLib = api;
1321
+ return api;
1322
+ })
1323
+ .catch((error) => {
1324
+ pdfJsPromise = null;
1325
+ throw error;
1326
+ });
1327
+
1328
+ return pdfJsPromise;
1329
+ }
1330
+
1331
+ function appendPdfPreviewNotice(targetEl, message) {
1332
+ if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
1333
+ return;
1334
+ }
1335
+ if (targetEl.querySelector(".preview-pdf-warning")) {
1336
+ return;
1337
+ }
1338
+ const warningEl = document.createElement("div");
1339
+ warningEl.className = "preview-warning preview-pdf-warning";
1340
+ warningEl.textContent = String(message || PDF_PREVIEW_UNAVAILABLE_MESSAGE);
1341
+ targetEl.appendChild(warningEl);
1342
+ }
1343
+
1344
+ async function loadPdfDocumentSource(src) {
1345
+ const embedded = decodePdfDataUri(src);
1346
+ if (embedded) {
1347
+ return { data: embedded };
1348
+ }
1349
+ const response = await fetch(src);
1350
+ if (!response.ok) {
1351
+ throw new Error("Failed to fetch PDF figure for preview.");
1352
+ }
1353
+ const bytes = new Uint8Array(await response.arrayBuffer());
1354
+ return { data: bytes };
1355
+ }
1356
+
1357
+ async function renderSinglePdfPreviewEmbed(embedEl, pdfjsLib) {
1358
+ if (!embedEl || embedEl.dataset.studioPdfPreviewRendered === "1") {
1359
+ return false;
1360
+ }
1361
+
1362
+ const src = embedEl.getAttribute("src") || "";
1363
+ if (!isPdfPreviewSource(src)) {
1364
+ return false;
1365
+ }
1366
+
1367
+ const measuredWidth = Math.max(1, Math.round(embedEl.getBoundingClientRect().width || 0));
1368
+ const styleText = embedEl.getAttribute("style") || "";
1369
+ const widthAttr = embedEl.getAttribute("width") || "";
1370
+ const figAlign = embedEl.getAttribute("data-fig-align") || "";
1371
+ const pdfSource = await loadPdfDocumentSource(src);
1372
+ const loadingTask = pdfjsLib.getDocument(pdfSource);
1373
+ const pdfDocument = await loadingTask.promise;
1374
+
1375
+ try {
1376
+ const page = await pdfDocument.getPage(1);
1377
+ const baseViewport = page.getViewport({ scale: 1 });
1378
+ const cssWidth = Math.max(1, measuredWidth || Math.round(baseViewport.width));
1379
+ const renderScale = Math.max(0.25, cssWidth / baseViewport.width) * Math.min(window.devicePixelRatio || 1, 2);
1380
+ const viewport = page.getViewport({ scale: renderScale });
1381
+ const canvas = document.createElement("canvas");
1382
+ const context = canvas.getContext("2d", { alpha: false });
1383
+ if (!context) {
1384
+ throw new Error("Canvas 2D context unavailable.");
1385
+ }
1386
+
1387
+ canvas.width = Math.max(1, Math.ceil(viewport.width));
1388
+ canvas.height = Math.max(1, Math.ceil(viewport.height));
1389
+ canvas.style.width = "100%";
1390
+ canvas.style.height = "auto";
1391
+ canvas.setAttribute("aria-label", "PDF figure preview");
1392
+
1393
+ await page.render({
1394
+ canvasContext: context,
1395
+ viewport,
1396
+ }).promise;
1397
+
1398
+ const wrapper = document.createElement("div");
1399
+ wrapper.className = "studio-pdf-preview";
1400
+ if (styleText) {
1401
+ wrapper.style.cssText = styleText;
1402
+ } else if (widthAttr) {
1403
+ wrapper.style.width = /^\d+(?:\.\d+)?$/.test(widthAttr) ? (widthAttr + "px") : widthAttr;
1404
+ } else {
1405
+ wrapper.style.width = "100%";
1406
+ }
1407
+ if (figAlign) {
1408
+ wrapper.setAttribute("data-fig-align", figAlign);
1409
+ }
1410
+ wrapper.title = "PDF figure preview (page 1)";
1411
+ wrapper.appendChild(canvas);
1412
+ embedEl.dataset.studioPdfPreviewRendered = "1";
1413
+ embedEl.replaceWith(wrapper);
1414
+ return true;
1415
+ } finally {
1416
+ if (typeof pdfDocument.cleanup === "function") {
1417
+ try { pdfDocument.cleanup(); } catch {}
1418
+ }
1419
+ if (typeof pdfDocument.destroy === "function") {
1420
+ try { await pdfDocument.destroy(); } catch {}
1421
+ }
1422
+ }
1423
+ }
1424
+
1425
+ async function renderPdfPreviewsInElement(targetEl) {
1426
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") {
1427
+ return;
1428
+ }
1429
+
1430
+ const embeds = Array.from(targetEl.querySelectorAll("embed[src]"))
1431
+ .filter((embedEl) => isPdfPreviewSource(embedEl.getAttribute("src") || ""));
1432
+ if (embeds.length === 0) {
1433
+ return;
1434
+ }
1435
+
1436
+ let pdfjsLib;
1437
+ try {
1438
+ pdfjsLib = await ensurePdfJs();
1439
+ } catch (error) {
1440
+ console.error("pdf.js load failed:", error);
1441
+ appendPdfPreviewNotice(targetEl, PDF_PREVIEW_UNAVAILABLE_MESSAGE);
1442
+ return;
1443
+ }
1444
+
1445
+ let hadFailure = false;
1446
+ for (const embedEl of embeds) {
1447
+ try {
1448
+ await renderSinglePdfPreviewEmbed(embedEl, pdfjsLib);
1449
+ } catch (error) {
1450
+ hadFailure = true;
1451
+ console.error("PDF preview render failed:", error);
1452
+ }
1453
+ }
1454
+
1455
+ if (hadFailure) {
1456
+ appendPdfPreviewNotice(targetEl, PDF_PREVIEW_RENDER_FAIL_MESSAGE);
1457
+ }
1458
+ }
1459
+
1250
1460
  function appendMathFallbackNotice(targetEl, message) {
1251
1461
  if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
1252
1462
  return;
@@ -1539,6 +1749,33 @@
1539
1749
  targetEl.classList.remove("preview-pending");
1540
1750
  }
1541
1751
 
1752
+ function scheduleResponsePaneRepaintNudge() {
1753
+ if (!critiqueViewEl || typeof critiqueViewEl.getBoundingClientRect !== "function") return;
1754
+ const schedule = typeof window.requestAnimationFrame === "function"
1755
+ ? window.requestAnimationFrame.bind(window)
1756
+ : (cb) => window.setTimeout(cb, 16);
1757
+
1758
+ schedule(() => {
1759
+ if (!critiqueViewEl || !critiqueViewEl.isConnected) return;
1760
+ void critiqueViewEl.getBoundingClientRect();
1761
+ if (!critiqueViewEl.classList) return;
1762
+ critiqueViewEl.classList.add("response-repaint-nudge");
1763
+ schedule(() => {
1764
+ if (!critiqueViewEl || !critiqueViewEl.classList) return;
1765
+ critiqueViewEl.classList.remove("response-repaint-nudge");
1766
+ });
1767
+ });
1768
+ }
1769
+
1770
+ function applyPendingResponseScrollReset() {
1771
+ if (!pendingResponseScrollReset || !critiqueViewEl) return false;
1772
+ if (rightView === "editor-preview") return false;
1773
+ critiqueViewEl.scrollTop = 0;
1774
+ critiqueViewEl.scrollLeft = 0;
1775
+ pendingResponseScrollReset = false;
1776
+ return true;
1777
+ }
1778
+
1542
1779
  async function getMermaidApi() {
1543
1780
  if (mermaidModulePromise) {
1544
1781
  return mermaidModulePromise;
@@ -1868,6 +2105,8 @@
1868
2105
 
1869
2106
  finishPreviewRender(targetEl);
1870
2107
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2108
+ decoratePdfEmbeds(targetEl);
2109
+ await renderPdfPreviewsInElement(targetEl);
1871
2110
  const annotationMode = (pane === "source" || pane === "response")
1872
2111
  ? (annotationsEnabled ? "highlight" : "hide")
1873
2112
  : "none";
@@ -1883,6 +2122,11 @@
1883
2122
  appendPreviewNotice(targetEl, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
1884
2123
  }
1885
2124
  }
2125
+
2126
+ if (pane === "response") {
2127
+ applyPendingResponseScrollReset();
2128
+ scheduleResponsePaneRepaintNudge();
2129
+ }
1886
2130
  } catch (error) {
1887
2131
  if (pane === "source") {
1888
2132
  if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
@@ -1893,6 +2137,10 @@
1893
2137
  const detail = error && error.message ? error.message : String(error || "unknown error");
1894
2138
  finishPreviewRender(targetEl);
1895
2139
  targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
2140
+ if (pane === "response") {
2141
+ applyPendingResponseScrollReset();
2142
+ scheduleResponsePaneRepaintNudge();
2143
+ }
1896
2144
  }
1897
2145
  }
1898
2146
 
@@ -1962,11 +2210,13 @@
1962
2210
  if (!editorText.trim()) {
1963
2211
  finishPreviewRender(critiqueViewEl);
1964
2212
  critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
2213
+ scheduleResponsePaneRepaintNudge();
1965
2214
  return;
1966
2215
  }
1967
2216
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
1968
2217
  finishPreviewRender(critiqueViewEl);
1969
2218
  critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
2219
+ scheduleResponsePaneRepaintNudge();
1970
2220
  return;
1971
2221
  }
1972
2222
  const nonce = ++responsePreviewRenderNonce;
@@ -1981,6 +2231,8 @@
1981
2231
  critiqueViewEl.innerHTML = thinking && thinking.trim()
1982
2232
  ? buildPlainMarkdownHtml(thinking)
1983
2233
  : "<pre class='plain-markdown'>No thinking available for this response.</pre>";
2234
+ applyPendingResponseScrollReset();
2235
+ scheduleResponsePaneRepaintNudge();
1984
2236
  return;
1985
2237
  }
1986
2238
 
@@ -1988,6 +2240,8 @@
1988
2240
  if (!markdown || !markdown.trim()) {
1989
2241
  finishPreviewRender(critiqueViewEl);
1990
2242
  critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
2243
+ applyPendingResponseScrollReset();
2244
+ scheduleResponsePaneRepaintNudge();
1991
2245
  return;
1992
2246
  }
1993
2247
 
@@ -2005,16 +2259,22 @@
2005
2259
  "Response is too large for markdown highlighting. Showing plain markdown.",
2006
2260
  markdown,
2007
2261
  );
2262
+ applyPendingResponseScrollReset();
2263
+ scheduleResponsePaneRepaintNudge();
2008
2264
  return;
2009
2265
  }
2010
2266
 
2011
2267
  finishPreviewRender(critiqueViewEl);
2012
2268
  critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
2269
+ applyPendingResponseScrollReset();
2270
+ scheduleResponsePaneRepaintNudge();
2013
2271
  return;
2014
2272
  }
2015
2273
 
2016
2274
  finishPreviewRender(critiqueViewEl);
2017
2275
  critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
2276
+ applyPendingResponseScrollReset();
2277
+ scheduleResponsePaneRepaintNudge();
2018
2278
  }
2019
2279
 
2020
2280
  function updateResultActionButtons(normalizedEditorText) {
@@ -3058,15 +3318,29 @@
3058
3318
  return lower.indexOf("## critiques") !== -1 && lower.indexOf("## document") !== -1;
3059
3319
  }
3060
3320
 
3061
- function handleIncomingResponse(markdown, kind, timestamp, thinking) {
3321
+ function handleIncomingResponse(markdown, kind, timestamp, thinking, options) {
3062
3322
  const responseTimestamp =
3063
3323
  typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0
3064
3324
  ? timestamp
3065
3325
  : Date.now();
3326
+ const responseThinking = typeof thinking === "string" ? thinking : "";
3327
+ const responseKind = kind === "critique" ? "critique" : "annotation";
3328
+ const resetScroll = options && Object.prototype.hasOwnProperty.call(options, "resetScroll")
3329
+ ? Boolean(options.resetScroll)
3330
+ : (
3331
+ latestResponseKind !== responseKind
3332
+ || latestResponseTimestamp !== responseTimestamp
3333
+ || latestResponseNormalized !== normalizeForCompare(markdown)
3334
+ || latestResponseThinkingNormalized !== normalizeForCompare(responseThinking)
3335
+ );
3336
+
3337
+ if (resetScroll) {
3338
+ pendingResponseScrollReset = true;
3339
+ }
3066
3340
 
3067
3341
  latestResponseMarkdown = markdown;
3068
- latestResponseThinking = typeof thinking === "string" ? thinking : "";
3069
- latestResponseKind = kind === "critique" ? "critique" : "annotation";
3342
+ latestResponseThinking = responseThinking;
3343
+ latestResponseKind = responseKind;
3070
3344
  latestResponseTimestamp = responseTimestamp;
3071
3345
  latestResponseIsStructuredCritique = isStructuredCritique(markdown);
3072
3346
  latestResponseHasContent = Boolean(markdown && markdown.trim());
@@ -3084,10 +3358,10 @@
3084
3358
  refreshResponseUi();
3085
3359
  }
3086
3360
 
3087
- function applyLatestPayload(payload) {
3361
+ function applyLatestPayload(payload, options) {
3088
3362
  if (!payload || typeof payload.markdown !== "string") return false;
3089
3363
  const responseKind = payload.kind === "critique" ? "critique" : "annotation";
3090
- handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking);
3364
+ handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking, options);
3091
3365
  return true;
3092
3366
  }
3093
3367
 
package/client/studio.css CHANGED
@@ -633,6 +633,91 @@
633
633
  color: var(--md-quote);
634
634
  }
635
635
 
636
+ .rendered-markdown .callout-note,
637
+ .rendered-markdown .callout-tip,
638
+ .rendered-markdown .callout-warning,
639
+ .rendered-markdown .callout-important,
640
+ .rendered-markdown .callout-caution {
641
+ margin: 1.15em 0;
642
+ padding: 0.8em 1rem 0.95em;
643
+ border: 1px solid var(--border-muted);
644
+ border-left-width: 4px;
645
+ border-radius: 10px;
646
+ background: var(--panel-2);
647
+ color: inherit;
648
+ }
649
+
650
+ .rendered-markdown .callout-note::before,
651
+ .rendered-markdown .callout-tip::before,
652
+ .rendered-markdown .callout-warning::before,
653
+ .rendered-markdown .callout-important::before,
654
+ .rendered-markdown .callout-caution::before {
655
+ display: inline-block;
656
+ margin-bottom: 0.45rem;
657
+ font-size: 0.76em;
658
+ font-weight: 700;
659
+ letter-spacing: 0.08em;
660
+ text-transform: uppercase;
661
+ }
662
+
663
+ .rendered-markdown .callout-note::before {
664
+ content: "Note";
665
+ color: var(--accent);
666
+ }
667
+
668
+ .rendered-markdown .callout-tip::before {
669
+ content: "Tip";
670
+ color: var(--ok);
671
+ }
672
+
673
+ .rendered-markdown .callout-warning::before {
674
+ content: "Warning";
675
+ color: var(--warn);
676
+ }
677
+
678
+ .rendered-markdown .callout-important::before {
679
+ content: "Important";
680
+ color: var(--error);
681
+ }
682
+
683
+ .rendered-markdown .callout-caution::before {
684
+ content: "Caution";
685
+ color: var(--error);
686
+ }
687
+
688
+ .rendered-markdown .callout-note {
689
+ border-left-color: var(--accent);
690
+ }
691
+
692
+ .rendered-markdown .callout-tip {
693
+ border-left-color: var(--ok);
694
+ }
695
+
696
+ .rendered-markdown .callout-warning {
697
+ border-left-color: var(--warn);
698
+ }
699
+
700
+ .rendered-markdown .callout-important,
701
+ .rendered-markdown .callout-caution {
702
+ border-left-color: var(--error);
703
+ }
704
+
705
+ .rendered-markdown .callout-note > :first-child,
706
+ .rendered-markdown .callout-tip > :first-child,
707
+ .rendered-markdown .callout-warning > :first-child,
708
+ .rendered-markdown .callout-important > :first-child,
709
+ .rendered-markdown .callout-caution > :first-child {
710
+ margin-top: 0;
711
+ }
712
+
713
+ .rendered-markdown .callout-note > :last-child,
714
+ .rendered-markdown .callout-tip > :last-child,
715
+ .rendered-markdown .callout-warning > :last-child,
716
+ .rendered-markdown .callout-important > :last-child,
717
+ .rendered-markdown .callout-caution > :last-child {
718
+ margin-bottom: 0;
719
+ }
720
+
636
721
  .rendered-markdown pre {
637
722
  background: var(--panel-2);
638
723
  border: 1px solid var(--md-codeblock-border);
@@ -765,10 +850,83 @@
765
850
  margin: 1.25em 0;
766
851
  }
767
852
 
853
+ .rendered-markdown .studio-page-break {
854
+ display: flex;
855
+ align-items: center;
856
+ gap: 0.85rem;
857
+ margin: 1.75em 0;
858
+ color: var(--muted);
859
+ font-size: 0.88em;
860
+ text-transform: uppercase;
861
+ letter-spacing: 0.08em;
862
+ }
863
+
864
+ .rendered-markdown .studio-page-break-rule {
865
+ flex: 1 1 auto;
866
+ height: 1px;
867
+ background: var(--md-hr);
868
+ opacity: 0.95;
869
+ }
870
+
871
+ .rendered-markdown .studio-page-break-label {
872
+ flex: 0 0 auto;
873
+ white-space: nowrap;
874
+ }
875
+
768
876
  .rendered-markdown img {
769
877
  max-width: 100%;
770
878
  }
771
879
 
880
+ .rendered-markdown img[data-fig-align="center"],
881
+ .rendered-markdown embed[data-fig-align="center"],
882
+ .rendered-markdown .studio-pdf-preview[data-fig-align="center"] {
883
+ display: block;
884
+ margin-left: auto;
885
+ margin-right: auto;
886
+ }
887
+
888
+ .rendered-markdown img[data-fig-align="right"],
889
+ .rendered-markdown embed[data-fig-align="right"],
890
+ .rendered-markdown .studio-pdf-preview[data-fig-align="right"] {
891
+ display: block;
892
+ margin-left: auto;
893
+ margin-right: 0;
894
+ }
895
+
896
+ .rendered-markdown embed[type="application/pdf"],
897
+ .rendered-markdown embed[src^="data:application/pdf"],
898
+ .rendered-markdown embed[src$=".pdf"] {
899
+ display: block;
900
+ width: 100%;
901
+ min-height: 18rem;
902
+ border: 1px solid var(--md-table-border);
903
+ border-radius: 10px;
904
+ background: #fff;
905
+ overflow: hidden;
906
+ }
907
+
908
+ .rendered-markdown .studio-pdf-preview {
909
+ display: block;
910
+ max-width: 100%;
911
+ border: 1px solid var(--md-table-border);
912
+ border-radius: 10px;
913
+ background: #fff;
914
+ overflow: hidden;
915
+ line-height: 0;
916
+ }
917
+
918
+ .rendered-markdown .studio-pdf-preview[data-fig-align="center"] {
919
+ margin-left: auto;
920
+ margin-right: auto;
921
+ }
922
+
923
+ .rendered-markdown .studio-pdf-preview canvas {
924
+ display: block;
925
+ width: 100%;
926
+ height: auto;
927
+ max-width: 100%;
928
+ }
929
+
772
930
  .rendered-markdown .studio-subfigure-group {
773
931
  margin: 1.25em auto;
774
932
  }
@@ -968,6 +1126,12 @@
968
1126
  opacity: 0.64;
969
1127
  }
970
1128
 
1129
+ .panel-scroll.response-repaint-nudge {
1130
+ outline: 1px solid transparent;
1131
+ -webkit-transform: translateZ(0);
1132
+ transform: translateZ(0);
1133
+ }
1134
+
971
1135
  .preview-error {
972
1136
  color: var(--warn);
973
1137
  margin-bottom: 0.75em;