Before submitting
User-facing report of the same symptom: #6173. This issue is the root-cause writeup.
Area
apps/web (desktop wraps this surface; Electron's Edit menu is the macOS trigger)
Steps to reproduce
- Open the integrated terminal in the desktop app on macOS.
- Select some terminal text (single line or multiline).
- Press Cmd+C, or use Edit → Copy.
- Paste into another app.
Expected behavior
The selected terminal text is on the clipboard.
Actual behavior
The clipboard is blank (multiline) or unchanged (short selections). Right-click Copy is also unreliable. Matches #6173.
Impact
Blocks work completely
Version or commit
Regressed in 1add47b32 (fix(web): add copying terminal selection with ctrl+c in the web app / #5638). Still present on main @ beab6886f.
Environment
macOS desktop app. Web is the same Ghostty canvas path; Electron { role: "editMenu" } makes Mac worse.
Root cause
The Ghostty terminal is a canvas plus a 1×1 empty IME textarea. There is no DOM selection of the visible text.
#5638 stopped calling writeTextToClipboard(getSelection()) on Cmd+C. It instead:
- Leaves the keydown default alive so a native
copy event can fire.
- On that event, always
preventDefault(), clipboardData?.setData(...), and cancels the deferred navigator.clipboard.writeText fallback.
- Does not put the Ghostty selection into the textarea first.
Two things then write emptiness:
- Chromium/Electron copy of the focused empty textarea.
- On macOS, the application menu
{ role: "editMenu" } Copy accelerator (webContents.copy() / getSelectedText()), which often delivers a copy event with no clipboardData and then writes the empty DOM selection after renderer JS.
Because the handler claims the gesture even when clipboardData is null, writeText never runs, and the OS clipboard ends up blank.
Ghostty formatting is not the bug. ghostty_terminal_selection_format_buf on a cell-drag selection of hello\nworld returns hello\nworld. hasSelection() / getSelection() already use that string. The text is available; copy throws it away.
Analysis
Copy shortcut path (apps/web/src/terminal/ghostty/surface.ts):
isTerminalCopyShortcut + hasSelection() → schedule clipboard.writeText(selection) on a microtask, do not preventDefault.
input copy listener: if hasSelection(), preventDefault, optional setData, bump copyShortcutToken (this cancels the microtask).
Desktop Edit menu (apps/desktop/src/window/DesktopApplicationMenu.ts):
{ role: "editMenu" } includes Copy with Cmd+C.
- Renderer
keydown.preventDefault does not stop that accelerator on macOS.
- The accelerator copies whatever DOM selection exists. The IME textarea is empty.
Why multiline vs short looked different in #6173: a native empty copy sometimes clears the clipboard (writes "") and sometimes is a no-op (previous contents remain). Both are the empty-textarea path, not a Ghostty format split.
Right-click Copy in the custom terminal menu is a separate seam (writeTextToClipboard after await contextMenu.show). The Cmd+C blankness is the empty-textarea / cancelled-fallback race above.
Solution
- Before native copy runs, park the Ghostty selection in the hidden textarea and
select() it, so Electron Edit → Copy and a DOM copy of the focused field have real text.
- Only
preventDefault + cancel writeText when clipboardData exists and setData("text/plain", selection) actually ran. A copy event with no clipboardData must leave the fallback alive and must not suppress the default copy of the primed textarea.
- Clear the primed textarea on the next non-copy key, composition start, and
clearSelection(), so IME is not composing over leftover copy text.
Keep clipboard.writeText as the WebKit/no-copy-event path. Do not go back to racing an empty textarea against the Edit menu.
Tests to lock it:
applyTerminalCopyEvent: missing clipboardData → do not claim the fallback.
primeTerminalCopyInput: textarea value and selection cover the Ghostty string.
- ABI: cell-drag selection via screen grid refs still formats with Ghostty copy semantics.
Before submitting
User-facing report of the same symptom: #6173. This issue is the root-cause writeup.
Area
apps/web (desktop wraps this surface; Electron's Edit menu is the macOS trigger)
Steps to reproduce
Expected behavior
The selected terminal text is on the clipboard.
Actual behavior
The clipboard is blank (multiline) or unchanged (short selections). Right-click Copy is also unreliable. Matches #6173.
Impact
Blocks work completely
Version or commit
Regressed in
1add47b32(fix(web): add copying terminal selection with ctrl+c in the web app/ #5638). Still present onmain@beab6886f.Environment
macOS desktop app. Web is the same Ghostty canvas path; Electron
{ role: "editMenu" }makes Mac worse.Root cause
The Ghostty terminal is a canvas plus a 1×1 empty IME textarea. There is no DOM selection of the visible text.
#5638 stopped calling
writeTextToClipboard(getSelection())on Cmd+C. It instead:copyevent can fire.preventDefault(),clipboardData?.setData(...), and cancels the deferrednavigator.clipboard.writeTextfallback.Two things then write emptiness:
{ role: "editMenu" }Copy accelerator (webContents.copy()/getSelectedText()), which often delivers acopyevent with noclipboardDataand then writes the empty DOM selection after renderer JS.Because the handler claims the gesture even when
clipboardDatais null,writeTextnever runs, and the OS clipboard ends up blank.Ghostty formatting is not the bug.
ghostty_terminal_selection_format_bufon a cell-drag selection ofhello\nworldreturnshello\nworld.hasSelection()/getSelection()already use that string. The text is available; copy throws it away.Analysis
Copy shortcut path (
apps/web/src/terminal/ghostty/surface.ts):isTerminalCopyShortcut+hasSelection()→ scheduleclipboard.writeText(selection)on a microtask, do notpreventDefault.inputcopylistener: ifhasSelection(),preventDefault, optionalsetData, bumpcopyShortcutToken(this cancels the microtask).Desktop Edit menu (
apps/desktop/src/window/DesktopApplicationMenu.ts):{ role: "editMenu" }includes Copy with Cmd+C.keydown.preventDefaultdoes not stop that accelerator on macOS.Why multiline vs short looked different in #6173: a native empty copy sometimes clears the clipboard (writes
"") and sometimes is a no-op (previous contents remain). Both are the empty-textarea path, not a Ghostty format split.Right-click Copy in the custom terminal menu is a separate seam (
writeTextToClipboardafterawait contextMenu.show). The Cmd+C blankness is the empty-textarea / cancelled-fallback race above.Solution
select()it, so Electron Edit → Copy and a DOMcopyof the focused field have real text.preventDefault+ cancelwriteTextwhenclipboardDataexists andsetData("text/plain", selection)actually ran. Acopyevent with noclipboardDatamust leave the fallback alive and must not suppress the default copy of the primed textarea.clearSelection(), so IME is not composing over leftover copy text.Keep
clipboard.writeTextas the WebKit/no-copy-event path. Do not go back to racing an empty textarea against the Edit menu.Tests to lock it:
applyTerminalCopyEvent: missingclipboardData→ do not claim the fallback.primeTerminalCopyInput: textarea value and selection cover the Ghostty string.