mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-16 02:42:09 +00:00
Clarify AI mention display contract
This commit is contained in:
parent
a40a9ae8f6
commit
e9768664c8
4 changed files with 42 additions and 26 deletions
|
|
@ -4,7 +4,7 @@ import { getSimpleStatusIndicator } from '@/utils/status';
|
|||
|
||||
export interface MentionResource {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: 'vm' | 'system-container' | 'app-container' | 'agent';
|
||||
status?: string;
|
||||
node?: string;
|
||||
|
|
@ -27,7 +27,9 @@ export function MentionAutocomplete(props: MentionAutocompleteProps) {
|
|||
const q = props.query.toLowerCase();
|
||||
if (!q) return props.resources.slice(0, 10); // Show first 10 if no query
|
||||
|
||||
return props.resources.filter((r) => r.name.toLowerCase().includes(q)).slice(0, 10); // Limit to 10 results
|
||||
return props.resources
|
||||
.filter((r) => r.displayName.toLowerCase().includes(q))
|
||||
.slice(0, 10); // Limit to 10 results
|
||||
};
|
||||
|
||||
// Reset selection when query changes
|
||||
|
|
@ -153,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.name}</span>
|
||||
<span class="font-medium text-base-content truncate">{resource.displayName}</span>
|
||||
<Show when={resource.status}>
|
||||
<StatusDot
|
||||
variant={getSimpleStatusIndicator(resource.status).variant}
|
||||
|
|
|
|||
|
|
@ -127,15 +127,15 @@ vi.mock('../MentionAutocomplete', () => ({
|
|||
MentionAutocomplete: (props: {
|
||||
visible: boolean;
|
||||
query: string;
|
||||
resources: Array<{ id: string; name: string }>;
|
||||
onSelect: (resource: { id: string; name: string }) => void;
|
||||
resources: Array<{ id: string; displayName: string }>;
|
||||
onSelect: (resource: { id: string; displayName: 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.name).join('|')}
|
||||
data-resource-labels={props.resources.map((resource) => resource.displayName).join('|')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -528,7 +528,7 @@ describe('AIChat', () => {
|
|||
{
|
||||
id: 'agent-1',
|
||||
name: 'secret-node-1',
|
||||
displayName: 'secret-node-1',
|
||||
displayName: '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: 'secret-node-2',
|
||||
displayName: '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: 'secret-node-1',
|
||||
displayName: 'redacted by policy',
|
||||
type: 'agent' as const,
|
||||
status: 'online',
|
||||
policy: governedPolicy,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ afterEach(cleanup);
|
|||
function makeResource(overrides?: Partial<MentionResource>): MentionResource {
|
||||
return {
|
||||
id: 'vm-100',
|
||||
name: 'web-server',
|
||||
displayName: 'web-server',
|
||||
type: 'vm',
|
||||
status: 'running',
|
||||
node: 'pve1',
|
||||
|
|
@ -17,25 +17,31 @@ function makeResource(overrides?: Partial<MentionResource>): MentionResource {
|
|||
}
|
||||
|
||||
const defaultResources: MentionResource[] = [
|
||||
makeResource({ id: 'vm-100', name: 'web-server', type: 'vm', status: 'running', node: 'pve1' }),
|
||||
makeResource({
|
||||
id: 'vm-100',
|
||||
displayName: 'web-server',
|
||||
type: 'vm',
|
||||
status: 'running',
|
||||
node: 'pve1',
|
||||
}),
|
||||
makeResource({
|
||||
id: 'ct-200',
|
||||
name: 'db-container',
|
||||
displayName: 'db-container',
|
||||
type: 'system-container',
|
||||
status: 'running',
|
||||
node: 'pve2',
|
||||
}),
|
||||
makeResource({ id: 'node-1', name: 'pve1', type: 'agent', status: 'running' }),
|
||||
makeResource({ id: 'node-1', displayName: 'pve1', type: 'agent', status: 'running' }),
|
||||
makeResource({
|
||||
id: 'docker-1',
|
||||
name: 'nginx-proxy',
|
||||
displayName: 'nginx-proxy',
|
||||
type: 'app-container',
|
||||
status: 'running',
|
||||
node: 'docker-host',
|
||||
}),
|
||||
makeResource({
|
||||
id: 'host-1',
|
||||
name: 'bare-metal-01',
|
||||
displayName: 'bare-metal-01',
|
||||
type: 'agent',
|
||||
status: 'stopped',
|
||||
node: undefined,
|
||||
|
|
@ -79,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.name)).toBeInTheDocument();
|
||||
expect(screen.getByText(r.displayName)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -97,7 +103,7 @@ describe('MentionAutocomplete', () => {
|
|||
|
||||
it('limits results to 10 items', () => {
|
||||
const manyResources = Array.from({ length: 15 }, (_, i) =>
|
||||
makeResource({ id: `vm-${i}`, name: `server-${i}` }),
|
||||
makeResource({ id: `vm-${i}`, displayName: `server-${i}` }),
|
||||
);
|
||||
renderAutocomplete({ resources: manyResources, query: '' });
|
||||
|
||||
|
|
|
|||
|
|
@ -438,7 +438,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
|||
const vmid = parseLegacyVmid(vm, platformData);
|
||||
mentionCandidates.push({
|
||||
id: `vm:${node}:${vmid}`,
|
||||
name: getPreferredResourceDisplayName(vm),
|
||||
displayName: 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}`,
|
||||
name: getPreferredResourceDisplayName(container),
|
||||
displayName: 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}`,
|
||||
name: displayName,
|
||||
displayName,
|
||||
type: 'agent',
|
||||
status: runtimeStatus,
|
||||
});
|
||||
|
|
@ -494,7 +494,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
|||
: container.id;
|
||||
mentionCandidates.push({
|
||||
id: `docker:${dockerActionId}:${originalContainerId}`,
|
||||
name: getPreferredResourceDisplayName(container),
|
||||
displayName: 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}`,
|
||||
name: getPreferredResourceDisplayName(node),
|
||||
displayName: getPreferredResourceDisplayName(node),
|
||||
type: 'agent',
|
||||
status: node.status,
|
||||
});
|
||||
|
|
@ -522,7 +522,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
|||
: agentResource.status;
|
||||
mentionCandidates.push({
|
||||
id: `agent:${agentActionId}`,
|
||||
name,
|
||||
displayName: name,
|
||||
type: 'agent',
|
||||
status: agentStatus,
|
||||
});
|
||||
|
|
@ -537,7 +537,15 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
|||
const prompt = input().trim();
|
||||
if (!prompt) return;
|
||||
const mentions = accumulatedMentions();
|
||||
const mentionsForAPI = mentions.length > 0 ? mentions : undefined;
|
||||
const mentionsForAPI =
|
||||
mentions.length > 0
|
||||
? mentions.map((mention) => ({
|
||||
id: mention.id,
|
||||
name: mention.displayName,
|
||||
type: mention.type,
|
||||
node: mention.node,
|
||||
}))
|
||||
: undefined;
|
||||
// Pass findingId from context on the first message, clear after success
|
||||
const ctx = aiChatStore.context;
|
||||
const findingId = ctx.findingId;
|
||||
|
|
@ -589,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.name} ${after}`;
|
||||
const newValue = `${before}@${resource.displayName} ${after}`;
|
||||
|
||||
setInput(newValue);
|
||||
setMentionActive(false);
|
||||
|
|
@ -605,7 +613,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
|||
setTimeout(() => {
|
||||
if (textareaRef) {
|
||||
textareaRef.focus();
|
||||
const newCursorPos = startIndex + resource.name.length + 2; // +2 for @ and space
|
||||
const newCursorPos = startIndex + resource.displayName.length + 2; // +2 for @ and space
|
||||
textareaRef.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
}, 0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue