mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-02 02:22:55 +08:00
feat: improve Hermes provider support
This commit is contained in:
@@ -48,15 +48,23 @@ function readStopReason(result) {
|
||||
return result.stopReason || result.stop_reason || result.reason || null;
|
||||
}
|
||||
|
||||
function buildPromptParams(sessionId, command, model) {
|
||||
const params = {
|
||||
function buildPromptParams(sessionId, command) {
|
||||
return {
|
||||
sessionId,
|
||||
prompt: [{ type: 'text', text: command }],
|
||||
};
|
||||
if (model) {
|
||||
params.modelId = model;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
function buildSessionSetupParams(sessionId, workingDir) {
|
||||
return {
|
||||
...(sessionId ? { sessionId } : {}),
|
||||
cwd: workingDir,
|
||||
mcpServers: [],
|
||||
};
|
||||
}
|
||||
|
||||
function canLoadSession(connection) {
|
||||
return connection?.initializeResult?.agentCapabilities?.loadSession === true;
|
||||
}
|
||||
|
||||
function findPermissionOption(options, kinds, fallbackOptionIds = []) {
|
||||
@@ -233,7 +241,7 @@ async function spawnHermes(command, options = {}, ws) {
|
||||
};
|
||||
|
||||
try {
|
||||
const resolvedModel = await providerModelsService.resolveResumeModel(PROVIDER, sessionId, requestedModel);
|
||||
await providerModelsService.resolveResumeModel(PROVIDER, sessionId, requestedModel);
|
||||
const connection = await hermesConnectionManager.getConnection(workingDir);
|
||||
activeHermesSessions.set(activeKey, {
|
||||
connection,
|
||||
@@ -278,20 +286,21 @@ async function spawnHermes(command, options = {}, ws) {
|
||||
|
||||
try {
|
||||
let sessionResult;
|
||||
if (sessionId) {
|
||||
if (sessionId && canLoadSession(connection)) {
|
||||
try {
|
||||
sessionResult = await connection.request('session/load', { sessionId, cwd: workingDir });
|
||||
sessionResult = await connection.request('session/load', buildSessionSetupParams(sessionId, workingDir));
|
||||
} catch {
|
||||
sessionResult = { sessionId };
|
||||
}
|
||||
} else {
|
||||
sessionResult = await connection.request('session/new', {
|
||||
cwd: workingDir,
|
||||
});
|
||||
sessionResult = await connection.request('session/new', buildSessionSetupParams(null, workingDir));
|
||||
}
|
||||
|
||||
registerSession(readSessionId(sessionResult) || sessionId, connection);
|
||||
const promptResult = await connection.request('session/prompt', buildPromptParams(capturedSessionId, command, resolvedModel));
|
||||
if (!capturedSessionId) {
|
||||
throw new Error('Hermes ACP did not return a session id.');
|
||||
}
|
||||
const promptResult = await connection.request('session/prompt', buildPromptParams(capturedSessionId, command));
|
||||
const finalSessionId = capturedSessionId || readSessionId(promptResult) || sessionId || activeKey;
|
||||
const stopReason = readStopReason(promptResult) || 'completed';
|
||||
const active = activeHermesSessions.get(finalSessionId) || activeHermesSessions.get(activeKey);
|
||||
|
||||
@@ -19,6 +19,7 @@ class AcpClient extends EventEmitter {
|
||||
this.buffer = '';
|
||||
this.requestHandlers = new Map();
|
||||
this.initialized = false;
|
||||
this.initializeResult = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -54,19 +55,20 @@ class AcpClient extends EventEmitter {
|
||||
}
|
||||
|
||||
this.start();
|
||||
await this.request('initialize', {
|
||||
this.initializeResult = await this.request('initialize', {
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: false,
|
||||
writeTextFile: false,
|
||||
},
|
||||
terminal: false,
|
||||
},
|
||||
clientInfo: {
|
||||
name: 'CloudCLI',
|
||||
title: 'CloudCLI',
|
||||
version: '1.0.0',
|
||||
},
|
||||
capabilities: {
|
||||
fs: false,
|
||||
terminal: false,
|
||||
session: {
|
||||
requestPermission: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.initialized = true;
|
||||
this.notify('initialized', {});
|
||||
@@ -95,7 +97,7 @@ class AcpClient extends EventEmitter {
|
||||
|
||||
const payload = { jsonrpc: '2.0', id, method, params };
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject });
|
||||
this.pending.set(id, { resolve, reject, method, params });
|
||||
this.writeMessage(payload);
|
||||
});
|
||||
}
|
||||
@@ -171,7 +173,13 @@ class AcpClient extends EventEmitter {
|
||||
}
|
||||
this.pending.delete(message.id);
|
||||
if (message.error) {
|
||||
pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
||||
const messageText = message.error.message || JSON.stringify(message.error);
|
||||
const error = new Error(`ACP ${pending.method} failed: ${messageText}`);
|
||||
error.code = message.error.code;
|
||||
error.data = message.error.data;
|
||||
error.method = pending.method;
|
||||
error.params = pending.params;
|
||||
pending.reject(error);
|
||||
} else {
|
||||
pending.resolve(message.result);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,7 @@ import type {
|
||||
ProviderModelsDefinition,
|
||||
ProviderSessionActiveModelChange,
|
||||
} from '@/shared/types.js';
|
||||
import {
|
||||
buildDefaultProviderCurrentActiveModel,
|
||||
readOptionalString,
|
||||
writeProviderSessionActiveModelChange,
|
||||
} from '@/shared/utils.js';
|
||||
import { readOptionalString } from '@/shared/utils.js';
|
||||
|
||||
export const HERMES_CONFIGURED_MODEL = '__hermes_configured_model__';
|
||||
|
||||
@@ -105,24 +101,21 @@ export class HermesProviderModels implements IProviderModels {
|
||||
return HERMES_FALLBACK_MODELS;
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ value: activeModel, label: activeModel },
|
||||
...HERMES_FALLBACK_MODELS.OPTIONS,
|
||||
];
|
||||
|
||||
return {
|
||||
OPTIONS: options,
|
||||
DEFAULT: activeModel,
|
||||
OPTIONS: [
|
||||
{
|
||||
value: HERMES_CONFIGURED_MODEL,
|
||||
label: 'Configured in Hermes',
|
||||
description: `Current Hermes model: ${activeModel}`,
|
||||
},
|
||||
],
|
||||
DEFAULT: HERMES_CONFIGURED_MODEL,
|
||||
};
|
||||
}
|
||||
|
||||
async getCurrentActiveModel(): Promise<ProviderCurrentActiveModel> {
|
||||
const configured = await this.readConfiguredModel();
|
||||
if (configured) {
|
||||
return { model: configured };
|
||||
}
|
||||
|
||||
return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
|
||||
return { model: configured ?? HERMES_CONFIGURED_MODEL };
|
||||
}
|
||||
|
||||
async changeActiveModel(input: ProviderChangeActiveModelInput): Promise<ProviderSessionActiveModelChange> {
|
||||
@@ -136,7 +129,13 @@ export class HermesProviderModels implements IProviderModels {
|
||||
};
|
||||
}
|
||||
|
||||
return writeProviderSessionActiveModelChange('hermes', input);
|
||||
return {
|
||||
provider: 'hermes',
|
||||
sessionId: input.sessionId,
|
||||
supported: false,
|
||||
changed: false,
|
||||
model: null,
|
||||
};
|
||||
}
|
||||
|
||||
private async readConfiguredModel(): Promise<string | null> {
|
||||
|
||||
@@ -60,6 +60,44 @@ function readEventSessionId(raw: AnyRecord, sessionId: string | null): string |
|
||||
return readOptionalString(raw.sessionId) ?? readOptionalString(raw.session_id) ?? sessionId;
|
||||
}
|
||||
|
||||
function readTextContent(value: unknown): string | null {
|
||||
const direct = readOptionalString(value);
|
||||
if (direct !== undefined) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const parts = value
|
||||
.map((entry) => readTextContent(entry))
|
||||
.filter((entry): entry is string => Boolean(entry?.trim()));
|
||||
return parts.length > 0 ? parts.join('') : null;
|
||||
}
|
||||
|
||||
const record = readObjectRecord(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestedContent = record.content;
|
||||
const nestedText = nestedContent === value ? null : readTextContent(nestedContent);
|
||||
|
||||
return readOptionalString(record.text)
|
||||
?? readOptionalString(record.content)
|
||||
?? nestedText
|
||||
?? readOptionalString(record.delta)
|
||||
?? readOptionalString(record.rawOutput)
|
||||
?? readOptionalString(record.raw_output)
|
||||
?? readOptionalString(record.output)
|
||||
?? null;
|
||||
}
|
||||
|
||||
function readToolPayload(raw: AnyRecord): AnyRecord {
|
||||
return readObjectRecord(raw.toolCall)
|
||||
?? readObjectRecord(raw.tool_call)
|
||||
?? readObjectRecord(raw.tool)
|
||||
?? raw;
|
||||
}
|
||||
|
||||
function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, history = false): NormalizedMessage[] {
|
||||
const envelope = readObjectRecord(rawMessage);
|
||||
if (!envelope) {
|
||||
@@ -75,10 +113,10 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
const baseId = readOptionalString(raw.id) ?? readOptionalString(raw.messageId) ?? readOptionalString(raw.message_id) ?? generateMessageId(PROVIDER);
|
||||
|
||||
if (['agent_message_chunk', 'assistant_message_chunk', 'message_delta', 'text_delta', 'text'].includes(type)) {
|
||||
const content = readOptionalString(raw.content)
|
||||
const content = readTextContent(raw.content)
|
||||
?? readOptionalString(raw.text)
|
||||
?? readOptionalString(raw.delta)
|
||||
?? readOptionalString(readObjectRecord(raw.message)?.content)
|
||||
?? readTextContent(readObjectRecord(raw.message)?.content)
|
||||
?? '';
|
||||
if (!content.trim()) {
|
||||
return [];
|
||||
@@ -96,9 +134,9 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
|
||||
if (['agent_message', 'assistant_message', 'message'].includes(type)) {
|
||||
const role = readOptionalString(raw.role) === 'user' ? 'user' : 'assistant';
|
||||
const content = readOptionalString(raw.content)
|
||||
const content = readTextContent(raw.content)
|
||||
?? readOptionalString(raw.text)
|
||||
?? readOptionalString(readObjectRecord(raw.message)?.content)
|
||||
?? readTextContent(readObjectRecord(raw.message)?.content)
|
||||
?? '';
|
||||
if (!content.trim()) {
|
||||
return [];
|
||||
@@ -115,7 +153,7 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
}
|
||||
|
||||
if (['agent_thought_chunk', 'thought_delta', 'thinking', 'reasoning'].includes(type)) {
|
||||
const content = readOptionalString(raw.content) ?? readOptionalString(raw.text) ?? readOptionalString(raw.delta) ?? '';
|
||||
const content = readTextContent(raw.content) ?? readOptionalString(raw.text) ?? readOptionalString(raw.delta) ?? '';
|
||||
if (!content.trim()) {
|
||||
return [];
|
||||
}
|
||||
@@ -130,8 +168,15 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
}
|
||||
|
||||
if (['tool_call', 'tool_use', 'tool_call_start'].includes(type)) {
|
||||
const tool = readObjectRecord(raw.tool);
|
||||
const toolId = readOptionalString(raw.toolCallId) ?? readOptionalString(raw.tool_call_id) ?? readOptionalString(raw.toolId) ?? baseId;
|
||||
const tool = readToolPayload(raw);
|
||||
const toolId = readOptionalString(raw.toolCallId)
|
||||
?? readOptionalString(raw.tool_call_id)
|
||||
?? readOptionalString(raw.toolId)
|
||||
?? readOptionalString(tool.toolCallId)
|
||||
?? readOptionalString(tool.tool_call_id)
|
||||
?? readOptionalString(tool.toolId)
|
||||
?? readOptionalString(tool.id)
|
||||
?? baseId;
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId: eventSessionId,
|
||||
@@ -143,22 +188,51 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
?? readOptionalString(raw.title)
|
||||
?? readOptionalString(raw.name)
|
||||
?? readOptionalString(tool?.name)
|
||||
?? readOptionalString(tool?.title)
|
||||
?? 'Tool',
|
||||
toolInput: raw.rawInput ?? raw.raw_input ?? raw.input ?? raw.arguments ?? raw.params ?? tool?.input ?? {},
|
||||
toolInput: raw.rawInput
|
||||
?? raw.raw_input
|
||||
?? raw.input
|
||||
?? raw.arguments
|
||||
?? raw.params
|
||||
?? tool?.rawInput
|
||||
?? tool?.raw_input
|
||||
?? tool?.input
|
||||
?? tool?.arguments
|
||||
?? {},
|
||||
toolId,
|
||||
})];
|
||||
}
|
||||
|
||||
if (['tool_call_update', 'tool_result', 'tool_call_result', 'tool_call_done'].includes(type)) {
|
||||
const tool = readToolPayload(raw);
|
||||
const content = readTextContent(raw.content)
|
||||
?? readTextContent(raw.rawOutput)
|
||||
?? readTextContent(raw.raw_output)
|
||||
?? readTextContent(raw.output)
|
||||
?? readTextContent(raw.result)
|
||||
?? readTextContent(tool.rawOutput)
|
||||
?? readTextContent(tool.raw_output)
|
||||
?? readTextContent(tool.output)
|
||||
?? readTextContent(tool.result)
|
||||
?? '';
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId: eventSessionId,
|
||||
timestamp,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_result',
|
||||
toolId: readOptionalString(raw.toolCallId) ?? readOptionalString(raw.tool_call_id) ?? readOptionalString(raw.toolId) ?? '',
|
||||
content: formatContent(raw.output ?? raw.result ?? raw.content ?? raw.delta ?? ''),
|
||||
isError: Boolean(raw.error) || raw.status === 'error',
|
||||
toolId: readOptionalString(raw.toolCallId)
|
||||
?? readOptionalString(raw.tool_call_id)
|
||||
?? readOptionalString(raw.toolId)
|
||||
?? readOptionalString(tool.toolCallId)
|
||||
?? readOptionalString(tool.tool_call_id)
|
||||
?? readOptionalString(tool.toolId)
|
||||
?? readOptionalString(tool.id)
|
||||
?? '',
|
||||
content: content || formatContent(raw.delta ?? ''),
|
||||
isError: Boolean(raw.error) || raw.status === 'error' || raw.status === 'failed',
|
||||
toolUseResult: raw.result ?? raw.output ?? raw.rawOutput ?? raw.raw_output ?? tool.result ?? tool.output ?? tool.rawOutput ?? tool.raw_output,
|
||||
})];
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,12 @@ function resolveResumeSessionId(
|
||||
return resolvedSessionId;
|
||||
}
|
||||
|
||||
function getHermesShellCommand(): string {
|
||||
return (process.env.HERMES_COMMAND_PATH || process.env.HERMES_CLI_PATH || 'hermes')
|
||||
.trim()
|
||||
.split(/\s+/)[0] || 'hermes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves provider command line for plain shell and agent-backed shell modes.
|
||||
*/
|
||||
@@ -161,6 +167,14 @@ function buildShellCommand(
|
||||
return initialCommand || 'opencode';
|
||||
}
|
||||
|
||||
if (provider === 'hermes') {
|
||||
const command = initialCommand || getHermesShellCommand();
|
||||
if (resumeSessionId) {
|
||||
return `${command} --resume "${resumeSessionId}"`;
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
const command = initialCommand || 'claude';
|
||||
if (resumeSessionId) {
|
||||
if (os.platform() === 'win32') {
|
||||
@@ -481,6 +495,8 @@ export function handleShellConnection(
|
||||
? 'Gemini'
|
||||
: provider === 'opencode'
|
||||
? 'OpenCode'
|
||||
: provider === 'hermes'
|
||||
? 'Hermes'
|
||||
: 'Claude';
|
||||
welcomeMsg = hasSession && resumeSessionId
|
||||
? `\x1b[36mResuming ${providerName} session ${resumeSessionId} in: ${projectPath}\x1b[0m\r\n`
|
||||
|
||||
@@ -946,7 +946,6 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
const codexModels = (await providerModelsService.getProviderModels('codex')).models;
|
||||
const geminiModels = (await providerModelsService.getProviderModels('gemini')).models;
|
||||
const opencodeModels = (await providerModelsService.getProviderModels('opencode')).models;
|
||||
const hermesModels = (await providerModelsService.getProviderModels('hermes')).models;
|
||||
|
||||
// Start the appropriate session
|
||||
if (provider === 'claude') {
|
||||
@@ -1006,7 +1005,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
projectPath: finalProjectPath,
|
||||
cwd: finalProjectPath,
|
||||
sessionId: sessionId || null,
|
||||
model: model || (hermesModels.DEFAULT === HERMES_CONFIGURED_MODEL ? undefined : hermesModels.DEFAULT)
|
||||
model: model === HERMES_CONFIGURED_MODEL ? undefined : model
|
||||
}, writer);
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,6 @@ export function useChatComposerState({
|
||||
codexModel,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
hermesModel,
|
||||
isLoading,
|
||||
canAbortSession,
|
||||
tokenBudget,
|
||||
@@ -339,7 +338,7 @@ export function useChatComposerState({
|
||||
: provider === 'opencode'
|
||||
? opencodeModel
|
||||
: provider === 'hermes'
|
||||
? (hermesModel === '__hermes_configured_model__' ? undefined : hermesModel)
|
||||
? undefined
|
||||
: claudeModel,
|
||||
tokenUsage: tokenBudget,
|
||||
};
|
||||
@@ -395,7 +394,6 @@ export function useChatComposerState({
|
||||
cursorModel,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
hermesModel,
|
||||
handleBuiltInCommand,
|
||||
handleCustomCommand,
|
||||
input,
|
||||
@@ -737,7 +735,7 @@ export function useChatComposerState({
|
||||
: provider === 'opencode'
|
||||
? opencodeModel
|
||||
: provider === 'hermes'
|
||||
? (hermesModel === '__hermes_configured_model__' ? undefined : hermesModel)
|
||||
? undefined
|
||||
: claudeModel;
|
||||
|
||||
// One message shape for every provider. The backend resolves the
|
||||
@@ -783,7 +781,6 @@ export function useChatComposerState({
|
||||
executeCommand,
|
||||
geminiModel,
|
||||
opencodeModel,
|
||||
hermesModel,
|
||||
isLoading,
|
||||
onSessionProcessing,
|
||||
onSessionEstablished,
|
||||
|
||||
@@ -417,6 +417,15 @@ export function useChatProviderState({ selectedSession, selectedProject }: UseCh
|
||||
model: string,
|
||||
sessionId?: string | null,
|
||||
) => {
|
||||
if (targetProvider === 'hermes') {
|
||||
setStoredProviderModel(targetProvider, model);
|
||||
return {
|
||||
scope: 'default' as const,
|
||||
changed: false,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedSessionId = typeof sessionId === 'string' ? sessionId.trim() : '';
|
||||
if (!normalizedSessionId) {
|
||||
setStoredProviderModel(targetProvider, model);
|
||||
|
||||
@@ -289,15 +289,11 @@ export default function ProviderSelectionEmptyState({
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate">{model.label}</div>
|
||||
{/*
|
||||
// * Temporarly commented out because the description of models from claude
|
||||
// * was a bit inconsistent. Will return it back when it becomes more consistent.
|
||||
*/}
|
||||
{/* {model.description && (
|
||||
{model.description && (
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{model.description}
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<Check className="ml-auto h-4 w-4 shrink-0 text-primary" />
|
||||
@@ -332,7 +328,7 @@ export default function ProviderSelectionEmptyState({
|
||||
defaultValue: "Ready with OpenCode {{model}}",
|
||||
}),
|
||||
hermes: t("providerSelection.readyPrompt.hermes", {
|
||||
model: hermesModel,
|
||||
model: provider === "hermes" ? currentModelLabel : hermesModel,
|
||||
defaultValue: "Ready with Hermes {{model}}",
|
||||
}),
|
||||
}[provider]
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from '../types';
|
||||
|
||||
type ProviderAuthStatusPayload = {
|
||||
installed?: boolean;
|
||||
authenticated?: boolean;
|
||||
email?: string | null;
|
||||
method?: string | null;
|
||||
@@ -34,6 +35,7 @@ const toProviderAuthStatus = (
|
||||
payload: ProviderAuthStatusPayload,
|
||||
fallbackError: string | null = null,
|
||||
): ProviderAuthStatus => ({
|
||||
installed: Boolean(payload.installed),
|
||||
authenticated: Boolean(payload.authenticated),
|
||||
email: payload.email ?? null,
|
||||
method: payload.method ?? null,
|
||||
@@ -78,6 +80,7 @@ export function useProviderAuthStatus(
|
||||
|
||||
if (!response.ok) {
|
||||
const status: ProviderAuthStatus = {
|
||||
installed: false,
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
@@ -95,6 +98,7 @@ export function useProviderAuthStatus(
|
||||
} catch (caughtError) {
|
||||
console.error(`Error checking ${provider} auth status:`, caughtError);
|
||||
const status: ProviderAuthStatus = {
|
||||
installed: false,
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LLMProvider } from '../../types/app';
|
||||
|
||||
export type ProviderAuthStatus = {
|
||||
installed: boolean;
|
||||
authenticated: boolean;
|
||||
email: string | null;
|
||||
method: string | null;
|
||||
@@ -22,10 +23,10 @@ export const PROVIDER_AUTH_STATUS_ENDPOINTS: Record<LLMProvider, string> = {
|
||||
};
|
||||
|
||||
export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuthStatusMap => ({
|
||||
claude: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
cursor: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
codex: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
gemini: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
opencode: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
hermes: { authenticated: false, email: null, method: null, error: null, loading },
|
||||
claude: { installed: false, authenticated: false, email: null, method: null, error: null, loading },
|
||||
cursor: { installed: false, authenticated: false, email: null, method: null, error: null, loading },
|
||||
codex: { installed: false, authenticated: false, email: null, method: null, error: null, loading },
|
||||
gemini: { installed: false, authenticated: false, email: null, method: null, error: null, loading },
|
||||
opencode: { installed: false, authenticated: false, email: null, method: null, error: null, loading },
|
||||
hermes: { installed: false, authenticated: false, email: null, method: null, error: null, loading },
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ export default function AgentListItem({
|
||||
}: AgentListItemProps) {
|
||||
const config = agentConfig[agentId];
|
||||
const colors = colorClasses[config.color];
|
||||
const isReady = agentId === 'hermes' ? authStatus.installed : authStatus.authenticated;
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
@@ -87,7 +88,7 @@ export default function AgentListItem({
|
||||
<div className="flex items-center justify-center gap-1.5">
|
||||
<SessionProviderLogo provider={agentId} className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate text-xs font-medium">{config.name}</span>
|
||||
{authStatus.authenticated && (
|
||||
{isReady && (
|
||||
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${colors.dot}`} />
|
||||
)}
|
||||
</div>
|
||||
@@ -107,10 +108,10 @@ export default function AgentListItem({
|
||||
>
|
||||
<SessionProviderLogo provider={agentId} className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{config.name}</span>
|
||||
{authStatus.authenticated ? (
|
||||
{isReady ? (
|
||||
<span className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${colors.dot}`} />
|
||||
) : authStatus.loading ? (
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-muted-foreground/30 animate-pulse" />
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 animate-pulse rounded-full bg-muted-foreground/30" />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -125,6 +125,8 @@ const hermesActionGroups: HermesActionGroup[] = [
|
||||
export default function AccountContent({ agent, authStatus, onLogin }: AccountContentProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
const config = agentConfig[agent];
|
||||
const isHermes = agent === 'hermes';
|
||||
const hermesReady = authStatus.installed;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -145,11 +147,17 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1">
|
||||
<div className={`font-medium ${config.textClass}`}>
|
||||
{t('agents.connectionStatus')}
|
||||
{isHermes
|
||||
? t('agents.hermes.setupStatus.title', { defaultValue: 'Setup status' })
|
||||
: t('agents.connectionStatus')}
|
||||
</div>
|
||||
<div className={`text-sm ${config.subtextClass}`}>
|
||||
{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.' })
|
||||
) : authStatus.authenticated ? (
|
||||
t('agents.authStatus.loggedInAs', {
|
||||
email: authStatus.email || t('agents.authStatus.authenticatedUser'),
|
||||
@@ -164,6 +172,19 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
||||
<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')}
|
||||
@@ -176,7 +197,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{authStatus.method !== 'api_key' && (
|
||||
{!isHermes && authStatus.method !== 'api_key' && (
|
||||
<div className="border-t border-border/50 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -201,7 +222,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agent === 'hermes' && (
|
||||
{isHermes && (
|
||||
<div className="border-t border-border/50 pt-4">
|
||||
<div className={`mb-3 font-medium ${config.textClass}`}>
|
||||
{t('agents.hermes.actions.title', { defaultValue: 'Hermes tools' })}
|
||||
|
||||
Reference in New Issue
Block a user