Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

268 lines
8.1 KiB
TypeScript

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 (
<div className="grid min-w-0" data-agent-turn-work-section={turnKey}>
{showDivider ? (
<div
className="mb-4 h-px w-full flex-none bg-[var(--line-2,var(--tutti-line-2))]"
data-testid="agent-transcript-turn-divider"
aria-hidden="true"
/>
) : null}
{model.leadingRows.length > 0 ? (
<div className="mb-4 grid gap-4">
{renderRows(model.leadingRows, renderRow)}
</div>
) : null}
<div
className="flex min-h-6 items-center gap-0.5 text-[12px] text-[var(--text-tertiary)]"
data-agent-turn-work-header={turnKey}
>
{model.collapseEligible ? (
<Button
type="button"
variant="chrome"
size="xs"
className="-ml-2 gap-0.5 px-2 text-[12px] font-normal"
aria-label={toggleLabel}
aria-expanded={expanded}
title={toggleLabel}
onClick={(event) => {
startDisclosureMotion(
event.currentTarget.closest<HTMLElement>(
"[data-agent-turn-work-header]"
)
);
disclosureStore.setExpandedOverride(disclosureKey, !expanded);
}}
>
<AgentTurnDurationLabel timing={model.timing} />
<ChevronDownIcon
aria-hidden="true"
data-icon="inline-end"
className={`transition-transform duration-150 ${
expanded ? "rotate-0" : "-rotate-90"
}`}
/>
</Button>
) : (
<AgentTurnDurationLabel timing={model.timing} />
)}
</div>
{model.sections.map((section, sectionIndex) => {
const content = renderRows(section.rows, renderRow);
if (section.kind !== "work" || !model.collapseEligible) {
const firstRow = section.rows[0];
return (
<div
key={`${section.kind}:${firstRow?.renderKey ?? firstRow?.row.id ?? sectionIndex}`}
className="grid gap-4 pt-4"
>
{content}
</div>
);
}
const firstRow = section.rows[0];
return (
<CollapsibleReveal
key={`work:${firstRow?.renderKey ?? firstRow?.row.id ?? sectionIndex}`}
expanded={expanded}
innerClassName="grid gap-4 pt-4"
onHeightTransitionEnd={handleRevealTransitionEnd}
>
{content}
</CollapsibleReveal>
);
})}
{footer}
</div>
);
}
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 (
<span>
{timing.observationGapPresentationState === "peer-offline"
? t("agentHost.agentGui.turnPeerDeviceOfflinePendingSync")
: t("agentHost.agentGui.turnPeerDeviceProgressSynchronizing")}
</span>
);
}
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 <span>{translateDuration(t, timing.kind, elapsedSeconds)}</span>;
}
function translateDuration(
t: ReturnType<typeof useTranslation>["t"],
kind: AgentTurnTiming["kind"],
elapsedSeconds: number
): string {
const duration = formatAgentTurnDuration(elapsedSeconds);
return kind === "live"
? translateLiveDuration(t, duration)
: translateSettledDuration(t, duration);
}
function translateLiveDuration(
t: ReturnType<typeof useTranslation>["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<typeof useTranslation>["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
});
}