Skip to content
Merged
74 changes: 69 additions & 5 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17288,6 +17288,25 @@ struct CMUXCLI {
throw CLIError(message: "Pane has no surface to target")
}

private func tmuxStoredStartCommand(
workspaceId: String,
surfaceId: String,
client: SocketClient
) throws -> String? {
let payload = try client.sendV2(method: "surface.list", params: ["workspace_id": workspaceId])
let surfaces = payload["surfaces"] as? [[String: Any]] ?? []
guard let surface = surfaces.first(where: { ($0["id"] as? String) == surfaceId }) else {
return nil
}
return [
surface["tmux_start_command"],
surface["pane_start_command"],
surface["initial_command"]
]
.compactMap { ($0 as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) }
.first { !$0.isEmpty }
}

private func tmuxResolveSurfaceTarget(
_ raw: String?,
client: SocketClient
Expand Down Expand Up @@ -20187,6 +20206,41 @@ struct CMUXCLI {
"orientation": "vertical"
])

case "respawn-pane", "respawnp":
let parsed = try parseTmuxArguments(
rawArgs,
valueFlags: ["-c", "-t"],
boolFlags: ["-k"]
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
guard parsed.hasFlag("-k") else {
throw CLIError(message: String(
localized: "cli.tmuxCompat.respawnPane.requiresForce",
defaultValue: "respawn-pane requires -k in cmux tmux compatibility mode"
))
}
let target = try tmuxResolveSurfaceTarget(parsed.value("-t"), client: client)
let commandText: String
if let explicitCommand = tmuxStartCommand(commandTokens: parsed.positional) {
commandText = explicitCommand
} else {
commandText = try tmuxStoredStartCommand(
workspaceId: target.workspaceId,
surfaceId: target.surfaceId,
client: client
) ?? "exec ${SHELL:-/bin/sh} -l"
}
var params: [String: Any] = [
"workspace_id": target.workspaceId,
"surface_id": target.surfaceId,
"command": commandText,
"tmux_start_command": commandText
]
if let cwd = parsed.value("-c")?.trimmingCharacters(in: .whitespacesAndNewlines),
!cwd.isEmpty {
params["working_directory"] = resolvePath(cwd)
}
_ = try client.sendV2(method: "surface.respawn", params: params)

case "send-keys", "send":
let parsed = try parseTmuxArguments(rawArgs, valueFlags: ["-t"], boolFlags: ["-l"])
let target = try tmuxResolveSurfaceTarget(parsed.value("-t"), client: client)
Expand Down Expand Up @@ -21120,14 +21174,24 @@ struct CMUXCLI {
let workspaceArg = workspaceOpt ?? (effectiveWindowRaw == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let commandText = (commandOpt ?? respawnRem3.dropFirst(respawnRem3.first == "--" ? 1 : 0).joined(separator: " ")).trimmingCharacters(in: .whitespacesAndNewlines)
let finalCommand = commandText.isEmpty ? "exec ${SHELL:-/bin/zsh} -l" : commandText
var params: [String: Any] = ["text": finalCommand + "\n"]
var params: [String: Any] = [
"command": finalCommand,
"tmux_start_command": finalCommand
]
let winId = try normalizeWindowHandle(effectiveWindowRaw, client: client)
if let winId { params["window_id"] = winId }
let wsId = try normalizeWorkspaceHandle(workspaceArg, client: client, windowHandle: winId, allowCurrent: winId == nil)
let wsHandle = try normalizeWorkspaceHandle(workspaceArg, client: client, windowHandle: winId, allowCurrent: winId == nil)
let wsId = try wsHandle.map { try resolveWorkspaceId($0, client: client, windowHandle: winId) }
if let wsId { params["workspace_id"] = wsId }
let sfId = try normalizeSurfaceHandle(surfaceArg, client: client, workspaceHandle: wsId, windowHandle: winId, allowFocused: true)
if let sfId { params["surface_id"] = sfId }
let payload = try client.sendV2(method: "surface.send_text", params: params)
let sfHandle = try normalizeSurfaceHandle(surfaceArg, client: client, workspaceHandle: wsId, windowHandle: winId, allowFocused: true)
if let sfHandle {
if let wsId {
params["surface_id"] = try resolveSurfaceId(sfHandle, workspaceId: wsId, client: client)
} else {
params["surface_id"] = sfHandle
}
}
let payload = try client.sendV2(method: "surface.respawn", params: params)
printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: "OK")

case "display-message":
Expand Down
136 changes: 136 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -13653,6 +13653,142 @@
}
}
},
"cli.tmuxCompat.respawnPane.requiresForce": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "respawn-pane requires -k in cmux tmux compatibility mode"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "cmux の tmux 互換モードでは respawn-pane に -k が必要です"
}
}
}
},
"rpc.v2.surface.respawn.failed": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Failed to respawn surface"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "サーフェスを再起動できませんでした"
}
}
}
},
"rpc.v2.surface.respawn.invalidFocus": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Missing or invalid focus"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "focus が指定されていないか無効です"
}
}
}
},
"rpc.v2.surface.respawn.noFocusedSurface": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "No focused surface"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "フォーカス中のサーフェスがありません"
}
}
}
},
"rpc.v2.surface.respawn.surfaceNotFoundForId": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Surface not found for the given surface_id"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "指定された surface_id のサーフェスが見つかりません"
}
}
}
},
"rpc.v2.surface.respawn.surfaceNotTerminal": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Surface is not a terminal"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "サーフェスはターミナルではありません"
}
}
}
},
"rpc.v2.surface.respawn.tabManagerUnavailable": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Unable to access the target workspace"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "対象のワークスペースにアクセスできません"
}
}
}
},
"rpc.v2.surface.respawn.workspaceNotFound": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Workspace not found"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペースが見つかりません"
}
}
}
},
"socket.notification.dismissSelectorRequired": {
"extractionState": "manual",
"localizations": {
Expand Down
44 changes: 42 additions & 2 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,23 @@ class GhosttyApp {

DispatchQueue.main.async {
guard let app = AppDelegate.shared else { return }
guard let callbackSurface = callbackContext.terminalSurface else {
#if DEBUG
cmuxDebugLog(
"surface.closeCallback.ignore surface=\(callbackSurfaceId.uuidString.prefix(5)) reason=missingCallbackSurface"
)
#endif
return
}
if let registeredSurface = TerminalSurfaceRegistry.shared.surface(id: callbackSurfaceId),
registeredSurface !== callbackSurface {
#if DEBUG
cmuxDebugLog(
"surface.closeCallback.ignore surface=\(callbackSurfaceId.uuidString.prefix(5)) reason=staleCallbackSurface"
)
#endif
return
}
// Close requests must be resolved by the callback's workspace/surface IDs only.
// If the mapping is already gone (duplicate/stale callback), ignore it.
if let callbackTabId,
Expand Down Expand Up @@ -5127,8 +5144,14 @@ final class TerminalSurfaceRegistry {

func unregister(_ surface: TerminalSurface) {
lock.lock()
let surfaceId = surface.id
surfaces.remove(surface)
surfaceFocusPlacements.removeValue(forKey: surface.id)
let stillRegistered = surfaces.allObjects
.compactMap { $0 as? TerminalSurface }
.contains { $0 !== surface && $0.id == surfaceId }
if !stillRegistered {
surfaceFocusPlacements.removeValue(forKey: surfaceId)
}
lock.unlock()

Task { @MainActor in
Expand Down Expand Up @@ -5338,6 +5361,14 @@ final class TerminalSurface: Identifiable, ObservableObject {
var requestedWorkingDirectory: String? { workingDirectory }
let focusPlacement: TerminalSurfaceFocusPlacement
private var additionalEnvironment: [String: String]
var respawnInitialEnvironmentOverrides: [String: String] {
initialEnvironmentOverrides
}
var respawnAdditionalEnvironment: [String: String] {
var environment = additionalEnvironment
environment.removeValue(forKey: SessionScrollbackReplayStore.environmentKey)
return environment
}
let hostedView: GhosttySurfaceScrollView
private let surfaceView: GhosttyNSView
private var lastPixelWidth: UInt32 = 0
Expand Down Expand Up @@ -5483,6 +5514,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
}

init(
id: UUID = UUID(),
tabId: UUID,
context: ghostty_surface_context_e,
configTemplate: CmuxSurfaceConfigTemplate?,
Expand All @@ -5499,7 +5531,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
dispatchPrecondition(condition: .onQueue(.main))
#endif

self.id = UUID()
self.id = id
self.tabId = tabId
self.surfaceContext = context
self.configTemplate = configTemplate
Expand Down Expand Up @@ -5541,6 +5573,14 @@ final class TerminalSurface: Identifiable, ObservableObject {
}
}

func debugWaitAfterCommand() -> Bool {
configTemplate?.waitAfterCommand ?? false
}

var launchContext: ghostty_surface_context_e {
surfaceContext
}

func updateWorkspaceId(_ newTabId: UUID) {
tabId = newTabId
attachedView?.tabId = newTabId
Expand Down
2 changes: 2 additions & 0 deletions Sources/Panels/TerminalPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ final class TerminalPanel: Panel, ObservableObject {

/// Create a new terminal panel with a fresh surface
convenience init(
id: UUID = UUID(),
workspaceId: UUID,
context: ghostty_surface_context_e = GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: CmuxSurfaceConfigTemplate? = nil,
Expand All @@ -168,6 +169,7 @@ final class TerminalPanel: Panel, ObservableObject {
focusPlacement: TerminalSurfaceFocusPlacement = .workspace
) {
let surface = TerminalSurface(
id: id,
tabId: workspaceId,
context: context,
configTemplate: configTemplate,
Expand Down
Loading
Loading