mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(studio): the extension page gets a project pill beside Add, so the project is switched in place; the settings row stays as the same control
This commit is contained in:
1 parent
5e00091026
commit
6ebff84af2
5 files changed
+70
-17
No files matched your search
@@ -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; }
|
||||
@@ -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 (
|
||||
<span className="scopeanchor">
|
||||
<button type="button" className="spill who" data-action="ext.scope" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||
{scope.label || scope.name}
|
||||
<i className="cv" aria-hidden>▾</i>
|
||||
</button>
|
||||
{open && (
|
||||
<ScopePicker
|
||||
scopes={scopes}
|
||||
at={scope.root}
|
||||
onPick={(root) => {
|
||||
setOpen(false);
|
||||
onPick(root);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="scoperow">
|
||||
@@ -34,21 +62,8 @@ export function ScopeRow({
|
||||
<span className="srow-name">{t("ext.manage_which_project")}</span>
|
||||
<span className="srow-desc">{t("ext.scope_hint")}</span>
|
||||
</div>
|
||||
<div className="srow-ctl scopeanchor">
|
||||
<button type="button" className="spill who" data-action="ext.scope" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||
{scope.label || scope.name}
|
||||
<i className="cv" aria-hidden>▾</i>
|
||||
</button>
|
||||
{open && (
|
||||
<ScopePicker
|
||||
scopes={scopes}
|
||||
at={scope.root}
|
||||
onPick={(root) => {
|
||||
setOpen(false);
|
||||
onPick(root);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="srow-ctl">
|
||||
<ScopePill scope={scope} scopes={scopes} onPick={onPick} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="scopeinfo">
|
||||
|
||||
@@ -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(<Settings
|
||||
hub={new MockHub()} onError={() => {}} 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");
|
||||
});
|
||||
@@ -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,
|
||||
<>
|
||||
<div className="ext-bar">
|
||||
<ExtTabs at={extTab} disabled={updatingPkg.applying} onPick={(tab) => { setInstalledTarget(null); setExtTab(tab); }} />
|
||||
{scope && <span className="ext-scope"><ScopePill scope={scope} scopes={scopes} onPick={setScopeAt} /></span>}
|
||||
<ExtAddMenu disabled={updatingPkg.applying} offer={new Set<ExtAddWay>(["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); }} />
|
||||
</div>
|
||||
|
||||
@@ -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(<ScopePill scope={scope()} scopes={scopes} onPick={onPick} />);
|
||||
await userEvent.click(screen.getByRole("button", { name: /app/ }));
|
||||
await userEvent.click(screen.getByRole("option", { name: /site/ }));
|
||||
expect(onPick).toHaveBeenCalledWith("/work/site");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user