fix(web): preserve agent composer shrink sizing
This commit is contained in:
@@ -46,6 +46,23 @@ function dayLabel(date: Date | null) {
|
|||||||
today.setDate(today.getDate() - 1);
|
today.setDate(today.getDate() - 1);
|
||||||
return dayKey(date) === dayKey(today) ? "Yesterday" : dayFormat.format(date);
|
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) {
|
function toolName(value: unknown) {
|
||||||
return typeof value === "string" ? value.replace(/_/g, " ") : "Tool";
|
return typeof value === "string" ? value.replace(/_/g, " ") : "Tool";
|
||||||
}
|
}
|
||||||
@@ -288,12 +305,10 @@ export function AgentConversation({ room, onOpenSidebar, isVisible = true }: Pro
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current;
|
||||||
if (!textarea) return;
|
if (!textarea) return;
|
||||||
// scrollHeight reflects the full draft even when the textarea is already
|
// Measure offscreen: collapsing a focused textarea can make its scroll
|
||||||
// capped. Avoid collapsing a focused field to its minimum height while
|
// parent reveal the caret, while its own scrollHeight cannot measure a
|
||||||
// measuring: the browser may scroll the enclosing transcript to reveal its
|
// smaller draft until its current height has been removed.
|
||||||
// caret, and no ResizeObserver can correct that transient scroll once the
|
textarea.style.height = `${Math.min(156, Math.max(46, measureComposerHeight(textarea)))}px`;
|
||||||
// field returns to the same capped height.
|
|
||||||
textarea.style.height = `${Math.min(156, Math.max(46, textarea.scrollHeight))}px`;
|
|
||||||
}, [draft]);
|
}, [draft]);
|
||||||
|
|
||||||
const updateDraft = (value: string) => {
|
const updateDraft = (value: string) => {
|
||||||
|
|||||||
@@ -107,11 +107,19 @@ try {
|
|||||||
if (process.env.RECORD_ONLY !== '1') for (const result of all) {
|
if (process.env.RECORD_ONLY !== '1') for (const result of all) {
|
||||||
for (const sample of result.bottom) assert.ok(Math.abs(sample.gap) <= 1,
|
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)}`);
|
`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;
|
const before = result.nonbottom.find(sample => sample.before)?.before;
|
||||||
for (const sample of result.nonbottom.filter(sample => sample.label?.startsWith('nonbottom-'))) {
|
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.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 })}`);
|
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');
|
process.stdout.write(JSON.stringify({ browser, all }, null, 2) + '\n');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user