Merge pull request #67302 from NousResearch/bb/salvage-66870-focus-tab-hijack

fix(desktop): don't hijack the active tab on reactive pane unhide (supersedes #66870)
This commit is contained in:
brooklyn! 2026-07-19 00:03:55 -04:00 committed by GitHub
commit 7db2decbea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 85 additions and 0 deletions

View file

@ -0,0 +1,76 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// Focus layout packs `workspace` and `files` into one tab group. A reactive
// unhide of `files` (cwd arrives on the first reply / new session) must NOT
// steal the active tab away from the workspace/new-session the user is looking
// at — the "files panel hijacks the tab ~1s after the first reply" bug.
// frontPaneInGroup only takes the active slot when the current active pane is
// not itself showable, so it still picks a valid tab when nothing is visible.
describe('reactive unhide in a shared (Focus) group', () => {
beforeEach(() => {
window.localStorage.clear()
vi.resetModules()
})
afterEach(() => {
vi.resetModules()
})
async function setupFocusGroup() {
const tree = await import('@/components/pane-shell/tree/store')
const model = await import('@/components/pane-shell/tree/model')
const { registry } = await import('@/contrib/registry')
for (const id of ['workspace', 'files']) {
registry.register({ id, area: 'panes', title: id, data: {}, render: () => null })
}
// Focus layout: sessions | (workspace + files share one group), workspace
// fronted — mirrors FOCUS_TREE's single-group packing in controller.tsx.
tree.declareDefaultTree(
model.split(
'row',
[
model.group(['sessions'], { id: 'grp-sessions' }),
model.group(['workspace', 'files'], { active: 'workspace', id: 'grp-focus' })
],
[1, 4.6]
)
)
const activeOf = (paneId: string) =>
model.findGroupOfPane(tree.$layoutTree.get()!, paneId)?.active ?? null
return { activeOf, tree }
}
it('does not steal the active tab from workspace when files unhides', async () => {
const { activeOf, tree } = await setupFocusGroup()
// Detached chat: no cwd → files hidden, workspace is the active tab.
tree.setTreePaneHidden('files', true)
expect(activeOf('workspace')).toBe('workspace')
// First reply adopts a cwd → the workspace wiring unhides files.
tree.setTreePaneHidden('files', false)
expect(tree.$hiddenTreePanes.get().has('files')).toBe(false)
// The user stays on the new session; files must not hijack the tab.
expect(activeOf('workspace')).toBe('workspace')
})
it('still fronts the unhidden pane when the active tab is not showable', async () => {
const { activeOf, tree } = await setupFocusGroup()
// Nothing valid showing: the active pane is itself hidden.
tree.setTreePaneHidden('workspace', true)
tree.setTreePaneHidden('files', true)
tree.setTreePaneHidden('files', false)
// With no showable active pane, the reactive unhide picks files so the
// group fronts a valid tab (preserves the #65375 "visible next time" intent).
expect(activeOf('files')).toBe('files')
})
})

View file

@ -149,6 +149,15 @@ function frontPaneInGroup(paneId: string) {
return
}
// Don't steal the active tab from a pane the user is already viewing. In the
// Focus layout `files` shares a group with `workspace`, so a reactive unhide
// (cwd arrives on the first reply) would otherwise yank the active tab off
// the new session onto files. Only take the active slot when the current
// active pane isn't itself showable — then fronting picks a valid tab.
if (group.active && !$hiddenTreePanes.get().has(group.active)) {
return
}
const next = setActivePaneOp(tree, group.id, paneId)
if (next !== tree) {