mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix (tui): coalesce rapid git-bash submit bursts into multiline paste
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createEditorSubmitHandler } from "./tui.js";
|
||||
import {
|
||||
createEditorSubmitHandler,
|
||||
createSubmitBurstCoalescer,
|
||||
shouldEnableWindowsGitBashPasteFallback,
|
||||
} from "./tui.js";
|
||||
|
||||
describe("createEditorSubmitHandler", () => {
|
||||
it("routes lines starting with ! to handleBangLine", () => {
|
||||
@@ -118,3 +122,70 @@ describe("createEditorSubmitHandler", () => {
|
||||
expect(handleBangLine).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSubmitBurstCoalescer", () => {
|
||||
it("coalesces rapid single-line submits into one multiline submit when enabled", () => {
|
||||
vi.useFakeTimers();
|
||||
const submit = vi.fn();
|
||||
let now = 1_000;
|
||||
const onSubmit = createSubmitBurstCoalescer({
|
||||
submit,
|
||||
enabled: true,
|
||||
burstWindowMs: 50,
|
||||
now: () => now,
|
||||
});
|
||||
|
||||
onSubmit("Line 1");
|
||||
now += 10;
|
||||
onSubmit("Line 2");
|
||||
now += 10;
|
||||
onSubmit("Line 3");
|
||||
|
||||
expect(submit).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(50);
|
||||
|
||||
expect(submit).toHaveBeenCalledTimes(1);
|
||||
expect(submit).toHaveBeenCalledWith("Line 1\nLine 2\nLine 3");
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("passes through immediately when disabled", () => {
|
||||
const submit = vi.fn();
|
||||
const onSubmit = createSubmitBurstCoalescer({
|
||||
submit,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
onSubmit("Line 1");
|
||||
onSubmit("Line 2");
|
||||
|
||||
expect(submit).toHaveBeenCalledTimes(2);
|
||||
expect(submit).toHaveBeenNthCalledWith(1, "Line 1");
|
||||
expect(submit).toHaveBeenNthCalledWith(2, "Line 2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldEnableWindowsGitBashPasteFallback", () => {
|
||||
it("enables fallback on Windows Git Bash env", () => {
|
||||
expect(
|
||||
shouldEnableWindowsGitBashPasteFallback({
|
||||
platform: "win32",
|
||||
env: {
|
||||
MSYSTEM: "MINGW64",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("disables fallback outside Windows", () => {
|
||||
expect(
|
||||
shouldEnableWindowsGitBashPasteFallback({
|
||||
platform: "darwin",
|
||||
env: {
|
||||
MSYSTEM: "MINGW64",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,6 +77,99 @@ export function createEditorSubmitHandler(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldEnableWindowsGitBashPasteFallback(params?: {
|
||||
platform?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const platform = params?.platform ?? process.platform;
|
||||
if (platform !== "win32") {
|
||||
return false;
|
||||
}
|
||||
const env = params?.env ?? process.env;
|
||||
const msystem = (env.MSYSTEM ?? "").toUpperCase();
|
||||
const shell = env.SHELL ?? "";
|
||||
const termProgram = (env.TERM_PROGRAM ?? "").toLowerCase();
|
||||
if (msystem.startsWith("MINGW") || msystem.startsWith("MSYS")) {
|
||||
return true;
|
||||
}
|
||||
if (shell.toLowerCase().includes("bash")) {
|
||||
return true;
|
||||
}
|
||||
return termProgram.includes("mintty");
|
||||
}
|
||||
|
||||
export function createSubmitBurstCoalescer(params: {
|
||||
submit: (value: string) => void;
|
||||
enabled: boolean;
|
||||
burstWindowMs?: number;
|
||||
now?: () => number;
|
||||
setTimer?: typeof setTimeout;
|
||||
clearTimer?: typeof clearTimeout;
|
||||
}) {
|
||||
const windowMs = Math.max(1, params.burstWindowMs ?? 50);
|
||||
const now = params.now ?? (() => Date.now());
|
||||
const setTimer = params.setTimer ?? setTimeout;
|
||||
const clearTimer = params.clearTimer ?? clearTimeout;
|
||||
let pending: string | null = null;
|
||||
let pendingAt = 0;
|
||||
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clearFlushTimer = () => {
|
||||
if (!flushTimer) {
|
||||
return;
|
||||
}
|
||||
clearTimer(flushTimer);
|
||||
flushTimer = null;
|
||||
};
|
||||
|
||||
const flushPending = () => {
|
||||
if (pending === null) {
|
||||
return;
|
||||
}
|
||||
const value = pending;
|
||||
pending = null;
|
||||
pendingAt = 0;
|
||||
clearFlushTimer();
|
||||
params.submit(value);
|
||||
};
|
||||
|
||||
const scheduleFlush = () => {
|
||||
clearFlushTimer();
|
||||
flushTimer = setTimer(() => {
|
||||
flushPending();
|
||||
}, windowMs);
|
||||
};
|
||||
|
||||
return (value: string) => {
|
||||
if (!params.enabled) {
|
||||
params.submit(value);
|
||||
return;
|
||||
}
|
||||
if (value.includes("\n")) {
|
||||
flushPending();
|
||||
params.submit(value);
|
||||
return;
|
||||
}
|
||||
const ts = now();
|
||||
if (pending === null) {
|
||||
pending = value;
|
||||
pendingAt = ts;
|
||||
scheduleFlush();
|
||||
return;
|
||||
}
|
||||
if (ts - pendingAt <= windowMs) {
|
||||
pending = `${pending}\n${value}`;
|
||||
pendingAt = ts;
|
||||
scheduleFlush();
|
||||
return;
|
||||
}
|
||||
flushPending();
|
||||
pending = value;
|
||||
pendingAt = ts;
|
||||
scheduleFlush();
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTuiSessionKey(params: {
|
||||
raw?: string;
|
||||
sessionScope: SessionScope;
|
||||
@@ -612,12 +705,16 @@ export async function runTui(opts: TuiOptions) {
|
||||
closeOverlay,
|
||||
});
|
||||
updateAutocompleteProvider();
|
||||
editor.onSubmit = createEditorSubmitHandler({
|
||||
const submitHandler = createEditorSubmitHandler({
|
||||
editor,
|
||||
handleCommand,
|
||||
sendMessage,
|
||||
handleBangLine: runLocalShellLine,
|
||||
});
|
||||
editor.onSubmit = createSubmitBurstCoalescer({
|
||||
submit: submitHandler,
|
||||
enabled: shouldEnableWindowsGitBashPasteFallback(),
|
||||
});
|
||||
|
||||
editor.onEscape = () => {
|
||||
void abortActive();
|
||||
|
||||
Reference in New Issue
Block a user