mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: sort modes by usage frequency
- Add modeUsageFrequency tracking to global state schema - Track mode usage when switching modes in ClineProvider - Update ModesView to sort modes by usage frequency (descending) - Add modeUsageFrequency to ExtensionState and context - Add comprehensive tests for mode usage tracking and sorting Fixes #5975
This commit is contained in:
parent
de13d8a5cf
commit
cdfd4f9d14
7 changed files with 281 additions and 2 deletions
|
|
@ -131,6 +131,7 @@ export const globalSettingsSchema = z.object({
|
|||
hasOpenedModeSelector: z.boolean().optional(),
|
||||
lastModeExportPath: z.string().optional(),
|
||||
lastModeImportPath: z.string().optional(),
|
||||
modeUsageFrequency: z.record(z.string(), z.number()).optional(),
|
||||
})
|
||||
|
||||
export type GlobalSettings = z.infer<typeof globalSettingsSchema>
|
||||
|
|
|
|||
|
|
@ -811,6 +811,11 @@ export class ClineProvider
|
|||
|
||||
await this.updateGlobalState("mode", newMode)
|
||||
|
||||
// Track mode usage frequency
|
||||
const modeUsageFrequency = this.getGlobalState("modeUsageFrequency") || {}
|
||||
modeUsageFrequency[newMode] = (modeUsageFrequency[newMode] || 0) + 1
|
||||
await this.updateGlobalState("modeUsageFrequency", modeUsageFrequency)
|
||||
|
||||
// Load the saved API config for the new mode if it exists
|
||||
const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
|
||||
const listApiConfig = await this.providerSettingsManager.listConfig()
|
||||
|
|
@ -1440,6 +1445,7 @@ export class ClineProvider
|
|||
alwaysAllowFollowupQuestions,
|
||||
followupAutoApproveTimeoutMs,
|
||||
diagnosticsEnabled,
|
||||
modeUsageFrequency,
|
||||
} = await this.getState()
|
||||
|
||||
const telemetryKey = process.env.POSTHOG_API_KEY
|
||||
|
|
@ -1561,6 +1567,7 @@ export class ClineProvider
|
|||
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
|
||||
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
|
||||
diagnosticsEnabled: diagnosticsEnabled ?? true,
|
||||
modeUsageFrequency: modeUsageFrequency ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1726,6 +1733,7 @@ export class ClineProvider
|
|||
codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
|
||||
},
|
||||
profileThresholds: stateValues.profileThresholds ?? {},
|
||||
modeUsageFrequency: stateValues.modeUsageFrequency ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1651,6 +1651,107 @@ describe("ClineProvider", () => {
|
|||
// Verify state was posted to webview
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
|
||||
})
|
||||
|
||||
test("tracks mode usage frequency when switching modes", async () => {
|
||||
// Mock the contextProxy's getValue to return modeUsageFrequency
|
||||
let modeUsageFrequency: Record<string, number> | undefined = undefined
|
||||
|
||||
vi.spyOn(provider.contextProxy, "getValue").mockImplementation((key: string) => {
|
||||
if (key === "modeUsageFrequency") return modeUsageFrequency
|
||||
return undefined
|
||||
})
|
||||
|
||||
vi.spyOn(provider.contextProxy, "setValue").mockImplementation(async (key: string, value: any) => {
|
||||
if (key === "modeUsageFrequency") {
|
||||
modeUsageFrequency = value
|
||||
}
|
||||
})
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getModeConfigId: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi.fn().mockResolvedValue([]),
|
||||
setModeConfig: vi.fn(),
|
||||
} as any
|
||||
|
||||
// Switch to architect mode
|
||||
await provider.handleModeSwitch("architect")
|
||||
|
||||
// Verify mode usage frequency was tracked
|
||||
expect(modeUsageFrequency).toEqual({
|
||||
architect: 1,
|
||||
})
|
||||
|
||||
// Set existing usage frequency
|
||||
modeUsageFrequency = { architect: 1, code: 3 }
|
||||
|
||||
// Switch to architect mode again
|
||||
await provider.handleModeSwitch("architect")
|
||||
|
||||
// Verify usage count was incremented
|
||||
expect(modeUsageFrequency).toEqual({
|
||||
architect: 2,
|
||||
code: 3,
|
||||
})
|
||||
|
||||
// Switch to a new mode
|
||||
await provider.handleModeSwitch("debug")
|
||||
|
||||
// Verify new mode was added to usage frequency
|
||||
expect(modeUsageFrequency).toEqual({
|
||||
architect: 2,
|
||||
code: 3,
|
||||
debug: 1,
|
||||
})
|
||||
})
|
||||
|
||||
test("includes modeUsageFrequency in state when posting to webview", async () => {
|
||||
// Mock the contextProxy to return modeUsageFrequency
|
||||
let modeUsageFrequency = { code: 5, architect: 3, debug: 1 }
|
||||
|
||||
vi.spyOn(provider.contextProxy, "getValue").mockImplementation((key: string) => {
|
||||
if (key === "modeUsageFrequency") return modeUsageFrequency
|
||||
return undefined
|
||||
})
|
||||
|
||||
vi.spyOn(provider.contextProxy, "setValue").mockImplementation(async (key: string, value: any) => {
|
||||
if (key === "modeUsageFrequency") {
|
||||
modeUsageFrequency = value
|
||||
}
|
||||
})
|
||||
|
||||
// Mock getValues to return all necessary state including modeUsageFrequency
|
||||
vi.spyOn(provider.contextProxy, "getValues").mockReturnValue({
|
||||
modeUsageFrequency,
|
||||
mode: "code",
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [],
|
||||
} as any)
|
||||
;(provider as any).providerSettingsManager = {
|
||||
getModeConfigId: vi.fn().mockResolvedValue(undefined),
|
||||
listConfig: vi.fn().mockResolvedValue([]),
|
||||
setModeConfig: vi.fn(),
|
||||
} as any
|
||||
|
||||
// Clear previous mock calls
|
||||
mockPostMessage.mockClear()
|
||||
|
||||
// Switch mode to trigger state post
|
||||
await provider.handleModeSwitch("code")
|
||||
|
||||
// Find the call that contains the state update
|
||||
const stateCalls = mockPostMessage.mock.calls.filter(
|
||||
(call: any[]) => call[0]?.type === "state" && call[0]?.state?.modeUsageFrequency,
|
||||
)
|
||||
|
||||
expect(stateCalls.length).toBeGreaterThan(0)
|
||||
const lastStateCall = stateCalls[stateCalls.length - 1]
|
||||
|
||||
// Verify the modeUsageFrequency was incremented correctly
|
||||
expect(lastStateCall[0].state.modeUsageFrequency).toEqual({
|
||||
code: 6,
|
||||
architect: 3,
|
||||
debug: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateCustomMode", () => {
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ export type ExtensionState = Pick<
|
|||
| "codebaseIndexConfig"
|
||||
| "codebaseIndexModels"
|
||||
| "profileThresholds"
|
||||
| "modeUsageFrequency"
|
||||
> & {
|
||||
version: string
|
||||
clineMessages: ClineMessage[]
|
||||
|
|
@ -283,6 +284,7 @@ export type ExtensionState = Pick<
|
|||
marketplaceInstalledMetadata?: { project: Record<string, any>; global: Record<string, any> }
|
||||
profileThresholds: Record<string, number>
|
||||
hasOpenedModeSelector: boolean
|
||||
modeUsageFrequency?: Record<string, number>
|
||||
}
|
||||
|
||||
export interface ClineSayTool {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
customInstructions,
|
||||
setCustomInstructions,
|
||||
customModes,
|
||||
modeUsageFrequency,
|
||||
} = useExtensionState()
|
||||
|
||||
// Use a local state to track the visually active mode
|
||||
|
|
@ -83,8 +84,24 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
// 3. Still sending the mode change to the backend for persistence
|
||||
const [visualMode, setVisualMode] = useState(mode)
|
||||
|
||||
// Memoize modes to preserve array order
|
||||
const modes = useMemo(() => getAllModes(customModes), [customModes])
|
||||
// Memoize modes and sort by usage frequency
|
||||
const modes = useMemo(() => {
|
||||
const allModes = getAllModes(customModes)
|
||||
|
||||
// Sort modes by usage frequency (descending)
|
||||
return [...allModes].sort((a, b) => {
|
||||
const freqA = modeUsageFrequency?.[a.slug] || 0
|
||||
const freqB = modeUsageFrequency?.[b.slug] || 0
|
||||
|
||||
// Sort by frequency first (higher frequency first)
|
||||
if (freqB !== freqA) {
|
||||
return freqB - freqA
|
||||
}
|
||||
|
||||
// If frequencies are equal, maintain original order
|
||||
return allModes.indexOf(a) - allModes.indexOf(b)
|
||||
})
|
||||
}, [customModes, modeUsageFrequency])
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [selectedPromptContent, setSelectedPromptContent] = useState("")
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const mockExtensionState = {
|
|||
currentApiConfigName: "",
|
||||
customInstructions: "Initial instructions",
|
||||
setCustomInstructions: vitest.fn(),
|
||||
modeUsageFrequency: {},
|
||||
}
|
||||
|
||||
const renderPromptsView = (props = {}) => {
|
||||
|
|
@ -231,4 +232,152 @@ describe("PromptsView", () => {
|
|||
text: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
describe("Mode sorting by usage frequency", () => {
|
||||
it("sorts modes by usage frequency in descending order", async () => {
|
||||
const modeUsageFrequency = {
|
||||
ask: 10,
|
||||
code: 5,
|
||||
architect: 15,
|
||||
debug: 3,
|
||||
}
|
||||
|
||||
renderPromptsView({ modeUsageFrequency })
|
||||
|
||||
// Open the mode selector
|
||||
const selectTrigger = screen.getByTestId("mode-select-trigger")
|
||||
fireEvent.click(selectTrigger)
|
||||
|
||||
// Wait for the dropdown to open
|
||||
await waitFor(() => {
|
||||
expect(selectTrigger).toHaveAttribute("aria-expanded", "true")
|
||||
})
|
||||
|
||||
// Get all mode options
|
||||
const modeOptions = screen.getAllByTestId(/^mode-option-/)
|
||||
const modeIds = modeOptions.map((option) => option.getAttribute("data-testid")?.replace("mode-option-", ""))
|
||||
|
||||
// Verify the order - architect (15) should be first, ask (10) second, code (5) third, debug (3) fourth
|
||||
expect(modeIds[0]).toBe("architect")
|
||||
expect(modeIds[1]).toBe("ask")
|
||||
expect(modeIds[2]).toBe("code")
|
||||
expect(modeIds[3]).toBe("debug")
|
||||
})
|
||||
|
||||
it("maintains original order for modes with equal usage frequency", async () => {
|
||||
const modeUsageFrequency = {
|
||||
code: 5,
|
||||
architect: 5,
|
||||
ask: 5,
|
||||
}
|
||||
|
||||
renderPromptsView({ modeUsageFrequency })
|
||||
|
||||
// Open the mode selector
|
||||
const selectTrigger = screen.getByTestId("mode-select-trigger")
|
||||
fireEvent.click(selectTrigger)
|
||||
|
||||
// Wait for the dropdown to open
|
||||
await waitFor(() => {
|
||||
expect(selectTrigger).toHaveAttribute("aria-expanded", "true")
|
||||
})
|
||||
|
||||
// Get all mode options
|
||||
const modeOptions = screen.getAllByTestId(/^mode-option-/)
|
||||
const modeIds = modeOptions.map((option) => option.getAttribute("data-testid")?.replace("mode-option-", ""))
|
||||
|
||||
// When frequencies are equal, modes should maintain their original order
|
||||
// The original order is: architect, code, ask, debug, orchestrator (from modes array)
|
||||
expect(modeIds[0]).toBe("architect")
|
||||
expect(modeIds[1]).toBe("code")
|
||||
expect(modeIds[2]).toBe("ask")
|
||||
})
|
||||
|
||||
it("places modes with no usage data at the end", async () => {
|
||||
const modeUsageFrequency = {
|
||||
code: 10,
|
||||
architect: 5,
|
||||
// ask and debug have no usage data
|
||||
}
|
||||
|
||||
renderPromptsView({ modeUsageFrequency })
|
||||
|
||||
// Open the mode selector
|
||||
const selectTrigger = screen.getByTestId("mode-select-trigger")
|
||||
fireEvent.click(selectTrigger)
|
||||
|
||||
// Wait for the dropdown to open
|
||||
await waitFor(() => {
|
||||
expect(selectTrigger).toHaveAttribute("aria-expanded", "true")
|
||||
})
|
||||
|
||||
// Get all mode options
|
||||
const modeOptions = screen.getAllByTestId(/^mode-option-/)
|
||||
const modeIds = modeOptions.map((option) => option.getAttribute("data-testid")?.replace("mode-option-", ""))
|
||||
|
||||
// code (10) should be first, architect (5) second, then modes with no usage
|
||||
expect(modeIds[0]).toBe("code")
|
||||
expect(modeIds[1]).toBe("architect")
|
||||
// ask and debug should be after the modes with usage data
|
||||
expect(modeIds.indexOf("ask")).toBeGreaterThan(1)
|
||||
expect(modeIds.indexOf("debug")).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it("handles empty usage frequency object correctly", async () => {
|
||||
renderPromptsView({ modeUsageFrequency: {} })
|
||||
|
||||
// Open the mode selector
|
||||
const selectTrigger = screen.getByTestId("mode-select-trigger")
|
||||
fireEvent.click(selectTrigger)
|
||||
|
||||
// Wait for the dropdown to open
|
||||
await waitFor(() => {
|
||||
expect(selectTrigger).toHaveAttribute("aria-expanded", "true")
|
||||
})
|
||||
|
||||
// Get all mode options
|
||||
const modeOptions = screen.getAllByTestId(/^mode-option-/)
|
||||
|
||||
// Should show all modes in their original order
|
||||
expect(modeOptions.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("includes custom modes in sorting", async () => {
|
||||
const customMode = {
|
||||
slug: "custom-mode",
|
||||
name: "Custom Mode",
|
||||
roleDefinition: "Custom role",
|
||||
groups: [],
|
||||
}
|
||||
|
||||
const modeUsageFrequency = {
|
||||
"custom-mode": 20,
|
||||
code: 10,
|
||||
architect: 5,
|
||||
}
|
||||
|
||||
renderPromptsView({
|
||||
modeUsageFrequency,
|
||||
customModes: [customMode],
|
||||
})
|
||||
|
||||
// Open the mode selector
|
||||
const selectTrigger = screen.getByTestId("mode-select-trigger")
|
||||
fireEvent.click(selectTrigger)
|
||||
|
||||
// Wait for the dropdown to open
|
||||
await waitFor(() => {
|
||||
expect(selectTrigger).toHaveAttribute("aria-expanded", "true")
|
||||
})
|
||||
|
||||
// Get all mode options
|
||||
const modeOptions = screen.getAllByTestId(/^mode-option-/)
|
||||
const modeIds = modeOptions.map((option) => option.getAttribute("data-testid")?.replace("mode-option-", ""))
|
||||
|
||||
// custom-mode (20) should be first, code (10) second, architect (5) third
|
||||
expect(modeIds[0]).toBe("custom-mode")
|
||||
expect(modeIds[1]).toBe("code")
|
||||
expect(modeIds[2]).toBe("architect")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
},
|
||||
codebaseIndexModels: { ollama: {}, openai: {} },
|
||||
alwaysAllowUpdateTodoList: true,
|
||||
modeUsageFrequency: {},
|
||||
})
|
||||
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue