feat: skill support duplicate and export use the latest draft (#42424)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
wangxiaolei
2026-09-17 09:52:25 +00:00
committed by GitHub
co-authored by autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
parent 99ba5a57de
commit bfd7ac015f
32 changed files with 248 additions and 107 deletions
+2 -2
View File
@@ -493,14 +493,14 @@ class WorkspaceSkillDuplicateApi(Resource):
@console_ns.route("/workspaces/current/skills/<string:skill_id>/export")
class WorkspaceSkillExportApi(Resource):
@console_ns.response(200, "Published Skill zip archive")
@console_ns.response(200, "Draft Skill zip archive")
@console_account_admission(
rbac_checks=[RBACCheck(RBACPermission.SKILL_VIEW, Workspace())],
)
@with_session(write=False)
def get(self, session: Session, request_context: RequestContext, skill_id: str):
try:
result = SkillManagementService(session=session).pull_published_archive(
result = SkillManagementService(session=session).export_draft_archive(
tenant_id=request_context.active_workspace_id, skill_id=skill_id
)
return send_file(
+1 -1
View File
@@ -12127,7 +12127,7 @@ Import a Skill zip package from multipart form field `file`.
| Code | Description |
| ---- | ----------- |
| 200 | Published Skill zip archive |
| 200 | Draft Skill zip archive |
### [PATCH] /workspaces/current/skills/{skill_id}/files
#### Parameters
+24 -39
View File
@@ -499,6 +499,13 @@ class SkillAssistActionResult:
plan: SkillAssistActionPlan
@dataclass(frozen=True, slots=True)
class DraftSkillArchive:
filename: str
mime_type: str
payload: bytes
@dataclass(frozen=True, slots=True)
class PublishedSkillArchive:
filename: str
@@ -2060,7 +2067,7 @@ class SkillManagementService:
}
def duplicate_skill(self, *, tenant_id: str, user_id: str, skill_id: str) -> dict[str, Any]:
"""Create a draft-only copy, preferring the latest published snapshot when present."""
"""Create an unpublished copy of the current saved draft."""
with self._session_scope() as session:
source = self._require_skill(session, tenant_id=tenant_id, skill_id=skill_id)
self._enforce_workspace_skill_limit(session, tenant_id=tenant_id)
@@ -2085,47 +2092,10 @@ class SkillManagementService:
skill_id=duplicate.id,
tags=self._skill_tags_by_id(session, tenant_id=tenant_id, skill_ids=[source.id]).get(source.id, []),
)
latest_version_id = source.latest_published_version_id
source_draft_files = list(
session.scalars(select(SkillDraftFile).where(SkillDraftFile.skill_id == source.id))
)
copied_draft_files = [self._copy_draft_file(file, skill_id=duplicate_id) for file in source_draft_files]
session.commit()
if latest_version_id is not None:
archive = self._load_version_archive(tenant_id=tenant_id, version_id=latest_version_id)
with self._session_scope() as session:
duplicate = self._require_skill(session, tenant_id=tenant_id, skill_id=duplicate_id)
duplicate_identity = (
duplicate.name,
duplicate.display_name,
duplicate.description,
duplicate.name_manually_edited,
)
files = self._draft_rows_from_archive_bytes(
tenant_id=tenant_id,
user_id=user_id,
skill=duplicate,
archive_bytes=archive,
)
# Parsing the published SKILL.md synchronizes metadata onto the
# supplied ORM object. A duplicate must retain its new identity,
# otherwise the reused request session autoflushes the source
# name and violates the tenant/name unique constraint.
(
duplicate.name,
duplicate.display_name,
duplicate.description,
duplicate.name_manually_edited,
) = duplicate_identity
else:
files = copied_draft_files
with self._session_scope() as session:
duplicate = self._require_skill(session, tenant_id=tenant_id, skill_id=duplicate_id)
if latest_version_id is not None:
for file in files:
file.skill_id = duplicate.id
files = [self._copy_draft_file(file, skill_id=duplicate_id) for file in source_draft_files]
for file in files:
if file.path == _SKILL_MD and file.content_text is not None:
synced_content = self._sync_skill_md_text(duplicate, file.content_text)
@@ -2251,6 +2221,20 @@ class SkillManagementService:
# follow-up action so restoring history cannot unexpectedly activate it.
return self.get_skill(tenant_id=tenant_id, skill_id=skill_id)
def export_draft_archive(self, *, tenant_id: str, skill_id: str) -> DraftSkillArchive:
"""Export the current saved draft without requiring or creating a published version."""
with self._session_scope() as session:
skill = self._require_skill(session, tenant_id=tenant_id, skill_id=skill_id)
files = list(session.scalars(select(SkillDraftFile).where(SkillDraftFile.skill_id == skill.id)))
# Snapshot the loaded rows so archive storage reads happen outside this scope.
draft_skill = Skill(
tenant_id=tenant_id, name=skill.name, display_name=skill.display_name, description=skill.description
)
draft_files = [self._copy_draft_file(file, skill_id=skill.id) for file in files]
filename = f"{skill.name}.zip"
archive_bytes, _ = self._build_archive_from_draft(skill=draft_skill, files=draft_files)
return DraftSkillArchive(filename=filename, mime_type="application/zip", payload=archive_bytes)
def pull_published_archive(self, *, tenant_id: str, skill_id: str) -> PublishedSkillArchive:
with self._session_scope() as session:
skill = self._require_skill(session, tenant_id=tenant_id, skill_id=skill_id)
@@ -4808,6 +4792,7 @@ class SkillManagementService:
__all__ = [
"DraftSkillArchive",
"PublishedSkillArchive",
"SkillAssistAttachmentPayload",
"SkillAssistHistoryMessagePayload",
@@ -503,7 +503,7 @@ def test_export_skill_returns_archive_response(app: Flask, request_context: Requ
api = WorkspaceSkillExportApi()
method = unwrap(api.get)
service = MagicMock()
service.pull_published_archive.return_value = MagicMock(
service.export_draft_archive.return_value = MagicMock(
payload=b"zip-bytes",
mime_type="application/zip",
filename="finance-sop.zip",
@@ -520,17 +520,17 @@ def test_export_skill_returns_archive_response(app: Flask, request_context: Requ
assert response.headers["Content-Disposition"].startswith("attachment;")
response.direct_passthrough = False
assert response.get_data() == b"zip-bytes"
service.pull_published_archive.assert_called_once_with(tenant_id="tenant-1", skill_id="skill-1")
service.export_draft_archive.assert_called_once_with(tenant_id="tenant-1", skill_id="skill-1")
def test_export_skill_maps_service_error(app: Flask, request_context: RequestContext) -> None:
api = WorkspaceSkillExportApi()
method = unwrap(api.get)
service = MagicMock()
service.pull_published_archive.side_effect = SkillManagementServiceError(
"skill_not_published",
"skill not published",
status_code=409,
service.export_draft_archive.side_effect = SkillManagementServiceError(
"skill_not_found",
"skill not found",
status_code=404,
)
with (
@@ -539,8 +539,8 @@ def test_export_skill_maps_service_error(app: Flask, request_context: RequestCon
):
payload, status = method(api, request_context, "skill-1")
assert status == 409
assert payload == {"code": "skill_not_published", "message": "skill not published"}
assert status == 404
assert payload == {"code": "skill_not_found", "message": "skill not found"}
def test_inner_api_pulls_published_skill_archive(app: Flask) -> None:
@@ -2493,7 +2493,7 @@ def test_apply_draft_file_operation_conflict_includes_current_file_version() ->
assert exc_info.value.details["current_file_content"] == skill_md["content"]
def test_duplicate_skill_copies_latest_published_content_without_history() -> None:
def test_duplicate_skill_copies_current_draft_without_history() -> None:
captured: dict[str, bytes] = {}
class CapturingToolFileManager(_FakeToolFileManager):
@@ -2543,6 +2543,19 @@ def test_duplicate_skill_copies_latest_published_content_without_history() -> No
)
service.publish_skill(tenant_id=TENANT, user_id=USER, skill_id=created["id"], payload=SkillPublishPayload())
service.replace_draft_tree(
tenant_id=TENANT,
user_id=USER,
skill_id=created["id"],
payload=SkillDraftTreePayload(
files=[
{"path": "SKILL.md", "content": _skill_md(body="# Latest draft body")},
{"path": "references", "kind": "directory"},
{"path": "references/draft.md", "content": "Draft policy."},
]
),
)
with patch("services.skill_management_service.storage.load_once", return_value=captured["archive"]):
duplicated = service.duplicate_skill(tenant_id=TENANT, user_id=USER, skill_id=created["id"])
@@ -2551,13 +2564,44 @@ def test_duplicate_skill_copies_latest_published_content_without_history() -> No
assert duplicated["tags"] == ["Finance"]
assert duplicated["latest_published_version_id"] is None
assert "name: finance-sop-copy" in duplicated["files"][0]["content"]
assert "# Published body" in duplicated["files"][0]["content"]
assert "# Latest draft body" in duplicated["files"][0]["content"]
references = next(file for file in duplicated["files"] if file["path"] == "references")
assert references["kind"] == "directory"
assert any(file["path"] == "references/policy.md" for file in duplicated["files"])
assert any(file["path"] == "references/draft.md" for file in duplicated["files"])
assert not any(file["path"] == "references/policy.md" for file in duplicated["files"])
assert service.list_versions(tenant_id=TENANT, skill_id=duplicated["id"]) == {"data": []}
@pytest.mark.parametrize("published", [False, True])
def test_export_skill_uses_current_draft(published: bool) -> None:
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop"))
if published:
service.publish_skill(tenant_id=TENANT, user_id=USER, skill_id=created["id"], payload=SkillPublishPayload())
service.replace_draft_tree(
tenant_id=TENANT,
user_id=USER,
skill_id=created["id"],
payload=SkillDraftTreePayload(
files=[
{"path": "SKILL.md", "content": _skill_md(body="# Latest draft body")},
{"path": "references", "kind": "directory"},
{"path": "references/draft.md", "content": "Draft policy."},
]
),
)
result = service.export_draft_archive(tenant_id=TENANT, skill_id=created["id"])
assert result.filename == "finance-sop.zip"
assert result.mime_type == "application/zip"
with zipfile.ZipFile(io.BytesIO(result.payload)) as archive:
assert set(archive.namelist()) == {"SKILL.md", "references/", "references/draft.md"}
assert "# Latest draft body" in archive.read("SKILL.md").decode()
assert archive.read("references/draft.md") == b"Draft policy."
assert len(service.list_versions(tenant_id=TENANT, skill_id=created["id"])["data"]) == int(published)
def test_duplicate_skill_does_not_copy_agent_references() -> None:
service = SkillManagementService(tool_file_manager=_FakeToolFileManager())
created = service.create_skill(
@@ -5593,7 +5593,7 @@ export const zGetWorkspacesCurrentSkillsBySkillIdExportPath = z.object({
})
/**
* Published Skill zip archive
* Draft Skill zip archive
*/
export const zGetWorkspacesCurrentSkillsBySkillIdExportResponse = z.record(z.string(), z.unknown())
+39 -2
View File
@@ -893,6 +893,14 @@ describe('SkillsPage', () => {
}),
)
await user.click(await screen.findByText('common.operation.export'))
const dialog = await screen.findByRole('alertdialog', {
name: 'skill.skillManagement.exportDialog.title',
})
expect(
within(dialog).getByText('skill.skillManagement.exportDialog.description'),
).toBeInTheDocument()
expect(mocks.exportSkillArchiveBlob).not.toHaveBeenCalled()
await user.click(within(dialog).getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(mocks.exportSkillArchiveBlob).toHaveBeenCalledWith('skill-1')
@@ -903,7 +911,7 @@ describe('SkillsPage', () => {
})
})
it('does not show export for an unpublished skill', async () => {
it('exports an unpublished skill from the card action menu', async () => {
const user = userEvent.setup()
mocks.skills = [createSkill({ latest_published_version_id: null })]
mocks.skillPages = [mocks.skills]
@@ -915,7 +923,36 @@ describe('SkillsPage', () => {
}),
)
expect(screen.queryByText('common.operation.export')).not.toBeInTheDocument()
await user.click(await screen.findByText('common.operation.export'))
const dialog = await screen.findByRole('alertdialog', {
name: 'skill.skillManagement.exportDialog.title',
})
expect(
within(dialog).getByText('skill.skillManagement.exportDialog.description'),
).toBeInTheDocument()
expect(mocks.exportSkillArchiveBlob).not.toHaveBeenCalled()
await user.click(within(dialog).getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(mocks.exportSkillArchiveBlob).toHaveBeenCalledWith('skill-1')
})
})
it('cancels export without downloading the skill', async () => {
const user = userEvent.setup()
renderSkillsPage()
await user.click(
await screen.findByRole('button', {
name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
}),
)
await user.click(await screen.findByText('common.operation.export'))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' }))
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument())
expect(mocks.exportSkillArchiveBlob).not.toHaveBeenCalled()
expect(mocks.downloadBlob).not.toHaveBeenCalled()
})
it('confirms deletion with the skill name and refreshes list data', async () => {
+78 -51
View File
@@ -417,6 +417,7 @@ function SkillCard({
const descriptionId = useId()
const draftStatusId = useId()
const [isDeleteOpen, setIsDeleteOpen] = useState(false)
const [isExportOpen, setIsExportOpen] = useState(false)
const duplicateMutation = useMutation(
consoleQuery.workspaces.current.skills.bySkillId.duplicate.post.mutationOptions(),
)
@@ -424,6 +425,7 @@ function SkillCard({
mutationFn: () => fetchSkillArchiveBlob(skill.id),
onSuccess: (blob) => {
downloadBlob({ data: blob, fileName: `${skill.name}.zip` })
setIsExportOpen(false)
},
onError: () => {
toast.error(tCommon(($) => $['operation.downloadFailed']))
@@ -526,60 +528,56 @@ function SkillCard({
</div>
</div>
)}
{(canEdit || canDelete || !!skill.latest_published_version_id) && (
<div className="pointer-events-none absolute top-[-0.5px] right-[-0.5px] flex h-16 w-30 items-start justify-end bg-[linear-gradient(67deg,var(--color-components-card-bg-alt-transparent)_0%,var(--color-components-card-bg-alt)_75%)] p-2 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 has-data-popup-open:opacity-100 [@media(hover:none)]:opacity-100">
<div className="pointer-events-none flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-lg backdrop-blur-xs group-focus-within:pointer-events-auto group-hover:pointer-events-auto has-data-popup-open:pointer-events-auto [@media(hover:none)]:pointer-events-auto">
<DropdownMenu modal={false}>
<DropdownMenuTrigger
render={
<IconButton
aria-label={t(($) => $['skillManagement.moreActions'], {
name: skill.display_name,
})}
size="lg"
className="data-popup-open:bg-state-base-hover"
<div className="pointer-events-none absolute top-[-0.5px] right-[-0.5px] flex h-16 w-30 items-start justify-end bg-[linear-gradient(67deg,var(--color-components-card-bg-alt-transparent)_0%,var(--color-components-card-bg-alt)_75%)] p-2 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 has-data-popup-open:opacity-100 [@media(hover:none)]:opacity-100">
<div className="pointer-events-none flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-lg backdrop-blur-xs group-focus-within:pointer-events-auto group-hover:pointer-events-auto has-data-popup-open:pointer-events-auto [@media(hover:none)]:pointer-events-auto">
<DropdownMenu modal={false}>
<DropdownMenuTrigger
render={
<IconButton
aria-label={t(($) => $['skillManagement.moreActions'], {
name: skill.display_name,
})}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<span aria-hidden className="i-ri-more-fill size-4.5" />
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} className="w-40">
{canEdit && (
<DropdownMenuItem className="gap-2" onClick={handleDuplicate}>
<span
aria-hidden
className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary"
/>
<span>{tCommon(($) => $['operation.duplicate'])}</span>
</DropdownMenuItem>
)}
<DropdownMenuItem className="gap-2" onClick={() => setIsExportOpen(true)}>
<span
aria-hidden
className="i-ri-download-2-line size-4 shrink-0 text-text-tertiary"
/>
<span>{tCommon(($) => $['operation.export'])}</span>
</DropdownMenuItem>
{canDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
className="gap-2"
onClick={() => setIsDeleteOpen(true)}
>
<span aria-hidden className="i-ri-more-fill size-4.5" />
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} className="w-40">
{canEdit && (
<DropdownMenuItem className="gap-2" onClick={handleDuplicate}>
<span
aria-hidden
className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary"
/>
<span>{tCommon(($) => $['operation.duplicate'])}</span>
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
<span>{tCommon(($) => $['operation.delete'])}</span>
</DropdownMenuItem>
)}
{skill.latest_published_version_id && (
<DropdownMenuItem className="gap-2" onClick={handleExport}>
<span
aria-hidden
className="i-ri-download-2-line size-4 shrink-0 text-text-tertiary"
/>
<span>{tCommon(($) => $['operation.export'])}</span>
</DropdownMenuItem>
)}
{canDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
className="gap-2"
onClick={() => setIsDeleteOpen(true)}
>
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
<span>{tCommon(($) => $['operation.delete'])}</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
<div className="pointer-events-none absolute top-26 right-3 left-3 flex h-6.5 min-w-0 items-start">
<div className="pointer-events-auto w-full min-w-0">
<SkillCardTags
@@ -590,6 +588,35 @@ function SkillCard({
/>
</div>
</div>
<AlertDialog
open={isExportOpen}
onOpenChange={(open) => {
if (!exportMutation.isPending) setIsExportOpen(open)
}}
>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
{t(($) => $['skillManagement.exportDialog.title'])}
</AlertDialogTitle>
<AlertDialogDescription className="system-md-regular text-text-secondary">
{t(($) => $['skillManagement.exportDialog.description'])}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton disabled={exportMutation.isPending}>
{tCommon(($) => $['operation.cancel'])}
</AlertDialogCancelButton>
<AlertDialogConfirmButton
tone="default"
loading={exportMutation.isPending}
onClick={handleExport}
>
{tCommon(($) => $['operation.confirm'])}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
<DeleteSkillDialog skill={skill} open={isDeleteOpen} onOpenChange={setIsDeleteOpen} />
</li>
)
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "لا تحتوي هذه الحزمة على SKILL.md. أضف الملف وقم باستيراده مرة أخرى.",
"skillManagement.errors.nameConflict": "توجد بالفعل مهارة اسمها \"{{name}}\". أعد تسميته إلى SKILL.md وقم باستيراده مرة أخرى.",
"skillManagement.errors.workspaceLimit": "وصلت مساحة العمل الخاصة بك إلى الحد الأقصى وهو 500 مهارة. احذف مهارة لإضافة مهارة جديدة.",
"skillManagement.exportDialog.description": "سيتم تصدير المسودة المحفوظة الحالية لمهارتك. إذا كانت لديك تغييرات غير محفوظة في المحرر، يُرجى حفظها قبل التصدير.",
"skillManagement.exportDialog.title": "تصدير المسودة المحفوظة الحالية",
"skillManagement.import": "استيراد",
"skillManagement.importFailed": "فشل استيراد المهارة.",
"skillManagement.importSuccess": "Skill مستورد.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Dieses Paket enthält kein SKILL.md. Fügen Sie die Datei hinzu und importieren Sie sie erneut.",
"skillManagement.errors.nameConflict": "Ein Skill mit dem Namen „{{name}}“ ist bereits vorhanden. Benennen Sie es in SKILL.md um und importieren Sie es erneut.",
"skillManagement.errors.workspaceLimit": "Ihr Arbeitsbereich hat die Grenze von 500 Fertigkeiten erreicht. Löschen Sie eine Fertigkeit, um eine neue hinzuzufügen.",
"skillManagement.exportDialog.description": "Dadurch wird der aktuell gespeicherte Entwurf Ihres Skills exportiert. Speichern Sie nicht gespeicherte Änderungen im Editor vor dem Export.",
"skillManagement.exportDialog.title": "Aktuell gespeicherten Entwurf exportieren",
"skillManagement.import": "Importieren",
"skillManagement.importFailed": "Der Skill konnte nicht importiert werden.",
"skillManagement.importSuccess": "Skill importiert.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "This package has no SKILL.md. Add the file and import it again.",
"skillManagement.errors.nameConflict": "A skill named \"{{name}}\" already exists. Rename it in SKILL.md and import again.",
"skillManagement.errors.workspaceLimit": "Your workspace has reached the limit of 500 skills. Delete a skill to add a new one.",
"skillManagement.exportDialog.description": "This will export the current saved draft of your skill. If you have unsaved changes in the editor, please save them before exporting.",
"skillManagement.exportDialog.title": "Export Current Saved Draft",
"skillManagement.import": "Import",
"skillManagement.importFailed": "Failed to import skill.",
"skillManagement.importSuccess": "Skill imported.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Este paquete no tiene SKILL.md. Agregue el archivo e impórtelo nuevamente.",
"skillManagement.errors.nameConflict": "Ya existe una habilidad denominada \"{{name}}\". Cambie el nombre a SKILL.md e impórtelo nuevamente.",
"skillManagement.errors.workspaceLimit": "Tu espacio de trabajo ha alcanzado el límite de 500 habilidades. Elimina una habilidad para agregar una nueva.",
"skillManagement.exportDialog.description": "Se exportará el borrador guardado actual de tu habilidad. Si tienes cambios sin guardar en el editor, guárdalos antes de exportar.",
"skillManagement.exportDialog.title": "Exportar el borrador guardado actual",
"skillManagement.import": "Importar",
"skillManagement.importFailed": "No se pudo importar la habilidad.",
"skillManagement.importSuccess": "Skill importado.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "این بسته فاقد SKILL.md است. فایل را اضافه کنید و دوباره آن را وارد کنید.",
"skillManagement.errors.nameConflict": "مهارتی به نام \"{{name}}\" از قبل وجود دارد. نام آن را در SKILL.md تغییر دهید و دوباره وارد کنید.",
"skillManagement.errors.workspaceLimit": "فضای کاری شما به حداکثر 500 مهارت رسیده است. برای افزودن مهارت جدید، مهارت را حذف کنید.",
"skillManagement.exportDialog.description": "با این کار از پیش‌نویس ذخیره‌شدهٔ فعلی مهارت شما خروجی گرفته می‌شود. اگر در ویرایشگر تغییرات ذخیره‌نشده‌ای دارید، پیش از خروجی گرفتن آن‌ها را ذخیره کنید.",
"skillManagement.exportDialog.title": "خروجی گرفتن از پیش‌نویس ذخیره‌شدهٔ فعلی",
"skillManagement.import": "وارد کردن",
"skillManagement.importFailed": "وارد کردن مهارت انجام نشد.",
"skillManagement.importSuccess": "Skill وارد شده است.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Ce package ne contient pas de SKILL.md. Ajoutez le fichier et importez-le à nouveau.",
"skillManagement.errors.nameConflict": "Une compétence nommée \"{{name}}\" existe déjà. Renommez-le en SKILL.md et importez-le à nouveau.",
"skillManagement.errors.workspaceLimit": "Votre espace de travail a atteint la limite de 500 compétences. Supprimez une compétence pour en ajouter une nouvelle.",
"skillManagement.exportDialog.description": "Le brouillon actuellement enregistré de votre compétence sera exporté. Si des modifications ne sont pas enregistrées dans l’éditeur, enregistrez-les avant l’exportation.",
"skillManagement.exportDialog.title": "Exporter le brouillon actuellement enregistré",
"skillManagement.import": "Importer",
"skillManagement.importFailed": "Échec de l'importation de la compétence.",
"skillManagement.importSuccess": "Skill importé.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "इस पैकेज में कोई SKILL.md नहीं है। फ़ाइल जोड़ें और इसे फिर से आयात करें।",
"skillManagement.errors.nameConflict": "\"{{name}}\" नामक कौशल पहले से मौजूद है। इसे SKILL.md में नाम बदलें और फिर से आयात करें।",
"skillManagement.errors.workspaceLimit": "आपका कार्यक्षेत्र 500 कौशल की सीमा तक पहुंच गया है। नया जोड़ने के लिए एक कौशल हटाएँ।",
"skillManagement.exportDialog.description": "इससे आपके कौशल का वर्तमान सहेजा गया ड्राफ़्ट निर्यात होगा। यदि संपादक में कोई बदलाव सहेजे नहीं गए हैं, तो निर्यात करने से पहले उन्हें सहेज लें।",
"skillManagement.exportDialog.title": "वर्तमान सहेजा गया ड्राफ़्ट निर्यात करें",
"skillManagement.import": "आयात करें",
"skillManagement.importFailed": "कौशल आयात करने में विफल.",
"skillManagement.importSuccess": "Skill आयातित.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Paket ini tidak memiliki SKILL.md. Tambahkan file dan impor lagi.",
"skillManagement.errors.nameConflict": "Skill bernama \"{{name}}\" sudah ada. Ubah namanya menjadi SKILL.md dan impor lagi.",
"skillManagement.errors.workspaceLimit": "Ruang kerja Anda telah mencapai batas 500 keterampilan. Hapus keterampilan untuk menambahkan keterampilan baru.",
"skillManagement.exportDialog.description": "Ini akan mengekspor draf keterampilan Anda yang tersimpan saat ini. Jika ada perubahan yang belum disimpan di editor, simpan terlebih dahulu sebelum mengekspor.",
"skillManagement.exportDialog.title": "Ekspor Draf Tersimpan Saat Ini",
"skillManagement.import": "Impor",
"skillManagement.importFailed": "Gagal mengimpor keterampilan.",
"skillManagement.importSuccess": "Skill diimpor.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Questo pacchetto non ha SKILL.md. Aggiungi il file e importalo di nuovo.",
"skillManagement.errors.nameConflict": "Esiste già una competenza denominata \"{{name}}\". Rinominalo in SKILL.md e importalo di nuovo.",
"skillManagement.errors.workspaceLimit": "La tua area di lavoro ha raggiunto il limite di 500 competenze. Elimina una competenza per aggiungerne una nuova.",
"skillManagement.exportDialog.description": "Verrà esportata la bozza attualmente salvata della tua competenza. Se ci sono modifiche non salvate nell’editor, salvale prima di esportare.",
"skillManagement.exportDialog.title": "Esporta la bozza attualmente salvata",
"skillManagement.import": "Importa",
"skillManagement.importFailed": "Impossibile importare la competenza.",
"skillManagement.importSuccess": "Skill importato.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "このスキルパッケージには SKILL.md がありません。SKILL.md を追加してから、もう一度インポートしてください。",
"skillManagement.errors.nameConflict": "「{{name}}」という名前のスキルはすでに存在します。SKILL.md で名前を変更してから、もう一度インポートしてください。",
"skillManagement.errors.workspaceLimit": "ワークスペースに追加できるスキルは 500 個までです。スキルを 1 つ削除してから、新しいスキルを追加してください。",
"skillManagement.exportDialog.description": "このスキルの保存済みの現在の下書きをエクスポートします。エディターに未保存の変更がある場合は、エクスポートする前に保存してください。",
"skillManagement.exportDialog.title": "保存済みの現在の下書きをエクスポート",
"skillManagement.import": "インポート",
"skillManagement.importFailed": "スキルのインポートに失敗しました。",
"skillManagement.importSuccess": "Skill がインポートされました。",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "이 패키지에는 SKILL.md가 없습니다. 파일을 추가하고 다시 가져오세요.",
"skillManagement.errors.nameConflict": "\"{{name}}\"라는 스킬이 이미 존재합니다. SKILL.md에서 이름을 바꾸고 다시 가져옵니다.",
"skillManagement.errors.workspaceLimit": "작업 영역이 스킬 한도인 500개에 도달했습니다. 새로운 스킬을 추가하려면 스킬을 삭제하세요.",
"skillManagement.exportDialog.description": "이 스킬의 현재 저장된 초안을 내보냅니다. 편집기에 저장하지 않은 변경 사항이 있으면 내보내기 전에 저장해 주세요.",
"skillManagement.exportDialog.title": "현재 저장된 초안 내보내기",
"skillManagement.import": "가져오기",
"skillManagement.importFailed": "스킬을 가져오지 못했습니다.",
"skillManagement.importSuccess": "Skill 가져왔습니다.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "ຊຸດນີ້ບໍ່ມີ SKILL.md. ເພີ່ມໄຟລ໌ແລະນໍາເຂົ້າມັນອີກເທື່ອຫນຶ່ງ.",
"skillManagement.errors.nameConflict": "ທັກສະທີ່ມີຊື່ວ່າ \"{{name}}\" ມີຢູ່ແລ້ວ. ປ່ຽນຊື່ມັນຢູ່ໃນ SKILL.md ແລະນໍາເຂົ້າອີກເທື່ອຫນຶ່ງ.",
"skillManagement.errors.workspaceLimit": "ພື້ນທີ່ເຮັດວຽກຂອງທ່ານຮອດຂີດຈຳກັດຂອງ 500 ທັກສະແລ້ວ. ລຶບທັກສະເພື່ອເພີ່ມອັນໃໝ່.",
"skillManagement.exportDialog.description": "ການດຳເນີນການນີ້ຈະສົ່ງອອກສະບັບຮ່າງຂອງທັກສະທີ່ບັນທຶກໄວ້ໃນປັດຈຸບັນ. ຖ້າມີການປ່ຽນແປງທີ່ຍັງບໍ່ໄດ້ບັນທຶກໃນຕົວແກ້ໄຂ, ກະລຸນາບັນທຶກກ່ອນສົ່ງອອກ.",
"skillManagement.exportDialog.title": "ສົ່ງອອກສະບັບຮ່າງທີ່ບັນທຶກໄວ້ໃນປັດຈຸບັນ",
"skillManagement.import": "ນຳເຂົ້າ",
"skillManagement.importFailed": "ລົ້ມເຫລວໃນການນໍາເຂົ້າທັກສະ.",
"skillManagement.importSuccess": "Skill ນໍາເຂົ້າ.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Dit pakket heeft geen SKILL.md. Voeg het bestand toe en importeer het opnieuw.",
"skillManagement.errors.nameConflict": "Er bestaat al een vaardigheid met de naam \"{{name}}\". Hernoem het in SKILL.md en importeer opnieuw.",
"skillManagement.errors.workspaceLimit": "Uw werkruimte heeft de limiet van 500 vaardigheden bereikt. Verwijder een vaardigheid om een ​​nieuwe toe te voegen.",
"skillManagement.exportDialog.description": "Hiermee exporteer je het huidige opgeslagen concept van je vaardigheid. Sla eventuele niet-opgeslagen wijzigingen in de editor op voordat je exporteert.",
"skillManagement.exportDialog.title": "Huidige opgeslagen concept exporteren",
"skillManagement.import": "Importeren",
"skillManagement.importFailed": "Kan vaardigheid niet importeren.",
"skillManagement.importSuccess": "Skill geïmporteerd.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Ten pakiet nie zawiera SKILL.md. Dodaj plik i zaimportuj go ponownie.",
"skillManagement.errors.nameConflict": "Umiejętność o nazwie „{{name}}” już istnieje. Zmień jego nazwę na SKILL.md i zaimportuj ponownie.",
"skillManagement.errors.workspaceLimit": "Twój obszar roboczy osiągnął limit 500 umiejętności. Usuń umiejętność, aby dodać nową.",
"skillManagement.exportDialog.description": "Zostanie wyeksportowany aktualnie zapisany szkic umiejętności. Jeśli w edytorze są niezapisane zmiany, zapisz je przed eksportem.",
"skillManagement.exportDialog.title": "Eksportuj aktualnie zapisany szkic",
"skillManagement.import": "Importuj",
"skillManagement.importFailed": "Nie udało się zaimportować umiejętności.",
"skillManagement.importSuccess": "Skill zaimportowano.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Este pacote não possui SKILL.md. Adicione o arquivo e importe-o novamente.",
"skillManagement.errors.nameConflict": "Uma habilidade chamada \"{{name}}\" já existe. Renomeie-o para SKILL.md e importe novamente.",
"skillManagement.errors.workspaceLimit": "Seu espaço de trabalho atingiu o limite de 500 habilidades. Exclua uma habilidade para adicionar uma nova.",
"skillManagement.exportDialog.description": "Isso exportará o rascunho salvo atual da sua habilidade. Se houver alterações não salvas no editor, salve-as antes de exportar.",
"skillManagement.exportDialog.title": "Exportar o rascunho salvo atual",
"skillManagement.import": "Importar",
"skillManagement.importFailed": "Falha ao importar habilidade.",
"skillManagement.importSuccess": "Skill importado.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Acest pachet nu are SKILL.md. Adăugați fișierul și importați-l din nou.",
"skillManagement.errors.nameConflict": "O abilitate numită „{{name}}” există deja. Redenumiți-l în SKILL.md și importați din nou.",
"skillManagement.errors.workspaceLimit": "Spațiul tău de lucru a atins limita de 500 de aptitudini. Ștergeți o abilitate pentru a adăuga una nouă.",
"skillManagement.exportDialog.description": "Se va exporta schița salvată curentă a abilității. Dacă există modificări nesalvate în editor, salvează-le înainte de export.",
"skillManagement.exportDialog.title": "Exportă schița salvată curentă",
"skillManagement.import": "Importați",
"skillManagement.importFailed": "Nu s-a putut importa abilitate.",
"skillManagement.importSuccess": "Skill importat.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "В этом пакете нет SKILL.md. Добавьте файл и импортируйте его еще раз.",
"skillManagement.errors.nameConflict": "Навык под названием «{{name}}» уже существует. Переименуйте его в SKILL.md и импортируйте снова.",
"skillManagement.errors.workspaceLimit": "Ваше рабочее пространство достигло предела в 500 навыков. Удалите навык, чтобы добавить новый.",
"skillManagement.exportDialog.description": "Будет экспортирован текущий сохранённый черновик навыка. Если в редакторе есть несохранённые изменения, сохраните их перед экспортом.",
"skillManagement.exportDialog.title": "Экспорт текущего сохранённого черновика",
"skillManagement.import": "Импортировать",
"skillManagement.importFailed": "Не удалось импортировать навык.",
"skillManagement.importSuccess": "Skill импортирован.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Ta paket nima SKILL.md. Dodajte datoteko in jo znova uvozite.",
"skillManagement.errors.nameConflict": "Spretnost z imenom \"{{name}}\" že obstaja. Preimenujte ga v SKILL.md in znova uvozite.",
"skillManagement.errors.workspaceLimit": "Vaš delovni prostor je dosegel omejitev 500 veščin. Izbrišite veščino, da dodate novo.",
"skillManagement.exportDialog.description": "Izvožen bo trenutno shranjeni osnutek vaše veščine. Če imate v urejevalniku neshranjene spremembe, jih pred izvozom shranite.",
"skillManagement.exportDialog.title": "Izvozi trenutno shranjeni osnutek",
"skillManagement.import": "Uvozi",
"skillManagement.importFailed": "Spretnosti ni bilo mogoče uvoziti.",
"skillManagement.importSuccess": "Skill uvoženo.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "แพ็คเกจนี้ไม่มี SKILL.md เพิ่มไฟล์และนำเข้าอีกครั้ง",
"skillManagement.errors.nameConflict": "มีทักษะชื่อ \"{{name}}\" อยู่แล้ว เปลี่ยนชื่อใน SKILL.md และนำเข้าอีกครั้ง",
"skillManagement.errors.workspaceLimit": "พื้นที่ทำงานของคุณถึงขีดจำกัด 500 ทักษะแล้ว ลบทักษะเพื่อเพิ่มทักษะใหม่",
"skillManagement.exportDialog.description": "การดำเนินการนี้จะส่งออกฉบับร่างของทักษะที่บันทึกไว้ปัจจุบัน หากมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึกในตัวแก้ไข โปรดบันทึกก่อนส่งออก",
"skillManagement.exportDialog.title": "ส่งออกฉบับร่างที่บันทึกไว้ปัจจุบัน",
"skillManagement.import": "นำเข้า",
"skillManagement.importFailed": "ไม่สามารถนำเข้าทักษะได้",
"skillManagement.importSuccess": "Skill นำเข้า",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Bu pakette SKILL.md yok. Dosyayı ekleyin ve tekrar içe aktarın.",
"skillManagement.errors.nameConflict": "\"{{name}}\" adlı bir beceri zaten mevcut. SKILL.md'de yeniden adlandırın ve tekrar içe aktarın.",
"skillManagement.errors.workspaceLimit": "Çalışma alanınız 500 beceri sınırına ulaştı. Yeni bir beceri eklemek için bir beceriyi silin.",
"skillManagement.exportDialog.description": "Becerinizin geçerli kayıtlı taslağı dışa aktarılacaktır. Düzenleyicide kaydedilmemiş değişiklikler varsa dışa aktarmadan önce kaydedin.",
"skillManagement.exportDialog.title": "Geçerli Kayıtlı Taslağı Dışa Aktar",
"skillManagement.import": "İçe Aktar",
"skillManagement.importFailed": "Beceri içe aktarılamadı.",
"skillManagement.importSuccess": "Skill içe aktarıldı.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Цей пакет не має SKILL.md. Додайте файл і імпортуйте його знову.",
"skillManagement.errors.nameConflict": "Уміння під назвою \"{{name}}\" уже існує. Перейменуйте його в SKILL.md і імпортуйте знову.",
"skillManagement.errors.workspaceLimit": "Ваша робоча область досягла ліміту в 500 навичок. Видаліть навик, щоб додати новий.",
"skillManagement.exportDialog.description": "Буде експортовано поточну збережену чернетку навички. Якщо в редакторі є незбережені зміни, збережіть їх перед експортом.",
"skillManagement.exportDialog.title": "Експорт поточної збереженої чернетки",
"skillManagement.import": "Імпортувати",
"skillManagement.importFailed": "Не вдалося імпортувати навички.",
"skillManagement.importSuccess": "Skill імпортовано.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "Gói này không có SKILL.md. Thêm tệp và nhập lại.",
"skillManagement.errors.nameConflict": "Một kỹ năng có tên \"{{name}}\" đã tồn tại. Đổi tên nó thành SKILL.md và nhập lại.",
"skillManagement.errors.workspaceLimit": "Không gian làm việc của bạn đã đạt đến giới hạn 500 kỹ năng. Xóa một kỹ năng để thêm một kỹ năng mới.",
"skillManagement.exportDialog.description": "Thao tác này sẽ xuất bản nháp hiện đã lưu của kỹ năng. Nếu có thay đổi chưa lưu trong trình chỉnh sửa, hãy lưu trước khi xuất.",
"skillManagement.exportDialog.title": "Xuất bản nháp hiện đã lưu",
"skillManagement.import": "Nhập",
"skillManagement.importFailed": "Không thể nhập kỹ năng.",
"skillManagement.importSuccess": "Skill đã nhập.",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "此 Skill 包中没有 SKILL.md。请添加该文件后重新导入。",
"skillManagement.errors.nameConflict": "名为「{{name}}」的 Skill 已存在。请在 SKILL.md 中修改名称后重新导入。",
"skillManagement.errors.workspaceLimit": "工作空间最多可添加 500 个 Skill。请先删除一个 Skill,再添加新 Skill。",
"skillManagement.exportDialog.description": "此操作将导出此 Skill 当前已保存的草稿。如果编辑器中有未保存的更改,请先保存后再导出。",
"skillManagement.exportDialog.title": "导出当前已保存的草稿",
"skillManagement.import": "导入",
"skillManagement.importFailed": "Skill 导入失败。",
"skillManagement.importSuccess": "Skill 已导入。",
+2
View File
@@ -227,6 +227,8 @@
"skillManagement.errors.missingSkillMd": "這個包沒有SKILL.md。新增檔案並再次匯入。",
"skillManagement.errors.nameConflict": "名為「{{name}}」的技能已存在。將其重新命名為SKILL.md並再次匯入。",
"skillManagement.errors.workspaceLimit": "您的工作區已達到 500 項技能的限制。刪除一項技能以新增技能。",
"skillManagement.exportDialog.description": "此操作將匯出此 Skill 目前已儲存的草稿。如果編輯器中有尚未儲存的變更,請先儲存再匯出。",
"skillManagement.exportDialog.title": "匯出目前已儲存的草稿",
"skillManagement.import": "匯入",
"skillManagement.importFailed": "Skill 匯入失敗。",
"skillManagement.importSuccess": "Skill 已匯入。",