Fixes 2 bugs
1. After dragging a non-focused surface from window A to window B
**quickly without making B the key window**, the focused surface in
window A is not receiving `keyDown` events.
https://github.com/user-attachments/assets/a8861c0a-9300-470d-bf7e-0f32a9ab2cd1
2. #12343 After dragging a surface from tab A to tab B within the same
window, the dragged surface is not rendering input correctly.
> The reason the thread is stuck is because the surface's occlusion
state is set to invisible after target tab's activate while dragging,
since the dragged surface is still in previous tree before dropping, and
after dropping the occlusion state of this surface is not updated to
visible, which causing the surface is accepting input but not rendering.
https://github.com/user-attachments/assets/d67f5dba-8609-4f67-a956-921982faf796
The reason the thread is stuck is because the surface's occlusion state is set to invisible after target tab's activate while dragging, since the dragged surface is still in previous tree before dropping, and after dropping the occlusion state of this surface is not updated to visible, which causing the surface is accepting input but not rendering.
This fixes a bug: after dragging a non-focused surface from window A to window B **quickly without making B the key window**, the focused surface in window A is not receiving `keyDown` events.
Regression of #12119, this memory leak affects new tabs, since the terminal controller is not deallocated correctly. Hitting `cmd+t` will create a new window with two tabs, but only one actually contains usable surface.
You can reproduce by:
1. Quit and Reopen Ghostty
2. Open a new window if no window is created (initial-window = false)
3. Close the window
4. Hit `cmd+t`
The 👻 Ghost Tab Issue
Previous failure scenario (User perspective):
1. Open a new tab
2. Instantly trigger close other tabs
(eg. through custom user keyboard shortcut)
3. Now you will see an empty Ghost Tab
(Only a window bar with empty content)
The previous failure mode is:
1. Create a tab or window now in `newTab(...)` / `newWindow(...)`.
2. Queue its initial show/focus work with `DispatchQueue.main.async`.
3. Close that tab or window with `closeTabImmediately()` /
`closeWindowImmediately()` before the queued callback runs.
4. The queued callback still runs anyway and calls `showWindow(...)` /
`makeKeyAndOrderFront(...)` on stale state.
5. The tab can be resurrected as a half-closed blank ghost tab.
The fix:
- Store deferred presentation work in a cancellable
DispatchWorkItem and cancel it from the close paths
before AppKit finishes tearing down the tab or window.
- This prevents the stale show/focus callback from
running after close.
## Summary
- After finishing an inline tab title edit (via keybind or
double-click), all keyboard input is lost because
`TabTitleEditor.finishEditing()` sets `makeFirstResponder(nil)`, leaving
the window itself as first responder with no path back to the terminal
surface.
- Adds a `tabTitleEditorDidFinishEditing` delegate callback to
`TabTitleEditorDelegate` that fires after every edit (commit or cancel).
- `TerminalWindow` implements it by calling
`makeFirstResponder(focusedSurface)` to restore keyboard focus to the
terminal.
Fixes https://github.com/ghostty-org/ghostty/discussions/11315
## Testing
- [x] Bind `prompt_tab_title` to a keybind (e.g. `keybind =
cmd+shift+i=prompt_tab_title`)
- [x] Trigger inline tab title edit via keybind, press Enter — verify
keyboard input works immediately
- [x] Trigger inline tab title edit via keybind, press Escape — verify
keyboard input works immediately
- [x] Double-click a tab title, press Enter — verify keyboard input
works immediately
- [x] Double-click a tab title, press Escape — verify keyboard input
works immediately
- [x] Verify Cmd+number tab switching works after all of the above
- [x] Verify split pane focus is correct after editing tab title with
splits open
AI disclosure: Codebase exploration and review via [Claude
Code](https://claude.com/claude-code)
After finishing an inline tab title edit (via keybind or double-click),
`TabTitleEditor.finishEditing()` calls `makeFirstResponder(nil)` to
clear focus from the text field, leaving the window itself as first
responder. No code path restores focus to the terminal surface, so all
keyboard input is lost until the user clicks into a pane.
Add a `tabTitleEditorDidFinishEditing` delegate callback that fires
after every edit (commit or cancel). TerminalWindow implements it by
calling `makeFirstResponder(focusedSurface)` to hand focus back to the
terminal.
Fixes https://github.com/ghostty-org/ghostty/discussions/11315
Co-Authored-By: Claude <noreply@anthropic.com>
Add initialContentSize fallback on TerminalViewContainer so
intrinsicContentSize returns the correct value immediately,
without waiting for @FocusedValue to propagate. This removes
the need for the DispatchQueue.main.asyncAfter 40ms delay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This fixes two overlapping issues regarding window positioning and Cmd+W window closures on macOS:
1. `window-position-x` and `window-position-y` coordinates were being ignored on initial launch because `TerminalWindow.setInitialWindowPosition` depended on the `TerminalController`, which isn't fully attached during `awakeFromNib`. This logic was moved so explicit coordinates are correctly enforced.
2. When closing a window via Cmd+W (leaving the app active), reopening the window would continuously cascade down and to the right rather than restoring to the previous position. It now checks if there are other windows open before cascading.
3. `LastWindowPosition` was updated to save both the frame origin and size (width/height), ensuring that restoring a closed window correctly mimics native AppKit State Restoration size behaviors while honoring explicit configurations.
Depends on #11030
- Update constraints of `TerminalGlassView`
- Use `TerminalViewContainer.DerivedConfig` to map styling properties
- Add TerminalViewContainerTests
- Instead of using delay, now the view updates are explicitly called by
window controllers