authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-11 18:08:34-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-12 02:12:40-08:00
log36cdceb4bff336dbb53ad638806e4e1a88043b73
tree916328c717e57901f6dfe3cf610ad9bd1106f993
parent3bfda73dcec029957c6894dc68ffff7f78a03047

Fix ChatView height scroll


2 files changed, 32 insertions(+), 9 deletions(-)

apps/web/src/components/ChatView.tsx+3-3
...@@ -628,7 +628,7 @@ export default function ChatView() {...@@ -628,7 +628,7 @@ export default function ChatView() {
628 // Empty state: no active thread628 // Empty state: no active thread
629 if (!activeThread) {629 if (!activeThread) {
630 return (630 return (
631 <div className="flex flex-1 flex-col bg-background text-muted-foreground/40">631 <div className="flex min-h-0 flex-1 flex-col bg-background text-muted-foreground/40">
632 {isElectron && <div className="drag-region h-[52px] shrink-0" />}632 {isElectron && <div className="drag-region h-[52px] shrink-0" />}
633 <div className="flex flex-1 items-center justify-center">633 <div className="flex flex-1 items-center justify-center">
634 <div className="text-center">634 <div className="text-center">
...@@ -640,7 +640,7 @@ export default function ChatView() {...@@ -640,7 +640,7 @@ export default function ChatView() {
640 }640 }
641641
642 return (642 return (
643 <div className="flex flex-1 flex-col bg-background">643 <div className="flex min-h-0 flex-1 flex-col bg-background">
644 {/* Top bar */}644 {/* Top bar */}
645 <header645 <header
646 className={`flex items-center justify-between border-b border-border px-5 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`}646 className={`flex items-center justify-between border-b border-border px-5 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`}
...@@ -787,7 +787,7 @@ export default function ChatView() {...@@ -787,7 +787,7 @@ export default function ChatView() {
787 )}787 )}
788788
789 {/* Messages */}789 {/* Messages */}
790 <div className="flex-1 overflow-y-auto px-5 py-4">790 <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
791 {activeThread.messages.length === 0 && !isWorking ? (791 {activeThread.messages.length === 0 && !isWorking ? (
792 <div className="flex h-full items-center justify-center">792 <div className="flex h-full items-center justify-center">
793 <p className="text-sm text-muted-foreground/30">793 <p className="text-sm text-muted-foreground/30">
apps/web/src/components/ThreadTerminalDrawer.tsx+29-6
...@@ -129,6 +129,7 @@ export default function ThreadTerminalDrawer({...@@ -129,6 +129,7 @@ export default function ThreadTerminalDrawer({
129 startY: number;129 startY: number;
130 startHeight: number;130 startHeight: number;
131 } | null>(null);131 } | null>(null);
132 const didResizeDuringDragRef = useRef(false);
132133
133 useEffect(() => {134 useEffect(() => {
134 onHeightChangeRef.current = onHeightChange;135 onHeightChangeRef.current = onHeightChange;
...@@ -152,11 +153,17 @@ export default function ThreadTerminalDrawer({...@@ -152,11 +153,17 @@ export default function ThreadTerminalDrawer({
152 [],153 [],
153 );154 );
154155
155 const fitAndResizeTerminal = useCallback(() => {156 const fitAndResizeTerminal = useCallback((preserveBottom = false) => {
156 const activeTerminal = terminalRef.current;157 const activeTerminal = terminalRef.current;
157 const activeFitAddon = fitAddonRef.current;158 const activeFitAddon = fitAddonRef.current;
158 if (!activeTerminal || !activeFitAddon) return;159 if (!activeTerminal || !activeFitAddon) return;
160 const wasAtBottom =
161 preserveBottom &&
162 activeTerminal.buffer.active.viewportY >= activeTerminal.buffer.active.baseY;
159 activeFitAddon.fit();163 activeFitAddon.fit();
164 if (wasAtBottom) {
165 activeTerminal.scrollToBottom();
166 }
160 void api.terminal167 void api.terminal
161 .resize({168 .resize({
162 threadId,169 threadId,
...@@ -177,13 +184,14 @@ export default function ThreadTerminalDrawer({...@@ -177,13 +184,14 @@ export default function ThreadTerminalDrawer({
177 if (event.button !== 0) return;184 if (event.button !== 0) return;
178 event.preventDefault();185 event.preventDefault();
179 event.currentTarget.setPointerCapture(event.pointerId);186 event.currentTarget.setPointerCapture(event.pointerId);
187 didResizeDuringDragRef.current = false;
180 resizeStateRef.current = {188 resizeStateRef.current = {
181 pointerId: event.pointerId,189 pointerId: event.pointerId,
182 startY: event.clientY,190 startY: event.clientY,
183 startHeight: drawerHeight,191 startHeight: drawerHeightRef.current,
184 };192 };
185 },193 },
186 [drawerHeight],194 [],
187 );195 );
188196
189 const handleResizePointerMove = useCallback(197 const handleResizePointerMove = useCallback(
...@@ -194,6 +202,11 @@ export default function ThreadTerminalDrawer({...@@ -194,6 +202,11 @@ export default function ThreadTerminalDrawer({
194 const clampedHeight = clampDrawerHeight(202 const clampedHeight = clampDrawerHeight(
195 resizeState.startHeight + (resizeState.startY - event.clientY),203 resizeState.startHeight + (resizeState.startY - event.clientY),
196 );204 );
205 if (clampedHeight === drawerHeightRef.current) {
206 return;
207 }
208 didResizeDuringDragRef.current = true;
209 drawerHeightRef.current = clampedHeight;
197 setDrawerHeight(clampedHeight);210 setDrawerHeight(clampedHeight);
198 },211 },
199 [],212 [],
...@@ -207,9 +220,13 @@ export default function ThreadTerminalDrawer({...@@ -207,9 +220,13 @@ export default function ThreadTerminalDrawer({
207 if (event.currentTarget.hasPointerCapture(event.pointerId)) {220 if (event.currentTarget.hasPointerCapture(event.pointerId)) {
208 event.currentTarget.releasePointerCapture(event.pointerId);221 event.currentTarget.releasePointerCapture(event.pointerId);
209 }222 }
223 if (!didResizeDuringDragRef.current) {
224 return;
225 }
210 syncHeight(drawerHeightRef.current);226 syncHeight(drawerHeightRef.current);
227 fitAndResizeTerminal(true);
211 },228 },
212 [syncHeight],229 [fitAndResizeTerminal, syncHeight],
213 );230 );
214231
215 useEffect(() => {232 useEffect(() => {
...@@ -219,7 +236,7 @@ export default function ThreadTerminalDrawer({...@@ -219,7 +236,7 @@ export default function ThreadTerminalDrawer({
219 if (changed) {236 if (changed) {
220 setDrawerHeight(clampedHeight);237 setDrawerHeight(clampedHeight);
221 } else {238 } else {
222 fitAndResizeTerminal();239 fitAndResizeTerminal(true);
223 }240 }
224 if (!resizeStateRef.current) {241 if (!resizeStateRef.current) {
225 syncHeight(clampedHeight);242 syncHeight(clampedHeight);
...@@ -366,7 +383,9 @@ export default function ThreadTerminalDrawer({...@@ -366,7 +383,9 @@ export default function ThreadTerminalDrawer({
366 }383 }
367 });384 });
368385
369 const fitTimer = window.setTimeout(fitAndResizeTerminal, 30);386 const fitTimer = window.setTimeout(() => {
387 fitAndResizeTerminal(true);
388 }, 30);
370 void openTerminal();389 void openTerminal();
371390
372 return () => {391 return () => {
...@@ -396,8 +415,12 @@ export default function ThreadTerminalDrawer({...@@ -396,8 +415,12 @@ export default function ThreadTerminalDrawer({
396 const terminal = terminalRef.current;415 const terminal = terminalRef.current;
397 const fitAddon = fitAddonRef.current;416 const fitAddon = fitAddonRef.current;
398 if (!terminal || !fitAddon) return;417 if (!terminal || !fitAddon) return;
418 const wasAtBottom = terminal.buffer.active.viewportY >= terminal.buffer.active.baseY;
399 const frame = window.requestAnimationFrame(() => {419 const frame = window.requestAnimationFrame(() => {
400 fitAddon.fit();420 fitAddon.fit();
421 if (wasAtBottom) {
422 terminal.scrollToBottom();
423 }
401 void api.terminal424 void api.terminal
402 .resize({425 .resize({
403 threadId,426 threadId,