diff --git a/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx b/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx index baa720f0d..ad9db32ac 100644 --- a/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx +++ b/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx @@ -110,12 +110,20 @@ export function MentionAutocomplete(props: MentionAutocompleteProps) { // Get status color const getStatusColor = (status?: string) => { - switch (status) { + switch (status?.toLowerCase()) { case 'running': + case 'online': + case 'healthy': + case 'up': return 'bg-green-500'; case 'stopped': + case 'offline': + case 'error': + case 'failed': return 'bg-red-500'; case 'paused': + case 'degraded': + case 'warning': return 'bg-yellow-500'; default: return 'bg-gray-400'; diff --git a/frontend-modern/src/components/AI/Chat/__tests__/mentionResources.test.ts b/frontend-modern/src/components/AI/Chat/__tests__/mentionResources.test.ts index cebb4dfb2..42ab4bfcd 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/mentionResources.test.ts +++ b/frontend-modern/src/components/AI/Chat/__tests__/mentionResources.test.ts @@ -113,6 +113,109 @@ describe('mentionResources', () => { expect(nodeOrHost[0].name).toBe('pve01'); }); + it('deduplicates docker host and host agent via agentId (#1252)', () => { + // Docker host has agentId matching the host agent's id + const state = { + nodes: [], + vms: [], + containers: [], + dockerHosts: [ + { + id: 'docker-host-abc', + agentId: 'agent-xyz', + hostname: 'docker01', + displayName: 'docker01', + status: 'online', + lastSeen: Date.now(), + containers: [], + }, + ], + hosts: [ + { + id: 'agent-xyz', + hostname: 'docker01', + displayName: 'docker01', + status: 'online', + lastSeen: Date.now(), + memory: { total: 16, used: 8, free: 8, usage: 50 }, + }, + ], + } as unknown as State; + + const resources = buildMentionResources(state); + const hostEntries = resources.filter((r) => r.type === 'host'); + expect(hostEntries).toHaveLength(1); + expect(hostEntries[0].name).toBe('docker01'); + }); + + it('deduplicates VM and host agent running inside it via linkedVmId (#1252)', () => { + const state = { + nodes: [], + vms: [ + { + id: 'pve01-100', + vmid: 100, + name: 'my-vm', + node: 'pve01', + instance: 'pve-instance-1', + status: 'running', + }, + ], + containers: [], + dockerHosts: [], + hosts: [ + { + id: 'agent-in-vm', + hostname: 'my-vm', + displayName: 'my-vm', + status: 'online', + lastSeen: Date.now(), + linkedVmId: 'pve01-100', + memory: { total: 8, used: 4, free: 4, usage: 50 }, + }, + ], + } as unknown as State; + + const resources = buildMentionResources(state); + const vmOrHost = resources.filter((r) => r.type === 'vm' || r.type === 'host'); + expect(vmOrHost).toHaveLength(1); + expect(vmOrHost[0].name).toBe('my-vm'); + }); + + it('deduplicates LXC container and host agent running inside it via linkedContainerId (#1252)', () => { + const state = { + nodes: [], + vms: [], + containers: [ + { + id: 'pve01-200', + vmid: 200, + name: 'my-ct', + node: 'pve01', + instance: 'pve-instance-1', + status: 'running', + }, + ], + dockerHosts: [], + hosts: [ + { + id: 'agent-in-ct', + hostname: 'my-ct', + displayName: 'my-ct', + status: 'online', + lastSeen: Date.now(), + linkedContainerId: 'pve01-200', + memory: { total: 4, used: 2, free: 2, usage: 50 }, + }, + ], + } as unknown as State; + + const resources = buildMentionResources(state); + const ctOrHost = resources.filter((r) => r.type === 'container' || r.type === 'host'); + expect(ctOrHost).toHaveLength(1); + expect(ctOrHost[0].name).toBe('my-ct'); + }); + it('deduplicates cluster node mentions from multiple instances and keeps the healthiest status', () => { const state = { nodes: [ diff --git a/frontend-modern/src/components/AI/Chat/mentionResources.ts b/frontend-modern/src/components/AI/Chat/mentionResources.ts index c4f0a7f06..1a0ef1ae7 100644 --- a/frontend-modern/src/components/AI/Chat/mentionResources.ts +++ b/frontend-modern/src/components/AI/Chat/mentionResources.ts @@ -138,7 +138,11 @@ export function buildMentionResources(state: MentionStateSubset): MentionResourc status: vm.status, node: vm.node, }, - [`vm-id:${vm.node}:${vm.vmid}`], + [ + `vm-id:${vm.node}:${vm.vmid}`, + // Register backend VM ID so host agents with linkedVmId can merge (#1252) + `vm-backend-id:${vm.id}`, + ], ); } @@ -153,12 +157,25 @@ export function buildMentionResources(state: MentionStateSubset): MentionResourc status: container.status, node: container.node, }, - [`lxc-id:${container.node}:${container.vmid}`], + [ + `lxc-id:${container.node}:${container.vmid}`, + // Register backend container ID so host agents with linkedContainerId can merge (#1252) + `lxc-backend-id:${container.id}`, + ], ); } for (const host of state.dockerHosts || []) { const hostName = host.displayName || host.hostname || host.id; + const dockerAliases = [ + `docker-host-id:${host.id}`, + `host-name:${hostName}`, + `host-hostname:${host.hostname}`, + ]; + // Link docker host to its host agent via agentId so they merge (#1252) + if (host.agentId) { + dockerAliases.push(`agent-host-id:${host.agentId}`); + } upsertMentionResource( byKey, aliasToKey, @@ -168,11 +185,7 @@ export function buildMentionResources(state: MentionStateSubset): MentionResourc type: 'host', status: host.status || 'online', }, - [ - `docker-host-id:${host.id}`, - `host-name:${hostName}`, - `host-hostname:${host.hostname}`, - ], + dockerAliases, ); for (const container of host.containers || []) { @@ -223,11 +236,16 @@ export function buildMentionResources(state: MentionStateSubset): MentionResourc `host-name:${hostName}`, `host-hostname:${host.hostname}`, ]; - // If this host agent is linked to a PVE node, add the node's backend ID alias so they merge (#1252). - // linkedNodeId is the node's backend ID (format: "instance-nodeName"). + // If this host agent is linked to a PVE entity, add its backend ID alias so they merge (#1252). if (host.linkedNodeId) { aliases.push(`node-backend-id:${host.linkedNodeId}`); } + if (host.linkedVmId) { + aliases.push(`vm-backend-id:${host.linkedVmId}`); + } + if (host.linkedContainerId) { + aliases.push(`lxc-backend-id:${host.linkedContainerId}`); + } upsertMentionResource( byKey, aliasToKey,