fix(toolbar): watch active ref for always-mounted views (AgentsView, ViewerView)

Views mounted via v-if+CSS-hide never unmount, so onMounted/onUnmounted only fire
once. When a RouterView (Tests) unmounts it clears _config, and the always-mounted
view never restores it. Fix: provideToolbar accepts optional active ref and uses
watch() instead of lifecycle hooks for those views.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nico 2026-04-03 23:09:39 +02:00
parent 9d0012edad
commit d48612665b
3 changed files with 25 additions and 15 deletions

View File

@ -9,7 +9,7 @@
* Views that don't provide toolbar shows nothing (login, home). * Views that don't provide toolbar shows nothing (login, home).
*/ */
import { onMounted, onUnmounted, ref, type Ref } from 'vue'; import { onMounted, onUnmounted, ref, watch, type Ref } from 'vue';
// --- Types --- // --- Types ---
@ -35,16 +35,24 @@ export interface ToolbarConfig {
const _config = ref<ToolbarConfig | null>(null); const _config = ref<ToolbarConfig | null>(null);
let _mountVersion = 0; let _mountVersion = 0;
export function provideToolbar(config: ToolbarConfig) { /**
* Register toolbar config for the active view.
*
* Pass `active` for always-mounted views (v-if + CSS hide, e.g. AgentsView, ViewerView).
* Omit `active` for route-mounted views (RouterView) uses onMounted/onUnmounted.
*/
export function provideToolbar(config: ToolbarConfig, active?: Ref<boolean>) {
let myVersion = 0; let myVersion = 0;
onMounted(() => {
myVersion = ++_mountVersion; function activate() { myVersion = ++_mountVersion; _config.value = config; }
_config.value = config; function deactivate() { if (myVersion === _mountVersion) _config.value = null; }
});
onUnmounted(() => { if (active !== undefined) {
// Only clear if no newer component has taken ownership since we mounted. watch(active, (isActive) => { isActive ? activate() : deactivate(); }, { immediate: true });
if (myVersion === _mountVersion) _config.value = null; } else {
}); onMounted(activate);
onUnmounted(deactivate);
}
} }
export function injectToolbar(): Ref<ToolbarConfig | null> { export function injectToolbar(): Ref<ToolbarConfig | null> {

View File

@ -98,7 +98,8 @@ const { connected, send: wsSend } = ws;
const { isLoggedIn } = auth; const { isLoggedIn } = auth;
const { selectedAgent, selectedMode, filteredAgents, defaultAgent, allAgents } = agents; const { selectedAgent, selectedMode, filteredAgents, defaultAgent, allAgents } = agents;
// Toolbar wrap chatStore state into ToolbarConnection shape // Toolbar always-mounted view: pass viewActive so config tracks route, not lifecycle
const viewActive = computed(() => agentsRoute.name === 'nyx');
const chatStore = useChatStore(); const chatStore = useChatStore();
provideToolbar({ provideToolbar({
groups: ['quad-view', 'themes', 'panels'], groups: ['quad-view', 'themes', 'panels'],
@ -119,7 +120,7 @@ provideToolbar({
Mode: selectedMode.value, Mode: selectedMode.value,
}), }),
}, },
}); }, viewActive);
// Agent picker disabled for now (auto-select default agent) // Agent picker disabled for now (auto-select default agent)
// TODO: re-enable when multi-agent UX is designed // TODO: re-enable when multi-agent UX is designed
@ -160,8 +161,6 @@ watch(
{ immediate: true }, { immediate: true },
); );
const viewActive = computed(() => agentsRoute.name === 'nyx');
// HUD state from ChatPane (for debug panels) // HUD state from ChatPane (for debug panels)
const hudState = ref({ hudTree: [] as any[], hudVersion: 0, connected: false }); const hudState = ref({ hudTree: [] as any[], hudVersion: 0, connected: false });
function onHudUpdate(payload: { hudTree: any[]; hudVersion: number; connected: boolean }) { function onHudUpdate(payload: { hudTree: any[]; hudVersion: number; connected: boolean }) {

View File

@ -88,8 +88,11 @@ import { useViewer } from '../composables/useViewer';
import { provideToolbar } from '../composables/useToolbar'; import { provideToolbar } from '../composables/useToolbar';
import { computed } from 'vue'; import { computed } from 'vue';
provideToolbar({ groups: ['themes'] }); import { useRoute } from 'vue-router';
import { getApiBase } from '../utils/apiBase'; import { getApiBase } from '../utils/apiBase';
const _route = useRoute();
const _viewActive = computed(() => _route.name === 'viewer');
provideToolbar({ groups: ['themes'] }, _viewActive);
const { const {
fstoken, viewerRoots, currentPath, fstoken, viewerRoots, currentPath,