Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/agent-gateway/web/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,10 @@ export const translations: Record<Locale, Record<string, string>> = {
"在应用 Skills 目录中添加 skill.json、SKILL.md 或 README.md 文件",
"settings.skillsRescan": "重新扫描",
"settings.skillsSearch": "搜索技能...",
"settings.skillsInstalledSortLabel": "已安装技能排序",
"settings.skillsInstalledSortNameAsc": "名称 A→Z",
"settings.skillsInstalledSortNameDesc": "名称 Z→A",
"settings.skillsInstalledSortNewest": "安装时间 新→旧",
"settings.skillsNoMatch": "没有匹配「{filter}」的技能",
"settings.skillsStoreSearch": "搜索 ClawHub 技能",
"settings.skillsStoreSortMostDownloaded": "下载最多",
Expand Down Expand Up @@ -3673,6 +3677,10 @@ export const translations: Record<Locale, Record<string, string>> = {
"Add skill.json, SKILL.md, or README.md files to the Skills directory",
"settings.skillsRescan": "Rescan",
"settings.skillsSearch": "Search Skills...",
"settings.skillsInstalledSortLabel": "Sort installed skills",
"settings.skillsInstalledSortNameAsc": "Name A→Z",
"settings.skillsInstalledSortNameDesc": "Name Z→A",
"settings.skillsInstalledSortNewest": "Installation time: newest first",
"settings.skillsNoMatch": 'No skills matching "{filter}"',
"settings.skillsStoreSearch": "Search ClawHub Skills",
"settings.skillsStoreSortMostDownloaded": "Most Downloaded",
Expand Down
7 changes: 7 additions & 0 deletions crates/agent-gateway/web/src/lib/skills/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export type SkillSummary = {
baseDir: string;
/** true only when the backend verified LiveAgent ownership metadata */
builtIn?: boolean;
/** skill directory creation/modification time in epoch milliseconds */
installedAt?: number | null;
/** full README.md content for fallback skills that do not declare metadata */
inlineContent?: string;
inlineContentTruncated?: boolean;
Expand Down Expand Up @@ -123,6 +125,7 @@ type SystemManageSkillResponse = {
skillFile: string;
baseDir: string;
builtIn?: boolean;
installedAt?: number | null;
source?: SkillSourceMetadata | null;
}> | null;
invalid?: Array<{ path: string; error: string }> | null;
Expand Down Expand Up @@ -445,6 +448,10 @@ async function managedSkillListToDiscovery(
skillFile,
baseDir,
builtIn: raw.builtIn === true,
installedAt:
typeof raw.installedAt === "number" && Number.isFinite(raw.installedAt)
? raw.installedAt
: null,
source: normalizeSkillSourceMetadata(raw.source),
}),
);
Expand Down
59 changes: 59 additions & 0 deletions crates/agent-gateway/web/src/lib/skills/installedSort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { isAlwaysEnabledSkillName } from "./builtin";
import type { SkillSummary } from "./index";

export type InstalledSkillSort = "name-asc" | "name-desc" | "installed-desc";

export const DEFAULT_INSTALLED_SKILL_SORT: InstalledSkillSort = "name-asc";

export function isInstalledSkillSort(value: unknown): value is InstalledSkillSort {
return value === "name-asc" || value === "name-desc" || value === "installed-desc";
}

function installedAtValue(skill: SkillSummary) {
return typeof skill.installedAt === "number" && Number.isFinite(skill.installedAt)
? skill.installedAt
: null;
}

export function sortInstalledSkillItems<T>(
items: readonly T[],
sort: InstalledSkillSort,
selectedNames: ReadonlySet<string>,
getSkill: (item: T) => SkillSummary,
): T[] {
return [...items].sort((left, right) => {
const leftSkill = getSkill(left);
const rightSkill = getSkill(right);
const leftRank = isAlwaysEnabledSkillName(leftSkill.name)
? 0
: selectedNames.has(leftSkill.name)
? 1
: 2;
const rightRank = isAlwaysEnabledSkillName(rightSkill.name)
? 0
: selectedNames.has(rightSkill.name)
? 1
: 2;

if (leftRank !== rightRank) {
return leftRank - rightRank;
}

if (sort === "installed-desc") {
const leftInstalledAt = installedAtValue(leftSkill);
const rightInstalledAt = installedAtValue(rightSkill);
if (leftInstalledAt !== null || rightInstalledAt !== null) {
if (leftInstalledAt === null) return 1;
if (rightInstalledAt === null) return -1;
if (leftInstalledAt !== rightInstalledAt) return rightInstalledAt - leftInstalledAt;
}
}

const nameOrder = leftSkill.name.localeCompare(rightSkill.name);
if (nameOrder !== 0) {
return sort === "name-desc" ? -nameOrder : nameOrder;
}

return leftSkill.baseDir.localeCompare(rightSkill.baseDir);
});
}
Loading
Loading