fix(web): preserve agent composer shrink sizing

This commit is contained in:
2026-11-08 18:42:27 -08:00
parent bb43b8beda
commit bc92651509
2 changed files with 29 additions and 6 deletions
@@ -46,6 +46,23 @@ function dayLabel(date: Date | null) {
today.setDate(today.getDate() - 1);
return dayKey(date) === dayKey(today) ? "Yesterday" : dayFormat.format(date);
}
function measureComposerHeight(textarea: HTMLTextAreaElement) {
const measurement = textarea.cloneNode(false) as HTMLTextAreaElement;
measurement.removeAttribute("id");
measurement.removeAttribute("name");
measurement.setAttribute("aria-hidden", "true");
measurement.tabIndex = -1;
measurement.value = textarea.value;
Object.assign(measurement.style, {
position: "fixed", left: "-10000px", top: "0", visibility: "hidden", pointerEvents: "none",
width: `${textarea.getBoundingClientRect().width}px`, height: "0px", minHeight: "0px",
maxHeight: "none", overflow: "hidden", resize: "none",
});
textarea.parentElement?.appendChild(measurement);
const height = measurement.scrollHeight;
measurement.remove();
return height;
}
function toolName(value: unknown) {
return typeof value === "string" ? value.replace(/_/g, " ") : "Tool";
}
@@ -288,12 +305,10 @@ export function AgentConversation({ room, onOpenSidebar, isVisible = true }: Pro
useLayoutEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
// scrollHeight reflects the full draft even when the textarea is already
// capped. Avoid collapsing a focused field to its minimum height while
// measuring: the browser may scroll the enclosing transcript to reveal its
// caret, and no ResizeObserver can correct that transient scroll once the
// field returns to the same capped height.
textarea.style.height = `${Math.min(156, Math.max(46, textarea.scrollHeight))}px`;
// Measure offscreen: collapsing a focused textarea can make its scroll
// parent reveal the caret, while its own scrollHeight cannot measure a
// smaller draft until its current height has been removed.
textarea.style.height = `${Math.min(156, Math.max(46, measureComposerHeight(textarea)))}px`;
}, [draft]);
const updateDraft = (value: string) => {
@@ -107,11 +107,19 @@ try {
if (process.env.RECORD_ONLY !== '1') for (const result of all) {
for (const sample of result.bottom) assert.ok(Math.abs(sample.gap) <= 1,
`bottom pin drift at ${result.config.width}px/${result.config.dpr}x: ${JSON.stringify(sample)}`);
const byLabel = Object.fromEntries(result.bottom.map(sample => [sample.label, sample]));
assert.equal(byLabel['wrapped-and-capped'].textareaHeight, 156, `textarea cap changed: ${JSON.stringify(result.bottom)}`);
assert.ok(byLabel.shrink.textareaHeight <= 48 && byLabel.cleared.textareaHeight <= 48,
`textarea did not shrink with the draft: ${JSON.stringify(result.bottom)}`);
assert.equal(byLabel.cleared.clientHeight, byLabel.empty.clientHeight,
`composer did not return to its empty height: ${JSON.stringify(result.bottom)}`);
const before = result.nonbottom.find(sample => sample.before)?.before;
for (const sample of result.nonbottom.filter(sample => sample.label?.startsWith('nonbottom-'))) {
assert.equal(sample.anchorId, before.anchorId, `nonbottom anchor changed: ${JSON.stringify({ before, sample })}`);
assert.ok(Math.abs(sample.anchorTop - before.anchorTop) <= 1, `nonbottom transcript jumped: ${JSON.stringify({ before, sample })}`);
}
const nonbottomShrink = result.nonbottom.find(sample => sample.label === 'nonbottom-shrink');
assert.ok(nonbottomShrink.textareaHeight <= 48, `nonbottom textarea did not shrink: ${JSON.stringify(nonbottomShrink)}`);
}
process.stdout.write(JSON.stringify({ browser, all }, null, 2) + '\n');
} finally {