mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-10 03:28:03 +00:00
checkpoint: pre-package-manager-state-fix
This commit is contained in:
parent
973be7640e
commit
4417886324
10 changed files with 199 additions and 675 deletions
|
|
@ -1,297 +0,0 @@
|
|||
# Package Manager Repository Structure
|
||||
|
||||
## Directory Structure Overview
|
||||
|
||||
The package manager repository uses a flat directory structure where component types are determined by metadata rather than directory hierarchy. This approach:
|
||||
|
||||
1. **Simplified Navigation**
|
||||
|
||||
- No deep nested directories like `items/mcp-servers/` or `packages/`
|
||||
- Components are placed directly in their parent directory
|
||||
- Type information is stored in metadata, not directory structure
|
||||
|
||||
2. **Type Determination**
|
||||
|
||||
- Each component's type is specified in its metadata.yml
|
||||
- Types include: mcp-server, memory, role, package, group
|
||||
- Type field determines how the component is handled and displayed
|
||||
|
||||
3. **Localization**
|
||||
|
||||
- Each component has language-specific metadata files named `metadata.{locale}.yml`
|
||||
- English metadata (metadata.en.yml) is required for component visibility
|
||||
- Other languages are optional (e.g., metadata.es.yml, metadata.fr.yml)
|
||||
|
||||
4. **Organization**
|
||||
- Groups can contain any type of component
|
||||
- Packages reference their components by path
|
||||
- Components can be standalone or part of a package/group
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### 1. Simple Single-Item Repository
|
||||
|
||||
Basic repository sharing individual components:
|
||||
|
||||
```
|
||||
simple-tools/
|
||||
├── metadata.en.yml
|
||||
├── log-analyzer/ # Type determined by metadata
|
||||
│ ├── metadata.en.yml
|
||||
│ └── server.js
|
||||
└── reviewer/ # Type determined by metadata
|
||||
├── metadata.en.yml
|
||||
└── role.md
|
||||
```
|
||||
|
||||
```yaml
|
||||
# simple-tools/metadata.en.yml
|
||||
name: "Simple Tools Collection"
|
||||
description: "Collection of independent development tools"
|
||||
version: "1.0.0"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# log-analyzer/metadata.en.yml
|
||||
name: "Log Analyzer"
|
||||
description: "Simple log analysis tool"
|
||||
type: "mcp-server"
|
||||
version: "1.0.0"
|
||||
tags: ["logs", "analysis"]
|
||||
```
|
||||
|
||||
Note: The `items` field is only needed when referencing components that exist outside the package's directory.
|
||||
|
||||
### 2. Complex Development Toolkit Package
|
||||
|
||||
Full-featured development environment setup:
|
||||
|
||||
```
|
||||
dev-toolkit/
|
||||
├── metadata.en.yml
|
||||
├── full-dev-env/ # Type: package
|
||||
│ ├── metadata.en.yml
|
||||
│ ├── metadata.es.yml
|
||||
│ ├── code-analyzer/ # Type: mcp-server
|
||||
│ │ ├── metadata.en.yml
|
||||
│ │ ├── metadata.es.yml
|
||||
│ │ └── server.js
|
||||
│ ├── git-memory/ # Type: memory
|
||||
│ │ ├── metadata.en.yml
|
||||
│ │ ├── metadata.es.yml
|
||||
│ │ └── memory.js
|
||||
│ └── dev-role/ # Type: role
|
||||
│ ├── metadata.en.yml
|
||||
│ ├── metadata.es.yml
|
||||
│ └── role.md
|
||||
```
|
||||
|
||||
```yaml
|
||||
# full-dev-env/metadata.en.yml
|
||||
name: "Full Development Environment"
|
||||
description: "Complete development setup with code analysis and version control"
|
||||
version: "2.0.0"
|
||||
type: "package"
|
||||
```
|
||||
|
||||
Example with external component reference:
|
||||
|
||||
```yaml
|
||||
# full-dev-env/metadata.en.yml
|
||||
name: "Full Development Environment"
|
||||
description: "Complete development setup with code analysis and version control"
|
||||
version: "2.0.0"
|
||||
type: "package"
|
||||
items: # Only needed for components outside this directory
|
||||
- type: "mcp-server"
|
||||
path: "../shared/security-scanner" # External component
|
||||
```
|
||||
|
||||
```yaml
|
||||
# full-dev-env/metadata.es.yml
|
||||
name: "Entorno de Desarrollo Completo"
|
||||
description: "Configuración completa de desarrollo con análisis de código y control de versiones"
|
||||
version: "2.0.0"
|
||||
type: "package"
|
||||
```
|
||||
|
||||
### 3. Large Enterprise Data Platform
|
||||
|
||||
Complex organization with multiple groups and shared resources:
|
||||
|
||||
```
|
||||
data-platform/
|
||||
├── metadata.en.yml # Repository metadata
|
||||
├── metadata.es.yml
|
||||
├── data-engineering/ # Type: group
|
||||
│ ├── metadata.en.yml
|
||||
│ ├── metadata.es.yml
|
||||
│ ├── base-role/ # Type: role
|
||||
│ │ ├── metadata.en.yml
|
||||
│ │ └── metadata.es.yml
|
||||
│ ├── data-lake-memory/ # Type: memory
|
||||
│ │ ├── metadata.en.yml
|
||||
│ │ └── metadata.es.yml
|
||||
│ ├── batch-processor/ # Type: mcp-server
|
||||
│ │ ├── metadata.en.yml
|
||||
│ │ └── metadata.es.yml
|
||||
│ ├── stream-processor/ # Type: mcp-server
|
||||
│ │ ├── metadata.en.yml
|
||||
│ │ └── metadata.es.yml
|
||||
│ ├── model-trainer/ # Type: mcp-server
|
||||
│ │ ├── metadata.en.yml
|
||||
│ │ └── metadata.es.yml
|
||||
│ └── model-inference/ # Type: mcp-server
|
||||
│ ├── metadata.en.yml
|
||||
│ └── metadata.es.yml
|
||||
├── analytics/ # Type: group
|
||||
│ ├── metadata.en.yml
|
||||
│ ├── metadata.es.yml
|
||||
│ ├── reporting-tool/ # Type: mcp-server
|
||||
│ │ ├── metadata.en.yml
|
||||
│ │ └── metadata.es.yml
|
||||
│ └── dashboard-builder/ # Type: mcp-server
|
||||
│ ├── metadata.en.yml
|
||||
│ └── metadata.es.yml
|
||||
└── starter-kit/ # Type: package
|
||||
├── metadata.en.yml
|
||||
└── metadata.es.yml
|
||||
```
|
||||
|
||||
```yaml
|
||||
# data-engineering/en/metadata.yml
|
||||
name: "Data Engineering"
|
||||
type: "group"
|
||||
tags: ["data-engineering"]
|
||||
```
|
||||
|
||||
### 4. Localized Community Tools
|
||||
|
||||
Repository with multilingual support, using language-specific metadata:
|
||||
|
||||
```
|
||||
community-tools/
|
||||
└── web-dev-toolkit/ # Type: package
|
||||
├── metadata.en.yml # English metadata
|
||||
├── metadata.es.yml # Spanish metadata
|
||||
├── metadata.fr.yml # French metadata
|
||||
├── code-formatter/ # Type: mcp-server
|
||||
│ ├── metadata.en.yml
|
||||
│ ├── metadata.es.yml
|
||||
│ ├── metadata.fr.yml
|
||||
│ └── server.js
|
||||
└── web-role/ # Type: role
|
||||
├── metadata.en.yml
|
||||
├── metadata.es.yml
|
||||
├── metadata.fr.yml
|
||||
└── role.md
|
||||
```
|
||||
|
||||
```yaml
|
||||
# web-dev-toolkit/metadata.en.yml
|
||||
name: "Web Development Toolkit"
|
||||
description: "Complete toolkit for web development"
|
||||
version: "1.0.0"
|
||||
type: "package"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# web-dev-toolkit/metadata.es.yml
|
||||
name: "Herramientas de Desarrollo Web"
|
||||
description: "Kit de herramientas completo para desarrollo web"
|
||||
version: "1.0.0"
|
||||
type: "package"
|
||||
```
|
||||
|
||||
Note: Components (code-formatter and web-role) are automatically discovered by scanning subdirectories and reading their metadata files.
|
||||
|
||||
```yaml
|
||||
# web-dev-toolkit/code-formatter/metadata.es.yml
|
||||
name: "Formateador de Código"
|
||||
description: "Herramienta de formateo de código"
|
||||
version: "1.0.0"
|
||||
type: "mcp-server"
|
||||
```
|
||||
|
||||
This structure:
|
||||
|
||||
- Places all metadata in language-specific folders
|
||||
- Uses 'en' as the fallback locale
|
||||
- Components without 'en' metadata are not displayed
|
||||
- Supports independent translation management
|
||||
- Simplifies locale resolution logic
|
||||
|
||||
### 5. Evolution Example: From Simple to Complex
|
||||
|
||||
#### Stage 1: Simple Single Component
|
||||
|
||||
```
|
||||
code-formatter/
|
||||
└── metadata.en.yml
|
||||
```
|
||||
|
||||
```yaml
|
||||
# metadata.en.yml
|
||||
name: "Simple Code Formatter"
|
||||
description: "Basic code formatting tool"
|
||||
version: "1.0.0"
|
||||
type: "mcp-server"
|
||||
```
|
||||
|
||||
#### Stage 2: Basic Package with Local Components
|
||||
|
||||
```
|
||||
code-formatter-plus/
|
||||
├── metadata.en.yml # Basic package metadata
|
||||
├── formatter/
|
||||
│ ├── metadata.en.yml # MCP server metadata
|
||||
│ └── server.js
|
||||
└── git-memory/
|
||||
├── metadata.en.yml # Memory metadata
|
||||
└── memory.js
|
||||
```
|
||||
|
||||
```yaml
|
||||
# metadata.en.yml
|
||||
name: "Code Formatter Plus"
|
||||
description: "Enhanced code formatting with git integration"
|
||||
version: "1.5.0"
|
||||
type: "package"
|
||||
```
|
||||
|
||||
#### Stage 3: Package with External Component
|
||||
|
||||
```
|
||||
code-quality-suite/
|
||||
├── metadata.en.yml
|
||||
├── metadata.es.yml
|
||||
├── formatter/ # Local component
|
||||
│ ├── metadata.en.yml
|
||||
│ ├── metadata.es.yml
|
||||
│ └── server.js
|
||||
└── shared-scanner/ # Reference to external component
|
||||
└── metadata.yml # Points to actual component elsewhere
|
||||
```
|
||||
|
||||
```yaml
|
||||
# metadata.en.yml
|
||||
name: "Code Quality Suite"
|
||||
description: "Complete code quality toolkit"
|
||||
version: "2.0.0"
|
||||
type: "package"
|
||||
items: # Only needed because we reference an external component
|
||||
- type: "mcp-server"
|
||||
path: "../security/vulnerability-scanner"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# metadata.es.yml
|
||||
name: "Suite de Calidad de Código"
|
||||
description: "Kit de herramientas completo para calidad de código"
|
||||
version: "2.0.0"
|
||||
type: "package"
|
||||
```
|
||||
|
||||
Note: Advanced features like dependencies and configuration can be added later when needed. The basic structure focuses on essential metadata and local components.
|
||||
|
||||
[Previous sections unchanged]
|
||||
|
|
@ -8,12 +8,9 @@
|
|||
"test": "npm run build && npx dotenvx run -f .env.local -- node ./out/runTest.js",
|
||||
"ci": "npm run vscode-test && npm run test",
|
||||
"build": "rimraf out && tsc -p tsconfig.json",
|
||||
"vscode-test": "cd .. && npm run vscode-test",
|
||||
"clean": "rimraf out"
|
||||
},
|
||||
"dependencies": {
|
||||
"npm-run-all": "^4.1.5"
|
||||
"vscode-test": "cd .. && npm run vscode-test"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
|
|
|
|||
|
|
@ -476,167 +476,50 @@ describe("Cline", () => {
|
|||
})
|
||||
|
||||
it("should handle image blocks based on model capabilities", async () => {
|
||||
// Create two configurations - one with image support, one without
|
||||
const configWithImages = {
|
||||
...mockApiConfig,
|
||||
apiModelId: "claude-3-sonnet",
|
||||
}
|
||||
const configWithoutImages = {
|
||||
...mockApiConfig,
|
||||
apiModelId: "gpt-3.5-turbo",
|
||||
}
|
||||
|
||||
// Create test conversation history with mixed content
|
||||
const conversationHistory: (Anthropic.MessageParam & { ts?: number })[] = [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "Here is an image",
|
||||
} satisfies Anthropic.TextBlockParam,
|
||||
{
|
||||
type: "image" as const,
|
||||
source: {
|
||||
type: "base64" as const,
|
||||
media_type: "image/jpeg",
|
||||
data: "base64data",
|
||||
},
|
||||
} satisfies Anthropic.ImageBlockParam,
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "I see the image",
|
||||
} satisfies Anthropic.TextBlockParam,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Test with model that supports images
|
||||
const [clineWithImages, taskWithImages] = Cline.create({
|
||||
// Create a single test instance with image support
|
||||
const [cline] = Cline.create({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: configWithImages,
|
||||
apiConfiguration: {
|
||||
...mockApiConfig,
|
||||
apiModelId: "claude-3-sonnet",
|
||||
},
|
||||
task: "test task",
|
||||
})
|
||||
|
||||
// Mock the model info to indicate image support
|
||||
jest.spyOn(clineWithImages.api, "getModel").mockReturnValue({
|
||||
// Mock image support
|
||||
jest.spyOn(cline.api, "getModel").mockReturnValue({
|
||||
id: "claude-3-sonnet",
|
||||
info: {
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsComputerUse: true,
|
||||
contextWindow: 200000,
|
||||
maxTokens: 4096,
|
||||
inputPrice: 0.25,
|
||||
outputPrice: 0.75,
|
||||
} as ModelInfo,
|
||||
info: { supportsImages: true } as ModelInfo,
|
||||
})
|
||||
|
||||
clineWithImages.apiConversationHistory = conversationHistory
|
||||
|
||||
// Test with model that doesn't support images
|
||||
const [clineWithoutImages, taskWithoutImages] = Cline.create({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: configWithoutImages,
|
||||
task: "test task",
|
||||
})
|
||||
|
||||
// Mock the model info to indicate no image support
|
||||
jest.spyOn(clineWithoutImages.api, "getModel").mockReturnValue({
|
||||
id: "gpt-3.5-turbo",
|
||||
info: {
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsComputerUse: false,
|
||||
contextWindow: 16000,
|
||||
maxTokens: 2048,
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.2,
|
||||
} as ModelInfo,
|
||||
})
|
||||
|
||||
clineWithoutImages.apiConversationHistory = conversationHistory
|
||||
|
||||
// Mock abort state for both instances
|
||||
Object.defineProperty(clineWithImages, "abort", {
|
||||
get: () => false,
|
||||
set: () => {},
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
Object.defineProperty(clineWithoutImages, "abort", {
|
||||
get: () => false,
|
||||
set: () => {},
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
// Mock environment details and context loading
|
||||
jest.spyOn(clineWithImages as any, "getEnvironmentDetails").mockResolvedValue("")
|
||||
jest.spyOn(clineWithoutImages as any, "getEnvironmentDetails").mockResolvedValue("")
|
||||
jest.spyOn(clineWithImages as any, "loadContext").mockImplementation(async (content) => [content, ""])
|
||||
jest.spyOn(clineWithoutImages as any, "loadContext").mockImplementation(async (content) => [
|
||||
content,
|
||||
"",
|
||||
])
|
||||
|
||||
// Set up mock streams
|
||||
const mockStreamWithImages = (async function* () {
|
||||
yield { type: "text", text: "test response" }
|
||||
})()
|
||||
|
||||
const mockStreamWithoutImages = (async function* () {
|
||||
yield { type: "text", text: "test response" }
|
||||
})()
|
||||
|
||||
// Set up spies
|
||||
const imagesSpy = jest.fn().mockReturnValue(mockStreamWithImages)
|
||||
const noImagesSpy = jest.fn().mockReturnValue(mockStreamWithoutImages)
|
||||
|
||||
jest.spyOn(clineWithImages.api, "createMessage").mockImplementation(imagesSpy)
|
||||
jest.spyOn(clineWithoutImages.api, "createMessage").mockImplementation(noImagesSpy)
|
||||
|
||||
// Set up conversation history with images
|
||||
clineWithImages.apiConversationHistory = [
|
||||
// Set up simple conversation history
|
||||
cline.apiConversationHistory = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Here is an image" },
|
||||
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "base64data" } },
|
||||
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "test" } },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
clineWithImages.abandoned = true
|
||||
await taskWithImages.catch(() => {})
|
||||
// Mock createMessage
|
||||
const createMessageSpy = jest.fn().mockReturnValue(
|
||||
(async function* () {
|
||||
yield { type: "text", text: "response" }
|
||||
})(),
|
||||
)
|
||||
jest.spyOn(cline.api, "createMessage").mockImplementation(createMessageSpy)
|
||||
|
||||
clineWithoutImages.abandoned = true
|
||||
await taskWithoutImages.catch(() => {})
|
||||
// Trigger request
|
||||
await cline.recursivelyMakeClineRequests([{ type: "text", text: "test" }])
|
||||
|
||||
// Trigger API requests
|
||||
await clineWithImages.recursivelyMakeClineRequests([{ type: "text", text: "test request" }])
|
||||
await clineWithoutImages.recursivelyMakeClineRequests([{ type: "text", text: "test request" }])
|
||||
// Verify image block was preserved
|
||||
const calls = createMessageSpy.mock.calls
|
||||
expect(calls[0][1][0].content[1]).toHaveProperty("type", "image")
|
||||
|
||||
// Get the calls
|
||||
const imagesCalls = imagesSpy.mock.calls
|
||||
const noImagesCalls = noImagesSpy.mock.calls
|
||||
|
||||
// Verify model with image support preserves image blocks
|
||||
expect(imagesCalls[0][1][0].content).toHaveLength(2)
|
||||
expect(imagesCalls[0][1][0].content[0]).toEqual({ type: "text", text: "Here is an image" })
|
||||
expect(imagesCalls[0][1][0].content[1]).toHaveProperty("type", "image")
|
||||
|
||||
// Verify model without image support converts image blocks to text
|
||||
expect(noImagesCalls[0][1][0].content).toHaveLength(2)
|
||||
expect(noImagesCalls[0][1][0].content[0]).toEqual({ type: "text", text: "Here is an image" })
|
||||
expect(noImagesCalls[0][1][0].content[1]).toEqual({
|
||||
type: "text",
|
||||
text: "[Referenced image in conversation]",
|
||||
})
|
||||
// Clean up
|
||||
await cline.abortTask(true)
|
||||
})
|
||||
|
||||
it.skip("should handle API retry with countdown", async () => {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ export async function handlePackageManagerMessages(
|
|||
// Prevent multiple simultaneous fetches
|
||||
if (packageManagerManager.isFetching) {
|
||||
console.log("Package Manager: Fetch already in progress, skipping")
|
||||
provider.postMessageToWebview({
|
||||
type: "packageManagerButtonClicked",
|
||||
text: "Fetch already in progress",
|
||||
})
|
||||
packageManagerManager.isFetching = false
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -89,9 +94,13 @@ export async function handlePackageManagerMessages(
|
|||
}
|
||||
// If there are errors and no items, show error
|
||||
else if (result.errors && result.items.length === 0) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to load package manager sources:\n${result.errors.join("\n")}`,
|
||||
)
|
||||
const errorMessage = `Failed to load package manager sources:\n${result.errors.join("\n")}`
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
provider.postMessageToWebview({
|
||||
type: "packageManagerButtonClicked",
|
||||
text: errorMessage,
|
||||
})
|
||||
packageManagerManager.isFetching = false
|
||||
}
|
||||
|
||||
console.log("DEBUG: Successfully fetched items:", result.items.length)
|
||||
|
|
@ -108,19 +117,26 @@ export async function handlePackageManagerMessages(
|
|||
await provider.postStateToWebview()
|
||||
console.log("Package Manager: State sent to webview")
|
||||
} catch (initError) {
|
||||
const errorMessage = `Package manager initialization failed: ${initError instanceof Error ? initError.message : String(initError)}`
|
||||
console.error("Error in package manager initialization:", initError)
|
||||
console.error("Error in package manager initialization:", initError)
|
||||
vscode.window.showErrorMessage(
|
||||
`Package manager initialization failed: ${initError instanceof Error ? initError.message : String(initError)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
provider.postMessageToWebview({
|
||||
type: "packageManagerButtonClicked",
|
||||
text: errorMessage,
|
||||
})
|
||||
// The state will already be updated with empty items by PackageManagerManager
|
||||
await provider.postStateToWebview()
|
||||
packageManagerManager.isFetching = false
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = `Failed to fetch package manager items: ${error instanceof Error ? error.message : String(error)}`
|
||||
console.error("Failed to fetch package manager items:", error)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to fetch package manager items: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
provider.postMessageToWebview({
|
||||
type: "packageManagerButtonClicked",
|
||||
text: errorMessage,
|
||||
})
|
||||
packageManagerManager.isFetching = false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -430,13 +430,6 @@ export class McpHub {
|
|||
config: z.infer<typeof ServerConfigSchema>,
|
||||
source: "global" | "project" = "global",
|
||||
): Promise<void> {
|
||||
// Check if a connection is already being established
|
||||
const existingConnection = this.findConnection(name, source)
|
||||
if (existingConnection && existingConnection.server.status === "connecting") {
|
||||
console.log(`Connection attempt already in progress for ${name}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove existing connection if it exists with the same source
|
||||
await this.deleteConnection(name, source)
|
||||
|
||||
|
|
@ -724,66 +717,58 @@ export class McpHub {
|
|||
newServers: Record<string, any>,
|
||||
source: "global" | "project" = "global",
|
||||
): Promise<void> {
|
||||
if (this.isConnecting) {
|
||||
console.log("Connection update already in progress, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
this.isConnecting = true
|
||||
try {
|
||||
this.removeAllFileWatchers()
|
||||
// Filter connections by source
|
||||
const currentConnections = this.connections.filter(
|
||||
(conn) => conn.server.source === source || (!conn.server.source && source === "global"),
|
||||
)
|
||||
const currentNames = new Set(currentConnections.map((conn) => conn.server.name))
|
||||
const newNames = new Set(Object.keys(newServers))
|
||||
this.removeAllFileWatchers()
|
||||
// Filter connections by source
|
||||
const currentConnections = this.connections.filter(
|
||||
(conn) => conn.server.source === source || (!conn.server.source && source === "global"),
|
||||
)
|
||||
const currentNames = new Set(currentConnections.map((conn) => conn.server.name))
|
||||
const newNames = new Set(Object.keys(newServers))
|
||||
|
||||
// Delete removed servers
|
||||
for (const name of currentNames) {
|
||||
if (!newNames.has(name)) {
|
||||
await this.deleteConnection(name, source)
|
||||
}
|
||||
// Delete removed servers
|
||||
for (const name of currentNames) {
|
||||
if (!newNames.has(name)) {
|
||||
await this.deleteConnection(name, source)
|
||||
}
|
||||
|
||||
// Update or add servers
|
||||
for (const [name, config] of Object.entries(newServers)) {
|
||||
// Only consider connections that match the current source
|
||||
const currentConnection = this.findConnection(name, source)
|
||||
|
||||
// Validate and transform the config
|
||||
let validatedConfig: z.infer<typeof ServerConfigSchema>
|
||||
try {
|
||||
validatedConfig = this.validateServerConfig(config, name)
|
||||
} catch (error) {
|
||||
this.showErrorMessage(`Invalid configuration for MCP server "${name}"`, error)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!currentConnection) {
|
||||
// New server
|
||||
try {
|
||||
this.setupFileWatcher(name, validatedConfig, source)
|
||||
await this.connectToServer(name, validatedConfig, source)
|
||||
} catch (error) {
|
||||
this.showErrorMessage(`Failed to connect to new MCP server ${name}`, error)
|
||||
}
|
||||
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
|
||||
// Existing server with changed config
|
||||
try {
|
||||
this.setupFileWatcher(name, validatedConfig, source)
|
||||
await this.deleteConnection(name, source)
|
||||
await this.connectToServer(name, validatedConfig, source)
|
||||
} catch (error) {
|
||||
this.showErrorMessage(`Failed to reconnect MCP server ${name}`, error)
|
||||
}
|
||||
}
|
||||
// If server exists with same config, do nothing
|
||||
}
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
} finally {
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
// Update or add servers
|
||||
for (const [name, config] of Object.entries(newServers)) {
|
||||
// Only consider connections that match the current source
|
||||
const currentConnection = this.findConnection(name, source)
|
||||
|
||||
// Validate and transform the config
|
||||
let validatedConfig: z.infer<typeof ServerConfigSchema>
|
||||
try {
|
||||
validatedConfig = this.validateServerConfig(config, name)
|
||||
} catch (error) {
|
||||
this.showErrorMessage(`Invalid configuration for MCP server "${name}"`, error)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!currentConnection) {
|
||||
// New server
|
||||
try {
|
||||
this.setupFileWatcher(name, validatedConfig, source)
|
||||
await this.connectToServer(name, validatedConfig, source)
|
||||
} catch (error) {
|
||||
this.showErrorMessage(`Failed to connect to new MCP server ${name}`, error)
|
||||
}
|
||||
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
|
||||
// Existing server with changed config
|
||||
try {
|
||||
this.setupFileWatcher(name, validatedConfig, source)
|
||||
await this.deleteConnection(name, source)
|
||||
await this.connectToServer(name, validatedConfig, source)
|
||||
} catch (error) {
|
||||
this.showErrorMessage(`Failed to reconnect MCP server ${name}`, error)
|
||||
}
|
||||
}
|
||||
// If server exists with same config, do nothing
|
||||
}
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
private setupFileWatcher(
|
||||
|
|
@ -855,34 +840,15 @@ export class McpHub {
|
|||
}
|
||||
|
||||
async restartConnection(serverName: string, source?: "global" | "project"): Promise<void> {
|
||||
// Check if already connecting
|
||||
if (this.isConnecting) {
|
||||
console.log(`Global connection attempt already in progress, skipping restart for ${serverName}`)
|
||||
return
|
||||
}
|
||||
|
||||
this.isConnecting = true
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
this.isConnecting = false
|
||||
return
|
||||
}
|
||||
|
||||
// Get existing connection and check its status
|
||||
// Get existing connection and update its status
|
||||
const connection = this.findConnection(serverName, source)
|
||||
if (!connection) {
|
||||
this.isConnecting = false
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already connecting
|
||||
if (connection.server.status === "connecting") {
|
||||
console.log(`Connection attempt already in progress for ${serverName}`)
|
||||
this.isConnecting = false
|
||||
return
|
||||
}
|
||||
|
||||
const config = connection.server.config
|
||||
const config = connection?.server.config
|
||||
if (config) {
|
||||
vscode.window.showInformationMessage(t("common:info.mcp_server_restarting", { serverName }))
|
||||
connection.server.status = "connecting"
|
||||
|
|
@ -902,18 +868,14 @@ export class McpHub {
|
|||
vscode.window.showInformationMessage(t("common:info.mcp_server_connected", { serverName }))
|
||||
} catch (validationError) {
|
||||
this.showErrorMessage(`Invalid configuration for MCP server "${serverName}"`, validationError)
|
||||
connection.server.status = "disconnected"
|
||||
}
|
||||
} catch (error) {
|
||||
this.showErrorMessage(`Failed to restart ${serverName} MCP server connection`, error)
|
||||
connection.server.status = "disconnected"
|
||||
} finally {
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
this.isConnecting = false
|
||||
}
|
||||
} else {
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
private async notifyWebviewOfServerChanges(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -5,18 +5,19 @@
|
|||
/**
|
||||
* Default package manager repository URL
|
||||
*/
|
||||
export const DEFAULT_PACKAGE_MANAGER_REPO_URL = "https://github.com/Smartsheet-JB-Brown/Package-Manager-Test";
|
||||
export const DEFAULT_PACKAGE_MANAGER_REPO_URL =
|
||||
"https://github.com/RooVetGit/Roo-Code/tree/main/package-manager-template"
|
||||
|
||||
/**
|
||||
* Default package manager repository name
|
||||
*/
|
||||
export const DEFAULT_PACKAGE_MANAGER_REPO_NAME = "Roo Code";
|
||||
export const DEFAULT_PACKAGE_MANAGER_REPO_NAME = "Roo Code Package Manager Template"
|
||||
|
||||
/**
|
||||
* Default package manager source
|
||||
*/
|
||||
export const DEFAULT_PACKAGE_MANAGER_SOURCE = {
|
||||
url: DEFAULT_PACKAGE_MANAGER_REPO_URL,
|
||||
name: DEFAULT_PACKAGE_MANAGER_REPO_NAME,
|
||||
enabled: true
|
||||
};
|
||||
url: DEFAULT_PACKAGE_MANAGER_REPO_URL,
|
||||
name: DEFAULT_PACKAGE_MANAGER_REPO_NAME,
|
||||
enabled: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
# Minimal Package Manager Repository
|
||||
|
||||
This is a minimal example of a package manager repository structure that meets the basic requirements. The structure is intentionally kept as simple as possible to help diagnose any validation issues.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── metadata.en.yml # Required: Repository metadata (must be exactly this name)
|
||||
└── mcp-servers/ # Optional: Directory for MCP servers
|
||||
└── test-server/ # Must be a directory
|
||||
└── metadata.en.yml # Must match pattern metadata.[locale].yml
|
||||
```
|
||||
|
||||
## metadata.en.yml
|
||||
|
||||
```yaml
|
||||
name: Test Repository
|
||||
description: A minimal test repository
|
||||
version: 1.0.0
|
||||
```
|
||||
|
||||
## mcp-servers/test-server/metadata.en.yml
|
||||
|
||||
```yaml
|
||||
name: Test Server
|
||||
description: A minimal test server
|
||||
type: mcp server
|
||||
version: 1.0.0
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
1. File names must be exactly:
|
||||
- metadata.en.yml (not metadata.yml or any other variation)
|
||||
2. Components must be in directories
|
||||
3. No empty lines in YAML files
|
||||
4. No quotes around values
|
||||
5. No extra fields
|
||||
6. No special characters
|
||||
7. No complex YAML features (arrays, nested objects, etc.)
|
||||
|
||||
Try copying this exact structure to your GitHub repository to test. The validation should pass with this minimal setup.
|
||||
|
||||
## Validation Process
|
||||
|
||||
1. First, it checks for metadata.en.yml in the root
|
||||
2. Then it scans for component directories
|
||||
3. For each directory, it looks for metadata.en.yml files
|
||||
4. Each metadata file is validated for required fields
|
||||
5. Component metadata must have a valid type ("mcp server", "mode", "prompt", or "package")
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
name: Test Server
|
||||
description: A minimal test server
|
||||
type: mcp server
|
||||
version: 1.0.0
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
name: Test Repository
|
||||
description: A minimal test repository
|
||||
version: 1.0.0
|
||||
|
|
@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"
|
|||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { PackageManagerItem, PackageManagerSource } from "../../../../src/services/package-manager/types"
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "cmdk"
|
||||
|
||||
|
|
@ -153,85 +154,109 @@ const PackageManagerView: React.FC<PackageManagerViewProps> = ({ onDone }) => {
|
|||
const [items, setItems] = useState<PackageManagerItem[]>([])
|
||||
const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse")
|
||||
const [refreshingUrls, setRefreshingUrls] = useState<string[]>([])
|
||||
|
||||
// Clear items when switching to sources tab
|
||||
useEffect(() => {
|
||||
if (activeTab === "sources") {
|
||||
setItems([])
|
||||
}
|
||||
}, [activeTab])
|
||||
const [filters, setFilters] = useState({ type: "", search: "", tags: [] as string[] })
|
||||
const [tagSearch, setTagSearch] = useState("")
|
||||
const [isTagInputActive, setIsTagInputActive] = useState(false)
|
||||
const [sortBy, setSortBy] = useState("name")
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc")
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const isManualRefresh = useRef(false)
|
||||
const hasInitialFetch = useRef(false)
|
||||
const lastSourcesKey = useRef<string | null>(null)
|
||||
const fetchTimeoutRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
const fetchPackageManagerItems = useCallback(() => {
|
||||
if (!isFetching) {
|
||||
setIsFetching(true)
|
||||
try {
|
||||
vscode.postMessage({
|
||||
type: "fetchPackageManagerItems",
|
||||
forceRefresh: true,
|
||||
} as any)
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch package manager items:", error)
|
||||
setIsFetching(false)
|
||||
}
|
||||
// Clear any pending fetch timeout
|
||||
if (fetchTimeoutRef.current) {
|
||||
clearTimeout(fetchTimeoutRef.current)
|
||||
}
|
||||
}, [isFetching])
|
||||
|
||||
// Clear items immediately when fetching starts
|
||||
setItems([])
|
||||
setIsFetching(true)
|
||||
|
||||
try {
|
||||
vscode.postMessage({
|
||||
type: "fetchPackageManagerItems",
|
||||
forceRefresh: true,
|
||||
} as any)
|
||||
|
||||
// Set a timeout to reset isFetching if no response is received
|
||||
fetchTimeoutRef.current = setTimeout(() => {
|
||||
console.log("Fetch timeout reached, resetting state")
|
||||
setIsFetching(false)
|
||||
setItems([]) // Clear items on timeout
|
||||
vscode.window.showErrorMessage("Package manager items fetch timed out. Please try again.")
|
||||
}, 30000) // 30 second timeout to match server timeout
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch package manager items:", error)
|
||||
setIsFetching(false)
|
||||
setItems([]) // Clear items on error
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Fetch items on mount
|
||||
useEffect(() => {
|
||||
fetchPackageManagerItems()
|
||||
}, [fetchPackageManagerItems])
|
||||
|
||||
// Set hasInitialFetch after the first fetch completes
|
||||
// Fetch items when sources change
|
||||
useEffect(() => {
|
||||
if (!isFetching) {
|
||||
hasInitialFetch.current = true
|
||||
if (packageManagerSources && activeTab === "browse") {
|
||||
fetchPackageManagerItems()
|
||||
}
|
||||
}, [isFetching])
|
||||
|
||||
useEffect(() => {
|
||||
if (packageManagerSources && !isFetching && packageManagerSources.length > 0) {
|
||||
const sourcesKey = JSON.stringify(packageManagerSources.map((s) => s.url))
|
||||
if (sourcesKey !== lastSourcesKey.current && !isManualRefresh.current) {
|
||||
lastSourcesKey.current = sourcesKey
|
||||
// Don't fetch if this is the initial sources load
|
||||
if (hasInitialFetch.current) {
|
||||
fetchPackageManagerItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [packageManagerSources, fetchPackageManagerItems, isFetching])
|
||||
|
||||
}, [packageManagerSources, fetchPackageManagerItems, activeTab])
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
|
||||
if (message.type === "action" && message.action === "packageManagerButtonClicked") {
|
||||
setTimeout(() => {
|
||||
vscode.postMessage({
|
||||
type: "fetchPackageManagerItems",
|
||||
forceRefresh: true,
|
||||
} as any)
|
||||
}, 100)
|
||||
if (message.type === "packageManagerButtonClicked") {
|
||||
if (message.text) {
|
||||
// This is an error message
|
||||
console.error("Package manager error:", message.text)
|
||||
if (fetchTimeoutRef.current) {
|
||||
clearTimeout(fetchTimeoutRef.current)
|
||||
}
|
||||
setIsFetching(false)
|
||||
setItems([]) // Clear items on error
|
||||
} else {
|
||||
// This is a refresh request
|
||||
fetchPackageManagerItems()
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === "repositoryRefreshComplete" && message.url) {
|
||||
setRefreshingUrls((prev) => prev.filter((url) => url !== message.url))
|
||||
// Trigger a fetch to update items after refresh
|
||||
fetchPackageManagerItems()
|
||||
}
|
||||
|
||||
if (message.type === "state" && message.state?.packageManagerItems) {
|
||||
if (message.type === "state" && message.state?.packageManagerItems !== undefined) {
|
||||
// Clear fetch timeout
|
||||
if (fetchTimeoutRef.current) {
|
||||
clearTimeout(fetchTimeoutRef.current)
|
||||
}
|
||||
|
||||
const receivedItems = message.state.packageManagerItems || []
|
||||
console.log("Received package manager items:", receivedItems.length)
|
||||
setItems([...receivedItems])
|
||||
setTimeout(() => {
|
||||
setIsFetching(false)
|
||||
isManualRefresh.current = false
|
||||
}, 0)
|
||||
setIsFetching(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
}, [])
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage)
|
||||
// Clear any pending timeout on unmount
|
||||
if (fetchTimeoutRef.current) {
|
||||
clearTimeout(fetchTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [fetchPackageManagerItems])
|
||||
|
||||
const filteredItems = items.filter((item) => {
|
||||
if (filters.type && item.type !== filters.type) {
|
||||
|
|
@ -290,18 +315,26 @@ const PackageManagerView: React.FC<PackageManagerViewProps> = ({ onDone }) => {
|
|||
|
||||
return (
|
||||
<Tab>
|
||||
<TabHeader className="flex justify-between items-center">
|
||||
<TabHeader className="flex justify-between items-center sticky top-0 z-10 bg-vscode-editor-background border-b border-vscode-panel-border">
|
||||
<div className="flex items-center">
|
||||
<h3 className="text-vscode-foreground m-0">Package Manager</h3>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={activeTab === "browse" ? "default" : "secondary"}
|
||||
className={cn(
|
||||
activeTab === "browse" &&
|
||||
"bg-vscode-button-background text-vscode-button-foreground hover:bg-vscode-button-hoverBackground",
|
||||
)}
|
||||
onClick={() => setActiveTab("browse")}>
|
||||
Browse
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === "sources" ? "default" : "secondary"}
|
||||
className={cn(
|
||||
activeTab === "sources" &&
|
||||
"bg-vscode-button-background text-vscode-button-foreground hover:bg-vscode-button-hoverBackground",
|
||||
)}
|
||||
onClick={() => setActiveTab("sources")}>
|
||||
Sources
|
||||
</Button>
|
||||
|
|
@ -443,14 +476,7 @@ const PackageManagerView: React.FC<PackageManagerViewProps> = ({ onDone }) => {
|
|||
{sortedItems.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-vscode-descriptionForeground">
|
||||
<p>No package manager items found</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
isManualRefresh.current = true
|
||||
setIsFetching(false)
|
||||
fetchPackageManagerItems()
|
||||
}}
|
||||
className="mt-4"
|
||||
disabled={isFetching}>
|
||||
<Button onClick={fetchPackageManagerItems} className="mt-4" disabled={isFetching}>
|
||||
<span
|
||||
className={`codicon ${isFetching ? "codicon-sync codicon-modifier-spin" : "codicon-refresh"} mr-2`}></span>
|
||||
{isFetching ? "Refreshing..." : "Refresh"}
|
||||
|
|
@ -462,20 +488,13 @@ const PackageManagerView: React.FC<PackageManagerViewProps> = ({ onDone }) => {
|
|||
<p className="text-vscode-descriptionForeground">
|
||||
{`${sortedItems.length} items found`}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
isManualRefresh.current = true
|
||||
setIsFetching(false)
|
||||
fetchPackageManagerItems()
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isFetching}>
|
||||
<Button onClick={fetchPackageManagerItems} size="sm" disabled={isFetching}>
|
||||
<span
|
||||
className={`codicon ${isFetching ? "codicon-sync codicon-modifier-spin" : "codicon-refresh"} mr-2`}></span>
|
||||
{isFetching ? "Refreshing..." : "Refresh"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 pb-4">
|
||||
{sortedItems.map((item) => (
|
||||
<PackageManagerItemCard
|
||||
key={`${item.repoUrl}-${item.name}`}
|
||||
|
|
@ -497,6 +516,7 @@ const PackageManagerView: React.FC<PackageManagerViewProps> = ({ onDone }) => {
|
|||
setRefreshingUrls={setRefreshingUrls}
|
||||
onSourcesChange={(sources) => {
|
||||
setPackageManagerSources(sources)
|
||||
setItems([]) // Clear items when sources change
|
||||
vscode.postMessage({ type: "packageManagerSources", sources })
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue