Adr 052 Rich Prompt Textarea And Sticky Bottom Scroll Hook
# ADR-052: Shared `RichPromptTextarea` + `use-sticky-bottom-scroll` Hook
**Status**: Accepted
**Date**: 2026-05-30
**Deciders**: ClawAI core team
## Context
Two recurring UX problems surfaced during the 2026-05 compare-mode rollout:
1. **Prompt entry consistency**. The main chat composer was a multiline
shadcn `<Textarea>` with autosize, Enter-to-submit, Shift+Enter-newline,
and IME-safe composition handling all inlined into `MessageComposer.tsx`.
The new in-thread compare panel needed the same behaviour for its
per-lane prompt input but shipped initially with a single-line shadcn
`<Input>` because reaching into `MessageComposer` would have bloated it
beyond the 50-line method ceiling. Users complained that pasting a
multi-paragraph prompt into the compare panel collapsed to one row.
2. **Streaming-token auto-scroll**. The chat thread (and the per-lane
compare output panel) renders messages with a virtualized list. When
tokens stream in, the user expects the viewport to auto-scroll IF
they're at the bottom and to STAY where they are if they've scrolled
up to read history. The legacy implementation was a `useEffect(() =>
ref.current?.scrollIntoView())` in the page, which yanked the viewport
down even while the user was scrolling up. Multiple bug reports
("the page keeps fighting me when I try to copy mid-response").
The CLAUDE.md frontend rules also explicitly forbid:
- defining sub-components inside `.tsx` files
- using `useState`/`useEffect`/`useCallback` directly in pages
- duplicating logic across feature surfaces
So the fix had to be a real, reusable component + hook pair — not a
copy-paste into each surface.
## Decision
Extract two shared primitives, both designed to be reusable across any
authoring or scroll-following surface in the app:
### 1. `RichPromptTextarea` (`src/components/chat/rich-prompt-textarea.tsx`)
A `forwardRef` shadcn `<Textarea>` wrapper that:
- Autosizes from `minRows` to `maxRows` then enables internal scroll
- Handles Enter → `onSubmit` (when value non-empty after trim AND
`!disabled`); Shift+Enter inserts a newline
- Tracks `compositionStart`/`compositionEnd` + `nativeEvent.isComposing`
to never submit while a CJK/IME composition is in flight
- Forwards the inner ref via `useImperativeHandle` so parents can
imperatively focus
The TSX is pure render composition. All imperative DOM work (autosize
math, key handling, composition tracking) lives in
`use-rich-prompt-textarea` (`src/hooks/chat/use-rich-prompt-textarea.ts`)
per the "TSX = render only" rule.
Adopters today: `MessageComposer` (main chat) + the in-thread compare
panel's per-lane prompt input. Future adopters: any new rich prompt
entry surface (e.g. system-prompt editor, context-pack item editor).
### 2. `use-sticky-bottom-scroll` (`src/hooks/chat/use-sticky-bottom-scroll.ts`)
A hook with the contract:when to use it
Community prompt sourced from the open-source GitHub repo ihabkhaled/ClawAI (Apache-2.0). A "Adr 052 Rich Prompt Textarea And Sticky Bottom Scroll Hook" style prompt — adapt the placeholders and specifics to your task. Imported as-is and not independently retested here, so check the output before relying on it.
tags
roleplaycommunitygeneral
source
ihabkhaled/ClawAI · Apache-2.0