From f4397a7a652b76ebb4015ffed760d1f2142a429e Mon Sep 17 00:00:00 2001
From: arc53-machine <232052973+arc53-machine@users.noreply.github.com>
Date: Tue, 29 Sep 2026 10:05:41 +0100
Subject: [PATCH] Show each connected account as its own tool, managed in place
A connected tool's card has its switch again and names its account, so
two accounts of one service are two tools you can turn on and off. Manage
connection in its menu opens the connection panel on the Tools page
instead of leaving for Connectors.
---
frontend/src/settings/Tools.test.tsx | 45 +++++++-----
frontend/src/settings/Tools.tsx | 104 +++++++++++++++++----------
2 files changed, 95 insertions(+), 54 deletions(-)
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 && (
-