pi-studio 0.6.0 → 0.6.2

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,22 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.2] — 2026-04-29
8
+
9
+ ### Changed
10
+ - Polished refreshed Studio theme surfaces so light-mode header actions, scratchpad panels, response-history badges, and editor surfaces are more consistent.
11
+ - Softened overly sharp derived borders in high-contrast themes while preserving theme hue and tightened secondary info text contrast in comment, outline, and scratchpad panels.
12
+
13
+ ## [0.6.1] — 2026-04-29
14
+
15
+ ### Added
16
+ - Added independent editor and response text-size controls, persisted locally and available in both full Studio and editor-only views.
17
+ - Added optional `pi-studio-dark` and `pi-studio-light` package themes tuned for Studio's browser workspace.
18
+
19
+ ### Changed
20
+ - Improved theme adaptation for Studio surfaces, borders, Markdown/code colours, light/dark detection, and softer active-pane borders across bundled and custom pi themes.
21
+ - 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.
22
+
7
23
  ## [0.6.0] — 2026-04-27
8
24
 
9
25
  ### 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");
@@ -201,6 +206,7 @@
201
206
  let compactInProgress = false;
202
207
  let modelLabel = (document.body && document.body.dataset && document.body.dataset.modelLabel) || "none";
203
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;
204
210
  let contextTokens = null;
205
211
  let contextWindow = null;
206
212
  let contextPercent = null;
@@ -588,6 +594,7 @@
588
594
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
589
595
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
590
596
  const EDITOR_LINE_NUMBERS_STORAGE_KEY = "piStudio.editorLineNumbersEnabled";
597
+ const EDITOR_FONT_SIZE_STORAGE_KEY = "piStudio.editorFontSize";
591
598
  // Single source of truth: language -> file extensions (and display label)
592
599
  var LANG_EXT_MAP = {
593
600
  markdown: { label: "Markdown", exts: ["md", "markdown", "mdx", "qmd"] },
@@ -628,6 +635,7 @@
628
635
  var SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
629
636
  const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
630
637
  const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
638
+ const RESPONSE_FONT_SIZE_STORAGE_KEY = "piStudio.responseFontSize";
631
639
  const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
632
640
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
633
641
  const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
@@ -647,6 +655,12 @@
647
655
  let annotationsEnabled = true;
648
656
  const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
649
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;
650
664
  let studioUiRefreshUi = null;
651
665
  if (studioUiRefreshEnabled && document.body) {
652
666
  document.body.classList.add("studio-ui-refresh");
@@ -752,6 +766,74 @@
752
766
  buttonEl.textContent = text;
753
767
  }
754
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
+
755
837
  function getStudioUiRefreshAnnotationHeaderEnabled() {
756
838
  try {
757
839
  return Boolean(stripAnnotationHeader(sourceTextEl.value).hadHeader);
@@ -776,7 +858,8 @@
776
858
  ? (getStudioUiRefreshSelectSummary(highlightSelect, "Syntax highlight") || editorLanguage || "Markdown")
777
859
  : "Off";
778
860
  const lineLabel = lineNumbersEnabled ? "Lines on" : "Lines off";
779
- setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel);
861
+ const editorSizeLabel = formatStudioFontSizeLabel(editorFontSize);
862
+ setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel + " · " + editorSizeLabel);
780
863
  }
781
864
  syncStudioUiRefreshReviewTrigger();
782
865
  }
@@ -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
 
@@ -1532,11 +1615,22 @@
1532
1615
  function updateFooterMeta() {
1533
1616
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
1534
1617
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
1618
+ const terminalDetailText = terminalSessionDetail && terminalSessionDetail.trim() ? terminalSessionDetail.trim() : terminalText;
1535
1619
  const contextText = formatContextUsageText(true);
1536
1620
  const contextTitleText = formatContextUsageText(false);
1537
- const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
1538
- const titleText = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextTitleText;
1539
- if (footerMetaTextEl) {
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) {
1540
1634
  footerMetaTextEl.textContent = text;
1541
1635
  footerMetaTextEl.title = titleText;
1542
1636
  } else if (footerMetaEl) {
@@ -9620,6 +9714,9 @@
9620
9714
  if (typeof message.terminalSessionLabel === "string") {
9621
9715
  terminalSessionLabel = message.terminalSessionLabel;
9622
9716
  }
9717
+ if (typeof message.terminalSessionDetail === "string") {
9718
+ terminalSessionDetail = message.terminalSessionDetail;
9719
+ }
9623
9720
  applyStudioRunQueueStateFromMessage(message);
9624
9721
  updateFooterMeta();
9625
9722
  setBusy(busy);
@@ -10028,6 +10125,9 @@
10028
10125
  if (typeof message.terminalSessionLabel === "string") {
10029
10126
  terminalSessionLabel = message.terminalSessionLabel;
10030
10127
  }
10128
+ if (typeof message.terminalSessionDetail === "string") {
10129
+ terminalSessionDetail = message.terminalSessionDetail;
10130
+ }
10031
10131
  applyStudioRunQueueStateFromMessage(message);
10032
10132
  updateFooterMeta();
10033
10133
 
@@ -10477,6 +10577,18 @@
10477
10577
  });
10478
10578
  }
10479
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
+
10480
10592
  if (lineNumbersSelect) {
10481
10593
  lineNumbersSelect.addEventListener("change", () => {
10482
10594
  setLineNumbersEnabled(lineNumbersSelect.value === "on");
@@ -11336,6 +11448,12 @@
11336
11448
  editorResizeObserver.observe(sourceEditorWrapEl);
11337
11449
  }
11338
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
+
11339
11457
  setSourceState(initialSourceState);
11340
11458
  refreshResponseUi();
11341
11459
  updateAnnotatedReplyHeaderButton();