diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index fa39019ce..dbf3b6244 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,7 +1,7 @@ import "@/index.css" import * as Sentry from "@sentry/solid" import { requestComputeConnect } from "@/components/amicode-defaults-capsule" -import { adoptWorkspaceProjects } from "@/utils/amicode-workspace-projects" +import { adoptWorkspaceProjects, workspaceProjects } from "@/utils/amicode-workspace-projects" import { I18nProvider } from "@opencode-ai/ui/context" import { DialogProvider } from "@opencode-ai/ui/context/dialog" import { FileComponentProvider } from "@opencode-ai/ui/context/file" @@ -482,7 +482,13 @@ function AmicodeNavigateBridge() { if (url.pathname === "/new-session") { const prompt = url.searchParams.get("prompt") || undefined const autoSend = url.searchParams.get("autoSend") === "1" - await tabs.newDraft({ server: server.key, directory: server.projects.list()[0]?.worktree ?? "" }, prompt) + // amicode#872: prefer workspace-projects (real VS Code folders) over + // the engine's project list (which returns the scaffold dir). + const wsProjects = workspaceProjects() + const directory = wsProjects.length > 0 + ? wsProjects[0].worktree + : server.projects.list()[0]?.worktree ?? "" + await tabs.newDraft({ server: server.key, directory }, prompt) if (autoSend) setPendingAutoSend(true) } else { // Navigate to an existing session by path (e.g. /session/:id) diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index 5640fc8c2..1a304e82e 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -8,6 +8,7 @@ import { ServerConnection, useServer } from "./server" import { createEffect, getOwner, onCleanup, startTransition } from "solid-js" import { useLocation, useNavigate, useParams } from "@solidjs/router" import { usePlatform } from "./platform" +import { workspaceProjects } from "@/utils/amicode-workspace-projects" import { uuid } from "@/utils/uuid" import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events" import { sessionHref } from "@/utils/session-route" @@ -60,9 +61,14 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const platform = usePlatform() /** amicode(workbench): TabsProvider sits ABOVE the SDK/Sync providers, so * a draft's project directory comes from the route or the server - * context — never from useSDK (there is no provider above us). */ + * context — never from useSDK (there is no provider above us). + * amicode#872: prefer the extension's workspace-projects signal over + * the engine's project list — the engine's first project is the scaffold + * dir, which the sidebar's orphan guard correctly rejects. */ const draftDirectory = () => { if (params.dir) return base64Decode(params.dir) ?? "" + const wsProjects = workspaceProjects() + if (wsProjects.length > 0) return wsProjects[0].worktree return server.projects.list()[0]?.worktree ?? "" } const fallback = server.key diff --git a/packages/app/src/utils/amicode-workspace-projects.test.ts b/packages/app/src/utils/amicode-workspace-projects.test.ts index 8f0614d96..980e59fdb 100644 --- a/packages/app/src/utils/amicode-workspace-projects.test.ts +++ b/packages/app/src/utils/amicode-workspace-projects.test.ts @@ -73,6 +73,79 @@ describe("prompt-project-selector — no false default (#673)", () => { }) }) +// ── #872: draft directory defaults to workspace folder ────────────────── +// Two code paths create drafts — tabs.tsx draftDirectory() and app.tsx +// AmicodeNavigateBridge. Both must prefer workspaceProjects() over +// server.projects.list() so the sidebar's orphan guard can match the path. + +describe("draft directory — tabs.tsx draftDirectory (#872)", () => { + const tabsSrc = readFileSync( + resolve(__dirname, "..", "context", "tabs.tsx"), + "utf8", + ) + + test("draftDirectory checks workspaceProjects before server.projects", () => { + // The draftDirectory function must reference workspaceProjects + const draftFn = tabsSrc.slice(tabsSrc.indexOf("const draftDirectory")) + const fnEnd = draftFn.indexOf("\n const fallback") + const fnBody = draftFn.slice(0, fnEnd) + expect(fnBody).toContain("workspaceProjects") + }) + + test("tabs.tsx imports workspaceProjects from the amicode module", () => { + expect(tabsSrc).toMatch(/import\s.*workspaceProjects.*from\s+["']@\/utils\/amicode-workspace-projects["']/) + }) + + test("draftDirectory falls back to server.projects when workspaceProjects is empty", () => { + // The fallback path must still exist + const draftFn = tabsSrc.slice(tabsSrc.indexOf("const draftDirectory")) + const fnEnd = draftFn.indexOf("\n const fallback") + const fnBody = draftFn.slice(0, fnEnd) + expect(fnBody).toContain("server.projects.list()") + }) +}) + +describe("draft directory — app.tsx navigate bridge (#872)", () => { + const appSrc = readFileSync( + resolve(__dirname, "..", "app.tsx"), + "utf8", + ) + + test("navigate bridge checks workspaceProjects before server.projects for /new-session", () => { + // Find the new-session branch in the navigate bridge + const newSessionIdx = appSrc.indexOf('url.pathname === "/new-session"') + const bridgeSlice = appSrc.slice(newSessionIdx, newSessionIdx + 600) + expect(bridgeSlice).toContain("workspaceProjects") + }) + + test("navigate bridge falls back to server.projects when workspaceProjects is empty", () => { + const newSessionIdx = appSrc.indexOf('url.pathname === "/new-session"') + const bridgeSlice = appSrc.slice(newSessionIdx, newSessionIdx + 600) + expect(bridgeSlice).toContain("server.projects.list()") + }) +}) + +describe("explicit project selection overrides the default (#872 AC4)", () => { + const ctrlSrc = readFileSync( + resolve(__dirname, "..", "pages", "session", "composer", "session-composer-controls.ts"), + "utf8", + ) + + test("selectProject writes the selected worktree directly to the draft (updateDraft)", () => { + // The explicit-selection path must call updateDraft with { directory: worktree } + // so it overrides the draftDirectory() default entirely. + const selectFn = ctrlSrc.slice(ctrlSrc.indexOf("const selectProject")) + expect(selectFn).toContain("tabs.updateDraft(search.draftId, { server: ServerConnection.key(conn), directory: worktree })") + }) + + test("selectProject does not re-invoke draftDirectory", () => { + // The explicit-selection path must not go through draftDirectory — it writes + // the selected worktree directly. + const selectFn = ctrlSrc.slice(ctrlSrc.indexOf("const selectProject")) + expect(selectFn).not.toContain("draftDirectory") + }) +}) + describe("session-composer-controls — toggle deselect (#673)", () => { const ctrlSrc = readFileSync( resolve(__dirname, "..", "pages", "session", "composer", "session-composer-controls.ts"),