Skip to content

Commit d9db117

Browse files
committed
perf: memoize input box components
1 parent 144445b commit d9db117

File tree

3 files changed

+82
-16
lines changed

3 files changed

+82
-16
lines changed

gui/src/components/mainInput/ContinueInputBox.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Editor, JSONContent } from "@tiptap/react";
22
import { ContextItemWithId, InputModifiers, RuleWithSource } from "core";
3-
import { useMemo } from "react";
3+
import { memo, useMemo } from "react";
44
import { defaultBorderRadius, vscBackground } from "..";
55
import { useAppSelector } from "../../redux/hooks";
66
import { selectSlashCommandComboBoxInputs } from "../../redux/selectors";
@@ -69,16 +69,20 @@ function ContinueInputBox(props: ContinueInputBoxProps) {
6969
const historyKey = isInEdit ? "edit" : "chat";
7070
const placeholder = isInEdit ? "Edit selected code" : undefined;
7171

72-
const toolbarOptions: ToolbarOptions = isInEdit
73-
? {
72+
const toolbarOptions: ToolbarOptions = useMemo(() => {
73+
if (isInEdit) {
74+
return {
7475
hideAddContext: false,
7576
hideImageUpload: false,
7677
hideUseCodebase: true,
7778
hideSelectModel: false,
7879
enterText:
7980
editModeState.applyState.status === "done" ? "Retry" : "Edit",
80-
}
81-
: {};
81+
} as ToolbarOptions;
82+
}
83+
// Stable empty object to avoid re-renders from identity changes
84+
return {} as ToolbarOptions;
85+
}, [isInEdit, editModeState.applyState.status]);
8286

8387
const { appliedRules = [], contextItems = [] } = props;
8488

@@ -123,4 +127,4 @@ function ContinueInputBox(props: ContinueInputBoxProps) {
123127
);
124128
}
125129

126-
export default ContinueInputBox;
130+
export default memo(ContinueInputBox);

gui/src/components/mainInput/InputToolbar.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
modelSupportsImages,
1010
modelSupportsReasoning,
1111
} from "core/llm/autodetect";
12-
import { useContext, useRef } from "react";
12+
import { memo, useContext, useRef } from "react";
1313
import { IdeMessengerContext } from "../../context/IdeMessenger";
1414
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
1515
import { selectUseActiveFile } from "../../redux/selectors";
@@ -242,4 +242,24 @@ function InputToolbar(props: InputToolbarProps) {
242242
);
243243
}
244244

245-
export default InputToolbar;
245+
function shallowToolbarOptionsEqual(a?: ToolbarOptions, b?: ToolbarOptions) {
246+
if (a === b) return true;
247+
if (!a || !b) return false;
248+
return (
249+
a.hideAddContext === b.hideAddContext &&
250+
a.hideImageUpload === b.hideImageUpload &&
251+
a.hideUseCodebase === b.hideUseCodebase &&
252+
a.hideSelectModel === b.hideSelectModel &&
253+
a.enterText === b.enterText
254+
);
255+
}
256+
257+
export default memo(
258+
InputToolbar,
259+
(prev, next) =>
260+
prev.hidden === next.hidden &&
261+
prev.disabled === next.disabled &&
262+
prev.isMainInput === next.isMainInput &&
263+
prev.activeKey === next.activeKey &&
264+
shallowToolbarOptionsEqual(prev.toolbarOptions, next.toolbarOptions),
265+
);

gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { Editor, EditorContent, JSONContent } from "@tiptap/react";
22
import { ContextProviderDescription, InputModifiers } from "core";
33
import { modelSupportsImages } from "core/llm/autodetect";
4-
import { useCallback, useContext, useEffect, useRef, useState } from "react";
4+
import {
5+
memo,
6+
useCallback,
7+
useContext,
8+
useEffect,
9+
useRef,
10+
useState,
11+
} from "react";
512
import { IdeMessengerContext } from "../../../context/IdeMessenger";
613
import useIsOSREnabled from "../../../hooks/useIsOSREnabled";
714
import useUpdatingRef from "../../../hooks/useUpdatingRef";
@@ -38,7 +45,7 @@ export interface TipTapEditorProps {
3845

3946
export const TIPPY_DIV_ID = "tippy-js-div";
4047

41-
export function TipTapEditor(props: TipTapEditorProps) {
48+
function TipTapEditorInner(props: TipTapEditorProps) {
4249
const dispatch = useAppDispatch();
4350
const mainEditorContext = useMainEditor();
4451

@@ -72,12 +79,13 @@ export function TipTapEditor(props: TipTapEditorProps) {
7279
return;
7380
}
7481
const placeholder = getPlaceholderText(props.placeholder, historyLength);
75-
76-
editor.extensionManager.extensions.filter(
77-
(extension) => extension.name === "placeholder",
78-
)[0].options["placeholder"] = placeholder;
79-
80-
editor.view.dispatch(editor.state.tr);
82+
const placeholderExt = editor.extensionManager.extensions.find(
83+
(e) => e.name === "placeholder",
84+
) as any;
85+
if (placeholderExt) {
86+
placeholderExt.options["placeholder"] = placeholder;
87+
editor.view.dispatch(editor.state.tr);
88+
}
8189
}, [editor, props.placeholder, historyLength]);
8290

8391
useEffect(() => {
@@ -281,3 +289,37 @@ export function TipTapEditor(props: TipTapEditorProps) {
281289
</InputBoxDiv>
282290
);
283291
}
292+
293+
function toolbarOptionsEqual(a?: ToolbarOptions, b?: ToolbarOptions) {
294+
if (a === b) return true;
295+
if (!a || !b) return false;
296+
return (
297+
a.hideAddContext === b.hideAddContext &&
298+
a.hideImageUpload === b.hideImageUpload &&
299+
a.hideUseCodebase === b.hideUseCodebase &&
300+
a.hideSelectModel === b.hideSelectModel &&
301+
a.enterText === b.enterText
302+
);
303+
}
304+
305+
const MemoInner = memo(
306+
TipTapEditorInner,
307+
(prev, next) =>
308+
prev.isMainInput === next.isMainInput &&
309+
prev.placeholder === next.placeholder &&
310+
prev.historyKey === next.historyKey &&
311+
prev.inputId === next.inputId &&
312+
toolbarOptionsEqual(prev.toolbarOptions, next.toolbarOptions) &&
313+
(prev.availableContextProviders?.length || 0) ===
314+
(next.availableContextProviders?.length || 0) &&
315+
(prev.availableSlashCommands?.length || 0) ===
316+
(next.availableSlashCommands?.length || 0),
317+
);
318+
319+
export function TipTapEditor(props: TipTapEditorProps) {
320+
return (
321+
<div className="relative w-full">
322+
<MemoInner {...props} />
323+
</div>
324+
);
325+
}

0 commit comments

Comments
 (0)