fix(desktop): restore slash capability lists and localize computer use

Show frequent commands, skills and plugins in matching visual and keyboard order.
Update discovery hints and computer-use labels across all five locales.
This commit is contained in:
程序员阿江(Relakkes)
2026-09-20 02:24:38 +08:00
parent 2d4f48765f
commit 22e050f463
13 changed files with 127 additions and 42 deletions
+1 -1
View File
@@ -149,7 +149,7 @@ describe('Content-only pages render without errors', () => {
expect(await screen.findByRole('option', { name: '/init' })).toBeInTheDocument()
expect(screen.getByRole('option', { name: '/context' })).toBeInTheDocument()
expect(screen.queryByText('lark-mail')).not.toBeInTheDocument()
expect(await screen.findByText('lark-mail')).toBeInTheDocument()
expect(screen.queryByText('internal-only')).not.toBeInTheDocument()
// Dynamic skills remain searchable before a session has been created.
+16 -3
View File
@@ -1862,7 +1862,7 @@ describe('ChatInput file mentions', () => {
})
})
it.each(['@', '/', '+'] as const)('hides withdrawn bundled capabilities in %s while keeping personal skills and other plugins', async (entry) => {
it.each(['@', '/', '/empty', '+'] as const)('hides withdrawn bundled capabilities in %s while keeping personal skills and other plugins', async (entry) => {
const withdrawnPackage = 'office-frontend-design@haha-connectors'
mocks.listReferences.mockResolvedValue({
plugins: [
@@ -1883,12 +1883,25 @@ describe('ChatInput file mentions', () => {
if (entry === '+') {
fireEvent.click(screen.getByLabelText('Open composer tools'))
fireEvent.change(screen.getByRole('combobox', { name: 'Search skills, plugins, files…' }), { target: { value: 'design' } })
} else setComposerText(`${entry}design`, 7)
} else if (entry === '/empty') setComposerText('/', 1)
else setComposerText(`${entry}design`, 7)
expect(await screen.findByRole('option', { name: 'Personal frontend design' })).toBeInTheDocument()
expect(await screen.findByRole('option', { name: 'Design tools' })).toBeInTheDocument()
expect(screen.queryByRole('option', { name: /Removed frontend/ })).not.toBeInTheDocument()
expect(screen.queryByRole('option', { name: /office-frontend-design/ })).not.toBeInTheDocument()
expect(mocks.wsSend).not.toHaveBeenCalled()
if (entry === '/empty') {
const options = screen.getAllByRole('option')
const skillIndex = options.indexOf(screen.getByRole('option', { name: 'Personal frontend design' }))
const pluginIndex = options.indexOf(screen.getByRole('option', { name: 'Design tools' }))
expect(skillIndex).toBeGreaterThan(options.indexOf(screen.getByRole('option', { name: '/model' })))
expect(pluginIndex).toBeGreaterThan(skillIndex)
for (let index = 0; index < pluginIndex; index++) fireEvent.keyDown(getComposerElement(), { key: 'ArrowDown' })
expect(screen.getByRole('option', { name: 'Design tools' })).toHaveAttribute('aria-selected', 'true')
fireEvent.keyDown(getComposerElement(), { key: 'Enter' })
expect(document.querySelector('.composer-mention')).toHaveTextContent('Design tools')
expect(mocks.wsSend).not.toHaveBeenCalled()
}
})
it.each(['empty', 'replacement'] as const)('refreshes skills when opening plus and drops stale entries during a pending %s response', async (result) => {
@@ -1987,7 +2000,7 @@ describe('ChatInput file mentions', () => {
render(<ChatInput compact />)
fireEvent.click(screen.getByLabelText('Open composer tools'))
const row = await screen.findByRole('option', { name: /Computer Use/ })
const row = await screen.findByRole('option', { name: /Computer use/ })
await waitFor(() => expect(row.querySelector('input[type="checkbox"]')).not.toBeChecked())
fireEvent.click(row.querySelector('input[type="checkbox"]')!)
@@ -1,5 +1,5 @@
import { createRef } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { act, fireEvent, render, screen, within } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import '@testing-library/jest-dom'
import { translate } from '@/i18n'
@@ -74,15 +74,15 @@ it('falls back to the shared icon vocabulary so skills match the mention menu',
expect(screen.getByText('Personal')).toBeInTheDocument()
})
it('uses the same command-plugin-skill order for option ids and keyboard references', () => {
it('uses the same command-skill-plugin order for option ids and keyboard references', () => {
const onSelect = vi.fn()
const itemRefs = { current: [] as (HTMLElement | null)[] }
render(<SlashCommandMenu id="mixed-slash" groups={{ system: [{ name: 'help', description: 'Help', kind: 'command' }], plugins: [{ name: 'plugin:video', description: 'Video tools', kind: 'plugin' }], skills: [{ name: 'render', description: 'Render video', kind: 'skill' }], ordered: [] }} selectedIndex={1} itemRefs={itemRefs} onSelect={onSelect} onHighlight={vi.fn()} showKeyboardHints={false} references={[{ kind: 'plugin', id: 'plugin:video', name: 'video', displayName: 'Video plugin', description: 'Video tools', source: 'plugin', modelText: 'Use video' }]} />)
const options = screen.getAllByRole('option')
expect(options.map(option => option.id)).toEqual(['mixed-slash-option-0', 'mixed-slash-option-1', 'mixed-slash-option-2'])
expect(screen.getByRole('option', { name: 'Video plugin' })).toHaveAttribute('aria-selected', 'true')
expect(screen.getByRole('option', { name: 'render' })).toHaveAttribute('aria-selected', 'true')
fireEvent.click(options[1]!)
expect(onSelect).toHaveBeenCalledWith('plugin:video')
expect(onSelect).toHaveBeenCalledWith('render')
expect(itemRefs.current[2]).toBe(options[2])
})
@@ -125,3 +125,38 @@ it('keeps command rows on one line while preserving searchable argument hints an
rerender(<SlashCommandMenu {...props} isSearching />)
expect(screen.getByText('[<condition> | clear]')).toHaveAttribute('title', '[<condition> | clear]')
})
it.each([
['en', 'Frequent commands', 'Type to search commands, skills and plugins', 'navigate', 'select', 'dismiss', 'Search results'],
['zh', '常用命令', '输入名称搜索命令、技能和插件', '导航', '选择', '关闭', '搜索结果'],
['zh-TW', '常用指令', '輸入名稱搜尋指令、技能和外掛', '導航', '選擇', '關閉', '搜尋結果'],
['jp', 'よく使うコマンド', '名前を入力してコマンド、スキル、プラグインを検索', '移動', '選択', '閉じる', '検索結果'],
['kr', '자주 쓰는 명령', '이름을 입력하여 명령, 스킬 및 플러그인 검색', '이동', '선택', '닫기', '검색 결과'],
] as const)('localizes slash discovery and navigation in %s with the frequent heading inside the scrolling group', (locale, frequent, hint, navigate, select, dismiss, results) => {
const previousLocale = useSettingsStore.getState().locale
act(() => useSettingsStore.setState({ locale }))
const props = {
id: `localized-${locale}`,
groups: { system: [{ name: 'compact', description: 'Compact' }], skills: [{ name: 'audit', description: 'Audit', kind: 'skill' as const }], plugins: [{ name: 'tools', description: 'Tools', kind: 'plugin' as const }], ordered: [] },
selectedIndex: 1,
itemRefs: { current: [] },
onSelect: vi.fn(),
onHighlight: vi.fn(),
showKeyboardHints: true,
}
const { rerender, unmount } = render(<SlashCommandMenu {...props} />)
try {
const listbox = screen.getByRole('listbox')
const frequentGroup = within(listbox).getByRole('group', { name: frequent })
expect(within(frequentGroup).getByText(frequent)).toBeInTheDocument()
expect(screen.getAllByRole('option').map(option => option.textContent)).toEqual(['/compactCompact', 'auditAudit', 'toolsTools'])
for (const text of [hint, navigate, select, dismiss, '↑↓']) expect(screen.getByText(text)).toBeInTheDocument()
expect(screen.queryByText('Up/Down')).not.toBeInTheDocument()
rerender(<SlashCommandMenu {...props} isSearching />)
expect(screen.getByText(results)).toBeInTheDocument()
expect(screen.queryByText(frequent)).not.toBeInTheDocument()
} finally {
unmount()
act(() => useSettingsStore.setState({ locale: previousLocale }))
}
})
@@ -141,20 +141,23 @@ export const SlashCommandMenu = forwardRef<HTMLDivElement, SlashCommandMenuProps
onMouseDown={event => event.preventDefault()}
className="absolute bottom-full left-0 right-0 z-[var(--z-dropdown)] mb-2 overflow-hidden rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] shadow-[var(--shadow-overlay)]"
>
<div className="px-4 pb-1 pt-3 text-xs font-medium text-[var(--color-text-tertiary)]">
{t(isSearching ? 'chat.slashSearchResults' : 'chat.slashFrequent')}
</div>
{isSearching ? <div className="px-4 pb-1 pt-3 text-xs font-medium text-[var(--color-text-tertiary)]">
{t('chat.slashSearchResults')}
</div> : null}
<div
id={id}
role="listbox"
aria-label={t('chat.slashCommands')}
className="max-h-[min(360px,45vh)] overflow-y-auto p-1.5"
>
{groups.system.map(renderSystemCommand)}
{groups.system.length > 0 ? <div role="group" aria-label={t(isSearching ? 'chat.slashCommands' : 'chat.slashFrequent')}>
{!isSearching ? <div className="px-3 pb-1 pt-2 text-xs font-medium text-[var(--color-text-tertiary)]">{t('chat.slashFrequent')}</div> : null}
{groups.system.map(renderSystemCommand)}
</div> : null}
{([
{ kind: 'plugin' as const, items: groups.plugins ?? [], label: t('chat.referencePlugins'), offset: groups.system.length },
{ kind: 'skill' as const, items: groups.skills, label: t('sidebar.skills'), offset: groups.system.length + (groups.plugins?.length ?? 0) },
{ kind: 'skill' as const, items: groups.skills, label: t('sidebar.skills'), offset: groups.system.length },
{ kind: 'plugin' as const, items: groups.plugins ?? [], label: t('chat.referencePlugins'), offset: groups.system.length + groups.skills.length },
]).map(group => group.items.length > 0 ? (
<div key={group.kind} role="group" aria-label={group.label}>
<div className="px-3 pb-1 pt-2 text-xs font-medium text-[var(--color-text-tertiary)]">{group.label}</div>
@@ -183,7 +186,7 @@ export const SlashCommandMenu = forwardRef<HTMLDivElement, SlashCommandMenuProps
{!isSearching ? <div className="px-4 pb-2 text-xs text-[var(--color-text-tertiary)]">{t('chat.slashSearchHint')}</div> : null}
{showKeyboardHints ? (
<div className="flex items-center gap-1.5 border-t border-[var(--color-border)] px-4 py-2 text-xs text-[var(--color-text-tertiary)]">
<kbd className="rounded border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1.5 py-0.5 font-mono text-[10px]">Up/Down</kbd>
<kbd className="rounded border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1.5 py-0.5 font-mono text-[10px]">↑↓</kbd>
<span>{t('chat.navigate')}</span>
<kbd className="ml-2 rounded border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1.5 py-0.5 font-mono text-[10px]">Enter</kbd>
<span>{t('chat.select')}</span>
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { TranslationKey } from '@/i18n'
import { translate, type TranslationKey } from '@/i18n'
import type { AgentDefinition } from '@/api/agents'
import type { ConnectorDto } from '@/types/connector'
import type { ComposerReferenceCandidate } from '@/types/composerReference'
@@ -201,3 +201,16 @@ it('exposes every mentionable plugin even without a connected connector', () =>
expect(connectors.children!.some(item => item.key === 'connector:frontend-design')).toBe(false)
expect(connectors.children!.some(item => item.key === 'connector:feishu')).toBe(true)
})
it.each([
['zh', '操作电脑'],
['zh-TW', '操作電腦'],
['en', 'Computer use'],
['jp', 'コンピューター操作'],
['kr', '컴퓨터 조작'],
] as const)('localizes the computer-use menu entry in %s', (locale, label) => {
const sections = buildCapabilitySections(buildInput({ t: (key, params) => translate(locale, key, params) }))
const entry = sections.flatMap(section => section.items).find(item => item.key === 'computer-use')!
expect(entry.label).toBe(label)
expect(entry.switch).toEqual({ checked: false, disabled: false })
})
@@ -173,7 +173,7 @@ describe('composerUtils', () => {
])
})
it('opens on a bounded set of frequent commands regardless of CLI registration order', () => {
it('opens with frequent commands followed by skills and plugins regardless of CLI registration order', () => {
const commands = mergeSlashCommands([
{ name: 'update-config', description: 'Configure' },
{ name: 'debug', description: 'Debug' },
@@ -182,7 +182,7 @@ describe('composerUtils', () => {
{ name: 'draw', description: 'Draw diagrams', kind: 'plugin' },
])
expect(filterSlashCommands(commands, '').map(command => command.name)).toEqual([
'compact', 'context', 'status', 'init', 'review', 'model',
'compact', 'context', 'status', 'init', 'review', 'model', 'video', 'draw',
])
expect(filterSlashCommands(commands, ' ')).toEqual(filterSlashCommands(commands, ''))
for (const name of ['update-config', 'debug', 'heapdump', 'video', 'draw', 'config', 'help']) {
@@ -190,8 +190,12 @@ describe('composerUtils', () => {
}
})
it('does not surface a skill merely because it shares a frequent command name', () => {
expect(filterSlashCommands([{ name: 'review', description: 'Custom review', kind: 'skill' }], '')).toEqual([])
it('keeps a same-named skill in its skill group without duplicating it as a frequent command', () => {
const skill = { name: 'review', description: 'Custom review', kind: 'skill' as const }
const groups = groupSlashCommands(filterSlashCommands([skill], ''))
expect(groups.system).toEqual([])
expect(groups.skills).toEqual([skill])
expect(groups.ordered).toEqual([skill])
})
it('keeps CLI-reported frequent commands available in the default list', () => {
@@ -349,5 +353,5 @@ it('orders plugin mentions between commands and skills without changing their ca
{ name: 'help', description: 'Help', kind: 'command' },
])
expect(groups.plugins?.map(item => item.name)).toEqual(['plugin:hyperframes'])
expect(groups.ordered.map(item => item.name)).toEqual(['help', 'plugin:hyperframes', 'skill:video'])
expect(groups.ordered.map(item => item.name)).toEqual(['help', 'skill:video', 'plugin:hyperframes'])
})
+8 -4
View File
@@ -183,7 +183,7 @@ export function groupSlashCommands(
system,
skills,
plugins,
ordered: [...system, ...plugins, ...skills],
ordered: [...system, ...skills, ...plugins],
}
}
@@ -330,9 +330,13 @@ export function filterSlashCommands(
): SlashCommandOption[] {
const normalized = filter.trim().toLowerCase()
if (!normalized.trim()) {
return FREQUENT_SLASH_COMMAND_NAMES.flatMap(name => commands.filter(command =>
command.name.trim().toLowerCase() === name && (!command.kind || command.kind === 'command'),
))
return [
...FREQUENT_SLASH_COMMAND_NAMES.flatMap(name => commands.filter(command =>
command.name.trim().toLowerCase() === name && (!command.kind || command.kind === 'command'),
)),
...commands.filter(command => command.kind === 'skill'),
...commands.filter(command => command.kind === 'plugin'),
]
}
const matches = commands.map((command, index) => ({
+3 -3
View File
@@ -467,7 +467,7 @@ export const en = {
'chat.capabilities.moreTools': 'More tools',
'chat.referenceSearchHint': 'Type to find more skills, plugins and project files',
'chat.slashFrequent': 'Frequent commands',
'chat.slashSearchHint': 'Type a name to find other commands',
'chat.slashSearchHint': 'Type to search commands, skills and plugins',
'chat.slashSearchResults': 'Search results',
'chat.pendingMessageQueuedLabel': 'Queued',
'repoLaunch.worktreeIsolatedDesc': 'Create an isolated worktree for this session; experiment safely without touching {branch} until merge.',
@@ -2437,10 +2437,10 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le
'chat.capabilities.teamCreateDescription': 'Describe the goal, agents do the rest',
'chat.capabilities.teamCreatePrompt': 'Create an agent team to work on the following goal. Split the work across teammates and coordinate them: ',
'chat.capabilities.teamMembers': '{count} members',
'chat.capabilities.computerUse': 'Computer Use',
'chat.capabilities.computerUse': 'Computer use',
'chat.capabilities.computerUseDescription': 'Let Claude operate apps on this computer',
'chat.capabilities.computerUseUnsupported': 'Not available on this platform — open settings',
'chat.capabilities.computerUseToggle': 'Toggle Computer Use',
'chat.capabilities.computerUseToggle': 'Toggle computer use',
'chat.capabilities.workflows': 'Workflows',
'chat.capabilities.workflowsDescription': 'Run a saved workflow',
'chat.capabilities.saveWorkflow': 'Save this session as a workflow',
+3 -3
View File
@@ -469,7 +469,7 @@ export const jp: Record<TranslationKey, string> = {
'chat.capabilities.moreTools': 'その他のツール',
'chat.referenceSearchHint': '名前を入力してスキル・プラグイン・プロジェクトファイルを検索',
'chat.slashFrequent': 'よく使うコマンド',
'chat.slashSearchHint': '名前を入力して他のコマンドを検索',
'chat.slashSearchHint': '名前を入力してコマンド、スキル、プラグインを検索',
'chat.slashSearchResults': '検索結果',
'chat.pendingMessageQueuedLabel': 'キュー済み',
'repoLaunch.worktreeIsolatedDesc': 'このセッション用に分離された worktree を作成し、マージまで {branch} に影響しません。',
@@ -2439,10 +2439,10 @@ export const jp: Record<TranslationKey, string> = {
'chat.capabilities.teamCreateDescription': '目標を伝えれば Agent が分担',
'chat.capabilities.teamCreatePrompt': '次の目標に取り組む Agent チームを作成し、分担と連携をしてください:',
'chat.capabilities.teamMembers': 'メンバー {count} 人',
'chat.capabilities.computerUse': 'Computer Use',
'chat.capabilities.computerUse': 'コンピューター操作',
'chat.capabilities.computerUseDescription': 'このコンピュータ上のアプリを操作',
'chat.capabilities.computerUseUnsupported': 'このプラットフォームでは利用不可 — 設定を開く',
'chat.capabilities.computerUseToggle': 'Computer Use を切り替え',
'chat.capabilities.computerUseToggle': 'コンピューター操作を切り替え',
'chat.capabilities.workflows': 'ワークフロー',
'chat.capabilities.workflowsDescription': '保存済みのワークフローを実行',
'chat.capabilities.saveWorkflow': 'このセッションをワークフローとして保存',
+3 -3
View File
@@ -469,7 +469,7 @@ export const kr: Record<TranslationKey, string> = {
'chat.capabilities.moreTools': '더 많은 도구',
'chat.referenceSearchHint': '이름을 입력하여 스킬, 플러그인 및 프로젝트 파일 검색',
'chat.slashFrequent': '자주 쓰는 명령',
'chat.slashSearchHint': '이름을 입력하여 다른 명령 검색',
'chat.slashSearchHint': '이름을 입력하여 명령, 스킬 및 플러그인 검색',
'chat.slashSearchResults': '검색 결과',
'chat.pendingMessageQueuedLabel': '대기 중',
'repoLaunch.worktreeIsolatedDesc': '이 세션용 격리 worktree를 만들어 병합 전까지 {branch}에 영향을 주지 않습니다.',
@@ -2439,10 +2439,10 @@ export const kr: Record<TranslationKey, string> = {
'chat.capabilities.teamCreateDescription': '목표를 말하면 Agent가 분업',
'chat.capabilities.teamCreatePrompt': '다음 목표를 수행할 Agent 팀을 만들고, 역할을 나누어 협업해 주세요: ',
'chat.capabilities.teamMembers': '멤버 {count}명',
'chat.capabilities.computerUse': 'Computer Use',
'chat.capabilities.computerUse': '컴퓨터 조작',
'chat.capabilities.computerUseDescription': '이 컴퓨터의 앱을 Claude가 조작',
'chat.capabilities.computerUseUnsupported': '이 플랫폼에서는 사용 불가 — 설정 열기',
'chat.capabilities.computerUseToggle': 'Computer Use 전환',
'chat.capabilities.computerUseToggle': '컴퓨터 조작 전환',
'chat.capabilities.workflows': '워크플로',
'chat.capabilities.workflowsDescription': '저장된 워크플로 실행',
'chat.capabilities.saveWorkflow': '이 세션을 워크플로로 저장',
+3 -3
View File
@@ -469,7 +469,7 @@ export const zh: Record<TranslationKey, string> = {
'chat.capabilities.moreTools': '更多工具',
'chat.referenceSearchHint': '輸入名稱,尋找更多技能、外掛和專案檔案',
'chat.slashFrequent': '常用指令',
'chat.slashSearchHint': '輸入名稱尋找其他指令',
'chat.slashSearchHint': '輸入名稱搜尋指令、技能和外掛',
'chat.slashSearchResults': '搜尋結果',
'chat.pendingMessageQueuedLabel': '已排隊',
'repoLaunch.worktreeIsolatedDesc': '為本會話建立隔離 worktree,安全實驗,合併前不影響 {branch}。',
@@ -2438,10 +2438,10 @@ export const zh: Record<TranslationKey, string> = {
'chat.capabilities.teamCreateDescription': '描述目標,Agent 自行分工協作',
'chat.capabilities.teamCreatePrompt': '幫我建立一個 Agent 團隊來完成下面的目標,請合理分工並協調各個成員:',
'chat.capabilities.teamMembers': '{count} 位成員',
'chat.capabilities.computerUse': 'Computer Use',
'chat.capabilities.computerUse': '操作電腦',
'chat.capabilities.computerUseDescription': '讓 Claude 操作本機應用',
'chat.capabilities.computerUseUnsupported': '目前平台不可用 — 開啟設定',
'chat.capabilities.computerUseToggle': '切換 Computer Use',
'chat.capabilities.computerUseToggle': '切換操作電腦功能',
'chat.capabilities.workflows': '工作流程',
'chat.capabilities.workflowsDescription': '執行已儲存的工作流程',
'chat.capabilities.saveWorkflow': '將目前對話儲存為工作流程',
+3 -3
View File
@@ -468,7 +468,7 @@ export const zh: Record<TranslationKey, string> = {
'chat.capabilities.moreTools': '更多工具',
'chat.referenceSearchHint': '输入名称,查找更多技能、插件和项目文件',
'chat.slashFrequent': '常用命令',
'chat.slashSearchHint': '输入名称查找其他命令',
'chat.slashSearchHint': '输入名称搜索命令、技能和插件',
'chat.slashSearchResults': '搜索结果',
'chat.pendingMessageQueuedLabel': '已排队',
'repoLaunch.worktreeIsolatedDesc': '为本会话创建隔离 worktree,安全实验,合并前不影响 {branch}。',
@@ -2437,10 +2437,10 @@ export const zh: Record<TranslationKey, string> = {
'chat.capabilities.teamCreateDescription': '描述目标,Agent 自行分工协作',
'chat.capabilities.teamCreatePrompt': '帮我创建一个 Agent 团队来完成下面的目标,请合理分工并协调各个成员:',
'chat.capabilities.teamMembers': '{count} 个成员',
'chat.capabilities.computerUse': 'Computer Use',
'chat.capabilities.computerUse': '操作电脑',
'chat.capabilities.computerUseDescription': '让 Claude 操作本机应用',
'chat.capabilities.computerUseUnsupported': '当前平台不可用 — 打开设置',
'chat.capabilities.computerUseToggle': '开关 Computer Use',
'chat.capabilities.computerUseToggle': '开关操作电脑功能',
'chat.capabilities.workflows': '工作流',
'chat.capabilities.workflowsDescription': '运行已保存的工作流',
'chat.capabilities.saveWorkflow': '把当前会话保存为工作流',
+15 -2
View File
@@ -339,7 +339,7 @@ describe('EmptySession', () => {
expect(screen.getByTestId('empty-session-composer-panel')).toHaveClass('rounded-[var(--radius-2xl)]')
})
it.each(['@', '/', '+'] as const)('hides withdrawn bundled capabilities in %s while keeping personal skills and other plugins', async (entry) => {
it.each(['@', '/', '/empty', '+'] as const)('hides withdrawn bundled capabilities in %s while keeping personal skills and other plugins', async (entry) => {
const withdrawnPackage = 'office-frontend-design@haha-connectors'
mocks.listReferences.mockResolvedValue({
plugins: [
@@ -360,12 +360,25 @@ describe('EmptySession', () => {
if (entry === '+') {
fireEvent.click(screen.getByLabelText('Open composer tools'))
fireEvent.change(screen.getByRole('combobox', { name: 'Search skills, plugins, files…' }), { target: { value: 'design' } })
} else setComposerText(`${entry}design`, 7)
} else if (entry === '/empty') setComposerText('/', 1)
else setComposerText(`${entry}design`, 7)
expect(await screen.findByRole('option', { name: 'Personal frontend design' })).toBeInTheDocument()
expect(await screen.findByRole('option', { name: 'Design tools' })).toBeInTheDocument()
expect(screen.queryByRole('option', { name: /Removed frontend/ })).not.toBeInTheDocument()
expect(screen.queryByRole('option', { name: /office-frontend-design/ })).not.toBeInTheDocument()
expect(mocks.wsSend).not.toHaveBeenCalled()
if (entry === '/empty') {
const options = screen.getAllByRole('option')
const skillIndex = options.indexOf(screen.getByRole('option', { name: 'Personal frontend design' }))
const pluginIndex = options.indexOf(screen.getByRole('option', { name: 'Design tools' }))
expect(skillIndex).toBeGreaterThan(options.indexOf(screen.getByRole('option', { name: '/model' })))
expect(pluginIndex).toBeGreaterThan(skillIndex)
for (let index = 0; index < pluginIndex; index++) fireEvent.keyDown(getComposerElement(), { key: 'ArrowDown' })
expect(screen.getByRole('option', { name: 'Design tools' })).toHaveAttribute('aria-selected', 'true')
fireEvent.keyDown(getComposerElement(), { key: 'Enter' })
expect(document.querySelector('.composer-mention')).toHaveTextContent('Design tools')
expect(mocks.wsSend).not.toHaveBeenCalled()
}
})
it.each(['empty', 'replacement'] as const)('refreshes skills when opening plus and drops stale entries during a pending %s response', async (result) => {