Rename AI chat mention labels

This commit is contained in:
rcourtman 2026-03-19 19:32:55 +00:00
parent f5fba35368
commit fa99046c64
5 changed files with 29 additions and 26 deletions

View file

@ -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

View file

@ -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) {
<span class="text-muted">{getTypeIcon(resource.type)}</span>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="font-medium text-base-content truncate">{resource.displayName}</span>
<span class="font-medium text-base-content truncate">{resource.label}</span>
<Show when={resource.status}>
<StatusDot
variant={getSimpleStatusIndicator(resource.status).variant}

View file

@ -127,15 +127,15 @@ vi.mock('../MentionAutocomplete', () => ({
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;
}) => (
<div
data-testid="mention-autocomplete"
data-visible={String(props.visible)}
data-query={props.query}
data-resource-count={String(props.resources.length)}
data-resource-labels={props.resources.map((resource) => resource.displayName).join('|')}
data-resource-labels={props.resources.map((resource) => resource.label).join('|')}
>
<button
type="button"
@ -528,7 +528,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,
@ -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,

View file

@ -8,7 +8,7 @@ afterEach(cleanup);
function makeResource(overrides?: Partial<MentionResource>): 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>): 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: '' });

View file

@ -438,7 +438,7 @@ export const AIChat: Component<AIChatProps> = (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<AIChatProps> = (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<AIChatProps> = (props) => {
: runtime.status || 'online';
mentionCandidates.push({
id: `agent:${dockerActionId}`,
displayName,
label: displayName,
type: 'agent',
status: runtimeStatus,
});
@ -494,7 +494,7 @@ export const AIChat: Component<AIChatProps> = (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<AIChatProps> = (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<AIChatProps> = (props) => {
: agentResource.status;
mentionCandidates.push({
id: `agent:${agentActionId}`,
displayName: name,
label: name,
type: 'agent',
status: agentStatus,
});
@ -541,7 +541,7 @@ export const AIChat: Component<AIChatProps> = (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<AIChatProps> = (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<AIChatProps> = (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);