Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
7 changes: 4 additions & 3 deletions apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useId, useState } from "react";
import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { AppText as Text } from "./ui/AppText";
import { cn } from "../lib/cn";
import { getProviderClient } from "../lib/providerClients";

type ProviderIconProps = {
Expand Down Expand Up @@ -158,7 +159,7 @@ export function ProviderInstanceIcon(props: {
readonly displayName: string;
readonly accentColor?: string;
readonly showBadge?: boolean;
readonly surfaceColor: string;
readonly surfaceColor?: string;
}) {
const accentColor = normalizeProviderAccentColor(props.accentColor);
return (
Expand All @@ -173,14 +174,14 @@ export function ProviderInstanceIcon(props: {
style={{ position: "absolute", right: -3, bottom: -3, width: 32, alignItems: "flex-end" }}
>
<View
className={accentColor ? undefined : "bg-card"}
className={cn(!accentColor && "bg-card", !props.surfaceColor && "border-screen")}
style={{
height: 12,
minWidth: 12,
paddingHorizontal: 2,
borderRadius: 999,
borderWidth: 1,
borderColor: props.surfaceColor,
...(props.surfaceColor ? { borderColor: props.surfaceColor } : {}),
backgroundColor: accentColor,
alignItems: "center",
justifyContent: "center",
Expand Down
51 changes: 43 additions & 8 deletions apps/mobile/src/features/threads/SubagentRow.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
import { resolveProviderInstanceDisplayName } from "@t3tools/client-runtime/state/provider-instance-display";
import {
resolveProviderInstanceDisplayName,
shouldShowInstanceBadge,
} from "@t3tools/client-runtime/state/provider-instance-display";
import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display";
import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts";
import type { ReactNode } from "react";
import { View } from "react-native";

import { AppText as Text } from "../../components/ui/AppText";
import { SymbolView } from "../../components/ui/AppSymbol";
import { ProviderIcon } from "../../components/ProviderIcon";
import { ProviderIcon, ProviderInstanceIcon } from "../../components/ProviderIcon";
import { cn } from "../../lib/cn";
import { useEnvironmentServerConfig, useProject, useThreadShell } from "../../state/entities";
import { subagentCardDetail } from "./subagent-card-presentation";
Expand Down Expand Up @@ -93,9 +96,30 @@ function SubagentMetadata(props: {
}) {
const { environmentId, subagent } = props;
const config = useEnvironmentServerConfig(environmentId);
const provider = config?.providers.find(
const providers = config?.providers ?? [];
const provider = providers.find(
(candidate) => candidate.instanceId === subagent.providerInstanceId,
);
const badgeEntries = providers.map((candidate) => {
const rawConfig = config?.settings.providerInstances[candidate.instanceId]?.config;
const instanceConfig =
rawConfig !== null && typeof rawConfig === "object"
? (rawConfig as Readonly<Record<string, unknown>>)
: null;
const agentId = instanceConfig?.source === "local" ? undefined : instanceConfig?.agentId;
return {
...candidate,
driverKind: candidate.driver,
acpRegistryAgentId: typeof agentId === "string" ? agentId.trim() || undefined : undefined,
};
});
const badgeEntry = badgeEntries.find(
(candidate) => candidate.instanceId === subagent.providerInstanceId,
);
const showBadge =
provider !== undefined &&
badgeEntry !== undefined &&
shouldShowInstanceBadge(badgeEntry, badgeEntries);
const parent = useThreadShell(scopeThreadRef(environmentId, subagent.threadId))?.source;
const child = useThreadShell(
subagent.childThreadId === null ? null : scopeThreadRef(environmentId, subagent.childThreadId),
Expand All @@ -114,11 +138,22 @@ function SubagentMetadata(props: {
});
return (
<View className="min-w-0 flex-row items-center gap-1.5">
<ProviderIcon
provider={provider?.driver ?? subagent.driver}
iconUrl={provider?.iconUrl}
size={12}
/>
{showBadge ? (
<ProviderInstanceIcon
provider={provider.driver}
iconUrl={provider.iconUrl}
size={12}
displayName={resolveProviderInstanceDisplayName(provider)}
accentColor={provider.accentColor}
showBadge
/>
) : (
<ProviderIcon
provider={provider?.driver ?? subagent.driver}
iconUrl={provider?.iconUrl}
size={12}
/>
)}
<Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
{provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""}
{modelLabel}
Expand Down
14 changes: 13 additions & 1 deletion apps/web/src/components/chat/ThreadLineageWorkflowRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AgentElapsed } from "./AgentElapsed";
import { SubagentTooltipContent } from "./SubagentTooltipContent";
import { ThreadHoverCardPopup } from "../ThreadHoverCard";
import { ThreadRelationshipIcon } from "./ThreadRelationshipIcon";
import type { ProviderInstanceEntry } from "../../providerInstances";
import { cn } from "../../lib/utils";
import { ThreadDetailsControl } from "./ThreadDetailsControl";
import { CollapsibleSectionHeader, SectionHeaderStatus } from "../ui/collapsible-section-header";
Expand Down Expand Up @@ -51,6 +52,7 @@ function WorkflowMemberRow({
providerInstanceId,
provider,
providers,
providerEntries,
driver,
onOpen,
isThreadAvailable,
Expand All @@ -59,6 +61,7 @@ function WorkflowMemberRow({
providerInstanceId: ProviderInstanceId;
provider: ServerProvider | undefined;
providers: ReadonlyArray<ServerProvider> | undefined;
providerEntries: ReadonlyArray<ProviderInstanceEntry>;
driver: ProviderDriverKind | undefined;
onOpen: (threadId: string) => void;
isThreadAvailable: (threadId: string) => boolean;
Expand All @@ -79,7 +82,12 @@ function WorkflowMemberRow({
/>
}
>
<ThreadRelationshipIcon driver={driver} provider={provider} status={member.status} />
<ThreadRelationshipIcon
driver={driver}
provider={provider}
providerEntries={providerEntries}
status={member.status}
/>
<span className="min-w-0 flex-1 truncate">{member.title}</span>
<span className="sr-only">{member.status}</span>
{member.startedAt ? (
Expand Down Expand Up @@ -114,6 +122,7 @@ export function ThreadLineageWorkflowRow({
providerInstanceId,
provider,
providers,
providerEntries,
driver,
onOpenThread,
isThreadAvailable,
Expand All @@ -126,6 +135,7 @@ export function ThreadLineageWorkflowRow({
readonly providerInstanceId: ProviderInstanceId;
readonly provider: ServerProvider | undefined;
readonly providers: ReadonlyArray<ServerProvider> | undefined;
readonly providerEntries: ReadonlyArray<ProviderInstanceEntry>;
readonly driver: ProviderDriverKind | undefined;
readonly onOpenThread: (threadId: string) => void;
readonly isThreadAvailable: (threadId: string) => boolean;
Expand Down Expand Up @@ -224,6 +234,7 @@ export function ThreadLineageWorkflowRow({
providerInstanceId={providerInstanceId}
provider={provider}
providers={providers}
providerEntries={providerEntries}
driver={driver}
onOpen={onOpenThread}
isThreadAvailable={isThreadAvailable}
Expand All @@ -241,6 +252,7 @@ export function ThreadLineageWorkflowRow({
providerInstanceId={providerInstanceId}
provider={provider}
providers={providers}
providerEntries={providerEntries}
driver={driver}
onOpen={onOpenThread}
isThreadAvailable={isThreadAvailable}
Expand Down
15 changes: 13 additions & 2 deletions apps/web/src/components/chat/ThreadRelationshipIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ProviderDriverKind, ServerProvider } from "@t3tools/contracts";
import { BotIcon, type LucideIcon } from "lucide-react";
import { cn } from "../../lib/utils";
import { shouldShowInstanceBadge, type ProviderInstanceEntry } from "../../providerInstances";
import { ProviderInstanceIcon } from "./ProviderInstanceIcon";

export function threadRelationshipStatusLabel(status: string | null): string {
Expand Down Expand Up @@ -45,22 +46,31 @@ export function threadRelationshipStatusLabel(status: string | null): string {
export function ThreadRelationshipIcon({
driver,
provider,
providerEntries = [],
status,
fallbackIcon: FallbackIcon = BotIcon,
}: {
driver?: ProviderDriverKind | undefined;
provider?: ServerProvider | undefined;
providerEntries?: ReadonlyArray<ProviderInstanceEntry>;
status: string | null;
fallbackIcon?: LucideIcon;
}) {
const iconClassName = "size-4 shrink-0 text-muted-foreground";
const entry = driver
? providerEntries.find((candidate) => candidate.instanceId === provider?.instanceId)
: undefined;
const showInstanceBadge = entry !== undefined && shouldShowInstanceBadge(entry, providerEntries);
return (
<span className="relative inline-flex size-4 shrink-0 items-center justify-center">
{driver ? (
<ProviderInstanceIcon
driverKind={driver}
displayName={provider?.displayName ?? driver}
displayName={entry?.displayName ?? provider?.displayName ?? driver}
accentColor={entry?.accentColor}
acpRegistryIconUrl={provider?.iconUrl}
showBadge={showInstanceBadge}
size="compact"
iconClassName={iconClassName}
className="z-auto"
/>
Expand All @@ -69,7 +79,8 @@ export function ThreadRelationshipIcon({
)}
<span
className={cn(
"absolute -bottom-1 -right-1 size-2 rounded-full border-2 border-card",
"absolute size-2 rounded-full border-2 border-card",
showInstanceBadge ? "-left-1 -top-1" : "-bottom-1 -right-1",
status === "running" ||
status === "in_progress" ||
status === "pending" ||
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/components/chat/ThreadRelationshipsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ import {
useThreadRelationshipShells,
} from "../../state/entities";
import { useEnvironmentProviders } from "../../state/environments";
import { serverEnvironment } from "../../state/server";
import {
applyProviderInstanceSettings,
deriveProviderInstanceEntries,
} from "../../providerInstances";
import { threadEnvironment } from "../../state/threads";
import { orchestrationEnvironment } from "../../state/orchestration";
import { useAtomCommand } from "../../state/use-atom-command";
Expand Down Expand Up @@ -224,6 +229,7 @@ export function ThreadRelationshipsPanel(props: {
const ref = scopeThreadRef(props.environmentId, props.threadId);
const projection = useThreadProjection(ref)?.projection ?? null;
const providersByEnvironment = useEnvironmentProviders();
const settings = useAtomValue(serverEnvironment.settingsValueAtom(props.environmentId));
const relationshipShells = useThreadRelationshipShells(ref);
const allProjects = useProjects();
const archived = useArchivedThreadSnapshots([props.environmentId]);
Expand Down Expand Up @@ -259,6 +265,14 @@ export function ThreadRelationshipsPanel(props: {
const [stoppingId, setStoppingId] = useState<string | null>(null);
// Derive graphs after hooks to preserve their compiler cache.
const providers = providersByEnvironment.get(props.environmentId);
const derivedProviderEntries = deriveProviderInstanceEntries(providers ?? []);
const providerEntries = settings
? applyProviderInstanceSettings(
derivedProviderEntries,
settings,
currentThread?.projectId ?? null,
)
: derivedProviderEntries;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const projects = allProjects.filter((project) => project.environmentId === props.environmentId);
const parentNativeStatus = parentProjection
? deriveProviderSubagentStatus(parentProjection)?.status
Expand Down Expand Up @@ -533,6 +547,7 @@ export function ThreadRelationshipsPanel(props: {
<ThreadRelationshipIcon
driver={isSubagent && !isParent ? providerDriver : undefined}
provider={provider}
providerEntries={providerEntries}
fallbackIcon={RelationshipIcon}
status={rowStatus}
/>
Expand Down Expand Up @@ -592,6 +607,7 @@ export function ThreadRelationshipsPanel(props: {
providerInstanceId={agent.providerInstanceId}
provider={provider}
providers={providers}
providerEntries={providerEntries}
driver={providerDriver}
onOpenThread={(memberThreadId) => openThread(memberThreadId as ThreadId)}
isThreadAvailable={(memberThreadId) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ describe("shouldShowInstanceBadge", () => {
expect(shouldShowInstanceBadge(entry, [entry])).toBe(true);
});

it("ignores a non-hex accent color", () => {
const entry = { driverKind: codex, accentColor: "blue", enabled: true };
expect(shouldShowInstanceBadge(entry, [entry])).toBe(false);
});

it("shows the badge when two entries share a driver, even without an accent", () => {
const first = { driverKind: codex, accentColor: undefined, enabled: true };
const second = { driverKind: codex, accentColor: undefined, enabled: true };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function shouldShowInstanceBadge(
readonly enabled: boolean;
}>,
): boolean {
if (entry.accentColor) return true;
if (normalizeProviderAccentColor(entry.accentColor)) return true;
let sharedProviderCount = 0;
for (const candidate of entries) {
if (candidate.driverKind !== entry.driverKind) continue;
Expand Down
Loading