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)}