pressship 0.1.13 → 0.1.14

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/assets/web/app.js CHANGED
@@ -11,9 +11,11 @@ void import("/vendor/marked.esm.js")
11
11
  breaks: true
12
12
  });
13
13
  markdownParser = (markdown) => marked.parse(markdown, { async: false });
14
+ refreshStudioAiMarkdownIfReady();
14
15
  })
15
16
  .catch(() => {
16
17
  markdownParser = basicMarkdownToHtml;
18
+ refreshStudioAiMarkdownIfReady();
17
19
  });
18
20
 
19
21
  const MARKDOWN_ALLOWED_TAGS = new Set([
@@ -120,6 +122,8 @@ function clampStudioLayoutValue(key, value) {
120
122
  }
121
123
 
122
124
  const STUDIO_SIDEBAR_TAB_KEY = "pressship.studio.sidebar.tab.v1";
125
+ const STUDIO_PANEL_STORAGE_KEY = "pressship.studio.panels.v1";
126
+ const STUDIO_THEME_STORAGE_KEY = "pressship.studio.theme.v1";
123
127
 
124
128
  function loadStudioSidebarTab(pluginKey) {
125
129
  if (!pluginKey) {
@@ -147,6 +151,60 @@ function saveStudioSidebarTab(pluginKey, tab) {
147
151
  }
148
152
  }
149
153
 
154
+ function loadStudioPanelState() {
155
+ try {
156
+ const raw = typeof localStorage !== "undefined" ? localStorage.getItem(STUDIO_PANEL_STORAGE_KEY) : null;
157
+ const parsed = raw ? JSON.parse(raw) : {};
158
+ return normalizeStudioPanelState(parsed);
159
+ } catch {
160
+ return normalizeStudioPanelState();
161
+ }
162
+ }
163
+
164
+ function normalizeStudioPanelState(value = {}) {
165
+ return {
166
+ files: value.files !== false,
167
+ sidebar: value.sidebar !== false
168
+ };
169
+ }
170
+
171
+ function saveStudioPanelState(panels) {
172
+ try {
173
+ localStorage.setItem(STUDIO_PANEL_STORAGE_KEY, JSON.stringify(normalizeStudioPanelState(panels)));
174
+ } catch {
175
+ // ignore
176
+ }
177
+ }
178
+
179
+ function normalizeStudioTheme(value) {
180
+ return value === "light" ? "light" : "dark";
181
+ }
182
+
183
+ function loadStudioTheme() {
184
+ try {
185
+ const raw = typeof localStorage !== "undefined" ? localStorage.getItem(STUDIO_THEME_STORAGE_KEY) : null;
186
+ return normalizeStudioTheme(raw);
187
+ } catch {
188
+ return "dark";
189
+ }
190
+ }
191
+
192
+ function saveStudioTheme(theme) {
193
+ try {
194
+ localStorage.setItem(STUDIO_THEME_STORAGE_KEY, normalizeStudioTheme(theme));
195
+ } catch {
196
+ // ignore
197
+ }
198
+ }
199
+
200
+ function studioTheme() {
201
+ return normalizeStudioTheme(state.studio.theme);
202
+ }
203
+
204
+ function studioMonacoTheme() {
205
+ return studioTheme() === "light" ? "vs" : "vs-dark";
206
+ }
207
+
150
208
  function createInitialStudioRelease() {
151
209
  return {
152
210
  tags: null,
@@ -359,6 +417,7 @@ const state = {
359
417
  playgroundUrl: "",
360
418
  playgroundUrls: null,
361
419
  activeTab: "editor",
420
+ openFiles: [],
362
421
  terminalOpen: true,
363
422
  collapsedFolders: new Set(),
364
423
  terminal: [],
@@ -374,6 +433,8 @@ const state = {
374
433
  editorKind: null,
375
434
  editorModels: [],
376
435
  layout: loadStudioLayout(),
436
+ panels: loadStudioPanelState(),
437
+ theme: loadStudioTheme(),
377
438
  sidebarTab: "ai",
378
439
  playgroundVersionModal: null,
379
440
  release: createInitialStudioRelease(),
@@ -423,6 +484,27 @@ const els = {
423
484
  const isMac =
424
485
  typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform || "");
425
486
  let monacoPromise = null;
487
+ const VIEW_ROUTE_PATHS = {
488
+ dashboard: "/dashboard",
489
+ studio: "/studio",
490
+ remote: "/wordpress.org",
491
+ local: "/local",
492
+ release: "/release",
493
+ settings: "/settings"
494
+ };
495
+ const ROUTE_VIEW_ALIASES = {
496
+ "": "dashboard",
497
+ dashboard: "dashboard",
498
+ studio: "studio",
499
+ "wordpress.org": "remote",
500
+ remote: "remote",
501
+ local: "local",
502
+ release: "release",
503
+ settings: "settings"
504
+ };
505
+ let applyingLocationRoute = false;
506
+ let initialRouteLoaderVisible = false;
507
+
426
508
  document.querySelectorAll("[data-kbd-mod]").forEach((node) => {
427
509
  node.textContent = isMac ? "⌘" : "Ctrl+";
428
510
  });
@@ -525,6 +607,242 @@ els.commandInput?.addEventListener("input", () => {
525
607
  renderCommandPalette();
526
608
  });
527
609
 
610
+ window.addEventListener("popstate", () => {
611
+ void applyLocationRoute({ replaceRoute: true });
612
+ });
613
+
614
+ function normalizeViewId(view) {
615
+ return Object.prototype.hasOwnProperty.call(VIEW_ROUTE_PATHS, view) ? view : "dashboard";
616
+ }
617
+
618
+ function normalizeBrowserPath(pathname = window.location.pathname) {
619
+ const trimmed = pathname.replace(/\/+$/, "");
620
+ return trimmed || "/";
621
+ }
622
+
623
+ function shouldShowInitialRouteLoader() {
624
+ const path = normalizeBrowserPath();
625
+ return path !== "/" && path !== VIEW_ROUTE_PATHS.dashboard;
626
+ }
627
+
628
+ function showInitialRouteLoader() {
629
+ if (!shouldShowInitialRouteLoader() || initialRouteLoaderVisible) {
630
+ return;
631
+ }
632
+ initialRouteLoaderVisible = true;
633
+ document.body.classList.add("is-initial-route-loading");
634
+ const loader = document.createElement("div");
635
+ loader.id = "initial-route-loader";
636
+ loader.className = "ps-initial-route-loader";
637
+ loader.setAttribute("role", "status");
638
+ loader.setAttribute("aria-live", "polite");
639
+ loader.innerHTML = `
640
+ <div class="ps-initial-route-loader-card">
641
+ <img src="/brand/pressship-symbol.png" alt="" />
642
+ <span class="dashicons dashicons-update" aria-hidden="true"></span>
643
+ <strong>Loading Pressship Studio</strong>
644
+ <p>Restoring the requested page...</p>
645
+ </div>
646
+ `;
647
+ document.body.append(loader);
648
+ }
649
+
650
+ function hideInitialRouteLoader() {
651
+ if (!initialRouteLoaderVisible) {
652
+ return;
653
+ }
654
+ initialRouteLoaderVisible = false;
655
+ document.body.classList.remove("is-initial-route-loading");
656
+ document.getElementById("initial-route-loader")?.remove();
657
+ }
658
+
659
+ function decodeRouteSegment(segment) {
660
+ try {
661
+ return decodeURIComponent(segment);
662
+ } catch {
663
+ return segment;
664
+ }
665
+ }
666
+
667
+ function encodeRouteSegments(value) {
668
+ return String(value ?? "")
669
+ .split("/")
670
+ .filter(Boolean)
671
+ .map((segment) => encodeURIComponent(segment))
672
+ .join("/");
673
+ }
674
+
675
+ function parseLocationRoute(pathname = window.location.pathname) {
676
+ const segments = pathname
677
+ .split("/")
678
+ .filter(Boolean)
679
+ .map((segment) => decodeRouteSegment(segment));
680
+ const head = segments[0] ?? "";
681
+
682
+ if (head === "studio") {
683
+ return {
684
+ view: "studio",
685
+ studio: {
686
+ project: segments[1] ?? "",
687
+ filePath: segments.slice(2).join("/")
688
+ }
689
+ };
690
+ }
691
+
692
+ return {
693
+ view: ROUTE_VIEW_ALIASES[head] ?? "dashboard"
694
+ };
695
+ }
696
+
697
+ function applyActiveViewShell(view) {
698
+ const nextView = normalizeViewId(view);
699
+ state.activeView = nextView;
700
+ document.body.dataset.activeView = nextView;
701
+ document.querySelectorAll(".view").forEach((node) => node.classList.remove("is-active"));
702
+ document.getElementById(`view-${nextView}`)?.classList.add("is-active");
703
+ document
704
+ .querySelectorAll("#adminmenu li")
705
+ .forEach((node) => node.classList.remove("wp-has-current-submenu"));
706
+ document
707
+ .querySelector(`#adminmenu li[data-view="${nextView}"]`)
708
+ ?.classList.add("wp-has-current-submenu");
709
+ return nextView;
710
+ }
711
+
712
+ function primeInitialRouteState(route) {
713
+ const nextView = applyActiveViewShell(route.view);
714
+ if (nextView !== "studio" || !route.studio?.project || state.studio.id) {
715
+ return;
716
+ }
717
+
718
+ const filePath = route.studio.filePath;
719
+ state.studio = {
720
+ ...state.studio,
721
+ scope: null,
722
+ id: route.studio.project,
723
+ plugin: { slug: route.studio.project, name: route.studio.project },
724
+ files: [],
725
+ selectedFile: filePath
726
+ ? {
727
+ path: filePath,
728
+ name: filePath.split("/").pop() ?? filePath,
729
+ directory: filePath.includes("/") ? filePath.split("/").slice(0, -1).join("/") : "",
730
+ size: 0
731
+ }
732
+ : null,
733
+ fileContent: "",
734
+ draftContent: "",
735
+ readOnly: true,
736
+ dirty: false,
737
+ loading: true,
738
+ running: false,
739
+ checking: false,
740
+ jobId: null,
741
+ checkJobId: null,
742
+ activeTab: "editor",
743
+ openFiles: filePath ? [filePath] : [],
744
+ terminal: [`Opening ${route.studio.project} from URL...`],
745
+ collapsedFolders: new Set(),
746
+ pendingConfirms: new Map()
747
+ };
748
+ }
749
+
750
+ function studioProjectRouteSegment() {
751
+ const plugin = state.studio.plugin;
752
+ const localPlugin = state.local.find((item) => item.id === state.studio.id);
753
+ return plugin?.slug || localPlugin?.slug || plugin?.name || state.studio.id || "";
754
+ }
755
+
756
+ function studioRoutePathForState() {
757
+ if (!state.studio.id) {
758
+ return VIEW_ROUTE_PATHS.studio;
759
+ }
760
+ const project = studioProjectRouteSegment();
761
+ if (!project) {
762
+ return VIEW_ROUTE_PATHS.studio;
763
+ }
764
+ const filePath =
765
+ state.studio.activeTab === "editor" && state.studio.selectedFile?.path
766
+ ? encodeRouteSegments(state.studio.selectedFile.path)
767
+ : "";
768
+ const projectPath = encodeURIComponent(project);
769
+ return filePath ? `/studio/${projectPath}/${filePath}` : `/studio/${projectPath}`;
770
+ }
771
+
772
+ function routePathForState() {
773
+ const view = normalizeViewId(state.activeView);
774
+ return view === "studio" ? studioRoutePathForState() : VIEW_ROUTE_PATHS[view];
775
+ }
776
+
777
+ function updateRouteFromState(options = {}) {
778
+ if (applyingLocationRoute && !options.force) {
779
+ return;
780
+ }
781
+ const nextPath = routePathForState();
782
+ if (normalizeBrowserPath(nextPath) === normalizeBrowserPath()) {
783
+ return;
784
+ }
785
+ const method = options.replace ? "replaceState" : "pushState";
786
+ history[method]({ pressshipView: state.activeView }, "", nextPath);
787
+ }
788
+
789
+ function resolveStudioRouteProject(project) {
790
+ if (!project) {
791
+ return null;
792
+ }
793
+ const normalizedProject = project.toLowerCase();
794
+ const matchesProject = (candidate) => {
795
+ if (!candidate) {
796
+ return false;
797
+ }
798
+ return candidate === project || candidate.toLowerCase() === normalizedProject;
799
+ };
800
+ const localPlugin = state.local.find((plugin) =>
801
+ [plugin.slug, plugin.id, plugin.name].some((candidate) => matchesProject(candidate))
802
+ );
803
+ if (localPlugin) {
804
+ return { scope: "local", id: localPlugin.id };
805
+ }
806
+
807
+ const remotePlugin = state.remote.find((plugin) =>
808
+ [plugin.slug, plugin.name].some((candidate) => matchesProject(candidate))
809
+ );
810
+ return { scope: "remote", id: remotePlugin?.slug || project };
811
+ }
812
+
813
+ async function applyLocationRoute(options = {}) {
814
+ const route = parseLocationRoute();
815
+ applyingLocationRoute = true;
816
+ try {
817
+ if (route.view === "studio") {
818
+ if (route.studio?.project) {
819
+ const target = resolveStudioRouteProject(route.studio.project);
820
+ if (target) {
821
+ await openStudio(target.scope, target.id, {
822
+ filePath: route.studio.filePath,
823
+ updateRoute: false
824
+ });
825
+ } else {
826
+ await showView("studio", { updateRoute: false });
827
+ renderStudio();
828
+ }
829
+ } else {
830
+ await showView("studio", { updateRoute: false });
831
+ renderStudio();
832
+ }
833
+ } else {
834
+ await showView(route.view, { updateRoute: false });
835
+ }
836
+ } finally {
837
+ applyingLocationRoute = false;
838
+ }
839
+
840
+ if (options.replaceRoute !== false) {
841
+ updateRouteFromState({ replace: true, force: true });
842
+ }
843
+ }
844
+
845
+ showInitialRouteLoader();
528
846
  void boot();
529
847
 
530
848
  async function boot() {
@@ -532,13 +850,15 @@ async function boot() {
532
850
  state.bootstrap = await api("/api/bootstrap");
533
851
  refreshTokenFromBootstrap(state.bootstrap);
534
852
  } catch (error) {
853
+ hideInitialRouteLoader();
535
854
  notice(`Could not load bootstrap state: ${error.message}`, "error");
536
855
  return;
537
856
  }
538
857
  state.settings = state.bootstrap.settings ?? null;
539
858
  state.playgrounds = state.bootstrap.playgrounds ?? [];
540
859
  state.aiAssistance.harnesses = state.bootstrap.aiHarnesses ?? [];
541
- document.body.dataset.activeView = state.activeView;
860
+ const initialRoute = parseLocationRoute();
861
+ primeInitialRouteState(initialRoute);
542
862
  renderAccount();
543
863
  for (const job of state.bootstrap.jobs ?? []) {
544
864
  upsertJob(job);
@@ -549,8 +869,16 @@ async function boot() {
549
869
  renderDashboard();
550
870
  renderStudio();
551
871
  renderPlaygroundsMenu();
872
+ if (state.activeView === "release") {
873
+ void loadReleaseBoard();
874
+ }
552
875
  void loadAiAssistance();
553
- await Promise.all([loadRemote(), loadLocal()]);
876
+ try {
877
+ await Promise.all([loadRemote(), loadLocal()]);
878
+ await applyLocationRoute({ replaceRoute: true });
879
+ } finally {
880
+ hideInitialRouteLoader();
881
+ }
554
882
  }
555
883
 
556
884
  function renderAccount() {
@@ -598,6 +926,10 @@ async function runAction(name, element) {
598
926
  await selectStudioFile(element.dataset.path);
599
927
  return;
600
928
 
929
+ case "studio-close-file-tab":
930
+ await closeStudioFileTab(element.dataset.path);
931
+ return;
932
+
601
933
  case "studio-toggle-folder":
602
934
  toggleStudioFolder(element.dataset.folder);
603
935
  return;
@@ -606,6 +938,18 @@ async function runAction(name, element) {
606
938
  toggleStudioTerminal();
607
939
  return;
608
940
 
941
+ case "studio-toggle-files":
942
+ toggleStudioPanel("files");
943
+ return;
944
+
945
+ case "studio-toggle-sidebar":
946
+ toggleStudioPanel("sidebar");
947
+ return;
948
+
949
+ case "studio-open-sidebar-tab":
950
+ openStudioSidebarTab(element.dataset.tab);
951
+ return;
952
+
609
953
  case "studio-save":
610
954
  await saveStudioFile();
611
955
  return;
@@ -614,6 +958,10 @@ async function runAction(name, element) {
614
958
  await runStudioCheck();
615
959
  return;
616
960
 
961
+ case "studio-toggle-theme":
962
+ toggleStudioTheme();
963
+ return;
964
+
617
965
  case "studio-check-note":
618
966
  revealStudioCheckNote(Number(element.dataset.line || 1), Number(element.dataset.column || 1));
619
967
  return;
@@ -1106,6 +1454,7 @@ async function openStudio(scope, id, options = {}) {
1106
1454
  playgroundUrl: "",
1107
1455
  playgroundUrls: null,
1108
1456
  activeTab: "editor",
1457
+ openFiles: [],
1109
1458
  terminalOpen: true,
1110
1459
  collapsedFolders: new Set(),
1111
1460
  terminal: [`Pressship Studio opened for ${scope === "local" ? "local plugin" : "WordPress.org plugin"} ${id}.`],
@@ -1121,13 +1470,15 @@ async function openStudio(scope, id, options = {}) {
1121
1470
  editorKind: null,
1122
1471
  editorModels: [],
1123
1472
  layout: state.studio.layout ?? loadStudioLayout(),
1473
+ panels: normalizeStudioPanelState(state.studio.panels ?? loadStudioPanelState()),
1474
+ theme: normalizeStudioTheme(state.studio.theme ?? loadStudioTheme()),
1124
1475
  sidebarTab,
1125
1476
  playgroundVersionModal: null,
1126
1477
  release: createInitialStudioRelease(),
1127
1478
  pendingConfirms: new Map()
1128
1479
  };
1129
1480
  saveStudioSidebarTab(pluginKey, sidebarTab);
1130
- showView("studio");
1481
+ await showView("studio", { updateRoute: false });
1131
1482
  renderStudio();
1132
1483
  if (scope === "local" && sidebarTab === "release") {
1133
1484
  void loadStudioReleaseTags();
@@ -1146,26 +1497,45 @@ async function openStudio(scope, id, options = {}) {
1146
1497
  state.studio.files = result.files ?? [];
1147
1498
  applyStudioCheckState(checkState.state);
1148
1499
  renderStudio();
1149
- const initialFile = chooseInitialStudioFile(state.studio.files, state.studio.plugin?.slug);
1500
+ const requestedFile = options.filePath
1501
+ ? state.studio.files.find((file) => file.path === options.filePath)
1502
+ : null;
1503
+ if (options.filePath && !requestedFile) {
1504
+ appendStudioTerminal(`Route file not found: ${options.filePath}`, "warning");
1505
+ }
1506
+ const initialFile = requestedFile ?? chooseInitialStudioFile(state.studio.files, state.studio.plugin?.slug);
1150
1507
  if (initialFile) {
1151
- await selectStudioFile(initialFile.path);
1508
+ await selectStudioFile(initialFile.path, {
1509
+ updateRoute: options.updateRoute,
1510
+ replaceRoute: options.replaceRoute
1511
+ });
1152
1512
  } else {
1153
1513
  state.studio.draftContent = "";
1154
1514
  remountStudioEditorIfNeeded();
1515
+ if (options.updateRoute !== false) {
1516
+ updateRouteFromState({ replace: options.replaceRoute });
1517
+ }
1155
1518
  }
1156
1519
  } else {
1157
1520
  state.studio.files = [{ path: "readme.txt", name: "readme.txt", directory: "", size: detail.readme?.length ?? 0 }];
1158
1521
  state.studio.selectedFile = state.studio.files[0];
1522
+ state.studio.openFiles = ["readme.txt"];
1159
1523
  state.studio.fileContent = detail.readme ?? "No hosted readme.txt could be loaded.";
1160
1524
  state.studio.draftContent = state.studio.fileContent;
1161
1525
  state.studio.readOnly = true;
1162
1526
  renderStudio();
1163
1527
  remountStudioEditorIfNeeded();
1528
+ if (options.updateRoute !== false) {
1529
+ updateRouteFromState({ replace: options.replaceRoute });
1530
+ }
1164
1531
  }
1165
1532
  } catch (error) {
1166
1533
  state.studio.loading = false;
1167
1534
  appendStudioTerminal(error.message, "error");
1168
1535
  renderStudio();
1536
+ if (options.updateRoute !== false) {
1537
+ updateRouteFromState({ replace: options.replaceRoute });
1538
+ }
1169
1539
  }
1170
1540
  }
1171
1541
 
@@ -1188,6 +1558,7 @@ async function selectStudioFile(relativePath, options = {}) {
1188
1558
  return;
1189
1559
  }
1190
1560
 
1561
+ ensureStudioFileTab(relativePath);
1191
1562
  state.studio.selectedFile = state.studio.files.find((file) => file.path === relativePath) ?? {
1192
1563
  path: relativePath,
1193
1564
  name: relativePath.split("/").pop() ?? relativePath,
@@ -1200,6 +1571,9 @@ async function selectStudioFile(relativePath, options = {}) {
1200
1571
  state.studio.activeTab = "editor";
1201
1572
  renderStudio();
1202
1573
  remountStudioEditorIfNeeded();
1574
+ if (options.updateRoute !== false) {
1575
+ updateRouteFromState({ replace: options.replaceRoute });
1576
+ }
1203
1577
 
1204
1578
  try {
1205
1579
  const result = await api(
@@ -1217,6 +1591,17 @@ async function selectStudioFile(relativePath, options = {}) {
1217
1591
  }
1218
1592
  }
1219
1593
 
1594
+ function ensureStudioFileTab(relativePath) {
1595
+ if (!relativePath) {
1596
+ return;
1597
+ }
1598
+ const openFiles = Array.isArray(state.studio.openFiles) ? state.studio.openFiles : [];
1599
+ if (!openFiles.includes(relativePath)) {
1600
+ openFiles.push(relativePath);
1601
+ }
1602
+ state.studio.openFiles = openFiles;
1603
+ }
1604
+
1220
1605
  async function saveStudioFile() {
1221
1606
  if (state.studio.scope !== "local" || !state.studio.id || !state.studio.selectedFile) {
1222
1607
  return;
@@ -1472,6 +1857,7 @@ async function selectStudioAiChange(filePath) {
1472
1857
  state.studio.activeTab = "editor";
1473
1858
  renderStudio();
1474
1859
  remountStudioEditorIfNeeded();
1860
+ updateRouteFromState();
1475
1861
  updateStudioAiSidebar();
1476
1862
  }
1477
1863
 
@@ -1571,6 +1957,35 @@ function renderStudio() {
1571
1957
  ? "WordPress.org plugin"
1572
1958
  : "Choose a plugin from WordPress.org or Local Library.";
1573
1959
 
1960
+ if (state.studio.loading && !state.studio.scope && state.studio.id) {
1961
+ const fileLabel = state.studio.selectedFile?.path
1962
+ ? `Restoring ${state.studio.selectedFile.path}.`
1963
+ : "Restoring the workspace.";
1964
+ els.studio.innerHTML = `
1965
+ <div class="studio-root studio-empty-root">
1966
+ <div class="studio-empty-state" aria-label="Opening Studio workspace">
1967
+ <section class="studio-empty-copy">
1968
+ <span class="studio-empty-kicker">
1969
+ <span class="dashicons dashicons-editor-code" aria-hidden="true"></span>
1970
+ Pressship Studio
1971
+ </span>
1972
+ <h1>Opening ${escapeHtml(title)}</h1>
1973
+ <p class="studio-empty-subtitle">${escapeHtml(fileLabel)}</p>
1974
+ </section>
1975
+ <section class="studio-empty-panel" aria-label="Loading workspace">
1976
+ ${loadingShell("Loading Studio workspace...")}
1977
+ </section>
1978
+ <div class="studio-empty-footer">
1979
+ <span class="dashicons dashicons-update" aria-hidden="true"></span>
1980
+ Matching the URL to a local or WordPress.org plugin.
1981
+ </div>
1982
+ </div>
1983
+ </div>
1984
+ `;
1985
+ updateStudioControls();
1986
+ return;
1987
+ }
1988
+
1574
1989
  if (!state.studio.id) {
1575
1990
  const pickerOptions = studioPickerOptions();
1576
1991
  const pickerDisabled = pickerOptions.length ? "" : "disabled";
@@ -1635,21 +2050,32 @@ function renderStudio() {
1635
2050
  const fileList = state.studio.files.length
1636
2051
  ? renderStudioFileTree(buildStudioFileTree(state.studio.files))
1637
2052
  : `<p class="studio-muted">No editable text files found.</p>`;
1638
- const editorTabLabel = state.studio.selectedFile?.path ?? "Editor";
1639
2053
  const playgroundPort = state.studio.playgroundUrl ? new URL(state.studio.playgroundUrl).port : "";
2054
+ const panels = normalizeStudioPanelState(state.studio.panels);
1640
2055
 
1641
2056
  els.studio.innerHTML = `
1642
- <div class="studio-root${state.studio.terminalOpen ? " has-terminal" : ""}">
2057
+ <div class="studio-root${state.studio.terminalOpen ? " has-terminal" : ""}${panels.files ? " has-files" : " is-files-collapsed"}${panels.sidebar ? " has-secondary-sidebar" : " is-secondary-sidebar-collapsed"}" data-theme="${escapeAttr(studioTheme())}">
1643
2058
  <header class="studio-titlebar">
1644
2059
  <div class="studio-title">
1645
2060
  <strong>${escapeHtml(title)}</strong>
1646
2061
  <span>${escapeHtml(source)}</span>
1647
2062
  </div>
1648
2063
  <div class="studio-title-actions">
2064
+ <button class="studio-layout-button${panels.files ? " is-active" : ""}" type="button" data-action="studio-toggle-files" aria-pressed="${panels.files ? "true" : "false"}" title="${panels.files ? "Hide Explorer" : "Show Explorer"}">
2065
+ <span class="dashicons dashicons-align-left" aria-hidden="true"></span>
2066
+ </button>
2067
+ ${renderStudioPlayButton()}
2068
+ <span class="studio-preview-state${state.studio.running ? " is-loading" : state.studio.playgroundUrl ? " is-ready" : ""}">
2069
+ <span aria-hidden="true"></span>
2070
+ ${escapeHtml(studioPreviewStateLabel())}
2071
+ </span>
1649
2072
  <button class="studio-icon-button" type="button" data-action="studio-toggle-terminal" aria-pressed="${state.studio.terminalOpen ? "true" : "false"}" title="${state.studio.terminalOpen ? "Hide terminal" : "Show terminal"}">
1650
2073
  <span class="dashicons dashicons-editor-kitchensink" aria-hidden="true"></span>
1651
2074
  <span>Terminal</span>
1652
2075
  </button>
2076
+ <button class="studio-layout-button${panels.sidebar ? " is-active" : ""}" type="button" data-action="studio-toggle-sidebar" aria-pressed="${panels.sidebar ? "true" : "false"}" title="${panels.sidebar ? "Hide Secondary Side Bar" : "Show Secondary Side Bar"}">
2077
+ <span class="dashicons dashicons-align-right" aria-hidden="true"></span>
2078
+ </button>
1653
2079
  <button class="studio-action-button" type="button" data-action="studio-save" id="studio-save-button" disabled>
1654
2080
  <span class="dashicons dashicons-saved" aria-hidden="true"></span>
1655
2081
  Save
@@ -1658,37 +2084,31 @@ function renderStudio() {
1658
2084
  <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
1659
2085
  Check
1660
2086
  </button>
2087
+ ${renderStudioThemeToggle()}
1661
2088
  </div>
1662
2089
  </header>
1663
2090
  <div class="studio-main">
1664
- <aside class="studio-files" aria-label="Plugin files">
1665
- <div class="studio-file-list">${fileList}</div>
1666
- </aside>
1667
- ${renderStudioResizer("files", "h", { label: "files panel" })}
2091
+ ${renderStudioActivityBar(panels)}
2092
+ ${
2093
+ panels.files
2094
+ ? `<aside class="studio-files" aria-label="Explorer">
2095
+ <header class="studio-pane-header">
2096
+ <strong>Explorer</strong>
2097
+ <button class="studio-pane-action" type="button" data-action="studio-toggle-files" aria-label="Hide Explorer" title="Hide Explorer">
2098
+ <span class="dashicons dashicons-no-alt" aria-hidden="true"></span>
2099
+ </button>
2100
+ </header>
2101
+ <div class="studio-file-list">${fileList}</div>
2102
+ </aside>
2103
+ ${renderStudioResizer("files", "h", { label: "Explorer" })}`
2104
+ : ""
2105
+ }
1668
2106
  <section class="studio-workbench" aria-label="Studio editor">
1669
2107
  <div class="studio-tabs" aria-label="Studio tabs and Playground controls">
1670
2108
  <div class="studio-tablist" role="tablist" aria-label="Studio tabs">
1671
- <button type="button" role="tab" aria-selected="${state.studio.activeTab === "editor" ? "true" : "false"}" class="studio-tab-button studio-editor-tab${state.studio.activeTab === "editor" ? " is-active" : ""}" data-action="studio-tab" data-tab="editor">
1672
- <span class="dashicons ${studioFileIcon(editorTabLabel)}" aria-hidden="true"></span>
1673
- <span>${escapeHtml(editorTabLabel)}</span>
1674
- ${state.studio.dirty ? `<em aria-label="Unsaved changes"></em>` : ""}
1675
- </button>
1676
- <button type="button" role="tab" aria-selected="${state.studio.activeTab === "home" ? "true" : "false"}" class="studio-tab-button studio-preview-tab${state.studio.activeTab === "home" ? " is-active" : ""}" data-action="studio-tab" data-tab="home">
1677
- <span class="dashicons dashicons-admin-home" aria-hidden="true"></span>
1678
- <span>Home</span>
1679
- ${playgroundPort ? `<small>${escapeHtml(`:${playgroundPort}`)}</small>` : ""}
1680
- </button>
1681
- <button type="button" role="tab" aria-selected="${state.studio.activeTab === "admin" ? "true" : "false"}" class="studio-tab-button studio-preview-tab${state.studio.activeTab === "admin" ? " is-active" : ""}" data-action="studio-tab" data-tab="admin">
1682
- <span class="dashicons dashicons-admin-site-alt3" aria-hidden="true"></span>
1683
- <span>WP Admin</span>
1684
- ${state.studio.playgroundUrl ? `<small>admin/password</small>` : ""}
1685
- </button>
2109
+ ${renderStudioPinnedPreviewTabs(playgroundPort)}
2110
+ ${renderStudioFileTabs()}
1686
2111
  </div>
1687
- ${renderStudioPlayButton()}
1688
- <span class="studio-preview-state${state.studio.running ? " is-loading" : state.studio.playgroundUrl ? " is-ready" : ""}">
1689
- <span aria-hidden="true"></span>
1690
- ${escapeHtml(studioPreviewStateLabel())}
1691
- </span>
1692
2112
  <span class="studio-tab-spacer"></span>
1693
2113
  <span id="studio-editor-status">${escapeHtml(studioEditorStatusLabel())}</span>
1694
2114
  </div>
@@ -1706,23 +2126,202 @@ function renderStudio() {
1706
2126
  <div id="studio-terminal-output" class="studio-terminal-output">
1707
2127
  ${renderStudioTerminal()}
1708
2128
  </div>
1709
- </section>`
2129
+ </section>`
1710
2130
  : ""
1711
2131
  }
1712
2132
  </section>
1713
- ${renderStudioResizer("ai", "h", { invert: true, label: "AI panel" })}
1714
- <aside class="studio-ai" id="studio-ai" aria-label="Studio AI chat">
1715
- ${renderStudioAiSidebar()}
1716
- </aside>
2133
+ ${
2134
+ panels.sidebar
2135
+ ? `${renderStudioResizer("ai", "h", { invert: true, label: "Secondary Side Bar" })}
2136
+ <aside class="studio-ai" id="studio-ai" aria-label="Secondary Side Bar">
2137
+ ${renderStudioAiSidebar()}
2138
+ </aside>`
2139
+ : ""
2140
+ }
1717
2141
  </div>
2142
+ ${renderStudioStatusBar()}
1718
2143
  ${renderStudioPlaygroundVersionModal()}
1719
2144
  </div>
1720
2145
  `;
1721
2146
  applyStudioLayout(els.studio.querySelector(".studio-root"));
1722
2147
  bindStudioResizers();
2148
+ scrollActiveStudioFileTabIntoView();
1723
2149
  updateStudioControls();
1724
2150
  }
1725
2151
 
2152
+ function renderStudioActivityBar(panels) {
2153
+ const tab = state.studio.sidebarTab === "release" ? "release" : "ai";
2154
+ const activityButton = ({ action, icon, label, active, tab: targetTab }) => `
2155
+ <button class="studio-activity-button${active ? " is-active" : ""}" type="button" data-action="${escapeAttr(action)}"${targetTab ? ` data-tab="${escapeAttr(targetTab)}"` : ""} aria-pressed="${active ? "true" : "false"}" aria-label="${escapeAttr(label)}" title="${escapeAttr(label)}">
2156
+ <span class="dashicons ${escapeAttr(icon)}" aria-hidden="true"></span>
2157
+ </button>
2158
+ `;
2159
+
2160
+ return `
2161
+ <nav class="studio-activitybar" aria-label="Studio workbench views">
2162
+ <div class="studio-activitybar-primary">
2163
+ ${activityButton({
2164
+ action: "studio-toggle-files",
2165
+ icon: "dashicons-open-folder",
2166
+ label: panels.files ? "Hide Explorer" : "Show Explorer",
2167
+ active: panels.files
2168
+ })}
2169
+ ${activityButton({
2170
+ action: "studio-tab",
2171
+ icon: "dashicons-editor-code",
2172
+ label: "Editor",
2173
+ active: state.studio.activeTab === "editor",
2174
+ tab: "editor"
2175
+ })}
2176
+ ${activityButton({
2177
+ action: "studio-tab",
2178
+ icon: "dashicons-admin-home",
2179
+ label: "Playground Home",
2180
+ active: state.studio.activeTab === "home",
2181
+ tab: "home"
2182
+ })}
2183
+ </div>
2184
+ <div class="studio-activitybar-secondary">
2185
+ ${activityButton({
2186
+ action: "studio-open-sidebar-tab",
2187
+ icon: "dashicons-format-chat",
2188
+ label: "AI Helper",
2189
+ active: panels.sidebar && tab === "ai",
2190
+ tab: "ai"
2191
+ })}
2192
+ ${activityButton({
2193
+ action: "studio-open-sidebar-tab",
2194
+ icon: "dashicons-update",
2195
+ label: "Release",
2196
+ active: panels.sidebar && tab === "release",
2197
+ tab: "release"
2198
+ })}
2199
+ ${activityButton({
2200
+ action: "studio-toggle-terminal",
2201
+ icon: "dashicons-editor-kitchensink",
2202
+ label: state.studio.terminalOpen ? "Hide Terminal" : "Show Terminal",
2203
+ active: state.studio.terminalOpen
2204
+ })}
2205
+ </div>
2206
+ </nav>
2207
+ `;
2208
+ }
2209
+
2210
+ function renderStudioStatusBar() {
2211
+ const plugin = state.studio.plugin;
2212
+ const file = state.studio.selectedFile?.path ?? "No file selected";
2213
+ const check = state.studio.checkSummary
2214
+ ? `${state.studio.checkSummary.error || 0} errors, ${state.studio.checkSummary.warning || 0} warnings`
2215
+ : "Plugin Check idle";
2216
+ const assistant = selectedStudioAiAssistant();
2217
+ return `
2218
+ <footer class="studio-statusbar" aria-label="Studio status">
2219
+ <span><span class="dashicons dashicons-admin-plugins" aria-hidden="true"></span>${escapeHtml(plugin?.slug ?? state.studio.id ?? "Studio")}</span>
2220
+ <span>${escapeHtml(file)}</span>
2221
+ <span>${escapeHtml(check)}</span>
2222
+ <span class="studio-statusbar-spacer"></span>
2223
+ <span>${escapeHtml(assistant === "none" ? "AI disabled" : assistantLabel(assistant))}</span>
2224
+ <span>${escapeHtml(state.studio.readOnly ? "Read-only" : "Writable")}</span>
2225
+ </footer>
2226
+ `;
2227
+ }
2228
+
2229
+ function renderStudioPinnedPreviewTabs(playgroundPort) {
2230
+ return `
2231
+ <span class="studio-pinned-tabs" aria-label="Pinned preview tabs">
2232
+ <button type="button" role="tab" aria-selected="${state.studio.activeTab === "home" ? "true" : "false"}" class="studio-tab-button studio-tab-pinned studio-preview-tab${state.studio.activeTab === "home" ? " is-active" : ""}" data-action="studio-tab" data-tab="home" title="Home">
2233
+ <span class="dashicons dashicons-admin-home" aria-hidden="true"></span>
2234
+ <span>Home</span>
2235
+ ${playgroundPort ? `<small>${escapeHtml(`:${playgroundPort}`)}</small>` : ""}
2236
+ </button>
2237
+ <button type="button" role="tab" aria-selected="${state.studio.activeTab === "admin" ? "true" : "false"}" class="studio-tab-button studio-tab-pinned studio-preview-tab${state.studio.activeTab === "admin" ? " is-active" : ""}" data-action="studio-tab" data-tab="admin" title="WP Admin">
2238
+ <span class="dashicons dashicons-admin-site-alt3" aria-hidden="true"></span>
2239
+ <span>WP Admin</span>
2240
+ ${state.studio.playgroundUrl ? `<small>admin/password</small>` : ""}
2241
+ </button>
2242
+ </span>
2243
+ `;
2244
+ }
2245
+
2246
+ function renderStudioFileTabs() {
2247
+ const openFiles = studioOpenFileTabs();
2248
+ if (!openFiles.length) {
2249
+ return `<span class="studio-file-tabs-empty">Open a file from Explorer</span>`;
2250
+ }
2251
+ return `
2252
+ <span class="studio-file-tabs" aria-label="Open files">
2253
+ ${openFiles.map((file) => renderStudioFileTab(file)).join("")}
2254
+ </span>
2255
+ `;
2256
+ }
2257
+
2258
+ function studioOpenFileTabs() {
2259
+ const knownFiles = new Map(state.studio.files.map((file) => [file.path, file]));
2260
+ const selectedPath = state.studio.selectedFile?.path;
2261
+ const paths = Array.isArray(state.studio.openFiles) ? [...state.studio.openFiles] : [];
2262
+ if (selectedPath && !paths.includes(selectedPath)) {
2263
+ paths.push(selectedPath);
2264
+ }
2265
+ state.studio.openFiles = paths;
2266
+ return paths.map((path) => knownFiles.get(path) ?? {
2267
+ path,
2268
+ name: path.split("/").pop() ?? path,
2269
+ directory: path.includes("/") ? path.split("/").slice(0, -1).join("/") : "",
2270
+ size: 0
2271
+ });
2272
+ }
2273
+
2274
+ function renderStudioFileTab(file) {
2275
+ const current = state.studio.activeTab === "editor" && file.path === state.studio.selectedFile?.path;
2276
+ const dirty = current && state.studio.dirty;
2277
+ return `
2278
+ <span class="studio-file-tab-wrap">
2279
+ <button type="button" role="tab" aria-selected="${current ? "true" : "false"}" class="studio-tab-button studio-editor-tab${current ? " is-active" : ""}" data-action="studio-file" data-path="${escapeAttr(file.path)}" title="${escapeAttr(file.path)}">
2280
+ <span class="dashicons ${studioFileIcon(file.path)}" aria-hidden="true"></span>
2281
+ <span>${escapeHtml(file.name ?? file.path)}</span>
2282
+ ${dirty ? `<em aria-label="Unsaved changes"></em>` : ""}
2283
+ </button>
2284
+ <button type="button" class="studio-tab-close" data-action="studio-close-file-tab" data-path="${escapeAttr(file.path)}" aria-label="Close ${escapeAttr(file.name ?? file.path)}" title="Close">
2285
+ <span class="dashicons dashicons-no-alt" aria-hidden="true"></span>
2286
+ </button>
2287
+ </span>
2288
+ `;
2289
+ }
2290
+
2291
+ function scrollActiveStudioFileTabIntoView() {
2292
+ if (state.studio.activeTab !== "editor" || !state.studio.selectedFile?.path) {
2293
+ return;
2294
+ }
2295
+ requestAnimationFrame(() => {
2296
+ const container = document.querySelector(".studio-file-tabs");
2297
+ if (!container) {
2298
+ return;
2299
+ }
2300
+ const selectedPath = state.studio.selectedFile?.path;
2301
+ const activeTab = Array.from(container.querySelectorAll(".studio-tab-button[data-path]")).find(
2302
+ (button) => button.dataset.path === selectedPath
2303
+ );
2304
+ const target = activeTab?.closest(".studio-file-tab-wrap") ?? activeTab;
2305
+ if (!target) {
2306
+ return;
2307
+ }
2308
+
2309
+ const padding = 12;
2310
+ const containerRect = container.getBoundingClientRect();
2311
+ const targetRect = target.getBoundingClientRect();
2312
+ const targetLeft = targetRect.left - containerRect.left + container.scrollLeft;
2313
+ const targetRight = targetLeft + targetRect.width;
2314
+ const visibleLeft = container.scrollLeft;
2315
+ const visibleRight = visibleLeft + container.clientWidth;
2316
+
2317
+ if (targetLeft < visibleLeft + padding) {
2318
+ container.scrollLeft = Math.max(0, targetLeft - padding);
2319
+ } else if (targetRight > visibleRight - padding) {
2320
+ container.scrollLeft = targetRight - container.clientWidth + padding;
2321
+ }
2322
+ });
2323
+ }
2324
+
1726
2325
  function renderStudioPlaygroundVersionModal() {
1727
2326
  const modal = state.studio.playgroundVersionModal;
1728
2327
  if (!modal) {
@@ -1879,6 +2478,21 @@ function renderStudioPlayButton() {
1879
2478
  `;
1880
2479
  }
1881
2480
 
2481
+ function renderStudioThemeToggle() {
2482
+ const theme = studioTheme();
2483
+ const isLight = theme === "light";
2484
+ const nextTheme = isLight ? "dark" : "light";
2485
+ return `
2486
+ <button class="studio-theme-switch" type="button" role="switch" aria-checked="${isLight ? "true" : "false"}" data-action="studio-toggle-theme" title="${escapeAttr(`Switch to ${nextTheme} mode`)}">
2487
+ <span class="dashicons dashicons-admin-appearance" aria-hidden="true"></span>
2488
+ <span class="studio-theme-switch-track" aria-hidden="true">
2489
+ <span></span>
2490
+ </span>
2491
+ <span class="studio-theme-switch-label">${escapeHtml(isLight ? "Light" : "Dark")}</span>
2492
+ </button>
2493
+ `;
2494
+ }
2495
+
1882
2496
  function studioPreviewStateLabel() {
1883
2497
  if (state.studio.running) {
1884
2498
  return "Starting Playground";
@@ -1906,10 +2520,62 @@ function switchStudioTab(tab) {
1906
2520
  if (!["editor", "home", "admin"].includes(tab)) {
1907
2521
  return;
1908
2522
  }
1909
- captureStudioEditorValue();
2523
+ const discardingDirtyEditor =
2524
+ state.studio.activeTab === "editor" &&
2525
+ tab !== "editor" &&
2526
+ state.studio.dirty;
2527
+ if (discardingDirtyEditor) {
2528
+ if (!confirm("Discard unsaved changes in the current file?")) {
2529
+ return;
2530
+ }
2531
+ state.studio.draftContent = state.studio.fileContent;
2532
+ state.studio.dirty = false;
2533
+ } else {
2534
+ captureStudioEditorValue();
2535
+ }
1910
2536
  state.studio.activeTab = tab;
1911
2537
  renderStudio();
1912
2538
  remountStudioEditorIfNeeded();
2539
+ updateRouteFromState();
2540
+ }
2541
+
2542
+ async function closeStudioFileTab(relativePath) {
2543
+ if (!relativePath) {
2544
+ return;
2545
+ }
2546
+ const openFiles = Array.isArray(state.studio.openFiles) ? state.studio.openFiles : [];
2547
+ const index = openFiles.indexOf(relativePath);
2548
+ if (index === -1) {
2549
+ return;
2550
+ }
2551
+ const isCurrent = relativePath === state.studio.selectedFile?.path;
2552
+ if (isCurrent && state.studio.dirty && !confirm("Discard unsaved changes in the current file?")) {
2553
+ return;
2554
+ }
2555
+
2556
+ captureStudioEditorValue();
2557
+ const nextOpenFiles = openFiles.filter((path) => path !== relativePath);
2558
+ state.studio.openFiles = nextOpenFiles;
2559
+
2560
+ if (!isCurrent) {
2561
+ renderStudio();
2562
+ remountStudioEditorIfNeeded();
2563
+ return;
2564
+ }
2565
+
2566
+ state.studio.dirty = false;
2567
+ const nextPath = nextOpenFiles[Math.min(index, nextOpenFiles.length - 1)];
2568
+ if (nextPath) {
2569
+ await selectStudioFile(nextPath, { force: true });
2570
+ return;
2571
+ }
2572
+
2573
+ state.studio.selectedFile = null;
2574
+ state.studio.fileContent = "";
2575
+ state.studio.draftContent = "";
2576
+ state.studio.activeTab = "home";
2577
+ renderStudio();
2578
+ updateRouteFromState();
1913
2579
  }
1914
2580
 
1915
2581
  function toggleStudioTerminal() {
@@ -1919,6 +2585,39 @@ function toggleStudioTerminal() {
1919
2585
  remountStudioEditorIfNeeded();
1920
2586
  }
1921
2587
 
2588
+ function toggleStudioPanel(panel) {
2589
+ if (!["files", "sidebar"].includes(panel)) {
2590
+ return;
2591
+ }
2592
+ captureStudioEditorValue();
2593
+ state.studio.panels = normalizeStudioPanelState(state.studio.panels);
2594
+ state.studio.panels[panel] = !state.studio.panels[panel];
2595
+ saveStudioPanelState(state.studio.panels);
2596
+ renderStudio();
2597
+ remountStudioEditorIfNeeded();
2598
+ requestAnimationFrame(() => state.studio.editor?.layout?.());
2599
+ }
2600
+
2601
+ function openStudioSidebarTab(tab) {
2602
+ captureStudioEditorValue();
2603
+ state.studio.panels = normalizeStudioPanelState(state.studio.panels);
2604
+ state.studio.panels.sidebar = true;
2605
+ saveStudioPanelState(state.studio.panels);
2606
+ setStudioSidebarTab(tab);
2607
+ renderStudio();
2608
+ remountStudioEditorIfNeeded();
2609
+ requestAnimationFrame(() => state.studio.editor?.layout?.());
2610
+ }
2611
+
2612
+ function toggleStudioTheme() {
2613
+ captureStudioEditorValue();
2614
+ state.studio.theme = studioTheme() === "light" ? "dark" : "light";
2615
+ saveStudioTheme(state.studio.theme);
2616
+ renderStudio();
2617
+ remountStudioEditorIfNeeded();
2618
+ requestAnimationFrame(() => state.studio.editor?.layout?.());
2619
+ }
2620
+
1922
2621
  function toggleStudioFolder(folderPath) {
1923
2622
  if (!folderPath) {
1924
2623
  return;
@@ -2027,6 +2726,12 @@ function renderStudioAiSidebar() {
2027
2726
 
2028
2727
  function renderStudioSidebarTabs(activeTab) {
2029
2728
  return `
2729
+ <header class="studio-secondary-header">
2730
+ <strong>${activeTab === "release" ? "Release" : "AI Helper"}</strong>
2731
+ <button class="studio-pane-action" type="button" data-action="studio-toggle-sidebar" aria-label="Hide Secondary Side Bar" title="Hide Secondary Side Bar">
2732
+ <span class="dashicons dashicons-no-alt" aria-hidden="true"></span>
2733
+ </button>
2734
+ </header>
2030
2735
  <div class="studio-sidebar-tabs ps-segmented" role="tablist" aria-label="Studio sidebar">
2031
2736
  <button class="ps-segmented-option${activeTab === "ai" ? " is-active" : ""}" type="button" role="tab" aria-selected="${activeTab === "ai"}" data-action="studio-sidebar-tab" data-tab="ai">
2032
2737
  <span class="dashicons dashicons-format-chat" aria-hidden="true"></span>
@@ -2245,7 +2950,7 @@ function renderStudioAiMessages() {
2245
2950
  <span>${escapeHtml(aiMessageRoleLabel(message))}</span>
2246
2951
  <time>${escapeHtml(formatTime(message.createdAt))}</time>
2247
2952
  </header>
2248
- <p>${escapeHtml(message.text)}</p>
2953
+ <div class="studio-ai-markdown">${renderStudioAiMarkdown(message.text)}</div>
2249
2954
  </div>
2250
2955
  </article>
2251
2956
  `
@@ -2711,7 +3416,7 @@ function appendStudioAiOutput(text, tone = "log") {
2711
3416
  }
2712
3417
  last.text = `${last.text}${output}`;
2713
3418
  last.tone = tone === "error" ? "error" : last.tone === "status" ? "log" : last.tone;
2714
- updateStudioAiSidebar();
3419
+ updateStudioAiMessageList();
2715
3420
  }
2716
3421
 
2717
3422
  function updateStudioAiSidebar() {
@@ -2723,11 +3428,40 @@ function updateStudioAiSidebar() {
2723
3428
  node.innerHTML = renderStudioAiSidebar();
2724
3429
  const messages = document.getElementById("studio-ai-messages");
2725
3430
  if (messages) {
2726
- messages.scrollTop = messages.scrollHeight;
3431
+ scrollStudioAiMessagesToBottom(messages);
2727
3432
  }
2728
3433
  updateStudioAiControls();
2729
3434
  }
2730
3435
 
3436
+ function updateStudioAiMessageList(options = {}) {
3437
+ const messages = document.getElementById("studio-ai-messages");
3438
+ if (!messages) {
3439
+ updateStudioAiSidebar();
3440
+ return;
3441
+ }
3442
+
3443
+ const shouldStick = options.forceScroll || isStudioAiMessagesNearBottom(messages);
3444
+ messages.innerHTML = renderStudioAiMessages();
3445
+ if (shouldStick) {
3446
+ scrollStudioAiMessagesToBottom(messages);
3447
+ }
3448
+ updateStudioAiControls();
3449
+ }
3450
+
3451
+ function isStudioAiMessagesNearBottom(messages) {
3452
+ return messages.scrollHeight - messages.scrollTop - messages.clientHeight < 80;
3453
+ }
3454
+
3455
+ function scrollStudioAiMessagesToBottom(messages) {
3456
+ const previousScrollBehavior = messages.style.scrollBehavior;
3457
+ messages.style.scrollBehavior = "auto";
3458
+ messages.scrollTop = messages.scrollHeight;
3459
+ requestAnimationFrame(() => {
3460
+ messages.scrollTop = messages.scrollHeight;
3461
+ messages.style.scrollBehavior = previousScrollBehavior;
3462
+ });
3463
+ }
3464
+
2731
3465
  function updateStudioAiControls() {
2732
3466
  const prompt = document.getElementById("studio-ai-prompt");
2733
3467
  const send = document.getElementById("studio-ai-send-button");
@@ -3078,7 +3812,7 @@ async function mountStudioEditor(content) {
3078
3812
  );
3079
3813
  state.studio.editorModels = [originalModel, modifiedModel];
3080
3814
  state.studio.editor = monaco.editor.createDiffEditor(container, {
3081
- theme: "vs-dark",
3815
+ theme: studioMonacoTheme(),
3082
3816
  readOnly: true,
3083
3817
  automaticLayout: true,
3084
3818
  minimap: { enabled: false },
@@ -3101,7 +3835,7 @@ async function mountStudioEditor(content) {
3101
3835
  state.studio.editor = monaco.editor.create(container, {
3102
3836
  value: content,
3103
3837
  language: languageForPath(state.studio.selectedFile?.path ?? ""),
3104
- theme: "vs-dark",
3838
+ theme: studioMonacoTheme(),
3105
3839
  readOnly: state.studio.readOnly,
3106
3840
  automaticLayout: true,
3107
3841
  minimap: { enabled: false },
@@ -4784,36 +5518,35 @@ function configureAutoRefresh() {
4784
5518
  * View switching with view-transitions
4785
5519
  * =================================================================== */
4786
5520
 
4787
- function showView(view) {
4788
- if (state.activeView === view) {
4789
- return;
5521
+ function showView(view, options = {}) {
5522
+ const nextView = normalizeViewId(view);
5523
+ if (state.activeView === nextView) {
5524
+ if (options.updateRoute !== false) {
5525
+ updateRouteFromState({ replace: options.replaceRoute });
5526
+ }
5527
+ return Promise.resolve();
4790
5528
  }
4791
5529
  const apply = () => {
4792
- state.activeView = view;
4793
- document.body.dataset.activeView = view;
4794
- document.querySelectorAll(".view").forEach((node) => node.classList.remove("is-active"));
4795
- document.getElementById(`view-${view}`)?.classList.add("is-active");
4796
- document
4797
- .querySelectorAll("#adminmenu li")
4798
- .forEach((node) => node.classList.remove("wp-has-current-submenu"));
4799
- document
4800
- .querySelector(`#adminmenu li[data-view="${view}"]`)
4801
- ?.classList.add("wp-has-current-submenu");
5530
+ applyActiveViewShell(nextView);
4802
5531
  closeDetail();
4803
- if (view === "release") {
5532
+ if (nextView === "release") {
4804
5533
  if (!state.releaseBoard.loading && !state.releaseBoard.plugins.length) {
4805
5534
  void loadReleaseBoard();
4806
5535
  } else {
4807
5536
  renderReleaseBoard();
4808
5537
  }
4809
5538
  }
5539
+ if (options.updateRoute !== false) {
5540
+ updateRouteFromState({ replace: options.replaceRoute });
5541
+ }
4810
5542
  };
4811
5543
 
4812
5544
  if (typeof document.startViewTransition === "function") {
4813
- document.startViewTransition(apply);
4814
- } else {
4815
- apply();
5545
+ const transition = document.startViewTransition(apply);
5546
+ return transition.updateCallbackDone?.catch(() => {}) ?? Promise.resolve();
4816
5547
  }
5548
+ apply();
5549
+ return Promise.resolve();
4817
5550
  }
4818
5551
 
4819
5552
  /* ===================================================================
@@ -5215,6 +5948,16 @@ function renderStudioAiMarkdown(value) {
5215
5948
  }
5216
5949
  }
5217
5950
 
5951
+ function refreshStudioAiMarkdownIfReady() {
5952
+ try {
5953
+ if ((state.studio.aiMessages ?? []).some((message) => message.role === "assistant")) {
5954
+ updateStudioAiMessageList();
5955
+ }
5956
+ } catch {
5957
+ // The markdown parser can finish loading before Studio state exists.
5958
+ }
5959
+ }
5960
+
5218
5961
  function basicMarkdownToHtml(value) {
5219
5962
  const lines = String(value ?? "").replace(/\r\n?/g, "\n").split("\n");
5220
5963
  const blocks = [];
@@ -5649,9 +6392,6 @@ function studioPluginKey() {
5649
6392
 
5650
6393
  function setStudioSidebarTab(tab) {
5651
6394
  const next = tab === "release" ? "release" : "ai";
5652
- if (state.studio.sidebarTab === next) {
5653
- return;
5654
- }
5655
6395
  state.studio.sidebarTab = next;
5656
6396
  saveStudioSidebarTab(studioPluginKey(), next);
5657
6397
  updateStudioSidebar();
@@ -5715,7 +6455,7 @@ function renderStudioReleasePane() {
5715
6455
  </header>
5716
6456
  <ol class="ps-release-funnel">
5717
6457
  ${renderStudioReleaseStepVersion(versionState, release)}
5718
- ${renderStudioReleaseStepTags(release)}
6458
+ ${renderStudioReleaseStepTags(versionState, release)}
5719
6459
  ${renderStudioReleaseStepValidate(versionState, release)}
5720
6460
  ${renderStudioReleaseStepPublish(versionState, release)}
5721
6461
  </ol>
@@ -5793,7 +6533,11 @@ function renderStudioReleaseStepVersion(versionState, release) {
5793
6533
  return renderStudioReleaseStepShell(1, "Version state", summary, body);
5794
6534
  }
5795
6535
 
5796
- function renderStudioReleaseStepTags(release) {
6536
+ function releaseTagDraftValue(versionState, release) {
6537
+ return release.newTagDraft || versionState?.localVersion || "";
6538
+ }
6539
+
6540
+ function renderStudioReleaseStepTags(versionState, release) {
5797
6541
  let body;
5798
6542
  if (release.tagsLoading && !release.tags) {
5799
6543
  body = loadingShell("Reading SVN tags…");
@@ -5814,22 +6558,31 @@ function renderStudioReleaseStepTags(release) {
5814
6558
  const tagRows = (list.tags ?? [])
5815
6559
  .map((tag) => renderStudioReleaseTagRow(tag))
5816
6560
  .join("");
6561
+ const newTagValue = releaseTagDraftValue(versionState, release);
6562
+ const currentVersionTag = versionState?.localVersion
6563
+ ? (list.tags ?? []).find((tag) => tag.name === versionState.localVersion)
6564
+ : null;
6565
+ const newTagControl = currentVersionTag
6566
+ ? currentVersionTag.isUncommitted
6567
+ ? `<p class="ps-release-tag-ready"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>${escapeHtml(`Tag ${versionState.localVersion} is ready. Run a dry-run release next.`)}</p>`
6568
+ : `<p class="ps-release-tag-ready is-blocked"><span class="dashicons dashicons-warning" aria-hidden="true"></span>${escapeHtml(`Tag ${versionState.localVersion} already exists on WordPress.org SVN. Bump the version before creating a new tag.`)}</p>`
6569
+ : `<div class="ps-release-step-newtag">
6570
+ <label>
6571
+ <span>Create tag from current version</span>
6572
+ <input type="text" id="studio-release-new-tag" value="${escapeAttr(newTagValue)}" placeholder="1.2.3" />
6573
+ </label>
6574
+ <button class="button button-secondary" type="button" data-action="studio-release-create">
6575
+ <span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>
6576
+ Create tag
6577
+ </button>
6578
+ </div>`;
5817
6579
  body = `
5818
6580
  <ul class="ps-release-tag-list">
5819
6581
  ${trunkRow}
5820
6582
  ${tagRows || `<li class="ps-release-tag-empty">No tags yet.</li>`}
5821
6583
  </ul>
5822
6584
  ${renderStudioReleaseSwitchConflict(release)}
5823
- <div class="ps-release-step-newtag">
5824
- <label>
5825
- <span>Create tag from current version</span>
5826
- <input type="text" id="studio-release-new-tag" value="${escapeAttr(release.newTagDraft ?? "")}" placeholder="1.2.3" />
5827
- </label>
5828
- <button class="button button-secondary" type="button" data-action="studio-release-create">
5829
- <span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>
5830
- Create tag
5831
- </button>
5832
- </div>
6585
+ ${newTagControl}
5833
6586
  ${release.newTagError ? `<p class="ps-release-inline-error"><span class="dashicons dashicons-warning" aria-hidden="true"></span>${escapeHtml(release.newTagError)}</p>` : ""}
5834
6587
  `;
5835
6588
  }
@@ -5881,9 +6634,11 @@ function renderStudioReleaseTagRow(tag) {
5881
6634
  const switchingLabel = release.switchingResolution === "override" || release.switchingResolution === "revert"
5882
6635
  ? "Resolving"
5883
6636
  : "Switching";
5884
- const switchButton = tag.isCurrent
5885
- ? ""
5886
- : `<button class="button button-small" type="button" data-action="studio-release-switch" data-tag="${escapeAttr(tag.name)}" ${anySwitching ? "disabled aria-disabled=\"true\"" : ""}>${switching ? `<span class="dashicons dashicons-update" aria-hidden="true"></span>${switchingLabel}` : "Switch"}</button>`;
6637
+ const switchButton = tag.isUncommitted
6638
+ ? `<span class="ps-release-tag-local-note" title="This tag exists locally and will be published by the release step.">Ready for release</span>`
6639
+ : tag.isCurrent
6640
+ ? ""
6641
+ : `<button class="button button-small" type="button" data-action="studio-release-switch" data-tag="${escapeAttr(tag.name)}" ${anySwitching ? "disabled aria-disabled=\"true\"" : ""}>${switching ? `<span class="dashicons dashicons-update" aria-hidden="true"></span>${switchingLabel}` : "Switch"}</button>`;
5887
6642
  const confirmKey = `delete-tag:${tag.name}`;
5888
6643
  const pendingConfirm = state.studio.pendingConfirms?.get(confirmKey);
5889
6644
  const deleteButton = tag.isUncommitted
@@ -5939,6 +6694,117 @@ function renderStudioReleaseStepValidate(versionState, release) {
5939
6694
  return renderStudioReleaseStepShell(3, "Validate", summaryLine, body);
5940
6695
  }
5941
6696
 
6697
+ function studioPublishRouteDetails(action) {
6698
+ switch (action) {
6699
+ case "submit":
6700
+ return {
6701
+ label: "Submit new plugin",
6702
+ shortLabel: "submit",
6703
+ icon: "dashicons-upload",
6704
+ description: "Use this for a first WordPress.org review. Pressship builds a package and uploads it to the plugin submission flow after confirmation.",
6705
+ dryRunLabel: "Dry-run submit",
6706
+ confirmLabel: "Confirm submit",
6707
+ resultLabel: "WordPress.org submission"
6708
+ };
6709
+ case "release":
6710
+ return {
6711
+ label: "Release update",
6712
+ shortLabel: "release",
6713
+ icon: "dashicons-update",
6714
+ description: "Use this for an approved plugin that already has an SVN repository. Pressship validates the package and publishes the current version after confirmation.",
6715
+ dryRunLabel: "Dry-run release",
6716
+ confirmLabel: "Confirm release",
6717
+ resultLabel: "SVN release"
6718
+ };
6719
+ default:
6720
+ return {
6721
+ label: "Auto decide",
6722
+ shortLabel: "auto",
6723
+ icon: "dashicons-controls-play",
6724
+ description: "Best default. Pressship checks WordPress.org and SVN, then chooses submit for first-time review or release for an existing plugin.",
6725
+ dryRunLabel: "Dry-run auto",
6726
+ confirmLabel: "Confirm publish",
6727
+ resultLabel: "publish route"
6728
+ };
6729
+ }
6730
+ }
6731
+
6732
+ function renderStudioReleasePublishOption(action, options = {}) {
6733
+ const details = studioPublishRouteDetails(action);
6734
+ const active = options.activeAction === action;
6735
+ const chosen = options.detectedRoute === action;
6736
+ const isDefault = options.defaultAction === action;
6737
+ const recommended = action === "auto";
6738
+ const disabled = options.running ? "disabled aria-disabled=\"true\"" : "";
6739
+ const badges = [
6740
+ recommended ? "Recommended" : "",
6741
+ isDefault ? "Default" : "",
6742
+ chosen ? "Selected" : ""
6743
+ ].filter(Boolean);
6744
+
6745
+ return `
6746
+ <button class="ps-release-publish-option${active ? " is-active" : ""}${chosen ? " is-selected" : ""}" type="button" data-action="dry-run-publish" data-id="${escapeAttr(state.studio.id)}" data-publish-action="${escapeAttr(action)}" ${disabled}>
6747
+ <span class="ps-release-option-icon dashicons ${escapeAttr(details.icon)}" aria-hidden="true"></span>
6748
+ <span class="ps-release-option-content">
6749
+ <span class="ps-release-option-title">
6750
+ <strong>${escapeHtml(details.label)}</strong>
6751
+ ${badges.map((badge) => `<em>${escapeHtml(badge)}</em>`).join("")}
6752
+ </span>
6753
+ <span class="ps-release-option-copy">${escapeHtml(details.description)}</span>
6754
+ </span>
6755
+ <span class="ps-release-option-action">${escapeHtml(details.dryRunLabel)}</span>
6756
+ </button>
6757
+ `;
6758
+ }
6759
+
6760
+ function renderStudioReleasePublishSummary(dryRun, pendingConfirm, running) {
6761
+ if (running) {
6762
+ return `
6763
+ <div class="ps-release-publish-summary is-running" role="status">
6764
+ <span class="dashicons dashicons-update" aria-hidden="true"></span>
6765
+ <p>
6766
+ <strong>Previewing publish route</strong>
6767
+ <small>Validating the package and checking which path is safe to confirm.</small>
6768
+ </p>
6769
+ </div>
6770
+ `;
6771
+ }
6772
+
6773
+ if (!dryRun) {
6774
+ return `
6775
+ <p class="ps-release-step-muted">
6776
+ Pick a dry-run path above. A dry-run validates the package and shows exactly what will happen; nothing is uploaded or committed until you confirm.
6777
+ </p>
6778
+ `;
6779
+ }
6780
+
6781
+ const routeAction = dryRun.route?.action ?? "publish";
6782
+ const details = studioPublishRouteDetails(routeAction);
6783
+ const packageSummary = dryRun.package?.fileCount
6784
+ ? `${dryRun.package.fileCount} packaged files`
6785
+ : dryRun.package?.topLevelFolder
6786
+ ? `Package folder: ${dryRun.package.topLevelFolder}`
6787
+ : "";
6788
+
6789
+ return `
6790
+ <div class="ps-release-publish-summary">
6791
+ <div class="ps-release-publish-result">
6792
+ <span class="dashicons ${escapeAttr(details.icon)}" aria-hidden="true"></span>
6793
+ <p>
6794
+ <strong>${escapeHtml(details.label)}</strong>
6795
+ <small>${escapeHtml(dryRun.route?.reason ?? `Ready to preview ${details.resultLabel}.`)}</small>
6796
+ ${packageSummary ? `<small>${escapeHtml(packageSummary)}</small>` : ""}
6797
+ </p>
6798
+ </div>
6799
+ ${dryRun.canConfirm && dryRun.approvalId
6800
+ ? `<button class="button button-primary ps-release-confirm-button${pendingConfirm ? " is-confirming" : ""}" type="button" data-action="studio-release-publish" data-approval-id="${escapeAttr(dryRun.approvalId)}" data-action-label="${escapeAttr(routeAction)}">
6801
+ ${pendingConfirm ? "Click again to confirm" : escapeHtml(details.confirmLabel)}
6802
+ </button>`
6803
+ : `<p class="ps-release-step-muted">Dry-run did not pass. Fix validation or version findings before publishing.</p>`}
6804
+ </div>
6805
+ `;
6806
+ }
6807
+
5942
6808
  function renderStudioReleaseStepPublish(versionState, release) {
5943
6809
  const defaultAction = state.settings?.defaultPublishAction ?? "auto";
5944
6810
  const dryRun = release.dryRun;
@@ -5946,39 +6812,23 @@ function renderStudioReleaseStepPublish(versionState, release) {
5946
6812
  const publishConfirmKey = "publish";
5947
6813
  const pendingConfirm = state.studio.pendingConfirms?.get(publishConfirmKey);
5948
6814
  const detectedRoute = dryRun?.route?.action;
6815
+ const activeAction = detectedRoute ?? defaultAction;
5949
6816
 
5950
6817
  const body = `
6818
+ <div class="ps-release-publish-guide">
6819
+ <strong>Dry-run first, confirm second.</strong>
6820
+ <span>Choose the route you want to preview. The dry-run is safe; the later confirm button performs the upload or SVN publish.</span>
6821
+ </div>
5951
6822
  <div class="ps-release-publish-options">
5952
- <button class="button${detectedRoute === "submit" ? " button-primary" : ""}" type="button" data-action="dry-run-publish" data-id="${escapeAttr(state.studio.id)}" data-publish-action="submit" ${running ? "disabled" : ""}>
5953
- <span class="dashicons dashicons-upload" aria-hidden="true"></span>
5954
- Dry-run submit
5955
- </button>
5956
- <button class="button${detectedRoute === "release" ? " button-primary" : ""}" type="button" data-action="dry-run-publish" data-id="${escapeAttr(state.studio.id)}" data-publish-action="release" ${running ? "disabled" : ""}>
5957
- <span class="dashicons dashicons-update" aria-hidden="true"></span>
5958
- Dry-run release
5959
- </button>
5960
- <button class="button${defaultAction === "auto" ? " button-primary" : ""}" type="button" data-action="dry-run-publish" data-id="${escapeAttr(state.studio.id)}" data-publish-action="auto" ${running ? "disabled" : ""}>
5961
- <span class="dashicons dashicons-controls-play" aria-hidden="true"></span>
5962
- Auto dry-run
5963
- </button>
6823
+ ${renderStudioReleasePublishOption("auto", { activeAction, defaultAction, detectedRoute, running })}
6824
+ ${renderStudioReleasePublishOption("submit", { activeAction, defaultAction, detectedRoute, running })}
6825
+ ${renderStudioReleasePublishOption("release", { activeAction, defaultAction, detectedRoute, running })}
5964
6826
  </div>
5965
- ${dryRun
5966
- ? `<div class="ps-release-publish-summary">
5967
- <p>
5968
- <strong>${escapeHtml(dryRun.route?.action ?? "publish")}</strong>
5969
- <small>${escapeHtml(dryRun.route?.reason ?? "")}</small>
5970
- </p>
5971
- ${dryRun.canConfirm && dryRun.approvalId
5972
- ? `<button class="button button-primary ps-release-confirm-button${pendingConfirm ? " is-confirming" : ""}" type="button" data-action="studio-release-publish" data-approval-id="${escapeAttr(dryRun.approvalId)}" data-action-label="${escapeAttr(dryRun.route?.action ?? "publish")}">
5973
- ${pendingConfirm ? "Click again to confirm" : `Confirm ${escapeHtml(dryRun.route?.action ?? "publish")}`}
5974
- </button>`
5975
- : `<p class="ps-release-step-muted">Dry-run did not pass — fix validation findings before publishing.</p>`}
5976
- </div>`
5977
- : `<p class="ps-release-step-muted">Run a dry-run to preview the publish plan.</p>`}
6827
+ ${renderStudioReleasePublishSummary(dryRun, pendingConfirm, running)}
5978
6828
  `;
5979
6829
 
5980
6830
  const summary = dryRun
5981
- ? `Detected route: ${escapeHtml(dryRun.route?.action ?? "publish")}`
6831
+ ? `Detected route: ${escapeHtml(studioPublishRouteDetails(dryRun.route?.action).shortLabel)}`
5982
6832
  : versionState?.releaseBlocked
5983
6833
  ? "Blocked — fix version state before publishing"
5984
6834
  : "";
@@ -6060,10 +6910,51 @@ async function refreshStudioAfterReleaseSwitch(result = {}) {
6060
6910
  }
6061
6911
  }
6062
6912
 
6913
+ async function refreshStudioAfterVersionChange(localId) {
6914
+ if (!localId || state.studio.id !== localId || state.studio.scope !== "local") {
6915
+ return;
6916
+ }
6917
+
6918
+ const selectedPath = state.studio.selectedFile?.path;
6919
+ try {
6920
+ const [detail, filesResult, checkState, versionState] = await Promise.all([
6921
+ api(`/api/plugins/local/${encodeURIComponent(localId)}`),
6922
+ api(`/api/plugins/local/${encodeURIComponent(localId)}/files`),
6923
+ api(`/api/plugins/local/${encodeURIComponent(localId)}/check-state`).catch(() => ({ state: null })),
6924
+ api(`/api/plugins/local/${encodeURIComponent(localId)}/version-state`).catch(() => null)
6925
+ ]);
6926
+
6927
+ applyStudioPluginDetail("local", localId, detail);
6928
+ state.studio.files = filesResult.files ?? [];
6929
+ applyStudioCheckState(checkState.state);
6930
+ if (versionState) {
6931
+ state.versionStates.set(localId, versionState);
6932
+ if (!state.studio.release.newTagDraft || state.studio.release.newTagDraft === versionState.latestSvnTag) {
6933
+ state.studio.release.newTagDraft = versionState.localVersion ?? "";
6934
+ }
6935
+ }
6936
+
6937
+ const selectedStillExists = selectedPath && state.studio.files.some((file) => file.path === selectedPath);
6938
+ if (selectedStillExists) {
6939
+ await selectStudioFile(selectedPath, { force: true });
6940
+ } else {
6941
+ renderStudio();
6942
+ remountStudioEditorIfNeeded();
6943
+ }
6944
+ updateStudioSidebar();
6945
+ updateStudioControls();
6946
+ } catch (error) {
6947
+ appendStudioTerminal(`Studio reload after version change failed: ${error.message}`, "error");
6948
+ renderStudio();
6949
+ updateStudioSidebar();
6950
+ }
6951
+ }
6952
+
6063
6953
  async function createStudioReleaseTag() {
6064
6954
  if (!state.studio.id) return;
6065
6955
  const input = document.getElementById("studio-release-new-tag");
6066
- const name = (input?.value ?? state.studio.release.newTagDraft ?? "").trim();
6956
+ const versionState = state.versionStates.get(state.studio.id);
6957
+ const name = (input?.value ?? releaseTagDraftValue(versionState, state.studio.release)).trim();
6067
6958
  if (!name) {
6068
6959
  state.studio.release.newTagError = "Enter a tag name first.";
6069
6960
  updateStudioSidebar();
@@ -6174,7 +7065,7 @@ async function bumpStudioReleaseVersion(localId, bump) {
6174
7065
  }, 1500);
6175
7066
  await loadLocal();
6176
7067
  if (state.studio.id === localId) {
6177
- await refreshStudioVersionState();
7068
+ await refreshStudioAfterVersionChange(localId);
6178
7069
  }
6179
7070
  } catch (error) {
6180
7071
  state.studio.release.bumpInFlight = null;
@@ -6218,7 +7109,7 @@ async function setStudioCustomReleaseVersion(localId) {
6218
7109
  }, 1500);
6219
7110
  await loadLocal();
6220
7111
  if (state.studio.id === localId) {
6221
- await refreshStudioVersionState();
7112
+ await refreshStudioAfterVersionChange(localId);
6222
7113
  }
6223
7114
  } catch (error) {
6224
7115
  state.studio.release.bumpInFlight = null;
@@ -6231,6 +7122,9 @@ function applyStudioVersionChangeResult(localId, result) {
6231
7122
  if (result && typeof result === "object") {
6232
7123
  const { checkState, ...versionState } = result;
6233
7124
  state.versionStates.set(localId, versionState);
7125
+ if (state.studio.id === localId && versionState.localVersion) {
7126
+ state.studio.release.newTagDraft = versionState.localVersion;
7127
+ }
6234
7128
  }
6235
7129
  if (state.studio.id === localId && result && Object.prototype.hasOwnProperty.call(result, "checkState")) {
6236
7130
  applyStudioCheckState(result.checkState);
@@ -6246,6 +7140,9 @@ async function refreshStudioVersionState() {
6246
7140
  `/api/plugins/local/${encodeURIComponent(state.studio.id)}/version-state`
6247
7141
  );
6248
7142
  state.versionStates.set(state.studio.id, versionState);
7143
+ if (!state.studio.release.newTagDraft && versionState.localVersion) {
7144
+ state.studio.release.newTagDraft = versionState.localVersion;
7145
+ }
6249
7146
  updateStudioSidebar();
6250
7147
  } catch (error) {
6251
7148
  // ignore — sidebar will keep stale data and notice was already shown