diff --git a/ui/goose2/src-tauri/capabilities/default.json b/ui/goose2/src-tauri/capabilities/default.json index 59473721..4fd21675 100644 --- a/ui/goose2/src-tauri/capabilities/default.json +++ b/ui/goose2/src-tauri/capabilities/default.json @@ -19,9 +19,6 @@ { "path": "$HOME/.goose/**" }, - { - "path": "$HOME/.goose/artifacts/**" - }, { "path": "$TEMP/**" }, diff --git a/ui/goose2/src-tauri/gen/schemas/capabilities.json b/ui/goose2/src-tauri/gen/schemas/capabilities.json index 12ce88ac..1fe86787 100644 --- a/ui/goose2/src-tauri/gen/schemas/capabilities.json +++ b/ui/goose2/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","opener:default",{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$HOME/.goose/artifacts/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"window-state:allow-restore-state","window-state:allow-save-window-state","dialog:allow-open","dialog:allow-save","app-test-driver:default","core:webview:allow-set-webview-zoom"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","opener:default",{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"window-state:allow-restore-state","window-state:allow-save-window-state","dialog:allow-open","dialog:allow-save","app-test-driver:default","core:webview:allow-set-webview-zoom"]}} \ No newline at end of file diff --git a/ui/goose2/src-tauri/src/commands/path_resolver.rs b/ui/goose2/src-tauri/src/commands/path_resolver.rs index 3efc39f6..a7542abe 100644 --- a/ui/goose2/src-tauri/src/commands/path_resolver.rs +++ b/ui/goose2/src-tauri/src/commands/path_resolver.rs @@ -61,8 +61,8 @@ mod tests { #[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()) + resolve_path_parts(vec!["/tmp/project".to_string(), "src".to_string()]), + Ok("/tmp/project/src".to_string()) ); } @@ -81,24 +81,16 @@ mod tests { }; 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()) + resolve_path_parts(vec!["~".to_string()]), + Ok(home.to_string_lossy().into_owned()) ); assert_eq!( - resolve_path_parts(vec!["~/artifacts".to_string()]), - Ok(home.join("artifacts").to_string_lossy().into_owned()) + resolve_path_parts(vec!["~/Documents".to_string()]), + Ok(home.join("Documents").to_string_lossy().into_owned()) ); assert_eq!( - resolve_path_parts(vec!["~\\artifacts".to_string()]), - Ok(home.join("artifacts").to_string_lossy().into_owned()) + resolve_path_parts(vec!["~\\Documents".to_string()]), + Ok(home.join("Documents").to_string_lossy().into_owned()) ); } diff --git a/ui/goose2/src-tauri/src/commands/projects.rs b/ui/goose2/src-tauri/src/commands/projects.rs index 31c129c1..cd41faa3 100644 --- a/ui/goose2/src-tauri/src/commands/projects.rs +++ b/ui/goose2/src-tauri/src/commands/projects.rs @@ -1,6 +1,6 @@ use serde::Deserialize; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; fn projects_dir() -> Result { let home = dirs::home_dir().ok_or("Could not determine home directory")?; @@ -120,10 +120,6 @@ where .collect()) } -fn project_artifacts_dir(project_dir: &Path) -> String { - project_dir.join("artifacts").to_string_lossy().into_owned() -} - #[derive(serde::Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] struct StoredProjectInfo { @@ -170,10 +166,9 @@ pub struct ProjectInfo { pub archived_at: Option, pub created_at: String, pub updated_at: String, - pub artifacts_dir: String, } -fn project_info_from_stored(project_dir: &Path, stored: StoredProjectInfo) -> ProjectInfo { +fn project_info_from_stored(stored: StoredProjectInfo) -> ProjectInfo { ProjectInfo { id: stored.id, name: stored.name, @@ -189,7 +184,6 @@ fn project_info_from_stored(project_dir: &Path, stored: StoredProjectInfo) -> Pr archived_at: stored.archived_at, created_at: stored.created_at, updated_at: stored.updated_at, - artifacts_dir: project_artifacts_dir(project_dir), } } @@ -223,7 +217,7 @@ pub fn list_projects() -> Result, String> { Err(_) => continue, }; - projects.push(project_info_from_stored(&path, info)); + projects.push(project_info_from_stored(info)); } projects.sort_by_key(|p| p.order); @@ -253,7 +247,7 @@ pub fn list_archived_projects() -> Result, String> { let raw = fs::read_to_string(&project_json).unwrap_or_default(); if let Ok(info) = serde_json::from_str::(&raw) { if info.archived_at.is_some() { - projects.push(project_info_from_stored(&path, info)); + projects.push(project_info_from_stored(info)); } } } @@ -329,7 +323,7 @@ pub fn create_project( .map_err(|e| format!("Failed to serialize project: {}", e))?; fs::write(&project_path, json).map_err(|e| format!("Failed to write project.json: {}", e))?; - Ok(project_info_from_stored(&dir, stored)) + Ok(project_info_from_stored(stored)) } #[allow(clippy::too_many_arguments)] @@ -374,7 +368,7 @@ pub fn update_project( .map_err(|e| format!("Failed to serialize project: {}", e))?; fs::write(&project_path, json).map_err(|e| format!("Failed to write project.json: {}", e))?; - Ok(project_info_from_stored(&dir, stored)) + Ok(project_info_from_stored(stored)) } #[tauri::command] @@ -400,8 +394,8 @@ pub fn reorder_projects(order: Vec<(String, i32)>) -> Result<(), String> { #[tauri::command] pub fn get_project(id: String) -> Result { - let (dir, info) = find_project_by_id(&id)?; - Ok(project_info_from_stored(&dir, info)) + let (_, info) = find_project_by_id(&id)?; + Ok(project_info_from_stored(info)) } #[tauri::command] @@ -472,8 +466,7 @@ pub fn restore_project(id: String) -> Result<(), String> { #[cfg(test)] mod tests { - use super::{project_artifacts_dir, StoredProjectInfo}; - use std::path::Path; + use super::StoredProjectInfo; #[test] fn deserializes_legacy_single_working_dir() { @@ -497,12 +490,4 @@ mod tests { assert_eq!(project.working_dirs, vec!["/tmp/legacy"]); } - - #[test] - fn builds_project_artifacts_dir_inside_project_storage() { - assert_eq!( - project_artifacts_dir(Path::new("/Users/test/.goose/projects/sample-project")), - "/Users/test/.goose/projects/sample-project/artifacts" - ); - } } diff --git a/ui/goose2/src-tauri/src/services/acp/goose_serve.rs b/ui/goose2/src-tauri/src/services/acp/goose_serve.rs index debbabf7..693e2ab5 100644 --- a/ui/goose2/src-tauri/src/services/acp/goose_serve.rs +++ b/ui/goose2/src-tauri/src/services/acp/goose_serve.rs @@ -146,10 +146,7 @@ async fn wait_for_server_ready(port: u16, child: &mut Child) -> Result<(), Strin } fn default_serve_working_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join(".goose") - .join("artifacts") + dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp")) } fn reserve_free_port() -> Result { diff --git a/ui/goose2/src/features/chat/hooks/ArtifactPolicyContext.tsx b/ui/goose2/src/features/chat/hooks/ArtifactPolicyContext.tsx index 0774a6d3..4271f377 100644 --- a/ui/goose2/src/features/chat/hooks/ArtifactPolicyContext.tsx +++ b/ui/goose2/src/features/chat/hooks/ArtifactPolicyContext.tsx @@ -83,49 +83,6 @@ function basenameOf(path: string): string { return parts[parts.length - 1] ?? path; } -function homeArtifactsRootFromRoots(roots: string[]): string | null { - return ( - roots.find((root) => /\/\.goose\/artifacts\/?$/.test(root.trim())) ?? null - ); -} - -function stripRootArtifactsSegment( - path: string, - roots: string[], -): string | null { - for (const root of roots) { - const normalizedRoot = root.replace(/\/+$/, ""); - const prefix = `${normalizedRoot}/artifacts/`; - if (path.startsWith(prefix)) { - return `${normalizedRoot}/${path.slice(prefix.length)}`; - } - } - return null; -} - -function fallbackArtifactPath(path: string, roots: string[]): string | null { - const normalized = path.trim(); - if (!/\/\.goose\/projects\/.+\/artifacts\/.+/.test(normalized)) { - const rootAnchored = stripRootArtifactsSegment(normalized, roots); - if (rootAnchored) { - return rootAnchored; - } - return null; - } - - const filename = basenameOf(normalized); - if (!filename || filename === normalized) { - return null; - } - - const homeArtifactsRoot = homeArtifactsRootFromRoots(roots); - if (!homeArtifactsRoot) { - return null; - } - - return `${homeArtifactsRoot.replace(/\/+$/, "")}/${filename}`; -} - export function ArtifactPolicyProvider({ messages, allowedRoots, @@ -190,14 +147,9 @@ export function ArtifactPolicyProvider({ return path; } - const fallbackPath = fallbackArtifactPath(path, normalizedRoots); - if (fallbackPath && (await pathExists(fallbackPath))) { - return fallbackPath; - } - return null; }, - [normalizedRoots], + [], ); const checkPathExists = useCallback( diff --git a/ui/goose2/src/features/chat/hooks/__tests__/ArtifactPolicyContext.test.tsx b/ui/goose2/src/features/chat/hooks/__tests__/ArtifactPolicyContext.test.tsx index 0ea636e8..e41a7f51 100644 --- a/ui/goose2/src/features/chat/hooks/__tests__/ArtifactPolicyContext.test.tsx +++ b/ui/goose2/src/features/chat/hooks/__tests__/ArtifactPolicyContext.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import type { Message } from "@/shared/types/messages"; import { @@ -85,32 +85,6 @@ function ReadOnlyProbe({ readArgs }: { readArgs: Record }) { ); } -function FallbackProbe({ - path = "/Users/test/.goose/projects/sample-project/artifacts/report.md", -}: { - path?: string; -}) { - const { pathExists, openResolvedPath } = useArtifactPolicyContext(); - - return ( -
- - -
- ); -} - describe("ArtifactPolicyContext", () => { it("computes one primary host per message and resolves tool cards by args identity", () => { mockPathExists.mockReset(); @@ -163,7 +137,7 @@ describe("ArtifactPolicyContext", () => { render( { render( , @@ -225,150 +199,6 @@ describe("ArtifactPolicyContext", () => { expect(screen.getByTestId("read-only-artifacts")).toHaveTextContent(""); }); - it("falls back to the home artifacts root when a project artifacts path is missing", async () => { - mockPathExists.mockReset(); - vi.mocked(openPath).mockReset(); - mockPathExists.mockImplementation( - async (path: string) => path === "/Users/test/.goose/artifacts/report.md", - ); - - render( - - - , - ); - - screen.getByRole("button", { name: "Check path" }).click(); - await waitFor(() => { - expect( - (window as Window & { __artifactExists?: boolean }).__artifactExists, - ).toBe(true); - }); - - screen.getByRole("button", { name: "Open path" }).click(); - await waitFor(() => { - expect(vi.mocked(openPath)).toHaveBeenCalledWith( - "/Users/test/.goose/artifacts/report.md", - ); - }); - }); - - it("falls back from a working-dir artifacts path to the project root when the file lives there", async () => { - mockPathExists.mockReset(); - vi.mocked(openPath).mockReset(); - mockPathExists.mockImplementation( - async (path: string) => - path === "/Users/test/project-a/README_ENHANCED.md", - ); - - render( - - - , - ); - - screen.getByRole("button", { name: "Check path" }).click(); - await waitFor(() => { - expect( - (window as Window & { __artifactExists?: boolean }).__artifactExists, - ).toBe(true); - }); - - screen.getByRole("button", { name: "Open path" }).click(); - await waitFor(() => { - expect(vi.mocked(openPath)).toHaveBeenCalledWith( - "/Users/test/project-a/README_ENHANCED.md", - ); - }); - }); - - it("does not strip /artifacts/ from a parent directory in the path", async () => { - mockPathExists.mockReset(); - vi.mocked(openPath).mockReset(); - // The file lives at the nested artifacts path — the parent `/artifacts/` should NOT be stripped - mockPathExists.mockImplementation( - async (path: string) => - path === "/Users/test/artifacts/project/artifacts/README_ENHANCED.md", - ); - - render( - - - , - ); - - screen.getByRole("button", { name: "Check path" }).click(); - await waitFor(() => { - expect( - (window as Window & { __artifactExists?: boolean }).__artifactExists, - ).toBe(true); - }); - - screen.getByRole("button", { name: "Open path" }).click(); - await waitFor(() => { - expect(vi.mocked(openPath)).toHaveBeenCalledWith( - "/Users/test/artifacts/project/artifacts/README_ENHANCED.md", - ); - }); - }); - - it("falls back correctly when /artifacts/ appears in a parent dir", async () => { - mockPathExists.mockReset(); - vi.mocked(openPath).mockReset(); - // File is NOT at the artifacts path, but IS at the root-stripped path - mockPathExists.mockImplementation( - async (path: string) => - path === "/Users/test/artifacts/project/README.md", - ); - - render( - - - , - ); - - screen.getByRole("button", { name: "Check path" }).click(); - await waitFor(() => { - expect( - (window as Window & { __artifactExists?: boolean }).__artifactExists, - ).toBe(true); - }); - - screen.getByRole("button", { name: "Open path" }).click(); - await waitFor(() => { - expect(vi.mocked(openPath)).toHaveBeenCalledWith( - "/Users/test/artifacts/project/README.md", - ); - }); - }); - it("uses assistant text after a tool call to populate file actions and the Files tab", () => { mockPathExists.mockReset(); vi.mocked(openPath).mockReset(); @@ -396,7 +226,7 @@ describe("ArtifactPolicyContext", () => { }, { type: "text", - text: "The file alpha.md has been created at /Users/test/.goose/artifacts/alpha.md.", + text: "The file alpha.md has been created at /Users/test/alpha.md.", }, ], }, @@ -405,7 +235,7 @@ describe("ArtifactPolicyContext", () => { render( , @@ -415,10 +245,10 @@ describe("ArtifactPolicyContext", () => { "primary_host", ); expect(screen.getByTestId("text-followup-path")).toHaveTextContent( - "/Users/test/.goose/artifacts/alpha.md", + "/Users/test/alpha.md", ); expect(screen.getByTestId("text-followup-artifacts")).toHaveTextContent( - "/Users/test/.goose/artifacts/alpha.md", + "/Users/test/alpha.md", ); }); }); diff --git a/ui/goose2/src/features/chat/hooks/__tests__/useArtifactLinkHandler.test.tsx b/ui/goose2/src/features/chat/hooks/__tests__/useArtifactLinkHandler.test.tsx index 610c2cd0..59040189 100644 --- a/ui/goose2/src/features/chat/hooks/__tests__/useArtifactLinkHandler.test.tsx +++ b/ui/goose2/src/features/chat/hooks/__tests__/useArtifactLinkHandler.test.tsx @@ -97,7 +97,7 @@ describe("useArtifactLinkHandler", () => { const user = userEvent.setup(); const blocked = makeCandidate({ allowed: false, - blockedReason: "Path is outside allowed project/artifacts roots.", + blockedReason: "Path is outside allowed roots.", }); mockResolveMarkdownHref.mockReturnValue(blocked); @@ -106,7 +106,7 @@ describe("useArtifactLinkHandler", () => { expect(mockOpenResolvedPath).not.toHaveBeenCalled(); expect(screen.getByTestId("notice")).toHaveTextContent( - "Path is outside allowed project/artifacts roots.", + "Path is outside allowed roots.", ); }); @@ -142,7 +142,7 @@ describe("useArtifactLinkHandler", () => { await user.click(screen.getByText("Blocked")); expect(screen.getByTestId("notice")).toHaveTextContent( - "Path is outside allowed project/artifacts roots.", + "Path is outside allowed roots.", ); }); }); diff --git a/ui/goose2/src/features/chat/hooks/useArtifactLinkHandler.ts b/ui/goose2/src/features/chat/hooks/useArtifactLinkHandler.ts index 52aaa54e..543b535d 100644 --- a/ui/goose2/src/features/chat/hooks/useArtifactLinkHandler.ts +++ b/ui/goose2/src/features/chat/hooks/useArtifactLinkHandler.ts @@ -30,8 +30,7 @@ export function useArtifactLinkHandler() { if (!resolved.allowed) { setPathNotice( - resolved.blockedReason || - "Path is outside allowed project/artifacts roots.", + resolved.blockedReason || "Path is outside allowed roots.", ); return; } diff --git a/ui/goose2/src/features/chat/lib/__tests__/artifactPathPolicy.test.ts b/ui/goose2/src/features/chat/lib/__tests__/artifactPathPolicy.test.ts index f1ed34b1..13079223 100644 --- a/ui/goose2/src/features/chat/lib/__tests__/artifactPathPolicy.test.ts +++ b/ui/goose2/src/features/chat/lib/__tests__/artifactPathPolicy.test.ts @@ -8,11 +8,7 @@ import { resolvePathCandidate, } from "../artifactPathPolicy"; -const roots = [ - "/Users/test/project-a", - "/Users/test/project-b", - "/Users/test/.goose/artifacts", -]; +const roots = ["/Users/test/project-a", "/Users/test/project-b", "/Users/test"]; describe("artifactPathPolicy", () => { it("prefers the latest write-oriented tool call over earlier tool calls", () => { @@ -112,7 +108,7 @@ describe("artifactPathPolicy", () => { expect(allowed.allowed).toBe(true); expect(allowed.blockedReason).toBeNull(); - const blocked = evaluatePathScope("/Users/test/outside/file.md", roots); + const blocked = evaluatePathScope("/Users/other/outside/file.md", roots); expect(blocked.allowed).toBe(false); expect(blocked.blockedReason).toContain("outside allowed"); }); @@ -128,7 +124,7 @@ describe("artifactPathPolicy", () => { toolCallIndex: 0, }, ], - ["/Users/test/.goose/artifacts"], + ["/Users/test"], ); expect(ranking.primaryCandidate?.resolvedPath).toBe( @@ -146,7 +142,7 @@ describe("artifactPathPolicy", () => { args: { path: "/Users/test/Desktop/coffee_shop_inventory.csv" }, toolCallIndex: 0, }, - ["/Users/test/.goose/artifacts"], + ["/Users/test/project-a"], ); expect(candidates).toHaveLength(1); @@ -268,7 +264,7 @@ describe("artifactPathPolicy", () => { toolCallIndex: 0, }, ], - ["/Users/test", "/Users/test/.goose/artifacts"], + ["/Users/test", "/Users/test"], ); expect(ranking.primaryCandidate?.resolvedPath).toBe( @@ -301,18 +297,18 @@ describe("artifactPathPolicy", () => { }, { type: "text", - text: "The file alpha.md has been created at /Users/test/.goose/artifacts/alpha.md.", + text: "The file alpha.md has been created at /Users/test/alpha.md.", }, ], }, ], - ["/Users/test/.goose/artifacts"], + ["/Users/test"], ); const ranking = result.byMessageId.get("assistant-1"); expect(ranking?.primaryToolCallId).toBe("tool-1"); expect(ranking?.primaryCandidate?.resolvedPath).toBe( - "/Users/test/.goose/artifacts/alpha.md", + "/Users/test/alpha.md", ); expect(ranking?.primaryCandidate?.allowed).toBe(true); }); @@ -367,7 +363,7 @@ describe("artifactPathPolicy", () => { toolCallIndex: 0, }, ], - ["/Users/test/.goose/artifacts"], + ["/Users/test"], ); expect(ranking.primaryCandidate?.allowed).toBe(true); @@ -382,10 +378,7 @@ describe("artifactPathPolicy", () => { }); it("preserves Windows drive roots when resolving relative paths", () => { - const windowsRoots = [ - "C:/Users/test/project-a", - "C:/Users/test/.goose/artifacts", - ]; + const windowsRoots = ["C:/Users/test/project-a", "C:/Users/test"]; const resolved = resolvePathCandidate( "output/final_report.md", windowsRoots, diff --git a/ui/goose2/src/features/chat/lib/artifactPathPolicyCore.ts b/ui/goose2/src/features/chat/lib/artifactPathPolicyCore.ts index e227e0e2..4f95a167 100644 --- a/ui/goose2/src/features/chat/lib/artifactPathPolicyCore.ts +++ b/ui/goose2/src/features/chat/lib/artifactPathPolicyCore.ts @@ -245,11 +245,7 @@ function pickBaseRoot(allowedRoots: string[]): string | null { const normalizedRoots = allowedRoots .map((root) => normalizePath(root)) .filter(Boolean); - if (normalizedRoots.length === 0) return null; - const projectRoots = normalizedRoots.filter( - (root) => !root.includes("/.goose/artifacts"), - ); - return projectRoots[0] ?? normalizedRoots[0]; + return normalizedRoots[0] ?? null; } export function resolvePathCandidate( @@ -294,7 +290,7 @@ export function evaluatePathScope( } return { allowed: false, - blockedReason: "Path is outside allowed project/artifacts roots.", + blockedReason: "Path is outside allowed roots.", }; } diff --git a/ui/goose2/src/features/chat/ui/__tests__/ToolCallAdapter.test.tsx b/ui/goose2/src/features/chat/ui/__tests__/ToolCallAdapter.test.tsx index ba267593..49dccd7a 100644 --- a/ui/goose2/src/features/chat/ui/__tests__/ToolCallAdapter.test.tsx +++ b/ui/goose2/src/features/chat/ui/__tests__/ToolCallAdapter.test.tsx @@ -167,7 +167,7 @@ describe("ToolCallAdapter — ArtifactActions", () => { it("disables button and shows blocked reason for disallowed primary candidate", () => { const blocked = makeCandidate({ allowed: false, - blockedReason: "Path is outside allowed project/artifacts roots.", + blockedReason: "Path is outside allowed roots.", }); mockResolveToolCardDisplay.mockReturnValue({ role: "primary_host", @@ -179,7 +179,7 @@ describe("ToolCallAdapter — ArtifactActions", () => { expect(screen.getByRole("button", { name: /open file/i })).toBeDisabled(); expect( - screen.getByText("Path is outside allowed project/artifacts roots."), + screen.getByText("Path is outside allowed roots."), ).toBeInTheDocument(); }); @@ -191,7 +191,7 @@ describe("ToolCallAdapter — ArtifactActions", () => { rawPath: "/outside/secret.md", resolvedPath: "/Users/test/outside/secret.md", allowed: false, - blockedReason: "Path is outside allowed project/artifacts roots.", + blockedReason: "Path is outside allowed roots.", }); mockResolveToolCardDisplay.mockReturnValue({ role: "primary_host", @@ -205,7 +205,7 @@ describe("ToolCallAdapter — ArtifactActions", () => { const secondaryBtn = screen.getByTitle(blockedSecondary.resolvedPath); expect(secondaryBtn).toBeDisabled(); expect( - screen.getByText("Path is outside allowed project/artifacts roots."), + screen.getByText("Path is outside allowed roots."), ).toBeInTheDocument(); }); diff --git a/ui/goose2/src/features/projects/api/projects.ts b/ui/goose2/src/features/projects/api/projects.ts index 621e4288..7d338387 100644 --- a/ui/goose2/src/features/projects/api/projects.ts +++ b/ui/goose2/src/features/projects/api/projects.ts @@ -15,7 +15,6 @@ export interface ProjectInfo { archivedAt: string | null; createdAt: string; updatedAt: string; - artifactsDir: string; } export interface ProjectIconCandidate { diff --git a/ui/goose2/src/features/projects/lib/chatProjectContext.test.ts b/ui/goose2/src/features/projects/lib/chatProjectContext.test.ts index 9420a9de..b1c915e5 100644 --- a/ui/goose2/src/features/projects/lib/chatProjectContext.test.ts +++ b/ui/goose2/src/features/projects/lib/chatProjectContext.test.ts @@ -19,7 +19,6 @@ describe("chatProjectContext", () => { preferredProvider: "goose", preferredModel: "claude-sonnet-4", workingDirs: ["/Users/wesb/dev/goose2"], - artifactsDir: "/Users/wesb/.goose/projects/goose2/artifacts", useWorktrees: true, order: 0, archivedAt: null, @@ -33,7 +32,7 @@ describe("chatProjectContext", () => { "Working directories: /Users/wesb/dev/goose2", ); expect(systemPrompt).toContain( - "Artifact directory: /Users/wesb/dev/goose2/artifacts", + "Default working directory: /Users/wesb/dev/goose2", ); expect(systemPrompt).toContain("Preferred provider: goose"); expect(systemPrompt).toContain( @@ -41,7 +40,7 @@ describe("chatProjectContext", () => { ); expect(systemPrompt).toContain(""); expect(systemPrompt).toContain( - "Write newly generated files to /Users/wesb/dev/goose2/artifacts by default.", + "Use /Users/wesb/dev/goose2 as the default working directory for this project.", ); expect(systemPrompt).toContain(""); expect(systemPrompt).toContain("Always read AGENTS.md before editing."); @@ -62,18 +61,17 @@ describe("chatProjectContext", () => { expect( getProjectFolderOption({ workingDirs: ["/Users/wesb/dev/goose2", "/Users/wesb/dev/other"], - artifactsDir: "/Users/wesb/.goose/projects/goose2/artifacts", }), ).toEqual([ { - id: "/Users/wesb/dev/goose2/artifacts", - name: "artifacts", - path: "/Users/wesb/dev/goose2/artifacts", + id: "/Users/wesb/dev/goose2", + name: "goose2", + path: "/Users/wesb/dev/goose2", }, { - id: "/Users/wesb/dev/other/artifacts", - name: "artifacts", - path: "/Users/wesb/dev/other/artifacts", + id: "/Users/wesb/dev/other", + name: "other", + path: "/Users/wesb/dev/other", }, ]); }); @@ -82,30 +80,19 @@ describe("chatProjectContext", () => { expect( getProjectFolderOption({ workingDirs: [], - artifactsDir: "/Users/wesb/.goose/projects/sample-project/artifacts", }), - ).toEqual([ - { - id: "/Users/wesb/.goose/projects/sample-project/artifacts", - name: "artifacts", - path: "/Users/wesb/.goose/projects/sample-project/artifacts", - }, - ]); + ).toEqual([]); }); it("returns an empty array when project is null", () => { expect(getProjectFolderOption(null)).toEqual([]); }); - it("returns only artifact subdirectories for working dirs", () => { + it("returns working dirs unchanged", () => { expect( getProjectArtifactRoots({ workingDirs: ["/Users/wesb/dev/goose2", "/Users/wesb/dev/other"], - artifactsDir: "/Users/wesb/.goose/projects/goose2/artifacts", }), - ).toEqual([ - "/Users/wesb/dev/goose2/artifacts", - "/Users/wesb/dev/other/artifacts", - ]); + ).toEqual(["/Users/wesb/dev/goose2", "/Users/wesb/dev/other"]); }); }); diff --git a/ui/goose2/src/features/projects/lib/chatProjectContext.ts b/ui/goose2/src/features/projects/lib/chatProjectContext.ts index 5b106972..9ba9377c 100644 --- a/ui/goose2/src/features/projects/lib/chatProjectContext.ts +++ b/ui/goose2/src/features/projects/lib/chatProjectContext.ts @@ -21,53 +21,35 @@ export function getProjectFolderName(path: string): string { return parts[parts.length - 1] ?? normalized; } -function appendArtifactsSegment(path: string): string { - return `${path.replace(/[\\/]+$/, "")}/artifacts`; -} - -function resolveProjectArtifactRoots( - project: Pick | null | undefined, +function resolveProjectRoots( + project: Pick | null | undefined, ): string[] { - const workingDirs = (project?.workingDirs ?? []) + return (project?.workingDirs ?? []) .map((directory) => trimValue(directory)) .filter((directory): directory is string => directory !== null); - - if (workingDirs.length > 0) { - return workingDirs.map(appendArtifactsSegment); - } - - const artifactsDir = trimValue(project?.artifactsDir); - return artifactsDir ? [artifactsDir] : []; } export function getProjectArtifactRoots( - project: Pick | null | undefined, + project: Pick | null | undefined, ): string[] { - return resolveProjectArtifactRoots(project); + return resolveProjectRoots(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; + const workingDirs = resolveProjectRoots(project); + return workingDirs[0]; } export async function defaultGlobalArtifactRoot(): Promise { - return (await resolvePath({ parts: ["~", ".goose", "artifacts"] })).path; + return (await resolvePath({ parts: ["~"] })).path; } export function getProjectFolderOption( - project: Pick | null | undefined, + project: Pick | null | undefined, ): ProjectFolderOption[] { - return resolveProjectArtifactRoots(project).map((d) => ({ + return resolveProjectRoots(project).map((d) => ({ id: d, name: getProjectFolderName(d), path: d, @@ -81,12 +63,10 @@ export function buildProjectSystemPrompt( return undefined; } - const artifactDir = resolveProjectDefaultArtifactRoot(project); + const workingDir = resolveProjectDefaultArtifactRoot(project); const settings: string[] = [`Project name: ${project.name}`]; const description = trimValue(project.description); - const workingDirs = (project.workingDirs ?? []) - .map((d) => trimValue(d)) - .filter((d): d is string => d !== null); + const workingDirs = resolveProjectRoots(project); const prompt = trimValue(project.prompt); if (description) { @@ -95,8 +75,8 @@ export function buildProjectSystemPrompt( if (workingDirs.length > 0) { settings.push(`Working directories: ${workingDirs.join(", ")}`); } - if (artifactDir) { - settings.push(`Artifact directory: ${artifactDir}`); + if (workingDir) { + settings.push(`Default working directory: ${workingDir}`); } if (project.preferredProvider) { settings.push(`Preferred provider: ${project.preferredProvider}`); @@ -114,13 +94,12 @@ export function buildProjectSystemPrompt( `\n${settings.join("\n")}\n`, ]; - if (artifactDir) { + if (workingDir) { sections.push( `\n` + - `Write newly generated files to ${artifactDir} by default.\n` + - `When creating translations, variants, summaries, or derived documents from existing project files, save the new file in ${artifactDir} instead of the project root.\n` + - `Only write outside ${artifactDir} when the user explicitly asks you to edit or create a file at a specific path.\n` + - `If you need to read existing files elsewhere in the project, that is fine, but generated outputs should stay in ${artifactDir} unless the user says otherwise.\n` + + `Use ${workingDir} as the default working directory for this project.\n` + + `Write newly generated files relative to ${workingDir} by default.\n` + + `Only write outside ${workingDir} when the user explicitly asks you to edit or create a file at a specific path.\n` + ``, ); } diff --git a/ui/goose2/src/features/projects/lib/sessionCwdSelection.test.ts b/ui/goose2/src/features/projects/lib/sessionCwdSelection.test.ts index f25f8da1..47a071cf 100644 --- a/ui/goose2/src/features/projects/lib/sessionCwdSelection.test.ts +++ b/ui/goose2/src/features/projects/lib/sessionCwdSelection.test.ts @@ -27,7 +27,6 @@ function makeProject(overrides: Partial = {}): ProjectInfo { archivedAt: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", - artifactsDir: "", ...overrides, }; } @@ -37,70 +36,64 @@ describe("sessionCwdSelection", () => { vi.mocked(resolvePath).mockReset(); }); - it("resolves the first workspace root to the default artifact root", () => { + it("resolves the first workspace root unchanged", () => { 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"); + ).toBe("/Users/wesb/dev/goose2"); }); - it("falls back to the stored project artifact root when no workspace roots exist", () => { + it("returns undefined 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 () => { + it("returns undefined for a pathless project fallback directory", () => { + expect( + resolveProjectDefaultArtifactRoot( + makeProject({ + workingDirs: [], + }), + ), + ).toBeUndefined(); + }); + + it("falls back to home for a pathless project session cwd", async () => { vi.mocked(resolvePath).mockResolvedValue({ - path: "/Users/wesb/.goose/artifacts", + path: "/Users/wesb", }); await expect( resolveSessionCwd( makeProject({ workingDirs: [], - artifactsDir: " ", }), ), - ).resolves.toBe("/Users/wesb/.goose/artifacts"); + ).resolves.toBe("/Users/wesb"); expect(resolvePath).toHaveBeenCalledWith({ - parts: ["~", ".goose", "artifacts"], + parts: ["~"], }); }); describe("defaultGlobalArtifactRoot", () => { - it("resolves the global artifact root through the path resolver", async () => { + it("resolves the home directory through the path resolver", async () => { vi.mocked(resolvePath).mockResolvedValue({ - path: "/Users/wesb/.goose/artifacts", + path: "/Users/wesb", }); - await expect(defaultGlobalArtifactRoot()).resolves.toBe( - "/Users/wesb/.goose/artifacts", - ); + await expect(defaultGlobalArtifactRoot()).resolves.toBe("/Users/wesb"); expect(resolvePath).toHaveBeenCalledWith({ - parts: ["~", ".goose", "artifacts"], + parts: ["~"], }); }); }); diff --git a/ui/goose2/src/features/projects/lib/sessionCwdSelection.ts b/ui/goose2/src/features/projects/lib/sessionCwdSelection.ts index 064585d8..efeb543b 100644 --- a/ui/goose2/src/features/projects/lib/sessionCwdSelection.ts +++ b/ui/goose2/src/features/projects/lib/sessionCwdSelection.ts @@ -19,15 +19,10 @@ function buildSessionCwdParts( .map((directory) => trimValue(directory)) .filter((directory): directory is string => directory !== null); if (workingDirs.length > 0) { - return [workingDirs[0], "artifacts"]; + return [workingDirs[0]]; } - const artifactRoot = trimValue(project?.artifactsDir); - if (artifactRoot) { - return [artifactRoot]; - } - - return ["~", ".goose", "artifacts"]; + return ["~"]; } export async function resolveSessionCwd( diff --git a/ui/goose2/src/features/projects/ui/__tests__/CreateProjectDialog.test.tsx b/ui/goose2/src/features/projects/ui/__tests__/CreateProjectDialog.test.tsx index 89156b86..da09860f 100644 --- a/ui/goose2/src/features/projects/ui/__tests__/CreateProjectDialog.test.tsx +++ b/ui/goose2/src/features/projects/ui/__tests__/CreateProjectDialog.test.tsx @@ -106,7 +106,6 @@ function makeEditingProject(overrides: Partial = {}): ProjectInfo { archivedAt: null, createdAt: "2024-01-01", updatedAt: "2024-01-01", - artifactsDir: "/home/user/code/.goose", ...overrides, }; } diff --git a/ui/goose2/src/shared/api/__tests__/acpNotificationHandler.test.ts b/ui/goose2/src/shared/api/__tests__/acpNotificationHandler.test.ts index 5384af29..53283a56 100644 --- a/ui/goose2/src/shared/api/__tests__/acpNotificationHandler.test.ts +++ b/ui/goose2/src/shared/api/__tests__/acpNotificationHandler.test.ts @@ -53,7 +53,7 @@ describe("acpNotificationHandler", () => { "local-session", "goose-session", "goose", - "/Users/aharvard/.goose/artifacts", + "/Users/aharvard", ); setActiveMessageId("goose-session", "assistant-1"); diff --git a/ui/goose2/src/shared/api/acp.ts b/ui/goose2/src/shared/api/acp.ts index 45764178..9a07f7d7 100644 --- a/ui/goose2/src/shared/api/acp.ts +++ b/ui/goose2/src/shared/api/acp.ts @@ -226,7 +226,7 @@ export async function acpLoadSession( gooseSessionId: string, workingDir?: string, ): Promise { - const effectiveWorkingDir = workingDir ?? "~/.goose/artifacts"; + const effectiveWorkingDir = workingDir ?? "~"; const sid = sessionId.slice(0, 8); const t0 = performance.now(); const rollbackSessionRegistration = sessionTracker.registerSession( diff --git a/ui/goose2/src/shared/api/acpApi.ts b/ui/goose2/src/shared/api/acpApi.ts index 33e7d848..0c23a2a8 100644 --- a/ui/goose2/src/shared/api/acpApi.ts +++ b/ui/goose2/src/shared/api/acpApi.ts @@ -98,7 +98,7 @@ export async function forkSession(sessionId: string): Promise { const client = await getClient(); const response = await client.unstable_forkSession({ sessionId, - cwd: "~/.goose/artifacts", + cwd: "~", }); return { sessionId: response.sessionId, diff --git a/ui/goose2/src/shared/i18n/locales/en/chat.json b/ui/goose2/src/shared/i18n/locales/en/chat.json index 63a1b4d3..763a0df9 100644 --- a/ui/goose2/src/shared/i18n/locales/en/chat.json +++ b/ui/goose2/src/shared/i18n/locales/en/chat.json @@ -197,7 +197,7 @@ "openFile": "Open file", "openFolder": "Open folder", "openPath": "Open path", - "pathOutsideRoots": "Path is outside allowed project/artifacts roots.", + "pathOutsideRoots": "Path is outside allowed roots", "structuredContent": "Structured content", "structuredOutput": "Structured output", "structuredOutputLines": "{{count}} lines"