From c3f06a8fda6051cea05c0a5301b353a65db7cd29 Mon Sep 17 00:00:00 2001 From: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com> Date: Sat, 20 Jun 2026 02:08:37 +0200 Subject: [PATCH] fix(desktop): refresh profile rail after deletion (#49289) --- apps/desktop/src/app/profiles/index.tsx | 5 ++-- apps/desktop/src/store/profile.test.ts | 35 ++++++++++++++++++++++++- apps/desktop/src/store/profile.ts | 10 +++++-- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/profiles/index.tsx b/apps/desktop/src/app/profiles/index.tsx index 3e44f7fd912..df5b58751a8 100644 --- a/apps/desktop/src/app/profiles/index.tsx +++ b/apps/desktop/src/app/profiles/index.tsx @@ -19,7 +19,6 @@ import { Textarea } from '@/components/ui/textarea' import { createProfile, deleteProfile, - getProfiles, getProfileSoul, type ProfileInfo, renameProfile, @@ -31,7 +30,7 @@ import { profileColorSoft, resolveProfileColor } from '@/lib/profile-color' import { slug } from '@/lib/sanitize' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' -import { $profileColors } from '@/store/profile' +import { $profileColors, refreshProfiles } from '@/store/profile' import { useRefreshHotkey } from '../hooks/use-refresh-hotkey' import { @@ -72,7 +71,7 @@ export function ProfilesView({ onClose }: ProfilesViewProps) { const refresh = useCallback(async () => { try { - const { profiles: list } = await getProfiles() + const list = await refreshProfiles() setProfiles(list) setSelectedName(current => { if (current && list.some(p => p.name === current)) { diff --git a/apps/desktop/src/store/profile.test.ts b/apps/desktop/src/store/profile.test.ts index 14edeb5c050..1306139151f 100644 --- a/apps/desktop/src/store/profile.test.ts +++ b/apps/desktop/src/store/profile.test.ts @@ -2,6 +2,7 @@ import { atom } from 'nanostores' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { HermesConnection } from '@/global' +import type { ProfileInfo } from '@/types/hermes' // Keep profile.ts's side-effecting imports inert: the gateway socket layer and // the REST query client must not run for real in a unit test. @@ -17,9 +18,20 @@ vi.mock('@/hermes', () => ({ vi.mock('@/lib/query-client', () => ({ queryClient: { invalidateQueries: vi.fn() } })) vi.mock('@/store/starmap', () => ({ resetStarmapGraph })) -const { $activeGatewayProfile, ensureGatewayProfile } = await import('./profile') +const { $activeGatewayProfile, $profiles, ensureGatewayProfile, refreshProfiles } = await import('./profile') const { $connection } = await import('./session') const { queryClient } = await import('@/lib/query-client') +const { getProfiles } = await import('@/hermes') + +const profile = (name: string, isDefault = false): ProfileInfo => ({ + has_env: false, + is_default: isDefault, + model: null, + name, + path: `/tmp/hermes/${name}`, + provider: null, + skill_count: 0 +}) const remoteConn = (over: Partial = {}): HermesConnection => ({ baseUrl: 'https://hermes-roy.tail.ts.net', mode: 'remote', profile: 'vps-remote', ...over }) as HermesConnection @@ -35,6 +47,7 @@ beforeEach(() => { $gateway.set({ id: 'live-socket' }) $activeGatewayProfile.set('default') $connection.set(localConn()) + $profiles.set([]) vi.stubGlobal('window', { hermesDesktop: { getConnection } }) vi.mocked(queryClient.invalidateQueries).mockClear() resetStarmapGraph.mockClear() @@ -101,3 +114,23 @@ describe('profile-scoped cache invalidation', () => { expect(resetStarmapGraph).toHaveBeenCalledTimes(1) }) }) + +describe('refreshProfiles shared rail list (#49289)', () => { + it('removes a deleted profile from the shared $profiles cache after Manage Profiles refreshes', async () => { + $profiles.set([profile('default', true), profile('test1')]) + vi.mocked(getProfiles).mockResolvedValueOnce({ profiles: [profile('default', true)] }) + + await refreshProfiles() + + expect($profiles.get().map(profile => profile.name)).toEqual(['default']) + }) + + it('leaves the shared $profiles cache intact when the refresh fails', async () => { + $profiles.set([profile('default', true), profile('test1')]) + vi.mocked(getProfiles).mockRejectedValueOnce(new Error('backend unavailable')) + + await expect(refreshProfiles()).rejects.toThrow('backend unavailable') + + expect($profiles.get().map(profile => profile.name)).toEqual(['default', 'test1']) + }) +}) diff --git a/apps/desktop/src/store/profile.ts b/apps/desktop/src/store/profile.ts index 2ff6987c0dc..8c13c10669d 100644 --- a/apps/desktop/src/store/profile.ts +++ b/apps/desktop/src/store/profile.ts @@ -38,6 +38,13 @@ export function setActiveProfile(name: string): void { $activeProfile.set(name || 'default') } +export async function refreshProfiles(): Promise { + const { profiles } = await getProfiles() + $profiles.set(profiles) + + return profiles +} + // ── Rail order ───────────────────────────────────────────────────────────── // User-defined order for the named (non-default) profile squares in the rail. // Names absent from the list fall back to alphabetical, appended at the tail — @@ -111,8 +118,7 @@ export async function refreshActiveProfile(): Promise { } try { - const { profiles } = await getProfiles() - $profiles.set(profiles) + await refreshProfiles() } catch { // Leave the cached list in place. }