Files
AgentHub/client/src/lib/keepIfSame.ts
T
nntrivi2001 965c62c5cf fix(workspace): stop the console remounting, and the idle polling re-renders
The Workspace page rendered `ConsoleArea` from two sites, picked by
`currentLane`. Every flip of that value unmounted the whole console
subtree and mounted it again at the other site: the terminal died, the
run history went with it, and `LaneConsolePane`'s lane-switch effect
wiped a half-typed prompt. It flipped on every page load (lanes arrive,
`refreshLanes` selects `lanes[0]`), on a lane being removed, and on any
refetch that re-picked the selection.

`lane-detail` is now always rendered and holds the only `ConsoleArea`;
the header and info block are the conditional parts. Same rule the
RunConsole already followed one level down — collapse with CSS, never by
unmounting — now enforced above it too.

Second half: every poller fed `setState` a brand-new object graph each
tick, re-rendering the lane strip, the card and the console area several
times a minute on a completely idle dashboard. `keepIfSame` keeps the
previous reference when the payload is value-identical, applied to the
5 s run/session poll, the lane list and counts, the `lane_update` WS
branch, and LaneCard's git/runtime/locks pollers.

Verified: npm run test:client (44 files / 454 tests), tsc --noEmit
clean, check-headers.sh clean. The Run screen snapshot was regenerated —
the diff is the added `<section>` wrapper and one indent level, no
content change.
2026-09-04 10:48:22 +07:00

35 lines
1.7 KiB
TypeScript

/**
* @file keepIfSame.ts
* @description State-updater helper for polled data: keeps the previous value's
* identity when the freshly fetched payload is value-identical, so a background
* poll that found nothing new does not re-render the tree it feeds.
*
* Every poller in this app (`api.lanes.list`, `api.run.list`, `api.lanes.git`,
* …) returns a brand-new object graph on each tick. Passing that straight into
* `setState` re-renders every consumer several times a minute even when the
* dashboard is completely idle — visible as the whole page "refreshing".
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/**
* Wraps a freshly fetched value as a functional state update that returns the
* previous state unchanged when the two are value-equal.
*
* Usage: `setLanes(keepIfSame(r.lanes))` instead of `setLanes(r.lanes)`.
*
* `prev` is typed wider than `T` so the same helper works for the `T | null`
* states the pollers start out in; the cast is sound because it only happens on
* the branch where `prev` serializes identically to `next`.
*
* @param next The value just fetched.
* @returns An updater for `setState`, yielding `prev` when nothing changed.
*/
export function keepIfSame<T>(next: T): (prev: T | null | undefined) => T {
// ponytail: JSON.stringify comparison — fine for the payloads this app polls
// (lane lists, run lists, a git-facts object: tens of small records, key order
// is stable because it comes from the same serializer every time). Swap for a
// structural deep-equal if a poller ever returns something big or key-unstable.
return (prev) => (JSON.stringify(prev) === JSON.stringify(next) ? (prev as T) : next);
}