mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
perf(ui): guard chat composer controls
Reduce Control UI draft-update work by guarding chat composer controls while keeping locale, session, model, settings, and busy-state invalidation. Verification: focused UI tests, format/lint/typecheck, autoreview clean, and changed gate tbx_01kt12rgjs8c077p2s0wmcsbyf.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { html, render } from "lit";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "../i18n/index.ts";
|
||||
import type { AppViewState } from "./app-view-state.ts";
|
||||
import type { ChatProps } from "./views/chat.ts";
|
||||
import type { QuickSettingsProps } from "./views/config-quick.ts";
|
||||
@@ -13,6 +14,7 @@ const chatProps = vi.hoisted(() => ({
|
||||
current: null as ChatProps | null,
|
||||
}));
|
||||
const localStorageValues = vi.hoisted(() => new Map<string, string>());
|
||||
const renderChatControlsMock = vi.hoisted(() => vi.fn(() => "chat-controls"));
|
||||
|
||||
vi.mock("../local-storage.ts", () => ({
|
||||
getSafeLocalStorage: () => ({
|
||||
@@ -33,10 +35,18 @@ vi.mock("./views/config-quick.ts", () => ({
|
||||
vi.mock("./views/chat.ts", () => ({
|
||||
renderChat: (props: ChatProps) => {
|
||||
chatProps.current = props;
|
||||
return html`<div data-testid="chat"></div>`;
|
||||
return html`<div data-testid="chat">${props.composerControls}</div>`;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./app-render.helpers.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./app-render.helpers.ts")>();
|
||||
return {
|
||||
...actual,
|
||||
renderChatControls: renderChatControlsMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./icons.ts", () => ({
|
||||
icons: {},
|
||||
}));
|
||||
@@ -218,10 +228,12 @@ function createState(overrides: Partial<AppViewState> = {}): AppViewState {
|
||||
} as unknown as AppViewState;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await i18n.setLocale("en");
|
||||
localStorageValues.clear();
|
||||
quickSettingsProps.current = null;
|
||||
chatProps.current = null;
|
||||
renderChatControlsMock.mockClear();
|
||||
});
|
||||
|
||||
describe("renderApp assistant avatar routing", () => {
|
||||
@@ -332,6 +344,35 @@ describe("renderApp assistant avatar routing", () => {
|
||||
expect(chatProps.current?.error).toBe("gateway disconnected");
|
||||
});
|
||||
|
||||
it("does not rebuild chat composer controls for draft-only rerenders", () => {
|
||||
const container = document.createElement("div");
|
||||
const state = createState({ tab: "chat", chatMessage: "" });
|
||||
|
||||
render(renderApp(state), container);
|
||||
state.chatMessage = "h";
|
||||
render(renderApp(state), container);
|
||||
state.chatMessage = "hello";
|
||||
render(renderApp(state), container);
|
||||
|
||||
expect(renderChatControlsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
state.chatSending = true;
|
||||
render(renderApp(state), container);
|
||||
|
||||
expect(renderChatControlsMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("rebuilds chat composer controls after locale changes", async () => {
|
||||
const container = document.createElement("div");
|
||||
const state = createState({ tab: "chat", chatMessage: "" });
|
||||
|
||||
render(renderApp(state), container);
|
||||
await i18n.setLocale("zh-CN");
|
||||
render(renderApp(state), container);
|
||||
|
||||
expect(renderChatControlsMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("passes security quick setting fields to Quick Settings", () => {
|
||||
const state = createState({
|
||||
configForm: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
import { styleMap } from "lit/directives/style-map.js";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { i18n, t } from "../i18n/index.ts";
|
||||
import { getSafeLocalStorage } from "../local-storage.ts";
|
||||
import {
|
||||
createChatSessionsLoadOverrides,
|
||||
@@ -754,6 +755,46 @@ function renderMeasured<T>(
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderGuardedChatControls(state: AppViewState) {
|
||||
return guard(
|
||||
[
|
||||
state.sessionKey,
|
||||
state.connected,
|
||||
state.client,
|
||||
state.onboarding,
|
||||
state.chatManualRefreshInFlight,
|
||||
state.chatLoading,
|
||||
state.chatSending,
|
||||
state.chatStream,
|
||||
state.chatRunId,
|
||||
state.chatMobileControlsOpen,
|
||||
state.sessionsHideCron ?? true,
|
||||
state.sessionsResult,
|
||||
state.sessionsShowArchived,
|
||||
state.agentsList,
|
||||
state.chatModelOverrides,
|
||||
state.chatModelSwitchPromises,
|
||||
state.chatModelsLoading,
|
||||
state.chatModelCatalog,
|
||||
state.settings.chatShowThinking,
|
||||
state.settings.chatShowToolCalls,
|
||||
state.settings.chatAutoScroll,
|
||||
state.chatSessionPickerOpen,
|
||||
state.chatSessionPickerSurface,
|
||||
state.chatSessionPickerQuery,
|
||||
state.chatSessionPickerAppliedQuery,
|
||||
state.chatSessionPickerLoading,
|
||||
state.chatSessionPickerError,
|
||||
state.chatSessionPickerResult,
|
||||
state.sessionSwitchNotice?.id ?? null,
|
||||
state.sessionSwitchNotice?.text ?? null,
|
||||
state.sessionSwitchFlashKey,
|
||||
i18n.getLocale(),
|
||||
],
|
||||
() => renderChatControls(state),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
|
||||
const list = state.agentsList?.agents ?? [];
|
||||
const parsed = parseAgentSessionKey(state.sessionKey);
|
||||
@@ -3149,7 +3190,7 @@ export function renderApp(state: AppViewState) {
|
||||
runStatus: state.chatRunStatus,
|
||||
onDismissError: () => dismissChatError(state),
|
||||
sessions: state.sessionsResult,
|
||||
composerControls: renderChatControls(state),
|
||||
composerControls: renderGuardedChatControls(state),
|
||||
workspaceFiles: {
|
||||
agentId: chatAgentId,
|
||||
list:
|
||||
|
||||
@@ -179,7 +179,7 @@ export type ChatProps = {
|
||||
onSplitRatioChange?: (ratio: number) => void;
|
||||
onChatScroll?: (event: Event) => void;
|
||||
basePath?: string;
|
||||
composerControls?: TemplateResult | typeof nothing;
|
||||
composerControls?: TemplateResult | typeof nothing | ReturnType<typeof guard>;
|
||||
workspaceFiles?: {
|
||||
agentId: string;
|
||||
list: AgentsFilesListResult | null;
|
||||
|
||||
Reference in New Issue
Block a user