Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f34f8c1
Sync ghostty fork with upstream main
lawrencecchen Mar 16, 2026
d768fe6
Keep main titlebar opaque on macOS 26
lawrencecchen Mar 17, 2026
2667d22
Fix Ghostty search and chrome integration after upstream sync
lawrencecchen Mar 18, 2026
fcdeac5
Fix Ghostty search fallback after upstream sync
lawrencecchen Mar 18, 2026
0609f57
Add automation socket send-key UI regression test
lawrencecchen Mar 20, 2026
6105c98
Fix empty display env handling in E2E workflow
lawrencecchen Mar 20, 2026
d937c39
Bootstrap terminal surface in automation socket UI test
lawrencecchen Mar 20, 2026
605fe5c
Fix automation socket UI test path resolution
lawrencecchen Mar 20, 2026
306b161
Route automation socket UI test to explicit window
lawrencecchen Mar 20, 2026
40132e5
Log raw automation socket responses in UI test
lawrencecchen Mar 20, 2026
d33e98e
Fix automation socket UI test CLI harness
lawrencecchen Mar 20, 2026
bfeb362
Prefer newest Xcode in E2E workflow
lawrencecchen Mar 20, 2026
8ecb149
Fallback to env cmux in automation socket UI test
lawrencecchen Mar 20, 2026
f363936
Try standalone cmux CLI in automation socket UI test
lawrencecchen Mar 20, 2026
09ad5a2
Use direct socket client in automation socket UI test
lawrencecchen Mar 20, 2026
b2f99cd
Merge remote-tracking branch 'origin/main' into task-sync-ghostty-ups…
lawrencecchen Mar 20, 2026
1b49b41
Fix focused E2E workflow shell script
lawrencecchen Mar 21, 2026
835ed70
Stabilize automation socket UI test launch
lawrencecchen Mar 21, 2026
f9fceed
Use netcat for automation socket UI tests
lawrencecchen Mar 21, 2026
c3df2e2
Force socket mode via environment in UI tests
lawrencecchen Mar 21, 2026
922b961
Log automation socket UI test diagnostics
lawrencecchen Mar 21, 2026
ede6ea4
Lowercase automation socket UI test tag
lawrencecchen Mar 21, 2026
bfd9294
Stabilize automation socket UI test foreground launch
lawrencecchen Mar 21, 2026
12a8769
Use clean UI test launch contract for automation socket tests
lawrencecchen Mar 21, 2026
8752400
Use direct socket client in automation UI test
lawrencecchen Mar 21, 2026
06da3d9
Harden automation socket UI test startup
lawrencecchen Mar 21, 2026
d14c7de
Use allowAll for repeated socket UI test
lawrencecchen Mar 21, 2026
2bb008d
Stabilize automation socket UI test harness
lawrencecchen Mar 21, 2026
85e0303
Run automation socket stress off main thread
lawrencecchen Mar 21, 2026
ed9da3e
Resolve automation socket test target via socket
lawrencecchen Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,14 @@ jobs:
- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
if compgen -G "/Applications/Xcode_*.app" >/dev/null 2>&1; then
XCODE_APP="$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort -V | tail -n 1)"
XCODE_DIR="$XCODE_APP/Contents/Developer"
elif [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
if [ -n "$XCODE_APP" ]; then
XCODE_DIR="$XCODE_APP/Contents/Developer"
else
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
Expand Down Expand Up @@ -217,9 +215,7 @@ jobs:
clang -framework Foundation -framework CoreGraphics \
-o "$HELPER_PATH" scripts/create-virtual-display.m

cat >"$MANIFEST_PATH" <<EOF
{"helperBinaryPath":"$HELPER_PATH"}
EOF
printf '{"helperBinaryPath":"%s"}\n' "$HELPER_PATH" >"$MANIFEST_PATH"
fi

# Start recording right before the test (after build/resolve).
Expand Down
11 changes: 7 additions & 4 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1769,15 +1769,18 @@ func startOrFocusTerminalSearch(
return true
}

terminalSurface.requestGhosttySearchActivation(.startSearch)
if terminalSurface.performBindingAction("start_search") {
DispatchQueue.main.async { [weak terminalSurface] in
guard let terminalSurface, terminalSurface.searchState == nil else { return }
terminalSurface.searchState = TerminalSurface.SearchState()
searchFocusNotifier(terminalSurface)
DispatchQueue.main.async {
cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(
terminalSurface,
searchFocusNotifier: searchFocusNotifier
)
}
Comment on lines +1774 to 1779
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid run-loop deferral for search fallback state initialization.

At Line 1743, forcing cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(...) onto DispatchQueue.main.async creates a timing gap where this function returns true while searchState can still be nil. That can desync immediate search-active checks and overlay activation paths.

Suggested fix
-    if terminalSurface.performBindingAction("start_search") {
-        DispatchQueue.main.async {
-            cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(
-                terminalSurface,
-                searchFocusNotifier: searchFocusNotifier
-            )
-        }
+    if terminalSurface.performBindingAction("start_search") {
+        if Thread.isMainThread {
+            cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(
+                terminalSurface,
+                searchFocusNotifier: searchFocusNotifier
+            )
+        } else {
+            DispatchQueue.main.async {
+                cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(
+                    terminalSurface,
+                    searchFocusNotifier: searchFocusNotifier
+                )
+            }
+        }
         return true
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 1743 - 1748, The call to
cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded is being deferred with
DispatchQueue.main.async which creates a timing gap where the function can
return true while searchState is still nil; remove the async deferral and invoke
cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(terminalSurface,
searchFocusNotifier: searchFocusNotifier) synchronously on the current execution
context (or, if it must run on the main thread, perform a main-thread safe
synchronous invocation—i.e. call directly when already on main, otherwise
dispatch synchronously to DispatchQueue.main) so that searchState is initialized
deterministically before any immediate search-active checks or overlay
activation logic runs.

return true
}

terminalSurface.clearGhosttySearchActivationRequest()
terminalSurface.searchState = TerminalSurface.SearchState()
searchFocusNotifier(terminalSurface)
return true
Expand Down
57 changes: 47 additions & 10 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,44 @@ struct TitlebarLayerBackground: NSViewRepresentable {
}
}

func cmuxMainWindowTitlebarOpacity(
backgroundOpacity: Double
) -> CGFloat {
GhosttyBackgroundTheme.compositedChromeOpacity(backgroundOpacity)
}

func cmuxResolveGhosttyChromeConfig(
loadedConfig: GhosttyConfig,
runtimeBackgroundColor: NSColor,
runtimeBackgroundOpacity: Double
) -> GhosttyConfig {
let runtimeOpacity = GhosttyBackgroundTheme.clampedOpacity(runtimeBackgroundOpacity)
let loadedOpacity = GhosttyBackgroundTheme.clampedOpacity(loadedConfig.backgroundOpacity)

// Newer Ghostty builds can drive the window background fully clear at runtime for
// translucent terminals. That clear backing color should not replace cmux chrome.
guard runtimeOpacity > 0.001 || loadedOpacity <= 0.001 else {
return loadedConfig
}

var resolved = loadedConfig
resolved.backgroundColor = runtimeBackgroundColor
resolved.backgroundOpacity = runtimeBackgroundOpacity
return resolved
}

func cmuxResolveGhosttyChromeConfig(
loadConfig: () -> GhosttyConfig = { GhosttyConfig.load() },
runtimeBackgroundColor: () -> NSColor = { GhosttyApp.shared.defaultBackgroundColor },
runtimeBackgroundOpacity: () -> Double = { GhosttyApp.shared.defaultBackgroundOpacity }
) -> GhosttyConfig {
cmuxResolveGhosttyChromeConfig(
loadedConfig: loadConfig(),
runtimeBackgroundColor: runtimeBackgroundColor(),
runtimeBackgroundOpacity: runtimeBackgroundOpacity()
)
}

final class SidebarState: ObservableObject {
@Published var isVisible: Bool
@Published var persistedWidth: CGFloat
Expand Down Expand Up @@ -1457,6 +1495,7 @@ struct ContentView: View {
@State private var workspaceHandoffFallbackTask: Task<Void, Never>?
@State private var didApplyUITestSidebarSelection = false
@State private var titlebarThemeGeneration: UInt64 = 0
@State private var titlebarThemeConfig = cmuxResolveGhosttyChromeConfig()
@State private var sidebarDraggedTabId: UUID?
@State private var titlebarTextUpdateCoalescer = NotificationBurstCoalescer(delay: 1.0 / 30.0)
@State private var sidebarResizerCursorReleaseWorkItem: DispatchWorkItem?
Expand Down Expand Up @@ -2183,8 +2222,7 @@ struct ContentView: View {
@State private var titlebarLeadingInset: CGFloat = 12
private var windowIdentifier: String { "cmux.main.\(windowId.uuidString)" }
private var fakeTitlebarTextColor: Color {
_ = titlebarThemeGeneration
let ghosttyBackground = GhosttyApp.shared.defaultBackgroundColor
let ghosttyBackground = titlebarThemeConfig.backgroundColor
return ghosttyBackground.isLightColor
? Color.black.opacity(0.78)
: Color.white.opacity(0.82)
Expand Down Expand Up @@ -2242,14 +2280,11 @@ struct ContentView: View {
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.background({
// The terminal area has two stacked semi-transparent layers: the Bonsplit
// container chrome background plus Ghostty's own Metal-rendered background.
// Compute the effective composited opacity so the titlebar matches visually.
let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity)
let effective = alpha >= 0.999 ? alpha : 1.0 - pow(1.0 - alpha, 2)
return TitlebarLayerBackground(
backgroundColor: GhosttyApp.shared.defaultBackgroundColor,
opacity: effective
backgroundColor: titlebarThemeConfig.backgroundColor,
opacity: cmuxMainWindowTitlebarOpacity(
backgroundOpacity: titlebarThemeConfig.backgroundOpacity
)
)
}())
.overlay(alignment: .bottom) {
Expand Down Expand Up @@ -2408,6 +2443,7 @@ struct ContentView: View {
syncSidebarSelectedWorkspaceIds()
applyUITestSidebarSelectionIfNeeded(tabs: tabManager.tabs)
updateTitlebarText()
titlebarThemeConfig = cmuxResolveGhosttyChromeConfig()

// Startup recovery (#399): if session restore or a race condition leaves the
// view in a broken state (empty tabs, no selection, unmounted workspaces),
Expand Down Expand Up @@ -2529,9 +2565,10 @@ struct ContentView: View {
})

view = AnyView(view.onChange(of: titlebarThemeGeneration) { oldValue, newValue in
titlebarThemeConfig = cmuxResolveGhosttyChromeConfig()
Comment on lines 2567 to +2568
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute titlebar theme when workspace selection changes

Updating titlebarThemeConfig only in the titlebarThemeGeneration observer makes the titlebar color/opacity depend on a later theme-refresh event, but switching to an already-mounted workspace with a different background can happen without bumping that generation. In that case the titlebar keeps the previous workspace's theme until another background notification arrives, so the chrome becomes visibly stale right after tab/workspace switches.

Useful? React with 👍 / 👎.

guard GhosttyApp.shared.backgroundLogEnabled else { return }
GhosttyApp.shared.logBackground(
"titlebar theme refresh applied oldGeneration=\(oldValue) generation=\(newValue) appBg=\(GhosttyApp.shared.defaultBackgroundColor.hexString()) appOpacity=\(String(format: "%.3f", GhosttyApp.shared.defaultBackgroundOpacity))"
"titlebar theme refresh applied oldGeneration=\(oldValue) generation=\(newValue) titlebarBg=\(titlebarThemeConfig.backgroundColor.hexString()) titlebarOpacity=\(String(format: "%.3f", titlebarThemeConfig.backgroundOpacity)) appBg=\(GhosttyApp.shared.defaultBackgroundColor.hexString()) appOpacity=\(String(format: "%.3f", GhosttyApp.shared.defaultBackgroundOpacity))"
)
})

Expand Down
43 changes: 43 additions & 0 deletions Sources/GhosttyConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,44 @@ struct GhosttyConfig {
case dark
}

enum BackgroundBlur: Hashable {
case disabled
case enabled
case macosGlassRegular
case macosGlassClear

var isGlassStyle: Bool {
switch self {
case .macosGlassRegular, .macosGlassClear:
return true
case .disabled, .enabled:
return false
}
}

static func parse(_ rawValue: String) -> Self? {
let normalized = rawValue
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()

switch normalized {
case "false", "0", "off", "disabled":
return .disabled
case "true", "1", "on":
return .enabled
case "macos-glass-regular":
return .macosGlassRegular
case "macos-glass-clear":
return .macosGlassClear
default:
if Double(normalized) != nil {
return .enabled
Comment on lines +40 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse zero-valued blur radii as disabled

BackgroundBlur.parse currently maps any numeric string to .enabled, so zero-form values like 00 or 0.0 are treated as blur-on instead of blur-off. This makes cmux’s parsed config diverge from Ghostty semantics for zero blur intensity and can trigger incorrect chrome/theme decisions (for example, blur-change refreshes and style checks) when config writers emit normalized numeric zero values.

Useful? React with 👍 / 👎.

}
return nil
}
}
}

private static let cmuxReleaseBundleIdentifier = "com.cmuxterm.app"
private static let loadCacheLock = NSLock()
private static var cachedConfigsByColorScheme: [ColorSchemePreference: GhosttyConfig] = [:]
Expand All @@ -23,6 +61,7 @@ struct GhosttyConfig {
// Colors (from theme or config)
var backgroundColor: NSColor = NSColor(hex: "#272822")!
var backgroundOpacity: Double = 1.0
var backgroundBlur: BackgroundBlur = .disabled
var foregroundColor: NSColor = NSColor(hex: "#fdfff1")!
var cursorColor: NSColor = NSColor(hex: "#c0c1b5")!
var cursorTextColor: NSColor = NSColor(hex: "#8d8e82")!
Expand Down Expand Up @@ -258,6 +297,10 @@ struct GhosttyConfig {
if let opacity = Double(value) {
backgroundOpacity = opacity
}
case "background-blur":
if let blur = BackgroundBlur.parse(value) {
backgroundBlur = blur
}
case "foreground":
if let color = NSColor(hex: value) {
foregroundColor = color
Expand Down
132 changes: 126 additions & 6 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,68 @@ private func cmuxTransparentWindowBaseColor() -> NSColor {
// avoids visual artifacts that can happen with a fully clear window background.
NSColor.white.withAlphaComponent(0.001)
}

enum CmuxPendingGhosttySearchRequest: Equatable {
case startSearch
case searchSelection
}

enum CmuxGhosttyStartSearchResolution: Equatable {
case ignore
case focusExisting
case updateExistingNeedle(String)
case createEmpty
case createWithNeedle(String)
}

func cmuxDecodeGhosttyOptionalCString(_ pointer: UnsafePointer<CChar>?) -> String? {
guard let pointer else { return nil }

// Guard obviously invalid low addresses so Ghostty callback corruption cannot crash us
// while handling an empty/open-search notification.
let address = UInt(bitPattern: pointer)
guard address >= 0x1000 else { return nil }

return String(validatingUTF8: pointer)
}

func cmuxResolveGhosttyStartSearch(
existingSearchState: Bool,
pendingRequest: CmuxPendingGhosttySearchRequest?,
needle: String?
) -> CmuxGhosttyStartSearchResolution {
if existingSearchState {
if let needle, !needle.isEmpty {
return .updateExistingNeedle(needle)
}
return .focusExisting
}

switch pendingRequest {
case .startSearch:
return .createEmpty
case .searchSelection:
if let needle, !needle.isEmpty {
return .createWithNeedle(needle)
}
return .ignore
Comment on lines +71 to +74
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle empty selection by opening search UI

cmuxResolveGhosttyStartSearch currently returns .ignore for .searchSelection whenever needle is empty, which is a normal outcome when the user triggers Find Selection without an active text selection. In that case the command now becomes a silent no-op and the user cannot type a manual query, whereas before this commit TabManager.searchSelection() always created/focused searchState first so Find Selection still opened the search UI.

Useful? React with 👍 / 👎.

case nil:
return .ignore
Comment on lines +75 to +76
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle start_search callbacks without pending state

cmuxResolveGhosttyStartSearch now drops every GHOSTTY_ACTION_START_SEARCH callback when pendingRequest is nil, even if Ghostty supplies a non-empty needle. Because pendingGhosttySearchRequest is only set in cmux-driven paths (startOrFocusTerminalSearch / .startSearch action), any Ghostty-originated start-search trigger (for example, a keybinding handled directly by Ghostty) becomes a no-op and never opens/focuses the find UI. Before this commit, the callback always created or updated searchState.

Useful? React with 👍 / 👎.

}
}

func cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(
_ terminalSurface: TerminalSurface,
searchFocusNotifier: @escaping (TerminalSurface) -> Void = {
NotificationCenter.default.post(name: .ghosttySearchFocus, object: $0)
}
) {
guard terminalSurface.pendingGhosttySearchActivationRequest == .startSearch else { return }
terminalSurface.clearGhosttySearchActivationRequest()
guard terminalSurface.searchState == nil else { return }
terminalSurface.searchState = TerminalSurface.SearchState()
searchFocusNotifier(terminalSurface)
}
#endif

#if DEBUG
Expand Down Expand Up @@ -2131,15 +2193,41 @@ class GhosttyApp {
return true
case GHOSTTY_ACTION_START_SEARCH:
guard let terminalSurface = surfaceView.terminalSurface else { return true }
let needle = action.action.start_search.needle.flatMap { String(cString: $0) }
let pendingRequest = terminalSurface.consumeGhosttySearchActivationRequest()
let shouldDecodeNeedle =
terminalSurface.searchState != nil || pendingRequest != .startSearch
Comment on lines +2196 to +2198
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move pending search state consumption onto main thread

consumeGhosttySearchActivationRequest() is called before hopping to DispatchQueue.main, but the same pending field is written from main-thread UI paths (request.../clear...). Because this callback path is not guaranteed to be on main (the handler explicitly uses main hopping elsewhere), this introduces an unsynchronized cross-thread read/write race that can intermittently drop or misclassify pending search requests.

Useful? React with 👍 / 👎.

let needle = shouldDecodeNeedle
Comment on lines +2197 to +2199
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read searchState on main thread in start-search callback

GHOSTTY_ACTION_START_SEARCH computes shouldDecodeNeedle using terminalSurface.searchState before hopping to DispatchQueue.main, but searchState is mutated from main-thread UI flows (open/close/find actions). When the callback is delivered off-main, this unsynchronized cross-thread read can race with those writes and produce stale resolution decisions (e.g., whether to decode/update the needle) with undefined behavior. Move the searchState read (or the full resolution step) into the main-queue block, or isolate this state behind main-actor access.

Useful? React with 👍 / 👎.

? cmuxDecodeGhosttyOptionalCString(action.action.start_search.needle)
: nil
DispatchQueue.main.async {
if let searchState = terminalSurface.searchState {
if let needle, !needle.isEmpty {
let resolution = cmuxResolveGhosttyStartSearch(
existingSearchState: terminalSurface.searchState != nil,
pendingRequest: pendingRequest,
needle: needle
)

switch resolution {
case .ignore:
#if DEBUG
dlog(
"find.startSearch ignored tab=\(terminalSurface.tabId.uuidString.prefix(5)) " +
"surface=\(terminalSurface.id.uuidString.prefix(5)) " +
"pending=\(String(describing: pendingRequest))"
)
#endif
return
case .focusExisting:
break
case .updateExistingNeedle(let needle):
if let searchState = terminalSurface.searchState {
searchState.needle = needle
}
} else {
terminalSurface.searchState = TerminalSurface.SearchState(needle: needle ?? "")
case .createEmpty:
terminalSurface.searchState = TerminalSurface.SearchState()
case .createWithNeedle(let needle):
terminalSurface.searchState = TerminalSurface.SearchState(needle: needle)
}

NotificationCenter.default.post(name: .ghosttySearchFocus, object: terminalSurface)
}
return true
Expand Down Expand Up @@ -2590,6 +2678,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
private var portalLifecycleState: PortalLifecycleState = .live
private var portalLifecycleGeneration: UInt64 = 1
private var activePortalHostLease: PortalHostLease?
private var pendingGhosttySearchRequest: CmuxPendingGhosttySearchRequest?
@Published var searchState: SearchState? = nil {
didSet {
if let searchState {
Expand Down Expand Up @@ -3629,6 +3718,24 @@ final class TerminalSurface: Identifiable, ObservableObject {
}
}

func requestGhosttySearchActivation(_ request: CmuxPendingGhosttySearchRequest) {
pendingGhosttySearchRequest = request
}
Comment on lines +3721 to +3723
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve first pending search activation request

requestGhosttySearchActivation unconditionally overwrites pendingGhosttySearchRequest, so back-to-back Find actions can be misresolved when Ghostty callbacks arrive out of order. For example, if searchSelection() sets .searchSelection and the user quickly triggers a plain find (.startSearch) before the first callback, the later write replaces the earlier intent and the selection callback is resolved as .startSearch, dropping the selected needle and opening an empty find state. Consider queueing requests or refusing to overwrite until the prior request is consumed/cleared.

Useful? React with 👍 / 👎.

Comment on lines +3721 to +3723
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve first pending search activation request

requestGhosttySearchActivation unconditionally overwrites pendingGhosttySearchRequest, so back-to-back Find actions can be misresolved when Ghostty callbacks arrive out of order. For example, if searchSelection() sets .searchSelection and the user quickly triggers a plain find (.startSearch) before the first callback, the later write replaces the earlier intent and the selection callback is resolved as .startSearch, dropping the selected needle and opening an empty find state. Consider queueing requests or refusing to overwrite until the prior request is consumed/cleared.

Useful? React with 👍 / 👎.


var pendingGhosttySearchActivationRequest: CmuxPendingGhosttySearchRequest? {
pendingGhosttySearchRequest
}

func consumeGhosttySearchActivationRequest() -> CmuxPendingGhosttySearchRequest? {
let request = pendingGhosttySearchRequest
pendingGhosttySearchRequest = nil
return request
}

func clearGhosttySearchActivationRequest() {
pendingGhosttySearchRequest = nil
}

@discardableResult
func toggleKeyboardCopyMode() -> Bool {
let handled = surfaceView.toggleKeyboardCopyMode()
Expand Down Expand Up @@ -4536,7 +4643,20 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
_ = performBindingAction("jump_to_prompt:\(delta * count)")
refreshKeyboardCopyModeViewportRowFromVisibleAnchor(surface: surface)
case .startSearch:
_ = performBindingAction("start_search")
if let terminalSurface, terminalSurface.searchState != nil {
NotificationCenter.default.post(name: .ghosttySearchFocus, object: terminalSurface)
break
}
terminalSurface?.requestGhosttySearchActivation(.startSearch)
if performBindingAction("start_search") {
if let terminalSurface {
DispatchQueue.main.async {
cmuxApplyPendingGhosttyStartSearchFallbackIfNeeded(terminalSurface)
}
}
} else {
terminalSurface?.clearGhosttySearchActivationRequest()
}
case .searchNext:
performBindingAction("navigate_search:next", repeatCount: count)
refreshKeyboardCopyModeViewportRowFromVisibleAnchor(surface: surface)
Expand Down
Loading
Loading