From 1a85d242e260475ffb12cd2238e40bda2a9a6e18 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:38:14 -0400 Subject: [PATCH] feat(chat): Quick/Reasoning toggle + resolved model display (#3325) --- app/src/lib/i18n/ar.ts | 2 + app/src/lib/i18n/bn.ts | 2 + app/src/lib/i18n/de.ts | 2 + app/src/lib/i18n/en.ts | 2 + app/src/lib/i18n/es.ts | 2 + app/src/lib/i18n/fr.ts | 2 + app/src/lib/i18n/hi.ts | 2 + app/src/lib/i18n/id.ts | 2 + app/src/lib/i18n/it.ts | 2 + app/src/lib/i18n/ko.ts | 2 + app/src/lib/i18n/pl.ts | 2 + app/src/lib/i18n/pt.ts | 2 + app/src/lib/i18n/ru.ts | 2 + app/src/lib/i18n/zh-CN.ts | 2 + app/src/pages/Accounts.tsx | 76 +------ app/src/pages/Conversations.tsx | 214 +++++------------- .../__tests__/Conversations.render.test.tsx | 71 ------ src/openhuman/agent/profiles.rs | 19 ++ src/openhuman/inference/provider/factory.rs | 57 +++++ .../inference/provider/factory_tests.rs | 77 +++++++ src/openhuman/inference/schemas.rs | 31 +++ tests/json_rpc_e2e.rs | 202 +++++++++++++++++ 22 files changed, 470 insertions(+), 305 deletions(-) diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 4e18876c7..ff6066929 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4107,6 +4107,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'المنسق', 'chat.agentProfile.exists': 'ملف تعريف الوكيل "{name}" موجود بالفعل.', 'chat.agentProfile.label': 'ملف تعريف الوكيل', + 'chat.agentProfile.quick': 'سريع', + 'chat.agentProfile.reasoning': 'تفكير', 'chat.agentProfile.namePlaceholder': 'اسم ملف التعريف', 'chat.agentProfile.promptStylePlaceholder': 'نمط المطالبة', 'chat.agentProfile.allowedToolsPlaceholder': 'الأدوات المسموح بها', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index a078099c6..f730b2131 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4183,6 +4183,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'অর্কেস্ট্রেটর', 'chat.agentProfile.exists': 'এজেন্ট প্রোফাইল "{name}" ইতিমধ্যেই বিদ্যমান৷', 'chat.agentProfile.label': 'এজেন্ট প্রোফাইল', + 'chat.agentProfile.quick': 'দ্রুত', + 'chat.agentProfile.reasoning': 'যুক্তি', 'chat.agentProfile.namePlaceholder': 'প্রোফাইল নাম', 'chat.agentProfile.promptStylePlaceholder': 'প্রম্পট স্টাইল', 'chat.agentProfile.allowedToolsPlaceholder': 'অনুমোদিত টুল', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 140208c58..e368cf807 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -4296,6 +4296,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Orchestrator', 'chat.agentProfile.exists': 'Agentenprofil „{name}“ ist bereits vorhanden.', 'chat.agentProfile.label': 'Agentenprofil', + 'chat.agentProfile.quick': 'Schnell', + 'chat.agentProfile.reasoning': 'Denkmodus', 'chat.agentProfile.namePlaceholder': 'Profilname', 'chat.agentProfile.promptStylePlaceholder': 'Eingabeaufforderungsstil', 'chat.agentProfile.allowedToolsPlaceholder': 'Zulässige Tools', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 2fc4ac0f4..cf1525081 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -4579,6 +4579,8 @@ const en: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Orchestrator', 'chat.agentProfile.exists': 'Agent profile "{name}" already exists.', 'chat.agentProfile.label': 'Agent profile', + 'chat.agentProfile.quick': 'Quick', + 'chat.agentProfile.reasoning': 'Reasoning', 'chat.agentProfile.namePlaceholder': 'Profile name', 'chat.agentProfile.promptStylePlaceholder': 'Prompt style', 'chat.agentProfile.allowedToolsPlaceholder': 'Allowed tools', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 04ab1b284..3af52424f 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -4263,6 +4263,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'orquestador', 'chat.agentProfile.exists': 'El perfil del agente "{name}" ya existe.', 'chat.agentProfile.label': 'Perfil del agente', + 'chat.agentProfile.quick': 'Rápido', + 'chat.agentProfile.reasoning': 'Razonamiento', 'chat.agentProfile.namePlaceholder': 'Nombre del perfil', 'chat.agentProfile.promptStylePlaceholder': 'estilo rápido', 'chat.agentProfile.allowedToolsPlaceholder': 'Herramientas permitidas', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index d2bff47e8..83f7de6ce 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -4278,6 +4278,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Orchestrator', 'chat.agentProfile.exists': "Le profil d'agent « {name} » existe déjà.", 'chat.agentProfile.label': "Profil d'agent", + 'chat.agentProfile.quick': 'Rapide', + 'chat.agentProfile.reasoning': 'Raisonnement', 'chat.agentProfile.namePlaceholder': 'Nom du profil', 'chat.agentProfile.promptStylePlaceholder': 'Style de prompt', 'chat.agentProfile.allowedToolsPlaceholder': 'Outils autorisés', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index bbb1a5850..0f7877d05 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4193,6 +4193,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'ऑर्केस्ट्रेटर', 'chat.agentProfile.exists': 'एजेंट प्रोफ़ाइल "{name}" पहले से मौजूद है।', 'chat.agentProfile.label': 'एजेंट प्रोफ़ाइल', + 'chat.agentProfile.quick': 'त्वरित', + 'chat.agentProfile.reasoning': 'तर्क', 'chat.agentProfile.namePlaceholder': 'प्रोफ़ाइल नाम', 'chat.agentProfile.promptStylePlaceholder': 'शीघ्र शैली', 'chat.agentProfile.allowedToolsPlaceholder': 'अनुमत उपकरण', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 1206d3820..33dde55c1 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4202,6 +4202,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Orchestrator', 'chat.agentProfile.exists': 'Profil agen "{name}" sudah ada.', 'chat.agentProfile.label': 'Profil agen', + 'chat.agentProfile.quick': 'Cepat', + 'chat.agentProfile.reasoning': 'Penalaran', 'chat.agentProfile.namePlaceholder': 'Nama profil', 'chat.agentProfile.promptStylePlaceholder': 'Gaya perintah', 'chat.agentProfile.allowedToolsPlaceholder': 'Alat yang diizinkan', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 419e69e87..a3cac5a9b 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -4252,6 +4252,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Orchestrator', 'chat.agentProfile.exists': 'Il profilo agente "{name}" esiste già.', 'chat.agentProfile.label': 'Profilo agente', + 'chat.agentProfile.quick': 'Veloce', + 'chat.agentProfile.reasoning': 'Ragionamento', 'chat.agentProfile.namePlaceholder': 'Nome profilo', 'chat.agentProfile.promptStylePlaceholder': 'Stile prompt', 'chat.agentProfile.allowedToolsPlaceholder': 'Strumenti consentiti', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 27b1bdf3a..b6e65e04b 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4147,6 +4147,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Orchestrator', 'chat.agentProfile.exists': '에이전트 프로필 "{name}"이(가) 이미 존재합니다.', 'chat.agentProfile.label': '에이전트 프로필', + 'chat.agentProfile.quick': '빠름', + 'chat.agentProfile.reasoning': '추론', 'chat.agentProfile.namePlaceholder': '프로필 이름', 'chat.agentProfile.promptStylePlaceholder': '프롬프트 스타일', 'chat.agentProfile.allowedToolsPlaceholder': '허용된 도구', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 0b7b0bcee..6d7e61b37 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -4256,6 +4256,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Orchestrator', 'chat.agentProfile.exists': 'Profil agenta „{name}” już istnieje.', 'chat.agentProfile.label': 'Profil agenta', + 'chat.agentProfile.quick': 'Szybki', + 'chat.agentProfile.reasoning': 'Rozumowanie', 'chat.agentProfile.namePlaceholder': 'Nazwa profilu', 'chat.agentProfile.promptStylePlaceholder': 'Styl promptu', 'chat.agentProfile.allowedToolsPlaceholder': 'Dozwolone narzędzia', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 1cb0afc74..84b4ab252 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -4255,6 +4255,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Orquestrador', 'chat.agentProfile.exists': 'O perfil de agente "{name}" já existe.', 'chat.agentProfile.label': 'Perfil do agente', + 'chat.agentProfile.quick': 'Rápido', + 'chat.agentProfile.reasoning': 'Raciocínio', 'chat.agentProfile.namePlaceholder': 'Nome do perfil', 'chat.agentProfile.promptStylePlaceholder': 'Estilo de prompt', 'chat.agentProfile.allowedToolsPlaceholder': 'Ferramentas permitidas', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index c4d0bbbf6..e5de78ece 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -4218,6 +4218,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': 'Оркестратор', 'chat.agentProfile.exists': 'Профиль агента «{name}» уже существует.', 'chat.agentProfile.label': 'Профиль агента', + 'chat.agentProfile.quick': 'Быстрый', + 'chat.agentProfile.reasoning': 'Рассуждение', 'chat.agentProfile.namePlaceholder': 'Имя профиля', 'chat.agentProfile.promptStylePlaceholder': 'Стиль подсказки', 'chat.agentProfile.allowedToolsPlaceholder': 'Разрешенные инструменты', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 2e5d18a7c..c6d68f59d 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -3983,6 +3983,8 @@ const messages: TranslationMap = { 'chat.agentProfile.defaultAgentLabel': '协调者', 'chat.agentProfile.exists': '代理配置文件“{name}”已存在。', 'chat.agentProfile.label': '代理简介', + 'chat.agentProfile.quick': '快速', + 'chat.agentProfile.reasoning': '推理', 'chat.agentProfile.namePlaceholder': '个人资料名称', 'chat.agentProfile.promptStylePlaceholder': '提示风格', 'chat.agentProfile.allowedToolsPlaceholder': '允许使用的工具', diff --git a/app/src/pages/Accounts.tsx b/app/src/pages/Accounts.tsx index 1bbd4010e..560187db3 100644 --- a/app/src/pages/Accounts.tsx +++ b/app/src/pages/Accounts.tsx @@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from 'react'; import AddAccountModal from '../components/accounts/AddAccountModal'; import { AgentIcon, ProviderIcon } from '../components/accounts/providerIcons'; -import RespondQueuePanel from '../components/accounts/RespondQueuePanel'; import WebviewHost from '../components/accounts/WebviewHost'; import { usePrewarmMostRecentAccount } from '../hooks/usePrewarmMostRecentAccount'; import { useT } from '../lib/i18n/I18nContext'; @@ -20,7 +19,6 @@ import { setLastActiveAccount, } from '../store/accountsSlice'; import { useAppDispatch, useAppSelector } from '../store/hooks'; -import { fetchRespondQueue } from '../store/providerSurfaceSlice'; import type { Account, AccountProvider, ProviderDescriptor } from '../types/accounts'; import { AGENT_ACCOUNT_ID as AGENT_ID } from '../utils/accountsFullscreen'; import { AgentChatPanel } from './Conversations'; @@ -108,18 +106,8 @@ const Accounts = () => { const order = useAppSelector(state => state.accounts.order); const activeAccountId = useAppSelector(state => state.accounts.activeAccountId); const unreadByAccount = useAppSelector(state => state.accounts.unread); - // Respond-queue selectors enabled - const respondQueue = useAppSelector(state => state.providerSurfaces.queue); - const respondQueueCount = useAppSelector(state => state.providerSurfaces.count); - const respondQueueStatus = useAppSelector(state => state.providerSurfaces.status); - const respondQueueError = useAppSelector(state => state.providerSurfaces.error); - const [addOpen, setAddOpen] = useState(false); const [ctxMenu, setCtxMenu] = useState(null); - // Respond Queue is a power-user surface (pending replies from connected - // accounts). Hidden by default so the agent chat gets full width; users - // can toggle it open from the header. (#XXXX) - const [respondQueueOpen, setRespondQueueOpen] = useState(false); useEffect(() => { startWebviewAccountService(); @@ -135,14 +123,6 @@ const Accounts = () => { ); usePrewarmMostRecentAccount({ accounts, accountsById, activeAccountId }); - useEffect(() => { - void dispatch(fetchRespondQueue()); - const id = window.setInterval(() => { - void dispatch(fetchRespondQueue({ silent: true })); - }, 10_000); - return () => window.clearInterval(id); - }, [dispatch]); - const connectedProviders = useMemo( () => new Set(accounts.map(a => a.provider)), [accounts] @@ -305,61 +285,7 @@ const Accounts = () => { {/* Main pane */}
{isAgentSelected ? ( -
-
- - {/* Floating toggle to reveal the Respond Queue side panel. - Pinned top-right inside the chat pane so it doesn't shift - layout when the panel opens/closes. */} - -
- {/* Respond queue side panel — hidden by default; reveals via toggle */} - {respondQueueOpen && ( - { - trackEvent('tauri_browser_click', { - surface: 'chat_right_sidebar', - action: 'refresh_respond_queue', - provider: 'agent', - }); - void dispatch(fetchRespondQueue()); - }} - /> - )} -
+ ) : active ? (
diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx index 53b0f68d2..bc3b3cae4 100644 --- a/app/src/pages/Conversations.tsx +++ b/app/src/pages/Conversations.tsx @@ -9,7 +9,6 @@ import ApprovalRequestCard from '../components/chat/ApprovalRequestCard'; import ArtifactCard from '../components/chat/ArtifactCard'; import ChatComposer from '../components/chat/ChatComposer'; import ChatFilesChip from '../components/chat/ChatFilesChip'; -import TokenUsagePill from '../components/chat/TokenUsagePill'; import { ConfirmationModal } from '../components/intelligence/ConfirmationModal'; import PillTabBar from '../components/PillTabBar'; import UpsellBanner from '../components/upsell/UpsellBanner'; @@ -31,13 +30,13 @@ import { trackEvent } from '../services/analytics'; import { applyOpenRouterFreeModels } from '../services/api/openrouterFreeModels'; import { threadApi } from '../services/api/threadApi'; import { chatCancel, chatSend, useRustChat } from '../services/chatService'; +import { callCoreRpc } from '../services/coreRpcClient'; import { store } from '../store'; import { loadAgentProfiles, selectActiveAgentProfileId, selectAgentProfile, selectAgentProfiles, - upsertAgentProfile, } from '../store/agentProfileSlice'; import { beginInferenceTurn, @@ -61,7 +60,6 @@ import { THREAD_NOT_FOUND_MESSAGE, updateThreadTitle, } from '../store/threadSlice'; -import type { AgentProfile } from '../types/agentProfile'; import type { ConfirmationModal as ConfirmationModalType } from '../types/intelligence'; import type { ThreadMessage } from '../types/thread'; import type { TaskBoardCard, TaskBoardCardStatus } from '../types/turnState'; @@ -117,12 +115,6 @@ type ReplyMode = 'text' | 'voice'; const AUTOCOMPLETE_POLL_DEBOUNCE_MS = 320; const AUTOCOMPLETE_MIN_CONTEXT_CHARS = 3; const debug = debugFactory('conversations'); -const DEFAULT_PROFILE_DRAFT = { - name: '', - agentId: 'orchestrator', - systemPromptSuffix: '', - allowedTools: '', -}; interface ConversationsProps { /** @@ -185,14 +177,6 @@ export function formatThreadLoadError(err: unknown): string { return String(err); } -function formatAgentProfileAgentLabel(agentId: string): string { - return agentId - .split(/[_-]+/) - .filter(Boolean) - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) - .join(' '); -} - const Conversations = ({ variant = 'page', composer: composerProp = 'text', @@ -226,8 +210,6 @@ const Conversations = ({ const [sendAdvisory, setSendAdvisory] = useState(null); const [openRouterStatus, setOpenRouterStatus] = useState<'idle' | 'saving' | 'error'>('idle'); const [pendingSendingThreadId, setPendingSendingThreadId] = useState(null); - const [profileDraftOpen, setProfileDraftOpen] = useState(false); - const [profileDraft, setProfileDraft] = useState(DEFAULT_PROFILE_DRAFT); const socketStatus = useAppSelector(selectSocketStatus); const agentProfiles = useAppSelector(selectAgentProfiles); const selectedAgentProfileId = useAppSelector(selectActiveAgentProfileId); @@ -273,29 +255,27 @@ const Conversations = ({ onConfirm: () => {}, onCancel: () => {}, }); - const agentProfileAgentOptions = useMemo(() => { - const seen = new Set(); - const options: Array<{ id: string; label: string }> = []; - for (const profile of agentProfiles) { - const id = profile.agentId.trim(); - if (!id || seen.has(id)) continue; - seen.add(id); - options.push({ - id, - label: profile.builtIn ? profile.name : formatAgentProfileAgentLabel(id), - }); - } - if (profileDraft.agentId && !seen.has(profileDraft.agentId)) { - options.push({ - id: profileDraft.agentId, - label: formatAgentProfileAgentLabel(profileDraft.agentId), - }); - } - if (options.length === 0) { - options.push({ id: 'orchestrator', label: t('chat.agentProfile.defaultAgentLabel') }); - } - return options; - }, [agentProfiles, profileDraft.agentId, t]); + const [resolvedModel, setResolvedModel] = useState(null); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const profile = agentProfiles.find(p => p.id === selectedAgentProfileId); + const hint = profile?.modelOverride ?? 'hint:chat'; + const res = await callCoreRpc<{ model: string }>({ + method: 'openhuman.inference_resolve_model', + params: { hint }, + }); + if (!cancelled) setResolvedModel(res.model); + } catch { + if (!cancelled) setResolvedModel(null); + } + })(); + return () => { + cancelled = true; + }; + }, [agentProfiles, selectedAgentProfileId]); const textInputRef = useRef(null); const isComposingTextRef = useRef(false); @@ -379,42 +359,6 @@ const Conversations = ({ } }; - const handleCreateAgentProfile = async () => { - const name = profileDraft.name.trim(); - if (!name) return; - const duplicate = agentProfiles.some( - profile => profile.name.trim().toLowerCase() === name.toLowerCase() - ); - if (duplicate) { - setSendAdvisory(t('chat.agentProfile.exists').replace('{name}', name)); - return; - } - const id = `profile-${globalThis.crypto.randomUUID().slice(0, 8)}`; - const allowedTools = profileDraft.allowedTools - .split(',') - .map(tool => tool.trim()) - .filter(Boolean); - const profile: AgentProfile = { - id, - name, - description: t('chat.agentProfile.customDescription'), - agentId: profileDraft.agentId, - systemPromptSuffix: profileDraft.systemPromptSuffix.trim() || null, - allowedTools: allowedTools.length > 0 ? allowedTools : null, - builtIn: false, - }; - try { - await dispatch(upsertAgentProfile(profile)).unwrap(); - await dispatch(selectAgentProfile(id)).unwrap(); - setProfileDraftOpen(false); - setProfileDraft(DEFAULT_PROFILE_DRAFT); - setSendAdvisory(null); - } catch (error) { - debug('agent profile create failed: %o', error); - setSendAdvisory(t('chat.agentProfile.createFailed')); - } - }; - useEffect(() => { let cancelled = false; @@ -1526,107 +1470,59 @@ const Conversations = ({ )}
)} + {resolvedModel && ( + + {resolvedModel} + + )} <> -
- +
+
{(selectedThreadId ?? activeThreadId) && ( )} -
)} - {!isSidebar && profileDraftOpen && ( -
-
- setProfileDraft(prev => ({ ...prev, name: event.target.value }))} - placeholder={t('chat.agentProfile.namePlaceholder')} - className="h-8 rounded-lg border border-stone-200 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-200 px-3 text-xs outline-none focus:border-primary-400" - /> - -
-