fix UI/UX: Mode switch performance from 1 sec -> instant (#2997)

* Fix mode switching performance in PromptsView by using local state for instant UI updates

* Update webview-ui/src/components/prompts/PromptsView.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
dlab-anton 2025-04-28 22:10:29 +07:00 committed by GitHub
parent 7020d1db2c
commit ecc7473a02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -60,6 +60,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
customModes,
} = useExtensionState()
// Use a local state to track the visually active mode
// This prevents flickering when switching modes rapidly by:
// 1. Updating the UI immediately when a mode is clicked
// 2. Not syncing with the backend mode state (which would cause flickering)
// 3. Still sending the mode change to the backend for persistence
const [visualMode, setVisualMode] = useState(mode)
// Memoize modes to preserve array order
const modes = useMemo(() => getAllModes(customModes), [customModes])
@ -126,22 +133,25 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
// Handle mode switching with explicit state initialization
const handleModeSwitch = useCallback(
(modeConfig: ModeConfig) => {
if (modeConfig.slug === mode) return // Prevent unnecessary updates
if (modeConfig.slug === visualMode) return // Prevent unnecessary updates
// First switch the mode
// Immediately update visual state for instant feedback
setVisualMode(modeConfig.slug)
// Then send the mode change message to the backend
switchMode(modeConfig.slug)
// Exit tools edit mode when switching modes
setIsToolsEditMode(false)
},
[mode, switchMode, setIsToolsEditMode],
[visualMode, switchMode, setIsToolsEditMode],
)
// Helper function to get current mode's config
const getCurrentMode = useCallback((): ModeConfig | undefined => {
const findMode = (m: ModeConfig): boolean => m.slug === mode
const findMode = (m: ModeConfig): boolean => m.slug === visualMode
return customModes?.find(findMode) || modes.find(findMode)
}, [mode, customModes, modes])
}, [visualMode, customModes, modes])
// Helper function to safely access mode properties
const getModeProperty = <T extends keyof ModeConfig>(
@ -472,7 +482,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div className="flex gap-2 items-center mb-3 flex-wrap py-1">
{modes.map((modeConfig) => {
const isActive = mode === modeConfig.slug
const isActive = visualMode === modeConfig.slug
return (
<button
key={modeConfig.slug}
@ -493,20 +503,20 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div style={{ marginBottom: "20px" }}>
{/* Only show name and delete for custom modes */}
{mode && findModeBySlug(mode, customModes) && (
{visualMode && findModeBySlug(visualMode, customModes) && (
<div className="flex gap-3 mb-4">
<div className="flex-1">
<div className="font-bold mb-1">{t("prompts:createModeDialog.name.label")}</div>
<div className="flex gap-2">
<VSCodeTextField
value={getModeProperty(findModeBySlug(mode, customModes), "name") ?? ""}
value={getModeProperty(findModeBySlug(visualMode, customModes), "name") ?? ""}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target =
(e as CustomEvent)?.detail?.target ||
((e as any).target as HTMLInputElement)
const customMode = findModeBySlug(mode, customModes)
const customMode = findModeBySlug(visualMode, customModes)
if (customMode) {
updateCustomMode(mode, {
updateCustomMode(visualMode, {
...customMode,
name: target.value,
source: customMode.source || "global",
@ -522,7 +532,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
onClick={() => {
vscode.postMessage({
type: "deleteCustomMode",
slug: mode,
slug: visualMode,
})
}}>
<span className="codicon codicon-trash"></span>
@ -534,7 +544,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div style={{ marginBottom: "16px" }}>
<div className="flex justify-between items-center mb-1">
<div className="font-bold">{t("prompts:roleDefinition.title")}</div>
{!findModeBySlug(mode, customModes) && (
{!findModeBySlug(visualMode, customModes) && (
<Button
variant="ghost"
size="icon"
@ -555,25 +565,25 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(mode, customModes)
const prompt = customModePrompts?.[mode] as PromptComponent
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
const customMode = findModeBySlug(visualMode, customModes)
const prompt = customModePrompts?.[visualMode] as PromptComponent
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(visualMode)
})()}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const customMode = findModeBySlug(mode, customModes)
const customMode = findModeBySlug(visualMode, customModes)
if (customMode) {
// For custom modes, update the JSON file
updateCustomMode(mode, {
updateCustomMode(visualMode, {
...customMode,
roleDefinition: value.trim() || "",
source: customMode.source || "global",
})
} else {
// For built-in modes, update the prompts
updateAgentPrompt(mode, {
updateAgentPrompt(visualMode, {
roleDefinition: value.trim() || undefined,
})
}
@ -617,7 +627,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div className="mb-4">
<div className="flex justify-between items-center mb-1">
<div className="font-bold">{t("prompts:tools.title")}</div>
{findModeBySlug(mode, customModes) && (
{findModeBySlug(visualMode, customModes) && (
<Button
variant="ghost"
size="icon"
@ -632,16 +642,16 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</Button>
)}
</div>
{!findModeBySlug(mode, customModes) && (
{!findModeBySlug(visualMode, customModes) && (
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("prompts:tools.builtInModesText")}
</div>
)}
{isToolsEditMode && findModeBySlug(mode, customModes) ? (
{isToolsEditMode && findModeBySlug(visualMode, customModes) ? (
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
{availableGroups.map((group) => {
const currentMode = getCurrentMode()
const isCustomMode = findModeBySlug(mode, customModes)
const isCustomMode = findModeBySlug(visualMode, customModes)
const customMode = isCustomMode
const isGroupEnabled = isCustomMode
? customMode?.groups?.some((g) => getGroupName(g) === group)
@ -710,7 +720,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>{t("prompts:customInstructions.title")}</div>
{!findModeBySlug(mode, customModes) && (
{!findModeBySlug(visualMode, customModes) && (
<Button
variant="ghost"
size="icon"
@ -738,8 +748,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(mode, customModes)
const prompt = customModePrompts?.[mode] as PromptComponent
const customMode = findModeBySlug(visualMode, customModes)
const prompt = customModePrompts?.[visualMode] as PromptComponent
return (
customMode?.customInstructions ??
prompt?.customInstructions ??
@ -750,18 +760,18 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const customMode = findModeBySlug(mode, customModes)
const customMode = findModeBySlug(visualMode, customModes)
if (customMode) {
// For custom modes, update the JSON file
updateCustomMode(mode, {
updateCustomMode(visualMode, {
...customMode,
customInstructions: value.trim() || undefined,
source: customMode.source || "global",
})
} else {
// For built-in modes, update the prompts
const existingPrompt = customModePrompts?.[mode] as PromptComponent
updateAgentPrompt(mode, {
const existingPrompt = customModePrompts?.[visualMode] as PromptComponent
updateAgentPrompt(visualMode, {
...existingPrompt,
customInstructions: value.trim(),
})