Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6d67c0a
agent-session SoT: add session version + authoritative pull snapshot …
azooz2003-bit Jun 23, 2026
000cbec
agent-session SoT: iOS re-pulls the session list on foreground (Slice A)
azooz2003-bit Jun 23, 2026
edc04ec
agent-session SoT: deterministic process-exit backstop (Slice B)
azooz2003-bit Jun 23, 2026
775ce2f
agent-session SoT spec: mark Slices A+B done; note ended-input-bar UI…
azooz2003-bit Jun 23, 2026
1392e7e
agent-session SoT: delete title/mtime detection heuristics (Slice D)
azooz2003-bit Jun 23, 2026
324f085
agent-session SoT spec: Slice D done; C/E/F scoped as follow-ups with…
azooz2003-bit Jun 23, 2026
6f2e0d8
agent-session SoT: move hook-store JSON reads off the main actor (Sli…
azooz2003-bit Jun 23, 2026
ef03f5c
agent-session SoT spec: Slice C done; E/F deferred with rationale
azooz2003-bit Jun 23, 2026
e6f4804
agent-session SoT spec: resolve Slice E (not needed; invariant alread…
azooz2003-bit Jun 23, 2026
5d0d79e
codex detection: plan (Slice F) — wrapper-emits-session-start, no glo…
azooz2003-bit Jun 23, 2026
4bcca3e
codex detection: cmux-codex-wrapper + PATH shim + per-invocation hook…
azooz2003-bit Jun 23, 2026
fbfc7da
codex detection: bind surface/transcript on live session-start via fe…
azooz2003-bit Jun 23, 2026
4a1a8c8
codex detection: plan status -> implemented + live-verified
azooz2003-bit Jun 23, 2026
bef654d
codex detection: fire-and-forget injected hooks so codex never blocks…
azooz2003-bit Jun 23, 2026
bd85a66
agent-session: scope mobile chat sessions by surface's current worksp…
azooz2003-bit Jun 23, 2026
e35ae95
agent-session: retain ended sessions in mobile.chat.sessions; re-pin …
azooz2003-bit Jun 23, 2026
da573c9
codex detection: route codex resume through cmux-codex-wrapper so res…
azooz2003-bit Jun 23, 2026
fd41368
codex detection: wrapper fires synthetic session-start on resume
azooz2003-bit Jun 23, 2026
383022d
agent-session: re-bind resumed sessions live from cmux's own authority
azooz2003-bit Jun 23, 2026
4ba2fd5
agent-session: harden GUI reliability (reducer clobber, deterministic…
azooz2003-bit Jun 24, 2026
dfa0365
mobile chat: accurate transcript-not-found message (home-dir case)
azooz2003-bit Jun 24, 2026
79636d0
agent-session: honor CLAUDE_CONFIG_DIR / CODEX_HOME in transcript fal…
azooz2003-bit Jun 24, 2026
7d5b8b1
agent-session: tree-aware end backstop (observe-floor liveness)
azooz2003-bit Jun 25, 2026
80955a0
settings: add Codex wrapper integration toggle (mirrors Claude)
azooz2003-bit Jun 25, 2026
972e1c9
codex hooks: emit a #!/bin/sh script-file command, not an inline snippet
azooz2003-bit Jun 25, 2026
14840ee
agent-session: observe-floor detection of untracked agents (process t…
azooz2003-bit Jun 26, 2026
f054e79
settings: document consented global Codex hook install (Layer 2)
azooz2003-bit Jun 26, 2026
19e9509
agent-session: structured agentChat.* debug trace across the pipeline
azooz2003-bit Jun 26, 2026
cd04d72
agent-chat: render Codex request_user_input pickers as tappable GUI q…
azooz2003-bit Jun 26, 2026
0286052
agent-chat: mark answered Codex pickers resolved (show selection, sto…
azooz2003-bit Jun 26, 2026
f319fbd
agent-chat: resolve Codex multi-question pickers by question id
azooz2003-bit Jun 26, 2026
8889850
codex hooks: persistent ~/.codex/hooks.json commands as #!/bin/sh scr…
azooz2003-bit Jun 26, 2026
0c8cad8
Merge remote-tracking branch 'origin/main' into feat-codex-detection
azooz2003-bit Jun 26, 2026
bdfa73b
ci: refresh swift file-length budget for agent-session growth + merge
azooz2003-bit Jun 26, 2026
328f9b6
agent-session: drop debug-socket exposure of mobile.chat.sessions (re…
azooz2003-bit Jun 26, 2026
29b4502
agent-session: fix Swift-6 captured-var-self warning in handleProcess…
azooz2003-bit Jun 26, 2026
030db3a
agent-session: host-side chat debug inspector + mobile.chat.sessions …
azooz2003-bit Jun 27, 2026
3392923
Merge remote-tracking branch 'origin/main' into feat-codex-detection
azooz2003-bit Jun 27, 2026
953c74f
agent-session: gate resumeInitiated log behind #if DEBUG; harden debu…
azooz2003-bit Jun 27, 2026
fe9df5e
agent-session: refresh swift file-length budget for #if DEBUG guard (…
azooz2003-bit Jun 27, 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
27 changes: 14 additions & 13 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# cmux-owned Swift file length budget.
# Format: max_lines<TAB>relative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
34629 CLI/cmux.swift
34643 CLI/cmux.swift
17854 Sources/AppDelegate.swift
16499 Sources/ContentView.swift
16476 Sources/ContentView.swift
13913 Sources/TerminalController.swift
12838 Sources/Workspace.swift
12886 Sources/Workspace.swift
12348 cmuxTests/AppDelegateShortcutRoutingTests.swift
12237 Sources/GhosttyTerminalView.swift
11567 Sources/Panels/BrowserPanel.swift
Expand Down Expand Up @@ -46,8 +46,8 @@
2126 cmuxTests/CmuxConfigTests.swift
2091 cmuxTests/ShortcutAndCommandPaletteTests.swift
2078 Sources/SessionPersistence.swift
1960 Sources/RestorableAgentSession.swift
1952 Sources/KeyboardShortcutSettingsFileStore.swift
1945 Sources/RestorableAgentSession.swift
1900 cmuxTests/NotificationAndMenuBarTests.swift
1866 Sources/Panels/BrowserWebAuthnSupport.swift
1810 Sources/SessionIndexStore.swift
Expand All @@ -71,8 +71,8 @@
1360 Sources/Feed/FeedButtonStyleDebugWindowController.swift
1295 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Config/GhosttyConfig.swift
1295 cmuxTests/MobileHostAuthorizationTests.swift
1287 cmuxTests/RestorableAgentSessionIndexTests.swift
1285 cmuxUITests/SidebarHelpMenuUITests.swift
1277 cmuxTests/RestorableAgentSessionIndexTests.swift
1258 Sources/Feed/FeedCoordinator.swift
1209 Packages/macOS/CmuxCommandPalette/Tests/CmuxCommandPaletteTests/CommandPaletteSearchEngineTests.swift
1204 cmuxTests/FileExplorerStoreTests.swift
Expand All @@ -98,12 +98,12 @@
928 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
926 Sources/DockPanelView.swift
920 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
914 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
905 Sources/CmuxSSHURLRequest.swift
899 Sources/Panels/MarkdownWebRenderer.swift
885 cmuxTests/SidebarWorkspaceDropPlannerTests.swift
885 Sources/Panels/TerminalPanel.swift
885 cmuxTests/SidebarWorkspaceDropPlannerTests.swift
877 Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/ChatConversationStoreTests.swift
877 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
871 cmuxTests/ClaudeHookSurfaceResolutionSwiftTests.swift
868 Sources/Panels/BrowserScreenshotSnapshotter.swift
859 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Workspace/ControlCommandCoordinator+Workspace.swift
Expand All @@ -116,14 +116,15 @@
803 Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift
802 Sources/WorkspaceContentView.swift
797 Sources/ClosedItemHistory.swift
782 CLI/CMUXCLI+AgentHookDefinitions.swift
779 cmuxUITests/BrowserOmnibarSuggestionsUITests.swift
773 Sources/App/MenuBarExtraController.swift
769 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+Input.swift
768 cmuxUITests/BrowserFixtureInteractionUITests.swift
762 Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift
761 Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift
760 Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
756 Sources/Panels/AgentSessionWebRendererCoordinator.swift
755 CLI/CMUXCLI+AgentHookDefinitions.swift
754 Sources/TerminalController+ControlWorkspaceContext.swift
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
739 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
Expand All @@ -142,6 +143,7 @@
696 cmuxTests/UpdatePillReleaseVisibilityTests.swift
691 Sources/NotificationSoundSettings.swift
691 cmuxTests/TaskManagerResourcesTests.swift
690 cmuxTests/SessionIndexViewTests.swift
683 Packages/macOS/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
683 Sources/Panels/CodexAppServerSession.swift
681 Sources/Panels/AgentSessionProcessStore.swift
Expand All @@ -151,7 +153,6 @@
668 cmuxTests/SettingsWindowPresenterTests.swift
664 Sources/CmuxTopSnapshot.swift
663 Sources/PortScanner.swift
663 cmuxTests/SessionIndexViewTests.swift
655 Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift
655 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift
653 Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Import/Detection/BrowserInstalledBrowserDetector.swift
Expand All @@ -167,16 +168,16 @@
620 cmuxTests/TerminalNotificationQueueTests.swift
613 Sources/SettingsNavigation.swift
612 cmuxUITests/FeedSidebarUITests.swift
608 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
607 Sources/SessionIndexModels.swift
607 Sources/SleepyFaceView.swift
604 Packages/macOS/CmuxCommandPalette/Tests/CmuxCommandPaletteTests/CommandPaletteNucleoFFITests.swift
602 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
601 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizerPrimaryPolicies.swift
599 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift
596 cmuxTests/CmuxEventBusTests.swift
594 Sources/SessionIndexModels.swift
594 cmuxTests/PortalTabDragRoutingTests.swift
590 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
591 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
589 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
588 cmuxTests/CommandPaletteShortcutCustomizationTests.swift
586 Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+PortScan.swift
586 Sources/JSONCParser.swift
Expand All @@ -197,6 +198,7 @@
552 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift
549 Sources/Panels/BrowserAutomation.swift
547 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift
546 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift
541 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlCommandCoordinator+Pane.swift
539 CLI/CMUXCLI+Themes.swift
539 CLI/CodexTeamsApprovalBridge.swift
Expand All @@ -212,7 +214,6 @@
526 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
525 Packages/macOS/CmuxSettings/Sources/CmuxSettings/SocketControl/SocketControlSettings.swift
524 CLI/CMUXCLI+AutoNaming.swift
523 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift
520 CLI/CMUXCLI+AmpExtension.swift
520 cmuxTests/MainWindowVisibilityControllerTests.swift
519 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/stress-two-column-cockpit-sidebar.swift
Expand Down
35 changes: 31 additions & 4 deletions CLI/CMUXCLI+AgentHookDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,32 +377,59 @@ extension CMUXCLI {

static func hookCommandString(for def: AgentHookDef, event: AgentHookDef.HookEvent) -> String {
let command = "cmux hooks \(def.name) \(event.cmuxSubcommand)"
let inline: String
if def.name == "codex", codexHookCanRunFireAndForget(event.cmuxSubcommand) {
return codexFireAndForgetAgentHookShellCommand(command, for: def)
inline = codexFireAndForgetAgentHookShellCommand(command, for: def)
} else {
inline = agentHookShellCommand(command, for: def)
}
if def.name == "codex" {
return codexPersistentHookScriptCommand(inline, eventTag: event.cmuxSubcommand)
}
return agentHookShellCommand(command, for: def)
return inline
}

/// Wraps a codex persistent hook command as a `#!/bin/sh` script file in the
/// cmux-owned hooks dir and returns its path. A bare executable path runs
/// correctly under any runtime, including ones (subrouters/proxies) that exec
/// the `command` string directly and fail an inline shell snippet with
/// "No such file or directory (os error 2)". Falls back to the inline command
/// on any write failure, so the persistent install can never regress.
private static func codexPersistentHookScriptCommand(_ inlineCommand: String, eventTag: String) -> String {
guard let dir = codexHookScriptsDirectory(),
let path = writeCodexHookScript(
subcommand: "persistent-\(eventTag)", body: inlineCommand, in: dir
) else {
return inlineCommand
}
return path
}

private static func codexHookCanRunFireAndForget(_ subcommand: String) -> Bool {
subcommand == "session-start" || subcommand == "prompt-submit"
}

static func feedHookCommandString(for def: AgentHookDef, agentEvent: String) -> String {
let inline: String
let noOpCommand = feedHookNoOpShellCommand(for: def, agentEvent: agentEvent)
switch def.format {
case .kiroAgentJSON:
return exitTwoPropagatingAgentHookShellCommand(
inline = exitTwoPropagatingAgentHookShellCommand(
"cmux hooks feed --source \(def.name) --event \(agentEvent)",
for: def,
noOpCommand: noOpCommand
)
default:
return agentHookShellCommand(
inline = agentHookShellCommand(
"cmux hooks feed --source \(def.name) --event \(agentEvent)",
for: def,
noOpCommand: noOpCommand
)
}
if def.name == "codex" {
return codexPersistentHookScriptCommand(inline, eventTag: "feed-\(agentEvent)")
}
return inline
}

private static func feedHookNoOpShellCommand(for def: AgentHookDef, agentEvent: String) -> String {
Expand Down
121 changes: 121 additions & 0 deletions CLI/CMUXCLI+CodexFireAndForgetHooks.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,125 @@
import Foundation

extension CMUXCLI {
/// The per-invocation Codex hook events the wrapper injects, paired with the
/// cmux subcommand they call and the codex hook timeout (ms). Lifecycle
/// events are short; feed events (`PreToolUse`/`PermissionRequest`) are long
/// because the user may take time to approve. This is the single source of
/// truth for `cmux-codex-wrapper`'s injection, mirrored from the historic
/// hand-rolled `cmux_codex_add_hook` calls in the wrapper.
static let codexWrapperInjectionEvents: [(agentEvent: String, cmuxSubcommand: String, timeoutMs: Int)] = [
("SessionStart", "session-start", 10000),
("UserPromptSubmit", "prompt-submit", 10000),
("Stop", "stop", 10000),
("PreToolUse", "pre-tool-use", 120000),
("PostToolUse", "post-tool-use", 10000),
("PermissionRequest", "notification", 120000),
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Emit, NUL-separated to stdout, the exact codex arg list the wrapper must
/// splice ahead of the user's args to enable + inject cmux's fire-and-forget
/// hooks for one codex invocation. Returns the arg list:
/// --enable\0hooks\0--dangerously-bypass-hook-trust\0
/// -c\0hooks.SessionStart=[{hooks=[{type="command",command='''<ff>''',timeout=10000}]}]\0
/// -c\0hooks.UserPromptSubmit=...\0 ... (one `-c` pair per event)
/// where `<ff>` is `codexFireAndForgetAgentHookShellCommand(...)` so each
/// hook returns `{}` to codex instantly and backgrounds the real cmux call.
/// Requires no live socket: pure string construction from the agent def.
func emitCodexWrapperInjectArgs() throws {
guard let codexDef = Self.agentDef(named: "codex") else {
throw CLIError(message: "Codex hook integration is unavailable.")
}
// Prefer a #!/bin/sh SCRIPT FILE as the hook command over an inline shell
// snippet. Some codex-compatible runtimes (subrouters, proxies) exec the
// `command` string directly as a program instead of via a shell, so an
// inline snippet fails with "No such file or directory (os error 2)". A
// bare executable file path runs correctly whether the runtime execs it
// directly or through a shell, and normal codex (which runs it via shell)
// is unaffected. The scripts are env-driven and identical across
// invocations, so they are written once into a cmux-owned dir (~/.cmux/
// hooks), not the user's ~/.codex. Any write failure falls back to the
// inline snippet so the working path can never regress.
let hooksDir = Self.codexHookScriptsDirectory()
var args: [String] = ["--enable", "hooks", "--dangerously-bypass-hook-trust"]
for event in Self.codexWrapperInjectionEvents {
let ff = Self.codexFireAndForgetAgentHookShellCommand(
"cmux hooks codex \(event.cmuxSubcommand)", for: codexDef
)
let command: String
if let scriptPath = hooksDir.flatMap({
Self.writeCodexHookScript(subcommand: event.cmuxSubcommand, body: ff, in: $0)
}), !scriptPath.contains("'''") {
command = scriptPath
} else {
command = ff
}
// TOML multi-line literal string ('''...''') preserves bytes verbatim
// and may contain single quotes, so the embedded `echo '{}'` / `sh -c
// '...'` survive with no escaping. TOML forbids only a literal triple
// single quote inside; guard against it (neither a path nor the
// command ever has one).
guard !command.contains("'''") else {
throw CLIError(message: "Codex hook command contains a triple single quote and cannot be TOML-encoded.")
}
let toml = "hooks.\(event.agentEvent)=[{hooks=[{type=\"command\",command='''\(command)''',timeout=\(event.timeoutMs)}]}]"
args.append("-c")
args.append(toml)
}
// NUL-TERMINATE each arg (trailing NUL after the last too) so a bash
// `while IFS= read -r -d '' arg` loop captures every element including
// the final one — a separator-only stream drops the unterminated last
// arg at EOF.
var out = Data()
for arg in args {
out.append(Data(arg.utf8))
out.append(0)
}
FileHandle.standardOutput.write(out)
}

/// The cmux-owned directory holding the generated codex hook scripts.
/// `~/.cmux/hooks` (NOT the user's `~/.codex`), created on demand. Returns
/// nil if it cannot be created, so the caller falls back to inline commands.
static func codexHookScriptsDirectory() -> URL? {
let home = FileManager.default.homeDirectoryForCurrentUser
let dir = home
.appendingPathComponent(".cmux", isDirectory: true)
.appendingPathComponent("hooks", isDirectory: true)
do {
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
} catch {
return nil
}
}

/// Writes (idempotently) a `#!/bin/sh` hook script for one event into `dir`
/// and returns its absolute path, or nil on any failure. The body is the
/// same env-driven fire-and-forget snippet used inline; as a real executable
/// file it runs under any runtime, including ones that exec the hook command
/// directly rather than through a shell. Content is identical across
/// invocations, so the file is only rewritten when missing or changed.
static func writeCodexHookScript(subcommand: String, body: String, in dir: URL) -> String? {
let safeName = subcommand.replacingOccurrences(
of: "[^A-Za-z0-9_-]", with: "-", options: .regularExpression
)
let url = dir.appendingPathComponent("cmux-codex-hook-\(safeName).sh", isDirectory: false)
let contents = "#!/bin/sh\n\(body)\n"
let fileManager = FileManager.default
if let existing = try? String(contentsOf: url, encoding: .utf8), existing == contents {
// Ensure it stays executable, then reuse.
try? fileManager.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
return url.path
}
do {
try contents.data(using: .utf8)?.write(to: url, options: .atomic)
try fileManager.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
return url.path
} catch {
return nil
}
}

static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
let runner = "payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\""
Expand Down
Loading
Loading