import { useCallback, useState, type JSX } from "react"; import { ArrowRightLeft, ChevronDown, ChevronUp, GitFork, LoaderCircle, MessageCircleQuestion, Users } from "lucide-react"; import { toast } from "@tutti-os/ui-system"; import { translate } from "../../../i18n/index"; import { useOptionalAgentHostApi } from "../../../agentActivityHost"; import { useOptionalAgentGUIRuntime } from "../../../agentActivityRuntime"; import { AgentMessageMarkdown, type AgentMessageMarkdownWorkspaceAppIcon } from "../../AgentMessageMarkdown"; import type { WorkspaceLinkAction } from "../../../contexts/workspace/presentation/renderer/actions/workspaceLinkActions"; import type { AgentCollaborationVM } from "../contracts/agentCollaborationVM"; import styles from "../../../agent-gui/agentGuiNode/AgentGUIConversation.styles"; /** * Transcript card for one durable collaboration run (model consult, fork, * delegate, or handoff). The daemon updates the same message in place as the * run progresses, so all durable state comes from the projected * `AgentCollaborationVM`; the card owns only UI-local interaction state * (result disclosure, in-flight adoption action + optimistic echo). */ export function AgentCollaborationRow({ collaboration, workspaceRoot, basePath, onLinkAction, workspaceAppIcons }: { collaboration: AgentCollaborationVM; workspaceRoot: string | null; basePath: string; onLinkAction?: (action: WorkspaceLinkAction) => void; workspaceAppIcons?: readonly AgentMessageMarkdownWorkspaceAppIcon[]; }): JSX.Element { "use memo"; const agentHostApi = useOptionalAgentHostApi(); const runtime = useOptionalAgentGUIRuntime(); const [resultExpanded, setResultExpanded] = useState(false); const [adoptionPending, setAdoptionPending] = useState(false); // Optimistic echo until the daemon's in-place message update arrives. const [localAdoption, setLocalAdoption] = useState(null); const adoption = localAdoption ?? collaboration.adoption; const resultText = collaboration.resultText?.trim() ?? ""; const canSubmitAdoption = Boolean( runtime?.setCollaborationAdoption && collaboration.workspaceId && collaboration.agentSessionId ); const showAdoptionControls = (collaboration.mode === "consult" || collaboration.mode === "delegate") && adoption !== "not_applicable" && collaboration.status === "completed" && canSubmitAdoption; const submitAdoption = useCallback( async (nextAdoption: "adopted" | "rejected") => { const setCollaborationAdoption = runtime?.setCollaborationAdoption; if ( !setCollaborationAdoption || !collaboration.workspaceId || !collaboration.agentSessionId || adoptionPending ) { return; } setAdoptionPending(true); try { const run = await setCollaborationAdoption({ adoption: nextAdoption, agentSessionId: collaboration.agentSessionId, runId: collaboration.runId, workspaceId: collaboration.workspaceId }); setLocalAdoption(run.adoption); } catch { const message = translate( "agentHost.agentGui.collaborationAdoptionFailed" ); if (agentHostApi?.toast?.error) { agentHostApi.toast.error(message); } else { toast.error(message); } } finally { setAdoptionPending(false); } }, [ adoptionPending, agentHostApi, collaboration.agentSessionId, collaboration.runId, collaboration.workspaceId, runtime ] ); const ModeIcon = collaborationModeIcon(collaboration.mode); const planText = collaboration.modelPlanName ?? collaboration.modelPlanId; return (
{collaborationTriggerLabel(collaboration.triggerSource)} {planText || collaboration.model ? ( {planText ? translate("agentHost.agentGui.collaborationPlanLabel", { name: planText }) : null} {planText && collaboration.model ? " · " : null} {collaboration.model} ) : null} {collaboration.status === "running" ? (
{collaboration.usage ? (
{translate("agentHost.agentGui.collaborationUsageTokens", { input: String(collaboration.usage.inputTokens), output: String(collaboration.usage.outputTokens) })}
) : null} {collaboration.failureReason ? (
{translate("agentHost.agentGui.collaborationFailureReason", { reason: collaboration.failureReason })}
) : null} {collaboration.mode === "consult" && resultText ? (
{resultExpanded ? (
) : null}
) : null} {showAdoptionControls && adoption === "pending" ? (
) : null} {adoption === "adopted" || adoption === "rejected" ? (
{translate( adoption === "adopted" ? "agentHost.agentGui.collaborationAdopted" : "agentHost.agentGui.collaborationRejected" )}
) : null}
); } function collaborationModeIcon( mode: AgentCollaborationVM["mode"] ): typeof MessageCircleQuestion { switch (mode) { case "fork": return GitFork; case "delegate": return Users; case "handoff": return ArrowRightLeft; default: return MessageCircleQuestion; } } function collaborationModeLabel(mode: AgentCollaborationVM["mode"]): string { switch (mode) { case "consult": return translate("agentHost.agentGui.collaborationModeConsult"); case "fork": return translate("agentHost.agentGui.collaborationModeFork"); case "delegate": return translate("agentHost.agentGui.collaborationModeDelegate"); case "handoff": return translate("agentHost.agentGui.collaborationModeHandoff"); default: return mode; } } function collaborationTriggerLabel( triggerSource: AgentCollaborationVM["triggerSource"] ): string { switch (triggerSource) { case "user": return translate("agentHost.agentGui.collaborationTriggerUser"); case "agent": return translate("agentHost.agentGui.collaborationTriggerAgent"); case "policy": return translate("agentHost.agentGui.collaborationTriggerPolicy"); default: return triggerSource; } } function collaborationStatusLabel( status: AgentCollaborationVM["status"] ): string { switch (status) { case "running": return translate("agentHost.agentGui.collaborationStatusRunning"); case "completed": return translate("agentHost.agentGui.collaborationStatusCompleted"); case "failed": return translate("agentHost.agentGui.collaborationStatusFailed"); case "canceled": return translate("agentHost.agentGui.collaborationStatusCanceled"); default: return status; } } function collaborationStatusClassName( status: AgentCollaborationVM["status"] ): string { switch (status) { case "failed": return "bg-[var(--on-danger)] text-[var(--state-danger)]"; case "running": return "bg-[color-mix(in_srgb,var(--tutti-purple)_12%,transparent)] text-[var(--tutti-purple)]"; default: return "bg-[var(--transparency-hover)] text-[var(--text-secondary)]"; } } function formatCollaborationDuration(durationMs: number): string { const totalSeconds = Math.max(0, Math.round(durationMs / 1000)); if (totalSeconds < 60) { return `${totalSeconds}s`; } const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}m ${seconds}s`; }