From e2832ae1fe75b47199ee9be98ae9835ff0f6adfa Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Fri, 12 Jun 2026 20:13:50 +0200 Subject: [PATCH] macOS: get latest TerminalEntity's kind, title & pwd This fixes these being wrong after creating a terminal window for the first time. --- macos/Sources/App/macOS/AppDelegate.swift | 8 +++ .../App Intents/Entities/TerminalEntity.swift | 67 +++++++++++++++++++ .../App Intents/NewTerminalIntent.swift | 6 +- .../App Intents/QuickTerminalIntent.swift | 14 +++- 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 9700a31ae..5003e3d32 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -104,6 +104,14 @@ class AppDelegate: NSObject, /// The current state of the quick terminal. private var quickTerminalControllerState: QuickTerminalState = .uninitialized + /// Whether the quick terminal has already been initialized. + var quickControllerInitialized: Bool { + if case .initialized = quickTerminalControllerState { + return true + } + return false + } + /// Our quick terminal. This starts out uninitialized and only initializes if used. var quickController: QuickTerminalController { switch quickTerminalControllerState { diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index 1a0a0acb5..63d05cd32 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -1,6 +1,13 @@ import AppKit import AppIntents +import Combine import SwiftUI +import os + +private let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: "AppIntents.TerminalEntity" +) struct TerminalEntity: AppEntity { let id: UUID @@ -68,6 +75,64 @@ struct TerminalEntity: AppEntity { self.kind = .normal } } + + /// Wait for the surface to be updated then create an entity + /// + /// The PTY/Config sets the title and pwd asynchronously shortly after the + /// surface is created, so concurrently wait for the second published + /// value of each (the first is the current value) before returning. + /// + /// If a value never arrives, the timeout completes the publisher and + /// we fall back to the current value. + /// + /// Waiting for the title and pwd also gives the SurfaceView time to lay + /// out, so the screenshot we capture afterwards reflects the rendered view. + @MainActor + init(view: Ghostty.SurfaceView) async { + self.id = view.id + self.tty = view.surfaceModel?.ttyName + + let waitTimeout = DispatchQueue.SchedulerTimeType.Stride.seconds(1) + let titleValues = view.$title.dropFirst() + .setFailureType(to: Error.self) + .timeout(waitTimeout, scheduler: DispatchQueue.main, customError: { EntityTimeoutError() }) + .handleEvents(receiveCompletion: { completion in + if case .failure = completion { + logger.error("failed to get terminal's title: timeout") + } + }) + .replaceError(with: view.title) + .values + let pwdValues = view.$pwd.dropFirst() + .setFailureType(to: Error.self) + .timeout(waitTimeout, scheduler: DispatchQueue.main, customError: { EntityTimeoutError() }) + .handleEvents(receiveCompletion: { completion in + if case .failure = completion { + logger.error("failed to get terminal's pwd: timeout") + } + }) + .replaceError(with: view.pwd) + .values + async let title = titleValues.first(where: { _ in true }) + async let pwd = pwdValues.first(where: { _ in true }) + + self.title = await title ?? "" + self.workingDirectory = await pwd ?? "" + + // Wait for the title and pwd then get latest pid and screenshots. + // This should gave SurfaceView enough time to layout in the window and we can get the most recent process's PID + // Determine the kind based on the window controller type + if view.window?.windowController is QuickTerminalController { + self.kind = .quick + } else { + self.kind = .normal + } + + self.pid = view.surfaceModel?.foregroundPID + if let nsImage = ImageRenderer(content: view.screenshot()).nsImage { + self.screenshot = nsImage + } + } } extension TerminalEntity { @@ -127,3 +192,5 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { } } } + +private struct EntityTimeoutError: Error {} diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index cab7fd262..35f53169f 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -112,7 +112,7 @@ struct NewTerminalIntent: AppIntent { withBaseConfig: config, withParent: parent?.window) if let view = newController.surfaceTree.root?.leftmostLeaf() { - return .result(value: TerminalEntity(view)) + return .result(value: await TerminalEntity(view: view)) } case .tab: @@ -121,7 +121,7 @@ struct NewTerminalIntent: AppIntent { from: parent?.window, withBaseConfig: config) if let view = newController?.surfaceTree.root?.leftmostLeaf() { - return .result(value: TerminalEntity(view)) + return .result(value: await TerminalEntity(view: view)) } case .splitLeft, .splitRight, .splitUp, .splitDown: @@ -135,7 +135,7 @@ struct NewTerminalIntent: AppIntent { direction: location.splitDirection!, baseConfig: config ) { - return .result(value: TerminalEntity(view)) + return .result(value: await TerminalEntity(view: view)) } } diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift index df0fe17a5..2f370ee80 100644 --- a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -20,14 +20,22 @@ struct QuickTerminalIntent: AppIntent { throw GhosttyIntentError.appUnavailable } + let wasInitialized = delegate.quickControllerInitialized + // This is safe to call even if it is already shown. let c = delegate.quickController + c.animateIn() // Grab all our terminals - let terminals = c.surfaceTree.root?.leaves().map { - TerminalEntity($0) - } ?? [] + var terminals: [TerminalEntity] = [] + for view in c.surfaceTree.root?.leaves() ?? [] { + if wasInitialized { + terminals.append(TerminalEntity(view)) + } else { + terminals.append(await TerminalEntity(view: view)) + } + } return .result(value: terminals) }