diff --git a/desktop/frontend-next/src/design/settings.css b/desktop/frontend-next/src/design/settings.css
index a67cfbaece..b290015039 100644
--- a/desktop/frontend-next/src/design/settings.css
+++ b/desktop/frontend-next/src/design/settings.css
@@ -459,3 +459,8 @@
.scopeaway > span { flex: 1; min-width: 0; }
.scopeaway .act { height: 24px; padding: 0 10px; border: 1px solid var(--border); border-radius: var(--r-pill); background: transparent; color: var(--text); font: var(--w-reg) var(--fs-meta)/1 var(--ui); cursor: pointer; }
.scopeaway .act:hover { background: var(--overlay); }
+.ext-bar .ext-scope { margin-left: auto; }
+.ext-bar .ext-scope + .ext-add { margin-inline-start: 0; }
+.scopeanchor { position: relative; display: inline-block; }
+.scopeanchor .scope-pick { top: calc(100% + 4px); right: 0; left: auto; }
+.scoperow .scopeanchor .scope-pick { top: calc(100% + 4px); right: 0; left: auto; }
diff --git a/desktop/frontend-next/src/ui/CapabilityScope.tsx b/desktop/frontend-next/src/ui/CapabilityScope.tsx
index 654e2f74ed..e27c43e131 100644
--- a/desktop/frontend-next/src/ui/CapabilityScope.tsx
+++ b/desktop/frontend-next/src/ui/CapabilityScope.tsx
@@ -14,6 +14,35 @@ export function Exception({ onClear, busy }: { onClear: () => void; busy: boolea
);
}
+// The project the extension lists answer for, as one pill that opens the list of
+// projects. It sits in settings and on the extension page, so choosing is never
+// a trip to another page.
+export function ScopePill({
+ scope, scopes, onPick,
+}: {
+ scope: CapabilityScope; scopes: CapabilityScope[]; onPick: (root: string) => void;
+}) {
+ const [open, setOpen] = useState(false);
+ return (
+
+
+ {open && (
+ {
+ setOpen(false);
+ onPick(root);
+ }}
+ />
+ )}
+
+ );
+}
+
// Which folder the extension lists answer for. The shell holds several projects
// at once and the pane follows whichever one is in front, so this is chosen on
// purpose here rather than inferred from the tab in front. Worktrees of one
@@ -25,7 +54,6 @@ export function ScopeRow({
}: {
scope: CapabilityScope; scopes: CapabilityScope[]; onPick: (root: string) => void;
}) {
- const [open, setOpen] = useState(false);
const shared = scope.repo && (scope.trees ?? 1) > 1;
return (
@@ -34,21 +62,8 @@ export function ScopeRow({
{t("ext.manage_which_project")}
{t("ext.scope_hint")}
-
-
- {open && (
-
{
- setOpen(false);
- onPick(root);
- }}
- />
- )}
+
+
diff --git a/desktop/frontend-next/src/ui/Settings.scope.test.tsx b/desktop/frontend-next/src/ui/Settings.scope.test.tsx
index 71ae1e820f..b027c2a3f8 100644
--- a/desktop/frontend-next/src/ui/Settings.scope.test.tsx
+++ b/desktop/frontend-next/src/ui/Settings.scope.test.tsx
@@ -38,3 +38,24 @@ it("keeps the selected project's skills when the previous read finishes late", a
expect(screen.getByText("only-b")).toBeTruthy();
expect(screen.queryByText("only-a")).toBeNull();
});
+
+it("switches the project from the extension page itself, without a trip to another page", async () => {
+ const scopes = [
+ { root: "/projects/a", name: "Project A", key: "a", repo: true, overrides: 0, current: true },
+ { root: "/projects/b", name: "Project B", key: "b", repo: true, overrides: 0 },
+ ];
+ const port = new MockPort() as unknown as AgentPort;
+ port.capabilityScopes = vi.fn(async () => scopes) as never;
+ port.mcp = vi.fn(async (root?: string) => ({ servers: [], scope: root === scopes[1].root ? scopes[1] : scopes[0] })) as never;
+ port.skills = vi.fn(async (root?: string) => ({ implicit: true, skills: [{ name: root === scopes[1].root ? "only-b" : "only-a", enabled: true }] })) as never;
+ render(
{}} port={port} status={null} theme="" reloadThemes={() => {}}
+ onTheme={() => {}} contrast="" onContrast={() => {}} weight="" onWeight={() => {}}
+ look={{}} onLook={() => {}} onClose={() => {}} onChanged={() => {}} at="ext" account={null}
+ accountUnread="" reloadAccount={() => {}}
+ />);
+ await userEvent.click(await screen.findByRole("button", { name: /Project A/ }));
+ await userEvent.click(screen.getByRole("option", { name: /Project B/ }));
+ await screen.findByText("only-b");
+ expect(document.querySelector(".scopeaway")?.textContent).toContain("Project B");
+});
diff --git a/desktop/frontend-next/src/ui/Settings.tsx b/desktop/frontend-next/src/ui/Settings.tsx
index cafe1d3264..975bd32160 100644
--- a/desktop/frontend-next/src/ui/Settings.tsx
+++ b/desktop/frontend-next/src/ui/Settings.tsx
@@ -47,7 +47,7 @@ import { planProtocolSwitch } from "./protocolswitch";
import { Boundary } from "./Boundary";
import { DEFAULT_DAYS } from "./Usage";
import { Appearance, SCHEMES } from "./Appearance";
-import { ScopeAway, ScopeRow } from "./CapabilityScope";
+import { ScopeAway, ScopePill, ScopeRow } from "./CapabilityScope";
import { SectionCard } from "./SectionCard";
import { savedNotApplied, reason } from "../i18n/kernel";
import type { SettingsProps as Props } from "./SettingsProps";
@@ -476,6 +476,7 @@ export function Settings({ hub, onError, port, networkPort, localWorkspacePort,
<>
{ setInstalledTarget(null); setExtTab(tab); }} />
+ {scope && }
(["market", ...(updatingPkg.name ? [] : ["source", "folder"] as const), ...(live ? ["service"] as const : [])])}
onPick={(way) => { setInstalledTarget(null); setExtTab(way === "market" ? "market" : "installed"); if (way === "service") setAdding(true); else if (way !== "market") setAddingPkg(way); }} />
diff --git a/desktop/frontend-next/src/ui/scope-row.interaction.test.tsx b/desktop/frontend-next/src/ui/scope-row.interaction.test.tsx
index f4058902cf..09c636b1e0 100644
--- a/desktop/frontend-next/src/ui/scope-row.interaction.test.tsx
+++ b/desktop/frontend-next/src/ui/scope-row.interaction.test.tsx
@@ -49,3 +49,14 @@ describe("the extension page naming another project", () => {
expect(onBack).toHaveBeenCalledOnce();
});
});
+
+describe("the project pill on the extension page", () => {
+ it("opens the same list in place and hands back the chosen root", async () => {
+ const { ScopePill } = await import("./CapabilityScope");
+ const onPick = vi.fn();
+ render();
+ await userEvent.click(screen.getByRole("button", { name: /app/ }));
+ await userEvent.click(screen.getByRole("option", { name: /site/ }));
+ expect(onPick).toHaveBeenCalledWith("/work/site");
+ });
+});