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
@usernameis 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.vuegainedhideUsernameandsingleLineprops, plus a default slot for the actor line.note_author.vuegained atruncateprop.- The rapid diffs header got
js-note-header-infoandjs-note-headline-metahooks.note_agent_actor_line.vuenow matches those, as well as the legacy.note-header-info/.note-headline-metaclasses. 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
- Issue: #628980 (closed)
- Follow-up to !256476 (merged) (work item comments)
- Backend task for the real field: #629909 (closed)
- Design spec: https://claude.ai/design/p/8aa78ff4-b554-4b06-9ee5-cc27a8bb030f?file=Agent+Presence+Standard+v3.dc.html
Screenshots or screen recordings 🦇
| Previously |
|---|
![]() |
Screen Recording Tests of sizing responsiveness
| Legacy MR | Rapid Diffs |
|---|---|
How to set up and validate locally
-
Check out the branch
628980-add-agent-presence-to-mr-comments. -
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. -
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 -
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.
-
Check both surfaces:
- The Overview tab (legacy notes).
- Diff comments on the Changes tab, with rapid diffs turned on by adding
?rapid_diffs=trueto the/diffsURL. To compare,?rapid_diffs_disabled=truegives 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.
-
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; PATCHWith it applied, Session is disabled with the tooltip "GitLab Duo Agent Platform is not available."
-
With the flag off, every comment looks the same as on master, and the "View session" button still shows where it did before.
-
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.












