Skip to content

Commit 697f4ba

Browse files
committed
Add cmux omo command for OpenCode + oh-my-openagent integration
Same pattern as `cmux claude-teams`: creates a tmux shim so oh-my-openagent's TmuxSessionManager spawns agents as native cmux splits instead of tmux panes. Sets TMUX/TMUX_PANE env vars, prepends shim to PATH, and execs into opencode. Closes #2085
1 parent 7ffa447 commit 697f4ba

2 files changed

Lines changed: 177 additions & 0 deletions

File tree

CLI/cmux.swift

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,7 @@ struct CMUXCLI {
14131413
// so help text is available even when cmux is not running.
14141414
if command != "__tmux-compat",
14151415
command != "claude-teams",
1416+
command != "omo",
14161417
(commandArgs.contains("--help") || commandArgs.contains("-h")) {
14171418
if dispatchSubcommandHelp(command: command, commandArgs: commandArgs) {
14181419
return
@@ -1463,6 +1464,15 @@ struct CMUXCLI {
14631464
return
14641465
}
14651466

1467+
if command == "omo" {
1468+
try runOMO(
1469+
commandArgs: commandArgs,
1470+
socketPath: resolvedSocketPath,
1471+
explicitPassword: socketPasswordArg
1472+
)
1473+
return
1474+
}
1475+
14661476
let client = SocketClient(path: resolvedSocketPath)
14671477
if resolvedSocketPath != socketPath {
14681478
cliTelemetry.breadcrumb(
@@ -6073,6 +6083,29 @@ struct CMUXCLI {
60736083
cmux claude-teams --continue
60746084
cmux claude-teams --model sonnet
60756085
""")
6086+
case "omo":
6087+
return String(localized: "cli.omo.usage", defaultValue: """
6088+
Usage: cmux omo [opencode-args...]
6089+
6090+
Launch OpenCode with oh-my-openagent in a cmux-aware environment.
6091+
6092+
oh-my-openagent orchestrates multiple AI models as specialized agents in
6093+
parallel. This command sets up a tmux shim so agent panes become native
6094+
cmux splits with sidebar metadata and notifications.
6095+
6096+
This command:
6097+
- sets a tmux-like environment so oh-my-openagent uses cmux splits
6098+
- prepends a private tmux shim to PATH
6099+
- forwards all remaining arguments to opencode
6100+
6101+
The tmux shim translates tmux window/pane commands into cmux workspace
6102+
and split operations in the current cmux session.
6103+
6104+
Examples:
6105+
cmux omo
6106+
cmux omo --continue
6107+
cmux omo --model claude-sonnet-4-6
6108+
""")
60766109
case "identify":
60776110
return """
60786111
Usage: cmux identify [--workspace <id|ref|index>] [--surface <id|ref|index>] [--no-caller]
@@ -9380,6 +9413,132 @@ struct CMUXCLI {
93809413
throw CLIError(message: "Failed to launch claude: \(String(cString: strerror(code)))")
93819414
}
93829415

9416+
// MARK: - cmux omo (OpenCode + oh-my-openagent)
9417+
9418+
private func resolveOpenCodeExecutable(searchPath: String?) -> String? {
9419+
let entries = searchPath?.split(separator: ":").map(String.init) ?? []
9420+
for entry in entries where !entry.isEmpty {
9421+
let candidate = URL(fileURLWithPath: entry, isDirectory: true)
9422+
.appendingPathComponent("opencode", isDirectory: false)
9423+
.path
9424+
guard FileManager.default.isExecutableFile(atPath: candidate) else { continue }
9425+
return candidate
9426+
}
9427+
return nil
9428+
}
9429+
9430+
private func createOMOShimDirectory() throws -> URL {
9431+
let homePath = ProcessInfo.processInfo.environment["HOME"] ?? NSHomeDirectory()
9432+
let root = URL(fileURLWithPath: homePath, isDirectory: true)
9433+
.appendingPathComponent(".cmuxterm", isDirectory: true)
9434+
.appendingPathComponent("omo-bin", isDirectory: true)
9435+
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true, attributes: nil)
9436+
let tmuxURL = root.appendingPathComponent("tmux", isDirectory: false)
9437+
let script = """
9438+
#!/usr/bin/env bash
9439+
set -euo pipefail
9440+
exec "${CMUX_OMO_CMUX_BIN:-cmux}" __tmux-compat "$@"
9441+
"""
9442+
let normalizedScript = script.trimmingCharacters(in: .whitespacesAndNewlines)
9443+
let existingScript = try? String(contentsOf: tmuxURL, encoding: .utf8)
9444+
if existingScript?.trimmingCharacters(in: .whitespacesAndNewlines) != normalizedScript {
9445+
try script.write(to: tmuxURL, atomically: false, encoding: .utf8)
9446+
}
9447+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: tmuxURL.path)
9448+
return root
9449+
}
9450+
9451+
private func configureOMOEnvironment(
9452+
processEnvironment: [String: String],
9453+
shimDirectory: URL,
9454+
executablePath: String,
9455+
socketPath: String,
9456+
explicitPassword: String?,
9457+
focusedContext: ClaudeTeamsFocusedContext?
9458+
) {
9459+
let updatedPath = prependPathEntries(
9460+
[shimDirectory.path],
9461+
to: processEnvironment["PATH"]
9462+
)
9463+
let fakeTmuxValue: String = {
9464+
if let focusedContext {
9465+
let windowToken = focusedContext.windowId ?? focusedContext.workspaceId
9466+
return "/tmp/cmux-omo/\(focusedContext.workspaceId),\(windowToken),\(focusedContext.paneHandle)"
9467+
}
9468+
return processEnvironment["TMUX"] ?? "/tmp/cmux-omo/default,0,0"
9469+
}()
9470+
let fakeTmuxPane = focusedContext.map { "%\($0.paneHandle)" }
9471+
?? processEnvironment["TMUX_PANE"]
9472+
?? "%1"
9473+
let fakeTerm = processEnvironment["CMUX_OMO_TERM"] ?? "screen-256color"
9474+
9475+
setenv("CMUX_OMO_CMUX_BIN", executablePath, 1)
9476+
setenv("PATH", updatedPath, 1)
9477+
setenv("TMUX", fakeTmuxValue, 1)
9478+
setenv("TMUX_PANE", fakeTmuxPane, 1)
9479+
setenv("TERM", fakeTerm, 1)
9480+
setenv("CMUX_SOCKET_PATH", socketPath, 1)
9481+
setenv("CMUX_SOCKET", socketPath, 1)
9482+
if let explicitPassword,
9483+
!explicitPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
9484+
setenv("CMUX_SOCKET_PASSWORD", explicitPassword, 1)
9485+
}
9486+
unsetenv("TERM_PROGRAM")
9487+
if let focusedContext {
9488+
setenv("CMUX_WORKSPACE_ID", focusedContext.workspaceId, 1)
9489+
if let surfaceId = focusedContext.surfaceId, !surfaceId.isEmpty {
9490+
setenv("CMUX_SURFACE_ID", surfaceId, 1)
9491+
}
9492+
}
9493+
}
9494+
9495+
private func runOMO(
9496+
commandArgs: [String],
9497+
socketPath: String,
9498+
explicitPassword: String?
9499+
) throws {
9500+
let processEnvironment = ProcessInfo.processInfo.environment
9501+
var launcherEnvironment = processEnvironment
9502+
launcherEnvironment["CMUX_SOCKET_PATH"] = socketPath
9503+
launcherEnvironment["CMUX_SOCKET"] = socketPath
9504+
if let explicitPassword,
9505+
!explicitPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
9506+
launcherEnvironment["CMUX_SOCKET_PASSWORD"] = explicitPassword
9507+
}
9508+
let shimDirectory = try createOMOShimDirectory()
9509+
let executablePath = resolvedExecutableURL()?.path ?? (args.first ?? "cmux")
9510+
let focusedContext = claudeTeamsFocusedContext(
9511+
processEnvironment: launcherEnvironment,
9512+
explicitPassword: explicitPassword
9513+
)
9514+
let openCodeExecutablePath = resolveOpenCodeExecutable(searchPath: launcherEnvironment["PATH"])
9515+
configureOMOEnvironment(
9516+
processEnvironment: launcherEnvironment,
9517+
shimDirectory: shimDirectory,
9518+
executablePath: executablePath,
9519+
socketPath: socketPath,
9520+
explicitPassword: explicitPassword,
9521+
focusedContext: focusedContext
9522+
)
9523+
9524+
let launchPath = openCodeExecutablePath ?? "opencode"
9525+
var argv = ([launchPath] + commandArgs).map { strdup($0) }
9526+
defer {
9527+
for item in argv {
9528+
free(item)
9529+
}
9530+
}
9531+
argv.append(nil)
9532+
9533+
if openCodeExecutablePath != nil {
9534+
execv(launchPath, &argv)
9535+
} else {
9536+
execvp("opencode", &argv)
9537+
}
9538+
let code = errno
9539+
throw CLIError(message: "Failed to launch opencode: \(String(cString: strerror(code)))")
9540+
}
9541+
93839542
private func runClaudeTeamsTmuxCompat(
93849543
commandArgs: [String],
93859544
client: SocketClient,
@@ -11361,6 +11520,7 @@ struct CMUXCLI {
1136111520
feedback [--email <email> --body <text> [--image <path> ...]]
1136211521
themes [list|set|clear]
1136311522
claude-teams [claude-args...]
11523+
omo [opencode-args...]
1136411524
ping
1136511525
version
1136611526
capabilities

Resources/Localizable.xcstrings

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,23 @@
132132
}
133133
}
134134
},
135+
"cli.omo.usage": {
136+
"extractionState": "manual",
137+
"localizations": {
138+
"en": {
139+
"stringUnit": {
140+
"state": "translated",
141+
"value": "Usage: cmux omo [opencode-args...]\n\nLaunch OpenCode with oh-my-openagent in a cmux-aware environment.\n\noh-my-openagent orchestrates multiple AI models as specialized agents in\nparallel. This command sets up a tmux shim so agent panes become native\ncmux splits with sidebar metadata and notifications.\n\nThis command:\n - sets a tmux-like environment so oh-my-openagent uses cmux splits\n - prepends a private tmux shim to PATH\n - forwards all remaining arguments to opencode\n\nThe tmux shim translates tmux window/pane commands into cmux workspace\nand split operations in the current cmux session.\n\nExamples:\n cmux omo\n cmux omo --continue\n cmux omo --model claude-sonnet-4-6"
142+
}
143+
},
144+
"ja": {
145+
"stringUnit": {
146+
"state": "translated",
147+
"value": "使い方: cmux omo [opencode-args...]\n\ncmux 対応の環境で OpenCode と oh-my-openagent を起動します。\n\noh-my-openagent は複数の AI モデルを専門エージェントとして並列に\nオーケストレーションします。このコマンドは tmux shim を設定し、\nエージェントのペインをネイティブの cmux split に変換します。\n\nこのコマンドは次を行います:\n - oh-my-openagent が cmux の split を使うよう tmux 風の環境を設定\n - 専用の tmux shim を PATH の先頭に追加\n - 残りの引数をそのまま opencode に渡す\n\ntmux shim は、tmux の window/pane コマンドを、現在の cmux セッション内の\nworkspace と split 操作に変換します。\n\n例:\n cmux omo\n cmux omo --continue\n cmux omo --model claude-sonnet-4-6"
148+
}
149+
}
150+
}
151+
},
135152
"applescript.error.disabled": {
136153
"extractionState": "manual",
137154
"localizations": {

0 commit comments

Comments
 (0)