FN-6212: cap chat input height on tablets

Limit the chat composer growth on tablet viewports while preserving desktop autosizing.

- Add a tablet-specific 200px max height for the chat input textarea.
- Apply the same cap in ChatView autosize calculations when tablet mode is active.
- Generalize chat input height helpers and cover the tablet cap in autosize tests.

Files changed:
 packages/dashboard/app/components/ChatView.css       |  9 +++++++++
 packages/dashboard/app/components/ChatView.tsx       | 20 +++++++++++++-------
 packages/dashboard/app/components/QuickChatFAB.tsx   |  4 ++--
 .../__tests__/ChatView.chat-input-autosize.test.tsx  | 14 ++++++++++++++
 4 files changed, 38 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-6212

Fusion-Task-Lineage: b11cd658-8289-4795-8e39-7d03a1d51d41
This commit is contained in:
gsxdsm
2026-06-11 00:09:41 -07:00
parent 1f366c8de1
commit 28e74c74db
4 changed files with 38 additions and 9 deletions

View File

@@ -1476,6 +1476,15 @@
overscroll-behavior: contain;
}
/* FN-6212: On tablet viewports (769–1024px), cap the composer to 200px
so it doesn't consume most of the visible thread. Desktop keeps 640px;
mobile is already flex-constrained by the chat-thread layout. */
@media (min-width: 769px) and (max-width: 1024px) {
.chat-input-textarea {
max-height: 200px;
}
}
.chat-input-textarea:focus {
outline: none;
border-color: var(--accent);

View File

@@ -60,19 +60,23 @@ export interface ChatViewProps {
// Keep a generous cap so pasted multi-paragraph text stays visible while
// still preventing the composer from overtaking the message pane on short viewports.
const CHAT_INPUT_MAX_HEIGHT_PX = 640;
const TABLET_INPUT_MAX_HEIGHT_PX = 200;
/** Canonical definition lives in packages/dashboard/src/chat.ts (ROOM_SKIP_SENTINEL). */
const ROOM_SKIP_SENTINEL = "__SKIP__";
let chatViewWasPreviouslyInactive = false;
export function resolveChatInputOverflowY(scrollHeight: number): "auto" | "hidden" {
return scrollHeight > CHAT_INPUT_MAX_HEIGHT_PX ? "auto" : "hidden";
export function resolveChatInputOverflowY(
scrollHeight: number,
maxHeight: number = CHAT_INPUT_MAX_HEIGHT_PX,
): "auto" | "hidden" {
return scrollHeight > maxHeight ? "auto" : "hidden";
}
export function clampChatInputHeight(scrollHeight: number): number {
export function clampChatInputHeight(scrollHeight: number, maxHeight: number = CHAT_INPUT_MAX_HEIGHT_PX): number {
// Floor matches QuickChat (clampQuickChatInputHeight) and the CSS min-height,
// so a 0-scrollHeight measurement (e.g. before layout) still yields a
// sensible inline height instead of collapsing the composer to 0.
return Math.max(40, Math.min(scrollHeight, CHAT_INPUT_MAX_HEIGHT_PX));
return Math.max(40, Math.min(scrollHeight, maxHeight));
}
function formatRelativeTime(dateStr: string, t: TFunction<"app">): string {
@@ -1863,10 +1867,12 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
return;
}
const effectiveMax = mode === "tablet" ? TABLET_INPUT_MAX_HEIGHT_PX : CHAT_INPUT_MAX_HEIGHT_PX;
composer.style.height = "auto";
composer.style.height = `${clampChatInputHeight(composer.scrollHeight)}px`;
composer.style.overflowY = resolveChatInputOverflowY(composer.scrollHeight);
}, []);
composer.style.height = `${clampChatInputHeight(composer.scrollHeight, effectiveMax)}px`;
composer.style.overflowY = resolveChatInputOverflowY(composer.scrollHeight, effectiveMax);
}, [mode]);
const handleComposerRef = useCallback((textarea: HTMLTextAreaElement | null) => {
inputRef.current = textarea;

View File

@@ -109,10 +109,10 @@ function formatModelTagName(modelInfo: ModelInfo | null, parsedSelection: Parsed
.trim();
}
export function clampQuickChatInputHeight(scrollHeight: number): number {
export function clampQuickChatInputHeight(scrollHeight: number, maxHeight: number = 640): number {
// Match ChatView's 640px cap so pasted multi-paragraph text remains visible,
// while keeping an upper bound that protects message visibility on short screens.
return Math.max(40, Math.min(scrollHeight, 640));
return Math.max(40, Math.min(scrollHeight, maxHeight));
}
function truncateToolValue(value: string, maxLength: number): string {

View File

@@ -33,9 +33,19 @@ describe("ChatView chat input autosize", () => {
expect(stopRule?.[0]).toContain("min-height: var(--chat-input-control-size)");
});
it("caps textarea max-height at 200px on tablet viewports", () => {
const tabletRule = chatViewCss.match(
/@media \(min-width: 769px\) and \(max-width: 1024px\)\s*\{\s*\.chat-input-textarea\s*\{[^}]*\}\s*\}/,
);
expect(tabletRule).not.toBeNull();
expect(tabletRule?.[0]).toContain("max-height: 200px");
});
it("clamps oversized textarea growth to the new max height", () => {
expect(clampChatInputHeight(600)).toBe(600);
expect(clampChatInputHeight(800)).toBe(640);
expect(clampChatInputHeight(800, 200)).toBe(200);
expect(clampChatInputHeight(600)).not.toBe(120);
});
@@ -45,7 +55,11 @@ describe("ChatView chat input autosize", () => {
it("keeps overflow hidden until content exceeds the max height cap", () => {
expect(resolveChatInputOverflowY(80)).toBe("hidden");
expect(resolveChatInputOverflowY(200)).toBe("hidden");
expect(resolveChatInputOverflowY(201)).toBe("hidden");
expect(resolveChatInputOverflowY(640)).toBe("hidden");
expect(resolveChatInputOverflowY(641)).toBe("auto");
expect(resolveChatInputOverflowY(200, 200)).toBe("hidden");
expect(resolveChatInputOverflowY(201, 200)).toBe("auto");
});
});