pi-studio 0.5.57 → 0.5.59

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.
@@ -133,6 +133,7 @@
133
133
  const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
134
134
  const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
135
135
  const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
136
+ const reviewNotesDeleteAllBtn = document.getElementById("reviewNotesDeleteAllBtn");
136
137
  const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
137
138
  const reviewNotesDoneBtn = document.getElementById("reviewNotesDoneBtn");
138
139
 
@@ -459,16 +460,92 @@
459
460
  return "working";
460
461
  }
461
462
 
463
+ async function writeTextToClipboard(text) {
464
+ const content = String(text || "");
465
+
466
+ try {
467
+ await fetchStudioJson("/clipboard", {
468
+ method: "POST",
469
+ body: JSON.stringify({ text: content }),
470
+ });
471
+ return true;
472
+ } catch {
473
+ // Fall back to browser clipboard APIs. The server-side clipboard path
474
+ // is most reliable for local Studio, but may be unavailable over SSH
475
+ // or on systems without a clipboard command.
476
+ }
477
+
478
+ // Prefer a copy-event payload first. It runs synchronously inside the
479
+ // user's click gesture and avoids browser quirks where copying a hidden
480
+ // textarea reports success but leaves the system clipboard unchanged.
481
+ if (document.execCommand && typeof document.addEventListener === "function") {
482
+ let handled = false;
483
+ const handleCopy = (event) => {
484
+ if (!event || !event.clipboardData) return;
485
+ event.clipboardData.setData("text/plain", content);
486
+ event.preventDefault();
487
+ handled = true;
488
+ };
489
+ try {
490
+ document.addEventListener("copy", handleCopy, true);
491
+ const ok = document.execCommand("copy");
492
+ if (ok && handled) return true;
493
+ } catch {
494
+ // Fall through to the other clipboard paths.
495
+ } finally {
496
+ document.removeEventListener("copy", handleCopy, true);
497
+ }
498
+ }
499
+
500
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
501
+ try {
502
+ await navigator.clipboard.writeText(content);
503
+ return true;
504
+ } catch {
505
+ // Fall through to the selection-based legacy path.
506
+ }
507
+ }
508
+
509
+ const textarea = document.createElement("textarea");
510
+ textarea.value = content;
511
+ textarea.setAttribute("readonly", "");
512
+ textarea.style.position = "fixed";
513
+ textarea.style.top = "0";
514
+ textarea.style.left = "0";
515
+ textarea.style.width = "1px";
516
+ textarea.style.height = "1px";
517
+ textarea.style.opacity = "0";
518
+ document.body.appendChild(textarea);
519
+ const activeEl = document.activeElement;
520
+ textarea.focus();
521
+ textarea.select();
522
+ textarea.setSelectionRange(0, textarea.value.length);
523
+ let ok = false;
524
+ try {
525
+ ok = document.execCommand && document.execCommand("copy");
526
+ } catch {
527
+ ok = false;
528
+ }
529
+ textarea.remove();
530
+ if (activeEl && typeof activeEl.focus === "function") {
531
+ try {
532
+ activeEl.focus();
533
+ } catch {
534
+ // Ignore focus restore failures.
535
+ }
536
+ }
537
+ return Boolean(ok);
538
+ }
539
+
462
540
  async function copyVisibleWorkingToClipboard() {
463
541
  const content = buildVisibleWorkingText();
464
542
  if (!content.trim()) {
465
543
  setStatus("No visible working details to copy yet.", "warning");
466
544
  return;
467
545
  }
468
- try {
469
- await navigator.clipboard.writeText(content);
546
+ if (await writeTextToClipboard(content)) {
470
547
  setStatus("Copied visible working text.", "success");
471
- } catch {
548
+ } else {
472
549
  setStatus("Clipboard write failed.", "warning");
473
550
  }
474
551
  }
@@ -567,6 +644,12 @@
567
644
  let lineNumbersEnabled = false;
568
645
  let lineNumbersRenderRaf = null;
569
646
  let annotationsEnabled = true;
647
+ const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
648
+ const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
649
+ let studioUiRefreshUi = null;
650
+ if (studioUiRefreshEnabled && document.body) {
651
+ document.body.classList.add("studio-ui-refresh");
652
+ }
570
653
  let scratchpadText = "";
571
654
  let scratchpadReturnFocusEl = null;
572
655
  let scratchpadPersistTimer = null;
@@ -585,6 +668,333 @@
585
668
  let suppressedEditorSelectionEnd = null;
586
669
  const previewJumpHighlightState = new WeakMap();
587
670
  const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
671
+
672
+ function readStudioUiRefreshEnabled() {
673
+ const normalize = (value) => String(value == null ? "" : value).trim().toLowerCase();
674
+ const queryValue = initialQueryParams.has("uiRefresh")
675
+ ? initialQueryParams.get("uiRefresh")
676
+ : (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;
679
+ if (queryValue !== null) {
680
+ const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalize(queryValue) !== "");
681
+ try {
682
+ if (enabled) window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, "1");
683
+ else window.localStorage && window.localStorage.removeItem(STUDIO_UI_REFRESH_STORAGE_KEY);
684
+ } catch {}
685
+ return enabled;
686
+ }
687
+ try {
688
+ return Boolean(window.localStorage && window.localStorage.getItem(STUDIO_UI_REFRESH_STORAGE_KEY) === "1");
689
+ } catch {
690
+ return false;
691
+ }
692
+ }
693
+
694
+ function makeStudioUiRefreshElement(tagName, className, text) {
695
+ const element = document.createElement(tagName);
696
+ if (className) element.className = className;
697
+ if (typeof text === "string") element.textContent = text;
698
+ return element;
699
+ }
700
+
701
+ function makeStudioUiRefreshSeparator() {
702
+ return makeStudioUiRefreshElement("span", "studio-refresh-sep");
703
+ }
704
+
705
+ function makeStudioUiRefreshIcon(kind) {
706
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
707
+ svg.setAttribute("viewBox", "0 0 24 24");
708
+ svg.setAttribute("aria-hidden", "true");
709
+ svg.classList.add("studio-refresh-icon");
710
+ const paths = kind === "focus-exit"
711
+ ? ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"]
712
+ : ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
713
+ for (const d of paths) {
714
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
715
+ path.setAttribute("d", d);
716
+ svg.appendChild(path);
717
+ }
718
+ return svg;
719
+ }
720
+
721
+ function setStudioUiRefreshFocusButtonIcon(buttonEl, isFocusedPane) {
722
+ if (!buttonEl || !studioUiRefreshEnabled) return;
723
+ buttonEl.replaceChildren(makeStudioUiRefreshIcon(isFocusedPane ? "focus-exit" : "focus"));
724
+ buttonEl.setAttribute("aria-label", isFocusedPane ? "Exit focus" : "Focus pane");
725
+ }
726
+
727
+ function appendStudioUiRefreshMenuSection(menuEl, heading, controls) {
728
+ const sectionEl = makeStudioUiRefreshElement("div", "studio-refresh-menu-section");
729
+ if (heading) {
730
+ sectionEl.appendChild(makeStudioUiRefreshElement("div", "studio-refresh-menu-heading", heading));
731
+ }
732
+ for (const control of controls) {
733
+ if (!control) continue;
734
+ const itemEl = makeStudioUiRefreshElement("div", "studio-refresh-menu-item");
735
+ itemEl.appendChild(control);
736
+ sectionEl.appendChild(itemEl);
737
+ }
738
+ menuEl.appendChild(sectionEl);
739
+ }
740
+
741
+ function getStudioUiRefreshSelectSummary(selectEl, prefix) {
742
+ if (!selectEl) return "";
743
+ const option = selectEl.options && selectEl.selectedIndex >= 0 ? selectEl.options[selectEl.selectedIndex] : null;
744
+ let label = option ? String(option.textContent || option.label || option.value || "") : String(selectEl.value || "");
745
+ if (prefix) label = label.replace(new RegExp("^" + prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*:?\\s*", "i"), "");
746
+ return label.trim();
747
+ }
748
+
749
+ function setStudioUiRefreshButtonText(buttonEl, text) {
750
+ if (!buttonEl) return;
751
+ buttonEl.textContent = text;
752
+ }
753
+
754
+ function getStudioUiRefreshAnnotationHeaderEnabled() {
755
+ try {
756
+ return Boolean(stripAnnotationHeader(sourceTextEl.value).hadHeader);
757
+ } catch {
758
+ return false;
759
+ }
760
+ }
761
+
762
+ function syncStudioUiRefreshSummaries() {
763
+ if (!studioUiRefreshUi) return;
764
+ if (studioUiRefreshUi.annotationsButton) {
765
+ const inlineLabel = annotationsEnabled ? "Inline on" : "Inline hidden";
766
+ if (isEditorOnlyMode) {
767
+ setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel);
768
+ } else {
769
+ const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
770
+ setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
771
+ }
772
+ }
773
+ if (studioUiRefreshUi.viewButton) {
774
+ const syntaxLabel = editorHighlightEnabled
775
+ ? (getStudioUiRefreshSelectSummary(highlightSelect, "Syntax highlight") || editorLanguage || "Markdown")
776
+ : "Off";
777
+ const lineLabel = lineNumbersEnabled ? "Lines on" : "Lines off";
778
+ setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel);
779
+ }
780
+ syncStudioUiRefreshReviewTrigger();
781
+ }
782
+
783
+ function closeStudioUiRefreshMenus() {
784
+ if (!studioUiRefreshUi || !studioUiRefreshUi.menus) return;
785
+ for (const item of studioUiRefreshUi.menus) {
786
+ item.menu.hidden = true;
787
+ item.button.classList.remove("is-open");
788
+ item.button.setAttribute("aria-expanded", "false");
789
+ }
790
+ }
791
+
792
+ function toggleStudioUiRefreshMenu(name) {
793
+ if (!studioUiRefreshUi || !studioUiRefreshUi.menus) return;
794
+ let willOpen = false;
795
+ for (const item of studioUiRefreshUi.menus) {
796
+ if (item.name === name) willOpen = item.menu.hidden;
797
+ }
798
+ for (const item of studioUiRefreshUi.menus) {
799
+ const isOpen = willOpen && item.name === name;
800
+ item.menu.hidden = !isOpen;
801
+ item.button.classList.toggle("is-open", isOpen);
802
+ item.button.setAttribute("aria-expanded", isOpen ? "true" : "false");
803
+ }
804
+ }
805
+
806
+ function syncStudioUiRefreshReviewTrigger() {
807
+ if (!studioUiRefreshUi || !studioUiRefreshUi.reviewButton) return;
808
+ const critiqueIsStop = getAbortablePendingKind() === "critique";
809
+ const reviewButton = studioUiRefreshUi.reviewButton;
810
+ reviewButton.textContent = critiqueIsStop ? "Stop critique" : "Review";
811
+ reviewButton.classList.toggle("request-stop-active", critiqueIsStop);
812
+ reviewButton.disabled = critiqueIsStop ? Boolean(critiqueBtn && critiqueBtn.disabled) : false;
813
+ reviewButton.title = critiqueIsStop
814
+ ? "Stop the running critique request. Shortcut: Esc."
815
+ : "Open review actions and settings.";
816
+ if (critiqueIsStop) {
817
+ closeStudioUiRefreshMenus();
818
+ }
819
+ }
820
+
821
+ function makeStudioUiRefreshMenu(buttonEl, name, menuClassName) {
822
+ const anchorEl = makeStudioUiRefreshElement("span", "studio-refresh-menu-anchor " + (menuClassName || ""));
823
+ const menuEl = makeStudioUiRefreshElement("div", "studio-refresh-menu");
824
+ menuEl.hidden = true;
825
+ buttonEl.type = "button";
826
+ buttonEl.classList.add("studio-refresh-chip");
827
+ buttonEl.setAttribute("aria-haspopup", "menu");
828
+ buttonEl.setAttribute("aria-expanded", "false");
829
+ buttonEl.addEventListener("click", (event) => {
830
+ event.stopPropagation();
831
+ if (name === "review" && getAbortablePendingKind() === "critique") {
832
+ requestCancelForPendingRequest("critique");
833
+ return;
834
+ }
835
+ toggleStudioUiRefreshMenu(name);
836
+ });
837
+ anchorEl.appendChild(buttonEl);
838
+ anchorEl.appendChild(menuEl);
839
+ return { name, anchor: anchorEl, button: buttonEl, menu: menuEl };
840
+ }
841
+
842
+ function setStudioUiRefreshPreference(enabled) {
843
+ try {
844
+ if (enabled) window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, "1");
845
+ else window.localStorage && window.localStorage.removeItem(STUDIO_UI_REFRESH_STORAGE_KEY);
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: Refresh" : "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();
588
998
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
589
999
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
590
1000
  throw new Error("Studio annotation helpers failed to load.");
@@ -1224,7 +1634,7 @@
1224
1634
 
1225
1635
  function updateSourceBadge() {
1226
1636
  const label = sourceState && sourceState.label ? sourceState.label : "blank";
1227
- sourceBadgeEl.textContent = "Editor origin: " + label;
1637
+ sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label;
1228
1638
  const descriptor = getCurrentStudioDocumentDescriptor();
1229
1639
  if (sourceBadgeEl) {
1230
1640
  sourceBadgeEl.title = descriptor.fileBacked
@@ -1285,6 +1695,9 @@
1285
1695
  btn.classList.toggle("is-active", isFocusedPane);
1286
1696
  btn.setAttribute("aria-pressed", isFocusedPane ? "true" : "false");
1287
1697
  btn.textContent = isFocusedPane ? "Exit focus" : "Focus pane";
1698
+ if (studioUiRefreshEnabled) {
1699
+ setStudioUiRefreshFocusButtonIcon(btn, isFocusedPane);
1700
+ }
1288
1701
  btn.title = isFocusedPane
1289
1702
  ? "Return to the two-pane layout. Shortcut: F10 or Cmd/Ctrl+Esc."
1290
1703
  : "Show only the " + paneName + " pane. Shortcut: F10 or Cmd/Ctrl+Esc.";
@@ -2837,6 +3250,107 @@
2837
3250
  }
2838
3251
  }
2839
3252
 
3253
+ function normalizeCopyableBlockText(text) {
3254
+ return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
3255
+ }
3256
+
3257
+ function getCopyablePreviewBlockText(blockEl) {
3258
+ if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
3259
+ if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
3260
+ return normalizeCopyableBlockText(
3261
+ Array.from(blockEl.querySelectorAll(".preview-code-line-content"))
3262
+ .map((lineEl) => lineEl && typeof lineEl.textContent === "string" ? lineEl.textContent : "")
3263
+ .join("\n"),
3264
+ );
3265
+ }
3266
+
3267
+ const codeEl = typeof blockEl.querySelector === "function"
3268
+ ? blockEl.querySelector("pre code, code")
3269
+ : null;
3270
+ if (codeEl && typeof codeEl.textContent === "string") {
3271
+ return normalizeCopyableBlockText(codeEl.textContent);
3272
+ }
3273
+
3274
+ const clone = typeof blockEl.cloneNode === "function" ? blockEl.cloneNode(true) : null;
3275
+ if (clone && typeof clone.querySelectorAll === "function") {
3276
+ Array.from(clone.querySelectorAll(".studio-copy-block-btn")).forEach((buttonEl) => {
3277
+ if (buttonEl && buttonEl.parentNode) buttonEl.parentNode.removeChild(buttonEl);
3278
+ });
3279
+ return normalizeCopyableBlockText(clone.textContent || "");
3280
+ }
3281
+
3282
+ return normalizeCopyableBlockText(blockEl.textContent || "");
3283
+ }
3284
+
3285
+ async function handleCopyPreviewBlockButtonClick(event) {
3286
+ const target = event && event.target;
3287
+ const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
3288
+ if (!copyBtn) return;
3289
+ event.preventDefault();
3290
+ event.stopPropagation();
3291
+ if (typeof event.stopImmediatePropagation === "function") {
3292
+ event.stopImmediatePropagation();
3293
+ }
3294
+
3295
+ const blockEl = copyBtn.closest(".studio-copyable-block");
3296
+ if (!blockEl) {
3297
+ setStatus("Could not find the block to copy.", "warning");
3298
+ return;
3299
+ }
3300
+
3301
+ const text = getCopyablePreviewBlockText(blockEl);
3302
+ if (!text.trim()) {
3303
+ setStatus("Nothing to copy from this block.", "warning");
3304
+ return;
3305
+ }
3306
+
3307
+ if (copyBtn.dataset && copyBtn.dataset.studioCopyBusy === "1") return;
3308
+ if (copyBtn.dataset) copyBtn.dataset.studioCopyBusy = "1";
3309
+ const ok = await writeTextToClipboard(text);
3310
+ if (ok) {
3311
+ setStatus("Copied block to clipboard.", "success");
3312
+ } else {
3313
+ setStatus("Clipboard write failed.", "warning");
3314
+ }
3315
+ if (copyBtn.dataset) {
3316
+ window.setTimeout(() => {
3317
+ if (copyBtn.dataset) copyBtn.dataset.studioCopyBusy = "0";
3318
+ }, 150);
3319
+ }
3320
+ }
3321
+
3322
+ function decorateCopyablePreviewBlocks(targetEl) {
3323
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
3324
+ const blocks = Array.from(targetEl.querySelectorAll("div.sourceCode, pre, .preview-code-lines"));
3325
+ blocks.forEach((blockEl) => {
3326
+ if (!blockEl || !(blockEl instanceof Element)) return;
3327
+ if (blockEl.dataset && blockEl.dataset.studioCopyDecorated === "1") return;
3328
+ if (blockEl.matches && blockEl.matches("pre") && blockEl.closest("div.sourceCode")) return;
3329
+ if (blockEl.closest && blockEl.closest("button, .studio-copy-block-btn")) return;
3330
+
3331
+ const initialText = getCopyablePreviewBlockText(blockEl);
3332
+ if (!initialText.trim()) return;
3333
+
3334
+ blockEl.classList.add("studio-copyable-block");
3335
+ if (blockEl.dataset) blockEl.dataset.studioCopyDecorated = "1";
3336
+
3337
+ const copyBtn = document.createElement("button");
3338
+ copyBtn.type = "button";
3339
+ copyBtn.className = "studio-copy-block-btn";
3340
+ copyBtn.textContent = "Copy";
3341
+ copyBtn.title = "Copy this block to the clipboard.";
3342
+ copyBtn.setAttribute("aria-label", "Copy this block to the clipboard");
3343
+ copyBtn.addEventListener("pointerdown", (event) => {
3344
+ event.stopPropagation();
3345
+ });
3346
+ copyBtn.addEventListener("mousedown", (event) => {
3347
+ event.stopPropagation();
3348
+ });
3349
+
3350
+ blockEl.appendChild(copyBtn);
3351
+ });
3352
+ }
3353
+
2840
3354
  async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
2841
3355
  const previewPrepared = annotationsEnabled
2842
3356
  ? prepareMarkdownForPandocPreview(markdown)
@@ -2879,6 +3393,7 @@
2879
3393
  if (shouldDecoratePreviewComments) {
2880
3394
  decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
2881
3395
  }
3396
+ decorateCopyablePreviewBlocks(targetEl);
2882
3397
 
2883
3398
  // Warn if relative images are present but unlikely to resolve (non-file-backed content)
2884
3399
  if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
@@ -3670,6 +4185,7 @@
3670
4185
  if (lineNumbersSelect) {
3671
4186
  lineNumbersSelect.value = lineNumbersEnabled ? "on" : "off";
3672
4187
  }
4188
+ syncStudioUiRefreshSummaries();
3673
4189
  updateLineNumberGutterVisibility();
3674
4190
  scheduleEditorLineNumberRender();
3675
4191
  if (editorHighlightEnabled && editorView === "markdown") {
@@ -4281,6 +4797,7 @@
4281
4797
  targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
4282
4798
  ensurePreviewSelectionActions(targetEl);
4283
4799
  updatePreviewCommentBlocksForElement(targetEl);
4800
+ decorateCopyablePreviewBlocks(targetEl);
4284
4801
  if (pane === "response") {
4285
4802
  applyPendingResponseScrollReset();
4286
4803
  scheduleResponsePaneRepaintNudge();
@@ -4357,6 +4874,7 @@
4357
4874
  if (stripAnnotationsBtn) {
4358
4875
  stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
4359
4876
  }
4877
+ syncStudioUiRefreshSummaries();
4360
4878
  }
4361
4879
 
4362
4880
  function scheduleEditorMetaUpdate() {
@@ -8066,6 +8584,12 @@
8066
8584
  ? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
8067
8585
  : "Inline annotations derived from all non-empty comments are currently off. Click to add them.";
8068
8586
  }
8587
+ if (reviewNotesDeleteAllBtn) {
8588
+ reviewNotesDeleteAllBtn.disabled = uiBusy || !hasNotes;
8589
+ reviewNotesDeleteAllBtn.title = hasNotes
8590
+ ? "Delete all local comments for this document or draft. Existing inline [an: ...] annotations in the editor text are left unchanged."
8591
+ : "No local comments to delete.";
8592
+ }
8069
8593
  if (reviewNotesDoneBtn) {
8070
8594
  reviewNotesDoneBtn.disabled = !isOpen;
8071
8595
  }
@@ -8417,6 +8941,21 @@
8417
8941
  setStatus("Deleted local comment.", "success");
8418
8942
  }
8419
8943
 
8944
+ function deleteAllReviewNotes() {
8945
+ if (!reviewNotes.length) {
8946
+ setStatus("No local comments to delete.", "warning");
8947
+ return;
8948
+ }
8949
+ const count = reviewNotes.length;
8950
+ const confirmed = window.confirm(
8951
+ "Delete all " + count + " local comment" + (count === 1 ? "" : "s") + " for this document?\n\n"
8952
+ + "Existing inline [an: ...] annotations in the editor text will not be removed.",
8953
+ );
8954
+ if (!confirmed) return;
8955
+ setReviewNotes([]);
8956
+ setStatus("Deleted all local comments.", "success");
8957
+ }
8958
+
8420
8959
  function convertReviewNoteToAnnotation(noteId) {
8421
8960
  if (uiBusy) {
8422
8961
  setStatus("Wait until the current Studio action finishes before toggling inline annotation state.", "warning");
@@ -8669,11 +9208,13 @@
8669
9208
  if (!highlightSelect) return;
8670
9209
  if (!editorHighlightEnabled) {
8671
9210
  highlightSelect.value = "off";
9211
+ syncStudioUiRefreshSummaries();
8672
9212
  return;
8673
9213
  }
8674
9214
  highlightSelect.value = (editorLanguage && SUPPORTED_LANGUAGES.indexOf(editorLanguage) !== -1)
8675
9215
  ? editorLanguage
8676
9216
  : "markdown";
9217
+ syncStudioUiRefreshSummaries();
8677
9218
  }
8678
9219
 
8679
9220
  function setEditorHighlightEnabled(enabled) {
@@ -8776,6 +9317,7 @@
8776
9317
  critiqueBtn.disabled = true;
8777
9318
  critiqueBtn.title = "Critique is unavailable in editor-only mode.";
8778
9319
  }
9320
+ syncStudioUiRefreshReviewTrigger();
8779
9321
  return;
8780
9322
  }
8781
9323
 
@@ -8813,6 +9355,7 @@
8813
9355
  ? "Critique text as-is (includes [an: ...] markers)."
8814
9356
  : "Critique text with [an: ...] markers stripped."));
8815
9357
  }
9358
+ syncStudioUiRefreshReviewTrigger();
8816
9359
  }
8817
9360
 
8818
9361
  function updateAnnotationModeUi() {
@@ -8823,6 +9366,7 @@
8823
9366
  : "Inline annotations Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.";
8824
9367
  }
8825
9368
 
9369
+ syncStudioUiRefreshSummaries();
8826
9370
  syncRunAndCritiqueButtons();
8827
9371
  }
8828
9372
 
@@ -9708,10 +10252,12 @@
9708
10252
  if (hasHeader) {
9709
10253
  insertHeaderBtn.textContent = "Annotation header: On";
9710
10254
  insertHeaderBtn.title = "Remove annotated-reply protocol header while keeping body text.";
10255
+ syncStudioUiRefreshSummaries();
9711
10256
  return;
9712
10257
  }
9713
10258
  insertHeaderBtn.textContent = "Annotation header: Off";
9714
10259
  insertHeaderBtn.title = "Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).";
10260
+ syncStudioUiRefreshSummaries();
9715
10261
  }
9716
10262
 
9717
10263
  function toggleAnnotatedReplyHeader() {
@@ -10078,7 +10624,7 @@
10078
10624
  }
10079
10625
 
10080
10626
  try {
10081
- await navigator.clipboard.writeText(content);
10627
+ await writeTextToClipboard(content);
10082
10628
  setStatus("Copied response text.", "success");
10083
10629
  } catch (error) {
10084
10630
  setStatus("Clipboard write failed.", "warning");
@@ -10302,7 +10848,7 @@
10302
10848
  }
10303
10849
 
10304
10850
  try {
10305
- await navigator.clipboard.writeText(content);
10851
+ await writeTextToClipboard(content);
10306
10852
  setStatus("Copied editor text.", "success");
10307
10853
  } catch (error) {
10308
10854
  setStatus("Clipboard write failed.", "warning");
@@ -10386,6 +10932,12 @@
10386
10932
  });
10387
10933
  }
10388
10934
 
10935
+ if (reviewNotesDeleteAllBtn) {
10936
+ reviewNotesDeleteAllBtn.addEventListener("click", () => {
10937
+ deleteAllReviewNotes();
10938
+ });
10939
+ }
10940
+
10389
10941
  if (reviewNoteGutterContentEl) {
10390
10942
  reviewNoteGutterContentEl.addEventListener("click", (event) => {
10391
10943
  const target = event.target;
@@ -10397,6 +10949,20 @@
10397
10949
  });
10398
10950
  }
10399
10951
 
10952
+ document.addEventListener("click", (event) => {
10953
+ const target = event.target;
10954
+ const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
10955
+ if (!copyBtn) return;
10956
+ void handleCopyPreviewBlockButtonClick(event);
10957
+ }, true);
10958
+
10959
+ document.addEventListener("pointerup", (event) => {
10960
+ const target = event.target;
10961
+ const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
10962
+ if (!copyBtn) return;
10963
+ void handleCopyPreviewBlockButtonClick(event);
10964
+ }, true);
10965
+
10400
10966
  function handlePreviewCommentActionMouseDown(event) {
10401
10967
  const target = event.target;
10402
10968
  const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
@@ -10483,7 +11049,7 @@
10483
11049
  }
10484
11050
 
10485
11051
  try {
10486
- await navigator.clipboard.writeText(String(scratchpadText || ""));
11052
+ await writeTextToClipboard(String(scratchpadText || ""));
10487
11053
  setStatus("Copied scratchpad text.", "success");
10488
11054
  } catch (error) {
10489
11055
  setStatus("Clipboard write failed.", "warning");