Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5560,10 +5560,6 @@ export default function ChatView(props: ChatViewProps) {
const timelineSkills = activeProviderStatus
? resolveProviderSkillsForCwd(activeProviderStatus, gitCwd)
: EMPTY_PROVIDER_SKILLS;
const timelineProviders =
environmentById.get(
displayedThreadRef?.environmentId ?? activeThread?.environmentId ?? environmentId,
)?.serverConfig?.providers ?? EMPTY_PROVIDERS;
const threadFindControlsRef = useRef<ThreadFindControls | null>(null);
const [isThreadFindActive, setIsThreadFindActive] = useState(false);
const openThreadFind = () => threadFindControlsRef.current?.open();
Expand Down Expand Up @@ -8134,7 +8130,6 @@ export default function ChatView(props: ChatViewProps) {
}
listRef={legendListRef}
timelineEntries={displayedTimeline.entries}
providerStatuses={timelineProviders}
runs={paintOnlyDisplayedTimeline ? [] : (serverProjection?.runs ?? [])}
latestRun={paintOnlyDisplayedTimeline ? null : activeActivityRun}
runningRunId={paintOnlyDisplayedTimeline ? null : activeRunningTurnId}
Expand Down
22 changes: 15 additions & 7 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,6 @@ function buildProps() {
listRef: createRef<LegendListRef | null>(),
latestRun: null,
turnDiffSummaries: [],
providerStatuses: [],
runs: [],
routeThreadKey: "environment-local:thread-1",
onOpenTurnDiff: () => {},
Expand Down Expand Up @@ -1975,10 +1974,21 @@ describe("MessagesTimeline", () => {
} as never,
});

const markup = renderToStaticMarkup(
const { RegistryProvider } = await import("@effect/atom-react");
const { serverEnvironment } = await import("../../state/server");
const renderWithProviders = (element: ReactElement) =>
renderToStaticMarkup(
<RegistryProvider
initialValues={[
[serverEnvironment.providersValueAtom(ACTIVE_THREAD_ENVIRONMENT_ID), providerStatuses],
]}
>
{element}
</RegistryProvider>,
);
const markup = renderWithProviders(
<MessagesTimeline
{...buildProps()}
providerStatuses={providerStatuses}
timelineEntries={[
buildHandoffEntry({
fromProviderInstanceIds: ["codex_personal"],
Expand Down Expand Up @@ -2010,10 +2020,9 @@ describe("MessagesTimeline", () => {
modelSelection: { instanceId: "claudeAgent", model: "claude-fable-5" },
},
] as never;
const legacyMarkup = renderToStaticMarkup(
const legacyMarkup = renderWithProviders(
<MessagesTimeline
{...buildProps()}
providerStatuses={providerStatuses}
runs={legacyRuns}
timelineEntries={[
buildHandoffEntry({
Expand All @@ -2028,10 +2037,9 @@ describe("MessagesTimeline", () => {
expect(legacyMarkup).toContain("Claude Fable 5");
expect(legacyMarkup).not.toContain("Full conversation context");

const bareMarkup = renderToStaticMarkup(
const bareMarkup = renderWithProviders(
<MessagesTimeline
{...buildProps()}
providerStatuses={providerStatuses}
timelineEntries={[
buildHandoffEntry({
fromProviderInstanceIds: ["codex_personal"],
Expand Down
17 changes: 4 additions & 13 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
type OrchestrationV2TurnItem,
type RunAttemptId,
type ScopedThreadRef,
type ServerProvider,
type ServerProviderSkill,
type RunId,
ThreadId,
Expand Down Expand Up @@ -286,7 +285,7 @@ import { useV2ItemSupport } from "../../state/v2ItemSupport";
import { Collapsible, CollapsibleTrigger, CollapsiblePanel } from "../ui/collapsible";
import {
isV2LifecycleItem,
SubagentAvatar,
EnvironmentSubagentAvatar,
SubagentElapsed,
SubagentNotificationLink,
V2LifecycleRow,
Expand Down Expand Up @@ -315,7 +314,6 @@ interface TimelineRowSharedState {
resolvedTheme: "light" | "dark";
workspaceRoot: string | undefined;
skills: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
providerStatuses: ReadonlyArray<ServerProvider>;
runs: ReadonlyArray<HandoffTimelineRun>;
activeThreadEnvironmentId: EnvironmentId;
onRevertToTurnCount: (targetTurnCount: number, messageId: MessageId) => void;
Expand Down Expand Up @@ -493,7 +491,6 @@ interface MessagesTimelineProps {
timestampFormat: TimestampFormat;
workspaceRoot: string | undefined;
skills?: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
providerStatuses: ReadonlyArray<ServerProvider>;
runs: ReadonlyArray<HandoffTimelineRun>;
anchorMessageId: MessageId | null;
onAnchorReady: (messageId: MessageId, anchorIndex: number) => void;
Expand Down Expand Up @@ -589,7 +586,6 @@ const ConversationTimeline = memo(function ConversationTimeline({
timestampFormat,
workspaceRoot,
skills = EMPTY_TIMELINE_SKILLS,
providerStatuses,
runs: runsProp,
anchorMessageId,
onAnchorReady,
Expand Down Expand Up @@ -1219,7 +1215,6 @@ const ConversationTimeline = memo(function ConversationTimeline({
resolvedTheme,
workspaceRoot,
skills,
providerStatuses,
runs,
activeThreadEnvironmentId,
onRevertToTurnCount,
Expand Down Expand Up @@ -2936,7 +2931,6 @@ function V2EventTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "event"
resourceSummary={row.resourceSummary}
createdAt={row.createdAt}
timestampFormat={ctx.timestampFormat}
providerStatuses={ctx.providerStatuses}
runs={ctx.runs}
onOpenThread={ctx.onOpenThread}
/>
Expand Down Expand Up @@ -3185,12 +3179,11 @@ const V2SubagentGroup = memo(function V2SubagentGroup({
>
<span className="flex shrink-0 items-center -space-x-1.5" aria-hidden>
{agents.slice(0, 3).map(({ item, status }) => (
<SubagentAvatar
<EnvironmentSubagentAvatar
key={item.id}
environmentId={ctx.activeThreadEnvironmentId}
providerInstanceId={item.providerInstanceId}
driver={item.driver}
provider={ctx.providerStatuses.find(
(provider) => provider.instanceId === item.providerInstanceId,
)}
status={agents.length === 1 ? status : undefined}
/>
))}
Expand Down Expand Up @@ -3233,7 +3226,6 @@ const V2SubagentGroup = memo(function V2SubagentGroup({
item={item}
createdAt={row.createdAt}
timestampFormat={ctx.timestampFormat}
providerStatuses={ctx.providerStatuses}
runs={ctx.runs}
onOpenThread={ctx.onOpenThread}
/>
Expand Down Expand Up @@ -5195,7 +5187,6 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: WorkEntryRowP
outcome={item.outcome}
createdAt={props.workEntry.createdAt}
timestampFormat={ctx.timestampFormat}
providerStatuses={ctx.providerStatuses}
onOpenThread={ctx.onOpenThread}
fallback={<WorkEntryLogRow {...props} />}
/>
Expand Down
84 changes: 61 additions & 23 deletions apps/web/src/components/chat/V2LifecycleRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SubagentTooltipContent } from "./SubagentTooltipContent";
import { useAtomValue } from "@effect/atom-react";
import { scopeThreadRef, scopeProjectRef } from "@t3tools/client-runtime/environment";
import { environmentThreadDetails } from "../../state/threads";
import { EMPTY_SERVER_PROVIDERS, serverEnvironment } from "../../state/server";
import { MiddleTruncate } from "../ui/middle-truncate";
import * as DateTime from "effect/DateTime";
import { WorkLogRow } from "./WorkLog";
Expand Down Expand Up @@ -72,7 +73,6 @@ export function V2LifecycleRow(props: {
readonly resourceSummary?: boolean | undefined;
readonly createdAt: string;
readonly timestampFormat: TimestampFormat;
readonly providerStatuses: ReadonlyArray<ServerProvider>;
readonly runs: ReadonlyArray<HandoffTimelineRun>;
readonly onOpenThread: (threadId: ThreadId) => void;
}) {
Expand Down Expand Up @@ -125,9 +125,9 @@ export function V2LifecycleRow(props: {
}
if (item.type === "handoff") {
return (
<HandoffRow
<EnvironmentHandoffRow
environmentId={props.environmentId}
endpoints={resolveHandoffEndpoints(item, props.runs)}
providers={props.providerStatuses}
failed={item.status === "failed"}
/>
);
Expand Down Expand Up @@ -192,10 +192,6 @@ export function V2LifecycleRow(props: {
origin={item.origin}
status={item.status}
driver={item.driver}
provider={props.providerStatuses.find(
(provider) => provider.instanceId === item.providerInstanceId,
)}
providers={props.providerStatuses}
title={formatSubagentDisplayTitle(item.title ?? "Subagent")}
result={item.result}
progress={item.progress}
Expand Down Expand Up @@ -253,14 +249,16 @@ export function SubagentElapsed({ agent }: { agent: Parameters<typeof AgentElaps
return <AgentElapsed agent={agent} />;
}

export function SubagentAvatar({
function SubagentAvatar({
driver,
provider,
displayName,
iconUrl,
status,
className,
}: {
driver?: ProviderDriverKind | undefined;
provider?: ServerProvider | undefined;
displayName?: string | undefined;
iconUrl?: string | undefined;
status?: OrchestrationV2TurnItem["status"] | undefined;
className?: string;
}) {
Expand All @@ -275,8 +273,8 @@ export function SubagentAvatar({
{driver ? (
<ProviderInstanceIcon
driverKind={driver}
displayName={provider?.displayName ?? driver}
acpRegistryIconUrl={provider?.iconUrl}
displayName={displayName ?? driver}
acpRegistryIconUrl={iconUrl}
className="z-auto"
iconClassName="size-3.5"
/>
Expand All @@ -295,6 +293,35 @@ export function SubagentAvatar({
);
}

/**
* Reads only the two provider fields the avatar draws. Provider status pushes
* replace the provider objects (fresh `checkedAt`), so selecting strings keeps
* the avatar from re-rendering when nothing it shows changed.
*/
export function EnvironmentSubagentAvatar({
environmentId,
providerInstanceId,
...props
}: {
environmentId: EnvironmentId;
providerInstanceId: ProviderInstanceId;
driver?: ProviderDriverKind | undefined;
status?: OrchestrationV2TurnItem["status"] | undefined;
}) {
const providersAtom = serverEnvironment.providersValueAtom(environmentId);
const displayName = useAtomValue(
providersAtom,
(providers) =>
providers?.find((provider) => provider.instanceId === providerInstanceId)?.displayName,
);
const iconUrl = useAtomValue(
providersAtom,
(providers) =>
providers?.find((provider) => provider.instanceId === providerInstanceId)?.iconUrl,
);
return <SubagentAvatar {...props} displayName={displayName} iconUrl={iconUrl} />;
}

const NOTIFICATION_OUTCOME_STATUS: Record<
OrchestrationV2Notification["outcome"],
OrchestrationV2TurnItem["status"] | null
Expand All @@ -320,7 +347,6 @@ export function SubagentNotificationLink(props: {
readonly outcome: OrchestrationV2Notification["outcome"];
readonly createdAt: string;
readonly timestampFormat: TimestampFormat;
readonly providerStatuses: ReadonlyArray<ServerProvider>;
readonly onOpenThread: (threadId: ThreadId) => void;
readonly fallback: ReactNode;
}) {
Expand All @@ -339,10 +365,6 @@ export function SubagentNotificationLink(props: {
origin={agent.origin}
status={agent.status}
driver={agent.driver}
provider={props.providerStatuses.find(
(provider) => provider.instanceId === agent.providerInstanceId,
)}
providers={props.providerStatuses}
title={formatSubagentDisplayTitle(agent.title ?? "Subagent")}
result={agent.result}
progress={agent.progress}
Expand All @@ -365,8 +387,6 @@ function SubagentTimelineLink(props: {
readonly providerInstanceId: ProviderInstanceId;
readonly origin: OrchestrationV2Subagent["origin"];
readonly driver: ProviderDriverKind;
readonly provider: ServerProvider | undefined;
readonly providers: ReadonlyArray<ServerProvider>;
readonly title: string;
readonly result: string | null;
readonly progress: string | undefined;
Expand Down Expand Up @@ -403,9 +423,10 @@ function SubagentTimelineLink(props: {
};
const content = (
<>
<SubagentAvatar
<EnvironmentSubagentAvatar
environmentId={props.parentRef.environmentId}
providerInstanceId={props.providerInstanceId}
driver={props.driver}
provider={props.provider}
status={status ?? undefined}
/>
<span className="min-w-0 flex-1">
Expand Down Expand Up @@ -501,6 +522,10 @@ function SubagentTimelineTooltip(
},
) {
const environmentId = props.parentRef.environmentId;
// The popup only mounts while open, so the full provider list is read here
// instead of in every timeline row.
const providers =
useAtomValue(serverEnvironment.providersValueAtom(environmentId)) ?? EMPTY_SERVER_PROVIDERS;
const parent = useThreadShell(props.parentRef)?.source;
const child = useThreadShell(
props.threadId ? scopeThreadRef(environmentId, props.threadId) : null,
Expand All @@ -516,8 +541,8 @@ function SubagentTimelineTooltip(
providerInstanceId={props.providerInstanceId}
origin={props.origin}
modelSelection={props.modelSelection}
provider={props.provider}
providers={props.providers}
provider={providers.find((provider) => provider.instanceId === props.providerInstanceId)}
providers={providers}
driver={props.driver}
elapsed={props.elapsed}
status={props.status}
Expand All @@ -531,6 +556,19 @@ function SubagentTimelineTooltip(
);
}

function EnvironmentHandoffRow({
environmentId,
...props
}: {
readonly environmentId: EnvironmentId;
readonly endpoints: HandoffEndpoints;
readonly failed: boolean;
}) {
const providers =
useAtomValue(serverEnvironment.providersValueAtom(environmentId)) ?? EMPTY_SERVER_PROVIDERS;
return <HandoffRow {...props} providers={providers} />;
}

export function HandoffRow(props: {
readonly endpoints: HandoffEndpoints;
readonly providers: ReadonlyArray<ServerProvider>;
Expand Down
Loading