diff --git a/frontend/src/settings/Tools.test.tsx b/frontend/src/settings/Tools.test.tsx
index 2a484cc1..dc9180f9 100644
--- a/frontend/src/settings/Tools.test.tsx
+++ b/frontend/src/settings/Tools.test.tsx
@@ -102,6 +102,16 @@ vi.mock('../api/services/connectorsService', () => ({
},
}));
+// The connector panel itself is tested on its own; here it only has to open
+// for the right service.
+vi.mock('../connectors/ConnectionDrawer', () => ({
+ default: ({ connector }: { connector: { key: string } | null }) =>
+ connector ?
{connector.key}
: null,
+}));
+vi.mock('../connectors/useConnectorLauncher', () => ({
+ default: () => ({ launch: vi.fn(), modals: null }),
+}));
+
import connectorsReducer from '../connectors/connectorsSlice';
import notificationsReducer from '../notifications/notificationsSlice';
import Tools from './Tools';
@@ -169,7 +179,12 @@ describe('Tools page', () => {
document.body.querySelectorAll('[role="menuitem"]'),
).find((item) => item.textContent?.includes(text));
- it('sends a connected tool to its connector page to manage it', async () => {
+ const card = (title: string) =>
+ Array.from(container.querySelectorAll('h2'))
+ .find((h) => h.textContent === title)!
+ .closest('[data-slot="card"]')!;
+
+ it('opens the connection panel right here to manage a connected tool', async () => {
await render();
await openMenu(0);
expect(menuItem('settings.tools.edit')).toBeUndefined();
@@ -177,30 +192,26 @@ describe('Tools page', () => {
menuItem('settings.connectors.manageConnection')!.click(),
);
expect(
- document.body.querySelector('[data-testid="where"]')?.textContent,
- ).toBe('/settings/connectors?connector=telegram');
+ document.body.querySelector('[data-testid="drawer"]')?.textContent,
+ ).toBe('telegram');
+ // Stays on the Tools page.
+ expect(document.body.querySelector('[data-testid="where"]')).toBeNull();
});
- const card = (title: string) =>
- Array.from(container.querySelectorAll('h2'))
- .find((h) => h.textContent === title)!
- .closest('[data-slot="card"]')!;
-
- it('manages a connected tool on its connector page, not with its own switch', async () => {
+ it('shows each connected account as its own tool with its own switch', async () => {
await render();
const telegram = card('Telegram');
- expect(telegram.querySelector('[role="switch"]')).toBeNull();
- // The catalog's plain description, not the tool's setup notes.
+ expect(telegram.querySelector('[role="switch"]')).not.toBeNull();
+ // Which account this card is, and the catalog's plain description.
+ expect(telegram.textContent).toContain('…abcd');
expect(telegram.textContent).toContain(
'settings.connectors.descriptions.telegram',
);
- const manage = Array.from(telegram.querySelectorAll('button')).find(
- (b) => b.textContent === 'settings.connectors.manageConnection',
- )!;
- await act(async () => manage.click());
expect(
- document.body.querySelector('[data-testid="where"]')?.textContent,
- ).toBe('/settings/connectors?connector=telegram');
+ Array.from(telegram.querySelectorAll('button')).some(
+ (b) => b.textContent === 'settings.connectors.manageConnection',
+ ),
+ ).toBe(false);
});
it('says a connected tool needs signing in again, with no raw MCP reconnect', async () => {
diff --git a/frontend/src/settings/Tools.tsx b/frontend/src/settings/Tools.tsx
index 16410868..82e38d50 100644
--- a/frontend/src/settings/Tools.tsx
+++ b/frontend/src/settings/Tools.tsx
@@ -21,6 +21,7 @@ import {
import { Switch } from '../components/ui/switch';
import { ActionMenu, type MenuOption } from '../components/ui/dropdown-menu';
import { EmptyState } from '../components/ui/empty-state';
+import ConnectionDrawer from '../connectors/ConnectionDrawer';
import ConnectorIcon from '../connectors/ConnectorIcon';
import {
connectionNeedsSignIn,
@@ -29,6 +30,8 @@ import {
selectConnectorCatalog,
} from '../connectors/connectorsSlice';
import { connectorDescription } from '../connectors/i18n';
+import type { Connection, ConnectorDefinition } from '../connectors/types';
+import useConnectorLauncher from '../connectors/useConnectorLauncher';
import { useLoaderState } from '../hooks';
import type { AvailableToolType } from '../modals/types';
import AddToolModal from '../modals/AddToolModal';
@@ -72,6 +75,10 @@ export default function Tools() {
const [toolToShare, setToolToShare] = React.useState(
null,
);
+ // The connection panel, opened here for a connected tool's service.
+ const [managed, setManaged] = React.useState(
+ null,
+ );
const [mcpStatuses, setMcpStatuses] = React.useState<{
[toolId: string]: string;
}>({});
@@ -254,14 +261,29 @@ export default function Tools() {
const handleSettingsClick = (tool: UserToolType) => {
const connection = connectionOf(tool);
if (connection) {
- navigate(
- `/settings/connectors?connector=${encodeURIComponent(connection.connector_key)}`,
- );
+ const connector = catalog.find((c) => c.key === connection.connector_key);
+ if (connector) setManaged(connector);
+ else
+ navigate(
+ `/settings/connectors?connector=${encodeURIComponent(connection.connector_key)}`,
+ );
return;
}
setSelectedTool(tool);
};
+ const { launch, modals } = useConnectorLauncher({
+ onConnected: () => getUserTools(),
+ });
+ // What tells two accounts of one service apart on their cards.
+ const accountLine = (connection: Connection) =>
+ connection.auth_kind === 'api_key'
+ ? t('settings.connectors.detail.keyEnding', {
+ hint: connection.account_label,
+ interpolation: { escapeValue: false },
+ })
+ : connection.account_label;
+
const handleGoBack = () => {
setSelectedTool(null);
getUserTools();
@@ -447,15 +469,11 @@ export default function Tools() {
{t('settings.tools.builtIn')}
)}
- {connectionNeedsSignIn(connection) ? (
+ {connectionNeedsSignIn(connection) && (
{t('settings.connectors.health.signInAgain')}
- ) : !tool.status ? (
-
- {t('settings.tools.off')}
-
- ) : null}
+ )}
{tool.name === 'mcp_tool' &&
!connection &&
mcpStatuses[tool.id] && (
@@ -509,37 +527,36 @@ export default function Tools() {
{connection && (
- // The way to the connection's page, marked with
- // the service it comes from.
+ // Which account this is: each account of a
+ // service is its own tool. mr-12 keeps the line
+ // clear of the switch.
-
+
+
+
+ {accountLine(connection)}
+
+
)}
- {/* A connected tool is turned on and off on its
- connection's page, the one home for it. */}
- {!connection && (
-