Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export function normalizeAgentProviderId(
provider === 'claude-code' ||
provider === 'codex' ||
provider === 'opencode' ||
provider === 'gemini'
provider === 'gemini' ||
provider === 'hermes'
) {
return provider
}
Expand Down
4 changes: 4 additions & 0 deletions src/app/main/diagnostics/performanceProcessClassifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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'
Expand Down
8 changes: 7 additions & 1 deletion src/app/main/ipc/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
}

Expand Down
29 changes: 29 additions & 0 deletions src/app/renderer/components/AgentProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,35 @@ function ProviderGlyph({ provider }: { provider: AgentProvider }): JSX.Element {
<path d="M49.04 24.001L47.958 23.958H47.957C36.134 23.492 26.508 13.866 26.042 2.043L25.999 0.96C25.978 0.424 25.537 0 25 0S24.022 0.424 24.001 0.96L23.958 2.043C23.492 13.866 13.866 23.492 2.042 23.958L0.96 24.001C0.424 24.022 0 24.463 0 25C0 25.537 0.424 25.978 0.961 25.999L2.043 26.041C13.866 26.508 23.492 36.134 23.958 47.957L24.001 49.04C24.022 49.576 24.463 50 25 50S25.978 49.576 25.999 49.04L26.042 47.957C26.508 36.134 36.134 26.508 47.957 26.041L49.039 25.999C49.576 25.978 50 25.537 50 25C50 24.463 49.576 24.022 49.04 24.001Z" />
</svg>
)
case 'hermes':
return (
<svg
className="agent-provider-icon__svg"
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2L2 7L12 12L22 7L12 2Z"
stroke="currentColor"
strokeWidth="1.7"
strokeLinejoin="round"
/>
<path
d="M2 17L12 22L22 17"
stroke="currentColor"
strokeWidth="1.7"
strokeLinejoin="round"
/>
<path
d="M2 12L12 17L22 12"
stroke="currentColor"
strokeWidth="1.7"
strokeLinejoin="round"
/>
</svg>
)
case 'codex':
default:
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion src/app/renderer/shell/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`
}

Expand Down
4 changes: 4 additions & 0 deletions src/app/renderer/styles/workspace-agent-item.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down
34 changes: 34 additions & 0 deletions src/contexts/agent/infrastructure/cli/AgentCommandFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export function resolveAgentCliCommand(provider: AgentProviderId): string {
return 'gemini'
}

if (provider === 'hermes') {
return 'hermes'
}

return 'codex'
}

Expand Down Expand Up @@ -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')
Expand Down
25 changes: 25 additions & 0 deletions src/contexts/agent/infrastructure/cli/AgentModelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const AGENT_PROVIDER_NPM_PACKAGES: Record<AgentProviderId, string> = {
codex: '@openai/codex',
opencode: 'opencode-ai',
gemini: '@google/gemini-cli',
hermes: 'hermes-agent',
}

interface CommandOutput {
Expand Down
8 changes: 8 additions & 0 deletions src/contexts/agent/infrastructure/cli/AgentSessionCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions src/contexts/agent/infrastructure/cli/AgentSessionLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,48 @@ async function findCodexResumeSessionId(cwd: string, startedAtMs: number): Promi
return sessionId ?? null
}

async function findHermesResumeSessionId(
_cwd: string,
startedAtMs: number,
): Promise<string | null> {
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,
Expand All @@ -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)
}

Expand Down
3 changes: 3 additions & 0 deletions src/contexts/settings/domain/agentSettings.defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down
1 change: 1 addition & 0 deletions src/contexts/settings/domain/agentSettings.executables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const DEFAULT_AGENT_EXECUTABLE_PATH_OVERRIDE_BY_PROVIDER: AgentExecutable
codex: '',
opencode: '',
gemini: '',
hermes: '',
}

export function normalizeAgentExecutablePathOverrideByProvider(
Expand Down
7 changes: 7 additions & 0 deletions src/contexts/settings/domain/agentSettings.providerMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const AGENT_PROVIDER_LABEL: Record<AgentProvider, string> = {
codex: 'Codex',
opencode: 'OpenCode',
gemini: 'Gemini CLI',
hermes: 'Hermes',
}

export interface AgentProviderCapabilities {
Expand Down Expand Up @@ -39,4 +40,10 @@ export const AGENT_PROVIDER_CAPABILITIES: Record<AgentProvider, AgentProviderCap
runtimeObservation: 'none',
experimental: false,
},
hermes: {
taskTitle: true,
worktreeNameSuggestion: true,
runtimeObservation: 'jsonl',
experimental: true,
},
}
6 changes: 3 additions & 3 deletions src/contexts/settings/domain/agentSettings.providers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const AGENT_PROVIDERS = ['claude-code', 'codex', 'opencode', 'gemini'] as const
export const TASK_TITLE_PROVIDERS = ['claude-code', 'codex'] as const
export const WORKTREE_NAME_SUGGESTION_PROVIDERS = ['claude-code', 'codex'] as const
export const AGENT_PROVIDERS = ['claude-code', 'codex', 'opencode', 'gemini', 'hermes'] as const
export const TASK_TITLE_PROVIDERS = ['claude-code', 'codex', 'hermes'] as const
export const WORKTREE_NAME_SUGGESTION_PROVIDERS = ['claude-code', 'codex', 'hermes'] as const
export const EXPERIMENTAL_AGENT_PROVIDERS = [] as const

export type AgentProvider = (typeof AGENT_PROVIDERS)[number]
Expand Down
Loading
Loading