feat(agent): add Cursor CLI as a supported agent - #213
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Note
|
| Layer / File(s) | Summary |
|---|---|
Agent model and UI exposure internal/agent/*, internal/config/config.go, internal/ui/* |
Cursor is added to agent parsing, launch commands, default-agent settings, session creation, sidebar glyphs, and related tests. |
Cursor hooks and status events internal/hooks/*, cmd/fleet/hook_handler.go, internal/hooks/hook_watcher.go |
Fleet installs and updates Cursor hooks idempotently, preserves valid user hooks, rejects malformed entries, and maps Cursor events to statuses and prompt counts. |
Cursor session lifecycle internal/session/* |
Cursor sessions use idle and hook-only status behavior and do not rely on Claude transcript or stored title data. |
UI wiring, diagnostics, and documentation internal/ui/app.go, internal/diagnostics/diagnostics.go, README.md, CLAUDE.md, changelog/unreleased/cursor-agent.md |
The UI installs Cursor hooks and restricts unsupported forks. Diagnostics report the Cursor CLI version. Documentation describes Cursor support and fork limitations. |
Estimated code review effort: 3 (Moderate) | ~25 minutes
Sequence Diagram(s)
sequenceDiagram
participant FleetUI as Fleet UI
participant CursorCLI as Cursor CLI
participant HookHandler as Hook handler
participant Session as Fleet session
FleetUI->>CursorCLI: launch cursor-agent or resume session
FleetUI->>CursorCLI: install or update hooks.json
CursorCLI->>HookHandler: emit Cursor lifecycle event
HookHandler->>Session: map event and update status
HookHandler->>Session: increment prompt count on beforeSubmitPrompt
Possibly related PRs
- brizzai/fleet#78: Introduces the shared agent and hook integration patterns extended here for Cursor.
- brizzai/fleet#84: Relates to shared launch and fork handling, including Cursor’s unsupported fork behavior.
- brizzai/fleet#134: Relates to hook-driven agent status handling extended here for Cursor.
Suggested reviewers: copilot, hayke102
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 52.94% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title clearly and concisely identifies adding Cursor CLI as a supported agent, which is the primary change in the pull request. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
✨ Finishing Touches
🧪 Generate unit tests (beta)
- Create PR with unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
# Conflicts: # CLAUDE.md # internal/ui/app.go
There was a problem hiding this comment.
Pull request overview
Adds Cursor CLI (cursor-agent) as a first-class session agent in fleet, integrating it into the agent model, UI selection surfaces, hook-based status pipeline, and diagnostics.
Changes:
- Introduces
agent.Cursorwith launch/resume command support and UI glyph/display updates. - Adds a dedicated Cursor hooks injector (
~/.cursor/hooks.json) and extends the hook handler to map Cursor hook events into fleet statuses. - Wires Cursor into settings/config defaults, session creation agent picker, naming fallback behavior, and diagnostics reporting.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/ui/sidebar.go | Adds Cursor agent glyph (✦) and maps agent.Cursor to the new glyph. |
| internal/ui/sidebar_test.go | Extends glyph rendering test coverage to include Cursor. |
| internal/ui/settings.go | Adds cursor to the default-agent settings cycle and display label/width logic. |
| internal/ui/session_create.go | Includes Cursor in the agent picker cycle order. |
| internal/ui/app.go | Injects Cursor hooks on startup when cursor-agent is present on PATH. |
| internal/session/session.go | Treats Cursor as a hook-only agent path for status updates; adjusts initial status behavior/commentary. |
| internal/session/agent_name.go | Adds Cursor to agent-title lookup (explicitly falls back to prompt heuristic). |
| internal/hooks/cursor_hooks.go | New Cursor hooks injector/merger for Cursor’s flat hooks.json schema. |
| internal/hooks/cursor_hooks_test.go | Adds tests for Cursor hook injection and preserving user hooks. |
| internal/diagnostics/diagnostics.go | Captures and reports cursor-agent --version in diagnostics output. |
| internal/config/config.go | Documents and normalizes default_agent: "cursor" in config parsing. |
| internal/agent/agent.go | Adds Cursor agent type, binary name, display name, and resume launch command. |
| internal/agent/agent_test.go | Extends agent parsing/launch command tests to include Cursor. |
| cmd/fleet/hook_handler.go | Adds Cursor event→status mapping and treats beforeSubmitPrompt as a prompt submission for prompt counting. |
| CLAUDE.md | Updates internal documentation to mention Cursor as a supported agent. |
| changelog/unreleased/cursor-agent.md | Adds a changelog fragment announcing Cursor CLI support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
internal/hooks/cursor_hooks.go:134
- mergeCursorHookEvent drops all existing entries for an event when the existing JSON can’t be unmarshaled into []cursorHookEntry (entries=nil). That contradicts InjectCursorHooks’ “preserving any existing user hooks” contract and can silently clobber a user’s hooks for that event.
func mergeCursorHookEvent(existing json.RawMessage) json.RawMessage {
var entries []cursorHookEntry
if existing != nil {
if err := json.Unmarshal(existing, &entries); err != nil {
entries = nil
}
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
internal/hooks/cursor_hooks_test.go:50
- The note about idempotency being untestable under
go testis outdated:GetHookCommand()includes the binary-name-independent--fleet-hookmarker, andisFleetHookmatches it. It would be useful to assert that a second InjectCursorHooks() call returns changed=false to lock in idempotency.
// Note: idempotency on re-install relies on the fleet-hook marker
// ("fleet hook-handler") matching the launch command, which requires the
// binary to be named "fleet" — true in production but not under `go test`
// (binary is *.test), so we don't assert no-op re-install here.
|
🚨 gitStream Monthly Automation Limit Reached 🚨 Your organization has exceeded the number of pull requests allowed for automation with gitStream. To continue automating your PR workflows and unlock additional features, please contact LinearB. |
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # CLAUDE.md
| if err != nil { | ||
| return false, fmt.Errorf("marshal hooks: %w", err) | ||
| } | ||
| root["hooks"] = eventsRaw |
There was a problem hiding this comment.
Nil-map panic that kills fleet at startup. If ~/.cursor/hooks.json holds a top-level null, json.Unmarshal at line 68 leaves root nil and returns no error — so the guard passes and this line panics with assignment to entry in nil map.
Reproduced:
os.WriteFile(dir+"/hooks.json", []byte("null"), 0644)
InjectCursorHooks(dir)
// panic: assignment to entry in nil map ... cursor_hooks.go:104InjectCursorHooks runs from loadSessions' tea.Cmd goroutine, so Bubble Tea's panic handler restores the terminal and exits — fleet dies on launch with a stack trace and the user can't reach the TUI to fix the file.
Lines 84-89 already handle exactly this hazard one level down ("hooks": null), with a test. The else at line 67 needs the same treatment:
} else {
if err := json.Unmarshal(orig, &root); err != nil {
...
}
if root == nil {
root = make(map[string]json.RawMessage)
}
}| // cursorHookEvents in internal/hooks/cursor_hooks.go). | ||
| case "beforeSubmitPrompt": | ||
| return "running" | ||
| case "beforeShellExecution": |
There was a problem hiding this comment.
The comment above says "waiting is only wrong for auto-approved commands, which resolve to running again almost immediately" — that isn't right. afterShellExecution fires when the command completes, not when the approval clears. So an auto-approved npm test that takes 90s shows ◐ waiting for the full 90s while the agent is actually working. A denied command may never fire afterShellExecution at all, pinning it indefinitely.
And Cursor is on the pure-hook UpdateStatus branch (session.go:770) with no pane fallback, so nothing corrects it.
Two things consume that wrong state:
Spacerotates to it as if it needs attention.quickApproveSelected(app.go:4243) gates only onStatusWaiting+IsAlive()— no agent gate. PressingYsendsythen Enter. With no approval prompt on screen, thatyis submitted as a new user message mid-turn.
The PR description is candid that this mapping was inferred from docs rather than observed against a live prompt. Given the Y blast radius, I'd want it observed before merge — and if Cursor really has no distinguishable approval signal, mapping beforeShellExecution→running is the safe default (loses the waiting state, never fabricates one).
| return "waiting" | ||
| case "afterShellExecution": | ||
| return "running" | ||
| case "stop": |
There was a problem hiding this comment.
Cursor's stop payload carries status: "completed" | "aborted" | "error", but this maps all three to finished unconditionally — no field of the payload is read.
So a turn that died on a model error or was aborted renders with the ● "done, come look" dot, and Space presents it alongside genuinely completed work. The user discovers it failed only after attaching.
OpenCode's adapter already distinguishes this (session.error → "error"), and applyHookStatus has a case "error" arm ready — it's just unreachable for Cursor. Reading status here would wire it up.
| return "running" | ||
| case "stop": | ||
| return "finished" | ||
| case "sessionEnd": |
There was a problem hiding this comment.
sessionEnd → "dead" lands in applyHookStatus's case "dead" (session.go:857 / 922), which sets StatusError unconditionally and triggers a crash dump — with no pane corroboration.
But Cursor fires sessionEnd when a conversation ends (reason completed / aborted / user_close), which isn't the same as the process exiting. The tmux liveness checks above have already passed at that point, so the pane is alive and sitting at its prompt.
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 "dead", so this arm was effectively unreachable before now. If Cursor's sessionEnd carries a reason, gate on it; otherwise a pane-death check before StatusError would be safer.
| // with no pane scraping. Before the first event there is no hook — the | ||
| // session sits idle at its prompt. | ||
| if s.Agent == agent.OpenCode || s.Agent == agent.Cursor { | ||
| if !hasHook { |
There was a problem hiding this comment.
This branch force-sets StatusIdle on every tick when hasHook is false — fine for OpenCode, but the failure mode is quieter for Cursor because there are more ways to end up with no hook file:
- hooks are injected only from
loadSessionsat startup (app.go:6670), so installingcursor-agentwhile fleet is running leaves the session hookless for the rest of the launch; - if
InjectCursorHooksreturns an error, app.go:6672 onlydebuglog.Logger.Errors it — nothing reaches the UI; handleSessionCreatechecks onlyexec.LookPath("cursor-agent"), so session creation still succeeds.
The user then gets a row that resets to ○ idle every 500ms forever: never Running, never Waiting on an approval, invisible to Space and the status pills. No error explains why — unlike a missing claude CLI, which surfaces as a warning in loadSessionsMsg.
Surfacing the injection error as a warning (same as the claude path) would make this diagnosable.
| } | ||
| // Install Cursor CLI hooks too, but only if cursor-agent is present — never | ||
| // create ~/.cursor for users who don't have it. | ||
| if _, err := exec.LookPath("cursor-agent"); err == nil { |
There was a problem hiding this comment.
This installs into Cursor's global ~/.cursor/hooks.json — which the Cursor IDE also reads, not just cursor-agent. And having the IDE installed is exactly what puts cursor-agent on PATH, so the LookPath guard selects for the affected users rather than against them.
Every agent action in the GUI then forks fleet hook-handler, which hits the instanceID == "" branch (hook_handler.go:140) and emits log.Warn("hook-handler: no FLEET_INSTANCE_ID env var", ...). Warn is above the default level, so it always writes.
Two consequences:
debug.logis size-truncated, so a normal editing session evicts fleet's own history — and the!bug-report flow pastes the last 100 lines into a public GitHub issue. Reports would ship 100 lines ofno FLEET_INSTANCE_IDinstead of diagnostics.beforeShellExecutionis a blocking hook, so every terminal command in the IDE waits on a fleet process spawn.
At minimum the no-instance-id path should drop to Debug. Better: check whether Cursor supports a CLI-scoped hooks file, or bail out early when FLEET_INSTANCE_ID is unset before doing any work.
| for i, raw := range entries { | ||
| var probe cursorHookEntryProbe | ||
| if err := json.Unmarshal(raw, &probe); err != nil { | ||
| continue // not a command-hook object (e.g. a prompt hook) — leave it untouched |
There was a problem hiding this comment.
The comment describes a branch that isn't reached. A prompt-hook entry ({"type":"prompt","prompt":"..."}) unmarshals into cursorHookEntryProbe without error — Command just decodes to "" — so it exits via !isFleetHook(probe.Command) on line 194, not this continue. Coverage confirms line 192 is never hit by TestInjectCursorHooksPreservesUnknownFieldsAndPromptHooks, which is the test that seeds one.
The test passing makes the comment look load-bearing. A maintainer reads "not a command-hook object (e.g. a prompt hook) — leave it untouched", concludes prompt hooks are already filtered here, and treats the line-194 guard as only about other users' command hooks — the natural next simplification. Drop or tighten it and the loop claims the first entry of any event as fleet's own and patches its command key, rewriting a user's prompt hook into {"type":"prompt","prompt":"...","command":"'/path/fleet' hook-handler --fleet-hook"}. The existing test still passes (entry count and prompt field both survive).
Either reword to describe what actually happens, or make the probe genuinely reject non-command entries (e.g. required-field check) so the comment becomes true.
| - Extension commands: `open_or_focus`, `close_tab`, `create_tab_group`, `ping` | ||
| - Service worker reconnects to native host on disconnect (2s delay) | ||
| - Claude Code + OpenAI Codex + OpenCode, macOS + Linux (see `docs/linux.md` for the platform split) | ||
| - Claude Code + OpenAI Codex + OpenCode + Cursor CLI, macOS + Linux (see `docs/linux.md` for the platform split) |
There was a problem hiding this comment.
CLAUDE.md is updated in five places for Cursor, but README.md still says three agents in ten — and it's the one users actually see:
- L8 tagline: "orchestrating Claude Code, Codex & OpenCode sessions"
- L85 requirements: the three CLIs as the "at least one"
- L95:
# 'A' — new session, pick the agent (Claude Code / Codex / OpenCode) - L102/104 feature heading + body, L134 resume list, L146, L159 comparison row ("✅ Claude + Codex + OpenCode"), L164, L166
- L178 keybindings table:
A→ "New session (pick agent: Claude Code / Codex / OpenCode)"
A Cursor CLI user reading the GitHub page concludes fleet doesn't support their agent; a user who presses A sees a fourth option the docs never mention. The OpenCode PR updated README in lockstep with CLAUDE.md, so this is drift introduced here rather than pre-existing.
A hooks.json holding a bare `null` unmarshals into a nil root map without returning an error, so the parse guard let it through and the root["hooks"] assignment panicked. InjectCursorHooks runs from loadSessions' tea.Cmd, so this took the TUI down at startup with a stack trace — leaving no way to reach the UI and fix the file. Initialize the root map the same way the events map one level down already does, and cover both with regression tests. Also adds the stale-fleet-path migration test the Claude twin has but this didn't: that patch branch runs on every install whose binary path changed, and a regression there would append a duplicate entry on each launch. Rewords the `continue` comment in mergeCursorHookEvent, which described a branch it doesn't reach — prompt hooks unmarshal cleanly into the probe with an empty Command and exit at the isFleetHook check below instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNvng2fxraDgEXNtmtgHs9
Verified against the shipped cursor-agent bundle (2026.07.23-e383d2b): afterShellExecution's payload carries `output` and `duration`, so it fires when the command *completes*, not when an approval clears. Mapping beforeShellExecution to waiting therefore held the row at ◐ for the entire runtime of every command the agent ran — and Cursor is on the pure-hook path with no pane fallback to correct it. `Space` rotated to busy sessions, and `Y` quick-approve (which gates only on StatusWaiting) would send "y"+Enter into a pane showing no prompt, injecting a stray message mid-turn. Cursor exposes no approval hook at all, so both shell hooks now map to running and fleet simply has no waiting signal for it. The stop hook carries status: "completed" | "aborted" | "error" (three literal call sites in the bundle). All three mapped to finished, so a turn that died on a model error rendered with the same "done, come look" dot as a successful one. Now only errors surface as errors; aborted is a user-initiated cancel, so it stays finished. Also widens emitHookMetrics, which matched only Claude's PascalCase event names — prompt/response volume and the first-response onboarding milestone were silently dropped for OpenCode and would have been for Cursor, registering those users as never having gotten a response. And drops the missing-FLEET_INSTANCE_ID log from Warn to Debug: fleet installs into the user-global ~/.cursor/hooks.json, which the Cursor IDE reads too, so this fired for every editor action. debug.log is size-truncated and the `!` bug-report flow pastes its last 100 lines into a public issue, so an editing session would evict the actual diagnostics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNvng2fxraDgEXNtmtgHs9
Cursor has no fork primitive, so its BuildLaunchCmd branch dropped LaunchOpts.ForkID — but `f` / "Fork Session" gated only on a captured session id, not the agent. Since Cursor's hook payload does carry session_id, that path was live: forking a Cursor session launched a bare cursor-agent, producing a row titled "<title> (fork)" holding an empty conversation with no error. That contradicts the documented invariant that a lit context-menu row can never dead-click. Adds Type.SupportsFork() so the capability lives next to the launch commands that implement it, and gates both forkSelected and the context menu on it — the row now dims with "Cursor has no fork" rather than lying. TestSupportsFork ties the two together: any agent claiming support must actually change its command when given a ForkID. Separately, a failed InjectCursorHooks was only logged. Cursor rides the pure-hook status path, so with no hooks.json every session resets to idle on each tick — never running, never waiting — with nothing on screen explaining why. It now surfaces as a startup warning like a missing claude CLI does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNvng2fxraDgEXNtmtgHs9
CLAUDE.md was updated for Cursor in five places but README.md still said three agents in eleven — the tagline, requirements, feature section, comparison table, and keybindings table. A Cursor CLI user reading the GitHub page would conclude fleet doesn't support their agent, and anyone pressing `A` would see a fourth option the docs never mention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNvng2fxraDgEXNtmtgHs9
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/hooks/hook_watcher.go (1)
268-276: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winExclude failed Cursor turns from response-success metrics.
A Cursor
stopevent withStatus == "error"reaches this branch.cmd/fleet/hook_handler.goLines 94-95 set that status for a failed turn. This code then records a response and can mark the first-response onboarding milestone.Skip response-success metrics when
curr.Status == "error". Add a regression test for a Cursorstoperror.Proposed fix
case "Stop", "stop", "session.idle": + if curr.Status == "error" { + return + } if prev == nil || !isResponseEnd(prev.Event) || !prev.UpdatedAt.Equal(curr.UpdatedAt) { analytics.Track(analytics.EventClaudeResponseReceived, nil)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/hooks/hook_watcher.go` around lines 268 - 276, Update the "Stop", "stop", and "session.idle" handling branch in the hook watcher to skip response-success analytics when curr.Status equals "error", including both response and first-response onboarding milestone tracking. Preserve existing tracking behavior for non-error events, and add a regression test covering a Cursor stop event with error status.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 102-104: Update the README paragraph describing Claude, Codex,
OpenCode, and Cursor to qualify the status behavior: Cursor uses hooks only and
has no pane-heuristic fallback. Also clarify that Cursor auto-naming uses
Fleet’s prompt heuristic because it cannot retrieve local chat titles, rather
than claiming identical status and naming behavior across all agents.
---
Outside diff comments:
In `@internal/hooks/hook_watcher.go`:
- Around line 268-276: Update the "Stop", "stop", and "session.idle" handling
branch in the hook watcher to skip response-success analytics when curr.Status
equals "error", including both response and first-response onboarding milestone
tracking. Preserve existing tracking behavior for non-error events, and add a
regression test covering a Cursor stop event with error status.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7f973d58-918a-4ac8-81cc-9e82d1e29faf
📒 Files selected for processing (9)
README.mdcmd/fleet/hook_handler.gocmd/fleet/hook_handler_test.gointernal/agent/agent.gointernal/agent/agent_test.gointernal/hooks/cursor_hooks.gointernal/hooks/cursor_hooks_test.gointernal/hooks/hook_watcher.gointernal/ui/app.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/hooks/cursor_hooks.go
- internal/ui/app.go
| ### Claude Code, Codex, OpenCode, or Cursor — per session | ||
|
|
||
| Pick the agent when you create a session: **`a`** fires instantly with your default agent, **`A`** opens a picker. Run Claude, Codex, and OpenCode sessions side by side in the same repo. Status, resume, and auto-naming work identically across all three — driven by each agent's own hook events, with pane heuristics filling the rare states that fire no hook. | ||
| Pick the agent when you create a session: **`a`** fires instantly with your default agent, **`A`** opens a picker. Run Claude, Codex, OpenCode, and Cursor sessions side by side in the same repo. Status, resume, and auto-naming work identically across all four — driven by each agent's own hook events, with pane heuristics filling the rare states that fire no hook. |
There was a problem hiding this comment.
📐 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 102 - 104, Update the README paragraph describing
Claude, Codex, OpenCode, and Cursor to qualify the status behavior: Cursor uses
hooks only and has no pane-heuristic fallback. Also clarify that Cursor
auto-naming uses Fleet’s prompt heuristic because it cannot retrieve local chat
titles, rather than claiming identical status and naming behavior across all
agents.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (3)
README.md:104
- The README claims status across all four agents is driven by hook events with pane heuristics filling no-hook states, but Cursor (and OpenCode) are hook-only with no pane fallback. This makes the sentence inaccurate and could mislead users debugging why a Cursor session stays idle when hooks aren’t installed.
Pick the agent when you create a session: **`a`** fires instantly with your default agent, **`A`** opens a picker. Run Claude, Codex, OpenCode, and Cursor sessions side by side in the same repo. Status, resume, and auto-naming work identically across all four — driven by each agent's own hook events, with pane heuristics filling the rare states that fire no hook.
CLAUDE.md:138
- This Cursor status mapping documentation contradicts the actual Cursor event mapping in cmd/fleet/hook_handler.go: beforeShellExecution/afterShellExecution are both mapped to running (and the comment explicitly says Cursor has no waiting signal). The docs currently claim beforeShellExecution→waiting, which would lead users to the wrong mental model when triaging status behavior.
- Cursor CLI status: pure hook-driven (no pane scraping), same `UpdateStatus` branch as OpenCode. Cursor CLI (`cursor-agent`) has real declarative hooks like Codex, but a flatter `hooks.json` schema (`{"version":1,"hooks":{"<event>":[{"command","type"}]}}`, no matcher-grouping) — installed to `~/.cursor/hooks.json` via `InjectCursorHooks`/`cursor_hooks.go`, only when `cursor-agent` is on PATH, no dir-trust seeding (Cursor's permission model is a global allowlist in `~/.cursor/cli-config.json`, not a per-directory trust flag). Event mapping in `mapEventToStatus`: no `sessionStart` case (and fleet doesn't subscribe to it) — a freshly launched session is already idle via `initialRunStatus`, and mapping it to finished the way Claude's `SessionStart` does would flip an untouched session away from idle before any turn ran; `stop`→finished, `beforeSubmitPrompt`→running, `beforeShellExecution`→waiting/`afterShellExecution`→running (Cursor has no dedicated approval hook, so these bracket the interactive y/n prompt instead), `sessionEnd`→dead. No known local source for reading a Cursor chat's title, so `ReadAgentSessionName` falls back to the prompt heuristic like OpenCode.
internal/ui/app.go:3836
- The fork action now gates on SupportsFork() and can be enabled for non-Claude agents, but this error message still says “Claude conversation ID”. Since ClaudeSessionID is used as the generic resume/session ID for multiple agents in this codebase, the message is misleading for Codex/OpenCode users.
if s.ClaudeSessionID == "" {
h.setError(fmt.Errorf("cannot fork: session has no Claude conversation ID yet"))
return nil
Captured from a live cursor-agent run (2026.07.23-e383d2b), the sessionEnd
payload is:
{reason: "completed", final_status, duration_ms, session_id,
conversation_id, transcript_path, is_background_agent, ...}
It fires when a *conversation* ends, with the process alive and sitting at
its prompt. Mapping it to "dead" put a healthy session into StatusError,
wrote a spurious crash dump, and made it eligible for Reload All Sessions
— which restarts dead/error sessions, killing a live agent.
Cursor's actual process death is already covered by tmux pane-death, the
same way Codex and OpenCode handle it; neither maps an end-of-session hook
to dead either.
The same capture confirms two things the earlier commits relied on: the
payload carries session_id and conversation_id with identical values (so
resume IDs are captured), and afterShellExecution carries a `duration`
(1171.189ms on a real command), confirming it fires at completion rather
than on approval.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNvng2fxraDgEXNtmtgHs9
# Conflicts: # internal/ui/app.go
|
@VainEyal heads up — I pushed 6 commits to this branch (maintainer edits), so please review them before merging. I'd rather you sign off than have me merge my own changes into your PR. To validate the review I installed Two of my review comments were wrong — retracted
Both threads are retracted and resolved. Sorry for the noise. What the 6 commits change
One thing I did NOT fix — worth your callCursor CLI appears to have a per-directory trust gate, which contradicts the comment in
Launching The bundle writes a trust marker ( Why it matters here: fleet creates a new worktree per session, so each one is a fresh directory. If that prompt appears, the session sits blocked on it — and because Cursor rides the pure-hook path (and I just removed the only waiting mapping), fleet will render it as idle rather than needing attention. That's arguably worse than the bug I fixed. Needs one interactive run in a real tmux pane to settle. If it's real, the fix is probably an Also worth knowingHooks install into the user-global |
# Conflicts: # CLAUDE.md
Summary
cursor-agent) as a fourth session agent alongside Claude, Codex, and OpenCode: selectable from theAagent picker or asdefault_agentin config/Settings.internal/agent: newCursortype with binary name, display name, and launch/resume command (cursor-agent/cursor-agent --resume <id>). No fork primitive exists incursor-agent, so fork-to-worktree stays Claude-only (existing guard covers it automatically).internal/hooks/cursor_hooks.go(new): installs fleet's hook command into~/.cursor/hooks.json, only whencursor-agentis on PATH. Cursor's hooks.json schema is flatter than Claude/Codex's nested{matcher, hooks:[...]}shape, so this is a small dedicated injector rather than a reuse of the Codex one — it does reuse the agent-neutral marker/path helpers (isFleetHook,GetHookCommand,FleetBinaryPath).UpdateStatus) — no pane-scraping. Cursor has no dedicated permission/approval hook, sobeforeShellExecution→waiting /afterShellExecution→running bracket the interactive approval prompt instead.~/.cursor/cli-config.json), not a per-directory trust flag.✦glyph for Cursor sessions; diagnostics reportscursor-agent --version.CLAUDE.mdand a changelog fragment (changelog/unreleased/cursor-agent.md, not flagged as a highlight) are updated accordingly.Type of Change
Checklist
make test)make lint) —golangci-lintisn't installed in the environment this was built in;go vet ./...andgo fmt ./...are cleanCLAUDE.md)changelog/unreleased/Notes for reviewers
beforeShellExecution/afterShellExecution→ waiting/running status mapping is inferred from Cursor's public docs, not observed against a live approval prompt — worth a manual smoke test with a realcursor-agentsession before merging.cursor-agent(confirmed locally; Cursor's installer also symlinks it as the more genericagent, which was deliberately avoided to reduce PATH-collision risk).Summary by CodeRabbit