diff --git a/desktop/src/features/agents/ui/AgentSessionToolItem.tsx b/desktop/src/features/agents/ui/AgentSessionToolItem.tsx index d8fa3b44d..9fc4b1111 100644 --- a/desktop/src/features/agents/ui/AgentSessionToolItem.tsx +++ b/desktop/src/features/agents/ui/AgentSessionToolItem.tsx @@ -1,60 +1,27 @@ import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { ArrowUpRight, ChevronDown, CircleDot, Wrench } from "lucide-react"; +import { ChevronDown } from "lucide-react"; -import { useAppNavigation } from "@/app/navigation/useAppNavigation"; -import { useUsersBatchQuery } from "@/features/profile/hooks"; -import { resolveUserLabel } from "@/features/profile/lib/identity"; -import type { Channel, UserProfileSummary } from "@/shared/api/types"; -import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext"; import { cn } from "@/shared/lib/cn"; import { rewriteRelayUrl } from "@/shared/lib/mediaUrl"; -import { Badge } from "@/shared/ui/badge"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; -import { UserAvatar } from "@/shared/ui/UserAvatar"; import type { TranscriptItem } from "./agentSessionTypes"; -import { - formatToolTitle, - getBuzzToolInfo, - getToolStatusDisplay, -} from "./agentSessionToolCatalog"; -import { - buildCompactToolSummary, - isCompactDeveloperTool, -} from "./agentSessionToolSummary"; -import { - asRecord, - formatCodeValue, - formatDuration, - formatTranscriptTime, - getResultArray, - getToolString, - getToolStringList, - shortenMiddle, -} from "./agentSessionUtils"; +import { getBuzzToolInfo } from "./agentSessionToolCatalog"; +import { buildCompactToolSummary } from "./agentSessionToolSummary"; +import { asRecord, formatCodeValue, formatDuration } from "./agentSessionUtils"; export function ToolItem({ compact = false, - isActive = false, item, }: { compact?: boolean; - isActive?: boolean; item: Extract; }) { const [isExpanded, setIsExpanded] = React.useState(false); - const status = getToolStatusDisplay(item.status, item.isError); const hasArgs = Object.keys(item.args).length > 0; const hasResult = item.result.trim().length > 0; const canonicalToolName = item.buzzToolName ?? item.toolName; const buzzTool = getBuzzToolInfo(canonicalToolName); - const ToolIcon = buzzTool?.icon ?? Wrench; - const showStatus = status.state !== "output-available"; - const toolTitle = formatToolTitle(canonicalToolName, item.title); - const useCompactSummary = isCompactDeveloperTool(item); - const compactSummary = useCompactSummary - ? buildCompactToolSummary(item) - : null; + const compactSummary = buildCompactToolSummary(item); const duration = getToolDuration(item); const handleToggle = React.useCallback( (event: React.SyntheticEvent) => { @@ -65,12 +32,7 @@ export function ToolItem({ return (
- {compactSummary ? ( - - ) : ( - <> - {ToolIcon ? ( - - ) : null} - - {toolTitle} - - {isActive ? ( - - - Live - - ) : null} - {buzzTool ? ( - - ) : null} - {showStatus ? ( - - - {status.label} - - ) : null} - - - - )} + { if (!thumbnailSrc || thumbnailFailed) return null; return resolveImageSrc(thumbnailSrc); @@ -175,7 +99,9 @@ function CompactToolSummaryRow({ return ( <> - {label} + + {label} + {resolvedThumbnail ? ( ) : preview ? ( {preview} ) : null} {duration ? ( - - {duration} - + {duration} ) : null} - + ); } @@ -433,239 +362,6 @@ function ToolCodeBlock({ ); } -const toolFullDateTimeFormat = new Intl.DateTimeFormat(undefined, { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", -}); - -function ToolTimestamp({ - duration, - item, -}: { - duration: string | null; - item: Extract; -}) { - const time = formatTranscriptTime(item.timestamp); - if (!time) return null; - const date = new Date(item.timestamp); - const fullDateTime = Number.isNaN(date.getTime()) - ? item.timestamp - : toolFullDateTimeFormat.format(date); - return ( - - - - {time} - {duration ? ` · ${duration}` : null} - - - {fullDateTime} - - ); -} - -function BuzzToolInlineAction({ - args, - result, -}: { - args: Record; - result: string; -}) { - const { channels } = useChannelNavigation(); - const { goChannel } = useAppNavigation(); - const resultValue = React.useMemo( - () => parseToolResultValue(result), - [result], - ); - const resultRecord = asRecord(resultValue); - const channelId = - getToolString(args, ["channel_id", "channelId"]) ?? - getToolString(resultRecord, ["channel_id", "channelId"]); - const pubkeys = React.useMemo( - () => getToolStringList(args, ["pubkeys", "pubkey"]), - [args], - ); - const profilesQuery = useUsersBatchQuery(pubkeys, { - enabled: pubkeys.length > 0, - }); - const profiles = profilesQuery.data?.profiles; - const openChannel = React.useCallback( - (messageId?: string) => { - if (!channelId) return; - void goChannel(channelId, messageId ? { messageId } : undefined); - }, - [channelId, goChannel], - ); - const action = React.useMemo( - () => - getBuzzToolInlineAction({ - args, - channelId, - channels, - openChannel, - profiles, - resultValue, - }), - [args, channelId, channels, openChannel, profiles, resultValue], - ); - - if (!action) { - return null; - } - - if (action.onClick) { - return ( - - ); - } - - return ( - - {action.avatar} - {action.label} - {action.value} - - ); -} - -type BuzzToolInlineActionModel = { - avatar?: React.ReactNode; - label: string; - value: string; - title: string; - onClick?: () => void; -}; - -function getBuzzToolInlineAction({ - args, - channelId, - channels, - openChannel, - profiles, - resultValue, -}: { - args: Record; - channelId: string | null; - channels: Channel[]; - openChannel: (messageId?: string) => void; - profiles: Record | undefined; - resultValue: unknown; -}): BuzzToolInlineActionModel | null { - const resultRecord = asRecord(resultValue); - const eventId = - getToolString(args, ["event_id", "eventId"]) ?? - getToolString(resultRecord, ["event_id", "eventId", "id"]); - - if (eventId && channelId) { - return { - label: resultRecord.accepted === true ? "posted" : "event", - onClick: () => openChannel(eventId), - title: eventId, - value: getChannelChipLabel(channels, channelId), - }; - } - - const messages = getResultArray(resultValue, resultRecord, "messages"); - if (messages) { - return { - label: "read", - onClick: channelId ? () => openChannel() : undefined, - title: `${messages.length} messages`, - value: `${messages.length} message${messages.length === 1 ? "" : "s"}`, - }; - } - - if (channelId) { - return { - label: "channel", - onClick: () => openChannel(), - title: channelId, - value: getChannelChipLabel(channels, channelId), - }; - } - - const workflowId = - getToolString(args, ["workflow_id", "workflowId"]) ?? - getToolString(resultRecord, ["workflow_id", "workflowId"]); - if (workflowId) { - return { - label: "workflow", - title: workflowId, - value: shortenMiddle(workflowId, 26), - }; - } - - const pubkeys = getToolStringList(args, ["pubkeys", "pubkey"]); - if (pubkeys.length > 0) { - if (pubkeys.length === 1) { - const pk = pubkeys[0]; - const displayName = resolveUserLabel({ pubkey: pk, profiles }); - const profile = profiles?.[pk.toLowerCase()]; - return { - avatar: ( - - ), - label: "user", - title: pk, - value: displayName, - }; - } - return { - label: "users", - title: pubkeys - .map((pk) => resolveUserLabel({ pubkey: pk, profiles })) - .join(", "), - value: `${pubkeys.length} users`, - }; - } - - const query = getToolString(args, ["query"]); - if (query) { - return { - label: "query", - title: query, - value: shortenMiddle(query, 30), - }; - } - - if (typeof resultRecord.accepted === "boolean") { - return { - label: "relay", - title: resultRecord.accepted ? "accepted" : "rejected", - value: resultRecord.accepted ? "accepted" : "rejected", - }; - } - - return null; -} - function parseToolResultValue(result: string): unknown { const trimmed = result.trim(); if (!trimmed) return null; @@ -682,8 +378,3 @@ function parseToolResultValue(result: string): unknown { return null; } } - -function getChannelChipLabel(channels: Channel[], channelId: string) { - const channel = channels.find((candidate) => candidate.id === channelId); - return channel ? `#${channel.name}` : `#${shortenMiddle(channelId, 22)}`; -} diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx index e45ff17dd..8a507d09d 100644 --- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx +++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx @@ -15,7 +15,6 @@ import { } from "@/features/profile/lib/identity"; import { cn } from "@/shared/lib/cn"; import { normalizePubkey } from "@/shared/lib/pubkey"; -import { Badge } from "@/shared/ui/badge"; import { Markdown } from "@/shared/ui/markdown"; import { Toggle } from "@/shared/ui/toggle"; import { UserAvatar } from "@/shared/ui/UserAvatar"; @@ -29,12 +28,35 @@ import { type TranscriptDisplayBlock, type TranscriptTurnSegment, } from "./agentSessionTranscriptGrouping"; -import { buildTranscriptPresentation } from "./agentSessionTranscriptPresentation"; import { formatTranscriptTime } from "./agentSessionUtils"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; -/** Dev-only: surface the observer wire label that produced each transcript row. */ -const SHOW_TRANSCRIPT_ACP_SOURCE = import.meta.env.DEV; +const TRANSCRIPT_ACP_SOURCE_STORAGE_KEY = "buzz:show-transcript-acp-source"; + +/** + * Opt-in only: source pills are useful while iterating on observer parsing, but + * they should not appear for every local dev session. + */ +const SHOW_TRANSCRIPT_ACP_SOURCE = shouldShowTranscriptAcpSource(); + +function shouldShowTranscriptAcpSource() { + const envValue = import.meta.env.VITE_SHOW_TRANSCRIPT_ACP_SOURCE; + if (envValue === "1" || envValue === "true") { + return true; + } + + if (typeof window === "undefined") { + return false; + } + + try { + return ( + window.localStorage.getItem(TRANSCRIPT_ACP_SOURCE_STORAGE_KEY) === "1" + ); + } catch { + return false; + } +} type AgentTranscriptIdentityProps = { agentAvatarUrl: string | null; @@ -48,7 +70,6 @@ export function AgentSessionTranscriptList({ agentPubkey, compact = false, emptyDescription, - isWorking = false, items, profiles, }: AgentTranscriptIdentityProps & { @@ -58,10 +79,6 @@ export function AgentSessionTranscriptList({ items: TranscriptItem[]; profiles?: UserProfileLookup; }) { - const presentation = React.useMemo( - () => buildTranscriptPresentation(items, isWorking), - [items, isWorking], - ); const displayBlocks = React.useMemo( () => buildTranscriptDisplayBlocks(items), [items], @@ -92,7 +109,6 @@ export function AgentSessionTranscriptList({ > {displayBlocks.map((block) => ( ; block: TranscriptDisplayBlock; compact: boolean; profiles?: UserProfileLookup; @@ -143,7 +157,6 @@ function TranscriptDisplayBlockView({ if (block.kind === "single") { return ( {block.segments.map((segment) => ( ; compact: boolean; profiles?: UserProfileLookup; segment: TranscriptTurnSegment; @@ -218,7 +228,6 @@ function TranscriptTurnSegmentView({ return (

{text}

{contextOpen && context ? ( - + ) : null}
[]; +}) { return (
+ {sections.map((section) => (
[]; +}) { + const label = formatTurnSetupLabel(items); + const detail = turnSetupDetail(items); + const setupText = [label, detail].filter(Boolean).join(" · "); + + if (!setupText) { + return null; + } + + return ( +

+ {setupText} +

+ ); +} + function TurnSetupFooter({ context = null, contextOpen = false, @@ -375,12 +414,35 @@ function TurnSetupFooter({ return ; } + const contextToggle = showContext ? ( + + {showSetup ? + ) : null; + return (
- {showSetup ? ( + {showContext && showSetup ? ( + + {contextToggle} + +

{tooltipText}

+
+
+ ) : null} + {!showContext && showSetup ? (
); } function TranscriptItemRow({ - activeItemIds, agentAvatarUrl, agentName, agentPubkey, @@ -422,7 +472,6 @@ function TranscriptItemRow({ item, profiles, }: AgentTranscriptIdentityProps & { - activeItemIds: ReadonlySet; compact: boolean; item: TranscriptItem; profiles?: UserProfileLookup; @@ -444,7 +493,6 @@ function TranscriptItemRow({ agentName={agentName} agentPubkey={agentPubkey} compact={compact} - isActive={activeItemIds.has(item.id)} item={item} profiles={profiles} /> @@ -486,12 +534,10 @@ const TranscriptItemView = React.memo(function TranscriptItemView({ agentName, agentPubkey, compact, - isActive, item, profiles, }: AgentTranscriptIdentityProps & { compact: boolean; - isActive: boolean; item: TranscriptItem; profiles?: UserProfileLookup; }) { @@ -502,17 +548,16 @@ const TranscriptItemView = React.memo(function TranscriptItemView({ agentName={agentName} agentPubkey={agentPubkey} compact={compact} - isActive={isActive} item={item} profiles={profiles} /> ); } if (item.type === "tool") { - return ; + return ; } if (item.type === "thought") { - return ; + return ; } if (item.type === "metadata") { return ; @@ -525,12 +570,10 @@ function MessageItem({ agentName, agentPubkey, compact, - isActive, item, profiles, }: AgentTranscriptIdentityProps & { compact: boolean; - isActive: boolean; item: Extract; profiles?: UserProfileLookup; }) { @@ -561,9 +604,6 @@ function MessageItem({ "flex animate-in fade-in duration-200 motion-reduce:animate-none", isAssistant ? "flex-row" : "flex-row items-start justify-end", compact ? "px-0 py-0.5" : "px-1 py-1", - isAssistant && - isActive && - "rounded-lg border border-primary/15 bg-primary/3 px-2 py-1.5", )} data-role={isAssistant ? "assistant-message" : "user-message"} data-testid={ @@ -596,15 +636,6 @@ function MessageItem({ {assistantLabel} - {isActive ? ( - - - Live - - ) : null}
) : null} @@ -630,11 +661,9 @@ function MessageItem({ function ThoughtItem({ compact, - isActive, item, }: { compact: boolean; - isActive: boolean; item: Extract; }) { return ( @@ -642,22 +671,12 @@ function ThoughtItem({ className={cn( "group not-prose w-full rounded-md border border-transparent", compact ? "px-0" : "px-1", - isActive && "border-primary/15 bg-primary/3 px-2 py-1", )} data-testid="transcript-thought-item" > - + {item.title} - {isActive ? ( - - - Live - - ) : null}