Skip to content
Merged
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
12 changes: 6 additions & 6 deletions Orbit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.4;
MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex";
PRODUCT_NAME = Orbit;
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -454,7 +454,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.4;
MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex";
PRODUCT_NAME = Orbit;
REGISTER_APP_GROUPS = YES;
Expand All @@ -476,7 +476,7 @@
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0.4;
MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.tests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -497,7 +497,7 @@
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0.4;
MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.tests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -516,7 +516,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.4;
MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.uitests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -535,7 +535,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.4;
MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.uitests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand Down
9 changes: 9 additions & 0 deletions Orbit/AppBundleConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
import Foundation

enum AppBundleConfiguration {
static var showsCodexDebugInfo: Bool {
#if DEBUG
let defaultValue = true
#else
let defaultValue = false
#endif
return boolValue(forKey: "OrbitShowCodexDebug", defaultValue: defaultValue)
}

static func stringValue(forKey key: String) -> String? {
if let environmentValue = ProcessInfo.processInfo.environment[environmentVariableName(for: key)] {
let trimmedValue = environmentValue.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down
126 changes: 124 additions & 2 deletions Orbit/CodexAppServerActionProvider.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import AppKit
import Foundation

private enum OrbitMcpStartupState: Equatable {
case unknown
case starting
case ready
case failed(String?)

var isResolved: Bool {
switch self {
case .ready, .failed:
return true
case .unknown, .starting:
return false
}
}
}

@MainActor
final class CodexAppServerActionProvider: ActionProvider {
let displayName = "Codex"
private static let browserToolServerNames = ["playwright", "chrome-devtools"]
private static func makeInitialMcpStartupStates() -> [String: OrbitMcpStartupState] {
Dictionary(uniqueKeysWithValues: browserToolServerNames.map { ($0, .unknown) })
}
private(set) var status: OrbitActionStatus = .idle
private let settings = OrbitSettings.shared
private(set) var authState: OrbitCodexAuthState = .unknown
Expand Down Expand Up @@ -46,8 +66,10 @@ final class CodexAppServerActionProvider: ActionProvider {
private var lastLoginURL: URL?
private var lastLoginID: String?
private var loginRequestTimeoutTask: Task<Void, Never>?
private var pendingTurnStartRetryTask: Task<Void, Never>?
private var lastEmittedLiveCommentary: String?
private var preparedCodexHome: OrbitPreparedCodexHome?
private var mcpStartupStates: [String: OrbitMcpStartupState] = CodexAppServerActionProvider.makeInitialMcpStartupStates()
var stateDidChange: (() -> Void)?

var isConfigured: Bool {
Expand Down Expand Up @@ -233,7 +255,7 @@ final class CodexAppServerActionProvider: ActionProvider {
streamedCommentaryBuffer = ""
hasEmittedEarlyCommentary = false
lastEmittedLiveCommentary = nil
sendTurnStart()
attemptPendingTurnStart()
}

func respondToToolPrompt(requestID: Int, questionID: String, answer: String) {
Expand Down Expand Up @@ -453,6 +475,7 @@ final class CodexAppServerActionProvider: ActionProvider {
serviceTier: resolvedServiceTier
)
self.preparedCodexHome = preparedCodexHome
mcpStartupStates = Self.makeInitialMcpStartupStates()
let launchCommand = resolvedCodexLaunchCommand(
for: codexExecutable,
preparedCodexHome: preparedCodexHome
Expand Down Expand Up @@ -801,7 +824,7 @@ final class CodexAppServerActionProvider: ActionProvider {
appendDebugEvent("<- thread/start ok \(String(threadID.suffix(6)))")
notifyStateChanged()
if pendingPrompt != nil {
sendTurnStart()
attemptPendingTurnStart()
} else {
status = .idle
}
Expand Down Expand Up @@ -934,11 +957,72 @@ final class CodexAppServerActionProvider: ActionProvider {
stateDidChange?()
}

private func attemptPendingTurnStart(forceAfterTimeout: Bool = false) {
guard activeThreadID != nil, pendingPrompt != nil, !isAwaitingTurnCompletion else { return }

if browserToolStartupStillPending && !forceAfterTimeout {
emitPhase(
.startingCodex,
detail: "connecting browser tools.",
rawSource: "waiting for browser tools"
)
schedulePendingTurnStartRetryIfNeeded()
return
}

pendingTurnStartRetryTask?.cancel()
pendingTurnStartRetryTask = nil
sendTurnStart()
}

private var browserToolStartupStillPending: Bool {
Self.browserToolServerNames.contains { serverName in
guard let state = mcpStartupStates[serverName] else { return false }
return !state.isResolved
}
}

private func schedulePendingTurnStartRetryIfNeeded() {
guard pendingTurnStartRetryTask == nil else { return }
pendingTurnStartRetryTask = Task { @MainActor [weak self] in
try? await Task.sleep(nanoseconds: 5_000_000_000)
guard let self else { return }
self.pendingTurnStartRetryTask = nil
self.attemptPendingTurnStart(forceAfterTimeout: true)
}
}

private var runtimeCapabilityNote: String? {
let browserFailures = Self.browserToolServerNames.compactMap { serverName -> String? in
guard case .failed(let errorMessage) = mcpStartupStates[serverName] else { return nil }
let detail = errorMessage?.trimmingCharacters(in: .whitespacesAndNewlines)
if let detail, !detail.isEmpty {
return "- \(serverName) browser tools are unavailable in this Orbit session: \(detail)"
}
return "- \(serverName) browser tools are unavailable in this Orbit session"
}

guard !browserFailures.isEmpty else { return nil }

return """
Runtime capability note:
\(browserFailures.joined(separator: "\n"))
- do not claim browser control is available unless the tools actually work in this session
"""
}

private func sendTurnStart() {
guard let threadID = activeThreadID, let pendingPrompt else { return }

var inputItems: [[String: Any]] = []

if let runtimeCapabilityNote {
inputItems.append([
"type": "text",
"text": runtimeCapabilityNote
])
}

if let latestRequest,
let screenshotPath = latestRequest.screenshotPath,
!screenshotPath.isEmpty {
Expand Down Expand Up @@ -1004,6 +1088,13 @@ final class CodexAppServerActionProvider: ActionProvider {

var inputItems: [[String: Any]] = []

if let runtimeCapabilityNote {
inputItems.append([
"type": "text",
"text": runtimeCapabilityNote
])
}

if let latestRequest,
let screenshotPath = latestRequest.screenshotPath,
!screenshotPath.isEmpty {
Expand Down Expand Up @@ -1255,10 +1346,38 @@ final class CodexAppServerActionProvider: ActionProvider {
return
}

if Self.browserToolServerNames.contains(serverName) {
let errorMessage = (params["error"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
switch status {
case "starting":
mcpStartupStates[serverName] = .starting
case "ready":
mcpStartupStates[serverName] = .ready
case "failed":
mcpStartupStates[serverName] = .failed(errorMessage)
default:
break
}
}

if status == "starting" {
emitPhase(.startingCodex, detail: "starting \(serverName) tools.", rawSource: "starting \(serverName) tools")
} else if status == "ready" && (serverName == "playwright" || serverName == "chrome-devtools") {
emitPhase(.thinking, rawSource: "\(serverName) tools are ready")
} else if status == "failed" && (serverName == "playwright" || serverName == "chrome-devtools") {
let detail = {
let trimmed = (params["error"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let trimmed, !trimmed.isEmpty {
return trimmed
}
return "browser tools failed to start."
}()
appendDebugEvent("\(serverName) tools failed: \(detail)")
}

if pendingPrompt != nil, activeThreadID != nil, !isAwaitingTurnCompletion {
attemptPendingTurnStart()
}
}

Expand Down Expand Up @@ -1992,6 +2111,8 @@ final class CodexAppServerActionProvider: ActionProvider {
loginRequestTimeoutTask = nil
startupTimeoutTask?.cancel()
startupTimeoutTask = nil
pendingTurnStartRetryTask?.cancel()
pendingTurnStartRetryTask = nil
stdoutHandle?.readabilityHandler = nil
stderrHandle?.readabilityHandler = nil
try? stdinHandle?.close()
Expand Down Expand Up @@ -2024,6 +2145,7 @@ final class CodexAppServerActionProvider: ActionProvider {
pendingLogoutRequestID = nil
lastLoginURL = nil
lastLoginID = nil
mcpStartupStates = Self.makeInitialMcpStartupStates()
authState = .unknown
lastEmittedProgress = nil
notifyStateChanged()
Expand Down
8 changes: 8 additions & 0 deletions Orbit/CodexRuntimeNode.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>
48 changes: 25 additions & 23 deletions Orbit/OrbitPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -999,35 +999,37 @@ struct OrbitPanelView: View {
aboutLink(icon: "cup.and.saucer", title: "Buy Me a Coffee", url: "https://buymeacoffee.com/4xiom")
}

Divider()

VStack(alignment: .leading, spacing: 8) {
Text("Codex Debug")
.font(.system(size: 10, weight: .semibold, design: .rounded))
.foregroundColor(.secondary)
if AppBundleConfiguration.showsCodexDebugInfo {
Divider()

if let activeTurnSummary = orbitManager.codexActiveTurnSummary, !activeTurnSummary.isEmpty {
Text(activeTurnSummary)
.font(.system(size: 10, weight: .medium, design: .monospaced))
VStack(alignment: .leading, spacing: 8) {
Text("Codex Debug")
.font(.system(size: 10, weight: .semibold, design: .rounded))
.foregroundColor(.secondary)
}

if !orbitManager.codexCollaborationModes.isEmpty {
Text("modes: \(orbitManager.codexCollaborationModes.joined(separator: ", "))")
.font(.system(size: 10, weight: .medium))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
if let activeTurnSummary = orbitManager.codexActiveTurnSummary, !activeTurnSummary.isEmpty {
Text(activeTurnSummary)
.font(.system(size: 10, weight: .medium, design: .monospaced))
.foregroundColor(.secondary)
}

if !orbitManager.codexDebugEvents.isEmpty {
Text(orbitManager.codexDebugEvents.suffix(4).joined(separator: "\n"))
.font(.system(size: 9, weight: .medium, design: .monospaced))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
if !orbitManager.codexCollaborationModes.isEmpty {
Text("modes: \(orbitManager.codexCollaborationModes.joined(separator: ", "))")
.font(.system(size: 10, weight: .medium))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}

if !orbitManager.codexDebugEvents.isEmpty {
Text(orbitManager.codexDebugEvents.suffix(4).joined(separator: "\n"))
.font(.system(size: 9, weight: .medium, design: .monospaced))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}

Divider()
Divider()
}

Text("Open source under MIT license")
.font(.system(size: 10, weight: .medium))
Expand Down
12 changes: 11 additions & 1 deletion scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,24 @@ xcodebuild -exportArchive \
-exportOptionsPlist "${EXPORT_OPTIONS}"

EXPORT_APP_PATH="${EXPORT_DIR}/${APP_NAME}.app"
NODE_ENTITLEMENTS_PATH="${PROJECT_DIR}/Orbit/CodexRuntimeNode.entitlements"
if [[ -f "${EXPORT_APP_PATH}/Contents/Resources/LocalSecrets.plist" ]]; then
echo "🧼 Removing bundled LocalSecrets from release app..."
rm -f "${EXPORT_APP_PATH}/Contents/Resources/LocalSecrets.plist"
fi

echo "🔏 Re-signing bundled runtime executables..."
while IFS= read -r executable_path; do
codesign --force --sign "${DEVELOPER_ID_IDENTITY}" --options runtime --timestamp "${executable_path}"
if [[ "$(basename "${executable_path}")" == "node" ]]; then
codesign --force \
--sign "${DEVELOPER_ID_IDENTITY}" \
--options runtime \
--timestamp \
--entitlements "${NODE_ENTITLEMENTS_PATH}" \
"${executable_path}"
else
codesign --force --sign "${DEVELOPER_ID_IDENTITY}" --options runtime --timestamp "${executable_path}"
fi
done < <(find "${EXPORT_APP_PATH}/Contents/Resources/CodexRuntime" -type f -perm -111 | sort)

codesign --force \
Expand Down
Loading