feat(THI-246 PR 2/3): Split rail features — tree, divider, collapse, new-tab, drag-reorder#114
Merged
Merged
Conversation
The PR 1 rail rendered a flat Session → Pane tree. Add the repos-mode branch: - Reads `groupingMode` from settings (the global toggle wired up in THI-243). Sessions mode keeps the existing Session → Pane rendering. - Repos mode uses `groupByRepo()` over the sortPendingFirst'd window list and renders Repo → Window. Per the THI-246 spec rewrite, this is a two-level tree (worktree level collapses since `repo_key` is already worktree-grained), and a session can appear under multiple repos when it spans them. - New `session:` chip on each window row in repos mode so the user can still see the tmux session a pane belongs to without the extra hierarchy level. The chip is hidden in sessions mode where it would duplicate the group header. - Three new tests covering the repos-mode rendering, the session-chip conditional, and the repos-mode empty-state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .sb-divider was a 7px placeholder in PR 1. Wire it up: - ARIA-conformant `role="separator"` with aria-valuemin/max/now, tabIndex, and a focus-visible accent edge so it's keyboard-reachable. - Primary-button pointer drag updates a transient `dragWidth` state on each pointer-move (grid template reflows live) and persists the final width on pointer-up. Settings store gets written ONCE per drag instead of per frame. - Pointer capture so the cursor can leave the gutter without the drag dropping. - Keyboard nudge: arrow keys = ±10px, Shift+arrow = ±50px, Home = 200px (min), End = 460px (max). Matches the WAI-ARIA separator pattern. - Width clamps to [200, 460] on every code path (clampRail) — even a corrupt persisted value gets corrected at display time. - Four new tests: ARIA bounds, pointer drag, clamp ceiling/floor, keyboard nudges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New persisted setting `splitRailCollapsed: boolean` (default false). - Header gains a small +/- toggle that flips the setting; the title drops while collapsed so the 44px column has room. - Collapsed body: one centered status-toned dot per visible window, ordered the same way the expanded tree would order them (groupingMode is honored — repos mode flattens groupByRepo()'s output; sessions mode flattens the per-session sortPendingFirst result). - Clicking a dot expands the rail AND selects that pane in one settings write, so the user lands on a useful state. - Divider pointer/keyboard handlers no-op while collapsed. - Four new tests: toggle flips setting, 44px column + dot-only rendering, dot click expands+selects, drag-disabled-while-collapsed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each session group (and each real repo bucket) ends with a "+ New tab" affordance that opens the existing NewWindowOverlay targeting that session. - New optional `onNewWindow(session)` prop on SplitView. When omitted (legacy callers, tests), the rows are hidden entirely. - App.tsx wires it to the existing `setNewWindowSession` setter — same modal the other views use, so the rail piggybacks on the well-tested spawn flow. - Sessions mode: one new-tab row per session group, targeting that session. - Repos mode: one new-tab row per real repo bucket, targeting the FIRST window's session (the one window the group is anchored on). The "Other" bucket skips the row since there's no repo cwd to inherit. - Visual: same row metrics as a pane row; plus icon; dim text that brightens to accent on hover so it reads as an action rather than a destination. - Four new tests cover sessions-mode wiring, repos-mode session resolution, no-handler hiding, and the Other-bucket skip. THI-244-style cwd-aware spawning (new pane opens in the repo's path when launched from a repo bucket) is wired at the NewWindowOverlay layer, not the rail — out of scope for this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group head rows are now draggable in the expanded rail. Drop onto another head row to reorder. - Two new persisted settings: `splitRailSessionOrder: string[]` (sessions mode) and `splitRailRepoOrder: string[]` (repos mode). Each is a list of IDs in display order; live-new entries append, missing entries are filtered at render time, periscope-style. - New `applyOrder()` helper does a stable sort with persisted IDs ranked first and live-new appended. `reorderArray()` does the insertion math. - "Other" bucket in repos mode is pulled out before reorder and re-pinned last, so it never participates in user reorder. Its head row also has no draggable attribute. - Drag is no-op while the rail is collapsed (no head rows render). - Visual: dragged row dims to 0.4; a 2px accent line above/below the hover target marks the drop position. CSS pseudo-elements so layout doesn't shift mid-drag. - Identity travels on React state, not dataTransfer — no DOM-sibling walk to find the drag source. dataTransfer is still populated so Firefox accepts the drag start. - Per-window reorder within a group is intentionally not in this commit — out of scope for the PR 2 rail-features pass. - Six new tests: persisted-order application (both modes), "Other" stays last, visual states on dragStart/dragOver, drop persistence, Other-not-draggable, drag-disabled-while-collapsed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setNewWindowSession (the App.tsx setter the prop wires to) signs `(sessionId: string | null) => void`. The "+ New tab" commit had the prop typed as `(session: Session) => void`, which only landed because the App.tsx call site happens to be checked separately from the component file — the full tsc pass caught it on push. Update the prop type to `(sessionId: string) => void` and adjust both call sites (sessions mode passes `session.id`, repos mode passes the first window's session ID). Tests updated to expect a string instead of the Session object. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oard nudges work The pointer-down handler calls preventDefault() to suppress text selection during the drag. preventDefault() ALSO suppresses the default focus-on-click behavior — meaning after clicking the divider, the element never received focus and the keyboard handler couldn't fire. Restore focus explicitly via `e.currentTarget.focus()`. New test asserts the divider becomes document.activeElement after pointerDown and stays focused past pointerUp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e884fe2 to
0522258
Compare
30 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on PR #113 (THI-246 PR 1/3 — foundation). PR 1 shipped the layout
switcher + click-to-select persistence + skeleton; this PR fills in the
rail's interactive behaviors. Detail-pane work (inline xterm, sidebar)
stays for PR 3.
Five features, each isolated in its own commit so review can slice if
needed:
74ba0d4groupingMode(sessions or repos)847a4580a00f00189f199d71a6ad743ea8conNewWindowtakes a session ID (folds into 189f199 conceptually)What ships
Tree shape follows
groupingModegroupByRepo()(reuses THI-243's helper).Window rows get a small
session:chip so the user still sees whichtmux session a pane belongs to without the extra hierarchy level. A
session that spans repos shows window rows under each repo it touches,
per the per-window bucketing rule THI-243 settled on.
Divider resize
role="separator"witharia-valuemin/max/now,tabIndex={0},focus-visible accent edge.
dragWidthstate oneach pointer-move (live reflow) and persists the final width once on
pointer-up.
min/max.
Collapse to dot strip
splitRailCollapsed: boolean.+/-toggle in the rail header.ordered the same way the expanded tree orders them (groupingMode is
honored — repos mode flattens
groupByRepo(); sessions mode flattensper-session
sortPendingFirst)."+ New tab" rows
session in that bucket. The "Other" bucket skips the row.
NewWindowOverlay— same flow the other views use.onNewWindow(sessionId)prop onSplitView; rows hidewhen omitted.
Drag-to-reorder groups
splitRailSessionOrder: string[]andsplitRailRepoOrder: string[]. Each is the user's preferred displayorder; live-new entries append, missing entries are filtered at render
time (periscope-style "prefs are hints, never membership").
not
dataTransfer— no DOM-sibling walk to find the drag source.::before/::after.persisted order.
Test plan (automated)
npm test). 21 new tests inSplitView.test.tsxcover the five features.npm run typecheck: clean.npm run build: clean (verified via the pre-push hook).Manual smoke (needs a live session — only the user can run this)
Open the dashboard, switch to Split layout. The rail should already
show your session groups from PR 1.
Tree shape toggle
Repos. The rail re-groups: instead of one group per tmux
session, you see one group per git repo (each window goes into the
bucket of its own cwd's git toplevel).
session:chip after thewindow name. In sessions mode the chip is hidden (would be
redundant with the group header).
daily-driver session), it shows windows under each repo it touches
— not collapsed under just one.
pinned to the bottom of the rail.
Divider resize
changes to
col-resize. The hairline brightens to the accentcolor.
detail pane reflows.
460px— width clamps. Drag past200px— clamps.→and←. Rail widthnudges 10px per press. Hold
Shiftfor 50px nudges.Homejumpsto 200px;
Endjumps to 460px.Collapse
shrinks to a 44px dot column; the "Projects" label disappears.
(amber = waiting, cyan = running, gray = idle, green = done,
red = error).
pointer drag, no keyboard nudge).
"+ New tab"
+ New tabrow at the bottom that readsas a dim action; hover turns it accent.
+ New tabunder a session. The existing New windowoverlay opens, pre-targeted to that session. Submit creates the
pane.
+ New tabrow;the Other group does not.
+ New tabunder a repo group. The overlay openspre-targeted to the first window's session in that group. (Note:
cwd-aware spawning so the new pane opens in the repo's path is a
THI-244 follow-up — currently the new pane uses whatever the
session's default cwd is.)
Drag-to-reorder
grab.flight"). Cursor changes to
grabbing.below the target depending on where the pointer sits relative to
the row's vertical midpoint.
updates.
draggable and stays pinned last regardless of where you try to
drop other groups around it.
reordering in one mode doesn't affect the other.
rendered).
Cross-feature
state, selected pane, and group orders all persist across layout
swaps and reloads.
.sb-*surfacesshould adapt (no hard-edged white-on-white, no invisible drop
indicators).
Out of scope (don't test these)
reorder ships here).
+ New tabcwd inheritance (THI-244 follow-up at theNewWindowOverlaylayer).Sequencing
This PR is stacked on PR #113 (its base is
thibaultdody/thi-246-split-foundation,not
main). Merge order: PR #113 → this PR → PR 3.If you'd rather rebase onto
mainafter #113 lands, this branch has noconflicts with main today (the THI-244 merge already happened in PR 1's
branch).
🤖 Generated with Claude Code