Open the composer's tools in a modal behind one counting button
The chips under the composer clogged it once a bot had a handful of MCP servers and tools. They are now one button reading "N tools" (with an alert icon when one is off or not working) that opens them in a modal, where any the bot was given can still be removed and more added.
This commit is contained in:
@@ -154,7 +154,7 @@ Envs are set in Settings, under Environment (super admin only), not in `.env`. T
|
||||
- **Per-bot models:** in a workspace, its `workspace_harness_settings` row wins (`source: "workspace"`). Otherwise a worker bot inherits the defaults per harness unless it has a `bot_harness_settings` row (same fields as `harness_settings`, unique per bot and harness). `grabHarnessSetting({ harness, bot_id })` returns the bot's row (`source: "bot"`) or the default; the coordinator always gets the default. Every entry point takes `bot_id` and passes it on (text loop -> media reading/transcription), so a bot's run uses its own models throughout. Saved by `saveBotHarnessSetting` (`inherit` deletes the row); the page data is `grabBotHarnessOverview`. A bot's chat header has a ghost model icon (`(partials)/header-icon-button.tsx`; the text model and provider show under the bot's name) that opens a modal with every harness (`(partials)/bot-models-modal.tsx`); the modal and `/admin/bots/[bot_id]/models` both use `(partials)/bot-harness-form.tsx`. On the coordinator the modal edits the defaults themselves (`api/admin/harness/save-settings`, the same settings as Settings, under Models, without the providers): super admins only, the icon is disabled for everyone else. Every one of these wraps `settings/models/(partials)/harness-settings-form.tsx`: per harness a provider select first (`harness-provider-select.tsx`: any provider, one provider, or on a bot "Same as the default"), then the model select narrowed to it. Removing a provider (`deleteHarnessProvider`) puts default and bot settings that picked it or its models back on any provider (`save` prop; "Same as the default" shows when the overview item has `default`).
|
||||
- **Tools:** `HarnessTool` (built per run with its bot bound): own tools validate with a zod `schema`; tools described elsewhere pass a JSON schema as `parameters` and validate themselves. `run-tool-call.ts` reports bad calls and failures back to the model as `Error: ...`, and caps every tool call at `AppData.HarnessToolTimeoutMs` (aborted, its result handed back as an error the model can work around). Bot tools in `src/harness/tools/bot/`: `update_memory`, `update_instructions` (`grabBotTools`, every run), `start_task` and, for the coordinator with workers, `delegate_task` (`grabBotChatTools`, chat replies only; the job's `requested_by_bot_id` is the coordinator, and `relayJobToRequester` posts the worker's result or failure in its conversation, pushed as `relay` by `pushBotNotice`), `ask_user_to_take_over` (see Take-over), plus for the coordinator `create_workspace` and `update_workspace` (a new workspace with its shared context, or changing one; both also in its tasks, so a task that gathers material can write it into the workspace), `create_bot` (add a worker with its name, role and instructions, and join it to a workspace straight away) and, in its own chat, `add_bot_to_workspace` (put an existing worker in one of the user's workspaces) and `remove_bot_from_workspace` (take it out, keeping its conversation there), `assign_computer` (grant or revoke a worker's direct access, `add/remove-bot-to/from-computer`; access through a workspace stays and the result says so), `update_bot` (change a worker's name, role or core instructions, `src/functions/backend/db/bots/update-bot.ts`), `find_bot` and `find_workspace` (look the crew, or the live workspaces, up by id, name, role/description or instructions/context), `delete_bot` (a worker with its conversations, tasks, schedules and workspace places; the coordinator can't go) and `delete_workspace` (its conversations, tasks, schedules and links; its bots and computers stay). `search_computers` (`src/harness/tools/computer/search-computers-tool.ts`, chat replies and tasks) runs one read-only command on all of a bot's computers at once over the sandbox REST API (`run-computer-command.ts`), each output under its `[computer:ID]`, secrets redacted, capped by `AppData.ComputerSearch*`.
|
||||
- **MCP:** `src/harness/mcp/`: `connectMcpServer` (Streamable HTTP, SSE fallback), `grabMcpTools` (each server tool becomes a `HarnessTool` with its JSON schema), `mcpResultToText` (text as is, images read by the vision harness, `isError` thrown). Computers are reached this way: `openComputerTools` (`src/harness/tools/computer/`) connects to the computer's AIO sandbox at `grabComputerMcpUrl` (the `MAIN_COMPUTER_URL` app setting for the main computer, for a dev server outside docker, e.g. `http://localhost:8170`, else `http://<hostname>:<port>/mcp`, or its IP address when the hostname can't be reached), leaves out `ComputerMcpExcludedTools`, and sets the computer's envs (see Envs).
|
||||
- **MCPs and custom tools (extensions):** installs add their own MCP servers (`mcp_servers`, Settings, under MCPs) and tools (`custom_tools`, Settings, under Tools). A bot gets the ones given to it (`bot_mcp_servers`, `bot_custom_tools`) and, in a workspace, that workspace's (`workspace_mcp_servers`, `workspace_custom_tools`); `botExtensionsSql` (`src/utils/bot-extensions-sql.ts`, tested) resolves a run's, `grabBotExtensions` reads them. `all_bots` ("every bot by default") only decides what bots are given: turning it on gives it to every bot (`attachToAllBots`, the coordinator included) and every new bot gets it (`attachDefaultExtensions`, from `createBot` and `ensureCoordinatorBot`); turning it off takes it from no one, and a bot that drops one keeps it dropped (`src/scripts/attach-every-bot-extensions.ts`, safe to rerun, moved installs from the old "reaches every bot" meaning). Super admins define them. Super admins and admins (the super admin alone for the coordinator, `assertBotExtensionManager`) give and take them per bot on the bot's settings page and under its chat's composer (`composer-tools.tsx`: chips for everything it can use there, a workspace's not removable, and an Add modal; before them `composer-computer-button.tsx` opens the bot's current computer in the viewer), and per workspace on the workspace's settings page; the settings pages share `settings/(partials)/extensions-group.tsx`, all through `useExtensions` (`src/hooks/`), `grabBotExtensionsOverview` / `grabWorkspaceExtensionsOverview` (`bot_extensions` / `workspace_extensions` page props, both on `grabExtensionsOverview`) and `api/admin/bots/` and `api/admin/workspaces/` `add-extension`, `remove-extension` (`setBotExtension`, `setWorkspaceExtension`); the MCPs and Tools pages' Access modal sets bots and workspaces in bulk (`setExtensionAccess`, an admin never touches the coordinator's or `all_bots`). Helpers in `src/functions/backend/db/extensions/`, API in `api/admin/mcps/` and `api/admin/tools/`, zod in `src/schemas/extension-schema.ts`. `openBotExtensions` (`src/harness/tools/extensions/`) turns them into harness tools for chat replies and tasks (built-ins win a name clash; `close()` in the runners' `finally`; `note` tells the bot about a server it can't use). Templates (`fillTemplate`, `src/utils/fill-template.ts`): `{{args.x}}` and `{{env.KEY}}`, envs from `grabRunEnvs` (workspace beats general, app settings never, no need to be set on computers); secret values are redacted from every result.
|
||||
- **MCPs and custom tools (extensions):** installs add their own MCP servers (`mcp_servers`, Settings, under MCPs) and tools (`custom_tools`, Settings, under Tools). A bot gets the ones given to it (`bot_mcp_servers`, `bot_custom_tools`) and, in a workspace, that workspace's (`workspace_mcp_servers`, `workspace_custom_tools`); `botExtensionsSql` (`src/utils/bot-extensions-sql.ts`, tested) resolves a run's, `grabBotExtensions` reads them. `all_bots` ("every bot by default") only decides what bots are given: turning it on gives it to every bot (`attachToAllBots`, the coordinator included) and every new bot gets it (`attachDefaultExtensions`, from `createBot` and `ensureCoordinatorBot`); turning it off takes it from no one, and a bot that drops one keeps it dropped (`src/scripts/attach-every-bot-extensions.ts`, safe to rerun, moved installs from the old "reaches every bot" meaning). Super admins define them. Super admins and admins (the super admin alone for the coordinator, `assertBotExtensionManager`) give and take them per bot on the bot's settings page and under its chat's composer (`composer-tools.tsx`: one "N tools" button (`composer-tools-modal.tsx`) counting everything it can use there, which opens a modal listing them (a workspace's not removable) with the add picker, so ten of them don't fill the composer; beside it `composer-computer-button.tsx` opens the bot's current computer in the viewer), and per workspace on the workspace's settings page; the settings pages share `settings/(partials)/extensions-group.tsx`, all through `useExtensions` (`src/hooks/`), `grabBotExtensionsOverview` / `grabWorkspaceExtensionsOverview` (`bot_extensions` / `workspace_extensions` page props, both on `grabExtensionsOverview`) and `api/admin/bots/` and `api/admin/workspaces/` `add-extension`, `remove-extension` (`setBotExtension`, `setWorkspaceExtension`); the MCPs and Tools pages' Access modal sets bots and workspaces in bulk (`setExtensionAccess`, an admin never touches the coordinator's or `all_bots`). Helpers in `src/functions/backend/db/extensions/`, API in `api/admin/mcps/` and `api/admin/tools/`, zod in `src/schemas/extension-schema.ts`. `openBotExtensions` (`src/harness/tools/extensions/`) turns them into harness tools for chat replies and tasks (built-ins win a name clash; `close()` in the runners' `finally`; `note` tells the bot about a server it can't use). Templates (`fillTemplate`, `src/utils/fill-template.ts`): `{{args.x}}` and `{{env.KEY}}`, envs from `grabRunEnvs` (workspace beats general, app settings never, no need to be set on computers); secret values are redacted from every result.
|
||||
- **Custom tools** (`CustomToolKinds` in `src/dict/custom-tool-dict.ts`): a name (`checkCustomToolName`: snake_case, never a built-in's, `ReservedToolNames`), a description, a JSON-schema `parameters` and a `config` per kind, built by `buildCustomTool` (`src/harness/tools/custom/`): `http` (`runHttpTool`: the app fetches, arguments URL-encoded in the URL, no body template sends the arguments as JSON), `script` (`runScriptTool`: bash/python/javascript on the run's computer, the task's or in chat the bot's current one, over the sandbox REST API in a subshell, computer envs set, arguments as JSON in `COBBLE_ARGS` and on stdin; left out without a computer), `prompt` (`runPromptTool`: one `generateText` turn with the bot's models). Super admins try one from the page (`testCustomTool`, the main computer for scripts): a saved one from its row (Try), or the form as it is, saved or not (`draft`, nothing recorded), from the Test panel in its modal (`custom-tool-test-panel.tsx`). Every run (a bot's call or a try; not bad arguments or a cancel) is saved as the tool's `status`, `last_error` and `last_checked_at` (`recordCustomToolRun`); editing resets it to `untested`.
|
||||
- **MCP servers** (`McpTransports` in `src/dict/mcp-dict.ts`): `remote` (URL plus templated headers, `connectMcpServer`) or `stdio` (a command run with the host's shell, `hostShell`, in `DATA_DIR/mcp/<id>` (`grabMcpDir`), after its `setup_command`, which `runMcpSetup` reruns whenever it changes (marker file) or on Reinstall; its env is only its own `env` map plus the shell basics, `grabMcpStdioEnv`, never the app's). Their tools are listed into `tools_cache` (`syncMcpServer`: on save, Refresh, and hourly for remote ones by the `sync-mcp-servers` cron) and named `<slug>__<tool>` (`mcpToolName`); the modal's Test connection checks the form as it is, saved or not, outside the pool (`testMcpDraft`, `testMcpServer`, `api/admin/mcps/test-mcp`, a new stdio server's setup runs in `mcp/0`); a run that can't connect, or connects again after failing, updates its `status` too (`recordMcpServerConnection`); runs build tools from the cache (`mcpHarnessTool`) and connect on the first call (`openMcpServer`). stdio servers live in a per-process pool (`src/harness/mcp/mcp-stdio-pool.ts`), stopped after `AppData.McpStdioIdleMs` idle, on save or delete.
|
||||
- **Check before and after acting:** every reply and task gets `ActionCheckNote` (`src/data/harness-prompts.ts`): look for an existing record before creating one (update it instead), and verify every action took effect before saying it's done. The tools back it: `update_memory` and `update_instructions` read the save back (`checkSavedText`, `grabBotSavedText`) and fail if it didn't take; `create_bot` refuses a name already on the crew, `create_schedule` a twin of one of the bot's schedules (`findDuplicateCronJob`: same place and same title, or same timing and brief), and `start_task`/`delegate_task` a task with the same title still open in that session (`grabOpenBotTask`); `create_workspace` already refuses a name in use.
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import Modal from "@moduletrace/twui/elements/Modal";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import H2 from "@moduletrace/twui/layout/H2";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { Plus } from "lucide-react";
|
||||
import ExtensionAddSelect from "@/src/components/general/extension-add-select";
|
||||
import type { BotExtensionsPayload } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
bot_name: string;
|
||||
addable: BotExtensionsPayload["addable"];
|
||||
loading?: boolean;
|
||||
onAdd: (params: { kind: "mcp" | "tool"; id: number }) => Promise<boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The "Add" chip under the composer: picks an MCP server or tool to give
|
||||
* the bot, everywhere it works.
|
||||
*/
|
||||
export default function ComposerAddToolModal({
|
||||
bot_name,
|
||||
addable,
|
||||
loading,
|
||||
onAdd,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (!addable.length) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
target={
|
||||
<Button
|
||||
title={`Give ${bot_name} a tool or MCP server`}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
beforeIcon={<Plus size={12} aria-hidden />}
|
||||
className="gap-1! px-2! py-0.5! text-[12px] leading-4"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
targetWrapperProps={{ className: "shrink-0" }}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
className="max-w-lg bg-paper p-6"
|
||||
>
|
||||
<Stack className="w-full gap-4! items-stretch">
|
||||
<Stack className="w-full gap-1! items-stretch">
|
||||
<H2 className="text-lg! leading-6! tracking-normal! font-bold text-ink">
|
||||
Add a tool
|
||||
</H2>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{`${bot_name} can use it everywhere it works, for everyone it works for.`}
|
||||
</Span>
|
||||
</Stack>
|
||||
<ExtensionAddSelect
|
||||
id="composer-add-extension"
|
||||
addable={addable}
|
||||
loading={loading}
|
||||
onAdd={async (picked) => {
|
||||
if (await onAdd(picked)) setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { CircleAlert, X } from "lucide-react";
|
||||
import extensionIcon from "@/src/components/general/extension-icon";
|
||||
import type { BotExtension } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
item: BotExtension;
|
||||
busy?: boolean;
|
||||
/**
|
||||
* Takes it from the bot; not given for a workspace's own or when the
|
||||
* user can't.
|
||||
*/
|
||||
onRemove?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One MCP server or tool the bot can use here, under the composer. One
|
||||
* that isn't working shows an alert icon, one that's off is struck out.
|
||||
*/
|
||||
export default function ComposerToolChip({ item, busy, onRemove }: Props) {
|
||||
const Icon = item.state == "error" ? CircleAlert : extensionIcon({ kind: item.kind });
|
||||
const notes = [
|
||||
item.detail,
|
||||
item.source == "workspace" ? "From this workspace" : "",
|
||||
!item.enabled ? "Off for every bot" : item.state == "error" ? "Not working" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<Row
|
||||
title={`${item.name}: ${notes.join(" · ")}${item.description ? `\n${item.description}` : ""}`}
|
||||
className={`max-w-48 gap-1.5! flex-nowrap items-center rounded-default-lg border border-solid border-hairline bg-surface pl-2 py-0.5 text-[12px] leading-4 ${onRemove ? "pr-0.5" : "pr-2"} ${item.enabled ? "text-ink" : "text-ink-secondary line-through"}`}
|
||||
>
|
||||
<Icon size={12} aria-hidden className="shrink-0" />
|
||||
<Span className="truncate">{item.name}</Span>
|
||||
{onRemove ? (
|
||||
<Button
|
||||
title={`Remove ${item.name}`}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
className="shrink-0 p-0.5!"
|
||||
disabled={busy}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<X size={12} aria-hidden />
|
||||
</Button>
|
||||
) : null}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { X } from "lucide-react";
|
||||
import extensionIcon from "@/src/components/general/extension-icon";
|
||||
import type { BotExtension } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
item: BotExtension;
|
||||
busy?: boolean;
|
||||
/**
|
||||
* Takes it from the bot; not given for a workspace's own or when the
|
||||
* user can't.
|
||||
*/
|
||||
onRemove?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One MCP server or tool the bot can use here, in the composer's tools
|
||||
* modal. One that isn't working says so, one that's off is struck out.
|
||||
*/
|
||||
export default function ComposerToolRow({ item, busy, onRemove }: Props) {
|
||||
const Icon = extensionIcon({ kind: item.kind });
|
||||
const notes = [
|
||||
item.detail,
|
||||
item.source == "workspace" ? "From this workspace" : "",
|
||||
!item.enabled
|
||||
? "Off for every bot"
|
||||
: item.state == "error"
|
||||
? "Not working"
|
||||
: item.state == "untested"
|
||||
? "Not checked yet"
|
||||
: "",
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<Row className="w-full gap-2.5! flex-nowrap items-center px-3 py-2 border-0 border-b border-solid border-hairline last:border-b-0">
|
||||
<Icon
|
||||
size={14}
|
||||
aria-hidden
|
||||
className="shrink-0 text-ink-secondary"
|
||||
/>
|
||||
<Stack className="grow min-w-0 gap-0!">
|
||||
<Span
|
||||
title={item.description}
|
||||
className={`text-sm leading-5 truncate w-full ${item.enabled ? "text-ink" : "text-ink-secondary line-through"}`}
|
||||
>
|
||||
{item.name}
|
||||
</Span>
|
||||
{notes.length ? (
|
||||
<Span className="text-xs leading-4 text-ink-secondary truncate w-full">
|
||||
{notes.join(" · ")}
|
||||
</Span>
|
||||
) : null}
|
||||
</Stack>
|
||||
{onRemove ? (
|
||||
<Button
|
||||
title={`Remove ${item.name}`}
|
||||
aria-label={`Remove ${item.name}`}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
disabled={busy}
|
||||
onClick={onRemove}
|
||||
className="shrink-0 p-1!"
|
||||
>
|
||||
<X size={14} aria-hidden />
|
||||
</Button>
|
||||
) : null}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useState } from "react";
|
||||
import Modal from "@moduletrace/twui/elements/Modal";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import H2 from "@moduletrace/twui/layout/H2";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { CircleAlert, Wrench } from "lucide-react";
|
||||
import ExtensionAddSelect from "@/src/components/general/extension-add-select";
|
||||
import type { BotExtension, BotExtensionsPayload } from "@/src/types";
|
||||
import ComposerToolRow from "./composer-tool-row";
|
||||
|
||||
type Props = {
|
||||
bot_name: string;
|
||||
attached: BotExtension[];
|
||||
addable: BotExtensionsPayload["addable"];
|
||||
can_manage: boolean;
|
||||
/**
|
||||
* The extension being changed, as `kind:id`.
|
||||
*/
|
||||
busy?: string;
|
||||
onAdd: (params: { kind: "mcp" | "tool"; id: number }) => Promise<boolean>;
|
||||
onRemove: (params: {
|
||||
kind: "mcp" | "tool";
|
||||
id: number;
|
||||
name: string;
|
||||
}) => Promise<boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The composer's tools button: how many MCP servers and tools the bot can
|
||||
* use here, opening them in a modal rather than filling the composer with
|
||||
* chips. Super admins and admins remove any it was given (even one every
|
||||
* bot started with) and add more; a workspace's own stay.
|
||||
*/
|
||||
export default function ComposerToolsModal({
|
||||
bot_name,
|
||||
attached,
|
||||
addable,
|
||||
can_manage,
|
||||
busy,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const count = attached.length;
|
||||
const has_problem = attached.some(
|
||||
(item) => !item.enabled || item.state == "error",
|
||||
);
|
||||
const TriggerIcon = has_problem ? CircleAlert : Wrench;
|
||||
const label = count ? `${count} ${count == 1 ? "tool" : "tools"}` : "Add tools";
|
||||
|
||||
return (
|
||||
<Modal
|
||||
target={
|
||||
<Button
|
||||
title={`The tools ${bot_name} can use here`}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
beforeIcon={<TriggerIcon size={12} aria-hidden />}
|
||||
className="shrink-0 gap-1! px-2! py-0.5! text-[12px] leading-4 text-ink-secondary"
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
}
|
||||
targetWrapperProps={{ className: "shrink-0" }}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
className="max-w-lg bg-paper p-6"
|
||||
>
|
||||
<Stack className="w-full gap-1! items-stretch">
|
||||
<H2 className="text-lg! leading-6! tracking-normal! font-bold text-ink">
|
||||
{`Tools ${bot_name} can use here`}
|
||||
</H2>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{count
|
||||
? "MCP servers and tools it was given, plus this workspace's. Remove one to take it away, or add another below."
|
||||
: "It has none yet. Give it an MCP server or tool below, or make one under Settings."}
|
||||
</Span>
|
||||
</Stack>
|
||||
|
||||
{count ? (
|
||||
<Stack
|
||||
aria-label={`Tools ${bot_name} can use here`}
|
||||
className="w-full mt-4 py-1 gap-0! items-stretch rounded-default border border-solid border-hairline bg-surface"
|
||||
>
|
||||
{attached.map((item) => (
|
||||
<ComposerToolRow
|
||||
key={`${item.kind}:${item.id}`}
|
||||
item={item}
|
||||
busy={busy == `${item.kind}:${item.id}`}
|
||||
onRemove={
|
||||
can_manage && item.source == "bot"
|
||||
? () =>
|
||||
onRemove({
|
||||
kind: item.kind,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
{can_manage && addable.length ? (
|
||||
<Stack className="w-full mt-4 gap-2! items-stretch">
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{`Give ${bot_name} another one. It can use it everywhere it works, for everyone it works for.`}
|
||||
</Span>
|
||||
<ExtensionAddSelect
|
||||
id="composer-add-extension"
|
||||
addable={addable}
|
||||
loading={Boolean(busy)}
|
||||
onAdd={onAdd}
|
||||
/>
|
||||
</Stack>
|
||||
) : null}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -4,9 +4,8 @@ import Span from "@moduletrace/twui/layout/Span";
|
||||
import useExtensions from "@/src/hooks/use-extensions";
|
||||
import type { BotExtensionsPayload, BotWithLastMessage } from "@/src/types";
|
||||
import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||
import ComposerAddToolModal from "./composer-add-tool-modal";
|
||||
import ComposerComputerButton from "./composer-computer-button";
|
||||
import ComposerToolChip from "./composer-tool-chip";
|
||||
import ComposerToolsModal from "./composer-tools-modal";
|
||||
import { Dot } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
@@ -16,10 +15,9 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The MCP servers and tools the bot can use here, under the composer.
|
||||
* Super admins and admins remove any it was given (even one every bot
|
||||
* started with) and add more; a workspace's own stay. The computer the
|
||||
* bot is on sits before them, opening it in the viewer.
|
||||
* Under the composer: one button counting the MCP servers and tools the
|
||||
* bot can use here (they open in a modal, so ten of them don't fill the
|
||||
* composer), then the computer it's on, opening it in the viewer.
|
||||
*/
|
||||
export default function ComposerTools({ bot, workspace_id, initial }: Props) {
|
||||
const extensions = useExtensions({
|
||||
@@ -40,34 +38,14 @@ export default function ComposerTools({ bot, workspace_id, initial }: Props) {
|
||||
return (
|
||||
<Row className="w-full max-w-container mx-auto gap-1.5! items-center px-3 md:px-6 pb-3 -mt-3">
|
||||
{has_tools ? (
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary">
|
||||
Tools
|
||||
</Span>
|
||||
) : null}
|
||||
{extensions.attached.map((item) => (
|
||||
<ComposerToolChip
|
||||
key={`${item.kind}:${item.id}`}
|
||||
item={item}
|
||||
busy={extensions.busy == `${item.kind}:${item.id}`}
|
||||
onRemove={
|
||||
extensions.can_manage && item.source == "bot"
|
||||
? () =>
|
||||
extensions.remove({
|
||||
kind: item.kind,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{extensions.can_manage ? (
|
||||
<ComposerAddToolModal
|
||||
bot_name={bot.name || "This bot"}
|
||||
<ComposerToolsModal
|
||||
bot_name={bot.name || "this bot"}
|
||||
attached={extensions.attached}
|
||||
addable={extensions.addable}
|
||||
loading={Boolean(extensions.busy)}
|
||||
can_manage={extensions.can_manage}
|
||||
busy={extensions.busy}
|
||||
onAdd={extensions.add}
|
||||
onRemove={extensions.remove}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user