From 7c6d00ee9338c4d4aa684d5cf654f9b77ec59492 Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Tue, 30 Jun 2026 21:32:37 +0000 Subject: [PATCH] feat: refine Hermes settings UX --- public/icons/hermes-agent.png | Bin 0 -> 4100 bytes .../list/hermes/hermes-auth.provider.ts | 6 +- .../list/hermes/hermes-models.provider.ts | 8 +- .../llm-logo-provider/HermesLogo.tsx | 14 +- .../tabs/agents-settings/AgentListItem.tsx | 2 +- .../sections/content/AccountContent.tsx | 251 ++++++++---------- src/stores/useSessionStore.ts | 26 +- 7 files changed, 143 insertions(+), 164 deletions(-) create mode 100644 public/icons/hermes-agent.png diff --git a/public/icons/hermes-agent.png b/public/icons/hermes-agent.png new file mode 100644 index 0000000000000000000000000000000000000000..2d629d2f2452f00a7bc5a3252c1c1829c0b476bd GIT binary patch literal 4100 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 - - - - + 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; } }