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:
parent
fd39226883
commit
85e1f1a8f4
2 changed files with 36 additions and 12 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue