+
);
}
@@ -170,15 +248,14 @@ function SkillRow({
skill,
currentUserId,
isAdmin,
- onToggleEnabled,
}: {
skill: Skill;
currentUserId: string | null;
isAdmin: boolean;
- onToggleEnabled: (enabled: boolean) => void;
}) {
const { t } = useI18n();
const setPublished = useSetSkillPublished();
+ const setPinned = useSetSkillPinned();
const deleteSkill = useDeleteCustomSkill();
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
@@ -216,6 +293,18 @@ function SkillRow({
}
}
+ async function handleTogglePinned() {
+ const next = !skill.pinned;
+ try {
+ await setPinned.mutateAsync({ skillName: skill.name, pinned: next });
+ toast.success(
+ next ? t.settings.skills.pinSuccess : t.settings.skills.unpinSuccess,
+ );
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : String(err));
+ }
+ }
+
return (
<>
-
@@ -223,7 +312,7 @@ function SkillRow({
{skill.name}
- {isBuiltin && isCustom && (
+ {isBuiltin && (
{t.settings.skills.badgeBuiltin}
@@ -261,13 +350,70 @@ function SkillRow({
{t.settings.skills.badgePublished}
)}
+ {isCustom && !isBuiltin && skill.source === "agent" && (
+
+ {t.settings.skills.badgeSourceAgent}
+
+ )}
+ {isCustom && !isBuiltin && skill.source === "upload" && (
+
+ {t.settings.skills.badgeSourceUpload}
+
+ )}
+ {skill.pinned && (
+
+ {t.settings.skills.badgePinned}
+
+ )}
{skill.description}
+ {skill.use_count !== undefined && (
+
+ {skill.use_count > 0
+ ? t.settings.skills.useCountInline.replace(
+ "{count}",
+ String(skill.use_count),
+ )
+ : t.settings.skills.useCountInlineZero}
+
+ )}
+ {canManage && (
+
+ )}
{canTogglePublish && (
)}
- onToggleEnabled(checked)}
- />
))}
diff --git a/frontend-web/src/components/workspace/workspace-nav-chat-list.tsx b/frontend-web/src/components/workspace/workspace-nav-chat-list.tsx
index 55c7147f..e913eba9 100644
--- a/frontend-web/src/components/workspace/workspace-nav-chat-list.tsx
+++ b/frontend-web/src/components/workspace/workspace-nav-chat-list.tsx
@@ -13,6 +13,7 @@ import {
} from "@/components/ui/sidebar";
import { getStoredAuth } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks";
+import { BotIcon, ListChecks, MessageSquarePlus, MessagesSquare, SparklesIcon, UsersIcon } from "lucide-react";
export function WorkspaceNavChatList() {
const { t } = useI18n();
diff --git a/frontend-web/src/core/i18n/locales/en-US.ts b/frontend-web/src/core/i18n/locales/en-US.ts
index e808cb97..fcc85c18 100644
--- a/frontend-web/src/core/i18n/locales/en-US.ts
+++ b/frontend-web/src/core/i18n/locales/en-US.ts
@@ -446,14 +446,66 @@ export const enUS: Translations = {
badgeFromOthersTitle: "Owned by user {user_id}",
badgePublished: "Published",
badgePublishedTitle: "Visible to all users",
+ badgeStale: "Stale",
+ badgeStaleTitle: "Idle for a long time — flagged by the curator",
+ badgeArchived: "Archived",
+ badgeArchivedTitle: "Archived — recoverable from the archive drawer",
+ badgePinned: "Pinned",
+ badgePinnedTitle: "Pinned — the curator will never auto-archive this skill",
+ badgeSourceAgent: "Agent-created",
+ badgeSourceAgentTitle: "Auto-generated by an agent during a conversation",
+ badgeSourceUpload: "Uploaded",
+ badgeSourceUploadTitle: "Manually uploaded or edited by a user",
+ tabAll: "All",
+ tabMine: "Mine",
+ entryStale: "Stale {count}",
+ entryArchived: "Archived {count}",
+ useCountInline: "Used {count} times",
+ useCountInlineZero: "Not used yet",
publishAction: "Publish",
unpublishAction: "Unpublish",
publishSuccess: "Skill published",
unpublishSuccess: "Skill unpublished",
+ pinAction: "Pin (prevent auto-archive)",
+ unpinAction: "Unpin",
+ pinSuccess: "Pinned",
+ unpinSuccess: "Unpinned",
deleteAction: "Delete skill",
deleteConfirm:
"Delete skill \"{name}\"? This cannot be undone and the skill will be removed from any agent that referenced it.",
deleteSuccess: "Skill deleted",
+ stateSheet: {
+ archivedTitle: "Archived skills",
+ archivedDescription:
+ "These skills have been archived by you or the curator and can be restored at any time.",
+ staleTitle: "Stale skills",
+ staleDescription:
+ "These skills have been idle for a while and were flagged by the curator. Using one again will auto-revive it; you can also act on them here.",
+ empty: "Nothing here",
+ actionRestore: "Restore",
+ actionArchiveNow: "Archive now",
+ actionView: "View",
+ actionPin: "Pin",
+ actionUnpin: "Unpin",
+ actionDelete: "Delete permanently",
+ moreMenu: "More",
+ archivedAt: "Archived {when}",
+ lastUsedAt: "Last used {when}",
+ neverUsed: "Never used",
+ restoreSuccess: "Restored",
+ archiveSuccess: "Archived",
+ pinSuccess: "Pinned",
+ unpinSuccess: "Unpinned",
+ },
+ viewDialog: {
+ title: "Skill details",
+ descriptionLabel: "Description",
+ stateLabel: "State",
+ sourceLabel: "Source",
+ useCountLabel: "Use count",
+ lastUsedLabel: "Last used",
+ close: "Close",
+ },
installDialog: {
title: "Install skill",
dropHint: "Drop a .zip or .skill archive here, or",
diff --git a/frontend-web/src/core/i18n/locales/types.ts b/frontend-web/src/core/i18n/locales/types.ts
index f3174e8a..230a8739 100644
--- a/frontend-web/src/core/i18n/locales/types.ts
+++ b/frontend-web/src/core/i18n/locales/types.ts
@@ -375,13 +375,63 @@ export interface Translations {
badgeFromOthersTitle: string;
badgePublished: string;
badgePublishedTitle: string;
+ badgeStale: string;
+ badgeStaleTitle: string;
+ badgeArchived: string;
+ badgeArchivedTitle: string;
+ badgePinned: string;
+ badgePinnedTitle: string;
+ badgeSourceAgent: string;
+ badgeSourceAgentTitle: string;
+ badgeSourceUpload: string;
+ badgeSourceUploadTitle: string;
+ tabAll: string;
+ tabMine: string;
+ entryStale: string;
+ entryArchived: string;
+ useCountInline: string;
+ useCountInlineZero: string;
publishAction: string;
unpublishAction: string;
publishSuccess: string;
unpublishSuccess: string;
+ pinAction: string;
+ unpinAction: string;
+ pinSuccess: string;
+ unpinSuccess: string;
deleteAction: string;
deleteConfirm: string;
deleteSuccess: string;
+ stateSheet: {
+ archivedTitle: string;
+ archivedDescription: string;
+ staleTitle: string;
+ staleDescription: string;
+ empty: string;
+ actionRestore: string;
+ actionArchiveNow: string;
+ actionView: string;
+ actionPin: string;
+ actionUnpin: string;
+ actionDelete: string;
+ moreMenu: string;
+ archivedAt: string;
+ lastUsedAt: string;
+ neverUsed: string;
+ restoreSuccess: string;
+ archiveSuccess: string;
+ pinSuccess: string;
+ unpinSuccess: string;
+ };
+ viewDialog: {
+ title: string;
+ descriptionLabel: string;
+ stateLabel: string;
+ sourceLabel: string;
+ useCountLabel: string;
+ lastUsedLabel: string;
+ close: string;
+ };
installDialog: {
title: string;
dropHint: string;
diff --git a/frontend-web/src/core/i18n/locales/zh-CN.ts b/frontend-web/src/core/i18n/locales/zh-CN.ts
index ae98d042..d35e8091 100644
--- a/frontend-web/src/core/i18n/locales/zh-CN.ts
+++ b/frontend-web/src/core/i18n/locales/zh-CN.ts
@@ -427,13 +427,63 @@ export const zhCN: Translations = {
badgeFromOthersTitle: "来自用户 {user_id}",
badgePublished: "已发布",
badgePublishedTitle: "该技能已对所有用户可见",
+ badgeStale: "过时",
+ badgeStaleTitle: "长期未使用,已被策展程序标记",
+ badgeArchived: "归档",
+ badgeArchivedTitle: "已被归档,可在归档抽屉中恢复",
+ badgePinned: "钉选",
+ badgePinnedTitle: "已钉选,策展程序不会自动归档此技能",
+ badgeSourceAgent: "Agent 创建",
+ badgeSourceAgentTitle: "在对话中由 Agent 自动生成",
+ badgeSourceUpload: "用户上传",
+ badgeSourceUploadTitle: "由用户手动上传或编辑",
+ tabAll: "全部",
+ tabMine: "我的",
+ entryStale: "过时 {count}",
+ entryArchived: "归档 {count}",
+ useCountInline: "调用 {count} 次",
+ useCountInlineZero: "尚未调用",
publishAction: "发布",
unpublishAction: "取消发布",
publishSuccess: "已发布",
unpublishSuccess: "已取消发布",
+ pinAction: "钉选(防止被自动归档)",
+ unpinAction: "取消钉选",
+ pinSuccess: "已钉选",
+ unpinSuccess: "已取消钉选",
deleteAction: "删除技能",
deleteConfirm: "确认删除技能 “{name}”?该操作不可恢复,且会从引用它的所有专家中自动移除。",
deleteSuccess: "技能已删除",
+ stateSheet: {
+ archivedTitle: "归档的技能",
+ archivedDescription: "这些技能已被你或策展程序归档,可随时恢复。",
+ staleTitle: "过时的技能",
+ staleDescription: "这些技能长期未被使用,已被策展程序标记。重新使用一次会自动恢复活跃,你也可以主动处理。",
+ empty: "暂无内容",
+ actionRestore: "恢复",
+ actionArchiveNow: "立即归档",
+ actionView: "查看详情",
+ actionPin: "钉选",
+ actionUnpin: "取消钉选",
+ actionDelete: "永久删除",
+ moreMenu: "更多操作",
+ archivedAt: "归档于 {when}",
+ lastUsedAt: "最近使用 {when}",
+ neverUsed: "尚未使用",
+ restoreSuccess: "已恢复",
+ archiveSuccess: "已归档",
+ pinSuccess: "已钉选",
+ unpinSuccess: "已取消钉选",
+ },
+ viewDialog: {
+ title: "技能详情",
+ descriptionLabel: "描述",
+ stateLabel: "状态",
+ sourceLabel: "来源",
+ useCountLabel: "调用次数",
+ lastUsedLabel: "最近使用",
+ close: "关闭",
+ },
installDialog: {
title: "安装技能",
dropHint: "拖拽 .zip 或 .skill 文件到此处,或者",
diff --git a/frontend-web/src/core/skills/api.ts b/frontend-web/src/core/skills/api.ts
index 6930bc86..e4b953e0 100644
--- a/frontend-web/src/core/skills/api.ts
+++ b/frontend-web/src/core/skills/api.ts
@@ -1,7 +1,7 @@
import { getBackendBaseURL } from "@/core/config";
import { apiFetch } from "@/core/api";
-import type { Skill } from "./type";
+import type { Skill, SkillState } from "./type";
export async function loadSkills() {
const skills = await apiFetch(`${getBackendBaseURL()}/api/skills`);
@@ -138,6 +138,42 @@ export async function setSkillPublished(
return response.json() as Promise