From cc548ac82404a321cdcda67d7a1fb7124eac5e50 Mon Sep 17 00:00:00 2001 From: 24baigei Date: Mon, 20 Jul 2026 23:46:50 +0800 Subject: [PATCH 1/2] feat(gui): replace the Windows title bar row with a floating controls capsule Drop the dedicated 32px Windows title bar row. Content now reaches the window top edge; a 6px data-tauri-drag-region strip along the top keeps drag + double-click-maximize, and ChatHeader's existing drag region keeps working as before. The minimize/restore/close buttons live in a frosted floating capsule at the top-right, styled after the app's pill controls: soft red close hover, 55% opacity when the window is unfocused, and a corner-flush variant when maximized so the close button lands in the screen corner (Fitts). The capsule occupies the window's top-right corner, so the component publishes a --win-chrome-reserve CSS variable and the surfaces that put buttons there (ChatHeader, right-dock header, workspace editor/preview/SSH overlay headers) pad themselves by it; the variable is unset on other platforms, making this a no-op outside Windows. Co-Authored-By: Claude Fable 5 --- .../src/components/WindowsTitleBar.tsx | 85 +++++++------------ .../WorkspaceCodeEditorOverlay.tsx | 5 +- .../WorkspaceFilePreviewOverlay.tsx | 5 +- .../WorkspaceImagePreviewOverlay.tsx | 5 +- .../WorkspaceSshTerminalOverlay.tsx | 5 +- .../src/pages/chat/components/ChatHeader.tsx | 2 + 6 files changed, 50 insertions(+), 57 deletions(-) diff --git a/crates/agent-gui/src/components/WindowsTitleBar.tsx b/crates/agent-gui/src/components/WindowsTitleBar.tsx index 8597011a8..8ccc0f1b6 100644 --- a/crates/agent-gui/src/components/WindowsTitleBar.tsx +++ b/crates/agent-gui/src/components/WindowsTitleBar.tsx @@ -1,7 +1,6 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; -import { type MouseEvent, useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; -import iconSimpleUrl from "../../src-tauri/icons/icon-simple.png"; import { useLocale } from "../i18n"; import { cn } from "../lib/shared/utils"; import { Maximize2, Minimize2, Minus, X } from "./icons"; @@ -122,17 +121,18 @@ export function WindowsTitleBar() { }; }, [getAppWindow, isVisible, syncMaximized]); - const startDragging = useCallback( - (event: MouseEvent) => { - if (event.button !== 0 || event.detail !== 1) { - return; - } - void getAppWindow() - .startDragging() - .catch((error) => reportWindowChromeError("drag", error)); - }, - [getAppWindow], - ); + // 悬浮胶囊会盖住窗口右上角:把预留宽度暴露成全局 CSS 变量,各视图的 + // 顶右头部(ChatHeader / 右侧面板 / 编辑器覆盖层)据此加内边距让位。 + useEffect(() => { + if (!isVisible || typeof document === "undefined") { + return undefined; + } + const root = document.documentElement; + root.style.setProperty("--win-chrome-reserve", "128px"); + return () => { + root.style.removeProperty("--win-chrome-reserve"); + }; + }, [isVisible]); const toggleMaximize = useCallback(() => { const appWindow = getAppWindow(); @@ -143,16 +143,6 @@ export function WindowsTitleBar() { .catch((error) => reportWindowChromeError("toggle maximized state for", error)); }, [getAppWindow]); - const handleTitleDoubleClick = useCallback( - (event: MouseEvent) => { - if (event.button !== 0) { - return; - } - toggleMaximize(); - }, - [toggleMaximize], - ); - const minimizeWindow = useCallback(() => { void getAppWindow() .minimize() @@ -172,36 +162,25 @@ export function WindowsTitleBar() { const maximizeLabel = isMaximized ? t("window.restore") : t("window.maximize"); return ( -
-
- - - {t("app.name")} - -
+ <> + {/* 顶缘拖拽热区:内容顶到窗顶后,这 6px 承担拖拽与双击最大化 + (data-tauri-drag-region 由 Tauri 核心处理,二者内建)。 */} +
-
-
-
+ + ); } diff --git a/crates/agent-gui/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx b/crates/agent-gui/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx index bcb648493..88c2950a4 100644 --- a/crates/agent-gui/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx +++ b/crates/agent-gui/src/components/workspace-editor/WorkspaceCodeEditorOverlay.tsx @@ -853,7 +853,10 @@ export function WorkspaceCodeEditorOverlay(props: WorkspaceCodeEditorOverlayProp )} > -
+
diff --git a/crates/agent-gui/src/components/workspace-editor/WorkspaceFilePreviewOverlay.tsx b/crates/agent-gui/src/components/workspace-editor/WorkspaceFilePreviewOverlay.tsx index 2f69e588f..9f3f9aab4 100644 --- a/crates/agent-gui/src/components/workspace-editor/WorkspaceFilePreviewOverlay.tsx +++ b/crates/agent-gui/src/components/workspace-editor/WorkspaceFilePreviewOverlay.tsx @@ -506,7 +506,10 @@ export function WorkspaceFilePreviewOverlay(props: WorkspaceFilePreviewOverlayPr )} > -
+
diff --git a/crates/agent-gui/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx b/crates/agent-gui/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx index fabc58ceb..c2e972f7c 100644 --- a/crates/agent-gui/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx +++ b/crates/agent-gui/src/components/workspace-editor/WorkspaceImagePreviewOverlay.tsx @@ -128,7 +128,10 @@ export function WorkspaceImagePreviewOverlay(props: WorkspaceImagePreviewOverlay )} > -
+
diff --git a/crates/agent-gui/src/components/workspace-editor/WorkspaceSshTerminalOverlay.tsx b/crates/agent-gui/src/components/workspace-editor/WorkspaceSshTerminalOverlay.tsx index 6b38ea880..f87f85d35 100644 --- a/crates/agent-gui/src/components/workspace-editor/WorkspaceSshTerminalOverlay.tsx +++ b/crates/agent-gui/src/components/workspace-editor/WorkspaceSshTerminalOverlay.tsx @@ -222,7 +222,10 @@ export function WorkspaceSshTerminalOverlay(props: WorkspaceSshTerminalOverlayPr )} > -
+
diff --git a/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx b/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx index 3b18c0232..7e1ed07dd 100644 --- a/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx +++ b/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx @@ -121,6 +121,8 @@ export const ChatHeader = memo(function ChatHeader(props: { "flex items-center justify-between gap-2 py-2.5 pr-4", !sidebarOpen && macOsTauri ? "pl-[232px]" : "pl-4", )} + // Windows 悬浮窗口控制胶囊占据右上角,头部右端让出同宽空间。 + style={{ paddingRight: "calc(1rem + var(--win-chrome-reserve, 0px))" }} >
{!sidebarOpen && !macOsTauri ? ( From d599a74d60415da1e17dcbcba4ffc2c2f18f728c Mon Sep 17 00:00:00 2001 From: 24baigei Date: Mon, 20 Jul 2026 23:47:19 +0800 Subject: [PATCH 2/2] feat(project-tools): streamline right-dock creation flow and chrome - Empty state (launcher) no longer renders the header row: the launcher cards are the full creation surface, so the lone "+" disappears. When multiple shells exist the New Terminal card becomes a split button with a chevron opening the same shell menu the "+" offered; the mobile close button floats alone at the top-right. - With tabs open, the create menu ("+") now sits inline right after the last tab, browser-style, instead of parked at the header's right edge. - The panel's left divider is a single full-height hairline that fades in from transparent over the first 3rem, so the top strip reads as one continuous surface in both states while the solid segment still lines up with the header's bottom border when tabs are open. - Localize the project-tools toggle tooltip and disabled reasons (zh-CN/en-US); they were hardcoded English. Co-Authored-By: Claude Fable 5 --- .../project-tools/RightDockLauncher.tsx | 83 ++++++--- .../project-tools/RightDockPanel.tsx | 160 +++++++++++------- .../project-tools/RightDockLauncher.tsx | 83 ++++++--- .../project-tools/RightDockPanel.tsx | 160 +++++++++++------- crates/agent-gui/src/i18n/config.ts | 8 + crates/agent-gui/src/pages/ChatPage.tsx | 8 +- 6 files changed, 332 insertions(+), 170 deletions(-) diff --git a/crates/agent-gateway/web/src/components/project-tools/RightDockLauncher.tsx b/crates/agent-gateway/web/src/components/project-tools/RightDockLauncher.tsx index b4c1d6759..b3f3ba65a 100644 --- a/crates/agent-gateway/web/src/components/project-tools/RightDockLauncher.tsx +++ b/crates/agent-gateway/web/src/components/project-tools/RightDockLauncher.tsx @@ -1,7 +1,7 @@ import { useLocale } from "../../i18n"; import { cn } from "../../lib/shared/utils"; import type { TerminalShellOption } from "../../lib/terminal/types"; -import { ChevronRight, Cpu, Plus, Terminal } from "../icons"; +import { ChevronDown, ChevronRight, Cpu, Plus, Terminal } from "../icons"; import { buttonVariants } from "../ui/button"; import { DropdownMenu, @@ -34,6 +34,7 @@ type RightDockCreateMenuProps = RightDockLauncherActions & { }; type RightDockChooserProps = RightDockLauncherActions & { + shellOptions: TerminalShellOption[]; terminalReady: boolean; terminalDisabledMessage?: string; disabledMessage?: string; @@ -105,7 +106,7 @@ export function RightDockCreateMenu(props: RightDockCreateMenuProps) { title={t("projectTools.newProjectTool")} className={cn( buttonVariants({ variant: "ghost", size: "icon" }), - "h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground", + "h-7 w-7 rounded-md text-muted-foreground/80 hover:text-foreground", )} > @@ -134,6 +135,7 @@ export function RightDockCreateMenu(props: RightDockCreateMenuProps) { export function RightDockChooser(props: RightDockChooserProps) { const { + shellOptions, terminalReady, terminalDisabledMessage, disabledMessage, @@ -184,26 +186,67 @@ export function RightDockChooser(props: RightDockChooserProps) {

{t("projectTools.getStartedHint")}

- {tools.map((tool) => ( - + ); + if (!splitShellMenu) { + return
{card}
; + } + return ( +
+ {card} + + + + + + {shellOptions.map((option) => ( + onCreateTerminal(option.id)} + disabled={tool.disabled} + className="gap-2 text-xs" + title={option.command || option.label} + > + + {option.label} + + ))} + +
- - ))} + ); + })}
{loading ? (
{t("projectTools.loading")}
diff --git a/crates/agent-gateway/web/src/components/project-tools/RightDockPanel.tsx b/crates/agent-gateway/web/src/components/project-tools/RightDockPanel.tsx index f137cb3a6..17a509049 100644 --- a/crates/agent-gateway/web/src/components/project-tools/RightDockPanel.tsx +++ b/crates/agent-gateway/web/src/components/project-tools/RightDockPanel.tsx @@ -732,7 +732,7 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel className={cn( "project-tools-panel zone-font-scale fixed inset-x-0 bottom-0 z-40 flex h-[min(72vh,34rem)] min-h-0 w-full shrink-0 flex-col overflow-hidden bg-background shadow-2xl transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none md:relative md:inset-auto md:z-10 md:h-full md:overflow-visible md:shadow-none", isOpen - ? "pointer-events-auto translate-y-0 border-t border-border opacity-100 md:w-[var(--project-tools-panel-width)] md:translate-x-0 md:border-l md:border-t-0" + ? "pointer-events-auto translate-y-0 border-t border-border opacity-100 md:w-[var(--project-tools-panel-width)] md:translate-x-0 md:border-t-0" : "pointer-events-none translate-y-full border-t border-transparent opacity-0 md:translate-x-3 md:translate-y-0 md:border-l-0 md:border-t-0", effectiveWidthCollapsed ? "md:w-0" : "md:w-[var(--project-tools-panel-width)]", )} @@ -749,6 +749,15 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel {effectiveShouldRenderContent ? ( <>