Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -5171,6 +5171,14 @@ final class CodexAgentModeCoordinator: AgentModeRunInteractionStateObserving {
await handleCodexNativeEvent(event, session: session)
}

@_spi(TestSupport)
public func test_settleCodexComputerUseActivationAfterTurn(
_ session: AgentModeViewModel.TabSession,
reason: String = "test"
) {
settleCodexComputerUseActivationAfterTurn(session, reason: reason)
}

@_spi(TestSupport)
public static func test_mergeCommandRunningUpdates(
existing: CodexNativeSessionController.CommandExecutionRunningUpdate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation

extension Notification.Name {
static let codexGoalSupportDidChange = Notification.Name("RepoPrompt.codexGoalSupportDidChange")
static let codexComputerUseAvailabilityDidChange = Notification.Name("RepoPrompt.codexComputerUseAvailabilityDidChange")
}

private enum CodexNativeFeatureGate: Hashable {
Expand Down Expand Up @@ -129,16 +130,75 @@ enum CodexGoalSupport {

enum CodexComputerUseWorkflow {
static let commandName = "computer-use"
static let disabledMessage = "Codex computer-use is currently disabled in RepoPrompt because it requires additional computer permissions/accessibility setup."

@MainActor
static var disabledMessage: String {
availability.primaryUnavailableMessage
}

@MainActor
static var availability: CodexComputerUseAvailability {
CodexComputerUseAvailability(status: currentStatus(includeTimestamp: false))
}

@MainActor
static var isEnabled: Bool {
CodexNativeFeatureGate.computerUse.isEnabled(persistedValue: false)
availability.isReady
}

@MainActor
static func currentStatus(includeTimestamp: Bool = true) -> CodexComputerUseStatus {
currentStatus(
persistedOptIn: GlobalSettingsStore.shared.codexComputerUseEnabled(),
includeTimestamp: includeTimestamp
)
}

static func currentStatus(
persistedOptIn: Bool,
statusService: CodexComputerUseStatusService = .shared,
includeTimestamp: Bool = true
) -> CodexComputerUseStatus {
let effectiveOptIn = CodexNativeFeatureGate.computerUse.isEnabled(persistedValue: persistedOptIn)
return statusService.currentStatus(
optInEnabled: effectiveOptIn,
includeTimestamp: includeTimestamp
)
}

static func resolvedAvailability(
persistedOptIn: Bool,
prerequisites: CodexComputerUsePrerequisiteSnapshot
) -> CodexComputerUseAvailability {
let featureOptIn = CodexNativeFeatureGate.computerUse.isEnabled(persistedValue: persistedOptIn)
return CodexComputerUseAvailability(
featureOptIn: featureOptIn,
prerequisites: prerequisites
)
}

@MainActor
static func setEnabled(_ enabled: Bool) {
GlobalSettingsStore.shared.setCodexComputerUseEnabled(enabled)
}

static func postAvailabilityDidChangeIfNeeded(previousValue: Bool, currentValue: Bool) {
guard currentValue != previousValue else { return }
NotificationCenter.default.post(name: .codexComputerUseAvailabilityDidChange, object: nil)
}

static func prerequisiteSnapshot() -> CodexComputerUsePrerequisiteSnapshot {
CodexComputerUseStatusService.shared.prerequisiteSnapshot()
}

#if DEBUG
static func setEnabledForTesting(_ value: Bool?) {
CodexNativeFeatureGate.computerUse.setEnabledForTesting(value)
}

static func setPrerequisiteSnapshotForTesting(_ snapshot: CodexComputerUsePrerequisiteSnapshot?) {
CodexComputerUseStatusService.setPrerequisiteSnapshotForTesting(snapshot)
}
#endif

static func bubbleWorkflowDefinition() -> AgentWorkflowDefinition {
Expand Down Expand Up @@ -167,9 +227,10 @@ enum CodexComputerUseWorkflow {

Safety requirements:
- Clarify missing target app/site/account, destination, credentials, or intended action before operating.
- Ask for explicit confirmation before destructive, purchasing, sending, publishing, account-changing, or otherwise externally visible actions.
- Treat macOS Screen Recording/Accessibility as OS prerequisites only; still ask for app access when Codex prompts for an allowed target app.
- Do not treat RepoPrompt MCP auto-approval as approval for non-RepoPrompt MCP, plugin, browser, or computer-use tools.
- Keep RepoPrompt workspace file edits separate from external UI automation; explain which surface you are acting on.
- Keep RepoPrompt workspace file edits and shell commands under the session's sandbox/approval policy; desktop app actions are a separate surface.
- Ask for explicit confirmation before destructive, purchasing, sending, publishing, account-changing, or otherwise externally visible actions.
- Prefer non-destructive inspection and reporting before taking action.

<user_instructions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1406,7 +1406,7 @@ struct AgentComposerView: View, Equatable {
steeringUnsupportedMessage = message
}
steeringUnsupportedDismissTask = Task {
try? await Task.sleep(nanoseconds: 3_000_000_000)
try? await Task.sleep(nanoseconds: 10_000_000_000)
guard !Task.isCancelled else { return }
await MainActor.run {
withAnimation(.easeInOut(duration: 0.2)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ struct GlobalScalarPreferences: Codable, Equatable {
var maxBackgroundAgentComposeTabs: Int?
var showBuiltInWorkflowCleanupGuidance: Bool?
var codexGoalSupportEnabled: Bool?
var codexComputerUseEnabled: Bool?
var restrictMCPAgentDiscoveryToRoleLabels: Bool?

init(
Expand All @@ -325,6 +326,7 @@ struct GlobalScalarPreferences: Codable, Equatable {
maxBackgroundAgentComposeTabs: Int? = nil,
showBuiltInWorkflowCleanupGuidance: Bool? = nil,
codexGoalSupportEnabled: Bool? = nil,
codexComputerUseEnabled: Bool? = nil,
restrictMCPAgentDiscoveryToRoleLabels: Bool? = nil
) {
self.proEditAgentMode = proEditAgentMode
Expand All @@ -335,6 +337,7 @@ struct GlobalScalarPreferences: Codable, Equatable {
self.maxBackgroundAgentComposeTabs = maxBackgroundAgentComposeTabs
self.showBuiltInWorkflowCleanupGuidance = showBuiltInWorkflowCleanupGuidance
self.codexGoalSupportEnabled = codexGoalSupportEnabled
self.codexComputerUseEnabled = codexComputerUseEnabled
self.restrictMCPAgentDiscoveryToRoleLabels = restrictMCPAgentDiscoveryToRoleLabels
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,60 @@ class GlobalSettingsStore: ObservableObject {
CodexGoalSupport.postDidChangeIfNeeded(previousValue: oldValue, currentValue: codexGoalSupportEnabled())
}

func codexComputerUseEnabled() -> Bool {
scalarPreferences.agentMode?.codexComputerUseEnabled ?? false
}

func setCodexComputerUseEnabled(_ enabled: Bool, commit: Bool = true) {
let previousAvailability = CodexComputerUseWorkflow.resolvedAvailability(
persistedOptIn: codexComputerUseEnabled(),
prerequisites: CodexComputerUseWorkflow.prerequisiteSnapshot()
).isReady
updateAgentModeScalar(commit: commit) { settings in
settings.codexComputerUseEnabled = enabled
}
let currentAvailability = CodexComputerUseWorkflow.resolvedAvailability(
persistedOptIn: codexComputerUseEnabled(),
prerequisites: CodexComputerUseWorkflow.prerequisiteSnapshot()
).isReady
CodexComputerUseWorkflow.postAvailabilityDidChangeIfNeeded(
previousValue: previousAvailability,
currentValue: currentAvailability
)
}

func codexRawEventLoggingEnabled() -> Bool {
CodexAppServerDiagnostics.rawEventLoggingEnabled(defaults: defaults)
}

func setCodexRawEventLoggingEnabled(_ enabled: Bool) {
defaults.set(enabled, forKey: CodexAppServerDiagnostics.rawEventLoggingEnabledKey)
}

func codexRawEventLogFilePath() -> String {
CodexAppServerDiagnostics.rawEventLogFilePath(defaults: defaults)
}

func setCodexRawEventLogFilePath(_ path: String) {
CodexAppServerDiagnostics.setRawEventLogFilePath(path, defaults: defaults)
}

func codexAppServerDiagnosticsEnabled() -> Bool {
CodexAppServerDiagnostics.appServerDiagnosticsEnabled(defaults: defaults)
}

func setCodexAppServerDiagnosticsEnabled(_ enabled: Bool) {
defaults.set(enabled, forKey: CodexAppServerDiagnostics.appServerDiagnosticsEnabledKey)
}

func codexAppServerDiagnosticsLogFilePath() -> String {
CodexAppServerDiagnostics.appServerDiagnosticsLogFilePath(defaults: defaults)
}

func setCodexAppServerDiagnosticsLogFilePath(_ path: String) {
CodexAppServerDiagnostics.setAppServerDiagnosticsLogFilePath(path, defaults: defaults)
}

#if DEBUG
func claudeRawEventLoggingEnabled() -> Bool {
defaults.bool(forKey: "claudeRawEventLoggingEnabled")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import AppKit
import Combine
import Foundation

@MainActor
final class AgentModeComputerUseSettingsViewModel: ObservableObject {
@Published private(set) var status: CodexComputerUseStatus
@Published private(set) var isRefreshing = false
@Published private(set) var lastActionMessage: String?

private let globalSettings: GlobalSettingsStore
private let statusService: CodexComputerUseStatusService
private let openURL: (URL) -> Void
private let pasteboard: NSPasteboard
private var refreshGeneration = 0

init(
globalSettings: GlobalSettingsStore = .shared,
statusService: CodexComputerUseStatusService = .shared,
openURL: @escaping (URL) -> Void = { NSWorkspace.shared.open($0) },
pasteboard: NSPasteboard = .general
) {
self.globalSettings = globalSettings
self.statusService = statusService
self.openURL = openURL
self.pasteboard = pasteboard
status = CodexComputerUseWorkflow.currentStatus(
persistedOptIn: globalSettings.codexComputerUseEnabled(),
statusService: statusService
)
}

var optInEnabled: Bool {
globalSettings.codexComputerUseEnabled()
}

func refresh() {
refreshGeneration += 1
let generation = refreshGeneration
isRefreshing = true
let refreshed = CodexComputerUseWorkflow.currentStatus(
persistedOptIn: globalSettings.codexComputerUseEnabled(),
statusService: statusService
)
guard generation == refreshGeneration else { return }
status = refreshed
isRefreshing = false
}

func setOptInEnabled(_ enabled: Bool) {
globalSettings.setCodexComputerUseEnabled(enabled)
refresh()
}

func requestScreenRecordingAccess() {
let result = statusService.requestScreenRecordingAccess()
lastActionMessage = result.userMessage
refresh()
}

func requestAccessibilityAccess() {
let result = statusService.requestAccessibilityAccess()
lastActionMessage = result.userMessage
refresh()
}

func openScreenRecordingSettings() {
openURL(Self.screenRecordingSettingsURL)
lastActionMessage = "Opened Screen Recording settings. Grant access, then return to RepoPrompt and refresh status."
}

func openAccessibilitySettings() {
openURL(Self.accessibilitySettingsURL)
lastActionMessage = "Opened Accessibility settings. Grant access, then return to RepoPrompt and refresh status."
}

func openCodexComputerUseGuide() {
openURL(Self.codexComputerUseDocsURL)
lastActionMessage = "Opened the Codex Computer Use setup guide."
}

func copyManualSetupInstructions(for requirement: CodexComputerUsePrerequisite) {
let instructions = manualSetupInstructions(for: requirement)
pasteboard.clearContents()
pasteboard.setString(instructions, forType: .string)
lastActionMessage = "Copied setup instructions to the clipboard."
}

private func manualSetupInstructions(for requirement: CodexComputerUsePrerequisite) -> String {
switch requirement {
case .featureOptIn:
"Open RepoPrompt Settings → Agent Mode → Computer Use, then enable /computer-use."
case .plugin, .liveAvailability:
"Open Codex Settings → Computer Use, install or enable the Computer Use plugin, then return to RepoPrompt Settings → Agent Mode → Computer Use and click Refresh. RepoPrompt detects Codex app-managed Computer Use installs and legacy ~/.codex/config.toml entries; live tool availability may be reported as unknown when Codex does not expose a catalog."
case .screenRecording:
"Open System Settings → Privacy & Security → Screen & System Audio Recording (or Screen Recording), grant access to RepoPrompt, Codex, or the helper macOS lists, then restart RepoPrompt/Codex if prompted and click Refresh."
case .accessibility:
"Open System Settings → Privacy & Security → Accessibility, grant access to RepoPrompt, Codex, or the helper macOS lists, then click Refresh."
}
}

private static let codexComputerUseDocsURL = URL(string: "https://developers.openai.com/codex/app/computer-use")!
private static let screenRecordingSettingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")!
private static let accessibilitySettingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!
}
Loading
Loading