Skip to content
Source
Sign inSign up
Open History to see the last commit

V2LifecycleRow.tsx

730 lines25.4 KB

1import { ThreadHoverCardPopup } from "../ThreadHoverCard";
2import { AgentElapsed } from "./AgentElapsed";
3import { projectedSubagentsToRuntime } from "@t3tools/client-runtime/state/subagentRuntime";
4import type { ReactNode } from "react";
5import { useThreadShell, useProject } from "../../state/entities";
6import { useEnvironment } from "../../state/environments";
7import { buildThreadRouteParams } from "../../threadRoutes";
8import { useNavigate } from "@tanstack/react-router";
9import { SubagentTooltipContent } from "./SubagentTooltipContent";
10import { useAtomValue } from "@effect/atom-react";
11import { scopeThreadRef, scopeProjectRef } from "@t3tools/client-runtime/environment";
12import { environmentThreadDetails } from "../../state/threads";
13import { EMPTY_SERVER_PROVIDERS, serverEnvironment } from "../../state/server";
14import { MiddleTruncate } from "../ui/middle-truncate";
15import * as DateTime from "effect/DateTime";
16import { WorkLogRow } from "./WorkLog";
17import {
18 resolveHandoffEndpoints,
19 type HandoffEndpoints,
20 type HandoffTimelineRun,
21 resolveModelSwitchChange,
22} from "@t3tools/client-runtime/handoff";
23import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display";
24import {
25 ProviderDriverKind,
26 type ModelSelection,
27 type OrchestrationV2ModelSwitch,
28 type OrchestrationV2Notification,
29 type OrchestrationV2TurnItem,
30 type OrchestrationV2RemoteTaskChild,
31 type OrchestrationV2Subagent,
32 type ProviderInstanceId,
33 type ServerProvider,
34 type ThreadId,
35 type EnvironmentId,
36 type NodeId,
37 type ScopedThreadRef,
38} from "@t3tools/contracts";
39import type { TimestampFormat } from "@t3tools/contracts/settings";
40import {
41 BotIcon,
42 ChevronRightIcon,
43 CircleStopIcon,
44 ArrowRightLeftIcon,
45 ArrowRightIcon,
46 EraserIcon,
47 GitForkIcon,
48 MessageSquareIcon,
49} from "lucide-react";
50​
51import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";
52import {
53 applyProviderInstanceSettings,
54 brandProviderInstanceEntries,
55 deriveProviderInstanceEntries,
56 shouldShowInstanceBadge,
57 type ProviderInstanceEntry,
58} from "../../providerInstances";
59import { formatShortTimestamp } from "../../timestampFormat";
60import { getTriggerDisplayModelName } from "./providerIconUtils";
61import { ProviderInstanceIcon, providerTextColor } from "./ProviderInstanceIcon";
62import { cn } from "~/lib/utils";
63import { TimelineSystemDivider } from "./TimelineSystemDivider";
64import { subagentResumedSince } from "./V2LifecycleRow.logic";
65import { Button, InlineButton } from "../ui/button";
66import { T3Wordmark } from "../T3Wordmark";
67​
68export type { HandoffTimelineRun } from "@t3tools/client-runtime/handoff";
69​
70export function V2LifecycleRow(props: {
71 readonly item: OrchestrationV2TurnItem;
72 readonly environmentId: EnvironmentId;
73 readonly resourceSummary?: boolean | undefined;
74 readonly runs: ReadonlyArray<HandoffTimelineRun>;
75 readonly onOpenThread: (threadId: ThreadId) => void;
76}) {
77 const { item } = props;
78 if (item.type === "run_interrupt_result") {
79 return (
80 <TimelineSystemDivider
81 label="Run interrupted"
82 detail={item.message}
83 icon={CircleStopIcon}
84 wrapDetail
85 />
86 );
87 }
88 if (item.type === "handoff") {
89 return (
90 <EnvironmentHandoffRow
91 environmentId={props.environmentId}
92 endpoints={resolveHandoffEndpoints(item, props.runs)}
93 failed={item.status === "failed"}
94 />
95 );
96 }
97 if (item.type === "fork") {
98 const relatedThreadId = item.source.type === "run" ? item.source.threadId : item.targetThreadId;
99 return (
100 <TimelineSystemDivider
101 label={item.source.type === "run" ? "Forked from conversation" : "Conversation fork"}
102 icon={GitForkIcon}
103 actionLabel={item.source.type === "run" ? "Open source conversation" : "Open fork"}
104 onAction={() => props.onOpenThread(relatedThreadId)}
105 />
106 );
107 }
108 if (item.type === "thread_created") {
109 if (props.resourceSummary) {
110 return (
111 <div
112 data-v2-item-type={item.type}
113 className="flex min-w-0 items-center gap-3 rounded-lg border border-border/60 p-3"
114 >
115 <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted/60">
116 <MessageSquareIcon className="size-4 text-secondary-label" aria-hidden />
117 </span>
118 <span className="min-w-0 flex-1 truncate text-sm font-medium">
119 {item.title ?? "Created thread"}
120 </span>
121 <Button
122 size="xs"
123 variant="outline"
124 aria-label={`Open ${item.title ?? "created thread"}`}
125 onClick={() => props.onOpenThread(item.targetThreadId)}
126 >
127 Open chat
128 </Button>
129 </div>
130 );
131 }
132 return (
133 <WorkLogRow
134 data-v2-item-type={item.type}
135 icon={<T3Wordmark className="size-4 text-icon-muted" aria-hidden />}
136 label={<>Created thread{item.title ? ` · ${item.title}` : ""}</>}
137 trailing={
138 <InlineButton
139 aria-label={`Open ${item.title ?? "created thread"}`}
140 onClick={() => props.onOpenThread(item.targetThreadId)}
141 >
142 Open chat
143 </InlineButton>
144 }
145 />
146 );
147 }
148 if (item.type === "subagent") {
149 return (
150 <SubagentTimelineLink
151 parentRef={scopeThreadRef(props.environmentId, item.threadId)}
152 subagentId={item.subagentId}
153 providerInstanceId={item.providerInstanceId}
154 origin={item.origin}
155 status={item.status}
156 driver={item.driver}
157 title={formatSubagentDisplayTitle(item.title ?? "Subagent")}
158 result={item.result}
159 progress={item.progress}
160 startedAt={item.startedAt}
161 completedAt={item.completedAt}
162 threadId={item.childThreadId}
163 remote={item.remoteChild}
164 onOpenThread={props.onOpenThread}
165 />
166 );
167 }
168 return null;
170​
171const STATUS_VISUALS: Record<
172 OrchestrationV2TurnItem["status"],
173 { dotClass: string; label: string }
174> = {
175 pending: { dotClass: "bg-info", label: "Queued" },
176 running: { dotClass: "bg-info", label: "Running" },
177 waiting: { dotClass: "bg-info", label: "Waiting" },
178 idle: { dotClass: "bg-muted-foreground/50", label: "Idle · resumable" },
179 completed: { dotClass: "bg-success", label: "Completed" },
180 failed: { dotClass: "bg-destructive", label: "Failed" },
181 cancelled: { dotClass: "bg-muted-foreground/60", label: "Stopped" },
182 interrupted: { dotClass: "bg-muted-foreground/60", label: "Stopped" },
183};
184​
185function subagentStatusVisual(status: OrchestrationV2TurnItem["status"]) {
186 return STATUS_VISUALS[status];
188​
189const SETTLED_SUBAGENT_STATUSES = new Set<OrchestrationV2TurnItem["status"]>([
190 "completed",
191 "failed",
192 "cancelled",
193 "interrupted",
194]);
195​
196const GENERIC_CHILD_END = /^Child task ended with status\b/i;
197​
198function plainDetail(text: string): string {
199 return text
200 .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
201 .replace(/`/g, "")
202 .replace(/^[ \t]*[-*][ \t]+/gm, "")
203 .replace(/\s+/g, " ")
204 .trim();
206​
207function isoOrNull(value: DateTime.Utc | null | undefined): string | null {
208 return value ? DateTime.formatIso(value) : null;
210​
211export function SubagentElapsed({ agent }: { agent: Parameters<typeof AgentElapsed>[0]["agent"] }) {
212 return <AgentElapsed agent={agent} />;
214​
215function SubagentAvatar({
216 driver,
217 displayName,
218 iconUrl,
219 accentColor,
220 showBadge,
221 status,
222 className,
223}: {
224 driver?: ProviderDriverKind | undefined;
225 displayName?: string | undefined;
226 iconUrl?: string | undefined;
227 accentColor?: string | undefined;
228 showBadge: boolean;
229 status?: OrchestrationV2TurnItem["status"] | undefined;
230 className?: string;
231}) {
232 return (
233 <span
234 aria-hidden
235 className={cn(
236 "relative inline-flex size-6 shrink-0 items-center justify-center rounded-full border border-border/70 bg-muted ring-2 ring-background",
237 className,
238 )}
239 >
240 {driver ? (
241 <ProviderInstanceIcon
242 driverKind={driver}
243 displayName={displayName ?? driver}
244 accentColor={accentColor}
245 acpRegistryIconUrl={iconUrl}
246 showBadge={showBadge}
247 size="compact"
248 className="z-auto"
249 iconClassName="size-3.5"
250 />
251 ) : (
252 <BotIcon className="size-3.5 text-muted-foreground" />
253 )}
254 {status ? (
255 <span
256 className={cn(
257 "absolute size-2 rounded-full ring-2 ring-background",
258 showBadge ? "-left-px -top-px" : "-right-px -bottom-px",
259 subagentStatusVisual(status).dotClass,
260 )}
261 />
262 ) : null}
263 </span>
264 );
266​
267// Select strings; status pushes replace providers even when avatar fields are unchanged.
268export function EnvironmentSubagentAvatar({
269 environmentId,
270 providerInstanceId,
271 driver,
272 remote,
273 ...props
274}: {
275 environmentId: EnvironmentId;
276 providerInstanceId: ProviderInstanceId;
277 driver?: ProviderDriverKind | undefined;
278 /** A task in a linked environment runs a provider instance there; its driver here is only a label. */
279 remote?: OrchestrationV2RemoteTaskChild | undefined;
280 status?: OrchestrationV2TurnItem["status"] | undefined;
281}) {
282 const providerEnvironmentId = remote?.environmentId ?? environmentId;
283 const providersAtom = serverEnvironment.providersValueAtom(providerEnvironmentId);
284 const settings = useAtomValue(serverEnvironment.settingsValueAtom(providerEnvironmentId));
285 const entries = (providers: ReadonlyArray<ServerProvider> | null | undefined) => {
286 const derived = deriveProviderInstanceEntries(providers ?? []);
287 return settings ? brandProviderInstanceEntries(derived, settings) : derived;
288 };
289 const providerDriver = useAtomValue(
290 providersAtom,
291 (providers) =>
292 providers?.find((provider) => provider.instanceId === providerInstanceId)?.driver,
293 );
294 const displayName = useAtomValue(
295 providersAtom,
296 (providers) =>
297 entries(providers).find((entry) => entry.instanceId === providerInstanceId)?.displayName,
298 );
299 const accentColor = useAtomValue(
300 providersAtom,
301 (providers) =>
302 entries(providers).find((entry) => entry.instanceId === providerInstanceId)?.accentColor,
303 );
304 const showBadge = useAtomValue(providersAtom, (providers) => {
305 const all = entries(providers);
306 const entry = all.find((candidate) => candidate.instanceId === providerInstanceId);
307 return entry !== undefined && shouldShowInstanceBadge(entry, all);
308 });
309 const iconUrl = useAtomValue(
310 providersAtom,
311 (providers) =>
312 providers?.find((provider) => provider.instanceId === providerInstanceId)?.iconUrl,
313 );
314 return (
315 <SubagentAvatar
316 {...props}
317 driver={providerDriver ?? (remote === undefined ? driver : undefined)}
318 displayName={displayName}
319 iconUrl={iconUrl}
320 accentColor={accentColor}
321 showBadge={showBadge}
322 />
323 );
325​
326const NOTIFICATION_OUTCOME_STATUS: Record<
327 OrchestrationV2Notification["outcome"],
328 OrchestrationV2TurnItem["status"] | null
329> = {
330 completed: "completed",
331 failed: "failed",
332 cancelled: "cancelled",
333 updated: null,
334 unknown: null,
335};
336​
337const NOTIFICATION_OUTCOME_LABEL: Record<OrchestrationV2Notification["outcome"], string> = {
338 completed: "Finished",
339 failed: "Failed",
340 cancelled: "Stopped",
341 updated: "Updated",
342 unknown: "Finished",
343};
344​
345export function SubagentNotificationLink(props: {
346 readonly parentRef: ScopedThreadRef;
347 readonly childThreadId: ThreadId;
348 readonly taskIds?: ReadonlyArray<NodeId> | undefined;
349 readonly outcome: OrchestrationV2Notification["outcome"];
350 readonly createdAt: string;
351 readonly timestampFormat: TimestampFormat;
352 readonly onOpenThread: (threadId: ThreadId) => void;
353 readonly fallback: ReactNode;
354}) {
355 // Follow-up tasks share a child conversation; this notice reports a particular task result.
356 const agent = useAtomValue(
357 environmentThreadDetails.threadAtom(props.parentRef),
358 (thread) =>
359 thread?.projection.subagents.find((agent) =>
360 props.taskIds === undefined
361 ? agent.childThreadId === props.childThreadId
362 : props.taskIds.includes(agent.id),
363 ) ?? null,
364 );
365 if (agent === null) return props.fallback;
366 const resumed =
367 NOTIFICATION_OUTCOME_STATUS[props.outcome] !== null &&
368 subagentResumedSince(agent, props.createdAt);
369 return (
370 <SubagentTimelineLink
371 parentRef={props.parentRef}
372 subagentId={agent.id}
373 providerInstanceId={agent.providerInstanceId}
374 origin={agent.origin}
375 status={agent.status}
376 driver={agent.driver}
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 {...(resumed
385 ? {}
386 : {
387 event: {
388 status: NOTIFICATION_OUTCOME_STATUS[props.outcome],
389 label: NOTIFICATION_OUTCOME_LABEL[props.outcome],
390 timestamp: formatShortTimestamp(props.createdAt, props.timestampFormat),
391 },
392 })}
393 />
394 );
396​
397function SubagentTimelineLink(props: {
398 readonly parentRef: ScopedThreadRef;
399 readonly subagentId: NodeId;
400 readonly providerInstanceId: ProviderInstanceId;
401 readonly origin: OrchestrationV2Subagent["origin"];
402 readonly driver: ProviderDriverKind;
403 readonly title: string;
404 readonly result: string | null;
405 readonly progress: string | undefined;
406 readonly status: OrchestrationV2TurnItem["status"];
407 readonly startedAt: DateTime.Utc | null;
408 readonly completedAt: DateTime.Utc | null;
409 readonly threadId: ThreadId | null;
410 /** The thread in a linked environment that runs this task, when it runs there. */
411 readonly remote?: OrchestrationV2RemoteTaskChild | undefined;
412 readonly onOpenThread: (threadId: ThreadId) => void;
413 readonly event?: {
414 readonly status: OrchestrationV2TurnItem["status"] | null;
415 readonly label: string;
416 readonly timestamp: string;
417 };
418}) {
419 const agent = useAtomValue(
420 environmentThreadDetails.threadAtom(props.parentRef),
421 (thread) => thread?.projection.subagents.find((agent) => agent.id === props.subagentId) ?? null,
422 );
423 const remote = props.remote ?? agent?.remoteChild;
424 const remoteEnvironment = useEnvironment(remote?.environmentId ?? null);
425 const navigate = useNavigate();
426 const threadId = props.threadId;
427 // A task in a linked environment opens there, if this client is connected to it.
428 const open =
429 threadId !== null
430 ? () => props.onOpenThread(threadId)
431 : remote !== undefined && remoteEnvironment !== null
432 ? () =>
433 void navigate({
434 to: "/$environmentId/$threadId",
435 params: buildThreadRouteParams(scopeThreadRef(remote.environmentId, remote.threadId)),
436 })
437 : null;
438 const liveStatus = agent?.status ?? props.status;
439 const status = props.event ? props.event.status : liveStatus;
440 const statusLabel = props.event?.label ?? subagentStatusVisual(liveStatus).label;
441 const result = (agent?.result ?? props.result)?.trim();
442 const progress = (agent?.progress ?? props.progress)?.trim();
443 const settled = SETTLED_SUBAGENT_STATUSES.has(status ?? liveStatus);
444 const rawDetail = settled ? result || progress : progress || result;
445 const detail =
446 rawDetail && !GENERIC_CHILD_END.test(rawDetail) ? plainDetail(rawDetail) || null : null;
447 const failed = status === "failed";
448 const timing = {
449 status: liveStatus,
450 startedAt: isoOrNull(agent?.startedAt ?? props.startedAt),
451 completedAt: isoOrNull(agent?.completedAt ?? props.completedAt),
452 };
453 const content = (
454 <>
455 <EnvironmentSubagentAvatar
456 environmentId={props.parentRef.environmentId}
457 providerInstanceId={props.providerInstanceId}
458 driver={props.driver}
459 remote={remote}
460 status={status ?? undefined}
461 />
462 <span className="min-w-0 flex-1">
463 <span className="flex items-baseline gap-2">
464 <span className="min-w-0 truncate text-xs font-medium text-foreground">
465 {props.title}
466 </span>
467 {remote !== undefined ? (
468 <span className="shrink-0 text-xs text-muted-foreground">on {remote.label}</span>
469 ) : null}
470 </span>
471 <span
472 className={cn(
473 "block text-2xs leading-relaxed",
474 failed ? "text-destructive" : "text-muted-foreground",
475 )}
476 >
477 {detail === null ? (
478 statusLabel
479 ) : detail.includes("/") && !detail.includes(" ") ? (
480 <MiddleTruncate value={detail} showTitle={false} className="flex" />
481 ) : (
482 <span className="block truncate">{detail}</span>
483 )}
484 </span>
485 </span>
486 {detail !== null && (props.event !== undefined || status !== "completed") ? (
487 <span
488 className={cn("shrink-0 text-xs", failed ? "text-destructive" : "text-muted-foreground")}
489 >
490 {statusLabel}
491 </span>
492 ) : null}
493 <span className="shrink-0 text-xs text-muted-foreground tabular-nums">
494 {props.event ? props.event.timestamp : <SubagentElapsed agent={timing} />}
495 </span>
496 {open !== null ? (
497 <ChevronRightIcon
498 aria-hidden
499 className="size-3.5 shrink-0 text-muted-foreground/60 transition-colors group-hover/subagent:text-foreground"
500 />
501 ) : null}
502 </>
503 );
504 const className =
505 "group/subagent flex w-full min-w-0 items-center gap-2.5 rounded-md px-2 py-1.5 text-left";
506 return (
507 <Tooltip>
508 <TooltipTrigger
509 delay={200}
510 render={
511 open === null ? (
512 <div data-v2-item-type="subagent" aria-description={statusLabel} className={className}>
513 {content}
514 </div>
515 ) : (
516 <button
517 type="button"
518 data-v2-item-type="subagent"
519 aria-label={`Open ${props.title}`}
520 aria-description={statusLabel}
521 onClick={open}
522 className={cn(
523 className,
524 "cursor-pointer transition-colors hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70",
525 )}
526 >
527 {content}
528 </button>
529 )
530 }
531 />
532 <ThreadHoverCardPopup>
533 <SubagentTimelineTooltip
534 {...props}
535 elapsed={agent ? <AgentElapsed agent={projectedSubagentsToRuntime([agent])[0]!} /> : null}
536 model={agent?.model ?? null}
537 modelSelection={agent?.modelSelection}
538 status={liveStatus}
539 result={agent?.result ?? props.result}
540 progress={agent?.progress ?? props.progress}
541 />
542 </ThreadHoverCardPopup>
543 </Tooltip>
544 );
546​
547function SubagentTimelineTooltip(
548 props: Parameters<typeof SubagentTimelineLink>[0] & {
549 model: string | null;
550 modelSelection: OrchestrationV2Subagent["modelSelection"];
551 elapsed: ReactNode;
552 },
553) {
554 const environmentId = props.parentRef.environmentId;
555 // Read providers only in the open popup to avoid per-row subscriptions.
556 const providers =
557 useAtomValue(serverEnvironment.providersValueAtom(environmentId)) ?? EMPTY_SERVER_PROVIDERS;
558 const parent = useThreadShell(props.parentRef)?.source;
559 const child = useThreadShell(
560 props.threadId ? scopeThreadRef(environmentId, props.threadId) : null,
561 )?.source;
562 const parentProject = useProject(
563 parent ? scopeProjectRef(environmentId, parent.projectId) : null,
564 );
565 const childProject = useProject(child ? scopeProjectRef(environmentId, child.projectId) : null);
566 return (
567 <SubagentTooltipContent
568 title={formatSubagentDisplayTitle(child?.title ?? props.title)}
569 model={props.model}
570 providerInstanceId={props.providerInstanceId}
571 origin={props.origin}
572 modelSelection={props.modelSelection}
573 provider={providers.find((provider) => provider.instanceId === props.providerInstanceId)}
574 providers={providers}
575 driver={props.driver}
576 elapsed={props.elapsed}
577 status={props.status}
578 result={props.result}
579 progress={props.progress}
580 parentThread={parent}
581 childThread={child}
582 parentProject={parentProject ?? undefined}
583 childProject={childProject ?? undefined}
584 />
585 );
587​
588function EnvironmentHandoffRow({
589 environmentId,
590 ...props
591}: {
592 readonly environmentId: EnvironmentId;
593 readonly endpoints: HandoffEndpoints;
594 readonly failed: boolean;
595}) {
596 const providers =
597 useAtomValue(serverEnvironment.providersValueAtom(environmentId)) ?? EMPTY_SERVER_PROVIDERS;
598 return <HandoffRow {...props} environmentId={environmentId} providers={providers} />;
600​
601/** Marks where `/clear` or an automation started a new provider session in this thread. */
602export function FreshSessionRow() {
603 return <TimelineSystemDivider label="New session" icon={EraserIcon} />;
605​
606/** A same-harness model or effort change. Shows only what changed. */
607export function ModelSwitchRow(props: {
608 readonly environmentId: EnvironmentId;
609 readonly modelSwitch: OrchestrationV2ModelSwitch;
610}) {
611 const providers =
612 useAtomValue(serverEnvironment.providersValueAtom(props.environmentId)) ??
613 EMPTY_SERVER_PROVIDERS;
614 const change = resolveModelSwitchChange(props.modelSwitch, providers);
615 if (change === null) return null;
616 const describe = (selection: ModelSelection, effort: string | undefined) => {
617 const provider = providers.find((candidate) => candidate.instanceId === selection.instanceId);
618 const model = provider?.models.find((candidate) => candidate.slug === selection.model);
619 const modelName = model ? getTriggerDisplayModelName(model, provider?.driver) : selection.model;
620 return [change.modelChanged ? modelName : null, effort].filter(Boolean).join(" ");
621 };
622 const { from, to } = props.modelSwitch;
623 return (
624 <TimelineSystemDivider
625 label={`${describe(from, change.effort?.from)} → ${describe(to, change.effort?.to)}`}
626 />
627 );
629​
630export function HandoffRow(props: {
631 readonly environmentId: EnvironmentId;
632 readonly endpoints: HandoffEndpoints;
633 readonly providers: ReadonlyArray<ServerProvider>;
634 readonly pending?: boolean;
635 readonly failed?: boolean;
636}) {
637 const { from: fromEndpoints, to } = props.endpoints;
638 const settings = useAtomValue(serverEnvironment.settingsValueAtom(props.environmentId));
639 const derived = deriveProviderInstanceEntries(props.providers);
640 const entries = settings ? applyProviderInstanceSettings(derived, settings, null) : derived;
641 return (
642 <TimelineSystemDivider
643 label={props.pending ? "Context handoff pending" : "Context handoff"}
644 icon={ArrowRightLeftIcon}
645 showDetailSeparator={false}
646 tone={props.failed ? "danger" : "neutral"}
647 detail={
648 <span className="inline-flex min-w-0 flex-wrap items-center justify-center gap-1.5">
649 {fromEndpoints.map((endpoint, index) => (
650 <span
651 key={`${endpoint.instanceId}:${endpoint.model ?? ""}`}
652 className="inline-flex min-w-0 items-center"
653 >
654 <HandoffEndpoint
655 entries={entries}
656 instanceId={endpoint.instanceId}
657 model={endpoint.model}
658 />
659 {index < fromEndpoints.length - 1 ? (
660 <span aria-hidden="true" className="ml-0.5">
661 ,
662 </span>
663 ) : null}
664 </span>
665 ))}
666 {fromEndpoints.length > 0 ? (
667 <ArrowRightIcon aria-hidden="true" className="size-3 shrink-0" />
668 ) : null}
669 <HandoffEndpoint entries={entries} instanceId={to.instanceId} model={to.model} />
670 </span>
671 }
672 />
673 );
675​
676function HandoffEndpoint(props: {
677 readonly entries: ReadonlyArray<ProviderInstanceEntry>;
678 readonly instanceId: ProviderInstanceId;
679 readonly model?: string | undefined;
680}) {
681 const entry = props.entries.find((candidate) => candidate.instanceId === props.instanceId);
682 const showBadge = entry !== undefined && shouldShowInstanceBadge(entry, props.entries);
683 const model = props.model?.trim();
684 const providerModel =
685 model === undefined || model.length === 0
686 ? undefined
687 : entry?.models.find((candidate) => candidate.slug === model);
688 const label =
689 providerModel !== undefined
690 ? getTriggerDisplayModelName(providerModel, entry?.driverKind)
691 : model !== undefined && model.length > 0
692 ? model
693 : (entry?.displayName ?? props.instanceId);
694 const labelColor = providerTextColor(
695 entry?.driverKind ?? ProviderDriverKind.make(props.instanceId),
696 );
697 return (
698 <Tooltip>
699 <TooltipTrigger
700 render={
701 <span
702 tabIndex={0}
703 className="inline-flex min-w-0 items-center gap-1.5 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
704 >
705 <ProviderInstanceIcon
706 driverKind={entry?.driverKind ?? ProviderDriverKind.make(props.instanceId)}
707 displayName={entry?.displayName ?? props.instanceId}
708 accentColor={entry?.accentColor}
709 acpRegistryAgentId={entry?.acpRegistryAgentId}
710 acpRegistryIconUrl={entry?.acpRegistryIconUrl}
711 showBadge={showBadge}
712 size="compact"
713 iconClassName={cn(showBadge && "opacity-60")}
714 indicatorBackground="var(--background)"
715 />
716 <span
717 className={cn("truncate font-medium", labelColor.className)}
718 style={labelColor.style}
719 >
720 {label}
721 </span>
722 </span>
723 }
724 />
725 <TooltipPopup>
726 {entry?.displayName ?? props.instanceId} · {label}
727 </TooltipPopup>
728 </Tooltip>
729 );