"use client"; import { useState } from "react"; import type { CommentTriggerPreviewAgent } from "@multica/core/types"; import { useAgentPresenceDetail } from "@multica/core/agents"; import { useCurrentWorkspace } from "@multica/core/paths"; import { ActorAvatar as ActorAvatarBase } from "@multica/ui/components/common/actor-avatar"; import { Popover, PopoverContent, PopoverTrigger, } from "@multica/ui/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@multica/ui/components/ui/tooltip"; import { cn } from "@multica/ui/lib/utils"; import { AgentStatusDot } from "../../common/actor-avatar"; import { useT } from "../../i18n"; // One agent renders in full ("Walt will start working", avatar + presence // dot, click toggles). Several agents collapse to an overlapping avatar // stack + count sentence, mirroring WorkspaceAgentWorkingChip on the issues // header. Hover layers stay read-only (Tooltip); per-agent toggling lives in // a click-opened Popover so the layer survives consecutive clicks. // Suppression is communicated by brightness alone: lit = will trigger, // dimmed = skipped. const AVATAR_SIZE = 16; const MAX_STACK_HEADS = 4; interface CommentTriggerChipsProps { agents: CommentTriggerPreviewAgent[]; suppressedAgentIds: Set; onToggle: (agentId: string) => void; } type IssuesT = ReturnType>["t"]; function sourceLabel(source: string, t: IssuesT): string { switch (source) { case "issue_assignee": return t(($) => $.comment.trigger_source_issue_assignee); case "mention_agent": return t(($) => $.comment.trigger_source_mention_agent); case "mention_squad_leader": return t(($) => $.comment.trigger_source_mention_squad_leader); default: return t(($) => $.comment.trigger_source_unknown); } } // Assignee / @mention reasons are intentionally omitted: the header // (name · source) already says why they fire, so a reason line there would // just restate it. Only the squad-leader link (non-obvious) and the unknown // fallback carry information the header doesn't. function sourceReason(agent: CommentTriggerPreviewAgent, t: IssuesT): string | null { switch (agent.source) { case "issue_assignee": case "mention_agent": return null; case "mention_squad_leader": return t(($) => $.comment.trigger_reason_mention_squad_leader); default: return agent.reason || t(($) => $.comment.trigger_reason_unknown); } } // Presence is display metadata only — the trigger list itself is always the // backend preview. Online-ish agents start right away; offline ones queue. function useTriggerPresenceLine(agentId: string, t: IssuesT): string | null { const ws = useCurrentWorkspace(); const detail = useAgentPresenceDetail(ws?.id, agentId); if (detail === "loading") return null; return detail.availability === "online" || detail.availability === "unstable" ? t(($) => $.comment.trigger_starts_now) : t(($) => $.comment.trigger_starts_when_online); } // One tooltip body for every trigger surface (single chip, popover rows): // who · why it fires (+ presence) · what a click does. function TriggerAgentTooltipBody({ agent, suppressed, t, }: { agent: CommentTriggerPreviewAgent; suppressed: boolean; t: IssuesT; }) { const presenceLine = useTriggerPresenceLine(agent.id, t); return (
{agent.name} {sourceLabel(agent.source, t)}
{suppressed ? (
{t(($) => $.comment.trigger_click_to_restore)}
) : ( <> {(() => { // Reason (when present) and presence share one line; either may be // absent, so join only the parts that exist to avoid a stray space. const line = [sourceReason(agent, t), presenceLine].filter(Boolean).join(" "); return line ?
{line}
: null; })()}
{t(($) => $.comment.trigger_click_to_skip)}
)}
); } export function CommentTriggerChips({ agents, suppressedAgentIds, onToggle, }: CommentTriggerChipsProps) { const { t } = useT("issues"); // Loading and errors render nothing: the preview is an enhancement, and // any interim chrome here reads as composer noise. if (agents.length === 0) return null; if (agents.length === 1) { const agent = agents[0]!; return ( ); } return ( ); } function SingleTriggerChip({ agent, suppressed, onToggle, t, }: { agent: CommentTriggerPreviewAgent; suppressed: boolean; onToggle: (agentId: string) => void; t: IssuesT; }) { const state = suppressed ? t(($) => $.comment.trigger_skipped_label) : sourceLabel(agent.source, t); // The avatar carries "who"; the sentence carries only condition + outcome, // so it stays fixed-width and never truncates on long agent names. const sentence = suppressed ? t(($) => $.comment.trigger_wont_trigger) : t(($) => $.comment.trigger_will_start); return ( $.comment.trigger_chip_aria, { name: agent.name, state })} onClick={() => onToggle(agent.id)} className={cn( // Sidebar-style resting state: muted until hover so the strip // reads as metadata, not content (see app-sidebar nav items). "inline-flex h-6 min-w-0 max-w-full animate-in fade-in cursor-pointer items-center gap-1.5 rounded-md px-1.5 text-[11px] font-medium text-muted-foreground transition-colors duration-200 hover:bg-muted hover:text-foreground", suppressed && "opacity-60", )} > {sentence} } /> ); } function MultiTriggerChip({ agents, suppressedAgentIds, onToggle, t, }: { agents: CommentTriggerPreviewAgent[]; suppressedAgentIds: Set; onToggle: (agentId: string) => void; t: IssuesT; }) { const [open, setOpen] = useState(false); const [tooltipHover, setTooltipHover] = useState(false); const activeCount = agents.filter((a) => !suppressedAgentIds.has(a.id)).length; const heads = agents.slice(0, MAX_STACK_HEADS); const overflow = agents.length - heads.length; // Mirror AgentAvatarStack: ~30% overlap reads as "stacked" without // obscuring the next avatar. const overlap = Math.round(AVATAR_SIZE * 0.3); // The avatar stack shows who; the sentence promises only what WILL happen, // so the count covers non-suppressed agents — skipped ones read as the // dimmed heads right next to the number. const sentence = activeCount === 0 ? t(($) => $.comment.trigger_none_will_trigger) : t(($) => $.comment.trigger_will_start_count, { count: activeCount }); const popoverTrigger = ( } > {heads.map((agent, i) => ( ))} {overflow > 0 && ( +{overflow} )} {sentence} ); return ( {t(($) => $.comment.trigger_click_to_manage)}
{t(($) => $.comment.trigger_preview_title)}
{agents.map((agent) => { const suppressed = suppressedAgentIds.has(agent.id); const state = suppressed ? t(($) => $.comment.trigger_skipped_label) : sourceLabel(agent.source, t); return ( $.comment.trigger_chip_aria, { name: agent.name, state })} onClick={() => onToggle(agent.id)} className={cn( "flex w-full cursor-pointer items-center gap-2 rounded-md px-1.5 py-1 text-left transition-colors hover:bg-muted", suppressed && "opacity-60", )} > {agent.name} {state} } /> ); })}
); } function TriggerAgentAvatar({ agent, suppressed, showDot = true, }: { agent: CommentTriggerPreviewAgent; suppressed: boolean; showDot?: boolean; }) { return ( {showDot && !suppressed && } ); }