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).
*/
import { onMounted, onUnmounted, ref, type Ref } from 'vue';
import { onMounted, onUnmounted, ref, watch, type Ref } from 'vue';
// --- Types ---
@ -35,16 +35,24 @@ export interface ToolbarConfig {
const _config = ref<ToolbarConfig | null>(null);
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;
onMounted(() => {
myVersion = ++_mountVersion;
_config.value = config;
});
onUnmounted(() => {
// Only clear if no newer component has taken ownership since we mounted.
if (myVersion === _mountVersion) _config.value = null;
});
function activate() { myVersion = ++_mountVersion; _config.value = config; }
function deactivate() { if (myVersion === _mountVersion) _config.value = null; }
if (active !== undefined) {
watch(active, (isActive) => { isActive ? activate() : deactivate(); }, { immediate: true });
} else {
onMounted(activate);
onUnmounted(deactivate);
}
}
export function injectToolbar(): Ref<ToolbarConfig | null> {

View File

@ -98,7 +98,8 @@ const { connected, send: wsSend } = ws;
const { isLoggedIn } = auth;
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();
provideToolbar({
groups: ['quad-view', 'themes', 'panels'],
@ -119,7 +120,7 @@ provideToolbar({
Mode: selectedMode.value,
}),
},
});
}, viewActive);
// Agent picker disabled for now (auto-select default agent)
// TODO: re-enable when multi-agent UX is designed
@ -160,8 +161,6 @@ watch(
{ immediate: true },
);
const viewActive = computed(() => agentsRoute.name === 'nyx');
// HUD state from ChatPane (for debug panels)
const hudState = ref({ hudTree: [] as any[], hudVersion: 0, connected: false });
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 { computed } from 'vue';
provideToolbar({ groups: ['themes'] });
import { useRoute } from 'vue-router';
import { getApiBase } from '../utils/apiBase';
const _route = useRoute();
const _viewActive = computed(() => _route.name === 'viewer');
provideToolbar({ groups: ['themes'] }, _viewActive);
const {
fstoken, viewerRoots, currentPath,