mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-01 18:13:03 +08:00
feat: refine Hermes settings UX
This commit is contained in:
BIN
public/icons/hermes-agent.png
Normal file
BIN
public/icons/hermes-agent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
@@ -29,7 +29,7 @@ export class HermesProviderAuth implements IProviderAuth {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
email: null,
|
email: null,
|
||||||
method: null,
|
method: null,
|
||||||
error: 'Hermes ACP is not installed',
|
error: 'Hermes is not installed',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +39,8 @@ export class HermesProviderAuth implements IProviderAuth {
|
|||||||
installed,
|
installed,
|
||||||
authenticated: credentials.authenticated,
|
authenticated: credentials.authenticated,
|
||||||
email: credentials.email,
|
email: credentials.email,
|
||||||
method: credentials.method,
|
method: credentials.method ?? 'managed_by_hermes',
|
||||||
error: credentials.authenticated ? undefined : 'Hermes credentials were not found',
|
error: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export const HERMES_FALLBACK_MODELS: ProviderModelsDefinition = {
|
|||||||
OPTIONS: [
|
OPTIONS: [
|
||||||
{
|
{
|
||||||
value: HERMES_CONFIGURED_MODEL,
|
value: HERMES_CONFIGURED_MODEL,
|
||||||
label: 'Configured in Hermes',
|
label: 'Use Hermes default',
|
||||||
description: 'Uses the provider and model selected with `hermes model`.',
|
description: 'Uses the provider and model selected in Hermes.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
DEFAULT: HERMES_CONFIGURED_MODEL,
|
DEFAULT: HERMES_CONFIGURED_MODEL,
|
||||||
@@ -105,8 +105,8 @@ export class HermesProviderModels implements IProviderModels {
|
|||||||
OPTIONS: [
|
OPTIONS: [
|
||||||
{
|
{
|
||||||
value: HERMES_CONFIGURED_MODEL,
|
value: HERMES_CONFIGURED_MODEL,
|
||||||
label: 'Configured in Hermes',
|
label: 'Use Hermes default',
|
||||||
description: `Current Hermes model: ${activeModel}`,
|
description: `Uses the provider and model selected in Hermes. Current config: ${activeModel}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
DEFAULT: HERMES_CONFIGURED_MODEL,
|
DEFAULT: HERMES_CONFIGURED_MODEL,
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ type HermesLogoProps = {
|
|||||||
|
|
||||||
export default function HermesLogo({ className = 'w-5 h-5' }: HermesLogoProps) {
|
export default function HermesLogo({ className = 'w-5 h-5' }: HermesLogoProps) {
|
||||||
return (
|
return (
|
||||||
<svg className={className} viewBox="0 0 24 24" role="img" aria-label="Hermes">
|
<img
|
||||||
<rect width="24" height="24" rx="6" fill="#047857" />
|
className={`${className} block object-contain`}
|
||||||
<path
|
src="/icons/hermes-agent.png"
|
||||||
d="M6.2 6.5h2.4v4.3h6.8V6.5h2.4v11h-2.4v-4.6H8.6v4.6H6.2v-11Z"
|
alt="Hermes"
|
||||||
fill="white"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<path d="M9.3 4.7h5.4l-1.2 1.2h-3L9.3 4.7Z" fill="#A7F3D0" />
|
|
||||||
</svg>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function AgentListItem({
|
|||||||
}: AgentListItemProps) {
|
}: AgentListItemProps) {
|
||||||
const config = agentConfig[agentId];
|
const config = agentConfig[agentId];
|
||||||
const colors = colorClasses[config.color];
|
const colors = colorClasses[config.color];
|
||||||
const isReady = agentId === 'hermes' ? authStatus.installed : authStatus.authenticated;
|
const isReady = agentId !== 'hermes' && authStatus.authenticated;
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
CheckCircle2,
|
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Layers3,
|
Layers3,
|
||||||
LogIn,
|
LogIn,
|
||||||
@@ -88,37 +87,20 @@ type HermesAction = {
|
|||||||
icon: typeof Layers3;
|
icon: typeof Layers3;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HermesActionGroup = {
|
const hermesActions: HermesAction[] = [
|
||||||
title: string;
|
|
||||||
actions: HermesAction[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const hermesActionGroups: HermesActionGroup[] = [
|
|
||||||
{
|
{
|
||||||
title: 'Setup',
|
label: 'Configure model',
|
||||||
actions: [
|
description: 'Choose the provider and model Hermes should use.',
|
||||||
{
|
command: 'hermes model',
|
||||||
label: 'Provider setup',
|
title: 'Configure Hermes Model',
|
||||||
description: 'Configure provider credentials and the active model.',
|
icon: Layers3,
|
||||||
command: 'hermes model',
|
},
|
||||||
title: 'Hermes Provider Setup',
|
{
|
||||||
icon: Layers3,
|
label: 'Manage credentials',
|
||||||
},
|
description: 'Update credential pools and API keys.',
|
||||||
{
|
command: 'hermes auth',
|
||||||
label: 'Credential pools',
|
title: 'Hermes Credentials',
|
||||||
description: 'Manage API keys and OAuth credentials.',
|
icon: KeyRound,
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -126,7 +108,6 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
|||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const config = agentConfig[agent];
|
const config = agentConfig[agent];
|
||||||
const isHermes = agent === 'hermes';
|
const isHermes = agent === 'hermes';
|
||||||
const hermesReady = authStatus.installed;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -143,130 +124,118 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${config.bgClass} border ${config.borderClass} rounded-lg p-4`}>
|
<div className={`${config.bgClass} border ${config.borderClass} rounded-lg p-4`}>
|
||||||
<div className="space-y-4">
|
{isHermes ? (
|
||||||
<div className="flex items-center gap-3">
|
<div className="space-y-4">
|
||||||
<div className="flex-1">
|
<div>
|
||||||
<div className={`font-medium ${config.textClass}`}>
|
<div className={`font-medium ${config.textClass}`}>
|
||||||
{isHermes
|
{t('agents.hermes.configuration.title', { defaultValue: 'Hermes configuration' })}
|
||||||
? t('agents.hermes.setupStatus.title', { defaultValue: 'Setup status' })
|
|
||||||
: t('agents.connectionStatus')}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-sm ${config.subtextClass}`}>
|
<div className={`text-sm ${config.subtextClass}`}>
|
||||||
|
{t('agents.hermes.configuration.description', {
|
||||||
|
defaultValue: 'Models and credentials are managed by Hermes.',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2 md:grid-cols-2">
|
||||||
|
{hermesActions.map((action, index) => {
|
||||||
|
const Icon = action.icon;
|
||||||
|
const isPrimary = index === 0;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={action.command}
|
||||||
|
type="button"
|
||||||
|
variant={isPrimary ? 'default' : 'outline'}
|
||||||
|
className={
|
||||||
|
isPrimary
|
||||||
|
? `${config.buttonClass} h-auto justify-start gap-3 px-3 py-2 text-left text-white`
|
||||||
|
: 'h-auto justify-start gap-3 border-border/70 bg-background/70 px-3 py-2 text-left'
|
||||||
|
}
|
||||||
|
onClick={() => onLogin(action.command, action.title)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className={isPrimary ? 'block text-sm font-medium text-white' : 'block text-sm font-medium text-foreground'}>
|
||||||
|
{action.label}
|
||||||
|
</span>
|
||||||
|
<span className={isPrimary ? 'block text-xs text-white/80' : 'block text-xs text-muted-foreground'}>
|
||||||
|
{action.description}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className={`font-medium ${config.textClass}`}>
|
||||||
|
{t('agents.connectionStatus')}
|
||||||
|
</div>
|
||||||
|
<div className={`text-sm ${config.subtextClass}`}>
|
||||||
|
{authStatus.loading ? (
|
||||||
|
t('agents.authStatus.checkingAuth')
|
||||||
|
) : authStatus.authenticated ? (
|
||||||
|
t('agents.authStatus.loggedInAs', {
|
||||||
|
email: authStatus.email || t('agents.authStatus.authenticatedUser'),
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
t('agents.authStatus.notConnected')
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
{authStatus.loading ? (
|
{authStatus.loading ? (
|
||||||
t('agents.authStatus.checkingAuth')
|
<Badge variant="secondary" className="bg-muted">
|
||||||
) : isHermes ? (
|
{t('agents.authStatus.checking')}
|
||||||
hermesReady
|
</Badge>
|
||||||
? 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.' })
|
|
||||||
) : authStatus.authenticated ? (
|
) : authStatus.authenticated ? (
|
||||||
t('agents.authStatus.loggedInAs', {
|
<Badge variant="secondary" className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||||
email: authStatus.email || t('agents.authStatus.authenticatedUser'),
|
{t('agents.authStatus.connected')}
|
||||||
})
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
t('agents.authStatus.notConnected')
|
<Badge variant="secondary" className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300">
|
||||||
|
{t('agents.authStatus.disconnected')}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{authStatus.loading ? (
|
|
||||||
<Badge variant="secondary" className="bg-muted">
|
|
||||||
{t('agents.authStatus.checking')}
|
|
||||||
</Badge>
|
|
||||||
) : isHermes ? (
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className={
|
|
||||||
hermesReady
|
|
||||||
? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300'
|
|
||||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{hermesReady
|
|
||||||
? t('agents.hermes.setupStatus.ready', { defaultValue: 'ACP ready' })
|
|
||||||
: t('agents.hermes.setupStatus.needsSetup', { defaultValue: 'Needs setup' })}
|
|
||||||
</Badge>
|
|
||||||
) : authStatus.authenticated ? (
|
|
||||||
<Badge variant="secondary" className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
|
||||||
{t('agents.authStatus.connected')}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="secondary" className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300">
|
|
||||||
{t('agents.authStatus.disconnected')}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isHermes && authStatus.method !== 'api_key' && (
|
{authStatus.method !== 'api_key' && (
|
||||||
<div className="border-t border-border/50 pt-4">
|
<div className="border-t border-border/50 pt-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className={`font-medium ${config.textClass}`}>
|
<div className={`font-medium ${config.textClass}`}>
|
||||||
{authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')}
|
{authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')}
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-sm ${config.subtextClass}`}>
|
<div className={`text-sm ${config.subtextClass}`}>
|
||||||
{authStatus.authenticated
|
{authStatus.authenticated
|
||||||
? t('agents.login.reAuthDescription')
|
? t('agents.login.reAuthDescription')
|
||||||
: t('agents.login.description', { agent: config.name })}
|
: t('agents.login.description', { agent: config.name })}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => onLogin()}
|
||||||
|
className={`${config.buttonClass} text-white`}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<LogIn className="mr-2 h-4 w-4" />
|
||||||
|
{authStatus.authenticated ? t('agents.login.reLoginButton') : t('agents.login.button')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={() => onLogin()}
|
|
||||||
className={`${config.buttonClass} text-white`}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<LogIn className="mr-2 h-4 w-4" />
|
|
||||||
{authStatus.authenticated ? t('agents.login.reLoginButton') : t('agents.login.button')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{isHermes && (
|
{authStatus.error && (
|
||||||
<div className="border-t border-border/50 pt-4">
|
<div className="border-t border-border/50 pt-4">
|
||||||
<div className={`mb-3 font-medium ${config.textClass}`}>
|
<div className="text-sm text-red-600 dark:text-red-400">
|
||||||
{t('agents.hermes.actions.title', { defaultValue: 'Hermes tools' })}
|
{t('agents.error', { error: authStatus.error })}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
)}
|
||||||
{hermesActionGroups.map((group) => (
|
</div>
|
||||||
<div key={group.title}>
|
)}
|
||||||
<div className={`mb-2 text-xs font-semibold uppercase ${config.subtextClass}`}>
|
|
||||||
{group.title}
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2 md:grid-cols-2">
|
|
||||||
{group.actions.map((action) => {
|
|
||||||
const Icon = action.icon;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={action.command}
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="h-auto justify-start gap-3 border-border/70 bg-background/70 px-3 py-2 text-left"
|
|
||||||
onClick={() => onLogin(action.command, action.title)}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
|
||||||
<span className="min-w-0">
|
|
||||||
<span className="block text-sm font-medium text-foreground">{action.label}</span>
|
|
||||||
<span className="block text-xs text-muted-foreground">{action.description}</span>
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{authStatus.error && (
|
|
||||||
<div className="border-t border-border/50 pt-4">
|
|
||||||
<div className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{t('agents.error', { error: authStatus.error })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
function compareMessagesChronologically(a: NormalizedMessage, b: NormalizedMessage): number {
|
||||||
const timeA = readMessageTime(a) ?? 0;
|
const timeA = readMessageTime(a) ?? 0;
|
||||||
const timeB = readMessageTime(b) ?? 0;
|
const timeB = readMessageTime(b) ?? 0;
|
||||||
@@ -248,7 +256,7 @@ function isAssistantTextEchoedInSameTurnOnServer(
|
|||||||
serverMessages: NormalizedMessage[],
|
serverMessages: NormalizedMessage[],
|
||||||
realtimeMessages: NormalizedMessage[],
|
realtimeMessages: NormalizedMessage[],
|
||||||
): boolean {
|
): boolean {
|
||||||
const assistantText = (message.content || '').trim();
|
const assistantText = assistantEchoFingerprint(message);
|
||||||
if (!assistantText) {
|
if (!assistantText) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -264,7 +272,7 @@ function isAssistantTextEchoedInSameTurnOnServer(
|
|||||||
.some((serverMessage) =>
|
.some((serverMessage) =>
|
||||||
serverMessage.kind === 'text'
|
serverMessage.kind === 'text'
|
||||||
&& serverMessage.role === 'assistant'
|
&& 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];
|
const prev = out[out.length - 1];
|
||||||
if (prev) {
|
if (prev) {
|
||||||
if (prev.kind === 'stream_delta' && m.kind === 'text' && m.role === 'assistant') {
|
if (prev.kind === 'stream_delta' && m.kind === 'text' && m.role === 'assistant') {
|
||||||
const ps = (prev.content || '').trim();
|
const ps = assistantEchoFingerprint(prev);
|
||||||
const ms = (m.content || '').trim();
|
const ms = assistantEchoFingerprint(m);
|
||||||
if (ps.length > 0 && ps === ms) {
|
if (ps && ps === ms) {
|
||||||
out[out.length - 1] = m;
|
out[out.length - 1] = m;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -294,8 +302,12 @@ function dedupeAdjacentAssistantEchoes(merged: NormalizedMessage[]): NormalizedM
|
|||||||
&& prev.role === 'assistant'
|
&& prev.role === 'assistant'
|
||||||
&& m.role === 'assistant'
|
&& m.role === 'assistant'
|
||||||
) {
|
) {
|
||||||
const ms = (m.content || '').trim();
|
const ps = assistantEchoFingerprint(prev);
|
||||||
if (ms.length > 0 && ms === (prev.content || '').trim()) {
|
const ms = assistantEchoFingerprint(m);
|
||||||
|
if (ms && ms === ps) {
|
||||||
|
if ((m.content || '').length > (prev.content || '').length) {
|
||||||
|
out[out.length - 1] = m;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user