Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,7 @@ export class ClineProvider
cloudIsAuthenticated,
sharingEnabled,
organizationAllowList,
organizationSettingsVersion,
maxConcurrentFileReads,
condensingApiConfigId,
customCondensingPrompt,
Expand Down Expand Up @@ -1617,6 +1618,7 @@ export class ClineProvider
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
sharingEnabled: sharingEnabled ?? false,
organizationAllowList,
organizationSettingsVersion,
condensingApiConfigId,
customCondensingPrompt,
codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
Expand Down Expand Up @@ -1703,6 +1705,19 @@ export class ClineProvider
)
}

let organizationSettingsVersion: number = -1

try {
if (CloudService.hasInstance()) {
const settings = CloudService.instance.getOrganizationSettings()
organizationSettingsVersion = settings?.version ?? -1
}
} catch (error) {
console.error(
`[getState] failed to get organization settings version: ${error instanceof Error ? error.message : String(error)}`,
)
}

// Return the same structure as before
return {
apiConfiguration: providerSettings,
Expand Down Expand Up @@ -1786,6 +1801,7 @@ export class ClineProvider
cloudIsAuthenticated,
sharingEnabled,
organizationAllowList,
organizationSettingsVersion,
// Explicitly add condensing settings
condensingApiConfigId: stateValues.condensingApiConfigId,
customCondensingPrompt: stateValues.customCondensingPrompt,
Expand Down
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ export type ExtensionState = Pick<
cloudApiUrl?: string
sharingEnabled: boolean
organizationAllowList: OrganizationAllowList
organizationSettingsVersion?: number
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be beneficial to add more explicit typing here to distinguish between undefined (not available) and number (valid version)? This could improve type safety throughout the application.


autoCondenseContext: boolean
autoCondenseContextPercent: number
Expand Down
17 changes: 16 additions & 1 deletion webview-ui/src/components/marketplace/MarketplaceView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from "react"
import { useState, useEffect, useMemo, useContext } from "react"
import { Button } from "@/components/ui/button"
import { Tab, TabContent, TabHeader } from "../common/Tab"
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
Expand All @@ -8,6 +8,7 @@ import { vscode } from "@/utils/vscode"
import { MarketplaceListView } from "./MarketplaceListView"
import { cn } from "@/lib/utils"
import { TooltipProvider } from "@/components/ui/tooltip"
import { ExtensionStateContext } from "@/context/ExtensionStateContext"

interface MarketplaceViewProps {
onDone?: () => void
Expand All @@ -18,6 +19,20 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace
const { t } = useAppTranslation()
const [state, manager] = useStateManager(stateManager)
const [hasReceivedInitialState, setHasReceivedInitialState] = useState(false)
const extensionState = useContext(ExtensionStateContext)
const [lastOrganizationSettingsVersion, setLastOrganizationSettingsVersion] = useState<number>(
extensionState?.organizationSettingsVersion ?? -1,
)

useEffect(() => {
const currentVersion = extensionState?.organizationSettingsVersion ?? -1
if (currentVersion !== lastOrganizationSettingsVersion) {
vscode.postMessage({
type: "fetchMarketplaceData",
})
}
setLastOrganizationSettingsVersion(currentVersion)
}, [extensionState?.organizationSettingsVersion, lastOrganizationSettingsVersion])

// Track when we receive the initial state
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { render, screen } from "@/utils/test-utils"
import userEvent from "@testing-library/user-event"

import { render, waitFor } from "@testing-library/react"
import { vi, describe, it, expect, beforeEach } from "vitest"
import { MarketplaceView } from "../MarketplaceView"
import { MarketplaceViewStateManager } from "../MarketplaceViewStateManager"
import { ExtensionStateContext } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"

vi.mock("@/utils/vscode", () => ({
vscode: {
postMessage: vi.fn(),
getState: vi.fn(() => ({})),
setState: vi.fn(),
},
}))

Expand All @@ -18,70 +17,146 @@ vi.mock("@/i18n/TranslationContext", () => ({
}),
}))

vi.mock("../useStateManager", () => ({
useStateManager: () => [
{
allItems: [],
displayItems: [],
isFetching: false,
activeTab: "mcp",
filters: { type: "", search: "", tags: [] },
},
{
transition: vi.fn(),
onStateChange: vi.fn(() => vi.fn()),
},
],
}))

vi.mock("../MarketplaceListView", () => ({
MarketplaceListView: ({ filterByType }: { filterByType: string }) => (
<div data-testid="marketplace-list-view">MarketplaceListView - {filterByType}</div>
),
}))

// Mock Tab components to avoid ExtensionStateContext dependency
vi.mock("@/components/common/Tab", () => ({
Tab: ({ children, ...props }: any) => <div {...props}>{children}</div>,
TabHeader: ({ children, ...props }: any) => <div {...props}>{children}</div>,
TabContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
TabList: ({ children, ...props }: any) => <div {...props}>{children}</div>,
TabTrigger: ({ children, ...props }: any) => <button {...props}>{children}</button>,
}))

describe("MarketplaceView", () => {
const mockOnDone = vi.fn()
const mockStateManager = new MarketplaceViewStateManager()
let stateManager: MarketplaceViewStateManager
let mockExtensionState: any

beforeEach(() => {
vi.clearAllMocks()
stateManager = new MarketplaceViewStateManager()

// Initialize state manager with some test data
stateManager.transition({
type: "FETCH_COMPLETE",
payload: {
items: [
{
id: "test-mcp",
name: "Test MCP",
type: "mcp" as const,
description: "Test MCP server",
tags: ["test"],
content: "Test content",
url: "https://test.com",
author: "Test Author",
},
],
},
})

mockExtensionState = {
organizationSettingsVersion: 1,
// Add other required properties for the context
didHydrateState: true,
showWelcome: false,
theme: {},
mcpServers: [],
filePaths: [],
openedTabs: [],
commands: [],
organizationAllowList: { allowAll: true, providers: {} },
cloudIsAuthenticated: false,
sharingEnabled: false,
hasOpenedModeSelector: false,
setHasOpenedModeSelector: vi.fn(),
alwaysAllowFollowupQuestions: false,
setAlwaysAllowFollowupQuestions: vi.fn(),
followupAutoApproveTimeoutMs: 60000,
setFollowupAutoApproveTimeoutMs: vi.fn(),
profileThresholds: {},
setProfileThresholds: vi.fn(),
// ... other required context properties
}
})

it("renders without crashing", () => {
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)

expect(screen.getByText("marketplace:title")).toBeInTheDocument()
expect(screen.getByText("marketplace:done")).toBeInTheDocument()
it("should trigger fetchMarketplaceData when organization settings version changes", async () => {
const { rerender } = render(
<ExtensionStateContext.Provider value={mockExtensionState}>
<MarketplaceView stateManager={stateManager} />
</ExtensionStateContext.Provider>,
)

// Initial render should not trigger fetch (version hasn't changed)
expect(vscode.postMessage).not.toHaveBeenCalledWith({
type: "fetchMarketplaceData",
})

// Update the organization settings version
mockExtensionState = {
...mockExtensionState,
organizationSettingsVersion: 2,
}

// Re-render with updated context
rerender(
<ExtensionStateContext.Provider value={mockExtensionState}>
<MarketplaceView stateManager={stateManager} />
</ExtensionStateContext.Provider>,
)

// Wait for the effect to run
await waitFor(() => {
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "fetchMarketplaceData",
})
})
})

it("calls onDone when Done button is clicked", async () => {
const user = userEvent.setup()
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
it("should trigger fetchMarketplaceData when organization settings version changes from -1", async () => {
// Start with -1 version (default)
mockExtensionState = {
...mockExtensionState,
organizationSettingsVersion: -1,
}

await user.click(screen.getByText("marketplace:done"))
expect(mockOnDone).toHaveBeenCalledTimes(1)
})
const { rerender } = render(
<ExtensionStateContext.Provider value={mockExtensionState}>
<MarketplaceView stateManager={stateManager} />
</ExtensionStateContext.Provider>,
)

it("renders tab buttons", () => {
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
// Clear any initial calls
vi.clearAllMocks()

expect(screen.getByText("MCP")).toBeInTheDocument()
expect(screen.getByText("Modes")).toBeInTheDocument()
// Update to a defined version
mockExtensionState = {
...mockExtensionState,
organizationSettingsVersion: 1,
}

rerender(
<ExtensionStateContext.Provider value={mockExtensionState}>
<MarketplaceView stateManager={stateManager} />
</ExtensionStateContext.Provider>,
)

// Should trigger fetch when transitioning from -1 to 1
await waitFor(() => {
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "fetchMarketplaceData",
})
})
})

it("renders MarketplaceListView", () => {
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)

expect(screen.getByTestId("marketplace-list-view")).toBeInTheDocument()
it("should not trigger fetchMarketplaceData when organization settings version remains the same", async () => {
const { rerender } = render(
<ExtensionStateContext.Provider value={mockExtensionState}>
<MarketplaceView stateManager={stateManager} />
</ExtensionStateContext.Provider>,
)

// Re-render with same version
rerender(
<ExtensionStateContext.Provider value={mockExtensionState}>
<MarketplaceView stateManager={stateManager} />
</ExtensionStateContext.Provider>,
)

// Should not trigger fetch when version hasn't changed
await waitFor(() => {
expect(vscode.postMessage).not.toHaveBeenCalledWith({
type: "fetchMarketplaceData",
})
})
})
})
3 changes: 3 additions & 0 deletions webview-ui/src/components/marketplace/useStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export function useStateManager(existingManager?: MarketplaceViewStateManager) {
prevState.isFetching !== newState.isFetching ||
prevState.activeTab !== newState.activeTab ||
JSON.stringify(prevState.allItems) !== JSON.stringify(newState.allItems) ||
JSON.stringify(prevState.organizationMcps) !== JSON.stringify(newState.organizationMcps) ||
JSON.stringify(prevState.displayItems) !== JSON.stringify(newState.displayItems) ||
JSON.stringify(prevState.displayOrganizationMcps) !==
JSON.stringify(newState.displayOrganizationMcps) ||
JSON.stringify(prevState.filters) !== JSON.stringify(newState.filters)

return hasChanged ? newState : prevState
Expand Down
3 changes: 3 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ExtensionStateContextType extends ExtensionState {
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
commands: Command[]
organizationAllowList: OrganizationAllowList
organizationSettingsVersion: number
cloudIsAuthenticated: boolean
sharingEnabled: boolean
maxConcurrentFileReads?: number
Expand Down Expand Up @@ -226,6 +227,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
cloudIsAuthenticated: false,
sharingEnabled: false,
organizationAllowList: ORGANIZATION_ALLOW_ALL,
organizationSettingsVersion: -1,
autoCondenseContext: true,
autoCondenseContextPercent: 100,
profileThresholds: {},
Expand Down Expand Up @@ -392,6 +394,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
screenshotQuality: state.screenshotQuality,
routerModels: extensionRouterModels,
cloudIsAuthenticated: state.cloudIsAuthenticated ?? false,
organizationSettingsVersion: state.organizationSettingsVersion ?? -1,
marketplaceItems,
marketplaceInstalledMetadata,
profileThresholds: state.profileThresholds ?? {},
Expand Down