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 +23 -0
- package/README.md +3 -1
- package/client/studio-client.js +260 -25
- package/client/studio.css +306 -137
- package/index.ts +382 -79
- package/package.json +8 -2
- package/themes/pi-studio-dark.json +78 -0
- package/themes/pi-studio-light.json +77 -0
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
|
-
-
|
|
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
|
package/client/studio-client.js
CHANGED
|
@@ -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
|
|
695
|
+
const normalizedQuery = normalize(queryValue);
|
|
696
|
+
const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
|
|
681
697
|
try {
|
|
682
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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: ? / " +
|
|
1383
|
+
return "Context: ? / " + formatContextNumber(contextWindow);
|
|
1284
1384
|
}
|
|
1285
1385
|
|
|
1286
|
-
let text = "Context: " +
|
|
1386
|
+
let text = "Context: " + formatContextNumber(contextTokens);
|
|
1287
1387
|
if (hasWindow) {
|
|
1288
|
-
text += " / " +
|
|
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
|
|
1519
|
-
const
|
|
1520
|
-
|
|
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 =
|
|
1635
|
+
footerMetaTextEl.title = titleText;
|
|
1523
1636
|
} else if (footerMetaEl) {
|
|
1524
1637
|
footerMetaEl.textContent = text;
|
|
1525
|
-
footerMetaEl.title =
|
|
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 ? "
|
|
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();
|