fix goose2 small-window chat and settings layouts (#9019)

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
morgmart
2026-05-05 13:11:08 -07:00
committed by GitHub
parent 8e69e634f3
commit fa54ebbe49
10 changed files with 173 additions and 28 deletions
@@ -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",
@@ -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"]}}
{"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"]}}
+1 -1
View File
@@ -19,7 +19,7 @@
"title": "Goose",
"width": 800,
"height": 600,
"minWidth": 800,
"minWidth": 608,
"minHeight": 600,
"visible": false,
"titleBarStyle": "Overlay",
+96 -4
View File
@@ -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<SectionId>([
"appearance",
"providers",
@@ -59,6 +66,39 @@ const SETTINGS_SECTIONS = new Set<SectionId>([
"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 (
<div className="flex h-screen w-screen flex-col overflow-hidden bg-background text-foreground">
@@ -102,7 +102,7 @@ export function AgentModelPicker({
disabled={loading && !selectedAgentLabel}
leftIcon={getProviderIcon(selectedAgentId, "size-3.5")}
rightIcon={<IconChevronDown className="opacity-50" />}
className="min-w-0"
className="min-w-0 max-w-full"
>
<span className={cn("truncate", isCompact ? "max-w-32" : "max-w-56")}>
{triggerModelLabel ??
@@ -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 (
<>
<div
className="shrink-0 overflow-hidden"
className={cn(
"shrink-0",
isCompactViewport ? "overflow-visible" : "overflow-hidden",
)}
style={{
width: isOpen ? CP_TOTAL_W : 0,
width: isOpen && !isCompactViewport ? CP_TOTAL_W : 0,
transition: `width ${reflowDuration}ms ease`,
}}
>
@@ -49,17 +70,31 @@ export function ChatContextPanel({
{isOpen ? (
<motion.div
key="context-panel"
className="flex h-full"
style={{
width: CP_TOTAL_W,
padding: CP_PAD,
}}
className={cn(
"flex",
isCompactViewport
? "absolute bottom-3 right-3 top-12 z-10 w-[min(340px,calc(100%-1.5rem))]"
: "h-full",
)}
style={
isCompactViewport
? undefined
: {
width: CP_TOTAL_W,
padding: CP_PAD,
}
}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={fadeTransition}
>
<aside className="flex min-w-0 flex-1 overflow-hidden rounded-xl border border-border bg-background">
<aside
className={cn(
"flex min-w-0 flex-1 overflow-hidden rounded-xl border border-border bg-background",
isCompactViewport && "shadow-modal",
)}
>
<ContextPanel
sessionId={activeSessionId}
projectName={project?.name}
+7 -2
View File
@@ -370,14 +370,19 @@ export function ChatInput({
return (
<TooltipProvider delayDuration={300}>
<div className={cn("relative z-10 px-4 pb-6 pt-0", className)}>
<div
className={cn(
"relative z-10 px-2 pb-3 pt-0 sm:px-4 sm:pb-6",
className,
)}
>
<div className="mx-auto max-w-3xl">
<Popover open={mentionOpen}>
{/* biome-ignore lint/a11y/noStaticElementInteractions: drop zone for file attachments */}
<div
ref={containerRef}
className={cn(
"relative rounded-2xl border border-border bg-background px-4 pb-3 pt-4 transition-colors",
"relative rounded-2xl border border-border bg-background px-3 pb-3 pt-4 transition-colors sm:px-4",
isAttachmentDragOver && "bg-muted/20",
)}
onDragEnter={handleDragEnter}
@@ -71,6 +71,7 @@ export function ChatInputSelector({
className={cn(
"min-w-0",
triggerVariant === "default" && "justify-between",
triggerVariant === "toolbar" && "max-w-40",
)}
>
<span className="truncate">{triggerLabel}</span>
@@ -209,9 +209,19 @@ export function ChatInputToolbar({
}, [isContextPopoverOpen, showContextUsage]);
return (
<div className="flex items-center justify-between gap-2">
<div
className={cn(
"flex items-center justify-between gap-2",
isCompact && "flex-wrap gap-y-2",
)}
>
{/* Left side: pickers */}
<div className="flex items-center gap-0.5">
<div
className={cn(
"flex min-w-0 items-center gap-0.5",
isCompact && "flex-1",
)}
>
{(agentProviders.length > 0 || providersLoading) && (
<AgentModelPicker
agents={agentProviders}
@@ -281,7 +291,7 @@ export function ChatInputToolbar({
</div>
{/* Right side: actions */}
<div className="flex items-center">
<div className={cn("flex shrink-0 items-center", isCompact && "ml-auto")}>
<div className="flex items-center gap-px">
{showContextUsage && (
<Popover
@@ -102,7 +102,7 @@ export function SettingsModal({
{/* biome-ignore lint/a11y/noStaticElementInteractions: click handler only prevents backdrop dismiss propagation */}
<div
className={cn(
"flex h-[min(600px,calc(100vh-4rem))] w-[calc(100vw-2rem)] max-w-3xl overflow-hidden rounded-xl border bg-background shadow-modal transition-opacity duration-300 ease-out",
"flex h-[min(600px,calc(100vh-4rem))] w-[calc(100vw-2rem)] max-w-3xl overflow-hidden rounded-xl border bg-background shadow-modal transition-opacity duration-300 ease-out max-[640px]:h-[calc(100vh-1rem)] max-[640px]:w-[calc(100vw-1rem)] max-[640px]:flex-col",
isLoaded ? "opacity-100" : "opacity-0",
)}
onClick={(e) => e.stopPropagation()}
@@ -110,13 +110,13 @@ export function SettingsModal({
{/* Sidebar */}
<div
className={cn(
"flex min-h-0 w-44 flex-shrink-0 flex-col border-r bg-muted/50 transition-all duration-700 ease-out",
"flex min-h-0 w-44 flex-shrink-0 flex-col border-r bg-muted/50 transition-all duration-700 ease-out max-[640px]:max-h-32 max-[640px]:w-full max-[640px]:border-b max-[640px]:border-r-0",
isLoaded ? "opacity-100 translate-x-0" : "opacity-0 -translate-x-2",
)}
>
<div
className={cn(
"px-4 py-4 transition-all duration-500 ease-out",
"px-4 py-4 transition-all duration-500 ease-out max-[640px]:px-3 max-[640px]:py-3",
isLoaded
? "opacity-100 translate-x-0"
: "opacity-0 -translate-x-2",
@@ -124,8 +124,8 @@ export function SettingsModal({
>
<h2 className="text-sm font-semibold">{t("title")}</h2>
</div>
<nav className="min-h-0 flex-1 overflow-y-auto px-2 pb-3">
<div className="flex flex-col gap-1">
<nav className="min-h-0 flex-1 overflow-y-auto px-2 pb-3 max-[640px]:overflow-x-auto max-[640px]:overflow-y-hidden max-[640px]:pb-2">
<div className="flex flex-col gap-1 max-[640px]:min-w-max max-[640px]:flex-row">
{navItems.map((item, index) => (
<Button
type="button"
@@ -134,7 +134,7 @@ export function SettingsModal({
key={item.id}
onClick={() => setActiveSection(item.id)}
className={cn(
"w-full justify-start rounded-lg px-3 py-2 transition-all duration-600 ease-out",
"w-full justify-start rounded-lg px-3 py-2 transition-all duration-600 ease-out max-[640px]:w-auto max-[640px]:shrink-0 max-[640px]:px-2.5",
activeSection === item.id
? "bg-background text-foreground shadow-sm hover:bg-background"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground duration-300",
@@ -162,13 +162,13 @@ export function SettingsModal({
size="icon-xs"
onClick={onClose}
aria-label={t("common:actions.close")}
className="absolute right-4 top-4 z-30 rounded-md text-muted-foreground hover:text-foreground"
className="absolute right-4 top-4 z-30 rounded-md text-muted-foreground hover:text-foreground max-[640px]:right-3 max-[640px]:top-3"
>
<X className="size-4" />
</Button>
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="px-6 pb-4">
<div className="px-6 pb-4 max-[640px]:px-4 max-[640px]:pt-1">
{activeSection === "appearance" && <AppearanceSettings />}
{activeSection === "providers" && <ProvidersSettings />}
{activeSection === "compaction" && <CompactionSettings />}