Skip to content

Drag thread tabs to arrange the channel workspace - #907

Merged
morgmart merged 5 commits into
mainfrom
morganm/tabs-pointer-drag
Oct 13, 2026
Merged

morgmart merged 5 commits into
mainfrom
morganm/tabs-pointer-drag

Conversation

@morgmart

@morgmart morgmart commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What this does

Thread tabs can be dragged to reorder them or move them between the main and side panes. A floating title pill and insertion line reuse the Panel Swap treatment. The whole pane header accepts a drop; dragging below the header toward the right edge can reveal an empty or closed side pane.

This is slice 4, rebased onto main containing merged #904–906 and #850. Short navigation tabs now hug their content up to the existing maximum width instead of reserving a fixed empty area.

Why it matters

Arranging a few open threads should be a direct gesture, not another set of move buttons. Generous targets make the destination discoverable without adding permanent chrome.

How it works

The existing tab store commits placement only on release. Escape, cancellation, window blur, resize and visit change cancel the proposal. Closed-pane drops preserve hidden tabs; drafts and readers remain mounted. Drag feedback redraws independently of the conversation page, and long strips scroll at their edges only during the gesture. The permanent channel tab and non-thread detail tabs are not draggable. Touch keeps native tab-strip scrolling; keyboard placement remains available.

Review repairs and verification

  • Test-only follow-up 1613837dced7f140a10a5bd418dbe91761a5642d addresses Wes’s deterministic-ordering comment: a real mounted ChannelTimeline now controls the independent virtualizer’s public viewport measurement at zero versus measured, then prepends older data on the same component. Removing the guard fails the zero case; the measured case preserves normal compensation. All 130 timeline tests and typecheck pass; independent test-honesty review is clear. The browser journey remains for native integration, not proof of exact measurement timing. No production code changed in this follow-up.

  • Previous production head cea365086 passed all applicable hosted CI, including the formerly failing WebKit shard (Windows validation conditionally skipped). Final head 1613837dc passed all applicable hosted checks (Windows native conditionally skipped), required approval remains, and Wes’s final exact-head review states “No further changes requested.” Final comment sweep is clear; Morgan authorized merging at these gates.

  • CI rendering repair at production head cea3650863d3f4d6a94dadda64a0c36a2b40ea80: WebKit could display a blank timeline when an older finite response arrived after a live row but before the first viewport measurement. The store contained the messages; premature prepend compensation froze Virtua’s initial empty range. ChannelTimeline now enables shift compensation only after Virtua has measured its viewport. No dependency patch, timer, retry or new lifecycle owner.

  • The existing Me recipient journey now holds the real finite response to exercise this ordering. It failed before the fix in WebKit and passes afterward; all signing/admission assertions remain. No additional browser cases or relaxed waits/assertions. Typecheck, 128 timeline/installed-Virtua tests, and all 44 cases across Me, history-loading, live and initial-position files pass in Chromium/WebKit (1.7m, local macOS). Temporary instrumentation and probes were removed; the installed dependency was restored byte-for-byte. Independent focused review found no blocker.

  • Mandatory commit/push hooks passed. The original blank-timeline failure was repaired without a hosted rerun, dependency change, test retry or assertion relaxation.

  • Drag fixes at 5e7a1ee54, retained in the final head above, base 55b71bf1d. Narrow overlaid side panes now win actual pointer hit testing for both insertion and tab-strip autoscroll; an underlying main header cannot take their drop. Existing geometric tolerance remains when the pointer is outside registered hosts.

  • A current navigation AbortSignal now cancels the existing drag owner immediately on navigation, including same-channel Back/Forward. Both Channels and Sessions supply their existing signal; this does not broaden the channel-lifetime guard or add another navigation/gesture owner.

  • Two browser scenarios added (four engine cases), no cases removed: native overlay hit testing and held pointer capture through actual browser history cannot be established by jsdom. Each failed for the intended defect before repair in Chromium/WebKit, then passed afterward. Assertions check final pane/order and restored selection, not only cue dismissal.

  • Typecheck and all 30 cases across complete navigation-thread-history, thread-tab-moves, channel-settings and buzz-links files pass in Chromium/WebKit (1.1m, local Apple Silicon macOS). Existing cases cover full-header insertion, closed/empty-side reveal, Escape cancellation and retained reader/editor/draft identity.

  • Independent focused rebase/remedy/test review found no blocker. Mandatory commit/push checks passed; remote head verified. Final hosted CI and Wes’s exact-head no-further-changes confirmation passed under Morgan’s explicit merge authorization. No manual CI rerun, full scan, new native build or thread resolution.

  • Morgan previously tried desktop dragging and approved the shared Panel Swap treatment. This repair changes no tokens, styles or geometry; new narrow-window pointer behavior is browser-verified, not fresh native/human acceptance. Sessions consumes the same cancellation owner but has no separate native-drag acceptance claim.

Earlier CI repairs retained

Drag feedback styles load through the shared stylesheet, not an eager UI import that changes CSS-layer order. Shared avatar alignment stays stable through artwork fallback. Channel-settings tests enforce the approved maximum tab width rather than the removed fixed width. These repairs are retained; broad fresh CI owns their current cross-platform validation.

Stack

Slice 4 of 6. Based on main including merged Placement setting #906. Next: Return highlight #908. Later slices are restacked individually after their predecessors merge.

@morgmart
morgmart marked this pull request as ready for review October 12, 2026 19:13
@morgmart
morgmart requested a review from a team as a October 12, 2026 19:13

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two changes needed: make overlapping-header hit testing follow the visible pane, and cancel an in-flight drag when navigation changes within the same channel. Details and minimal fixes are inline.

Star Lord’s automated source review via Wes’s account; head 24ad1e8d21c5c18ed205e36fe5e645cccabc85a6, base 27ff5cdd07594a141db940ddd61be97387bc4088. Source-only: no app or tests executed. The public description has no attached images. The current-head CI snapshot reports CI required failing and a WebKit shard cancelled; causes were not diagnosed. This is a non-blocking COMMENT, not an approval.

Comment on lines +108 to +110
for (const [pane, host] of hosts.current) {
if (!visible(host) || !latest.current.canMove(active.id, pane))
continue;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 — Choose the frontmost header when pane bounds overlap. In the supported narrow-window/container layout, Channels.module.css:521–559 overlays the side pane (position: absolute; z-index: 2) without shrinking the main pane. Both headers pass visible() and this loop returns the first geometric match; normal main-then-side registration therefore targets the covered main header even when the pointer is over the side header. Reordering an own-channel side thread can unexpectedly move it to main instead. Resolve the actual frontmost header (or explicitly prioritize the overlay) before computing insertion, and cover a narrow desktop pointer drag. This preserves the design guide’s existing narrow-overlay behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 5e7a1ee. The existing drag owner now uses frontmost pointer hit testing for both insertion and tab-strip autoscroll, so a visible side overlay wins over the covered main header. Forgiving geometric bounds remain when the pointer is outside registered hosts.

Added an actual narrow-overlay pointer reorder regression. It moved the tab into main before the fix in both Chromium and WebKit; now it preserves the side pane and verifies the reordered tab IDs. All 30 affected browser cases, typecheck and mandatory push checks pass. Fresh hosted CI is running; leaving the thread open for verification.

Comment on lines +396 to +397
const drag = useTabDrag({
visit,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 — Bind drag cancellation to the navigation visit, not only the channel. visit above is memoized only by [queries, currentId], so useTabDrag’s effect cleanup does not run for Back/Forward between thread visits in the same channel. ChannelsPage keeps the workspace mounted and enterVisit restores selection without removing the dragged tab. If history changes while the pointer is held, releasing still commits the old gesture and moveTab selects its thread, overwriting the newly restored selection. Pass an actual navigation-entry/attempt cancellation token into the drag owner (separately from the channel lifetime) and verify that same-channel history cancels without committing a move.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 5e7a1ee. Both page owners now pass their existing navigation AbortSignal separately from the channel lifetime. Aborting navigation immediately cancels the held gesture; changing the signal retires its listeners, and already-aborted navigation cannot start a new drag.

The real browser regression goes Back between two same-channel threads while holding a drag, checks cue retirement before pointer release, then verifies unchanged placement/order and the restored selection. It failed before the fix and passes afterward in Chromium and WebKit. The full four affected browser files pass all 30 cases, and independent focused review found no blocker. Leaving this thread open for verification.

@baxen baxen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Approving on behalf of baxen. They tested the full stack from #909 on desktop: tab drafts surviving channel switches, moving tabs between panes, reusing an already-open tab, the Main pane setting, resuming the cursor on tab return, the return highlight, and same-thread links.

@morgmart
morgmart force-pushed the morganm/tabs-placement-preference branch from 27ff5cd to 0c05165 Compare October 12, 2026 23:22
Base automatically changed from morganm/tabs-placement-preference to main October 12, 2026 23:38
morgmart and others added 3 commits October 12, 2026 16:38
Use full header insertion targets and right-edge separation, retain content on drop, and size short tabs to their content.

Co-authored-by: Carl <goose@block.xyz>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Establish host CSS layer order before drag rules; keep compact navigation width expectations aligned with content-sized tabs, and keep artwork fallback changes from shifting the avatar baseline.

Co-authored-by: Carl <goose@block.xyz>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Use frontmost hit testing for overlay insertion and edge scrolling. Cancel the existing gesture on navigation abort, including same-channel Back/Forward. Cover both with real pointer regressions in Chromium and WebKit.

Co-authored-by: Carl <goose@block.xyz>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
@morgmart
morgmart force-pushed the morganm/tabs-pointer-drag branch from 24ad1e8 to 5e7a1ee Compare October 12, 2026 23:44

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No further changes requested. The two findings from the previous review are addressed: frontmost-pane hit testing governs insertion/autoscroll, and navigation aborts cancel held drags within the same channel; I found no additional actionable defect in the repair or regression-test source.

Star Lord automated source review via Wes’s account; head 5e7a1ee54c0b688d6c55fc90735f7fa534fff032, base 55b71bf1db0ff6f5700dafc0d9f347a7d52ec8ed. Source-only: no tests or app executed; CI was incomplete at the inspected snapshot, and fresh native/human acceptance remains unverified.

An older finite head can arrive while the first live row mounts. Do not request Virtua shift compensation until it has measured a viewport; otherwise WebKit can freeze the initial empty range. Hold the real head response in the existing Me recipient journey to cover this ordering without weakening admission assertions.

Co-authored-by: Carl <goose@block.xyz>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
@morgmart

Copy link
Copy Markdown
Contributor Author

🤖 The CI failure exposed a real WebKit initialization race, now fixed in cea3650: an older finite head arriving before the first viewport measurement could freeze the virtual list’s initial empty range. The existing timeline owner now waits for measured geometry before requesting prepend compensation.

The existing Me test holds the real finite response to exercise that order; it failed against pre-fix production in WebKit and passes after repair. All 128 timeline tests, all 44 affected Chromium/WebKit browser cases, typecheck and push hooks pass. No dependency patch or relaxed assertions. @wesbillman, please include this focused follow-up in the final no-further-issues review. Fresh CI is running; merging still waits for both gates.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One change requested: make the new cold-viewport regression deterministic; the inline comment identifies the missing ordering boundary and a minimal test-layer fix. I found no additional production defect in the viewport guard; the prior drag-fix review remains unchanged.

Star Lord automated source review via Wes’s account; head cea3650863d3f4d6a94dadda64a0c36a2b40ea80, base 55b71bf1db0ff6f5700dafc0d9f347a7d52ec8ed. Source-only: no tests or app executed; browser, JavaScript and Rust checks were still running at the single CI snapshot, and native/human acceptance remains unverified.

`[Open Beta](buzz://channel/${other})`,
);
await expect.poll(() => readHead).toBe(true);
releaseHead();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 — Make the cold-viewport regression ordering explicit. readHead only proves route.fetch() completed; app.append() only queues the relay publication (fixture.mjs:2420–2429), not its receipt/render in the page. Releasing here therefore does not establish that the live row mounted before the finite head or that Virtua still has a zero viewport. If the head wins delivery, there is no prepend; if viewport measurement wins, the previous shift={prepend} also works. The new regression can consequently pass with the production fix removed. Keep the browser journey, but add a mounted timeline regression controlling viewportSize at zero versus measured and asserting the corresponding prepend shift value, or explicitly gate those browser boundaries. This satisfies the repository's deterministic-ordering rule without adding sleeps.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Addressed in test-only follow-up 1613837. Added a real mounted ChannelTimeline regression that controls the independent virtualizer’s public viewportSize at zero versus measured and prepends the older row on the same mounted instance. React owns actual refs and effects; no hooks are mocked and no production seam was added.

Removing the production guard deterministically fails the zero-viewport case while the measured case passes; restoring it passes all 130 timeline tests and typecheck. The original browser journey remains, with its comment corrected so it does not claim guaranteed measurement ordering. Independent focused test review is clear. Production is unchanged from cea3650, whose hosted CI passed; fresh CI is running on this head. Leaving the thread open for verification.

Exercise the real timeline against zero and measured Virtua viewport states, retaining the native browser journey for integration. Removing the production guard fails the zero-viewport case deterministically.

Co-authored-by: Carl <goose@block.xyz>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No further changes requested. The two mounted timeline cases address the prior ordering finding by explicitly controlling zero versus measured viewport size across a same-instance prepend; the existing browser journey remains, with no new browser cases or production changes in this follow-up.

Star Lord automated source review via Wes’s account; head 1613837dced7f140a10a5bd418dbe91761a5642d, base 55b71bf1db0ff6f5700dafc0d9f347a7d52ec8ed. Source-only: no tests or app executed; CI was incomplete at the single snapshot, and native/human acceptance remains unverified.

@morgmart
morgmart merged commit ebadc94 into main Oct 13, 2026
23 checks passed
@morgmart
morgmart deleted the morganm/tabs-pointer-drag branch October 13, 2026 00:41
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.

3 participants