revert: remove Collective Wisdom V1 (#94266)
Reverts the in-tree org skill-marketplace: hermes_wisdom package, three model tools, CLI/gateway/desktop/dashboard/Telegram/Slack surfaces. Later non-Wisdom work on shared files (guest onboarding i18n, dashboard startup schema, Slack adapter, tui_gateway) is kept; Wisdom-only call sites and config were stripped from those files.
This commit is contained in:
@@ -42,7 +42,6 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "8.0.1",
|
||||
"@rolldown/plugin-babel": "0.2.3",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/babel__core": "7.20.5",
|
||||
"@types/node": "22.20.1",
|
||||
"@types/qrcode": "1.5.6",
|
||||
|
||||
@@ -1,572 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
getWisdomStatus,
|
||||
getWisdomDiscovery,
|
||||
getWisdomCandidates,
|
||||
getWisdomDrafts,
|
||||
getWisdomSkill,
|
||||
getWisdomInstallations,
|
||||
getWisdomVersionContent,
|
||||
planWisdomInstall,
|
||||
applyWisdomInstall,
|
||||
checkWisdom,
|
||||
submitWisdomPublication,
|
||||
saveWisdomPreparedDraft,
|
||||
suggestWisdomSkill,
|
||||
reviewWisdomPublication,
|
||||
reviseWisdomDraft,
|
||||
decideWisdomDraft,
|
||||
setupWisdom,
|
||||
getActionStatus
|
||||
} = vi.hoisted(() => ({
|
||||
getWisdomStatus: vi.fn(),
|
||||
getWisdomDiscovery: vi.fn(),
|
||||
getWisdomCandidates: vi.fn(),
|
||||
getWisdomDrafts: vi.fn(),
|
||||
getWisdomSkill: vi.fn(),
|
||||
getWisdomInstallations: vi.fn(),
|
||||
getWisdomVersionContent: vi.fn(),
|
||||
planWisdomInstall: vi.fn(),
|
||||
applyWisdomInstall: vi.fn(),
|
||||
checkWisdom: vi.fn(),
|
||||
submitWisdomPublication: vi.fn(),
|
||||
saveWisdomPreparedDraft: vi.fn(),
|
||||
suggestWisdomSkill: vi.fn(),
|
||||
reviewWisdomPublication: vi.fn(),
|
||||
reviseWisdomDraft: vi.fn(),
|
||||
decideWisdomDraft: vi.fn(),
|
||||
setupWisdom: vi.fn(),
|
||||
getActionStatus: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: {
|
||||
decideWisdomDraft,
|
||||
setupWisdom,
|
||||
getActionStatus,
|
||||
getWisdomCandidates,
|
||||
getWisdomDiscovery,
|
||||
getWisdomDrafts,
|
||||
getWisdomSkill,
|
||||
getWisdomInstallations,
|
||||
getWisdomVersionContent,
|
||||
getWisdomStatus,
|
||||
reviewWisdomPublication,
|
||||
reviseWisdomDraft,
|
||||
submitWisdomPublication,
|
||||
saveWisdomPreparedDraft,
|
||||
suggestWisdomSkill,
|
||||
planWisdomInstall,
|
||||
applyWisdomInstall,
|
||||
checkWisdom
|
||||
}
|
||||
}))
|
||||
|
||||
import { CollectiveWisdomPanel } from './CollectiveWisdomPanel'
|
||||
|
||||
function systemSpecification() {
|
||||
return {
|
||||
hermes: { minimum_version: '0.17.0' },
|
||||
platforms: [],
|
||||
architectures: [],
|
||||
model: { capabilities: [], minimum_context_window: null },
|
||||
tools: [],
|
||||
plugins: [],
|
||||
credentials: [],
|
||||
connections: [],
|
||||
filesystem: { read: [], write: [] },
|
||||
network: { destinations: [] },
|
||||
runtime: { shell: false, browser: false, code: false, sandbox: true },
|
||||
hardware: [],
|
||||
known_limitations: []
|
||||
}
|
||||
}
|
||||
|
||||
function manifestJson() {
|
||||
return JSON.stringify({
|
||||
schema_version: 1,
|
||||
name: 'deployment-checklist',
|
||||
requirements: systemSpecification()
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' })
|
||||
getWisdomCandidates.mockResolvedValue({ candidates: [] })
|
||||
getWisdomDrafts.mockResolvedValue({ drafts: [] })
|
||||
getWisdomDiscovery.mockResolvedValue({ next_cursor: null, skills: [] })
|
||||
getWisdomInstallations.mockResolvedValue({ installations: [], notifications: [] })
|
||||
checkWisdom.mockResolvedValue({ installations: [] })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('CollectiveWisdomPanel', () => {
|
||||
it('requires explicit disclosure setup before loading collective data', async () => {
|
||||
getWisdomStatus
|
||||
.mockResolvedValueOnce({ configured: false, verified_org_id: null })
|
||||
.mockResolvedValueOnce({ configured: true, verified_org_id: 'org-1' })
|
||||
setupWisdom.mockResolvedValue({ ok: true, name: 'wisdom-setup', pid: 1 })
|
||||
getActionStatus.mockResolvedValue({
|
||||
name: 'wisdom-setup',
|
||||
running: false,
|
||||
exit_code: 0,
|
||||
pid: 1,
|
||||
lines: []
|
||||
})
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
expect(await screen.findByText(/Candidate qualification stays on this profile/)).toBeTruthy()
|
||||
expect(getWisdomDiscovery).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: /set up this profile/ }))
|
||||
|
||||
await waitFor(() => expect(setupWisdom).toHaveBeenCalledWith('research'))
|
||||
expect(getActionStatus).toHaveBeenCalledWith('wisdom-setup', 80)
|
||||
await waitFor(() => expect(getWisdomDiscovery).toHaveBeenCalledWith('research'))
|
||||
})
|
||||
|
||||
it('shows the structured setup failure instead of trailing log punctuation', async () => {
|
||||
getWisdomStatus.mockResolvedValue({ configured: false, verified_org_id: null })
|
||||
setupWisdom.mockResolvedValue({ ok: true, name: 'wisdom-setup', pid: 1 })
|
||||
getActionStatus.mockResolvedValue({
|
||||
name: 'wisdom-setup',
|
||||
running: false,
|
||||
exit_code: 3,
|
||||
pid: 1,
|
||||
lines: [
|
||||
'=== wisdom-setup started ===',
|
||||
'{',
|
||||
' "category": 3,',
|
||||
' "error": "Collective Wisdom is unavailable for this account",',
|
||||
' "ok": false',
|
||||
'}'
|
||||
]
|
||||
})
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: /set up this profile/ }))
|
||||
|
||||
expect(await screen.findByText('Collective Wisdom is unavailable for this account')).toBeTruthy()
|
||||
expect(screen.queryByText('}')).toBeNull()
|
||||
})
|
||||
|
||||
it('escapes server-controlled text and labels server scan state explicitly', async () => {
|
||||
getWisdomDiscovery.mockResolvedValue({
|
||||
next_cursor: null,
|
||||
skills: [
|
||||
{
|
||||
id: 'skill-1',
|
||||
slug: '<img src=x onerror=alert(1)>',
|
||||
author_description: '<script>window.pwned=true</script>',
|
||||
latest_version: 1,
|
||||
install_count: 0,
|
||||
state: 'active',
|
||||
security_check: { status: 'pass' }
|
||||
}
|
||||
]
|
||||
})
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
expect(await screen.findByText('<img src=x onerror=alert(1)>')).toBeTruthy()
|
||||
expect(screen.getByText('Security: Pass')).toBeTruthy()
|
||||
expect(document.querySelector('script')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the first and returning qualification notice attached to each candidate', async () => {
|
||||
getWisdomCandidates.mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
local_skill_id: 'local-first',
|
||||
name: 'first-skill',
|
||||
editorial_name: 'First Skill',
|
||||
editorial_description: 'A friendly description for people.',
|
||||
path: '/skills/first-skill',
|
||||
content_hash: 'sha256:first',
|
||||
eligibility: 'eligible',
|
||||
reason: null,
|
||||
qualification: 'meaningful_refinements',
|
||||
qualification_sequence: 1,
|
||||
notice_variant: 'first',
|
||||
organization_name: 'Nous Research',
|
||||
contribution_state: 'new'
|
||||
},
|
||||
{
|
||||
local_skill_id: 'local-returning',
|
||||
name: 'returning-skill',
|
||||
path: '/skills/returning-skill',
|
||||
content_hash: 'sha256:returning',
|
||||
eligibility: 'eligible',
|
||||
reason: null,
|
||||
qualification: 'consecutive_business_days',
|
||||
qualification_sequence: 2,
|
||||
notice_variant: 'returning',
|
||||
organization_name: 'Nous Research',
|
||||
contribution_state: 'new'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
|
||||
expect(await screen.findByText(/Your organization \(Nous Research\) has enabled Collective Wisdom/)).toBeTruthy()
|
||||
expect(screen.getByText('First Skill')).toBeTruthy()
|
||||
expect(screen.getByText('A friendly description for people.')).toBeTruthy()
|
||||
expect(screen.getByText(/Hermes detected another skill you created that could be useful to your team/)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps all files local until the final moderation confirmation', async () => {
|
||||
getWisdomCandidates.mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
local_skill_id: 'local-1',
|
||||
name: 'candidate-skill',
|
||||
eligibility: 'eligible',
|
||||
qualification: 'manual_selection',
|
||||
contribution_state: 'new'
|
||||
}
|
||||
]
|
||||
})
|
||||
suggestWisdomSkill.mockResolvedValue({ network_submission: false, local_draft_id: 'local:1' })
|
||||
const review = {
|
||||
publication_mode: 'moderated',
|
||||
draft: { id: 'local:1', slug: 'candidate-skill', state: 'prepared', authorDescription: 'Owner copy' },
|
||||
files: [
|
||||
{ path: 'SKILL.md', mode: 'file', hash: 'content', content_utf8: '# Local skill' },
|
||||
{ path: 'skill.manifest.json', mode: 'file', hash: 'manifest', content_utf8: manifestJson() }
|
||||
],
|
||||
hashes: { content: 'content', author_description: 'description', package_manifest: 'manifest' }
|
||||
}
|
||||
reviewWisdomPublication.mockResolvedValue(review)
|
||||
submitWisdomPublication.mockResolvedValue({
|
||||
draft_id: 'server:1',
|
||||
publication_state: 'pending_moderation',
|
||||
portal_url: 'https://portal.example/review/1'
|
||||
})
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
fireEvent.click(await screen.findByText('View all local skills (1)'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Start contribution' }))
|
||||
const submit = await screen.findByRole('button', { name: 'Submit for approval' })
|
||||
expect(screen.getByLabelText('Edit SKILL.md')).toHaveProperty('value', '# Local skill')
|
||||
expect(submitWisdomPublication).not.toHaveBeenCalled()
|
||||
expect(suggestWisdomSkill).toHaveBeenCalledTimes(1)
|
||||
await waitFor(() => expect(submit).toHaveProperty('disabled', false))
|
||||
fireEvent.click(submit)
|
||||
await screen.findByText('Waiting for collective administrator approval')
|
||||
expect(submitWisdomPublication).toHaveBeenCalledWith(review, 'research')
|
||||
expect(suggestWisdomSkill).toHaveBeenCalledTimes(1)
|
||||
expect(decideWisdomDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses user-facing activity copy and hides completed drafts from the action queue', async () => {
|
||||
getWisdomDrafts.mockResolvedValue({
|
||||
drafts: [
|
||||
{ id: 'draft-ready', slug: 'deployment-checklist', state: 'ready' },
|
||||
{ id: 'draft-pending', slug: 'policy-canary', state: 'pending_moderation' },
|
||||
{ id: 'draft-published', slug: 'incident-handoff', state: 'published' }
|
||||
]
|
||||
})
|
||||
getWisdomInstallations.mockResolvedValue({
|
||||
installations: [],
|
||||
notifications: [
|
||||
{
|
||||
event_id: 'decision-1',
|
||||
kind: 'owner_decision',
|
||||
skill_id: 'local-internal-id',
|
||||
payload: { slug: 'incident-handoff', state: 'published' }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
|
||||
expect(await screen.findByText('incident-handoff was approved and is now shared with your team.')).toBeTruthy()
|
||||
expect(screen.queryByText('owner_decision')).toBeNull()
|
||||
expect(screen.queryByText('local-internal-id')).toBeNull()
|
||||
expect(screen.getByText('deployment-checklist')).toBeTruthy()
|
||||
expect(screen.getByText('policy-canary')).toBeTruthy()
|
||||
expect(screen.getByText('Waiting for collective administrator approval')).toBeTruthy()
|
||||
expect(
|
||||
screen.getByText('Drafts awaiting your review and submissions waiting for collective approval.')
|
||||
).toBeTruthy()
|
||||
expect(screen.queryByText('incident-handoff', { selector: 'span.font-mono' })).toBeNull()
|
||||
})
|
||||
|
||||
it('edits owner copy and Markdown through a rescanned successor before approval', async () => {
|
||||
getWisdomDrafts.mockResolvedValue({
|
||||
drafts: [{ id: 'draft-1', slug: 'deployment-checklist', state: 'ready' }]
|
||||
})
|
||||
const initialReview = {
|
||||
publication_mode: 'open',
|
||||
draft: {
|
||||
id: 'draft-1',
|
||||
slug: 'deployment-checklist',
|
||||
state: 'ready',
|
||||
authorDescription: 'Original description',
|
||||
scanVerdict: 'pass',
|
||||
scan: { verdict: 'pass' },
|
||||
explanation: 'Two inert files.',
|
||||
systemSpec: { hermes: { minimum_version: '0.17.0' } }
|
||||
},
|
||||
files: [
|
||||
{ path: 'SKILL.md', mode: 'file', hash: 'sha256:old-skill', content_utf8: '# Original\n' },
|
||||
{
|
||||
path: 'skill.manifest.json',
|
||||
mode: 'file',
|
||||
hash: 'sha256:old-manifest',
|
||||
content_utf8: manifestJson()
|
||||
}
|
||||
],
|
||||
hashes: {
|
||||
content: 'sha256:old-content',
|
||||
author_description: 'sha256:old-description',
|
||||
package_manifest: 'sha256:old-manifest'
|
||||
},
|
||||
receipt: null
|
||||
}
|
||||
const revisedReview = {
|
||||
...initialReview,
|
||||
draft: {
|
||||
...initialReview.draft,
|
||||
id: 'draft-2',
|
||||
authorDescription: 'Updated description'
|
||||
},
|
||||
files: [
|
||||
{ path: 'SKILL.md', mode: 'file', hash: 'sha256:new-skill', content_utf8: '# Updated\n' },
|
||||
initialReview.files[1]
|
||||
],
|
||||
hashes: {
|
||||
content: 'sha256:new-content',
|
||||
author_description: 'sha256:new-description',
|
||||
package_manifest: 'sha256:old-manifest'
|
||||
}
|
||||
}
|
||||
reviewWisdomPublication.mockResolvedValueOnce(initialReview).mockResolvedValueOnce(revisedReview)
|
||||
reviseWisdomDraft.mockResolvedValue({
|
||||
draft: { id: 'draft-2' },
|
||||
local_scan: {},
|
||||
notice: 'rescanned'
|
||||
})
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: /View details/ }))
|
||||
fireEvent.change(await screen.findByLabelText('Edit owner-authored description'), {
|
||||
target: { value: 'Updated description' }
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('Edit SKILL.md'), {
|
||||
target: { value: '# Updated\n' }
|
||||
})
|
||||
expect(screen.queryByLabelText('Edit skill.manifest.json')).toBeNull()
|
||||
const minimumVersion = screen.getByLabelText('Minimum Hermes version')
|
||||
expect(minimumVersion).toHaveProperty('value', '0.17.0')
|
||||
expect(screen.getByText(/Hermes pre-fills new drafts from this authoring device/)).toBeTruthy()
|
||||
expect(screen.getByText(/Checked systems are the allowed install targets/)).toBeTruthy()
|
||||
fireEvent.change(minimumVersion, { target: { value: '' } })
|
||||
expect(screen.getByText('Minimum Hermes version is required.')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Save changes & rescan' })).toHaveProperty('disabled', true)
|
||||
fireEvent.change(minimumVersion, { target: { value: '0.17.0' } })
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'Shell commands' }))
|
||||
|
||||
expect(screen.getByText(/These changes have not been scanned/)).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Publish to team' })).toHaveProperty('disabled', true)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Preview' }))
|
||||
expect(screen.getByRole('heading', { name: 'Updated' })).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save changes & rescan' }))
|
||||
|
||||
await waitFor(() => expect(reviseWisdomDraft).toHaveBeenCalledTimes(1))
|
||||
expect(reviseWisdomDraft).toHaveBeenCalledWith(
|
||||
'draft-1',
|
||||
'Updated description',
|
||||
[
|
||||
{ path: 'SKILL.md', content_utf8: '# Updated\n' },
|
||||
{
|
||||
path: 'skill.manifest.json',
|
||||
content_utf8: JSON.stringify({
|
||||
schema_version: 1,
|
||||
name: 'deployment-checklist',
|
||||
requirements: {
|
||||
...systemSpecification(),
|
||||
runtime: { shell: true, browser: false, code: false, sandbox: true }
|
||||
}
|
||||
})
|
||||
}
|
||||
],
|
||||
initialReview.hashes,
|
||||
'research'
|
||||
)
|
||||
expect(await screen.findByText('content sha256:new-content')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Publish to team' })).toHaveProperty('disabled', false)
|
||||
})
|
||||
|
||||
it('shows exact version bytes and applies only a verified install receipt', async () => {
|
||||
getWisdomDiscovery.mockResolvedValue({
|
||||
next_cursor: null,
|
||||
skills: [
|
||||
{
|
||||
id: 'skill-1',
|
||||
slug: 'managed-skill',
|
||||
author_description: 'Does work',
|
||||
latest_version: 2,
|
||||
install_count: 0,
|
||||
state: 'active'
|
||||
}
|
||||
]
|
||||
})
|
||||
getWisdomSkill.mockResolvedValue({ skill: { id: 'skill-1', slug: 'managed-skill' }, versions: [{ version: 2 }] })
|
||||
getWisdomVersionContent.mockResolvedValue({
|
||||
commit: 'sha256:commit',
|
||||
content_hash: 'sha256:content',
|
||||
files: [{ path: 'SKILL.md', mode: 'file', hash: 'sha256:file', content_utf8: '# Exact bytes' }]
|
||||
})
|
||||
planWisdomInstall.mockResolvedValue({
|
||||
receipt: 'wip_1',
|
||||
skill_id: 'skill-1',
|
||||
version: 2,
|
||||
compatibility: { outcome: 'compatible' }
|
||||
})
|
||||
applyWisdomInstall.mockResolvedValue({ installed: true })
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: /managed-skill/ }))
|
||||
expect(await screen.findByText('# Exact bytes')).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Install…' }))
|
||||
expect(await screen.findByText(/wip_1/)).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Confirm install' }))
|
||||
await waitFor(() => expect(applyWisdomInstall).toHaveBeenCalledWith('wip_1', false, 'research'))
|
||||
})
|
||||
|
||||
it('refreshes shared discovery explicitly and after checking for updates', async () => {
|
||||
const initialSkill = {
|
||||
id: 'skill-1',
|
||||
slug: 'initial-skill',
|
||||
author_description: 'Already visible',
|
||||
latest_version: 1,
|
||||
install_count: 0,
|
||||
state: 'active'
|
||||
}
|
||||
const refreshedSkill = {
|
||||
id: 'skill-2',
|
||||
slug: 'refreshed-skill',
|
||||
author_description: 'Found by refresh',
|
||||
latest_version: 1,
|
||||
install_count: 0,
|
||||
state: 'active'
|
||||
}
|
||||
const checkedSkill = {
|
||||
id: 'skill-3',
|
||||
slug: 'found-during-update-check',
|
||||
author_description: 'Found while checking updates',
|
||||
latest_version: 1,
|
||||
install_count: 0,
|
||||
state: 'active'
|
||||
}
|
||||
getWisdomDiscovery
|
||||
.mockResolvedValueOnce({ next_cursor: null, skills: [initialSkill] })
|
||||
.mockResolvedValueOnce({ next_cursor: null, skills: [initialSkill, refreshedSkill] })
|
||||
.mockResolvedValueOnce({ next_cursor: null, skills: [initialSkill, refreshedSkill, checkedSkill] })
|
||||
checkWisdom.mockResolvedValue({ installations: [] })
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
expect(await screen.findByText('initial-skill')).toBeTruthy()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Refresh shared skills' }))
|
||||
expect(await screen.findByText('refreshed-skill')).toBeTruthy()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Check updates' }))
|
||||
await waitFor(() => expect(checkWisdom).toHaveBeenCalledWith('research'))
|
||||
expect(await screen.findByText('found-during-update-check')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('checks on load and shows the target version on installed skills with pending manual updates', async () => {
|
||||
getWisdomDiscovery.mockResolvedValue({
|
||||
next_cursor: null,
|
||||
skills: [
|
||||
{
|
||||
id: 'skill-1',
|
||||
slug: 'gateway-pull-canary',
|
||||
author_description: 'Managed canary',
|
||||
latest_version: 2,
|
||||
install_count: 1,
|
||||
state: 'active'
|
||||
}
|
||||
]
|
||||
})
|
||||
getWisdomInstallations.mockResolvedValue({
|
||||
installations: [
|
||||
{
|
||||
skill_id: 'skill-1',
|
||||
slug: 'gateway-pull-canary',
|
||||
version: 1,
|
||||
update_mode: 'MANUAL',
|
||||
state: 'active',
|
||||
target_path: '/managed/gateway-pull-canary'
|
||||
}
|
||||
],
|
||||
notifications: []
|
||||
})
|
||||
checkWisdom.mockResolvedValue({
|
||||
installations: [
|
||||
{
|
||||
skill_id: 'skill-1',
|
||||
state: 'update_available',
|
||||
plan: { skill_id: 'skill-1', version: 2, receipt: 'wup_1' }
|
||||
}
|
||||
]
|
||||
})
|
||||
getWisdomSkill.mockResolvedValue({
|
||||
skill: { id: 'skill-1', slug: 'gateway-pull-canary' },
|
||||
versions: [{ version: 2 }]
|
||||
})
|
||||
getWisdomVersionContent.mockResolvedValue({ commit: 'sha256:commit', content_hash: 'sha256:content', files: [] })
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
|
||||
await waitFor(() => expect(checkWisdom).toHaveBeenCalledWith('research'))
|
||||
expect(await screen.findByText('v2 update available')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Check updates (1)' })).toBeTruthy()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /gateway-pull-canary/ }))
|
||||
expect(await screen.findByRole('button', { name: 'Review update' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('plans a pasted Portal link and requires explicit confirmation before install', async () => {
|
||||
const portalLink =
|
||||
'http://127.0.0.1:3111/orgs/wisdom-local/wisdom/skills/0a192cc7-486e-426d-a6b4-493119c1c011?version=1'
|
||||
planWisdomInstall
|
||||
.mockResolvedValueOnce({
|
||||
receipt: 'wip_link_default',
|
||||
skill_id: '0a192cc7-486e-426d-a6b4-493119c1c011',
|
||||
version: 1,
|
||||
compatibility: { outcome: 'compatible' }
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
receipt: 'wip_link_auto',
|
||||
skill_id: '0a192cc7-486e-426d-a6b4-493119c1c011',
|
||||
version: 1,
|
||||
update_mode: 'AUTO_WITH_NOTICE',
|
||||
compatibility: { outcome: 'compatible' }
|
||||
})
|
||||
applyWisdomInstall.mockResolvedValue({ installed: true })
|
||||
|
||||
render(<CollectiveWisdomPanel profile="research" />)
|
||||
const input = await screen.findByLabelText('Install from link or skill ID')
|
||||
fireEvent.change(input, { target: { value: portalLink } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Review install' }))
|
||||
|
||||
await waitFor(() => expect(planWisdomInstall).toHaveBeenCalledWith(portalLink, 'research', undefined))
|
||||
expect(applyWisdomInstall).not.toHaveBeenCalled()
|
||||
const dialog = await screen.findByRole('dialog', { name: 'Verified managed action plan' })
|
||||
expect(within(dialog).getByText(/wip_link_default/)).toBeTruthy()
|
||||
fireEvent.change(within(dialog).getByLabelText('Future updates'), { target: { value: 'AUTO_WITH_NOTICE' } })
|
||||
await waitFor(() => expect(planWisdomInstall).toHaveBeenLastCalledWith(portalLink, 'research', 'AUTO_WITH_NOTICE'))
|
||||
expect(within(dialog).getByText(/wip_link_auto/)).toBeTruthy()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Confirm install' }))
|
||||
await waitFor(() => expect(applyWisdomInstall).toHaveBeenCalledWith('wip_link_auto', false, 'research'))
|
||||
await waitFor(() => expect(input).toHaveProperty('value', ''))
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,10 +27,6 @@ import {
|
||||
const CREATE_TEMPLATE = `---
|
||||
name: my-skill
|
||||
description: One-line description of when to use this skill.
|
||||
metadata:
|
||||
hermes:
|
||||
editorial_name: My Skill
|
||||
editorial_description: A human-readable summary of what this skill helps with.
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { WisdomAgentActivity } from './WisdomAgentActivity'
|
||||
|
||||
const { read } = vi.hoisted(() => ({ read: vi.fn() }))
|
||||
vi.mock('@/lib/api', () => ({ api: { getWisdomMediation: read } }))
|
||||
afterEach(cleanup)
|
||||
|
||||
it.each(['agent', 'fixed'])('shows requested advice and canonical warnings without an apply control in %s mode', async mode => {
|
||||
read.mockResolvedValue({ mode, assessments: [{ id: 'one', state: 'delivered', advice: {
|
||||
title: 'A useful skill', explanation: 'May overlap with your existing runbook.'
|
||||
} }], interactions: [{ id: 'control', assessment_id: 'one', state: 'pending', operation: 'update', facts: {
|
||||
slug: 'Runbook', version: 2, sensitive_expansion: ['Additional network access'],
|
||||
security_check: { status: 'pass', checks: [] }, professionalism_check: { status: 'advisory', checks: [] }
|
||||
} }] })
|
||||
render(<WisdomAgentActivity profile="research" />)
|
||||
await screen.findByText('A useful skill')
|
||||
expect(screen.getByText('Additional network access')).toBeTruthy()
|
||||
expect(screen.queryByRole('button')).toBeNull()
|
||||
expect(read).toHaveBeenCalledWith('research')
|
||||
})
|
||||
|
||||
it('leaves the fixed notification surface unchanged', async () => {
|
||||
read.mockResolvedValue({ mode: 'fixed', assessments: [], interactions: [] })
|
||||
const { container } = render(<WisdomAgentActivity />)
|
||||
await vi.waitFor(() => expect(read).toHaveBeenCalled())
|
||||
expect(container.textContent).toBe('')
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '@/lib/api'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { WisdomReviewTables } from './WisdomChecks'
|
||||
|
||||
export function WisdomAgentActivity({ profile }: { profile?: string }) {
|
||||
const { t } = useI18n()
|
||||
const [activity, setActivity] = useState<Awaited<ReturnType<typeof api.getWisdomMediation>> | null>(null)
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
let revision = 0
|
||||
setActivity(null)
|
||||
const refresh = async () => {
|
||||
const request = ++revision
|
||||
try {
|
||||
const result = await api.getWisdomMediation(profile)
|
||||
if (active && request === revision) setActivity(result)
|
||||
} catch {
|
||||
// Passive reads never clear previously delivered advice on an outage.
|
||||
}
|
||||
}
|
||||
void refresh()
|
||||
const timer = window.setInterval(() => void refresh(), 30_000)
|
||||
return () => { active = false; window.clearInterval(timer) }
|
||||
}, [profile])
|
||||
if (!activity) return null
|
||||
const entries = activity.assessments.filter(entry => entry.advice)
|
||||
if (!entries.length) return null
|
||||
return <section aria-label={t.skills.wisdom.title} className="border-y border-border py-3">
|
||||
<h3 className="text-sm font-semibold">{t.skills.wisdom.title}</h3>
|
||||
{entries.map(entry => <article key={entry.id} className="min-w-0 border-t border-border py-3">
|
||||
<h4 className="break-words text-sm font-medium">{entry.advice?.title}</h4>
|
||||
<p className="mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary">{entry.advice?.explanation}</p>
|
||||
{activity.interactions.filter(item => item.assessment_id === entry.id).map(item =>
|
||||
<div key={item.id} className="mt-2 min-w-0">
|
||||
<p className="text-xs font-medium">{item.facts.editorial_name || item.facts.slug}{item.facts.version ? ` · v${item.facts.version}` : ''}</p>
|
||||
<p className="mt-1 text-xs text-text-secondary">{item.operation}: {item.state.replaceAll('_', ' ')}</p>
|
||||
{item.facts.compatibility && <p className="text-xs text-text-secondary">{item.facts.compatibility.outcome.replaceAll('_', ' ')}</p>}
|
||||
{item.facts.modified && <p className="text-xs text-red-400">{t.skills.wisdom.unsavedChanges}</p>}
|
||||
{item.facts.sensitive_expansion?.map((warning, index) => <p key={index} className="break-words text-xs text-red-400">{warning}</p>)}
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-xs">{t.skills.wisdom.reviewExact}</summary>
|
||||
<WisdomReviewTables security={item.facts.security_check} professionalism={item.facts.professionalism_check} />
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
</article>)}
|
||||
</section>
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { AlertTriangle, CheckCircle2, CircleHelp, Loader2, XCircle } from 'lucide-react'
|
||||
|
||||
import type { WisdomReviewCheck, WisdomReviewCheckRow, WisdomReviewStatus } from '@/lib/api'
|
||||
|
||||
const PROFESSIONALISM_LABELS: Record<string, string> = {
|
||||
profanity_or_abuse: 'Profanity or abusive language',
|
||||
hate_or_harassment: 'Hate or harassment',
|
||||
sexual_or_graphic_language: 'Sexual or graphic language',
|
||||
manipulative_or_spam: 'Manipulative, deceptive, or spam-like wording'
|
||||
}
|
||||
|
||||
function statusLabel(status: WisdomReviewStatus): string {
|
||||
return status.replaceAll('_', ' ').replace(/^./, value => value.toUpperCase())
|
||||
}
|
||||
|
||||
function StatusIcon({ status }: { status: WisdomReviewStatus }) {
|
||||
if (status === 'pass') return <CheckCircle2 aria-hidden className="h-3.5 w-3.5" />
|
||||
if (status === 'blocked') return <XCircle aria-hidden className="h-3.5 w-3.5" />
|
||||
if (status === 'pending' || status === 'retry' || status === 'running') {
|
||||
return <Loader2 aria-hidden className="h-3.5 w-3.5 animate-spin" />
|
||||
}
|
||||
if (status === 'advisory') return <AlertTriangle aria-hidden className="h-3.5 w-3.5" />
|
||||
return <CircleHelp aria-hidden className="h-3.5 w-3.5" />
|
||||
}
|
||||
|
||||
function tone(status: WisdomReviewStatus): string {
|
||||
if (status === 'pass') return 'border-emerald-500/50 text-emerald-300'
|
||||
if (status === 'blocked') return 'border-red-500/60 text-red-300'
|
||||
if (status === 'advisory') return 'border-amber-500/60 text-amber-300'
|
||||
return 'border-border text-text-tertiary'
|
||||
}
|
||||
|
||||
export function WisdomCheckBadge({ label, value }: { label: string; value?: WisdomReviewCheck | null }) {
|
||||
const status = value?.status ?? 'unavailable'
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 border px-2 py-1 text-[11px] ${tone(status)}`}>
|
||||
<StatusIcon status={status} />
|
||||
{label}: {statusLabel(status)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function rowLabel(row: WisdomReviewCheckRow): string {
|
||||
return row.label || PROFESSIONALISM_LABELS[row.key] || statusLabel(row.key as WisdomReviewStatus)
|
||||
}
|
||||
|
||||
function CheckTable({ label, note, value }: { label: string; note: string; value?: WisdomReviewCheck | null }) {
|
||||
const status = value?.status ?? 'unavailable'
|
||||
const rows = value?.checks ?? []
|
||||
return (
|
||||
<section aria-label={label} className="border-t border-border py-3 first:border-0">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h4 className="text-sm font-medium">{label}</h4>
|
||||
<WisdomCheckBadge label="Result" value={{ status }} />
|
||||
</div>
|
||||
{value?.summary && <p className="mt-1 text-xs text-text-secondary">{value.summary}</p>}
|
||||
<p className="mt-1 text-[11px] text-text-tertiary">{note}</p>
|
||||
{rows.length > 0 && (
|
||||
<div className="mt-3 overflow-x-auto">
|
||||
<table className="w-full border-collapse text-left text-xs">
|
||||
<thead className="text-text-tertiary">
|
||||
<tr>
|
||||
<th className="border-b border-border py-2 pr-3 font-medium">Check</th>
|
||||
<th className="border-b border-border py-2 pr-3 font-medium">Status</th>
|
||||
<th className="border-b border-border py-2 font-medium">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={row.key}>
|
||||
<th className="border-b border-border/60 py-2 pr-3 font-normal">{rowLabel(row)}</th>
|
||||
<td className="border-b border-border/60 py-2 pr-3">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<StatusIcon status={row.status} /> {statusLabel(row.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="border-b border-border/60 py-2 text-text-secondary">
|
||||
{row.details.length > 0
|
||||
? row.details.join(' ')
|
||||
: row.finding_count > 0
|
||||
? `${row.finding_count} finding${row.finding_count === 1 ? '' : 's'}`
|
||||
: 'No known matches detected'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function WisdomReviewTables({
|
||||
professionalism,
|
||||
security
|
||||
}: {
|
||||
professionalism?: WisdomReviewCheck | null
|
||||
security?: WisdomReviewCheck | null
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-3 border-y border-border">
|
||||
<CheckTable
|
||||
label={security?.source === 'local_preflight' ? 'Security check (local preflight)' : 'Security check'}
|
||||
note={
|
||||
security?.source === 'local_preflight'
|
||||
? 'Local checks complete. Required Gateway checks run after you confirm upload and before publication.'
|
||||
: 'Deterministic Gateway scan. A pass means no known matches were detected, not that the package is certified secure.'
|
||||
}
|
||||
value={security}
|
||||
/>
|
||||
<CheckTable
|
||||
label="Professionalism check"
|
||||
note="Agent-assessed and advisory. It does not block publication or installation."
|
||||
value={professionalism}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Markdown } from '@/components/Markdown'
|
||||
import type { WisdomDraftReview } from '@/lib/api'
|
||||
import { Button } from '@nous-research/ui/ui/components/button'
|
||||
import { WisdomManifestEditor } from './WisdomManifestEditor'
|
||||
|
||||
interface Props {
|
||||
file: WisdomDraftReview['files'][number]
|
||||
value: string
|
||||
disabled?: boolean
|
||||
reviewSource?: 'local' | 'server'
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function WisdomFileEditor({ file, value, disabled = false, reviewSource = 'server', onChange }: Props) {
|
||||
const [mode, setMode] = useState<'source' | 'preview'>('source')
|
||||
|
||||
if (file.path === 'skill.manifest.json') {
|
||||
return (
|
||||
<details className="border-t border-border py-3" open>
|
||||
<summary className="cursor-pointer font-mono text-xs">
|
||||
{file.path} · {reviewSource === 'local' ? 'local draft' : 'server-reviewed'} {file.hash}
|
||||
</summary>
|
||||
<WisdomManifestEditor value={value} disabled={disabled} onChange={onChange} />
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
const supportsPreview = file.path.toLowerCase().endsWith('.md')
|
||||
|
||||
return (
|
||||
<details className="border-t border-border py-3" open>
|
||||
<summary className="cursor-pointer font-mono text-xs">
|
||||
{file.path} · {reviewSource === 'local' ? 'local draft' : 'server-reviewed'} {file.hash}
|
||||
</summary>
|
||||
{supportsPreview && (
|
||||
<div className="mt-3 flex gap-2" aria-label={`${file.path} editor mode`}>
|
||||
<Button size="xs" outlined={mode !== 'source'} onClick={() => setMode('source')}>
|
||||
Source
|
||||
</Button>
|
||||
<Button size="xs" outlined={mode !== 'preview'} onClick={() => setMode('preview')}>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{supportsPreview && mode === 'preview' ? (
|
||||
<div className="mt-3 min-h-48 max-h-[32rem] overflow-auto border border-border bg-muted/10 p-4">
|
||||
<Markdown content={value} />
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
aria-label={`Edit ${file.path}`}
|
||||
className="mt-3 min-h-64 max-h-[32rem] w-full resize-y border border-border bg-background/40 px-3 py-2 font-mono text-xs leading-relaxed shadow-sm placeholder:text-muted-foreground focus-visible:border-foreground/25 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={disabled}
|
||||
spellCheck={false}
|
||||
value={value}
|
||||
onChange={event => onChange(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
</details>
|
||||
)
|
||||
}
|
||||
@@ -1,628 +0,0 @@
|
||||
import { useId } from 'react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
|
||||
import { Button } from '@nous-research/ui/ui/components/button'
|
||||
import { Checkbox } from '@nous-research/ui/ui/components/checkbox'
|
||||
import { Input } from '@nous-research/ui/ui/components/input'
|
||||
import { Label } from '@nous-research/ui/ui/components/label'
|
||||
import { parseWisdomManifest, wisdomManifestValidationError } from '@/lib/wisdom-manifest'
|
||||
import type {
|
||||
WisdomManifestV1,
|
||||
WisdomPluginRequirement,
|
||||
WisdomSystemSpecification,
|
||||
WisdomToolRequirement
|
||||
} from '@/lib/wisdom-manifest'
|
||||
|
||||
interface SpecificationEditorProps {
|
||||
value: WisdomSystemSpecification
|
||||
disabled?: boolean
|
||||
onChange: (value: WisdomSystemSpecification) => void
|
||||
}
|
||||
|
||||
interface ManifestEditorProps {
|
||||
value: string
|
||||
disabled?: boolean
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
interface StringListFieldProps {
|
||||
label: string
|
||||
description: string
|
||||
itemLabel: string
|
||||
value: string[]
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
onChange: (value: string[]) => void
|
||||
}
|
||||
|
||||
const PLATFORM_OPTIONS = [
|
||||
{ value: 'macOS', label: 'macOS' },
|
||||
{ value: 'Linux', label: 'Linux' },
|
||||
{ value: 'Windows', label: 'Windows' }
|
||||
] as const
|
||||
|
||||
const ARCHITECTURE_OPTIONS = [
|
||||
{ value: 'arm64', label: 'ARM64 / Apple silicon' },
|
||||
{ value: 'x86_64', label: 'x86-64 / AMD64' }
|
||||
] as const
|
||||
|
||||
function StringListField({
|
||||
label,
|
||||
description,
|
||||
itemLabel,
|
||||
value,
|
||||
disabled = false,
|
||||
placeholder,
|
||||
onChange
|
||||
}: StringListFieldProps) {
|
||||
const id = useId()
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-xs font-medium">{label}</Label>
|
||||
<p className="mt-0.5 text-[11px] leading-4 text-text-tertiary">{description}</p>
|
||||
</div>
|
||||
{value.map((item, index) => (
|
||||
<div key={`${id}-${index}`} className="flex items-center gap-2">
|
||||
<Input
|
||||
aria-label={`${itemLabel} ${index + 1}`}
|
||||
disabled={disabled}
|
||||
maxLength={512}
|
||||
placeholder={placeholder}
|
||||
value={item}
|
||||
onChange={event =>
|
||||
onChange(value.map((existing, itemIndex) => (itemIndex === index ? event.target.value : existing)))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
aria-label={`Remove ${itemLabel} ${index + 1}`}
|
||||
size="icon"
|
||||
outlined
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(value.filter((_, itemIndex) => itemIndex !== index))}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
size="sm"
|
||||
outlined
|
||||
disabled={disabled || value.length >= 64}
|
||||
onClick={() => onChange([...value, ''])}
|
||||
prefix={<Plus className="h-3.5 w-3.5" />}
|
||||
>
|
||||
Add {itemLabel}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PresetMultiSelect({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
options,
|
||||
disabled,
|
||||
onChange
|
||||
}: {
|
||||
label: string
|
||||
description: string
|
||||
value: string[]
|
||||
options: ReadonlyArray<{ value: string; label: string }>
|
||||
disabled?: boolean
|
||||
onChange: (value: string[]) => void
|
||||
}) {
|
||||
const id = useId()
|
||||
const presets = new Set(options.map(option => option.value))
|
||||
const custom = value.filter(item => !presets.has(item))
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-xs font-medium">{label}</Label>
|
||||
<p className="mt-0.5 text-[11px] leading-4 text-text-tertiary">{description}</p>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{options.map(option => {
|
||||
const checkboxId = `${id}-${option.value}`
|
||||
const checked = value.includes(option.value)
|
||||
return (
|
||||
<div key={option.value} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onCheckedChange={next =>
|
||||
onChange(next === true ? [...value, option.value] : value.filter(item => item !== option.value))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor={checkboxId} className="cursor-pointer text-xs font-normal">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<StringListField
|
||||
label={`Other ${label.toLowerCase()}`}
|
||||
description="Keep this empty unless the skill targets a platform not listed above."
|
||||
itemLabel={`other ${label.toLowerCase().replace(/s$/, '')}`}
|
||||
value={custom}
|
||||
disabled={disabled}
|
||||
onChange={next => onChange([...value.filter(item => presets.has(item)), ...next])}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BooleanField({
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
disabled,
|
||||
onChange
|
||||
}: {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
checked: boolean
|
||||
disabled?: boolean
|
||||
onChange: (checked: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start gap-2">
|
||||
<Checkbox id={id} checked={checked} disabled={disabled} onCheckedChange={next => onChange(next === true)} />
|
||||
<div>
|
||||
<Label htmlFor={id} className="cursor-pointer text-xs font-medium">
|
||||
{label}
|
||||
</Label>
|
||||
<p className="mt-0.5 text-[11px] leading-4 text-text-tertiary">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolRequirements({
|
||||
value,
|
||||
disabled,
|
||||
onChange
|
||||
}: {
|
||||
value: WisdomToolRequirement[]
|
||||
disabled?: boolean
|
||||
onChange: (value: WisdomToolRequirement[]) => void
|
||||
}) {
|
||||
const id = useId()
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{value.map((tool, index) => (
|
||||
<div key={index} className="grid gap-3 border border-border/80 p-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label htmlFor={`${id}-tool-${index}`} className="text-xs">
|
||||
Tool name
|
||||
</Label>
|
||||
<Input
|
||||
id={`${id}-tool-${index}`}
|
||||
className="mt-1"
|
||||
disabled={disabled}
|
||||
maxLength={512}
|
||||
value={tool.name}
|
||||
onChange={event =>
|
||||
onChange(
|
||||
value.map((item, itemIndex) => (itemIndex === index ? { ...item, name: event.target.value } : item))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={`${id}-tool-version-${index}`} className="text-xs">
|
||||
Minimum version (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id={`${id}-tool-version-${index}`}
|
||||
className="mt-1"
|
||||
disabled={disabled}
|
||||
maxLength={512}
|
||||
value={tool.minimum_version ?? ''}
|
||||
onChange={event =>
|
||||
onChange(
|
||||
value.map((item, itemIndex) =>
|
||||
itemIndex === index ? { ...item, minimum_version: event.target.value || null } : item
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<BooleanField
|
||||
id={`${id}-tool-admin-${index}`}
|
||||
label="Administrator permission required"
|
||||
description="Installing or enabling this tool needs an administrator's action."
|
||||
checked={tool.requires_admin}
|
||||
disabled={disabled}
|
||||
onChange={requires_admin =>
|
||||
onChange(value.map((item, itemIndex) => (itemIndex === index ? { ...item, requires_admin } : item)))
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3 text-[11px] text-text-tertiary">
|
||||
<span>Automatic installation is always off.</span>
|
||||
<Button
|
||||
size="sm"
|
||||
outlined
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(value.filter((_, itemIndex) => itemIndex !== index))}
|
||||
prefix={<Trash2 className="h-3.5 w-3.5" />}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
size="sm"
|
||||
outlined
|
||||
disabled={disabled || value.length >= 64}
|
||||
onClick={() =>
|
||||
onChange([...value, { name: '', minimum_version: null, auto_install: false, requires_admin: false }])
|
||||
}
|
||||
prefix={<Plus className="h-3.5 w-3.5" />}
|
||||
>
|
||||
Add tool
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PluginRequirements({
|
||||
value,
|
||||
disabled,
|
||||
onChange
|
||||
}: {
|
||||
value: WisdomPluginRequirement[]
|
||||
disabled?: boolean
|
||||
onChange: (value: WisdomPluginRequirement[]) => void
|
||||
}) {
|
||||
const id = useId()
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{value.map((plugin, index) => (
|
||||
<div key={index} className="grid gap-3 border border-border/80 p-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label htmlFor={`${id}-plugin-${index}`} className="text-xs">
|
||||
Plugin ID
|
||||
</Label>
|
||||
<Input
|
||||
id={`${id}-plugin-${index}`}
|
||||
className="mt-1"
|
||||
disabled={disabled}
|
||||
maxLength={512}
|
||||
value={plugin.id}
|
||||
onChange={event =>
|
||||
onChange(
|
||||
value.map((item, itemIndex) => (itemIndex === index ? { ...item, id: event.target.value } : item))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={`${id}-plugin-version-${index}`} className="text-xs">
|
||||
Minimum version (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id={`${id}-plugin-version-${index}`}
|
||||
className="mt-1"
|
||||
disabled={disabled}
|
||||
maxLength={512}
|
||||
value={plugin.minimum_version ?? ''}
|
||||
onChange={event =>
|
||||
onChange(
|
||||
value.map((item, itemIndex) =>
|
||||
itemIndex === index ? { ...item, minimum_version: event.target.value || null } : item
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<BooleanField
|
||||
id={`${id}-plugin-required-${index}`}
|
||||
label="Required"
|
||||
description="Without this plugin, the skill cannot provide its complete behavior."
|
||||
checked={plugin.required}
|
||||
disabled={disabled}
|
||||
onChange={required =>
|
||||
onChange(value.map((item, itemIndex) => (itemIndex === index ? { ...item, required } : item)))
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
outlined
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(value.filter((_, itemIndex) => itemIndex !== index))}
|
||||
prefix={<Trash2 className="h-3.5 w-3.5" />}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
size="sm"
|
||||
outlined
|
||||
disabled={disabled || value.length >= 64}
|
||||
onClick={() => onChange([...value, { id: '', minimum_version: null, required: true }])}
|
||||
prefix={<Plus className="h-3.5 w-3.5" />}
|
||||
>
|
||||
Add plugin
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WisdomSystemSpecificationEditor({ value, disabled = false, onChange }: SpecificationEditorProps) {
|
||||
const id = useId()
|
||||
const update = <K extends keyof WisdomSystemSpecification>(key: K, next: WisdomSystemSpecification[K]) => {
|
||||
onChange({ ...value, [key]: next })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="border border-border bg-muted/10 p-3 text-[11px] leading-4 text-text-secondary">
|
||||
Hermes pre-fills new drafts from this authoring device and requirements explicitly recorded on the skill.
|
||||
Review these values before sharing: selected platforms and architectures restrict where teammates can install
|
||||
the skill. Clear every selection only when the skill is known to work everywhere.
|
||||
</p>
|
||||
<fieldset className="grid gap-4 border border-border p-4 sm:grid-cols-2">
|
||||
<legend className="px-2 text-xs font-semibold uppercase tracking-wide">Compatibility targets</legend>
|
||||
<div>
|
||||
<Label htmlFor={`${id}-hermes-version`} className="text-xs font-medium">
|
||||
Minimum Hermes version
|
||||
</Label>
|
||||
<p className="mb-2 mt-0.5 text-[11px] text-text-tertiary">Older Hermes installations will be blocked.</p>
|
||||
<Input
|
||||
id={`${id}-hermes-version`}
|
||||
disabled={disabled}
|
||||
maxLength={512}
|
||||
value={value.hermes.minimum_version}
|
||||
onChange={event => update('hermes', { minimum_version: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={`${id}-context-window`} className="text-xs font-medium">
|
||||
Minimum model context window
|
||||
</Label>
|
||||
<p className="mb-2 mt-0.5 text-[11px] text-text-tertiary">Leave blank when no minimum is required.</p>
|
||||
<Input
|
||||
id={`${id}-context-window`}
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
disabled={disabled}
|
||||
value={value.model.minimum_context_window ?? ''}
|
||||
onChange={event =>
|
||||
update('model', {
|
||||
...value.model,
|
||||
minimum_context_window: event.target.value === '' ? null : Number(event.target.value)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<PresetMultiSelect
|
||||
label="Platforms"
|
||||
description="Checked systems are the allowed install targets. New drafts start with this authoring system."
|
||||
value={value.platforms}
|
||||
options={PLATFORM_OPTIONS}
|
||||
disabled={disabled}
|
||||
onChange={platforms => update('platforms', platforms)}
|
||||
/>
|
||||
<PresetMultiSelect
|
||||
label="Architectures"
|
||||
description="Checked processors are the allowed install targets. New drafts start with this device."
|
||||
value={value.architectures}
|
||||
options={ARCHITECTURE_OPTIONS}
|
||||
disabled={disabled}
|
||||
onChange={architectures => update('architectures', architectures)}
|
||||
/>
|
||||
<StringListField
|
||||
label="Model capabilities"
|
||||
description="Capabilities the active model must provide, such as vision or tool use."
|
||||
itemLabel="model capability"
|
||||
value={value.model.capabilities}
|
||||
disabled={disabled}
|
||||
placeholder="vision"
|
||||
onChange={capabilities => update('model', { ...value.model, capabilities })}
|
||||
/>
|
||||
<StringListField
|
||||
label="Hardware"
|
||||
description="Physical hardware requirements, such as a GPU."
|
||||
itemLabel="hardware requirement"
|
||||
value={value.hardware}
|
||||
disabled={disabled}
|
||||
placeholder="gpu"
|
||||
onChange={hardware => update('hardware', hardware)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="border border-border p-4">
|
||||
<legend className="px-2 text-xs font-semibold uppercase tracking-wide">Runtime access</legend>
|
||||
<p className="mb-4 text-[11px] leading-4 text-text-tertiary">
|
||||
These requirements are checked locally before installation. They do not grant permissions by themselves.
|
||||
</p>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<BooleanField
|
||||
id={`${id}-shell`}
|
||||
label="Shell commands"
|
||||
description="The skill needs to run terminal commands."
|
||||
checked={value.runtime.shell}
|
||||
disabled={disabled}
|
||||
onChange={shell => update('runtime', { ...value.runtime, shell })}
|
||||
/>
|
||||
<BooleanField
|
||||
id={`${id}-browser`}
|
||||
label="Browser control"
|
||||
description="The skill needs an interactive browser."
|
||||
checked={value.runtime.browser}
|
||||
disabled={disabled}
|
||||
onChange={browser => update('runtime', { ...value.runtime, browser })}
|
||||
/>
|
||||
<BooleanField
|
||||
id={`${id}-code`}
|
||||
label="Code execution"
|
||||
description="The skill needs a code-execution environment."
|
||||
checked={value.runtime.code}
|
||||
disabled={disabled}
|
||||
onChange={code => update('runtime', { ...value.runtime, code })}
|
||||
/>
|
||||
<BooleanField
|
||||
id={`${id}-sandbox`}
|
||||
label="Sandbox required"
|
||||
description="The skill must run with sandbox isolation enabled."
|
||||
checked={value.runtime.sandbox}
|
||||
disabled={disabled}
|
||||
onChange={sandbox => update('runtime', { ...value.runtime, sandbox })}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="border border-border p-4">
|
||||
<legend className="px-2 text-xs font-semibold uppercase tracking-wide">Tools and plugins</legend>
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h4 className="text-xs font-medium">Tools</h4>
|
||||
<p className="mb-3 mt-0.5 text-[11px] text-text-tertiary">
|
||||
Hermes pre-fills explicit skill requirements and checks whether they are enabled. This manifest can never
|
||||
install them automatically.
|
||||
</p>
|
||||
<ToolRequirements value={value.tools} disabled={disabled} onChange={tools => update('tools', tools)} />
|
||||
</div>
|
||||
<div className="border-t border-border pt-4">
|
||||
<h4 className="text-xs font-medium">Plugins</h4>
|
||||
<p className="mb-3 mt-0.5 text-[11px] text-text-tertiary">
|
||||
Hermes pre-fills explicitly declared plugin IDs. Confirm whether each is required.
|
||||
</p>
|
||||
<PluginRequirements
|
||||
value={value.plugins}
|
||||
disabled={disabled}
|
||||
onChange={plugins => update('plugins', plugins)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="grid gap-5 border border-border p-4 sm:grid-cols-2">
|
||||
<legend className="px-2 text-xs font-semibold uppercase tracking-wide">Credentials and connections</legend>
|
||||
<StringListField
|
||||
label="Credentials"
|
||||
description="Names only. Secret values are never included in the manifest."
|
||||
itemLabel="credential"
|
||||
value={value.credentials}
|
||||
disabled={disabled}
|
||||
placeholder="EXAMPLE_TOKEN"
|
||||
onChange={credentials => update('credentials', credentials)}
|
||||
/>
|
||||
<StringListField
|
||||
label="Connections"
|
||||
description="Configured services or linked accounts the skill needs."
|
||||
itemLabel="connection"
|
||||
value={value.connections}
|
||||
disabled={disabled}
|
||||
placeholder="team-mcp"
|
||||
onChange={connections => update('connections', connections)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="grid gap-5 border border-border p-4 sm:grid-cols-2">
|
||||
<legend className="px-2 text-xs font-semibold uppercase tracking-wide">Data access</legend>
|
||||
<StringListField
|
||||
label="Filesystem read access"
|
||||
description="Paths the skill needs to read."
|
||||
itemLabel="read path"
|
||||
value={value.filesystem.read}
|
||||
disabled={disabled}
|
||||
placeholder="~/project"
|
||||
onChange={read => update('filesystem', { ...value.filesystem, read })}
|
||||
/>
|
||||
<StringListField
|
||||
label="Filesystem write access"
|
||||
description="Paths the skill needs to modify."
|
||||
itemLabel="write path"
|
||||
value={value.filesystem.write}
|
||||
disabled={disabled}
|
||||
placeholder="~/project/output"
|
||||
onChange={write => update('filesystem', { ...value.filesystem, write })}
|
||||
/>
|
||||
<StringListField
|
||||
label="Network destinations"
|
||||
description="Hosts or services the skill needs to contact."
|
||||
itemLabel="network destination"
|
||||
value={value.network.destinations}
|
||||
disabled={disabled}
|
||||
placeholder="api.example.com"
|
||||
onChange={destinations => update('network', { destinations })}
|
||||
/>
|
||||
<StringListField
|
||||
label="Known limitations"
|
||||
description="Important constraints teammates should understand before installation."
|
||||
itemLabel="known limitation"
|
||||
value={value.known_limitations}
|
||||
disabled={disabled}
|
||||
placeholder="Manual verification is still required"
|
||||
onChange={known_limitations => update('known_limitations', known_limitations)}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WisdomManifestEditor({ value, disabled = false, onChange }: ManifestEditorProps) {
|
||||
const id = useId()
|
||||
let manifest: WisdomManifestV1
|
||||
try {
|
||||
manifest = parseWisdomManifest(value)
|
||||
} catch (reason) {
|
||||
return (
|
||||
<div role="alert" className="mt-3 border border-red-500/50 p-3 text-xs text-red-300">
|
||||
This server-reviewed manifest cannot be safely represented as a form. Reload the draft and try again.{' '}
|
||||
{reason instanceof Error ? reason.message : String(reason)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const validationError = wisdomManifestValidationError(value)
|
||||
const update = (next: WisdomManifestV1) => onChange(JSON.stringify(next))
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-4">
|
||||
{validationError && (
|
||||
<div role="alert" className="border border-amber-500/50 bg-amber-500/5 p-3 text-xs text-amber-200">
|
||||
{validationError}
|
||||
</div>
|
||||
)}
|
||||
<fieldset className="grid gap-4 border border-border p-4 sm:grid-cols-[1fr_auto]">
|
||||
<legend className="px-2 text-xs font-semibold uppercase tracking-wide">Skill identity</legend>
|
||||
<div>
|
||||
<Label htmlFor={`${id}-manifest-name`} className="text-xs font-medium">
|
||||
Skill name
|
||||
</Label>
|
||||
<p className="mb-2 mt-0.5 text-[11px] text-text-tertiary">The package name recorded in the manifest.</p>
|
||||
<Input
|
||||
id={`${id}-manifest-name`}
|
||||
disabled={disabled}
|
||||
maxLength={512}
|
||||
value={manifest.name}
|
||||
onChange={event => update({ ...manifest, name: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-32">
|
||||
<Label className="text-xs font-medium">Schema version</Label>
|
||||
<p className="mt-2 font-mono text-sm">1</p>
|
||||
<p className="mt-0.5 text-[11px] text-text-tertiary">Fixed by the V1 contract.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
<WisdomSystemSpecificationEditor
|
||||
value={manifest.requirements}
|
||||
disabled={disabled}
|
||||
onChange={requirements => update({ ...manifest, requirements })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { StrictMode } from 'react'
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { WisdomNotificationSettings } from './WisdomNotificationSettings'
|
||||
|
||||
const { prepare, read, choose } = vi.hoisted(() => ({ prepare: vi.fn(), read: vi.fn(), choose: vi.fn() }))
|
||||
vi.mock('@/lib/api', () => ({ api: { prepareWisdomMute: prepare, getWisdomMute: read, chooseWisdomMute: choose } }))
|
||||
const control = {
|
||||
id: 'a'.repeat(32),
|
||||
expires_at: Date.now() / 1000 + 600,
|
||||
organization_id: 'org',
|
||||
sync: null,
|
||||
mute: { org_id: 'org', revision: 3, duration: null, muted: false, forever: false, muted_until: null }
|
||||
}
|
||||
const snapshot = { organization_id: 'org', gateway_available: true, mute: control.mute, sync: null }
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
prepare.mockResolvedValue(control)
|
||||
read.mockResolvedValue(snapshot)
|
||||
choose.mockResolvedValue(snapshot)
|
||||
})
|
||||
afterEach(cleanup)
|
||||
async function open() {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Notification settings' }))
|
||||
await screen.findByText('Notifications on', { selector: 'p' })
|
||||
}
|
||||
|
||||
describe('Wisdom notification settings', () => {
|
||||
it('loads only on request and changes only after Save', async () => {
|
||||
render(<WisdomNotificationSettings profile="demo" />)
|
||||
expect(prepare).not.toHaveBeenCalled()
|
||||
await open()
|
||||
fireEvent.change(screen.getByRole('combobox'), { target: { value: '1_week' } })
|
||||
expect(choose).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||
await waitFor(() => expect(choose).toHaveBeenCalledOnce())
|
||||
expect(choose).toHaveBeenCalledWith(control.id, '1_week', 'demo')
|
||||
})
|
||||
|
||||
it('keeps the original choice when a reply is lost', async () => {
|
||||
choose.mockRejectedValueOnce(new Error('timeout'))
|
||||
render(<WisdomNotificationSettings profile="demo" />)
|
||||
await open()
|
||||
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'forever' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||
await screen.findByRole('button', { name: 'Retry' })
|
||||
expect((screen.getByRole('combobox') as HTMLSelectElement).disabled).toBe(true)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
|
||||
await waitFor(() => expect(choose).toHaveBeenCalledTimes(2))
|
||||
expect(choose.mock.calls[0]).toEqual(choose.mock.calls[1])
|
||||
})
|
||||
|
||||
it('does not hide shared state on a transient poll error', async () => {
|
||||
render(<WisdomNotificationSettings profile="demo" />)
|
||||
await open()
|
||||
read.mockRejectedValue(new Error('offline'))
|
||||
fireEvent(window, new Event('focus'))
|
||||
await screen.findByRole('alert')
|
||||
expect(screen.getByText('Notifications on', { selector: 'p' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('remains usable under StrictMode and separates pending from shared state', async () => {
|
||||
prepare.mockResolvedValue({
|
||||
...control,
|
||||
sync: {
|
||||
preference_sync: 'pending',
|
||||
requested_duration: 'forever',
|
||||
mutation_id: 'queued'
|
||||
}
|
||||
})
|
||||
render(
|
||||
<StrictMode>
|
||||
<WisdomNotificationSettings profile="demo" />
|
||||
</StrictMode>
|
||||
)
|
||||
await open()
|
||||
expect(screen.getByText('Your choice is saved locally and waiting to sync.')).toBeTruthy()
|
||||
expect(choose).not.toHaveBeenCalled()
|
||||
expect(screen.getByText('Notifications on', { selector: 'p' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('drops responses from the previous profile', async () => {
|
||||
let finish!: (value: typeof control) => void
|
||||
prepare.mockReturnValueOnce(
|
||||
new Promise(resolve => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
const { rerender } = render(<WisdomNotificationSettings profile="first" />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Notification settings' }))
|
||||
await waitFor(() => expect(prepare).toHaveBeenCalledOnce())
|
||||
rerender(<WisdomNotificationSettings profile="second" />)
|
||||
await act(async () => finish(control))
|
||||
expect(screen.queryByRole('combobox')).toBeNull()
|
||||
await open()
|
||||
expect(prepare).toHaveBeenLastCalledWith('second')
|
||||
})
|
||||
})
|
||||
@@ -1,152 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createWisdomMuteController, initialWisdomMuteState, type WisdomMuteDuration } from '@hermes/shared'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { api } from '@/lib/api'
|
||||
import { Button } from '@nous-research/ui/ui/components/button'
|
||||
import { Volume2, VolumeX, RefreshCw, Loader2 } from 'lucide-react'
|
||||
|
||||
export function WisdomNotificationSettings({ profile }: { profile?: string }) {
|
||||
return <Settings key={profile || 'default'} profile={profile} />
|
||||
}
|
||||
|
||||
function Settings({ profile }: { profile?: string }) {
|
||||
const { t, locale } = useI18n()
|
||||
const copy = t.skills.wisdom.notificationPreferences
|
||||
const [open, setOpen] = useState(false)
|
||||
const [state, setState] = useState(initialWisdomMuteState)
|
||||
const [scope] = useState(profile)
|
||||
const controller = useRef<ReturnType<typeof createWisdomMuteController> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const next = createWisdomMuteController({
|
||||
prepare: () => api.prepareWisdomMute(scope),
|
||||
read: () => api.getWisdomMute(scope),
|
||||
choose: (id, duration) => api.chooseWisdomMute(id, duration, scope),
|
||||
changed: setState
|
||||
})
|
||||
controller.current = next
|
||||
void next.refresh()
|
||||
const poll = () => {
|
||||
if (document.visibilityState !== 'hidden') void next.poll()
|
||||
}
|
||||
const timer = window.setInterval(poll, 15_000)
|
||||
window.addEventListener('focus', poll)
|
||||
return () => {
|
||||
next.dispose()
|
||||
controller.current = null
|
||||
window.clearInterval(timer)
|
||||
window.removeEventListener('focus', poll)
|
||||
}
|
||||
}, [open, scope])
|
||||
|
||||
const muted = state.snapshot?.mute?.muted
|
||||
const until = state.snapshot?.mute?.muted_until
|
||||
const sync = state.snapshot?.sync?.preference_sync
|
||||
const notice =
|
||||
sync === 'pending' || sync === 'syncing'
|
||||
? copy.pending
|
||||
: sync === 'failed'
|
||||
? copy.failed
|
||||
: sync === 'conflict'
|
||||
? copy.conflict
|
||||
: sync === 'expired'
|
||||
? copy.expired
|
||||
: null
|
||||
const Icon = muted ? VolumeX : Volume2
|
||||
const choice = state.choice === undefined ? '' : (state.choice ?? 'off')
|
||||
|
||||
return (
|
||||
<section className="border-b border-border min-w-0 py-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex max-w-full items-center gap-2 text-sm"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen(value => !value)}
|
||||
>
|
||||
<Icon className="size-4 shrink-0" aria-hidden="true" />
|
||||
<span className="break-words text-start">{copy.title}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="mt-3 min-w-0 space-y-3">
|
||||
<p className="text-text-secondary text-xs">{copy.scope}</p>
|
||||
<p className="break-words text-sm" role="status">
|
||||
{state.snapshot?.mute
|
||||
? muted
|
||||
? copy.muted
|
||||
: copy.on
|
||||
: state.busy
|
||||
? t.common.loading
|
||||
: t.skills.wisdom.unavailable}
|
||||
{muted && until && (
|
||||
<>
|
||||
{' '}
|
||||
· <time dateTime={until}>{new Date(until).toLocaleString(locale)}</time>
|
||||
</>
|
||||
)}
|
||||
{muted && state.snapshot?.mute?.forever && <> · {copy.forever}</>}
|
||||
</p>
|
||||
{notice && (
|
||||
<p className="text-xs" role="status">
|
||||
{notice}
|
||||
</p>
|
||||
)}
|
||||
{state.error && (
|
||||
<p className="text-red-500 text-xs" role="alert">
|
||||
{t.skills.wisdom.unavailable}
|
||||
</p>
|
||||
)}
|
||||
<form
|
||||
className="flex min-w-0 flex-wrap items-center gap-2"
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
void controller.current?.apply()
|
||||
}}
|
||||
>
|
||||
<select
|
||||
aria-label={copy.title}
|
||||
value={choice}
|
||||
disabled={state.busy || !state.control || state.retryingChoice}
|
||||
className="border-border bg-bg-primary h-9 min-w-0 max-w-full flex-1 rounded border px-2 text-sm"
|
||||
onChange={event =>
|
||||
controller.current?.select(
|
||||
event.target.value === 'off' ? null : (event.target.value as WisdomMuteDuration)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{copy.title}
|
||||
</option>
|
||||
<option value="off">{copy.on}</option>
|
||||
<option value="1_day">{copy.day}</option>
|
||||
<option value="1_week">{copy.week}</option>
|
||||
<option value="30_days">{copy.month}</option>
|
||||
<option value="forever">{copy.forever}</option>
|
||||
</select>
|
||||
<Button
|
||||
className="h-9 shrink-0"
|
||||
type="button"
|
||||
size="sm"
|
||||
outlined
|
||||
disabled={state.busy}
|
||||
aria-label={t.common.refresh}
|
||||
title={t.common.refresh}
|
||||
onClick={() => void controller.current?.refresh()}
|
||||
>
|
||||
<RefreshCw className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
className="h-9 shrink-0"
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={state.busy || (!state.retryingChoice && (state.choice === undefined || !state.control))}
|
||||
>
|
||||
{state.busy && <Loader2 className="size-4 animate-spin" aria-hidden="true" />}
|
||||
{state.retryingChoice ? t.common.retry : t.common.save}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { WisdomSyncStatus } from './WisdomSyncStatus'
|
||||
const { read, retry } = vi.hoisted(() => ({ read: vi.fn(), retry: vi.fn() }))
|
||||
vi.mock('@/lib/api', () => ({ api: { getWisdomSync: read, retryWisdomSync: retry } }))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
useI18n: () => ({ t: { common: { refresh: 'Refresh', loading: 'Loading' }, skills: { wisdom: {} } } })
|
||||
}))
|
||||
const counts = { pending: 0, syncing: 0, retryable: 0, conflict: 0, uncertain: 0, waiting_for_receipt: 0 }
|
||||
const snapshot = { delivery: counts, operation: { ...counts, retryable: 1 }, can_retry: true }
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
read.mockResolvedValue(snapshot)
|
||||
retry.mockResolvedValue({ delivery: counts, operation: counts, can_retry: false })
|
||||
})
|
||||
afterEach(cleanup)
|
||||
it('opens read-only and only retries on the explicit control', async () => {
|
||||
render(<WisdomSyncStatus profile="research" />)
|
||||
expect(read).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Receipt and report sync' }))
|
||||
await screen.findByText('Retry available: 1')
|
||||
expect(read).toHaveBeenCalledWith('research')
|
||||
expect(retry).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Retry sync' }))
|
||||
await waitFor(() => expect(screen.getAllByText('Up to date')).toHaveLength(2))
|
||||
expect(retry).toHaveBeenCalledExactlyOnceWith('research')
|
||||
expect(screen.getByRole('button', { name: 'Retry sync' }).hasAttribute('disabled')).toBe(true)
|
||||
})
|
||||
it('does not expose a retired profiles in-flight result', async () => {
|
||||
let finish!: (v: typeof snapshot) => void
|
||||
read.mockReturnValueOnce(
|
||||
new Promise(resolve => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
const view = render(<WisdomSyncStatus profile="research" />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Receipt and report sync' }))
|
||||
view.rerender(<WisdomSyncStatus profile="other" />)
|
||||
await act(async () => {
|
||||
finish(snapshot)
|
||||
})
|
||||
expect(screen.queryByText('Retry available: 1')).toBeNull()
|
||||
expect(screen.getByRole('button', { name: 'Receipt and report sync' }).getAttribute('aria-expanded')).toBe('false')
|
||||
})
|
||||
it('shows unavailable without raw errors or an enabled retry', async () => {
|
||||
read.mockRejectedValueOnce(new Error('secret transport payload'))
|
||||
render(<WisdomSyncStatus />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Receipt and report sync' }))
|
||||
expect((await screen.findByRole('alert')).textContent).toContain('Sync status could not be confirmed.')
|
||||
expect(screen.queryByText('secret transport payload')).toBeNull()
|
||||
expect(screen.getByRole('button', { name: 'Retry sync' }).hasAttribute('disabled')).toBe(true)
|
||||
})
|
||||
@@ -1,111 +0,0 @@
|
||||
import { createWisdomSyncController, initialWisdomSyncView, wisdomSyncCopy, type WisdomSyncState } from '@hermes/shared'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Button } from '@nous-research/ui/ui/components/button'
|
||||
import { Loader2, RefreshCw } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
export function WisdomSyncStatus({ profile }: { profile?: string }) {
|
||||
return <SyncStatus key={profile || 'default'} profile={profile} />
|
||||
}
|
||||
|
||||
function SyncStatus({ profile }: { profile?: string }) {
|
||||
const { t } = useI18n()
|
||||
const copy = t.skills.wisdom.syncRecovery ?? wisdomSyncCopy
|
||||
const [open, setOpen] = useState(false)
|
||||
const [scope] = useState(profile)
|
||||
const [view, setView] = useState(initialWisdomSyncView)
|
||||
const controller = useRef<ReturnType<typeof createWisdomSyncController> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const next = createWisdomSyncController({
|
||||
read: () => api.getWisdomSync(scope),
|
||||
retry: () => api.retryWisdomSync(scope),
|
||||
changed: setView
|
||||
})
|
||||
controller.current = next
|
||||
void next.refresh()
|
||||
const poll = () => {
|
||||
if (document.visibilityState !== 'hidden') void next.refresh()
|
||||
}
|
||||
const timer = window.setInterval(poll, 15_000)
|
||||
return () => {
|
||||
next.dispose()
|
||||
controller.current = null
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}, [open, scope])
|
||||
return (
|
||||
<section className="min-w-0 border-b border-border py-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen(value => !value)}
|
||||
className="flex max-w-full items-center gap-2 text-sm"
|
||||
>
|
||||
<RefreshCw aria-hidden="true" className="size-4 shrink-0" />
|
||||
<span className="break-words text-start">{copy.title}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="mt-3 min-w-0 space-y-3">
|
||||
<p className="text-xs text-text-secondary">{copy.scope}</p>
|
||||
{view.error && (
|
||||
<p role="alert" className="text-xs text-red-500">
|
||||
{copy.unavailable}
|
||||
</p>
|
||||
)}
|
||||
{!view.snapshot && view.busy && (
|
||||
<p role="status" className="text-xs">
|
||||
{t.common.loading}
|
||||
</p>
|
||||
)}
|
||||
{view.snapshot && !view.error && (
|
||||
<dl className="space-y-2 text-xs" aria-live="polite">
|
||||
{(['delivery', 'operation'] as const).map(kind => {
|
||||
const counts = view.snapshot![kind]
|
||||
const states = (Object.keys(counts) as WisdomSyncState[]).filter(state => counts[state] > 0)
|
||||
return (
|
||||
<div key={kind}>
|
||||
<dt className="font-medium">{copy[kind]}</dt>
|
||||
<dd className="break-words text-text-secondary">
|
||||
{states.length
|
||||
? states.map(state => (
|
||||
<div key={state}>
|
||||
{copy.states[state]}: {counts[state]}
|
||||
</div>
|
||||
))
|
||||
: copy.current}
|
||||
</dd>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</dl>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
outlined
|
||||
title={t.common.refresh}
|
||||
aria-label={t.common.refresh}
|
||||
disabled={view.busy}
|
||||
onClick={() => void controller.current?.refresh()}
|
||||
>
|
||||
<RefreshCw className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={view.busy || view.error || !view.snapshot?.can_retry}
|
||||
onClick={() => void controller.current?.retry()}
|
||||
>
|
||||
{view.busy && <Loader2 className="size-4 animate-spin" aria-hidden="true" />}
|
||||
{copy.retry}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -360,127 +360,6 @@ export const af: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Kennisgewinginstellings",
|
||||
scope: "Jou proaktiewe kennisgewings oor kliënte in hierdie organisasie. Handmatige blaai en deel bly beskikbaar.",
|
||||
on: "Kennisgewings aan",
|
||||
muted: "Kennisgewings gedemp",
|
||||
day: "1 dag",
|
||||
week: "1 week",
|
||||
month: "30 dae",
|
||||
forever: "Onbepaald",
|
||||
pending: "Jou keuse is plaaslik gestoor en wag om te sinchroniseer.",
|
||||
failed: "Sinchronisering het misluk. Verfris instellings voordat jy weer kies.",
|
||||
conflict: "Jou voorkeur het op 'n ander kliënt verander. Verfris om dit na te gaan.",
|
||||
expired: "Hierdie keuse het verval. Verfris instellings om weer te probeer.",
|
||||
},
|
||||
tab: 'Kollektief',
|
||||
browseHub: 'Blaai deur Hub',
|
||||
title: 'Kollektiewe Wysheid',
|
||||
loading: 'Kollektiewe Wysheid laai',
|
||||
unavailable: 'Kollektiewe Wysheid is nie beskikbaar nie.',
|
||||
setup: 'Kollektiewe Wysheid is nie vir hierdie profiel opgestel nie.',
|
||||
setupDisclosure:
|
||||
'Kandidaatkwalifikasie bly op hierdie profiel. Slegs privaat konsepinhoud wat deur die eienaar goedgekeur is, outeurteks, verklarende manifesmetadata en bestuurde installasiestatus bereik die Gateway.',
|
||||
setupAction: 'Ek verstaan — stel hierdie profiel op',
|
||||
settingUp: 'Word opgestel…',
|
||||
scanLocal: 'Skandeer plaaslike vaardighede',
|
||||
search: 'Soek gedeelde vaardighede…',
|
||||
potential: 'Voorgestelde bydraes',
|
||||
potentialHelp:
|
||||
'Vaardighede wat Hermes plaaslik gekwalifiseer het op grond van gebruik of betekenisvolle verfyning. Niks word gedeel voordat jy dit hersien nie.',
|
||||
noSuggestions: 'Geen plaaslike vaardighede voldoen tans aan die outomatiese kwalifikasiereëls nie.',
|
||||
browseLocal: count => `Bekyk alle plaaslike vaardighede (${count})`,
|
||||
browseLocalHelp:
|
||||
'Handmatige keuse beteken nie dat Hermes hierdie vaardighede gebruik of outomaties gekwalifiseer het nie.',
|
||||
ownerReview: 'Jou bydraekonsepte',
|
||||
ownerReviewHelp: 'Konsepte wat op jou hersiening wag en indienings wat op kollektiewe goedkeuring wag.',
|
||||
noDrafts: 'Geen aktiewe bydraekonsepte of indienings nie.',
|
||||
prepare: 'Begin bydrae',
|
||||
continueDraft: 'Gaan voort met konsep',
|
||||
localOnly: 'Kan vanaf hierdie toestel gedeel word',
|
||||
qualifiedLocally: 'Hermes het hierdie plaaslike vaardigheid as ’n moontlike bydrae geïdentifiseer.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Jou organisasie (${organizationName})` : 'Jou organisasie'} het Collective Wisdom geaktiveer, ’n funksie wat nuttige vaardighede outomaties oor alle spanlede opspoor. Geluk! Hermes het ’n vaardigheid opgespoor wat vir jou span nuttig kan wees.`,
|
||||
qualificationReturning: 'Hermes het nog ’n vaardigheid opgespoor wat vir jou span nuttig kan wees.',
|
||||
savedLocally: '’n Privaat konsep is op hierdie toestel gestoor.',
|
||||
reviewExact: 'Bekyk besonderhede',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Bedienerhersiening is aan die gang',
|
||||
ready: 'Gereed vir jou hersiening',
|
||||
owner_approved: 'Deur jou goedgekeur',
|
||||
publishing: 'Word gepubliseer',
|
||||
pending_moderation: 'Wag op goedkeuring deur die kollektief-administrateur',
|
||||
changes_requested: 'Veranderinge versoek'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'bedienerskandering geslaag',
|
||||
noDescription: 'Geen beskrywing deur die eienaar nie.',
|
||||
managedInstalls: 'bestuurde installasies',
|
||||
close: 'Sluit',
|
||||
readEvery: 'Lees elke rou lêer. Goedkeuring is aan die presiese drie hutswaardes hieronder gebind.',
|
||||
editReview:
|
||||
'Wysig die beskrywing, SKILL.md of verklarende manifes hier. Stoor skep ’n nuwe privaat hersiening, voer skanderings weer uit en lewer nuwe hutswaardes; dit herskryf nie jou plaaslike bronvaardigheid nie.',
|
||||
unsavedChanges: 'Hierdie veranderinge is nog nie geskandeer nie. Stoor en skandeer weer voor goedkeuring.',
|
||||
saveAndRescan: 'Stoor veranderinge en skandeer weer',
|
||||
savingRevision: 'Word gestoor en weer geskandeer…',
|
||||
resetChanges: 'Gooi wysigings weg',
|
||||
reviewedHashes: 'Hutswaardes vir die hersiening wat tans deur die bediener nagegaan is',
|
||||
prepareTitle: 'Hersien plaaslike pakket voor oplaai',
|
||||
prepareNotice:
|
||||
'Hierdie velde bly op hierdie profiel totdat jy dit uitdruklik vir ’n privaat bedienerhersiening deur die eienaar indien. Plaaslike kwalifikasietellings en -redes word nooit ingesluit nie.',
|
||||
ownerDescription: 'Beskrywing deur die eienaar',
|
||||
systemSpecification: 'Stelselspesifikasie (verklarende metadata; geen afhanklikhede word geïnstalleer nie)',
|
||||
localOverlay: 'Plaaslike oorlaag',
|
||||
cancel: 'Kanselleer',
|
||||
submit: 'Dien konsep in',
|
||||
submitting: 'Word ingedien…',
|
||||
publishing: 'Word gepubliseer…',
|
||||
approve: 'Keur presiese inhoud goed en publiseer',
|
||||
decline: 'Weier',
|
||||
checkUpdates: count => `Kyk vir opdaterings${count ? ` (${count})` : ''}`,
|
||||
checking: 'Word nagegaan…',
|
||||
refreshShared: 'Verfris gedeelde vaardighede',
|
||||
refreshingShared: 'Word verfris…',
|
||||
installReferenceLabel: "Installeer vanaf 'n skakel of vaardigheid-ID",
|
||||
installReferencePlaceholder: "Plak 'n Portal-skakel, vaardigheid-ID of skill-id@vN",
|
||||
installReferenceHelp: "Hermes verifieer die presiese weergawe en wys 'n versoenbaarheidsplan voor installasie.",
|
||||
reviewInstall: 'Hersien installasie',
|
||||
planningInstall: 'Word geverifieer…',
|
||||
updateModeLabel: 'Toekomstige opdaterings',
|
||||
updateModeDefault: 'Gebruik organisasie se verstek',
|
||||
updateModeManual: 'Handmatig',
|
||||
updateModeAutomatic: 'Outomaties met kennisgewing',
|
||||
updateModeRequired: 'Vereis',
|
||||
updateModeHelp:
|
||||
'Gateway pas jou organisasie se huidige beleid toe. Veiligheidsensitiewe veranderings vereis steeds jou goedkeuring.',
|
||||
updateModePlan: mode => `Toekomstige opdaterings: ${mode}`,
|
||||
activityReady: count => `${count} nuwe ${count === 1 ? 'kennisgewing' : 'kennisgewings'}`,
|
||||
decisionPublished: skill => `${skill} is goedgekeur en word nou met jou span gedeel.`,
|
||||
decisionChanges: skill => `${skill} benodig veranderinge voordat dit gedeel kan word.`,
|
||||
decisionDeclined: skill => `${skill} is nie vir deling goedgekeur nie.`,
|
||||
decisionChanged: (skill, state) => `${skill} se bydraestatus het na ${state} verander.`,
|
||||
installedNotice: (skill, version) =>
|
||||
`${skill}${version ? ` ${version}` : ''} is op hierdie profiel geïnstalleer.`,
|
||||
updateNotice: (skill, version) => `’n Opdatering is vir ${skill}${version ? ` ${version}` : ''} beskikbaar.`,
|
||||
newSkillNotice: skill => `${skill} is met jou kollektief gedeel.`,
|
||||
archivedNotice: skill => `${skill} is nie meer vir nuwe installasies beskikbaar nie.`,
|
||||
takedownNotice: skill => `${skill} is uit die kollektief verwyder.`,
|
||||
markSeen: 'Merk alles as gelees',
|
||||
install: 'Installeer…',
|
||||
uninstall: 'Deïnstalleer…',
|
||||
checkSkill: 'Gaan hierdie vaardigheid na',
|
||||
updateAvailable: version => (version ? `v${version}-opdatering beskikbaar` : 'Opdatering beskikbaar'),
|
||||
reviewUpdate: 'Hersien opdatering',
|
||||
installed: (version, mode) => `Geïnstalleer v${version} · ${mode}`,
|
||||
confirmAction: action => `Bevestig ${action}`,
|
||||
acceptCompatibility: 'Ek het die versoenbaarheidsaksies nagegaan en aanvaar dit.',
|
||||
acceptSensitive: 'Ek aanvaar die nuwe sensitiewe vereistes uitdruklik.',
|
||||
preserveModified: 'Bewaar eers my gewysigde kopie as ’n onbestuurde vurk.'
|
||||
},
|
||||
title: "Vaardighede",
|
||||
searchPlaceholder: "Soek vaardighede en gereedskapstelle...",
|
||||
enabledOf: "{enabled}/{total} geaktiveer",
|
||||
|
||||
@@ -305,124 +305,6 @@ export const ar = defineLocale({
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "إعدادات الإشعارات",
|
||||
scope: "إشعاراتك الاستباقية عبر تطبيقات هذه المؤسسة. يبقى التصفح والمشاركة اليدويان متاحين.",
|
||||
on: "الإشعارات مفعلة",
|
||||
muted: "الإشعارات مكتومة",
|
||||
day: "يوم واحد",
|
||||
week: "أسبوع واحد",
|
||||
month: "30 يومًا",
|
||||
forever: "بلا حد زمني",
|
||||
pending: "تم حفظ اختيارك محليًا وهو بانتظار المزامنة.",
|
||||
failed: "فشلت المزامنة. حدّث الإعدادات قبل الاختيار مجددًا.",
|
||||
conflict: "تغير تفضيلك في تطبيق آخر. حدّث لمراجعته.",
|
||||
expired: "انتهت صلاحية هذا الاختيار. حدّث الإعدادات للمحاولة مجددًا.",
|
||||
},
|
||||
tab: 'المجموعة',
|
||||
browseHub: 'تصفح Hub',
|
||||
title: 'الحكمة الجماعية',
|
||||
loading: 'جارٍ تحميل الحكمة الجماعية',
|
||||
unavailable: 'الحكمة الجماعية غير متاحة.',
|
||||
setup: 'لم يتم إعداد الحكمة الجماعية لهذا الملف الشخصي.',
|
||||
setupDisclosure:
|
||||
'يبقى تأهيل المرشحين في هذا الملف الشخصي. لا تصل إلى البوابة إلا محتويات المسودة الخاصة التي وافق عليها المالك، ونص المؤلف، وبيانات البيان التعريفية التصريحية، وحالة التثبيت المُدار.',
|
||||
setupAction: 'أفهم ذلك — إعداد هذا الملف الشخصي',
|
||||
settingUp: 'جارٍ الإعداد…',
|
||||
scanLocal: 'فحص المهارات المحلية',
|
||||
search: 'البحث في المهارات المشتركة…',
|
||||
potential: 'مساهمات مقترحة',
|
||||
potentialHelp: 'مهارات أهلها Hermes محليًا بناءً على الاستخدام أو التحسينات المهمة. لن تتم مشاركتها قبل مراجعتك.',
|
||||
noSuggestions: 'لا توجد حاليًا مهارات محلية تستوفي قواعد التأهيل التلقائي.',
|
||||
browseLocal: count => `عرض كل المهارات المحلية (${count})`,
|
||||
browseLocalHelp: 'الاختيار اليدوي لا يعني أن Hermes استخدم هذه المهارات أو أهلها تلقائيًا.',
|
||||
ownerReview: 'مسودات مساهماتك',
|
||||
ownerReviewHelp: 'مسودات تنتظر مراجعتك وطلبات تنتظر موافقة المجموعة.',
|
||||
noDrafts: 'لا توجد مسودات أو طلبات مساهمة نشطة.',
|
||||
prepare: 'بدء المساهمة',
|
||||
continueDraft: 'متابعة المسودة',
|
||||
localOnly: 'متاحة للمشاركة من هذا الجهاز',
|
||||
qualifiedLocally: 'حدّد Hermes هذه المهارة المحلية كمساهمة محتملة.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `فعّلت مؤسستك (${organizationName})` : 'فعّلت مؤسستك'} Collective Wisdom، وهي ميزة مصممة لاكتشاف المهارات المفيدة تلقائيًا لدى جميع أعضاء الفريق. تهانينا! اكتشف Hermes مهارة قد تكون مفيدة لفريقك.`,
|
||||
qualificationReturning: 'اكتشف Hermes مهارة أخرى قد تكون مفيدة لفريقك.',
|
||||
savedLocally: 'تم حفظ مسودة خاصة على هذا الجهاز.',
|
||||
reviewExact: 'عرض التفاصيل',
|
||||
draftState: (state: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'مراجعة الخادم جارية',
|
||||
ready: 'جاهزة لمراجعتك',
|
||||
owner_approved: 'وافقت عليها',
|
||||
publishing: 'جارٍ النشر',
|
||||
pending_moderation: 'في انتظار موافقة مسؤول المجموعة',
|
||||
changes_requested: 'طُلبت تغييرات'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'نجح فحص الخادم',
|
||||
noDescription: 'لا يوجد وصف كتبه المالك.',
|
||||
managedInstalls: 'تثبيتات مُدارة',
|
||||
close: 'إغلاق',
|
||||
readEvery: 'اقرأ كل ملف خام. ترتبط الموافقة بقيم التجزئة الثلاث الدقيقة المعروضة أدناه.',
|
||||
editReview:
|
||||
'عدّل الوصف أو SKILL.md أو البيان التصريحي هنا. ينشئ الحفظ مراجعة خاصة جديدة ويعيد إجراء الفحوصات ويُرجع قيماً جديدة للتجزئة؛ ولا يعيد كتابة المهارة المصدر المحلية.',
|
||||
unsavedChanges: 'لم تُفحص هذه التغييرات. احفظها وأعد الفحص قبل الموافقة.',
|
||||
saveAndRescan: 'حفظ التغييرات وإعادة الفحص',
|
||||
savingRevision: 'جارٍ الحفظ وإعادة الفحص…',
|
||||
resetChanges: 'تجاهل التعديلات',
|
||||
reviewedHashes: 'قيم تجزئة المراجعة التي فحصها الخادم حالياً',
|
||||
prepareTitle: 'راجع الحزمة المحلية قبل الرفع',
|
||||
prepareNotice:
|
||||
'تبقى هذه الحقول في هذا الملف الشخصي حتى ترسلها صراحةً لمراجعة خادم خاصة بالمالك. لا يتم تضمين أعداد التأهيل المحلية أو أسبابه أبداً.',
|
||||
ownerDescription: 'وصف كتبه المالك',
|
||||
systemSpecification: 'مواصفات النظام (بيانات تعريفية تصريحية؛ لا يتم تثبيت أي تبعيات)',
|
||||
localOverlay: 'التراكب المحلي',
|
||||
cancel: 'إلغاء',
|
||||
submit: 'إرسال المسودة',
|
||||
submitting: 'جارٍ الإرسال…',
|
||||
publishing: 'جارٍ النشر…',
|
||||
approve: 'الموافقة على المحتوى الدقيق ونشره',
|
||||
decline: 'رفض',
|
||||
checkUpdates: (count: number) => `التحقق من التحديثات${count ? ` (${count})` : ''}`,
|
||||
checking: 'جارٍ التحقق…',
|
||||
refreshShared: 'تحديث المهارات المشتركة',
|
||||
refreshingShared: 'جارٍ التحديث…',
|
||||
installReferenceLabel: 'التثبيت من رابط أو معرّف مهارة',
|
||||
installReferencePlaceholder: 'الصق رابط Portal أو معرّف المهارة أو skill-id@vN',
|
||||
installReferenceHelp: 'يتحقق Hermes من الإصدار الدقيق ويعرض خطة التوافق قبل التثبيت.',
|
||||
reviewInstall: 'مراجعة التثبيت',
|
||||
planningInstall: 'جارٍ التحقق…',
|
||||
updateModeLabel: 'التحديثات المستقبلية',
|
||||
updateModeDefault: 'استخدام الإعداد الافتراضي للمؤسسة',
|
||||
updateModeManual: 'يدوي',
|
||||
updateModeAutomatic: 'تلقائي مع إشعار',
|
||||
updateModeRequired: 'إلزامي',
|
||||
updateModeHelp: 'تطبق Gateway سياسة مؤسستك الحالية. ولا تزال التغييرات الحساسة أمنيًا تتطلب موافقتك.',
|
||||
updateModePlan: mode => `التحديثات المستقبلية: ${mode}`,
|
||||
activityReady: (count: number) => `${count} من الإشعارات الجديدة`,
|
||||
decisionPublished: (skill: string) => `تمت الموافقة على ${skill} وأصبحت مشتركة مع فريقك.`,
|
||||
decisionChanges: (skill: string) => `تحتاج ${skill} إلى تغييرات قبل مشاركتها.`,
|
||||
decisionDeclined: (skill: string) => `لم تتم الموافقة على مشاركة ${skill}.`,
|
||||
decisionChanged: (skill: string, state: string) => `تغيّرت حالة مساهمة ${skill} إلى ${state}.`,
|
||||
installedNotice: (skill: string, version?: string) =>
|
||||
`تم تثبيت ${skill}${version ? ` ${version}` : ''} في هذا الملف الشخصي.`,
|
||||
updateNotice: (skill: string, version?: string) => `يتوفر تحديث لـ ${skill}${version ? ` ${version}` : ''}.`,
|
||||
newSkillNotice: (skill: string) => `تمت مشاركة ${skill} مع مجموعتك.`,
|
||||
archivedNotice: (skill: string) => `لم تعد ${skill} متاحة للتثبيتات الجديدة.`,
|
||||
takedownNotice: (skill: string) => `تمت إزالة ${skill} من المجموعة.`,
|
||||
markSeen: 'تعليم الكل كمقروء',
|
||||
install: 'تثبيت…',
|
||||
uninstall: 'إلغاء التثبيت…',
|
||||
checkSkill: 'التحقق من هذه المهارة',
|
||||
updateAvailable: (version?: number) => (version ? `يتوفر التحديث v${version}` : 'يتوفر تحديث'),
|
||||
reviewUpdate: 'مراجعة التحديث',
|
||||
installed: (version: number, mode: string) => `مثبتة v${version} · ${mode}`,
|
||||
confirmAction: (action: string) => `تأكيد ${action}`,
|
||||
acceptCompatibility: 'راجعت إجراءات التوافق وأوافق عليها.',
|
||||
acceptSensitive: 'أوافق صراحةً على المتطلبات الحساسة الجديدة.',
|
||||
preserveModified: 'احتفظ أولاً بنسختي المعدلة كتفرع غير مُدار.'
|
||||
},
|
||||
title: "المهارات",
|
||||
searchPlaceholder: "بحث في المهارات ومجموعات الأدوات...",
|
||||
enabledOf: "{enabled}/{total} مفعلة",
|
||||
|
||||
@@ -360,129 +360,6 @@ export const de: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Benachrichtigungseinstellungen",
|
||||
scope: "Ihre proaktiven Benachrichtigungen auf allen Clients dieser Organisation. Manuelles Durchsuchen und Teilen bleiben verfügbar.",
|
||||
on: "Benachrichtigungen an",
|
||||
muted: "Benachrichtigungen stumm",
|
||||
day: "1 Tag",
|
||||
week: "1 Woche",
|
||||
month: "30 Tage",
|
||||
forever: "Unbefristet",
|
||||
pending: "Ihre Auswahl ist lokal gespeichert und wartet auf Synchronisierung.",
|
||||
failed: "Synchronisierung fehlgeschlagen. Aktualisieren Sie die Einstellungen vor einer neuen Auswahl.",
|
||||
conflict: "Ihre Einstellung wurde auf einem anderen Client geändert. Zum Prüfen aktualisieren.",
|
||||
expired: "Diese Auswahl ist abgelaufen. Aktualisieren Sie die Einstellungen und versuchen Sie es erneut.",
|
||||
},
|
||||
tab: 'Kollektiv',
|
||||
browseHub: 'Hub durchsuchen',
|
||||
title: 'Kollektives Wissen',
|
||||
loading: 'Kollektives Wissen wird geladen',
|
||||
unavailable: 'Kollektives Wissen ist nicht verfügbar.',
|
||||
setup: 'Kollektives Wissen ist für dieses Profil nicht eingerichtet.',
|
||||
setupDisclosure:
|
||||
'Die Qualifizierung von Kandidaten bleibt in diesem Profil. Nur vom Eigentümer freigegebene private Entwurfsinhalte, Autorentext, deklarative Manifest-Metadaten und der Status verwalteter Installationen erreichen das Gateway.',
|
||||
setupAction: 'Verstanden — dieses Profil einrichten',
|
||||
settingUp: 'Einrichtung läuft…',
|
||||
scanLocal: 'Lokale Skills scannen',
|
||||
search: 'Geteilte Skills suchen…',
|
||||
potential: 'Vorgeschlagene Beiträge',
|
||||
potentialHelp:
|
||||
'Skills, die Hermes anhand lokaler Nutzung oder wesentlicher Verbesserungen qualifiziert hat. Vor deiner Prüfung wird nichts geteilt.',
|
||||
noSuggestions: 'Derzeit erfüllt kein lokaler Skill die Regeln für die automatische Qualifikation.',
|
||||
browseLocal: count => `Alle lokalen Skills anzeigen (${count})`,
|
||||
browseLocalHelp:
|
||||
'Die manuelle Auswahl bedeutet nicht, dass Hermes diese Skills verwendet oder automatisch qualifiziert hat.',
|
||||
ownerReview: 'Deine Beitragsentwürfe',
|
||||
ownerReviewHelp:
|
||||
'Entwürfe, die auf deine Prüfung warten, und Einreichungen, die auf die Freigabe des Kollektivs warten.',
|
||||
noDrafts: 'Keine aktiven Beitragsentwürfe oder Einreichungen.',
|
||||
prepare: 'Beitrag beginnen',
|
||||
continueDraft: 'Entwurf fortsetzen',
|
||||
localOnly: 'Kann von diesem Gerät geteilt werden',
|
||||
qualifiedLocally: 'Hermes hat diesen lokalen Skill als möglichen Beitrag erkannt.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Deine Organisation (${organizationName})` : 'Deine Organisation'} hat Collective Wisdom aktiviert. Diese Funktion erkennt automatisch nützliche Skills aller Teammitglieder. Glückwunsch! Hermes hat einen Skill erkannt, der für dein Team nützlich sein könnte.`,
|
||||
qualificationReturning: 'Hermes hat einen weiteren Skill erkannt, der für dein Team nützlich sein könnte.',
|
||||
savedLocally: 'Ein privater Entwurf ist auf diesem Gerät gespeichert.',
|
||||
reviewExact: 'Details anzeigen',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Serverprüfung läuft',
|
||||
ready: 'Bereit für deine Prüfung',
|
||||
owner_approved: 'Von dir freigegeben',
|
||||
publishing: 'Wird veröffentlicht',
|
||||
pending_moderation: 'Wartet auf Freigabe durch die Kollektivverwaltung',
|
||||
changes_requested: 'Änderungen angefordert'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'Serverprüfung bestanden',
|
||||
noDescription: 'Keine vom Eigentümer verfasste Beschreibung.',
|
||||
managedInstalls: 'verwaltete Installationen',
|
||||
close: 'Schließen',
|
||||
readEvery: 'Lies jede Rohdatei. Die Freigabe ist an die drei unten gezeigten exakten Hashes gebunden.',
|
||||
editReview:
|
||||
'Bearbeite hier die Beschreibung, SKILL.md oder das deklarative Manifest. Beim Speichern wird eine neue private Revision erstellt, erneut gescannt und mit neuen Hashes versehen; dein lokaler Quell-Skill wird nicht überschrieben.',
|
||||
unsavedChanges:
|
||||
'Diese Änderungen wurden noch nicht gescannt. Speichere und scanne erneut, bevor du sie freigibst.',
|
||||
saveAndRescan: 'Änderungen speichern und neu scannen',
|
||||
savingRevision: 'Speichern und erneutes Scannen…',
|
||||
resetChanges: 'Änderungen verwerfen',
|
||||
reviewedHashes: 'Hashes der aktuell servergeprüften Revision',
|
||||
prepareTitle: 'Lokales Paket vor dem Hochladen prüfen',
|
||||
prepareNotice:
|
||||
'Diese Felder bleiben in diesem Profil, bis du sie ausdrücklich zur privaten Serverprüfung durch den Eigentümer einreichst. Lokale Qualifizierungszahlen und -gründe werden nie übermittelt.',
|
||||
ownerDescription: 'Vom Eigentümer verfasste Beschreibung',
|
||||
systemSpecification: 'Systemspezifikation (deklarative Metadaten; keine Abhängigkeiten werden installiert)',
|
||||
localOverlay: 'Lokale Überlagerung',
|
||||
cancel: 'Abbrechen',
|
||||
submit: 'Entwurf einreichen',
|
||||
submitting: 'Wird eingereicht…',
|
||||
publishing: 'Wird veröffentlicht…',
|
||||
approve: 'Exakten Inhalt freigeben und veröffentlichen',
|
||||
decline: 'Ablehnen',
|
||||
checkUpdates: count => `Updates prüfen${count ? ` (${count})` : ''}`,
|
||||
checking: 'Wird geprüft…',
|
||||
refreshShared: 'Geteilte Skills aktualisieren',
|
||||
refreshingShared: 'Wird aktualisiert…',
|
||||
installReferenceLabel: 'Über Link oder Skill-ID installieren',
|
||||
installReferencePlaceholder: 'Portal-Link, Skill-ID oder skill-id@vN einfügen',
|
||||
installReferenceHelp: 'Hermes prüft die genaue Version und zeigt vor der Installation einen Kompatibilitätsplan.',
|
||||
reviewInstall: 'Installation prüfen',
|
||||
planningInstall: 'Wird verifiziert…',
|
||||
updateModeLabel: 'Zukünftige Updates',
|
||||
updateModeDefault: 'Organisationsstandard verwenden',
|
||||
updateModeManual: 'Manuell',
|
||||
updateModeAutomatic: 'Automatisch mit Benachrichtigung',
|
||||
updateModeRequired: 'Erforderlich',
|
||||
updateModeHelp:
|
||||
'Gateway wendet die aktuelle Richtlinie Ihrer Organisation an. Sicherheitsrelevante Änderungen erfordern weiterhin Ihre Zustimmung.',
|
||||
updateModePlan: mode => `Zukünftige Updates: ${mode}`,
|
||||
activityReady: count => `${count} neue ${count === 1 ? 'Benachrichtigung' : 'Benachrichtigungen'}`,
|
||||
decisionPublished: skill => `${skill} wurde freigegeben und wird jetzt mit deinem Team geteilt.`,
|
||||
decisionChanges: skill => `${skill} muss vor dem Teilen geändert werden.`,
|
||||
decisionDeclined: skill => `${skill} wurde nicht zum Teilen freigegeben.`,
|
||||
decisionChanged: (skill, state) => `Der Beitragsstatus von ${skill} wurde in ${state} geändert.`,
|
||||
installedNotice: (skill, version) =>
|
||||
`${skill}${version ? ` ${version}` : ''} wurde in diesem Profil installiert.`,
|
||||
updateNotice: (skill, version) => `Für ${skill}${version ? ` ${version}` : ''} ist ein Update verfügbar.`,
|
||||
newSkillNotice: skill => `${skill} wurde mit deinem Kollektiv geteilt.`,
|
||||
archivedNotice: skill => `${skill} ist für neue Installationen nicht mehr verfügbar.`,
|
||||
takedownNotice: skill => `${skill} wurde aus dem Kollektiv entfernt.`,
|
||||
markSeen: 'Alle als gelesen markieren',
|
||||
install: 'Installieren…',
|
||||
uninstall: 'Deinstallieren…',
|
||||
checkSkill: 'Diesen Skill prüfen',
|
||||
updateAvailable: version => (version ? `Update v${version} verfügbar` : 'Update verfügbar'),
|
||||
reviewUpdate: 'Update prüfen',
|
||||
installed: (version, mode) => `Installiert v${version} · ${mode}`,
|
||||
confirmAction: action => `${action} bestätigen`,
|
||||
acceptCompatibility: 'Ich habe die Kompatibilitätsmaßnahmen geprüft und akzeptiere sie.',
|
||||
acceptSensitive: 'Ich akzeptiere die neuen sensiblen Anforderungen ausdrücklich.',
|
||||
preserveModified: 'Meine geänderte Kopie zuerst als nicht verwalteten Fork aufbewahren.'
|
||||
},
|
||||
title: "Skills",
|
||||
searchPlaceholder: "Skills und Toolsets suchen...",
|
||||
enabledOf: "{enabled}/{total} aktiviert",
|
||||
|
||||
@@ -436,125 +436,6 @@ export const en: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Notification settings",
|
||||
scope: "Your proactive notifications across clients in this organization. Manual browsing and sharing remain available.",
|
||||
on: "Notifications on",
|
||||
muted: "Notifications muted",
|
||||
day: "1 day",
|
||||
week: "1 week",
|
||||
month: "30 days",
|
||||
forever: "Indefinitely",
|
||||
pending: "Your choice is saved locally and waiting to sync.",
|
||||
failed: "Sync failed. Refresh settings before choosing again.",
|
||||
conflict: "Your preference changed on another client. Refresh to review it.",
|
||||
expired: "This choice expired. Refresh settings to try again.",
|
||||
},
|
||||
tab: 'Collective',
|
||||
browseHub: 'Browse hub',
|
||||
title: 'Collective Wisdom',
|
||||
loading: 'Loading Collective Wisdom',
|
||||
unavailable: 'Collective Wisdom is unavailable.',
|
||||
setup: 'Collective Wisdom is not set up for this profile.',
|
||||
setupDisclosure:
|
||||
'Candidate qualification stays on this profile. Only owner-approved private draft bytes, author copy, declarative manifest metadata, and managed-install state reach the Gateway.',
|
||||
setupAction: 'I understand — set up this profile',
|
||||
settingUp: 'Setting up…',
|
||||
scanLocal: 'Scan local skills',
|
||||
search: 'Search shared skills…',
|
||||
potential: 'Suggested contributions',
|
||||
potentialHelp:
|
||||
'Skills Hermes qualified locally from use or meaningful refinement. Nothing is shared until you review it.',
|
||||
noSuggestions: 'No local skills currently meet the automatic qualification rules.',
|
||||
browseLocal: count => `View all local skills (${count})`,
|
||||
browseLocalHelp: 'Manual selection does not mean Hermes used or automatically qualified these skills.',
|
||||
ownerReview: 'Your contribution drafts',
|
||||
ownerReviewHelp: 'Drafts awaiting your review and submissions waiting for collective approval.',
|
||||
noDrafts: 'No active contribution drafts or submissions.',
|
||||
prepare: 'Start contribution',
|
||||
continueDraft: 'Continue draft',
|
||||
localOnly: 'Available to share from this device',
|
||||
qualifiedLocally: 'Hermes identified this local skill as a possible contribution.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Your organization (${organizationName})` : 'Your organization'} has enabled Collective Wisdom, a feature designed to automatically detect and share useful skills across all team members. Congratulations! Hermes detected a skill you created that could be useful to your team!`,
|
||||
qualificationReturning: 'Hermes detected another skill you created that could be useful to your team!',
|
||||
savedLocally: 'A private draft is saved on this device.',
|
||||
reviewExact: 'View details',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Server review in progress',
|
||||
ready: 'Ready for your review',
|
||||
owner_approved: 'Approved by you',
|
||||
publishing: 'Publishing',
|
||||
pending_moderation: 'Waiting for collective administrator approval',
|
||||
changes_requested: 'Changes requested'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'server scan passed',
|
||||
noDescription: 'No owner-authored description.',
|
||||
managedInstalls: 'managed installs',
|
||||
close: 'Close',
|
||||
readEvery: 'Read every raw file. Approval is bound to the exact three hashes shown below.',
|
||||
editReview:
|
||||
'Edit the description, SKILL.md, or declarative manifest here. Saving creates a new private revision, reruns scans, and returns new hashes; it does not rewrite your local source skill.',
|
||||
unsavedChanges: 'These changes have not been scanned. Save and rescan before approving.',
|
||||
saveAndRescan: 'Save changes & rescan',
|
||||
savingRevision: 'Saving & rescanning…',
|
||||
resetChanges: 'Discard edits',
|
||||
reviewedHashes: 'Hashes for the currently server-reviewed revision',
|
||||
prepareTitle: 'Review local package before upload',
|
||||
prepareNotice:
|
||||
'These fields stay on this profile until you explicitly submit them for owner-only server review. Local qualification counts and reasons are never included.',
|
||||
ownerDescription: 'Owner-authored description',
|
||||
systemSpecification: 'System Specification (declarative metadata; no dependencies are installed)',
|
||||
localOverlay: 'Local overlay',
|
||||
cancel: 'Cancel',
|
||||
submit: 'Submit draft',
|
||||
submitting: 'Submitting…',
|
||||
publishing: 'Publishing…',
|
||||
approve: 'Approve exact content & publish',
|
||||
decline: 'Decline',
|
||||
checkUpdates: count => `Check updates${count ? ` (${count})` : ''}`,
|
||||
checking: 'Checking…',
|
||||
refreshShared: 'Refresh shared skills',
|
||||
refreshingShared: 'Refreshing…',
|
||||
installReferenceLabel: 'Install from link or skill ID',
|
||||
installReferencePlaceholder: 'Paste a Portal link, skill ID, or skill-id@vN',
|
||||
installReferenceHelp: 'Hermes verifies the exact version and shows a compatibility plan before installing.',
|
||||
reviewInstall: 'Review install',
|
||||
planningInstall: 'Verifying…',
|
||||
updateModeLabel: 'Future updates',
|
||||
updateModeDefault: 'Use organization default',
|
||||
updateModeManual: 'Manual',
|
||||
updateModeAutomatic: 'Automatic with notice',
|
||||
updateModeRequired: 'Required',
|
||||
updateModeHelp:
|
||||
"Gateway applies your organization's current policy. Safety-sensitive changes still require your approval.",
|
||||
updateModePlan: mode => `Future updates: ${mode}`,
|
||||
activityReady: count => `${count} new ${count === 1 ? 'notification' : 'notifications'}`,
|
||||
decisionPublished: skill => `${skill} was approved and is now shared with your team.`,
|
||||
decisionChanges: skill => `${skill} needs changes before it can be shared.`,
|
||||
decisionDeclined: skill => `${skill} was not approved for sharing.`,
|
||||
decisionChanged: (skill, state) => `${skill} contribution status changed to ${state}.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} was installed on this profile.`,
|
||||
updateNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} is available to update.`,
|
||||
newSkillNotice: skill => `${skill} was shared with your collective.`,
|
||||
archivedNotice: skill => `${skill} is no longer available for new installs.`,
|
||||
takedownNotice: skill => `${skill} was removed from the collective.`,
|
||||
markSeen: 'Mark all read',
|
||||
install: 'Install…',
|
||||
uninstall: 'Uninstall…',
|
||||
checkSkill: 'Check this skill',
|
||||
updateAvailable: version => (version ? `v${version} update available` : 'Update available'),
|
||||
reviewUpdate: 'Review update',
|
||||
installed: (version, mode) => `Installed v${version} · ${mode}`,
|
||||
confirmAction: action => `Confirm ${action}`,
|
||||
acceptCompatibility: 'I reviewed and accept the compatibility actions.',
|
||||
acceptSensitive: 'I explicitly accept the new sensitive requirements.',
|
||||
preserveModified: 'Preserve my modified copy as an unmanaged fork first.'
|
||||
},
|
||||
title: "Skills",
|
||||
searchPlaceholder: "Search skills and toolsets...",
|
||||
enabledOf: "{enabled}/{total} enabled",
|
||||
|
||||
@@ -361,128 +361,6 @@ export const es: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Ajustes de notificaciones",
|
||||
scope: "Tus notificaciones proactivas en los clientes de esta organización. La consulta y el uso compartido manual siguen disponibles.",
|
||||
on: "Notificaciones activadas",
|
||||
muted: "Notificaciones silenciadas",
|
||||
day: "1 día",
|
||||
week: "1 semana",
|
||||
month: "30 días",
|
||||
forever: "Indefinidamente",
|
||||
pending: "Tu elección se guardó localmente y está pendiente de sincronización.",
|
||||
failed: "Error de sincronización. Actualiza los ajustes antes de volver a elegir.",
|
||||
conflict: "Tu preferencia cambió en otro cliente. Actualiza para revisarla.",
|
||||
expired: "Esta elección caducó. Actualiza los ajustes para reintentarlo.",
|
||||
},
|
||||
tab: 'Colectivo',
|
||||
browseHub: 'Explorar Hub',
|
||||
title: 'Sabiduría colectiva',
|
||||
loading: 'Cargando Sabiduría colectiva',
|
||||
unavailable: 'Sabiduría colectiva no está disponible.',
|
||||
setup: 'Sabiduría colectiva no está configurada para este perfil.',
|
||||
setupDisclosure:
|
||||
'La calificación de candidatos permanece en este perfil. Solo llegan al Gateway el contenido del borrador privado aprobado por su propietario, el texto del autor, los metadatos declarativos del manifiesto y el estado de las instalaciones gestionadas.',
|
||||
setupAction: 'Entiendo — configurar este perfil',
|
||||
settingUp: 'Configurando…',
|
||||
scanLocal: 'Analizar habilidades locales',
|
||||
search: 'Buscar habilidades compartidas…',
|
||||
potential: 'Contribuciones sugeridas',
|
||||
potentialHelp:
|
||||
'Habilidades que Hermes ha calificado localmente por su uso o mejoras significativas. No se comparte nada hasta que lo revises.',
|
||||
noSuggestions: 'Actualmente ninguna habilidad local cumple las reglas de calificación automática.',
|
||||
browseLocal: count => `Ver todas las habilidades locales (${count})`,
|
||||
browseLocalHelp:
|
||||
'La selección manual no significa que Hermes haya usado o calificado automáticamente estas habilidades.',
|
||||
ownerReview: 'Tus borradores de contribución',
|
||||
ownerReviewHelp: 'Borradores pendientes de tu revisión y envíos pendientes de aprobación colectiva.',
|
||||
noDrafts: 'No hay borradores ni envíos de contribución activos.',
|
||||
prepare: 'Iniciar contribución',
|
||||
continueDraft: 'Continuar borrador',
|
||||
localOnly: 'Disponible para compartir desde este dispositivo',
|
||||
qualifiedLocally: 'Hermes identificó esta habilidad local como una posible contribución.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Tu organización (${organizationName})` : 'Tu organización'} ha activado Collective Wisdom, una función diseñada para detectar automáticamente habilidades útiles entre todos los miembros del equipo. ¡Enhorabuena! Hermes ha detectado una habilidad que podría ser útil para tu equipo.`,
|
||||
qualificationReturning: 'Hermes ha detectado otra habilidad que podría ser útil para tu equipo.',
|
||||
savedLocally: 'Hay un borrador privado guardado en este dispositivo.',
|
||||
reviewExact: 'Ver detalles',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Revisión del servidor en curso',
|
||||
ready: 'Lista para tu revisión',
|
||||
owner_approved: 'Aprobada por ti',
|
||||
publishing: 'Publicando',
|
||||
pending_moderation: 'Esperando la aprobación del administrador del colectivo',
|
||||
changes_requested: 'Cambios solicitados'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'análisis del servidor superado',
|
||||
noDescription: 'No hay descripción escrita por el propietario.',
|
||||
managedInstalls: 'instalaciones gestionadas',
|
||||
close: 'Cerrar',
|
||||
readEvery:
|
||||
'Lee cada archivo sin procesar. La aprobación queda vinculada a los tres hashes exactos que aparecen abajo.',
|
||||
editReview:
|
||||
'Edita aquí la descripción, SKILL.md o el manifiesto declarativo. Al guardar se crea una nueva revisión privada, se repiten los análisis y se generan nuevos hashes; la habilidad local original no se modifica.',
|
||||
unsavedChanges: 'Estos cambios aún no se han analizado. Guarda y vuelve a analizar antes de aprobar.',
|
||||
saveAndRescan: 'Guardar cambios y volver a analizar',
|
||||
savingRevision: 'Guardando y volviendo a analizar…',
|
||||
resetChanges: 'Descartar cambios',
|
||||
reviewedHashes: 'Hashes de la revisión analizada actualmente por el servidor',
|
||||
prepareTitle: 'Revisar el paquete local antes de subirlo',
|
||||
prepareNotice:
|
||||
'Estos campos permanecen en este perfil hasta que los envíes expresamente a una revisión privada del servidor para el propietario. Nunca se incluyen los recuentos ni los motivos de calificación locales.',
|
||||
ownerDescription: 'Descripción escrita por el propietario',
|
||||
systemSpecification: 'Especificación del sistema (metadatos declarativos; no se instala ninguna dependencia)',
|
||||
localOverlay: 'Capa local',
|
||||
cancel: 'Cancelar',
|
||||
submit: 'Enviar borrador',
|
||||
submitting: 'Enviando…',
|
||||
publishing: 'Publicando…',
|
||||
approve: 'Aprobar el contenido exacto y publicar',
|
||||
decline: 'Rechazar',
|
||||
checkUpdates: count => `Buscar actualizaciones${count ? ` (${count})` : ''}`,
|
||||
checking: 'Comprobando…',
|
||||
refreshShared: 'Actualizar habilidades compartidas',
|
||||
refreshingShared: 'Actualizando…',
|
||||
installReferenceLabel: 'Instalar desde un enlace o ID de habilidad',
|
||||
installReferencePlaceholder: 'Pega un enlace del Portal, un ID de habilidad o skill-id@vN',
|
||||
installReferenceHelp: 'Hermes verifica la versión exacta y muestra un plan de compatibilidad antes de instalar.',
|
||||
reviewInstall: 'Revisar instalación',
|
||||
planningInstall: 'Verificando…',
|
||||
updateModeLabel: 'Actualizaciones futuras',
|
||||
updateModeDefault: 'Usar el valor predeterminado de la organización',
|
||||
updateModeManual: 'Manual',
|
||||
updateModeAutomatic: 'Automática con aviso',
|
||||
updateModeRequired: 'Obligatoria',
|
||||
updateModeHelp:
|
||||
'Gateway aplica la política actual de tu organización. Los cambios sensibles para la seguridad siguen requiriendo tu aprobación.',
|
||||
updateModePlan: mode => `Actualizaciones futuras: ${mode}`,
|
||||
activityReady: count => `${count} ${count === 1 ? 'notificación nueva' : 'notificaciones nuevas'}`,
|
||||
decisionPublished: skill => `${skill} fue aprobada y ahora se comparte con tu equipo.`,
|
||||
decisionChanges: skill => `${skill} necesita cambios antes de poder compartirse.`,
|
||||
decisionDeclined: skill => `${skill} no fue aprobada para compartir.`,
|
||||
decisionChanged: (skill, state) => `El estado de la contribución ${skill} cambió a ${state}.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} se instaló en este perfil.`,
|
||||
updateNotice: (skill, version) =>
|
||||
`Hay una actualización disponible para ${skill}${version ? ` ${version}` : ''}.`,
|
||||
newSkillNotice: skill => `${skill} se compartió con tu colectivo.`,
|
||||
archivedNotice: skill => `${skill} ya no está disponible para nuevas instalaciones.`,
|
||||
takedownNotice: skill => `${skill} se eliminó del colectivo.`,
|
||||
markSeen: 'Marcar todo como leído',
|
||||
install: 'Instalar…',
|
||||
uninstall: 'Desinstalar…',
|
||||
checkSkill: 'Comprobar esta habilidad',
|
||||
updateAvailable: version => (version ? `Actualización v${version} disponible` : 'Actualización disponible'),
|
||||
reviewUpdate: 'Revisar actualización',
|
||||
installed: (version, mode) => `Instalada v${version} · ${mode}`,
|
||||
confirmAction: action => `Confirmar ${action}`,
|
||||
acceptCompatibility: 'He revisado y acepto las acciones de compatibilidad.',
|
||||
acceptSensitive: 'Acepto expresamente los nuevos requisitos sensibles.',
|
||||
preserveModified: 'Conservar primero mi copia modificada como una bifurcación no gestionada.'
|
||||
},
|
||||
title: "Habilidades",
|
||||
searchPlaceholder: "Buscar habilidades y conjuntos de herramientas...",
|
||||
enabledOf: "{enabled}/{total} habilitados",
|
||||
|
||||
@@ -361,127 +361,6 @@ export const fr: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Paramètres des notifications",
|
||||
scope: "Vos notifications proactives sur les clients de cette organisation. La consultation et le partage manuels restent disponibles.",
|
||||
on: "Notifications activées",
|
||||
muted: "Notifications en sourdine",
|
||||
day: "1 jour",
|
||||
week: "1 semaine",
|
||||
month: "30 jours",
|
||||
forever: "Indéfiniment",
|
||||
pending: "Votre choix est enregistré localement et attend la synchronisation.",
|
||||
failed: "Échec de la synchronisation. Actualisez les paramètres avant de choisir à nouveau.",
|
||||
conflict: "Votre préférence a changé sur un autre client. Actualisez pour la vérifier.",
|
||||
expired: "Ce choix a expiré. Actualisez les paramètres pour réessayer.",
|
||||
},
|
||||
tab: 'Collectif',
|
||||
browseHub: 'Parcourir le Hub',
|
||||
title: 'Savoir collectif',
|
||||
loading: 'Chargement du Savoir collectif',
|
||||
unavailable: 'Le Savoir collectif est indisponible.',
|
||||
setup: 'Le Savoir collectif n’est pas configuré pour ce profil.',
|
||||
setupDisclosure:
|
||||
'La qualification des candidats reste dans ce profil. Seuls le contenu du brouillon privé approuvé par son propriétaire, le texte de l’auteur, les métadonnées déclaratives du manifeste et l’état des installations gérées parviennent à la Gateway.',
|
||||
setupAction: 'J’ai compris — configurer ce profil',
|
||||
settingUp: 'Configuration…',
|
||||
scanLocal: 'Analyser les compétences locales',
|
||||
search: 'Rechercher des compétences partagées…',
|
||||
potential: 'Contributions suggérées',
|
||||
potentialHelp:
|
||||
'Compétences qualifiées localement par Hermes selon leur usage ou des améliorations significatives. Rien n’est partagé avant votre examen.',
|
||||
noSuggestions: 'Aucune compétence locale ne satisfait actuellement les règles de qualification automatique.',
|
||||
browseLocal: count => `Voir toutes les compétences locales (${count})`,
|
||||
browseLocalHelp:
|
||||
'La sélection manuelle ne signifie pas que Hermes a utilisé ou qualifié automatiquement ces compétences.',
|
||||
ownerReview: 'Vos brouillons de contribution',
|
||||
ownerReviewHelp: 'Brouillons en attente de votre examen et soumissions en attente de l’approbation du collectif.',
|
||||
noDrafts: 'Aucun brouillon ni aucune soumission de contribution en cours.',
|
||||
prepare: 'Commencer la contribution',
|
||||
continueDraft: 'Continuer le brouillon',
|
||||
localOnly: 'Peut être partagé depuis cet appareil',
|
||||
qualifiedLocally: 'Hermes a identifié cette compétence locale comme une contribution possible.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Votre organisation (${organizationName})` : 'Votre organisation'} a activé Collective Wisdom, une fonctionnalité conçue pour détecter automatiquement les compétences utiles de tous les membres de l’équipe. Félicitations ! Hermes a détecté une compétence qui pourrait être utile à votre équipe.`,
|
||||
qualificationReturning: 'Hermes a détecté une autre compétence qui pourrait être utile à votre équipe.',
|
||||
savedLocally: 'Un brouillon privé est enregistré sur cet appareil.',
|
||||
reviewExact: 'Voir les détails',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Vérification du serveur en cours',
|
||||
ready: 'Prêt pour votre examen',
|
||||
owner_approved: 'Approuvé par vous',
|
||||
publishing: 'Publication en cours',
|
||||
pending_moderation: 'En attente de l’approbation de l’administrateur du collectif',
|
||||
changes_requested: 'Modifications demandées'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'analyse serveur réussie',
|
||||
noDescription: 'Aucune description rédigée par le propriétaire.',
|
||||
managedInstalls: 'installations gérées',
|
||||
close: 'Fermer',
|
||||
readEvery: 'Lisez chaque fichier brut. L’approbation est liée aux trois hachages exacts affichés ci-dessous.',
|
||||
editReview:
|
||||
'Modifiez ici la description, SKILL.md ou le manifeste déclaratif. L’enregistrement crée une nouvelle révision privée, relance les analyses et renvoie de nouveaux hachages ; il ne réécrit pas votre compétence locale source.',
|
||||
unsavedChanges: 'Ces modifications n’ont pas été analysées. Enregistrez et relancez l’analyse avant d’approuver.',
|
||||
saveAndRescan: 'Enregistrer et relancer l’analyse',
|
||||
savingRevision: 'Enregistrement et nouvelle analyse…',
|
||||
resetChanges: 'Abandonner les modifications',
|
||||
reviewedHashes: 'Hachages de la révision actuellement examinée par le serveur',
|
||||
prepareTitle: 'Examiner le paquet local avant l’envoi',
|
||||
prepareNotice:
|
||||
'Ces champs restent dans ce profil jusqu’à ce que vous les soumettiez explicitement à un examen serveur privé réservé au propriétaire. Les compteurs et motifs de qualification locaux ne sont jamais inclus.',
|
||||
ownerDescription: 'Description rédigée par le propriétaire',
|
||||
systemSpecification: 'Spécification système (métadonnées déclaratives ; aucune dépendance n’est installée)',
|
||||
localOverlay: 'Superposition locale',
|
||||
cancel: 'Annuler',
|
||||
submit: 'Soumettre le brouillon',
|
||||
submitting: 'Envoi…',
|
||||
publishing: 'Publication…',
|
||||
approve: 'Approuver le contenu exact et publier',
|
||||
decline: 'Refuser',
|
||||
checkUpdates: count => `Rechercher des mises à jour${count ? ` (${count})` : ''}`,
|
||||
checking: 'Vérification…',
|
||||
refreshShared: 'Actualiser les compétences partagées',
|
||||
refreshingShared: 'Actualisation…',
|
||||
installReferenceLabel: 'Installer depuis un lien ou un ID de compétence',
|
||||
installReferencePlaceholder: 'Collez un lien du portail, un ID de compétence ou skill-id@vN',
|
||||
installReferenceHelp:
|
||||
'Hermes vérifie la version exacte et affiche un plan de compatibilité avant l’installation.',
|
||||
reviewInstall: 'Vérifier l’installation',
|
||||
planningInstall: 'Validation…',
|
||||
updateModeLabel: 'Mises à jour futures',
|
||||
updateModeDefault: 'Utiliser la valeur par défaut de l’organisation',
|
||||
updateModeManual: 'Manuelles',
|
||||
updateModeAutomatic: 'Automatiques avec notification',
|
||||
updateModeRequired: 'Obligatoires',
|
||||
updateModeHelp:
|
||||
'Gateway applique la politique actuelle de votre organisation. Les changements sensibles restent soumis à votre approbation.',
|
||||
updateModePlan: mode => `Mises à jour futures : ${mode}`,
|
||||
activityReady: count => `${count} nouvelle${count === 1 ? ' notification' : 's notifications'}`,
|
||||
decisionPublished: skill => `${skill} a été approuvée et est maintenant partagée avec votre équipe.`,
|
||||
decisionChanges: skill => `${skill} doit être modifiée avant de pouvoir être partagée.`,
|
||||
decisionDeclined: skill => `${skill} n’a pas été approuvée pour le partage.`,
|
||||
decisionChanged: (skill, state) => `Le statut de la contribution ${skill} est passé à ${state}.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} a été installée dans ce profil.`,
|
||||
updateNotice: (skill, version) => `Une mise à jour est disponible pour ${skill}${version ? ` ${version}` : ''}.`,
|
||||
newSkillNotice: skill => `${skill} a été partagée avec votre collectif.`,
|
||||
archivedNotice: skill => `${skill} n’est plus disponible pour de nouvelles installations.`,
|
||||
takedownNotice: skill => `${skill} a été retirée du collectif.`,
|
||||
markSeen: 'Tout marquer comme lu',
|
||||
install: 'Installer…',
|
||||
uninstall: 'Désinstaller…',
|
||||
checkSkill: 'Vérifier cette compétence',
|
||||
updateAvailable: version => (version ? `Mise à jour v${version} disponible` : 'Mise à jour disponible'),
|
||||
reviewUpdate: 'Vérifier la mise à jour',
|
||||
installed: (version, mode) => `Installée v${version} · ${mode}`,
|
||||
confirmAction: action => `Confirmer ${action}`,
|
||||
acceptCompatibility: 'J’ai examiné et j’accepte les actions de compatibilité.',
|
||||
acceptSensitive: 'J’accepte explicitement les nouvelles exigences sensibles.',
|
||||
preserveModified: 'Conserver d’abord ma copie modifiée comme fork non géré.'
|
||||
},
|
||||
title: "Compétences",
|
||||
searchPlaceholder: "Rechercher des compétences et des outils...",
|
||||
enabledOf: "{enabled}/{total} activées",
|
||||
|
||||
@@ -368,128 +368,6 @@ export const ga: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Socruithe fógraí",
|
||||
scope: "Do chuid fógraí réamhghníomhacha ar chliaint na heagraíochta seo. Tá brabhsáil agus comhroinnt de láimh ar fáil fós.",
|
||||
on: "Fógraí ar siúl",
|
||||
muted: "Fógraí balbhaithe",
|
||||
day: "1 lá",
|
||||
week: "1 seachtain",
|
||||
month: "30 lá",
|
||||
forever: "Go ceann tréimhse éiginnte",
|
||||
pending: "Sábháladh do rogha go háitiúil agus tá sí ag fanacht le sioncronú.",
|
||||
failed: "Theip ar an sioncronú. Athnuaigh na socruithe sula roghnaíonn tú arís.",
|
||||
conflict: "Athraíodh do rogha ar chliant eile. Athnuaigh chun í a sheiceáil.",
|
||||
expired: "Tá an rogha seo imithe in éag. Athnuaigh na socruithe chun triail eile a bhaint as.",
|
||||
},
|
||||
tab: 'Comhchoiteann',
|
||||
browseHub: 'Brabhsáil an Mol',
|
||||
title: 'Eagna Chomhchoiteann',
|
||||
loading: 'Eagna Chomhchoiteann á lódáil',
|
||||
unavailable: 'Níl Eagna Chomhchoiteann ar fáil.',
|
||||
setup: 'Níl Eagna Chomhchoiteann socraithe don phróifíl seo.',
|
||||
setupDisclosure:
|
||||
'Fanann cáiliú iarrthóirí ar an bpróifíl seo. Ní shroicheann an Gateway ach ábhar dréachta príobháidí atá ceadaithe ag an úinéir, cóip an údair, meiteashonraí dearbhaitheacha an fhorógra agus staid na suiteálacha bainistithe.',
|
||||
setupAction: 'Tuigim — socraigh an phróifíl seo',
|
||||
settingUp: 'Á shocrú…',
|
||||
scanLocal: 'Scan scileanna áitiúla',
|
||||
search: 'Cuardaigh scileanna comhroinnte…',
|
||||
potential: 'Ranníocaíochtaí molta',
|
||||
potentialHelp:
|
||||
'Scileanna a cháiligh Hermes go háitiúil ó úsáid nó ó mhionchoigeartú suntasach. Ní roinntear aon rud go dtí go ndéanann tú athbhreithniú air.',
|
||||
noSuggestions: 'Níl aon scileanna áitiúla ann faoi láthair a chomhlíonann na rialacha cáilithe uathoibríocha.',
|
||||
browseLocal: count => `Féach ar gach scil áitiúil (${count})`,
|
||||
browseLocalHelp:
|
||||
'Ní chiallaíonn roghnú láimhe gur úsáid Hermes na scileanna seo nó gur cháiligh sé iad go huathoibríoch.',
|
||||
ownerReview: 'Do dhréachtaí ranníocaíochta',
|
||||
ownerReviewHelp:
|
||||
'Dréachtaí ag fanacht le d’athbhreithniú agus aighneachtaí ag fanacht le ceadú an chomhchoitinn.',
|
||||
noDrafts: 'Níl aon dréachtaí ná aighneachtaí ranníocaíochta gníomhacha ann.',
|
||||
prepare: 'Tosaigh ranníocaíocht',
|
||||
continueDraft: 'Lean leis an dréacht',
|
||||
localOnly: 'Ar fáil le roinnt ón ngléas seo',
|
||||
qualifiedLocally: 'D’aithin Hermes an scil áitiúil seo mar ranníocaíocht fhéideartha.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Chumasaigh d’eagraíocht (${organizationName})` : 'Chumasaigh d’eagraíocht'} Collective Wisdom, gné a aimsíonn scileanna úsáideacha go huathoibríoch i measc bhaill uile na foirne. Comhghairdeas! D’aimsigh Hermes scil a d’fhéadfadh a bheith úsáideach do d’fhoireann.`,
|
||||
qualificationReturning: 'D’aimsigh Hermes scil eile a d’fhéadfadh a bheith úsáideach do d’fhoireann.',
|
||||
savedLocally: 'Tá dréacht príobháideach sábháilte ar an ngléas seo.',
|
||||
reviewExact: 'Féach ar na sonraí',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Athbhreithniú an fhreastalaí ar siúl',
|
||||
ready: 'Réidh le haghaidh d’athbhreithnithe',
|
||||
owner_approved: 'Ceadaithe agat',
|
||||
publishing: 'Á fhoilsiú',
|
||||
pending_moderation: 'Ag fanacht le ceadú ó riarthóir an chomhchoitinn',
|
||||
changes_requested: 'Athruithe iarrtha'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'd’éirigh le scanadh an fhreastalaí',
|
||||
noDescription: 'Níl cur síos scríofa ag an úinéir.',
|
||||
managedInstalls: 'suiteálacha bainistithe',
|
||||
close: 'Dún',
|
||||
readEvery: 'Léigh gach comhad amh. Tá an ceadú ceangailte leis na trí hais bheachta thíos.',
|
||||
editReview:
|
||||
'Cuir an cur síos, SKILL.md nó an forógra dearbhaitheach in eagar anseo. Cruthaíonn sábháil leasú príobháideach nua, ritheann sé na scantaí arís agus tugann sé hais nua ar ais; ní athscríobhann sé do bhunscil áitiúil.',
|
||||
unsavedChanges: 'Níor scanadh na hathruithe seo. Sábháil agus scan arís sula gceadaíonn tú.',
|
||||
saveAndRescan: 'Sábháil athruithe agus scan arís',
|
||||
savingRevision: 'Á shábháil agus á scanadh arís…',
|
||||
resetChanges: 'Caith na hathruithe amach',
|
||||
reviewedHashes: 'Hais an leasaithe atá athbhreithnithe ag an bhfreastalaí faoi láthair',
|
||||
prepareTitle: 'Athbhreithnigh an pacáiste áitiúil roimh uaslódáil',
|
||||
prepareNotice:
|
||||
'Fanann na réimsí seo ar an bpróifíl seo go dtí go gcuireann tú isteach iad go sainráite le haghaidh athbhreithniú príobháideach freastalaí don úinéir. Ní chuirtear comhairimh ná cúiseanna cáiliúcháin áitiúla san áireamh riamh.',
|
||||
ownerDescription: 'Cur síos scríofa ag an úinéir',
|
||||
systemSpecification: 'Sonraíocht Córais (meiteashonraí dearbhaitheacha; ní shuiteáiltear spleáchais)',
|
||||
localOverlay: 'Forleagan áitiúil',
|
||||
cancel: 'Cealaigh',
|
||||
submit: 'Cuir dréacht isteach',
|
||||
submitting: 'Á chur isteach…',
|
||||
publishing: 'Á fhoilsiú…',
|
||||
approve: 'Ceadaigh an t-ábhar beacht agus foilsigh',
|
||||
decline: 'Diúltaigh',
|
||||
checkUpdates: count => `Seiceáil nuashonruithe${count ? ` (${count})` : ''}`,
|
||||
checking: 'Á sheiceáil…',
|
||||
refreshShared: 'Athnuaigh scileanna comhroinnte',
|
||||
refreshingShared: 'Á athnuachan…',
|
||||
installReferenceLabel: 'Suiteáil ó nasc nó ó ID scile',
|
||||
installReferencePlaceholder: 'Greamaigh nasc Portal, ID scile nó skill-id@vN',
|
||||
installReferenceHelp:
|
||||
'Fíoraíonn Hermes an leagan cruinn agus taispeánann sé plean comhoiriúnachta roimh shuiteáil.',
|
||||
reviewInstall: 'Athbhreithnigh an tsuiteáil',
|
||||
planningInstall: 'Á fhíorú…',
|
||||
updateModeLabel: 'Nuashonruithe amach anseo',
|
||||
updateModeDefault: 'Úsáid réamhshocrú na heagraíochta',
|
||||
updateModeManual: 'De láimh',
|
||||
updateModeAutomatic: 'Uathoibríoch le fógra',
|
||||
updateModeRequired: 'Riachtanach',
|
||||
updateModeHelp:
|
||||
'Cuireann Gateway polasaí reatha d’eagraíochta i bhfeidhm. Teastaíonn do chead fós le haghaidh athruithe slándála-íogaire.',
|
||||
updateModePlan: mode => `Nuashonruithe amach anseo: ${mode}`,
|
||||
activityReady: count => `${count} fógra nua`,
|
||||
decisionPublished: skill => `Ceadaíodh ${skill} agus tá sé roinnte le d’fhoireann anois.`,
|
||||
decisionChanges: skill => `Teastaíonn athruithe ar ${skill} sular féidir é a roinnt.`,
|
||||
decisionDeclined: skill => `Níor ceadaíodh ${skill} lena roinnt.`,
|
||||
decisionChanged: (skill, state) => `Athraíodh stádas ranníocaíochta ${skill} go ${state}.`,
|
||||
installedNotice: (skill, version) => `Suiteáladh ${skill}${version ? ` ${version}` : ''} ar an bpróifíl seo.`,
|
||||
updateNotice: (skill, version) => `Tá nuashonrú ar fáil do ${skill}${version ? ` ${version}` : ''}.`,
|
||||
newSkillNotice: skill => `Roinneadh ${skill} le do chomhchoiteann.`,
|
||||
archivedNotice: skill => `Níl ${skill} ar fáil do shuiteálacha nua a thuilleadh.`,
|
||||
takedownNotice: skill => `Baineadh ${skill} den chomhchoiteann.`,
|
||||
markSeen: 'Marcáil gach ceann léite',
|
||||
install: 'Suiteáil…',
|
||||
uninstall: 'Díshuiteáil…',
|
||||
checkSkill: 'Seiceáil an scil seo',
|
||||
updateAvailable: version => (version ? `Nuashonrú v${version} ar fáil` : 'Nuashonrú ar fáil'),
|
||||
reviewUpdate: 'Athbhreithnigh an nuashonrú',
|
||||
installed: (version, mode) => `Suiteáilte v${version} · ${mode}`,
|
||||
confirmAction: action => `Deimhnigh ${action}`,
|
||||
acceptCompatibility: 'D’athbhreithnigh mé na gníomhartha comhoiriúnachta agus glacaim leo.',
|
||||
acceptSensitive: 'Glacaim go sainráite leis na riachtanais íogaire nua.',
|
||||
preserveModified: 'Caomhnaigh mo chóip leasaithe mar fhorc neamhbhainistithe ar dtús.'
|
||||
},
|
||||
title: "Scileanna",
|
||||
searchPlaceholder: "Cuardaigh scileanna agus toolsets...",
|
||||
enabledOf: "{enabled}/{total} cumasaithe",
|
||||
|
||||
@@ -360,126 +360,6 @@ export const hu: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Értesítési beállítások",
|
||||
scope: "Proaktív értesítései a szervezet klienseiben. A kézi böngészés és megosztás továbbra is elérhető.",
|
||||
on: "Értesítések bekapcsolva",
|
||||
muted: "Értesítések némítva",
|
||||
day: "1 nap",
|
||||
week: "1 hét",
|
||||
month: "30 nap",
|
||||
forever: "Határozatlan ideig",
|
||||
pending: "Választása helyben mentve, szinkronizálásra vár.",
|
||||
failed: "A szinkronizálás sikertelen. Új választás előtt frissítse a beállításokat.",
|
||||
conflict: "A beállítás egy másik kliensen megváltozott. Frissítsen az ellenőrzéshez.",
|
||||
expired: "A választás lejárt. Frissítse a beállításokat, és próbálja újra.",
|
||||
},
|
||||
tab: 'Közösség',
|
||||
browseHub: 'Hub böngészése',
|
||||
title: 'Kollektív tudás',
|
||||
loading: 'Kollektív tudás betöltése',
|
||||
unavailable: 'A Kollektív tudás nem érhető el.',
|
||||
setup: 'A Kollektív tudás nincs beállítva ehhez a profilhoz.',
|
||||
setupDisclosure:
|
||||
'A jelöltek minősítése ebben a profilban marad. A Gateway csak a tulajdonos által jóváhagyott privát piszkozat tartalmát, a szerzői szöveget, a deklaratív jegyzék metaadatait és a felügyelt telepítés állapotát kapja meg.',
|
||||
setupAction: 'Értem — profil beállítása',
|
||||
settingUp: 'Beállítás…',
|
||||
scanLocal: 'Helyi készségek vizsgálata',
|
||||
search: 'Megosztott készségek keresése…',
|
||||
potential: 'Javasolt hozzájárulások',
|
||||
potentialHelp:
|
||||
'A Hermes által helyben, használat vagy jelentős finomítás alapján minősített készségek. Az ellenőrzésed előtt semmi sem kerül megosztásra.',
|
||||
noSuggestions: 'Jelenleg egyetlen helyi készség sem felel meg az automatikus minősítési szabályoknak.',
|
||||
browseLocal: count => `Összes helyi készség megtekintése (${count})`,
|
||||
browseLocalHelp:
|
||||
'A kézi kiválasztás nem jelenti azt, hogy a Hermes használta vagy automatikusan minősítette ezeket a készségeket.',
|
||||
ownerReview: 'Saját hozzájárulási piszkozatok',
|
||||
ownerReviewHelp: 'Az ellenőrzésedre váró piszkozatok és a kollektíva jóváhagyására váró beküldések.',
|
||||
noDrafts: 'Nincs aktív hozzájárulási piszkozat vagy beküldés.',
|
||||
prepare: 'Hozzájárulás indítása',
|
||||
continueDraft: 'Piszkozat folytatása',
|
||||
localOnly: 'Erről az eszközről megosztható',
|
||||
qualifiedLocally: 'A Hermes ezt a helyi készséget lehetséges hozzájárulásként azonosította.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `A szervezeted (${organizationName})` : 'A szervezeted'} engedélyezte a Collective Wisdom funkciót, amely automatikusan felismeri a csapattagok hasznos készségeit. Gratulálunk! A Hermes olyan készséget talált, amely hasznos lehet a csapatodnak.`,
|
||||
qualificationReturning: 'A Hermes egy újabb készséget talált, amely hasznos lehet a csapatodnak.',
|
||||
savedLocally: 'Ezen az eszközön privát piszkozat van mentve.',
|
||||
reviewExact: 'Részletek megtekintése',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Szerveres ellenőrzés folyamatban',
|
||||
ready: 'Áttekintésre kész',
|
||||
owner_approved: 'Általad jóváhagyva',
|
||||
publishing: 'Közzététel',
|
||||
pending_moderation: 'A közösségi rendszergazda jóváhagyására vár',
|
||||
changes_requested: 'Módosításokat kértek'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'szerveres vizsgálat sikeres',
|
||||
noDescription: 'Nincs a tulajdonos által írt leírás.',
|
||||
managedInstalls: 'felügyelt telepítés',
|
||||
close: 'Bezárás',
|
||||
readEvery: 'Olvass el minden nyers fájlt. A jóváhagyás az alábbi három pontos kivonathoz kötődik.',
|
||||
editReview:
|
||||
'Itt szerkesztheted a leírást, a SKILL.md fájlt vagy a deklaratív jegyzéket. A mentés új privát változatot hoz létre, újra lefuttatja a vizsgálatokat és új kivonatokat ad; a helyi forráskészséget nem írja át.',
|
||||
unsavedChanges: 'Ezeket a módosításokat még nem vizsgáltuk. Jóváhagyás előtt mentsd és vizsgáld újra.',
|
||||
saveAndRescan: 'Módosítások mentése és újravizsgálata',
|
||||
savingRevision: 'Mentés és újravizsgálat…',
|
||||
resetChanges: 'Szerkesztések elvetése',
|
||||
reviewedHashes: 'A szerver által jelenleg ellenőrzött változat kivonatai',
|
||||
prepareTitle: 'Helyi csomag áttekintése feltöltés előtt',
|
||||
prepareNotice:
|
||||
'Ezek a mezők ebben a profilban maradnak, amíg kifejezetten el nem küldöd őket a tulajdonos privát szerveres ellenőrzésére. A helyi minősítési számok és indokok soha nem kerülnek bele.',
|
||||
ownerDescription: 'Tulajdonos által írt leírás',
|
||||
systemSpecification: 'Rendszerspecifikáció (deklaratív metaadatok; nem telepít függőségeket)',
|
||||
localOverlay: 'Helyi fedvény',
|
||||
cancel: 'Mégse',
|
||||
submit: 'Piszkozat beküldése',
|
||||
submitting: 'Beküldés…',
|
||||
publishing: 'Közzététel…',
|
||||
approve: 'Pontos tartalom jóváhagyása és közzététele',
|
||||
decline: 'Elutasítás',
|
||||
checkUpdates: count => `Frissítések keresése${count ? ` (${count})` : ''}`,
|
||||
checking: 'Ellenőrzés…',
|
||||
refreshShared: 'Megosztott készségek frissítése',
|
||||
refreshingShared: 'Frissítés…',
|
||||
installReferenceLabel: 'Telepítés hivatkozásból vagy készségazonosítóból',
|
||||
installReferencePlaceholder: 'Illessz be Portal-hivatkozást, készségazonosítót vagy skill-id@vN értéket',
|
||||
installReferenceHelp: 'A Hermes ellenőrzi a pontos verziót, és telepítés előtt kompatibilitási tervet mutat.',
|
||||
reviewInstall: 'Telepítés áttekintése',
|
||||
planningInstall: 'Ellenőrzés…',
|
||||
updateModeLabel: 'Jövőbeli frissítések',
|
||||
updateModeDefault: 'Szervezeti alapértelmezés használata',
|
||||
updateModeManual: 'Kézi',
|
||||
updateModeAutomatic: 'Automatikus értesítéssel',
|
||||
updateModeRequired: 'Kötelező',
|
||||
updateModeHelp:
|
||||
'A Gateway a szervezet aktuális szabályzatát alkalmazza. A biztonságérzékeny változások továbbra is jóváhagyást igényelnek.',
|
||||
updateModePlan: mode => `Jövőbeli frissítések: ${mode}`,
|
||||
activityReady: count => `${count} új értesítés`,
|
||||
decisionPublished: skill => `${skill} jóváhagyva, és mostantól meg van osztva a csapatoddal.`,
|
||||
decisionChanges: skill => `${skill} megosztás előtt módosítást igényel.`,
|
||||
decisionDeclined: skill => `${skill} megosztását nem hagyták jóvá.`,
|
||||
decisionChanged: (skill, state) => `${skill} hozzájárulási állapota erre változott: ${state}.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} telepítve ebbe a profilba.`,
|
||||
updateNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} frissíthető.`,
|
||||
newSkillNotice: skill => `${skill} megosztva a közösségeddel.`,
|
||||
archivedNotice: skill => `${skill} már nem érhető el új telepítésekhez.`,
|
||||
takedownNotice: skill => `${skill} eltávolítva a közösségből.`,
|
||||
markSeen: 'Összes megjelölése olvasottként',
|
||||
install: 'Telepítés…',
|
||||
uninstall: 'Eltávolítás…',
|
||||
checkSkill: 'Készség ellenőrzése',
|
||||
updateAvailable: version => (version ? `v${version} frissítés érhető el` : 'Frissítés érhető el'),
|
||||
reviewUpdate: 'Frissítés áttekintése',
|
||||
installed: (version, mode) => `Telepítve v${version} · ${mode}`,
|
||||
confirmAction: action => `${action} megerősítése`,
|
||||
acceptCompatibility: 'Áttekintettem és elfogadom a kompatibilitási műveleteket.',
|
||||
acceptSensitive: 'Kifejezetten elfogadom az új érzékeny követelményeket.',
|
||||
preserveModified: 'Először őrizze meg a módosított példányomat nem felügyelt elágazásként.'
|
||||
},
|
||||
title: "Készségek",
|
||||
searchPlaceholder: "Készségek és eszközkészletek keresése...",
|
||||
enabledOf: "{enabled}/{total} engedélyezve",
|
||||
|
||||
@@ -360,128 +360,6 @@ export const it: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Impostazioni notifiche",
|
||||
scope: "Le tue notifiche proattive sui client di questa organizzazione. Consultazione e condivisione manuali restano disponibili.",
|
||||
on: "Notifiche attive",
|
||||
muted: "Notifiche silenziate",
|
||||
day: "1 giorno",
|
||||
week: "1 settimana",
|
||||
month: "30 giorni",
|
||||
forever: "A tempo indeterminato",
|
||||
pending: "La scelta è salvata localmente e attende la sincronizzazione.",
|
||||
failed: "Sincronizzazione non riuscita. Aggiorna le impostazioni prima di scegliere di nuovo.",
|
||||
conflict: "La preferenza è cambiata su un altro client. Aggiorna per verificarla.",
|
||||
expired: "Questa scelta è scaduta. Aggiorna le impostazioni per riprovare.",
|
||||
},
|
||||
tab: 'Collettivo',
|
||||
browseHub: 'Esplora Hub',
|
||||
title: 'Sapienza collettiva',
|
||||
loading: 'Caricamento della Sapienza collettiva',
|
||||
unavailable: 'La Sapienza collettiva non è disponibile.',
|
||||
setup: 'La Sapienza collettiva non è configurata per questo profilo.',
|
||||
setupDisclosure:
|
||||
'La qualificazione dei candidati rimane in questo profilo. Raggiungono il Gateway solo i contenuti della bozza privata approvati dal proprietario, il testo dell’autore, i metadati dichiarativi del manifesto e lo stato delle installazioni gestite.',
|
||||
setupAction: 'Ho capito — configura questo profilo',
|
||||
settingUp: 'Configurazione…',
|
||||
scanLocal: 'Scansiona le competenze locali',
|
||||
search: 'Cerca competenze condivise…',
|
||||
potential: 'Contributi suggeriti',
|
||||
potentialHelp:
|
||||
'Competenze qualificate localmente da Hermes in base all’uso o a miglioramenti significativi. Nulla viene condiviso prima della tua revisione.',
|
||||
noSuggestions: 'Al momento nessuna competenza locale soddisfa le regole di qualificazione automatica.',
|
||||
browseLocal: count => `Visualizza tutte le competenze locali (${count})`,
|
||||
browseLocalHelp:
|
||||
'La selezione manuale non significa che Hermes abbia usato o qualificato automaticamente queste competenze.',
|
||||
ownerReview: 'Le tue bozze di contributo',
|
||||
ownerReviewHelp: 'Bozze in attesa della tua revisione e invii in attesa dell’approvazione del collettivo.',
|
||||
noDrafts: 'Nessuna bozza o invio di contributo attivo.',
|
||||
prepare: 'Inizia contributo',
|
||||
continueDraft: 'Continua bozza',
|
||||
localOnly: 'Disponibile per la condivisione da questo dispositivo',
|
||||
qualifiedLocally: 'Hermes ha identificato questa competenza locale come possibile contributo.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `La tua organizzazione (${organizationName})` : 'La tua organizzazione'} ha attivato Collective Wisdom, una funzione progettata per rilevare automaticamente competenze utili tra tutti i membri del team. Congratulazioni! Hermes ha rilevato una competenza che potrebbe essere utile al tuo team.`,
|
||||
qualificationReturning: 'Hermes ha rilevato un’altra competenza che potrebbe essere utile al tuo team.',
|
||||
savedLocally: 'Una bozza privata è salvata su questo dispositivo.',
|
||||
reviewExact: 'Visualizza dettagli',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Revisione del server in corso',
|
||||
ready: 'Pronta per la tua revisione',
|
||||
owner_approved: 'Approvata da te',
|
||||
publishing: 'Pubblicazione in corso',
|
||||
pending_moderation: 'In attesa dell’approvazione dell’amministratore del collettivo',
|
||||
changes_requested: 'Modifiche richieste'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'scansione del server superata',
|
||||
noDescription: 'Nessuna descrizione scritta dal proprietario.',
|
||||
managedInstalls: 'installazioni gestite',
|
||||
close: 'Chiudi',
|
||||
readEvery: 'Leggi ogni file grezzo. L’approvazione è vincolata ai tre hash esatti mostrati qui sotto.',
|
||||
editReview:
|
||||
'Modifica qui la descrizione, SKILL.md o il manifesto dichiarativo. Il salvataggio crea una nuova revisione privata, ripete le scansioni e restituisce nuovi hash; non riscrive la competenza sorgente locale.',
|
||||
unsavedChanges: 'Queste modifiche non sono state scansionate. Salva e ripeti la scansione prima di approvare.',
|
||||
saveAndRescan: 'Salva modifiche e ripeti scansione',
|
||||
savingRevision: 'Salvataggio e nuova scansione…',
|
||||
resetChanges: 'Annulla modifiche',
|
||||
reviewedHashes: 'Hash della revisione attualmente esaminata dal server',
|
||||
prepareTitle: 'Rivedi il pacchetto locale prima del caricamento',
|
||||
prepareNotice:
|
||||
'Questi campi rimangono in questo profilo finché non li invii esplicitamente alla revisione privata del server riservata al proprietario. Conteggi e motivi di qualificazione locali non vengono mai inclusi.',
|
||||
ownerDescription: 'Descrizione scritta dal proprietario',
|
||||
systemSpecification: 'Specifica di sistema (metadati dichiarativi; non viene installata alcuna dipendenza)',
|
||||
localOverlay: 'Sovrapposizione locale',
|
||||
cancel: 'Annulla',
|
||||
submit: 'Invia bozza',
|
||||
submitting: 'Invio…',
|
||||
publishing: 'Pubblicazione…',
|
||||
approve: 'Approva il contenuto esatto e pubblica',
|
||||
decline: 'Rifiuta',
|
||||
checkUpdates: count => `Controlla aggiornamenti${count ? ` (${count})` : ''}`,
|
||||
checking: 'Controllo…',
|
||||
refreshShared: 'Aggiorna skill condivise',
|
||||
refreshingShared: 'Aggiornamento…',
|
||||
installReferenceLabel: 'Installa da link o ID skill',
|
||||
installReferencePlaceholder: 'Incolla un link del Portal, un ID skill o skill-id@vN',
|
||||
installReferenceHelp:
|
||||
'Hermes verifica la versione esatta e mostra un piano di compatibilità prima dell’installazione.',
|
||||
reviewInstall: 'Rivedi installazione',
|
||||
planningInstall: 'Verifica…',
|
||||
updateModeLabel: 'Aggiornamenti futuri',
|
||||
updateModeDefault: 'Usa l’impostazione predefinita dell’organizzazione',
|
||||
updateModeManual: 'Manuali',
|
||||
updateModeAutomatic: 'Automatici con notifica',
|
||||
updateModeRequired: 'Obbligatori',
|
||||
updateModeHelp:
|
||||
'Gateway applica la politica corrente dell’organizzazione. Le modifiche sensibili per la sicurezza richiedono comunque la tua approvazione.',
|
||||
updateModePlan: mode => `Aggiornamenti futuri: ${mode}`,
|
||||
activityReady: count => `${count} ${count === 1 ? 'nuova notifica' : 'nuove notifiche'}`,
|
||||
decisionPublished: skill => `${skill} è stata approvata e ora è condivisa con il tuo team.`,
|
||||
decisionChanges: skill => `${skill} richiede modifiche prima di poter essere condivisa.`,
|
||||
decisionDeclined: skill => `${skill} non è stata approvata per la condivisione.`,
|
||||
decisionChanged: (skill, state) => `Lo stato del contributo ${skill} è cambiato in ${state}.`,
|
||||
installedNotice: (skill, version) =>
|
||||
`${skill}${version ? ` ${version}` : ''} è stata installata in questo profilo.`,
|
||||
updateNotice: (skill, version) => `È disponibile un aggiornamento per ${skill}${version ? ` ${version}` : ''}.`,
|
||||
newSkillNotice: skill => `${skill} è stata condivisa con il tuo collettivo.`,
|
||||
archivedNotice: skill => `${skill} non è più disponibile per nuove installazioni.`,
|
||||
takedownNotice: skill => `${skill} è stata rimossa dal collettivo.`,
|
||||
markSeen: 'Segna tutto come letto',
|
||||
install: 'Installa…',
|
||||
uninstall: 'Disinstalla…',
|
||||
checkSkill: 'Controlla questa competenza',
|
||||
updateAvailable: version => (version ? `Aggiornamento v${version} disponibile` : 'Aggiornamento disponibile'),
|
||||
reviewUpdate: "Rivedi l'aggiornamento",
|
||||
installed: (version, mode) => `Installata v${version} · ${mode}`,
|
||||
confirmAction: action => `Conferma ${action}`,
|
||||
acceptCompatibility: 'Ho esaminato e accetto le azioni di compatibilità.',
|
||||
acceptSensitive: 'Accetto esplicitamente i nuovi requisiti sensibili.',
|
||||
preserveModified: 'Conserva prima la mia copia modificata come fork non gestito.'
|
||||
},
|
||||
title: "Competenze",
|
||||
searchPlaceholder: "Cerca competenze e toolset...",
|
||||
enabledOf: "{enabled}/{total} abilitati",
|
||||
|
||||
@@ -359,126 +359,6 @@ export const ja: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "通知設定",
|
||||
scope: "この組織の各クライアントで受け取る通知を管理します。手動での閲覧や共有は引き続き利用できます。",
|
||||
on: "通知オン",
|
||||
muted: "通知ミュート",
|
||||
day: "1 日",
|
||||
week: "1 週間",
|
||||
month: "30 日",
|
||||
forever: "無期限",
|
||||
pending: "選択はローカルに保存され、同期待ちです。",
|
||||
failed: "同期に失敗しました。設定を更新してから選び直してください。",
|
||||
conflict: "別のクライアントで設定が変更されました。更新して確認してください。",
|
||||
expired: "この選択は期限切れです。設定を更新して再試行してください。",
|
||||
},
|
||||
tab: 'コレクティブ',
|
||||
browseHub: 'Hub を閲覧',
|
||||
title: 'コレクティブ・ウィズダム',
|
||||
loading: 'コレクティブ・ウィズダムを読み込み中',
|
||||
unavailable: 'コレクティブ・ウィズダムは利用できません。',
|
||||
setup: 'このプロファイルではコレクティブ・ウィズダムが設定されていません。',
|
||||
setupDisclosure:
|
||||
'候補の適格性評価はこのプロファイル内に保持されます。所有者が承認した非公開ドラフトの内容、作者の説明、宣言的なマニフェストのメタデータ、管理対象インストールの状態だけが Gateway に送信されます。',
|
||||
setupAction: '理解しました — このプロファイルを設定',
|
||||
settingUp: '設定中…',
|
||||
scanLocal: 'ローカルスキルをスキャン',
|
||||
search: '共有スキルを検索…',
|
||||
potential: '提案された貢献',
|
||||
potentialHelp:
|
||||
'使用状況または有意な改善に基づき、Hermes がローカルで適格と判断したスキルです。レビューするまで共有されません。',
|
||||
noSuggestions: '現在、自動適格性ルールを満たすローカルスキルはありません。',
|
||||
browseLocal: count => `すべてのローカルスキルを表示 (${count})`,
|
||||
browseLocalHelp:
|
||||
'手動選択できることは、Hermes がそのスキルを使用または自動的に適格と判断したことを意味しません。',
|
||||
ownerReview: 'あなたの貢献ドラフト',
|
||||
ownerReviewHelp: 'あなたのレビュー待ちのドラフトと、コレクティブの承認待ちの提出です。',
|
||||
noDrafts: '進行中の貢献ドラフトまたは提出はありません。',
|
||||
prepare: '貢献を開始',
|
||||
continueDraft: 'ドラフトを続ける',
|
||||
localOnly: 'このデバイスから共有できます',
|
||||
qualifiedLocally: 'Hermes がこのローカルスキルを貢献候補として認識しました。',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `あなたの組織(${organizationName})` : 'あなたの組織'}では、チーム全体から役立つスキルを自動検出する Collective Wisdom が有効になっています。おめでとうございます。Hermes がチームに役立つ可能性のあるスキルを検出しました。`,
|
||||
qualificationReturning: 'Hermes がチームに役立つ可能性のある別のスキルを検出しました。',
|
||||
savedLocally: '非公開ドラフトがこのデバイスに保存されています。',
|
||||
reviewExact: '詳細を表示',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'サーバーレビュー中',
|
||||
ready: 'レビュー待ち',
|
||||
owner_approved: 'あなたが承認済み',
|
||||
publishing: '公開中',
|
||||
pending_moderation: 'コレクティブ管理者の承認待ち',
|
||||
changes_requested: '変更が必要です'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'サーバースキャン合格',
|
||||
noDescription: '所有者による説明はありません。',
|
||||
managedInstalls: '件の管理対象インストール',
|
||||
close: '閉じる',
|
||||
readEvery: '下記のすべての生ファイルを確認してください。承認は表示された正確な3つのハッシュに紐づきます。',
|
||||
editReview:
|
||||
'ここで説明、SKILL.md、宣言的マニフェストを編集できます。保存すると新しい非公開リビジョンが作成され、スキャンが再実行されて新しいハッシュが返されます。ローカルの元スキルは書き換えません。',
|
||||
unsavedChanges: 'これらの変更はまだスキャンされていません。承認前に保存して再スキャンしてください。',
|
||||
saveAndRescan: '変更を保存して再スキャン',
|
||||
savingRevision: '保存して再スキャン中…',
|
||||
resetChanges: '編集を破棄',
|
||||
reviewedHashes: '現在サーバーでレビュー済みのリビジョンのハッシュ',
|
||||
prepareTitle: 'アップロード前にローカルパッケージを確認',
|
||||
prepareNotice:
|
||||
'所有者限定のサーバーレビューに明示的に提出するまで、これらの項目はこのプロファイルに保持されます。ローカルの適格性の回数や理由は含まれません。',
|
||||
ownerDescription: '所有者による説明',
|
||||
systemSpecification: 'システム仕様(宣言的メタデータ。依存関係はインストールされません)',
|
||||
localOverlay: 'ローカルオーバーレイ',
|
||||
cancel: 'キャンセル',
|
||||
submit: '下書きを提出',
|
||||
submitting: '提出中…',
|
||||
publishing: '公開中…',
|
||||
approve: '正確な内容を承認して公開',
|
||||
decline: '辞退',
|
||||
checkUpdates: count => `更新を確認${count ? `(${count})` : ''}`,
|
||||
checking: '確認中…',
|
||||
refreshShared: '共有スキルを更新',
|
||||
refreshingShared: '更新中…',
|
||||
installReferenceLabel: 'リンクまたはスキル ID からインストール',
|
||||
installReferencePlaceholder: 'Portal リンク、スキル ID、または skill-id@vN を貼り付け',
|
||||
installReferenceHelp: 'Hermes はインストール前に正確なバージョンを検証し、互換性プランを表示します。',
|
||||
reviewInstall: 'インストールを確認',
|
||||
planningInstall: '検証中…',
|
||||
updateModeLabel: '今後の更新',
|
||||
updateModeDefault: '組織のデフォルトを使用',
|
||||
updateModeManual: '手動',
|
||||
updateModeAutomatic: '通知付き自動更新',
|
||||
updateModeRequired: '必須',
|
||||
updateModeHelp: 'Gateway は組織の現在のポリシーを適用します。安全性に関わる変更には引き続き承認が必要です。',
|
||||
updateModePlan: mode => `今後の更新: ${mode}`,
|
||||
activityReady: count => `新しい通知 ${count} 件`,
|
||||
decisionPublished: skill => `${skill} は承認され、チームと共有されました。`,
|
||||
decisionChanges: skill => `${skill} は共有前に変更が必要です。`,
|
||||
decisionDeclined: skill => `${skill} は共有が承認されませんでした。`,
|
||||
decisionChanged: (skill, state) => `${skill} の貢献ステータスが ${state} に変わりました。`,
|
||||
installedNotice: (skill, version) =>
|
||||
`${skill}${version ? ` ${version}` : ''} をこのプロファイルにインストールしました。`,
|
||||
updateNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} の更新を利用できます。`,
|
||||
newSkillNotice: skill => `${skill} がコレクティブに共有されました。`,
|
||||
archivedNotice: skill => `${skill} は新規インストールできなくなりました。`,
|
||||
takedownNotice: skill => `${skill} はコレクティブから削除されました。`,
|
||||
markSeen: 'すべて既読にする',
|
||||
install: 'インストール…',
|
||||
uninstall: 'アンインストール…',
|
||||
checkSkill: 'このスキルを確認',
|
||||
updateAvailable: version => (version ? `v${version} に更新可能` : '更新可能'),
|
||||
reviewUpdate: '更新を確認',
|
||||
installed: (version, mode) => `インストール済み v${version} · ${mode}`,
|
||||
confirmAction: action => `${action}を確認`,
|
||||
acceptCompatibility: '互換性に関する操作を確認し、同意します。',
|
||||
acceptSensitive: '新しい機密要件を明示的に受け入れます。',
|
||||
preserveModified: '先に変更済みコピーを管理対象外のフォークとして保持します。'
|
||||
},
|
||||
title: "スキル",
|
||||
searchPlaceholder: "スキルとツールセットを検索...",
|
||||
enabledOf: "{enabled}/{total} 有効",
|
||||
|
||||
@@ -359,124 +359,6 @@ export const ko: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "알림 설정",
|
||||
scope: "이 조직의 여러 클라이언트에서 받는 사전 알림입니다. 수동 탐색과 공유는 계속 사용할 수 있습니다.",
|
||||
on: "알림 켜짐",
|
||||
muted: "알림 음소거",
|
||||
day: "1일",
|
||||
week: "1주",
|
||||
month: "30일",
|
||||
forever: "무기한",
|
||||
pending: "선택 사항이 로컬에 저장되어 동기화를 기다리고 있습니다.",
|
||||
failed: "동기화에 실패했습니다. 설정을 새로 고친 후 다시 선택하세요.",
|
||||
conflict: "다른 클라이언트에서 설정이 변경되었습니다. 새로 고쳐 확인하세요.",
|
||||
expired: "선택 사항이 만료되었습니다. 설정을 새로 고쳐 다시 시도하세요.",
|
||||
},
|
||||
tab: '컬렉티브',
|
||||
browseHub: 'Hub 둘러보기',
|
||||
title: '집단 지식',
|
||||
loading: '집단 지식 불러오는 중',
|
||||
unavailable: '집단 지식을 사용할 수 없습니다.',
|
||||
setup: '이 프로필에 집단 지식이 설정되지 않았습니다.',
|
||||
setupDisclosure:
|
||||
'후보 자격 평가는 이 프로필에만 남습니다. 소유자가 승인한 비공개 초안 내용, 작성자 문구, 선언형 매니페스트 메타데이터 및 관리형 설치 상태만 Gateway에 전달됩니다.',
|
||||
setupAction: '이해했습니다 — 이 프로필 설정',
|
||||
settingUp: '설정 중…',
|
||||
scanLocal: '로컬 스킬 검사',
|
||||
search: '공유 스킬 검색…',
|
||||
potential: '제안된 기여',
|
||||
potentialHelp:
|
||||
'사용 또는 의미 있는 개선을 바탕으로 Hermes가 로컬에서 자격을 확인한 스킬입니다. 검토하기 전에는 공유되지 않습니다.',
|
||||
noSuggestions: '현재 자동 자격 규칙을 충족하는 로컬 스킬이 없습니다.',
|
||||
browseLocal: count => `모든 로컬 스킬 보기 (${count})`,
|
||||
browseLocalHelp: '수동 선택은 Hermes가 해당 스킬을 사용했거나 자동으로 자격을 확인했다는 뜻이 아닙니다.',
|
||||
ownerReview: '내 기여 초안',
|
||||
ownerReviewHelp: '검토를 기다리는 초안과 컬렉티브 승인을 기다리는 제출입니다.',
|
||||
noDrafts: '진행 중인 기여 초안이나 제출이 없습니다.',
|
||||
prepare: '기여 시작',
|
||||
continueDraft: '초안 계속하기',
|
||||
localOnly: '이 기기에서 공유 가능',
|
||||
qualifiedLocally: 'Hermes가 이 로컬 스킬을 기여 후보로 식별했습니다.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `조직(${organizationName})에서` : '조직에서'} 모든 팀원의 유용한 스킬을 자동으로 감지하는 Collective Wisdom을 활성화했습니다. 축하합니다! Hermes가 팀에 유용할 수 있는 스킬을 감지했습니다.`,
|
||||
qualificationReturning: 'Hermes가 팀에 유용할 수 있는 또 다른 스킬을 감지했습니다.',
|
||||
savedLocally: '이 기기에 비공개 초안이 저장되어 있습니다.',
|
||||
reviewExact: '세부정보 보기',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: '서버 검토 진행 중',
|
||||
ready: '검토 준비 완료',
|
||||
owner_approved: '내가 승인함',
|
||||
publishing: '게시 중',
|
||||
pending_moderation: '컬렉티브 관리자 승인 대기 중',
|
||||
changes_requested: '변경 요청됨'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: '서버 검사 통과',
|
||||
noDescription: '소유자가 작성한 설명이 없습니다.',
|
||||
managedInstalls: '관리형 설치',
|
||||
close: '닫기',
|
||||
readEvery: '모든 원본 파일을 읽으세요. 승인은 아래에 표시된 정확한 세 해시에 결합됩니다.',
|
||||
editReview:
|
||||
'여기에서 설명, SKILL.md 또는 선언형 매니페스트를 수정하세요. 저장하면 새 비공개 리비전이 생성되고 검사가 다시 실행되며 새 해시가 반환됩니다. 로컬 원본 스킬은 변경되지 않습니다.',
|
||||
unsavedChanges: '이 변경 사항은 아직 검사되지 않았습니다. 승인하기 전에 저장하고 다시 검사하세요.',
|
||||
saveAndRescan: '변경 사항 저장 및 다시 검사',
|
||||
savingRevision: '저장하고 다시 검사하는 중…',
|
||||
resetChanges: '편집 내용 버리기',
|
||||
reviewedHashes: '현재 서버에서 검토한 리비전의 해시',
|
||||
prepareTitle: '업로드 전 로컬 패키지 검토',
|
||||
prepareNotice:
|
||||
'소유자 전용 서버 검토에 명시적으로 제출할 때까지 이 필드는 이 프로필에 남습니다. 로컬 자격 횟수와 사유는 절대 포함되지 않습니다.',
|
||||
ownerDescription: '소유자가 작성한 설명',
|
||||
systemSpecification: '시스템 사양(선언형 메타데이터, 종속 항목은 설치되지 않음)',
|
||||
localOverlay: '로컬 오버레이',
|
||||
cancel: '취소',
|
||||
submit: '초안 제출',
|
||||
submitting: '제출 중…',
|
||||
publishing: '게시 중…',
|
||||
approve: '정확한 내용 승인 및 게시',
|
||||
decline: '거절',
|
||||
checkUpdates: count => `업데이트 확인${count ? ` (${count})` : ''}`,
|
||||
checking: '확인 중…',
|
||||
refreshShared: '공유 스킬 새로 고침',
|
||||
refreshingShared: '새로 고치는 중…',
|
||||
installReferenceLabel: '링크 또는 스킬 ID로 설치',
|
||||
installReferencePlaceholder: 'Portal 링크, 스킬 ID 또는 skill-id@vN 붙여넣기',
|
||||
installReferenceHelp: 'Hermes가 정확한 버전을 검증하고 설치 전에 호환성 계획을 표시합니다.',
|
||||
reviewInstall: '설치 검토',
|
||||
planningInstall: '검증 중…',
|
||||
updateModeLabel: '향후 업데이트',
|
||||
updateModeDefault: '조직 기본값 사용',
|
||||
updateModeManual: '수동',
|
||||
updateModeAutomatic: '알림 후 자동',
|
||||
updateModeRequired: '필수',
|
||||
updateModeHelp: 'Gateway는 조직의 현재 정책을 적용합니다. 보안에 민감한 변경은 계속 승인이 필요합니다.',
|
||||
updateModePlan: mode => `향후 업데이트: ${mode}`,
|
||||
activityReady: count => `새 알림 ${count}개`,
|
||||
decisionPublished: skill => `${skill}이(가) 승인되어 이제 팀과 공유됩니다.`,
|
||||
decisionChanges: skill => `${skill}을(를) 공유하려면 변경이 필요합니다.`,
|
||||
decisionDeclined: skill => `${skill}은(는) 공유 승인을 받지 못했습니다.`,
|
||||
decisionChanged: (skill, state) => `${skill} 기여 상태가 ${state}(으)로 변경되었습니다.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''}이(가) 이 프로필에 설치되었습니다.`,
|
||||
updateNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} 업데이트를 사용할 수 있습니다.`,
|
||||
newSkillNotice: skill => `${skill}이(가) 컬렉티브에 공유되었습니다.`,
|
||||
archivedNotice: skill => `${skill}은(는) 더 이상 새로 설치할 수 없습니다.`,
|
||||
takedownNotice: skill => `${skill}이(가) 컬렉티브에서 제거되었습니다.`,
|
||||
markSeen: '모두 읽음으로 표시',
|
||||
install: '설치…',
|
||||
uninstall: '제거…',
|
||||
checkSkill: '이 스킬 확인',
|
||||
updateAvailable: version => (version ? `v${version} 업데이트 가능` : '업데이트 가능'),
|
||||
reviewUpdate: '업데이트 검토',
|
||||
installed: (version, mode) => `설치됨 v${version} · ${mode}`,
|
||||
confirmAction: action => `${action} 확인`,
|
||||
acceptCompatibility: '호환성 작업을 검토했으며 동의합니다.',
|
||||
acceptSensitive: '새로운 민감 요구 사항에 명시적으로 동의합니다.',
|
||||
preserveModified: '먼저 수정한 복사본을 관리되지 않는 포크로 보존합니다.'
|
||||
},
|
||||
title: "스킬",
|
||||
searchPlaceholder: "스킬 및 도구 세트 검색...",
|
||||
enabledOf: "{enabled}/{total} 활성화됨",
|
||||
|
||||
@@ -361,127 +361,6 @@ export const pt: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Definições de notificações",
|
||||
scope: "As suas notificações proativas nos clientes desta organização. A consulta e a partilha manuais continuam disponíveis.",
|
||||
on: "Notificações ativadas",
|
||||
muted: "Notificações silenciadas",
|
||||
day: "1 dia",
|
||||
week: "1 semana",
|
||||
month: "30 dias",
|
||||
forever: "Indefinidamente",
|
||||
pending: "A sua escolha foi guardada localmente e aguarda sincronização.",
|
||||
failed: "A sincronização falhou. Atualize as definições antes de escolher novamente.",
|
||||
conflict: "A sua preferência mudou noutro cliente. Atualize para a rever.",
|
||||
expired: "Esta escolha expirou. Atualize as definições para tentar novamente.",
|
||||
},
|
||||
tab: 'Coletivo',
|
||||
browseHub: 'Explorar o Hub',
|
||||
title: 'Sabedoria coletiva',
|
||||
loading: 'A carregar a Sabedoria coletiva',
|
||||
unavailable: 'A Sabedoria coletiva não está disponível.',
|
||||
setup: 'A Sabedoria coletiva não está configurada para este perfil.',
|
||||
setupDisclosure:
|
||||
'A qualificação de candidatos permanece neste perfil. Apenas o conteúdo do rascunho privado aprovado pelo proprietário, o texto do autor, os metadados declarativos do manifesto e o estado das instalações geridas chegam ao Gateway.',
|
||||
setupAction: 'Compreendo — configurar este perfil',
|
||||
settingUp: 'A configurar…',
|
||||
scanLocal: 'Analisar competências locais',
|
||||
search: 'Pesquisar competências partilhadas…',
|
||||
potential: 'Contribuições sugeridas',
|
||||
potentialHelp:
|
||||
'Competências qualificadas localmente pelo Hermes com base no uso ou em melhorias significativas. Nada é partilhado antes da tua revisão.',
|
||||
noSuggestions: 'Atualmente, nenhuma competência local cumpre as regras de qualificação automática.',
|
||||
browseLocal: count => `Ver todas as competências locais (${count})`,
|
||||
browseLocalHelp:
|
||||
'A seleção manual não significa que o Hermes tenha usado ou qualificado automaticamente estas competências.',
|
||||
ownerReview: 'Os teus rascunhos de contribuição',
|
||||
ownerReviewHelp: 'Rascunhos à espera da tua revisão e submissões à espera da aprovação do coletivo.',
|
||||
noDrafts: 'Não há rascunhos ou submissões de contribuição ativos.',
|
||||
prepare: 'Iniciar contribuição',
|
||||
continueDraft: 'Continuar rascunho',
|
||||
localOnly: 'Disponível para partilhar a partir deste dispositivo',
|
||||
qualifiedLocally: 'O Hermes identificou esta competência local como uma possível contribuição.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `A sua organização (${organizationName})` : 'A sua organização'} ativou o Collective Wisdom, uma funcionalidade concebida para detetar automaticamente competências úteis entre todos os membros da equipa. Parabéns! O Hermes detetou uma competência que poderá ser útil para a sua equipa.`,
|
||||
qualificationReturning: 'O Hermes detetou outra competência que poderá ser útil para a sua equipa.',
|
||||
savedLocally: 'Foi guardado um rascunho privado neste dispositivo.',
|
||||
reviewExact: 'Ver detalhes',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Revisão do servidor em curso',
|
||||
ready: 'Pronto para a tua revisão',
|
||||
owner_approved: 'Aprovado por ti',
|
||||
publishing: 'A publicar',
|
||||
pending_moderation: 'A aguardar aprovação do administrador do coletivo',
|
||||
changes_requested: 'Alterações solicitadas'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'análise do servidor aprovada',
|
||||
noDescription: 'Sem descrição escrita pelo proprietário.',
|
||||
managedInstalls: 'instalações geridas',
|
||||
close: 'Fechar',
|
||||
readEvery: 'Lê cada ficheiro em bruto. A aprovação está vinculada aos três hashes exatos apresentados abaixo.',
|
||||
editReview:
|
||||
'Edita aqui a descrição, o SKILL.md ou o manifesto declarativo. Guardar cria uma nova revisão privada, repete as análises e devolve novos hashes; não reescreve a competência de origem local.',
|
||||
unsavedChanges: 'Estas alterações ainda não foram analisadas. Guarda e volta a analisar antes de aprovar.',
|
||||
saveAndRescan: 'Guardar alterações e voltar a analisar',
|
||||
savingRevision: 'A guardar e a analisar novamente…',
|
||||
resetChanges: 'Descartar alterações',
|
||||
reviewedHashes: 'Hashes da revisão atualmente analisada pelo servidor',
|
||||
prepareTitle: 'Rever o pacote local antes de carregar',
|
||||
prepareNotice:
|
||||
'Estes campos permanecem neste perfil até serem explicitamente submetidos a uma revisão privada do servidor para o proprietário. As contagens e os motivos de qualificação locais nunca são incluídos.',
|
||||
ownerDescription: 'Descrição escrita pelo proprietário',
|
||||
systemSpecification: 'Especificação do sistema (metadados declarativos; nenhuma dependência é instalada)',
|
||||
localOverlay: 'Sobreposição local',
|
||||
cancel: 'Cancelar',
|
||||
submit: 'Submeter rascunho',
|
||||
submitting: 'A submeter…',
|
||||
publishing: 'A publicar…',
|
||||
approve: 'Aprovar o conteúdo exato e publicar',
|
||||
decline: 'Recusar',
|
||||
checkUpdates: count => `Procurar atualizações${count ? ` (${count})` : ''}`,
|
||||
checking: 'A verificar…',
|
||||
refreshShared: 'Atualizar skills partilhadas',
|
||||
refreshingShared: 'A atualizar…',
|
||||
installReferenceLabel: 'Instalar a partir de uma ligação ou ID da skill',
|
||||
installReferencePlaceholder: 'Cola uma ligação do Portal, um ID da skill ou skill-id@vN',
|
||||
installReferenceHelp:
|
||||
'O Hermes verifica a versão exata e mostra um plano de compatibilidade antes da instalação.',
|
||||
reviewInstall: 'Rever instalação',
|
||||
planningInstall: 'A validar…',
|
||||
updateModeLabel: 'Atualizações futuras',
|
||||
updateModeDefault: 'Usar a predefinição da organização',
|
||||
updateModeManual: 'Manual',
|
||||
updateModeAutomatic: 'Automática com aviso',
|
||||
updateModeRequired: 'Obrigatória',
|
||||
updateModeHelp:
|
||||
'O Gateway aplica a política atual da sua organização. Alterações sensíveis à segurança continuam a exigir a sua aprovação.',
|
||||
updateModePlan: mode => `Atualizações futuras: ${mode}`,
|
||||
activityReady: count => `${count} ${count === 1 ? 'nova notificação' : 'novas notificações'}`,
|
||||
decisionPublished: skill => `${skill} foi aprovada e está agora partilhada com a tua equipa.`,
|
||||
decisionChanges: skill => `${skill} precisa de alterações antes de poder ser partilhada.`,
|
||||
decisionDeclined: skill => `${skill} não foi aprovada para partilha.`,
|
||||
decisionChanged: (skill, state) => `O estado da contribuição ${skill} mudou para ${state}.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} foi instalada neste perfil.`,
|
||||
updateNotice: (skill, version) => `Está disponível uma atualização para ${skill}${version ? ` ${version}` : ''}.`,
|
||||
newSkillNotice: skill => `${skill} foi partilhada com o teu coletivo.`,
|
||||
archivedNotice: skill => `${skill} já não está disponível para novas instalações.`,
|
||||
takedownNotice: skill => `${skill} foi removida do coletivo.`,
|
||||
markSeen: 'Marcar tudo como lido',
|
||||
install: 'Instalar…',
|
||||
uninstall: 'Desinstalar…',
|
||||
checkSkill: 'Verificar esta competência',
|
||||
updateAvailable: version => (version ? `Atualização v${version} disponível` : 'Atualização disponível'),
|
||||
reviewUpdate: 'Rever atualização',
|
||||
installed: (version, mode) => `Instalada v${version} · ${mode}`,
|
||||
confirmAction: action => `Confirmar ${action}`,
|
||||
acceptCompatibility: 'Revi e aceito as ações de compatibilidade.',
|
||||
acceptSensitive: 'Aceito explicitamente os novos requisitos sensíveis.',
|
||||
preserveModified: 'Preservar primeiro a minha cópia modificada como uma bifurcação não gerida.'
|
||||
},
|
||||
title: "Competências",
|
||||
searchPlaceholder: "Pesquisar competências e conjuntos de ferramentas...",
|
||||
enabledOf: "{enabled}/{total} ativadas",
|
||||
|
||||
@@ -360,125 +360,6 @@ export const ru: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Настройки уведомлений",
|
||||
scope: "Ваши инициативные уведомления во всех клиентах этой организации. Просмотр и публикация вручную остаются доступны.",
|
||||
on: "Уведомления включены",
|
||||
muted: "Уведомления отключены",
|
||||
day: "1 день",
|
||||
week: "1 неделя",
|
||||
month: "30 дней",
|
||||
forever: "Бессрочно",
|
||||
pending: "Выбор сохранён локально и ожидает синхронизации.",
|
||||
failed: "Сбой синхронизации. Обновите настройки и выберите снова.",
|
||||
conflict: "Настройка изменена в другом клиенте. Обновите для проверки.",
|
||||
expired: "Срок действия выбора истёк. Обновите настройки и повторите.",
|
||||
},
|
||||
tab: 'Коллектив',
|
||||
browseHub: 'Открыть Hub',
|
||||
title: 'Коллективные знания',
|
||||
loading: 'Загрузка Коллективных знаний',
|
||||
unavailable: 'Коллективные знания недоступны.',
|
||||
setup: 'Коллективные знания не настроены для этого профиля.',
|
||||
setupDisclosure:
|
||||
'Квалификация кандидатов остаётся в этом профиле. В Gateway передаются только одобренные владельцем данные приватного черновика, авторский текст, декларативные метаданные манифеста и состояние управляемой установки.',
|
||||
setupAction: 'Понятно — настроить этот профиль',
|
||||
settingUp: 'Настройка…',
|
||||
scanLocal: 'Сканировать локальные навыки',
|
||||
search: 'Искать общие навыки…',
|
||||
potential: 'Предлагаемые вклады',
|
||||
potentialHelp:
|
||||
'Навыки, локально квалифицированные Hermes на основе использования или значимых улучшений. До вашей проверки ничего не публикуется.',
|
||||
noSuggestions: 'Сейчас ни один локальный навык не соответствует правилам автоматической квалификации.',
|
||||
browseLocal: count => `Показать все локальные навыки (${count})`,
|
||||
browseLocalHelp: 'Ручной выбор не означает, что Hermes использовал или автоматически квалифицировал эти навыки.',
|
||||
ownerReview: 'Ваши черновики вкладов',
|
||||
ownerReviewHelp: 'Черновики, ожидающие вашей проверки, и заявки, ожидающие одобрения коллектива.',
|
||||
noDrafts: 'Нет активных черновиков или заявок на вклад.',
|
||||
prepare: 'Начать вклад',
|
||||
continueDraft: 'Продолжить черновик',
|
||||
localOnly: 'Можно поделиться с этого устройства',
|
||||
qualifiedLocally: 'Hermes определил этот локальный навык как возможный вклад.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Ваша организация (${organizationName})` : 'Ваша организация'} включила Collective Wisdom — функцию, которая автоматически находит полезные навыки у всех участников команды. Поздравляем! Hermes обнаружил навык, который может быть полезен вашей команде.`,
|
||||
qualificationReturning: 'Hermes обнаружил ещё один навык, который может быть полезен вашей команде.',
|
||||
savedLocally: 'Приватный черновик сохранён на этом устройстве.',
|
||||
reviewExact: 'Показать детали',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Идёт проверка сервером',
|
||||
ready: 'Готово к вашей проверке',
|
||||
owner_approved: 'Одобрено вами',
|
||||
publishing: 'Публикация',
|
||||
pending_moderation: 'Ожидается одобрение администратора коллектива',
|
||||
changes_requested: 'Запрошены изменения'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'проверка сервером пройдена',
|
||||
noDescription: 'Нет описания от владельца.',
|
||||
managedInstalls: 'управляемых установок',
|
||||
close: 'Закрыть',
|
||||
readEvery: 'Прочитайте каждый исходный файл. Одобрение привязано к трём точным хешам ниже.',
|
||||
editReview:
|
||||
'Здесь можно изменить описание, SKILL.md или декларативный манифест. Сохранение создаёт новую приватную редакцию, повторно запускает проверки и возвращает новые хеши; локальный исходный навык не перезаписывается.',
|
||||
unsavedChanges: 'Эти изменения ещё не проверены. Сохраните и повторно просканируйте их перед одобрением.',
|
||||
saveAndRescan: 'Сохранить и повторно сканировать',
|
||||
savingRevision: 'Сохранение и повторное сканирование…',
|
||||
resetChanges: 'Отменить изменения',
|
||||
reviewedHashes: 'Хеши редакции, проверенной сервером',
|
||||
prepareTitle: 'Проверьте локальный пакет перед загрузкой',
|
||||
prepareNotice:
|
||||
'Эти поля остаются в профиле, пока вы явно не отправите их на приватную серверную проверку владельца. Локальные показатели и причины квалификации никогда не включаются.',
|
||||
ownerDescription: 'Описание от владельца',
|
||||
systemSpecification: 'Системная спецификация (декларативные метаданные; зависимости не устанавливаются)',
|
||||
localOverlay: 'Локальный слой',
|
||||
cancel: 'Отмена',
|
||||
submit: 'Отправить черновик',
|
||||
submitting: 'Отправка…',
|
||||
publishing: 'Публикация…',
|
||||
approve: 'Одобрить точное содержимое и опубликовать',
|
||||
decline: 'Отклонить',
|
||||
checkUpdates: count => `Проверить обновления${count ? ` (${count})` : ''}`,
|
||||
checking: 'Проверка…',
|
||||
refreshShared: 'Обновить общие навыки',
|
||||
refreshingShared: 'Обновление…',
|
||||
installReferenceLabel: 'Установить по ссылке или ID навыка',
|
||||
installReferencePlaceholder: 'Вставьте ссылку Portal, ID навыка или skill-id@vN',
|
||||
installReferenceHelp: 'Hermes проверит точную версию и покажет план совместимости перед установкой.',
|
||||
reviewInstall: 'Проверить установку',
|
||||
planningInstall: 'Проверка…',
|
||||
updateModeLabel: 'Будущие обновления',
|
||||
updateModeDefault: 'Использовать настройку организации',
|
||||
updateModeManual: 'Вручную',
|
||||
updateModeAutomatic: 'Автоматически с уведомлением',
|
||||
updateModeRequired: 'Обязательно',
|
||||
updateModeHelp:
|
||||
'Gateway применяет текущую политику организации. Изменения, затрагивающие безопасность, по-прежнему требуют вашего одобрения.',
|
||||
updateModePlan: mode => `Будущие обновления: ${mode}`,
|
||||
activityReady: count => `${count} новых уведомлений`,
|
||||
decisionPublished: skill => `${skill} одобрен и теперь доступен вашей команде.`,
|
||||
decisionChanges: skill => `${skill} нужно изменить перед публикацией.`,
|
||||
decisionDeclined: skill => `${skill} не одобрен для публикации.`,
|
||||
decisionChanged: (skill, state) => `Состояние вклада ${skill} изменено на ${state}.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} установлен в этом профиле.`,
|
||||
updateNotice: (skill, version) => `Для ${skill}${version ? ` ${version}` : ''} доступно обновление.`,
|
||||
newSkillNotice: skill => `${skill} опубликован в вашем коллективе.`,
|
||||
archivedNotice: skill => `${skill} больше недоступен для новых установок.`,
|
||||
takedownNotice: skill => `${skill} удалён из коллектива.`,
|
||||
markSeen: 'Отметить всё прочитанным',
|
||||
install: 'Установить…',
|
||||
uninstall: 'Удалить…',
|
||||
checkSkill: 'Проверить этот навык',
|
||||
updateAvailable: version => (version ? `Доступно обновление v${version}` : 'Доступно обновление'),
|
||||
reviewUpdate: 'Просмотреть обновление',
|
||||
installed: (version, mode) => `Установлено v${version} · ${mode}`,
|
||||
confirmAction: action => `Подтвердить: ${action}`,
|
||||
acceptCompatibility: 'Я проверил(-а) и принимаю действия по совместимости.',
|
||||
acceptSensitive: 'Я явно принимаю новые чувствительные требования.',
|
||||
preserveModified: 'Сначала сохранить мою изменённую копию как неуправляемый форк.'
|
||||
},
|
||||
title: "Навыки",
|
||||
searchPlaceholder: "Поиск навыков и наборов инструментов...",
|
||||
enabledOf: "{enabled}/{total} включено",
|
||||
|
||||
@@ -360,126 +360,6 @@ export const tr: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Bildirim ayarları",
|
||||
scope: "Bu kuruluştaki istemcilerde proaktif bildirimleriniz. Elle göz atma ve paylaşma kullanılabilir kalır.",
|
||||
on: "Bildirimler açık",
|
||||
muted: "Bildirimler sessizde",
|
||||
day: "1 gün",
|
||||
week: "1 hafta",
|
||||
month: "30 gün",
|
||||
forever: "Süresiz",
|
||||
pending: "Seçiminiz yerel olarak kaydedildi ve eşitleme bekliyor.",
|
||||
failed: "Eşitleme başarısız. Yeniden seçmeden önce ayarları yenileyin.",
|
||||
conflict: "Tercihiniz başka bir istemcide değişti. İncelemek için yenileyin.",
|
||||
expired: "Bu seçimin süresi doldu. Tekrar denemek için ayarları yenileyin.",
|
||||
},
|
||||
tab: 'Kolektif',
|
||||
browseHub: 'Hub’a göz at',
|
||||
title: 'Kolektif Bilgelik',
|
||||
loading: 'Kolektif Bilgelik yükleniyor',
|
||||
unavailable: 'Kolektif Bilgelik kullanılamıyor.',
|
||||
setup: 'Kolektif Bilgelik bu profil için ayarlanmamış.',
|
||||
setupDisclosure:
|
||||
'Aday yeterlilik bilgileri bu profilde kalır. Gateway’e yalnızca sahibinin onayladığı özel taslak içeriği, yazar metni, bildirimsel manifest meta verileri ve yönetilen kurulum durumu ulaşır.',
|
||||
setupAction: 'Anladım — bu profili ayarla',
|
||||
settingUp: 'Ayarlanıyor…',
|
||||
scanLocal: 'Yerel becerileri tara',
|
||||
search: 'Paylaşılan becerileri ara…',
|
||||
potential: 'Önerilen katkılar',
|
||||
potentialHelp:
|
||||
"Hermes'in kullanım veya anlamlı iyileştirmeye göre yerel olarak nitelendirdiği beceriler. Siz inceleyene kadar hiçbir şey paylaşılmaz.",
|
||||
noSuggestions: 'Şu anda hiçbir yerel beceri otomatik nitelendirme kurallarını karşılamıyor.',
|
||||
browseLocal: count => `Tüm yerel becerileri görüntüle (${count})`,
|
||||
browseLocalHelp:
|
||||
"Manuel seçim, Hermes'in bu becerileri kullandığı veya otomatik olarak nitelendirdiği anlamına gelmez.",
|
||||
ownerReview: 'Katkı taslaklarınız',
|
||||
ownerReviewHelp: 'İncelemenizi bekleyen taslaklar ve kolektif onayı bekleyen gönderimler.',
|
||||
noDrafts: 'Etkin katkı taslağı veya gönderimi yok.',
|
||||
prepare: 'Katkıyı başlat',
|
||||
continueDraft: 'Taslağa devam et',
|
||||
localOnly: 'Bu cihazdan paylaşılabilir',
|
||||
qualifiedLocally: 'Hermes bu yerel beceriyi olası bir katkı olarak belirledi.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Kuruluşunuz (${organizationName})` : 'Kuruluşunuz'} tüm ekip üyelerindeki yararlı becerileri otomatik olarak algılamak için tasarlanan Collective Wisdom özelliğini etkinleştirdi. Tebrikler! Hermes ekibiniz için yararlı olabilecek bir beceri algıladı.`,
|
||||
qualificationReturning: 'Hermes ekibiniz için yararlı olabilecek başka bir beceri algıladı.',
|
||||
savedLocally: 'Bu cihazda özel bir taslak kayıtlı.',
|
||||
reviewExact: 'Ayrıntıları görüntüle',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Sunucu incelemesi sürüyor',
|
||||
ready: 'İncelemeniz için hazır',
|
||||
owner_approved: 'Tarafınızdan onaylandı',
|
||||
publishing: 'Yayımlanıyor',
|
||||
pending_moderation: 'Kolektif yöneticisinin onayı bekleniyor',
|
||||
changes_requested: 'Değişiklik istendi'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'sunucu taraması geçti',
|
||||
noDescription: 'Sahibi tarafından yazılmış açıklama yok.',
|
||||
managedInstalls: 'yönetilen kurulum',
|
||||
close: 'Kapat',
|
||||
readEvery: 'Her ham dosyayı okuyun. Onay, aşağıda gösterilen üç kesin karmaya bağlıdır.',
|
||||
editReview:
|
||||
'Açıklamayı, SKILL.md dosyasını veya bildirimsel manifesti burada düzenleyin. Kaydetmek yeni bir özel revizyon oluşturur, taramaları yeniden çalıştırır ve yeni karmalar döndürür; yerel kaynak becerinizi yeniden yazmaz.',
|
||||
unsavedChanges: 'Bu değişiklikler taranmadı. Onaylamadan önce kaydedip yeniden tarayın.',
|
||||
saveAndRescan: 'Değişiklikleri kaydet ve yeniden tara',
|
||||
savingRevision: 'Kaydediliyor ve yeniden taranıyor…',
|
||||
resetChanges: 'Düzenlemeleri at',
|
||||
reviewedHashes: 'Sunucunun şu anda incelediği revizyonun karmaları',
|
||||
prepareTitle: 'Yüklemeden önce yerel paketi inceleyin',
|
||||
prepareNotice:
|
||||
'Bu alanlar, yalnızca sahibin görebildiği sunucu incelemesine açıkça gönderene kadar bu profilde kalır. Yerel yeterlilik sayıları ve nedenleri hiçbir zaman eklenmez.',
|
||||
ownerDescription: 'Sahibi tarafından yazılmış açıklama',
|
||||
systemSpecification: 'Sistem Belirtimi (bildirimsel meta veri; hiçbir bağımlılık kurulmaz)',
|
||||
localOverlay: 'Yerel katman',
|
||||
cancel: 'İptal',
|
||||
submit: 'Taslağı gönder',
|
||||
submitting: 'Gönderiliyor…',
|
||||
publishing: 'Yayımlanıyor…',
|
||||
approve: 'Kesin içeriği onayla ve yayımla',
|
||||
decline: 'Reddet',
|
||||
checkUpdates: count => `Güncellemeleri denetle${count ? ` (${count})` : ''}`,
|
||||
checking: 'Denetleniyor…',
|
||||
refreshShared: 'Paylaşılan becerileri yenile',
|
||||
refreshingShared: 'Yenileniyor…',
|
||||
installReferenceLabel: 'Bağlantıdan veya beceri kimliğinden yükle',
|
||||
installReferencePlaceholder: 'Portal bağlantısı, beceri kimliği veya skill-id@vN yapıştırın',
|
||||
installReferenceHelp: 'Hermes tam sürümü doğrular ve yüklemeden önce bir uyumluluk planı gösterir.',
|
||||
reviewInstall: 'Yüklemeyi incele',
|
||||
planningInstall: 'Doğrulanıyor…',
|
||||
updateModeLabel: 'Gelecekteki güncellemeler',
|
||||
updateModeDefault: 'Kuruluş varsayılanını kullan',
|
||||
updateModeManual: 'Manuel',
|
||||
updateModeAutomatic: 'Bildirimli otomatik',
|
||||
updateModeRequired: 'Zorunlu',
|
||||
updateModeHelp:
|
||||
'Gateway kuruluşunuzun güncel politikasını uygular. Güvenlik açısından hassas değişiklikler yine onayınızı gerektirir.',
|
||||
updateModePlan: mode => `Gelecekteki güncellemeler: ${mode}`,
|
||||
activityReady: count => `${count} yeni bildirim`,
|
||||
decisionPublished: skill => `${skill} onaylandı ve artık ekibinizle paylaşılıyor.`,
|
||||
decisionChanges: skill => `${skill} paylaşılmadan önce değişiklik gerektiriyor.`,
|
||||
decisionDeclined: skill => `${skill} paylaşım için onaylanmadı.`,
|
||||
decisionChanged: (skill, state) => `${skill} katkısının durumu ${state} olarak değişti.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} bu profile kuruldu.`,
|
||||
updateNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} için güncelleme var.`,
|
||||
newSkillNotice: skill => `${skill} kolektifinizle paylaşıldı.`,
|
||||
archivedNotice: skill => `${skill} artık yeni kurulumlar için kullanılamıyor.`,
|
||||
takedownNotice: skill => `${skill} kolektiften kaldırıldı.`,
|
||||
markSeen: 'Tümünü okundu işaretle',
|
||||
install: 'Kur…',
|
||||
uninstall: 'Kaldır…',
|
||||
checkSkill: 'Bu beceriyi denetle',
|
||||
updateAvailable: version => (version ? `v${version} güncellemesi hazır` : 'Güncelleme hazır'),
|
||||
reviewUpdate: 'Güncellemeyi incele',
|
||||
installed: (version, mode) => `Kurulu v${version} · ${mode}`,
|
||||
confirmAction: action => `${action} işlemini onayla`,
|
||||
acceptCompatibility: 'Uyumluluk işlemlerini inceledim ve kabul ediyorum.',
|
||||
acceptSensitive: 'Yeni hassas gereksinimleri açıkça kabul ediyorum.',
|
||||
preserveModified: 'Önce değiştirilmiş kopyamı yönetilmeyen bir çatal olarak koru.'
|
||||
},
|
||||
title: "Yetenekler",
|
||||
searchPlaceholder: "Yetenek ve araç setlerinde ara...",
|
||||
enabledOf: "{enabled}/{total} etkin",
|
||||
|
||||
@@ -17,98 +17,6 @@ export type Locale =
|
||||
| "hu"
|
||||
| "ar";
|
||||
|
||||
export interface WisdomTranslations {
|
||||
syncRecovery?: import('@hermes/shared').WisdomSyncCopy
|
||||
notificationPreferences: import('@hermes/shared').WisdomMuteCopy;
|
||||
tab: string
|
||||
browseHub: string
|
||||
title: string
|
||||
loading: string
|
||||
unavailable: string
|
||||
setup: string
|
||||
setupDisclosure: string
|
||||
setupAction: string
|
||||
settingUp: string
|
||||
scanLocal: string
|
||||
search: string
|
||||
potential: string
|
||||
potentialHelp: string
|
||||
noSuggestions: string
|
||||
browseLocal: (count: number) => string
|
||||
browseLocalHelp: string
|
||||
ownerReview: string
|
||||
ownerReviewHelp: string
|
||||
noDrafts: string
|
||||
prepare: string
|
||||
continueDraft: string
|
||||
localOnly: string
|
||||
qualifiedLocally: string
|
||||
qualificationFirst: (organizationName?: string | null) => string
|
||||
qualificationReturning: string
|
||||
savedLocally: string
|
||||
reviewExact: string
|
||||
draftState: (state: string) => string
|
||||
serverScanPassed: string
|
||||
noDescription: string
|
||||
managedInstalls: string
|
||||
close: string
|
||||
readEvery: string
|
||||
editReview: string
|
||||
unsavedChanges: string
|
||||
saveAndRescan: string
|
||||
savingRevision: string
|
||||
resetChanges: string
|
||||
reviewedHashes: string
|
||||
prepareTitle: string
|
||||
prepareNotice: string
|
||||
ownerDescription: string
|
||||
systemSpecification: string
|
||||
localOverlay: string
|
||||
cancel: string
|
||||
submit: string
|
||||
submitting: string
|
||||
publishing: string
|
||||
approve: string
|
||||
decline: string
|
||||
checkUpdates: (count: number) => string
|
||||
checking: string
|
||||
refreshShared: string
|
||||
refreshingShared: string
|
||||
installReferenceLabel: string
|
||||
installReferencePlaceholder: string
|
||||
installReferenceHelp: string
|
||||
reviewInstall: string
|
||||
planningInstall: string
|
||||
updateModeLabel: string
|
||||
updateModeDefault: string
|
||||
updateModeManual: string
|
||||
updateModeAutomatic: string
|
||||
updateModeRequired: string
|
||||
updateModeHelp: string
|
||||
updateModePlan: (mode: string) => string
|
||||
activityReady: (count: number) => string
|
||||
decisionPublished: (skill: string) => string
|
||||
decisionChanges: (skill: string) => string
|
||||
decisionDeclined: (skill: string) => string
|
||||
decisionChanged: (skill: string, state: string) => string
|
||||
installedNotice: (skill: string, version?: string) => string
|
||||
updateNotice: (skill: string, version?: string) => string
|
||||
newSkillNotice: (skill: string) => string
|
||||
archivedNotice: (skill: string) => string
|
||||
takedownNotice: (skill: string) => string
|
||||
markSeen: string
|
||||
install: string
|
||||
uninstall: string
|
||||
checkSkill: string
|
||||
updateAvailable: (version?: number) => string
|
||||
reviewUpdate: string
|
||||
installed: (version: number, mode: string) => string
|
||||
confirmAction: (action: string) => string
|
||||
acceptCompatibility: string
|
||||
acceptSensitive: string
|
||||
preserveModified: string
|
||||
}
|
||||
|
||||
export interface Translations {
|
||||
// ── Common ──
|
||||
common: {
|
||||
@@ -543,7 +451,6 @@ export interface Translations {
|
||||
|
||||
// ── Skills page ──
|
||||
skills: {
|
||||
wisdom: WisdomTranslations;
|
||||
title: string;
|
||||
searchPlaceholder: string;
|
||||
enabledOf: string;
|
||||
|
||||
@@ -361,125 +361,6 @@ export const uk: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "Налаштування сповіщень",
|
||||
scope: "Ваші проактивні сповіщення в усіх клієнтах цієї організації. Перегляд і поширення вручну залишаються доступними.",
|
||||
on: "Сповіщення ввімкнено",
|
||||
muted: "Сповіщення вимкнено",
|
||||
day: "1 день",
|
||||
week: "1 тиждень",
|
||||
month: "30 днів",
|
||||
forever: "Безстроково",
|
||||
pending: "Вибір збережено локально й очікує синхронізації.",
|
||||
failed: "Помилка синхронізації. Оновіть налаштування перед новим вибором.",
|
||||
conflict: "Налаштування змінено в іншому клієнті. Оновіть для перевірки.",
|
||||
expired: "Термін дії вибору минув. Оновіть налаштування та повторіть.",
|
||||
},
|
||||
tab: 'Колектив',
|
||||
browseHub: 'Переглянути Hub',
|
||||
title: 'Колективна мудрість',
|
||||
loading: 'Завантаження Колективної мудрості',
|
||||
unavailable: 'Колективна мудрість недоступна.',
|
||||
setup: 'Колективну мудрість не налаштовано для цього профілю.',
|
||||
setupDisclosure:
|
||||
'Кваліфікація кандидатів залишається в цьому профілі. До Gateway надходять лише схвалені власником дані приватної чернетки, авторський текст, декларативні метадані маніфесту та стан керованого встановлення.',
|
||||
setupAction: 'Зрозуміло — налаштувати цей профіль',
|
||||
settingUp: 'Налаштування…',
|
||||
scanLocal: 'Сканувати локальні навички',
|
||||
search: 'Шукати спільні навички…',
|
||||
potential: 'Запропоновані внески',
|
||||
potentialHelp:
|
||||
'Навички, локально кваліфіковані Hermes на основі використання або суттєвих удосконалень. До вашої перевірки нічого не поширюється.',
|
||||
noSuggestions: 'Наразі жодна локальна навичка не відповідає правилам автоматичної кваліфікації.',
|
||||
browseLocal: count => `Переглянути всі локальні навички (${count})`,
|
||||
browseLocalHelp: 'Ручний вибір не означає, що Hermes використовував або автоматично кваліфікував ці навички.',
|
||||
ownerReview: 'Ваші чернетки внесків',
|
||||
ownerReviewHelp: 'Чернетки, що очікують вашої перевірки, і подання, що очікують схвалення колективу.',
|
||||
noDrafts: 'Немає активних чернеток або подань внеску.',
|
||||
prepare: 'Почати внесок',
|
||||
continueDraft: 'Продовжити чернетку',
|
||||
localOnly: 'Можна поширити з цього пристрою',
|
||||
qualifiedLocally: 'Hermes визначив цю локальну навичку як можливий внесок.',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `Ваша організація (${organizationName})` : 'Ваша організація'} увімкнула Collective Wisdom — функцію, яка автоматично знаходить корисні навички в усіх учасників команди. Вітаємо! Hermes виявив навичку, яка може бути корисною вашій команді.`,
|
||||
qualificationReturning: 'Hermes виявив ще одну навичку, яка може бути корисною вашій команді.',
|
||||
savedLocally: 'Приватну чернетку збережено на цьому пристрої.',
|
||||
reviewExact: 'Переглянути деталі',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: 'Триває перевірка сервером',
|
||||
ready: 'Готово до вашої перевірки',
|
||||
owner_approved: 'Схвалено вами',
|
||||
publishing: 'Публікується',
|
||||
pending_moderation: 'Очікує схвалення адміністратора колективу',
|
||||
changes_requested: 'Запитано зміни'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: 'серверне сканування пройдено',
|
||||
noDescription: 'Немає опису від власника.',
|
||||
managedInstalls: 'керованих встановлень',
|
||||
close: 'Закрити',
|
||||
readEvery: 'Прочитайте кожен необроблений файл. Схвалення прив’язане до трьох точних хешів нижче.',
|
||||
editReview:
|
||||
'Тут можна змінити опис, SKILL.md або декларативний маніфест. Збереження створює нову приватну редакцію, повторно запускає сканування та повертає нові хеші; локальна вихідна навичка не перезаписується.',
|
||||
unsavedChanges: 'Ці зміни ще не сканувалися. Збережіть і повторно проскануйте їх перед схваленням.',
|
||||
saveAndRescan: 'Зберегти зміни й повторно сканувати',
|
||||
savingRevision: 'Збереження й повторне сканування…',
|
||||
resetChanges: 'Відкинути зміни',
|
||||
reviewedHashes: 'Хеші редакції, яку наразі перевірив сервер',
|
||||
prepareTitle: 'Перевірте локальний пакет перед завантаженням',
|
||||
prepareNotice:
|
||||
'Ці поля залишаються в цьому профілі, доки ви явно не надішлете їх на приватну серверну перевірку власника. Локальні показники й причини кваліфікації ніколи не додаються.',
|
||||
ownerDescription: 'Опис від власника',
|
||||
systemSpecification: 'Специфікація системи (декларативні метадані; залежності не встановлюються)',
|
||||
localOverlay: 'Локальний шар',
|
||||
cancel: 'Скасувати',
|
||||
submit: 'Надіслати чернетку',
|
||||
submitting: 'Надсилання…',
|
||||
publishing: 'Публікація…',
|
||||
approve: 'Схвалити точний вміст і опублікувати',
|
||||
decline: 'Відхилити',
|
||||
checkUpdates: count => `Перевірити оновлення${count ? ` (${count})` : ''}`,
|
||||
checking: 'Перевірка…',
|
||||
refreshShared: 'Оновити спільні навички',
|
||||
refreshingShared: 'Оновлення…',
|
||||
installReferenceLabel: 'Встановити за посиланням або ID навички',
|
||||
installReferencePlaceholder: 'Вставте посилання Portal, ID навички або skill-id@vN',
|
||||
installReferenceHelp: 'Hermes перевіряє точну версію та показує план сумісності перед установленням.',
|
||||
reviewInstall: 'Переглянути встановлення',
|
||||
planningInstall: 'Перевірка…',
|
||||
updateModeLabel: 'Майбутні оновлення',
|
||||
updateModeDefault: 'Використовувати налаштування організації',
|
||||
updateModeManual: 'Вручну',
|
||||
updateModeAutomatic: 'Автоматично зі сповіщенням',
|
||||
updateModeRequired: 'Обов’язково',
|
||||
updateModeHelp:
|
||||
'Gateway застосовує поточну політику організації. Зміни, чутливі до безпеки, і надалі потребують вашого схвалення.',
|
||||
updateModePlan: mode => `Майбутні оновлення: ${mode}`,
|
||||
activityReady: count => `${count} нових сповіщень`,
|
||||
decisionPublished: skill => `${skill} схвалено й тепер поширено для вашої команди.`,
|
||||
decisionChanges: skill => `${skill} потрібно змінити перед поширенням.`,
|
||||
decisionDeclined: skill => `${skill} не схвалено для поширення.`,
|
||||
decisionChanged: (skill, state) => `Стан внеску ${skill} змінено на ${state}.`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} встановлено в цьому профілі.`,
|
||||
updateNotice: (skill, version) => `Для ${skill}${version ? ` ${version}` : ''} доступне оновлення.`,
|
||||
newSkillNotice: skill => `${skill} поширено у вашому колективі.`,
|
||||
archivedNotice: skill => `${skill} більше не доступна для нових встановлень.`,
|
||||
takedownNotice: skill => `${skill} видалено з колективу.`,
|
||||
markSeen: 'Позначити все як прочитане',
|
||||
install: 'Встановити…',
|
||||
uninstall: 'Видалити…',
|
||||
checkSkill: 'Перевірити цю навичку',
|
||||
updateAvailable: version => (version ? `Доступне оновлення v${version}` : 'Доступне оновлення'),
|
||||
reviewUpdate: 'Переглянути оновлення',
|
||||
installed: (version, mode) => `Встановлено v${version} · ${mode}`,
|
||||
confirmAction: action => `Підтвердити: ${action}`,
|
||||
acceptCompatibility: 'Я перевірив(-ла) й приймаю дії щодо сумісності.',
|
||||
acceptSensitive: 'Я явно приймаю нові чутливі вимоги.',
|
||||
preserveModified: 'Спочатку зберегти мою змінену копію як некерований форк.'
|
||||
},
|
||||
title: "Навички",
|
||||
searchPlaceholder: "Пошук навичок та наборів інструментів...",
|
||||
enabledOf: "{enabled}/{total} увімкнено",
|
||||
|
||||
@@ -359,123 +359,6 @@ export const zhHant: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "通知設定",
|
||||
scope: "管理此組織中跨用戶端的主動通知。手動瀏覽和分享仍可使用。",
|
||||
on: "通知已開啟",
|
||||
muted: "通知已靜音",
|
||||
day: "1 天",
|
||||
week: "1 週",
|
||||
month: "30 天",
|
||||
forever: "無限期",
|
||||
pending: "選擇已儲存在本機,正在等待同步。",
|
||||
failed: "同步失敗。請重新整理設定後重新選擇。",
|
||||
conflict: "其他用戶端變更了偏好。請重新整理查看。",
|
||||
expired: "此選擇已過期。請重新整理設定後再試。",
|
||||
},
|
||||
tab: '集體',
|
||||
browseHub: '瀏覽 Hub',
|
||||
title: '集體智慧',
|
||||
loading: '正在載入集體智慧',
|
||||
unavailable: '集體智慧目前無法使用。',
|
||||
setup: '此設定檔尚未設定集體智慧。',
|
||||
setupDisclosure:
|
||||
'候選資格評估會保留在此設定檔中。只有經擁有者核准的私人草稿內容、作者文案、宣告式資訊清單中繼資料和受管理安裝狀態會傳送到閘道。',
|
||||
setupAction: '我已瞭解 — 設定此設定檔',
|
||||
settingUp: '正在設定…',
|
||||
scanLocal: '掃描本機技能',
|
||||
search: '搜尋共享技能…',
|
||||
potential: '建議的貢獻',
|
||||
potentialHelp: 'Hermes 根據本機使用或有意義的改進判定為合格的技能。在你審核前不會共享。',
|
||||
noSuggestions: '目前沒有本機技能符合自動資格規則。',
|
||||
browseLocal: count => `檢視所有本機技能 (${count})`,
|
||||
browseLocalHelp: '手動選擇並不表示 Hermes 使用過這些技能或已自動判定其合格。',
|
||||
ownerReview: '你的貢獻草稿',
|
||||
ownerReviewHelp: '等待你審核的草稿,以及等待集體核准的提交。',
|
||||
noDrafts: '沒有進行中的貢獻草稿或提交。',
|
||||
prepare: '開始貢獻',
|
||||
continueDraft: '繼續編輯草稿',
|
||||
localOnly: '可從此裝置共享',
|
||||
qualifiedLocally: 'Hermes 將此本機技能識別為可能的貢獻。',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `您的組織(${organizationName})` : '您的組織'}已啟用 Collective Wisdom,此功能會自動探索所有團隊成員的實用技能。恭喜!Hermes 偵測到一項可能對您的團隊有用的技能。`,
|
||||
qualificationReturning: 'Hermes 又偵測到一項可能對您的團隊有用的技能。',
|
||||
savedLocally: '私人草稿已儲存在此裝置上。',
|
||||
reviewExact: '檢視詳細資料',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: '伺服器正在審核',
|
||||
ready: '等待你審核',
|
||||
owner_approved: '你已核准',
|
||||
publishing: '正在發佈',
|
||||
pending_moderation: '等待集體管理員核准',
|
||||
changes_requested: '已要求修改'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: '伺服器掃描已通過',
|
||||
noDescription: '沒有擁有者撰寫的描述。',
|
||||
managedInstalls: '個受管理安裝',
|
||||
close: '關閉',
|
||||
readEvery: '請閱讀下方每個原始檔案。核准將綁定到顯示的三個精確雜湊值。',
|
||||
editReview:
|
||||
'可在此編輯描述、SKILL.md 或宣告式資訊清單。儲存會建立新的私人修訂、重新執行掃描並傳回新雜湊值;不會改寫你的本機來源技能。',
|
||||
unsavedChanges: '這些變更尚未掃描。請先儲存並重新掃描,再進行核准。',
|
||||
saveAndRescan: '儲存變更並重新掃描',
|
||||
savingRevision: '正在儲存並重新掃描…',
|
||||
resetChanges: '捨棄編輯',
|
||||
reviewedHashes: '目前伺服器已審核修訂的雜湊值',
|
||||
prepareTitle: '上傳前審核本機套件',
|
||||
prepareNotice:
|
||||
'在你明確提交至僅擁有者可見的伺服器審核前,這些欄位會保留在此設定檔中。本機資格計數和原因永遠不會包含在內。',
|
||||
ownerDescription: '擁有者撰寫的描述',
|
||||
systemSpecification: '系統規格(宣告式中繼資料;不會安裝相依項目)',
|
||||
localOverlay: '本機覆疊',
|
||||
cancel: '取消',
|
||||
submit: '提交草稿',
|
||||
submitting: '正在提交…',
|
||||
publishing: '正在發佈…',
|
||||
approve: '核准精確內容並發佈',
|
||||
decline: '拒絕',
|
||||
checkUpdates: count => `檢查更新${count ? `(${count})` : ''}`,
|
||||
checking: '正在檢查…',
|
||||
refreshShared: '重新整理共享技能',
|
||||
refreshingShared: '正在重新整理…',
|
||||
installReferenceLabel: '從連結或技能 ID 安裝',
|
||||
installReferencePlaceholder: '貼上 Portal 連結、技能 ID 或 skill-id@vN',
|
||||
installReferenceHelp: 'Hermes 會先驗證確切版本並顯示相容性計畫,再進行安裝。',
|
||||
reviewInstall: '檢查安裝',
|
||||
planningInstall: '正在驗證…',
|
||||
updateModeLabel: '後續更新',
|
||||
updateModeDefault: '使用組織預設設定',
|
||||
updateModeManual: '手動',
|
||||
updateModeAutomatic: '自動更新並通知',
|
||||
updateModeRequired: '必須更新',
|
||||
updateModeHelp: 'Gateway 會套用組織目前的政策。涉及安全的變更仍需你的核准。',
|
||||
updateModePlan: mode => `後續更新:${mode}`,
|
||||
activityReady: count => `${count} 則新通知`,
|
||||
decisionPublished: skill => `${skill} 已獲核准,現在已與你的團隊共享。`,
|
||||
decisionChanges: skill => `${skill} 需要修改後才能共享。`,
|
||||
decisionDeclined: skill => `${skill} 未獲共享核准。`,
|
||||
decisionChanged: (skill, state) => `${skill} 的貢獻狀態已變更為${state}。`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} 已安裝到此設定檔。`,
|
||||
updateNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} 有可用更新。`,
|
||||
newSkillNotice: skill => `${skill} 已與你的集體共享。`,
|
||||
archivedNotice: skill => `${skill} 已不再可用於新安裝。`,
|
||||
takedownNotice: skill => `${skill} 已從集體中移除。`,
|
||||
markSeen: '全部標示為已讀',
|
||||
install: '安裝…',
|
||||
uninstall: '解除安裝…',
|
||||
checkSkill: '檢查此技能',
|
||||
updateAvailable: version => (version ? `可更新至 v${version}` : '有可用更新'),
|
||||
reviewUpdate: '檢視更新',
|
||||
installed: (version, mode) => `已安裝 v${version} · ${mode}`,
|
||||
confirmAction: action => `確認${action}`,
|
||||
acceptCompatibility: '我已審核並接受相容性操作。',
|
||||
acceptSensitive: '我明確接受新的敏感需求。',
|
||||
preserveModified: '先將我修改過的副本保留為不受管理的分支。'
|
||||
},
|
||||
title: "技能",
|
||||
searchPlaceholder: "搜尋技能與工具集...",
|
||||
enabledOf: "已啟用 {enabled}/{total}",
|
||||
|
||||
@@ -355,123 +355,6 @@ export const zh: Translations = {
|
||||
},
|
||||
|
||||
skills: {
|
||||
wisdom: {
|
||||
notificationPreferences: {
|
||||
title: "通知设置",
|
||||
scope: "管理此组织中跨客户端的主动通知。手动浏览和分享仍可使用。",
|
||||
on: "通知已开启",
|
||||
muted: "通知已静音",
|
||||
day: "1 天",
|
||||
week: "1 周",
|
||||
month: "30 天",
|
||||
forever: "无限期",
|
||||
pending: "选择已保存在本地,正在等待同步。",
|
||||
failed: "同步失败。请刷新设置后重新选择。",
|
||||
conflict: "其他客户端更改了偏好。请刷新查看。",
|
||||
expired: "此选择已过期。请刷新设置后重试。",
|
||||
},
|
||||
tab: '集体',
|
||||
browseHub: '浏览 Hub',
|
||||
title: '集体智慧',
|
||||
loading: '正在加载集体智慧',
|
||||
unavailable: '集体智慧暂不可用。',
|
||||
setup: '此配置文件尚未设置集体智慧。',
|
||||
setupDisclosure:
|
||||
'候选资格评估保留在此配置文件中。只有经所有者批准的私有草稿内容、作者文案、声明式清单元数据和托管安装状态会发送到网关。',
|
||||
setupAction: '我已了解 — 设置此配置文件',
|
||||
settingUp: '正在设置…',
|
||||
scanLocal: '扫描本地技能',
|
||||
search: '搜索共享技能…',
|
||||
potential: '建议的贡献',
|
||||
potentialHelp: 'Hermes 根据本地使用或有意义的改进判定为合格的技能。在你审核前不会共享。',
|
||||
noSuggestions: '目前没有本地技能符合自动资格规则。',
|
||||
browseLocal: count => `查看所有本地技能 (${count})`,
|
||||
browseLocalHelp: '手动选择并不表示 Hermes 使用过这些技能或已自动判定其合格。',
|
||||
ownerReview: '你的贡献草稿',
|
||||
ownerReviewHelp: '等待你审核的草稿,以及等待集体批准的提交。',
|
||||
noDrafts: '没有进行中的贡献草稿或提交。',
|
||||
prepare: '开始贡献',
|
||||
continueDraft: '继续编辑草稿',
|
||||
localOnly: '可从此设备共享',
|
||||
qualifiedLocally: 'Hermes 将此本地技能识别为可能的贡献。',
|
||||
qualificationFirst: organizationName =>
|
||||
`${organizationName ? `您的组织(${organizationName})` : '您的组织'}已启用 Collective Wisdom,此功能会自动发现所有团队成员的实用技能。恭喜!Hermes 检测到一项可能对您的团队有用的技能。`,
|
||||
qualificationReturning: 'Hermes 又检测到一项可能对您的团队有用的技能。',
|
||||
savedLocally: '私有草稿已保存在此设备上。',
|
||||
reviewExact: '查看详情',
|
||||
draftState: state => {
|
||||
const labels: Record<string, string> = {
|
||||
vetting: '服务器正在审核',
|
||||
ready: '等待你审核',
|
||||
owner_approved: '你已批准',
|
||||
publishing: '正在发布',
|
||||
pending_moderation: '等待集体管理员批准',
|
||||
changes_requested: '已要求修改'
|
||||
}
|
||||
return labels[state] || state.replaceAll('_', ' ')
|
||||
},
|
||||
serverScanPassed: '服务器扫描已通过',
|
||||
noDescription: '没有所有者撰写的描述。',
|
||||
managedInstalls: '个托管安装',
|
||||
close: '关闭',
|
||||
readEvery: '请阅读下方每个原始文件。批准将绑定到显示的三个准确哈希值。',
|
||||
editReview:
|
||||
'可在此编辑描述、SKILL.md 或声明式清单。保存会创建新的私有修订、重新运行扫描并返回新哈希值;不会改写你的本地源技能。',
|
||||
unsavedChanges: '这些更改尚未扫描。请先保存并重新扫描,再进行批准。',
|
||||
saveAndRescan: '保存更改并重新扫描',
|
||||
savingRevision: '正在保存并重新扫描…',
|
||||
resetChanges: '放弃编辑',
|
||||
reviewedHashes: '当前服务器已审核修订的哈希值',
|
||||
prepareTitle: '上传前审核本地包',
|
||||
prepareNotice:
|
||||
'在你明确提交到仅所有者可见的服务器审核前,这些字段会保留在此配置文件中。本地资格计数和原因永远不会包含在内。',
|
||||
ownerDescription: '所有者撰写的描述',
|
||||
systemSpecification: '系统规格(声明式元数据;不会安装依赖项)',
|
||||
localOverlay: '本地叠加层',
|
||||
cancel: '取消',
|
||||
submit: '提交草稿',
|
||||
submitting: '正在提交…',
|
||||
publishing: '正在发布…',
|
||||
approve: '批准准确内容并发布',
|
||||
decline: '拒绝',
|
||||
checkUpdates: count => `检查更新${count ? `(${count})` : ''}`,
|
||||
checking: '正在检查…',
|
||||
refreshShared: '刷新共享技能',
|
||||
refreshingShared: '正在刷新…',
|
||||
installReferenceLabel: '通过链接或技能 ID 安装',
|
||||
installReferencePlaceholder: '粘贴 Portal 链接、技能 ID 或 skill-id@vN',
|
||||
installReferenceHelp: 'Hermes 会先验证准确版本并显示兼容性计划,然后再安装。',
|
||||
reviewInstall: '检查安装',
|
||||
planningInstall: '验证中…',
|
||||
updateModeLabel: '后续更新',
|
||||
updateModeDefault: '使用组织默认设置',
|
||||
updateModeManual: '手动',
|
||||
updateModeAutomatic: '自动更新并通知',
|
||||
updateModeRequired: '必须更新',
|
||||
updateModeHelp: 'Gateway 会应用组织的当前策略。涉及安全的变更仍需你的批准。',
|
||||
updateModePlan: mode => `后续更新:${mode}`,
|
||||
activityReady: count => `${count} 条新通知`,
|
||||
decisionPublished: skill => `${skill} 已获批准,现在已与你的团队共享。`,
|
||||
decisionChanges: skill => `${skill} 需要修改后才能共享。`,
|
||||
decisionDeclined: skill => `${skill} 未获共享批准。`,
|
||||
decisionChanged: (skill, state) => `${skill} 的贡献状态已更改为${state}。`,
|
||||
installedNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} 已安装到此配置文件。`,
|
||||
updateNotice: (skill, version) => `${skill}${version ? ` ${version}` : ''} 有可用更新。`,
|
||||
newSkillNotice: skill => `${skill} 已与你的集体共享。`,
|
||||
archivedNotice: skill => `${skill} 已不再可用于新安装。`,
|
||||
takedownNotice: skill => `${skill} 已从集体中移除。`,
|
||||
markSeen: '全部标为已读',
|
||||
install: '安装…',
|
||||
uninstall: '卸载…',
|
||||
checkSkill: '检查此技能',
|
||||
updateAvailable: version => (version ? `可更新至 v${version}` : '有可用更新'),
|
||||
reviewUpdate: '查看更新',
|
||||
installed: (version, mode) => `已安装 v${version} · ${mode}`,
|
||||
confirmAction: action => `确认${action}`,
|
||||
acceptCompatibility: '我已审核并接受兼容性操作。',
|
||||
acceptSensitive: '我明确接受新的敏感要求。',
|
||||
preserveModified: '先将我修改过的副本保留为不受管理的分支。'
|
||||
},
|
||||
title: "技能",
|
||||
searchPlaceholder: "搜索技能和工具集...",
|
||||
enabledOf: "已启用 {enabled}/{total}",
|
||||
|
||||
@@ -49,37 +49,6 @@ function jsonFetchMock(body: unknown = { ok: true }) {
|
||||
}
|
||||
|
||||
describe("fetchJSON", () => {
|
||||
it("keeps native Wisdom mute choices scoped and preserves explicit unmute", async () => {
|
||||
const fetchMock = jsonFetchMock();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
setManagementProfile("other-profile");
|
||||
const controlId = "a".repeat(32);
|
||||
|
||||
await api.getWisdomMute("worker");
|
||||
await api.prepareWisdomMute("worker");
|
||||
await api.chooseWisdomMute(controlId, null, "worker");
|
||||
|
||||
expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
|
||||
"/api/wisdom/mute?profile=worker",
|
||||
"/api/wisdom/mute/prepare",
|
||||
"/api/wisdom/mute/choose",
|
||||
]);
|
||||
expect(JSON.parse(fetchMock.mock.calls[1][1]?.body as string)).toEqual({ profile: "worker" });
|
||||
expect(JSON.parse(fetchMock.mock.calls[2][1]?.body as string)).toEqual({
|
||||
control_id: controlId, duration: null, profile: "worker",
|
||||
});
|
||||
expect(fetchMock.mock.calls[2][1]?.method).toBe("POST");
|
||||
});
|
||||
|
||||
it("uses the dedicated profile-scoped local Wisdom entitlement endpoint", async () => {
|
||||
const fetchMock = jsonFetchMock({ entitled: false, scopes: [] });
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await api.getWisdomEntitlement("worker");
|
||||
|
||||
expect(fetchMock.mock.calls[0][0]).toBe("/api/wisdom/entitlement?profile=worker");
|
||||
});
|
||||
|
||||
it("tries the one-shot reload path for loopback 401s", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
+1637
-1862
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
parseWisdomManifest,
|
||||
wisdomManifestValidationError,
|
||||
wisdomSystemSpecificationValidationError
|
||||
} from './wisdom-manifest'
|
||||
|
||||
function validManifest() {
|
||||
return {
|
||||
schema_version: 1,
|
||||
name: 'deployment-checklist',
|
||||
requirements: {
|
||||
hermes: { minimum_version: '0.20.0' },
|
||||
platforms: [],
|
||||
architectures: [],
|
||||
model: { capabilities: [], minimum_context_window: null },
|
||||
tools: [],
|
||||
plugins: [],
|
||||
credentials: [],
|
||||
connections: [],
|
||||
filesystem: { read: [], write: [] },
|
||||
network: { destinations: [] },
|
||||
runtime: { shell: false, browser: false, code: false, sandbox: true },
|
||||
hardware: [],
|
||||
known_limitations: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Wisdom V1 manifest form contract', () => {
|
||||
it('parses the complete schema used by the structured form', () => {
|
||||
const manifest = parseWisdomManifest(JSON.stringify(validManifest()))
|
||||
expect(manifest.name).toBe('deployment-checklist')
|
||||
expect(manifest.requirements.runtime.sandbox).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects unknown fields instead of dropping them from a form rewrite', () => {
|
||||
const manifest = validManifest()
|
||||
const value = { ...manifest, unsupported: 'must not disappear' }
|
||||
expect(() => parseWisdomManifest(JSON.stringify(value))).toThrow(/unsupported fields/)
|
||||
})
|
||||
|
||||
it('never accepts automatic tool installation', () => {
|
||||
const manifest = {
|
||||
...validManifest(),
|
||||
requirements: {
|
||||
...validManifest().requirements,
|
||||
tools: [
|
||||
{
|
||||
name: 'terminal',
|
||||
minimum_version: null,
|
||||
auto_install: true,
|
||||
requires_admin: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
expect(() => parseWisdomManifest(JSON.stringify(manifest))).toThrow(/cannot request automatic installation/)
|
||||
})
|
||||
|
||||
it('reports form-editable semantic errors without losing the form shape', () => {
|
||||
const manifest = parseWisdomManifest(JSON.stringify(validManifest()))
|
||||
manifest.requirements.hermes.minimum_version = ''
|
||||
expect(wisdomSystemSpecificationValidationError(manifest.requirements)).toBe('Minimum Hermes version is required.')
|
||||
expect(wisdomManifestValidationError(JSON.stringify(manifest))).toBe('Minimum Hermes version is required.')
|
||||
})
|
||||
})
|
||||
@@ -1,236 +0,0 @@
|
||||
export interface WisdomToolRequirement {
|
||||
name: string
|
||||
minimum_version: string | null
|
||||
auto_install: false
|
||||
requires_admin: boolean
|
||||
}
|
||||
|
||||
export interface WisdomPluginRequirement {
|
||||
id: string
|
||||
minimum_version: string | null
|
||||
required: boolean
|
||||
}
|
||||
|
||||
export interface WisdomSystemSpecification extends Record<string, unknown> {
|
||||
hermes: { minimum_version: string }
|
||||
platforms: string[]
|
||||
architectures: string[]
|
||||
model: {
|
||||
capabilities: string[]
|
||||
minimum_context_window: number | null
|
||||
}
|
||||
tools: WisdomToolRequirement[]
|
||||
plugins: WisdomPluginRequirement[]
|
||||
credentials: string[]
|
||||
connections: string[]
|
||||
filesystem: { read: string[]; write: string[] }
|
||||
network: { destinations: string[] }
|
||||
runtime: { shell: boolean; browser: boolean; code: boolean; sandbox: boolean }
|
||||
hardware: string[]
|
||||
known_limitations: string[]
|
||||
}
|
||||
|
||||
export interface WisdomManifestV1 {
|
||||
schema_version: 1
|
||||
name: string
|
||||
requirements: WisdomSystemSpecification
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!isRecord(value)) throw new Error(`${label} must be an object`)
|
||||
return value
|
||||
}
|
||||
|
||||
function requireString(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string') throw new Error(`${label} must be text`)
|
||||
return value
|
||||
}
|
||||
|
||||
function requireNullableString(value: unknown, label: string): string | null {
|
||||
if (value === null) return null
|
||||
return requireString(value, label)
|
||||
}
|
||||
|
||||
function requireBoolean(value: unknown, label: string): boolean {
|
||||
if (typeof value !== 'boolean') throw new Error(`${label} must be true or false`)
|
||||
return value
|
||||
}
|
||||
|
||||
function requireStringList(value: unknown, label: string): string[] {
|
||||
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
||||
throw new Error(`${label} must be a list of text values`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function requireExactKeys(value: Record<string, unknown>, keys: string[], label: string): void {
|
||||
const expected = new Set(keys)
|
||||
const unexpected = Object.keys(value).filter(key => !expected.has(key))
|
||||
const missing = keys.filter(key => !(key in value))
|
||||
if (unexpected.length > 0) throw new Error(`${label} contains unsupported fields: ${unexpected.join(', ')}`)
|
||||
if (missing.length > 0) throw new Error(`${label} is missing fields: ${missing.join(', ')}`)
|
||||
}
|
||||
|
||||
export function parseWisdomSystemSpecification(value: unknown): WisdomSystemSpecification {
|
||||
const specification = requireRecord(value, 'System Specification')
|
||||
requireExactKeys(
|
||||
specification,
|
||||
[
|
||||
'hermes',
|
||||
'platforms',
|
||||
'architectures',
|
||||
'model',
|
||||
'tools',
|
||||
'plugins',
|
||||
'credentials',
|
||||
'connections',
|
||||
'filesystem',
|
||||
'network',
|
||||
'runtime',
|
||||
'hardware',
|
||||
'known_limitations'
|
||||
],
|
||||
'System Specification'
|
||||
)
|
||||
|
||||
const hermes = requireRecord(specification.hermes, 'Hermes requirement')
|
||||
requireExactKeys(hermes, ['minimum_version'], 'Hermes requirement')
|
||||
const model = requireRecord(specification.model, 'Model requirement')
|
||||
requireExactKeys(model, ['capabilities', 'minimum_context_window'], 'Model requirement')
|
||||
if (model.minimum_context_window !== null && typeof model.minimum_context_window !== 'number') {
|
||||
throw new Error('Minimum context window must be a number or blank')
|
||||
}
|
||||
const filesystem = requireRecord(specification.filesystem, 'Filesystem requirement')
|
||||
requireExactKeys(filesystem, ['read', 'write'], 'Filesystem requirement')
|
||||
const network = requireRecord(specification.network, 'Network requirement')
|
||||
requireExactKeys(network, ['destinations'], 'Network requirement')
|
||||
const runtime = requireRecord(specification.runtime, 'Runtime requirement')
|
||||
requireExactKeys(runtime, ['shell', 'browser', 'code', 'sandbox'], 'Runtime requirement')
|
||||
|
||||
if (!Array.isArray(specification.tools)) throw new Error('Tools must be a list')
|
||||
const tools = specification.tools.map((raw, index) => {
|
||||
const tool = requireRecord(raw, `Tool ${index + 1}`)
|
||||
requireExactKeys(tool, ['name', 'minimum_version', 'auto_install', 'requires_admin'], `Tool ${index + 1}`)
|
||||
if (tool.auto_install !== false) throw new Error(`Tool ${index + 1} cannot request automatic installation`)
|
||||
return {
|
||||
name: requireString(tool.name, `Tool ${index + 1} name`),
|
||||
minimum_version: requireNullableString(tool.minimum_version, `Tool ${index + 1} minimum version`),
|
||||
auto_install: false as const,
|
||||
requires_admin: requireBoolean(tool.requires_admin, `Tool ${index + 1} administrator requirement`)
|
||||
}
|
||||
})
|
||||
|
||||
if (!Array.isArray(specification.plugins)) throw new Error('Plugins must be a list')
|
||||
const plugins = specification.plugins.map((raw, index) => {
|
||||
const plugin = requireRecord(raw, `Plugin ${index + 1}`)
|
||||
requireExactKeys(plugin, ['id', 'minimum_version', 'required'], `Plugin ${index + 1}`)
|
||||
return {
|
||||
id: requireString(plugin.id, `Plugin ${index + 1} ID`),
|
||||
minimum_version: requireNullableString(plugin.minimum_version, `Plugin ${index + 1} minimum version`),
|
||||
required: requireBoolean(plugin.required, `Plugin ${index + 1} requirement`)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
hermes: { minimum_version: requireString(hermes.minimum_version, 'Minimum Hermes version') },
|
||||
platforms: requireStringList(specification.platforms, 'Platforms'),
|
||||
architectures: requireStringList(specification.architectures, 'Architectures'),
|
||||
model: {
|
||||
capabilities: requireStringList(model.capabilities, 'Model capabilities'),
|
||||
minimum_context_window: model.minimum_context_window === null ? null : Number(model.minimum_context_window)
|
||||
},
|
||||
tools,
|
||||
plugins,
|
||||
credentials: requireStringList(specification.credentials, 'Credentials'),
|
||||
connections: requireStringList(specification.connections, 'Connections'),
|
||||
filesystem: {
|
||||
read: requireStringList(filesystem.read, 'Readable paths'),
|
||||
write: requireStringList(filesystem.write, 'Writable paths')
|
||||
},
|
||||
network: { destinations: requireStringList(network.destinations, 'Network destinations') },
|
||||
runtime: {
|
||||
shell: requireBoolean(runtime.shell, 'Shell requirement'),
|
||||
browser: requireBoolean(runtime.browser, 'Browser requirement'),
|
||||
code: requireBoolean(runtime.code, 'Code execution requirement'),
|
||||
sandbox: requireBoolean(runtime.sandbox, 'Sandbox requirement')
|
||||
},
|
||||
hardware: requireStringList(specification.hardware, 'Hardware'),
|
||||
known_limitations: requireStringList(specification.known_limitations, 'Known limitations')
|
||||
}
|
||||
}
|
||||
|
||||
export function parseWisdomManifest(value: string): WisdomManifestV1 {
|
||||
const manifest = requireRecord(JSON.parse(value) as unknown, 'Manifest')
|
||||
requireExactKeys(manifest, ['schema_version', 'name', 'requirements'], 'Manifest')
|
||||
if (manifest.schema_version !== 1) throw new Error('Only manifest schema version 1 is supported')
|
||||
return {
|
||||
schema_version: 1,
|
||||
name: requireString(manifest.name, 'Skill name'),
|
||||
requirements: parseWisdomSystemSpecification(manifest.requirements)
|
||||
}
|
||||
}
|
||||
|
||||
function boundedTextError(value: string, label: string): string | null {
|
||||
if (!value.trim()) return `${label} is required.`
|
||||
if (new TextEncoder().encode(value).length > 512) return `${label} must be 512 UTF-8 bytes or fewer.`
|
||||
return null
|
||||
}
|
||||
|
||||
function listError(values: string[], label: string): string | null {
|
||||
if (values.length > 64) return `${label} can contain at most 64 entries.`
|
||||
for (const value of values) {
|
||||
const error = boundedTextError(value, `${label} entry`)
|
||||
if (error) return error
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function wisdomSystemSpecificationValidationError(specification: WisdomSystemSpecification): string | null {
|
||||
const errors = [
|
||||
boundedTextError(specification.hermes.minimum_version, 'Minimum Hermes version'),
|
||||
specification.model.minimum_context_window !== null &&
|
||||
(!Number.isSafeInteger(specification.model.minimum_context_window) ||
|
||||
specification.model.minimum_context_window < 1)
|
||||
? 'Minimum context window must be blank or a positive whole number.'
|
||||
: null,
|
||||
listError(specification.platforms, 'Platforms'),
|
||||
listError(specification.architectures, 'Architectures'),
|
||||
listError(specification.model.capabilities, 'Model capabilities'),
|
||||
listError(specification.credentials, 'Credentials'),
|
||||
listError(specification.connections, 'Connections'),
|
||||
listError(specification.filesystem.read, 'Readable paths'),
|
||||
listError(specification.filesystem.write, 'Writable paths'),
|
||||
listError(specification.network.destinations, 'Network destinations'),
|
||||
listError(specification.hardware, 'Hardware requirements'),
|
||||
listError(specification.known_limitations, 'Known limitations'),
|
||||
specification.tools.length > 64 ? 'Tools can contain at most 64 entries.' : null,
|
||||
specification.plugins.length > 64 ? 'Plugins can contain at most 64 entries.' : null
|
||||
]
|
||||
for (const [index, tool] of specification.tools.entries()) {
|
||||
errors.push(boundedTextError(tool.name, `Tool ${index + 1} name`))
|
||||
if (tool.minimum_version !== null) errors.push(boundedTextError(tool.minimum_version, `Tool ${index + 1} version`))
|
||||
}
|
||||
for (const [index, plugin] of specification.plugins.entries()) {
|
||||
errors.push(boundedTextError(plugin.id, `Plugin ${index + 1} ID`))
|
||||
if (plugin.minimum_version !== null) {
|
||||
errors.push(boundedTextError(plugin.minimum_version, `Plugin ${index + 1} version`))
|
||||
}
|
||||
}
|
||||
return errors.find((error): error is string => !!error) ?? null
|
||||
}
|
||||
|
||||
export function wisdomManifestValidationError(value: string): string | null {
|
||||
let manifest: WisdomManifestV1
|
||||
try {
|
||||
manifest = parseWisdomManifest(value)
|
||||
} catch (reason) {
|
||||
return reason instanceof Error ? reason.message : String(reason)
|
||||
}
|
||||
return (
|
||||
boundedTextError(manifest.name, 'Skill name') ?? wisdomSystemSpecificationValidationError(manifest.requirements)
|
||||
)
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createWisdomMuteController,
|
||||
type WisdomMuteControl,
|
||||
type WisdomMuteSnapshot,
|
||||
type WisdomMuteState
|
||||
} from '@hermes/shared'
|
||||
|
||||
const control: WisdomMuteControl = {
|
||||
id: 'a'.repeat(32),
|
||||
expires_at: 2000,
|
||||
organization_id: 'org',
|
||||
sync: null,
|
||||
mute: { org_id: 'org', revision: 3, duration: null, muted: false, forever: false, muted_until: null }
|
||||
}
|
||||
const snapshot: WisdomMuteSnapshot = { organization_id: 'org', gateway_available: true, mute: control.mute, sync: null }
|
||||
function setup() {
|
||||
const states: WisdomMuteState[] = []
|
||||
const deps = {
|
||||
prepare: vi.fn().mockResolvedValue(structuredClone(control)),
|
||||
read: vi.fn().mockResolvedValue(structuredClone(snapshot)),
|
||||
choose: vi.fn().mockResolvedValue(structuredClone(snapshot)),
|
||||
now: () => 1000_000,
|
||||
changed: (state: WisdomMuteState) => states.push(state)
|
||||
}
|
||||
return { deps, controller: createWisdomMuteController(deps), state: () => states.at(-1)! }
|
||||
}
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>(done => {
|
||||
resolve = done
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('native Wisdom mute state', () => {
|
||||
it('opening and selecting do not apply', async () => {
|
||||
const { controller, deps } = setup()
|
||||
await controller.refresh()
|
||||
controller.select('1_week')
|
||||
expect(deps.choose).not.toHaveBeenCalled()
|
||||
await controller.apply()
|
||||
expect(deps.choose).toHaveBeenCalledExactlyOnceWith(control.id, '1_week')
|
||||
})
|
||||
it('deduplicates submissions and retries the original choice after an uncertain response', async () => {
|
||||
const { controller, deps, state } = setup()
|
||||
await controller.refresh()
|
||||
const response = deferred<WisdomMuteSnapshot>()
|
||||
deps.choose.mockReturnValueOnce(response.promise)
|
||||
controller.select('1_day')
|
||||
const first = controller.apply()
|
||||
await controller.apply()
|
||||
controller.select('forever')
|
||||
expect(deps.choose).toHaveBeenCalledOnce()
|
||||
response.resolve({ ...snapshot, organization_id: 'wrong' })
|
||||
await first
|
||||
expect(state().retryingChoice).toBe(true)
|
||||
controller.select('forever')
|
||||
await controller.apply()
|
||||
expect(deps.choose.mock.calls).toEqual([
|
||||
[control.id, '1_day'],
|
||||
[control.id, '1_day']
|
||||
])
|
||||
})
|
||||
it('retains last known shared state through read failures', async () => {
|
||||
const { controller, deps, state } = setup()
|
||||
await controller.refresh()
|
||||
deps.read.mockRejectedValue(new Error('offline'))
|
||||
await controller.poll()
|
||||
expect(state().snapshot?.mute?.muted).toBe(false)
|
||||
expect(state().error).toBe(true)
|
||||
})
|
||||
it('ignores an old poll after a new explicit refresh', async () => {
|
||||
const { controller, deps, state } = setup()
|
||||
await controller.refresh()
|
||||
const old = deferred<WisdomMuteSnapshot>()
|
||||
deps.read.mockReturnValue(old.promise)
|
||||
const reading = controller.poll()
|
||||
await controller.refresh()
|
||||
old.resolve({ ...snapshot, mute: { ...control.mute, revision: 0 } })
|
||||
await reading
|
||||
expect(state().control?.mute.revision).toBe(3)
|
||||
})
|
||||
it('invalidates stale revision and expired menus without rebasing the choice', async () => {
|
||||
const { controller, deps, state } = setup()
|
||||
await controller.refresh()
|
||||
controller.select('forever')
|
||||
deps.read.mockResolvedValue({ ...snapshot, mute: { ...control.mute, revision: 4 } })
|
||||
await controller.poll()
|
||||
expect(state().control).toBeNull()
|
||||
await controller.apply()
|
||||
expect(deps.choose).not.toHaveBeenCalled()
|
||||
await controller.refresh()
|
||||
controller.select('forever')
|
||||
deps.now = () => 3000_000
|
||||
// A separate controller uses the new clock at construction.
|
||||
const expired = createWisdomMuteController(deps)
|
||||
await expired.refresh()
|
||||
expect(state().control).toBeNull()
|
||||
expect(state().error).toBe(true)
|
||||
})
|
||||
it('drops foreign organization state and never updates disposed surfaces', async () => {
|
||||
const { controller, deps, state } = setup()
|
||||
await controller.refresh()
|
||||
deps.read.mockResolvedValue({ ...snapshot, organization_id: 'other' })
|
||||
await controller.poll()
|
||||
expect(state().snapshot).toBeNull()
|
||||
const old = deferred<WisdomMuteControl>()
|
||||
deps.prepare.mockReturnValue(old.promise)
|
||||
const preparing = controller.refresh()
|
||||
controller.dispose()
|
||||
const before = state()
|
||||
old.resolve(control)
|
||||
await preparing
|
||||
expect(state()).toBe(before)
|
||||
})
|
||||
it('does not silently renew after a rejected choice; pending state stays visible', async () => {
|
||||
const { controller, deps, state } = setup()
|
||||
await controller.refresh()
|
||||
deps.choose.mockResolvedValue({
|
||||
...snapshot,
|
||||
sync: {
|
||||
mutation_id: control.id,
|
||||
requested_duration: 'forever',
|
||||
requested_until: null,
|
||||
request_expires_at: 9000,
|
||||
preference_sync: 'conflict'
|
||||
}
|
||||
})
|
||||
deps.prepare.mockRejectedValue(new Error('offline'))
|
||||
controller.select('forever')
|
||||
await controller.apply()
|
||||
expect(state().snapshot?.sync?.preference_sync).toBe('conflict')
|
||||
expect(state().control).toBeNull()
|
||||
expect(deps.choose).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { createWisdomSyncController, type WisdomSyncSnapshot } from '@hermes/shared'
|
||||
|
||||
const counts = { pending: 0, syncing: 0, retryable: 0, conflict: 0, uncertain: 0, waiting_for_receipt: 0 }
|
||||
const snapshot: WisdomSyncSnapshot = { delivery: counts, operation: { ...counts, retryable: 1 }, can_retry: true }
|
||||
function setup() {
|
||||
const deps = {
|
||||
read: vi.fn().mockResolvedValue(snapshot),
|
||||
retry: vi.fn().mockResolvedValue({ ...snapshot, can_retry: false }),
|
||||
changed: vi.fn()
|
||||
}
|
||||
return { deps, controller: createWisdomSyncController(deps) }
|
||||
}
|
||||
it('reads without retrying and requires current retryable state', async () => {
|
||||
const { deps, controller } = setup()
|
||||
await controller.retry()
|
||||
expect(deps.retry).not.toHaveBeenCalled()
|
||||
await controller.refresh()
|
||||
expect(deps.retry).not.toHaveBeenCalled()
|
||||
await controller.retry()
|
||||
await controller.retry()
|
||||
expect(deps.retry).toHaveBeenCalledOnce()
|
||||
})
|
||||
it('serializes reads/retries and discards responses after disposal', async () => {
|
||||
const { deps, controller } = setup()
|
||||
let done!: (value: WisdomSyncSnapshot) => void
|
||||
deps.read.mockReturnValue(
|
||||
new Promise(resolve => {
|
||||
done = resolve
|
||||
})
|
||||
)
|
||||
const first = controller.refresh()
|
||||
await controller.refresh()
|
||||
await controller.retry()
|
||||
expect(deps.read).toHaveBeenCalledOnce()
|
||||
controller.dispose()
|
||||
const changed = deps.changed.mock.calls.length
|
||||
done(snapshot)
|
||||
await first
|
||||
expect(deps.changed).toHaveBeenCalledTimes(changed)
|
||||
await controller.retry()
|
||||
expect(deps.retry).not.toHaveBeenCalled()
|
||||
})
|
||||
it('a failed read disables retries until a successful refresh', async () => {
|
||||
const { deps, controller } = setup()
|
||||
await controller.refresh()
|
||||
deps.read.mockRejectedValueOnce(new Error('private backend details'))
|
||||
await controller.refresh()
|
||||
expect(deps.changed.mock.lastCall![0].error).toBe(true)
|
||||
expect(JSON.stringify(deps.changed.mock.lastCall)).not.toContain('private backend details')
|
||||
await controller.retry()
|
||||
expect(deps.retry).not.toHaveBeenCalled()
|
||||
await controller.refresh()
|
||||
await controller.retry()
|
||||
expect(deps.retry).toHaveBeenCalledOnce()
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { MemoryRouter } from "react-router";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
profile: "eligible",
|
||||
probe: vi.fn(),
|
||||
setAfterTitle: vi.fn(),
|
||||
setEnd: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/api", () => ({ api: {
|
||||
getSkills: async () => [],
|
||||
getToolsets: async () => [],
|
||||
getWisdomEntitlement: state.probe,
|
||||
} }));
|
||||
vi.mock("@/contexts/useProfileScope", () => ({ useProfileScope: () => ({ profile: state.profile }) }));
|
||||
vi.mock("@/contexts/usePageHeader", () => ({ usePageHeader: () => state }));
|
||||
vi.mock("@/components/ToolsetConfigDrawer", () => ({ ToolsetConfigDrawer: () => null }));
|
||||
vi.mock("@/components/SkillEditorDialog", () => ({ SkillEditorDialog: () => null }));
|
||||
vi.mock("@/components/CollectiveWisdomPanel", () => ({ CollectiveWisdomPanel: () => null }));
|
||||
vi.mock("@/plugins", () => ({ PluginSlot: () => null }));
|
||||
|
||||
import SkillsPage from "./SkillsPage";
|
||||
import { en } from "@/i18n/en";
|
||||
|
||||
const pageView = () => <MemoryRouter><SkillsPage /></MemoryRouter>;
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.useRealTimers();
|
||||
state.profile = "eligible";
|
||||
state.probe.mockReset();
|
||||
});
|
||||
|
||||
it("does not reuse entitlement after switching away and back with pending probes", async () => {
|
||||
state.probe.mockResolvedValue({ entitled: true, expires_at: Date.now() / 1000 + 60 });
|
||||
const page = render(pageView());
|
||||
expect(await screen.findByRole("button", { name: en.skills.wisdom.tab })).toBeTruthy();
|
||||
|
||||
state.probe.mockImplementation(() => new Promise(() => {}));
|
||||
state.profile = "other";
|
||||
page.rerender(pageView());
|
||||
expect(screen.queryByRole("button", { name: en.skills.wisdom.tab })).toBeNull();
|
||||
state.profile = "eligible";
|
||||
page.rerender(pageView());
|
||||
expect(screen.queryByRole("button", { name: en.skills.wisdom.tab })).toBeNull();
|
||||
});
|
||||
|
||||
it("hides entitlement at expiry even while the next probe is pending", async () => {
|
||||
vi.useFakeTimers();
|
||||
state.probe.mockResolvedValueOnce({ entitled: true, expires_at: Date.now() / 1000 + 16 });
|
||||
state.probe.mockImplementation(() => new Promise(() => {}));
|
||||
await act(async () => { render(pageView()); });
|
||||
expect(screen.queryByRole("button", { name: en.skills.wisdom.tab })).not.toBeNull();
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(16_001); });
|
||||
expect(state.probe).toHaveBeenCalledTimes(2);
|
||||
expect(screen.queryByRole("button", { name: en.skills.wisdom.tab })).toBeNull();
|
||||
});
|
||||
+13
-102
@@ -42,7 +42,6 @@ import type {
|
||||
import { useProfileScope } from "@/contexts/useProfileScope";
|
||||
import { ToolsetConfigDrawer } from "@/components/ToolsetConfigDrawer";
|
||||
import { SkillEditorDialog } from "@/components/SkillEditorDialog";
|
||||
import { CollectiveWisdomPanel } from "@/components/CollectiveWisdomPanel";
|
||||
import { useToast } from "@nous-research/ui/hooks/use-toast";
|
||||
import { Toast } from "@nous-research/ui/ui/components/toast";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@nous-research/ui/ui/components/card";
|
||||
@@ -122,19 +121,6 @@ function toolsetIcon(
|
||||
return Wrench;
|
||||
}
|
||||
|
||||
type SkillPresentation = {
|
||||
name: string;
|
||||
description: string;
|
||||
editorial_name?: string;
|
||||
editorial_description?: string;
|
||||
};
|
||||
|
||||
const skillDisplayName = (skill: SkillPresentation) =>
|
||||
skill.editorial_name?.trim() || skill.name;
|
||||
|
||||
const skillDisplayDescription = (skill: SkillPresentation) =>
|
||||
skill.editorial_description?.trim() || skill.description;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Component */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -143,15 +129,8 @@ export default function SkillsPage() {
|
||||
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
||||
const [toolsets, setToolsets] = useState<ToolsetInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// Keep the response profile-keyed so a profile switch fails closed before
|
||||
// its first request settles. Positive responses are also bounded by exp.
|
||||
const [wisdomEntitlement, setWisdomEntitlement] = useState<{
|
||||
scope: { profile: string };
|
||||
entitled: boolean;
|
||||
expiresAt: number | null;
|
||||
} | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [view, setView] = useState<"skills" | "toolsets" | "hub" | "collective">("skills");
|
||||
const [view, setView] = useState<"skills" | "toolsets" | "hub">("skills");
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(null);
|
||||
const [togglingSkills, setTogglingSkills] = useState<Set<string>>(new Set());
|
||||
const [configToolset, setConfigToolset] = useState<ToolsetInfo | null>(null);
|
||||
@@ -172,8 +151,6 @@ export default function SkillsPage() {
|
||||
const {
|
||||
profile: selectedProfile,
|
||||
} = useProfileScope();
|
||||
// A return visit must not reuse the previous visit's successful probe.
|
||||
const wisdomScope = useMemo(() => ({ profile: selectedProfile }), [selectedProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
// Promise-chain shape: setState fires only inside async callbacks so the
|
||||
@@ -196,51 +173,6 @@ export default function SkillsPage() {
|
||||
};
|
||||
}, [selectedProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let pollTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let expiryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const schedulePoll = () => {
|
||||
pollTimer = setTimeout(probe, 15_000);
|
||||
};
|
||||
const probe = () => {
|
||||
api
|
||||
.getWisdomEntitlement(selectedProfile || undefined)
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
clearTimeout(expiryTimer);
|
||||
const expiresAt = typeof result.expires_at === "number" ? result.expires_at : null;
|
||||
const entitled =
|
||||
result.entitled === true && expiresAt !== null && expiresAt * 1000 > Date.now();
|
||||
setWisdomEntitlement({ scope: wisdomScope, entitled, expiresAt });
|
||||
if (entitled) {
|
||||
expiryTimer = setTimeout(
|
||||
() =>
|
||||
!cancelled &&
|
||||
setWisdomEntitlement({ scope: wisdomScope, entitled: false, expiresAt }),
|
||||
Math.min(expiresAt * 1000 - Date.now(), 2_147_483_647),
|
||||
);
|
||||
}
|
||||
})
|
||||
// A failed recheck replaces, rather than preserves, prior positive data.
|
||||
.catch(() =>
|
||||
!cancelled &&
|
||||
setWisdomEntitlement({ scope: wisdomScope, entitled: false, expiresAt: null }),
|
||||
)
|
||||
.finally(() => !cancelled && schedulePoll());
|
||||
};
|
||||
|
||||
probe();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(pollTimer);
|
||||
clearTimeout(expiryTimer);
|
||||
};
|
||||
}, [selectedProfile, wisdomScope]);
|
||||
const wisdomEntitled =
|
||||
wisdomEntitlement?.scope === wisdomScope && wisdomEntitlement.entitled === true;
|
||||
|
||||
/* ---- Toggle skill ---- */
|
||||
const handleToggleSkill = async (skill: SkillInfo) => {
|
||||
setTogglingSkills((prev) => new Set(prev).add(skill.name));
|
||||
@@ -339,8 +271,6 @@ export default function SkillsPage() {
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(lowerSearch) ||
|
||||
s.description.toLowerCase().includes(lowerSearch) ||
|
||||
skillDisplayName(s).toLowerCase().includes(lowerSearch) ||
|
||||
skillDisplayDescription(s).toLowerCase().includes(lowerSearch) ||
|
||||
(s.category ?? "").toLowerCase().includes(lowerSearch),
|
||||
);
|
||||
}, [skills, isSearching, lowerSearch]);
|
||||
@@ -348,18 +278,14 @@ export default function SkillsPage() {
|
||||
const activeSkills = useMemo(() => {
|
||||
if (isSearching) return [];
|
||||
if (!activeCategory)
|
||||
return [...skills].sort((a, b) =>
|
||||
skillDisplayName(a).localeCompare(skillDisplayName(b)),
|
||||
);
|
||||
return [...skills].sort((a, b) => a.name.localeCompare(b.name));
|
||||
return skills
|
||||
.filter((s) =>
|
||||
activeCategory === "__none__"
|
||||
? !s.category
|
||||
: s.category === activeCategory,
|
||||
)
|
||||
.sort((a, b) =>
|
||||
skillDisplayName(a).localeCompare(skillDisplayName(b)),
|
||||
);
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [skills, activeCategory, isSearching]);
|
||||
|
||||
const allCategories = useMemo(() => {
|
||||
@@ -489,24 +415,13 @@ export default function SkillsPage() {
|
||||
/>
|
||||
<PanelItem
|
||||
icon={Search}
|
||||
label={t.skills.wisdom.browseHub}
|
||||
label="Browse hub"
|
||||
active={view === "hub"}
|
||||
onClick={() => {
|
||||
setView("hub");
|
||||
setSearch("");
|
||||
}}
|
||||
/>
|
||||
{wisdomEntitled && (
|
||||
<PanelItem
|
||||
icon={Sparkles}
|
||||
label={t.skills.wisdom.tab}
|
||||
active={view === "collective"}
|
||||
onClick={() => {
|
||||
setView("collective");
|
||||
setSearch("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{view === "skills" &&
|
||||
@@ -651,8 +566,6 @@ export default function SkillsPage() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : view === "collective" && wisdomEntitled ? (
|
||||
<CollectiveWisdomPanel profile={selectedProfile || undefined} />
|
||||
) : view === "toolsets" ? (
|
||||
/* Toolsets grid */
|
||||
<>
|
||||
@@ -843,11 +756,11 @@ function SkillRow({
|
||||
skill.enabled ? "text-foreground" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{skillDisplayName(skill)}
|
||||
{skill.name}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-2">
|
||||
{skillDisplayDescription(skill) || noDescriptionLabel}
|
||||
{skill.description || noDescriptionLabel}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -1358,11 +1271,11 @@ function HubResultCard({
|
||||
type="button"
|
||||
className="flex-1 min-w-0 text-left"
|
||||
onClick={onOpen}
|
||||
aria-label={`Open ${skillDisplayName(result)}`}
|
||||
aria-label={`Open ${result.name}`}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 mb-0.5">
|
||||
<span className="font-mono-ui text-sm hover:underline">
|
||||
{skillDisplayName(result)}
|
||||
{result.name}
|
||||
</span>
|
||||
<Badge tone={trust.tone} className="text-xs">
|
||||
{trust.label}
|
||||
@@ -1377,7 +1290,7 @@ function HubResultCard({
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-text-secondary line-clamp-2">
|
||||
{skillDisplayDescription(result)}
|
||||
{result.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-1 mt-1">
|
||||
{result.tags.slice(0, 5).map((tag) => (
|
||||
@@ -1476,7 +1389,7 @@ function SkillDetailDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<Package className="h-4 w-4" />
|
||||
{skillDisplayName(result)}
|
||||
{result.name}
|
||||
<Badge tone={trust.tone} className="text-xs">
|
||||
{trust.label}
|
||||
</Badge>
|
||||
@@ -1490,15 +1403,13 @@ function SkillDetailDialog({
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Preview the SKILL.md source and run a security scan for{" "}
|
||||
{skillDisplayName(result)} before installing.
|
||||
Preview the SKILL.md source and run a security scan for {result.name}{" "}
|
||||
before installing.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
<p className="text-xs text-text-secondary">
|
||||
{skillDisplayDescription(result)}
|
||||
</p>
|
||||
<p className="text-xs text-text-secondary">{result.description}</p>
|
||||
<p className="text-xs font-mono text-text-tertiary truncate">
|
||||
{result.identifier}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user