From ea7d712cf0bd0e73ecb636d2036dfa4af784f182 Mon Sep 17 00:00:00 2001 From: Wes Date: Sun, 29 Mar 2026 20:34:23 -0700 Subject: [PATCH] feat: add Skills and Agents pages with sidebar navigation (#4) - Add SkillsView and AgentsView stub pages matching goose2.0 layout (title left, action button right, search bar, empty state) - Add MainPanelLayout shared component for page layout - Add SearchBar shared component for page-level search - Add page-transition CSS animation (matching goose2.0) - Add storage path constants (~/.goose/) - Wire up sidebar nav items (Agents, Skills) with view routing - Add AppView type and view state management to AppShell - Sidebar active state uses subtle bg-background-secondary treatment --- ui/goose2/src/app/AppShell.tsx | 39 +++++++++++++-- .../src/features/agents/ui/AgentsView.tsx | 50 +++++++++++++++++++ ui/goose2/src/features/sidebar/ui/Sidebar.tsx | 17 +++++-- .../src/features/skills/ui/SkillsView.tsx | 50 +++++++++++++++++++ ui/goose2/src/shared/constants/storage.ts | 21 ++++++++ ui/goose2/src/shared/styles/globals.css | 39 +++++++++++++++ ui/goose2/src/shared/ui/MainPanelLayout.tsx | 21 ++++++++ ui/goose2/src/shared/ui/SearchBar.tsx | 35 +++++++++++++ 8 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 ui/goose2/src/features/agents/ui/AgentsView.tsx create mode 100644 ui/goose2/src/features/skills/ui/SkillsView.tsx create mode 100644 ui/goose2/src/shared/constants/storage.ts create mode 100644 ui/goose2/src/shared/ui/MainPanelLayout.tsx create mode 100644 ui/goose2/src/shared/ui/SearchBar.tsx diff --git a/ui/goose2/src/app/AppShell.tsx b/ui/goose2/src/app/AppShell.tsx index 132bfd4ab0..ff8db87aa6 100644 --- a/ui/goose2/src/app/AppShell.tsx +++ b/ui/goose2/src/app/AppShell.tsx @@ -4,9 +4,13 @@ import { Sidebar } from "@/features/sidebar/ui/Sidebar"; import { StatusBar } from "@/features/status/ui/StatusBar"; import { HomeScreen } from "@/features/home/ui/HomeScreen"; import { ChatView } from "@/features/chat/ui/ChatView"; +import { SkillsView } from "@/features/skills/ui/SkillsView"; +import { AgentsView } from "@/features/agents/ui/AgentsView"; import { SettingsModal } from "@/features/settings/ui/SettingsModal"; import type { Tab } from "@/features/tabs/types"; +export type AppView = "home" | "chat" | "skills" | "agents"; + const SIDEBAR_WIDTH = 240; const SIDEBAR_COLLAPSED_WIDTH = 48; @@ -15,24 +19,35 @@ export function AppShell({ children }: { children?: React.ReactNode }) { const [settingsOpen, setSettingsOpen] = useState(false); const [tabs, setTabs] = useState([]); const [activeTabId, setActiveTabId] = useState(null); - const isHome = activeTabId === null; + const [activeView, setActiveView] = useState("home"); + const isHome = activeTabId === null && activeView === "home"; const handleNewTab = () => { const id = String(Date.now()); setTabs((prev) => [...prev, { id, title: "New Chat" }]); setActiveTabId(id); + setActiveView("chat"); }; const handleTabClose = (id: string) => { setTabs((prev) => { const next = prev.filter((t) => t.id !== id); if (activeTabId === id) { - setActiveTabId(next[0]?.id ?? null); + const nextId = next[0]?.id ?? null; + setActiveTabId(nextId); + if (!nextId) setActiveView("home"); } return next; }); }; + const handleNavigate = (view: AppView) => { + setActiveView(view); + if (view !== "chat") { + setActiveTabId(null); + } + }; + const toggleSidebar = () => setSidebarCollapsed((prev) => !prev); useEffect(() => { @@ -52,6 +67,19 @@ export function AppShell({ children }: { children?: React.ReactNode }) { return () => window.removeEventListener("keydown", handler); }, []); + const renderContent = () => { + switch (activeView) { + case "skills": + return ; + case "agents": + return ; + case "chat": + return ; + case "home": + return activeTabId ? : ; + } + }; + return (
{/* Tab bar — full width across the top */} @@ -61,7 +89,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) { onTabSelect={setActiveTabId} onTabClose={handleTabClose} onNewTab={handleNewTab} - onHomeClick={() => setActiveTabId(null)} + onHomeClick={() => handleNavigate("home")} /> {/* Main content area — sidebar + content as flex row */} @@ -81,13 +109,16 @@ export function AppShell({ children }: { children?: React.ReactNode }) { width={SIDEBAR_WIDTH} onCollapse={toggleSidebar} onSettingsClick={() => setSettingsOpen(true)} + onNavigate={handleNavigate} + onNewChat={handleNewTab} + activeView={activeView} className="h-full shadow-xl rounded-xl" />
{/* Content area */}
- {children ?? (isHome ? : )} + {children ?? renderContent()}
diff --git a/ui/goose2/src/features/agents/ui/AgentsView.tsx b/ui/goose2/src/features/agents/ui/AgentsView.tsx new file mode 100644 index 0000000000..e70dd04746 --- /dev/null +++ b/ui/goose2/src/features/agents/ui/AgentsView.tsx @@ -0,0 +1,50 @@ +import { useState } from "react"; +import { Bot, Plus } from "lucide-react"; +import { SearchBar } from "@/shared/ui/SearchBar"; + +export function AgentsView() { + const [search, setSearch] = useState(""); + + return ( +
+
+ {/* Header */} +
+
+

Agents

+

+ Custom agent configurations for specific workflows +

+
+
+ +
+
+ + {/* Search */} + + + {/* Empty state */} +
+ +
+

No agents yet

+

+ Create an agent or add a YAML file to your agents folder. +

+
+
+
+
+ ); +} diff --git a/ui/goose2/src/features/sidebar/ui/Sidebar.tsx b/ui/goose2/src/features/sidebar/ui/Sidebar.tsx index 30f34c1a3b..805451c8aa 100644 --- a/ui/goose2/src/features/sidebar/ui/Sidebar.tsx +++ b/ui/goose2/src/features/sidebar/ui/Sidebar.tsx @@ -10,6 +10,7 @@ import { } from "lucide-react"; import { cn } from "@/shared/lib/cn"; import { GooseIcon } from "@/shared/ui/icons/GooseIcon"; +import type { AppView } from "@/app/AppShell"; interface SidebarProps { collapsed: boolean; @@ -18,13 +19,15 @@ interface SidebarProps { onSettingsClick?: () => void; onSearchClick?: () => void; onNewChat?: () => void; + onNavigate?: (view: AppView) => void; + activeView?: AppView; className?: string; } -const NAV_ITEMS = [ +const NAV_ITEMS: readonly { id: AppView; label: string; icon: typeof Bot }[] = [ { id: "agents", label: "Agents", icon: Bot }, { id: "skills", label: "Skills", icon: BookOpen }, -] as const; +]; const RECENT_CHATS = [ { id: "1", name: "Debug login flow", time: "2m" }, @@ -40,6 +43,8 @@ export function Sidebar({ onSettingsClick, onSearchClick, onNewChat, + onNavigate, + activeView, className, }: SidebarProps) { const [expanded, setExpanded] = useState(!collapsed); @@ -77,6 +82,7 @@ export function Sidebar({ > + + + + {/* Search */} + + + {/* Empty state */} +
+ +
+

No skills yet

+

+ Skills you add will appear here. +

+
+
+ + + ); +} diff --git a/ui/goose2/src/shared/constants/storage.ts b/ui/goose2/src/shared/constants/storage.ts new file mode 100644 index 0000000000..29860a886e --- /dev/null +++ b/ui/goose2/src/shared/constants/storage.ts @@ -0,0 +1,21 @@ +/** + * Storage path constants. + * + * All persistent data lives under ~/.goose/ to stay consistent with the + * goose CLI and goose2.0 desktop app. + */ + +/** Root directory for all Goose data. */ +export const GOOSE_DIR = ".goose"; + +/** Subdirectory for saved recipes / skills. */ +export const RECIPES_DIR = `${GOOSE_DIR}/recipes`; + +/** Subdirectory for agent configurations. */ +export const AGENTS_DIR = `${GOOSE_DIR}/agents`; + +/** Subdirectory for session history. */ +export const SESSIONS_DIR = `${GOOSE_DIR}/sessions`; + +/** Subdirectory for extension state. */ +export const EXTENSIONS_DIR = `${GOOSE_DIR}/extensions`; diff --git a/ui/goose2/src/shared/styles/globals.css b/ui/goose2/src/shared/styles/globals.css index ae3757afe2..11f295b64d 100644 --- a/ui/goose2/src/shared/styles/globals.css +++ b/ui/goose2/src/shared/styles/globals.css @@ -187,6 +187,38 @@ padding-right: calc(0.5rem * var(--density-spacing)); } +/* ═══════════════════════════════════════════════════════════ + Page-transition animation (matches goose2.0) + ═══════════════════════════════════════════════════════════ */ +@keyframes page-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.page-transition { + opacity: 0; + animation: page-fade-in 0.08s ease-out forwards; + pointer-events: auto; +} + +/* Content fade-in (used for list areas after loading) */ +@keyframes content-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.content-fade-in { + animation: content-fade-in 0.3s ease-out forwards; +} + @media (prefers-reduced-motion: reduce) { *, *::before, @@ -196,4 +228,11 @@ /* biome-ignore lint/complexity/noImportantStyles: required to override all transitions for reduced-motion accessibility */ transition-duration: 0.01ms !important; } + + .page-transition { + /* biome-ignore lint/complexity/noImportantStyles: required to override animation for reduced-motion accessibility */ + animation: none !important; + /* biome-ignore lint/complexity/noImportantStyles: required to override opacity for reduced-motion accessibility */ + opacity: 1 !important; + } } diff --git a/ui/goose2/src/shared/ui/MainPanelLayout.tsx b/ui/goose2/src/shared/ui/MainPanelLayout.tsx new file mode 100644 index 0000000000..d530da52be --- /dev/null +++ b/ui/goose2/src/shared/ui/MainPanelLayout.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react"; + +/** + * Layout wrapper for full-page views (Skills, Agents, etc.). + * + * Fills the parent container (typically `
` inside AppShell) + * and provides a flex-column context for header + scrollable content. + */ +export function MainPanelLayout({ + children, + backgroundColor = "bg-background-primary", +}: { + children: ReactNode; + backgroundColor?: string; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/ui/goose2/src/shared/ui/SearchBar.tsx b/ui/goose2/src/shared/ui/SearchBar.tsx new file mode 100644 index 0000000000..fabb30ae11 --- /dev/null +++ b/ui/goose2/src/shared/ui/SearchBar.tsx @@ -0,0 +1,35 @@ +import { Search } from "lucide-react"; +import { cn } from "@/shared/lib/cn"; + +interface SearchBarProps { + /** Current search value (controlled) */ + value: string; + /** Called when the search term changes */ + onChange: (term: string) => void; + /** Placeholder text */ + placeholder?: string; + /** Optional className for the wrapper */ + className?: string; +} + +export function SearchBar({ + value, + onChange, + placeholder, + className, +}: SearchBarProps) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full pl-9 pr-3 py-2 text-sm bg-background-secondary border border-border-secondary/50 rounded-lg placeholder:text-foreground-secondary/40 focus:outline-none focus:ring-1 focus:ring-ring focus:border-border-primary transition-colors" + /> +
+ ); +}