Skip to content

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

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

feat(desktop): refine sidebar session drag reordering#43661
OmarB97 wants to merge 6 commits 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.

Dragging a sidebar session row between the PINNED and SESSIONS sections
did nothing: each section owns a private dnd-kit DndContext (scoped to
in-section reorder from the grab handle), and the row body's native drag
only targets the composer. Pinning required the context menu or
shift-click.

The native row drag already carries application/x-hermes-session, so the
two sections become native drop zones for it: drop a row on Pinned to
pin, drop a pinned row on Sessions to unpin — matching Claude Desktop's
drag-to-pin gesture. A marker MIME (application/x-hermes-session-pinned)
rides the drag because dragover can only inspect types, letting each
zone engage only for drags it would act on; the payload now also carries
the durable pin id (lineage root) so drop targets key the store the same
way the context-menu path does. The hovered section highlights, drops
land on the header too (works while collapsed or empty), and the target
section auto-opens so the landed row is visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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
…ns render, drag hint

Three gaps from the drag-to-pin feature (#145):

Pinned messaging/cron-adjacent rows now render: sessionByAnyId only
indexed cron + recents, so pinning a messaging-platform row (menu,
shift-click, or drag) stored an id the Pinned section could never
resolve — the pin silently vanished. Messaging sessions join the index,
and pinned rows are filtered out of their platform section so pinning
MOVES the row (matching how recents behave) instead of duplicating it.

Drops are positional: rows carry data-session-id, the drop zone hands
the drop event to its handler, and sessionDropAnchor() resolves the row
under the pointer (top half = before, bottom half = after). Drag-to-pin
inserts at that index in the raw pinned-id store (translated via the
anchor's durable pin id, since rendered rows can be a subset of stored
ids); drag-to-unpin splices into the saved flat-list order, falling back
to the old surface-at-top reconcile for header drops, fresh anchors, or
grouped/ALL-profiles views.

The empty-Pinned hint now teaches the gesture: 'Drag a chat here, or
shift-click to pin' (en/zh/zh-hant/ja).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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).

…nned rows

The Sessions label counted unpinned rows against a total that includes
pinned ones, so pinning a single row left the count stuck at '17/18'
forever — advertising a page that can never arrive. Pinned rows are
always loaded (the refresh keep-set preserves them), so drop them from
BOTH sides of the label.

Messaging sections get the same honesty: pinned rows are hidden from
the section (they live in Pinned) but still count as LOADED for the
count label and reveal-step math, so hiding them can't make the pager
think more rows remain on disk. Section recency now comes from every
loaded row so a platform doesn't reshuffle when its newest thread gets
pinned, and a platform only drops its section when it has nothing left
to show AND nothing more on disk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@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.

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

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants