mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
(cherry picked from commit a0da52a076)
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
239 lines
8.1 KiB
TypeScript
239 lines
8.1 KiB
TypeScript
import * as React from "react";
|
|
import { CheckCheck } from "lucide-react";
|
|
|
|
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
|
import { cn } from "@/shared/lib/cn";
|
|
import { useProfilePanel } from "@/shared/context/ProfilePanelContext";
|
|
import { Markdown } from "@/shared/ui/markdown";
|
|
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
|
import { useAgentSessionTranscriptVariant } from "../agentSessionTranscriptContext";
|
|
import { MessageLinkHoverCue } from "../activityRenderClasses/MessageLinkHoverCue";
|
|
import { TranscriptTimestamp } from "../activityRenderClasses/TranscriptTimestamp";
|
|
import { useTranscriptBubbleOverflow } from "../activityRenderClasses/useTranscriptBubbleOverflow";
|
|
import { compactSummaryTone } from "./CompactToolSummaryRow";
|
|
import type { SentMessageLink } from "./messageLinks";
|
|
import { SentMessageContextDialog } from "./SentMessageContextDialog";
|
|
import { useSentMessageBody } from "./useSentMessageBody";
|
|
|
|
export function CompactMessageSummary({
|
|
args,
|
|
avatarUrl,
|
|
description,
|
|
displayName,
|
|
duration,
|
|
hasArgs,
|
|
hasResult,
|
|
isError,
|
|
label,
|
|
messageLink,
|
|
preview,
|
|
pubkey,
|
|
result,
|
|
timestamp,
|
|
}: {
|
|
args: Record<string, unknown>;
|
|
avatarUrl: string | null;
|
|
description?: string;
|
|
displayName: string;
|
|
duration: string | null;
|
|
hasArgs: boolean;
|
|
hasResult: boolean;
|
|
isError: boolean;
|
|
label: string;
|
|
messageLink: SentMessageLink | null;
|
|
preview: string | null;
|
|
pubkey: string;
|
|
result: string;
|
|
timestamp: string;
|
|
}) {
|
|
const [detailsOpen, setDetailsOpen] = React.useState(false);
|
|
const resolvedContent = useSentMessageBody(messageLink, preview);
|
|
const variant = useAgentSessionTranscriptVariant();
|
|
const { goChannel } = useAppNavigation();
|
|
const { openProfilePanel } = useProfilePanel();
|
|
const isCompactPreview = variant === "compactPreview";
|
|
const isConversation = variant === "conversation";
|
|
// Focus mode is a reading view, so sent messages remain fully visible there.
|
|
// The default activity surface keeps the compact clamp.
|
|
const shouldClampBubble = !isCompactPreview && !isConversation;
|
|
const [bubbleRef, hasBubbleOverflow] =
|
|
useTranscriptBubbleOverflow(shouldClampBubble);
|
|
const canOpenMessage = !isCompactPreview && messageLink !== null;
|
|
const mutedTone = compactSummaryTone();
|
|
const avatarClassName = cn(
|
|
"mr-2 mt-1 shrink-0",
|
|
isCompactPreview ? "size-5" : "size-7",
|
|
);
|
|
const handleBubbleClick = React.useCallback(
|
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
if (!messageLink || isNestedInteractiveTarget(event)) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void goChannel(messageLink.channelId, {
|
|
messageId: messageLink.messageId,
|
|
});
|
|
},
|
|
[goChannel, messageLink],
|
|
);
|
|
const handleBubbleKeyDown = React.useCallback(
|
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
if (
|
|
!messageLink ||
|
|
isNestedInteractiveTarget(event) ||
|
|
(event.key !== "Enter" && event.key !== " ")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void goChannel(messageLink.channelId, {
|
|
messageId: messageLink.messageId,
|
|
});
|
|
},
|
|
[goChannel, messageLink],
|
|
);
|
|
const bubbleLinkProps = canOpenMessage
|
|
? {
|
|
onClick: handleBubbleClick,
|
|
onKeyDown: handleBubbleKeyDown,
|
|
role: "link" as const,
|
|
tabIndex: 0,
|
|
}
|
|
: {};
|
|
return (
|
|
<>
|
|
<div className="flex max-w-full flex-row items-start justify-start">
|
|
{openProfilePanel && !isCompactPreview ? (
|
|
<button
|
|
aria-label={`Open ${displayName} profile`}
|
|
className={cn(
|
|
avatarClassName,
|
|
"pointer-events-auto rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
)}
|
|
onClick={(event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
openProfilePanel(pubkey);
|
|
}}
|
|
type="button"
|
|
>
|
|
<UserAvatar
|
|
avatarUrl={avatarUrl}
|
|
className="size-full text-xs"
|
|
displayName={displayName}
|
|
size="sm"
|
|
testId="transcript-agent-sent-avatar"
|
|
/>
|
|
</button>
|
|
) : (
|
|
<UserAvatar
|
|
avatarUrl={avatarUrl}
|
|
className={cn(
|
|
avatarClassName,
|
|
isCompactPreview ? "text-3xs" : "text-xs",
|
|
)}
|
|
displayName={displayName}
|
|
size="sm"
|
|
testId="transcript-agent-sent-avatar"
|
|
/>
|
|
)}
|
|
<div
|
|
className={cn(
|
|
"flex min-w-0 flex-1 flex-col items-start gap-1",
|
|
isConversation && "pr-9",
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"w-full min-w-0 rounded-2xl border px-3 py-2",
|
|
isCompactPreview
|
|
? "text-xs leading-4"
|
|
: "text-sm leading-relaxed",
|
|
shouldClampBubble && "relative max-h-36 overflow-hidden",
|
|
isConversation && "relative pr-4",
|
|
canOpenMessage &&
|
|
"group/bubble cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
isCompactPreview
|
|
? isError
|
|
? "border-destructive/25 bg-destructive/10 text-destructive"
|
|
: "border-transparent bg-muted text-foreground"
|
|
: isError
|
|
? "border-destructive/25 bg-destructive/10 text-destructive"
|
|
: "border-transparent bg-muted text-foreground",
|
|
canOpenMessage &&
|
|
(isError ? "hover:bg-destructive/15" : "hover:bg-muted/90"),
|
|
)}
|
|
data-testid="transcript-tool-message-preview"
|
|
ref={bubbleRef}
|
|
{...bubbleLinkProps}
|
|
>
|
|
<Markdown
|
|
className={isCompactPreview ? "text-xs leading-4" : "leading-5"}
|
|
content={resolvedContent || "Message content unavailable."}
|
|
/>
|
|
{hasBubbleOverflow ? (
|
|
<span
|
|
className={cn(
|
|
"pointer-events-none absolute inset-x-0 bottom-0 h-8 rounded-b-2xl bg-linear-to-b from-transparent",
|
|
isError
|
|
? "to-destructive/10"
|
|
: isCompactPreview
|
|
? "to-muted"
|
|
: "to-muted",
|
|
)}
|
|
/>
|
|
) : null}
|
|
{canOpenMessage ? <MessageLinkHoverCue /> : null}
|
|
</div>
|
|
<div className="inline-flex max-w-full items-center gap-1.5 px-1">
|
|
<TranscriptTimestamp
|
|
messageLink={messageLink}
|
|
timestamp={timestamp}
|
|
/>
|
|
<button
|
|
aria-label="Show sent message context"
|
|
className={cn(
|
|
"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm transition-colors hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
mutedTone,
|
|
)}
|
|
data-testid="transcript-sent-message-context-button"
|
|
onClick={() => setDetailsOpen(true)}
|
|
title="Show sent message context"
|
|
type="button"
|
|
>
|
|
<CheckCheck className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<SentMessageContextDialog
|
|
args={args}
|
|
description={description}
|
|
duration={duration}
|
|
hasArgs={hasArgs}
|
|
hasResult={hasResult}
|
|
isError={isError}
|
|
label={label}
|
|
onOpenChange={setDetailsOpen}
|
|
open={detailsOpen}
|
|
preview={preview}
|
|
result={result}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function isNestedInteractiveTarget(
|
|
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
|
|
) {
|
|
const target =
|
|
event.target instanceof Element
|
|
? event.target.closest(
|
|
"a,button,input,select,textarea,summary,[role='button'],[role='link']",
|
|
)
|
|
: null;
|
|
|
|
return target !== null && target !== event.currentTarget;
|
|
}
|