fix(desktop): keep bot chat focused when clicking the Bots pane (#96062)
Clicking a bot row moved the layout interaction tracker to the sidebar group, so $focusedStoredSessionId fell back to the primary selection — which is null in Bot Mode, because bot chats open as tiles and never set $selectedStoredSessionId. The Bots plugin reads that null 'focused' edge as 'the chat lost the center', releases its open claim, and the Bots home re-asserts over the still-visible chat: the UI jumps to the list instead of staying in the chat. $focusedStoredSessionId now answers from the main zone's active tile in Bot Mode before falling back to the selection, so a chrome-sidebar click no longer fabricates a null edge; a genuinely closed chat (no tile in main) still surfaces null and the home returns as before. Regression tests cover the sidebar-click case (red before the fix), plus guards for the closed-chat and sessions-mode derivations.
This commit is contained in:
@@ -2,11 +2,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ClientSessionState } from '@/app/types'
|
||||
import { findGroupOfPane, group, split } from '@/components/pane-shell/tree/model'
|
||||
import { $layoutTree } from '@/components/pane-shell/tree/store'
|
||||
import { $layoutTree, noteActiveTreeGroup } from '@/components/pane-shell/tree/store'
|
||||
import { $workspaceMode, setWorkspaceScope } from '@/components/pane-shell/workspace-scope'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
import { $activeSessionId, $connection, $selectedStoredSessionId, setSessions } from '@/store/session'
|
||||
import type { SessionTile } from '@/store/session-states'
|
||||
import {
|
||||
$focusedStoredSessionId,
|
||||
$sessionStates,
|
||||
$sessionTiles,
|
||||
blankDraftTile,
|
||||
@@ -505,6 +507,76 @@ describe('boot-restore selection homing (⌘R tab persistence)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('$focusedStoredSessionId in Bot Mode (#96062)', () => {
|
||||
afterEach(() => {
|
||||
$layoutTree.set(null)
|
||||
$selectedStoredSessionId.set(null)
|
||||
setWorkspaceScope('sessions')
|
||||
})
|
||||
|
||||
it('a Bots-pane click keeps the main-zone bot tile focused instead of collapsing to a null selection edge', () => {
|
||||
// Bot chats open as TILES and never set $selectedStoredSessionId. Clicking
|
||||
// a roster row moves the interaction tracker to the sidebar group, whose
|
||||
// active pane is chrome ('hermes-bots:pane'), not a session tile. The old
|
||||
// derivation then fell back to the null primary selection and published a
|
||||
// NULL "focused session" edge — which the Bots plugin read as "the chat
|
||||
// lost the center", releasing its open claim and re-asserting the Bots
|
||||
// home over the still-visible chat (the reported "jumps to the list").
|
||||
setWorkspaceScope('bots', 'bot:b')
|
||||
$selectedStoredSessionId.set(null)
|
||||
$layoutTree.set(
|
||||
split('row', [
|
||||
group(['sessions', 'hermes-bots:pane'], { active: 'hermes-bots:pane', id: 'grp-sessions' }),
|
||||
group(['workspace', tilePane('chat-b')], { active: tilePane('chat-b'), id: 'grp-main' })
|
||||
])
|
||||
)
|
||||
noteActiveTreeGroup('grp-sessions')
|
||||
|
||||
expect($focusedStoredSessionId.get()).toBe('chat-b')
|
||||
})
|
||||
|
||||
it('the main-zone tile also answers while the tracker sits on the workspace tab itself', () => {
|
||||
setWorkspaceScope('bots', 'bot:b')
|
||||
$selectedStoredSessionId.set(null)
|
||||
$layoutTree.set(group(['workspace', tilePane('chat-b')], { active: tilePane('chat-b'), id: 'grp-main' }))
|
||||
noteActiveTreeGroup('grp-main')
|
||||
|
||||
expect($focusedStoredSessionId.get()).toBe('chat-b')
|
||||
})
|
||||
|
||||
it('a closed bot chat (no tile in main) still falls back to the selection', () => {
|
||||
setWorkspaceScope('bots', 'bot:b')
|
||||
$selectedStoredSessionId.set(null)
|
||||
$layoutTree.set(
|
||||
split('row', [
|
||||
group(['sessions', 'hermes-bots:pane'], { active: 'hermes-bots:pane', id: 'grp-sessions' }),
|
||||
group(['workspace'], { active: 'workspace', id: 'grp-main' })
|
||||
])
|
||||
)
|
||||
noteActiveTreeGroup('grp-sessions')
|
||||
|
||||
// No tile owns the main zone — the chat was closed — so the null edge is
|
||||
// genuine and must still surface (that is what lets the Bots home return).
|
||||
expect($focusedStoredSessionId.get()).toBeNull()
|
||||
})
|
||||
|
||||
it('sessions mode keeps collapsing to the primary selection (derivation gated to Bot Mode)', () => {
|
||||
$selectedStoredSessionId.set('primary-1')
|
||||
$layoutTree.set(
|
||||
split('row', [
|
||||
group(['sessions'], { active: 'sessions', id: 'grp-sessions' }),
|
||||
group(['workspace', tilePane('stacked')], { active: tilePane('stacked'), id: 'grp-main' })
|
||||
])
|
||||
)
|
||||
noteActiveTreeGroup('grp-sessions')
|
||||
|
||||
// The main-zone tile must NOT answer here: in sessions mode the sidebar
|
||||
// highlight follows the primary selection exactly as it always has.
|
||||
expect($workspaceMode.get()).toBe('sessions')
|
||||
expect($focusedStoredSessionId.get()).toBe('primary-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('focusedSessionNeedsRoute', () => {
|
||||
it('routes when the session is not on screen', () => {
|
||||
expect(focusedSessionNeedsRoute(null, false)).toBe(true)
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
noteActiveTreeGroup,
|
||||
revealTreePane
|
||||
} from '@/components/pane-shell/tree/store'
|
||||
import { $workspaceMode } from '@/components/pane-shell/workspace-scope'
|
||||
import type { WorkspaceMode } from '@/contrib/types'
|
||||
import { stableArray } from '@/lib/stable-array'
|
||||
import { readJson, writeJson } from '@/lib/storage'
|
||||
@@ -1590,11 +1591,36 @@ export function reopenLastClosedTile(): void {
|
||||
/** Stored id of the focused session (the interacted zone's tile, else the
|
||||
* primary's selection). Null on a fresh draft. */
|
||||
export const $focusedStoredSessionId = computed(
|
||||
[$activeTreeGroup, $layoutTree, $selectedStoredSessionId],
|
||||
(groupId, tree, selected) => {
|
||||
[$activeTreeGroup, $layoutTree, $selectedStoredSessionId, $workspaceMode],
|
||||
(groupId, tree, selected, workspaceMode) => {
|
||||
const active = groupId && tree ? findGroup(tree, groupId)?.active : undefined
|
||||
|
||||
return active?.startsWith(TILE_PANE_PREFIX) ? active.slice(TILE_PANE_PREFIX.length) : selected
|
||||
if (active?.startsWith(TILE_PANE_PREFIX)) {
|
||||
return active.slice(TILE_PANE_PREFIX.length)
|
||||
}
|
||||
|
||||
// The interaction tracker can point at sidebar CHROME while a chat still
|
||||
// holds the main zone's active tab — clicking a Bots-pane roster row moves
|
||||
// it to the sidebar group, whose active pane ('hermes-bots:pane') is not a
|
||||
// session tile. In sessions mode the primary selection answers, exactly as
|
||||
// always. In Bot Mode that fallback alone publishes a NULL "focused"
|
||||
// edge: bot chats open as TILES and never set $selectedStoredSessionId,
|
||||
// so the selection is null while the chat is plainly on screen. The Bots
|
||||
// plugin reads that null edge as "the chat lost the center", releases its
|
||||
// open claim, and re-asserts the Bots home over the still-visible chat —
|
||||
// the reported "clicking a bot chat jumps to the list" (#96062). Bot
|
||||
// Mode's on-screen truth is the main zone's active TILE; only when the
|
||||
// main zone holds no tile (chat closed) does the selection answer, so a
|
||||
// genuine close still lets the home return.
|
||||
if (workspaceMode === 'bots' && tree) {
|
||||
const mainActive = findGroupOfPane(tree, 'workspace')?.active
|
||||
|
||||
if (mainActive?.startsWith(TILE_PANE_PREFIX)) {
|
||||
return mainActive.slice(TILE_PANE_PREFIX.length)
|
||||
}
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user