Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b2c2f76
test: capture omo model override regression
austinywang May 12, 2026
07b24ed
fix: apply omo model override to plugin config
austinywang May 12, 2026
6f3039e
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 12, 2026
1405a3c
test: capture omo model override reset
austinywang May 12, 2026
3803784
fix: refresh omo shadow config per invocation
austinywang May 12, 2026
849a7f7
fix: write omo relay config atomically
austinywang May 12, 2026
7cb1304
ci: retrigger external checks
austinywang May 12, 2026
716c3e8
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 12, 2026
bd132ba
fix: honor omo model terminator
austinywang May 12, 2026
4e90051
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 12, 2026
6fe0cdb
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 12, 2026
4c935ca
docs: document omo model override contract
austinywang May 12, 2026
9ace766
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 13, 2026
b3a23eb
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 14, 2026
bf38bba
fix: reject invalid omo user config
austinywang May 14, 2026
dbcbafa
test: keep invalid omo config assertion precise
austinywang May 14, 2026
e7b2d88
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 18, 2026
36deafa
fix: avoid leaking omo config paths
austinywang May 18, 2026
ce844fb
test: require invalid omo config to leave no shadow json
austinywang May 18, 2026
8a8ca7f
fix: avoid shadowing invalid omo config
austinywang May 18, 2026
9ae5491
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 18, 2026
9dbcec1
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 18, 2026
2298a4b
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang May 19, 2026
52e9c8d
Merge remote-tracking branch 'origin/main' into issue-3682-omo-change…
austinywang May 19, 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
157 changes: 127 additions & 30 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14723,6 +14723,32 @@ struct CMUXCLI {

private static let omoPluginName = "oh-my-opencode"
private static let openCodeSessionPluginConfigSpec = "./plugins/cmux-session.js"
private static let omoModelOverrideAgentKeys = [
"build",
"plan",
"sisyphus",
"hephaestus",
"sisyphus-junior",
"OpenCode-Builder",
"prometheus",
"metis",
"momus",
"oracle",
"librarian",
"explore",
"multimodal-looker",
"atlas",
]
private static let omoModelOverrideCategoryKeys = [
"quick",
"deep",
"ultrabrain",
"unspecified-low",
"unspecified-high",
"visual-engineering",
"artistry",
"writing",
]

private func resolveExecutableInPath(_ name: String) -> String? {
let entries = ProcessInfo.processInfo.environment["PATH"]?.split(separator: ":").map(String.init) ?? []
Expand Down Expand Up @@ -14838,6 +14864,66 @@ struct CMUXCLI {
return nil
}

private func omoRequestedModel(from commandArgs: [String]) -> String? {
var requestedModel: String?
var index = commandArgs.startIndex
while index < commandArgs.endIndex {
let arg = commandArgs[index]
if arg == "--" { break }

if arg == "--model" || arg == "-m" {
let nextIndex = commandArgs.index(after: index)
guard nextIndex < commandArgs.endIndex else { return requestedModel }
if commandArgs[nextIndex] == "--" {
break
}
let value = commandArgs[nextIndex].trimmingCharacters(in: .whitespacesAndNewlines)
if !value.isEmpty {
requestedModel = value
}
index = commandArgs.index(after: nextIndex)
continue
Comment thread
lawrencecchen marked this conversation as resolved.
Resolved
}

if arg.hasPrefix("--model=") {
let value = String(arg.dropFirst("--model=".count)).trimmingCharacters(in: .whitespacesAndNewlines)
if !value.isEmpty {
requestedModel = value
}
} else if arg.hasPrefix("-m=") {
let value = String(arg.dropFirst("-m=".count)).trimmingCharacters(in: .whitespacesAndNewlines)
if !value.isEmpty {
requestedModel = value
}
}

index = commandArgs.index(after: index)
}

return requestedModel
}

private func omoApplyRequestedModelOverride(_ requestedModel: String, to omoConfig: inout [String: Any]) {
let model = requestedModel.trimmingCharacters(in: .whitespacesAndNewlines)
guard !model.isEmpty else { return }

var agents = (omoConfig["agents"] as? [String: Any]) ?? [:]
for agent in Self.omoModelOverrideAgentKeys {
var agentConfig = (agents[agent] as? [String: Any]) ?? [:]
agentConfig["model"] = model
agents[agent] = agentConfig
}
omoConfig["agents"] = agents

var categories = (omoConfig["categories"] as? [String: Any]) ?? [:]
for category in Self.omoModelOverrideCategoryKeys {
var categoryConfig = (categories[category] as? [String: Any]) ?? [:]
categoryConfig["model"] = model
categories[category] = categoryConfig
}
omoConfig["categories"] = categories
}

private func omoBindableLoopbackPort(_ port: UInt16) -> UInt16? {
let socketDescriptor = socket(AF_INET, SOCK_STREAM, 0)
guard socketDescriptor >= 0 else { return nil }
Expand Down Expand Up @@ -14911,7 +14997,7 @@ struct CMUXCLI {
/// Creates a shadow config directory that layers oh-my-opencode on top of the user's
/// existing opencode config without modifying the original. Sets OPENCODE_CONFIG_DIR
/// to point at the shadow directory.
private func omoEnsurePlugin() throws {
private func omoEnsurePlugin(requestedModel: String?) throws {
let userDir = omoUserConfigDir()
let shadowDir = omoShadowConfigDir()
let fm = FileManager.default
Expand All @@ -14923,9 +15009,15 @@ struct CMUXCLI {
let shadowJsonURL = shadowDir.appendingPathComponent("opencode.json")

var config: [String: Any]
if let data = try? Data(contentsOf: userJsonURL) {
if fm.fileExists(atPath: userJsonURL.path) {
let data: Data
do {
data = try Data(contentsOf: userJsonURL)
} catch {
throw CLIError(message: "Failed to read \(userJsonURL.lastPathComponent). Check file permissions and retry.")
}
guard let existing = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw CLIError(message: "Failed to parse \(userJsonURL.path). Fix the JSON syntax and retry.")
throw CLIError(message: "Failed to parse \(userJsonURL.lastPathComponent). Fix the JSON syntax and retry.")
}
config = existing
} else {
Expand Down Expand Up @@ -15012,53 +15104,57 @@ struct CMUXCLI {
// Without this, the TmuxSessionManager won't spawn visual panes even though
// $TMUX is set (tmux.enabled defaults to false).
let omoConfigURL = shadowDir.appendingPathComponent("oh-my-opencode.json")
let userOmoConfig = userDir.appendingPathComponent("oh-my-opencode.json")
// Rebuild the shadow config from the user's source config on every run so
// a previous --model overlay cannot persist into a later bare `cmux omo`.
var omoConfig: [String: Any]
if let data = try? Data(contentsOf: omoConfigURL),
let existing = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
omoConfig = existing
} else {
// Check if user has a config we symlinked, read from source
let userOmoConfig = userDir.appendingPathComponent("oh-my-opencode.json")
if let data = try? Data(contentsOf: userOmoConfig),
let existing = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
if fm.fileExists(atPath: userOmoConfig.path) {
let data: Data
do {
data = try Data(contentsOf: userOmoConfig)
} catch {
throw CLIError(message: "Failed to read \(userOmoConfig.lastPathComponent). Check file permissions and retry.")
}
do {
guard let existing = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw CLIError(message: "\(userOmoConfig.lastPathComponent) must contain a JSON object.")
}
omoConfig = existing
// Remove the symlink so we can write our own copy
try? fm.removeItem(at: omoConfigURL)
} else {
omoConfig = [:]
} catch let error as CLIError {
throw error
} catch {
throw CLIError(message: "Failed to parse \(userOmoConfig.lastPathComponent). Fix the JSON syntax and retry.")
}
} else {
omoConfig = [:]
}
var tmuxConfig = (omoConfig["tmux"] as? [String: Any]) ?? [:]
var needsWrite = false
if tmuxConfig["enabled"] as? Bool != true {
tmuxConfig["enabled"] = true
needsWrite = true
}
// Lower the default min widths so agent panes spawn in normal-sized windows.
// oh-my-openagent defaults: main_pane_min_width=120, agent_pane_min_width=40,
// requiring 161+ columns. Most terminal windows are narrower.
if tmuxConfig["main_pane_min_width"] == nil {
tmuxConfig["main_pane_min_width"] = 60
needsWrite = true
}
if tmuxConfig["agent_pane_min_width"] == nil {
tmuxConfig["agent_pane_min_width"] = 30
needsWrite = true
}
if tmuxConfig["main_pane_size"] == nil {
tmuxConfig["main_pane_size"] = 50
needsWrite = true
}
if needsWrite {
omoConfig["tmux"] = tmuxConfig
// Remove symlink if it exists (we need a real file)
if let attrs = try? fm.attributesOfItem(atPath: omoConfigURL.path),
attrs[.type] as? FileAttributeType == .typeSymbolicLink {
try? fm.removeItem(at: omoConfigURL)
}
let output = try JSONSerialization.data(withJSONObject: omoConfig, options: [.prettyPrinted, .sortedKeys])
try output.write(to: omoConfigURL, options: .atomic)
if let requestedModel {
omoApplyRequestedModelOverride(requestedModel, to: &omoConfig)
}
omoConfig["tmux"] = tmuxConfig
// Remove symlink if it exists (we need a real file)
if let attrs = try? fm.attributesOfItem(atPath: omoConfigURL.path),
attrs[.type] as? FileAttributeType == .typeSymbolicLink {
try? fm.removeItem(at: omoConfigURL)
}
let configOutput = try JSONSerialization.data(withJSONObject: omoConfig, options: [.prettyPrinted, .sortedKeys])
try configOutput.write(to: omoConfigURL, options: .atomic)

// Point OpenCode at the shadow config
setenv("OPENCODE_CONFIG_DIR", shadowDir.path, 1)
Expand Down Expand Up @@ -15119,7 +15215,8 @@ struct CMUXCLI {
}

// Ensure oh-my-opencode plugin is registered and installed
try omoEnsurePlugin()
let requestedModel = omoRequestedModel(from: commandArgs)
try omoEnsurePlugin(requestedModel: requestedModel)

let shimDirectory = try createOMOShimDirectory()
let executablePath = resolvedExecutableURL()?.path ?? (args.first ?? "cmux")
Expand Down
Loading
Loading