Skip to content

Commit 1a11665

Browse files
authored
feat: installedMetadata MVP (#10)
* feat: `installedMetadata` MVP * feat: removal support for installed items (+ general refactors)
1 parent e25b3e7 commit 1a11665

21 files changed

+646
-193
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@
447447
"cheerio": "^1.0.0",
448448
"chokidar": "^4.0.1",
449449
"clone-deep": "^4.0.1",
450-
"config-rocket": "^0.5.3",
450+
"config-rocket": "^0.5.8",
451451
"default-shell": "^2.2.0",
452452
"delay": "^6.0.0",
453453
"diff": "^5.2.0",

src/core/webview/ClineProvider.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
7474
return this._workspaceTracker
7575
}
7676
protected mcpHub?: McpHub // Change from private to protected
77-
private marketplaceManager?: MarketplaceManager
77+
private marketplaceManager: MarketplaceManager
7878

7979
public isViewLaunched = false
8080
public settingsImportedAt?: number
@@ -114,6 +114,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
114114
.catch((error) => {
115115
this.log(`Failed to initialize MCP Hub: ${error}`)
116116
})
117+
118+
this.marketplaceManager = new MarketplaceManager(this.context)
117119
}
118120

119121
// Adds a new Cline instance to clineStack, marking the start of a new task.
@@ -218,6 +220,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
218220
this._workspaceTracker = undefined
219221
await this.mcpHub?.unregisterClient()
220222
this.mcpHub = undefined
223+
this.marketplaceManager?.cleanup()
221224
this.customModesManager?.dispose()
222225
this.log("Disposed all disposables")
223226
ClineProvider.activeInstances.delete(this)
@@ -1226,7 +1229,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
12261229
const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
12271230
const cwd = this.cwd
12281231

1229-
const marketplaceItems = this.marketplaceManager?.getCurrentItems() || []
1232+
const marketplaceItems = this.marketplaceManager.getCurrentItems() || []
1233+
const marketplaceInstalledMetadata = this.marketplaceManager.IMM.fullMetadata
12301234
// Check if there's a system prompt override for the current mode
12311235
const currentMode = mode ?? defaultModeSlug
12321236
const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)
@@ -1235,6 +1239,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
12351239
version: this.context.extension?.packageJSON?.version ?? "",
12361240
marketplaceItems,
12371241
marketplaceSources: marketplaceSources ?? [],
1242+
marketplaceInstalledMetadata,
12381243
apiConfiguration,
12391244
customInstructions,
12401245
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,

src/core/webview/marketplaceMessageHandler.ts

Lines changed: 137 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,83 @@ export async function handleMarketplaceMessages(
2424
await provider.contextProxy.setValue(key, value)
2525

2626
switch (message.type) {
27-
case "webviewDidLaunch": {
28-
// For webviewDidLaunch, we don't do anything - marketplace items will be loaded by explicit fetchMarketplaceItems
27+
case "openExternal": {
28+
if (message.url) {
29+
try {
30+
vscode.env.openExternal(vscode.Uri.parse(message.url))
31+
} catch (error) {
32+
console.error(
33+
`Marketplace: Failed to open URL: ${error instanceof Error ? error.message : String(error)}`,
34+
)
35+
vscode.window.showErrorMessage(
36+
`Failed to open URL: ${error instanceof Error ? error.message : String(error)}`,
37+
)
38+
}
39+
} else {
40+
console.error("Marketplace: openExternal called without a URL")
41+
}
2942
return true
3043
}
44+
45+
case "marketplaceSources": {
46+
if (message.sources) {
47+
// Enforce maximum of 10 sources
48+
const MAX_SOURCES = 10
49+
let updatedSources: MarketplaceSource[]
50+
51+
if (message.sources.length > MAX_SOURCES) {
52+
// Truncate to maximum allowed and show warning
53+
updatedSources = message.sources.slice(0, MAX_SOURCES)
54+
vscode.window.showWarningMessage(
55+
`Maximum of ${MAX_SOURCES} marketplace sources allowed. Additional sources have been removed.`,
56+
)
57+
} else {
58+
updatedSources = message.sources
59+
}
60+
61+
// Validate sources using the validation utility
62+
const validationErrors = validateSources(updatedSources)
63+
64+
// Filter out invalid sources
65+
if (validationErrors.length > 0) {
66+
// Create a map of invalid indices
67+
const invalidIndices = new Set<number>()
68+
validationErrors.forEach((error: ValidationError) => {
69+
// Extract index from error message (Source #X: ...)
70+
const match = error.message.match(/Source #(\d+):/)
71+
if (match && match[1]) {
72+
const index = parseInt(match[1], 10) - 1 // Convert to 0-based index
73+
if (index >= 0 && index < updatedSources.length) {
74+
invalidIndices.add(index)
75+
}
76+
}
77+
})
78+
79+
// Filter out invalid sources
80+
updatedSources = updatedSources.filter((_, index) => !invalidIndices.has(index))
81+
82+
// Show validation errors
83+
const errorMessage = `Marketplace sources validation failed:\n${validationErrors.map((e: ValidationError) => e.message).join("\n")}`
84+
console.error(errorMessage)
85+
vscode.window.showErrorMessage(errorMessage)
86+
}
87+
88+
// Update the global state with the validated sources
89+
await updateGlobalState("marketplaceSources", updatedSources)
90+
91+
// Clean up cache directories for repositories that are no longer in the sources list
92+
try {
93+
await marketplaceManager.cleanupCacheDirectories(updatedSources)
94+
} catch (error) {
95+
console.error("Marketplace: Error during cache cleanup:", error)
96+
}
97+
98+
// Update the webview with the new state
99+
await provider.postStateToWebview()
100+
}
101+
return true
102+
}
103+
31104
case "fetchMarketplaceItems": {
32105
// Prevent multiple simultaneous fetches
33106
if (marketplaceManager.isFetching) {
@@ -116,81 +189,6 @@ export async function handleMarketplaceMessages(
116189
}
117190
return true
118191
}
119-
case "marketplaceSources": {
120-
if (message.sources) {
121-
// Enforce maximum of 10 sources
122-
const MAX_SOURCES = 10
123-
let updatedSources: MarketplaceSource[]
124-
125-
if (message.sources.length > MAX_SOURCES) {
126-
// Truncate to maximum allowed and show warning
127-
updatedSources = message.sources.slice(0, MAX_SOURCES)
128-
vscode.window.showWarningMessage(
129-
`Maximum of ${MAX_SOURCES} marketplace sources allowed. Additional sources have been removed.`,
130-
)
131-
} else {
132-
updatedSources = message.sources
133-
}
134-
135-
// Validate sources using the validation utility
136-
const validationErrors = validateSources(updatedSources)
137-
138-
// Filter out invalid sources
139-
if (validationErrors.length > 0) {
140-
// Create a map of invalid indices
141-
const invalidIndices = new Set<number>()
142-
validationErrors.forEach((error: ValidationError) => {
143-
// Extract index from error message (Source #X: ...)
144-
const match = error.message.match(/Source #(\d+):/)
145-
if (match && match[1]) {
146-
const index = parseInt(match[1], 10) - 1 // Convert to 0-based index
147-
if (index >= 0 && index < updatedSources.length) {
148-
invalidIndices.add(index)
149-
}
150-
}
151-
})
152-
153-
// Filter out invalid sources
154-
updatedSources = updatedSources.filter((_, index) => !invalidIndices.has(index))
155-
156-
// Show validation errors
157-
const errorMessage = `Marketplace sources validation failed:\n${validationErrors.map((e: ValidationError) => e.message).join("\n")}`
158-
console.error(errorMessage)
159-
vscode.window.showErrorMessage(errorMessage)
160-
}
161-
162-
// Update the global state with the validated sources
163-
await updateGlobalState("marketplaceSources", updatedSources)
164-
165-
// Clean up cache directories for repositories that are no longer in the sources list
166-
try {
167-
await marketplaceManager.cleanupCacheDirectories(updatedSources)
168-
} catch (error) {
169-
console.error("Marketplace: Error during cache cleanup:", error)
170-
}
171-
172-
// Update the webview with the new state
173-
await provider.postStateToWebview()
174-
}
175-
return true
176-
}
177-
case "openExternal": {
178-
if (message.url) {
179-
try {
180-
vscode.env.openExternal(vscode.Uri.parse(message.url))
181-
} catch (error) {
182-
console.error(
183-
`Marketplace: Failed to open URL: ${error instanceof Error ? error.message : String(error)}`,
184-
)
185-
vscode.window.showErrorMessage(
186-
`Failed to open URL: ${error instanceof Error ? error.message : String(error)}`,
187-
)
188-
}
189-
} else {
190-
console.error("Marketplace: openExternal called without a URL")
191-
}
192-
return true
193-
}
194192

195193
case "filterMarketplaceItems": {
196194
if (message.filters) {
@@ -210,47 +208,6 @@ export async function handleMarketplaceMessages(
210208
return true
211209
}
212210

213-
case "installMarketplaceItem": {
214-
if (message.mpItem) {
215-
try {
216-
await marketplaceManager.installMarketplaceItem(message.mpItem, message.mpInstallOptions)
217-
} catch (error) {
218-
vscode.window.showErrorMessage(
219-
`Failed to install item "${message.mpItem.name}":\n${error instanceof Error ? error.message : String(error)}`,
220-
)
221-
}
222-
} else {
223-
console.error("Marketplace: installMarketplaceItem called without `mpItem`")
224-
}
225-
return true
226-
}
227-
case "installMarketplaceItemWithParameters":
228-
if (message.payload) {
229-
const result = installMarketplaceItemWithParametersPayloadSchema.safeParse(message.payload)
230-
231-
if (result.success) {
232-
const { item, parameters } = result.data
233-
234-
try {
235-
await marketplaceManager.installMarketplaceItem(item, { parameters })
236-
} catch (error) {
237-
console.error(`Error submitting marketplace parameters: ${error}`)
238-
vscode.window.showErrorMessage(
239-
`Failed to install item "${item.name}":\n${error instanceof Error ? error.message : String(error)}`,
240-
)
241-
}
242-
} else {
243-
console.error("Invalid payload for installMarketplaceItemWithParameters message:", message.payload)
244-
vscode.window.showErrorMessage(
245-
'Invalid "payload" received for installation: item or parameters missing.',
246-
)
247-
}
248-
}
249-
return true
250-
case "cancelMarketplaceInstall":
251-
vscode.window.showInformationMessage("Marketplace installation cancelled.")
252-
return true
253-
254211
case "refreshMarketplaceSource": {
255212
if (message.url) {
256213
try {
@@ -298,6 +255,68 @@ export async function handleMarketplaceMessages(
298255
return true
299256
}
300257

258+
case "installMarketplaceItem": {
259+
if (message.mpItem) {
260+
try {
261+
await marketplaceManager
262+
.installMarketplaceItem(message.mpItem, message.mpInstallOptions)
263+
.then(async (r) => r === "$COMMIT" && (await provider.postStateToWebview()))
264+
} catch (error) {
265+
vscode.window.showErrorMessage(
266+
`Failed to install item "${message.mpItem.name}":\n${error instanceof Error ? error.message : String(error)}`,
267+
)
268+
}
269+
} else {
270+
console.error("Marketplace: installMarketplaceItem called without `mpItem`")
271+
}
272+
return true
273+
}
274+
case "installMarketplaceItemWithParameters":
275+
if (message.payload) {
276+
const result = installMarketplaceItemWithParametersPayloadSchema.safeParse(message.payload)
277+
278+
if (result.success) {
279+
const { item, parameters } = result.data
280+
281+
try {
282+
await marketplaceManager
283+
.installMarketplaceItem(item, { parameters })
284+
.then(async (r) => r === "$COMMIT" && (await provider.postStateToWebview()))
285+
} catch (error) {
286+
console.error(`Error submitting marketplace parameters: ${error}`)
287+
vscode.window.showErrorMessage(
288+
`Failed to install item "${item.name}":\n${error instanceof Error ? error.message : String(error)}`,
289+
)
290+
}
291+
} else {
292+
console.error("Invalid payload for installMarketplaceItemWithParameters message:", message.payload)
293+
vscode.window.showErrorMessage(
294+
'Invalid "payload" received for installation: item or parameters missing.',
295+
)
296+
}
297+
}
298+
return true
299+
case "cancelMarketplaceInstall": {
300+
vscode.window.showInformationMessage("Marketplace installation cancelled.")
301+
return true
302+
}
303+
case "removeInstalledMarketplaceItem": {
304+
if (message.mpItem) {
305+
try {
306+
await marketplaceManager
307+
.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
308+
.then(async (r) => r === "$COMMIT" && (await provider.postStateToWebview()))
309+
} catch (error) {
310+
vscode.window.showErrorMessage(
311+
`Failed to remove item "${message.mpItem.name}":\n${error instanceof Error ? error.message : String(error)}`,
312+
)
313+
}
314+
} else {
315+
console.error("Marketplace: removeInstalledMarketplaceItem called without `mpItem`")
316+
}
317+
return true
318+
}
319+
301320
default:
302321
return false
303322
}

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,15 @@ import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-
4343
import { getModels } from "../../api/providers/fetchers/cache"
4444

4545
const marketplaceMessages = new Set([
46-
"marketplaceSources",
4746
"openExternal",
47+
"marketplaceSources",
4848
"fetchMarketplaceItems",
49+
"filterMarketplaceItems",
50+
"refreshMarketplaceSource",
4951
"installMarketplaceItem",
5052
"installMarketplaceItemWithParameters",
5153
"cancelMarketplaceInstall",
52-
"refreshMarketplaceSource",
53-
"filterMarketplaceItems",
54+
"removeInstalledMarketplaceItem",
5455
])
5556

5657
export const webviewMessageHandler = async (

0 commit comments

Comments
 (0)