Skip to content
84 changes: 83 additions & 1 deletion CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4584,7 +4584,7 @@ struct CMUXCLI {
var surfaceRaw = surfaceOpt
var args = argsWithoutSurfaceFlag

let verbsWithoutSurface: Set<String> = ["open", "open-split", "new", "identify"]
let verbsWithoutSurface: Set<String> = ["open", "open-split", "new", "identify", "agent-session"]
if surfaceRaw == nil, let first = args.first {
if !first.hasPrefix("-") && !verbsWithoutSurface.contains(first.lowercased()) {
surfaceRaw = first
Expand Down Expand Up @@ -5839,6 +5839,88 @@ struct CMUXCLI {
return
}

// Agent session commands
func agentCallerContext() -> [String: Any] {
var caller: [String: Any] = [:]
if let sid = ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"] {
caller["surface_id"] = sid
}
if let wid = ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] {
caller["workspace_id"] = wid
}
return caller
}

if subcommand == "agent-session" {
guard let verb = subArgs.first?.lowercased() else {
throw CLIError(message: "browser agent-session requires: open|tab|dispose|list")
}
let verbArgs = Array(subArgs.dropFirst())

switch verb {
case "open":
guard let profileId = optionValue(verbArgs, name: "--profile") else {
throw CLIError(message: "browser agent-session open requires --profile <uuid>")
}
let url = optionValue(verbArgs, name: "--url")
let paneId = optionValue(verbArgs, name: "--pane")
var params: [String: Any] = ["profile_id": profileId]
if let url { params["url"] = url }
if let paneId { params["pane_id"] = paneId }
params["caller"] = agentCallerContext()
let payload = try client.sendV2(method: "browser.agent_session.open", params: params)
output(payload, fallback: "OK")
return

case "tab":
guard let sessionId = optionValue(verbArgs, name: "--session") else {
throw CLIError(message: "browser agent-session tab requires --session <uuid>")
}
let url = optionValue(verbArgs, name: "--url")
let paneId = optionValue(verbArgs, name: "--pane")
var params: [String: Any] = ["session_id": sessionId]
if let url { params["url"] = url }
if let paneId { params["pane_id"] = paneId }
params["caller"] = agentCallerContext()
let payload = try client.sendV2(method: "browser.agent_session.tab", params: params)
output(payload, fallback: "OK")
return

case "dispose":
guard let sessionId = optionValue(verbArgs, name: "--session") else {
throw CLIError(message: "browser agent-session dispose requires --session <uuid>")
}
var params: [String: Any] = ["session_id": sessionId]
params["caller"] = agentCallerContext()
let payload = try client.sendV2(method: "browser.agent_session.dispose", params: params)
output(payload, fallback: "OK")
return

case "list":
var params: [String: Any] = [:]
params["caller"] = agentCallerContext()
let payload = try client.sendV2(method: "browser.agent_session.list", params: params)
if effectiveJSONOutput {
print(jsonString(formatIDs(payload, mode: effectiveIDFormat)))
} else if let sessions = payload["sessions"] as? [[String: Any]] {
if sessions.isEmpty {
print("No active agent sessions")
} else {
for s in sessions {
let sid = s["session_id"] as? String ?? "?"
let profile = s["profile_name"] as? String ?? "?"
let tabs = s["tab_count"] as? Int ?? 0
print("\(sid) profile=\(profile) tabs=\(tabs)")
}
}
}
return

default:
throw CLIError(message: "Unknown agent-session verb: \(verb). Use: open|tab|dispose|list")
}
}

throw CLIError(message: "Unsupported browser subcommand: \(subcommand)")
}

Expand Down
12 changes: 12 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
A5001400 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001410 /* Panel.swift */; };
A5001401 /* TerminalPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001411 /* TerminalPanel.swift */; };
A5001402 /* BrowserPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001412 /* BrowserPanel.swift */; };
B3A91002 /* BrowserAgentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A91001 /* BrowserAgentSession.swift */; };
B3A91004 /* BrowserAgentSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A91003 /* BrowserAgentSessionStore.swift */; };
A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; };
A5001404 /* BrowserPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001414 /* BrowserPanelView.swift */; };
A5007420 /* BrowserPopupWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5007421 /* BrowserPopupWindowController.swift */; };
Expand Down Expand Up @@ -110,6 +112,7 @@
A5001623 /* cmux.sdef in Resources */ = {isa = PBXBuildFile; fileRef = A5001622 /* cmux.sdef */; };
E12E88F82733EC42F32C36A3 /* BrowserConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */; };
1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */; };
B3A91006B3A91006B3A91006 /* BrowserAgentSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A91005B3A91005B3A91005 /* BrowserAgentSessionTests.swift */; };
46F6AC15863EC84DCD3770A2 /* TerminalAndGhosttyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */; };
6B524A0BA34FD46A771335AB /* WorkspaceUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F8ED91A4B55D34BE6A0668 /* WorkspaceUnitTests.swift */; };
063BC42CEE257D6213A2E30C /* WindowAndDragTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */; };
Expand Down Expand Up @@ -204,6 +207,8 @@
A5001410 /* Panel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/Panel.swift; sourceTree = "<group>"; };
A5001411 /* TerminalPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanel.swift; sourceTree = "<group>"; };
A5001412 /* BrowserPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPanel.swift; sourceTree = "<group>"; };
B3A91001 /* BrowserAgentSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserAgentSession.swift; sourceTree = "<group>"; };
B3A91003 /* BrowserAgentSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserAgentSessionStore.swift; sourceTree = "<group>"; };
A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = "<group>"; };
A5001414 /* BrowserPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPanelView.swift; sourceTree = "<group>"; };
A5007421 /* BrowserPopupWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPopupWindowController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -279,6 +284,7 @@
A5001622 /* cmux.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = cmux.sdef; sourceTree = "<group>"; };
970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserConfigTests.swift; sourceTree = "<group>"; };
58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPanelTests.swift; sourceTree = "<group>"; };
B3A91005B3A91005B3A91005 /* BrowserAgentSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserAgentSessionTests.swift; sourceTree = "<group>"; };
02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalAndGhosttyTests.swift; sourceTree = "<group>"; };
71F8ED91A4B55D34BE6A0668 /* WorkspaceUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceUnitTests.swift; sourceTree = "<group>"; };
BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAndDragTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -434,6 +440,8 @@
A5001410 /* Panel.swift */,
A5001411 /* TerminalPanel.swift */,
A5001412 /* BrowserPanel.swift */,
B3A91001 /* BrowserAgentSession.swift */,
B3A91003 /* BrowserAgentSessionStore.swift */,
A5001413 /* TerminalPanelView.swift */,
A5001414 /* BrowserPanelView.swift */,
A5007421 /* BrowserPopupWindowController.swift */,
Expand Down Expand Up @@ -534,6 +542,7 @@
A5008382 /* CommandPaletteSearchEngineTests.swift */,
970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */,
58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */,
B3A91005B3A91005B3A91005 /* BrowserAgentSessionTests.swift */,
02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */,
71F8ED91A4B55D34BE6A0668 /* WorkspaceUnitTests.swift */,
BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */,
Expand Down Expand Up @@ -729,6 +738,8 @@
A5001400 /* Panel.swift in Sources */,
A5001401 /* TerminalPanel.swift in Sources */,
A5001402 /* BrowserPanel.swift in Sources */,
B3A91002 /* BrowserAgentSession.swift in Sources */,
B3A91004 /* BrowserAgentSessionStore.swift in Sources */,
A5001403 /* TerminalPanelView.swift in Sources */,
A5001404 /* BrowserPanelView.swift in Sources */,
A5007420 /* BrowserPopupWindowController.swift in Sources */,
Expand Down Expand Up @@ -797,6 +808,7 @@
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */,
E12E88F82733EC42F32C36A3 /* BrowserConfigTests.swift in Sources */,
1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */,
B3A91006B3A91006B3A91006 /* BrowserAgentSessionTests.swift in Sources */,
46F6AC15863EC84DCD3770A2 /* TerminalAndGhosttyTests.swift in Sources */,
6B524A0BA34FD46A771335AB /* WorkspaceUnitTests.swift in Sources */,
063BC42CEE257D6213A2E30C /* WindowAndDragTests.swift in Sources */,
Expand Down
7 changes: 7 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2326,6 +2326,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
NSWindow.allowsAutomaticWindowTabbing = false
disableNativeTabbingShortcut()
ensureApplicationIcon()

// Garbage-collect orphaned agent session data stores left by unclean shutdown.
#if canImport(WebKit)
Task { @MainActor in
await BrowserAgentSessionStore.shared.garbageCollect()
}
#endif
if !isRunningUnderXCTest {
configureUserNotifications()
installMenuBarVisibilityObserver()
Expand Down
30 changes: 30 additions & 0 deletions Sources/Panels/BrowserAgentSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

/// Represents one agent's isolated browser session against a specific profile.
///
/// Each `BrowserAgentSession` maps an agent terminal surface to a cloned
/// `WKWebsiteDataStore` derived from a source `BrowserProfileDefinition`.
/// Multiple agents sharing the same imported profile each get their own
/// session with independent cookie state (clone-on-first-use).
///
/// ```
/// Agent -> BrowserAgentSession -> BrowserProfileDefinition -> WKWebsiteDataStore
/// ```
struct BrowserAgentSession: Codable, Identifiable, Sendable {
/// Unique identifier for this agent session.
let id: UUID

/// Internal UUID of the agent's terminal surface that owns this session.
let agentSurfaceUUID: UUID

/// The original imported profile this session was cloned from.
let sourceProfileId: UUID

/// Agent-specific `WKWebsiteDataStore` identifier (used with
/// `WKWebsiteDataStore(forIdentifier:)`). Cookies from `sourceProfileId`
/// are cloned into this store on first use.
let dataStoreId: UUID

/// When this session was created.
let createdAt: Date
}
Loading
Loading