fix(tui): clear selections from blank composer space

Clicking blank space in the transcript or composer now clears active TUI/input selections like a normal text surface. TextInput clicks stop bubbling so cursor placement and selection gestures keep their local behavior.

Made-with: Cursor
This commit is contained in:
Brooklyn Nicholson
2026-04-27 16:49:49 -05:00
parent 77dfb399ae
commit 56b0dc7d08
4 changed files with 35 additions and 4 deletions

View File

@@ -285,6 +285,7 @@ export interface AppLayoutActions {
answerClarify: (answer: string) => void
answerSecret: (value: string) => void
answerSudo: (pw: string) => void
clearSelection: () => void
onModelSelect: (value: string) => void
resumeById: (id: string) => void
setStickyPrompt: (value: string) => void

View File

@@ -25,6 +25,7 @@ import type { Msg, PanelSection, SlashCatalog } from '../types.js'
import { createGatewayEventHandler } from './createGatewayEventHandler.js'
import { createSlashHandler } from './createSlashHandler.js'
import { getInputSelection } from './inputSelectionStore.js'
import { type GatewayRpc, type TranscriptRow } from './interfaces.js'
import { $overlayState, patchOverlayState } from './overlayStore.js'
import { scrollWithSelectionBy } from './scroll.js'
@@ -147,6 +148,11 @@ export function useMainApp(gw: GatewayClient) {
selection.setSelectionBgColor(ui.theme.color.selectionBg)
}, [selection, ui.theme.color.selectionBg])
const clearSelection = useCallback(() => {
selection.clearSelection()
getInputSelection()?.clear()
}, [selection])
const composer = useComposerState({
gw,
onClipboardPaste: quiet => clipboardPasteRef.current(quiet),
@@ -519,6 +525,7 @@ export function useMainApp(gw: GatewayClient) {
[
appendMessage,
bellOnComplete,
clearSelection,
composerActions.setInput,
gateway,
panel,
@@ -691,11 +698,12 @@ export function useMainApp(gw: GatewayClient) {
answerClarify,
answerSecret,
answerSudo,
clearSelection,
onModelSelect,
resumeById: session.resumeById,
setStickyPrompt
}),
[answerApproval, answerClarify, answerSecret, answerSudo, onModelSelect, session.resumeById]
[answerApproval, answerClarify, answerSecret, answerSudo, clearSelection, onModelSelect, session.resumeById]
)
const appComposer = useMemo(

View File

@@ -47,7 +47,18 @@ const TranscriptPane = memo(function TranscriptPane({
return (
<>
<ScrollBox flexDirection="column" flexGrow={1} flexShrink={1} ref={transcript.scrollRef} stickyScroll>
<ScrollBox
flexDirection="column"
flexGrow={1}
flexShrink={1}
onClick={(e: { cellIsBlank?: boolean }) => {
if (e.cellIsBlank) {
actions.clearSelection()
}
}}
ref={transcript.scrollRef}
stickyScroll
>
<Box flexDirection="column" paddingX={1}>
{transcript.virtualHistory.topSpacer > 0 ? <Box height={transcript.virtualHistory.topSpacer} /> : null}
@@ -118,7 +129,17 @@ const ComposerPane = memo(function ComposerPane({
const inputHeight = inputVisualHeight(composer.input, inputColumns)
return (
<NoSelect flexDirection="column" flexShrink={0} fromLeftEdge paddingX={1}>
<NoSelect
flexDirection="column"
flexShrink={0}
fromLeftEdge
onClick={(e: { cellIsBlank?: boolean }) => {
if (e.cellIsBlank) {
actions.clearSelection()
}
}}
paddingX={1}
>
<QueuedMessages
cols={composer.cols}
queued={composer.queuedDisplay}

View File

@@ -915,11 +915,12 @@ export function TextInput({
return (
<Box
onClick={(e: { localRow?: number; localCol?: number }) => {
onClick={(e: { localCol?: number; localRow?: number; stopImmediatePropagation?: () => void }) => {
if (!focus) {
return
}
e.stopImmediatePropagation?.()
clearSel()
const pos = mouseOffset(e)
const next = offsetFromPosition(display, pos.row, pos.col, columns)