diff --git a/ui/goose2/scripts/check-file-sizes.mjs b/ui/goose2/scripts/check-file-sizes.mjs index 397f47e0..ed74d763 100644 --- a/ui/goose2/scripts/check-file-sizes.mjs +++ b/ui/goose2/scripts/check-file-sizes.mjs @@ -11,7 +11,7 @@ const EXCEPTIONS = { "Drag-and-drop handlers for session-to-project moves and project reorder, plus activeProjectId highlight.", }, "src/features/chat/ui/ChatView.tsx": { - limit: 535, + limit: 560, justification: "ACP prewarm guards, project-aware working dir selection, working context sync, and chat bootstrapping still live together here.", }, diff --git a/ui/goose2/src-tauri/src/commands/mod.rs b/ui/goose2/src-tauri/src/commands/mod.rs index 4611b7e6..7401328c 100644 --- a/ui/goose2/src-tauri/src/commands/mod.rs +++ b/ui/goose2/src-tauri/src/commands/mod.rs @@ -7,6 +7,7 @@ pub mod extensions; pub mod git; pub mod git_changes; pub mod model_setup; +pub mod path_resolver; pub mod projects; pub mod skills; pub mod system; diff --git a/ui/goose2/src-tauri/src/commands/path_resolver.rs b/ui/goose2/src-tauri/src/commands/path_resolver.rs new file mode 100644 index 00000000..3efc39f6 --- /dev/null +++ b/ui/goose2/src-tauri/src/commands/path_resolver.rs @@ -0,0 +1,112 @@ +use std::path::PathBuf; + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolvePathRequest { + pub parts: Vec, +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolvePathResponse { + pub path: String, +} + +fn trim_part(part: &str) -> Option<&str> { + let trimmed = part.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } +} + +fn expand_home_prefix(part: &str) -> Option { + let home = dirs::home_dir()?; + match part { + "~" => Some(home), + _ => part + .strip_prefix("~/") + .or_else(|| part.strip_prefix("~\\")) + .map(|relative| home.join(relative)), + } +} + +fn resolve_path_parts(parts: Vec) -> Result { + let mut normalized_parts = parts.iter().filter_map(|part| trim_part(part)).peekable(); + + let first = normalized_parts + .next() + .ok_or_else(|| "Path parts must include at least one non-empty segment".to_string())?; + let mut path = expand_home_prefix(first).unwrap_or_else(|| PathBuf::from(first)); + + for part in normalized_parts { + path.push(part); + } + + Ok(path.to_string_lossy().into_owned()) +} + +#[tauri::command] +pub fn resolve_path(request: ResolvePathRequest) -> Result { + Ok(ResolvePathResponse { + path: resolve_path_parts(request.parts)?, + }) +} + +#[cfg(test)] +mod tests { + use super::resolve_path_parts; + + #[test] + fn joins_absolute_path_and_subpath() { + assert_eq!( + resolve_path_parts(vec!["/tmp/project".to_string(), "artifacts".to_string()]), + Ok("/tmp/project/artifacts".to_string()) + ); + } + + #[test] + fn ignores_empty_parts() { + assert_eq!( + resolve_path_parts(vec![" ".to_string(), "/tmp/project".to_string()]), + Ok("/tmp/project".to_string()) + ); + } + + #[test] + fn expands_home_segments() { + let Some(home) = dirs::home_dir() else { + return; + }; + + assert_eq!( + resolve_path_parts(vec![ + "~".to_string(), + ".goose".to_string(), + "artifacts".to_string() + ]), + Ok(home + .join(".goose") + .join("artifacts") + .to_string_lossy() + .into_owned()) + ); + assert_eq!( + resolve_path_parts(vec!["~/artifacts".to_string()]), + Ok(home.join("artifacts").to_string_lossy().into_owned()) + ); + assert_eq!( + resolve_path_parts(vec!["~\\artifacts".to_string()]), + Ok(home.join("artifacts").to_string_lossy().into_owned()) + ); + } + + #[test] + fn errors_when_no_non_empty_parts_exist() { + assert_eq!( + resolve_path_parts(vec![" ".to_string(), "".to_string()]), + Err("Path parts must include at least one non-empty segment".to_string()) + ); + } +} diff --git a/ui/goose2/src-tauri/src/lib.rs b/ui/goose2/src-tauri/src/lib.rs index 3b46d768..ff900e01 100644 --- a/ui/goose2/src-tauri/src/lib.rs +++ b/ui/goose2/src-tauri/src/lib.rs @@ -84,6 +84,7 @@ pub fn run() { commands::agent_setup::check_agent_auth, commands::agent_setup::install_agent, commands::agent_setup::authenticate_agent, + commands::path_resolver::resolve_path, commands::system::get_home_dir, commands::system::save_exported_session_file, commands::system::path_exists, diff --git a/ui/goose2/src/app/AppShell.tsx b/ui/goose2/src/app/AppShell.tsx index 6c316b98..f2564195 100644 --- a/ui/goose2/src/app/AppShell.tsx +++ b/ui/goose2/src/app/AppShell.tsx @@ -21,8 +21,7 @@ import { clearReplayBuffer, getAndDeleteReplayBuffer, } from "@/features/chat/hooks/replayBuffer"; -import { getHomeDir } from "@/shared/api/system"; -import { resolveEffectiveWorkingDir } from "@/features/projects/lib/chatProjectContext"; +import { resolveSessionCwd } from "@/features/projects/lib/sessionCwdSelection"; export type AppView = | "home" @@ -93,11 +92,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) { .projects.find((candidate) => candidate.id === session.projectId) ?? null) : null; - const workingDir = - resolveEffectiveWorkingDir(project) ?? - (!project - ? resolveEffectiveWorkingDir(null, await getHomeDir()) - : undefined); + const workingDir = await resolveSessionCwd(project); await acpLoadSession(sessionId, gooseSessionId, workingDir); useChatStore.getState().setSessionLoading(sessionId, false); const buffer = getAndDeleteReplayBuffer(sessionId); @@ -315,19 +310,15 @@ export function AppShell({ children }: { children?: React.ReactNode }) { : (useProjectStore .getState() .projects.find((project) => project.id === projectId) ?? null); - const nextWorkingDir = - resolveEffectiveWorkingDir(nextProject) ?? - (nextProject == null - ? resolveEffectiveWorkingDir(null, await getHomeDir()) - : undefined); - if (!nextWorkingDir) { + const workingDir = await resolveSessionCwd(nextProject); + if (!workingDir) { return; } await acpPrepareSession( sessionId, session.providerId ?? agentStore.selectedProvider ?? "goose", + workingDir, { - workingDir: nextWorkingDir, personaId: session.personaId, }, ); diff --git a/ui/goose2/src/features/chat/hooks/__tests__/useChat.attachments.test.ts b/ui/goose2/src/features/chat/hooks/__tests__/useChat.attachments.test.ts index da14807a..2cf709ef 100644 --- a/ui/goose2/src/features/chat/hooks/__tests__/useChat.attachments.test.ts +++ b/ui/goose2/src/features/chat/hooks/__tests__/useChat.attachments.test.ts @@ -32,7 +32,7 @@ describe("useChat attachments", () => { activeSessionId: null, isLoading: false, contextPanelOpenBySession: {}, - activeWorkingContextBySession: {}, + activeWorkspaceBySession: {}, modelsBySession: {}, modelCacheByProvider: {}, }); diff --git a/ui/goose2/src/features/chat/hooks/__tests__/useChat.test.ts b/ui/goose2/src/features/chat/hooks/__tests__/useChat.test.ts index 546bcfc7..4c9509a5 100644 --- a/ui/goose2/src/features/chat/hooks/__tests__/useChat.test.ts +++ b/ui/goose2/src/features/chat/hooks/__tests__/useChat.test.ts @@ -65,7 +65,7 @@ describe("useChat", () => { activeSessionId: null, isLoading: false, contextPanelOpenBySession: {}, - activeWorkingContextBySession: {}, + activeWorkspaceBySession: {}, modelsBySession: {}, modelCacheByProvider: {}, }); @@ -353,16 +353,22 @@ describe("useChat", () => { ], }); - const { result } = renderHook(() => useChat("session-1", "openai")); + const { result } = renderHook(() => + useChat("session-1", "openai", undefined, undefined, async () => "/tmp"), + ); await act(async () => { await result.current.sendMessage("Hello"); }); - expect(mockAcpPrepareSession).toHaveBeenCalledWith("session-1", "openai", { - workingDir: undefined, - personaId: undefined, - }); + expect(mockAcpPrepareSession).toHaveBeenCalledWith( + "session-1", + "openai", + "/tmp", + { + personaId: undefined, + }, + ); expect(mockAcpSetModel).toHaveBeenCalledWith("session-1", "gpt-4.1"); expect(mockAcpSendMessage).toHaveBeenCalledWith("session-1", "Hello", { systemPrompt: undefined, diff --git a/ui/goose2/src/features/chat/hooks/useChat.ts b/ui/goose2/src/features/chat/hooks/useChat.ts index 1150f107..6f7868d6 100644 --- a/ui/goose2/src/features/chat/hooks/useChat.ts +++ b/ui/goose2/src/features/chat/hooks/useChat.ts @@ -83,7 +83,7 @@ export function useChat( providerOverride?: string, systemPromptOverride?: string, personaInfo?: { id: string; name: string }, - workingDirOverride?: string, + getWorkingDir?: () => Promise, ) { const store = useChatStore(); const abortRef = useRef(null); @@ -218,8 +218,11 @@ export function useChat( try { if (wasDraft || selectedModelId) { - await acpPrepareSession(sessionId, providerId, { - workingDir: workingDirOverride, + const workingDir = await getWorkingDir?.(); + if (!workingDir) { + throw new Error("Missing session working directory"); + } + await acpPrepareSession(sessionId, providerId, workingDir, { personaId: effectivePersonaInfo?.id, }); if (selectedModelId) { @@ -299,7 +302,7 @@ export function useChat( providerOverride, systemPromptOverride, resolvePersonaInfo, - workingDirOverride, + getWorkingDir, ], ); diff --git a/ui/goose2/src/features/chat/stores/__tests__/chatSessionStore.test.ts b/ui/goose2/src/features/chat/stores/__tests__/chatSessionStore.test.ts index 202909ad..497869fa 100644 --- a/ui/goose2/src/features/chat/stores/__tests__/chatSessionStore.test.ts +++ b/ui/goose2/src/features/chat/stores/__tests__/chatSessionStore.test.ts @@ -20,7 +20,7 @@ function resetStore() { activeSessionId: null, isLoading: false, contextPanelOpenBySession: {}, - activeWorkingContextBySession: {}, + activeWorkspaceBySession: {}, modelsBySession: {}, modelCacheByProvider: {}, }); diff --git a/ui/goose2/src/features/chat/stores/chatSessionStore.ts b/ui/goose2/src/features/chat/stores/chatSessionStore.ts index 8915037b..a1c30b94 100644 --- a/ui/goose2/src/features/chat/stores/chatSessionStore.ts +++ b/ui/goose2/src/features/chat/stores/chatSessionStore.ts @@ -37,7 +37,7 @@ export interface ChatSession { userSetName?: boolean; } -export interface WorkingContext { +export interface ActiveWorkspace { path: string; branch: string | null; } @@ -47,7 +47,7 @@ interface ChatSessionStoreState { activeSessionId: string | null; isLoading: boolean; contextPanelOpenBySession: Record; - activeWorkingContextBySession: Record; + activeWorkspaceBySession: Record; modelsBySession: Record; modelCacheByProvider: Record; } @@ -83,8 +83,8 @@ interface ChatSessionStoreActions { setActiveSession: (sessionId: string | null) => void; setContextPanelOpen: (sessionId: string, open: boolean) => void; - setActiveWorkingContext: (sessionId: string, context: WorkingContext) => void; - clearActiveWorkingContext: (sessionId: string) => void; + setActiveWorkspace: (sessionId: string, context: ActiveWorkspace) => void; + clearActiveWorkspace: (sessionId: string) => void; setSessionModels: (sessionId: string, models: ModelOption[]) => void; switchSessionProvider: ( sessionId: string, @@ -300,7 +300,7 @@ export const useChatSessionStore = create((set, get) => ({ activeSessionId: null, isLoading: false, contextPanelOpenBySession: {}, - activeWorkingContextBySession: {}, + activeWorkspaceBySession: {}, modelsBySession: {}, modelCacheByProvider: loadModelCache(), @@ -346,7 +346,7 @@ export const useChatSessionStore = create((set, get) => ({ const { [id]: _ignoredPanelState, ...remainingPanelState } = get().contextPanelOpenBySession; const { [id]: _ignoredContext, ...remainingContextState } = - get().activeWorkingContextBySession; + get().activeWorkspaceBySession; const remainingModels = { ...get().modelsBySession }; delete remainingModels[id]; set((state) => ({ @@ -354,7 +354,7 @@ export const useChatSessionStore = create((set, get) => ({ activeSessionId: state.activeSessionId === id ? null : state.activeSessionId, contextPanelOpenBySession: remainingPanelState, - activeWorkingContextBySession: remainingContextState, + activeWorkspaceBySession: remainingContextState, modelsBySession: remainingModels, })); removeDraftSessionRecord(id); @@ -544,19 +544,19 @@ export const useChatSessionStore = create((set, get) => ({ })); }, - setActiveWorkingContext: (sessionId, context) => { + setActiveWorkspace: (sessionId, context) => { set((state) => ({ - activeWorkingContextBySession: { - ...state.activeWorkingContextBySession, + activeWorkspaceBySession: { + ...state.activeWorkspaceBySession, [sessionId]: context, }, })); }, - clearActiveWorkingContext: (sessionId) => { + clearActiveWorkspace: (sessionId) => { set((state) => { - const { [sessionId]: _, ...rest } = state.activeWorkingContextBySession; - return { activeWorkingContextBySession: rest }; + const { [sessionId]: _, ...rest } = state.activeWorkspaceBySession; + return { activeWorkspaceBySession: rest }; }); }, diff --git a/ui/goose2/src/features/chat/ui/ChatView.tsx b/ui/goose2/src/features/chat/ui/ChatView.tsx index 0478343d..495f757b 100644 --- a/ui/goose2/src/features/chat/ui/ChatView.tsx +++ b/ui/goose2/src/features/chat/ui/ChatView.tsx @@ -17,11 +17,11 @@ import { acpPrepareSession, acpSetModel } from "@/shared/api/acp"; import { buildProjectSystemPrompt, composeSystemPrompt, - defaultArtifactsDir, + defaultGlobalArtifactRoot, getProjectArtifactRoots, - resolveProjectWorkingDir, + resolveProjectDefaultArtifactRoot, } from "@/features/projects/lib/chatProjectContext"; -import { getHomeDir } from "@/shared/api/system"; +import { resolveSessionCwd } from "@/features/projects/lib/sessionCwdSelection"; import { ArtifactPolicyProvider } from "../hooks/ArtifactPolicyContext"; import type { ModelOption } from "../types"; import { ChatContextPanel } from "./ChatContextPanel"; @@ -55,11 +55,11 @@ export function ChatView({ (s) => s.contextPanelOpenBySession[activeSessionId] ?? false, ); const setContextPanelOpen = useChatSessionStore((s) => s.setContextPanelOpen); - const activeWorkingContext = useChatSessionStore( - (s) => s.activeWorkingContextBySession[activeSessionId], + const activeWorkspace = useChatSessionStore( + (s) => s.activeWorkspaceBySession[activeSessionId], ); - const clearActiveWorkingContext = useChatSessionStore( - (s) => s.clearActiveWorkingContext, + const clearActiveWorkspace = useChatSessionStore( + (s) => s.clearActiveWorkspace, ); const { @@ -85,7 +85,7 @@ export function ChatView({ ? s.projects.find((candidate) => candidate.id === session.projectId) : undefined, ); - const [homeArtifactsRoot, setHomeArtifactsRoot] = useState( + const [globalArtifactRoot, setGlobalArtifactRoot] = useState( null, ); const project = storedProject ?? null; @@ -115,36 +115,30 @@ export function ChatView({ () => getProjectArtifactRoots(project), [project], ); - const resolvedProjectWorkingDir = useMemo( - () => resolveProjectWorkingDir(project), + const projectDefaultArtifactRoot = useMemo( + () => resolveProjectDefaultArtifactRoot(project), [project], ); const projectMetadataPending = Boolean( - session?.projectId && !resolvedProjectWorkingDir && projectsLoading, + session?.projectId && !projectDefaultArtifactRoot && projectsLoading, ); - const defaultWorkingDir = resolvedProjectWorkingDir - ? resolvedProjectWorkingDir - : !session?.projectId - ? (homeArtifactsRoot ?? undefined) - : undefined; - const effectiveWorkingDir = activeWorkingContext?.path ?? defaultWorkingDir; const allowedArtifactRoots = useMemo(() => { const roots = [ ...projectArtifactRoots.map((path) => path.trim()).filter(Boolean), ]; - if (homeArtifactsRoot) { - roots.push(homeArtifactsRoot); + if (globalArtifactRoot) { + roots.push(globalArtifactRoot); } return [...new Set(roots)]; - }, [homeArtifactsRoot, projectArtifactRoots]); + }, [globalArtifactRoot, projectArtifactRoots]); const projectSystemPrompt = useMemo( () => buildProjectSystemPrompt(project), [project], ); const workingContextPrompt = useMemo(() => { - if (!activeWorkingContext?.branch) return undefined; - return `\nActive branch: ${activeWorkingContext.branch}\nWorking directory: ${activeWorkingContext.path}\n`; - }, [activeWorkingContext?.branch, activeWorkingContext?.path]); + if (!activeWorkspace?.branch) return undefined; + return `\nActive branch: ${activeWorkspace.branch}\nWorking directory: ${activeWorkspace.path}\n`; + }, [activeWorkspace?.branch, activeWorkspace?.path]); const effectiveSystemPrompt = useMemo( () => @@ -158,14 +152,14 @@ export function ChatView({ useEffect(() => { let cancelled = false; - getHomeDir() - .then((homeDir) => { + defaultGlobalArtifactRoot() + .then((artifactRoot) => { if (cancelled) return; - setHomeArtifactsRoot(defaultArtifactsDir(homeDir)); + setGlobalArtifactRoot(artifactRoot); }) .catch(() => { if (cancelled) return; - setHomeArtifactsRoot(null); + setGlobalArtifactRoot(null); }); return () => { cancelled = true; @@ -177,32 +171,41 @@ export function ChatView({ const prevProjectId = prevProjectIdRef.current; prevProjectIdRef.current = session?.projectId; if (prevProjectId !== undefined && prevProjectId !== session?.projectId) { - clearActiveWorkingContext(activeSessionId); + clearActiveWorkspace(activeSessionId); } - }, [session?.projectId, activeSessionId, clearActiveWorkingContext]); + }, [session?.projectId, activeSessionId, clearActiveWorkspace]); - const prevContextRef = useRef(activeWorkingContext); + const prevWorkspaceRef = useRef(activeWorkspace); useEffect(() => { - const prev = prevContextRef.current; + const prev = prevWorkspaceRef.current; if ( - !activeWorkingContext || + !activeWorkspace || !selectedProvider || session?.draft || - activeWorkingContext === prev + activeWorkspace === prev ) { return; } - prevContextRef.current = activeWorkingContext; - if (prev && prev.path === activeWorkingContext.path) return; - void acpPrepareSession(activeSessionId, selectedProvider, { - workingDir: activeWorkingContext.path, - personaId: selectedPersonaId ?? undefined, - }).catch((error) => { + prevWorkspaceRef.current = activeWorkspace; + if (prev && prev.path === activeWorkspace.path) return; + + async function prepareWorkspaceSession() { + const workingDir = await resolveSessionCwd(project, activeWorkspace.path); + if (!workingDir) { + return; + } + await acpPrepareSession(activeSessionId, selectedProvider, workingDir, { + personaId: selectedPersonaId ?? undefined, + }); + } + + void prepareWorkspaceSession().catch((error) => { console.error("Failed to prepare ACP session:", error); }); }, [ - activeWorkingContext, + activeWorkspace, activeSessionId, + project, selectedProvider, selectedPersonaId, session?.draft, @@ -230,19 +233,32 @@ export function ChatView({ .getState() .projects.find((candidate) => candidate.id === projectId) ?? null); - const nextWorkingDir = - resolveProjectWorkingDir(nextProject) ?? - (projectId == null ? (homeArtifactsRoot ?? undefined) : undefined); useChatSessionStore .getState() .updateSession(activeSessionId, { projectId }); - if (!session?.draft && selectedProvider && nextWorkingDir) { - void acpPrepareSession(activeSessionId, selectedProvider, { - workingDir: nextWorkingDir, - personaId: selectedPersonaId ?? undefined, - }).catch((error) => { + if (!session?.draft && selectedProvider) { + async function updateProjectSessionCwd() { + const workingDir = await resolveSessionCwd( + nextProject, + activeWorkspace?.path, + ); + if (!workingDir) { + return; + } + + await acpPrepareSession( + activeSessionId, + selectedProvider, + workingDir, + { + personaId: selectedPersonaId ?? undefined, + }, + ); + } + + void updateProjectSessionCwd().catch((error) => { console.error( "Failed to update ACP session working directory:", error, @@ -252,7 +268,7 @@ export function ChatView({ }, [ activeSessionId, - homeArtifactsRoot, + activeWorkspace?.path, selectedPersonaId, selectedProvider, session?.draft, @@ -331,6 +347,10 @@ export function ChatView({ const personaInfo = selectedPersona ? { id: selectedPersona.id, name: selectedPersona.displayName } : undefined; + const resolveCurrentSessionCwd = useCallback( + () => resolveSessionCwd(project, activeWorkspace?.path), + [project, activeWorkspace?.path], + ); const { messages, chatState, @@ -343,7 +363,7 @@ export function ChatView({ selectedProvider, effectiveSystemPrompt, personaInfo, - effectiveWorkingDir, + resolveCurrentSessionCwd, ); const isLoadingHistory = useChatStore( (s) => diff --git a/ui/goose2/src/features/chat/ui/ContextPanel.tsx b/ui/goose2/src/features/chat/ui/ContextPanel.tsx index 69e804d0..960a8ef9 100644 --- a/ui/goose2/src/features/chat/ui/ContextPanel.tsx +++ b/ui/goose2/src/features/chat/ui/ContextPanel.tsx @@ -15,7 +15,7 @@ import { import type { CreatedWorktree } from "@/shared/types/git"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs"; import { useChatSessionStore } from "../stores/chatSessionStore"; -import type { WorkingContext } from "../stores/chatSessionStore"; +import type { ActiveWorkspace } from "../stores/chatSessionStore"; import { WorkspaceWidget } from "./widgets/WorkspaceWidget"; import { ChangesWidget } from "./widgets/ChangesWidget"; import { ArtifactsWidget } from "./widgets/ArtifactsWidget"; @@ -39,35 +39,33 @@ export function ContextPanel({ }: ContextPanelProps) { const { t } = useTranslation("chat"); const [activeTab, setActiveTab] = useState("details"); - const primaryWorkingDir = projectWorkingDirs[0] ?? null; + const primaryWorkspaceRoot = projectWorkingDirs[0] ?? null; const activeContext = useChatSessionStore( - (s) => s.activeWorkingContextBySession[sessionId], - ); - const setActiveWorkingContext = useChatSessionStore( - (s) => s.setActiveWorkingContext, + (s) => s.activeWorkspaceBySession[sessionId], ); + const setActiveWorkspace = useChatSessionStore((s) => s.setActiveWorkspace); - const gitQueryPath = activeContext?.path ?? primaryWorkingDir; + const gitTargetPath = activeContext?.path ?? primaryWorkspaceRoot; const { data: gitState, error, isLoading, isFetching, refetch, - } = useGitState(gitQueryPath, activeTab === "details"); + } = useGitState(gitTargetPath, activeTab === "details"); const { data: changedFiles, isLoading: isFilesLoading, refetch: refetchFiles, - } = useChangedFiles(gitQueryPath, activeTab === "details"); + } = useChangedFiles(gitTargetPath, activeTab === "details"); const handleContextChange = useCallback( - (context: WorkingContext) => { - setActiveWorkingContext(sessionId, context); + (context: ActiveWorkspace) => { + setActiveWorkspace(sessionId, context); }, - [sessionId, setActiveWorkingContext], + [sessionId, setActiveWorkspace], ); const refetchAll = useCallback(async () => { @@ -149,11 +147,11 @@ export function ContextPanel({ const handleOpenChangedFile = useCallback( (filePath: string) => { - if (!gitQueryPath) return; - const fullPath = `${gitQueryPath}/${filePath}`; + if (!gitTargetPath) return; + const fullPath = `${gitTargetPath}/${filePath}`; void openPath(fullPath); }, - [gitQueryPath], + [gitTargetPath], ); const handleRefresh = useCallback(() => { @@ -202,7 +200,7 @@ export function ContextPanel({ files={changedFiles} isLoading={isFilesLoading} currentBranch={gitState?.currentBranch ?? null} - repoPath={gitQueryPath ?? ""} + repoPath={gitTargetPath ?? ""} onOpenFile={handleOpenChangedFile} /> diff --git a/ui/goose2/src/features/chat/ui/widgets/WorkingContextPicker.tsx b/ui/goose2/src/features/chat/ui/widgets/WorkingContextPicker.tsx index 1fc4cf86..87c1787c 100644 --- a/ui/goose2/src/features/chat/ui/widgets/WorkingContextPicker.tsx +++ b/ui/goose2/src/features/chat/ui/widgets/WorkingContextPicker.tsx @@ -20,13 +20,13 @@ import { import { buttonVariants } from "@/shared/ui/button"; import { cn } from "@/shared/lib/cn"; import type { GitState } from "@/shared/types/git"; -import type { WorkingContext } from "../../stores/chatSessionStore"; +import type { ActiveWorkspace } from "../../stores/chatSessionStore"; interface WorkingContextPickerProps { currentProjectPath: string | null; gitState: GitState | undefined; - activeContext: WorkingContext | undefined; - onSelect: (context: WorkingContext) => void; + activeContext: ActiveWorkspace | undefined; + onSelect: (context: ActiveWorkspace) => void; onSwitchBranch: (path: string, branch: string) => Promise; onStashAndSwitch: (path: string, branch: string) => Promise; } @@ -63,7 +63,7 @@ export function WorkingContextPicker({ }: WorkingContextPickerProps) { const { t } = useTranslation("chat"); const [open, setOpen] = useState(false); - const [pendingSwitch, setPendingSwitch] = useState( + const [pendingSwitch, setPendingSwitch] = useState( null, ); const [switching, setSwitching] = useState(false); diff --git a/ui/goose2/src/features/chat/ui/widgets/WorkspaceActionsMenu.tsx b/ui/goose2/src/features/chat/ui/widgets/WorkspaceActionsMenu.tsx index 184554a9..0aff95e6 100644 --- a/ui/goose2/src/features/chat/ui/widgets/WorkspaceActionsMenu.tsx +++ b/ui/goose2/src/features/chat/ui/widgets/WorkspaceActionsMenu.tsx @@ -5,7 +5,7 @@ import type { CreatedWorktree, GitState } from "@/shared/types/git"; import { Button } from "@/shared/ui/button"; import { SplitButton } from "@/shared/ui/split-button"; import { Spinner } from "@/shared/ui/spinner"; -import type { WorkingContext } from "../../stores/chatSessionStore"; +import type { ActiveWorkspace } from "../../stores/chatSessionStore"; import { formatErrorMessage } from "./formatError"; import { WorkspaceCreateDialog, @@ -15,9 +15,9 @@ import { interface WorkspaceActionsMenuProps { currentProjectPath: string; gitState: GitState; - activeContext: WorkingContext | undefined; + activeContext: ActiveWorkspace | undefined; disabled?: boolean; - onContextChange: (context: WorkingContext) => void; + onContextChange: (context: ActiveWorkspace) => void; onFetch: (path: string) => Promise; onPull: (path: string) => Promise; onCreateBranch: ( diff --git a/ui/goose2/src/features/chat/ui/widgets/WorkspaceCreateDialog.tsx b/ui/goose2/src/features/chat/ui/widgets/WorkspaceCreateDialog.tsx index 78bd26e4..5d8963db 100644 --- a/ui/goose2/src/features/chat/ui/widgets/WorkspaceCreateDialog.tsx +++ b/ui/goose2/src/features/chat/ui/widgets/WorkspaceCreateDialog.tsx @@ -21,7 +21,7 @@ import { SelectTrigger, SelectValue, } from "@/shared/ui/select"; -import type { WorkingContext } from "../../stores/chatSessionStore"; +import type { ActiveWorkspace } from "../../stores/chatSessionStore"; import { formatErrorMessage } from "./formatError"; import { shortenPath } from "./WorkingContextPicker"; @@ -35,7 +35,7 @@ interface WorkspaceCreateDialogProps { currentPath: string; activeBranch: string | null; onClose: () => void; - onContextChange: (context: WorkingContext) => void; + onContextChange: (context: ActiveWorkspace) => void; onCreateBranch: ( path: string, name: string, diff --git a/ui/goose2/src/features/chat/ui/widgets/WorkspaceWidget.tsx b/ui/goose2/src/features/chat/ui/widgets/WorkspaceWidget.tsx index 695be9f4..752a925a 100644 --- a/ui/goose2/src/features/chat/ui/widgets/WorkspaceWidget.tsx +++ b/ui/goose2/src/features/chat/ui/widgets/WorkspaceWidget.tsx @@ -3,7 +3,7 @@ import { IconFolder, IconGitBranch, IconRefresh } from "@tabler/icons-react"; import type { CreatedWorktree, GitState } from "@/shared/types/git"; import { Button } from "@/shared/ui/button"; import { Spinner } from "@/shared/ui/spinner"; -import type { WorkingContext } from "../../stores/chatSessionStore"; +import type { ActiveWorkspace } from "../../stores/chatSessionStore"; import { Widget } from "./Widget"; import { WorkspaceActionsMenu } from "./WorkspaceActionsMenu"; import { WorkingContextPicker, shortenPath } from "./WorkingContextPicker"; @@ -16,8 +16,8 @@ interface WorkspaceWidgetProps { isLoading: boolean; isFetching: boolean; error: Error | null; - activeContext: WorkingContext | undefined; - onContextChange: (context: WorkingContext) => void; + activeContext: ActiveWorkspace | undefined; + onContextChange: (context: ActiveWorkspace) => void; onSwitchBranch: (path: string, branch: string) => Promise; onStashAndSwitch: (path: string, branch: string) => Promise; onInitRepo: (path: string) => Promise; @@ -58,7 +58,7 @@ export function WorkspaceWidget({ onRefresh, }: WorkspaceWidgetProps) { const { t } = useTranslation("chat"); - const primaryWorkingDir = projectWorkingDirs[0] ?? null; + const primaryWorkspaceRoot = projectWorkingDirs[0] ?? null; const gitErrorMessage = error instanceof Error ? error.message : t("contextPanel.errors.gitRead"); @@ -73,7 +73,7 @@ export function WorkspaceWidget({ variant="ghost" size="icon-xs" onClick={onRefresh} - disabled={!primaryWorkingDir || isFetching} + disabled={!primaryWorkspaceRoot || isFetching} className="rounded-md" aria-label={t("contextPanel.actions.refreshGitStatus")} title={t("contextPanel.actions.refreshGitStatus")} @@ -103,7 +103,7 @@ export function WorkspaceWidget({

)} - {!primaryWorkingDir ? ( + {!primaryWorkspaceRoot ? (

{t("contextPanel.empty.folderNotSet")}

) : isLoading && !gitState ? (
@@ -115,7 +115,7 @@ export function WorkspaceWidget({ ) : gitState?.isGitRepo ? (

- {shortenPath(primaryWorkingDir)} + {shortenPath(primaryWorkspaceRoot)}