authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 19:14:30-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-27 19:27:05-08:00
logaa267ebb0597ed8e3f367410ba4636465c1643f6
tree2f80af647bb982cae0c43cbb9d80383955897762
parent1a33f25970445cfd2367eefaffe41f52abcabee0

clamp


1 files changed, 29 insertions(+), 27 deletions(-)

apps/web/src/components/ChatView.tsx+29-27
...@@ -111,6 +111,7 @@ import { SidebarTrigger } from "./ui/sidebar";...@@ -111,6 +111,7 @@ import { SidebarTrigger } from "./ui/sidebar";
111import { newCommandId, newMessageId } from "~/lib/utils";111import { newCommandId, newMessageId } from "~/lib/utils";
112import { readNativeApi } from "~/nativeApi";112import { readNativeApi } from "~/nativeApi";
113import { useAppSettings } from "../appSettings";113import { useAppSettings } from "../appSettings";
114import { clamp } from "effect/Number";
114115
115function formatMessageMeta(createdAt: string, duration: string | null): string {116function formatMessageMeta(createdAt: string, duration: string | null): string {
116 if (!duration) return formatTimestamp(createdAt);117 if (!duration) return formatTimestamp(createdAt);
...@@ -2338,13 +2339,7 @@ const MessageCopyButton = memo(function MessageCopyButton({ text }: { text: stri...@@ -2338,13 +2339,7 @@ const MessageCopyButton = memo(function MessageCopyButton({ text }: { text: stri
2338 }, [text]);2339 }, [text]);
23392340
2340 return (2341 return (
2341 <Button2342 <Button type="button" size="xs" variant="outline" onClick={handleCopy} title="Copy message">
2342 type="button"
2343 size="xs"
2344 variant="outline"
2345 onClick={handleCopy}
2346 title="Copy message"
2347 >
2348 {copied ? <CheckIcon className="size-3 text-success" /> : <CopyIcon className="size-3" />}2343 {copied ? <CheckIcon className="size-3 text-success" /> : <CopyIcon className="size-3" />}
2349 </Button>2344 </Button>
2350 );2345 );
...@@ -2499,7 +2494,10 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -2499,7 +2494,10 @@ const MessagesTimeline = memo(function MessagesTimeline({
2499 return firstCurrentTurnRowIndex;2494 return firstCurrentTurnRowIndex;
2500 }, [activeTurnInProgress, activeTurnStartedAt, rows]);2495 }, [activeTurnInProgress, activeTurnStartedAt, rows]);
25012496
2502 const virtualizedRowCount = Math.max(0, Math.min(rows.length, firstUnvirtualizedRowIndex));2497 const virtualizedRowCount = clamp(firstUnvirtualizedRowIndex, {
2498 minimum: 0,
2499 maximum: rows.length,
2500 });
25032501
2504 const rowVirtualizer = useVirtualizer({2502 const rowVirtualizer = useVirtualizer({
2505 count: virtualizedRowCount,2503 count: virtualizedRowCount,
...@@ -2594,25 +2592,29 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -2594,25 +2592,29 @@ const MessagesTimeline = memo(function MessagesTimeline({
2594 <div className="group relative max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3">2592 <div className="group relative max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3">
2595 {userImages.length > 0 && (2593 {userImages.length > 0 && (
2596 <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">2594 <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
2597 {userImages.map((image: NonNullable<TimelineMessage["attachments"]>[number]) => (2595 {userImages.map(
2598 <div2596 (image: NonNullable<TimelineMessage["attachments"]>[number]) => (
2599 key={image.id}2597 <div
2600 className="overflow-hidden rounded-lg border border-border/80 bg-background/70"2598 key={image.id}
2601 >2599 className="overflow-hidden rounded-lg border border-border/80 bg-background/70"
2602 {image.previewUrl ? (2600 >
2603 <img2601 {image.previewUrl ? (
2604 src={image.previewUrl}2602 <img
2605 alt={image.name}2603 src={image.previewUrl}
2606 className="h-full max-h-[220px] w-full cursor-zoom-in object-cover"2604 alt={image.name}
2607 onClick={() => onImageExpand({ src: image.previewUrl!, name: image.name })}2605 className="h-full max-h-[220px] w-full cursor-zoom-in object-cover"
2608 />2606 onClick={() =>
2609 ) : (2607 onImageExpand({ src: image.previewUrl!, name: image.name })
2610 <div className="flex min-h-[72px] items-center justify-center px-2 py-3 text-center text-[11px] text-muted-foreground/70">2608 }
2611 {image.name}2609 />
2612 </div>2610 ) : (
2613 )}2611 <div className="flex min-h-[72px] items-center justify-center px-2 py-3 text-center text-[11px] text-muted-foreground/70">
2614 </div>2612 {image.name}
2615 ))}2613 </div>
2614 )}
2615 </div>
2616 ),
2617 )}
2616 </div>2618 </div>
2617 )}2619 )}
2618 {row.message.text && (2620 {row.message.text && (