Merge remote-tracking branch 'upstream/dev' into desktop-wsl-onboarding

This commit is contained in:
LukeParkerDev
2026-05-05 15:15:18 +10:00
4 changed files with 179 additions and 141 deletions

View File

@@ -35,6 +35,9 @@ type TauriApi = {
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
const titlebarHeight = 40
const minTitlebarZoom = 0.25
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
export function Titlebar() {
const layout = useLayout()
@@ -51,7 +54,14 @@ export function Titlebar() {
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
const web = createMemo(() => platform.platform === "web")
const zoom = () => platform.webviewZoom?.() ?? 1
const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined)
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
const minHeight = () => {
if (mac()) return `${titlebarHeight / zoom()}px`
if (windows()) return `${titlebarHeight / Math.min(titlebarZoom(), 1)}px`
return undefined
}
const windowsControlsWidth = () => `${windowsControlsBaseWidth / Math.max(titlebarZoom(), 1)}px`
const [history, setHistory] = createStore({
stack: [] as string[],
@@ -165,156 +175,161 @@ export function Titlebar() {
return (
<header
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
class="h-10 shrink-0 bg-background-base relative overflow-hidden"
style={{ "min-height": minHeight() }}
data-tauri-drag-region
onMouseDown={drag}
onDblClick={maximize}
>
<div
classList={{
"flex items-center min-w-0": true,
"pl-2": !mac(),
}}
class="grid h-full min-h-full w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
style={{ zoom: counterZoom() }}
>
<Show when={mac()}>
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
variant="ghost"
class="titlebar-icon rounded-md"
onClick={layout.mobileSidebar.toggle}
aria-label={language.t("sidebar.menu.toggle")}
aria-expanded={layout.mobileSidebar.opened()}
/>
</div>
</Show>
<Show when={!mac()}>
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
variant="ghost"
class="titlebar-icon rounded-md"
onClick={layout.mobileSidebar.toggle}
aria-label={language.t("sidebar.menu.toggle")}
aria-expanded={layout.mobileSidebar.opened()}
/>
</div>
</Show>
<div class="flex items-center gap-1 shrink-0">
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
>
<Button
variant="ghost"
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
<div
classList={{
"flex items-center min-w-0": true,
"pl-2": !mac(),
}}
>
<Show when={mac()}>
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
variant="ghost"
class="titlebar-icon rounded-md"
onClick={layout.mobileSidebar.toggle}
aria-label={language.t("sidebar.menu.toggle")}
aria-expanded={layout.mobileSidebar.opened()}
/>
</div>
</Show>
<Show when={!mac()}>
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
variant="ghost"
class="titlebar-icon rounded-md"
onClick={layout.mobileSidebar.toggle}
aria-label={language.t("sidebar.menu.toggle")}
aria-expanded={layout.mobileSidebar.opened()}
/>
</div>
</Show>
<div class="flex items-center gap-1 shrink-0">
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
>
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
</Button>
</TooltipKeybind>
<div class="hidden xl:flex items-center shrink-0">
<Show when={params.dir}>
<div
class="flex items-center shrink-0 w-8 mr-1"
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
<Button
variant="ghost"
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
>
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
</Button>
</TooltipKeybind>
<div class="hidden xl:flex items-center shrink-0">
<Show when={params.dir}>
<div
class="transition-opacity"
classList={{
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
}}
class="flex items-center shrink-0 w-8 mr-1"
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
>
<TooltipKeybind
placement="bottom"
title={language.t("command.session.new")}
keybind={command.keybind("session.new")}
openDelay={2000}
<div
class="transition-opacity"
classList={{
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
}}
>
<Button
variant="ghost"
icon={creating() ? "new-session-active" : "new-session"}
class="titlebar-icon w-8 h-6 p-0 box-border"
disabled={layout.sidebar.opened()}
tabIndex={layout.sidebar.opened() ? -1 : undefined}
onClick={() => {
if (!params.dir) return
navigate(`/${params.dir}/session`)
}}
aria-label={language.t("command.session.new")}
aria-current={creating() ? "page" : undefined}
/>
</TooltipKeybind>
</div>
</div>
</Show>
<div
class="flex items-center shrink-0"
classList={{
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
"duration-180 ease-out": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(),
}}
>
<Show when={hasProjects() && nav()}>
<div class="flex items-center gap-0 transition-transform">
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-left"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canBack()}
onClick={back}
aria-label={language.t("common.goBack")}
/>
</Tooltip>
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-right"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canForward()}
onClick={forward}
aria-label={language.t("common.goForward")}
/>
</Tooltip>
<TooltipKeybind
placement="bottom"
title={language.t("command.session.new")}
keybind={command.keybind("session.new")}
openDelay={2000}
>
<Button
variant="ghost"
icon={creating() ? "new-session-active" : "new-session"}
class="titlebar-icon w-8 h-6 p-0 box-border"
disabled={layout.sidebar.opened()}
tabIndex={layout.sidebar.opened() ? -1 : undefined}
onClick={() => {
if (!params.dir) return
navigate(`/${params.dir}/session`)
}}
aria-label={language.t("command.session.new")}
aria-current={creating() ? "page" : undefined}
/>
</TooltipKeybind>
</div>
</div>
</Show>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
</div>
)}
<div
class="flex items-center shrink-0"
classList={{
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
"duration-180 ease-out": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(),
}}
>
<Show when={hasProjects() && nav()}>
<div class="flex items-center gap-0 transition-transform">
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-left"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canBack()}
onClick={back}
aria-label={language.t("common.goBack")}
/>
</Tooltip>
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-right"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canForward()}
onClick={forward}
aria-label={language.t("common.goForward")}
/>
</Tooltip>
</div>
</Show>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
</div>
)}
</div>
</div>
</div>
</div>
</div>
<div class="min-w-0 flex items-center justify-center pointer-events-none">
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
</div>
<div class="min-w-0 flex items-center justify-center pointer-events-none">
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
</div>
<div
classList={{
"flex items-center min-w-0 justify-end": true,
"pr-2": !windows(),
}}
data-tauri-drag-region
onMouseDown={drag}
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
<Show when={windows()}>
{!tauriApi() && <div class="w-36 shrink-0" />}
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
<div
classList={{
"flex items-center min-w-0 justify-end": true,
"pr-2": !windows(),
}}
data-tauri-drag-region
onMouseDown={drag}
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
<Show when={windows()}>
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>
</div>
</header>
)

View File

@@ -13,7 +13,7 @@ import type {
WslServersState,
} from "../preload/types"
import { getStore } from "./store"
import { setTitlebar } from "./windows"
import { setTitlebar, updateTitlebar } from "./windows"
const pickerFilters = (ext?: string[]) => {
if (!ext || ext.length === 0) return undefined
@@ -258,7 +258,12 @@ export function registerIpcHandlers(deps: Deps) {
})
ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor))
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => {
event.sender.setZoomFactor(factor)
const win = BrowserWindow.fromWebContents(event.sender)
if (!win) return
updateTitlebar(win)
})
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (!win) return

View File

@@ -21,6 +21,8 @@ protocol.registerSchemesAsPrivileged([
])
let backgroundColor: string | undefined
const titlebarThemes = new WeakMap<BrowserWindow, Partial<TitlebarTheme>>()
const titlebarHeight = 40
export function setBackgroundColor(color: string) {
backgroundColor = color
@@ -43,18 +45,23 @@ function tone() {
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
}
function overlay(theme: Partial<TitlebarTheme> = {}) {
function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
const mode = theme.mode ?? tone()
return {
color: "#00000000",
symbolColor: mode === "dark" ? "white" : "black",
height: 40,
height: Math.max(titlebarHeight, Math.round(titlebarHeight * zoom)),
}
}
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
titlebarThemes.set(win, theme)
updateTitlebar(win)
}
export function updateTitlebar(win: BrowserWindow) {
if (process.platform !== "win32") return
win.setTitleBarOverlay(overlay(theme))
win.setTitleBarOverlay(overlay(titlebarThemes.get(win), win.webContents.getZoomFactor()))
}
export function setDockIcon() {
@@ -188,6 +195,7 @@ function wireZoom(win: BrowserWindow) {
win.webContents.setZoomFactor(1)
win.webContents.on("zoom-changed", () => {
win.webContents.setZoomFactor(1)
updateTitlebar(win)
})
}

View File

@@ -12,6 +12,7 @@ const OS_NAME = (() => {
})()
const [webviewZoom, setWebviewZoom] = createSignal(1)
let requestedZoom = 1
const MAX_ZOOM_LEVEL = 10
const MIN_ZOOM_LEVEL = 0.2
@@ -19,8 +20,17 @@ const MIN_ZOOM_LEVEL = 0.2
const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
const applyZoom = (next: number) => {
setWebviewZoom(next)
void window.api.setZoomFactor(next)
requestedZoom = next
void window.api
.setZoomFactor(next)
.then(() => {
if (requestedZoom !== next) return
setWebviewZoom(next)
})
.catch(() => {
if (requestedZoom !== next) return
requestedZoom = webviewZoom()
})
}
window.addEventListener("keydown", (event) => {
@@ -28,12 +38,12 @@ window.addEventListener("keydown", (event) => {
if (event.key === "-") {
event.preventDefault()
applyZoom(clamp(webviewZoom() - 0.2))
applyZoom(clamp(requestedZoom - 0.2))
return
}
if (event.key === "=" || event.key === "+") {
event.preventDefault()
applyZoom(clamp(webviewZoom() + 0.2))
applyZoom(clamp(requestedZoom + 0.2))
return
}
if (event.key === "0") {