diff --git a/docs/design/statusline-text-selection.md b/docs/design/statusline-text-selection.md new file mode 100644 index 00000000000..4a0b1e9e05c --- /dev/null +++ b/docs/design/statusline-text-selection.md @@ -0,0 +1,31 @@ +# Statusline text selection + +## Problem + +Virtualized History enables terminal-wide mouse tracking, so the terminal cannot +provide native text selection. Qwen Code's application-level selection currently +accepts presses only inside the history viewport, leaving the footer/statusline +unselectable. + +## Design + +Keep one selection controller and give it an ordered list of selectable frame +rectangles. The history viewport remains the primary rectangle. The default +layout passes a ref for the rendered footer through `Composer`, and +`MainContent` supplies its measured rectangle as the second target. + +The controller records which rectangle owns a selection when the press starts. +Drag coordinates remain clamped to that rectangle, and frame/layout changes are +compared only within it. Input, dialogs, scrollbars, and other controls remain +outside the selectable targets, so their existing mouse behavior is unchanged. + +This applies only to the existing Virtualized History path. Normal-buffer mode +continues to use terminal-native selection. + +## Verification + +- Dragging within history still highlights and copies history text. +- Dragging within a multi-line footer highlights and copies footer text. +- Presses in the gap between the history and footer do not start a selection. +- Footer selection is cleared when its content or layout changes. +- A live Virtualized History session can copy visible statusline text. diff --git a/docs/design/vp-mouse-selection/design.md b/docs/design/vp-mouse-selection/design.md index b02ceefa873..4f7beafe189 100644 --- a/docs/design/vp-mouse-selection/design.md +++ b/docs/design/vp-mouse-selection/design.md @@ -136,6 +136,8 @@ virtualRow = scrollTop + viewportRow Before starting a selection, hit-test that `(col, layoutRow)` lies inside the history viewport content region and not in the scrollbar column, composer, or footer; presses elsewhere fall through to the existing scrollbar-drag / click-to-focus handlers. This arbitration is the contract between the new selection subscriber and the existing mouse subscribers. +The issue #8131 follow-up keeps that history-region arbitration but registers the footer as a separate selectable rectangle. A drag remains clamped to the rectangle where it started, so the composer and other controls stay excluded. + Anchors are stored in **virtual-row space** so a selection stays pinned to content, but in PR 1 any non-selection scroll/resize/streaming clears the selection (off-screen content is not cached), so virtual-row anchoring here is just consistent bookkeeping, not cross-screen persistence. ### Copy diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 1c104a73f85..1e297a45dec 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Box, Text, useIsScreenReaderEnabled } from 'ink'; -import { useCallback, useRef, useState } from 'react'; +import { Box, Text, useIsScreenReaderEnabled, type DOMElement } from 'ink'; +import { useCallback, useRef, useState, type RefObject } from 'react'; import { LoadingIndicator } from './LoadingIndicator.js'; import { InputPrompt } from './InputPrompt.js'; import { Footer } from './Footer.js'; @@ -20,7 +20,11 @@ import { StreamingState } from '../types.js'; import { FeedbackDialog } from '../FeedbackDialog.js'; import { t } from '../../i18n/index.js'; -export const Composer = () => { +interface ComposerProps { + footerRef?: RefObject; +} + +export const Composer = ({ footerRef }: ComposerProps) => { const config = useConfig(); const isScreenReaderEnabled = useIsScreenReaderEnabled(); const uiState = useUIState(); @@ -152,7 +156,7 @@ export const Composer = () => { (showShortcuts ? ( ) : ( - !isScreenReaderEnabled &&