Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# OSIM Changelog

## [Unreleased]
### Added
* Show warning icons with recorded reasons and available metadata when AI-Bot skips field suggestions, including when AI generation is disabled (`OSIDB-5469`)

### Fixed
* Show Aegis Multi Suggest loading indicators only for selected fields and clear them when generation completes (`AEGIS-542`)

## [2026.10.1]
# [2026.10.1]
### Added
* Show why an automated workflow label was applied, on the flaw state badge and in the flaw queue (`OSIDB-5347`)
* Show optional Aegis suggestion metadata in tooltips (`OSIDB-5272`)
Expand Down
25 changes: 18 additions & 7 deletions src/components/Aegis/AegisActions.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, ref, useId } from 'vue';

import { useFlaw } from '@/composables/useFlaw';

Expand All @@ -13,6 +13,7 @@ const props = defineProps<{
isFetchingSuggestion: boolean;
isFieldAIBot?: boolean;
selectedIndex: number;
skipTooltipText?: string;
suggestions: string[];
tooltipText: string;
}>();
Expand All @@ -31,9 +32,12 @@ const isEmbargoed = computed(() => Boolean(flaw.value.embargoed));

const showFeedbackModal = ref(false);
const showSuggestionTooltip = ref(false);
const suggestionTooltipId = useId();

const showSkipWarning = computed(() => !props.hasAppliedSuggestion && !!props.skipTooltipText);

const hasSuggestionTooltip = computed(() => {
return props.hasAppliedSuggestion || props.isFieldAIBot;
return props.hasAppliedSuggestion || props.isFieldAIBot || showSkipWarning.value;
});

function toggleSuggestionTooltip() {
Expand All @@ -60,8 +64,9 @@ function handleFeedbackCancel() {
}

const safeTooltipText = computed(() => {
if (!props.tooltipText) return '';
return props.tooltipText
const text = showSkipWarning.value ? props.skipTooltipText : props.tooltipText;
if (!text) return '';
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
Expand All @@ -87,9 +92,14 @@ const safeTooltipText = computed(() => {
@click.stop
>
<i
class="bi bi-info-circle me-1"
style="cursor: pointer; font-size: 0.9em; color: #6c757d;"
title="Click to see suggestion details"
class="bi me-1"
:class="showSkipWarning ? 'bi-exclamation-triangle text-warning' : 'bi-info-circle text-secondary'"
style="cursor: pointer; font-size: 0.9em;"
:title="showSkipWarning
? 'Click to see why the AI-Bot suggestion is unavailable'
: 'Click to see suggestion details'"
:aria-label="showSkipWarning ? 'AI-Bot suggestion unavailable' : 'AI suggestion details'"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
:aria-describedby="showSuggestionTooltip ? suggestionTooltipId : undefined"
role="button"
tabindex="0"
@click.stop.prevent="toggleSuggestionTooltip"
Expand All @@ -99,6 +109,7 @@ const safeTooltipText = computed(() => {
<!-- eslint-disable vue/no-v-html -->
<div
v-if="showSuggestionTooltip"
:id="suggestionTooltipId"
class="aegis-tooltip"
v-html="safeTooltipText"
></div>
Expand Down
4 changes: 3 additions & 1 deletion src/components/Aegis/AegisComponentActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const {
'components',
);

const { getAIBotEntry, getAIBotTooltip, isFieldValueAIBot } = useAegisMetadataTracking();
const { getAIBotEntry, getAIBotSkipTooltip, getAIBotTooltip, isFieldValueAIBot } = useAegisMetadataTracking();
const skipTooltip = computed(() => getAIBotSkipTooltip('components'));
const { sendFeedback: sendBotFeedback } = useSimpleFeedback();

const isComponentsAIBot = computed(() => isFieldValueAIBot('components', modelValue.value));
Expand Down Expand Up @@ -119,6 +120,7 @@ const suggestionTooltip = computed(() => {
:suggestions="allSuggestions as string[]"
:selectedIndex="selectedSuggestionIndex"
:tooltipText="suggestionTooltip"
:skipTooltipText="skipTooltip"
@suggest="suggestComponents"
@selectSuggestion="selectSuggestion"
@revert="revertComponents"
Expand Down
4 changes: 3 additions & 1 deletion src/components/Aegis/AegisCvssActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ const {
suggestCvss,
} = useAegisSuggestion(props.aegisContext as AegisSuggestionContextRefs, suggestedCvssVector, '_cvss3_vector');

const { getAIBotTooltip } = useAegisMetadataTracking();
const { getAIBotSkipTooltip, getAIBotTooltip } = useAegisMetadataTracking();
const skipTooltip = computed(() => getAIBotSkipTooltip('_cvss3_vector'));

const {
canShowFeedbackExtended,
Expand Down Expand Up @@ -97,6 +98,7 @@ const suggestionTooltip = computed(() => {
:suggestions="allSuggestions"
:selectedIndex="selectedSuggestionIndex"
:tooltipText="suggestionTooltip"
:skipTooltipText="skipTooltip"
@suggest="suggestCvss"
@selectSuggestion="selectSuggestion"
@revert="revertCvss"
Expand Down
6 changes: 4 additions & 2 deletions src/components/Aegis/AegisCweActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const {
suggestCwe,
} = useAegisSuggestion(props.aegisContext as AegisSuggestionContextRefs, modelValue, 'cwe_id');

const { getAIBotTooltip } = useAegisMetadataTracking();
const { getAIBotSkipTooltip, getAIBotTooltip } = useAegisMetadataTracking();
const skipTooltip = computed(() => getAIBotSkipTooltip('cwe_id'));

const {
canShowFeedbackExtended,
Expand Down Expand Up @@ -93,7 +94,7 @@ onMounted(() => {

<template>
<AegisActions
v-if="osimRuntime.flags?.aiCweSuggestions === true || isCweAIBot"
v-if="osimRuntime.flags?.aiCweSuggestions === true || isCweAIBot || skipTooltip"
:canSuggest="canSuggest && !!osimRuntime.flags?.aiCweSuggestions"
:hasAppliedSuggestion="hasAppliedSuggestion"
:hasMultipleSuggestions="hasMultipleSuggestions"
Expand All @@ -103,6 +104,7 @@ onMounted(() => {
:suggestions="allSuggestions as string[]"
:selectedIndex="selectedSuggestionIndex"
:tooltipText="suggestionTooltip"
:skipTooltipText="skipTooltip"
@suggest="suggestCwe"
@selectSuggestion="selectSuggestion"
@revert="revertCwe"
Expand Down
4 changes: 3 additions & 1 deletion src/components/Aegis/AegisDescriptionActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ defineExpose({
isSuggesting,
});

const { getAIBotTooltip } = useAegisMetadataTracking();
const { getAIBotSkipTooltip, getAIBotTooltip } = useAegisMetadataTracking();
const skipTooltip = computed(() => getAIBotSkipTooltip('cve_description'));

const {
canShowFeedbackExtended: canShowDescriptionFeedbackExtended,
Expand Down Expand Up @@ -76,6 +77,7 @@ const suggestionTooltip = computed(() => {
:suggestions="[]"
:selectedIndex="0"
:tooltipText="suggestionTooltip"
:skipTooltipText="skipTooltip"
@suggest="suggestDescription"
@revert="revertDescription"
@feedback="handleDescriptionFeedback"
Expand Down
4 changes: 3 additions & 1 deletion src/components/Aegis/AegisImpactActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ const {
'impact',
);

const { getAIBotTooltip } = useAegisMetadataTracking();
const { getAIBotSkipTooltip, getAIBotTooltip } = useAegisMetadataTracking();
const skipTooltip = computed(() => getAIBotSkipTooltip('impact'));

const {
canShowFeedbackExtended,
Expand Down Expand Up @@ -98,6 +99,7 @@ const suggestionTooltip = computed(() => {
:suggestions="allSuggestions as string[]"
:selectedIndex="selectedSuggestionIndex"
:tooltipText="suggestionTooltip"
:skipTooltipText="skipTooltip"
@suggest="suggestImpact"
@selectSuggestion="selectSuggestion"
@revert="revertImpact"
Expand Down
4 changes: 3 additions & 1 deletion src/components/Aegis/AegisMitigationActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const {
'mitigation',
);

const { getAIBotTooltip, isFieldValueAIBot } = useAegisMetadataTracking();
const { getAIBotSkipTooltip, getAIBotTooltip, isFieldValueAIBot } = useAegisMetadataTracking();
const skipTooltip = computed(() => getAIBotSkipTooltip('mitigation'));
const isMitigationAIBot = computed(() => isFieldValueAIBot('mitigation', modelValue.value));

defineExpose({
Expand Down Expand Up @@ -74,6 +75,7 @@ const suggestionTooltip = computed(() => {
:suggestions="allSuggestions"
:selectedIndex="selectedSuggestionIndex"
:tooltipText="suggestionTooltip"
:skipTooltipText="skipTooltip"
@suggest="suggestMitigation"
@selectSuggestion="selectSuggestion"
@revert="revertMitigation"
Expand Down
6 changes: 4 additions & 2 deletions src/components/Aegis/AegisStatementActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ const {
'statement',
);

const { getAIBotTooltip } = useAegisMetadataTracking();
const { getAIBotSkipTooltip, getAIBotTooltip } = useAegisMetadataTracking();
const skipTooltip = computed(() => getAIBotSkipTooltip('statement'));

const {
canShowFeedbackExtended,
Expand Down Expand Up @@ -78,7 +79,7 @@ const suggestionTooltip = computed(() => {

<template>
<AegisActions
v-if="osimRuntime.flags?.aiStatementSuggestions || isStatementAIBot"
v-if="osimRuntime.flags?.aiStatementSuggestions || isStatementAIBot || skipTooltip"
:canSuggest="canSuggest && !!osimRuntime.flags?.aiStatementSuggestions"
:hasAppliedSuggestion="hasAppliedSuggestion"
:hasMultipleSuggestions="hasMultipleSuggestions"
Expand All @@ -88,6 +89,7 @@ const suggestionTooltip = computed(() => {
:suggestions="allSuggestions as string[]"
:selectedIndex="selectedSuggestionIndex"
:tooltipText="suggestionTooltip"
:skipTooltipText="skipTooltip"
@suggest="suggestStatement"
@selectSuggestion="selectSuggestion"
@revert="revertStatement"
Expand Down
6 changes: 4 additions & 2 deletions src/components/Aegis/AegisTitleActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ defineExpose({
isSuggesting,
});

const { getAIBotTooltip } = useAegisMetadataTracking();
const { getAIBotSkipTooltip, getAIBotTooltip } = useAegisMetadataTracking();
const skipTooltip = computed(() => getAIBotSkipTooltip('title'));

const {
canShowFeedbackExtended: canShowTitleFeedbackExtended,
Expand Down Expand Up @@ -66,7 +67,7 @@ const suggestionTooltip = computed(() => {

<template>
<AegisActions
v-if="osimRuntime.flags?.aiTitleSuggestions === true || isTitleAIBot"
v-if="osimRuntime.flags?.aiTitleSuggestions === true || isTitleAIBot || skipTooltip"
:canSuggest="canSuggest && !!osimRuntime.flags?.aiTitleSuggestions"
:hasAppliedSuggestion="hasAppliedTitleSuggestion"
:hasMultipleSuggestions="false"
Expand All @@ -76,6 +77,7 @@ const suggestionTooltip = computed(() => {
:suggestions="[]"
:selectedIndex="0"
:tooltipText="suggestionTooltip"
:skipTooltipText="skipTooltip"
@suggest="suggestTitle"
@revert="revertTitle"
@feedback="handleTitleFeedback"
Expand Down
88 changes: 88 additions & 0 deletions src/components/Aegis/__tests__/AegisActions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,94 @@ describe('aegisActions', () => {
expect(starIcon.classes()).toContain('disabled');
});

it.each(['click', 'Enter', ' '])('opens the skip warning using %j and closes it on blur', async (activation) => {
const wrapper = mountWithConfig(AegisActions, {
props: {
...feedbackProps,
canShowFeedback: false,
hasAppliedSuggestion: false,
skipTooltipText: 'Suggestion unavailable\nReason: confidence below threshold',
},
});

const warning = wrapper.get('.bi-exclamation-triangle');
expect(warning.classes()).toContain('text-warning');
expect(warning.attributes('role')).toBe('button');
expect(warning.attributes('tabindex')).toBe('0');
expect(warning.attributes('aria-label')).toContain('unavailable');
expect(wrapper.find('.bi-info-circle').exists()).toBe(false);
expect(wrapper.find('.aegis-tooltip').exists()).toBe(false);
expect(wrapper.find('.bi-arrow-counterclockwise').exists()).toBe(false);
expect(wrapper.find('.bi-hand-thumbs-up').exists()).toBe(false);

if (activation === 'click') await warning.trigger('click');
else await warning.trigger('keydown', { key: activation });

expect(wrapper.get('.aegis-tooltip').text()).toContain('Reason: confidence below threshold');
expect(wrapper.get('.aegis-tooltip').findAll('br')).toHaveLength(1);
expect(wrapper.emitted('suggest')).toBeUndefined();
expect(wrapper.emitted('feedback')).toBeUndefined();
await warning.trigger('blur');
expect(wrapper.find('.aegis-tooltip').exists()).toBe(false);
});

it('renders recorded reasons and tool names literally instead of interpreting markup', async () => {
const reason = '<img src=x onerror="alert(1)"> & <script>alert(2)</script>';
const tool = '<a href="javascript:alert(3)">tool</a>';
const wrapper = mountWithConfig(AegisActions, {
props: {
...feedbackProps,
canShowFeedback: false,
hasAppliedSuggestion: false,
skipTooltipText: `Suggestion unavailable\nReason: ${reason}\nTools used: ${tool}`,
},
});
await wrapper.get('.bi-exclamation-triangle').trigger('click');
const tooltip = wrapper.get('.aegis-tooltip');
expect(tooltip.text()).toContain(reason);
expect(tooltip.text()).toContain(tool);
expect(tooltip.find('img, script, a').exists()).toBe(false);
await wrapper.get('.bi-exclamation-triangle').trigger('click');
expect(wrapper.find('.aegis-tooltip').exists()).toBe(false);
});

it('replaces a historical warning with normal suggestion details when a manual suggestion is applied', async () => {
const wrapper = mountWithConfig(AegisActions, {
props: {
...feedbackProps,
hasAppliedSuggestion: false,
skipTooltipText: 'Historical skip reason',
tooltipText: 'Generated by Aegis-AI\nExplanation: New manual suggestion',
},
});
await wrapper.get('.bi-exclamation-triangle').trigger('click');
expect(wrapper.get('.aegis-tooltip').text()).toContain('Historical skip reason');
await wrapper.setProps({ hasAppliedSuggestion: true });
expect(wrapper.find('.bi-exclamation-triangle').exists()).toBe(false);
expect(wrapper.get('.bi-info-circle').attributes('aria-label')).toContain('suggestion details');
expect(wrapper.get('.aegis-tooltip').text()).toContain('New manual suggestion');
expect(wrapper.get('.aegis-tooltip').text()).not.toContain('Historical skip reason');
await wrapper.get('.bi-hand-thumbs-up').trigger('click');
expect(wrapper.emitted('feedback')).toEqual([['positive', '']]);
await wrapper.get('.bi-arrow-counterclockwise').trigger('click');
expect(wrapper.emitted('revert')).toHaveLength(1);
});

it('keeps generation available alongside a warning when enabled', async () => {
const wrapper = mountWithConfig(AegisActions, {
props: {
...feedbackProps,
canShowFeedback: false,
canSuggest: true,
hasAppliedSuggestion: false,
skipTooltipText: 'Suggestion unavailable',
},
});
expect(wrapper.find('.bi-exclamation-triangle').exists()).toBe(true);
await wrapper.get('.bi-stars').trigger('click');
expect(wrapper.emitted('suggest')).toHaveLength(1);
});

it('disables feedback buttons with a tooltip when the flaw is embargoed', async () => {
useFlaw().flaw.value.embargoed = true;

Expand Down
Loading
Loading