Skip to content

feat(desktop): refine sidebar session drag reordering - #43661

Open
OmarB97 wants to merge 1 commit into
NousResearch:mainfrom
OmarB97:feat/sidebar-drag-to-pin-sessions
Open

feat(desktop): refine sidebar session drag reordering#43661
OmarB97 wants to merge 1 commit into
NousResearch:mainfrom
OmarB97:feat/sidebar-drag-to-pin-sessions

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Problem

The first pass made sidebar rows draggable between Pinned and Sessions for pin/unpin, but the final accepted UX needs the whole row to behave like Claude/Codex session reordering:

  • drag from anywhere on the session row, not only the far-left handle,
  • reorder within Pinned or Sessions at an exact slot,
  • move between sections and choose the exact cross-section position,
  • show the rows shuffling naturally instead of a bold drop line, and
  • avoid the rapid snapback/flicker when the pointer pauses between rows or over the moving row.

Change

This updates the sidebar drag path around the native session row drag payload:

  • Pinned and flat Sessions rows now use the row body itself as the drag surface for reorder and cross-section placement.
  • Drop zones keep a stable insertion anchor instead of recomputing from only the immediate DOM target.
  • The hovered row has edge bands for intentional before/after movement; the middle band preserves the previous anchor, which prevents rapid back-and-forth shuffling while hovering.
  • The dragged row is ignored as a hover target, so animated previews do not chase themselves under the pointer.
  • Row chrome uses Motion layout animation for the preview shuffle instead of a bold separator/drop line.
  • Local drag-end resets the drop-zone active/anchor state, covering the stuck section-highlight path.
  • Virtualized sidebar rows receive the same drag lifecycle callbacks.
  • Flat Sessions only accepts unpinned same-section drags that actually belong to that Sessions list, so messaging rows do not get inserted into the saved Sessions order by accident.

The existing dnd-kit grab-handle path is left for grouped workspace ordering; the flat Pinned/Sessions row reorder path no longer requires the handle.

How to review

  1. Drag a row within Sessions by grabbing the row body/text area: neighboring rows should animate aside and the drop should persist at the hovered slot.
  2. Drag a row from Sessions into Pinned: the preview should appear at the exact hovered position, and dropping should pin it there.
  3. Drag a row within Pinned, including downward moves: dropping after a row should land immediately after that row.
  4. Drag a pinned row into Sessions: it should unpin and land at the exact hovered slot in the flat Sessions list.
  5. Pause the pointer between two rows or over the moving row: the preview should stay stable instead of rapidly flipping back and forth.
  6. End a drag outside the section or through an odd leave path: the section highlight should clear.

Testing

  • npx vitest run --environment jsdom src/app/chat/sidebar/use-session-drop-zone.test.tsx — 21 tests passed.
  • npm --prefix apps/desktop run typecheck — passed.
  • npx eslint src/app/chat/sidebar/index.tsx src/app/chat/sidebar/session-row.tsx src/app/chat/sidebar/use-session-drop-zone.ts src/app/chat/sidebar/use-session-drop-zone.test.tsx src/app/chat/sidebar/virtual-session-list.tsx — passed.
  • git diff --check — passed.
  • npm --prefix apps/desktop run build — passed. The build-stamp script warned that the worktree was dirty before commit, as expected during pre-commit verification.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Jun 10, 2026
@OmarB97

OmarB97 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a second commit folding in three follow-ups so the PR lands complete:

  • Positional drops — rows carry data-session-id and the new sessionDropAnchor() resolves the row under the pointer (top half = before, bottom half = after), so drag-to-pin inserts at the drop position in the pinned store and drag-to-unpin splices into the saved flat-list order. Header/empty-space drops and grouped views keep the previous append/recency behavior.
  • Pinned messaging rows rendersessionByAnyId now indexes the messaging slice (pinning one previously stored an unresolvable id that rendered nothing, a pre-existing context-menu bug too), and pinned rows are filtered out of their platform section so a pin moves the row instead of duplicating it.
  • Discoverability — the empty-Pinned hint becomes "Drag a chat here, or shift-click to pin" across en/zh/zh-hant/ja.

Verified the same way as the base commit (live Chromium drag simulation + 13 unit tests; tsc/eslint clean on this branch).

@OmarB97

OmarB97 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Third commit: pin-aware count labels and pagers. The Sessions label previously counted unpinned rows against a pin-inclusive total (pin one row and it reads 17/18 forever); pinned rows are always loaded, so they're now dropped from both sides. Messaging platform sections count pinned rows as loaded for their label/reveal-step (they're housed in Pinned, not missing from disk), take section recency from every loaded row, and only drop their section when nothing is left to show and nothing more exists on disk. tsc/eslint clean on this branch; the drop-zone suite still passes 13/13.

@OmarB97 OmarB97 changed the title feat(desktop): drag sessions between Pinned and Sessions to pin/unpin feat(desktop): refine sidebar session drag reordering Jun 11, 2026

OmarB97 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Maintainer-ready after refresh.

I merged current upstream/main into this sidebar drag/reordering PR and pushed head 4094eb8d2774bd30e6021bdcf2091506bee91fb4.

Verification:

  • npm --prefix apps/desktop run test:ui -- src/app/chat/sidebar/use-session-drop-zone.test.tsx src/store/session.test.ts (41 passed)
  • npm --prefix apps/desktop run typecheck
  • npm exec eslint -- on the changed desktop files
  • git diff --check upstream/main...HEAD

MeshBoard merge dry-run passes with green checks and a fresh base. I attempted the actual merge through meshctl pr merge, but GitHub denied MergePullRequest for my account, so this is ready for a maintainer to merge.

@alt-glitch alt-glitch added comp/desktop Electron desktop app (apps/desktop/*) and removed comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Jun 26, 2026
@OmarB97
OmarB97 force-pushed the feat/sidebar-drag-to-pin-sessions branch from 4094eb8 to 51929ea Compare July 6, 2026 01:16
@OmarB97

OmarB97 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Re-authored onto current main and force-pushed (51929ea).

Why a rebuild was needed: since this PR was opened, main extracted the sidebar session section into sessions-section.tsx and refactored the row into primitive chrome (SidebarRowShell/Body/Grab/Lead). The original diff targeted the old inline SidebarSessionsSection in index.tsx and the old div/button row, so it no longer applied. It also called reorderPinnedSession(pinId, index), which doesn't exist on current main.

What's implemented (faithful to the PR's intent):

  • Drag-to-pin / unpin / reorder via native DnD on the row body: drop a row on Pinned to pin, drop a pinned row on Sessions to unpin, drag within either to reorder. A motion.div layout="position" FLIP preview animates the shuffle before the drop commits.
  • inline-refs: drag payload now carries pinId + pinned, plus a pinned-marker MIME so dragover can filter by pin-state before the payload is readable.
  • use-session-drop-zone.ts (+ its 21-case test) ported verbatim.
  • session-row: row body is the native drag source with a stable data-session-id hit-test anchor wrapping the animated motion.div, so drop-zone rects don't chase the preview animation.
  • Messaging/recents counts and sessionByAnyId now account for pinned rows the sections no longer render (a pinned messaging thread relocates to Pinned cleanly).

Adaptations to current main:

  • reorderPinnedSession(pinId, index)pinSession(pinId, index). insertUniqueId already filters an existing pin out before re-inserting at index, so one call serves both pin and pin-reorder.
  • The flat-list drop path now sets sidebarSessionOrderManual (current main gates the custom order behind manual mode).
  • The existing dnd-kit grab-handle reorder is retained alongside the new native drag, so keyboard/handle reordering still works and there's no regression if the native path needs tuning.

Verification: npm run typecheck, build, eslint, and the 21 drop-zone unit tests are all green.

⚠️ Needs manual QA in the running app: drag interactions can't be exercised headlessly. Please verify drag-to-pin, drag-to-unpin, same-section reorder, cross-section preview, and the FLIP animation — with particular attention to the motion.div layout animation inside the virtualized recents list and its interaction with a concurrent dnd-kit grab drag.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for rebuilding this against the extracted sidebar structure. The native drop-zone design addresses a real gap on current main: SidebarSessionRow only produces the composer drag payload today (apps/desktop/src/app/chat/sidebar/session-row.tsx:133-147).

Problems

  • apps/desktop/src/app/chat/sidebar/index.tsx:535-557 accepts every pinned drag into Sessions. A pinned messaging thread is therefore unpinned and inserted into agentSessions order, although messaging rows are fetched and rendered separately (apps/desktop/src/app/session/hooks/use-session-list-actions.ts:34-42). Its preview is also shown in the wrong section before disappearing.

Suggested changes

  • Restrict positional Sessions preview/order persistence at index.tsx:548 to local Sessions rows, while preserving any intended unpin behavior for messaging rows. Add coverage for that pinned-messaging drop path; the new hook tests use generic payloads and do not exercise the sidebar's source-specific ordering.

Automated hermes-sweeper review.

// the pre-positional behavior. Manual mode must be flagged for the custom
// order to win over the default started_at sort.
if (!showAllProfiles && !agentsGrouped) {
const nextIds = placeSessionIdAtAnchor(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This ordering path also runs after a pinned messaging row is dropped here: flags.pinned accepts it at line 535, but it is absent from agentSessions, so this persists a messaging ID into the local Sessions order and previews it in the wrong section. Gate positional preview/order writes to rows from the local Sessions slice; add a pinned-messaging drop test.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
OmarB97 added a commit to OmarB97/hermes-agent that referenced this pull request Jul 15, 2026
…estore live context, bulk actions, and fluid drag (#283)

* fix(desktop): restore live session context and bulk archive

* feat(desktop): refine sidebar session drag reordering

Re-authors PR NousResearch#43661 onto current main. Since the original PR, the sidebar
session section was extracted into sessions-section.tsx and the row into the
primitive chrome (SidebarRowShell/Body/Grab/Lead), so the original section and
row edits no longer applied; this rebuilds the feature on the current shape.

Native drag-and-drop for sidebar session rows: drag a row body onto Pinned to
pin it, drag a pinned row onto Sessions to unpin, and drag within either section
to reorder — with a live motion.div FLIP preview that animates the row shuffle
before the drop commits. The grab-handle dnd-kit reorder is retained alongside.

- inline-refs: the session drag payload carries pinId + pinned, plus a
  pinned-marker MIME so dragover can filter by pin-state before the payload is
  readable at drop.
- use-session-drop-zone (new): hook + helpers — anchor resolution with a
  dead-band hysteresis, preview splicing, and pin/unpin/reorder drop handling.
- session-row: the row body is the native drag source with a stable
  data-session-id hit-test anchor, wrapping a motion.div layout="position"
  visual so rects stay put while the preview animates.
- sessions-section / virtual-session-list: thread the drag callbacks and the
  drop-zone frame (dropActive highlight + handlers) through the extracted
  section.
- index: pinned/sessions drop zones wired to pinSession(id, index) and the flat
  session-order store; sessionByAnyId, messaging, and recents counts now account
  for pinned rows the sections no longer render.

pinSession(id, index) replaces the original PR's undefined reorderPinnedSession:
insertUniqueId already filters an existing pin out before re-inserting at the
given index, so one call serves both pin and pin-reorder.

Drag interactions can't be exercised headlessly; typecheck, build, lint, and the
21 drop-zone unit tests are green. Drag-to-pin/unpin/reorder and the FLIP
animation need manual QA in the running app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(desktop): satisfy restored feature lint

* style(desktop): fix named import order

* fix(desktop): restore packaged sidebar drag activation

* fix(desktop): restore continuous cross-section session drag

* fix(desktop): persist the rendered drag preview

* fix(desktop): commit sidebar drag on physical release

* fix(desktop): retain context usage between turns

* fix(desktop): honor persisted sidebar row order

* fix(desktop): ignore the active row during drop collision

* fix(desktop): preserve context window on warm resume

* fix(desktop): stabilize cross-lane drop anchors

* fix(desktop): retain session order through sidebar load

* fix(desktop): settle session drops at pointer release

* fix(desktop): restore session multi-select and reliable compress

* fix(desktop): repair bulk archive and session drag feel

* fix(desktop): persist profile session archives

---------

Co-authored-by: Omar Baradei <omar@kostudios.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@teknium1 teknium1 added the area/sessions Session lifecycle, resume, persistence, history label Jul 19, 2026
OmarB97 added a commit to OmarB97/hermes-agent that referenced this pull request Jul 20, 2026
…estore live context, bulk actions, and fluid drag (#283)

* fix(desktop): restore live session context and bulk archive

* feat(desktop): refine sidebar session drag reordering

Re-authors PR NousResearch#43661 onto current main. Since the original PR, the sidebar
session section was extracted into sessions-section.tsx and the row into the
primitive chrome (SidebarRowShell/Body/Grab/Lead), so the original section and
row edits no longer applied; this rebuilds the feature on the current shape.

Native drag-and-drop for sidebar session rows: drag a row body onto Pinned to
pin it, drag a pinned row onto Sessions to unpin, and drag within either section
to reorder — with a live motion.div FLIP preview that animates the row shuffle
before the drop commits. The grab-handle dnd-kit reorder is retained alongside.

- inline-refs: the session drag payload carries pinId + pinned, plus a
  pinned-marker MIME so dragover can filter by pin-state before the payload is
  readable at drop.
- use-session-drop-zone (new): hook + helpers — anchor resolution with a
  dead-band hysteresis, preview splicing, and pin/unpin/reorder drop handling.
- session-row: the row body is the native drag source with a stable
  data-session-id hit-test anchor, wrapping a motion.div layout="position"
  visual so rects stay put while the preview animates.
- sessions-section / virtual-session-list: thread the drag callbacks and the
  drop-zone frame (dropActive highlight + handlers) through the extracted
  section.
- index: pinned/sessions drop zones wired to pinSession(id, index) and the flat
  session-order store; sessionByAnyId, messaging, and recents counts now account
  for pinned rows the sections no longer render.

pinSession(id, index) replaces the original PR's undefined reorderPinnedSession:
insertUniqueId already filters an existing pin out before re-inserting at the
given index, so one call serves both pin and pin-reorder.

Drag interactions can't be exercised headlessly; typecheck, build, lint, and the
21 drop-zone unit tests are green. Drag-to-pin/unpin/reorder and the FLIP
animation need manual QA in the running app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(desktop): satisfy restored feature lint

* style(desktop): fix named import order

* fix(desktop): restore packaged sidebar drag activation

* fix(desktop): restore continuous cross-section session drag

* fix(desktop): persist the rendered drag preview

* fix(desktop): commit sidebar drag on physical release

* fix(desktop): retain context usage between turns

* fix(desktop): honor persisted sidebar row order

* fix(desktop): ignore the active row during drop collision

* fix(desktop): preserve context window on warm resume

* fix(desktop): stabilize cross-lane drop anchors

* fix(desktop): retain session order through sidebar load

* fix(desktop): settle session drops at pointer release

* fix(desktop): restore session multi-select and reliable compress

* fix(desktop): repair bulk archive and session drag feel

* fix(desktop): persist profile session archives

---------

Co-authored-by: Omar Baradei <omar@kostudios.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Reimplements sidebar drag-to-pin on upstream's POINTER drag session. The
original branch rode the native HTML5 session transport (HERMES_SESSION_MIME,
a DataTransfer drop-zone hook), which no longer exists: session-drag.ts
replaced it with a resolver over the shared pointer drag session, and the
sidebar became a deny area. The feature is re-authored as a second resolver
target rather than rebased.

- sidebar/session-drop.ts: the sidebar's drop resolution. Registered lists
  publish their FULL ordering (a virtualized list only paints a window, so
  the DOM alone can't commit an order) plus the commit; geometry is
  snapshotted once at drag start and each move is pure math against it.
- session-drag.ts: the sidebar lists are tested before the zones — they sit
  in a zone that hosts no chat surface, which would otherwise deny the drop.
  Zone stack/split/link behavior is untouched.
- Drop on PINNED pins (or re-slots) at the hovered position; drop a pinned
  row on SESSIONS unpins it and lands it at the hovered slot; dropping
  inside either list reorders. A release that changes nothing is a no-op,
  so a stray drag never flips recents into manual ordering.

UX changed deliberately from the original: rows do NOT shuffle live under
the pointer. Upstream's drag doctrine is placement-on-release (the tab strip
shows an insertion divider, not a live shuffle), so the target list draws a
ring plus a hairline at the insertion slot. That also removes the reason for
the old hysteresis/dead-band logic — nothing animates under the pointer to
chase. The dnd-kit grab handle keeps working for in-section reorder.

Verified: npm run typecheck (all three projects), npx vitest run (3820
passed), eslint + prettier on the touched files, git diff --check.
@OmarB97
OmarB97 force-pushed the feat/sidebar-drag-to-pin-sessions branch from 51929ea to ceded62 Compare July 29, 2026 17:22
@OmarB97

OmarB97 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Rewritten on top of current main — the branch is now a fresh single commit off main, not a rebase of the old one.

Why: this branch was built on the native HTML5 session transport (HERMES_SESSION_MIME, writeSessionDrag/readSessionDrag, a DataTransfer-based use-session-drop-zone hook). main has since replaced that transport wholesale: apps/desktop/src/app/chat/session-drag.ts is a resolver over the shared pointer drag session, the native session DnD and SessionTileDropBridge are gone, and the sidebar became an explicit deny area. Rebasing would have carried a dead API forward, so the intent was re-authored against the current architecture.

What it does now

  • Drop a row on Pinned → pins it at the hovered slot (already pinned → re-slots it).
  • Drop a pinned row on Sessions → unpins it and lands it at the hovered slot.
  • Drop inside either list → reorders at that slot. A release that changes nothing is a no-op, so a stray drag can't flip recents into manual ordering.
  • The flat Sessions list still refuses foreign rows (cron/messaging), which would otherwise be spliced into the saved Sessions order.
  • Zone stack / split / composer-link behavior is untouched; the dnd-kit grab handle still works for in-section reorder.

Deliberate UX change: rows no longer shuffle live under the pointer. main's drag doctrine is placement-on-release — the tab strip "shows an insertion divider, not a live shuffle" — so the hovered list draws a ring plus a hairline at the insertion slot instead of animating a preview. That also removes the reason for the old middle-band hysteresis: nothing animates under the pointer to chase.

Files: new sidebar/session-drop.ts (+ tests); session-drag.ts tests the sidebar lists before the zones; sessions-section.tsx registers its flat list and draws the affordance; session-row.tsx tags rows with their slot and carries the pin id in the payload; sidebar/index.tsx owns the pin/unpin/reorder commits.

Registered lists publish their full ordering rather than being read off the DOM — a virtualized list only paints a window, so a DOM-derived order would silently drop off-screen sessions.

Verification: npm run typecheck (all three projects), npx vitest run — 3820 passed / 2 skipped, eslint + prettier on the touched files, git diff --check.

@Nicolas-Formenton

Copy link
Copy Markdown
Contributor

FYI: opened #84984 (drag to reorder profile groups in the All-profiles sidebar). Same two files touched (sessions-section.tsx, index.tsx) but different code paths — profile groups branch only, no overlap with session-row dragging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants