| ... | @@ -111,6 +111,7 @@ import { SidebarTrigger } from "./ui/sidebar"; | ... | @@ -111,6 +111,7 @@ import { SidebarTrigger } from "./ui/sidebar"; |
| 111 | import { newCommandId, newMessageId } from "~/lib/utils"; | 111 | import { newCommandId, newMessageId } from "~/lib/utils"; |
| 112 | import { readNativeApi } from "~/nativeApi"; | 112 | import { readNativeApi } from "~/nativeApi"; |
| 113 | import { useAppSettings } from "../appSettings"; | 113 | import { useAppSettings } from "../appSettings"; |
| | 114 | import { clamp } from "effect/Number"; |
| 114 | | 115 | |
| 115 | function formatMessageMeta(createdAt: string, duration: string | null): string { | 116 | function 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]); |
| 2339 | | 2340 | |
| 2340 | return ( | 2341 | return ( |
| 2341 | <Button | 2342 | <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]); |
| 2501 | | 2496 | |
| 2502 | const virtualizedRowCount = Math.max(0, Math.min(rows.length, firstUnvirtualizedRowIndex)); | 2497 | const virtualizedRowCount = clamp(firstUnvirtualizedRowIndex, { |
| | 2498 | minimum: 0, |
| | 2499 | maximum: rows.length, |
| | 2500 | }); |
| 2503 | | 2501 | |
| 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 | <div | 2596 | (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 | <img | 2601 | {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 && ( |