Skip to content

feat(desktop): drag-to-pin follow-ups — positional drop, messaging pins render, drag hint#146

Merged
OmarB97 merged 1 commit into
mainfrom
feat/sidebar-drag-pin-followups
Jun 10, 2026
Merged

feat(desktop): drag-to-pin follow-ups — positional drop, messaging pins render, drag hint#146
OmarB97 merged 1 commit into
mainfrom
feat/sidebar-drag-pin-followups

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Why

Drag-to-pin (#145) shipped with three documented gaps, all called out in its Risks/gaps and on the MeshBoard task hermes-desktop-drag-sessions-between-sessions-and:

  1. Pinned messaging rows silently vanish. sessionByAnyId indexes only cron + recents, so pinning a messaging-platform row (context menu, shift-click, or the new drag) stores an id the Pinned section can never resolve — the pin takes effect in the store and renders nothing. This predates feat(desktop): drag sessions between Pinned and Sessions to pin/unpin #145 (the menu had the same bug) but drag-to-pin made it much easier to hit.
  2. Drops append instead of inserting. Drag-to-pin always appended to the end of the pinned list; drag-to-unpin always surfaced the row at the top of Sessions. Claude Desktop inserts at the pointer.
  3. The gesture is undiscoverable. The empty-Pinned hint only mentioned shift-click.

What changed

  • apps/desktop/src/app/chat/sidebar/index.tsx
    • sessionByAnyId now also indexes $messagingSessions (recents still win id collisions), so messaging pins resolve and render in Pinned. Side benefit: server FTS hits for loaded messaging sessions resolve to the real row instead of a synthesized placeholder.
    • messagingGroups filters pinnedRealIdSet so pinning MOVES the row out of its platform section (same semantics as recents) instead of duplicating it; a platform whose every row is pinned drops its section.
    • The two drop handlers are positional. Pin: translate the anchor row under the pointer into an index in the RAW pinned-id store by indexOf of the anchor's durable pin id — rendered rows can be a subset of stored ids, so rendered position is not trusted. Unpin: splice the dropped id into the saved flat-list order before/after the anchor; header drops, fresh-row anchors, and grouped/ALL-profiles views keep the previous behavior (recency reconcile surfaces the row at the top). The handler block also moved below the memos it reads so declaration order matches data flow.
  • apps/desktop/src/app/chat/sidebar/use-session-drop-zone.tsonDropSession now receives the drop event; new exported sessionDropAnchor(event) resolves the [data-session-id] row under the pointer with top-half/bottom-half = before/after semantics.
  • apps/desktop/src/app/chat/sidebar/session-row.tsx — rows carry data-session-id (used by the anchor lookup; also handy for tests).
  • apps/desktop/src/i18n/{en,zh,zh-hant,ja}.ts — empty-Pinned hint becomes "Drag a chat here, or shift-click to pin" (each locale keeps its existing pin/chat terminology).
  • apps/desktop/src/app/chat/sidebar/use-session-drop-zone.test.tsx — 4 new sessionDropAnchor tests (before/after midpoint, nested children, null off-row); drop tests updated for the (payload, event) signature.

How to review

  1. use-session-drop-zone.tssessionDropAnchor and the event pass-through are the whole mechanism.
  2. index.tsx drop handlers — the pin-index translation comment explains why anchor → raw-store index goes through the durable pin id; the unpin path's guards (flat-list only, anchor must exist in the saved order) fall back to pre-positional behavior rather than guessing.
  3. index.tsx messaging changes — one added list in the index memo + one continue guard in the platform-split loop; check the dep arrays picked up messagingSessions / pinnedRealIdSet.
  4. Locale files — copy-only.

Evidence

Live verification in real Chromium (vite dev renderer, seeded stores; same harness as #145):

  • Positional pin: dropping "Recent Two" on the bottom half of "Pinned Alpha" landed $pinnedSessionIds = ["s-pin-1","s-2","s-pin-2"] — between the two existing pins, rendered in that order.
  • Positional unpin: dropping "Pinned Bravo" on the top half of "Recent Three" produced saved order ["s-1","s-pin-2","s-3","s-4"] and Sessions rendered One → Bravo → Three → Four.
  • Messaging pin: dragging "Discord thread B" onto Pinned gave pinnedIds=[...,"m-2"], the row rendered under Pinned, and the Discord section listed only m-1 — per-section row audit: Pinned => [s-pin-1,s-2,m-2] || Sessions => [s-1,s-pin-2,s-3,s-4] || Discord => [m-1].
  • Empty-Pinned hint renders "Drag a chat here, or shift-click to pin".
  • npx vitest run --environment jsdom src/app/chat/sidebar/use-session-drop-zone.test.tsx: 13/13 pass.

Verification

  • npm run type-check (tsc -b): pass.
  • npx eslint on all eight touched files: pass, no warnings.
  • Full renderer suite (npx vitest run --environment jsdom) at origin/main (410c71f) + this diff: no new failures — the sorted FAIL set equals the documented baseline minus the two use-prompt-actions file-attach failures that fix(desktop): restore file.attach client upload path (Phase 2b companion to #142) #144 fixed on main (diff-verified against the recorded baseline set).
  • npm run build (tsc -b + production vite build): pass.

Risks / gaps

  • A platform section disappears entirely when ALL its loaded rows are pinned; the rows live in Pinned and unpinning restores the section. Intentional (mirrors recents), noted here for reviewers — non-actionable.
  • Per-platform "load more" counts treat pinned rows as not-loaded (loaded count shrinks by the pinned rows), so a step can over-report by the number of pinned rows for that platform. Worst case is an off-by-a-few label on the load-more affordance; accepted, noted on the MeshBoard task with the messaging-pin item.
  • Positional unpin into a grouped/ALL-profiles view intentionally degrades to the recency-reconcile (top) placement — those views derive their own order, so a saved-order splice would be invisible anyway. Accepted scope.
  • Upstream port: these follow-ups are cherry-picked onto the open upstream PR branch (feat(desktop): refine sidebar session drag reordering NousResearch/hermes-agent#43661) so the port stays complete; tracked on the same MeshBoard task.

Collaborators

  • @OmarB97 (operator — requested the follow-ups)
  • Claude Fable 5 (ko-mac.claude — implementation, tests, verification)

…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>
@github-actions

Copy link
Copy Markdown

🔎 Lint report: feat/sidebar-drag-pin-followups vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10583 on HEAD, 10583 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5544 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

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.

1 participant