Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
5269a5a
Test unified surface selection reads
austinywang Aug 12, 2026
932782b
Implement unified surface selection reads
austinywang Aug 12, 2026
2f6ba70
Harden surface selection contracts
austinywang Aug 12, 2026
3caaad1
Use modern SwiftUI change callback
austinywang Aug 12, 2026
64428da
Fix read-selection RPC example
austinywang Aug 12, 2026
8c48965
Mount WebKit selection behavior fixture
austinywang Aug 12, 2026
8f87781
Retain WebKit selections across focus handoff
austinywang Aug 12, 2026
47144ca
Track WebKit selection in the page world
austinywang Aug 12, 2026
6389dd0
Preserve WebKit selections across native focus
austinywang Aug 12, 2026
8af3343
Treat WebKit focus reconciliation as observational
austinywang Aug 12, 2026
d8d907f
Exercise WebKit selection callbacks in page world
austinywang Aug 12, 2026
e977f30
Probe WebKit selection tracker state
austinywang Aug 12, 2026
79ee7df
Make WebKit selection reads snapshot-only
austinywang Aug 12, 2026
4dbee45
Preserve browser navigation lifecycle in selection test
austinywang Aug 12, 2026
5e76a32
Use local URL for browser selection fixture
austinywang Aug 12, 2026
a6e6b68
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Aug 12, 2026
3cf798d
Stabilize browser portal layout assertions
austinywang Aug 12, 2026
ecc564d
Test stale browser selection invalidation
austinywang Aug 25, 2026
4880365
Invalidate stale selections and avoid topology sweeps
austinywang Aug 25, 2026
98c3dc6
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Aug 25, 2026
866806e
Keep current Ghostty and Bonsplit revisions
austinywang Aug 25, 2026
cf10d6b
Bound selection invalidation and polling
austinywang Aug 25, 2026
927ed85
Harden async and relayed selection reads
austinywang Aug 25, 2026
255cb20
Make selection timeout and RPC routing safe
austinywang Aug 25, 2026
def4539
Bound terminal and raw RPC selection reads
austinywang Aug 25, 2026
a022bcd
Bound shared selection snapshots
austinywang Aug 25, 2026
802370a
Test immutable WebKit selection bridge
austinywang Aug 25, 2026
024b7cf
Harden WebKit selection lifecycle and deadlines
austinywang Aug 25, 2026
cecaf84
Harden selection bridge and terminal capture
austinywang Aug 26, 2026
03984fd
Bound WebKit selection extraction
austinywang Aug 26, 2026
ce4df2f
Serialize terminal selection reads with teardown
austinywang Aug 26, 2026
ef89d3d
Harden selection clearing and Unicode bounds
austinywang Aug 26, 2026
d58f9ca
Harden selection routing and WebKit deadlines
austinywang Aug 26, 2026
65aee70
Split surface selection test support
austinywang Aug 26, 2026
577cbb2
Bound selection evaluation and relay ownership
austinywang Aug 26, 2026
efac590
Close canceled selection evaluation race
austinywang Aug 26, 2026
6ead2e4
Fix selection timeout script type reference
austinywang Aug 26, 2026
a67ecf6
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Aug 27, 2026
60aa8ff
Align Bonsplit revision with current main APIs
austinywang Aug 27, 2026
c0ce9d8
Harden merged surface selection lifecycle
austinywang Aug 27, 2026
e7a9e13
Return WebKit selection evaluation results
austinywang Aug 28, 2026
9d923ad
Fix WebKit selection script indentation
austinywang Aug 28, 2026
bc8011d
Stabilize browser selection fixture focus transitions
austinywang Aug 28, 2026
f464766
Merge current main and preserve selection test wiring
austinywang Aug 28, 2026
fd87a8b
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
e5655dd
Merge origin/main into issue-10020-surface-selection-context
austinywang Aug 28, 2026
4bb044b
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
c880b8f
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
6df52d4
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
19b4ccb
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
54ffd3e
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
e0102e2
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
61b39e7
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
58fa01d
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
144afc5
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
cb504dd
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 28, 2026
d284ac4
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 1, 2026
b36a0bb
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 1, 2026
073aded
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 1, 2026
15830fc
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 1, 2026
1b4a36a
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 1, 2026
7780bd7
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 1, 2026
afbb797
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 2, 2026
3111744
Preserve browser selections across native focus handoff
austinywang Sep 2, 2026
afba5a1
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 2, 2026
b04a102
Make focus handoff regression assertion environment independent
austinywang Sep 2, 2026
728b8e2
Give CLI routing tests a bounded cold-start margin
austinywang Sep 2, 2026
f59a6ab
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 2, 2026
0f1e8aa
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 2, 2026
b43dcdf
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 2, 2026
b7d7eb3
Allow more headroom for hosted CLI routing tests
austinywang Sep 2, 2026
abdae7b
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 2, 2026
f34cd66
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 2, 2026
3593e6c
Merge remote-tracking branch 'origin/main' into issue-10020-surface-s…
austinywang Sep 2, 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
1 change: 1 addition & 0 deletions CLI/CMUXCLI+CommandSuggestions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ extension CMUXCLI {
"popup",
"previous-window",
"read-screen",
"read-selection",
"refresh-surfaces",
"reload-config",
"remote-daemon-status",
Expand Down
217 changes: 217 additions & 0 deletions CLI/CMUXCLI+SurfaceSelection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import Foundation

extension CMUXCLI {
func runSurfaceSelectionCommand(
commandName: String,
commandArgs: [String],
client: SocketClient,
jsonOutput: Bool,
windowOverride: String?,
includeContextInPlainOutput: Bool
) throws {
let (workspaceOption, remainingAfterWorkspace) = parseOption(
commandArgs,
name: "--workspace"
)
let (surfaceOption, remainingAfterSurface) = parseOption(
remainingAfterWorkspace,
name: "--surface"
)
let (windowOption, trailing) = parseOption(
remainingAfterSurface,
name: "--window"
)
guard trailing.isEmpty else {
throw CLIError(message: String(
format: String(
localized: "cli.readSelection.error.unexpectedArguments",
defaultValue: "%@: unexpected arguments: %@"
),
commandName,
trailing.joined(separator: " ")
))
}

let windowRaw = windowOption ?? windowOverride
let workspaceRaw = workspaceOption
?? Self.callerWorkspaceForSurfaceHandle(surfaceOption, windowRaw: windowRaw)
let surfaceRaw = surfaceOption
?? (workspaceOption == nil && windowRaw == nil
? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"]
: nil)

var params: [String: Any] = [:]
let windowID = try normalizeWindowHandle(windowRaw, client: client)
if let windowID {
params["window_id"] = windowID
}
let workspaceID = try normalizeWorkspaceHandle(
workspaceRaw,
client: client,
windowHandle: windowID
)
if let workspaceID {
params["workspace_id"] = workspaceID
}
let surfaceID = try normalizeSurfaceHandle(
surfaceRaw,
client: client,
workspaceHandle: workspaceID,
windowHandle: windowID
)
if let surfaceID {
params["surface_id"] = surfaceID
}

let payload = try client.sendV2(
method: "surface.read_selection",
params: params
)
if jsonOutput {
print(jsonString(payload))
return
}
guard (payload["has_selection"] as? Bool) == true else {
if includeContextInPlainOutput {
let metadata = surfaceSelectionMetadataLines(payload)
if !metadata.isEmpty {
print(metadata.joined(separator: "\n"))
print("")
}
}
print(String(
localized: "cli.readSelection.output.noActiveSelection",
defaultValue: "Has selection: false"
))
return
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let text = (payload["text"] as? String) ?? ""
guard includeContextInPlainOutput else {
print(text)
return
}

let metadata = surfaceSelectionMetadataLines(payload)
if !metadata.isEmpty {
print(metadata.joined(separator: "\n"))
print("")
}
print(text)
}

private func surfaceSelectionMetadataLines(
_ payload: [String: Any]
) -> [String] {
var lines: [String] = []
if let kind = payload["kind"] as? String, !kind.isEmpty {
lines.append(String(
format: String(
localized: "cli.readSelection.output.kind",
defaultValue: "Kind: %@"
),
kind
))
}
if let filePath = payload["file_path"] as? String, !filePath.isEmpty {
lines.append(String(
format: String(
localized: "cli.readSelection.output.file",
defaultValue: "File: %@"
),
filePath
))
}
if let range = payload["line_range"] as? [String: Any],
let start = surfaceSelectionLineNumber(range["start"]),
let end = surfaceSelectionLineNumber(range["end"]) {
if start == end {
lines.append(String(
format: String(
localized: "cli.readSelection.output.line",
defaultValue: "Line: %lld"
),
Int64(start)
))
} else {
lines.append(String(
format: String(
localized: "cli.readSelection.output.lines",
defaultValue: "Lines: %lld-%lld"
),
Int64(start),
Int64(end)
))
}
}
if let url = payload["url"] as? String, !url.isEmpty {
lines.append(String(
format: String(
localized: "cli.readSelection.output.url",
defaultValue: "URL: %@"
),
url
))
}
return lines
}

private func surfaceSelectionLineNumber(_ value: Any?) -> Int? {
if let value = value as? Int {
return value
}
return (value as? NSNumber)?.intValue
}

static var readSelectionHelp: String {
String(localized: "cli.help.readSelection", defaultValue: """
Usage: cmux read-selection [flags]

Read the active selection from any selectable surface. Plain output includes source context; --json returns the complete response.

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

Example:
cmux read-selection --surface surface:2
cmux read-selection --surface surface:2 --json
""")
}

static var readScreenHelp: String {
String(localized: "cli.help.readScreen", defaultValue: """
Usage: cmux read-screen [flags]

Read terminal text from a surface as plain text.

Flags:
--workspace <id|ref|index> Target workspace (default: $CMUX_WORKSPACE_ID)
--surface <id|ref|index> Target surface (default: $CMUX_SURFACE_ID)
--window <id|ref|index> Window context for workspace/surface refs and indexes
--scrollback Include scrollback (not just visible viewport)
--lines <n> Limit to the last n lines (implies --scrollback)
--selection Read only the active selection; cannot be combined with --scrollback or --lines

Example:
cmux read-screen
cmux read-screen --surface surface:2 --scrollback --lines 200
cmux read-screen --surface surface:2 --selection
""")
}

static var readSelectionUsageLine: String {
String(
localized: "cli.usage.readSelection",
defaultValue: "read-selection [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>]"
)
}

static var readScreenUsageLine: String {
String(
localized: "cli.usage.readScreen",
defaultValue: "read-screen [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] [--scrollback] [--lines <n>] [--selection]"
)
}
}
72 changes: 54 additions & 18 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4861,6 +4861,14 @@ struct CMUXCLI {
if normalizedCommand == "restore" {
return false
}
if normalizedCommand == "read-screen" || normalizedCommand == "read-selection" {
return false
}
if normalizedCommand == "rpc",
commandArgs.first?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
== "surface.read_selection" {
return false
}
if normalizedCommand == "surface", commandArgs.first?.lowercased() == "resume" {
return false
}
Expand Down Expand Up @@ -6617,7 +6625,14 @@ struct CMUXCLI {
!method.isEmpty else {
throw CLIError(message: "Usage: cmux rpc <method> [json-params]")
}
let params = try parseRPCParams(Array(commandArgs.dropFirst()))
var params = try parseRPCParams(Array(commandArgs.dropFirst()))
if method.lowercased() == "surface.read_selection",
let windowId,
params["window_id"] == nil || params["window_id"] is NSNull {
// Selection reads must not focus the target window, but an
// explicit global --window still constrains routing to it.
params["window_id"] = try normalizeWindowHandle(windowId, client: client) ?? windowId
}
let response = try client.sendV2(method: method, params: params)
let output: Any = idFormatArg == nil ? response : formatIDs(response, mode: idFormat)
print(jsonString(output))
Expand Down Expand Up @@ -7356,7 +7371,40 @@ struct CMUXCLI {
print(handle)
}

case "read-selection":
try runSurfaceSelectionCommand(
commandName: "read-selection",
commandArgs: commandArgs,
client: client,
jsonOutput: jsonOutput,
windowOverride: windowId,
includeContextInPlainOutput: true
)

case "read-screen":
let selectionOnly = commandArgs.contains("--selection")
if selectionOnly {
let hasScrollback = commandArgs.contains("--scrollback")
let hasLines = commandArgs.contains {
$0 == "--lines" || $0.hasPrefix("--lines=")
}
if hasScrollback || hasLines {
throw CLIError(message: String(
localized: "cli.readSelection.error.readScreenConflict",
defaultValue: "read-screen: --selection cannot be combined with --scrollback or --lines"
))
}
try runSurfaceSelectionCommand(
commandName: "read-screen",
commandArgs: commandArgs.filter { $0 != "--selection" },
client: client,
jsonOutput: jsonOutput,
windowOverride: windowId,
includeContextInPlainOutput: false
)
break
}

let (wsArg, rem0) = parseOption(commandArgs, name: "--workspace")
let (sfArg, rem1) = parseOption(rem0, name: "--surface")
let (windowOpt, rem2) = parseOption(rem1, name: "--window")
Expand Down Expand Up @@ -20027,23 +20075,10 @@ struct CMUXCLI {
Flags:
-p, --print Print to stdout only
"""
case "read-selection":
return Self.readSelectionHelp
case "read-screen":
return """
Usage: cmux read-screen [flags]

Read terminal text from a surface as plain text.

Flags:
--workspace <id|ref|index> Target workspace (default: $CMUX_WORKSPACE_ID)
--surface <id|ref|index> Target surface (default: $CMUX_SURFACE_ID)
--window <id|ref|index> Window context for workspace/surface refs and indexes
--scrollback Include scrollback (not just visible viewport)
--lines <n> Limit to the last n lines (implies --scrollback)

Example:
cmux read-screen
cmux read-screen --surface surface:2 --scrollback --lines 200
"""
return Self.readScreenHelp
case "send":
return """
Usage: cmux send [flags] [--] <text>
Expand Down Expand Up @@ -40981,7 +41016,8 @@ export default CMUXSessionRestore;
rename-workspace [--workspace <id|ref|index>] [--window <id|ref|index>] <title>
rename-window [--workspace <id|ref|index>] [--window <id|ref|index>] <title>
current-workspace [--window <id|ref|index>]
read-screen [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] [--scrollback] [--lines <n>]
\(Self.readSelectionUsageLine)
\(Self.readScreenUsageLine)
send [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] <text>
send-key [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] <key>
send-panel --panel <id|ref|index> [--workspace <id|ref|index>] [--window <id|ref|index>] <text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,21 @@ public final class ControlCommandCoordinator {
handles.removeRef(kind: kind, uuid: uuid)
}

/// Returns whether an opaque-handle topology refresh is needed.
public var needsHandleTopologyRefresh: Bool {
handles.needsTopologyRefresh
}

/// Records completion of the current opaque-handle topology refresh.
public func markHandleTopologyRefreshCompleted() {
handles.markTopologyRefreshCompleted()
}

/// Reopens opaque-handle refresh after an external topology mutation.
public func invalidateHandleTopologyRefresh() {
handles.invalidateTopologyRefresh()
}

// MARK: - Wire helpers

/// The `kind:N` ref for an optional id as a JSON value: the ref string, or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ extension ControlCommandExecutionPolicy {
"surface.list",
"surface.current",
"surface.read_text",
"surface.read_selection",
"pane.list",
"pane.surfaces",
"list_windows",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ public enum ControlCommandExecutionPolicy: Sendable, Equatable {
// never runs inline on the main thread, and no in-process main-thread
// caller needs it.
"surface.read_text",
// Selection providers own AppKit/WebKit state on the main actor, then
// return one immutable snapshot for response shaping on this worker.
// The async bridge must never be entered inline by a main-thread caller.
"surface.read_selection",
// The surface catalog verbs await main-actor catalog work that can sit on the
// network (a cloud provider materializing a pane); like `vm.*` they park the
// worker instead of holding the main actor.
Expand Down
Loading