A skill is a runbook: a name, a unique slug, a one-line description of when it applies, and the markdown body. A run only ever sees the slug and description (one load_skill tool carries them), and reads the whole body only when it calls the tool, so a long runbook costs nothing until needed. Skills are the third extension kind beside MCP servers and custom tools, on the same botExtensionsSql path, so per-bot, per-workspace and all-bots grants work the same way, and attachDefaultExtensions hands an every-bot skill to new bots. load_skill is a reserved tool name, so a custom tool cannot shadow it, and a built-in wins a clash. Managed on Settings > Skills; ExtensionRow links each kind to its own page and a skill never reports a failure, since nothing connects or runs.
93 lines
3.6 KiB
TypeScript
93 lines
3.6 KiB
TypeScript
import Button from "@moduletrace/twui/layout/Button";
|
|
import Row from "@moduletrace/twui/layout/Row";
|
|
import { BookOpen, Plug, Wrench } from "lucide-react";
|
|
import ExtensionAddSelect from "@/src/components/general/extension-add-select";
|
|
import useExtensions from "@/src/hooks/use-extensions";
|
|
import type { BotExtensionsPayload } from "@/src/types";
|
|
import ExtensionRow from "./extension-row";
|
|
import SettingsGroup from "./settings-group";
|
|
import SettingsList from "./settings-list";
|
|
import SettingsStatus from "./settings-status";
|
|
|
|
type Props = {
|
|
owner: { bot_id: number } | { workspace_id: number };
|
|
owner_name: string;
|
|
description: string;
|
|
initial?: BotExtensionsPayload | null;
|
|
};
|
|
|
|
/**
|
|
* The MCP servers, tools and skills a bot or a workspace has, removable,
|
|
* and a picker to add more. On the bot and workspace settings pages.
|
|
*/
|
|
export default function ExtensionsGroup({ owner, owner_name, description, initial }: Props) {
|
|
const extensions = useExtensions({ owner, owner_name, initial });
|
|
const id = "bot_id" in owner ? `bot-${owner.bot_id}` : `workspace-${owner.workspace_id}`;
|
|
|
|
return (
|
|
<SettingsGroup id="tools" title="Tools, MCP servers and skills" description={description}>
|
|
<SettingsList is_empty={!extensions.attached.length} empty="No tools, MCP servers or skills yet.">
|
|
{extensions.attached.map((item) => (
|
|
<ExtensionRow
|
|
key={`${item.kind}:${item.id}`}
|
|
item={item}
|
|
busy={extensions.busy == `${item.kind}:${item.id}`}
|
|
onRemove={
|
|
extensions.can_manage
|
|
? () => extensions.remove({ kind: item.kind, id: item.id, name: item.name })
|
|
: undefined
|
|
}
|
|
/>
|
|
))}
|
|
</SettingsList>
|
|
{extensions.can_manage ? (
|
|
<ExtensionAddSelect
|
|
id={`${id}-add-extension`}
|
|
addable={extensions.addable}
|
|
loading={Boolean(extensions.busy)}
|
|
onAdd={extensions.add}
|
|
/>
|
|
) : null}
|
|
<Row className="gap-1!">
|
|
<Button
|
|
title="Manage MCP servers"
|
|
type="button"
|
|
variant="ghost"
|
|
color="gray"
|
|
size="small"
|
|
href="/admin/settings/mcps"
|
|
beforeIcon={<Plug size={14} aria-hidden />}
|
|
className="gap-1!"
|
|
>
|
|
MCP servers
|
|
</Button>
|
|
<Button
|
|
title="Manage tools"
|
|
type="button"
|
|
variant="ghost"
|
|
color="gray"
|
|
size="small"
|
|
href="/admin/settings/tools"
|
|
beforeIcon={<Wrench size={14} aria-hidden />}
|
|
className="gap-1!"
|
|
>
|
|
Tools
|
|
</Button>
|
|
<Button
|
|
title="Manage skills"
|
|
type="button"
|
|
variant="ghost"
|
|
color="gray"
|
|
size="small"
|
|
href="/admin/settings/skills"
|
|
beforeIcon={<BookOpen size={14} aria-hidden />}
|
|
className="gap-1!"
|
|
>
|
|
Skills
|
|
</Button>
|
|
</Row>
|
|
<SettingsStatus status={extensions.status} />
|
|
</SettingsGroup>
|
|
);
|
|
}
|