pi-studio 0.5.43 → 0.5.44

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,14 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.44] — 2026-04-03
8
+
9
+ ### Changed
10
+ - Studio browser tabs now show live browser-side activity state during Studio-owned work, including prefixes like `Running…`, `Responding…`, `Critiquing…`, or `Compacting…`, instead of only changing on completion.
11
+ - Studio favicons now use a clearer browser-side status badge: ready/completed states use a green circular badge, while active running states use an amber hollow ring so busy vs finished is easier to distinguish at a glance without relying on animation.
12
+ - Studio raw editor now has an optional line-number gutter, intended as a lightweight navigation aid when working in **Editor (Raw)** before switching back to preview.
13
+ - Studio editor controls now use a single combined **Syntax highlight** selector with `Off` plus all supported languages, replacing the separate highlight on/off and language selectors.
14
+
7
15
  ## [0.5.43] — 2026-04-01
8
16
 
9
17
  ### Changed
@@ -4,6 +4,12 @@
4
4
  const statusSpinnerEl = document.getElementById("statusSpinner");
5
5
  const footerMetaEl = document.getElementById("footerMeta");
6
6
  const footerMetaTextEl = document.getElementById("footerMetaText");
7
+ let faviconLinkEl = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
8
+ if (!faviconLinkEl) {
9
+ faviconLinkEl = document.createElement("link");
10
+ faviconLinkEl.rel = "icon";
11
+ document.head.appendChild(faviconLinkEl);
12
+ }
7
13
  const BRAILLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
8
14
  let spinnerTimer = null;
9
15
  let spinnerFrameIndex = 0;
@@ -41,6 +47,9 @@
41
47
  const sourceEditorWrapEl = document.getElementById("sourceEditorWrap");
42
48
  const sourceTextEl = document.getElementById("sourceText");
43
49
  const sourceHighlightEl = document.getElementById("sourceHighlight");
50
+ const lineNumberGutterEl = document.getElementById("lineNumberGutter");
51
+ const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
52
+ const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
44
53
  const sourcePreviewEl = document.getElementById("sourcePreview");
45
54
  const leftPaneEl = document.getElementById("leftPane");
46
55
  const rightPaneEl = document.getElementById("rightPane");
@@ -84,7 +93,7 @@
84
93
  const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
85
94
  const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
86
95
  const highlightSelect = document.getElementById("highlightSelect");
87
- const langSelect = document.getElementById("langSelect");
96
+ const lineNumbersSelect = document.getElementById("lineNumbersSelect");
88
97
  const annotationModeSelect = document.getElementById("annotationModeSelect");
89
98
  const compactBtn = document.getElementById("compactBtn");
90
99
  const leftFocusBtn = document.getElementById("leftFocusBtn");
@@ -158,6 +167,7 @@
158
167
  let titleAttentionMessage = "";
159
168
  let titleAttentionRequestId = null;
160
169
  let titleAttentionRequestKind = null;
170
+ let lastRenderedFaviconHref = "";
161
171
 
162
172
  function parseFiniteNumber(value) {
163
173
  if (value == null || value === "") return null;
@@ -203,6 +213,7 @@
203
213
  const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
204
214
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
205
215
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
216
+ const EDITOR_LINE_NUMBERS_STORAGE_KEY = "piStudio.editorLineNumbersEnabled";
206
217
  // Single source of truth: language -> file extensions (and display label)
207
218
  var LANG_EXT_MAP = {
208
219
  markdown: { label: "Markdown", exts: ["md", "markdown", "mdx", "qmd"] },
@@ -258,6 +269,8 @@
258
269
  let editorLanguage = "markdown";
259
270
  let responseHighlightEnabled = false;
260
271
  let editorHighlightRenderRaf = null;
272
+ let lineNumbersEnabled = false;
273
+ let lineNumbersRenderRaf = null;
261
274
  let annotationsEnabled = true;
262
275
  let scratchpadText = "";
263
276
  let scratchpadReturnFocusEl = null;
@@ -664,14 +677,136 @@
664
677
  updateDocumentTitle();
665
678
  }
666
679
 
680
+ function truncateTitleSegment(text, maxLength) {
681
+ const normalized = normalizeActivityLabel(text);
682
+ if (!normalized) return "";
683
+ if (!Number.isFinite(maxLength) || maxLength <= 1 || normalized.length <= maxLength) {
684
+ return normalized;
685
+ }
686
+ return normalized.slice(0, maxLength - 1).trimEnd() + "…";
687
+ }
688
+
689
+ function readThemeColor(variableName, fallback) {
690
+ try {
691
+ const value = window.getComputedStyle(document.documentElement).getPropertyValue(variableName);
692
+ const trimmed = typeof value === "string" ? value.trim() : "";
693
+ return trimmed || fallback;
694
+ } catch {
695
+ return fallback;
696
+ }
697
+ }
698
+
699
+ function getTitleActionMessage(kind) {
700
+ if (kind === "annotation") return "Replying…";
701
+ if (kind === "critique") return "Critiquing…";
702
+ if (kind === "direct") return "Running…";
703
+ if (kind === "compact") return "Compacting…";
704
+ if (kind === "send_to_editor") return "Sending to editor…";
705
+ if (kind === "get_from_editor") return "Loading from editor…";
706
+ if (kind === "load_git_diff") return "Loading git diff…";
707
+ if (kind === "refresh_from_disk") return "Refreshing from disk…";
708
+ if (kind === "save_as" || kind === "save_over") return "Saving…";
709
+ return "Working…";
710
+ }
711
+
712
+ function getTitleBusyMessage() {
713
+ const activeKind = pendingKind || (agentBusyFromServer ? stickyStudioKind : null);
714
+ const hasStudioOwnedBusyState = uiBusy
715
+ || Boolean(pendingRequestId)
716
+ || Boolean(pendingKind)
717
+ || compactInProgress
718
+ || Boolean(agentBusyFromServer && stickyStudioKind)
719
+ || Boolean(agentBusyFromServer && studioRunChainActive);
720
+
721
+ if (!hasStudioOwnedBusyState) return "";
722
+
723
+ if (
724
+ pendingKind === "compact"
725
+ || compactInProgress
726
+ || (agentBusyFromServer && stickyStudioKind === "compact")
727
+ ) {
728
+ return "Compacting…";
729
+ }
730
+
731
+ if (terminalActivityPhase === "tool") {
732
+ if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
733
+ return truncateTitleSegment(withEllipsis(terminalActivityLabel), 34);
734
+ }
735
+ if (activeKind) return getTitleActionMessage(activeKind);
736
+ if (agentBusyFromServer && studioRunChainActive) return "Running…";
737
+ return "Working…";
738
+ }
739
+
740
+ if (terminalActivityPhase === "responding") {
741
+ if (activeKind === "critique") return "Critiquing…";
742
+ if (activeKind === "annotation") return "Replying…";
743
+ return "Responding…";
744
+ }
745
+
746
+ if (activeKind) return getTitleActionMessage(activeKind);
747
+ if (uiBusy || (agentBusyFromServer && studioRunChainActive)) return "Running…";
748
+ return "";
749
+ }
750
+
751
+ function getDynamicTitlePrefix() {
752
+ if (titleAttentionMessage) return titleAttentionMessage;
753
+ if (wsState === "Connecting") return reconnectAttempt > 0 ? "Reconnecting…" : "Connecting…";
754
+ if (wsState === "Disconnected") return "Disconnected";
755
+ return getTitleBusyMessage();
756
+ }
757
+
758
+ function buildStudioFaviconHref() {
759
+ const fg = readThemeColor("--text", "#111111");
760
+ const bg = readThemeColor("--bg", "#ffffff");
761
+ const accent = readThemeColor("--accent", fg);
762
+ const ok = readThemeColor("--ok", "#16a34a");
763
+ const warn = readThemeColor("--warn", accent);
764
+ const error = readThemeColor("--error", "#dc2626");
765
+
766
+ let badgeSvg = "";
767
+
768
+ if (titleAttentionMessage) {
769
+ badgeSvg = `<circle cx="50" cy="14" r="9" fill="${ok}" stroke="${bg}" stroke-width="4" />`;
770
+ } else if (wsState === "Disconnected") {
771
+ badgeSvg = `<circle cx="50" cy="14" r="9" fill="${error}" stroke="${bg}" stroke-width="4" />`;
772
+ } else if (wsState === "Connecting") {
773
+ badgeSvg = `<circle cx="50" cy="14" r="9" fill="${accent}" stroke="${bg}" stroke-width="4" />`;
774
+ } else if (getTitleBusyMessage()) {
775
+ badgeSvg = `<circle cx="50" cy="14" r="10" fill="none" stroke="${warn}" stroke-width="5" />`;
776
+ }
777
+
778
+ const svg = [
779
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">',
780
+ `<text x="32" y="35" text-anchor="middle" dominant-baseline="middle" font-size="50" font-weight="700" font-family="ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" fill="${fg}">π</text>`,
781
+ badgeSvg,
782
+ "</svg>",
783
+ ].join("");
784
+ return "data:image/svg+xml," + encodeURIComponent(svg);
785
+ }
786
+
667
787
  function updateDocumentTitle() {
668
788
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
669
789
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
670
790
  const titleParts = ["pi Studio"];
671
791
  if (terminalText && terminalText !== "unknown") titleParts.push(terminalText);
672
792
  if (modelText && modelText !== "none") titleParts.push(modelText);
673
- if (titleAttentionMessage) titleParts.unshift(titleAttentionMessage);
674
- document.title = titleParts.join(" · ");
793
+
794
+ const titlePrefix = getDynamicTitlePrefix();
795
+ if (titlePrefix) titleParts.unshift(titlePrefix);
796
+
797
+ const nextTitle = titleParts.join(" · ");
798
+ if (document.title !== nextTitle) {
799
+ document.title = nextTitle;
800
+ }
801
+
802
+ if (faviconLinkEl) {
803
+ const nextFaviconHref = buildStudioFaviconHref();
804
+ if (nextFaviconHref !== lastRenderedFaviconHref) {
805
+ faviconLinkEl.href = nextFaviconHref;
806
+ faviconLinkEl.type = "image/svg+xml";
807
+ lastRenderedFaviconHref = nextFaviconHref;
808
+ }
809
+ }
675
810
  }
676
811
 
677
812
  function updateFooterMeta() {
@@ -705,7 +840,7 @@
705
840
  function startFooterSpinner() {
706
841
  if (spinnerTimer) return;
707
842
  spinnerTimer = window.setInterval(() => {
708
- spinnerFrameIndex = (spinnerFrameIndex + 1) % BRAILLE_SPINNER_FRAMES.length;
843
+ spinnerFrameIndex += 1;
709
844
  renderStatus();
710
845
  }, 80);
711
846
  }
@@ -2410,6 +2545,9 @@
2410
2545
  if (editorHighlightEnabled && editorView === "markdown") {
2411
2546
  scheduleEditorHighlightRender();
2412
2547
  }
2548
+ if (lineNumbersEnabled && editorView === "markdown") {
2549
+ scheduleEditorLineNumberRender();
2550
+ }
2413
2551
  if (rightView === "editor-preview") {
2414
2552
  scheduleResponseEditorPreviewRender(previewDelayMs);
2415
2553
  }
@@ -2643,7 +2781,7 @@
2643
2781
  syncRunAndCritiqueButtons();
2644
2782
  copyDraftBtn.disabled = uiBusy;
2645
2783
  if (highlightSelect) highlightSelect.disabled = uiBusy;
2646
- if (langSelect) langSelect.disabled = uiBusy;
2784
+ if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
2647
2785
  if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
2648
2786
  if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
2649
2787
  if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
@@ -2710,6 +2848,9 @@
2710
2848
  schedule(() => {
2711
2849
  syncEditorHighlightScroll();
2712
2850
  });
2851
+ if (lineNumbersEnabled && editorView === "markdown") {
2852
+ scheduleEditorLineNumberRender();
2853
+ }
2713
2854
 
2714
2855
  updateAnnotatedReplyHeaderButton();
2715
2856
 
@@ -2745,7 +2886,11 @@
2745
2886
  }
2746
2887
 
2747
2888
  updateEditorHighlightState();
2748
- updateLangSelectVisibility();
2889
+ syncHighlightSelectUi();
2890
+ updateLineNumberGutterVisibility();
2891
+ if (!showPreview && lineNumbersEnabled) {
2892
+ scheduleEditorLineNumberRender();
2893
+ }
2749
2894
  }
2750
2895
 
2751
2896
  function setRightView(nextView) {
@@ -2765,6 +2910,115 @@
2765
2910
  syncActionButtons();
2766
2911
  }
2767
2912
 
2913
+ function lineNumbersShouldBeVisible() {
2914
+ return Boolean(
2915
+ lineNumbersEnabled
2916
+ && editorView === "markdown"
2917
+ && sourceEditorWrapEl
2918
+ && lineNumberGutterEl
2919
+ && lineNumberGutterContentEl
2920
+ && lineNumberMeasureEl,
2921
+ );
2922
+ }
2923
+
2924
+ function getEditorLineNumberGutterWidthCss(lineCount) {
2925
+ const digits = Math.max(2, String(Math.max(1, lineCount || 0)).length);
2926
+ return "calc(" + digits + "ch + 18px)";
2927
+ }
2928
+
2929
+ function updateLineNumberGutterVisibility() {
2930
+ const visible = lineNumbersShouldBeVisible();
2931
+ if (sourceEditorWrapEl) {
2932
+ sourceEditorWrapEl.classList.toggle("line-numbers-enabled", visible);
2933
+ if (!visible) {
2934
+ sourceEditorWrapEl.style.setProperty("--editor-line-number-gutter-width", "0px");
2935
+ }
2936
+ }
2937
+ if (lineNumberGutterEl) {
2938
+ lineNumberGutterEl.hidden = !visible;
2939
+ }
2940
+ if (!visible) {
2941
+ if (lineNumberGutterContentEl) lineNumberGutterContentEl.innerHTML = "";
2942
+ if (lineNumberMeasureEl) lineNumberMeasureEl.innerHTML = "";
2943
+ }
2944
+ return visible;
2945
+ }
2946
+
2947
+ function renderEditorLineNumbersNow() {
2948
+ if (!updateLineNumberGutterVisibility()) return;
2949
+
2950
+ const text = String(sourceTextEl.value || "").replace(/\r\n/g, "\n");
2951
+ const lines = text.split("\n");
2952
+ const lineCount = Math.max(1, lines.length);
2953
+ sourceEditorWrapEl.style.setProperty("--editor-line-number-gutter-width", getEditorLineNumberGutterWidthCss(lineCount));
2954
+
2955
+ const styles = window.getComputedStyle(sourceTextEl);
2956
+ const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
2957
+ const paddingTop = parseFloat(styles.paddingTop) || 0;
2958
+ const paddingRight = parseFloat(styles.paddingRight) || 0;
2959
+ const paddingBottom = parseFloat(styles.paddingBottom) || 0;
2960
+ const paddingLeft = parseFloat(styles.paddingLeft) || 0;
2961
+ const contentWidth = Math.max(1, sourceTextEl.clientWidth - paddingLeft - paddingRight);
2962
+
2963
+ lineNumberGutterContentEl.style.paddingTop = paddingTop + "px";
2964
+ lineNumberGutterContentEl.style.paddingBottom = paddingBottom + "px";
2965
+ lineNumberMeasureEl.style.width = contentWidth + "px";
2966
+ lineNumberMeasureEl.innerHTML = lines
2967
+ .map((line) => "<div class='editor-line-number-measure-line'>" + (line.length ? escapeHtml(line) : "&#8203;") + "</div>")
2968
+ .join("");
2969
+
2970
+ const measureLines = Array.from(lineNumberMeasureEl.children);
2971
+ lineNumberGutterContentEl.innerHTML = measureLines
2972
+ .map((lineEl, index) => {
2973
+ const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
2974
+ return "<div class='editor-line-number-row' style='height:" + height.toFixed(2) + "px'>" + (index + 1) + "</div>";
2975
+ })
2976
+ .join("");
2977
+
2978
+ syncEditorHighlightScroll();
2979
+ }
2980
+
2981
+ function scheduleEditorLineNumberRender() {
2982
+ if (lineNumbersRenderRaf !== null) {
2983
+ if (typeof window.cancelAnimationFrame === "function") {
2984
+ window.cancelAnimationFrame(lineNumbersRenderRaf);
2985
+ } else {
2986
+ window.clearTimeout(lineNumbersRenderRaf);
2987
+ }
2988
+ lineNumbersRenderRaf = null;
2989
+ }
2990
+
2991
+ const schedule = typeof window.requestAnimationFrame === "function"
2992
+ ? window.requestAnimationFrame.bind(window)
2993
+ : (cb) => window.setTimeout(cb, 16);
2994
+
2995
+ lineNumbersRenderRaf = schedule(() => {
2996
+ lineNumbersRenderRaf = null;
2997
+ renderEditorLineNumbersNow();
2998
+ });
2999
+ }
3000
+
3001
+ function readStoredEditorLineNumbersEnabled() {
3002
+ return readStoredToggle(EDITOR_LINE_NUMBERS_STORAGE_KEY);
3003
+ }
3004
+
3005
+ function persistEditorLineNumbersEnabled(enabled) {
3006
+ persistStoredToggle(EDITOR_LINE_NUMBERS_STORAGE_KEY, enabled);
3007
+ }
3008
+
3009
+ function setLineNumbersEnabled(enabled) {
3010
+ lineNumbersEnabled = Boolean(enabled);
3011
+ persistEditorLineNumbersEnabled(lineNumbersEnabled);
3012
+ if (lineNumbersSelect) {
3013
+ lineNumbersSelect.value = lineNumbersEnabled ? "on" : "off";
3014
+ }
3015
+ updateLineNumberGutterVisibility();
3016
+ scheduleEditorLineNumberRender();
3017
+ if (editorHighlightEnabled && editorView === "markdown") {
3018
+ scheduleEditorHighlightRender();
3019
+ }
3020
+ }
3021
+
2768
3022
  function getToken() {
2769
3023
  const query = new URLSearchParams(window.location.search || "");
2770
3024
  const hash = new URLSearchParams((window.location.hash || "").replace(/^#/, ""));
@@ -3291,9 +3545,13 @@
3291
3545
  }
3292
3546
 
3293
3547
  function syncEditorHighlightScroll() {
3294
- if (!sourceHighlightEl) return;
3295
- sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
3296
- sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
3548
+ if (sourceHighlightEl) {
3549
+ sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
3550
+ sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
3551
+ }
3552
+ if (lineNumberGutterEl) {
3553
+ lineNumberGutterEl.scrollTop = sourceTextEl.scrollTop;
3554
+ }
3297
3555
  }
3298
3556
 
3299
3557
  function runEditorMetaUpdateNow() {
@@ -3521,14 +3779,22 @@
3521
3779
  syncEditorHighlightScroll();
3522
3780
  }
3523
3781
 
3782
+ function syncHighlightSelectUi() {
3783
+ if (!highlightSelect) return;
3784
+ if (!editorHighlightEnabled) {
3785
+ highlightSelect.value = "off";
3786
+ return;
3787
+ }
3788
+ highlightSelect.value = (editorLanguage && SUPPORTED_LANGUAGES.indexOf(editorLanguage) !== -1)
3789
+ ? editorLanguage
3790
+ : "markdown";
3791
+ }
3792
+
3524
3793
  function setEditorHighlightEnabled(enabled) {
3525
3794
  editorHighlightEnabled = Boolean(enabled);
3526
3795
  persistEditorHighlightEnabled(editorHighlightEnabled);
3527
- if (highlightSelect) {
3528
- highlightSelect.value = editorHighlightEnabled ? "on" : "off";
3529
- }
3796
+ syncHighlightSelectUi();
3530
3797
  updateEditorHighlightState();
3531
- updateLangSelectVisibility();
3532
3798
  }
3533
3799
 
3534
3800
  function readStoredEditorLanguage() {
@@ -3552,9 +3818,7 @@
3552
3818
  function setEditorLanguage(lang) {
3553
3819
  editorLanguage = (lang && SUPPORTED_LANGUAGES.indexOf(lang) !== -1) ? lang : "markdown";
3554
3820
  persistEditorLanguage(editorLanguage);
3555
- if (langSelect) {
3556
- langSelect.value = editorLanguage;
3557
- }
3821
+ syncHighlightSelectUi();
3558
3822
  if (editorHighlightEnabled && editorView === "markdown") {
3559
3823
  scheduleEditorHighlightRender();
3560
3824
  }
@@ -3563,11 +3827,13 @@
3563
3827
  }
3564
3828
  }
3565
3829
 
3566
- function updateLangSelectVisibility() {
3567
- if (!langSelect) return;
3568
- const highlightActive = editorHighlightEnabled && editorView === "markdown";
3569
- const previewActive = editorView === "preview";
3570
- langSelect.hidden = !(highlightActive || previewActive);
3830
+ function setEditorHighlightMode(mode) {
3831
+ if (mode === "off") {
3832
+ setEditorHighlightEnabled(false);
3833
+ return;
3834
+ }
3835
+ setEditorLanguage(mode);
3836
+ setEditorHighlightEnabled(true);
3571
3837
  }
3572
3838
 
3573
3839
  function setResponseHighlightEnabled(enabled) {
@@ -3618,7 +3884,7 @@
3618
3884
  queueSteerBtn.title = "Queue steering is unavailable in editor-only mode.";
3619
3885
  }
3620
3886
  if (critiqueBtn) {
3621
- critiqueBtn.textContent = "Critique editor text";
3887
+ critiqueBtn.textContent = "Critique text";
3622
3888
  critiqueBtn.classList.remove("request-stop-active");
3623
3889
  critiqueBtn.disabled = true;
3624
3890
  critiqueBtn.title = "Critique is unavailable in editor-only mode.";
@@ -3649,7 +3915,7 @@
3649
3915
  }
3650
3916
 
3651
3917
  if (critiqueBtn) {
3652
- critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique editor text";
3918
+ critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique text";
3653
3919
  critiqueBtn.classList.toggle("request-stop-active", critiqueIsStop);
3654
3920
  critiqueBtn.disabled = critiqueIsStop ? wsState === "Disconnected" : (uiBusy || canQueueSteering);
3655
3921
  critiqueBtn.title = critiqueIsStop
@@ -3657,8 +3923,8 @@
3657
3923
  : (canQueueSteering
3658
3924
  ? "Critique queueing is not supported while Run editor text is active."
3659
3925
  : (annotationsEnabled
3660
- ? "Critique editor text as-is (includes [an: ...] markers)."
3661
- : "Critique editor text with [an: ...] markers stripped."));
3926
+ ? "Critique text as-is (includes [an: ...] markers)."
3927
+ : "Critique text with [an: ...] markers stripped."));
3662
3928
  }
3663
3929
  }
3664
3930
 
@@ -4298,6 +4564,7 @@
4298
4564
  root.style.setProperty(key, message.vars[key]);
4299
4565
  }
4300
4566
  });
4567
+ updateDocumentTitle();
4301
4568
  }
4302
4569
  }
4303
4570
 
@@ -4517,11 +4784,11 @@
4517
4784
  if (!insertHeaderBtn) return;
4518
4785
  const hasHeader = stripAnnotationHeader(sourceTextEl.value).hadHeader;
4519
4786
  if (hasHeader) {
4520
- insertHeaderBtn.textContent = "Remove annotated reply header";
4787
+ insertHeaderBtn.textContent = "Annotation header: On";
4521
4788
  insertHeaderBtn.title = "Remove annotated-reply protocol header while keeping body text.";
4522
4789
  return;
4523
4790
  }
4524
- insertHeaderBtn.textContent = "Insert annotated reply header";
4791
+ insertHeaderBtn.textContent = "Annotation header: Off";
4525
4792
  insertHeaderBtn.title = "Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).";
4526
4793
  }
4527
4794
 
@@ -4617,7 +4884,7 @@
4617
4884
 
4618
4885
  if (highlightSelect) {
4619
4886
  highlightSelect.addEventListener("change", () => {
4620
- setEditorHighlightEnabled(highlightSelect.value === "on");
4887
+ setEditorHighlightMode(highlightSelect.value);
4621
4888
  });
4622
4889
  }
4623
4890
 
@@ -4627,9 +4894,9 @@
4627
4894
  });
4628
4895
  }
4629
4896
 
4630
- if (langSelect) {
4631
- langSelect.addEventListener("change", () => {
4632
- setEditorLanguage(langSelect.value);
4897
+ if (lineNumbersSelect) {
4898
+ lineNumbersSelect.addEventListener("change", () => {
4899
+ setLineNumbersEnabled(lineNumbersSelect.value === "on");
4633
4900
  });
4634
4901
  }
4635
4902
 
@@ -4742,23 +5009,26 @@
4742
5009
  });
4743
5010
 
4744
5011
  sourceTextEl.addEventListener("scroll", () => {
4745
- if (!editorHighlightEnabled || editorView !== "markdown") return;
5012
+ if (editorView !== "markdown") return;
4746
5013
  syncEditorHighlightScroll();
4747
5014
  });
4748
5015
 
4749
5016
  sourceTextEl.addEventListener("keyup", () => {
4750
- if (!editorHighlightEnabled || editorView !== "markdown") return;
5017
+ if (editorView !== "markdown") return;
4751
5018
  syncEditorHighlightScroll();
4752
5019
  });
4753
5020
 
4754
5021
  sourceTextEl.addEventListener("mouseup", () => {
4755
- if (!editorHighlightEnabled || editorView !== "markdown") return;
5022
+ if (editorView !== "markdown") return;
4756
5023
  syncEditorHighlightScroll();
4757
5024
  });
4758
5025
 
4759
5026
  window.addEventListener("resize", () => {
4760
- if (!editorHighlightEnabled || editorView !== "markdown") return;
5027
+ if (editorView !== "markdown") return;
4761
5028
  syncEditorHighlightScroll();
5029
+ if (lineNumbersEnabled) {
5030
+ scheduleEditorLineNumberRender();
5031
+ }
4762
5032
  });
4763
5033
 
4764
5034
  insertHeaderBtn.addEventListener("click", () => {
@@ -5286,6 +5556,14 @@
5286
5556
  reader.readAsText(file);
5287
5557
  });
5288
5558
 
5559
+ if (sourceEditorWrapEl && typeof ResizeObserver === "function") {
5560
+ const editorResizeObserver = new ResizeObserver(() => {
5561
+ if (editorView !== "markdown" || !lineNumbersEnabled) return;
5562
+ scheduleEditorLineNumberRender();
5563
+ });
5564
+ editorResizeObserver.observe(sourceEditorWrapEl);
5565
+ }
5566
+
5289
5567
  setSourceState(initialSourceState);
5290
5568
  refreshResponseUi();
5291
5569
  updateAnnotatedReplyHeaderButton();
@@ -5293,13 +5571,17 @@
5293
5571
  setScratchpadText(readStoredScratchpadText() || "", { persist: false });
5294
5572
 
5295
5573
  const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
5296
- const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value === "on");
5574
+ const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value !== "off");
5297
5575
  setEditorHighlightEnabled(initialHighlightEnabled);
5298
5576
 
5299
5577
  const initialDetectedLang = detectLanguageFromName(initialSourceState.path || initialSourceState.label || "");
5300
5578
  const storedLang = readStoredEditorLanguage();
5301
5579
  setEditorLanguage(initialDetectedLang || storedLang || "markdown");
5302
5580
 
5581
+ const storedLineNumbersEnabled = readStoredEditorLineNumbersEnabled();
5582
+ const initialLineNumbersEnabled = storedLineNumbersEnabled ?? Boolean(lineNumbersSelect && lineNumbersSelect.value === "on");
5583
+ setLineNumbersEnabled(initialLineNumbersEnabled);
5584
+
5303
5585
  const storedResponseHighlightEnabled = readStoredResponseHighlightEnabled();
5304
5586
  const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
5305
5587
  setResponseHighlightEnabled(initialResponseHighlightEnabled);
package/client/studio.css CHANGED
@@ -87,14 +87,19 @@
87
87
  }
88
88
 
89
89
  #sendRunBtn,
90
- #queueSteerBtn,
91
- #critiqueBtn {
90
+ #queueSteerBtn {
92
91
  min-width: 10rem;
93
92
  display: inline-flex;
94
93
  justify-content: center;
95
94
  align-items: center;
96
95
  }
97
96
 
97
+ #critiqueBtn {
98
+ display: inline-flex;
99
+ justify-content: center;
100
+ align-items: center;
101
+ }
102
+
98
103
  #sendRunBtn:not(:disabled):not(.request-stop-active),
99
104
  #queueSteerBtn:not(:disabled),
100
105
  #loadResponseBtn:not(:disabled):not([hidden]) {
@@ -412,6 +417,7 @@
412
417
  }
413
418
 
414
419
  .editor-highlight-wrap {
420
+ --editor-line-number-gutter-width: 0px;
415
421
  position: relative;
416
422
  display: flex;
417
423
  flex: 1 1 auto;
@@ -424,13 +430,63 @@
424
430
  overscroll-behavior: none;
425
431
  }
426
432
 
433
+ .editor-line-number-gutter {
434
+ position: absolute;
435
+ inset: 0 auto 0 0;
436
+ width: var(--editor-line-number-gutter-width);
437
+ overflow: hidden;
438
+ border-right: 1px solid var(--border-muted);
439
+ background: var(--panel-2);
440
+ color: var(--muted);
441
+ pointer-events: none;
442
+ z-index: 0;
443
+ }
444
+
445
+ .editor-line-number-gutter-content {
446
+ min-height: 100%;
447
+ padding: 10px 8px 10px 0;
448
+ text-align: right;
449
+ font-family: var(--font-mono);
450
+ font-size: 12px;
451
+ line-height: 1.45;
452
+ font-variant-numeric: tabular-nums;
453
+ white-space: nowrap;
454
+ }
455
+
456
+ .editor-line-number-row {
457
+ display: block;
458
+ user-select: none;
459
+ }
460
+
461
+ .editor-line-number-measure {
462
+ position: absolute;
463
+ top: 0;
464
+ left: 0;
465
+ visibility: hidden;
466
+ pointer-events: none;
467
+ overflow: hidden;
468
+ white-space: pre-wrap;
469
+ word-break: normal;
470
+ overflow-wrap: break-word;
471
+ font-family: var(--font-mono);
472
+ font-size: 13px;
473
+ line-height: 1.45;
474
+ tab-size: 2;
475
+ z-index: -1;
476
+ }
477
+
478
+ .editor-line-number-measure-line {
479
+ display: block;
480
+ min-height: 1.45em;
481
+ }
482
+
427
483
  .editor-highlight {
428
484
  position: absolute;
429
485
  inset: 0;
430
486
  margin: 0;
431
487
  border: 0;
432
488
  border-radius: 8px;
433
- padding: 10px;
489
+ padding: 10px 10px 10px calc(10px + var(--editor-line-number-gutter-width));
434
490
  overflow: auto;
435
491
  pointer-events: none;
436
492
  white-space: pre-wrap;
@@ -457,6 +513,7 @@
457
513
  resize: none;
458
514
  outline: none;
459
515
  overscroll-behavior: none;
516
+ padding: 10px 10px 10px calc(10px + var(--editor-line-number-gutter-width));
460
517
  }
461
518
 
462
519
  #sourceText.highlight-active {
package/index.ts CHANGED
@@ -955,7 +955,7 @@ function rawDataToString(data: RawData): string {
955
955
  if (typeof data === "string") return data;
956
956
  if (data instanceof Buffer) return data.toString("utf-8");
957
957
  if (Array.isArray(data)) return Buffer.concat(data).toString("utf-8");
958
- return Buffer.from(data).toString("utf-8");
958
+ return Buffer.from(data as ArrayBuffer).toString("utf-8");
959
959
  }
960
960
 
961
961
  function isValidRequestId(id: string): boolean {
@@ -3259,7 +3259,7 @@ function renderStudioAnnotationPdfLatex(text: string): string {
3259
3259
  function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
3260
3260
  const replaced = replaceStudioInlineAnnotationMarkers(
3261
3261
  String(text ?? ""),
3262
- (marker) => {
3262
+ (marker: { body: string }) => {
3263
3263
  const cleaned = renderStudioAnnotationPdfLatex(marker.body);
3264
3264
  if (!cleaned) return "";
3265
3265
  return `\\studioannotation{${cleaned}}`;
@@ -3276,7 +3276,7 @@ function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
3276
3276
 
3277
3277
  function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
3278
3278
  if (!hasStudioMarkdownAnnotationMarkers(markdown)) return String(markdown ?? "");
3279
- return transformStudioMarkdownOutsideFences(markdown, (segment) => replaceStudioAnnotationMarkersForPdfInSegment(segment));
3279
+ return transformStudioMarkdownOutsideFences(markdown, (segment: string) => replaceStudioAnnotationMarkersForPdfInSegment(segment));
3280
3280
  }
3281
3281
 
3282
3282
  interface StudioPdfRenderOptions {
@@ -4325,13 +4325,13 @@ function replaceStudioAnnotationMarkersInDiffTokenLine(line: string, macroName:
4325
4325
  const wrapText = (text: string): string => text ? `\\${macroName}{${text}}` : "";
4326
4326
  const rewritten = replaceStudioInlineAnnotationMarkers(
4327
4327
  body,
4328
- (marker) => {
4328
+ (marker: { body: string }) => {
4329
4329
  const markerText = decodeStudioGeneratedCodeLatexText(normalizeStudioAnnotationText(marker.body));
4330
4330
  const cleaned = makeStudioHighlightingMathScriptsVerbatimSafe(renderStudioAnnotationPdfLatex(markerText));
4331
4331
  if (!cleaned) return "";
4332
4332
  return `\\studioannotation{${cleaned}}`;
4333
4333
  },
4334
- (segment) => wrapText(segment),
4334
+ (segment: string) => wrapText(segment),
4335
4335
  );
4336
4336
 
4337
4337
  return rewritten === body ? line : (rewritten || wrapText(body));
@@ -5866,7 +5866,7 @@ ${cssVarsBlock}
5866
5866
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
5867
5867
  </div>
5868
5868
  <div class="source-actions-row">
5869
- <button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Insert annotated reply header</button>
5869
+ <button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
5870
5870
  <select id="annotationModeSelect" aria-label="Annotation visibility mode" title="On: keep and send [an: ...] markers. Hidden: keep markers in editor, hide in preview, and strip before Run/Critique.">
5871
5871
  <option value="on" selected>Annotations: On</option>
5872
5872
  <option value="off">Annotations: Hidden</option>
@@ -5876,46 +5876,51 @@ ${cssVarsBlock}
5876
5876
  </div>
5877
5877
  <div class="source-actions-row">
5878
5878
  <select id="lensSelect" aria-label="Critique focus">
5879
- <option value="auto" selected>Critique focus: Auto</option>
5880
- <option value="writing">Critique focus: Writing</option>
5881
- <option value="code">Critique focus: Code</option>
5879
+ <option value="auto" selected>Critique: Auto</option>
5880
+ <option value="writing">Critique: Writing</option>
5881
+ <option value="code">Critique: Code</option>
5882
5882
  </select>
5883
- <button id="critiqueBtn" type="button">Critique editor text</button>
5883
+ <button id="critiqueBtn" type="button">Critique text</button>
5884
5884
  <select id="highlightSelect" aria-label="Editor syntax highlighting">
5885
5885
  <option value="off">Syntax highlight: Off</option>
5886
- <option value="on" selected>Syntax highlight: On</option>
5886
+ <option value="bash">Syntax highlight: Bash</option>
5887
+ <option value="c">Syntax highlight: C</option>
5888
+ <option value="cpp">Syntax highlight: C++</option>
5889
+ <option value="css">Syntax highlight: CSS</option>
5890
+ <option value="diff">Syntax highlight: Diff</option>
5891
+ <option value="fortran">Syntax highlight: Fortran</option>
5892
+ <option value="go">Syntax highlight: Go</option>
5893
+ <option value="html">Syntax highlight: HTML</option>
5894
+ <option value="java">Syntax highlight: Java</option>
5895
+ <option value="javascript">Syntax highlight: JavaScript</option>
5896
+ <option value="json">Syntax highlight: JSON</option>
5897
+ <option value="julia">Syntax highlight: Julia</option>
5898
+ <option value="latex">Syntax highlight: LaTeX</option>
5899
+ <option value="lua">Syntax highlight: Lua</option>
5900
+ <option value="markdown" selected>Syntax highlight: Markdown</option>
5901
+ <option value="matlab">Syntax highlight: MATLAB</option>
5902
+ <option value="text">Syntax highlight: Plain Text</option>
5903
+ <option value="python">Syntax highlight: Python</option>
5904
+ <option value="r">Syntax highlight: R</option>
5905
+ <option value="rust">Syntax highlight: Rust</option>
5906
+ <option value="swift">Syntax highlight: Swift</option>
5907
+ <option value="toml">Syntax highlight: TOML</option>
5908
+ <option value="typescript">Syntax highlight: TypeScript</option>
5909
+ <option value="xml">Syntax highlight: XML</option>
5910
+ <option value="yaml">Syntax highlight: YAML</option>
5887
5911
  </select>
5888
- <select id="langSelect" aria-label="Highlight language">
5889
- <option value="bash">Lang: Bash</option>
5890
- <option value="c">Lang: C</option>
5891
- <option value="cpp">Lang: C++</option>
5892
- <option value="css">Lang: CSS</option>
5893
- <option value="diff">Lang: Diff</option>
5894
- <option value="fortran">Lang: Fortran</option>
5895
- <option value="go">Lang: Go</option>
5896
- <option value="html">Lang: HTML</option>
5897
- <option value="java">Lang: Java</option>
5898
- <option value="javascript">Lang: JavaScript</option>
5899
- <option value="json">Lang: JSON</option>
5900
- <option value="julia">Lang: Julia</option>
5901
- <option value="latex">Lang: LaTeX</option>
5902
- <option value="lua">Lang: Lua</option>
5903
- <option value="markdown" selected>Lang: Markdown</option>
5904
- <option value="matlab">Lang: MATLAB</option>
5905
- <option value="text">Lang: Plain Text</option>
5906
- <option value="python">Lang: Python</option>
5907
- <option value="r">Lang: R</option>
5908
- <option value="rust">Lang: Rust</option>
5909
- <option value="swift">Lang: Swift</option>
5910
- <option value="toml">Lang: TOML</option>
5911
- <option value="typescript">Lang: TypeScript</option>
5912
- <option value="xml">Lang: XML</option>
5913
- <option value="yaml">Lang: YAML</option>
5912
+ <select id="lineNumbersSelect" aria-label="Editor line numbers">
5913
+ <option value="off" selected>Line numbers: Off</option>
5914
+ <option value="on">Line numbers: On</option>
5914
5915
  </select>
5915
5916
  </div>
5916
5917
  </div>
5917
5918
  </div>
5918
5919
  <div id="sourceEditorWrap" class="editor-highlight-wrap">
5920
+ <div id="lineNumberGutter" class="editor-line-number-gutter" hidden aria-hidden="true">
5921
+ <div id="lineNumberGutterContent" class="editor-line-number-gutter-content"></div>
5922
+ </div>
5923
+ <div id="lineNumberMeasure" class="editor-line-number-measure" aria-hidden="true"></div>
5919
5924
  <pre id="sourceHighlight" class="editor-highlight" aria-hidden="true"></pre>
5920
5925
  <textarea id="sourceText" placeholder="Paste or edit text here.">${initialText}</textarea>
5921
5926
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.43",
3
+ "version": "0.5.44",
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",
@@ -25,7 +25,8 @@
25
25
  "assets/screenshots"
26
26
  ],
27
27
  "scripts": {
28
- "test": "node --test"
28
+ "test": "node --test",
29
+ "typecheck": "tsc --noEmit"
29
30
  },
30
31
  "pi": {
31
32
  "extensions": [
@@ -37,5 +38,11 @@
37
38
  },
38
39
  "dependencies": {
39
40
  "ws": "^8.18.0"
41
+ },
42
+ "devDependencies": {
43
+ "@mariozechner/pi-coding-agent": "^0.64.0",
44
+ "@types/node": "^24.3.0",
45
+ "@types/ws": "^8.18.1",
46
+ "typescript": "^5.7.3"
40
47
  }
41
48
  }