Support trailing browser output flags

This commit is contained in:
Lawrence Chen 2026-03-05 17:56:37 -08:00
parent 0fb2b414b0
commit 103f80fac2
2 changed files with 92 additions and 26 deletions

View file

@ -2583,7 +2583,34 @@ struct CMUXCLI {
throw CLIError(message: "browser requires a subcommand")
}
let (surfaceOpt, argsWithoutSurfaceFlag) = parseOption(commandArgs, name: "--surface")
var effectiveJSONOutput = jsonOutput
var effectiveIDFormat = idFormat
var browserArgs = commandArgs
// Browser-skill examples often place output flags at the end of the command.
// Strip trailing display flags so they don't become part of a URL or selector.
while !browserArgs.isEmpty {
if browserArgs.last == "--json" {
effectiveJSONOutput = true
browserArgs.removeLast()
continue
}
if browserArgs.count >= 2,
browserArgs[browserArgs.count - 2] == "--id-format" {
let raw = browserArgs.last!
guard let parsed = try CLIIDFormat.parse(raw) else {
throw CLIError(message: "--id-format must be one of: refs, uuids, both")
}
effectiveIDFormat = parsed
browserArgs.removeLast(2)
continue
}
break
}
let (surfaceOpt, argsWithoutSurfaceFlag) = parseOption(browserArgs, name: "--surface")
var surfaceRaw = surfaceOpt
var args = argsWithoutSurfaceFlag
@ -2612,8 +2639,8 @@ struct CMUXCLI {
}
func output(_ payload: [String: Any], fallback: String) {
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
if effectiveJSONOutput {
print(jsonString(formatIDs(payload, mode: effectiveIDFormat)))
return
}
print(fallback)
@ -2746,8 +2773,8 @@ struct CMUXCLI {
}
}
let payload = try client.sendV2(method: "browser.open_split", params: params)
let surfaceText = formatHandle(payload, kind: "surface", idFormat: idFormat) ?? "unknown"
let paneText = formatHandle(payload, kind: "pane", idFormat: idFormat) ?? "unknown"
let surfaceText = formatHandle(payload, kind: "surface", idFormat: effectiveIDFormat) ?? "unknown"
let paneText = formatHandle(payload, kind: "pane", idFormat: effectiveIDFormat) ?? "unknown"
let placement = ((payload["created_split"] as? Bool) == true) ? "split" : "reuse"
output(payload, fallback: "OK surface=\(surfaceText) pane=\(paneText) placement=\(placement)")
return
@ -2787,8 +2814,8 @@ struct CMUXCLI {
if subcommand == "url" || subcommand == "get-url" {
let sid = try requireSurface()
let payload = try client.sendV2(method: "browser.url.get", params: ["surface_id": sid])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
if effectiveJSONOutput {
print(jsonString(formatIDs(payload, mode: effectiveIDFormat)))
} else {
print((payload["url"] as? String) ?? "")
}
@ -2805,8 +2832,8 @@ struct CMUXCLI {
if ["is-webview-focused", "is_webview_focused"].contains(subcommand) {
let sid = try requireSurface()
let payload = try client.sendV2(method: "browser.is_webview_focused", params: ["surface_id": sid])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
if effectiveJSONOutput {
print(jsonString(formatIDs(payload, mode: effectiveIDFormat)))
} else {
print((payload["focused"] as? Bool) == true ? "true" : "false")
}
@ -2839,8 +2866,8 @@ struct CMUXCLI {
}
let payload = try client.sendV2(method: "browser.snapshot", params: params)
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
if effectiveJSONOutput {
print(jsonString(formatIDs(payload, mode: effectiveIDFormat)))
} else if let text = payload["snapshot"] as? String {
print(text)
} else {
@ -3059,8 +3086,8 @@ struct CMUXCLI {
try data.write(to: URL(fileURLWithPath: outPathOpt))
}
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
if effectiveJSONOutput {
print(jsonString(formatIDs(payload, mode: effectiveIDFormat)))
} else if let outPathOpt {
print("OK \(outPathOpt)")
} else {
@ -3120,8 +3147,8 @@ struct CMUXCLI {
"styles": "browser.get.styles",
]
let payload = try client.sendV2(method: methodMap[getVerb]!, params: params)
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
if effectiveJSONOutput {
print(jsonString(formatIDs(payload, mode: effectiveIDFormat)))
} else if let value = payload["value"] {
if let str = value as? String {
print(str)
@ -3160,8 +3187,8 @@ struct CMUXCLI {
throw CLIError(message: "Unsupported browser is subcommand: \(isVerb)")
}
let payload = try client.sendV2(method: method, params: ["surface_id": sid, "selector": selector])
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
if effectiveJSONOutput {
print(jsonString(formatIDs(payload, mode: effectiveIDFormat)))
} else if let value = payload["value"] {
print("\(value)")
} else {

View file

@ -91,6 +91,32 @@ def _run_cli_text(cli: str, args: list[str], retries: int = 3) -> str:
raise cmuxError(f"CLI failed ({' '.join(args)}): {last_merged}")
def _run_cli_tail_json(cli: str, args: list[str], retries: int = 3) -> dict:
last_merged = ""
for attempt in range(1, retries + 1):
proc = subprocess.run(
[cli, "--socket", SOCKET_PATH] + args,
capture_output=True,
text=True,
check=False,
)
if proc.returncode == 0:
try:
return json.loads(proc.stdout or "{}")
except Exception as exc: # noqa: BLE001
raise cmuxError(f"Invalid CLI JSON output for {' '.join(args)}: {proc.stdout!r} ({exc})")
merged = f"{proc.stdout}\n{proc.stderr}".strip()
last_merged = merged
if "Command timed out" in merged and attempt < retries:
time.sleep(0.2)
continue
raise cmuxError(f"CLI failed ({' '.join(args)}): {merged}")
raise cmuxError(f"CLI failed ({' '.join(args)}): {last_merged}")
def _run_cli_expect_failure(cli: str, args: list[str], needles: list[str]) -> None:
proc = subprocess.run(
[cli, "--socket", SOCKET_PATH, "--json"] + args,
@ -144,15 +170,6 @@ def main() -> int:
cli = _find_cli_binary()
with _local_test_server() as page_url:
opened = _run_cli_json(cli, ["browser", "open", page_url])
surface = str(opened.get("surface_ref") or opened.get("surface_id") or "")
_must(bool(surface), f"browser open returned no surface handle: {opened}")
_must(surface.startswith("surface:"), f"Expected short surface ref from browser open, got: {opened}")
_run_cli_json(cli, ["browser", surface, "wait", "--load-state", "complete", "--timeout-ms", "15000"])
snapshot_text = _run_cli_text(cli, ["browser", surface, "snapshot", "--interactive"])
_must("ref=e" in snapshot_text, f"Expected snapshot text with refs from CLI: {snapshot_text!r}")
identify = _run_cli_json(cli, ["identify"])
focused = identify.get("focused") or {}
workspace = str(
@ -163,6 +180,28 @@ def main() -> int:
or ""
)
_must(bool(workspace), f"Expected workspace handle from identify: {identify}")
os.environ["CMUX_WORKSPACE_ID"] = workspace
opened_tail_json = _run_cli_tail_json(
cli,
["browser", "open", page_url, "--workspace", workspace, "--id-format", "both", "--json"],
)
tail_surface = str(opened_tail_json.get("surface_ref") or "")
_must(tail_surface.startswith("surface:"), f"Expected trailing --json browser open to return surface_ref: {opened_tail_json}")
_must(bool(opened_tail_json.get("surface_id")), f"Expected trailing --id-format both to preserve surface_id: {opened_tail_json}")
_must("--json" not in str(opened_tail_json.get("url") or ""), f"Trailing output flags leaked into browser open URL: {opened_tail_json}")
_run_cli_json(cli, ["browser", tail_surface, "wait", "--load-state", "complete", "--timeout-ms", "15000"])
tail_url_payload = _run_cli_json(cli, ["browser", tail_surface, "url"])
_must(str(tail_url_payload.get("url") or "").startswith(page_url), f"Expected trailing --json browser open to navigate: {tail_url_payload}")
opened = _run_cli_json(cli, ["browser", "open", page_url])
surface = str(opened.get("surface_ref") or opened.get("surface_id") or "")
_must(bool(surface), f"browser open returned no surface handle: {opened}")
_must(surface.startswith("surface:"), f"Expected short surface ref from browser open, got: {opened}")
_run_cli_json(cli, ["browser", surface, "wait", "--load-state", "complete", "--timeout-ms", "15000"])
snapshot_text = _run_cli_text(cli, ["browser", surface, "snapshot", "--interactive"])
_must("ref=e" in snapshot_text, f"Expected snapshot text with refs from CLI: {snapshot_text!r}")
opened_routed = _run_cli_json(cli, ["browser", "open", page_url, "--workspace", workspace])
routed_surface = str(opened_routed.get("surface_ref") or opened_routed.get("surface_id") or "")