Skip to content

fix(desktop): give a header-hidden zone a way back - #84458

Open
matebenyovszky wants to merge 1 commit into
NousResearch:mainfrom
matebenyovszky:fix/reveal-hidden-zone-header
Open

fix(desktop): give a header-hidden zone a way back#84458
matebenyovszky wants to merge 1 commit into
NousResearch:mainfrom
matebenyovszky:fix/reveal-hidden-zone-header

Conversation

@matebenyovszky

Copy link
Copy Markdown

What does this PR do?

Hiding a zone's header is one double-tap on its tab strip (tree-group.tsx, hideHeaderDoubleTap). Getting it back is the problem: the hide takes the strip with it, and the strip is the only host of the zone menu — so the hidden zone has no tab, no ✕, and no "Show header". A zone of closeable tiles (a preview, the Browser) is then stranded on screen.

I hit this with two file previews open. Both zones had been double-tap-hidden, and there was no affordance left on either one: no tab, no ✕, and right-clicking the body reaches the pane's content, not the zone. The only escapes were ⌘W (works — closeFocusedSessionTab keys on isMainStripPane) and ⌃Tab (needs ≥2 tabs in the zone, so it does nothing for a lone preview). Neither is discoverable from a zone that shows no chrome at all.

model.ts has documented the way out since the flag was added:

 * Header hidden entirely (double-click the header to hide, double-click the
 * zone's top edge to bring it back).

The top edge was never implemented. This PR implements it.

Relationship to #81638 (open, same trap): that PR restores the workspace zone's bar from the sidebar session-row menu — isWorkspaceTabBarHidden() is workspace-specific, and the sidebar row is the surface it hangs off. A preview or Browser zone has no sidebar row, so it stays stranded. This PR is zone-agnostic: every zone carries its own way back, on itself. They're complementary — #81638 gives the chat case a discoverable menu item, this gives every other zone any affordance at all. If maintainers would rather have one mechanism, I'm happy to adapt.

Also adjacent: #79520 (session-tile self-heal) and #75848 (which established that an explicit hide persists — deliberately kept intact here).

Related Issue

No existing issue — found while using the desktop app. Happy to file one if preferred.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • apps/desktop/src/components/pane-shell/tree/renderer/lone-header.ts — add showRevealEdge() next to forceLoneHeaderForPanes(); both answer "when does a zone show chrome it would otherwise go without", so they belong together and stay unit-testable. Gated on an explicit hide (headerHidden === true): a contextual hide (lone side chrome, a headerVeto page) is not a state the user chose and lifts on its own, so it keeps its clean edge.
  • apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx — render a 6px reveal strip at the top of an explicitly-hidden zone. Double-click reveals (mirroring the gesture that hid it); Enter/Space reveal from the keyboard; right-click opens the zone menu, so Close/Show header are reachable without the strip.
  • apps/desktop/src/components/pane-shell/tree/renderer/lone-header.test.ts — 4 tests for showRevealEdge (explicit hide, contextual hide, minimized, empty).

Two things I deliberately did not do:

  • Not an overlay. The strip is a flex child, so 6px of content is laid out below it rather than 6px of the content's own top row being swallowed — a preview's SOURCE/DIFF/Edit row sits at the very top of its pane and must stay clickable.
  • Not a ZoneMenu around the body. Wrapping the body would put the menu everywhere the comment in tree-group.tsx already claims it lives, but toggling that wrapper adds/removes a ContextMenuTrigger above the pane layers and remounts the whole zone — a chat transcript would lose its scroll position on every header toggle.

No i18n additions: t.zones.showHeader already exists in every locale.

How to Test

  1. Open any preview (click a file in the file tree) so it docks as its own zone.
  2. Double-click its tab strip — the header disappears. There is now no tab, no ✕, and right-clicking the body gives the pane's own menu.
  3. Before this PR: the zone is stuck; only ⌘W (hover it first) gets rid of it.
  4. After: a 6px strip sits at the top of the zone. Double-click it → the header, tab and ✕ are back. Right-click it → the zone menu (Show header / Close / Close others…).
  5. Tab to the strip and press Enter → same reveal.
  6. Repeat with the chat zone and with a minimized zone (the latter shows no strip — a minimized group is its header).

Checklist

Code

  • I've read the Contributing Guide

  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)

  • I searched for existing PRs to make sure this isn't a duplicate — see the fix(desktop): restore hidden tab bar from sidebar row menu #81638 comparison above

  • My PR contains only changes related to this fix/feature (no unrelated commits)

  • I've run the desktop workspace's CI scripts. check:lint: 0 errors. check:test:ui: the pane-shell scope this change lives in is fully green (18 files / 104 tests). This change is renderer-only and touches no Python, so pytest tests/ is unaffected.

    For transparency: on my Windows box check:test:ui and check:test:desktop:platforms also report failures in unrelated files — use-prompt-actions/utils and tool/fallback-model (number formatting under a non-English system locale), and the ssh-* / update-relaunch / windows-hermes-path electron tests (bash -n and POSIX venv layout). I verified these are pre-existing: with my change stashed, the same tests fail identically. They should pass on CI's ubuntu-latest. Flagging in case they're news to anyone — happy to open a separate issue.

  • I've added tests for my changes

  • I've tested on my platform: Windows 11 (26100), built with npm run build + electron-builder --dir and exercised in the packaged app

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — the model.ts comment already described this behavior; the code now matches it
  • N/A — no config keys
  • N/A — no architecture or workflow change
  • I've considered cross-platform impact — renderer-only (React/Tailwind), no platform-specific code paths
  • N/A — no tool behavior change

Screenshots / Logs

Before: two preview zones (INDEX.HTML, .ENV) with no tab strip and no way to close them.
After: the reveal strip restores the header, tabs and ✕ on both.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

fix(desktop): give a header-hidden zone a way back

  • The 6px-tall full-width strip is the only interactive surface and offers no visible affordance — a sighted user cannot know the edge exists until hovering it, and a single click is intentionally a no-op. Consider a subtle persistent visual cue (a 1px accent line, or a faint handle on hover) so the way back is discoverable without reading the changelog. tree-group.tsx ~line 600-627.
  • Handler duplication: the reveal logic is split across onDoubleClick and onKeyDown (Enter/Space). A native <button> already fires click on Enter/Space with event.detail === 0; dispatching from a single onClick on detail === 0 (keyboard) or detail === 2 (double-click) would remove the duplicated keydown branch and the preventDefault dance.
  • Accessibility/target size: 6px is far below the ~24px minimum interactive target. Consider enlarging the hit area (e.g. padding or a taller interactive band that doesn't overlap content). The aria-label + title are present and the t.zones.showHeader i18n key exists — good.
  • The showRevealEdge predicate is well tested in lone-header.test.ts, but the new render wiring in tree-group.tsx (edge only when revealEdge, handlers fire) has no component test. A render test asserting the strip appears iff showRevealEdge(...) is true would lock the behavior.

@Enough1122 Enough1122 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One real edge-case regression plus an accessibility gap on the new reveal strip (see inline).

// the flag was added: double-click reveals the header, right-click opens the
// zone menu. A flex CHILD, not an overlay — 6px the content is laid out
// below, rather than 6px of the content's own top row swallowed.
const revealEdge = showRevealEdge({ headerHidden: node.headerHidden, isEmpty, minimized: node.minimized })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strip keys only on the EXPLICIT node.headerHidden === true, but the header itself is also suppressed by the contextual paneChrome(active).headerVeto (line 255) — which applies to the workspace pane when it shows a full page (skills/messaging/artifacts). Sequence: hide the workspace header via the zone menu → navigate to a full-page route (headerVeto active) → the strip still shows → double-click it → setTreeGroupHeaderHidden(node.id, false) clears the explicit flag → revealEdge becomes false (strip disappears) while headerHidden is STILL true via the veto → the zone loses both the header and the only affordance to restore it (the strip's right-click zone menu), and back on chat a lone uncloseable workspace pane stays header-hidden by the contextual default. Before this PR the zone was stuck with no affordance either, but the strip now actively removes the one path back after a double-click. Suggest gating the strip on the EFFECTIVE header-hidden state minus the veto, or clearing the veto (leaving the page) before revealing; also, the button has no onClick — only onKeyDown — so assistive-tech activation that dispatches a click gets no response.

Hiding a zone's header takes its tab strip with it, and the strip is the
only host of the zone menu -- so an explicitly hidden zone has no tab, no
close button and no menu. A zone of closeable tiles (a preview, the
Browser) is then stranded on screen: only Cmd/Ctrl+W (hover it first) or
a layout reset gets rid of it, and neither is discoverable from a zone
showing no chrome at all.

model.ts has documented the way out since the flag was added -- "double-
click the zone's top edge to bring it back" -- but the edge was never
implemented. Implement it: double-click (or Enter/Space) reveals the
header, right-click opens the zone menu, so Close is reachable without
the strip.

The strip is a flex child rather than an overlay, so 6px of content is
laid out below it instead of the content's own top row being swallowed
-- a preview's SOURCE/DIFF/Edit row sits at the very top of its pane. It
carries a faint persistent tint so it can be found without hovering.

Only an EXPLICIT hide qualifies. A contextual hide (lone side chrome) is
not a state the user chose and lifts on its own. A full-page view's
headerVeto is excluded too: it suppresses the header independently, so
revealing there would be a dead gesture -- clearing the flag cannot
bring a vetoed header back, and it would spend the strip (and the zone
menu it hosts) on that page for nothing. The veto lifts by itself and
the flag is still set underneath, so the edge returns then.

One handler reads `event.detail`: 0 for a keyboard or assistive-tech
activation, 2 for the second click of a double-click, so a stray single
click aimed at the content row below is ignored. Listening on dblclick +
keydown instead would miss the synthetic click AT dispatches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@matebenyovszky
matebenyovszky force-pushed the fix/reveal-hidden-zone-header branch from 60a1056 to 2816893 Compare August 16, 2026 17:12
@matebenyovszky

Copy link
Copy Markdown
Author

Thanks — the veto interaction is a real bug and I've fixed it. Point by point.

1. headerVeto (the inline comment) — fixed. You're right that the strip keyed only on the explicit flag. showRevealEdge now takes headerVetoed and returns false while a full-page view is suppressing the header, so the dead gesture can't happen. The veto lifts on its own and the flag is still set underneath, so the edge comes back then.

One correction to the scenario, though: the zone does not stay header-hidden back on chat. setTreeGroupHeaderHidden(id, false) writes an explicit false, and node.headerHidden ?? (shown.length <= 1 && !forceLoneHeader) short-circuits on it — the contextual default never applies, so the header returns as soon as the page closes. The defect was narrower than described: while the page was open the strip was a dead affordance, and double-clicking it spent the strip (and the zone menu it hosts) for nothing on that page. Still worth fixing, and it is.

2. No onClick — fixed, and it subsumes the handler-duplication note. Now a single onClick reading event.detail: 0 for a keyboard or assistive-tech activation, 2 for the second click of a double-click, and a stray single click (1) aimed at the content row below is ignored. That drops the keydown branch and the preventDefault dance, and fixes AT activation, which dblclick + keydown genuinely missed.

3. Discoverability — partly taken. The strip now carries a faint persistent tint (bg-(--ui-accent)/15) that deepens on hover, so it's visible without hovering rather than only on hover.

4. Target size — pushing back, with reasoning. 24px is the right default, but the only ways to get there are eating 24px of content in every header-hidden zone or overlaying the content — and overlaying is what the flex-child layout deliberately avoids, because a preview's SOURCE / DIFF / Edit row sits at the very top of its pane and must stay clickable. WCAG 2.5.8 exempts a target whose function is available through an equivalent control on the same page; here the same zone menu is on the strip's right-click, and Close is also on ⌘W and the tab strip once revealed. I'd rather keep the small target than make the fix cost 24px of every hidden zone, but I'll happily change it if maintainers disagree.

5. Render test — added. reveal-edge.test.tsx renders the real TreeGroup (same harness as tool-panel-close.test.tsx) and covers: the strip appears on an explicitly hidden zone and the tab strip is gone; it stays away while the header is visible; it stays away under a veto (the regression above); double-click clears the flag and a single click does not; a detail: 0 activation clears it. The veto case is a valid differential — it differs from the passing case only by veto: true.

Verified on Windows 11: typecheck 0 errors, lint 0 errors, and the whole pane-shell scope green (23 files / 121 assertions).

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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants