mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
fix goose2 small-window chat and settings layouts (#9019)
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
@@ -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"]}}
|
||||
@@ -19,7 +19,7 @@
|
||||
"title": "Goose",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"minWidth": 800,
|
||||
"minWidth": 608,
|
||||
"minHeight": 600,
|
||||
"visible": false,
|
||||
"titleBarStyle": "Overlay",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
Reference in New Issue
Block a user