Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/src/lib/i18n/__tests__/coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,12 @@ describe('i18n coverage', () => {
.map(([key]) => key);
expect(keysWithEmDashes).toEqual([]);
});

// The OpenHuman Managed search option must name the provider behind it, so
// the managed path does not read as an unattributed black box (#5136). The
// provider name is a proper noun, so it stays literal in every locale.
it.each(['en', ...LOCALES])('locale %s names Exa in the managed search copy', locale => {
const flat = locale === 'en' ? enFlat : loadLocale(locale);
expect(flat['settings.search.engineManagedDesc']).toContain('Exa');
});
});
2 changes: 1 addition & 1 deletion app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1663,7 +1663,7 @@ const messages: TranslationMap = {
'settings.search.engineDisabledDesc': 'أزل أدوات البحث من سياق الوكيل وقائمة الأدوات المتاحة.',
'settings.search.engineManagedLabel': 'OpenHuman مُدار',
'settings.search.engineManagedDesc':
'خطأ تم سحبها من خلال الركيزة الخلفية - لا حاجة لمفتاح Xqx0xx.',
'الإعداد الافتراضي. يتم توجيهه عبر خادم OpenHuman الخلفي، المدعوم حاليًا بواسطة Exa: لا حاجة إلى مفتاح API.',
'settings.search.localManagedUnavailable':
'بحث OpenHuman المُدار غير متاح للمستخدمين المحليين. أضف مفتاح Parallel أو Brave الخاص بك لتفعيل البحث على الويب.',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
3 changes: 2 additions & 1 deletion app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1696,7 +1696,8 @@ const messages: TranslationMap = {
'settings.search.engineDisabledDesc':
'এজেন্ট প্রেক্ষাপট এবং উপলব্ধ টুল তালিকা থেকে সার্চ টুলগুলি সরিয়ে দিন।',
'settings.search.engineManagedLabel': 'OpenHuman পরিচালিত',
'settings.search.engineManagedDesc': 'ডিফল্ট xqx1x ব্যাক-এন্ড দ্বারা রুট',
'settings.search.engineManagedDesc':
'ডিফল্ট। OpenHuman ব্যাক-এন্ডের মাধ্যমে রুট করা হয়, বর্তমানে Exa দ্বারা চালিত: কোনো API key প্রয়োজন নেই।',
'settings.search.localManagedUnavailable':
'লোকাল ব্যবহারকারীদের জন্য OpenHuman Managed সার্চ উপলভ্য নয়। ওয়েব সার্চ চালু করতে আপনার নিজের Parallel বা Brave API key যোগ করুন।',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1757,7 +1757,7 @@ const messages: TranslationMap = {
'Suchwerkzeuge aus dem Agenten-Kontext und der verfügbaren Tool-Liste entfernen.',
'settings.search.engineManagedLabel': 'OpenHuman Verwaltet',
'settings.search.engineManagedDesc':
'Standard. Wird über das OpenHuman-Backend weitergeleitet – kein API-Schlüssel erforderlich.',
'Standard. Wird über das OpenHuman-Backend geleitet, aktuell betrieben von Exa: kein API-Schlüssel erforderlich.',
'settings.search.localManagedUnavailable':
'Die von OpenHuman verwaltete Suche ist für lokale Benutzer nicht verfügbar. Füge deinen eigenen Parallel- oder Brave-API-Schlüssel hinzu, um die Websuche zu aktivieren.',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1796,7 +1796,7 @@ const en: TranslationMap = {
'Remove search tools from the agent context and available tool list.',
'settings.search.engineManagedLabel': 'OpenHuman Managed',
'settings.search.engineManagedDesc':
'Default. Routed through the OpenHuman backend: no API key required.',
'Default. Routed through the OpenHuman backend, currently powered by Exa: no API key required.',
'settings.search.localManagedUnavailable':
'OpenHuman Managed search is not available for local users. Add your own Parallel, Brave, or Querit API key to enable web search.',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1735,7 +1735,7 @@ const messages: TranslationMap = {
'Elimina las herramientas de búsqueda del contexto del agente y de la lista de herramientas disponibles.',
'settings.search.engineManagedLabel': 'OpenHuman Gestionado',
'settings.search.engineManagedDesc':
'Predeterminado. Enrutado a través del backend OpenHuman: no se requiere la clave API.',
'Predeterminado. Enrutado a través del backend de OpenHuman, actualmente con tecnología de Exa: no se requiere clave API.',
'settings.search.localManagedUnavailable':
'La búsqueda gestionada por OpenHuman no está disponible para usuarios locales. Añade tu propia API key de Parallel o Brave para habilitar la búsqueda web.',
'settings.search.engineParallelLabel': 'paralelo',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1751,7 +1751,7 @@ const messages: TranslationMap = {
"Supprimer les outils de recherche du contexte agent et de la liste d'outils disponibles.",
'settings.search.engineManagedLabel': 'OpenHuman Géré',
'settings.search.engineManagedDesc':
'Par défaut. Routé via le backend OpenHuman: aucune clé API requise.',
'Par défaut. Acheminé via le backend OpenHuman, actuellement propulsé par Exa: aucune clé API requise.',
'settings.search.localManagedUnavailable':
'La recherche gérée par OpenHuman n’est pas disponible pour les utilisateurs locaux. Ajoutez votre propre clé API Parallel ou Brave pour activer la recherche web.',
'settings.search.engineParallelLabel': 'Parallèle',
Expand Down
3 changes: 2 additions & 1 deletion app/src/lib/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1694,7 +1694,8 @@ const messages: TranslationMap = {
'settings.search.engineDisabledLabel': 'Disabled',
'settings.search.engineDisabledDesc': 'एजेंट संदर्भ और उपलब्ध टूल सूची से खोज टूल हटाएं।',
'settings.search.engineManagedLabel': 'OpenHuman प्रबंधित',
'settings.search.engineManagedDesc': 'डिफ़ॉल्ट। OpenHuman backend: no API key required.',
'settings.search.engineManagedDesc':
'डिफ़ॉल्ट। OpenHuman बैक-एंड के माध्यम से रूट किया जाता है, वर्तमान में Exa द्वारा संचालित: किसी API key की आवश्यकता नहीं।',
'settings.search.localManagedUnavailable':
'लोकल उपयोगकर्ताओं के लिए OpenHuman Managed search उपलब्ध नहीं है। वेब सर्च चालू करने के लिए अपनी Parallel या Brave API key जोड़ें।',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1711,7 +1711,7 @@ const messages: TranslationMap = {
'Hapus alat pencarian dari konteks agen dan daftar alat yang tersedia.',
'settings.search.engineManagedLabel': 'OpenHuman Dikelola',
'settings.search.engineManagedDesc':
'Baku. Diarahkan melalui backend OpenHuman: tidak diperlukan kunci API.',
'Baku. Diarahkan melalui backend OpenHuman, saat ini didukung oleh Exa: tidak diperlukan kunci API.',
'settings.search.localManagedUnavailable':
'Pencarian OpenHuman Managed tidak tersedia untuk pengguna lokal. Tambahkan API key Parallel atau Brave Anda sendiri untuk mengaktifkan pencarian web.',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1735,7 +1735,7 @@ const messages: TranslationMap = {
"Rimuovi gli strumenti di ricerca dal contesto dell'agente e dall'elenco degli strumenti disponibili.",
'settings.search.engineManagedLabel': 'OpenHuman Gestito',
'settings.search.engineManagedDesc':
'Predefinito. Instradato tramite il backend OpenHuman: nessuna chiave API necessaria.',
'Predefinito. Instradato tramite il backend di OpenHuman, attualmente basato su Exa: nessuna chiave API necessaria.',
'settings.search.localManagedUnavailable':
'La ricerca gestita da OpenHuman non è disponibile per gli utenti locali. Aggiungi la tua chiave API Parallel o Brave per abilitare la ricerca web.',
'settings.search.engineParallelLabel': 'parallelo',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1686,7 +1686,7 @@ const messages: TranslationMap = {
'에이전트 컨텍스트 및 사용 가능한 도구 목록에서 검색 도구를 제거합니다.',
'settings.search.engineManagedLabel': 'OpenHuman 관리됨',
'settings.search.engineManagedDesc':
'기본값입니다. OpenHuman 백엔드를 통해 라우팅되며 API 키가 필요하지 않습니다.',
'기본값입니다. OpenHuman 백엔드를 통해 라우팅되며 현재 Exa로 구동됩니다. API 키가 필요하지 않습니다.',
'settings.search.localManagedUnavailable':
'로컬 사용자는 OpenHuman 관리 검색을 사용할 수 없습니다. 웹 검색을 활성화하려면 자체 Parallel, Brave 또는 Querit API 키를 추가하세요.',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
3 changes: 2 additions & 1 deletion app/src/lib/i18n/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1727,7 +1727,8 @@ const messages: TranslationMap = {
'settings.search.engineDisabledDesc':
'Usuń narzędzia wyszukiwania z kontekstu agenta i listy dostępnych narzędzi.',
'settings.search.engineManagedLabel': 'OpenHuman zarządzane',
'settings.search.engineManagedDesc': 'Domyślnie. Przez backend OpenHuman: bez klucza API.',
'settings.search.engineManagedDesc':
'Domyślnie. Kierowane przez backend OpenHuman, obecnie oparte na Exa: bez klucza API.',
'settings.search.localManagedUnavailable':
'Wyszukiwarka zarządzana przez OpenHuman jest niedostępna dla użytkowników lokalnych. Dodaj własny klucz API Parallel lub Brave, aby włączyć wyszukiwanie w sieci.',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1733,7 +1733,7 @@ const messages: TranslationMap = {
'Remover ferramentas de busca do contexto do agente e da lista de ferramentas disponíveis.',
'settings.search.engineManagedLabel': 'OpenHuman Gerenciado',
'settings.search.engineManagedDesc':
'Padrão. Roteado através do backend OpenHuman: nenhuma chave API é necessária.',
'Padrão. Roteado através do backend do OpenHuman, atualmente com tecnologia Exa: nenhuma chave API é necessária.',
'settings.search.localManagedUnavailable':
'A busca gerenciada pela OpenHuman não está disponível para usuários locais. Adicione sua própria chave de API do Parallel ou Brave para habilitar a busca na web.',
'settings.search.engineParallelLabel': 'Paralelo',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1716,7 +1716,7 @@ const messages: TranslationMap = {
'Удалить инструменты поиска из контекста агента и списка доступных инструментов.',
'settings.search.engineManagedLabel': 'OpenHuman Управляемый',
'settings.search.engineManagedDesc':
'По умолчанию. Маршрутизируется через серверную часть OpenHuman: ключ API не ​​требуется.',
'По умолчанию. Маршрутизируется через серверную часть OpenHuman, сейчас на базе Exa: ключ API не требуется.',
'settings.search.localManagedUnavailable':
'Поиск OpenHuman Managed недоступен для локальных пользователей. Добавьте свой ключ API Parallel или Brave, чтобы включить веб-поиск.',
'settings.search.engineParallelLabel': 'Параллельно',
Expand Down
3 changes: 2 additions & 1 deletion app/src/lib/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1607,7 +1607,8 @@ const messages: TranslationMap = {
'settings.search.engineDisabledLabel': 'Disabled',
'settings.search.engineDisabledDesc': '从智能体上下文和可用工具列表中移除搜索工具。',
'settings.search.engineManagedLabel': 'OpenHuman 托管',
'settings.search.engineManagedDesc': '默认选项。通过 OpenHuman 后端路由,无需 API 密钥。',
'settings.search.engineManagedDesc':
'默认选项。通过 OpenHuman 后端路由,当前由 Exa 提供支持,无需 API 密钥。',
'settings.search.localManagedUnavailable':
'本地用户无法使用 OpenHuman 托管搜索。请添加你自己的 Parallel、Brave 或 Querit API 密钥以启用网页搜索。',
'settings.search.engineParallelLabel': 'Parallel',
Expand Down
124 changes: 124 additions & 0 deletions app/src/utils/__tests__/toolTimelineFormatting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
buildProcessingBlocks,
categorizeTool,
extractAgentSources,
extractSearchProvider,
formatTimelineEntry,
formatToolName,
isKnownClientTool,
Expand Down Expand Up @@ -167,6 +168,87 @@ describe('formatTimelineEntry', () => {
).toEqual({ title: 'Searching: rust async trait' });
});

it('attributes a completed web_search to the resolved provider', () => {
expect(
formatTimelineEntry(
entry({
name: 'web_search',
status: 'success',
argsBuffer: JSON.stringify({ query: 'rust async trait' }),
result: 'Search results for: rust async trait (via Exa)\n1. Some title\n https://x.dev',
})
)
).toEqual({ title: 'Searched with Exa', detail: 'rust async trait' });
});

it('reflects a different provider from the result (attribution is dynamic)', () => {
expect(
formatTimelineEntry(
entry({
name: 'web_search',
status: 'success',
argsBuffer: JSON.stringify({ query: 'weather' }),
result: 'Search results for: weather (via Brave)\n1. Forecast',
})
)
).toEqual({ title: 'Searched with Brave', detail: 'weather' });
});

it('keeps the running label when no result is present yet', () => {
expect(
formatTimelineEntry(
entry({
name: 'web_search',
status: 'running',
argsBuffer: JSON.stringify({ query: 'rust async trait' }),
})
)
).toEqual({ title: 'Searching: rust async trait' });
});

// `web_search_tool` is the name the core actually registers and streams for
// the canonical search slot; `web_search` is only the settings-family id.
// Without this the real production row fell through to "Web Search Tool".
it('formats the streamed web_search_tool name while running', () => {
expect(
formatTimelineEntry(
entry({
name: 'web_search_tool',
status: 'running',
argsBuffer: JSON.stringify({ query: 'rust async trait' }),
})
)
).toEqual({ title: 'Searching: rust async trait' });
});

it('attributes a completed web_search_tool from the markdown result', () => {
// Production renders tool results as markdown (`output_for_llm(true)`),
// so the marker arrives on the markdown heading line.
expect(
formatTimelineEntry(
entry({
name: 'web_search_tool',
status: 'success',
argsBuffer: JSON.stringify({ query: 'rust async trait' }),
result: '# Search results — `rust async trait` (via Exa)\n\n## [T](https://x.dev)',
})
)
).toEqual({ title: 'Searched with Exa', detail: 'rust async trait' });
});

it('attributes a completed web_search_tool that returned no results', () => {
expect(
formatTimelineEntry(
entry({
name: 'web_search_tool',
status: 'success',
argsBuffer: JSON.stringify({ query: 'zzzz' }),
result: '_No results for `zzzz`_ (via Exa)',
})
)
).toEqual({ title: 'Searched with Exa', detail: 'zzzz' });
});

it('formats file_read with shortened path', () => {
expect(
formatTimelineEntry(
Expand Down Expand Up @@ -233,6 +315,45 @@ describe('formatTimelineEntry', () => {
});
});

describe('extractSearchProvider', () => {
it('reads the provider from a `(via …)` marker', () => {
expect(extractSearchProvider('Search results for: q (via Exa)\n1. foo')).toBe('Exa');
expect(extractSearchProvider('Search results for: q (via Brave)')).toBe('Brave');
expect(extractSearchProvider('# Search results — `q` (via Querit)')).toBe('Querit');
});

it('returns undefined when there is no marker or no result', () => {
expect(extractSearchProvider(undefined)).toBeUndefined();
expect(extractSearchProvider('')).toBeUndefined();
expect(extractSearchProvider('No results found for: q')).toBeUndefined();
});

it('trims surrounding whitespace in the provider name', () => {
expect(extractSearchProvider('foo (via Exa )')).toBe('Exa');
});

it('ignores a `(via …)` string that appears only in a result excerpt', () => {
expect(
extractSearchProvider('Search results for: q\n1. Title\n Booked (via SomeAirline) today.')
).toBeUndefined();
});

it('ignores an implausibly long marker', () => {
expect(extractSearchProvider(`Search results for: q (via ${'x'.repeat(64)})`)).toBeUndefined();
});

it('reads the trailing marker when the echoed query also contains one', () => {
// The heading echoes the user's query, so a query like `login (via OAuth)`
// puts a decoy marker ahead of the real one. Only the trailing marker counts.
expect(extractSearchProvider('Search results for: login (via OAuth) (via Exa)')).toBe('Exa');
});

it('reads the marker from an empty-result heading', () => {
expect(extractSearchProvider('No results found for: q (via Exa)')).toBe('Exa');
expect(extractSearchProvider('_No results for `q`_ (via Brave)')).toBe('Brave');
});
});

describe('formatToolName', () => {
it('returns human-readable names for known tools', () => {
expect(formatToolName('shell')).toBe('Running command');
Expand Down Expand Up @@ -277,6 +398,9 @@ describe('isKnownClientTool', () => {
expect(isKnownClientTool('shell')).toBe(true);
expect(isKnownClientTool('subagent:researcher')).toBe(true);
expect(isKnownClientTool('delegate_to_integrations_agent')).toBe(true);
// The streamed search-slot name, so the client label wins over the
// server's humanized "Web Search Tool".
expect(isKnownClientTool('web_search_tool')).toBe(true);
});

it('does not recognize dynamic Composio/MCP actions (server labels them)', () => {
Expand Down
Loading
Loading