mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'このセッションをワークフローとして保存',
|
||||
|
||||
@@ -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': '이 세션을 워크플로로 저장',
|
||||
|
||||
@@ -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': '將目前對話儲存為工作流程',
|
||||
|
||||
@@ -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': '把当前会话保存为工作流',
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user