Merge main HEAD (segment 5) into sandboxed-agent-rebase

Resolve 83 conflicts: 10 backend, 62 frontend, 11 config/lock files.
Preserve sandbox/agent/collaboration features while adopting main's
UI refactorings (Dialog/AlertDialog/Popover), model provider updates,
and enterprise features.

Made-with: Cursor
This commit is contained in:
Novice
2026-03-23 14:20:06 +08:00
1671 changed files with 124822 additions and 22302 deletions

View File

@ -266,10 +266,117 @@ describe('useInstallMultiState', () => {
expect(result.current.plugins[1]).toBeDefined()
})
})
it('should fall back to latest_version when marketplace plugin version is missing', async () => {
mockMarketplaceData = {
data: {
list: [{
plugin: {
plugin_id: 'test-org/plugin-0',
org: 'test-org',
name: 'Test Plugin 0',
version: '',
latest_version: '2.0.0',
},
version: {
unique_identifier: 'plugin-0-uid',
},
}],
},
}
const params = createDefaultParams({
allPlugins: [createMarketplaceDependency(0)] as Dependency[],
})
const { result } = renderHook(() => useInstallMultiState(params))
await waitFor(() => {
expect(result.current.plugins[0]?.version).toBe('2.0.0')
})
})
it('should resolve marketplace dependency from organization and plugin fields', async () => {
mockMarketplaceData = createMarketplaceApiData([0])
const params = createDefaultParams({
allPlugins: [
{
type: 'marketplace',
value: {
organization: 'test-org',
plugin: 'plugin-0',
version: '1.0.0',
},
} as GitHubItemAndMarketPlaceDependency,
] as Dependency[],
})
const { result } = renderHook(() => useInstallMultiState(params))
await waitFor(() => {
expect(result.current.plugins[0]).toBeDefined()
expect(result.current.errorIndexes).not.toContain(0)
})
})
})
// ==================== Error Handling ====================
describe('Error Handling', () => {
it('should mark marketplace index as error when identifier misses plugin and version parts', async () => {
const params = createDefaultParams({
allPlugins: [
{
type: 'marketplace',
value: {
marketplace_plugin_unique_identifier: 'invalid-identifier',
version: '1.0.0',
},
} as GitHubItemAndMarketPlaceDependency,
] as Dependency[],
})
const { result } = renderHook(() => useInstallMultiState(params))
await waitFor(() => {
expect(result.current.errorIndexes).toContain(0)
})
})
it('should mark marketplace index as error when identifier has an empty plugin segment', async () => {
const params = createDefaultParams({
allPlugins: [
{
type: 'marketplace',
value: {
marketplace_plugin_unique_identifier: 'test-org/:1.0.0',
version: '1.0.0',
},
} as GitHubItemAndMarketPlaceDependency,
] as Dependency[],
})
const { result } = renderHook(() => useInstallMultiState(params))
await waitFor(() => {
expect(result.current.errorIndexes).toContain(0)
})
})
it('should mark marketplace index as error when identifier is missing', async () => {
const params = createDefaultParams({
allPlugins: [
{
type: 'marketplace',
value: {
version: '1.0.0',
},
} as GitHubItemAndMarketPlaceDependency,
] as Dependency[],
})
const { result } = renderHook(() => useInstallMultiState(params))
await waitFor(() => {
expect(result.current.errorIndexes).toContain(0)
})
})
it('should mark all marketplace indexes as errors on fetch failure', async () => {
mockMarketplaceError = new Error('Fetch failed')
@ -303,6 +410,19 @@ describe('useInstallMultiState', () => {
expect(result.current.errorIndexes).not.toContain(0)
})
})
it('should ignore marketplace requests whose dsl index cannot be mapped', () => {
const duplicatedMarketplaceDependency = createMarketplaceDependency(0)
const allPlugins = [duplicatedMarketplaceDependency] as Dependency[]
allPlugins.filter = vi.fn(() => [duplicatedMarketplaceDependency, duplicatedMarketplaceDependency]) as typeof allPlugins.filter
const params = createDefaultParams({ allPlugins })
const { result } = renderHook(() => useInstallMultiState(params))
expect(result.current.plugins).toHaveLength(1)
expect(result.current.errorIndexes).toEqual([])
})
})
// ==================== Loaded All Data Notification ====================

View File

@ -14,14 +14,55 @@ type UseInstallMultiStateParams = {
onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
}
type MarketplacePluginInfo = {
organization: string
plugin: string
version?: string
}
type MarketplaceRequest = {
dslIndex: number
dependency: GitHubItemAndMarketPlaceDependency
info: MarketplacePluginInfo
}
export function getPluginKey(plugin: Plugin | undefined): string {
return `${plugin?.org || plugin?.author}/${plugin?.name}`
}
function parseMarketplaceIdentifier(identifier: string) {
const [orgPart, nameAndVersionPart] = identifier.split('@')[0].split('/')
const [name, version] = nameAndVersionPart.split(':')
return { organization: orgPart, plugin: name, version }
function parseMarketplaceIdentifier(identifier?: string): MarketplacePluginInfo | null {
if (!identifier)
return null
const withoutHash = identifier.split('@')[0]
const [organization, nameAndVersionPart] = withoutHash.split('/')
if (!organization || !nameAndVersionPart)
return null
const [plugin, version] = nameAndVersionPart.split(':')
if (!plugin)
return null
return { organization, plugin, version }
}
function getMarketplacePluginInfo(
value: GitHubItemAndMarketPlaceDependency['value'],
): MarketplacePluginInfo | null {
const parsedInfo = parseMarketplaceIdentifier(
value.marketplace_plugin_unique_identifier || value.plugin_unique_identifier,
)
if (parsedInfo)
return parsedInfo
if (!value.organization || !value.plugin)
return null
return {
organization: value.organization,
plugin: value.plugin,
version: value.version,
}
}
function initPluginsFromDependencies(allPlugins: Dependency[]): (Plugin | undefined)[] {
@ -61,67 +102,73 @@ export function useInstallMultiState({
}, [])
}, [allPlugins])
// Marketplace data fetching: by unique identifier and by meta info
const { marketplaceRequests, invalidMarketplaceIndexes } = useMemo(() => {
return marketplacePlugins.reduce<{
marketplaceRequests: MarketplaceRequest[]
invalidMarketplaceIndexes: number[]
}>((acc, dependency, marketplaceIndex) => {
const dslIndex = marketPlaceInDSLIndex[marketplaceIndex]
if (dslIndex === undefined)
return acc
const marketplaceInfo = getMarketplacePluginInfo(dependency.value)
if (!marketplaceInfo)
acc.invalidMarketplaceIndexes.push(dslIndex)
else
acc.marketplaceRequests.push({ dslIndex, dependency, info: marketplaceInfo })
return acc
}, {
marketplaceRequests: [],
invalidMarketplaceIndexes: [],
})
}, [marketPlaceInDSLIndex, marketplacePlugins])
// Marketplace data fetching: by normalized marketplace info
const {
isLoading: isFetchingById,
data: infoGetById,
error: infoByIdError,
} = useFetchPluginsInMarketPlaceByInfo(
marketplacePlugins.map(d => parseMarketplaceIdentifier(d.value.marketplace_plugin_unique_identifier!)),
)
const {
isLoading: isFetchingByMeta,
data: infoByMeta,
error: infoByMetaError,
} = useFetchPluginsInMarketPlaceByInfo(
marketplacePlugins.map(d => d.value!),
marketplaceRequests.map(request => request.info),
)
// Derive marketplace plugin data and errors from API responses
const { marketplacePluginMap, marketplaceErrorIndexes } = useMemo(() => {
const pluginMap = new Map<number, Plugin>()
const errorSet = new Set<number>()
const errorSet = new Set<number>(invalidMarketplaceIndexes)
// Process "by ID" response
if (!isFetchingById && infoGetById?.data.list) {
const sortedList = marketplacePlugins.map((d) => {
const id = d.value.marketplace_plugin_unique_identifier?.split(':')[0]
const retPluginInfo = infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin
return { ...retPluginInfo, from: d.type } as Plugin
})
marketPlaceInDSLIndex.forEach((index, i) => {
if (sortedList[i]) {
pluginMap.set(index, {
...sortedList[i],
version: sortedList[i]!.version || sortedList[i]!.latest_version,
const payloads = infoGetById.data.list
const pluginById = new Map(
payloads.map(item => [item.plugin.plugin_id, item.plugin]),
)
marketplaceRequests.forEach((request, requestIndex) => {
const pluginId = (
request.dependency.value.marketplace_plugin_unique_identifier
|| request.dependency.value.plugin_unique_identifier
)?.split(':')[0]
const pluginInfo = (pluginId ? pluginById.get(pluginId) : undefined) || payloads[requestIndex]?.plugin
if (pluginInfo) {
pluginMap.set(request.dslIndex, {
...pluginInfo,
from: request.dependency.type,
version: pluginInfo.version || pluginInfo.latest_version,
})
}
else { errorSet.add(index) }
})
}
// Process "by meta" response (may overwrite "by ID" results)
if (!isFetchingByMeta && infoByMeta?.data.list) {
const payloads = infoByMeta.data.list
marketPlaceInDSLIndex.forEach((index, i) => {
if (payloads[i]) {
const item = payloads[i]
pluginMap.set(index, {
...item.plugin,
plugin_id: item.version.unique_identifier,
} as Plugin)
}
else { errorSet.add(index) }
else { errorSet.add(request.dslIndex) }
})
}
// Mark all marketplace indexes as errors on fetch failure
if (infoByMetaError || infoByIdError)
if (infoByIdError)
marketPlaceInDSLIndex.forEach(index => errorSet.add(index))
return { marketplacePluginMap: pluginMap, marketplaceErrorIndexes: errorSet }
}, [isFetchingById, isFetchingByMeta, infoGetById, infoByMeta, infoByMetaError, infoByIdError, marketPlaceInDSLIndex, marketplacePlugins])
}, [invalidMarketplaceIndexes, isFetchingById, infoGetById, infoByIdError, marketPlaceInDSLIndex, marketplaceRequests])
// GitHub-fetched plugins and errors (imperative state from child callbacks)
const [githubPluginMap, setGithubPluginMap] = useState<Map<number, Plugin>>(() => new Map())