Skip to content

fix(desktop): add responsive sidebar collapse#1585

Merged
esengine merged 3 commits into
esengine:mainfrom
T1anjiu:fix/desktop-responsive-sidebars
May 24, 2026
Merged

fix(desktop): add responsive sidebar collapse#1585
esengine merged 3 commits into
esengine:mainfrom
T1anjiu:fix/desktop-responsive-sidebars

Conversation

@T1anjiu

@T1anjiu T1anjiu commented May 23, 2026

Copy link
Copy Markdown

Summary

Add responsive, stage-based sidebar auto-collapse in the desktop chat UI so the center composer stays usable on narrow windows, and restore only the sidebars that were auto-collapsed when the window widens again.

Problem

When the desktop window is narrowed, the left session sidebar and right context panel can keep occupying fixed width. That leaves the middle chat column too narrow, which is especially bad for the composer footer: model selection and inline tools can overflow the input area and become clipped.

The issue lives in the desktop shell layout:

  • desktop/src/App.tsx owns sideCollapsed and ctxCollapsed state
  • desktop/src/styles.css defines the three-column grid layout and composer footer sizing
  • There was no responsive stage logic, so sidebar behavior depended only on manual toggles

Fix

  • desktop/src/App.tsx — Added width-stage detection for wide, compact, and narrow layouts.
  • desktop/src/App.tsx — Auto-collapses the right context panel first, then the left sidebar at a lower breakpoint.
  • desktop/src/App.tsx — Restores only sidebars that were auto-collapsed when the window becomes wide again.
  • desktop/src/App.tsx — Keeps manual sidebar toggles independent, so a user-closed sidebar is not reopened automatically.
  • desktop/src/styles.css — Switched the shell grid to CSS variables and added a smooth transition for sidebar width changes.
  • desktop/src/styles.css — Added composer footer overflow guards so the bottom toolbar does not spill out in narrow layouts.

Verification

Check Result
npm run build in desktop/ ✅ passes
git diff --check ✅ passes
git status --short --branch after commit ✅ clean

Closed #855

@esengine

Copy link
Copy Markdown
Owner

Solid PR overall — the stage state machine handles WIDE/COMPACT/NARROW transitions correctly, the auto-vs-manual tracking via refs is the right pattern, RAF-debounced resize is good practice, and the CSS variable refactor of .app columns is clean. One blocker to fix before this can land, plus two follow-on gaps worth handling in the same revision.

Blocker — overflow: hidden on .composer-foot clips the model-picker dropdown

The new rule

.composer-foot {
  ...
  overflow: hidden;
}

breaks the ModelMenu popup. In desktop/src/ui/composer.tsx:497-565:

  • .composer-foot is the flex row.
  • Inside it sits <div ref={modelWrapRef} style={{ position: "relative" }}> — this becomes the containing block for the popup.
  • <ModelMenu> renders with position: absolute; bottom: calc(100% + 6px); width: 260px, popping upward roughly 150–200px above the model-pill — i.e. outside .composer-foot's box.

With overflow: hidden on .composer-foot, that out-of-box area gets clipped to zero. The user clicks the model-pill and sees nothing — model switching becomes effectively invisible. The @// Popup (line 593) is rendered as a sibling of .composer-foot rather than a descendant, so it's not affected; only ModelMenu is the casualty.

The fix is small: just drop overflow: hidden from .composer-foot. The flex-overflow problem is already solved by the min-width: 0 chain you added on .composer-foot, .grow, and .model-pill. Keep the text-truncation rules at the leaf level (.model-pill span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } and same on .cf-btn .label if needed). overflow: hidden on the flex parent is a sledgehammer that breaks any absolutely-positioned descendant escaping the box.

Follow-on gap 1 — initial narrow startup isn't covered

if (prev === null) {
  responsiveStageRef.current = next;
  return;
}

First sync() call after mount records the stage and exits. If the user opens the app on a narrow window with the default open state (or with LS-restored open state), the composer overflows until they manually resize — which is exactly the screenshot scenario in #855.

Run the normal stage logic on first mount too:

if (prev === null) {
  if (next === RESPONSIVE_STAGE.NARROW) {
    if (!ctxCollapsedRef.current) { setCtxCollapsed(true); autoCtxCollapsedRef.current = true; }
    if (!sideCollapsedRef.current) { setSideCollapsed(true); autoSideCollapsedRef.current = true; }
  } else if (next === RESPONSIVE_STAGE.COMPACT) {
    if (!ctxCollapsedRef.current) { setCtxCollapsed(true); autoCtxCollapsedRef.current = true; }
  }
  responsiveStageRef.current = next;
  return;
}

Follow-on gap 2 — auto-collapse pollutes localStorage

sideCollapsed / ctxCollapsed are persisted by the existing effects. When auto-collapse flips them, that gets written too. After the user spends time on a narrow window, LS becomes sideCollapsed=1, ctxCollapsed=1. Next launch on a wide screen: both panels restore as collapsed, the first sync() doesn't reopen them (refs don't persist, so autoSideCollapsedRef.current is false on mount), and the user sees both sidebars hidden on a window that has plenty of room.

The cleanest fix is to skip the LS write when the change came from auto-collapse — gate the existing persistence effects on the auto refs:

useEffect(() => {
  if (autoSideCollapsedRef.current) return;
  localStorage.setItem("reasonix.sideCollapsed", sideCollapsed ? "1" : "0");
}, [sideCollapsed]);

(Same shape for ctx.) That way the persisted value reflects what the user explicitly chose, and the responsive layer never writes to LS.

Nit (optional)

RESPONSIVE_STAGE as a const-object + (typeof X)[keyof typeof X] works, but a string-literal union is half the code:

type ResponsiveStage = "wide" | "compact" | "narrow";

No strong opinion — keep if you prefer the named constants.

Push a fixup with the blocker fix (and ideally the two follow-on gaps) and I'll merge.

@esengine esengine merged commit e02d098 into esengine:main May 24, 2026
4 checks passed
esengine added a commit that referenced this pull request May 24, 2026
Follow-up to #1585. The responsive sidebar logic was split across six
refs (autoSideCollapsedRef / autoCtxCollapsedRef / suppressSide... /
suppressCtx... / sideCollapsedRef / ctxCollapsedRef) and a 70-line
useEffect that did everything inline. Each panel's "is this collapsed
because the user asked, or because the layout forced it" state was
encoded across two of those refs; the resize logic had to spell out
all 6 stage-transition combinations by hand.

Extracted the per-panel collapse into useAutoCollapse(persistKey):

  - collapsed: the boolean for CSS
  - toggle:    user-driven flip; persists to localStorage
  - requireCollapsed:  layout asks for collapsed — claims as auto if
                       the panel wasn't already collapsed
  - releaseCollapsed:  layout asks for expanded — restores only if WE
                       collapsed it (manual collapses stay)

The resize effect drops to one branch per stage:

  WIDE    → release both
  COMPACT → require ctx (only when entering from wider — coming from
            narrow we preserve a user's manual ctx-open); release side
  NARROW  → require both

Also: fixed the one-line CSS indent slip on .app[data-side-collapsed].

Behavior is unchanged — same five stage-transition outcomes that #1585
hand-coded, just expressed declaratively.

Co-authored-by: reasonix <reasonix@deepseek.com>
esengine pushed a commit that referenced this pull request May 24, 2026
…moved, persisted usage stats, plan dispatch gate

Headline themes:
- Desktop: bundle the CLI-hosted React dashboard, retire Tauri+Preact duplicate (#1418)
- Config: drop preset abstraction; flash/pro are direct model selections (#1657, #1630)
- Stats: persist cumulative usage to session meta + auto-restore on startup (#1667, #1680, #1643, #1628)
- Plans: editMode="plan" enforced at the ToolRegistry dispatch gate (#1681); step advance fix (#1629)
- Context: fold once at turn start, drop pre-flight + byte-ceiling (#1642, #1646); collapsible compacted card (#1649)
- Subagents: per-skill flash/pro override + Settings UI (#1632)
- Desktop polish: sidebar drag-resize (#1688), responsive collapse (#1585), copy/edit overlay + msg-history nav (#1645), Esc closes modal not turn (#1685), QQ tab isolation (#1672), DiffCard for edits (#1662), theme-aware highlighting (#1655), system events toggle (#1654/#1650), macOS TCC inheritance (#1614), dashboard.enabled (#1612)
- Dashboard polish: persistent session URL (#1586, #1589, #1599), theme-aware highlighting (#1664), IME confirm-enter guard (#1689), code-fence lang fix (#1677), vendor chunk split (#1587), markdown table h-scroll (#1562)
- TUI: Alt+S input stash/recall; static history isolated from input rerenders (#1635); legacy mouse drop (#1637, #1648); multi-edit gated in review (#1647)
- Diff: SplitDiff column border holds under CJK (#1686)
- MCP: workspace roots passed to servers (#1625); codeCommand honors mcpServers (#1603)
- Config plumbing: (baseUrl, apiKey) resolved as a tuple (#1658); stale model id self-heal (#1663)

See CHANGELOG for the full list.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows Reasonix Desktop UI 问题

2 participants