From 269aa13bba321c6ba9e367ef8a54335ef83a5fc5 Mon Sep 17 00:00:00 2001
From: arc53-machine <232052973+arc53-machine@users.noreply.github.com>
Date: Mon, 28 Sep 2026 18:45:27 +0100
Subject: [PATCH] Address review: tool group order in the agent builder, saved
S3/Reddit account
- Connection groups sit between the default and custom tools; a second
sort that did not know connection labels moved them to the top.
- Upload picks the first saved account when connections load after the
modal opened on S3 or Reddit, without overriding the user's choice.
---
frontend/src/agents/NewAgent.test.tsx | 52 +++++++++++++++++++++++++--
frontend/src/agents/NewAgent.tsx | 16 ++++-----
frontend/src/upload/Upload.test.tsx | 42 ++++++++++++++++++++++
frontend/src/upload/Upload.tsx | 25 ++++++++-----
4 files changed, 114 insertions(+), 21 deletions(-)
diff --git a/frontend/src/agents/NewAgent.test.tsx b/frontend/src/agents/NewAgent.test.tsx
index 5bd59f13..d6ffa3b8 100644
--- a/frontend/src/agents/NewAgent.test.tsx
+++ b/frontend/src/agents/NewAgent.test.tsx
@@ -27,6 +27,8 @@ const mocks = vi.hoisted(() => {
dispatch: vi.fn(),
getAgent: vi.fn(() => jsonResponse({})),
createAgent: vi.fn(() => jsonResponse({ message: 'Name is taken' }, false)),
+ tools: null as unknown[] | null,
+ connections: [] as unknown[],
};
});
const { jsonResponse } = mocks;
@@ -40,7 +42,7 @@ vi.mock('../api/services/userService', () => ({
default: {
getUserTools: () =>
jsonResponse({
- tools: [
+ tools: mocks.tools ?? [
{
id: 'tool-1',
name: 'remote_device',
@@ -69,7 +71,7 @@ vi.mock('../api/services/devicesService', () => ({
vi.mock('../api/services/connectorsService', () => ({
default: {
- listConnections: () => Promise.resolve({ connections: [] }),
+ listConnections: () => Promise.resolve({ connections: mocks.connections }),
},
}));
@@ -93,7 +95,9 @@ vi.mock('../components/MultiSelectPopover', () => ({
{trigger}
{items.map((item) => (
-
{item.descriptionNode}
+
+ {item.descriptionNode}
+
))}
),
@@ -164,6 +168,8 @@ describe('NewAgent form', () => {
mocks.dispatch.mockClear();
mocks.getAgent.mockClear();
mocks.createAgent.mockClear();
+ mocks.tools = null;
+ mocks.connections = [];
});
const render = async () => {
@@ -251,6 +257,46 @@ describe('NewAgent form', () => {
expect(description.className).toContain('sm:col-start-2');
});
+ it('lists tool groups as built-in, default, one per connection, then custom', async () => {
+ mocks.tools = [
+ { id: 'custom', name: 'api_tool', display_name: 'My API' },
+ {
+ id: 'linear',
+ name: 'mcp_tool',
+ display_name: 'Linear',
+ connection_id: 'c-lin',
+ },
+ { id: 'memory', name: 'memory', display_name: 'Memory', builtin: true },
+ {
+ id: 'notion',
+ name: 'mcp_tool',
+ display_name: 'Notion',
+ connection_id: 'c-not',
+ },
+ {
+ id: 'reader',
+ name: 'read_webpage',
+ display_name: 'Reader',
+ default: true,
+ },
+ ];
+ mocks.connections = [
+ { id: 'c-lin', name: 'Linear', account_label: 'a@x', icon: 'linear' },
+ { id: 'c-not', name: 'Notion', account_label: 'b@x', icon: 'notion' },
+ ];
+ await render();
+ const groups = Array.from(
+ container.querySelectorAll('[data-testid="picker"] [data-group]'),
+ ).map((item) => item.getAttribute('data-group'));
+ const order = groups.filter((g, i) => groups.indexOf(g) === i);
+ expect(order).toEqual([
+ 'agents.form.toolsPopup.groupBuiltin',
+ 'agents.form.toolsPopup.groupDefault',
+ 'agents.form.toolsPopup.groupConnection',
+ 'agents.form.toolsPopup.groupCustom',
+ ]);
+ });
+
it('labels every picker with a floating label', async () => {
await render();
const labels = Array.from(
diff --git a/frontend/src/agents/NewAgent.tsx b/frontend/src/agents/NewAgent.tsx
index ba2719a0..92b68320 100644
--- a/frontend/src/agents/NewAgent.tsx
+++ b/frontend/src/agents/NewAgent.tsx
@@ -636,7 +636,12 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
return t('agents.form.toolsPopup.groupCustom');
};
const tools: MultiSelectPopoverItem[] = [...visibleTools]
- .sort((a, b) => rank(a) - rank(b))
+ .sort(
+ (a, b) =>
+ rank(a) - rank(b) ||
+ // Keeps each connection's tools together.
+ (rank(a) === 2 ? groupFor(a).localeCompare(groupFor(b)) : 0),
+ )
.map((tool: UserToolType) => {
const connection = connectionOf(tool);
const base: MultiSelectPopoverItem = {
@@ -666,15 +671,6 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
}
return base;
});
- const groupOrder = [
- t('agents.form.toolsPopup.groupBuiltin'),
- t('agents.form.toolsPopup.groupDefault'),
- t('agents.form.toolsPopup.groupCustom'),
- ];
- tools.sort(
- (a, b) =>
- groupOrder.indexOf(a.group || '') - groupOrder.indexOf(b.group || ''),
- );
setUserTools(tools);
setRawUserTools(visibleTools);
};
diff --git a/frontend/src/upload/Upload.test.tsx b/frontend/src/upload/Upload.test.tsx
index 457a8777..1373ef84 100644
--- a/frontend/src/upload/Upload.test.tsx
+++ b/frontend/src/upload/Upload.test.tsx
@@ -169,6 +169,48 @@ describe('Upload source-type tiles', () => {
connectorsState.connections = [];
});
+ it('picks the saved account once connections finish loading', async () => {
+ connectorsState.catalog = [
+ {
+ key: 's3',
+ icon: 's3',
+ sync_ingestor: 's3',
+ auth_kind: 'api_key',
+ available: true,
+ missing_settings: [],
+ credential_fields: [
+ { key: 'aws_access_key_id', label: 'Access key ID', secret: false },
+ ],
+ },
+ ];
+ const props = {
+ receivedFile: [],
+ setModalState: vi.fn(),
+ isOnboarding: false,
+ renderTab: null,
+ close: vi.fn(),
+ initialIngestor: 's3' as const,
+ };
+ await act(async () => root.render());
+ const triggers = () =>
+ Array.from(
+ document.body.querySelectorAll('[data-slot="select-trigger"]'),
+ ).map((el) => el.textContent);
+ expect(triggers()).not.toContain('modals.uploadDoc.newCredentials');
+ connectorsState.connections = [
+ {
+ id: 'k1',
+ connector_key: 's3',
+ status: 'connected',
+ account_label: '…WXYZ',
+ },
+ ];
+ await act(async () => root.render());
+ expect(triggers()).toContain('settings.connectors.detail.keyEnding');
+ connectorsState.catalog = [];
+ connectorsState.connections = [];
+ });
+
it('leaves the disabled Train button on the default variant', async () => {
await render();
const crawler = tiles().find((tile) =>
diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx
index 418004c0..22c5e4e5 100644
--- a/frontend/src/upload/Upload.tsx
+++ b/frontend/src/upload/Upload.tsx
@@ -1,7 +1,7 @@
import { ChevronLeft, CircleAlert, FileText } from 'lucide-react';
import { envVar } from '@/env';
import { cn } from '@/lib/utils';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { nanoid } from '@reduxjs/toolkit';
import type { FileRejection } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
@@ -179,7 +179,10 @@ function Upload({