Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import type {
SessionNotification,
} from "@agentclientprotocol/sdk";
import type { SessionUpdate, ToolCall } from "@features/sessions/types";
import { XCircle } from "@phosphor-icons/react";
import { Box, Flex, Text } from "@radix-ui/themes";
import { ArrowDown, XCircle } from "@phosphor-icons/react";
import { Box, Button, Flex, Text } from "@radix-ui/themes";
import {
type AcpMessage,
isJsonRpcNotification,
isJsonRpcRequest,
isJsonRpcResponse,
type UserShellExecuteParams,
} from "@shared/types/session-events";
import { memo, useLayoutEffect, useMemo, useRef } from "react";
import {
memo,
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { GitActionMessage, parseGitActionMessage } from "./GitActionMessage";
import { GitActionResult } from "./GitActionResult";
import { SessionFooter } from "./SessionFooter";
Expand Down Expand Up @@ -47,6 +54,9 @@ interface ConversationViewProps {
isCloud?: boolean;
}

const SCROLL_THRESHOLD = 100;
const SHOW_BUTTON_THRESHOLD = 300;

export function ConversationView({
events,
isPromptPending,
Expand All @@ -57,43 +67,82 @@ export function ConversationView({
const items = useMemo(() => buildConversationItems(events), [events]);
const lastTurn = items.filter((i): i is Turn => i.type === "turn").pop();

// Scroll to bottom on initial mount
const hasScrolledRef = useRef(false);
const isNearBottomRef = useRef(true);
const prevItemsLengthRef = useRef(0);
const [showScrollButton, setShowScrollButton] = useState(false);

// Update isNearBottom on scroll
useLayoutEffect(() => {
const el = scrollRef.current;
if (!el) return;

const handleScroll = () => {
const distanceFromBottom =
el.scrollHeight - el.scrollTop - el.clientHeight;
isNearBottomRef.current = distanceFromBottom <= SCROLL_THRESHOLD;
setShowScrollButton(distanceFromBottom > SHOW_BUTTON_THRESHOLD);
};

el.addEventListener("scroll", handleScroll);
return () => el.removeEventListener("scroll", handleScroll);
}, []);

// Scroll to bottom on first render and when new content arrives
useLayoutEffect(() => {
if (hasScrolledRef.current) return;
const el = scrollRef.current;
if (el && items.length > 0) {
if (!el) return;

const isNewContent = items.length > prevItemsLengthRef.current;
prevItemsLengthRef.current = items.length;

if (isNearBottomRef.current || isNewContent) {
el.scrollTop = el.scrollHeight;
hasScrolledRef.current = true;
}
}, [items]);

const scrollToBottom = useCallback(() => {
const el = scrollRef.current;
if (el) {
el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
}
}, []);

return (
<div
ref={scrollRef}
className="scrollbar-hide flex-1 overflow-auto bg-white p-2 pb-16 dark:bg-gray-1"
>
<div className="flex flex-col gap-3">
{items.map((item) =>
item.type === "turn" ? (
<TurnView
key={item.id}
turn={item}
repoPath={repoPath}
isCloud={isCloud}
/>
) : (
<UserShellExecuteView key={item.id} item={item} />
),
)}
<div className="relative flex-1">
<div
ref={scrollRef}
className="scrollbar-hide absolute inset-0 overflow-auto bg-white p-2 pb-16 dark:bg-gray-1"
>
<div className="flex flex-col gap-3">
{items.map((item) =>
item.type === "turn" ? (
<TurnView
key={item.id}
turn={item}
repoPath={repoPath}
isCloud={isCloud}
/>
) : (
<UserShellExecuteView key={item.id} item={item} />
),
)}
</div>
<SessionFooter
isPromptPending={isPromptPending}
lastGenerationDuration={
lastTurn?.isComplete ? lastTurn.durationMs : null
}
lastStopReason={lastTurn?.stopReason}
/>
</div>
<SessionFooter
isPromptPending={isPromptPending}
lastGenerationDuration={
lastTurn?.isComplete ? lastTurn.durationMs : null
}
lastStopReason={lastTurn?.stopReason}
/>
{showScrollButton && (
<Box className="absolute right-4 bottom-4 z-10">
<Button size="1" variant="solid" onClick={scrollToBottom}>
<ArrowDown size={14} weight="bold" />
Scroll to bottom
</Button>
</Box>
)}
</div>
);
}
Expand Down