From a1e05fab84a36dd2da7d07382b1353d30a723feb Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:37:32 -0800 Subject: [PATCH] Improve CLI workspace command responsiveness --- Sources/TerminalController.swift | 158 +++++++++++++++--- Sources/Workspace.swift | 4 +- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 33 ++++ 3 files changed, 167 insertions(+), 28 deletions(-) diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 3fa0d18f..adf25673 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -515,7 +515,11 @@ class TerminalController { let cmd = parts[0].lowercased() let args = parts.count > 1 ? parts[1] : "" - return withSocketCommandPolicy(commandKey: cmd, isV2: false) { + #if DEBUG + let startedAt = ProcessInfo.processInfo.systemUptime + #endif + + let response = withSocketCommandPolicy(commandKey: cmd, isV2: false) { switch cmd { case "ping": return "PONG" @@ -807,6 +811,18 @@ class TerminalController { return "ERROR: Unknown command '\(cmd)'. Use 'help' for available commands." } } + + #if DEBUG + if cmd == "new_workspace" || cmd == "send" || cmd == "send_surface" { + let elapsedMs = (ProcessInfo.processInfo.systemUptime - startedAt) * 1000.0 + let status = response.hasPrefix("OK") ? "ok" : "err" + dlog( + "socket.v1 cmd=\(cmd) status=\(status) ms=\(String(format: "%.2f", elapsedMs)) main=\(Thread.isMainThread ? 1 : 0)" + ) + } + #endif + + return response } // MARK: - V2 JSON Socket Protocol @@ -841,7 +857,11 @@ class TerminalController { v2MainSync { self.v2RefreshKnownRefs() } - return withSocketCommandPolicy(commandKey: method, isV2: true) { + #if DEBUG + let startedAt = ProcessInfo.processInfo.systemUptime + #endif + + let response = withSocketCommandPolicy(commandKey: method, isV2: true) { switch method { case "system.ping": return v2Ok(id: id, result: ["pong": true]) @@ -1181,6 +1201,18 @@ class TerminalController { return v2Error(id: id, code: "method_not_found", message: "Unknown method") } } + + #if DEBUG + if method == "workspace.create" || method == "surface.send_text" { + let elapsedMs = (ProcessInfo.processInfo.systemUptime - startedAt) * 1000.0 + let status = response.contains("\"ok\":true") ? "ok" : "err" + dlog( + "socket.v2 method=\(method) status=\(status) ms=\(String(format: "%.2f", elapsedMs)) main=\(Thread.isMainThread ? 1 : 0)" + ) + } + #endif + + return response } private func v2Capabilities() -> [String: Any] { @@ -1781,10 +1813,20 @@ class TerminalController { } var newId: UUID? + let shouldFocus = v2FocusAllowed() + #if DEBUG + let startedAt = ProcessInfo.processInfo.systemUptime + #endif v2MainSync { - let ws = tabManager.addWorkspace(select: v2FocusAllowed()) + let ws = tabManager.addWorkspace(select: shouldFocus) newId = ws.id } + #if DEBUG + let elapsedMs = (ProcessInfo.processInfo.systemUptime - startedAt) * 1000.0 + dlog( + "socket.workspace.create focus=\(shouldFocus ? 1 : 0) ms=\(String(format: "%.2f", elapsedMs)) main=\(Thread.isMainThread ? 1 : 0)" + ) + #endif guard let newId else { return .err(code: "internal_error", message: "Failed to create workspace", data: nil) @@ -3109,19 +3151,23 @@ class TerminalController { result = .err(code: "invalid_params", message: "Surface is not a terminal", data: ["surface_id": surfaceId.uuidString]) return } + #if DEBUG + let waitStart = ProcessInfo.processInfo.systemUptime + #endif guard let surface = waitForTerminalSurface(terminalPanel, waitUpTo: 2.0) else { result = .err(code: "internal_error", message: "Surface not ready", data: ["surface_id": surfaceId.uuidString]) return } - - for char in text { - if char.unicodeScalars.count == 1, - let scalar = char.unicodeScalars.first, - handleControlScalar(scalar, surface: surface) { - continue - } - sendTextEvent(surface: surface, text: String(char)) + #if DEBUG + let waitMs = (ProcessInfo.processInfo.systemUptime - waitStart) * 1000.0 + if waitMs >= 8 { + dlog( + "socket.surface.send_text.wait_surface workspace=\(ws.id.uuidString.prefix(8)) surface=\(surfaceId.uuidString.prefix(8)) ms=\(String(format: "%.2f", waitMs))" + ) } + #endif + + sendSocketText(text, surface: surface) // Ensure we present a new frame after injecting input so snapshot-based tests (and // socket-driven agents) can observe the updated terminal without requiring a focus // change to trigger a draw. @@ -8753,10 +8799,19 @@ class TerminalController { var newTabId: UUID? let focus = socketCommandAllowsInAppFocusMutations() + #if DEBUG + let startedAt = ProcessInfo.processInfo.systemUptime + #endif DispatchQueue.main.sync { let workspace = tabManager.addTab(select: focus) newTabId = workspace.id } + #if DEBUG + let elapsedMs = (ProcessInfo.processInfo.systemUptime - startedAt) * 1000.0 + dlog( + "socket.new_workspace focus=\(focus ? 1 : 0) ms=\(String(format: "%.2f", elapsedMs)) main=\(Thread.isMainThread ? 1 : 0)" + ) + #endif return "OK \(newTabId?.uuidString ?? "unknown")" } @@ -9741,6 +9796,69 @@ class TerminalController { sendKeyEvent(surface: surface, keycode: 0, text: text) } + enum SocketTextChunk: Equatable { + case text(String) + case control(UnicodeScalar) + } + + nonisolated static func socketTextChunks(_ text: String) -> [SocketTextChunk] { + guard !text.isEmpty else { return [] } + + var chunks: [SocketTextChunk] = [] + chunks.reserveCapacity(8) + var bufferedText = "" + bufferedText.reserveCapacity(text.count) + + func flushBufferedText() { + guard !bufferedText.isEmpty else { return } + chunks.append(.text(bufferedText)) + bufferedText.removeAll(keepingCapacity: true) + } + + for scalar in text.unicodeScalars { + if isSocketControlScalar(scalar) { + flushBufferedText() + chunks.append(.control(scalar)) + } else { + bufferedText.unicodeScalars.append(scalar) + } + } + flushBufferedText() + return chunks + } + + private nonisolated static func isSocketControlScalar(_ scalar: UnicodeScalar) -> Bool { + switch scalar.value { + case 0x0A, 0x0D, 0x09, 0x1B, 0x7F: + return true + default: + return false + } + } + + private func sendSocketText(_ text: String, surface: ghostty_surface_t) { + let chunks = Self.socketTextChunks(text) + #if DEBUG + let startedAt = ProcessInfo.processInfo.systemUptime + #endif + for chunk in chunks { + switch chunk { + case .text(let value): + sendTextEvent(surface: surface, text: value) + case .control(let scalar): + _ = handleControlScalar(scalar, surface: surface) + } + } + #if DEBUG + let elapsedMs = (ProcessInfo.processInfo.systemUptime - startedAt) * 1000.0 + if elapsedMs >= 8 || chunks.count > 1 { + dlog( + "socket.send_text.inject chars=\(text.count) chunks=\(chunks.count) ms=\(String(format: "%.2f", elapsedMs))" + ) + } + #endif + } + private func handleControlScalar(_ scalar: UnicodeScalar, surface: ghostty_surface_t) -> Bool { switch scalar.value { case 0x0A, 0x0D: @@ -9859,14 +9977,7 @@ class TerminalController { .replacingOccurrences(of: "\\r", with: "\r") .replacingOccurrences(of: "\\t", with: "\t") - for char in unescaped { - if char.unicodeScalars.count == 1, - let scalar = char.unicodeScalars.first, - handleControlScalar(scalar, surface: surface) { - continue - } - sendTextEvent(surface: surface, text: String(char)) - } + sendSocketText(unescaped, surface: surface) success = true } if let error { return error } @@ -9890,14 +10001,7 @@ class TerminalController { .replacingOccurrences(of: "\\r", with: "\r") .replacingOccurrences(of: "\\t", with: "\t") - for char in unescaped { - if char.unicodeScalars.count == 1, - let scalar = char.unicodeScalars.first, - handleControlScalar(scalar, surface: surface) { - continue - } - sendTextEvent(surface: surface, text: String(char)) - } + sendSocketText(unescaped, surface: surface) success = true } diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index d839dd75..17d9654c 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -335,7 +335,9 @@ final class Workspace: Identifiable, ObservableObject { // Configure bonsplit with keepAllAlive to preserve terminal state // and keep split entry instantaneous. - let appearance = Self.bonsplitAppearance(from: GhosttyConfig.load()) + // Avoid re-reading/parsing Ghostty config on every new workspace; this hot path + // runs for socket/CLI workspace creation and can cause visible typing lag. + let appearance = Self.bonsplitAppearance(from: GhosttyApp.shared.defaultBackgroundColor) let config = BonsplitConfiguration( allowSplits: true, allowCloseTabs: true, diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 0864f9d5..6d34b8b5 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -3524,3 +3524,36 @@ final class TerminalControllerSidebarDedupeTests: XCTestCase { ) } } + +final class TerminalControllerSocketTextChunkTests: XCTestCase { + func testSocketTextChunksReturnsSingleChunkForPlainText() { + XCTAssertEqual( + TerminalController.socketTextChunks("echo hello"), + [.text("echo hello")] + ) + } + + func testSocketTextChunksSplitsControlScalars() { + XCTAssertEqual( + TerminalController.socketTextChunks("abc\rdef\tghi"), + [ + .text("abc"), + .control("\r".unicodeScalars.first!), + .text("def"), + .control("\t".unicodeScalars.first!), + .text("ghi") + ] + ) + } + + func testSocketTextChunksDoesNotEmitEmptyTextChunksAroundConsecutiveControls() { + XCTAssertEqual( + TerminalController.socketTextChunks("\r\n\t"), + [ + .control("\r".unicodeScalars.first!), + .control("\n".unicodeScalars.first!), + .control("\t".unicodeScalars.first!) + ] + ) + } +}