TUI: dedupe duplicate backspace events in input

This commit is contained in:
Vignesh Natarajan
2026-02-20 20:10:22 -08:00
parent 18b4b47708
commit d7a7ebb75a
3 changed files with 62 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { getSlashCommands, parseCommand } from "./commands.js";
import {
createBackspaceDeduper,
resolveFinalAssistantText,
resolveGatewayDisconnectState,
resolveTuiSessionKey,
@@ -87,3 +88,35 @@ describe("resolveGatewayDisconnectState", () => {
expect(state.pairingHint).toBeUndefined();
});
});
describe("createBackspaceDeduper", () => {
it("suppresses duplicate backspace events within the dedupe window", () => {
let now = 1000;
const dedupe = createBackspaceDeduper({
dedupeWindowMs: 8,
now: () => now,
});
expect(dedupe("\x7f")).toBe("\x7f");
now += 1;
expect(dedupe("\x08")).toBe("");
});
it("preserves backspace events outside the dedupe window", () => {
let now = 1000;
const dedupe = createBackspaceDeduper({
dedupeWindowMs: 8,
now: () => now,
});
expect(dedupe("\x7f")).toBe("\x7f");
now += 10;
expect(dedupe("\x7f")).toBe("\x7f");
});
it("never suppresses non-backspace keys", () => {
const dedupe = createBackspaceDeduper();
expect(dedupe("a")).toBe("a");
expect(dedupe("\x1b[A")).toBe("\x1b[A");
});
});

View File

@@ -1,7 +1,9 @@
import {
CombinedAutocompleteProvider,
Container,
Key,
Loader,
matchesKey,
ProcessTerminal,
Text,
TUI,
@@ -215,6 +217,24 @@ export function resolveGatewayDisconnectState(reason?: string): {
};
}
export function createBackspaceDeduper(params?: { dedupeWindowMs?: number; now?: () => number }) {
const dedupeWindowMs = Math.max(0, Math.floor(params?.dedupeWindowMs ?? 8));
const now = params?.now ?? (() => Date.now());
let lastBackspaceAt = -1;
return (data: string): string => {
if (!matchesKey(data, Key.backspace)) {
return data;
}
const ts = now();
if (lastBackspaceAt >= 0 && ts - lastBackspaceAt <= dedupeWindowMs) {
return "";
}
lastBackspaceAt = ts;
return data;
};
}
export async function runTui(opts: TuiOptions) {
const config = loadConfig();
const initialSessionInput = (opts.session ?? "").trim();
@@ -395,6 +415,14 @@ export async function runTui(opts: TuiOptions) {
});
const tui = new TUI(new ProcessTerminal());
const dedupeBackspace = createBackspaceDeduper();
tui.addInputListener((data) => {
const next = dedupeBackspace(data);
if (next.length === 0) {
return { consume: true };
}
return { data: next };
});
const header = new Text("", 1, 0);
const statusContainer = new Container();
const footer = new Text("", 1, 0);