Merge pull request #74277 from NousResearch/bb/remembered-route-per-profile

fix(desktop): scope the remembered route per profile
This commit is contained in:
brooklyn! 2026-07-29 13:29:48 -05:00 committed by GitHub
commit b84389c625
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 83 additions and 9 deletions

View file

@ -71,15 +71,17 @@ export function useDesktopIntegrations({
// lands where you were. Overlays (settings/command-center/…) aren't stored —
// you don't want to boot into a modal.
useEffect(() => {
const routeProfile = rememberedSessionProfile($sessions.get(), routedSessionId, $activeGatewayProfile.get())
if (routedSessionId) {
setRememberedSessionId(
routedSessionId,
rememberedSessionProfile($sessions.get(), routedSessionId, $activeGatewayProfile.get())
)
setRememberedSessionId(routedSessionId, routeProfile)
}
if (!isOverlayView(appViewForPath(locationPathname))) {
setRememberedRoute(locationPathname)
// Keyed by the same owner as the id above: a session route embeds a
// session id, so remembering it globally would restore another profile's
// conversation on cold start.
setRememberedRoute(locationPathname, routeProfile)
}
}, [locationPathname, routedSessionId])
@ -97,7 +99,8 @@ export function useDesktopIntegrations({
}
restoredRef.current = true
const route = getRememberedRoute()
const activeProfile = $activeGatewayProfile.get()
const route = getRememberedRoute(activeProfile)
if (route && route !== NEW_CHAT_ROUTE && !isOverlayView(appViewForPath(route))) {
navigate(route, { replace: true })
@ -105,7 +108,7 @@ export function useDesktopIntegrations({
return
}
const last = getRememberedSessionId($activeGatewayProfile.get())
const last = getRememberedSessionId(activeProfile)
if (last) {
navigate(sessionRoute(last), { replace: true })

View file

@ -12,12 +12,14 @@ import {
$sessions,
$unreadFinishedSessionIds,
applyConfiguredDefaultProjectDir,
getRememberedRoute,
getRememberedSessionId,
mergeSessionPage,
rememberedSessionProfile,
resolveComposerSessionKey,
sessionPinId,
setCurrentCwd,
setRememberedRoute,
setRememberedSessionId,
setSelectedStoredSessionId,
setSessions,
@ -507,6 +509,59 @@ describe('remembered session id (per profile)', () => {
})
})
describe('remembered route (per profile)', () => {
beforeEach(() => {
localStorage.clear()
})
afterEach(() => {
localStorage.clear()
})
it('scopes the remembered route by profile so one profile cannot restore another', () => {
// A session route embeds a session id. Remembered globally, a cold start
// under 'default' would navigate straight into ai-engineer's conversation.
setRememberedRoute('/session/work-session', 'ai-engineer')
setRememberedRoute('/session/personal-session', 'default')
expect(getRememberedRoute('ai-engineer')).toBe('/session/work-session')
expect(getRememberedRoute('default')).toBe('/session/personal-session')
expect(getRememberedRoute('research')).toBeNull()
})
it('keeps the default profile on the legacy unsuffixed key for back-compat', () => {
localStorage.setItem('hermes.desktop.lastRoute', '/skills')
expect(getRememberedRoute('default')).toBe('/skills')
expect(getRememberedRoute(undefined)).toBe('/skills')
expect(getRememberedRoute('')).toBe('/skills')
expect(getRememberedRoute(null)).toBe('/skills')
})
it('clearing one profile leaves the others intact', () => {
setRememberedRoute('/session/work-session', 'ai-engineer')
setRememberedRoute('/session/personal-session', 'default')
setRememberedRoute(null, 'ai-engineer')
expect(getRememberedRoute('ai-engineer')).toBeNull()
expect(getRememberedRoute('default')).toBe('/session/personal-session')
})
it('route and session id agree on the owner, so restore cannot cross profiles', () => {
// The cold-start restore prefers the route over the id, so the two keys
// must be written under the same owner or the id scoping is bypassed.
const owner = rememberedSessionProfile([session({ id: 'stored-1', profile: 'ai-engineer' })], 'stored-1', 'default')
setRememberedSessionId('stored-1', owner)
setRememberedRoute('/session/stored-1', owner)
expect(getRememberedRoute('default')).toBeNull()
expect(getRememberedSessionId('default')).toBeNull()
expect(getRememberedRoute('ai-engineer')).toBe('/session/stored-1')
})
})
describe('rememberedSessionProfile', () => {
it('keys by the session row owning profile, not the active one', () => {
const sessions = [session({ id: 'stored-1', profile: 'ai-engineer' })]

View file

@ -72,10 +72,26 @@ export function rememberedSessionProfile(
// The last non-overlay route (a page like /skills, or a session route), so a
// relaunch lands back where you were instead of a bare new-chat.
//
// Scoped per profile for the same reason the remembered session id is: a single
// global key remembered ONE route across every profile, and a session route
// carries a session id in its path. Restoring under profile B would navigate to
// a session owned by profile A — the remembered-id scoping above is bypassed
// entirely, because the route is preferred over the id on cold start
// (#67603 family). The default profile keeps the original unsuffixed key so
// existing installs' remembered route survives the upgrade.
const LAST_ROUTE_KEY = 'hermes.desktop.lastRoute'
export const getRememberedRoute = (): null | string => storedString(LAST_ROUTE_KEY)
export const setRememberedRoute = (path: null | string) => persistString(LAST_ROUTE_KEY, path)
function rememberedRouteKey(profile?: null | string): string {
const key = (profile ?? '').trim()
return !key || key === 'default' ? LAST_ROUTE_KEY : `${LAST_ROUTE_KEY}.${key}`
}
export const getRememberedRoute = (profile?: null | string): null | string =>
storedString(rememberedRouteKey(profile))
export const setRememberedRoute = (path: null | string, profile?: null | string) =>
persistString(rememberedRouteKey(profile), path)
let configuredDefaultProjectDir = ''