Feat: Add the agent presence actor line to merge request comments

What does this MR do and why?

This brings the agent actor line from work item comments (!256476 (merged)) to merge request comments. It covers both the legacy MR notes app (app/assets/javascripts/notes/components/noteable_note.vue) and rapid diffs (app/assets/javascripts/rapid_diffs/app/discussions/noteable_note.vue). Both reuse the existing note_agent_actor_line.vue component.

Everything is behind the default-off agent_presence_consolidation flag. With the flag off, nothing changes: the note header looks the same and the existing "View session" button stays.

With the flag on, and when a note has agent presence data:

  • The raw service account @username is hidden.
  • A · separator and a "Session by [Agent] on behalf of [User]" line appear in the header.
  • The header stays on one row. The line drops the agent name, then the initiator, as space runs out, and only wraps if even "Session" doesn't fit.
  • The standalone "View session" button is removed, because the line already links to the session.

This MR uses mock data.

Why mock data, and what the mock assumes

The backend doesn't send agent_presence on REST notes yet; that work is tracked in #629909 (closed). The frontend assumes a snake_case version of the GraphQL agentPresence shape from the first MR: agent_presence: { session_id, agent_name, agent_catalog_web_path, initiator_type, initiator: { label, web_path }, user_permissions: { read_duo_workflow } }, and converts it to camelCase. If the backend ends up with a different shape, only the agentPresence computed property in each of the two note components needs to change.

Rapid diffs needed its own set of changes, since it has its own note header rather than sharing the legacy one.

What changed in rapid diffs
  • note_header.vue gained hideUsername and singleLine props, plus a default slot for the actor line.
  • note_author.vue gained a truncate prop.
  • The rapid diffs header got js-note-header-info and js-note-headline-meta hooks. note_agent_actor_line.vue now matches those, as well as the legacy .note-header-info / .note-headline-meta classes. It doesn't reuse the legacy classes directly, because those bring in notes-page styles that would restyle the rapid diffs header.
  • In single-line mode, the rapid diffs header grows to fill its row, the same way the legacy header does. Without this, the note actions would wrap below it, and the line wouldn't grow back when the window widens.

The feature flag is now pushed on the MR diffs action as well as show, because rapid diffs is served by diffs. Without that, the line could never render there.

Validating this MR is tidier than the first one: there's one patch instead of one per state.

Why validation is simpler here

The patch mocks the field in the backend serializer (ee/app/serializers/ee/note_entity.rb), which feeds both the legacy and rapid diffs notes. You choose a state by typing a keyword in any comment, so every state can sit side by side on one MR. You don't need an agent service account, and you don't need to restart anything, because Rails reloads the serializer in development.

References

Screenshots or screen recordings 🦇

Previously
Screenshot_2026-10-01_at_3.01.00_PM
Legacy MR notes Rapid diffs
Started by a person (Session, agent, and behalf of ref is clickable) Screenshot_2026-10-02_at_12.01.17_PM Screenshot_2026-10-02_at_11.52.41_AM
Trigger known (Session, agent, and triggered ref of are clickable) Screenshot_2026-10-02_at_12.01.26_PM Screenshot_2026-10-02_at_11.52.54_AM
Trigger unknown (Session are agent are clickable, triggered by is not) Screenshot_2026-10-02_at_12.01.35_PM Screenshot_2026-10-02_at_11.53.05_AM
No catalog (Session and behalf of are clickable, agent is not Screenshot_2026-10-02_at_12.01.43_PM Screenshot_2026-10-02_at_11.53.19_AM
Can't view session - no access Screenshot_2026-10-02_at_12.01.53_PM Screenshot_2026-10-02_at_11.53.34_AM
No DAP Access Screenshot_2026-10-02_at_12.06.04_PM Screenshot_2026-10-02_at_12.05.27_PM

Screen Recording Tests of sizing responsiveness

Legacy MR Rapid Diffs

How to set up and validate locally

  1. Check out the branch 628980-add-agent-presence-to-mr-comments.

  2. In the Rails console, enable the flag for a top-level group (not a subgroup):

    Feature.enable(:agent_presence_consolidation, Group.find_by_full_path('<top-level-group>'))

    To check rapid diffs, also enable Feature.enable(:rapid_diffs_on_mr_show). Although, I believe the toggle works here too on the diffs page.

  3. Apply the mock patch. It's safe to run more than once.

    Copy and paste this block
    cd "$(git rev-parse --show-toplevel)" &&
    git checkout HEAD -- ee/app/serializers/ee/note_entity.rb &&
    git apply <<'PATCH'
    diff --git a/ee/app/serializers/ee/note_entity.rb b/ee/app/serializers/ee/note_entity.rb
    index e022958529f2..b500b735dd84 100644
    --- a/ee/app/serializers/ee/note_entity.rb
    +++ b/ee/app/serializers/ee/note_entity.rb
    @@ -28,6 +28,42 @@ module NoteEntity
             authorized_duo_session_workflow.id
           end
     
    +      # REVIEW-ONLY MOCK, not part of the merge request. Stands in for agent_presence until
    +      # https://gitlab.com/gitlab-org/gitlab/-/work_items/629909 lands. Put [mock:<state>] in the
    +      # comment that tags the agent; the agent's reply in that thread gets the presence.
    +      expose :agent_presence, if: ->(note, _) { mock_agent_presence_tag(note) } do |note|
    +        tag = mock_agent_presence_tag(note)
    +        state = mock_agent_presence_state(tag.note)
    +        workflow = note.duo_metadata&.workflow || note.duo_created_workflow_link&.workflow ||
    +          ::Ai::DuoWorkflows::Workflow.for_project(note.project).last
    +        person = {
    +          label: "@#{tag.author.username}", web_path: "/#{tag.author.username}",
    +          user_id: tag.author.to_global_id.to_s, username: tag.author.username
    +        }
    +        presence = {
    +          session_id: workflow&.to_global_id&.to_s || 'gid://gitlab/Ai::DuoWorkflows::Workflow/1',
    +          agent_name: 'Developer Agent',
    +          agent_catalog_web_path: '/explore/ai-catalog/flows/32',
    +          initiator_type: 'USER',
    +          initiator: person,
    +          user_permissions: { read_duo_workflow: true }
    +        }
    +
    +        case state
    +        when 'trigger'
    +          presence.merge(initiator_type: 'TRIGGER',
    +            initiator: { label: 'scheduled scan', web_path: '/-/security/policies' })
    +        when 'automation'
    +          presence.merge(initiator_type: 'TRIGGER', initiator: { label: 'an automation', web_path: '' })
    +        when 'no_catalog'
    +          presence.merge(agent_catalog_web_path: nil)
    +        when 'no_access'
    +          presence.merge(user_permissions: { read_duo_workflow: false })
    +        else
    +          presence
    +        end
    +      end
    +
           expose :duo_session_url, if: ->(note, _) { !note.system? } do |note|
             note.duo_metadata&.workflow&.web_url
           end
    @@ -90,6 +126,18 @@ def authorized_duo_session_workflow
           end
           strong_memoize_attr :authorized_duo_session_workflow
     
    +      # REVIEW-ONLY MOCK: only agent-posted notes get presence, from the comment in the thread
    +      # that tags the agent with a keyword. The rich text editor saves the brackets escaped.
    +      def mock_agent_presence_tag(note)
    +        return unless note.author&.bot?
    +
    +        @mock_agent_presence_tag ||= note.discussion.notes.find { |n| mock_agent_presence_state(n.note) }
    +      end
    +
    +      def mock_agent_presence_state(text)
    +        text.to_s[/\\?\[mock:(\w+)\\?\]/, 1]
    +      end
    +
           def readable_duo_workflow(workflow)
             workflow if workflow && can?(current_user, :read_duo_workflow, workflow)
           end
    PATCH
  4. Go to an MR in a project under the enabled group. Write a comment that @-mentions an agent (for example @duo-developer-<group>) and includes a keyword. The keyword can go anywhere in the comment. The actor line appears on the agent's reply in that thread, not on your comment.

    Keyword What the agent's reply should show
    [mock:person] "Session by Developer Agent on behalf of @you". The agent name links to its AI Catalog page, and the initiator links to your profile. Hovering the initiator shows your user popover.
    [mock:trigger] "Session by Developer Agent triggered by scheduled scan", with the trigger linked.
    [mock:automation] "Session by Developer Agent triggered by an automation", as plain text with no link.
    [mock:no_catalog] The agent name is plain text with no link.
    [mock:no_access] The Session label is disabled, with the tooltip "You do not have access to this session."
    No keyword in the thread The agent's reply looks unchanged.

    Note:

    • Only notes posted by an agent or service account get the line.
    • "On behalf of" is whoever wrote the comment with the keyword.
    • "Session" opens the session that posted the reply. If there isn't one, it opens the latest Duo session in the project, or uses a placeholder ID.
    • The rich text editor saving the brackets as \[mock:…\] is fine.
  5. Check both surfaces:

    • The Overview tab (legacy notes).
    • Diff comments on the Changes tab, with rapid diffs turned on by adding ?rapid_diffs=true to the /diffs URL. To compare, ?rapid_diffs_disabled=true gives legacy diffs.

    On each, resize the window narrower and then wider. The line should drop the agent name, then the initiator, then wrap onto its own row. Widening should restore it. The note actions should stay on the header row.

  6. Optional: the "GitLab Duo Agent Platform is not available" state. This one is decided in the frontend, so it has its own small patch.

    Copy and paste this block
    cd "$(git rev-parse --show-toplevel)" &&
    git checkout HEAD -- ee/app/assets/javascripts/ai/shared/widgets/agent_actor_line.vue &&
    git apply <<'PATCH'
    diff --git a/ee/app/assets/javascripts/ai/shared/widgets/agent_actor_line.vue b/ee/app/assets/javascripts/ai/shared/widgets/agent_actor_line.vue
    index f30590fb1604..9f66c2dc373e 100644
    --- a/ee/app/assets/javascripts/ai/shared/widgets/agent_actor_line.vue
    +++ b/ee/app/assets/javascripts/ai/shared/widgets/agent_actor_line.vue
    @@ -66,7 +66,7 @@ export default {
         duoChatAvailable: {
           query: duoChatAvailableQuery,
           update(data) {
    -        return data?.currentUser?.duoChatAvailable ?? false;
    +        return false; // REVIEW-ONLY MOCK: stands in for a viewer without GitLab Duo
           },
           error() {
             this.duoChatAvailable = false;
    PATCH

    With it applied, Session is disabled with the tooltip "GitLab Duo Agent Platform is not available."

  7. With the flag off, every comment looks the same as on master, and the "View session" button still shows where it did before.

  8. Clean up.

    Copy and paste this block
    cd "$(git rev-parse --show-toplevel)" &&
    git checkout HEAD -- ee/app/serializers/ee/note_entity.rb \
      ee/app/assets/javascripts/ai/shared/widgets/agent_actor_line.vue

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Lindsey Shelton

Merge request reports

Loading
Loading