Skip to content

Commit 6627b4f

Browse files
authored
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.
1 parent 3b64ea8 commit 6627b4f

3 files changed

Lines changed: 87 additions & 5 deletions

File tree

ui/src/ui/app-render.assistant-avatar.test.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { html, render } from "lit";
44
import { beforeEach, describe, expect, it, vi } from "vitest";
5+
import { i18n } from "../i18n/index.ts";
56
import type { AppViewState } from "./app-view-state.ts";
67
import type { ChatProps } from "./views/chat.ts";
78
import type { QuickSettingsProps } from "./views/config-quick.ts";
@@ -13,6 +14,7 @@ const chatProps = vi.hoisted(() => ({
1314
current: null as ChatProps | null,
1415
}));
1516
const localStorageValues = vi.hoisted(() => new Map<string, string>());
17+
const renderChatControlsMock = vi.hoisted(() => vi.fn(() => "chat-controls"));
1618

1719
vi.mock("../local-storage.ts", () => ({
1820
getSafeLocalStorage: () => ({
@@ -33,10 +35,18 @@ vi.mock("./views/config-quick.ts", () => ({
3335
vi.mock("./views/chat.ts", () => ({
3436
renderChat: (props: ChatProps) => {
3537
chatProps.current = props;
36-
return html`<div data-testid="chat"></div>`;
38+
return html`<div data-testid="chat">${props.composerControls}</div>`;
3739
},
3840
}));
3941

42+
vi.mock("./app-render.helpers.ts", async (importOriginal) => {
43+
const actual = await importOriginal<typeof import("./app-render.helpers.ts")>();
44+
return {
45+
...actual,
46+
renderChatControls: renderChatControlsMock,
47+
};
48+
});
49+
4050
vi.mock("./icons.ts", () => ({
4151
icons: {},
4252
}));
@@ -218,10 +228,12 @@ function createState(overrides: Partial<AppViewState> = {}): AppViewState {
218228
} as unknown as AppViewState;
219229
}
220230

221-
beforeEach(() => {
231+
beforeEach(async () => {
232+
await i18n.setLocale("en");
222233
localStorageValues.clear();
223234
quickSettingsProps.current = null;
224235
chatProps.current = null;
236+
renderChatControlsMock.mockClear();
225237
});
226238

227239
describe("renderApp assistant avatar routing", () => {
@@ -332,6 +344,35 @@ describe("renderApp assistant avatar routing", () => {
332344
expect(chatProps.current?.error).toBe("gateway disconnected");
333345
});
334346

347+
it("does not rebuild chat composer controls for draft-only rerenders", () => {
348+
const container = document.createElement("div");
349+
const state = createState({ tab: "chat", chatMessage: "" });
350+
351+
render(renderApp(state), container);
352+
state.chatMessage = "h";
353+
render(renderApp(state), container);
354+
state.chatMessage = "hello";
355+
render(renderApp(state), container);
356+
357+
expect(renderChatControlsMock).toHaveBeenCalledTimes(1);
358+
359+
state.chatSending = true;
360+
render(renderApp(state), container);
361+
362+
expect(renderChatControlsMock).toHaveBeenCalledTimes(2);
363+
});
364+
365+
it("rebuilds chat composer controls after locale changes", async () => {
366+
const container = document.createElement("div");
367+
const state = createState({ tab: "chat", chatMessage: "" });
368+
369+
render(renderApp(state), container);
370+
await i18n.setLocale("zh-CN");
371+
render(renderApp(state), container);
372+
373+
expect(renderChatControlsMock).toHaveBeenCalledTimes(2);
374+
});
375+
335376
it("passes security quick setting fields to Quick Settings", () => {
336377
const state = createState({
337378
configForm: {

ui/src/ui/app-render.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { html, nothing } from "lit";
2+
import { guard } from "lit/directives/guard.js";
23
import { styleMap } from "lit/directives/style-map.js";
3-
import { t } from "../i18n/index.ts";
4+
import { i18n, t } from "../i18n/index.ts";
45
import { getSafeLocalStorage } from "../local-storage.ts";
56
import {
67
createChatSessionsLoadOverrides,
@@ -754,6 +755,46 @@ function renderMeasured<T>(
754755
return result;
755756
}
756757

758+
function renderGuardedChatControls(state: AppViewState) {
759+
return guard(
760+
[
761+
state.sessionKey,
762+
state.connected,
763+
state.client,
764+
state.onboarding,
765+
state.chatManualRefreshInFlight,
766+
state.chatLoading,
767+
state.chatSending,
768+
state.chatStream,
769+
state.chatRunId,
770+
state.chatMobileControlsOpen,
771+
state.sessionsHideCron ?? true,
772+
state.sessionsResult,
773+
state.sessionsShowArchived,
774+
state.agentsList,
775+
state.chatModelOverrides,
776+
state.chatModelSwitchPromises,
777+
state.chatModelsLoading,
778+
state.chatModelCatalog,
779+
state.settings.chatShowThinking,
780+
state.settings.chatShowToolCalls,
781+
state.settings.chatAutoScroll,
782+
state.chatSessionPickerOpen,
783+
state.chatSessionPickerSurface,
784+
state.chatSessionPickerQuery,
785+
state.chatSessionPickerAppliedQuery,
786+
state.chatSessionPickerLoading,
787+
state.chatSessionPickerError,
788+
state.chatSessionPickerResult,
789+
state.sessionSwitchNotice?.id ?? null,
790+
state.sessionSwitchNotice?.text ?? null,
791+
state.sessionSwitchFlashKey,
792+
i18n.getLocale(),
793+
],
794+
() => renderChatControls(state),
795+
);
796+
}
797+
757798
function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
758799
const list = state.agentsList?.agents ?? [];
759800
const parsed = parseAgentSessionKey(state.sessionKey);
@@ -3149,7 +3190,7 @@ export function renderApp(state: AppViewState) {
31493190
runStatus: state.chatRunStatus,
31503191
onDismissError: () => dismissChatError(state),
31513192
sessions: state.sessionsResult,
3152-
composerControls: renderChatControls(state),
3193+
composerControls: renderGuardedChatControls(state),
31533194
workspaceFiles: {
31543195
agentId: chatAgentId,
31553196
list:

ui/src/ui/views/chat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export type ChatProps = {
179179
onSplitRatioChange?: (ratio: number) => void;
180180
onChatScroll?: (event: Event) => void;
181181
basePath?: string;
182-
composerControls?: TemplateResult | typeof nothing;
182+
composerControls?: TemplateResult | typeof nothing | ReturnType<typeof guard>;
183183
workspaceFiles?: {
184184
agentId: string;
185185
list: AgentsFilesListResult | null;

0 commit comments

Comments
 (0)