mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-13 09:01:54 +00:00
fix(desktop): new chat honours the active profile instead of rubberbanding to default (#45057)
The top "New Session" button (and /new, the keyboard shortcut) cleared $newChatProfile to null, meaning "use the live gateway context". But createBackendSessionForSend turned a null into an omitted `profile` param on session.create. In global-remote mode one backend serves every profile, so an omitted profile silently binds the new chat to the launch (default) profile's home/state.db — the session "rubberbands back to default" even though the rail still shows the selected profile. The per-profile "+" worked because it sets $newChatProfile explicitly. Resolve a null $newChatProfile to the active gateway profile at the single session-creation chokepoint so session.create always carries the live profile. Harmless for single-profile and local-pooled users: a backend resolves its own launch profile to None (_profile_home), so passing it changes nothing.
This commit is contained in:
parent
d62979a6f3
commit
79c3ed3cc9
2 changed files with 129 additions and 6 deletions
119
apps/desktop/src/app/session/hooks/use-session-actions.test.tsx
Normal file
119
apps/desktop/src/app/session/hooks/use-session-actions.test.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { cleanup, render, waitFor } from '@testing-library/react'
|
||||
import type { MutableRefObject } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { $activeGatewayProfile, $newChatProfile } from '@/store/profile'
|
||||
import { $currentCwd } from '@/store/session'
|
||||
|
||||
import type { ClientSessionState } from '../../types'
|
||||
|
||||
import { useSessionActions } from './use-session-actions'
|
||||
|
||||
vi.mock('@/hermes', async importOriginal => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
deleteSession: vi.fn(),
|
||||
getSessionMessages: vi.fn(),
|
||||
listAllProfileSessions: vi.fn(),
|
||||
setApiRequestProfile: vi.fn(),
|
||||
setSessionArchived: vi.fn()
|
||||
}))
|
||||
|
||||
const RUNTIME_SESSION_ID = 'rt-new-001'
|
||||
|
||||
function Harness({
|
||||
onReady,
|
||||
requestGateway
|
||||
}: {
|
||||
onReady: (create: (preview?: string | null) => Promise<string | null>) => void
|
||||
requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
||||
}) {
|
||||
const ref = <T,>(value: T): MutableRefObject<T> => ({ current: value })
|
||||
|
||||
const actions = useSessionActions({
|
||||
activeSessionId: null,
|
||||
activeSessionIdRef: ref<string | null>(null),
|
||||
busyRef: ref(false),
|
||||
creatingSessionRef: ref(false),
|
||||
ensureSessionState: () => ({}) as ClientSessionState,
|
||||
getRouteToken: () => 'token',
|
||||
navigate: vi.fn() as never,
|
||||
requestGateway,
|
||||
runtimeIdByStoredSessionIdRef: ref(new Map<string, string>()),
|
||||
selectedStoredSessionId: null,
|
||||
selectedStoredSessionIdRef: ref<string | null>(null),
|
||||
sessionStateByRuntimeIdRef: ref(new Map<string, ClientSessionState>()),
|
||||
syncSessionStateToView: vi.fn(),
|
||||
updateSessionState: () => ({}) as ClientSessionState
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onReady(actions.createBackendSessionForSend)
|
||||
}, [actions.createBackendSessionForSend, onReady])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function createWith(profileSetup: () => void): Promise<Record<string, unknown> | undefined> {
|
||||
let createParams: Record<string, unknown> | undefined
|
||||
|
||||
const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
|
||||
if (method === 'session.create') {
|
||||
createParams = params
|
||||
|
||||
return { session_id: RUNTIME_SESSION_ID, stored_session_id: null } as never
|
||||
}
|
||||
|
||||
return {} as never
|
||||
})
|
||||
|
||||
$currentCwd.set('')
|
||||
profileSetup()
|
||||
|
||||
let create: ((preview?: string | null) => Promise<string | null>) | null = null
|
||||
render(<Harness onReady={c => (create = c)} requestGateway={requestGateway} />)
|
||||
await waitFor(() => expect(create).not.toBeNull())
|
||||
await create!()
|
||||
|
||||
return createParams
|
||||
}
|
||||
|
||||
describe('createBackendSessionForSend profile routing', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
$newChatProfile.set(null)
|
||||
$activeGatewayProfile.set('default')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('routes a plain new chat (no explicit profile) to the live gateway profile', async () => {
|
||||
// The "rubberband to default" bug: the top New Session button clears
|
||||
// $newChatProfile to null. In global-remote mode one backend serves every
|
||||
// profile, so an omitted `profile` lands the chat on the launch (default)
|
||||
// profile. The session must instead carry the active gateway profile.
|
||||
const params = await createWith(() => {
|
||||
$activeGatewayProfile.set('coder')
|
||||
$newChatProfile.set(null)
|
||||
})
|
||||
|
||||
expect(params).toMatchObject({ profile: 'coder' })
|
||||
})
|
||||
|
||||
it('honours an explicit per-profile "+" selection', async () => {
|
||||
const params = await createWith(() => {
|
||||
$activeGatewayProfile.set('coder')
|
||||
$newChatProfile.set('analyst')
|
||||
})
|
||||
|
||||
expect(params).toMatchObject({ profile: 'analyst' })
|
||||
})
|
||||
|
||||
it('passes the default profile for single-profile users (backend resolves it to launch)', async () => {
|
||||
const params = await createWith(() => {
|
||||
$activeGatewayProfile.set('default')
|
||||
$newChatProfile.set(null)
|
||||
})
|
||||
|
||||
expect(params).toMatchObject({ profile: 'default' })
|
||||
})
|
||||
})
|
||||
|
|
@ -407,13 +407,17 @@ export function useSessionActions({
|
|||
creatingSessionRef.current = true
|
||||
|
||||
try {
|
||||
// Route the new chat to the chosen profile's backend (null = primary,
|
||||
// so single-profile users are unaffected).
|
||||
await ensureGatewayProfile($newChatProfile.get())
|
||||
// A plain new session (top "New Session", /new, keybind) leaves
|
||||
// $newChatProfile null to mean "use the live context"; the per-profile
|
||||
// "+" sets it explicitly. Resolve null to the active gateway profile so
|
||||
// session.create always carries it: in global-remote mode one backend
|
||||
// serves every profile, so an omitted profile param silently lands the
|
||||
// chat on the launch (default) profile — the "rubberbands back to
|
||||
// default" bug. This is a no-op for single-profile/local-pooled users:
|
||||
// a backend resolves its own launch profile to None (_profile_home).
|
||||
const newChatProfile = $newChatProfile.get() ?? normalizeProfileKey($activeGatewayProfile.get())
|
||||
await ensureGatewayProfile(newChatProfile)
|
||||
const cwd = $currentCwd.get().trim() || workspaceCwdForNewSession()
|
||||
// Pass the owning profile so a new chat under a non-launch profile (global
|
||||
// remote mode) builds its agent + persists against THAT profile's home/db.
|
||||
const newChatProfile = $newChatProfile.get()
|
||||
|
||||
const created = await requestGateway<SessionCreateResponse>('session.create', {
|
||||
cols: 96,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue