diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md
index bf2e74812..5a4408b2a 100644
--- a/docs/release-control/v6/internal/subsystems/ai-runtime.md
+++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md
@@ -141,6 +141,9 @@ Structured mention resolution also uses the shared AI tools discovery
canonicalization helpers now, so chat prefetch and discovery responses agree
on resource-type and target-ID formatting instead of maintaining chat-local
copies.
+The chat mention picker now also carries the canonical preferred resource
+label as `label` through the structured mention payload, so mention search,
+selection, and submission do not depend on a raw `displayName` field fork.
The same governed-context rule also applies to the main unified AI resource
overview: infrastructure, workload, alert-label, and top-consumer summaries
must not leak raw resource names, cluster labels, IP addresses, or unresolved
diff --git a/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx b/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx
index e20a75f72..c764dc3f4 100644
--- a/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx
+++ b/frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx
@@ -4,7 +4,7 @@ import { getSimpleStatusIndicator } from '@/utils/status';
export interface MentionResource {
id: string;
- displayName: string;
+ label: string;
type: 'vm' | 'system-container' | 'app-container' | 'agent';
status?: string;
node?: string;
@@ -28,7 +28,7 @@ export function MentionAutocomplete(props: MentionAutocompleteProps) {
if (!q) return props.resources.slice(0, 10); // Show first 10 if no query
return props.resources
- .filter((r) => r.displayName.toLowerCase().includes(q))
+ .filter((r) => r.label.toLowerCase().includes(q))
.slice(0, 10); // Limit to 10 results
};
@@ -155,7 +155,7 @@ export function MentionAutocomplete(props: MentionAutocompleteProps) {
{getTypeIcon(resource.type)}
-
{resource.displayName}
+
{resource.label}
({
MentionAutocomplete: (props: {
visible: boolean;
query: string;
- resources: Array<{ id: string; displayName: string }>;
- onSelect: (resource: { id: string; displayName: string }) => void;
+ resources: Array<{ id: string; label: string }>;
+ onSelect: (resource: { id: string; label: string }) => void;
}) => (
resource.displayName).join('|')}
+ data-resource-labels={props.resources.map((resource) => resource.label).join('|')}
>
{
{
id: 'agent-1',
name: 'secret-node-1',
- displayName: 'redacted by policy',
+ label: 'redacted by policy',
type: 'agent' as const,
status: 'online',
policy: governedPolicy,
@@ -537,7 +537,7 @@ describe('AIChat', () => {
{
id: 'agent-2',
name: 'secret-node-2',
- displayName: 'redacted by policy',
+ label: 'redacted by policy',
type: 'agent' as const,
status: 'online',
policy: governedPolicy,
@@ -571,7 +571,7 @@ describe('AIChat', () => {
{
id: 'agent-1',
name: 'secret-node-1',
- displayName: 'redacted by policy',
+ label: 'redacted by policy',
type: 'agent' as const,
status: 'online',
policy: governedPolicy,
diff --git a/frontend-modern/src/components/AI/Chat/__tests__/MentionAutocomplete.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/MentionAutocomplete.test.tsx
index 91caea6e9..014c09033 100644
--- a/frontend-modern/src/components/AI/Chat/__tests__/MentionAutocomplete.test.tsx
+++ b/frontend-modern/src/components/AI/Chat/__tests__/MentionAutocomplete.test.tsx
@@ -8,7 +8,7 @@ afterEach(cleanup);
function makeResource(overrides?: Partial): MentionResource {
return {
id: 'vm-100',
- displayName: 'web-server',
+ label: 'web-server',
type: 'vm',
status: 'running',
node: 'pve1',
@@ -19,29 +19,29 @@ function makeResource(overrides?: Partial): MentionResource {
const defaultResources: MentionResource[] = [
makeResource({
id: 'vm-100',
- displayName: 'web-server',
+ label: 'web-server',
type: 'vm',
status: 'running',
node: 'pve1',
}),
makeResource({
id: 'ct-200',
- displayName: 'db-container',
+ label: 'db-container',
type: 'system-container',
status: 'running',
node: 'pve2',
}),
- makeResource({ id: 'node-1', displayName: 'pve1', type: 'agent', status: 'running' }),
+ makeResource({ id: 'node-1', label: 'pve1', type: 'agent', status: 'running' }),
makeResource({
id: 'docker-1',
- displayName: 'nginx-proxy',
+ label: 'nginx-proxy',
type: 'app-container',
status: 'running',
node: 'docker-host',
}),
makeResource({
id: 'host-1',
- displayName: 'bare-metal-01',
+ label: 'bare-metal-01',
type: 'agent',
status: 'stopped',
node: undefined,
@@ -85,7 +85,7 @@ describe('MentionAutocomplete', () => {
it('shows all resources (up to 10) when query is empty', () => {
renderAutocomplete({ query: '' });
for (const r of defaultResources) {
- expect(screen.getByText(r.displayName)).toBeInTheDocument();
+ expect(screen.getByText(r.label)).toBeInTheDocument();
}
});
@@ -103,7 +103,7 @@ describe('MentionAutocomplete', () => {
it('limits results to 10 items', () => {
const manyResources = Array.from({ length: 15 }, (_, i) =>
- makeResource({ id: `vm-${i}`, displayName: `server-${i}` }),
+ makeResource({ id: `vm-${i}`, label: `server-${i}` }),
);
renderAutocomplete({ resources: manyResources, query: '' });
diff --git a/frontend-modern/src/components/AI/Chat/index.tsx b/frontend-modern/src/components/AI/Chat/index.tsx
index 0ef4902c5..c5e43c835 100644
--- a/frontend-modern/src/components/AI/Chat/index.tsx
+++ b/frontend-modern/src/components/AI/Chat/index.tsx
@@ -438,7 +438,7 @@ export const AIChat: Component = (props) => {
const vmid = parseLegacyVmid(vm, platformData);
mentionCandidates.push({
id: `vm:${node}:${vmid}`,
- displayName: getPreferredResourceDisplayName(vm),
+ label: getPreferredResourceDisplayName(vm),
type: 'vm',
status: vm.status === 'running' ? 'running' : 'stopped',
node,
@@ -453,7 +453,7 @@ export const AIChat: Component = (props) => {
const vmid = parseLegacyVmid(container, platformData);
mentionCandidates.push({
id: `system-container:${node}:${vmid}`,
- displayName: getPreferredResourceDisplayName(container),
+ label: getPreferredResourceDisplayName(container),
type: 'system-container',
status: container.status === 'running' ? 'running' : 'stopped',
node,
@@ -482,7 +482,7 @@ export const AIChat: Component = (props) => {
: runtime.status || 'online';
mentionCandidates.push({
id: `agent:${dockerActionId}`,
- displayName,
+ label: displayName,
type: 'agent',
status: runtimeStatus,
});
@@ -494,7 +494,7 @@ export const AIChat: Component = (props) => {
: container.id;
mentionCandidates.push({
id: `docker:${dockerActionId}:${originalContainerId}`,
- displayName: getPreferredResourceDisplayName(container),
+ label: getPreferredResourceDisplayName(container),
type: 'app-container',
status: container.status === 'running' ? 'running' : 'exited',
node: hostnameOrId,
@@ -506,7 +506,7 @@ export const AIChat: Component = (props) => {
for (const node of nodes) {
mentionCandidates.push({
id: `node:${node.platformId || ''}:${node.name}`,
- displayName: getPreferredResourceDisplayName(node),
+ label: getPreferredResourceDisplayName(node),
type: 'agent',
status: node.status,
});
@@ -522,7 +522,7 @@ export const AIChat: Component = (props) => {
: agentResource.status;
mentionCandidates.push({
id: `agent:${agentActionId}`,
- displayName: name,
+ label: name,
type: 'agent',
status: agentStatus,
});
@@ -541,7 +541,7 @@ export const AIChat: Component = (props) => {
mentions.length > 0
? mentions.map((mention) => ({
id: mention.id,
- name: mention.displayName,
+ name: mention.label,
type: mention.type,
node: mention.node,
}))
@@ -597,7 +597,7 @@ export const AIChat: Component = (props) => {
// Replace @query with the resource name
const before = currentInput.slice(0, startIndex);
const after = currentInput.slice(cursorPos);
- const newValue = `${before}@${resource.displayName} ${after}`;
+ const newValue = `${before}@${resource.label} ${after}`;
setInput(newValue);
setMentionActive(false);
@@ -613,7 +613,7 @@ export const AIChat: Component = (props) => {
setTimeout(() => {
if (textareaRef) {
textareaRef.focus();
- const newCursorPos = startIndex + resource.displayName.length + 2; // +2 for @ and space
+ const newCursorPos = startIndex + resource.label.length + 2; // +2 for @ and space
textareaRef.setSelectionRange(newCursorPos, newCursorPos);
}
}, 0);