File browser sidebar auto-opens unexpectedly on desktop

NousResearch/hermes-agent · cluster 65173 · apps/desktop

Data as of

Nothing open on GitHub. Fix merged in #65375. Check it covers your case.

The short answer

0open issues
0open PRs
9items grouped
Known?
Yes. 9 related items grouped here, 5 issues already closed.
Fix event
A fix was merged to main in PR #65375. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #65375, #122369. That is separate from the fix event.
Still open
Nothing open on GitHub.

Multiple reports and related fixes address the desktop file browser/sidebar panel automatically reopening on new session creation or first reply, ignoring user-collapsed state.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #65375 fix(desktop): don't auto-expand user-collapsed side on reactive unhide

    merged to main

    files: apps/desktop/src/components/pane-shell/tree/reactive-unhide.test.ts, apps/desktop/src/components/pane-shell/tree/store.ts, apps/desktop/src/store/review.ts

    Show the diff
    • apps/desktop/src/components/pane-shell/tree/reactive-unhide.test.ts
      @@ -0,0 +1,147 @@
      +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
      +
      +// Ground-truth repro for "reactive unhides silently re-open a user-collapsed
      +// side": `setTreePaneHidden` is the primitive that backs `bindPaneVisibility`
      +// (e.g. `bindPaneVisibility('files', $hasWorkspace)`). The original
      +// implementation auto-called `revealTreePane` on every unhide, which expanded
      +// the parent column even when the user had explicitly collapsed it — so a
      +// workspace transition (`$currentCwd` flipping on session create / resume)
      +// forced the right sidebar open and persisted that open state to localStorage.
      +//
      +// We mirror controller.tsx exactly: register the `files` pane with
      +// `placement: 'right'`, declare the default tree, bind the right side to
      +// the file-browser open store, then collapse the right sidebar and trigger
      +// the reactive unhide via the same primitive `bindPaneVisibility` invokes.
      +
      +describe('reactive pane unhide', () => {
      +  beforeEach(() => {
      +    window.localStorage.clear()
      +    vi.resetModules()
      +  })
      +
      +  afterEach(() => {
      +    vi.resetModules()
      +  })
      +
      +  async function setupWithFiles() {
      +    const tree = await import('@/components/pane-shell/tree/store')
      +    const layout = await import('@/store/layout')
      +    const model = await import('@/components/pane-shell/tree/model')
      +    const { registry } = await import('@/contrib/registry')
      +
      +    // Register `files` like controller.tsx does — placement 'right' is what
      +    // makes `treeSideOfPane('files')` return 'right' (and therefore what
      +    // makes the buggy `revealTreePane` auto-expand the right column).
      +    registry.register({
      +      id: 'files',
      +      area: 'panes',
      +      title: 'files',
      +      data: { placement: 'right' },
      +      render: () => null
      +    })
      +
      +    // Declare a minimal default tree mirroring the production DEFAULT_TREE's
      +    // row shape (sessions | workspace | right-column-with-files).
      +    tree.declareDefaultTree(
      +      model.split('row', [
      +        model.group(['sessions'], { id: 'grp-sessions' }),
      +        model.group(['workspace'], { id: 'grp-main' }),
      +        model.split('column', [
      +          model.split('row', [
      +            model.group(['files'], { id: 'grp-files' })
      +          ], [1], 'spl-rail'),
      +          model.group(['terminal'], { id: 'grp-terminal' })
      +        ], [1.6, 1], 'spl-right')
      +      ], [0.85, 3, 1.6])
      +    )
      +
      +    // Mirror controller.tsx:512.
      +    tree.bindTreeSideVisibility('right', layout.$fileBrowserOpen, layout.setFileBrowserOpen)
      +
      +    return { tree, layout }
      +  }
      +
      +  it('reactive unhide does NOT expand a user-collapsed side', async () => {
      +    const { tree, layout } = await setupWithFiles()
      +
      +    // Simulate the production scenario: a detached chat has no cwd, so
      +    // `bindPaneVisibility('files', $hasWorkspace)` calls `setTreePaneHidden(
      +    // 'files', true)` (hidden). Then the user collapses the right sidebar.
      +    // Then a new session acquires a cwd and the workspace wiring unhides
      +    // `files`. That last step is what the bug corrupts.
      +    tree.setTreePaneHidden('files', true)
      +    expect(tree.$hiddenTreePanes.get().has('files')).toBe(true)
      +
      +    layout.setFileBrowserOpen(false)
      +    expect(layout.$fileBrowserOpen.get()).toBe(false)
      +    expect(tree.$collapsedTreeSides.get().has('right')).toBe(true)
      +
      +    // Workspace flips: `bindPaneVisibility('files', $hasWorkspace)` calls
      +    // `setTreePaneHidden('files', false)` — model it directly.
      +    tree.setTreePaneHidden('files', false)
      +
      +    // The pane unhides…
      +    expect(tree.$hiddenTreePanes.get().has('files')).toBe(false)
      +
      +    // …but the user-collapsed side MUST stay collapsed. Before the fix, the
      +    // unhide called `revealTreePane`, which saw the side was collapsed and
      +    // called `setFileBrowserOpen(true)` — silently re-opening the right
      +    // sidebar on every session create / resume.
      +    expect(layout.$fileBrowserOpen.get()).toBe(false)
      +    expect(tree.$collapsedTreeSides.get().has('right')).toBe(true)
      +  })
      +
      +  it('reactive HIDE (hidden=true) leaves the side flag alone', async () => {
      +    const { tree, layout } = await setupWithFiles()
      +
      +    layout.setFileBrowserOpen(false)
      +    expect(tree.$collapsedTreeSides.get().has('right')).toBe(true)
      +
      +    // A reactive HIDE (workspace goes away) must not flip the side either —
      +    // collapsing the side is the user's domain; hiding individual panes
      +    // inside it is the workspace domain.
      +    tree.setTreePaneHidden('files', true)
      +
      +    expect(tree.$hiddenTreePanes.get().has('files')).toBe(true)
      +    expect(layout.$fileBrowserOpen.get()).toBe(false)
      +    expect(tree.$collapsedTreeSides.get().has('right')).toBe(true)
      +  })
      +
      +  it('explicit setFileBrowserOpen still expands the side (user toggle)', async () => {
      +    const { tree, layout } = await setupWithFiles()
      +
      +    layout.setFileBrowserOpen(false)
      +    expect(tree.$collapsedTreeSides.get().has('right')).toBe(true)
      +
      +    // The user toggles the sidebar back open — this MUST still work; the
      +    // binding mirrors `$fileBrowserOpen` to `$collapsedTreeSides`.
      +    layout.setFileBrowserOpen(true)
      +    expect(layout.$fileBrowserOpen.get()).toBe(true)
      +    expect(tree.$collapsedTreeSides.get().has('right')).toBe(false)
      +  })
      +
      +  it('reactive unhide does not invoke the right side opener directly', async () => {
      +    const { tree, layout } = await setupWithFiles()
      +
      +    // Spy on the opener that `revealTreePane` would call when expanding a
      +    // collapsed side — the bug is exactly this call firing on reactive unhide.
      +    const openerSpy = vi.fn()
      +    tree.bindTreeSideVisibility('right', layout.$fileBrowserOpen, openerSpy)
      +
      +    layout.setFileBrowserOpen(false)
      +    expect(tree.$collapsedTreeSides.get().has('right')).toBe(true)
      +
      +    tree.setTreePaneHidden('files', true)
      +    expect(tree.$hiddenTreePanes.get().has('files')).toBe(true)
      +
      +    openerSpy.mockClear()
      +
      +    // Reactive unhide fires via the same primitive the workspace wiring uses.
      +    tree.setTreePaneHidden('files', false)
      +
      +    // The opener MUST NOT be called — before the fix, the auto-reveal would
      +    // call `setFileBrowserOpen(true)` via this opener.
      +    expect(tree.$hiddenTreePanes.get().has('files')).toBe(false)
      +    expect(openerSpy).not.toHaveBeenCalled()
      +  })
      +})
    • apps/desktop/src/components/pane-shell/tree/store.ts
      @@ -126,9 +126,33 @@ export function setTreePaneHidden(paneId: string, hidden: boolean) {
       
         $hiddenTreePanes.set(next)
       
      -  // Unhiding is an intent to SEE the pane — front it in its group.
      +  // Reactive unhides (e.g. `bindPaneVisibility('files', $hasWorkspace)`) are
      +  // state-driven, not user intent — opening the side or fronting the tab in
      +  // response to an environmental flag change would clobber an explicit user
      +  // collapse (Cmd+J) and silently re-open the rail after every session create.
      +  // Callers that want user-intent semantics (open the side, front the tab)
      +  // must call `revealTreePane` explicitly. We still front the pane in its
      +  // group so it's visible the next time the column is shown.
         if (!hidden) {
      -    revealTreePane(paneId)
      +    frontPaneInGroup(paneId)
      +  }
      +}
      +
      +/** Make `paneId` the active tab in its group without touching side collapse
      + *  or zone-minimized state — the safe "make it visible next time the column
      + *  is shown" primitive that reactive unhides need. */
      +function frontPaneInGroup(paneId: string) {
      +  const tree = $layoutTree.get()
      +  const group = tree ? findGroupOfPane(tree, paneId) : null
      +
      +  if (!tree || !group || group.active === paneId) {
      +    return
      +  }
      +
      +  const next = setActivePaneOp(tree, group.id, paneId)
      +
      +  if (next !== tree) {
      +    commit(next)
         }
       }
       
    • apps/desktop/src/store/review.ts
      @@ -2,6 +2,7 @@ import { atom, computed } from 'nanostores'
       
       import { SIDEBAR_COLLAPSE_MEDIA_QUERY } from '@/app/layout-constants'
       import { PANE_TOGGLE_REVEAL_EVENT } from '@/components/pane-shell'
      +import { revealTreePane } from '@/components/pane-shell/tree/store'
       import type { HermesReviewFile, HermesReviewShipInfo } from '@/global'
       import { matchesQuery } from '@/hooks/use-media-query'
       import { desktopGit } from '@/lib/desktop-git'
      @@ -274,6 +275,7 @@ export function toggleReview(): void {
           closeReview()
         } else {
           openReview()
      +    revealTreePane(REVIEW_PANE_ID)
         }
       }
       
  • #122369 fix(desktop): add a File Browser toggle to Appearance settings

    merged to main

    files: apps/desktop/src/app/settings/appearance-settings.tsx, apps/desktop/src/app/settings/file-browser-setting.test.tsx, apps/desktop/src/app/settings/settings-manifest.ts, apps/desktop/src/i18n/ar.ts, apps/desktop/src/i18n/de.ts, apps/desktop/src/i18n/en.ts, apps/desktop/src/i18n/es.ts, apps/desktop/src/i18n/fr.ts, apps/desktop/src/i18n/ja.ts, apps/desktop/src/i18n/ru.ts, apps/desktop/src/i18n/types.ts, apps/desktop/src/i18n/zh-hant.ts, apps/desktop/src/i18n/zh.ts

2 PRs closed without merging

From the close comments: superseded by another PR 2

  • #66079 fix(desktop): stop auto-opening file tree on new session creation (#66059) · superseded by another PR → #65375
  • #67288 fix(desktop): stop files panel auto-opening on first reply · superseded by another PR → #65375, #67286
Closed issues · 5
  • Closed#65173File browser panel reopens on session start despite persisted closed state; no settings toggle for default
  • Closed#65376Right sidebar (file browser) re-opens after Cmd+J collapse when a new session is created
  • Closed#66059[Bug]: The file tree keeps opening automatically.
  • Closed#66917[desktop] File browser sidebar auto-opens on every new session when no project is active
  • Closed#67286[Bug]: desktop files panel auto-opens after first reply in a new session
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.25: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
apps/desktop
Merged PRs
#65375, #122369
Closed issues
5
Attempts
2 PRs closed without merging
Fix link checked
26 Sep 2026

For your agent

The same facts, typed.