Skip to content

[CPLAT-12912] Resizable panes with tuikit split - #181

Merged
gavin-jeong merged 3 commits into
masterfrom
CPLAT-12912-split
Oct 21, 2026
Merged

gavin-jeong merged 3 commits into
masterfrom
CPLAT-12912-split

Conversation

@gavin-jeong

Copy link
Copy Markdown
Collaborator

JIRA: https://sendbird.atlassian.net/browse/CPLAT-12912

What

tuikit goes from c19aa4f to 5b7fabe, which adds the split package (keyolk/tuikit#11). ccx resized its panes three ways: [ ] ±5 (15-85), a mouse drag that grabbed a cell of each pane (20-80), and set:ratio N; the config and plugins views clamped to 20-80 on their own. All of them now go through one split.Model on App, shared by the sessions, conversation, config and plugins splits, as the old splitRatio was.

This branch starts from master and does not depend on #180.

The split

Before After
Ratio splitRatio int percent split.Model: fraction 0.15-0.85, default 0.35, step 0.05
Minimums list ≥ 30 (ListWidth), preview ≥ 20 (adjustSplitRatio only) MinA: 30, MinB: 20, enforced on every write and every layout
Widths ListWidth/PreviewWidth arithmetic, plus ~20 copies of width - list - 1 SplitPane.layout → split.Model.Layout; ListWidth/PreviewWidth read it
Divider raw ANSI │ DrawDivider: │, heavy ┃ while dragging (visible under NO_COLOR)
Narrow list alone under 40 columns or 10 rows same rules (Collapse: 40, minSplitHeight); a width that cannot fit 30 + 1 + 20 also shows the list alone, full width

SplitPane.layout is the only source of widths: Render, renderSessionSplit, the drag-selection panes (splitRects) and the click/wheel hit tests (inPreview) all read the same rects.

Mouse

  • handleSplitMouse runs first in handleMouse, before selection, overlays and clicks. A press on the divider starts a drag and is consumed; motion moves the divider; release ends it. A double click on the divider resets it.
  • tryStartDragResize, handleDragResize, the dragResizing field and its guard in handleSelectionMouse are removed.
  • The old grab zone (±1 cell into each pane) is not kept (Grab: 0). Those cells belong to the panes, so a drag that starts there selects text. TestDragNextToDividerResizesInsteadOfSelecting asserted the opposite and is now TestDragNextToDividerSelectsInsteadOfResizing.
  • While an overlay or copy mode owns the mouse, or the preview is closed or zoomed, the split gets an empty area: it never takes a press, and a drag already in progress still ends on release.

Keys

Key Action Where
< / > shrink / grow the list every split view (new: session.split_shrink, session.split_grow)
= reset to 35% every split view (new: session.split_reset)
[ / ] shrink / grow (kept as aliases) only where they already resized; config page cycling and inspector facet cycling are unchanged
  • The defaults come from split.Bindings. keys.Conflicts stays clean, and no other binding in any scope uses <, > or =, so nothing was rebound.
  • At a bound, </> show "Split at its limit" instead of doing nothing silently.
  • Footer: <>= resize, last in priority, where []:resize was. ? overlay: a < / > / = row in the sessions list, sessions preview, conversation, inspector, config and plugins help.
  • set:ratio N is Set(N/100), clamped like any other write.

Saved state

  • ui.json saves {"split": {"ratio": 0.4}}.
  • split_ratio (a percentage) from an older ui.json or config.yaml is read once, in 15-85, and dropped on the next save. split.ratio wins when both are present.
  • A ratio that is not a fraction (40, -1, 1.5) or an old percentage outside 15-85 keeps the default.
  • Saving needs no new code: Update already calls saveUIState after every message and quit flushes.

Other

  • restoreConvPreviewAnchor re-rendered the preview at a width computed from a fixed 50% ratio. It now re-renders at the width the preview is drawn at.
  • hexToRGB was only used for the raw-ANSI divider and is removed.

Tests

Positions come from the rendered View(): the divider glyph left of the preview text, never the layout code. Persistence tests use withUIStateDir (HOME, XDG_CONFIG_HOME, XDG_STATE_HOME in t.TempDir()).

  • TestSplitDividerDragMovesBoundaryEverywhere: press on the divider found in the frame, move, release. The divider shows heavy during the drag and moves in View. The list and preview selection panes match the new position. A click on old preview columns now focuses the list, and a click right of the new divider focuses the preview. Nothing selects.
  • TestSplitPanePressStillSelects, TestDragNextToDividerSelectsInsteadOfResizing, TestSplitDoubleClickOnDividerResets.
  • TestSplitKeysMoveDivider (>, <, =, ]), TestSplitClampHasNoDeadZone (40 × >, then one < moves at once, in both directions, minimums held), TestSplitKeysInConfigViewKeepPageCycling.
  • TestSplitKeysShowInHelpAndFooter, TestSplitKeysDoNotShadowBindings.
  • TestSplitKeysRewrapConversationPreview: after > and after a drag, the conversation preview shows every word, in order, none cut.
  • TestSetRatioCommandIsAPercentOfTheSplit, TestSplitNarrowTerminal (60 and 80 columns fit, 45 columns shows the list alone at full width).
  • TestSplitRatioSavedAndRestored, TestSplitGarbageInUIJSONFallsBackToDefault, TestSplitMigratesOldPercent (ui.json, config.yaml, new field wins).

Each new guard was checked with a compiling mutation, then restored:

Mutation Failing test
split runs after selection TestSplitDividerDragMovesBoundaryEverywhere, TestSplitDoubleClickOnDividerResets, TestDragOnSplitBorderStillResizes
selection panes laid out with the default ratio TestSplitDividerDragMovesBoundaryEverywhere
click hit test laid out with the default ratio TestSplitDividerDragMovesBoundaryEverywhere
no < / > arm TestSplitKeysMoveDivider, TestSplitClampHasNoDeadZone
no = arm TestSplitKeysMoveDivider
MinB 2 instead of 20 TestSplitClampHasNoDeadZone, TestSetRatioCommandIsAPercentOfTheSplit, TestSplitNarrowTerminal
divider never drawn heavy TestSplitDividerDragMovesBoundaryEverywhere
split body without the title-row offset TestDragPastPreviewBottomScrollsAndKeepsLines, TestDragInScrolledPreviewCopiesWhatIsShown
keys skip resizeAll TestSplitKeysRewrapConversationPreview
footer hint without = TestSplitKeysShowInHelpAndFooter
? row missing TestSplitKeysShowInHelpAndFooter
split_reset default not from the kit TestSplitKeysShowInHelpAndFooter
split.State not saved TestSplitRatioSavedAndRestored, TestSplitMigratesOldPercent
split_ratio not migrated TestSplitMigratesOldPercent
restore accepts any ratio TestSplitGarbageInUIJSONFallsBackToDefault

Two mutations survive because they are equivalent in what the user sees:

  • set:ratio clamping without the area: Layout clamps to the minimums when it draws, and the next key steps from the drawn position.
  • Not resizing the active pane during a drag: Render and syncSessionSplitLayout re-size from the same layout on the next frame. The call stays, as the old drag code had it.

gofmt -l is clean. go vet ./... passes. go test ./..., go test -race ./... (one run) and env -u LC_ALL -u LC_CTYPE LANG=ko_KR.UTF-8 go test ./... all pass.

Bump tuikit to 5b7fabe and replace ccx's three resize paths with one
split.Model shared by the sessions, conversation, config and plugins
splits (they already shared one ratio).

- The ratio is a fraction, 0.15-0.85, list >= 30 and preview >= 20
  columns, default 0.35. Every write clamps, so a key press always moves
  the divider by at least a cell or reports "Split at its limit".
- SplitPane.layout (split.Model.Layout plus ccx's closed/zoomed/short
  rules) is the one source of widths: Render, renderSessionSplit, the
  drag-selection panes and the click/wheel hit tests read the same rects.
  ListWidth/PreviewWidth now come from it, and the hand-written
  `width - list - 1` arithmetic is gone.
- Mouse: the split sees each event before selection and clicks. A press
  on the divider drags it (heavy glyph while dragging), a double click
  resets it. tryStartDragResize/handleDragResize and the dragResizing
  guard are removed. The old +-1 cell grab zone is dropped (Grab 0): those
  cells belong to the panes, so a drag there selects text.
- Keys: `<` shrink, `>` grow, `=` reset (session keymap split_shrink,
  split_grow, split_reset; defaults from split.Bindings). `[` `]` stay
  as aliases where they already resized, not where they cycle config
  pages or inspector facets. Conflicts() stays clean. Footer `<>=
  resize`, and a row in every split view's `?` overlay.
- `set:ratio N` is Set(N/100); the config/plugins 20-80 clamp is gone.
- ui.json saves {"split": {"ratio": r}}. split_ratio (a percentage) from
  an older ui.json or config.yaml is read once and dropped on the next
  save; garbage keeps the default.
@upwind-code-us

upwind-code-us Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Upwind Upwind Code Scan - ✅ Passed

0 newly introduced vulnerabilities · 0 resolved · 2 total in this PR vs master

Total breakdown: 🔶 2 High

View full analysis in Upwind Console

Scan completed in 7s

Scan history (2 scans)
Commit Scanned at New Resolved Net
0de3d2e 2026-10-21 15:50 UTC 0 0 0
3f2895f < 2026-10-21 23:07 UTC 0 0 0

Last scanned: 3f2895f · 2026-10-21 23:07 UTC

Conflicts:
- go.mod/go.sum: keep tuikit 5b7fabe (split), which contains master's
  b0beaa4 (selection v2, link); termenv drops as master did.
- drag_selection.go: master's sel.Update (auto-scroll ticks, offset
  scroll) without the dragResizing guard; the split sees divider
  presses first in handleMouse, so selection never gets them.
- drag_selection_test.go: master's tuitest.Reversed highlight check.
The OSC 8 title in the Outputs digest is followed by a detail cut to the
preview width; with the divider moved, that width must come from the
split, or the row overflows the pane and loses its ellipsis.
@Kairo-Kim Kairo-Kim added the auto-review/approved Auto-approved by the Slack auto-reviewer bot label Oct 21, 2026

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

@gavin-jeong
gavin-jeong merged commit 333c94b into master Oct 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review/approved Auto-approved by the Slack auto-reviewer bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants