From 77cfea2b61eed95128c78e1f240baa3fe83e5076 Mon Sep 17 00:00:00 2001 From: YellowSnnowmann <167776381+YellowSnnowmann@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:40:29 +0530 Subject: [PATCH] fix(composio): show connection ID in picker when no identity cached (#3405) --- .../AddMemorySourceDialog.test.tsx | 60 +++++++++---------- .../intelligence/AddMemorySourceDialog.tsx | 19 +++--- app/src/lib/i18n/ar.ts | 1 - app/src/lib/i18n/bn.ts | 1 - app/src/lib/i18n/de.ts | 1 - app/src/lib/i18n/en.ts | 1 - app/src/lib/i18n/es.ts | 1 - app/src/lib/i18n/fr.ts | 1 - app/src/lib/i18n/hi.ts | 1 - app/src/lib/i18n/id.ts | 1 - app/src/lib/i18n/it.ts | 1 - app/src/lib/i18n/ko.ts | 1 - app/src/lib/i18n/pl.ts | 1 - app/src/lib/i18n/pt.ts | 1 - app/src/lib/i18n/ru.ts | 1 - app/src/lib/i18n/zh-CN.ts | 1 - src/openhuman/composio/ops.rs | 10 +++- src/openhuman/composio/ops_tests.rs | 24 ++++---- 18 files changed, 56 insertions(+), 71 deletions(-) diff --git a/app/src/components/intelligence/AddMemorySourceDialog.test.tsx b/app/src/components/intelligence/AddMemorySourceDialog.test.tsx index 83fcbc50e..786deb461 100644 --- a/app/src/components/intelligence/AddMemorySourceDialog.test.tsx +++ b/app/src/components/intelligence/AddMemorySourceDialog.test.tsx @@ -79,7 +79,7 @@ async function openListbox() { describe('deduplicateConnections', () => { it('returns an empty array for empty input', () => { - expect(deduplicateConnections([], 'Account')).toEqual([]); + expect(deduplicateConnections([])).toEqual([]); }); it('uses accountEmail as the identity label', () => { @@ -89,7 +89,7 @@ describe('deduplicateConnections', () => { status: 'ACTIVE', accountEmail: 'user@example.com', }; - const result = deduplicateConnections([conn], 'Account'); + const result = deduplicateConnections([conn]); expect(result).toHaveLength(1); expect(result[0].label).toBe('Gmail · user@example.com'); expect(result[0].conn.id).toBe('conn-1'); @@ -97,31 +97,31 @@ describe('deduplicateConnections', () => { it('falls back to workspace when accountEmail is absent', () => { const conn = { id: 'conn-2', toolkit: 'Slack', status: 'ACTIVE', workspace: 'my-workspace' }; - const result = deduplicateConnections([conn], 'Account'); + const result = deduplicateConnections([conn]); expect(result[0].label).toBe('Slack · my-workspace'); }); it('falls back to username when neither email nor workspace is present', () => { const conn = { id: 'conn-3', toolkit: 'GitHub', status: 'ACTIVE', username: 'octocat' }; - const result = deduplicateConnections([conn], 'Account'); + const result = deduplicateConnections([conn]); expect(result[0].label).toBe('GitHub · octocat'); }); - it('uses numbered Account label when no identity field is available', () => { + it('uses connection ID as label when no identity field is available', () => { const conn = { id: 'conn-x', toolkit: 'Notion', status: 'ACTIVE' }; - const result = deduplicateConnections([conn], 'Account'); - expect(result[0].label).toBe('Notion · Account 1'); + const result = deduplicateConnections([conn]); + expect(result[0].label).toBe('Notion · conn-x'); }); - it('numbers multiple no-identity connections per toolkit', () => { + it('shows each connection ID for multiple no-identity connections', () => { const conns = [ { id: 'conn-a', toolkit: 'Notion', status: 'ACTIVE' }, { id: 'conn-b', toolkit: 'Notion', status: 'ACTIVE' }, ]; - const result = deduplicateConnections(conns, 'Account'); + const result = deduplicateConnections(conns); expect(result).toHaveLength(2); - expect(result[0].label).toBe('Notion · Account 1'); - expect(result[1].label).toBe('Notion · Account 2'); + expect(result[0].label).toBe('Notion · conn-a'); + expect(result[1].label).toBe('Notion · conn-b'); }); it('deduplicates connections with the same toolkit and identity', () => { @@ -129,7 +129,7 @@ describe('deduplicateConnections', () => { { id: 'conn-1', toolkit: 'Gmail', status: 'ACTIVE', accountEmail: 'a@example.com' }, { id: 'conn-2', toolkit: 'Gmail', status: 'ACTIVE', accountEmail: 'a@example.com' }, ]; - const result = deduplicateConnections(conns, 'Account'); + const result = deduplicateConnections(conns); expect(result).toHaveLength(1); expect(result[0].conn.id).toBe('conn-1'); expect(result[0].label).toBe('Gmail · a@example.com'); @@ -140,32 +140,31 @@ describe('deduplicateConnections', () => { { id: 'conn-1', toolkit: 'Gmail', status: 'ACTIVE', accountEmail: 'a@example.com' }, { id: 'conn-2', toolkit: 'Gmail', status: 'ACTIVE', accountEmail: 'b@example.com' }, ]; - const result = deduplicateConnections(conns, 'Account'); + const result = deduplicateConnections(conns); expect(result).toHaveLength(2); }); - it('does not expose raw connection IDs in any label', () => { + it('uses the connection ID in the label when no identity is available', () => { const conns = [ { id: 'raw-uuid-abc123', toolkit: 'Linear', status: 'ACTIVE' }, { id: 'raw-uuid-def456', toolkit: 'Linear', status: 'ACTIVE' }, ]; - const result = deduplicateConnections(conns, 'Account'); - for (const { label } of result) { - expect(label).not.toContain('raw-uuid'); - } + const result = deduplicateConnections(conns); + expect(result[0].label).toBe('Linear · raw-uuid-abc123'); + expect(result[1].label).toBe('Linear · raw-uuid-def456'); }); - it('numbers no-identity connections per toolkit independently', () => { + it('shows connection IDs for no-identity connections across toolkits', () => { const conns = [ { id: 'n-1', toolkit: 'Notion', status: 'ACTIVE' }, { id: 's-1', toolkit: 'Slack', status: 'ACTIVE' }, { id: 'n-2', toolkit: 'Notion', status: 'ACTIVE' }, ]; - const result = deduplicateConnections(conns, 'Account'); + const result = deduplicateConnections(conns); expect(result).toHaveLength(3); - expect(result.find(r => r.conn.id === 'n-1')?.label).toBe('Notion · Account 1'); - expect(result.find(r => r.conn.id === 'n-2')?.label).toBe('Notion · Account 2'); - expect(result.find(r => r.conn.id === 's-1')?.label).toBe('Slack · Account 1'); + expect(result.find(r => r.conn.id === 'n-1')?.label).toBe('Notion · n-1'); + expect(result.find(r => r.conn.id === 'n-2')?.label).toBe('Notion · n-2'); + expect(result.find(r => r.conn.id === 's-1')?.label).toBe('Slack · s-1'); }); it('prefers ACTIVE over EXPIRED when deduplicating same toolkit+identity', () => { @@ -174,7 +173,7 @@ describe('deduplicateConnections', () => { { id: 'conn-expired', toolkit: 'Gmail', status: 'EXPIRED', accountEmail: 'x@example.com' }, { id: 'conn-active', toolkit: 'Gmail', status: 'ACTIVE', accountEmail: 'x@example.com' }, ]; - const result = deduplicateConnections(conns, 'Account'); + const result = deduplicateConnections(conns); expect(result).toHaveLength(1); expect(result[0].conn.id).toBe('conn-active'); }); @@ -185,7 +184,7 @@ describe('deduplicateConnections', () => { { id: 'conn-same', toolkit: 'Notion', status: 'ACTIVE' }, { id: 'conn-same', toolkit: 'Notion', status: 'ACTIVE' }, ]; - const result = deduplicateConnections(conns, 'Account'); + const result = deduplicateConnections(conns); expect(result).toHaveLength(1); expect(result[0].conn.id).toBe('conn-same'); }); @@ -196,7 +195,7 @@ describe('deduplicateConnections', () => { { id: 'pending', toolkit: 'Linear', status: 'PENDING', accountEmail: 'a@b.com' }, { id: 'connected', toolkit: 'Linear', status: 'CONNECTED', accountEmail: 'a@b.com' }, ]; - const result = deduplicateConnections(conns, 'Account'); + const result = deduplicateConnections(conns); expect(result).toHaveLength(1); // CONNECTED ranks same as ACTIVE — must win over EXPIRED and PENDING expect(result[0].conn.id).toBe('connected'); @@ -260,7 +259,7 @@ describe('AddMemorySourceDialog — Composio picker', () => { }); }); - it('shows numbered Account labels for connections without identity fields', async () => { + it('shows connection IDs for connections without identity fields', async () => { mockListConnections.mockResolvedValue({ connections: [ { id: 'conn-a', toolkit: 'Notion', status: 'ACTIVE' }, @@ -270,12 +269,9 @@ describe('AddMemorySourceDialog — Composio picker', () => { await openComposioStep(); await openListbox(); await waitFor(() => { - expect(screen.queryByText('Notion · Account 1')).toBeTruthy(); - expect(screen.queryByText('Notion · Account 2')).toBeTruthy(); + expect(screen.queryByText('Notion · conn-a')).toBeTruthy(); + expect(screen.queryByText('Notion · conn-b')).toBeTruthy(); }); - // Raw IDs must not appear - expect(screen.queryByText('conn-a')).toBeNull(); - expect(screen.queryByText('conn-b')).toBeNull(); }); it('auto-fills the source label when a connection is selected', async () => { diff --git a/app/src/components/intelligence/AddMemorySourceDialog.tsx b/app/src/components/intelligence/AddMemorySourceDialog.tsx index 280de882d..d17c60dc8 100644 --- a/app/src/components/intelligence/AddMemorySourceDialog.tsx +++ b/app/src/components/intelligence/AddMemorySourceDialog.tsx @@ -544,16 +544,14 @@ function statusRank(conn: ComposioConnection): number { * - Connections sharing the same toolkit + identity (accountEmail / workspace / * username) OR the same raw connection id are collapsed to the first * occurrence, preventing both labeled and identity-less duplicates. - * - Connections with no identity field get a numbered "Account N" suffix - * scoped per toolkit, so users can distinguish them without seeing raw IDs. + * - Connections with no identity field fall back to showing the raw connection ID + * so users can unambiguously distinguish accounts. */ export function deduplicateConnections( - connections: ComposioConnection[], - accountLabel: string + connections: ComposioConnection[] ): Array<{ conn: ComposioConnection; label: string }> { const sorted = [...connections].sort((a, b) => statusRank(a) - statusRank(b)); const seen = new Set(); - const unidentifiedCount: Record = {}; const result: Array<{ conn: ComposioConnection; label: string }> = []; for (const conn of sorted) { @@ -574,9 +572,9 @@ export function deduplicateConnections( seen.add(key); result.push({ conn, label: `${conn.toolkit} · ${identity}` }); } else { - unidentifiedCount[conn.toolkit] = (unidentifiedCount[conn.toolkit] ?? 0) + 1; - const n = unidentifiedCount[conn.toolkit]; - result.push({ conn, label: `${conn.toolkit} · ${accountLabel} ${n}` }); + // Fall back to the raw connection ID so the user can unambiguously + // distinguish accounts when no identity data is available. + result.push({ conn, label: `${conn.toolkit} · ${conn.id}` }); } } return result; @@ -612,9 +610,8 @@ function ComposioPicker({ const listboxRef = useRef(null); // useMemo must be declared before any early returns (Rules of Hooks). - const accountLabel = t('memorySources.connectionAccount'); const entries = useMemo(() => { - const deduped = deduplicateConnections(connections, accountLabel).map(({ conn, label }) => ({ + const deduped = deduplicateConnections(connections).map(({ conn, label }) => ({ conn, label, supported: isToolkitSupported(conn.toolkit, supportedToolkits), @@ -622,7 +619,7 @@ function ComposioPicker({ // Supported connections first so the actionable ones surface at the top; // stable within each partition (dedup already ranked by health/status). return [...deduped.filter(e => e.supported), ...deduped.filter(e => !e.supported)]; - }, [connections, accountLabel, supportedToolkits]); + }, [connections, supportedToolkits]); // Indexes of keyboard-selectable (supported) options — unsupported rows are // skipped during arrow navigation, mirroring a native