diff --git a/supacode/Domain/AgentDetection/DetectedAgent.swift b/supacode/Domain/AgentDetection/DetectedAgent.swift index ccc9015c..1872ca58 100644 --- a/supacode/Domain/AgentDetection/DetectedAgent.swift +++ b/supacode/Domain/AgentDetection/DetectedAgent.swift @@ -6,7 +6,7 @@ enum DetectedAgent: String, CaseIterable, Equatable, Identifiable, Sendable { case claude case codex case gemini - case cursor + case cursor = "cursor-agent" case cline case opencode case copilot @@ -17,7 +17,12 @@ enum DetectedAgent: String, CaseIterable, Equatable, Identifiable, Sendable { var id: String { rawValue } var displayName: String { - rawValue + switch self { + case .cursor: + return "cursor" + default: + return rawValue + } } var iconLookupToken: String { @@ -26,6 +31,8 @@ enum DetectedAgent: String, CaseIterable, Equatable, Identifiable, Sendable { return "claude" case .copilot: return "copilot" + case .cursor: + return "cursor" default: return rawValue } diff --git a/supacode/Features/Terminal/Models/CommandIconMap.swift b/supacode/Features/Terminal/Models/CommandIconMap.swift index abb17f37..a9360ac5 100644 --- a/supacode/Features/Terminal/Models/CommandIconMap.swift +++ b/supacode/Features/Terminal/Models/CommandIconMap.swift @@ -32,12 +32,14 @@ enum CommandIconMap { private static let firstTokenMapping: [String: TabIconSource] = [ // Coding agents "aider": TabIconSource(systemSymbol: "sparkle"), + "agent": TabIconSource(systemSymbol: "sparkle", assetName: "Cursor"), "amp": TabIconSource(systemSymbol: "sparkle", assetName: "Amp"), "claude": TabIconSource(systemSymbol: "sparkle", assetName: "ClaudeCode"), "cline": TabIconSource(systemSymbol: "sparkle", assetName: "Cline"), "codex": TabIconSource(systemSymbol: "sparkle", assetName: "Codex"), "copilot": TabIconSource(systemSymbol: "sparkle", assetName: "GitHubCopilot"), "cursor": TabIconSource(systemSymbol: "sparkle", assetName: "Cursor"), + "cursor-agent": TabIconSource(systemSymbol: "sparkle", assetName: "Cursor"), "droid": TabIconSource(systemSymbol: "sparkle", assetName: "Droid"), "gemini": TabIconSource(systemSymbol: "sparkle", assetName: "Gemini"), "kimi": TabIconSource(systemSymbol: "sparkle", assetName: "Kimi"), diff --git a/supacode/Infrastructure/AgentDetection/AgentClassifier.swift b/supacode/Infrastructure/AgentDetection/AgentClassifier.swift index 90b6cde5..7170b8aa 100644 --- a/supacode/Infrastructure/AgentDetection/AgentClassifier.swift +++ b/supacode/Infrastructure/AgentDetection/AgentClassifier.swift @@ -10,7 +10,7 @@ func identifyAgent(processName: String) -> DetectedAgent? { return .codex case "gemini": return .gemini - case "cursor": + case "agent", "cursor", "cursor-agent": return .cursor case "cline": return .cline diff --git a/supacode/Infrastructure/AgentDetection/ScreenHeuristics.swift b/supacode/Infrastructure/AgentDetection/ScreenHeuristics.swift index dd5df635..f94a6bc2 100644 --- a/supacode/Infrastructure/AgentDetection/ScreenHeuristics.swift +++ b/supacode/Infrastructure/AgentDetection/ScreenHeuristics.swift @@ -112,14 +112,16 @@ private func detectGemini(_ content: String) -> AgentRawState { private func detectCursor(_ content: String) -> AgentRawState { let lower = content.lowercased() - if lower.contains("(y) (enter)") + if lower.contains("workspace trust required") + || lower.contains("trust this workspace") + || lower.contains("(y) (enter)") || lower.contains("keep (n)") || lower.contains("skip (esc or n)") || (lower.contains("(y)") && (lower.contains("allow") || lower.contains("run"))) { return .blocked } - if lower.contains("ctrl+c to stop") || hasCursorSpinner(content) { + if lower.contains("trusting workspace") || lower.contains("ctrl+c to stop") || hasCursorSpinner(content) { return .working } return .idle diff --git a/supacodeTests/AgentClassifierTests.swift b/supacodeTests/AgentClassifierTests.swift index f1090cb8..34e45c76 100644 --- a/supacodeTests/AgentClassifierTests.swift +++ b/supacodeTests/AgentClassifierTests.swift @@ -9,7 +9,9 @@ struct AgentClassifierTests { #expect(identifyAgent(processName: "claude-code") == .claude) #expect(identifyAgent(processName: "codex") == .codex) #expect(identifyAgent(processName: "gemini") == .gemini) + #expect(identifyAgent(processName: "agent") == .cursor) #expect(identifyAgent(processName: "cursor") == .cursor) + #expect(identifyAgent(processName: "cursor-agent") == .cursor) #expect(identifyAgent(processName: "cline") == .cline) #expect(identifyAgent(processName: "opencode") == .opencode) #expect(identifyAgent(processName: "open-code") == .opencode) @@ -22,6 +24,42 @@ struct AgentClassifierTests { #expect(identifyAgent(processName: "amp-local") == .amp) } + @Test func identifiesCursorAgentAliasCommandLines() throws { + let job = ForegroundJob( + processGroupID: 42, + processes: [ + ForegroundProcess( + pid: 100, + name: "agent", + argv0: "agent", + cmdline: "agent" + ) + ] + ) + + let result = try #require(identifyAgentInJob(job)) + #expect(result.agent == .cursor) + #expect(result.name == "agent") + } + + @Test func identifiesCursorAgentCommandLines() throws { + let job = ForegroundJob( + processGroupID: 42, + processes: [ + ForegroundProcess( + pid: 100, + name: "node", + argv0: "node", + cmdline: "node /opt/homebrew/bin/cursor-agent" + ) + ] + ) + + let result = try #require(identifyAgentInJob(job)) + #expect(result.agent == .cursor) + #expect(result.name == "cursor-agent") + } + @Test func ignoresPlainShellsAndUnknownProcesses() { #expect(identifyAgent(processName: "zsh") == nil) #expect(identifyAgent(processName: "bash") == nil) diff --git a/supacodeTests/CommandIconMapTests.swift b/supacodeTests/CommandIconMapTests.swift index 4597e03d..ad4b0f5d 100644 --- a/supacodeTests/CommandIconMapTests.swift +++ b/supacodeTests/CommandIconMapTests.swift @@ -65,12 +65,14 @@ struct CommandIconMapTests { @Test func codingAgentsResolved() { // Sample of the coding-agent set — they all share the sparkle SF // Symbol fallback, asset names match the imageset folders. + #expect(CommandIconMap.iconForFirstToken("agent")?.assetName == "Cursor") #expect(CommandIconMap.iconForFirstToken("claude")?.assetName == "ClaudeCode") #expect(CommandIconMap.iconForFirstToken("codex")?.assetName == "Codex") #expect(CommandIconMap.iconForFirstToken("gemini")?.assetName == "Gemini") #expect(CommandIconMap.iconForFirstToken("copilot")?.assetName == "GitHubCopilot") #expect(CommandIconMap.iconForFirstToken("pi")?.assetName == "Pi") #expect(CommandIconMap.iconForFirstToken("cursor")?.assetName == "Cursor") + #expect(CommandIconMap.iconForFirstToken("cursor-agent")?.assetName == "Cursor") #expect(CommandIconMap.iconForFirstToken("cline")?.assetName == "Cline") #expect(CommandIconMap.iconForFirstToken("droid")?.assetName == "Droid") // Aider has no bundled brand asset — sparkle fallback only. diff --git a/supacodeTests/DetectedAgentTests.swift b/supacodeTests/DetectedAgentTests.swift index c448abe5..96fc13c7 100644 --- a/supacodeTests/DetectedAgentTests.swift +++ b/supacodeTests/DetectedAgentTests.swift @@ -5,7 +5,8 @@ import Testing struct DetectedAgentTests { @Test func displayNamesUseCommandStyleTokens() { for agent in DetectedAgent.allCases { - #expect(agent.displayName == agent.rawValue) + let expectedDisplayName = agent == .cursor ? "cursor" : agent.rawValue + #expect(agent.displayName == expectedDisplayName) } } } diff --git a/supacodeTests/ScreenHeuristicsTests.swift b/supacodeTests/ScreenHeuristicsTests.swift index f5edeb90..dbcb1ac6 100644 --- a/supacodeTests/ScreenHeuristicsTests.swift +++ b/supacodeTests/ScreenHeuristicsTests.swift @@ -161,6 +161,17 @@ struct ScreenHeuristicsTests { #expect(DetectedAgent.gemini.detectState(in: "done") == .idle) #expect(DetectedAgent.cursor.detectState(in: "Run command? (y) (enter)") == .blocked) + #expect( + DetectedAgent.cursor.detectState( + in: """ + ⚠ Workspace Trust Required + Cursor Agent can execute code and access files in this directory. + [a] Trust this workspace + [q] Quit + """ + ) == .blocked + ) + #expect(DetectedAgent.cursor.detectState(in: "⏳ Trusting workspace...") == .working) #expect(DetectedAgent.cursor.detectState(in: "⬡ indexing") == .working) #expect(DetectedAgent.cursor.detectState(in: "done") == .idle)