Skip to content

fix(desktop): session tabs no longer trap — self-heal phantom tiles, never hide chat tabs behind a hidden strip - #79520

Open
leonphull wants to merge 3 commits into
NousResearch:mainfrom
leonphull:fix/desktop-session-tab-traps
Open

fix(desktop): session tabs no longer trap — self-heal phantom tiles, never hide chat tabs behind a hidden strip#79520
leonphull wants to merge 3 commits into
NousResearch:mainfrom
leonphull:fix/desktop-session-tab-traps

Conversation

@leonphull

Copy link
Copy Markdown
Contributor

Summary

Fixes two production-observed traps that make desktop chat tabs appear completely broken after a crash or an unlucky toggle. Both were hit today on macOS (remote/SSH connection mode) while recovering from the FD-exhaustion outage (#78873); root-caused live via CDP against the affected machines.

Fixes #79476, fixes #79518.

1. Phantom session tiles (crash-stale storage) — #79476

Symptom: "Open in new tab" vanishes from the session context menu; Cmd-click on sidebar sessions is a dead click.

Root cause: $sessionTiles (localStorage) and the layout tree persist separately. A hard restart can commit one without the other; focusOpenSession() trusted the tile list unconditionally and returned 'tile' for a pane that doesn't exist. Downstream, openSession(..., 'tab') early-returns (dead click) and the actions menu computes alreadyTabbed=true forever.

Fix: after the reveal attempt, if the pane is still absent from the layout tree, the persisted tile is phantom — discardSessionTile() it and report a miss so the caller opens the session for real. Self-heals on first interaction instead of requiring a manual localStorage wipe.

In-flight guard: tiles opened during the current run are exempt (pane adoption is async via the registry; a just-opened tile is briefly pane-less and must not be discarded mid-flight). Only storage-hydrated tiles can be judged phantom.

2. Hidden tab strip dead end — #79518

Symptom: every sidebar click on an open session looks dead; new tabs arrive invisible; there is NO way back (the only "Show header" affordance lives in the strip's own context menu).

Root cause: two sites preserve headerHidden where the pane is a chat tab: revealTreePane() (fronts session tabs into an invisible strip) and the adoption path (re-pins the zone's hidden flag onto newly adopted session tiles — the "standing preference" branch, correct for tool panels only).

Fix: use the existing isSessionStripPane() distinction at both sites. Revealing or adopting a session strip pane (workspace / session-tile:*) into a header-hidden zone force-shows the bar; tool panels keep honoring the standing preference (terminal close/reopen keeps working as designed).

This follows the desktop AGENTS.md doctrine: recovery ladders must end in a real affordance, and persisted UI state is a cache to validate at the boundary, not owned truth.

Tests

  • src/store/session-states.test.ts: phantom tile dropped + reported as miss; in-flight tile survives the adoption gap (simulates the exact mid-write crash shape).
  • src/components/pane-shell/tree/hidden-strip-recovery.test.ts (new): reveal un-hides for session tiles; adoption un-hides for new session tiles; tool panels still honor the hidden preference.
  • Full desktop suite: 4418 passed | 2 skipped on this branch.

Verification beyond unit tests

Both fixes were validated against the live production instances that hit the bugs (CDP Runtime.evaluate against the packaged app): the phantom-tile state and the hidden-strip state were reproduced from the users' real localStorage payloads, and the patched build restores tab behavior on first interaction.

…storage

A hard app restart (crash, SIGKILL, forced quit) can leave localStorage tile state
(hermes.sessionTiles) referencing panes that no longer exist in the layout tree.
focusOpenSession() then claimed 'tile' for the phantom entry:

- revealTreePane() silently no-ops (nothing to reveal)
- every sidebar Cmd+click / "open as tab" becomes a dead click
- session-actions-menu computes alreadyTabbed=true forever, so "Open in new tab"
  disappears from the menu for every affected session

Fix: when the pane is still absent from the layout tree after the reveal attempt,
treat the persisted tile as phantom — discardSessionTile() it and report a miss so
the caller re-opens the session for real. Self-heals crash-stale storage on the
first interaction instead of requiring a manual localStorage wipe.

Regression test simulates the mid-write crash shape: pane removed from the tree
while the $sessionTiles entry survives.

Closes NousResearch#79476.
Pane adoption is async (registry -> layout tree), so a tile opened during the
current run can be momentarily pane-less without being phantom. Discarding it
mid-flight would cancel a legitimate open. Track runtime-opened tile ids and
only judge storage-hydrated tiles (boot / profile switch) as phantom when their
pane never materializes.

Refs NousResearch#79476.
The tab strip is the only affordance that shows/switches/closes chat tabs,
and the only "show header" control lives in the header's own context menu.
Once a zone hosting session tiles had headerHidden=true (user toggle or
crash-stale layout storage), the UI became an inescapable dead end:

- revealTreePane fronted session tabs invisibly -> every sidebar click on an
  open session looked dead (nothing visible changed)
- pane adoption re-pinned the hidden flag (standing-preference branch), so
  even newly opened tabs arrived invisible

Fix: revealing or adopting a SESSION strip pane (workspace / session-tile:*)
into a header-hidden zone force-shows the bar. Tool panels keep honoring the
zone's standing hidden preference (the case the original branch protects).

Three regression tests: reveal un-hides for session tiles, adoption un-hides
for new session tiles, tool panels still honor the preference.
@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 5, 2026
@matebenyovszky

Copy link
Copy Markdown

Cross-referencing for whoever reviews this area: there's a third zone class with the same "trapped behind a hidden strip" shape — a preview / Browser tile docked in its own zone. Once its header is double-tap-hidden it has no tab, no ✕ and no zone menu, and neither the session self-heal here nor the sidebar-row restore in #81638 reaches it (no session state, no sidebar row).

Opened #84458 for that one: a top-edge reveal strip on the zone itself, so the way back doesn't depend on another surface existing.

Not suggesting any change to this PR — just so the three don't get reviewed as duplicates of each other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

3 participants