-
Notifications
You must be signed in to change notification settings - Fork 6
feat(agent): add Cursor CLI as a supported agent #213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ae1d310
505cdf6
3312949
435ee66
fb570cf
d3111e1
63153b9
29f472b
e8653e3
9c8eebf
5e76a80
bedefb8
6f8d0fe
7e3e2a6
16b02ba
98e10de
c5edf50
81229d6
da96ae7
1a0a612
bbe60ea
2b3d694
835f0cf
56ffad8
e5e2423
4e5aeb3
fe2d5d4
e9216b4
1423ddd
30a8f13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| type: added | ||
| --- | ||
|
|
||
| **Cursor joins the agent lineup.** You can now run sessions with Cursor CLI (`cursor-agent`) alongside Claude, Codex, and OpenCode — pick it from the `A` picker or set it as your `default_agent`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,13 +21,16 @@ type hookPayload struct { | |
| Prompt string `json:"prompt,omitempty"` | ||
| // Reason is set on SessionEnd: "clear", "logout", "prompt_input_exit", "other". | ||
| Reason string `json:"reason,omitempty"` | ||
| // Status is set on Cursor's stop hook: "completed", "aborted", or "error". | ||
| Status string `json:"status,omitempty"` | ||
| } | ||
|
|
||
| // mapEventToStatus maps a hook event to a fleet status string. Claude and Codex | ||
| // send Claude-style event names; the OpenCode status plugin sends OpenCode-native | ||
| // names (session.busy/session.idle/permission.asked) — these are additive, the | ||
| // other agents never emit them, so the handler stays agent-neutral. | ||
| func mapEventToStatus(event string) string { | ||
| // names (session.busy/session.idle/permission.asked); Cursor CLI's hooks.json | ||
| // sends its own lowerCamelCase event names — these are all additive, no agent | ||
| // emits another's names, so the handler stays agent-neutral. | ||
| func mapEventToStatus(event, status string) string { | ||
| switch event { | ||
| case "UserPromptSubmit": | ||
| return "running" | ||
|
|
@@ -62,11 +65,58 @@ func mapEventToStatus(event string) string { | |
| // the next session.idle. Without this, waiting can stick if OpenCode | ||
| // doesn't re-emit session.status{busy} after an in-flight approval. | ||
| return "running" | ||
| // Cursor CLI events (from hooks.json, see internal/hooks/cursor_hooks.go). | ||
| // Both shell hooks map to running, never waiting: afterShellExecution's | ||
| // payload carries `output` and `duration`, so it fires when the command | ||
| // *completes*, not when an approval clears. Mapping beforeShellExecution to | ||
| // waiting would therefore hold the row at ◐ for the entire runtime of every | ||
| // command the agent runs — and Cursor is on the pure-hook path with no pane | ||
| // fallback to correct it, so `Space` would rotate to a busy session and `Y` | ||
| // would inject a stray "y"+Enter into a pane showing no prompt. Cursor has | ||
| // no dedicated approval hook, so fleet simply has no waiting signal for it. | ||
| // | ||
| // No sessionStart case: unlike Claude, Cursor's initial status (see | ||
| // initialRunStatus in session.go) starts idle, not running — so there's | ||
| // nothing for sessionStart to correct, and mapping it to "finished" (as | ||
| // Claude's SessionStart does) would immediately flip a freshly launched, | ||
| // untouched session to finished before any turn ran. It falls through to | ||
| // the default unmapped case below; fleet subscribes to no such hook (see | ||
| // cursorHookEvents in internal/hooks/cursor_hooks.go). | ||
| case "beforeSubmitPrompt": | ||
| return "running" | ||
| case "beforeShellExecution", "afterShellExecution": | ||
| return "running" | ||
| case "stop": | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cursor's So a turn that died on a model error or was aborted renders with the ● "done, come look" dot, and OpenCode's adapter already distinguishes this ( |
||
| // Cursor's stop payload reports how the turn ended. Only "completed" is | ||
| // finished work; "error" surfaces as an error so a failed turn doesn't | ||
| // render with the same "done, come look" dot as a successful one. | ||
| // "aborted" is a user-initiated cancel — the turn is over, not broken. | ||
| if status == "error" { | ||
| return "error" | ||
| } | ||
| return "finished" | ||
| case "sessionEnd": | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
But Cursor fires Result: a healthy session shows a red ✕ error until the next prompt, writes a spurious crash dump, and gets swept into Reload All Sessions — which restarts dead/error sessions, killing a live agent. OpenCode is the only other agent on this branch and never emits |
||
| // Not "dead". Observed payload (cursor-agent 2026.07.23-e383d2b): | ||
| // {reason: "completed", final_status, duration_ms, ...} — this fires when | ||
| // a *conversation* ends, with the process alive and back at its prompt. | ||
| // Routing it to "dead" sets StatusError and writes a crash dump for a | ||
| // healthy session, then Reload All Sessions restarts a live agent. | ||
| // Cursor's real process death comes from tmux pane-death, same as Codex | ||
| // and OpenCode, neither of which maps an end-of-session hook to dead. | ||
| return "finished" | ||
| default: | ||
| return "" | ||
| } | ||
| } | ||
|
|
||
| // isPromptSubmit reports whether event is a user-prompt-submission hook — | ||
| // Claude/Codex's UserPromptSubmit, or Cursor's beforeSubmitPrompt equivalent | ||
| // (see internal/hooks/cursor_hooks.go) — used to gate prompt-text capture and | ||
| // prompt-count increments in handleHookHandler. | ||
| func isPromptSubmit(event string) bool { | ||
| return event == "UserPromptSubmit" || event == "beforeSubmitPrompt" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So for a Cursor session, To be fair this gap is pre-existing — OpenCode's |
||
| } | ||
|
|
||
| // isCompactSessionStart reports the SessionStart that Claude Code fires when a | ||
| // compaction completes. Its status must NOT be forced to "finished": on | ||
| // auto-compaction the turn is still running, so finishing here would flash a | ||
|
|
@@ -104,7 +154,14 @@ func handleHookHandler() { | |
|
|
||
| instanceID := os.Getenv("FLEET_INSTANCE_ID") | ||
| if instanceID == "" { | ||
| log.Warn("hook-handler: no FLEET_INSTANCE_ID env var", | ||
| // Debug, not Warn: fleet installs Cursor's hooks into the user-global | ||
| // ~/.cursor/hooks.json, which the Cursor IDE reads too — so this fires | ||
| // for every prompt and shell command in the editor, none of which is | ||
| // fleet's business. At Warn it always writes, and since debug.log is | ||
| // size-truncated and the `!` bug-report flow pastes its last 100 lines | ||
| // into a public issue, an editing session would evict the diagnostics | ||
| // the report exists to carry. | ||
| log.Debug("hook-handler: no FLEET_INSTANCE_ID env var (not a fleet session)", | ||
| "event", payload.HookEventName, | ||
| "claudeSession", payload.SessionID, | ||
| "source", payload.Source, | ||
|
|
@@ -120,7 +177,7 @@ func handleHookHandler() { | |
| return | ||
| } | ||
|
|
||
| status := mapEventToStatus(payload.HookEventName) | ||
| status := mapEventToStatus(payload.HookEventName, payload.Status) | ||
|
|
||
| // Special handling for Notification events. | ||
| if payload.HookEventName == "Notification" && payload.Matcher != nil { | ||
|
|
@@ -148,9 +205,10 @@ func handleHookHandler() { | |
| ) | ||
|
|
||
| // Extract user prompt and prompt count. | ||
| promptSubmit := isPromptSubmit(payload.HookEventName) | ||
| var userPrompt string | ||
| var promptCount int | ||
| if payload.HookEventName == "UserPromptSubmit" && payload.Prompt != "" { | ||
| if promptSubmit && payload.Prompt != "" { | ||
| userPrompt = payload.Prompt | ||
| } | ||
|
|
||
|
|
@@ -165,7 +223,7 @@ func handleHookHandler() { | |
| } | ||
|
|
||
| // Increment prompt count on new user prompt submissions. | ||
| if payload.HookEventName == "UserPromptSubmit" { | ||
| if promptSubmit { | ||
| promptCount++ | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the Cursor status and naming claims.
This paragraph says that all four agents have identical status and auto-naming behavior and that pane heuristics fill missing hook states. Cursor uses a hook-only status path with no pane fallback. Cursor naming uses Fleet's prompt heuristic because Cursor has no local chat-title retrieval. Update the text to describe these Cursor-specific differences.
🤖 Prompt for AI Agents