perf: trim tui startup and refresh work

This commit is contained in:
Peter Steinberger
2026-05-31 16:29:56 +01:00
parent 507c6fd5ca
commit b9dc3c3894
3 changed files with 124 additions and 5 deletions

View File

@@ -190,6 +190,50 @@ describe("tui session actions", () => {
expect(listSessions).toHaveBeenCalledTimes(2);
});
it("skips UI work when session refresh metadata is unchanged", async () => {
const listSessions = vi.fn().mockResolvedValue({
ts: Date.now(),
path: "/tmp/sessions.json",
count: 1,
defaults: {},
sessions: [
{
key: "agent:main:main",
model: "sonnet-4.6",
modelProvider: "anthropic",
totalTokens: 42,
updatedAt: 200,
},
],
});
const state = createBaseState({
sessionInfo: {
model: "sonnet-4.6",
modelProvider: "anthropic",
totalTokens: 42,
updatedAt: 100,
},
});
const updateFooter = vi.fn();
const updateAutocompleteProvider = vi.fn();
const requestRender = vi.fn();
const { refreshSessionInfo } = createTestSessionActions({
client: { listSessions } as unknown as TuiBackend,
state,
updateFooter,
updateAutocompleteProvider,
tui: { requestRender } as unknown as import("@earendil-works/pi-tui").TUI,
});
await refreshSessionInfo();
expect(state.sessionInfo.updatedAt).toBe(200);
expect(updateAutocompleteProvider).not.toHaveBeenCalled();
expect(updateFooter).not.toHaveBeenCalled();
expect(requestRender).not.toHaveBeenCalled();
});
it("keeps patched model selection when a refresh returns an older snapshot", async () => {
const listSessions = vi.fn().mockResolvedValue({
ts: Date.now(),

View File

@@ -49,6 +49,46 @@ type SessionInfoEntry = SessionInfo & {
providerOverride?: string;
};
function thinkingLevelsEqual(
left?: Array<{ id: string; label: string }>,
right?: Array<{ id: string; label: string }>,
): boolean {
if (left === right) {
return true;
}
if (!left || !right || left.length !== right.length) {
return false;
}
return left.every((level, index) => {
const other = right[index];
return other?.id === level.id && other.label === level.label;
});
}
function goalEquals(left: SessionInfo["goal"], right: SessionInfo["goal"]): boolean {
return left === right || JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
}
function sessionInfoUiEquals(left: SessionInfo, right: SessionInfo): boolean {
return (
left.thinkingLevel === right.thinkingLevel &&
thinkingLevelsEqual(left.thinkingLevels, right.thinkingLevels) &&
left.fastMode === right.fastMode &&
left.verboseLevel === right.verboseLevel &&
left.traceLevel === right.traceLevel &&
left.reasoningLevel === right.reasoningLevel &&
left.model === right.model &&
left.modelProvider === right.modelProvider &&
left.contextTokens === right.contextTokens &&
left.inputTokens === right.inputTokens &&
left.outputTokens === right.outputTokens &&
left.totalTokens === right.totalTokens &&
left.responseUsage === right.responseUsage &&
left.displayName === right.displayName &&
goalEquals(left.goal, right.goal)
);
}
export function createSessionActions(context: SessionActionContext) {
const {
client,
@@ -225,10 +265,17 @@ export function createSessionActions(context: SessionActionContext) {
next.model = selection.model;
}
const previous = state.sessionInfo;
const uiChanged = !sessionInfoUiEquals(previous, next);
if (!uiChanged && previous.updatedAt === next.updatedAt) {
return;
}
state.sessionInfo = next;
updateAutocompleteProvider();
updateFooter();
tui.requestRender();
if (uiChanged) {
updateAutocompleteProvider();
updateFooter();
tui.requestRender();
}
};
const runRefreshSessionInfo = async () => {

View File

@@ -29,7 +29,6 @@ import {
import { getSlashCommands } from "./commands.js";
import { ChatLog } from "./components/chat-log.js";
import { CustomEditor } from "./components/custom-editor.js";
import { GatewayChatClient } from "./gateway-chat.js";
import { resolveLocalRunShutdownGraceMs } from "./local-run-shutdown.js";
import { editorTheme, theme } from "./theme/theme.js";
import type { TuiBackend } from "./tui-backend.js";
@@ -522,6 +521,8 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
let dynamicSlashCommandsKey: string | null = null;
let dynamicSlashCommandsInFlightKey: string | null = null;
let dynamicSlashCommandsRequestId = 0;
let dynamicSlashCommandsReady = false;
let dynamicSlashCommandsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
let lastCtrlCAt = 0;
let exitRequested = false;
let exitResult: TuiResult = { exitReason: "exit" };
@@ -714,6 +715,7 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
const { EmbeddedTuiBackend } = await import("./embedded-backend.js");
client = new EmbeddedTuiBackend();
} else {
const { GatewayChatClient } = await import("./gateway-chat.js");
client = await GatewayChatClient.connect({
url: opts.url,
token: opts.token,
@@ -769,9 +771,19 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
);
};
const clearDynamicSlashCommandsRefreshTimer = () => {
if (!dynamicSlashCommandsRefreshTimer) {
return;
}
clearTimeout(dynamicSlashCommandsRefreshTimer);
dynamicSlashCommandsRefreshTimer = null;
};
const refreshDynamicSlashCommands = () => {
clearDynamicSlashCommandsRefreshTimer();
const key = resolveDynamicSlashCommandsKey();
if (
!dynamicSlashCommandsReady ||
!isConnected ||
!client.listCommands ||
dynamicSlashCommandsKey === key ||
@@ -807,9 +819,21 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
});
};
const scheduleDynamicSlashCommandsRefresh = () => {
if (
!dynamicSlashCommandsReady ||
dynamicSlashCommandsRefreshTimer ||
dynamicSlashCommandsKey === resolveDynamicSlashCommandsKey()
) {
return;
}
dynamicSlashCommandsRefreshTimer = setTimeout(refreshDynamicSlashCommands, 0);
dynamicSlashCommandsRefreshTimer.unref?.();
};
const updateAutocompleteProvider = () => {
applyAutocompleteProvider();
refreshDynamicSlashCommands();
scheduleDynamicSlashCommandsRefresh();
};
tui.addChild(root);
@@ -1474,6 +1498,8 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
4000,
);
tui.requestRender();
dynamicSlashCommandsReady = true;
scheduleDynamicSlashCommandsRefresh();
if (!autoMessageSent && autoMessage) {
autoMessageSent = true;
await sendMessage(autoMessage);
@@ -1494,6 +1520,8 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
dynamicSlashCommands = [];
dynamicSlashCommandsKey = null;
dynamicSlashCommandsInFlightKey = null;
dynamicSlashCommandsReady = false;
clearDynamicSlashCommandsRefreshTimer();
dynamicSlashCommandsRequestId += 1;
updateAutocompleteProvider();
pauseStreamingWatchdog();