refactor(webui): shrink fork implementation

This commit is contained in:
Xubin Ren
2026-06-10 04:26:06 +08:00
parent 1f926e3769
commit 916525f94a
24 changed files with 134 additions and 879 deletions
+46 -152
View File
@@ -5,13 +5,13 @@ import {
useRef,
useState,
type ReactNode,
type SVGProps,
} from "react";
import {
Check,
ChevronRight,
Clock3,
Copy,
GitFork,
ImageIcon,
Sparkles,
Wrench,
@@ -22,12 +22,6 @@ import { AttachmentTile } from "@/components/AttachmentTile";
import { CliAppMentionText } from "@/components/CliAppMentionText";
import { ImageLightbox } from "@/components/ImageLightbox";
import { MarkdownText, preloadMarkdownText } from "@/components/MarkdownText";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { copyTextToClipboard } from "@/lib/clipboard";
import { formatTurnLatency } from "@/lib/format";
@@ -90,7 +84,7 @@ export function MessageBubble({
};
}, []);
const onCopyMessage = useCallback(() => {
const onCopyAssistantReply = useCallback(() => {
void copyTextToClipboard(message.content).then((ok) => {
if (!ok) return;
setCopied(true);
@@ -114,11 +108,6 @@ export function MessageBubble({
const hasImages = images.length > 0;
const hasMedia = media.length > 0;
const hasText = message.content.trim().length > 0;
const showUserActions = hasText;
const timeLabel = formatMessageClock(message.createdAt);
const copyLabel = copied
? t("message.copiedMessage", { defaultValue: "Copied" })
: t("message.copyMessage", { defaultValue: "Copy" });
return (
<div
className={cn(
@@ -144,43 +133,6 @@ export function MessageBubble({
/>
</p>
) : null}
{showUserActions ? (
<TooltipProvider delayDuration={180} skipDelayDuration={80}>
<div
className={cn(
"mt-0.5 flex h-8 items-center justify-end gap-1 self-end",
"text-[13px] text-muted-foreground/65 opacity-0 transition-opacity duration-150",
"group-focus-within:opacity-100 group-hover:opacity-100",
)}
>
{hasText ? (
<MessageActionTooltip label={copyLabel}>
<button
type="button"
onClick={onCopyMessage}
aria-label={copyLabel}
className={cn(
"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full",
"transition-colors hover:bg-muted/55 hover:text-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
>
{copied ? (
<Check className="h-3.5 w-3.5" aria-hidden />
) : (
<Copy className="h-3.5 w-3.5" aria-hidden />
)}
</button>
</MessageActionTooltip>
) : null}
{timeLabel ? (
<span className="ml-1 shrink-0 select-none tabular-nums" title={timeLabel}>
{timeLabel}
</span>
) : null}
</div>
</TooltipProvider>
) : null}
</div>
);
}
@@ -235,54 +187,50 @@ export function MessageBubble({
</MarkdownText>
{media.length > 0 ? <MessageMedia media={media} align="left" /> : null}
{showAssistantFooterRow ? (
<TooltipProvider delayDuration={180} skipDelayDuration={80}>
<div className="mt-2 flex min-h-8 flex-wrap items-center gap-x-2 gap-y-1 text-muted-foreground">
{showCopyButton ? (
<MessageActionTooltip label={copyReplyLabel}>
<button
type="button"
onClick={onCopyMessage}
aria-label={copyReplyLabel}
className={cn(
"inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full",
"transition-colors hover:bg-muted/55 hover:text-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
>
{copied ? (
<Check className="h-4 w-4" aria-hidden />
) : (
<Copy className="h-4 w-4" aria-hidden />
)}
</button>
</MessageActionTooltip>
) : null}
{showForkButton ? (
<MessageActionTooltip label={forkLabel}>
<button
type="button"
onClick={onForkFromHere}
aria-label={forkLabel}
className={cn(
"inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full",
"transition-colors hover:bg-muted/55 hover:text-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
>
<ForkFromHereIcon className="h-4 w-4" aria-hidden />
</button>
</MessageActionTooltip>
) : null}
{showLatencyFooter ? (
<span
className="text-[11px] leading-none text-muted-foreground/70 tabular-nums"
title={t("message.turnLatencyTitle")}
>
{formatTurnLatency(latencyMs)}
</span>
) : null}
</div>
</TooltipProvider>
<div className="mt-2 flex min-h-8 flex-wrap items-center gap-x-2 gap-y-1 text-muted-foreground">
{showCopyButton ? (
<button
type="button"
onClick={onCopyAssistantReply}
aria-label={copyReplyLabel}
title={copyReplyLabel}
className={cn(
"inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full",
"transition-colors hover:bg-muted/55 hover:text-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
>
{copied ? (
<Check className="h-4 w-4" aria-hidden />
) : (
<Copy className="h-4 w-4" aria-hidden />
)}
</button>
) : null}
{showForkButton ? (
<button
type="button"
onClick={onForkFromHere}
aria-label={forkLabel}
title={forkLabel}
className={cn(
"inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full",
"transition-colors hover:bg-muted/55 hover:text-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
>
<GitFork className="h-4 w-4" aria-hidden />
</button>
) : null}
{showLatencyFooter ? (
<span
className="text-[11px] leading-none text-muted-foreground/70 tabular-nums"
title={t("message.turnLatencyTitle")}
>
{formatTurnLatency(latencyMs)}
</span>
) : null}
</div>
) : null}
</>
)}
@@ -290,27 +238,6 @@ export function MessageBubble({
);
}
function MessageActionTooltip({
label,
children,
}: {
label: string;
children: ReactNode;
}) {
return (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent
side="top"
align="center"
className="rounded-full border-border/70 bg-background px-2.5 py-1 text-[12px] font-medium text-foreground shadow-[0_8px_24px_rgba(15,23,42,0.13)] dark:border-white/10 dark:bg-neutral-900 dark:text-white"
>
{label}
</TooltipContent>
</Tooltip>
);
}
function AutomationSourceBadge({ label, triggerLabel }: { label: string; triggerLabel: string }) {
return (
<div
@@ -330,39 +257,6 @@ function AutomationSourceBadge({ label, triggerLabel }: { label: string; trigger
);
}
function formatMessageClock(createdAt: number): string {
if (!Number.isFinite(createdAt) || createdAt <= 0) return "";
try {
return new Intl.DateTimeFormat(undefined, {
hour: "2-digit",
minute: "2-digit",
}).format(new Date(createdAt));
} catch {
return "";
}
}
function ForkFromHereIcon({ className, ...props }: SVGProps<SVGSVGElement>) {
// Tabler Icons "arrow-fork" (MIT, Copyright Paweł Kuna).
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M16 3h5v5" />
<path d="M8 3h-5v5" />
<path d="M21 3l-7.536 7.536a5 5 0 0 0 -1.464 3.534v6.93" />
<path d="M3 3l7.536 7.536a5 5 0 0 1 1.464 3.534v.93" />
</svg>
);
}
function mergeMcpMentionPresets(
presets: McpPresetInfo[],
attachments: UIMcpPresetAttachment[] | undefined,