goose2 session working dir (#8624)

This commit is contained in:
Lifei Zhou
2026-04-18 04:50:37 +10:00
committed by GitHub
parent d52cde3fb9
commit 2533765359
24 changed files with 448 additions and 257 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ const EXCEPTIONS = {
"Drag-and-drop handlers for session-to-project moves and project reorder, plus activeProjectId highlight.", "Drag-and-drop handlers for session-to-project moves and project reorder, plus activeProjectId highlight.",
}, },
"src/features/chat/ui/ChatView.tsx": { "src/features/chat/ui/ChatView.tsx": {
limit: 535, limit: 560,
justification: justification:
"ACP prewarm guards, project-aware working dir selection, working context sync, and chat bootstrapping still live together here.", "ACP prewarm guards, project-aware working dir selection, working context sync, and chat bootstrapping still live together here.",
}, },
+1
View File
@@ -7,6 +7,7 @@ pub mod extensions;
pub mod git; pub mod git;
pub mod git_changes; pub mod git_changes;
pub mod model_setup; pub mod model_setup;
pub mod path_resolver;
pub mod projects; pub mod projects;
pub mod skills; pub mod skills;
pub mod system; pub mod system;
@@ -0,0 +1,112 @@
use std::path::PathBuf;
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolvePathRequest {
pub parts: Vec<String>,
}
#[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<PathBuf> {
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<String>) -> Result<String, String> {
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<ResolvePathResponse, String> {
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())
);
}
}
+1
View File
@@ -84,6 +84,7 @@ pub fn run() {
commands::agent_setup::check_agent_auth, commands::agent_setup::check_agent_auth,
commands::agent_setup::install_agent, commands::agent_setup::install_agent,
commands::agent_setup::authenticate_agent, commands::agent_setup::authenticate_agent,
commands::path_resolver::resolve_path,
commands::system::get_home_dir, commands::system::get_home_dir,
commands::system::save_exported_session_file, commands::system::save_exported_session_file,
commands::system::path_exists, commands::system::path_exists,
+5 -14
View File
@@ -21,8 +21,7 @@ import {
clearReplayBuffer, clearReplayBuffer,
getAndDeleteReplayBuffer, getAndDeleteReplayBuffer,
} from "@/features/chat/hooks/replayBuffer"; } from "@/features/chat/hooks/replayBuffer";
import { getHomeDir } from "@/shared/api/system"; import { resolveSessionCwd } from "@/features/projects/lib/sessionCwdSelection";
import { resolveEffectiveWorkingDir } from "@/features/projects/lib/chatProjectContext";
export type AppView = export type AppView =
| "home" | "home"
@@ -93,11 +92,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
.projects.find((candidate) => candidate.id === session.projectId) ?? .projects.find((candidate) => candidate.id === session.projectId) ??
null) null)
: null; : null;
const workingDir = const workingDir = await resolveSessionCwd(project);
resolveEffectiveWorkingDir(project) ??
(!project
? resolveEffectiveWorkingDir(null, await getHomeDir())
: undefined);
await acpLoadSession(sessionId, gooseSessionId, workingDir); await acpLoadSession(sessionId, gooseSessionId, workingDir);
useChatStore.getState().setSessionLoading(sessionId, false); useChatStore.getState().setSessionLoading(sessionId, false);
const buffer = getAndDeleteReplayBuffer(sessionId); const buffer = getAndDeleteReplayBuffer(sessionId);
@@ -315,19 +310,15 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
: (useProjectStore : (useProjectStore
.getState() .getState()
.projects.find((project) => project.id === projectId) ?? null); .projects.find((project) => project.id === projectId) ?? null);
const nextWorkingDir = const workingDir = await resolveSessionCwd(nextProject);
resolveEffectiveWorkingDir(nextProject) ?? if (!workingDir) {
(nextProject == null
? resolveEffectiveWorkingDir(null, await getHomeDir())
: undefined);
if (!nextWorkingDir) {
return; return;
} }
await acpPrepareSession( await acpPrepareSession(
sessionId, sessionId,
session.providerId ?? agentStore.selectedProvider ?? "goose", session.providerId ?? agentStore.selectedProvider ?? "goose",
workingDir,
{ {
workingDir: nextWorkingDir,
personaId: session.personaId, personaId: session.personaId,
}, },
); );
@@ -32,7 +32,7 @@ describe("useChat attachments", () => {
activeSessionId: null, activeSessionId: null,
isLoading: false, isLoading: false,
contextPanelOpenBySession: {}, contextPanelOpenBySession: {},
activeWorkingContextBySession: {}, activeWorkspaceBySession: {},
modelsBySession: {}, modelsBySession: {},
modelCacheByProvider: {}, modelCacheByProvider: {},
}); });
@@ -65,7 +65,7 @@ describe("useChat", () => {
activeSessionId: null, activeSessionId: null,
isLoading: false, isLoading: false,
contextPanelOpenBySession: {}, contextPanelOpenBySession: {},
activeWorkingContextBySession: {}, activeWorkspaceBySession: {},
modelsBySession: {}, modelsBySession: {},
modelCacheByProvider: {}, 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 act(async () => {
await result.current.sendMessage("Hello"); await result.current.sendMessage("Hello");
}); });
expect(mockAcpPrepareSession).toHaveBeenCalledWith("session-1", "openai", { expect(mockAcpPrepareSession).toHaveBeenCalledWith(
workingDir: undefined, "session-1",
personaId: undefined, "openai",
}); "/tmp",
{
personaId: undefined,
},
);
expect(mockAcpSetModel).toHaveBeenCalledWith("session-1", "gpt-4.1"); expect(mockAcpSetModel).toHaveBeenCalledWith("session-1", "gpt-4.1");
expect(mockAcpSendMessage).toHaveBeenCalledWith("session-1", "Hello", { expect(mockAcpSendMessage).toHaveBeenCalledWith("session-1", "Hello", {
systemPrompt: undefined, systemPrompt: undefined,
+7 -4
View File
@@ -83,7 +83,7 @@ export function useChat(
providerOverride?: string, providerOverride?: string,
systemPromptOverride?: string, systemPromptOverride?: string,
personaInfo?: { id: string; name: string }, personaInfo?: { id: string; name: string },
workingDirOverride?: string, getWorkingDir?: () => Promise<string | undefined>,
) { ) {
const store = useChatStore(); const store = useChatStore();
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
@@ -218,8 +218,11 @@ export function useChat(
try { try {
if (wasDraft || selectedModelId) { if (wasDraft || selectedModelId) {
await acpPrepareSession(sessionId, providerId, { const workingDir = await getWorkingDir?.();
workingDir: workingDirOverride, if (!workingDir) {
throw new Error("Missing session working directory");
}
await acpPrepareSession(sessionId, providerId, workingDir, {
personaId: effectivePersonaInfo?.id, personaId: effectivePersonaInfo?.id,
}); });
if (selectedModelId) { if (selectedModelId) {
@@ -299,7 +302,7 @@ export function useChat(
providerOverride, providerOverride,
systemPromptOverride, systemPromptOverride,
resolvePersonaInfo, resolvePersonaInfo,
workingDirOverride, getWorkingDir,
], ],
); );
@@ -20,7 +20,7 @@ function resetStore() {
activeSessionId: null, activeSessionId: null,
isLoading: false, isLoading: false,
contextPanelOpenBySession: {}, contextPanelOpenBySession: {},
activeWorkingContextBySession: {}, activeWorkspaceBySession: {},
modelsBySession: {}, modelsBySession: {},
modelCacheByProvider: {}, modelCacheByProvider: {},
}); });
@@ -37,7 +37,7 @@ export interface ChatSession {
userSetName?: boolean; userSetName?: boolean;
} }
export interface WorkingContext { export interface ActiveWorkspace {
path: string; path: string;
branch: string | null; branch: string | null;
} }
@@ -47,7 +47,7 @@ interface ChatSessionStoreState {
activeSessionId: string | null; activeSessionId: string | null;
isLoading: boolean; isLoading: boolean;
contextPanelOpenBySession: Record<string, boolean>; contextPanelOpenBySession: Record<string, boolean>;
activeWorkingContextBySession: Record<string, WorkingContext>; activeWorkspaceBySession: Record<string, ActiveWorkspace>;
modelsBySession: Record<string, ModelOption[]>; modelsBySession: Record<string, ModelOption[]>;
modelCacheByProvider: Record<string, ModelOption[]>; modelCacheByProvider: Record<string, ModelOption[]>;
} }
@@ -83,8 +83,8 @@ interface ChatSessionStoreActions {
setActiveSession: (sessionId: string | null) => void; setActiveSession: (sessionId: string | null) => void;
setContextPanelOpen: (sessionId: string, open: boolean) => void; setContextPanelOpen: (sessionId: string, open: boolean) => void;
setActiveWorkingContext: (sessionId: string, context: WorkingContext) => void; setActiveWorkspace: (sessionId: string, context: ActiveWorkspace) => void;
clearActiveWorkingContext: (sessionId: string) => void; clearActiveWorkspace: (sessionId: string) => void;
setSessionModels: (sessionId: string, models: ModelOption[]) => void; setSessionModels: (sessionId: string, models: ModelOption[]) => void;
switchSessionProvider: ( switchSessionProvider: (
sessionId: string, sessionId: string,
@@ -300,7 +300,7 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
activeSessionId: null, activeSessionId: null,
isLoading: false, isLoading: false,
contextPanelOpenBySession: {}, contextPanelOpenBySession: {},
activeWorkingContextBySession: {}, activeWorkspaceBySession: {},
modelsBySession: {}, modelsBySession: {},
modelCacheByProvider: loadModelCache(), modelCacheByProvider: loadModelCache(),
@@ -346,7 +346,7 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
const { [id]: _ignoredPanelState, ...remainingPanelState } = const { [id]: _ignoredPanelState, ...remainingPanelState } =
get().contextPanelOpenBySession; get().contextPanelOpenBySession;
const { [id]: _ignoredContext, ...remainingContextState } = const { [id]: _ignoredContext, ...remainingContextState } =
get().activeWorkingContextBySession; get().activeWorkspaceBySession;
const remainingModels = { ...get().modelsBySession }; const remainingModels = { ...get().modelsBySession };
delete remainingModels[id]; delete remainingModels[id];
set((state) => ({ set((state) => ({
@@ -354,7 +354,7 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
activeSessionId: activeSessionId:
state.activeSessionId === id ? null : state.activeSessionId, state.activeSessionId === id ? null : state.activeSessionId,
contextPanelOpenBySession: remainingPanelState, contextPanelOpenBySession: remainingPanelState,
activeWorkingContextBySession: remainingContextState, activeWorkspaceBySession: remainingContextState,
modelsBySession: remainingModels, modelsBySession: remainingModels,
})); }));
removeDraftSessionRecord(id); removeDraftSessionRecord(id);
@@ -544,19 +544,19 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
})); }));
}, },
setActiveWorkingContext: (sessionId, context) => { setActiveWorkspace: (sessionId, context) => {
set((state) => ({ set((state) => ({
activeWorkingContextBySession: { activeWorkspaceBySession: {
...state.activeWorkingContextBySession, ...state.activeWorkspaceBySession,
[sessionId]: context, [sessionId]: context,
}, },
})); }));
}, },
clearActiveWorkingContext: (sessionId) => { clearActiveWorkspace: (sessionId) => {
set((state) => { set((state) => {
const { [sessionId]: _, ...rest } = state.activeWorkingContextBySession; const { [sessionId]: _, ...rest } = state.activeWorkspaceBySession;
return { activeWorkingContextBySession: rest }; return { activeWorkspaceBySession: rest };
}); });
}, },
+70 -50
View File
@@ -17,11 +17,11 @@ import { acpPrepareSession, acpSetModel } from "@/shared/api/acp";
import { import {
buildProjectSystemPrompt, buildProjectSystemPrompt,
composeSystemPrompt, composeSystemPrompt,
defaultArtifactsDir, defaultGlobalArtifactRoot,
getProjectArtifactRoots, getProjectArtifactRoots,
resolveProjectWorkingDir, resolveProjectDefaultArtifactRoot,
} from "@/features/projects/lib/chatProjectContext"; } from "@/features/projects/lib/chatProjectContext";
import { getHomeDir } from "@/shared/api/system"; import { resolveSessionCwd } from "@/features/projects/lib/sessionCwdSelection";
import { ArtifactPolicyProvider } from "../hooks/ArtifactPolicyContext"; import { ArtifactPolicyProvider } from "../hooks/ArtifactPolicyContext";
import type { ModelOption } from "../types"; import type { ModelOption } from "../types";
import { ChatContextPanel } from "./ChatContextPanel"; import { ChatContextPanel } from "./ChatContextPanel";
@@ -55,11 +55,11 @@ export function ChatView({
(s) => s.contextPanelOpenBySession[activeSessionId] ?? false, (s) => s.contextPanelOpenBySession[activeSessionId] ?? false,
); );
const setContextPanelOpen = useChatSessionStore((s) => s.setContextPanelOpen); const setContextPanelOpen = useChatSessionStore((s) => s.setContextPanelOpen);
const activeWorkingContext = useChatSessionStore( const activeWorkspace = useChatSessionStore(
(s) => s.activeWorkingContextBySession[activeSessionId], (s) => s.activeWorkspaceBySession[activeSessionId],
); );
const clearActiveWorkingContext = useChatSessionStore( const clearActiveWorkspace = useChatSessionStore(
(s) => s.clearActiveWorkingContext, (s) => s.clearActiveWorkspace,
); );
const { const {
@@ -85,7 +85,7 @@ export function ChatView({
? s.projects.find((candidate) => candidate.id === session.projectId) ? s.projects.find((candidate) => candidate.id === session.projectId)
: undefined, : undefined,
); );
const [homeArtifactsRoot, setHomeArtifactsRoot] = useState<string | null>( const [globalArtifactRoot, setGlobalArtifactRoot] = useState<string | null>(
null, null,
); );
const project = storedProject ?? null; const project = storedProject ?? null;
@@ -115,36 +115,30 @@ export function ChatView({
() => getProjectArtifactRoots(project), () => getProjectArtifactRoots(project),
[project], [project],
); );
const resolvedProjectWorkingDir = useMemo( const projectDefaultArtifactRoot = useMemo(
() => resolveProjectWorkingDir(project), () => resolveProjectDefaultArtifactRoot(project),
[project], [project],
); );
const projectMetadataPending = Boolean( 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 allowedArtifactRoots = useMemo(() => {
const roots = [ const roots = [
...projectArtifactRoots.map((path) => path.trim()).filter(Boolean), ...projectArtifactRoots.map((path) => path.trim()).filter(Boolean),
]; ];
if (homeArtifactsRoot) { if (globalArtifactRoot) {
roots.push(homeArtifactsRoot); roots.push(globalArtifactRoot);
} }
return [...new Set(roots)]; return [...new Set(roots)];
}, [homeArtifactsRoot, projectArtifactRoots]); }, [globalArtifactRoot, projectArtifactRoots]);
const projectSystemPrompt = useMemo( const projectSystemPrompt = useMemo(
() => buildProjectSystemPrompt(project), () => buildProjectSystemPrompt(project),
[project], [project],
); );
const workingContextPrompt = useMemo(() => { const workingContextPrompt = useMemo(() => {
if (!activeWorkingContext?.branch) return undefined; if (!activeWorkspace?.branch) return undefined;
return `<active-working-context>\nActive branch: ${activeWorkingContext.branch}\nWorking directory: ${activeWorkingContext.path}\n</active-working-context>`; return `<active-working-context>\nActive branch: ${activeWorkspace.branch}\nWorking directory: ${activeWorkspace.path}\n</active-working-context>`;
}, [activeWorkingContext?.branch, activeWorkingContext?.path]); }, [activeWorkspace?.branch, activeWorkspace?.path]);
const effectiveSystemPrompt = useMemo( const effectiveSystemPrompt = useMemo(
() => () =>
@@ -158,14 +152,14 @@ export function ChatView({
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
getHomeDir() defaultGlobalArtifactRoot()
.then((homeDir) => { .then((artifactRoot) => {
if (cancelled) return; if (cancelled) return;
setHomeArtifactsRoot(defaultArtifactsDir(homeDir)); setGlobalArtifactRoot(artifactRoot);
}) })
.catch(() => { .catch(() => {
if (cancelled) return; if (cancelled) return;
setHomeArtifactsRoot(null); setGlobalArtifactRoot(null);
}); });
return () => { return () => {
cancelled = true; cancelled = true;
@@ -177,32 +171,41 @@ export function ChatView({
const prevProjectId = prevProjectIdRef.current; const prevProjectId = prevProjectIdRef.current;
prevProjectIdRef.current = session?.projectId; prevProjectIdRef.current = session?.projectId;
if (prevProjectId !== undefined && prevProjectId !== 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(() => { useEffect(() => {
const prev = prevContextRef.current; const prev = prevWorkspaceRef.current;
if ( if (
!activeWorkingContext || !activeWorkspace ||
!selectedProvider || !selectedProvider ||
session?.draft || session?.draft ||
activeWorkingContext === prev activeWorkspace === prev
) { ) {
return; return;
} }
prevContextRef.current = activeWorkingContext; prevWorkspaceRef.current = activeWorkspace;
if (prev && prev.path === activeWorkingContext.path) return; if (prev && prev.path === activeWorkspace.path) return;
void acpPrepareSession(activeSessionId, selectedProvider, {
workingDir: activeWorkingContext.path, async function prepareWorkspaceSession() {
personaId: selectedPersonaId ?? undefined, const workingDir = await resolveSessionCwd(project, activeWorkspace.path);
}).catch((error) => { if (!workingDir) {
return;
}
await acpPrepareSession(activeSessionId, selectedProvider, workingDir, {
personaId: selectedPersonaId ?? undefined,
});
}
void prepareWorkspaceSession().catch((error) => {
console.error("Failed to prepare ACP session:", error); console.error("Failed to prepare ACP session:", error);
}); });
}, [ }, [
activeWorkingContext, activeWorkspace,
activeSessionId, activeSessionId,
project,
selectedProvider, selectedProvider,
selectedPersonaId, selectedPersonaId,
session?.draft, session?.draft,
@@ -230,19 +233,32 @@ export function ChatView({
.getState() .getState()
.projects.find((candidate) => candidate.id === projectId) ?? .projects.find((candidate) => candidate.id === projectId) ??
null); null);
const nextWorkingDir =
resolveProjectWorkingDir(nextProject) ??
(projectId == null ? (homeArtifactsRoot ?? undefined) : undefined);
useChatSessionStore useChatSessionStore
.getState() .getState()
.updateSession(activeSessionId, { projectId }); .updateSession(activeSessionId, { projectId });
if (!session?.draft && selectedProvider && nextWorkingDir) { if (!session?.draft && selectedProvider) {
void acpPrepareSession(activeSessionId, selectedProvider, { async function updateProjectSessionCwd() {
workingDir: nextWorkingDir, const workingDir = await resolveSessionCwd(
personaId: selectedPersonaId ?? undefined, nextProject,
}).catch((error) => { activeWorkspace?.path,
);
if (!workingDir) {
return;
}
await acpPrepareSession(
activeSessionId,
selectedProvider,
workingDir,
{
personaId: selectedPersonaId ?? undefined,
},
);
}
void updateProjectSessionCwd().catch((error) => {
console.error( console.error(
"Failed to update ACP session working directory:", "Failed to update ACP session working directory:",
error, error,
@@ -252,7 +268,7 @@ export function ChatView({
}, },
[ [
activeSessionId, activeSessionId,
homeArtifactsRoot, activeWorkspace?.path,
selectedPersonaId, selectedPersonaId,
selectedProvider, selectedProvider,
session?.draft, session?.draft,
@@ -331,6 +347,10 @@ export function ChatView({
const personaInfo = selectedPersona const personaInfo = selectedPersona
? { id: selectedPersona.id, name: selectedPersona.displayName } ? { id: selectedPersona.id, name: selectedPersona.displayName }
: undefined; : undefined;
const resolveCurrentSessionCwd = useCallback(
() => resolveSessionCwd(project, activeWorkspace?.path),
[project, activeWorkspace?.path],
);
const { const {
messages, messages,
chatState, chatState,
@@ -343,7 +363,7 @@ export function ChatView({
selectedProvider, selectedProvider,
effectiveSystemPrompt, effectiveSystemPrompt,
personaInfo, personaInfo,
effectiveWorkingDir, resolveCurrentSessionCwd,
); );
const isLoadingHistory = useChatStore( const isLoadingHistory = useChatStore(
(s) => (s) =>
+14 -16
View File
@@ -15,7 +15,7 @@ import {
import type { CreatedWorktree } from "@/shared/types/git"; import type { CreatedWorktree } from "@/shared/types/git";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
import { useChatSessionStore } from "../stores/chatSessionStore"; import { useChatSessionStore } from "../stores/chatSessionStore";
import type { WorkingContext } from "../stores/chatSessionStore"; import type { ActiveWorkspace } from "../stores/chatSessionStore";
import { WorkspaceWidget } from "./widgets/WorkspaceWidget"; import { WorkspaceWidget } from "./widgets/WorkspaceWidget";
import { ChangesWidget } from "./widgets/ChangesWidget"; import { ChangesWidget } from "./widgets/ChangesWidget";
import { ArtifactsWidget } from "./widgets/ArtifactsWidget"; import { ArtifactsWidget } from "./widgets/ArtifactsWidget";
@@ -39,35 +39,33 @@ export function ContextPanel({
}: ContextPanelProps) { }: ContextPanelProps) {
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const [activeTab, setActiveTab] = useState<ContextPanelTab>("details"); const [activeTab, setActiveTab] = useState<ContextPanelTab>("details");
const primaryWorkingDir = projectWorkingDirs[0] ?? null; const primaryWorkspaceRoot = projectWorkingDirs[0] ?? null;
const activeContext = useChatSessionStore( const activeContext = useChatSessionStore(
(s) => s.activeWorkingContextBySession[sessionId], (s) => s.activeWorkspaceBySession[sessionId],
);
const setActiveWorkingContext = useChatSessionStore(
(s) => s.setActiveWorkingContext,
); );
const setActiveWorkspace = useChatSessionStore((s) => s.setActiveWorkspace);
const gitQueryPath = activeContext?.path ?? primaryWorkingDir; const gitTargetPath = activeContext?.path ?? primaryWorkspaceRoot;
const { const {
data: gitState, data: gitState,
error, error,
isLoading, isLoading,
isFetching, isFetching,
refetch, refetch,
} = useGitState(gitQueryPath, activeTab === "details"); } = useGitState(gitTargetPath, activeTab === "details");
const { const {
data: changedFiles, data: changedFiles,
isLoading: isFilesLoading, isLoading: isFilesLoading,
refetch: refetchFiles, refetch: refetchFiles,
} = useChangedFiles(gitQueryPath, activeTab === "details"); } = useChangedFiles(gitTargetPath, activeTab === "details");
const handleContextChange = useCallback( const handleContextChange = useCallback(
(context: WorkingContext) => { (context: ActiveWorkspace) => {
setActiveWorkingContext(sessionId, context); setActiveWorkspace(sessionId, context);
}, },
[sessionId, setActiveWorkingContext], [sessionId, setActiveWorkspace],
); );
const refetchAll = useCallback(async () => { const refetchAll = useCallback(async () => {
@@ -149,11 +147,11 @@ export function ContextPanel({
const handleOpenChangedFile = useCallback( const handleOpenChangedFile = useCallback(
(filePath: string) => { (filePath: string) => {
if (!gitQueryPath) return; if (!gitTargetPath) return;
const fullPath = `${gitQueryPath}/${filePath}`; const fullPath = `${gitTargetPath}/${filePath}`;
void openPath(fullPath); void openPath(fullPath);
}, },
[gitQueryPath], [gitTargetPath],
); );
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
@@ -202,7 +200,7 @@ export function ContextPanel({
files={changedFiles} files={changedFiles}
isLoading={isFilesLoading} isLoading={isFilesLoading}
currentBranch={gitState?.currentBranch ?? null} currentBranch={gitState?.currentBranch ?? null}
repoPath={gitQueryPath ?? ""} repoPath={gitTargetPath ?? ""}
onOpenFile={handleOpenChangedFile} onOpenFile={handleOpenChangedFile}
/> />
<ArtifactsWidget /> <ArtifactsWidget />
@@ -20,13 +20,13 @@ import {
import { buttonVariants } from "@/shared/ui/button"; import { buttonVariants } from "@/shared/ui/button";
import { cn } from "@/shared/lib/cn"; import { cn } from "@/shared/lib/cn";
import type { GitState } from "@/shared/types/git"; import type { GitState } from "@/shared/types/git";
import type { WorkingContext } from "../../stores/chatSessionStore"; import type { ActiveWorkspace } from "../../stores/chatSessionStore";
interface WorkingContextPickerProps { interface WorkingContextPickerProps {
currentProjectPath: string | null; currentProjectPath: string | null;
gitState: GitState | undefined; gitState: GitState | undefined;
activeContext: WorkingContext | undefined; activeContext: ActiveWorkspace | undefined;
onSelect: (context: WorkingContext) => void; onSelect: (context: ActiveWorkspace) => void;
onSwitchBranch: (path: string, branch: string) => Promise<void>; onSwitchBranch: (path: string, branch: string) => Promise<void>;
onStashAndSwitch: (path: string, branch: string) => Promise<void>; onStashAndSwitch: (path: string, branch: string) => Promise<void>;
} }
@@ -63,7 +63,7 @@ export function WorkingContextPicker({
}: WorkingContextPickerProps) { }: WorkingContextPickerProps) {
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [pendingSwitch, setPendingSwitch] = useState<WorkingContext | null>( const [pendingSwitch, setPendingSwitch] = useState<ActiveWorkspace | null>(
null, null,
); );
const [switching, setSwitching] = useState(false); const [switching, setSwitching] = useState(false);
@@ -5,7 +5,7 @@ import type { CreatedWorktree, GitState } from "@/shared/types/git";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { SplitButton } from "@/shared/ui/split-button"; import { SplitButton } from "@/shared/ui/split-button";
import { Spinner } from "@/shared/ui/spinner"; import { Spinner } from "@/shared/ui/spinner";
import type { WorkingContext } from "../../stores/chatSessionStore"; import type { ActiveWorkspace } from "../../stores/chatSessionStore";
import { formatErrorMessage } from "./formatError"; import { formatErrorMessage } from "./formatError";
import { import {
WorkspaceCreateDialog, WorkspaceCreateDialog,
@@ -15,9 +15,9 @@ import {
interface WorkspaceActionsMenuProps { interface WorkspaceActionsMenuProps {
currentProjectPath: string; currentProjectPath: string;
gitState: GitState; gitState: GitState;
activeContext: WorkingContext | undefined; activeContext: ActiveWorkspace | undefined;
disabled?: boolean; disabled?: boolean;
onContextChange: (context: WorkingContext) => void; onContextChange: (context: ActiveWorkspace) => void;
onFetch: (path: string) => Promise<void>; onFetch: (path: string) => Promise<void>;
onPull: (path: string) => Promise<void>; onPull: (path: string) => Promise<void>;
onCreateBranch: ( onCreateBranch: (
@@ -21,7 +21,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/shared/ui/select"; } from "@/shared/ui/select";
import type { WorkingContext } from "../../stores/chatSessionStore"; import type { ActiveWorkspace } from "../../stores/chatSessionStore";
import { formatErrorMessage } from "./formatError"; import { formatErrorMessage } from "./formatError";
import { shortenPath } from "./WorkingContextPicker"; import { shortenPath } from "./WorkingContextPicker";
@@ -35,7 +35,7 @@ interface WorkspaceCreateDialogProps {
currentPath: string; currentPath: string;
activeBranch: string | null; activeBranch: string | null;
onClose: () => void; onClose: () => void;
onContextChange: (context: WorkingContext) => void; onContextChange: (context: ActiveWorkspace) => void;
onCreateBranch: ( onCreateBranch: (
path: string, path: string,
name: string, name: string,
@@ -3,7 +3,7 @@ import { IconFolder, IconGitBranch, IconRefresh } from "@tabler/icons-react";
import type { CreatedWorktree, GitState } from "@/shared/types/git"; import type { CreatedWorktree, GitState } from "@/shared/types/git";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { Spinner } from "@/shared/ui/spinner"; import { Spinner } from "@/shared/ui/spinner";
import type { WorkingContext } from "../../stores/chatSessionStore"; import type { ActiveWorkspace } from "../../stores/chatSessionStore";
import { Widget } from "./Widget"; import { Widget } from "./Widget";
import { WorkspaceActionsMenu } from "./WorkspaceActionsMenu"; import { WorkspaceActionsMenu } from "./WorkspaceActionsMenu";
import { WorkingContextPicker, shortenPath } from "./WorkingContextPicker"; import { WorkingContextPicker, shortenPath } from "./WorkingContextPicker";
@@ -16,8 +16,8 @@ interface WorkspaceWidgetProps {
isLoading: boolean; isLoading: boolean;
isFetching: boolean; isFetching: boolean;
error: Error | null; error: Error | null;
activeContext: WorkingContext | undefined; activeContext: ActiveWorkspace | undefined;
onContextChange: (context: WorkingContext) => void; onContextChange: (context: ActiveWorkspace) => void;
onSwitchBranch: (path: string, branch: string) => Promise<void>; onSwitchBranch: (path: string, branch: string) => Promise<void>;
onStashAndSwitch: (path: string, branch: string) => Promise<void>; onStashAndSwitch: (path: string, branch: string) => Promise<void>;
onInitRepo: (path: string) => Promise<void>; onInitRepo: (path: string) => Promise<void>;
@@ -58,7 +58,7 @@ export function WorkspaceWidget({
onRefresh, onRefresh,
}: WorkspaceWidgetProps) { }: WorkspaceWidgetProps) {
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const primaryWorkingDir = projectWorkingDirs[0] ?? null; const primaryWorkspaceRoot = projectWorkingDirs[0] ?? null;
const gitErrorMessage = const gitErrorMessage =
error instanceof Error ? error.message : t("contextPanel.errors.gitRead"); error instanceof Error ? error.message : t("contextPanel.errors.gitRead");
@@ -73,7 +73,7 @@ export function WorkspaceWidget({
variant="ghost" variant="ghost"
size="icon-xs" size="icon-xs"
onClick={onRefresh} onClick={onRefresh}
disabled={!primaryWorkingDir || isFetching} disabled={!primaryWorkspaceRoot || isFetching}
className="rounded-md" className="rounded-md"
aria-label={t("contextPanel.actions.refreshGitStatus")} aria-label={t("contextPanel.actions.refreshGitStatus")}
title={t("contextPanel.actions.refreshGitStatus")} title={t("contextPanel.actions.refreshGitStatus")}
@@ -103,7 +103,7 @@ export function WorkspaceWidget({
</p> </p>
)} )}
{!primaryWorkingDir ? ( {!primaryWorkspaceRoot ? (
<p className="truncate">{t("contextPanel.empty.folderNotSet")}</p> <p className="truncate">{t("contextPanel.empty.folderNotSet")}</p>
) : isLoading && !gitState ? ( ) : isLoading && !gitState ? (
<div className="flex items-center gap-2 text-foreground"> <div className="flex items-center gap-2 text-foreground">
@@ -115,7 +115,7 @@ export function WorkspaceWidget({
) : gitState?.isGitRepo ? ( ) : gitState?.isGitRepo ? (
<div className="space-y-2"> <div className="space-y-2">
<WorkingContextPicker <WorkingContextPicker
currentProjectPath={primaryWorkingDir} currentProjectPath={primaryWorkspaceRoot}
gitState={gitState} gitState={gitState}
activeContext={activeContext} activeContext={activeContext}
onSelect={onContextChange} onSelect={onContextChange}
@@ -123,7 +123,7 @@ export function WorkspaceWidget({
onStashAndSwitch={onStashAndSwitch} onStashAndSwitch={onStashAndSwitch}
/> />
<WorkspaceActionsMenu <WorkspaceActionsMenu
currentProjectPath={primaryWorkingDir} currentProjectPath={primaryWorkspaceRoot}
gitState={gitState} gitState={gitState}
activeContext={activeContext} activeContext={activeContext}
disabled={isFetching} disabled={isFetching}
@@ -137,13 +137,13 @@ export function WorkspaceWidget({
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
<p className="truncate text-foreground-subtle"> <p className="truncate text-foreground-subtle">
{shortenPath(primaryWorkingDir)} {shortenPath(primaryWorkspaceRoot)}
</p> </p>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="xs" size="xs"
onClick={() => void onInitRepo(primaryWorkingDir)} onClick={() => void onInitRepo(primaryWorkspaceRoot)}
> >
<IconGitBranch className="size-3" /> <IconGitBranch className="size-3" />
{t("contextPanel.git.initRepo")} {t("contextPanel.git.initRepo")}
@@ -2,12 +2,9 @@ import { describe, expect, it } from "vitest";
import { import {
buildProjectSystemPrompt, buildProjectSystemPrompt,
composeSystemPrompt, composeSystemPrompt,
defaultArtifactsDir,
getProjectArtifactRoots, getProjectArtifactRoots,
getProjectFolderName, getProjectFolderName,
getProjectFolderOption, getProjectFolderOption,
resolveEffectiveWorkingDir,
resolveProjectWorkingDir,
} from "./chatProjectContext"; } from "./chatProjectContext";
describe("chatProjectContext", () => { describe("chatProjectContext", () => {
@@ -111,84 +108,4 @@ describe("chatProjectContext", () => {
"/Users/wesb/dev/other/artifacts", "/Users/wesb/dev/other/artifacts",
]); ]);
}); });
it("resolves the first working directory to an artifacts subfolder", () => {
expect(
resolveProjectWorkingDir({
workingDirs: ["/Users/wesb/dev/goose2", "/Users/wesb/dev/other"],
artifactsDir: "/Users/wesb/.goose/projects/goose2/artifacts",
}),
).toBe("/Users/wesb/dev/goose2/artifacts");
});
it("falls back to the project artifacts dir when no working dirs exist", () => {
expect(
resolveProjectWorkingDir({
workingDirs: [],
artifactsDir: "/Users/wesb/.goose/projects/sample-project/artifacts",
}),
).toBe("/Users/wesb/.goose/projects/sample-project/artifacts");
});
describe("defaultArtifactsDir", () => {
it("normalises path separators and appends .goose/artifacts", () => {
expect(defaultArtifactsDir("/Users/wesb")).toBe(
"/Users/wesb/.goose/artifacts",
);
});
it("normalises backslashes on Windows-style paths", () => {
expect(defaultArtifactsDir("C:\\Users\\wesb\\")).toBe(
"C:/Users/wesb/.goose/artifacts",
);
});
it("strips trailing slashes", () => {
expect(defaultArtifactsDir("/Users/wesb/")).toBe(
"/Users/wesb/.goose/artifacts",
);
});
});
describe("resolveEffectiveWorkingDir", () => {
it("returns the project working dir without requiring homeDir", () => {
expect(
resolveEffectiveWorkingDir({
workingDirs: ["/Users/wesb/dev/goose2"],
artifactsDir: "/Users/wesb/.goose/projects/goose2/artifacts",
}),
).toBe("/Users/wesb/dev/goose2/artifacts");
});
it("returns the project working dir when available", () => {
expect(
resolveEffectiveWorkingDir(
{
workingDirs: ["/Users/wesb/dev/goose2"],
artifactsDir: "/Users/wesb/.goose/projects/goose2/artifacts",
},
"/Users/wesb",
),
).toBe("/Users/wesb/dev/goose2/artifacts");
});
it("returns undefined when a project exists but has no working dirs", () => {
expect(
resolveEffectiveWorkingDir(
{ workingDirs: [], artifactsDir: "" },
"/Users/wesb",
),
).toBeUndefined();
});
it("falls back to home artifacts dir when no project", () => {
expect(resolveEffectiveWorkingDir(null, "/Users/wesb")).toBe(
"/Users/wesb/.goose/artifacts",
);
});
it("does not resolve a non-project fallback without homeDir", () => {
expect(resolveEffectiveWorkingDir(null)).toBeUndefined();
});
});
}); });
@@ -1,5 +1,5 @@
import type { ProjectInfo } from "../api/projects"; import type { ProjectInfo } from "../api/projects";
import { resolvePath } from "@/shared/api/pathResolver";
export interface ProjectFolderOption { export interface ProjectFolderOption {
id: string; id: string;
name: string; name: string;
@@ -25,7 +25,7 @@ function appendArtifactsSegment(path: string): string {
return `${path.replace(/[\\/]+$/, "")}/artifacts`; return `${path.replace(/[\\/]+$/, "")}/artifacts`;
} }
function resolveProjectFolderPaths( function resolveProjectArtifactRoots(
project: Pick<ProjectInfo, "workingDirs" | "artifactsDir"> | null | undefined, project: Pick<ProjectInfo, "workingDirs" | "artifactsDir"> | null | undefined,
): string[] { ): string[] {
const workingDirs = (project?.workingDirs ?? []) const workingDirs = (project?.workingDirs ?? [])
@@ -43,25 +43,37 @@ function resolveProjectFolderPaths(
export function getProjectArtifactRoots( export function getProjectArtifactRoots(
project: Pick<ProjectInfo, "workingDirs" | "artifactsDir"> | null | undefined, project: Pick<ProjectInfo, "workingDirs" | "artifactsDir"> | null | undefined,
): string[] { ): string[] {
return resolveProjectFolderPaths(project); return resolveProjectArtifactRoots(project);
}
export function resolveProjectDefaultArtifactRoot(
project: ProjectInfo | null | undefined,
): string | undefined {
const workingDirs = (project?.workingDirs ?? [])
.map((directory) => trimValue(directory))
.filter((directory): directory is string => directory !== null);
if (workingDirs.length > 0) {
return appendArtifactsSegment(workingDirs[0]);
}
return trimValue(project?.artifactsDir) ?? undefined;
}
export async function defaultGlobalArtifactRoot(): Promise<string> {
return (await resolvePath({ parts: ["~", ".goose", "artifacts"] })).path;
} }
export function getProjectFolderOption( export function getProjectFolderOption(
project: Pick<ProjectInfo, "workingDirs" | "artifactsDir"> | null | undefined, project: Pick<ProjectInfo, "workingDirs" | "artifactsDir"> | null | undefined,
): ProjectFolderOption[] { ): ProjectFolderOption[] {
return resolveProjectFolderPaths(project).map((d) => ({ return resolveProjectArtifactRoots(project).map((d) => ({
id: d, id: d,
name: getProjectFolderName(d), name: getProjectFolderName(d),
path: d, path: d,
})); }));
} }
export function resolveProjectWorkingDir(
project: Pick<ProjectInfo, "workingDirs" | "artifactsDir"> | null | undefined,
): string | undefined {
return resolveProjectFolderPaths(project)[0];
}
export function buildProjectSystemPrompt( export function buildProjectSystemPrompt(
project: ProjectInfo | null | undefined, project: ProjectInfo | null | undefined,
): string | undefined { ): string | undefined {
@@ -69,7 +81,7 @@ export function buildProjectSystemPrompt(
return undefined; return undefined;
} }
const artifactDir = resolveProjectWorkingDir(project); const artifactDir = resolveProjectDefaultArtifactRoot(project);
const settings: string[] = [`Project name: ${project.name}`]; const settings: string[] = [`Project name: ${project.name}`];
const description = trimValue(project.description); const description = trimValue(project.description);
const workingDirs = (project.workingDirs ?? []) const workingDirs = (project.workingDirs ?? [])
@@ -120,40 +132,6 @@ export function buildProjectSystemPrompt(
return sections.join("\n\n"); return sections.join("\n\n");
} }
/**
* Builds the default artifacts directory from a raw home-dir string.
* Normalises path separators and trailing slashes before appending `.goose/artifacts`.
*/
export function defaultArtifactsDir(homeDir: string): string {
const normalizedHome = homeDir.replace(/\\/g, "/").replace(/\/+$/, "");
return `${normalizedHome}/.goose/artifacts`;
}
/**
* Resolves the effective working directory for a session.
* Uses the project's working dir if available, otherwise falls back to
* `~/.goose/artifacts` using the provided home directory.
*
* When a project is provided but has no configured working dirs, returns
* `undefined` so the caller can decide how to handle it.
*/
export function resolveEffectiveWorkingDir(
project: Pick<ProjectInfo, "workingDirs" | "artifactsDir"> | null | undefined,
homeDir?: string,
): string | undefined {
const projectDir = resolveProjectWorkingDir(project);
if (projectDir) {
return projectDir;
}
if (project) {
return undefined;
}
if (!homeDir) {
return undefined;
}
return defaultArtifactsDir(homeDir);
}
export function composeSystemPrompt( export function composeSystemPrompt(
...parts: Array<string | null | undefined> ...parts: Array<string | null | undefined>
): string | undefined { ): string | undefined {
@@ -0,0 +1,107 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolvePath } from "@/shared/api/pathResolver";
import type { ProjectInfo } from "../api/projects";
import { resolveSessionCwd } from "./sessionCwdSelection";
import {
defaultGlobalArtifactRoot,
resolveProjectDefaultArtifactRoot,
} from "./chatProjectContext";
vi.mock("@/shared/api/pathResolver", () => ({
resolvePath: vi.fn(),
}));
function makeProject(overrides: Partial<ProjectInfo> = {}): ProjectInfo {
return {
id: "project-1",
name: "Project",
description: "",
prompt: "",
icon: "folder",
color: "#000000",
preferredProvider: null,
preferredModel: null,
workingDirs: [],
useWorktrees: false,
order: 0,
archivedAt: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
artifactsDir: "",
...overrides,
};
}
describe("sessionCwdSelection", () => {
beforeEach(() => {
vi.mocked(resolvePath).mockReset();
});
it("resolves the first workspace root to the default artifact root", () => {
expect(
resolveProjectDefaultArtifactRoot(
makeProject({
workingDirs: ["/Users/wesb/dev/goose2", "/Users/wesb/dev/other"],
artifactsDir: "/Users/wesb/.goose/projects/goose2/artifacts",
}),
),
).toBe("/Users/wesb/dev/goose2/artifacts");
});
it("falls back to the stored project artifact root when no workspace roots exist", () => {
expect(
resolveProjectDefaultArtifactRoot(
makeProject({
workingDirs: [],
artifactsDir: "/Users/wesb/.goose/projects/sample-project/artifacts",
}),
),
).toBe("/Users/wesb/.goose/projects/sample-project/artifacts");
});
it("returns undefined for a pathless project artifact root", () => {
expect(
resolveProjectDefaultArtifactRoot(
makeProject({
workingDirs: [],
artifactsDir: " ",
}),
),
).toBeUndefined();
});
it("falls back to global artifacts for a pathless project session cwd", async () => {
vi.mocked(resolvePath).mockResolvedValue({
path: "/Users/wesb/.goose/artifacts",
});
await expect(
resolveSessionCwd(
makeProject({
workingDirs: [],
artifactsDir: " ",
}),
),
).resolves.toBe("/Users/wesb/.goose/artifacts");
expect(resolvePath).toHaveBeenCalledWith({
parts: ["~", ".goose", "artifacts"],
});
});
describe("defaultGlobalArtifactRoot", () => {
it("resolves the global artifact root through the path resolver", async () => {
vi.mocked(resolvePath).mockResolvedValue({
path: "/Users/wesb/.goose/artifacts",
});
await expect(defaultGlobalArtifactRoot()).resolves.toBe(
"/Users/wesb/.goose/artifacts",
);
expect(resolvePath).toHaveBeenCalledWith({
parts: ["~", ".goose", "artifacts"],
});
});
});
});
@@ -0,0 +1,42 @@
import type { ProjectInfo } from "../api/projects";
import { resolvePath } from "@/shared/api/pathResolver";
function trimValue(value: string | null | undefined): string | null {
const trimmed = value?.trim();
return trimmed ? trimmed : null;
}
function buildSessionCwdParts(
project: ProjectInfo | null | undefined,
activeWorkspacePath?: string | null,
): string[] {
const trimmedWorkspacePath = trimValue(activeWorkspacePath);
if (trimmedWorkspacePath) {
return [trimmedWorkspacePath];
}
const workingDirs = (project?.workingDirs ?? [])
.map((directory) => trimValue(directory))
.filter((directory): directory is string => directory !== null);
if (workingDirs.length > 0) {
return [workingDirs[0], "artifacts"];
}
const artifactRoot = trimValue(project?.artifactsDir);
if (artifactRoot) {
return [artifactRoot];
}
return ["~", ".goose", "artifacts"];
}
export async function resolveSessionCwd(
project: ProjectInfo | null | undefined,
activeWorkspacePath?: string | null,
): Promise<string> {
return (
await resolvePath({
parts: buildSessionCwdParts(project, activeWorkspacePath),
})
).path;
}
+1 -2
View File
@@ -21,7 +21,6 @@ export interface AcpSendMessageOptions {
} }
export interface AcpPrepareSessionOptions { export interface AcpPrepareSessionOptions {
workingDir?: string;
personaId?: string; personaId?: string;
} }
@@ -67,9 +66,9 @@ export async function acpSendMessage(
export async function acpPrepareSession( export async function acpPrepareSession(
sessionId: string, sessionId: string,
providerId: string, providerId: string,
workingDir: string,
options: AcpPrepareSessionOptions = {}, options: AcpPrepareSessionOptions = {},
): Promise<void> { ): Promise<void> {
const workingDir = options.workingDir ?? "~/.goose/artifacts";
await sessionTracker.prepareSession( await sessionTracker.prepareSession(
sessionId, sessionId,
providerId, providerId,
+1
View File
@@ -1,3 +1,4 @@
export * from "./agents"; export * from "./agents";
export * from "./acp"; export * from "./acp";
export * from "./git"; export * from "./git";
export * from "./pathResolver";
+17
View File
@@ -0,0 +1,17 @@
import { invoke } from "@tauri-apps/api/core";
export interface ResolvePathParams {
parts: string[];
}
export interface ResolvedPath {
path: string;
}
export async function resolvePath({
parts,
}: ResolvePathParams): Promise<ResolvedPath> {
return invoke("resolve_path", {
request: { parts },
});
}
-2
View File
@@ -1,7 +1,5 @@
export class GooseClient { export class GooseClient {
closed = Promise.resolve(); closed = Promise.resolve();
constructor(..._args: unknown[]) {}
async initialize(..._args: unknown[]): Promise<void> {} async initialize(..._args: unknown[]): Promise<void> {}
} }