pi-studio 0.5.30 → 0.5.31

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,12 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.31] — 2026-03-24
8
+
9
+ ### Fixed
10
+ - 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.
11
+ - 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.
12
+
7
13
  ## [0.5.30] — 2026-03-24
8
14
 
9
15
  ### Fixed
@@ -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";
@@ -971,6 +972,7 @@
971
972
  }
972
973
 
973
974
  function clearActiveResponseView() {
975
+ pendingResponseScrollReset = false;
974
976
  latestResponseMarkdown = "";
975
977
  latestResponseThinking = "";
976
978
  latestResponseKind = "annotation";
@@ -1016,13 +1018,13 @@
1016
1018
  }
1017
1019
  }
1018
1020
 
1019
- function applySelectedHistoryItem() {
1021
+ function applySelectedHistoryItem(options) {
1020
1022
  const item = getSelectedHistoryItem();
1021
1023
  if (!item) {
1022
1024
  clearActiveResponseView();
1023
1025
  return false;
1024
1026
  }
1025
- handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking);
1027
+ handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking, options);
1026
1028
  return true;
1027
1029
  }
1028
1030
 
@@ -1035,9 +1037,13 @@
1035
1037
  return false;
1036
1038
  }
1037
1039
 
1040
+ const previousItem = getSelectedHistoryItem();
1041
+ const previousId = previousItem && typeof previousItem.id === "string" ? previousItem.id : null;
1038
1042
  const nextIndex = Math.max(0, Math.min(total - 1, Number(index) || 0));
1039
1043
  responseHistoryIndex = nextIndex;
1040
- const applied = applySelectedHistoryItem();
1044
+ const nextItem = getSelectedHistoryItem();
1045
+ const nextId = nextItem && typeof nextItem.id === "string" ? nextItem.id : null;
1046
+ const applied = applySelectedHistoryItem({ resetScroll: previousId !== nextId });
1041
1047
  updateHistoryControls();
1042
1048
 
1043
1049
  if (applied && !(options && options.silent)) {
@@ -1539,6 +1545,33 @@
1539
1545
  targetEl.classList.remove("preview-pending");
1540
1546
  }
1541
1547
 
1548
+ function scheduleResponsePaneRepaintNudge() {
1549
+ if (!critiqueViewEl || typeof critiqueViewEl.getBoundingClientRect !== "function") return;
1550
+ const schedule = typeof window.requestAnimationFrame === "function"
1551
+ ? window.requestAnimationFrame.bind(window)
1552
+ : (cb) => window.setTimeout(cb, 16);
1553
+
1554
+ schedule(() => {
1555
+ if (!critiqueViewEl || !critiqueViewEl.isConnected) return;
1556
+ void critiqueViewEl.getBoundingClientRect();
1557
+ if (!critiqueViewEl.classList) return;
1558
+ critiqueViewEl.classList.add("response-repaint-nudge");
1559
+ schedule(() => {
1560
+ if (!critiqueViewEl || !critiqueViewEl.classList) return;
1561
+ critiqueViewEl.classList.remove("response-repaint-nudge");
1562
+ });
1563
+ });
1564
+ }
1565
+
1566
+ function applyPendingResponseScrollReset() {
1567
+ if (!pendingResponseScrollReset || !critiqueViewEl) return false;
1568
+ if (rightView === "editor-preview") return false;
1569
+ critiqueViewEl.scrollTop = 0;
1570
+ critiqueViewEl.scrollLeft = 0;
1571
+ pendingResponseScrollReset = false;
1572
+ return true;
1573
+ }
1574
+
1542
1575
  async function getMermaidApi() {
1543
1576
  if (mermaidModulePromise) {
1544
1577
  return mermaidModulePromise;
@@ -1883,6 +1916,11 @@
1883
1916
  appendPreviewNotice(targetEl, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
1884
1917
  }
1885
1918
  }
1919
+
1920
+ if (pane === "response") {
1921
+ applyPendingResponseScrollReset();
1922
+ scheduleResponsePaneRepaintNudge();
1923
+ }
1886
1924
  } catch (error) {
1887
1925
  if (pane === "source") {
1888
1926
  if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
@@ -1893,6 +1931,10 @@
1893
1931
  const detail = error && error.message ? error.message : String(error || "unknown error");
1894
1932
  finishPreviewRender(targetEl);
1895
1933
  targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
1934
+ if (pane === "response") {
1935
+ applyPendingResponseScrollReset();
1936
+ scheduleResponsePaneRepaintNudge();
1937
+ }
1896
1938
  }
1897
1939
  }
1898
1940
 
@@ -1962,11 +2004,13 @@
1962
2004
  if (!editorText.trim()) {
1963
2005
  finishPreviewRender(critiqueViewEl);
1964
2006
  critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
2007
+ scheduleResponsePaneRepaintNudge();
1965
2008
  return;
1966
2009
  }
1967
2010
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
1968
2011
  finishPreviewRender(critiqueViewEl);
1969
2012
  critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
2013
+ scheduleResponsePaneRepaintNudge();
1970
2014
  return;
1971
2015
  }
1972
2016
  const nonce = ++responsePreviewRenderNonce;
@@ -1981,6 +2025,8 @@
1981
2025
  critiqueViewEl.innerHTML = thinking && thinking.trim()
1982
2026
  ? buildPlainMarkdownHtml(thinking)
1983
2027
  : "<pre class='plain-markdown'>No thinking available for this response.</pre>";
2028
+ applyPendingResponseScrollReset();
2029
+ scheduleResponsePaneRepaintNudge();
1984
2030
  return;
1985
2031
  }
1986
2032
 
@@ -1988,6 +2034,8 @@
1988
2034
  if (!markdown || !markdown.trim()) {
1989
2035
  finishPreviewRender(critiqueViewEl);
1990
2036
  critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
2037
+ applyPendingResponseScrollReset();
2038
+ scheduleResponsePaneRepaintNudge();
1991
2039
  return;
1992
2040
  }
1993
2041
 
@@ -2005,16 +2053,22 @@
2005
2053
  "Response is too large for markdown highlighting. Showing plain markdown.",
2006
2054
  markdown,
2007
2055
  );
2056
+ applyPendingResponseScrollReset();
2057
+ scheduleResponsePaneRepaintNudge();
2008
2058
  return;
2009
2059
  }
2010
2060
 
2011
2061
  finishPreviewRender(critiqueViewEl);
2012
2062
  critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
2063
+ applyPendingResponseScrollReset();
2064
+ scheduleResponsePaneRepaintNudge();
2013
2065
  return;
2014
2066
  }
2015
2067
 
2016
2068
  finishPreviewRender(critiqueViewEl);
2017
2069
  critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
2070
+ applyPendingResponseScrollReset();
2071
+ scheduleResponsePaneRepaintNudge();
2018
2072
  }
2019
2073
 
2020
2074
  function updateResultActionButtons(normalizedEditorText) {
@@ -3058,15 +3112,29 @@
3058
3112
  return lower.indexOf("## critiques") !== -1 && lower.indexOf("## document") !== -1;
3059
3113
  }
3060
3114
 
3061
- function handleIncomingResponse(markdown, kind, timestamp, thinking) {
3115
+ function handleIncomingResponse(markdown, kind, timestamp, thinking, options) {
3062
3116
  const responseTimestamp =
3063
3117
  typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0
3064
3118
  ? timestamp
3065
3119
  : Date.now();
3120
+ const responseThinking = typeof thinking === "string" ? thinking : "";
3121
+ const responseKind = kind === "critique" ? "critique" : "annotation";
3122
+ const resetScroll = options && Object.prototype.hasOwnProperty.call(options, "resetScroll")
3123
+ ? Boolean(options.resetScroll)
3124
+ : (
3125
+ latestResponseKind !== responseKind
3126
+ || latestResponseTimestamp !== responseTimestamp
3127
+ || latestResponseNormalized !== normalizeForCompare(markdown)
3128
+ || latestResponseThinkingNormalized !== normalizeForCompare(responseThinking)
3129
+ );
3130
+
3131
+ if (resetScroll) {
3132
+ pendingResponseScrollReset = true;
3133
+ }
3066
3134
 
3067
3135
  latestResponseMarkdown = markdown;
3068
- latestResponseThinking = typeof thinking === "string" ? thinking : "";
3069
- latestResponseKind = kind === "critique" ? "critique" : "annotation";
3136
+ latestResponseThinking = responseThinking;
3137
+ latestResponseKind = responseKind;
3070
3138
  latestResponseTimestamp = responseTimestamp;
3071
3139
  latestResponseIsStructuredCritique = isStructuredCritique(markdown);
3072
3140
  latestResponseHasContent = Boolean(markdown && markdown.trim());
@@ -3084,10 +3152,10 @@
3084
3152
  refreshResponseUi();
3085
3153
  }
3086
3154
 
3087
- function applyLatestPayload(payload) {
3155
+ function applyLatestPayload(payload, options) {
3088
3156
  if (!payload || typeof payload.markdown !== "string") return false;
3089
3157
  const responseKind = payload.kind === "critique" ? "critique" : "annotation";
3090
- handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking);
3158
+ handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking, options);
3091
3159
  return true;
3092
3160
  }
3093
3161
 
package/client/studio.css CHANGED
@@ -968,6 +968,12 @@
968
968
  opacity: 0.64;
969
969
  }
970
970
 
971
+ .panel-scroll.response-repaint-nudge {
972
+ outline: 1px solid transparent;
973
+ -webkit-transform: translateZ(0);
974
+ transform: translateZ(0);
975
+ }
976
+
971
977
  .preview-error {
972
978
  color: var(--warn);
973
979
  margin-bottom: 0.75em;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.30",
3
+ "version": "0.5.31",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",