authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-10-03 18:12:05-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 22:12:05-03:00
logdab26f582cf7826fec67fbed64b46f59f3421280
tree5886944359b1c57f25237e52ef5d0f28f43ed09e
parent2a455570e6ee70a59241c30126b50c7df1696a6a
signature Signed by PGP key B5690EEEBB952194

fix(web): subagent finish notifications look like subagent cards (#15281)


2 files changed, 111 insertions(+), 11 deletions(-)

apps/web/src/components/chat/MessagesTimeline.tsx+28-3
......@@ -265,6 +265,7 @@ import {
265265 isV2LifecycleItem,
266266 SubagentAvatar,
267267 SubagentElapsed,
268 SubagentNotificationLink,
268269 V2LifecycleRow,
269270 type HandoffTimelineRun,
270271} from "./V2LifecycleRow";
......@@ -4946,12 +4947,36 @@ function ReasoningTraceContent({ entries }: { entries: ReadonlyArray<TimelineWor
49464947 );
49474948}
49484949
4949const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
4950type WorkEntryRowProps = {
49504951 workEntry: TimelineWorkEntry;
49514952 workspaceRoot: string | undefined;
49524953 displayLabel?: string | undefined;
49534954 onToggleEntry?: ((collapsed: boolean) => void) | undefined;
4954}) {
4955};
4956
4957const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: WorkEntryRowProps) {
4958 const ctx = use(TimelineRowCtx);
4959 const item = props.workEntry.projectedItem?.item;
4960 const childThreadId =
4961 item?.type === "notification" ? notificationChildThreadId(item.source) : undefined;
4962 if (item?.type !== "notification" || childThreadId === undefined) {
4963 return <WorkEntryLogRow {...props} />;
4964 }
4965 return (
4966 <SubagentNotificationLink
4967 parentRef={scopeThreadRef(ctx.activeThreadEnvironmentId, item.threadId)}
4968 childThreadId={childThreadId}
4969 outcome={item.outcome}
4970 createdAt={props.workEntry.createdAt}
4971 timestampFormat={ctx.timestampFormat}
4972 providerStatuses={ctx.providerStatuses}
4973 onOpenThread={ctx.onOpenThread}
4974 fallback={<WorkEntryLogRow {...props} />}
4975 />
4976 );
4977});
4978
4979function WorkEntryLogRow(props: WorkEntryRowProps) {
49554980 const { workEntry, workspaceRoot, displayLabel } = props;
49564981 const ctx = use(TimelineRowCtx);
49574982 const { threadRef, onImageExpand, timestampFormat } = ctx;
......@@ -5285,7 +5310,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
52855310 ) : null}
52865311 </WorkLogRow>
52875312 );
5288});
5313}
52895314
52905315function QuestionAnswerHistory({
52915316 answer,
apps/web/src/components/chat/V2LifecycleRow.tsx+83-8
......@@ -15,6 +15,7 @@ import { Fragment } from "react";
1515import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display";
1616import {
1717 ProviderDriverKind,
18 type OrchestrationV2Notification,
1819 type OrchestrationV2TurnItem,
1920 type ProviderInstanceId,
2021 type ServerProvider,
......@@ -326,6 +327,69 @@ export function SubagentAvatar({
326327 );
327328}
328329
330/** The status a notification reported, frozen at the time it arrived. */
331const NOTIFICATION_OUTCOME_STATUS: Record<
332 OrchestrationV2Notification["outcome"],
333 OrchestrationV2TurnItem["status"] | null
334> = {
335 completed: "completed",
336 failed: "failed",
337 cancelled: "cancelled",
338 updated: null,
339 unknown: null,
340};
341
342const NOTIFICATION_OUTCOME_LABEL: Record<OrchestrationV2Notification["outcome"], string> = {
343 completed: "Finished",
344 failed: "Failed",
345 cancelled: "Stopped",
346 updated: "Updated",
347 unknown: "Finished",
348};
349
350/** A notification about one subagent, drawn as that subagent's card. Renders `fallback` when the parent has no record of it. */
351export function SubagentNotificationLink(props: {
352 readonly parentRef: ScopedThreadRef;
353 readonly childThreadId: ThreadId;
354 readonly outcome: OrchestrationV2Notification["outcome"];
355 readonly createdAt: string;
356 readonly timestampFormat: TimestampFormat;
357 readonly providerStatuses: ReadonlyArray<ServerProvider>;
358 readonly onOpenThread: (threadId: ThreadId) => void;
359 readonly fallback: ReactNode;
360}) {
361 const agent = useAtomValue(
362 environmentThreadDetails.threadAtom(props.parentRef),
363 (thread) =>
364 thread?.projection.subagents.find((agent) => agent.childThreadId === props.childThreadId) ??
365 null,
366 );
367 if (agent === null) return props.fallback;
368 return (
369 <SubagentTimelineLink
370 parentRef={props.parentRef}
371 subagentId={agent.id}
372 status={agent.status}
373 driver={agent.driver}
374 provider={props.providerStatuses.find(
375 (provider) => provider.instanceId === agent.providerInstanceId,
376 )}
377 title={formatSubagentDisplayTitle(agent.title ?? "Subagent")}
378 result={agent.result}
379 progress={agent.progress}
380 startedAt={agent.startedAt}
381 completedAt={agent.completedAt}
382 threadId={props.childThreadId}
383 onOpenThread={props.onOpenThread}
384 event={{
385 status: NOTIFICATION_OUTCOME_STATUS[props.outcome],
386 label: NOTIFICATION_OUTCOME_LABEL[props.outcome],
387 timestamp: formatShortTimestamp(props.createdAt, props.timestampFormat),
388 }}
389 />
390 );
391}
392
329393function SubagentTimelineLink(props: {
330394 readonly parentRef: ScopedThreadRef;
331395 readonly subagentId: NodeId;
......@@ -339,35 +403,46 @@ function SubagentTimelineLink(props: {
339403 readonly completedAt: DateTime.Utc | null;
340404 readonly threadId: ThreadId | null;
341405 readonly onOpenThread: (threadId: ThreadId) => void;
406 /** Draws a past event about the subagent: its status then, and when it happened instead of elapsed time. */
407 readonly event?: {
408 readonly status: OrchestrationV2TurnItem["status"] | null;
409 readonly label: string;
410 readonly timestamp: string;
411 };
342412}) {
343413 const agent = useAtomValue(
344414 environmentThreadDetails.threadAtom(props.parentRef),
345415 (thread) => thread?.projection.subagents.find((agent) => agent.id === props.subagentId) ?? null,
346416 );
347417 const threadId = props.threadId;
348 const status = agent?.status ?? props.status;
349 const statusLabel = subagentStatusVisual(status).label;
418 const liveStatus = agent?.status ?? props.status;
419 const status = props.event ? props.event.status : liveStatus;
420 const statusLabel = props.event?.label ?? subagentStatusVisual(liveStatus).label;
350421 const result = (agent?.result ?? props.result)?.trim();
351422 const progress = (agent?.progress ?? props.progress)?.trim();
352 const settled = SETTLED_SUBAGENT_STATUSES.has(status);
423 const settled = SETTLED_SUBAGENT_STATUSES.has(status ?? liveStatus);
353424 const rawDetail = settled ? result || progress : progress || result;
354425 const detail =
355426 rawDetail && !GENERIC_CHILD_END.test(rawDetail) ? plainDetail(rawDetail) || null : null;
356427 const failed = status === "failed";
357428 const timing = {
358 status,
429 status: liveStatus,
359430 startedAt: isoOrNull(agent?.startedAt ?? props.startedAt),
360431 completedAt: isoOrNull(agent?.completedAt ?? props.completedAt),
361432 };
362433 const content = (
363434 <>
364 <SubagentAvatar driver={props.driver} provider={props.provider} status={status} />
435 <SubagentAvatar
436 driver={props.driver}
437 provider={props.provider}
438 status={status ?? undefined}
439 />
365440 <span className="min-w-0 flex-1">
366441 <span className="flex items-baseline gap-2">
367442 <span className="min-w-0 truncate text-xs font-medium text-foreground">
368443 {props.title}
369444 </span>
370 {detail !== null && status !== "completed" ? (
445 {detail !== null && (props.event !== undefined || status !== "completed") ? (
371446 <span
372447 className={cn(
373448 "shrink-0 text-3xs",
......@@ -394,7 +469,7 @@ function SubagentTimelineLink(props: {
394469 </span>
395470 </span>
396471 <span className="shrink-0 font-mono text-3xs text-muted-foreground/80">
397 <SubagentElapsed agent={timing} />
472 {props.event ? props.event.timestamp : <SubagentElapsed agent={timing} />}
398473 </span>
399474 {threadId !== null ? (
400475 <ChevronRightIcon
......@@ -437,7 +512,7 @@ function SubagentTimelineLink(props: {
437512 {...props}
438513 elapsed={agent ? <AgentElapsed agent={projectedSubagentsToRuntime([agent])[0]!} /> : null}
439514 model={agent?.model ?? null}
440 status={status}
515 status={liveStatus}
441516 result={agent?.result ?? props.result}
442517 progress={agent?.progress ?? props.progress}
443518 />