diff --git a/apps/web/src/branding.logic.ts b/apps/web/src/branding.logic.ts new file mode 100644 index 00000000000..b87276f1b9c --- /dev/null +++ b/apps/web/src/branding.logic.ts @@ -0,0 +1,34 @@ +const NIGHTLY_SERVER_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/; + +export function formatAppDisplayName(input: { + readonly baseName: string; + readonly stageLabel: string; +}): string { + return `${input.baseName} (${input.stageLabel})`; +} + +export function resolveServerBackedAppStageLabel(input: { + readonly primaryServerVersion: string | null | undefined; + readonly fallbackStageLabel: string; +}): string { + return input.primaryServerVersion && + NIGHTLY_SERVER_VERSION_PATTERN.test(input.primaryServerVersion) + ? "Nightly" + : input.fallbackStageLabel; +} + +export function resolveServerBackedAppDisplayName(input: { + readonly baseName: string; + readonly fallbackDisplayName: string; + readonly fallbackStageLabel: string; + readonly primaryServerVersion: string | null | undefined; +}): string { + const stageLabel = resolveServerBackedAppStageLabel({ + primaryServerVersion: input.primaryServerVersion, + fallbackStageLabel: input.fallbackStageLabel, + }); + + return stageLabel === input.fallbackStageLabel + ? input.fallbackDisplayName + : formatAppDisplayName({ baseName: input.baseName, stageLabel }); +} diff --git a/apps/web/src/branding.test.ts b/apps/web/src/branding.test.ts index d9b69bce94a..4aa969c0279 100644 --- a/apps/web/src/branding.test.ts +++ b/apps/web/src/branding.test.ts @@ -1,4 +1,8 @@ import { afterEach, describe, expect, it, vi } from "vite-plus/test"; +import { + resolveServerBackedAppDisplayName, + resolveServerBackedAppStageLabel, +} from "./branding.logic"; const originalWindow = globalThis.window; @@ -55,3 +59,47 @@ describe("branding", () => { expect(branding.HOSTED_APP_CHANNEL_LABEL).toBeNull(); }); }); + +describe("branding logic", () => { + it("returns Nightly for nightly primary server versions", () => { + expect( + resolveServerBackedAppStageLabel({ + primaryServerVersion: "0.0.28-nightly.20260616.12", + fallbackStageLabel: "Alpha", + }), + ).toBe("Nightly"); + }); + + it("updates the display name for nightly primary server versions", () => { + expect( + resolveServerBackedAppDisplayName({ + baseName: "T3 Code", + fallbackDisplayName: "T3 Code (Alpha)", + fallbackStageLabel: "Alpha", + primaryServerVersion: "0.0.28-nightly.20260616.12", + }), + ).toBe("T3 Code (Nightly)"); + }); + + it("keeps the fallback display name for stable primary server versions", () => { + expect( + resolveServerBackedAppDisplayName({ + baseName: "T3 Code", + fallbackDisplayName: "T3 Code (Alpha)", + fallbackStageLabel: "Alpha", + primaryServerVersion: "0.0.27", + }), + ).toBe("T3 Code (Alpha)"); + }); + + it("keeps the fallback display name for malformed nightly primary server versions", () => { + expect( + resolveServerBackedAppDisplayName({ + baseName: "T3 Code", + fallbackDisplayName: "T3 Code (Alpha)", + fallbackStageLabel: "Alpha", + primaryServerVersion: "0.0.28-nightly.20260616", + }), + ).toBe("T3 Code (Alpha)"); + }); +}); diff --git a/apps/web/src/branding.ts b/apps/web/src/branding.ts index 5c1309ca06b..7fc57cf0d03 100644 --- a/apps/web/src/branding.ts +++ b/apps/web/src/branding.ts @@ -1,4 +1,5 @@ import type { DesktopAppBranding } from "@t3tools/contracts"; +import { formatAppDisplayName } from "./branding.logic"; function readInjectedDesktopAppBranding(): DesktopAppBranding | null { if (typeof window === "undefined") { @@ -21,5 +22,6 @@ export const APP_STAGE_LABEL = HOSTED_APP_CHANNEL_LABEL ?? (import.meta.env.DEV ? "Dev" : "Alpha"); export const APP_DISPLAY_NAME = - injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`; + injectedDesktopAppBranding?.displayName ?? + formatAppDisplayName({ baseName: APP_BASE_NAME, stageLabel: APP_STAGE_LABEL }); export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0"; diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 5c70d447d2b..4e7614ed551 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -9,10 +9,10 @@ import { import type { SidebarThreadSummary, Thread } from "../types"; import { cn } from "../lib/utils"; import { isLatestTurnSettled } from "../session-logic"; +import { resolveServerBackedAppStageLabel } from "../branding.logic"; export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 100; -const NIGHTLY_SERVER_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/; // Visible sidebar rows are prewarmed into the thread-detail cache so opening a // nearby thread usually reuses an already-hot subscription. export const SIDEBAR_THREAD_PREWARM_LIMIT = 10; @@ -69,10 +69,7 @@ export function resolveSidebarStageBadgeLabel(input: { primaryServerVersion: string | null | undefined; fallbackStageLabel: string; }): string { - return input.primaryServerVersion && - NIGHTLY_SERVER_VERSION_PATTERN.test(input.primaryServerVersion) - ? "Nightly" - : input.fallbackStageLabel; + return resolveServerBackedAppStageLabel(input); } export function createThreadJumpHintVisibilityController(input: { diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 838a990d6c6..453649bfdc5 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -15,7 +15,6 @@ import { isElectron } from "./env"; import { ManagedRelayAuthProvider } from "./cloud/managedAuth"; import { hasCloudPublicConfig } from "./cloud/publicConfig"; import { getRouter } from "./router"; -import { APP_DISPLAY_NAME } from "./branding"; import { syncDocumentWindowControlsOverlayClass } from "./lib/windowControlsOverlay"; import { AppRoot } from "./AppRoot"; @@ -28,8 +27,6 @@ if (isElectron) { syncDocumentWindowControlsOverlayClass(); } -document.title = APP_DISPLAY_NAME; - const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; const app = ; diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index d01518a3858..035f59ad93a 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -10,7 +10,8 @@ import { } from "@tanstack/react-router"; import { useEffect, useEffectEvent, useRef, useState } from "react"; -import { APP_DISPLAY_NAME } from "../branding"; +import { APP_BASE_NAME, APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../branding"; +import { resolveServerBackedAppDisplayName } from "../branding.logic"; import { AppSidebarLayout } from "../components/AppSidebarLayout"; import { CommandPalette } from "../components/CommandPalette"; import { RelayClientInstallDialog } from "../components/cloud/RelayClientInstallDialog"; @@ -96,11 +97,21 @@ function RootRouteView() { }, [pathname]); if (pathname === "/pair") { - return ; + return ( + <> + + + + ); } if (authGateState.status !== "authenticated" && authGateState.status !== "hosted-static") { - return ; + return ( + <> + + + + ); } const appShell = ( @@ -114,6 +125,7 @@ function RootRouteView() { return ( + {primaryEnvironmentAuthenticated ? : null} @@ -127,6 +139,23 @@ function RootRouteView() { ); } +function DocumentTitleSync() { + const primaryServerVersion = + useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; + const title = resolveServerBackedAppDisplayName({ + baseName: APP_BASE_NAME, + fallbackDisplayName: APP_DISPLAY_NAME, + fallbackStageLabel: APP_STAGE_LABEL, + primaryServerVersion, + }); + + useEffect(() => { + document.title = title; + }, [title]); + + return null; +} + function HostedStaticEnvironmentBootstrap() { const { environments } = useEnvironments(); const activeEnvironmentId = useActiveEnvironmentId();