Add a tabbed session inbox to the Duo side panel behind a feature flag

Problem

The panel session list is reverse chronological, so a session waiting on your approval sits between two finished ones and is distinguished only by a status icon. Search and sort help you find a session you already know about; they do not help you notice the one that needs you.

Proposal

Add ee/app/assets/javascripts/ai/duo_agents_platform/panel/agent_session_inbox.vue, used only by the panel router. It renders the search bar from #616492 unchanged, then a GlTabs header with Needs a decision and All, then the list. The existing agent_flow_list.vue rows stay for now; the row redesign is a separate issue.

One query serves both tabs, as two aliased connections on duoWorkflowWorkflows:

needsDecision: duoWorkflowWorkflows(
  type: "non_foundational_chat_agents"
  statusGroup: AWAITING_INPUT
  first: $first
  search: $search
  sort: $sort
) {
  pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
  edges { node { ...FlowFragment } }
}
all: duoWorkflowWorkflows(
  type: "non_foundational_chat_agents"
  first: $first
  after: $after
  search: $search
  sort: $sort
) {
  pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
  edges { node { ...FlowFragment } }
}

Both aliases must pass type: "non_foundational_chat_agents", which is what the panel's current getUserAgentFlows query passes. Dropping it would pull chat threads into the session list: Ai::DuoWorkflows::WorkflowsFinder maps that type to without_workflow_definition(Ai::FoundationalChatAgent.workflow_definitions), which excludes chat and agentic_chat/v1 along with the other foundational chat agents. Chat sessions are the single largest definition group in a typical database, so this is not a rounding error.

Both aliases share the search and sort variables, so the unchanged search bar filters both tabs consistently, and one query keeps the existing single 10s poll rather than doubling it.

Tab counts come from what is loaded rather than a server aggregate: edges.length for that connection, rendered as 20+ when its hasNextPage is true. Page size is 20 via DEFAULT_AGENT_PLATFORM_PAGINATION_VARIABLES. No backend change.

The decision tab's empty state is its own state, not the generic "no sessions" one: "Nothing needs you right now", with "Your agents are grinding away. The moment one of them wants a decision, it lands here." and a See what's running button that switches to the All tab.

Acceptance criteria

  • Panel sessions route renders the new component when duo_panel_session_inbox is enabled, and the current component when it is not.
  • Needs a decision shows sessions in statusGroup: AWAITING_INPUT; All shows every non-chat session.
  • Neither tab lists Duo Chat threads: both aliases pass type: "non_foundational_chat_agents", matching the current panel query. A spec asserts the variable is sent.
  • Both tab labels carry a count badge showing n, or 20+ when that connection has a next page.
  • Typing in the search bar or changing the sort updates both tabs.
  • Empty decision tab shows the dedicated empty state and its button switches to All.
  • Feature flag duo_panel_session_inbox (type: wip, default_enabled: false, actor current_user) is pushed from ee/lib/ee/gitlab/gon_helper.rb.

Technical notes

AWAITING_INPUT is input_required, plan_approval_required and tool_call_approval_required per Ai::DuoWorkflows::Workflow::GROUPED_STATUSES. Paused and failed sessions stay in All.

non_foundational_chat_agents excludes every foundational chat agent, not only Duo Chat, so agents like duo_planner/v1 and orbit_agent are filtered out too. That is what the panel does today, so keep it and treat any change to the definition set as a separate decision.

Keep the edges { node } shape. A second shape (nodes) for the same field and args collides in the Apollo cache. There is no custom keyArgs for duoWorkflowWorkflows, so each alias caches under its own args — assert that in the spec rather than assuming it. Cursors are per alias, so each tab needs its own after/before variables if both paginate.

Out of scope

Row layout, per-row action buttons, and read state.

Edited by Andrew Fontaine