From c12103ef1c565d952922c9177611befcd6a9612d Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 9 Apr 2026 13:53:24 +0800 Subject: [PATCH] feat(runtime): use avatar for owner display and add owner filter - Show owner avatar + name in runtime list items (replaces text-only) - Show owner avatar + name in runtime detail info grid - Add per-owner filter pills in "All" mode for quick filtering Co-Authored-By: Claude Opus 4.6 (1M context) --- .../runtimes/components/runtime-detail.tsx | 18 +++- .../runtimes/components/runtime-list.tsx | 89 +++++++++++++++---- .../runtimes/components/runtimes-page.tsx | 3 + 3 files changed, 89 insertions(+), 21 deletions(-) diff --git a/packages/views/runtimes/components/runtime-detail.tsx b/packages/views/runtimes/components/runtime-detail.tsx index 0ff82eb38..f825f1a8d 100644 --- a/packages/views/runtimes/components/runtime-detail.tsx +++ b/packages/views/runtimes/components/runtime-detail.tsx @@ -20,6 +20,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@multica/ui/components/ui/alert-dialog"; +import { ActorAvatar } from "../../common/actor-avatar"; import { formatLastSeen } from "../utils"; import { RuntimeModeIcon, StatusBadge, InfoField } from "./shared"; import { PingSection } from "./ping-section"; @@ -50,9 +51,8 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) { // Resolve owner info const ownerMember = runtime.owner_id - ? members.find((m) => m.user_id === runtime.owner_id) + ? members.find((m) => m.user_id === runtime.owner_id) ?? null : null; - const ownerName = ownerMember?.name ?? null; // Permission check for delete const currentMember = user @@ -118,7 +118,19 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) { label="Last Seen" value={formatLastSeen(runtime.last_seen_at)} /> - {ownerName && } + {ownerMember && ( +
+
Owner
+
+ + {ownerMember.name} +
+
+ )} {runtime.device_info && ( )} diff --git a/packages/views/runtimes/components/runtime-list.tsx b/packages/views/runtimes/components/runtime-list.tsx index 04c112b74..4bbb1a8f4 100644 --- a/packages/views/runtimes/components/runtime-list.tsx +++ b/packages/views/runtimes/components/runtime-list.tsx @@ -1,8 +1,9 @@ import { Server } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; -import type { AgentRuntime } from "@multica/core/types"; +import type { AgentRuntime, MemberWithUser } from "@multica/core/types"; import { useWorkspaceId } from "@multica/core/hooks"; import { memberListOptions } from "@multica/core/workspace/queries"; +import { ActorAvatar } from "../../common/actor-avatar"; import { RuntimeModeIcon } from "./shared"; type RuntimeFilter = "mine" | "all"; @@ -10,12 +11,12 @@ type RuntimeFilter = "mine" | "all"; function RuntimeListItem({ runtime, isSelected, - ownerName, + ownerMember, onClick, }: { runtime: AgentRuntime; isSelected: boolean; - ownerName: string | null; + ownerMember: MemberWithUser | null; onClick: () => void; }) { return ( @@ -34,9 +35,19 @@ function RuntimeListItem({
{runtime.name}
-
- {runtime.provider} · {runtime.runtime_mode} - {ownerName && <> · {ownerName}} +
+ {ownerMember && ( + <> + + {ownerMember.name} + · + + )} + {runtime.provider}
void; filter: RuntimeFilter; onFilterChange: (filter: RuntimeFilter) => void; + ownerFilter: string | null; + onOwnerFilterChange: (ownerId: string | null) => void; }) { const wsId = useWorkspaceId(); const { data: members = [] } = useQuery(memberListOptions(wsId)); - const getOwnerName = (ownerId: string | null) => { + const getOwnerMember = (ownerId: string | null) => { if (!ownerId) return null; - return members.find((m) => m.user_id === ownerId)?.name ?? null; + return members.find((m) => m.user_id === ownerId) ?? null; }; + // Get unique owners from runtimes for filter dropdown + const uniqueOwners = filter === "all" + ? Array.from(new Set(runtimes.map((r) => r.owner_id).filter(Boolean) as string[])) + .map((id) => members.find((m) => m.user_id === id)) + .filter(Boolean) as MemberWithUser[] + : []; + + // Apply client-side owner filter when in "all" mode + const filteredRuntimes = filter === "all" && ownerFilter + ? runtimes.filter((r) => r.owner_id === ownerFilter) + : runtimes; + return (

Runtimes

- {runtimes.filter((r) => r.status === "online").length}/ - {runtimes.length} online + {filteredRuntimes.filter((r) => r.status === "online").length}/ + {filteredRuntimes.length} online
{/* Filter toggle */} -
+
+ + {/* Owner filter pills (shown in "all" mode when there are multiple owners) */} + {filter === "all" && uniqueOwners.length > 1 && ( + <> +
+ {uniqueOwners.map((m) => ( + + ))} + + )}
- {runtimes.length === 0 ? ( + {filteredRuntimes.length === 0 ? (

- {filter === "mine" ? "No runtimes owned by you" : "No runtimes registered"} + {filter === "mine" ? "No runtimes owned by you" : ownerFilter ? "No runtimes for this owner" : "No runtimes registered"}

Run{" "} @@ -119,12 +172,12 @@ export function RuntimeList({

) : (
- {runtimes.map((runtime) => ( + {filteredRuntimes.map((runtime) => ( onSelect(runtime.id)} /> ))} diff --git a/packages/views/runtimes/components/runtimes-page.tsx b/packages/views/runtimes/components/runtimes-page.tsx index 3d550fd5d..db8b5d5fd 100644 --- a/packages/views/runtimes/components/runtimes-page.tsx +++ b/packages/views/runtimes/components/runtimes-page.tsx @@ -24,6 +24,7 @@ export default function RuntimesPage() { const wsId = useWorkspaceId(); const qc = useQueryClient(); const [filter, setFilter] = useState("mine"); + const [ownerFilter, setOwnerFilter] = useState(null); const [selectedId, setSelectedId] = useState(""); const ownerParam = filter === "mine" ? "me" as const : undefined; @@ -102,6 +103,8 @@ export default function RuntimesPage() { onSelect={setSelectedId} filter={filter} onFilterChange={setFilter} + ownerFilter={ownerFilter} + onOwnerFilterChange={setOwnerFilter} />