diff --git a/packages/views/agents/components/runtime-machine-filter-dropdown.test.tsx b/packages/views/agents/components/runtime-machine-filter-dropdown.test.tsx
index 4700b288d2..b636d91962 100644
--- a/packages/views/agents/components/runtime-machine-filter-dropdown.test.tsx
+++ b/packages/views/agents/components/runtime-machine-filter-dropdown.test.tsx
@@ -31,6 +31,7 @@ function makeMachine(
runningCount: 0,
queuedCount: 0,
providerNames: ["claude"],
+ ownerIds: ["user-1"],
lastSeenAt: "2026-05-17T11:59:50Z",
...overrides,
};
diff --git a/packages/views/runtimes/components/runtime-machines.test.ts b/packages/views/runtimes/components/runtime-machines.test.ts
index 027a4446af..8fa0151ef5 100644
--- a/packages/views/runtimes/components/runtime-machines.test.ts
+++ b/packages/views/runtimes/components/runtime-machines.test.ts
@@ -52,6 +52,21 @@ describe("runtime machine grouping", () => {
});
});
+ it("collects distinct runtime owners for the machine, ignoring null owners", () => {
+ const machines = buildRuntimeMachines(
+ [
+ makeRuntime({ id: "rt-a", provider: "claude", owner_id: "user-1" }),
+ makeRuntime({ id: "rt-b", provider: "codex", owner_id: "user-2" }),
+ makeRuntime({ id: "rt-c", provider: "cursor", owner_id: "user-1" }),
+ makeRuntime({ id: "rt-d", provider: "pi", owner_id: null }),
+ ],
+ { now: NOW, localDaemonId: "daemon-1" },
+ );
+
+ expect(machines).toHaveLength(1);
+ expect(machines[0]?.ownerIds).toEqual(["user-1", "user-2"]);
+ });
+
it("counts machines with any offline runtime as issues", () => {
const machines = buildRuntimeMachines(
[
diff --git a/packages/views/runtimes/components/runtime-machines.ts b/packages/views/runtimes/components/runtime-machines.ts
index ae00d0dcd2..a8106cb08d 100644
--- a/packages/views/runtimes/components/runtime-machines.ts
+++ b/packages/views/runtimes/components/runtime-machines.ts
@@ -27,6 +27,8 @@ export interface RuntimeMachine {
runningCount: number;
queuedCount: number;
providerNames: string[];
+ /** Distinct workspace user ids that own this machine's runtimes. */
+ ownerIds: string[];
lastSeenAt: string | null;
}
@@ -129,6 +131,7 @@ function placeholderLocalMachine(
runningCount: 0,
queuedCount: 0,
providerNames: [],
+ ownerIds: [],
lastSeenAt: null,
};
}
@@ -181,6 +184,12 @@ function finalizeRuntimeMachine(
);
const first = runtimes[0];
const providerNames = Array.from(new Set(runtimes.map((r) => r.provider))).sort();
+ // Distinct owners of this machine's runtimes. In practice a daemon has a
+ // single registrant, but a machine can technically host runtimes owned by
+ // different members, so keep every distinct owner for the sidebar avatars.
+ const ownerIds = Array.from(
+ new Set(runtimes.map((r) => r.owner_id).filter((id): id is string => !!id)),
+ );
// Device-name consolidation is only safe for the current user's own
// local runtimes — the list spans the whole workspace, so a host-name
// match alone could claim another member's identically-named machine.
@@ -248,6 +257,7 @@ function finalizeRuntimeMachine(
runningCount: workload.runningCount,
queuedCount: workload.queuedCount,
providerNames,
+ ownerIds,
lastSeenAt: latestLastSeenAt(runtimes),
};
}
diff --git a/packages/views/runtimes/components/runtimes-page.tsx b/packages/views/runtimes/components/runtimes-page.tsx
index ee0cf6b9c6..13b482ec98 100644
--- a/packages/views/runtimes/components/runtimes-page.tsx
+++ b/packages/views/runtimes/components/runtimes-page.tsx
@@ -10,6 +10,7 @@ import {
Server,
} from "lucide-react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
+import type { MemberWithUser } from "@multica/core/types";
import { useAuthStore } from "@multica/core/auth";
import { useWorkspaceId } from "@multica/core/hooks";
import { agentTaskSnapshotOptions } from "@multica/core/agents";
@@ -34,6 +35,7 @@ import { CloudRuntimeDialog } from "./cloud-runtime-dialog";
import { RuntimeProfilesDialog } from "./runtime-profiles-dialog";
import { ProviderLogo } from "./provider-logo";
import { RuntimeList, buildWorkloadIndex } from "./runtime-list";
+import { ActorAvatar } from "../../common/actor-avatar";
import {
pendingRuntimesForProfiles,
type PendingRuntimeProfile,
@@ -638,15 +640,18 @@ function MachineRow({
- {busyCount > 0 ? (
-
- {t(($) => $.machine.busy_count, { count: busyCount })}
-
- ) : (
-
- {runtimeCount}
-
- )}
+
+
+ {busyCount > 0 ? (
+
+ {t(($) => $.machine.busy_count, { count: busyCount })}
+
+ ) : (
+
+ {runtimeCount}
+
+ )}
+
@@ -675,6 +680,55 @@ function ProviderIconStack({ providers }: { providers: string[] }) {
);
}
+// Owner avatars for a machine's runtimes, shown in the sidebar row. Surfaces
+// "whose machine is this" at a glance: the runtime owner is where commands and
+// file access actually run, so a member can tell a shared (public) runtime from
+// their own machine before routing work to it. Renders plain, non-interactive
+// avatars (the row itself is the click target) with the owner name as a native
+// tooltip. Owners that no longer resolve to a workspace member are skipped
+// rather than rendered as an "Unknown" placeholder.
+function MachineOwners({ ownerIds }: { ownerIds: string[] }) {
+ const wsId = useWorkspaceId();
+ const { data: members = [] } = useQuery(memberListOptions(wsId));
+ const owners = useMemo(() => {
+ if (ownerIds.length === 0) return [];
+ const byUserId = new Map(members.map((m) => [m.user_id, m]));
+ return ownerIds
+ .map((id) => byUserId.get(id))
+ .filter((m): m is MemberWithUser => !!m);
+ }, [ownerIds, members]);
+
+ if (owners.length === 0) return null;
+
+ const visible = owners.slice(0, 2);
+ const extra = owners.length - visible.length;
+ return (
+
+ {visible.map((owner) => (
+
+
+
+ ))}
+ {extra > 0 && (
+
+ +{extra}
+
+ )}
+
+ );
+}
+
function MachineDetail({
machine,
updatableIds,