hermes-agent/apps/desktop/src/lib/drag-ghost.ts
Brooklyn Nicholson 092a97ef75 fix(desktop): session-tab drag, focus sync, and pop-out isolation
Make every session tab speak the same drag language as a sidebar row, and
keep tab focus 1:1 across the sidebar, tiles, and main:

- Drag a session tile's OR the main workspace tab onto a composer to link the
  chat (@session chip), or onto a zone/edge to stack/split — tabDrag now
  returns whether it took the drag, so the workspace tab defers to the generic
  pane move on a fresh draft (nothing to link).
- A lone tool panel (terminal/logs) dragged to its own zone keeps its header,
  so it stays draggable/closable instead of stranding a dead, tab-less zone.
- The sidebar highlight follows the FOCUSED session (interacted tile, else the
  main selection) rather than only the main one.
- Clicking an already-open session jumps to its tab — an open tile, or the
  workspace tab when it's the main session and focus sits on a tile — instead
  of a dead no-op reload.
- Secondary (single-chat pop-out) windows boot to the default tree with no
  tiles and never persist their stripped-down layout back, so they can't
  inherit or clobber the primary window's tabs/splits.

The pointer drag ghost is extracted to a shared lib/drag-ghost and the drop
affordances drop their now-unused labels.
2026-07-15 03:13:33 -04:00

43 lines
1.6 KiB
TypeScript

/**
* A flat, pointer-following drag chip — the shared "what am I holding"
* affordance for in-app pointer drags. Plain DOM (no React) so it survives a
* pointer-capture drag without re-renders and tears down synchronously on Esc.
*
* Flat by design: a solid app surface with the dragged item's label, no
* border / radius / shadow, dimmed — it copies the real row/tab it represents
* rather than reading as a separate pill. Any pointer drag whose source does
* not stay visibly "held" can reuse this (the drag primitive in
* `pane-shell/tree/renderer/drag-session.ts`, and anything built on it).
*/
/** How far (px) the chip trails the pointer so it never sits under the cursor. */
const OFFSET_X = 14
const OFFSET_Y = 12
export interface DragGhost {
/** Reposition the chip near the current pointer point. */
moveTo(x: number, y: number): void
/** Remove the chip from the DOM. Idempotent. */
destroy(): void
}
export function createDragGhost(label: string): DragGhost {
const el = document.createElement('div')
el.textContent = label
el.style.cssText =
'position:fixed;left:0;top:0;z-index:9999;pointer-events:none;max-width:16rem;overflow:hidden;' +
'text-overflow:ellipsis;white-space:nowrap;padding:0.25rem 0.625rem;opacity:0.6;' +
'background:var(--ui-sidebar-surface-background,var(--dt-card));color:var(--ui-text-primary);' +
'font-size:0.75rem;font-weight:500;will-change:transform'
document.body.appendChild(el)
return {
moveTo(x, y) {
el.style.transform = `translate3d(${x + OFFSET_X}px, ${y + OFFSET_Y}px, 0)`
},
destroy() {
el.remove()
}
}
}