feat(cursor): add support for Cursor Agent CLI integration (#236)

* feat(cursor): add support for Cursor Agent CLI integration

- Introduced new command `hapi cursor` to start Cursor Agent sessions.
- Added functionality for resuming sessions and managing permission modes.
- Updated documentation to include Cursor Agent usage and installation instructions.
- Enhanced existing codebase to accommodate Cursor as a recognized agent flavor.
- Implemented local and remote session handling for Cursor Agent.

This update expands HAPI's capabilities by integrating support for the Cursor Agent, allowing users to leverage its features alongside existing agents.

* Remove TODO.md file as it is no longer needed following the integration of Cursor Agent CLI support. This cleanup helps streamline project documentation and reflects the completion of the associated tasks.

* feat(cursor): implement remote mode and fix --hapi-starting-mode

- Consume --hapi-starting-mode in cursor command (do not forward to agent)
- Implement cursorRemoteLauncher: spawn agent -p with stream-json, --trust
- Add cursorEventConverter for NDJSON parsing (system/assistant/tool_call/result)
- Multi-turn via --resume session_id
- Update docs: cursor supports both local and remote modes

Made-with: Cursor

* fix: type error

* fix(cursor): address PR review - model UI, sessionId metadata, duplicate flags

- HappyComposer: use isClaudeFlavor for model mode (cursor has no model modes)
- cursorLocalLauncher: call onSessionFound for resume so cursorSessionId in metadata
- cursorCommand: do not forward parsed flags to cursorArgs (avoid duplicates)

Made-with: Cursor
This commit is contained in:
Mao Mr
2026-03-03 10:02:47 +08:00
committed by GitHub
parent 72a23fc761
commit c9be2894ac
35 changed files with 1097 additions and 30 deletions
+1 -1
View File
@@ -372,7 +372,7 @@ export class ApiClient {
async spawnSession(
machineId: string,
directory: string,
agent?: 'claude' | 'codex' | 'gemini' | 'opencode',
agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode',
model?: string,
yolo?: boolean,
sessionType?: 'simple' | 'worktree',
@@ -20,7 +20,7 @@ import { useActiveSuggestions } from '@/hooks/useActiveSuggestions'
import { applySuggestion } from '@/utils/applySuggestion'
import { usePlatform } from '@/hooks/usePlatform'
import { usePWAInstall } from '@/hooks/usePWAInstall'
import { isCodexFamilyFlavor } from '@/lib/agentFlavorUtils'
import { isClaudeFlavor } from '@/lib/agentFlavorUtils'
import { markSkillUsed } from '@/lib/recent-skills'
import { FloatingOverlay } from '@/components/ChatInput/FloatingOverlay'
import { Autocomplete } from '@/components/ChatInput/Autocomplete'
@@ -324,7 +324,7 @@ export function HappyComposer(props: {
useEffect(() => {
const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => {
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelModeChange && !isCodexFamilyFlavor(agentFlavor)) {
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelModeChange && isClaudeFlavor(agentFlavor)) {
e.preventDefault()
const currentIndex = MODEL_MODES.indexOf(modelMode as typeof MODEL_MODES[number])
const nextIndex = (currentIndex + 1) % MODEL_MODES.length
@@ -398,7 +398,7 @@ export function HappyComposer(props: {
}, [onModelModeChange, controlsDisabled, haptic])
const showPermissionSettings = Boolean(onPermissionModeChange && permissionModeOptions.length > 0)
const showModelSettings = Boolean(onModelModeChange && !isCodexFamilyFlavor(agentFlavor))
const showModelSettings = Boolean(onModelModeChange && isClaudeFlavor(agentFlavor))
const showSettingsButton = Boolean(showPermissionSettings || showModelSettings)
const showAbortButton = true
const voiceEnabled = Boolean(onVoiceToggle)
@@ -14,7 +14,7 @@ export function AgentSelector(props: {
{t('newSession.agent')}
</label>
<div className="flex gap-3">
{(['claude', 'codex', 'gemini', 'opencode'] as const).map((agentType) => (
{(['claude', 'codex', 'cursor', 'gemini', 'opencode'] as const).map((agentType) => (
<label
key={agentType}
className="flex items-center gap-1.5 cursor-pointer"
+1 -1
View File
@@ -3,7 +3,7 @@ import type { AgentType } from './types'
const AGENT_STORAGE_KEY = 'hapi:newSession:agent'
const YOLO_STORAGE_KEY = 'hapi:newSession:yolo'
const VALID_AGENTS: AgentType[] = ['claude', 'codex', 'gemini', 'opencode']
const VALID_AGENTS: AgentType[] = ['claude', 'codex', 'cursor', 'gemini', 'opencode']
export function loadPreferredAgent(): AgentType {
try {
+2 -1
View File
@@ -1,4 +1,4 @@
export type AgentType = 'claude' | 'codex' | 'gemini' | 'opencode'
export type AgentType = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'
export type SessionType = 'simple' | 'worktree'
export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]> = {
@@ -14,6 +14,7 @@ export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
],
cursor: [],
gemini: [
{ value: 'auto', label: 'Auto' },
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
+1 -1
View File
@@ -6,7 +6,7 @@ import { queryKeys } from '@/lib/query-keys'
type SpawnInput = {
machineId: string
directory: string
agent?: 'claude' | 'codex' | 'gemini' | 'opencode'
agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'
model?: string
yolo?: boolean
sessionType?: 'simple' | 'worktree'
+5 -1
View File
@@ -6,6 +6,10 @@ export function isClaudeFlavor(flavor?: string | null): boolean {
return flavor === 'claude'
}
export function isCursorFlavor(flavor?: string | null): boolean {
return flavor === 'cursor'
}
export function isKnownFlavor(flavor?: string | null): boolean {
return isClaudeFlavor(flavor) || isCodexFamilyFlavor(flavor)
return isClaudeFlavor(flavor) || isCodexFamilyFlavor(flavor) || isCursorFlavor(flavor)
}