diff --git a/src/commands/knowledge-hub/cloud-enabled.tsx b/src/commands/knowledge-hub/cloud-enabled.tsx
index 30333cb8f..ba235738b 100644
--- a/src/commands/knowledge-hub/cloud-enabled.tsx
+++ b/src/commands/knowledge-hub/cloud-enabled.tsx
@@ -1,15 +1,148 @@
-import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { Box, Text, Dialog } from '@anthropic/ink';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Box, Pane, Tab, Tabs, Text, useInput, useTabHeaderFocus, useTerminalFocus } from '@anthropic/ink';
+import { SearchBox } from '../../components/SearchBox.js';
import { SelectMulti } from '../../components/CustomSelect/SelectMulti.js';
+import { useSearchInput } from '../../hooks/useSearchInput.js';
import {
listFavoriteItems,
loadFavoriteItem,
unloadFavoriteItem,
+ type FavoriteItemType,
type FavoriteItemWithStatus,
} from '../../costrict/favorite/favorite.js';
import { useSetAppState } from '../../state/AppState.js';
import { getOriginalCwd } from '../../bootstrap/state.js';
import type { LocalJSXCommandCall } from '../../types/command.js';
+import { useKeybinding } from '../../keybindings/useKeybinding.js';
+import { getCoStrictBaseURL } from '../../costrict/provider/auth.js';
+
+type TabId = FavoriteItemType;
+
+const TAB_CONFIG: { id: TabId; title: string }[] = [
+ { id: 'skill', title: 'Skills' },
+ { id: 'agent', title: 'Agents' },
+ { id: 'command', title: 'Commands' },
+ { id: 'mcp', title: 'MCP' },
+];
+
+function formatScore(score: number | undefined): string | undefined {
+ if (score === undefined) return undefined;
+ if (score < 1000) return String(score);
+ const k = score / 1000;
+ // 如果是整数k,不显示小数;否则保留1位小数
+ if (Number.isInteger(k)) return `${k}k`;
+ return `${k.toFixed(1)}k`;
+}
+
+function TabContent({
+ items,
+ onChange,
+ onCancel,
+}: {
+ items: FavoriteItemWithStatus[];
+ onChange: (selectedSlugs: string[]) => void;
+ onCancel: () => void;
+}): React.ReactNode {
+ const { headerFocused, focusHeader } = useTabHeaderFocus();
+ const isTerminalFocused = useTerminalFocus();
+ const [isSearchMode, setIsSearchMode] = useState(false);
+ const {
+ query: searchQuery,
+ setQuery: setSearchQuery,
+ cursorOffset,
+ } = useSearchInput({
+ isActive: isSearchMode,
+ onExit: () => setIsSearchMode(false),
+ onCancel: () => {
+ setIsSearchMode(false);
+ setSearchQuery('');
+ },
+ });
+
+ // Capture '/' to enter search mode
+ useInput(
+ (input, _key) => {
+ if (!isSearchMode && !headerFocused && input === '/') {
+ setIsSearchMode(true);
+ }
+ },
+ { isActive: !isSearchMode && !headerFocused },
+ );
+
+ const filteredItems = useMemo(() => {
+ if (!searchQuery) return items;
+ const lower = searchQuery.toLowerCase();
+ return items.filter(
+ item =>
+ item.name.toLowerCase().includes(lower) ||
+ item.description.toLowerCase().includes(lower),
+ );
+ }, [items, searchQuery]);
+
+ const options = useMemo(
+ () =>
+ filteredItems.map(item => ({
+ label: (
+ <>
+ {item.name}
+ {item.score !== undefined && (
+ · 分值:{formatScore(item.score)}
+ )}
+ >
+ ),
+ value: item.slug,
+ description: item.description || undefined,
+ })),
+ [filteredItems],
+ );
+
+ const defaultValue = useMemo(
+ () => filteredItems.filter(item => item.status === 'Active').map(item => item.slug),
+ [filteredItems],
+ );
+
+ if (items.length === 0) {
+ return (
+
+ No favorites in this category
+
+ );
+ }
+
+ return (
+
+ {isSearchMode && (
+
+ )}
+ {filteredItems.length === 0 && searchQuery && (
+
+ No favorites match "{searchQuery}"
+
+ )}
+ {filteredItems.length > 0 && (
+
+ )}
+ {!isSearchMode && items.length > 0 && (
+
+ type / to search, space/enter to toggle
+
+ )}
+
+ );
+}
function CloudEnabledMenu({
onDone,
@@ -18,9 +151,26 @@ function CloudEnabledMenu({
}): React.ReactNode {
const setAppState = useSetAppState();
const [items, setItems] = useState(null);
+ const [activeTab, setActiveTab] = useState('skill');
const previousSlugs = useRef>(new Set());
const isFirstChange = useRef(true);
+ const storeUrl = useMemo(() => {
+ try {
+ return `${getCoStrictBaseURL()}/cloud`;
+ } catch {
+ return 'https://zgsm.sangfor.com/cloud';
+ }
+ }, []);
+
+ useKeybinding(
+ 'confirm:no',
+ () => {
+ onDone('Cancelled', { display: 'system' });
+ },
+ { context: 'Confirmation' },
+ );
+
useEffect(() => {
let cancelled = false;
listFavoriteItems()
@@ -45,23 +195,9 @@ function CloudEnabledMenu({
};
}, [onDone]);
- const handleCancel = () => {
- onDone('Cancelled', { display: 'system' });
- };
-
- const options = useMemo(
- () =>
- (items ?? []).map(item => ({
- label: `[${item.itemType}] ${item.name} (${item.status})`,
- value: item.slug,
- })),
- [items],
- );
-
- const defaultValue = useMemo(
- () => (items ?? []).filter(item => item.status === 'Active').map(item => item.slug),
- [items],
- );
+ const handleTabChange = useCallback((tabId: string) => {
+ setActiveTab(tabId as TabId);
+ }, []);
const handleChange = async (selectedSlugs: string[]) => {
if (isFirstChange.current) {
@@ -81,12 +217,26 @@ function CloudEnabledMenu({
const hasMcpChanges = [...toLoad, ...toUnload].some(item => item.itemType === 'mcp');
const hasAgentChanges = [...toLoad, ...toUnload].some(item => item.itemType === 'agent');
+ // Optimistically update UI state so tab switching shows correct status
+ setItems(prev => {
+ if (!prev) return prev;
+ const map = new Map(prev.map(i => [i.slug, i]));
+ for (const item of toLoad) {
+ const existing = map.get(item.slug);
+ if (existing) map.set(item.slug, { ...existing, status: 'Active' });
+ }
+ for (const item of toUnload) {
+ const existing = map.get(item.slug);
+ if (existing) map.set(item.slug, { ...existing, status: 'Unloaded' });
+ }
+ return Array.from(map.values());
+ });
+
const results = await Promise.allSettled([
...toLoad.map(item => loadFavoriteItem(item.slug)),
...toUnload.map(item => unloadFavoriteItem(item.slug)),
]);
- // Trigger MCP reconnection if any MCP servers were added or removed
if (hasMcpChanges) {
setAppState(prev => ({
...prev,
@@ -97,7 +247,6 @@ function CloudEnabledMenu({
}));
}
- // Refresh agent definitions if any agents were added or removed
if (hasAgentChanges) {
const { getAgentDefinitionsWithOverrides, getActiveAgentsFromList } = await import(
'@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
@@ -123,35 +272,39 @@ function CloudEnabledMenu({
}
};
+ const handleCancel = () => {
+ onDone('Cancelled', { display: 'system' });
+ };
+
if (items === null) {
return (
-
+
);
}
return (
-
+
+
+
+ 云端订阅项目,如需订阅请访问 {storeUrl}
+
+
+
+ {TAB_CONFIG.map(tab => (
+
+ item.itemType === tab.id)}
+ onChange={handleChange}
+ onCancel={handleCancel}
+ />
+
+ ))}
+
+
);
}
diff --git a/src/costrict/favorite/favorite.ts b/src/costrict/favorite/favorite.ts
index 67b149b70..91350356e 100644
--- a/src/costrict/favorite/favorite.ts
+++ b/src/costrict/favorite/favorite.ts
@@ -56,6 +56,7 @@ export type FavoriteItem = {
category?: string
version?: string
favoriteCount?: number
+ score?: number
favorited?: boolean
createdBy?: string
createdAt?: string
@@ -203,6 +204,10 @@ function parseFavoriteListItem(
? data.favorited
: data.favorited === 'true' || data.favorited === 1
+ logForDebugging(
+ `[favorite] name=${data.name} score=${data.score} experienceScore=${data.experienceScore} favoriteCount=${data.favoriteCount} createdBy=${data.createdBy} fields=[${Object.keys(data).join(',')}]`,
+ )
+
return {
id: String(data.id),
slug: String(data.slug ?? data.id),
@@ -214,6 +219,18 @@ function parseFavoriteListItem(
version: typeof data.version === 'string' ? data.version : undefined,
favoriteCount:
typeof data.favoriteCount === 'number' ? data.favoriteCount : undefined,
+ score:
+ typeof data.experienceScore === 'number'
+ ? data.experienceScore
+ : typeof data.experienceScore === 'string'
+ ? Number(data.experienceScore) || undefined
+ : typeof data.score === 'number'
+ ? data.score
+ : typeof data.score === 'string'
+ ? Number(data.score) || undefined
+ : typeof data.favoriteCount === 'number'
+ ? data.favoriteCount
+ : undefined,
favorited,
createdBy: typeof data.createdBy === 'string' ? data.createdBy : undefined,
createdAt: typeof data.createdAt === 'string' ? data.createdAt : undefined,
@@ -251,6 +268,18 @@ async function getRemoteItem(id: string): Promise {
version: typeof data.version === 'string' ? data.version : undefined,
favoriteCount:
typeof data.favoriteCount === 'number' ? data.favoriteCount : undefined,
+ score:
+ typeof data.experienceScore === 'number'
+ ? data.experienceScore
+ : typeof data.experienceScore === 'string'
+ ? Number(data.experienceScore) || undefined
+ : typeof data.score === 'number'
+ ? data.score
+ : typeof data.score === 'string'
+ ? Number(data.score) || undefined
+ : typeof data.favoriteCount === 'number'
+ ? data.favoriteCount
+ : undefined,
favorited: typeof data.favorited === 'boolean' ? data.favorited : undefined,
createdBy: typeof data.createdBy === 'string' ? data.createdBy : undefined,
createdAt: typeof data.createdAt === 'string' ? data.createdAt : undefined,