The pane waited 150 ms on hover and 60 ms on a keyboard step before following the sidebar highlight, so every preview paid for the fast walk it might have been. Replace both with a leading-edge 120 ms gap: the first move shows at once, a burst is coalesced to its start and its end. The row's own hover no longer eases either — `background` was on a 120 ms transition, which is what read as lag on the row itself. The sidebar also rebuilt its whole tree on every `SessionUpdated`, a couple of times a minute per live session, dropping `:hover` from the row under the pointer. Refreshes that change nothing structural now patch the activity rail and meta line into the standing rows instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
/**
|
|
* When the content pane follows the sidebar highlight (docs/PREVIEW.md).
|
|
*
|
|
* The highlight itself is never scheduled — that is a class on a row and lands
|
|
* on the same frame as the input. This governs only the pane swap behind it,
|
|
* which mounts a terminal and, for a session the connection pool isn't already
|
|
* holding, opens an attach socket the daemon answers with a `tmux capture-pane`.
|
|
*
|
|
* A plain debounce would put that cost in front of every preview, including the
|
|
* one preview the operator actually meant. Instead the first move shows at once
|
|
* and the rest of a burst is coalesced: walking a list with `j` held down, or
|
|
* sweeping the pointer from the top of the sidebar to the bottom, costs one
|
|
* preview at the start and one at the row you stopped on, not one per row.
|
|
*/
|
|
|
|
/** Shortest gap between two pane swaps. Long enough that a fast walk mounts a
|
|
* couple of terminals rather than one per row; short enough that a deliberate
|
|
* second move never waits on it. */
|
|
export const PREVIEW_MIN_GAP_MS = 120;
|
|
|
|
export interface PreviewScheduleDeps {
|
|
now?: () => number;
|
|
setTimer?: (fn: () => void, ms: number) => number;
|
|
clearTimer?: (handle: number) => void;
|
|
/** Override the gap; tests and callers otherwise take {@link PREVIEW_MIN_GAP_MS}. */
|
|
minGapMs?: number;
|
|
}
|
|
|
|
export interface PreviewSchedule {
|
|
(sessionId: string | null): void;
|
|
/** Drop a pending trailing preview without showing it. */
|
|
cancel(): void;
|
|
}
|
|
|
|
/**
|
|
* Wrap `show` so it fires on the leading edge and at most once per gap.
|
|
*
|
|
* `show` is called with the session to preview, or `null` to put back whatever
|
|
* the operator actually opened.
|
|
*/
|
|
export function createPreviewSchedule(
|
|
show: (sessionId: string | null) => void,
|
|
deps: PreviewScheduleDeps = {},
|
|
): PreviewSchedule {
|
|
const now = deps.now ?? (() => Date.now());
|
|
const setTimer = deps.setTimer ?? ((fn, ms) => window.setTimeout(fn, ms));
|
|
const clearTimer = deps.clearTimer ?? ((h) => window.clearTimeout(h));
|
|
const minGap = deps.minGapMs ?? PREVIEW_MIN_GAP_MS;
|
|
|
|
let lastShownAt = Number.NEGATIVE_INFINITY;
|
|
let shown: string | null | undefined;
|
|
let pendingTimer: number | undefined;
|
|
let pendingId: string | null = null;
|
|
|
|
const emit = (id: string | null) => {
|
|
// Nothing to do when the burst ended on the view already up: re-showing it
|
|
// would tear down and re-mount the pane for no visible change.
|
|
if (shown === id) return;
|
|
shown = id;
|
|
lastShownAt = now();
|
|
show(id);
|
|
};
|
|
|
|
const cancelPending = () => {
|
|
if (pendingTimer === undefined) return;
|
|
clearTimer(pendingTimer);
|
|
pendingTimer = undefined;
|
|
};
|
|
|
|
const schedule = ((sessionId: string | null) => {
|
|
pendingId = sessionId;
|
|
const waited = now() - lastShownAt;
|
|
if (waited >= minGap) {
|
|
cancelPending();
|
|
emit(sessionId);
|
|
return;
|
|
}
|
|
if (pendingTimer !== undefined) return; // already waiting; `pendingId` wins
|
|
pendingTimer = setTimer(() => {
|
|
pendingTimer = undefined;
|
|
emit(pendingId);
|
|
}, minGap - waited);
|
|
}) as PreviewSchedule;
|
|
|
|
schedule.cancel = cancelPending;
|
|
return schedule;
|
|
}
|