pi-studio 0.5.58 → 0.6.0
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 +22 -0
- package/client/studio-client.js +472 -10
- package/client/studio.css +583 -1
- package/index.ts +70 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.0] — 2026-04-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- The refreshed Studio layout is now the default, with the classic layout still available via the footer UI switch or `?uiRefresh=0`.
|
|
14
|
+
- Working-view tool output now replaces image/base64 payloads with compact placeholders instead of dumping raw image data.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Queued steering now updates Studio's active effective-prompt metadata so response history and prompt loading reflect the original run plus steering messages.
|
|
18
|
+
- Newly arrived responses now force the right response pane to reset to the top, while editor-preview/document views still preserve scroll.
|
|
19
|
+
|
|
20
|
+
## [0.5.59] — 2026-04-27
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Added an opt-in refreshed Studio layout, available via the footer UI switch or `?uiRefresh=1`, while keeping the classic layout as the default.
|
|
24
|
+
- Added refreshed layout support for editor-only Studio mode.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Tuned refreshed pane headers, toolbar grouping, focus controls, dropdown labels, queue steering sizing, and preview export styling.
|
|
28
|
+
|
|
7
29
|
## [0.5.58] — 2026-04-24
|
|
8
30
|
|
|
9
31
|
### Added
|
package/client/studio-client.js
CHANGED
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
const reviewNotesListEl = document.getElementById("reviewNotesList");
|
|
133
133
|
const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
|
|
134
134
|
const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
|
|
135
|
+
const reviewNotesPromptBtn = document.getElementById("reviewNotesPromptBtn");
|
|
135
136
|
const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
|
|
136
137
|
const reviewNotesDeleteAllBtn = document.getElementById("reviewNotesDeleteAllBtn");
|
|
137
138
|
const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
|
|
@@ -644,6 +645,12 @@
|
|
|
644
645
|
let lineNumbersEnabled = false;
|
|
645
646
|
let lineNumbersRenderRaf = null;
|
|
646
647
|
let annotationsEnabled = true;
|
|
648
|
+
const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
|
|
649
|
+
const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
|
|
650
|
+
let studioUiRefreshUi = null;
|
|
651
|
+
if (studioUiRefreshEnabled && document.body) {
|
|
652
|
+
document.body.classList.add("studio-ui-refresh");
|
|
653
|
+
}
|
|
647
654
|
let scratchpadText = "";
|
|
648
655
|
let scratchpadReturnFocusEl = null;
|
|
649
656
|
let scratchpadPersistTimer = null;
|
|
@@ -662,6 +669,332 @@
|
|
|
662
669
|
let suppressedEditorSelectionEnd = null;
|
|
663
670
|
const previewJumpHighlightState = new WeakMap();
|
|
664
671
|
const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
|
|
672
|
+
|
|
673
|
+
function readStudioUiRefreshEnabled() {
|
|
674
|
+
const normalize = (value) => String(value == null ? "" : value).trim().toLowerCase();
|
|
675
|
+
const queryValue = initialQueryParams.has("uiRefresh")
|
|
676
|
+
? initialQueryParams.get("uiRefresh")
|
|
677
|
+
: (initialQueryParams.has("studioUiRefresh") ? initialQueryParams.get("studioUiRefresh") : null);
|
|
678
|
+
const isTruthy = (value) => ["1", "true", "yes", "on", "v2", "refresh", "fresh"].indexOf(normalize(value)) !== -1;
|
|
679
|
+
const isFalsey = (value) => ["0", "false", "no", "off", "classic"].indexOf(normalize(value)) !== -1;
|
|
680
|
+
if (queryValue !== null) {
|
|
681
|
+
const normalizedQuery = normalize(queryValue);
|
|
682
|
+
const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
|
|
683
|
+
try {
|
|
684
|
+
window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
|
|
685
|
+
} catch {}
|
|
686
|
+
return enabled;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
const stored = window.localStorage ? window.localStorage.getItem(STUDIO_UI_REFRESH_STORAGE_KEY) : null;
|
|
690
|
+
if (stored !== null) return stored !== "0" && !isFalsey(stored);
|
|
691
|
+
} catch {}
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function makeStudioUiRefreshElement(tagName, className, text) {
|
|
696
|
+
const element = document.createElement(tagName);
|
|
697
|
+
if (className) element.className = className;
|
|
698
|
+
if (typeof text === "string") element.textContent = text;
|
|
699
|
+
return element;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function makeStudioUiRefreshSeparator() {
|
|
703
|
+
return makeStudioUiRefreshElement("span", "studio-refresh-sep");
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function makeStudioUiRefreshIcon(kind) {
|
|
707
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
708
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
709
|
+
svg.setAttribute("aria-hidden", "true");
|
|
710
|
+
svg.classList.add("studio-refresh-icon");
|
|
711
|
+
const paths = kind === "focus-exit"
|
|
712
|
+
? ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"]
|
|
713
|
+
: ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
|
|
714
|
+
for (const d of paths) {
|
|
715
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
716
|
+
path.setAttribute("d", d);
|
|
717
|
+
svg.appendChild(path);
|
|
718
|
+
}
|
|
719
|
+
return svg;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function setStudioUiRefreshFocusButtonIcon(buttonEl, isFocusedPane) {
|
|
723
|
+
if (!buttonEl || !studioUiRefreshEnabled) return;
|
|
724
|
+
buttonEl.replaceChildren(makeStudioUiRefreshIcon(isFocusedPane ? "focus-exit" : "focus"));
|
|
725
|
+
buttonEl.setAttribute("aria-label", isFocusedPane ? "Exit focus" : "Focus pane");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function appendStudioUiRefreshMenuSection(menuEl, heading, controls) {
|
|
729
|
+
const sectionEl = makeStudioUiRefreshElement("div", "studio-refresh-menu-section");
|
|
730
|
+
if (heading) {
|
|
731
|
+
sectionEl.appendChild(makeStudioUiRefreshElement("div", "studio-refresh-menu-heading", heading));
|
|
732
|
+
}
|
|
733
|
+
for (const control of controls) {
|
|
734
|
+
if (!control) continue;
|
|
735
|
+
const itemEl = makeStudioUiRefreshElement("div", "studio-refresh-menu-item");
|
|
736
|
+
itemEl.appendChild(control);
|
|
737
|
+
sectionEl.appendChild(itemEl);
|
|
738
|
+
}
|
|
739
|
+
menuEl.appendChild(sectionEl);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function getStudioUiRefreshSelectSummary(selectEl, prefix) {
|
|
743
|
+
if (!selectEl) return "";
|
|
744
|
+
const option = selectEl.options && selectEl.selectedIndex >= 0 ? selectEl.options[selectEl.selectedIndex] : null;
|
|
745
|
+
let label = option ? String(option.textContent || option.label || option.value || "") : String(selectEl.value || "");
|
|
746
|
+
if (prefix) label = label.replace(new RegExp("^" + prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*:?\\s*", "i"), "");
|
|
747
|
+
return label.trim();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function setStudioUiRefreshButtonText(buttonEl, text) {
|
|
751
|
+
if (!buttonEl) return;
|
|
752
|
+
buttonEl.textContent = text;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function getStudioUiRefreshAnnotationHeaderEnabled() {
|
|
756
|
+
try {
|
|
757
|
+
return Boolean(stripAnnotationHeader(sourceTextEl.value).hadHeader);
|
|
758
|
+
} catch {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function syncStudioUiRefreshSummaries() {
|
|
764
|
+
if (!studioUiRefreshUi) return;
|
|
765
|
+
if (studioUiRefreshUi.annotationsButton) {
|
|
766
|
+
const inlineLabel = annotationsEnabled ? "Inline on" : "Inline hidden";
|
|
767
|
+
if (isEditorOnlyMode) {
|
|
768
|
+
setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel);
|
|
769
|
+
} else {
|
|
770
|
+
const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
|
|
771
|
+
setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (studioUiRefreshUi.viewButton) {
|
|
775
|
+
const syntaxLabel = editorHighlightEnabled
|
|
776
|
+
? (getStudioUiRefreshSelectSummary(highlightSelect, "Syntax highlight") || editorLanguage || "Markdown")
|
|
777
|
+
: "Off";
|
|
778
|
+
const lineLabel = lineNumbersEnabled ? "Lines on" : "Lines off";
|
|
779
|
+
setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel);
|
|
780
|
+
}
|
|
781
|
+
syncStudioUiRefreshReviewTrigger();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function closeStudioUiRefreshMenus() {
|
|
785
|
+
if (!studioUiRefreshUi || !studioUiRefreshUi.menus) return;
|
|
786
|
+
for (const item of studioUiRefreshUi.menus) {
|
|
787
|
+
item.menu.hidden = true;
|
|
788
|
+
item.button.classList.remove("is-open");
|
|
789
|
+
item.button.setAttribute("aria-expanded", "false");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function toggleStudioUiRefreshMenu(name) {
|
|
794
|
+
if (!studioUiRefreshUi || !studioUiRefreshUi.menus) return;
|
|
795
|
+
let willOpen = false;
|
|
796
|
+
for (const item of studioUiRefreshUi.menus) {
|
|
797
|
+
if (item.name === name) willOpen = item.menu.hidden;
|
|
798
|
+
}
|
|
799
|
+
for (const item of studioUiRefreshUi.menus) {
|
|
800
|
+
const isOpen = willOpen && item.name === name;
|
|
801
|
+
item.menu.hidden = !isOpen;
|
|
802
|
+
item.button.classList.toggle("is-open", isOpen);
|
|
803
|
+
item.button.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function syncStudioUiRefreshReviewTrigger() {
|
|
808
|
+
if (!studioUiRefreshUi || !studioUiRefreshUi.reviewButton) return;
|
|
809
|
+
const critiqueIsStop = getAbortablePendingKind() === "critique";
|
|
810
|
+
const reviewButton = studioUiRefreshUi.reviewButton;
|
|
811
|
+
reviewButton.textContent = critiqueIsStop ? "Stop critique" : "Review";
|
|
812
|
+
reviewButton.classList.toggle("request-stop-active", critiqueIsStop);
|
|
813
|
+
reviewButton.disabled = critiqueIsStop ? Boolean(critiqueBtn && critiqueBtn.disabled) : false;
|
|
814
|
+
reviewButton.title = critiqueIsStop
|
|
815
|
+
? "Stop the running critique request. Shortcut: Esc."
|
|
816
|
+
: "Open review actions and settings.";
|
|
817
|
+
if (critiqueIsStop) {
|
|
818
|
+
closeStudioUiRefreshMenus();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function makeStudioUiRefreshMenu(buttonEl, name, menuClassName) {
|
|
823
|
+
const anchorEl = makeStudioUiRefreshElement("span", "studio-refresh-menu-anchor " + (menuClassName || ""));
|
|
824
|
+
const menuEl = makeStudioUiRefreshElement("div", "studio-refresh-menu");
|
|
825
|
+
menuEl.hidden = true;
|
|
826
|
+
buttonEl.type = "button";
|
|
827
|
+
buttonEl.classList.add("studio-refresh-chip");
|
|
828
|
+
buttonEl.setAttribute("aria-haspopup", "menu");
|
|
829
|
+
buttonEl.setAttribute("aria-expanded", "false");
|
|
830
|
+
buttonEl.addEventListener("click", (event) => {
|
|
831
|
+
event.stopPropagation();
|
|
832
|
+
if (name === "review" && getAbortablePendingKind() === "critique") {
|
|
833
|
+
requestCancelForPendingRequest("critique");
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
toggleStudioUiRefreshMenu(name);
|
|
837
|
+
});
|
|
838
|
+
anchorEl.appendChild(buttonEl);
|
|
839
|
+
anchorEl.appendChild(menuEl);
|
|
840
|
+
return { name, anchor: anchorEl, button: buttonEl, menu: menuEl };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function setStudioUiRefreshPreference(enabled) {
|
|
844
|
+
try {
|
|
845
|
+
window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
|
|
846
|
+
} catch {}
|
|
847
|
+
try {
|
|
848
|
+
const url = new URL(window.location.href);
|
|
849
|
+
url.searchParams.set("uiRefresh", enabled ? "1" : "0");
|
|
850
|
+
window.location.assign(url.toString());
|
|
851
|
+
} catch {
|
|
852
|
+
window.location.reload();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function setupStudioUiRefreshToggleButton() {
|
|
857
|
+
if (!footerMetaEl || document.getElementById("studioUiRefreshToggleBtn")) return;
|
|
858
|
+
const button = makeStudioUiRefreshElement("button", "footer-compact-btn studio-ui-refresh-toggle", studioUiRefreshEnabled ? "UI: Fresh" : "UI: Classic");
|
|
859
|
+
button.id = "studioUiRefreshToggleBtn";
|
|
860
|
+
button.type = "button";
|
|
861
|
+
button.title = studioUiRefreshEnabled
|
|
862
|
+
? "Switch Studio to the classic layout."
|
|
863
|
+
: "Switch Studio to the refreshed layout.";
|
|
864
|
+
button.addEventListener("click", () => {
|
|
865
|
+
setStudioUiRefreshPreference(!studioUiRefreshEnabled);
|
|
866
|
+
});
|
|
867
|
+
if (compactBtn && compactBtn.parentNode === footerMetaEl) {
|
|
868
|
+
compactBtn.insertAdjacentElement("afterend", button);
|
|
869
|
+
} else {
|
|
870
|
+
footerMetaEl.appendChild(button);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function setupStudioUiRefreshPrototype() {
|
|
875
|
+
if (!studioUiRefreshEnabled || studioUiRefreshUi) return;
|
|
876
|
+
const leftHeaderEl = document.getElementById("leftSectionHeader");
|
|
877
|
+
const sourceMetaEl = leftPaneEl ? leftPaneEl.querySelector(".source-meta") : null;
|
|
878
|
+
if (!leftHeaderEl || !sourceMetaEl || !copyDraftBtn) return;
|
|
879
|
+
|
|
880
|
+
let reviewMenu = null;
|
|
881
|
+
if (!isEditorOnlyMode && critiqueBtn && lensSelect) {
|
|
882
|
+
const reviewButton = makeStudioUiRefreshElement("button", "studio-refresh-tool-tab studio-refresh-review-btn", "Review");
|
|
883
|
+
reviewMenu = makeStudioUiRefreshMenu(reviewButton, "review", "studio-refresh-review-anchor");
|
|
884
|
+
appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn]);
|
|
885
|
+
appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const headerTopEl = makeStudioUiRefreshElement("div", "studio-refresh-header-top");
|
|
889
|
+
const titleGroupEl = makeStudioUiRefreshElement("div", "studio-refresh-title-group");
|
|
890
|
+
if (leftFocusBtn) {
|
|
891
|
+
setStudioUiRefreshFocusButtonIcon(leftFocusBtn, false);
|
|
892
|
+
titleGroupEl.appendChild(leftFocusBtn);
|
|
893
|
+
}
|
|
894
|
+
titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
895
|
+
if (isEditorOnlyMode) {
|
|
896
|
+
titleGroupEl.appendChild(makeStudioUiRefreshElement("span", "studio-refresh-static-title", "Editor (Raw)"));
|
|
897
|
+
} else if (editorViewSelect) {
|
|
898
|
+
titleGroupEl.appendChild(editorViewSelect);
|
|
899
|
+
}
|
|
900
|
+
headerTopEl.appendChild(titleGroupEl);
|
|
901
|
+
const headerToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
|
|
902
|
+
if (reviewNotesBtn) headerToolsEl.appendChild(reviewNotesBtn);
|
|
903
|
+
if (outlineBtn) headerToolsEl.appendChild(outlineBtn);
|
|
904
|
+
if (scratchpadBtn) headerToolsEl.appendChild(scratchpadBtn);
|
|
905
|
+
if (reviewMenu) headerToolsEl.appendChild(reviewMenu.anchor);
|
|
906
|
+
headerTopEl.appendChild(headerToolsEl);
|
|
907
|
+
|
|
908
|
+
const headerUtilityEl = makeStudioUiRefreshElement("div", "studio-refresh-header-utility");
|
|
909
|
+
const utilityLeftEl = makeStudioUiRefreshElement("div", "studio-refresh-utility-left");
|
|
910
|
+
if (sourceBadgeEl) utilityLeftEl.appendChild(sourceBadgeEl);
|
|
911
|
+
if (sourceBadgeEl && (resourceDirBtn || resourceDirLabel || resourceDirInputWrap || syncBadgeEl)) {
|
|
912
|
+
utilityLeftEl.appendChild(makeStudioUiRefreshSeparator());
|
|
913
|
+
}
|
|
914
|
+
if (resourceDirBtn) utilityLeftEl.appendChild(resourceDirBtn);
|
|
915
|
+
if (resourceDirLabel) utilityLeftEl.appendChild(resourceDirLabel);
|
|
916
|
+
if (resourceDirInputWrap) utilityLeftEl.appendChild(resourceDirInputWrap);
|
|
917
|
+
if (syncBadgeEl) utilityLeftEl.appendChild(syncBadgeEl);
|
|
918
|
+
headerUtilityEl.appendChild(utilityLeftEl);
|
|
919
|
+
leftHeaderEl.replaceChildren(headerTopEl, headerUtilityEl);
|
|
920
|
+
|
|
921
|
+
const rightHeaderEl = document.getElementById("rightSectionHeader");
|
|
922
|
+
if (rightHeaderEl && rightViewSelect) {
|
|
923
|
+
const rightIdentityEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-identity studio-refresh-pane-identity-right");
|
|
924
|
+
const rightTitleGroupEl = makeStudioUiRefreshElement("div", "studio-refresh-title-group");
|
|
925
|
+
if (rightFocusBtn) {
|
|
926
|
+
setStudioUiRefreshFocusButtonIcon(rightFocusBtn, false);
|
|
927
|
+
rightTitleGroupEl.appendChild(rightFocusBtn);
|
|
928
|
+
rightTitleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
929
|
+
}
|
|
930
|
+
if (isEditorOnlyMode) {
|
|
931
|
+
rightTitleGroupEl.appendChild(makeStudioUiRefreshElement("span", "studio-refresh-static-title", "Editor (Preview)"));
|
|
932
|
+
} else {
|
|
933
|
+
rightTitleGroupEl.appendChild(rightViewSelect);
|
|
934
|
+
}
|
|
935
|
+
rightIdentityEl.appendChild(rightTitleGroupEl);
|
|
936
|
+
const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
|
|
937
|
+
if (exportPdfBtn) rightToolsEl.appendChild(exportPdfBtn);
|
|
938
|
+
rightHeaderEl.replaceChildren(rightIdentityEl, rightToolsEl);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const toolbarEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar");
|
|
942
|
+
const toolbarMainEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-main");
|
|
943
|
+
const actionsEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-actions");
|
|
944
|
+
const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
|
|
945
|
+
if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
|
|
946
|
+
if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
|
|
947
|
+
const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
|
|
948
|
+
actionLineTwoEl.appendChild(copyDraftBtn);
|
|
949
|
+
if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
|
|
950
|
+
if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
|
|
951
|
+
actionsEl.appendChild(actionLineTwoEl);
|
|
952
|
+
|
|
953
|
+
const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
|
|
954
|
+
const annotationsButton = makeStudioUiRefreshElement("button", "", "Annotations");
|
|
955
|
+
const annotationsMenu = makeStudioUiRefreshMenu(annotationsButton, "annotations", "studio-refresh-annotations-anchor");
|
|
956
|
+
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display", isEditorOnlyMode ? [annotationModeSelect] : [annotationModeSelect, insertHeaderBtn]);
|
|
957
|
+
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Actions", [stripAnnotationsBtn, saveAnnotatedBtn]);
|
|
958
|
+
const viewButton = makeStudioUiRefreshElement("button", "", "View");
|
|
959
|
+
const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
|
|
960
|
+
appendStudioUiRefreshMenuSection(viewMenu.menu, "Display", [highlightSelect, lineNumbersSelect]);
|
|
961
|
+
stateEl.appendChild(annotationsMenu.anchor);
|
|
962
|
+
stateEl.appendChild(viewMenu.anchor);
|
|
963
|
+
|
|
964
|
+
toolbarMainEl.appendChild(actionsEl);
|
|
965
|
+
toolbarMainEl.appendChild(stateEl);
|
|
966
|
+
toolbarEl.appendChild(toolbarMainEl);
|
|
967
|
+
sourceMetaEl.replaceChildren(toolbarEl);
|
|
968
|
+
|
|
969
|
+
studioUiRefreshUi = {
|
|
970
|
+
annotationsButton,
|
|
971
|
+
viewButton,
|
|
972
|
+
reviewButton: reviewMenu ? reviewMenu.button : null,
|
|
973
|
+
menus: [annotationsMenu, viewMenu].concat(reviewMenu ? [reviewMenu] : []),
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
document.addEventListener("click", (event) => {
|
|
977
|
+
const target = event.target;
|
|
978
|
+
if (target instanceof Element && target.closest(".studio-refresh-menu-anchor")) return;
|
|
979
|
+
closeStudioUiRefreshMenus();
|
|
980
|
+
});
|
|
981
|
+
document.addEventListener("keydown", (event) => {
|
|
982
|
+
if (event.key === "Escape") closeStudioUiRefreshMenus();
|
|
983
|
+
});
|
|
984
|
+
toolbarEl.addEventListener("change", () => {
|
|
985
|
+
window.setTimeout(syncStudioUiRefreshSummaries, 0);
|
|
986
|
+
});
|
|
987
|
+
toolbarEl.addEventListener("click", (event) => {
|
|
988
|
+
const target = event.target;
|
|
989
|
+
if (target instanceof Element && target.closest(".studio-refresh-menu")) {
|
|
990
|
+
window.setTimeout(syncStudioUiRefreshSummaries, 0);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
syncStudioUiRefreshSummaries();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
setupStudioUiRefreshToggleButton();
|
|
997
|
+
setupStudioUiRefreshPrototype();
|
|
665
998
|
const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
|
|
666
999
|
if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
|
|
667
1000
|
throw new Error("Studio annotation helpers failed to load.");
|
|
@@ -932,7 +1265,24 @@
|
|
|
932
1265
|
}
|
|
933
1266
|
}
|
|
934
1267
|
|
|
935
|
-
function
|
|
1268
|
+
function formatCompactNumber(value) {
|
|
1269
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "?";
|
|
1270
|
+
const sign = value < 0 ? "-" : "";
|
|
1271
|
+
const abs = Math.abs(value);
|
|
1272
|
+
if (abs < 1000) return sign + formatNumber(abs);
|
|
1273
|
+
const units = [
|
|
1274
|
+
{ divisor: 1_000_000_000, suffix: "B" },
|
|
1275
|
+
{ divisor: 1_000_000, suffix: "M" },
|
|
1276
|
+
{ divisor: 1_000, suffix: "k" },
|
|
1277
|
+
];
|
|
1278
|
+
const unit = units.find((entry) => abs >= entry.divisor) || units[units.length - 1];
|
|
1279
|
+
const scaled = abs / unit.divisor;
|
|
1280
|
+
const decimals = scaled >= 100 ? 0 : 1;
|
|
1281
|
+
return sign + scaled.toFixed(decimals).replace(/\.0$/, "") + unit.suffix;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function formatContextUsageText(compact) {
|
|
1285
|
+
const formatContextNumber = compact ? formatCompactNumber : formatNumber;
|
|
936
1286
|
const hasWindow = typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0;
|
|
937
1287
|
const hasTokens = typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens >= 0;
|
|
938
1288
|
let percentValue = typeof contextPercent === "number" && Number.isFinite(contextPercent)
|
|
@@ -947,12 +1297,12 @@
|
|
|
947
1297
|
return "Context: unknown";
|
|
948
1298
|
}
|
|
949
1299
|
if (!hasTokens && hasWindow) {
|
|
950
|
-
return "Context: ? / " +
|
|
1300
|
+
return "Context: ? / " + formatContextNumber(contextWindow);
|
|
951
1301
|
}
|
|
952
1302
|
|
|
953
|
-
let text = "Context: " +
|
|
1303
|
+
let text = "Context: " + formatContextNumber(contextTokens);
|
|
954
1304
|
if (hasWindow) {
|
|
955
|
-
text += " / " +
|
|
1305
|
+
text += " / " + formatContextNumber(contextWindow);
|
|
956
1306
|
}
|
|
957
1307
|
if (percentValue != null && Number.isFinite(percentValue)) {
|
|
958
1308
|
const bounded = Math.max(0, Math.min(100, percentValue));
|
|
@@ -1182,14 +1532,16 @@
|
|
|
1182
1532
|
function updateFooterMeta() {
|
|
1183
1533
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
1184
1534
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
1185
|
-
const contextText = formatContextUsageText();
|
|
1535
|
+
const contextText = formatContextUsageText(true);
|
|
1536
|
+
const contextTitleText = formatContextUsageText(false);
|
|
1186
1537
|
const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
|
|
1538
|
+
const titleText = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextTitleText;
|
|
1187
1539
|
if (footerMetaTextEl) {
|
|
1188
1540
|
footerMetaTextEl.textContent = text;
|
|
1189
|
-
footerMetaTextEl.title =
|
|
1541
|
+
footerMetaTextEl.title = titleText;
|
|
1190
1542
|
} else if (footerMetaEl) {
|
|
1191
1543
|
footerMetaEl.textContent = text;
|
|
1192
|
-
footerMetaEl.title =
|
|
1544
|
+
footerMetaEl.title = titleText;
|
|
1193
1545
|
}
|
|
1194
1546
|
updateDocumentTitle();
|
|
1195
1547
|
}
|
|
@@ -1301,7 +1653,7 @@
|
|
|
1301
1653
|
|
|
1302
1654
|
function updateSourceBadge() {
|
|
1303
1655
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
1304
|
-
sourceBadgeEl.textContent = "Editor origin: " + label;
|
|
1656
|
+
sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label;
|
|
1305
1657
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
1306
1658
|
if (sourceBadgeEl) {
|
|
1307
1659
|
sourceBadgeEl.title = descriptor.fileBacked
|
|
@@ -1362,6 +1714,9 @@
|
|
|
1362
1714
|
btn.classList.toggle("is-active", isFocusedPane);
|
|
1363
1715
|
btn.setAttribute("aria-pressed", isFocusedPane ? "true" : "false");
|
|
1364
1716
|
btn.textContent = isFocusedPane ? "Exit focus" : "Focus pane";
|
|
1717
|
+
if (studioUiRefreshEnabled) {
|
|
1718
|
+
setStudioUiRefreshFocusButtonIcon(btn, isFocusedPane);
|
|
1719
|
+
}
|
|
1365
1720
|
btn.title = isFocusedPane
|
|
1366
1721
|
? "Return to the two-pane layout. Shortcut: F10 or Cmd/Ctrl+Esc."
|
|
1367
1722
|
: "Show only the " + paneName + " pane. Shortcut: F10 or Cmd/Ctrl+Esc.";
|
|
@@ -3849,6 +4204,7 @@
|
|
|
3849
4204
|
if (lineNumbersSelect) {
|
|
3850
4205
|
lineNumbersSelect.value = lineNumbersEnabled ? "on" : "off";
|
|
3851
4206
|
}
|
|
4207
|
+
syncStudioUiRefreshSummaries();
|
|
3852
4208
|
updateLineNumberGutterVisibility();
|
|
3853
4209
|
scheduleEditorLineNumberRender();
|
|
3854
4210
|
if (editorHighlightEnabled && editorView === "markdown") {
|
|
@@ -4537,6 +4893,7 @@
|
|
|
4537
4893
|
if (stripAnnotationsBtn) {
|
|
4538
4894
|
stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
4539
4895
|
}
|
|
4896
|
+
syncStudioUiRefreshSummaries();
|
|
4540
4897
|
}
|
|
4541
4898
|
|
|
4542
4899
|
function scheduleEditorMetaUpdate() {
|
|
@@ -5412,6 +5769,87 @@
|
|
|
5412
5769
|
};
|
|
5413
5770
|
}
|
|
5414
5771
|
|
|
5772
|
+
function getDiffFileLabelForLine(source, lineNumber) {
|
|
5773
|
+
const lines = String(source || "").replace(/\r\n/g, "\n").split("\n");
|
|
5774
|
+
const safeLine = Math.max(1, Math.min(Math.floor(Number(lineNumber) || 1), Math.max(1, lines.length)));
|
|
5775
|
+
let currentFile = "";
|
|
5776
|
+
for (let i = 0; i < safeLine; i += 1) {
|
|
5777
|
+
const line = String(lines[i] || "");
|
|
5778
|
+
const diffMatch = line.match(/^diff --git\s+a\/(.+?)\s+b\/(.+?)\s*$/);
|
|
5779
|
+
if (diffMatch) {
|
|
5780
|
+
currentFile = diffMatch[2] || diffMatch[1] || currentFile;
|
|
5781
|
+
continue;
|
|
5782
|
+
}
|
|
5783
|
+
const plusMatch = line.match(/^\+\+\+\s+(?:b\/)?(.+)\s*$/);
|
|
5784
|
+
if (plusMatch && plusMatch[1] && plusMatch[1] !== "/dev/null") {
|
|
5785
|
+
currentFile = plusMatch[1];
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
return currentFile.trim();
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
function getReviewNotePromptFileLabel(note, source) {
|
|
5792
|
+
if (sourceState && sourceState.path) return String(sourceState.path);
|
|
5793
|
+
const bounds = getResolvedReviewNoteLineBounds(note, source);
|
|
5794
|
+
const diffFile = bounds ? getDiffFileLabelForLine(source, bounds.lineStart) : "";
|
|
5795
|
+
if (diffFile) return diffFile;
|
|
5796
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
5797
|
+
return descriptor && descriptor.fileBacked ? descriptor.label : "";
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
function formatReviewNotePromptLineRange(bounds, note) {
|
|
5801
|
+
const start = bounds ? bounds.lineStart : Math.max(1, Number(note && note.lineStart) || 1);
|
|
5802
|
+
const end = bounds ? bounds.lineEnd : Math.max(start, Number(note && note.lineEnd) || start);
|
|
5803
|
+
return start === end ? "L" + start : ("L" + start + "-L" + end);
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
function buildReviewNotesPrompt() {
|
|
5807
|
+
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5808
|
+
const notes = getDisplayReviewNotes().filter((note) => String(note && note.text ? note.text : "").trim());
|
|
5809
|
+
if (!notes.length) return "";
|
|
5810
|
+
|
|
5811
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
5812
|
+
const documentLabel = descriptor && descriptor.label ? descriptor.label : (sourceState && sourceState.label ? sourceState.label : "Studio document");
|
|
5813
|
+
const parts = [
|
|
5814
|
+
"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.",
|
|
5815
|
+
"Document: " + documentLabel,
|
|
5816
|
+
"",
|
|
5817
|
+
"## Comments",
|
|
5818
|
+
];
|
|
5819
|
+
|
|
5820
|
+
notes.forEach((note, index) => {
|
|
5821
|
+
const bounds = getResolvedReviewNoteLineBounds(note, source);
|
|
5822
|
+
const fileLabel = getReviewNotePromptFileLabel(note, source);
|
|
5823
|
+
const location = (fileLabel ? (fileLabel + ":") : "") + formatReviewNotePromptLineRange(bounds, note);
|
|
5824
|
+
const comment = String(note && note.text ? note.text : "").trim();
|
|
5825
|
+
const anchor = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
|
|
5826
|
+
.replace(/\s+/g, " ")
|
|
5827
|
+
.trim();
|
|
5828
|
+
parts.push(
|
|
5829
|
+
"### Comment " + (index + 1) + " — " + location,
|
|
5830
|
+
"",
|
|
5831
|
+
comment,
|
|
5832
|
+
);
|
|
5833
|
+
if (anchor) {
|
|
5834
|
+
parts.push("", "> " + anchor.replace(/\n/g, "\n> "));
|
|
5835
|
+
}
|
|
5836
|
+
parts.push("");
|
|
5837
|
+
});
|
|
5838
|
+
|
|
5839
|
+
return parts.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
5840
|
+
}
|
|
5841
|
+
|
|
5842
|
+
function loadReviewNotesPromptIntoEditor() {
|
|
5843
|
+
const prompt = buildReviewNotesPrompt();
|
|
5844
|
+
if (!prompt.trim()) {
|
|
5845
|
+
setStatus("No non-empty comments to load as a prompt.", "warning");
|
|
5846
|
+
return;
|
|
5847
|
+
}
|
|
5848
|
+
setEditorText(prompt, { preserveScroll: false, preserveSelection: false });
|
|
5849
|
+
setSourceState({ source: "blank", label: "comments prompt", path: null });
|
|
5850
|
+
setStatus("Loaded comments prompt into editor.", "success");
|
|
5851
|
+
}
|
|
5852
|
+
|
|
5415
5853
|
function buildReviewNoteLineMap(text) {
|
|
5416
5854
|
const source = String(text || "");
|
|
5417
5855
|
const lineMap = new Map();
|
|
@@ -8235,12 +8673,19 @@
|
|
|
8235
8673
|
? "Select preview text and use Comment for a local preview-anchored comment."
|
|
8236
8674
|
: "Switch to Editor (Raw) to comment on the current line.");
|
|
8237
8675
|
}
|
|
8676
|
+
if (reviewNotesPromptBtn) {
|
|
8677
|
+
const promptCandidates = reviewNotes.filter((note) => String(note && note.text ? note.text : "").trim());
|
|
8678
|
+
reviewNotesPromptBtn.disabled = uiBusy || promptCandidates.length === 0;
|
|
8679
|
+
reviewNotesPromptBtn.title = promptCandidates.length > 0
|
|
8680
|
+
? "Load local comments, line numbers, and file labels into the editor as a prompt."
|
|
8681
|
+
: "No non-empty local comments to load as a prompt.";
|
|
8682
|
+
}
|
|
8238
8683
|
if (reviewNotesInlineAllBtn) {
|
|
8239
8684
|
const currentText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
8240
8685
|
const toggleCandidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, currentText).canToggle);
|
|
8241
8686
|
const allInline = toggleCandidates.length > 0 && toggleCandidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
|
|
8242
8687
|
reviewNotesInlineAllBtn.disabled = uiBusy || toggleCandidates.length === 0;
|
|
8243
|
-
reviewNotesInlineAllBtn.textContent = allInline ? "
|
|
8688
|
+
reviewNotesInlineAllBtn.textContent = allInline ? "Inline: On" : "Inline: Off";
|
|
8244
8689
|
reviewNotesInlineAllBtn.setAttribute("aria-pressed", allInline ? "true" : "false");
|
|
8245
8690
|
reviewNotesInlineAllBtn.title = allInline
|
|
8246
8691
|
? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
|
|
@@ -8870,11 +9315,13 @@
|
|
|
8870
9315
|
if (!highlightSelect) return;
|
|
8871
9316
|
if (!editorHighlightEnabled) {
|
|
8872
9317
|
highlightSelect.value = "off";
|
|
9318
|
+
syncStudioUiRefreshSummaries();
|
|
8873
9319
|
return;
|
|
8874
9320
|
}
|
|
8875
9321
|
highlightSelect.value = (editorLanguage && SUPPORTED_LANGUAGES.indexOf(editorLanguage) !== -1)
|
|
8876
9322
|
? editorLanguage
|
|
8877
9323
|
: "markdown";
|
|
9324
|
+
syncStudioUiRefreshSummaries();
|
|
8878
9325
|
}
|
|
8879
9326
|
|
|
8880
9327
|
function setEditorHighlightEnabled(enabled) {
|
|
@@ -8977,6 +9424,7 @@
|
|
|
8977
9424
|
critiqueBtn.disabled = true;
|
|
8978
9425
|
critiqueBtn.title = "Critique is unavailable in editor-only mode.";
|
|
8979
9426
|
}
|
|
9427
|
+
syncStudioUiRefreshReviewTrigger();
|
|
8980
9428
|
return;
|
|
8981
9429
|
}
|
|
8982
9430
|
|
|
@@ -9014,6 +9462,7 @@
|
|
|
9014
9462
|
? "Critique text as-is (includes [an: ...] markers)."
|
|
9015
9463
|
: "Critique text with [an: ...] markers stripped."));
|
|
9016
9464
|
}
|
|
9465
|
+
syncStudioUiRefreshReviewTrigger();
|
|
9017
9466
|
}
|
|
9018
9467
|
|
|
9019
9468
|
function updateAnnotationModeUi() {
|
|
@@ -9024,6 +9473,7 @@
|
|
|
9024
9473
|
: "Inline annotations Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.";
|
|
9025
9474
|
}
|
|
9026
9475
|
|
|
9476
|
+
syncStudioUiRefreshSummaries();
|
|
9027
9477
|
syncRunAndCritiqueButtons();
|
|
9028
9478
|
}
|
|
9029
9479
|
|
|
@@ -9362,6 +9812,7 @@
|
|
|
9362
9812
|
setBusy(false);
|
|
9363
9813
|
setWsState("Ready");
|
|
9364
9814
|
|
|
9815
|
+
pendingResponseScrollReset = true;
|
|
9365
9816
|
let appliedFromHistory = false;
|
|
9366
9817
|
if (Array.isArray(message.responseHistory)) {
|
|
9367
9818
|
appliedFromHistory = setResponseHistory(message.responseHistory, {
|
|
@@ -9390,6 +9841,9 @@
|
|
|
9390
9841
|
if (pendingRequestId) return;
|
|
9391
9842
|
|
|
9392
9843
|
const hasHistory = Array.isArray(message.responseHistory);
|
|
9844
|
+
if (followLatest) {
|
|
9845
|
+
pendingResponseScrollReset = true;
|
|
9846
|
+
}
|
|
9393
9847
|
if (hasHistory) {
|
|
9394
9848
|
setResponseHistory(message.responseHistory, {
|
|
9395
9849
|
autoSelectLatest: followLatest,
|
|
@@ -9413,7 +9867,7 @@
|
|
|
9413
9867
|
return;
|
|
9414
9868
|
}
|
|
9415
9869
|
|
|
9416
|
-
if (!hasHistory && applyLatestPayload(payload)) {
|
|
9870
|
+
if (!hasHistory && applyLatestPayload(payload, { resetScroll: true })) {
|
|
9417
9871
|
queuedLatestResponse = null;
|
|
9418
9872
|
updateResultActionButtons();
|
|
9419
9873
|
setStatus("Updated from latest response.", "success");
|
|
@@ -9909,10 +10363,12 @@
|
|
|
9909
10363
|
if (hasHeader) {
|
|
9910
10364
|
insertHeaderBtn.textContent = "Annotation header: On";
|
|
9911
10365
|
insertHeaderBtn.title = "Remove annotated-reply protocol header while keeping body text.";
|
|
10366
|
+
syncStudioUiRefreshSummaries();
|
|
9912
10367
|
return;
|
|
9913
10368
|
}
|
|
9914
10369
|
insertHeaderBtn.textContent = "Annotation header: Off";
|
|
9915
10370
|
insertHeaderBtn.title = "Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).";
|
|
10371
|
+
syncStudioUiRefreshSummaries();
|
|
9916
10372
|
}
|
|
9917
10373
|
|
|
9918
10374
|
function toggleAnnotatedReplyHeader() {
|
|
@@ -10581,6 +11037,12 @@
|
|
|
10581
11037
|
});
|
|
10582
11038
|
}
|
|
10583
11039
|
|
|
11040
|
+
if (reviewNotesPromptBtn) {
|
|
11041
|
+
reviewNotesPromptBtn.addEventListener("click", () => {
|
|
11042
|
+
loadReviewNotesPromptIntoEditor();
|
|
11043
|
+
});
|
|
11044
|
+
}
|
|
11045
|
+
|
|
10584
11046
|
if (reviewNotesInlineAllBtn) {
|
|
10585
11047
|
reviewNotesInlineAllBtn.addEventListener("click", () => {
|
|
10586
11048
|
toggleAllReviewNotesInlineAnnotations();
|