From fa54ebbe49304e92117d8e4c04a5ead619c709a5 Mon Sep 17 00:00:00 2001 From: morgmart <98432065+morgmart@users.noreply.github.com> Date: Tue, 5 May 2026 13:11:08 -0700 Subject: [PATCH] fix goose2 small-window chat and settings layouts (#9019) Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> --- ui/goose2/src-tauri/capabilities/default.json | 2 + .../src-tauri/gen/schemas/capabilities.json | 2 +- ui/goose2/src-tauri/tauri.conf.json | 2 +- ui/goose2/src/app/AppShell.tsx | 100 +++++++++++++++++- .../src/features/chat/ui/AgentModelPicker.tsx | 2 +- .../src/features/chat/ui/ChatContextPanel.tsx | 51 +++++++-- ui/goose2/src/features/chat/ui/ChatInput.tsx | 9 +- .../features/chat/ui/ChatInputSelector.tsx | 1 + .../src/features/chat/ui/ChatInputToolbar.tsx | 16 ++- .../features/settings/ui/SettingsModal.tsx | 16 +-- 10 files changed, 173 insertions(+), 28 deletions(-) diff --git a/ui/goose2/src-tauri/capabilities/default.json b/ui/goose2/src-tauri/capabilities/default.json index 4fd2167549..5efbe97945 100644 --- a/ui/goose2/src-tauri/capabilities/default.json +++ b/ui/goose2/src-tauri/capabilities/default.json @@ -9,6 +9,8 @@ "core:window:allow-toggle-maximize", "core:window:allow-show", "core:window:allow-close", + "core:window:allow-set-size", + "core:window:allow-set-min-size", "opener:default", { "identifier": "opener:allow-open-path", diff --git a/ui/goose2/src-tauri/gen/schemas/capabilities.json b/ui/goose2/src-tauri/gen/schemas/capabilities.json index 1fe8678760..b18d5c5fba 100644 --- a/ui/goose2/src-tauri/gen/schemas/capabilities.json +++ b/ui/goose2/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","opener:default",{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"window-state:allow-restore-state","window-state:allow-save-window-state","dialog:allow-open","dialog:allow-save","app-test-driver:default","core:webview:allow-set-webview-zoom"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","core:window:allow-set-size","core:window:allow-set-min-size","opener:default",{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"window-state:allow-restore-state","window-state:allow-save-window-state","dialog:allow-open","dialog:allow-save","app-test-driver:default","core:webview:allow-set-webview-zoom"]}} \ No newline at end of file diff --git a/ui/goose2/src-tauri/tauri.conf.json b/ui/goose2/src-tauri/tauri.conf.json index 43e889a65f..7697524324 100644 --- a/ui/goose2/src-tauri/tauri.conf.json +++ b/ui/goose2/src-tauri/tauri.conf.json @@ -19,7 +19,7 @@ "title": "Goose", "width": 800, "height": 600, - "minWidth": 800, + "minWidth": 608, "minHeight": 600, "visible": false, "titleBarStyle": "Overlay", diff --git a/ui/goose2/src/app/AppShell.tsx b/ui/goose2/src/app/AppShell.tsx index 8c9c323979..45374f30f2 100644 --- a/ui/goose2/src/app/AppShell.tsx +++ b/ui/goose2/src/app/AppShell.tsx @@ -48,6 +48,13 @@ const SIDEBAR_MIN_WIDTH = 180; const SIDEBAR_MAX_WIDTH = 380; const SIDEBAR_SNAP_COLLAPSE_THRESHOLD = 100; const SIDEBAR_COLLAPSED_WIDTH = 48; +const APP_SHELL_HORIZONTAL_CHROME_WIDTH = 28; +const MIN_MAIN_CONTENT_WIDTH = 532; +const MIN_WINDOW_HEIGHT = 600; +const COLLAPSED_WINDOW_MIN_WIDTH = + SIDEBAR_COLLAPSED_WIDTH + + APP_SHELL_HORIZONTAL_CHROME_WIDTH + + MIN_MAIN_CONTENT_WIDTH; const SETTINGS_SECTIONS = new Set([ "appearance", "providers", @@ -59,6 +66,39 @@ const SETTINGS_SECTIONS = new Set([ "doctor", "about", ]); + +function getExpandedSidebarFitWidth(sidebarWidth: number) { + return ( + sidebarWidth + APP_SHELL_HORIZONTAL_CHROME_WIDTH + MIN_MAIN_CONTENT_WIDTH + ); +} + +async function ensureWindowWidth(minWidth: number) { + if (!window.__TAURI_INTERNALS__ || window.innerWidth >= minWidth) { + return; + } + + const { getCurrentWindow, LogicalSize } = await import( + "@tauri-apps/api/window" + ); + await getCurrentWindow().setSize( + new LogicalSize(minWidth, window.innerHeight), + ); +} + +async function syncWindowMinimumSize() { + if (!window.__TAURI_INTERNALS__) { + return; + } + + const { getCurrentWindow, LogicalSize } = await import( + "@tauri-apps/api/window" + ); + await getCurrentWindow().setMinSize( + new LogicalSize(COLLAPSED_WINDOW_MIN_WIDTH, MIN_WINDOW_HEIGHT), + ); +} + export function AppShell({ children }: { children?: React.ReactNode }) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [sidebarWidth, setSidebarWidth] = useState(SIDEBAR_DEFAULT_WIDTH); @@ -548,7 +588,30 @@ export function AppShell({ children }: { children?: React.ReactNode }) { handleNavigate("agents"), ); - const toggleSidebar = () => setSidebarCollapsed((prev) => !prev); + const collapseSidebar = useCallback(() => { + setSidebarCollapsed(true); + }, []); + + const expandSidebar = useCallback(async () => { + const expandedFitWidth = getExpandedSidebarFitWidth(sidebarWidth); + + try { + await ensureWindowWidth(expandedFitWidth); + } catch (error) { + console.warn("Failed to resize window before expanding sidebar:", error); + } + + setSidebarCollapsed(false); + }, [sidebarWidth]); + + const toggleSidebar = useCallback(() => { + if (sidebarCollapsed) { + void expandSidebar(); + return; + } + + collapseSidebar(); + }, [collapseSidebar, expandSidebar, sidebarCollapsed]); const handleResizeStart = useCallback( (e: React.MouseEvent) => { @@ -595,10 +658,39 @@ export function AppShell({ children }: { children?: React.ReactNode }) { ); const handleResizeDoubleClick = useCallback(() => { - setSidebarCollapsed(false); setSidebarWidth(SIDEBAR_DEFAULT_WIDTH); + void ensureWindowWidth(getExpandedSidebarFitWidth(SIDEBAR_DEFAULT_WIDTH)) + .catch((error) => { + console.warn( + "Failed to resize window before resetting sidebar:", + error, + ); + }) + .finally(() => setSidebarCollapsed(false)); }, []); + useEffect(() => { + void syncWindowMinimumSize().catch((error) => { + console.warn("Failed to update window minimum size:", error); + }); + }, []); + + useEffect(() => { + if (sidebarCollapsed) { + return; + } + + const handleWindowResize = () => { + if (window.innerWidth < getExpandedSidebarFitWidth(sidebarWidth)) { + setSidebarCollapsed(true); + } + }; + + handleWindowResize(); + window.addEventListener("resize", handleWindowResize); + return () => window.removeEventListener("resize", handleWindowResize); + }, [sidebarCollapsed, sidebarWidth]); + useEffect(() => { const handler = (e: KeyboardEvent) => { // Cmd+, for settings @@ -609,7 +701,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) { // Cmd+B for sidebar toggle if (e.key === "b" && e.metaKey) { e.preventDefault(); - setSidebarCollapsed((prev) => !prev); + toggleSidebar(); } // Cmd+W returns to home instead of closing the window if (e.key === "w" && e.metaKey) { @@ -628,7 +720,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) { }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); - }, [clearActiveSession, sessionStore]); + }, [clearActiveSession, sessionStore, toggleSidebar]); return (
diff --git a/ui/goose2/src/features/chat/ui/AgentModelPicker.tsx b/ui/goose2/src/features/chat/ui/AgentModelPicker.tsx index 5d82c27324..2accb0e0b7 100644 --- a/ui/goose2/src/features/chat/ui/AgentModelPicker.tsx +++ b/ui/goose2/src/features/chat/ui/AgentModelPicker.tsx @@ -102,7 +102,7 @@ export function AgentModelPicker({ disabled={loading && !selectedAgentLabel} leftIcon={getProviderIcon(selectedAgentId, "size-3.5")} rightIcon={} - className="min-w-0" + className="min-w-0 max-w-full" > {triggerModelLabel ?? diff --git a/ui/goose2/src/features/chat/ui/ChatContextPanel.tsx b/ui/goose2/src/features/chat/ui/ChatContextPanel.tsx index 916e8d3193..946bac0b63 100644 --- a/ui/goose2/src/features/chat/ui/ChatContextPanel.tsx +++ b/ui/goose2/src/features/chat/ui/ChatContextPanel.tsx @@ -3,7 +3,9 @@ import { IconLayoutSidebarRightFilled, } from "@tabler/icons-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { useEffect, useState } from "react"; import { Button } from "@/shared/ui/button"; +import { cn } from "@/shared/lib/cn"; import { ContextPanel } from "./ContextPanel"; const CP_PAD = 12; @@ -12,6 +14,7 @@ const CP_TOGGLE_RIGHT = CP_PAD + 12; const CP_TOGGLE_TOP = CP_PAD + 10; const CP_FADE_S = 0.15; const CP_REFLOW_MS = 200; +const CP_COMPACT_QUERY = "(max-width: 900px)"; interface ChatContextPanelProps { activeSessionId: string; @@ -33,15 +36,33 @@ export function ChatContextPanel({ setOpen, }: ChatContextPanelProps) { const shouldReduceMotion = useReducedMotion(); + const [isCompactViewport, setIsCompactViewport] = useState(false); const fadeTransition = { duration: shouldReduceMotion ? 0 : CP_FADE_S }; const reflowDuration = shouldReduceMotion ? 0 : CP_REFLOW_MS; + useEffect(() => { + if (!window.matchMedia) return; + + const mediaQuery = window.matchMedia(CP_COMPACT_QUERY); + setIsCompactViewport(mediaQuery.matches); + + const handleChange = (event: MediaQueryListEvent) => { + setIsCompactViewport(event.matches); + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, []); + return ( <>
@@ -49,17 +70,31 @@ export function ChatContextPanel({ {isOpen ? ( -