Files
cobble/src/pages/admin/settings/(partials)/extensions-group.tsx
T
tben 08d02e6033 Give bots skills: runbooks they load when a job matches
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.
2026-10-22 12:31:42 +01:00

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>
);
}