Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/stats-range-arrow-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nanocollective/nanocoder": patch
---

Fixed `/stats` dropping arrow-key presses and freezing on a range tab. The range stepped from the value captured in the input handler's closure, but Ink re-registers that handler in a passive effect that runs after the frame is painted — so a press arriving before the effect landed was dispatched with the previous render's range, recomputed the tab it had already moved to, and wedged there until another key broke the tie. The range now steps from the value React holds.
9 changes: 9 additions & 0 deletions source/commands/stats.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,16 @@ test('StatsDisplay changes range with arrow keys and closes on Escape', async t
await waitFor(frameMatches(/\[3m\]/));
t.regex(stripAnsi(lastFrame() ?? ''), /\[3m\]/);

// Two presses in one tick. Ink re-registers the input handler in a passive
// effect that runs after the frame is painted, so the second press is still
// dispatched with the previous render's range - stepping from that captured
// value lands on 3m again and wedges the tabs there.
stdin.write('\u001B[C');
stdin.write('\u001B[C');
await waitFor(frameMatches(/\[7d\]/));
t.regex(stripAnsi(lastFrame() ?? ''), /\[7d\]/);

stdin.write('\u001B[D');
await waitFor(frameMatches(/\[all-time\]/));
t.regex(stripAnsi(lastFrame() ?? ''), /\[all-time\]/);

Expand Down
25 changes: 14 additions & 11 deletions source/components/stats/stats-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,20 @@ export function StatsDisplay({
return;
}
// Match Settings tabs: left/right only (no letter shortcut).
if (key.leftArrow) {
const idx = STATS_RANGES.indexOf(range);
const prev =
STATS_RANGES[(idx - 1 + STATS_RANGES.length) % STATS_RANGES.length];
if (prev) setRange(prev);
return;
}
if (key.rightArrow) {
const idx = STATS_RANGES.indexOf(range);
const next = STATS_RANGES[(idx + 1) % STATS_RANGES.length];
if (next) setRange(next);
// Step from the range React holds, not the one this closure captured:
// Ink re-registers the handler in a passive effect that runs after the
// frame is painted, so a press arriving right after a range change is
// still dispatched here with the previous render's value.
if (key.leftArrow || key.rightArrow) {
const delta = key.rightArrow ? 1 : -1;
setRange(prev => {
const idx = STATS_RANGES.indexOf(prev);
return (
STATS_RANGES[
(idx + delta + STATS_RANGES.length) % STATS_RANGES.length
] ?? prev
);
});
}
},
{isActive: interactive},
Expand Down
Loading