fix(skills): share again under an existing skill bundle name as a new version (#29090)

* fix(skills): republish under an existing skill bundle name as a new version

Look up the caller's package by bundle name before packaging so a re-share
reuses its package id, and explain a name a teammate already holds instead of
a generic failure. Also stop classifying cloud request failures as filesystem
failures (same fix as #20848).

Fixes #20770

* fix(skills): keep the cloud request error importable without Electron

The runtime now classifies SkillCloudRequestError, and orcad has no Electron.

* fix(skills): map a held name only at lookup; leave classification to #20848

A finalize 409 after a signed-out preview is the user's own name, so it falls
back to the generic publish failure instead of blaming a teammate.

* fix(skills): say the skill name is in use instead of blaming a teammate

Orca has no team UI, so the copy states only what the server told us.
This commit is contained in:
Jinwoo Hong
2026-10-15 14:09:41 -04:00
committed by GitHub
parent 0c7efcb3ff
commit ebef2ea99c
24 changed files with 274 additions and 33 deletions
@@ -55,6 +55,9 @@ ORCA skills share --skill <selector> [--skill <selector> ...] --bundle-name <nam
enable the switch in the desktop app if they want this action.
- Orca stages one agent-published bundle at a time per host. If another publish is active,
wait for it to finish before retrying `agent_skill_sharing_busy`.
- Sharing again under a `--bundle-name` the account already published adds a new version to
that package. `agent_skill_bundle_name_taken` means the name is already in use; do not
retry, ask the user for a different name.
- Run the command in an Orca terminal on the machine that stores the skills. Forwarded WSL,
SSH, and paired-runtime invocations fail before discovery so Orca cannot read from the
wrong filesystem.
File diff suppressed because one or more lines are too long
+1
View File
@@ -50,6 +50,7 @@ function registerSharingHandlers(
const preparations = new SkillSharePreparationService(
join(app.getPath('userData'), 'skill-share-preparations'),
{
findOwnedPackageId: (name, request) => runtime.findOwnedSkillPackageId(name, request),
publishVersion: (request) => runtime.publishSkillPackageVersion(request),
createShare: (packageId, request) => runtime.createSkillPackageShare(packageId, request)
},
@@ -8,6 +8,7 @@ import type { AgentSkillShareRequest } from '../../shared/agent-skill-sharing-co
import { getDefaultSettings } from '../../shared/constants'
import type { DiscoveredSkill } from '../../shared/skills'
import { extractSkillBundleArchive } from '../skills/skill-bundle-extraction'
import { SkillCloudRequestError } from '../skills/skill-cloud-request-error'
import { OrcaRuntimeService } from './orca-runtime'
const mocks = vi.hoisted(() => ({ userDataPath: '/tmp' }))
@@ -61,6 +62,7 @@ function runtimeWithCloud(options: {
isEnabled: () => boolean
failPublish?: boolean
waitForAbort?: boolean
nameTaken?: boolean
}) {
const manifests: string[][] = []
let notifyStarted: (() => void) | null = null
@@ -116,7 +118,14 @@ function runtimeWithCloud(options: {
agentSkillSharingEnabled: options.isEnabled()
})
} as never)
runtime.setSkillCloudService({ publishVersion, createShare } as never)
const findOwnedPackageId = vi.fn(async () => {
if (options.nameTaken) {
throw new SkillCloudRequestError(409, 'skill_package_name_taken', 'taken')
}
return { status: 'ok' as const, value: null }
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: agent sharing calls only these three cloud methods.
runtime.setSkillCloudService({ findOwnedPackageId, publishVersion, createShare } as never)
return { runtime, publishVersion, createShare, manifests, started }
}
@@ -203,6 +212,20 @@ describe('agent skill sharing runtime', () => {
expect(await operationDirectories()).toEqual([])
})
it('tells the agent to pick another bundle name when the name is in use', async () => {
const alpha = await createSkill('alpha-id', 'alpha')
const { runtime, publishVersion } = runtimeWithCloud({ isEnabled: () => true, nameTaken: true })
await expect(
runtime.publishDiscoveredSkillsFromAgent(request(['alpha-id']), [alpha])
).rejects.toMatchObject({
code: 'agent_skill_bundle_name_taken',
message: expect.stringContaining('"team-skills"')
})
expect(publishVersion).not.toHaveBeenCalled()
expect(await operationDirectories()).toEqual([])
})
it('cancels an upload and cleans preparation files', async () => {
const alpha = await createSkill('alpha-id', 'alpha')
const controller = new AbortController()
+2
View File
@@ -20,6 +20,7 @@ import {
AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE,
AGENT_SKILL_SELECTOR_NOT_FOUND_CODE,
AGENT_SKILL_SHARING_BUSY_CODE,
AGENT_SKILL_BUNDLE_NAME_TAKEN_CODE,
AGENT_SKILL_SHARING_UNSUPPORTED_ENVIRONMENT_CODE
} from '../../../shared/agent-skill-sharing-contract'
import {
@@ -165,6 +166,7 @@ const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE,
AGENT_SKILL_SELECTOR_NOT_FOUND_CODE,
AGENT_SKILL_SHARING_BUSY_CODE,
AGENT_SKILL_BUNDLE_NAME_TAKEN_CODE,
AGENT_SKILL_SHARING_UNSUPPORTED_ENVIRONMENT_CODE,
SKILL_INSTALL_RPC_ERROR_CODE,
// Why: an owner conflict is a distinct client decision (reload the host, re-adopt,
@@ -48,6 +48,10 @@ export type RuntimeSkillCommandSurface = {
publishSkillPackageVersion(
request: SkillCloudPublishRequest
): Promise<SkillCloudOperation<SkillCloudVersion>>
findOwnedSkillPackageId(
name: string,
options: SkillCloudOptions
): ReturnType<SkillCloudService['findOwnedPackageId']>
createSkillPackageShare(
packageId: string,
request: SkillCloudOptions & { pinnedVersionId?: string; idempotencyKey?: string }
@@ -4,6 +4,7 @@ import {
isAgentSkillSharingEnabled
} from '../../shared/agent-skill-sharing-gate'
import {
AGENT_SKILL_BUNDLE_NAME_TAKEN_CODE,
AGENT_SKILL_NOT_SHAREABLE_CODE,
AGENT_SKILL_SHARING_BUSY_CODE,
AgentSkillSharingError,
@@ -11,6 +12,7 @@ import {
type AgentSkillShareRequest
} from '../../shared/agent-skill-sharing-contract'
import type { DiscoveredSkill } from '../../shared/skills'
import { SKILL_SHARE_NAME_TAKEN_ERROR } from '../../shared/skill-sharing-contract'
import { selectDiscoveredSkills } from '../skills/agent-skill-selection'
import { SkillSharePreparationService } from '../skills/skill-share-preparation-service'
import { RuntimeSkillInstallQueries } from './runtime-skill-install-queries'
@@ -71,6 +73,7 @@ export class RuntimeSkillCommands
const preparations = new SkillSharePreparationService(
join(this.userDataPath(), 'agent-skill-share-operations'),
{
findOwnedPackageId: (name, input) => cloud.findOwnedPackageId(name, input),
publishVersion: (input) => cloud.publishVersion(input),
createShare: (packageId, input) => cloud.createShare(packageId, input)
},
@@ -135,6 +138,14 @@ export class RuntimeSkillCommands
}
}
: published
} catch (error) {
if (error instanceof Error && error.message === SKILL_SHARE_NAME_TAKEN_ERROR) {
throw new AgentSkillSharingError(
AGENT_SKILL_BUNDLE_NAME_TAKEN_CODE,
`The skill bundle name "${request.bundleName}" is already in use. Choose a different --bundle-name.`
)
}
throw error
} finally {
signal?.removeEventListener('abort', cancel)
await preparations.dispose()
@@ -151,6 +162,9 @@ export class RuntimeSkillCommands
publishSkillPackageVersion(request: SkillCloudPublishRequest) {
return this.requireCloud().publishVersion(request)
}
findOwnedSkillPackageId(name: string, options: SkillCloudOptions) {
return this.requireCloud().findOwnedPackageId(name, options)
}
createSkillPackageShare(
packageId: string,
request: SkillCloudOptions & { pinnedVersionId?: string; idempotencyKey?: string }
@@ -0,0 +1,10 @@
// Why: kept apart from the request so the headless runtime can classify it without Electron.
export class SkillCloudRequestError extends Error {
constructor(
readonly statusCode: number,
readonly code: string,
message: string
) {
super(message)
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { skillCloudRequest, type SkillCloudRequestError } from './skill-cloud-request'
import { skillCloudRequest } from './skill-cloud-request'
import type { SkillCloudRequestError } from './skill-cloud-request-error'
vi.mock('electron', () => ({ app: { isPackaged: false } }))
+1 -10
View File
@@ -1,15 +1,6 @@
import { resolveArtifactCloudApiUrl } from '../artifacts/artifact-cloud-config'
import { createSkillCloudDeadline } from './skill-cloud-deadline'
export class SkillCloudRequestError extends Error {
constructor(
readonly statusCode: number,
readonly code: string,
message: string
) {
super(message)
}
}
import { SkillCloudRequestError } from './skill-cloud-request-error'
export async function skillCloudRequest<T>(input: {
apiUrl?: string
+19
View File
@@ -15,6 +15,7 @@ import { resolveArtifactCloudApiUrl } from '../artifacts/artifact-cloud-config'
import { runSkillCloudOperation } from './skill-cloud-auth'
import { uploadSkillPackageToSignedPolicy } from './skill-cloud-direct-upload'
import { skillCloudRequest } from './skill-cloud-request'
import { SkillCloudRequestError } from './skill-cloud-request-error'
import { startSkillPhaseOperation } from './skill-operation-observability'
const ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
@@ -229,6 +230,24 @@ export class SkillCloudService {
})
}
/** Id of the caller's live package with this name; null when the name is free. */
findOwnedPackageId(name: string, options: SkillCloudOptions) {
return this.withAuth(options, async (token, apiUrl) => {
const found = await skillCloudRequest<{ package: { id: string } }>({
apiUrl,
authToken: token,
path: `/v1/skill-packages/by-name/${encodeURIComponent(name)}`
}).catch((error: unknown) => {
// Why: any 404, so an API without this route still publishes the first share.
if (error instanceof SkillCloudRequestError && error.statusCode === 404) {
return null
}
throw error
})
return found?.package.id ?? null
})
}
listOwnedShares(
options: SkillCloudOptions
): Promise<SkillCloudOperation<SkillCloudOwnedShare[]>> {
@@ -0,0 +1,106 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { SKILL_SHARE_NAME_TAKEN_ERROR } from '../../shared/skill-sharing-contract'
import { SkillCloudService } from './skill-cloud-service'
import { SkillSharePreparationService } from './skill-share-preparation-service'
vi.mock('electron', () => ({ app: { isPackaged: false } }))
const API_URL = 'http://127.0.0.1:8787'
let root = ''
let source = ''
let lookups: string[] = []
function stubLookup(status: number, body: unknown): void {
vi.stubGlobal(
'fetch',
vi.fn(async (input: URL | RequestInfo) => {
lookups.push(new URL(String(input)).pathname)
return Response.json(body, { status })
})
)
}
function service(): SkillSharePreparationService {
const cloud = new SkillCloudService(root)
return new SkillSharePreparationService(join(root, 'preparations'), {
findOwnedPackageId: (name) => cloud.findOwnedPackageId(name, { apiUrl: API_URL }),
publishVersion: vi.fn(),
createShare: vi.fn()
})
}
beforeEach(async () => {
vi.stubEnv('ORCA_CLOUD_AUTH_TOKEN', 'desktop-e2e-token')
root = await mkdtemp(join(tmpdir(), 'orca-skill-share-name-'))
source = join(root, 'source')
await mkdir(source)
await writeFile(join(source, 'SKILL.md'), '---\nname: lw-skill\ndescription: d\n---\n# d\n')
lookups = []
})
afterEach(async () => {
vi.unstubAllEnvs()
vi.unstubAllGlobals()
await rm(root, { recursive: true, force: true })
})
describe('skill share package id by name', () => {
it('reuses the id of a package the caller already published under the name', async () => {
stubLookup(200, { package: { id: 'pkg_existing' } })
const preview = await service().prepare({ sourceDirectory: source, bundleName: 'lw-test' })
expect(preview.packageId).toBe('pkg_existing')
expect(lookups).toEqual(['/v1/skill-packages/by-name/lw-test'])
})
it('mints a new id when the name is free', async () => {
stubLookup(404, { code: 'skill_package_not_found' })
const preview = await service().prepare({ sourceDirectory: source, bundleName: 'lw-test' })
expect(preview.packageId).toMatch(/^[0-9a-f-]{36}$/)
})
it('mints a new id when signed out, leaving sign-in to publish', async () => {
vi.unstubAllEnvs()
stubLookup(200, { package: { id: 'pkg_existing' } })
const preview = await service().prepare({ sourceDirectory: source, bundleName: 'lw-test' })
expect(preview.packageId).toMatch(/^[0-9a-f-]{36}$/)
expect(lookups).toEqual([])
})
it('fails before packaging when the name is already in use', async () => {
stubLookup(409, { code: 'skill_package_name_taken', message: 'taken' })
await expect(
service().prepare({ sourceDirectory: source, bundleName: 'lw-test' })
).rejects.toThrow(SKILL_SHARE_NAME_TAKEN_ERROR)
})
it('fails the share on any other lookup failure instead of guessing a new id', async () => {
stubLookup(503, { code: 'skill_service_unavailable' })
await expect(
service().prepare({ sourceDirectory: source, bundleName: 'lw-test' })
).rejects.toMatchObject({ statusCode: 503 })
})
it('keeps an explicit package id without a lookup', async () => {
stubLookup(200, { package: { id: 'pkg_existing' } })
const preview = await service().prepare({
sourceDirectory: source,
bundleName: 'lw-test',
packageId: 'pkg_managed'
})
expect(preview.packageId).toBe('pkg_managed')
expect(lookups).toEqual([])
})
})
@@ -8,6 +8,10 @@ import { writeSkillInstallReceipt } from './skill-install-provenance'
const temporaryDirectories: string[] = []
async function nameIsFree() {
return { status: 'ok' as const, value: null }
}
async function createSource(): Promise<{ root: string; source: string }> {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-share-preparation-'))
temporaryDirectories.push(root)
@@ -74,7 +78,7 @@ describe('SkillSharePreparationService', () => {
})
const service = new SkillSharePreparationService(
join(root, 'preparations'),
{ publishVersion: vi.fn(), createShare: vi.fn() },
{ findOwnedPackageId: nameIsFree, publishVersion: vi.fn(), createShare: vi.fn() },
{ installStateDirectory: stateDirectory, platform: 'win32' }
)
@@ -113,7 +117,7 @@ describe('SkillSharePreparationService', () => {
})
const service = new SkillSharePreparationService(
join(root, 'preparations'),
{ publishVersion: vi.fn(), createShare: vi.fn() },
{ findOwnedPackageId: nameIsFree, publishVersion: vi.fn(), createShare: vi.fn() },
{ installStateDirectory: stateDirectory, platform: 'win32' }
)
@@ -133,7 +137,7 @@ describe('SkillSharePreparationService', () => {
})
const service = new SkillSharePreparationService(
preparationRoot,
{ publishVersion: vi.fn(), createShare: vi.fn() },
{ findOwnedPackageId: nameIsFree, publishVersion: vi.fn(), createShare: vi.fn() },
{ initializeRoot }
)
@@ -152,6 +156,7 @@ describe('SkillSharePreparationService', () => {
await mkdir(join(preparationRoot, 'abandoned'), { recursive: true })
await writeFile(join(preparationRoot, 'abandoned', 'package.tar.gz'), 'private bytes')
const service = new SkillSharePreparationService(preparationRoot, {
findOwnedPackageId: nameIsFree,
publishVersion: vi.fn(),
createShare: vi.fn()
})
@@ -164,6 +169,7 @@ describe('SkillSharePreparationService', () => {
it('enforces the preparation cap while archives are still being created', async () => {
const { root, source } = await createSource()
const service = new SkillSharePreparationService(join(root, 'preparations'), {
findOwnedPackageId: nameIsFree,
publishVersion: vi.fn(),
createShare: vi.fn()
})
@@ -193,6 +199,7 @@ describe('SkillSharePreparationService', () => {
value: { id: 'share_retry', url: 'https://share.test/skills/share/share_retry' }
})
const service = new SkillSharePreparationService(join(root, 'preparations'), {
findOwnedPackageId: nameIsFree,
publishVersion,
createShare
})
@@ -234,6 +241,7 @@ describe('SkillSharePreparationService', () => {
value: { id: 'share_retry', url: 'https://share.test/skills/share/share_retry' }
})
const service = new SkillSharePreparationService(join(root, 'preparations'), {
findOwnedPackageId: nameIsFree,
publishVersion,
createShare
})
@@ -1,15 +1,17 @@
import { randomUUID } from 'node:crypto'
import { mkdir, realpath, rm } from 'node:fs/promises'
import { join } from 'node:path'
import type {
SkillSharePreview,
SkillShareProgress,
SkillSharePublishInput,
SkillSharePublishOperation
import {
SKILL_SHARE_NAME_TAKEN_ERROR,
type SkillSharePreview,
type SkillShareProgress,
type SkillSharePublishInput,
type SkillSharePublishOperation
} from '../../shared/skill-sharing-contract'
import type { SkillCloudVersion } from '../../shared/skill-cloud-contract'
import { createSkillBundleArchive, type CreatedSkillBundle } from './skill-bundle-creation'
import type { SkillCloudService } from './skill-cloud-service'
import { SkillCloudRequestError } from './skill-cloud-request-error'
import { readSkillInstallReceipt } from './skill-install-provenance'
const PREPARATION_TTL_MS = 30 * 60 * 1000
@@ -66,7 +68,10 @@ export class SkillSharePreparationService {
constructor(
private readonly root: string,
private readonly cloud: Pick<SkillCloudService, 'createShare' | 'publishVersion'>,
private readonly cloud: Pick<
SkillCloudService,
'createShare' | 'findOwnedPackageId' | 'publishVersion'
>,
private readonly options: {
initializeRoot?: () => Promise<void>
installStateDirectory?: string
@@ -124,12 +129,13 @@ export class SkillSharePreparationService {
: source
})
)
const bundleName = input.bundleName ?? 'shared-skill'
const created = await createSkillBundleArchive({
sources,
bundleName: input.bundleName ?? 'shared-skill',
bundleName,
description: input.description,
archivePath: join(directory, 'package.tar.gz'),
packageId: input.packageId ?? randomUUID(),
packageId: input.packageId ?? (await this.packageIdForName(bundleName)),
versionId: randomUUID()
})
const value: Preparation = {
@@ -208,6 +214,17 @@ export class SkillSharePreparationService {
}
}
// Why: the cloud keeps one live package per name, and the id is baked into the archive.
private async packageIdForName(bundleName: string): Promise<string> {
const owned = await this.cloud.findOwnedPackageId(bundleName, {}).catch((error: unknown) => {
throw error instanceof SkillCloudRequestError && error.code === 'skill_package_name_taken'
? new Error(SKILL_SHARE_NAME_TAKEN_ERROR, { cause: error })
: error
})
// Why: signed out, the preview still works without an account; publish asks for sign-in.
return (owned.status === 'ok' && owned.value) || randomUUID()
}
cancel(preparationId: string): void {
this.preparations.get(preparationId)?.controller?.abort()
}
@@ -152,6 +152,24 @@ describe('SkillShareDialog', () => {
)
})
it('explains a skill name that is already in use', async () => {
setup(
[],
false,
[skill],
{},
Promise.reject(
new Error(
"Error invoking remote method 'skills:prepareShare': Error: skill-share-name-taken"
)
)
)
expect((await screen.findByRole('alert')).textContent).toBe(
'The skill name “private-skill” is already in use. Rename your skill to share it.'
)
})
it('releases a prepared archive when the dialog unmounts', async () => {
const { skills, unmount } = setup([])
await screen.findByRole('heading', { name: 'Share skill' })
@@ -4,9 +4,10 @@ import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogFooter } from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import type {
SkillSharePreview,
SkillShareProgress
import {
SKILL_SHARE_NAME_TAKEN_ERROR,
type SkillSharePreview,
type SkillShareProgress
} from '../../../../shared/skill-sharing-contract'
import type { ManagedSkillInstall } from '../../../../shared/skill-install-contract'
import type { DiscoveredSkill } from '../../../../shared/skills'
@@ -40,6 +41,17 @@ function operationError(status: string): string {
)
}
// Why: IPC wraps the main-process message, so match it as a substring.
function nameTakenError(cause: unknown, name: string): string | null {
return String(cause).includes(SKILL_SHARE_NAME_TAKEN_ERROR)
? translate(
'auto.components.skills.SkillShareDialog.nameTaken',
'The skill name “{{name}}” is already in use. Rename your skill to share it.',
{ name }
)
: null
}
export function SkillShareDialog({
skills,
skill,
@@ -78,6 +90,7 @@ export function SkillShareDialog({
setError(null)
setPublishingNewVersion(false)
setPreparing(true)
const bundleName = derivedBundleName(selectedSkills)
void (async () => {
let managedInstall: ManagedSkillInstall | null = null
try {
@@ -90,7 +103,7 @@ export function SkillShareDialog({
}
const nextPreview = await window.api.skills.prepareShare({
skillIds: selectedSkills.map((skill) => skill.id),
bundleName: derivedBundleName(selectedSkills),
bundleName,
...(managedInstall ? { packageId: managedInstall.packageId } : {})
})
retainedPreparationId = nextPreview.preparationId
@@ -119,10 +132,11 @@ export function SkillShareDialog({
console.warn('[skills] share preparation failed:', cause)
if (generation.current === current) {
setError(
translate(
'auto.components.skills.SkillShareDialog.prepareFailed',
'Could not prepare this skill for sharing.'
)
nameTakenError(cause, bundleName) ??
translate(
'auto.components.skills.SkillShareDialog.prepareFailed',
'Could not prepare this skill for sharing.'
)
)
}
})
+1
View File
@@ -4925,6 +4925,7 @@
"peopleRequired": "Select at least one teammate.",
"publishCancelled": "Upload cancelled. The prepared copy is still available to retry.",
"publishFailed": "Could not publish this skill. The prepared copy is still available to retry.",
"nameTaken": "The skill name “{{name}}” is already in use. Rename your skill to share it.",
"copied": "Share link copied",
"preparing": "Preparing preview…",
"ready": "Skill link ready",
+1
View File
@@ -4787,6 +4787,7 @@
"peopleRequired": "Selecciona al menos un compañero de equipo.",
"publishCancelled": "Subida cancelada. La copia preparada todavía está disponible para volver a intentarlo.",
"publishFailed": "No se pudo publicar esta habilidad. La copia preparada todavía está disponible para volver a intentarlo.",
"nameTaken": "El nombre de habilidad “{{name}}” ya está en uso. Cambia el nombre de tu habilidad para compartirla.",
"copied": "Compartir enlace copiado",
"preparing": "Preparando vista previa…",
"ready": "Enlace de habilidad listo",
+1
View File
@@ -4806,6 +4806,7 @@
"peopleRequired": "Sélectionnez au moins un membre de l'équipe.",
"publishCancelled": "Téléversement annulé. La copie préparée reste disponible pour une nouvelle tentative.",
"publishFailed": "Impossible de publier ce skill. La copie préparée reste disponible pour une nouvelle tentative.",
"nameTaken": "Le nom de skill « {{name}} » est déjà utilisé. Renommez votre skill pour le partager.",
"copied": "Lien de partage copié",
"preparing": "Préparation de l'aperçu…",
"ready": "Lien du skill prêt",
+1
View File
@@ -4787,6 +4787,7 @@
"peopleRequired": "少なくとも 1 人のチームメイトを選択してください。",
"publishCancelled": "アップロードがキャンセルされました。準備されたコピーは引き続き再試行できます。",
"publishFailed": "このスキルを公開できませんでした。準備されたコピーは引き続き再試行できます。",
"nameTaken": "スキル名「{{name}}」はすでに使用されています。共有するにはスキルの名前を変更してください。",
"copied": "共有リンクがコピーされました",
"preparing": "プレビューを準備しています…",
"ready": "スキルリンク準備完了",
+1
View File
@@ -4787,6 +4787,7 @@
"peopleRequired": "팀원을 한 명 이상 선택하세요.",
"publishCancelled": "업로드가 취소되었습니다. 준비된 사본은 아직 재시도할 수 있습니다.",
"publishFailed": "이 스킬을 게시할 수 없습니다. 준비된 사본은 아직 재시도할 수 있습니다.",
"nameTaken": "스킬 이름 “{{name}}”은(는) 이미 사용 중입니다. 공유하려면 스킬 이름을 변경하세요.",
"copied": "공유 링크가 복사되었습니다.",
"preparing": "미리보기 준비 중…",
"ready": "스킬링크 준비 완료",
+1
View File
@@ -4813,6 +4813,7 @@
"peopleRequired": "选择至少一名队友。",
"publishCancelled": "上传已取消。准备好的副本仍然可以重试。",
"publishFailed": "无法发布该技能。准备好的副本仍然可以重试。",
"nameTaken": "技能名称“{{name}}”已被使用。请重命名您的技能后再共享。",
"copied": "分享链接已复制",
"preparing": "正在准备预览...",
"ready": "技能链接已准备好",
@@ -8,6 +8,7 @@ export const AGENT_SKILL_SELECTOR_NOT_FOUND_CODE = 'agent_skill_selector_not_fou
export const AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE = 'agent_skill_selector_ambiguous'
export const AGENT_SKILL_SHARING_BUSY_CODE = 'agent_skill_sharing_busy'
export const AGENT_SKILL_NOT_SHAREABLE_CODE = 'agent_skill_not_shareable'
export const AGENT_SKILL_BUNDLE_NAME_TAKEN_CODE = 'agent_skill_bundle_name_taken'
export const AgentSkillShareRequestSchema = z
.object({
+3
View File
@@ -17,6 +17,9 @@ import type {
SkillBundleSelectedSkill
} from './skill-bundle-install-contract'
// Why: a plain message survives Electron IPC, which drops error classes and fields.
export const SKILL_SHARE_NAME_TAKEN_ERROR = 'skill-share-name-taken'
export type SkillSharePreview = {
preparationId: string
packageId: string