Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,35 @@ import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from
* Custom RouteReuseStrategy that reuses component instances when navigating
* between routes that use the same component.
*
* Only applies when the route path ends with /editor. Otherwise defaults to
* Applies when the route path ends with /editor, or when navigating between the
* clients shell routes (workspaces → environments → chat). Editor/config/deployments
* panels use the editor or default rules. Otherwise defaults to
* Angular's default strategy (no reuse, standard route config matching).
*/
export class ComponentReuseStrategy implements RouteReuseStrategy {
private storedRoutes = new Map<string, DetachedRouteHandle>();

/**
* Returns true when the route is a leaf under the `clients` parent with a component.
*/
private isUnderClientsRoute(route: ActivatedRouteSnapshot): boolean {
return route.pathFromRoot.some((segment) => segment.routeConfig?.path === 'clients');
}

/**
* Main clients shell routes (workspaces list → workspace → environment/chat).
* Excludes editor/config/deployments so those keep distinct route-config semantics.
*/
private isClientsShellRoute(route: ActivatedRouteSnapshot): boolean {
if (!route.component || !this.isUnderClientsRoute(route)) {
return false;
}

const path = route.routeConfig?.path ?? '';

return path === '' || path === ':clientId' || path === ':clientId/agents/:agentId';
}

/**
* Returns true if the route path ends with /editor, meaning the custom reuse strategy applies.
*/
Expand Down Expand Up @@ -75,6 +98,14 @@ export class ComponentReuseStrategy implements RouteReuseStrategy {
* For editor routes: reuses when components are identical AND route paths match.
*/
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
const futureIsShell = this.isClientsShellRoute(future);
const currIsShell = this.isClientsShellRoute(curr);

// Workspaces / environments / chat: reuse shell across param-only navigation
if (futureIsShell && currIsShell && future.component && future.component === curr.component) {
return true;
}

const futureIsEditor = this.isEditorRoute(future);
const currIsEditor = this.isEditorRoute(curr);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('agentsReducer', () => {
});

describe('loadClientAgents', () => {
it('should set loading to true for the client, clear existing agents, and clear error', () => {
it('should set loading to true for the client, preserve existing agents, and clear error', () => {
const state: AgentsState = {
...initialAgentsState,
entities: { [clientId]: [mockAgent] },
Expand All @@ -75,7 +75,7 @@ describe('agentsReducer', () => {
const newState = agentsReducer(state, loadClientAgents({ clientId, params: {} }));

expect(newState.loading[clientId]).toBe(true);
expect(newState.entities[clientId]).toEqual([]);
expect(newState.entities[clientId]).toEqual([mockAgent]);
expect(newState.errors[clientId]).toBeNull();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ export const agentsReducer = createReducer(
on(loadClientAgents, (state, { clientId }) =>
updateClientState(state, clientId, (clientState) => ({
...clientState,
agents: [], // Clear existing agents when starting new load
loading: true,
error: null,
})),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('clientsReducer', () => {
});

describe('loadClients', () => {
it('should set loading to true, clear existing clients, and clear error', () => {
it('should set loading to true, preserve existing clients, and clear error', () => {
const state: ClientsState = {
...initialClientsState,
entities: [mockClient],
Expand All @@ -80,7 +80,7 @@ describe('clientsReducer', () => {
const newState = clientsReducer(state, loadClients({}));

expect(newState.loading).toBe(true);
expect(newState.entities).toEqual([]);
expect(newState.entities).toEqual([mockClient]);
expect(newState.error).toBeNull();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export const clientsReducer = createReducer(
// Load Clients
on(loadClients, (state) => ({
...state,
entities: [], // Clear existing clients when starting new load
loading: true,
error: null,
})),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,10 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
);
readonly activeClientId$: Observable<string | null> = this.clientsFacade.activeClientId$;
readonly activeClient$: Observable<ClientResponseDto | null> = this.clientsFacade.activeClient$;
readonly clientsLoading$: Observable<boolean> = this.clientsFacade.loading$;
readonly clientsLoading$: Observable<boolean> = combineLatest([
this.clientsFacade.loading$,
this.clientsFacade.clients$,
]).pipe(map(([loading, clients]) => loading && clients.length === 0));
readonly clientsError$: Observable<string | null> = this.clientsFacade.error$;
readonly clientsDeleting$: Observable<boolean> = this.clientsFacade.deleting$;
readonly clientsCreating$: Observable<boolean> = this.clientsFacade.creating$;
Expand Down Expand Up @@ -300,7 +303,10 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
return of(false);
}

return this.agentsFacade.getClientAgentsLoading$(clientId);
return combineLatest([
this.agentsFacade.getClientAgentsLoading$(clientId),
this.agentsFacade.getClientAgents$(clientId),
]).pipe(map(([loading, agents]) => loading && agents.length === 0));
}),
);
readonly agentsDeleting$: Observable<boolean> = this.activeClientId$.pipe(
Expand Down Expand Up @@ -1229,8 +1235,19 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
}
});

// Load clients on init
this.clientsFacade.loadClients();
// Load clients on init only when not already cached (avoids spinner on route reuse)
this.clientsFacade.hasClients$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe((hasClients) => {
if (!hasClients) {
this.clientsFacade.loadClients();
}
});

// Sync local active client from store when component is recreated
this.activeClientId$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe((clientId) => {
if (clientId) {
this.activeClientId = clientId;
}
});

// Load provider model catalog when client + selected agent are set (includes deep-link and reducer-driven selection).
combineLatest([this.activeClientId$, this.selectedAgent$])
Expand Down Expand Up @@ -1599,7 +1616,7 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
this.selectedAgentId.set(null);
// Update active client
this.activeClientId = clientId;
this.agentsFacade.loadClientAgents(clientId);
this.loadClientAgentsIfNeeded(clientId);

// Clear search agent query
if (this.searchAgentQuery()) {
Expand Down Expand Up @@ -1974,6 +1991,17 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
}
}

private loadClientAgentsIfNeeded(clientId: string): void {
this.agentsFacade
.hasClientAgents$(clientId)
.pipe(take(1), takeUntilDestroyed(this.destroyRef))
.subscribe((hasAgents) => {
if (!hasAgents) {
this.agentsFacade.loadClientAgents(clientId);
}
});
}

onClientSelect(clientId: string, navigate = true): void {
// Use local state for immediate check to avoid race conditions
const currentActiveClientId = this.activeClientId;
Expand Down Expand Up @@ -2034,7 +2062,7 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
// This ensures agents are loaded even if the subscription doesn't fire due to timing
if (this.activeClientId !== clientId) {
this.activeClientId = clientId;
this.agentsFacade.loadClientAgents(clientId);
this.loadClientAgentsIfNeeded(clientId);
// Ensure socket is connected before setting client
this.ensureSocketConnectedAndSetClient(clientId);
}
Expand Down
Loading