import { useCallback, useRef, type JSX, type ReactNode } from "react";
import { Button } from "@tutti-os/ui-system/components";
import { ChevronDownIcon } from "@tutti-os/ui-system/icons";
import { useTranslation } from "../../../i18n/index";
import { CollapsibleReveal } from "./CollapsibleReveal";
import type { AgentTurnDisclosureStore } from "./AgentTurnDisclosureContext";
import { useElapsedSeconds } from "./useElapsedSeconds";
import {
formatAgentTurnDuration,
type AgentTurnDuration,
type AgentTurnTiming,
type AgentTurnWorkSectionModel,
type AgentTurnWorkSectionRow
} from "./agentTurnWorkSectionModel";
interface AgentTurnWorkSectionProps {
model: AgentTurnWorkSectionModel;
sessionId: string;
turnKey: string;
showDivider?: boolean;
disclosureStore: AgentTurnDisclosureStore;
onDisclosureMotionChange?: (
turnKey: string,
active: boolean,
anchorElement?: HTMLElement | null
) => void;
footer?: ReactNode;
renderRow: (
row: AgentTurnWorkSectionRow["row"],
rowIndex: number,
renderKey?: string
) => JSX.Element;
}
export function AgentTurnWorkSection({
model,
sessionId,
turnKey,
showDivider = false,
disclosureStore,
onDisclosureMotionChange,
renderRow,
footer
}: AgentTurnWorkSectionProps): JSX.Element {
const { t } = useTranslation();
const disclosureKey = `${sessionId}:${turnKey}`;
const expanded = model.collapseEligible
? (disclosureStore.expandedOverrides[disclosureKey] ?? false)
: true;
const toggleLabel = expanded
? t("agentHost.agentGui.collapseTurnWork")
: t("agentHost.agentGui.expandTurnWork");
const pendingRevealTransitionsRef = useRef(0);
const disclosureMotionActiveRef = useRef(false);
const collapsibleSectionCount = model.sections.reduce(
(count, section) =>
count + (section.kind === "work" && model.collapseEligible ? 1 : 0),
0
);
const finishDisclosureMotion = useCallback(() => {
if (!disclosureMotionActiveRef.current) {
return;
}
disclosureMotionActiveRef.current = false;
pendingRevealTransitionsRef.current = 0;
onDisclosureMotionChange?.(turnKey, false);
}, [onDisclosureMotionChange, turnKey]);
const startDisclosureMotion = useCallback(
(anchorElement: HTMLElement | null) => {
pendingRevealTransitionsRef.current = collapsibleSectionCount;
if (collapsibleSectionCount === 0) {
return;
}
disclosureMotionActiveRef.current = true;
onDisclosureMotionChange?.(turnKey, true, anchorElement);
},
[collapsibleSectionCount, onDisclosureMotionChange, turnKey]
);
const handleRevealTransitionEnd = useCallback(() => {
if (!disclosureMotionActiveRef.current) {
return;
}
pendingRevealTransitionsRef.current -= 1;
if (pendingRevealTransitionsRef.current <= 0) {
finishDisclosureMotion();
}
}, [finishDisclosureMotion]);
return (
{showDivider ? (
) : null}
{model.leadingRows.length > 0 ? (
{renderRows(model.leadingRows, renderRow)}
) : null}
{model.collapseEligible ? (
) : (
)}
{model.sections.map((section, sectionIndex) => {
const content = renderRows(section.rows, renderRow);
if (section.kind !== "work" || !model.collapseEligible) {
const firstRow = section.rows[0];
return (
{content}
);
}
const firstRow = section.rows[0];
return (
{content}
);
})}
{footer}
);
}
function renderRows(
rows: readonly AgentTurnWorkSectionRow[],
renderRow: AgentTurnWorkSectionProps["renderRow"]
): ReactNode {
return rows.map(({ row, rowIndex, renderKey }) =>
renderRow(row, rowIndex, renderKey)
);
}
function AgentTurnDurationLabel({
timing
}: {
timing: AgentTurnTiming;
}): JSX.Element {
const { t } = useTranslation();
const liveElapsedSeconds = useElapsedSeconds(
timing.kind === "live" &&
timing.observationGapPresentationState === undefined &&
!Number.isFinite(timing.frozenAtUnixMs)
? timing.startedAtUnixMs
: null
);
if (
timing.kind === "live" &&
timing.observationGapPresentationState !== undefined
) {
return (
{timing.observationGapPresentationState === "peer-offline"
? t("agentHost.agentGui.turnPeerDeviceOfflinePendingSync")
: t("agentHost.agentGui.turnPeerDeviceProgressSynchronizing")}
);
}
const elapsedSeconds =
timing.kind === "live"
? Number.isFinite(timing.frozenAtUnixMs)
? Math.max(
0,
Math.floor(
((timing.frozenAtUnixMs as number) - timing.startedAtUnixMs) /
1_000
)
)
: (liveElapsedSeconds ?? 0)
: timing.elapsedSeconds;
return {translateDuration(t, timing.kind, elapsedSeconds)};
}
function translateDuration(
t: ReturnType["t"],
kind: AgentTurnTiming["kind"],
elapsedSeconds: number
): string {
const duration = formatAgentTurnDuration(elapsedSeconds);
return kind === "live"
? translateLiveDuration(t, duration)
: translateSettledDuration(t, duration);
}
function translateLiveDuration(
t: ReturnType["t"],
duration: AgentTurnDuration
): string {
if (duration.kind === "seconds") {
return t("agentHost.agentGui.turnProcessedSeconds", {
seconds: duration.seconds
});
}
if (duration.kind === "minutes") {
return t("agentHost.agentGui.turnProcessedMinutes", {
minutes: duration.minutes
});
}
return t("agentHost.agentGui.turnProcessedMinutesSeconds", {
minutes: duration.minutes,
seconds: duration.seconds
});
}
function translateSettledDuration(
t: ReturnType["t"],
duration: AgentTurnDuration
): string {
if (duration.kind === "seconds") {
return t("agentHost.agentGui.turnTotalSeconds", {
seconds: duration.seconds
});
}
if (duration.kind === "minutes") {
return t("agentHost.agentGui.turnTotalMinutes", {
minutes: duration.minutes
});
}
return t("agentHost.agentGui.turnTotalMinutesSeconds", {
minutes: duration.minutes,
seconds: duration.seconds
});
}