claude-code-best/src/commands/knowledge-hub/cloud-enabled.tsx
林凯90331 02ffda9d4c fix: cloud favorite status priority and runtime lifecycle
- deriveStatus: prioritize favorite lifecycle over local config presence
- autoEnableCloudFavorites: skip unloaded items to prevent re-loading
- load/unload: sync agent/command favorites to ~/.claude/{agents,commands}/
- /hub toggle: trigger MCP reconnection and agent definition refresh
- cloud-enabled menu: trigger load for newly checked items

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:38:10 +08:00

161 lines
5.0 KiB
TypeScript

import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Box, Text, Dialog } from '@anthropic/ink';
import { SelectMulti } from '../../components/CustomSelect/SelectMulti.js';
import {
listFavoriteItems,
loadFavoriteItem,
unloadFavoriteItem,
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';
function CloudEnabledMenu({
onDone,
}: {
onDone: (result?: string, options?: { display?: 'skip' | 'system' | 'user' }) => void;
}): React.ReactNode {
const setAppState = useSetAppState();
const [items, setItems] = useState<FavoriteItemWithStatus[] | null>(null);
const previousSlugs = useRef<Set<string>>(new Set());
const isFirstChange = useRef(true);
useEffect(() => {
let cancelled = false;
listFavoriteItems()
.then(data => {
if (cancelled) return;
if (data.length === 0) {
onDone('No cloud favorites found', { display: 'system' });
} else {
setItems(data);
previousSlugs.current = new Set(data.filter(item => item.status === 'Active').map(item => item.slug));
}
})
.catch((error: unknown) => {
if (cancelled) return;
const message = error instanceof Error ? error.message : String(error);
onDone(`Failed to load cloud favorites: ${message}`, {
display: 'system',
});
});
return () => {
cancelled = true;
};
}, [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 handleChange = async (selectedSlugs: string[]) => {
if (isFirstChange.current) {
isFirstChange.current = false;
}
const prevSet = previousSlugs.current;
const nextSet = new Set(selectedSlugs);
const toLoad = (items ?? []).filter(item => nextSet.has(item.slug) && !prevSet.has(item.slug));
const toUnload = (items ?? []).filter(item => !nextSet.has(item.slug) && prevSet.has(item.slug));
previousSlugs.current = nextSet;
if (toLoad.length === 0 && toUnload.length === 0) return;
const hasMcpChanges = [...toLoad, ...toUnload].some(item => item.itemType === 'mcp');
const hasAgentChanges = [...toLoad, ...toUnload].some(item => item.itemType === 'agent');
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,
mcp: {
...prev.mcp,
pluginReconnectKey: prev.mcp.pluginReconnectKey + 1,
},
}));
}
// 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'
);
getAgentDefinitionsWithOverrides.cache?.clear?.();
const freshAgentDefs = await getAgentDefinitionsWithOverrides(getOriginalCwd());
setAppState(prev => ({
...prev,
agentDefinitions: {
...freshAgentDefs,
allAgents: freshAgentDefs.allAgents,
activeAgents: getActiveAgentsFromList(freshAgentDefs.allAgents),
},
}));
}
const failures = results.filter(r => r.status === 'rejected') as PromiseRejectedResult[];
if (failures.length > 0) {
const reasons = failures.map(r => (r.reason instanceof Error ? r.reason.message : String(r.reason)));
onDone(`Cloud enabled items updated with ${failures.length} errors: ${reasons.join('; ')}`, {
display: 'system',
});
}
};
if (items === null) {
return (
<Dialog
title="Cloud Enabled Items"
subtitle="Toggle items to enable/disable (auto-download on enable)"
onCancel={handleCancel}
>
<Box>
<Text>Loading cloud favorites...</Text>
</Box>
</Dialog>
);
}
return (
<Dialog
title="Cloud Enabled Items"
subtitle="Toggle items to enable/disable (auto-download on enable)"
onCancel={handleCancel}
>
<SelectMulti
key="knowledge-hub-loaded"
options={options}
defaultValue={defaultValue}
onChange={handleChange}
onCancel={handleCancel}
hideIndexes
/>
</Dialog>
);
}
export const call: LocalJSXCommandCall = async onDone => {
return <CloudEnabledMenu onDone={onDone} />;
};