Display a Goal badge in the user message

Problem

A mockup puts a "Goal" badge on the chat bubble that shows the user's own /goal message. Until recently that bubble was rendered by a closed, hardcoded renderer in GitLab's chat code, with no way to inject anything into it.

Proposal

duo-ui duo-ui!695 (merged) (merged) now routes user messages through duo-ui's message registry, so a messageWidgets descriptor can claim them. duo-ui duo-ui!696 (merged) (open, needs retarget to main) adds the header/content slots this badge needs, and is the prerequisite this issue is blocked on.

  • Contribute a messageWidgets entry that wraps duo-ui's UserMessage and fills its header slot with the badge, instead of rebuilding the bubble.
  • Mark goal messages with message_sub_type: 'goal' from a messageTransformer — or reuse the backend's value if it already sets one.
  • Strip the /goal token from the rendered body; the badge replaces it visually.
  • Cover both the optimistic local message and messages replayed from checkpoint/history, and mirror the wiring in duo_agentic_chat_state_manager_light.vue.

Technical notes

The epic's UX note names the work-item-objective icon. The usage example in duo-ui merge request duo-ui!696 (merged) uses issue-type-objective instead. Both exist in @gitlab/svgs. Use the epic's work-item-objective and flag the mismatch for UX to confirm.

Acceptance criteria

Prerequisites

  • duo-ui duo-ui!696 (merged) retargeted to main, merged, and released; then bump @gitlab/duo-ui in package.json past that release. !254241 (the open bump to ^16.3.0) only carries duo-ui!695 (merged).
  • duo-ui duo-ui!695 (merged) having merged is what makes this possible at all: user messages now route through duo-ui's message registry, so a messageWidgets descriptor can claim them.

Work

  • The goal plugin contributes a messageWidgets entry whose matchMessage claims the goal-bearing user message and whose component is a thin GitLab-side wrapper around duo-ui's newly exported UserMessage, filling its header slot with the badge. Do not rebuild the bubble — the header slot exists so the markdown pipeline, bubble styling, attribution label, and context references stay with duo-ui.
  • Mark the message with message_sub_type: 'goal' (the field the duo-ui MR's example matches on) from a messageTransformer in the same plugin, following plugins/compact/transformers/drop_noop_compaction.js. Transformers run in transformedMessages and must be pure — keep the detection there rather than in matchMessage, which duo-ui calls for every renderer against every message. If the backend already sets message_sub_type, prefer that and drop the transformer.
  • The /goal token is not rendered in the message body — the badge replaces it.
  • Correct for the optimistic local message (addDuoChatMessage({ content, role: 'user' })) and for messages replayed from checkpoint/history (transformChatMessages, normalizeDuoMessages), so the badge survives a reload.
  • duo_agentic_chat_state_manager_light.vue gets the same treatment — it keeps its own copy of the log and its own messageRenderers.
  • Add Jest coverage for the transformer, the widget descriptor, and the wrapper.

References

Part of epic &23107.

Edited by 🤖 GitLab Bot 🤖