diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 281c576c..1974a600 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -948,7 +948,7 @@ "packages/ui/package.json": "b1d168d0371e9094faae1107fc6c00be197f09bc69daa2247a3890d607f4b629", "packages/app/public/amico.svg": "a14b9d543d895bcdf0758f7b9ef5908ee0acaac794446494af059b159247db8f", "packages/app/public/oc-theme-preload.js": "27227e802b3494e7c545da903e679efdb30ccc754cd4eb5cdf08005a40d560b6", - "packages/app/src/app.tsx": "26db81a03c2f303701037cd2d3f0bf91b585193f01b6b31db5f869f4f8e76fc7", + "packages/app/src/app.tsx": "c4ae567006dcc0fd1a530ac5ccf837444e67534fbe08dda1dc85ca9e25d822bd", "packages/app/src/design-polish.css": "42cc6efaefe9a71dedd12fcb0bf2549453d9a46097cb025cf087064b29d3ccce", "packages/app/src/entry.tsx": "f35e1017f4c9d478d254b2a38043e5750064b6bef25169c3e07ae9f72ff1049c", "packages/app/src/index.css": "08179e06ce2d419a2d98acc96025f91c7709062ea9f3ad245e88dc35e75ff9f7", @@ -1062,7 +1062,7 @@ "packages/app/src/context/server.tsx": "08e978e4b4b3094221a2a0b92e069f39cbedb6d6cf95935881e21cde52265ee2", "packages/app/src/context/settings.tsx": "050f1831173d9f337bb00d54c034670553cd22bb457c7b536e4d799eb35de8a3", "packages/app/src/context/split.tsx": "891aeb290b369b327a63ce548dcaab1daca08e41fab430648787be7f84e53ec0", - "packages/app/src/context/tabs.tsx": "6ca4ae0d85d5456cdda6f9aaec7e6aab0ca8aef0296b61f8bae7ee51da150a0a", + "packages/app/src/context/tabs.tsx": "eb494b5785defdd3a4901874be355ba8ddd7bab4c009359b558c413f9ee63ffa", "packages/app/src/context/vault-panel.ts": "f91cb5fb49a9f0d46bd2a1b131f9a218b30147e2ad3617d2a2dc4f1e14599a53", "packages/app/src/context/workbench.tsx": "69a44da2b69989bd2c67ebffdb49941841f3f4718d6a70d659c828b57928d9eb", "packages/app/src/context/zoom-keybind.test.ts": "a1385c93639687a7dac7d21f8f02841d82a55e42465ce3836b97b16c82b52469", @@ -1107,7 +1107,7 @@ "packages/app/src/utils/amicode-hidden-project.ts": "d845a80ecad2d497528a5071f55cffab72e7b451f16d106cdb594f5bbd62c0ee", "packages/app/src/utils/amicode-pane.ts": "46c18a2dc47c5db154f70272d25031e6834539ad03f6ab25613f082426f7c642", "packages/app/src/utils/amicode-route-info.ts": "a0268a223e8de09fae9d4de8bee6a7774c14c87c6254cb4ed1d921db18587f44", - "packages/app/src/utils/amicode-workspace-projects.test.ts": "8d6f68cfec613bd470c991dcb5f49f6171606b09cbbd37c2fabd37a9129a3896", + "packages/app/src/utils/amicode-workspace-projects.test.ts": "8b5c32172e2039b3d9c52f2257023263fc846132e7c185e4a63ae014bc2e2bfb", "packages/app/src/utils/amicode-workspace-projects.ts": "cbe091c463177375c93cb43676c202bd7bf06baf36fdec779f7d641558ff6bee", "packages/app/src/utils/boot-parity.ts": "5231be428fc832cd941aafbe70e4620b696321b87a2769d28372ecd2feb9dc41", "packages/app/src/utils/chrome-dropdown.ts": "f8b878c5df4e1afd96b0751c73daaad0a8127411030724cb8f9a5089ebb29eac", diff --git a/packages/app-bundle/overlay/packages/app/src/app.tsx b/packages/app-bundle/overlay/packages/app/src/app.tsx index fa39019c..dbf3b624 100644 --- a/packages/app-bundle/overlay/packages/app/src/app.tsx +++ b/packages/app-bundle/overlay/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-bundle/overlay/packages/app/src/context/tabs.tsx b/packages/app-bundle/overlay/packages/app/src/context/tabs.tsx index 5640fc8c..1a304e82 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/tabs.tsx +++ b/packages/app-bundle/overlay/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-bundle/overlay/packages/app/src/utils/amicode-workspace-projects.test.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-workspace-projects.test.ts index 8f0614d9..980e59fd 100644 --- a/packages/app-bundle/overlay/packages/app/src/utils/amicode-workspace-projects.test.ts +++ b/packages/app-bundle/overlay/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"),