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`).
|
- **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*`.
|
- **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).
|
- **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`.
|
- **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.
|
- **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.
|
- **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 useExtensions from "@/src/hooks/use-extensions";
|
||||||
import type { BotExtensionsPayload, BotWithLastMessage } from "@/src/types";
|
import type { BotExtensionsPayload, BotWithLastMessage } from "@/src/types";
|
||||||
import { BotComputersContext } from "../(data)/bot-computers-context";
|
import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||||
import ComposerAddToolModal from "./composer-add-tool-modal";
|
|
||||||
import ComposerComputerButton from "./composer-computer-button";
|
import ComposerComputerButton from "./composer-computer-button";
|
||||||
import ComposerToolChip from "./composer-tool-chip";
|
import ComposerToolsModal from "./composer-tools-modal";
|
||||||
import { Dot } from "lucide-react";
|
import { Dot } from "lucide-react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -16,10 +15,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The MCP servers and tools the bot can use here, under the composer.
|
* Under the composer: one button counting the MCP servers and tools the
|
||||||
* Super admins and admins remove any it was given (even one every bot
|
* bot can use here (they open in a modal, so ten of them don't fill the
|
||||||
* started with) and add more; a workspace's own stay. The computer the
|
* composer), then the computer it's on, opening it in the viewer.
|
||||||
* bot is on sits before them, opening it in the viewer.
|
|
||||||
*/
|
*/
|
||||||
export default function ComposerTools({ bot, workspace_id, initial }: Props) {
|
export default function ComposerTools({ bot, workspace_id, initial }: Props) {
|
||||||
const extensions = useExtensions({
|
const extensions = useExtensions({
|
||||||
@@ -40,34 +38,14 @@ export default function ComposerTools({ bot, workspace_id, initial }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Row className="w-full max-w-container mx-auto gap-1.5! items-center px-3 md:px-6 pb-3 -mt-3">
|
<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 ? (
|
{has_tools ? (
|
||||||
<Span className="text-[12px] leading-4 text-ink-secondary">
|
<ComposerToolsModal
|
||||||
Tools
|
bot_name={bot.name || "this bot"}
|
||||||
</Span>
|
attached={extensions.attached}
|
||||||
) : 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"}
|
|
||||||
addable={extensions.addable}
|
addable={extensions.addable}
|
||||||
loading={Boolean(extensions.busy)}
|
can_manage={extensions.can_manage}
|
||||||
|
busy={extensions.busy}
|
||||||
onAdd={extensions.add}
|
onAdd={extensions.add}
|
||||||
|
onRemove={extensions.remove}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user