Skip to content

feat(tui): resizable, collapsible sidebar rail - #46818

Open
iceteaSA wants to merge 18 commits into
anomalyco:devfrom
iceteaSA:sidebar-rail
Open

iceteaSA wants to merge 18 commits into
anomalyco:devfrom
iceteaSA:sidebar-rail

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 2, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #40086

Stacked on #46117 (sidebar_width config). The first commit here is that PR's tip; review from 98ab5979d4 onward. I'll rebase once #46117 lands.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds a 2-column rail between the content and the sidebar: a divider with a handle glyph at the top — when expanded, when collapsed. Drag the rail to resize; click it (or <leader>b) to collapse or expand. Width and state persist across restarts.

  • Three sidebar states: auto (shown when the terminal is wide), collapsed (rail only), hide. Click and <leader>b both toggle autocollapsed and never write hide; a separate session.sidebar.hide command does. In narrow terminals the toggle only opens the overlay and no longer persists state.
  • Drag resize: mouse down on the rail, drag anywhere. Width follows the cursor live and is written once on release. A collapsed rail drags open from the minimum width, not from the stored one. Bounds are the existing clampSidebarWidth (20 … min(100, cols − 40)).
  • Keyboard: session.sidebar.width.grow / .shrink (±4 cols) / .reset (removes the override), all unbound by default and reachable from the palette.
  • Config: sidebar: "auto" | "collapsed" | "hide" in tui.json as the initial state; the persisted KV value wins after that. sidebar_width from feat(tui): make the sidebar width configurable #46117 is the default the drag override sits on top of.
  • Mouse off (OPENCODE_DISABLE_MOUSE / mouse: false): the rail and glyph still render, no handlers are attached, keyboard commands work.

Why it's built the way it is:

  • Drag handlers live on the row that contains both the content and the sidebar, not on the rail. OpenTUI binds pointer capture to whatever is under the cursor on the first drag event (one cell over), so a narrow rail is never the captured target. mousedown is on the rail; drag/drag-end bubble up from wherever the cursor lands.
  • The width override uses kv.get/kv.set and a new kv.delete, not kv.signalsignal re-seeds its default into the store when the key is missing, which would make reset a no-op.
  • The rail is 2 columns because a width={1} border box places its child one column past itself; a 1-column rail put the glyph off-screen at the right edge. The rail+sidebar row has flexShrink={0} because wrapping the sidebar in a row made it yoga-shrinkable under a wide content column. Both have position-asserting tests that render the production geometry.
  • A no-motion click toggles; a moved drag persists. OpenTUI emits drag-end only after a capture and may then re-dispatch the up to the rail, so drag-end clears the gesture first and the rail's up handler is a no-op without one — one state write per click, one width write per drag.

How did you verify your code works?

packages/tui: bun test 251 pass / 1 skip, bun typecheck clean. packages/opencode typecheck clean.

Ran the TUI from source over a PTY at 140×40 and 100×40 with injected SGR mouse events and checked kv.json after each step: drag both directions (one write per gesture), collapse and expand by click and by <leader>b, collapsed drag from the floor, restart persistence, reset (key removed, layout snaps back live), grow/shrink, mouse disabled (rail renders, drag inert, keyboard works), narrow overlay with no rail and no persisted state.

Screenshots / recordings

Expanded, 140 columns (the rail is the │◂ left of the sidebar):

  ┃  ...content...                                             │◂ Context
  ┃                                                             │  0 tokens

Collapsed:

  ┃  ...content...                                                                          │▸
  ┃                                                                                         │

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

The sidebar width was hardcoded to 42 in two independent places. It is now a single clamped source fed by the sidebar_width config key. The clamp enforces a minimum of 20 for legibility, a terminal-width-derived maximum of terminal width minus 40 to preserve main content, and a hard cap of 100.
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.

[FEATURE]: Add persistent ui.sidebar.enabled config to disable Context sidebar

1 participant