diff --git a/hive-ag3nt/assets/agent.css b/hive-ag3nt/assets/agent.css index ff913b4..15ea477 100644 --- a/hive-ag3nt/assets/agent.css +++ b/hive-ag3nt/assets/agent.css @@ -156,18 +156,21 @@ pre.diff { .term-input { padding: 0.4em 1em 0.8em; } .term-input .sendform-term { display: flex; - align-items: center; + align-items: flex-start; gap: 0.5em; border-top: 1px dashed var(--purple-dim); padding-top: 0.5em; } +.term-input .prompt, .term-input .submit-hint { + padding-top: 0.25em; +} .term-input .prompt { color: var(--green); text-shadow: 0 0 6px rgba(166, 227, 161, 0.6); user-select: none; flex: 0 0 auto; } -.term-input input { +.term-input textarea { flex: 1; background: transparent; border: 0; @@ -177,11 +180,15 @@ pre.diff { font-size: 1em; padding: 0.2em 0; caret-color: var(--green); + resize: none; + overflow-y: auto; + line-height: 1.4; + min-height: 1.4em; } -.term-input input::placeholder { color: var(--muted); } +.term-input textarea::placeholder { color: var(--muted); } .term-input .submit-hint { color: var(--muted); font-size: 0.8em; flex: 0 0 auto; } .term-input.disabled .prompt { color: var(--muted); text-shadow: none; } -.term-input.disabled input { color: var(--muted); } +.term-input.disabled textarea { color: var(--muted); } .live { background: rgba(255, 255, 255, 0.02); border: 1px solid var(--purple-dim); diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index 0bab2a7..54e107b 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -47,7 +47,7 @@ return; } // Clear text inputs the operator typed into (the form value was sent). - f.querySelectorAll('input[type="text"], input:not([type])').forEach((i) => { i.value = ''; }); + f.querySelectorAll('input[type="text"], input:not([type]), textarea').forEach((i) => { i.value = ''; }); // Re-enable the button — refreshState() often skips re-rendering the // form (status unchanged), so without this the spinner sticks and // the operator can't submit again. @@ -165,20 +165,37 @@ action: '/send', method: 'POST', class: 'sendform-term', 'data-async': '', }); + const ta = el('textarea', { + name: 'body', placeholder: 'message ' + label + '…', + required: '', autocomplete: 'off', rows: '1', + }); + // Enter submits, Shift+Enter inserts a newline. Auto-grow up to + // ~8 rows of content, then scroll inside the textarea. + const MAX_PX = 12 * 16; // ~8 lines @ 1.5 line-height, 1em base + const grow = () => { + ta.style.height = 'auto'; + ta.style.height = Math.min(ta.scrollHeight, MAX_PX) + 'px'; + }; + ta.addEventListener('input', grow); + ta.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { + e.preventDefault(); + if (ta.value.trim()) form.requestSubmit(); + } + }); + // Reset height after async submit clears the value. + form.addEventListener('submit', () => setTimeout(grow, 0)); form.append( el('span', { class: 'prompt' }, 'operator@' + label + ' ▸'), - el('input', { - name: 'body', placeholder: 'message ' + label + '…', - required: '', autocomplete: 'off', - }), - el('span', { class: 'submit-hint' }, 'enter ↵'), + ta, + el('span', { class: 'submit-hint' }, '↵ send · ⇧↵ newline'), ); slot.append(form); termInputRendered = true; } slot.classList.toggle('disabled', !online); - const input = slot.querySelector('input'); - if (input) input.disabled = !online; + const ta = slot.querySelector('textarea'); + if (ta) ta.disabled = !online; } // Track banner activity by reference-counting in-flight turns. A turn