Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
12cee18
Add Roughdraft markdown review handoff
austinywang May 20, 2026
f202fee
Avoid leaking Roughdraft process output
austinywang May 20, 2026
3342fd9
Merge remote-tracking branch 'origin/main' into feat-roughdraft-md-in…
austinywang May 22, 2026
d27d563
fix: sanitize roughdraft cli errors
austinywang May 22, 2026
9423020
fix: preserve cli process timeout state
austinywang May 22, 2026
e4fbe67
test: remove roughdraft socket wait
austinywang May 22, 2026
bb52e50
fix: address roughdraft review feedback
austinywang May 22, 2026
9338240
merge: resolve conflicts with main
austinywang Jun 4, 2026
4703cf2
fix: add roughdraft help localization
austinywang Jun 4, 2026
37bb5a1
Merge remote-tracking branch 'origin/main' into feat-roughdraft-md-in…
austinywang Jun 14, 2026
ecea1ca
chore: avoid conflict marker scan false positives
austinywang Jun 14, 2026
88c3d4b
fix: address roughdraft review feedback
austinywang Jun 14, 2026
dfe0606
chore: regenerate Swift file length budget
austinywang Jun 14, 2026
7a724be
fix: localize roughdraft strings
austinywang Jun 14, 2026
9abae6b
Merge remote-tracking branch 'origin/main' into feat-roughdraft-md-in…
austinywang Jun 14, 2026
18cb76d
chore: refresh Swift file length budget
austinywang Jun 14, 2026
f3f10a0
test: cover roughdraft browser proxy bypass
austinywang Jun 14, 2026
06b3bd3
fix: bypass remote proxy for roughdraft URLs
austinywang Jun 14, 2026
b7bd864
Merge remote-tracking branch 'origin/main' into feat-roughdraft-md-in…
austinywang Jun 14, 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
24 changes: 12 additions & 12 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# 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.
33857 CLI/cmux.swift
34033 CLI/cmux.swift
17914 Sources/AppDelegate.swift
16740 Sources/ContentView.swift
16709 Sources/ContentView.swift
14612 Sources/TerminalController.swift
13595 Sources/Panels/BrowserPanel.swift
12088 Sources/GhosttyTerminalView.swift
Expand All @@ -13,15 +13,15 @@
7911 Sources/Panels/BrowserPanelView.swift
7350 cmuxTests/WorkspaceUnitTests.swift
6944 cmuxTests/WorkspaceRemoteConnectionTests.swift
6363 cmuxTests/GhosttyConfigTests.swift
6317 cmuxTests/SessionPersistenceTests.swift
6299 cmuxTests/GhosttyConfigTests.swift
6153 CLI/cmux_open.swift
6074 Sources/TabManager.swift
6074 Sources/TextBoxInput.swift
5925 cmuxTests/TerminalAndGhosttyTests.swift
5522 cmuxTests/BrowserConfigTests.swift
4921 Sources/cmuxApp.swift
5113 Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
4921 Sources/cmuxApp.swift
4460 Sources/Panels/FilePreviewPanel.swift
4400 cmuxTests/BrowserPanelTests.swift
4227 Sources/BrowserWindowPortal.swift
Expand All @@ -32,9 +32,9 @@
3664 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
3397 Sources/CmuxConfig.swift
3331 cmuxTests/TabManagerSessionSnapshotTests.swift
3204 Sources/Update/UpdateTitlebarAccessory.swift
3200 Sources/Update/UpdateTitlebarAccessory.swift
2981 cmuxTests/CMUXOpenCommandTests.swift
2878 Sources/SessionIndexView.swift
2871 cmuxTests/CMUXOpenCommandTests.swift
2573 Sources/KeyboardShortcutSettings.swift
2565 Sources/Panels/CmuxWebView.swift
2546 cmuxTests/WorkspaceManualUnreadTests.swift
Expand All @@ -59,11 +59,11 @@
1652 cmuxTests/CMUXCLIErrorOutputRegressionTests.swift
1574 cmuxTests/MarkdownPanelTests.swift
1560 cmuxTests/TextBoxMentionCompletionTests.swift
1498 cmuxTests/OmnibarAndToolsTests.swift
1497 cmuxTests/OmnibarAndToolsTests.swift
1496 cmuxUITests/MultiWindowNotificationsUITests.swift
1446 Sources/FileExplorerStore.swift
1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
1382 cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift
1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
1373 cmuxTests/AppDelegateIssue2907RoutingTests.swift
1366 Sources/Feed/FeedButtonStyleDebugWindowController.swift
1362 Sources/CMUXInstalledExtensionSidebarHostView.swift
Expand Down Expand Up @@ -92,9 +92,9 @@
937 Sources/TextBoxMentionIndexStore.swift
934 Sources/App/ShortcutRoutingSupport.swift
926 Sources/DockPanelView.swift
920 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
919 Packages/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift
918 cmuxTests/WorkspaceGroupTests.swift
920 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
905 Sources/CmuxSSHURLRequest.swift
901 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
893 Sources/WorkspaceContentView.swift
Expand All @@ -117,13 +117,14 @@
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
746 Sources/App/MenuBarExtraController.swift
738 Packages/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
738 Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
736 Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
716 Sources/TaskManagerSnapshot.swift
715 Packages/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+Input.swift
715 Sources/AppleScriptSupport.swift
710 Sources/TerminalSSHSessionDetector.swift
706 CLI/CMUXCLI+Config.swift
707 CLI/CMUXCLI+AgentHookDefinitions.swift
706 CLI/CMUXCLI+Config.swift
699 Sources/RightSidebarPanelView.swift
699 cmuxTests/TerminalNotificationClearAllTests.swift
698 cmuxTests/RestorableAgentHookProviderResumeTests.swift
Expand Down Expand Up @@ -187,7 +188,6 @@
528 cmuxTests/CLINotifyProcessTestSupport.swift
528 cmuxUITests/AutomationSocketUITests.swift
527 CLI/CLISocketPathResolver.swift
726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
523 Packages/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+PortScan.swift
520 CLI/CMUXCLI+AmpExtension.swift
520 cmuxTests/MainWindowVisibilityControllerTests.swift
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ App path:
**Claude Code** outputs:

```markdown
=======================================================
-------------------------------------------------------
[cmux DEV my-tag.app](file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app)
=======================================================
-------------------------------------------------------
```

**Codex** outputs:

```markdown
=======================================================
-------------------------------------------------------
[my-tag: file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app](file:///Users/someone/Library/Developer/Xcode/DerivedData/cmux-my-tag/Build/Products/Debug/cmux%20DEV%20my-tag.app)
=======================================================
-------------------------------------------------------
```

Never use `/tmp/cmux-<tag>/...` app links in chat output.
Expand Down
180 changes: 178 additions & 2 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5048,6 +5048,10 @@ struct CMUXCLI {
case "markdown":
try runMarkdownCommand(commandArgs: commandArgs, client: client, jsonOutput: jsonOutput, idFormat: idFormat)

// Roughdraft markdown review integration
case "roughdraft":
try runRoughdraftCommand(commandArgs: commandArgs, client: client, jsonOutput: jsonOutput, idFormat: idFormat)

default:
print(usage())
throw CLIError(message: "Unknown command: \(command)")
Expand Down Expand Up @@ -5280,6 +5284,156 @@ struct CMUXCLI {
return false
}

// MARK: - Roughdraft Commands

private func roughdraftOpenUsage() -> String {
String(localized: "cli.roughdraft.usage.open", defaultValue: "Usage: cmux roughdraft open <path> [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] [--focus <true|false>]")
}

private func roughdraftUnknownFlagMessage(_ flag: String) -> String {
String.localizedStringWithFormat(
String(localized: "cli.roughdraft.error.unknownFlag", defaultValue: "roughdraft open: unknown flag '%@'. %@"),
flag,
roughdraftOpenUsage()
)
}

private func roughdraftUnexpectedArgumentMessage(_ argument: String) -> String {
String.localizedStringWithFormat(
String(localized: "cli.roughdraft.error.unexpectedArgument", defaultValue: "roughdraft open: unexpected argument '%@'. %@"),
argument,
roughdraftOpenUsage()
)
}

private func roughdraftOpenFailedMessage(status: Int32, timedOut: Bool) -> String {
if timedOut {
return String(localized: "cli.roughdraft.error.openTimedOut", defaultValue: "The Markdown review app did not return a document URL before the timeout. Verify that the required CLI exits promptly, then retry.")
}

return String.localizedStringWithFormat(
String(localized: "cli.roughdraft.error.openFailed", defaultValue: "Failed to open the Markdown review app. Verify that the required CLI is installed and the file path is readable, then retry. Diagnostic: status %lld."),
Int64(status)
)
}

private func runRoughdraftCommand(
commandArgs: [String],
client: SocketClient,
jsonOutput: Bool,
idFormat: CLIIDFormat
) throws {
var args = commandArgs

let (workspaceOpt, argsAfterWorkspace) = parseOption(args, name: "--workspace")
let (windowOpt, argsAfterWindow) = parseOption(argsAfterWorkspace, name: "--window")
let (surfaceOpt, argsAfterSurface) = parseOption(argsAfterWindow, name: "--surface")
let (focusOpt, argsAfterFocus) = parseOption(argsAfterSurface, name: "--focus")
args = argsAfterFocus

let subArgs: [String]
if let first = args.first, first.lowercased() == "open" {
subArgs = Array(args.dropFirst())
} else if args.count == 1, let first = args.first, !first.hasPrefix("-") {
subArgs = [first]
} else {
if let first = args.first, first.hasPrefix("-") {
throw CLIError(message: roughdraftUnknownFlagMessage(first))
} else if let first = args.first, looksLikePath(first) || first.contains(".") {
subArgs = args
} else if let first = args.first {
throw CLIError(message: String.localizedStringWithFormat(
String(localized: "cli.roughdraft.error.unknownSubcommand", defaultValue: "Unknown roughdraft subcommand: %@. Usage: cmux roughdraft open <path>"),
first
))
} else {
subArgs = []
}
}

guard let rawPath = subArgs.first, !rawPath.isEmpty else {
throw CLIError(message: String(localized: "cli.roughdraft.error.requiresPath", defaultValue: "roughdraft open requires a Markdown file path. Usage: cmux roughdraft open <path>"))
}
let trailingArgs = Array(subArgs.dropFirst())
if let unknownFlag = trailingArgs.first(where: { $0.hasPrefix("-") }) {
throw CLIError(message: roughdraftUnknownFlagMessage(unknownFlag))
}
if let extraArg = trailingArgs.first {
throw CLIError(message: roughdraftUnexpectedArgumentMessage(extraArg))
}

let absolutePath = resolvePath(rawPath)
let roughdraftURL = try roughdraftDocumentURL(for: absolutePath)

var params: [String: Any] = [
"url": roughdraftURL,
"bypass_remote_proxy": true,
]
if let surfaceRaw = surfaceOpt {
if let surface = try normalizeSurfaceHandle(surfaceRaw, client: client) {
params["surface_id"] = surface
}
}
let workspaceRaw = workspaceOpt ?? (windowOpt == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
if let workspaceRaw {
if let workspace = try normalizeWorkspaceHandle(workspaceRaw, client: client) {
params["workspace_id"] = workspace
}
}
if let windowRaw = windowOpt {
if let window = try normalizeWindowHandle(windowRaw, client: client) {
params["window_id"] = window
}
}
try applyFocusOption(focusOpt, defaultValue: false, to: &params)

var payload = try client.sendV2(method: "browser.open_split", params: params)
payload["roughdraft_url"] = roughdraftURL
payload["path"] = absolutePath

if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
} else {
let surfaceText = formatHandle(payload, kind: "surface", idFormat: idFormat) ?? "unknown"
let paneText = formatHandle(payload, kind: "pane", idFormat: idFormat) ?? "unknown"
let placement = ((payload["created_split"] as? Bool) == true) ? "split" : "reuse"
print(String.localizedStringWithFormat(
String(localized: "cli.roughdraft.output.opened", defaultValue: "OK surface=%@ pane=%@ placement=%@ path=%@ url=%@"),
surfaceText,
paneText,
placement,
absolutePath,
roughdraftURL
))
}
}

private func roughdraftDocumentURL(for path: String) throws -> String {
let result = runProcess(
executablePath: "/usr/bin/env",
arguments: ["roughdraft", "open", path, "--print-url", "--no-watch"],
timeout: 30
)

guard result.status == 0 else {
throw CLIError(message: roughdraftOpenFailedMessage(status: result.status, timedOut: result.timedOut))
}

let lines = result.stdout.split(whereSeparator: \.isNewline)
.map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
for line in lines {
guard let url = URL(string: line),
let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https",
url.host?.isEmpty == false else {
continue
}
return line
}

throw CLIError(message: String(localized: "cli.roughdraft.error.noDocumentURL", defaultValue: "The Markdown review app did not return a document URL."))
}

private func shouldOpenAsPathArgument(_ arg: String) -> Bool {
if looksLikePath(arg) {
return true
Expand Down Expand Up @@ -5401,6 +5555,7 @@ struct CMUXCLI {
"respawn-pane",
"restore-session",
"right-sidebar",
"roughdraft",
"rpc",
"select-workspace",
"send",
Expand Down Expand Up @@ -11604,14 +11759,14 @@ struct CMUXCLI {
arguments: [String],
stdinText: String? = nil,
timeout: TimeInterval? = nil
) -> (status: Int32, stdout: String, stderr: String) {
) -> (status: Int32, stdout: String, stderr: String, timedOut: Bool) {
let result = CLIProcessRunner.runProcess(
executablePath: executablePath,
arguments: arguments,
stdinText: stdinText,
timeout: timeout
)
return (result.status, result.stdout, result.stderr)
return (result.status, result.stdout, result.stderr, result.timedOut)
}

private func runBrowserCommand(
Expand Down Expand Up @@ -15564,6 +15719,26 @@ struct CMUXCLI {
cmux markdown open ./docs/design.md --workspace 0
cmux markdown open plan.md --direction down
"""
case "roughdraft":
return String(localized: "cli.help.roughdraft", defaultValue: """
Usage: cmux roughdraft open <path> [options]
cmux roughdraft <path> (shorthand for 'open')

Open a local Markdown file in a Markdown review browser pane.
This requires the local `roughdraft` CLI. cmux asks it for a local
document URL without launching an external browser, then opens that
URL in the workspace browser surface.

Options:
--workspace <id|ref|index> Target workspace (default: $CMUX_WORKSPACE_ID)
--surface <id|ref|index> Source surface to split from (default: focused surface)
--window <id|ref|index> Target window
--focus <true|false> Focus the Markdown review browser pane (default: false)

Examples:
cmux roughdraft open plan.md
cmux roughdraft ~/project/README.md --focus true
""")
default:
return nil
}
Expand Down Expand Up @@ -33744,6 +33919,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
display-message [-p|--print] <text>

markdown [open] <path> [--focus <true|false>] (open markdown file in formatted viewer panel with live reload)
roughdraft [open] <path> [--focus <true|false>] (open markdown file in a Markdown review browser pane)
diff [patch-file|-] [--source <unstaged|staged|branch|last-turn>] [--cwd <path>] [--base <ref>] [--focus <true|false>] [--no-focus] [--title <text>] [--layout <split|unified>] [--font-size <points>] (open patch input or git source in a browser split)

browser [--surface <id|ref|index> | <surface>] <subcommand> ...
Expand Down
Loading
Loading