feat(ConfigTab): drop Skills/Tools tag inputs, give Prompt Files its own section

User feedback (2026-05-04 conversation):
> "Skills and Tools are having their own tab as plugin, and Prompt
> Files are in the file system which can be directly edited. Am I
> missing something?"
> "Tools should be merged into plugin then, and for prompt files... it
> should be in another section than in skill& tools"

The "Skills & Tools" section in ConfigTab had three TagList inputs:
  - Skills:       managed via the dedicated SkillsTab (per-workspace
                  skill folders) — duplicate UI affordance
  - Tools:        managed via the Plugins tab (install a plugin → its
                  tools become available) — duplicate UI affordance
  - Prompt Files: load order for system-prompt files — semantically
                  unrelated to skills/tools

Drop the Skills + Tools inputs. Move Prompt Files into its own
section with explanatory copy that names the auto-loaded files
(system-prompt.md, CLAUDE.md, AGENTS.md) and points users at the
Files tab for actual editing.

Schema fields `config.skills` and `config.tools` are KEPT (load-bearing
for runtime skill loading + tool registry); only the inline editor goes
away. Operators who need to edit them can still use the Raw YAML toggle.

Tests:
- New ConfigTab.sections.test.tsx with 4 cases:
  1. "Skills & Tools" section title is gone
  2. Skills tag input is absent
  3. Tools tag input is absent
  4. Prompt Files section exists with explanatory copy

Sibling ConfigTab tests (hermes, provider) all still pass (20/20).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-05-04 20:02:05 -07:00
parent daefdd21c5
commit beab899501
2 changed files with 143 additions and 4 deletions

View File

@ -886,10 +886,24 @@ export function ConfigTab({ workspaceId }: Props) {
</Section>
)}
<Section title="Skills & Tools" defaultOpen={false}>
<TagList label="Skills" values={config.skills || []} onChange={(v) => update("skills", v)} placeholder="e.g. code-review" />
<TagList label="Tools" values={config.tools || []} onChange={(v) => update("tools", v)} placeholder="e.g. web_search, filesystem" />
<TagList label="Prompt Files" values={config.prompt_files || []} onChange={(v) => update("prompt_files", v)} placeholder="e.g. system-prompt.md" />
{/* Skills + Tools used to live here as TagList inputs. They were
redundant with their dedicated tabs:
- Skills managed via SkillsTab (per-workspace skill folders)
- Tools managed via the Plugins tab (install/uninstall)
Editing them here only set the config.yaml field; the
actual install/load happened elsewhere. Removed to stop
showing the misnamed list-input affordance. */}
<Section title="Prompt Files" defaultOpen={false}>
<p className="text-[10px] text-ink-soft px-1 pb-1">
Markdown files that compose this workspace&apos;s system prompt.
Loaded in order at boot from the workspace config dir
(e.g. <code className="font-mono">system-prompt.md</code>,{' '}
<code className="font-mono">CLAUDE.md</code>,{' '}
<code className="font-mono">AGENTS.md</code>). Edit the file
contents directly via the Files tab.
</p>
<TagList label="Files (load order)" values={config.prompt_files || []} onChange={(v) => update("prompt_files", v)} placeholder="e.g. system-prompt.md" />
</Section>
<Section title="A2A Protocol" defaultOpen={false}>

View File

@ -0,0 +1,125 @@
// @vitest-environment jsdom
//
// Regression tests for the ConfigTab section restructure (user feedback
// 2026-05-04: "Skills and Tools are having their own tab as plugin, and
// Prompt Files are in the file system which can be directly edited. Am
// I missing something?" + "Tools should be merged into plugin then, and
// for prompt files... should be in another section than in skill& tools").
//
// What this pins:
// 1. The "Skills & Tools" section title is gone.
// 2. Editable Skills + Tools tag inputs are gone (managed elsewhere).
// 3. A dedicated "Prompt Files" section exists with explanatory text.
//
// If a future PR re-adds the Skills/Tools tag inputs to ConfigTab, this
// suite catches it.
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
import { render, screen, cleanup, waitFor, fireEvent } from "@testing-library/react";
import React from "react";
afterEach(cleanup);
const apiGet = vi.fn();
vi.mock("@/lib/api", () => ({
api: {
get: (path: string) => apiGet(path),
patch: vi.fn(),
put: vi.fn(),
post: vi.fn(),
del: vi.fn(),
},
}));
const storeUpdateNodeData = vi.fn();
const storeRestartWorkspace = vi.fn();
vi.mock("@/store/canvas", () => ({
useCanvasStore: Object.assign(
(selector: (s: unknown) => unknown) =>
selector({ restartWorkspace: storeRestartWorkspace, updateNodeData: storeUpdateNodeData }),
{
getState: () => ({
restartWorkspace: storeRestartWorkspace,
updateNodeData: storeUpdateNodeData,
}),
},
),
}));
vi.mock("../AgentCardSection", () => ({
AgentCardSection: () => <div data-testid="agent-card-stub" />,
}));
import { ConfigTab } from "../ConfigTab";
beforeEach(() => {
apiGet.mockReset();
apiGet.mockImplementation((path: string) => {
if (path === `/workspaces/ws-test`) {
return Promise.resolve({ runtime: "claude-code" });
}
if (path === `/workspaces/ws-test/model`) {
return Promise.resolve({ model: "claude-opus-4-7" });
}
if (path === `/workspaces/ws-test/provider`) {
return Promise.resolve({ provider: "anthropic-oauth", source: "default" });
}
if (path === `/workspaces/ws-test/files/config.yaml`) {
return Promise.resolve({ content: "name: test\nruntime: claude-code\n" });
}
if (path === "/templates") {
return Promise.resolve([
{ id: "claude-code", name: "Claude Code", runtime: "claude-code", providers: [] },
]);
}
return Promise.reject(new Error(`unmocked api.get: ${path}`));
});
});
describe("ConfigTab section restructure", () => {
it("does not render a 'Skills & Tools' section title", async () => {
render(<ConfigTab workspaceId="ws-test" />);
await waitFor(() => expect(apiGet).toHaveBeenCalled());
// Section button uses the title as its accessible name; should be absent.
expect(screen.queryByRole("button", { name: /Skills\s*&\s*Tools/i })).toBeNull();
});
it("does not render an editable Skills tag input", async () => {
render(<ConfigTab workspaceId="ws-test" />);
await waitFor(() => expect(apiGet).toHaveBeenCalled());
// TagList renders its label; check no input labelled "Skills" in the form.
// (Skills are managed via the dedicated Skills tab.)
const skillsLabels = screen
.queryAllByText(/^Skills$/)
.filter((el) => el.tagName.toLowerCase() === "label");
expect(skillsLabels).toHaveLength(0);
});
it("does not render an editable Tools tag input", async () => {
render(<ConfigTab workspaceId="ws-test" />);
await waitFor(() => expect(apiGet).toHaveBeenCalled());
// Tools are managed via the Plugins tab — install a plugin → its tools
// become available. No reason to type tool names here.
const toolsLabels = screen
.queryAllByText(/^Tools$/)
.filter((el) => el.tagName.toLowerCase() === "label");
expect(toolsLabels).toHaveLength(0);
});
it("renders a dedicated 'Prompt Files' section with explanatory copy", async () => {
render(<ConfigTab workspaceId="ws-test" />);
await waitFor(() => expect(apiGet).toHaveBeenCalled());
// Section is collapsed by default — find + expand first.
const sectionButton = screen.getByRole("button", { name: /Prompt Files/i });
expect(sectionButton).toBeTruthy();
fireEvent.click(sectionButton);
// Explanatory copy mentions system-prompt.md (split across <code> tags
// so use textContent on any element rather than the default text matcher).
await waitFor(() => {
const matches = screen.queryAllByText((_, el) =>
(el?.textContent || "").includes("system-prompt.md"),
);
expect(matches.length).toBeGreaterThan(0);
});
});
});