Files
cobble/src/hooks/use-extensions.ts
T
tbenandClaude Opus 5.5 6294b911e6 Give bots tools and MCP servers per bot and workspace, with status and tests
- Bots and workspaces get tools and MCP servers from their settings
  pages; bots also from chips under the chat composer, removable there
- "Every bot" now gives one to every bot now and every new bot; each can
  still drop it (attach-every-bot-extensions.ts moves old installs over)
- Status lines: MCP servers record whether they connect, tools whether
  their last run worked
- Test connection in the MCP form and a Test panel in the tool form check
  the form as it is, without saving
- Hide Google sign-in on the login form

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:35:13 +01:00

78 lines
2.5 KiB
TypeScript

import { useState } from "react";
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
import type { BotExtension, BotExtensionsPayload } from "@/src/types";
type Params = {
/**
* Whose they are: a bot's (`workspace_id` is where it's seen, so that
* workspace's show too) or a workspace's.
*/
owner: { bot_id: number; workspace_id?: number | null } | { workspace_id: number };
/**
* The bot's or workspace's name, for the confirmations.
*/
owner_name: string;
initial?: BotExtensionsPayload | null;
};
type Status = { error?: boolean; success?: boolean; msg?: string };
/**
* A bot's or workspace's MCP servers and tools, with adding and removing
* them (each asks first). Shared by the bot and workspace settings pages
* and the chat's composer.
*/
export default function useExtensions({ owner, owner_name, initial }: Params) {
const [data, setData] = useState<BotExtensionsPayload>(
initial || { attached: [], addable: [], can_manage: false },
);
const [busy, setBusy] = useState<string>();
const [status, setStatus] = useState<Status>();
const api = "bot_id" in owner ? "bots" : "workspaces";
const change = async ({
action,
kind,
id,
}: {
action: "add" | "remove";
kind: BotExtension["kind"];
id: number;
}) => {
setBusy(`${kind}:${id}`);
setStatus(undefined);
const res = await postAdminApi<BotExtensionsPayload>({
path: `${api}/${action}-extension`,
body: { ...owner, kind, id },
});
setBusy(undefined);
if (!res.success || !res.singleRes) {
setStatus({ error: true, msg: res.msg || "That didn't work." });
return false;
}
setData(res.singleRes);
setStatus({ success: true, msg: res.msg || "Saved." });
return true;
};
const add = async ({ kind, id }: { kind: BotExtension["kind"]; id: number }) => {
const item = data.addable.find((entry) => entry.kind == kind && entry.id == id);
if (!window.confirm(`Give ${owner_name} ${item?.name || "this"}?`)) return false;
return change({ action: "add", kind, id });
};
const remove = async ({ kind, id, name }: { kind: BotExtension["kind"]; id: number; name: string }) => {
if (!window.confirm(`Remove ${name} from ${owner_name}?`)) return false;
return change({ action: "remove", kind, id });
};
return { ...data, busy, status, add, remove };
}