diff --git a/api/controllers/console/workspace/skills.py b/api/controllers/console/workspace/skills.py index 05730ac72bf..2c194a2a27d 100644 --- a/api/controllers/console/workspace/skills.py +++ b/api/controllers/console/workspace/skills.py @@ -493,14 +493,14 @@ class WorkspaceSkillDuplicateApi(Resource): @console_ns.route("/workspaces/current/skills//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( diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 3cda6c6e435..57d3a9ff43a 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -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 diff --git a/api/services/skill_management_service.py b/api/services/skill_management_service.py index d89be84067c..473d617eea9 100644 --- a/api/services/skill_management_service.py +++ b/api/services/skill_management_service.py @@ -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", diff --git a/api/tests/unit_tests/controllers/console/workspace/test_skills.py b/api/tests/unit_tests/controllers/console/workspace/test_skills.py index 7a61c10d355..060d78e7a21 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_skills.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_skills.py @@ -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: diff --git a/api/tests/unit_tests/services/test_skill_management_service.py b/api/tests/unit_tests/services/test_skill_management_service.py index 315e3b6b3b5..adfd2fa9b54 100644 --- a/api/tests/unit_tests/services/test_skill_management_service.py +++ b/api/tests/unit_tests/services/test_skill_management_service.py @@ -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( diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index 485b9de83ab..0c7739a14b8 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -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()) diff --git a/web/features/skills/__tests__/page.spec.tsx b/web/features/skills/__tests__/page.spec.tsx index 4a812e3686b..8149615e037 100644 --- a/web/features/skills/__tests__/page.spec.tsx +++ b/web/features/skills/__tests__/page.spec.tsx @@ -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 () => { diff --git a/web/features/skills/page.tsx b/web/features/skills/page.tsx index 5510253acaa..a60e8f24a8a 100644 --- a/web/features/skills/page.tsx +++ b/web/features/skills/page.tsx @@ -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({ )} - {(canEdit || canDelete || !!skill.latest_published_version_id) && ( -
-
- - $['skillManagement.moreActions'], { - name: skill.display_name, - })} - size="lg" - className="data-popup-open:bg-state-base-hover" +
+
+ + $['skillManagement.moreActions'], { + name: skill.display_name, + })} + size="lg" + className="data-popup-open:bg-state-base-hover" + > + + + } + /> + + {canEdit && ( + + + {tCommon(($) => $['operation.duplicate'])} + + )} + setIsExportOpen(true)}> + + {tCommon(($) => $['operation.export'])} + + {canDelete && ( + <> + + setIsDeleteOpen(true)} > - - - } - /> - - {canEdit && ( - - - {tCommon(($) => $['operation.duplicate'])} + + {tCommon(($) => $['operation.delete'])} - )} - {skill.latest_published_version_id && ( - - - {tCommon(($) => $['operation.export'])} - - )} - {canDelete && ( - <> - - setIsDeleteOpen(true)} - > - - {tCommon(($) => $['operation.delete'])} - - - )} - - -
+ + )} + +
- )} +
+ { + if (!exportMutation.isPending) setIsExportOpen(open) + }} + > + +
+ + {t(($) => $['skillManagement.exportDialog.title'])} + + + {t(($) => $['skillManagement.exportDialog.description'])} + +
+ + + {tCommon(($) => $['operation.cancel'])} + + + {tCommon(($) => $['operation.confirm'])} + + +
+
) diff --git a/web/i18n/ar-TN/skill.json b/web/i18n/ar-TN/skill.json index 24224f1cef6..3943c3e1483 100644 --- a/web/i18n/ar-TN/skill.json +++ b/web/i18n/ar-TN/skill.json @@ -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 مستورد.", diff --git a/web/i18n/de-DE/skill.json b/web/i18n/de-DE/skill.json index c5364396fa5..835b7df069d 100644 --- a/web/i18n/de-DE/skill.json +++ b/web/i18n/de-DE/skill.json @@ -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.", diff --git a/web/i18n/en-US/skill.json b/web/i18n/en-US/skill.json index f5da7162225..3bc3a904d67 100644 --- a/web/i18n/en-US/skill.json +++ b/web/i18n/en-US/skill.json @@ -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.", diff --git a/web/i18n/es-ES/skill.json b/web/i18n/es-ES/skill.json index 12c49f80fa6..839fd35d393 100644 --- a/web/i18n/es-ES/skill.json +++ b/web/i18n/es-ES/skill.json @@ -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.", diff --git a/web/i18n/fa-IR/skill.json b/web/i18n/fa-IR/skill.json index 7d6dcae5d08..82f13ebe6c0 100644 --- a/web/i18n/fa-IR/skill.json +++ b/web/i18n/fa-IR/skill.json @@ -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 وارد شده است.", diff --git a/web/i18n/fr-FR/skill.json b/web/i18n/fr-FR/skill.json index f47ac8cb14b..85901c4db20 100644 --- a/web/i18n/fr-FR/skill.json +++ b/web/i18n/fr-FR/skill.json @@ -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é.", diff --git a/web/i18n/hi-IN/skill.json b/web/i18n/hi-IN/skill.json index 5b1316d4965..0a2ac7307bc 100644 --- a/web/i18n/hi-IN/skill.json +++ b/web/i18n/hi-IN/skill.json @@ -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 आयातित.", diff --git a/web/i18n/id-ID/skill.json b/web/i18n/id-ID/skill.json index ed37dacd430..a3e71d763d9 100644 --- a/web/i18n/id-ID/skill.json +++ b/web/i18n/id-ID/skill.json @@ -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.", diff --git a/web/i18n/it-IT/skill.json b/web/i18n/it-IT/skill.json index 56c843d3f84..fdbfcd5648b 100644 --- a/web/i18n/it-IT/skill.json +++ b/web/i18n/it-IT/skill.json @@ -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.", diff --git a/web/i18n/ja-JP/skill.json b/web/i18n/ja-JP/skill.json index 670b6fb590b..b3a906712b1 100644 --- a/web/i18n/ja-JP/skill.json +++ b/web/i18n/ja-JP/skill.json @@ -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 がインポートされました。", diff --git a/web/i18n/ko-KR/skill.json b/web/i18n/ko-KR/skill.json index 5996e37586d..ea439d14f7a 100644 --- a/web/i18n/ko-KR/skill.json +++ b/web/i18n/ko-KR/skill.json @@ -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 가져왔습니다.", diff --git a/web/i18n/lo-LA/skill.json b/web/i18n/lo-LA/skill.json index 191bee70450..c55f8482e1f 100644 --- a/web/i18n/lo-LA/skill.json +++ b/web/i18n/lo-LA/skill.json @@ -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 ນໍາເຂົ້າ.", diff --git a/web/i18n/nl-NL/skill.json b/web/i18n/nl-NL/skill.json index 01ef4f359bf..335f11c4966 100644 --- a/web/i18n/nl-NL/skill.json +++ b/web/i18n/nl-NL/skill.json @@ -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.", diff --git a/web/i18n/pl-PL/skill.json b/web/i18n/pl-PL/skill.json index 77f5d0a5b4d..5e98b5153d7 100644 --- a/web/i18n/pl-PL/skill.json +++ b/web/i18n/pl-PL/skill.json @@ -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.", diff --git a/web/i18n/pt-BR/skill.json b/web/i18n/pt-BR/skill.json index aeb9e73fe5a..9b5b1d995e0 100644 --- a/web/i18n/pt-BR/skill.json +++ b/web/i18n/pt-BR/skill.json @@ -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.", diff --git a/web/i18n/ro-RO/skill.json b/web/i18n/ro-RO/skill.json index fad5ed9487e..9222e91cd18 100644 --- a/web/i18n/ro-RO/skill.json +++ b/web/i18n/ro-RO/skill.json @@ -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.", diff --git a/web/i18n/ru-RU/skill.json b/web/i18n/ru-RU/skill.json index 065f49bbd8c..6fc685dfaa7 100644 --- a/web/i18n/ru-RU/skill.json +++ b/web/i18n/ru-RU/skill.json @@ -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 импортирован.", diff --git a/web/i18n/sl-SI/skill.json b/web/i18n/sl-SI/skill.json index 8406a9cc568..4613ff865f5 100644 --- a/web/i18n/sl-SI/skill.json +++ b/web/i18n/sl-SI/skill.json @@ -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.", diff --git a/web/i18n/th-TH/skill.json b/web/i18n/th-TH/skill.json index 33abaab9fd6..bd38d237229 100644 --- a/web/i18n/th-TH/skill.json +++ b/web/i18n/th-TH/skill.json @@ -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 นำเข้า", diff --git a/web/i18n/tr-TR/skill.json b/web/i18n/tr-TR/skill.json index 927af752a9b..d41784ff135 100644 --- a/web/i18n/tr-TR/skill.json +++ b/web/i18n/tr-TR/skill.json @@ -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ı.", diff --git a/web/i18n/uk-UA/skill.json b/web/i18n/uk-UA/skill.json index cc6f7786614..83b83933b0f 100644 --- a/web/i18n/uk-UA/skill.json +++ b/web/i18n/uk-UA/skill.json @@ -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 імпортовано.", diff --git a/web/i18n/vi-VN/skill.json b/web/i18n/vi-VN/skill.json index fad3325b186..30b67e9a5b5 100644 --- a/web/i18n/vi-VN/skill.json +++ b/web/i18n/vi-VN/skill.json @@ -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.", diff --git a/web/i18n/zh-Hans/skill.json b/web/i18n/zh-Hans/skill.json index 4b31ab48ad6..ff85565bb84 100644 --- a/web/i18n/zh-Hans/skill.json +++ b/web/i18n/zh-Hans/skill.json @@ -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 已导入。", diff --git a/web/i18n/zh-Hant/skill.json b/web/i18n/zh-Hant/skill.json index 727c93bf1a7..fef432ad0ed 100644 --- a/web/i18n/zh-Hant/skill.json +++ b/web/i18n/zh-Hant/skill.json @@ -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 已匯入。",