mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
perf: reduce tui refresh work
This commit is contained in:
@@ -97,6 +97,22 @@ describe("ChatLog", () => {
|
||||
expect(chatLog.children.length).toBe(20);
|
||||
});
|
||||
|
||||
it("clears visible tool entries and stale tool references", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
chatLog.startTool("tool-1", "read_file", { path: "a.txt" });
|
||||
chatLog.updateToolResult("tool-1", { content: [{ type: "text", text: "done" }] });
|
||||
|
||||
let rendered = normalizeTestText(chatLog.render(120).join("\n"));
|
||||
expect(rendered).toContain("Read File");
|
||||
|
||||
chatLog.clearTools();
|
||||
chatLog.updateToolResult("tool-1", { content: [{ type: "text", text: "stale" }] });
|
||||
|
||||
rendered = normalizeTestText(chatLog.render(120).join("\n"));
|
||||
expect(rendered).not.toContain("Read File");
|
||||
expect(rendered).not.toContain("stale");
|
||||
});
|
||||
|
||||
it("prunes system messages atomically when a non-system entry overflows the log", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
|
||||
@@ -99,6 +99,13 @@ export class ChatLog extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
clearTools() {
|
||||
for (const tool of this.toolById.values()) {
|
||||
this.removeChild(tool);
|
||||
}
|
||||
this.toolById.clear();
|
||||
}
|
||||
|
||||
restorePendingUsers() {
|
||||
for (const entry of this.pendingUsers.values()) {
|
||||
if (this.children.includes(entry.component)) {
|
||||
|
||||
@@ -101,6 +101,7 @@ function createHarness(params?: {
|
||||
params?.setEmptySession ?? (vi.fn().mockResolvedValue(undefined) as SetEmptySessionMock);
|
||||
const addUser = vi.fn();
|
||||
const addSystem = vi.fn();
|
||||
const clearTools = vi.fn();
|
||||
const reserveAssistantSlot = vi.fn();
|
||||
const requestRender = vi.fn();
|
||||
const noteLocalRunId = vi.fn();
|
||||
@@ -144,7 +145,7 @@ function createHarness(params?: {
|
||||
resetSession,
|
||||
runGoalCommand,
|
||||
} as never,
|
||||
chatLog: { addUser, addSystem, reserveAssistantSlot } as never,
|
||||
chatLog: { addUser, addSystem, clearTools, reserveAssistantSlot } as never,
|
||||
tui: { requestRender } as never,
|
||||
opts: params?.opts ?? {},
|
||||
state: state as never,
|
||||
@@ -187,6 +188,7 @@ function createHarness(params?: {
|
||||
setEmptySession,
|
||||
addUser,
|
||||
addSystem,
|
||||
clearTools,
|
||||
reserveAssistantSlot,
|
||||
requestRender,
|
||||
loadHistory,
|
||||
@@ -732,6 +734,60 @@ describe("tui command handlers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hides tools locally for /verbose off without reloading history", async () => {
|
||||
const patchResult = { entry: { verboseLevel: "off" } };
|
||||
const patchSession = vi.fn().mockResolvedValue(patchResult);
|
||||
const applySessionInfoFromPatch = vi.fn();
|
||||
const loadHistory = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshSessionInfo = vi.fn().mockResolvedValue(undefined);
|
||||
const { handleCommand, clearTools } = createHarness({
|
||||
patchSession,
|
||||
applySessionInfoFromPatch,
|
||||
loadHistory,
|
||||
refreshSessionInfo,
|
||||
});
|
||||
|
||||
await handleCommand("/verbose off");
|
||||
|
||||
expect(patchSession).toHaveBeenCalledWith({
|
||||
key: "agent:main:main",
|
||||
verboseLevel: "off",
|
||||
});
|
||||
expect(applySessionInfoFromPatch).toHaveBeenCalledWith(patchResult);
|
||||
expect(clearTools).toHaveBeenCalledTimes(1);
|
||||
expect(refreshSessionInfo).toHaveBeenCalledTimes(1);
|
||||
expect(loadHistory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reloads history for /verbose on so prior tool output becomes visible", async () => {
|
||||
const loadHistory = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshSessionInfo = vi.fn().mockResolvedValue(undefined);
|
||||
const { handleCommand, clearTools } = createHarness({
|
||||
loadHistory,
|
||||
refreshSessionInfo,
|
||||
});
|
||||
|
||||
await handleCommand("/verbose on");
|
||||
|
||||
expect(loadHistory).toHaveBeenCalledTimes(1);
|
||||
expect(refreshSessionInfo).not.toHaveBeenCalled();
|
||||
expect(clearTools).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refreshes session info for /trace without reloading history", async () => {
|
||||
const loadHistory = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshSessionInfo = vi.fn().mockResolvedValue(undefined);
|
||||
const { handleCommand } = createHarness({
|
||||
loadHistory,
|
||||
refreshSessionInfo,
|
||||
});
|
||||
|
||||
await handleCommand("/trace on");
|
||||
|
||||
expect(refreshSessionInfo).toHaveBeenCalledTimes(1);
|
||||
expect(loadHistory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports send failures and marks activity status as error", async () => {
|
||||
const setActivityStatus = vi.fn();
|
||||
const { handleCommand, addSystem, state } = createHarness({
|
||||
|
||||
@@ -507,7 +507,12 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
});
|
||||
chatLog.addSystem(`verbose set to ${args}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await loadHistory();
|
||||
if (args === "off") {
|
||||
chatLog.clearTools();
|
||||
await refreshSessionInfo();
|
||||
} else {
|
||||
await loadHistory();
|
||||
}
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`verbose failed: ${String(err)}`);
|
||||
}
|
||||
@@ -524,7 +529,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
});
|
||||
chatLog.addSystem(`trace set to ${args}`);
|
||||
applySessionInfoFromPatch(result);
|
||||
await loadHistory();
|
||||
await refreshSessionInfo();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`trace failed: ${String(err)}`);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("tui session actions", () => {
|
||||
const first = refreshSessionInfo();
|
||||
const second = refreshSessionInfo();
|
||||
|
||||
await Promise.resolve();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(listSessions).toHaveBeenCalledTimes(1);
|
||||
expect(listSessions).toHaveBeenNthCalledWith(1, {
|
||||
limit: TUI_SESSION_LOOKUP_LIMIT,
|
||||
@@ -119,8 +119,7 @@ describe("tui session actions", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await first;
|
||||
await Promise.resolve();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(listSessions).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -138,7 +137,7 @@ describe("tui session actions", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await second;
|
||||
await Promise.all([first, second]);
|
||||
|
||||
expect(state.sessionInfo.model).toBe("Minimax-M2.7");
|
||||
expect(updateAutocompleteProvider).toHaveBeenCalledTimes(2);
|
||||
@@ -146,6 +145,51 @@ describe("tui session actions", () => {
|
||||
expect(requestRender).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("coalesces refresh bursts into a single follow-up lookup", async () => {
|
||||
let resolveFirst: ((value: unknown) => void) | undefined;
|
||||
let resolveSecond: ((value: unknown) => void) | undefined;
|
||||
|
||||
const listSessions = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveFirst = resolve;
|
||||
}),
|
||||
)
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveSecond = resolve;
|
||||
}),
|
||||
);
|
||||
const { refreshSessionInfo } = createTestSessionActions({
|
||||
client: { listSessions } as unknown as TuiBackend,
|
||||
});
|
||||
|
||||
const first = refreshSessionInfo();
|
||||
const second = refreshSessionInfo();
|
||||
const third = refreshSessionInfo();
|
||||
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(listSessions).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveFirst?.({
|
||||
defaults: {},
|
||||
sessions: [{ key: "agent:main:main", updatedAt: 1 }],
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(listSessions).toHaveBeenCalledTimes(2);
|
||||
|
||||
resolveSecond?.({
|
||||
defaults: {},
|
||||
sessions: [{ key: "agent:main:main", updatedAt: 2 }],
|
||||
});
|
||||
await Promise.all([first, second, third]);
|
||||
|
||||
expect(listSessions).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps patched model selection when a refresh returns an older snapshot", async () => {
|
||||
const listSessions = vi.fn().mockResolvedValue({
|
||||
ts: Date.now(),
|
||||
|
||||
@@ -69,7 +69,8 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
rememberSessionKey,
|
||||
emptySessionInfoDefaults,
|
||||
} = context;
|
||||
let refreshSessionInfoPromise: Promise<void> = Promise.resolve();
|
||||
let refreshSessionInfoInFlight: Promise<void> | null = null;
|
||||
let refreshSessionInfoQueued = false;
|
||||
let lastSessionDefaults: SessionInfoDefaults | null = null;
|
||||
|
||||
const applyAgentsResult = (result: TuiAgentsList) => {
|
||||
@@ -274,12 +275,25 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
}
|
||||
};
|
||||
|
||||
const drainRefreshSessionInfo = async () => {
|
||||
do {
|
||||
// Many TUI paths ask for the same session snapshot at once; keep one in-flight
|
||||
// lookup and at most one follow-up so bursts do not queue stale backend calls.
|
||||
refreshSessionInfoQueued = false;
|
||||
await runRefreshSessionInfo();
|
||||
} while (refreshSessionInfoQueued);
|
||||
};
|
||||
|
||||
const refreshSessionInfo = async () => {
|
||||
refreshSessionInfoPromise = refreshSessionInfoPromise.then(
|
||||
runRefreshSessionInfo,
|
||||
runRefreshSessionInfo,
|
||||
);
|
||||
await refreshSessionInfoPromise;
|
||||
if (refreshSessionInfoInFlight) {
|
||||
refreshSessionInfoQueued = true;
|
||||
await refreshSessionInfoInFlight;
|
||||
return;
|
||||
}
|
||||
refreshSessionInfoInFlight = drainRefreshSessionInfo().finally(() => {
|
||||
refreshSessionInfoInFlight = null;
|
||||
});
|
||||
await refreshSessionInfoInFlight;
|
||||
};
|
||||
|
||||
const applySessionInfoFromPatch = (
|
||||
|
||||
Reference in New Issue
Block a user