agent terminal: multi-line textarea input

swap the single-line <input> for an auto-growing <textarea>. enter
submits, shift+enter newlines, ime composition respected (skip submit
while isComposing). height caps at ~12em then scrolls. submit-hint
updates to '↵ send · ⇧↵ newline'. async-form handler now also clears
textareas on success.
This commit is contained in:
müde 2026-05-15 19:21:00 +02:00
parent fd39226883
commit 85e1f1a8f4
2 changed files with 36 additions and 12 deletions

View file

@ -156,18 +156,21 @@ pre.diff {
.term-input { padding: 0.4em 1em 0.8em; } .term-input { padding: 0.4em 1em 0.8em; }
.term-input .sendform-term { .term-input .sendform-term {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 0.5em; gap: 0.5em;
border-top: 1px dashed var(--purple-dim); border-top: 1px dashed var(--purple-dim);
padding-top: 0.5em; padding-top: 0.5em;
} }
.term-input .prompt, .term-input .submit-hint {
padding-top: 0.25em;
}
.term-input .prompt { .term-input .prompt {
color: var(--green); color: var(--green);
text-shadow: 0 0 6px rgba(166, 227, 161, 0.6); text-shadow: 0 0 6px rgba(166, 227, 161, 0.6);
user-select: none; user-select: none;
flex: 0 0 auto; flex: 0 0 auto;
} }
.term-input input { .term-input textarea {
flex: 1; flex: 1;
background: transparent; background: transparent;
border: 0; border: 0;
@ -177,11 +180,15 @@ pre.diff {
font-size: 1em; font-size: 1em;
padding: 0.2em 0; padding: 0.2em 0;
caret-color: var(--green); 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 .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 .prompt { color: var(--muted); text-shadow: none; }
.term-input.disabled input { color: var(--muted); } .term-input.disabled textarea { color: var(--muted); }
.live { .live {
background: rgba(255, 255, 255, 0.02); background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--purple-dim); border: 1px solid var(--purple-dim);

View file

@ -47,7 +47,7 @@
return; return;
} }
// Clear text inputs the operator typed into (the form value was sent). // 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 // Re-enable the button — refreshState() often skips re-rendering the
// form (status unchanged), so without this the spinner sticks and // form (status unchanged), so without this the spinner sticks and
// the operator can't submit again. // the operator can't submit again.
@ -165,20 +165,37 @@
action: '/send', method: 'POST', action: '/send', method: 'POST',
class: 'sendform-term', 'data-async': '', 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( form.append(
el('span', { class: 'prompt' }, 'operator@' + label + ' ▸'), el('span', { class: 'prompt' }, 'operator@' + label + ' ▸'),
el('input', { ta,
name: 'body', placeholder: 'message ' + label + '…', el('span', { class: 'submit-hint' }, '↵ send · ⇧↵ newline'),
required: '', autocomplete: 'off',
}),
el('span', { class: 'submit-hint' }, 'enter ↵'),
); );
slot.append(form); slot.append(form);
termInputRendered = true; termInputRendered = true;
} }
slot.classList.toggle('disabled', !online); slot.classList.toggle('disabled', !online);
const input = slot.querySelector('input'); const ta = slot.querySelector('textarea');
if (input) input.disabled = !online; if (ta) ta.disabled = !online;
} }
// Track banner activity by reference-counting in-flight turns. A turn // Track banner activity by reference-counting in-flight turns. A turn