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
2 changes: 1 addition & 1 deletion CLAUDE.md
50 changes: 48 additions & 2 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000;
const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000;
const DESKTOP_UPDATE_CHANNEL = "latest";
const DESKTOP_UPDATE_ALLOW_PRERELEASE = false;
const WINDOWS_TITLEBAR_HEIGHT = 40;
const WINDOWS_TITLEBAR_LIGHT_COLOR = "#ffffff";
const WINDOWS_TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";
const WINDOWS_TITLEBAR_DARK_COLOR = "#161616";
const WINDOWS_TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";

type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"];
type LinuxDesktopNamedApp = Electron.App & {
Expand Down Expand Up @@ -1204,6 +1209,7 @@ function registerIpcHandlers(): void {
}

nativeTheme.themeSource = theme;
syncAllWindowsTitleBarOverlays();
});

ipcMain.removeHandler(CONTEXT_MENU_CHANNEL);
Expand Down Expand Up @@ -1337,6 +1343,36 @@ function getIconOption(): { icon: string } | Record<string, never> {
return iconPath ? { icon: iconPath } : {};
}

function getWindowsTitleBarOverlayOptions() {
const isDark =
nativeTheme.themeSource === "dark" ||
(nativeTheme.themeSource === "system" && nativeTheme.shouldUseDarkColors);
return {
height: WINDOWS_TITLEBAR_HEIGHT,
color: isDark ? WINDOWS_TITLEBAR_DARK_COLOR : WINDOWS_TITLEBAR_LIGHT_COLOR,
symbolColor: isDark ? WINDOWS_TITLEBAR_DARK_SYMBOL_COLOR : WINDOWS_TITLEBAR_LIGHT_SYMBOL_COLOR,
} as const;
}

function syncWindowsTitleBarOverlay(window: BrowserWindow) {
if (process.platform !== "win32") {
return;
}

window.setTitleBarOverlay(getWindowsTitleBarOverlayOptions());
}

function syncAllWindowsTitleBarOverlays() {
if (process.platform !== "win32") {
return;
}

for (const window of BrowserWindow.getAllWindows()) {
if (window.isDestroyed()) continue;
syncWindowsTitleBarOverlay(window);
}
}

function createWindow(): BrowserWindow {
const window = new BrowserWindow({
width: 1100,
Expand All @@ -1347,8 +1383,13 @@ function createWindow(): BrowserWindow {
autoHideMenuBar: true,
...getIconOption(),
title: APP_DISPLAY_NAME,
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 16, y: 18 },
...(process.platform === "win32"
? {
frame: false,
titleBarStyle: "hidden" as const,
titleBarOverlay: getWindowsTitleBarOverlayOptions(),
}
: { titleBarStyle: "hiddenInset" as const, trafficLightPosition: { x: 16, y: 18 } }),
webPreferences: {
preload: Path.join(__dirname, "preload.js"),
contextIsolation: true,
Expand All @@ -1357,6 +1398,8 @@ function createWindow(): BrowserWindow {
},
});

syncWindowsTitleBarOverlay(window);

window.webContents.on("context-menu", (event, params) => {
event.preventDefault();

Expand Down Expand Up @@ -1463,6 +1506,9 @@ app
.then(() => {
writeDesktopLogHeader("app ready");
configureAppIdentity();
nativeTheme.on("updated", () => {
syncAllWindowsTitleBarOverlays();
});
configureApplicationMenu();
registerDesktopProtocol();
configureAutoUpdater();
Expand Down
117 changes: 79 additions & 38 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { useDebouncedValue } from "@tanstack/react-pacer";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useGitStatus } from "~/lib/gitStatusState";
import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";
import { isElectron } from "../env";
import { isElectron, isWindowsElectron } from "../env";
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
import {
clampCollapsedComposerCursor,
Expand Down Expand Up @@ -153,7 +153,8 @@ import { selectThreadTerminalState, useTerminalStateStore } from "../terminalSta
import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";
import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
import { MessagesTimeline } from "./chat/MessagesTimeline";
import { ChatHeader } from "./chat/ChatHeader";
import { ChatHeader, ChatHeaderActions } from "./chat/ChatHeader";
import { DesktopTitleBar } from "./DesktopTitleBar";
import { ContextWindowMeter } from "./chat/ContextWindowMeter";
import { buildExpandedImagePreview, ExpandedImagePreview } from "./chat/ExpandedImagePreview";
import { AVAILABLE_PROVIDER_OPTIONS, ProviderModelPicker } from "./chat/ProviderModelPicker";
Expand Down Expand Up @@ -3911,9 +3912,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
</header>
)}
{isElectron && (
<div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5">
<span className="text-xs text-muted-foreground/50">No active thread</span>
</div>
<DesktopTitleBar
title="No active thread"
tone={isWindowsElectron ? "default" : "subtle"}
/>
)}
<div className="flex flex-1 items-center justify-center">
<div className="text-center">
Expand All @@ -3926,41 +3928,80 @@ export default function ChatView({ threadId }: ChatViewProps) {

return (
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
{/* Top bar */}
<header
className={cn(
"border-b border-border px-3 sm:px-5",
isElectron ? "drag-region flex h-[52px] items-center" : "py-2 sm:py-3",
)}
>
<ChatHeader
activeThreadId={activeThread.id}
activeThreadTitle={activeThread.title}
activeProjectName={activeProject?.name}
isGitRepo={isGitRepo}
openInCwd={gitCwd}
activeProjectScripts={activeProject?.scripts}
preferredScriptId={
activeProject ? (lastInvokedScriptByProjectId[activeProject.id] ?? null) : null
{isWindowsElectron ? (
<DesktopTitleBar
title={activeThread.title}
reserveNativeWindowControlsOverlay={!diffOpen}
{...(activeProject?.name ? { subtitle: activeProject.name } : {})}
trailing={
<div className="min-w-0 max-w-[min(62vw,48rem)]">
<ChatHeaderActions
activeThreadId={activeThread.id}
activeProjectName={activeProject?.name}
isGitRepo={isGitRepo}
openInCwd={gitCwd}
activeProjectScripts={activeProject?.scripts}
preferredScriptId={
activeProject ? (lastInvokedScriptByProjectId[activeProject.id] ?? null) : null
}
keybindings={keybindings}
availableEditors={availableEditors}
terminalAvailable={activeProject !== undefined}
terminalOpen={terminalState.terminalOpen}
terminalToggleShortcutLabel={terminalToggleShortcutLabel}
diffToggleShortcutLabel={diffPanelShortcutLabel}
gitCwd={gitCwd}
diffOpen={diffOpen}
onRunProjectScript={(script) => {
void runProjectScript(script);
}}
onAddProjectScript={saveProjectScript}
onUpdateProjectScript={updateProjectScript}
onDeleteProjectScript={deleteProjectScript}
onToggleTerminal={toggleTerminalVisibility}
onToggleDiff={onToggleDiff}
/>
</div>
}
keybindings={keybindings}
availableEditors={availableEditors}
terminalAvailable={activeProject !== undefined}
terminalOpen={terminalState.terminalOpen}
terminalToggleShortcutLabel={terminalToggleShortcutLabel}
diffToggleShortcutLabel={diffPanelShortcutLabel}
gitCwd={gitCwd}
diffOpen={diffOpen}
onRunProjectScript={(script) => {
void runProjectScript(script);
}}
onAddProjectScript={saveProjectScript}
onUpdateProjectScript={updateProjectScript}
onDeleteProjectScript={deleteProjectScript}
onToggleTerminal={toggleTerminalVisibility}
onToggleDiff={onToggleDiff}
/>
</header>
) : (
<header
className={cn(
"border-b border-border px-3 sm:px-5",
isElectron && !isWindowsElectron
? "drag-region flex h-[52px] items-center"
: "py-2 sm:py-3",
)}
>
<ChatHeader
activeThreadId={activeThread.id}
activeThreadTitle={activeThread.title}
activeProjectName={activeProject?.name}
isGitRepo={isGitRepo}
openInCwd={gitCwd}
activeProjectScripts={activeProject?.scripts}
preferredScriptId={
activeProject ? (lastInvokedScriptByProjectId[activeProject.id] ?? null) : null
}
keybindings={keybindings}
availableEditors={availableEditors}
terminalAvailable={activeProject !== undefined}
terminalOpen={terminalState.terminalOpen}
terminalToggleShortcutLabel={terminalToggleShortcutLabel}
diffToggleShortcutLabel={diffPanelShortcutLabel}
gitCwd={gitCwd}
diffOpen={diffOpen}
onRunProjectScript={(script) => {
void runProjectScript(script);
}}
onAddProjectScript={saveProjectScript}
onUpdateProjectScript={updateProjectScript}
onDeleteProjectScript={deleteProjectScript}
onToggleTerminal={toggleTerminalVisibility}
onToggleDiff={onToggleDiff}
/>
</header>
)}

{/* Error banner */}
<ProviderStatusBanner status={activeProviderStatus} />
Expand Down
73 changes: 73 additions & 0 deletions apps/web/src/components/DesktopTitleBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { ReactNode } from "react";

import { isWindowsElectron } from "~/env";
import { cn } from "~/lib/utils";

interface DesktopTitleBarProps {
title: string;
subtitle?: string;
trailing?: ReactNode;
className?: string;
reserveNativeWindowControlsOverlay?: boolean;
tone?: "default" | "subtle";
}

export function DesktopTitleBar(props: DesktopTitleBarProps) {
const reserveNativeWindowControlsOverlay =
props.reserveNativeWindowControlsOverlay ?? isWindowsElectron;
const tone = props.tone ?? "default";

return (
<div
className={cn(
"drag-region relative flex h-[52px] shrink-0 items-center border-b border-border/70 bg-background ps-5 pe-0 desktop-windows:h-[var(--desktop-titlebar-height)] desktop-windows:border-b-0 desktop-windows:bg-[var(--desktop-titlebar-surface)] desktop-windows:ps-4 desktop-windows:after:pointer-events-none desktop-windows:after:absolute desktop-windows:after:inset-x-0 desktop-windows:after:-bottom-px desktop-windows:after:border-b desktop-windows:after:border-border/70",
props.className,
)}
>
<div className="min-w-0 flex-1 pe-4 desktop-windows:pe-3">
<div className="min-w-0">
<div className="flex min-w-0 flex-col desktop-windows:flex-row desktop-windows:items-center desktop-windows:gap-2">
<div
className={cn(
"truncate",
tone === "subtle"
? "text-xs font-medium tracking-wide text-muted-foreground/70"
: "text-[12px] font-medium tracking-tight text-foreground/92",
)}
>
{props.title}
</div>
{props.subtitle ? (
<div
className={cn(
"truncate desktop-windows:text-[11px]",
tone === "subtle"
? "text-[10px] text-muted-foreground/50"
: "text-[10px] text-muted-foreground/85",
)}
>
{props.subtitle}
</div>
) : null}
</div>
</div>
</div>

<div className="ms-auto flex h-full shrink-0 items-center gap-1 [-webkit-app-region:no-drag]">
{props.trailing ? (
<div
className={cn(
"flex h-full items-center gap-1",
reserveNativeWindowControlsOverlay ? "me-3 desktop-windows:me-2" : "pe-3",
)}
>
{props.trailing}
</div>
) : null}
{reserveNativeWindowControlsOverlay ? (
<div aria-hidden="true" className="pointer-events-none h-full w-[138px] shrink-0" />
) : null}
</div>
</div>
);
}
34 changes: 27 additions & 7 deletions apps/web/src/components/DiffPanelShell.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import type { ReactNode } from "react";

import { isElectron } from "~/env";
import { isElectron, isWindowsElectron } from "~/env";
import { cn } from "~/lib/utils";

import { Skeleton } from "./ui/skeleton";

export type DiffPanelMode = "inline" | "sheet" | "sidebar";

function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {
const shouldUseDragRegion = isElectron && mode !== "sheet";
const shouldUseDragRegion = isElectron && mode !== "sheet" && !isWindowsElectron;
return cn(
"flex items-center justify-between gap-2 px-4",
shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12",
"flex items-center justify-between gap-2",
shouldUseDragRegion
? "drag-region h-[52px] border-b border-border px-4"
: mode !== "sheet"
? "h-12 px-4 desktop-windows:h-[var(--desktop-titlebar-height)]"
: "h-12 px-4",
);
}

function shouldReserveNativeOverlayInset(mode: DiffPanelMode) {
return isWindowsElectron && mode !== "sheet";
}

export function DiffPanelShell(props: {
mode: DiffPanelMode;
header: ReactNode;
children: ReactNode;
}) {
const shouldUseDragRegion = isElectron && props.mode !== "sheet";
const shouldUseDragRegion = isElectron && props.mode !== "sheet" && !isWindowsElectron;
const reserveNativeOverlayInset = shouldReserveNativeOverlayInset(props.mode);

return (
<div
Expand All @@ -34,8 +43,19 @@ export function DiffPanelShell(props: {
{shouldUseDragRegion ? (
<div className={getDiffPanelHeaderRowClassName(props.mode)}>{props.header}</div>
) : (
<div className="border-b border-border">
<div className={getDiffPanelHeaderRowClassName(props.mode)}>{props.header}</div>
<div
className={cn(
"relative border-b border-border/70 bg-background",
isWindowsElectron &&
"border-b-0 bg-[var(--desktop-titlebar-surface)] after:pointer-events-none after:absolute after:inset-x-0 after:-bottom-px after:border-b after:border-border/70",
)}
>
<div className={getDiffPanelHeaderRowClassName(props.mode)}>
{props.header}
{reserveNativeOverlayInset ? (
<div aria-hidden="true" className="pointer-events-none h-full w-[138px] shrink-0" />
) : null}
</div>
</div>
)}
{props.children}
Expand Down
Loading
Loading