Derive Duo chat panel view and heading from the route
What does this MR do and why?
The Duo chat panel duplicated the state that decides which sub-view is shown. DuoAgenticChat kept a private multithreadedView data field (CHAT vs LIST) that was mutated imperatively, while the router separately owned the same concept. The two drifted, which is the root cause of the rail/body desync reported in issue 601680.
Concretely, on a full page reload the persisted history stack is replayed as [history_route, show_route]. The first push mounts the component at mode: 'history' (view LIST) and kicks off a load; the second push fires the mode watcher, but switchMode('active') early-returned on its isLoading guard, so the view stayed LIST while the route said show. The result: the rail highlighted Active Chat, the body rendered the history list, and clicking a history thread was a dead no-op (pushing the already-current route throws NavigationDuplicated, so the mode prop never changed and the watcher never fired).
This MR deletes the drift class rather than guarding one instance of it, by making the route the single source of truth:
- Route parity —
onBackToListnow pushes the history route, so every view transition is expressed as a navigation. - Derive the view —
multithreadedViewbecomes a computed off$route.name, mirroring the existingROUTE_TO_TABmapping. The data field and all six imperative writes are removed, including a non-obvious one viaObject.assign(this, resetThreadContent()).thread_utils.jsexisted only to carry that key and is deleted. - Header title — the panel heading had the same inversion in
content_container.vue: the route-derived title was last in precedence behind a child-supplied override. After a reload restoring the history view, the hydrating workflow pushed a stale conversation title once the route had already settled, so the heading showed the last conversation instead of "History". A child title now only applies on routes that show a single conversation.
Intentional behaviour change
connectToStream no longer forces the CHAT view when a stream is already active on mount. The route wins instead. If you reload while on the history list with a live stream, you stay on the history list rather than being pulled into the conversation; the stream continues regardless. Forcing CHAT irrespective of the route was itself an instance of the drift being removed.
This is the only user-visible change beyond the bug fixes, and it is worth a specific look during review.
Why no changelog trailer
The affected paths are reached through duo_panel_history_stack_persistence, which is default_enabled: false, so there is no user-visible surface on production today. This work is a prerequisite for default-enabling that flag.
References
Screenshots or screen recordings
How to set up and validate locally
- Enable the flag in a rails console:
Feature.enable(:duo_panel_history_stack_persistence) - Open GitLab Duo Chat from any page that hosts the panel and create two or three conversations.
- Open the History tab and click a conversation. Confirm the rail shows Active Chat and the body shows the transcript.
- Reload the page. Expected: the body still shows the conversation, the rail and heading agree, and clicking a history thread opens it.
- Switch to the History tab and reload. Expected: the history list is restored and the heading reads "History", not the last conversation title.
- Confirm back navigation after a reload still returns to the history list, which is the behaviour the feature flag exists to provide.
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.