Skip to content

Commit 1748d6f

Browse files
authored
Merge pull request Stack-Cairn#182 from AlphaCatMeow/feat/skills-hub-installed-sort
feat(skills-hub): 添加已安装技能排序与重排动画
2 parents 5aa4611 + aea759f commit 1748d6f

13 files changed

Lines changed: 1959 additions & 857 deletions

File tree

crates/agent-gateway/web/src/i18n/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,10 @@ export const translations: Record<Locale, Record<string, string>> = {
17501750
"在应用 Skills 目录中添加 skill.json、SKILL.md 或 README.md 文件",
17511751
"settings.skillsRescan": "重新扫描",
17521752
"settings.skillsSearch": "搜索技能...",
1753+
"settings.skillsInstalledSortLabel": "已安装技能排序",
1754+
"settings.skillsInstalledSortNameAsc": "名称 A→Z",
1755+
"settings.skillsInstalledSortNameDesc": "名称 Z→A",
1756+
"settings.skillsInstalledSortNewest": "安装时间 新→旧",
17531757
"settings.skillsNoMatch": "没有匹配「{filter}」的技能",
17541758
"settings.skillsStoreSearch": "搜索 ClawHub 技能",
17551759
"settings.skillsStoreSortMostDownloaded": "下载最多",
@@ -3673,6 +3677,10 @@ export const translations: Record<Locale, Record<string, string>> = {
36733677
"Add skill.json, SKILL.md, or README.md files to the Skills directory",
36743678
"settings.skillsRescan": "Rescan",
36753679
"settings.skillsSearch": "Search Skills...",
3680+
"settings.skillsInstalledSortLabel": "Sort installed skills",
3681+
"settings.skillsInstalledSortNameAsc": "Name A→Z",
3682+
"settings.skillsInstalledSortNameDesc": "Name Z→A",
3683+
"settings.skillsInstalledSortNewest": "Installation time: newest first",
36763684
"settings.skillsNoMatch": 'No skills matching "{filter}"',
36773685
"settings.skillsStoreSearch": "Search ClawHub Skills",
36783686
"settings.skillsStoreSortMostDownloaded": "Most Downloaded",

crates/agent-gateway/web/src/lib/skills/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export type SkillSummary = {
2424
baseDir: string;
2525
/** true only when the backend verified LiveAgent ownership metadata */
2626
builtIn?: boolean;
27+
/** skill directory creation/modification time in epoch milliseconds */
28+
installedAt?: number | null;
2729
/** full README.md content for fallback skills that do not declare metadata */
2830
inlineContent?: string;
2931
inlineContentTruncated?: boolean;
@@ -123,6 +125,7 @@ type SystemManageSkillResponse = {
123125
skillFile: string;
124126
baseDir: string;
125127
builtIn?: boolean;
128+
installedAt?: number | null;
126129
source?: SkillSourceMetadata | null;
127130
}> | null;
128131
invalid?: Array<{ path: string; error: string }> | null;
@@ -445,6 +448,10 @@ async function managedSkillListToDiscovery(
445448
skillFile,
446449
baseDir,
447450
builtIn: raw.builtIn === true,
451+
installedAt:
452+
typeof raw.installedAt === "number" && Number.isFinite(raw.installedAt)
453+
? raw.installedAt
454+
: null,
448455
source: normalizeSkillSourceMetadata(raw.source),
449456
}),
450457
);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { isAlwaysEnabledSkillName } from "./builtin";
2+
import type { SkillSummary } from "./index";
3+
4+
export type InstalledSkillSort = "name-asc" | "name-desc" | "installed-desc";
5+
6+
export const DEFAULT_INSTALLED_SKILL_SORT: InstalledSkillSort = "name-asc";
7+
8+
export function isInstalledSkillSort(value: unknown): value is InstalledSkillSort {
9+
return value === "name-asc" || value === "name-desc" || value === "installed-desc";
10+
}
11+
12+
function installedAtValue(skill: SkillSummary) {
13+
return typeof skill.installedAt === "number" && Number.isFinite(skill.installedAt)
14+
? skill.installedAt
15+
: null;
16+
}
17+
18+
export function sortInstalledSkillItems<T>(
19+
items: readonly T[],
20+
sort: InstalledSkillSort,
21+
selectedNames: ReadonlySet<string>,
22+
getSkill: (item: T) => SkillSummary,
23+
): T[] {
24+
return [...items].sort((left, right) => {
25+
const leftSkill = getSkill(left);
26+
const rightSkill = getSkill(right);
27+
const leftRank = isAlwaysEnabledSkillName(leftSkill.name)
28+
? 0
29+
: selectedNames.has(leftSkill.name)
30+
? 1
31+
: 2;
32+
const rightRank = isAlwaysEnabledSkillName(rightSkill.name)
33+
? 0
34+
: selectedNames.has(rightSkill.name)
35+
? 1
36+
: 2;
37+
38+
if (leftRank !== rightRank) {
39+
return leftRank - rightRank;
40+
}
41+
42+
if (sort === "installed-desc") {
43+
const leftInstalledAt = installedAtValue(leftSkill);
44+
const rightInstalledAt = installedAtValue(rightSkill);
45+
if (leftInstalledAt !== null || rightInstalledAt !== null) {
46+
if (leftInstalledAt === null) return 1;
47+
if (rightInstalledAt === null) return -1;
48+
if (leftInstalledAt !== rightInstalledAt) return rightInstalledAt - leftInstalledAt;
49+
}
50+
}
51+
52+
const nameOrder = leftSkill.name.localeCompare(rightSkill.name);
53+
if (nameOrder !== 0) {
54+
return sort === "name-desc" ? -nameOrder : nameOrder;
55+
}
56+
57+
return leftSkill.baseDir.localeCompare(rightSkill.baseDir);
58+
});
59+
}

0 commit comments

Comments
 (0)