Skip to content
Closed
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 @@ -41,14 +41,16 @@ const agentIcons = {
codex: require('@/assets/images/icon-gpt.png'),
openclaw: require('@/assets/images/icon-openclaw.png'),
gemini: require('@/assets/images/icon-gemini.png'),
hermes: require('@/assets/images/icon-hermes.png'),
};

type AgentKey = 'claude' | 'codex' | 'openclaw' | 'gemini';
type AgentKey = 'claude' | 'codex' | 'openclaw' | 'gemini' | 'hermes';
const AGENTS: { key: AgentKey; label: string }[] = [
{ key: 'claude', label: 'claude code' },
{ key: 'codex', label: 'codex' },
{ key: 'openclaw', label: 'openclaw' },
{ key: 'gemini', label: 'gemini' },
{ key: 'hermes', label: 'hermes' },
];

// Sample data for pickers
Expand Down
4 changes: 3 additions & 1 deletion packages/happy-app/sources/app/(app)/new/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const agentIcons = {
openclaw: require('@/assets/images/icon-openclaw.png'),
gemini: require('@/assets/images/icon-gemini.png'),
agy: require('@/assets/images/icon-agy.png'),
hermes: require('@/assets/images/icon-hermes.png'),
};

type AgentKey = NewSessionAgentType;
Expand All @@ -85,6 +86,7 @@ const ALL_AGENTS: { key: AgentKey; label: string }[] = [
{ key: 'codex', label: 'codex' },
{ key: 'openclaw', label: 'openclaw' },
{ key: 'agy', label: 'agy' },
{ key: 'hermes', label: 'hermes' },
];

type PickerItem = { key: string; label: string; subtitle?: string; dimmed?: boolean };
Expand Down Expand Up @@ -899,7 +901,7 @@ function NewSessionScreen() {
const availableAgents = React.useMemo(() => {
const availability = selectedMachine?.metadata?.cliAvailability;
if (!availability) return ALL_AGENTS;
return ALL_AGENTS.filter(a => availability[a.key]);
return ALL_AGENTS.filter(a => availability[a.key] === true);
}, [selectedMachine]);

// If current agent not available on this machine, switch to first available
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/app/(app)/session/[id]/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ function SessionInfoContent({ session }: { session: Session }) {
if (flavor === 'claude') return 'Claude';
if (flavor === 'gpt' || flavor === 'openai') return 'Codex';
if (flavor === 'gemini') return 'Gemini';
if (flavor === 'hermes') return 'Hermes';
if (flavor === 'openclaw') return 'OpenClaw';
return flavor;
})()}
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/app/(app)/settings/agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const agentLabels: Record<AgentKey, string> = {
claude: 'Claude Code',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
agy: 'Agy',
};
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions packages/happy-app/sources/components/AgentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ interface AgentInputProps {
sessionStatusModelLabel?: string | null;
sessionStatusEffortLabel?: string | null;
onFileViewerPress?: () => void;
agentType?: 'claude' | 'codex' | 'gemini' | 'openclaw' | 'agy';
agentType?: 'claude' | 'codex' | 'gemini' | 'hermes' | 'openclaw' | 'agy';
onAgentClick?: () => void;
machineName?: string | null;
onMachineClick?: () => void;
Expand Down Expand Up @@ -705,11 +705,12 @@ export const AgentInput = React.memo(React.forwardRef<MultiTextInputHandle, Agen
const hasImages = (props.selectedImages?.length ?? 0) > 0;
const hasComposerContent = hasText || hasImages;

// Check if this is a Codex, Gemini, or OpenClaw session
// Check if this is a Codex, Gemini, Hermes, or OpenClaw session
// Use metadata.flavor for existing sessions, agentType prop for new sessions
const isRig = isRigMetadata(props.metadata);
const isCodex = !isRig && (props.metadata?.flavor === 'codex' || props.agentType === 'codex');
const isGemini = props.metadata?.flavor === 'gemini' || props.agentType === 'gemini';
const isHermes = props.metadata?.flavor === 'hermes' || props.agentType === 'hermes';
const isOpenClaw = props.metadata?.flavor === 'openclaw' || props.agentType === 'openclaw';
const displayPermissionMode = React.useMemo(() => (
props.permissionMode ? hackMode(props.permissionMode) : null
Expand Down
27 changes: 25 additions & 2 deletions packages/happy-app/sources/components/modelModeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type EffortLevel = ModeOption;
export type PermissionModeKey = string;
export type ModelModeKey = string;

export type AgentFlavor = 'claude' | 'codex' | 'gemini' | string | null | undefined;
export type AgentFlavor = 'claude' | 'codex' | 'gemini' | 'hermes' | string | null | undefined;

type Translate = (key: any) => string;

Expand Down Expand Up @@ -143,13 +143,33 @@ export function getAgyPermissionModes(translate: Translate): PermissionMode[] {
];
}

export function getHermesPermissionModes(translate: Translate): PermissionMode[] {
// Hermes ACP currently exposes only a single "default" surface (`hooks_auto_accept`
// toggle is handled CLI-side). Keep the picker minimal until the agent advertises
// operating modes via metadata.operatingModes.
return [
{ key: 'default', name: translate('agentInput.permissionMode.default'), description: null },
];
}

export function getHermesModelModes(): ModelMode[] {
// Hermes broadcasts its model list at runtime via the ACP available_models event,
// populating metadata.models. The hardcoded fallback only needs a sentinel.
return [
{ key: 'default', name: 'default model', description: null },
];
}

export function getHardcodedPermissionModes(flavor: AgentFlavor, translate: Translate): PermissionMode[] {
if (flavor === 'codex') {
return getCodexPermissionModes(translate);
}
if (flavor === 'gemini') {
return getGeminiPermissionModes(translate);
}
if (flavor === 'hermes') {
return getHermesPermissionModes(translate);
}
if (flavor === 'openclaw') {
return getOpenClawPermissionModes(translate);
}
Expand Down Expand Up @@ -186,6 +206,9 @@ export function getHardcodedModelModes(flavor: AgentFlavor, _translate: Translat
if (flavor === 'gemini') {
return getGeminiModelModes();
}
if (flavor === 'hermes') {
return getHermesModelModes();
}
if (flavor === 'openclaw') {
return getOpenClawModelModes();
}
Expand Down Expand Up @@ -288,7 +311,7 @@ export function getAvailablePermissionModes(
}
return modes;
}
if (flavor === 'claude' || flavor === 'codex' || flavor === 'openclaw' || flavor === 'agy') {
if (flavor === 'claude' || flavor === 'codex' || flavor === 'hermes' || flavor === 'openclaw' || flavor === 'agy') {
return hackModes(getHardcodedPermissionModes(flavor, translate));
}

Expand Down
8 changes: 6 additions & 2 deletions packages/happy-app/sources/sync/agentDefaults.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as z from 'zod';

export const agentKeys = ['claude', 'codex', 'gemini', 'openclaw', 'agy'] as const;
export const agentKeys = ['claude', 'codex', 'gemini', 'hermes', 'openclaw', 'agy'] as const;
export type AgentKey = typeof agentKeys[number];

export const AgentDefaultOverrideSchema = z.object({
Expand All @@ -13,6 +13,7 @@ export const AgentDefaultOverridesSchema = z.object({
claude: AgentDefaultOverrideSchema.optional(),
codex: AgentDefaultOverrideSchema.optional(),
gemini: AgentDefaultOverrideSchema.optional(),
hermes: AgentDefaultOverrideSchema.optional(),
openclaw: AgentDefaultOverrideSchema.optional(),
agy: AgentDefaultOverrideSchema.optional(),
}).passthrough().default({});
Expand All @@ -33,12 +34,15 @@ const codeAgentDefaults: Record<AgentKey, AgentDefaultConfig> = {
claude: { permissionMode: 'bypassPermissions', modelMode: 'opus', effortLevel: 'medium' },
codex: { permissionMode: 'yolo', modelMode: 'gpt-5.5', effortLevel: 'medium' },
gemini: { permissionMode: 'default', modelMode: 'gemini-2.5-pro', effortLevel: null },
// Hermes advertises models dynamically via ACP available_models — keep
// the static default empty so the runtime list wins.
hermes: { permissionMode: 'default', modelMode: 'default', effortLevel: null },
openclaw: { permissionMode: 'default', modelMode: 'default', effortLevel: null },
agy: { permissionMode: 'default', modelMode: 'Gemini 3.1 Pro (High)', effortLevel: null },
};

export function normalizeAgentKey(flavor: string | null | undefined): AgentKey {
if (flavor === 'codex' || flavor === 'gemini' || flavor === 'openclaw' || flavor === 'agy') {
if (flavor === 'codex' || flavor === 'gemini' || flavor === 'hermes' || flavor === 'openclaw' || flavor === 'agy') {
return flavor;
}
return 'claude';
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-app/sources/sync/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export interface SpawnSessionOptions {
directory: string;
approvedNewDirectoryCreation?: boolean;
token?: string;
agent?: 'codex' | 'claude' | 'gemini' | 'openclaw' | 'agy';
agent?: 'codex' | 'claude' | 'gemini' | 'hermes' | 'openclaw' | 'agy';
permissionMode?: string;
modelMode?: string;
effortLevel?: string;
Expand Down Expand Up @@ -245,7 +245,7 @@ export async function machineSpawnNewSession(options: SpawnSessionOptions): Prom
directory: string
approvedNewDirectoryCreation?: boolean,
token?: string,
agent?: 'codex' | 'claude' | 'gemini' | 'openclaw' | 'agy',
agent?: 'codex' | 'claude' | 'gemini' | 'hermes' | 'openclaw' | 'agy',
permissionMode?: string,
modelMode?: string,
effortLevel?: string,
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-app/sources/sync/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const VOICE_SOFT_PAYWALL_SHOWN_KEY = 'voice-soft-paywall-shown';
const VOICE_ONBOARDING_PROMPT_LOAD_COUNT_KEY = 'voice-onboarding-prompt-load-count';
const VOICE_MESSAGE_COUNT_KEY = 'voice-message-count';

export type NewSessionAgentType = 'claude' | 'codex' | 'gemini' | 'openclaw' | 'agy';
export type NewSessionAgentType = 'claude' | 'codex' | 'gemini' | 'hermes' | 'openclaw' | 'agy';
export type NewSessionSessionType = 'simple' | 'worktree';

export interface NewSessionDraft {
Expand Down Expand Up @@ -146,7 +146,7 @@ export function loadNewSessionDraft(): NewSessionDraft | null {
const input = typeof parsed.input === 'string' ? parsed.input : '';
const selectedMachineId = typeof parsed.selectedMachineId === 'string' ? parsed.selectedMachineId : null;
const selectedPath = typeof parsed.selectedPath === 'string' ? parsed.selectedPath : null;
const agentType: NewSessionAgentType = parsed.agentType === 'codex' || parsed.agentType === 'gemini' || parsed.agentType === 'openclaw' || parsed.agentType === 'agy'
const agentType: NewSessionAgentType = parsed.agentType === 'codex' || parsed.agentType === 'gemini' || parsed.agentType === 'hermes' || parsed.agentType === 'openclaw' || parsed.agentType === 'agy'
? parsed.agentType
: 'claude';
const permissionMode: PermissionModeKey | null = typeof parsed.permissionMode === 'string'
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/sync/storageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ export const MachineMetadataSchema = z.object({
claude: z.boolean(),
codex: z.boolean(),
gemini: z.boolean(),
// Optional for backward compatibility with CLIs predating Hermes support.
hermes: z.boolean().optional(),
openclaw: z.boolean(),
agy: z.boolean().optional(), // optional: older CLIs don't report agy
detectedAt: z.number(),
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-app/sources/sync/typesRaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ const rawAgentRecordSchema = z.discriminatedUnion('type', [z.object({
}), z.object({
// ACP (Agent Communication Protocol) - unified format for all agent providers
type: z.literal('acp'),
provider: z.enum(['gemini', 'codex', 'claude', 'opencode']),
provider: z.enum(['gemini', 'codex', 'claude', 'opencode', 'hermes']),
data: z.discriminatedUnion('type', [
// Core message types
z.object({ type: z.literal('reasoning'), message: z.string() }),
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/_default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ export const en = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ export const ca: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ export const en: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ export const es: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ export const it: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ export const ja: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ export const pl: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ export const pt: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ export const ru: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/zh-Hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ export const zhHans: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/text/translations/zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ export const zhHant: TranslationStructure = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
hermes: 'Hermes',
openclaw: 'OpenClaw',
},
model: {
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ This will:
happy codex
happy agy # Antigravity CLI (Gemini's successor)
happy gemini # deprecated — use `happy agy`
happy hermes
happy openclaw

# or any ACP-compatible CLI
Expand Down Expand Up @@ -102,6 +103,7 @@ happy connect status
| `happy codex` | Start Codex mode |
| `happy agy` | Start agy (Antigravity CLI) session |
| `happy gemini` | Start Gemini CLI session (**deprecated** — use `happy agy`) |
| `happy hermes` | Start Hermes Agent CLI session |
| `happy openclaw` | Start OpenClaw session |
| `happy acp` | Start any ACP-compatible agent |
| `happy resume <id>` | Resume a previous session |
Expand Down
8 changes: 7 additions & 1 deletion packages/happy-cli/src/agent/acp/acpAgentConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { describe, expect, it } from 'vitest';
import { KNOWN_ACP_AGENTS, resolveAcpAgentConfig } from './acpAgentConfig';

describe('KNOWN_ACP_AGENTS', () => {
it('defines built-in Gemini and OpenCode command mappings', () => {
it('defines built-in Gemini, OpenCode, and Hermes command mappings', () => {
expect(KNOWN_ACP_AGENTS).toEqual({
gemini: { command: 'gemini', args: ['--experimental-acp'] },
opencode: { command: 'opencode', args: ['acp'] },
hermes: { command: 'hermes', args: ['acp'] },
});
});
});
Expand All @@ -17,6 +18,11 @@ describe('resolveAcpAgentConfig', () => {
command: 'gemini',
args: ['--experimental-acp'],
});
expect(resolveAcpAgentConfig(['hermes'])).toEqual({
agentName: 'hermes',
command: 'hermes',
args: ['acp'],
});
});

it('appends extra CLI args for known agent aliases', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-cli/src/agent/acp/acpAgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type AcpAgentConfig = {
export const KNOWN_ACP_AGENTS: Record<string, AcpAgentConfig> = {
gemini: { command: 'gemini', args: ['--experimental-acp'] },
opencode: { command: 'opencode', args: ['acp'] },
hermes: { command: 'hermes', args: ['acp'] },
};

export type ResolvedAcpAgentConfig = {
Expand Down
5 changes: 4 additions & 1 deletion packages/happy-cli/src/agent/acp/runAcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,16 @@ type PendingTurn = {
timeout: NodeJS.Timeout;
};

function resolveSessionFlavor(agentName: string): 'gemini' | 'opencode' | 'acp' {
function resolveSessionFlavor(agentName: string): 'gemini' | 'opencode' | 'hermes' | 'acp' {
if (agentName === 'gemini') {
return 'gemini';
}
if (agentName === 'opencode') {
return 'opencode';
}
if (agentName === 'hermes') {
return 'hermes';
}
return 'acp';
}

Expand Down
2 changes: 1 addition & 1 deletion packages/happy-cli/src/agent/core/AgentBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface McpServerConfig {
export type AgentTransport = 'native-claude' | 'mcp-codex' | 'acp';

/** Agent identifier */
export type AgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'agy' | 'claude-acp' | 'codex-acp';
export type AgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'agy' | 'claude-acp' | 'codex-acp' | 'hermes';

/**
* Configuration for creating an agent backend
Expand Down
Loading
Loading