mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(intelligence): deduplicate and label connections in integration source picker (#3361)
This commit is contained in:
@@ -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<typeof vi.fn>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderDialog() {
|
||||
const onClose = vi.fn();
|
||||
const onAdded = vi.fn();
|
||||
renderWithProviders(<AddMemorySourceDialog open onClose={onClose} onAdded={onAdded} />);
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string, number> = {
|
||||
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<string>();
|
||||
const unidentifiedCount: Record<string, number> = {};
|
||||
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({
|
||||
<select
|
||||
value={connectionId}
|
||||
onChange={e => {
|
||||
const conn = connections.find(c => c.id === e.target.value);
|
||||
if (conn) {
|
||||
const identity = conn.accountEmail ?? conn.workspace ?? conn.username ?? conn.id;
|
||||
setConnection(conn.id, conn.toolkit, `${conn.toolkit} · ${identity}`);
|
||||
const entry = dedupedConnections.find(({ conn }) => conn.id === e.target.value);
|
||||
if (entry) {
|
||||
setConnection(entry.conn.id, entry.conn.toolkit, entry.label);
|
||||
}
|
||||
}}
|
||||
className="mt-1 block w-full rounded-md border border-stone-300 bg-white px-3 py-2
|
||||
@@ -530,14 +602,11 @@ function ComposioPicker({
|
||||
focus:ring-1 focus:ring-primary-400 dark:border-neutral-600
|
||||
dark:bg-neutral-800 dark:text-neutral-100 dark:focus:border-primary-500">
|
||||
<option value="">{t('memorySources.selectConnection')}</option>
|
||||
{connections.map(conn => {
|
||||
const identity = conn.accountEmail ?? conn.workspace ?? conn.username ?? conn.id;
|
||||
return (
|
||||
<option key={conn.id} value={conn.id}>
|
||||
{conn.toolkit} · {identity}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
{dedupedConnections.map(({ conn, label }) => (
|
||||
<option key={conn.id} value={conn.id}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -1929,6 +1929,7 @@ const messages: TranslationMap = {
|
||||
'لا توجد مصادر عرفية بعد أضف ملفاً، ×1xx repo، ×x0x مكعب، أو صفحة على الشبكة للبدء.',
|
||||
'memorySources.loadingConnections': 'علاقات الحب...',
|
||||
'memorySources.noConnections': 'لم يتم العثور على أي وصلات اكسكساكسية نشطة. إجمع التكامل أولاً',
|
||||
'memorySources.connectionAccount': 'الحساب',
|
||||
'memorySources.pickConnection': 'اختر اتصال',
|
||||
'memorySources.selectConnection': '- اختيار اتصال -',
|
||||
'memorySources.composioListFailed': 'فشل في تحميل الأتصالات Xqx0x.',
|
||||
|
||||
@@ -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 সংযোগ লোড করতে ব্যর্থ।',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -1969,6 +1969,7 @@ const messages: TranslationMap = {
|
||||
'अभी तक कोई कस्टम स्रोत नहीं है। एक फ़ोल्डर जोड़ें, GitHub रेपो, RSS फीड, या वेब पेज शुरू करने के लिए।',
|
||||
'memorySources.loadingConnections': 'कनेक्शन लोड हो रहा है...',
|
||||
'memorySources.noConnections': 'कोई सक्रिय Composio कनेक्शन नहीं मिला। पहले एकीकरण कनेक्ट करें।',
|
||||
'memorySources.connectionAccount': 'खाता',
|
||||
'memorySources.pickConnection': 'कनेक्शन चुनें',
|
||||
'memorySources.selectConnection': '- एक कनेक्शन चुनें -',
|
||||
'memorySources.composioListFailed': 'Composio कनेक्शन लोड करने में विफल रहा।',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -1946,6 +1946,7 @@ const messages: TranslationMap = {
|
||||
'아직 사용자 지정 소스가 없습니다. 폴더, GitHub 리포지토리, RSS 피드 또는 웹 페이지를 추가해 시작하세요.',
|
||||
'memorySources.loadingConnections': '연결을 불러오는 중…',
|
||||
'memorySources.noConnections': '활성 Composio 연결을 찾을 수 없습니다. 먼저 통합을 연결하세요.',
|
||||
'memorySources.connectionAccount': '계정',
|
||||
'memorySources.pickConnection': '연결 선택',
|
||||
'memorySources.selectConnection': '— 연결 선택 —',
|
||||
'memorySources.composioListFailed': 'Composio 연결을 불러오지 못했습니다.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -1982,6 +1982,7 @@ const messages: TranslationMap = {
|
||||
'memorySources.loadingConnections': 'Загрузка подключений…',
|
||||
'memorySources.noConnections':
|
||||
'Активных соединений Composio не найдено. Сначала подключите интеграцию.',
|
||||
'memorySources.connectionAccount': 'Аккаунт',
|
||||
'memorySources.pickConnection': 'Выберите соединение',
|
||||
'memorySources.selectConnection': '— Выберите соединение —',
|
||||
'memorySources.composioListFailed': 'Не удалось загрузить соединения Composio.',
|
||||
|
||||
@@ -1868,6 +1868,7 @@ const messages: TranslationMap = {
|
||||
'暂无自定义来源。添加文件夹、GitHub 仓库、RSS 订阅或网页即可开始。',
|
||||
'memorySources.loadingConnections': '正在加载连接…',
|
||||
'memorySources.noConnections': '未找到活跃的 Composio 连接。请先连接一个集成。',
|
||||
'memorySources.connectionAccount': '账户',
|
||||
'memorySources.pickConnection': '选择连接',
|
||||
'memorySources.selectConnection': '— 选择连接 —',
|
||||
'memorySources.composioListFailed': '加载 Composio 连接失败。',
|
||||
|
||||
Reference in New Issue
Block a user