Skip to content

Commit b76e323

Browse files
committed
feat: auto-refresh marketplace data when organization settings change
- Added organizationSettingsVersion to ExtensionState type - Modified ClineProvider to include version in state sent to webview - Updated CloudService callback to trigger marketplace refresh on settings change - Added logic to MarketplaceView to detect version changes and request data refresh - Added comprehensive tests for the new functionality This ensures marketplace items (MCPs and modes) stay in sync when organization settings are updated in the cloud.
1 parent 75f93c4 commit b76e323

File tree

6 files changed

+183
-62
lines changed

6 files changed

+183
-62
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,7 @@ export class ClineProvider
16171617
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
16181618
sharingEnabled: sharingEnabled ?? false,
16191619
organizationAllowList,
1620+
organizationSettingsVersion: this.getOrganizationSettingsVersion(),
16201621
condensingApiConfigId,
16211622
customCondensingPrompt,
16221623
codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
@@ -1978,4 +1979,19 @@ export class ClineProvider
19781979
...gitInfo,
19791980
}
19801981
}
1982+
1983+
/**
1984+
* Get the current organization settings version
1985+
*/
1986+
private getOrganizationSettingsVersion(): number | undefined {
1987+
try {
1988+
if (CloudService.hasInstance()) {
1989+
const settings = CloudService.instance.getOrganizationSettings()
1990+
return settings?.version
1991+
}
1992+
} catch (error) {
1993+
this.log(`Failed to get organization settings version: ${error}`)
1994+
}
1995+
return undefined
1996+
}
19811997
}

src/extension.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,14 @@ export async function activate(context: vscode.ExtensionContext) {
7676

7777
// Initialize Roo Code Cloud service.
7878
await CloudService.createInstance(context, {
79-
stateChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),
79+
stateChanged: () => {
80+
const provider = ClineProvider.getVisibleInstance()
81+
if (provider) {
82+
provider.postStateToWebview()
83+
// Also refresh marketplace data when organization settings change
84+
provider.fetchMarketplaceData()
85+
}
86+
},
8087
log: cloudLogger,
8188
})
8289

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ export type ExtensionState = Pick<
313313
cloudApiUrl?: string
314314
sharingEnabled: boolean
315315
organizationAllowList: OrganizationAllowList
316+
organizationSettingsVersion?: number
316317

317318
autoCondenseContext: boolean
318319
autoCondenseContextPercent: number

webview-ui/src/components/marketplace/MarketplaceView.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useMemo } from "react"
1+
import { useState, useEffect, useMemo, useContext } from "react"
22
import { Button } from "@/components/ui/button"
33
import { Tab, TabContent, TabHeader } from "../common/Tab"
44
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
@@ -8,6 +8,7 @@ import { vscode } from "@/utils/vscode"
88
import { MarketplaceListView } from "./MarketplaceListView"
99
import { cn } from "@/lib/utils"
1010
import { TooltipProvider } from "@/components/ui/tooltip"
11+
import { ExtensionStateContext } from "@/context/ExtensionStateContext"
1112

1213
interface MarketplaceViewProps {
1314
onDone?: () => void
@@ -18,6 +19,25 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace
1819
const { t } = useAppTranslation()
1920
const [state, manager] = useStateManager(stateManager)
2021
const [hasReceivedInitialState, setHasReceivedInitialState] = useState(false)
22+
const extensionState = useContext(ExtensionStateContext)
23+
const [lastOrganizationSettingsVersion, setLastOrganizationSettingsVersion] = useState<number | undefined>(
24+
extensionState?.organizationSettingsVersion,
25+
)
26+
27+
// Track when organization settings version changes and trigger refresh
28+
useEffect(() => {
29+
if (
30+
extensionState?.organizationSettingsVersion !== undefined &&
31+
lastOrganizationSettingsVersion !== undefined &&
32+
extensionState.organizationSettingsVersion !== lastOrganizationSettingsVersion
33+
) {
34+
// Organization settings version changed, refresh marketplace data
35+
vscode.postMessage({
36+
type: "fetchMarketplaceData",
37+
})
38+
}
39+
setLastOrganizationSettingsVersion(extensionState?.organizationSettingsVersion)
40+
}, [extensionState?.organizationSettingsVersion, lastOrganizationSettingsVersion])
2141

2242
// Track when we receive the initial state
2343
useEffect(() => {
Lines changed: 134 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,161 @@
1-
import { render, screen } from "@/utils/test-utils"
2-
import userEvent from "@testing-library/user-event"
3-
1+
import { render, waitFor } from "@testing-library/react"
2+
import { vi, describe, it, expect, beforeEach } from "vitest"
43
import { MarketplaceView } from "../MarketplaceView"
54
import { MarketplaceViewStateManager } from "../MarketplaceViewStateManager"
5+
import { ExtensionStateContext } from "@/context/ExtensionStateContext"
6+
import { vscode } from "@/utils/vscode"
67

8+
// Mock vscode API
79
vi.mock("@/utils/vscode", () => ({
810
vscode: {
911
postMessage: vi.fn(),
10-
getState: vi.fn(() => ({})),
11-
setState: vi.fn(),
1212
},
1313
}))
1414

15+
// Mock the translation hook
1516
vi.mock("@/i18n/TranslationContext", () => ({
1617
useAppTranslation: () => ({
1718
t: (key: string) => key,
1819
}),
1920
}))
2021

21-
vi.mock("../useStateManager", () => ({
22-
useStateManager: () => [
23-
{
24-
allItems: [],
25-
displayItems: [],
26-
isFetching: false,
27-
activeTab: "mcp",
28-
filters: { type: "", search: "", tags: [] },
29-
},
30-
{
31-
transition: vi.fn(),
32-
onStateChange: vi.fn(() => vi.fn()),
33-
},
34-
],
35-
}))
36-
37-
vi.mock("../MarketplaceListView", () => ({
38-
MarketplaceListView: ({ filterByType }: { filterByType: string }) => (
39-
<div data-testid="marketplace-list-view">MarketplaceListView - {filterByType}</div>
40-
),
41-
}))
42-
43-
// Mock Tab components to avoid ExtensionStateContext dependency
44-
vi.mock("@/components/common/Tab", () => ({
45-
Tab: ({ children, ...props }: any) => <div {...props}>{children}</div>,
46-
TabHeader: ({ children, ...props }: any) => <div {...props}>{children}</div>,
47-
TabContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
48-
TabList: ({ children, ...props }: any) => <div {...props}>{children}</div>,
49-
TabTrigger: ({ children, ...props }: any) => <button {...props}>{children}</button>,
50-
}))
51-
5222
describe("MarketplaceView", () => {
53-
const mockOnDone = vi.fn()
54-
const mockStateManager = new MarketplaceViewStateManager()
23+
let stateManager: MarketplaceViewStateManager
24+
let mockExtensionState: any
5525

5626
beforeEach(() => {
5727
vi.clearAllMocks()
28+
stateManager = new MarketplaceViewStateManager()
29+
30+
// Initialize state manager with some test data
31+
stateManager.transition({
32+
type: "FETCH_COMPLETE",
33+
payload: {
34+
items: [
35+
{
36+
id: "test-mcp",
37+
name: "Test MCP",
38+
type: "mcp" as const,
39+
description: "Test MCP server",
40+
tags: ["test"],
41+
content: "Test content",
42+
url: "https://test.com",
43+
author: "Test Author",
44+
},
45+
],
46+
},
47+
})
48+
49+
mockExtensionState = {
50+
organizationSettingsVersion: 1,
51+
// Add other required properties for the context
52+
didHydrateState: true,
53+
showWelcome: false,
54+
theme: {},
55+
mcpServers: [],
56+
filePaths: [],
57+
openedTabs: [],
58+
commands: [],
59+
organizationAllowList: { allowAll: true, providers: {} },
60+
cloudIsAuthenticated: false,
61+
sharingEnabled: false,
62+
hasOpenedModeSelector: false,
63+
setHasOpenedModeSelector: vi.fn(),
64+
alwaysAllowFollowupQuestions: false,
65+
setAlwaysAllowFollowupQuestions: vi.fn(),
66+
followupAutoApproveTimeoutMs: 60000,
67+
setFollowupAutoApproveTimeoutMs: vi.fn(),
68+
profileThresholds: {},
69+
setProfileThresholds: vi.fn(),
70+
// ... other required context properties
71+
}
5872
})
5973

60-
it("renders without crashing", () => {
61-
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
62-
63-
expect(screen.getByText("marketplace:title")).toBeInTheDocument()
64-
expect(screen.getByText("marketplace:done")).toBeInTheDocument()
65-
})
66-
67-
it("calls onDone when Done button is clicked", async () => {
68-
const user = userEvent.setup()
69-
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
70-
71-
await user.click(screen.getByText("marketplace:done"))
72-
expect(mockOnDone).toHaveBeenCalledTimes(1)
74+
it("should trigger fetchMarketplaceData when organization settings version changes", async () => {
75+
const { rerender } = render(
76+
<ExtensionStateContext.Provider value={mockExtensionState}>
77+
<MarketplaceView stateManager={stateManager} />
78+
</ExtensionStateContext.Provider>,
79+
)
80+
81+
// Initial render should not trigger fetch (version hasn't changed)
82+
expect(vscode.postMessage).not.toHaveBeenCalledWith({
83+
type: "fetchMarketplaceData",
84+
})
85+
86+
// Update the organization settings version
87+
mockExtensionState = {
88+
...mockExtensionState,
89+
organizationSettingsVersion: 2,
90+
}
91+
92+
// Re-render with updated context
93+
rerender(
94+
<ExtensionStateContext.Provider value={mockExtensionState}>
95+
<MarketplaceView stateManager={stateManager} />
96+
</ExtensionStateContext.Provider>,
97+
)
98+
99+
// Wait for the effect to run
100+
await waitFor(() => {
101+
expect(vscode.postMessage).toHaveBeenCalledWith({
102+
type: "fetchMarketplaceData",
103+
})
104+
})
73105
})
74106

75-
it("renders tab buttons", () => {
76-
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
77-
78-
expect(screen.getByText("MCP")).toBeInTheDocument()
79-
expect(screen.getByText("Modes")).toBeInTheDocument()
107+
it("should not trigger fetchMarketplaceData when organization settings version is undefined", async () => {
108+
// Start with undefined version
109+
mockExtensionState = {
110+
...mockExtensionState,
111+
organizationSettingsVersion: undefined,
112+
}
113+
114+
const { rerender } = render(
115+
<ExtensionStateContext.Provider value={mockExtensionState}>
116+
<MarketplaceView stateManager={stateManager} />
117+
</ExtensionStateContext.Provider>,
118+
)
119+
120+
// Update to a defined version
121+
mockExtensionState = {
122+
...mockExtensionState,
123+
organizationSettingsVersion: 1,
124+
}
125+
126+
rerender(
127+
<ExtensionStateContext.Provider value={mockExtensionState}>
128+
<MarketplaceView stateManager={stateManager} />
129+
</ExtensionStateContext.Provider>,
130+
)
131+
132+
// Should not trigger fetch when transitioning from undefined
133+
await waitFor(() => {
134+
expect(vscode.postMessage).not.toHaveBeenCalledWith({
135+
type: "fetchMarketplaceData",
136+
})
137+
})
80138
})
81139

82-
it("renders MarketplaceListView", () => {
83-
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
84-
85-
expect(screen.getByTestId("marketplace-list-view")).toBeInTheDocument()
140+
it("should not trigger fetchMarketplaceData when organization settings version remains the same", async () => {
141+
const { rerender } = render(
142+
<ExtensionStateContext.Provider value={mockExtensionState}>
143+
<MarketplaceView stateManager={stateManager} />
144+
</ExtensionStateContext.Provider>,
145+
)
146+
147+
// Re-render with same version
148+
rerender(
149+
<ExtensionStateContext.Provider value={mockExtensionState}>
150+
<MarketplaceView stateManager={stateManager} />
151+
</ExtensionStateContext.Provider>,
152+
)
153+
154+
// Should not trigger fetch when version hasn't changed
155+
await waitFor(() => {
156+
expect(vscode.postMessage).not.toHaveBeenCalledWith({
157+
type: "fetchMarketplaceData",
158+
})
159+
})
86160
})
87161
})

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ExtensionStateContextType extends ExtensionState {
3535
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
3636
commands: Command[]
3737
organizationAllowList: OrganizationAllowList
38+
organizationSettingsVersion?: number
3839
cloudIsAuthenticated: boolean
3940
sharingEnabled: boolean
4041
maxConcurrentFileReads?: number
@@ -226,6 +227,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
226227
cloudIsAuthenticated: false,
227228
sharingEnabled: false,
228229
organizationAllowList: ORGANIZATION_ALLOW_ALL,
230+
organizationSettingsVersion: undefined,
229231
autoCondenseContext: true,
230232
autoCondenseContextPercent: 100,
231233
profileThresholds: {},
@@ -392,6 +394,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
392394
screenshotQuality: state.screenshotQuality,
393395
routerModels: extensionRouterModels,
394396
cloudIsAuthenticated: state.cloudIsAuthenticated ?? false,
397+
organizationSettingsVersion: state.organizationSettingsVersion,
395398
marketplaceItems,
396399
marketplaceInstalledMetadata,
397400
profileThresholds: state.profileThresholds ?? {},

0 commit comments

Comments
 (0)