mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
fix(useInstalledSkills): keep agent skills list stable while toggling (#14472)
### What this PR does Before this PR: Enabling or disabling a skill in agent mode refreshed the whole installed skills list, which unmounted the scrollable content and jumped the view back to the top. After this PR: The toggled skill is updated in place, so the list stays mounted and the scroll position is preserved. Fixes #14461 ### Why we need it and why it was done in this way The following tradeoffs were made: Avoiding a full list refresh on every toggle keeps the UI stable and prevents the scroll reset. The following alternatives were considered: Preserving scroll position across a list refresh in the scrollbar wrapper, but updating the toggled item directly is smaller and avoids remounting the list. Links to places where the discussion took place: https://github.com/CherryHQ/cherry-studio/issues/14461 ### Breaking changes None. ### Special notes for your reviewer The hook test covers the in-place update behavior and verifies the list is not reloaded after a toggle. ### Checklist This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR. Approvers are expected to review this list. - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle) - [ ] Refactor: You have [left the code cleaner than you found it](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html) - [ ] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [x] Self-review: I have reviewed my own code (e.g., via [`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`, or GitHub UI) before requesting review from others ### Release note ```release-note Fix the agent skills page so enabling or disabling a skill no longer resets the scroll position to the top. ``` --------- Signed-off-by: 404-Page-Found <Lucas20220605@gmail.com>
This commit is contained in:
78
src/renderer/src/hooks/__tests__/useSkills.test.ts
Normal file
78
src/renderer/src/hooks/__tests__/useSkills.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { InstalledSkill } from '@renderer/types'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useInstalledSkills } from '../useSkills'
|
||||
|
||||
const mockList = vi.fn()
|
||||
const mockToggle = vi.fn()
|
||||
const mockUninstall = vi.fn()
|
||||
|
||||
function createSkill(overrides: Partial<InstalledSkill> = {}): InstalledSkill {
|
||||
return {
|
||||
id: 'skill-1',
|
||||
name: 'Skill One',
|
||||
description: 'First skill',
|
||||
folderName: 'skill-one',
|
||||
source: 'builtin',
|
||||
sourceUrl: null,
|
||||
namespace: null,
|
||||
author: null,
|
||||
tags: [],
|
||||
contentHash: 'hash-1',
|
||||
isEnabled: false,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('useInstalledSkills', () => {
|
||||
beforeEach(() => {
|
||||
mockList.mockResolvedValue({
|
||||
success: true,
|
||||
data: [
|
||||
createSkill(),
|
||||
createSkill({ id: 'skill-2', name: 'Skill Two', folderName: 'skill-two', contentHash: 'hash-2' })
|
||||
]
|
||||
})
|
||||
mockToggle.mockImplementation(async ({ skillId, isEnabled }) => ({
|
||||
success: true,
|
||||
data: createSkill({ id: skillId, isEnabled, updatedAt: 2 })
|
||||
}))
|
||||
mockUninstall.mockResolvedValue({ success: true, data: null })
|
||||
|
||||
;(window as any).api = {
|
||||
skill: {
|
||||
list: mockList,
|
||||
toggle: mockToggle,
|
||||
uninstall: mockUninstall
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as any).api
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('updates the toggled skill in place without reloading the list', async () => {
|
||||
const { result } = renderHook(() => useInstalledSkills('agent-1'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.skills).toHaveLength(2)
|
||||
})
|
||||
|
||||
let toggleSuccess = false
|
||||
await act(async () => {
|
||||
toggleSuccess = await result.current.toggle('skill-1', true)
|
||||
})
|
||||
|
||||
expect(toggleSuccess).toBe(true)
|
||||
expect(mockToggle).toHaveBeenCalledWith({ skillId: 'skill-1', agentId: 'agent-1', isEnabled: true })
|
||||
expect(mockList).toHaveBeenCalledTimes(1)
|
||||
expect(result.current.skills.find((skill) => skill.id === 'skill-1')).toEqual(
|
||||
createSkill({ id: 'skill-1', isEnabled: true, updatedAt: 2 })
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -47,14 +47,19 @@ export function useInstalledSkills(agentId?: string) {
|
||||
try {
|
||||
const result = await window.api.skill.toggle({ skillId, agentId, isEnabled })
|
||||
if (result.success) {
|
||||
await refresh()
|
||||
const updatedSkill = result.data
|
||||
if (updatedSkill) {
|
||||
setSkills((currentSkills) =>
|
||||
currentSkills.map((skill) => (skill.id === updatedSkill.id ? updatedSkill : skill))
|
||||
)
|
||||
}
|
||||
}
|
||||
return result.success
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
[agentId, refresh]
|
||||
[agentId]
|
||||
)
|
||||
|
||||
const uninstall = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user