Skip to content
Closed
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
5 changes: 3 additions & 2 deletions CLI/CMUXCLI+CodexFireAndForgetHooks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,11 @@ extension CMUXCLI {
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\""
let noOp = stdinDrainingHookNoOpShellCommand
return [
"cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"",
"cmux_cli=\"${CMUX_CODEX_HOOK_CMUX_BIN:-}\"",
"if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; fi",
Comment on lines +122 to +123

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all references to CMUX_CODEX_HOOK_CMUX_BIN to confirm production vs. test-only usage.
rg -n "CMUX_CODEX_HOOK_CMUX_BIN"

Repository: manaflow-ai/cmux

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked files matching relevant names:"
git ls-files | rg '(^|/)(CMUXCLI\+CodexFireAndForgetHooks\.swift|CLICodexHookTimeoutRegressionTests\.swift|.*Codex.*Hook.*|.*FireAndForget.*|.*Codex.*)' || true

echo
echo "Locate file by name:"
fd -a -i 'CMUXCLI\+CodexFireAndForgetHooks.swift' . || true
fd -a -i 'CLICodexHookTimeoutRegressionTests.swift' . || true

echo
echo "Search related env/fallback references:"
rg -n "CMUX_CODEX_HOOK_CMUX_BIN|CMUX_BUNDLED_CLI_PATH|CODEX|cmux_cli|BUNDLED_CLI" . --glob '!build/**' --glob '!dist/**' --glob '!node_modules/**' | head -200 || true

Repository: manaflow-ai/cmux

Length of output: 26900


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ -f "CLI/CMUXCLI+CodexFireAndForgetHooks.swift" ]; then
  echo "CLI/CMUXCLI+CodexFireAndForgetHooks.swift"
  wc -l "CLI/CMUXCLI+CodexFireAndForgetHooks.swift"
  sed -n '80,150p' "CLI/CMUXCLI+CodexFireAndForgetHooks.swift" | nl -ba -v80
else
  echo "Missing CLI/CMUXCLI+CodexFireAndForgetHooks.swift"
fi

if [ -f "cmuxTests/CLICodexHookTimeoutRegressionTests.swift" ]; then
  echo
  echo "cmuxTests/CLICodexHookTimeoutRegressionTests.swift"
  wc -l "cmuxTests/CLICodexHookTimeoutRegressionTests.swift"
  rg -n -C 4 "CMUX_CODEX_HOOK_CMUX_BIN|CMUX_BUNDLED_CLI_PATH|hook|CMUX" "cmuxTests/CLICodexHookTimeoutRegressionTests.swift" || true
fi

echo
echo "Search repository for exact/likely identifiers:"
rg -n "CMUX_CODEX_HOOK_CMUX_BIN|CMUX_BUNDLED_CLI_PATH|CodexFireAndForget|codex.*hook|CMUXCLI\+CodexFireAndForgetHooks" . \
  --glob '!build/**' --glob '!dist/**' --glob '!node_modules/**' --glob '!DerivedData/**' \
  | head -300 || true

Repository: manaflow-ai/cmux

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "CLI/CMUXCLI+CodexFireAndForgetHooks.swift"
wc -l "CLI/CMUXCLI+CodexFireAndForgetHooks.swift"
sed -n '80,150p' "CLI/CMUXCLI+CodexFireAndForgetHooks.swift"

echo
echo "Code generation occurrences for CMUX_CODEX_HOOK_CMUX_BIN / bundled path:"
rg -n -C 3 "CMUX_CODEX_HOOK_CMUX_BIN|CMUX_BUNDLED_CLI_PATH|cmux_cli=|Fallback|fallback" "CLI/CMUXCLI+CodexFireAndForgetHooks.swift" "cmuxTests/CLICodexHookTimeoutRegressionTestSupport.swift" "cmuxTests/CLICodexHookTimeoutRegressionTests.swift" "tests/test_codex_wrapper_resume_hooks.py" "tests/test_codex_feed_hooks.py" || true

echo
echo "Focused tracked file contexts:"
for f in "CLI/CMUXCLI+CodexFireAndForgetHooks.swift" "cmuxTests/CLICodexHookTimeoutRegressionTestSupport.swift" "cmuxTests/CLICodexHookTimeoutRegressionTests.swift" "tests/test_codex_wrapper_resume_hooks.py"; do
  if [ -f "$f" ]; then
    echo "--- $f ($(wc -l < "$f") lines) ---"
    rg -n -C 4 "CMUX_CODEX_HOOK_CMUX_BIN|CMUX_BUNDLED_CLI_PATH|cmux_cli" "$f" || true
  fi
done

Repository: manaflow-ai/cmux

Length of output: 18016


Add production scope guidance for CMUX_CODEX_HOOK_CMUX_BIN.

This line makes CMUX_CODEX_HOOK_CMUX_BIN the first checked executable for Codex hooks before CMUX_BUNDLED_CLI_PATH. Add a nearby comment stating whether this is a supported production override or intended only for test harnesses, matching the surrounding fallback-documentation style.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/CMUXCLI`+CodexFireAndForgetHooks.swift around lines 122 - 123, Clarify
the production scope of CMUX_CODEX_HOOK_CMUX_BIN by adding a nearby comment in
the hook command setup, stating whether it is a supported production executable
override or restricted to test harnesses. Match the existing documentation style
around the CMUX_BUNDLED_CLI_PATH fallback without changing the
executable-selection behavior.

"if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi",
"agent_pid=\"${CMUX_CODEX_PID:-${PPID:-}}\"",
"if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { \(noOp); exit 0; }; cat >\"$payload\" || true; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else \(noOp); fi",
"if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { \(noOp); exit 0; }; cat >\"$payload\" || true; set -- \"$cmux_cli\"; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then set -- \"$@\" --socket \"$CMUX_SOCKET_PATH\"; fi; set -- \"$@\" \(routedArguments); if [ -n \"${CMUX_WORKSPACE_ID:-}\" ]; then set -- \"$@\" --workspace \"$CMUX_WORKSPACE_ID\" --surface \"$CMUX_SURFACE_ID\"; fi; CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$@\" >/dev/null 2>&1 & echo '{}'; else \(noOp); fi",
].joined(separator: "; ")
}
}
251 changes: 251 additions & 0 deletions CLI/CMUXCLI+TerminalOverlay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import Foundation

extension CMUXCLI {
static let surfaceOverlayCommandUsageLine = String(
localized: "cli.surfaceOverlay.usageLine",
defaultValue: "surface overlay <set|list|remove|clear> [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]"
)

static let surfaceOverlayCommandHelp = String(
localized: "cli.surfaceOverlay.help",
defaultValue: """
Usage: cmux surface overlay set <id> <text> [--anchor <viewport|scrollback|sticky>] [--position <left|center|right>] [target flags]
cmux surface overlay list [target flags]
cmux surface overlay remove <id> [target flags]
cmux surface overlay clear [target flags]

Render passive one-row strips over a terminal without taking keyboard or mouse input.
Viewport stays at the visible top. Scrollback stays at the captured top row.
Sticky follows the captured row, then pins when that row reaches the viewport top.

Target flags:
--workspace <id|ref|index> Workspace context (default: $CMUX_WORKSPACE_ID)
--surface <id|ref|index> Terminal context (default: $CMUX_SURFACE_ID)
--window <id|ref|index> Window context for workspace and surface refs/indexes

Set flags:
--anchor <viewport|scrollback|sticky> Vertical anchor (default: viewport)
--position <left|center|right> Text alignment (default: center)

Use '-' as text to read the overlay from standard input.

Examples:
cmux surface overlay set latest-message "check the auth error"
printf 'build\\npassed' | cmux surface overlay set build-status - --position right
cmux surface overlay set review-note "inspect this output" --anchor scrollback --position left
cmux surface overlay set latest-message "keep this visible" --anchor sticky --position left
"""
)
Comment on lines +4 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Approve help-text constants; document the -- terminator convention.

The -- terminator, used by overlayArguments(splitAtTerminator:) to let literal text through the "unknown flag" check (Lines 69-77, 212-219), is never mentioned in surfaceOverlayCommandHelp. A user typing overlay text that starts with -- gets an "unknown flag" error with no documented workaround.

📝 Suggested help text addition
         Use '-' as text to read the overlay from standard input.
+        Use '--' before text to stop flag parsing, so text can start with '--'.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static let surfaceOverlayCommandUsageLine = String(
localized: "cli.surfaceOverlay.usageLine",
defaultValue: "surface overlay <set|list|remove|clear> [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]"
)
static let surfaceOverlayCommandHelp = String(
localized: "cli.surfaceOverlay.help",
defaultValue: """
Usage: cmux surface overlay set <id> <text> [--anchor <viewport|scrollback>] [--position <left|center|right>] [target flags]
cmux surface overlay list [target flags]
cmux surface overlay remove <id> [target flags]
cmux surface overlay clear [target flags]
Render passive text above a terminal without taking keyboard or mouse input.
A viewport overlay stays at the visible top. A scrollback overlay captures the current top row.
Target flags:
--workspace <id|ref|index> Workspace context (default: $CMUX_WORKSPACE_ID)
--surface <id|ref|index> Terminal context (default: $CMUX_SURFACE_ID)
--window <id|ref|index> Window context for workspace and surface refs/indexes
Set flags:
--anchor <viewport|scrollback> Vertical anchor (default: viewport)
--position <left|center|right> Horizontal position (default: center)
Use '-' as text to read the overlay from standard input.
Examples:
cmux surface overlay set latest-message "check the auth error"
printf 'build\\npassed' | cmux surface overlay set build-status - --position right
cmux surface overlay set review-note "inspect this output" --anchor scrollback --position left
"""
)
static let surfaceOverlayCommandUsageLine = String(
localized: "cli.surfaceOverlay.usageLine",
defaultValue: "surface overlay <set|list|remove|clear> [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]"
)
static let surfaceOverlayCommandHelp = String(
localized: "cli.surfaceOverlay.help",
defaultValue: """
Usage: cmux surface overlay set <id> <text> [--anchor <viewport|scrollback>] [--position <left|center|right>] [target flags]
cmux surface overlay list [target flags]
cmux surface overlay remove <id> [target flags]
cmux surface overlay clear [target flags]
Render passive text above a terminal without taking keyboard or mouse input.
A viewport overlay stays at the visible top. A scrollback overlay captures the current top row.
Target flags:
--workspace <id|ref|index> Workspace context (default: $CMUX_WORKSPACE_ID)
--surface <id|ref|index> Terminal context (default: $CMUX_SURFACE_ID)
--window <id|ref|index> Window context for workspace and surface refs/indexes
Set flags:
--anchor <viewport|scrollback> Vertical anchor (default: viewport)
--position <left|center|right> Horizontal position (default: center)
Use '-' as text to read the overlay from standard input.
Use '--' before text to stop flag parsing, so text can start with '--'.
Examples:
cmux surface overlay set latest-message "check the auth error"
printf 'build\\npassed' | cmux surface overlay set build-status - --position right
cmux surface overlay set review-note "inspect this output" --anchor scrollback --position left
"""
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/CMUXCLI`+TerminalOverlay.swift around lines 4 - 36, Update the
surfaceOverlayCommandHelp text to document the -- argument terminator supported
by overlayArguments(splitAtTerminator:), including that it allows overlay text
beginning with -- to bypass unknown-flag parsing. Add a concise usage example or
instruction near the existing stdin/text guidance without changing command
behavior.


func runSurfaceOverlayCommand(
commandArgs: [String],
client: SocketClient,
jsonOutput: Bool,
idFormat: CLIIDFormat,
windowOverride: String?
) throws {
guard let subcommand = commandArgs.first?.lowercased() else {
throw CLIError(message: String(
localized: "cli.surfaceOverlay.error.missingSubcommand",
defaultValue: "surface overlay requires set, list, remove, or clear"
))
}
let rest = Array(commandArgs.dropFirst())
let target = try surfaceCommandTarget(rest, client: client, windowOverride: windowOverride)
var params = target.params

switch subcommand {
case "set":
let (anchor, remainingAfterAnchor) = parseOption(target.remaining, name: "--anchor")
let (position, remainingAfterPosition) = parseOption(
remainingAfterAnchor,
name: "--position"
)
let split = overlayArguments(splitAtTerminator: remainingAfterPosition)
guard let id = split.before.first else {
throw CLIError(message: String(
localized: "cli.surfaceOverlay.error.setRequiresID",
defaultValue: "surface overlay set requires an id"
))
}
if let unknown = split.before.dropFirst().first(where: { $0.hasPrefix("--") }) {
throw CLIError(message: String.localizedStringWithFormat(
String(
localized: "cli.surfaceOverlay.error.unknownFlagFormat",
defaultValue: "surface overlay: unknown flag '%@'"
),
unknown
))
}
let textTokens = Array(split.before.dropFirst()) + split.after
guard !textTokens.isEmpty else {
throw CLIError(message: String(
localized: "cli.surfaceOverlay.error.setRequiresText",
defaultValue: "surface overlay set requires text or '-' for standard input"
))
}
let text: String
if textTokens == ["-"] {
text = String(
data: FileHandle.standardInput.readDataToEndOfFile(),
encoding: .utf8
) ?? ""
} else {
text = textTokens.joined(separator: " ")
}
Comment on lines +80 to +95

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard against empty text after reading standard input.

textTokens.isEmpty is checked at Line 79, before - is resolved to stdin content. If textTokens == ["-"] and standard input is empty or closed, text becomes "" at Line 90, and the command proceeds to call surface.overlay.set with empty text, unlike publishLatestCodexUserMessageOverlay, which explicitly skips publishing when boundedPrompt is empty after trimming (Line 178).

Add the same empty-content check after resolving text from stdin, so set - with empty input fails with a clear error instead of creating a blank overlay.

🛡️ Suggested fix
             } else {
                 text = textTokens.joined(separator: " ")
             }
+            guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
+                throw CLIError(message: String(
+                    localized: "cli.surfaceOverlay.error.setRequiresText",
+                    defaultValue: "surface overlay set requires text or '-' for standard input"
+                ))
+            }
             params["overlay_id"] = id
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let textTokens = Array(split.before.dropFirst()) + split.after
guard !textTokens.isEmpty else {
throw CLIError(message: String(
localized: "cli.surfaceOverlay.error.setRequiresText",
defaultValue: "surface overlay set requires text or '-' for standard input"
))
}
let text: String
if textTokens == ["-"] {
text = String(
data: FileHandle.standardInput.readDataToEndOfFile(),
encoding: .utf8
) ?? ""
} else {
text = textTokens.joined(separator: " ")
}
let textTokens = Array(split.before.dropFirst()) + split.after
guard !textTokens.isEmpty else {
throw CLIError(message: String(
localized: "cli.surfaceOverlay.error.setRequiresText",
defaultValue: "surface overlay set requires text or '-' for standard input"
))
}
let text: String
if textTokens == ["-"] {
text = String(
data: FileHandle.standardInput.readDataToEndOfFile(),
encoding: .utf8
) ?? ""
} else {
text = textTokens.joined(separator: " ")
}
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw CLIError(message: String(
localized: "cli.surfaceOverlay.error.setRequiresText",
defaultValue: "surface overlay set requires text or '-' for standard input"
))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/CMUXCLI`+TerminalOverlay.swift around lines 78 - 93, After the
stdin-or-joined-token resolution in the overlay set command, validate the
resulting text is non-empty before calling surface.overlay.set. Reuse the
existing setRequiresText CLIError for empty stdin content, while preserving the
current handling of non-empty stdin and regular text tokens.

params["overlay_id"] = id
params["text"] = text
if let anchor { params["anchor"] = anchor }
if let position { params["position"] = position }
let payload = try client.sendV2(method: "surface.overlay.set", params: params)
printSurfaceOverlayPayload(
payload,
jsonOutput: jsonOutput,
idFormat: idFormat,
fallback: (payload["overlay"] as? [String: Any])?["id"] as? String ?? id
)

case "list":
try requireNoSurfaceOverlayArguments(target.remaining, subcommand: subcommand)
let payload = try client.sendV2(method: "surface.overlay.list", params: params)
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
for overlay in payload["overlays"] as? [[String: Any]] ?? [] {
let id = overlay["id"] as? String ?? ""
let anchor = overlay["anchor"] as? String ?? ""
let position = overlay["position"] as? String ?? ""
let text = (overlay["text"] as? String ?? "")
.replacingOccurrences(of: "\n", with: "\\n")
print("\(id)\t\(anchor)\t\(position)\t\(text)")
}
}

case "remove":
let split = overlayArguments(splitAtTerminator: target.remaining)
let arguments = split.before + split.after
guard let id = arguments.first else {
throw CLIError(message: String(
localized: "cli.surfaceOverlay.error.removeRequiresID",
defaultValue: "surface overlay remove requires an id"
))
}
guard arguments.count == 1 else {
throw CLIError(message: String(
localized: "cli.surfaceOverlay.error.removeExtraArguments",
defaultValue: "surface overlay remove accepts one id"
))
}
params["overlay_id"] = id
let payload = try client.sendV2(method: "surface.overlay.remove", params: params)
printSurfaceOverlayPayload(
payload,
jsonOutput: jsonOutput,
idFormat: idFormat,
fallback: (payload["removed"] as? Bool) == true ? "true" : "false"
)

case "clear":
try requireNoSurfaceOverlayArguments(target.remaining, subcommand: subcommand)
let payload = try client.sendV2(method: "surface.overlay.clear", params: params)
let removed = payload["removed"] as? Int ?? 0
printSurfaceOverlayPayload(
payload,
jsonOutput: jsonOutput,
idFormat: idFormat,
fallback: String(removed)
)

default:
throw CLIError(message: String.localizedStringWithFormat(
String(
localized: "cli.surfaceOverlay.error.unsupportedSubcommandFormat",
defaultValue: "Unsupported surface overlay subcommand: %@"
),
subcommand
))
}
}

func publishLatestCodexUserMessageOverlay(
_ prompt: String,
workspaceId: String,
surfaceId: String,
client: SocketClient
) throws {
let boundedPrompt = terminalOverlayText(
prompt,
maximumUTF8Bytes: 16_384
)
guard !boundedPrompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return
}
_ = try client.sendV2(
method: "surface.overlay.set",
params: [
"workspace_id": workspaceId,
"surface_id": surfaceId,
"overlay_id": "agent.codex.latest-user-message",
"text": boundedPrompt,
"anchor": "viewport",
"position": "left",
]
)
}

private func terminalOverlayText(_ text: String, maximumUTF8Bytes: Int) -> String {
guard text.utf8.count > maximumUTF8Bytes else { return text }
var result = ""
result.reserveCapacity(maximumUTF8Bytes)
var remainingBytes = maximumUTF8Bytes
for character in text {
let value = String(character)
let byteCount = value.utf8.count
guard byteCount <= remainingBytes else { break }
result.append(character)
remainingBytes -= byteCount
}
return result
}

private func overlayArguments(
splitAtTerminator args: [String]
) -> (before: [String], after: [String]) {
guard let index = args.firstIndex(of: "--") else {
return (args, [])
}
return (
Array(args[..<index]),
Array(args[args.index(after: index)...])
)
}

private func requireNoSurfaceOverlayArguments(
_ args: [String],
subcommand: String
) throws {
guard args.isEmpty else {
throw CLIError(message: String.localizedStringWithFormat(
String(
localized: "cli.surfaceOverlay.error.unexpectedArgumentFormat",
defaultValue: "surface overlay %@: unexpected argument '%@'"
),
subcommand,
args[0]
))
}
}

private func printSurfaceOverlayPayload(
_ payload: [String: Any],
jsonOutput: Bool,
idFormat: CLIIDFormat,
fallback: String
) {
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
print(fallback)
}
}
}
45 changes: 37 additions & 8 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6981,6 +6981,14 @@ struct CMUXCLI {
idFormat: idFormat,
windowOverride: windowOverride
)
case "overlay":
try runSurfaceOverlayCommand(
commandArgs: Array(commandArgs.dropFirst()),
client: client,
jsonOutput: jsonOutput,
idFormat: idFormat,
windowOverride: windowOverride
)
default:
throw CLIError(message: "Unsupported surface subcommand: \(subcommand)")
}
Expand All @@ -7002,7 +7010,7 @@ struct CMUXCLI {
optionNames: Self.surfaceResumeSetValueOptions,
context: "surface resume set"
)
let target = try surfaceResumeTarget(rest, client: client, windowOverride: windowOverride)
let target = try surfaceCommandTarget(rest, client: client, windowOverride: windowOverride)
var params = target.params
let splitRemaining = splitAtArgumentTerminator(target.remaining)
let (name, rem1) = parseOption(splitRemaining.options, name: "--name")
Expand Down Expand Up @@ -7057,7 +7065,7 @@ struct CMUXCLI {
optionNames: Self.surfaceResumeTargetValueOptions,
context: "surface resume \(subcommand)"
)
let params = try surfaceResumeTarget(rest, client: client, windowOverride: windowOverride).params
let params = try surfaceCommandTarget(rest, client: client, windowOverride: windowOverride).params
let payload = try client.sendV2(method: "surface.resume.get", params: params)
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
Expand All @@ -7075,7 +7083,7 @@ struct CMUXCLI {
optionNames: Self.surfaceResumeClearValueOptions,
context: "surface resume clear"
)
let target = try surfaceResumeTarget(rest, client: client, windowOverride: windowOverride)
let target = try surfaceCommandTarget(rest, client: client, windowOverride: windowOverride)
var params = target.params
let (checkpoint, rem1) = parseOption(target.remaining, name: "--checkpoint")
let (checkpointID, rem2) = parseOption(rem1, name: "--checkpoint-id")
Expand Down Expand Up @@ -7191,7 +7199,7 @@ struct CMUXCLI {
}
}

private struct SurfaceResumeTarget {
struct SurfaceCommandTarget {
var params: [String: Any]
var remaining: [String]
}
Expand All @@ -7204,11 +7212,11 @@ struct CMUXCLI {
return (Array(args[..<delimiterIndex]), Array(args[argvStart...]))
}

private func surfaceResumeTarget(
func surfaceCommandTarget(
_ args: [String],
client: SocketClient,
windowOverride: String?
) throws -> SurfaceResumeTarget {
) throws -> SurfaceCommandTarget {
let splitArgs = splitAtArgumentTerminator(args)
let (workspaceOpt, rem1) = parseOption(splitArgs.options, name: "--workspace")
let (surfaceOpt, rem2) = parseOption(rem1, name: "--surface")
Expand Down Expand Up @@ -7236,7 +7244,7 @@ struct CMUXCLI {
)
if let surfaceId { params["surface_id"] = surfaceId }
let remainingWithArgv = remaining + (splitArgs.argv.map { ["--"] + $0 } ?? [])
return SurfaceResumeTarget(params: params, remaining: remainingWithArgv)
return SurfaceCommandTarget(params: params, remaining: remainingWithArgv)
}

private func cliShellQuote(_ value: String) -> String {
Expand Down Expand Up @@ -16783,6 +16791,8 @@ struct CMUXCLI {
"""
case "surface", "surface-resume":
return """
\(Self.surfaceOverlayCommandHelp)

Usage: cmux surface resume set [flags] -- <argv...>
cmux surface resume set [flags] --shell <command>
cmux surface resume show [--json] [flags]
Expand Down Expand Up @@ -31579,6 +31589,24 @@ export default CMUXSessionRestore;
nestedPromptEvent: nestedPromptSubmit,
env: env
)
// The prompt strip describes accepted user input, not the duration
// of the agent turn. Publish it before lifecycle/socket work so a
// fast Stop cannot race the strip out of existence.
if !suppressVisibleMutations,
!incomingCodexTurnIsTerminal,
def.name == "codex",
let prompt = feedPromptText(from: input.rawObject ?? input.object) {
do {
try publishLatestCodexUserMessageOverlay(
prompt,
workspaceId: workspaceId,
surfaceId: surfaceId,
client: client
)
} catch {
Comment on lines +31592 to +31606

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Preserve the error detail in the overlay-publish catch block.

The catch block still discards error and records only the static string "codex-hook.prompt-submit.overlay-failed". Other failure paths in this same function preserve error detail. claude-hook.stop.ignored uses data: ["error": String(describing: error)]. reportAgentHookFailure also forwards error: error. Without the error detail, a failed overlay publish is not diagnosable from telemetry alone.

🐛 Proposed fix to preserve the error detail
                 } catch {
-                    telemetry.breadcrumb("codex-hook.prompt-submit.overlay-failed")
+                    telemetry.breadcrumb(
+                        "codex-hook.prompt-submit.overlay-failed",
+                        data: ["error": String(describing: error)]
+                    )
                 }

This issue was flagged in a previous review pass on an earlier commit and remains unresolved in the current code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux.swift` around lines 31592 - 31606, Update the catch block around
publishLatestCodexUserMessageOverlay in the codex prompt-strip flow to include
String(describing: error) in the recorded telemetry data, while preserving the
existing "codex-hook.prompt-submit.overlay-failed" event and surrounding
behavior.

telemetry.breadcrumb("codex-hook.prompt-submit.overlay-failed")
}
}
if !suppressVisibleMutations && !incomingCodexTurnIsTerminal {
if codexPromptTurnWentTerminal() {
stopStaleCodexPromptSubmit()
Expand Down Expand Up @@ -32675,7 +32703,7 @@ export default CMUXSessionRestore;
}
}

private func feedPromptText(from object: [String: Any]?) -> String? {
func feedPromptText(from object: [String: Any]?) -> String? {
guard let object else { return nil }
if let direct = firstString(in: object, keys: ["prompt", "text", "message", "body"]) {
return direct
Expand Down Expand Up @@ -36076,6 +36104,7 @@ export default CMUXSessionRestore;
split-off --surface <id|ref|index> <left|right|up|down> [--workspace <id|ref|index>] [--window <id|ref|index>] [--focus <true|false>]
reorder-surface --surface <id|ref|index> (--index <n> | --before <id|ref|index> | --after <id|ref|index>) [--workspace <id|ref|index>] [--window <id|ref|index>] [--focus <true|false>]
tab-action --action <name> [--tab <id|ref|index>] [--surface <id|ref|index>] [--workspace <id|ref|index>] [--window <id|ref|index>] [--title <text>] [--url <url>] [--focus <true|false>]
\(Self.surfaceOverlayCommandUsageLine)
surface resume <set|show|get|clear> [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]
rename-tab [--workspace <id|ref|index>] [--tab <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] <title>
drag-surface-to-split --surface <id|ref|index> <left|right|up|down> [--workspace <id|ref|index>] [--window <id|ref|index>] [--focus <true|false>]
Expand Down
Loading