mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): group installed skills by source
This commit is contained in:
1 parent
693d0fb9b7
commit
6545e66e36
4 files changed
+103
-5
No files matched your search
@@ -3702,6 +3702,8 @@ body { font-size: 14px; line-height: 1.75; }
|
||||
.prefs-main { padding-top: 28px; }
|
||||
.prefs-col { max-width: 820px; }
|
||||
.grp { padding: 20px 22px 18px; }
|
||||
.prefs .skgroup + .skgroup { margin-top: 20px; }
|
||||
.prefs .skgroup > h4 { margin: 0 0 8px; font: var(--w-med) var(--prefs-body)/1.5 var(--ui); color: var(--text); overflow-wrap: anywhere; }
|
||||
|
||||
/* Progressive disclosure for source-specific compatibility. Most users only
|
||||
need address, key and models; relay-only controls remain discoverable here. */
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ExtTabs, MarketGroup } from "./Market";
|
||||
import { InstalledLocation } from "./InstalledLocation";
|
||||
import { Switch } from "./Switch";
|
||||
import { ServerRow } from "./ServerRow";
|
||||
import { SkillRow } from "./SkillRow";
|
||||
import { SkillGroups } from "./SkillRow";
|
||||
import { Hooks } from "./Hooks";
|
||||
import { Network } from "./Network";
|
||||
import { Shell as ShellPicker } from "./Shell";
|
||||
@@ -713,9 +713,7 @@ export function Settings({ hub, onError, port, networkPort, networkHost, status,
|
||||
: "模型自动发现已关闭:仅显式指定的技能会运行。开关立即生效,自下一条消息起,无需新建会话。",
|
||||
)}
|
||||
>
|
||||
{looseSkills.map((sk) => (
|
||||
<SkillRow key={sk.name} sk={sk} implicit={implicit} port={port} onDone={afterExtChange} root={scopeAt} onFailed={setFailed} />
|
||||
))}
|
||||
<SkillGroups skills={looseSkills} implicit={implicit} port={port} onDone={afterExtChange} root={scopeAt} onFailed={setFailed} />
|
||||
{extErrors.skills && <div className="rnote" data-s="bad" role="alert">{extErrors.skills} <button className="act" data-action="extensions.refresh" disabled={extRefreshing} onClick={reloadExt}>{t("重试")}</button></div>}
|
||||
{looseSkills.length === 0 && !extRefreshing && !extErrors.skills && <div className="empty">{t("当前工作目录下没有技能。")}</div>}
|
||||
</Group>
|
||||
|
||||
@@ -7,7 +7,22 @@ import { reason } from "../i18n/kernel";
|
||||
|
||||
// One skill, and the two things a reader wants from it: whether the model can
|
||||
// reach it at all, and which of the layers it was switched at.
|
||||
const SCOPE: Record<string, string> = { project: "项目", custom: "自定义", global: "我的", builtin: "内置" };
|
||||
const SCOPE: Record<string, string> = { global: "我的", project: "项目", custom: "自定义", builtin: "内置" };
|
||||
|
||||
export function SkillGroups({ skills, ...rowProps }: { skills: SkillEntry[] } & Omit<Parameters<typeof SkillRow>[0], "sk">) {
|
||||
const groups = new Map(Object.keys(SCOPE).map((scope) => [scope, [] as SkillEntry[]]));
|
||||
for (const sk of skills) {
|
||||
const scope = sk.scope ?? "";
|
||||
if (!groups.has(scope)) groups.set(scope, []);
|
||||
groups.get(scope)!.push(sk);
|
||||
}
|
||||
return <>{Array.from(groups, ([scope, rows]) => rows.length > 0 && (
|
||||
<section className="skgroup" key={scope} aria-label={t(SCOPE[scope] ?? scope) || t("其他")}>
|
||||
<h4>{t(SCOPE[scope] ?? scope) || t("其他")}</h4>
|
||||
{rows.map((sk) => <SkillRow key={sk.name} sk={sk} {...rowProps} />)}
|
||||
</section>
|
||||
))}</>;
|
||||
}
|
||||
|
||||
// Which way the model can reach this skill, and whether it can reach it at all.
|
||||
// The second was being read back off the first by comparing the sentence — a
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "./testkit";
|
||||
import { Settings } from "./Settings";
|
||||
import { MockPort } from "../port/mock";
|
||||
import { MockHub } from "../port/mock_hub";
|
||||
import type { SkillEntry } from "../port/port";
|
||||
|
||||
afterEach(() => { cleanup(); vi.restoreAllMocks(); });
|
||||
|
||||
function draw(port: MockPort) {
|
||||
return render(<Settings
|
||||
hub={new MockHub()} onError={() => {}} port={port} status={null} theme="light" reloadThemes={() => {}}
|
||||
onTheme={() => {}} contrast="" onContrast={() => {}} weight="" onWeight={() => {}}
|
||||
look={{}} onLook={() => {}} onClose={() => {}} onChanged={() => {}} at="ext:installed" account={null}
|
||||
accountUnread="" reloadAccount={() => {}}
|
||||
/>);
|
||||
}
|
||||
|
||||
const inventory: SkillEntry[] = [
|
||||
{ name: "builtin-one", scope: "builtin", enabled: true },
|
||||
{ name: "project-one", scope: "project", enabled: true },
|
||||
{ name: "mine-one", scope: "global", enabled: true, switchScope: "project" },
|
||||
{ name: "builtin-two", scope: "builtin", enabled: false },
|
||||
{ name: "mine-two", scope: "global", enabled: false },
|
||||
{ name: "package-skill", scope: "plugin", plugin: "review-kit", enabled: true },
|
||||
];
|
||||
|
||||
it("groups the mixed skill catalog by source and retains disabled rows and catalog order within a group", async () => {
|
||||
const port = new MockPort();
|
||||
vi.spyOn(port, "skills").mockResolvedValue({ implicit: true, skills: inventory });
|
||||
draw(port);
|
||||
await screen.findByText("mine-one");
|
||||
const skills = document.querySelector<HTMLElement>("#set-skills")!;
|
||||
const groups = within(skills).getAllByRole("region");
|
||||
expect(groups.map((group) => within(group).getByRole("heading").textContent)).toEqual(["我的", "项目", "内置"]);
|
||||
expect(groups.map((group) => Array.from(group.querySelectorAll(".nm"), (row) => row.textContent))).toEqual([
|
||||
["mine-one", "mine-two"], ["project-one"], ["builtin-one", "builtin-two"],
|
||||
]);
|
||||
expect(within(skills).queryByText("package-skill")).toBeNull();
|
||||
expect(skills.querySelector(".now")?.textContent).toBe("3/5 已启用");
|
||||
});
|
||||
|
||||
it("keeps source grouping independent of project overrides and preserves the skill switch and refresh", async () => {
|
||||
const port = new MockPort();
|
||||
let skills = inventory.map((skill) => ({ ...skill }));
|
||||
const read = vi.spyOn(port, "skills").mockImplementation(async () => ({ implicit: false, skills }));
|
||||
const toggle = vi.spyOn(port, "setSkillEnabled").mockImplementation(async (name, enabled) => {
|
||||
skills = skills.map((skill) => skill.name === name ? { ...skill, enabled } : skill);
|
||||
});
|
||||
draw(port);
|
||||
await screen.findByText("mine-one");
|
||||
const mine = screen.getByRole("region", { name: "我的" });
|
||||
expect(within(mine).getByRole("button", { name: "仅本项目" })).toBeTruthy();
|
||||
expect(mine.textContent).toContain("调不到");
|
||||
const control = within(mine).getByRole("switch", { name: "关闭 mine-one" });
|
||||
control.focus();
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(toggle).toHaveBeenCalledExactlyOnceWith("mine-one", false, "project", undefined);
|
||||
await waitFor(() => expect(read).toHaveBeenCalledTimes(2));
|
||||
expect(await within(mine).findByRole("switch", { name: "启用 mine-one" })).toBeTruthy();
|
||||
expect(document.activeElement).toBe(control);
|
||||
expect(document.querySelector("#set-skills .now")?.textContent).toBe("2/5 已启用");
|
||||
});
|
||||
|
||||
it("shows nonempty custom and unlabelled sources without assigning them to mine or builtin", async () => {
|
||||
const port = new MockPort();
|
||||
vi.spyOn(port, "skills").mockResolvedValue({ implicit: true, skills: [
|
||||
{ name: "custom-one", scope: "custom", enabled: true },
|
||||
{ name: "unlabelled-one", enabled: true },
|
||||
{ name: "compat-one", scope: "codex_compat", enabled: true },
|
||||
] });
|
||||
draw(port);
|
||||
await screen.findByText("custom-one");
|
||||
const pane = within(document.querySelector<HTMLElement>("#set-skills")!);
|
||||
expect(within(pane.getByRole("region", { name: "自定义" })).getByText("custom-one")).toBeTruthy();
|
||||
expect(within(pane.getByRole("region", { name: "其他" })).getByText("unlabelled-one")).toBeTruthy();
|
||||
expect(within(pane.getByRole("region", { name: "codex_compat" })).getByText("compat-one")).toBeTruthy();
|
||||
expect(pane.queryByRole("region", { name: "我的" })).toBeNull();
|
||||
expect(pane.queryByRole("region", { name: "内置" })).toBeNull();
|
||||
});
|
||||
Reference in new issue
Block a user