diff --git a/apps/desktop/src/app/chat/sidebar/gateway-groups.test.tsx b/apps/desktop/src/app/chat/sidebar/gateway-groups.test.tsx index c53143c270..4176503eab 100644 --- a/apps/desktop/src/app/chat/sidebar/gateway-groups.test.tsx +++ b/apps/desktop/src/app/chat/sidebar/gateway-groups.test.tsx @@ -83,18 +83,37 @@ it('keeps equal profile names on separate gateways and routes section creation a .closest('[data-gateway-group]') ?.getAttribute('data-gateway-group') ).toBe(JSON.stringify([null, 'default'])) - expect(screen.getByText('This computer · default')).toBeTruthy() - expect(screen.getByText('Homelab · default')).toBeTruthy() - expect(screen.getByText('Cloud workspace · default')).toBeTruthy() - fireEvent.click(screen.getByRole('button', { name: 'New session in Homelab · default' })) + act(() => + $sessions.set([ + ...$sessions.get(), + makeSessionInfo({ + id: 'remote-work', + connection_id: 'remote-1', + profile: 'work', + title: 'Work session', + last_active: Date.now() / 1000 + }) + ]) + ) + const gateway = screen.getByText('Homelab').closest('[data-gateway-section]') as HTMLElement + expect(within(gateway).getByText('default')).toBeTruthy() + expect(within(gateway).getByText('work')).toBeTruthy() + expect(gateway.querySelectorAll('[data-gateway-group]')).toHaveLength(2) + fireEvent.click(within(gateway).getAllByRole('button', { name: 'New session in default' })[0]) expect($newChatRoute.get()).toMatchObject({ connectionId: 'remote-1', profile: 'default' }) fireEvent.click(screen.getByText('cloud-1 session')) expect(resume).toHaveBeenLastCalledWith( 'cloud-1', expect.objectContaining({ connection_id: 'cloud-1', profile: 'default' }) ) - const group = screen.getByText('Homelab · default').closest('[data-gateway-group]')! - fireEvent.click(within(group as HTMLElement).getByRole('button', { name: 'Hide Homelab · default sessions' })) + const group = within(gateway).getByText('default').closest('[data-gateway-group]')! + fireEvent.click(within(group as HTMLElement).getByRole('button', { name: 'Hide default sessions' })) + expect(screen.queryByText('remote-1 session')).toBeNull() + expect(screen.getByText('Work session')).toBeTruthy() + fireEvent.click(within(gateway).getByRole('button', { name: 'Hide Homelab sessions' })) + expect(screen.queryByText('Work session')).toBeNull() + fireEvent.click(within(gateway).getByRole('button', { name: 'Show Homelab sessions' })) + expect(screen.getByText('Work session')).toBeTruthy() expect(screen.queryByText('remote-1 session')).toBeNull() expect(screen.getByText('local session')).toBeTruthy() }) diff --git a/apps/desktop/src/app/chat/sidebar/gateway-groups.tsx b/apps/desktop/src/app/chat/sidebar/gateway-groups.tsx index 07bc128a6a..82281dcd41 100644 --- a/apps/desktop/src/app/chat/sidebar/gateway-groups.tsx +++ b/apps/desktop/src/app/chat/sidebar/gateway-groups.tsx @@ -21,6 +21,7 @@ import { ProfileGlyph } from '@/components/ui/profile-glyph' import type { SessionInfo } from '@/hermes' import { useI18n } from '@/i18n' import { useStoreSelector } from '@/lib/use-session-slice' +import { $connectionsRegistry } from '@/store/connection-registry-state' import { newSessionInAgent, newSessionInProfile } from '@/store/profile' import { $sessionProfilesUsage } from '@/store/session' import { $sidebarSessionRankIds } from '@/store/sidebar-sort' @@ -45,12 +46,49 @@ interface GatewayProfileGroupsProps { renderRows: (sessions: SessionInfo[]) => ReactNode sensors?: ReturnType onNewSessionSplit?: NewSessionSplitHandler + nested?: boolean } -export function GatewayProfileGroups({ groups, renderRows, sensors, onNewSessionSplit }: GatewayProfileGroupsProps) { +export function GatewayProfileGroups({ + groups, + renderRows, + sensors, + onNewSessionSplit, + nested = false +}: GatewayProfileGroupsProps) { + const registry = useStore($connectionsRegistry) const order = useStore($gatewayGroupOrder) + const gatewayProfiles = new Map() + const sections: SidebarSessionGroup[] = [] - const ordered = [...groups].sort((a, b) => { + for (const group of groups) { + // Unknown legacy ownership stays unassigned; never guess a local gateway. + if (nested || !group.connectionId) { + sections.push(nested ? { ...group, label: group.profile! } : group) + + continue + } + + const id = JSON.stringify(['gateway', group.connectionId]) + const profiles = gatewayProfiles.get(id) + + if (profiles) { + profiles.push(group) + } else { + gatewayProfiles.set(id, [group]) + sections.push({ + id, + connectionId: group.connectionId, + label: + registry?.connections.find(connection => connection.id === group.connectionId)?.label || group.connectionId, + mode: 'profile', + path: null, + sessions: [] + }) + } + } + + const ordered = [...sections].sort((a, b) => { const left = order.indexOf(a.id) const right = order.indexOf(b.id) @@ -70,7 +108,19 @@ export function GatewayProfileGroups({ groups, renderRows, sensors, onNewSession onMove={direction => reorderGatewayGroups(arrayMove(ids, index, index + direction))} onNewSessionSplit={onNewSessionSplit} renderRows={renderRows} - /> + > + {gatewayProfiles.has(group.id) && ( +
+ +
+ )} + ))} ) @@ -83,9 +133,18 @@ interface GatewayProfileGroupProps { onNewSessionSplit?: NewSessionSplitHandler first: boolean last: boolean + children?: ReactNode } -function GatewayProfileGroup({ group, renderRows, onMove, first, last, onNewSessionSplit }: GatewayProfileGroupProps) { +function GatewayProfileGroup({ + group, + renderRows, + onMove, + first, + last, + onNewSessionSplit, + children +}: GatewayProfileGroupProps) { const { t } = useI18n() const s = t.sidebar const copy = s.gatewayGroups @@ -120,35 +179,42 @@ function GatewayProfileGroup({ group, renderRows, onMove, first, last, onNewSess } return ( - + - - startNewSessionDrag( - placement => { - if (!open) { - toggleGatewayGroup(group.id) - } + {group.profile && ( + + startNewSessionDrag( + placement => { + if (!open) { + toggleGatewayGroup(group.id) + } - onNewSessionSplit(placement.dir, { - anchor: placement.anchor, - before: placement.before, - profile: group.profile, - route - }) - }, - event, - { label: s.newSessionIn(label), profile: group.profile, route } - ) - : undefined - } - /> + onNewSessionSplit(placement.dir, { + anchor: placement.anchor, + before: placement.before, + profile: group.profile, + route + }) + }, + event, + { label: s.newSessionIn(label), profile: group.profile, route } + ) + : undefined + } + /> + )}