Improve CLI workspace command responsiveness
This commit is contained in:
parent
9c48d18666
commit
a1e05fab84
3 changed files with 167 additions and 28 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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!)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue