diff --git a/public/icons/hermes-agent.png b/public/icons/hermes-agent.png new file mode 100644 index 00000000..2d629d2f Binary files /dev/null and b/public/icons/hermes-agent.png differ diff --git a/server/modules/providers/list/hermes/hermes-auth.provider.ts b/server/modules/providers/list/hermes/hermes-auth.provider.ts index ed3b1ed3..dec4a4ea 100644 --- a/server/modules/providers/list/hermes/hermes-auth.provider.ts +++ b/server/modules/providers/list/hermes/hermes-auth.provider.ts @@ -29,7 +29,7 @@ export class HermesProviderAuth implements IProviderAuth { authenticated: false, email: null, method: null, - error: 'Hermes ACP is not installed', + error: 'Hermes is not installed', }; } @@ -39,8 +39,8 @@ export class HermesProviderAuth implements IProviderAuth { installed, authenticated: credentials.authenticated, email: credentials.email, - method: credentials.method, - error: credentials.authenticated ? undefined : 'Hermes credentials were not found', + method: credentials.method ?? 'managed_by_hermes', + error: undefined, }; } diff --git a/server/modules/providers/list/hermes/hermes-models.provider.ts b/server/modules/providers/list/hermes/hermes-models.provider.ts index 01e4d8bb..ae1eff26 100644 --- a/server/modules/providers/list/hermes/hermes-models.provider.ts +++ b/server/modules/providers/list/hermes/hermes-models.provider.ts @@ -17,8 +17,8 @@ export const HERMES_FALLBACK_MODELS: ProviderModelsDefinition = { OPTIONS: [ { value: HERMES_CONFIGURED_MODEL, - label: 'Configured in Hermes', - description: 'Uses the provider and model selected with `hermes model`.', + label: 'Use Hermes default', + description: 'Uses the provider and model selected in Hermes.', }, ], DEFAULT: HERMES_CONFIGURED_MODEL, @@ -105,8 +105,8 @@ export class HermesProviderModels implements IProviderModels { OPTIONS: [ { value: HERMES_CONFIGURED_MODEL, - label: 'Configured in Hermes', - description: `Current Hermes model: ${activeModel}`, + label: 'Use Hermes default', + description: `Uses the provider and model selected in Hermes. Current config: ${activeModel}`, }, ], DEFAULT: HERMES_CONFIGURED_MODEL, diff --git a/src/components/llm-logo-provider/HermesLogo.tsx b/src/components/llm-logo-provider/HermesLogo.tsx index 49c7598f..4e957c59 100644 --- a/src/components/llm-logo-provider/HermesLogo.tsx +++ b/src/components/llm-logo-provider/HermesLogo.tsx @@ -4,13 +4,11 @@ type HermesLogoProps = { export default function HermesLogo({ className = 'w-5 h-5' }: HermesLogoProps) { return ( - - - - - + Hermes ); } diff --git a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx index ac6932e0..a2717181 100644 --- a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx +++ b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx @@ -72,7 +72,7 @@ export default function AgentListItem({ }: AgentListItemProps) { const config = agentConfig[agentId]; const colors = colorClasses[config.color]; - const isReady = agentId === 'hermes' ? authStatus.installed : authStatus.authenticated; + const isReady = agentId !== 'hermes' && authStatus.authenticated; if (isMobile) { return ( diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx index 0e3673ff..ffe3e55e 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx @@ -1,5 +1,4 @@ import { - CheckCircle2, KeyRound, Layers3, LogIn, @@ -88,37 +87,20 @@ type HermesAction = { icon: typeof Layers3; }; -type HermesActionGroup = { - title: string; - actions: HermesAction[]; -}; - -const hermesActionGroups: HermesActionGroup[] = [ +const hermesActions: HermesAction[] = [ { - title: 'Setup', - actions: [ - { - label: 'Provider setup', - description: 'Configure provider credentials and the active model.', - command: 'hermes model', - title: 'Hermes Provider Setup', - icon: Layers3, - }, - { - label: 'Credential pools', - description: 'Manage API keys and OAuth credentials.', - command: 'hermes auth', - title: 'Hermes Credential Pools', - icon: KeyRound, - }, - { - label: 'ACP check', - description: 'Validate the Hermes ACP adapter.', - command: 'hermes acp --check', - title: 'Hermes ACP Check', - icon: CheckCircle2, - }, - ], + label: 'Configure model', + description: 'Choose the provider and model Hermes should use.', + command: 'hermes model', + title: 'Configure Hermes Model', + icon: Layers3, + }, + { + label: 'Manage credentials', + description: 'Update credential pools and API keys.', + command: 'hermes auth', + title: 'Hermes Credentials', + icon: KeyRound, }, ]; @@ -126,7 +108,6 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo const { t } = useTranslation('settings'); const config = agentConfig[agent]; const isHermes = agent === 'hermes'; - const hermesReady = authStatus.installed; return (
@@ -143,130 +124,118 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
-
-
-
+ {isHermes ? ( +
+
- {isHermes - ? t('agents.hermes.setupStatus.title', { defaultValue: 'Setup status' }) - : t('agents.connectionStatus')} + {t('agents.hermes.configuration.title', { defaultValue: 'Hermes configuration' })}
+ {t('agents.hermes.configuration.description', { + defaultValue: 'Models and credentials are managed by Hermes.', + })} +
+
+
+ {hermesActions.map((action, index) => { + const Icon = action.icon; + const isPrimary = index === 0; + return ( + + ); + })} +
+
+ ) : ( +
+
+
+
+ {t('agents.connectionStatus')} +
+
+ {authStatus.loading ? ( + t('agents.authStatus.checkingAuth') + ) : authStatus.authenticated ? ( + t('agents.authStatus.loggedInAs', { + email: authStatus.email || t('agents.authStatus.authenticatedUser'), + }) + ) : ( + t('agents.authStatus.notConnected') + )} +
+
+
{authStatus.loading ? ( - t('agents.authStatus.checkingAuth') - ) : isHermes ? ( - hermesReady - ? t('agents.hermes.setupStatus.readyDescription', { defaultValue: 'Hermes ACP is installed. Credentials and models are managed by Hermes.' }) - : t('agents.hermes.setupStatus.needsSetupDescription', { defaultValue: 'Install Hermes or run the ACP check to validate the adapter.' }) + + {t('agents.authStatus.checking')} + ) : authStatus.authenticated ? ( - t('agents.authStatus.loggedInAs', { - email: authStatus.email || t('agents.authStatus.authenticatedUser'), - }) + + {t('agents.authStatus.connected')} + ) : ( - t('agents.authStatus.notConnected') + + {t('agents.authStatus.disconnected')} + )}
-
- {authStatus.loading ? ( - - {t('agents.authStatus.checking')} - - ) : isHermes ? ( - - {hermesReady - ? t('agents.hermes.setupStatus.ready', { defaultValue: 'ACP ready' }) - : t('agents.hermes.setupStatus.needsSetup', { defaultValue: 'Needs setup' })} - - ) : authStatus.authenticated ? ( - - {t('agents.authStatus.connected')} - - ) : ( - - {t('agents.authStatus.disconnected')} - - )} -
-
- {!isHermes && authStatus.method !== 'api_key' && ( -
-
-
-
- {authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')} -
-
- {authStatus.authenticated - ? t('agents.login.reAuthDescription') - : t('agents.login.description', { agent: config.name })} + {authStatus.method !== 'api_key' && ( +
+
+
+
+ {authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')} +
+
+ {authStatus.authenticated + ? t('agents.login.reAuthDescription') + : t('agents.login.description', { agent: config.name })} +
+
-
-
- )} + )} - {isHermes && ( -
-
- {t('agents.hermes.actions.title', { defaultValue: 'Hermes tools' })} + {authStatus.error && ( +
+
+ {t('agents.error', { error: authStatus.error })} +
-
- {hermesActionGroups.map((group) => ( -
-
- {group.title} -
-
- {group.actions.map((action) => { - const Icon = action.icon; - return ( - - ); - })} -
-
- ))} -
-
- )} - - {authStatus.error && ( -
-
- {t('agents.error', { error: authStatus.error })} -
-
- )} -
+ )} +
+ )}
); diff --git a/src/stores/useSessionStore.ts b/src/stores/useSessionStore.ts index 999e1645..1141d926 100644 --- a/src/stores/useSessionStore.ts +++ b/src/stores/useSessionStore.ts @@ -166,6 +166,14 @@ function hasServerEchoForLocalUser( }); } +function assistantEchoFingerprint(message: NormalizedMessage): string | null { + const content = (message.content || '').trim(); + if (!content) { + return null; + } + return content.replace(/\s+/g, ''); +} + function compareMessagesChronologically(a: NormalizedMessage, b: NormalizedMessage): number { const timeA = readMessageTime(a) ?? 0; const timeB = readMessageTime(b) ?? 0; @@ -248,7 +256,7 @@ function isAssistantTextEchoedInSameTurnOnServer( serverMessages: NormalizedMessage[], realtimeMessages: NormalizedMessage[], ): boolean { - const assistantText = (message.content || '').trim(); + const assistantText = assistantEchoFingerprint(message); if (!assistantText) { return false; } @@ -264,7 +272,7 @@ function isAssistantTextEchoedInSameTurnOnServer( .some((serverMessage) => serverMessage.kind === 'text' && serverMessage.role === 'assistant' - && (serverMessage.content || '').trim() === assistantText, + && assistantEchoFingerprint(serverMessage) === assistantText, ); } @@ -281,9 +289,9 @@ function dedupeAdjacentAssistantEchoes(merged: NormalizedMessage[]): NormalizedM const prev = out[out.length - 1]; if (prev) { if (prev.kind === 'stream_delta' && m.kind === 'text' && m.role === 'assistant') { - const ps = (prev.content || '').trim(); - const ms = (m.content || '').trim(); - if (ps.length > 0 && ps === ms) { + const ps = assistantEchoFingerprint(prev); + const ms = assistantEchoFingerprint(m); + if (ps && ps === ms) { out[out.length - 1] = m; continue; } @@ -294,8 +302,12 @@ function dedupeAdjacentAssistantEchoes(merged: NormalizedMessage[]): NormalizedM && prev.role === 'assistant' && m.role === 'assistant' ) { - const ms = (m.content || '').trim(); - if (ms.length > 0 && ms === (prev.content || '').trim()) { + const ps = assistantEchoFingerprint(prev); + const ms = assistantEchoFingerprint(m); + if (ms && ms === ps) { + if ((m.content || '').length > (prev.content || '').length) { + out[out.length - 1] = m; + } continue; } }