diff --git a/ui/goose2/src/features/chat/stores/chatSessionStore.ts b/ui/goose2/src/features/chat/stores/chatSessionStore.ts index a39e4cca0d..05162c914b 100644 --- a/ui/goose2/src/features/chat/stores/chatSessionStore.ts +++ b/ui/goose2/src/features/chat/stores/chatSessionStore.ts @@ -29,6 +29,7 @@ interface ChatSessionStoreState { sessions: ChatSession[]; activeSessionId: string | null; isLoading: boolean; + contextPanelOpenBySession: Record; } interface ChatSessionStoreActions { @@ -47,6 +48,8 @@ interface ChatSessionStoreActions { setActiveSession: (sessionId: string | null) => void; + setContextPanelOpen: (sessionId: string, open: boolean) => void; + // Helpers getSession: (id: string) => ChatSession | undefined; getActiveSession: () => ChatSession | null; @@ -99,6 +102,7 @@ export const useChatSessionStore = create((set, get) => ({ sessions: loadCachedSessions(), activeSessionId: null, isLoading: false, + contextPanelOpenBySession: {}, // Session lifecycle createSession: async (opts) => { @@ -193,7 +197,12 @@ export const useChatSessionStore = create((set, get) => ({ const nextSessions = get() .sessions.map((s) => (s.id === id ? { ...s, archivedAt } : s)) .filter((s) => !s.archivedAt); - set({ sessions: nextSessions }); + const { [id]: _, ...remainingPanelState } = + get().contextPanelOpenBySession; + set({ + sessions: nextSessions, + contextPanelOpenBySession: remainingPanelState, + }); persistSessions(nextSessions); } catch (err) { set({ @@ -219,6 +228,15 @@ export const useChatSessionStore = create((set, get) => ({ set({ activeSessionId: sessionId }); }, + setContextPanelOpen: (sessionId, open) => { + set((state) => ({ + contextPanelOpenBySession: { + ...state.contextPanelOpenBySession, + [sessionId]: open, + }, + })); + }, + // Helpers getSession: (id) => get().sessions.find((s) => s.id === id), diff --git a/ui/goose2/src/features/chat/ui/ChatView.tsx b/ui/goose2/src/features/chat/ui/ChatView.tsx index c46e62f4b1..acd7955bac 100644 --- a/ui/goose2/src/features/chat/ui/ChatView.tsx +++ b/ui/goose2/src/features/chat/ui/ChatView.tsx @@ -65,7 +65,10 @@ export function ChatView({ onCreateProject, }: ChatViewProps) { const [activeSessionId] = useState(() => sessionId ?? crypto.randomUUID()); - const [isContextPanelOpen, setIsContextPanelOpen] = useState(true); + const isContextPanelOpen = useChatSessionStore( + (s) => s.contextPanelOpenBySession[activeSessionId] ?? false, + ); + const setContextPanelOpen = useChatSessionStore((s) => s.setContextPanelOpen); const shouldReduceMotion = useReducedMotion(); const fadeTransition = { duration: shouldReduceMotion ? 0 : CONTEXT_PANEL_FADE_DURATION_SECONDS, @@ -452,7 +455,9 @@ export function ChatView({ type="button" variant="ghost" size="icon-sm" - onClick={() => setIsContextPanelOpen((prev) => !prev)} + onClick={() => + setContextPanelOpen(activeSessionId, !isContextPanelOpen) + } aria-label={ isContextPanelOpen ? "Close context panel" : "Open context panel" } diff --git a/ui/goose2/src/features/chat/ui/ContextPanel.tsx b/ui/goose2/src/features/chat/ui/ContextPanel.tsx index ded1442eba..128ecbf34f 100644 --- a/ui/goose2/src/features/chat/ui/ContextPanel.tsx +++ b/ui/goose2/src/features/chat/ui/ContextPanel.tsx @@ -1,21 +1,14 @@ import { type ReactNode, useState } from "react"; import { - Activity, - FileCode, - FileText, - FolderOpen, - GitBranch, - RefreshCw, - Server, -} from "lucide-react"; + IconFolder, + IconGitBranch, + IconRefresh, + IconServer, + IconFileCode, + IconActivity, +} from "@tabler/icons-react"; import { useGitState } from "@/shared/hooks/useGitState"; import { Badge } from "@/shared/ui/badge"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/shared/ui/accordion"; import { Button } from "@/shared/ui/button"; import { Spinner } from "@/shared/ui/spinner"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs"; @@ -28,38 +21,33 @@ interface ContextPanelProps { type ContextPanelTab = "details" | "files"; -function PanelSection({ - value, +function Widget({ title, icon, + action, children, }: { - value: string; title: string; icon: ReactNode; + action?: ReactNode; children: ReactNode; }) { return ( - - - +
+
+
{icon} {title} - - - {children} - +
+ {action} +
+
+ {children} +
+
); } -function formatDirtyFileCount(count: number) { - if (count === 0) { - return "Clean"; - } - - return count === 1 ? "1 file changed" : `${count} files changed`; -} - export function ContextPanel({ projectName, projectColor, @@ -74,10 +62,6 @@ export function ContextPanel({ refetch, } = useGitState(projectWorkingDir, activeTab === "details"); - const branchName = gitState?.currentBranch ?? "Detached HEAD"; - const dirtyLabel = gitState - ? formatDirtyFileCount(gitState.dirtyFileCount) - : null; const gitErrorMessage = error instanceof Error ? error.message : "Unable to read git status."; @@ -85,9 +69,9 @@ export function ContextPanel({ setActiveTab(value as ContextPanelTab)} - className="h-full" + className="flex h-full min-w-0 flex-1 flex-col" > -
+
Details @@ -98,148 +82,116 @@ export function ContextPanel({
- -
- +
+ } + action={ + + } > - } - > -
-
-
- {projectName ? ( -
- - - {projectName} +
+ {projectName ? ( +
+ + + {projectName} + +
+ ) : ( +

No project assigned.

+ )} +

+ {projectWorkingDir ?? "Folder not set"} +

+ + {!projectWorkingDir ? null : isLoading && !gitState ? ( +
+ + Loading git status… +
+ ) : error ? ( +

{gitErrorMessage}

+ ) : gitState?.isGitRepo ? ( +
+ {gitState.worktrees.map((wt) => ( +
+
+ + + {wt.branch ?? "detached"}
- ) : ( -

No project assigned.

- )} -

- {projectWorkingDir - ? `Folder: ${projectWorkingDir}` - : "Folder not set"} -

-
- - + {wt.isMain ? ( + + Main + + ) : null} +
+ ))}
+ ) : ( +

Not a git repository.

+ )} +
+ - {!projectWorkingDir ? null : isLoading && !gitState ? ( -
- - Loading git status... -
- ) : error ? ( -

{gitErrorMessage}

- ) : gitState?.isGitRepo ? ( -
-
-
- - {branchName} -
- - {gitState.isWorktree ? "Worktree" : "Main repo"} - -
+ }> +

No changes

+
-
- {dirtyLabel} - 0 ? "secondary" : "outline" - } - > - {gitState.dirtyFileCount > 0 ? "Changed" : "Clean"} - -
+ } + > +

No servers configured

+
- {gitState.worktrees.length > 1 ? ( -

{`${gitState.worktrees.length} worktrees detected`}

- ) : null} -
- ) : ( -

Not a git repository.

- )} -
- - - } - > -

- Not wired yet in goose2: running/background process state and - stop actions. -

-
- - } - > -

- Not wired yet in goose2: git file changes and diff counts. -

-
- - } - > -

- Not wired yet in goose2: configured MCP server discovery and - status. -

-
- + } + > +

No active processes

+
- -
-
-
- - Files for this session -
-

- Not wired yet in goose2: artifact list and file opening behavior. + +

+ } + > +

+ No files for this session yet.

-
+
diff --git a/ui/goose2/src/features/chat/ui/__tests__/ContextPanel.test.tsx b/ui/goose2/src/features/chat/ui/__tests__/ContextPanel.test.tsx index 76410d6add..92304763dc 100644 --- a/ui/goose2/src/features/chat/ui/__tests__/ContextPanel.test.tsx +++ b/ui/goose2/src/features/chat/ui/__tests__/ContextPanel.test.tsx @@ -55,17 +55,14 @@ describe("ContextPanel", () => { expect(screen.getByText("Workspace")).toBeInTheDocument(); expect(screen.getByText("Desktop UX")).toBeInTheDocument(); expect(screen.getByText("main")).toBeInTheDocument(); - expect(screen.getByText("3 files changed")).toBeInTheDocument(); - expect(screen.getByText("Main repo")).toBeInTheDocument(); - expect(screen.getByText("2 worktrees detected")).toBeInTheDocument(); + expect(screen.getByText("feat/context-panel")).toBeInTheDocument(); + expect(screen.getByText("Main")).toBeInTheDocument(); await user.click(screen.getByRole("tab", { name: /files/i })); - expect(screen.getByText("Files for this session")).toBeInTheDocument(); + expect(screen.getByText("Session Files")).toBeInTheDocument(); expect( - screen.getByText( - /Not wired yet in goose2: artifact list and file opening behavior/i, - ), + screen.getByText("No files for this session yet."), ).toBeInTheDocument(); });