Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ebb401e
feat: support multiple Codex homes in Vault
austinywang May 13, 2026
a3afee4
fix: use unified logging for Codex home warnings
austinywang May 13, 2026
50526af
test: cover empty configured Codex homes
austinywang May 13, 2026
e5cc452
fix: treat empty configured Codex homes as empty
austinywang May 13, 2026
1f76640
test: normalize Codex home resume expectation
austinywang May 13, 2026
40413ce
test: align detached transfer helper with resume state
austinywang May 13, 2026
d93e9dd
Merge remote-tracking branch 'origin/main' into issue-4049-multiple-c…
austinywang May 14, 2026
e52960d
refactor: clarify Codex sessions root matching
austinywang May 14, 2026
5eee2ce
fix: keep Codex SQL details out of Vault errors
austinywang May 15, 2026
94e54ed
Merge remote-tracking branch 'origin/main' into issue-4049-multiple-c…
austinywang May 18, 2026
d9b3e51
Merge remote-tracking branch 'origin/main' into issue-4049-multiple-c…
austinywang May 18, 2026
6bb26e2
Merge remote-tracking branch 'origin/main' into issue-4049-multiple-c…
austinywang May 18, 2026
3e706e5
fix: clarify Codex home labels and warnings
austinywang May 18, 2026
cb9acf2
docs: clarify Codex home discovery behavior
austinywang May 18, 2026
681c528
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang May 19, 2026
c998212
Persist normalized Codex home paths
austinywang May 19, 2026
f5ae39e
Merge remote-tracking branch 'origin/main' into issue-4049-multiple-c…
austinywang May 19, 2026
53d2890
refactor: observe Codex home changes with async sequence
austinywang May 19, 2026
0cea502
fix: add Codex home localization fallbacks
austinywang May 19, 2026
81975cb
Merge remote-tracking branch 'origin/main' into issue-4049-multiple-c…
austinywang May 19, 2026
df64aa0
merge: resolve conflicts with main
austinywang May 19, 2026
be5a8f2
fix: replace vault sleep timers
austinywang May 19, 2026
6b71489
fix: return consumed vault drag entry
austinywang May 19, 2026
31a95e8
fix: avoid codex home notification warning
austinywang May 19, 2026
d945ec2
merge: sync issue-4049 with main
austinywang May 22, 2026
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
476 changes: 476 additions & 0 deletions Resources/Localizable.xcstrings

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Sources/CmuxSettingsJSONPathSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ extension CmuxSettingsFileStore {
"terminal.showScrollBar",
"terminal.autoResumeAgentSessions",
"terminal.resumeCommands",
"codex.additionalHomes",
"notifications.dockBadge",
"notifications.showInMenuBar",
"notifications.unreadPaneRing",
Expand Down
10 changes: 10 additions & 0 deletions Sources/KeyboardShortcutSettingsFileStore+Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ extension CmuxSettingsFileStore {
"resumeCommands": [],
],
],
[
"codex": [
"additionalHomes": [
[
"path": "~/.codex-work",
"displayName": "Work Codex",
],
],
],
],
Comment thread
austinywang marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
[
"notifications": [
"dockBadge": NotificationBadgeSettings.defaultDockBadgeEnabled,
Expand Down
69 changes: 69 additions & 0 deletions Sources/KeyboardShortcutSettingsFileStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ final class CmuxSettingsFileStore {
if let terminalSection = root["terminal"] as? [String: Any] {
parseTerminalSection(terminalSection, sourcePath: sourcePath, snapshot: &snapshot)
}
if let codexSection = root["codex"] as? [String: Any] {
parseCodexSection(codexSection, sourcePath: sourcePath, snapshot: &snapshot)
}
if let notificationsSection = root["notifications"] as? [String: Any] {
parseNotificationsSection(notificationsSection, sourcePath: sourcePath, snapshot: &snapshot)
}
Expand Down Expand Up @@ -486,6 +489,20 @@ final class CmuxSettingsFileStore {
}
}

private func parseCodexSection(
_ section: [String: Any],
sourcePath: String,
snapshot: inout ResolvedSettingsSnapshot
) {
guard section.keys.contains("additionalHomes") else { return }
guard let homes = parseCodexAdditionalHomes(section["additionalHomes"], sourcePath: sourcePath) else {
return
}
snapshot.managedUserDefaults[CodexSessionHomeSettings.additionalHomesKey] = .string(
CodexSessionHomeSettings.encodedAdditionalHomes(homes)
)
}

private func parseSidebarSection(
_ section: [String: Any],
sourcePath: String,
Expand Down Expand Up @@ -1272,6 +1289,7 @@ final class CmuxSettingsFileStore {
let changes = sideEffects.changes
let apply = {
var agentSessionAutoResumeDidChange = false
var codexSessionHomesDidChange = false
for change in changes {
if change.defaultsKey == TerminalScrollBarSettings.showScrollBarKey {
TerminalScrollBarSettings.notifyDidChange(notificationCenter: notificationCenter)
Expand All @@ -1281,6 +1299,10 @@ final class CmuxSettingsFileStore {
agentSessionAutoResumeDidChange = true
}

if change.defaultsKey == CodexSessionHomeSettings.additionalHomesKey {
codexSessionHomesDidChange = true
}

if change.defaultsKey == LanguageSettings.languageKey {
let rawValue = UserDefaults.standard.string(forKey: change.defaultsKey) ?? ""
LanguageSettings.apply(AppLanguage(rawValue: rawValue) ?? .system)
Expand All @@ -1292,6 +1314,9 @@ final class CmuxSettingsFileStore {
if agentSessionAutoResumeDidChange {
AgentSessionAutoResumeSettings.notifyDidChange(notificationCenter: notificationCenter)
}
if codexSessionHomesDidChange {
CodexSessionHomeSettings.notifyDidChange(notificationCenter: notificationCenter)
}
}
if Thread.isMainThread {
apply()
Expand Down Expand Up @@ -1388,6 +1413,50 @@ final class CmuxSettingsFileStore {
return strings
}

private func parseCodexAdditionalHomes(
_ rawValue: Any?,
sourcePath: String
) -> [CodexSessionHomeSetting]? {
guard let values = rawValue as? [Any] else {
logInvalid("codex.additionalHomes", sourcePath: sourcePath)
return nil
}

var homes: [CodexSessionHomeSetting] = []
var seen: Set<String> = []
for (index, value) in values.enumerated() {
let parsed: CodexSessionHomeSetting?
if let path = jsonString(value) {
parsed = CodexSessionHomeSetting(path: path)
} else if let object = value as? [String: Any],
let path = jsonString(object["path"]) {
let displayName = jsonString(object["displayName"]) ?? jsonString(object["name"])
parsed = CodexSessionHomeSetting(path: path, displayName: displayName)
} else {
logInvalid("codex.additionalHomes[\(index)]", sourcePath: sourcePath)
continue
}

guard let parsed else { continue }
let trimmedPath = parsed.path.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedPath.isEmpty else {
logInvalid("codex.additionalHomes[\(index)].path", sourcePath: sourcePath)
continue
}
let normalized = ((trimmedPath as NSString).expandingTildeInPath as NSString).standardizingPath
guard seen.insert(normalized).inserted else { continue }
let cleanedDisplayName = parsed.displayName?
.trimmingCharacters(in: .whitespacesAndNewlines)
homes.append(
CodexSessionHomeSetting(
path: normalized,
displayName: (cleanedDisplayName?.isEmpty == false) ? cleanedDisplayName : nil
)
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return homes
}

}

typealias KeyboardShortcutSettingsFileStore = CmuxSettingsFileStore
Expand Down
124 changes: 120 additions & 4 deletions Sources/SessionIndexModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,85 @@ enum OpenCodeDatabaseSnapshot {
}
}

// MARK: - Codex homes

struct CodexSessionHomeSetting: Codable, Equatable, Sendable {
let path: String
let displayName: String?

init(path: String, displayName: String? = nil) {
self.path = path
self.displayName = Self.normalizedOptional(displayName)
}

private static func normalizedOptional(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed?.isEmpty == false ? trimmed : nil
}
}

enum CodexSessionHomeSettings {
static let additionalHomesKey = "codexSessionAdditionalHomes"
static let didChangeNotification = Notification.Name("CodexSessionHomeSettings.didChange")

static func additionalHomes(defaults: UserDefaults = .standard) -> [CodexSessionHomeSetting] {
guard let raw = defaults.string(forKey: additionalHomesKey),
let data = raw.data(using: .utf8),
let decoded = try? JSONDecoder().decode([CodexSessionHomeSetting].self, from: data) else {
return []
}
return decoded
}

static func encodedAdditionalHomes(_ homes: [CodexSessionHomeSetting]) -> String {
guard let data = try? JSONEncoder().encode(homes),
let raw = String(data: data, encoding: .utf8) else {
return "[]"
}
return raw
}

static func notifyDidChange(notificationCenter: NotificationCenter = .default) {
notificationCenter.post(name: didChangeNotification, object: nil)
}
}

struct CodexSessionHome: Hashable, Sendable {
let path: String
let label: String
let isDefault: Bool

var stateDatabasePath: String {
(path as NSString).appendingPathComponent("state_5.sqlite")
}

var sessionsRoot: String {
(path as NSString).appendingPathComponent("sessions")
}

var resumeCodexHome: String? {
isDefault ? nil : path
}

static func defaultHome() -> CodexSessionHome {
let path = ("~/.codex" as NSString).expandingTildeInPath
return CodexSessionHome(
path: (path as NSString).standardizingPath,
label: String(localized: "sessionIndex.codexHome.defaultLabel", defaultValue: "Default Codex"),
isDefault: true
)
}

static func additionalHome(from setting: CodexSessionHomeSetting) -> CodexSessionHome? {
let trimmed = setting.path.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let expanded = (trimmed as NSString).expandingTildeInPath
let standardized = (expanded as NSString).standardizingPath
let label = setting.displayName ?? (standardized as NSString).abbreviatingWithTildeInPath
return CodexSessionHome(path: standardized, label: label, isDefault: false)
}
}

// MARK: - Session entry

struct PullRequestLink: Hashable {
Expand All @@ -197,7 +276,7 @@ struct PullRequestLink: Hashable {
/// Agent-specific fields used to build the resume command with appropriate flags.
enum AgentSpecifics: Hashable {
case claude(model: String?, permissionMode: String?, configDirectoryForResume: String?)
case codex(model: String?, approvalPolicy: String?, sandboxMode: String?, effort: String?)
case codex(model: String?, approvalPolicy: String?, sandboxMode: String?, effort: String?, codexHome: String?)
case grok(model: String?, permissionMode: String?, sandboxMode: String?, grokHome: String?)
case opencode(providerModel: String?, agentName: String?)
case rovodev
Expand Down Expand Up @@ -264,6 +343,33 @@ struct SessionEntry: Identifiable, Hashable {
let modified: Date
let fileURL: URL?
let specifics: AgentSpecifics
let sourceLabel: String?

init(
id: String,
agent: SessionAgent,
sessionId: String,
title: String,
cwd: String?,
gitBranch: String?,
pullRequest: PullRequestLink?,
modified: Date,
fileURL: URL?,
specifics: AgentSpecifics,
sourceLabel: String? = nil
) {
self.id = id
self.agent = agent
self.sessionId = sessionId
self.title = title
self.cwd = cwd
self.gitBranch = gitBranch
self.pullRequest = pullRequest
self.modified = modified
self.fileURL = fileURL
self.specifics = specifics
self.sourceLabel = sourceLabel
}

var resumeWorkingDirectory: String? {
guard let cwd, !cwd.isEmpty else { return nil }
Expand Down Expand Up @@ -293,7 +399,8 @@ struct SessionEntry: Identifiable, Hashable {
model: model,
permissionMode: permissionMode,
configDirectoryForResume: configDirectory
)
),
sourceLabel: sourceLabel
)
}

Expand Down Expand Up @@ -326,7 +433,7 @@ struct SessionEntry: Identifiable, Hashable {
["CLAUDE_CONFIG_DIR": $0, "CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV": "1", "CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV_KEYS": "CLAUDE_CONFIG_DIR"]
} ?? [:]
return Self.withShellEnvironment(environment, command: parts.joined(separator: " "))
case let .codex(model, approval, sandbox, effort):
case let .codex(model, approval, sandbox, effort, codexHome):
var parts = ["codex resume \(sessionId)"]
if let model, !model.isEmpty {
parts.append("-m \(Self.shellQuote(model))")
Expand All @@ -340,7 +447,8 @@ struct SessionEntry: Identifiable, Hashable {
if let effort, !effort.isEmpty {
parts.append("-c model_reasoning_effort=\(Self.shellQuote(effort))")
}
return parts.joined(separator: " ")
let environment = codexHome.map { ["CODEX_HOME": $0] } ?? [:]
return Self.withShellEnvironment(environment, command: parts.joined(separator: " "))
case let .grok(model, permissionMode, sandboxMode, grokHome):
var argv = ["grok", "-r", sessionId]
if let model, !model.isEmpty {
Expand Down Expand Up @@ -544,4 +652,12 @@ struct SessionEntry: Identifiable, Hashable {
guard let cwd, !cwd.isEmpty else { return nil }
return (cwd as NSString).lastPathComponent
}

var sourceAndCwdLabel: String? {
let parts = [sourceLabel, cwdLabel].compactMap { value -> String? in
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed?.isEmpty == false ? trimmed : nil
}
return parts.isEmpty ? nil : parts.joined(separator: " - ")
}
}
Loading
Loading