From 505b04783bd0b38d618b584dd7ba932013d8e09e Mon Sep 17 00:00:00 2001 From: YellowSnnowmann <167776381+YellowSnnowmann@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:12:23 +0530 Subject: [PATCH] fix(intelligence): deduplicate and label connections in integration source picker (#3361) --- .../AddMemorySourceDialog.test.tsx | 279 ++++++++++++++++++ .../intelligence/AddMemorySourceDialog.tsx | 95 +++++- 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 + 16 files changed, 375 insertions(+), 13 deletions(-) create mode 100644 app/src/components/intelligence/AddMemorySourceDialog.test.tsx diff --git a/app/src/components/intelligence/AddMemorySourceDialog.test.tsx b/app/src/components/intelligence/AddMemorySourceDialog.test.tsx new file mode 100644 index 000000000..a75d88ba2 --- /dev/null +++ b/app/src/components/intelligence/AddMemorySourceDialog.test.tsx @@ -0,0 +1,279 @@ +/** + * Tests for the AddMemorySourceDialog — focused on the Composio connection + * picker: deduplication, readable labels, and no raw connection IDs in the + * rendered dropdown (issue #3356). + */ +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { listConnections } from '../../lib/composio/composioApi'; +import { renderWithProviders } from '../../test/test-utils'; +import { AddMemorySourceDialog, deduplicateConnections } from './AddMemorySourceDialog'; + +// --------------------------------------------------------------------------- +// Module mocks +// --------------------------------------------------------------------------- + +vi.mock('../../lib/composio/composioApi', () => ({ listConnections: vi.fn() })); + +vi.mock('../../services/memorySourcesService', () => ({ + addMemorySource: vi.fn(), + SOURCE_KIND_ICONS: { + folder: '📁', + composio: '🔗', + github_repo: '🐙', + rss_feed: '📡', + web_page: '🌐', + twitter_query: '🐦', + }, + SOURCE_KIND_LABEL_KEYS: { + folder: 'memorySources.kind.folder', + composio: 'memorySources.kind.composio', + github_repo: 'memorySources.kind.github_repo', + rss_feed: 'memorySources.kind.rss_feed', + web_page: 'memorySources.kind.web_page', + twitter_query: 'memorySources.kind.twitter_query', + }, +})); + +const mockListConnections = listConnections as ReturnType; + +// --------------------------------------------------------------------------- +// Helper +// --------------------------------------------------------------------------- + +function renderDialog() { + const onClose = vi.fn(); + const onAdded = vi.fn(); + renderWithProviders(); + return { onClose, onAdded }; +} + +async function openComposioStep() { + renderDialog(); + // The i18n context renders the real English string from en.ts + const integrationBtn = screen.getByText('Integration'); + fireEvent.click(integrationBtn); + // Wait for async connection fetch + await waitFor(() => expect(mockListConnections).toHaveBeenCalledTimes(1)); +} + +// --------------------------------------------------------------------------- +// Unit tests: deduplicateConnections helper +// --------------------------------------------------------------------------- + +describe('deduplicateConnections', () => { + it('returns an empty array for empty input', () => { + expect(deduplicateConnections([], 'Account')).toEqual([]); + }); + + it('uses accountEmail as the identity label', () => { + const conn = { + id: 'conn-1', + toolkit: 'Gmail', + status: 'ACTIVE', + accountEmail: 'user@example.com', + }; + const result = deduplicateConnections([conn], 'Account'); + expect(result).toHaveLength(1); + expect(result[0].label).toBe('Gmail · user@example.com'); + expect(result[0].conn.id).toBe('conn-1'); + }); + + 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'); + 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'); + expect(result[0].label).toBe('GitHub · octocat'); + }); + + it('uses numbered Account 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'); + }); + + it('numbers multiple no-identity connections per toolkit', () => { + const conns = [ + { id: 'conn-a', toolkit: 'Notion', status: 'ACTIVE' }, + { id: 'conn-b', toolkit: 'Notion', status: 'ACTIVE' }, + ]; + const result = deduplicateConnections(conns, 'Account'); + expect(result).toHaveLength(2); + expect(result[0].label).toBe('Notion · Account 1'); + expect(result[1].label).toBe('Notion · Account 2'); + }); + + it('deduplicates connections with the same toolkit and identity', () => { + const conns = [ + { 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'); + expect(result).toHaveLength(1); + expect(result[0].conn.id).toBe('conn-1'); + expect(result[0].label).toBe('Gmail · a@example.com'); + }); + + it('keeps connections with the same toolkit but different identities', () => { + const conns = [ + { 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'); + expect(result).toHaveLength(2); + }); + + it('does not expose raw connection IDs in any label', () => { + 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'); + } + }); + + it('numbers no-identity connections per toolkit independently', () => { + 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'); + 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'); + }); + + it('prefers ACTIVE over EXPIRED when deduplicating same toolkit+identity', () => { + // Backend returns EXPIRED first — the ACTIVE one should win + const conns = [ + { 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'); + expect(result).toHaveLength(1); + expect(result[0].conn.id).toBe('conn-active'); + }); + + it('deduplicates identity-less connections with the same conn.id', () => { + // Same connection returned twice with no identity — must not produce duplicate React keys + const conns = [ + { id: 'conn-same', toolkit: 'Notion', status: 'ACTIVE' }, + { id: 'conn-same', toolkit: 'Notion', status: 'ACTIVE' }, + ]; + const result = deduplicateConnections(conns, 'Account'); + expect(result).toHaveLength(1); + expect(result[0].conn.id).toBe('conn-same'); + }); + + it('sorts CONNECTED equal to ACTIVE above PENDING and EXPIRED', () => { + const conns = [ + { id: 'exp', toolkit: 'Linear', status: 'EXPIRED', accountEmail: 'a@b.com' }, + { 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'); + expect(result).toHaveLength(1); + // CONNECTED ranks same as ACTIVE — must win over EXPIRED and PENDING + expect(result[0].conn.id).toBe('connected'); + }); +}); + +// --------------------------------------------------------------------------- +// Component tests: ComposioPicker inside the dialog +// --------------------------------------------------------------------------- + +describe('AddMemorySourceDialog — Composio picker', () => { + beforeEach(() => { + mockListConnections.mockReset(); + }); + + it('shows loading state while fetching connections', async () => { + // Never resolves during this test + mockListConnections.mockReturnValue(new Promise(() => {})); + renderDialog(); + fireEvent.click(screen.getByText('Integration')); + await waitFor(() => expect(screen.queryByText('Loading connections…')).toBeTruthy()); + }); + + it('shows no-connections message when list is empty', async () => { + mockListConnections.mockResolvedValue({ connections: [] }); + await openComposioStep(); + await waitFor(() => + expect( + screen.queryByText('No active Composio connections found. Connect an integration first.') + ).toBeTruthy() + ); + }); + + it('renders readable labels — toolkit · identity — not raw IDs', async () => { + mockListConnections.mockResolvedValue({ + connections: [ + { id: 'raw-id-xyz', toolkit: 'Gmail', status: 'ACTIVE', accountEmail: 'user@gmail.com' }, + ], + }); + await openComposioStep(); + await waitFor(() => expect(screen.queryByText('Gmail · user@gmail.com')).toBeTruthy()); + expect(screen.queryByText('raw-id-xyz')).toBeNull(); + }); + + it('deduplicates same toolkit+identity connections in the dropdown', async () => { + mockListConnections.mockResolvedValue({ + connections: [ + { id: 'conn-1', toolkit: 'Gmail', status: 'ACTIVE', accountEmail: 'x@example.com' }, + { id: 'conn-2', toolkit: 'Gmail', status: 'ACTIVE', accountEmail: 'x@example.com' }, + ], + }); + await openComposioStep(); + await waitFor(() => { + const options = screen.getAllByRole('option'); + const gmailOptions = options.filter(o => o.textContent?.includes('Gmail · x@example.com')); + expect(gmailOptions).toHaveLength(1); + }); + }); + + it('shows numbered Account labels for connections without identity fields', async () => { + mockListConnections.mockResolvedValue({ + connections: [ + { id: 'conn-a', toolkit: 'Notion', status: 'ACTIVE' }, + { id: 'conn-b', toolkit: 'Notion', status: 'ACTIVE' }, + ], + }); + await openComposioStep(); + await waitFor(() => { + expect(screen.queryByText('Notion · Account 1')).toBeTruthy(); + expect(screen.queryByText('Notion · Account 2')).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 () => { + mockListConnections.mockResolvedValue({ + connections: [ + { id: 'conn-1', toolkit: 'Slack', status: 'ACTIVE', workspace: 'my-workspace' }, + ], + }); + await openComposioStep(); + await waitFor(() => expect(screen.queryByText('Slack · my-workspace')).toBeTruthy()); + + const select = screen.getByRole('combobox'); + fireEvent.change(select, { target: { value: 'conn-1' } }); + + // The label field should be auto-filled + await waitFor(() => { + const labelInput = screen.getByPlaceholderText('My research notes'); + expect((labelInput as HTMLInputElement).value).toBe('Slack · my-workspace'); + }); + }); +}); diff --git a/app/src/components/intelligence/AddMemorySourceDialog.tsx b/app/src/components/intelligence/AddMemorySourceDialog.tsx index dfe8f624c..5277c5160 100644 --- a/app/src/components/intelligence/AddMemorySourceDialog.tsx +++ b/app/src/components/intelligence/AddMemorySourceDialog.tsx @@ -8,7 +8,7 @@ * presents them as a dropdown — the user picks an existing OAuth * connection rather than typing toolkit + connection_id. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { listConnections } from '../../lib/composio/composioApi'; import type { ComposioConnection } from '../../lib/composio/types'; @@ -487,6 +487,73 @@ function KindFields(props: KindFieldsProps) { } } +/** Active-first status rank — lower is better. */ +const STATUS_RANK: Record = { + ACTIVE: 0, + CONNECTED: 0, + PENDING: 1, + INITIATED: 1, + INITIALIZING: 1, + EXPIRED: 2, + FAILED: 3, + ERROR: 3, +}; + +function statusRank(conn: ComposioConnection): number { + return STATUS_RANK[conn.status.toUpperCase()] ?? 2; +} + +/** + * Deduplicates and labels connections for display in the picker. + * + * - Sorts by status rank first (ACTIVE/CONNECTED before EXPIRED/FAILED) so + * that when two connections share the same toolkit + identity, the healthier + * one wins rather than the first-returned one. + * - 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. + */ +export function deduplicateConnections( + connections: ComposioConnection[], + accountLabel: string +): 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) { + // Always dedup by raw connection id to guard against identity-less dupes. + if (seen.has(conn.id)) { + console.debug( + `[ui-flow][composio-picker] dropping duplicate connection toolkit=${conn.toolkit} id=${conn.id}` + ); + continue; + } + seen.add(conn.id); + + const identity = conn.accountEmail ?? conn.workspace ?? conn.username; + if (identity) { + const key = `${conn.toolkit}:${identity}`; + if (seen.has(key)) { + console.debug( + `[ui-flow][composio-picker] dropping duplicate connection toolkit=${conn.toolkit} id=${conn.id}` + ); + continue; + } + 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}` }); + } + } + return result; +} + function ComposioPicker({ connections, loadingConnections, @@ -494,6 +561,12 @@ function ComposioPicker({ setConnection, }: KindFieldsProps) { const { t } = useT(); + // useMemo must be declared before any early returns (Rules of Hooks). + const accountLabel = t('memorySources.connectionAccount'); + const dedupedConnections = useMemo( + () => deduplicateConnections(connections, accountLabel), + [connections, accountLabel] + ); if (loadingConnections) { return ( @@ -519,10 +592,9 @@ function ComposioPicker({ ); diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index e1a4f81f9..b11e349a9 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -1929,6 +1929,7 @@ const messages: TranslationMap = { 'لا توجد مصادر عرفية بعد أضف ملفاً، ×1xx repo، ×x0x مكعب، أو صفحة على الشبكة للبدء.', 'memorySources.loadingConnections': 'علاقات الحب...', 'memorySources.noConnections': 'لم يتم العثور على أي وصلات اكسكساكسية نشطة. إجمع التكامل أولاً', + 'memorySources.connectionAccount': 'الحساب', 'memorySources.pickConnection': 'اختر اتصال', 'memorySources.selectConnection': '- اختيار اتصال -', 'memorySources.composioListFailed': 'فشل في تحميل الأتصالات Xqx0x.', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index b7884d1bf..5a6e782ab 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -1968,6 +1968,7 @@ const messages: TranslationMap = { 'কোনো স্বনির্ধারিত উৎস পাওয়া যায়নি। একটি ফোল্ডার যোগ করুন, xqx1x rex, xqxqx pxkx ফিড, অথবা ওয়েব পেজ আরম্ভ করুন।', 'memorySources.loadingConnections': 'সংযোগ লোড করা হচ্ছে...', 'memorySources.noConnections': 'কোনো সক্রিয় xqxqx সংযোগ পাওয়া যায়নি। একটা সম্পর্ক প্রথমে।', + 'memorySources.connectionAccount': 'অ্যাকাউন্ট', 'memorySources.pickConnection': 'একটি সংযোগ নির্বাচন করুন', 'memorySources.selectConnection': '- একটি সংযোগ নির্বাচন করুন -', 'memorySources.composioListFailed': 'Xqxqx সংযোগ লোড করতে ব্যর্থ।', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index b193a84c0..1f602f114 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -2019,6 +2019,7 @@ const messages: TranslationMap = { 'memorySources.loadingConnections': 'Verbindungen werden geladen...', 'memorySources.noConnections': 'Keine aktiven Composio-Verbindungen gefunden. Verbinden Sie zuerst eine Integration.', + 'memorySources.connectionAccount': 'Konto', 'memorySources.pickConnection': 'Wählen Sie eine Verbindung', 'memorySources.selectConnection': 'Wählen Sie eine Verbindung aus.', 'memorySources.composioListFailed': 'Fehler beim Laden der Composio-Verbindungen.', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 73a4da931..6a2c70ed9 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -2298,6 +2298,7 @@ const en: TranslationMap = { 'memorySources.loadingConnections': 'Loading connections…', 'memorySources.noConnections': 'No active Composio connections found. Connect an integration first.', + 'memorySources.connectionAccount': 'Account', 'memorySources.pickConnection': 'Pick a connection', 'memorySources.selectConnection': '— Select a connection —', 'memorySources.composioListFailed': 'Failed to load Composio connections.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 2fc099241..e246a3c0a 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -2011,6 +2011,7 @@ const messages: TranslationMap = { 'memorySources.loadingConnections': 'Cargando conexiones…', 'memorySources.noConnections': 'No se encontraron conexiones activas de Composio. Conecta una integración primero.', + 'memorySources.connectionAccount': 'Cuenta', 'memorySources.pickConnection': 'Elige una conexión', 'memorySources.selectConnection': '— Seleccionar una conexión —', 'memorySources.composioListFailed': 'Error al cargar las conexiones Composio.', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 665f14489..ee9136352 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -2017,6 +2017,7 @@ const messages: TranslationMap = { 'memorySources.loadingConnections': 'Chargement des connexions…', 'memorySources.noConnections': "Aucune connexion Composio active trouvée. Connectez d'abord une intégration.", + 'memorySources.connectionAccount': 'Compte', 'memorySources.pickConnection': 'Choisissez une connexion', 'memorySources.selectConnection': '— Sélectionnez une connexion —', 'memorySources.composioListFailed': 'Échec du chargement des connexions Composio.', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 980ddc943..c6d10a476 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -1969,6 +1969,7 @@ const messages: TranslationMap = { 'अभी तक कोई कस्टम स्रोत नहीं है। एक फ़ोल्डर जोड़ें, GitHub रेपो, RSS फीड, या वेब पेज शुरू करने के लिए।', 'memorySources.loadingConnections': 'कनेक्शन लोड हो रहा है...', 'memorySources.noConnections': 'कोई सक्रिय Composio कनेक्शन नहीं मिला। पहले एकीकरण कनेक्ट करें।', + 'memorySources.connectionAccount': 'खाता', 'memorySources.pickConnection': 'कनेक्शन चुनें', 'memorySources.selectConnection': '- एक कनेक्शन चुनें -', 'memorySources.composioListFailed': 'Composio कनेक्शन लोड करने में विफल रहा।', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 1f021566a..ea498c6d5 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -1972,6 +1972,7 @@ const messages: TranslationMap = { 'memorySources.loadingConnections': 'Memuat koneksi...', 'memorySources.noConnections': 'Tak ditemukan koneksi Composio yang aktif. Hubungkan integrasi dulu.', + 'memorySources.connectionAccount': 'Akun', 'memorySources.pickConnection': 'Pilih koneksi', 'memorySources.selectConnection': '- Pilih koneksi -', 'memorySources.composioListFailed': 'Gagal memuat koneksi Composio.', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 2b23ed57d..6a3f410a1 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -2001,6 +2001,7 @@ const messages: TranslationMap = { 'memorySources.loadingConnections': 'Caricamento delle connessioni…', 'memorySources.noConnections': "Nessuna connessione Composio attiva trovata. Collega prima un'integrazione.", + 'memorySources.connectionAccount': 'Account', 'memorySources.pickConnection': 'Scegli una connessione', 'memorySources.selectConnection': '— Seleziona una connessione —', 'memorySources.composioListFailed': 'Impossibile caricare le connessioni Composio.', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index ef66fdda6..da2220a5f 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -1946,6 +1946,7 @@ const messages: TranslationMap = { '아직 사용자 지정 소스가 없습니다. 폴더, GitHub 리포지토리, RSS 피드 또는 웹 페이지를 추가해 시작하세요.', 'memorySources.loadingConnections': '연결을 불러오는 중…', 'memorySources.noConnections': '활성 Composio 연결을 찾을 수 없습니다. 먼저 통합을 연결하세요.', + 'memorySources.connectionAccount': '계정', 'memorySources.pickConnection': '연결 선택', 'memorySources.selectConnection': '— 연결 선택 —', 'memorySources.composioListFailed': 'Composio 연결을 불러오지 못했습니다.', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 807e0f8ef..08ba9c1c9 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -1990,6 +1990,7 @@ const messages: TranslationMap = { 'memorySources.loadingConnections': 'Wczytywanie połączeń…', 'memorySources.noConnections': 'Nie znaleziono aktywnych połączeń Composio. Najpierw połącz integrację.', + 'memorySources.connectionAccount': 'Konto', 'memorySources.pickConnection': 'Wybierz połączenie', 'memorySources.selectConnection': '— Wybierz połączenie —', 'memorySources.composioListFailed': 'Nie udało się wczytać połączeń Composio.', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 4d768b638..bcd358b6d 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -2009,6 +2009,7 @@ const messages: TranslationMap = { 'memorySources.loadingConnections': 'Carregando conexões…', 'memorySources.noConnections': 'Nenhuma conexão Composio ativa encontrada. Conecte uma integração primeiro.', + 'memorySources.connectionAccount': 'Conta', 'memorySources.pickConnection': 'Escolha uma conexão', 'memorySources.selectConnection': '— Selecionar uma conexão —', 'memorySources.composioListFailed': 'Falha ao carregar as conexões Composio.', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index bbff41e81..a96152892 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -1982,6 +1982,7 @@ const messages: TranslationMap = { 'memorySources.loadingConnections': 'Загрузка подключений…', 'memorySources.noConnections': 'Активных соединений Composio не ​​найдено. Сначала подключите интеграцию.', + 'memorySources.connectionAccount': 'Аккаунт', 'memorySources.pickConnection': 'Выберите соединение', 'memorySources.selectConnection': '— Выберите соединение —', 'memorySources.composioListFailed': 'Не удалось загрузить соединения Composio.', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 9b95a7b60..18f7fa40f 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -1868,6 +1868,7 @@ const messages: TranslationMap = { '暂无自定义来源。添加文件夹、GitHub 仓库、RSS 订阅或网页即可开始。', 'memorySources.loadingConnections': '正在加载连接…', 'memorySources.noConnections': '未找到活跃的 Composio 连接。请先连接一个集成。', + 'memorySources.connectionAccount': '账户', 'memorySources.pickConnection': '选择连接', 'memorySources.selectConnection': '— 选择连接 —', 'memorySources.composioListFailed': '加载 Composio 连接失败。',