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
2 changes: 2 additions & 0 deletions packages/happy-app/sources/app/(app)/new/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const agentIcons = {
rig: require('@/assets/images/logo-black.png'),
claude: require('@/assets/images/icon-claude.png'),
codex: require('@/assets/images/icon-gpt.png'),
cursor: require('@/assets/images/icon-cursor.png'),
openclaw: require('@/assets/images/icon-openclaw.png'),
gemini: require('@/assets/images/icon-gemini.png'),
agy: require('@/assets/images/icon-agy.png'),
Expand All @@ -116,6 +117,7 @@ type AgentKey = NewSessionAgentType;
const ALL_AGENTS: { key: AgentKey; label: string }[] = [
{ key: 'claude', label: 'claude code' },
{ key: 'codex', label: 'codex' },
{ key: 'cursor', label: 'cursor' },
{ key: 'agy', label: 'antigravity' },
{ key: 'rig', label: 'happy' },
];
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 @@ -44,6 +44,7 @@ type FieldConfig = {
const agentLabels: Record<AgentKey, string> = {
claude: getHarnessName('claude'),
codex: getHarnessName('codex'),
cursor: getHarnessName('cursor'),
gemini: getHarnessName('gemini'),
openclaw: getHarnessName('openclaw'),
agy: getHarnessName('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.
2 changes: 1 addition & 1 deletion packages/happy-app/sources/components/AgentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ interface AgentInputProps {
/** Plan quota windows from agent state, for the week stat and its popup. */
sessionStatusUsageLimits?: UsageLimitsLike | null;
onFileViewerPress?: () => void;
agentType?: 'claude' | 'codex' | 'gemini' | 'openclaw' | 'agy';
agentType?: 'claude' | 'codex' | 'gemini' | 'openclaw' | 'agy' | 'cursor';
onAgentClick?: () => void;
machineName?: string | null;
onMachineClick?: () => void;
Expand Down
5 changes: 4 additions & 1 deletion packages/happy-app/sources/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const harnessIcons: Record<AvatarHarnessIcon, number> = {
claude: require('@/assets/images/icon-claude.png'),
codex: require('@/assets/images/icon-gpt.png'),
agy: require('@/assets/images/icon-agy.png'),
cursor: require('@/assets/images/icon-cursor.png'),
rig: require('@/assets/images/logo-black.png'),
};

Expand All @@ -45,7 +46,9 @@ function harnessBadgeSizes(size: number, harness: AvatarHarnessIcon) {
? Math.round(size * 0.3)
: harness === 'claude'
? Math.round(size * 0.34)
: Math.round(size * 0.42);
: harness === 'cursor'
? Math.round(size * 0.32)
: Math.round(size * 0.42);
return { circleSize, iconSize };
}

Expand Down
13 changes: 13 additions & 0 deletions packages/happy-app/sources/components/modelModeOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
getCodexPermissionModes,
getClaudeModelModes,
getClaudePermissionModes,
getCursorModelModes,
getCursorPermissionModes,
getGeminiPermissionModes,
getDefaultEffortKey,
getDefaultModelKey,
Expand Down Expand Up @@ -251,6 +253,17 @@ describe('modelModeOptions', () => {
expect(keys).not.toContain('sonnet');
});

it('returns the curated cursor models and default model key', () => {
const models = getAvailableModels('cursor', null, translate);
expect(models).toEqual(getCursorModelModes());
const keys = models.map((m) => m.key);
expect(keys).toContain('default[]');
expect(getDefaultModelKey('cursor')).toBe('default[]');
expect(getDefaultPermissionModeKey('cursor')).toBe('agent');
expect(getCursorPermissionModes(translate).map((m) => m.key)).toContain('agent');
expect(getCursorPermissionModes(translate).map((m) => m.key)).toContain('plan');
});

it('resolves the first matching preferred key', () => {
const options = [
{ key: 'a', name: 'A' },
Expand Down
32 changes: 32 additions & 0 deletions packages/happy-app/sources/components/modelModeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,15 +287,44 @@ export function getHardcodedPermissionModes(flavor: AgentFlavor, translate: Tran
if (flavor === 'agy') {
return getAgyPermissionModes(translate);
}
if (flavor === 'cursor') {
return getCursorPermissionModes(translate);
}
return getClaudePermissionModes(translate);
}

export function getCursorPermissionModes(translate: Translate): PermissionMode[] {
return [
{ key: 'agent', name: 'Agent', description: translate('agentInput.permissionMode.default') },
{ key: 'plan', name: 'Plan', description: translate('agentInput.permissionMode.plan') },
{ key: 'ask', name: 'Ask', description: 'Q&A mode - no edits or command execution' },
{ key: 'bypassPermissions', name: 'Yolo', description: translate('agentInput.permissionMode.bypassPermissions') },
{ key: 'default', name: 'Default', description: translate('agentInput.permissionMode.default') },
];
}

export function getOpenClawModelModes(): ModelMode[] {
return [
{ key: 'default', name: 'Default model', description: null },
];
}

export function getCursorModelModes(): ModelMode[] {
return [
{ key: 'default[]', name: 'Auto', description: 'Cursor Router (Default)' },
{ key: 'grok-4.6[effort=high,fast=true]', name: 'Grok 4.6', description: 'xAI' },

@jdistler jdistler Sep 1, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Curated these to appear right away in model selector, but the true large list loads quickly as they are dynamically discovered (see video demo in PR description)

{ key: 'composer-2.5[fast=true]', name: 'Composer 2.5', description: 'Cursor' },
{ key: 'claude-opus-5[thinking=true,context=300k,effort=high,fast=false]', name: 'Claude Opus 5', description: 'Anthropic' },
{ key: 'claude-sonnet-5[thinking=true,context=300k,effort=high]', name: 'Claude Sonnet 5', description: 'Anthropic' },
{ key: 'gpt-5.6-sol[context=272k,reasoning=medium,fast=false]', name: 'GPT-5.6 Sol', description: 'OpenAI' },
{ key: 'gpt-5.5[context=272k,reasoning=medium,fast=false]', name: 'GPT-5.5', description: 'OpenAI' },
{ key: 'gemini-3.7-flash[effort=high]', name: 'Gemini 3.7 Flash', description: 'Google' },
{ key: 'gemini-3.1-pro[]', name: 'Gemini 3.1 Pro', description: 'Google' },
{ key: 'claude-fable-5[thinking=true,context=300k,effort=high]', name: 'Claude Fable 5', description: 'Anthropic' },
{ key: 'grok-4.5[effort=high,fast=true]', name: 'Grok 4.5', description: 'xAI' },
];
}

// Keys are the exact display names `agy --model` accepts (as printed by `agy models`).
export function getAgyModelModes(): ModelMode[] {
return [
Expand Down Expand Up @@ -326,6 +355,9 @@ export function getHardcodedModelModes(flavor: AgentFlavor, _translate: Translat
if (flavor === 'agy') {
return getAgyModelModes();
}
if (flavor === 'cursor') {
return getCursorModelModes();
}
return getClaudeModelModes();
}

Expand Down
2 changes: 2 additions & 0 deletions packages/happy-app/sources/sync/agentDefaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,7 @@ describe('agent defaults', () => {
expect(resolveAgentDefaultConfig({}, 'gemini', '1.0.0').permissionMode).toBe('default');
expect(resolveAgentDefaultConfig({}, 'openclaw', '1.0.0').permissionMode).toBe('default');
expect(resolveAgentDefaultConfig({}, 'agy', '1.0.0').permissionMode).toBe('default');
expect(resolveAgentDefaultConfig({}, 'cursor', '1.0.0').permissionMode).toBe('agent');
expect(resolveAgentDefaultConfig({}, 'cursor', '1.0.0').modelMode).toBe('default[]');
});
});
6 changes: 4 additions & 2 deletions packages/happy-app/sources/sync/agentDefaults.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as z from 'zod';
import { compareVersionsWithPrerelease, isWellFormedVersion } from '@/utils/versionUtils';

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

export const AgentDefaultOverrideSchema = z.object({
Expand All @@ -16,6 +16,7 @@ export const AgentDefaultOverridesSchema = z.object({
gemini: AgentDefaultOverrideSchema.optional(),
openclaw: AgentDefaultOverrideSchema.optional(),
agy: AgentDefaultOverrideSchema.optional(),
cursor: AgentDefaultOverrideSchema.optional(),
}).passthrough().default({});

export type AgentDefaultOverride = z.infer<typeof AgentDefaultOverrideSchema>;
Expand All @@ -37,6 +38,7 @@ const codeAgentDefaults: Record<AgentKey, AgentDefaultConfig> = {
gemini: { permissionMode: 'default', modelMode: 'gemini-2.5-pro', effortLevel: null },
openclaw: { permissionMode: 'default', modelMode: 'default', effortLevel: null },
agy: { permissionMode: 'default', modelMode: 'Gemini 3.1 Pro (High)', effortLevel: null },
cursor: { permissionMode: 'agent', modelMode: 'default[]', effortLevel: null },
};

// `auto` first shipped in happy-cli 1.2.1-beta.2, for Claude and Codex alike.
Expand All @@ -60,7 +62,7 @@ function resolveCodeDefaultPermissionMode(
}

export function normalizeAgentKey(flavor: string | null | undefined): AgentKey {
if (flavor === 'codex' || flavor === 'gemini' || flavor === 'openclaw' || flavor === 'agy') {
if (flavor === 'codex' || flavor === 'gemini' || flavor === 'openclaw' || flavor === 'agy' || flavor === 'cursor') {
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 @@ -172,7 +172,7 @@ export interface SpawnSessionOptions {
directory: string;
approvedNewDirectoryCreation?: boolean;
token?: string;
agent?: 'codex' | 'claude' | 'gemini' | 'openclaw' | 'agy' | 'rig';
agent?: 'codex' | 'claude' | 'gemini' | 'openclaw' | 'agy' | 'cursor' | 'rig';
permissionMode?: string;
modelMode?: string;
effortLevel?: string;
Expand Down Expand Up @@ -274,7 +274,7 @@ export async function machineSpawnNewSession(options: SpawnSessionOptions): Prom
directory: string
approvedNewDirectoryCreation?: boolean,
token?: string,
agent?: 'codex' | 'claude' | 'gemini' | 'openclaw' | 'agy' | 'rig',
agent?: 'codex' | 'claude' | 'gemini' | 'openclaw' | 'agy' | 'cursor' | 'rig',
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' | 'rig';
export type NewSessionAgentType = 'claude' | 'codex' | 'gemini' | 'openclaw' | 'agy' | 'cursor' | 'rig';
export type NewSessionSessionType = 'simple' | 'worktree';

export interface NewSessionDraft {
Expand Down Expand Up @@ -162,7 +162,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' || parsed.agentType === 'rig'
const agentType: NewSessionAgentType = parsed.agentType === 'codex' || parsed.agentType === 'gemini' || parsed.agentType === 'openclaw' || parsed.agentType === 'agy' || parsed.agentType === 'cursor' || parsed.agentType === 'rig'
? parsed.agentType
: 'claude';
const permissionMode: PermissionModeKey | null = typeof parsed.permissionMode === 'string'
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/sync/storageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ export const MachineMetadataSchema = z.object({
gemini: z.boolean(),
openclaw: z.boolean(),
agy: z.boolean().optional(), // optional: older CLIs don't report agy
cursor: z.boolean().optional(),
rig: z.boolean().optional(), // Rig runs its own Happy-connected daemon
detectedAt: z.number(),
}).optional(),
Expand Down
3 changes: 2 additions & 1 deletion packages/happy-app/sources/utils/avatarHarness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { describe, expect, it } from 'vitest';
import { resolveAvatarHarness } from './avatarHarness';

describe('resolveAvatarHarness', () => {
it('keeps the existing Claude, Codex, and Antigravity mappings', () => {
it('keeps the existing Claude, Codex, Cursor, and Antigravity mappings', () => {
expect(resolveAvatarHarness('claude')).toBe('claude');
expect(resolveAvatarHarness('codex')).toBe('codex');
expect(resolveAvatarHarness('cursor')).toBe('cursor');
expect(resolveAvatarHarness('agy')).toBe('agy');
});

Expand Down
3 changes: 2 additions & 1 deletion packages/happy-app/sources/utils/avatarHarness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
* metadata. Only the active harnesses have badges; retired and unknown
* flavors stay badge-free.
*/
export type AvatarHarnessIcon = 'claude' | 'codex' | 'agy' | 'rig';
export type AvatarHarnessIcon = 'claude' | 'codex' | 'agy' | 'cursor' | 'rig';

const ACTIVE_HARNESS_ICONS: ReadonlySet<string> = new Set([
'claude',
'codex',
'agy',
'cursor',
]);

export function resolveAvatarHarness(
Expand Down
15 changes: 9 additions & 6 deletions packages/happy-app/sources/utils/harnessCatalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,33 @@ import { describe, expect, it } from 'vitest';
import { HARNESS_NAMES, isRetiredHarness, listAvailableHarnesses } from './harnessCatalog';

describe('harness catalog', () => {
it('names Happy and Antigravity by product, not by CLI id', () => {
it('names Happy, Cursor, and Antigravity by product, not by CLI id', () => {
expect(HARNESS_NAMES.rig).toBe('Happy');
expect(HARNESS_NAMES.cursor).toBe('Cursor');
expect(HARNESS_NAMES.agy).toBe('Antigravity');
});

it('retires Gemini and OpenClaw only', () => {
expect(isRetiredHarness('gemini')).toBe(true);
expect(isRetiredHarness('openclaw')).toBe(true);
expect(isRetiredHarness('claude')).toBe(false);
expect(isRetiredHarness('cursor')).toBe(false);
// Antigravity is what Gemini's own error message redirects people to.
expect(isRetiredHarness('agy')).toBe(false);
});

it('lists only installed harnesses, in pick order', () => {
const harnesses = listAvailableHarnesses({
availability: { claude: true, codex: true, agy: true },
availability: { claude: true, codex: true, cursor: true, agy: true },
happyAgentAvailable: true,
selected: 'claude',
});

expect(harnesses.map((harness) => harness.key)).toEqual(['claude', 'codex', 'agy', 'rig']);
expect(harnesses.map((harness) => harness.key)).toEqual(['claude', 'codex', 'cursor', 'agy', 'rig']);
expect(harnesses.map((harness) => harness.name)).toEqual([
'Claude Code',
'Codex',
'Cursor',
'Antigravity',
'Happy',
]);
Expand Down Expand Up @@ -90,20 +93,20 @@ describe('harness catalog', () => {
availability: null,
happyAgentAvailable: false,
selected: 'agy',
}).map((harness) => harness.key)).toEqual(['claude', 'codex']);
}).map((harness) => harness.key)).toEqual(['claude', 'codex', 'cursor']);
});

it('falls back to the whole catalog when a machine reports no capabilities', () => {
expect(listAvailableHarnesses({
availability: null,
happyAgentAvailable: false,
selected: null,
}).map((harness) => harness.key)).toEqual(['claude', 'codex']);
}).map((harness) => harness.key)).toEqual(['claude', 'codex', 'cursor']);

expect(listAvailableHarnesses({
availability: {},
happyAgentAvailable: false,
selected: null,
}).map((harness) => harness.key)).toEqual(['claude', 'codex', 'rig']);
}).map((harness) => harness.key)).toEqual(['claude', 'codex', 'cursor', 'rig']);
});
});
2 changes: 2 additions & 0 deletions packages/happy-app/sources/utils/harnessCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { NewSessionAgentType } from '@/sync/persistence';
export const HARNESS_NAMES: Record<NewSessionAgentType, string> = {
claude: 'Claude Code',
codex: 'Codex',
cursor: 'Cursor',
rig: 'Happy',
agy: 'Antigravity',
gemini: 'Gemini',
Expand All @@ -32,6 +33,7 @@ export const RETIRED_HARNESSES: ReadonlySet<NewSessionAgentType> = new Set([
export const HARNESS_ORDER: readonly NewSessionAgentType[] = [
'claude',
'codex',
'cursor',
'agy',
'rig',
];
Expand Down
1 change: 1 addition & 0 deletions packages/happy-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This will:

```
happy codex
happy cursor # Cursor Agent CLI (via ACP)
happy agy # Antigravity CLI (Gemini's successor)
happy gemini # deprecated — use `happy agy`
happy openclaw
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,9 +2,10 @@ 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, Cursor, and OpenCode command mappings', () => {
expect(KNOWN_ACP_AGENTS).toEqual({
gemini: { command: 'gemini', args: ['--experimental-acp'] },
cursor: { command: 'agent', args: ['acp'] },
opencode: { command: 'opencode', args: ['acp'] },
});
});
Expand All @@ -17,6 +18,11 @@ describe('resolveAcpAgentConfig', () => {
command: 'gemini',
args: ['--experimental-acp'],
});
expect(resolveAcpAgentConfig(['cursor'])).toEqual({
agentName: 'cursor',
command: 'agent',
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 @@ -5,6 +5,7 @@ export type AcpAgentConfig = {

export const KNOWN_ACP_AGENTS: Record<string, AcpAgentConfig> = {
gemini: { command: 'gemini', args: ['--experimental-acp'] },
cursor: { command: 'agent', args: ['acp'] },
opencode: { command: 'opencode', args: ['acp'] },
};

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' | 'cursor' | 'acp' {
if (agentName === 'gemini') {
return 'gemini';
}
if (agentName === 'opencode') {
return 'opencode';
}
if (agentName === 'cursor') {
return 'cursor';
}
return 'acp';
}

Expand Down
1 change: 1 addition & 0 deletions packages/happy-cli/src/agent/adapters/MessageAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,5 +280,6 @@ export const adapters = {
gemini: new MessageAdapter({ agentType: 'gemini' }),
codex: new MessageAdapter({ agentType: 'codex' }),
claude: new MessageAdapter({ agentType: 'claude' }),
cursor: new MessageAdapter({ agentType: 'cursor' }),
opencode: new MessageAdapter({ agentType: 'opencode' }),
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/**
* Supported agent types for the mobile app
*/
export type MobileAgentType = 'gemini' | 'codex' | 'claude' | 'opencode';
export type MobileAgentType = 'gemini' | 'codex' | 'claude' | 'cursor' | 'opencode';

/**
* Message roles for the mobile app
Expand Down
Loading