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();