Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,9 +584,10 @@ export const make = Effect.gen(function* () {
}
});

window.on("page-title-updated", (event) => {
window.on("page-title-updated", (event, title) => {
event.preventDefault();
window.setTitle(environment.displayName);
const trimmed = title.trim();
window.setTitle(trimmed === "" ? environment.displayName : trimmed);
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
});
window.on("resize", scheduleBoundsPersist);
window.on("move", scheduleBoundsPersist);
Expand Down Expand Up @@ -660,7 +661,6 @@ export const make = Effect.gen(function* () {
}
clearDevelopmentLoadRetry();
developmentLoadRetryIndex = 0;
window.setTitle(environment.displayName);
});
window.webContents.on(
"did-fail-load",
Expand Down
12 changes: 11 additions & 1 deletion apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,17 @@
object-fit: contain;
}
</style>
<title>T3 Code (Alpha)</title>
<title>T3 Code</title>
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the packaged web title before React mounts

When the production web client is built without VITE_HOSTED_APP_CHANNEL, as it is for the client bundled into npx t3, branding.ts resolves the fallback display name to T3 Code (Alpha), but this static title is now T3 Code and the new boot script only corrects it when desktopBridge exists. Local and remote browser tabs therefore flash the wrong title on every load until DocumentTitleSync mounts; keep the static fallback aligned with the packaged web branding or initialize it using the same channel logic.

AGENTS.md reference: AGENTS.md:L149-L149

Useful? React with 👍 / 👎.

<script>
(() => {
// The desktop preload injects branding before page scripts run, so the
// native window title carries the stage label from first paint.
const branding = window.desktopBridge?.getAppBranding?.();
if (branding?.displayName) {
document.title = branding.displayName;
}
})();
</script>
</head>
<body>
<div id="root">
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/branding.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,19 @@ export function resolveServerBackedAppDisplayName(input: {
? input.fallbackDisplayName
: formatAppDisplayName({ baseName: input.baseName, stageLabel });
}

export function resolveWindowTitle(input: {
readonly appDisplayName: string;
readonly projectTitle: string | null;
readonly threadTitle: string | null;
readonly desktop: boolean;
Comment on lines +39 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the new window-title behavior

This introduces user-visible behavior across browser tabs and desktop windows—including different title formats per client and live updates when project or thread titles change—but the commit does not update any page under docs/user/. Add shipped-product documentation explaining the behavior and its web/desktop differences so the feature complies with the repository’s documentation requirement.

AGENTS.md reference: AGENTS.md:L75-L75

Useful? React with 👍 / 👎.

}): string {
const threadTitle = input.threadTitle?.trim() ?? "";
if (threadTitle === "") {
return input.appDisplayName;
}

const projectTitle = input.projectTitle?.trim() ?? "";
const context = projectTitle === "" ? threadTitle : `${projectTitle} / ${threadTitle}`;
return input.desktop ? context : `${context} — ${input.appDisplayName}`;
}
58 changes: 58 additions & 0 deletions apps/web/src/branding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import {
resolveServerBackedAppDisplayName,
resolveServerBackedAppStageLabel,
resolveWindowTitle,
} from "./branding.logic";

const originalWindow = globalThis.window;
Expand Down Expand Up @@ -114,3 +115,60 @@ describe("branding logic", () => {
).toBe("T3 Code (Alpha)");
});
});

describe("resolveWindowTitle", () => {
it("falls back to the app display name without an active thread", () => {
expect(
resolveWindowTitle({
appDisplayName: "T3 Code (Nightly)",
projectTitle: null,
threadTitle: null,
desktop: true,
}),
).toBe("T3 Code (Nightly)");
});

it("joins project and thread titles on desktop without the app name", () => {
expect(
resolveWindowTitle({
appDisplayName: "T3 Code (Nightly)",
projectTitle: "acme-web",
threadTitle: "New thread",
desktop: true,
}),
).toBe("acme-web / New thread");
});

it("keeps the app display name as a suffix on the web", () => {
expect(
resolveWindowTitle({
appDisplayName: "T3 Code (Alpha)",
projectTitle: "acme-web",
threadTitle: "New thread",
desktop: false,
}),
).toBe("acme-web / New thread — T3 Code (Alpha)");
});

it("omits the separator without a project title", () => {
expect(
resolveWindowTitle({
appDisplayName: "T3 Code (Nightly)",
projectTitle: " ",
threadTitle: "Fix login bug",
desktop: true,
}),
).toBe("Fix login bug");
});

it("ignores whitespace-only thread titles", () => {
expect(
resolveWindowTitle({
appDisplayName: "T3 Code (Nightly)",
projectTitle: "acme-web",
threadTitle: " ",
desktop: true,
}),
).toBe("T3 Code (Nightly)");
});
});
7 changes: 7 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import * as Cause from "effect/Cause";
import * as Schema from "effect/Schema";
import { AsyncResult } from "effect/unstable/reactivity";
import { isElectron } from "../env";
import { clearWindowTitleContext, setWindowTitleContext } from "../windowTitleStore";
import { readLocalApi } from "../localApi";
import { useDiffPanelStore } from "../diffPanelStore";
import {
Expand Down Expand Up @@ -1826,6 +1827,12 @@ function ChatViewContent(props: ChatViewProps) {
[activeThread?.environmentId, activeThread?.projectId],
);
const activeProject = useProject(activeProjectRef);
const activeProjectTitle = activeProject?.title ?? null;
const activeThreadTitle = activeThread?.title ?? null;
useEffect(() => {
setWindowTitleContext({ projectTitle: activeProjectTitle, threadTitle: activeThreadTitle });
}, [activeProjectTitle, activeThreadTitle]);
useEffect(() => clearWindowTitleContext, []);
const handleNewThreadInActiveProject = useCallback(() => {
startNewThreadForProject(activeProjectRef, handleNewThread);
}, [activeProjectRef, handleNewThread]);
Expand Down
21 changes: 15 additions & 6 deletions apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
import { useEffect, useEffectEvent, useRef, useState } from "react";

import { APP_BASE_NAME, APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../branding";
import { resolveServerBackedAppDisplayName } from "../branding.logic";
import { resolveServerBackedAppDisplayName, resolveWindowTitle } from "../branding.logic";
import { isElectron } from "../env";
import { useWindowTitleContextStore } from "../windowTitleStore";
import { AppSidebarLayout } from "../components/AppSidebarLayout";
import { CommandPalette } from "../components/CommandPalette";
import { ConfirmDialogHost } from "../components/ConfirmDialogHost";
Expand Down Expand Up @@ -209,11 +211,17 @@ function FontAppearanceSync() {
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,
const { projectTitle, threadTitle } = useWindowTitleContextStore();
const title = resolveWindowTitle({
Comment thread
chasemakes marked this conversation as resolved.
Resolved
appDisplayName: resolveServerBackedAppDisplayName({
baseName: APP_BASE_NAME,
fallbackDisplayName: APP_DISPLAY_NAME,
fallbackStageLabel: APP_STAGE_LABEL,
primaryServerVersion,
}),
projectTitle,
threadTitle,
desktop: isElectron,
});

useEffect(() => {
Expand Down Expand Up @@ -257,6 +265,7 @@ function RootRouteErrorView({ error, reset }: ErrorComponentProps) {

return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
<DocumentTitleSync />
<div className="pointer-events-none absolute inset-0 opacity-80">
<div className="absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-red-500)_16%,transparent),transparent)]" />
<div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />
Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/windowTitleStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { create } from "zustand";

export interface WindowTitleContext {
projectTitle: string | null;
threadTitle: string | null;
}

/** Active project/thread context mirrored into document.title by the root layout. */
export const useWindowTitleContextStore = create<WindowTitleContext>()(() => ({
projectTitle: null,
threadTitle: null,
}));

export function setWindowTitleContext(context: WindowTitleContext): void {
const current = useWindowTitleContextStore.getState();
if (
current.projectTitle === context.projectTitle &&
current.threadTitle === context.threadTitle
) {
return;
}
useWindowTitleContextStore.setState(context);
}

export function clearWindowTitleContext(): void {
setWindowTitleContext({ projectTitle: null, threadTitle: null });
}
Loading