From b61198fc49e55213ee29a545f7bab3691f43d6b5 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 29 Aug 2026 07:29:49 -0700 Subject: The composer learns the second dimension: a textarea that grows with the text (capped at five lines), Enter still sends, and a newline is one Shift+Enter or Ctrl+J away — bubbles already knew how to show them Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/messages.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index ea368ee..8b0f6c9 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2007,15 +2007,33 @@ export function messagesFeature(ctx) { }, ui.msgReconnecting ? h('span', { class: 'spinner sm' }) : t('msgReconnect'))); } + // Enter sends; Shift+Enter (the textarea's native edit) and Ctrl+J insert + // a newline. Height follows the text as it's typed — imperatively, since + // typing doesn't re-render — with rows as the re-render fallback. + const growComposer = (el) => { + el.style.height = 'auto'; + el.style.height = Math.min(el.scrollHeight, 120) + 'px'; + }; const composer = (placeholder, onSend, onType, draftKey) => h('div', { class: 'col', style: 'gap:6px' }, signerNotice(), h('div', { class: 'chat-compose' }, - h('input', { - class: 'grow', type: 'text', id: 'msg-draft', placeholder, + h('textarea', { + class: 'grow', id: 'msg-draft', placeholder, rows: String(Math.min(5, draftFor(draftKey).split('\n').length)), value: draftFor(draftKey), maxlength: '2000', - onInput: (e) => { setDraft(draftKey, e.target.value); if (onType && e.target.value) onType(); }, - onKeydown: (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend(); } }, + onInput: (e) => { setDraft(draftKey, e.target.value); growComposer(e.target); if (onType && e.target.value) onType(); }, + onKeydown: (e) => { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend(); return; } + if (e.ctrlKey && (e.key === 'j' || e.key === 'J')) { + e.preventDefault(); + const el = e.target; + const { selectionStart: s0, selectionEnd: s1, value } = el; + el.value = value.slice(0, s0) + '\n' + value.slice(s1); + el.selectionStart = el.selectionEnd = s0 + 1; + setDraft(draftKey, el.value); + growComposer(el); + } + }, // Focus summons the keyboard; some browsers resize only after its // animation, so re-stick once now and once when it has settled. onFocus: () => { stickToBottom(); setTimeout(stickToBottom, 350); }, -- cgit v1.2.3