fix(app): restore native composer auto-growth (#3681)

Native multiline inputs were always scrollable, which kept iOS content-size measurement pinned to one line. Keep native sizing intrinsic below the cap and enable internal scrolling only after the composer reaches it.
This commit is contained in:
Mohamed Boudra
2026-08-22 11:36:22 +02:00
committed by GitHub
parent 4c6b79dc44
commit b4e0ff7685
3 changed files with 70 additions and 16 deletions
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { resolveComposerInputHeightStyle, shouldScrollComposerInput } from "./height-style";
describe("composer input height style", () => {
it("applies the measured height alongside its bounds", () => {
expect(
resolveComposerInputHeightStyle({
inputHeight: 96,
minInputHeight: 30,
maxInputHeight: 240,
applyMeasuredHeight: true,
}),
).toEqual({
height: 96,
minHeight: 30,
maxHeight: 240,
});
});
it("lets a native multiline input expose its intrinsic content height", () => {
expect(
resolveComposerInputHeightStyle({
inputHeight: 96,
minInputHeight: 30,
maxInputHeight: 240,
applyMeasuredHeight: false,
}),
).toEqual({
minHeight: 30,
maxHeight: 240,
});
});
it("keeps native measurement unconstrained until the composer reaches its cap", () => {
expect(shouldScrollComposerInput({ inputHeight: 30, maxInputHeight: 240 })).toBe(false);
expect(shouldScrollComposerInput({ inputHeight: 240, maxInputHeight: 240 })).toBe(true);
});
});
@@ -0,0 +1,24 @@
interface ComposerInputHeightStyleInput {
inputHeight: number;
minInputHeight: number;
maxInputHeight: number;
applyMeasuredHeight: boolean;
}
export function resolveComposerInputHeightStyle(input: ComposerInputHeightStyleInput) {
// Native must remain intrinsically sized below the cap so
// onContentSizeChange can observe growth. Web gets its height from the DOM
// mirror and therefore needs that measurement applied explicitly.
return {
...(input.applyMeasuredHeight ? { height: input.inputHeight } : {}),
minHeight: input.minInputHeight,
maxHeight: input.maxInputHeight,
};
}
export function shouldScrollComposerInput(input: {
inputHeight: number;
maxInputHeight: number;
}): boolean {
return input.inputHeight >= input.maxInputHeight;
}
+8 -16
View File
@@ -56,6 +56,7 @@ import { useIsCompactFormFactor } from "@/constants/layout";
import { useComposerKeyboardScope } from "@/composer/keyboard-scope"; import { useComposerKeyboardScope } from "@/composer/keyboard-scope";
import { RenderProfile } from "@/utils/render-profiler"; import { RenderProfile } from "@/utils/render-profiler";
import { useComposerHeightMirror } from "./height-mirror"; import { useComposerHeightMirror } from "./height-mirror";
import { resolveComposerInputHeightStyle, shouldScrollComposerInput } from "./height-style";
import { resolveComposerInputMode, type ComposerInputMode } from "@/composer/input-mode"; import { resolveComposerInputMode, type ComposerInputMode } from "@/composer/input-mode";
import type { NativePastedFile } from "@/composer/native-pasted-image"; import type { NativePastedFile } from "@/composer/native-pasted-image";
import { import {
@@ -990,20 +991,6 @@ function resolveMaxInputHeight(windowHeight: number): number {
return Math.max(DEFAULT_MAX_INPUT_HEIGHT, Math.floor(windowHeight * MAX_INPUT_VIEWPORT_RATIO)); return Math.max(DEFAULT_MAX_INPUT_HEIGHT, Math.floor(windowHeight * MAX_INPUT_VIEWPORT_RATIO));
} }
function computeTextInputHeightStyle(inputHeight: number, maxInputHeight: number) {
if (isWeb) {
return {
height: inputHeight,
minHeight: MIN_INPUT_HEIGHT,
maxHeight: maxInputHeight,
};
}
return {
minHeight: MIN_INPUT_HEIGHT,
maxHeight: maxInputHeight,
};
}
function isTextAreaLike(v: unknown): v is TextAreaHandle { function isTextAreaLike(v: unknown): v is TextAreaHandle {
return typeof v === "object" && v !== null && "scrollHeight" in v; return typeof v === "object" && v !== null && "scrollHeight" in v;
} }
@@ -1764,7 +1751,12 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
() => [ () => [
styles.textInput, styles.textInput,
mode.isMonospace && styles.textInputMonospace, mode.isMonospace && styles.textInputMonospace,
computeTextInputHeightStyle(inputHeight, maxInputHeight), resolveComposerInputHeightStyle({
inputHeight,
minInputHeight: MIN_INPUT_HEIGHT,
maxInputHeight,
applyMeasuredHeight: isWeb,
}),
], ],
[inputHeight, maxInputHeight, mode.isMonospace], [inputHeight, maxInputHeight, mode.isMonospace],
); );
@@ -1838,7 +1830,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
onFocus={handleInputFocus} onFocus={handleInputFocus}
onBlur={handleInputBlur} onBlur={handleInputBlur}
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled} editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
scrollEnabled={isWeb ? inputHeight >= maxInputHeight : true} scrollEnabled={shouldScrollComposerInput({ inputHeight, maxInputHeight })}
autoFocus={false} autoFocus={false}
onContentSizeChange={handleContentSizeChange} onContentSizeChange={handleContentSizeChange}
onKeyPress={shouldHandleWebKeyPress ? handleDesktopKeyPress : undefined} onKeyPress={shouldHandleWebKeyPress ? handleDesktopKeyPress : undefined}