Skip to content

Commit 7e71392

Browse files
authored
feat(web): background work line shows the newest activity, no dots (#25299)
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
1 parent 06d4b57 commit 7e71392

4 files changed

Lines changed: 80 additions & 48 deletions

File tree

‎apps/web/src/components/ChatView.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ import { ChatRightPanel } from "./chat/ChatRightPanel";
354354
import { useDraftEnvironmentRouting } from "./chat/useDraftEnvironmentRouting";
355355
import { branchMismatchBannerItem } from "./chat/ThreadActivityBanners";
356356
import { ThreadNow } from "./chat/ThreadNow";
357+
import type { ThreadStructure } from "@t3tools/client-runtime/state/shell";
357358
import { useDelayedStatus } from "../hooks/useDelayedStatus";
358359
import { useTimelineFollow } from "./chat/useTimelineFollow";
359360
import {
@@ -376,6 +377,7 @@ import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplat
376377

377378
const EMPTY_FEEDBACK_SUBMISSIONS: ReadonlyArray<CodexFeedbackSubmission> = [];
378379
const EMPTY_PROVIDER_SKILLS: ServerProvider["skills"] = [];
380+
const EMPTY_TURN_ITEMS: ThreadStructure["turnItems"] = [];
379381
const EMPTY_AUTO_UPDATE_ENVIRONMENTS: ReadonlyArray<EnvironmentPresentation> = [];
380382

381383
const EMPTY_PENDING_FILE_SURFACE_IDS: ReadonlySet<string> = new Set();
@@ -2405,6 +2407,7 @@ export default function ChatView(props: ChatViewProps) {
24052407
runtimeMode,
24062408
isWorking,
24072409
pendingBackgroundTasks,
2410+
turnItems: serverProjection?.turnItems ?? EMPTY_TURN_ITEMS,
24082411
activePendingApproval,
24092412
activePendingUserInput,
24102413
localCheckoutBranchMismatch,

‎apps/web/src/components/chat/ThreadActivityBanners.tsx‎

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@ import type {
33
OrchestrationV2ProviderGoal,
44
ThreadId,
55
} from "@t3tools/contracts";
6-
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
6+
import type { EnvironmentThreadShell, ThreadStructure } from "@t3tools/client-runtime/state/shell";
77
import {
88
presentPendingBackgroundWork,
99
presentProviderGoal,
1010
} from "@t3tools/client-runtime/state/thread-execution";
1111
import { childThreadInputsTitle } from "@t3tools/client-runtime/state/threads";
12-
import { GitBranchIcon, MessageCircleIcon, ShieldQuestionIcon, TargetIcon } from "lucide-react";
12+
import { formatDuration } from "@t3tools/shared/orchestrationTiming";
13+
import * as DateTime from "effect/DateTime";
14+
import {
15+
CircleDashedIcon,
16+
GitBranchIcon,
17+
MessageCircleIcon,
18+
ShieldQuestionIcon,
19+
TargetIcon,
20+
TerminalIcon,
21+
} from "lucide-react";
1322
import { Fragment } from "react";
23+
import { tickTextContent } from "~/lib/tickTextContent";
1424
import { cn } from "~/lib/utils";
1525
import { Button, InlineButton } from "../ui/button";
1626
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
@@ -99,6 +109,7 @@ export function goalNowItem(input: {
99109
export function backgroundWorkNowItem(input: {
100110
readonly threadId: ThreadId;
101111
readonly tasks: ReadonlyArray<OrchestrationV2PendingBackgroundTask>;
112+
readonly turnItems: ThreadStructure["turnItems"];
102113
readonly stopDisabled: boolean;
103114
readonly stopping: boolean;
104115
readonly onStop: () => Promise<void>;
@@ -107,51 +118,67 @@ export function backgroundWorkNowItem(input: {
107118
const { onOpenThread } = input;
108119
const presentation = presentPendingBackgroundWork(input.tasks);
109120
if (presentation === null) return null;
121+
const rows = presentation.items.map((task) => {
122+
// Same key derivePendingBackgroundWork gives turn-item tasks; subagent items carry progress.
123+
const item = input.turnItems.findLast(
124+
(candidate) => (candidate.nativeItemRef?.nativeId || candidate.id) === task.taskId,
125+
);
126+
const subagent = item?.type === "subagent" ? item : undefined;
127+
return {
128+
task,
129+
activity: subagent?.progress?.replace(/\s+/gu, " ").trim() || null,
130+
startedAtMs: item?.startedAt ? DateTime.toEpochMillis(item.startedAt) : null,
131+
updatedAtMs: subagent ? DateTime.toEpochMillis(subagent.updatedAt) : 0,
132+
childThreadId: task.childThreadId ?? subagent?.childThreadId ?? undefined,
133+
};
134+
});
135+
const [only] = rows.length === 1 ? rows : [];
136+
const newest = rows
137+
.filter((row) => row.activity)
138+
.toSorted((left, right) => right.updatedAtMs - left.updatedAtMs)[0];
139+
const isCommand = (row: (typeof rows)[number]) => row.task.kind !== "subagent";
140+
const verb = presentation.waiting ? "Waiting on" : "Running";
141+
const onlyThreadId = only?.childThreadId;
142+
const startedAtMs = only?.startedAtMs ?? null;
110143
return {
111144
id: `background-work:${input.threadId}`,
112-
icon: (
113-
<span
114-
className={cn(
115-
"size-1.5 rounded-full",
116-
presentation.waiting ? "bg-foreground" : "bg-muted-foreground",
117-
)}
118-
aria-hidden="true"
119-
/>
145+
icon: rows.every(isCommand) ? <TerminalIcon /> : <CircleDashedIcon />,
146+
title: only ? (
147+
<>
148+
{verb} <span className={cn(isCommand(only) && "font-mono")}>{only.task.label}</span>
149+
</>
150+
) : (
151+
presentation.title
152+
),
153+
description: only ? (
154+
only.activity
155+
) : newest ? (
156+
`${newest.task.label}: ${newest.activity}`
157+
) : rows.every(isCommand) ? (
158+
<span className="font-mono">{rows.map((row) => row.task.label).join(", ")}</span>
159+
) : (
160+
rows.map((row) => row.task.label).join(", ")
120161
),
121-
title: presentation.title,
122-
description:
123-
presentation.items.length === 1 && presentation.items[0]?.childThreadId === undefined
124-
? undefined
125-
: presentation.items.map((item, index) => {
126-
const childThreadId = item.childThreadId;
127-
const separator = index < presentation.items.length - 1 ? "," : null;
128-
return (
129-
<Fragment key={item.taskId}>
130-
{index > 0 ? " " : null}
131-
{childThreadId === undefined ? (
132-
<>
133-
{item.label}
134-
{separator}
135-
</>
136-
) : (
137-
<span className="whitespace-nowrap">
138-
<InlineButton
139-
tone="muted"
140-
aria-label={`${presentation.items.length === 1 ? "Show" : "Open"} subagent ${item.label}`}
141-
onClick={() => onOpenThread(childThreadId)}
142-
>
143-
{presentation.items.length === 1 ? "Show" : item.label}
144-
</InlineButton>
145-
{separator}
146-
</span>
147-
)}
148-
</Fragment>
149-
);
150-
}),
151162
actions: (
152-
<InlineButton disabled={input.stopDisabled} onClick={() => void input.onStop()}>
153-
{input.stopping ? "Stopping..." : "Stop"}
154-
</InlineButton>
163+
<>
164+
{startedAtMs === null ? null : (
165+
<span
166+
ref={tickTextContent(
167+
(nowMs) => formatDuration(Math.max(1_000, nowMs - startedAtMs)),
168+
true,
169+
)}
170+
className="tabular-nums"
171+
/>
172+
)}
173+
{onlyThreadId === undefined ? null : (
174+
<InlineButton tone="muted" onClick={() => onOpenThread(onlyThreadId)}>
175+
Open
176+
</InlineButton>
177+
)}
178+
<InlineButton disabled={input.stopDisabled} onClick={() => void input.onStop()}>
179+
{input.stopping ? "Stopping..." : "Stop"}
180+
</InlineButton>
181+
</>
155182
),
156183
};
157184
}

‎apps/web/src/components/chat/ThreadNow.tsx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,14 @@ function ThreadNowLine({
5454
{item.description ? (
5555
<>
5656
{" "}
57-
<span aria-hidden>·</span> {item.description}
57+
<span className="ms-0.5 text-muted-foreground/60 light:text-muted-foreground/80">
58+
{item.description}
59+
</span>
5860
</>
5961
) : null}
6062
</NoticeText>
6163
{item.actions ? (
62-
<>
63-
<span aria-hidden>·</span>
64-
<span className="flex shrink-0 items-center gap-1.5">{item.actions}</span>
65-
</>
64+
<span className="ms-1.5 flex shrink-0 items-center gap-2.5">{item.actions}</span>
6665
) : null}
6766
</div>
6867
);

‎apps/web/src/components/chat/useThreadLifecycle.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ interface ThreadLifecycleInput {
8282
readonly runtimeMode: RuntimeMode;
8383
readonly isWorking: boolean;
8484
readonly pendingBackgroundTasks: Parameters<typeof backgroundWorkNowItem>[0]["tasks"];
85+
readonly turnItems: Parameters<typeof backgroundWorkNowItem>[0]["turnItems"];
8586
readonly activePendingApproval: PendingThreadRequests["activePendingApproval"];
8687
readonly activePendingUserInput: PendingThreadRequests["activePendingUserInput"];
8788
readonly localCheckoutBranchMismatch: {
@@ -132,6 +133,7 @@ export function useThreadLifecycle(input: ThreadLifecycleInput) {
132133
runtimeMode,
133134
isWorking,
134135
pendingBackgroundTasks,
136+
turnItems,
135137
activePendingApproval,
136138
activePendingUserInput,
137139
localCheckoutBranchMismatch,
@@ -513,6 +515,7 @@ export function useThreadLifecycle(input: ThreadLifecycleInput) {
513515
? backgroundWorkNowItem({
514516
threadId: activeThread.id,
515517
tasks: activeBackgroundTasks,
518+
turnItems,
516519
stopDisabled: !canOperateThread || isStoppingBackgroundWork,
517520
stopping: isStoppingBackgroundWork,
518521
onStop: handleStopBackgroundWorkStable,

0 commit comments

Comments
 (0)