@@ -17,6 +17,10 @@ type TerminalSelectionTarget = {
1717 readonly hasSelection : ( ) => boolean
1818}
1919
20+ type TerminalDisposable = {
21+ readonly dispose : ( ) => void
22+ }
23+
2024export type TerminalCopyKeyboardEvent = {
2125 readonly altKey : boolean
2226 readonly ctrlKey : boolean
@@ -32,6 +36,7 @@ export type TerminalCopyInteractionTerminal = TerminalSelectionTarget & {
3236 readonly modes : {
3337 readonly mouseTrackingMode : TerminalMouseTrackingMode
3438 }
39+ readonly onSelectionChange ?: ( handler : ( ) => void ) => TerminalDisposable
3540}
3641
3742type TerminalCopyClipboardData = {
@@ -197,6 +202,7 @@ class TerminalSelectionContextSnapshot {
197202class TerminalCopyInteractionController {
198203 private readonly selectionContext : TerminalSelectionContextSnapshot
199204 private readonly selectionDrag : ReturnType < typeof createTerminalSelectionDragController >
205+ private selectionChangeDisposable : TerminalDisposable | null = null
200206
201207 constructor ( private readonly args : TerminalCopyInteractionArgs ) {
202208 this . selectionContext = new TerminalSelectionContextSnapshot ( args . terminal )
@@ -205,15 +211,36 @@ class TerminalCopyInteractionController {
205211
206212 readonly attach = ( ) : { readonly dispose : ( ) => void } => {
207213 this . args . terminal . attachCustomKeyEventHandler ?.( this . onTerminalKeyEvent )
214+ this . selectionChangeDisposable = this . args . terminal . onSelectionChange ?.( this . onTerminalSelectionChange ) ?? null
208215 this . args . host . addEventListener ( "mousedown" , this . onMouseDown , true )
209216 this . args . host . addEventListener ( "mouseup" , this . onMouseUp , true )
210217 this . args . host . addEventListener ( "contextmenu" , this . onContextMenu , true )
211218 this . args . host . addEventListener ( "copy" , this . onCopy , true )
212219 return { dispose : this . dispose }
213220 }
214221
222+ private readonly shouldLetBrowserHandleCopyShortcut = ( event : TerminalCopyKeyboardEvent ) : boolean =>
223+ shouldLetBrowserHandleTerminalCopyShortcut ( event , this . args . terminal ) ||
224+ ( isKeyboardCopyShortcut ( event ) && this . selectionContext . has ( ) )
225+
215226 private readonly onTerminalKeyEvent = ( event : TerminalCopyKeyboardEvent ) : boolean =>
216- ! shouldLetBrowserHandleTerminalCopyShortcut ( event , this . args . terminal )
227+ ! this . shouldLetBrowserHandleCopyShortcut ( event )
228+
229+ private readonly onTerminalSelectionChange = ( ) : void => {
230+ // CHANGE: keep a copyable snapshot before terminal redraws can drop xterm's live selection.
231+ // WHY: Claude Code periodically repaints the TUI; xterm selection is buffer-bound and may vanish during repaint.
232+ // QUOTE(ТЗ): "когда очистился выделение бы не спадало"
233+ // REF: user-message-2026-06-15-terminal-redraw-selection
234+ // SOURCE: n/a
235+ // FORMAT THEOREM: selected(t) before redraw(t) => cachedSelection(t)
236+ // PURITY: SHELL
237+ // EFFECT: reads terminal.hasSelection() and terminal.getSelection().
238+ // INVARIANT: empty redraw selection events do not erase the last user-created non-empty selection snapshot.
239+ // COMPLEXITY: O(n)/O(1) where n = selected text length.
240+ if ( this . args . terminal . hasSelection ( ) ) {
241+ this . selectionContext . refresh ( )
242+ }
243+ }
217244
218245 private readonly hasProtectedSelectionContext = ( ) : boolean =>
219246 hasActiveMouseTracking ( this . args . terminal ) &&
@@ -295,6 +322,8 @@ class TerminalCopyInteractionController {
295322 }
296323
297324 private readonly dispose = ( ) : void => {
325+ this . selectionChangeDisposable ?. dispose ( )
326+ this . selectionChangeDisposable = null
298327 this . selectionDrag . dispose ( )
299328 this . selectionContext . clear ( )
300329 this . args . host . removeEventListener ( "mousedown" , this . onMouseDown , true )
0 commit comments