pi-studio 0.5.59 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.1] — 2026-04-29
8
+
9
+ ### Added
10
+ - Added independent editor and response text-size controls, persisted locally and available in both full Studio and editor-only views.
11
+ - Added optional `pi-studio-dark` and `pi-studio-light` package themes tuned for Studio's browser workspace.
12
+
13
+ ### Changed
14
+ - Improved theme adaptation for Studio surfaces, borders, Markdown/code colours, light/dark detection, and softer active-pane borders across bundled and custom pi themes.
15
+ - Footer model/session/context metadata now keeps context usage visible by truncating longer model/session labels first, with full working-directory details available in hover text.
16
+
17
+ ## [0.6.0] — 2026-04-27
18
+
19
+ ### Added
20
+ - The comments rail now includes **Comments → prompt**, which turns non-empty local comments into an editor prompt with line anchors and file labels when available.
21
+
22
+ ### Changed
23
+ - The refreshed Studio layout is now the default, with the classic layout still available via the footer UI switch or `?uiRefresh=0`.
24
+ - Working-view tool output now replaces image/base64 payloads with compact placeholders instead of dumping raw image data.
25
+
26
+ ### Fixed
27
+ - Queued steering now updates Studio's active effective-prompt metadata so response history and prompt loading reflect the original run plus steering messages.
28
+ - Newly arrived responses now force the right response pane to reset to the top, while editor-preview/document views still preserve scroll.
29
+
7
30
  ## [0.5.59] — 2026-04-27
8
31
 
9
32
  ### Added
package/README.md CHANGED
@@ -35,6 +35,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
35
35
  - strips markers before send (optional)
36
36
  - saves `.annotated.md`
37
37
  - Renders Markdown/LaTeX/code previews (math + Mermaid), theme-synced with pi
38
+ - Ships optional `pi-studio-dark` and `pi-studio-light` themes tuned for Studio's browser workspace
38
39
  - Exports right-pane preview as PDF (pandoc + LaTeX)
39
40
  - Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf`
40
41
  - Shows model/session/context usage in the footer, plus a compact-context action
@@ -77,7 +78,8 @@ pi -e https://github.com/omaclaren/pi-studio
77
78
  - For remote SSH sessions, keep Studio bound to localhost and use SSH local port forwarding; `/studio` prints the localhost URL and an SSH tunnel hint when SSH is detected.
78
79
  - Full Studio is a singleton per Pi session: use `/studio` to open it, `/studio-replace` to explicitly replace it, and `/studio-editor-only` for extra editing/preview tabs that do not take over the full Studio session view.
79
80
  - Studio is designed as a complement to terminal pi, not a replacement.
80
- - Editor/code font uses a best-effort terminal-monospace match when the current terminal config exposes it; set `PI_STUDIO_FONT_MONO` to force a specific CSS `font-family` stack.
81
+ - Installing pi-studio makes the optional `pi-studio-dark` and `pi-studio-light` themes available in pi's theme selector; it does not change your active theme.
82
+ - Editor/code font uses a best-effort terminal-monospace match when the current terminal config exposes it; set `PI_STUDIO_FONT_MONO` to force a specific CSS `font-family` stack. Use `PI_STUDIO_FONT_UI` or `PI_STUDIO_FONT_PROSE` to override the Studio UI or rendered-preview font stacks.
81
83
  - Full preview/PDF quality depends on `pandoc` (and `xelatex` for PDF):
82
84
  - `brew install pandoc`
83
85
  - install TeX Live/MacTeX for PDF export
@@ -4,6 +4,9 @@
4
4
  const statusSpinnerEl = document.getElementById("statusSpinner");
5
5
  const footerMetaEl = document.getElementById("footerMeta");
6
6
  const footerMetaTextEl = document.getElementById("footerMetaText");
7
+ const footerMetaModelEl = document.getElementById("footerMetaModel");
8
+ const footerMetaTerminalEl = document.getElementById("footerMetaTerminal");
9
+ const footerMetaContextEl = document.getElementById("footerMetaContext");
7
10
  let faviconLinkEl = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
8
11
  if (!faviconLinkEl) {
9
12
  faviconLinkEl = document.createElement("link");
@@ -70,6 +73,7 @@
70
73
  const rightViewSelect = document.getElementById("rightViewSelect");
71
74
  const followSelect = document.getElementById("followSelect");
72
75
  const responseHighlightSelect = document.getElementById("responseHighlightSelect");
76
+ const responseFontSizeSelect = document.getElementById("responseFontSizeSelect");
73
77
  const pullLatestBtn = document.getElementById("pullLatestBtn");
74
78
  const insertHeaderBtn = document.getElementById("insertHeaderBtn");
75
79
  const critiqueBtn = document.getElementById("critiqueBtn");
@@ -103,6 +107,7 @@
103
107
  const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
104
108
  const highlightSelect = document.getElementById("highlightSelect");
105
109
  const lineNumbersSelect = document.getElementById("lineNumbersSelect");
110
+ const editorFontSizeSelect = document.getElementById("editorFontSizeSelect");
106
111
  const annotationModeSelect = document.getElementById("annotationModeSelect");
107
112
  const compactBtn = document.getElementById("compactBtn");
108
113
  const leftFocusBtn = document.getElementById("leftFocusBtn");
@@ -132,6 +137,7 @@
132
137
  const reviewNotesListEl = document.getElementById("reviewNotesList");
133
138
  const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
134
139
  const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
140
+ const reviewNotesPromptBtn = document.getElementById("reviewNotesPromptBtn");
135
141
  const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
136
142
  const reviewNotesDeleteAllBtn = document.getElementById("reviewNotesDeleteAllBtn");
137
143
  const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
@@ -200,6 +206,7 @@
200
206
  let compactInProgress = false;
201
207
  let modelLabel = (document.body && document.body.dataset && document.body.dataset.modelLabel) || "none";
202
208
  let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
209
+ let terminalSessionDetail = (document.body && document.body.dataset && document.body.dataset.terminalDetail) || terminalSessionLabel;
203
210
  let contextTokens = null;
204
211
  let contextWindow = null;
205
212
  let contextPercent = null;
@@ -587,6 +594,7 @@
587
594
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
588
595
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
589
596
  const EDITOR_LINE_NUMBERS_STORAGE_KEY = "piStudio.editorLineNumbersEnabled";
597
+ const EDITOR_FONT_SIZE_STORAGE_KEY = "piStudio.editorFontSize";
590
598
  // Single source of truth: language -> file extensions (and display label)
591
599
  var LANG_EXT_MAP = {
592
600
  markdown: { label: "Markdown", exts: ["md", "markdown", "mdx", "qmd"] },
@@ -627,6 +635,7 @@
627
635
  var SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
628
636
  const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
629
637
  const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
638
+ const RESPONSE_FONT_SIZE_STORAGE_KEY = "piStudio.responseFontSize";
630
639
  const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
631
640
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
632
641
  const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
@@ -646,6 +655,12 @@
646
655
  let annotationsEnabled = true;
647
656
  const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
648
657
  const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
658
+ const EDITOR_FONT_SIZE_OPTIONS = [10, 11, 12, 13, 14, 15, 16, 18];
659
+ const RESPONSE_FONT_SIZE_OPTIONS = [11, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 18, 20];
660
+ const DEFAULT_EDITOR_FONT_SIZE = studioUiRefreshEnabled ? 12 : 13;
661
+ const DEFAULT_RESPONSE_FONT_SIZE = studioUiRefreshEnabled ? 13.5 : 15;
662
+ let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
663
+ let responseFontSize = DEFAULT_RESPONSE_FONT_SIZE;
649
664
  let studioUiRefreshUi = null;
650
665
  if (studioUiRefreshEnabled && document.body) {
651
666
  document.body.classList.add("studio-ui-refresh");
@@ -674,21 +689,21 @@
674
689
  const queryValue = initialQueryParams.has("uiRefresh")
675
690
  ? initialQueryParams.get("uiRefresh")
676
691
  : (initialQueryParams.has("studioUiRefresh") ? initialQueryParams.get("studioUiRefresh") : null);
677
- const isTruthy = (value) => ["1", "true", "yes", "on", "v2", "refresh"].indexOf(normalize(value)) !== -1;
678
- const isFalsey = (value) => ["0", "false", "no", "off"].indexOf(normalize(value)) !== -1;
692
+ const isTruthy = (value) => ["1", "true", "yes", "on", "v2", "refresh", "fresh"].indexOf(normalize(value)) !== -1;
693
+ const isFalsey = (value) => ["0", "false", "no", "off", "classic"].indexOf(normalize(value)) !== -1;
679
694
  if (queryValue !== null) {
680
- const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalize(queryValue) !== "");
695
+ const normalizedQuery = normalize(queryValue);
696
+ const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
681
697
  try {
682
- if (enabled) window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, "1");
683
- else window.localStorage && window.localStorage.removeItem(STUDIO_UI_REFRESH_STORAGE_KEY);
698
+ window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
684
699
  } catch {}
685
700
  return enabled;
686
701
  }
687
702
  try {
688
- return Boolean(window.localStorage && window.localStorage.getItem(STUDIO_UI_REFRESH_STORAGE_KEY) === "1");
689
- } catch {
690
- return false;
691
- }
703
+ const stored = window.localStorage ? window.localStorage.getItem(STUDIO_UI_REFRESH_STORAGE_KEY) : null;
704
+ if (stored !== null) return stored !== "0" && !isFalsey(stored);
705
+ } catch {}
706
+ return true;
692
707
  }
693
708
 
694
709
  function makeStudioUiRefreshElement(tagName, className, text) {
@@ -751,6 +766,74 @@
751
766
  buttonEl.textContent = text;
752
767
  }
753
768
 
769
+ function formatStudioFontSizeLabel(size) {
770
+ const value = Number(size);
771
+ if (!Number.isFinite(value)) return "";
772
+ return String(value).replace(/\.0$/, "") + "px";
773
+ }
774
+
775
+ function normalizeStudioFontSize(value, options, fallback) {
776
+ const parsed = Number(value);
777
+ if (!Number.isFinite(parsed)) return fallback;
778
+ for (const option of options) {
779
+ if (Math.abs(option - parsed) < 0.001) return option;
780
+ }
781
+ return fallback;
782
+ }
783
+
784
+ function readStoredFontSize(storageKey, options, fallback) {
785
+ try {
786
+ if (!window.localStorage) return fallback;
787
+ return normalizeStudioFontSize(window.localStorage.getItem(storageKey), options, fallback);
788
+ } catch {
789
+ return fallback;
790
+ }
791
+ }
792
+
793
+ function persistStoredFontSize(storageKey, size) {
794
+ try {
795
+ if (window.localStorage) window.localStorage.setItem(storageKey, String(size));
796
+ } catch {
797
+ // ignore storage failures
798
+ }
799
+ }
800
+
801
+ function syncFontSizeSelect(selectEl, size) {
802
+ if (!selectEl) return;
803
+ selectEl.value = String(size);
804
+ }
805
+
806
+ function applyStudioFontSizeVariables() {
807
+ if (!document.body || !document.body.style) return;
808
+ const editorLineNumberSize = Math.max(10, editorFontSize - 1);
809
+ const responseRawSize = Math.max(11, responseFontSize - 1.5);
810
+ document.body.style.setProperty("--studio-editor-font-size", formatStudioFontSizeLabel(editorFontSize));
811
+ document.body.style.setProperty("--studio-editor-line-number-font-size", formatStudioFontSizeLabel(editorLineNumberSize));
812
+ document.body.style.setProperty("--studio-response-font-size", formatStudioFontSizeLabel(responseFontSize));
813
+ document.body.style.setProperty("--studio-response-raw-font-size", formatStudioFontSizeLabel(responseRawSize));
814
+ document.body.style.setProperty("--studio-working-font-size", formatStudioFontSizeLabel(responseRawSize));
815
+ }
816
+
817
+ function setEditorFontSize(size, options) {
818
+ editorFontSize = normalizeStudioFontSize(size, EDITOR_FONT_SIZE_OPTIONS, DEFAULT_EDITOR_FONT_SIZE);
819
+ if (!options || options.persist !== false) persistStoredFontSize(EDITOR_FONT_SIZE_STORAGE_KEY, editorFontSize);
820
+ syncFontSizeSelect(editorFontSizeSelect, editorFontSize);
821
+ applyStudioFontSizeVariables();
822
+ syncStudioUiRefreshSummaries();
823
+ scheduleEditorLineNumberRender();
824
+ if (editorHighlightEnabled && editorView === "markdown") {
825
+ scheduleEditorHighlightRender();
826
+ }
827
+ }
828
+
829
+ function setResponseFontSize(size, options) {
830
+ responseFontSize = normalizeStudioFontSize(size, RESPONSE_FONT_SIZE_OPTIONS, DEFAULT_RESPONSE_FONT_SIZE);
831
+ if (!options || options.persist !== false) persistStoredFontSize(RESPONSE_FONT_SIZE_STORAGE_KEY, responseFontSize);
832
+ syncFontSizeSelect(responseFontSizeSelect, responseFontSize);
833
+ applyStudioFontSizeVariables();
834
+ scheduleResponsePaneRepaintNudge();
835
+ }
836
+
754
837
  function getStudioUiRefreshAnnotationHeaderEnabled() {
755
838
  try {
756
839
  return Boolean(stripAnnotationHeader(sourceTextEl.value).hadHeader);
@@ -775,7 +858,8 @@
775
858
  ? (getStudioUiRefreshSelectSummary(highlightSelect, "Syntax highlight") || editorLanguage || "Markdown")
776
859
  : "Off";
777
860
  const lineLabel = lineNumbersEnabled ? "Lines on" : "Lines off";
778
- setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel);
861
+ const editorSizeLabel = formatStudioFontSizeLabel(editorFontSize);
862
+ setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel + " · " + editorSizeLabel);
779
863
  }
780
864
  syncStudioUiRefreshReviewTrigger();
781
865
  }
@@ -841,8 +925,7 @@
841
925
 
842
926
  function setStudioUiRefreshPreference(enabled) {
843
927
  try {
844
- if (enabled) window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, "1");
845
- else window.localStorage && window.localStorage.removeItem(STUDIO_UI_REFRESH_STORAGE_KEY);
928
+ window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
846
929
  } catch {}
847
930
  try {
848
931
  const url = new URL(window.location.href);
@@ -855,7 +938,7 @@
855
938
 
856
939
  function setupStudioUiRefreshToggleButton() {
857
940
  if (!footerMetaEl || document.getElementById("studioUiRefreshToggleBtn")) return;
858
- const button = makeStudioUiRefreshElement("button", "footer-compact-btn studio-ui-refresh-toggle", studioUiRefreshEnabled ? "UI: Refresh" : "UI: Classic");
941
+ const button = makeStudioUiRefreshElement("button", "footer-compact-btn studio-ui-refresh-toggle", studioUiRefreshEnabled ? "UI: Fresh" : "UI: Classic");
859
942
  button.id = "studioUiRefreshToggleBtn";
860
943
  button.type = "button";
861
944
  button.title = studioUiRefreshEnabled
@@ -957,7 +1040,7 @@
957
1040
  appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Actions", [stripAnnotationsBtn, saveAnnotatedBtn]);
958
1041
  const viewButton = makeStudioUiRefreshElement("button", "", "View");
959
1042
  const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
960
- appendStudioUiRefreshMenuSection(viewMenu.menu, "Display", [highlightSelect, lineNumbersSelect]);
1043
+ appendStudioUiRefreshMenuSection(viewMenu.menu, "Display", [highlightSelect, lineNumbersSelect, editorFontSizeSelect]);
961
1044
  stateEl.appendChild(annotationsMenu.anchor);
962
1045
  stateEl.appendChild(viewMenu.anchor);
963
1046
 
@@ -1265,7 +1348,24 @@
1265
1348
  }
1266
1349
  }
1267
1350
 
1268
- function formatContextUsageText() {
1351
+ function formatCompactNumber(value) {
1352
+ if (typeof value !== "number" || !Number.isFinite(value)) return "?";
1353
+ const sign = value < 0 ? "-" : "";
1354
+ const abs = Math.abs(value);
1355
+ if (abs < 1000) return sign + formatNumber(abs);
1356
+ const units = [
1357
+ { divisor: 1_000_000_000, suffix: "B" },
1358
+ { divisor: 1_000_000, suffix: "M" },
1359
+ { divisor: 1_000, suffix: "k" },
1360
+ ];
1361
+ const unit = units.find((entry) => abs >= entry.divisor) || units[units.length - 1];
1362
+ const scaled = abs / unit.divisor;
1363
+ const decimals = scaled >= 100 ? 0 : 1;
1364
+ return sign + scaled.toFixed(decimals).replace(/\.0$/, "") + unit.suffix;
1365
+ }
1366
+
1367
+ function formatContextUsageText(compact) {
1368
+ const formatContextNumber = compact ? formatCompactNumber : formatNumber;
1269
1369
  const hasWindow = typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0;
1270
1370
  const hasTokens = typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens >= 0;
1271
1371
  let percentValue = typeof contextPercent === "number" && Number.isFinite(contextPercent)
@@ -1280,12 +1380,12 @@
1280
1380
  return "Context: unknown";
1281
1381
  }
1282
1382
  if (!hasTokens && hasWindow) {
1283
- return "Context: ? / " + formatNumber(contextWindow);
1383
+ return "Context: ? / " + formatContextNumber(contextWindow);
1284
1384
  }
1285
1385
 
1286
- let text = "Context: " + formatNumber(contextTokens);
1386
+ let text = "Context: " + formatContextNumber(contextTokens);
1287
1387
  if (hasWindow) {
1288
- text += " / " + formatNumber(contextWindow);
1388
+ text += " / " + formatContextNumber(contextWindow);
1289
1389
  }
1290
1390
  if (percentValue != null && Number.isFinite(percentValue)) {
1291
1391
  const bounded = Math.max(0, Math.min(100, percentValue));
@@ -1515,14 +1615,27 @@
1515
1615
  function updateFooterMeta() {
1516
1616
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
1517
1617
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
1518
- const contextText = formatContextUsageText();
1519
- const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
1520
- if (footerMetaTextEl) {
1618
+ const terminalDetailText = terminalSessionDetail && terminalSessionDetail.trim() ? terminalSessionDetail.trim() : terminalText;
1619
+ const contextText = formatContextUsageText(true);
1620
+ const contextTitleText = formatContextUsageText(false);
1621
+ const contextDisplayText = contextText.replace(/^Context:\s*/i, "");
1622
+ const text = modelText + " · " + terminalText + " · " + contextDisplayText;
1623
+ const titleText = "Model: " + modelText + " · " + terminalDetailText + " · " + contextTitleText;
1624
+ if (footerMetaModelEl && footerMetaTerminalEl && footerMetaContextEl) {
1625
+ footerMetaModelEl.textContent = modelText;
1626
+ footerMetaTerminalEl.textContent = terminalText;
1627
+ footerMetaContextEl.textContent = contextDisplayText;
1628
+ footerMetaModelEl.title = "Model: " + modelText;
1629
+ footerMetaTerminalEl.title = terminalDetailText;
1630
+ footerMetaContextEl.title = contextTitleText;
1631
+ if (footerMetaTextEl) footerMetaTextEl.title = titleText;
1632
+ if (footerMetaEl) footerMetaEl.title = titleText;
1633
+ } else if (footerMetaTextEl) {
1521
1634
  footerMetaTextEl.textContent = text;
1522
- footerMetaTextEl.title = text;
1635
+ footerMetaTextEl.title = titleText;
1523
1636
  } else if (footerMetaEl) {
1524
1637
  footerMetaEl.textContent = text;
1525
- footerMetaEl.title = text;
1638
+ footerMetaEl.title = titleText;
1526
1639
  }
1527
1640
  updateDocumentTitle();
1528
1641
  }
@@ -5750,6 +5863,87 @@
5750
5863
  };
5751
5864
  }
5752
5865
 
5866
+ function getDiffFileLabelForLine(source, lineNumber) {
5867
+ const lines = String(source || "").replace(/\r\n/g, "\n").split("\n");
5868
+ const safeLine = Math.max(1, Math.min(Math.floor(Number(lineNumber) || 1), Math.max(1, lines.length)));
5869
+ let currentFile = "";
5870
+ for (let i = 0; i < safeLine; i += 1) {
5871
+ const line = String(lines[i] || "");
5872
+ const diffMatch = line.match(/^diff --git\s+a\/(.+?)\s+b\/(.+?)\s*$/);
5873
+ if (diffMatch) {
5874
+ currentFile = diffMatch[2] || diffMatch[1] || currentFile;
5875
+ continue;
5876
+ }
5877
+ const plusMatch = line.match(/^\+\+\+\s+(?:b\/)?(.+)\s*$/);
5878
+ if (plusMatch && plusMatch[1] && plusMatch[1] !== "/dev/null") {
5879
+ currentFile = plusMatch[1];
5880
+ }
5881
+ }
5882
+ return currentFile.trim();
5883
+ }
5884
+
5885
+ function getReviewNotePromptFileLabel(note, source) {
5886
+ if (sourceState && sourceState.path) return String(sourceState.path);
5887
+ const bounds = getResolvedReviewNoteLineBounds(note, source);
5888
+ const diffFile = bounds ? getDiffFileLabelForLine(source, bounds.lineStart) : "";
5889
+ if (diffFile) return diffFile;
5890
+ const descriptor = getCurrentStudioDocumentDescriptor();
5891
+ return descriptor && descriptor.fileBacked ? descriptor.label : "";
5892
+ }
5893
+
5894
+ function formatReviewNotePromptLineRange(bounds, note) {
5895
+ const start = bounds ? bounds.lineStart : Math.max(1, Number(note && note.lineStart) || 1);
5896
+ const end = bounds ? bounds.lineEnd : Math.max(start, Number(note && note.lineEnd) || start);
5897
+ return start === end ? "L" + start : ("L" + start + "-L" + end);
5898
+ }
5899
+
5900
+ function buildReviewNotesPrompt() {
5901
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5902
+ const notes = getDisplayReviewNotes().filter((note) => String(note && note.text ? note.text : "").trim());
5903
+ if (!notes.length) return "";
5904
+
5905
+ const descriptor = getCurrentStudioDocumentDescriptor();
5906
+ const documentLabel = descriptor && descriptor.label ? descriptor.label : (sourceState && sourceState.label ? sourceState.label : "Studio document");
5907
+ const parts = [
5908
+ "Please address the following Studio comments. Use the file names and line numbers as anchors. The full document is not included here, only the comments and their anchors.",
5909
+ "Document: " + documentLabel,
5910
+ "",
5911
+ "## Comments",
5912
+ ];
5913
+
5914
+ notes.forEach((note, index) => {
5915
+ const bounds = getResolvedReviewNoteLineBounds(note, source);
5916
+ const fileLabel = getReviewNotePromptFileLabel(note, source);
5917
+ const location = (fileLabel ? (fileLabel + ":") : "") + formatReviewNotePromptLineRange(bounds, note);
5918
+ const comment = String(note && note.text ? note.text : "").trim();
5919
+ const anchor = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
5920
+ .replace(/\s+/g, " ")
5921
+ .trim();
5922
+ parts.push(
5923
+ "### Comment " + (index + 1) + " — " + location,
5924
+ "",
5925
+ comment,
5926
+ );
5927
+ if (anchor) {
5928
+ parts.push("", "> " + anchor.replace(/\n/g, "\n> "));
5929
+ }
5930
+ parts.push("");
5931
+ });
5932
+
5933
+ return parts.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
5934
+ }
5935
+
5936
+ function loadReviewNotesPromptIntoEditor() {
5937
+ const prompt = buildReviewNotesPrompt();
5938
+ if (!prompt.trim()) {
5939
+ setStatus("No non-empty comments to load as a prompt.", "warning");
5940
+ return;
5941
+ }
5942
+ setEditorText(prompt, { preserveScroll: false, preserveSelection: false });
5943
+ setSourceState({ source: "blank", label: "comments prompt", path: null });
5944
+ setStatus("Loaded comments prompt into editor.", "success");
5945
+ }
5946
+
5753
5947
  function buildReviewNoteLineMap(text) {
5754
5948
  const source = String(text || "");
5755
5949
  const lineMap = new Map();
@@ -8573,12 +8767,19 @@
8573
8767
  ? "Select preview text and use Comment for a local preview-anchored comment."
8574
8768
  : "Switch to Editor (Raw) to comment on the current line.");
8575
8769
  }
8770
+ if (reviewNotesPromptBtn) {
8771
+ const promptCandidates = reviewNotes.filter((note) => String(note && note.text ? note.text : "").trim());
8772
+ reviewNotesPromptBtn.disabled = uiBusy || promptCandidates.length === 0;
8773
+ reviewNotesPromptBtn.title = promptCandidates.length > 0
8774
+ ? "Load local comments, line numbers, and file labels into the editor as a prompt."
8775
+ : "No non-empty local comments to load as a prompt.";
8776
+ }
8576
8777
  if (reviewNotesInlineAllBtn) {
8577
8778
  const currentText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
8578
8779
  const toggleCandidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, currentText).canToggle);
8579
8780
  const allInline = toggleCandidates.length > 0 && toggleCandidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
8580
8781
  reviewNotesInlineAllBtn.disabled = uiBusy || toggleCandidates.length === 0;
8581
- reviewNotesInlineAllBtn.textContent = allInline ? "All inline: On" : "All inline: Off";
8782
+ reviewNotesInlineAllBtn.textContent = allInline ? "Inline: On" : "Inline: Off";
8582
8783
  reviewNotesInlineAllBtn.setAttribute("aria-pressed", allInline ? "true" : "false");
8583
8784
  reviewNotesInlineAllBtn.title = allInline
8584
8785
  ? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
@@ -9513,6 +9714,9 @@
9513
9714
  if (typeof message.terminalSessionLabel === "string") {
9514
9715
  terminalSessionLabel = message.terminalSessionLabel;
9515
9716
  }
9717
+ if (typeof message.terminalSessionDetail === "string") {
9718
+ terminalSessionDetail = message.terminalSessionDetail;
9719
+ }
9516
9720
  applyStudioRunQueueStateFromMessage(message);
9517
9721
  updateFooterMeta();
9518
9722
  setBusy(busy);
@@ -9705,6 +9909,7 @@
9705
9909
  setBusy(false);
9706
9910
  setWsState("Ready");
9707
9911
 
9912
+ pendingResponseScrollReset = true;
9708
9913
  let appliedFromHistory = false;
9709
9914
  if (Array.isArray(message.responseHistory)) {
9710
9915
  appliedFromHistory = setResponseHistory(message.responseHistory, {
@@ -9733,6 +9938,9 @@
9733
9938
  if (pendingRequestId) return;
9734
9939
 
9735
9940
  const hasHistory = Array.isArray(message.responseHistory);
9941
+ if (followLatest) {
9942
+ pendingResponseScrollReset = true;
9943
+ }
9736
9944
  if (hasHistory) {
9737
9945
  setResponseHistory(message.responseHistory, {
9738
9946
  autoSelectLatest: followLatest,
@@ -9756,7 +9964,7 @@
9756
9964
  return;
9757
9965
  }
9758
9966
 
9759
- if (!hasHistory && applyLatestPayload(payload)) {
9967
+ if (!hasHistory && applyLatestPayload(payload, { resetScroll: true })) {
9760
9968
  queuedLatestResponse = null;
9761
9969
  updateResultActionButtons();
9762
9970
  setStatus("Updated from latest response.", "success");
@@ -9917,6 +10125,9 @@
9917
10125
  if (typeof message.terminalSessionLabel === "string") {
9918
10126
  terminalSessionLabel = message.terminalSessionLabel;
9919
10127
  }
10128
+ if (typeof message.terminalSessionDetail === "string") {
10129
+ terminalSessionDetail = message.terminalSessionDetail;
10130
+ }
9920
10131
  applyStudioRunQueueStateFromMessage(message);
9921
10132
  updateFooterMeta();
9922
10133
 
@@ -10366,6 +10577,18 @@
10366
10577
  });
10367
10578
  }
10368
10579
 
10580
+ if (editorFontSizeSelect) {
10581
+ editorFontSizeSelect.addEventListener("change", () => {
10582
+ setEditorFontSize(editorFontSizeSelect.value);
10583
+ });
10584
+ }
10585
+
10586
+ if (responseFontSizeSelect) {
10587
+ responseFontSizeSelect.addEventListener("change", () => {
10588
+ setResponseFontSize(responseFontSizeSelect.value);
10589
+ });
10590
+ }
10591
+
10369
10592
  if (lineNumbersSelect) {
10370
10593
  lineNumbersSelect.addEventListener("change", () => {
10371
10594
  setLineNumbersEnabled(lineNumbersSelect.value === "on");
@@ -10926,6 +11149,12 @@
10926
11149
  });
10927
11150
  }
10928
11151
 
11152
+ if (reviewNotesPromptBtn) {
11153
+ reviewNotesPromptBtn.addEventListener("click", () => {
11154
+ loadReviewNotesPromptIntoEditor();
11155
+ });
11156
+ }
11157
+
10929
11158
  if (reviewNotesInlineAllBtn) {
10930
11159
  reviewNotesInlineAllBtn.addEventListener("click", () => {
10931
11160
  toggleAllReviewNotesInlineAnnotations();
@@ -11219,6 +11448,12 @@
11219
11448
  editorResizeObserver.observe(sourceEditorWrapEl);
11220
11449
  }
11221
11450
 
11451
+ const initialEditorFontSize = readStoredFontSize(EDITOR_FONT_SIZE_STORAGE_KEY, EDITOR_FONT_SIZE_OPTIONS, DEFAULT_EDITOR_FONT_SIZE);
11452
+ setEditorFontSize(initialEditorFontSize, { persist: false });
11453
+
11454
+ const initialResponseFontSize = readStoredFontSize(RESPONSE_FONT_SIZE_STORAGE_KEY, RESPONSE_FONT_SIZE_OPTIONS, DEFAULT_RESPONSE_FONT_SIZE);
11455
+ setResponseFontSize(initialResponseFontSize, { persist: false });
11456
+
11222
11457
  setSourceState(initialSourceState);
11223
11458
  refreshResponseUi();
11224
11459
  updateAnnotatedReplyHeaderButton();