diff --git a/src/app/main/controlSurface/handlers/nodeControlHandlerRuntimeMetadata.ts b/src/app/main/controlSurface/handlers/nodeControlHandlerRuntimeMetadata.ts index 83135771c..398927a81 100644 --- a/src/app/main/controlSurface/handlers/nodeControlHandlerRuntimeMetadata.ts +++ b/src/app/main/controlSurface/handlers/nodeControlHandlerRuntimeMetadata.ts @@ -2,7 +2,13 @@ import type { AgentProviderId, TerminalRuntimeKind } from '../../../../shared/co import { createAppError } from '../../../../shared/errors/appError' export function managedAgentProvider(value: unknown): AgentProviderId { - if (value === 'claude-code' || value === 'codex' || value === 'opencode' || value === 'gemini') { + if ( + value === 'claude-code' || + value === 'codex' || + value === 'opencode' || + value === 'gemini' || + value === 'hermes' + ) { return value } throw createAppError('agent.launch_failed', { debugMessage: 'Invalid launched provider.' }) diff --git a/src/app/main/controlSurface/handlers/sessionLaunchPayloadSupport.ts b/src/app/main/controlSurface/handlers/sessionLaunchPayloadSupport.ts index be52eb89f..eaf6dc4a1 100644 --- a/src/app/main/controlSurface/handlers/sessionLaunchPayloadSupport.ts +++ b/src/app/main/controlSurface/handlers/sessionLaunchPayloadSupport.ts @@ -45,7 +45,8 @@ export function normalizeAgentProviderId( provider === 'claude-code' || provider === 'codex' || provider === 'opencode' || - provider === 'gemini' + provider === 'gemini' || + provider === 'hermes' ) { return provider } diff --git a/src/app/main/diagnostics/performanceProcessClassifier.ts b/src/app/main/diagnostics/performanceProcessClassifier.ts index f796b25f0..40b400bb1 100644 --- a/src/app/main/diagnostics/performanceProcessClassifier.ts +++ b/src/app/main/diagnostics/performanceProcessClassifier.ts @@ -57,6 +57,9 @@ function classifyExternalAgent( if (nameStem === 'gemini' || hasExecutableToken(commandLine, 'gemini')) { return 'external-agent-gemini' } + if (nameStem === 'hermes' || hasExecutableToken(commandLine, 'hermes')) { + return 'external-agent-hermes' + } return null } @@ -144,6 +147,7 @@ export function resolvePerformanceProcessScope( kind === 'external-agent-claude' || kind === 'external-agent-opencode' || kind === 'external-agent-gemini' || + kind === 'external-agent-hermes' || kind === 'external-output-stub' ) { return 'external-agent' diff --git a/src/app/main/ipc/normalize.ts b/src/app/main/ipc/normalize.ts index 2184380ae..be5ba9dbb 100644 --- a/src/app/main/ipc/normalize.ts +++ b/src/app/main/ipc/normalize.ts @@ -2,7 +2,13 @@ import type { AgentProviderId } from '../../../shared/contracts/dto' import { createAppError } from '../../../shared/errors/appError' export function normalizeProvider(value: unknown): AgentProviderId { - if (value !== 'claude-code' && value !== 'codex' && value !== 'opencode' && value !== 'gemini') { + if ( + value !== 'claude-code' && + value !== 'codex' && + value !== 'opencode' && + value !== 'gemini' && + value !== 'hermes' + ) { throw createAppError('common.invalid_input', { debugMessage: 'Invalid provider' }) } diff --git a/src/app/renderer/components/AgentProviderIcon.tsx b/src/app/renderer/components/AgentProviderIcon.tsx index c7a06b3cc..410d3fe7e 100644 --- a/src/app/renderer/components/AgentProviderIcon.tsx +++ b/src/app/renderer/components/AgentProviderIcon.tsx @@ -79,6 +79,35 @@ function ProviderGlyph({ provider }: { provider: AgentProvider }): JSX.Element { ) + case 'hermes': + return ( + + ) case 'codex': default: return ( diff --git a/src/app/renderer/i18n/locales/en.settingsPanel.diagnostics.ts b/src/app/renderer/i18n/locales/en.settingsPanel.diagnostics.ts index 3fc085377..296a1f1ee 100644 --- a/src/app/renderer/i18n/locales/en.settingsPanel.diagnostics.ts +++ b/src/app/renderer/i18n/locales/en.settingsPanel.diagnostics.ts @@ -55,6 +55,7 @@ export const enSettingsPanelDiagnostics = { 'external-agent-claude': 'Claude CLI', 'external-agent-opencode': 'OpenCode CLI', 'external-agent-gemini': 'Gemini CLI', + 'external-agent-hermes': 'Hermes CLI', 'external-output-stub': 'Output stub', 'external-shell': 'Shell', 'windows-console-host': 'Windows console host', diff --git a/src/app/renderer/i18n/locales/zh-CN.settingsPanel.diagnostics.ts b/src/app/renderer/i18n/locales/zh-CN.settingsPanel.diagnostics.ts index a47cbc19d..59ba0538c 100644 --- a/src/app/renderer/i18n/locales/zh-CN.settingsPanel.diagnostics.ts +++ b/src/app/renderer/i18n/locales/zh-CN.settingsPanel.diagnostics.ts @@ -55,6 +55,7 @@ export const zhCNSettingsPanelDiagnostics = { 'external-agent-claude': 'Claude CLI', 'external-agent-opencode': 'OpenCode CLI', 'external-agent-gemini': 'Gemini CLI', + 'external-agent-hermes': 'Hermes CLI', 'external-output-stub': '输出 stub', 'external-shell': 'Shell', 'windows-console-host': 'Windows console host', diff --git a/src/app/renderer/shell/utils/format.ts b/src/app/renderer/shell/utils/format.ts index 3a4200586..56d54d037 100644 --- a/src/app/renderer/shell/utils/format.ts +++ b/src/app/renderer/shell/utils/format.ts @@ -65,7 +65,9 @@ export function toAgentNodeTitle(provider: AgentProvider, model: string | null): ? 'opencode' : provider === 'gemini' ? 'gemini' - : 'codex' + : provider === 'hermes' + ? 'hermes' + : 'codex' return `${providerTitle} · ${model ?? translate('common.defaultModel')}` } diff --git a/src/app/renderer/styles/workspace-agent-item.css b/src/app/renderer/styles/workspace-agent-item.css index 77b50e1ba..3a643349b 100644 --- a/src/app/renderer/styles/workspace-agent-item.css +++ b/src/app/renderer/styles/workspace-agent-item.css @@ -33,6 +33,10 @@ --agent-provider-accent: rgba(196, 181, 253, 0.96); } +.agent-provider-icon[data-agent-provider='hermes'] { + --agent-provider-accent: rgba(94, 234, 212, 0.96); +} + .agent-provider-icon__glyph, .agent-provider-icon__svg { width: 11px; diff --git a/src/contexts/agent/infrastructure/cli/AgentCliAvailability.ts b/src/contexts/agent/infrastructure/cli/AgentCliAvailability.ts index 99f71b48f..173b12d9f 100644 --- a/src/contexts/agent/infrastructure/cli/AgentCliAvailability.ts +++ b/src/contexts/agent/infrastructure/cli/AgentCliAvailability.ts @@ -5,7 +5,13 @@ import type { } from '@shared/contracts/dto' import { resolveAgentProviderAvailability } from './AgentExecutableResolver' -const AGENT_PROVIDERS: readonly AgentProviderId[] = ['claude-code', 'codex', 'opencode', 'gemini'] +const AGENT_PROVIDERS: readonly AgentProviderId[] = [ + 'claude-code', + 'codex', + 'opencode', + 'gemini', + 'hermes', +] function toAvailabilityRecord( entries: AgentProviderAvailability[], diff --git a/src/contexts/agent/infrastructure/cli/AgentCommandFactory.ts b/src/contexts/agent/infrastructure/cli/AgentCommandFactory.ts index 2cc75f506..f5ae81413 100644 --- a/src/contexts/agent/infrastructure/cli/AgentCommandFactory.ts +++ b/src/contexts/agent/infrastructure/cli/AgentCommandFactory.ts @@ -36,6 +36,10 @@ export function resolveAgentCliCommand(provider: AgentProviderId): string { return 'gemini' } + if (provider === 'hermes') { + return 'hermes' + } + return 'codex' } @@ -203,6 +207,36 @@ export function buildAgentLaunchCommand(input: BuildAgentLaunchCommandInput): Ag } } + if (input.provider === 'hermes') { + if (input.mode === 'resume') { + if (!resumeSessionId) { + throw new Error('hermes resume requires explicit session id') + } + + return { + command: 'hermes', + args: ['chat', '--resume', resumeSessionId], + launchMode: 'resume', + effectiveModel, + resumeSessionId, + } + } + + const args = ['chat'] + const prompt = normalizePrompt(input.prompt) + if (prompt.length > 0) { + args.push('-q', prompt) + } + + return { + command: 'hermes', + args, + launchMode: 'new', + effectiveModel, + resumeSessionId: null, + } + } + if (input.mode === 'resume') { if (!resumeSessionId) { throw new Error('codex resume requires explicit session id') diff --git a/src/contexts/agent/infrastructure/cli/AgentModelService.ts b/src/contexts/agent/infrastructure/cli/AgentModelService.ts index 5b31bb2f7..06731da3a 100644 --- a/src/contexts/agent/infrastructure/cli/AgentModelService.ts +++ b/src/contexts/agent/infrastructure/cli/AgentModelService.ts @@ -199,10 +199,23 @@ async function listOpenCodeModelsFromCli( })) } +const HERMES_STATIC_MODELS: AgentModelOption[] = [ + { + id: 'hermes-default', + displayName: 'Hermes Default', + description: 'Hermes default model (provider-configured)', + isDefault: true, + }, +] + function listClaudeCodeStaticModels(): AgentModelOption[] { return CLAUDE_CODE_STATIC_MODELS.map(model => ({ ...model })) } +function listHermesStaticModels(): AgentModelOption[] { + return HERMES_STATIC_MODELS.map(model => ({ ...model })) +} + export function disposeAgentModelService(): void { codexModelsRequestInFlight = null cachedCodexModels = null @@ -280,6 +293,18 @@ export async function listAgentModels(options: { } } + if (provider === 'hermes') { + const fetchedAt = new Date().toISOString() + + return { + provider, + source: 'hermes-static', + fetchedAt, + models: listHermesStaticModels(), + error: null, + } + } + if (provider === 'gemini') { const cachedResult = readCachedGeminiModels() if (cachedResult) { diff --git a/src/contexts/agent/infrastructure/cli/AgentProviderInstaller.ts b/src/contexts/agent/infrastructure/cli/AgentProviderInstaller.ts index fe79ea1e5..17b131fb5 100644 --- a/src/contexts/agent/infrastructure/cli/AgentProviderInstaller.ts +++ b/src/contexts/agent/infrastructure/cli/AgentProviderInstaller.ts @@ -27,6 +27,7 @@ const AGENT_PROVIDER_NPM_PACKAGES: Record = { codex: '@openai/codex', opencode: 'opencode-ai', gemini: '@google/gemini-cli', + hermes: 'hermes-agent', } interface CommandOutput { diff --git a/src/contexts/agent/infrastructure/cli/AgentSessionCatalog.ts b/src/contexts/agent/infrastructure/cli/AgentSessionCatalog.ts index 4669406e5..78204dd21 100644 --- a/src/contexts/agent/infrastructure/cli/AgentSessionCatalog.ts +++ b/src/contexts/agent/infrastructure/cli/AgentSessionCatalog.ts @@ -487,6 +487,14 @@ export async function listAgentSessions( const limit = normalizeLimit(input.limit) const titleCache = options?.titleCache + if (input.provider === 'hermes') { + return { + provider: input.provider, + cwd: resolvedCwd, + sessions: [], + } + } + const sessions = input.provider === 'claude-code' ? await listClaudeSessions(resolvedCwd, limit, titleCache) diff --git a/src/contexts/agent/infrastructure/cli/AgentSessionLocator.ts b/src/contexts/agent/infrastructure/cli/AgentSessionLocator.ts index a8c175a3a..9efd907d6 100644 --- a/src/contexts/agent/infrastructure/cli/AgentSessionLocator.ts +++ b/src/contexts/agent/infrastructure/cli/AgentSessionLocator.ts @@ -300,6 +300,48 @@ async function findCodexResumeSessionId(cwd: string, startedAtMs: number): Promi return sessionId ?? null } +async function findHermesResumeSessionId( + _cwd: string, + startedAtMs: number, +): Promise { + try { + const sessionsIndexPath = join( + resolveHomeDirectoryCandidates()[0] ?? '', + '.hermes', + 'sessions', + 'sessions.json', + ) + const raw = await fs.readFile(sessionsIndexPath, 'utf8') + const index = JSON.parse(raw) as Record< + string, + { session_id?: string; platform?: string; updated_at?: string } + > + + let bestSessionId: string | null = null + let bestUpdatedAt = 0 + + for (const entry of Object.values(index)) { + if (!entry.session_id || entry.platform !== 'cli') { + continue + } + + const updatedAt = entry.updated_at ? Date.parse(entry.updated_at) : 0 + if (Number.isNaN(updatedAt)) { + continue + } + + if (updatedAt > bestUpdatedAt && updatedAt >= startedAtMs - 6000) { + bestUpdatedAt = updatedAt + bestSessionId = entry.session_id + } + } + + return bestSessionId + } catch { + return null + } +} + async function tryFindResumeSessionId( provider: AgentProviderId, cwd: string, @@ -317,6 +359,10 @@ async function tryFindResumeSessionId( return await findOpenCodeResumeSessionId(cwd, startedAtMs) } + if (provider === 'hermes') { + return await findHermesResumeSessionId(cwd, startedAtMs) + } + return await findGeminiResumeSessionId(cwd, startedAtMs) } diff --git a/src/contexts/settings/domain/agentSettings.defaults.ts b/src/contexts/settings/domain/agentSettings.defaults.ts index eebbc7a13..e40a27cd9 100644 --- a/src/contexts/settings/domain/agentSettings.defaults.ts +++ b/src/contexts/settings/domain/agentSettings.defaults.ts @@ -21,18 +21,21 @@ export const DEFAULT_AGENT_SETTINGS: AgentSettings = { codex: false, opencode: false, gemini: false, + hermes: false, }, customModelByProvider: { 'claude-code': '', codex: '', opencode: '', gemini: '', + hermes: '', }, customModelOptionsByProvider: { 'claude-code': [], codex: [], opencode: [], gemini: [], + hermes: [], }, taskTitleProvider: 'default', taskTitleModel: '', diff --git a/src/contexts/settings/domain/agentSettings.executables.ts b/src/contexts/settings/domain/agentSettings.executables.ts index eaf73ddbe..5650dda8a 100644 --- a/src/contexts/settings/domain/agentSettings.executables.ts +++ b/src/contexts/settings/domain/agentSettings.executables.ts @@ -10,6 +10,7 @@ export const DEFAULT_AGENT_EXECUTABLE_PATH_OVERRIDE_BY_PROVIDER: AgentExecutable codex: '', opencode: '', gemini: '', + hermes: '', } export function normalizeAgentExecutablePathOverrideByProvider( diff --git a/src/contexts/settings/domain/agentSettings.providerMeta.ts b/src/contexts/settings/domain/agentSettings.providerMeta.ts index 4b1db4cdc..3a7ce0a3f 100644 --- a/src/contexts/settings/domain/agentSettings.providerMeta.ts +++ b/src/contexts/settings/domain/agentSettings.providerMeta.ts @@ -5,6 +5,7 @@ export const AGENT_PROVIDER_LABEL: Record = { codex: 'Codex', opencode: 'OpenCode', gemini: 'Gemini CLI', + hermes: 'Hermes', } export interface AgentProviderCapabilities { @@ -39,4 +40,10 @@ export const AGENT_PROVIDER_CAPABILITIES: Record { diff --git a/src/contexts/terminal/presentation/main-ipc/sessionStateWatcherInteraction.ts b/src/contexts/terminal/presentation/main-ipc/sessionStateWatcherInteraction.ts index 02e15f58a..ff810e2f9 100644 --- a/src/contexts/terminal/presentation/main-ipc/sessionStateWatcherInteraction.ts +++ b/src/contexts/terminal/presentation/main-ipc/sessionStateWatcherInteraction.ts @@ -1,7 +1,7 @@ import type { AgentProviderId } from '../../../../shared/contracts/dto' function isJsonlProvider(provider: AgentProviderId): boolean { - return provider === 'claude-code' || provider === 'codex' + return provider === 'claude-code' || provider === 'codex' || provider === 'hermes' } function isExplicitSubmitInteraction(data: string | undefined): boolean { diff --git a/src/contexts/terminal/presentation/main-ipc/sessionStateWatcherShared.ts b/src/contexts/terminal/presentation/main-ipc/sessionStateWatcherShared.ts index d7cc5abc8..d8d04fd9c 100644 --- a/src/contexts/terminal/presentation/main-ipc/sessionStateWatcherShared.ts +++ b/src/contexts/terminal/presentation/main-ipc/sessionStateWatcherShared.ts @@ -22,5 +22,5 @@ export function logSessionStateWatcherDiagnostics( } export function isJsonlProvider(provider: AgentProviderId): boolean { - return provider === 'claude-code' || provider === 'codex' + return provider === 'claude-code' || provider === 'codex' || provider === 'hermes' } diff --git a/src/contexts/workspace/application/nodeControl/nodeDataFactory.ts b/src/contexts/workspace/application/nodeControl/nodeDataFactory.ts index 4f12fa1ff..232b83375 100644 --- a/src/contexts/workspace/application/nodeControl/nodeDataFactory.ts +++ b/src/contexts/workspace/application/nodeControl/nodeDataFactory.ts @@ -120,7 +120,8 @@ function normalizeAgentProvider(value: unknown): AgentProviderId | null { normalized === 'claude-code' || normalized === 'codex' || normalized === 'opencode' || - normalized === 'gemini' + normalized === 'gemini' || + normalized === 'hermes' ) { return normalized } diff --git a/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/hooks/useNodesStore.terminalProviderHint.ts b/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/hooks/useNodesStore.terminalProviderHint.ts index f7784607b..286769ecd 100644 --- a/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/hooks/useNodesStore.terminalProviderHint.ts +++ b/src/contexts/workspace/presentation/renderer/components/workspaceCanvas/hooks/useNodesStore.terminalProviderHint.ts @@ -27,5 +27,9 @@ export function resolveTerminalProviderHintFromCommand(command: string): AgentPr return 'gemini' } + if (executableName === 'hermes') { + return 'hermes' + } + return null } diff --git a/src/contexts/workspace/presentation/renderer/utils/agentTitle.ts b/src/contexts/workspace/presentation/renderer/utils/agentTitle.ts index d267c633d..64c779e86 100644 --- a/src/contexts/workspace/presentation/renderer/utils/agentTitle.ts +++ b/src/contexts/workspace/presentation/renderer/utils/agentTitle.ts @@ -15,6 +15,10 @@ export function providerTitlePrefix(provider: AgentProvider): string { return 'gemini' } + if (provider === 'hermes') { + return 'hermes' + } + return 'codex' } diff --git a/src/contexts/worktree/infrastructure/git/WorktreeNameSuggester.ts b/src/contexts/worktree/infrastructure/git/WorktreeNameSuggester.ts index f7ace9fa4..ee1c8fbb1 100644 --- a/src/contexts/worktree/infrastructure/git/WorktreeNameSuggester.ts +++ b/src/contexts/worktree/infrastructure/git/WorktreeNameSuggester.ts @@ -120,7 +120,7 @@ function testModeSuggestion(input: SuggestWorktreeNamesInput): SuggestWorktreeNa } async function executeCommand( - provider: 'claude-code' | 'codex', + provider: 'claude-code' | 'codex' | 'hermes', args: string[], cwd: string, ): Promise { diff --git a/src/contexts/worktree/infrastructure/git/WorktreeNameSuggestionCommandFactory.ts b/src/contexts/worktree/infrastructure/git/WorktreeNameSuggestionCommandFactory.ts index 538878f5c..a23ecd847 100644 --- a/src/contexts/worktree/infrastructure/git/WorktreeNameSuggestionCommandFactory.ts +++ b/src/contexts/worktree/infrastructure/git/WorktreeNameSuggestionCommandFactory.ts @@ -47,6 +47,24 @@ export function buildWorktreeNameSuggestionCommand( } } + if (input.provider === 'hermes') { + const args = ['chat', '-q'] + + if (effectiveModel) { + args.push('--model', effectiveModel) + } + + args.push(input.prompt) + + return { + command: 'hermes', + args, + provider: input.provider, + effectiveModel, + outputMode: 'stdout', + } + } + const args = ['exec', '--skip-git-repo-check', '--sandbox', 'read-only'] if (effectiveModel) { diff --git a/src/shared/contracts/dto/agent.ts b/src/shared/contracts/dto/agent.ts index 9bc8a6850..8e1ad66c0 100644 --- a/src/shared/contracts/dto/agent.ts +++ b/src/shared/contracts/dto/agent.ts @@ -1,6 +1,11 @@ -export type AgentProviderId = 'claude-code' | 'codex' | 'opencode' | 'gemini' +export type AgentProviderId = 'claude-code' | 'codex' | 'opencode' | 'gemini' | 'hermes' -export type AgentModelCatalogSource = 'claude-static' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' +export type AgentModelCatalogSource = + | 'claude-static' + | 'codex-cli' + | 'opencode-cli' + | 'gemini-cli' + | 'hermes-static' export type ExecutableResolutionSource = | 'override' | 'shell_env_path' @@ -11,6 +16,7 @@ export type AgentSessionSummarySource = | 'claude-jsonl' | 'codex-file' | 'gemini-file' + | 'hermes-db' | 'opencode-cli' | 'opencode-db' | 'control-surface' diff --git a/src/shared/contracts/dto/performanceDiagnostics.ts b/src/shared/contracts/dto/performanceDiagnostics.ts index 3ce44f0d3..023ff7e56 100644 --- a/src/shared/contracts/dto/performanceDiagnostics.ts +++ b/src/shared/contracts/dto/performanceDiagnostics.ts @@ -15,6 +15,7 @@ export type PerformanceProcessKind = | 'external-agent-claude' | 'external-agent-opencode' | 'external-agent-gemini' + | 'external-agent-hermes' | 'external-output-stub' | 'external-shell' | 'windows-console-host' diff --git a/tests/unit/contexts/agentConfig.spec.ts b/tests/unit/contexts/agentConfig.spec.ts index d362d1249..9d2a0a13a 100644 --- a/tests/unit/contexts/agentConfig.spec.ts +++ b/tests/unit/contexts/agentConfig.spec.ts @@ -42,7 +42,13 @@ describe('agent settings normalization', () => { agentProviderOrder: ['gemini', 'codex', 'gemini', 'invalid'], }) - expect(result.agentProviderOrder).toEqual(['gemini', 'codex', 'claude-code', 'opencode']) + expect(result.agentProviderOrder).toEqual([ + 'gemini', + 'codex', + 'claude-code', + 'opencode', + 'hermes', + ]) }) it('keeps valid provider, custom model, and model option fields', () => { diff --git a/tests/unit/contexts/agentSettings.spec.ts b/tests/unit/contexts/agentSettings.spec.ts index 8306980e3..fb7d08ae4 100644 --- a/tests/unit/contexts/agentSettings.spec.ts +++ b/tests/unit/contexts/agentSettings.spec.ts @@ -17,6 +17,7 @@ describe('normalizeAgentSettings', () => { codex: '', opencode: '', gemini: '', + hermes: '', }) }) @@ -254,6 +255,7 @@ describe('normalizeAgentSettings', () => { codex: '/opt/tools/codex', opencode: '', gemini: '', + hermes: '', }) }) diff --git a/tests/unit/contexts/settingsPanel.spec.tsx b/tests/unit/contexts/settingsPanel.spec.tsx index 7db2595bc..93ca5670b 100644 --- a/tests/unit/contexts/settingsPanel.spec.tsx +++ b/tests/unit/contexts/settingsPanel.spec.tsx @@ -159,7 +159,7 @@ describe('SettingsPanel', () => { expect(onChange).toHaveBeenCalledWith({ ...DEFAULT_AGENT_SETTINGS, - agentProviderOrder: ['codex', 'claude-code', 'opencode', 'gemini'], + agentProviderOrder: ['codex', 'claude-code', 'opencode', 'gemini', 'hermes'], }) })