mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(terminal): one touch contact is one paging gesture; bound slow ticks and wobble
Review of #527 (Ark0N, 2026-10-11): 1. A slow touch drag paged a full screen per line. touchmove calls the fallback once per whole cell, so a slow drag's calls were further apart than the 150 ms gesture gap and each pre-paid a page (12 lines at 170 ms sent 12 PageUps). The touch paths now tag every call, touchmove and momentum frames alike, with a per-contact touchId (incremented on touchstart), and pageKeysForGesture never splits a contact by time; a new contact is a new gesture. 2. A pre-paid debt now survives up to PAGE_KEY_DEBT_MS (1 s) of quiet when the next event goes the same way, so slow small wheel ticks (40 px every 250 ms, a Firefox 1-line notch) pay it back instead of each pre-paying. A reversal inside the gap (or inside one contact) drops the debt and accumulates without pre-paying, so a +/-3 px wobble no longer pages back and forth. A reversal after the gap still pages at once. Main pane and tiles share the helper, so both get it. 3. A wheel consumed by the dialog check now logs page-keys-dialog-open. 4. The Dashboard wiki names newer Codex beside OpenCode; the invariants paragraph describes the new gesture boundaries. Tests: unit regressions through the real _maybePageCliTranscript, helper cases, tile/main parity for ticks and wobble, and a Chromium test that drives the real initTerminal touch listeners (12 PageUps before, 1 after).
This commit is contained in:
1 parent
eaf924a291
commit
3ea1e03aaf
7 files changed
+426
-32
No files matched your search
@@ -202,12 +202,14 @@ describe('PageUp/PageDown fallback for a hollow local buffer (issue #205 round 2
|
||||
clock.now = () => now;
|
||||
|
||||
app._maybePageCliTranscript({ shiftKey: false }, -1); // first event: one PageUp
|
||||
app._maybePageCliTranscript({ shiftKey: false }, 1); // reversal: one PageDown at once
|
||||
now += 1_000;
|
||||
app._maybePageCliTranscript({ shiftKey: false }, 1); // after a pause: another at once
|
||||
app._maybePageCliTranscript({ shiftKey: false }, 1); // reversal inside the gap: accumulates, no page
|
||||
now += 200;
|
||||
app._maybePageCliTranscript({ shiftKey: false }, -1); // a reversal after the gap: a PageUp at once
|
||||
now += 1_100;
|
||||
app._maybePageCliTranscript({ shiftKey: false }, 1); // after a pause: a PageDown at once
|
||||
app._maybePageCliTranscript({ shiftKey: false }, 0.05); // sub-row jitter in the same gesture: nothing
|
||||
app._flushWheelSgrQueue();
|
||||
expect(sent).toEqual([{ id: 'sess-1', data: '\x1b[5~\x1b[6~\x1b[6~' }]);
|
||||
expect(sent).toEqual([{ id: 'sess-1', data: '\x1b[5~\x1b[5~\x1b[6~' }]);
|
||||
});
|
||||
|
||||
it('lets wheel notches accumulate instead of paging a full screen on each one', () => {
|
||||
@@ -422,7 +424,145 @@ describe('PageUp/PageDown fallback for a hollow local buffer (issue #205 round 2
|
||||
// Wheel: after the forwarding gate, before the local smooth scroll.
|
||||
expect(source).toContain('if (this._maybePageCliTranscript(ev, lines)) return;');
|
||||
// Touch: touchmove and the momentum loop both fall through to it.
|
||||
expect(source.match(/else if \(!this\._maybePageCliTranscript\(\{ shiftKey: false \}, lines\)\)/g)).toHaveLength(2);
|
||||
expect(
|
||||
source.match(
|
||||
/else if \(!this\._maybePageCliTranscript\(\{ shiftKey: false, touchId: touchContactId \}, lines\)\)/g
|
||||
)
|
||||
).toHaveLength(2);
|
||||
// ...and every contact gets its own id at touchstart, so a contact is one gesture.
|
||||
expect(source).toContain('touchContactId++;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('page-key gestures across slow touch drags, slow wheel ticks and jitter (#527 review)', () => {
|
||||
const UP = '\x1b[5~';
|
||||
const DOWN = '\x1b[6~';
|
||||
|
||||
/** A hollow 38-row pane (19 rows a page) on a hand-moved clock, with every key sent. */
|
||||
function pane(mode = 'codex') {
|
||||
const h = hollowApp({ mode, rows: 38 });
|
||||
let now = 10_000;
|
||||
h.clock.now = () => now;
|
||||
const keys = () => {
|
||||
h.app._flushWheelSgrQueue();
|
||||
return h.sent.map((s) => s.data).join('');
|
||||
};
|
||||
return { ...h, keys, advance: (ms: number) => (now += ms) };
|
||||
}
|
||||
const count = (keys: string, key: string) => keys.split(key).length - 1;
|
||||
|
||||
it('keeps one touch contact one gesture, however slowly the finger moves', () => {
|
||||
const p = pane();
|
||||
// touchmove calls once per whole cell, so a slow drag arrives as single lines
|
||||
// 170 ms apart, each past the 150 ms gesture gap. It sent one PageUp per line.
|
||||
for (let i = 0; i < 12; i++) {
|
||||
expect(p.app._maybePageCliTranscript({ shiftKey: false, touchId: 1 }, -1)).toBe(true);
|
||||
p.advance(170);
|
||||
}
|
||||
expect(p.keys()).toBe(UP);
|
||||
|
||||
// A new contact is a new gesture: its first line pages at once.
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, touchId: 2 }, -1);
|
||||
expect(p.keys()).toBe(UP + UP);
|
||||
});
|
||||
|
||||
it('pages a slow 40-line drag twice, and keeps momentum in the same gesture', () => {
|
||||
const p = pane();
|
||||
for (let i = 0; i < 40; i++) {
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, touchId: 7 }, -1);
|
||||
p.advance(170);
|
||||
}
|
||||
// 1 at once, then 18 owed back, then 19 more: two pages by line 38.
|
||||
expect(count(p.keys(), UP)).toBe(2);
|
||||
|
||||
// Momentum frames after the finger lifts carry the same contact: they
|
||||
// continue the drag's travel instead of opening a gesture of their own.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, touchId: 7 }, -1);
|
||||
p.advance(16);
|
||||
}
|
||||
expect(count(p.keys(), UP)).toBe(2);
|
||||
});
|
||||
|
||||
it('never splits a contact on a pause, but does not carry its debt into the next contact', () => {
|
||||
const p = pane();
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, touchId: 3 }, -1); // pages, owes 18
|
||||
p.advance(5_000); // the finger rests, still down
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, touchId: 3 }, -1);
|
||||
expect(p.keys()).toBe(UP);
|
||||
p.advance(10);
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, touchId: 4 }, -1); // a new contact pages at once
|
||||
expect(p.keys()).toBe(UP + UP);
|
||||
});
|
||||
|
||||
it('pages slow small wheel ticks once per half screen, not once per tick', () => {
|
||||
// Five 40 px ticks 250 ms apart (1.6 rows each, under the 2-row trackpad
|
||||
// size) used to page once per tick. The first one still pages at once.
|
||||
const p = pane();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: -40, deltaMode: 0 }, -1.6);
|
||||
p.advance(250);
|
||||
}
|
||||
expect(p.keys()).toBe(UP);
|
||||
|
||||
// Firefox reports a notch as 1 line (deltaMode 1): the same.
|
||||
const f = pane();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
f.app._maybePageCliTranscript({ shiftKey: false, deltaY: -1, deltaMode: 1 }, -1);
|
||||
f.advance(250);
|
||||
}
|
||||
expect(f.keys()).toBe(UP);
|
||||
});
|
||||
|
||||
it('lets a pre-paid debt lapse after a second of quiet, so the next tick pages at once again', () => {
|
||||
const p = pane();
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: -40 }, -1.6);
|
||||
p.advance(900);
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: -40 }, -1.6); // inside the second: owed
|
||||
expect(p.keys()).toBe(UP);
|
||||
p.advance(1_100);
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: -40 }, -1.6); // debt lapsed: pages
|
||||
expect(p.keys()).toBe(UP + UP);
|
||||
});
|
||||
|
||||
it('does not page back and forth on a +/-3 px trackpad wobble', () => {
|
||||
const p = pane();
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const dy = i % 2 ? 3 : -3;
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: dy }, dy / 25);
|
||||
p.advance(16);
|
||||
}
|
||||
// The opening event is a fresh gesture and pages; no reversal inside the
|
||||
// gap pre-pays, so the wobble sends nothing after it.
|
||||
expect(p.keys()).toBe(UP);
|
||||
});
|
||||
|
||||
it('still pages a deliberate reversal at once after a pause', () => {
|
||||
const p = pane();
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: -25 }, -1);
|
||||
p.advance(200);
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: 25 }, 1);
|
||||
expect(p.keys()).toBe(UP + DOWN);
|
||||
});
|
||||
|
||||
it('does not let a reversal inherit the other direction’s debt', () => {
|
||||
const p = pane();
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: -25 }, -1); // UP, owes 18 upward
|
||||
p.advance(16);
|
||||
// Downward travel just short of a page: the upward debt must not count toward it.
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: 25 * 18 }, 18);
|
||||
expect(p.keys()).toBe(UP);
|
||||
p.advance(16);
|
||||
p.app._maybePageCliTranscript({ shiftKey: false, deltaY: 25 }, 1);
|
||||
expect(p.keys()).toBe(UP + DOWN);
|
||||
});
|
||||
|
||||
it('logs a wheel the dialog check consumed', () => {
|
||||
const p = pane();
|
||||
p.app.tabAlerts = new Map([['sess-1', 'action']]);
|
||||
expect(p.app._maybePageCliTranscript({ shiftKey: false }, -1)).toBe(true);
|
||||
expect(p.keys()).toBe('');
|
||||
expect(p.logs.some((line) => line.includes('[scroll] sess-1 → page-keys-dialog-open'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -464,24 +604,40 @@ describe('pageKeysForGesture, the pure gesture rules both panes share', () => {
|
||||
expect(steps.map((s) => s.keys).join('')).toBe(UP);
|
||||
});
|
||||
|
||||
it('drops a pre-paid debt when its gesture ends', () => {
|
||||
// A flick pre-pays a page; a second flick after a pause pages at once again
|
||||
// instead of first paying off the first one's debt.
|
||||
it('keeps a same-direction pre-paid debt for PAGE_KEY_DEBT_MS, then drops it with its gesture', () => {
|
||||
const { PAGE_KEY_DEBT_MS } = loadTerminalUiHarness().windowRef.CodemanTerminalInput;
|
||||
expect(PAGE_KEY_DEBT_MS).toBe(1000);
|
||||
// A tick 500 ms later, past the 150 ms gap, pays the debt back instead of
|
||||
// pre-paying a second page; a flick after a second of quiet pages at once.
|
||||
const steps = run([
|
||||
[-1, 0],
|
||||
[-1, 500],
|
||||
[-1, 1600],
|
||||
]);
|
||||
expect(steps.map((s) => s.keys)).toEqual([UP, UP]);
|
||||
expect(steps.map((s) => s.keys)).toEqual([UP, '', UP]);
|
||||
});
|
||||
|
||||
it('starts a new gesture on a direction change, and ignores sub-row jitter', () => {
|
||||
it('pre-pays a reversal only after the gap, and ignores sub-row jitter', () => {
|
||||
const steps = run([
|
||||
[-1, 0],
|
||||
[1, 10],
|
||||
[0.05, 20],
|
||||
[-0.05, 1000],
|
||||
[1, 10], // reversal inside the gap: accumulates, no page
|
||||
[-1, 20], // and back: still nothing
|
||||
[1, 300], // a reversal after the gap is a new gesture: pages at once
|
||||
[0.05, 310],
|
||||
[-0.05, 1500],
|
||||
]);
|
||||
expect(steps.map((s) => s.keys)).toEqual([UP, DOWN, '', '']);
|
||||
expect(steps.map((s) => s.keys)).toEqual([UP, '', '', DOWN, '', '']);
|
||||
});
|
||||
|
||||
it('treats one touch contact as one gesture, and a new contact as a new one', () => {
|
||||
const steps = run([
|
||||
[-1, 0, { touchId: 1 }],
|
||||
[-1, 5_000, { touchId: 1 }], // same contact, any pause: owed
|
||||
[1, 5_010, { touchId: 1 }], // a reversal within the contact never pre-pays
|
||||
[-1, 5_020, { touchId: 2 }], // a new contact pages at once
|
||||
[-1, 5_030], // a wheel event is not that contact: a new gesture
|
||||
]);
|
||||
expect(steps.map((s) => s.keys)).toEqual([UP, '', '', UP, UP]);
|
||||
});
|
||||
|
||||
it('sends nothing for a pinch or a mostly horizontal swipe, and leaves the state as it was', () => {
|
||||
@@ -525,7 +681,7 @@ describe('the paging gates asked for another pane (a TerminalTile)', () => {
|
||||
[-1, {}],
|
||||
[-10, {}],
|
||||
[-30, {}],
|
||||
[1, {}],
|
||||
[20, {}], // a reversal inside the gap pages by travel, not at once
|
||||
[-5, { ctrlKey: true }],
|
||||
[-5, { deltaX: 400, deltaY: -125 }],
|
||||
[-1000, {}],
|
||||
|
||||
Reference in new issue
Block a user