diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 927b9a8e44a5e..69409e5e4da27 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -354,6 +354,7 @@ import { ChatRightPanel } from "./chat/ChatRightPanel"; import { useDraftEnvironmentRouting } from "./chat/useDraftEnvironmentRouting"; import { branchMismatchBannerItem } from "./chat/ThreadActivityBanners"; import { ThreadNow } from "./chat/ThreadNow"; +import type { ThreadStructure } from "@t3tools/client-runtime/state/shell"; import { useDelayedStatus } from "../hooks/useDelayedStatus"; import { useTimelineFollow } from "./chat/useTimelineFollow"; import { @@ -376,6 +377,7 @@ import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplat const EMPTY_FEEDBACK_SUBMISSIONS: ReadonlyArray = []; const EMPTY_PROVIDER_SKILLS: ServerProvider["skills"] = []; +const EMPTY_TURN_ITEMS: ThreadStructure["turnItems"] = []; const EMPTY_AUTO_UPDATE_ENVIRONMENTS: ReadonlyArray = []; const EMPTY_PENDING_FILE_SURFACE_IDS: ReadonlySet = new Set(); @@ -2405,6 +2407,7 @@ export default function ChatView(props: ChatViewProps) { runtimeMode, isWorking, pendingBackgroundTasks, + turnItems: serverProjection?.turnItems ?? EMPTY_TURN_ITEMS, activePendingApproval, activePendingUserInput, localCheckoutBranchMismatch, diff --git a/apps/web/src/components/chat/ThreadActivityBanners.tsx b/apps/web/src/components/chat/ThreadActivityBanners.tsx index e132587a75951..168b674d26c92 100644 --- a/apps/web/src/components/chat/ThreadActivityBanners.tsx +++ b/apps/web/src/components/chat/ThreadActivityBanners.tsx @@ -3,14 +3,24 @@ import type { OrchestrationV2ProviderGoal, ThreadId, } from "@t3tools/contracts"; -import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import type { EnvironmentThreadShell, ThreadStructure } from "@t3tools/client-runtime/state/shell"; import { presentPendingBackgroundWork, presentProviderGoal, } from "@t3tools/client-runtime/state/thread-execution"; import { childThreadInputsTitle } from "@t3tools/client-runtime/state/threads"; -import { GitBranchIcon, MessageCircleIcon, ShieldQuestionIcon, TargetIcon } from "lucide-react"; +import { formatDuration } from "@t3tools/shared/orchestrationTiming"; +import * as DateTime from "effect/DateTime"; +import { + CircleDashedIcon, + GitBranchIcon, + MessageCircleIcon, + ShieldQuestionIcon, + TargetIcon, + TerminalIcon, +} from "lucide-react"; import { Fragment } from "react"; +import { tickTextContent } from "~/lib/tickTextContent"; import { cn } from "~/lib/utils"; import { Button, InlineButton } from "../ui/button"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; @@ -99,6 +109,7 @@ export function goalNowItem(input: { export function backgroundWorkNowItem(input: { readonly threadId: ThreadId; readonly tasks: ReadonlyArray; + readonly turnItems: ThreadStructure["turnItems"]; readonly stopDisabled: boolean; readonly stopping: boolean; readonly onStop: () => Promise; @@ -107,51 +118,67 @@ export function backgroundWorkNowItem(input: { const { onOpenThread } = input; const presentation = presentPendingBackgroundWork(input.tasks); if (presentation === null) return null; + const rows = presentation.items.map((task) => { + // Same key derivePendingBackgroundWork gives turn-item tasks; subagent items carry progress. + const item = input.turnItems.findLast( + (candidate) => (candidate.nativeItemRef?.nativeId || candidate.id) === task.taskId, + ); + const subagent = item?.type === "subagent" ? item : undefined; + return { + task, + activity: subagent?.progress?.replace(/\s+/gu, " ").trim() || null, + startedAtMs: item?.startedAt ? DateTime.toEpochMillis(item.startedAt) : null, + updatedAtMs: subagent ? DateTime.toEpochMillis(subagent.updatedAt) : 0, + childThreadId: task.childThreadId ?? subagent?.childThreadId ?? undefined, + }; + }); + const [only] = rows.length === 1 ? rows : []; + const newest = rows + .filter((row) => row.activity) + .toSorted((left, right) => right.updatedAtMs - left.updatedAtMs)[0]; + const isCommand = (row: (typeof rows)[number]) => row.task.kind !== "subagent"; + const verb = presentation.waiting ? "Waiting on" : "Running"; + const onlyThreadId = only?.childThreadId; + const startedAtMs = only?.startedAtMs ?? null; return { id: `background-work:${input.threadId}`, - icon: ( -