perf: reduce tui refresh work

This commit is contained in:
Peter Steinberger
2026-05-31 16:10:02 +01:00
parent 6b1b2ff20a
commit 45ab822918
6 changed files with 155 additions and 13 deletions
+16
View File
@@ -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++) {
+7
View File
@@ -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)) {
+57 -1
View File
@@ -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({
+7 -2
View File
@@ -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)}`);
}
+48 -4
View File
@@ -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(),
+20 -6
View File
@@ -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 = (