+ );
+}
diff --git a/apps/web/src/layouts/main-panel-tabs/resolve-tab-icon.ts b/apps/web/src/layouts/main-panel-tabs/resolve-tab-icon.ts
index 722cd93920..3c198ebe2c 100644
--- a/apps/web/src/layouts/main-panel-tabs/resolve-tab-icon.ts
+++ b/apps/web/src/layouts/main-panel-tabs/resolve-tab-icon.ts
@@ -1,5 +1,6 @@
import type { ComponentType, SVGProps } from "react";
import {
+ ClockRewind,
BarChartSquare02,
CheckDone01,
Code02,
@@ -32,6 +33,7 @@ export type SystemTabId =
| "code"
| "content"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -46,6 +48,7 @@ export const SYSTEM_TAB_ICONS: Record = {
code: Code02,
content: File02,
assets: Package,
+ releases: ClockRewind,
hosting: Server01,
e2e: CheckDone01,
analytics: BarChartSquare02,
diff --git a/apps/web/src/layouts/main-panel-tabs/tab-route.ts b/apps/web/src/layouts/main-panel-tabs/tab-route.ts
index 85a28c98d0..bdd2364fa9 100644
--- a/apps/web/src/layouts/main-panel-tabs/tab-route.ts
+++ b/apps/web/src/layouts/main-panel-tabs/tab-route.ts
@@ -40,6 +40,7 @@ export type TabRouteLocation =
section:
| "settings"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -96,6 +97,7 @@ export type TabRouteTarget =
to:
| typeof PROJECT_ROUTE.settings
| typeof PROJECT_ROUTE.assets
+ | typeof PROJECT_ROUTE.releases
| typeof PROJECT_ROUTE.hosting
| typeof PROJECT_ROUTE.e2e
| typeof PROJECT_ROUTE.analytics
@@ -232,6 +234,7 @@ const AGENT_SECTION_BY_TAB: Readonly<
> = {
settings: "settings",
assets: "assets",
+ releases: "releases",
hosting: "hosting",
e2e: "e2e",
analytics: "analytics",
@@ -632,6 +635,7 @@ export function navigateToTabRouteTarget(
return;
case PROJECT_ROUTE.settings:
case PROJECT_ROUTE.assets:
+ case PROJECT_ROUTE.releases:
case PROJECT_ROUTE.hosting:
case PROJECT_ROUTE.e2e:
case PROJECT_ROUTE.analytics:
diff --git a/apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts b/apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts
index 538cf0ec7d..9f5f7ecdfb 100644
--- a/apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts
+++ b/apps/web/src/layouts/main-panel-tabs/use-main-panel-tabs.ts
@@ -224,6 +224,7 @@ export function useMainPanelTabs(ctx: {
const nativeViewPending = {
assets: nativeViews.assetsPending,
siteAccess: nativeViews.siteAccessPending,
+ releases: nativeViews.releasesPending,
};
const { activeTab: rawActiveTab, mainOpen } = resolveActiveTabAndOpen({
diff --git a/apps/web/src/layouts/main-panel-tabs/use-project-native-view-presence.ts b/apps/web/src/layouts/main-panel-tabs/use-project-native-view-presence.ts
index 3797fc1288..28cb7502ab 100644
--- a/apps/web/src/layouts/main-panel-tabs/use-project-native-view-presence.ts
+++ b/apps/web/src/layouts/main-panel-tabs/use-project-native-view-presence.ts
@@ -2,7 +2,10 @@ import { useQuery } from "@tanstack/react-query";
import type { VirtualMCPEntity } from "@decocms/shared/sdk/types";
import { matchSiteSlugConfig } from "@/components/file-picker/match-site-slug-config";
import { useFileConfigsQuery } from "@/hooks/use-file-configs";
-import { useControlPlaneViews } from "@/hooks/use-organization-settings";
+import {
+ useControlPlaneViews,
+ useOrgFlagState,
+} from "@/hooks/use-organization-settings";
import { usePublicConfig } from "@/hooks/use-public-config";
import { KEYS } from "@/lib/query-keys";
import { useProjectContext } from "@/sdk";
@@ -16,10 +19,11 @@ export interface ProjectNativeViewPresenceResult {
presence: ProjectNativeViewPresence;
assetsPending: boolean;
siteAccessPending: boolean;
+ releasesPending: boolean;
}
/**
- * Resolve the five native per-project views once for every surface that needs
+ * Resolve the native per-project views once for every surface that needs
* them. Product rollout, deployment wiring, tenant ownership, and per-site
* resources are presence; the Layout preference is deliberately layered on by
* the sidebar only.
@@ -31,6 +35,8 @@ export function useProjectNativeViewPresence(
const config = usePublicConfig();
const controlPlaneViews = useControlPlaneViews();
const fileConfigs = useFileConfigsQuery();
+ // The hosted Deco CMS (Blocks v8) is behind the content-protocol flag.
+ const hostedCms = useOrgFlagState("site_editor_content_protocol");
const siteSlug = resolveAgentSiteSlug(project);
const hostingEnabled = config.hostingEnabled === true;
const monitorEnabled =
@@ -56,6 +62,7 @@ export function useProjectNativeViewPresence(
return {
presence: {
assets: !!matchSiteSlugConfig(fileConfigs.data?.configs ?? [], siteSlug),
+ releases: hostedCms === true && !!project?.metadata?.siteSlug,
hosting: hostingEnabled && ownsSite && controlPlaneViews.hosting,
e2e: hostingEnabled && ownsSite && controlPlaneViews.e2e,
analytics: hostingEnabled && ownsSite && controlPlaneViews.analytics,
@@ -63,6 +70,7 @@ export function useProjectNativeViewPresence(
experiments: controlPlaneViews.experiments,
},
assetsPending: !!siteSlug && fileConfigs.isPending,
+ releasesPending: hostedCms === undefined,
// A disabled TanStack query is still pending; it is only unresolved when a
// probe can actually run.
siteAccessPending:
diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx
index 61749c6289..a6d1c92d74 100644
--- a/apps/web/src/router.tsx
+++ b/apps/web/src/router.tsx
@@ -815,6 +815,21 @@ const agentAssetsRoute = createRoute({
),
});
+const agentReleasesRoute = createRoute({
+ pendingComponent: ChatLayoutPending,
+ errorComponent: ChatLayoutError,
+ getParentRoute: () => agentWorkspaceRoute,
+ path: "/releases",
+ staticData: {
+ pageTitle: "common.mainPanelTabs.releases",
+ defaultMain: "releases",
+ mainView: "releases",
+ },
+ component: lazyRouteComponent(
+ () => import("./routes/workspace/agent-releases.tsx"),
+ ),
+});
+
const agentHostingRoute = createRoute({
pendingComponent: ChatLayoutPending,
errorComponent: ChatLayoutError,
@@ -1598,6 +1613,7 @@ const agentWorkspaceWithChildren = agentWorkspaceRoute.addChildren([
agentSettingsRoute,
agentLibraryRoute,
agentAssetsRoute,
+ agentReleasesRoute,
agentHostingRoute,
agentE2eRoute,
agentAnalyticsRoute,
diff --git a/apps/web/src/routes/workspace/agent-releases.tsx b/apps/web/src/routes/workspace/agent-releases.tsx
new file mode 100644
index 0000000000..a612b789c4
--- /dev/null
+++ b/apps/web/src/routes/workspace/agent-releases.tsx
@@ -0,0 +1,21 @@
+import { ChatLayout } from "@/components/chat-layout";
+import { AgentViewGuard } from "./agent-view-guard";
+import { ReleasesTab } from "@/layouts/main-panel-tabs/releases-tab";
+import { useRouteVirtualMcpId } from "@/layouts/thread-route";
+
+function AgentReleasesContent() {
+ const virtualMcpId = useRouteVirtualMcpId();
+ return (
+
+
+
+ );
+}
+
+export default function AgentReleasesRoute() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/views/virtual-mcp/settings/use-project-views.ts b/apps/web/src/views/virtual-mcp/settings/use-project-views.ts
index 215bb65927..592a79812b 100644
--- a/apps/web/src/views/virtual-mcp/settings/use-project-views.ts
+++ b/apps/web/src/views/virtual-mcp/settings/use-project-views.ts
@@ -414,6 +414,7 @@ export function useProjectViews({
board: t("sidebar.navDestinations.tasks"),
"site-editor": t("virtualMcp.layoutTabContent.siteEditor"),
assets: t("common.mainPanelTabs.assets"),
+ releases: t("common.mainPanelTabs.releases"),
hosting: t("common.mainPanelTabs.hosting"),
e2e: t("common.mainPanelTabs.e2e"),
analytics: t("common.mainPanelTabs.analytics"),
diff --git a/apps/web/src/views/virtual-mcp/settings/view-row-actions.tsx b/apps/web/src/views/virtual-mcp/settings/view-row-actions.tsx
index f19ea18700..0fda8d90f6 100644
--- a/apps/web/src/views/virtual-mcp/settings/view-row-actions.tsx
+++ b/apps/web/src/views/virtual-mcp/settings/view-row-actions.tsx
@@ -4,6 +4,7 @@
*/
import {
+ ClockRewind,
ArrowUpRight,
BarChartSquare02,
CheckDone01,
@@ -39,6 +40,7 @@ export function SidebarViewIcon({ viewId }: { viewId: ProjectSidebarViewId }) {
if (viewId === "board") return ;
if (viewId === "site-editor") return ;
if (viewId === "assets") return ;
+ if (viewId === "releases") return ;
if (viewId === "hosting") return ;
if (viewId === "e2e") return ;
if (viewId === "analytics") return ;
diff --git a/packages/shared/src/sdk/types/virtual-mcp.ts b/packages/shared/src/sdk/types/virtual-mcp.ts
index 13ec929e91..fb4209fa8f 100644
--- a/packages/shared/src/sdk/types/virtual-mcp.ts
+++ b/packages/shared/src/sdk/types/virtual-mcp.ts
@@ -145,6 +145,7 @@ export const VirtualMcpSidebarViewSchema = z.enum([
"board",
"site-editor",
"assets",
+ "releases",
"hosting",
"e2e",
"analytics",
diff --git a/packages/shared/src/tools/tool-io.ts b/packages/shared/src/tools/tool-io.ts
index 089219a7fa..f5c4add63e 100644
--- a/packages/shared/src/tools/tool-io.ts
+++ b/packages/shared/src/tools/tool-io.ts
@@ -2516,6 +2516,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -2575,6 +2576,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -2783,6 +2785,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -2842,6 +2845,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -2974,6 +2978,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3033,6 +3038,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3202,6 +3208,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3261,6 +3268,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3421,6 +3429,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3480,6 +3489,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3640,6 +3650,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3699,6 +3710,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3839,6 +3851,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -3898,6 +3911,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -4056,6 +4070,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -4115,6 +4130,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -5415,6 +5431,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
@@ -5474,6 +5491,7 @@ export interface StudioToolIO {
| "board"
| "site-editor"
| "assets"
+ | "releases"
| "hosting"
| "e2e"
| "analytics"
From 50e00c67d75ab9578422ab2f5205d291a3be7905 Mon Sep 17 00:00:00 2001
From: gimenes
Date: Fri, 9 Oct 2026 11:57:22 -0300
Subject: [PATCH 2/4] fix(web): Versions screen in business language, one toast
per action
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Releases screen speaks to business users: "Published" is the version
the site shows (badge, header "Published 2h ago", "Nothing published yet"),
and "Publish this version" replaces "Make current". No sha, CDN or git
words on screen; the sha moves to a tooltip and a row shows its version
note's title line only. The English tab and app card are renamed
"Versions", matching pt-br.
Publishing a version shows one toast, shared with the editor's Publish
(sonner id site-editor-publish): "Published · This version is live.", or
"Couldn't publish this version." with Try again and the error's own text
behind Details. A failed load offers Try again, with Details. The jargon
test now covers releases.* and the Versions nav labels.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
---
.../sections-editor/serve-save-error.test.ts | 42 ++++++-
.../sections-editor/site-editor-toast.tsx | 66 ++++++++++-
apps/web/src/i18n/en/common.ts | 2 +-
apps/web/src/i18n/en/projects.ts | 4 +-
apps/web/src/i18n/en/releases.ts | 35 +++---
apps/web/src/i18n/en/site-editor.ts | 2 +
apps/web/src/i18n/pt-br/projects.ts | 2 +-
apps/web/src/i18n/pt-br/releases.ts | 27 +++--
apps/web/src/i18n/pt-br/site-editor.ts | 2 +
apps/web/src/i18n/site-editor-copy.test.ts | 8 +-
.../layouts/main-panel-tabs/releases-tab.tsx | 107 ++++++++++--------
11 files changed, 213 insertions(+), 84 deletions(-)
diff --git a/apps/web/src/components/sections-editor/serve-save-error.test.ts b/apps/web/src/components/sections-editor/serve-save-error.test.ts
index 3469465d26..aca7c5c22f 100644
--- a/apps/web/src/components/sections-editor/serve-save-error.test.ts
+++ b/apps/web/src/components/sections-editor/serve-save-error.test.ts
@@ -9,7 +9,13 @@ import {
saveErrorDetail,
saveErrorMessage,
} from "./serve-save-error";
-import { SITE_EDITOR_SAVE_TOAST, toastSaveError } from "./site-editor-toast";
+import {
+ SITE_EDITOR_PUBLISH_TOAST,
+ SITE_EDITOR_SAVE_TOAST,
+ toastPublishFailed,
+ toastPublished,
+ toastSaveError,
+} from "./site-editor-toast";
const t: TFunction = (key, vars) => interpolate(en[key], vars);
@@ -84,3 +90,37 @@ describe("one toast per save failure", () => {
expect(shown).toHaveLength(1);
});
});
+
+describe("one toast per publish", () => {
+ afterEach(() => {
+ toast.dismiss();
+ });
+
+ test("a failure, its retry and the success share one toast", () => {
+ let retried = 0;
+ toastPublishFailed(t, {
+ headline: "Couldn't publish this version.",
+ detail: "latest-update-failed",
+ retry: () => {
+ retried++;
+ },
+ });
+ const failed = toast
+ .getToasts()
+ .filter((item) => item.id === SITE_EDITOR_PUBLISH_TOAST);
+ expect(failed).toHaveLength(1);
+ const action = (failed[0] as { action?: { label: string } }).action;
+ expect(action?.label).toBe("Try again");
+ // The headline never carries the developer detail.
+ expect((failed[0] as { title?: unknown }).title).toBe(
+ "Couldn't publish this version.",
+ );
+ toastPublished(t, "This version is live.");
+ const shown = toast
+ .getToasts()
+ .filter((item) => item.id === SITE_EDITOR_PUBLISH_TOAST);
+ expect(shown).toHaveLength(1);
+ expect((shown[0] as { title?: unknown }).title).toBe("Published");
+ expect(retried).toBe(0);
+ });
+});
diff --git a/apps/web/src/components/sections-editor/site-editor-toast.tsx b/apps/web/src/components/sections-editor/site-editor-toast.tsx
index 5233c03b09..4b65067480 100644
--- a/apps/web/src/components/sections-editor/site-editor-toast.tsx
+++ b/apps/web/src/components/sections-editor/site-editor-toast.tsx
@@ -13,9 +13,18 @@ import { saveErrorDetail, saveErrorMessage } from "./serve-save-error";
/** Every save failure in the site editor shares this toast. */
export const SITE_EDITOR_SAVE_TOAST = "site-editor-save";
+/** Publishing (from the editor or the Versions screen), its outcome and a
+ * retry of it share this toast. */
+export const SITE_EDITOR_PUBLISH_TOAST = "site-editor-publish";
/** A collapsed "Details" line holding the developer detail of a failure. */
-function ErrorDetails({ label, detail }: { label: string; detail: string }) {
+export function ErrorDetails({
+ label,
+ detail,
+}: {
+ label: string;
+ detail: string;
+}) {
return (
@@ -46,3 +55,58 @@ export function toastSaveError(
description: errorDetailsDescription(t, saveErrorDetail(error, source)),
});
}
+
+/** Published: what the site shows now. `description` says what went live. */
+export function toastPublished(t: TFunction, description: string) {
+ toast.success(t("siteEditor.publish.published"), {
+ id: SITE_EDITOR_PUBLISH_TOAST,
+ description,
+ });
+}
+
+/**
+ * Publishing didn't finish: `headline` says so in plain words, `detail` (the
+ * error's own text) sits behind Details, and `retry` offers "Try again".
+ * `tone: "warning"` when nothing is lost (the changes are saved).
+ */
+export function toastPublishFailed(
+ t: TFunction,
+ {
+ headline,
+ body,
+ detail,
+ retry,
+ tone = "error",
+ }: {
+ headline: string;
+ body?: string;
+ detail?: string | null;
+ retry?: () => void;
+ tone?: "error" | "warning";
+ },
+) {
+ const details = errorDetailsDescription(t, detail ?? null);
+ toast[tone](headline, {
+ id: SITE_EDITOR_PUBLISH_TOAST,
+ description:
+ body && details ? (
+ <>
+
{body}
+ {details}
+ >
+ ) : (
+ (body ?? details)
+ ),
+ action: retry
+ ? { label: t("siteEditor.tryAgain"), onClick: retry }
+ : undefined,
+ // A failure waits for the user: it carries the next step.
+ duration: retry ? Number.POSITIVE_INFINITY : undefined,
+ });
+}
+
+/** The developer detail of a failed request, for Details. */
+export function errorDetail(error: unknown): string | null {
+ if (error instanceof Error) return error.message || null;
+ return error == null ? null : String(error);
+}
diff --git a/apps/web/src/i18n/en/common.ts b/apps/web/src/i18n/en/common.ts
index 2e128554af..c87b349e8e 100644
--- a/apps/web/src/i18n/en/common.ts
+++ b/apps/web/src/i18n/en/common.ts
@@ -269,7 +269,7 @@ export const common = {
"common.mainPanelTabs.code": "Code",
"common.mainPanelTabs.content": "Content",
"common.mainPanelTabs.assets": "Assets",
- "common.mainPanelTabs.releases": "Releases",
+ "common.mainPanelTabs.releases": "Versions",
"common.mainPanelTabs.hosting": "Hosting",
"common.mainPanelTabs.e2e": "E2E",
"common.mainPanelTabs.analytics": "Deco Analytics",
diff --git a/apps/web/src/i18n/en/projects.ts b/apps/web/src/i18n/en/projects.ts
index 44ac9b7e53..28b05fdc4c 100644
--- a/apps/web/src/i18n/en/projects.ts
+++ b/apps/web/src/i18n/en/projects.ts
@@ -76,8 +76,8 @@ export const projects = {
"projects.apps.siteEditorCaption": "Pages, sections and content",
"projects.apps.assets": "Assets",
"projects.apps.assetsCaption": "Images and files the site uses",
- "projects.apps.releases": "Releases",
- "projects.apps.releasesCaption": "What the site serves, and rollbacks",
+ "projects.apps.releases": "Versions",
+ "projects.apps.releasesCaption": "What's live, and earlier versions",
"projects.apps.hosting": "Hosting",
"projects.apps.hostingCaption": "Deploys, domains and environments",
"projects.apps.e2e": "End-to-end tests",
diff --git a/apps/web/src/i18n/en/releases.ts b/apps/web/src/i18n/en/releases.ts
index 91daefcf0a..2dad88f854 100644
--- a/apps/web/src/i18n/en/releases.ts
+++ b/apps/web/src/i18n/en/releases.ts
@@ -1,21 +1,26 @@
+/**
+ * The Versions screen of a hosted site (the `releases` route). Business-user
+ * copy: "Published" is the version the site shows; no CDN, git, commit, sha
+ * or "current" (`site-editor-copy.test.ts` enforces it).
+ */
export const releases = {
- "releases.title": "Releases",
+ "releases.title": "Versions",
"releases.subtitle":
- "Every change merged to main, newest first. A Publish also creates a release on the CDN; Make current switches the CDN to any release.",
- "releases.loadFailed": "Couldn't load releases",
- "releases.currentOnCdn": "Current on the CDN:",
- "releases.madeCurrentAgo": "made current {when}",
- "releases.nothingOnCdn": "Nothing on the CDN yet",
- "releases.current": "Current",
- "releases.actions": "Release actions",
- "releases.makeCurrent": "Make current",
- "releases.makeCurrentFailed": "Making it current failed. Try again.",
- "releases.makeCurrentTitle": "Make {sha} current?",
- "releases.makeCurrentBody":
- "The CDN serves this release from now on. Git doesn't change, and the next Publish makes its own release current.",
+ "Every version of your site, newest first. You can publish an earlier one to bring it back.",
+ "releases.loadFailed": "Couldn't load your versions.",
+ "releases.publishedAgo": "Published {when}",
+ "releases.nothingPublished": "Nothing published yet",
+ "releases.published": "Published",
+ "releases.actions": "Version actions",
+ "releases.publishVersion": "Publish this version",
+ "releases.publishVersionTitle": "Publish this version?",
+ "releases.publishVersionBody":
+ "Your site will show this version from now on. Your next Publish replaces it with your latest changes.",
"releases.schemaMismatchBody":
- "This release was made with a different schema than main's. Sites built from main keep their current content until the schemas match again.",
- "releases.madeCurrent": "{sha} is now current",
+ "This version was made for an earlier design of your site. Your site keeps showing what it shows now until its design matches this version again. Publish anyway?",
+ "releases.publishAnyway": "Publish anyway",
+ "releases.versionLive": "This version is live.",
+ "releases.publishVersionFailed": "Couldn't publish this version.",
"releases.loadMore": "Load more",
"releases.cancel": "Cancel",
};
diff --git a/apps/web/src/i18n/en/site-editor.ts b/apps/web/src/i18n/en/site-editor.ts
index 1ce4919223..921ec2b6b5 100644
--- a/apps/web/src/i18n/en/site-editor.ts
+++ b/apps/web/src/i18n/en/site-editor.ts
@@ -6,6 +6,8 @@
*/
export const siteEditor = {
"siteEditor.details": "Details",
+ "siteEditor.tryAgain": "Try again",
+ "siteEditor.publish.published": "Published",
"siteEditor.save.failed": "Couldn't save your change. Try again.",
"siteEditor.save.conflict":
"Not saved: someone else changed this at the same time. We're loading their version, so make your change again.",
diff --git a/apps/web/src/i18n/pt-br/projects.ts b/apps/web/src/i18n/pt-br/projects.ts
index 228222e0d3..cf0747f3e5 100644
--- a/apps/web/src/i18n/pt-br/projects.ts
+++ b/apps/web/src/i18n/pt-br/projects.ts
@@ -71,7 +71,7 @@ export const projects = {
"projects.apps.assets": "Assets",
"projects.apps.assetsCaption": "Imagens e arquivos que o site usa",
"projects.apps.releases": "Versões",
- "projects.apps.releasesCaption": "O que o site serve, e reversões",
+ "projects.apps.releasesCaption": "O que está no ar e versões anteriores",
"projects.apps.hosting": "Hosting",
"projects.apps.hostingCaption": "Deploys, domínios e ambientes",
"projects.apps.e2e": "Testes ponta a ponta",
diff --git a/apps/web/src/i18n/pt-br/releases.ts b/apps/web/src/i18n/pt-br/releases.ts
index 729faac8fc..e836c8f0f4 100644
--- a/apps/web/src/i18n/pt-br/releases.ts
+++ b/apps/web/src/i18n/pt-br/releases.ts
@@ -3,22 +3,21 @@ import type { releases as enReleases } from "../en/releases.ts";
export const releases = {
"releases.title": "Versões",
"releases.subtitle":
- "Todas as alterações mescladas na main, da mais recente para a mais antiga. Publicar também cria uma versão na CDN; Tornar atual faz a CDN servir qualquer versão.",
- "releases.loadFailed": "Não foi possível carregar as versões",
- "releases.currentOnCdn": "Atual na CDN:",
- "releases.madeCurrentAgo": "tornada atual {when}",
- "releases.nothingOnCdn": "Nada na CDN ainda",
- "releases.current": "Atual",
+ "Todas as versões do seu site, da mais recente para a mais antiga. Você pode publicar uma anterior para trazê-la de volta.",
+ "releases.loadFailed": "Não foi possível carregar suas versões.",
+ "releases.publishedAgo": "Publicada {when}",
+ "releases.nothingPublished": "Nada publicado ainda",
+ "releases.published": "Publicada",
"releases.actions": "Ações da versão",
- "releases.makeCurrent": "Tornar atual",
- "releases.makeCurrentFailed":
- "Não foi possível torná-la atual. Tente de novo.",
- "releases.makeCurrentTitle": "Tornar {sha} a versão atual?",
- "releases.makeCurrentBody":
- "A CDN passa a servir esta versão. O git não muda, e a próxima publicação torna a sua própria versão atual.",
+ "releases.publishVersion": "Publicar esta versão",
+ "releases.publishVersionTitle": "Publicar esta versão?",
+ "releases.publishVersionBody":
+ "Seu site passa a mostrar esta versão. A próxima publicação a substitui pelas suas alterações mais recentes.",
"releases.schemaMismatchBody":
- "Esta versão foi criada com um schema diferente do da main. Sites gerados a partir da main mantêm o conteúdo atual até os schemas voltarem a coincidir.",
- "releases.madeCurrent": "{sha} agora é a versão atual",
+ "Esta versão foi feita para um design anterior do seu site. O site continua mostrando o que mostra agora até o design voltar a corresponder a esta versão. Publicar mesmo assim?",
+ "releases.publishAnyway": "Publicar mesmo assim",
+ "releases.versionLive": "Esta versão está no ar.",
+ "releases.publishVersionFailed": "Não foi possível publicar esta versão.",
"releases.loadMore": "Carregar mais",
"releases.cancel": "Cancelar",
} satisfies Record;
diff --git a/apps/web/src/i18n/pt-br/site-editor.ts b/apps/web/src/i18n/pt-br/site-editor.ts
index 8de443666f..e2813e0006 100644
--- a/apps/web/src/i18n/pt-br/site-editor.ts
+++ b/apps/web/src/i18n/pt-br/site-editor.ts
@@ -2,6 +2,8 @@ import type { siteEditor as siteEditorEn } from "../en/site-editor.ts";
export const siteEditor = {
"siteEditor.details": "Detalhes",
+ "siteEditor.tryAgain": "Tentar de novo",
+ "siteEditor.publish.published": "Publicado",
"siteEditor.save.failed":
"Não foi possível salvar sua alteração. Tente de novo.",
"siteEditor.save.conflict":
diff --git a/apps/web/src/i18n/site-editor-copy.test.ts b/apps/web/src/i18n/site-editor-copy.test.ts
index 18c66225ad..6af8739aea 100644
--- a/apps/web/src/i18n/site-editor-copy.test.ts
+++ b/apps/web/src/i18n/site-editor-copy.test.ts
@@ -8,7 +8,12 @@ import { en } from "./en/index.ts";
import { ptBR } from "./pt-br/index.ts";
/** Namespaces whose every string a business user reads in the site editor. */
-const SITE_EDITOR_PREFIXES = ["siteEditor."];
+const SITE_EDITOR_PREFIXES = [
+ "siteEditor.",
+ "releases.",
+ "common.mainPanelTabs.releases",
+ "projects.apps.releases",
+];
const BANNED: RegExp[] = [
/\bCDN\b/i,
@@ -21,6 +26,7 @@ const BANNED: RegExp[] = [
/latest\.json/i,
/\brevision/i,
/\bsha\b/i,
+ /\bcurrent\b/i,
/make current/i,
/tornar atual/i,
];
diff --git a/apps/web/src/layouts/main-panel-tabs/releases-tab.tsx b/apps/web/src/layouts/main-panel-tabs/releases-tab.tsx
index 8b4e39828f..109ed00b36 100644
--- a/apps/web/src/layouts/main-panel-tabs/releases-tab.tsx
+++ b/apps/web/src/layouts/main-panel-tabs/releases-tab.tsx
@@ -1,14 +1,17 @@
/**
- * Releases tab — a hosted v8 site's timeline: main's commits, newest first.
- * A commit with a companion release on the CDN can be made current; the one
- * latest.json names is Current (read, never inferred). A commit without a
- * companion (a developer push, or a Publish whose release write failed) is
- * just a merge: listed, with no badge and no action.
+ * Versions tab (the `releases` route) — a hosted v8 site's timeline: main's
+ * commits, newest first. A commit with a companion release on the CDN can be
+ * published ("Make current" on the API); the one latest.json names is
+ * Published (read, never inferred). A commit without a companion (a
+ * developer push, or a Publish whose release write failed) is listed with no
+ * badge and no action.
+ *
+ * Business-user copy: no sha, CDN or git words on screen; one toast per
+ * action (shared with Publish), with the error's own text behind Details.
*/
import { useState } from "react";
import { ClockRewind, DotsVertical } from "@untitledui/icons";
-import { toast } from "sonner";
import { Button } from "@decocms/ui/components/button.tsx";
import {
AlertDialog,
@@ -31,6 +34,12 @@ import { cn } from "@decocms/ui/lib/utils.ts";
import { useT } from "@/i18n/use-t.ts";
import { formatTimeAgo } from "@/lib/format-time.ts";
import { useProjectContext } from "@/sdk";
+import {
+ ErrorDetails,
+ errorDetail,
+ toastPublishFailed,
+ toastPublished,
+} from "@/components/sections-editor/site-editor-toast.tsx";
import {
HostedRequestError,
type ReleaseCommit,
@@ -39,6 +48,8 @@ import {
} from "./releases-api";
const short = (sha: string) => sha.slice(0, 7);
+/** A commit message's title line: the version note a Publish wrote. */
+const titleLine = (message: string) => message.split("\n")[0]?.trim() ?? "";
/** What the confirmation dialog is asking about. */
type PendingConfirm = {
@@ -65,9 +76,23 @@ export function ReleasesTab({ virtualMcpId }: { virtualMcpId: string }) {
return (